Compare commits
43 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0ae34d98ad | ||
|
|
392f597966 | ||
|
|
c3e801c586 | ||
|
|
86c4faa12f | ||
|
|
b545a02125 | ||
|
|
4762103c2a | ||
|
|
a982ff848f | ||
|
|
416e940bdd | ||
|
|
6899dab525 | ||
|
|
7f28b16825 | ||
|
|
196941a310 | ||
|
|
1e82854b7e | ||
|
|
cc81f91dfb | ||
|
|
be633875a5 | ||
|
|
ff28aed411 | ||
|
|
0bc287b863 | ||
|
|
d325d60a21 | ||
|
|
9b8bc2a2cd | ||
|
|
57978ece43 | ||
|
|
31de418b58 | ||
|
|
3376d63f57 | ||
|
|
87e49e4d91 | ||
|
|
2727be4df9 | ||
|
|
7498f508f1 | ||
|
|
0ebe6f78cf | ||
|
|
c327d9d3f4 | ||
|
|
60f6fe3454 | ||
|
|
c47d805806 | ||
|
|
14552268fd | ||
|
|
23eb000abe | ||
|
|
7abe14dba8 | ||
|
|
f0c4643bbd | ||
|
|
6883907163 | ||
|
|
49a62b6414 | ||
|
|
160b65771c | ||
|
|
214e997b72 | ||
|
|
825a353228 | ||
|
|
310cc3e315 | ||
|
|
75ff03e45b | ||
|
|
eb95efb0f3 | ||
|
|
059ebd88fe | ||
|
|
e8435a8915 | ||
|
|
e41e3f8463 |
7
.github/ISSUE_TEMPLATE/0_feature_request.yml
vendored
@@ -18,11 +18,8 @@ body:
|
||||
- type: textarea
|
||||
id: environment
|
||||
attributes:
|
||||
label: Zed Version and System Specs
|
||||
description: Zed version, release channel, architecture (x86_64 or aarch64), OS (macOS version / Linux distro and version) and RAM amount.
|
||||
placeholder: |
|
||||
<!-- In Zed run `copy system specs into clipboard` from the Zed command palette and paste here. -->
|
||||
<!-- Alternatively spawn `request feature` and this field will be autopopulated -->
|
||||
label: Environment
|
||||
description: Run the `copy system specs into clipboard` command palette action and paste the output in the field below. If you are unable to run the command, please include your Zed version and release channel, operating system and version, RAM amount, and architecture.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
|
||||
13
.github/ISSUE_TEMPLATE/1_bug_report.yml
vendored
@@ -20,19 +20,14 @@ body:
|
||||
- type: textarea
|
||||
id: environment
|
||||
attributes:
|
||||
label: Zed Version and System Specs
|
||||
description: Zed version, release channel, architecture (x86_64 or aarch64), OS (macOS version / Linux distro and version) and RAM amount.
|
||||
placeholder: |
|
||||
<!-- In Zed run `copy system specs into clipboard` from the Zed command palette and paste here. -->
|
||||
<!-- Alternatively spawn `file bug report` and this field will be autopopulated -->
|
||||
<!-- If Zed won't launch, include the equivalent with other relevant details (e.g. video card driver version for display bugs, etc) -->
|
||||
<!-- Zed Version: 0.xxx.x; Channel: Stable, OS: macOS xx.xx, RAM: XXGB, Architecture: x86_64"
|
||||
label: Environment
|
||||
description: Run the `copy system specs into clipboard` command palette action and paste the output in the field below. If you are unable to run the command, please include your Zed version and release channel, operating system and version, RAM amount, and architecture.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: If applicable, add screenshots or screencasts of the incorrect state / behavior
|
||||
description: Drag images / videos into the text input below
|
||||
label: If applicable, add mockups / screenshots to help explain present your vision of the feature
|
||||
description: Drag issues into the text input below
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
|
||||
1
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,4 +1,3 @@
|
||||
# yaml-language-server: $schema=https://json.schemastore.org/github-issue-config.json
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Language Request
|
||||
|
||||
@@ -8,7 +8,7 @@ on:
|
||||
jobs:
|
||||
update_top_ranking_issues:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository == 'zed-industries/zed'
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
- name: Set up uv
|
||||
|
||||
@@ -8,7 +8,7 @@ on:
|
||||
jobs:
|
||||
update_top_ranking_issues:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository == 'zed-industries/zed'
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
- name: Set up uv
|
||||
|
||||
2
.github/workflows/release_nightly.yml
vendored
@@ -140,7 +140,7 @@ jobs:
|
||||
name: Create a Linux *.tar.gz bundle for ARM
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
runs-on:
|
||||
- buildjet-16vcpu-ubuntu-2204-arm
|
||||
- hosted-linux-arm-1
|
||||
needs: tests
|
||||
env:
|
||||
DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
|
||||
|
||||
1243
Cargo.lock
generated
@@ -117,7 +117,6 @@ members = [
|
||||
"crates/tab_switcher",
|
||||
"crates/task",
|
||||
"crates/tasks_ui",
|
||||
"crates/telemetry",
|
||||
"crates/telemetry_events",
|
||||
"crates/terminal",
|
||||
"crates/terminal_view",
|
||||
@@ -306,7 +305,6 @@ supermaven_api = { path = "crates/supermaven_api" }
|
||||
tab_switcher = { path = "crates/tab_switcher" }
|
||||
task = { path = "crates/task" }
|
||||
tasks_ui = { path = "crates/tasks_ui" }
|
||||
telemetry = { path = "crates/telemetry" }
|
||||
telemetry_events = { path = "crates/telemetry_events" }
|
||||
terminal = { path = "crates/terminal" }
|
||||
terminal_view = { path = "crates/terminal_view" }
|
||||
|
||||
@@ -59,11 +59,6 @@
|
||||
"gitignore": "vcs",
|
||||
"gitkeep": "vcs",
|
||||
"gitmodules": "vcs",
|
||||
"TAG_EDITMSG": "vcs",
|
||||
"MERGE_MSG": "vcs",
|
||||
"COMMIT_EDITMSG": "vcs",
|
||||
"NOTES_EDITMSG": "vcs",
|
||||
"EDIT_DESCRIPTION": "vcs",
|
||||
"gleam": "gleam",
|
||||
"go": "go",
|
||||
"gql": "graphql",
|
||||
@@ -113,7 +108,6 @@
|
||||
"mdf": "storage",
|
||||
"mdx": "document",
|
||||
"metadata": "code",
|
||||
"metal": "metal",
|
||||
"mjs": "javascript",
|
||||
"mka": "audio",
|
||||
"mkv": "video",
|
||||
@@ -323,9 +317,6 @@
|
||||
"lua": {
|
||||
"icon": "icons/file_icons/lua.svg"
|
||||
},
|
||||
"metal": {
|
||||
"icon": "icons/file_icons/metal.svg"
|
||||
},
|
||||
"nim": {
|
||||
"icon": "icons/file_icons/nim.svg"
|
||||
},
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
<svg width="16" height="16" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M4.56 4.502 3.25 3.027V11.5h1.5V6.973l2.69 3.025 1.31 1.475V7.918l3.306 3.582h2.042L8.55 5.491 7.25 4.081V7.528L4.56 4.502Z" fill="#000"/></svg>
|
||||
|
Before Width: | Height: | Size: 269 B |
@@ -1,5 +1,5 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M2 8C2 9.18669 2.35189 10.3467 3.01118 11.3334C3.67047 12.3201 4.60754 13.0892 5.7039 13.5433C6.80026 13.9974 8.00666 14.1162 9.17054 13.8847C10.3344 13.6532 11.4035 13.0818 12.2426 12.2426C13.0818 11.4035 13.6532 10.3344 13.8847 9.17054C14.1162 8.00666 13.9974 6.80026 13.5433 5.7039C13.0892 4.60754 12.3201 3.67047 11.3334 3.01118C10.3467 2.35189 9.18669 2 8 2C6.32263 2.00631 4.71265 2.66082 3.50667 3.82667L2 5.33333" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M2 2V5.33333H5.33333" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M8 5V8.5L10 9.5" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1.5 6C1.5 6.89002 1.76392 7.76004 2.25839 8.50007C2.75285 9.24009 3.45566 9.81686 4.27792 10.1575C5.10019 10.4981 6.00499 10.5872 6.87791 10.4135C7.75082 10.2399 8.55264 9.81132 9.18198 9.18198C9.81132 8.55264 10.2399 7.75082 10.4135 6.87791C10.5872 6.00499 10.4981 5.10019 10.1575 4.27792C9.81686 3.45566 9.24009 2.75285 8.50007 2.25839C7.76004 1.76392 6.89002 1.5 6 1.5C4.74198 1.50473 3.53448 1.99561 2.63 2.87L1.5 4" stroke="#919081" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M1.5 1.5V4H4" stroke="#919081" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M6 3.5V6L8 7" stroke="#919081" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 840 B After Width: | Height: | Size: 778 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-message-circle-more"><path d="M7.9 20A9 9 0 1 0 4 16.1L2 22Z"/><path d="M8 12h.01"/><path d="M12 12h.01"/><path d="M16 12h.01"/></svg>
|
||||
|
Before Width: | Height: | Size: 337 B |
@@ -1,4 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12.6666 14V12.6667C12.6666 11.9594 12.3856 11.2811 11.8855 10.781C11.3854 10.281 10.7072 10 9.99992 10H5.99992C5.29267 10 4.6144 10.281 4.1143 10.781C3.6142 11.2811 3.33325 11.9594 3.33325 12.6667V14" stroke="black" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M7.99992 7.33333C9.47268 7.33333 10.6666 6.13943 10.6666 4.66667C10.6666 3.19391 9.47268 2 7.99992 2C6.52716 2 5.33325 3.19391 5.33325 4.66667C5.33325 6.13943 6.52716 7.33333 7.99992 7.33333Z" stroke="black" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.5 0.875C5.49797 0.875 3.875 2.49797 3.875 4.5C3.875 6.15288 4.98124 7.54738 6.49373 7.98351C5.2997 8.12901 4.27557 8.55134 3.50407 9.31167C2.52216 10.2794 2.02502 11.72 2.02502 13.5999C2.02502 13.8623 2.23769 14.0749 2.50002 14.0749C2.76236 14.0749 2.97502 13.8623 2.97502 13.5999C2.97502 11.8799 3.42786 10.7206 4.17091 9.9883C4.91536 9.25463 6.02674 8.87499 7.49995 8.87499C8.97317 8.87499 10.0846 9.25463 10.8291 9.98831C11.5721 10.7206 12.025 11.8799 12.025 13.5999C12.025 13.8623 12.2376 14.0749 12.5 14.0749C12.7623 14.075 12.975 13.8623 12.975 13.6C12.975 11.72 12.4778 10.2794 11.4959 9.31166C10.7244 8.55135 9.70025 8.12903 8.50625 7.98352C10.0187 7.5474 11.125 6.15289 11.125 4.5C11.125 2.49797 9.50203 0.875 7.5 0.875ZM4.825 4.5C4.825 3.02264 6.02264 1.825 7.5 1.825C8.97736 1.825 10.175 3.02264 10.175 4.5C10.175 5.97736 8.97736 7.175 7.5 7.175C6.02264 7.175 4.825 5.97736 4.825 4.5Z" fill="black"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 690 B After Width: | Height: | Size: 1.0 KiB |
@@ -1,4 +1,8 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3.33325 8H12.6666" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M8 3.33333V12.6667" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M8 2.75C8 2.47386 7.77614 2.25 7.5 2.25C7.22386 2.25 7 2.47386 7 2.75V7H2.75C2.47386 7 2.25 7.22386 2.25 7.5C2.25 7.77614 2.47386 8 2.75 8H7V12.25C7 12.5261 7.22386 12.75 7.5 12.75C7.77614 12.75 8 12.5261 8 12.25V8H12.25C12.5261 8 12.75 7.77614 12.75 7.5C12.75 7.22386 12.5261 7 12.25 7H8V2.75Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 327 B After Width: | Height: | Size: 491 B |
@@ -1,4 +1 @@
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 6.5L9.99556 4.21778C9.27778 3.5 8.12 3 7 3C6.20888 3 5.43552 3.2346 4.77772 3.67412C4.11992 4.11365 3.60723 4.73836 3.30448 5.46927C3.00173 6.20017 2.92252 7.00444 3.07686 7.78036C3.2312 8.55628 3.61216 9.26902 4.17157 9.82842C4.73098 10.3878 5.44372 10.7688 6.21964 10.9231C6.99556 11.0775 7.79983 10.9983 8.53073 10.6955C8.88113 10.5504 9.20712 10.357 9.5 10.1225" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M12 4V6.5H9.5" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-rotate-cw"><path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/></svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 673 B After Width: | Height: | Size: 303 B |
@@ -1,4 +1,4 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8.14667 1.33334H7.85333C7.49971 1.33334 7.16057 1.47382 6.91053 1.72387C6.66048 1.97392 6.52 2.31305 6.52 2.66668V2.78668C6.51976 3.02049 6.45804 3.25014 6.34103 3.45257C6.22401 3.655 6.05583 3.8231 5.85333 3.94001L5.56667 4.10668C5.36398 4.2237 5.13405 4.28531 4.9 4.28531C4.66595 4.28531 4.43603 4.2237 4.23333 4.10668L4.13333 4.05334C3.82738 3.87685 3.46389 3.82897 3.12267 3.92022C2.78145 4.01146 2.49037 4.23437 2.31333 4.54001L2.16667 4.79334C1.99018 5.0993 1.9423 5.46279 2.03354 5.80401C2.12478 6.14523 2.34769 6.43631 2.65333 6.61334L2.75333 6.68001C2.95485 6.79635 3.12241 6.9634 3.23937 7.16456C3.35632 7.36573 3.4186 7.59399 3.42 7.82668V8.16668C3.42093 8.40162 3.35977 8.63265 3.2427 8.83635C3.12563 9.04005 2.95681 9.2092 2.75333 9.32668L2.65333 9.38668C2.34769 9.56371 2.12478 9.85479 2.03354 10.196C1.9423 10.5372 1.99018 10.9007 2.16667 11.2067L2.31333 11.46C2.49037 11.7657 2.78145 11.9886 3.12267 12.0798C3.46389 12.171 3.82738 12.1232 4.13333 11.9467L4.23333 11.8933C4.43603 11.7763 4.66595 11.7147 4.9 11.7147C5.13405 11.7147 5.36398 11.7763 5.56667 11.8933L5.85333 12.06C6.05583 12.1769 6.22401 12.345 6.34103 12.5475C6.45804 12.7499 6.51976 12.9795 6.52 13.2133V13.3333C6.52 13.687 6.66048 14.0261 6.91053 14.2762C7.16057 14.5262 7.49971 14.6667 7.85333 14.6667H8.14667C8.50029 14.6667 8.83943 14.5262 9.08948 14.2762C9.33953 14.0261 9.48 13.687 9.48 13.3333V13.2133C9.48024 12.9795 9.54196 12.7499 9.65898 12.5475C9.77599 12.345 9.94418 12.1769 10.1467 12.06L10.4333 11.8933C10.636 11.7763 10.866 11.7147 11.1 11.7147C11.3341 11.7147 11.564 11.7763 11.7667 11.8933L11.8667 11.9467C12.1726 12.1232 12.5361 12.171 12.8773 12.0798C13.2186 11.9886 13.5096 11.7657 13.6867 11.46L13.8333 11.2C14.0098 10.8941 14.0577 10.5306 13.9665 10.1893C13.8752 9.84812 13.6523 9.55704 13.3467 9.38001L13.2467 9.32668C13.0432 9.2092 12.8744 9.04005 12.7573 8.83635C12.6402 8.63265 12.5791 8.40162 12.58 8.16668V7.83334C12.5791 7.5984 12.6402 7.36738 12.7573 7.16367C12.8744 6.95997 13.0432 6.79082 13.2467 6.67334L13.3467 6.61334C13.6523 6.43631 13.8752 6.14523 13.9665 5.80401C14.0577 5.46279 14.0098 5.0993 13.8333 4.79334L13.6867 4.54001C13.5096 4.23437 13.2186 4.01146 12.8773 3.92022C12.5361 3.82897 12.1726 3.87685 11.8667 4.05334L11.7667 4.10668C11.564 4.2237 11.3341 4.28531 11.1 4.28531C10.866 4.28531 10.636 4.2237 10.4333 4.10668L10.1467 3.94001C9.94418 3.8231 9.77599 3.655 9.65898 3.45257C9.54196 3.25014 9.48024 3.02049 9.48 2.78668V2.66668C9.48 2.31305 9.33953 1.97392 9.08948 1.72387C8.83943 1.47382 8.50029 1.33334 8.14667 1.33334Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M8 10C9.10457 10 10 9.10457 10 8C10 6.89543 9.10457 6 8 6C6.89543 6 6 6.89543 6 8C6 9.10457 6.89543 10 8 10Z" fill="black"/>
|
||||
<path d="M3.16089 10.2476L3.99598 10.3784C4.61244 10.4749 5.05269 11.0395 5.00728 11.6755L4.94576 12.5377C4.92784 12.789 5.06165 13.0255 5.28326 13.1348L5.90091 13.4391C6.12253 13.5485 6.38717 13.5075 6.56817 13.3371L7.1888 12.7505C7.64641 12.3178 8.35245 12.3178 8.81059 12.7505L9.43121 13.3371C9.61222 13.5081 9.87629 13.5485 10.0985 13.4391L10.7173 13.1341C10.9384 13.0255 11.0716 12.7895 11.0537 12.539L10.9921 11.6755C10.9467 11.0395 11.3869 10.4749 12.0033 10.3784L12.8385 10.2476C13.0817 10.2097 13.2776 10.0233 13.3325 9.77768L13.4848 9.09455C13.5398 8.8489 13.4425 8.59408 13.2393 8.45229L12.5422 7.96404C12.0279 7.60355 11.8708 6.89963 12.1814 6.34659L12.6025 5.59745C12.7249 5.3793 12.7047 5.10616 12.5511 4.9094L12.1241 4.36128C11.9706 4.16451 11.7149 4.08325 11.4795 4.15719L10.6719 4.41016C10.0752 4.59714 9.43903 4.28367 9.20962 3.69035L8.90017 2.88803C8.80937 2.65339 8.58777 2.4994 8.34108 2.5L7.65649 2.50184C7.40979 2.50244 7.1888 2.65766 7.09921 2.89291L6.79751 3.68607C6.57053 4.28307 5.93138 4.59898 5.33284 4.41077L4.49178 4.1468C4.25583 4.07225 3.99897 4.15413 3.84545 4.35212L3.42133 4.90084C3.26781 5.09943 3.2493 5.37319 3.37414 5.59133L3.80483 6.34232C4.12201 6.89591 3.96671 7.60659 3.44941 7.96897L2.76065 8.45169C2.55756 8.59408 2.4602 8.84891 2.51516 9.09393L2.66747 9.77708C2.72184 10.0233 2.91777 10.2097 3.16089 10.2476Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M9.41432 6.83576C8.63332 6.05481 7.36676 6.05476 6.58575 6.83571C5.8048 7.61672 5.80476 8.88327 6.58571 9.66427C7.36671 10.4452 8.63326 10.4452 9.41426 9.66432C10.1952 8.88332 10.1952 7.61676 9.41432 6.83576Z" fill="black"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 1.7 KiB |
@@ -1,5 +0,0 @@
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5.27772 1.38585L4.39187 4.07909C4.34653 4.21692 4.26946 4.34219 4.16685 4.44479C4.06425 4.5474 3.93898 4.62447 3.80115 4.66981L1.10791 5.55566L3.80115 6.44151C3.93898 6.48685 4.06425 6.56392 4.16685 6.66653C4.26946 6.76913 4.34653 6.8944 4.39187 7.03223L5.27772 9.72547L6.16357 7.03223C6.20891 6.8944 6.28598 6.76913 6.38859 6.66653C6.49119 6.56392 6.61646 6.48685 6.7543 6.44151L9.44753 5.55566L6.7543 4.66981C6.61646 4.62447 6.49119 4.5474 6.38859 4.44479C6.28598 4.34219 6.20891 4.21692 6.16357 4.07909L5.27772 1.38585Z" fill="black" fill-opacity="0.15" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M8.35938 12.3555C8.35938 12.0664 8.52734 11.8086 9.00781 11.3594L10.2031 10.2344C10.6094 9.85156 10.7891 9.60156 10.7891 9.34375C10.7891 9.05469 10.5781 8.85938 10.2734 8.85938C10.0391 8.85938 9.87109 8.95312 9.66406 9.21094C9.42578 9.50781 9.25391 9.60938 8.99219 9.60938C8.61719 9.60938 8.35156 9.35938 8.35156 9.01172C8.35156 8.25 9.26953 7.57812 10.3594 7.57812C11.4961 7.57812 12.3438 8.26172 12.3438 9.17969C12.3438 9.75391 12.0391 10.3008 11.418 10.8516L10.4961 11.6719V11.7344H11.8047C12.2578 11.7344 12.5391 11.9766 12.5391 12.3711C12.5391 12.7656 12.2656 13 11.8047 13H9.08203C8.65234 13 8.35938 12.7383 8.35938 12.3555Z" fill="black"/>
|
||||
<path d="M11.0834 1.38585V3.71918M9.91675 2.55248H12.2501" stroke="black" stroke-opacity="0.75" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.5 KiB |
@@ -1,5 +0,0 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7 8.9V11C5.34478 11 4.65522 11 3 11V10.4L7 5.6V5H3V7.1" stroke="black" stroke-width="1.5"/>
|
||||
<path d="M12 5L14 8L12 11" stroke="black" stroke-width="1.5"/>
|
||||
<path d="M10 6.5L11 8L10 9.5" stroke="black" stroke-width="1.5"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 334 B |
@@ -17,7 +17,6 @@
|
||||
"escape": "menu::Cancel",
|
||||
"ctrl-escape": "menu::Cancel",
|
||||
"ctrl-c": "menu::Cancel",
|
||||
"alt-shift-enter": "menu::Restart",
|
||||
"alt-enter": ["picker::ConfirmInput", { "secondary": false }],
|
||||
"ctrl-alt-enter": ["picker::ConfirmInput", { "secondary": true }],
|
||||
"ctrl-shift-w": "workspace::CloseWindow",
|
||||
@@ -426,10 +425,7 @@
|
||||
"ctrl-shift-r": "task::Rerun",
|
||||
"ctrl-alt-r": "task::Rerun",
|
||||
"alt-t": "task::Rerun",
|
||||
"alt-shift-t": "task::Spawn",
|
||||
"alt-shift-r": ["task::Spawn", { "reveal_target": "center" }]
|
||||
// also possible to spawn tasks by name:
|
||||
// "foo-bar": ["task::Spawn", { "task_name": "MyTask", "reveal_target": "dock" }]
|
||||
"alt-shift-t": "task::Spawn"
|
||||
}
|
||||
},
|
||||
// Bindings from Sublime Text
|
||||
@@ -472,7 +468,6 @@
|
||||
},
|
||||
{
|
||||
"context": "Editor && showing_completions",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"enter": "editor::ConfirmCompletion",
|
||||
"tab": "editor::ComposeCompletion"
|
||||
@@ -480,7 +475,6 @@
|
||||
},
|
||||
{
|
||||
"context": "Editor && inline_completion && !showing_completions",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"tab": "editor::AcceptInlineCompletion"
|
||||
}
|
||||
|
||||
@@ -24,7 +24,6 @@
|
||||
"cmd-escape": "menu::Cancel",
|
||||
"ctrl-escape": "menu::Cancel",
|
||||
"ctrl-c": "menu::Cancel",
|
||||
"alt-shift-enter": "menu::Restart",
|
||||
"cmd-shift-w": "workspace::CloseWindow",
|
||||
"shift-escape": "workspace::ToggleZoom",
|
||||
"cmd-o": "workspace::Open",
|
||||
@@ -224,9 +223,7 @@
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-n": "assistant2::NewThread",
|
||||
"cmd-shift-h": "assistant2::OpenHistory",
|
||||
"cmd-shift-m": "assistant2::ToggleModelSelector",
|
||||
"cmd-shift-a": "assistant2::ToggleContextPicker"
|
||||
"cmd-shift-h": "assistant2::OpenHistory"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -497,9 +494,8 @@
|
||||
"bindings": {
|
||||
"cmd-shift-r": "task::Spawn",
|
||||
"cmd-alt-r": "task::Rerun",
|
||||
"ctrl-alt-shift-r": ["task::Spawn", { "reveal_target": "center" }]
|
||||
// also possible to spawn tasks by name:
|
||||
// "foo-bar": ["task_name::Spawn", { "task_name": "MyTask", "reveal_target": "dock" }]
|
||||
"alt-t": "task::Spawn",
|
||||
"alt-shift-t": "task::Spawn"
|
||||
}
|
||||
},
|
||||
// Bindings from Sublime Text
|
||||
@@ -610,7 +606,6 @@
|
||||
"context": "PromptEditor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-shift-a": "assistant2::ToggleContextPicker",
|
||||
"ctrl-[": "assistant::CyclePreviousInlineAssist",
|
||||
"ctrl-]": "assistant::CycleNextInlineAssist"
|
||||
}
|
||||
|
||||
@@ -15,10 +15,8 @@
|
||||
"ctrl-b": "editor::MoveLeft",
|
||||
"ctrl-n": "editor::MoveDown",
|
||||
"ctrl-p": "editor::MoveUp",
|
||||
"home": ["editor::MoveToBeginningOfLine", { "stop_at_soft_wraps": false }],
|
||||
"end": ["editor::MoveToEndOfLine", { "stop_at_soft_wraps": false }],
|
||||
"ctrl-a": ["editor::MoveToBeginningOfLine", { "stop_at_soft_wraps": false }],
|
||||
"ctrl-e": ["editor::MoveToEndOfLine", { "stop_at_soft_wraps": false }],
|
||||
"ctrl-a": "editor::MoveToBeginningOfLine",
|
||||
"ctrl-e": "editor::MoveToEndOfLine",
|
||||
"alt-f": "editor::MoveToNextSubwordEnd",
|
||||
"alt-b": "editor::MoveToPreviousSubwordStart",
|
||||
"ctrl-d": "editor::Delete",
|
||||
@@ -55,14 +53,6 @@
|
||||
"shift shift": "file_finder::Toggle"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "BufferSearchBar > Editor",
|
||||
"bindings": {
|
||||
"ctrl-s": "search::SelectNextMatch",
|
||||
"ctrl-r": "search::SelectPrevMatch",
|
||||
"ctrl-g": "buffer_search::Dismiss"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Pane",
|
||||
"bindings": {
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"ctrl->": "zed::IncreaseBufferFontSize",
|
||||
"ctrl-<": "zed::DecreaseBufferFontSize",
|
||||
"ctrl-shift-j": "editor::JoinLines",
|
||||
"ctrl-d": "editor::DuplicateSelection",
|
||||
"ctrl-d": "editor::DuplicateLineDown",
|
||||
"ctrl-y": "editor::DeleteLine",
|
||||
"ctrl-m": "editor::ScrollCursorCenter",
|
||||
"ctrl-pagedown": "editor::MovePageDown",
|
||||
|
||||
@@ -4,41 +4,19 @@
|
||||
"ctrl-shift-[": "pane::ActivatePrevItem",
|
||||
"ctrl-shift-]": "pane::ActivateNextItem",
|
||||
"ctrl-pageup": "pane::ActivatePrevItem",
|
||||
"ctrl-pagedown": "pane::ActivateNextItem",
|
||||
"ctrl-1": ["workspace::ActivatePane", 0],
|
||||
"ctrl-2": ["workspace::ActivatePane", 1],
|
||||
"ctrl-3": ["workspace::ActivatePane", 2],
|
||||
"ctrl-4": ["workspace::ActivatePane", 3],
|
||||
"ctrl-5": ["workspace::ActivatePane", 4],
|
||||
"ctrl-6": ["workspace::ActivatePane", 5],
|
||||
"ctrl-7": ["workspace::ActivatePane", 6],
|
||||
"ctrl-8": ["workspace::ActivatePane", 7],
|
||||
"ctrl-9": ["workspace::ActivatePane", 8],
|
||||
"ctrl-shift-1": ["workspace::MoveItemToPane", { "destination": 0, "focus": true }],
|
||||
"ctrl-shift-2": ["workspace::MoveItemToPane", { "destination": 1 }],
|
||||
"ctrl-shift-3": ["workspace::MoveItemToPane", { "destination": 2 }],
|
||||
"ctrl-shift-4": ["workspace::MoveItemToPane", { "destination": 3 }],
|
||||
"ctrl-shift-5": ["workspace::MoveItemToPane", { "destination": 4 }],
|
||||
"ctrl-shift-6": ["workspace::MoveItemToPane", { "destination": 5 }],
|
||||
"ctrl-shift-7": ["workspace::MoveItemToPane", { "destination": 6 }],
|
||||
"ctrl-shift-8": ["workspace::MoveItemToPane", { "destination": 7 }],
|
||||
"ctrl-shift-9": ["workspace::MoveItemToPane", { "destination": 8 }]
|
||||
"ctrl-pagedown": "pane::ActivateNextItem"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor",
|
||||
"bindings": {
|
||||
"ctrl-alt-up": "editor::AddSelectionAbove",
|
||||
"ctrl-alt-down": "editor::AddSelectionBelow",
|
||||
"ctrl-shift-up": "editor::MoveLineUp",
|
||||
"ctrl-shift-down": "editor::MoveLineDown",
|
||||
"ctrl-shift-m": "editor::SelectLargerSyntaxNode",
|
||||
"ctrl-shift-l": "editor::SplitSelectionIntoLines",
|
||||
"ctrl-shift-a": "editor::SelectLargerSyntaxNode",
|
||||
"ctrl-shift-d": "editor::DuplicateSelection",
|
||||
"ctrl-shift-d": "editor::DuplicateLineDown",
|
||||
"alt-f3": "editor::SelectAllMatches", // find_all_under
|
||||
"f9": "editor::SortLinesCaseSensitive",
|
||||
"ctrl-f9": "editor::SortLinesCaseInsensitive",
|
||||
"f12": "editor::GoToDefinition",
|
||||
"ctrl-f12": "editor::GoToDefinitionSplit",
|
||||
"shift-f12": "editor::FindAllReferences",
|
||||
|
||||
@@ -15,10 +15,8 @@
|
||||
"ctrl-b": "editor::MoveLeft",
|
||||
"ctrl-n": "editor::MoveDown",
|
||||
"ctrl-p": "editor::MoveUp",
|
||||
"home": ["editor::MoveToBeginningOfLine", { "stop_at_soft_wraps": false }],
|
||||
"end": ["editor::MoveToEndOfLine", { "stop_at_soft_wraps": false }],
|
||||
"ctrl-a": ["editor::MoveToBeginningOfLine", { "stop_at_soft_wraps": false }],
|
||||
"ctrl-e": ["editor::MoveToEndOfLine", { "stop_at_soft_wraps": false }],
|
||||
"ctrl-a": "editor::MoveToBeginningOfLine",
|
||||
"ctrl-e": "editor::MoveToEndOfLine",
|
||||
"alt-f": "editor::MoveToNextSubwordEnd",
|
||||
"alt-b": "editor::MoveToPreviousSubwordStart",
|
||||
"ctrl-d": "editor::Delete",
|
||||
@@ -55,14 +53,6 @@
|
||||
"shift shift": "file_finder::Toggle"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "BufferSearchBar > Editor",
|
||||
"bindings": {
|
||||
"ctrl-s": "search::SelectNextMatch",
|
||||
"ctrl-r": "search::SelectPrevMatch",
|
||||
"ctrl-g": "buffer_search::Dismiss"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Pane",
|
||||
"bindings": {
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"ctrl->": "zed::IncreaseBufferFontSize",
|
||||
"ctrl-<": "zed::DecreaseBufferFontSize",
|
||||
"ctrl-shift-j": "editor::JoinLines",
|
||||
"cmd-d": "editor::DuplicateSelection",
|
||||
"cmd-d": "editor::DuplicateLineDown",
|
||||
"cmd-backspace": "editor::DeleteLine",
|
||||
"cmd-pagedown": "editor::MovePageDown",
|
||||
"cmd-pageup": "editor::MovePageUp",
|
||||
|
||||
@@ -4,25 +4,7 @@
|
||||
"cmd-shift-[": "pane::ActivatePrevItem",
|
||||
"cmd-shift-]": "pane::ActivateNextItem",
|
||||
"ctrl-pageup": "pane::ActivatePrevItem",
|
||||
"ctrl-pagedown": "pane::ActivateNextItem",
|
||||
"ctrl-1": ["workspace::ActivatePane", 0],
|
||||
"ctrl-2": ["workspace::ActivatePane", 1],
|
||||
"ctrl-3": ["workspace::ActivatePane", 2],
|
||||
"ctrl-4": ["workspace::ActivatePane", 3],
|
||||
"ctrl-5": ["workspace::ActivatePane", 4],
|
||||
"ctrl-6": ["workspace::ActivatePane", 5],
|
||||
"ctrl-7": ["workspace::ActivatePane", 6],
|
||||
"ctrl-8": ["workspace::ActivatePane", 7],
|
||||
"ctrl-9": ["workspace::ActivatePane", 8],
|
||||
"ctrl-shift-1": ["workspace::MoveItemToPane", { "destination": 0, "focus": true }],
|
||||
"ctrl-shift-2": ["workspace::MoveItemToPane", { "destination": 1 }],
|
||||
"ctrl-shift-3": ["workspace::MoveItemToPane", { "destination": 2 }],
|
||||
"ctrl-shift-4": ["workspace::MoveItemToPane", { "destination": 3 }],
|
||||
"ctrl-shift-5": ["workspace::MoveItemToPane", { "destination": 4 }],
|
||||
"ctrl-shift-6": ["workspace::MoveItemToPane", { "destination": 5 }],
|
||||
"ctrl-shift-7": ["workspace::MoveItemToPane", { "destination": 6 }],
|
||||
"ctrl-shift-8": ["workspace::MoveItemToPane", { "destination": 7 }],
|
||||
"ctrl-shift-9": ["workspace::MoveItemToPane", { "destination": 8 }]
|
||||
"ctrl-pagedown": "pane::ActivateNextItem"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -36,10 +18,8 @@
|
||||
"ctrl-shift-m": "editor::SelectLargerSyntaxNode",
|
||||
"cmd-shift-l": "editor::SplitSelectionIntoLines",
|
||||
"cmd-shift-a": "editor::SelectLargerSyntaxNode",
|
||||
"cmd-shift-d": "editor::DuplicateSelection",
|
||||
"cmd-shift-d": "editor::DuplicateLineDown",
|
||||
"ctrl-cmd-g": "editor::SelectAllMatches", // find_all_under
|
||||
"f5": "editor::SortLinesCaseSensitive",
|
||||
"ctrl-f5": "editor::SortLinesCaseInsensitive",
|
||||
"shift-f12": "editor::FindAllReferences",
|
||||
"alt-cmd-down": "editor::GoToDefinition",
|
||||
"ctrl-alt-cmd-down": "editor::GoToDefinitionSplit",
|
||||
|
||||
@@ -230,8 +230,8 @@
|
||||
"ctrl-pageup": "pane::ActivatePrevItem",
|
||||
"insert": "vim::InsertBefore",
|
||||
// tree-sitter related commands
|
||||
"[ x": "vim::SelectLargerSyntaxNode",
|
||||
"] x": "vim::SelectSmallerSyntaxNode",
|
||||
"[ x": "editor::SelectLargerSyntaxNode",
|
||||
"] x": "editor::SelectSmallerSyntaxNode",
|
||||
"] d": "editor::GoToDiagnostic",
|
||||
"[ d": "editor::GoToPrevDiagnostic",
|
||||
"] c": "editor::GoToHunk",
|
||||
|
||||
@@ -101,8 +101,6 @@
|
||||
// Whether to show the informational hover box when moving the mouse
|
||||
// over symbols in the editor.
|
||||
"hover_popover_enabled": true,
|
||||
// Time to wait before showing the informational hover box
|
||||
"hover_popover_delay": 350,
|
||||
// Whether to confirm before quitting Zed.
|
||||
"confirm_quit": false,
|
||||
// Whether to restore last closed project when fresh Zed instance is opened.
|
||||
@@ -146,9 +144,6 @@
|
||||
// 4. Highlight the full line (default):
|
||||
// "all"
|
||||
"current_line_highlight": "all",
|
||||
// The debounce delay before querying highlights from the language
|
||||
// server based on the current cursor location.
|
||||
"lsp_highlight_debounce": 75,
|
||||
// Whether to pop the completions menu while typing in an editor without
|
||||
// explicitly requesting it.
|
||||
"show_completions_on_input": true,
|
||||
@@ -160,9 +155,6 @@
|
||||
/// Whether to show the signature help after completion or a bracket pair inserted.
|
||||
/// If `auto_signature_help` is enabled, this setting will be treated as enabled also.
|
||||
"show_signature_help_after_edits": false,
|
||||
/// Whether to show the inline completions next to the completions provided by a language server.
|
||||
/// Only has an effect if inline completion provider supports it.
|
||||
"show_inline_completions_in_menu": true,
|
||||
// Whether to show wrap guides (vertical rulers) in the editor.
|
||||
// Setting this to true will show a guide at the 'preferred_line_length' value
|
||||
// if 'soft_wrap' is set to 'preferred_line_length', and will show any
|
||||
@@ -257,14 +249,7 @@
|
||||
// Whether to show selected symbol occurrences in the scrollbar.
|
||||
"selected_symbol": true,
|
||||
// Whether to show diagnostic indicators in the scrollbar.
|
||||
"diagnostics": true,
|
||||
/// Forcefully enable or disable the scrollbar for each axis
|
||||
"axes": {
|
||||
/// When false, forcefully disables the horizontal scrollbar. Otherwise, obey other settings.
|
||||
"horizontal": true,
|
||||
/// When false, forcefully disables the vertical scrollbar. Otherwise, obey other settings.
|
||||
"vertical": true
|
||||
}
|
||||
"diagnostics": true
|
||||
},
|
||||
// Enable middle-click paste on Linux.
|
||||
"middle_click_paste": true,
|
||||
@@ -314,8 +299,6 @@
|
||||
"vertical_scroll_margin": 3,
|
||||
// Whether to scroll when clicking near the edge of the visible text area.
|
||||
"autoscroll_on_clicks": false,
|
||||
// The number of characters to keep on either side when scrolling with the mouse
|
||||
"horizontal_scroll_margin": 5,
|
||||
// Scroll sensitivity multiplier. This multiplier is applied
|
||||
// to both the horizontal and vertical delta values while scrolling.
|
||||
"scroll_sensitivity": 1.0,
|
||||
@@ -488,14 +471,6 @@
|
||||
// Default width of the chat panel.
|
||||
"default_width": 240
|
||||
},
|
||||
"git_panel": {
|
||||
// Whether to show the git panel button in the status bar.
|
||||
"button": true,
|
||||
// Where to the git panel. Can be 'left' or 'right'.
|
||||
"dock": "left",
|
||||
// Default width of the git panel.
|
||||
"default_width": 360
|
||||
},
|
||||
"message_editor": {
|
||||
// Whether to automatically replace emoji shortcodes with emoji characters.
|
||||
// For example: typing `:wave:` gets replaced with `👋`.
|
||||
@@ -566,8 +541,6 @@
|
||||
// 4. Save when idle for a certain amount of time:
|
||||
// "autosave": { "after_delay": {"milliseconds": 500} },
|
||||
"autosave": "off",
|
||||
// Maximum number of tabs per pane. Unset for unlimited.
|
||||
"max_tabs": null,
|
||||
// Settings related to the editor's tab bar.
|
||||
"tab_bar": {
|
||||
// Whether or not to show the tab bar in the editor
|
||||
@@ -1101,9 +1074,6 @@
|
||||
"prettier": {
|
||||
"allowed": true
|
||||
}
|
||||
},
|
||||
"Zig": {
|
||||
"language_servers": ["zls", "..."]
|
||||
}
|
||||
},
|
||||
// Different settings for specific language models.
|
||||
|
||||
@@ -15,14 +15,10 @@
|
||||
// Whether to allow multiple instances of the same task to be run, or rather wait for the existing ones to finish, defaults to `false`.
|
||||
"allow_concurrent_runs": false,
|
||||
// What to do with the terminal pane and tab, after the command was started:
|
||||
// * `always` — always show the task's pane, and focus the corresponding tab in it (default)
|
||||
// * `no_focus` — always show the task's pane, add the task's tab in it, but don't focus it
|
||||
// * `never` — do not alter focus, but still add/reuse the task's tab in its pane
|
||||
// * `always` — always show the terminal pane, add and focus the corresponding task's tab in it (default)
|
||||
// * `no_focus` — always show the terminal pane, add/reuse the task's tab there, but don't focus it
|
||||
// * `never` — avoid changing current terminal pane focus, but still add/reuse the task's tab there
|
||||
"reveal": "always",
|
||||
// Where to place the task's terminal item after starting the task:
|
||||
// * `dock` — in the terminal dock, "regular" terminal items' place (default)
|
||||
// * `center` — in the central pane group, "main" editor area
|
||||
"reveal_target": "dock",
|
||||
// What to do with the terminal pane and tab, after the command had finished:
|
||||
// * `never` — Do nothing when the command finishes (default)
|
||||
// * `always` — always hide the terminal tab, hide the pane also if it was the last tab in it
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"style": {
|
||||
"border": "#464b57ff",
|
||||
"border.variant": "#363c46ff",
|
||||
"border.focused": "#47679eff",
|
||||
"border.focused": "#293b5bff",
|
||||
"border.selected": "#293b5bff",
|
||||
"border.transparent": "#00000000",
|
||||
"border.disabled": "#414754ff",
|
||||
@@ -384,7 +384,7 @@
|
||||
"style": {
|
||||
"border": "#c9c9caff",
|
||||
"border.variant": "#dfdfe0ff",
|
||||
"border.focused": "#7d82e8ff",
|
||||
"border.focused": "#cbcdf6ff",
|
||||
"border.selected": "#cbcdf6ff",
|
||||
"border.transparent": "#00000000",
|
||||
"border.disabled": "#d3d3d4ff",
|
||||
|
||||
@@ -493,7 +493,7 @@ impl Render for ActivityIndicator {
|
||||
}),
|
||||
),
|
||||
)
|
||||
.anchor(gpui::Corner::BottomLeft)
|
||||
.anchor(gpui::AnchorCorner::BottomLeft)
|
||||
.menu(move |cx| {
|
||||
let strong_this = this.upgrade()?;
|
||||
let mut has_work = false;
|
||||
|
||||
@@ -55,7 +55,7 @@ use language_model::{
|
||||
LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry, Role,
|
||||
ZED_CLOUD_PROVIDER_ID,
|
||||
};
|
||||
use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu};
|
||||
use language_model_selector::{LanguageModelPickerDelegate, LanguageModelSelector};
|
||||
use multi_buffer::MultiBufferRow;
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use project::lsp_store::LocalLspAdapterDelegate;
|
||||
@@ -108,6 +108,7 @@ pub fn init(cx: &mut AppContext) {
|
||||
|
||||
workspace.toggle_panel_focus::<AssistantPanel>(cx);
|
||||
})
|
||||
.register_action(AssistantPanel::inline_assist)
|
||||
.register_action(ContextEditor::quote_selection)
|
||||
.register_action(ContextEditor::insert_selection)
|
||||
.register_action(ContextEditor::copy_code)
|
||||
@@ -142,7 +143,7 @@ pub struct AssistantPanel {
|
||||
languages: Arc<LanguageRegistry>,
|
||||
fs: Arc<dyn Fs>,
|
||||
subscriptions: Vec<Subscription>,
|
||||
model_selector_menu_handle: PopoverMenuHandle<LanguageModelSelector>,
|
||||
model_selector_menu_handle: PopoverMenuHandle<Picker<LanguageModelPickerDelegate>>,
|
||||
model_summary_editor: View<Editor>,
|
||||
authenticate_provider_task: Option<(LanguageModelProviderId, Task<()>)>,
|
||||
configuration_subscription: Option<Subscription>,
|
||||
@@ -304,7 +305,7 @@ impl PickerDelegate for SavedContextPickerDelegate {
|
||||
ListItem::new(ix)
|
||||
.inset(true)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.toggle_state(selected)
|
||||
.selected(selected)
|
||||
.child(item),
|
||||
)
|
||||
}
|
||||
@@ -340,12 +341,11 @@ impl AssistantPanel {
|
||||
) -> Self {
|
||||
let model_selector_menu_handle = PopoverMenuHandle::default();
|
||||
let model_summary_editor = cx.new_view(Editor::single_line);
|
||||
let context_editor_toolbar = cx.new_view(|cx| {
|
||||
let context_editor_toolbar = cx.new_view(|_| {
|
||||
ContextEditorToolbarItem::new(
|
||||
workspace,
|
||||
model_selector_menu_handle.clone(),
|
||||
model_summary_editor.clone(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
@@ -441,7 +441,7 @@ impl AssistantPanel {
|
||||
)
|
||||
}
|
||||
})
|
||||
.toggle_state(
|
||||
.selected(
|
||||
pane.active_item()
|
||||
.map_or(false, |item| item.downcast::<ContextHistory>().is_some()),
|
||||
);
|
||||
@@ -4455,36 +4455,23 @@ impl FollowableItem for ContextEditor {
|
||||
}
|
||||
|
||||
pub struct ContextEditorToolbarItem {
|
||||
fs: Arc<dyn Fs>,
|
||||
active_context_editor: Option<WeakView<ContextEditor>>,
|
||||
model_summary_editor: View<Editor>,
|
||||
language_model_selector: View<LanguageModelSelector>,
|
||||
language_model_selector_menu_handle: PopoverMenuHandle<LanguageModelSelector>,
|
||||
model_selector_menu_handle: PopoverMenuHandle<Picker<LanguageModelPickerDelegate>>,
|
||||
}
|
||||
|
||||
impl ContextEditorToolbarItem {
|
||||
pub fn new(
|
||||
workspace: &Workspace,
|
||||
model_selector_menu_handle: PopoverMenuHandle<LanguageModelSelector>,
|
||||
model_selector_menu_handle: PopoverMenuHandle<Picker<LanguageModelPickerDelegate>>,
|
||||
model_summary_editor: View<Editor>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
Self {
|
||||
fs: workspace.app_state().fs.clone(),
|
||||
active_context_editor: None,
|
||||
model_summary_editor,
|
||||
language_model_selector: cx.new_view(|cx| {
|
||||
let fs = workspace.app_state().fs.clone();
|
||||
LanguageModelSelector::new(
|
||||
move |model, cx| {
|
||||
update_settings_file::<AssistantSettings>(
|
||||
fs.clone(),
|
||||
cx,
|
||||
move |settings, _| settings.set_model(model.clone()),
|
||||
);
|
||||
},
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
language_model_selector_menu_handle: model_selector_menu_handle,
|
||||
model_selector_menu_handle,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4573,8 +4560,17 @@ impl Render for ContextEditorToolbarItem {
|
||||
// .map(|remaining_items| format!("Files to scan: {}", remaining_items))
|
||||
// })
|
||||
.child(
|
||||
LanguageModelSelectorPopoverMenu::new(
|
||||
self.language_model_selector.clone(),
|
||||
LanguageModelSelector::new(
|
||||
{
|
||||
let fs = self.fs.clone();
|
||||
move |model, cx| {
|
||||
update_settings_file::<AssistantSettings>(
|
||||
fs.clone(),
|
||||
cx,
|
||||
move |settings, _| settings.set_model(model.clone()),
|
||||
);
|
||||
}
|
||||
},
|
||||
ButtonLike::new("active-model")
|
||||
.style(ButtonStyle::Subtle)
|
||||
.child(
|
||||
@@ -4620,7 +4616,7 @@ impl Render for ContextEditorToolbarItem {
|
||||
Tooltip::for_action("Change Model", &ToggleModelSelector, cx)
|
||||
}),
|
||||
)
|
||||
.with_handle(self.language_model_selector_menu_handle.clone()),
|
||||
.with_handle(self.model_selector_menu_handle.clone()),
|
||||
)
|
||||
.children(self.render_remaining_tokens(cx));
|
||||
|
||||
@@ -4955,7 +4951,7 @@ fn render_slash_command_output_toggle(
|
||||
("slash-command-output-fold-indicator", row.0 as u64),
|
||||
!is_folded,
|
||||
)
|
||||
.toggle_state(is_folded)
|
||||
.selected(is_folded)
|
||||
.on_click(move |_e, cx| fold(!is_folded, cx))
|
||||
.into_any_element()
|
||||
}
|
||||
@@ -4970,7 +4966,7 @@ fn fold_toggle(
|
||||
) -> AnyElement {
|
||||
move |row, is_folded, fold, _cx| {
|
||||
Disclosure::new((name, row.0 as u64), !is_folded)
|
||||
.toggle_state(is_folded)
|
||||
.selected(is_folded)
|
||||
.on_click(move |_e, cx| fold(!is_folded, cx))
|
||||
.into_any_element()
|
||||
}
|
||||
@@ -5012,7 +5008,7 @@ fn render_quote_selection_output_toggle(
|
||||
_cx: &mut WindowContext,
|
||||
) -> AnyElement {
|
||||
Disclosure::new(("quote-selection-indicator", row.0 as u64), !is_folded)
|
||||
.toggle_state(is_folded)
|
||||
.selected(is_folded)
|
||||
.on_click(move |_e, cx| fold(!is_folded, cx))
|
||||
.into_any_element()
|
||||
}
|
||||
@@ -5035,7 +5031,7 @@ fn render_pending_slash_command_gutter_decoration(
|
||||
icon = icon.icon_color(Color::Muted);
|
||||
}
|
||||
PendingSlashCommandStatus::Running { .. } => {
|
||||
icon = icon.toggle_state(true);
|
||||
icon = icon.selected(true);
|
||||
}
|
||||
PendingSlashCommandStatus::Error(_) => icon = icon.icon_color(Color::Error),
|
||||
}
|
||||
@@ -5117,11 +5113,9 @@ fn make_lsp_adapter_delegate(
|
||||
return Ok(None::<Arc<dyn LspAdapterDelegate>>);
|
||||
};
|
||||
let http_client = project.client().http_client().clone();
|
||||
project.lsp_store().update(cx, |_, cx| {
|
||||
project.lsp_store().update(cx, |lsp_store, cx| {
|
||||
Ok(Some(LocalLspAdapterDelegate::new(
|
||||
project.languages().clone(),
|
||||
project.environment(),
|
||||
cx.weak_model(),
|
||||
lsp_store,
|
||||
&worktree,
|
||||
http_client,
|
||||
project.fs().clone(),
|
||||
|
||||
@@ -17,7 +17,7 @@ use futures::{
|
||||
channel::mpsc,
|
||||
stream::{self, StreamExt},
|
||||
};
|
||||
use gpui::{prelude::*, AppContext, Model, SharedString, Task, TestAppContext, WeakView};
|
||||
use gpui::{AppContext, Model, SharedString, Task, TestAppContext, WeakView};
|
||||
use language::{Buffer, BufferSnapshot, LanguageRegistry, LspAdapterDelegate};
|
||||
use language_model::{LanguageModelCacheConfiguration, LanguageModelRegistry, Role};
|
||||
use parking_lot::Mutex;
|
||||
@@ -35,7 +35,7 @@ use std::{
|
||||
sync::{atomic::AtomicBool, Arc},
|
||||
};
|
||||
use text::{network::Network, OffsetRangeExt as _, ReplicaId, ToOffset};
|
||||
use ui::{IconName, WindowContext};
|
||||
use ui::{Context as _, IconName, WindowContext};
|
||||
use unindent::Unindent;
|
||||
use util::{
|
||||
test::{generate_marked_text, marked_text_ranges},
|
||||
|
||||
@@ -716,7 +716,7 @@ impl ContextStore {
|
||||
let candidates = metadata
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(id, metadata)| StringMatchCandidate::new(id, &metadata.title))
|
||||
.map(|(id, metadata)| StringMatchCandidate::new(id, metadata.title.clone()))
|
||||
.collect::<Vec<_>>();
|
||||
let matches = fuzzy::match_strings(
|
||||
&candidates,
|
||||
|
||||
@@ -33,7 +33,7 @@ use language_model::{
|
||||
LanguageModel, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage,
|
||||
LanguageModelTextStream, Role,
|
||||
};
|
||||
use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu};
|
||||
use language_model_selector::LanguageModelSelector;
|
||||
use language_models::report_assistant_event;
|
||||
use multi_buffer::MultiBufferRow;
|
||||
use parking_lot::Mutex;
|
||||
@@ -47,7 +47,6 @@ use std::{
|
||||
iter, mem,
|
||||
ops::{Range, RangeInclusive},
|
||||
pin::Pin,
|
||||
rc::Rc,
|
||||
sync::Arc,
|
||||
task::{self, Poll},
|
||||
time::{Duration, Instant},
|
||||
@@ -175,7 +174,7 @@ impl InlineAssistant {
|
||||
if let Some(editor) = item.act_as::<Editor>(cx) {
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.push_code_action_provider(
|
||||
Rc::new(AssistantCodeActionProvider {
|
||||
Arc::new(AssistantCodeActionProvider {
|
||||
editor: cx.view().downgrade(),
|
||||
workspace: workspace.downgrade(),
|
||||
}),
|
||||
@@ -1359,8 +1358,8 @@ enum PromptEditorEvent {
|
||||
|
||||
struct PromptEditor {
|
||||
id: InlineAssistId,
|
||||
fs: Arc<dyn Fs>,
|
||||
editor: View<Editor>,
|
||||
language_model_selector: View<LanguageModelSelector>,
|
||||
edited_since_done: bool,
|
||||
gutter_dimensions: Arc<Mutex<GutterDimensions>>,
|
||||
prompt_history: VecDeque<String>,
|
||||
@@ -1442,15 +1441,6 @@ impl Render for PromptEditor {
|
||||
]
|
||||
}
|
||||
CodegenStatus::Error(_) | CodegenStatus::Done => {
|
||||
let must_rerun =
|
||||
self.edited_since_done || matches!(status, CodegenStatus::Error(_));
|
||||
// when accept button isn't visible, then restart maps to confirm
|
||||
// when accept button is visible, then restart must be mapped to an alternate keyboard shortcut
|
||||
let restart_key: &dyn gpui::Action = if must_rerun {
|
||||
&menu::Confirm
|
||||
} else {
|
||||
&menu::Restart
|
||||
};
|
||||
vec![
|
||||
IconButton::new("cancel", IconName::Close)
|
||||
.icon_color(Color::Muted)
|
||||
@@ -1460,22 +1450,23 @@ impl Render for PromptEditor {
|
||||
cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::CancelRequested)),
|
||||
)
|
||||
.into_any_element(),
|
||||
IconButton::new("restart", IconName::RotateCw)
|
||||
.icon_color(Color::Muted)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(|cx| {
|
||||
Tooltip::with_meta(
|
||||
"Regenerate Transformation",
|
||||
Some(restart_key),
|
||||
"Current change will be discarded",
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.on_click(cx.listener(|_, _, cx| {
|
||||
cx.emit(PromptEditorEvent::StartRequested);
|
||||
}))
|
||||
.into_any_element(),
|
||||
if !must_rerun {
|
||||
if self.edited_since_done || matches!(status, CodegenStatus::Error(_)) {
|
||||
IconButton::new("restart", IconName::RotateCw)
|
||||
.icon_color(Color::Info)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(|cx| {
|
||||
Tooltip::with_meta(
|
||||
"Restart Transformation",
|
||||
Some(&menu::Confirm),
|
||||
"Changes will be discarded",
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.on_click(cx.listener(|_, _, cx| {
|
||||
cx.emit(PromptEditorEvent::StartRequested);
|
||||
}))
|
||||
.into_any_element()
|
||||
} else {
|
||||
IconButton::new("confirm", IconName::Check)
|
||||
.icon_color(Color::Info)
|
||||
.shape(IconButtonShape::Square)
|
||||
@@ -1484,8 +1475,6 @@ impl Render for PromptEditor {
|
||||
cx.emit(PromptEditorEvent::ConfirmRequested);
|
||||
}))
|
||||
.into_any_element()
|
||||
} else {
|
||||
div().into_any_element()
|
||||
},
|
||||
]
|
||||
}
|
||||
@@ -1502,7 +1491,6 @@ impl Render for PromptEditor {
|
||||
.py(cx.line_height() / 2.5)
|
||||
.on_action(cx.listener(Self::confirm))
|
||||
.on_action(cx.listener(Self::cancel))
|
||||
.on_action(cx.listener(Self::restart))
|
||||
.on_action(cx.listener(Self::move_up))
|
||||
.on_action(cx.listener(Self::move_down))
|
||||
.capture_action(cx.listener(Self::cycle_prev))
|
||||
@@ -1512,27 +1500,43 @@ impl Render for PromptEditor {
|
||||
.w(gutter_dimensions.full_width() + (gutter_dimensions.margin / 2.0))
|
||||
.justify_center()
|
||||
.gap_2()
|
||||
.child(LanguageModelSelectorPopoverMenu::new(
|
||||
self.language_model_selector.clone(),
|
||||
IconButton::new("context", IconName::SettingsAlt)
|
||||
.shape(IconButtonShape::Square)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
.tooltip(move |cx| {
|
||||
Tooltip::with_meta(
|
||||
format!(
|
||||
"Using {}",
|
||||
LanguageModelRegistry::read_global(cx)
|
||||
.active_model()
|
||||
.map(|model| model.name().0)
|
||||
.unwrap_or_else(|| "No model selected".into()),
|
||||
),
|
||||
None,
|
||||
"Change Model",
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
))
|
||||
.child(
|
||||
LanguageModelSelector::new(
|
||||
{
|
||||
let fs = self.fs.clone();
|
||||
move |model, cx| {
|
||||
update_settings_file::<AssistantSettings>(
|
||||
fs.clone(),
|
||||
cx,
|
||||
move |settings, _| settings.set_model(model.clone()),
|
||||
);
|
||||
}
|
||||
},
|
||||
IconButton::new("context", IconName::SettingsAlt)
|
||||
.shape(IconButtonShape::Square)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
.tooltip(move |cx| {
|
||||
Tooltip::with_meta(
|
||||
format!(
|
||||
"Using {}",
|
||||
LanguageModelRegistry::read_global(cx)
|
||||
.active_model()
|
||||
.map(|model| model.name().0)
|
||||
.unwrap_or_else(|| "No model selected".into()),
|
||||
),
|
||||
None,
|
||||
"Change Model",
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
)
|
||||
.info_text(
|
||||
"Inline edits use context\n\
|
||||
from the currently selected\n\
|
||||
assistant panel tab.",
|
||||
),
|
||||
)
|
||||
.map(|el| {
|
||||
let CodegenStatus::Error(error) = self.codegen.read(cx).status(cx) else {
|
||||
return el;
|
||||
@@ -1546,7 +1550,7 @@ impl Render for PromptEditor {
|
||||
v_flex()
|
||||
.child(
|
||||
IconButton::new("rate-limit-error", IconName::XCircle)
|
||||
.toggle_state(self.show_rate_limit_notice)
|
||||
.selected(self.show_rate_limit_notice)
|
||||
.shape(IconButtonShape::Square)
|
||||
.icon_size(IconSize::Small)
|
||||
.on_click(cx.listener(Self::toggle_rate_limit_notice)),
|
||||
@@ -1556,7 +1560,7 @@ impl Render for PromptEditor {
|
||||
anchored()
|
||||
.position_mode(gpui::AnchoredPositionMode::Local)
|
||||
.position(point(px(0.), px(24.)))
|
||||
.anchor(gpui::Corner::TopLeft)
|
||||
.anchor(gpui::AnchorCorner::TopLeft)
|
||||
.child(self.render_rate_limit_notice(cx)),
|
||||
)
|
||||
})),
|
||||
@@ -1638,19 +1642,6 @@ impl PromptEditor {
|
||||
let mut this = Self {
|
||||
id,
|
||||
editor: prompt_editor,
|
||||
language_model_selector: cx.new_view(|cx| {
|
||||
let fs = fs.clone();
|
||||
LanguageModelSelector::new(
|
||||
move |model, cx| {
|
||||
update_settings_file::<AssistantSettings>(
|
||||
fs.clone(),
|
||||
cx,
|
||||
move |settings, _| settings.set_model(model.clone()),
|
||||
);
|
||||
},
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
edited_since_done: false,
|
||||
gutter_dimensions,
|
||||
prompt_history,
|
||||
@@ -1659,6 +1650,7 @@ impl PromptEditor {
|
||||
_codegen_subscription: cx.observe(&codegen, Self::handle_codegen_changed),
|
||||
editor_subscriptions: Vec::new(),
|
||||
codegen,
|
||||
fs,
|
||||
pending_token_count: Task::ready(Ok(())),
|
||||
token_counts: None,
|
||||
_token_count_subscriptions: token_count_subscriptions,
|
||||
@@ -1849,10 +1841,6 @@ impl PromptEditor {
|
||||
}
|
||||
}
|
||||
|
||||
fn restart(&mut self, _: &menu::Restart, cx: &mut ViewContext<Self>) {
|
||||
cx.emit(PromptEditorEvent::StartRequested);
|
||||
}
|
||||
|
||||
fn cancel(&mut self, _: &editor::actions::Cancel, cx: &mut ViewContext<Self>) {
|
||||
match self.codegen.read(cx).status(cx) {
|
||||
CodegenStatus::Idle | CodegenStatus::Done | CodegenStatus::Error(_) => {
|
||||
@@ -2149,15 +2137,15 @@ impl PromptEditor {
|
||||
"dont-show-again",
|
||||
Label::new("Don't show again"),
|
||||
if dismissed_rate_limit_notice() {
|
||||
ui::ToggleState::Selected
|
||||
ui::Selection::Selected
|
||||
} else {
|
||||
ui::ToggleState::Unselected
|
||||
ui::Selection::Unselected
|
||||
},
|
||||
|selection, cx| {
|
||||
let is_dismissed = match selection {
|
||||
ui::ToggleState::Unselected => false,
|
||||
ui::ToggleState::Indeterminate => return,
|
||||
ui::ToggleState::Selected => true,
|
||||
ui::Selection::Unselected => false,
|
||||
ui::Selection::Indeterminate => return,
|
||||
ui::Selection::Selected => true,
|
||||
};
|
||||
|
||||
set_rate_limit_notice_dismissed(is_dismissed, cx)
|
||||
|
||||
@@ -11,8 +11,8 @@ use futures::{
|
||||
use fuzzy::StringMatchCandidate;
|
||||
use gpui::{
|
||||
actions, point, size, transparent_black, Action, AppContext, BackgroundExecutor, Bounds,
|
||||
EventEmitter, Global, PromptLevel, ReadGlobal, Subscription, Task, TextStyle, TitlebarOptions,
|
||||
UpdateGlobal, View, WindowBounds, WindowHandle, WindowOptions,
|
||||
EventEmitter, Global, HighlightStyle, PromptLevel, ReadGlobal, Subscription, Task, TextStyle,
|
||||
TitlebarOptions, UpdateGlobal, View, WindowBounds, WindowHandle, WindowOptions,
|
||||
};
|
||||
use heed::{
|
||||
types::{SerdeBincode, SerdeJson, Str},
|
||||
@@ -232,13 +232,13 @@ impl PickerDelegate for PromptPickerDelegate {
|
||||
let element = ListItem::new(ix)
|
||||
.inset(true)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.toggle_state(selected)
|
||||
.selected(selected)
|
||||
.child(h_flex().h_5().line_height(relative(1.)).child(Label::new(
|
||||
prompt.title.clone().unwrap_or("Untitled".into()),
|
||||
)))
|
||||
.end_slot::<IconButton>(default.then(|| {
|
||||
IconButton::new("toggle-default-prompt", IconName::SparkleFilled)
|
||||
.toggle_state(true)
|
||||
.selected(true)
|
||||
.icon_color(Color::Accent)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(move |cx| Tooltip::text("Remove from Default Prompt", cx))
|
||||
@@ -274,7 +274,7 @@ impl PickerDelegate for PromptPickerDelegate {
|
||||
})
|
||||
.child(
|
||||
IconButton::new("toggle-default-prompt", IconName::Sparkle)
|
||||
.toggle_state(default)
|
||||
.selected(default)
|
||||
.selected_icon(IconName::SparkleFilled)
|
||||
.icon_color(if default { Color::Accent } else { Color::Muted })
|
||||
.shape(IconButtonShape::Square)
|
||||
@@ -928,8 +928,10 @@ impl PromptLibrary {
|
||||
status: cx.theme().status().clone(),
|
||||
inlay_hints_style:
|
||||
editor::make_inlay_hints_style(cx),
|
||||
inline_completion_styles:
|
||||
editor::make_suggestion_styles(cx),
|
||||
suggestions_style: HighlightStyle {
|
||||
color: Some(cx.theme().status().predictive),
|
||||
..HighlightStyle::default()
|
||||
},
|
||||
..EditorStyle::default()
|
||||
},
|
||||
)),
|
||||
@@ -1053,7 +1055,7 @@ impl PromptLibrary {
|
||||
IconName::Sparkle,
|
||||
)
|
||||
.style(ButtonStyle::Transparent)
|
||||
.toggle_state(prompt_metadata.default)
|
||||
.selected(prompt_metadata.default)
|
||||
.selected_icon(IconName::SparkleFilled)
|
||||
.icon_color(if prompt_metadata.default {
|
||||
Color::Accent
|
||||
@@ -1439,7 +1441,10 @@ impl PromptStore {
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(ix, metadata)| {
|
||||
Some(StringMatchCandidate::new(ix, metadata.title.as_ref()?))
|
||||
Some(StringMatchCandidate::new(
|
||||
ix,
|
||||
metadata.title.as_ref()?.to_string(),
|
||||
))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let matches = fuzzy::match_strings(
|
||||
|
||||
@@ -7,13 +7,11 @@ use editor::{CompletionProvider, Editor};
|
||||
use fuzzy::{match_strings, StringMatchCandidate};
|
||||
use gpui::{AppContext, Model, Task, ViewContext, WeakView, WindowContext};
|
||||
use language::{Anchor, Buffer, CodeLabel, Documentation, HighlightId, LanguageServerId, ToPoint};
|
||||
use parking_lot::Mutex;
|
||||
use parking_lot::{Mutex, RwLock};
|
||||
use project::CompletionIntent;
|
||||
use rope::Point;
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
ops::Range,
|
||||
rc::Rc,
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering::SeqCst},
|
||||
Arc,
|
||||
@@ -80,7 +78,11 @@ impl SlashCommandCompletionProvider {
|
||||
.command_names(cx)
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(ix, def)| StringMatchCandidate::new(ix, &def))
|
||||
.map(|(ix, def)| StringMatchCandidate {
|
||||
id: ix,
|
||||
string: def.to_string(),
|
||||
char_bag: def.as_ref().into(),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let command_name = command_name.to_string();
|
||||
let editor = self.editor.clone();
|
||||
@@ -149,7 +151,6 @@ impl SlashCommandCompletionProvider {
|
||||
server_id: LanguageServerId(0),
|
||||
lsp_completion: Default::default(),
|
||||
confirm,
|
||||
resolved: true,
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
@@ -243,7 +244,6 @@ impl SlashCommandCompletionProvider {
|
||||
server_id: LanguageServerId(0),
|
||||
lsp_completion: Default::default(),
|
||||
confirm,
|
||||
resolved: true,
|
||||
}
|
||||
})
|
||||
.collect())
|
||||
@@ -326,12 +326,22 @@ impl CompletionProvider for SlashCommandCompletionProvider {
|
||||
&self,
|
||||
_: Model<Buffer>,
|
||||
_: Vec<usize>,
|
||||
_: Rc<RefCell<Box<[project::Completion]>>>,
|
||||
_: Arc<RwLock<Box<[project::Completion]>>>,
|
||||
_: &mut ViewContext<Editor>,
|
||||
) -> Task<Result<bool>> {
|
||||
Task::ready(Ok(true))
|
||||
}
|
||||
|
||||
fn apply_additional_edits_for_completion(
|
||||
&self,
|
||||
_: Model<Buffer>,
|
||||
_: project::Completion,
|
||||
_: bool,
|
||||
_: &mut ViewContext<Editor>,
|
||||
) -> Task<Result<Option<language::Transaction>>> {
|
||||
Task::ready(Ok(None))
|
||||
}
|
||||
|
||||
fn is_completion_trigger(
|
||||
&self,
|
||||
buffer: &Model<Buffer>,
|
||||
|
||||
@@ -218,7 +218,10 @@ impl Options {
|
||||
}
|
||||
|
||||
fn match_candidates_for_args() -> [StringMatchCandidate; 1] {
|
||||
[StringMatchCandidate::new(0, INCLUDE_WARNINGS_ARGUMENT)]
|
||||
[StringMatchCandidate::new(
|
||||
0,
|
||||
INCLUDE_WARNINGS_ARGUMENT.to_string(),
|
||||
)]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -249,7 +249,11 @@ fn tab_items_for_queries(
|
||||
.enumerate()
|
||||
.filter_map(|(id, (full_path, ..))| {
|
||||
let path_string = full_path.as_deref()?.to_string_lossy().to_string();
|
||||
Some(fuzzy::StringMatchCandidate::new(id, &path_string))
|
||||
Some(fuzzy::StringMatchCandidate {
|
||||
id,
|
||||
char_bag: path_string.as_str().into(),
|
||||
string: path_string,
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let mut processed_matches = HashSet::default();
|
||||
|
||||
@@ -176,7 +176,7 @@ impl PickerDelegate for SlashCommandDelegate {
|
||||
ListItem::new(ix)
|
||||
.inset(true)
|
||||
.spacing(ListItemSpacing::Dense)
|
||||
.toggle_state(selected)
|
||||
.selected(selected)
|
||||
.tooltip({
|
||||
let description = info.description.clone();
|
||||
move |cx| cx.new_view(|_| Tooltip::new(description.clone())).into()
|
||||
@@ -217,10 +217,11 @@ impl PickerDelegate for SlashCommandDelegate {
|
||||
)),
|
||||
)
|
||||
.child(
|
||||
Label::new(info.description.clone())
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
.text_ellipsis(),
|
||||
div().overflow_hidden().text_ellipsis().child(
|
||||
Label::new(info.description.clone())
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -228,7 +229,7 @@ impl PickerDelegate for SlashCommandDelegate {
|
||||
ListItem::new(ix)
|
||||
.inset(true)
|
||||
.spacing(ListItemSpacing::Dense)
|
||||
.toggle_state(selected)
|
||||
.selected(selected)
|
||||
.child(renderer(cx)),
|
||||
),
|
||||
}
|
||||
@@ -316,8 +317,8 @@ impl<T: PopoverTrigger> RenderOnce for SlashCommandSelector<T> {
|
||||
PopoverMenu::new("model-switcher")
|
||||
.menu(move |_cx| Some(picker_view.clone()))
|
||||
.trigger(self.trigger)
|
||||
.attach(gpui::Corner::TopLeft)
|
||||
.anchor(gpui::Corner::BottomLeft)
|
||||
.attach(gpui::AnchorCorner::TopLeft)
|
||||
.anchor(gpui::AnchorCorner::BottomLeft)
|
||||
.offset(gpui::Point {
|
||||
x: px(0.0),
|
||||
y: px(-16.0),
|
||||
|
||||
@@ -20,7 +20,7 @@ use language::Buffer;
|
||||
use language_model::{
|
||||
LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, Role,
|
||||
};
|
||||
use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu};
|
||||
use language_model_selector::LanguageModelSelector;
|
||||
use language_models::report_assistant_event;
|
||||
use settings::{update_settings_file, Settings};
|
||||
use std::{
|
||||
@@ -476,9 +476,9 @@ enum PromptEditorEvent {
|
||||
|
||||
struct PromptEditor {
|
||||
id: TerminalInlineAssistId,
|
||||
fs: Arc<dyn Fs>,
|
||||
height_in_lines: u8,
|
||||
editor: View<Editor>,
|
||||
language_model_selector: View<LanguageModelSelector>,
|
||||
edited_since_done: bool,
|
||||
prompt_history: VecDeque<String>,
|
||||
prompt_history_ix: Option<usize>,
|
||||
@@ -614,8 +614,17 @@ impl Render for PromptEditor {
|
||||
.w_12()
|
||||
.justify_center()
|
||||
.gap_2()
|
||||
.child(LanguageModelSelectorPopoverMenu::new(
|
||||
self.language_model_selector.clone(),
|
||||
.child(LanguageModelSelector::new(
|
||||
{
|
||||
let fs = self.fs.clone();
|
||||
move |model, cx| {
|
||||
update_settings_file::<AssistantSettings>(
|
||||
fs.clone(),
|
||||
cx,
|
||||
move |settings, _| settings.set_model(model.clone()),
|
||||
);
|
||||
}
|
||||
},
|
||||
IconButton::new("context", IconName::SettingsAlt)
|
||||
.shape(IconButtonShape::Square)
|
||||
.icon_size(IconSize::Small)
|
||||
@@ -709,19 +718,6 @@ impl PromptEditor {
|
||||
id,
|
||||
height_in_lines: 1,
|
||||
editor: prompt_editor,
|
||||
language_model_selector: cx.new_view(|cx| {
|
||||
let fs = fs.clone();
|
||||
LanguageModelSelector::new(
|
||||
move |model, cx| {
|
||||
update_settings_file::<AssistantSettings>(
|
||||
fs.clone(),
|
||||
cx,
|
||||
move |settings, _| settings.set_model(model.clone()),
|
||||
);
|
||||
},
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
edited_since_done: false,
|
||||
prompt_history,
|
||||
prompt_history_ix: None,
|
||||
@@ -729,6 +725,7 @@ impl PromptEditor {
|
||||
_codegen_subscription: cx.observe(&codegen, Self::handle_codegen_changed),
|
||||
editor_subscriptions: Vec::new(),
|
||||
codegen,
|
||||
fs,
|
||||
pending_token_count: Task::ready(Ok(())),
|
||||
token_count: None,
|
||||
_token_count_subscriptions: token_count_subscriptions,
|
||||
|
||||
@@ -13,55 +13,30 @@ path = "src/assistant.rs"
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
anthropic = { workspace = true, features = ["schemars"] }
|
||||
anyhow.workspace = true
|
||||
assets.workspace = true
|
||||
assistant_tool.workspace = true
|
||||
async-watch.workspace = true
|
||||
client.workspace = true
|
||||
chrono.workspace = true
|
||||
client.workspace = true
|
||||
collections.workspace = true
|
||||
command_palette_hooks.workspace = true
|
||||
context_server.workspace = true
|
||||
db.workspace = true
|
||||
editor.workspace = true
|
||||
feature_flags.workspace = true
|
||||
fs.workspace = true
|
||||
futures.workspace = true
|
||||
fuzzy.workspace = true
|
||||
gpui.workspace = true
|
||||
handlebars.workspace = true
|
||||
html_to_markdown.workspace = true
|
||||
http_client.workspace = true
|
||||
language.workspace = true
|
||||
language_model.workspace = true
|
||||
language_model_selector.workspace = true
|
||||
language_models.workspace = true
|
||||
log.workspace = true
|
||||
lsp.workspace = true
|
||||
markdown.workspace = true
|
||||
menu.workspace = true
|
||||
multi_buffer.workspace = true
|
||||
ollama = { workspace = true, features = ["schemars"] }
|
||||
open_ai = { workspace = true, features = ["schemars"] }
|
||||
ordered-float.workspace = true
|
||||
paths.workspace = true
|
||||
parking_lot.workspace = true
|
||||
picker.workspace = true
|
||||
project.workspace = true
|
||||
proto.workspace = true
|
||||
rope.workspace = true
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
serde_json_lenient.workspace = true
|
||||
settings.workspace = true
|
||||
similar.workspace = true
|
||||
smol.workspace = true
|
||||
telemetry_events.workspace = true
|
||||
terminal_view.workspace = true
|
||||
text.workspace = true
|
||||
terminal.workspace = true
|
||||
theme.workspace = true
|
||||
time.workspace = true
|
||||
time_format.workspace = true
|
||||
@@ -70,8 +45,3 @@ unindent.workspace = true
|
||||
util.workspace = true
|
||||
uuid.workspace = true
|
||||
workspace.workspace = true
|
||||
zed_actions.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
rand.workspace = true
|
||||
indoc.workspace = true
|
||||
|
||||
@@ -213,33 +213,26 @@ impl ActiveThread {
|
||||
|
||||
div()
|
||||
.id(("message-container", ix))
|
||||
.py_1()
|
||||
.px_2()
|
||||
.p_2()
|
||||
.child(
|
||||
v_flex()
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.rounded_md()
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_between()
|
||||
.py_1()
|
||||
.px_2()
|
||||
.p_1p5()
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.child(
|
||||
Icon::new(role_icon)
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(Label::new(role_name).size(LabelSize::XSmall)),
|
||||
.gap_2()
|
||||
.child(Icon::new(role_icon).size(IconSize::Small))
|
||||
.child(Label::new(role_name).size(LabelSize::Small)),
|
||||
),
|
||||
)
|
||||
.child(v_flex().px_2().py_1().text_ui(cx).child(markdown.clone()))
|
||||
.child(v_flex().p_1p5().text_ui(cx).child(markdown.clone()))
|
||||
.when_some(context, |parent, context| {
|
||||
parent.child(
|
||||
h_flex().flex_wrap().gap_2().p_1p5().children(
|
||||
@@ -256,6 +249,6 @@ impl ActiveThread {
|
||||
|
||||
impl Render for ActiveThread {
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
list(self.list_state.clone()).flex_1().py_1()
|
||||
list(self.list_state.clone()).flex_1()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,84 +1,41 @@
|
||||
mod active_thread;
|
||||
mod assistant_panel;
|
||||
mod assistant_settings;
|
||||
mod context;
|
||||
mod context_picker;
|
||||
mod context_store;
|
||||
mod context_strip;
|
||||
mod inline_assistant;
|
||||
mod inline_prompt_editor;
|
||||
mod message_editor;
|
||||
mod prompts;
|
||||
mod streaming_diff;
|
||||
mod terminal_inline_assistant;
|
||||
mod thread;
|
||||
mod thread_history;
|
||||
mod thread_store;
|
||||
mod ui;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use client::Client;
|
||||
use command_palette_hooks::CommandPaletteFilter;
|
||||
use feature_flags::{Assistant2FeatureFlag, FeatureFlagAppExt};
|
||||
use fs::Fs;
|
||||
use gpui::{actions, AppContext};
|
||||
use prompts::PromptLoadingParams;
|
||||
use settings::Settings as _;
|
||||
use util::ResultExt;
|
||||
|
||||
pub use crate::assistant_panel::AssistantPanel;
|
||||
use crate::assistant_settings::AssistantSettings;
|
||||
pub use crate::inline_assistant::InlineAssistant;
|
||||
|
||||
actions!(
|
||||
assistant2,
|
||||
[
|
||||
ToggleFocus,
|
||||
NewThread,
|
||||
ToggleContextPicker,
|
||||
ToggleModelSelector,
|
||||
OpenHistory,
|
||||
Chat,
|
||||
CycleNextInlineAssist,
|
||||
CyclePreviousInlineAssist
|
||||
Chat
|
||||
]
|
||||
);
|
||||
|
||||
const NAMESPACE: &str = "assistant2";
|
||||
|
||||
/// Initializes the `assistant2` crate.
|
||||
pub fn init(fs: Arc<dyn Fs>, client: Arc<Client>, stdout_is_a_pty: bool, cx: &mut AppContext) {
|
||||
AssistantSettings::register(cx);
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
assistant_panel::init(cx);
|
||||
|
||||
let prompt_builder = prompts::PromptBuilder::new(Some(PromptLoadingParams {
|
||||
fs: fs.clone(),
|
||||
repo_path: stdout_is_a_pty
|
||||
.then(|| std::env::current_dir().log_err())
|
||||
.flatten(),
|
||||
cx,
|
||||
}))
|
||||
.log_err()
|
||||
.map(Arc::new)
|
||||
.unwrap_or_else(|| Arc::new(prompts::PromptBuilder::new(None).unwrap()));
|
||||
inline_assistant::init(
|
||||
fs.clone(),
|
||||
prompt_builder.clone(),
|
||||
client.telemetry().clone(),
|
||||
cx,
|
||||
);
|
||||
terminal_inline_assistant::init(
|
||||
fs.clone(),
|
||||
prompt_builder.clone(),
|
||||
client.telemetry().clone(),
|
||||
cx,
|
||||
);
|
||||
|
||||
feature_gate_assistant2_actions(cx);
|
||||
}
|
||||
|
||||
fn feature_gate_assistant2_actions(cx: &mut AppContext) {
|
||||
const ASSISTANT1_NAMESPACE: &str = "assistant";
|
||||
|
||||
CommandPaletteFilter::update_global(cx, |filter, _cx| {
|
||||
filter.hide_namespace(NAMESPACE);
|
||||
});
|
||||
@@ -87,10 +44,12 @@ fn feature_gate_assistant2_actions(cx: &mut AppContext) {
|
||||
if is_enabled {
|
||||
CommandPaletteFilter::update_global(cx, |filter, _cx| {
|
||||
filter.show_namespace(NAMESPACE);
|
||||
filter.hide_namespace(ASSISTANT1_NAMESPACE);
|
||||
});
|
||||
} else {
|
||||
CommandPaletteFilter::update_global(cx, |filter, _cx| {
|
||||
filter.hide_namespace(NAMESPACE);
|
||||
filter.show_namespace(ASSISTANT1_NAMESPACE);
|
||||
});
|
||||
}
|
||||
})
|
||||
|
||||
@@ -3,21 +3,18 @@ use std::sync::Arc;
|
||||
use anyhow::Result;
|
||||
use assistant_tool::ToolWorkingSet;
|
||||
use client::zed_urls;
|
||||
use fs::Fs;
|
||||
use gpui::{
|
||||
prelude::*, px, svg, Action, AnyElement, AppContext, AsyncWindowContext, EventEmitter,
|
||||
FocusHandle, FocusableView, FontWeight, Model, Pixels, Task, View, ViewContext, WeakView,
|
||||
WindowContext,
|
||||
};
|
||||
use language::LanguageRegistry;
|
||||
use settings::Settings;
|
||||
use time::UtcOffset;
|
||||
use ui::{prelude::*, KeyBinding, Tab, Tooltip};
|
||||
use ui::{prelude::*, Divider, IconButtonShape, KeyBinding, Tab, Tooltip};
|
||||
use workspace::dock::{DockPosition, Panel, PanelEvent};
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::active_thread::ActiveThread;
|
||||
use crate::assistant_settings::{AssistantDockPosition, AssistantSettings};
|
||||
use crate::message_editor::MessageEditor;
|
||||
use crate::thread::{ThreadError, ThreadId};
|
||||
use crate::thread_history::{PastThread, ThreadHistory};
|
||||
@@ -27,22 +24,9 @@ use crate::{NewThread, OpenHistory, ToggleFocus};
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
cx.observe_new_views(
|
||||
|workspace: &mut Workspace, _cx: &mut ViewContext<Workspace>| {
|
||||
workspace
|
||||
.register_action(|workspace, _: &ToggleFocus, cx| {
|
||||
workspace.toggle_panel_focus::<AssistantPanel>(cx);
|
||||
})
|
||||
.register_action(|workspace, _: &NewThread, cx| {
|
||||
if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
|
||||
panel.update(cx, |panel, cx| panel.new_thread(cx));
|
||||
workspace.focus_panel::<AssistantPanel>(cx);
|
||||
}
|
||||
})
|
||||
.register_action(|workspace, _: &OpenHistory, cx| {
|
||||
if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
|
||||
workspace.focus_panel::<AssistantPanel>(cx);
|
||||
panel.update(cx, |panel, cx| panel.open_history(cx));
|
||||
}
|
||||
});
|
||||
workspace.register_action(|workspace, _: &ToggleFocus, cx| {
|
||||
workspace.toggle_panel_focus::<AssistantPanel>(cx);
|
||||
});
|
||||
},
|
||||
)
|
||||
.detach();
|
||||
@@ -55,7 +39,6 @@ enum ActiveView {
|
||||
|
||||
pub struct AssistantPanel {
|
||||
workspace: WeakView<Workspace>,
|
||||
fs: Arc<dyn Fs>,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
thread_store: Model<ThreadStore>,
|
||||
thread: View<ActiveThread>,
|
||||
@@ -64,8 +47,6 @@ pub struct AssistantPanel {
|
||||
local_timezone: UtcOffset,
|
||||
active_view: ActiveView,
|
||||
history: View<ThreadHistory>,
|
||||
width: Option<Pixels>,
|
||||
height: Option<Pixels>,
|
||||
}
|
||||
|
||||
impl AssistantPanel {
|
||||
@@ -95,7 +76,6 @@ impl AssistantPanel {
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let thread = thread_store.update(cx, |this, cx| this.create_thread(cx));
|
||||
let fs = workspace.app_state().fs.clone();
|
||||
let language_registry = workspace.project().read(cx).languages().clone();
|
||||
let workspace = workspace.weak_handle();
|
||||
let weak_self = cx.view().downgrade();
|
||||
@@ -103,35 +83,24 @@ impl AssistantPanel {
|
||||
Self {
|
||||
active_view: ActiveView::Thread,
|
||||
workspace: workspace.clone(),
|
||||
fs: fs.clone(),
|
||||
language_registry: language_registry.clone(),
|
||||
thread_store: thread_store.clone(),
|
||||
thread: cx.new_view(|cx| {
|
||||
ActiveThread::new(
|
||||
thread.clone(),
|
||||
workspace.clone(),
|
||||
workspace,
|
||||
language_registry,
|
||||
tools.clone(),
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
message_editor: cx.new_view(|cx| {
|
||||
MessageEditor::new(
|
||||
fs.clone(),
|
||||
workspace,
|
||||
thread_store.downgrade(),
|
||||
thread.clone(),
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
message_editor: cx.new_view(|cx| MessageEditor::new(thread.clone(), cx)),
|
||||
tools,
|
||||
local_timezone: UtcOffset::from_whole_seconds(
|
||||
chrono::Local::now().offset().local_minus_utc(),
|
||||
)
|
||||
.unwrap(),
|
||||
history: cx.new_view(|cx| ThreadHistory::new(weak_self, thread_store, cx)),
|
||||
width: None,
|
||||
height: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,10 +108,6 @@ impl AssistantPanel {
|
||||
self.local_timezone
|
||||
}
|
||||
|
||||
pub(crate) fn thread_store(&self) -> &Model<ThreadStore> {
|
||||
&self.thread_store
|
||||
}
|
||||
|
||||
fn new_thread(&mut self, cx: &mut ViewContext<Self>) {
|
||||
let thread = self
|
||||
.thread_store
|
||||
@@ -158,24 +123,10 @@ impl AssistantPanel {
|
||||
cx,
|
||||
)
|
||||
});
|
||||
self.message_editor = cx.new_view(|cx| {
|
||||
MessageEditor::new(
|
||||
self.fs.clone(),
|
||||
self.workspace.clone(),
|
||||
self.thread_store.downgrade(),
|
||||
thread,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
self.message_editor = cx.new_view(|cx| MessageEditor::new(thread, cx));
|
||||
self.message_editor.focus_handle(cx).focus(cx);
|
||||
}
|
||||
|
||||
fn open_history(&mut self, cx: &mut ViewContext<Self>) {
|
||||
self.active_view = ActiveView::History;
|
||||
self.history.focus_handle(cx).focus(cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub(crate) fn open_thread(&mut self, thread_id: &ThreadId, cx: &mut ViewContext<Self>) {
|
||||
let Some(thread) = self
|
||||
.thread_store
|
||||
@@ -194,15 +145,7 @@ impl AssistantPanel {
|
||||
cx,
|
||||
)
|
||||
});
|
||||
self.message_editor = cx.new_view(|cx| {
|
||||
MessageEditor::new(
|
||||
self.fs.clone(),
|
||||
self.workspace.clone(),
|
||||
self.thread_store.downgrade(),
|
||||
thread,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
self.message_editor = cx.new_view(|cx| MessageEditor::new(thread, cx));
|
||||
self.message_editor.focus_handle(cx).focus(cx);
|
||||
}
|
||||
|
||||
@@ -236,38 +179,13 @@ impl Panel for AssistantPanel {
|
||||
true
|
||||
}
|
||||
|
||||
fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
|
||||
settings::update_settings_file::<AssistantSettings>(
|
||||
self.fs.clone(),
|
||||
cx,
|
||||
move |settings, _| {
|
||||
let dock = match position {
|
||||
DockPosition::Left => AssistantDockPosition::Left,
|
||||
DockPosition::Bottom => AssistantDockPosition::Bottom,
|
||||
DockPosition::Right => AssistantDockPosition::Right,
|
||||
};
|
||||
settings.set_dock(dock);
|
||||
},
|
||||
);
|
||||
fn set_position(&mut self, _position: DockPosition, _cx: &mut ViewContext<Self>) {}
|
||||
|
||||
fn size(&self, _cx: &WindowContext) -> Pixels {
|
||||
px(640.)
|
||||
}
|
||||
|
||||
fn size(&self, cx: &WindowContext) -> Pixels {
|
||||
let settings = AssistantSettings::get_global(cx);
|
||||
match self.position(cx) {
|
||||
DockPosition::Left | DockPosition::Right => {
|
||||
self.width.unwrap_or(settings.default_width)
|
||||
}
|
||||
DockPosition::Bottom => self.height.unwrap_or(settings.default_height),
|
||||
}
|
||||
}
|
||||
|
||||
fn set_size(&mut self, size: Option<Pixels>, cx: &mut ViewContext<Self>) {
|
||||
match self.position(cx) {
|
||||
DockPosition::Left | DockPosition::Right => self.width = size,
|
||||
DockPosition::Bottom => self.height = size,
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
fn set_size(&mut self, _size: Option<Pixels>, _cx: &mut ViewContext<Self>) {}
|
||||
|
||||
fn set_active(&mut self, _active: bool, _cx: &mut ViewContext<Self>) {}
|
||||
|
||||
@@ -276,7 +194,7 @@ impl Panel for AssistantPanel {
|
||||
}
|
||||
|
||||
fn icon(&self, _cx: &WindowContext) -> Option<IconName> {
|
||||
Some(IconName::ZedAssistant2)
|
||||
Some(IconName::ZedAssistant)
|
||||
}
|
||||
|
||||
fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
|
||||
@@ -300,17 +218,15 @@ impl AssistantPanel {
|
||||
.px(DynamicSpacing::Base08.rems(cx))
|
||||
.bg(cx.theme().colors().tab_bar_background)
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.child(h_flex().children(self.thread.read(cx).summary(cx).map(Label::new)))
|
||||
.child(
|
||||
h_flex()
|
||||
.h_full()
|
||||
.pl_1()
|
||||
.border_l_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.gap(DynamicSpacing::Base02.rems(cx))
|
||||
.gap(DynamicSpacing::Base08.rems(cx))
|
||||
.child(Divider::vertical())
|
||||
.child(
|
||||
IconButton::new("new-thread", IconName::Plus)
|
||||
.shape(IconButtonShape::Square)
|
||||
.icon_size(IconSize::Small)
|
||||
.style(ButtonStyle::Subtle)
|
||||
.tooltip({
|
||||
@@ -330,6 +246,7 @@ impl AssistantPanel {
|
||||
)
|
||||
.child(
|
||||
IconButton::new("open-history", IconName::HistoryRerun)
|
||||
.shape(IconButtonShape::Square)
|
||||
.icon_size(IconSize::Small)
|
||||
.style(ButtonStyle::Subtle)
|
||||
.tooltip({
|
||||
@@ -349,6 +266,7 @@ impl AssistantPanel {
|
||||
)
|
||||
.child(
|
||||
IconButton::new("configure-assistant", IconName::Settings)
|
||||
.shape(IconButtonShape::Square)
|
||||
.icon_size(IconSize::Small)
|
||||
.style(ButtonStyle::Subtle)
|
||||
.tooltip(move |cx| Tooltip::text("Configure Assistant", cx))
|
||||
@@ -374,6 +292,7 @@ impl AssistantPanel {
|
||||
|
||||
v_flex()
|
||||
.gap_2()
|
||||
.mx_auto()
|
||||
.child(
|
||||
v_flex().w_full().child(
|
||||
svg()
|
||||
@@ -388,14 +307,13 @@ impl AssistantPanel {
|
||||
.when(!recent_threads.is_empty(), |parent| {
|
||||
parent
|
||||
.child(
|
||||
h_flex().w_full().justify_center().child(
|
||||
Label::new("Recent Threads:")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
h_flex()
|
||||
.w_full()
|
||||
.justify_center()
|
||||
.child(Label::new("Recent Threads:").size(LabelSize::Small)),
|
||||
)
|
||||
.child(
|
||||
v_flex().mx_auto().w_4_5().gap_2().children(
|
||||
v_flex().gap_2().children(
|
||||
recent_threads
|
||||
.into_iter()
|
||||
.map(|thread| PastThread::new(thread, cx.view().downgrade())),
|
||||
@@ -591,7 +509,9 @@ impl Render for AssistantPanel {
|
||||
this.new_thread(cx);
|
||||
}))
|
||||
.on_action(cx.listener(|this, _: &OpenHistory, cx| {
|
||||
this.open_history(cx);
|
||||
this.active_view = ActiveView::History;
|
||||
this.history.focus_handle(cx).focus(cx);
|
||||
cx.notify();
|
||||
}))
|
||||
.child(self.render_toolbar(cx))
|
||||
.map(|parent| match self.active_view {
|
||||
@@ -600,7 +520,7 @@ impl Render for AssistantPanel {
|
||||
.child(
|
||||
h_flex()
|
||||
.border_t_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.child(self.message_editor.clone()),
|
||||
)
|
||||
.children(self.render_last_error(cx)),
|
||||
|
||||
@@ -1,501 +0,0 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use ::open_ai::Model as OpenAiModel;
|
||||
use anthropic::Model as AnthropicModel;
|
||||
use gpui::Pixels;
|
||||
use language_model::{CloudModel, LanguageModel};
|
||||
use ollama::Model as OllamaModel;
|
||||
use schemars::{schema::Schema, JsonSchema};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsSources};
|
||||
|
||||
#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum AssistantDockPosition {
|
||||
Left,
|
||||
#[default]
|
||||
Right,
|
||||
Bottom,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
#[serde(tag = "name", rename_all = "snake_case")]
|
||||
pub enum AssistantProviderContentV1 {
|
||||
#[serde(rename = "zed.dev")]
|
||||
ZedDotDev { default_model: Option<CloudModel> },
|
||||
#[serde(rename = "openai")]
|
||||
OpenAi {
|
||||
default_model: Option<OpenAiModel>,
|
||||
api_url: Option<String>,
|
||||
available_models: Option<Vec<OpenAiModel>>,
|
||||
},
|
||||
#[serde(rename = "anthropic")]
|
||||
Anthropic {
|
||||
default_model: Option<AnthropicModel>,
|
||||
api_url: Option<String>,
|
||||
},
|
||||
#[serde(rename = "ollama")]
|
||||
Ollama {
|
||||
default_model: Option<OllamaModel>,
|
||||
api_url: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct AssistantSettings {
|
||||
pub enabled: bool,
|
||||
pub button: bool,
|
||||
pub dock: AssistantDockPosition,
|
||||
pub default_width: Pixels,
|
||||
pub default_height: Pixels,
|
||||
pub default_model: LanguageModelSelection,
|
||||
pub inline_alternatives: Vec<LanguageModelSelection>,
|
||||
pub using_outdated_settings_version: bool,
|
||||
pub enable_experimental_live_diffs: bool,
|
||||
}
|
||||
|
||||
/// Assistant panel settings
|
||||
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||
#[serde(untagged)]
|
||||
pub enum AssistantSettingsContent {
|
||||
Versioned(VersionedAssistantSettingsContent),
|
||||
Legacy(LegacyAssistantSettingsContent),
|
||||
}
|
||||
|
||||
impl JsonSchema for AssistantSettingsContent {
|
||||
fn schema_name() -> String {
|
||||
VersionedAssistantSettingsContent::schema_name()
|
||||
}
|
||||
|
||||
fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> Schema {
|
||||
VersionedAssistantSettingsContent::json_schema(gen)
|
||||
}
|
||||
|
||||
fn is_referenceable() -> bool {
|
||||
VersionedAssistantSettingsContent::is_referenceable()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for AssistantSettingsContent {
|
||||
fn default() -> Self {
|
||||
Self::Versioned(VersionedAssistantSettingsContent::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl AssistantSettingsContent {
|
||||
pub fn is_version_outdated(&self) -> bool {
|
||||
match self {
|
||||
AssistantSettingsContent::Versioned(settings) => match settings {
|
||||
VersionedAssistantSettingsContent::V1(_) => true,
|
||||
VersionedAssistantSettingsContent::V2(_) => false,
|
||||
},
|
||||
AssistantSettingsContent::Legacy(_) => true,
|
||||
}
|
||||
}
|
||||
|
||||
fn upgrade(&self) -> AssistantSettingsContentV2 {
|
||||
match self {
|
||||
AssistantSettingsContent::Versioned(settings) => match settings {
|
||||
VersionedAssistantSettingsContent::V1(settings) => AssistantSettingsContentV2 {
|
||||
enabled: settings.enabled,
|
||||
button: settings.button,
|
||||
dock: settings.dock,
|
||||
default_width: settings.default_width,
|
||||
default_height: settings.default_width,
|
||||
default_model: settings
|
||||
.provider
|
||||
.clone()
|
||||
.and_then(|provider| match provider {
|
||||
AssistantProviderContentV1::ZedDotDev { default_model } => {
|
||||
default_model.map(|model| LanguageModelSelection {
|
||||
provider: "zed.dev".to_string(),
|
||||
model: model.id().to_string(),
|
||||
})
|
||||
}
|
||||
AssistantProviderContentV1::OpenAi { default_model, .. } => {
|
||||
default_model.map(|model| LanguageModelSelection {
|
||||
provider: "openai".to_string(),
|
||||
model: model.id().to_string(),
|
||||
})
|
||||
}
|
||||
AssistantProviderContentV1::Anthropic { default_model, .. } => {
|
||||
default_model.map(|model| LanguageModelSelection {
|
||||
provider: "anthropic".to_string(),
|
||||
model: model.id().to_string(),
|
||||
})
|
||||
}
|
||||
AssistantProviderContentV1::Ollama { default_model, .. } => {
|
||||
default_model.map(|model| LanguageModelSelection {
|
||||
provider: "ollama".to_string(),
|
||||
model: model.id().to_string(),
|
||||
})
|
||||
}
|
||||
}),
|
||||
inline_alternatives: None,
|
||||
enable_experimental_live_diffs: None,
|
||||
},
|
||||
VersionedAssistantSettingsContent::V2(settings) => settings.clone(),
|
||||
},
|
||||
AssistantSettingsContent::Legacy(settings) => AssistantSettingsContentV2 {
|
||||
enabled: None,
|
||||
button: settings.button,
|
||||
dock: settings.dock,
|
||||
default_width: settings.default_width,
|
||||
default_height: settings.default_height,
|
||||
default_model: Some(LanguageModelSelection {
|
||||
provider: "openai".to_string(),
|
||||
model: settings
|
||||
.default_open_ai_model
|
||||
.clone()
|
||||
.unwrap_or_default()
|
||||
.id()
|
||||
.to_string(),
|
||||
}),
|
||||
inline_alternatives: None,
|
||||
enable_experimental_live_diffs: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_dock(&mut self, dock: AssistantDockPosition) {
|
||||
match self {
|
||||
AssistantSettingsContent::Versioned(settings) => match settings {
|
||||
VersionedAssistantSettingsContent::V1(settings) => {
|
||||
settings.dock = Some(dock);
|
||||
}
|
||||
VersionedAssistantSettingsContent::V2(settings) => {
|
||||
settings.dock = Some(dock);
|
||||
}
|
||||
},
|
||||
AssistantSettingsContent::Legacy(settings) => {
|
||||
settings.dock = Some(dock);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_model(&mut self, language_model: Arc<dyn LanguageModel>) {
|
||||
let model = language_model.id().0.to_string();
|
||||
let provider = language_model.provider_id().0.to_string();
|
||||
|
||||
match self {
|
||||
AssistantSettingsContent::Versioned(settings) => match settings {
|
||||
VersionedAssistantSettingsContent::V1(settings) => match provider.as_ref() {
|
||||
"zed.dev" => {
|
||||
log::warn!("attempted to set zed.dev model on outdated settings");
|
||||
}
|
||||
"anthropic" => {
|
||||
let api_url = match &settings.provider {
|
||||
Some(AssistantProviderContentV1::Anthropic { api_url, .. }) => {
|
||||
api_url.clone()
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
settings.provider = Some(AssistantProviderContentV1::Anthropic {
|
||||
default_model: AnthropicModel::from_id(&model).ok(),
|
||||
api_url,
|
||||
});
|
||||
}
|
||||
"ollama" => {
|
||||
let api_url = match &settings.provider {
|
||||
Some(AssistantProviderContentV1::Ollama { api_url, .. }) => {
|
||||
api_url.clone()
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
settings.provider = Some(AssistantProviderContentV1::Ollama {
|
||||
default_model: Some(ollama::Model::new(&model, None, None)),
|
||||
api_url,
|
||||
});
|
||||
}
|
||||
"openai" => {
|
||||
let (api_url, available_models) = match &settings.provider {
|
||||
Some(AssistantProviderContentV1::OpenAi {
|
||||
api_url,
|
||||
available_models,
|
||||
..
|
||||
}) => (api_url.clone(), available_models.clone()),
|
||||
_ => (None, None),
|
||||
};
|
||||
settings.provider = Some(AssistantProviderContentV1::OpenAi {
|
||||
default_model: OpenAiModel::from_id(&model).ok(),
|
||||
api_url,
|
||||
available_models,
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
VersionedAssistantSettingsContent::V2(settings) => {
|
||||
settings.default_model = Some(LanguageModelSelection { provider, model });
|
||||
}
|
||||
},
|
||||
AssistantSettingsContent::Legacy(settings) => {
|
||||
if let Ok(model) = OpenAiModel::from_id(&language_model.id().0) {
|
||||
settings.default_open_ai_model = Some(model);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
|
||||
#[serde(tag = "version")]
|
||||
pub enum VersionedAssistantSettingsContent {
|
||||
#[serde(rename = "1")]
|
||||
V1(AssistantSettingsContentV1),
|
||||
#[serde(rename = "2")]
|
||||
V2(AssistantSettingsContentV2),
|
||||
}
|
||||
|
||||
impl Default for VersionedAssistantSettingsContent {
|
||||
fn default() -> Self {
|
||||
Self::V2(AssistantSettingsContentV2 {
|
||||
enabled: None,
|
||||
button: None,
|
||||
dock: None,
|
||||
default_width: None,
|
||||
default_height: None,
|
||||
default_model: None,
|
||||
inline_alternatives: None,
|
||||
enable_experimental_live_diffs: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
|
||||
pub struct AssistantSettingsContentV2 {
|
||||
/// Whether the Assistant is enabled.
|
||||
///
|
||||
/// Default: true
|
||||
enabled: Option<bool>,
|
||||
/// Whether to show the assistant panel button in the status bar.
|
||||
///
|
||||
/// Default: true
|
||||
button: Option<bool>,
|
||||
/// Where to dock the assistant.
|
||||
///
|
||||
/// Default: right
|
||||
dock: Option<AssistantDockPosition>,
|
||||
/// Default width in pixels when the assistant is docked to the left or right.
|
||||
///
|
||||
/// Default: 640
|
||||
default_width: Option<f32>,
|
||||
/// Default height in pixels when the assistant is docked to the bottom.
|
||||
///
|
||||
/// Default: 320
|
||||
default_height: Option<f32>,
|
||||
/// The default model to use when creating new chats.
|
||||
default_model: Option<LanguageModelSelection>,
|
||||
/// Additional models with which to generate alternatives when performing inline assists.
|
||||
inline_alternatives: Option<Vec<LanguageModelSelection>>,
|
||||
/// Enable experimental live diffs in the assistant panel.
|
||||
///
|
||||
/// Default: false
|
||||
enable_experimental_live_diffs: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
pub struct LanguageModelSelection {
|
||||
#[schemars(schema_with = "providers_schema")]
|
||||
pub provider: String,
|
||||
pub model: String,
|
||||
}
|
||||
|
||||
fn providers_schema(_: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
|
||||
schemars::schema::SchemaObject {
|
||||
enum_values: Some(vec![
|
||||
"anthropic".into(),
|
||||
"google".into(),
|
||||
"ollama".into(),
|
||||
"openai".into(),
|
||||
"zed.dev".into(),
|
||||
"copilot_chat".into(),
|
||||
]),
|
||||
..Default::default()
|
||||
}
|
||||
.into()
|
||||
}
|
||||
|
||||
impl Default for LanguageModelSelection {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
provider: "openai".to_string(),
|
||||
model: "gpt-4".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
|
||||
pub struct AssistantSettingsContentV1 {
|
||||
/// Whether the Assistant is enabled.
|
||||
///
|
||||
/// Default: true
|
||||
enabled: Option<bool>,
|
||||
/// Whether to show the assistant panel button in the status bar.
|
||||
///
|
||||
/// Default: true
|
||||
button: Option<bool>,
|
||||
/// Where to dock the assistant.
|
||||
///
|
||||
/// Default: right
|
||||
dock: Option<AssistantDockPosition>,
|
||||
/// Default width in pixels when the assistant is docked to the left or right.
|
||||
///
|
||||
/// Default: 640
|
||||
default_width: Option<f32>,
|
||||
/// Default height in pixels when the assistant is docked to the bottom.
|
||||
///
|
||||
/// Default: 320
|
||||
default_height: Option<f32>,
|
||||
/// The provider of the assistant service.
|
||||
///
|
||||
/// This can be "openai", "anthropic", "ollama", "zed.dev"
|
||||
/// each with their respective default models and configurations.
|
||||
provider: Option<AssistantProviderContentV1>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
|
||||
pub struct LegacyAssistantSettingsContent {
|
||||
/// Whether to show the assistant panel button in the status bar.
|
||||
///
|
||||
/// Default: true
|
||||
pub button: Option<bool>,
|
||||
/// Where to dock the assistant.
|
||||
///
|
||||
/// Default: right
|
||||
pub dock: Option<AssistantDockPosition>,
|
||||
/// Default width in pixels when the assistant is docked to the left or right.
|
||||
///
|
||||
/// Default: 640
|
||||
pub default_width: Option<f32>,
|
||||
/// Default height in pixels when the assistant is docked to the bottom.
|
||||
///
|
||||
/// Default: 320
|
||||
pub default_height: Option<f32>,
|
||||
/// The default OpenAI model to use when creating new chats.
|
||||
///
|
||||
/// Default: gpt-4-1106-preview
|
||||
pub default_open_ai_model: Option<OpenAiModel>,
|
||||
/// OpenAI API base URL to use when creating new chats.
|
||||
///
|
||||
/// Default: https://api.openai.com/v1
|
||||
pub openai_api_url: Option<String>,
|
||||
}
|
||||
|
||||
impl Settings for AssistantSettings {
|
||||
const KEY: Option<&'static str> = Some("assistant");
|
||||
|
||||
const PRESERVED_KEYS: Option<&'static [&'static str]> = Some(&["version"]);
|
||||
|
||||
type FileContent = AssistantSettingsContent;
|
||||
|
||||
fn load(
|
||||
sources: SettingsSources<Self::FileContent>,
|
||||
_: &mut gpui::AppContext,
|
||||
) -> anyhow::Result<Self> {
|
||||
let mut settings = AssistantSettings::default();
|
||||
|
||||
for value in sources.defaults_and_customizations() {
|
||||
if value.is_version_outdated() {
|
||||
settings.using_outdated_settings_version = true;
|
||||
}
|
||||
|
||||
let value = value.upgrade();
|
||||
merge(&mut settings.enabled, value.enabled);
|
||||
merge(&mut settings.button, value.button);
|
||||
merge(&mut settings.dock, value.dock);
|
||||
merge(
|
||||
&mut settings.default_width,
|
||||
value.default_width.map(Into::into),
|
||||
);
|
||||
merge(
|
||||
&mut settings.default_height,
|
||||
value.default_height.map(Into::into),
|
||||
);
|
||||
merge(&mut settings.default_model, value.default_model);
|
||||
merge(&mut settings.inline_alternatives, value.inline_alternatives);
|
||||
merge(
|
||||
&mut settings.enable_experimental_live_diffs,
|
||||
value.enable_experimental_live_diffs,
|
||||
);
|
||||
}
|
||||
|
||||
Ok(settings)
|
||||
}
|
||||
}
|
||||
|
||||
fn merge<T>(target: &mut T, value: Option<T>) {
|
||||
if let Some(value) = value {
|
||||
*target = value;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use fs::Fs;
|
||||
use gpui::{ReadGlobal, TestAppContext};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_deserialize_assistant_settings_with_version(cx: &mut TestAppContext) {
|
||||
let fs = fs::FakeFs::new(cx.executor().clone());
|
||||
fs.create_dir(paths::settings_file().parent().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
cx.update(|cx| {
|
||||
let test_settings = settings::SettingsStore::test(cx);
|
||||
cx.set_global(test_settings);
|
||||
AssistantSettings::register(cx);
|
||||
});
|
||||
|
||||
cx.update(|cx| {
|
||||
assert!(!AssistantSettings::get_global(cx).using_outdated_settings_version);
|
||||
assert_eq!(
|
||||
AssistantSettings::get_global(cx).default_model,
|
||||
LanguageModelSelection {
|
||||
provider: "zed.dev".into(),
|
||||
model: "claude-3-5-sonnet".into(),
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
cx.update(|cx| {
|
||||
settings::SettingsStore::global(cx).update_settings_file::<AssistantSettings>(
|
||||
fs.clone(),
|
||||
|settings, _| {
|
||||
*settings = AssistantSettingsContent::Versioned(
|
||||
VersionedAssistantSettingsContent::V2(AssistantSettingsContentV2 {
|
||||
default_model: Some(LanguageModelSelection {
|
||||
provider: "test-provider".into(),
|
||||
model: "gpt-99".into(),
|
||||
}),
|
||||
inline_alternatives: None,
|
||||
enabled: None,
|
||||
button: None,
|
||||
dock: None,
|
||||
default_width: None,
|
||||
default_height: None,
|
||||
enable_experimental_live_diffs: None,
|
||||
}),
|
||||
)
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
let raw_settings_value = fs.load(paths::settings_file()).await.unwrap();
|
||||
assert!(raw_settings_value.contains(r#""version": "2""#));
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct AssistantSettingsTest {
|
||||
assistant: AssistantSettingsContent,
|
||||
}
|
||||
|
||||
let assistant_settings: AssistantSettingsTest =
|
||||
serde_json_lenient::from_str(&raw_settings_value).unwrap();
|
||||
|
||||
assert!(!assistant_settings.assistant.is_version_outdated());
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,8 @@
|
||||
use gpui::SharedString;
|
||||
use language_model::{LanguageModelRequestMessage, MessageContent};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use util::post_inc;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Serialize, Deserialize)]
|
||||
pub struct ContextId(pub(crate) usize);
|
||||
|
||||
impl ContextId {
|
||||
pub fn post_inc(&mut self) -> Self {
|
||||
Self(post_inc(&mut self.0))
|
||||
}
|
||||
}
|
||||
|
||||
/// Some context attached to a message in a thread.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Context {
|
||||
pub id: ContextId,
|
||||
pub name: SharedString,
|
||||
pub kind: ContextKind,
|
||||
pub text: SharedString,
|
||||
@@ -24,67 +11,4 @@ pub struct Context {
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub enum ContextKind {
|
||||
File,
|
||||
Directory,
|
||||
FetchedUrl,
|
||||
Thread,
|
||||
}
|
||||
|
||||
pub fn attach_context_to_message(
|
||||
message: &mut LanguageModelRequestMessage,
|
||||
context: impl IntoIterator<Item = Context>,
|
||||
) {
|
||||
let mut file_context = String::new();
|
||||
let mut directory_context = String::new();
|
||||
let mut fetch_context = String::new();
|
||||
let mut thread_context = String::new();
|
||||
|
||||
for context in context.into_iter() {
|
||||
match context.kind {
|
||||
ContextKind::File => {
|
||||
file_context.push_str(&context.text);
|
||||
file_context.push('\n');
|
||||
}
|
||||
ContextKind::Directory => {
|
||||
directory_context.push_str(&context.text);
|
||||
directory_context.push('\n');
|
||||
}
|
||||
ContextKind::FetchedUrl => {
|
||||
fetch_context.push_str(&context.name);
|
||||
fetch_context.push('\n');
|
||||
fetch_context.push_str(&context.text);
|
||||
fetch_context.push('\n');
|
||||
}
|
||||
ContextKind::Thread => {
|
||||
thread_context.push_str(&context.name);
|
||||
thread_context.push('\n');
|
||||
thread_context.push_str(&context.text);
|
||||
thread_context.push('\n');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut context_text = String::new();
|
||||
if !file_context.is_empty() {
|
||||
context_text.push_str("The following files are available:\n");
|
||||
context_text.push_str(&file_context);
|
||||
}
|
||||
|
||||
if !directory_context.is_empty() {
|
||||
context_text.push_str("The following directories are available:\n");
|
||||
context_text.push_str(&directory_context);
|
||||
}
|
||||
|
||||
if !fetch_context.is_empty() {
|
||||
context_text.push_str("The following fetched results are available\n");
|
||||
context_text.push_str(&fetch_context);
|
||||
}
|
||||
|
||||
if !thread_context.is_empty() {
|
||||
context_text.push_str("The following previous conversation threads are available\n");
|
||||
context_text.push_str(&thread_context);
|
||||
}
|
||||
|
||||
if !context_text.is_empty() {
|
||||
message.content.push(MessageContent::Text(context_text));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,150 +1,45 @@
|
||||
mod directory_context_picker;
|
||||
mod fetch_context_picker;
|
||||
mod file_context_picker;
|
||||
mod thread_context_picker;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use gpui::{
|
||||
AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, SharedString, Task, View,
|
||||
WeakModel, WeakView,
|
||||
};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use ui::{prelude::*, ListItem, ListItemSpacing};
|
||||
use util::ResultExt;
|
||||
use workspace::Workspace;
|
||||
use gpui::{DismissEvent, SharedString, Task, WeakView};
|
||||
use picker::{Picker, PickerDelegate, PickerEditorPosition};
|
||||
use ui::{prelude::*, ListItem, ListItemSpacing, PopoverMenu, PopoverTrigger, Tooltip};
|
||||
|
||||
use crate::context::ContextKind;
|
||||
use crate::context_picker::directory_context_picker::DirectoryContextPicker;
|
||||
use crate::context_picker::fetch_context_picker::FetchContextPicker;
|
||||
use crate::context_picker::file_context_picker::FileContextPicker;
|
||||
use crate::context_picker::thread_context_picker::ThreadContextPicker;
|
||||
use crate::context_store::ContextStore;
|
||||
use crate::thread_store::ThreadStore;
|
||||
use crate::message_editor::MessageEditor;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum ContextPickerMode {
|
||||
Default,
|
||||
File(View<FileContextPicker>),
|
||||
Directory(View<DirectoryContextPicker>),
|
||||
Fetch(View<FetchContextPicker>),
|
||||
Thread(View<ThreadContextPicker>),
|
||||
}
|
||||
|
||||
pub(super) struct ContextPicker {
|
||||
mode: ContextPickerMode,
|
||||
picker: View<Picker<ContextPickerDelegate>>,
|
||||
}
|
||||
|
||||
impl ContextPicker {
|
||||
pub fn new(
|
||||
workspace: WeakView<Workspace>,
|
||||
thread_store: Option<WeakModel<ThreadStore>>,
|
||||
context_store: WeakModel<ContextStore>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let mut entries = vec![
|
||||
ContextPickerEntry {
|
||||
name: "File".into(),
|
||||
kind: ContextKind::File,
|
||||
icon: IconName::File,
|
||||
},
|
||||
ContextPickerEntry {
|
||||
name: "Folder".into(),
|
||||
kind: ContextKind::Directory,
|
||||
icon: IconName::Folder,
|
||||
},
|
||||
ContextPickerEntry {
|
||||
name: "Fetch".into(),
|
||||
kind: ContextKind::FetchedUrl,
|
||||
icon: IconName::Globe,
|
||||
},
|
||||
];
|
||||
|
||||
if thread_store.is_some() {
|
||||
entries.push(ContextPickerEntry {
|
||||
name: "Thread".into(),
|
||||
kind: ContextKind::Thread,
|
||||
icon: IconName::MessageCircle,
|
||||
});
|
||||
}
|
||||
|
||||
let delegate = ContextPickerDelegate {
|
||||
context_picker: cx.view().downgrade(),
|
||||
workspace,
|
||||
thread_store,
|
||||
context_store,
|
||||
entries,
|
||||
selected_ix: 0,
|
||||
};
|
||||
|
||||
let picker = cx.new_view(|cx| {
|
||||
Picker::nonsearchable_uniform_list(delegate, cx).max_height(Some(rems(20.).into()))
|
||||
});
|
||||
|
||||
ContextPicker {
|
||||
mode: ContextPickerMode::Default,
|
||||
picker,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reset_mode(&mut self) {
|
||||
self.mode = ContextPickerMode::Default;
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<DismissEvent> for ContextPicker {}
|
||||
|
||||
impl FocusableView for ContextPicker {
|
||||
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
|
||||
match &self.mode {
|
||||
ContextPickerMode::Default => self.picker.focus_handle(cx),
|
||||
ContextPickerMode::File(file_picker) => file_picker.focus_handle(cx),
|
||||
ContextPickerMode::Directory(directory_picker) => directory_picker.focus_handle(cx),
|
||||
ContextPickerMode::Fetch(fetch_picker) => fetch_picker.focus_handle(cx),
|
||||
ContextPickerMode::Thread(thread_picker) => thread_picker.focus_handle(cx),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ContextPicker {
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
v_flex()
|
||||
.w(px(400.))
|
||||
.min_w(px(400.))
|
||||
.map(|parent| match &self.mode {
|
||||
ContextPickerMode::Default => parent.child(self.picker.clone()),
|
||||
ContextPickerMode::File(file_picker) => parent.child(file_picker.clone()),
|
||||
ContextPickerMode::Directory(directory_picker) => {
|
||||
parent.child(directory_picker.clone())
|
||||
}
|
||||
ContextPickerMode::Fetch(fetch_picker) => parent.child(fetch_picker.clone()),
|
||||
ContextPickerMode::Thread(thread_picker) => parent.child(thread_picker.clone()),
|
||||
})
|
||||
}
|
||||
#[derive(IntoElement)]
|
||||
pub(super) struct ContextPicker<T: PopoverTrigger> {
|
||||
message_editor: WeakView<MessageEditor>,
|
||||
trigger: T,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct ContextPickerEntry {
|
||||
name: SharedString,
|
||||
kind: ContextKind,
|
||||
description: SharedString,
|
||||
icon: IconName,
|
||||
}
|
||||
|
||||
pub(crate) struct ContextPickerDelegate {
|
||||
context_picker: WeakView<ContextPicker>,
|
||||
workspace: WeakView<Workspace>,
|
||||
thread_store: Option<WeakModel<ThreadStore>>,
|
||||
context_store: WeakModel<ContextStore>,
|
||||
entries: Vec<ContextPickerEntry>,
|
||||
all_entries: Vec<ContextPickerEntry>,
|
||||
filtered_entries: Vec<ContextPickerEntry>,
|
||||
message_editor: WeakView<MessageEditor>,
|
||||
selected_ix: usize,
|
||||
}
|
||||
|
||||
impl<T: PopoverTrigger> ContextPicker<T> {
|
||||
pub(crate) fn new(message_editor: WeakView<MessageEditor>, trigger: T) -> Self {
|
||||
ContextPicker {
|
||||
message_editor,
|
||||
trigger,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PickerDelegate for ContextPickerDelegate {
|
||||
type ListItem = ListItem;
|
||||
|
||||
fn match_count(&self) -> usize {
|
||||
self.entries.len()
|
||||
self.filtered_entries.len()
|
||||
}
|
||||
|
||||
fn selected_index(&self) -> usize {
|
||||
@@ -152,7 +47,7 @@ impl PickerDelegate for ContextPickerDelegate {
|
||||
}
|
||||
|
||||
fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Picker<Self>>) {
|
||||
self.selected_ix = ix.min(self.entries.len().saturating_sub(1));
|
||||
self.selected_ix = ix.min(self.filtered_entries.len().saturating_sub(1));
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
@@ -160,75 +55,52 @@ impl PickerDelegate for ContextPickerDelegate {
|
||||
"Select a context source…".into()
|
||||
}
|
||||
|
||||
fn update_matches(&mut self, _query: String, _cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
|
||||
Task::ready(())
|
||||
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
|
||||
let all_commands = self.all_entries.clone();
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let filtered_commands = cx
|
||||
.background_executor()
|
||||
.spawn(async move {
|
||||
if query.is_empty() {
|
||||
all_commands
|
||||
} else {
|
||||
all_commands
|
||||
.into_iter()
|
||||
.filter(|model_info| {
|
||||
model_info
|
||||
.name
|
||||
.to_lowercase()
|
||||
.contains(&query.to_lowercase())
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.delegate.filtered_entries = filtered_commands;
|
||||
this.delegate.set_selected_index(0, cx);
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
|
||||
if let Some(entry) = self.entries.get(self.selected_ix) {
|
||||
self.context_picker
|
||||
.update(cx, |this, cx| {
|
||||
match entry.kind {
|
||||
ContextKind::File => {
|
||||
this.mode = ContextPickerMode::File(cx.new_view(|cx| {
|
||||
FileContextPicker::new(
|
||||
self.context_picker.clone(),
|
||||
self.workspace.clone(),
|
||||
self.context_store.clone(),
|
||||
cx,
|
||||
)
|
||||
}));
|
||||
}
|
||||
ContextKind::Directory => {
|
||||
this.mode = ContextPickerMode::Directory(cx.new_view(|cx| {
|
||||
DirectoryContextPicker::new(
|
||||
self.context_picker.clone(),
|
||||
self.workspace.clone(),
|
||||
self.context_store.clone(),
|
||||
cx,
|
||||
)
|
||||
}));
|
||||
}
|
||||
ContextKind::FetchedUrl => {
|
||||
this.mode = ContextPickerMode::Fetch(cx.new_view(|cx| {
|
||||
FetchContextPicker::new(
|
||||
self.context_picker.clone(),
|
||||
self.workspace.clone(),
|
||||
self.context_store.clone(),
|
||||
cx,
|
||||
)
|
||||
}));
|
||||
}
|
||||
ContextKind::Thread => {
|
||||
if let Some(thread_store) = self.thread_store.as_ref() {
|
||||
this.mode = ContextPickerMode::Thread(cx.new_view(|cx| {
|
||||
ThreadContextPicker::new(
|
||||
thread_store.clone(),
|
||||
self.context_picker.clone(),
|
||||
self.context_store.clone(),
|
||||
cx,
|
||||
)
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cx.focus_self();
|
||||
if let Some(entry) = self.filtered_entries.get(self.selected_ix) {
|
||||
self.message_editor
|
||||
.update(cx, |_message_editor, _cx| {
|
||||
println!("Insert context from {}", entry.name);
|
||||
})
|
||||
.log_err();
|
||||
.ok();
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
}
|
||||
|
||||
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
|
||||
self.context_picker
|
||||
.update(cx, |this, cx| match this.mode {
|
||||
ContextPickerMode::Default => cx.emit(DismissEvent),
|
||||
ContextPickerMode::File(_)
|
||||
| ContextPickerMode::Directory(_)
|
||||
| ContextPickerMode::Fetch(_)
|
||||
| ContextPickerMode::Thread(_) => {}
|
||||
})
|
||||
.log_err();
|
||||
fn dismissed(&mut self, _cx: &mut ViewContext<Picker<Self>>) {}
|
||||
|
||||
fn editor_position(&self) -> PickerEditorPosition {
|
||||
PickerEditorPosition::End
|
||||
}
|
||||
|
||||
fn render_match(
|
||||
@@ -237,21 +109,89 @@ impl PickerDelegate for ContextPickerDelegate {
|
||||
selected: bool,
|
||||
_cx: &mut ViewContext<Picker<Self>>,
|
||||
) -> Option<Self::ListItem> {
|
||||
let entry = &self.entries[ix];
|
||||
let entry = self.filtered_entries.get(ix)?;
|
||||
|
||||
Some(
|
||||
ListItem::new(ix)
|
||||
.inset(true)
|
||||
.spacing(ListItemSpacing::Dense)
|
||||
.toggle_state(selected)
|
||||
.selected(selected)
|
||||
.tooltip({
|
||||
let description = entry.description.clone();
|
||||
move |cx| cx.new_view(|_cx| Tooltip::new(description.clone())).into()
|
||||
})
|
||||
.child(
|
||||
h_flex()
|
||||
v_flex()
|
||||
.group(format!("context-entry-label-{ix}"))
|
||||
.w_full()
|
||||
.py_0p5()
|
||||
.min_w(px(250.))
|
||||
.max_w(px(400.))
|
||||
.gap_2()
|
||||
.child(Icon::new(entry.icon).size(IconSize::Small))
|
||||
.child(Label::new(entry.name.clone()).single_line()),
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.child(Icon::new(entry.icon).size(IconSize::XSmall))
|
||||
.child(
|
||||
Label::new(entry.name.clone())
|
||||
.single_line()
|
||||
.size(LabelSize::Small),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div().overflow_hidden().text_ellipsis().child(
|
||||
Label::new(entry.description.clone())
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: PopoverTrigger> RenderOnce for ContextPicker<T> {
|
||||
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
|
||||
let entries = vec![
|
||||
ContextPickerEntry {
|
||||
name: "directory".into(),
|
||||
description: "Insert any directory".into(),
|
||||
icon: IconName::Folder,
|
||||
},
|
||||
ContextPickerEntry {
|
||||
name: "file".into(),
|
||||
description: "Insert any file".into(),
|
||||
icon: IconName::File,
|
||||
},
|
||||
ContextPickerEntry {
|
||||
name: "web".into(),
|
||||
description: "Fetch content from URL".into(),
|
||||
icon: IconName::Globe,
|
||||
},
|
||||
];
|
||||
|
||||
let delegate = ContextPickerDelegate {
|
||||
all_entries: entries.clone(),
|
||||
message_editor: self.message_editor.clone(),
|
||||
filtered_entries: entries,
|
||||
selected_ix: 0,
|
||||
};
|
||||
|
||||
let picker =
|
||||
cx.new_view(|cx| Picker::uniform_list(delegate, cx).max_height(Some(rems(20.).into())));
|
||||
|
||||
let handle = self
|
||||
.message_editor
|
||||
.update(cx, |this, _| this.context_picker_handle.clone())
|
||||
.ok();
|
||||
PopoverMenu::new("context-picker")
|
||||
.menu(move |_cx| Some(picker.clone()))
|
||||
.trigger(self.trigger)
|
||||
.attach(gpui::AnchorCorner::TopLeft)
|
||||
.anchor(gpui::AnchorCorner::BottomLeft)
|
||||
.offset(gpui::Point {
|
||||
x: px(0.0),
|
||||
y: px(-16.0),
|
||||
})
|
||||
.when_some(handle, |this, handle| this.with_handle(handle))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,117 +0,0 @@
|
||||
// TODO: Remove this once we've implemented the functionality.
|
||||
#![allow(unused)]
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use fuzzy::PathMatch;
|
||||
use gpui::{AppContext, DismissEvent, FocusHandle, FocusableView, Task, View, WeakModel, WeakView};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use project::{PathMatchCandidateSet, WorktreeId};
|
||||
use ui::{prelude::*, ListItem};
|
||||
use util::ResultExt as _;
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::context_picker::ContextPicker;
|
||||
use crate::context_store::ContextStore;
|
||||
|
||||
pub struct DirectoryContextPicker {
|
||||
picker: View<Picker<DirectoryContextPickerDelegate>>,
|
||||
}
|
||||
|
||||
impl DirectoryContextPicker {
|
||||
pub fn new(
|
||||
context_picker: WeakView<ContextPicker>,
|
||||
workspace: WeakView<Workspace>,
|
||||
context_store: WeakModel<ContextStore>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let delegate =
|
||||
DirectoryContextPickerDelegate::new(context_picker, workspace, context_store);
|
||||
let picker = cx.new_view(|cx| Picker::uniform_list(delegate, cx));
|
||||
|
||||
Self { picker }
|
||||
}
|
||||
}
|
||||
|
||||
impl FocusableView for DirectoryContextPicker {
|
||||
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
|
||||
self.picker.focus_handle(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for DirectoryContextPicker {
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
self.picker.clone()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DirectoryContextPickerDelegate {
|
||||
context_picker: WeakView<ContextPicker>,
|
||||
workspace: WeakView<Workspace>,
|
||||
context_store: WeakModel<ContextStore>,
|
||||
matches: Vec<PathMatch>,
|
||||
selected_index: usize,
|
||||
}
|
||||
|
||||
impl DirectoryContextPickerDelegate {
|
||||
pub fn new(
|
||||
context_picker: WeakView<ContextPicker>,
|
||||
workspace: WeakView<Workspace>,
|
||||
context_store: WeakModel<ContextStore>,
|
||||
) -> Self {
|
||||
Self {
|
||||
context_picker,
|
||||
workspace,
|
||||
context_store,
|
||||
matches: Vec::new(),
|
||||
selected_index: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PickerDelegate for DirectoryContextPickerDelegate {
|
||||
type ListItem = ListItem;
|
||||
|
||||
fn match_count(&self) -> usize {
|
||||
self.matches.len()
|
||||
}
|
||||
|
||||
fn selected_index(&self) -> usize {
|
||||
self.selected_index
|
||||
}
|
||||
|
||||
fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Picker<Self>>) {
|
||||
self.selected_index = ix;
|
||||
}
|
||||
|
||||
fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
|
||||
"Search folders…".into()
|
||||
}
|
||||
|
||||
fn update_matches(&mut self, _query: String, _cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
|
||||
// TODO: Implement this once we fix the issues with the file context picker.
|
||||
Task::ready(())
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _secondary: bool, _cx: &mut ViewContext<Picker<Self>>) {
|
||||
// TODO: Implement this once we fix the issues with the file context picker.
|
||||
}
|
||||
|
||||
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
|
||||
self.context_picker
|
||||
.update(cx, |this, cx| {
|
||||
this.reset_mode();
|
||||
cx.emit(DismissEvent);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
fn render_match(
|
||||
&self,
|
||||
_ix: usize,
|
||||
_selected: bool,
|
||||
_cx: &mut ViewContext<Picker<Self>>,
|
||||
) -> Option<Self::ListItem> {
|
||||
None
|
||||
}
|
||||
}
|
||||
@@ -1,225 +0,0 @@
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{bail, Context as _, Result};
|
||||
use futures::AsyncReadExt as _;
|
||||
use gpui::{AppContext, DismissEvent, FocusHandle, FocusableView, Task, View, WeakModel, WeakView};
|
||||
use html_to_markdown::{convert_html_to_markdown, markdown, TagHandler};
|
||||
use http_client::{AsyncBody, HttpClientWithUrl};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use ui::{prelude::*, ListItem, ViewContext};
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::context::ContextKind;
|
||||
use crate::context_picker::ContextPicker;
|
||||
use crate::context_store::ContextStore;
|
||||
|
||||
pub struct FetchContextPicker {
|
||||
picker: View<Picker<FetchContextPickerDelegate>>,
|
||||
}
|
||||
|
||||
impl FetchContextPicker {
|
||||
pub fn new(
|
||||
context_picker: WeakView<ContextPicker>,
|
||||
workspace: WeakView<Workspace>,
|
||||
context_store: WeakModel<ContextStore>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let delegate = FetchContextPickerDelegate::new(context_picker, workspace, context_store);
|
||||
let picker = cx.new_view(|cx| Picker::uniform_list(delegate, cx));
|
||||
|
||||
Self { picker }
|
||||
}
|
||||
}
|
||||
|
||||
impl FocusableView for FetchContextPicker {
|
||||
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
|
||||
self.picker.focus_handle(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for FetchContextPicker {
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
self.picker.clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
|
||||
enum ContentType {
|
||||
Html,
|
||||
Plaintext,
|
||||
Json,
|
||||
}
|
||||
|
||||
pub struct FetchContextPickerDelegate {
|
||||
context_picker: WeakView<ContextPicker>,
|
||||
workspace: WeakView<Workspace>,
|
||||
context_store: WeakModel<ContextStore>,
|
||||
url: String,
|
||||
}
|
||||
|
||||
impl FetchContextPickerDelegate {
|
||||
pub fn new(
|
||||
context_picker: WeakView<ContextPicker>,
|
||||
workspace: WeakView<Workspace>,
|
||||
context_store: WeakModel<ContextStore>,
|
||||
) -> Self {
|
||||
FetchContextPickerDelegate {
|
||||
context_picker,
|
||||
workspace,
|
||||
context_store,
|
||||
url: String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn build_message(http_client: Arc<HttpClientWithUrl>, url: &str) -> Result<String> {
|
||||
let mut url = url.to_owned();
|
||||
if !url.starts_with("https://") && !url.starts_with("http://") {
|
||||
url = format!("https://{url}");
|
||||
}
|
||||
|
||||
let mut response = http_client.get(&url, AsyncBody::default(), true).await?;
|
||||
|
||||
let mut body = Vec::new();
|
||||
response
|
||||
.body_mut()
|
||||
.read_to_end(&mut body)
|
||||
.await
|
||||
.context("error reading response body")?;
|
||||
|
||||
if response.status().is_client_error() {
|
||||
let text = String::from_utf8_lossy(body.as_slice());
|
||||
bail!(
|
||||
"status error {}, response: {text:?}",
|
||||
response.status().as_u16()
|
||||
);
|
||||
}
|
||||
|
||||
let Some(content_type) = response.headers().get("content-type") else {
|
||||
bail!("missing Content-Type header");
|
||||
};
|
||||
let content_type = content_type
|
||||
.to_str()
|
||||
.context("invalid Content-Type header")?;
|
||||
let content_type = match content_type {
|
||||
"text/html" => ContentType::Html,
|
||||
"text/plain" => ContentType::Plaintext,
|
||||
"application/json" => ContentType::Json,
|
||||
_ => ContentType::Html,
|
||||
};
|
||||
|
||||
match content_type {
|
||||
ContentType::Html => {
|
||||
let mut handlers: Vec<TagHandler> = vec![
|
||||
Rc::new(RefCell::new(markdown::WebpageChromeRemover)),
|
||||
Rc::new(RefCell::new(markdown::ParagraphHandler)),
|
||||
Rc::new(RefCell::new(markdown::HeadingHandler)),
|
||||
Rc::new(RefCell::new(markdown::ListHandler)),
|
||||
Rc::new(RefCell::new(markdown::TableHandler::new())),
|
||||
Rc::new(RefCell::new(markdown::StyledTextHandler)),
|
||||
];
|
||||
if url.contains("wikipedia.org") {
|
||||
use html_to_markdown::structure::wikipedia;
|
||||
|
||||
handlers.push(Rc::new(RefCell::new(wikipedia::WikipediaChromeRemover)));
|
||||
handlers.push(Rc::new(RefCell::new(wikipedia::WikipediaInfoboxHandler)));
|
||||
handlers.push(Rc::new(
|
||||
RefCell::new(wikipedia::WikipediaCodeHandler::new()),
|
||||
));
|
||||
} else {
|
||||
handlers.push(Rc::new(RefCell::new(markdown::CodeHandler)));
|
||||
}
|
||||
|
||||
convert_html_to_markdown(&body[..], &mut handlers)
|
||||
}
|
||||
ContentType::Plaintext => Ok(std::str::from_utf8(&body)?.to_owned()),
|
||||
ContentType::Json => {
|
||||
let json: serde_json::Value = serde_json::from_slice(&body)?;
|
||||
|
||||
Ok(format!(
|
||||
"```json\n{}\n```",
|
||||
serde_json::to_string_pretty(&json)?
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PickerDelegate for FetchContextPickerDelegate {
|
||||
type ListItem = ListItem;
|
||||
|
||||
fn match_count(&self) -> usize {
|
||||
if self.url.is_empty() {
|
||||
0
|
||||
} else {
|
||||
1
|
||||
}
|
||||
}
|
||||
|
||||
fn no_matches_text(&self, _cx: &mut WindowContext) -> SharedString {
|
||||
"Enter the URL that you would like to fetch".into()
|
||||
}
|
||||
|
||||
fn selected_index(&self) -> usize {
|
||||
0
|
||||
}
|
||||
|
||||
fn set_selected_index(&mut self, _ix: usize, _cx: &mut ViewContext<Picker<Self>>) {}
|
||||
|
||||
fn placeholder_text(&self, _cx: &mut ui::WindowContext) -> Arc<str> {
|
||||
"Enter a URL…".into()
|
||||
}
|
||||
|
||||
fn update_matches(&mut self, query: String, _cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
|
||||
self.url = query;
|
||||
|
||||
Task::ready(())
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
|
||||
let Some(workspace) = self.workspace.upgrade() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let http_client = workspace.read(cx).client().http_client().clone();
|
||||
let url = self.url.clone();
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let text = Self::build_message(http_client, &url).await?;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.delegate
|
||||
.context_store
|
||||
.update(cx, |context_store, _cx| {
|
||||
context_store.insert_context(ContextKind::FetchedUrl, url, text);
|
||||
})
|
||||
})??;
|
||||
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
|
||||
self.context_picker
|
||||
.update(cx, |this, cx| {
|
||||
this.reset_mode();
|
||||
cx.emit(DismissEvent);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
fn render_match(
|
||||
&self,
|
||||
ix: usize,
|
||||
selected: bool,
|
||||
_cx: &mut ViewContext<Picker<Self>>,
|
||||
) -> Option<Self::ListItem> {
|
||||
Some(
|
||||
ListItem::new(ix)
|
||||
.inset(true)
|
||||
.toggle_state(selected)
|
||||
.child(Label::new(self.url.clone())),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,309 +0,0 @@
|
||||
use std::fmt::Write as _;
|
||||
use std::ops::RangeInclusive;
|
||||
use std::path::Path;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::Arc;
|
||||
|
||||
use fuzzy::PathMatch;
|
||||
use gpui::{AppContext, DismissEvent, FocusHandle, FocusableView, Task, View, WeakModel, WeakView};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use project::{PathMatchCandidateSet, WorktreeId};
|
||||
use ui::{prelude::*, ListItem};
|
||||
use util::ResultExt as _;
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::context::ContextKind;
|
||||
use crate::context_picker::ContextPicker;
|
||||
use crate::context_store::ContextStore;
|
||||
|
||||
pub struct FileContextPicker {
|
||||
picker: View<Picker<FileContextPickerDelegate>>,
|
||||
}
|
||||
|
||||
impl FileContextPicker {
|
||||
pub fn new(
|
||||
context_picker: WeakView<ContextPicker>,
|
||||
workspace: WeakView<Workspace>,
|
||||
context_store: WeakModel<ContextStore>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let delegate = FileContextPickerDelegate::new(context_picker, workspace, context_store);
|
||||
let picker = cx.new_view(|cx| Picker::uniform_list(delegate, cx));
|
||||
|
||||
Self { picker }
|
||||
}
|
||||
}
|
||||
|
||||
impl FocusableView for FileContextPicker {
|
||||
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
|
||||
self.picker.focus_handle(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for FileContextPicker {
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
self.picker.clone()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct FileContextPickerDelegate {
|
||||
context_picker: WeakView<ContextPicker>,
|
||||
workspace: WeakView<Workspace>,
|
||||
context_store: WeakModel<ContextStore>,
|
||||
matches: Vec<PathMatch>,
|
||||
selected_index: usize,
|
||||
}
|
||||
|
||||
impl FileContextPickerDelegate {
|
||||
pub fn new(
|
||||
context_picker: WeakView<ContextPicker>,
|
||||
workspace: WeakView<Workspace>,
|
||||
context_store: WeakModel<ContextStore>,
|
||||
) -> Self {
|
||||
Self {
|
||||
context_picker,
|
||||
workspace,
|
||||
context_store,
|
||||
matches: Vec::new(),
|
||||
selected_index: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn search(
|
||||
&mut self,
|
||||
query: String,
|
||||
cancellation_flag: Arc<AtomicBool>,
|
||||
workspace: &View<Workspace>,
|
||||
cx: &mut ViewContext<Picker<Self>>,
|
||||
) -> Task<Vec<PathMatch>> {
|
||||
if query.is_empty() {
|
||||
let workspace = workspace.read(cx);
|
||||
let project = workspace.project().read(cx);
|
||||
let recent_matches = workspace
|
||||
.recent_navigation_history(Some(10), cx)
|
||||
.into_iter()
|
||||
.filter_map(|(project_path, _)| {
|
||||
let worktree = project.worktree_for_id(project_path.worktree_id, cx)?;
|
||||
Some(PathMatch {
|
||||
score: 0.,
|
||||
positions: Vec::new(),
|
||||
worktree_id: project_path.worktree_id.to_usize(),
|
||||
path: project_path.path,
|
||||
path_prefix: worktree.read(cx).root_name().into(),
|
||||
distance_to_relative_ancestor: 0,
|
||||
is_dir: false,
|
||||
})
|
||||
});
|
||||
|
||||
let file_matches = project.worktrees(cx).flat_map(|worktree| {
|
||||
let worktree = worktree.read(cx);
|
||||
let path_prefix: Arc<str> = worktree.root_name().into();
|
||||
worktree.files(true, 0).map(move |entry| PathMatch {
|
||||
score: 0.,
|
||||
positions: Vec::new(),
|
||||
worktree_id: worktree.id().to_usize(),
|
||||
path: entry.path.clone(),
|
||||
path_prefix: path_prefix.clone(),
|
||||
distance_to_relative_ancestor: 0,
|
||||
is_dir: false,
|
||||
})
|
||||
});
|
||||
|
||||
Task::ready(recent_matches.chain(file_matches).collect())
|
||||
} else {
|
||||
let worktrees = workspace.read(cx).visible_worktrees(cx).collect::<Vec<_>>();
|
||||
let candidate_sets = worktrees
|
||||
.into_iter()
|
||||
.map(|worktree| {
|
||||
let worktree = worktree.read(cx);
|
||||
|
||||
PathMatchCandidateSet {
|
||||
snapshot: worktree.snapshot(),
|
||||
include_ignored: worktree
|
||||
.root_entry()
|
||||
.map_or(false, |entry| entry.is_ignored),
|
||||
include_root_name: true,
|
||||
candidates: project::Candidates::Files,
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let executor = cx.background_executor().clone();
|
||||
cx.foreground_executor().spawn(async move {
|
||||
fuzzy::match_path_sets(
|
||||
candidate_sets.as_slice(),
|
||||
query.as_str(),
|
||||
None,
|
||||
false,
|
||||
100,
|
||||
&cancellation_flag,
|
||||
executor,
|
||||
)
|
||||
.await
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PickerDelegate for FileContextPickerDelegate {
|
||||
type ListItem = ListItem;
|
||||
|
||||
fn match_count(&self) -> usize {
|
||||
self.matches.len()
|
||||
}
|
||||
|
||||
fn selected_index(&self) -> usize {
|
||||
self.selected_index
|
||||
}
|
||||
|
||||
fn set_selected_index(&mut self, ix: usize, _cx: &mut ViewContext<Picker<Self>>) {
|
||||
self.selected_index = ix;
|
||||
}
|
||||
|
||||
fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
|
||||
"Search files…".into()
|
||||
}
|
||||
|
||||
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
|
||||
let Some(workspace) = self.workspace.upgrade() else {
|
||||
return Task::ready(());
|
||||
};
|
||||
|
||||
let search_task = self.search(query, Arc::<AtomicBool>::default(), &workspace, cx);
|
||||
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
// TODO: This should be probably be run in the background.
|
||||
let paths = search_task.await;
|
||||
|
||||
this.update(&mut cx, |this, _cx| {
|
||||
this.delegate.matches = paths;
|
||||
})
|
||||
.log_err();
|
||||
})
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
|
||||
let mat = &self.matches[self.selected_index];
|
||||
|
||||
let workspace = self.workspace.clone();
|
||||
let Some(project) = workspace
|
||||
.upgrade()
|
||||
.map(|workspace| workspace.read(cx).project().clone())
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let path = mat.path.clone();
|
||||
let worktree_id = WorktreeId::from_usize(mat.worktree_id);
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let Some(open_buffer_task) = project
|
||||
.update(&mut cx, |project, cx| {
|
||||
project.open_buffer((worktree_id, path.clone()), cx)
|
||||
})
|
||||
.ok()
|
||||
else {
|
||||
return anyhow::Ok(());
|
||||
};
|
||||
|
||||
let buffer = open_buffer_task.await?;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.delegate.context_store.update(cx, |context_store, cx| {
|
||||
let mut text = String::new();
|
||||
text.push_str(&codeblock_fence_for_path(Some(&path), None));
|
||||
text.push_str(&buffer.read(cx).text());
|
||||
if !text.ends_with('\n') {
|
||||
text.push('\n');
|
||||
}
|
||||
|
||||
text.push_str("```\n");
|
||||
|
||||
context_store.insert_context(
|
||||
ContextKind::File,
|
||||
path.to_string_lossy().to_string(),
|
||||
text,
|
||||
);
|
||||
})
|
||||
})??;
|
||||
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
|
||||
self.context_picker
|
||||
.update(cx, |this, cx| {
|
||||
this.reset_mode();
|
||||
cx.emit(DismissEvent);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
fn render_match(
|
||||
&self,
|
||||
ix: usize,
|
||||
selected: bool,
|
||||
_cx: &mut ViewContext<Picker<Self>>,
|
||||
) -> Option<Self::ListItem> {
|
||||
let path_match = &self.matches[ix];
|
||||
|
||||
let (file_name, directory) = if path_match.path.as_ref() == Path::new("") {
|
||||
(SharedString::from(path_match.path_prefix.clone()), None)
|
||||
} else {
|
||||
let file_name = path_match
|
||||
.path
|
||||
.file_name()
|
||||
.unwrap_or_default()
|
||||
.to_string_lossy()
|
||||
.to_string()
|
||||
.into();
|
||||
|
||||
let mut directory = format!("{}/", path_match.path_prefix);
|
||||
if let Some(parent) = path_match
|
||||
.path
|
||||
.parent()
|
||||
.filter(|parent| parent != &Path::new(""))
|
||||
{
|
||||
directory.push_str(&parent.to_string_lossy());
|
||||
directory.push('/');
|
||||
}
|
||||
|
||||
(file_name, Some(directory))
|
||||
};
|
||||
|
||||
Some(
|
||||
ListItem::new(ix).inset(true).toggle_state(selected).child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(Label::new(file_name))
|
||||
.children(directory.map(|directory| {
|
||||
Label::new(directory)
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
})),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn codeblock_fence_for_path(path: Option<&Path>, row_range: Option<RangeInclusive<u32>>) -> String {
|
||||
let mut text = String::new();
|
||||
write!(text, "```").unwrap();
|
||||
|
||||
if let Some(path) = path {
|
||||
if let Some(extension) = path.extension().and_then(|ext| ext.to_str()) {
|
||||
write!(text, "{} ", extension).unwrap();
|
||||
}
|
||||
|
||||
write!(text, "{}", path.display()).unwrap();
|
||||
} else {
|
||||
write!(text, "untitled").unwrap();
|
||||
}
|
||||
|
||||
if let Some(row_range) = row_range {
|
||||
write!(text, ":{}-{}", row_range.start() + 1, row_range.end() + 1).unwrap();
|
||||
}
|
||||
|
||||
text.push('\n');
|
||||
text
|
||||
}
|
||||
@@ -1,209 +0,0 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use fuzzy::StringMatchCandidate;
|
||||
use gpui::{AppContext, DismissEvent, FocusHandle, FocusableView, Task, View, WeakModel, WeakView};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use ui::{prelude::*, ListItem};
|
||||
|
||||
use crate::context::ContextKind;
|
||||
use crate::context_picker::ContextPicker;
|
||||
use crate::context_store;
|
||||
use crate::thread::ThreadId;
|
||||
use crate::thread_store::ThreadStore;
|
||||
|
||||
pub struct ThreadContextPicker {
|
||||
picker: View<Picker<ThreadContextPickerDelegate>>,
|
||||
}
|
||||
|
||||
impl ThreadContextPicker {
|
||||
pub fn new(
|
||||
thread_store: WeakModel<ThreadStore>,
|
||||
context_picker: WeakView<ContextPicker>,
|
||||
context_store: WeakModel<context_store::ContextStore>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let delegate =
|
||||
ThreadContextPickerDelegate::new(thread_store, context_picker, context_store);
|
||||
let picker = cx.new_view(|cx| Picker::uniform_list(delegate, cx));
|
||||
|
||||
ThreadContextPicker { picker }
|
||||
}
|
||||
}
|
||||
|
||||
impl FocusableView for ThreadContextPicker {
|
||||
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
|
||||
self.picker.focus_handle(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ThreadContextPicker {
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
self.picker.clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct ThreadContextEntry {
|
||||
id: ThreadId,
|
||||
summary: SharedString,
|
||||
}
|
||||
|
||||
pub struct ThreadContextPickerDelegate {
|
||||
thread_store: WeakModel<ThreadStore>,
|
||||
context_picker: WeakView<ContextPicker>,
|
||||
context_store: WeakModel<context_store::ContextStore>,
|
||||
matches: Vec<ThreadContextEntry>,
|
||||
selected_index: usize,
|
||||
}
|
||||
|
||||
impl ThreadContextPickerDelegate {
|
||||
pub fn new(
|
||||
thread_store: WeakModel<ThreadStore>,
|
||||
context_picker: WeakView<ContextPicker>,
|
||||
context_store: WeakModel<context_store::ContextStore>,
|
||||
) -> Self {
|
||||
ThreadContextPickerDelegate {
|
||||
thread_store,
|
||||
context_picker,
|
||||
context_store,
|
||||
matches: Vec::new(),
|
||||
selected_index: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PickerDelegate for ThreadContextPickerDelegate {
|
||||
type ListItem = ListItem;
|
||||
|
||||
fn match_count(&self) -> usize {
|
||||
self.matches.len()
|
||||
}
|
||||
|
||||
fn selected_index(&self) -> usize {
|
||||
self.selected_index
|
||||
}
|
||||
|
||||
fn set_selected_index(&mut self, ix: usize, _cx: &mut ViewContext<Picker<Self>>) {
|
||||
self.selected_index = ix;
|
||||
}
|
||||
|
||||
fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
|
||||
"Search threads…".into()
|
||||
}
|
||||
|
||||
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
|
||||
let Ok(threads) = self.thread_store.update(cx, |this, cx| {
|
||||
this.threads(cx)
|
||||
.into_iter()
|
||||
.map(|thread| {
|
||||
const DEFAULT_SUMMARY: SharedString = SharedString::new_static("New Thread");
|
||||
|
||||
let id = thread.read(cx).id().clone();
|
||||
let summary = thread.read(cx).summary().unwrap_or(DEFAULT_SUMMARY);
|
||||
ThreadContextEntry { id, summary }
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
}) else {
|
||||
return Task::ready(());
|
||||
};
|
||||
|
||||
let executor = cx.background_executor().clone();
|
||||
let search_task = cx.background_executor().spawn(async move {
|
||||
if query.is_empty() {
|
||||
threads
|
||||
} else {
|
||||
let candidates = threads
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(id, thread)| StringMatchCandidate::new(id, &thread.summary))
|
||||
.collect::<Vec<_>>();
|
||||
let matches = fuzzy::match_strings(
|
||||
&candidates,
|
||||
&query,
|
||||
false,
|
||||
100,
|
||||
&Default::default(),
|
||||
executor,
|
||||
)
|
||||
.await;
|
||||
|
||||
matches
|
||||
.into_iter()
|
||||
.map(|mat| threads[mat.candidate_id].clone())
|
||||
.collect()
|
||||
}
|
||||
});
|
||||
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let matches = search_task.await;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.delegate.matches = matches;
|
||||
this.delegate.selected_index = 0;
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
|
||||
let entry = &self.matches[self.selected_index];
|
||||
|
||||
let Some(thread_store) = self.thread_store.upgrade() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(thread) = thread_store.update(cx, |this, cx| this.open_thread(&entry.id, cx))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
self.context_store
|
||||
.update(cx, |context_store, cx| {
|
||||
let text = thread.update(cx, |thread, _cx| {
|
||||
let mut text = String::new();
|
||||
|
||||
for message in thread.messages() {
|
||||
text.push_str(match message.role {
|
||||
language_model::Role::User => "User:",
|
||||
language_model::Role::Assistant => "Assistant:",
|
||||
language_model::Role::System => "System:",
|
||||
});
|
||||
text.push('\n');
|
||||
|
||||
text.push_str(&message.text);
|
||||
text.push('\n');
|
||||
}
|
||||
|
||||
text
|
||||
});
|
||||
|
||||
context_store.insert_context(ContextKind::Thread, entry.summary.clone(), text);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
|
||||
self.context_picker
|
||||
.update(cx, |this, cx| {
|
||||
this.reset_mode();
|
||||
cx.emit(DismissEvent);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
fn render_match(
|
||||
&self,
|
||||
ix: usize,
|
||||
selected: bool,
|
||||
_cx: &mut ViewContext<Picker<Self>>,
|
||||
) -> Option<Self::ListItem> {
|
||||
let thread = &self.matches[ix];
|
||||
|
||||
Some(
|
||||
ListItem::new(ix)
|
||||
.inset(true)
|
||||
.toggle_state(selected)
|
||||
.child(thread.summary.clone()),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
use gpui::SharedString;
|
||||
|
||||
use crate::context::{Context, ContextId, ContextKind};
|
||||
|
||||
pub struct ContextStore {
|
||||
context: Vec<Context>,
|
||||
next_context_id: ContextId,
|
||||
}
|
||||
|
||||
impl ContextStore {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
context: Vec::new(),
|
||||
next_context_id: ContextId(0),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn context(&self) -> &Vec<Context> {
|
||||
&self.context
|
||||
}
|
||||
|
||||
pub fn drain(&mut self) -> Vec<Context> {
|
||||
self.context.drain(..).collect()
|
||||
}
|
||||
|
||||
pub fn clear(&mut self) {
|
||||
self.context.clear();
|
||||
}
|
||||
|
||||
pub fn insert_context(
|
||||
&mut self,
|
||||
kind: ContextKind,
|
||||
name: impl Into<SharedString>,
|
||||
text: impl Into<SharedString>,
|
||||
) {
|
||||
self.context.push(Context {
|
||||
id: self.next_context_id.post_inc(),
|
||||
name: name.into(),
|
||||
kind,
|
||||
text: text.into(),
|
||||
});
|
||||
}
|
||||
|
||||
pub fn remove_context(&mut self, id: &ContextId) {
|
||||
self.context.retain(|context| context.id != *id);
|
||||
}
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use gpui::{FocusHandle, Model, View, WeakModel, WeakView};
|
||||
use ui::{prelude::*, PopoverMenu, PopoverMenuHandle, Tooltip};
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::context_picker::ContextPicker;
|
||||
use crate::context_store::ContextStore;
|
||||
use crate::thread_store::ThreadStore;
|
||||
use crate::ui::ContextPill;
|
||||
use crate::ToggleContextPicker;
|
||||
|
||||
pub struct ContextStrip {
|
||||
context_store: Model<ContextStore>,
|
||||
context_picker: View<ContextPicker>,
|
||||
context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
|
||||
focus_handle: FocusHandle,
|
||||
}
|
||||
|
||||
impl ContextStrip {
|
||||
pub fn new(
|
||||
context_store: Model<ContextStore>,
|
||||
workspace: WeakView<Workspace>,
|
||||
thread_store: Option<WeakModel<ThreadStore>>,
|
||||
focus_handle: FocusHandle,
|
||||
context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
Self {
|
||||
context_store: context_store.clone(),
|
||||
context_picker: cx.new_view(|cx| {
|
||||
ContextPicker::new(
|
||||
workspace.clone(),
|
||||
thread_store.clone(),
|
||||
context_store.downgrade(),
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
context_picker_menu_handle,
|
||||
focus_handle,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ContextStrip {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let context = self.context_store.read(cx).context();
|
||||
let context_picker = self.context_picker.clone();
|
||||
let focus_handle = self.focus_handle.clone();
|
||||
|
||||
h_flex()
|
||||
.flex_wrap()
|
||||
.gap_1()
|
||||
.child(
|
||||
PopoverMenu::new("context-picker")
|
||||
.menu(move |_cx| Some(context_picker.clone()))
|
||||
.trigger(
|
||||
IconButton::new("add-context", IconName::Plus)
|
||||
.icon_size(IconSize::Small)
|
||||
.style(ui::ButtonStyle::Filled)
|
||||
.tooltip(move |cx| {
|
||||
Tooltip::for_action_in(
|
||||
"Add Context",
|
||||
&ToggleContextPicker,
|
||||
&focus_handle,
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
)
|
||||
.attach(gpui::Corner::TopLeft)
|
||||
.anchor(gpui::Corner::BottomLeft)
|
||||
.offset(gpui::Point {
|
||||
x: px(0.0),
|
||||
y: px(-16.0),
|
||||
})
|
||||
.with_handle(self.context_picker_menu_handle.clone()),
|
||||
)
|
||||
.children(context.iter().map(|context| {
|
||||
ContextPill::new(context.clone()).on_remove({
|
||||
let context = context.clone();
|
||||
let context_store = self.context_store.clone();
|
||||
Rc::new(cx.listener(move |_this, _event, cx| {
|
||||
context_store.update(cx, |this, _cx| {
|
||||
this.remove_context(&context.id);
|
||||
});
|
||||
cx.notify();
|
||||
}))
|
||||
})
|
||||
}))
|
||||
.when(!context.is_empty(), |parent| {
|
||||
parent.child(
|
||||
IconButton::new("remove-all-context", IconName::Eraser)
|
||||
.icon_size(IconSize::Small)
|
||||
.tooltip(move |cx| Tooltip::text("Remove All Context", cx))
|
||||
.on_click({
|
||||
let context_store = self.context_store.clone();
|
||||
cx.listener(move |_this, _event, cx| {
|
||||
context_store.update(cx, |this, _cx| this.clear());
|
||||
cx.notify();
|
||||
})
|
||||
}),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,191 +0,0 @@
|
||||
use gpui::{AnyElement, EventEmitter};
|
||||
use ui::{prelude::*, IconButtonShape, Tooltip};
|
||||
|
||||
pub enum CodegenStatus {
|
||||
Idle,
|
||||
Pending,
|
||||
Done,
|
||||
Error(anyhow::Error),
|
||||
}
|
||||
|
||||
/// This is just CodegenStatus without the anyhow::Error, which causes a lifetime issue for rendering the Cancel button.
|
||||
#[derive(Copy, Clone)]
|
||||
pub enum CancelButtonState {
|
||||
Idle,
|
||||
Pending,
|
||||
Done,
|
||||
Error,
|
||||
}
|
||||
|
||||
impl Into<CancelButtonState> for &CodegenStatus {
|
||||
fn into(self) -> CancelButtonState {
|
||||
match self {
|
||||
CodegenStatus::Idle => CancelButtonState::Idle,
|
||||
CodegenStatus::Pending => CancelButtonState::Pending,
|
||||
CodegenStatus::Done => CancelButtonState::Done,
|
||||
CodegenStatus::Error(_) => CancelButtonState::Error,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub enum PromptMode {
|
||||
Generate { supports_execute: bool },
|
||||
Transform,
|
||||
}
|
||||
|
||||
impl PromptMode {
|
||||
fn start_label(self) -> &'static str {
|
||||
match self {
|
||||
PromptMode::Generate { .. } => "Generate",
|
||||
PromptMode::Transform => "Transform",
|
||||
}
|
||||
}
|
||||
fn tooltip_interrupt(self) -> &'static str {
|
||||
match self {
|
||||
PromptMode::Generate { .. } => "Interrupt Generation",
|
||||
PromptMode::Transform => "Interrupt Transform",
|
||||
}
|
||||
}
|
||||
|
||||
fn tooltip_restart(self) -> &'static str {
|
||||
match self {
|
||||
PromptMode::Generate { .. } => "Restart Generation",
|
||||
PromptMode::Transform => "Restart Transform",
|
||||
}
|
||||
}
|
||||
|
||||
fn tooltip_accept(self) -> &'static str {
|
||||
match self {
|
||||
PromptMode::Generate { .. } => "Accept Generation",
|
||||
PromptMode::Transform => "Accept Transform",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render_cancel_button<T: EventEmitter<PromptEditorEvent>>(
|
||||
cancel_button_state: CancelButtonState,
|
||||
edited_since_done: bool,
|
||||
mode: PromptMode,
|
||||
cx: &mut ViewContext<T>,
|
||||
) -> Vec<AnyElement> {
|
||||
match cancel_button_state {
|
||||
CancelButtonState::Idle => {
|
||||
vec![
|
||||
IconButton::new("cancel", IconName::Close)
|
||||
.icon_color(Color::Muted)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(|cx| Tooltip::for_action("Cancel Assist", &menu::Cancel, cx))
|
||||
.on_click(cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::CancelRequested)))
|
||||
.into_any_element(),
|
||||
Button::new("start", mode.start_label())
|
||||
.icon(IconName::Return)
|
||||
.icon_color(Color::Muted)
|
||||
.on_click(cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::StartRequested)))
|
||||
.into_any_element(),
|
||||
]
|
||||
}
|
||||
CancelButtonState::Pending => vec![
|
||||
IconButton::new("cancel", IconName::Close)
|
||||
.icon_color(Color::Muted)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(|cx| Tooltip::text("Cancel Assist", cx))
|
||||
.on_click(cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::CancelRequested)))
|
||||
.into_any_element(),
|
||||
IconButton::new("stop", IconName::Stop)
|
||||
.icon_color(Color::Error)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(move |cx| {
|
||||
Tooltip::with_meta(
|
||||
mode.tooltip_interrupt(),
|
||||
Some(&menu::Cancel),
|
||||
"Changes won't be discarded",
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.on_click(cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::StopRequested)))
|
||||
.into_any_element(),
|
||||
],
|
||||
CancelButtonState::Done | CancelButtonState::Error => {
|
||||
let cancel = IconButton::new("cancel", IconName::Close)
|
||||
.icon_color(Color::Muted)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(|cx| Tooltip::for_action("Cancel Assist", &menu::Cancel, cx))
|
||||
.on_click(cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::CancelRequested)))
|
||||
.into_any_element();
|
||||
|
||||
let has_error = matches!(cancel_button_state, CancelButtonState::Error);
|
||||
if has_error || edited_since_done {
|
||||
vec![
|
||||
cancel,
|
||||
IconButton::new("restart", IconName::RotateCw)
|
||||
.icon_color(Color::Info)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(move |cx| {
|
||||
Tooltip::with_meta(
|
||||
mode.tooltip_restart(),
|
||||
Some(&menu::Confirm),
|
||||
"Changes will be discarded",
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.on_click(cx.listener(|_, _, cx| {
|
||||
cx.emit(PromptEditorEvent::StartRequested);
|
||||
}))
|
||||
.into_any_element(),
|
||||
]
|
||||
} else {
|
||||
let mut buttons = vec![
|
||||
cancel,
|
||||
IconButton::new("accept", IconName::Check)
|
||||
.icon_color(Color::Info)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(move |cx| {
|
||||
Tooltip::for_action(mode.tooltip_accept(), &menu::Confirm, cx)
|
||||
})
|
||||
.on_click(cx.listener(|_, _, cx| {
|
||||
cx.emit(PromptEditorEvent::ConfirmRequested { execute: false });
|
||||
}))
|
||||
.into_any_element(),
|
||||
];
|
||||
|
||||
match mode {
|
||||
PromptMode::Generate { supports_execute } => {
|
||||
if supports_execute {
|
||||
buttons.push(
|
||||
IconButton::new("confirm", IconName::Play)
|
||||
.icon_color(Color::Info)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(|cx| {
|
||||
Tooltip::for_action(
|
||||
"Execute Generated Command",
|
||||
&menu::SecondaryConfirm,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.on_click(cx.listener(|_, _, cx| {
|
||||
cx.emit(PromptEditorEvent::ConfirmRequested {
|
||||
execute: true,
|
||||
});
|
||||
}))
|
||||
.into_any_element(),
|
||||
)
|
||||
}
|
||||
}
|
||||
PromptMode::Transform => {}
|
||||
}
|
||||
|
||||
buttons
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub enum PromptEditorEvent {
|
||||
StartRequested,
|
||||
StopRequested,
|
||||
ConfirmRequested { execute: bool },
|
||||
CancelRequested,
|
||||
DismissRequested,
|
||||
Resized { height_in_lines: u8 },
|
||||
}
|
||||
@@ -1,97 +1,51 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use editor::{Editor, EditorElement, EditorStyle};
|
||||
use fs::Fs;
|
||||
use gpui::{AppContext, FocusableView, Model, TextStyle, View, WeakModel, WeakView};
|
||||
use gpui::{AppContext, FocusableView, Model, TextStyle, View};
|
||||
use language_model::{LanguageModelRegistry, LanguageModelRequestTool};
|
||||
use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu};
|
||||
use settings::{update_settings_file, Settings};
|
||||
use language_model_selector::LanguageModelSelector;
|
||||
use picker::Picker;
|
||||
use settings::Settings;
|
||||
use theme::ThemeSettings;
|
||||
use ui::{
|
||||
prelude::*, ButtonLike, CheckboxWithLabel, ElevationIndex, KeyBinding, PopoverMenuHandle,
|
||||
Tooltip,
|
||||
prelude::*, ButtonLike, CheckboxWithLabel, ElevationIndex, IconButtonShape, KeyBinding,
|
||||
PopoverMenuHandle, Tooltip,
|
||||
};
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::assistant_settings::AssistantSettings;
|
||||
use crate::context_picker::ContextPicker;
|
||||
use crate::context_store::ContextStore;
|
||||
use crate::context_strip::ContextStrip;
|
||||
use crate::context::{Context, ContextKind};
|
||||
use crate::context_picker::{ContextPicker, ContextPickerDelegate};
|
||||
use crate::thread::{RequestKind, Thread};
|
||||
use crate::thread_store::ThreadStore;
|
||||
use crate::{Chat, ToggleContextPicker, ToggleModelSelector};
|
||||
use crate::ui::ContextPill;
|
||||
use crate::{Chat, ToggleModelSelector};
|
||||
|
||||
pub struct MessageEditor {
|
||||
thread: Model<Thread>,
|
||||
editor: View<Editor>,
|
||||
context_store: Model<ContextStore>,
|
||||
context_strip: View<ContextStrip>,
|
||||
context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
|
||||
language_model_selector: View<LanguageModelSelector>,
|
||||
language_model_selector_menu_handle: PopoverMenuHandle<LanguageModelSelector>,
|
||||
context: Vec<Context>,
|
||||
pub(crate) context_picker_handle: PopoverMenuHandle<Picker<ContextPickerDelegate>>,
|
||||
use_tools: bool,
|
||||
}
|
||||
|
||||
impl MessageEditor {
|
||||
pub fn new(
|
||||
fs: Arc<dyn Fs>,
|
||||
workspace: WeakView<Workspace>,
|
||||
thread_store: WeakModel<ThreadStore>,
|
||||
thread: Model<Thread>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let context_store = cx.new_model(|_cx| ContextStore::new());
|
||||
let context_picker_menu_handle = PopoverMenuHandle::default();
|
||||
|
||||
let editor = cx.new_view(|cx| {
|
||||
let mut editor = Editor::auto_height(80, cx);
|
||||
editor.set_placeholder_text("Ask anything, @ to add context", cx);
|
||||
editor.set_show_indent_guides(false, cx);
|
||||
|
||||
editor
|
||||
});
|
||||
pub fn new(thread: Model<Thread>, cx: &mut ViewContext<Self>) -> Self {
|
||||
let mocked_context = vec![Context {
|
||||
name: "shape.rs".into(),
|
||||
kind: ContextKind::File,
|
||||
text: "```rs\npub enum Shape {\n Circle,\n Square,\n Triangle,\n}".into(),
|
||||
}];
|
||||
|
||||
Self {
|
||||
thread,
|
||||
editor: editor.clone(),
|
||||
context_store: context_store.clone(),
|
||||
context_strip: cx.new_view(|cx| {
|
||||
ContextStrip::new(
|
||||
context_store,
|
||||
workspace.clone(),
|
||||
Some(thread_store.clone()),
|
||||
editor.focus_handle(cx),
|
||||
context_picker_menu_handle.clone(),
|
||||
cx,
|
||||
)
|
||||
editor: cx.new_view(|cx| {
|
||||
let mut editor = Editor::auto_height(80, cx);
|
||||
editor.set_placeholder_text("Ask anything or type @ to add context", cx);
|
||||
|
||||
editor
|
||||
}),
|
||||
context_picker_menu_handle,
|
||||
language_model_selector: cx.new_view(|cx| {
|
||||
let fs = fs.clone();
|
||||
LanguageModelSelector::new(
|
||||
move |model, cx| {
|
||||
update_settings_file::<AssistantSettings>(
|
||||
fs.clone(),
|
||||
cx,
|
||||
move |settings, _cx| settings.set_model(model.clone()),
|
||||
);
|
||||
},
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
language_model_selector_menu_handle: PopoverMenuHandle::default(),
|
||||
context: mocked_context,
|
||||
context_picker_handle: PopoverMenuHandle::default(),
|
||||
use_tools: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn toggle_model_selector(&mut self, _: &ToggleModelSelector, cx: &mut ViewContext<Self>) {
|
||||
self.language_model_selector_menu_handle.toggle(cx);
|
||||
}
|
||||
|
||||
fn toggle_context_picker(&mut self, _: &ToggleContextPicker, cx: &mut ViewContext<Self>) {
|
||||
self.context_picker_menu_handle.toggle(cx);
|
||||
}
|
||||
|
||||
fn chat(&mut self, _: &Chat, cx: &mut ViewContext<Self>) {
|
||||
self.send_to_model(RequestKind::Chat, cx);
|
||||
}
|
||||
@@ -118,7 +72,7 @@ impl MessageEditor {
|
||||
editor.clear(cx);
|
||||
text
|
||||
});
|
||||
let context = self.context_store.update(cx, |this, _cx| this.drain());
|
||||
let context = self.context.drain(..).collect::<Vec<_>>();
|
||||
|
||||
self.thread.update(cx, |thread, cx| {
|
||||
thread.insert_user_message(user_message, context, cx);
|
||||
@@ -144,11 +98,13 @@ impl MessageEditor {
|
||||
}
|
||||
|
||||
fn render_language_model_selector(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let active_provider = LanguageModelRegistry::read_global(cx).active_provider();
|
||||
let active_model = LanguageModelRegistry::read_global(cx).active_model();
|
||||
let focus_handle = self.language_model_selector.focus_handle(cx).clone();
|
||||
|
||||
LanguageModelSelectorPopoverMenu::new(
|
||||
self.language_model_selector.clone(),
|
||||
LanguageModelSelector::new(
|
||||
|model, _cx| {
|
||||
println!("Selected {:?}", model.name());
|
||||
},
|
||||
ButtonLike::new("active-model")
|
||||
.style(ButtonStyle::Subtle)
|
||||
.child(
|
||||
@@ -160,8 +116,16 @@ impl MessageEditor {
|
||||
.overflow_x_hidden()
|
||||
.flex_grow()
|
||||
.whitespace_nowrap()
|
||||
.child(match active_model {
|
||||
Some(model) => h_flex()
|
||||
.child(match (active_provider, active_model) {
|
||||
(Some(provider), Some(model)) => h_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
Icon::new(
|
||||
model.icon().unwrap_or_else(|| provider.icon()),
|
||||
)
|
||||
.color(Color::Muted)
|
||||
.size(IconSize::XSmall),
|
||||
)
|
||||
.child(
|
||||
Label::new(model.name().0)
|
||||
.size(LabelSize::Small)
|
||||
@@ -180,11 +144,8 @@ impl MessageEditor {
|
||||
.size(IconSize::XSmall),
|
||||
),
|
||||
)
|
||||
.tooltip(move |cx| {
|
||||
Tooltip::for_action_in("Change Model", &ToggleModelSelector, &focus_handle, cx)
|
||||
}),
|
||||
.tooltip(move |cx| Tooltip::for_action("Change Model", &ToggleModelSelector, cx)),
|
||||
)
|
||||
.with_handle(self.language_model_selector_menu_handle.clone())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,21 +158,45 @@ impl FocusableView for MessageEditor {
|
||||
impl Render for MessageEditor {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let font_size = TextSize::Default.rems(cx);
|
||||
let line_height = font_size.to_pixels(cx.rem_size()) * 1.5;
|
||||
let line_height = font_size.to_pixels(cx.rem_size()) * 1.3;
|
||||
let focus_handle = self.editor.focus_handle(cx);
|
||||
let bg_color = cx.theme().colors().editor_background;
|
||||
|
||||
v_flex()
|
||||
.key_context("MessageEditor")
|
||||
.on_action(cx.listener(Self::chat))
|
||||
.on_action(cx.listener(Self::toggle_model_selector))
|
||||
.on_action(cx.listener(Self::toggle_context_picker))
|
||||
.size_full()
|
||||
.gap_2()
|
||||
.p_2()
|
||||
.bg(bg_color)
|
||||
.child(self.context_strip.clone())
|
||||
.child(div().id("thread_editor").overflow_y_scroll().h_12().child({
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.child(
|
||||
h_flex()
|
||||
.flex_wrap()
|
||||
.gap_2()
|
||||
.child(ContextPicker::new(
|
||||
cx.view().downgrade(),
|
||||
IconButton::new("add-context", IconName::Plus)
|
||||
.shape(IconButtonShape::Square)
|
||||
.icon_size(IconSize::Small),
|
||||
))
|
||||
.children(
|
||||
self.context
|
||||
.iter()
|
||||
.map(|context| ContextPill::new(context.clone())),
|
||||
)
|
||||
.when(!self.context.is_empty(), |parent| {
|
||||
parent.child(
|
||||
IconButton::new("remove-all-context", IconName::Eraser)
|
||||
.shape(IconButtonShape::Square)
|
||||
.icon_size(IconSize::Small)
|
||||
.tooltip(move |cx| Tooltip::text("Remove All Context", cx))
|
||||
.on_click(cx.listener(|this, _event, cx| {
|
||||
this.context.clear();
|
||||
cx.notify();
|
||||
})),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.child({
|
||||
let settings = ThemeSettings::get_global(cx);
|
||||
let text_style = TextStyle {
|
||||
color: cx.theme().colors().editor_foreground,
|
||||
@@ -226,30 +211,30 @@ impl Render for MessageEditor {
|
||||
EditorElement::new(
|
||||
&self.editor,
|
||||
EditorStyle {
|
||||
background: bg_color,
|
||||
background: cx.theme().colors().editor_background,
|
||||
local_player: cx.theme().players().local(),
|
||||
text: text_style,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
}))
|
||||
})
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_between()
|
||||
.child(CheckboxWithLabel::new(
|
||||
.child(h_flex().gap_2().child(CheckboxWithLabel::new(
|
||||
"use-tools",
|
||||
Label::new("Tools"),
|
||||
self.use_tools.into(),
|
||||
cx.listener(|this, selection, _cx| {
|
||||
this.use_tools = match selection {
|
||||
ToggleState::Selected => true,
|
||||
ToggleState::Unselected | ToggleState::Indeterminate => false,
|
||||
Selection::Selected => true,
|
||||
Selection::Unselected | Selection::Indeterminate => false,
|
||||
};
|
||||
}),
|
||||
))
|
||||
)))
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.gap_2()
|
||||
.child(self.render_language_model_selector(cx))
|
||||
.child(
|
||||
ButtonLike::new("chat")
|
||||
|
||||
@@ -1,312 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use assets::Assets;
|
||||
use fs::Fs;
|
||||
use futures::StreamExt;
|
||||
use gpui::AssetSource;
|
||||
use handlebars::{Handlebars, RenderError};
|
||||
use language::{BufferSnapshot, LanguageName, Point};
|
||||
use parking_lot::Mutex;
|
||||
use serde::Serialize;
|
||||
use std::{ops::Range, path::PathBuf, sync::Arc, time::Duration};
|
||||
use text::LineEnding;
|
||||
use util::ResultExt;
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct ContentPromptDiagnosticContext {
|
||||
pub line_number: usize,
|
||||
pub error_message: String,
|
||||
pub code_content: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct ContentPromptContext {
|
||||
pub content_type: String,
|
||||
pub language_name: Option<String>,
|
||||
pub is_insert: bool,
|
||||
pub is_truncated: bool,
|
||||
pub document_content: String,
|
||||
pub user_prompt: String,
|
||||
pub rewrite_section: Option<String>,
|
||||
pub diagnostic_errors: Vec<ContentPromptDiagnosticContext>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct TerminalAssistantPromptContext {
|
||||
pub os: String,
|
||||
pub arch: String,
|
||||
pub shell: Option<String>,
|
||||
pub working_directory: Option<String>,
|
||||
pub latest_output: Vec<String>,
|
||||
pub user_prompt: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct ProjectSlashCommandPromptContext {
|
||||
pub context_buffer: String,
|
||||
}
|
||||
|
||||
pub struct PromptLoadingParams<'a> {
|
||||
pub fs: Arc<dyn Fs>,
|
||||
pub repo_path: Option<PathBuf>,
|
||||
pub cx: &'a gpui::AppContext,
|
||||
}
|
||||
|
||||
pub struct PromptBuilder {
|
||||
handlebars: Arc<Mutex<Handlebars<'static>>>,
|
||||
}
|
||||
|
||||
impl PromptBuilder {
|
||||
pub fn new(loading_params: Option<PromptLoadingParams>) -> Result<Self> {
|
||||
let mut handlebars = Handlebars::new();
|
||||
Self::register_built_in_templates(&mut handlebars)?;
|
||||
|
||||
let handlebars = Arc::new(Mutex::new(handlebars));
|
||||
|
||||
if let Some(params) = loading_params {
|
||||
Self::watch_fs_for_template_overrides(params, handlebars.clone());
|
||||
}
|
||||
|
||||
Ok(Self { handlebars })
|
||||
}
|
||||
|
||||
/// Watches the filesystem for changes to prompt template overrides.
|
||||
///
|
||||
/// This function sets up a file watcher on the prompt templates directory. It performs
|
||||
/// an initial scan of the directory and registers any existing template overrides.
|
||||
/// Then it continuously monitors for changes, reloading templates as they are
|
||||
/// modified or added.
|
||||
///
|
||||
/// If the templates directory doesn't exist initially, it waits for it to be created.
|
||||
/// If the directory is removed, it restores the built-in templates and waits for the
|
||||
/// directory to be recreated.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `params` - A `PromptLoadingParams` struct containing the filesystem, repository path,
|
||||
/// and application context.
|
||||
/// * `handlebars` - An `Arc<Mutex<Handlebars>>` for registering and updating templates.
|
||||
fn watch_fs_for_template_overrides(
|
||||
params: PromptLoadingParams,
|
||||
handlebars: Arc<Mutex<Handlebars<'static>>>,
|
||||
) {
|
||||
let templates_dir = paths::prompt_overrides_dir(params.repo_path.as_deref());
|
||||
params.cx.background_executor()
|
||||
.spawn(async move {
|
||||
let Some(parent_dir) = templates_dir.parent() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let mut found_dir_once = false;
|
||||
loop {
|
||||
// Check if the templates directory exists and handle its status
|
||||
// If it exists, log its presence and check if it's a symlink
|
||||
// If it doesn't exist:
|
||||
// - Log that we're using built-in prompts
|
||||
// - Check if it's a broken symlink and log if so
|
||||
// - Set up a watcher to detect when it's created
|
||||
// After the first check, set the `found_dir_once` flag
|
||||
// This allows us to avoid logging when looping back around after deleting the prompt overrides directory.
|
||||
let dir_status = params.fs.is_dir(&templates_dir).await;
|
||||
let symlink_status = params.fs.read_link(&templates_dir).await.ok();
|
||||
if dir_status {
|
||||
let mut log_message = format!("Prompt template overrides directory found at {}", templates_dir.display());
|
||||
if let Some(target) = symlink_status {
|
||||
log_message.push_str(" -> ");
|
||||
log_message.push_str(&target.display().to_string());
|
||||
}
|
||||
log::info!("{}.", log_message);
|
||||
} else {
|
||||
if !found_dir_once {
|
||||
log::info!("No prompt template overrides directory found at {}. Using built-in prompts.", templates_dir.display());
|
||||
if let Some(target) = symlink_status {
|
||||
log::info!("Symlink found pointing to {}, but target is invalid.", target.display());
|
||||
}
|
||||
}
|
||||
|
||||
if params.fs.is_dir(parent_dir).await {
|
||||
let (mut changes, _watcher) = params.fs.watch(parent_dir, Duration::from_secs(1)).await;
|
||||
while let Some(changed_paths) = changes.next().await {
|
||||
if changed_paths.iter().any(|p| &p.path == &templates_dir) {
|
||||
let mut log_message = format!("Prompt template overrides directory detected at {}", templates_dir.display());
|
||||
if let Ok(target) = params.fs.read_link(&templates_dir).await {
|
||||
log_message.push_str(" -> ");
|
||||
log_message.push_str(&target.display().to_string());
|
||||
}
|
||||
log::info!("{}.", log_message);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
found_dir_once = true;
|
||||
|
||||
// Initial scan of the prompt overrides directory
|
||||
if let Ok(mut entries) = params.fs.read_dir(&templates_dir).await {
|
||||
while let Some(Ok(file_path)) = entries.next().await {
|
||||
if file_path.to_string_lossy().ends_with(".hbs") {
|
||||
if let Ok(content) = params.fs.load(&file_path).await {
|
||||
let file_name = file_path.file_stem().unwrap().to_string_lossy();
|
||||
log::debug!("Registering prompt template override: {}", file_name);
|
||||
handlebars.lock().register_template_string(&file_name, content).log_err();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Watch both the parent directory and the template overrides directory:
|
||||
// - Monitor the parent directory to detect if the template overrides directory is deleted.
|
||||
// - Monitor the template overrides directory to re-register templates when they change.
|
||||
// Combine both watch streams into a single stream.
|
||||
let (parent_changes, parent_watcher) = params.fs.watch(parent_dir, Duration::from_secs(1)).await;
|
||||
let (changes, watcher) = params.fs.watch(&templates_dir, Duration::from_secs(1)).await;
|
||||
let mut combined_changes = futures::stream::select(changes, parent_changes);
|
||||
|
||||
while let Some(changed_paths) = combined_changes.next().await {
|
||||
if changed_paths.iter().any(|p| &p.path == &templates_dir) {
|
||||
if !params.fs.is_dir(&templates_dir).await {
|
||||
log::info!("Prompt template overrides directory removed. Restoring built-in prompt templates.");
|
||||
Self::register_built_in_templates(&mut handlebars.lock()).log_err();
|
||||
break;
|
||||
}
|
||||
}
|
||||
for event in changed_paths {
|
||||
if event.path.starts_with(&templates_dir) && event.path.extension().map_or(false, |ext| ext == "hbs") {
|
||||
log::info!("Reloading prompt template override: {}", event.path.display());
|
||||
if let Some(content) = params.fs.load(&event.path).await.log_err() {
|
||||
let file_name = event.path.file_stem().unwrap().to_string_lossy();
|
||||
handlebars.lock().register_template_string(&file_name, content).log_err();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
drop(watcher);
|
||||
drop(parent_watcher);
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn register_built_in_templates(handlebars: &mut Handlebars) -> Result<()> {
|
||||
for path in Assets.list("prompts")? {
|
||||
if let Some(id) = path.split('/').last().and_then(|s| s.strip_suffix(".hbs")) {
|
||||
if let Some(prompt) = Assets.load(path.as_ref()).log_err().flatten() {
|
||||
log::debug!("Registering built-in prompt template: {}", id);
|
||||
let prompt = String::from_utf8_lossy(prompt.as_ref());
|
||||
handlebars.register_template_string(id, LineEnding::normalize_cow(prompt))?
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn generate_inline_transformation_prompt(
|
||||
&self,
|
||||
user_prompt: String,
|
||||
language_name: Option<&LanguageName>,
|
||||
buffer: BufferSnapshot,
|
||||
range: Range<usize>,
|
||||
) -> Result<String, RenderError> {
|
||||
let content_type = match language_name.as_ref().map(|l| l.0.as_ref()) {
|
||||
None | Some("Markdown" | "Plain Text") => "text",
|
||||
Some(_) => "code",
|
||||
};
|
||||
|
||||
const MAX_CTX: usize = 50000;
|
||||
let is_insert = range.is_empty();
|
||||
let mut is_truncated = false;
|
||||
|
||||
let before_range = 0..range.start;
|
||||
let truncated_before = if before_range.len() > MAX_CTX {
|
||||
is_truncated = true;
|
||||
let start = buffer.clip_offset(range.start - MAX_CTX, text::Bias::Right);
|
||||
start..range.start
|
||||
} else {
|
||||
before_range
|
||||
};
|
||||
|
||||
let after_range = range.end..buffer.len();
|
||||
let truncated_after = if after_range.len() > MAX_CTX {
|
||||
is_truncated = true;
|
||||
let end = buffer.clip_offset(range.end + MAX_CTX, text::Bias::Left);
|
||||
range.end..end
|
||||
} else {
|
||||
after_range
|
||||
};
|
||||
|
||||
let mut document_content = String::new();
|
||||
for chunk in buffer.text_for_range(truncated_before) {
|
||||
document_content.push_str(chunk);
|
||||
}
|
||||
if is_insert {
|
||||
document_content.push_str("<insert_here></insert_here>");
|
||||
} else {
|
||||
document_content.push_str("<rewrite_this>\n");
|
||||
for chunk in buffer.text_for_range(range.clone()) {
|
||||
document_content.push_str(chunk);
|
||||
}
|
||||
document_content.push_str("\n</rewrite_this>");
|
||||
}
|
||||
for chunk in buffer.text_for_range(truncated_after) {
|
||||
document_content.push_str(chunk);
|
||||
}
|
||||
|
||||
let rewrite_section = if !is_insert {
|
||||
let mut section = String::new();
|
||||
for chunk in buffer.text_for_range(range.clone()) {
|
||||
section.push_str(chunk);
|
||||
}
|
||||
Some(section)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let diagnostics = buffer.diagnostics_in_range::<_, Point>(range, false);
|
||||
let diagnostic_errors: Vec<ContentPromptDiagnosticContext> = diagnostics
|
||||
.map(|entry| {
|
||||
let start = entry.range.start;
|
||||
ContentPromptDiagnosticContext {
|
||||
line_number: (start.row + 1) as usize,
|
||||
error_message: entry.diagnostic.message.clone(),
|
||||
code_content: buffer.text_for_range(entry.range.clone()).collect(),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
let context = ContentPromptContext {
|
||||
content_type: content_type.to_string(),
|
||||
language_name: language_name.map(|s| s.to_string()),
|
||||
is_insert,
|
||||
is_truncated,
|
||||
document_content,
|
||||
user_prompt,
|
||||
rewrite_section,
|
||||
diagnostic_errors,
|
||||
};
|
||||
self.handlebars.lock().render("content_prompt", &context)
|
||||
}
|
||||
|
||||
pub fn generate_terminal_assistant_prompt(
|
||||
&self,
|
||||
user_prompt: &str,
|
||||
shell: Option<&str>,
|
||||
working_directory: Option<&str>,
|
||||
latest_output: &[String],
|
||||
) -> Result<String, RenderError> {
|
||||
let context = TerminalAssistantPromptContext {
|
||||
os: std::env::consts::OS.to_string(),
|
||||
arch: std::env::consts::ARCH.to_string(),
|
||||
shell: shell.map(|s| s.to_string()),
|
||||
working_directory: working_directory.map(|s| s.to_string()),
|
||||
latest_output: latest_output.to_vec(),
|
||||
user_prompt: user_prompt.to_string(),
|
||||
};
|
||||
|
||||
self.handlebars
|
||||
.lock()
|
||||
.render("terminal_assistant_prompt", &context)
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@ use serde::{Deserialize, Serialize};
|
||||
use util::{post_inc, TryFutureExt as _};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::context::{attach_context_to_message, Context};
|
||||
use crate::context::{Context, ContextKind};
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum RequestKind {
|
||||
@@ -192,7 +192,26 @@ impl Thread {
|
||||
}
|
||||
|
||||
if let Some(context) = self.context_for_message(message.id) {
|
||||
attach_context_to_message(&mut request_message, context.clone());
|
||||
let mut file_context = String::new();
|
||||
|
||||
for context in context.iter() {
|
||||
match context.kind {
|
||||
ContextKind::File => {
|
||||
file_context.push_str(&context.text);
|
||||
file_context.push_str("\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut context_text = String::new();
|
||||
if !file_context.is_empty() {
|
||||
context_text.push_str("The following files are available:\n");
|
||||
context_text.push_str(&file_context);
|
||||
}
|
||||
|
||||
request_message
|
||||
.content
|
||||
.push(MessageContent::Text(context_text))
|
||||
}
|
||||
|
||||
if !message.text.is_empty() {
|
||||
|
||||
@@ -2,7 +2,7 @@ use gpui::{
|
||||
uniform_list, AppContext, FocusHandle, FocusableView, Model, UniformListScrollHandle, WeakView,
|
||||
};
|
||||
use time::{OffsetDateTime, UtcOffset};
|
||||
use ui::{prelude::*, IconButtonShape, ListItem, ListItemSpacing, Tooltip};
|
||||
use ui::{prelude::*, IconButtonShape, ListItem};
|
||||
|
||||
use crate::thread::Thread;
|
||||
use crate::thread_store::ThreadStore;
|
||||
@@ -66,10 +66,10 @@ impl Render for ThreadHistory {
|
||||
threads[range]
|
||||
.iter()
|
||||
.map(|thread| {
|
||||
h_flex().w_full().pb_1().child(PastThread::new(
|
||||
PastThread::new(
|
||||
thread.clone(),
|
||||
history.assistant_panel.clone(),
|
||||
))
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
},
|
||||
@@ -117,29 +117,17 @@ impl RenderOnce for PastThread {
|
||||
.unwrap_or(UtcOffset::UTC),
|
||||
time_format::TimestampFormat::EnhancedAbsolute,
|
||||
);
|
||||
|
||||
ListItem::new(("past-thread", self.thread.entity_id()))
|
||||
.outlined()
|
||||
.start_slot(
|
||||
Icon::new(IconName::MessageCircle)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.child(Label::new(summary).size(LabelSize::Small).text_ellipsis())
|
||||
.start_slot(Icon::new(IconName::MessageBubbles))
|
||||
.child(Label::new(summary))
|
||||
.end_slot(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(
|
||||
Label::new(thread_timestamp)
|
||||
.color(Color::Disabled)
|
||||
.size(LabelSize::Small),
|
||||
)
|
||||
.child(Label::new(thread_timestamp).color(Color::Disabled))
|
||||
.child(
|
||||
IconButton::new("delete", IconName::TrashAlt)
|
||||
.shape(IconButtonShape::Square)
|
||||
.icon_size(IconSize::Small)
|
||||
.tooltip(|cx| Tooltip::text("Delete Thread", cx))
|
||||
.on_click({
|
||||
let assistant_panel = self.assistant_panel.clone();
|
||||
let id = id.clone();
|
||||
|
||||
@@ -1,52 +1,25 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use gpui::ClickEvent;
|
||||
use ui::{prelude::*, IconButtonShape};
|
||||
use ui::prelude::*;
|
||||
|
||||
use crate::context::Context;
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct ContextPill {
|
||||
context: Context,
|
||||
on_remove: Option<Rc<dyn Fn(&ClickEvent, &mut WindowContext)>>,
|
||||
}
|
||||
|
||||
impl ContextPill {
|
||||
pub fn new(context: Context) -> Self {
|
||||
Self {
|
||||
context,
|
||||
on_remove: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_remove(mut self, on_remove: Rc<dyn Fn(&ClickEvent, &mut WindowContext)>) -> Self {
|
||||
self.on_remove = Some(on_remove);
|
||||
self
|
||||
Self { context }
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for ContextPill {
|
||||
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.pl_1p5()
|
||||
.pr_0p5()
|
||||
.pb(px(1.))
|
||||
div()
|
||||
.px_1()
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border.opacity(0.5))
|
||||
.bg(cx.theme().colors().element_background)
|
||||
.border_color(cx.theme().colors().border)
|
||||
.rounded_md()
|
||||
.child(Label::new(self.context.name.clone()).size(LabelSize::Small))
|
||||
.when_some(self.on_remove, |parent, on_remove| {
|
||||
parent.child(
|
||||
IconButton::new(("remove", self.context.id.0), IconName::Close)
|
||||
.shape(IconButtonShape::Square)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.on_click({
|
||||
let on_remove = on_remove.clone();
|
||||
move |event, cx| on_remove(event, cx)
|
||||
}),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +40,6 @@ schemars.workspace = true
|
||||
serde.workspace = true
|
||||
serde_derive.workspace = true
|
||||
settings.workspace = true
|
||||
telemetry.workspace = true
|
||||
util.workspace = true
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
|
||||
@@ -20,7 +20,7 @@ pub struct CallSettingsContent {
|
||||
|
||||
/// Whether your current project should be shared when joining an empty channel.
|
||||
///
|
||||
/// Default: false
|
||||
/// Default: true
|
||||
pub share_on_join: Option<bool>,
|
||||
}
|
||||
|
||||
|
||||
@@ -250,9 +250,7 @@ impl ActiveCall {
|
||||
cx.spawn(move |this, mut cx| async move {
|
||||
let result = invite.await;
|
||||
if result.is_ok() {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.report_call_event("Participant Invited", cx)
|
||||
})?;
|
||||
this.update(&mut cx, |this, cx| this.report_call_event("invite", cx))?;
|
||||
} else {
|
||||
//TODO: report collaboration error
|
||||
log::error!("invite failed: {:?}", result);
|
||||
@@ -320,7 +318,7 @@ impl ActiveCall {
|
||||
this.update(&mut cx, |this, cx| this.set_room(room.clone(), cx))?
|
||||
.await?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.report_call_event("Incoming Call Accepted", cx)
|
||||
this.report_call_event("accept incoming", cx)
|
||||
})?;
|
||||
Ok(())
|
||||
})
|
||||
@@ -333,7 +331,7 @@ impl ActiveCall {
|
||||
.borrow_mut()
|
||||
.take()
|
||||
.ok_or_else(|| anyhow!("no incoming call"))?;
|
||||
telemetry::event!("Incoming Call Declined", room_id = call.room_id);
|
||||
report_call_event_for_room("decline incoming", call.room_id, None, &self.client);
|
||||
self.client.send(proto::DeclineCall {
|
||||
room_id: call.room_id,
|
||||
})?;
|
||||
@@ -368,7 +366,7 @@ impl ActiveCall {
|
||||
this.update(&mut cx, |this, cx| this.set_room(room.clone(), cx))?
|
||||
.await?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.report_call_event("Channel Joined", cx)
|
||||
this.report_call_event("join channel", cx)
|
||||
})?;
|
||||
Ok(room)
|
||||
})
|
||||
@@ -376,7 +374,7 @@ impl ActiveCall {
|
||||
|
||||
pub fn hang_up(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
|
||||
cx.notify();
|
||||
self.report_call_event("Call Ended", cx);
|
||||
self.report_call_event("hang up", cx);
|
||||
|
||||
Audio::end_call(cx);
|
||||
|
||||
@@ -395,7 +393,7 @@ impl ActiveCall {
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<u64>> {
|
||||
if let Some((room, _)) = self.room.as_ref() {
|
||||
self.report_call_event("Project Shared", cx);
|
||||
self.report_call_event("share project", cx);
|
||||
room.update(cx, |room, cx| room.share_project(project, cx))
|
||||
} else {
|
||||
Task::ready(Err(anyhow!("no active call")))
|
||||
@@ -408,7 +406,7 @@ impl ActiveCall {
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Result<()> {
|
||||
if let Some((room, _)) = self.room.as_ref() {
|
||||
self.report_call_event("Project Unshared", cx);
|
||||
self.report_call_event("unshare project", cx);
|
||||
room.update(cx, |room, cx| room.unshare_project(project, cx))
|
||||
} else {
|
||||
Err(anyhow!("no active call"))
|
||||
@@ -488,15 +486,35 @@ impl ActiveCall {
|
||||
pub fn report_call_event(&self, operation: &'static str, cx: &mut AppContext) {
|
||||
if let Some(room) = self.room() {
|
||||
let room = room.read(cx);
|
||||
telemetry::event!(
|
||||
operation,
|
||||
room_id = room.id(),
|
||||
channel_id = room.channel_id()
|
||||
)
|
||||
report_call_event_for_room(operation, room.id(), room.channel_id(), &self.client);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn report_call_event_for_room(
|
||||
operation: &'static str,
|
||||
room_id: u64,
|
||||
channel_id: Option<ChannelId>,
|
||||
client: &Arc<Client>,
|
||||
) {
|
||||
let telemetry = client.telemetry();
|
||||
|
||||
telemetry.report_call_event(operation, Some(room_id), channel_id)
|
||||
}
|
||||
|
||||
pub fn report_call_event_for_channel(
|
||||
operation: &'static str,
|
||||
channel_id: ChannelId,
|
||||
client: &Arc<Client>,
|
||||
cx: &AppContext,
|
||||
) {
|
||||
let room = ActiveCall::global(cx).read(cx).room();
|
||||
|
||||
let telemetry = client.telemetry();
|
||||
|
||||
telemetry.report_call_event(operation, room.map(|r| r.read(cx).id()), Some(channel_id))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use gpui::TestAppContext;
|
||||
|
||||
@@ -1288,12 +1288,6 @@ impl Room {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn muted_by_user(&self) -> bool {
|
||||
self.live_kit
|
||||
.as_ref()
|
||||
.map_or(false, |live_kit| live_kit.muted_by_user)
|
||||
}
|
||||
|
||||
pub fn is_speaking(&self) -> bool {
|
||||
self.live_kit
|
||||
.as_ref()
|
||||
|
||||
@@ -243,9 +243,7 @@ impl ActiveCall {
|
||||
cx.spawn(move |this, mut cx| async move {
|
||||
let result = invite.await;
|
||||
if result.is_ok() {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.report_call_event("Participant Invited", cx)
|
||||
})?;
|
||||
this.update(&mut cx, |this, cx| this.report_call_event("invite", cx))?;
|
||||
} else {
|
||||
//TODO: report collaboration error
|
||||
log::error!("invite failed: {:?}", result);
|
||||
@@ -313,7 +311,7 @@ impl ActiveCall {
|
||||
this.update(&mut cx, |this, cx| this.set_room(room.clone(), cx))?
|
||||
.await?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.report_call_event("Incoming Call Accepted", cx)
|
||||
this.report_call_event("accept incoming", cx)
|
||||
})?;
|
||||
Ok(())
|
||||
})
|
||||
@@ -326,7 +324,7 @@ impl ActiveCall {
|
||||
.borrow_mut()
|
||||
.take()
|
||||
.ok_or_else(|| anyhow!("no incoming call"))?;
|
||||
telemetry::event!("Incoming Call Declined", room_id = call.room_id);
|
||||
report_call_event_for_room("decline incoming", call.room_id, None, &self.client);
|
||||
self.client.send(proto::DeclineCall {
|
||||
room_id: call.room_id,
|
||||
})?;
|
||||
@@ -361,7 +359,7 @@ impl ActiveCall {
|
||||
this.update(&mut cx, |this, cx| this.set_room(room.clone(), cx))?
|
||||
.await?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.report_call_event("Channel Joined", cx)
|
||||
this.report_call_event("join channel", cx)
|
||||
})?;
|
||||
Ok(room)
|
||||
})
|
||||
@@ -369,7 +367,7 @@ impl ActiveCall {
|
||||
|
||||
pub fn hang_up(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
|
||||
cx.notify();
|
||||
self.report_call_event("Call Ended", cx);
|
||||
self.report_call_event("hang up", cx);
|
||||
|
||||
Audio::end_call(cx);
|
||||
|
||||
@@ -388,7 +386,7 @@ impl ActiveCall {
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<u64>> {
|
||||
if let Some((room, _)) = self.room.as_ref() {
|
||||
self.report_call_event("Project Shared", cx);
|
||||
self.report_call_event("share project", cx);
|
||||
room.update(cx, |room, cx| room.share_project(project, cx))
|
||||
} else {
|
||||
Task::ready(Err(anyhow!("no active call")))
|
||||
@@ -401,7 +399,7 @@ impl ActiveCall {
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Result<()> {
|
||||
if let Some((room, _)) = self.room.as_ref() {
|
||||
self.report_call_event("Project Unshared", cx);
|
||||
self.report_call_event("unshare project", cx);
|
||||
room.update(cx, |room, cx| room.unshare_project(project, cx))
|
||||
} else {
|
||||
Err(anyhow!("no active call"))
|
||||
@@ -481,15 +479,35 @@ impl ActiveCall {
|
||||
pub fn report_call_event(&self, operation: &'static str, cx: &mut AppContext) {
|
||||
if let Some(room) = self.room() {
|
||||
let room = room.read(cx);
|
||||
telemetry::event!(
|
||||
operation,
|
||||
room_id = room.id(),
|
||||
channel_id = room.channel_id()
|
||||
);
|
||||
report_call_event_for_room(operation, room.id(), room.channel_id(), &self.client);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn report_call_event_for_room(
|
||||
operation: &'static str,
|
||||
room_id: u64,
|
||||
channel_id: Option<ChannelId>,
|
||||
client: &Arc<Client>,
|
||||
) {
|
||||
let telemetry = client.telemetry();
|
||||
|
||||
telemetry.report_call_event(operation, Some(room_id), channel_id)
|
||||
}
|
||||
|
||||
pub fn report_call_event_for_channel(
|
||||
operation: &'static str,
|
||||
channel_id: ChannelId,
|
||||
client: &Arc<Client>,
|
||||
cx: &AppContext,
|
||||
) {
|
||||
let room = ActiveCall::global(cx).read(cx).room();
|
||||
|
||||
let telemetry = client.telemetry();
|
||||
|
||||
telemetry.report_call_event(operation, room.map(|r| r.read(cx).id()), Some(channel_id))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use gpui::TestAppContext;
|
||||
|
||||
@@ -1307,12 +1307,6 @@ impl Room {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn muted_by_user(&self) -> bool {
|
||||
self.live_kit
|
||||
.as_ref()
|
||||
.map_or(false, |live_kit| live_kit.muted_by_user)
|
||||
}
|
||||
|
||||
pub fn is_speaking(&self) -> bool {
|
||||
self.live_kit
|
||||
.as_ref()
|
||||
|
||||
@@ -51,7 +51,6 @@ tokio-socks = { version = "0.5.2", default-features = false, features = ["future
|
||||
url.workspace = true
|
||||
util.workspace = true
|
||||
worktree.workspace = true
|
||||
telemetry.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
clock = { workspace = true, features = ["test-support"] }
|
||||
|
||||
@@ -4,8 +4,7 @@ use crate::{ChannelId, TelemetrySettings};
|
||||
use anyhow::Result;
|
||||
use clock::SystemClock;
|
||||
use collections::{HashMap, HashSet};
|
||||
use futures::channel::mpsc;
|
||||
use futures::{Future, StreamExt};
|
||||
use futures::Future;
|
||||
use gpui::{AppContext, BackgroundExecutor, Task};
|
||||
use http_client::{self, AsyncBody, HttpClient, HttpClientWithUrl, Method, Request};
|
||||
use once_cell::sync::Lazy;
|
||||
@@ -18,8 +17,9 @@ use std::io::Write;
|
||||
use std::time::Instant;
|
||||
use std::{env, mem, path::PathBuf, sync::Arc, time::Duration};
|
||||
use telemetry_events::{
|
||||
AppEvent, AssistantEvent, CallEvent, EditEvent, Event, EventRequestBody, EventWrapper,
|
||||
InlineCompletionEvent,
|
||||
ActionEvent, AppEvent, AssistantEvent, CallEvent, EditEvent, EditorEvent, Event,
|
||||
EventRequestBody, EventWrapper, ExtensionEvent, InlineCompletionEvent, InlineCompletionRating,
|
||||
InlineCompletionRatingEvent, ReplEvent, SettingEvent,
|
||||
};
|
||||
use util::{ResultExt, TryFutureExt};
|
||||
use worktree::{UpdatedEntriesSet, WorktreeId};
|
||||
@@ -245,6 +245,7 @@ impl Telemetry {
|
||||
})
|
||||
.detach();
|
||||
|
||||
// TODO: Replace all hardware stuff with nested SystemSpecs json
|
||||
let this = Arc::new(Self {
|
||||
clock,
|
||||
http_client: client,
|
||||
@@ -252,21 +253,6 @@ impl Telemetry {
|
||||
state,
|
||||
});
|
||||
|
||||
let (tx, mut rx) = mpsc::unbounded();
|
||||
::telemetry::init(tx);
|
||||
|
||||
cx.background_executor()
|
||||
.spawn({
|
||||
let this = Arc::downgrade(&this);
|
||||
async move {
|
||||
while let Some(event) = rx.next().await {
|
||||
let Some(state) = this.upgrade() else { break };
|
||||
state.report_event(Event::Flexible(event))
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
// We should only ever have one instance of Telemetry, leak the subscription to keep it alive
|
||||
// rather than store in TelemetryState, complicating spawn as subscriptions are not Send
|
||||
std::mem::forget(cx.on_app_quit({
|
||||
@@ -334,6 +320,27 @@ impl Telemetry {
|
||||
drop(state);
|
||||
}
|
||||
|
||||
pub fn report_editor_event(
|
||||
self: &Arc<Self>,
|
||||
file_extension: Option<String>,
|
||||
vim_mode: bool,
|
||||
operation: &'static str,
|
||||
copilot_enabled: bool,
|
||||
copilot_enabled_for_language: bool,
|
||||
is_via_ssh: bool,
|
||||
) {
|
||||
let event = Event::Editor(EditorEvent {
|
||||
file_extension,
|
||||
vim_mode,
|
||||
operation: operation.into(),
|
||||
copilot_enabled,
|
||||
copilot_enabled_for_language,
|
||||
is_via_ssh,
|
||||
});
|
||||
|
||||
self.report_event(event)
|
||||
}
|
||||
|
||||
pub fn report_inline_completion_event(
|
||||
self: &Arc<Self>,
|
||||
provider: String,
|
||||
@@ -349,6 +356,24 @@ impl Telemetry {
|
||||
self.report_event(event)
|
||||
}
|
||||
|
||||
pub fn report_inline_completion_rating_event(
|
||||
self: &Arc<Self>,
|
||||
rating: InlineCompletionRating,
|
||||
input_events: Arc<str>,
|
||||
input_excerpt: Arc<str>,
|
||||
output_excerpt: Arc<str>,
|
||||
feedback: String,
|
||||
) {
|
||||
let event = Event::InlineCompletionRating(InlineCompletionRatingEvent {
|
||||
rating,
|
||||
input_events,
|
||||
input_excerpt,
|
||||
output_excerpt,
|
||||
feedback,
|
||||
});
|
||||
self.report_event(event);
|
||||
}
|
||||
|
||||
pub fn report_assistant_event(self: &Arc<Self>, event: AssistantEvent) {
|
||||
self.report_event(Event::Assistant(event));
|
||||
}
|
||||
@@ -376,6 +401,22 @@ impl Telemetry {
|
||||
event
|
||||
}
|
||||
|
||||
pub fn report_setting_event(self: &Arc<Self>, setting: &'static str, value: String) {
|
||||
let event = Event::Setting(SettingEvent {
|
||||
setting: setting.to_string(),
|
||||
value,
|
||||
});
|
||||
|
||||
self.report_event(event)
|
||||
}
|
||||
|
||||
pub fn report_extension_event(self: &Arc<Self>, extension_id: Arc<str>, version: Arc<str>) {
|
||||
self.report_event(Event::Extension(ExtensionEvent {
|
||||
extension_id,
|
||||
version,
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn log_edit_event(self: &Arc<Self>, environment: &'static str, is_via_ssh: bool) {
|
||||
let mut state = self.state.lock();
|
||||
let period_data = state.event_coalescer.log_event(environment);
|
||||
@@ -395,6 +436,15 @@ impl Telemetry {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn report_action_event(self: &Arc<Self>, source: &'static str, action: String) {
|
||||
let event = Event::Action(ActionEvent {
|
||||
source: source.to_string(),
|
||||
action,
|
||||
});
|
||||
|
||||
self.report_event(event)
|
||||
}
|
||||
|
||||
pub fn report_discovered_project_events(
|
||||
self: &Arc<Self>,
|
||||
worktree_id: WorktreeId,
|
||||
@@ -441,6 +491,21 @@ impl Telemetry {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn report_repl_event(
|
||||
self: &Arc<Self>,
|
||||
kernel_language: String,
|
||||
kernel_status: String,
|
||||
repl_session_id: String,
|
||||
) {
|
||||
let event = Event::Repl(ReplEvent {
|
||||
kernel_language,
|
||||
kernel_status,
|
||||
repl_session_id,
|
||||
});
|
||||
|
||||
self.report_event(event)
|
||||
}
|
||||
|
||||
fn report_event(self: &Arc<Self>, event: Event) {
|
||||
let mut state = self.state.lock();
|
||||
|
||||
|
||||
@@ -16,9 +16,7 @@ use util::TryFutureExt as _;
|
||||
|
||||
pub type UserId = u64;
|
||||
|
||||
#[derive(
|
||||
Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, serde::Serialize, serde::Deserialize,
|
||||
)]
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
|
||||
pub struct ChannelId(pub u64);
|
||||
|
||||
impl std::fmt::Display for ChannelId {
|
||||
|
||||
@@ -8,12 +8,7 @@ It contains our back-end logic for collaboration, to which we connect from the Z
|
||||
|
||||
## Database setup
|
||||
|
||||
Before you can run the collab server locally, you'll need to set up a zed Postgres database. Follow the steps sequentially:
|
||||
|
||||
1. Ensure you have postgres installed. If not, install with `brew install postgresql@15`.
|
||||
2. Follow the steps on Brew's formula and verify your `$PATH` contains `/opt/homebrew/opt/postgresql@15/bin`.
|
||||
3. If you hadn't done it before, create the `postgres` user with `createuser -s postgres`.
|
||||
4. You are now ready to run the `bootstrap` script:
|
||||
Before you can run the collab server locally, you'll need to set up a zed Postgres database.
|
||||
|
||||
```sh
|
||||
script/bootstrap
|
||||
|
||||
@@ -610,10 +610,6 @@ fn for_snowflake(
|
||||
"Kernel Status Changed".to_string(),
|
||||
serde_json::to_value(e).unwrap(),
|
||||
),
|
||||
Event::Flexible(e) => (
|
||||
e.event_type.clone(),
|
||||
serde_json::to_value(&e.event_properties).unwrap(),
|
||||
),
|
||||
};
|
||||
|
||||
if let serde_json::Value::Object(ref mut map) = event_properties {
|
||||
|
||||
@@ -310,9 +310,6 @@ impl Server {
|
||||
.add_request_handler(forward_read_only_project_request::<proto::OpenBufferByPath>)
|
||||
.add_request_handler(forward_read_only_project_request::<proto::GitBranches>)
|
||||
.add_request_handler(forward_read_only_project_request::<proto::GetStagedText>)
|
||||
.add_request_handler(
|
||||
forward_mutating_project_request::<proto::RegisterBufferWithLanguageServers>,
|
||||
)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::UpdateGitBranch>)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::GetCompletions>)
|
||||
.add_request_handler(
|
||||
|
||||
@@ -994,12 +994,10 @@ async fn test_language_server_statuses(cx_a: &mut TestAppContext, cx_b: &mut Tes
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
let (project_a, _) = client_a.build_local_project("/dir", cx_a).await;
|
||||
let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
|
||||
|
||||
let _buffer_a = project_a
|
||||
.update(cx_a, |p, cx| {
|
||||
p.open_local_buffer_with_lsp("/dir/main.rs", cx)
|
||||
})
|
||||
.update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -1589,6 +1587,7 @@ async fn test_mutual_editor_inlay_hint_cache_update(
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let fake_language_server = fake_language_servers.next().await.unwrap();
|
||||
let editor_a = workspace_a
|
||||
.update(cx_a, |workspace, cx| {
|
||||
workspace.open_path((worktree_id, "main.rs"), None, true, cx)
|
||||
@@ -1598,8 +1597,6 @@ async fn test_mutual_editor_inlay_hint_cache_update(
|
||||
.downcast::<Editor>()
|
||||
.unwrap();
|
||||
|
||||
let fake_language_server = fake_language_servers.next().await.unwrap();
|
||||
|
||||
// Set up the language server to return an additional inlay hint on each request.
|
||||
let edits_made = Arc::new(AtomicUsize::new(0));
|
||||
let closure_edits_made = Arc::clone(&edits_made);
|
||||
|
||||
@@ -3891,7 +3891,13 @@ async fn test_collaborating_with_diagnostics(
|
||||
// Cause the language server to start.
|
||||
let _buffer = project_a
|
||||
.update(cx_a, |project, cx| {
|
||||
project.open_local_buffer_with_lsp("/a/other.rs", cx)
|
||||
project.open_buffer(
|
||||
ProjectPath {
|
||||
worktree_id,
|
||||
path: Path::new("other.rs").into(),
|
||||
},
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -4170,9 +4176,7 @@ async fn test_collaborating_with_lsp_progress_updates_and_diagnostics_ordering(
|
||||
// Join the project as client B and open all three files.
|
||||
let project_b = client_b.join_remote_project(project_id, cx_b).await;
|
||||
let guest_buffers = futures::future::try_join_all(file_names.iter().map(|file_name| {
|
||||
project_b.update(cx_b, |p, cx| {
|
||||
p.open_buffer_with_lsp((worktree_id, file_name), cx)
|
||||
})
|
||||
project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, file_name), cx))
|
||||
}))
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -4226,7 +4230,7 @@ async fn test_collaborating_with_lsp_progress_updates_and_diagnostics_ordering(
|
||||
cx.subscribe(&project_b, move |_, _, event, cx| {
|
||||
if let project::Event::DiskBasedDiagnosticsFinished { .. } = event {
|
||||
disk_based_diagnostics_finished.store(true, SeqCst);
|
||||
for (buffer, _) in &guest_buffers {
|
||||
for buffer in &guest_buffers {
|
||||
assert_eq!(
|
||||
buffer
|
||||
.read(cx)
|
||||
@@ -4347,6 +4351,7 @@ async fn test_formatting_buffer(
|
||||
cx_a: &mut TestAppContext,
|
||||
cx_b: &mut TestAppContext,
|
||||
) {
|
||||
executor.allow_parking();
|
||||
let mut server = TestServer::start(executor.clone()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
@@ -4374,16 +4379,10 @@ async fn test_formatting_buffer(
|
||||
.await
|
||||
.unwrap();
|
||||
let project_b = client_b.join_remote_project(project_id, cx_b).await;
|
||||
let lsp_store_b = project_b.update(cx_b, |p, _| p.lsp_store());
|
||||
|
||||
let buffer_b = project_b
|
||||
.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx));
|
||||
let buffer_b = cx_b.executor().spawn(open_buffer).await.unwrap();
|
||||
|
||||
let _handle = lsp_store_b.update(cx_b, |lsp_store, cx| {
|
||||
lsp_store.register_buffer_with_language_servers(&buffer_b, cx)
|
||||
});
|
||||
let fake_language_server = fake_language_servers.next().await.unwrap();
|
||||
fake_language_server.handle_request::<lsp::request::Formatting, _, _>(|_, _| async move {
|
||||
Ok(Some(vec![
|
||||
@@ -4432,8 +4431,6 @@ async fn test_formatting_buffer(
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
executor.allow_parking();
|
||||
project_b
|
||||
.update(cx_b, |project, cx| {
|
||||
project.format(
|
||||
@@ -4506,12 +4503,8 @@ async fn test_prettier_formatting_buffer(
|
||||
.await
|
||||
.unwrap();
|
||||
let project_b = client_b.join_remote_project(project_id, cx_b).await;
|
||||
let (buffer_b, _) = project_b
|
||||
.update(cx_b, |p, cx| {
|
||||
p.open_buffer_with_lsp((worktree_id, "a.ts"), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.ts"), cx));
|
||||
let buffer_b = cx_b.executor().spawn(open_buffer).await.unwrap();
|
||||
|
||||
cx_a.update(|cx| {
|
||||
SettingsStore::update_global(cx, |store, cx| {
|
||||
@@ -4627,12 +4620,8 @@ async fn test_definition(
|
||||
let project_b = client_b.join_remote_project(project_id, cx_b).await;
|
||||
|
||||
// Open the file on client B.
|
||||
let (buffer_b, _handle) = project_b
|
||||
.update(cx_b, |p, cx| {
|
||||
p.open_buffer_with_lsp((worktree_id, "a.rs"), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx));
|
||||
let buffer_b = cx_b.executor().spawn(open_buffer).await.unwrap();
|
||||
|
||||
// Request the definition of a symbol as the guest.
|
||||
let fake_language_server = fake_language_servers.next().await.unwrap();
|
||||
@@ -4776,12 +4765,8 @@ async fn test_references(
|
||||
let project_b = client_b.join_remote_project(project_id, cx_b).await;
|
||||
|
||||
// Open the file on client B.
|
||||
let (buffer_b, _handle) = project_b
|
||||
.update(cx_b, |p, cx| {
|
||||
p.open_buffer_with_lsp((worktree_id, "one.rs"), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "one.rs"), cx));
|
||||
let buffer_b = cx_b.executor().spawn(open_buffer).await.unwrap();
|
||||
|
||||
// Request references to a symbol as the guest.
|
||||
let fake_language_server = fake_language_servers.next().await.unwrap();
|
||||
@@ -5027,12 +5012,8 @@ async fn test_document_highlights(
|
||||
let project_b = client_b.join_remote_project(project_id, cx_b).await;
|
||||
|
||||
// Open the file on client B.
|
||||
let (buffer_b, _handle) = project_b
|
||||
.update(cx_b, |p, cx| {
|
||||
p.open_buffer_with_lsp((worktree_id, "main.rs"), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let open_b = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx));
|
||||
let buffer_b = cx_b.executor().spawn(open_b).await.unwrap();
|
||||
|
||||
// Request document highlights as the guest.
|
||||
let fake_language_server = fake_language_servers.next().await.unwrap();
|
||||
@@ -5149,12 +5130,8 @@ async fn test_lsp_hover(
|
||||
let project_b = client_b.join_remote_project(project_id, cx_b).await;
|
||||
|
||||
// Open the file as the guest
|
||||
let (buffer_b, _handle) = project_b
|
||||
.update(cx_b, |p, cx| {
|
||||
p.open_buffer_with_lsp((worktree_id, "main.rs"), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx));
|
||||
let buffer_b = cx_b.executor().spawn(open_buffer).await.unwrap();
|
||||
|
||||
let mut servers_with_hover_requests = HashMap::default();
|
||||
for i in 0..language_server_names.len() {
|
||||
@@ -5329,12 +5306,9 @@ async fn test_project_symbols(
|
||||
let project_b = client_b.join_remote_project(project_id, cx_b).await;
|
||||
|
||||
// Cause the language server to start.
|
||||
let _buffer = project_b
|
||||
.update(cx_b, |p, cx| {
|
||||
p.open_buffer_with_lsp((worktree_id, "one.rs"), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let open_buffer_task =
|
||||
project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "one.rs"), cx));
|
||||
let _buffer = cx_b.executor().spawn(open_buffer_task).await.unwrap();
|
||||
|
||||
let fake_language_server = fake_language_servers.next().await.unwrap();
|
||||
fake_language_server.handle_request::<lsp::WorkspaceSymbolRequest, _, _>(|_, _| async move {
|
||||
@@ -5426,12 +5400,8 @@ async fn test_open_buffer_while_getting_definition_pointing_to_it(
|
||||
.unwrap();
|
||||
let project_b = client_b.join_remote_project(project_id, cx_b).await;
|
||||
|
||||
let (buffer_b1, _lsp) = project_b
|
||||
.update(cx_b, |p, cx| {
|
||||
p.open_buffer_with_lsp((worktree_id, "a.rs"), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let open_buffer_task = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx));
|
||||
let buffer_b1 = cx_b.executor().spawn(open_buffer_task).await.unwrap();
|
||||
|
||||
let fake_language_server = fake_language_servers.next().await.unwrap();
|
||||
fake_language_server.handle_request::<lsp::request::GotoDefinition, _, _>(|_, _| async move {
|
||||
@@ -5447,22 +5417,13 @@ async fn test_open_buffer_while_getting_definition_pointing_to_it(
|
||||
let buffer_b2;
|
||||
if rng.gen() {
|
||||
definitions = project_b.update(cx_b, |p, cx| p.definition(&buffer_b1, 23, cx));
|
||||
(buffer_b2, _) = project_b
|
||||
.update(cx_b, |p, cx| {
|
||||
p.open_buffer_with_lsp((worktree_id, "b.rs"), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
buffer_b2 = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "b.rs"), cx));
|
||||
} else {
|
||||
(buffer_b2, _) = project_b
|
||||
.update(cx_b, |p, cx| {
|
||||
p.open_buffer_with_lsp((worktree_id, "b.rs"), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
buffer_b2 = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "b.rs"), cx));
|
||||
definitions = project_b.update(cx_b, |p, cx| p.definition(&buffer_b1, 23, cx));
|
||||
}
|
||||
|
||||
let buffer_b2 = buffer_b2.await.unwrap();
|
||||
let definitions = definitions.await.unwrap();
|
||||
assert_eq!(definitions.len(), 1);
|
||||
assert_eq!(definitions[0].target.buffer, buffer_b2);
|
||||
|
||||
@@ -426,10 +426,8 @@ async fn test_ssh_collaboration_formatting_with_prettier(
|
||||
executor.run_until_parked();
|
||||
|
||||
// Opens the buffer and formats it
|
||||
let (buffer_b, _handle) = project_b
|
||||
.update(cx_b, |p, cx| {
|
||||
p.open_buffer_with_lsp((worktree_id, "a.ts"), cx)
|
||||
})
|
||||
let buffer_b = project_b
|
||||
.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.ts"), cx))
|
||||
.await
|
||||
.expect("user B opens buffer for formatting");
|
||||
|
||||
|
||||
@@ -44,6 +44,7 @@ gpui.workspace = true
|
||||
language.workspace = true
|
||||
menu.workspace = true
|
||||
notifications.workspace = true
|
||||
parking_lot.workspace = true
|
||||
picker.workspace = true
|
||||
project.workspace = true
|
||||
release_channel.workspace = true
|
||||
@@ -56,7 +57,6 @@ serde_json.workspace = true
|
||||
settings.workspace = true
|
||||
smallvec.workspace = true
|
||||
story = { workspace = true, optional = true }
|
||||
telemetry.workspace = true
|
||||
theme.workspace = true
|
||||
time.workspace = true
|
||||
time_format.workspace = true
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use anyhow::Result;
|
||||
use call::ActiveCall;
|
||||
use call::report_call_event_for_channel;
|
||||
use channel::{Channel, ChannelBuffer, ChannelBufferEvent, ChannelStore};
|
||||
use client::{
|
||||
proto::{self, PeerId},
|
||||
@@ -66,13 +66,11 @@ impl ChannelView {
|
||||
cx.spawn(|mut cx| async move {
|
||||
let channel_view = channel_view.await?;
|
||||
pane.update(&mut cx, |pane, cx| {
|
||||
telemetry::event!(
|
||||
"Channel Notes Opened",
|
||||
report_call_event_for_channel(
|
||||
"open channel notes",
|
||||
channel_id,
|
||||
room_id = ActiveCall::global(cx)
|
||||
.read(cx)
|
||||
.room()
|
||||
.map(|r| r.read(cx).id())
|
||||
&workspace.read(cx).app_state().client,
|
||||
cx,
|
||||
);
|
||||
pane.add_item(Box::new(channel_view.clone()), true, true, None, cx);
|
||||
})?;
|
||||
|
||||
@@ -12,15 +12,10 @@ use language::{
|
||||
language_settings::SoftWrap, Anchor, Buffer, BufferSnapshot, CodeLabel, LanguageRegistry,
|
||||
LanguageServerId, ToOffset,
|
||||
};
|
||||
use parking_lot::RwLock;
|
||||
use project::{search::SearchQuery, Completion};
|
||||
use settings::Settings;
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
ops::Range,
|
||||
rc::Rc,
|
||||
sync::{Arc, LazyLock},
|
||||
time::Duration,
|
||||
};
|
||||
use std::{ops::Range, sync::Arc, sync::LazyLock, time::Duration};
|
||||
use theme::ThemeSettings;
|
||||
use ui::{prelude::*, TextSize};
|
||||
|
||||
@@ -73,12 +68,22 @@ impl CompletionProvider for MessageEditorCompletionProvider {
|
||||
&self,
|
||||
_buffer: Model<Buffer>,
|
||||
_completion_indices: Vec<usize>,
|
||||
_completions: Rc<RefCell<Box<[Completion]>>>,
|
||||
_completions: Arc<RwLock<Box<[Completion]>>>,
|
||||
_cx: &mut ViewContext<Editor>,
|
||||
) -> Task<anyhow::Result<bool>> {
|
||||
Task::ready(Ok(false))
|
||||
}
|
||||
|
||||
fn apply_additional_edits_for_completion(
|
||||
&self,
|
||||
_buffer: Model<Buffer>,
|
||||
_completion: Completion,
|
||||
_push_to_history: bool,
|
||||
_cx: &mut ViewContext<Editor>,
|
||||
) -> Task<Result<Option<language::Transaction>>> {
|
||||
Task::ready(Ok(None))
|
||||
}
|
||||
|
||||
fn is_completion_trigger(
|
||||
&self,
|
||||
_buffer: &Model<Buffer>,
|
||||
@@ -309,7 +314,6 @@ impl MessageEditor {
|
||||
server_id: LanguageServerId(0), // TODO: Make this optional or something?
|
||||
lsp_completion: Default::default(), // TODO: Make this optional or something?
|
||||
confirm: None,
|
||||
resolved: true,
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
@@ -377,7 +381,11 @@ impl MessageEditor {
|
||||
|
||||
let candidates = names
|
||||
.into_iter()
|
||||
.map(|user| StringMatchCandidate::new(0, &user))
|
||||
.map(|user| StringMatchCandidate {
|
||||
id: 0,
|
||||
string: user.clone(),
|
||||
char_bag: user.chars().collect(),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Some((start_anchor, query, candidates))
|
||||
@@ -393,7 +401,11 @@ impl MessageEditor {
|
||||
LazyLock::new(|| {
|
||||
let emojis = emojis::iter()
|
||||
.flat_map(|s| s.shortcodes())
|
||||
.map(|emoji| StringMatchCandidate::new(0, emoji))
|
||||
.map(|emoji| StringMatchCandidate {
|
||||
id: 0,
|
||||
string: emoji.to_string(),
|
||||
char_bag: emoji.chars().collect(),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
emojis
|
||||
});
|
||||
|
||||
@@ -393,8 +393,11 @@ impl CollabPanel {
|
||||
// Populate the active user.
|
||||
if let Some(user) = user_store.current_user() {
|
||||
self.match_candidates.clear();
|
||||
self.match_candidates
|
||||
.push(StringMatchCandidate::new(0, &user.github_login));
|
||||
self.match_candidates.push(StringMatchCandidate {
|
||||
id: 0,
|
||||
string: user.github_login.clone(),
|
||||
char_bag: user.github_login.chars().collect(),
|
||||
});
|
||||
let matches = executor.block(match_strings(
|
||||
&self.match_candidates,
|
||||
&query,
|
||||
@@ -433,10 +436,11 @@ impl CollabPanel {
|
||||
self.match_candidates.clear();
|
||||
self.match_candidates
|
||||
.extend(room.remote_participants().values().map(|participant| {
|
||||
StringMatchCandidate::new(
|
||||
participant.user.id as usize,
|
||||
&participant.user.github_login,
|
||||
)
|
||||
StringMatchCandidate {
|
||||
id: participant.user.id as usize,
|
||||
string: participant.user.github_login.clone(),
|
||||
char_bag: participant.user.github_login.chars().collect(),
|
||||
}
|
||||
}));
|
||||
let mut matches = executor.block(match_strings(
|
||||
&self.match_candidates,
|
||||
@@ -485,8 +489,10 @@ impl CollabPanel {
|
||||
self.match_candidates.clear();
|
||||
self.match_candidates
|
||||
.extend(room.pending_participants().iter().enumerate().map(
|
||||
|(id, participant)| {
|
||||
StringMatchCandidate::new(id, &participant.github_login)
|
||||
|(id, participant)| StringMatchCandidate {
|
||||
id,
|
||||
string: participant.github_login.clone(),
|
||||
char_bag: participant.github_login.chars().collect(),
|
||||
},
|
||||
));
|
||||
let matches = executor.block(match_strings(
|
||||
@@ -513,12 +519,17 @@ impl CollabPanel {
|
||||
|
||||
if channel_store.channel_count() > 0 || self.channel_editing_state.is_some() {
|
||||
self.match_candidates.clear();
|
||||
self.match_candidates.extend(
|
||||
channel_store
|
||||
.ordered_channels()
|
||||
.enumerate()
|
||||
.map(|(ix, (_, channel))| StringMatchCandidate::new(ix, &channel.name)),
|
||||
);
|
||||
self.match_candidates
|
||||
.extend(
|
||||
channel_store
|
||||
.ordered_channels()
|
||||
.enumerate()
|
||||
.map(|(ix, (_, channel))| StringMatchCandidate {
|
||||
id: ix,
|
||||
string: channel.name.clone().into(),
|
||||
char_bag: channel.name.chars().collect(),
|
||||
}),
|
||||
);
|
||||
let matches = executor.block(match_strings(
|
||||
&self.match_candidates,
|
||||
&query,
|
||||
@@ -589,12 +600,14 @@ impl CollabPanel {
|
||||
let channel_invites = channel_store.channel_invitations();
|
||||
if !channel_invites.is_empty() {
|
||||
self.match_candidates.clear();
|
||||
self.match_candidates.extend(
|
||||
channel_invites
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(ix, channel)| StringMatchCandidate::new(ix, &channel.name)),
|
||||
);
|
||||
self.match_candidates
|
||||
.extend(channel_invites.iter().enumerate().map(|(ix, channel)| {
|
||||
StringMatchCandidate {
|
||||
id: ix,
|
||||
string: channel.name.clone().into(),
|
||||
char_bag: channel.name.chars().collect(),
|
||||
}
|
||||
}));
|
||||
let matches = executor.block(match_strings(
|
||||
&self.match_candidates,
|
||||
&query,
|
||||
@@ -624,12 +637,17 @@ impl CollabPanel {
|
||||
let incoming = user_store.incoming_contact_requests();
|
||||
if !incoming.is_empty() {
|
||||
self.match_candidates.clear();
|
||||
self.match_candidates.extend(
|
||||
incoming
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(ix, user)| StringMatchCandidate::new(ix, &user.github_login)),
|
||||
);
|
||||
self.match_candidates
|
||||
.extend(
|
||||
incoming
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(ix, user)| StringMatchCandidate {
|
||||
id: ix,
|
||||
string: user.github_login.clone(),
|
||||
char_bag: user.github_login.chars().collect(),
|
||||
}),
|
||||
);
|
||||
let matches = executor.block(match_strings(
|
||||
&self.match_candidates,
|
||||
&query,
|
||||
@@ -648,12 +666,17 @@ impl CollabPanel {
|
||||
let outgoing = user_store.outgoing_contact_requests();
|
||||
if !outgoing.is_empty() {
|
||||
self.match_candidates.clear();
|
||||
self.match_candidates.extend(
|
||||
outgoing
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(ix, user)| StringMatchCandidate::new(ix, &user.github_login)),
|
||||
);
|
||||
self.match_candidates
|
||||
.extend(
|
||||
outgoing
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(ix, user)| StringMatchCandidate {
|
||||
id: ix,
|
||||
string: user.github_login.clone(),
|
||||
char_bag: user.github_login.chars().collect(),
|
||||
}),
|
||||
);
|
||||
let matches = executor.block(match_strings(
|
||||
&self.match_candidates,
|
||||
&query,
|
||||
@@ -680,12 +703,17 @@ impl CollabPanel {
|
||||
let contacts = user_store.contacts();
|
||||
if !contacts.is_empty() {
|
||||
self.match_candidates.clear();
|
||||
self.match_candidates.extend(
|
||||
contacts
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(ix, contact)| StringMatchCandidate::new(ix, &contact.user.github_login)),
|
||||
);
|
||||
self.match_candidates
|
||||
.extend(
|
||||
contacts
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(ix, contact)| StringMatchCandidate {
|
||||
id: ix,
|
||||
string: contact.user.github_login.clone(),
|
||||
char_bag: contact.user.github_login.chars().collect(),
|
||||
}),
|
||||
);
|
||||
|
||||
let matches = executor.block(match_strings(
|
||||
&self.match_candidates,
|
||||
@@ -813,7 +841,7 @@ impl CollabPanel {
|
||||
ListItem::new(SharedString::from(user.github_login.clone()))
|
||||
.start_slot(Avatar::new(user.avatar_uri.clone()))
|
||||
.child(Label::new(user.github_login.clone()))
|
||||
.toggle_state(is_selected)
|
||||
.selected(is_selected)
|
||||
.end_slot(if is_pending {
|
||||
Label::new("Calling").color(Color::Muted).into_any_element()
|
||||
} else if is_current_user {
|
||||
@@ -866,7 +894,7 @@ impl CollabPanel {
|
||||
.into();
|
||||
|
||||
ListItem::new(project_id as usize)
|
||||
.toggle_state(is_selected)
|
||||
.selected(is_selected)
|
||||
.on_click(cx.listener(move |this, _, cx| {
|
||||
this.workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
@@ -896,7 +924,7 @@ impl CollabPanel {
|
||||
let id = peer_id.map_or(usize::MAX, |id| id.as_u64() as usize);
|
||||
|
||||
ListItem::new(("screen", id))
|
||||
.toggle_state(is_selected)
|
||||
.selected(is_selected)
|
||||
.start_slot(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
@@ -936,7 +964,7 @@ impl CollabPanel {
|
||||
let channel_store = self.channel_store.read(cx);
|
||||
let has_channel_buffer_changed = channel_store.has_channel_buffer_changed(channel_id);
|
||||
ListItem::new("channel-notes")
|
||||
.toggle_state(is_selected)
|
||||
.selected(is_selected)
|
||||
.on_click(cx.listener(move |this, _, cx| {
|
||||
this.open_channel_notes(channel_id, cx);
|
||||
}))
|
||||
@@ -968,7 +996,7 @@ impl CollabPanel {
|
||||
let channel_store = self.channel_store.read(cx);
|
||||
let has_messages_notification = channel_store.has_new_messages(channel_id);
|
||||
ListItem::new("channel-chat")
|
||||
.toggle_state(is_selected)
|
||||
.selected(is_selected)
|
||||
.on_click(cx.listener(move |this, _, cx| {
|
||||
this.join_channel_chat(channel_id, cx);
|
||||
}))
|
||||
@@ -2225,7 +2253,7 @@ impl CollabPanel {
|
||||
})
|
||||
.inset(true)
|
||||
.end_slot::<AnyElement>(button)
|
||||
.toggle_state(is_selected),
|
||||
.selected(is_selected),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2242,7 +2270,7 @@ impl CollabPanel {
|
||||
let item = ListItem::new(github_login.clone())
|
||||
.indent_level(1)
|
||||
.indent_step_size(px(20.))
|
||||
.toggle_state(is_selected)
|
||||
.selected(is_selected)
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
@@ -2353,7 +2381,7 @@ impl CollabPanel {
|
||||
ListItem::new(github_login.clone())
|
||||
.indent_level(1)
|
||||
.indent_step_size(px(20.))
|
||||
.toggle_state(is_selected)
|
||||
.selected(is_selected)
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
@@ -2397,7 +2425,7 @@ impl CollabPanel {
|
||||
];
|
||||
|
||||
ListItem::new(("channel-invite", channel.id.0 as usize))
|
||||
.toggle_state(is_selected)
|
||||
.selected(is_selected)
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
@@ -2420,7 +2448,7 @@ impl CollabPanel {
|
||||
ListItem::new("contact-placeholder")
|
||||
.child(Icon::new(IconName::Plus))
|
||||
.child(Label::new("Add a Contact"))
|
||||
.toggle_state(is_selected)
|
||||
.selected(is_selected)
|
||||
.on_click(cx.listener(|this, _, cx| this.toggle_contact_finder(cx)))
|
||||
}
|
||||
|
||||
@@ -2519,7 +2547,7 @@ impl CollabPanel {
|
||||
// Add one level of depth for the disclosure arrow.
|
||||
.indent_level(depth + 1)
|
||||
.indent_step_size(px(20.))
|
||||
.toggle_state(is_selected || is_active)
|
||||
.selected(is_selected || is_active)
|
||||
.toggle(disclosed)
|
||||
.on_toggle(
|
||||
cx.listener(move |this, _, cx| {
|
||||
@@ -2708,7 +2736,7 @@ impl Render for CollabPanel {
|
||||
deferred(
|
||||
anchored()
|
||||
.position(*position)
|
||||
.anchor(gpui::Corner::TopLeft)
|
||||
.anchor(gpui::AnchorCorner::TopLeft)
|
||||
.child(menu.clone()),
|
||||
)
|
||||
.with_priority(1)
|
||||
|
||||
@@ -89,15 +89,15 @@ impl ChannelModal {
|
||||
cx.notify()
|
||||
}
|
||||
|
||||
fn set_channel_visibility(&mut self, selection: &ToggleState, cx: &mut ViewContext<Self>) {
|
||||
fn set_channel_visibility(&mut self, selection: &Selection, cx: &mut ViewContext<Self>) {
|
||||
self.channel_store.update(cx, |channel_store, cx| {
|
||||
channel_store
|
||||
.set_channel_visibility(
|
||||
self.channel_id,
|
||||
match selection {
|
||||
ToggleState::Unselected => ChannelVisibility::Members,
|
||||
ToggleState::Selected => ChannelVisibility::Public,
|
||||
ToggleState::Indeterminate => return,
|
||||
Selection::Unselected => ChannelVisibility::Members,
|
||||
Selection::Selected => ChannelVisibility::Public,
|
||||
Selection::Indeterminate => return,
|
||||
},
|
||||
cx,
|
||||
)
|
||||
@@ -159,9 +159,9 @@ impl Render for ChannelModal {
|
||||
"is-public",
|
||||
Label::new("Public").size(LabelSize::Small),
|
||||
if visibility == ChannelVisibility::Public {
|
||||
ui::ToggleState::Selected
|
||||
ui::Selection::Selected
|
||||
} else {
|
||||
ui::ToggleState::Unselected
|
||||
ui::Selection::Unselected
|
||||
},
|
||||
cx.listener(Self::set_channel_visibility),
|
||||
))
|
||||
@@ -272,7 +272,11 @@ impl PickerDelegate for ChannelModalDelegate {
|
||||
self.match_candidates.clear();
|
||||
self.match_candidates
|
||||
.extend(self.members.iter().enumerate().map(|(id, member)| {
|
||||
StringMatchCandidate::new(id, &member.user.github_login)
|
||||
StringMatchCandidate {
|
||||
id,
|
||||
string: member.user.github_login.clone(),
|
||||
char_bag: member.user.github_login.chars().collect(),
|
||||
}
|
||||
}));
|
||||
|
||||
let matches = cx.background_executor().block(match_strings(
|
||||
@@ -382,7 +386,7 @@ impl PickerDelegate for ChannelModalDelegate {
|
||||
ListItem::new(ix)
|
||||
.inset(true)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.toggle_state(selected)
|
||||
.selected(selected)
|
||||
.start_slot(Avatar::new(user.avatar_uri.clone()))
|
||||
.child(Label::new(user.github_login.clone()))
|
||||
.end_slot(h_flex().gap_2().map(|slot| {
|
||||
@@ -409,7 +413,7 @@ impl PickerDelegate for ChannelModalDelegate {
|
||||
Some(
|
||||
deferred(
|
||||
anchored()
|
||||
.anchor(gpui::Corner::TopRight)
|
||||
.anchor(gpui::AnchorCorner::TopRight)
|
||||
.child(menu.clone()),
|
||||
)
|
||||
.with_priority(1),
|
||||
|
||||
@@ -151,7 +151,7 @@ impl PickerDelegate for ContactFinderDelegate {
|
||||
ListItem::new(ix)
|
||||
.inset(true)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.toggle_state(selected)
|
||||
.selected(selected)
|
||||
.start_slot(Avatar::new(user.avatar_uri.clone()))
|
||||
.child(Label::new(user.github_login.clone()))
|
||||
.end_slot::<Icon>(icon_path.map(Icon::from_path)),
|
||||
|
||||
@@ -44,7 +44,7 @@ fn notification_window_options(
|
||||
let notification_margin_height = px(-48.);
|
||||
|
||||
let bounds = gpui::Bounds::<Pixels> {
|
||||
origin: screen.bounds().top_right()
|
||||
origin: screen.bounds().upper_right()
|
||||
- point(
|
||||
size.width + notification_margin_width,
|
||||
notification_margin_height,
|
||||
|
||||
@@ -25,7 +25,6 @@ settings.workspace = true
|
||||
theme.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
telemetry.workspace = true
|
||||
workspace.workspace = true
|
||||
zed_actions.workspace = true
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ use std::{
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use client::parse_zed_link;
|
||||
use client::{parse_zed_link, telemetry::Telemetry};
|
||||
use collections::HashMap;
|
||||
use command_palette_hooks::{
|
||||
CommandInterceptResult, CommandPaletteFilter, CommandPaletteInterceptor,
|
||||
@@ -63,12 +63,18 @@ impl CommandPalette {
|
||||
let Some(previous_focus_handle) = cx.focused() else {
|
||||
return;
|
||||
};
|
||||
let telemetry = workspace.client().telemetry().clone();
|
||||
workspace.toggle_modal(cx, move |cx| {
|
||||
CommandPalette::new(previous_focus_handle, query, cx)
|
||||
CommandPalette::new(previous_focus_handle, telemetry, query, cx)
|
||||
});
|
||||
}
|
||||
|
||||
fn new(previous_focus_handle: FocusHandle, query: &str, cx: &mut ViewContext<Self>) -> Self {
|
||||
fn new(
|
||||
previous_focus_handle: FocusHandle,
|
||||
telemetry: Arc<Telemetry>,
|
||||
query: &str,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let filter = CommandPaletteFilter::try_global(cx);
|
||||
|
||||
let commands = cx
|
||||
@@ -86,8 +92,12 @@ impl CommandPalette {
|
||||
})
|
||||
.collect();
|
||||
|
||||
let delegate =
|
||||
CommandPaletteDelegate::new(cx.view().downgrade(), commands, previous_focus_handle);
|
||||
let delegate = CommandPaletteDelegate::new(
|
||||
cx.view().downgrade(),
|
||||
commands,
|
||||
telemetry,
|
||||
previous_focus_handle,
|
||||
);
|
||||
|
||||
let picker = cx.new_view(|cx| {
|
||||
let picker = Picker::uniform_list(delegate, cx);
|
||||
@@ -123,6 +133,7 @@ pub struct CommandPaletteDelegate {
|
||||
commands: Vec<Command>,
|
||||
matches: Vec<StringMatch>,
|
||||
selected_ix: usize,
|
||||
telemetry: Arc<Telemetry>,
|
||||
previous_focus_handle: FocusHandle,
|
||||
updating_matches: Option<(
|
||||
Task<()>,
|
||||
@@ -156,6 +167,7 @@ impl CommandPaletteDelegate {
|
||||
fn new(
|
||||
command_palette: WeakView<CommandPalette>,
|
||||
commands: Vec<Command>,
|
||||
telemetry: Arc<Telemetry>,
|
||||
previous_focus_handle: FocusHandle,
|
||||
) -> Self {
|
||||
Self {
|
||||
@@ -164,6 +176,7 @@ impl CommandPaletteDelegate {
|
||||
matches: vec![],
|
||||
commands,
|
||||
selected_ix: 0,
|
||||
telemetry,
|
||||
previous_focus_handle,
|
||||
updating_matches: None,
|
||||
}
|
||||
@@ -270,7 +283,11 @@ impl PickerDelegate for CommandPaletteDelegate {
|
||||
let candidates = commands
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(ix, command)| StringMatchCandidate::new(ix, &command.name))
|
||||
.map(|(ix, command)| StringMatchCandidate {
|
||||
id: ix,
|
||||
string: command.name.to_string(),
|
||||
char_bag: command.name.chars().collect(),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let matches = if query.is_empty() {
|
||||
candidates
|
||||
@@ -354,11 +371,9 @@ impl PickerDelegate for CommandPaletteDelegate {
|
||||
let action_ix = self.matches[self.selected_ix].candidate_id;
|
||||
let command = self.commands.swap_remove(action_ix);
|
||||
|
||||
telemetry::event!(
|
||||
"Action Invoked",
|
||||
source = "command palette",
|
||||
action = command.name
|
||||
);
|
||||
self.telemetry
|
||||
.report_action_event("command palette", command.name.clone());
|
||||
|
||||
self.matches.clear();
|
||||
self.commands.clear();
|
||||
HitCounts::update_global(cx, |hit_counts, _cx| {
|
||||
@@ -382,7 +397,7 @@ impl PickerDelegate for CommandPaletteDelegate {
|
||||
ListItem::new(ix)
|
||||
.inset(true)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.toggle_state(selected)
|
||||
.selected(selected)
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
|
||||
@@ -19,9 +19,6 @@ pub fn init(cx: &mut AppContext) {
|
||||
pub struct CommandPaletteFilter {
|
||||
hidden_namespaces: HashSet<&'static str>,
|
||||
hidden_action_types: HashSet<TypeId>,
|
||||
/// Actions that have explicitly been shown. These should be shown even if
|
||||
/// they are in a hidden namespace.
|
||||
shown_action_types: HashSet<TypeId>,
|
||||
}
|
||||
|
||||
#[derive(Deref, DerefMut, Default)]
|
||||
@@ -56,11 +53,6 @@ impl CommandPaletteFilter {
|
||||
let name = action.name();
|
||||
let namespace = name.split("::").next().unwrap_or("malformed action name");
|
||||
|
||||
// If this action has specifically been shown then it should be visible.
|
||||
if self.shown_action_types.contains(&action.type_id()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
self.hidden_namespaces.contains(namespace)
|
||||
|| self.hidden_action_types.contains(&action.type_id())
|
||||
}
|
||||
@@ -77,16 +69,12 @@ impl CommandPaletteFilter {
|
||||
|
||||
/// Hides all actions with the given types.
|
||||
pub fn hide_action_types(&mut self, action_types: &[TypeId]) {
|
||||
for action_type in action_types {
|
||||
self.hidden_action_types.insert(*action_type);
|
||||
self.shown_action_types.remove(action_type);
|
||||
}
|
||||
self.hidden_action_types.extend(action_types);
|
||||
}
|
||||
|
||||
/// Shows all actions with the given types.
|
||||
pub fn show_action_types<'a>(&mut self, action_types: impl Iterator<Item = &'a TypeId>) {
|
||||
for action_type in action_types {
|
||||
self.shown_action_types.insert(*action_type);
|
||||
self.hidden_action_types.remove(action_type);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,12 +6,13 @@ use anyhow::{anyhow, Result};
|
||||
use chrono::DateTime;
|
||||
use fs::Fs;
|
||||
use futures::{io::BufReader, stream::BoxStream, AsyncBufReadExt, AsyncReadExt, StreamExt};
|
||||
use gpui::{prelude::*, AppContext, AsyncAppContext, Global};
|
||||
use gpui::{AppContext, AsyncAppContext, Global};
|
||||
use http_client::{AsyncBody, HttpClient, Method, Request as HttpRequest};
|
||||
use paths::home_dir;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::watch_config_file;
|
||||
use strum::EnumIter;
|
||||
use ui::Context;
|
||||
|
||||
pub const COPILOT_CHAT_COMPLETION_URL: &str = "https://api.githubcopilot.com/chat/completions";
|
||||
pub const COPILOT_CHAT_AUTH_URL: &str = "https://api.github.com/copilot_internal/v2/token";
|
||||
@@ -34,9 +35,9 @@ pub enum Model {
|
||||
Gpt4,
|
||||
#[serde(alias = "gpt-3.5-turbo", rename = "gpt-3.5-turbo")]
|
||||
Gpt3_5Turbo,
|
||||
#[serde(alias = "o1-preview", rename = "o1")]
|
||||
#[serde(alias = "o1-preview", rename = "o1-preview-2024-09-12")]
|
||||
O1Preview,
|
||||
#[serde(alias = "o1-mini", rename = "o1-mini")]
|
||||
#[serde(alias = "o1-mini", rename = "o1-mini-2024-09-12")]
|
||||
O1Mini,
|
||||
#[serde(alias = "claude-3-5-sonnet", rename = "claude-3.5-sonnet")]
|
||||
Claude3_5Sonnet,
|
||||
|
||||
@@ -55,14 +55,6 @@ impl InlineCompletionProvider for CopilotCompletionProvider {
|
||||
"copilot"
|
||||
}
|
||||
|
||||
fn display_name() -> &'static str {
|
||||
"Copilot"
|
||||
}
|
||||
|
||||
fn show_completions_in_menu() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn is_enabled(
|
||||
&self,
|
||||
buffer: &Model<Buffer>,
|
||||
@@ -304,6 +296,7 @@ mod tests {
|
||||
editor.set_inline_completion_provider(Some(copilot_provider), cx)
|
||||
});
|
||||
|
||||
// When inserting, ensure autocompletion is favored over Copilot suggestions.
|
||||
cx.set_state(indoc! {"
|
||||
oneˇ
|
||||
two
|
||||
@@ -331,13 +324,9 @@ mod tests {
|
||||
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
||||
cx.update_editor(|editor, cx| {
|
||||
assert!(editor.context_menu_visible());
|
||||
assert!(!editor.context_menu_contains_inline_completion());
|
||||
assert!(!editor.has_active_inline_completion());
|
||||
// Since we have both, the copilot suggestion is not shown inline
|
||||
assert_eq!(editor.text(cx), "one.\ntwo\nthree\n");
|
||||
assert_eq!(editor.display_text(cx), "one.\ntwo\nthree\n");
|
||||
|
||||
// Confirming a non-copilot completion inserts it and hides the context menu, without showing
|
||||
// Confirming a completion inserts it and hides the context menu, without showing
|
||||
// the copilot suggestion afterwards.
|
||||
editor
|
||||
.confirm_completion(&Default::default(), cx)
|
||||
@@ -349,14 +338,13 @@ mod tests {
|
||||
assert_eq!(editor.display_text(cx), "one.completion_a\ntwo\nthree\n");
|
||||
});
|
||||
|
||||
// Reset editor and only return copilot suggestions
|
||||
// Ensure Copilot suggestions are shown right away if no autocompletion is available.
|
||||
cx.set_state(indoc! {"
|
||||
oneˇ
|
||||
two
|
||||
three
|
||||
"});
|
||||
cx.simulate_keystroke(".");
|
||||
|
||||
drop(handle_completion_request(
|
||||
&mut cx,
|
||||
indoc! {"
|
||||
@@ -379,17 +367,53 @@ mod tests {
|
||||
cx.update_editor(|editor, cx| {
|
||||
assert!(!editor.context_menu_visible());
|
||||
assert!(editor.has_active_inline_completion());
|
||||
// Since only the copilot is available, it's shown inline
|
||||
assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n");
|
||||
assert_eq!(editor.text(cx), "one.\ntwo\nthree\n");
|
||||
});
|
||||
|
||||
// Ensure existing inline completion is interpolated when inserting again.
|
||||
// Reset editor, and ensure autocompletion is still favored over Copilot suggestions.
|
||||
cx.set_state(indoc! {"
|
||||
oneˇ
|
||||
two
|
||||
three
|
||||
"});
|
||||
cx.simulate_keystroke(".");
|
||||
drop(handle_completion_request(
|
||||
&mut cx,
|
||||
indoc! {"
|
||||
one.|<>
|
||||
two
|
||||
three
|
||||
"},
|
||||
vec!["completion_a", "completion_b"],
|
||||
));
|
||||
handle_copilot_completion_request(
|
||||
&copilot_lsp,
|
||||
vec![crate::request::Completion {
|
||||
text: "one.copilot1".into(),
|
||||
range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
|
||||
..Default::default()
|
||||
}],
|
||||
vec![],
|
||||
);
|
||||
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
||||
cx.update_editor(|editor, cx| {
|
||||
assert!(editor.context_menu_visible());
|
||||
assert!(!editor.has_active_inline_completion());
|
||||
|
||||
// When hiding the context menu, the Copilot suggestion becomes visible.
|
||||
editor.cancel(&Default::default(), cx);
|
||||
assert!(!editor.context_menu_visible());
|
||||
assert!(editor.has_active_inline_completion());
|
||||
assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n");
|
||||
assert_eq!(editor.text(cx), "one.\ntwo\nthree\n");
|
||||
});
|
||||
|
||||
// Ensure existing completion is interpolated when inserting again.
|
||||
cx.simulate_keystroke("c");
|
||||
executor.run_until_parked();
|
||||
cx.update_editor(|editor, cx| {
|
||||
assert!(!editor.context_menu_visible());
|
||||
assert!(!editor.context_menu_contains_inline_completion());
|
||||
assert!(editor.has_active_inline_completion());
|
||||
assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n");
|
||||
assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n");
|
||||
@@ -409,7 +433,6 @@ mod tests {
|
||||
cx.update_editor(|editor, cx| {
|
||||
assert!(!editor.context_menu_visible());
|
||||
assert!(editor.has_active_inline_completion());
|
||||
assert!(!editor.context_menu_contains_inline_completion());
|
||||
assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
|
||||
assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n");
|
||||
|
||||
@@ -857,7 +880,7 @@ mod tests {
|
||||
cx.update_editor(|editor, cx| editor.next_inline_completion(&Default::default(), cx));
|
||||
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
||||
cx.update_editor(|editor, cx| {
|
||||
assert!(!editor.context_menu_visible());
|
||||
assert!(!editor.context_menu_visible(), "Even there are some completions available, those are not triggered when active copilot suggestion is present");
|
||||
assert!(editor.has_active_inline_completion());
|
||||
assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
|
||||
assert_eq!(editor.text(cx), "one\ntw\nthree\n");
|
||||
@@ -911,9 +934,15 @@ mod tests {
|
||||
);
|
||||
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
||||
cx.update_editor(|editor, cx| {
|
||||
assert!(editor.context_menu_visible());
|
||||
assert!(!editor.context_menu_contains_inline_completion());
|
||||
assert!(!editor.has_active_inline_completion(),);
|
||||
assert!(
|
||||
editor.context_menu_visible(),
|
||||
"On completion trigger input, the completions should be fetched and visible"
|
||||
);
|
||||
assert!(
|
||||
!editor.has_active_inline_completion(),
|
||||
"On completion trigger input, copilot suggestion should be dismissed"
|
||||
);
|
||||
assert_eq!(editor.display_text(cx), "one\ntwo.\nthree\n");
|
||||
assert_eq!(editor.text(cx), "one\ntwo.\nthree\n");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1050,7 +1050,6 @@ fn editor_blocks(
|
||||
.ok()?
|
||||
}
|
||||
|
||||
Block::FoldedBuffer { .. } => FILE_HEADER.into(),
|
||||
Block::ExcerptBoundary {
|
||||
starts_new_buffer, ..
|
||||
} => {
|
||||
|
||||
@@ -72,7 +72,6 @@ smol.workspace = true
|
||||
snippet.workspace = true
|
||||
sum_tree.workspace = true
|
||||
task.workspace = true
|
||||
telemetry.workspace = true
|
||||
text.workspace = true
|
||||
time.workspace = true
|
||||
time_format.workspace = true
|
||||
|
||||
@@ -251,7 +251,6 @@ gpui::actions!(
|
||||
DisplayCursorNames,
|
||||
DuplicateLineDown,
|
||||
DuplicateLineUp,
|
||||
DuplicateSelection,
|
||||
ExpandAllHunkDiffs,
|
||||
ExpandMacroRecursively,
|
||||
FindAllReferences,
|
||||
|
||||
@@ -1,21 +1,31 @@
|
||||
use std::cell::RefCell;
|
||||
use std::{cmp::Reverse, ops::Range, rc::Rc};
|
||||
use std::{
|
||||
cell::Cell,
|
||||
cmp::{min, Reverse},
|
||||
ops::Range,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||
use gpui::{
|
||||
div, px, uniform_list, AnyElement, BackgroundExecutor, Div, FontWeight, ListSizingBehavior,
|
||||
Model, ScrollStrategy, SharedString, StrikethroughStyle, StyledText, UniformListScrollHandle,
|
||||
ViewContext, WeakView,
|
||||
Model, MouseButton, Pixels, ScrollStrategy, SharedString, StrikethroughStyle, StyledText,
|
||||
UniformListScrollHandle, ViewContext, WeakView,
|
||||
};
|
||||
use language::Buffer;
|
||||
use language::{CodeLabel, Documentation};
|
||||
use lsp::LanguageServerId;
|
||||
use multi_buffer::{Anchor, ExcerptId};
|
||||
use ordered_float::OrderedFloat;
|
||||
use parking_lot::{Mutex, RwLock};
|
||||
use project::{CodeAction, Completion, TaskSourceKind};
|
||||
use std::iter;
|
||||
use task::ResolvedTask;
|
||||
use ui::{prelude::*, Color, IntoElement, ListItem, Pixels, Popover, Styled};
|
||||
use util::ResultExt;
|
||||
use ui::{
|
||||
h_flex, ActiveTheme as _, Color, FluentBuilder as _, InteractiveElement as _, IntoElement,
|
||||
Label, LabelCommon as _, LabelSize, ListItem, ParentElement as _, Popover, Selectable as _,
|
||||
StatefulInteractiveElement as _, Styled, StyledExt as _,
|
||||
};
|
||||
use util::ResultExt as _;
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::{
|
||||
@@ -24,9 +34,6 @@ use crate::{
|
||||
render_parsed_markdown, split_words, styled_runs_for_code_label, CodeActionProvider,
|
||||
CompletionId, CompletionProvider, DisplayRow, Editor, EditorStyle, ResolvedTasks,
|
||||
};
|
||||
use crate::{AcceptInlineCompletion, InlineCompletionMenuHint, InlineCompletionText};
|
||||
|
||||
pub const MAX_COMPLETIONS_ASIDE_WIDTH: Pixels = px(500.);
|
||||
|
||||
pub enum CodeContextMenu {
|
||||
Completions(CompletionsMenu),
|
||||
@@ -105,37 +112,22 @@ impl CodeContextMenu {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn origin(&self, cursor_position: DisplayPoint) -> ContextMenuOrigin {
|
||||
match self {
|
||||
CodeContextMenu::Completions(menu) => menu.origin(cursor_position),
|
||||
CodeContextMenu::CodeActions(menu) => menu.origin(cursor_position),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render(
|
||||
&self,
|
||||
style: &EditorStyle,
|
||||
max_height_in_lines: u32,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) -> AnyElement {
|
||||
match self {
|
||||
CodeContextMenu::Completions(menu) => menu.render(style, max_height_in_lines, cx),
|
||||
CodeContextMenu::CodeActions(menu) => menu.render(style, max_height_in_lines, cx),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render_aside(
|
||||
&self,
|
||||
cursor_position: DisplayPoint,
|
||||
style: &EditorStyle,
|
||||
max_height: Pixels,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) -> Option<AnyElement> {
|
||||
) -> (ContextMenuOrigin, AnyElement) {
|
||||
match self {
|
||||
CodeContextMenu::Completions(menu) => {
|
||||
menu.render_aside(style, max_height, workspace, cx)
|
||||
CodeContextMenu::Completions(menu) => (
|
||||
ContextMenuOrigin::EditorPoint(cursor_position),
|
||||
menu.render(style, max_height, workspace, cx),
|
||||
),
|
||||
CodeContextMenu::CodeActions(menu) => {
|
||||
menu.render(cursor_position, style, max_height, cx)
|
||||
}
|
||||
CodeContextMenu::CodeActions(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -151,19 +143,15 @@ pub struct CompletionsMenu {
|
||||
sort_completions: bool,
|
||||
pub initial_position: Anchor,
|
||||
pub buffer: Model<Buffer>,
|
||||
pub completions: Rc<RefCell<Box<[Completion]>>>,
|
||||
match_candidates: Rc<[StringMatchCandidate]>,
|
||||
pub entries: Rc<[CompletionEntry]>,
|
||||
pub completions: Arc<RwLock<Box<[Completion]>>>,
|
||||
match_candidates: Arc<[StringMatchCandidate]>,
|
||||
pub matches: Arc<[StringMatch]>,
|
||||
pub selected_item: usize,
|
||||
scroll_handle: UniformListScrollHandle,
|
||||
resolve_completions: bool,
|
||||
pub aside_was_displayed: Cell<bool>,
|
||||
show_completion_documentation: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) enum CompletionEntry {
|
||||
Match(StringMatch),
|
||||
InlineCompletionHint(InlineCompletionMenuHint),
|
||||
last_rendered_range: Arc<Mutex<Option<Range<usize>>>>,
|
||||
}
|
||||
|
||||
impl CompletionsMenu {
|
||||
@@ -174,11 +162,17 @@ impl CompletionsMenu {
|
||||
initial_position: Anchor,
|
||||
buffer: Model<Buffer>,
|
||||
completions: Box<[Completion]>,
|
||||
aside_was_displayed: bool,
|
||||
) -> Self {
|
||||
let match_candidates = completions
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(id, completion)| StringMatchCandidate::new(id, &completion.label.filter_text()))
|
||||
.map(|(id, completion)| {
|
||||
StringMatchCandidate::new(
|
||||
id,
|
||||
completion.label.text[completion.label.filter_range.clone()].into(),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
Self {
|
||||
@@ -186,13 +180,15 @@ impl CompletionsMenu {
|
||||
sort_completions,
|
||||
initial_position,
|
||||
buffer,
|
||||
show_completion_documentation,
|
||||
completions: RefCell::new(completions).into(),
|
||||
completions: Arc::new(RwLock::new(completions)),
|
||||
match_candidates,
|
||||
entries: Vec::new().into(),
|
||||
matches: Vec::new().into(),
|
||||
selected_item: 0,
|
||||
scroll_handle: UniformListScrollHandle::new(),
|
||||
resolve_completions: true,
|
||||
aside_was_displayed: Cell::new(aside_was_displayed),
|
||||
show_completion_documentation,
|
||||
last_rendered_range: Arc::new(Mutex::new(None)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -217,25 +213,22 @@ impl CompletionsMenu {
|
||||
documentation: None,
|
||||
lsp_completion: Default::default(),
|
||||
confirm: None,
|
||||
resolved: true,
|
||||
})
|
||||
.collect();
|
||||
|
||||
let match_candidates = choices
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(id, completion)| StringMatchCandidate::new(id, &completion))
|
||||
.map(|(id, completion)| StringMatchCandidate::new(id, completion.to_string()))
|
||||
.collect();
|
||||
let entries = choices
|
||||
let matches = choices
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(id, completion)| {
|
||||
CompletionEntry::Match(StringMatch {
|
||||
candidate_id: id,
|
||||
score: 1.,
|
||||
positions: vec![],
|
||||
string: completion.clone(),
|
||||
})
|
||||
.map(|(id, completion)| StringMatch {
|
||||
candidate_id: id,
|
||||
score: 1.,
|
||||
positions: vec![],
|
||||
string: completion.clone(),
|
||||
})
|
||||
.collect();
|
||||
Self {
|
||||
@@ -243,13 +236,15 @@ impl CompletionsMenu {
|
||||
sort_completions,
|
||||
initial_position: selection.start,
|
||||
buffer,
|
||||
completions: RefCell::new(completions).into(),
|
||||
completions: Arc::new(RwLock::new(completions)),
|
||||
match_candidates,
|
||||
entries,
|
||||
matches,
|
||||
selected_item: 0,
|
||||
scroll_handle: UniformListScrollHandle::new(),
|
||||
resolve_completions: false,
|
||||
aside_was_displayed: Cell::new(false),
|
||||
show_completion_documentation: false,
|
||||
last_rendered_range: Arc::new(Mutex::new(None)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -258,11 +253,7 @@ impl CompletionsMenu {
|
||||
provider: Option<&dyn CompletionProvider>,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) {
|
||||
self.selected_item = 0;
|
||||
self.scroll_handle
|
||||
.scroll_to_item(self.selected_item, ScrollStrategy::Top);
|
||||
self.resolve_selected_completion(provider, cx);
|
||||
cx.notify();
|
||||
self.update_selection_index(0, provider, cx);
|
||||
}
|
||||
|
||||
fn select_prev(
|
||||
@@ -270,15 +261,7 @@ impl CompletionsMenu {
|
||||
provider: Option<&dyn CompletionProvider>,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) {
|
||||
if self.selected_item > 0 {
|
||||
self.selected_item -= 1;
|
||||
} else {
|
||||
self.selected_item = self.entries.len() - 1;
|
||||
}
|
||||
self.scroll_handle
|
||||
.scroll_to_item(self.selected_item, ScrollStrategy::Top);
|
||||
self.resolve_selected_completion(provider, cx);
|
||||
cx.notify();
|
||||
self.update_selection_index(self.prev_match_index(), provider, cx);
|
||||
}
|
||||
|
||||
fn select_next(
|
||||
@@ -286,15 +269,7 @@ impl CompletionsMenu {
|
||||
provider: Option<&dyn CompletionProvider>,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) {
|
||||
if self.selected_item + 1 < self.entries.len() {
|
||||
self.selected_item += 1;
|
||||
} else {
|
||||
self.selected_item = 0;
|
||||
}
|
||||
self.scroll_handle
|
||||
.scroll_to_item(self.selected_item, ScrollStrategy::Top);
|
||||
self.resolve_selected_completion(provider, cx);
|
||||
cx.notify();
|
||||
self.update_selection_index(self.next_match_index(), provider, cx);
|
||||
}
|
||||
|
||||
fn select_last(
|
||||
@@ -302,34 +277,41 @@ impl CompletionsMenu {
|
||||
provider: Option<&dyn CompletionProvider>,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) {
|
||||
self.selected_item = self.entries.len() - 1;
|
||||
self.scroll_handle
|
||||
.scroll_to_item(self.selected_item, ScrollStrategy::Top);
|
||||
self.resolve_selected_completion(provider, cx);
|
||||
cx.notify();
|
||||
self.update_selection_index(self.matches.len() - 1, provider, cx);
|
||||
}
|
||||
|
||||
pub fn show_inline_completion_hint(&mut self, hint: InlineCompletionMenuHint) {
|
||||
let hint = CompletionEntry::InlineCompletionHint(hint);
|
||||
|
||||
self.entries = match self.entries.first() {
|
||||
Some(CompletionEntry::InlineCompletionHint { .. }) => {
|
||||
let mut entries = Vec::from(&*self.entries);
|
||||
entries[0] = hint;
|
||||
entries
|
||||
}
|
||||
_ => {
|
||||
let mut entries = Vec::with_capacity(self.entries.len() + 1);
|
||||
entries.push(hint);
|
||||
entries.extend_from_slice(&self.entries);
|
||||
entries
|
||||
}
|
||||
fn update_selection_index(
|
||||
&mut self,
|
||||
match_index: usize,
|
||||
provider: Option<&dyn CompletionProvider>,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) {
|
||||
if self.selected_item != match_index {
|
||||
self.selected_item = match_index;
|
||||
self.scroll_handle
|
||||
.scroll_to_item(self.selected_item, ScrollStrategy::Top);
|
||||
self.resolve_visible_completions(provider, cx);
|
||||
cx.notify();
|
||||
}
|
||||
.into();
|
||||
self.selected_item = 0;
|
||||
}
|
||||
|
||||
pub fn resolve_selected_completion(
|
||||
fn prev_match_index(&self) -> usize {
|
||||
if self.selected_item > 0 {
|
||||
self.selected_item - 1
|
||||
} else {
|
||||
self.matches.len() - 1
|
||||
}
|
||||
}
|
||||
|
||||
fn next_match_index(&self) -> usize {
|
||||
if self.selected_item + 1 < self.matches.len() {
|
||||
self.selected_item + 1
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resolve_visible_completions(
|
||||
&mut self,
|
||||
provider: Option<&dyn CompletionProvider>,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
@@ -341,268 +323,256 @@ impl CompletionsMenu {
|
||||
return;
|
||||
};
|
||||
|
||||
match &self.entries[self.selected_item] {
|
||||
CompletionEntry::Match(entry) => {
|
||||
let completion_index = entry.candidate_id;
|
||||
let resolve_task = provider.resolve_completions(
|
||||
self.buffer.clone(),
|
||||
vec![completion_index],
|
||||
self.completions.clone(),
|
||||
cx,
|
||||
);
|
||||
// Attempt to resolve completions for every item that will be displayed. This matters
|
||||
// because single line documentation may be displayed inline with the completion.
|
||||
//
|
||||
// When navigating to the very beginning or end of completions, `last_rendered_range` may
|
||||
// have no overlap with the completions that will be displayed, so instead use a range based
|
||||
// on the last rendered count.
|
||||
const APPROXIMATE_VISIBLE_COUNT: usize = 12;
|
||||
let last_rendered_range = self.last_rendered_range.lock().clone();
|
||||
let visible_count = last_rendered_range
|
||||
.clone()
|
||||
.map_or(APPROXIMATE_VISIBLE_COUNT, |range| range.count());
|
||||
let matches_range = if self.selected_item == 0 {
|
||||
0..min(visible_count, self.matches.len())
|
||||
} else if self.selected_item == self.matches.len() - 1 {
|
||||
self.matches.len().saturating_sub(visible_count)..self.matches.len()
|
||||
} else {
|
||||
last_rendered_range.unwrap_or_else(|| self.selected_item..self.selected_item + 1)
|
||||
};
|
||||
|
||||
cx.spawn(move |editor, mut cx| async move {
|
||||
if let Some(true) = resolve_task.await.log_err() {
|
||||
editor.update(&mut cx, |_, cx| cx.notify()).ok();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
// Expand the range to resolve more completions than are predicted to be visible, to reduce
|
||||
// jank on navigation.
|
||||
const EXTRA_TO_RESOLVE: usize = 4;
|
||||
let matches_indices = util::iterate_expanded_and_wrapped_usize_range(
|
||||
matches_range.clone(),
|
||||
EXTRA_TO_RESOLVE,
|
||||
EXTRA_TO_RESOLVE,
|
||||
self.matches.len(),
|
||||
);
|
||||
|
||||
// Avoid work by sometimes filtering out completions that already have documentation.
|
||||
// This filtering doesn't happen if the completions are currently being updated.
|
||||
let candidate_ids = matches_indices.map(|i| self.matches[i].candidate_id);
|
||||
let candidate_ids = match self.completions.try_read() {
|
||||
None => candidate_ids.collect::<Vec<usize>>(),
|
||||
Some(completions) => candidate_ids
|
||||
.filter(|i| completions[*i].documentation.is_none())
|
||||
.collect::<Vec<usize>>(),
|
||||
};
|
||||
|
||||
// Current selection is always resolved even if it already has documentation, to handle
|
||||
// out-of-spec language servers that return more results later.
|
||||
let selected_candidate_id = self.matches[self.selected_item].candidate_id;
|
||||
let candidate_ids = iter::once(selected_candidate_id)
|
||||
.chain(
|
||||
candidate_ids
|
||||
.into_iter()
|
||||
.filter(|id| *id != selected_candidate_id),
|
||||
)
|
||||
.collect::<Vec<usize>>();
|
||||
|
||||
let resolve_task = provider.resolve_completions(
|
||||
self.buffer.clone(),
|
||||
candidate_ids,
|
||||
self.completions.clone(),
|
||||
cx,
|
||||
);
|
||||
|
||||
cx.spawn(move |editor, mut cx| async move {
|
||||
if let Some(true) = resolve_task.await.log_err() {
|
||||
editor.update(&mut cx, |_, cx| cx.notify()).ok();
|
||||
}
|
||||
CompletionEntry::InlineCompletionHint { .. } => {}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
pub fn visible(&self) -> bool {
|
||||
!self.entries.is_empty()
|
||||
}
|
||||
|
||||
fn origin(&self, cursor_position: DisplayPoint) -> ContextMenuOrigin {
|
||||
ContextMenuOrigin::EditorPoint(cursor_position)
|
||||
fn visible(&self) -> bool {
|
||||
!self.matches.is_empty()
|
||||
}
|
||||
|
||||
fn render(
|
||||
&self,
|
||||
style: &EditorStyle,
|
||||
max_height_in_lines: u32,
|
||||
max_height: Pixels,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) -> AnyElement {
|
||||
let completions = self.completions.borrow_mut();
|
||||
let show_completion_documentation = self.show_completion_documentation;
|
||||
let widest_completion_ix = self
|
||||
.entries
|
||||
.matches
|
||||
.iter()
|
||||
.enumerate()
|
||||
.max_by_key(|(_, mat)| match mat {
|
||||
CompletionEntry::Match(mat) => {
|
||||
let completion = &completions[mat.candidate_id];
|
||||
let documentation = &completion.documentation;
|
||||
.max_by_key(|(_, mat)| {
|
||||
let completions = self.completions.read();
|
||||
let completion = &completions[mat.candidate_id];
|
||||
let documentation = &completion.documentation;
|
||||
|
||||
let mut len = completion.label.text.chars().count();
|
||||
if let Some(Documentation::SingleLine(text)) = documentation {
|
||||
if show_completion_documentation {
|
||||
len += text.chars().count();
|
||||
}
|
||||
let mut len = completion.label.text.chars().count();
|
||||
if let Some(Documentation::SingleLine(text)) = documentation {
|
||||
if show_completion_documentation {
|
||||
len += text.chars().count();
|
||||
}
|
||||
|
||||
len
|
||||
}
|
||||
CompletionEntry::InlineCompletionHint(InlineCompletionMenuHint {
|
||||
provider_name,
|
||||
..
|
||||
}) => provider_name.len(),
|
||||
|
||||
len
|
||||
})
|
||||
.map(|(ix, _)| ix);
|
||||
drop(completions);
|
||||
|
||||
let selected_item = self.selected_item;
|
||||
let completions = self.completions.clone();
|
||||
let matches = self.entries.clone();
|
||||
let matches = self.matches.clone();
|
||||
let selected_item = self.selected_item;
|
||||
let style = style.clone();
|
||||
|
||||
let multiline_docs = if show_completion_documentation {
|
||||
let mat = &self.matches[selected_item];
|
||||
match &self.completions.read()[mat.candidate_id].documentation {
|
||||
Some(Documentation::MultiLinePlainText(text)) => {
|
||||
Some(div().child(SharedString::from(text.clone())))
|
||||
}
|
||||
Some(Documentation::MultiLineMarkdown(parsed)) if !parsed.text.is_empty() => {
|
||||
Some(div().child(render_parsed_markdown(
|
||||
"completions_markdown",
|
||||
parsed,
|
||||
&style,
|
||||
workspace,
|
||||
cx,
|
||||
)))
|
||||
}
|
||||
Some(Documentation::Undocumented) if self.aside_was_displayed.get() => {
|
||||
Some(div().child("No documentation"))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let aside_contents = if let Some(multiline_docs) = multiline_docs {
|
||||
Some(multiline_docs)
|
||||
} else if self.aside_was_displayed.get() {
|
||||
Some(div().child("Fetching documentation..."))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
self.aside_was_displayed.set(aside_contents.is_some());
|
||||
|
||||
let aside_contents = aside_contents.map(|div| {
|
||||
div.id("multiline_docs")
|
||||
.max_h(max_height)
|
||||
.flex_1()
|
||||
.px_1p5()
|
||||
.py_1()
|
||||
.min_w(px(260.))
|
||||
.max_w(px(640.))
|
||||
.w(px(500.))
|
||||
.overflow_y_scroll()
|
||||
.occlude()
|
||||
});
|
||||
|
||||
let last_rendered_range = self.last_rendered_range.clone();
|
||||
|
||||
let list = uniform_list(
|
||||
cx.view().clone(),
|
||||
"completions",
|
||||
matches.len(),
|
||||
move |_editor, range, cx| {
|
||||
last_rendered_range.lock().replace(range.clone());
|
||||
let start_ix = range.start;
|
||||
let completions_guard = completions.borrow_mut();
|
||||
let completions_guard = completions.read();
|
||||
|
||||
matches[range]
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(ix, mat)| {
|
||||
let item_ix = start_ix + ix;
|
||||
match mat {
|
||||
CompletionEntry::Match(mat) => {
|
||||
let candidate_id = mat.candidate_id;
|
||||
let completion = &completions_guard[candidate_id];
|
||||
let candidate_id = mat.candidate_id;
|
||||
let completion = &completions_guard[candidate_id];
|
||||
|
||||
let documentation = if show_completion_documentation {
|
||||
&completion.documentation
|
||||
} else {
|
||||
&None
|
||||
};
|
||||
let documentation = if show_completion_documentation {
|
||||
&completion.documentation
|
||||
} else {
|
||||
&None
|
||||
};
|
||||
|
||||
let filter_start = completion.label.filter_range.start;
|
||||
let highlights = gpui::combine_highlights(
|
||||
mat.ranges().map(|range| {
|
||||
(
|
||||
filter_start + range.start..filter_start + range.end,
|
||||
FontWeight::BOLD.into(),
|
||||
)
|
||||
}),
|
||||
styled_runs_for_code_label(&completion.label, &style.syntax)
|
||||
.map(|(range, mut highlight)| {
|
||||
// Ignore font weight for syntax highlighting, as we'll use it
|
||||
// for fuzzy matches.
|
||||
highlight.font_weight = None;
|
||||
let highlights = gpui::combine_highlights(
|
||||
mat.ranges().map(|range| (range, FontWeight::BOLD.into())),
|
||||
styled_runs_for_code_label(&completion.label, &style.syntax).map(
|
||||
|(range, mut highlight)| {
|
||||
// Ignore font weight for syntax highlighting, as we'll use it
|
||||
// for fuzzy matches.
|
||||
highlight.font_weight = None;
|
||||
|
||||
if completion.lsp_completion.deprecated.unwrap_or(false)
|
||||
{
|
||||
highlight.strikethrough =
|
||||
Some(StrikethroughStyle {
|
||||
thickness: 1.0.into(),
|
||||
..Default::default()
|
||||
});
|
||||
highlight.color =
|
||||
Some(cx.theme().colors().text_muted);
|
||||
}
|
||||
if completion.lsp_completion.deprecated.unwrap_or(false) {
|
||||
highlight.strikethrough = Some(StrikethroughStyle {
|
||||
thickness: 1.0.into(),
|
||||
..Default::default()
|
||||
});
|
||||
highlight.color = Some(cx.theme().colors().text_muted);
|
||||
}
|
||||
|
||||
(range, highlight)
|
||||
}),
|
||||
);
|
||||
|
||||
let completion_label =
|
||||
StyledText::new(completion.label.text.clone())
|
||||
.with_highlights(&style.text, highlights);
|
||||
let documentation_label =
|
||||
if let Some(Documentation::SingleLine(text)) = documentation {
|
||||
if text.trim().is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(
|
||||
Label::new(text.clone())
|
||||
.ml_4()
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let color_swatch = completion
|
||||
.color()
|
||||
.map(|color| div().size_4().bg(color).rounded_sm());
|
||||
|
||||
div().min_w(px(220.)).max_w(px(540.)).child(
|
||||
ListItem::new(mat.candidate_id)
|
||||
.inset(true)
|
||||
.toggle_state(item_ix == selected_item)
|
||||
.on_click(cx.listener(move |editor, _event, cx| {
|
||||
cx.stop_propagation();
|
||||
if let Some(task) = editor.confirm_completion(
|
||||
&ConfirmCompletion {
|
||||
item_ix: Some(item_ix),
|
||||
},
|
||||
cx,
|
||||
) {
|
||||
task.detach_and_log_err(cx)
|
||||
}
|
||||
}))
|
||||
.start_slot::<Div>(color_swatch)
|
||||
.child(h_flex().overflow_hidden().child(completion_label))
|
||||
.end_slot::<Label>(documentation_label),
|
||||
)
|
||||
}
|
||||
CompletionEntry::InlineCompletionHint(InlineCompletionMenuHint {
|
||||
provider_name,
|
||||
..
|
||||
}) => div().min_w(px(250.)).max_w(px(500.)).child(
|
||||
ListItem::new("inline-completion")
|
||||
.inset(true)
|
||||
.toggle_state(item_ix == selected_item)
|
||||
.start_slot(Icon::new(IconName::ZedPredict))
|
||||
.child(
|
||||
StyledText::new(format!(
|
||||
"{} Completion",
|
||||
SharedString::new_static(provider_name)
|
||||
))
|
||||
.with_highlights(&style.text, None),
|
||||
)
|
||||
.on_click(cx.listener(move |editor, _event, cx| {
|
||||
cx.stop_propagation();
|
||||
editor.accept_inline_completion(
|
||||
&AcceptInlineCompletion {},
|
||||
cx,
|
||||
);
|
||||
})),
|
||||
(range, highlight)
|
||||
},
|
||||
),
|
||||
}
|
||||
);
|
||||
let completion_label = StyledText::new(completion.label.text.clone())
|
||||
.with_highlights(&style.text, highlights);
|
||||
let documentation_label =
|
||||
if let Some(Documentation::SingleLine(text)) = documentation {
|
||||
if text.trim().is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(
|
||||
Label::new(text.clone())
|
||||
.ml_4()
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let color_swatch = completion
|
||||
.color()
|
||||
.map(|color| div().size_4().bg(color).rounded_sm());
|
||||
|
||||
div().min_w(px(220.)).max_w(px(540.)).child(
|
||||
ListItem::new(mat.candidate_id)
|
||||
.inset(true)
|
||||
.selected(item_ix == selected_item)
|
||||
.on_click(cx.listener(move |editor, _event, cx| {
|
||||
cx.stop_propagation();
|
||||
if let Some(task) = editor.confirm_completion(
|
||||
&ConfirmCompletion {
|
||||
item_ix: Some(item_ix),
|
||||
},
|
||||
cx,
|
||||
) {
|
||||
task.detach_and_log_err(cx)
|
||||
}
|
||||
}))
|
||||
.start_slot::<Div>(color_swatch)
|
||||
.child(h_flex().overflow_hidden().child(completion_label))
|
||||
.end_slot::<Label>(documentation_label),
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
},
|
||||
)
|
||||
.occlude()
|
||||
.max_h(max_height_in_lines as f32 * cx.line_height())
|
||||
.max_h(max_height)
|
||||
.track_scroll(self.scroll_handle.clone())
|
||||
.with_width_from_item(widest_completion_ix)
|
||||
.with_sizing_behavior(ListSizingBehavior::Infer);
|
||||
|
||||
Popover::new().child(list).into_any_element()
|
||||
}
|
||||
|
||||
fn render_aside(
|
||||
&self,
|
||||
style: &EditorStyle,
|
||||
max_height: Pixels,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) -> Option<AnyElement> {
|
||||
if !self.show_completion_documentation {
|
||||
return None;
|
||||
}
|
||||
|
||||
let multiline_docs = match &self.entries[self.selected_item] {
|
||||
CompletionEntry::Match(mat) => {
|
||||
match self.completions.borrow_mut()[mat.candidate_id]
|
||||
.documentation
|
||||
.as_ref()?
|
||||
{
|
||||
Documentation::MultiLinePlainText(text) => {
|
||||
div().child(SharedString::from(text.clone()))
|
||||
}
|
||||
Documentation::MultiLineMarkdown(parsed) if !parsed.text.is_empty() => div()
|
||||
.child(render_parsed_markdown(
|
||||
"completions_markdown",
|
||||
parsed,
|
||||
&style,
|
||||
workspace,
|
||||
cx,
|
||||
)),
|
||||
Documentation::MultiLineMarkdown(_) => return None,
|
||||
Documentation::SingleLine(_) => return None,
|
||||
Documentation::Undocumented => return None,
|
||||
}
|
||||
}
|
||||
CompletionEntry::InlineCompletionHint(hint) => match &hint.text {
|
||||
InlineCompletionText::Edit { text, highlights } => div()
|
||||
.my_1()
|
||||
.rounded(px(6.))
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.child(
|
||||
gpui::StyledText::new(text.clone())
|
||||
.with_highlights(&style.text, highlights.clone()),
|
||||
),
|
||||
InlineCompletionText::Move(text) => div().child(text.clone()),
|
||||
},
|
||||
};
|
||||
|
||||
Some(
|
||||
Popover::new()
|
||||
.child(
|
||||
multiline_docs
|
||||
.id("multiline_docs")
|
||||
.max_h(max_height)
|
||||
.px_0p5()
|
||||
.min_w(px(260.))
|
||||
.max_w(MAX_COMPLETIONS_ASIDE_WIDTH)
|
||||
.overflow_y_scroll()
|
||||
.occlude(),
|
||||
)
|
||||
.into_any_element(),
|
||||
)
|
||||
Popover::new()
|
||||
.child(list)
|
||||
.when_some(aside_contents, |popover, aside_contents| {
|
||||
popover.aside(aside_contents)
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
pub async fn filter(&mut self, query: Option<&str>, executor: BackgroundExecutor) {
|
||||
@@ -645,7 +615,7 @@ impl CompletionsMenu {
|
||||
}
|
||||
}
|
||||
|
||||
let completions = self.completions.borrow_mut();
|
||||
let completions = self.completions.read();
|
||||
if self.sort_completions {
|
||||
matches.sort_unstable_by_key(|mat| {
|
||||
// We do want to strike a balance here between what the language server tells us
|
||||
@@ -697,14 +667,17 @@ impl CompletionsMenu {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for mat in &mut matches {
|
||||
let completion = &completions[mat.candidate_id];
|
||||
mat.string.clone_from(&completion.label.text);
|
||||
for position in &mut mat.positions {
|
||||
*position += completion.label.filter_range.start;
|
||||
}
|
||||
}
|
||||
drop(completions);
|
||||
|
||||
let mut new_entries: Vec<_> = matches.into_iter().map(CompletionEntry::Match).collect();
|
||||
if let Some(CompletionEntry::InlineCompletionHint(hint)) = self.entries.first() {
|
||||
new_entries.insert(0, CompletionEntry::InlineCompletionHint(hint.clone()));
|
||||
}
|
||||
|
||||
self.entries = new_entries.into();
|
||||
self.matches = matches.into();
|
||||
self.selected_item = 0;
|
||||
}
|
||||
}
|
||||
@@ -713,13 +686,13 @@ impl CompletionsMenu {
|
||||
pub struct AvailableCodeAction {
|
||||
pub excerpt_id: ExcerptId,
|
||||
pub action: CodeAction,
|
||||
pub provider: Rc<dyn CodeActionProvider>,
|
||||
pub provider: Arc<dyn CodeActionProvider>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct CodeActionContents {
|
||||
pub tasks: Option<Rc<ResolvedTasks>>,
|
||||
pub actions: Option<Rc<[AvailableCodeAction]>>,
|
||||
pub tasks: Option<Arc<ResolvedTasks>>,
|
||||
pub actions: Option<Arc<[AvailableCodeAction]>>,
|
||||
}
|
||||
|
||||
impl CodeActionContents {
|
||||
@@ -804,7 +777,7 @@ pub enum CodeActionsItem {
|
||||
CodeAction {
|
||||
excerpt_id: ExcerptId,
|
||||
action: CodeAction,
|
||||
provider: Rc<dyn CodeActionProvider>,
|
||||
provider: Arc<dyn CodeActionProvider>,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -880,23 +853,16 @@ impl CodeActionsMenu {
|
||||
!self.actions.is_empty()
|
||||
}
|
||||
|
||||
fn origin(&self, cursor_position: DisplayPoint) -> ContextMenuOrigin {
|
||||
if let Some(row) = self.deployed_from_indicator {
|
||||
ContextMenuOrigin::GutterIndicator(row)
|
||||
} else {
|
||||
ContextMenuOrigin::EditorPoint(cursor_position)
|
||||
}
|
||||
}
|
||||
|
||||
fn render(
|
||||
&self,
|
||||
cursor_position: DisplayPoint,
|
||||
_style: &EditorStyle,
|
||||
max_height_in_lines: u32,
|
||||
max_height: Pixels,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) -> AnyElement {
|
||||
) -> (ContextMenuOrigin, AnyElement) {
|
||||
let actions = self.actions.clone();
|
||||
let selected_item = self.selected_item;
|
||||
let list = uniform_list(
|
||||
let element = uniform_list(
|
||||
cx.view().clone(),
|
||||
"code_actions_menu",
|
||||
self.actions.len(),
|
||||
@@ -908,14 +874,27 @@ impl CodeActionsMenu {
|
||||
.enumerate()
|
||||
.map(|(ix, action)| {
|
||||
let item_ix = range.start + ix;
|
||||
let selected = item_ix == selected_item;
|
||||
let selected = selected_item == item_ix;
|
||||
let colors = cx.theme().colors();
|
||||
div().min_w(px(220.)).max_w(px(540.)).child(
|
||||
ListItem::new(item_ix)
|
||||
.inset(true)
|
||||
.toggle_state(selected)
|
||||
.when_some(action.as_code_action(), |this, action| {
|
||||
this.on_click(cx.listener(move |editor, _, cx| {
|
||||
div()
|
||||
.px_1()
|
||||
.rounded_md()
|
||||
.text_color(colors.text)
|
||||
.when(selected, |style| {
|
||||
style
|
||||
.bg(colors.element_active)
|
||||
.text_color(colors.text_accent)
|
||||
})
|
||||
.hover(|style| {
|
||||
style
|
||||
.bg(colors.element_hover)
|
||||
.text_color(colors.text_accent)
|
||||
})
|
||||
.whitespace_nowrap()
|
||||
.when_some(action.as_code_action(), |this, action| {
|
||||
this.on_mouse_down(
|
||||
MouseButton::Left,
|
||||
cx.listener(move |editor, _, cx| {
|
||||
cx.stop_propagation();
|
||||
if let Some(task) = editor.confirm_code_action(
|
||||
&ConfirmCodeAction {
|
||||
@@ -925,21 +904,17 @@ impl CodeActionsMenu {
|
||||
) {
|
||||
task.detach_and_log_err(cx)
|
||||
}
|
||||
}))
|
||||
.child(
|
||||
h_flex()
|
||||
.overflow_hidden()
|
||||
.child(
|
||||
// TASK: It would be good to make lsp_action.title a SharedString to avoid allocating here.
|
||||
action.lsp_action.title.replace("\n", ""),
|
||||
)
|
||||
.when(selected, |this| {
|
||||
this.text_color(colors.text_accent)
|
||||
}),
|
||||
)
|
||||
})
|
||||
.when_some(action.as_task(), |this, task| {
|
||||
this.on_click(cx.listener(move |editor, _, cx| {
|
||||
}),
|
||||
)
|
||||
// TASK: It would be good to make lsp_action.title a SharedString to avoid allocating here.
|
||||
.child(SharedString::from(
|
||||
action.lsp_action.title.replace("\n", ""),
|
||||
))
|
||||
})
|
||||
.when_some(action.as_task(), |this, task| {
|
||||
this.on_mouse_down(
|
||||
MouseButton::Left,
|
||||
cx.listener(move |editor, _, cx| {
|
||||
cx.stop_propagation();
|
||||
if let Some(task) = editor.confirm_code_action(
|
||||
&ConfirmCodeAction {
|
||||
@@ -949,23 +924,18 @@ impl CodeActionsMenu {
|
||||
) {
|
||||
task.detach_and_log_err(cx)
|
||||
}
|
||||
}))
|
||||
.child(
|
||||
h_flex()
|
||||
.overflow_hidden()
|
||||
.child(task.resolved_label.replace("\n", ""))
|
||||
.when(selected, |this| {
|
||||
this.text_color(colors.text_accent)
|
||||
}),
|
||||
)
|
||||
}),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.child(SharedString::from(task.resolved_label.replace("\n", "")))
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
},
|
||||
)
|
||||
.elevation_1(cx)
|
||||
.p_1()
|
||||
.max_h(max_height)
|
||||
.occlude()
|
||||
.max_h(max_height_in_lines as f32 * cx.line_height())
|
||||
.track_scroll(self.scroll_handle.clone())
|
||||
.with_width_from_item(
|
||||
self.actions
|
||||
@@ -979,8 +949,15 @@ impl CodeActionsMenu {
|
||||
})
|
||||
.map(|(ix, _)| ix),
|
||||
)
|
||||
.with_sizing_behavior(ListSizingBehavior::Infer);
|
||||
.with_sizing_behavior(ListSizingBehavior::Infer)
|
||||
.into_any_element();
|
||||
|
||||
Popover::new().child(list).into_any_element()
|
||||
let cursor_position = if let Some(row) = self.deployed_from_indicator {
|
||||
ContextMenuOrigin::GutterIndicator(row)
|
||||
} else {
|
||||
ContextMenuOrigin::EditorPoint(cursor_position)
|
||||
};
|
||||
|
||||
(cursor_position, element)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
|
||||
mod block_map;
|
||||
mod crease_map;
|
||||
mod custom_highlights;
|
||||
mod fold_map;
|
||||
mod inlay_map;
|
||||
pub(crate) mod invisibles;
|
||||
@@ -270,7 +269,7 @@ impl DisplayMap {
|
||||
let start = buffer_snapshot.anchor_before(range.start);
|
||||
let end = buffer_snapshot.anchor_after(range.end);
|
||||
BlockProperties {
|
||||
placement: BlockPlacement::Replace(start..=end),
|
||||
placement: BlockPlacement::Replace(start..end),
|
||||
render,
|
||||
height,
|
||||
style,
|
||||
@@ -337,38 +336,6 @@ impl DisplayMap {
|
||||
block_map.remove_intersecting_replace_blocks(offset_ranges, inclusive);
|
||||
}
|
||||
|
||||
pub fn fold_buffer(&mut self, buffer_id: language::BufferId, cx: &mut ModelContext<Self>) {
|
||||
let snapshot = self.buffer.read(cx).snapshot(cx);
|
||||
let edits = self.buffer_subscription.consume().into_inner();
|
||||
let tab_size = Self::tab_size(&self.buffer, cx);
|
||||
let (snapshot, edits) = self.inlay_map.sync(snapshot, edits);
|
||||
let (snapshot, edits) = self.fold_map.read(snapshot, edits);
|
||||
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
|
||||
let (snapshot, edits) = self
|
||||
.wrap_map
|
||||
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
|
||||
let mut block_map = self.block_map.write(snapshot, edits);
|
||||
block_map.fold_buffer(buffer_id, self.buffer.read(cx), cx)
|
||||
}
|
||||
|
||||
pub fn unfold_buffer(&mut self, buffer_id: language::BufferId, cx: &mut ModelContext<Self>) {
|
||||
let snapshot = self.buffer.read(cx).snapshot(cx);
|
||||
let edits = self.buffer_subscription.consume().into_inner();
|
||||
let tab_size = Self::tab_size(&self.buffer, cx);
|
||||
let (snapshot, edits) = self.inlay_map.sync(snapshot, edits);
|
||||
let (snapshot, edits) = self.fold_map.read(snapshot, edits);
|
||||
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
|
||||
let (snapshot, edits) = self
|
||||
.wrap_map
|
||||
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
|
||||
let mut block_map = self.block_map.write(snapshot, edits);
|
||||
block_map.unfold_buffer(buffer_id, self.buffer.read(cx), cx)
|
||||
}
|
||||
|
||||
pub(crate) fn buffer_folded(&self, buffer_id: language::BufferId) -> bool {
|
||||
self.block_map.folded_buffers.contains(&buffer_id)
|
||||
}
|
||||
|
||||
pub fn insert_creases(
|
||||
&mut self,
|
||||
creases: impl IntoIterator<Item = Crease<Anchor>>,
|
||||
@@ -568,16 +535,10 @@ pub(crate) struct Highlights<'a> {
|
||||
pub styles: HighlightStyles,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct InlineCompletionStyles {
|
||||
pub insertion: HighlightStyle,
|
||||
pub whitespace: HighlightStyle,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Copy)]
|
||||
pub struct HighlightStyles {
|
||||
pub inlay_hint: Option<HighlightStyle>,
|
||||
pub inline_completion: Option<InlineCompletionStyles>,
|
||||
pub suggestion: Option<HighlightStyle>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -745,11 +706,7 @@ impl DisplaySnapshot {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn next_line_boundary(
|
||||
&self,
|
||||
mut point: MultiBufferPoint,
|
||||
) -> (MultiBufferPoint, DisplayPoint) {
|
||||
let original_point = point;
|
||||
pub fn next_line_boundary(&self, mut point: MultiBufferPoint) -> (Point, DisplayPoint) {
|
||||
loop {
|
||||
let mut inlay_point = self.inlay_snapshot.to_inlay_point(point);
|
||||
let mut fold_point = self.fold_snapshot.to_fold_point(inlay_point, Bias::Right);
|
||||
@@ -760,7 +717,7 @@ impl DisplaySnapshot {
|
||||
let mut display_point = self.point_to_display_point(point, Bias::Right);
|
||||
*display_point.column_mut() = self.line_len(display_point.row());
|
||||
let next_point = self.display_point_to_point(display_point, Bias::Right);
|
||||
if next_point == point || original_point == point || original_point == next_point {
|
||||
if next_point == point {
|
||||
return (point, display_point);
|
||||
}
|
||||
point = next_point;
|
||||
@@ -902,7 +859,7 @@ impl DisplaySnapshot {
|
||||
language_aware,
|
||||
HighlightStyles {
|
||||
inlay_hint: Some(editor_style.inlay_hints_style),
|
||||
inline_completion: Some(editor_style.inline_completion_styles),
|
||||
suggestion: Some(editor_style.suggestions_style),
|
||||
},
|
||||
)
|
||||
.flat_map(|chunk| {
|
||||
@@ -1118,6 +1075,10 @@ impl DisplaySnapshot {
|
||||
|| self.fold_snapshot.is_line_folded(buffer_row)
|
||||
}
|
||||
|
||||
pub fn is_line_replaced(&self, buffer_row: MultiBufferRow) -> bool {
|
||||
self.block_snapshot.is_line_replaced(buffer_row)
|
||||
}
|
||||
|
||||
pub fn is_block_line(&self, display_row: DisplayRow) -> bool {
|
||||
self.block_snapshot.is_block_line(BlockRow(display_row.0))
|
||||
}
|
||||
@@ -2264,7 +2225,7 @@ pub mod tests {
|
||||
[BlockProperties {
|
||||
placement: BlockPlacement::Replace(
|
||||
buffer_snapshot.anchor_before(Point::new(1, 2))
|
||||
..=buffer_snapshot.anchor_after(Point::new(2, 3)),
|
||||
..buffer_snapshot.anchor_after(Point::new(2, 3)),
|
||||
),
|
||||
height: 4,
|
||||
style: BlockStyle::Fixed,
|
||||
|
||||
@@ -1,174 +0,0 @@
|
||||
use collections::BTreeMap;
|
||||
use gpui::HighlightStyle;
|
||||
use language::Chunk;
|
||||
use multi_buffer::{Anchor, MultiBufferChunks, MultiBufferSnapshot, ToOffset as _};
|
||||
use std::{
|
||||
any::TypeId,
|
||||
cmp,
|
||||
iter::{self, Peekable},
|
||||
ops::Range,
|
||||
sync::Arc,
|
||||
vec,
|
||||
};
|
||||
use sum_tree::TreeMap;
|
||||
|
||||
pub struct CustomHighlightsChunks<'a> {
|
||||
buffer_chunks: MultiBufferChunks<'a>,
|
||||
buffer_chunk: Option<Chunk<'a>>,
|
||||
offset: usize,
|
||||
multibuffer_snapshot: &'a MultiBufferSnapshot,
|
||||
|
||||
highlight_endpoints: Peekable<vec::IntoIter<HighlightEndpoint>>,
|
||||
active_highlights: BTreeMap<TypeId, HighlightStyle>,
|
||||
text_highlights: Option<&'a TreeMap<TypeId, Arc<(HighlightStyle, Vec<Range<Anchor>>)>>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
|
||||
struct HighlightEndpoint {
|
||||
offset: usize,
|
||||
is_start: bool,
|
||||
tag: TypeId,
|
||||
style: HighlightStyle,
|
||||
}
|
||||
|
||||
impl<'a> CustomHighlightsChunks<'a> {
|
||||
pub fn new(
|
||||
range: Range<usize>,
|
||||
language_aware: bool,
|
||||
text_highlights: Option<&'a TreeMap<TypeId, Arc<(HighlightStyle, Vec<Range<Anchor>>)>>>,
|
||||
multibuffer_snapshot: &'a MultiBufferSnapshot,
|
||||
) -> Self {
|
||||
Self {
|
||||
buffer_chunks: multibuffer_snapshot.chunks(range.clone(), language_aware),
|
||||
buffer_chunk: None,
|
||||
offset: range.start,
|
||||
|
||||
text_highlights,
|
||||
highlight_endpoints: create_highlight_endpoints(
|
||||
&range,
|
||||
text_highlights,
|
||||
multibuffer_snapshot,
|
||||
),
|
||||
active_highlights: Default::default(),
|
||||
multibuffer_snapshot,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn seek(&mut self, new_range: Range<usize>) {
|
||||
self.highlight_endpoints =
|
||||
create_highlight_endpoints(&new_range, self.text_highlights, self.multibuffer_snapshot);
|
||||
self.offset = new_range.start;
|
||||
self.buffer_chunks.seek(new_range);
|
||||
self.buffer_chunk.take();
|
||||
self.active_highlights.clear()
|
||||
}
|
||||
}
|
||||
|
||||
fn create_highlight_endpoints(
|
||||
range: &Range<usize>,
|
||||
text_highlights: Option<&TreeMap<TypeId, Arc<(HighlightStyle, Vec<Range<Anchor>>)>>>,
|
||||
buffer: &MultiBufferSnapshot,
|
||||
) -> iter::Peekable<vec::IntoIter<HighlightEndpoint>> {
|
||||
let mut highlight_endpoints = Vec::new();
|
||||
if let Some(text_highlights) = text_highlights {
|
||||
let start = buffer.anchor_after(range.start);
|
||||
let end = buffer.anchor_after(range.end);
|
||||
for (&tag, text_highlights) in text_highlights.iter() {
|
||||
let style = text_highlights.0;
|
||||
let ranges = &text_highlights.1;
|
||||
|
||||
let start_ix = match ranges.binary_search_by(|probe| {
|
||||
let cmp = probe.end.cmp(&start, &buffer);
|
||||
if cmp.is_gt() {
|
||||
cmp::Ordering::Greater
|
||||
} else {
|
||||
cmp::Ordering::Less
|
||||
}
|
||||
}) {
|
||||
Ok(i) | Err(i) => i,
|
||||
};
|
||||
|
||||
for range in &ranges[start_ix..] {
|
||||
if range.start.cmp(&end, &buffer).is_ge() {
|
||||
break;
|
||||
}
|
||||
|
||||
highlight_endpoints.push(HighlightEndpoint {
|
||||
offset: range.start.to_offset(&buffer),
|
||||
is_start: true,
|
||||
tag,
|
||||
style,
|
||||
});
|
||||
highlight_endpoints.push(HighlightEndpoint {
|
||||
offset: range.end.to_offset(&buffer),
|
||||
is_start: false,
|
||||
tag,
|
||||
style,
|
||||
});
|
||||
}
|
||||
}
|
||||
highlight_endpoints.sort();
|
||||
}
|
||||
highlight_endpoints.into_iter().peekable()
|
||||
}
|
||||
|
||||
impl<'a> Iterator for CustomHighlightsChunks<'a> {
|
||||
type Item = Chunk<'a>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let mut next_highlight_endpoint = usize::MAX;
|
||||
while let Some(endpoint) = self.highlight_endpoints.peek().copied() {
|
||||
if endpoint.offset <= self.offset {
|
||||
if endpoint.is_start {
|
||||
self.active_highlights.insert(endpoint.tag, endpoint.style);
|
||||
} else {
|
||||
self.active_highlights.remove(&endpoint.tag);
|
||||
}
|
||||
self.highlight_endpoints.next();
|
||||
} else {
|
||||
next_highlight_endpoint = endpoint.offset;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let chunk = self
|
||||
.buffer_chunk
|
||||
.get_or_insert_with(|| self.buffer_chunks.next().unwrap());
|
||||
if chunk.text.is_empty() {
|
||||
*chunk = self.buffer_chunks.next().unwrap();
|
||||
}
|
||||
|
||||
let (prefix, suffix) = chunk
|
||||
.text
|
||||
.split_at(chunk.text.len().min(next_highlight_endpoint - self.offset));
|
||||
|
||||
chunk.text = suffix;
|
||||
self.offset += prefix.len();
|
||||
let mut prefix = Chunk {
|
||||
text: prefix,
|
||||
..chunk.clone()
|
||||
};
|
||||
if !self.active_highlights.is_empty() {
|
||||
let mut highlight_style = HighlightStyle::default();
|
||||
for active_highlight in self.active_highlights.values() {
|
||||
highlight_style.highlight(*active_highlight);
|
||||
}
|
||||
prefix.highlight_style = Some(highlight_style);
|
||||
}
|
||||
Some(prefix)
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for HighlightEndpoint {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for HighlightEndpoint {
|
||||
fn cmp(&self, other: &Self) -> cmp::Ordering {
|
||||
self.offset
|
||||
.cmp(&other.offset)
|
||||
.then_with(|| other.is_start.cmp(&self.is_start))
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,22 @@
|
||||
use crate::{HighlightStyles, InlayId};
|
||||
use collections::BTreeSet;
|
||||
use collections::{BTreeMap, BTreeSet};
|
||||
use gpui::HighlightStyle;
|
||||
use language::{Chunk, Edit, Point, TextSummary};
|
||||
use multi_buffer::{Anchor, MultiBufferRow, MultiBufferRows, MultiBufferSnapshot, ToOffset};
|
||||
use std::{
|
||||
cmp,
|
||||
ops::{Add, AddAssign, Range, Sub, SubAssign},
|
||||
use multi_buffer::{
|
||||
Anchor, MultiBufferChunks, MultiBufferRow, MultiBufferRows, MultiBufferSnapshot, ToOffset,
|
||||
};
|
||||
use sum_tree::{Bias, Cursor, SumTree};
|
||||
use std::{
|
||||
any::TypeId,
|
||||
cmp,
|
||||
iter::Peekable,
|
||||
ops::{Add, AddAssign, Range, Sub, SubAssign},
|
||||
sync::Arc,
|
||||
vec,
|
||||
};
|
||||
use sum_tree::{Bias, Cursor, SumTree, TreeMap};
|
||||
use text::{Patch, Rope};
|
||||
|
||||
use super::{custom_highlights::CustomHighlightsChunks, Highlights};
|
||||
use super::Highlights;
|
||||
|
||||
/// Decides where the [`Inlay`]s should be displayed.
|
||||
///
|
||||
@@ -55,9 +62,9 @@ impl Inlay {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn inline_completion<T: Into<Rope>>(id: usize, position: Anchor, text: T) -> Self {
|
||||
pub fn suggestion<T: Into<Rope>>(id: usize, position: Anchor, text: T) -> Self {
|
||||
Self {
|
||||
id: InlayId::InlineCompletion(id),
|
||||
id: InlayId::Suggestion(id),
|
||||
position,
|
||||
text: text.into(),
|
||||
}
|
||||
@@ -200,15 +207,39 @@ pub struct InlayBufferRows<'a> {
|
||||
max_buffer_row: MultiBufferRow,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
|
||||
struct HighlightEndpoint {
|
||||
offset: InlayOffset,
|
||||
is_start: bool,
|
||||
tag: TypeId,
|
||||
style: HighlightStyle,
|
||||
}
|
||||
|
||||
impl PartialOrd for HighlightEndpoint {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for HighlightEndpoint {
|
||||
fn cmp(&self, other: &Self) -> cmp::Ordering {
|
||||
self.offset
|
||||
.cmp(&other.offset)
|
||||
.then_with(|| other.is_start.cmp(&self.is_start))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct InlayChunks<'a> {
|
||||
transforms: Cursor<'a, Transform, (InlayOffset, usize)>,
|
||||
buffer_chunks: CustomHighlightsChunks<'a>,
|
||||
buffer_chunks: MultiBufferChunks<'a>,
|
||||
buffer_chunk: Option<Chunk<'a>>,
|
||||
inlay_chunks: Option<text::Chunks<'a>>,
|
||||
inlay_chunk: Option<&'a str>,
|
||||
output_offset: InlayOffset,
|
||||
max_output_offset: InlayOffset,
|
||||
highlight_styles: HighlightStyles,
|
||||
highlight_endpoints: Peekable<vec::IntoIter<HighlightEndpoint>>,
|
||||
active_highlights: BTreeMap<TypeId, HighlightStyle>,
|
||||
highlights: Highlights<'a>,
|
||||
snapshot: &'a InlaySnapshot,
|
||||
}
|
||||
@@ -224,6 +255,22 @@ impl<'a> InlayChunks<'a> {
|
||||
self.buffer_chunk = None;
|
||||
self.output_offset = new_range.start;
|
||||
self.max_output_offset = new_range.end;
|
||||
|
||||
let mut highlight_endpoints = Vec::new();
|
||||
if let Some(text_highlights) = self.highlights.text_highlights {
|
||||
if !text_highlights.is_empty() {
|
||||
self.snapshot.apply_text_highlights(
|
||||
&mut self.transforms,
|
||||
&new_range,
|
||||
text_highlights,
|
||||
&mut highlight_endpoints,
|
||||
);
|
||||
self.transforms.seek(&new_range.start, Bias::Right, &());
|
||||
highlight_endpoints.sort();
|
||||
}
|
||||
}
|
||||
self.highlight_endpoints = highlight_endpoints.into_iter().peekable();
|
||||
self.active_highlights.clear();
|
||||
}
|
||||
|
||||
pub fn offset(&self) -> InlayOffset {
|
||||
@@ -239,6 +286,21 @@ impl<'a> Iterator for InlayChunks<'a> {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut next_highlight_endpoint = InlayOffset(usize::MAX);
|
||||
while let Some(endpoint) = self.highlight_endpoints.peek().copied() {
|
||||
if endpoint.offset <= self.output_offset {
|
||||
if endpoint.is_start {
|
||||
self.active_highlights.insert(endpoint.tag, endpoint.style);
|
||||
} else {
|
||||
self.active_highlights.remove(&endpoint.tag);
|
||||
}
|
||||
self.highlight_endpoints.next();
|
||||
} else {
|
||||
next_highlight_endpoint = endpoint.offset;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let chunk = match self.transforms.item()? {
|
||||
Transform::Isomorphic(_) => {
|
||||
let chunk = self
|
||||
@@ -252,15 +314,24 @@ impl<'a> Iterator for InlayChunks<'a> {
|
||||
chunk
|
||||
.text
|
||||
.len()
|
||||
.min(self.transforms.end(&()).0 .0 - self.output_offset.0),
|
||||
.min(self.transforms.end(&()).0 .0 - self.output_offset.0)
|
||||
.min(next_highlight_endpoint.0 - self.output_offset.0),
|
||||
);
|
||||
|
||||
chunk.text = suffix;
|
||||
self.output_offset.0 += prefix.len();
|
||||
Chunk {
|
||||
let mut prefix = Chunk {
|
||||
text: prefix,
|
||||
..chunk.clone()
|
||||
};
|
||||
if !self.active_highlights.is_empty() {
|
||||
let mut highlight_style = HighlightStyle::default();
|
||||
for active_highlight in self.active_highlights.values() {
|
||||
highlight_style.highlight(*active_highlight);
|
||||
}
|
||||
prefix.highlight_style = Some(highlight_style);
|
||||
}
|
||||
prefix
|
||||
}
|
||||
Transform::Inlay(inlay) => {
|
||||
let mut inlay_style_and_highlight = None;
|
||||
@@ -275,15 +346,7 @@ impl<'a> Iterator for InlayChunks<'a> {
|
||||
}
|
||||
|
||||
let mut highlight_style = match inlay.id {
|
||||
InlayId::InlineCompletion(_) => {
|
||||
self.highlight_styles.inline_completion.map(|s| {
|
||||
if inlay.text.chars().all(|c| c.is_whitespace()) {
|
||||
s.whitespace
|
||||
} else {
|
||||
s.insertion
|
||||
}
|
||||
})
|
||||
}
|
||||
InlayId::Suggestion(_) => self.highlight_styles.suggestion,
|
||||
InlayId::Hint(_) => self.highlight_styles.inlay_hint,
|
||||
};
|
||||
let next_inlay_highlight_endpoint;
|
||||
@@ -322,6 +385,13 @@ impl<'a> Iterator for InlayChunks<'a> {
|
||||
|
||||
self.output_offset.0 += chunk.len();
|
||||
|
||||
if !self.active_highlights.is_empty() {
|
||||
for active_highlight in self.active_highlights.values() {
|
||||
highlight_style
|
||||
.get_or_insert(Default::default())
|
||||
.highlight(*active_highlight);
|
||||
}
|
||||
}
|
||||
Chunk {
|
||||
text: chunk,
|
||||
highlight_style,
|
||||
@@ -623,7 +693,7 @@ impl InlayMap {
|
||||
let inlay_id = if i % 2 == 0 {
|
||||
InlayId::Hint(post_inc(next_inlay_id))
|
||||
} else {
|
||||
InlayId::InlineCompletion(post_inc(next_inlay_id))
|
||||
InlayId::Suggestion(post_inc(next_inlay_id))
|
||||
};
|
||||
log::info!(
|
||||
"creating inlay {:?} at buffer offset {} with bias {:?} and text {:?}",
|
||||
@@ -990,13 +1060,21 @@ impl InlaySnapshot {
|
||||
let mut cursor = self.transforms.cursor::<(InlayOffset, usize)>(&());
|
||||
cursor.seek(&range.start, Bias::Right, &());
|
||||
|
||||
let mut highlight_endpoints = Vec::new();
|
||||
if let Some(text_highlights) = highlights.text_highlights {
|
||||
if !text_highlights.is_empty() {
|
||||
self.apply_text_highlights(
|
||||
&mut cursor,
|
||||
&range,
|
||||
text_highlights,
|
||||
&mut highlight_endpoints,
|
||||
);
|
||||
cursor.seek(&range.start, Bias::Right, &());
|
||||
}
|
||||
}
|
||||
highlight_endpoints.sort();
|
||||
let buffer_range = self.to_buffer_offset(range.start)..self.to_buffer_offset(range.end);
|
||||
let buffer_chunks = CustomHighlightsChunks::new(
|
||||
buffer_range,
|
||||
language_aware,
|
||||
highlights.text_highlights,
|
||||
&self.buffer,
|
||||
);
|
||||
let buffer_chunks = self.buffer.chunks(buffer_range, language_aware);
|
||||
|
||||
InlayChunks {
|
||||
transforms: cursor,
|
||||
@@ -1007,11 +1085,71 @@ impl InlaySnapshot {
|
||||
output_offset: range.start,
|
||||
max_output_offset: range.end,
|
||||
highlight_styles: highlights.styles,
|
||||
highlight_endpoints: highlight_endpoints.into_iter().peekable(),
|
||||
active_highlights: Default::default(),
|
||||
highlights,
|
||||
snapshot: self,
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_text_highlights(
|
||||
&self,
|
||||
cursor: &mut Cursor<'_, Transform, (InlayOffset, usize)>,
|
||||
range: &Range<InlayOffset>,
|
||||
text_highlights: &TreeMap<TypeId, Arc<(HighlightStyle, Vec<Range<Anchor>>)>>,
|
||||
highlight_endpoints: &mut Vec<HighlightEndpoint>,
|
||||
) {
|
||||
while cursor.start().0 < range.end {
|
||||
let transform_start = self
|
||||
.buffer
|
||||
.anchor_after(self.to_buffer_offset(cmp::max(range.start, cursor.start().0)));
|
||||
let transform_end =
|
||||
{
|
||||
let overshoot = InlayOffset(range.end.0 - cursor.start().0 .0);
|
||||
self.buffer.anchor_before(self.to_buffer_offset(cmp::min(
|
||||
cursor.end(&()).0,
|
||||
cursor.start().0 + overshoot,
|
||||
)))
|
||||
};
|
||||
|
||||
for (&tag, text_highlights) in text_highlights.iter() {
|
||||
let style = text_highlights.0;
|
||||
let ranges = &text_highlights.1;
|
||||
|
||||
let start_ix = match ranges.binary_search_by(|probe| {
|
||||
let cmp = probe.end.cmp(&transform_start, &self.buffer);
|
||||
if cmp.is_gt() {
|
||||
cmp::Ordering::Greater
|
||||
} else {
|
||||
cmp::Ordering::Less
|
||||
}
|
||||
}) {
|
||||
Ok(i) | Err(i) => i,
|
||||
};
|
||||
for range in &ranges[start_ix..] {
|
||||
if range.start.cmp(&transform_end, &self.buffer).is_ge() {
|
||||
break;
|
||||
}
|
||||
|
||||
highlight_endpoints.push(HighlightEndpoint {
|
||||
offset: self.to_inlay_offset(range.start.to_offset(&self.buffer)),
|
||||
is_start: true,
|
||||
tag,
|
||||
style,
|
||||
});
|
||||
highlight_endpoints.push(HighlightEndpoint {
|
||||
offset: self.to_inlay_offset(range.end.to_offset(&self.buffer)),
|
||||
is_start: false,
|
||||
tag,
|
||||
style,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
cursor.next(&());
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn text(&self) -> String {
|
||||
self.chunks(Default::default()..self.len(), false, Highlights::default())
|
||||
@@ -1067,12 +1205,11 @@ mod tests {
|
||||
hover_links::InlayHighlight,
|
||||
InlayId, MultiBuffer,
|
||||
};
|
||||
use gpui::{AppContext, HighlightStyle};
|
||||
use gpui::AppContext;
|
||||
use project::{InlayHint, InlayHintLabel, ResolveState};
|
||||
use rand::prelude::*;
|
||||
use settings::SettingsStore;
|
||||
use std::{any::TypeId, cmp::Reverse, env, sync::Arc};
|
||||
use sum_tree::TreeMap;
|
||||
use std::{cmp::Reverse, env, sync::Arc};
|
||||
use text::Patch;
|
||||
use util::post_inc;
|
||||
|
||||
@@ -1252,7 +1389,7 @@ mod tests {
|
||||
text: "|123|".into(),
|
||||
},
|
||||
Inlay {
|
||||
id: InlayId::InlineCompletion(post_inc(&mut next_inlay_id)),
|
||||
id: InlayId::Suggestion(post_inc(&mut next_inlay_id)),
|
||||
position: buffer.read(cx).snapshot(cx).anchor_after(3),
|
||||
text: "|456|".into(),
|
||||
},
|
||||
@@ -1468,7 +1605,7 @@ mod tests {
|
||||
text: "|456|".into(),
|
||||
},
|
||||
Inlay {
|
||||
id: InlayId::InlineCompletion(post_inc(&mut next_inlay_id)),
|
||||
id: InlayId::Suggestion(post_inc(&mut next_inlay_id)),
|
||||
position: buffer.read(cx).snapshot(cx).anchor_before(7),
|
||||
text: "\n|567|\n".into(),
|
||||
},
|
||||
|
||||
@@ -9,16 +9,13 @@ pub struct EditorSettings {
|
||||
pub cursor_blink: bool,
|
||||
pub cursor_shape: Option<CursorShape>,
|
||||
pub current_line_highlight: CurrentLineHighlight,
|
||||
pub lsp_highlight_debounce: u64,
|
||||
pub hover_popover_enabled: bool,
|
||||
pub hover_popover_delay: u64,
|
||||
pub toolbar: Toolbar,
|
||||
pub scrollbar: Scrollbar,
|
||||
pub gutter: Gutter,
|
||||
pub scroll_beyond_last_line: ScrollBeyondLastLine,
|
||||
pub vertical_scroll_margin: f32,
|
||||
pub autoscroll_on_clicks: bool,
|
||||
pub horizontal_scroll_margin: f32,
|
||||
pub scroll_sensitivity: f32,
|
||||
pub relative_line_numbers: bool,
|
||||
pub seed_search_query_from_cursor: SeedQuerySetting,
|
||||
@@ -35,7 +32,6 @@ pub struct EditorSettings {
|
||||
pub auto_signature_help: bool,
|
||||
pub show_signature_help_after_edits: bool,
|
||||
pub jupyter: Jupyter,
|
||||
pub show_inline_completions_in_menu: bool,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||
@@ -107,7 +103,6 @@ pub struct Scrollbar {
|
||||
pub search_results: bool,
|
||||
pub diagnostics: bool,
|
||||
pub cursors: bool,
|
||||
pub axes: ScrollbarAxes,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
|
||||
@@ -135,21 +130,6 @@ pub enum ShowScrollbar {
|
||||
Never,
|
||||
}
|
||||
|
||||
/// Forcefully enable or disable the scrollbar for each axis
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub struct ScrollbarAxes {
|
||||
/// When false, forcefully disables the horizontal scrollbar. Otherwise, obey other settings.
|
||||
///
|
||||
/// Default: true
|
||||
pub horizontal: bool,
|
||||
|
||||
/// When false, forcefully disables the vertical scrollbar. Otherwise, obey other settings.
|
||||
///
|
||||
/// Default: true
|
||||
pub vertical: bool,
|
||||
}
|
||||
|
||||
/// The key to use for adding multiple cursors
|
||||
///
|
||||
/// Default: alt
|
||||
@@ -205,20 +185,12 @@ pub struct EditorSettingsContent {
|
||||
///
|
||||
/// Default: all
|
||||
pub current_line_highlight: Option<CurrentLineHighlight>,
|
||||
/// The debounce delay before querying highlights from the language
|
||||
/// server based on the current cursor location.
|
||||
///
|
||||
/// Default: 75
|
||||
pub lsp_highlight_debounce: Option<u64>,
|
||||
/// Whether to show the informational hover box when moving the mouse
|
||||
/// over symbols in the editor.
|
||||
///
|
||||
/// Default: true
|
||||
pub hover_popover_enabled: Option<bool>,
|
||||
/// Time to wait before showing the informational hover box
|
||||
///
|
||||
/// Default: 350
|
||||
pub hover_popover_delay: Option<u64>,
|
||||
|
||||
/// Toolbar related settings
|
||||
pub toolbar: Option<ToolbarContent>,
|
||||
/// Scrollbar related settings
|
||||
@@ -237,10 +209,6 @@ pub struct EditorSettingsContent {
|
||||
///
|
||||
/// Default: false
|
||||
pub autoscroll_on_clicks: Option<bool>,
|
||||
/// The number of characters to keep on either side when scrolling with the mouse.
|
||||
///
|
||||
/// Default: 5.
|
||||
pub horizontal_scroll_margin: Option<f32>,
|
||||
/// Scroll sensitivity multiplier. This multiplier is applied
|
||||
/// to both the horizontal and vertical delta values while scrolling.
|
||||
///
|
||||
@@ -301,12 +269,6 @@ pub struct EditorSettingsContent {
|
||||
/// Default: false
|
||||
pub show_signature_help_after_edits: Option<bool>,
|
||||
|
||||
/// Whether to show the inline completions next to the completions provided by a language server.
|
||||
/// Only has an effect if inline completion provider supports it.
|
||||
///
|
||||
/// Default: true
|
||||
pub show_inline_completions_in_menu: Option<bool>,
|
||||
|
||||
/// Jupyter REPL settings.
|
||||
pub jupyter: Option<JupyterContent>,
|
||||
}
|
||||
@@ -356,22 +318,6 @@ pub struct ScrollbarContent {
|
||||
///
|
||||
/// Default: true
|
||||
pub cursors: Option<bool>,
|
||||
/// Forcefully enable or disable the scrollbar for each axis
|
||||
pub axes: Option<ScrollbarAxesContent>,
|
||||
}
|
||||
|
||||
/// Forcefully enable or disable the scrollbar for each axis
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
pub struct ScrollbarAxesContent {
|
||||
/// When false, forcefully disables the horizontal scrollbar. Otherwise, obey other settings.
|
||||
///
|
||||
/// Default: true
|
||||
horizontal: Option<bool>,
|
||||
|
||||
/// When false, forcefully disables the vertical scrollbar. Otherwise, obey other settings.
|
||||
///
|
||||
/// Default: true
|
||||
vertical: Option<bool>,
|
||||
}
|
||||
|
||||
/// Gutter related settings
|
||||
|
||||