Merge remote-tracking branch 'origin/main' into paint-context

This commit is contained in:
Nathan Sobo 2023-08-08 18:27:16 -06:00
commit db96fb1307
163 changed files with 9459 additions and 4729 deletions

View file

@ -6,14 +6,23 @@ jobs:
discord_release: discord_release:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Get appropriate URL
id: get-appropriate-url
run: |
if [ "${{ github.event.release.prerelease }}" == "true" ]; then
URL="https://zed.dev/releases/preview/latest"
else
URL="https://zed.dev/releases/stable/latest"
fi
echo "::set-output name=URL::$URL"
- name: Discord Webhook Action - name: Discord Webhook Action
uses: tsickert/discord-webhook@v5.3.0 uses: tsickert/discord-webhook@v5.3.0
if: ${{ ! github.event.release.prerelease }}
with: with:
webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }} webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }}
content: | content: |
📣 Zed ${{ github.event.release.tag_name }} was just released! 📣 Zed ${{ github.event.release.tag_name }} was just released!
Restart your Zed or head to https://zed.dev/releases/stable/latest to grab it. Restart your Zed or head to ${{ steps.get-appropriate-url.outputs.URL }} to grab it.
${{ github.event.release.body }} ${{ github.event.release.body }}

5
.zed/settings.json Normal file
View file

@ -0,0 +1,5 @@
{
"JSON": {
"tab_size": 4
}
}

1860
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -63,7 +63,7 @@ members = [
"crates/theme", "crates/theme",
"crates/theme_selector", "crates/theme_selector",
"crates/util", "crates/util",
"crates/vector_store", "crates/semantic_index",
"crates/vim", "crates/vim",
"crates/vcs_menu", "crates/vcs_menu",
"crates/workspace", "crates/workspace",
@ -79,6 +79,7 @@ resolver = "2"
anyhow = { version = "1.0.57" } anyhow = { version = "1.0.57" }
async-trait = { version = "0.1" } async-trait = { version = "0.1" }
ctor = { version = "0.1" } ctor = { version = "0.1" }
derive_more = { version = "0.99.17" }
env_logger = { version = "0.9" } env_logger = { version = "0.9" }
futures = { version = "0.3" } futures = { version = "0.3" }
globset = { version = "0.4" } globset = { version = "0.4" }
@ -109,10 +110,10 @@ pretty_assertions = "1.3.0"
tree-sitter-bash = { git = "https://github.com/tree-sitter/tree-sitter-bash", rev = "1b0321ee85701d5036c334a6f04761cdc672e64c" } tree-sitter-bash = { git = "https://github.com/tree-sitter/tree-sitter-bash", rev = "1b0321ee85701d5036c334a6f04761cdc672e64c" }
tree-sitter-c = "0.20.1" tree-sitter-c = "0.20.1"
tree-sitter-cpp = "0.20.0" tree-sitter-cpp = { git = "https://github.com/tree-sitter/tree-sitter-cpp", rev="f44509141e7e483323d2ec178f2d2e6c0fc041c1" }
tree-sitter-css = { git = "https://github.com/tree-sitter/tree-sitter-css", rev = "769203d0f9abe1a9a691ac2b9fe4bb4397a73c51" } tree-sitter-css = { git = "https://github.com/tree-sitter/tree-sitter-css", rev = "769203d0f9abe1a9a691ac2b9fe4bb4397a73c51" }
tree-sitter-elixir = { git = "https://github.com/elixir-lang/tree-sitter-elixir", rev = "4ba9dab6e2602960d95b2b625f3386c27e08084e" } tree-sitter-elixir = { git = "https://github.com/elixir-lang/tree-sitter-elixir", rev = "a2861e88a730287a60c11ea9299c033c7d076e30" }
tree-sitter-elm = "5.6.4" tree-sitter-elm = { git = "https://github.com/elm-tooling/tree-sitter-elm", rev = "692c50c0b961364c40299e73c1306aecb5d20f40"}
tree-sitter-embedded-template = "0.20.0" tree-sitter-embedded-template = "0.20.0"
tree-sitter-glsl = { git = "https://github.com/theHamsta/tree-sitter-glsl", rev = "2a56fb7bc8bb03a1892b4741279dd0a8758b7fb3" } tree-sitter-glsl = { git = "https://github.com/theHamsta/tree-sitter-glsl", rev = "2a56fb7bc8bb03a1892b4741279dd0a8758b7fb3" }
tree-sitter-go = { git = "https://github.com/tree-sitter/tree-sitter-go", rev = "aeb2f33b366fd78d5789ff104956ce23508b85db" } tree-sitter-go = { git = "https://github.com/tree-sitter/tree-sitter-go", rev = "aeb2f33b366fd78d5789ff104956ce23508b85db" }
@ -131,9 +132,10 @@ tree-sitter-svelte = { git = "https://github.com/Himujjal/tree-sitter-svelte", r
tree-sitter-racket = { git = "https://github.com/zed-industries/tree-sitter-racket", rev = "eb010cf2c674c6fd9a6316a84e28ef90190fe51a"} tree-sitter-racket = { git = "https://github.com/zed-industries/tree-sitter-racket", rev = "eb010cf2c674c6fd9a6316a84e28ef90190fe51a"}
tree-sitter-yaml = { git = "https://github.com/zed-industries/tree-sitter-yaml", rev = "f545a41f57502e1b5ddf2a6668896c1b0620f930"} tree-sitter-yaml = { git = "https://github.com/zed-industries/tree-sitter-yaml", rev = "f545a41f57502e1b5ddf2a6668896c1b0620f930"}
tree-sitter-lua = "0.0.14" tree-sitter-lua = "0.0.14"
tree-sitter-nix = { git = "https://github.com/nix-community/tree-sitter-nix", rev = "66e3e9ce9180ae08fc57372061006ef83f0abde7" }
[patch.crates-io] [patch.crates-io]
tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "49226023693107fba9a1191136a4f47f38cdca73" } tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "1c65ca24bc9a734ab70115188f465e12eecf224e" }
async-task = { git = "https://github.com/zed-industries/async-task", rev = "341b57d6de98cdfd7b418567b8de2022ca993a6e" } async-task = { git = "https://github.com/zed-industries/async-task", rev = "341b57d6de98cdfd7b418567b8de2022ca993a6e" }
# TODO - Remove when a version is released with this PR: https://github.com/servo/core-foundation-rs/pull/457 # TODO - Remove when a version is released with this PR: https://github.com/servo/core-foundation-rs/pull/457

View file

@ -1,6 +1,6 @@
# syntax = docker/dockerfile:1.2 # syntax = docker/dockerfile:1.2
FROM rust:1.70-bullseye as builder FROM rust:1.71-bullseye as builder
WORKDIR app WORKDIR app
COPY . . COPY . .

View file

@ -0,0 +1,27 @@
<svg width="14" height="16" viewBox="0 0 14 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.375 8.74577V10.375C7.30597 10.375 6.69403 10.375 4.625 10.375V10.1226L9.375 5.87742V5.625H4.625V7.27717" stroke="black" stroke-width="1.25"/>
<circle cx="0.5" cy="8" r="0.5" fill="black" fill-opacity="0.3"/>
<circle cx="1.49976" cy="5.82825" r="0.5" fill="black" fill-opacity="0.6"/>
<circle cx="1.49976" cy="10.1719" r="0.5" fill="black" fill-opacity="0.6"/>
<circle cx="13.5" cy="8.01581" r="0.5" fill="black" fill-opacity="0.3"/>
<circle cx="12.5" cy="5.84387" r="0.5" fill="black" fill-opacity="0.6"/>
<circle cx="12.5" cy="10.1877" r="0.5" fill="black" fill-opacity="0.6"/>
<circle cx="6.99213" cy="1.48438" r="0.5" fill="black" fill-opacity="0.3"/>
<circle cx="4.50391" cy="2.48438" r="0.5" fill="black" fill-opacity="0.6"/>
<circle cx="2.49976" cy="3.48438" r="0.5" fill="black" fill-opacity="0.3"/>
<circle cx="2.49976" cy="12.5" r="0.5" fill="black" fill-opacity="0.3"/>
<circle cx="0.5" cy="12.016" r="0.5" fill="black" fill-opacity="0.3"/>
<circle cx="0.5" cy="3.98438" r="0.5" fill="black" fill-opacity="0.3"/>
<circle cx="13.5" cy="12.016" r="0.5" fill="black" fill-opacity="0.3"/>
<circle cx="13.5" cy="3.98438" r="0.5" fill="black" fill-opacity="0.3"/>
<circle cx="2.49976" cy="14.516" r="0.5" fill="black" fill-opacity="0.3"/>
<circle cx="2.48413" cy="1.48438" r="0.5" fill="black" fill-opacity="0.3"/>
<circle cx="11.5" cy="14.516" r="0.5" fill="black" fill-opacity="0.3"/>
<circle cx="11.5" cy="1.48438" r="0.5" fill="black" fill-opacity="0.3"/>
<circle cx="11.5" cy="3.48438" r="0.5" fill="black" fill-opacity="0.3"/>
<circle cx="11.5" cy="12.516" r="0.5" fill="black" fill-opacity="0.3"/>
<circle cx="9.49609" cy="2.48438" r="0.5" fill="black" fill-opacity="0.6"/>
<circle cx="6.99213" cy="14.5" r="0.5" fill="black" fill-opacity="0.3"/>
<circle cx="4.50391" cy="13.516" r="0.5" fill="black" fill-opacity="0.6"/>
<circle cx="9.49609" cy="13.5" r="0.5" fill="black" fill-opacity="0.6"/>
</svg>

After

Width:  |  Height:  |  Size: 2 KiB

View file

@ -1,5 +1,5 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6 7.63H8" stroke="black" stroke-opacity="0.66" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/> <path d="M6 7.63H8" stroke="black" stroke-opacity="0.6" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<rect x="2" y="2" width="10" height="3" rx="0.5" fill="black" fill-opacity="0.33" stroke="black" stroke-width="1.25"/> <rect x="2" y="2" width="10" height="3" rx="0.5" fill="black" fill-opacity="0.3" stroke="black" stroke-width="1.25"/>
<path d="M2.59375 5H11.4375L10.5581 11.5664C10.5248 11.8146 10.313 12 10.0625 12H3.93944C3.68812 12 3.47585 11.8134 3.44358 11.5642L2.59375 5Z" stroke="black" stroke-width="1.25"/> <path d="M2.59375 5H11.4375L10.5581 11.5664C10.5248 11.8146 10.313 12 10.0625 12H3.93944C3.68812 12 3.47585 11.8134 3.44358 11.5642L2.59375 5Z" stroke="black" stroke-width="1.25"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 529 B

After

Width:  |  Height:  |  Size: 527 B

Before After
Before After

View file

@ -1,6 +1,6 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7 11C5.46973 11 4.1268 11.1873 3.31522 11.3327C2.94367 11.3992 2.60079 11.0563 2.66733 10.6848C2.81266 9.8732 3 8.53027 3 7C3 5.8387 2.89211 4.78529 2.77656 3.99011C2.73589 3.71017 3.19546 3.51715 3.36119 3.7464C4.09612 4.76304 5.23301 6.23301 6.5 7.5C7.76699 8.76699 9.23696 9.90388 10.2536 10.6388C10.4828 10.8045 10.2898 11.2641 10.0099 11.2234C9.21472 11.1079 8.1613 11 7 11Z" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/> <path d="M7 11C5.46973 11 4.1268 11.1873 3.31522 11.3327C2.94367 11.3992 2.60079 11.0563 2.66733 10.6848C2.81266 9.8732 3 8.53027 3 7C3 5.8387 2.89211 4.78529 2.77656 3.99011C2.73589 3.71017 3.19546 3.51715 3.36119 3.7464C4.09612 4.76304 5.23301 6.23301 6.5 7.5C7.76699 8.76699 9.23696 9.90388 10.2536 10.6388C10.4828 10.8045 10.2898 11.2641 10.0099 11.2234C9.21472 11.1079 8.1613 11 7 11Z" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12.3594 3.35938C12.3594 3.35938 12.0146 2.9209 11.5312 2.4375C11.0479 1.9541 10.6406 1.64062 10.6406 1.64062" stroke="black" stroke-opacity="0.33" stroke-width="1.25" stroke-linecap="round"/> <path d="M12.365 3.8478L10.3381 1.82088" stroke="black" stroke-opacity="0.3" stroke-width="1.25" stroke-linecap="round"/>
<path d="M11.3516 7.36803C11.3516 7.36803 10.7962 5.88996 9.48438 4.57812C8.17254 3.26629 6.64062 2.64155 6.64062 2.64155" stroke="black" stroke-opacity="0.66" stroke-width="1.25" stroke-linecap="round"/> <path d="M11.3516 7.36803L6.64062 2.64155" stroke="black" stroke-opacity="0.6" stroke-width="1.25" stroke-linecap="round"/>
<rect x="2.72266" y="8.73828" width="3.58525" height="2.72899" rx="0.5" transform="rotate(45 2.72266 8.73828)" fill="black"/> <rect x="2.72266" y="8.73828" width="3.58525" height="2.72899" rx="0.5" transform="rotate(45 2.72266 8.73828)" fill="black"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 950 B

Before After
Before After

View file

@ -1,4 +1,4 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 10.5C12 10.7761 11.7761 11 11.5 11H2.5C2.22386 11 2 10.7761 2 10.5V4.88C2 4.60386 2.22386 4.38 2.5 4.38H4.4342C4.61518 4.38 4.78204 4.2822 4.87046 4.12428L5.35681 3.25572C5.44524 3.0978 5.61209 3 5.79308 3H8.20692C8.38791 3 8.55476 3.0978 8.64319 3.25572L9.12954 4.12428C9.21796 4.2822 9.38482 4.38 9.5658 4.38H11.5C11.7761 4.38 12 4.60386 12 4.88V10.5Z" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M12 10.5C12 10.7761 11.7761 11 11.5 11H2.5C2.22386 11 2 10.7761 2 10.5V4.88C2 4.60386 2.22386 4.38 2.5 4.38H4.4342C4.61518 4.38 4.78204 4.2822 4.87046 4.12428L5.35681 3.25572C5.44524 3.0978 5.61209 3 5.79308 3H8.20692C8.38791 3 8.55476 3.0978 8.64319 3.25572L9.12954 4.12428C9.21796 4.2822 9.38482 4.38 9.5658 4.38H11.5C11.7761 4.38 12 4.60386 12 4.88V10.5Z" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7.005 9C7.90246 9 8.63 8.27246 8.63 7.375C8.63 6.47754 7.90246 5.75 7.005 5.75C6.10754 5.75 5.38 6.47754 5.38 7.375C5.38 8.27246 6.10754 9 7.005 9Z" fill="black" fill-opacity="0.33" stroke="black" stroke-linecap="round" stroke-linejoin="round"/> <path d="M7.005 9C7.90246 9 8.63 8.27246 8.63 7.375C8.63 6.47754 7.90246 5.75 7.005 5.75C6.10754 5.75 5.38 6.47754 5.38 7.375C5.38 8.27246 6.10754 9 7.005 9Z" fill="black" fill-opacity="0.3" stroke="black" stroke-linecap="round" stroke-linejoin="round"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 851 B

After

Width:  |  Height:  |  Size: 850 B

Before After
Before After

View file

@ -0,0 +1,4 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.46115 8.43419C7.30678 8.43419 8.92229 7.43411 8.92229 5.21171C8.92229 2.98933 7.30678 1.98926 5.46115 1.98926C3.61553 1.98926 2 2.98933 2 5.21171C2 6.028 2.21794 6.67935 2.58519 7.17685C2.7184 7.35732 2.69033 7.77795 2.58387 7.97539C2.32908 8.44793 2.81048 8.9657 3.33372 8.84571C3.72539 8.75597 4.13621 8.63447 4.49574 8.4715C4.62736 8.41181 4.7727 8.38777 4.91631 8.40402C5.09471 8.42416 5.27678 8.43419 5.46115 8.43419Z" fill="black" fill-opacity="0.33" stroke="black" stroke-width="0.990499" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M11.6055 5.87971C11.4329 5.66762 11.1208 5.6357 10.9088 5.80842C10.6967 5.98114 10.6648 6.29308 10.8375 6.50518L11.6055 5.87971ZM6.4361 10.4149C6.21522 10.2536 5.90539 10.3018 5.74404 10.5226C5.58268 10.7435 5.6309 11.0533 5.85177 11.2147L6.4361 10.4149ZM12.3808 8.25929C12.3808 7.28754 12.1013 6.48847 11.6055 5.87971L10.8375 6.50518C11.1712 6.91492 11.3903 7.48485 11.3903 8.25929H12.3808ZM11.6988 10.5186C12.137 9.92499 12.3808 9.16705 12.3808 8.25929H11.3903C11.3903 8.98414 11.1982 9.52892 10.9019 9.93034L11.6988 10.5186ZM9.1854 11.9702C9.58603 12.1518 10.0316 12.2822 10.4412 12.3761L10.6625 11.4106C10.2888 11.3249 9.91276 11.2124 9.59435 11.068L9.1854 11.9702ZM8.42443 11.977C8.62663 11.977 8.8273 11.9661 9.02494 11.9437L8.91361 10.9595C8.75447 10.9775 8.59097 10.9865 8.42443 10.9865V11.977ZM5.85177 11.2147C6.5749 11.743 7.49105 11.977 8.42443 11.977V10.9865C7.64656 10.9865 6.9503 10.7906 6.4361 10.4149L5.85177 11.2147ZM9.59435 11.068C9.38377 10.9726 9.14869 10.9329 8.91361 10.9595L9.02494 11.9437C9.07704 11.9378 9.13271 11.9463 9.1854 11.9702L9.59435 11.068ZM10.8658 11.2581C10.8784 11.2813 10.8772 11.2932 10.8762 11.2995C10.8746 11.3097 10.8681 11.3291 10.8481 11.3517C10.8049 11.4004 10.7343 11.4271 10.6625 11.4106L10.4412 12.3761C10.8927 12.4796 11.3244 12.3073 11.5891 12.0089C11.8602 11.7033 11.9778 11.2332 11.7377 10.7879L10.8658 11.2581ZM10.9019 9.93034C10.7358 10.1554 10.7116 10.4435 10.7161 10.6293C10.7209 10.8293 10.7634 11.0682 10.8658 11.2581L11.7377 10.7879C11.739 10.7905 11.7304 10.7736 11.7214 10.7331C11.713 10.6954 11.7074 10.6506 11.7063 10.6054C11.7052 10.5594 11.709 10.5234 11.7139 10.5006C11.7196 10.4738 11.7217 10.4876 11.6988 10.5186L10.9019 9.93034Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View file

@ -1,5 +1,5 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<ellipse cx="7" cy="4" rx="5" ry="2" fill="black" fill-opacity="0.33" stroke="black" stroke-width="1.25"/> <ellipse cx="7" cy="4" rx="5" ry="2" fill="black" fill-opacity="0.3" stroke="black" stroke-width="1.25"/>
<path d="M12 4V10C12 11.1046 9.76142 12 7 12C4.23858 12 2 11.1046 2 10V4" stroke="black" stroke-width="1.25"/> <path d="M12 4V10C12 11.1046 9.76142 12 7 12C4.23858 12 2 11.1046 2 10V4" stroke="black" stroke-width="1.25"/>
<path d="M12 7C12 8.10457 9.76142 9 7 9C4.23858 9 2 8.10457 2 7" stroke="black" stroke-width="1.25"/> <path d="M12 7C12 8.10457 9.76142 9 7 9C4.23858 9 2 8.10457 2 7" stroke="black" stroke-width="1.25"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 423 B

After

Width:  |  Height:  |  Size: 422 B

Before After
Before After

View file

@ -1,5 +1,5 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2 4H10" stroke="black" stroke-opacity="0.66" stroke-width="1.25" stroke-linecap="round"/> <path d="M2 4H10" stroke="black" stroke-opacity="0.6" stroke-width="1.25" stroke-linecap="round"/>
<path d="M2 7H12" stroke="black" stroke-width="1.25" stroke-linecap="round"/> <path d="M2 7H12" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
<path d="M2 10H8" stroke="black" stroke-opacity="0.33" stroke-width="1.25" stroke-linecap="round"/> <path d="M2 10H8" stroke="black" stroke-opacity="0.3" stroke-width="1.25" stroke-linecap="round"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 381 B

After

Width:  |  Height:  |  Size: 379 B

Before After
Before After

View file

@ -1,20 +1,35 @@
{ {
"suffixes": { "suffixes": {
"aac": "audio", "aac": "audio",
"accdb": "storage",
"bak": "backup",
"bash": "terminal", "bash": "terminal",
"bash_aliases": "terminal",
"bash_logout": "terminal",
"bash_profile": "terminal",
"bashrc": "terminal",
"bmp": "image", "bmp": "image",
"c": "code", "c": "code",
"cc": "code",
"conf": "settings", "conf": "settings",
"cpp": "code", "cpp": "code",
"cc": "code",
"css": "code", "css": "code",
"csv": "storage",
"dat": "storage",
"db": "storage",
"dbf": "storage",
"dll": "storage",
"doc": "document", "doc": "document",
"docx": "document", "docx": "document",
"eslintrc": "eslint", "eslintrc": "eslint",
"eslintrc.js": "eslint", "eslintrc.js": "eslint",
"eslintrc.json": "eslint", "eslintrc.json": "eslint",
"fmp": "storage",
"fp7": "storage",
"flac": "audio", "flac": "audio",
"fish": "terminal", "fish": "terminal",
"frm": "storage",
"gdb": "storage",
"gitattributes": "vcs", "gitattributes": "vcs",
"gitignore": "vcs", "gitignore": "vcs",
"gitmodules": "vcs", "gitmodules": "vcs",
@ -25,8 +40,7 @@
"hbs": "template", "hbs": "template",
"htm": "template", "htm": "template",
"html": "template", "html": "template",
"svelte": "template", "ib": "storage",
"hpp": "code",
"ico": "image", "ico": "image",
"ini": "settings", "ini": "settings",
"java": "code", "java": "code",
@ -34,23 +48,30 @@
"jpg": "image", "jpg": "image",
"js": "code", "js": "code",
"json": "storage", "json": "storage",
"ldf": "storage",
"lock": "lock", "lock": "lock",
"log": "log", "log": "log",
"mdb": "storage",
"md": "document", "md": "document",
"mdf": "storage",
"mdx": "document", "mdx": "document",
"mp3": "audio", "mp3": "audio",
"mp4": "video", "mp4": "video",
"myd": "storage",
"myi": "storage",
"ods": "document", "ods": "document",
"odp": "document", "odp": "document",
"odt": "document", "odt": "document",
"ogg": "video", "ogg": "video",
"pdb": "storage",
"pdf": "document", "pdf": "document",
"php": "code", "php": "code",
"png": "image", "png": "image",
"ppt": "document", "ppt": "document",
"pptx": "document", "pptx": "document",
"prettierrc": "prettier",
"prettierignore": "prettier", "prettierignore": "prettier",
"prettierrc": "prettier",
"profile": "terminal",
"ps1": "terminal", "ps1": "terminal",
"psd": "image", "psd": "image",
"py": "code", "py": "code",
@ -58,26 +79,19 @@
"rkt": "code", "rkt": "code",
"rs": "rust", "rs": "rust",
"rtf": "document", "rtf": "document",
"sav": "storage",
"scm": "code", "scm": "code",
"sh": "terminal", "sh": "terminal",
"bashrc": "terminal", "sqlite": "storage",
"bash_profile": "terminal", "sdf": "storage",
"bash_aliases": "terminal", "svelte": "template",
"bash_logout": "terminal",
"profile": "terminal",
"zshrc": "terminal",
"zshenv": "terminal",
"zsh_profile": "terminal",
"zsh_aliases": "terminal",
"zsh_histfile": "terminal",
"zlogin": "terminal",
"sql": "code",
"svg": "image", "svg": "image",
"swift": "code", "swift": "code",
"tiff": "image",
"toml": "toml",
"ts": "typescript", "ts": "typescript",
"tsx": "code", "tsx": "code",
"tiff": "image",
"toml": "toml",
"tsv": "storage",
"txt": "document", "txt": "document",
"wav": "audio", "wav": "audio",
"webm": "video", "webm": "video",
@ -86,7 +100,13 @@
"xml": "template", "xml": "template",
"yaml": "settings", "yaml": "settings",
"yml": "settings", "yml": "settings",
"zsh": "terminal" "zlogin": "terminal",
"zsh": "terminal",
"zsh_aliases": "terminal",
"zshenv": "terminal",
"zsh_histfile": "terminal",
"zsh_profile": "terminal",
"zshrc": "terminal"
}, },
"types": { "types": {
"audio": { "audio": {

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 687 B

After

Width:  |  Height:  |  Size: 43 KiB

Before After
Before After

View file

@ -1,5 +1,4 @@
<svg width="15" height="14" viewBox="0 0 15 14" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="15" height="14" viewBox="0 0 15 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7 2.53125H2.21875V10.625L4.5 4.59375H7.96875L7 2.53125Z" fill="black"/> <path d="M3.49165 6.13802C3.4991 5.86198 3.72386 5.64062 4 5.64062H13C13.2761 5.64062 13.4991 5.86198 13.4916 6.13802C13.4529 7.57407 13.2341 11.625 12 11.625H2C3.23412 11.625 3.45287 7.57407 3.49165 6.13802Z" fill="black" stroke="black" stroke-width="1.25" stroke-linejoin="round"/>
<path d="M4.47293 4.94363C4.54554 4.74743 4.73263 4.61719 4.94184 4.61719H12.8755C13.2237 4.61719 13.4653 4.9642 13.3445 5.29074L11.1208 11.2986C11.0482 11.4948 10.8611 11.625 10.6519 11.625H2.71821C2.37002 11.625 2.12844 11.278 2.2493 10.9514L4.47293 4.94363Z" fill="black" fill-opacity="0.33" stroke="black" stroke-width="1.25"/> <path d="M4.00781 11.625H2.42841C2.18186 11.625 1.97212 11.4453 1.93432 11.2017L0.651964 2.93603C0.604944 2.63296 0.839355 2.35938 1.14605 2.35938H4.6164C4.95332 2.35938 5.26759 2.52904 5.45244 2.81072L5.8125 3.35938H8.89008C9.37767 3.35938 9.79418 3.71103 9.87593 4.19171L10.125 5.65625" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8 4.4024L7.27505 2.93264C7.10664 2.59119 6.75894 2.375 6.37821 2.375H2.5C2.22386 2.375 2 2.59886 2 2.875V11.125C2 11.4011 2.22386 11.625 2.5 11.625H4.00781" stroke="black" stroke-width="1.25" stroke-linejoin="round"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 745 B

After

Width:  |  Height:  |  Size: 760 B

Before After
Before After

View file

@ -1,6 +1,6 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="4" cy="10" r="2" stroke="black" stroke-width="1.25"/> <circle cx="4" cy="10" r="2" stroke="black" stroke-width="1.25"/>
<circle cx="10" cy="4" r="2" fill="black" fill-opacity="0.33" stroke="black" stroke-width="1.25"/> <circle cx="10" cy="4" r="2" fill="black" fill-opacity="0.3" stroke="black" stroke-width="1.25"/>
<line x1="3.625" y1="2.625" x2="3.625" y2="7.375" stroke="black" stroke-width="1.25" stroke-linecap="round"/> <line x1="3.625" y1="2.625" x2="3.625" y2="7.375" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
<path d="M10 6V6C10 8.20914 8.20914 10 6 10V10" stroke="black" stroke-width="1.25"/> <path d="M10 6V6C10 8.20914 8.20914 10 6 10V10" stroke="black" stroke-width="1.25"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 463 B

After

Width:  |  Height:  |  Size: 462 B

Before After
Before After

View file

@ -1,6 +1,6 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.5 3C6.91421 3 7.25 2.66421 7.25 2.25C7.25 1.83579 6.91421 1.5 6.5 1.5C6.08579 1.5 5.75 1.83579 5.75 2.25C5.75 2.66421 6.08579 3 6.5 3Z" fill="black" stroke="black" stroke-linecap="round" stroke-linejoin="round"/> <path d="M6.5 3C6.91421 3 7.25 2.66421 7.25 2.25C7.25 1.83579 6.91421 1.5 6.5 1.5C6.08579 1.5 5.75 1.83579 5.75 2.25C5.75 2.66421 6.08579 3 6.5 3Z" fill="black" stroke="black" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6 8L9 5L12 8H6Z" fill="black" fill-opacity="0.33"/> <path d="M6 8L9 5L12 8H6Z" fill="black" fill-opacity="0.3"/>
<path d="M2 10L5 7L7.375 9.375" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/> <path d="M2 10L5 7L7.375 9.375" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6 8L7.5 6.5L9 5L10.5 6.5L12 8" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/> <path d="M6 8L7.5 6.5L9 5L10.5 6.5L12 8" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3.375 2H2.5C2.22386 2 2 2.22386 2 2.5V11.5C2 11.7761 2.22386 12 2.5 12H7.35938M9.64062 2H11.5C11.7761 2 12 2.22386 12 2.5V11.5C12 11.7761 11.7761 12 11.5 12H10.125" stroke="black" stroke-width="1.25" stroke-linecap="round"/> <path d="M3.375 2H2.5C2.22386 2 2 2.22386 2 2.5V11.5C2 11.7761 2.22386 12 2.5 12H7.35938M9.64062 2H11.5C11.7761 2 12 2.22386 12 2.5V11.5C12 11.7761 11.7761 12 11.5 12H10.125" stroke="black" stroke-width="1.25" stroke-linecap="round"/>

Before

Width:  |  Height:  |  Size: 866 B

After

Width:  |  Height:  |  Size: 865 B

Before After
Before After

View file

@ -1,6 +1,6 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="3" y="5" width="8" height="7" rx="0.5" stroke="black" stroke-width="1.25"/> <rect x="3" y="5" width="8" height="7" rx="0.5" stroke="black" stroke-width="1.25"/>
<path d="M4 4C4 2.89543 4.89543 2 6 2H8C9.10457 2 10 2.89543 10 4V5H4V4Z" stroke="black" stroke-opacity="0.66" stroke-width="1.25"/> <path d="M4 4C4 2.89543 4.89543 2 6 2H8C9.10457 2 10 2.89543 10 4V5H4V4Z" stroke="black" stroke-opacity="0.6" stroke-width="1.25"/>
<circle cx="7" cy="8" r="1" fill="black"/> <circle cx="7" cy="8" r="1" fill="black"/>
<path d="M7 8V9.375" stroke="black" stroke-width="1.25" stroke-linecap="round"/> <path d="M7 8V9.375" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 445 B

After

Width:  |  Height:  |  Size: 444 B

Before After
Before After

View file

@ -0,0 +1,3 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 12L9.41379 9.41379M2 6.31034C2 3.92981 3.92981 2 6.31034 2C8.6909 2 10.6207 3.92981 10.6207 6.31034C10.6207 8.6909 8.6909 10.6207 6.31034 10.6207C3.92981 10.6207 2 8.6909 2 6.31034Z" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 383 B

View file

@ -1,8 +1,8 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.03125 2.96875C2.03125 2.41647 2.47897 1.96875 3.03125 1.96875H5V12H3.03125C2.47897 12 2.03125 11.5523 2.03125 11V2.96875Z" fill="black" fill-opacity="0.33"/> <path d="M2.03125 2.96875C2.03125 2.41647 2.47897 1.96875 3.03125 1.96875H5V12H3.03125C2.47897 12 2.03125 11.5523 2.03125 11V2.96875Z" fill="black" fill-opacity="0.3"/>
<rect x="2" y="2" width="10" height="10" rx="0.5" stroke="black" stroke-width="1.25"/> <rect x="2" y="2" width="10" height="10" rx="0.5" stroke="black" stroke-width="1.25"/>
<path d="M9.5 5L7.5 5" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/> <path d="M9.5 5L7.5 5" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9.5 7H7.5" stroke="black" stroke-opacity="0.66" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/> <path d="M9.5 7H7.5" stroke="black" stroke-opacity="0.6" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9.5 9H7.5" stroke="black" stroke-opacity="0.33" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/> <path d="M9.5 9H7.5" stroke="black" stroke-opacity="0.3" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5 2V13" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/> <path d="M5 2V13" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 823 B

After

Width:  |  Height:  |  Size: 820 B

Before After
Before After

View file

@ -1,4 +1,4 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1.62677 3.88472L6.99983 6.78517M1.62677 3.88472L1.63137 9.90006L7.00442 12.8005M1.62677 3.88472L4.31117 2.54211M6.99983 6.78517L7.00442 12.8005M6.99983 6.78517L9.68414 5.33084M7.00442 12.8005L12.373 9.89186L12.3684 3.87652M4.31117 2.54211L6.99556 1.1995L12.3684 3.87652M4.31117 2.54211L9.68414 5.33084M12.3684 3.87652L9.68414 5.33084" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/> <path d="M1.62677 3.88472L6.99983 6.78517M1.62677 3.88472L1.63137 9.90006L7.00442 12.8005M1.62677 3.88472L4.31117 2.54211M6.99983 6.78517L7.00442 12.8005M6.99983 6.78517L9.68414 5.33084M7.00442 12.8005L12.373 9.89186L12.3684 3.87652M4.31117 2.54211L6.99556 1.1995L12.3684 3.87652M4.31117 2.54211L9.68414 5.33084M12.3684 3.87652L9.68414 5.33084" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7.03125 12.5625V6.78125L1.5625 3.9375V9.75L7.03125 12.5625Z" fill="black" fill-opacity="0.33"/> <path d="M7.03125 12.5625V6.78125L1.5625 3.9375V9.75L7.03125 12.5625Z" fill="black" fill-opacity="0.3"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 638 B

After

Width:  |  Height:  |  Size: 637 B

Before After
Before After

View file

@ -0,0 +1,3 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7 3V11M11 7H3" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 188 B

View file

@ -1,12 +1,12 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2 2.86328H8.51563" stroke="black" stroke-width="1.25" stroke-linecap="round"/> <path d="M2 2.86328H8.51563" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
<path d="M11 2.86328L12 2.86328" stroke="black" stroke-opacity="0.33" stroke-width="1.25" stroke-linecap="round"/> <path d="M11 2.86328L12 2.86328" stroke="black" stroke-opacity="0.3" stroke-width="1.25" stroke-linecap="round"/>
<path d="M9.64062 5.6263L12 5.6263" stroke="black" stroke-opacity="0.66" stroke-width="1.25" stroke-linecap="round"/> <path d="M9.64062 5.6263L12 5.6263" stroke="black" stroke-opacity="0.6" stroke-width="1.25" stroke-linecap="round"/>
<path d="M4.79688 5.6263L7.15625 5.6263" stroke="black" stroke-width="1.25" stroke-linecap="round"/> <path d="M4.79688 5.6263L7.15625 5.6263" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
<path d="M2 5.6263L2.35937 5.6263" stroke="black" stroke-opacity="0.33" stroke-width="1.25" stroke-linecap="round"/> <path d="M2 5.6263L2.35937 5.6263" stroke="black" stroke-opacity="0.3" stroke-width="1.25" stroke-linecap="round"/>
<path d="M7.15625 8.3737L12 8.3737" stroke="black" stroke-width="1.25" stroke-linecap="round"/> <path d="M7.15625 8.3737L12 8.3737" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
<path d="M2 8.3737L4.64062 8.3737" stroke="black" stroke-opacity="0.66" stroke-width="1.25" stroke-linecap="round"/> <path d="M2 8.3737L4.64062 8.3737" stroke="black" stroke-opacity="0.6" stroke-width="1.25" stroke-linecap="round"/>
<path d="M2 11.1094H3.54687" stroke="black" stroke-width="1.25" stroke-linecap="round"/> <path d="M2 11.1094H3.54687" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
<path d="M5.97656 11.1094H8.35938" stroke="black" stroke-width="1.25" stroke-linecap="round"/> <path d="M5.97656 11.1094H8.35938" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
<path d="M10.8203 11.1094L12 11.1094" stroke="black" stroke-opacity="0.33" stroke-width="1.25" stroke-linecap="round"/> <path d="M10.8203 11.1094L12 11.1094" stroke="black" stroke-opacity="0.3" stroke-width="1.25" stroke-linecap="round"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Before After
Before After

View file

@ -0,0 +1,5 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.03125 2V2.03125M2.03125 8C2.03125 10 5 10 5 10M2.03125 8V2.03125M2.03125 8L2.03125 11M2.03125 2.03125C2.03125 4 5 4 5 4" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<rect x="7.375" y="2.375" width="4.25" height="3.25" rx="1.125" fill="black" fill-opacity="0.33" stroke="black" stroke-width="1.25"/>
<rect x="7.375" y="8.375" width="4.25" height="3.25" rx="1.125" fill="black" fill-opacity="0.33" stroke="black" stroke-width="1.25"/>
</svg>

After

Width:  |  Height:  |  Size: 588 B

View file

@ -0,0 +1,11 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7 12C4.97279 12 3.22735 10.7936 2.4425 9.0595M7 2C9.11228 2 10.9186 3.30981 11.6512 5.16152" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<circle cx="1.65625" cy="1.67188" r="0.625" fill="black" fill-opacity="0.5"/>
<circle cx="3.71094" cy="1.67188" r="0.625" fill="black" fill-opacity="0.5"/>
<circle cx="4.96094" cy="3.36719" r="0.625" fill="black" fill-opacity="0.5"/>
<circle cx="3.71094" cy="4.79688" r="0.625" fill="black" fill-opacity="0.5"/>
<circle cx="4.60156" cy="6.67188" r="0.625" fill="black" fill-opacity="0.5"/>
<circle cx="1.65625" cy="4.17188" r="0.625" fill="black" fill-opacity="0.5"/>
<circle cx="1.65625" cy="6.67188" r="0.625" fill="black" fill-opacity="0.5"/>
<path d="M10.7802 10.8195C10.838 10.8195 10.8906 10.8527 10.9155 10.9048L11.7174 12.5811C11.8088 12.7721 12.0017 12.8938 12.2135 12.8938H12.3394C12.7483 12.8938 13.0142 12.4635 12.8314 12.0978L12.1619 10.7589C12.1232 10.6816 12.1582 10.5823 12.241 10.5349C12.7565 10.2397 13.0695 9.66858 13.0695 9.00391C13.0695 8.43361 12.8777 7.97006 12.5248 7.64951C12.1725 7.3295 11.6652 7.15703 11.043 7.15703H9.49609C9.19234 7.15703 8.94609 7.40327 8.94609 7.70703V12.3438C8.94609 12.6475 9.19234 12.8938 9.49609 12.8938H9.60156C9.90532 12.8938 10.1516 12.6475 10.1516 12.3438V10.9695C10.1516 10.8867 10.2187 10.8195 10.3016 10.8195H10.7802ZM10.1516 8.31328C10.1516 8.23044 10.2187 8.16328 10.3016 8.16328H10.8984C11.2023 8.16328 11.4371 8.2449 11.5954 8.38814C11.7529 8.5308 11.8406 8.73993 11.8406 9.00781C11.8406 9.28155 11.751 9.49461 11.5909 9.63971C11.4302 9.7854 11.1925 9.86797 10.8867 9.86797H10.3016C10.2187 9.86797 10.1516 9.80081 10.1516 9.71797V8.31328Z" fill="black" stroke="black" stroke-width="0.1"/>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View file

@ -0,0 +1,5 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.10517 5.8012C4.07193 5.73172 4.00176 5.6875 3.92475 5.6875H3.44609C3.33564 5.6875 3.24609 5.77704 3.24609 5.8875V7.26172C3.24609 7.53786 3.02224 7.76172 2.74609 7.76172H2.64062C2.36448 7.76172 2.14062 7.53786 2.14062 7.26172V2.625C2.14062 2.34886 2.36448 2.125 2.64062 2.125H4.1875C5.41406 2.125 6.16406 2.80469 6.16406 3.92188C6.16406 4.57081 5.85885 5.12418 5.36073 5.40943C5.25888 5.46775 5.20921 5.59421 5.2617 5.69918L5.93117 7.03811C6.09739 7.37056 5.85564 7.76172 5.48395 7.76172H5.35806C5.16552 7.76172 4.99009 7.65117 4.907 7.47748L4.10517 5.8012ZM3.44609 3.03125C3.33564 3.03125 3.24609 3.12079 3.24609 3.23125V4.63594C3.24609 4.74639 3.33564 4.83594 3.44609 4.83594H4.03125C4.66016 4.83594 5.03516 4.49609 5.03516 3.92578C5.03516 3.36719 4.66797 3.03125 4.04297 3.03125H3.44609Z" fill="black" fill-opacity="0.5"/>
<path d="M3.92475 5.7375C3.98251 5.7375 4.03514 5.77067 4.06006 5.82277L4.8619 7.49905C4.95329 7.69011 5.14627 7.81172 5.35806 7.81172H5.48395C5.89281 7.81172 6.15873 7.38145 5.97589 7.01575L5.30642 5.67682C5.26778 5.59953 5.30269 5.50028 5.38557 5.45282C5.90107 5.15762 6.21406 4.58655 6.21406 3.92188C6.21406 3.35158 6.02226 2.88803 5.66936 2.56748C5.31705 2.24747 4.80973 2.075 4.1875 2.075H2.64062C2.33687 2.075 2.09062 2.32124 2.09062 2.625V7.26172C2.09062 7.56548 2.33687 7.81172 2.64062 7.81172H2.74609C3.04985 7.81172 3.29609 7.56548 3.29609 7.26172V5.8875C3.29609 5.80466 3.36325 5.7375 3.44609 5.7375H3.92475ZM3.29609 3.23125C3.29609 3.14841 3.36325 3.08125 3.44609 3.08125H4.04297C4.34688 3.08125 4.58164 3.16287 4.73988 3.30611C4.89748 3.44876 4.98516 3.6579 4.98516 3.92578C4.98516 4.19952 4.89553 4.41258 4.73546 4.55768C4.57475 4.70337 4.33706 4.78594 4.03125 4.78594H3.44609C3.36325 4.78594 3.29609 4.71878 3.29609 4.63594V3.23125Z" stroke="black" stroke-opacity="0.5" stroke-width="0.1"/>
<path d="M9.32812 6.65625V9.32812M9.32812 12V9.32812M12 9.32812H9.32812M6.65625 9.32812H9.32812M9.32812 9.32812L11.1094 7.54688M9.32812 9.32812L7.54688 11.1094M9.32812 9.32812L11.1094 11.1094M9.32812 9.32812L7.54688 7.54688" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View file

@ -0,0 +1,5 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.96454 5.6762C3.93131 5.60672 3.86114 5.5625 3.78412 5.5625H3.30547C3.19501 5.5625 3.10547 5.65204 3.10547 5.7625V7.13672C3.10547 7.41286 2.88161 7.63672 2.60547 7.63672H2.5C2.22386 7.63672 2 7.41286 2 7.13672V2.5C2 2.22386 2.22386 2 2.5 2H4.04688C5.27344 2 6.02344 2.67969 6.02344 3.79688C6.02344 4.44581 5.71823 4.99918 5.2201 5.28443C5.11826 5.34275 5.06859 5.46921 5.12107 5.57418L5.79054 6.91311C5.95677 7.24556 5.71502 7.63672 5.34333 7.63672H5.21743C5.02489 7.63672 4.84946 7.52617 4.76638 7.35248L3.96454 5.6762ZM3.30547 2.90625C3.19501 2.90625 3.10547 2.99579 3.10547 3.10625V4.51094C3.10547 4.62139 3.19501 4.71094 3.30547 4.71094H3.89062C4.51953 4.71094 4.89453 4.37109 4.89453 3.80078C4.89453 3.24219 4.52734 2.90625 3.90234 2.90625H3.30547Z" fill="black" fill-opacity="0.5"/>
<path d="M3.78412 5.6125C3.84188 5.6125 3.89451 5.64567 3.91944 5.69777L4.72127 7.37405C4.81266 7.56511 5.00564 7.68672 5.21743 7.68672H5.34333C5.75219 7.68672 6.01811 7.25645 5.83526 6.89075L5.1658 5.55182C5.12715 5.47453 5.16207 5.37528 5.24495 5.32782C5.76044 5.03262 6.07344 4.46155 6.07344 3.79688C6.07344 3.22658 5.88164 2.76303 5.52873 2.44248C5.17642 2.12247 4.6691 1.95 4.04688 1.95H2.5C2.19624 1.95 1.95 2.19624 1.95 2.5V7.13672C1.95 7.44048 2.19624 7.68672 2.5 7.68672H2.60547C2.90923 7.68672 3.15547 7.44048 3.15547 7.13672V5.7625C3.15547 5.67966 3.22263 5.6125 3.30547 5.6125H3.78412ZM3.15547 3.10625C3.15547 3.02341 3.22263 2.95625 3.30547 2.95625H3.90234C4.20626 2.95625 4.44101 3.03787 4.59926 3.18111C4.75686 3.32376 4.84453 3.5329 4.84453 3.80078C4.84453 4.07452 4.75491 4.28758 4.59484 4.43268C4.43413 4.57837 4.19643 4.66094 3.89062 4.66094H3.30547C3.22263 4.66094 3.15547 4.59378 3.15547 4.51094V3.10625Z" stroke="black" stroke-opacity="0.5" stroke-width="0.1"/>
<path d="M7.5 5.88672C9.433 5.88672 11 7.45372 11 9.38672V12M11 12L13 10M11 12L9 10" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 2 KiB

View file

@ -1,4 +1,4 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.27935 9.98207C4.32063 9.4038 3.9204 8.89049 3.35998 8.80276L2.60081 8.68387C2.37979 8.64945 2.20167 8.48001 2.15225 8.25614L2.01378 7.63511C1.96382 7.41235 2.05233 7.1807 2.23696 7.05125L2.8631 6.61242C3.33337 6.28297 3.47456 5.6369 3.18621 5.13364L2.79467 4.45092C2.68118 4.25261 2.69801 4.00374 2.83757 3.82321L3.22314 3.32436C3.3627 3.14438 3.59621 3.06994 3.81071 3.13772L4.57531 3.37769C5.11944 3.54879 5.70048 3.26159 5.90683 2.71886L6.1811 1.99782C6.26255 1.78395 6.46345 1.64285 6.68772 1.6423L7.31007 1.64063C7.53434 1.64007 7.73579 1.78006 7.81834 1.99337L8.09965 2.72275C8.30821 3.26214 8.88655 3.54712 9.42903 3.37714L10.1632 3.14716C10.3772 3.07994 10.6096 3.15382 10.7492 3.3327L11.1374 3.83099" stroke="black" stroke-opacity="0.66" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/> <path d="M4.27935 9.98207C4.32063 9.4038 3.9204 8.89049 3.35998 8.80276L2.60081 8.68387C2.37979 8.64945 2.20167 8.48001 2.15225 8.25614L2.01378 7.63511C1.96382 7.41235 2.05233 7.1807 2.23696 7.05125L2.8631 6.61242C3.33337 6.28297 3.47456 5.6369 3.18621 5.13364L2.79467 4.45092C2.68118 4.25261 2.69801 4.00374 2.83757 3.82321L3.22314 3.32436C3.3627 3.14438 3.59621 3.06994 3.81071 3.13772L4.57531 3.37769C5.11944 3.54879 5.70048 3.26159 5.90683 2.71886L6.1811 1.99782C6.26255 1.78395 6.46345 1.64285 6.68772 1.6423L7.31007 1.64063C7.53434 1.64007 7.73579 1.78006 7.81834 1.99337L8.09965 2.72275C8.30821 3.26214 8.88655 3.54712 9.42903 3.37714L10.1632 3.14716C10.3772 3.07994 10.6096 3.15382 10.7492 3.3327L11.1374 3.83099" stroke="black" stroke-opacity="0.6" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.76988 10.5933C7.76988 10.6595 7.8236 10.7133 7.88988 10.7133H7.97588C8.32602 10.7133 8.60988 10.9971 8.60988 11.3472V11.3472C8.60988 11.6974 8.32602 11.9812 7.97588 11.9812H6.05587C5.70573 11.9812 5.42188 11.6974 5.42188 11.3472V11.3472C5.42188 10.9971 5.70573 10.7133 6.05587 10.7133H6.14188C6.20815 10.7133 6.26188 10.6595 6.26188 10.5933V6.66925C6.26188 6.60298 6.20815 6.54925 6.14188 6.54925H6.05588C5.70573 6.54925 5.42188 6.2654 5.42188 5.91525V5.91525C5.42188 5.5651 5.70573 5.28125 6.05588 5.28125H8.89988C10.0518 5.28125 11.8619 5.71487 11.8619 7.15185C11.8619 7.67078 11.7284 8.10362 11.4642 8.45348C11.1981 8.79765 10.8458 9.05637 10.4056 9.22931V9.22931C10.3782 9.24007 10.3673 9.27304 10.3829 9.29801L11.2163 10.6342C11.247 10.6834 11.3008 10.7133 11.3588 10.7133H11.7319C12.082 10.7133 12.3659 10.9971 12.3659 11.3472V11.3472C12.3659 11.6974 12.082 11.9812 11.7319 11.9812H10.5637C10.4955 11.9812 10.432 11.9465 10.3952 11.889L8.96523 9.65406C8.92847 9.59661 8.86496 9.56185 8.79676 9.56185H7.96988C7.85942 9.56185 7.76988 9.65139 7.76988 9.76185V10.5933ZM8.61188 6.54925C9.02963 6.54925 10.125 6.54925 10.2339 7.18785C10.2975 7.56123 10.1181 7.86557 9.88118 8.07715C9.64227 8.29046 9.20527 8.38985 8.58788 8.38985H7.86988C7.81465 8.38985 7.76988 8.34508 7.76988 8.28985V6.64925C7.76988 6.59402 7.81465 6.54925 7.86988 6.54925H8.61188Z" fill="black"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M7.76988 10.5933C7.76988 10.6595 7.8236 10.7133 7.88988 10.7133H7.97588C8.32602 10.7133 8.60988 10.9971 8.60988 11.3472C8.60988 11.6974 8.32602 11.9812 7.97588 11.9812H6.05587C5.70573 11.9812 5.42188 11.6974 5.42188 11.3472C5.42188 10.9971 5.70573 10.7133 6.05587 10.7133H6.14188C6.20815 10.7133 6.26188 10.6595 6.26188 10.5933V6.66925C6.26188 6.60298 6.20815 6.54925 6.14188 6.54925H6.05588C5.70573 6.54925 5.42188 6.2654 5.42188 5.91525C5.42188 5.5651 5.70573 5.28125 6.05588 5.28125H8.89988C10.0518 5.28125 11.8619 5.71487 11.8619 7.15185C11.8619 7.67078 11.7284 8.10362 11.4642 8.45348C11.1981 8.79765 10.8458 9.05637 10.4056 9.22931C10.3782 9.24007 10.3673 9.27304 10.3829 9.29801L11.2163 10.6342C11.247 10.6834 11.3008 10.7133 11.3588 10.7133H11.7319C12.082 10.7133 12.3659 10.9971 12.3659 11.3472C12.3659 11.6974 12.082 11.9812 11.7319 11.9812H10.5637C10.4955 11.9812 10.432 11.9465 10.3952 11.889L8.96523 9.65406C8.92847 9.59661 8.86496 9.56185 8.79676 9.56185H7.96988C7.85942 9.56185 7.76988 9.65139 7.76988 9.76185V10.5933ZM8.61188 6.54925C9.02963 6.54925 10.125 6.54925 10.2339 7.18785C10.2975 7.56123 10.1181 7.86557 9.88118 8.07715C9.64227 8.29046 9.20527 8.38985 8.58788 8.38985H7.86988C7.81465 8.38985 7.76988 8.34508 7.76988 8.28985V6.64925C7.76988 6.59402 7.81465 6.54925 7.86988 6.54925H8.61188Z" fill="black"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Before After
Before After

View file

@ -1,4 +1,4 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.60081 8.94324L3.35998 9.06214C3.9204 9.14986 4.32063 9.66317 4.27935 10.2414L4.22342 11.0252C4.20713 11.2536 4.32877 11.4686 4.53024 11.568L5.09174 11.8446C5.29321 11.9441 5.53379 11.9068 5.69834 11.7519L6.26255 11.2186C6.67855 10.8253 7.32041 10.8253 7.7369 11.2186L8.3011 11.7519C8.46565 11.9074 8.70572 11.9441 8.90772 11.8446L9.47027 11.5674C9.67124 11.4686 9.79234 11.2541 9.77607 11.0264L9.72007 10.2414C9.67883 9.66317 10.079 9.14986 10.6394 9.06214L11.3986 8.94324C11.6197 8.90883 11.7978 8.73938 11.8477 8.51607L11.9862 7.89504C12.0362 7.67172 11.9477 7.44007 11.763 7.31117L11.1293 6.86731C10.6617 6.53959 10.5189 5.89966 10.8013 5.3969L11.1841 4.71586C11.2954 4.51754 11.277 4.26923 11.1374 4.09036L10.7492 3.59207C10.6096 3.41319 10.3772 3.33932 10.1632 3.40653L9.42903 3.63651C8.88655 3.80649 8.30821 3.52152 8.09965 2.98213L7.81834 2.25275C7.73579 2.03944 7.53434 1.89945 7.31007 1.9L6.68772 1.90167C6.46345 1.90222 6.26255 2.04333 6.1811 2.25719L5.90683 2.97824C5.70048 3.52097 5.11944 3.80816 4.57531 3.63706L3.81071 3.39709C3.59621 3.32932 3.3627 3.40375 3.22314 3.58374L2.83757 4.08258C2.69801 4.26312 2.68118 4.51199 2.79467 4.7103L3.18621 5.39302C3.47456 5.89628 3.33337 6.54235 2.8631 6.87179L2.23696 7.31062C2.05233 7.44007 1.96382 7.67173 2.01378 7.89448L2.15225 8.51552C2.20167 8.73938 2.37979 8.90883 2.60081 8.94324Z" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/> <path d="M2.60081 8.94324L3.35998 9.06214C3.9204 9.14986 4.32063 9.66317 4.27935 10.2414L4.22342 11.0252C4.20713 11.2536 4.32877 11.4686 4.53024 11.568L5.09174 11.8446C5.29321 11.9441 5.53379 11.9068 5.69834 11.7519L6.26255 11.2186C6.67855 10.8253 7.32041 10.8253 7.7369 11.2186L8.3011 11.7519C8.46565 11.9074 8.70572 11.9441 8.90772 11.8446L9.47027 11.5674C9.67124 11.4686 9.79234 11.2541 9.77607 11.0264L9.72007 10.2414C9.67883 9.66317 10.079 9.14986 10.6394 9.06214L11.3986 8.94324C11.6197 8.90883 11.7978 8.73938 11.8477 8.51607L11.9862 7.89504C12.0362 7.67172 11.9477 7.44007 11.763 7.31117L11.1293 6.86731C10.6617 6.53959 10.5189 5.89966 10.8013 5.3969L11.1841 4.71586C11.2954 4.51754 11.277 4.26923 11.1374 4.09036L10.7492 3.59207C10.6096 3.41319 10.3772 3.33932 10.1632 3.40653L9.42903 3.63651C8.88655 3.80649 8.30821 3.52152 8.09965 2.98213L7.81834 2.25275C7.73579 2.03944 7.53434 1.89945 7.31007 1.9L6.68772 1.90167C6.46345 1.90222 6.26255 2.04333 6.1811 2.25719L5.90683 2.97824C5.70048 3.52097 5.11944 3.80816 4.57531 3.63706L3.81071 3.39709C3.59621 3.32932 3.3627 3.40375 3.22314 3.58374L2.83757 4.08258C2.69801 4.26312 2.68118 4.51199 2.79467 4.7103L3.18621 5.39302C3.47456 5.89628 3.33337 6.54235 2.8631 6.87179L2.23696 7.31062C2.05233 7.44007 1.96382 7.67173 2.01378 7.89448L2.15225 8.51552C2.20167 8.73938 2.37979 8.90883 2.60081 8.94324Z" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8.14913 5.85093L8.14909 5.85089C7.51453 5.21637 6.48549 5.21637 5.85092 5.85089L5.85089 5.85092C5.21637 6.48549 5.21637 7.51453 5.85089 8.14909L5.85093 8.14913C6.48549 8.78362 7.51452 8.78362 8.14908 8.14913L8.14913 8.14908C8.78362 7.51452 8.78362 6.48549 8.14913 5.85093Z" fill="black" fill-opacity="0.33" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/> <path d="M8.14913 5.85093L8.14909 5.85089C7.51453 5.21637 6.48549 5.21637 5.85092 5.85089L5.85089 5.85092C5.21637 6.48549 5.21637 7.51453 5.85089 8.14909L5.85093 8.14913C6.48549 8.78362 7.51452 8.78362 8.14908 8.14913L8.14913 8.14908C8.78362 7.51452 8.78362 6.48549 8.14913 5.85093Z" fill="black" fill-opacity="0.3" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Before After
Before After

View file

@ -1,5 +1,5 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5 5H9" stroke="black" stroke-width="1.25" stroke-linecap="round"/> <path d="M5 5H9" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
<path d="M7 5L7 10" stroke="black" stroke-width="1.25" stroke-linecap="round"/> <path d="M7 5L7 10" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
<path d="M4 2H2.5C2.22386 2 2 2.22386 2 2.5V11.5C2 11.7761 2.22386 12 2.5 12H4M10 2H11.5C11.7761 2 12 2.22386 12 2.5V11.5C12 11.7761 11.7761 12 11.5 12H10" stroke="black" stroke-opacity="0.66" stroke-width="1.25" stroke-linecap="round"/> <path d="M4 2H2.5C2.22386 2 2 2.22386 2 2.5V11.5C2 11.7761 2.22386 12 2.5 12H4M10 2H11.5C11.7761 2 12 2.22386 12 2.5V11.5C12 11.7761 11.7761 12 11.5 12H10" stroke="black" stroke-opacity="0.6" stroke-width="1.25" stroke-linecap="round"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 498 B

After

Width:  |  Height:  |  Size: 497 B

Before After
Before After

View file

@ -1,5 +1,5 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 4.375V2.5C12 2.22386 11.7761 2 11.5 2H2.5C2.22386 2 2 2.22386 2 2.5V11.5C2 11.7761 2.22386 12 2.5 12H3.375" stroke="black" stroke-opacity="0.66" stroke-width="1.25" stroke-linecap="round"/> <path d="M12 4.375V2.5C12 2.22386 11.7761 2 11.5 2H2.5C2.22386 2 2 2.22386 2 2.5V11.5C2 11.7761 2.22386 12 2.5 12H3.375" stroke="black" stroke-opacity="0.6" stroke-width="1.25" stroke-linecap="round"/>
<path d="M10.6836 7.82805C10.7933 7.65392 10.9823 7.57377 11.174 7.57377C11.2904 7.57377 11.4019 7.59384 11.5092 7.62792C11.8324 7.73069 12.2148 7.63925 12.3392 7.32368L12.3773 7.22707C12.4703 6.99131 12.3823 6.71761 12.1522 6.61154C11.8328 6.46436 11.4984 6.375 11.1262 6.375C9.87708 6.375 8.91935 7.60671 9.4239 8.84869C9.54205 9.13951 9.74219 9.36166 9.9515 9.54337C10.1061 9.6776 10.2858 9.80516 10.4475 9.92002C10.4972 9.95529 10.5452 9.98936 10.5903 10.0221C11.0283 10.34 11.2526 10.5876 11.2526 10.9466C11.2526 11.1518 11.1622 11.3133 11.016 11.4128C10.8777 11.5071 10.7055 11.5357 10.5454 11.5222C10.3931 11.5093 10.2529 11.4717 10.1214 11.4196C9.81633 11.2989 9.45533 11.4015 9.33641 11.7073L9.2814 11.8487C9.19162 12.0796 9.2749 12.3463 9.49799 12.4539C10.0894 12.7391 10.7377 12.8279 11.3915 12.5872C12.0569 12.3423 12.595 11.7708 12.595 10.9068C12.595 10.1301 12.1336 9.69583 11.6966 9.36109C11.606 9.29163 11.5259 9.23292 11.4493 9.17682C11.3259 9.08638 11.1964 8.99109 11.0734 8.88536C10.8937 8.73082 10.7518 8.57274 10.6595 8.38613C10.5746 8.21464 10.5815 7.99013 10.6836 7.82805Z" fill="black"/> <path d="M10.6836 7.82805C10.7933 7.65392 10.9823 7.57377 11.174 7.57377C11.2904 7.57377 11.4019 7.59384 11.5092 7.62792C11.8324 7.73069 12.2148 7.63925 12.3392 7.32368L12.3773 7.22707C12.4703 6.99131 12.3823 6.71761 12.1522 6.61154C11.8328 6.46436 11.4984 6.375 11.1262 6.375C9.87708 6.375 8.91935 7.60671 9.4239 8.84869C9.54205 9.13951 9.74219 9.36166 9.9515 9.54337C10.1061 9.6776 10.2858 9.80516 10.4475 9.92002C10.4972 9.95529 10.5452 9.98936 10.5903 10.0221C11.0283 10.34 11.2526 10.5876 11.2526 10.9466C11.2526 11.1518 11.1622 11.3133 11.016 11.4128C10.8777 11.5071 10.7055 11.5357 10.5454 11.5222C10.3931 11.5093 10.2529 11.4717 10.1214 11.4196C9.81633 11.2989 9.45533 11.4015 9.33641 11.7073L9.2814 11.8487C9.19162 12.0796 9.2749 12.3463 9.49799 12.4539C10.0894 12.7391 10.7377 12.8279 11.3915 12.5872C12.0569 12.3423 12.595 11.7708 12.595 10.9068C12.595 10.1301 12.1336 9.69583 11.6966 9.36109C11.606 9.29163 11.5259 9.23292 11.4493 9.17682C11.3259 9.08638 11.1964 8.99109 11.0734 8.88536C10.8937 8.73082 10.7518 8.57274 10.6595 8.38613C10.5746 8.21464 10.5815 7.99013 10.6836 7.82805Z" fill="black"/>
<path d="M6.98644 7.70936H7.69396C7.98162 7.70936 8.21481 7.47617 8.21481 7.18851V7.02346C8.21481 6.73581 7.98162 6.50261 7.69396 6.50261H4.96848C4.68082 6.50261 4.44763 6.73581 4.44763 7.02346V7.18851C4.44763 7.47617 4.68082 7.70936 4.96848 7.70936H5.676V12.102C5.676 12.3896 5.90919 12.6228 6.19685 12.6228H6.46559C6.75325 12.6228 6.98644 12.3896 6.98644 12.102V7.70936Z" fill="black"/> <path d="M6.98644 7.70936H7.69396C7.98162 7.70936 8.21481 7.47617 8.21481 7.18851V7.02346C8.21481 6.73581 7.98162 6.50261 7.69396 6.50261H4.96848C4.68082 6.50261 4.44763 6.73581 4.44763 7.02346V7.18851C4.44763 7.47617 4.68082 7.70936 4.96848 7.70936H5.676V12.102C5.676 12.3896 5.90919 12.6228 6.19685 12.6228H6.46559C6.75325 12.6228 6.98644 12.3896 6.98644 12.102V7.70936Z" fill="black"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Before After
Before After

View file

@ -1,4 +1,4 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1.65625 2H11.8437C12.1199 2 12.3438 2.22386 12.3438 2.5V9.34375M12.3438 12H2.15625C1.88011 12 1.65625 11.7761 1.65625 11.5V4.65625" stroke="black" stroke-width="1.25" stroke-linecap="round"/> <path d="M1.65625 2H11.8437C12.1199 2 12.3438 2.22386 12.3438 2.5V9.34375M12.3438 12H2.15625C1.88011 12 1.65625 11.7761 1.65625 11.5V4.65625" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
<path d="M9 7.01562L5.65624 9.3125L5.65624 4.6875L9 7.01562Z" fill="black" fill-opacity="0.33" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/> <path d="M9 7.01562L5.65624 9.3125L5.65624 4.6875L9 7.01562Z" fill="black" fill-opacity="0.3" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 484 B

After

Width:  |  Height:  |  Size: 483 B

Before After
Before After

View file

@ -22,6 +22,7 @@
"alt-cmd-right": "pane::ActivateNextItem", "alt-cmd-right": "pane::ActivateNextItem",
"cmd-w": "pane::CloseActiveItem", "cmd-w": "pane::CloseActiveItem",
"alt-cmd-t": "pane::CloseInactiveItems", "alt-cmd-t": "pane::CloseInactiveItems",
"ctrl-alt-cmd-w": "workspace::CloseInactiveTabsAndPanes",
"cmd-k u": "pane::CloseCleanItems", "cmd-k u": "pane::CloseCleanItems",
"cmd-k cmd-w": "pane::CloseAllItems", "cmd-k cmd-w": "pane::CloseAllItems",
"cmd-shift-w": "workspace::CloseWindow", "cmd-shift-w": "workspace::CloseWindow",
@ -226,12 +227,26 @@
"alt-enter": "search::SelectAllMatches" "alt-enter": "search::SelectAllMatches"
} }
}, },
{
"context": "BufferSearchBar > Editor",
"bindings": {
"up": "search::PreviousHistoryQuery",
"down": "search::NextHistoryQuery"
}
},
{ {
"context": "ProjectSearchBar", "context": "ProjectSearchBar",
"bindings": { "bindings": {
"escape": "project_search::ToggleFocus" "escape": "project_search::ToggleFocus"
} }
}, },
{
"context": "ProjectSearchBar > Editor",
"bindings": {
"up": "search::PreviousHistoryQuery",
"down": "search::NextHistoryQuery"
}
},
{ {
"context": "ProjectSearchView", "context": "ProjectSearchView",
"bindings": { "bindings": {
@ -411,7 +426,6 @@
"cmd-k cmd-t": "theme_selector::Toggle", "cmd-k cmd-t": "theme_selector::Toggle",
"cmd-k cmd-s": "zed::OpenKeymap", "cmd-k cmd-s": "zed::OpenKeymap",
"cmd-t": "project_symbols::Toggle", "cmd-t": "project_symbols::Toggle",
"cmd-ctrl-t": "semantic_search::Toggle",
"cmd-p": "file_finder::Toggle", "cmd-p": "file_finder::Toggle",
"cmd-shift-p": "command_palette::Toggle", "cmd-shift-p": "command_palette::Toggle",
"cmd-shift-m": "diagnostics::Deploy", "cmd-shift-m": "diagnostics::Deploy",

View file

@ -324,8 +324,8 @@
// the terminal will default to matching the buffer's font family. // the terminal will default to matching the buffer's font family.
// "font_family": "Zed Mono" // "font_family": "Zed Mono"
}, },
// Difference settings for vector_store // Difference settings for semantic_index
"vector_store": { "semantic_index": {
"enabled": false, "enabled": false,
"reindexing_delay_seconds": 600 "reindexing_delay_seconds": 600
}, },

View file

@ -1637,6 +1637,7 @@ impl ConversationEditor {
let mut editor = Editor::for_buffer(conversation.read(cx).buffer.clone(), None, cx); let mut editor = Editor::for_buffer(conversation.read(cx).buffer.clone(), None, cx);
editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx); editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
editor.set_show_gutter(false, cx); editor.set_show_gutter(false, cx);
editor.set_show_wrap_guides(false, cx);
editor editor
}); });

View file

@ -12,10 +12,7 @@ use client::{
use collections::{HashMap, HashSet}; use collections::{HashMap, HashSet};
use fs::FakeFs; use fs::FakeFs;
use futures::{channel::oneshot, StreamExt as _}; use futures::{channel::oneshot, StreamExt as _};
use gpui::{ use gpui::{executor::Deterministic, ModelHandle, TestAppContext, WindowHandle};
elements::*, executor::Deterministic, AnyElement, Entity, ModelHandle, TestAppContext, View,
ViewContext, ViewHandle, WeakViewHandle,
};
use language::LanguageRegistry; use language::LanguageRegistry;
use parking_lot::Mutex; use parking_lot::Mutex;
use project::{Project, WorktreeId}; use project::{Project, WorktreeId};
@ -466,42 +463,8 @@ impl TestClient {
&self, &self,
project: &ModelHandle<Project>, project: &ModelHandle<Project>,
cx: &mut TestAppContext, cx: &mut TestAppContext,
) -> ViewHandle<Workspace> { ) -> WindowHandle<Workspace> {
struct WorkspaceContainer { cx.add_window(|cx| Workspace::test_new(project.clone(), cx))
workspace: Option<WeakViewHandle<Workspace>>,
}
impl Entity for WorkspaceContainer {
type Event = ();
}
impl View for WorkspaceContainer {
fn ui_name() -> &'static str {
"WorkspaceContainer"
}
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
if let Some(workspace) = self
.workspace
.as_ref()
.and_then(|workspace| workspace.upgrade(cx))
{
ChildView::new(&workspace, cx).into_any()
} else {
Empty::new().into_any()
}
}
}
// We use a workspace container so that we don't need to remove the window in order to
// drop the workspace and we can use a ViewHandle instead.
let (window_id, container) = cx.add_window(|_| WorkspaceContainer { workspace: None });
let workspace = cx.add_view(window_id, |cx| Workspace::test_new(project.clone(), cx));
container.update(cx, |container, cx| {
container.workspace = Some(workspace.downgrade());
cx.notify();
});
workspace
} }
} }

View file

@ -7,8 +7,7 @@ use client::{User, RECEIVE_TIMEOUT};
use collections::HashSet; use collections::HashSet;
use editor::{ use editor::{
test::editor_test_context::EditorTestContext, ConfirmCodeAction, ConfirmCompletion, test::editor_test_context::EditorTestContext, ConfirmCodeAction, ConfirmCompletion,
ConfirmRename, Editor, ExcerptRange, MultiBuffer, Redo, Rename, ToOffset, ToggleCodeActions, ConfirmRename, Editor, ExcerptRange, MultiBuffer, Redo, Rename, ToggleCodeActions, Undo,
Undo,
}; };
use fs::{repository::GitFileStatus, FakeFs, Fs as _, LineEnding, RemoveOptions}; use fs::{repository::GitFileStatus, FakeFs, Fs as _, LineEnding, RemoveOptions};
use futures::StreamExt as _; use futures::StreamExt as _;
@ -1208,7 +1207,7 @@ async fn test_share_project(
cx_c: &mut TestAppContext, cx_c: &mut TestAppContext,
) { ) {
deterministic.forbid_parking(); deterministic.forbid_parking();
let (window_b, _) = cx_b.add_window(|_| EmptyView); let window_b = cx_b.add_window(|_| EmptyView);
let mut server = TestServer::start(&deterministic).await; let mut server = TestServer::start(&deterministic).await;
let client_a = server.create_client(cx_a, "user_a").await; let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await; let client_b = server.create_client(cx_b, "user_b").await;
@ -1316,7 +1315,7 @@ async fn test_share_project(
.await .await
.unwrap(); .unwrap();
let editor_b = cx_b.add_view(window_b, |cx| Editor::for_buffer(buffer_b, None, cx)); let editor_b = window_b.add_view(cx_b, |cx| Editor::for_buffer(buffer_b, None, cx));
// Client A sees client B's selection // Client A sees client B's selection
deterministic.run_until_parked(); deterministic.run_until_parked();
@ -1499,8 +1498,8 @@ async fn test_host_disconnect(
deterministic.run_until_parked(); deterministic.run_until_parked();
assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared())); assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared()));
let (window_id_b, workspace_b) = let window_b = cx_b.add_window(|cx| Workspace::test_new(project_b.clone(), cx));
cx_b.add_window(|cx| Workspace::test_new(project_b.clone(), cx)); let workspace_b = window_b.root(cx_b);
let editor_b = workspace_b let editor_b = workspace_b
.update(cx_b, |workspace, cx| { .update(cx_b, |workspace, cx| {
workspace.open_path((worktree_id, "b.txt"), None, true, cx) workspace.open_path((worktree_id, "b.txt"), None, true, cx)
@ -1509,11 +1508,9 @@ async fn test_host_disconnect(
.unwrap() .unwrap()
.downcast::<Editor>() .downcast::<Editor>()
.unwrap(); .unwrap();
assert!(cx_b assert!(window_b.read_with(cx_b, |cx| editor_b.is_focused(cx)));
.read_window(window_id_b, |cx| editor_b.is_focused(cx))
.unwrap());
editor_b.update(cx_b, |editor, cx| editor.insert("X", cx)); editor_b.update(cx_b, |editor, cx| editor.insert("X", cx));
assert!(cx_b.is_window_edited(workspace_b.window_id())); assert!(window_b.is_edited(cx_b));
// Drop client A's connection. Collaborators should disappear and the project should not be shown as shared. // Drop client A's connection. Collaborators should disappear and the project should not be shown as shared.
server.forbid_connections(); server.forbid_connections();
@ -1525,10 +1522,10 @@ async fn test_host_disconnect(
assert!(worktree_a.read_with(cx_a, |tree, _| !tree.as_local().unwrap().is_shared())); assert!(worktree_a.read_with(cx_a, |tree, _| !tree.as_local().unwrap().is_shared()));
// Ensure client B's edited state is reset and that the whole window is blurred. // Ensure client B's edited state is reset and that the whole window is blurred.
cx_b.read_window(window_id_b, |cx| { window_b.read_with(cx_b, |cx| {
assert_eq!(cx.focused_view_id(), None); assert_eq!(cx.focused_view_id(), None);
}); });
assert!(!cx_b.is_window_edited(workspace_b.window_id())); assert!(!window_b.is_edited(cx_b));
// Ensure client B is not prompted to save edits when closing window after disconnecting. // Ensure client B is not prompted to save edits when closing window after disconnecting.
let can_close = workspace_b let can_close = workspace_b
@ -3445,13 +3442,11 @@ async fn test_newline_above_or_below_does_not_move_guest_cursor(
.update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx)) .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
.await .await
.unwrap(); .unwrap();
let (window_a, _) = cx_a.add_window(|_| EmptyView); let window_a = cx_a.add_window(|_| EmptyView);
let editor_a = cx_a.add_view(window_a, |cx| { let editor_a = window_a.add_view(cx_a, |cx| Editor::for_buffer(buffer_a, Some(project_a), cx));
Editor::for_buffer(buffer_a, Some(project_a), cx)
});
let mut editor_cx_a = EditorTestContext { let mut editor_cx_a = EditorTestContext {
cx: cx_a, cx: cx_a,
window_id: window_a, window: window_a.into(),
editor: editor_a, editor: editor_a,
}; };
@ -3460,13 +3455,11 @@ async fn test_newline_above_or_below_does_not_move_guest_cursor(
.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx)) .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
.await .await
.unwrap(); .unwrap();
let (window_b, _) = cx_b.add_window(|_| EmptyView); let window_b = cx_b.add_window(|_| EmptyView);
let editor_b = cx_b.add_view(window_b, |cx| { let editor_b = window_b.add_view(cx_b, |cx| Editor::for_buffer(buffer_b, Some(project_b), cx));
Editor::for_buffer(buffer_b, Some(project_b), cx)
});
let mut editor_cx_b = EditorTestContext { let mut editor_cx_b = EditorTestContext {
cx: cx_b, cx: cx_b,
window_id: window_b, window: window_b.into(),
editor: editor_b, editor: editor_b,
}; };
@ -4205,8 +4198,8 @@ async fn test_collaborating_with_completion(
.update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)) .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
.await .await
.unwrap(); .unwrap();
let (window_b, _) = cx_b.add_window(|_| EmptyView); let window_b = cx_b.add_window(|_| EmptyView);
let editor_b = cx_b.add_view(window_b, |cx| { let editor_b = window_b.add_view(cx_b, |cx| {
Editor::for_buffer(buffer_b.clone(), Some(project_b.clone()), cx) Editor::for_buffer(buffer_b.clone(), Some(project_b.clone()), cx)
}); });
@ -5316,7 +5309,8 @@ async fn test_collaborating_with_code_actions(
// Join the project as client B. // Join the project as client B.
let project_b = client_b.build_remote_project(project_id, cx_b).await; let project_b = client_b.build_remote_project(project_id, cx_b).await;
let (_window_b, workspace_b) = cx_b.add_window(|cx| Workspace::test_new(project_b.clone(), cx)); let window_b = cx_b.add_window(|cx| Workspace::test_new(project_b.clone(), cx));
let workspace_b = window_b.root(cx_b);
let editor_b = workspace_b let editor_b = workspace_b
.update(cx_b, |workspace, cx| { .update(cx_b, |workspace, cx| {
workspace.open_path((worktree_id, "main.rs"), None, true, cx) workspace.open_path((worktree_id, "main.rs"), None, true, cx)
@ -5540,7 +5534,8 @@ async fn test_collaborating_with_renames(
.unwrap(); .unwrap();
let project_b = client_b.build_remote_project(project_id, cx_b).await; let project_b = client_b.build_remote_project(project_id, cx_b).await;
let (_window_b, workspace_b) = cx_b.add_window(|cx| Workspace::test_new(project_b.clone(), cx)); let window_b = cx_b.add_window(|cx| Workspace::test_new(project_b.clone(), cx));
let workspace_b = window_b.root(cx_b);
let editor_b = workspace_b let editor_b = workspace_b
.update(cx_b, |workspace, cx| { .update(cx_b, |workspace, cx| {
workspace.open_path((worktree_id, "one.rs"), None, true, cx) workspace.open_path((worktree_id, "one.rs"), None, true, cx)
@ -5571,6 +5566,7 @@ async fn test_collaborating_with_renames(
.unwrap(); .unwrap();
prepare_rename.await.unwrap(); prepare_rename.await.unwrap();
editor_b.update(cx_b, |editor, cx| { editor_b.update(cx_b, |editor, cx| {
use editor::ToOffset;
let rename = editor.pending_rename().unwrap(); let rename = editor.pending_rename().unwrap();
let buffer = editor.buffer().read(cx).snapshot(cx); let buffer = editor.buffer().read(cx).snapshot(cx);
assert_eq!( assert_eq!(
@ -6445,8 +6441,10 @@ async fn test_basic_following(
.await .await
.unwrap(); .unwrap();
let workspace_a = client_a.build_workspace(&project_a, cx_a); let window_a = client_a.build_workspace(&project_a, cx_a);
let workspace_b = client_b.build_workspace(&project_b, cx_b); let workspace_a = window_a.root(cx_a);
let window_b = client_b.build_workspace(&project_b, cx_b);
let workspace_b = window_b.root(cx_b);
// Client A opens some editors. // Client A opens some editors.
let pane_a = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone()); let pane_a = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
@ -6529,7 +6527,8 @@ async fn test_basic_following(
cx_c.foreground().run_until_parked(); cx_c.foreground().run_until_parked();
let active_call_c = cx_c.read(ActiveCall::global); let active_call_c = cx_c.read(ActiveCall::global);
let project_c = client_c.build_remote_project(project_id, cx_c).await; let project_c = client_c.build_remote_project(project_id, cx_c).await;
let workspace_c = client_c.build_workspace(&project_c, cx_c); let window_c = client_c.build_workspace(&project_c, cx_c);
let workspace_c = window_c.root(cx_c);
active_call_c active_call_c
.update(cx_c, |call, cx| call.set_location(Some(&project_c), cx)) .update(cx_c, |call, cx| call.set_location(Some(&project_c), cx))
.await .await
@ -6547,7 +6546,7 @@ async fn test_basic_following(
cx_d.foreground().run_until_parked(); cx_d.foreground().run_until_parked();
let active_call_d = cx_d.read(ActiveCall::global); let active_call_d = cx_d.read(ActiveCall::global);
let project_d = client_d.build_remote_project(project_id, cx_d).await; let project_d = client_d.build_remote_project(project_id, cx_d).await;
let workspace_d = client_d.build_workspace(&project_d, cx_d); let workspace_d = client_d.build_workspace(&project_d, cx_d).root(cx_d);
active_call_d active_call_d
.update(cx_d, |call, cx| call.set_location(Some(&project_d), cx)) .update(cx_d, |call, cx| call.set_location(Some(&project_d), cx))
.await .await
@ -6645,6 +6644,7 @@ async fn test_basic_following(
} }
// Client C closes the project. // Client C closes the project.
window_c.remove(cx_c);
cx_c.drop_last(workspace_c); cx_c.drop_last(workspace_c);
// Clients A and B see that client B is following A, and client C is not present in the followers. // Clients A and B see that client B is following A, and client C is not present in the followers.
@ -6874,9 +6874,7 @@ async fn test_basic_following(
}); });
// Client B activates a panel, and the previously-opened screen-sharing item gets activated. // Client B activates a panel, and the previously-opened screen-sharing item gets activated.
let panel = cx_b.add_view(workspace_b.window_id(), |_| { let panel = window_b.add_view(cx_b, |_| TestPanel::new(DockPosition::Left));
TestPanel::new(DockPosition::Left)
});
workspace_b.update(cx_b, |workspace, cx| { workspace_b.update(cx_b, |workspace, cx| {
workspace.add_panel(panel, cx); workspace.add_panel(panel, cx);
workspace.toggle_panel_focus::<TestPanel>(cx); workspace.toggle_panel_focus::<TestPanel>(cx);
@ -6904,7 +6902,7 @@ async fn test_basic_following(
// Client B activates an item that doesn't implement following, // Client B activates an item that doesn't implement following,
// so the previously-opened screen-sharing item gets activated. // so the previously-opened screen-sharing item gets activated.
let unfollowable_item = cx_b.add_view(workspace_b.window_id(), |_| TestItem::new()); let unfollowable_item = window_b.add_view(cx_b, |_| TestItem::new());
workspace_b.update(cx_b, |workspace, cx| { workspace_b.update(cx_b, |workspace, cx| {
workspace.active_pane().update(cx, |pane, cx| { workspace.active_pane().update(cx, |pane, cx| {
pane.add_item(Box::new(unfollowable_item), true, true, None, cx) pane.add_item(Box::new(unfollowable_item), true, true, None, cx)
@ -7066,10 +7064,10 @@ async fn test_following_tab_order(
.await .await
.unwrap(); .unwrap();
let workspace_a = client_a.build_workspace(&project_a, cx_a); let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
let pane_a = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone()); let pane_a = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
let workspace_b = client_b.build_workspace(&project_b, cx_b); let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
let pane_b = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone()); let pane_b = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
let client_b_id = project_a.read_with(cx_a, |project, _| { let client_b_id = project_a.read_with(cx_a, |project, _| {
@ -7192,7 +7190,7 @@ async fn test_peers_following_each_other(
.unwrap(); .unwrap();
// Client A opens some editors. // Client A opens some editors.
let workspace_a = client_a.build_workspace(&project_a, cx_a); let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
let pane_a1 = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone()); let pane_a1 = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
let _editor_a1 = workspace_a let _editor_a1 = workspace_a
.update(cx_a, |workspace, cx| { .update(cx_a, |workspace, cx| {
@ -7204,7 +7202,7 @@ async fn test_peers_following_each_other(
.unwrap(); .unwrap();
// Client B opens an editor. // Client B opens an editor.
let workspace_b = client_b.build_workspace(&project_b, cx_b); let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
let pane_b1 = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone()); let pane_b1 = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
let _editor_b1 = workspace_b let _editor_b1 = workspace_b
.update(cx_b, |workspace, cx| { .update(cx_b, |workspace, cx| {
@ -7363,7 +7361,7 @@ async fn test_auto_unfollowing(
.unwrap(); .unwrap();
// Client A opens some editors. // Client A opens some editors.
let workspace_a = client_a.build_workspace(&project_a, cx_a); let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
let _editor_a1 = workspace_a let _editor_a1 = workspace_a
.update(cx_a, |workspace, cx| { .update(cx_a, |workspace, cx| {
workspace.open_path((worktree_id, "1.txt"), None, true, cx) workspace.open_path((worktree_id, "1.txt"), None, true, cx)
@ -7374,7 +7372,7 @@ async fn test_auto_unfollowing(
.unwrap(); .unwrap();
// Client B starts following client A. // Client B starts following client A.
let workspace_b = client_b.build_workspace(&project_b, cx_b); let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
let pane_b = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone()); let pane_b = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
let leader_id = project_b.read_with(cx_b, |project, _| { let leader_id = project_b.read_with(cx_b, |project, _| {
project.collaborators().values().next().unwrap().peer_id project.collaborators().values().next().unwrap().peer_id
@ -7502,14 +7500,14 @@ async fn test_peers_simultaneously_following_each_other(
client_a.fs.insert_tree("/a", json!({})).await; client_a.fs.insert_tree("/a", json!({})).await;
let (project_a, _) = client_a.build_local_project("/a", cx_a).await; let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
let workspace_a = client_a.build_workspace(&project_a, cx_a); let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
let project_id = active_call_a let project_id = active_call_a
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
.await .await
.unwrap(); .unwrap();
let project_b = client_b.build_remote_project(project_id, cx_b).await; let project_b = client_b.build_remote_project(project_id, cx_b).await;
let workspace_b = client_b.build_workspace(&project_b, cx_b); let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
deterministic.run_until_parked(); deterministic.run_until_parked();
let client_a_id = project_b.read_with(cx_b, |project, _| { let client_a_id = project_b.read_with(cx_b, |project, _| {
@ -7601,8 +7599,8 @@ async fn test_on_input_format_from_host_to_guest(
.update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)) .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
.await .await
.unwrap(); .unwrap();
let (window_a, _) = cx_a.add_window(|_| EmptyView); let window_a = cx_a.add_window(|_| EmptyView);
let editor_a = cx_a.add_view(window_a, |cx| { let editor_a = window_a.add_view(cx_a, |cx| {
Editor::for_buffer(buffer_a, Some(project_a.clone()), cx) Editor::for_buffer(buffer_a, Some(project_a.clone()), cx)
}); });
@ -7730,8 +7728,8 @@ async fn test_on_input_format_from_guest_to_host(
.update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)) .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
.await .await
.unwrap(); .unwrap();
let (window_b, _) = cx_b.add_window(|_| EmptyView); let window_b = cx_b.add_window(|_| EmptyView);
let editor_b = cx_b.add_view(window_b, |cx| { let editor_b = window_b.add_view(cx_b, |cx| {
Editor::for_buffer(buffer_b, Some(project_b.clone()), cx) Editor::for_buffer(buffer_b, Some(project_b.clone()), cx)
}); });
@ -7891,7 +7889,7 @@ async fn test_mutual_editor_inlay_hint_cache_update(
.await .await
.unwrap(); .unwrap();
let workspace_a = client_a.build_workspace(&project_a, cx_a); let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
cx_a.foreground().start_waiting(); cx_a.foreground().start_waiting();
let _buffer_a = project_a let _buffer_a = project_a
@ -7959,7 +7957,7 @@ async fn test_mutual_editor_inlay_hint_cache_update(
"Host editor update the cache version after every cache/view change", "Host editor update the cache version after every cache/view change",
); );
}); });
let workspace_b = client_b.build_workspace(&project_b, cx_b); let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
let editor_b = workspace_b let editor_b = workspace_b
.update(cx_b, |workspace, cx| { .update(cx_b, |workspace, cx| {
workspace.open_path((worktree_id, "main.rs"), None, true, cx) workspace.open_path((worktree_id, "main.rs"), None, true, cx)
@ -8198,8 +8196,8 @@ async fn test_inlay_hint_refresh_is_forwarded(
.await .await
.unwrap(); .unwrap();
let workspace_a = client_a.build_workspace(&project_a, cx_a); let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
let workspace_b = client_b.build_workspace(&project_b, cx_b); let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
cx_a.foreground().start_waiting(); cx_a.foreground().start_waiting();
cx_b.foreground().start_waiting(); cx_b.foreground().start_waiting();

View file

@ -183,7 +183,7 @@ async fn apply_server_operation(
let username; let username;
{ {
let mut plan = plan.lock(); let mut plan = plan.lock();
let mut user = plan.user(user_id); let user = plan.user(user_id);
if user.online { if user.online {
return false; return false;
} }

View file

@ -374,7 +374,7 @@ impl CollabTitlebarItem {
"Share Feedback", "Share Feedback",
feedback::feedback_editor::GiveFeedback, feedback::feedback_editor::GiveFeedback,
), ),
ContextMenuItem::action("Sign out", SignOut), ContextMenuItem::action("Sign Out", SignOut),
] ]
} else { } else {
vec![ vec![

View file

@ -305,18 +305,18 @@ impl ContactList {
github_login github_login
); );
let mut answer = cx.prompt(PromptLevel::Warning, &prompt_message, &["Remove", "Cancel"]); let mut answer = cx.prompt(PromptLevel::Warning, &prompt_message, &["Remove", "Cancel"]);
let window_id = cx.window_id(); let window = cx.window();
cx.spawn(|_, mut cx| async move { cx.spawn(|_, mut cx| async move {
if answer.next().await == Some(0) { if answer.next().await == Some(0) {
if let Err(e) = user_store if let Err(e) = user_store
.update(&mut cx, |store, cx| store.remove_contact(user_id, cx)) .update(&mut cx, |store, cx| store.remove_contact(user_id, cx))
.await .await
{ {
cx.prompt( window.prompt(
window_id,
PromptLevel::Info, PromptLevel::Info,
&format!("Failed to remove contact: {}", e), &format!("Failed to remove contact: {}", e),
&["Ok"], &["Ok"],
&mut cx,
); );
} }
} }

View file

@ -7,7 +7,7 @@ use gpui::{
elements::*, elements::*,
geometry::{rect::RectF, vector::vec2f}, geometry::{rect::RectF, vector::vec2f},
platform::{CursorStyle, MouseButton, WindowBounds, WindowKind, WindowOptions}, platform::{CursorStyle, MouseButton, WindowBounds, WindowKind, WindowOptions},
AnyElement, AppContext, Entity, View, ViewContext, AnyElement, AppContext, Entity, View, ViewContext, WindowHandle,
}; };
use util::ResultExt; use util::ResultExt;
use workspace::AppState; use workspace::AppState;
@ -16,10 +16,10 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
let app_state = Arc::downgrade(app_state); let app_state = Arc::downgrade(app_state);
let mut incoming_call = ActiveCall::global(cx).read(cx).incoming(); let mut incoming_call = ActiveCall::global(cx).read(cx).incoming();
cx.spawn(|mut cx| async move { cx.spawn(|mut cx| async move {
let mut notification_windows = Vec::new(); let mut notification_windows: Vec<WindowHandle<IncomingCallNotification>> = Vec::new();
while let Some(incoming_call) = incoming_call.next().await { while let Some(incoming_call) = incoming_call.next().await {
for window_id in notification_windows.drain(..) { for window in notification_windows.drain(..) {
cx.remove_window(window_id); window.remove(&mut cx);
} }
if let Some(incoming_call) = incoming_call { if let Some(incoming_call) = incoming_call {
@ -31,7 +31,7 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
for screen in cx.platform().screens() { for screen in cx.platform().screens() {
let screen_bounds = screen.bounds(); let screen_bounds = screen.bounds();
let (window_id, _) = cx.add_window( let window = cx.add_window(
WindowOptions { WindowOptions {
bounds: WindowBounds::Fixed(RectF::new( bounds: WindowBounds::Fixed(RectF::new(
screen_bounds.upper_right() screen_bounds.upper_right()
@ -49,7 +49,7 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
|_| IncomingCallNotification::new(incoming_call.clone(), app_state.clone()), |_| IncomingCallNotification::new(incoming_call.clone(), app_state.clone()),
); );
notification_windows.push(window_id); notification_windows.push(window);
} }
} }
} }

View file

@ -26,7 +26,7 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
for screen in cx.platform().screens() { for screen in cx.platform().screens() {
let screen_bounds = screen.bounds(); let screen_bounds = screen.bounds();
let (window_id, _) = cx.add_window( let window = cx.add_window(
WindowOptions { WindowOptions {
bounds: WindowBounds::Fixed(RectF::new( bounds: WindowBounds::Fixed(RectF::new(
screen_bounds.upper_right() - vec2f(PADDING + window_size.x(), PADDING), screen_bounds.upper_right() - vec2f(PADDING + window_size.x(), PADDING),
@ -52,20 +52,20 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
notification_windows notification_windows
.entry(*project_id) .entry(*project_id)
.or_insert(Vec::new()) .or_insert(Vec::new())
.push(window_id); .push(window);
} }
} }
room::Event::RemoteProjectUnshared { project_id } => { room::Event::RemoteProjectUnshared { project_id } => {
if let Some(window_ids) = notification_windows.remove(&project_id) { if let Some(windows) = notification_windows.remove(&project_id) {
for window_id in window_ids { for window in windows {
cx.update_window(window_id, |cx| cx.remove_window()); window.remove(cx);
} }
} }
} }
room::Event::Left => { room::Event::Left => {
for (_, window_ids) in notification_windows.drain() { for (_, windows) in notification_windows.drain() {
for window_id in window_ids { for window in windows {
cx.update_window(window_id, |cx| cx.remove_window()); window.remove(cx);
} }
} }
} }

View file

@ -20,11 +20,11 @@ pub fn init(cx: &mut AppContext) {
{ {
status_indicator = Some(cx.add_status_bar_item(|_| SharingStatusIndicator)); status_indicator = Some(cx.add_status_bar_item(|_| SharingStatusIndicator));
} }
} else if let Some((window_id, _)) = status_indicator.take() { } else if let Some(window) = status_indicator.take() {
cx.update_window(window_id, |cx| cx.remove_window()); window.update(cx, |cx| cx.remove_window());
} }
} else if let Some((window_id, _)) = status_indicator.take() { } else if let Some(window) = status_indicator.take() {
cx.update_window(window_id, |cx| cx.remove_window()); window.update(cx, |cx| cx.remove_window());
} }
}) })
.detach(); .detach();

View file

@ -1,8 +1,8 @@
use collections::CommandPaletteFilter; use collections::CommandPaletteFilter;
use fuzzy::{StringMatch, StringMatchCandidate}; use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{ use gpui::{
actions, elements::*, keymap_matcher::Keystroke, Action, AppContext, Element, MouseState, actions, anyhow::anyhow, elements::*, keymap_matcher::Keystroke, Action, AnyWindowHandle,
ViewContext, AppContext, Element, MouseState, ViewContext,
}; };
use picker::{Picker, PickerDelegate, PickerEvent}; use picker::{Picker, PickerDelegate, PickerEvent};
use std::cmp; use std::cmp;
@ -28,7 +28,7 @@ pub struct CommandPaletteDelegate {
pub enum Event { pub enum Event {
Dismissed, Dismissed,
Confirmed { Confirmed {
window_id: usize, window: AnyWindowHandle,
focused_view_id: usize, focused_view_id: usize,
action: Box<dyn Action>, action: Box<dyn Action>,
}, },
@ -80,12 +80,13 @@ impl PickerDelegate for CommandPaletteDelegate {
query: String, query: String,
cx: &mut ViewContext<Picker<Self>>, cx: &mut ViewContext<Picker<Self>>,
) -> gpui::Task<()> { ) -> gpui::Task<()> {
let window_id = cx.window_id();
let view_id = self.focused_view_id; let view_id = self.focused_view_id;
let window = cx.window();
cx.spawn(move |picker, mut cx| async move { cx.spawn(move |picker, mut cx| async move {
let actions = cx let actions = window
.available_actions(window_id, view_id) .available_actions(view_id, &cx)
.into_iter() .into_iter()
.flatten()
.filter_map(|(name, action, bindings)| { .filter_map(|(name, action, bindings)| {
let filtered = cx.read(|cx| { let filtered = cx.read(|cx| {
if cx.has_global::<CommandPaletteFilter>() { if cx.has_global::<CommandPaletteFilter>() {
@ -162,13 +163,15 @@ impl PickerDelegate for CommandPaletteDelegate {
fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<Self>>) { fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<Self>>) {
if !self.matches.is_empty() { if !self.matches.is_empty() {
let window_id = cx.window_id(); let window = cx.window();
let focused_view_id = self.focused_view_id; let focused_view_id = self.focused_view_id;
let action_ix = self.matches[self.selected_ix].candidate_id; let action_ix = self.matches[self.selected_ix].candidate_id;
let action = self.actions.remove(action_ix).action; let action = self.actions.remove(action_ix).action;
cx.app_context() cx.app_context()
.spawn(move |mut cx| async move { .spawn(move |mut cx| async move {
cx.dispatch_action(window_id, focused_view_id, action.as_ref()) window
.dispatch_action(focused_view_id, action.as_ref(), &mut cx)
.ok_or_else(|| anyhow!("window was closed"))
}) })
.detach_and_log_err(cx); .detach_and_log_err(cx);
} }
@ -295,8 +298,9 @@ mod tests {
let app_state = init_test(cx); let app_state = init_test(cx);
let project = Project::test(app_state.fs.clone(), [], cx).await; let project = Project::test(app_state.fs.clone(), [], cx).await;
let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
let editor = cx.add_view(window_id, |cx| { let workspace = window.root(cx);
let editor = window.add_view(cx, |cx| {
let mut editor = Editor::single_line(None, cx); let mut editor = Editor::single_line(None, cx);
editor.set_text("abc", cx); editor.set_text("abc", cx);
editor editor

View file

@ -1,5 +1,5 @@
use gpui::{ use gpui::{
anyhow, anyhow::{self, anyhow},
elements::*, elements::*,
geometry::vector::Vector2F, geometry::vector::Vector2F,
keymap_matcher::KeymapContext, keymap_matcher::KeymapContext,
@ -218,12 +218,14 @@ impl ContextMenu {
if let Some(ContextMenuItem::Item { action, .. }) = self.items.get(ix) { if let Some(ContextMenuItem::Item { action, .. }) = self.items.get(ix) {
match action { match action {
ContextMenuItemAction::Action(action) => { ContextMenuItemAction::Action(action) => {
let window_id = cx.window_id(); let window = cx.window();
let view_id = self.parent_view_id; let view_id = self.parent_view_id;
let action = action.boxed_clone(); let action = action.boxed_clone();
cx.app_context() cx.app_context()
.spawn(|mut cx| async move { .spawn(|mut cx| async move {
cx.dispatch_action(window_id, view_id, action.as_ref()) window
.dispatch_action(view_id, action.as_ref(), &mut cx)
.ok_or_else(|| anyhow!("window was closed"))
}) })
.detach_and_log_err(cx); .detach_and_log_err(cx);
} }
@ -480,17 +482,19 @@ impl ContextMenu {
.on_down(MouseButton::Left, |_, _, _| {}) // Capture these events .on_down(MouseButton::Left, |_, _, _| {}) // Capture these events
.on_click(MouseButton::Left, move |_, menu, cx| { .on_click(MouseButton::Left, move |_, menu, cx| {
menu.cancel(&Default::default(), cx); menu.cancel(&Default::default(), cx);
let window_id = cx.window_id(); let window = cx.window();
match &action { match &action {
ContextMenuItemAction::Action(action) => { ContextMenuItemAction::Action(action) => {
let action = action.boxed_clone(); let action = action.boxed_clone();
cx.app_context() cx.app_context()
.spawn(|mut cx| async move { .spawn(|mut cx| async move {
cx.dispatch_action( window
window_id, .dispatch_action(
view_id, view_id,
action.as_ref(), action.as_ref(),
&mut cx,
) )
.ok_or_else(|| anyhow!("window was closed"))
}) })
.detach_and_log_err(cx); .detach_and_log_err(cx);
} }

View file

@ -338,9 +338,9 @@ impl Copilot {
let (server, fake_server) = let (server, fake_server) =
LanguageServer::fake("copilot".into(), Default::default(), cx.to_async()); LanguageServer::fake("copilot".into(), Default::default(), cx.to_async());
let http = util::http::FakeHttpClient::create(|_| async { unreachable!() }); let http = util::http::FakeHttpClient::create(|_| async { unreachable!() });
let this = cx.add_model(|cx| Self { let this = cx.add_model(|_| Self {
http: http.clone(), http: http.clone(),
node_runtime: NodeRuntime::instance(http, cx.background().clone()), node_runtime: NodeRuntime::instance(http),
server: CopilotServer::Running(RunningCopilotServer { server: CopilotServer::Running(RunningCopilotServer {
lsp: Arc::new(server), lsp: Arc::new(server),
sign_in_status: SignInStatus::Authorized, sign_in_status: SignInStatus::Authorized,

View file

@ -4,7 +4,7 @@ use gpui::{
geometry::rect::RectF, geometry::rect::RectF,
platform::{WindowBounds, WindowKind, WindowOptions}, platform::{WindowBounds, WindowKind, WindowOptions},
AnyElement, AnyViewHandle, AppContext, ClipboardItem, Element, Entity, View, ViewContext, AnyElement, AnyViewHandle, AppContext, ClipboardItem, Element, Entity, View, ViewContext,
ViewHandle, WindowHandle,
}; };
use theme::ui::modal; use theme::ui::modal;
@ -18,43 +18,43 @@ const COPILOT_SIGN_UP_URL: &'static str = "https://github.com/features/copilot";
pub fn init(cx: &mut AppContext) { pub fn init(cx: &mut AppContext) {
if let Some(copilot) = Copilot::global(cx) { if let Some(copilot) = Copilot::global(cx) {
let mut code_verification: Option<ViewHandle<CopilotCodeVerification>> = None; let mut verification_window: Option<WindowHandle<CopilotCodeVerification>> = None;
cx.observe(&copilot, move |copilot, cx| { cx.observe(&copilot, move |copilot, cx| {
let status = copilot.read(cx).status(); let status = copilot.read(cx).status();
match &status { match &status {
crate::Status::SigningIn { prompt } => { crate::Status::SigningIn { prompt } => {
if let Some(code_verification_handle) = code_verification.as_mut() { if let Some(window) = verification_window.as_mut() {
let window_id = code_verification_handle.window_id(); let updated = window
let updated = cx.update_window(window_id, |cx| { .root(cx)
code_verification_handle.update(cx, |code_verification, cx| { .map(|root| {
code_verification.set_status(status.clone(), cx) root.update(cx, |verification, cx| {
}); verification.set_status(status.clone(), cx);
cx.activate_window(); cx.activate_window();
}); })
if updated.is_none() { })
code_verification = Some(create_copilot_auth_window(cx, &status)); .is_some();
if !updated {
verification_window = Some(create_copilot_auth_window(cx, &status));
} }
} else if let Some(_prompt) = prompt { } else if let Some(_prompt) = prompt {
code_verification = Some(create_copilot_auth_window(cx, &status)); verification_window = Some(create_copilot_auth_window(cx, &status));
} }
} }
Status::Authorized | Status::Unauthorized => { Status::Authorized | Status::Unauthorized => {
if let Some(code_verification) = code_verification.as_ref() { if let Some(window) = verification_window.as_ref() {
let window_id = code_verification.window_id(); if let Some(verification) = window.root(cx) {
cx.update_window(window_id, |cx| { verification.update(cx, |verification, cx| {
code_verification.update(cx, |code_verification, cx| { verification.set_status(status, cx);
code_verification.set_status(status, cx)
});
cx.platform().activate(true); cx.platform().activate(true);
cx.activate_window(); cx.activate_window();
}); });
} }
} }
}
_ => { _ => {
if let Some(code_verification) = code_verification.take() { if let Some(code_verification) = verification_window.take() {
cx.update_window(code_verification.window_id(), |cx| cx.remove_window()); code_verification.update(cx, |cx| cx.remove_window());
} }
} }
} }
@ -66,7 +66,7 @@ pub fn init(cx: &mut AppContext) {
fn create_copilot_auth_window( fn create_copilot_auth_window(
cx: &mut AppContext, cx: &mut AppContext,
status: &Status, status: &Status,
) -> ViewHandle<CopilotCodeVerification> { ) -> WindowHandle<CopilotCodeVerification> {
let window_size = theme::current(cx).copilot.modal.dimensions(); let window_size = theme::current(cx).copilot.modal.dimensions();
let window_options = WindowOptions { let window_options = WindowOptions {
bounds: WindowBounds::Fixed(RectF::new(Default::default(), window_size)), bounds: WindowBounds::Fixed(RectF::new(Default::default(), window_size)),
@ -78,10 +78,9 @@ fn create_copilot_auth_window(
is_movable: true, is_movable: true,
screen: None, screen: None,
}; };
let (_, view) = cx.add_window(window_options, |_cx| { cx.add_window(window_options, |_cx| {
CopilotCodeVerification::new(status.clone()) CopilotCodeVerification::new(status.clone())
}); })
view
} }
pub struct CopilotCodeVerification { pub struct CopilotCodeVerification {

View file

@ -855,7 +855,8 @@ mod tests {
let language_server_id = LanguageServerId(0); let language_server_id = LanguageServerId(0);
let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await; let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
let workspace = window.root(cx);
// Create some diagnostics // Create some diagnostics
project.update(cx, |project, cx| { project.update(cx, |project, cx| {
@ -942,7 +943,7 @@ mod tests {
}); });
// Open the project diagnostics view while there are already diagnostics. // Open the project diagnostics view while there are already diagnostics.
let view = cx.add_view(window_id, |cx| { let view = window.add_view(cx, |cx| {
ProjectDiagnosticsEditor::new(project.clone(), workspace.downgrade(), cx) ProjectDiagnosticsEditor::new(project.clone(), workspace.downgrade(), cx)
}); });
@ -1248,9 +1249,10 @@ mod tests {
let server_id_1 = LanguageServerId(100); let server_id_1 = LanguageServerId(100);
let server_id_2 = LanguageServerId(101); let server_id_2 = LanguageServerId(101);
let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await; let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
let workspace = window.root(cx);
let view = cx.add_view(window_id, |cx| { let view = window.add_view(cx, |cx| {
ProjectDiagnosticsEditor::new(project.clone(), workspace.downgrade(), cx) ProjectDiagnosticsEditor::new(project.clone(), workspace.downgrade(), cx)
}); });

View file

@ -6,7 +6,7 @@ use gpui::{
geometry::{rect::RectF, vector::Vector2F}, geometry::{rect::RectF, vector::Vector2F},
platform::{CursorStyle, MouseButton}, platform::{CursorStyle, MouseButton},
scene::{MouseDown, MouseDrag}, scene::{MouseDown, MouseDrag},
AnyElement, Element, View, ViewContext, WeakViewHandle, WindowContext, AnyElement, AnyWindowHandle, Element, View, ViewContext, WeakViewHandle, WindowContext,
}; };
const DEAD_ZONE: f32 = 4.; const DEAD_ZONE: f32 = 4.;
@ -21,7 +21,7 @@ enum State<V: View> {
region: RectF, region: RectF,
}, },
Dragging { Dragging {
window_id: usize, window: AnyWindowHandle,
position: Vector2F, position: Vector2F,
region_offset: Vector2F, region_offset: Vector2F,
region: RectF, region: RectF,
@ -49,14 +49,14 @@ impl<V: View> Clone for State<V> {
region, region,
}, },
State::Dragging { State::Dragging {
window_id, window,
position, position,
region_offset, region_offset,
region, region,
payload, payload,
render, render,
} => Self::Dragging { } => Self::Dragging {
window_id: window_id.clone(), window: window.clone(),
position: position.clone(), position: position.clone(),
region_offset: region_offset.clone(), region_offset: region_offset.clone(),
region: region.clone(), region: region.clone(),
@ -87,16 +87,16 @@ impl<V: View> DragAndDrop<V> {
self.containers.insert(handle); self.containers.insert(handle);
} }
pub fn currently_dragged<T: Any>(&self, window_id: usize) -> Option<(Vector2F, Rc<T>)> { pub fn currently_dragged<T: Any>(&self, window: AnyWindowHandle) -> Option<(Vector2F, Rc<T>)> {
self.currently_dragged.as_ref().and_then(|state| { self.currently_dragged.as_ref().and_then(|state| {
if let State::Dragging { if let State::Dragging {
position, position,
payload, payload,
window_id: window_dragged_from, window: window_dragged_from,
.. ..
} = state } = state
{ {
if &window_id != window_dragged_from { if &window != window_dragged_from {
return None; return None;
} }
@ -126,9 +126,9 @@ impl<V: View> DragAndDrop<V> {
cx: &mut WindowContext, cx: &mut WindowContext,
render: Rc<impl 'static + Fn(&T, &mut ViewContext<V>) -> AnyElement<V>>, render: Rc<impl 'static + Fn(&T, &mut ViewContext<V>) -> AnyElement<V>>,
) { ) {
let window_id = cx.window_id(); let window = cx.window();
cx.update_global(|this: &mut Self, cx| { cx.update_global(|this: &mut Self, cx| {
this.notify_containers_for_window(window_id, cx); this.notify_containers_for_window(window, cx);
match this.currently_dragged.as_ref() { match this.currently_dragged.as_ref() {
Some(&State::Down { Some(&State::Down {
@ -141,7 +141,7 @@ impl<V: View> DragAndDrop<V> {
}) => { }) => {
if (event.position - (region.origin() + region_offset)).length() > DEAD_ZONE { if (event.position - (region.origin() + region_offset)).length() > DEAD_ZONE {
this.currently_dragged = Some(State::Dragging { this.currently_dragged = Some(State::Dragging {
window_id, window,
region_offset, region_offset,
region, region,
position: event.position, position: event.position,
@ -163,7 +163,7 @@ impl<V: View> DragAndDrop<V> {
.. ..
}) => { }) => {
this.currently_dragged = Some(State::Dragging { this.currently_dragged = Some(State::Dragging {
window_id, window,
region_offset, region_offset,
region, region,
position: event.position, position: event.position,
@ -188,14 +188,14 @@ impl<V: View> DragAndDrop<V> {
State::Down { .. } => None, State::Down { .. } => None,
State::DeadZone { .. } => None, State::DeadZone { .. } => None,
State::Dragging { State::Dragging {
window_id, window,
region_offset, region_offset,
position, position,
region, region,
payload, payload,
render, render,
} => { } => {
if cx.window_id() != window_id { if cx.window() != window {
return None; return None;
} }
@ -260,27 +260,27 @@ impl<V: View> DragAndDrop<V> {
pub fn cancel_dragging<P: Any>(&mut self, cx: &mut WindowContext) { pub fn cancel_dragging<P: Any>(&mut self, cx: &mut WindowContext) {
if let Some(State::Dragging { if let Some(State::Dragging {
payload, window_id, .. payload, window, ..
}) = &self.currently_dragged }) = &self.currently_dragged
{ {
if payload.is::<P>() { if payload.is::<P>() {
let window_id = *window_id; let window = *window;
self.currently_dragged = Some(State::Canceled); self.currently_dragged = Some(State::Canceled);
self.notify_containers_for_window(window_id, cx); self.notify_containers_for_window(window, cx);
} }
} }
} }
fn finish_dragging(&mut self, cx: &mut WindowContext) { fn finish_dragging(&mut self, cx: &mut WindowContext) {
if let Some(State::Dragging { window_id, .. }) = self.currently_dragged.take() { if let Some(State::Dragging { window, .. }) = self.currently_dragged.take() {
self.notify_containers_for_window(window_id, cx); self.notify_containers_for_window(window, cx);
} }
} }
fn notify_containers_for_window(&mut self, window_id: usize, cx: &mut WindowContext) { fn notify_containers_for_window(&mut self, window: AnyWindowHandle, cx: &mut WindowContext) {
self.containers.retain(|container| { self.containers.retain(|container| {
if let Some(container) = container.upgrade(cx) { if let Some(container) = container.upgrade(cx) {
if container.window_id() == window_id { if container.window() == window {
container.update(cx, |_, cx| cx.notify()); container.update(cx, |_, cx| cx.notify());
} }
true true

View file

@ -47,6 +47,7 @@ workspace = { path = "../workspace" }
aho-corasick = "0.7" aho-corasick = "0.7"
anyhow.workspace = true anyhow.workspace = true
convert_case = "0.6.0"
futures.workspace = true futures.workspace = true
indoc = "1.0.4" indoc = "1.0.4"
itertools = "0.10" itertools = "0.10"
@ -56,12 +57,12 @@ ordered-float.workspace = true
parking_lot.workspace = true parking_lot.workspace = true
postage.workspace = true postage.workspace = true
pulldown-cmark = { version = "0.9.2", default-features = false } pulldown-cmark = { version = "0.9.2", default-features = false }
rand.workspace = true
schemars.workspace = true schemars.workspace = true
serde.workspace = true serde.workspace = true
serde_derive.workspace = true serde_derive.workspace = true
smallvec.workspace = true smallvec.workspace = true
smol.workspace = true smol.workspace = true
rand.workspace = true
tree-sitter-rust = { workspace = true, optional = true } tree-sitter-rust = { workspace = true, optional = true }
tree-sitter-html = { workspace = true, optional = true } tree-sitter-html = { workspace = true, optional = true }

View file

@ -397,7 +397,7 @@ impl InlayMap {
buffer_snapshot: MultiBufferSnapshot, buffer_snapshot: MultiBufferSnapshot,
mut buffer_edits: Vec<text::Edit<usize>>, mut buffer_edits: Vec<text::Edit<usize>>,
) -> (InlaySnapshot, Vec<InlayEdit>) { ) -> (InlaySnapshot, Vec<InlayEdit>) {
let mut snapshot = &mut self.snapshot; let snapshot = &mut self.snapshot;
if buffer_edits.is_empty() { if buffer_edits.is_empty() {
if snapshot.buffer.trailing_excerpt_update_count() if snapshot.buffer.trailing_excerpt_update_count()
@ -572,7 +572,6 @@ impl InlayMap {
}) })
.collect(); .collect();
let buffer_snapshot = snapshot.buffer.clone(); let buffer_snapshot = snapshot.buffer.clone();
drop(snapshot);
let (snapshot, edits) = self.sync(buffer_snapshot, buffer_edits); let (snapshot, edits) = self.sync(buffer_snapshot, buffer_edits);
(snapshot, edits) (snapshot, edits)
} }
@ -635,7 +634,6 @@ impl InlayMap {
} }
log::info!("removing inlays: {:?}", to_remove); log::info!("removing inlays: {:?}", to_remove);
drop(snapshot);
let (snapshot, edits) = self.splice(to_remove, to_insert); let (snapshot, edits) = self.splice(to_remove, to_insert);
(snapshot, edits) (snapshot, edits)
} }

View file

@ -28,6 +28,7 @@ use blink_manager::BlinkManager;
use client::{ClickhouseEvent, TelemetrySettings}; use client::{ClickhouseEvent, TelemetrySettings};
use clock::{Global, ReplicaId}; use clock::{Global, ReplicaId};
use collections::{BTreeMap, Bound, HashMap, HashSet, VecDeque}; use collections::{BTreeMap, Bound, HashMap, HashSet, VecDeque};
use convert_case::{Case, Casing};
use copilot::Copilot; use copilot::Copilot;
pub use display_map::DisplayPoint; pub use display_map::DisplayPoint;
use display_map::*; use display_map::*;
@ -89,7 +90,7 @@ use std::{
cmp::{self, Ordering, Reverse}, cmp::{self, Ordering, Reverse},
mem, mem,
num::NonZeroU32, num::NonZeroU32,
ops::{ControlFlow, Deref, DerefMut, Range}, ops::{ControlFlow, Deref, DerefMut, Range, RangeInclusive},
path::Path, path::Path,
sync::Arc, sync::Arc,
time::{Duration, Instant}, time::{Duration, Instant},
@ -231,6 +232,13 @@ actions!(
SortLinesCaseInsensitive, SortLinesCaseInsensitive,
ReverseLines, ReverseLines,
ShuffleLines, ShuffleLines,
ConvertToUpperCase,
ConvertToLowerCase,
ConvertToTitleCase,
ConvertToSnakeCase,
ConvertToKebabCase,
ConvertToUpperCamelCase,
ConvertToLowerCamelCase,
Transpose, Transpose,
Cut, Cut,
Copy, Copy,
@ -353,6 +361,13 @@ pub fn init(cx: &mut AppContext) {
cx.add_action(Editor::sort_lines_case_insensitive); cx.add_action(Editor::sort_lines_case_insensitive);
cx.add_action(Editor::reverse_lines); cx.add_action(Editor::reverse_lines);
cx.add_action(Editor::shuffle_lines); cx.add_action(Editor::shuffle_lines);
cx.add_action(Editor::convert_to_upper_case);
cx.add_action(Editor::convert_to_lower_case);
cx.add_action(Editor::convert_to_title_case);
cx.add_action(Editor::convert_to_snake_case);
cx.add_action(Editor::convert_to_kebab_case);
cx.add_action(Editor::convert_to_upper_camel_case);
cx.add_action(Editor::convert_to_lower_camel_case);
cx.add_action(Editor::delete_to_previous_word_start); cx.add_action(Editor::delete_to_previous_word_start);
cx.add_action(Editor::delete_to_previous_subword_start); cx.add_action(Editor::delete_to_previous_subword_start);
cx.add_action(Editor::delete_to_next_word_end); cx.add_action(Editor::delete_to_next_word_end);
@ -543,6 +558,7 @@ pub struct Editor {
show_local_selections: bool, show_local_selections: bool,
mode: EditorMode, mode: EditorMode,
show_gutter: bool, show_gutter: bool,
show_wrap_guides: Option<bool>,
placeholder_text: Option<Arc<str>>, placeholder_text: Option<Arc<str>>,
highlighted_rows: Option<Range<u32>>, highlighted_rows: Option<Range<u32>>,
#[allow(clippy::type_complexity)] #[allow(clippy::type_complexity)]
@ -1375,6 +1391,7 @@ impl Editor {
show_local_selections: true, show_local_selections: true,
mode, mode,
show_gutter: mode == EditorMode::Full, show_gutter: mode == EditorMode::Full,
show_wrap_guides: None,
placeholder_text: None, placeholder_text: None,
highlighted_rows: None, highlighted_rows: None,
background_highlights: Default::default(), background_highlights: Default::default(),
@ -1537,7 +1554,7 @@ impl Editor {
self.collapse_matches = collapse_matches; self.collapse_matches = collapse_matches;
} }
fn range_for_match<T: std::marker::Copy>(&self, range: &Range<T>) -> Range<T> { pub fn range_for_match<T: std::marker::Copy>(&self, range: &Range<T>) -> Range<T> {
if self.collapse_matches { if self.collapse_matches {
return range.start..range.start; return range.start..range.start;
} }
@ -4219,7 +4236,7 @@ impl Editor {
_: &SortLinesCaseSensitive, _: &SortLinesCaseSensitive,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) { ) {
self.manipulate_lines(cx, |text| text.sort()) self.manipulate_lines(cx, |lines| lines.sort())
} }
pub fn sort_lines_case_insensitive( pub fn sort_lines_case_insensitive(
@ -4227,7 +4244,7 @@ impl Editor {
_: &SortLinesCaseInsensitive, _: &SortLinesCaseInsensitive,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) { ) {
self.manipulate_lines(cx, |text| text.sort_by_key(|line| line.to_lowercase())) self.manipulate_lines(cx, |lines| lines.sort_by_key(|line| line.to_lowercase()))
} }
pub fn reverse_lines(&mut self, _: &ReverseLines, cx: &mut ViewContext<Self>) { pub fn reverse_lines(&mut self, _: &ReverseLines, cx: &mut ViewContext<Self>) {
@ -4265,19 +4282,19 @@ impl Editor {
let text = buffer let text = buffer
.text_for_range(start_point..end_point) .text_for_range(start_point..end_point)
.collect::<String>(); .collect::<String>();
let mut text = text.split("\n").collect_vec(); let mut lines = text.split("\n").collect_vec();
let text_len = text.len(); let lines_len = lines.len();
callback(&mut text); callback(&mut lines);
// This is a current limitation with selections. // This is a current limitation with selections.
// If we wanted to support removing or adding lines, we'd need to fix the logic associated with selections. // If we wanted to support removing or adding lines, we'd need to fix the logic associated with selections.
debug_assert!( debug_assert!(
text.len() == text_len, lines.len() == lines_len,
"callback should not change the number of lines" "callback should not change the number of lines"
); );
edits.push((start_point..end_point, text.join("\n"))); edits.push((start_point..end_point, lines.join("\n")));
let start_anchor = buffer.anchor_after(start_point); let start_anchor = buffer.anchor_after(start_point);
let end_anchor = buffer.anchor_before(end_point); let end_anchor = buffer.anchor_before(end_point);
@ -4304,6 +4321,97 @@ impl Editor {
}); });
} }
pub fn convert_to_upper_case(&mut self, _: &ConvertToUpperCase, cx: &mut ViewContext<Self>) {
self.manipulate_text(cx, |text| text.to_uppercase())
}
pub fn convert_to_lower_case(&mut self, _: &ConvertToLowerCase, cx: &mut ViewContext<Self>) {
self.manipulate_text(cx, |text| text.to_lowercase())
}
pub fn convert_to_title_case(&mut self, _: &ConvertToTitleCase, cx: &mut ViewContext<Self>) {
self.manipulate_text(cx, |text| text.to_case(Case::Title))
}
pub fn convert_to_snake_case(&mut self, _: &ConvertToSnakeCase, cx: &mut ViewContext<Self>) {
self.manipulate_text(cx, |text| text.to_case(Case::Snake))
}
pub fn convert_to_kebab_case(&mut self, _: &ConvertToKebabCase, cx: &mut ViewContext<Self>) {
self.manipulate_text(cx, |text| text.to_case(Case::Kebab))
}
pub fn convert_to_upper_camel_case(
&mut self,
_: &ConvertToUpperCamelCase,
cx: &mut ViewContext<Self>,
) {
self.manipulate_text(cx, |text| text.to_case(Case::UpperCamel))
}
pub fn convert_to_lower_camel_case(
&mut self,
_: &ConvertToLowerCamelCase,
cx: &mut ViewContext<Self>,
) {
self.manipulate_text(cx, |text| text.to_case(Case::Camel))
}
fn manipulate_text<Fn>(&mut self, cx: &mut ViewContext<Self>, mut callback: Fn)
where
Fn: FnMut(&str) -> String,
{
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
let buffer = self.buffer.read(cx).snapshot(cx);
let mut new_selections = Vec::new();
let mut edits = Vec::new();
let mut selection_adjustment = 0i32;
for selection in self.selections.all::<usize>(cx) {
let selection_is_empty = selection.is_empty();
let (start, end) = if selection_is_empty {
let word_range = movement::surrounding_word(
&display_map,
selection.start.to_display_point(&display_map),
);
let start = word_range.start.to_offset(&display_map, Bias::Left);
let end = word_range.end.to_offset(&display_map, Bias::Left);
(start, end)
} else {
(selection.start, selection.end)
};
let text = buffer.text_for_range(start..end).collect::<String>();
let old_length = text.len() as i32;
let text = callback(&text);
new_selections.push(Selection {
start: (start as i32 - selection_adjustment) as usize,
end: ((start + text.len()) as i32 - selection_adjustment) as usize,
goal: SelectionGoal::None,
..selection
});
selection_adjustment += old_length - text.len() as i32;
edits.push((start..end, text));
}
self.transact(cx, |this, cx| {
this.buffer.update(cx, |buffer, cx| {
buffer.edit(edits, None, cx);
});
this.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.select(new_selections);
});
this.request_autoscroll(Autoscroll::fit(), cx);
});
}
pub fn duplicate_line(&mut self, _: &DuplicateLine, cx: &mut ViewContext<Self>) { pub fn duplicate_line(&mut self, _: &DuplicateLine, cx: &mut ViewContext<Self>) {
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
let buffer = &display_map.buffer_snapshot; let buffer = &display_map.buffer_snapshot;
@ -6374,8 +6482,8 @@ impl Editor {
.range .range
.to_offset(definition.target.buffer.read(cx)); .to_offset(definition.target.buffer.read(cx));
if Some(&definition.target.buffer) == self.buffer.read(cx).as_singleton().as_ref() {
let range = self.range_for_match(&range); let range = self.range_for_match(&range);
if Some(&definition.target.buffer) == self.buffer.read(cx).as_singleton().as_ref() {
self.change_selections(Some(Autoscroll::fit()), cx, |s| { self.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.select_ranges([range]); s.select_ranges([range]);
}); });
@ -6392,7 +6500,6 @@ impl Editor {
// When selecting a definition in a different buffer, disable the nav history // When selecting a definition in a different buffer, disable the nav history
// to avoid creating a history entry at the previous cursor location. // to avoid creating a history entry at the previous cursor location.
pane.update(cx, |pane, _| pane.disable_history()); pane.update(cx, |pane, _| pane.disable_history());
let range = target_editor.range_for_match(&range);
target_editor.change_selections(Some(Autoscroll::fit()), cx, |s| { target_editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.select_ranges([range]); s.select_ranges([range]);
}); });
@ -7188,6 +7295,10 @@ impl Editor {
pub fn wrap_guides(&self, cx: &AppContext) -> SmallVec<[(usize, bool); 2]> { pub fn wrap_guides(&self, cx: &AppContext) -> SmallVec<[(usize, bool); 2]> {
let mut wrap_guides = smallvec::smallvec![]; let mut wrap_guides = smallvec::smallvec![];
if self.show_wrap_guides == Some(false) {
return wrap_guides;
}
let settings = self.buffer.read(cx).settings_at(0, cx); let settings = self.buffer.read(cx).settings_at(0, cx);
if settings.show_wrap_guides { if settings.show_wrap_guides {
if let SoftWrap::Column(soft_wrap) = self.soft_wrap_mode(cx) { if let SoftWrap::Column(soft_wrap) = self.soft_wrap_mode(cx) {
@ -7245,6 +7356,11 @@ impl Editor {
cx.notify(); cx.notify();
} }
pub fn set_show_wrap_guides(&mut self, show_gutter: bool, cx: &mut ViewContext<Self>) {
self.show_wrap_guides = Some(show_gutter);
cx.notify();
}
pub fn reveal_in_finder(&mut self, _: &RevealInFinder, cx: &mut ViewContext<Self>) { pub fn reveal_in_finder(&mut self, _: &RevealInFinder, cx: &mut ViewContext<Self>) {
if let Some(buffer) = self.buffer().read(cx).as_singleton() { if let Some(buffer) = self.buffer().read(cx).as_singleton() {
if let Some(file) = buffer.read(cx).file().and_then(|f| f.as_local()) { if let Some(file) = buffer.read(cx).file().and_then(|f| f.as_local()) {
@ -7433,6 +7549,78 @@ impl Editor {
results results
} }
pub fn background_highlight_row_ranges<T: 'static>(
&self,
search_range: Range<Anchor>,
display_snapshot: &DisplaySnapshot,
count: usize,
) -> Vec<RangeInclusive<DisplayPoint>> {
let mut results = Vec::new();
let buffer = &display_snapshot.buffer_snapshot;
let Some((_, ranges)) = self.background_highlights
.get(&TypeId::of::<T>()) else {
return vec![];
};
let start_ix = match ranges.binary_search_by(|probe| {
let cmp = probe.end.cmp(&search_range.start, buffer);
if cmp.is_gt() {
Ordering::Greater
} else {
Ordering::Less
}
}) {
Ok(i) | Err(i) => i,
};
let mut push_region = |start: Option<Point>, end: Option<Point>| {
if let (Some(start_display), Some(end_display)) = (start, end) {
results.push(
start_display.to_display_point(display_snapshot)
..=end_display.to_display_point(display_snapshot),
);
}
};
let mut start_row: Option<Point> = None;
let mut end_row: Option<Point> = None;
if ranges.len() > count {
return vec![];
}
for range in &ranges[start_ix..] {
if range.start.cmp(&search_range.end, buffer).is_ge() {
break;
}
let end = range.end.to_point(buffer);
if let Some(current_row) = &end_row {
if end.row == current_row.row {
continue;
}
}
let start = range.start.to_point(buffer);
if start_row.is_none() {
assert_eq!(end_row, None);
start_row = Some(start);
end_row = Some(end);
continue;
}
if let Some(current_end) = end_row.as_mut() {
if start.row > current_end.row + 1 {
push_region(start_row, end_row);
start_row = Some(start);
end_row = Some(end);
} else {
// Merge two hunks.
*current_end = end;
}
} else {
unreachable!();
}
}
// We might still have a hunk that was not rendered (if there was a search hit on the last line)
push_region(start_row, end_row);
results
}
pub fn highlight_text<T: 'static>( pub fn highlight_text<T: 'static>(
&mut self, &mut self,
ranges: Vec<Range<Anchor>>, ranges: Vec<Range<Anchor>>,

View file

@ -48,7 +48,8 @@ fn test_edit_events(cx: &mut TestAppContext) {
}); });
let events = Rc::new(RefCell::new(Vec::new())); let events = Rc::new(RefCell::new(Vec::new()));
let (_, editor1) = cx.add_window({ let editor1 = cx
.add_window({
let events = events.clone(); let events = events.clone();
|cx| { |cx| {
cx.subscribe(&cx.handle(), move |_, _, event, _| { cx.subscribe(&cx.handle(), move |_, _, event, _| {
@ -62,8 +63,10 @@ fn test_edit_events(cx: &mut TestAppContext) {
.detach(); .detach();
Editor::for_buffer(buffer.clone(), None, cx) Editor::for_buffer(buffer.clone(), None, cx)
} }
}); })
let (_, editor2) = cx.add_window({ .root(cx);
let editor2 = cx
.add_window({
let events = events.clone(); let events = events.clone();
|cx| { |cx| {
cx.subscribe(&cx.handle(), move |_, _, event, _| { cx.subscribe(&cx.handle(), move |_, _, event, _| {
@ -77,7 +80,8 @@ fn test_edit_events(cx: &mut TestAppContext) {
.detach(); .detach();
Editor::for_buffer(buffer.clone(), None, cx) Editor::for_buffer(buffer.clone(), None, cx)
} }
}); })
.root(cx);
assert_eq!(mem::take(&mut *events.borrow_mut()), []); assert_eq!(mem::take(&mut *events.borrow_mut()), []);
// Mutating editor 1 will emit an `Edited` event only for that editor. // Mutating editor 1 will emit an `Edited` event only for that editor.
@ -173,7 +177,9 @@ fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) {
let buffer = cx.add_model(|cx| language::Buffer::new(0, "123456", cx)); let buffer = cx.add_model(|cx| language::Buffer::new(0, "123456", cx));
let group_interval = buffer.read_with(cx, |buffer, _| buffer.transaction_group_interval()); let group_interval = buffer.read_with(cx, |buffer, _| buffer.transaction_group_interval());
let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
let (_, editor) = cx.add_window(|cx| build_editor(buffer.clone(), cx)); let editor = cx
.add_window(|cx| build_editor(buffer.clone(), cx))
.root(cx);
editor.update(cx, |editor, cx| { editor.update(cx, |editor, cx| {
editor.start_transaction_at(now, cx); editor.start_transaction_at(now, cx);
@ -343,10 +349,12 @@ fn test_ime_composition(cx: &mut TestAppContext) {
fn test_selection_with_mouse(cx: &mut TestAppContext) { fn test_selection_with_mouse(cx: &mut TestAppContext) {
init_test(cx, |_| {}); init_test(cx, |_| {});
let (_, editor) = cx.add_window(|cx| { let editor = cx
.add_window(|cx| {
let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx); let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
build_editor(buffer, cx) build_editor(buffer, cx)
}); })
.root(cx);
editor.update(cx, |view, cx| { editor.update(cx, |view, cx| {
view.begin_selection(DisplayPoint::new(2, 2), false, 1, cx); view.begin_selection(DisplayPoint::new(2, 2), false, 1, cx);
}); });
@ -410,10 +418,12 @@ fn test_selection_with_mouse(cx: &mut TestAppContext) {
fn test_canceling_pending_selection(cx: &mut TestAppContext) { fn test_canceling_pending_selection(cx: &mut TestAppContext) {
init_test(cx, |_| {}); init_test(cx, |_| {});
let (_, view) = cx.add_window(|cx| { let view = cx
.add_window(|cx| {
let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx); let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
build_editor(buffer, cx) build_editor(buffer, cx)
}); })
.root(cx);
view.update(cx, |view, cx| { view.update(cx, |view, cx| {
view.begin_selection(DisplayPoint::new(2, 2), false, 1, cx); view.begin_selection(DisplayPoint::new(2, 2), false, 1, cx);
@ -456,10 +466,12 @@ fn test_clone(cx: &mut TestAppContext) {
true, true,
); );
let (_, editor) = cx.add_window(|cx| { let editor = cx
.add_window(|cx| {
let buffer = MultiBuffer::build_simple(&text, cx); let buffer = MultiBuffer::build_simple(&text, cx);
build_editor(buffer, cx) build_editor(buffer, cx)
}); })
.root(cx);
editor.update(cx, |editor, cx| { editor.update(cx, |editor, cx| {
editor.change_selections(None, cx, |s| s.select_ranges(selection_ranges.clone())); editor.change_selections(None, cx, |s| s.select_ranges(selection_ranges.clone()));
@ -473,9 +485,11 @@ fn test_clone(cx: &mut TestAppContext) {
); );
}); });
let (_, cloned_editor) = editor.update(cx, |editor, cx| { let cloned_editor = editor
.update(cx, |editor, cx| {
cx.add_window(Default::default(), |cx| editor.clone(cx)) cx.add_window(Default::default(), |cx| editor.clone(cx))
}); })
.root(cx);
let snapshot = editor.update(cx, |e, cx| e.snapshot(cx)); let snapshot = editor.update(cx, |e, cx| e.snapshot(cx));
let cloned_snapshot = cloned_editor.update(cx, |e, cx| e.snapshot(cx)); let cloned_snapshot = cloned_editor.update(cx, |e, cx| e.snapshot(cx));
@ -509,9 +523,10 @@ async fn test_navigation_history(cx: &mut TestAppContext) {
let fs = FakeFs::new(cx.background()); let fs = FakeFs::new(cx.background());
let project = Project::test(fs, [], cx).await; let project = Project::test(fs, [], cx).await;
let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx)); let window = cx.add_window(|cx| Workspace::test_new(project, cx));
let workspace = window.root(cx);
let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
cx.add_view(window_id, |cx| { window.add_view(cx, |cx| {
let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx); let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
let mut editor = build_editor(buffer.clone(), cx); let mut editor = build_editor(buffer.clone(), cx);
let handle = cx.handle(); let handle = cx.handle();
@ -618,10 +633,12 @@ async fn test_navigation_history(cx: &mut TestAppContext) {
fn test_cancel(cx: &mut TestAppContext) { fn test_cancel(cx: &mut TestAppContext) {
init_test(cx, |_| {}); init_test(cx, |_| {});
let (_, view) = cx.add_window(|cx| { let view = cx
.add_window(|cx| {
let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx); let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
build_editor(buffer, cx) build_editor(buffer, cx)
}); })
.root(cx);
view.update(cx, |view, cx| { view.update(cx, |view, cx| {
view.begin_selection(DisplayPoint::new(3, 4), false, 1, cx); view.begin_selection(DisplayPoint::new(3, 4), false, 1, cx);
@ -661,7 +678,8 @@ fn test_cancel(cx: &mut TestAppContext) {
fn test_fold_action(cx: &mut TestAppContext) { fn test_fold_action(cx: &mut TestAppContext) {
init_test(cx, |_| {}); init_test(cx, |_| {});
let (_, view) = cx.add_window(|cx| { let view = cx
.add_window(|cx| {
let buffer = MultiBuffer::build_simple( let buffer = MultiBuffer::build_simple(
&" &"
impl Foo { impl Foo {
@ -684,7 +702,8 @@ fn test_fold_action(cx: &mut TestAppContext) {
cx, cx,
); );
build_editor(buffer.clone(), cx) build_editor(buffer.clone(), cx)
}); })
.root(cx);
view.update(cx, |view, cx| { view.update(cx, |view, cx| {
view.change_selections(None, cx, |s| { view.change_selections(None, cx, |s| {
@ -752,7 +771,9 @@ fn test_move_cursor(cx: &mut TestAppContext) {
init_test(cx, |_| {}); init_test(cx, |_| {});
let buffer = cx.update(|cx| MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx)); let buffer = cx.update(|cx| MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx));
let (_, view) = cx.add_window(|cx| build_editor(buffer.clone(), cx)); let view = cx
.add_window(|cx| build_editor(buffer.clone(), cx))
.root(cx);
buffer.update(cx, |buffer, cx| { buffer.update(cx, |buffer, cx| {
buffer.edit( buffer.edit(
@ -827,10 +848,12 @@ fn test_move_cursor(cx: &mut TestAppContext) {
fn test_move_cursor_multibyte(cx: &mut TestAppContext) { fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
init_test(cx, |_| {}); init_test(cx, |_| {});
let (_, view) = cx.add_window(|cx| { let view = cx
.add_window(|cx| {
let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcde\nαβγδε\n", cx); let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcde\nαβγδε\n", cx);
build_editor(buffer.clone(), cx) build_editor(buffer.clone(), cx)
}); })
.root(cx);
assert_eq!('ⓐ'.len_utf8(), 3); assert_eq!('ⓐ'.len_utf8(), 3);
assert_eq!('α'.len_utf8(), 2); assert_eq!('α'.len_utf8(), 2);
@ -932,10 +955,12 @@ fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) { fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
init_test(cx, |_| {}); init_test(cx, |_| {});
let (_, view) = cx.add_window(|cx| { let view = cx
.add_window(|cx| {
let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx); let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx);
build_editor(buffer.clone(), cx) build_editor(buffer.clone(), cx)
}); })
.root(cx);
view.update(cx, |view, cx| { view.update(cx, |view, cx| {
view.change_selections(None, cx, |s| { view.change_selections(None, cx, |s| {
s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]); s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]);
@ -982,10 +1007,12 @@ fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
fn test_beginning_end_of_line(cx: &mut TestAppContext) { fn test_beginning_end_of_line(cx: &mut TestAppContext) {
init_test(cx, |_| {}); init_test(cx, |_| {});
let (_, view) = cx.add_window(|cx| { let view = cx
.add_window(|cx| {
let buffer = MultiBuffer::build_simple("abc\n def", cx); let buffer = MultiBuffer::build_simple("abc\n def", cx);
build_editor(buffer, cx) build_editor(buffer, cx)
}); })
.root(cx);
view.update(cx, |view, cx| { view.update(cx, |view, cx| {
view.change_selections(None, cx, |s| { view.change_selections(None, cx, |s| {
s.select_display_ranges([ s.select_display_ranges([
@ -1145,10 +1172,12 @@ fn test_beginning_end_of_line(cx: &mut TestAppContext) {
fn test_prev_next_word_boundary(cx: &mut TestAppContext) { fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
init_test(cx, |_| {}); init_test(cx, |_| {});
let (_, view) = cx.add_window(|cx| { let view = cx
.add_window(|cx| {
let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx); let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx);
build_editor(buffer, cx) build_editor(buffer, cx)
}); })
.root(cx);
view.update(cx, |view, cx| { view.update(cx, |view, cx| {
view.change_selections(None, cx, |s| { view.change_selections(None, cx, |s| {
s.select_display_ranges([ s.select_display_ranges([
@ -1197,10 +1226,13 @@ fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) { fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
init_test(cx, |_| {}); init_test(cx, |_| {});
let (_, view) = cx.add_window(|cx| { let view = cx
let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx); .add_window(|cx| {
let buffer =
MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx);
build_editor(buffer, cx) build_editor(buffer, cx)
}); })
.root(cx);
view.update(cx, |view, cx| { view.update(cx, |view, cx| {
view.set_wrap_width(Some(140.), cx); view.set_wrap_width(Some(140.), cx);
@ -1257,7 +1289,8 @@ async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut gpui::TestAppCon
let mut cx = EditorTestContext::new(cx).await; let mut cx = EditorTestContext::new(cx).await;
let line_height = cx.editor(|editor, cx| editor.style(cx).text.line_height(cx.font_cache())); let line_height = cx.editor(|editor, cx| editor.style(cx).text.line_height(cx.font_cache()));
cx.simulate_window_resize(cx.window_id, vec2f(100., 4. * line_height)); let window = cx.window;
window.simulate_resize(vec2f(100., 4. * line_height), &mut cx);
cx.set_state( cx.set_state(
&r#"ˇone &r#"ˇone
@ -1368,7 +1401,8 @@ async fn test_scroll_page_up_page_down(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {}); init_test(cx, |_| {});
let mut cx = EditorTestContext::new(cx).await; let mut cx = EditorTestContext::new(cx).await;
let line_height = cx.editor(|editor, cx| editor.style(cx).text.line_height(cx.font_cache())); let line_height = cx.editor(|editor, cx| editor.style(cx).text.line_height(cx.font_cache()));
cx.simulate_window_resize(cx.window_id, vec2f(1000., 4. * line_height + 0.5)); let window = cx.window;
window.simulate_resize(vec2f(1000., 4. * line_height + 0.5), &mut cx);
cx.set_state( cx.set_state(
&r#"ˇone &r#"ˇone
@ -1406,7 +1440,8 @@ async fn test_move_page_up_page_down(cx: &mut gpui::TestAppContext) {
let mut cx = EditorTestContext::new(cx).await; let mut cx = EditorTestContext::new(cx).await;
let line_height = cx.editor(|editor, cx| editor.style(cx).text.line_height(cx.font_cache())); let line_height = cx.editor(|editor, cx| editor.style(cx).text.line_height(cx.font_cache()));
cx.simulate_window_resize(cx.window_id, vec2f(100., 4. * line_height)); let window = cx.window;
window.simulate_resize(vec2f(100., 4. * line_height), &mut cx);
cx.set_state( cx.set_state(
&r#" &r#"
@ -1530,10 +1565,12 @@ async fn test_delete_to_beginning_of_line(cx: &mut gpui::TestAppContext) {
fn test_delete_to_word_boundary(cx: &mut TestAppContext) { fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
init_test(cx, |_| {}); init_test(cx, |_| {});
let (_, view) = cx.add_window(|cx| { let view = cx
.add_window(|cx| {
let buffer = MultiBuffer::build_simple("one two three four", cx); let buffer = MultiBuffer::build_simple("one two three four", cx);
build_editor(buffer.clone(), cx) build_editor(buffer.clone(), cx)
}); })
.root(cx);
view.update(cx, |view, cx| { view.update(cx, |view, cx| {
view.change_selections(None, cx, |s| { view.change_selections(None, cx, |s| {
@ -1566,10 +1603,12 @@ fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
fn test_newline(cx: &mut TestAppContext) { fn test_newline(cx: &mut TestAppContext) {
init_test(cx, |_| {}); init_test(cx, |_| {});
let (_, view) = cx.add_window(|cx| { let view = cx
.add_window(|cx| {
let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx); let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx);
build_editor(buffer.clone(), cx) build_editor(buffer.clone(), cx)
}); })
.root(cx);
view.update(cx, |view, cx| { view.update(cx, |view, cx| {
view.change_selections(None, cx, |s| { view.change_selections(None, cx, |s| {
@ -1589,7 +1628,8 @@ fn test_newline(cx: &mut TestAppContext) {
fn test_newline_with_old_selections(cx: &mut TestAppContext) { fn test_newline_with_old_selections(cx: &mut TestAppContext) {
init_test(cx, |_| {}); init_test(cx, |_| {});
let (_, editor) = cx.add_window(|cx| { let editor = cx
.add_window(|cx| {
let buffer = MultiBuffer::build_simple( let buffer = MultiBuffer::build_simple(
" "
a a
@ -1612,7 +1652,8 @@ fn test_newline_with_old_selections(cx: &mut TestAppContext) {
]) ])
}); });
editor editor
}); })
.root(cx);
editor.update(cx, |editor, cx| { editor.update(cx, |editor, cx| {
// Edit the buffer directly, deleting ranges surrounding the editor's selections // Edit the buffer directly, deleting ranges surrounding the editor's selections
@ -1817,12 +1858,14 @@ async fn test_newline_comments(cx: &mut gpui::TestAppContext) {
fn test_insert_with_old_selections(cx: &mut TestAppContext) { fn test_insert_with_old_selections(cx: &mut TestAppContext) {
init_test(cx, |_| {}); init_test(cx, |_| {});
let (_, editor) = cx.add_window(|cx| { let editor = cx
.add_window(|cx| {
let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx); let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
let mut editor = build_editor(buffer.clone(), cx); let mut editor = build_editor(buffer.clone(), cx);
editor.change_selections(None, cx, |s| s.select_ranges([3..4, 11..12, 19..20])); editor.change_selections(None, cx, |s| s.select_ranges([3..4, 11..12, 19..20]));
editor editor
}); })
.root(cx);
editor.update(cx, |editor, cx| { editor.update(cx, |editor, cx| {
// Edit the buffer directly, deleting ranges surrounding the editor's selections // Edit the buffer directly, deleting ranges surrounding the editor's selections
@ -2329,10 +2372,12 @@ async fn test_delete(cx: &mut gpui::TestAppContext) {
fn test_delete_line(cx: &mut TestAppContext) { fn test_delete_line(cx: &mut TestAppContext) {
init_test(cx, |_| {}); init_test(cx, |_| {});
let (_, view) = cx.add_window(|cx| { let view = cx
.add_window(|cx| {
let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx); let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
build_editor(buffer, cx) build_editor(buffer, cx)
}); })
.root(cx);
view.update(cx, |view, cx| { view.update(cx, |view, cx| {
view.change_selections(None, cx, |s| { view.change_selections(None, cx, |s| {
s.select_display_ranges([ s.select_display_ranges([
@ -2352,10 +2397,12 @@ fn test_delete_line(cx: &mut TestAppContext) {
); );
}); });
let (_, view) = cx.add_window(|cx| { let view = cx
.add_window(|cx| {
let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx); let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
build_editor(buffer, cx) build_editor(buffer, cx)
}); })
.root(cx);
view.update(cx, |view, cx| { view.update(cx, |view, cx| {
view.change_selections(None, cx, |s| { view.change_selections(None, cx, |s| {
s.select_display_ranges([DisplayPoint::new(2, 0)..DisplayPoint::new(0, 1)]) s.select_display_ranges([DisplayPoint::new(2, 0)..DisplayPoint::new(0, 1)])
@ -2650,14 +2697,94 @@ async fn test_manipulate_lines_with_multi_selection(cx: &mut TestAppContext) {
"}); "});
} }
#[gpui::test]
async fn test_manipulate_text(cx: &mut TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorTestContext::new(cx).await;
// Test convert_to_upper_case()
cx.set_state(indoc! {"
«hello worldˇ»
"});
cx.update_editor(|e, cx| e.convert_to_upper_case(&ConvertToUpperCase, cx));
cx.assert_editor_state(indoc! {"
«HELLO WORLDˇ»
"});
// Test convert_to_lower_case()
cx.set_state(indoc! {"
«HELLO WORLDˇ»
"});
cx.update_editor(|e, cx| e.convert_to_lower_case(&ConvertToLowerCase, cx));
cx.assert_editor_state(indoc! {"
«hello worldˇ»
"});
// From here on out, test more complex cases of manipulate_text()
// Test no selection case - should affect words cursors are in
// Cursor at beginning, middle, and end of word
cx.set_state(indoc! {"
ˇhello big beauˇtiful worldˇ
"});
cx.update_editor(|e, cx| e.convert_to_upper_case(&ConvertToUpperCase, cx));
cx.assert_editor_state(indoc! {"
«HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
"});
// Test multiple selections on a single line and across multiple lines
cx.set_state(indoc! {"
«Theˇ» quick «brown
foxˇ» jumps «overˇ»
the «lazyˇ» dog
"});
cx.update_editor(|e, cx| e.convert_to_upper_case(&ConvertToUpperCase, cx));
cx.assert_editor_state(indoc! {"
«THEˇ» quick «BROWN
FOXˇ» jumps «OVERˇ»
the «LAZYˇ» dog
"});
// Test case where text length grows
cx.set_state(indoc! {"
«tschüߡ»
"});
cx.update_editor(|e, cx| e.convert_to_upper_case(&ConvertToUpperCase, cx));
cx.assert_editor_state(indoc! {"
«TSCHÜSSˇ»
"});
// Test to make sure we don't crash when text shrinks
cx.set_state(indoc! {"
aaa_bbbˇ
"});
cx.update_editor(|e, cx| e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, cx));
cx.assert_editor_state(indoc! {"
«aaaBbbˇ»
"});
// Test to make sure we all aware of the fact that each word can grow and shrink
// Final selections should be aware of this fact
cx.set_state(indoc! {"
aaa_bˇbb bbˇb_ccc ˇccc_ddd
"});
cx.update_editor(|e, cx| e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, cx));
cx.assert_editor_state(indoc! {"
«aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
"});
}
#[gpui::test] #[gpui::test]
fn test_duplicate_line(cx: &mut TestAppContext) { fn test_duplicate_line(cx: &mut TestAppContext) {
init_test(cx, |_| {}); init_test(cx, |_| {});
let (_, view) = cx.add_window(|cx| { let view = cx
.add_window(|cx| {
let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx); let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
build_editor(buffer, cx) build_editor(buffer, cx)
}); })
.root(cx);
view.update(cx, |view, cx| { view.update(cx, |view, cx| {
view.change_selections(None, cx, |s| { view.change_selections(None, cx, |s| {
s.select_display_ranges([ s.select_display_ranges([
@ -2680,10 +2807,12 @@ fn test_duplicate_line(cx: &mut TestAppContext) {
); );
}); });
let (_, view) = cx.add_window(|cx| { let view = cx
.add_window(|cx| {
let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx); let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
build_editor(buffer, cx) build_editor(buffer, cx)
}); })
.root(cx);
view.update(cx, |view, cx| { view.update(cx, |view, cx| {
view.change_selections(None, cx, |s| { view.change_selections(None, cx, |s| {
s.select_display_ranges([ s.select_display_ranges([
@ -2707,10 +2836,12 @@ fn test_duplicate_line(cx: &mut TestAppContext) {
fn test_move_line_up_down(cx: &mut TestAppContext) { fn test_move_line_up_down(cx: &mut TestAppContext) {
init_test(cx, |_| {}); init_test(cx, |_| {});
let (_, view) = cx.add_window(|cx| { let view = cx
.add_window(|cx| {
let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx); let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
build_editor(buffer, cx) build_editor(buffer, cx)
}); })
.root(cx);
view.update(cx, |view, cx| { view.update(cx, |view, cx| {
view.fold_ranges( view.fold_ranges(
vec![ vec![
@ -2806,10 +2937,12 @@ fn test_move_line_up_down(cx: &mut TestAppContext) {
fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) { fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
init_test(cx, |_| {}); init_test(cx, |_| {});
let (_, editor) = cx.add_window(|cx| { let editor = cx
.add_window(|cx| {
let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx); let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
build_editor(buffer, cx) build_editor(buffer, cx)
}); })
.root(cx);
editor.update(cx, |editor, cx| { editor.update(cx, |editor, cx| {
let snapshot = editor.buffer.read(cx).snapshot(cx); let snapshot = editor.buffer.read(cx).snapshot(cx);
editor.insert_blocks( editor.insert_blocks(
@ -2834,8 +2967,7 @@ fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
fn test_transpose(cx: &mut TestAppContext) { fn test_transpose(cx: &mut TestAppContext) {
init_test(cx, |_| {}); init_test(cx, |_| {});
_ = cx _ = cx.add_window(|cx| {
.add_window(|cx| {
let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), cx); let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), cx);
editor.change_selections(None, cx, |s| s.select_ranges([1..1])); editor.change_selections(None, cx, |s| s.select_ranges([1..1]));
@ -2852,11 +2984,9 @@ fn test_transpose(cx: &mut TestAppContext) {
assert_eq!(editor.selections.ranges(cx), [3..3]); assert_eq!(editor.selections.ranges(cx), [3..3]);
editor editor
}) });
.1;
_ = cx _ = cx.add_window(|cx| {
.add_window(|cx| {
let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), cx); let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), cx);
editor.change_selections(None, cx, |s| s.select_ranges([3..3])); editor.change_selections(None, cx, |s| s.select_ranges([3..3]));
@ -2878,11 +3008,9 @@ fn test_transpose(cx: &mut TestAppContext) {
assert_eq!(editor.selections.ranges(cx), [6..6]); assert_eq!(editor.selections.ranges(cx), [6..6]);
editor editor
}) });
.1;
_ = cx _ = cx.add_window(|cx| {
.add_window(|cx| {
let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), cx); let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), cx);
editor.change_selections(None, cx, |s| s.select_ranges([1..1, 2..2, 4..4])); editor.change_selections(None, cx, |s| s.select_ranges([1..1, 2..2, 4..4]));
@ -2907,11 +3035,9 @@ fn test_transpose(cx: &mut TestAppContext) {
assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]); assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]);
editor editor
}) });
.1;
_ = cx _ = cx.add_window(|cx| {
.add_window(|cx| {
let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), cx); let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), cx);
editor.change_selections(None, cx, |s| s.select_ranges([4..4])); editor.change_selections(None, cx, |s| s.select_ranges([4..4]));
@ -2928,8 +3054,7 @@ fn test_transpose(cx: &mut TestAppContext) {
assert_eq!(editor.selections.ranges(cx), [11..11]); assert_eq!(editor.selections.ranges(cx), [11..11]);
editor editor
}) });
.1;
} }
#[gpui::test] #[gpui::test]
@ -3132,10 +3257,12 @@ async fn test_paste_multiline(cx: &mut gpui::TestAppContext) {
fn test_select_all(cx: &mut TestAppContext) { fn test_select_all(cx: &mut TestAppContext) {
init_test(cx, |_| {}); init_test(cx, |_| {});
let (_, view) = cx.add_window(|cx| { let view = cx
.add_window(|cx| {
let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx); let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
build_editor(buffer, cx) build_editor(buffer, cx)
}); })
.root(cx);
view.update(cx, |view, cx| { view.update(cx, |view, cx| {
view.select_all(&SelectAll, cx); view.select_all(&SelectAll, cx);
assert_eq!( assert_eq!(
@ -3149,10 +3276,12 @@ fn test_select_all(cx: &mut TestAppContext) {
fn test_select_line(cx: &mut TestAppContext) { fn test_select_line(cx: &mut TestAppContext) {
init_test(cx, |_| {}); init_test(cx, |_| {});
let (_, view) = cx.add_window(|cx| { let view = cx
.add_window(|cx| {
let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx); let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
build_editor(buffer, cx) build_editor(buffer, cx)
}); })
.root(cx);
view.update(cx, |view, cx| { view.update(cx, |view, cx| {
view.change_selections(None, cx, |s| { view.change_selections(None, cx, |s| {
s.select_display_ranges([ s.select_display_ranges([
@ -3196,10 +3325,12 @@ fn test_select_line(cx: &mut TestAppContext) {
fn test_split_selection_into_lines(cx: &mut TestAppContext) { fn test_split_selection_into_lines(cx: &mut TestAppContext) {
init_test(cx, |_| {}); init_test(cx, |_| {});
let (_, view) = cx.add_window(|cx| { let view = cx
.add_window(|cx| {
let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx); let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
build_editor(buffer, cx) build_editor(buffer, cx)
}); })
.root(cx);
view.update(cx, |view, cx| { view.update(cx, |view, cx| {
view.fold_ranges( view.fold_ranges(
vec![ vec![
@ -3267,10 +3398,12 @@ fn test_split_selection_into_lines(cx: &mut TestAppContext) {
fn test_add_selection_above_below(cx: &mut TestAppContext) { fn test_add_selection_above_below(cx: &mut TestAppContext) {
init_test(cx, |_| {}); init_test(cx, |_| {});
let (_, view) = cx.add_window(|cx| { let view = cx
.add_window(|cx| {
let buffer = MultiBuffer::build_simple("abc\ndefghi\n\njk\nlmno\n", cx); let buffer = MultiBuffer::build_simple("abc\ndefghi\n\njk\nlmno\n", cx);
build_editor(buffer, cx) build_editor(buffer, cx)
}); })
.root(cx);
view.update(cx, |view, cx| { view.update(cx, |view, cx| {
view.change_selections(None, cx, |s| { view.change_selections(None, cx, |s| {
@ -3555,7 +3688,7 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) {
let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx)); let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx));
let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
let (_, view) = cx.add_window(|cx| build_editor(buffer, cx)); let view = cx.add_window(|cx| build_editor(buffer, cx)).root(cx);
view.condition(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx)) view.condition(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx))
.await; .await;
@ -3718,7 +3851,7 @@ async fn test_autoindent_selections(cx: &mut gpui::TestAppContext) {
let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx)); let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx));
let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
let (_, editor) = cx.add_window(|cx| build_editor(buffer, cx)); let editor = cx.add_window(|cx| build_editor(buffer, cx)).root(cx);
editor editor
.condition(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx)) .condition(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
.await; .await;
@ -4281,7 +4414,7 @@ async fn test_surround_with_pair(cx: &mut gpui::TestAppContext) {
let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx)); let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx));
let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
let (_, view) = cx.add_window(|cx| build_editor(buffer, cx)); let view = cx.add_window(|cx| build_editor(buffer, cx)).root(cx);
view.condition(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx)) view.condition(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx))
.await; .await;
@ -4429,7 +4562,7 @@ async fn test_delete_autoclose_pair(cx: &mut gpui::TestAppContext) {
let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx)); let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx));
let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
let (_, editor) = cx.add_window(|cx| build_editor(buffer, cx)); let editor = cx.add_window(|cx| build_editor(buffer, cx)).root(cx);
editor editor
.condition(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx)) .condition(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx))
.await; .await;
@ -4519,7 +4652,7 @@ async fn test_snippets(cx: &mut gpui::TestAppContext) {
); );
let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx)); let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
let (_, editor) = cx.add_window(|cx| build_editor(buffer, cx)); let editor = cx.add_window(|cx| build_editor(buffer, cx)).root(cx);
editor.update(cx, |editor, cx| { editor.update(cx, |editor, cx| {
let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap(); let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
@ -4649,7 +4782,7 @@ async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) {
let fake_server = fake_servers.next().await.unwrap(); let fake_server = fake_servers.next().await.unwrap();
let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
let (_, editor) = cx.add_window(|cx| build_editor(buffer, cx)); let editor = cx.add_window(|cx| build_editor(buffer, cx)).root(cx);
editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx)); editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
assert!(cx.read(|cx| editor.is_dirty(cx))); assert!(cx.read(|cx| editor.is_dirty(cx)));
@ -4761,7 +4894,7 @@ async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) {
let fake_server = fake_servers.next().await.unwrap(); let fake_server = fake_servers.next().await.unwrap();
let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
let (_, editor) = cx.add_window(|cx| build_editor(buffer, cx)); let editor = cx.add_window(|cx| build_editor(buffer, cx)).root(cx);
editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx)); editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
assert!(cx.read(|cx| editor.is_dirty(cx))); assert!(cx.read(|cx| editor.is_dirty(cx)));
@ -4875,7 +5008,7 @@ async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) {
let fake_server = fake_servers.next().await.unwrap(); let fake_server = fake_servers.next().await.unwrap();
let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
let (_, editor) = cx.add_window(|cx| build_editor(buffer, cx)); let editor = cx.add_window(|cx| build_editor(buffer, cx)).root(cx);
editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx)); editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
let format = editor.update(cx, |editor, cx| { let format = editor.update(cx, |editor, cx| {
@ -5653,7 +5786,7 @@ fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
multibuffer multibuffer
}); });
let (_, view) = cx.add_window(|cx| build_editor(multibuffer, cx)); let view = cx.add_window(|cx| build_editor(multibuffer, cx)).root(cx);
view.update(cx, |view, cx| { view.update(cx, |view, cx| {
assert_eq!(view.text(cx), "aaaa\nbbbb"); assert_eq!(view.text(cx), "aaaa\nbbbb");
view.change_selections(None, cx, |s| { view.change_selections(None, cx, |s| {
@ -5723,7 +5856,7 @@ fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
multibuffer multibuffer
}); });
let (_, view) = cx.add_window(|cx| build_editor(multibuffer, cx)); let view = cx.add_window(|cx| build_editor(multibuffer, cx)).root(cx);
view.update(cx, |view, cx| { view.update(cx, |view, cx| {
let (expected_text, selection_ranges) = marked_text_ranges( let (expected_text, selection_ranges) = marked_text_ranges(
indoc! {" indoc! {"
@ -5799,7 +5932,8 @@ fn test_refresh_selections(cx: &mut TestAppContext) {
multibuffer multibuffer
}); });
let (_, editor) = cx.add_window(|cx| { let editor = cx
.add_window(|cx| {
let mut editor = build_editor(multibuffer.clone(), cx); let mut editor = build_editor(multibuffer.clone(), cx);
let snapshot = editor.snapshot(cx); let snapshot = editor.snapshot(cx);
editor.change_selections(None, cx, |s| { editor.change_selections(None, cx, |s| {
@ -5814,7 +5948,8 @@ fn test_refresh_selections(cx: &mut TestAppContext) {
] ]
); );
editor editor
}); })
.root(cx);
// Refreshing selections is a no-op when excerpts haven't changed. // Refreshing selections is a no-op when excerpts haven't changed.
editor.update(cx, |editor, cx| { editor.update(cx, |editor, cx| {
@ -5884,7 +6019,8 @@ fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
multibuffer multibuffer
}); });
let (_, editor) = cx.add_window(|cx| { let editor = cx
.add_window(|cx| {
let mut editor = build_editor(multibuffer.clone(), cx); let mut editor = build_editor(multibuffer.clone(), cx);
let snapshot = editor.snapshot(cx); let snapshot = editor.snapshot(cx);
editor.begin_selection(Point::new(1, 3).to_display_point(&snapshot), false, 1, cx); editor.begin_selection(Point::new(1, 3).to_display_point(&snapshot), false, 1, cx);
@ -5893,7 +6029,8 @@ fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
[Point::new(1, 3)..Point::new(1, 3)] [Point::new(1, 3)..Point::new(1, 3)]
); );
editor editor
}); })
.root(cx);
multibuffer.update(cx, |multibuffer, cx| { multibuffer.update(cx, |multibuffer, cx| {
multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx); multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
@ -5956,7 +6093,7 @@ async fn test_extra_newline_insertion(cx: &mut gpui::TestAppContext) {
let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx)); let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx));
let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
let (_, view) = cx.add_window(|cx| build_editor(buffer, cx)); let view = cx.add_window(|cx| build_editor(buffer, cx)).root(cx);
view.condition(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx)) view.condition(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx))
.await; .await;
@ -5992,10 +6129,12 @@ async fn test_extra_newline_insertion(cx: &mut gpui::TestAppContext) {
fn test_highlighted_ranges(cx: &mut TestAppContext) { fn test_highlighted_ranges(cx: &mut TestAppContext) {
init_test(cx, |_| {}); init_test(cx, |_| {});
let (_, editor) = cx.add_window(|cx| { let editor = cx
.add_window(|cx| {
let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx); let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
build_editor(buffer.clone(), cx) build_editor(buffer.clone(), cx)
}); })
.root(cx);
editor.update(cx, |editor, cx| { editor.update(cx, |editor, cx| {
struct Type1; struct Type1;
@ -6084,8 +6223,11 @@ async fn test_following(cx: &mut gpui::TestAppContext) {
.unwrap(); .unwrap();
cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)) cx.add_model(|cx| MultiBuffer::singleton(buffer, cx))
}); });
let (_, leader) = cx.add_window(|cx| build_editor(buffer.clone(), cx)); let leader = cx
let (_, follower) = cx.update(|cx| { .add_window(|cx| build_editor(buffer.clone(), cx))
.root(cx);
let follower = cx
.update(|cx| {
cx.add_window( cx.add_window(
WindowOptions { WindowOptions {
bounds: WindowBounds::Fixed(RectF::from_points(vec2f(0., 0.), vec2f(10., 80.))), bounds: WindowBounds::Fixed(RectF::from_points(vec2f(0., 0.), vec2f(10., 80.))),
@ -6093,7 +6235,8 @@ async fn test_following(cx: &mut gpui::TestAppContext) {
}, },
|cx| build_editor(buffer.clone(), cx), |cx| build_editor(buffer.clone(), cx),
) )
}); })
.root(cx);
let is_still_following = Rc::new(RefCell::new(true)); let is_still_following = Rc::new(RefCell::new(true));
let follower_edit_event_count = Rc::new(RefCell::new(0)); let follower_edit_event_count = Rc::new(RefCell::new(0));
@ -6224,7 +6367,9 @@ async fn test_following_with_multiple_excerpts(cx: &mut gpui::TestAppContext) {
let fs = FakeFs::new(cx.background()); let fs = FakeFs::new(cx.background());
let project = Project::test(fs, ["/file.rs".as_ref()], cx).await; let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); let workspace = cx
.add_window(|cx| Workspace::test_new(project.clone(), cx))
.root(cx);
let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
let leader = pane.update(cx, |_, cx| { let leader = pane.update(cx, |_, cx| {
@ -6968,7 +7113,7 @@ async fn test_copilot_multibuffer(
); );
multibuffer multibuffer
}); });
let (_, editor) = cx.add_window(|cx| build_editor(multibuffer, cx)); let editor = cx.add_window(|cx| build_editor(multibuffer, cx)).root(cx);
handle_copilot_completion_request( handle_copilot_completion_request(
&copilot_lsp, &copilot_lsp,
@ -7098,7 +7243,7 @@ async fn test_copilot_disabled_globs(
); );
multibuffer multibuffer
}); });
let (_, editor) = cx.add_window(|cx| build_editor(multibuffer, cx)); let editor = cx.add_window(|cx| build_editor(multibuffer, cx)).root(cx);
let mut copilot_requests = copilot_lsp let mut copilot_requests = copilot_lsp
.handle_request::<copilot::request::GetCompletions, _, _>(move |_params, _cx| async move { .handle_request::<copilot::request::GetCompletions, _, _>(move |_params, _cx| async move {
@ -7177,7 +7322,9 @@ async fn test_on_type_formatting_not_triggered(cx: &mut gpui::TestAppContext) {
.await; .await;
let project = Project::test(fs, ["/a".as_ref()], cx).await; let project = Project::test(fs, ["/a".as_ref()], cx).await;
project.update(cx, |project, _| project.languages().add(Arc::new(language))); project.update(cx, |project, _| project.languages().add(Arc::new(language)));
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); let workspace = cx
.add_window(|cx| Workspace::test_new(project.clone(), cx))
.root(cx);
let worktree_id = workspace.update(cx, |workspace, cx| { let worktree_id = workspace.update(cx, |workspace, cx| {
workspace.project().read_with(cx, |project, cx| { workspace.project().read_with(cx, |project, cx| {
project.worktrees(cx).next().unwrap().read(cx).id() project.worktrees(cx).next().unwrap().read(cx).id()
@ -7282,7 +7429,7 @@ async fn test_language_server_restart_due_to_settings_change(cx: &mut gpui::Test
.await; .await;
let project = Project::test(fs, ["/a".as_ref()], cx).await; let project = Project::test(fs, ["/a".as_ref()], cx).await;
project.update(cx, |project, _| project.languages().add(Arc::new(language))); project.update(cx, |project, _| project.languages().add(Arc::new(language)));
let (_, _workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); let _window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
let _buffer = project let _buffer = project
.update(cx, |project, cx| { .update(cx, |project, cx| {
project.open_local_buffer("/a/main.rs", cx) project.open_local_buffer("/a/main.rs", cx)

View file

@ -172,6 +172,10 @@ impl EditorElement {
.on_drag(MouseButton::Left, { .on_drag(MouseButton::Left, {
let position_map = position_map.clone(); let position_map = position_map.clone();
move |event, editor, cx| { move |event, editor, cx| {
if event.end {
return;
}
if !Self::mouse_dragged( if !Self::mouse_dragged(
editor, editor,
event.platform_event, event.platform_event,
@ -542,8 +546,20 @@ impl EditorElement {
}); });
} }
let scroll_left =
layout.position_map.snapshot.scroll_position().x() * layout.position_map.em_width;
for (wrap_position, active) in layout.wrap_guides.iter() { for (wrap_position, active) in layout.wrap_guides.iter() {
let x = text_bounds.origin_x() + wrap_position + layout.position_map.em_width / 2.; let x =
(text_bounds.origin_x() + wrap_position + layout.position_map.em_width / 2.)
- scroll_left;
if x < text_bounds.origin_x()
|| (layout.show_scrollbars && x > self.scrollbar_left(&bounds))
{
continue;
}
let color = if *active { let color = if *active {
self.style.active_wrap_guide self.style.active_wrap_guide
} else { } else {
@ -1032,6 +1048,10 @@ impl EditorElement {
scene.pop_layer(); scene.pop_layer();
} }
fn scrollbar_left(&self, bounds: &RectF) -> f32 {
bounds.max_x() - self.style.theme.scrollbar.width
}
fn paint_scrollbar( fn paint_scrollbar(
&mut self, &mut self,
scene: &mut SceneBuilder, scene: &mut SceneBuilder,
@ -1050,7 +1070,7 @@ impl EditorElement {
let top = bounds.min_y(); let top = bounds.min_y();
let bottom = bounds.max_y(); let bottom = bounds.max_y();
let right = bounds.max_x(); let right = bounds.max_x();
let left = right - style.width; let left = self.scrollbar_left(&bounds);
let row_range = &layout.scrollbar_row_range; let row_range = &layout.scrollbar_row_range;
let max_row = layout.max_row as f32 + (row_range.end - row_range.start); let max_row = layout.max_row as f32 + (row_range.end - row_range.start);
@ -1087,8 +1107,6 @@ impl EditorElement {
if layout.is_singleton && scrollbar_settings.selections { if layout.is_singleton && scrollbar_settings.selections {
let start_anchor = Anchor::min(); let start_anchor = Anchor::min();
let end_anchor = Anchor::max(); let end_anchor = Anchor::max();
let mut start_row = None;
let mut end_row = None;
let color = scrollbar_theme.selections; let color = scrollbar_theme.selections;
let border = Border { let border = Border {
width: 1., width: 1.,
@ -1099,10 +1117,9 @@ impl EditorElement {
bottom: false, bottom: false,
left: true, left: true,
}; };
let mut push_region = |start, end| { let mut push_region = |start: DisplayPoint, end: DisplayPoint| {
if let (Some(start_display), Some(end_display)) = (start, end) { let start_y = y_for_row(start.row() as f32);
let start_y = y_for_row(start_display as f32); let mut end_y = y_for_row(end.row() as f32);
let mut end_y = y_for_row(end_display as f32);
if end_y - start_y < 1. { if end_y - start_y < 1. {
end_y = start_y + 1.; end_y = start_y + 1.;
} }
@ -1114,39 +1131,18 @@ impl EditorElement {
border, border,
corner_radius: style.thumb.corner_radius, corner_radius: style.thumb.corner_radius,
}) })
}
}; };
for (row, _) in &editor let background_ranges = editor
.background_highlights_in_range_for::<crate::items::BufferSearchHighlights>( .background_highlight_row_ranges::<crate::items::BufferSearchHighlights>(
start_anchor..end_anchor, start_anchor..end_anchor,
&layout.position_map.snapshot, &layout.position_map.snapshot,
&theme, 50000,
) );
{ for row in background_ranges {
let start_display = row.start; let start = row.start();
let end_display = row.end; let end = row.end();
push_region(*start, *end);
if start_row.is_none() {
assert_eq!(end_row, None);
start_row = Some(start_display.row());
end_row = Some(end_display.row());
continue;
} }
if let Some(current_end) = end_row.as_mut() {
if start_display.row() > *current_end + 1 {
push_region(start_row, end_row);
start_row = Some(start_display.row());
end_row = Some(end_display.row());
} else {
// Merge two hunks.
*current_end = end_display.row();
}
} else {
unreachable!();
}
}
// We might still have a hunk that was not rendered (if there was a search hit on the last line)
push_region(start_row, end_row);
} }
if layout.is_singleton && scrollbar_settings.git_diff { if layout.is_singleton && scrollbar_settings.git_diff {
@ -1235,6 +1231,10 @@ impl EditorElement {
}) })
.on_drag(MouseButton::Left, { .on_drag(MouseButton::Left, {
move |event, editor: &mut Editor, cx| { move |event, editor: &mut Editor, cx| {
if event.end {
return;
}
let y = event.prev_mouse_position.y(); let y = event.prev_mouse_position.y();
let new_y = event.position.y(); let new_y = event.position.y();
if thumb_top < y && y < thumb_bottom { if thumb_top < y && y < thumb_bottom {
@ -2978,10 +2978,12 @@ mod tests {
fn test_layout_line_numbers(cx: &mut TestAppContext) { fn test_layout_line_numbers(cx: &mut TestAppContext) {
init_test(cx, |_| {}); init_test(cx, |_| {});
let (_, editor) = cx.add_window(|cx| { let editor = cx
.add_window(|cx| {
let buffer = MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx); let buffer = MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx);
Editor::new(EditorMode::Full, buffer, None, None, cx) Editor::new(EditorMode::Full, buffer, None, None, cx)
}); })
.root(cx);
let element = EditorElement::new(editor.read_with(cx, |editor, cx| editor.style(cx))); let element = EditorElement::new(editor.read_with(cx, |editor, cx| editor.style(cx)));
let layouts = editor.update(cx, |editor, cx| { let layouts = editor.update(cx, |editor, cx| {
@ -2997,10 +2999,12 @@ mod tests {
fn test_layout_with_placeholder_text_and_blocks(cx: &mut TestAppContext) { fn test_layout_with_placeholder_text_and_blocks(cx: &mut TestAppContext) {
init_test(cx, |_| {}); init_test(cx, |_| {});
let (_, editor) = cx.add_window(|cx| { let editor = cx
.add_window(|cx| {
let buffer = MultiBuffer::build_simple("", cx); let buffer = MultiBuffer::build_simple("", cx);
Editor::new(EditorMode::Full, buffer, None, None, cx) Editor::new(EditorMode::Full, buffer, None, None, cx)
}); })
.root(cx);
editor.update(cx, |editor, cx| { editor.update(cx, |editor, cx| {
editor.set_placeholder_text("hello", cx); editor.set_placeholder_text("hello", cx);
@ -3214,10 +3218,12 @@ mod tests {
info!( info!(
"Creating editor with mode {editor_mode:?}, width {editor_width} and text '{input_text}'" "Creating editor with mode {editor_mode:?}, width {editor_width} and text '{input_text}'"
); );
let (_, editor) = cx.add_window(|cx| { let editor = cx
.add_window(|cx| {
let buffer = MultiBuffer::build_simple(&input_text, cx); let buffer = MultiBuffer::build_simple(&input_text, cx);
Editor::new(editor_mode, buffer, None, None, cx) Editor::new(editor_mode, buffer, None, None, cx)
}); })
.root(cx);
let mut element = EditorElement::new(editor.read_with(cx, |editor, cx| editor.style(cx))); let mut element = EditorElement::new(editor.read_with(cx, |editor, cx| editor.style(cx)));
let (_, layout_state) = editor.update(cx, |editor, cx| { let (_, layout_state) = editor.update(cx, |editor, cx| {

View file

@ -571,7 +571,6 @@ fn new_update_task(
if let Some(buffer) = if let Some(buffer) =
refresh_multi_buffer.buffer(pending_refresh_query.buffer_id) refresh_multi_buffer.buffer(pending_refresh_query.buffer_id)
{ {
drop(refresh_multi_buffer);
editor.inlay_hint_cache.update_tasks.insert( editor.inlay_hint_cache.update_tasks.insert(
pending_refresh_query.excerpt_id, pending_refresh_query.excerpt_id,
UpdateTask { UpdateTask {
@ -1136,7 +1135,9 @@ mod tests {
) )
.await; .await;
let project = Project::test(fs, ["/a".as_ref()], cx).await; let project = Project::test(fs, ["/a".as_ref()], cx).await;
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); let workspace = cx
.add_window(|cx| Workspace::test_new(project.clone(), cx))
.root(cx);
let worktree_id = workspace.update(cx, |workspace, cx| { let worktree_id = workspace.update(cx, |workspace, cx| {
workspace.project().read_with(cx, |project, cx| { workspace.project().read_with(cx, |project, cx| {
project.worktrees(cx).next().unwrap().read(cx).id() project.worktrees(cx).next().unwrap().read(cx).id()
@ -1836,7 +1837,9 @@ mod tests {
.await; .await;
let project = Project::test(fs, ["/a".as_ref()], cx).await; let project = Project::test(fs, ["/a".as_ref()], cx).await;
project.update(cx, |project, _| project.languages().add(Arc::new(language))); project.update(cx, |project, _| project.languages().add(Arc::new(language)));
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); let workspace = cx
.add_window(|cx| Workspace::test_new(project.clone(), cx))
.root(cx);
let worktree_id = workspace.update(cx, |workspace, cx| { let worktree_id = workspace.update(cx, |workspace, cx| {
workspace.project().read_with(cx, |project, cx| { workspace.project().read_with(cx, |project, cx| {
project.worktrees(cx).next().unwrap().read(cx).id() project.worktrees(cx).next().unwrap().read(cx).id()
@ -1989,7 +1992,9 @@ mod tests {
project.update(cx, |project, _| { project.update(cx, |project, _| {
project.languages().add(Arc::clone(&language)) project.languages().add(Arc::clone(&language))
}); });
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); let workspace = cx
.add_window(|cx| Workspace::test_new(project.clone(), cx))
.root(cx);
let worktree_id = workspace.update(cx, |workspace, cx| { let worktree_id = workspace.update(cx, |workspace, cx| {
workspace.project().read_with(cx, |project, cx| { workspace.project().read_with(cx, |project, cx| {
project.worktrees(cx).next().unwrap().read(cx).id() project.worktrees(cx).next().unwrap().read(cx).id()
@ -2075,8 +2080,9 @@ mod tests {
deterministic.run_until_parked(); deterministic.run_until_parked();
cx.foreground().run_until_parked(); cx.foreground().run_until_parked();
let (_, editor) = let editor = cx
cx.add_window(|cx| Editor::for_multibuffer(multibuffer, Some(project.clone()), cx)); .add_window(|cx| Editor::for_multibuffer(multibuffer, Some(project.clone()), cx))
.root(cx);
let editor_edited = Arc::new(AtomicBool::new(false)); let editor_edited = Arc::new(AtomicBool::new(false));
let fake_server = fake_servers.next().await.unwrap(); let fake_server = fake_servers.next().await.unwrap();
let closure_editor_edited = Arc::clone(&editor_edited); let closure_editor_edited = Arc::clone(&editor_edited);
@ -2328,7 +2334,9 @@ all hints should be invalidated and requeried for all of its visible excerpts"
project.update(cx, |project, _| { project.update(cx, |project, _| {
project.languages().add(Arc::clone(&language)) project.languages().add(Arc::clone(&language))
}); });
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); let workspace = cx
.add_window(|cx| Workspace::test_new(project.clone(), cx))
.root(cx);
let worktree_id = workspace.update(cx, |workspace, cx| { let worktree_id = workspace.update(cx, |workspace, cx| {
workspace.project().read_with(cx, |project, cx| { workspace.project().read_with(cx, |project, cx| {
project.worktrees(cx).next().unwrap().read(cx).id() project.worktrees(cx).next().unwrap().read(cx).id()
@ -2373,8 +2381,9 @@ all hints should be invalidated and requeried for all of its visible excerpts"
deterministic.run_until_parked(); deterministic.run_until_parked();
cx.foreground().run_until_parked(); cx.foreground().run_until_parked();
let (_, editor) = let editor = cx
cx.add_window(|cx| Editor::for_multibuffer(multibuffer, Some(project.clone()), cx)); .add_window(|cx| Editor::for_multibuffer(multibuffer, Some(project.clone()), cx))
.root(cx);
let editor_edited = Arc::new(AtomicBool::new(false)); let editor_edited = Arc::new(AtomicBool::new(false));
let fake_server = fake_servers.next().await.unwrap(); let fake_server = fake_servers.next().await.unwrap();
let closure_editor_edited = Arc::clone(&editor_edited); let closure_editor_edited = Arc::clone(&editor_edited);
@ -2562,7 +2571,9 @@ all hints should be invalidated and requeried for all of its visible excerpts"
let project = Project::test(fs, ["/a".as_ref()], cx).await; let project = Project::test(fs, ["/a".as_ref()], cx).await;
project.update(cx, |project, _| project.languages().add(Arc::new(language))); project.update(cx, |project, _| project.languages().add(Arc::new(language)));
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); let workspace = cx
.add_window(|cx| Workspace::test_new(project.clone(), cx))
.root(cx);
let worktree_id = workspace.update(cx, |workspace, cx| { let worktree_id = workspace.update(cx, |workspace, cx| {
workspace.project().read_with(cx, |project, cx| { workspace.project().read_with(cx, |project, cx| {
project.worktrees(cx).next().unwrap().read(cx).id() project.worktrees(cx).next().unwrap().read(cx).id()

View file

@ -28,7 +28,10 @@ use std::{
path::{Path, PathBuf}, path::{Path, PathBuf},
}; };
use text::Selection; use text::Selection;
use util::{paths::FILE_ROW_COLUMN_DELIMITER, ResultExt, TryFutureExt}; use util::{
paths::{PathExt, FILE_ROW_COLUMN_DELIMITER},
ResultExt, TryFutureExt,
};
use workspace::item::{BreadcrumbText, FollowableItemHandle}; use workspace::item::{BreadcrumbText, FollowableItemHandle};
use workspace::{ use workspace::{
item::{FollowableItem, Item, ItemEvent, ItemHandle, ProjectItem}, item::{FollowableItem, Item, ItemEvent, ItemHandle, ProjectItem},
@ -546,9 +549,7 @@ impl Item for Editor {
.and_then(|f| f.as_local())? .and_then(|f| f.as_local())?
.abs_path(cx); .abs_path(cx);
let file_path = util::paths::compact(&file_path) let file_path = file_path.compact().to_string_lossy().to_string();
.to_string_lossy()
.to_string();
Some(file_path.into()) Some(file_path.into())
} }

View file

@ -69,7 +69,8 @@ impl<'a> EditorLspTestContext<'a> {
.insert_tree("/root", json!({ "dir": { file_name.clone(): "" }})) .insert_tree("/root", json!({ "dir": { file_name.clone(): "" }}))
.await; .await;
let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
let workspace = window.root(cx);
project project
.update(cx, |project, cx| { .update(cx, |project, cx| {
project.find_or_create_local_worktree("/root", true, cx) project.find_or_create_local_worktree("/root", true, cx)
@ -98,7 +99,7 @@ impl<'a> EditorLspTestContext<'a> {
Self { Self {
cx: EditorTestContext { cx: EditorTestContext {
cx, cx,
window_id, window: window.into(),
editor, editor,
}, },
lsp, lsp,

View file

@ -3,7 +3,8 @@ use crate::{
}; };
use futures::Future; use futures::Future;
use gpui::{ use gpui::{
keymap_matcher::Keystroke, AppContext, ContextHandle, ModelContext, ViewContext, ViewHandle, keymap_matcher::Keystroke, AnyWindowHandle, AppContext, ContextHandle, ModelContext,
ViewContext, ViewHandle,
}; };
use indoc::indoc; use indoc::indoc;
use language::{Buffer, BufferSnapshot}; use language::{Buffer, BufferSnapshot};
@ -21,7 +22,7 @@ use super::build_editor;
pub struct EditorTestContext<'a> { pub struct EditorTestContext<'a> {
pub cx: &'a mut gpui::TestAppContext, pub cx: &'a mut gpui::TestAppContext,
pub window_id: usize, pub window: AnyWindowHandle,
pub editor: ViewHandle<Editor>, pub editor: ViewHandle<Editor>,
} }
@ -32,16 +33,14 @@ impl<'a> EditorTestContext<'a> {
let buffer = project let buffer = project
.update(cx, |project, cx| project.create_buffer("", None, cx)) .update(cx, |project, cx| project.create_buffer("", None, cx))
.unwrap(); .unwrap();
let (window_id, editor) = cx.update(|cx| { let window = cx.add_window(|cx| {
cx.add_window(Default::default(), |cx| {
cx.focus_self(); cx.focus_self();
build_editor(MultiBuffer::build_from_buffer(buffer, cx), cx) build_editor(MultiBuffer::build_from_buffer(buffer, cx), cx)
})
}); });
let editor = window.root(cx);
Self { Self {
cx, cx,
window_id, window: window.into(),
editor, editor,
} }
} }
@ -113,7 +112,8 @@ impl<'a> EditorTestContext<'a> {
let keystroke_under_test_handle = let keystroke_under_test_handle =
self.add_assertion_context(format!("Simulated Keystroke: {:?}", keystroke_text)); self.add_assertion_context(format!("Simulated Keystroke: {:?}", keystroke_text));
let keystroke = Keystroke::parse(keystroke_text).unwrap(); let keystroke = Keystroke::parse(keystroke_text).unwrap();
self.cx.dispatch_keystroke(self.window_id, keystroke, false);
self.cx.dispatch_keystroke(self.window, keystroke, false);
keystroke_under_test_handle keystroke_under_test_handle
} }

View file

@ -617,8 +617,9 @@ mod tests {
.await; .await;
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx)); let window = cx.add_window(|cx| Workspace::test_new(project, cx));
cx.dispatch_action(window_id, Toggle); let workspace = window.root(cx);
cx.dispatch_action(window.into(), Toggle);
let finder = cx.read(|cx| workspace.read(cx).modal::<FileFinder>().unwrap()); let finder = cx.read(|cx| workspace.read(cx).modal::<FileFinder>().unwrap());
finder finder
@ -631,8 +632,8 @@ mod tests {
}); });
let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone()); let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone());
cx.dispatch_action(window_id, SelectNext); cx.dispatch_action(window.into(), SelectNext);
cx.dispatch_action(window_id, Confirm); cx.dispatch_action(window.into(), Confirm);
active_pane active_pane
.condition(cx, |pane, _| pane.active_item().is_some()) .condition(cx, |pane, _| pane.active_item().is_some())
.await; .await;
@ -671,8 +672,9 @@ mod tests {
.await; .await;
let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx)); let window = cx.add_window(|cx| Workspace::test_new(project, cx));
cx.dispatch_action(window_id, Toggle); let workspace = window.root(cx);
cx.dispatch_action(window.into(), Toggle);
let finder = cx.read(|cx| workspace.read(cx).modal::<FileFinder>().unwrap()); let finder = cx.read(|cx| workspace.read(cx).modal::<FileFinder>().unwrap());
let file_query = &first_file_name[..3]; let file_query = &first_file_name[..3];
@ -704,8 +706,8 @@ mod tests {
}); });
let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone()); let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone());
cx.dispatch_action(window_id, SelectNext); cx.dispatch_action(window.into(), SelectNext);
cx.dispatch_action(window_id, Confirm); cx.dispatch_action(window.into(), Confirm);
active_pane active_pane
.condition(cx, |pane, _| pane.active_item().is_some()) .condition(cx, |pane, _| pane.active_item().is_some())
.await; .await;
@ -754,8 +756,9 @@ mod tests {
.await; .await;
let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx)); let window = cx.add_window(|cx| Workspace::test_new(project, cx));
cx.dispatch_action(window_id, Toggle); let workspace = window.root(cx);
cx.dispatch_action(window.into(), Toggle);
let finder = cx.read(|cx| workspace.read(cx).modal::<FileFinder>().unwrap()); let finder = cx.read(|cx| workspace.read(cx).modal::<FileFinder>().unwrap());
let file_query = &first_file_name[..3]; let file_query = &first_file_name[..3];
@ -787,8 +790,8 @@ mod tests {
}); });
let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone()); let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone());
cx.dispatch_action(window_id, SelectNext); cx.dispatch_action(window.into(), SelectNext);
cx.dispatch_action(window_id, Confirm); cx.dispatch_action(window.into(), Confirm);
active_pane active_pane
.condition(cx, |pane, _| pane.active_item().is_some()) .condition(cx, |pane, _| pane.active_item().is_some())
.await; .await;
@ -837,8 +840,11 @@ mod tests {
.await; .await;
let project = Project::test(app_state.fs.clone(), ["/dir".as_ref()], cx).await; let project = Project::test(app_state.fs.clone(), ["/dir".as_ref()], cx).await;
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx)); let workspace = cx
let (_, finder) = cx.add_window(|cx| { .add_window(|cx| Workspace::test_new(project, cx))
.root(cx);
let finder = cx
.add_window(|cx| {
Picker::new( Picker::new(
FileFinderDelegate::new( FileFinderDelegate::new(
workspace.downgrade(), workspace.downgrade(),
@ -849,7 +855,8 @@ mod tests {
), ),
cx, cx,
) )
}); })
.root(cx);
let query = test_path_like("hi"); let query = test_path_like("hi");
finder finder
@ -931,8 +938,11 @@ mod tests {
cx, cx,
) )
.await; .await;
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx)); let workspace = cx
let (_, finder) = cx.add_window(|cx| { .add_window(|cx| Workspace::test_new(project, cx))
.root(cx);
let finder = cx
.add_window(|cx| {
Picker::new( Picker::new(
FileFinderDelegate::new( FileFinderDelegate::new(
workspace.downgrade(), workspace.downgrade(),
@ -943,7 +953,8 @@ mod tests {
), ),
cx, cx,
) )
}); })
.root(cx);
finder finder
.update(cx, |f, cx| { .update(cx, |f, cx| {
f.delegate_mut().spawn_search(test_path_like("hi"), cx) f.delegate_mut().spawn_search(test_path_like("hi"), cx)
@ -967,8 +978,11 @@ mod tests {
cx, cx,
) )
.await; .await;
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx)); let workspace = cx
let (_, finder) = cx.add_window(|cx| { .add_window(|cx| Workspace::test_new(project, cx))
.root(cx);
let finder = cx
.add_window(|cx| {
Picker::new( Picker::new(
FileFinderDelegate::new( FileFinderDelegate::new(
workspace.downgrade(), workspace.downgrade(),
@ -979,7 +993,8 @@ mod tests {
), ),
cx, cx,
) )
}); })
.root(cx);
// Even though there is only one worktree, that worktree's filename // Even though there is only one worktree, that worktree's filename
// is included in the matching, because the worktree is a single file. // is included in the matching, because the worktree is a single file.
@ -1015,61 +1030,6 @@ mod tests {
finder.read_with(cx, |f, _| assert_eq!(f.delegate().matches.len(), 0)); finder.read_with(cx, |f, _| assert_eq!(f.delegate().matches.len(), 0));
} }
#[gpui::test]
async fn test_multiple_matches_with_same_relative_path(cx: &mut TestAppContext) {
let app_state = init_test(cx);
app_state
.fs
.as_fake()
.insert_tree(
"/root",
json!({
"dir1": { "a.txt": "" },
"dir2": { "a.txt": "" }
}),
)
.await;
let project = Project::test(
app_state.fs.clone(),
["/root/dir1".as_ref(), "/root/dir2".as_ref()],
cx,
)
.await;
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
let (_, finder) = cx.add_window(|cx| {
Picker::new(
FileFinderDelegate::new(
workspace.downgrade(),
workspace.read(cx).project().clone(),
None,
Vec::new(),
cx,
),
cx,
)
});
// Run a search that matches two files with the same relative path.
finder
.update(cx, |f, cx| {
f.delegate_mut().spawn_search(test_path_like("a.t"), cx)
})
.await;
// Can switch between different matches with the same relative path.
finder.update(cx, |finder, cx| {
let delegate = finder.delegate_mut();
assert_eq!(delegate.matches.len(), 2);
assert_eq!(delegate.selected_index(), 0);
delegate.set_selected_index(1, cx);
assert_eq!(delegate.selected_index(), 1);
delegate.set_selected_index(0, cx);
assert_eq!(delegate.selected_index(), 0);
});
}
#[gpui::test] #[gpui::test]
async fn test_path_distance_ordering(cx: &mut TestAppContext) { async fn test_path_distance_ordering(cx: &mut TestAppContext) {
let app_state = init_test(cx); let app_state = init_test(cx);
@ -1089,7 +1049,9 @@ mod tests {
.await; .await;
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx)); let workspace = cx
.add_window(|cx| Workspace::test_new(project, cx))
.root(cx);
let worktree_id = cx.read(|cx| { let worktree_id = cx.read(|cx| {
let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>(); let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
assert_eq!(worktrees.len(), 1); assert_eq!(worktrees.len(), 1);
@ -1103,7 +1065,8 @@ mod tests {
worktree_id, worktree_id,
path: Arc::from(Path::new("/root/dir2/b.txt")), path: Arc::from(Path::new("/root/dir2/b.txt")),
})); }));
let (_, finder) = cx.add_window(|cx| { let finder = cx
.add_window(|cx| {
Picker::new( Picker::new(
FileFinderDelegate::new( FileFinderDelegate::new(
workspace.downgrade(), workspace.downgrade(),
@ -1114,7 +1077,8 @@ mod tests {
), ),
cx, cx,
) )
}); })
.root(cx);
finder finder
.update(cx, |f, cx| { .update(cx, |f, cx| {
@ -1151,8 +1115,11 @@ mod tests {
.await; .await;
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx)); let workspace = cx
let (_, finder) = cx.add_window(|cx| { .add_window(|cx| Workspace::test_new(project, cx))
.root(cx);
let finder = cx
.add_window(|cx| {
Picker::new( Picker::new(
FileFinderDelegate::new( FileFinderDelegate::new(
workspace.downgrade(), workspace.downgrade(),
@ -1163,7 +1130,8 @@ mod tests {
), ),
cx, cx,
) )
}); })
.root(cx);
finder finder
.update(cx, |f, cx| { .update(cx, |f, cx| {
f.delegate_mut().spawn_search(test_path_like("dir"), cx) f.delegate_mut().spawn_search(test_path_like("dir"), cx)
@ -1198,7 +1166,8 @@ mod tests {
.await; .await;
let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx)); let window = cx.add_window(|cx| Workspace::test_new(project, cx));
let workspace = window.root(cx);
let worktree_id = cx.read(|cx| { let worktree_id = cx.read(|cx| {
let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>(); let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
assert_eq!(worktrees.len(), 1); assert_eq!(worktrees.len(), 1);
@ -1216,7 +1185,7 @@ mod tests {
"fir", "fir",
1, 1,
"first.rs", "first.rs",
window_id, window.into(),
&workspace, &workspace,
&deterministic, &deterministic,
cx, cx,
@ -1231,7 +1200,7 @@ mod tests {
"sec", "sec",
1, 1,
"second.rs", "second.rs",
window_id, window.into(),
&workspace, &workspace,
&deterministic, &deterministic,
cx, cx,
@ -1253,7 +1222,7 @@ mod tests {
"thi", "thi",
1, 1,
"third.rs", "third.rs",
window_id, window.into(),
&workspace, &workspace,
&deterministic, &deterministic,
cx, cx,
@ -1285,7 +1254,7 @@ mod tests {
"sec", "sec",
1, 1,
"second.rs", "second.rs",
window_id, window.into(),
&workspace, &workspace,
&deterministic, &deterministic,
cx, cx,
@ -1324,7 +1293,7 @@ mod tests {
"thi", "thi",
1, 1,
"third.rs", "third.rs",
window_id, window.into(),
&workspace, &workspace,
&deterministic, &deterministic,
cx, cx,
@ -1404,7 +1373,8 @@ mod tests {
.detach(); .detach();
deterministic.run_until_parked(); deterministic.run_until_parked();
let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx)); let window = cx.add_window(|cx| Workspace::test_new(project, cx));
let workspace = window.root(cx);
let worktree_id = cx.read(|cx| { let worktree_id = cx.read(|cx| {
let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>(); let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
assert_eq!(worktrees.len(), 1,); assert_eq!(worktrees.len(), 1,);
@ -1439,7 +1409,7 @@ mod tests {
"sec", "sec",
1, 1,
"second.rs", "second.rs",
window_id, window.into(),
&workspace, &workspace,
&deterministic, &deterministic,
cx, cx,
@ -1461,7 +1431,7 @@ mod tests {
"fir", "fir",
1, 1,
"first.rs", "first.rs",
window_id, window.into(),
&workspace, &workspace,
&deterministic, &deterministic,
cx, cx,
@ -1493,12 +1463,12 @@ mod tests {
input: &str, input: &str,
expected_matches: usize, expected_matches: usize,
expected_editor_title: &str, expected_editor_title: &str,
window_id: usize, window: gpui::AnyWindowHandle,
workspace: &ViewHandle<Workspace>, workspace: &ViewHandle<Workspace>,
deterministic: &gpui::executor::Deterministic, deterministic: &gpui::executor::Deterministic,
cx: &mut gpui::TestAppContext, cx: &mut gpui::TestAppContext,
) -> Vec<FoundPath> { ) -> Vec<FoundPath> {
cx.dispatch_action(window_id, Toggle); cx.dispatch_action(window, Toggle);
let finder = cx.read(|cx| workspace.read(cx).modal::<FileFinder>().unwrap()); let finder = cx.read(|cx| workspace.read(cx).modal::<FileFinder>().unwrap());
finder finder
.update(cx, |finder, cx| { .update(cx, |finder, cx| {
@ -1515,8 +1485,8 @@ mod tests {
}); });
let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone()); let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone());
cx.dispatch_action(window_id, SelectNext); cx.dispatch_action(window, SelectNext);
cx.dispatch_action(window_id, Confirm); cx.dispatch_action(window, Confirm);
deterministic.run_until_parked(); deterministic.run_until_parked();
active_pane active_pane
.condition(cx, |pane, _| pane.active_item().is_some()) .condition(cx, |pane, _| pane.active_item().is_some())

View file

@ -135,7 +135,7 @@ impl Entity for GoToLine {
fn release(&mut self, cx: &mut AppContext) { fn release(&mut self, cx: &mut AppContext) {
let scroll_position = self.prev_scroll_position.take(); let scroll_position = self.prev_scroll_position.take();
cx.update_window(self.active_editor.window_id(), |cx| { self.active_editor.window().update(cx, |cx| {
self.active_editor.update(cx, |editor, cx| { self.active_editor.update(cx, |editor, cx| {
editor.highlight_rows(None); editor.highlight_rows(None);
if let Some(scroll_position) = scroll_position { if let Some(scroll_position) = scroll_position {

View file

@ -22,6 +22,7 @@ sqlez = { path = "../sqlez" }
async-task = "4.0.3" async-task = "4.0.3"
backtrace = { version = "0.3", optional = true } backtrace = { version = "0.3", optional = true }
ctor.workspace = true ctor.workspace = true
derive_more.workspace = true
dhat = { version = "0.3", optional = true } dhat = { version = "0.3", optional = true }
env_logger = { version = "0.9", optional = true } env_logger = { version = "0.9", optional = true }
etagere = "0.2" etagere = "0.2"

File diff suppressed because it is too large Load diff

View file

@ -77,9 +77,9 @@ pub(crate) fn setup_menu_handlers(foreground_platform: &dyn ForegroundPlatform,
let cx = app.0.clone(); let cx = app.0.clone();
move |action| { move |action| {
let mut cx = cx.borrow_mut(); let mut cx = cx.borrow_mut();
if let Some(main_window_id) = cx.platform.main_window_id() { if let Some(main_window) = cx.active_window() {
let dispatched = cx let dispatched = main_window
.update_window(main_window_id, |cx| { .update(&mut *cx, |cx| {
if let Some(view_id) = cx.focused_view_id() { if let Some(view_id) = cx.focused_view_id() {
cx.dispatch_action(Some(view_id), action); cx.dispatch_action(Some(view_id), action);
true true

View file

@ -9,7 +9,7 @@ use collections::{hash_map::Entry, HashMap, HashSet};
#[cfg(any(test, feature = "test-support"))] #[cfg(any(test, feature = "test-support"))]
use crate::util::post_inc; use crate::util::post_inc;
use crate::ElementStateId; use crate::{AnyWindowHandle, ElementStateId};
lazy_static! { lazy_static! {
static ref LEAK_BACKTRACE: bool = static ref LEAK_BACKTRACE: bool =
@ -26,7 +26,7 @@ pub struct RefCounts {
entity_counts: HashMap<usize, usize>, entity_counts: HashMap<usize, usize>,
element_state_counts: HashMap<ElementStateId, ElementStateRefCount>, element_state_counts: HashMap<ElementStateId, ElementStateRefCount>,
dropped_models: HashSet<usize>, dropped_models: HashSet<usize>,
dropped_views: HashSet<(usize, usize)>, dropped_views: HashSet<(AnyWindowHandle, usize)>,
dropped_element_states: HashSet<ElementStateId>, dropped_element_states: HashSet<ElementStateId>,
#[cfg(any(test, feature = "test-support"))] #[cfg(any(test, feature = "test-support"))]
@ -55,12 +55,12 @@ impl RefCounts {
} }
} }
pub fn inc_view(&mut self, window_id: usize, view_id: usize) { pub fn inc_view(&mut self, window: AnyWindowHandle, view_id: usize) {
match self.entity_counts.entry(view_id) { match self.entity_counts.entry(view_id) {
Entry::Occupied(mut entry) => *entry.get_mut() += 1, Entry::Occupied(mut entry) => *entry.get_mut() += 1,
Entry::Vacant(entry) => { Entry::Vacant(entry) => {
entry.insert(1); entry.insert(1);
self.dropped_views.remove(&(window_id, view_id)); self.dropped_views.remove(&(window, view_id));
} }
} }
} }
@ -94,12 +94,12 @@ impl RefCounts {
} }
} }
pub fn dec_view(&mut self, window_id: usize, view_id: usize) { pub fn dec_view(&mut self, window: AnyWindowHandle, view_id: usize) {
let count = self.entity_counts.get_mut(&view_id).unwrap(); let count = self.entity_counts.get_mut(&view_id).unwrap();
*count -= 1; *count -= 1;
if *count == 0 { if *count == 0 {
self.entity_counts.remove(&view_id); self.entity_counts.remove(&view_id);
self.dropped_views.insert((window_id, view_id)); self.dropped_views.insert((window, view_id));
} }
} }
@ -120,7 +120,7 @@ impl RefCounts {
&mut self, &mut self,
) -> ( ) -> (
HashSet<usize>, HashSet<usize>,
HashSet<(usize, usize)>, HashSet<(AnyWindowHandle, usize)>,
HashSet<ElementStateId>, HashSet<ElementStateId>,
) { ) {
( (

View file

@ -4,9 +4,9 @@ use crate::{
keymap_matcher::{Binding, Keystroke}, keymap_matcher::{Binding, Keystroke},
platform, platform,
platform::{Event, InputHandler, KeyDownEvent, Platform}, platform::{Event, InputHandler, KeyDownEvent, Platform},
Action, AppContext, BorrowAppContext, BorrowWindowContext, Entity, FontCache, Handle, Action, AnyWindowHandle, AppContext, BorrowAppContext, BorrowWindowContext, Entity, FontCache,
ModelContext, ModelHandle, Subscription, Task, View, ViewContext, ViewHandle, WeakHandle, Handle, ModelContext, ModelHandle, Subscription, Task, View, ViewContext, ViewHandle,
WindowContext, WeakHandle, WindowContext, WindowHandle,
}; };
use collections::BTreeMap; use collections::BTreeMap;
use futures::Future; use futures::Future;
@ -60,7 +60,7 @@ impl TestAppContext {
RefCounts::new(leak_detector), RefCounts::new(leak_detector),
(), (),
); );
cx.next_entity_id = first_entity_id; cx.next_id = first_entity_id;
let cx = TestAppContext { let cx = TestAppContext {
cx: Rc::new(RefCell::new(cx)), cx: Rc::new(RefCell::new(cx)),
foreground_platform, foreground_platform,
@ -72,8 +72,8 @@ impl TestAppContext {
cx cx
} }
pub fn dispatch_action<A: Action>(&mut self, window_id: usize, action: A) { pub fn dispatch_action<A: Action>(&mut self, window: AnyWindowHandle, action: A) {
self.update_window(window_id, |window| { self.update_window(window, |window| {
window.dispatch_action(window.focused_view_id(), &action); window.dispatch_action(window.focused_view_id(), &action);
}) })
.expect("window not found"); .expect("window not found");
@ -81,10 +81,10 @@ impl TestAppContext {
pub fn available_actions( pub fn available_actions(
&self, &self,
window_id: usize, window: AnyWindowHandle,
view_id: usize, view_id: usize,
) -> Vec<(&'static str, Box<dyn Action>, SmallVec<[Binding; 1]>)> { ) -> Vec<(&'static str, Box<dyn Action>, SmallVec<[Binding; 1]>)> {
self.read_window(window_id, |cx| cx.available_actions(view_id)) self.read_window(window, |cx| cx.available_actions(view_id))
.unwrap_or_default() .unwrap_or_default()
} }
@ -92,11 +92,13 @@ impl TestAppContext {
self.update(|cx| cx.dispatch_global_action_any(&action)); self.update(|cx| cx.dispatch_global_action_any(&action));
} }
pub fn dispatch_keystroke(&mut self, window_id: usize, keystroke: Keystroke, is_held: bool) { pub fn dispatch_keystroke(
let handled = self &mut self,
.cx window: AnyWindowHandle,
.borrow_mut() keystroke: Keystroke,
.update_window(window_id, |cx| { is_held: bool,
) {
let handled = window.update(self, |cx| {
if cx.dispatch_keystroke(&keystroke) { if cx.dispatch_keystroke(&keystroke) {
return true; return true;
} }
@ -112,13 +114,12 @@ impl TestAppContext {
} }
false false
}) });
.unwrap_or(false);
if !handled && !keystroke.cmd && !keystroke.ctrl { if !handled && !keystroke.cmd && !keystroke.ctrl {
WindowInputHandler { WindowInputHandler {
app: self.cx.clone(), app: self.cx.clone(),
window_id, window,
} }
.replace_text_in_range(None, &keystroke.key) .replace_text_in_range(None, &keystroke.key)
} }
@ -126,18 +127,18 @@ impl TestAppContext {
pub fn read_window<T, F: FnOnce(&WindowContext) -> T>( pub fn read_window<T, F: FnOnce(&WindowContext) -> T>(
&self, &self,
window_id: usize, window: AnyWindowHandle,
callback: F, callback: F,
) -> Option<T> { ) -> Option<T> {
self.cx.borrow().read_window(window_id, callback) self.cx.borrow().read_window(window, callback)
} }
pub fn update_window<T, F: FnOnce(&mut WindowContext) -> T>( pub fn update_window<T, F: FnOnce(&mut WindowContext) -> T>(
&mut self, &mut self,
window_id: usize, window: AnyWindowHandle,
callback: F, callback: F,
) -> Option<T> { ) -> Option<T> {
self.cx.borrow_mut().update_window(window_id, callback) self.cx.borrow_mut().update_window(window, callback)
} }
pub fn add_model<T, F>(&mut self, build_model: F) -> ModelHandle<T> pub fn add_model<T, F>(&mut self, build_model: F) -> ModelHandle<T>
@ -148,26 +149,17 @@ impl TestAppContext {
self.cx.borrow_mut().add_model(build_model) self.cx.borrow_mut().add_model(build_model)
} }
pub fn add_window<T, F>(&mut self, build_root_view: F) -> (usize, ViewHandle<T>) pub fn add_window<V, F>(&mut self, build_root_view: F) -> WindowHandle<V>
where where
T: View, V: View,
F: FnOnce(&mut ViewContext<T>) -> T, F: FnOnce(&mut ViewContext<V>) -> V,
{ {
let (window_id, view) = self let window = self
.cx .cx
.borrow_mut() .borrow_mut()
.add_window(Default::default(), build_root_view); .add_window(Default::default(), build_root_view);
self.simulate_window_activation(Some(window_id)); window.simulate_activation(self);
(window_id, view) window
}
pub fn add_view<T, F>(&mut self, window_id: usize, build_view: F) -> ViewHandle<T>
where
T: View,
F: FnOnce(&mut ViewContext<T>) -> T,
{
self.update_window(window_id, |cx| cx.add_view(build_view))
.expect("window not found")
} }
pub fn observe_global<E, F>(&mut self, callback: F) -> Subscription pub fn observe_global<E, F>(&mut self, callback: F) -> Subscription
@ -190,8 +182,8 @@ impl TestAppContext {
self.cx.borrow_mut().subscribe_global(callback) self.cx.borrow_mut().subscribe_global(callback)
} }
pub fn window_ids(&self) -> Vec<usize> { pub fn windows(&self) -> Vec<AnyWindowHandle> {
self.cx.borrow().windows.keys().copied().collect() self.cx.borrow().windows().collect()
} }
pub fn remove_all_windows(&mut self) { pub fn remove_all_windows(&mut self) {
@ -261,76 +253,6 @@ impl TestAppContext {
self.foreground_platform.as_ref().did_prompt_for_new_path() self.foreground_platform.as_ref().did_prompt_for_new_path()
} }
pub fn simulate_prompt_answer(&self, window_id: usize, answer: usize) {
use postage::prelude::Sink as _;
let mut done_tx = self
.platform_window_mut(window_id)
.pending_prompts
.borrow_mut()
.pop_front()
.expect("prompt was not called");
done_tx.try_send(answer).ok();
}
pub fn has_pending_prompt(&self, window_id: usize) -> bool {
let window = self.platform_window_mut(window_id);
let prompts = window.pending_prompts.borrow_mut();
!prompts.is_empty()
}
pub fn current_window_title(&self, window_id: usize) -> Option<String> {
self.platform_window_mut(window_id).title.clone()
}
pub fn simulate_window_close(&self, window_id: usize) -> bool {
let handler = self
.platform_window_mut(window_id)
.should_close_handler
.take();
if let Some(mut handler) = handler {
let should_close = handler();
self.platform_window_mut(window_id).should_close_handler = Some(handler);
should_close
} else {
false
}
}
pub fn simulate_window_resize(&self, window_id: usize, size: Vector2F) {
let mut window = self.platform_window_mut(window_id);
window.size = size;
let mut handlers = mem::take(&mut window.resize_handlers);
drop(window);
for handler in &mut handlers {
handler();
}
self.platform_window_mut(window_id).resize_handlers = handlers;
}
pub fn simulate_window_activation(&self, to_activate: Option<usize>) {
self.cx.borrow_mut().update(|cx| {
let other_window_ids = cx
.windows
.keys()
.filter(|window_id| Some(**window_id) != to_activate)
.copied()
.collect::<Vec<_>>();
for window_id in other_window_ids {
cx.window_changed_active_status(window_id, false)
}
if let Some(to_activate) = to_activate {
cx.window_changed_active_status(to_activate, true)
}
});
}
pub fn is_window_edited(&self, window_id: usize) -> bool {
self.platform_window_mut(window_id).edited
}
pub fn leak_detector(&self) -> Arc<Mutex<LeakDetector>> { pub fn leak_detector(&self) -> Arc<Mutex<LeakDetector>> {
self.cx.borrow().leak_detector() self.cx.borrow().leak_detector()
} }
@ -351,18 +273,6 @@ impl TestAppContext {
self.assert_dropped(weak); self.assert_dropped(weak);
} }
fn platform_window_mut(&self, window_id: usize) -> std::cell::RefMut<platform::test::Window> {
std::cell::RefMut::map(self.cx.borrow_mut(), |state| {
let window = state.windows.get_mut(&window_id).unwrap();
let test_window = window
.platform_window
.as_any_mut()
.downcast_mut::<platform::test::Window>()
.unwrap();
test_window
})
}
pub fn set_condition_duration(&mut self, duration: Option<Duration>) { pub fn set_condition_duration(&mut self, duration: Option<Duration>) {
self.condition_duration = duration; self.condition_duration = duration;
} }
@ -405,19 +315,39 @@ impl BorrowAppContext for TestAppContext {
} }
impl BorrowWindowContext for TestAppContext { impl BorrowWindowContext for TestAppContext {
fn read_with<T, F: FnOnce(&WindowContext) -> T>(&self, window_id: usize, f: F) -> T { type Result<T> = T;
fn read_window<T, F: FnOnce(&WindowContext) -> T>(&self, window: AnyWindowHandle, f: F) -> T {
self.cx self.cx
.borrow() .borrow()
.read_window(window_id, f) .read_window(window, f)
.expect("window was closed") .expect("window was closed")
} }
fn update<T, F: FnOnce(&mut WindowContext) -> T>(&mut self, window_id: usize, f: F) -> T { fn read_window_optional<T, F>(&self, window: AnyWindowHandle, f: F) -> Option<T>
where
F: FnOnce(&WindowContext) -> Option<T>,
{
BorrowWindowContext::read_window(self, window, f)
}
fn update_window<T, F: FnOnce(&mut WindowContext) -> T>(
&mut self,
window: AnyWindowHandle,
f: F,
) -> T {
self.cx self.cx
.borrow_mut() .borrow_mut()
.update_window(window_id, f) .update_window(window, f)
.expect("window was closed") .expect("window was closed")
} }
fn update_window_optional<T, F>(&mut self, window: AnyWindowHandle, f: F) -> Option<T>
where
F: FnOnce(&mut WindowContext) -> Option<T>,
{
BorrowWindowContext::update_window(self, window, f)
}
} }
impl<T: Entity> ModelHandle<T> { impl<T: Entity> ModelHandle<T> {
@ -532,6 +462,71 @@ impl<T: Entity> ModelHandle<T> {
} }
} }
impl AnyWindowHandle {
pub fn has_pending_prompt(&self, cx: &mut TestAppContext) -> bool {
let window = self.platform_window_mut(cx);
let prompts = window.pending_prompts.borrow_mut();
!prompts.is_empty()
}
pub fn current_title(&self, cx: &mut TestAppContext) -> Option<String> {
self.platform_window_mut(cx).title.clone()
}
pub fn simulate_close(&self, cx: &mut TestAppContext) -> bool {
let handler = self.platform_window_mut(cx).should_close_handler.take();
if let Some(mut handler) = handler {
let should_close = handler();
self.platform_window_mut(cx).should_close_handler = Some(handler);
should_close
} else {
false
}
}
pub fn simulate_resize(&self, size: Vector2F, cx: &mut TestAppContext) {
let mut window = self.platform_window_mut(cx);
window.size = size;
let mut handlers = mem::take(&mut window.resize_handlers);
drop(window);
for handler in &mut handlers {
handler();
}
self.platform_window_mut(cx).resize_handlers = handlers;
}
pub fn is_edited(&self, cx: &mut TestAppContext) -> bool {
self.platform_window_mut(cx).edited
}
pub fn simulate_prompt_answer(&self, answer: usize, cx: &mut TestAppContext) {
use postage::prelude::Sink as _;
let mut done_tx = self
.platform_window_mut(cx)
.pending_prompts
.borrow_mut()
.pop_front()
.expect("prompt was not called");
done_tx.try_send(answer).ok();
}
fn platform_window_mut<'a>(
&self,
cx: &'a mut TestAppContext,
) -> std::cell::RefMut<'a, platform::test::Window> {
std::cell::RefMut::map(cx.cx.borrow_mut(), |state| {
let window = state.windows.get_mut(&self).unwrap();
let test_window = window
.platform_window
.as_any_mut()
.downcast_mut::<platform::test::Window>()
.unwrap();
test_window
})
}
}
impl<T: View> ViewHandle<T> { impl<T: View> ViewHandle<T> {
pub fn next_notification(&self, cx: &TestAppContext) -> impl Future<Output = ()> { pub fn next_notification(&self, cx: &TestAppContext) -> impl Future<Output = ()> {
use postage::prelude::{Sink as _, Stream as _}; use postage::prelude::{Sink as _, Stream as _};

View file

@ -13,9 +13,10 @@ use crate::{
}, },
text_layout::TextLayoutCache, text_layout::TextLayoutCache,
util::post_inc, util::post_inc,
Action, AnyView, AnyViewHandle, AppContext, BorrowAppContext, BorrowWindowContext, Effect, Action, AnyView, AnyViewHandle, AnyWindowHandle, AppContext, BorrowAppContext,
Element, Entity, Handle, LayoutContext, MouseRegion, MouseRegionId, PaintContext, SceneBuilder, BorrowWindowContext, Effect, Element, Entity, Handle, LayoutContext, MouseRegion,
Subscription, View, ViewContext, ViewHandle, WindowInvalidation, MouseRegionId, PaintContext, SceneBuilder, Subscription, View, ViewContext, ViewHandle,
WindowInvalidation,
}; };
use anyhow::{anyhow, bail, Result}; use anyhow::{anyhow, bail, Result};
use collections::{HashMap, HashSet}; use collections::{HashMap, HashSet};
@ -60,7 +61,7 @@ pub struct Window {
impl Window { impl Window {
pub fn new<V, F>( pub fn new<V, F>(
window_id: usize, handle: AnyWindowHandle,
platform_window: Box<dyn platform::Window>, platform_window: Box<dyn platform::Window>,
cx: &mut AppContext, cx: &mut AppContext,
build_view: F, build_view: F,
@ -92,7 +93,7 @@ impl Window {
appearance, appearance,
}; };
let mut window_context = WindowContext::mutable(cx, &mut window, window_id); let mut window_context = WindowContext::mutable(cx, &mut window, handle);
let root_view = window_context.add_view(|cx| build_view(cx)); let root_view = window_context.add_view(|cx| build_view(cx));
if let Some(invalidation) = window_context.window.invalidation.take() { if let Some(invalidation) = window_context.window.invalidation.take() {
window_context.invalidate(invalidation, appearance); window_context.invalidate(invalidation, appearance);
@ -113,7 +114,7 @@ impl Window {
pub struct WindowContext<'a> { pub struct WindowContext<'a> {
pub(crate) app_context: Reference<'a, AppContext>, pub(crate) app_context: Reference<'a, AppContext>,
pub(crate) window: Reference<'a, Window>, pub(crate) window: Reference<'a, Window>,
pub(crate) window_id: usize, pub(crate) window_handle: AnyWindowHandle,
pub(crate) removed: bool, pub(crate) removed: bool,
} }
@ -142,42 +143,66 @@ impl BorrowAppContext for WindowContext<'_> {
} }
impl BorrowWindowContext for WindowContext<'_> { impl BorrowWindowContext for WindowContext<'_> {
fn read_with<T, F: FnOnce(&WindowContext) -> T>(&self, window_id: usize, f: F) -> T { type Result<T> = T;
if self.window_id == window_id {
fn read_window<T, F: FnOnce(&WindowContext) -> T>(&self, handle: AnyWindowHandle, f: F) -> T {
if self.window_handle == handle {
f(self) f(self)
} else { } else {
panic!("read_with called with id of window that does not belong to this context") panic!("read_with called with id of window that does not belong to this context")
} }
} }
fn update<T, F: FnOnce(&mut WindowContext) -> T>(&mut self, window_id: usize, f: F) -> T { fn read_window_optional<T, F>(&self, window: AnyWindowHandle, f: F) -> Option<T>
if self.window_id == window_id { where
F: FnOnce(&WindowContext) -> Option<T>,
{
BorrowWindowContext::read_window(self, window, f)
}
fn update_window<T, F: FnOnce(&mut WindowContext) -> T>(
&mut self,
handle: AnyWindowHandle,
f: F,
) -> T {
if self.window_handle == handle {
f(self) f(self)
} else { } else {
panic!("update called with id of window that does not belong to this context") panic!("update called with id of window that does not belong to this context")
} }
} }
fn update_window_optional<T, F>(&mut self, handle: AnyWindowHandle, f: F) -> Option<T>
where
F: FnOnce(&mut WindowContext) -> Option<T>,
{
BorrowWindowContext::update_window(self, handle, f)
}
} }
impl<'a> WindowContext<'a> { impl<'a> WindowContext<'a> {
pub fn mutable( pub fn mutable(
app_context: &'a mut AppContext, app_context: &'a mut AppContext,
window: &'a mut Window, window: &'a mut Window,
window_id: usize, handle: AnyWindowHandle,
) -> Self { ) -> Self {
Self { Self {
app_context: Reference::Mutable(app_context), app_context: Reference::Mutable(app_context),
window: Reference::Mutable(window), window: Reference::Mutable(window),
window_id, window_handle: handle,
removed: false, removed: false,
} }
} }
pub fn immutable(app_context: &'a AppContext, window: &'a Window, window_id: usize) -> Self { pub fn immutable(
app_context: &'a AppContext,
window: &'a Window,
handle: AnyWindowHandle,
) -> Self {
Self { Self {
app_context: Reference::Immutable(app_context), app_context: Reference::Immutable(app_context),
window: Reference::Immutable(window), window: Reference::Immutable(window),
window_id, window_handle: handle,
removed: false, removed: false,
} }
} }
@ -186,8 +211,8 @@ impl<'a> WindowContext<'a> {
self.removed = true; self.removed = true;
} }
pub fn window_id(&self) -> usize { pub fn window(&self) -> AnyWindowHandle {
self.window_id self.window_handle
} }
pub fn app_context(&mut self) -> &mut AppContext { pub fn app_context(&mut self) -> &mut AppContext {
@ -210,10 +235,10 @@ impl<'a> WindowContext<'a> {
where where
F: FnOnce(&mut dyn AnyView, &mut Self) -> T, F: FnOnce(&mut dyn AnyView, &mut Self) -> T,
{ {
let window_id = self.window_id; let handle = self.window_handle;
let mut view = self.views.remove(&(window_id, view_id))?; let mut view = self.views.remove(&(handle, view_id))?;
let result = f(view.as_mut(), self); let result = f(view.as_mut(), self);
self.views.insert((window_id, view_id), view); self.views.insert((handle, view_id), view);
Some(result) Some(result)
} }
@ -238,9 +263,9 @@ impl<'a> WindowContext<'a> {
} }
pub fn defer(&mut self, callback: impl 'static + FnOnce(&mut WindowContext)) { pub fn defer(&mut self, callback: impl 'static + FnOnce(&mut WindowContext)) {
let window_id = self.window_id; let handle = self.window_handle;
self.app_context.defer(move |cx| { self.app_context.defer(move |cx| {
cx.update_window(window_id, |cx| callback(cx)); cx.update_window(handle, |cx| callback(cx));
}) })
} }
@ -280,10 +305,10 @@ impl<'a> WindowContext<'a> {
H: Handle<E>, H: Handle<E>,
F: 'static + FnMut(H, &E::Event, &mut WindowContext) -> bool, F: 'static + FnMut(H, &E::Event, &mut WindowContext) -> bool,
{ {
let window_id = self.window_id; let window_handle = self.window_handle;
self.app_context self.app_context
.subscribe_internal(handle, move |emitter, event, cx| { .subscribe_internal(handle, move |emitter, event, cx| {
cx.update_window(window_id, |cx| callback(emitter, event, cx)) cx.update_window(window_handle, |cx| callback(emitter, event, cx))
.unwrap_or(false) .unwrap_or(false)
}) })
} }
@ -292,17 +317,17 @@ impl<'a> WindowContext<'a> {
where where
F: 'static + FnMut(bool, &mut WindowContext) -> bool, F: 'static + FnMut(bool, &mut WindowContext) -> bool,
{ {
let window_id = self.window_id; let handle = self.window_handle;
let subscription_id = post_inc(&mut self.next_subscription_id); let subscription_id = post_inc(&mut self.next_subscription_id);
self.pending_effects self.pending_effects
.push_back(Effect::WindowActivationObservation { .push_back(Effect::WindowActivationObservation {
window_id, window: handle,
subscription_id, subscription_id,
callback: Box::new(callback), callback: Box::new(callback),
}); });
Subscription::WindowActivationObservation( Subscription::WindowActivationObservation(
self.window_activation_observations self.window_activation_observations
.subscribe(window_id, subscription_id), .subscribe(handle, subscription_id),
) )
} }
@ -310,17 +335,17 @@ impl<'a> WindowContext<'a> {
where where
F: 'static + FnMut(bool, &mut WindowContext) -> bool, F: 'static + FnMut(bool, &mut WindowContext) -> bool,
{ {
let window_id = self.window_id; let window = self.window_handle;
let subscription_id = post_inc(&mut self.next_subscription_id); let subscription_id = post_inc(&mut self.next_subscription_id);
self.pending_effects self.pending_effects
.push_back(Effect::WindowFullscreenObservation { .push_back(Effect::WindowFullscreenObservation {
window_id, window,
subscription_id, subscription_id,
callback: Box::new(callback), callback: Box::new(callback),
}); });
Subscription::WindowActivationObservation( Subscription::WindowActivationObservation(
self.window_activation_observations self.window_activation_observations
.subscribe(window_id, subscription_id), .subscribe(window, subscription_id),
) )
} }
@ -328,17 +353,17 @@ impl<'a> WindowContext<'a> {
where where
F: 'static + FnMut(WindowBounds, Uuid, &mut WindowContext) -> bool, F: 'static + FnMut(WindowBounds, Uuid, &mut WindowContext) -> bool,
{ {
let window_id = self.window_id; let window = self.window_handle;
let subscription_id = post_inc(&mut self.next_subscription_id); let subscription_id = post_inc(&mut self.next_subscription_id);
self.pending_effects self.pending_effects
.push_back(Effect::WindowBoundsObservation { .push_back(Effect::WindowBoundsObservation {
window_id, window,
subscription_id, subscription_id,
callback: Box::new(callback), callback: Box::new(callback),
}); });
Subscription::WindowBoundsObservation( Subscription::WindowBoundsObservation(
self.window_bounds_observations self.window_bounds_observations
.subscribe(window_id, subscription_id), .subscribe(window, subscription_id),
) )
} }
@ -347,13 +372,13 @@ impl<'a> WindowContext<'a> {
F: 'static F: 'static
+ FnMut(&Keystroke, &MatchResult, Option<&Box<dyn Action>>, &mut WindowContext) -> bool, + FnMut(&Keystroke, &MatchResult, Option<&Box<dyn Action>>, &mut WindowContext) -> bool,
{ {
let window_id = self.window_id; let window = self.window_handle;
let subscription_id = post_inc(&mut self.next_subscription_id); let subscription_id = post_inc(&mut self.next_subscription_id);
self.keystroke_observations self.keystroke_observations
.add_callback(window_id, subscription_id, Box::new(callback)); .add_callback(window, subscription_id, Box::new(callback));
Subscription::KeystrokeObservation( Subscription::KeystrokeObservation(
self.keystroke_observations self.keystroke_observations
.subscribe(window_id, subscription_id), .subscribe(window, subscription_id),
) )
} }
@ -361,11 +386,11 @@ impl<'a> WindowContext<'a> {
&self, &self,
view_id: usize, view_id: usize,
) -> Vec<(&'static str, Box<dyn Action>, SmallVec<[Binding; 1]>)> { ) -> Vec<(&'static str, Box<dyn Action>, SmallVec<[Binding; 1]>)> {
let window_id = self.window_id; let handle = self.window_handle;
let mut contexts = Vec::new(); let mut contexts = Vec::new();
let mut handler_depths_by_action_id = HashMap::<TypeId, usize>::default(); let mut handler_depths_by_action_id = HashMap::<TypeId, usize>::default();
for (depth, view_id) in self.ancestors(view_id).enumerate() { for (depth, view_id) in self.ancestors(view_id).enumerate() {
if let Some(view_metadata) = self.views_metadata.get(&(window_id, view_id)) { if let Some(view_metadata) = self.views_metadata.get(&(handle, view_id)) {
contexts.push(view_metadata.keymap_context.clone()); contexts.push(view_metadata.keymap_context.clone());
if let Some(actions) = self.actions.get(&view_metadata.type_id) { if let Some(actions) = self.actions.get(&view_metadata.type_id) {
handler_depths_by_action_id handler_depths_by_action_id
@ -410,13 +435,13 @@ impl<'a> WindowContext<'a> {
} }
pub(crate) fn dispatch_keystroke(&mut self, keystroke: &Keystroke) -> bool { pub(crate) fn dispatch_keystroke(&mut self, keystroke: &Keystroke) -> bool {
let window_id = self.window_id; let handle = self.window_handle;
if let Some(focused_view_id) = self.focused_view_id() { if let Some(focused_view_id) = self.focused_view_id() {
let dispatch_path = self let dispatch_path = self
.ancestors(focused_view_id) .ancestors(focused_view_id)
.filter_map(|view_id| { .filter_map(|view_id| {
self.views_metadata self.views_metadata
.get(&(window_id, view_id)) .get(&(handle, view_id))
.map(|view| (view_id, view.keymap_context.clone())) .map(|view| (view_id, view.keymap_context.clone()))
}) })
.collect(); .collect();
@ -441,15 +466,10 @@ impl<'a> WindowContext<'a> {
} }
}; };
self.keystroke( self.keystroke(handle, keystroke.clone(), handled_by, match_result.clone());
window_id,
keystroke.clone(),
handled_by,
match_result.clone(),
);
keystroke_handled keystroke_handled
} else { } else {
self.keystroke(window_id, keystroke.clone(), None, MatchResult::None); self.keystroke(handle, keystroke.clone(), None, MatchResult::None);
false false
} }
} }
@ -457,7 +477,7 @@ impl<'a> WindowContext<'a> {
pub(crate) fn dispatch_event(&mut self, event: Event, event_reused: bool) -> bool { pub(crate) fn dispatch_event(&mut self, event: Event, event_reused: bool) -> bool {
let mut mouse_events = SmallVec::<[_; 2]>::new(); let mut mouse_events = SmallVec::<[_; 2]>::new();
let mut notified_views: HashSet<usize> = Default::default(); let mut notified_views: HashSet<usize> = Default::default();
let window_id = self.window_id; let handle = self.window_handle;
// 1. Handle platform event. Keyboard events get dispatched immediately, while mouse events // 1. Handle platform event. Keyboard events get dispatched immediately, while mouse events
// get mapped into the mouse-specific MouseEvent type. // get mapped into the mouse-specific MouseEvent type.
@ -518,6 +538,18 @@ impl<'a> WindowContext<'a> {
// NOTE: The order of event pushes is important! MouseUp events MUST be fired // NOTE: The order of event pushes is important! MouseUp events MUST be fired
// before click events, and so the MouseUp events need to be pushed before // before click events, and so the MouseUp events need to be pushed before
// MouseClick events. // MouseClick events.
// Synthesize one last drag event to end the drag
mouse_events.push(MouseEvent::Drag(MouseDrag {
region: Default::default(),
prev_mouse_position: self.window.mouse_position,
platform_event: MouseMovedEvent {
position: e.position,
pressed_button: Some(e.button),
modifiers: e.modifiers,
},
end: true,
}));
mouse_events.push(MouseEvent::Up(MouseUp { mouse_events.push(MouseEvent::Up(MouseUp {
region: Default::default(), region: Default::default(),
platform_event: e.clone(), platform_event: e.clone(),
@ -565,8 +597,16 @@ impl<'a> WindowContext<'a> {
region: Default::default(), region: Default::default(),
prev_mouse_position: self.window.mouse_position, prev_mouse_position: self.window.mouse_position,
platform_event: e.clone(), platform_event: e.clone(),
end: false,
})); }));
} else if let Some((_, clicked_button)) = self.window.clicked_region { } else if let Some((_, clicked_button)) = self.window.clicked_region {
mouse_events.push(MouseEvent::Drag(MouseDrag {
region: Default::default(),
prev_mouse_position: self.window.mouse_position,
platform_event: e.clone(),
end: true,
}));
// Mouse up event happened outside the current window. Simulate mouse up button event // Mouse up event happened outside the current window. Simulate mouse up button event
let button_event = e.to_button_event(clicked_button); let button_event = e.to_button_event(clicked_button);
mouse_events.push(MouseEvent::Up(MouseUp { mouse_events.push(MouseEvent::Up(MouseUp {
@ -801,19 +841,19 @@ impl<'a> WindowContext<'a> {
} }
for view_id in notified_views { for view_id in notified_views {
self.notify_view(window_id, view_id); self.notify_view(handle, view_id);
} }
any_event_handled any_event_handled
} }
pub(crate) fn dispatch_key_down(&mut self, event: &KeyDownEvent) -> bool { pub(crate) fn dispatch_key_down(&mut self, event: &KeyDownEvent) -> bool {
let window_id = self.window_id; let handle = self.window_handle;
if let Some(focused_view_id) = self.window.focused_view_id { if let Some(focused_view_id) = self.window.focused_view_id {
for view_id in self.ancestors(focused_view_id).collect::<Vec<_>>() { for view_id in self.ancestors(focused_view_id).collect::<Vec<_>>() {
if let Some(mut view) = self.views.remove(&(window_id, view_id)) { if let Some(mut view) = self.views.remove(&(handle, view_id)) {
let handled = view.key_down(event, self, view_id); let handled = view.key_down(event, self, view_id);
self.views.insert((window_id, view_id), view); self.views.insert((handle, view_id), view);
if handled { if handled {
return true; return true;
} }
@ -827,12 +867,12 @@ impl<'a> WindowContext<'a> {
} }
pub(crate) fn dispatch_key_up(&mut self, event: &KeyUpEvent) -> bool { pub(crate) fn dispatch_key_up(&mut self, event: &KeyUpEvent) -> bool {
let window_id = self.window_id; let handle = self.window_handle;
if let Some(focused_view_id) = self.window.focused_view_id { if let Some(focused_view_id) = self.window.focused_view_id {
for view_id in self.ancestors(focused_view_id).collect::<Vec<_>>() { for view_id in self.ancestors(focused_view_id).collect::<Vec<_>>() {
if let Some(mut view) = self.views.remove(&(window_id, view_id)) { if let Some(mut view) = self.views.remove(&(handle, view_id)) {
let handled = view.key_up(event, self, view_id); let handled = view.key_up(event, self, view_id);
self.views.insert((window_id, view_id), view); self.views.insert((handle, view_id), view);
if handled { if handled {
return true; return true;
} }
@ -846,12 +886,12 @@ impl<'a> WindowContext<'a> {
} }
pub(crate) fn dispatch_modifiers_changed(&mut self, event: &ModifiersChangedEvent) -> bool { pub(crate) fn dispatch_modifiers_changed(&mut self, event: &ModifiersChangedEvent) -> bool {
let window_id = self.window_id; let handle = self.window_handle;
if let Some(focused_view_id) = self.window.focused_view_id { if let Some(focused_view_id) = self.window.focused_view_id {
for view_id in self.ancestors(focused_view_id).collect::<Vec<_>>() { for view_id in self.ancestors(focused_view_id).collect::<Vec<_>>() {
if let Some(mut view) = self.views.remove(&(window_id, view_id)) { if let Some(mut view) = self.views.remove(&(handle, view_id)) {
let handled = view.modifiers_changed(event, self, view_id); let handled = view.modifiers_changed(event, self, view_id);
self.views.insert((window_id, view_id), view); self.views.insert((handle, view_id), view);
if handled { if handled {
return true; return true;
} }
@ -886,14 +926,14 @@ impl<'a> WindowContext<'a> {
} }
pub fn render_view(&mut self, params: RenderParams) -> Result<Box<dyn AnyRootElement>> { pub fn render_view(&mut self, params: RenderParams) -> Result<Box<dyn AnyRootElement>> {
let window_id = self.window_id; let handle = self.window_handle;
let view_id = params.view_id; let view_id = params.view_id;
let mut view = self let mut view = self
.views .views
.remove(&(window_id, view_id)) .remove(&(handle, view_id))
.ok_or_else(|| anyhow!("view not found"))?; .ok_or_else(|| anyhow!("view not found"))?;
let element = view.render(self, view_id); let element = view.render(self, view_id);
self.views.insert((window_id, view_id), view); self.views.insert((handle, view_id), view);
Ok(element) Ok(element)
} }
@ -921,9 +961,9 @@ impl<'a> WindowContext<'a> {
} else if old_parent_id == new_parent_id { } else if old_parent_id == new_parent_id {
current_view_id = *old_parent_id.unwrap(); current_view_id = *old_parent_id.unwrap();
} else { } else {
let window_id = self.window_id; let handle = self.window_handle;
for view_id_to_notify in view_ids_to_notify { for view_id_to_notify in view_ids_to_notify {
self.notify_view(window_id, view_id_to_notify); self.notify_view(handle, view_id_to_notify);
} }
break; break;
} }
@ -1091,7 +1131,7 @@ impl<'a> WindowContext<'a> {
} }
pub fn focus(&mut self, view_id: Option<usize>) { pub fn focus(&mut self, view_id: Option<usize>) {
self.app_context.focus(self.window_id, view_id); self.app_context.focus(self.window_handle, view_id);
} }
pub fn window_bounds(&self) -> WindowBounds { pub fn window_bounds(&self) -> WindowBounds {
@ -1131,17 +1171,6 @@ impl<'a> WindowContext<'a> {
self.window.platform_window.prompt(level, msg, answers) self.window.platform_window.prompt(level, msg, answers)
} }
pub fn replace_root_view<V, F>(&mut self, build_root_view: F) -> ViewHandle<V>
where
V: View,
F: FnOnce(&mut ViewContext<V>) -> V,
{
let root_view = self.add_view(|cx| build_root_view(cx));
self.window.root_view = Some(root_view.clone().into_any());
self.window.focused_view_id = Some(root_view.id());
root_view
}
pub fn add_view<T, F>(&mut self, build_view: F) -> ViewHandle<T> pub fn add_view<T, F>(&mut self, build_view: F) -> ViewHandle<T>
where where
T: View, T: View,
@ -1155,26 +1184,26 @@ impl<'a> WindowContext<'a> {
T: View, T: View,
F: FnOnce(&mut ViewContext<T>) -> Option<T>, F: FnOnce(&mut ViewContext<T>) -> Option<T>,
{ {
let window_id = self.window_id; let handle = self.window_handle;
let view_id = post_inc(&mut self.next_entity_id); let view_id = post_inc(&mut self.next_id);
let mut cx = ViewContext::mutable(self, view_id); let mut cx = ViewContext::mutable(self, view_id);
let handle = if let Some(view) = build_view(&mut cx) { let handle = if let Some(view) = build_view(&mut cx) {
let mut keymap_context = KeymapContext::default(); let mut keymap_context = KeymapContext::default();
view.update_keymap_context(&mut keymap_context, cx.app_context()); view.update_keymap_context(&mut keymap_context, cx.app_context());
self.views_metadata.insert( self.views_metadata.insert(
(window_id, view_id), (handle, view_id),
ViewMetadata { ViewMetadata {
type_id: TypeId::of::<T>(), type_id: TypeId::of::<T>(),
keymap_context, keymap_context,
}, },
); );
self.views.insert((window_id, view_id), Box::new(view)); self.views.insert((handle, view_id), Box::new(view));
self.window self.window
.invalidation .invalidation
.get_or_insert_with(Default::default) .get_or_insert_with(Default::default)
.updated .updated
.insert(view_id); .insert(view_id);
Some(ViewHandle::new(window_id, view_id, &self.ref_counts)) Some(ViewHandle::new(handle, view_id, &self.ref_counts))
} else { } else {
None None
}; };
@ -1351,7 +1380,7 @@ pub struct ChildView {
impl ChildView { impl ChildView {
pub fn new(view: &AnyViewHandle, cx: &AppContext) -> Self { pub fn new(view: &AnyViewHandle, cx: &AppContext) -> Self {
let view_name = cx.view_ui_name(view.window_id(), view.id()).unwrap(); let view_name = cx.view_ui_name(view.window, view.id()).unwrap();
Self { Self {
view_id: view.id(), view_id: view.id(),
view_name, view_name,

View file

@ -2,11 +2,11 @@ use std::{cell::RefCell, ops::Range, rc::Rc};
use pathfinder_geometry::rect::RectF; use pathfinder_geometry::rect::RectF;
use crate::{platform::InputHandler, window::WindowContext, AnyView, AppContext}; use crate::{platform::InputHandler, window::WindowContext, AnyView, AnyWindowHandle, AppContext};
pub struct WindowInputHandler { pub struct WindowInputHandler {
pub app: Rc<RefCell<AppContext>>, pub app: Rc<RefCell<AppContext>>,
pub window_id: usize, pub window: AnyWindowHandle,
} }
impl WindowInputHandler { impl WindowInputHandler {
@ -21,13 +21,12 @@ impl WindowInputHandler {
// //
// See https://github.com/zed-industries/community/issues/444 // See https://github.com/zed-industries/community/issues/444
let mut app = self.app.try_borrow_mut().ok()?; let mut app = self.app.try_borrow_mut().ok()?;
app.update_window(self.window_id, |cx| { self.window.update_optional(&mut *app, |cx| {
let view_id = cx.window.focused_view_id?; let view_id = cx.window.focused_view_id?;
let view = cx.views.get(&(self.window_id, view_id))?; let view = cx.views.get(&(self.window, view_id))?;
let result = f(view.as_ref(), &cx); let result = f(view.as_ref(), &cx);
Some(result) Some(result)
}) })
.flatten()
} }
fn update_focused_view<T, F>(&mut self, f: F) -> Option<T> fn update_focused_view<T, F>(&mut self, f: F) -> Option<T>
@ -35,7 +34,8 @@ impl WindowInputHandler {
F: FnOnce(&mut dyn AnyView, &mut WindowContext, usize) -> T, F: FnOnce(&mut dyn AnyView, &mut WindowContext, usize) -> T,
{ {
let mut app = self.app.try_borrow_mut().ok()?; let mut app = self.app.try_borrow_mut().ok()?;
app.update_window(self.window_id, |cx| { self.window
.update(&mut *app, |cx| {
let view_id = cx.window.focused_view_id?; let view_id = cx.window.focused_view_id?;
cx.update_any_view(view_id, |view, cx| f(view, cx, view_id)) cx.update_any_view(view_id, |view, cx| f(view, cx, view_id))
}) })
@ -83,9 +83,8 @@ impl InputHandler for WindowInputHandler {
} }
fn rect_for_range(&self, range_utf16: Range<usize>) -> Option<RectF> { fn rect_for_range(&self, range_utf16: Range<usize>) -> Option<RectF> {
self.app self.window.read_optional_with(&*self.app.borrow(), |cx| {
.borrow() cx.rect_for_text_range(range_utf16)
.read_window(self.window_id, |cx| cx.rect_for_text_range(range_utf16)) })
.flatten()
} }
} }

View file

@ -147,6 +147,9 @@ impl<V: View> Element<V> for Resizable<V> {
let max_size = side.relevant_component(constraint.max); let max_size = side.relevant_component(constraint.max);
let on_resize = self.on_resize.clone(); let on_resize = self.on_resize.clone();
move |event, view: &mut V, cx| { move |event, view: &mut V, cx| {
if event.end {
return;
}
let new_size = min_size let new_size = min_size
.max(prev_size + side.compute_delta(event)) .max(prev_size + side.compute_delta(event))
.min(max_size) .min(max_size)

View file

@ -19,7 +19,7 @@ use crate::{
}, },
keymap_matcher::KeymapMatcher, keymap_matcher::KeymapMatcher,
text_layout::{LineLayout, RunStyle}, text_layout::{LineLayout, RunStyle},
Action, ClipboardItem, Menu, Scene, Action, AnyWindowHandle, ClipboardItem, Menu, Scene,
}; };
use anyhow::{anyhow, bail, Result}; use anyhow::{anyhow, bail, Result};
use async_task::Runnable; use async_task::Runnable;
@ -58,13 +58,13 @@ pub trait Platform: Send + Sync {
fn open_window( fn open_window(
&self, &self,
id: usize, handle: AnyWindowHandle,
options: WindowOptions, options: WindowOptions,
executor: Rc<executor::Foreground>, executor: Rc<executor::Foreground>,
) -> Box<dyn Window>; ) -> Box<dyn Window>;
fn main_window_id(&self) -> Option<usize>; fn main_window(&self) -> Option<AnyWindowHandle>;
fn add_status_item(&self, id: usize) -> Box<dyn Window>; fn add_status_item(&self, handle: AnyWindowHandle) -> Box<dyn Window>;
fn write_to_clipboard(&self, item: ClipboardItem); fn write_to_clipboard(&self, item: ClipboardItem);
fn read_from_clipboard(&self) -> Option<ClipboardItem>; fn read_from_clipboard(&self) -> Option<ClipboardItem>;

View file

@ -21,7 +21,7 @@ pub use fonts::FontSystem;
use platform::{MacForegroundPlatform, MacPlatform}; use platform::{MacForegroundPlatform, MacPlatform};
pub use renderer::Surface; pub use renderer::Surface;
use std::{ops::Range, rc::Rc, sync::Arc}; use std::{ops::Range, rc::Rc, sync::Arc};
use window::Window; use window::MacWindow;
use crate::executor; use crate::executor;

View file

@ -1,12 +1,12 @@
use super::{ use super::{
event::key_to_native, screen::Screen, status_item::StatusItem, BoolExt as _, Dispatcher, event::key_to_native, screen::Screen, status_item::StatusItem, BoolExt as _, Dispatcher,
FontSystem, Window, FontSystem, MacWindow,
}; };
use crate::{ use crate::{
executor, executor,
keymap_matcher::KeymapMatcher, keymap_matcher::KeymapMatcher,
platform::{self, AppVersion, CursorStyle, Event}, platform::{self, AppVersion, CursorStyle, Event},
Action, ClipboardItem, Menu, MenuItem, Action, AnyWindowHandle, ClipboardItem, Menu, MenuItem,
}; };
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use block::ConcreteBlock; use block::ConcreteBlock;
@ -590,18 +590,18 @@ impl platform::Platform for MacPlatform {
fn open_window( fn open_window(
&self, &self,
id: usize, handle: AnyWindowHandle,
options: platform::WindowOptions, options: platform::WindowOptions,
executor: Rc<executor::Foreground>, executor: Rc<executor::Foreground>,
) -> Box<dyn platform::Window> { ) -> Box<dyn platform::Window> {
Box::new(Window::open(id, options, executor, self.fonts())) Box::new(MacWindow::open(handle, options, executor, self.fonts()))
} }
fn main_window_id(&self) -> Option<usize> { fn main_window(&self) -> Option<AnyWindowHandle> {
Window::main_window_id() MacWindow::main_window()
} }
fn add_status_item(&self, _id: usize) -> Box<dyn platform::Window> { fn add_status_item(&self, _handle: AnyWindowHandle) -> Box<dyn platform::Window> {
Box::new(StatusItem::add(self.fonts())) Box::new(StatusItem::add(self.fonts()))
} }

View file

@ -13,6 +13,7 @@ use crate::{
Event, InputHandler, KeyDownEvent, Modifiers, ModifiersChangedEvent, MouseButton, Event, InputHandler, KeyDownEvent, Modifiers, ModifiersChangedEvent, MouseButton,
MouseButtonEvent, MouseMovedEvent, Scene, WindowBounds, WindowKind, MouseButtonEvent, MouseMovedEvent, Scene, WindowBounds, WindowKind,
}, },
AnyWindowHandle,
}; };
use block::ConcreteBlock; use block::ConcreteBlock;
use cocoa::{ use cocoa::{
@ -282,7 +283,7 @@ struct InsertText {
} }
struct WindowState { struct WindowState {
id: usize, handle: AnyWindowHandle,
native_window: id, native_window: id,
kind: WindowKind, kind: WindowKind,
event_callback: Option<Box<dyn FnMut(Event) -> bool>>, event_callback: Option<Box<dyn FnMut(Event) -> bool>>,
@ -422,11 +423,11 @@ impl WindowState {
} }
} }
pub struct Window(Rc<RefCell<WindowState>>); pub struct MacWindow(Rc<RefCell<WindowState>>);
impl Window { impl MacWindow {
pub fn open( pub fn open(
id: usize, handle: AnyWindowHandle,
options: platform::WindowOptions, options: platform::WindowOptions,
executor: Rc<executor::Foreground>, executor: Rc<executor::Foreground>,
fonts: Arc<dyn platform::FontSystem>, fonts: Arc<dyn platform::FontSystem>,
@ -504,7 +505,7 @@ impl Window {
assert!(!native_view.is_null()); assert!(!native_view.is_null());
let window = Self(Rc::new(RefCell::new(WindowState { let window = Self(Rc::new(RefCell::new(WindowState {
id, handle,
native_window, native_window,
kind: options.kind, kind: options.kind,
event_callback: None, event_callback: None,
@ -621,13 +622,13 @@ impl Window {
} }
} }
pub fn main_window_id() -> Option<usize> { pub fn main_window() -> Option<AnyWindowHandle> {
unsafe { unsafe {
let app = NSApplication::sharedApplication(nil); let app = NSApplication::sharedApplication(nil);
let main_window: id = msg_send![app, mainWindow]; let main_window: id = msg_send![app, mainWindow];
if msg_send![main_window, isKindOfClass: WINDOW_CLASS] { if msg_send![main_window, isKindOfClass: WINDOW_CLASS] {
let id = get_window_state(&*main_window).borrow().id; let handle = get_window_state(&*main_window).borrow().handle;
Some(id) Some(handle)
} else { } else {
None None
} }
@ -635,7 +636,7 @@ impl Window {
} }
} }
impl Drop for Window { impl Drop for MacWindow {
fn drop(&mut self) { fn drop(&mut self) {
let this = self.0.borrow(); let this = self.0.borrow();
let window = this.native_window; let window = this.native_window;
@ -649,7 +650,7 @@ impl Drop for Window {
} }
} }
impl platform::Window for Window { impl platform::Window for MacWindow {
fn bounds(&self) -> WindowBounds { fn bounds(&self) -> WindowBounds {
self.0.as_ref().borrow().bounds() self.0.as_ref().borrow().bounds()
} }
@ -881,7 +882,7 @@ impl platform::Window for Window {
fn is_topmost_for_position(&self, position: Vector2F) -> bool { fn is_topmost_for_position(&self, position: Vector2F) -> bool {
let self_borrow = self.0.borrow(); let self_borrow = self.0.borrow();
let self_id = self_borrow.id; let self_handle = self_borrow.handle;
unsafe { unsafe {
let app = NSApplication::sharedApplication(nil); let app = NSApplication::sharedApplication(nil);
@ -898,8 +899,8 @@ impl platform::Window for Window {
let is_panel: BOOL = msg_send![top_most_window, isKindOfClass: PANEL_CLASS]; let is_panel: BOOL = msg_send![top_most_window, isKindOfClass: PANEL_CLASS];
let is_window: BOOL = msg_send![top_most_window, isKindOfClass: WINDOW_CLASS]; let is_window: BOOL = msg_send![top_most_window, isKindOfClass: WINDOW_CLASS];
if is_panel == YES || is_window == YES { if is_panel == YES || is_window == YES {
let topmost_window_id = get_window_state(&*top_most_window).borrow().id; let topmost_window = get_window_state(&*top_most_window).borrow().handle;
topmost_window_id == self_id topmost_window == self_handle
} else { } else {
// Someone else's window is on top // Someone else's window is on top
false false

View file

@ -5,7 +5,7 @@ use crate::{
vector::{vec2f, Vector2F}, vector::{vec2f, Vector2F},
}, },
keymap_matcher::KeymapMatcher, keymap_matcher::KeymapMatcher,
Action, ClipboardItem, Menu, Action, AnyWindowHandle, ClipboardItem, Menu,
}; };
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use collections::VecDeque; use collections::VecDeque;
@ -102,7 +102,7 @@ pub struct Platform {
fonts: Arc<dyn super::FontSystem>, fonts: Arc<dyn super::FontSystem>,
current_clipboard_item: Mutex<Option<ClipboardItem>>, current_clipboard_item: Mutex<Option<ClipboardItem>>,
cursor: Mutex<CursorStyle>, cursor: Mutex<CursorStyle>,
active_window_id: Arc<Mutex<Option<usize>>>, active_window: Arc<Mutex<Option<AnyWindowHandle>>>,
} }
impl Platform { impl Platform {
@ -112,7 +112,7 @@ impl Platform {
fonts: Arc::new(super::current::FontSystem::new()), fonts: Arc::new(super::current::FontSystem::new()),
current_clipboard_item: Default::default(), current_clipboard_item: Default::default(),
cursor: Mutex::new(CursorStyle::Arrow), cursor: Mutex::new(CursorStyle::Arrow),
active_window_id: Default::default(), active_window: Default::default(),
} }
} }
} }
@ -146,30 +146,30 @@ impl super::Platform for Platform {
fn open_window( fn open_window(
&self, &self,
id: usize, handle: AnyWindowHandle,
options: super::WindowOptions, options: super::WindowOptions,
_executor: Rc<super::executor::Foreground>, _executor: Rc<super::executor::Foreground>,
) -> Box<dyn super::Window> { ) -> Box<dyn super::Window> {
*self.active_window_id.lock() = Some(id); *self.active_window.lock() = Some(handle);
Box::new(Window::new( Box::new(Window::new(
id, handle,
match options.bounds { match options.bounds {
WindowBounds::Maximized | WindowBounds::Fullscreen => vec2f(1024., 768.), WindowBounds::Maximized | WindowBounds::Fullscreen => vec2f(1024., 768.),
WindowBounds::Fixed(rect) => rect.size(), WindowBounds::Fixed(rect) => rect.size(),
}, },
self.active_window_id.clone(), self.active_window.clone(),
)) ))
} }
fn main_window_id(&self) -> Option<usize> { fn main_window(&self) -> Option<AnyWindowHandle> {
self.active_window_id.lock().clone() self.active_window.lock().clone()
} }
fn add_status_item(&self, id: usize) -> Box<dyn crate::platform::Window> { fn add_status_item(&self, handle: AnyWindowHandle) -> Box<dyn crate::platform::Window> {
Box::new(Window::new( Box::new(Window::new(
id, handle,
vec2f(24., 24.), vec2f(24., 24.),
self.active_window_id.clone(), self.active_window.clone(),
)) ))
} }
@ -256,7 +256,7 @@ impl super::Screen for Screen {
} }
pub struct Window { pub struct Window {
id: usize, handle: AnyWindowHandle,
pub(crate) size: Vector2F, pub(crate) size: Vector2F,
scale_factor: f32, scale_factor: f32,
current_scene: Option<crate::Scene>, current_scene: Option<crate::Scene>,
@ -270,13 +270,17 @@ pub struct Window {
pub(crate) title: Option<String>, pub(crate) title: Option<String>,
pub(crate) edited: bool, pub(crate) edited: bool,
pub(crate) pending_prompts: RefCell<VecDeque<oneshot::Sender<usize>>>, pub(crate) pending_prompts: RefCell<VecDeque<oneshot::Sender<usize>>>,
active_window_id: Arc<Mutex<Option<usize>>>, active_window: Arc<Mutex<Option<AnyWindowHandle>>>,
} }
impl Window { impl Window {
pub fn new(id: usize, size: Vector2F, active_window_id: Arc<Mutex<Option<usize>>>) -> Self { pub fn new(
handle: AnyWindowHandle,
size: Vector2F,
active_window: Arc<Mutex<Option<AnyWindowHandle>>>,
) -> Self {
Self { Self {
id, handle,
size, size,
event_handlers: Default::default(), event_handlers: Default::default(),
resize_handlers: Default::default(), resize_handlers: Default::default(),
@ -290,7 +294,7 @@ impl Window {
title: None, title: None,
edited: false, edited: false,
pending_prompts: Default::default(), pending_prompts: Default::default(),
active_window_id, active_window,
} }
} }
@ -342,7 +346,7 @@ impl super::Window for Window {
} }
fn activate(&self) { fn activate(&self) {
*self.active_window_id.lock() = Some(self.id); *self.active_window.lock() = Some(self.handle);
} }
fn set_title(&mut self, title: &str) { fn set_title(&mut self, title: &str) {

View file

@ -32,6 +32,7 @@ pub struct MouseDrag {
pub region: RectF, pub region: RectF,
pub prev_mouse_position: Vector2F, pub prev_mouse_position: Vector2F,
pub platform_event: MouseMovedEvent, pub platform_event: MouseMovedEvent,
pub end: bool,
} }
impl Deref for MouseDrag { impl Deref for MouseDrag {

View file

@ -182,8 +182,8 @@ impl CachedLspAdapter {
self.adapter.workspace_configuration(cx) self.adapter.workspace_configuration(cx)
} }
pub async fn process_diagnostics(&self, params: &mut lsp::PublishDiagnosticsParams) { pub fn process_diagnostics(&self, params: &mut lsp::PublishDiagnosticsParams) {
self.adapter.process_diagnostics(params).await self.adapter.process_diagnostics(params)
} }
pub async fn process_completion(&self, completion_item: &mut lsp::CompletionItem) { pub async fn process_completion(&self, completion_item: &mut lsp::CompletionItem) {
@ -262,7 +262,7 @@ pub trait LspAdapter: 'static + Send + Sync {
container_dir: PathBuf, container_dir: PathBuf,
) -> Option<LanguageServerBinary>; ) -> Option<LanguageServerBinary>;
async fn process_diagnostics(&self, _: &mut lsp::PublishDiagnosticsParams) {} fn process_diagnostics(&self, _: &mut lsp::PublishDiagnosticsParams) {}
async fn process_completion(&self, _: &mut lsp::CompletionItem) {} async fn process_completion(&self, _: &mut lsp::CompletionItem) {}
@ -339,6 +339,8 @@ pub struct LanguageConfig {
#[serde(default)] #[serde(default)]
pub line_comment: Option<Arc<str>>, pub line_comment: Option<Arc<str>>,
#[serde(default)] #[serde(default)]
pub collapsed_placeholder: String,
#[serde(default)]
pub block_comment: Option<(Arc<str>, Arc<str>)>, pub block_comment: Option<(Arc<str>, Arc<str>)>,
#[serde(default)] #[serde(default)]
pub overrides: HashMap<String, LanguageConfigOverride>, pub overrides: HashMap<String, LanguageConfigOverride>,
@ -408,6 +410,7 @@ impl Default for LanguageConfig {
line_comment: Default::default(), line_comment: Default::default(),
block_comment: Default::default(), block_comment: Default::default(),
overrides: Default::default(), overrides: Default::default(),
collapsed_placeholder: Default::default(),
} }
} }
} }
@ -523,9 +526,10 @@ pub struct OutlineConfig {
pub struct EmbeddingConfig { pub struct EmbeddingConfig {
pub query: Query, pub query: Query,
pub item_capture_ix: u32, pub item_capture_ix: u32,
pub name_capture_ix: u32, pub name_capture_ix: Option<u32>,
pub context_capture_ix: Option<u32>, pub context_capture_ix: Option<u32>,
pub extra_context_capture_ix: Option<u32>, pub collapse_capture_ix: Option<u32>,
pub keep_capture_ix: Option<u32>,
} }
struct InjectionConfig { struct InjectionConfig {
@ -840,8 +844,8 @@ impl LanguageRegistry {
} }
} }
} }
Err(err) => { Err(e) => {
log::error!("failed to load language {name} - {err}"); log::error!("failed to load language {name}:\n{:?}", e);
let mut state = this.state.write(); let mut state = this.state.write();
state.mark_language_loaded(id); state.mark_language_loaded(id);
if let Some(mut txs) = state.loading_languages.remove(&id) { if let Some(mut txs) = state.loading_languages.remove(&id) {
@ -849,7 +853,7 @@ impl LanguageRegistry {
let _ = tx.send(Err(anyhow!( let _ = tx.send(Err(anyhow!(
"failed to load language {}: {}", "failed to load language {}: {}",
name, name,
err e
))); )));
} }
} }
@ -1184,25 +1188,39 @@ impl Language {
pub fn with_queries(mut self, queries: LanguageQueries) -> Result<Self> { pub fn with_queries(mut self, queries: LanguageQueries) -> Result<Self> {
if let Some(query) = queries.highlights { if let Some(query) = queries.highlights {
self = self.with_highlights_query(query.as_ref())?; self = self
.with_highlights_query(query.as_ref())
.context("Error loading highlights query")?;
} }
if let Some(query) = queries.brackets { if let Some(query) = queries.brackets {
self = self.with_brackets_query(query.as_ref())?; self = self
.with_brackets_query(query.as_ref())
.context("Error loading brackets query")?;
} }
if let Some(query) = queries.indents { if let Some(query) = queries.indents {
self = self.with_indents_query(query.as_ref())?; self = self
.with_indents_query(query.as_ref())
.context("Error loading indents query")?;
} }
if let Some(query) = queries.outline { if let Some(query) = queries.outline {
self = self.with_outline_query(query.as_ref())?; self = self
.with_outline_query(query.as_ref())
.context("Error loading outline query")?;
} }
if let Some(query) = queries.embedding { if let Some(query) = queries.embedding {
self = self.with_embedding_query(query.as_ref())?; self = self
.with_embedding_query(query.as_ref())
.context("Error loading embedding query")?;
} }
if let Some(query) = queries.injections { if let Some(query) = queries.injections {
self = self.with_injection_query(query.as_ref())?; self = self
.with_injection_query(query.as_ref())
.context("Error loading injection query")?;
} }
if let Some(query) = queries.overrides { if let Some(query) = queries.overrides {
self = self.with_override_query(query.as_ref())?; self = self
.with_override_query(query.as_ref())
.context("Error loading override query")?;
} }
Ok(self) Ok(self)
} }
@ -1247,23 +1265,26 @@ impl Language {
let mut item_capture_ix = None; let mut item_capture_ix = None;
let mut name_capture_ix = None; let mut name_capture_ix = None;
let mut context_capture_ix = None; let mut context_capture_ix = None;
let mut extra_context_capture_ix = None; let mut collapse_capture_ix = None;
let mut keep_capture_ix = None;
get_capture_indices( get_capture_indices(
&query, &query,
&mut [ &mut [
("item", &mut item_capture_ix), ("item", &mut item_capture_ix),
("name", &mut name_capture_ix), ("name", &mut name_capture_ix),
("context", &mut context_capture_ix), ("context", &mut context_capture_ix),
("context.extra", &mut extra_context_capture_ix), ("keep", &mut keep_capture_ix),
("collapse", &mut collapse_capture_ix),
], ],
); );
if let Some((item_capture_ix, name_capture_ix)) = item_capture_ix.zip(name_capture_ix) { if let Some(item_capture_ix) = item_capture_ix {
grammar.embedding_config = Some(EmbeddingConfig { grammar.embedding_config = Some(EmbeddingConfig {
query, query,
item_capture_ix, item_capture_ix,
name_capture_ix, name_capture_ix,
context_capture_ix, context_capture_ix,
extra_context_capture_ix, collapse_capture_ix,
keep_capture_ix,
}); });
} }
Ok(self) Ok(self)
@ -1466,12 +1487,6 @@ impl Language {
None None
} }
pub async fn process_diagnostics(&self, diagnostics: &mut lsp::PublishDiagnosticsParams) {
for adapter in &self.adapters {
adapter.process_diagnostics(diagnostics).await;
}
}
pub async fn process_completion(self: &Arc<Self>, completion: &mut lsp::CompletionItem) { pub async fn process_completion(self: &Arc<Self>, completion: &mut lsp::CompletionItem) {
for adapter in &self.adapters { for adapter in &self.adapters {
adapter.process_completion(completion).await; adapter.process_completion(completion).await;
@ -1548,9 +1563,20 @@ impl Language {
pub fn grammar(&self) -> Option<&Arc<Grammar>> { pub fn grammar(&self) -> Option<&Arc<Grammar>> {
self.grammar.as_ref() self.grammar.as_ref()
} }
pub fn default_scope(self: &Arc<Self>) -> LanguageScope {
LanguageScope {
language: self.clone(),
override_id: None,
}
}
} }
impl LanguageScope { impl LanguageScope {
pub fn collapsed_placeholder(&self) -> &str {
self.language.config.collapsed_placeholder.as_ref()
}
pub fn line_comment_prefix(&self) -> Option<&Arc<str>> { pub fn line_comment_prefix(&self) -> Option<&Arc<str>> {
Override::as_option( Override::as_option(
self.config_override().map(|o| &o.line_comment), self.config_override().map(|o| &o.line_comment),
@ -1724,7 +1750,7 @@ impl LspAdapter for Arc<FakeLspAdapter> {
unreachable!(); unreachable!();
} }
async fn process_diagnostics(&self, _: &mut lsp::PublishDiagnosticsParams) {} fn process_diagnostics(&self, _: &mut lsp::PublishDiagnosticsParams) {}
async fn disk_based_diagnostic_sources(&self) -> Vec<String> { async fn disk_based_diagnostic_sources(&self) -> Vec<String> {
self.disk_based_diagnostics_sources.clone() self.disk_based_diagnostics_sources.clone()

View file

@ -61,7 +61,9 @@ async fn test_lsp_logs(cx: &mut TestAppContext) {
.receive_notification::<lsp::notification::DidOpenTextDocument>() .receive_notification::<lsp::notification::DidOpenTextDocument>()
.await; .await;
let (_, log_view) = cx.add_window(|cx| LspLogView::new(project.clone(), log_store.clone(), cx)); let log_view = cx
.add_window(|cx| LspLogView::new(project.clone(), log_store.clone(), cx))
.root(cx);
language_server.notify::<lsp::notification::LogMessage>(lsp::LogMessageParams { language_server.notify::<lsp::notification::LogMessage>(lsp::LogMessageParams {
message: "hello from the server".into(), message: "hello from the server".into(),

View file

@ -58,11 +58,14 @@ fn build_bridge(swift_target: &SwiftTarget) {
"cargo:rerun-if-changed={}/Package.resolved", "cargo:rerun-if-changed={}/Package.resolved",
SWIFT_PACKAGE_NAME SWIFT_PACKAGE_NAME
); );
let swift_package_root = swift_package_root(); let swift_package_root = swift_package_root();
let swift_target_folder = swift_target_folder();
if !Command::new("swift") if !Command::new("swift")
.arg("build") .arg("build")
.args(["--configuration", &env::var("PROFILE").unwrap()]) .args(["--configuration", &env::var("PROFILE").unwrap()])
.args(["--triple", &swift_target.target.triple]) .args(["--triple", &swift_target.target.triple])
.args(["--build-path".into(), swift_target_folder])
.current_dir(&swift_package_root) .current_dir(&swift_package_root)
.status() .status()
.unwrap() .unwrap()
@ -128,6 +131,12 @@ fn swift_package_root() -> PathBuf {
env::current_dir().unwrap().join(SWIFT_PACKAGE_NAME) env::current_dir().unwrap().join(SWIFT_PACKAGE_NAME)
} }
fn swift_target_folder() -> PathBuf {
env::current_dir()
.unwrap()
.join(format!("../../target/{SWIFT_PACKAGE_NAME}"))
}
fn copy_dir(source: &Path, destination: &Path) { fn copy_dir(source: &Path, destination: &Path) {
assert!( assert!(
Command::new("rm") Command::new("rm")
@ -155,8 +164,7 @@ fn copy_dir(source: &Path, destination: &Path) {
impl SwiftTarget { impl SwiftTarget {
fn out_dir_path(&self) -> PathBuf { fn out_dir_path(&self) -> PathBuf {
swift_package_root() swift_target_folder()
.join(".build")
.join(&self.target.unversioned_triple) .join(&self.target.unversioned_triple)
.join(env::var("PROFILE").unwrap()) .join(env::var("PROFILE").unwrap())
} }

View file

@ -1,9 +1,6 @@
use anyhow::{anyhow, bail, Context, Result}; use anyhow::{anyhow, bail, Context, Result};
use async_compression::futures::bufread::GzipDecoder; use async_compression::futures::bufread::GzipDecoder;
use async_tar::Archive; use async_tar::Archive;
use futures::lock::Mutex;
use futures::{future::Shared, FutureExt};
use gpui::{executor::Background, Task};
use serde::Deserialize; use serde::Deserialize;
use smol::{fs, io::BufReader, process::Command}; use smol::{fs, io::BufReader, process::Command};
use std::process::{Output, Stdio}; use std::process::{Output, Stdio};
@ -33,20 +30,12 @@ pub struct NpmInfoDistTags {
pub struct NodeRuntime { pub struct NodeRuntime {
http: Arc<dyn HttpClient>, http: Arc<dyn HttpClient>,
background: Arc<Background>,
installation_path: Mutex<Option<Shared<Task<Result<PathBuf, Arc<anyhow::Error>>>>>>,
} }
impl NodeRuntime { impl NodeRuntime {
pub fn instance(http: Arc<dyn HttpClient>, background: Arc<Background>) -> Arc<NodeRuntime> { pub fn instance(http: Arc<dyn HttpClient>) -> Arc<NodeRuntime> {
RUNTIME_INSTANCE RUNTIME_INSTANCE
.get_or_init(|| { .get_or_init(|| Arc::new(NodeRuntime { http }))
Arc::new(NodeRuntime {
http,
background,
installation_path: Mutex::new(None),
})
})
.clone() .clone()
} }
@ -61,7 +50,9 @@ impl NodeRuntime {
subcommand: &str, subcommand: &str,
args: &[&str], args: &[&str],
) -> Result<Output> { ) -> Result<Output> {
let attempt = |installation_path: PathBuf| async move { let attempt = || async move {
let installation_path = self.install_if_needed().await?;
let mut env_path = installation_path.join("bin").into_os_string(); let mut env_path = installation_path.join("bin").into_os_string();
if let Some(existing_path) = std::env::var_os("PATH") { if let Some(existing_path) = std::env::var_os("PATH") {
if !existing_path.is_empty() { if !existing_path.is_empty() {
@ -92,10 +83,9 @@ impl NodeRuntime {
command.output().await.map_err(|e| anyhow!("{e}")) command.output().await.map_err(|e| anyhow!("{e}"))
}; };
let installation_path = self.install_if_needed().await?; let mut output = attempt().await;
let mut output = attempt(installation_path.clone()).await;
if output.is_err() { if output.is_err() {
output = attempt(installation_path).await; output = attempt().await;
if output.is_err() { if output.is_err() {
return Err(anyhow!( return Err(anyhow!(
"failed to launch npm subcommand {subcommand} subcommand" "failed to launch npm subcommand {subcommand} subcommand"
@ -167,23 +157,8 @@ impl NodeRuntime {
} }
async fn install_if_needed(&self) -> Result<PathBuf> { async fn install_if_needed(&self) -> Result<PathBuf> {
let task = self log::info!("Node runtime install_if_needed");
.installation_path
.lock()
.await
.get_or_insert_with(|| {
let http = self.http.clone();
self.background
.spawn(async move { Self::install(http).await.map_err(Arc::new) })
.shared()
})
.clone();
task.await.map_err(|e| anyhow!("{}", e))
}
async fn install(http: Arc<dyn HttpClient>) -> Result<PathBuf> {
log::info!("installing Node runtime");
let arch = match consts::ARCH { let arch = match consts::ARCH {
"x86_64" => "x64", "x86_64" => "x64",
"aarch64" => "arm64", "aarch64" => "arm64",
@ -214,7 +189,8 @@ impl NodeRuntime {
let file_name = format!("node-{VERSION}-darwin-{arch}.tar.gz"); let file_name = format!("node-{VERSION}-darwin-{arch}.tar.gz");
let url = format!("https://nodejs.org/dist/{VERSION}/{file_name}"); let url = format!("https://nodejs.org/dist/{VERSION}/{file_name}");
let mut response = http let mut response = self
.http
.get(&url, Default::default(), true) .get(&url, Default::default(), true)
.await .await
.context("error downloading Node binary tarball")?; .context("error downloading Node binary tarball")?;

View file

@ -2769,11 +2769,10 @@ impl Project {
language_server language_server
.on_notification::<lsp::notification::PublishDiagnostics, _>({ .on_notification::<lsp::notification::PublishDiagnostics, _>({
let adapter = adapter.clone(); let adapter = adapter.clone();
move |mut params, cx| { move |mut params, mut cx| {
let this = this; let this = this;
let adapter = adapter.clone(); let adapter = adapter.clone();
cx.spawn(|mut cx| async move { adapter.process_diagnostics(&mut params);
adapter.process_diagnostics(&mut params).await;
if let Some(this) = this.upgrade(&cx) { if let Some(this) = this.upgrade(&cx) {
this.update(&mut cx, |this, cx| { this.update(&mut cx, |this, cx| {
this.update_diagnostics( this.update_diagnostics(
@ -2785,8 +2784,6 @@ impl Project {
.log_err(); .log_err();
}); });
} }
})
.detach();
} }
}) })
.detach(); .detach();

View file

@ -1,7 +1,6 @@
use crate::{worktree::WorktreeHandle, Event, *}; use crate::{search::PathMatcher, worktree::WorktreeHandle, Event, *};
use fs::{FakeFs, LineEnding, RealFs}; use fs::{FakeFs, LineEnding, RealFs};
use futures::{future, StreamExt}; use futures::{future, StreamExt};
use globset::Glob;
use gpui::{executor::Deterministic, test::subscribe, AppContext}; use gpui::{executor::Deterministic, test::subscribe, AppContext};
use language::{ use language::{
language_settings::{AllLanguageSettings, LanguageSettingsContent}, language_settings::{AllLanguageSettings, LanguageSettingsContent},
@ -3641,7 +3640,7 @@ async fn test_search_with_inclusions(cx: &mut gpui::TestAppContext) {
search_query, search_query,
false, false,
true, true,
vec![Glob::new("*.odd").unwrap().compile_matcher()], vec![PathMatcher::new("*.odd").unwrap()],
Vec::new() Vec::new()
), ),
cx cx
@ -3659,7 +3658,7 @@ async fn test_search_with_inclusions(cx: &mut gpui::TestAppContext) {
search_query, search_query,
false, false,
true, true,
vec![Glob::new("*.rs").unwrap().compile_matcher()], vec![PathMatcher::new("*.rs").unwrap()],
Vec::new() Vec::new()
), ),
cx cx
@ -3681,8 +3680,8 @@ async fn test_search_with_inclusions(cx: &mut gpui::TestAppContext) {
false, false,
true, true,
vec![ vec![
Glob::new("*.ts").unwrap().compile_matcher(), PathMatcher::new("*.ts").unwrap(),
Glob::new("*.odd").unwrap().compile_matcher(), PathMatcher::new("*.odd").unwrap(),
], ],
Vec::new() Vec::new()
), ),
@ -3705,9 +3704,9 @@ async fn test_search_with_inclusions(cx: &mut gpui::TestAppContext) {
false, false,
true, true,
vec![ vec![
Glob::new("*.rs").unwrap().compile_matcher(), PathMatcher::new("*.rs").unwrap(),
Glob::new("*.ts").unwrap().compile_matcher(), PathMatcher::new("*.ts").unwrap(),
Glob::new("*.odd").unwrap().compile_matcher(), PathMatcher::new("*.odd").unwrap(),
], ],
Vec::new() Vec::new()
), ),
@ -3752,7 +3751,7 @@ async fn test_search_with_exclusions(cx: &mut gpui::TestAppContext) {
false, false,
true, true,
Vec::new(), Vec::new(),
vec![Glob::new("*.odd").unwrap().compile_matcher()], vec![PathMatcher::new("*.odd").unwrap()],
), ),
cx cx
) )
@ -3775,7 +3774,7 @@ async fn test_search_with_exclusions(cx: &mut gpui::TestAppContext) {
false, false,
true, true,
Vec::new(), Vec::new(),
vec![Glob::new("*.rs").unwrap().compile_matcher()], vec![PathMatcher::new("*.rs").unwrap()],
), ),
cx cx
) )
@ -3797,8 +3796,8 @@ async fn test_search_with_exclusions(cx: &mut gpui::TestAppContext) {
true, true,
Vec::new(), Vec::new(),
vec![ vec![
Glob::new("*.ts").unwrap().compile_matcher(), PathMatcher::new("*.ts").unwrap(),
Glob::new("*.odd").unwrap().compile_matcher(), PathMatcher::new("*.odd").unwrap(),
], ],
), ),
cx cx
@ -3821,9 +3820,9 @@ async fn test_search_with_exclusions(cx: &mut gpui::TestAppContext) {
true, true,
Vec::new(), Vec::new(),
vec![ vec![
Glob::new("*.rs").unwrap().compile_matcher(), PathMatcher::new("*.rs").unwrap(),
Glob::new("*.ts").unwrap().compile_matcher(), PathMatcher::new("*.ts").unwrap(),
Glob::new("*.odd").unwrap().compile_matcher(), PathMatcher::new("*.odd").unwrap(),
], ],
), ),
cx cx
@ -3860,8 +3859,8 @@ async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContex
search_query, search_query,
false, false,
true, true,
vec![Glob::new("*.odd").unwrap().compile_matcher()], vec![PathMatcher::new("*.odd").unwrap()],
vec![Glob::new("*.odd").unwrap().compile_matcher()], vec![PathMatcher::new("*.odd").unwrap()],
), ),
cx cx
) )
@ -3878,8 +3877,8 @@ async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContex
search_query, search_query,
false, false,
true, true,
vec![Glob::new("*.ts").unwrap().compile_matcher()], vec![PathMatcher::new("*.ts").unwrap()],
vec![Glob::new("*.ts").unwrap().compile_matcher()], vec![PathMatcher::new("*.ts").unwrap()],
), ),
cx cx
) )
@ -3897,12 +3896,12 @@ async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContex
false, false,
true, true,
vec![ vec![
Glob::new("*.ts").unwrap().compile_matcher(), PathMatcher::new("*.ts").unwrap(),
Glob::new("*.odd").unwrap().compile_matcher() PathMatcher::new("*.odd").unwrap()
], ],
vec![ vec![
Glob::new("*.ts").unwrap().compile_matcher(), PathMatcher::new("*.ts").unwrap(),
Glob::new("*.odd").unwrap().compile_matcher() PathMatcher::new("*.odd").unwrap()
], ],
), ),
cx cx
@ -3921,12 +3920,12 @@ async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContex
false, false,
true, true,
vec![ vec![
Glob::new("*.ts").unwrap().compile_matcher(), PathMatcher::new("*.ts").unwrap(),
Glob::new("*.odd").unwrap().compile_matcher() PathMatcher::new("*.odd").unwrap()
], ],
vec![ vec![
Glob::new("*.rs").unwrap().compile_matcher(), PathMatcher::new("*.rs").unwrap(),
Glob::new("*.odd").unwrap().compile_matcher() PathMatcher::new("*.odd").unwrap()
], ],
), ),
cx cx

View file

@ -1,5 +1,5 @@
use aho_corasick::{AhoCorasick, AhoCorasickBuilder}; use aho_corasick::{AhoCorasick, AhoCorasickBuilder};
use anyhow::Result; use anyhow::{Context, Result};
use client::proto; use client::proto;
use globset::{Glob, GlobMatcher}; use globset::{Glob, GlobMatcher};
use itertools::Itertools; use itertools::Itertools;
@ -9,7 +9,7 @@ use smol::future::yield_now;
use std::{ use std::{
io::{BufRead, BufReader, Read}, io::{BufRead, BufReader, Read},
ops::Range, ops::Range,
path::Path, path::{Path, PathBuf},
sync::Arc, sync::Arc,
}; };
@ -20,8 +20,8 @@ pub enum SearchQuery {
query: Arc<str>, query: Arc<str>,
whole_word: bool, whole_word: bool,
case_sensitive: bool, case_sensitive: bool,
files_to_include: Vec<GlobMatcher>, files_to_include: Vec<PathMatcher>,
files_to_exclude: Vec<GlobMatcher>, files_to_exclude: Vec<PathMatcher>,
}, },
Regex { Regex {
regex: Regex, regex: Regex,
@ -29,18 +29,43 @@ pub enum SearchQuery {
multiline: bool, multiline: bool,
whole_word: bool, whole_word: bool,
case_sensitive: bool, case_sensitive: bool,
files_to_include: Vec<GlobMatcher>, files_to_include: Vec<PathMatcher>,
files_to_exclude: Vec<GlobMatcher>, files_to_exclude: Vec<PathMatcher>,
}, },
} }
#[derive(Clone, Debug)]
pub struct PathMatcher {
maybe_path: PathBuf,
glob: GlobMatcher,
}
impl std::fmt::Display for PathMatcher {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.maybe_path.to_string_lossy().fmt(f)
}
}
impl PathMatcher {
pub fn new(maybe_glob: &str) -> Result<Self, globset::Error> {
Ok(PathMatcher {
glob: Glob::new(&maybe_glob)?.compile_matcher(),
maybe_path: PathBuf::from(maybe_glob),
})
}
pub fn is_match<P: AsRef<Path>>(&self, other: P) -> bool {
other.as_ref().starts_with(&self.maybe_path) || self.glob.is_match(other)
}
}
impl SearchQuery { impl SearchQuery {
pub fn text( pub fn text(
query: impl ToString, query: impl ToString,
whole_word: bool, whole_word: bool,
case_sensitive: bool, case_sensitive: bool,
files_to_include: Vec<GlobMatcher>, files_to_include: Vec<PathMatcher>,
files_to_exclude: Vec<GlobMatcher>, files_to_exclude: Vec<PathMatcher>,
) -> Self { ) -> Self {
let query = query.to_string(); let query = query.to_string();
let search = AhoCorasickBuilder::new() let search = AhoCorasickBuilder::new()
@ -61,8 +86,8 @@ impl SearchQuery {
query: impl ToString, query: impl ToString,
whole_word: bool, whole_word: bool,
case_sensitive: bool, case_sensitive: bool,
files_to_include: Vec<GlobMatcher>, files_to_include: Vec<PathMatcher>,
files_to_exclude: Vec<GlobMatcher>, files_to_exclude: Vec<PathMatcher>,
) -> Result<Self> { ) -> Result<Self> {
let mut query = query.to_string(); let mut query = query.to_string();
let initial_query = Arc::from(query.as_str()); let initial_query = Arc::from(query.as_str());
@ -96,16 +121,16 @@ impl SearchQuery {
message.query, message.query,
message.whole_word, message.whole_word,
message.case_sensitive, message.case_sensitive,
deserialize_globs(&message.files_to_include)?, deserialize_path_matches(&message.files_to_include)?,
deserialize_globs(&message.files_to_exclude)?, deserialize_path_matches(&message.files_to_exclude)?,
) )
} else { } else {
Ok(Self::text( Ok(Self::text(
message.query, message.query,
message.whole_word, message.whole_word,
message.case_sensitive, message.case_sensitive,
deserialize_globs(&message.files_to_include)?, deserialize_path_matches(&message.files_to_include)?,
deserialize_globs(&message.files_to_exclude)?, deserialize_path_matches(&message.files_to_exclude)?,
)) ))
} }
} }
@ -120,12 +145,12 @@ impl SearchQuery {
files_to_include: self files_to_include: self
.files_to_include() .files_to_include()
.iter() .iter()
.map(|g| g.glob().to_string()) .map(|matcher| matcher.to_string())
.join(","), .join(","),
files_to_exclude: self files_to_exclude: self
.files_to_exclude() .files_to_exclude()
.iter() .iter()
.map(|g| g.glob().to_string()) .map(|matcher| matcher.to_string())
.join(","), .join(","),
} }
} }
@ -266,7 +291,7 @@ impl SearchQuery {
matches!(self, Self::Regex { .. }) matches!(self, Self::Regex { .. })
} }
pub fn files_to_include(&self) -> &[GlobMatcher] { pub fn files_to_include(&self) -> &[PathMatcher] {
match self { match self {
Self::Text { Self::Text {
files_to_include, .. files_to_include, ..
@ -277,7 +302,7 @@ impl SearchQuery {
} }
} }
pub fn files_to_exclude(&self) -> &[GlobMatcher] { pub fn files_to_exclude(&self) -> &[PathMatcher] {
match self { match self {
Self::Text { Self::Text {
files_to_exclude, .. files_to_exclude, ..
@ -306,11 +331,63 @@ impl SearchQuery {
} }
} }
fn deserialize_globs(glob_set: &str) -> Result<Vec<GlobMatcher>> { fn deserialize_path_matches(glob_set: &str) -> anyhow::Result<Vec<PathMatcher>> {
glob_set glob_set
.split(',') .split(',')
.map(str::trim) .map(str::trim)
.filter(|glob_str| !glob_str.is_empty()) .filter(|glob_str| !glob_str.is_empty())
.map(|glob_str| Ok(Glob::new(glob_str)?.compile_matcher())) .map(|glob_str| {
PathMatcher::new(glob_str)
.with_context(|| format!("deserializing path match glob {glob_str}"))
})
.collect() .collect()
} }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn path_matcher_creation_for_valid_paths() {
for valid_path in [
"file",
"Cargo.toml",
".DS_Store",
"~/dir/another_dir/",
"./dir/file",
"dir/[a-z].txt",
"../dir/filé",
] {
let path_matcher = PathMatcher::new(valid_path).unwrap_or_else(|e| {
panic!("Valid path {valid_path} should be accepted, but got: {e}")
});
assert!(
path_matcher.is_match(valid_path),
"Path matcher for valid path {valid_path} should match itself"
)
}
}
#[test]
fn path_matcher_creation_for_globs() {
for invalid_glob in ["dir/[].txt", "dir/[a-z.txt", "dir/{file"] {
match PathMatcher::new(invalid_glob) {
Ok(_) => panic!("Invalid glob {invalid_glob} should not be accepted"),
Err(_expected) => {}
}
}
for valid_glob in [
"dir/?ile",
"dir/*.txt",
"dir/**/file",
"dir/[a-z].txt",
"{dir,file}",
] {
match PathMatcher::new(valid_glob) {
Ok(_expected) => {}
Err(e) => panic!("Valid glob {valid_glob} should be accepted, but got: {e}"),
}
}
}
}

View file

@ -1,5 +1,5 @@
use crate::Project; use crate::Project;
use gpui::{ModelContext, ModelHandle, WeakModelHandle}; use gpui::{AnyWindowHandle, ModelContext, ModelHandle, WeakModelHandle};
use std::path::PathBuf; use std::path::PathBuf;
use terminal::{Terminal, TerminalBuilder, TerminalSettings}; use terminal::{Terminal, TerminalBuilder, TerminalSettings};
@ -11,7 +11,7 @@ impl Project {
pub fn create_terminal( pub fn create_terminal(
&mut self, &mut self,
working_directory: Option<PathBuf>, working_directory: Option<PathBuf>,
window_id: usize, window: AnyWindowHandle,
cx: &mut ModelContext<Self>, cx: &mut ModelContext<Self>,
) -> anyhow::Result<ModelHandle<Terminal>> { ) -> anyhow::Result<ModelHandle<Terminal>> {
if self.is_remote() { if self.is_remote() {
@ -27,7 +27,7 @@ impl Project {
settings.env.clone(), settings.env.clone(),
Some(settings.blinking.clone()), Some(settings.blinking.clone()),
settings.alternate_scroll, settings.alternate_scroll,
window_id, window,
) )
.map(|builder| { .map(|builder| {
let terminal_handle = cx.add_model(|cx| builder.subscribe(cx)); let terminal_handle = cx.add_model(|cx| builder.subscribe(cx));

View file

@ -2369,7 +2369,7 @@ impl BackgroundScannerState {
} }
// Remove any git repositories whose .git entry no longer exists. // Remove any git repositories whose .git entry no longer exists.
let mut snapshot = &mut self.snapshot; let snapshot = &mut self.snapshot;
let mut repositories = mem::take(&mut snapshot.git_repositories); let mut repositories = mem::take(&mut snapshot.git_repositories);
let mut repository_entries = mem::take(&mut snapshot.repository_entries); let mut repository_entries = mem::take(&mut snapshot.repository_entries);
repositories.retain(|work_directory_id, _| { repositories.retain(|work_directory_id, _| {

View file

@ -4,7 +4,7 @@ use collections::HashMap;
use gpui::{AppContext, AssetSource}; use gpui::{AppContext, AssetSource};
use serde_derive::Deserialize; use serde_derive::Deserialize;
use util::iife; use util::{iife, paths::PathExt};
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
struct TypeConfig { struct TypeConfig {
@ -48,14 +48,7 @@ impl FileAssociations {
// FIXME: Associate a type with the languages and have the file's langauge // FIXME: Associate a type with the languages and have the file's langauge
// override these associations // override these associations
iife!({ iife!({
let suffix = path let suffix = path.icon_suffix()?;
.file_name()
.and_then(|os_str| os_str.to_str())
.and_then(|file_name| {
file_name
.find('.')
.and_then(|dot_index| file_name.get(dot_index + 1..))
})?;
this.suffixes this.suffixes
.get(suffix) .get(suffix)

View file

@ -115,6 +115,7 @@ actions!(
[ [
ExpandSelectedEntry, ExpandSelectedEntry,
CollapseSelectedEntry, CollapseSelectedEntry,
CollapseAllEntries,
NewDirectory, NewDirectory,
NewFile, NewFile,
Copy, Copy,
@ -140,6 +141,7 @@ pub fn init(assets: impl AssetSource, cx: &mut AppContext) {
file_associations::init(assets, cx); file_associations::init(assets, cx);
cx.add_action(ProjectPanel::expand_selected_entry); cx.add_action(ProjectPanel::expand_selected_entry);
cx.add_action(ProjectPanel::collapse_selected_entry); cx.add_action(ProjectPanel::collapse_selected_entry);
cx.add_action(ProjectPanel::collapse_all_entries);
cx.add_action(ProjectPanel::select_prev); cx.add_action(ProjectPanel::select_prev);
cx.add_action(ProjectPanel::select_next); cx.add_action(ProjectPanel::select_next);
cx.add_action(ProjectPanel::new_file); cx.add_action(ProjectPanel::new_file);
@ -430,7 +432,7 @@ impl ProjectPanel {
menu_entries.push(ContextMenuItem::action("Reveal in Finder", RevealInFinder)); menu_entries.push(ContextMenuItem::action("Reveal in Finder", RevealInFinder));
if entry.is_dir() { if entry.is_dir() {
menu_entries.push(ContextMenuItem::action( menu_entries.push(ContextMenuItem::action(
"Search inside", "Search Inside",
NewSearchInDirectory, NewSearchInDirectory,
)); ));
} }
@ -514,6 +516,12 @@ impl ProjectPanel {
} }
} }
pub fn collapse_all_entries(&mut self, _: &CollapseAllEntries, cx: &mut ViewContext<Self>) {
self.expanded_dir_ids.clear();
self.update_visible_entries(None, cx);
cx.notify();
}
fn toggle_expanded(&mut self, entry_id: ProjectEntryId, cx: &mut ViewContext<Self>) { fn toggle_expanded(&mut self, entry_id: ProjectEntryId, cx: &mut ViewContext<Self>) {
if let Some(worktree_id) = self.project.read(cx).worktree_id_for_entry(entry_id, cx) { if let Some(worktree_id) = self.project.read(cx).worktree_id_for_entry(entry_id, cx) {
if let Some(expanded_dir_ids) = self.expanded_dir_ids.get_mut(&worktree_id) { if let Some(expanded_dir_ids) = self.expanded_dir_ids.get_mut(&worktree_id) {
@ -1407,7 +1415,7 @@ impl ProjectPanel {
if cx if cx
.global::<DragAndDrop<Workspace>>() .global::<DragAndDrop<Workspace>>()
.currently_dragged::<ProjectEntryId>(cx.window_id()) .currently_dragged::<ProjectEntryId>(cx.window())
.is_some() .is_some()
&& dragged_entry_destination && dragged_entry_destination
.as_ref() .as_ref()
@ -1451,7 +1459,7 @@ impl ProjectPanel {
.on_up(MouseButton::Left, move |_, this, cx| { .on_up(MouseButton::Left, move |_, this, cx| {
if let Some((_, dragged_entry)) = cx if let Some((_, dragged_entry)) = cx
.global::<DragAndDrop<Workspace>>() .global::<DragAndDrop<Workspace>>()
.currently_dragged::<ProjectEntryId>(cx.window_id()) .currently_dragged::<ProjectEntryId>(cx.window())
{ {
this.move_entry( this.move_entry(
*dragged_entry, *dragged_entry,
@ -1464,7 +1472,7 @@ impl ProjectPanel {
.on_move(move |_, this, cx| { .on_move(move |_, this, cx| {
if cx if cx
.global::<DragAndDrop<Workspace>>() .global::<DragAndDrop<Workspace>>()
.currently_dragged::<ProjectEntryId>(cx.window_id()) .currently_dragged::<ProjectEntryId>(cx.window())
.is_some() .is_some()
{ {
this.dragged_entry_destination = if matches!(kind, EntryKind::File(_)) { this.dragged_entry_destination = if matches!(kind, EntryKind::File(_)) {
@ -1718,7 +1726,7 @@ impl ClipboardEntry {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use gpui::{TestAppContext, ViewHandle}; use gpui::{AnyWindowHandle, TestAppContext, ViewHandle, WindowHandle};
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
use project::FakeFs; use project::FakeFs;
use serde_json::json; use serde_json::json;
@ -1772,7 +1780,9 @@ mod tests {
.await; .await;
let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await; let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); let workspace = cx
.add_window(|cx| Workspace::test_new(project.clone(), cx))
.root(cx);
let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)); let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx));
assert_eq!( assert_eq!(
visible_entries_as_strings(&panel, 0..50, cx), visible_entries_as_strings(&panel, 0..50, cx),
@ -1860,7 +1870,8 @@ mod tests {
.await; .await;
let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await; let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
let workspace = window.root(cx);
let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)); let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx));
select_path(&panel, "root1", cx); select_path(&panel, "root1", cx);
@ -1882,7 +1893,7 @@ mod tests {
// Add a file with the root folder selected. The filename editor is placed // Add a file with the root folder selected. The filename editor is placed
// before the first file in the root folder. // before the first file in the root folder.
panel.update(cx, |panel, cx| panel.new_file(&NewFile, cx)); panel.update(cx, |panel, cx| panel.new_file(&NewFile, cx));
cx.read_window(window_id, |cx| { window.read_with(cx, |cx| {
let panel = panel.read(cx); let panel = panel.read(cx);
assert!(panel.filename_editor.is_focused(cx)); assert!(panel.filename_editor.is_focused(cx));
}); });
@ -2211,7 +2222,8 @@ mod tests {
.await; .await;
let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await; let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
let workspace = window.root(cx);
let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)); let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx));
select_path(&panel, "root1", cx); select_path(&panel, "root1", cx);
@ -2233,7 +2245,7 @@ mod tests {
// Add a file with the root folder selected. The filename editor is placed // Add a file with the root folder selected. The filename editor is placed
// before the first file in the root folder. // before the first file in the root folder.
panel.update(cx, |panel, cx| panel.new_file(&NewFile, cx)); panel.update(cx, |panel, cx| panel.new_file(&NewFile, cx));
cx.read_window(window_id, |cx| { window.read_with(cx, |cx| {
let panel = panel.read(cx); let panel = panel.read(cx);
assert!(panel.filename_editor.is_focused(cx)); assert!(panel.filename_editor.is_focused(cx));
}); });
@ -2311,7 +2323,9 @@ mod tests {
.await; .await;
let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await; let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await;
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); let workspace = cx
.add_window(|cx| Workspace::test_new(project.clone(), cx))
.root(cx);
let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)); let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx));
panel.update(cx, |panel, cx| { panel.update(cx, |panel, cx| {
@ -2384,7 +2398,8 @@ mod tests {
.await; .await;
let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await; let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await;
let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
let workspace = window.root(cx);
let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)); let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx));
toggle_expand_dir(&panel, "src/test", cx); toggle_expand_dir(&panel, "src/test", cx);
@ -2401,9 +2416,9 @@ mod tests {
" third.rs" " third.rs"
] ]
); );
ensure_single_file_is_opened(window_id, &workspace, "test/first.rs", cx); ensure_single_file_is_opened(window, "test/first.rs", cx);
submit_deletion(window_id, &panel, cx); submit_deletion(window.into(), &panel, cx);
assert_eq!( assert_eq!(
visible_entries_as_strings(&panel, 0..10, cx), visible_entries_as_strings(&panel, 0..10, cx),
&[ &[
@ -2414,7 +2429,7 @@ mod tests {
], ],
"Project panel should have no deleted file, no other file is selected in it" "Project panel should have no deleted file, no other file is selected in it"
); );
ensure_no_open_items_and_panes(window_id, &workspace, cx); ensure_no_open_items_and_panes(window.into(), &workspace, cx);
select_path(&panel, "src/test/second.rs", cx); select_path(&panel, "src/test/second.rs", cx);
panel.update(cx, |panel, cx| panel.open_file(&Open, cx)); panel.update(cx, |panel, cx| panel.open_file(&Open, cx));
@ -2428,9 +2443,9 @@ mod tests {
" third.rs" " third.rs"
] ]
); );
ensure_single_file_is_opened(window_id, &workspace, "test/second.rs", cx); ensure_single_file_is_opened(window, "test/second.rs", cx);
cx.update_window(window_id, |cx| { window.update(cx, |cx| {
let active_items = workspace let active_items = workspace
.read(cx) .read(cx)
.panes() .panes()
@ -2446,13 +2461,13 @@ mod tests {
.expect("Open item should be an editor"); .expect("Open item should be an editor");
open_editor.update(cx, |editor, cx| editor.set_text("Another text!", cx)); open_editor.update(cx, |editor, cx| editor.set_text("Another text!", cx));
}); });
submit_deletion(window_id, &panel, cx); submit_deletion(window.into(), &panel, cx);
assert_eq!( assert_eq!(
visible_entries_as_strings(&panel, 0..10, cx), visible_entries_as_strings(&panel, 0..10, cx),
&["v src", " v test", " third.rs"], &["v src", " v test", " third.rs"],
"Project panel should have no deleted file, with one last file remaining" "Project panel should have no deleted file, with one last file remaining"
); );
ensure_no_open_items_and_panes(window_id, &workspace, cx); ensure_no_open_items_and_panes(window.into(), &workspace, cx);
} }
#[gpui::test] #[gpui::test]
@ -2473,7 +2488,8 @@ mod tests {
.await; .await;
let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await; let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await;
let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
let workspace = window.root(cx);
let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)); let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx));
select_path(&panel, "src/", cx); select_path(&panel, "src/", cx);
@ -2484,7 +2500,7 @@ mod tests {
&["v src <== selected", " > test"] &["v src <== selected", " > test"]
); );
panel.update(cx, |panel, cx| panel.new_directory(&NewDirectory, cx)); panel.update(cx, |panel, cx| panel.new_directory(&NewDirectory, cx));
cx.read_window(window_id, |cx| { window.read_with(cx, |cx| {
let panel = panel.read(cx); let panel = panel.read(cx);
assert!(panel.filename_editor.is_focused(cx)); assert!(panel.filename_editor.is_focused(cx));
}); });
@ -2515,7 +2531,7 @@ mod tests {
&["v src", " > test <== selected"] &["v src", " > test <== selected"]
); );
panel.update(cx, |panel, cx| panel.new_file(&NewFile, cx)); panel.update(cx, |panel, cx| panel.new_file(&NewFile, cx));
cx.read_window(window_id, |cx| { window.read_with(cx, |cx| {
let panel = panel.read(cx); let panel = panel.read(cx);
assert!(panel.filename_editor.is_focused(cx)); assert!(panel.filename_editor.is_focused(cx));
}); });
@ -2565,7 +2581,7 @@ mod tests {
], ],
); );
panel.update(cx, |panel, cx| panel.rename(&Rename, cx)); panel.update(cx, |panel, cx| panel.rename(&Rename, cx));
cx.read_window(window_id, |cx| { window.read_with(cx, |cx| {
let panel = panel.read(cx); let panel = panel.read(cx);
assert!(panel.filename_editor.is_focused(cx)); assert!(panel.filename_editor.is_focused(cx));
}); });
@ -2619,7 +2635,9 @@ mod tests {
.await; .await;
let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await; let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await;
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); let workspace = cx
.add_window(|cx| Workspace::test_new(project.clone(), cx))
.root(cx);
let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)); let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx));
let new_search_events_count = Arc::new(AtomicUsize::new(0)); let new_search_events_count = Arc::new(AtomicUsize::new(0));
@ -2678,6 +2696,65 @@ mod tests {
); );
} }
#[gpui::test]
async fn test_collapse_all_entries(cx: &mut gpui::TestAppContext) {
init_test_with_editor(cx);
let fs = FakeFs::new(cx.background());
fs.insert_tree(
"/project_root",
json!({
"dir_1": {
"nested_dir": {
"file_a.py": "# File contents",
"file_b.py": "# File contents",
"file_c.py": "# File contents",
},
"file_1.py": "# File contents",
"file_2.py": "# File contents",
"file_3.py": "# File contents",
},
"dir_2": {
"file_1.py": "# File contents",
"file_2.py": "# File contents",
"file_3.py": "# File contents",
}
}),
)
.await;
let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
let workspace = cx
.add_window(|cx| Workspace::test_new(project.clone(), cx))
.root(cx);
let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx));
panel.update(cx, |panel, cx| {
panel.collapse_all_entries(&CollapseAllEntries, cx)
});
cx.foreground().run_until_parked();
assert_eq!(
visible_entries_as_strings(&panel, 0..10, cx),
&["v project_root", " > dir_1", " > dir_2",]
);
// Open dir_1 and make sure nested_dir was collapsed when running collapse_all_entries
toggle_expand_dir(&panel, "project_root/dir_1", cx);
cx.foreground().run_until_parked();
assert_eq!(
visible_entries_as_strings(&panel, 0..10, cx),
&[
"v project_root",
" v dir_1 <== selected",
" > nested_dir",
" file_1.py",
" file_2.py",
" file_3.py",
" > dir_2",
]
);
}
fn toggle_expand_dir( fn toggle_expand_dir(
panel: &ViewHandle<ProjectPanel>, panel: &ViewHandle<ProjectPanel>,
path: impl AsRef<Path>, path: impl AsRef<Path>,
@ -2801,13 +2878,11 @@ mod tests {
} }
fn ensure_single_file_is_opened( fn ensure_single_file_is_opened(
window_id: usize, window: WindowHandle<Workspace>,
workspace: &ViewHandle<Workspace>,
expected_path: &str, expected_path: &str,
cx: &mut TestAppContext, cx: &mut TestAppContext,
) { ) {
cx.read_window(window_id, |cx| { window.update_root(cx, |workspace, cx| {
let workspace = workspace.read(cx);
let worktrees = workspace.worktrees(cx).collect::<Vec<_>>(); let worktrees = workspace.worktrees(cx).collect::<Vec<_>>();
assert_eq!(worktrees.len(), 1); assert_eq!(worktrees.len(), 1);
let worktree_id = WorktreeId::from_usize(worktrees[0].id()); let worktree_id = WorktreeId::from_usize(worktrees[0].id());
@ -2829,12 +2904,12 @@ mod tests {
} }
fn submit_deletion( fn submit_deletion(
window_id: usize, window: AnyWindowHandle,
panel: &ViewHandle<ProjectPanel>, panel: &ViewHandle<ProjectPanel>,
cx: &mut TestAppContext, cx: &mut TestAppContext,
) { ) {
assert!( assert!(
!cx.has_pending_prompt(window_id), !window.has_pending_prompt(cx),
"Should have no prompts before the deletion" "Should have no prompts before the deletion"
); );
panel.update(cx, |panel, cx| { panel.update(cx, |panel, cx| {
@ -2844,27 +2919,27 @@ mod tests {
.detach_and_log_err(cx); .detach_and_log_err(cx);
}); });
assert!( assert!(
cx.has_pending_prompt(window_id), window.has_pending_prompt(cx),
"Should have a prompt after the deletion" "Should have a prompt after the deletion"
); );
cx.simulate_prompt_answer(window_id, 0); window.simulate_prompt_answer(0, cx);
assert!( assert!(
!cx.has_pending_prompt(window_id), !window.has_pending_prompt(cx),
"Should have no prompts after prompt was replied to" "Should have no prompts after prompt was replied to"
); );
cx.foreground().run_until_parked(); cx.foreground().run_until_parked();
} }
fn ensure_no_open_items_and_panes( fn ensure_no_open_items_and_panes(
window_id: usize, window: AnyWindowHandle,
workspace: &ViewHandle<Workspace>, workspace: &ViewHandle<Workspace>,
cx: &mut TestAppContext, cx: &mut TestAppContext,
) { ) {
assert!( assert!(
!cx.has_pending_prompt(window_id), !window.has_pending_prompt(cx),
"Should have no prompts after deletion operation closes the file" "Should have no prompts after deletion operation closes the file"
); );
cx.read_window(window_id, |cx| { window.read_with(cx, |cx| {
let open_project_paths = workspace let open_project_paths = workspace
.read(cx) .read(cx)
.panes() .panes()
@ -2878,3 +2953,4 @@ mod tests {
}); });
} }
} }
// TODO - a workspace command?

View file

@ -326,10 +326,11 @@ mod tests {
}, },
); );
let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
let workspace = window.root(cx);
// Create the project symbols view. // Create the project symbols view.
let symbols = cx.add_view(window_id, |cx| { let symbols = window.add_view(cx, |cx| {
ProjectSymbols::new( ProjectSymbols::new(
ProjectSymbolsDelegate::new(workspace.downgrade(), project.clone()), ProjectSymbolsDelegate::new(workspace.downgrade(), project.clone()),
cx, cx,

View file

@ -5,6 +5,7 @@ use gpui::{
elements::{Label, LabelStyle}, elements::{Label, LabelStyle},
AnyElement, Element, View, AnyElement, Element, View,
}; };
use util::paths::PathExt;
use workspace::WorkspaceLocation; use workspace::WorkspaceLocation;
pub struct HighlightedText { pub struct HighlightedText {
@ -61,7 +62,7 @@ impl HighlightedWorkspaceLocation {
.paths() .paths()
.iter() .iter()
.map(|path| { .map(|path| {
let path = util::paths::compact(&path); let path = path.compact();
let highlighted_text = Self::highlights_for_path( let highlighted_text = Self::highlights_for_path(
path.as_ref(), path.as_ref(),
&string_match.positions, &string_match.positions,

View file

@ -11,6 +11,7 @@ use highlighted_workspace_location::HighlightedWorkspaceLocation;
use ordered_float::OrderedFloat; use ordered_float::OrderedFloat;
use picker::{Picker, PickerDelegate, PickerEvent}; use picker::{Picker, PickerDelegate, PickerEvent};
use std::sync::Arc; use std::sync::Arc;
use util::paths::PathExt;
use workspace::{ use workspace::{
notifications::simple_message_notification::MessageNotification, Workspace, WorkspaceLocation, notifications::simple_message_notification::MessageNotification, Workspace, WorkspaceLocation,
WORKSPACE_DB, WORKSPACE_DB,
@ -134,7 +135,7 @@ impl PickerDelegate for RecentProjectsDelegate {
let combined_string = location let combined_string = location
.paths() .paths()
.iter() .iter()
.map(|path| util::paths::compact(&path).to_string_lossy().into_owned()) .map(|path| path.compact().to_string_lossy().into_owned())
.collect::<Vec<_>>() .collect::<Vec<_>>()
.join(""); .join("");
StringMatchCandidate::new(id, combined_string) StringMatchCandidate::new(id, combined_string)

View file

@ -20,6 +20,7 @@ settings = { path = "../settings" }
theme = { path = "../theme" } theme = { path = "../theme" }
util = { path = "../util" } util = { path = "../util" }
workspace = { path = "../workspace" } workspace = { path = "../workspace" }
semantic_index = { path = "../semantic_index" }
anyhow.workspace = true anyhow.workspace = true
futures.workspace = true futures.workspace = true
log.workspace = true log.workspace = true

View file

@ -1,6 +1,6 @@
use crate::{ use crate::{
SearchOptions, SelectAllMatches, SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, NextHistoryQuery, PreviousHistoryQuery, SearchHistory, SearchOptions, SelectAllMatches,
ToggleRegex, ToggleWholeWord, SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleRegex, ToggleWholeWord,
}; };
use collections::HashMap; use collections::HashMap;
use editor::Editor; use editor::Editor;
@ -46,6 +46,8 @@ pub fn init(cx: &mut AppContext) {
cx.add_action(BufferSearchBar::select_prev_match_on_pane); cx.add_action(BufferSearchBar::select_prev_match_on_pane);
cx.add_action(BufferSearchBar::select_all_matches_on_pane); cx.add_action(BufferSearchBar::select_all_matches_on_pane);
cx.add_action(BufferSearchBar::handle_editor_cancel); cx.add_action(BufferSearchBar::handle_editor_cancel);
cx.add_action(BufferSearchBar::next_history_query);
cx.add_action(BufferSearchBar::previous_history_query);
add_toggle_option_action::<ToggleCaseSensitive>(SearchOptions::CASE_SENSITIVE, cx); add_toggle_option_action::<ToggleCaseSensitive>(SearchOptions::CASE_SENSITIVE, cx);
add_toggle_option_action::<ToggleWholeWord>(SearchOptions::WHOLE_WORD, cx); add_toggle_option_action::<ToggleWholeWord>(SearchOptions::WHOLE_WORD, cx);
add_toggle_option_action::<ToggleRegex>(SearchOptions::REGEX, cx); add_toggle_option_action::<ToggleRegex>(SearchOptions::REGEX, cx);
@ -65,7 +67,7 @@ fn add_toggle_option_action<A: Action>(option: SearchOptions, cx: &mut AppContex
} }
pub struct BufferSearchBar { pub struct BufferSearchBar {
pub query_editor: ViewHandle<Editor>, query_editor: ViewHandle<Editor>,
active_searchable_item: Option<Box<dyn SearchableItemHandle>>, active_searchable_item: Option<Box<dyn SearchableItemHandle>>,
active_match_index: Option<usize>, active_match_index: Option<usize>,
active_searchable_item_subscription: Option<Subscription>, active_searchable_item_subscription: Option<Subscription>,
@ -76,6 +78,7 @@ pub struct BufferSearchBar {
default_options: SearchOptions, default_options: SearchOptions,
query_contains_error: bool, query_contains_error: bool,
dismissed: bool, dismissed: bool,
search_history: SearchHistory,
} }
impl Entity for BufferSearchBar { impl Entity for BufferSearchBar {
@ -106,6 +109,48 @@ impl View for BufferSearchBar {
.map(|active_searchable_item| active_searchable_item.supported_options()) .map(|active_searchable_item| active_searchable_item.supported_options())
.unwrap_or_default(); .unwrap_or_default();
let previous_query_keystrokes =
cx.binding_for_action(&PreviousHistoryQuery {})
.map(|binding| {
binding
.keystrokes()
.iter()
.map(|k| k.to_string())
.collect::<Vec<_>>()
});
let next_query_keystrokes = cx.binding_for_action(&NextHistoryQuery {}).map(|binding| {
binding
.keystrokes()
.iter()
.map(|k| k.to_string())
.collect::<Vec<_>>()
});
let new_placeholder_text = match (previous_query_keystrokes, next_query_keystrokes) {
(Some(previous_query_keystrokes), Some(next_query_keystrokes)) => {
format!(
"Search ({}/{} for previous/next query)",
previous_query_keystrokes.join(" "),
next_query_keystrokes.join(" ")
)
}
(None, Some(next_query_keystrokes)) => {
format!(
"Search ({} for next query)",
next_query_keystrokes.join(" ")
)
}
(Some(previous_query_keystrokes), None) => {
format!(
"Search ({} for previous query)",
previous_query_keystrokes.join(" ")
)
}
(None, None) => String::new(),
};
self.query_editor.update(cx, |editor, cx| {
editor.set_placeholder_text(new_placeholder_text, cx);
});
Flex::row() Flex::row()
.with_child( .with_child(
Flex::row() Flex::row()
@ -258,6 +303,7 @@ impl BufferSearchBar {
pending_search: None, pending_search: None,
query_contains_error: false, query_contains_error: false,
dismissed: true, dismissed: true,
search_history: SearchHistory::default(),
} }
} }
@ -341,7 +387,7 @@ impl BufferSearchBar {
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) -> oneshot::Receiver<()> { ) -> oneshot::Receiver<()> {
let options = options.unwrap_or(self.default_options); let options = options.unwrap_or(self.default_options);
if query != self.query_editor.read(cx).text(cx) || self.search_options != options { if query != self.query(cx) || self.search_options != options {
self.query_editor.update(cx, |query_editor, cx| { self.query_editor.update(cx, |query_editor, cx| {
query_editor.buffer().update(cx, |query_buffer, cx| { query_editor.buffer().update(cx, |query_buffer, cx| {
let len = query_buffer.len(cx); let len = query_buffer.len(cx);
@ -674,7 +720,7 @@ impl BufferSearchBar {
fn update_matches(&mut self, cx: &mut ViewContext<Self>) -> oneshot::Receiver<()> { fn update_matches(&mut self, cx: &mut ViewContext<Self>) -> oneshot::Receiver<()> {
let (done_tx, done_rx) = oneshot::channel(); let (done_tx, done_rx) = oneshot::channel();
let query = self.query_editor.read(cx).text(cx); let query = self.query(cx);
self.pending_search.take(); self.pending_search.take();
if let Some(active_searchable_item) = self.active_searchable_item.as_ref() { if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
if query.is_empty() { if query.is_empty() {
@ -707,6 +753,7 @@ impl BufferSearchBar {
) )
}; };
let query_text = query.as_str().to_string();
let matches = active_searchable_item.find_matches(query, cx); let matches = active_searchable_item.find_matches(query, cx);
let active_searchable_item = active_searchable_item.downgrade(); let active_searchable_item = active_searchable_item.downgrade();
@ -720,6 +767,7 @@ impl BufferSearchBar {
.insert(active_searchable_item.downgrade(), matches); .insert(active_searchable_item.downgrade(), matches);
this.update_match_index(cx); this.update_match_index(cx);
this.search_history.add(query_text);
if !this.dismissed { if !this.dismissed {
let matches = this let matches = this
.searchable_items_with_matches .searchable_items_with_matches
@ -753,6 +801,28 @@ impl BufferSearchBar {
cx.notify(); cx.notify();
} }
} }
fn next_history_query(&mut self, _: &NextHistoryQuery, cx: &mut ViewContext<Self>) {
if let Some(new_query) = self.search_history.next().map(str::to_string) {
let _ = self.search(&new_query, Some(self.search_options), cx);
} else {
self.search_history.reset_selection();
let _ = self.search("", Some(self.search_options), cx);
}
}
fn previous_history_query(&mut self, _: &PreviousHistoryQuery, cx: &mut ViewContext<Self>) {
if self.query(cx).is_empty() {
if let Some(new_query) = self.search_history.current().map(str::to_string) {
let _ = self.search(&new_query, Some(self.search_options), cx);
return;
}
}
if let Some(new_query) = self.search_history.previous().map(str::to_string) {
let _ = self.search(&new_query, Some(self.search_options), cx);
}
}
} }
#[cfg(test)] #[cfg(test)]
@ -779,11 +849,10 @@ mod tests {
cx, cx,
) )
}); });
let (window_id, _root_view) = cx.add_window(|_| EmptyView); let window = cx.add_window(|_| EmptyView);
let editor = window.add_view(cx, |cx| Editor::for_buffer(buffer.clone(), None, cx));
let editor = cx.add_view(window_id, |cx| Editor::for_buffer(buffer.clone(), None, cx)); let search_bar = window.add_view(cx, |cx| {
let search_bar = cx.add_view(window_id, |cx| {
let mut search_bar = BufferSearchBar::new(cx); let mut search_bar = BufferSearchBar::new(cx);
search_bar.set_active_pane_item(Some(&editor), cx); search_bar.set_active_pane_item(Some(&editor), cx);
search_bar.show(cx); search_bar.show(cx);
@ -1159,11 +1228,10 @@ mod tests {
"Should pick a query with multiple results" "Should pick a query with multiple results"
); );
let buffer = cx.add_model(|cx| Buffer::new(0, buffer_text, cx)); let buffer = cx.add_model(|cx| Buffer::new(0, buffer_text, cx));
let (window_id, _root_view) = cx.add_window(|_| EmptyView); let window = cx.add_window(|_| EmptyView);
let editor = window.add_view(cx, |cx| Editor::for_buffer(buffer.clone(), None, cx));
let editor = cx.add_view(window_id, |cx| Editor::for_buffer(buffer.clone(), None, cx)); let search_bar = window.add_view(cx, |cx| {
let search_bar = cx.add_view(window_id, |cx| {
let mut search_bar = BufferSearchBar::new(cx); let mut search_bar = BufferSearchBar::new(cx);
search_bar.set_active_pane_item(Some(&editor), cx); search_bar.set_active_pane_item(Some(&editor), cx);
search_bar.show(cx); search_bar.show(cx);
@ -1179,12 +1247,13 @@ mod tests {
search_bar.activate_current_match(cx); search_bar.activate_current_match(cx);
}); });
cx.read_window(window_id, |cx| { window.read_with(cx, |cx| {
assert!( assert!(
!editor.is_focused(cx), !editor.is_focused(cx),
"Initially, the editor should not be focused" "Initially, the editor should not be focused"
); );
}); });
let initial_selections = editor.update(cx, |editor, cx| { let initial_selections = editor.update(cx, |editor, cx| {
let initial_selections = editor.selections.display_ranges(cx); let initial_selections = editor.selections.display_ranges(cx);
assert_eq!( assert_eq!(
@ -1201,7 +1270,7 @@ mod tests {
cx.focus(search_bar.query_editor.as_any()); cx.focus(search_bar.query_editor.as_any());
search_bar.select_all_matches(&SelectAllMatches, cx); search_bar.select_all_matches(&SelectAllMatches, cx);
}); });
cx.read_window(window_id, |cx| { window.read_with(cx, |cx| {
assert!( assert!(
editor.is_focused(cx), editor.is_focused(cx),
"Should focus editor after successful SelectAllMatches" "Should focus editor after successful SelectAllMatches"
@ -1225,7 +1294,7 @@ mod tests {
search_bar.update(cx, |search_bar, cx| { search_bar.update(cx, |search_bar, cx| {
search_bar.select_next_match(&SelectNextMatch, cx); search_bar.select_next_match(&SelectNextMatch, cx);
}); });
cx.read_window(window_id, |cx| { window.read_with(cx, |cx| {
assert!( assert!(
editor.is_focused(cx), editor.is_focused(cx),
"Should still have editor focused after SelectNextMatch" "Should still have editor focused after SelectNextMatch"
@ -1254,7 +1323,7 @@ mod tests {
cx.focus(search_bar.query_editor.as_any()); cx.focus(search_bar.query_editor.as_any());
search_bar.select_all_matches(&SelectAllMatches, cx); search_bar.select_all_matches(&SelectAllMatches, cx);
}); });
cx.read_window(window_id, |cx| { window.read_with(cx, |cx| {
assert!( assert!(
editor.is_focused(cx), editor.is_focused(cx),
"Should focus editor after successful SelectAllMatches" "Should focus editor after successful SelectAllMatches"
@ -1278,7 +1347,7 @@ mod tests {
search_bar.update(cx, |search_bar, cx| { search_bar.update(cx, |search_bar, cx| {
search_bar.select_prev_match(&SelectPrevMatch, cx); search_bar.select_prev_match(&SelectPrevMatch, cx);
}); });
cx.read_window(window_id, |cx| { window.read_with(cx, |cx| {
assert!( assert!(
editor.is_focused(cx), editor.is_focused(cx),
"Should still have editor focused after SelectPrevMatch" "Should still have editor focused after SelectPrevMatch"
@ -1314,7 +1383,7 @@ mod tests {
search_bar.update(cx, |search_bar, cx| { search_bar.update(cx, |search_bar, cx| {
search_bar.select_all_matches(&SelectAllMatches, cx); search_bar.select_all_matches(&SelectAllMatches, cx);
}); });
cx.read_window(window_id, |cx| { window.read_with(cx, |cx| {
assert!( assert!(
!editor.is_focused(cx), !editor.is_focused(cx),
"Should not switch focus to editor if SelectAllMatches does not find any matches" "Should not switch focus to editor if SelectAllMatches does not find any matches"
@ -1333,4 +1402,154 @@ mod tests {
); );
}); });
} }
#[gpui::test]
async fn test_search_query_history(cx: &mut TestAppContext) {
crate::project_search::tests::init_test(cx);
let buffer_text = r#"
A regular expression (shortened as regex or regexp;[1] also referred to as
rational expression[2][3]) is a sequence of characters that specifies a search
pattern in text. Usually such patterns are used by string-searching algorithms
for "find" or "find and replace" operations on strings, or for input validation.
"#
.unindent();
let buffer = cx.add_model(|cx| Buffer::new(0, buffer_text, cx));
let window = cx.add_window(|_| EmptyView);
let editor = window.add_view(cx, |cx| Editor::for_buffer(buffer.clone(), None, cx));
let search_bar = window.add_view(cx, |cx| {
let mut search_bar = BufferSearchBar::new(cx);
search_bar.set_active_pane_item(Some(&editor), cx);
search_bar.show(cx);
search_bar
});
// Add 3 search items into the history.
search_bar
.update(cx, |search_bar, cx| search_bar.search("a", None, cx))
.await
.unwrap();
search_bar
.update(cx, |search_bar, cx| search_bar.search("b", None, cx))
.await
.unwrap();
search_bar
.update(cx, |search_bar, cx| {
search_bar.search("c", Some(SearchOptions::CASE_SENSITIVE), cx)
})
.await
.unwrap();
// Ensure that the latest search is active.
search_bar.read_with(cx, |search_bar, cx| {
assert_eq!(search_bar.query(cx), "c");
assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
});
// Next history query after the latest should set the query to the empty string.
search_bar.update(cx, |search_bar, cx| {
search_bar.next_history_query(&NextHistoryQuery, cx);
});
search_bar.read_with(cx, |search_bar, cx| {
assert_eq!(search_bar.query(cx), "");
assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
});
search_bar.update(cx, |search_bar, cx| {
search_bar.next_history_query(&NextHistoryQuery, cx);
});
search_bar.read_with(cx, |search_bar, cx| {
assert_eq!(search_bar.query(cx), "");
assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
});
// First previous query for empty current query should set the query to the latest.
search_bar.update(cx, |search_bar, cx| {
search_bar.previous_history_query(&PreviousHistoryQuery, cx);
});
search_bar.read_with(cx, |search_bar, cx| {
assert_eq!(search_bar.query(cx), "c");
assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
});
// Further previous items should go over the history in reverse order.
search_bar.update(cx, |search_bar, cx| {
search_bar.previous_history_query(&PreviousHistoryQuery, cx);
});
search_bar.read_with(cx, |search_bar, cx| {
assert_eq!(search_bar.query(cx), "b");
assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
});
// Previous items should never go behind the first history item.
search_bar.update(cx, |search_bar, cx| {
search_bar.previous_history_query(&PreviousHistoryQuery, cx);
});
search_bar.read_with(cx, |search_bar, cx| {
assert_eq!(search_bar.query(cx), "a");
assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
});
search_bar.update(cx, |search_bar, cx| {
search_bar.previous_history_query(&PreviousHistoryQuery, cx);
});
search_bar.read_with(cx, |search_bar, cx| {
assert_eq!(search_bar.query(cx), "a");
assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
});
// Next items should go over the history in the original order.
search_bar.update(cx, |search_bar, cx| {
search_bar.next_history_query(&NextHistoryQuery, cx);
});
search_bar.read_with(cx, |search_bar, cx| {
assert_eq!(search_bar.query(cx), "b");
assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
});
search_bar
.update(cx, |search_bar, cx| search_bar.search("ba", None, cx))
.await
.unwrap();
search_bar.read_with(cx, |search_bar, cx| {
assert_eq!(search_bar.query(cx), "ba");
assert_eq!(search_bar.search_options, SearchOptions::NONE);
});
// New search input should add another entry to history and move the selection to the end of the history.
search_bar.update(cx, |search_bar, cx| {
search_bar.previous_history_query(&PreviousHistoryQuery, cx);
});
search_bar.read_with(cx, |search_bar, cx| {
assert_eq!(search_bar.query(cx), "c");
assert_eq!(search_bar.search_options, SearchOptions::NONE);
});
search_bar.update(cx, |search_bar, cx| {
search_bar.previous_history_query(&PreviousHistoryQuery, cx);
});
search_bar.read_with(cx, |search_bar, cx| {
assert_eq!(search_bar.query(cx), "b");
assert_eq!(search_bar.search_options, SearchOptions::NONE);
});
search_bar.update(cx, |search_bar, cx| {
search_bar.next_history_query(&NextHistoryQuery, cx);
});
search_bar.read_with(cx, |search_bar, cx| {
assert_eq!(search_bar.query(cx), "c");
assert_eq!(search_bar.search_options, SearchOptions::NONE);
});
search_bar.update(cx, |search_bar, cx| {
search_bar.next_history_query(&NextHistoryQuery, cx);
});
search_bar.read_with(cx, |search_bar, cx| {
assert_eq!(search_bar.query(cx), "ba");
assert_eq!(search_bar.search_options, SearchOptions::NONE);
});
search_bar.update(cx, |search_bar, cx| {
search_bar.next_history_query(&NextHistoryQuery, cx);
});
search_bar.read_with(cx, |search_bar, cx| {
assert_eq!(search_bar.query(cx), "");
assert_eq!(search_bar.search_options, SearchOptions::NONE);
});
}
} }

View file

@ -1,15 +1,14 @@
use crate::{ use crate::{
SearchOptions, SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleRegex, NextHistoryQuery, PreviousHistoryQuery, SearchHistory, SearchOptions, SelectNextMatch,
ToggleWholeWord, SelectPrevMatch, ToggleCaseSensitive, ToggleRegex, ToggleWholeWord,
}; };
use anyhow::Result; use anyhow::Context;
use collections::HashMap; use collections::HashMap;
use editor::{ use editor::{
items::active_match_index, scroll::autoscroll::Autoscroll, Anchor, Editor, MultiBuffer, items::active_match_index, scroll::autoscroll::Autoscroll, Anchor, Editor, MultiBuffer,
SelectAll, MAX_TAB_TITLE_LEN, SelectAll, MAX_TAB_TITLE_LEN,
}; };
use futures::StreamExt; use futures::StreamExt;
use globset::{Glob, GlobMatcher};
use gpui::{ use gpui::{
actions, actions,
elements::*, elements::*,
@ -18,7 +17,12 @@ use gpui::{
Task, View, ViewContext, ViewHandle, WeakModelHandle, WeakViewHandle, Task, View, ViewContext, ViewHandle, WeakModelHandle, WeakViewHandle,
}; };
use menu::Confirm; use menu::Confirm;
use project::{search::SearchQuery, Entry, Project}; use postage::stream::Stream;
use project::{
search::{PathMatcher, SearchQuery},
Entry, Project,
};
use semantic_index::SemanticIndex;
use smallvec::SmallVec; use smallvec::SmallVec;
use std::{ use std::{
any::{Any, TypeId}, any::{Any, TypeId},
@ -36,7 +40,10 @@ use workspace::{
ItemNavHistory, Pane, ToolbarItemLocation, ToolbarItemView, Workspace, WorkspaceId, ItemNavHistory, Pane, ToolbarItemLocation, ToolbarItemView, Workspace, WorkspaceId,
}; };
actions!(project_search, [SearchInNew, ToggleFocus, NextField]); actions!(
project_search,
[SearchInNew, ToggleFocus, NextField, ToggleSemanticSearch]
);
#[derive(Default)] #[derive(Default)]
struct ActiveSearches(HashMap<WeakModelHandle<Project>, WeakViewHandle<ProjectSearchView>>); struct ActiveSearches(HashMap<WeakModelHandle<Project>, WeakViewHandle<ProjectSearchView>>);
@ -49,6 +56,8 @@ pub fn init(cx: &mut AppContext) {
cx.add_action(ProjectSearchBar::search_in_new); cx.add_action(ProjectSearchBar::search_in_new);
cx.add_action(ProjectSearchBar::select_next_match); cx.add_action(ProjectSearchBar::select_next_match);
cx.add_action(ProjectSearchBar::select_prev_match); cx.add_action(ProjectSearchBar::select_prev_match);
cx.add_action(ProjectSearchBar::next_history_query);
cx.add_action(ProjectSearchBar::previous_history_query);
cx.capture_action(ProjectSearchBar::tab); cx.capture_action(ProjectSearchBar::tab);
cx.capture_action(ProjectSearchBar::tab_previous); cx.capture_action(ProjectSearchBar::tab_previous);
add_toggle_option_action::<ToggleCaseSensitive>(SearchOptions::CASE_SENSITIVE, cx); add_toggle_option_action::<ToggleCaseSensitive>(SearchOptions::CASE_SENSITIVE, cx);
@ -76,6 +85,7 @@ struct ProjectSearch {
match_ranges: Vec<Range<Anchor>>, match_ranges: Vec<Range<Anchor>>,
active_query: Option<SearchQuery>, active_query: Option<SearchQuery>,
search_id: usize, search_id: usize,
search_history: SearchHistory,
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
@ -89,6 +99,7 @@ pub struct ProjectSearchView {
model: ModelHandle<ProjectSearch>, model: ModelHandle<ProjectSearch>,
query_editor: ViewHandle<Editor>, query_editor: ViewHandle<Editor>,
results_editor: ViewHandle<Editor>, results_editor: ViewHandle<Editor>,
semantic: Option<SemanticSearchState>,
search_options: SearchOptions, search_options: SearchOptions,
panels_with_errors: HashSet<InputPanel>, panels_with_errors: HashSet<InputPanel>,
active_match_index: Option<usize>, active_match_index: Option<usize>,
@ -98,6 +109,12 @@ pub struct ProjectSearchView {
excluded_files_editor: ViewHandle<Editor>, excluded_files_editor: ViewHandle<Editor>,
} }
struct SemanticSearchState {
file_count: usize,
outstanding_file_count: usize,
_progress_task: Task<()>,
}
pub struct ProjectSearchBar { pub struct ProjectSearchBar {
active_project_search: Option<ViewHandle<ProjectSearchView>>, active_project_search: Option<ViewHandle<ProjectSearchView>>,
subscription: Option<Subscription>, subscription: Option<Subscription>,
@ -117,6 +134,7 @@ impl ProjectSearch {
match_ranges: Default::default(), match_ranges: Default::default(),
active_query: None, active_query: None,
search_id: 0, search_id: 0,
search_history: SearchHistory::default(),
} }
} }
@ -130,6 +148,7 @@ impl ProjectSearch {
match_ranges: self.match_ranges.clone(), match_ranges: self.match_ranges.clone(),
active_query: self.active_query.clone(), active_query: self.active_query.clone(),
search_id: self.search_id, search_id: self.search_id,
search_history: self.search_history.clone(),
}) })
} }
@ -138,6 +157,7 @@ impl ProjectSearch {
.project .project
.update(cx, |project, cx| project.search(query.clone(), cx)); .update(cx, |project, cx| project.search(query.clone(), cx));
self.search_id += 1; self.search_id += 1;
self.search_history.add(query.as_str().to_string());
self.active_query = Some(query); self.active_query = Some(query);
self.match_ranges.clear(); self.match_ranges.clear();
self.pending_search = Some(cx.spawn_weak(|this, mut cx| async move { self.pending_search = Some(cx.spawn_weak(|this, mut cx| async move {
@ -172,6 +192,58 @@ impl ProjectSearch {
})); }));
cx.notify(); cx.notify();
} }
fn semantic_search(&mut self, query: SearchQuery, cx: &mut ModelContext<Self>) {
let search = SemanticIndex::global(cx).map(|index| {
index.update(cx, |semantic_index, cx| {
semantic_index.search_project(
self.project.clone(),
query.as_str().to_owned(),
10,
query.files_to_include().to_vec(),
query.files_to_exclude().to_vec(),
cx,
)
})
});
self.search_id += 1;
self.match_ranges.clear();
self.search_history.add(query.as_str().to_string());
self.pending_search = Some(cx.spawn(|this, mut cx| async move {
let results = search?.await.log_err()?;
let (_task, mut match_ranges) = this.update(&mut cx, |this, cx| {
this.excerpts.update(cx, |excerpts, cx| {
excerpts.clear(cx);
let matches = results
.into_iter()
.map(|result| (result.buffer, vec![result.range.start..result.range.start]))
.collect();
excerpts.stream_excerpts_with_context_lines(matches, 3, cx)
})
});
while let Some(match_range) = match_ranges.next().await {
this.update(&mut cx, |this, cx| {
this.match_ranges.push(match_range);
while let Ok(Some(match_range)) = match_ranges.try_next() {
this.match_ranges.push(match_range);
}
cx.notify();
});
}
this.update(&mut cx, |this, cx| {
this.pending_search.take();
cx.notify();
});
None
}));
cx.notify();
}
} }
pub enum ViewEvent { pub enum ViewEvent {
@ -195,13 +267,67 @@ impl View for ProjectSearchView {
enum Status {} enum Status {}
let theme = theme::current(cx).clone(); let theme = theme::current(cx).clone();
let text = if self.query_editor.read(cx).text(cx).is_empty() { let text = if model.pending_search.is_some() {
"" Cow::Borrowed("Searching...")
} else if model.pending_search.is_some() { } else if let Some(semantic) = &self.semantic {
"Searching..." if semantic.outstanding_file_count > 0 {
Cow::Owned(format!(
"Indexing. {} of {}...",
semantic.file_count - semantic.outstanding_file_count,
semantic.file_count
))
} else { } else {
"No results" Cow::Borrowed("Indexing complete")
}
} else if self.query_editor.read(cx).text(cx).is_empty() {
Cow::Borrowed("")
} else {
Cow::Borrowed("No results")
}; };
let previous_query_keystrokes =
cx.binding_for_action(&PreviousHistoryQuery {})
.map(|binding| {
binding
.keystrokes()
.iter()
.map(|k| k.to_string())
.collect::<Vec<_>>()
});
let next_query_keystrokes =
cx.binding_for_action(&NextHistoryQuery {}).map(|binding| {
binding
.keystrokes()
.iter()
.map(|k| k.to_string())
.collect::<Vec<_>>()
});
let new_placeholder_text = match (previous_query_keystrokes, next_query_keystrokes) {
(Some(previous_query_keystrokes), Some(next_query_keystrokes)) => {
format!(
"Search ({}/{} for previous/next query)",
previous_query_keystrokes.join(" "),
next_query_keystrokes.join(" ")
)
}
(None, Some(next_query_keystrokes)) => {
format!(
"Search ({} for next query)",
next_query_keystrokes.join(" ")
)
}
(Some(previous_query_keystrokes), None) => {
format!(
"Search ({} for previous query)",
previous_query_keystrokes.join(" ")
)
}
(None, None) => String::new(),
};
self.query_editor.update(cx, |editor, cx| {
editor.set_placeholder_text(new_placeholder_text, cx);
});
MouseEventHandler::<Status, _>::new(0, cx, |_, _| { MouseEventHandler::<Status, _>::new(0, cx, |_, _| {
Label::new(text, theme.search.results_status.clone()) Label::new(text, theme.search.results_status.clone())
.aligned() .aligned()
@ -490,6 +616,7 @@ impl ProjectSearchView {
model, model,
query_editor, query_editor,
results_editor, results_editor,
semantic: None,
search_options: options, search_options: options,
panels_with_errors: HashSet::new(), panels_with_errors: HashSet::new(),
active_match_index: None, active_match_index: None,
@ -509,8 +636,7 @@ impl ProjectSearchView {
if !dir_entry.is_dir() { if !dir_entry.is_dir() {
return; return;
} }
let filter_path = dir_entry.path.join("**"); let Some(filter_str) = dir_entry.path.to_str() else { return; };
let Some(filter_str) = filter_path.to_str() else { return; };
let model = cx.add_model(|cx| ProjectSearch::new(workspace.project().clone(), cx)); let model = cx.add_model(|cx| ProjectSearch::new(workspace.project().clone(), cx));
let search = cx.add_view(|cx| ProjectSearchView::new(model, cx)); let search = cx.add_view(|cx| ProjectSearchView::new(model, cx));
@ -577,6 +703,16 @@ impl ProjectSearchView {
} }
fn search(&mut self, cx: &mut ViewContext<Self>) { fn search(&mut self, cx: &mut ViewContext<Self>) {
if let Some(semantic) = &mut self.semantic {
if semantic.outstanding_file_count > 0 {
return;
}
if let Some(query) = self.build_search_query(cx) {
self.model
.update(cx, |model, cx| model.semantic_search(query, cx));
}
}
if let Some(query) = self.build_search_query(cx) { if let Some(query) = self.build_search_query(cx) {
self.model.update(cx, |model, cx| model.search(query, cx)); self.model.update(cx, |model, cx| model.search(query, cx));
} }
@ -585,7 +721,7 @@ impl ProjectSearchView {
fn build_search_query(&mut self, cx: &mut ViewContext<Self>) -> Option<SearchQuery> { fn build_search_query(&mut self, cx: &mut ViewContext<Self>) -> Option<SearchQuery> {
let text = self.query_editor.read(cx).text(cx); let text = self.query_editor.read(cx).text(cx);
let included_files = let included_files =
match Self::load_glob_set(&self.included_files_editor.read(cx).text(cx)) { match Self::parse_path_matches(&self.included_files_editor.read(cx).text(cx)) {
Ok(included_files) => { Ok(included_files) => {
self.panels_with_errors.remove(&InputPanel::Include); self.panels_with_errors.remove(&InputPanel::Include);
included_files included_files
@ -597,7 +733,7 @@ impl ProjectSearchView {
} }
}; };
let excluded_files = let excluded_files =
match Self::load_glob_set(&self.excluded_files_editor.read(cx).text(cx)) { match Self::parse_path_matches(&self.excluded_files_editor.read(cx).text(cx)) {
Ok(excluded_files) => { Ok(excluded_files) => {
self.panels_with_errors.remove(&InputPanel::Exclude); self.panels_with_errors.remove(&InputPanel::Exclude);
excluded_files excluded_files
@ -637,11 +773,14 @@ impl ProjectSearchView {
} }
} }
fn load_glob_set(text: &str) -> Result<Vec<GlobMatcher>> { fn parse_path_matches(text: &str) -> anyhow::Result<Vec<PathMatcher>> {
text.split(',') text.split(',')
.map(str::trim) .map(str::trim)
.filter(|glob_str| !glob_str.is_empty()) .filter(|maybe_glob_str| !maybe_glob_str.is_empty())
.map(|glob_str| anyhow::Ok(Glob::new(glob_str)?.compile_matcher())) .map(|maybe_glob_str| {
PathMatcher::new(maybe_glob_str)
.with_context(|| format!("parsing {maybe_glob_str} as path matcher"))
})
.collect() .collect()
} }
@ -654,6 +793,7 @@ impl ProjectSearchView {
let range_to_select = match_ranges[new_index].clone(); let range_to_select = match_ranges[new_index].clone();
self.results_editor.update(cx, |editor, cx| { self.results_editor.update(cx, |editor, cx| {
let range_to_select = editor.range_for_match(&range_to_select);
editor.unfold_ranges([range_to_select.clone()], false, true, cx); editor.unfold_ranges([range_to_select.clone()], false, true, cx);
editor.change_selections(Some(Autoscroll::fit()), cx, |s| { editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.select_ranges([range_to_select]) s.select_ranges([range_to_select])
@ -695,8 +835,12 @@ impl ProjectSearchView {
let is_new_search = self.search_id != prev_search_id; let is_new_search = self.search_id != prev_search_id;
self.results_editor.update(cx, |editor, cx| { self.results_editor.update(cx, |editor, cx| {
if is_new_search { if is_new_search {
let range_to_select = match_ranges
.first()
.clone()
.map(|range| editor.range_for_match(range));
editor.change_selections(Some(Autoscroll::fit()), cx, |s| { editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.select_ranges(match_ranges.first().cloned()) s.select_ranges(range_to_select)
}); });
} }
editor.highlight_background::<Self>( editor.highlight_background::<Self>(
@ -873,6 +1017,7 @@ impl ProjectSearchBar {
if let Some(search_view) = self.active_project_search.as_ref() { if let Some(search_view) = self.active_project_search.as_ref() {
search_view.update(cx, |search_view, cx| { search_view.update(cx, |search_view, cx| {
search_view.search_options.toggle(option); search_view.search_options.toggle(option);
search_view.semantic = None;
search_view.search(cx); search_view.search(cx);
}); });
cx.notify(); cx.notify();
@ -882,6 +1027,61 @@ impl ProjectSearchBar {
} }
} }
fn toggle_semantic_search(&mut self, cx: &mut ViewContext<Self>) -> bool {
if let Some(search_view) = self.active_project_search.as_ref() {
search_view.update(cx, |search_view, cx| {
if search_view.semantic.is_some() {
search_view.semantic = None;
} else if let Some(semantic_index) = SemanticIndex::global(cx) {
// TODO: confirm that it's ok to send this project
search_view.search_options = SearchOptions::none();
let project = search_view.model.read(cx).project.clone();
let index_task = semantic_index.update(cx, |semantic_index, cx| {
semantic_index.index_project(project, cx)
});
cx.spawn(|search_view, mut cx| async move {
let (files_to_index, mut files_remaining_rx) = index_task.await?;
search_view.update(&mut cx, |search_view, cx| {
cx.notify();
search_view.semantic = Some(SemanticSearchState {
file_count: files_to_index,
outstanding_file_count: files_to_index,
_progress_task: cx.spawn(|search_view, mut cx| async move {
while let Some(count) = files_remaining_rx.recv().await {
search_view
.update(&mut cx, |search_view, cx| {
if let Some(semantic_search_state) =
&mut search_view.semantic
{
semantic_search_state.outstanding_file_count =
count;
cx.notify();
if count == 0 {
return;
}
}
})
.ok();
}
}),
});
})?;
anyhow::Ok(())
})
.detach_and_log_err(cx);
}
cx.notify();
});
cx.notify();
true
} else {
false
}
}
fn render_nav_button( fn render_nav_button(
&self, &self,
icon: &'static str, icon: &'static str,
@ -959,6 +1159,42 @@ impl ProjectSearchBar {
.into_any() .into_any()
} }
fn render_semantic_search_button(&self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
let tooltip_style = theme::current(cx).tooltip.clone();
let is_active = if let Some(search) = self.active_project_search.as_ref() {
let search = search.read(cx);
search.semantic.is_some()
} else {
false
};
let region_id = 3;
MouseEventHandler::<Self, _>::new(region_id, cx, |state, cx| {
let theme = theme::current(cx);
let style = theme
.search
.option_button
.in_state(is_active)
.style_for(state);
Label::new("Semantic", style.text.clone())
.contained()
.with_style(style.container)
})
.on_click(MouseButton::Left, move |_, this, cx| {
this.toggle_semantic_search(cx);
})
.with_cursor_style(CursorStyle::PointingHand)
.with_tooltip::<Self>(
region_id,
format!("Toggle Semantic Search"),
Some(Box::new(ToggleSemanticSearch)),
tooltip_style,
cx,
)
.into_any()
}
fn is_option_enabled(&self, option: SearchOptions, cx: &AppContext) -> bool { fn is_option_enabled(&self, option: SearchOptions, cx: &AppContext) -> bool {
if let Some(search) = self.active_project_search.as_ref() { if let Some(search) = self.active_project_search.as_ref() {
search.read(cx).search_options.contains(option) search.read(cx).search_options.contains(option)
@ -966,6 +1202,47 @@ impl ProjectSearchBar {
false false
} }
} }
fn next_history_query(&mut self, _: &NextHistoryQuery, cx: &mut ViewContext<Self>) {
if let Some(search_view) = self.active_project_search.as_ref() {
search_view.update(cx, |search_view, cx| {
let new_query = search_view.model.update(cx, |model, _| {
if let Some(new_query) = model.search_history.next().map(str::to_string) {
new_query
} else {
model.search_history.reset_selection();
String::new()
}
});
search_view.set_query(&new_query, cx);
});
}
}
fn previous_history_query(&mut self, _: &PreviousHistoryQuery, cx: &mut ViewContext<Self>) {
if let Some(search_view) = self.active_project_search.as_ref() {
search_view.update(cx, |search_view, cx| {
if search_view.query_editor.read(cx).text(cx).is_empty() {
if let Some(new_query) = search_view
.model
.read(cx)
.search_history
.current()
.map(str::to_string)
{
search_view.set_query(&new_query, cx);
return;
}
}
if let Some(new_query) = search_view.model.update(cx, |model, _| {
model.search_history.previous().map(str::to_string)
}) {
search_view.set_query(&new_query, cx);
}
});
}
}
} }
impl Entity for ProjectSearchBar { impl Entity for ProjectSearchBar {
@ -1048,8 +1325,14 @@ impl View for ProjectSearchBar {
.with_child(self.render_nav_button(">", Direction::Next, cx)) .with_child(self.render_nav_button(">", Direction::Next, cx))
.aligned(), .aligned(),
) )
.with_child( .with_child({
let row = if SemanticIndex::enabled(cx) {
Flex::row().with_child(self.render_semantic_search_button(cx))
} else {
Flex::row() Flex::row()
};
let row = row
.with_child(self.render_option_button( .with_child(self.render_option_button(
"Case", "Case",
SearchOptions::CASE_SENSITIVE, SearchOptions::CASE_SENSITIVE,
@ -1067,8 +1350,10 @@ impl View for ProjectSearchBar {
)) ))
.contained() .contained()
.with_style(theme.search.option_button_group) .with_style(theme.search.option_button_group)
.aligned(), .aligned();
)
row
})
.contained() .contained()
.with_margin_bottom(row_spacing), .with_margin_bottom(row_spacing),
) )
@ -1139,6 +1424,7 @@ pub mod tests {
use editor::DisplayPoint; use editor::DisplayPoint;
use gpui::{color::Color, executor::Deterministic, TestAppContext}; use gpui::{color::Color, executor::Deterministic, TestAppContext};
use project::FakeFs; use project::FakeFs;
use semantic_index::semantic_index_settings::SemanticIndexSettings;
use serde_json::json; use serde_json::json;
use settings::SettingsStore; use settings::SettingsStore;
use std::sync::Arc; use std::sync::Arc;
@ -1161,7 +1447,9 @@ pub mod tests {
.await; .await;
let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
let search = cx.add_model(|cx| ProjectSearch::new(project, cx)); let search = cx.add_model(|cx| ProjectSearch::new(project, cx));
let (_, search_view) = cx.add_window(|cx| ProjectSearchView::new(search.clone(), cx)); let search_view = cx
.add_window(|cx| ProjectSearchView::new(search.clone(), cx))
.root(cx);
search_view.update(cx, |search_view, cx| { search_view.update(cx, |search_view, cx| {
search_view search_view
@ -1278,7 +1566,8 @@ pub mod tests {
) )
.await; .await;
let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx)); let window = cx.add_window(|cx| Workspace::test_new(project, cx));
let workspace = window.root(cx);
let active_item = cx.read(|cx| { let active_item = cx.read(|cx| {
workspace workspace
@ -1309,9 +1598,9 @@ pub mod tests {
}; };
let search_view_id = search_view.id(); let search_view_id = search_view.id();
cx.spawn( cx.spawn(|mut cx| async move {
|mut cx| async move { cx.dispatch_action(window_id, search_view_id, &ToggleFocus) }, window.dispatch_action(search_view_id, &ToggleFocus, &mut cx);
) })
.detach(); .detach();
deterministic.run_until_parked(); deterministic.run_until_parked();
search_view.update(cx, |search_view, cx| { search_view.update(cx, |search_view, cx| {
@ -1362,7 +1651,7 @@ pub mod tests {
); );
}); });
cx.spawn( cx.spawn(
|mut cx| async move { cx.dispatch_action(window_id, search_view_id, &ToggleFocus) }, |mut cx| async move { window.dispatch_action(search_view_id, &ToggleFocus, &mut cx) },
) )
.detach(); .detach();
deterministic.run_until_parked(); deterministic.run_until_parked();
@ -1393,9 +1682,9 @@ pub mod tests {
"Search view with mismatching query should be focused after search results are available", "Search view with mismatching query should be focused after search results are available",
); );
}); });
cx.spawn( cx.spawn(|mut cx| async move {
|mut cx| async move { cx.dispatch_action(window_id, search_view_id, &ToggleFocus) }, window.dispatch_action(search_view_id, &ToggleFocus, &mut cx);
) })
.detach(); .detach();
deterministic.run_until_parked(); deterministic.run_until_parked();
search_view.update(cx, |search_view, cx| { search_view.update(cx, |search_view, cx| {
@ -1423,9 +1712,9 @@ pub mod tests {
); );
}); });
cx.spawn( cx.spawn(|mut cx| async move {
|mut cx| async move { cx.dispatch_action(window_id, search_view_id, &ToggleFocus) }, window.dispatch_action(search_view_id, &ToggleFocus, &mut cx);
) })
.detach(); .detach();
deterministic.run_until_parked(); deterministic.run_until_parked();
search_view.update(cx, |search_view, cx| { search_view.update(cx, |search_view, cx| {
@ -1462,7 +1751,9 @@ pub mod tests {
let worktree_id = project.read_with(cx, |project, cx| { let worktree_id = project.read_with(cx, |project, cx| {
project.worktrees(cx).next().unwrap().read(cx).id() project.worktrees(cx).next().unwrap().read(cx).id()
}); });
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx)); let workspace = cx
.add_window(|cx| Workspace::test_new(project, cx))
.root(cx);
let active_item = cx.read(|cx| { let active_item = cx.read(|cx| {
workspace workspace
@ -1540,7 +1831,7 @@ pub mod tests {
search_view.included_files_editor.update(cx, |editor, cx| { search_view.included_files_editor.update(cx, |editor, cx| {
assert_eq!( assert_eq!(
editor.display_text(cx), editor.display_text(cx),
a_dir_entry.path.join("**").display().to_string(), a_dir_entry.path.to_str().unwrap(),
"New search in directory should have included dir entry path" "New search in directory should have included dir entry path"
); );
}); });
@ -1564,6 +1855,193 @@ pub mod tests {
}); });
} }
#[gpui::test]
async fn test_search_query_history(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.background());
fs.insert_tree(
"/dir",
json!({
"one.rs": "const ONE: usize = 1;",
"two.rs": "const TWO: usize = one::ONE + one::ONE;",
"three.rs": "const THREE: usize = one::ONE + two::TWO;",
"four.rs": "const FOUR: usize = one::ONE + three::THREE;",
}),
)
.await;
let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
let window = cx.add_window(|cx| Workspace::test_new(project, cx));
let workspace = window.root(cx);
workspace.update(cx, |workspace, cx| {
ProjectSearchView::deploy(workspace, &workspace::NewSearch, cx)
});
let search_view = cx.read(|cx| {
workspace
.read(cx)
.active_pane()
.read(cx)
.active_item()
.and_then(|item| item.downcast::<ProjectSearchView>())
.expect("Search view expected to appear after new search event trigger")
});
let search_bar = window.add_view(cx, |cx| {
let mut search_bar = ProjectSearchBar::new();
search_bar.set_active_pane_item(Some(&search_view), cx);
// search_bar.show(cx);
search_bar
});
// Add 3 search items into the history + another unsubmitted one.
search_view.update(cx, |search_view, cx| {
search_view.search_options = SearchOptions::CASE_SENSITIVE;
search_view
.query_editor
.update(cx, |query_editor, cx| query_editor.set_text("ONE", cx));
search_view.search(cx);
});
cx.foreground().run_until_parked();
search_view.update(cx, |search_view, cx| {
search_view
.query_editor
.update(cx, |query_editor, cx| query_editor.set_text("TWO", cx));
search_view.search(cx);
});
cx.foreground().run_until_parked();
search_view.update(cx, |search_view, cx| {
search_view
.query_editor
.update(cx, |query_editor, cx| query_editor.set_text("THREE", cx));
search_view.search(cx);
});
cx.foreground().run_until_parked();
search_view.update(cx, |search_view, cx| {
search_view.query_editor.update(cx, |query_editor, cx| {
query_editor.set_text("JUST_TEXT_INPUT", cx)
});
});
cx.foreground().run_until_parked();
// Ensure that the latest input with search settings is active.
search_view.update(cx, |search_view, cx| {
assert_eq!(
search_view.query_editor.read(cx).text(cx),
"JUST_TEXT_INPUT"
);
assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
});
// Next history query after the latest should set the query to the empty string.
search_bar.update(cx, |search_bar, cx| {
search_bar.next_history_query(&NextHistoryQuery, cx);
});
search_view.update(cx, |search_view, cx| {
assert_eq!(search_view.query_editor.read(cx).text(cx), "");
assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
});
search_bar.update(cx, |search_bar, cx| {
search_bar.next_history_query(&NextHistoryQuery, cx);
});
search_view.update(cx, |search_view, cx| {
assert_eq!(search_view.query_editor.read(cx).text(cx), "");
assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
});
// First previous query for empty current query should set the query to the latest submitted one.
search_bar.update(cx, |search_bar, cx| {
search_bar.previous_history_query(&PreviousHistoryQuery, cx);
});
search_view.update(cx, |search_view, cx| {
assert_eq!(search_view.query_editor.read(cx).text(cx), "THREE");
assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
});
// Further previous items should go over the history in reverse order.
search_bar.update(cx, |search_bar, cx| {
search_bar.previous_history_query(&PreviousHistoryQuery, cx);
});
search_view.update(cx, |search_view, cx| {
assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO");
assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
});
// Previous items should never go behind the first history item.
search_bar.update(cx, |search_bar, cx| {
search_bar.previous_history_query(&PreviousHistoryQuery, cx);
});
search_view.update(cx, |search_view, cx| {
assert_eq!(search_view.query_editor.read(cx).text(cx), "ONE");
assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
});
search_bar.update(cx, |search_bar, cx| {
search_bar.previous_history_query(&PreviousHistoryQuery, cx);
});
search_view.update(cx, |search_view, cx| {
assert_eq!(search_view.query_editor.read(cx).text(cx), "ONE");
assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
});
// Next items should go over the history in the original order.
search_bar.update(cx, |search_bar, cx| {
search_bar.next_history_query(&NextHistoryQuery, cx);
});
search_view.update(cx, |search_view, cx| {
assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO");
assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
});
search_view.update(cx, |search_view, cx| {
search_view
.query_editor
.update(cx, |query_editor, cx| query_editor.set_text("TWO_NEW", cx));
search_view.search(cx);
});
cx.foreground().run_until_parked();
search_view.update(cx, |search_view, cx| {
assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO_NEW");
assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
});
// New search input should add another entry to history and move the selection to the end of the history.
search_bar.update(cx, |search_bar, cx| {
search_bar.previous_history_query(&PreviousHistoryQuery, cx);
});
search_view.update(cx, |search_view, cx| {
assert_eq!(search_view.query_editor.read(cx).text(cx), "THREE");
assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
});
search_bar.update(cx, |search_bar, cx| {
search_bar.previous_history_query(&PreviousHistoryQuery, cx);
});
search_view.update(cx, |search_view, cx| {
assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO");
assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
});
search_bar.update(cx, |search_bar, cx| {
search_bar.next_history_query(&NextHistoryQuery, cx);
});
search_view.update(cx, |search_view, cx| {
assert_eq!(search_view.query_editor.read(cx).text(cx), "THREE");
assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
});
search_bar.update(cx, |search_bar, cx| {
search_bar.next_history_query(&NextHistoryQuery, cx);
});
search_view.update(cx, |search_view, cx| {
assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO_NEW");
assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
});
search_bar.update(cx, |search_bar, cx| {
search_bar.next_history_query(&NextHistoryQuery, cx);
});
search_view.update(cx, |search_view, cx| {
assert_eq!(search_view.query_editor.read(cx).text(cx), "");
assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
});
}
pub fn init_test(cx: &mut TestAppContext) { pub fn init_test(cx: &mut TestAppContext) {
cx.foreground().forbid_parking(); cx.foreground().forbid_parking();
let fonts = cx.font_cache(); let fonts = cx.font_cache();
@ -1573,6 +2051,7 @@ pub mod tests {
cx.update(|cx| { cx.update(|cx| {
cx.set_global(SettingsStore::test(cx)); cx.set_global(SettingsStore::test(cx));
cx.set_global(ActiveSearches::default()); cx.set_global(ActiveSearches::default());
settings::register::<SemanticIndexSettings>(cx);
theme::init((), cx); theme::init((), cx);
cx.update_global::<SettingsStore, _, _>(|store, _| { cx.update_global::<SettingsStore, _, _>(|store, _| {

View file

@ -3,6 +3,7 @@ pub use buffer_search::BufferSearchBar;
use gpui::{actions, Action, AppContext}; use gpui::{actions, Action, AppContext};
use project::search::SearchQuery; use project::search::SearchQuery;
pub use project_search::{ProjectSearchBar, ProjectSearchView}; pub use project_search::{ProjectSearchBar, ProjectSearchView};
use smallvec::SmallVec;
pub mod buffer_search; pub mod buffer_search;
pub mod project_search; pub mod project_search;
@ -21,6 +22,8 @@ actions!(
SelectNextMatch, SelectNextMatch,
SelectPrevMatch, SelectPrevMatch,
SelectAllMatches, SelectAllMatches,
NextHistoryQuery,
PreviousHistoryQuery,
] ]
); );
@ -53,6 +56,10 @@ impl SearchOptions {
} }
} }
pub fn none() -> SearchOptions {
SearchOptions::NONE
}
pub fn from_query(query: &SearchQuery) -> SearchOptions { pub fn from_query(query: &SearchQuery) -> SearchOptions {
let mut options = SearchOptions::NONE; let mut options = SearchOptions::NONE;
options.set(SearchOptions::WHOLE_WORD, query.whole_word()); options.set(SearchOptions::WHOLE_WORD, query.whole_word());
@ -61,3 +68,187 @@ impl SearchOptions {
options options
} }
} }
const SEARCH_HISTORY_LIMIT: usize = 20;
#[derive(Default, Debug, Clone)]
pub struct SearchHistory {
history: SmallVec<[String; SEARCH_HISTORY_LIMIT]>,
selected: Option<usize>,
}
impl SearchHistory {
pub fn add(&mut self, search_string: String) {
if let Some(i) = self.selected {
if search_string == self.history[i] {
return;
}
}
if let Some(previously_searched) = self.history.last_mut() {
if search_string.find(previously_searched.as_str()).is_some() {
*previously_searched = search_string;
self.selected = Some(self.history.len() - 1);
return;
}
}
self.history.push(search_string);
if self.history.len() > SEARCH_HISTORY_LIMIT {
self.history.remove(0);
}
self.selected = Some(self.history.len() - 1);
}
pub fn next(&mut self) -> Option<&str> {
let history_size = self.history.len();
if history_size == 0 {
return None;
}
let selected = self.selected?;
if selected == history_size - 1 {
return None;
}
let next_index = selected + 1;
self.selected = Some(next_index);
Some(&self.history[next_index])
}
pub fn current(&self) -> Option<&str> {
Some(&self.history[self.selected?])
}
pub fn previous(&mut self) -> Option<&str> {
let history_size = self.history.len();
if history_size == 0 {
return None;
}
let prev_index = match self.selected {
Some(selected_index) => {
if selected_index == 0 {
return None;
} else {
selected_index - 1
}
}
None => history_size - 1,
};
self.selected = Some(prev_index);
Some(&self.history[prev_index])
}
pub fn reset_selection(&mut self) {
self.selected = None;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add() {
let mut search_history = SearchHistory::default();
assert_eq!(
search_history.current(),
None,
"No current selection should be set fo the default search history"
);
search_history.add("rust".to_string());
assert_eq!(
search_history.current(),
Some("rust"),
"Newly added item should be selected"
);
// check if duplicates are not added
search_history.add("rust".to_string());
assert_eq!(
search_history.history.len(),
1,
"Should not add a duplicate"
);
assert_eq!(search_history.current(), Some("rust"));
// check if new string containing the previous string replaces it
search_history.add("rustlang".to_string());
assert_eq!(
search_history.history.len(),
1,
"Should replace previous item if it's a substring"
);
assert_eq!(search_history.current(), Some("rustlang"));
// push enough items to test SEARCH_HISTORY_LIMIT
for i in 0..SEARCH_HISTORY_LIMIT * 2 {
search_history.add(format!("item{i}"));
}
assert!(search_history.history.len() <= SEARCH_HISTORY_LIMIT);
}
#[test]
fn test_next_and_previous() {
let mut search_history = SearchHistory::default();
assert_eq!(
search_history.next(),
None,
"Default search history should not have a next item"
);
search_history.add("Rust".to_string());
assert_eq!(search_history.next(), None);
search_history.add("JavaScript".to_string());
assert_eq!(search_history.next(), None);
search_history.add("TypeScript".to_string());
assert_eq!(search_history.next(), None);
assert_eq!(search_history.current(), Some("TypeScript"));
assert_eq!(search_history.previous(), Some("JavaScript"));
assert_eq!(search_history.current(), Some("JavaScript"));
assert_eq!(search_history.previous(), Some("Rust"));
assert_eq!(search_history.current(), Some("Rust"));
assert_eq!(search_history.previous(), None);
assert_eq!(search_history.current(), Some("Rust"));
assert_eq!(search_history.next(), Some("JavaScript"));
assert_eq!(search_history.current(), Some("JavaScript"));
assert_eq!(search_history.next(), Some("TypeScript"));
assert_eq!(search_history.current(), Some("TypeScript"));
assert_eq!(search_history.next(), None);
assert_eq!(search_history.current(), Some("TypeScript"));
}
#[test]
fn test_reset_selection() {
let mut search_history = SearchHistory::default();
search_history.add("Rust".to_string());
search_history.add("JavaScript".to_string());
search_history.add("TypeScript".to_string());
assert_eq!(search_history.current(), Some("TypeScript"));
search_history.reset_selection();
assert_eq!(search_history.current(), None);
assert_eq!(
search_history.previous(),
Some("TypeScript"),
"Should start from the end after reset on previous item query"
);
search_history.previous();
assert_eq!(search_history.current(), Some("JavaScript"));
search_history.previous();
assert_eq!(search_history.current(), Some("Rust"));
search_history.reset_selection();
assert_eq!(search_history.current(), None);
}
}

View file

@ -1,11 +1,11 @@
[package] [package]
name = "vector_store" name = "semantic_index"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2021"
publish = false publish = false
[lib] [lib]
path = "src/vector_store.rs" path = "src/semantic_index.rs"
doctest = false doctest = false
[dependencies] [dependencies]
@ -20,6 +20,7 @@ editor = { path = "../editor" }
rpc = { path = "../rpc" } rpc = { path = "../rpc" }
settings = { path = "../settings" } settings = { path = "../settings" }
anyhow.workspace = true anyhow.workspace = true
postage.workspace = true
futures.workspace = true futures.workspace = true
smol.workspace = true smol.workspace = true
rusqlite = { version = "0.27.0", features = ["blob", "array", "modern_sqlite"] } rusqlite = { version = "0.27.0", features = ["blob", "array", "modern_sqlite"] }
@ -33,8 +34,10 @@ async-trait.workspace = true
bincode = "1.3.3" bincode = "1.3.3"
matrixmultiply = "0.3.7" matrixmultiply = "0.3.7"
tiktoken-rs = "0.5.0" tiktoken-rs = "0.5.0"
parking_lot.workspace = true
rand.workspace = true rand.workspace = true
schemars.workspace = true schemars.workspace = true
globset.workspace = true
[dev-dependencies] [dev-dependencies]
gpui = { path = "../gpui", features = ["test-support"] } gpui = { path = "../gpui", features = ["test-support"] }
@ -43,7 +46,20 @@ project = { path = "../project", features = ["test-support"] }
rpc = { path = "../rpc", features = ["test-support"] } rpc = { path = "../rpc", features = ["test-support"] }
workspace = { path = "../workspace", features = ["test-support"] } workspace = { path = "../workspace", features = ["test-support"] }
settings = { path = "../settings", features = ["test-support"]} settings = { path = "../settings", features = ["test-support"]}
tree-sitter-rust = "*"
pretty_assertions.workspace = true
rand.workspace = true rand.workspace = true
unindent.workspace = true unindent.workspace = true
tempdir.workspace = true tempdir.workspace = true
ctor.workspace = true
env_logger.workspace = true
tree-sitter-typescript.workspace = true
tree-sitter-json.workspace = true
tree-sitter-rust.workspace = true
tree-sitter-toml.workspace = true
tree-sitter-cpp.workspace = true
tree-sitter-elixir.workspace = true
tree-sitter-lua.workspace = true
tree-sitter-ruby.workspace = true
tree-sitter-php.workspace = true

View file

@ -1,20 +1,20 @@
use std::{ use crate::{parsing::Document, SEMANTIC_INDEX_VERSION};
cmp::Ordering, use anyhow::{anyhow, Context, Result};
collections::HashMap, use project::{search::PathMatcher, Fs};
path::{Path, PathBuf},
rc::Rc,
time::SystemTime,
};
use anyhow::{anyhow, Result};
use crate::parsing::ParsedFile;
use crate::VECTOR_STORE_VERSION;
use rpc::proto::Timestamp; use rpc::proto::Timestamp;
use rusqlite::{ use rusqlite::{
params, params,
types::{FromSql, FromSqlResult, ValueRef}, types::{FromSql, FromSqlResult, ValueRef},
}; };
use std::{
cmp::Ordering,
collections::HashMap,
ops::Range,
path::{Path, PathBuf},
rc::Rc,
sync::Arc,
time::SystemTime,
};
#[derive(Debug)] #[derive(Debug)]
pub struct FileRecord { pub struct FileRecord {
@ -42,48 +42,94 @@ pub struct VectorDatabase {
} }
impl VectorDatabase { impl VectorDatabase {
pub fn new(path: String) -> Result<Self> { pub async fn new(fs: Arc<dyn Fs>, path: Arc<PathBuf>) -> Result<Self> {
if let Some(db_directory) = path.parent() {
fs.create_dir(db_directory).await?;
}
let this = Self { let this = Self {
db: rusqlite::Connection::open(path)?, db: rusqlite::Connection::open(path.as_path())?,
}; };
this.initialize_database()?; this.initialize_database()?;
Ok(this) Ok(this)
} }
fn get_existing_version(&self) -> Result<i64> {
let mut version_query = self
.db
.prepare("SELECT version from semantic_index_config")?;
version_query
.query_row([], |row| Ok(row.get::<_, i64>(0)?))
.map_err(|err| anyhow!("version query failed: {err}"))
}
fn initialize_database(&self) -> Result<()> { fn initialize_database(&self) -> Result<()> {
rusqlite::vtab::array::load_module(&self.db)?; rusqlite::vtab::array::load_module(&self.db)?;
// This will create the database if it doesnt exist // Delete existing tables, if SEMANTIC_INDEX_VERSION is bumped
if self
.get_existing_version()
.map_or(false, |version| version == SEMANTIC_INDEX_VERSION as i64)
{
log::trace!("vector database schema up to date");
return Ok(());
}
log::trace!("vector database schema out of date. updating...");
self.db
.execute("DROP TABLE IF EXISTS documents", [])
.context("failed to drop 'documents' table")?;
self.db
.execute("DROP TABLE IF EXISTS files", [])
.context("failed to drop 'files' table")?;
self.db
.execute("DROP TABLE IF EXISTS worktrees", [])
.context("failed to drop 'worktrees' table")?;
self.db
.execute("DROP TABLE IF EXISTS semantic_index_config", [])
.context("failed to drop 'semantic_index_config' table")?;
// Initialize Vector Databasing Tables // Initialize Vector Databasing Tables
self.db.execute( self.db.execute(
"CREATE TABLE IF NOT EXISTS worktrees ( "CREATE TABLE semantic_index_config (
version INTEGER NOT NULL
)",
[],
)?;
self.db.execute(
"INSERT INTO semantic_index_config (version) VALUES (?1)",
params![SEMANTIC_INDEX_VERSION],
)?;
self.db.execute(
"CREATE TABLE worktrees (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
absolute_path VARCHAR NOT NULL absolute_path VARCHAR NOT NULL
); );
CREATE UNIQUE INDEX IF NOT EXISTS worktrees_absolute_path ON worktrees (absolute_path); CREATE UNIQUE INDEX worktrees_absolute_path ON worktrees (absolute_path);
", ",
[], [],
)?; )?;
self.db.execute( self.db.execute(
"CREATE TABLE IF NOT EXISTS files ( "CREATE TABLE files (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
worktree_id INTEGER NOT NULL, worktree_id INTEGER NOT NULL,
relative_path VARCHAR NOT NULL, relative_path VARCHAR NOT NULL,
mtime_seconds INTEGER NOT NULL, mtime_seconds INTEGER NOT NULL,
mtime_nanos INTEGER NOT NULL, mtime_nanos INTEGER NOT NULL,
vector_store_version INTEGER NOT NULL,
FOREIGN KEY(worktree_id) REFERENCES worktrees(id) ON DELETE CASCADE FOREIGN KEY(worktree_id) REFERENCES worktrees(id) ON DELETE CASCADE
)", )",
[], [],
)?; )?;
self.db.execute( self.db.execute(
"CREATE TABLE IF NOT EXISTS documents ( "CREATE TABLE documents (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
file_id INTEGER NOT NULL, file_id INTEGER NOT NULL,
offset INTEGER NOT NULL, start_byte INTEGER NOT NULL,
end_byte INTEGER NOT NULL,
name VARCHAR NOT NULL, name VARCHAR NOT NULL,
embedding BLOB NOT NULL, embedding BLOB NOT NULL,
FOREIGN KEY(file_id) REFERENCES files(id) ON DELETE CASCADE FOREIGN KEY(file_id) REFERENCES files(id) ON DELETE CASCADE
@ -91,6 +137,7 @@ impl VectorDatabase {
[], [],
)?; )?;
log::trace!("vector database initialized with updated schema.");
Ok(()) Ok(())
} }
@ -102,43 +149,44 @@ impl VectorDatabase {
Ok(()) Ok(())
} }
pub fn insert_file(&self, worktree_id: i64, indexed_file: ParsedFile) -> Result<()> { pub fn insert_file(
&self,
worktree_id: i64,
path: PathBuf,
mtime: SystemTime,
documents: Vec<Document>,
) -> Result<()> {
// Write to files table, and return generated id. // Write to files table, and return generated id.
self.db.execute( self.db.execute(
" "
DELETE FROM files WHERE worktree_id = ?1 AND relative_path = ?2; DELETE FROM files WHERE worktree_id = ?1 AND relative_path = ?2;
", ",
params![worktree_id, indexed_file.path.to_str()], params![worktree_id, path.to_str()],
)?; )?;
let mtime = Timestamp::from(indexed_file.mtime); let mtime = Timestamp::from(mtime);
self.db.execute( self.db.execute(
" "
INSERT INTO files INSERT INTO files
(worktree_id, relative_path, mtime_seconds, mtime_nanos, vector_store_version) (worktree_id, relative_path, mtime_seconds, mtime_nanos)
VALUES VALUES
(?1, ?2, $3, $4, $5); (?1, ?2, $3, $4);
", ",
params![ params![worktree_id, path.to_str(), mtime.seconds, mtime.nanos],
worktree_id,
indexed_file.path.to_str(),
mtime.seconds,
mtime.nanos,
VECTOR_STORE_VERSION
],
)?; )?;
let file_id = self.db.last_insert_rowid(); let file_id = self.db.last_insert_rowid();
// Currently inserting at approximately 3400 documents a second // Currently inserting at approximately 3400 documents a second
// I imagine we can speed this up with a bulk insert of some kind. // I imagine we can speed this up with a bulk insert of some kind.
for document in indexed_file.documents { for document in documents {
let embedding_blob = bincode::serialize(&document.embedding)?; let embedding_blob = bincode::serialize(&document.embedding)?;
self.db.execute( self.db.execute(
"INSERT INTO documents (file_id, offset, name, embedding) VALUES (?1, ?2, ?3, ?4)", "INSERT INTO documents (file_id, start_byte, end_byte, name, embedding) VALUES (?1, ?2, ?3, ?4, $5)",
params![ params![
file_id, file_id,
document.offset.to_string(), document.range.start.to_string(),
document.range.end.to_string(),
document.name, document.name,
embedding_blob embedding_blob
], ],
@ -148,6 +196,23 @@ impl VectorDatabase {
Ok(()) Ok(())
} }
pub fn worktree_previously_indexed(&self, worktree_root_path: &Path) -> Result<bool> {
let mut worktree_query = self
.db
.prepare("SELECT id FROM worktrees WHERE absolute_path = ?1")?;
let worktree_id = worktree_query
.query_row(params![worktree_root_path.to_string_lossy()], |row| {
Ok(row.get::<_, i64>(0)?)
})
.map_err(|err| anyhow!(err));
if worktree_id.is_ok() {
return Ok(true);
} else {
return Ok(false);
}
}
pub fn find_or_create_worktree(&self, worktree_root_path: &Path) -> Result<i64> { pub fn find_or_create_worktree(&self, worktree_root_path: &Path) -> Result<i64> {
// Check that the absolute path doesnt exist // Check that the absolute path doesnt exist
let mut worktree_query = self let mut worktree_query = self
@ -201,12 +266,12 @@ impl VectorDatabase {
pub fn top_k_search( pub fn top_k_search(
&self, &self,
worktree_ids: &[i64],
query_embedding: &Vec<f32>, query_embedding: &Vec<f32>,
limit: usize, limit: usize,
) -> Result<Vec<(i64, PathBuf, usize, String)>> { file_ids: &[i64],
) -> Result<Vec<(i64, f32)>> {
let mut results = Vec::<(i64, f32)>::with_capacity(limit + 1); let mut results = Vec::<(i64, f32)>::with_capacity(limit + 1);
self.for_each_document(&worktree_ids, |id, embedding| { self.for_each_document(file_ids, |id, embedding| {
let similarity = dot(&embedding, &query_embedding); let similarity = dot(&embedding, &query_embedding);
let ix = match results let ix = match results
.binary_search_by(|(_, s)| similarity.partial_cmp(&s).unwrap_or(Ordering::Equal)) .binary_search_by(|(_, s)| similarity.partial_cmp(&s).unwrap_or(Ordering::Equal))
@ -218,29 +283,57 @@ impl VectorDatabase {
results.truncate(limit); results.truncate(limit);
})?; })?;
let ids = results.into_iter().map(|(id, _)| id).collect::<Vec<_>>(); Ok(results)
self.get_documents_by_ids(&ids)
} }
fn for_each_document( pub fn retrieve_included_file_ids(
&self, &self,
worktree_ids: &[i64], worktree_ids: &[i64],
mut f: impl FnMut(i64, Vec<f32>), includes: &[PathMatcher],
) -> Result<()> { excludes: &[PathMatcher],
) -> Result<Vec<i64>> {
let mut file_query = self.db.prepare(
"
SELECT
id, relative_path
FROM
files
WHERE
worktree_id IN rarray(?)
",
)?;
let mut file_ids = Vec::<i64>::new();
let mut rows = file_query.query([ids_to_sql(worktree_ids)])?;
while let Some(row) = rows.next()? {
let file_id = row.get(0)?;
let relative_path = row.get_ref(1)?.as_str()?;
let included =
includes.is_empty() || includes.iter().any(|glob| glob.is_match(relative_path));
let excluded = excludes.iter().any(|glob| glob.is_match(relative_path));
if included && !excluded {
file_ids.push(file_id);
}
}
Ok(file_ids)
}
fn for_each_document(&self, file_ids: &[i64], mut f: impl FnMut(i64, Vec<f32>)) -> Result<()> {
let mut query_statement = self.db.prepare( let mut query_statement = self.db.prepare(
" "
SELECT SELECT
documents.id, documents.embedding id, embedding
FROM FROM
documents, files documents
WHERE WHERE
documents.file_id = files.id AND file_id IN rarray(?)
files.worktree_id IN rarray(?)
", ",
)?; )?;
query_statement query_statement
.query_map(params![ids_to_sql(worktree_ids)], |row| { .query_map(params![ids_to_sql(&file_ids)], |row| {
Ok((row.get(0)?, row.get::<_, Embedding>(1)?)) Ok((row.get(0)?, row.get::<_, Embedding>(1)?))
})? })?
.filter_map(|row| row.ok()) .filter_map(|row| row.ok())
@ -248,11 +341,15 @@ impl VectorDatabase {
Ok(()) Ok(())
} }
fn get_documents_by_ids(&self, ids: &[i64]) -> Result<Vec<(i64, PathBuf, usize, String)>> { pub fn get_documents_by_ids(&self, ids: &[i64]) -> Result<Vec<(i64, PathBuf, Range<usize>)>> {
let mut statement = self.db.prepare( let mut statement = self.db.prepare(
" "
SELECT SELECT
documents.id, files.worktree_id, files.relative_path, documents.offset, documents.name documents.id,
files.worktree_id,
files.relative_path,
documents.start_byte,
documents.end_byte
FROM FROM
documents, files documents, files
WHERE WHERE
@ -266,15 +363,14 @@ impl VectorDatabase {
row.get::<_, i64>(0)?, row.get::<_, i64>(0)?,
row.get::<_, i64>(1)?, row.get::<_, i64>(1)?,
row.get::<_, String>(2)?.into(), row.get::<_, String>(2)?.into(),
row.get(3)?, row.get(3)?..row.get(4)?,
row.get(4)?,
)) ))
})?; })?;
let mut values_by_id = HashMap::<i64, (i64, PathBuf, usize, String)>::default(); let mut values_by_id = HashMap::<i64, (i64, PathBuf, Range<usize>)>::default();
for row in result_iter { for row in result_iter {
let (id, worktree_id, path, offset, name) = row?; let (id, worktree_id, path, range) = row?;
values_by_id.insert(id, (worktree_id, path, offset, name)); values_by_id.insert(id, (worktree_id, path, range));
} }
let mut results = Vec::with_capacity(ids.len()); let mut results = Vec::with_capacity(ids.len());

View file

@ -67,17 +67,16 @@ impl EmbeddingProvider for DummyEmbeddings {
} }
} }
const INPUT_LIMIT: usize = 8190; const OPENAI_INPUT_LIMIT: usize = 8190;
impl OpenAIEmbeddings { impl OpenAIEmbeddings {
fn truncate(span: String) -> String { fn truncate(span: String) -> String {
let mut tokens = OPENAI_BPE_TOKENIZER.encode_with_special_tokens(span.as_ref()); let mut tokens = OPENAI_BPE_TOKENIZER.encode_with_special_tokens(span.as_ref());
if tokens.len() > INPUT_LIMIT { if tokens.len() > OPENAI_INPUT_LIMIT {
tokens.truncate(INPUT_LIMIT); tokens.truncate(OPENAI_INPUT_LIMIT);
let result = OPENAI_BPE_TOKENIZER.decode(tokens.clone()); let result = OPENAI_BPE_TOKENIZER.decode(tokens.clone());
if result.is_ok() { if result.is_ok() {
let transformed = result.unwrap(); let transformed = result.unwrap();
// assert_ne!(transformed, span);
return transformed; return transformed;
} }
} }
@ -88,6 +87,7 @@ impl OpenAIEmbeddings {
async fn send_request(&self, api_key: &str, spans: Vec<&str>) -> Result<Response<AsyncBody>> { async fn send_request(&self, api_key: &str, spans: Vec<&str>) -> Result<Response<AsyncBody>> {
let request = Request::post("https://api.openai.com/v1/embeddings") let request = Request::post("https://api.openai.com/v1/embeddings")
.redirect_policy(isahc::config::RedirectPolicy::Follow) .redirect_policy(isahc::config::RedirectPolicy::Follow)
.timeout(Duration::from_secs(4))
.header("Content-Type", "application/json") .header("Content-Type", "application/json")
.header("Authorization", format!("Bearer {}", api_key)) .header("Authorization", format!("Bearer {}", api_key))
.body( .body(
@ -106,7 +106,7 @@ impl OpenAIEmbeddings {
#[async_trait] #[async_trait]
impl EmbeddingProvider for OpenAIEmbeddings { impl EmbeddingProvider for OpenAIEmbeddings {
async fn embed_batch(&self, spans: Vec<&str>) -> Result<Vec<Vec<f32>>> { async fn embed_batch(&self, spans: Vec<&str>) -> Result<Vec<Vec<f32>>> {
const BACKOFF_SECONDS: [usize; 3] = [65, 180, 360]; const BACKOFF_SECONDS: [usize; 3] = [45, 75, 125];
const MAX_RETRIES: usize = 3; const MAX_RETRIES: usize = 3;
let api_key = OPENAI_API_KEY let api_key = OPENAI_API_KEY
@ -114,6 +114,7 @@ impl EmbeddingProvider for OpenAIEmbeddings {
.ok_or_else(|| anyhow!("no api key"))?; .ok_or_else(|| anyhow!("no api key"))?;
let mut request_number = 0; let mut request_number = 0;
let mut truncated = false;
let mut response: Response<AsyncBody>; let mut response: Response<AsyncBody>;
let mut spans: Vec<String> = spans.iter().map(|x| x.to_string()).collect(); let mut spans: Vec<String> = spans.iter().map(|x| x.to_string()).collect();
while request_number < MAX_RETRIES { while request_number < MAX_RETRIES {
@ -132,14 +133,25 @@ impl EmbeddingProvider for OpenAIEmbeddings {
match response.status() { match response.status() {
StatusCode::TOO_MANY_REQUESTS => { StatusCode::TOO_MANY_REQUESTS => {
let delay = Duration::from_secs(BACKOFF_SECONDS[request_number - 1] as u64); let delay = Duration::from_secs(BACKOFF_SECONDS[request_number - 1] as u64);
log::trace!(
"open ai rate limiting, delaying request by {:?} seconds",
delay.as_secs()
);
self.executor.timer(delay).await; self.executor.timer(delay).await;
} }
StatusCode::BAD_REQUEST => { StatusCode::BAD_REQUEST => {
log::info!("BAD REQUEST: {:?}", &response.status()); // Only truncate if it hasnt been truncated before
// Don't worry about delaying bad request, as we can assume if !truncated {
// we haven't been rate limited yet.
for span in spans.iter_mut() { for span in spans.iter_mut() {
*span = Self::truncate(span.to_string()); *span = Self::truncate(span.clone());
}
truncated = true;
} else {
// If failing once already truncated, log the error and break the loop
let mut body = String::new();
response.body_mut().read_to_string(&mut body).await?;
log::trace!("open ai bad request: {:?} {:?}", &response.status(), body);
break;
} }
} }
StatusCode::OK => { StatusCode::OK => {
@ -147,7 +159,7 @@ impl EmbeddingProvider for OpenAIEmbeddings {
response.body_mut().read_to_string(&mut body).await?; response.body_mut().read_to_string(&mut body).await?;
let response: OpenAIEmbeddingResponse = serde_json::from_str(&body)?; let response: OpenAIEmbeddingResponse = serde_json::from_str(&body)?;
log::info!( log::trace!(
"openai embedding completed. tokens: {:?}", "openai embedding completed. tokens: {:?}",
response.usage.total_tokens response.usage.total_tokens
); );

View file

@ -0,0 +1,321 @@
use anyhow::{anyhow, Ok, Result};
use language::{Grammar, Language};
use std::{
cmp::{self, Reverse},
collections::HashSet,
ops::Range,
path::Path,
sync::Arc,
};
use tree_sitter::{Parser, QueryCursor};
#[derive(Debug, PartialEq, Clone)]
pub struct Document {
pub name: String,
pub range: Range<usize>,
pub content: String,
pub embedding: Vec<f32>,
}
const CODE_CONTEXT_TEMPLATE: &str =
"The below code snippet is from file '<path>'\n\n```<language>\n<item>\n```";
const ENTIRE_FILE_TEMPLATE: &str =
"The below snippet is from file '<path>'\n\n```<language>\n<item>\n```";
const MARKDOWN_CONTEXT_TEMPLATE: &str = "The below file contents is from file '<path>'\n\n<item>";
pub const PARSEABLE_ENTIRE_FILE_TYPES: &[&str] =
&["TOML", "YAML", "CSS", "HEEX", "ERB", "SVELTE", "HTML"];
pub struct CodeContextRetriever {
pub parser: Parser,
pub cursor: QueryCursor,
}
// Every match has an item, this represents the fundamental treesitter symbol and anchors the search
// Every match has one or more 'name' captures. These indicate the display range of the item for deduplication.
// If there are preceeding comments, we track this with a context capture
// If there is a piece that should be collapsed in hierarchical queries, we capture it with a collapse capture
// If there is a piece that should be kept inside a collapsed node, we capture it with a keep capture
#[derive(Debug, Clone)]
pub struct CodeContextMatch {
pub start_col: usize,
pub item_range: Option<Range<usize>>,
pub name_range: Option<Range<usize>>,
pub context_ranges: Vec<Range<usize>>,
pub collapse_ranges: Vec<Range<usize>>,
}
impl CodeContextRetriever {
pub fn new() -> Self {
Self {
parser: Parser::new(),
cursor: QueryCursor::new(),
}
}
fn parse_entire_file(
&self,
relative_path: &Path,
language_name: Arc<str>,
content: &str,
) -> Result<Vec<Document>> {
let document_span = ENTIRE_FILE_TEMPLATE
.replace("<path>", relative_path.to_string_lossy().as_ref())
.replace("<language>", language_name.as_ref())
.replace("<item>", &content);
Ok(vec![Document {
range: 0..content.len(),
content: document_span,
embedding: Vec::new(),
name: language_name.to_string(),
}])
}
fn parse_markdown_file(&self, relative_path: &Path, content: &str) -> Result<Vec<Document>> {
let document_span = MARKDOWN_CONTEXT_TEMPLATE
.replace("<path>", relative_path.to_string_lossy().as_ref())
.replace("<item>", &content);
Ok(vec![Document {
range: 0..content.len(),
content: document_span,
embedding: Vec::new(),
name: "Markdown".to_string(),
}])
}
fn get_matches_in_file(
&mut self,
content: &str,
grammar: &Arc<Grammar>,
) -> Result<Vec<CodeContextMatch>> {
let embedding_config = grammar
.embedding_config
.as_ref()
.ok_or_else(|| anyhow!("no embedding queries"))?;
self.parser.set_language(grammar.ts_language).unwrap();
let tree = self
.parser
.parse(&content, None)
.ok_or_else(|| anyhow!("parsing failed"))?;
let mut captures: Vec<CodeContextMatch> = Vec::new();
let mut collapse_ranges: Vec<Range<usize>> = Vec::new();
let mut keep_ranges: Vec<Range<usize>> = Vec::new();
for mat in self.cursor.matches(
&embedding_config.query,
tree.root_node(),
content.as_bytes(),
) {
let mut start_col = 0;
let mut item_range: Option<Range<usize>> = None;
let mut name_range: Option<Range<usize>> = None;
let mut context_ranges: Vec<Range<usize>> = Vec::new();
collapse_ranges.clear();
keep_ranges.clear();
for capture in mat.captures {
if capture.index == embedding_config.item_capture_ix {
item_range = Some(capture.node.byte_range());
start_col = capture.node.start_position().column;
} else if Some(capture.index) == embedding_config.name_capture_ix {
name_range = Some(capture.node.byte_range());
} else if Some(capture.index) == embedding_config.context_capture_ix {
context_ranges.push(capture.node.byte_range());
} else if Some(capture.index) == embedding_config.collapse_capture_ix {
collapse_ranges.push(capture.node.byte_range());
} else if Some(capture.index) == embedding_config.keep_capture_ix {
keep_ranges.push(capture.node.byte_range());
}
}
captures.push(CodeContextMatch {
start_col,
item_range,
name_range,
context_ranges,
collapse_ranges: subtract_ranges(&collapse_ranges, &keep_ranges),
});
}
Ok(captures)
}
pub fn parse_file_with_template(
&mut self,
relative_path: &Path,
content: &str,
language: Arc<Language>,
) -> Result<Vec<Document>> {
let language_name = language.name();
if PARSEABLE_ENTIRE_FILE_TYPES.contains(&language_name.as_ref()) {
return self.parse_entire_file(relative_path, language_name, &content);
} else if &language_name.to_string() == &"Markdown".to_string() {
return self.parse_markdown_file(relative_path, &content);
}
let mut documents = self.parse_file(content, language)?;
for document in &mut documents {
document.content = CODE_CONTEXT_TEMPLATE
.replace("<path>", relative_path.to_string_lossy().as_ref())
.replace("<language>", language_name.as_ref())
.replace("item", &document.content);
}
Ok(documents)
}
pub fn parse_file(&mut self, content: &str, language: Arc<Language>) -> Result<Vec<Document>> {
let grammar = language
.grammar()
.ok_or_else(|| anyhow!("no grammar for language"))?;
// Iterate through query matches
let matches = self.get_matches_in_file(content, grammar)?;
let language_scope = language.default_scope();
let placeholder = language_scope.collapsed_placeholder();
let mut documents = Vec::new();
let mut collapsed_ranges_within = Vec::new();
let mut parsed_name_ranges = HashSet::new();
for (i, context_match) in matches.iter().enumerate() {
// Items which are collapsible but not embeddable have no item range
let item_range = if let Some(item_range) = context_match.item_range.clone() {
item_range
} else {
continue;
};
// Checks for deduplication
let name;
if let Some(name_range) = context_match.name_range.clone() {
name = content
.get(name_range.clone())
.map_or(String::new(), |s| s.to_string());
if parsed_name_ranges.contains(&name_range) {
continue;
}
parsed_name_ranges.insert(name_range);
} else {
name = String::new();
}
collapsed_ranges_within.clear();
'outer: for remaining_match in &matches[(i + 1)..] {
for collapsed_range in &remaining_match.collapse_ranges {
if item_range.start <= collapsed_range.start
&& item_range.end >= collapsed_range.end
{
collapsed_ranges_within.push(collapsed_range.clone());
} else {
break 'outer;
}
}
}
collapsed_ranges_within.sort_by_key(|r| (r.start, Reverse(r.end)));
let mut document_content = String::new();
for context_range in &context_match.context_ranges {
add_content_from_range(
&mut document_content,
content,
context_range.clone(),
context_match.start_col,
);
document_content.push_str("\n");
}
let mut offset = item_range.start;
for collapsed_range in &collapsed_ranges_within {
if collapsed_range.start > offset {
add_content_from_range(
&mut document_content,
content,
offset..collapsed_range.start,
context_match.start_col,
);
offset = collapsed_range.start;
}
if collapsed_range.end > offset {
document_content.push_str(placeholder);
offset = collapsed_range.end;
}
}
if offset < item_range.end {
add_content_from_range(
&mut document_content,
content,
offset..item_range.end,
context_match.start_col,
);
}
documents.push(Document {
name,
content: document_content,
range: item_range.clone(),
embedding: vec![],
})
}
return Ok(documents);
}
}
pub(crate) fn subtract_ranges(
ranges: &[Range<usize>],
ranges_to_subtract: &[Range<usize>],
) -> Vec<Range<usize>> {
let mut result = Vec::new();
let mut ranges_to_subtract = ranges_to_subtract.iter().peekable();
for range in ranges {
let mut offset = range.start;
while offset < range.end {
if let Some(range_to_subtract) = ranges_to_subtract.peek() {
if offset < range_to_subtract.start {
let next_offset = cmp::min(range_to_subtract.start, range.end);
result.push(offset..next_offset);
offset = next_offset;
} else {
let next_offset = cmp::min(range_to_subtract.end, range.end);
offset = next_offset;
}
if offset >= range_to_subtract.end {
ranges_to_subtract.next();
}
} else {
result.push(offset..range.end);
offset = range.end;
}
}
}
result
}
fn add_content_from_range(
output: &mut String,
content: &str,
range: Range<usize>,
start_col: usize,
) {
for mut line in content.get(range.clone()).unwrap_or("").lines() {
for _ in 0..start_col {
if line.starts_with(' ') {
line = &line[1..];
} else {
break;
}
}
output.push_str(line);
output.push('\n');
}
output.pop();
}

View file

@ -0,0 +1,817 @@
mod db;
mod embedding;
mod parsing;
pub mod semantic_index_settings;
#[cfg(test)]
mod semantic_index_tests;
use crate::semantic_index_settings::SemanticIndexSettings;
use anyhow::{anyhow, Result};
use db::VectorDatabase;
use embedding::{EmbeddingProvider, OpenAIEmbeddings};
use futures::{channel::oneshot, Future};
use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task, WeakModelHandle};
use language::{Anchor, Buffer, Language, LanguageRegistry};
use parking_lot::Mutex;
use parsing::{CodeContextRetriever, Document, PARSEABLE_ENTIRE_FILE_TYPES};
use postage::watch;
use project::{search::PathMatcher, Fs, Project, WorktreeId};
use smol::channel;
use std::{
cmp::Ordering,
collections::HashMap,
mem,
ops::Range,
path::{Path, PathBuf},
sync::{Arc, Weak},
time::{Instant, SystemTime},
};
use util::{
channel::{ReleaseChannel, RELEASE_CHANNEL, RELEASE_CHANNEL_NAME},
http::HttpClient,
paths::EMBEDDINGS_DIR,
ResultExt,
};
const SEMANTIC_INDEX_VERSION: usize = 6;
const EMBEDDINGS_BATCH_SIZE: usize = 80;
pub fn init(
fs: Arc<dyn Fs>,
http_client: Arc<dyn HttpClient>,
language_registry: Arc<LanguageRegistry>,
cx: &mut AppContext,
) {
settings::register::<SemanticIndexSettings>(cx);
let db_file_path = EMBEDDINGS_DIR
.join(Path::new(RELEASE_CHANNEL_NAME.as_str()))
.join("embeddings_db");
if *RELEASE_CHANNEL == ReleaseChannel::Stable
|| !settings::get::<SemanticIndexSettings>(cx).enabled
{
return;
}
cx.spawn(move |mut cx| async move {
let semantic_index = SemanticIndex::new(
fs,
db_file_path,
Arc::new(OpenAIEmbeddings {
client: http_client,
executor: cx.background(),
}),
language_registry,
cx.clone(),
)
.await?;
cx.update(|cx| {
cx.set_global(semantic_index.clone());
});
anyhow::Ok(())
})
.detach();
}
pub struct SemanticIndex {
fs: Arc<dyn Fs>,
database_url: Arc<PathBuf>,
embedding_provider: Arc<dyn EmbeddingProvider>,
language_registry: Arc<LanguageRegistry>,
db_update_tx: channel::Sender<DbOperation>,
parsing_files_tx: channel::Sender<PendingFile>,
_db_update_task: Task<()>,
_embed_batch_tasks: Vec<Task<()>>,
_batch_files_task: Task<()>,
_parsing_files_tasks: Vec<Task<()>>,
projects: HashMap<WeakModelHandle<Project>, ProjectState>,
}
struct ProjectState {
worktree_db_ids: Vec<(WorktreeId, i64)>,
outstanding_job_count_rx: watch::Receiver<usize>,
_outstanding_job_count_tx: Arc<Mutex<watch::Sender<usize>>>,
}
struct JobHandle {
tx: Weak<Mutex<watch::Sender<usize>>>,
}
impl ProjectState {
fn db_id_for_worktree_id(&self, id: WorktreeId) -> Option<i64> {
self.worktree_db_ids
.iter()
.find_map(|(worktree_id, db_id)| {
if *worktree_id == id {
Some(*db_id)
} else {
None
}
})
}
fn worktree_id_for_db_id(&self, id: i64) -> Option<WorktreeId> {
self.worktree_db_ids
.iter()
.find_map(|(worktree_id, db_id)| {
if *db_id == id {
Some(*worktree_id)
} else {
None
}
})
}
}
pub struct PendingFile {
worktree_db_id: i64,
relative_path: PathBuf,
absolute_path: PathBuf,
language: Arc<Language>,
modified_time: SystemTime,
job_handle: JobHandle,
}
pub struct SearchResult {
pub buffer: ModelHandle<Buffer>,
pub range: Range<Anchor>,
}
enum DbOperation {
InsertFile {
worktree_id: i64,
documents: Vec<Document>,
path: PathBuf,
mtime: SystemTime,
job_handle: JobHandle,
},
Delete {
worktree_id: i64,
path: PathBuf,
},
FindOrCreateWorktree {
path: PathBuf,
sender: oneshot::Sender<Result<i64>>,
},
FileMTimes {
worktree_id: i64,
sender: oneshot::Sender<Result<HashMap<PathBuf, SystemTime>>>,
},
WorktreePreviouslyIndexed {
path: Arc<Path>,
sender: oneshot::Sender<Result<bool>>,
},
}
enum EmbeddingJob {
Enqueue {
worktree_id: i64,
path: PathBuf,
mtime: SystemTime,
documents: Vec<Document>,
job_handle: JobHandle,
},
Flush,
}
impl SemanticIndex {
pub fn global(cx: &AppContext) -> Option<ModelHandle<SemanticIndex>> {
if cx.has_global::<ModelHandle<Self>>() {
Some(cx.global::<ModelHandle<SemanticIndex>>().clone())
} else {
None
}
}
pub fn enabled(cx: &AppContext) -> bool {
settings::get::<SemanticIndexSettings>(cx).enabled
&& *RELEASE_CHANNEL != ReleaseChannel::Stable
}
async fn new(
fs: Arc<dyn Fs>,
database_url: PathBuf,
embedding_provider: Arc<dyn EmbeddingProvider>,
language_registry: Arc<LanguageRegistry>,
mut cx: AsyncAppContext,
) -> Result<ModelHandle<Self>> {
let t0 = Instant::now();
let database_url = Arc::new(database_url);
let db = cx
.background()
.spawn(VectorDatabase::new(fs.clone(), database_url.clone()))
.await?;
log::trace!(
"db initialization took {:?} milliseconds",
t0.elapsed().as_millis()
);
Ok(cx.add_model(|cx| {
let t0 = Instant::now();
// Perform database operations
let (db_update_tx, db_update_rx) = channel::unbounded();
let _db_update_task = cx.background().spawn({
async move {
while let Ok(job) = db_update_rx.recv().await {
Self::run_db_operation(&db, job)
}
}
});
// Group documents into batches and send them to the embedding provider.
let (embed_batch_tx, embed_batch_rx) =
channel::unbounded::<Vec<(i64, Vec<Document>, PathBuf, SystemTime, JobHandle)>>();
let mut _embed_batch_tasks = Vec::new();
for _ in 0..cx.background().num_cpus() {
let embed_batch_rx = embed_batch_rx.clone();
_embed_batch_tasks.push(cx.background().spawn({
let db_update_tx = db_update_tx.clone();
let embedding_provider = embedding_provider.clone();
async move {
while let Ok(embeddings_queue) = embed_batch_rx.recv().await {
Self::compute_embeddings_for_batch(
embeddings_queue,
&embedding_provider,
&db_update_tx,
)
.await;
}
}
}));
}
// Group documents into batches and send them to the embedding provider.
let (batch_files_tx, batch_files_rx) = channel::unbounded::<EmbeddingJob>();
let _batch_files_task = cx.background().spawn(async move {
let mut queue_len = 0;
let mut embeddings_queue = vec![];
while let Ok(job) = batch_files_rx.recv().await {
Self::enqueue_documents_to_embed(
job,
&mut queue_len,
&mut embeddings_queue,
&embed_batch_tx,
);
}
});
// Parse files into embeddable documents.
let (parsing_files_tx, parsing_files_rx) = channel::unbounded::<PendingFile>();
let mut _parsing_files_tasks = Vec::new();
for _ in 0..cx.background().num_cpus() {
let fs = fs.clone();
let parsing_files_rx = parsing_files_rx.clone();
let batch_files_tx = batch_files_tx.clone();
let db_update_tx = db_update_tx.clone();
_parsing_files_tasks.push(cx.background().spawn(async move {
let mut retriever = CodeContextRetriever::new();
while let Ok(pending_file) = parsing_files_rx.recv().await {
Self::parse_file(
&fs,
pending_file,
&mut retriever,
&batch_files_tx,
&parsing_files_rx,
&db_update_tx,
)
.await;
}
}));
}
log::trace!(
"semantic index task initialization took {:?} milliseconds",
t0.elapsed().as_millis()
);
Self {
fs,
database_url,
embedding_provider,
language_registry,
db_update_tx,
parsing_files_tx,
_db_update_task,
_embed_batch_tasks,
_batch_files_task,
_parsing_files_tasks,
projects: HashMap::new(),
}
}))
}
fn run_db_operation(db: &VectorDatabase, job: DbOperation) {
match job {
DbOperation::InsertFile {
worktree_id,
documents,
path,
mtime,
job_handle,
} => {
db.insert_file(worktree_id, path, mtime, documents)
.log_err();
drop(job_handle)
}
DbOperation::Delete { worktree_id, path } => {
db.delete_file(worktree_id, path).log_err();
}
DbOperation::FindOrCreateWorktree { path, sender } => {
let id = db.find_or_create_worktree(&path);
sender.send(id).ok();
}
DbOperation::FileMTimes {
worktree_id: worktree_db_id,
sender,
} => {
let file_mtimes = db.get_file_mtimes(worktree_db_id);
sender.send(file_mtimes).ok();
}
DbOperation::WorktreePreviouslyIndexed { path, sender } => {
let worktree_indexed = db.worktree_previously_indexed(path.as_ref());
sender.send(worktree_indexed).ok();
}
}
}
async fn compute_embeddings_for_batch(
mut embeddings_queue: Vec<(i64, Vec<Document>, PathBuf, SystemTime, JobHandle)>,
embedding_provider: &Arc<dyn EmbeddingProvider>,
db_update_tx: &channel::Sender<DbOperation>,
) {
let mut batch_documents = vec![];
for (_, documents, _, _, _) in embeddings_queue.iter() {
batch_documents.extend(documents.iter().map(|document| document.content.as_str()));
}
if let Ok(embeddings) = embedding_provider.embed_batch(batch_documents).await {
log::trace!(
"created {} embeddings for {} files",
embeddings.len(),
embeddings_queue.len(),
);
let mut i = 0;
let mut j = 0;
for embedding in embeddings.iter() {
while embeddings_queue[i].1.len() == j {
i += 1;
j = 0;
}
embeddings_queue[i].1[j].embedding = embedding.to_owned();
j += 1;
}
for (worktree_id, documents, path, mtime, job_handle) in embeddings_queue.into_iter() {
db_update_tx
.send(DbOperation::InsertFile {
worktree_id,
documents,
path,
mtime,
job_handle,
})
.await
.unwrap();
}
}
}
fn enqueue_documents_to_embed(
job: EmbeddingJob,
queue_len: &mut usize,
embeddings_queue: &mut Vec<(i64, Vec<Document>, PathBuf, SystemTime, JobHandle)>,
embed_batch_tx: &channel::Sender<Vec<(i64, Vec<Document>, PathBuf, SystemTime, JobHandle)>>,
) {
let should_flush = match job {
EmbeddingJob::Enqueue {
documents,
worktree_id,
path,
mtime,
job_handle,
} => {
*queue_len += &documents.len();
embeddings_queue.push((worktree_id, documents, path, mtime, job_handle));
*queue_len >= EMBEDDINGS_BATCH_SIZE
}
EmbeddingJob::Flush => true,
};
if should_flush {
embed_batch_tx
.try_send(mem::take(embeddings_queue))
.unwrap();
*queue_len = 0;
}
}
async fn parse_file(
fs: &Arc<dyn Fs>,
pending_file: PendingFile,
retriever: &mut CodeContextRetriever,
batch_files_tx: &channel::Sender<EmbeddingJob>,
parsing_files_rx: &channel::Receiver<PendingFile>,
db_update_tx: &channel::Sender<DbOperation>,
) {
if let Some(content) = fs.load(&pending_file.absolute_path).await.log_err() {
if let Some(documents) = retriever
.parse_file_with_template(
&pending_file.relative_path,
&content,
pending_file.language,
)
.log_err()
{
log::trace!(
"parsed path {:?}: {} documents",
pending_file.relative_path,
documents.len()
);
if documents.len() == 0 {
db_update_tx
.send(DbOperation::InsertFile {
worktree_id: pending_file.worktree_db_id,
documents,
path: pending_file.relative_path,
mtime: pending_file.modified_time,
job_handle: pending_file.job_handle,
})
.await
.unwrap();
} else {
batch_files_tx
.try_send(EmbeddingJob::Enqueue {
worktree_id: pending_file.worktree_db_id,
path: pending_file.relative_path,
mtime: pending_file.modified_time,
job_handle: pending_file.job_handle,
documents,
})
.unwrap();
}
}
}
if parsing_files_rx.len() == 0 {
batch_files_tx.try_send(EmbeddingJob::Flush).unwrap();
}
}
fn find_or_create_worktree(&self, path: PathBuf) -> impl Future<Output = Result<i64>> {
let (tx, rx) = oneshot::channel();
self.db_update_tx
.try_send(DbOperation::FindOrCreateWorktree { path, sender: tx })
.unwrap();
async move { rx.await? }
}
fn get_file_mtimes(
&self,
worktree_id: i64,
) -> impl Future<Output = Result<HashMap<PathBuf, SystemTime>>> {
let (tx, rx) = oneshot::channel();
self.db_update_tx
.try_send(DbOperation::FileMTimes {
worktree_id,
sender: tx,
})
.unwrap();
async move { rx.await? }
}
fn worktree_previously_indexed(&self, path: Arc<Path>) -> impl Future<Output = Result<bool>> {
let (tx, rx) = oneshot::channel();
self.db_update_tx
.try_send(DbOperation::WorktreePreviouslyIndexed { path, sender: tx })
.unwrap();
async move { rx.await? }
}
pub fn project_previously_indexed(
&mut self,
project: ModelHandle<Project>,
cx: &mut ModelContext<Self>,
) -> Task<Result<bool>> {
let worktree_scans_complete = project
.read(cx)
.worktrees(cx)
.map(|worktree| {
let scan_complete = worktree.read(cx).as_local().unwrap().scan_complete();
async move {
scan_complete.await;
}
})
.collect::<Vec<_>>();
let worktrees_indexed_previously = project
.read(cx)
.worktrees(cx)
.map(|worktree| self.worktree_previously_indexed(worktree.read(cx).abs_path()))
.collect::<Vec<_>>();
cx.spawn(|_, _cx| async move {
futures::future::join_all(worktree_scans_complete).await;
let worktree_indexed_previously =
futures::future::join_all(worktrees_indexed_previously).await;
Ok(worktree_indexed_previously
.iter()
.filter(|worktree| worktree.is_ok())
.all(|v| v.as_ref().log_err().is_some_and(|v| v.to_owned())))
})
}
pub fn index_project(
&mut self,
project: ModelHandle<Project>,
cx: &mut ModelContext<Self>,
) -> Task<Result<(usize, watch::Receiver<usize>)>> {
let t0 = Instant::now();
let worktree_scans_complete = project
.read(cx)
.worktrees(cx)
.map(|worktree| {
let scan_complete = worktree.read(cx).as_local().unwrap().scan_complete();
async move {
scan_complete.await;
}
})
.collect::<Vec<_>>();
let worktree_db_ids = project
.read(cx)
.worktrees(cx)
.map(|worktree| {
self.find_or_create_worktree(worktree.read(cx).abs_path().to_path_buf())
})
.collect::<Vec<_>>();
let language_registry = self.language_registry.clone();
let db_update_tx = self.db_update_tx.clone();
let parsing_files_tx = self.parsing_files_tx.clone();
cx.spawn(|this, mut cx| async move {
futures::future::join_all(worktree_scans_complete).await;
let worktree_db_ids = futures::future::join_all(worktree_db_ids).await;
let worktrees = project.read_with(&cx, |project, cx| {
project
.worktrees(cx)
.map(|worktree| worktree.read(cx).snapshot())
.collect::<Vec<_>>()
});
let mut worktree_file_mtimes = HashMap::new();
let mut db_ids_by_worktree_id = HashMap::new();
for (worktree, db_id) in worktrees.iter().zip(worktree_db_ids) {
let db_id = db_id?;
db_ids_by_worktree_id.insert(worktree.id(), db_id);
worktree_file_mtimes.insert(
worktree.id(),
this.read_with(&cx, |this, _| this.get_file_mtimes(db_id))
.await?,
);
}
let (job_count_tx, job_count_rx) = watch::channel_with(0);
let job_count_tx = Arc::new(Mutex::new(job_count_tx));
this.update(&mut cx, |this, _| {
this.projects.insert(
project.downgrade(),
ProjectState {
worktree_db_ids: db_ids_by_worktree_id
.iter()
.map(|(a, b)| (*a, *b))
.collect(),
outstanding_job_count_rx: job_count_rx.clone(),
_outstanding_job_count_tx: job_count_tx.clone(),
},
);
});
cx.background()
.spawn(async move {
let mut count = 0;
for worktree in worktrees.into_iter() {
let mut file_mtimes = worktree_file_mtimes.remove(&worktree.id()).unwrap();
for file in worktree.files(false, 0) {
let absolute_path = worktree.absolutize(&file.path);
if let Ok(language) = language_registry
.language_for_file(&absolute_path, None)
.await
{
if !PARSEABLE_ENTIRE_FILE_TYPES.contains(&language.name().as_ref())
&& &language.name().as_ref() != &"Markdown"
&& language
.grammar()
.and_then(|grammar| grammar.embedding_config.as_ref())
.is_none()
{
continue;
}
let path_buf = file.path.to_path_buf();
let stored_mtime = file_mtimes.remove(&file.path.to_path_buf());
let already_stored = stored_mtime
.map_or(false, |existing_mtime| existing_mtime == file.mtime);
if !already_stored {
count += 1;
*job_count_tx.lock().borrow_mut() += 1;
let job_handle = JobHandle {
tx: Arc::downgrade(&job_count_tx),
};
parsing_files_tx
.try_send(PendingFile {
worktree_db_id: db_ids_by_worktree_id[&worktree.id()],
relative_path: path_buf,
absolute_path,
language,
job_handle,
modified_time: file.mtime,
})
.unwrap();
}
}
}
for file in file_mtimes.keys() {
db_update_tx
.try_send(DbOperation::Delete {
worktree_id: db_ids_by_worktree_id[&worktree.id()],
path: file.to_owned(),
})
.unwrap();
}
}
log::trace!(
"walking worktree took {:?} milliseconds",
t0.elapsed().as_millis()
);
anyhow::Ok((count, job_count_rx))
})
.await
})
}
pub fn outstanding_job_count_rx(
&self,
project: &ModelHandle<Project>,
) -> Option<watch::Receiver<usize>> {
Some(
self.projects
.get(&project.downgrade())?
.outstanding_job_count_rx
.clone(),
)
}
pub fn search_project(
&mut self,
project: ModelHandle<Project>,
phrase: String,
limit: usize,
includes: Vec<PathMatcher>,
excludes: Vec<PathMatcher>,
cx: &mut ModelContext<Self>,
) -> Task<Result<Vec<SearchResult>>> {
let project_state = if let Some(state) = self.projects.get(&project.downgrade()) {
state
} else {
return Task::ready(Err(anyhow!("project not added")));
};
let worktree_db_ids = project
.read(cx)
.worktrees(cx)
.filter_map(|worktree| {
let worktree_id = worktree.read(cx).id();
project_state.db_id_for_worktree_id(worktree_id)
})
.collect::<Vec<_>>();
let embedding_provider = self.embedding_provider.clone();
let database_url = self.database_url.clone();
let fs = self.fs.clone();
cx.spawn(|this, mut cx| async move {
let database = VectorDatabase::new(fs.clone(), database_url.clone()).await?;
let phrase_embedding = embedding_provider
.embed_batch(vec![&phrase])
.await?
.into_iter()
.next()
.unwrap();
let file_ids =
database.retrieve_included_file_ids(&worktree_db_ids, &includes, &excludes)?;
let batch_n = cx.background().num_cpus();
let ids_len = file_ids.clone().len();
let batch_size = if ids_len <= batch_n {
ids_len
} else {
ids_len / batch_n
};
let mut result_tasks = Vec::new();
for batch in file_ids.chunks(batch_size) {
let batch = batch.into_iter().map(|v| *v).collect::<Vec<i64>>();
let limit = limit.clone();
let fs = fs.clone();
let database_url = database_url.clone();
let phrase_embedding = phrase_embedding.clone();
let task = cx.background().spawn(async move {
let database = VectorDatabase::new(fs, database_url).await.log_err();
if database.is_none() {
return Err(anyhow!("failed to acquire database connection"));
} else {
database
.unwrap()
.top_k_search(&phrase_embedding, limit, batch.as_slice())
}
});
result_tasks.push(task);
}
let batch_results = futures::future::join_all(result_tasks).await;
let mut results = Vec::new();
for batch_result in batch_results {
if batch_result.is_ok() {
for (id, similarity) in batch_result.unwrap() {
let ix = match results.binary_search_by(|(_, s)| {
similarity.partial_cmp(&s).unwrap_or(Ordering::Equal)
}) {
Ok(ix) => ix,
Err(ix) => ix,
};
results.insert(ix, (id, similarity));
results.truncate(limit);
}
}
}
let ids = results.into_iter().map(|(id, _)| id).collect::<Vec<i64>>();
let documents = database.get_documents_by_ids(ids.as_slice())?;
let mut tasks = Vec::new();
let mut ranges = Vec::new();
let weak_project = project.downgrade();
project.update(&mut cx, |project, cx| {
for (worktree_db_id, file_path, byte_range) in documents {
let project_state =
if let Some(state) = this.read(cx).projects.get(&weak_project) {
state
} else {
return Err(anyhow!("project not added"));
};
if let Some(worktree_id) = project_state.worktree_id_for_db_id(worktree_db_id) {
tasks.push(project.open_buffer((worktree_id, file_path), cx));
ranges.push(byte_range);
}
}
Ok(())
})?;
let buffers = futures::future::join_all(tasks).await;
Ok(buffers
.into_iter()
.zip(ranges)
.filter_map(|(buffer, range)| {
let buffer = buffer.log_err()?;
let range = buffer.read_with(&cx, |buffer, _| {
buffer.anchor_before(range.start)..buffer.anchor_after(range.end)
});
Some(SearchResult { buffer, range })
})
.collect::<Vec<_>>())
})
}
}
impl Entity for SemanticIndex {
type Event = ();
}
impl Drop for JobHandle {
fn drop(&mut self) {
if let Some(tx) = self.tx.upgrade() {
let mut tx = tx.lock();
*tx.borrow_mut() -= 1;
}
}
}

View file

@ -4,21 +4,21 @@ use serde::{Deserialize, Serialize};
use settings::Setting; use settings::Setting;
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct VectorStoreSettings { pub struct SemanticIndexSettings {
pub enabled: bool, pub enabled: bool,
pub reindexing_delay_seconds: usize, pub reindexing_delay_seconds: usize,
} }
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)] #[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
pub struct VectorStoreSettingsContent { pub struct SemanticIndexSettingsContent {
pub enabled: Option<bool>, pub enabled: Option<bool>,
pub reindexing_delay_seconds: Option<usize>, pub reindexing_delay_seconds: Option<usize>,
} }
impl Setting for VectorStoreSettings { impl Setting for SemanticIndexSettings {
const KEY: Option<&'static str> = Some("vector_store"); const KEY: Option<&'static str> = Some("semantic_index");
type FileContent = VectorStoreSettingsContent; type FileContent = SemanticIndexSettingsContent;
fn load( fn load(
default_value: &Self::FileContent, default_value: &Self::FileContent,

File diff suppressed because it is too large Load diff

Some files were not shown because too many files have changed in this diff Show more