Compare commits

..

9 Commits

Author SHA1 Message Date
Nate Butler
a3e04867ab Update git_ui.rs 2024-12-16 15:30:27 -05:00
Nate Butler
3abf165c1d Update git_panel.rs 2024-12-16 15:30:25 -05:00
Nate Butler
48eae645f2 Revert "Merge branch 'main' into git-panel-commit-editor"
This reverts commit 78b6cae754, reversing
changes made to 1cac4b6c00.
2024-12-16 15:29:44 -05:00
Nate Butler
78b6cae754 Merge branch 'main' into git-panel-commit-editor 2024-12-16 15:26:45 -05:00
Nate Butler
1cac4b6c00 Revert "wip"
This reverts commit aee641d79d.
2024-12-16 15:12:49 -05:00
Nate Butler
aee641d79d wip 2024-12-16 13:24:55 -05:00
Nate Butler
4f439ae35f Update git_panel.rs 2024-12-13 21:19:16 -05:00
Nate Butler
c2eea3a474 start on commit editor 2024-12-13 21:06:18 -05:00
Nate Butler
695f06c020 Checkpoint 2024-12-13 20:18:09 -05:00
152 changed files with 3370 additions and 6841 deletions

6
Cargo.lock generated
View File

@@ -2768,6 +2768,7 @@ dependencies = [
"language",
"menu",
"notifications",
"parking_lot",
"picker",
"pretty_assertions",
"project",
@@ -5181,14 +5182,17 @@ dependencies = [
"anyhow",
"collections",
"db",
"editor",
"git",
"gpui",
"language",
"project",
"schemars",
"serde",
"serde_derive",
"serde_json",
"settings",
"theme",
"ui",
"util",
"windows 0.58.0",
@@ -12634,7 +12638,6 @@ dependencies = [
"sha2",
"shellexpand 2.1.2",
"util",
"zed_actions",
]
[[package]]
@@ -16107,7 +16110,6 @@ name = "zed_actions"
version = "0.1.0"
dependencies = [
"gpui",
"schemars",
"serde",
]

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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
@@ -474,12 +470,18 @@
"context": "Editor && showing_completions",
"use_key_equivalents": true,
"bindings": {
"enter": "editor::ConfirmCompletion",
"enter": "editor::ConfirmCompletion"
}
},
{
"context": "Editor && !inline_completion && showing_completions",
"use_key_equivalents": true,
"bindings": {
"tab": "editor::ComposeCompletion"
}
},
{
"context": "Editor && inline_completion && !showing_completions",
"context": "Editor && inline_completion",
"use_key_equivalents": true,
"bindings": {
"tab": "editor::AcceptInlineCompletion"

View File

@@ -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
@@ -545,12 +541,18 @@
"context": "Editor && showing_completions",
"use_key_equivalents": true,
"bindings": {
"enter": "editor::ConfirmCompletion",
"enter": "editor::ConfirmCompletion"
}
},
{
"context": "Editor && !inline_completion && showing_completions",
"use_key_equivalents": true,
"bindings": {
"tab": "editor::ComposeCompletion"
}
},
{
"context": "Editor && inline_completion && !showing_completions",
"context": "Editor && inline_completion",
"use_key_equivalents": true,
"bindings": {
"tab": "editor::AcceptInlineCompletion"
@@ -610,7 +612,6 @@
"context": "PromptEditor",
"use_key_equivalents": true,
"bindings": {
"cmd-shift-a": "assistant2::ToggleContextPicker",
"ctrl-[": "assistant::CyclePreviousInlineAssist",
"ctrl-]": "assistant::CycleNextInlineAssist"
}

View File

@@ -4,32 +4,12 @@
"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",
@@ -37,8 +17,6 @@
"ctrl-shift-a": "editor::SelectLargerSyntaxNode",
"ctrl-shift-d": "editor::DuplicateSelection",
"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",

View File

@@ -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"
}
},
{
@@ -38,8 +20,6 @@
"cmd-shift-a": "editor::SelectLargerSyntaxNode",
"cmd-shift-d": "editor::DuplicateSelection",
"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",

View File

@@ -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.
@@ -554,8 +552,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

View File

@@ -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

View File

@@ -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",

View File

@@ -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;

View File

@@ -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)

View File

@@ -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,

View File

@@ -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(),
}),
@@ -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))
@@ -1556,7 +1544,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)),
)
})),
@@ -1849,10 +1837,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(_) => {

View File

@@ -1439,7 +1439,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(

View File

@@ -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();
@@ -324,7 +326,7 @@ 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))

View File

@@ -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(),
)]
}
}

View File

@@ -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();

View File

@@ -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),
),
),
),
),
@@ -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),

View File

@@ -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()
}
}

View File

@@ -3,8 +3,6 @@ mod assistant_panel;
mod assistant_settings;
mod context;
mod context_picker;
mod context_store;
mod context_strip;
mod inline_assistant;
mod message_editor;
mod prompts;
@@ -15,9 +13,9 @@ mod thread_history;
mod thread_store;
mod ui;
use std::any::TypeId;
use std::sync::Arc;
use assistant_settings::AssistantSettings;
use client::Client;
use command_palette_hooks::CommandPaletteFilter;
use feature_flags::{Assistant2FeatureFlag, FeatureFlagAppExt};
@@ -28,18 +26,16 @@ 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,
ToggleInlineAssist,
CycleNextInlineAssist,
CyclePreviousInlineAssist
]
@@ -81,8 +77,6 @@ pub fn init(fs: Arc<dyn Fs>, client: Arc<Client>, stdout_is_a_pty: bool, cx: &mu
fn feature_gate_assistant2_actions(cx: &mut AppContext) {
const ASSISTANT1_NAMESPACE: &str = "assistant";
let inline_assist_actions = [TypeId::of::<zed_actions::InlineAssist>()];
CommandPaletteFilter::update_global(cx, |filter, _cx| {
filter.hide_namespace(NAMESPACE);
});
@@ -92,11 +86,6 @@ fn feature_gate_assistant2_actions(cx: &mut AppContext) {
CommandPaletteFilter::update_global(cx, |filter, _cx| {
filter.show_namespace(NAMESPACE);
filter.hide_namespace(ASSISTANT1_NAMESPACE);
// We're hiding all of the `assistant: ` actions, but we want to
// keep the inline assist action around so we can use the same
// one in Assistant2.
filter.show_action_types(inline_assist_actions.iter());
});
} else {
CommandPaletteFilter::update_global(cx, |filter, _cx| {

View File

@@ -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};
@@ -42,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>,
@@ -51,8 +47,6 @@ pub struct AssistantPanel {
local_timezone: UtcOffset,
active_view: ActiveView,
history: View<ThreadHistory>,
width: Option<Pixels>,
height: Option<Pixels>,
}
impl AssistantPanel {
@@ -82,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();
@@ -90,7 +83,6 @@ 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| {
@@ -102,23 +94,13 @@ impl AssistantPanel {
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(workspace, 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,
}
}
@@ -126,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
@@ -145,15 +123,8 @@ 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(self.workspace.clone(), thread, cx));
self.message_editor.focus_handle(cx).focus(cx);
}
@@ -175,15 +146,8 @@ 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(self.workspace.clone(), thread, cx));
self.message_editor.focus_handle(cx).focus(cx);
}
@@ -217,38 +181,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>) {}
@@ -281,17 +220,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({
@@ -311,6 +248,7 @@ impl AssistantPanel {
)
.child(
IconButton::new("open-history", IconName::HistoryRerun)
.shape(IconButtonShape::Square)
.icon_size(IconSize::Small)
.style(ButtonStyle::Subtle)
.tooltip({
@@ -330,6 +268,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))
@@ -355,6 +294,7 @@ impl AssistantPanel {
v_flex()
.gap_2()
.mx_auto()
.child(
v_flex().w_full().child(
svg()
@@ -369,14 +309,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())),
@@ -583,7 +522,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)),

View File

@@ -157,22 +157,6 @@ impl AssistantSettingsContent {
}
}
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();

View File

@@ -1,5 +1,4 @@
use gpui::SharedString;
use language_model::{LanguageModelRequestMessage, MessageContent};
use serde::{Deserialize, Serialize};
use util::post_inc;
@@ -25,53 +24,4 @@ pub struct Context {
pub enum ContextKind {
File,
FetchedUrl,
Thread,
}
pub fn attach_context_to_message(
message: &mut LanguageModelRequestMessage,
context: impl IntoIterator<Item = Context>,
) {
let mut file_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::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 !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);
}
message.content.push(MessageContent::Text(context_text));
}

View File

@@ -1,30 +1,26 @@
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,
WeakView,
};
use picker::{Picker, PickerDelegate};
use ui::{prelude::*, ListItem, ListItemSpacing};
use ui::{prelude::*, ListItem, ListItemSpacing, Tooltip};
use util::ResultExt;
use workspace::Workspace;
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>),
Fetch(View<FetchContextPicker>),
Thread(View<ThreadContextPicker>),
}
pub(super) struct ContextPicker {
@@ -35,38 +31,30 @@ pub(super) struct ContextPicker {
impl ContextPicker {
pub fn new(
workspace: WeakView<Workspace>,
thread_store: Option<WeakModel<ThreadStore>>,
context_store: WeakModel<ContextStore>,
message_editor: WeakView<MessageEditor>,
cx: &mut ViewContext<Self>,
) -> Self {
let mut entries = vec![
ContextPickerEntry {
name: "Directory".into(),
icon: IconName::Folder,
},
ContextPickerEntry {
name: "File".into(),
icon: IconName::File,
},
ContextPickerEntry {
name: "Fetch".into(),
icon: IconName::Globe,
},
];
if thread_store.is_some() {
entries.push(ContextPickerEntry {
name: "Thread".into(),
icon: IconName::MessageCircle,
});
}
let delegate = ContextPickerDelegate {
context_picker: cx.view().downgrade(),
workspace,
thread_store,
context_store,
entries,
workspace: workspace.clone(),
message_editor: message_editor.clone(),
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: "fetch".into(),
description: "Fetch content from URL".into(),
icon: IconName::Globe,
},
],
selected_ix: 0,
};
@@ -93,7 +81,6 @@ impl FocusableView for ContextPicker {
ContextPickerMode::Default => self.picker.focus_handle(cx),
ContextPickerMode::File(file_picker) => file_picker.focus_handle(cx),
ContextPickerMode::Fetch(fetch_picker) => fetch_picker.focus_handle(cx),
ContextPickerMode::Thread(thread_picker) => thread_picker.focus_handle(cx),
}
}
}
@@ -107,7 +94,6 @@ impl Render for ContextPicker {
ContextPickerMode::Default => parent.child(self.picker.clone()),
ContextPickerMode::File(file_picker) => parent.child(file_picker.clone()),
ContextPickerMode::Fetch(fetch_picker) => parent.child(fetch_picker.clone()),
ContextPickerMode::Thread(thread_picker) => parent.child(thread_picker.clone()),
})
}
}
@@ -115,14 +101,14 @@ impl Render for ContextPicker {
#[derive(Clone)]
struct ContextPickerEntry {
name: SharedString,
description: SharedString,
icon: IconName,
}
pub(crate) struct ContextPickerDelegate {
context_picker: WeakView<ContextPicker>,
workspace: WeakView<Workspace>,
thread_store: Option<WeakModel<ThreadStore>>,
context_store: WeakModel<ContextStore>,
message_editor: WeakView<MessageEditor>,
entries: Vec<ContextPickerEntry>,
selected_ix: usize,
}
@@ -156,38 +142,26 @@ impl PickerDelegate for ContextPickerDelegate {
self.context_picker
.update(cx, |this, cx| {
match entry.name.to_string().as_str() {
"File" => {
"file" => {
this.mode = ContextPickerMode::File(cx.new_view(|cx| {
FileContextPicker::new(
self.context_picker.clone(),
self.workspace.clone(),
self.context_store.clone(),
self.message_editor.clone(),
cx,
)
}));
}
"Fetch" => {
"fetch" => {
this.mode = ContextPickerMode::Fetch(cx.new_view(|cx| {
FetchContextPicker::new(
self.context_picker.clone(),
self.workspace.clone(),
self.context_store.clone(),
self.message_editor.clone(),
cx,
)
}));
}
"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,
)
}));
}
}
_ => {}
}
@@ -201,9 +175,7 @@ impl PickerDelegate for ContextPickerDelegate {
self.context_picker
.update(cx, |this, cx| match this.mode {
ContextPickerMode::Default => cx.emit(DismissEvent),
ContextPickerMode::File(_)
| ContextPickerMode::Fetch(_)
| ContextPickerMode::Thread(_) => {}
ContextPickerMode::File(_) | ContextPickerMode::Fetch(_) => {}
})
.log_err();
}
@@ -221,13 +193,34 @@ impl PickerDelegate for ContextPickerDelegate {
.inset(true)
.spacing(ListItemSpacing::Dense)
.toggle_state(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),
),
),
),
)
}

View File

@@ -4,16 +4,16 @@ 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 gpui::{AppContext, DismissEvent, FocusHandle, FocusableView, Task, View, 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 ui::{prelude::*, ListItem, ListItemSpacing, ViewContext};
use workspace::Workspace;
use crate::context::ContextKind;
use crate::context_picker::ContextPicker;
use crate::context_store::ContextStore;
use crate::message_editor::MessageEditor;
pub struct FetchContextPicker {
picker: View<Picker<FetchContextPickerDelegate>>,
@@ -23,10 +23,10 @@ impl FetchContextPicker {
pub fn new(
context_picker: WeakView<ContextPicker>,
workspace: WeakView<Workspace>,
context_store: WeakModel<ContextStore>,
message_editor: WeakView<MessageEditor>,
cx: &mut ViewContext<Self>,
) -> Self {
let delegate = FetchContextPickerDelegate::new(context_picker, workspace, context_store);
let delegate = FetchContextPickerDelegate::new(context_picker, workspace, message_editor);
let picker = cx.new_view(|cx| Picker::uniform_list(delegate, cx));
Self { picker }
@@ -55,7 +55,7 @@ enum ContentType {
pub struct FetchContextPickerDelegate {
context_picker: WeakView<ContextPicker>,
workspace: WeakView<Workspace>,
context_store: WeakModel<ContextStore>,
message_editor: WeakView<MessageEditor>,
url: String,
}
@@ -63,12 +63,12 @@ impl FetchContextPickerDelegate {
pub fn new(
context_picker: WeakView<ContextPicker>,
workspace: WeakView<Workspace>,
context_store: WeakModel<ContextStore>,
message_editor: WeakView<MessageEditor>,
) -> Self {
FetchContextPickerDelegate {
context_picker,
workspace,
context_store,
message_editor,
url: String::new(),
}
}
@@ -150,15 +150,7 @@ 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()
1
}
fn selected_index(&self) -> usize {
@@ -189,9 +181,9 @@ impl PickerDelegate for FetchContextPickerDelegate {
this.update(&mut cx, |this, cx| {
this.delegate
.context_store
.update(cx, |context_store, _cx| {
context_store.insert_context(ContextKind::FetchedUrl, url, text);
.message_editor
.update(cx, |message_editor, _cx| {
message_editor.insert_context(ContextKind::FetchedUrl, url, text);
})
})??;
@@ -218,8 +210,9 @@ impl PickerDelegate for FetchContextPickerDelegate {
Some(
ListItem::new(ix)
.inset(true)
.spacing(ListItemSpacing::Sparse)
.toggle_state(selected)
.child(Label::new(self.url.clone())),
.child(self.url.clone()),
)
}
}

View File

@@ -5,16 +5,16 @@ use std::sync::atomic::AtomicBool;
use std::sync::Arc;
use fuzzy::PathMatch;
use gpui::{AppContext, DismissEvent, FocusHandle, FocusableView, Task, View, WeakModel, WeakView};
use gpui::{AppContext, DismissEvent, FocusHandle, FocusableView, Task, View, WeakView};
use picker::{Picker, PickerDelegate};
use project::{PathMatchCandidateSet, WorktreeId};
use ui::{prelude::*, ListItem};
use ui::{prelude::*, ListItem, ListItemSpacing};
use util::ResultExt as _;
use workspace::Workspace;
use crate::context::ContextKind;
use crate::context_picker::ContextPicker;
use crate::context_store::ContextStore;
use crate::message_editor::MessageEditor;
pub struct FileContextPicker {
picker: View<Picker<FileContextPickerDelegate>>,
@@ -24,10 +24,10 @@ impl FileContextPicker {
pub fn new(
context_picker: WeakView<ContextPicker>,
workspace: WeakView<Workspace>,
context_store: WeakModel<ContextStore>,
message_editor: WeakView<MessageEditor>,
cx: &mut ViewContext<Self>,
) -> Self {
let delegate = FileContextPickerDelegate::new(context_picker, workspace, context_store);
let delegate = FileContextPickerDelegate::new(context_picker, workspace, message_editor);
let picker = cx.new_view(|cx| Picker::uniform_list(delegate, cx));
Self { picker }
@@ -49,7 +49,7 @@ impl Render for FileContextPicker {
pub struct FileContextPickerDelegate {
context_picker: WeakView<ContextPicker>,
workspace: WeakView<Workspace>,
context_store: WeakModel<ContextStore>,
message_editor: WeakView<MessageEditor>,
matches: Vec<PathMatch>,
selected_index: usize,
}
@@ -58,12 +58,12 @@ impl FileContextPickerDelegate {
pub fn new(
context_picker: WeakView<ContextPicker>,
workspace: WeakView<Workspace>,
context_store: WeakModel<ContextStore>,
message_editor: WeakView<MessageEditor>,
) -> Self {
Self {
context_picker,
workspace,
context_store,
message_editor,
matches: Vec::new(),
selected_index: 0,
}
@@ -214,22 +214,24 @@ impl PickerDelegate for FileContextPickerDelegate {
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');
}
this.delegate
.message_editor
.update(cx, |message_editor, 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");
text.push_str("```\n");
context_store.insert_context(
ContextKind::File,
path.to_string_lossy().to_string(),
text,
);
})
message_editor.insert_context(
ContextKind::File,
path.to_string_lossy().to_string(),
text,
);
})
})??;
anyhow::Ok(())
@@ -252,29 +254,14 @@ impl PickerDelegate for FileContextPickerDelegate {
selected: bool,
_cx: &mut ViewContext<Picker<Self>>,
) -> Option<Self::ListItem> {
let path_match = &self.matches[ix];
let file_name = path_match
.path
.file_name()
.unwrap_or_default()
.to_string_lossy()
.to_string();
let directory = path_match
.path
.parent()
.map(|directory| format!("{}/", directory.to_string_lossy()));
let mat = &self.matches[ix];
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)
})),
),
ListItem::new(ix)
.inset(true)
.spacing(ListItemSpacing::Sparse)
.toggle_state(selected)
.child(mat.path.to_string_lossy().to_string()),
)
}
}

View File

@@ -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()),
)
}
}

View File

@@ -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);
}
}

View File

@@ -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();
})
}),
)
})
}
}

View File

@@ -1,16 +1,10 @@
use crate::context::attach_context_to_message;
use crate::context_picker::ContextPicker;
use crate::context_store::ContextStore;
use crate::context_strip::ContextStrip;
use crate::thread_store::ThreadStore;
use crate::{
assistant_settings::AssistantSettings,
prompts::PromptBuilder,
streaming_diff::{CharOperation, LineDiff, LineOperation, StreamingDiff},
terminal_inline_assistant::TerminalInlineAssistant,
CycleNextInlineAssist, CyclePreviousInlineAssist,
CycleNextInlineAssist, CyclePreviousInlineAssist, ToggleInlineAssist,
};
use crate::{AssistantPanel, ToggleContextPicker};
use anyhow::{Context as _, Result};
use client::{telemetry::Telemetry, ErrorExt};
use collections::{hash_map, HashMap, HashSet, VecDeque};
@@ -30,8 +24,7 @@ use futures::{channel::mpsc, future::LocalBoxFuture, join, SinkExt, Stream, Stre
use gpui::{
anchored, deferred, point, AnyElement, AppContext, ClickEvent, CursorStyle, EventEmitter,
FocusHandle, FocusableView, FontWeight, Global, HighlightStyle, Model, ModelContext,
Subscription, Task, TextStyle, UpdateGlobal, View, ViewContext, WeakModel, WeakView,
WindowContext,
Subscription, Task, TextStyle, UpdateGlobal, View, ViewContext, WeakView, WindowContext,
};
use language::{Buffer, IndentKind, Point, Selection, TransactionId};
use language_model::{
@@ -52,7 +45,6 @@ use std::{
iter, mem,
ops::{Range, RangeInclusive},
pin::Pin,
rc::Rc,
sync::Arc,
task::{self, Poll},
time::Instant,
@@ -61,9 +53,7 @@ use telemetry_events::{AssistantEvent, AssistantKind, AssistantPhase};
use terminal_view::{terminal_panel::TerminalPanel, TerminalView};
use text::{OffsetRangeExt, ToPoint as _};
use theme::ThemeSettings;
use ui::{
prelude::*, CheckboxWithLabel, IconButtonShape, KeyBinding, Popover, PopoverMenuHandle, Tooltip,
};
use ui::{prelude::*, CheckboxWithLabel, IconButtonShape, KeyBinding, Popover, Tooltip};
use util::{RangeExt, ResultExt};
use workspace::{dock::Panel, ShowConfiguration};
use workspace::{notifications::NotificationId, ItemHandle, Toast, Workspace};
@@ -75,7 +65,9 @@ pub fn init(
cx: &mut AppContext,
) {
cx.set_global(InlineAssistant::new(fs, prompt_builder, telemetry));
cx.observe_new_views(|_workspace: &mut Workspace, cx| {
cx.observe_new_views(|workspace: &mut Workspace, cx| {
workspace.register_action(InlineAssistant::toggle_inline_assist);
let workspace = cx.view().clone();
InlineAssistant::update_global(cx, |inline_assistant, cx| {
inline_assistant.register_workspace(&workspace, cx)
@@ -185,16 +177,10 @@ impl InlineAssistant {
) {
if let Some(editor) = item.act_as::<Editor>(cx) {
editor.update(cx, |editor, cx| {
let thread_store = workspace
.read(cx)
.panel::<AssistantPanel>(cx)
.map(|assistant_panel| assistant_panel.read(cx).thread_store().downgrade());
editor.push_code_action_provider(
Rc::new(AssistantCodeActionProvider {
Arc::new(AssistantCodeActionProvider {
editor: cx.view().downgrade(),
workspace: workspace.downgrade(),
thread_store,
}),
cx,
);
@@ -202,9 +188,9 @@ impl InlineAssistant {
}
}
pub fn inline_assist(
pub fn toggle_inline_assist(
workspace: &mut Workspace,
_action: &zed_actions::InlineAssist,
_action: &ToggleInlineAssist,
cx: &mut ViewContext<Workspace>,
) {
let settings = AssistantSettings::get_global(cx);
@@ -222,19 +208,15 @@ impl InlineAssistant {
.map_or(false, |provider| provider.is_authenticated(cx))
};
let thread_store = workspace
.panel::<AssistantPanel>(cx)
.map(|assistant_panel| assistant_panel.read(cx).thread_store().downgrade());
let handle_assist = |cx: &mut ViewContext<Workspace>| match inline_assist_target {
InlineAssistTarget::Editor(active_editor) => {
InlineAssistant::update_global(cx, |assistant, cx| {
assistant.assist(&active_editor, cx.view().downgrade(), thread_store, cx)
assistant.assist(&active_editor, Some(cx.view().downgrade()), cx)
})
}
InlineAssistTarget::Terminal(active_terminal) => {
TerminalInlineAssistant::update_global(cx, |assistant, cx| {
assistant.assist(&active_terminal, cx.view().downgrade(), thread_store, cx)
assistant.assist(&active_terminal, Some(cx.view().downgrade()), cx)
})
}
};
@@ -281,8 +263,7 @@ impl InlineAssistant {
pub fn assist(
&mut self,
editor: &View<Editor>,
workspace: WeakView<Workspace>,
thread_store: Option<WeakModel<ThreadStore>>,
workspace: Option<WeakView<Workspace>>,
cx: &mut WindowContext,
) {
let (snapshot, initial_selections) = editor.update(cx, |editor, cx| {
@@ -361,13 +342,11 @@ impl InlineAssistant {
let mut assist_to_focus = None;
for range in codegen_ranges {
let assist_id = self.next_assist_id.post_inc();
let context_store = cx.new_model(|_cx| ContextStore::new());
let codegen = cx.new_model(|cx| {
Codegen::new(
editor.read(cx).buffer().clone(),
range.clone(),
None,
context_store.clone(),
self.telemetry.clone(),
self.prompt_builder.clone(),
cx,
@@ -383,9 +362,6 @@ impl InlineAssistant {
prompt_buffer.clone(),
codegen.clone(),
self.fs.clone(),
context_store,
workspace.clone(),
thread_store.clone(),
cx,
)
});
@@ -452,8 +428,7 @@ impl InlineAssistant {
initial_prompt: String,
initial_transaction_id: Option<TransactionId>,
focus: bool,
workspace: WeakView<Workspace>,
thread_store: Option<WeakModel<ThreadStore>>,
workspace: Option<WeakView<Workspace>>,
cx: &mut WindowContext,
) -> InlineAssistId {
let assist_group_id = self.next_assist_group_id.post_inc();
@@ -469,14 +444,11 @@ impl InlineAssistant {
range.end = range.end.bias_right(&snapshot);
}
let context_store = cx.new_model(|_cx| ContextStore::new());
let codegen = cx.new_model(|cx| {
Codegen::new(
editor.read(cx).buffer().clone(),
range.clone(),
initial_transaction_id,
context_store.clone(),
self.telemetry.clone(),
self.prompt_builder.clone(),
cx,
@@ -492,9 +464,6 @@ impl InlineAssistant {
prompt_buffer.clone(),
codegen.clone(),
self.fs.clone(),
context_store,
workspace.clone(),
thread_store,
cx,
)
});
@@ -1486,8 +1455,6 @@ enum PromptEditorEvent {
struct PromptEditor {
id: InlineAssistId,
editor: View<Editor>,
context_strip: View<ContextStrip>,
context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
language_model_selector: View<LanguageModelSelector>,
edited_since_done: bool,
gutter_dimensions: Arc<Mutex<GutterDimensions>>,
@@ -1505,7 +1472,11 @@ impl EventEmitter<PromptEditorEvent> for PromptEditor {}
impl Render for PromptEditor {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let gutter_dimensions = *self.gutter_dimensions.lock();
let mut buttons = Vec::new();
let mut buttons = vec![Button::new("add-context", "Add Context")
.style(ButtonStyle::Filled)
.icon(IconName::Plus)
.icon_position(IconPosition::Start)
.into_any_element()];
let codegen = self.codegen.read(cx);
if codegen.alternative_count(cx) > 1 {
buttons.push(self.render_cycle_controls(cx));
@@ -1598,115 +1569,91 @@ impl Render for PromptEditor {
}
});
v_flex()
h_flex()
.key_context("PromptEditor")
.bg(cx.theme().colors().editor_background)
.block_mouse_down()
.cursor(CursorStyle::Arrow)
.border_y_1()
.border_color(cx.theme().status().info_border)
.size_full()
.py(cx.line_height() / 2.5)
.on_action(cx.listener(Self::confirm))
.on_action(cx.listener(Self::cancel))
.on_action(cx.listener(Self::move_up))
.on_action(cx.listener(Self::move_down))
.capture_action(cx.listener(Self::cycle_prev))
.capture_action(cx.listener(Self::cycle_next))
.child(
h_flex()
.key_context("PromptEditor")
.bg(cx.theme().colors().editor_background)
.block_mouse_down()
.cursor(CursorStyle::Arrow)
.on_action(cx.listener(Self::toggle_context_picker))
.on_action(cx.listener(Self::confirm))
.on_action(cx.listener(Self::cancel))
.on_action(cx.listener(Self::move_up))
.on_action(cx.listener(Self::move_down))
.capture_action(cx.listener(Self::cycle_prev))
.capture_action(cx.listener(Self::cycle_next))
.child(
h_flex()
.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,
)
}),
))
.map(|el| {
let CodegenStatus::Error(error) = self.codegen.read(cx).status(cx)
else {
return el;
};
let error_message = SharedString::from(error.to_string());
if error.error_code() == proto::ErrorCode::RateLimitExceeded
&& cx.has_flag::<ZedPro>()
{
el.child(
v_flex()
.child(
IconButton::new(
"rate-limit-error",
IconName::XCircle,
)
.toggle_state(self.show_rate_limit_notice)
.shape(IconButtonShape::Square)
.icon_size(IconSize::Small)
.on_click(
cx.listener(Self::toggle_rate_limit_notice),
),
)
.children(self.show_rate_limit_notice.then(|| {
deferred(
anchored()
.position_mode(
gpui::AnchoredPositionMode::Local,
)
.position(point(px(0.), px(24.)))
.anchor(gpui::Corner::TopLeft)
.child(self.render_rate_limit_notice(cx)),
)
})),
)
} else {
el.child(
div()
.id("error")
.tooltip(move |cx| {
Tooltip::text(error_message.clone(), cx)
})
.child(
Icon::new(IconName::XCircle)
.size(IconSize::Small)
.color(Color::Error),
),
)
}
.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(div().flex_1().child(self.render_editor(cx)))
.child(h_flex().gap_2().pr_6().children(buttons)),
)
.child(
h_flex()
.child(
h_flex()
.w(gutter_dimensions.full_width() + (gutter_dimensions.margin / 2.0))
.justify_center()
.gap_2(),
)
.child(self.context_strip.clone()),
))
.map(|el| {
let CodegenStatus::Error(error) = self.codegen.read(cx).status(cx) else {
return el;
};
let error_message = SharedString::from(error.to_string());
if error.error_code() == proto::ErrorCode::RateLimitExceeded
&& cx.has_flag::<ZedPro>()
{
el.child(
v_flex()
.child(
IconButton::new("rate-limit-error", IconName::XCircle)
.toggle_state(self.show_rate_limit_notice)
.shape(IconButtonShape::Square)
.icon_size(IconSize::Small)
.on_click(cx.listener(Self::toggle_rate_limit_notice)),
)
.children(self.show_rate_limit_notice.then(|| {
deferred(
anchored()
.position_mode(gpui::AnchoredPositionMode::Local)
.position(point(px(0.), px(24.)))
.anchor(gpui::AnchorCorner::TopLeft)
.child(self.render_rate_limit_notice(cx)),
)
})),
)
} else {
el.child(
div()
.id("error")
.tooltip(move |cx| Tooltip::text(error_message.clone(), cx))
.child(
Icon::new(IconName::XCircle)
.size(IconSize::Small)
.color(Color::Error),
),
)
}
}),
)
.child(div().flex_1().child(self.render_editor(cx)))
.child(h_flex().gap_2().pr_6().children(buttons))
}
}
@@ -1727,9 +1674,6 @@ impl PromptEditor {
prompt_buffer: Model<MultiBuffer>,
codegen: Model<Codegen>,
fs: Arc<dyn Fs>,
context_store: Model<ContextStore>,
workspace: WeakView<Workspace>,
thread_store: Option<WeakModel<ThreadStore>>,
cx: &mut ViewContext<Self>,
) -> Self {
let prompt_editor = cx.new_view(|cx| {
@@ -1750,22 +1694,10 @@ impl PromptEditor {
editor.set_placeholder_text(Self::placeholder_text(codegen.read(cx)), cx);
editor
});
let context_picker_menu_handle = PopoverMenuHandle::default();
let mut this = Self {
id,
editor: prompt_editor.clone(),
context_strip: cx.new_view(|cx| {
ContextStrip::new(
context_store,
workspace.clone(),
thread_store.clone(),
prompt_editor.focus_handle(cx),
context_picker_menu_handle.clone(),
cx,
)
}),
context_picker_menu_handle,
editor: prompt_editor,
language_model_selector: cx.new_view(|cx| {
let fs = fs.clone();
LanguageModelSelector::new(
@@ -1923,10 +1855,6 @@ impl PromptEditor {
}
}
fn toggle_context_picker(&mut self, _: &ToggleContextPicker, cx: &mut ViewContext<Self>) {
self.context_picker_menu_handle.toggle(cx);
}
fn cancel(&mut self, _: &editor::actions::Cancel, cx: &mut ViewContext<Self>) {
match self.codegen.read(cx).status(cx) {
CodegenStatus::Idle | CodegenStatus::Done | CodegenStatus::Error(_) => {
@@ -2237,7 +2165,7 @@ pub struct InlineAssist {
decorations: Option<InlineAssistDecorations>,
codegen: Model<Codegen>,
_subscriptions: Vec<Subscription>,
workspace: WeakView<Workspace>,
workspace: Option<WeakView<Workspace>>,
}
impl InlineAssist {
@@ -2251,7 +2179,7 @@ impl InlineAssist {
end_block_id: CustomBlockId,
range: Range<Anchor>,
codegen: Model<Codegen>,
workspace: WeakView<Workspace>,
workspace: Option<WeakView<Workspace>>,
cx: &mut WindowContext,
) -> Self {
let prompt_editor_focus_handle = prompt_editor.focus_handle(cx);
@@ -2311,7 +2239,11 @@ impl InlineAssist {
if let CodegenStatus::Error(error) = codegen.read(cx).status(cx) {
if assist.decorations.is_none() {
if let Some(workspace) = assist.workspace.upgrade() {
if let Some(workspace) = assist
.workspace
.as_ref()
.and_then(|workspace| workspace.upgrade())
{
let error = format!("Inline assistant error: {}", error);
workspace.update(cx, |workspace, cx| {
struct InlineAssistantError;
@@ -2364,7 +2296,6 @@ pub struct Codegen {
buffer: Model<MultiBuffer>,
range: Range<Anchor>,
initial_transaction_id: Option<TransactionId>,
context_store: Model<ContextStore>,
telemetry: Arc<Telemetry>,
builder: Arc<PromptBuilder>,
is_insertion: bool,
@@ -2375,7 +2306,6 @@ impl Codegen {
buffer: Model<MultiBuffer>,
range: Range<Anchor>,
initial_transaction_id: Option<TransactionId>,
context_store: Model<ContextStore>,
telemetry: Arc<Telemetry>,
builder: Arc<PromptBuilder>,
cx: &mut ModelContext<Self>,
@@ -2385,7 +2315,6 @@ impl Codegen {
buffer.clone(),
range.clone(),
false,
Some(context_store.clone()),
Some(telemetry.clone()),
builder.clone(),
cx,
@@ -2400,7 +2329,6 @@ impl Codegen {
buffer,
range,
initial_transaction_id,
context_store,
telemetry,
builder,
};
@@ -2473,7 +2401,6 @@ impl Codegen {
self.buffer.clone(),
self.range.clone(),
false,
Some(self.context_store.clone()),
Some(self.telemetry.clone()),
self.builder.clone(),
cx,
@@ -2553,7 +2480,6 @@ pub struct CodegenAlternative {
status: CodegenStatus,
generation: Task<()>,
diff: Diff,
context_store: Option<Model<ContextStore>>,
telemetry: Option<Arc<Telemetry>>,
_subscription: gpui::Subscription,
builder: Arc<PromptBuilder>,
@@ -2592,7 +2518,6 @@ impl CodegenAlternative {
buffer: Model<MultiBuffer>,
range: Range<Anchor>,
active: bool,
context_store: Option<Model<ContextStore>>,
telemetry: Option<Arc<Telemetry>>,
builder: Arc<PromptBuilder>,
cx: &mut ModelContext<Self>,
@@ -2630,7 +2555,6 @@ impl CodegenAlternative {
status: CodegenStatus::Idle,
generation: Task::ready(()),
diff: Diff::default(),
context_store,
telemetry,
_subscription: cx.subscribe(&buffer, Self::handle_buffer_event),
builder,
@@ -2716,11 +2640,7 @@ impl CodegenAlternative {
Ok(())
}
fn build_request(
&self,
user_prompt: String,
cx: &mut AppContext,
) -> Result<LanguageModelRequest> {
fn build_request(&self, user_prompt: String, cx: &AppContext) -> Result<LanguageModelRequest> {
let buffer = self.buffer.read(cx).snapshot(cx);
let language = buffer.language_at(self.range.start);
let language_name = if let Some(language) = language.as_ref() {
@@ -2753,24 +2673,15 @@ impl CodegenAlternative {
.generate_inline_transformation_prompt(user_prompt, language_name, buffer, range)
.map_err(|e| anyhow::anyhow!("Failed to generate content prompt: {}", e))?;
let mut request_message = LanguageModelRequestMessage {
role: Role::User,
content: Vec::new(),
cache: false,
};
if let Some(context_store) = &self.context_store {
let context = context_store.update(cx, |this, _cx| this.context().clone());
attach_context_to_message(&mut request_message, context);
}
request_message.content.push(prompt.into());
Ok(LanguageModelRequest {
tools: Vec::new(),
stop: Vec::new(),
temperature: None,
messages: vec![request_message],
messages: vec![LanguageModelRequestMessage {
role: Role::User,
content: vec![prompt.into()],
cache: false,
}],
})
}
@@ -3365,7 +3276,6 @@ where
struct AssistantCodeActionProvider {
editor: WeakView<Editor>,
workspace: WeakView<Workspace>,
thread_store: Option<WeakModel<ThreadStore>>,
}
impl CodeActionProvider for AssistantCodeActionProvider {
@@ -3430,7 +3340,6 @@ impl CodeActionProvider for AssistantCodeActionProvider {
) -> Task<Result<ProjectTransaction>> {
let editor = self.editor.clone();
let workspace = self.workspace.clone();
let thread_store = self.thread_store.clone();
cx.spawn(|mut cx| async move {
let editor = editor.upgrade().context("editor was released")?;
let range = editor
@@ -3477,8 +3386,7 @@ impl CodeActionProvider for AssistantCodeActionProvider {
"Fix Diagnostics".into(),
None,
true,
workspace,
thread_store,
Some(workspace),
cx,
);
assistant.start_assist(assist_id, cx);
@@ -3564,7 +3472,6 @@ mod tests {
range.clone(),
true,
None,
None,
prompt_builder,
cx,
)
@@ -3629,7 +3536,6 @@ mod tests {
range.clone(),
true,
None,
None,
prompt_builder,
cx,
)
@@ -3697,7 +3603,6 @@ mod tests {
range.clone(),
true,
None,
None,
prompt_builder,
cx,
)
@@ -3764,7 +3669,6 @@ mod tests {
range.clone(),
true,
None,
None,
prompt_builder,
cx,
)
@@ -3820,7 +3724,6 @@ mod tests {
range.clone(),
false,
None,
None,
prompt_builder,
cx,
)

View File

@@ -1,95 +1,77 @@
use std::sync::Arc;
use std::rc::Rc;
use editor::{Editor, EditorElement, EditorStyle};
use fs::Fs;
use gpui::{AppContext, FocusableView, Model, TextStyle, View, WeakModel, WeakView};
use gpui::{AppContext, FocusableView, Model, TextStyle, View, WeakView};
use language_model::{LanguageModelRegistry, LanguageModelRequestTool};
use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu};
use settings::{update_settings_file, Settings};
use settings::Settings;
use theme::ThemeSettings;
use ui::{
prelude::*, ButtonLike, CheckboxWithLabel, ElevationIndex, KeyBinding, PopoverMenuHandle,
Tooltip,
prelude::*, ButtonLike, CheckboxWithLabel, ElevationIndex, IconButtonShape, KeyBinding,
PopoverMenu, PopoverMenuHandle, Tooltip,
};
use workspace::Workspace;
use crate::assistant_settings::AssistantSettings;
use crate::context::{Context, ContextId, ContextKind};
use crate::context_picker::ContextPicker;
use crate::context_store::ContextStore;
use crate::context_strip::ContextStrip;
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>,
context: Vec<Context>,
next_context_id: ContextId,
context_picker: View<ContextPicker>,
pub(crate) context_picker_handle: PopoverMenuHandle<ContextPicker>,
language_model_selector: View<LanguageModelSelector>,
language_model_selector_menu_handle: PopoverMenuHandle<LanguageModelSelector>,
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
});
let weak_self = cx.view().downgrade();
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,
context: Vec::new(),
next_context_id: ContextId(0),
context_picker: cx.new_view(|cx| ContextPicker::new(workspace.clone(), weak_self, cx)),
context_picker_handle: PopoverMenuHandle::default(),
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()),
);
|model, _cx| {
println!("Selected {:?}", model.name());
},
cx,
)
}),
language_model_selector_menu_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);
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(),
});
}
fn chat(&mut self, _: &Chat, cx: &mut ViewContext<Self>) {
@@ -118,7 +100,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,8 +126,8 @@ 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(),
@@ -160,8 +142,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 +170,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 +184,60 @@ 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;
let context_picker = self.context_picker.clone();
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(
PopoverMenu::new("context-picker")
.menu(move |_cx| Some(context_picker.clone()))
.trigger(
IconButton::new("add-context", IconName::Plus)
.shape(IconButtonShape::Square)
.icon_size(IconSize::Small),
)
.attach(gpui::AnchorCorner::TopLeft)
.anchor(gpui::AnchorCorner::BottomLeft)
.offset(gpui::Point {
x: px(0.0),
y: px(-16.0),
})
.with_handle(self.context_picker_handle.clone()),
)
.children(self.context.iter().map(|context| {
ContextPill::new(context.clone()).on_remove({
let context = context.clone();
Rc::new(cx.listener(move |this, _event, cx| {
this.context.retain(|other| other.id != context.id);
cx.notify();
}))
})
}))
.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,17 +252,17 @@ 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(),
@@ -246,10 +272,10 @@ impl Render for MessageEditor {
ToggleState::Unselected | ToggleState::Indeterminate => false,
};
}),
))
)))
.child(
h_flex()
.gap_1()
.gap_2()
.child(self.render_language_model_selector(cx))
.child(
ButtonLike::new("chat")

View File

@@ -1,11 +1,5 @@
use crate::assistant_settings::AssistantSettings;
use crate::context::attach_context_to_message;
use crate::context_picker::ContextPicker;
use crate::context_store::ContextStore;
use crate::context_strip::ContextStrip;
use crate::prompts::PromptBuilder;
use crate::thread_store::ThreadStore;
use crate::ToggleContextPicker;
use anyhow::{Context as _, Result};
use client::telemetry::Telemetry;
use collections::{HashMap, VecDeque};
@@ -17,7 +11,7 @@ use fs::Fs;
use futures::{channel::mpsc, SinkExt, StreamExt};
use gpui::{
AppContext, Context, EventEmitter, FocusHandle, FocusableView, Global, Model, ModelContext,
Subscription, Task, TextStyle, UpdateGlobal, View, WeakModel, WeakView,
Subscription, Task, TextStyle, UpdateGlobal, View, WeakView,
};
use language::Buffer;
use language_model::{
@@ -31,7 +25,7 @@ use telemetry_events::{AssistantEvent, AssistantKind, AssistantPhase};
use terminal::Terminal;
use terminal_view::TerminalView;
use theme::ThemeSettings;
use ui::{prelude::*, text_for_action, IconButtonShape, PopoverMenuHandle, Tooltip};
use ui::{prelude::*, text_for_action, IconButtonShape, Tooltip};
use util::ResultExt;
use workspace::{notifications::NotificationId, Toast, Workspace};
@@ -88,8 +82,7 @@ impl TerminalInlineAssistant {
pub fn assist(
&mut self,
terminal_view: &View<TerminalView>,
workspace: WeakView<Workspace>,
thread_store: Option<WeakModel<ThreadStore>>,
workspace: Option<WeakView<Workspace>>,
cx: &mut WindowContext,
) {
let terminal = terminal_view.read(cx).terminal().clone();
@@ -97,7 +90,6 @@ impl TerminalInlineAssistant {
let prompt_buffer = cx.new_model(|cx| {
MultiBuffer::singleton(cx.new_model(|cx| Buffer::local(String::new(), cx)), cx)
});
let context_store = cx.new_model(|_cx| ContextStore::new());
let codegen = cx.new_model(|_| Codegen::new(terminal, self.telemetry.clone()));
let prompt_editor = cx.new_view(|cx| {
@@ -107,9 +99,6 @@ impl TerminalInlineAssistant {
prompt_buffer.clone(),
codegen,
self.fs.clone(),
context_store.clone(),
workspace.clone(),
thread_store.clone(),
cx,
)
});
@@ -127,7 +116,6 @@ impl TerminalInlineAssistant {
terminal_view,
prompt_editor,
workspace.clone(),
context_store,
cx,
);
@@ -258,21 +246,12 @@ impl TerminalInlineAssistant {
&latest_output,
)?;
let mut request_message = LanguageModelRequestMessage {
role: Role::User,
content: vec![],
cache: false,
};
let context = assist
.context_store
.update(cx, |this, _cx| this.context().clone());
attach_context_to_message(&mut request_message, context);
request_message.content.push(prompt.into());
Ok(LanguageModelRequest {
messages: vec![request_message],
messages: vec![LanguageModelRequestMessage {
role: Role::User,
content: vec![prompt.into()],
cache: false,
}],
tools: Vec::new(),
stop: Vec::new(),
temperature: None,
@@ -382,8 +361,7 @@ struct TerminalInlineAssist {
terminal: WeakView<TerminalView>,
prompt_editor: Option<View<PromptEditor>>,
codegen: Model<Codegen>,
workspace: WeakView<Workspace>,
context_store: Model<ContextStore>,
workspace: Option<WeakView<Workspace>>,
_subscriptions: Vec<Subscription>,
}
@@ -392,8 +370,7 @@ impl TerminalInlineAssist {
assist_id: TerminalInlineAssistId,
terminal: &View<TerminalView>,
prompt_editor: View<PromptEditor>,
workspace: WeakView<Workspace>,
context_store: Model<ContextStore>,
workspace: Option<WeakView<Workspace>>,
cx: &mut WindowContext,
) -> Self {
let codegen = prompt_editor.read(cx).codegen.clone();
@@ -402,7 +379,6 @@ impl TerminalInlineAssist {
prompt_editor: Some(prompt_editor.clone()),
codegen: codegen.clone(),
workspace: workspace.clone(),
context_store,
_subscriptions: vec![
cx.subscribe(&prompt_editor, |prompt_editor, event, cx| {
TerminalInlineAssistant::update_global(cx, |this, cx| {
@@ -420,7 +396,11 @@ impl TerminalInlineAssist {
if let CodegenStatus::Error(error) = &codegen.read(cx).status {
if assist.prompt_editor.is_none() {
if let Some(workspace) = assist.workspace.upgrade() {
if let Some(workspace) = assist
.workspace
.as_ref()
.and_then(|workspace| workspace.upgrade())
{
let error =
format!("Terminal inline assistant error: {}", error);
workspace.update(cx, |workspace, cx| {
@@ -461,8 +441,6 @@ struct PromptEditor {
id: TerminalInlineAssistId,
height_in_lines: u8,
editor: View<Editor>,
context_strip: View<ContextStrip>,
context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
language_model_selector: View<LanguageModelSelector>,
edited_since_done: bool,
prompt_history: VecDeque<String>,
@@ -478,7 +456,11 @@ impl EventEmitter<PromptEditorEvent> for PromptEditor {}
impl Render for PromptEditor {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let status = &self.codegen.read(cx).status;
let mut buttons = Vec::new();
let mut buttons = vec![Button::new("add-context", "Add Context")
.style(ButtonStyle::Filled)
.icon(IconName::Plus)
.icon_position(IconPosition::Start)
.into_any_element()];
buttons.extend(match status {
CodegenStatus::Idle => vec![
@@ -576,71 +558,64 @@ impl Render for PromptEditor {
}
});
v_flex()
h_flex()
.bg(cx.theme().colors().editor_background)
.border_y_1()
.border_color(cx.theme().status().info_border)
.py_2()
.size_full()
.h_full()
.w_full()
.on_action(cx.listener(Self::confirm))
.on_action(cx.listener(Self::secondary_confirm))
.on_action(cx.listener(Self::cancel))
.on_action(cx.listener(Self::move_up))
.on_action(cx.listener(Self::move_down))
.child(
h_flex()
.key_context("PromptEditor")
.bg(cx.theme().colors().editor_background)
.on_action(cx.listener(Self::toggle_context_picker))
.on_action(cx.listener(Self::confirm))
.on_action(cx.listener(Self::secondary_confirm))
.on_action(cx.listener(Self::cancel))
.on_action(cx.listener(Self::move_up))
.on_action(cx.listener(Self::move_down))
.child(
h_flex()
.w_12()
.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,
)
}),
))
.children(
if let CodegenStatus::Error(error) = &self.codegen.read(cx).status {
let error_message = SharedString::from(error.to_string());
Some(
div()
.id("error")
.tooltip(move |cx| {
Tooltip::text(error_message.clone(), cx)
})
.child(
Icon::new(IconName::XCircle)
.size(IconSize::Small)
.color(Color::Error),
),
)
} else {
None
},
),
)
.child(div().flex_1().child(self.render_prompt_editor(cx)))
.child(h_flex().gap_1().pr_4().children(buttons)),
.w_12()
.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,
)
}),
))
.children(
if let CodegenStatus::Error(error) = &self.codegen.read(cx).status {
let error_message = SharedString::from(error.to_string());
Some(
div()
.id("error")
.tooltip(move |cx| Tooltip::text(error_message.clone(), cx))
.child(
Icon::new(IconName::XCircle)
.size(IconSize::Small)
.color(Color::Error),
),
)
} else {
None
},
),
)
.child(h_flex().child(self.context_strip.clone()))
.child(div().flex_1().child(self.render_prompt_editor(cx)))
.child(h_flex().gap_1().pr_4().children(buttons))
}
}
@@ -660,9 +635,6 @@ impl PromptEditor {
prompt_buffer: Model<MultiBuffer>,
codegen: Model<Codegen>,
fs: Arc<dyn Fs>,
context_store: Model<ContextStore>,
workspace: WeakView<Workspace>,
thread_store: Option<WeakModel<ThreadStore>>,
cx: &mut ViewContext<Self>,
) -> Self {
let prompt_editor = cx.new_view(|cx| {
@@ -679,23 +651,11 @@ impl PromptEditor {
editor.set_placeholder_text(Self::placeholder_text(cx), cx);
editor
});
let context_picker_menu_handle = PopoverMenuHandle::default();
let mut this = Self {
id,
height_in_lines: 1,
editor: prompt_editor.clone(),
context_strip: cx.new_view(|cx| {
ContextStrip::new(
context_store,
workspace.clone(),
thread_store.clone(),
prompt_editor.focus_handle(cx),
context_picker_menu_handle.clone(),
cx,
)
}),
context_picker_menu_handle,
editor: prompt_editor,
language_model_selector: cx.new_view(|cx| {
let fs = fs.clone();
LanguageModelSelector::new(
@@ -804,10 +764,6 @@ impl PromptEditor {
}
}
fn toggle_context_picker(&mut self, _: &ToggleContextPicker, cx: &mut ViewContext<Self>) {
self.context_picker_menu_handle.toggle(cx);
}
fn cancel(&mut self, _: &editor::actions::Cancel, cx: &mut ViewContext<Self>) {
match &self.codegen.read(cx).status {
CodegenStatus::Idle | CodegenStatus::Done | CodegenStatus::Error(_) => {

View File

@@ -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,38 @@ 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();
let mut fetch_context = String::new();
for context in context.iter() {
match context.kind {
ContextKind::File => {
file_context.push_str(&context.text);
file_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');
}
}
}
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 !fetch_context.is_empty() {
context_text.push_str("The following fetched results are available\n");
context_text.push_str(&fetch_context);
}
request_message
.content
.push(MessageContent::Text(context_text))
}
if !message.text.is_empty() {

View File

@@ -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;
@@ -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();

View File

@@ -29,17 +29,14 @@ impl RenderOnce for ContextPill {
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
h_flex()
.gap_1()
.pl_1p5()
.pr_0p5()
.pb(px(1.))
.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)
IconButton::new("remove", IconName::Close)
.shape(IconButtonShape::Square)
.icon_size(IconSize::XSmall)
.on_click({

View File

@@ -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

View File

@@ -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,7 +68,7 @@ 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))
@@ -386,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))
@@ -402,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
});

View File

@@ -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,
@@ -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)

View File

@@ -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(
@@ -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),

View File

@@ -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,

View File

@@ -283,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

View File

@@ -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);
}
}

View File

@@ -55,10 +55,6 @@ impl InlineCompletionProvider for CopilotCompletionProvider {
"copilot"
}
fn display_name() -> &'static str {
"Copilot"
}
fn is_enabled(
&self,
buffer: &Model<Buffer>,
@@ -328,15 +324,10 @@ mod tests {
cx.update_editor(|editor, cx| {
// We want to show both: the inline completion and the completion menu
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.context_menu_next(&Default::default(), cx);
editor
.confirm_completion(&Default::default(), cx)
.unwrap()
@@ -347,14 +338,13 @@ mod tests {
assert_eq!(editor.display_text(cx), "one.completion_a\ntwo\nthree\n");
});
// Reset editor and only return copilot suggestions
// Reset editor and test that accepting completions works
cx.set_state(indoc! {"
oneˇ
two
three
"});
cx.simulate_keystroke(".");
drop(handle_completion_request(
&mut cx,
indoc! {"
@@ -362,7 +352,7 @@ mod tests {
two
three
"},
vec![],
vec!["completion_a", "completion_b"],
));
handle_copilot_completion_request(
&copilot_lsp,
@@ -375,32 +365,19 @@ mod tests {
);
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
cx.update_editor(|editor, cx| {
assert!(!editor.context_menu_visible());
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.
cx.simulate_keystroke("c");
drop(handle_completion_request(
&mut cx,
indoc! {"
one.c|<>
two
three
"},
vec!["completion_a", "completion_b"],
));
executor.run_until_parked();
cx.update_editor(|editor, cx| {
// Since we have an LSP completion too, the inline completion is
// shown in the menu now
assert!(editor.context_menu_visible());
assert!(editor.context_menu_contains_inline_completion());
assert!(!editor.context_menu_visible());
assert!(editor.has_active_inline_completion());
assert_eq!(editor.display_text(cx), "one.c\ntwo\nthree\n");
assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n");
assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n");
});
@@ -416,14 +393,6 @@ mod tests {
);
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
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.c\ntwo\nthree\n");
assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n");
// Canceling should first hide the menu and make Copilot suggestion visible.
editor.cancel(&Default::default(), cx);
assert!(!editor.context_menu_visible());
assert!(editor.has_active_inline_completion());
assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
@@ -928,8 +897,8 @@ 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_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
assert_eq!(editor.text(cx), "one\ntwo.\nthree\n");
});
}

View File

@@ -1050,7 +1050,6 @@ fn editor_blocks(
.ok()?
}
Block::FoldedBuffer { .. } => FILE_HEADER.into(),
Block::ExcerptBoundary {
starts_new_buffer, ..
} => {

View File

@@ -1,23 +1,23 @@
use std::cell::RefCell;
use std::{cell::Cell, cmp::Reverse, ops::Range, rc::Rc};
use std::{cell::Cell, cmp::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::RwLock;
use project::{CodeAction, Completion, TaskSourceKind};
use task::ResolvedTask;
use ui::{
h_flex, ActiveTheme as _, Color, FluentBuilder as _, InteractiveElement as _, IntoElement,
Label, LabelCommon as _, LabelSize, ListItem, ParentElement as _, Popover,
StatefulInteractiveElement as _, Styled, Toggleable as _,
StatefulInteractiveElement as _, Styled, StyledExt as _, Toggleable as _,
};
use util::ResultExt as _;
use workspace::Workspace;
@@ -28,7 +28,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 enum CodeContextMenu {
Completions(CompletionsMenu),
@@ -107,24 +106,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,
cursor_position: DisplayPoint,
style: &EditorStyle,
max_height_in_lines: u32,
max_height: Pixels,
workspace: Option<WeakView<Workspace>>,
cx: &mut ViewContext<Editor>,
) -> AnyElement {
) -> (ContextMenuOrigin, AnyElement) {
match self {
CodeContextMenu::Completions(menu) => {
menu.render(style, max_height_in_lines, 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(menu) => menu.render(style, max_height_in_lines, cx),
}
}
}
@@ -140,9 +137,9 @@ 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,
@@ -150,12 +147,6 @@ pub struct CompletionsMenu {
show_completion_documentation: bool,
}
#[derive(Clone, Debug)]
pub(crate) enum CompletionEntry {
Match(StringMatch),
InlineCompletionHint(InlineCompletionMenuHint),
}
impl CompletionsMenu {
pub fn new(
id: CompletionId,
@@ -169,7 +160,12 @@ impl CompletionsMenu {
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 {
@@ -178,9 +174,9 @@ impl CompletionsMenu {
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,
@@ -215,18 +211,16 @@ impl CompletionsMenu {
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 {
@@ -234,9 +228,9 @@ 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,
@@ -265,7 +259,7 @@ impl CompletionsMenu {
if self.selected_item > 0 {
self.selected_item -= 1;
} else {
self.selected_item = self.entries.len() - 1;
self.selected_item = self.matches.len() - 1;
}
self.scroll_handle
.scroll_to_item(self.selected_item, ScrollStrategy::Top);
@@ -278,7 +272,7 @@ impl CompletionsMenu {
provider: Option<&dyn CompletionProvider>,
cx: &mut ViewContext<Editor>,
) {
if self.selected_item + 1 < self.entries.len() {
if self.selected_item + 1 < self.matches.len() {
self.selected_item += 1;
} else {
self.selected_item = 0;
@@ -294,33 +288,13 @@ impl CompletionsMenu {
provider: Option<&dyn CompletionProvider>,
cx: &mut ViewContext<Editor>,
) {
self.selected_item = self.entries.len() - 1;
self.selected_item = self.matches.len() - 1;
self.scroll_handle
.scroll_to_item(self.selected_item, ScrollStrategy::Top);
self.resolve_selected_completion(provider, cx);
cx.notify();
}
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
}
}
.into();
self.selected_item = 0;
}
pub fn resolve_selected_completion(
&mut self,
provider: Option<&dyn CompletionProvider>,
@@ -333,112 +307,86 @@ 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,
);
let completion_index = self.matches[self.selected_item].candidate_id;
let resolve_task = provider.resolve_completions(
self.buffer.clone(),
vec![completion_index],
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();
}
})
.detach();
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 max_height = max_height_in_lines as f32 * cx.line_height();
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);
let completions = self.completions.clone();
let matches = self.matches.clone();
let selected_item = self.selected_item;
let style = style.clone();
let multiline_docs = match &self.entries[selected_item] {
CompletionEntry::Match(mat) if show_completion_documentation => {
match &completions[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,
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,
}
CompletionEntry::InlineCompletionHint(hint) => Some(match &hint.text {
InlineCompletionText::Edit { text, highlights } => div()
.my_1()
.rounded_md()
.bg(cx.theme().colors().editor_background)
.child(
gpui::StyledText::new(text.clone())
.with_highlights(&style.text, highlights.clone()),
),
InlineCompletionText::Move(text) => div().child(text.clone()),
}),
_ => None,
} else {
None
};
let aside_contents = if let Some(multiline_docs) = multiline_docs {
Some(multiline_docs)
} else if show_completion_documentation && self.aside_was_displayed.get() {
} else if self.aside_was_displayed.get() {
Some(div().child("Fetching documentation..."))
} else {
None
@@ -458,130 +406,95 @@ impl CompletionsMenu {
.occlude()
});
drop(completions);
let completions = self.completions.clone();
let matches = self.entries.clone();
let list = uniform_list(
cx.view().clone(),
"completions",
matches.len(),
move |_editor, range, cx| {
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)
.on_click(cx.listener(move |editor, _event, cx| {
cx.stop_propagation();
editor.accept_inline_completion(
&AcceptInlineCompletion {},
cx,
);
}))
.child(
StyledText::new(SharedString::new_static(provider_name))
.with_highlights(&style.text, None),
),
(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),
)
})
.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);
@@ -634,7 +547,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
@@ -686,14 +599,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;
}
}
@@ -702,13 +618,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 {
@@ -793,7 +709,7 @@ pub enum CodeActionsItem {
CodeAction {
excerpt_id: ExcerptId,
action: CodeAction,
provider: Rc<dyn CodeActionProvider>,
provider: Arc<dyn CodeActionProvider>,
},
}
@@ -869,23 +785,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(),
@@ -897,14 +806,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 {
@@ -914,21 +836,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 {
@@ -938,23 +856,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
@@ -968,8 +881,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)
}
}

View File

@@ -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>>,
@@ -745,11 +712,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 +723,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;
@@ -1118,6 +1081,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 +2231,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,

File diff suppressed because it is too large Load Diff

View File

@@ -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))
}
}

View File

@@ -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.
///
@@ -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;
@@ -322,6 +393,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,
@@ -990,13 +1068,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 +1093,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 +1213,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;

View File

@@ -73,7 +73,7 @@ use fuzzy::StringMatchCandidate;
use code_context_menus::{
AvailableCodeAction, CodeActionContents, CodeActionsItem, CodeActionsMenu, CodeContextMenu,
CompletionEntry, CompletionsMenu, ContextMenuOrigin,
CompletionsMenu, ContextMenuOrigin,
};
use git::blame::GitBlame;
use gpui::{
@@ -127,6 +127,7 @@ pub use multi_buffer::{
use multi_buffer::{
ExpandExcerptDirection, MultiBufferDiffHunk, MultiBufferPoint, MultiBufferRow, ToOffsetUtf16,
};
use parking_lot::RwLock;
use project::{
lsp_store::{FormatTarget, FormatTrigger, OpenLspBufferHandle},
project_settings::{GitGutterSetting, ProjectSettings},
@@ -457,21 +458,6 @@ pub fn make_suggestion_styles(cx: &WindowContext) -> InlineCompletionStyles {
type CompletionId = usize;
#[derive(Debug, Clone)]
struct InlineCompletionMenuHint {
provider_name: &'static str,
text: InlineCompletionText,
}
#[derive(Clone, Debug)]
enum InlineCompletionText {
Move(SharedString),
Edit {
text: SharedString,
highlights: Vec<(Range<usize>, HighlightStyle)>,
},
}
enum InlineCompletion {
Edit(Vec<(Range<Anchor>, String)>),
Move(Anchor),
@@ -620,7 +606,7 @@ pub struct Editor {
scrollbar_marker_state: ScrollbarMarkerState,
active_indent_guides_state: ActiveIndentGuidesState,
nav_history: Option<ItemNavHistory>,
context_menu: RefCell<Option<CodeContextMenu>>,
context_menu: RwLock<Option<CodeContextMenu>>,
mouse_context_menu: Option<MouseContextMenu>,
hunk_controls_menu_handle: PopoverMenuHandle<ui::ContextMenu>,
completion_tasks: Vec<(CompletionId, Task<Option<()>>)>,
@@ -628,7 +614,7 @@ pub struct Editor {
auto_signature_help: Option<bool>,
find_all_references_task_sources: Vec<Anchor>,
next_completion_id: CompletionId,
available_code_actions: Option<(Location, Rc<[AvailableCodeAction]>)>,
available_code_actions: Option<(Location, Arc<[AvailableCodeAction]>)>,
code_actions_task: Option<Task<Result<()>>>,
document_highlights_task: Option<Task<()>>,
linked_editing_range_task: Option<Task<Option<()>>>,
@@ -649,7 +635,7 @@ pub struct Editor {
gutter_hovered: bool,
hovered_link_state: Option<HoveredLinkState>,
inline_completion_provider: Option<RegisteredInlineCompletionProvider>,
code_action_providers: Vec<Rc<dyn CodeActionProvider>>,
code_action_providers: Vec<Arc<dyn CodeActionProvider>>,
active_inline_completion: Option<InlineCompletionState>,
// enable_inline_completions is a switch that Vim can use to disable
// inline completions based on its mode.
@@ -692,7 +678,6 @@ pub struct Editor {
next_scroll_position: NextScrollCursorCenterTopBottom,
addons: HashMap<TypeId, Box<dyn Addon>>,
registered_buffers: HashMap<BufferId, OpenLspBufferHandle>,
toggle_fold_multiple_buffers: Task<()>,
_scroll_cursor_center_top_bottom_task: Task<()>,
}
@@ -1206,7 +1191,7 @@ impl Editor {
let mut code_action_providers = Vec::new();
if let Some(project) = project.clone() {
get_unstaged_changes_for_buffers(&project, buffer.read(cx).all_buffers(), cx);
code_action_providers.push(Rc::new(project) as Rc<_>);
code_action_providers.push(Arc::new(project) as Arc<_>);
}
let mut this = Self {
@@ -1252,7 +1237,7 @@ impl Editor {
scrollbar_marker_state: ScrollbarMarkerState::default(),
active_indent_guides_state: ActiveIndentGuidesState::default(),
nav_history: None,
context_menu: RefCell::new(None),
context_menu: RwLock::new(None),
mouse_context_menu: None,
hunk_controls_menu_handle: PopoverMenuHandle::default(),
completion_tasks: Default::default(),
@@ -1340,7 +1325,6 @@ impl Editor {
addons: HashMap::default(),
registered_buffers: HashMap::default(),
_scroll_cursor_center_top_bottom_task: Task::ready(()),
toggle_fold_multiple_buffers: Task::ready(()),
text_style_refinement: None,
};
this.tasks_update_task = Some(this.refresh_runnables(cx));
@@ -1397,16 +1381,18 @@ impl Editor {
if self.pending_rename.is_some() {
key_context.add("renaming");
}
match self.context_menu.borrow().as_ref() {
Some(CodeContextMenu::Completions(_)) => {
key_context.add("menu");
key_context.add("showing_completions")
if self.context_menu_visible() {
match self.context_menu.read().as_ref() {
Some(CodeContextMenu::Completions(_)) => {
key_context.add("menu");
key_context.add("showing_completions")
}
Some(CodeContextMenu::CodeActions(_)) => {
key_context.add("menu");
key_context.add("showing_code_actions")
}
None => {}
}
Some(CodeContextMenu::CodeActions(_)) => {
key_context.add("menu");
key_context.add("showing_code_actions")
}
None => {}
}
// Disable vim contexts when a sub-editor (e.g. rename/inline assistant) is focused.
@@ -1898,9 +1884,10 @@ impl Editor {
if local {
let new_cursor_position = self.selections.newest_anchor().head();
let mut context_menu = self.context_menu.borrow_mut();
let mut context_menu = self.context_menu.write();
let completion_menu = match context_menu.as_ref() {
Some(CodeContextMenu::Completions(menu)) => Some(menu),
_ => {
*context_menu = None;
None
@@ -1924,7 +1911,7 @@ impl Editor {
.await;
this.update(&mut cx, |this, cx| {
let mut context_menu = this.context_menu.borrow_mut();
let mut context_menu = this.context_menu.write();
let Some(CodeContextMenu::Completions(menu)) = context_menu.as_ref()
else {
return;
@@ -2473,9 +2460,6 @@ impl Editor {
}
if self.hide_context_menu(cx).is_some() {
if self.has_active_inline_completion() {
self.update_visible_inline_completion(cx);
}
return true;
}
@@ -2856,6 +2840,7 @@ impl Editor {
);
}
let had_active_inline_completion = this.has_active_inline_completion();
this.change_selections_inner(Some(Autoscroll::fit()), false, cx, |s| {
s.select(new_selections)
});
@@ -2876,7 +2861,8 @@ impl Editor {
this.show_signature_help(&ShowSignatureHelp, cx);
}
this.trigger_completion_on_input(&text, true, cx);
let trigger_in_words = !had_active_inline_completion;
this.trigger_completion_on_input(&text, trigger_in_words, cx);
linked_editing_ranges::refresh_linked_ranges(this, cx);
this.refresh_inline_completion(true, false, cx);
});
@@ -3663,7 +3649,7 @@ impl Editor {
return;
};
if !self.snippet_stack.is_empty() && self.context_menu.borrow().as_ref().is_some() {
if !self.snippet_stack.is_empty() && self.context_menu.read().as_ref().is_some() {
return;
}
@@ -3682,7 +3668,7 @@ impl Editor {
let query = Self::completion_query(&self.buffer.read(cx).read(cx), position);
let aside_was_displayed = match self.context_menu.borrow().deref() {
let aside_was_displayed = match self.context_menu.read().deref() {
Some(CodeContextMenu::Completions(menu)) => menu.aside_was_displayed.get(),
_ => false,
};
@@ -3722,43 +3708,43 @@ impl Editor {
completions.into(),
aside_was_displayed,
);
menu.filter(query.as_deref(), cx.background_executor().clone())
.await;
menu.visible().then_some(menu)
if menu.matches.is_empty() {
None
} else {
Some(menu)
}
} else {
None
};
editor.update(&mut cx, |editor, cx| {
match editor.context_menu.borrow().as_ref() {
let mut context_menu = editor.context_menu.write();
match context_menu.as_ref() {
None => {}
Some(CodeContextMenu::Completions(prev_menu)) => {
if prev_menu.id > id {
return;
}
}
_ => return,
}
if editor.focus_handle.is_focused(cx) && menu.is_some() {
let mut menu = menu.unwrap();
menu.resolve_selected_completion(editor.completion_provider.as_deref(), cx);
if let Some(hint) = editor.inline_completion_menu_hint(cx) {
editor.hide_active_inline_completion(cx);
menu.show_inline_completion_hint(hint);
}
*editor.context_menu.borrow_mut() =
Some(CodeContextMenu::Completions(menu));
*context_menu = Some(CodeContextMenu::Completions(menu));
drop(context_menu);
cx.notify();
} else if editor.completion_tasks.len() <= 1 {
// If there are no more completion tasks and the last menu was
// empty, we should hide it. If it was already hidden, we should
// also show the copilot completion when available.
drop(context_menu);
editor.hide_context_menu(cx);
}
})?;
@@ -3795,6 +3781,7 @@ impl Editor {
) -> Option<Task<std::result::Result<(), anyhow::Error>>> {
use language::ToOffset as _;
self.discard_inline_completion(true, cx);
let completions_menu =
if let CodeContextMenu::Completions(menu) = self.hide_context_menu(cx)? {
menu
@@ -3803,23 +3790,10 @@ impl Editor {
};
let mat = completions_menu
.entries
.matches
.get(item_ix.unwrap_or(completions_menu.selected_item))?;
let mat = match mat {
CompletionEntry::InlineCompletionHint { .. } => {
self.accept_inline_completion(&AcceptInlineCompletion, cx);
cx.stop_propagation();
return Some(Task::ready(Ok(())));
}
CompletionEntry::Match(mat) => {
self.discard_inline_completion(true, cx);
mat
}
};
let buffer_handle = completions_menu.buffer;
let completions = completions_menu.completions.borrow_mut();
let completions = completions_menu.completions.read();
let completion = completions.get(mat.candidate_id)?;
cx.stop_propagation();
@@ -3985,7 +3959,7 @@ impl Editor {
}
pub fn toggle_code_actions(&mut self, action: &ToggleCodeActions, cx: &mut ViewContext<Self>) {
let mut context_menu = self.context_menu.borrow_mut();
let mut context_menu = self.context_menu.write();
if let Some(CodeContextMenu::CodeActions(code_actions)) = context_menu.as_ref() {
if code_actions.deployed_from_indicator == action.deployed_from_indicator {
// Toggle if we're selecting the same one
@@ -4065,7 +4039,7 @@ impl Editor {
};
let resolved_tasks =
tasks.zip(task_context).map(|(tasks, task_context)| {
Rc::new(ResolvedTasks {
Arc::new(ResolvedTasks {
templates: tasks.resolve(&task_context).collect(),
position: snapshot.buffer_snapshot.anchor_before(Point::new(
multibuffer_point.row,
@@ -4080,7 +4054,7 @@ impl Editor {
.as_ref()
.map_or(true, |actions| actions.is_empty());
if let Ok(task) = editor.update(&mut cx, |editor, cx| {
*editor.context_menu.borrow_mut() =
*editor.context_menu.write() =
Some(CodeContextMenu::CodeActions(CodeActionsMenu {
buffer,
actions: CodeActionContents {
@@ -4265,7 +4239,7 @@ impl Editor {
pub fn push_code_action_provider(
&mut self,
provider: Rc<dyn CodeActionProvider>,
provider: Arc<dyn CodeActionProvider>,
cx: &mut ViewContext<Self>,
) {
self.code_action_providers.push(provider);
@@ -4700,17 +4674,6 @@ impl Editor {
Some(active_inline_completion.completion)
}
fn hide_active_inline_completion(&mut self, cx: &mut ViewContext<Self>) {
if let Some(active_inline_completion) = self.active_inline_completion.as_ref() {
self.splice_inlays(
active_inline_completion.inlay_ids.clone(),
Default::default(),
cx,
);
self.clear_highlights::<InlineCompletionHighlight>(cx);
}
}
fn update_visible_inline_completion(&mut self, cx: &mut ViewContext<Self>) -> Option<()> {
let selection = self.selections.newest_anchor();
let cursor = selection.head();
@@ -4782,34 +4745,32 @@ impl Editor {
invalidation_row_range = edit_start_row..cursor_row;
completion = InlineCompletion::Move(first_edit_start);
} else {
if !self.has_active_completions_menu() {
if edits
.iter()
.all(|(range, _)| range.to_offset(&multibuffer).is_empty())
{
let mut inlays = Vec::new();
for (range, new_text) in &edits {
let inlay = Inlay::inline_completion(
post_inc(&mut self.next_inlay_id),
range.start,
new_text.as_str(),
);
inlay_ids.push(inlay.id);
inlays.push(inlay);
}
self.splice_inlays(vec![], inlays, cx);
} else {
let background_color = cx.theme().status().deleted_background;
self.highlight_text::<InlineCompletionHighlight>(
edits.iter().map(|(range, _)| range.clone()).collect(),
HighlightStyle {
background_color: Some(background_color),
..Default::default()
},
cx,
if edits
.iter()
.all(|(range, _)| range.to_offset(&multibuffer).is_empty())
{
let mut inlays = Vec::new();
for (range, new_text) in &edits {
let inlay = Inlay::inline_completion(
post_inc(&mut self.next_inlay_id),
range.start,
new_text.as_str(),
);
inlay_ids.push(inlay.id);
inlays.push(inlay);
}
self.splice_inlays(vec![], inlays, cx);
} else {
let background_color = cx.theme().status().deleted_background;
self.highlight_text::<InlineCompletionHighlight>(
edits.iter().map(|(range, _)| range.clone()).collect(),
HighlightStyle {
background_color: Some(background_color),
..Default::default()
},
cx,
);
}
invalidation_row_range = edit_start_row..edit_end_row;
@@ -4828,54 +4789,11 @@ impl Editor {
completion,
invalidation_range,
});
if self.has_active_completions_menu() {
if let Some(hint) = self.inline_completion_menu_hint(cx) {
match self.context_menu.borrow_mut().as_mut() {
Some(CodeContextMenu::Completions(menu)) => {
menu.show_inline_completion_hint(hint);
}
_ => {}
}
}
}
cx.notify();
Some(())
}
fn inline_completion_menu_hint(
&mut self,
cx: &mut ViewContext<Self>,
) -> Option<InlineCompletionMenuHint> {
if self.has_active_inline_completion() {
let provider_name = self.inline_completion_provider()?.display_name();
let editor_snapshot = self.snapshot(cx);
let text = match &self.active_inline_completion.as_ref()?.completion {
InlineCompletion::Edit(edits) => {
inline_completion_edit_text(&editor_snapshot, edits, cx)
}
InlineCompletion::Move(target) => {
let target_point =
target.to_point(&editor_snapshot.display_snapshot.buffer_snapshot);
let target_line = target_point.row + 1;
InlineCompletionText::Move(
format!("Jump to edit in line {}", target_line).into(),
)
}
};
Some(InlineCompletionMenuHint {
provider_name,
text,
})
} else {
None
}
}
fn inline_completion_provider(&self) -> Option<Arc<dyn InlineCompletionProviderHandle>> {
Some(self.inline_completion_provider.as_ref()?.provider.clone())
}
@@ -5082,58 +5000,35 @@ impl Editor {
}))
}
#[cfg(feature = "test-support")]
pub fn context_menu_visible(&self) -> bool {
self.context_menu
.borrow()
.read()
.as_ref()
.map_or(false, |menu| menu.visible())
}
#[cfg(feature = "test-support")]
pub fn context_menu_contains_inline_completion(&self) -> bool {
self.context_menu
.borrow()
.as_ref()
.map_or(false, |menu| match menu {
CodeContextMenu::Completions(menu) => menu.entries.first().map_or(false, |entry| {
matches!(entry, CompletionEntry::InlineCompletionHint(_))
}),
CodeContextMenu::CodeActions(_) => false,
})
}
fn context_menu_origin(&self, cursor_position: DisplayPoint) -> Option<ContextMenuOrigin> {
self.context_menu
.borrow()
.as_ref()
.map(|menu| menu.origin(cursor_position))
}
fn render_context_menu(
&self,
cursor_position: DisplayPoint,
style: &EditorStyle,
max_height_in_lines: u32,
max_height: Pixels,
cx: &mut ViewContext<Editor>,
) -> Option<AnyElement> {
self.context_menu.borrow().as_ref().and_then(|menu| {
if menu.visible() {
Some(menu.render(
style,
max_height_in_lines,
self.workspace.as_ref().map(|(w, _)| w.clone()),
cx,
))
} else {
None
}
) -> Option<(ContextMenuOrigin, AnyElement)> {
self.context_menu.read().as_ref().map(|menu| {
menu.render(
cursor_position,
style,
max_height,
self.workspace.as_ref().map(|(w, _)| w.clone()),
cx,
)
})
}
fn hide_context_menu(&mut self, cx: &mut ViewContext<Self>) -> Option<CodeContextMenu> {
cx.notify();
self.completion_tasks.clear();
self.context_menu.borrow_mut().take()
self.context_menu.write().take()
}
fn show_snippet_choices(
@@ -5150,7 +5045,7 @@ impl Editor {
let id = post_inc(&mut self.next_completion_id);
if let Some(buffer) = buffer {
*self.context_menu.borrow_mut() = Some(CodeContextMenu::Completions(
*self.context_menu.write() = Some(CodeContextMenu::Completions(
CompletionsMenu::new_snippet_choices(id, true, choices, selection, buffer),
));
}
@@ -7204,7 +7099,7 @@ impl Editor {
if self
.context_menu
.borrow_mut()
.write()
.as_mut()
.map(|menu| menu.select_first(self.completion_provider.as_deref(), cx))
.unwrap_or(false)
@@ -7313,7 +7208,7 @@ impl Editor {
if self
.context_menu
.borrow_mut()
.write()
.as_mut()
.map(|menu| menu.select_last(self.completion_provider.as_deref(), cx))
.unwrap_or(false)
@@ -7366,25 +7261,25 @@ impl Editor {
}
pub fn context_menu_first(&mut self, _: &ContextMenuFirst, cx: &mut ViewContext<Self>) {
if let Some(context_menu) = self.context_menu.borrow_mut().as_mut() {
if let Some(context_menu) = self.context_menu.write().as_mut() {
context_menu.select_first(self.completion_provider.as_deref(), cx);
}
}
pub fn context_menu_prev(&mut self, _: &ContextMenuPrev, cx: &mut ViewContext<Self>) {
if let Some(context_menu) = self.context_menu.borrow_mut().as_mut() {
if let Some(context_menu) = self.context_menu.write().as_mut() {
context_menu.select_prev(self.completion_provider.as_deref(), cx);
}
}
pub fn context_menu_next(&mut self, _: &ContextMenuNext, cx: &mut ViewContext<Self>) {
if let Some(context_menu) = self.context_menu.borrow_mut().as_mut() {
if let Some(context_menu) = self.context_menu.write().as_mut() {
context_menu.select_next(self.completion_provider.as_deref(), cx);
}
}
pub fn context_menu_last(&mut self, _: &ContextMenuLast, cx: &mut ViewContext<Self>) {
if let Some(context_menu) = self.context_menu.borrow_mut().as_mut() {
if let Some(context_menu) = self.context_menu.write().as_mut() {
context_menu.select_last(self.completion_provider.as_deref(), cx);
}
}
@@ -9607,7 +9502,7 @@ impl Editor {
let location_tasks = definitions
.into_iter()
.map(|definition| match definition {
HoverLink::Text(link) => Task::ready(Ok(Some(link.target))),
HoverLink::Text(link) => Task::Ready(Some(Ok(Some(link.target)))),
HoverLink::InlayHint(lsp_location, server_id) => {
editor.compute_target_location(lsp_location, server_id, cx)
}
@@ -9649,7 +9544,7 @@ impl Editor {
cx: &mut ViewContext<Self>,
) -> Task<anyhow::Result<Option<Location>>> {
let Some(project) = self.project.clone() else {
return Task::ready(Ok(None));
return Task::Ready(Some(Ok(None)));
};
cx.spawn(move |editor, mut cx| async move {
@@ -10416,42 +10311,22 @@ impl Editor {
}
pub fn toggle_fold(&mut self, _: &actions::ToggleFold, cx: &mut ViewContext<Self>) {
if self.is_singleton(cx) {
let selection = self.selections.newest::<Point>(cx);
let selection = self.selections.newest::<Point>(cx);
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
let range = if selection.is_empty() {
let point = selection.head().to_display_point(&display_map);
let start = DisplayPoint::new(point.row(), 0).to_point(&display_map);
let end = DisplayPoint::new(point.row(), display_map.line_len(point.row()))
.to_point(&display_map);
start..end
} else {
selection.range()
};
if display_map.folds_in_range(range).next().is_some() {
self.unfold_lines(&Default::default(), cx)
} else {
self.fold(&Default::default(), cx)
}
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
let range = if selection.is_empty() {
let point = selection.head().to_display_point(&display_map);
let start = DisplayPoint::new(point.row(), 0).to_point(&display_map);
let end = DisplayPoint::new(point.row(), display_map.line_len(point.row()))
.to_point(&display_map);
start..end
} else {
let multi_buffer_snapshot = self.buffer.read(cx).snapshot(cx);
let mut toggled_buffers = HashSet::default();
for (_, buffer_snapshot, _) in multi_buffer_snapshot.excerpts_in_ranges(
self.selections
.disjoint_anchors()
.into_iter()
.map(|selection| selection.range()),
) {
let buffer_id = buffer_snapshot.remote_id();
if toggled_buffers.insert(buffer_id) {
if self.buffer_folded(buffer_id, cx) {
self.unfold_buffer(buffer_id, cx);
} else {
self.fold_buffer(buffer_id, cx);
}
}
}
selection.range()
};
if display_map.folds_in_range(range).next().is_some() {
self.unfold_lines(&Default::default(), cx)
} else {
self.fold(&Default::default(), cx)
}
}
@@ -10480,61 +10355,44 @@ impl Editor {
}
pub fn fold(&mut self, _: &actions::Fold, cx: &mut ViewContext<Self>) {
if self.is_singleton(cx) {
let mut to_fold = Vec::new();
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
let selections = self.selections.all_adjusted(cx);
let mut to_fold = Vec::new();
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
let selections = self.selections.all_adjusted(cx);
for selection in selections {
let range = selection.range().sorted();
let buffer_start_row = range.start.row;
for selection in selections {
let range = selection.range().sorted();
let buffer_start_row = range.start.row;
if range.start.row != range.end.row {
let mut found = false;
let mut row = range.start.row;
while row <= range.end.row {
if let Some(crease) = display_map.crease_for_buffer_row(MultiBufferRow(row))
{
found = true;
row = crease.range().end.row + 1;
to_fold.push(crease);
} else {
row += 1
}
}
if found {
continue;
if range.start.row != range.end.row {
let mut found = false;
let mut row = range.start.row;
while row <= range.end.row {
if let Some(crease) = display_map.crease_for_buffer_row(MultiBufferRow(row)) {
found = true;
row = crease.range().end.row + 1;
to_fold.push(crease);
} else {
row += 1
}
}
for row in (0..=range.start.row).rev() {
if let Some(crease) = display_map.crease_for_buffer_row(MultiBufferRow(row)) {
if crease.range().end.row >= buffer_start_row {
to_fold.push(crease);
if row <= range.start.row {
break;
}
}
}
if found {
continue;
}
}
self.fold_creases(to_fold, true, cx);
} else {
let multi_buffer_snapshot = self.buffer.read(cx).snapshot(cx);
let mut folded_buffers = HashSet::default();
for (_, buffer_snapshot, _) in multi_buffer_snapshot.excerpts_in_ranges(
self.selections
.disjoint_anchors()
.into_iter()
.map(|selection| selection.range()),
) {
let buffer_id = buffer_snapshot.remote_id();
if folded_buffers.insert(buffer_id) {
self.fold_buffer(buffer_id, cx);
for row in (0..=range.start.row).rev() {
if let Some(crease) = display_map.crease_for_buffer_row(MultiBufferRow(row)) {
if crease.range().end.row >= buffer_start_row {
to_fold.push(crease);
if row <= range.start.row {
break;
}
}
}
}
}
self.fold_creases(to_fold, true, cx);
}
fn fold_at_level(&mut self, fold_at: &FoldAtLevel, cx: &mut ViewContext<Self>) {
@@ -10574,30 +10432,22 @@ impl Editor {
}
pub fn fold_all(&mut self, _: &actions::FoldAll, cx: &mut ViewContext<Self>) {
if self.buffer.read(cx).is_singleton() {
let mut fold_ranges = Vec::new();
let snapshot = self.buffer.read(cx).snapshot(cx);
for row in 0..snapshot.max_row().0 {
if let Some(foldable_range) =
self.snapshot(cx).crease_for_buffer_row(MultiBufferRow(row))
{
fold_ranges.push(foldable_range);
}
}
self.fold_creases(fold_ranges, true, cx);
} else {
self.toggle_fold_multiple_buffers = cx.spawn(|editor, mut cx| async move {
editor
.update(&mut cx, |editor, cx| {
for buffer_id in editor.buffer.read(cx).excerpt_buffer_ids() {
editor.fold_buffer(buffer_id, cx);
}
})
.ok();
});
if !self.buffer.read(cx).is_singleton() {
return;
}
let mut fold_ranges = Vec::new();
let snapshot = self.buffer.read(cx).snapshot(cx);
for row in 0..snapshot.max_row().0 {
if let Some(foldable_range) =
self.snapshot(cx).crease_for_buffer_row(MultiBufferRow(row))
{
fold_ranges.push(foldable_range);
}
}
self.fold_creases(fold_ranges, true, cx);
}
pub fn fold_function_bodies(
@@ -10669,38 +10519,22 @@ impl Editor {
}
pub fn unfold_lines(&mut self, _: &UnfoldLines, cx: &mut ViewContext<Self>) {
if self.is_singleton(cx) {
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
let buffer = &display_map.buffer_snapshot;
let selections = self.selections.all::<Point>(cx);
let ranges = selections
.iter()
.map(|s| {
let range = s.display_range(&display_map).sorted();
let mut start = range.start.to_point(&display_map);
let mut end = range.end.to_point(&display_map);
start.column = 0;
end.column = buffer.line_len(MultiBufferRow(end.row));
start..end
})
.collect::<Vec<_>>();
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
let buffer = &display_map.buffer_snapshot;
let selections = self.selections.all::<Point>(cx);
let ranges = selections
.iter()
.map(|s| {
let range = s.display_range(&display_map).sorted();
let mut start = range.start.to_point(&display_map);
let mut end = range.end.to_point(&display_map);
start.column = 0;
end.column = buffer.line_len(MultiBufferRow(end.row));
start..end
})
.collect::<Vec<_>>();
self.unfold_ranges(&ranges, true, true, cx);
} else {
let multi_buffer_snapshot = self.buffer.read(cx).snapshot(cx);
let mut unfolded_buffers = HashSet::default();
for (_, buffer_snapshot, _) in multi_buffer_snapshot.excerpts_in_ranges(
self.selections
.disjoint_anchors()
.into_iter()
.map(|selection| selection.range()),
) {
let buffer_id = buffer_snapshot.remote_id();
if unfolded_buffers.insert(buffer_id) {
self.unfold_buffer(buffer_id, cx);
}
}
}
self.unfold_ranges(&ranges, true, true, cx);
}
pub fn unfold_recursive(&mut self, _: &UnfoldRecursive, cx: &mut ViewContext<Self>) {
@@ -10740,20 +10574,8 @@ impl Editor {
}
pub fn unfold_all(&mut self, _: &actions::UnfoldAll, cx: &mut ViewContext<Self>) {
if self.buffer.read(cx).is_singleton() {
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
self.unfold_ranges(&[0..display_map.buffer_snapshot.len()], true, true, cx);
} else {
self.toggle_fold_multiple_buffers = cx.spawn(|editor, mut cx| async move {
editor
.update(&mut cx, |editor, cx| {
for buffer_id in editor.buffer.read(cx).excerpt_buffer_ids() {
editor.unfold_buffer(buffer_id, cx);
}
})
.ok();
});
}
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
self.unfold_ranges(&[0..display_map.buffer_snapshot.len()], true, true, cx);
}
pub fn fold_selected_ranges(&mut self, _: &FoldSelectedRanges, cx: &mut ViewContext<Self>) {
@@ -10840,45 +10662,6 @@ impl Editor {
});
}
pub fn fold_buffer(&mut self, buffer_id: BufferId, cx: &mut ViewContext<Self>) {
if self.buffer().read(cx).is_singleton() || self.buffer_folded(buffer_id, cx) {
return;
}
let Some(buffer) = self.buffer().read(cx).buffer(buffer_id) else {
return;
};
let folded_excerpts = self.buffer().read(cx).excerpts_for_buffer(&buffer, cx);
self.display_map
.update(cx, |display_map, cx| display_map.fold_buffer(buffer_id, cx));
cx.emit(EditorEvent::BufferFoldToggled {
ids: folded_excerpts.iter().map(|&(id, _)| id).collect(),
folded: true,
});
cx.notify();
}
pub fn unfold_buffer(&mut self, buffer_id: BufferId, cx: &mut ViewContext<Self>) {
if self.buffer().read(cx).is_singleton() || !self.buffer_folded(buffer_id, cx) {
return;
}
let Some(buffer) = self.buffer().read(cx).buffer(buffer_id) else {
return;
};
let unfolded_excerpts = self.buffer().read(cx).excerpts_for_buffer(&buffer, cx);
self.display_map.update(cx, |display_map, cx| {
display_map.unfold_buffer(buffer_id, cx);
});
cx.emit(EditorEvent::BufferFoldToggled {
ids: unfolded_excerpts.iter().map(|&(id, _)| id).collect(),
folded: false,
});
cx.notify();
}
pub fn buffer_folded(&self, buffer: BufferId, cx: &AppContext) -> bool {
self.display_map.read(cx).buffer_folded(buffer)
}
/// Removes any folds with the given ranges.
pub fn remove_folds_with_type<T: ToOffset + Clone>(
&mut self,
@@ -12896,7 +12679,7 @@ impl Editor {
}
pub fn has_active_completions_menu(&self) -> bool {
self.context_menu.borrow().as_ref().map_or(false, |menu| {
self.context_menu.read().as_ref().map_or(false, |menu| {
menu.visible() && matches!(menu, CodeContextMenu::Completions(_))
})
}
@@ -13392,7 +13175,7 @@ pub trait CompletionProvider {
&self,
buffer: Model<Buffer>,
completion_indices: Vec<usize>,
completions: Rc<RefCell<Box<[Completion]>>>,
completions: Arc<RwLock<Box<[Completion]>>>,
cx: &mut ViewContext<Editor>,
) -> Task<Result<bool>>;
@@ -13510,7 +13293,7 @@ fn snippet_completions(
snippet
.prefix
.iter()
.map(move |prefix| StringMatchCandidate::new(ix, &prefix))
.map(move |prefix| StringMatchCandidate::new(ix, prefix.clone()))
})
.collect::<Vec<StringMatchCandidate>>();
@@ -13622,7 +13405,7 @@ impl CompletionProvider for Model<Project> {
&self,
buffer: Model<Buffer>,
completion_indices: Vec<usize>,
completions: Rc<RefCell<Box<[Completion]>>>,
completions: Arc<RwLock<Box<[Completion]>>>,
cx: &mut ViewContext<Editor>,
) -> Task<Result<bool>> {
self.update(cx, |project, cx| {
@@ -14037,10 +13820,6 @@ pub enum EditorEvent {
ExcerptsRemoved {
ids: Vec<ExcerptId>,
},
BufferFoldToggled {
ids: Vec<ExcerptId>,
folded: bool,
},
ExcerptsEdited {
ids: Vec<ExcerptId>,
},
@@ -14592,64 +14371,6 @@ pub fn diagnostic_block_renderer(
})
}
fn inline_completion_edit_text(
editor_snapshot: &EditorSnapshot,
edits: &Vec<(Range<Anchor>, String)>,
cx: &WindowContext,
) -> InlineCompletionText {
let edit_start = edits
.first()
.unwrap()
.0
.start
.to_display_point(editor_snapshot);
let mut text = String::new();
let mut offset = DisplayPoint::new(edit_start.row(), 0).to_offset(editor_snapshot, Bias::Left);
let mut highlights = Vec::new();
for (old_range, new_text) in edits {
let old_offset_range = old_range.to_offset(&editor_snapshot.buffer_snapshot);
text.extend(
editor_snapshot
.buffer_snapshot
.chunks(offset..old_offset_range.start, false)
.map(|chunk| chunk.text),
);
offset = old_offset_range.end;
let start = text.len();
text.push_str(new_text);
let end = text.len();
highlights.push((
start..end,
HighlightStyle {
background_color: Some(cx.theme().status().created_background),
..Default::default()
},
));
}
let edit_end = edits
.last()
.unwrap()
.0
.end
.to_display_point(editor_snapshot);
let end_of_line = DisplayPoint::new(edit_end.row(), editor_snapshot.line_len(edit_end.row()))
.to_offset(editor_snapshot, Bias::Right);
text.extend(
editor_snapshot
.buffer_snapshot
.chunks(offset..end_of_line, false)
.map(|chunk| chunk.text),
);
InlineCompletionText::Edit {
text: text.into(),
highlights,
}
}
pub fn highlight_diagnostic_message(
diagnostic: &Diagnostic,
mut max_message_rows: Option<u8>,

View File

@@ -11,7 +11,6 @@ pub struct EditorSettings {
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,
@@ -197,10 +196,7 @@ pub struct EditorSettingsContent {
///
/// 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

View File

@@ -4064,7 +4064,7 @@ async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
let snapshot = editor.snapshot(cx);
let snapshot = &snapshot.buffer_snapshot;
let placement = BlockPlacement::Replace(
snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
snapshot.anchor_after(Point::new(1, 0))..snapshot.anchor_after(Point::new(3, 0)),
);
editor.insert_blocks(
[BlockProperties {
@@ -8342,9 +8342,9 @@ async fn test_completion(cx: &mut gpui::TestAppContext) {
additional edit
"});
cx.simulate_keystroke(" ");
assert!(cx.editor(|e, _| e.context_menu.borrow_mut().is_none()));
assert!(cx.editor(|e, _| e.context_menu.read().is_none()));
cx.simulate_keystroke("s");
assert!(cx.editor(|e, _| e.context_menu.borrow_mut().is_none()));
assert!(cx.editor(|e, _| e.context_menu.read().is_none()));
cx.assert_editor_state(indoc! {"
one.second_completion
@@ -8406,12 +8406,12 @@ async fn test_completion(cx: &mut gpui::TestAppContext) {
});
cx.set_state("editorˇ");
cx.simulate_keystroke(".");
assert!(cx.editor(|e, _| e.context_menu.borrow_mut().is_none()));
assert!(cx.editor(|e, _| e.context_menu.read().is_none()));
cx.simulate_keystroke("c");
cx.simulate_keystroke("l");
cx.simulate_keystroke("o");
cx.assert_editor_state("editor.cloˇ");
assert!(cx.editor(|e, _| e.context_menu.borrow_mut().is_none()));
assert!(cx.editor(|e, _| e.context_menu.read().is_none()));
cx.update_editor(|editor, cx| {
editor.show_completions(&ShowCompletions { trigger: None }, cx);
});
@@ -8468,9 +8468,11 @@ async fn test_completion_page_up_down_keys(cx: &mut gpui::TestAppContext) {
cx.executor().run_until_parked();
cx.update_editor(|editor, _| {
if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
{
assert_eq!(completion_menu_entries(&menu.entries), &["first", "last"]);
if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() {
assert_eq!(
menu.matches.iter().map(|m| &m.string).collect::<Vec<_>>(),
&["first", "last"]
);
} else {
panic!("expected completion menu to be open");
}
@@ -8478,8 +8480,7 @@ async fn test_completion_page_up_down_keys(cx: &mut gpui::TestAppContext) {
cx.update_editor(|editor, cx| {
editor.move_page_down(&MovePageDown::default(), cx);
if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
{
if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() {
assert!(
menu.selected_item == 1,
"expected PageDown to select the last item from the context menu"
@@ -8491,8 +8492,7 @@ async fn test_completion_page_up_down_keys(cx: &mut gpui::TestAppContext) {
cx.update_editor(|editor, cx| {
editor.move_page_up(&MovePageUp::default(), cx);
if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
{
if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() {
assert!(
menu.selected_item == 0,
"expected PageUp to select the first item from the context menu"
@@ -8560,10 +8560,9 @@ async fn test_completion_sort(cx: &mut gpui::TestAppContext) {
cx.executor().run_until_parked();
cx.update_editor(|editor, _| {
if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
{
if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() {
assert_eq!(
completion_menu_entries(&menu.entries),
menu.matches.iter().map(|m| &m.string).collect::<Vec<_>>(),
&["r", "ret", "Range", "return"]
);
} else {
@@ -10649,9 +10648,7 @@ async fn test_completions_with_additional_edits(cx: &mut gpui::TestAppContext) {
}
#[gpui::test]
async fn test_completions_resolve_updates_labels_if_filter_text_matches(
cx: &mut gpui::TestAppContext,
) {
async fn test_completions_resolve_updates_labels(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorLspTestContext::new_rust(
@@ -10670,34 +10667,20 @@ async fn test_completions_resolve_updates_labels_if_filter_text_matches(
cx.set_state(indoc! {"fn main() { let a = 2ˇ; }"});
cx.simulate_keystroke(".");
let item1 = lsp::CompletionItem {
label: "id".to_string(),
filter_text: Some("id".to_string()),
let completion_item = lsp::CompletionItem {
label: "unresolved".to_string(),
detail: None,
documentation: None,
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
new_text: ".id".to_string(),
})),
..lsp::CompletionItem::default()
};
let item2 = lsp::CompletionItem {
label: "other".to_string(),
filter_text: Some("other".to_string()),
detail: None,
documentation: None,
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
new_text: ".other".to_string(),
new_text: ".unresolved".to_string(),
})),
..lsp::CompletionItem::default()
};
cx.handle_request::<lsp::request::Completion, _, _>(move |_, _, _| {
let item1 = item1.clone();
let item2 = item2.clone();
async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
let item = completion_item.clone();
async move { Ok(Some(lsp::CompletionResponse::Array(vec![item]))) }
})
.next()
.await;
@@ -10705,20 +10688,15 @@ async fn test_completions_resolve_updates_labels_if_filter_text_matches(
cx.condition(|editor, _| editor.context_menu_visible())
.await;
cx.update_editor(|editor, _| {
let context_menu = editor.context_menu.borrow_mut();
let context_menu = editor.context_menu.read();
let context_menu = context_menu
.as_ref()
.expect("Should have the context menu deployed");
match context_menu {
CodeContextMenu::Completions(completions_menu) => {
let completions = completions_menu.completions.borrow_mut();
assert_eq!(
completions
.iter()
.map(|completion| &completion.label.text)
.collect::<Vec<_>>(),
vec!["id", "other"]
)
let completions = completions_menu.completions.read();
assert_eq!(completions.len(), 1, "Should have one completion");
assert_eq!(completions.get(0).unwrap().label.text, "unresolved");
}
CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
}
@@ -10726,33 +10704,12 @@ async fn test_completions_resolve_updates_labels_if_filter_text_matches(
cx.handle_request::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| async move {
Ok(lsp::CompletionItem {
label: "method id()".to_string(),
filter_text: Some("id".to_string()),
label: "resolved".to_string(),
detail: Some("Now resolved!".to_string()),
documentation: Some(lsp::Documentation::String("Docs".to_string())),
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
new_text: ".id".to_string(),
})),
..lsp::CompletionItem::default()
})
})
.next()
.await;
cx.run_until_parked();
cx.update_editor(|editor, cx| {
editor.context_menu_next(&Default::default(), cx);
});
cx.handle_request::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| async move {
Ok(lsp::CompletionItem {
label: "invalid changed label".to_string(),
detail: Some("Now resolved!".to_string()),
documentation: Some(lsp::Documentation::String("Docs".to_string())),
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
new_text: ".id".to_string(),
new_text: ".resolved".to_string(),
})),
..lsp::CompletionItem::default()
})
@@ -10762,20 +10719,18 @@ async fn test_completions_resolve_updates_labels_if_filter_text_matches(
cx.run_until_parked();
cx.update_editor(|editor, _| {
let context_menu = editor.context_menu.borrow_mut();
let context_menu = editor.context_menu.read();
let context_menu = context_menu
.as_ref()
.expect("Should have the context menu deployed");
match context_menu {
CodeContextMenu::Completions(completions_menu) => {
let completions = completions_menu.completions.borrow_mut();
let completions = completions_menu.completions.read();
assert_eq!(completions.len(), 1, "Should have one completion");
assert_eq!(
completions
.iter()
.map(|completion| &completion.label.text)
.collect::<Vec<_>>(),
vec!["method id()", "other"],
"Should update first completion label, but not second as the filter text did not match."
completions.get(0).unwrap().label.text,
"resolved",
"Should update the completion label after resolving"
);
}
CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
@@ -10955,11 +10910,15 @@ async fn test_completions_default_resolve_data_handling(cx: &mut gpui::TestAppCo
.await;
cx.run_until_parked();
cx.update_editor(|editor, _| {
let menu = editor.context_menu.borrow_mut();
let menu = editor.context_menu.read();
match menu.as_ref().expect("should have the completions menu") {
CodeContextMenu::Completions(completions_menu) => {
assert_eq!(
completion_menu_entries(&completions_menu.entries),
completions_menu
.matches
.iter()
.map(|c| c.string.as_str())
.collect::<Vec<_>>(),
vec!["Some(2)", "vec![2]"]
);
}
@@ -11056,10 +11015,9 @@ async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui:
cx.simulate_keystroke("-");
cx.executor().run_until_parked();
cx.update_editor(|editor, _| {
if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
{
if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() {
assert_eq!(
completion_menu_entries(&menu.entries),
menu.matches.iter().map(|m| &m.string).collect::<Vec<_>>(),
&["bg-red", "bg-blue", "bg-yellow"]
);
} else {
@@ -11070,10 +11028,9 @@ async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui:
cx.simulate_keystroke("l");
cx.executor().run_until_parked();
cx.update_editor(|editor, _| {
if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
{
if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() {
assert_eq!(
completion_menu_entries(&menu.entries),
menu.matches.iter().map(|m| &m.string).collect::<Vec<_>>(),
&["bg-blue", "bg-yellow"]
);
} else {
@@ -11087,25 +11044,17 @@ async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui:
cx.simulate_keystroke("l");
cx.executor().run_until_parked();
cx.update_editor(|editor, _| {
if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
{
assert_eq!(completion_menu_entries(&menu.entries), &["bg-yellow"]);
if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() {
assert_eq!(
menu.matches.iter().map(|m| &m.string).collect::<Vec<_>>(),
&["bg-yellow"]
);
} else {
panic!("expected completion menu to be open");
}
});
}
fn completion_menu_entries(entries: &[CompletionEntry]) -> Vec<&str> {
entries
.iter()
.flat_map(|e| match e {
CompletionEntry::Match(mat) => Some(mat.string.as_str()),
_ => None,
})
.collect()
}
#[gpui::test]
async fn test_document_format_with_prettier(cx: &mut gpui::TestAppContext) {
init_test(cx, |settings| {
@@ -13078,7 +13027,6 @@ fn assert_indent_guides(
let indent_guides = cx.update_editor(|editor, cx| {
let snapshot = editor.snapshot(cx).display_snapshot;
let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
editor,
MultiBufferRow(range.start)..MultiBufferRow(range.end),
true,
&snapshot,
@@ -13957,581 +13905,6 @@ async fn test_find_enclosing_node_with_task(cx: &mut gpui::TestAppContext) {
});
}
#[gpui::test]
async fn test_multi_buffer_folding(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
"/a",
json!({
"first.rs": sample_text_1,
"second.rs": sample_text_2,
"third.rs": sample_text_3,
}),
)
.await;
let project = Project::test(fs, ["/a".as_ref()], cx).await;
let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
let worktree = project.update(cx, |project, cx| {
let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
assert_eq!(worktrees.len(), 1);
worktrees.pop().unwrap()
});
let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
let buffer_1 = project
.update(cx, |project, cx| {
project.open_buffer((worktree_id, "first.rs"), cx)
})
.await
.unwrap();
let buffer_2 = project
.update(cx, |project, cx| {
project.open_buffer((worktree_id, "second.rs"), cx)
})
.await
.unwrap();
let buffer_3 = project
.update(cx, |project, cx| {
project.open_buffer((worktree_id, "third.rs"), cx)
})
.await
.unwrap();
let multi_buffer = cx.new_model(|cx| {
let mut multi_buffer = MultiBuffer::new(ReadWrite);
multi_buffer.push_excerpts(
buffer_1.clone(),
[
ExcerptRange {
context: Point::new(0, 0)..Point::new(3, 0),
primary: None,
},
ExcerptRange {
context: Point::new(5, 0)..Point::new(7, 0),
primary: None,
},
ExcerptRange {
context: Point::new(9, 0)..Point::new(10, 4),
primary: None,
},
],
cx,
);
multi_buffer.push_excerpts(
buffer_2.clone(),
[
ExcerptRange {
context: Point::new(0, 0)..Point::new(3, 0),
primary: None,
},
ExcerptRange {
context: Point::new(5, 0)..Point::new(7, 0),
primary: None,
},
ExcerptRange {
context: Point::new(9, 0)..Point::new(10, 4),
primary: None,
},
],
cx,
);
multi_buffer.push_excerpts(
buffer_3.clone(),
[
ExcerptRange {
context: Point::new(0, 0)..Point::new(3, 0),
primary: None,
},
ExcerptRange {
context: Point::new(5, 0)..Point::new(7, 0),
primary: None,
},
ExcerptRange {
context: Point::new(9, 0)..Point::new(10, 4),
primary: None,
},
],
cx,
);
multi_buffer
});
let multi_buffer_editor = cx.new_view(|cx| {
Editor::new(
EditorMode::Full,
multi_buffer,
Some(project.clone()),
true,
cx,
)
});
let full_text = "\n\n\naaaa\nbbbb\ncccc\n\n\n\nffff\ngggg\n\n\n\njjjj\n\n\n\n\nllll\nmmmm\nnnnn\n\n\n\nqqqq\nrrrr\n\n\n\nuuuu\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n\n1111\n2222\n\n\n\n5555\n";
assert_eq!(
multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
full_text,
);
multi_buffer_editor.update(cx, |editor, cx| {
editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
});
assert_eq!(
multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
"\n\n\n\n\nllll\nmmmm\nnnnn\n\n\n\nqqqq\nrrrr\n\n\n\nuuuu\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n\n1111\n2222\n\n\n\n5555\n",
"After folding the first buffer, its text should not be displayed"
);
multi_buffer_editor.update(cx, |editor, cx| {
editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
});
assert_eq!(
multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
"\n\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n\n1111\n2222\n\n\n\n5555\n",
"After folding the second buffer, its text should not be displayed"
);
multi_buffer_editor.update(cx, |editor, cx| {
editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
});
assert_eq!(
multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
"\n\n\n\n\n",
"After folding the third buffer, its text should not be displayed"
);
// Emulate selection inside the fold logic, that should work
multi_buffer_editor.update(cx, |editor, cx| {
editor.snapshot(cx).next_line_boundary(Point::new(0, 4));
});
multi_buffer_editor.update(cx, |editor, cx| {
editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
});
assert_eq!(
multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
"\n\n\n\n\nllll\nmmmm\nnnnn\n\n\n\nqqqq\nrrrr\n\n\n\nuuuu\n\n\n",
"After unfolding the second buffer, its text should be displayed"
);
multi_buffer_editor.update(cx, |editor, cx| {
editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
});
assert_eq!(
multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
"\n\n\naaaa\nbbbb\ncccc\n\n\n\nffff\ngggg\n\n\n\njjjj\n\n\n\n\nllll\nmmmm\nnnnn\n\n\n\nqqqq\nrrrr\n\n\n\nuuuu\n\n\n",
"After unfolding the first buffer, its and 2nd buffer's text should be displayed"
);
multi_buffer_editor.update(cx, |editor, cx| {
editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
});
assert_eq!(
multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
full_text,
"After unfolding the all buffers, all original text should be displayed"
);
}
#[gpui::test]
async fn test_multi_buffer_single_excerpts_folding(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let sample_text_1 = "1111\n2222\n3333".to_string();
let sample_text_2 = "4444\n5555\n6666".to_string();
let sample_text_3 = "7777\n8888\n9999".to_string();
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
"/a",
json!({
"first.rs": sample_text_1,
"second.rs": sample_text_2,
"third.rs": sample_text_3,
}),
)
.await;
let project = Project::test(fs, ["/a".as_ref()], cx).await;
let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
let worktree = project.update(cx, |project, cx| {
let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
assert_eq!(worktrees.len(), 1);
worktrees.pop().unwrap()
});
let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
let buffer_1 = project
.update(cx, |project, cx| {
project.open_buffer((worktree_id, "first.rs"), cx)
})
.await
.unwrap();
let buffer_2 = project
.update(cx, |project, cx| {
project.open_buffer((worktree_id, "second.rs"), cx)
})
.await
.unwrap();
let buffer_3 = project
.update(cx, |project, cx| {
project.open_buffer((worktree_id, "third.rs"), cx)
})
.await
.unwrap();
let multi_buffer = cx.new_model(|cx| {
let mut multi_buffer = MultiBuffer::new(ReadWrite);
multi_buffer.push_excerpts(
buffer_1.clone(),
[ExcerptRange {
context: Point::new(0, 0)..Point::new(3, 0),
primary: None,
}],
cx,
);
multi_buffer.push_excerpts(
buffer_2.clone(),
[ExcerptRange {
context: Point::new(0, 0)..Point::new(3, 0),
primary: None,
}],
cx,
);
multi_buffer.push_excerpts(
buffer_3.clone(),
[ExcerptRange {
context: Point::new(0, 0)..Point::new(3, 0),
primary: None,
}],
cx,
);
multi_buffer
});
let multi_buffer_editor = cx.new_view(|cx| {
Editor::new(
EditorMode::Full,
multi_buffer,
Some(project.clone()),
true,
cx,
)
});
let full_text = "\n\n\n1111\n2222\n3333\n\n\n\n\n4444\n5555\n6666\n\n\n\n\n7777\n8888\n9999\n";
assert_eq!(
multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
full_text,
);
multi_buffer_editor.update(cx, |editor, cx| {
editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
});
assert_eq!(
multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
"\n\n\n\n\n4444\n5555\n6666\n\n\n\n\n7777\n8888\n9999\n",
"After folding the first buffer, its text should not be displayed"
);
multi_buffer_editor.update(cx, |editor, cx| {
editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
});
assert_eq!(
multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
"\n\n\n\n\n\n\n7777\n8888\n9999\n",
"After folding the second buffer, its text should not be displayed"
);
multi_buffer_editor.update(cx, |editor, cx| {
editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
});
assert_eq!(
multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
"\n\n\n\n\n",
"After folding the third buffer, its text should not be displayed"
);
multi_buffer_editor.update(cx, |editor, cx| {
editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
});
assert_eq!(
multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
"\n\n\n\n\n4444\n5555\n6666\n\n\n",
"After unfolding the second buffer, its text should be displayed"
);
multi_buffer_editor.update(cx, |editor, cx| {
editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
});
assert_eq!(
multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
"\n\n\n1111\n2222\n3333\n\n\n\n\n4444\n5555\n6666\n\n\n",
"After unfolding the first buffer, its text should be displayed"
);
multi_buffer_editor.update(cx, |editor, cx| {
editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
});
assert_eq!(
multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
full_text,
"After unfolding all buffers, all original text should be displayed"
);
}
#[gpui::test]
async fn test_multi_buffer_with_single_excerpt_folding(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
"/a",
json!({
"main.rs": sample_text,
}),
)
.await;
let project = Project::test(fs, ["/a".as_ref()], cx).await;
let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
let worktree = project.update(cx, |project, cx| {
let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
assert_eq!(worktrees.len(), 1);
worktrees.pop().unwrap()
});
let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
let buffer_1 = project
.update(cx, |project, cx| {
project.open_buffer((worktree_id, "main.rs"), cx)
})
.await
.unwrap();
let multi_buffer = cx.new_model(|cx| {
let mut multi_buffer = MultiBuffer::new(ReadWrite);
multi_buffer.push_excerpts(
buffer_1.clone(),
[ExcerptRange {
context: Point::new(0, 0)
..Point::new(
sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
0,
),
primary: None,
}],
cx,
);
multi_buffer
});
let multi_buffer_editor = cx.new_view(|cx| {
Editor::new(
EditorMode::Full,
multi_buffer,
Some(project.clone()),
true,
cx,
)
});
let selection_range = Point::new(1, 0)..Point::new(2, 0);
multi_buffer_editor.update(cx, |editor, cx| {
enum TestHighlight {}
let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
editor.highlight_text::<TestHighlight>(
vec![highlight_range.clone()],
HighlightStyle::color(Hsla::green()),
cx,
);
editor.change_selections(None, cx, |s| s.select_ranges(Some(highlight_range)));
});
let full_text = format!("\n\n\n{sample_text}\n");
assert_eq!(
multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
full_text,
);
}
#[gpui::test]
fn test_inline_completion_text(cx: &mut TestAppContext) {
init_test(cx, |_| {});
// Test case 1: Simple insertion
{
let window = cx.add_window(|cx| {
let buffer = MultiBuffer::build_simple("Hello, world!", cx);
Editor::new(EditorMode::Full, buffer, None, true, cx)
});
let cx = &mut VisualTestContext::from_window(*window, cx);
window
.update(cx, |editor, cx| {
let snapshot = editor.snapshot(cx);
let edit_range = snapshot.buffer_snapshot.anchor_after(Point::new(0, 6))
..snapshot.buffer_snapshot.anchor_before(Point::new(0, 6));
let edits = vec![(edit_range, " beautiful".to_string())];
let InlineCompletionText::Edit { text, highlights } =
inline_completion_edit_text(&snapshot, &edits, cx)
else {
panic!("Failed to generate inline completion text");
};
assert_eq!(text, "Hello, beautiful world!");
assert_eq!(highlights.len(), 1);
assert_eq!(highlights[0].0, 6..16);
assert_eq!(
highlights[0].1.background_color,
Some(cx.theme().status().created_background)
);
})
.unwrap();
}
// Test case 2: Replacement
{
let window = cx.add_window(|cx| {
let buffer = MultiBuffer::build_simple("This is a test.", cx);
Editor::new(EditorMode::Full, buffer, None, true, cx)
});
let cx = &mut VisualTestContext::from_window(*window, cx);
window
.update(cx, |editor, cx| {
let snapshot = editor.snapshot(cx);
let edits = vec![(
snapshot.buffer_snapshot.anchor_after(Point::new(0, 0))
..snapshot.buffer_snapshot.anchor_before(Point::new(0, 4)),
"That".to_string(),
)];
let InlineCompletionText::Edit { text, highlights } =
inline_completion_edit_text(&snapshot, &edits, cx)
else {
panic!("Failed to generate inline completion text");
};
assert_eq!(text, "That is a test.");
assert_eq!(highlights.len(), 1);
assert_eq!(highlights[0].0, 0..4);
assert_eq!(
highlights[0].1.background_color,
Some(cx.theme().status().created_background)
);
})
.unwrap();
}
// Test case 3: Multiple edits
{
let window = cx.add_window(|cx| {
let buffer = MultiBuffer::build_simple("Hello, world!", cx);
Editor::new(EditorMode::Full, buffer, None, true, cx)
});
let cx = &mut VisualTestContext::from_window(*window, cx);
window
.update(cx, |editor, cx| {
let snapshot = editor.snapshot(cx);
let edits = vec![
(
snapshot.buffer_snapshot.anchor_after(Point::new(0, 0))
..snapshot.buffer_snapshot.anchor_before(Point::new(0, 5)),
"Greetings".into(),
),
(
snapshot.buffer_snapshot.anchor_after(Point::new(0, 12))
..snapshot.buffer_snapshot.anchor_before(Point::new(0, 12)),
" and universe".into(),
),
];
let InlineCompletionText::Edit { text, highlights } =
inline_completion_edit_text(&snapshot, &edits, cx)
else {
panic!("Failed to generate inline completion text");
};
assert_eq!(text, "Greetings, world and universe!");
assert_eq!(highlights.len(), 2);
assert_eq!(highlights[0].0, 0..9);
assert_eq!(highlights[1].0, 16..29);
assert_eq!(
highlights[0].1.background_color,
Some(cx.theme().status().created_background)
);
assert_eq!(
highlights[1].1.background_color,
Some(cx.theme().status().created_background)
);
})
.unwrap();
}
// Test case 4: Multiple lines with edits
{
let window = cx.add_window(|cx| {
let buffer =
MultiBuffer::build_simple("First line\nSecond line\nThird line\nFourth line", cx);
Editor::new(EditorMode::Full, buffer, None, true, cx)
});
let cx = &mut VisualTestContext::from_window(*window, cx);
window
.update(cx, |editor, cx| {
let snapshot = editor.snapshot(cx);
let edits = vec![
(
snapshot.buffer_snapshot.anchor_before(Point::new(1, 7))
..snapshot.buffer_snapshot.anchor_before(Point::new(1, 11)),
"modified".to_string(),
),
(
snapshot.buffer_snapshot.anchor_before(Point::new(2, 0))
..snapshot.buffer_snapshot.anchor_before(Point::new(2, 10)),
"New third line".to_string(),
),
(
snapshot.buffer_snapshot.anchor_before(Point::new(3, 6))
..snapshot.buffer_snapshot.anchor_before(Point::new(3, 6)),
" updated".to_string(),
),
];
let InlineCompletionText::Edit { text, highlights } =
inline_completion_edit_text(&snapshot, &edits, cx)
else {
panic!("Failed to generate inline completion text");
};
assert_eq!(text, "Second modified\nNew third line\nFourth updated line");
assert_eq!(highlights.len(), 3);
assert_eq!(highlights[0].0, 7..15); // "modified"
assert_eq!(highlights[1].0, 16..30); // "New third line"
assert_eq!(highlights[2].0, 37..45); // " updated"
for highlight in &highlights {
assert_eq!(
highlight.1.background_color,
Some(cx.theme().status().created_background)
);
}
})
.unwrap();
}
}
fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
point..point

File diff suppressed because it is too large Load Diff

View File

@@ -23,6 +23,7 @@ use std::{ops::Range, sync::Arc, time::Duration};
use theme::ThemeSettings;
use ui::{prelude::*, window_is_transparent, Scrollbar, ScrollbarState};
use util::TryFutureExt;
pub const HOVER_DELAY_MILLIS: u64 = 350;
pub const HOVER_REQUEST_DELAY_MILLIS: u64 = 200;
pub const MIN_POPOVER_CHARACTER_WIDTH: f32 = 20.;
@@ -130,12 +131,10 @@ pub fn hover_at_inlay(editor: &mut Editor, inlay_hover: InlayHover, cx: &mut Vie
hide_hover(editor, cx);
}
let hover_popover_delay = EditorSettings::get_global(cx).hover_popover_delay;
let task = cx.spawn(|this, mut cx| {
async move {
cx.background_executor()
.timer(Duration::from_millis(hover_popover_delay))
.timer(Duration::from_millis(HOVER_DELAY_MILLIS))
.await;
this.update(&mut cx, |this, _| {
this.hover_state.diagnostic_popover = None;
@@ -237,8 +236,6 @@ fn show_hover(
}
}
let hover_popover_delay = EditorSettings::get_global(cx).hover_popover_delay;
let task = cx.spawn(|this, mut cx| {
async move {
// If we need to delay, delay a set amount initially before making the lsp request
@@ -248,7 +245,7 @@ fn show_hover(
// Construct delay task to wait for later
let total_delay = Some(
cx.background_executor()
.timer(Duration::from_millis(hover_popover_delay)),
.timer(Duration::from_millis(HOVER_DELAY_MILLIS)),
);
cx.background_executor()
@@ -859,7 +856,6 @@ mod tests {
InlayId, PointForPosition,
};
use collections::BTreeSet;
use gpui::AppContext;
use indoc::indoc;
use language::{language_settings::InlayHintSettings, Diagnostic, DiagnosticSet};
use lsp::LanguageServerId;
@@ -869,10 +865,6 @@ mod tests {
use std::sync::atomic::AtomicUsize;
use text::Bias;
fn get_hover_popover_delay(cx: &gpui::TestAppContext) -> u64 {
cx.read(|cx: &AppContext| -> u64 { EditorSettings::get_global(cx).hover_popover_delay })
}
impl InfoPopover {
fn get_rendered_text(&self, cx: &gpui::AppContext) -> String {
let mut rendered_text = String::new();
@@ -897,6 +889,7 @@ mod tests {
cx: &mut gpui::TestAppContext,
) {
init_test(cx, |_| {});
const HOVER_DELAY_MILLIS: u64 = 350;
let mut cx = EditorLspTestContext::new_rust(
lsp::ServerCapabilities {
@@ -970,7 +963,7 @@ mod tests {
}))
});
cx.background_executor
.advance_clock(Duration::from_millis(get_hover_popover_delay(&cx) + 100));
.advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
requests.next().await;
cx.editor(|editor, cx| {
@@ -1049,7 +1042,7 @@ mod tests {
hover_at(editor, Some(anchor), cx)
});
cx.background_executor
.advance_clock(Duration::from_millis(get_hover_popover_delay(&cx) + 100));
.advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
request.next().await;
// verify that the information popover is no longer visible
@@ -1103,7 +1096,7 @@ mod tests {
}))
});
cx.background_executor
.advance_clock(Duration::from_millis(get_hover_popover_delay(&cx) + 100));
.advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
requests.next().await;
cx.editor(|editor, cx| {
@@ -1139,7 +1132,7 @@ mod tests {
hover_at(editor, Some(anchor), cx)
});
cx.background_executor
.advance_clock(Duration::from_millis(get_hover_popover_delay(&cx) + 100));
.advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
request.next().await;
cx.editor(|editor, _| {
assert!(!editor.hover_state.visible());
@@ -1401,7 +1394,7 @@ mod tests {
}))
});
cx.background_executor
.advance_clock(Duration::from_millis(get_hover_popover_delay(&cx) + 100));
.advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
cx.background_executor.run_until_parked();
cx.editor(|Editor { hover_state, .. }, _| {
@@ -1689,7 +1682,7 @@ mod tests {
);
});
cx.background_executor
.advance_clock(Duration::from_millis(get_hover_popover_delay(&cx) + 100));
.advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
cx.background_executor.run_until_parked();
cx.update_editor(|editor, cx| {
let hover_state = &editor.hover_state;
@@ -1743,7 +1736,7 @@ mod tests {
);
});
cx.background_executor
.advance_clock(Duration::from_millis(get_hover_popover_delay(&cx) + 100));
.advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
cx.background_executor.run_until_parked();
cx.update_editor(|editor, cx| {
let hover_state = &editor.hover_state;

View File

@@ -1,7 +1,8 @@
use collections::{HashMap, HashSet};
use git::diff::DiffHunkStatus;
use gpui::{
Action, AppContext, Corner, CursorStyle, Hsla, Model, MouseButton, Subscription, Task, View,
Action, AnchorCorner, AppContext, CursorStyle, Hsla, Model, MouseButton, Subscription, Task,
View,
};
use language::{Buffer, BufferId, Point};
use multi_buffer::{
@@ -742,7 +743,7 @@ impl Editor {
},
),
)
.anchor(Corner::TopRight)
.anchor(AnchorCorner::TopRight)
.with_handle(hunk_controls_menu_handle)
.menu(move |cx| {
let focus = focus.clone();

View File

@@ -56,7 +56,6 @@ impl Editor {
}
Some(indent_guides_in_range(
self,
visible_buffer_range,
self.should_show_indent_guides() == Some(true),
snapshot,
@@ -153,7 +152,6 @@ impl Editor {
}
pub fn indent_guides_in_range(
editor: &Editor,
visible_buffer_range: Range<MultiBufferRow>,
ignore_disabled_for_language: bool,
snapshot: &DisplaySnapshot,
@@ -171,19 +169,15 @@ pub fn indent_guides_in_range(
.indent_guides_in_range(start_anchor..end_anchor, ignore_disabled_for_language, cx)
.into_iter()
.filter(|indent_guide| {
if editor.buffer_folded(indent_guide.buffer_id, cx) {
return false;
}
let start =
MultiBufferRow(indent_guide.multibuffer_row_range.start.0.saturating_sub(1));
// Filter out indent guides that are inside a fold
// All indent guides that are starting "offscreen" have a start value of the first visible row minus one
// Therefore checking if a line is folded at first visible row minus one causes the other indent guides that are not related to the fold to disappear as well
let is_folded = snapshot.is_line_folded(start);
let line_indent = snapshot.line_indent_for_buffer_row(start);
let contained_in_fold =
line_indent.len(indent_guide.tab_size) <= indent_guide.indent_level();
!(is_folded && contained_in_fold)
})
.collect()

View File

@@ -317,10 +317,6 @@ impl InlineCompletionProvider for FakeInlineCompletionProvider {
"fake-completion-provider"
}
fn display_name() -> &'static str {
"Fake Completion Provider"
}
fn is_enabled(
&self,
_buffer: &gpui::Model<language::Buffer>,

View File

@@ -19,7 +19,6 @@ const SUGGESTIONS_BY_EXTENSION_ID: &[(&str, &[&str])] = &[
("clojure", &["bb", "clj", "cljc", "cljs", "edn"]),
("neocmake", &["CMakeLists.txt", "cmake"]),
("csharp", &["cs"]),
("cython", &["pyx", "pxd", "pxi"]),
("dart", &["dart"]),
("dockerfile", &["Dockerfile"]),
("elisp", &["el"]),

View File

@@ -113,7 +113,13 @@ impl PickerDelegate for ExtensionVersionSelectorDelegate {
.iter()
.enumerate()
.map(|(id, extension)| {
StringMatchCandidate::new(id, &format!("v{}", extension.manifest.version))
let text = format!("v{}", extension.manifest.version);
StringMatchCandidate {
id,
char_bag: text.as_str().into(),
string: text,
}
})
.collect::<Vec<_>>();

View File

@@ -328,7 +328,11 @@ impl ExtensionsPage {
let match_candidates = dev_extensions
.iter()
.enumerate()
.map(|(ix, manifest)| StringMatchCandidate::new(ix, &manifest.name))
.map(|(ix, manifest)| StringMatchCandidate {
id: ix,
string: manifest.name.clone(),
char_bag: manifest.name.as_str().into(),
})
.collect::<Vec<_>>();
let matches = match_strings(
@@ -449,17 +453,18 @@ impl ExtensionsPage {
.gap_2()
.justify_between()
.child(
Label::new(format!(
"{}: {}",
if extension.authors.len() > 1 {
"Authors"
} else {
"Author"
},
extension.authors.join(", ")
))
.size(LabelSize::Small)
.text_ellipsis(),
div().overflow_x_hidden().text_ellipsis().child(
Label::new(format!(
"{}: {}",
if extension.authors.len() > 1 {
"Authors"
} else {
"Author"
},
extension.authors.join(", ")
))
.size(LabelSize::Small),
),
)
.child(Label::new("<>").size(LabelSize::Small)),
)
@@ -468,10 +473,11 @@ impl ExtensionsPage {
.gap_2()
.justify_between()
.children(extension.description.as_ref().map(|description| {
Label::new(description.clone())
.size(LabelSize::Small)
.color(Color::Default)
.text_ellipsis()
div().overflow_x_hidden().text_ellipsis().child(
Label::new(description.clone())
.size(LabelSize::Small)
.color(Color::Default),
)
}))
.children(repository_url.map(|repository_url| {
IconButton::new(
@@ -548,17 +554,18 @@ impl ExtensionsPage {
.gap_2()
.justify_between()
.child(
Label::new(format!(
"{}: {}",
if extension.manifest.authors.len() > 1 {
"Authors"
} else {
"Author"
},
extension.manifest.authors.join(", ")
))
.size(LabelSize::Small)
.text_ellipsis(),
div().overflow_x_hidden().text_ellipsis().child(
Label::new(format!(
"{}: {}",
if extension.manifest.authors.len() > 1 {
"Authors"
} else {
"Author"
},
extension.manifest.authors.join(", ")
))
.size(LabelSize::Small),
),
)
.child(
Label::new(format!(
@@ -573,10 +580,11 @@ impl ExtensionsPage {
.gap_2()
.justify_between()
.children(extension.manifest.description.as_ref().map(|description| {
Label::new(description.clone())
.size(LabelSize::Small)
.color(Color::Default)
.text_ellipsis()
div().overflow_x_hidden().text_ellipsis().child(
Label::new(description.clone())
.size(LabelSize::Small)
.color(Color::Default),
)
}))
.child(
h_flex()

View File

@@ -1261,8 +1261,8 @@ impl PickerDelegate for FileFinderDelegate {
.child(
PopoverMenu::new("menu-popover")
.with_handle(self.popover_menu_handle.clone())
.attach(gpui::Corner::TopRight)
.anchor(gpui::Corner::BottomRight)
.attach(gpui::AnchorCorner::TopRight)
.anchor(gpui::AnchorCorner::BottomRight)
.trigger(
Button::new("actions-trigger", "Split Options")
.selected_label_color(Color::Accent)

View File

@@ -131,7 +131,7 @@ impl PickerDelegate for OpenPathDelegate {
.iter()
.enumerate()
.map(|(ix, path)| {
StringMatchCandidate::new(ix, &path.to_string_lossy())
StringMatchCandidate::new(ix, path.to_string_lossy().into())
})
.collect::<Vec<_>>();

View File

@@ -18,12 +18,22 @@ pub struct StringMatchCandidate {
pub char_bag: CharBag,
}
impl Match for StringMatch {
fn score(&self) -> f64 {
self.score
}
fn set_positions(&mut self, positions: Vec<usize>) {
self.positions = positions;
}
}
impl StringMatchCandidate {
pub fn new(id: usize, string: &str) -> Self {
pub fn new(id: usize, string: String) -> Self {
Self {
id,
string: string.into(),
char_bag: string.into(),
char_bag: CharBag::from(string.as_str()),
string,
}
}
}
@@ -46,39 +56,29 @@ pub struct StringMatch {
pub string: String,
}
impl Match for StringMatch {
fn score(&self) -> f64 {
self.score
}
fn set_positions(&mut self, positions: Vec<usize>) {
self.positions = positions;
}
}
impl StringMatch {
pub fn ranges(&self) -> impl '_ + Iterator<Item = Range<usize>> {
let mut positions = self.positions.iter().peekable();
iter::from_fn(move || {
if let Some(start) = positions.next().copied() {
let Some(char_len) = self.char_len_at_index(start) else {
if start >= self.string.len() {
log::error!(
"Invariant violation: Index {start} out of range or not on a utf-8 boundary in string {:?}",
"Invariant violation: Index {start} out of range in string {:?}",
self.string
);
return None;
};
let mut end = start + char_len;
}
let mut end = start + self.char_len_at_index(start);
while let Some(next_start) = positions.peek() {
if end == **next_start {
let Some(char_len) = self.char_len_at_index(end) else {
if end >= self.string.len() {
log::error!(
"Invariant violation: Index {end} out of range or not on a utf-8 boundary in string {:?}",
"Invariant violation: Index {end} out of range in string {:?}",
self.string
);
return None;
};
end += char_len;
}
end += self.char_len_at_index(end);
positions.next();
} else {
break;
@@ -91,12 +91,8 @@ impl StringMatch {
})
}
/// Gets the byte length of the utf-8 character at a byte offset. If the index is out of range
/// or not on a utf-8 boundary then None is returned.
fn char_len_at_index(&self, ix: usize) -> Option<usize> {
self.string
.get(ix..)
.and_then(|slice| slice.chars().next().map(|char| char.len_utf8()))
fn char_len_at_index(&self, ix: usize) -> usize {
self.string[ix..].chars().next().unwrap().len_utf8()
}
}

View File

@@ -46,8 +46,7 @@ pub trait GitRepository: Send + Sync {
fn blame(&self, path: &Path, content: Rope) -> Result<crate::blame::Blame>;
/// Returns the path to the repository, typically the `.git` folder.
fn dot_git_dir(&self) -> PathBuf;
fn path(&self) -> PathBuf;
}
impl std::fmt::Debug for dyn GitRepository {
@@ -86,7 +85,7 @@ impl GitRepository for RealGitRepository {
}
}
fn dot_git_dir(&self) -> PathBuf {
fn path(&self) -> PathBuf {
let repo = self.repository.lock();
repo.path().into()
}
@@ -234,7 +233,7 @@ pub struct FakeGitRepository {
#[derive(Debug, Clone)]
pub struct FakeGitRepositoryState {
pub dot_git_dir: PathBuf,
pub path: PathBuf,
pub event_emitter: smol::channel::Sender<PathBuf>,
pub index_contents: HashMap<PathBuf, String>,
pub blames: HashMap<PathBuf, Blame>,
@@ -250,9 +249,9 @@ impl FakeGitRepository {
}
impl FakeGitRepositoryState {
pub fn new(dot_git_dir: PathBuf, event_emitter: smol::channel::Sender<PathBuf>) -> Self {
pub fn new(path: PathBuf, event_emitter: smol::channel::Sender<PathBuf>) -> Self {
FakeGitRepositoryState {
dot_git_dir,
path,
event_emitter,
index_contents: Default::default(),
blames: Default::default(),
@@ -284,9 +283,9 @@ impl GitRepository for FakeGitRepository {
None
}
fn dot_git_dir(&self) -> PathBuf {
fn path(&self) -> PathBuf {
let state = self.state.lock();
state.dot_git_dir.clone()
state.path.clone()
}
fn status(&self, path_prefixes: &[PathBuf]) -> Result<GitStatus> {
@@ -335,7 +334,7 @@ impl GitRepository for FakeGitRepository {
state.current_branch_name = Some(name.to_owned());
state
.event_emitter
.try_send(state.dot_git_dir.clone())
.try_send(state.path.clone())
.expect("Dropped repo change event");
Ok(())
}
@@ -345,7 +344,7 @@ impl GitRepository for FakeGitRepository {
state.branches.insert(name.to_owned());
state
.event_emitter
.try_send(state.dot_git_dir.clone())
.try_send(state.path.clone())
.expect("Dropped repo change event");
Ok(())
}

View File

@@ -14,19 +14,22 @@ path = "src/git_ui.rs"
[dependencies]
anyhow.workspace = true
collections.workspace = true
db.workspace = true
editor.workspace = true
git.workspace = true
gpui.workspace = true
language.workspace = true
project.workspace = true
schemars.workspace = true
serde.workspace = true
serde_derive.workspace = true
serde_json.workspace = true
settings.workspace = true
theme.workspace = true
ui.workspace = true
util.workspace = true
workspace.workspace = true
git.workspace = true
collections.workspace = true
[target.'cfg(windows)'.dependencies]
windows.workspace = true

View File

@@ -1,4 +1,7 @@
use anyhow::Context;
use collections::HashMap;
use editor::Editor;
use language::Buffer;
use std::{
cell::OnceCell,
collections::HashSet,
@@ -8,6 +11,7 @@ use std::{
sync::Arc,
time::Duration,
};
use theme::ThemeSettings;
use git::repository::GitFileStatus;
@@ -24,7 +28,7 @@ use ui::{
use workspace::dock::{DockPosition, Panel, PanelEvent};
use workspace::Workspace;
use crate::{git_status_icon, settings::GitPanelSettings};
use crate::{git_status_icon, settings::GitPanelSettings, GitState};
use crate::{CommitAllChanges, CommitStagedChanges, DiscardAll, StageAll, UnstageAll};
actions!(git_panel, [ToggleFocus]);
@@ -84,6 +88,8 @@ pub struct GitPanel {
selected_item: Option<usize>,
show_scrollbar: bool,
expanded_dir_ids: HashMap<WorktreeId, Vec<ProjectEntryId>>,
git_state: Model<GitState>,
editor: View<Editor>,
// The entries that are currently shown in the panel, aka
// not hidden by folding or such
@@ -104,11 +110,17 @@ impl GitPanel {
}
pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> View<Self> {
let git_state = GitState::get_global(cx);
let fs = workspace.app_state().fs.clone();
let weak_workspace = workspace.weak_handle();
let project = workspace.project().clone();
let language_registry = workspace.app_state().languages.clone();
let git_panel = cx.new_view(|cx: &mut ViewContext<Self>| {
let state = git_state.read(cx);
let current_commit_message = state.commit_message.clone();
let focus_handle = cx.focus_handle();
cx.on_focus(&focus_handle, Self::focus_in).detach();
cx.on_focus_out(&focus_handle, |this, _, cx| {
@@ -131,6 +143,55 @@ impl GitPanel {
})
.detach();
let editor = cx.new_view(|cx| {
let theme = ThemeSettings::get_global(cx);
let mut text_style = cx.text_style();
let refinement = TextStyleRefinement {
font_family: Some(theme.buffer_font.family.clone()),
font_features: Some(FontFeatures::disable_ligatures()),
font_size: Some(px(12.).into()),
color: Some(cx.theme().colors().editor_foreground),
background_color: Some(gpui::transparent_black()),
..Default::default()
};
text_style.refine(&refinement);
let mut editor = Editor::auto_height(10, cx);
if let Some(message) = current_commit_message {
editor.set_text(message, cx);
} else {
editor.set_text("", cx);
}
// editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
editor.set_use_autoclose(false);
editor.set_show_gutter(false, cx);
editor.set_show_wrap_guides(false, cx);
editor.set_show_indent_guides(false, cx);
editor.set_text_style_refinement(refinement);
editor.set_placeholder_text("Enter commit message", cx);
editor
});
let buffer = editor
.read(cx)
.buffer()
.read(cx)
.as_singleton()
.expect("commit editor must be singleton");
cx.subscribe(&buffer, Self::on_buffer_event).detach();
let markdown = language_registry.language_for_name("Markdown");
cx.spawn(|_, mut cx| async move {
let markdown = markdown.await.context("failed to load Markdown language")?;
buffer.update(&mut cx, |buffer, cx| {
buffer.set_language(Some(markdown), cx)
})
})
.detach_and_log_err(cx);
let scroll_handle = UniformListScrollHandle::new();
let mut this = Self {
@@ -142,6 +203,8 @@ impl GitPanel {
visible_entries: Vec::new(),
current_modifiers: cx.modifiers(),
expanded_dir_ids: Default::default(),
git_state,
editor,
width: Some(px(360.)),
scrollbar_state: ScrollbarState::new(scroll_handle.clone()).parent_view(cx.view()),
@@ -269,14 +332,24 @@ impl GitPanel {
println!("Discard all triggered");
}
fn clear_message(&mut self, cx: &mut ViewContext<Self>) {
let git_state = self.git_state.clone();
git_state.update(cx, |state, _cx| state.clear_message());
self.editor.update(cx, |editor, cx| editor.set_text("", cx));
}
/// Commit all staged changes
fn commit_staged_changes(&mut self, _: &CommitStagedChanges, _cx: &mut ViewContext<Self>) {
fn commit_staged_changes(&mut self, _: &CommitStagedChanges, cx: &mut ViewContext<Self>) {
self.clear_message(cx);
// TODO: Implement commit all staged
println!("Commit staged changes triggered");
}
/// Commit all changes, regardless of whether they are staged or not
fn commit_all_changes(&mut self, _: &CommitAllChanges, _cx: &mut ViewContext<Self>) {
fn commit_all_changes(&mut self, _: &CommitAllChanges, cx: &mut ViewContext<Self>) {
self.clear_message(cx);
// TODO: Implement commit all changes
println!("Commit all changes triggered");
}
@@ -426,6 +499,23 @@ impl GitPanel {
cx.notify();
}
fn on_buffer_event(
&mut self,
_buffer: Model<Buffer>,
event: &language::BufferEvent,
cx: &mut ViewContext<Self>,
) {
if let language::BufferEvent::Reparsed | language::BufferEvent::Edited = event {
let commit_message = self.editor.update(cx, |editor, cx| editor.text(cx));
self.git_state.update(cx, |state, _cx| {
state.commit_message = Some(commit_message.into());
});
cx.notify();
}
}
}
impl GitPanel {
@@ -499,6 +589,13 @@ impl GitPanel {
}
pub fn render_commit_editor(&self, cx: &ViewContext<Self>) -> impl IntoElement {
let git_state = self.git_state.clone();
let commit_message = git_state.read(cx).commit_message.clone();
let editor = self.editor.clone();
let editor_focus_handle = editor.read(cx).focus_handle(cx).clone();
println!("{:?}", commit_message);
let focus_handle_1 = self.focus_handle(cx).clone();
let focus_handle_2 = self.focus_handle(cx).clone();
@@ -534,25 +631,26 @@ impl GitPanel {
div().w_full().h(px(140.)).px_2().pt_1().pb_2().child(
v_flex()
.id("commit-editor-container")
.relative()
.h_full()
.py_2p5()
.px_3()
.bg(cx.theme().colors().editor_background)
.font_buffer(cx)
.text_ui_sm(cx)
.text_color(cx.theme().colors().text_muted)
.child("Add a message")
.gap_1()
.child(div().flex_grow())
.child(h_flex().child(div().gap_1().flex_grow()).child(
if self.current_modifiers.alt {
commit_all_button
} else {
commit_staged_button
},
))
.cursor(CursorStyle::OperationNotAllowed)
.opacity(0.5),
.on_click(cx.listener(move |_, _: &ClickEvent, cx| cx.focus(&editor_focus_handle)))
.child(self.editor.clone())
.child(
h_flex()
.absolute()
.bottom_2p5()
.right_3()
.child(div().gap_1().flex_grow())
.child(if self.current_modifiers.alt {
commit_all_button
} else {
commit_staged_button
}),
),
)
}

View File

@@ -1,8 +1,8 @@
use ::settings::Settings;
use git::repository::GitFileStatus;
use gpui::{actions, AppContext, Hsla};
use gpui::{actions, prelude::*, AppContext, Global, Hsla, Model};
use settings::GitPanelSettings;
use ui::{Color, Icon, IconName, IntoElement};
use ui::{Color, Icon, IconName, IntoElement, SharedString};
pub mod git_panel;
mod settings;
@@ -14,14 +14,50 @@ actions!(
UnstageAll,
DiscardAll,
CommitStagedChanges,
CommitAllChanges
CommitAllChanges,
ClearMessage
]
);
pub fn init(cx: &mut AppContext) {
GitPanelSettings::register(cx);
let git_state = cx.new_model(|_cx| GitState::new());
cx.set_global(GlobalGitState(git_state));
}
struct GlobalGitState(Model<GitState>);
impl Global for GlobalGitState {}
pub struct GitState {
commit_message: Option<SharedString>,
}
impl GitState {
pub fn new() -> Self {
GitState {
commit_message: None,
}
}
pub fn set_message(&mut self, message: Option<SharedString>) {
self.commit_message = message;
}
pub fn clear_message(&mut self) {
self.commit_message = None;
}
pub fn get_global(cx: &mut AppContext) -> Model<GitState> {
cx.global::<GlobalGitState>().0.clone()
}
}
// impl EventEmitter<Event> for GitState {}
// #[derive(Clone, Debug, PartialEq, Eq)]
// pub enum Event {}
const ADDED_COLOR: Hsla = Hsla {
h: 142. / 360.,
s: 0.68,

View File

@@ -218,13 +218,13 @@ impl Render for GradientViewer {
let height = square_bounds.size.height;
let horizontal_offset = height;
let vertical_offset = px(30.);
let mut path = gpui::Path::new(square_bounds.bottom_left());
let mut path = gpui::Path::new(square_bounds.lower_left());
path.line_to(square_bounds.origin + point(horizontal_offset, vertical_offset));
path.line_to(
square_bounds.top_right() + point(-horizontal_offset, vertical_offset),
square_bounds.upper_right() + point(-horizontal_offset, vertical_offset),
);
path.line_to(square_bounds.bottom_right());
path.line_to(square_bounds.bottom_left());
path.line_to(square_bounds.lower_right());
path.line_to(square_bounds.lower_left());
cx.paint_path(
path,
linear_gradient(

View File

@@ -49,17 +49,17 @@ impl PaintingViewer {
let height = square_bounds.size.height;
let horizontal_offset = height;
let vertical_offset = px(30.);
let mut path = Path::new(square_bounds.bottom_left());
let mut path = Path::new(square_bounds.lower_left());
path.curve_to(
square_bounds.origin + point(horizontal_offset, vertical_offset),
square_bounds.origin + point(px(0.0), vertical_offset),
);
path.line_to(square_bounds.top_right() + point(-horizontal_offset, vertical_offset));
path.line_to(square_bounds.upper_right() + point(-horizontal_offset, vertical_offset));
path.curve_to(
square_bounds.bottom_right(),
square_bounds.top_right() + point(px(0.0), vertical_offset),
square_bounds.lower_right(),
square_bounds.upper_right() + point(px(0.0), vertical_offset),
);
path.line_to(square_bounds.bottom_left());
path.line_to(square_bounds.lower_left());
lines.push(path);
Self {

View File

@@ -86,7 +86,7 @@ fn main() {
.unwrap();
let bounds = Bounds {
origin: screen.bounds().top_right()
origin: screen.bounds().upper_right()
- point(size.width + margin_offset, -margin_offset),
size,
};
@@ -101,7 +101,7 @@ fn main() {
.unwrap();
let bounds = Bounds {
origin: screen.bounds().bottom_left()
origin: screen.bounds().lower_left()
- point(-margin_offset, size.height + margin_offset),
size,
};
@@ -116,7 +116,7 @@ fn main() {
.unwrap();
let bounds = Bounds {
origin: screen.bounds().bottom_right()
origin: screen.bounds().lower_right()
- point(size.width + margin_offset, size.height + margin_offset),
size,
};

View File

@@ -2,8 +2,8 @@ use smallvec::SmallVec;
use taffy::style::{Display, Position};
use crate::{
point, AnyElement, Axis, Bounds, Corner, Edges, Element, GlobalElementId, IntoElement,
LayoutId, ParentElement, Pixels, Point, Size, Style, WindowContext,
point, AnyElement, Bounds, Edges, Element, GlobalElementId, IntoElement, LayoutId,
ParentElement, Pixels, Point, Size, Style, WindowContext,
};
/// The state that the anchored element element uses to track its children.
@@ -15,7 +15,7 @@ pub struct AnchoredState {
/// will avoid overflowing the window bounds.
pub struct Anchored {
children: SmallVec<[AnyElement; 2]>,
anchor_corner: Corner,
anchor_corner: AnchorCorner,
fit_mode: AnchoredFitMode,
anchor_position: Option<Point<Pixels>>,
position_mode: AnchoredPositionMode,
@@ -26,7 +26,7 @@ pub struct Anchored {
pub fn anchored() -> Anchored {
Anchored {
children: SmallVec::new(),
anchor_corner: Corner::TopLeft,
anchor_corner: AnchorCorner::TopLeft,
fit_mode: AnchoredFitMode::SwitchAnchor,
anchor_position: None,
position_mode: AnchoredPositionMode::Window,
@@ -35,7 +35,7 @@ pub fn anchored() -> Anchored {
impl Anchored {
/// Sets which corner of the anchored element should be anchored to the current position.
pub fn anchor(mut self, anchor: Corner) -> Self {
pub fn anchor(mut self, anchor: AnchorCorner) -> Self {
self.anchor_corner = anchor;
self
}
@@ -120,7 +120,7 @@ impl Element for Anchored {
for child_layout_id in &request_layout.child_layout_ids {
let child_bounds = cx.layout_bounds(*child_layout_id);
child_min = child_min.min(&child_bounds.origin);
child_max = child_max.max(&child_bounds.bottom_right());
child_max = child_max.max(&child_bounds.lower_right());
}
let size: Size<Pixels> = (child_max - child_min).into();
@@ -140,23 +140,19 @@ impl Element for Anchored {
let mut anchor_corner = self.anchor_corner;
if desired.left() < limits.left() || desired.right() > limits.right() {
let switched = Bounds::from_corner_and_size(
anchor_corner.other_side_corner_along(Axis::Horizontal),
origin,
size,
);
let switched = anchor_corner
.switch_axis(Axis::Horizontal)
.get_bounds(origin, size);
if !(switched.left() < limits.left() || switched.right() > limits.right()) {
anchor_corner = anchor_corner.other_side_corner_along(Axis::Horizontal);
anchor_corner = anchor_corner.switch_axis(Axis::Horizontal);
desired = switched
}
}
if desired.top() < limits.top() || desired.bottom() > limits.bottom() {
let switched = Bounds::from_corner_and_size(
anchor_corner.other_side_corner_along(Axis::Vertical),
origin,
size,
);
let switched = anchor_corner
.switch_axis(Axis::Vertical)
.get_bounds(origin, size);
if !(switched.top() < limits.top() || switched.bottom() > limits.bottom()) {
desired = switched;
}
@@ -218,6 +214,11 @@ impl IntoElement for Anchored {
}
}
enum Axis {
Horizontal,
Vertical,
}
/// Which algorithm to use when fitting the anchored element to be inside the window.
#[derive(Copy, Clone, PartialEq)]
pub enum AnchoredFitMode {
@@ -242,25 +243,83 @@ impl AnchoredPositionMode {
fn get_position_and_bounds(
&self,
anchor_position: Option<Point<Pixels>>,
anchor_corner: Corner,
anchor_corner: AnchorCorner,
size: Size<Pixels>,
bounds: Bounds<Pixels>,
) -> (Point<Pixels>, Bounds<Pixels>) {
match self {
AnchoredPositionMode::Window => {
let anchor_position = anchor_position.unwrap_or(bounds.origin);
let bounds = Bounds::from_corner_and_size(anchor_corner, anchor_position, size);
let bounds = anchor_corner.get_bounds(anchor_position, size);
(anchor_position, bounds)
}
AnchoredPositionMode::Local => {
let anchor_position = anchor_position.unwrap_or_default();
let bounds = Bounds::from_corner_and_size(
anchor_corner,
bounds.origin + anchor_position,
size,
);
let bounds = anchor_corner.get_bounds(bounds.origin + anchor_position, size);
(anchor_position, bounds)
}
}
}
}
/// Which corner of the anchored element should be considered the anchor.
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum AnchorCorner {
/// The top left corner
TopLeft,
/// The top right corner
TopRight,
/// The bottom left corner
BottomLeft,
/// The bottom right corner
BottomRight,
}
impl AnchorCorner {
fn get_bounds(&self, origin: Point<Pixels>, size: Size<Pixels>) -> Bounds<Pixels> {
let origin = match self {
Self::TopLeft => origin,
Self::TopRight => Point {
x: origin.x - size.width,
y: origin.y,
},
Self::BottomLeft => Point {
x: origin.x,
y: origin.y - size.height,
},
Self::BottomRight => Point {
x: origin.x - size.width,
y: origin.y - size.height,
},
};
Bounds { origin, size }
}
/// Get the point corresponding to this anchor corner in `bounds`.
pub fn corner(&self, bounds: Bounds<Pixels>) -> Point<Pixels> {
match self {
Self::TopLeft => bounds.origin,
Self::TopRight => bounds.upper_right(),
Self::BottomLeft => bounds.lower_left(),
Self::BottomRight => bounds.lower_right(),
}
}
fn switch_axis(self, axis: Axis) -> Self {
match axis {
Axis::Vertical => match self {
AnchorCorner::TopLeft => AnchorCorner::BottomLeft,
AnchorCorner::TopRight => AnchorCorner::BottomRight,
AnchorCorner::BottomLeft => AnchorCorner::TopLeft,
AnchorCorner::BottomRight => AnchorCorner::TopRight,
},
Axis::Horizontal => match self {
AnchorCorner::TopLeft => AnchorCorner::TopRight,
AnchorCorner::TopRight => AnchorCorner::TopLeft,
AnchorCorner::BottomLeft => AnchorCorner::BottomRight,
AnchorCorner::BottomRight => AnchorCorner::BottomLeft,
},
}
}
}

View File

@@ -1193,7 +1193,7 @@ impl Element for Div {
for (ix, child_layout_id) in request_layout.child_layout_ids.iter().enumerate() {
let child_bounds = cx.layout_bounds(*child_layout_id);
child_min = child_min.min(&child_bounds.origin);
child_max = child_max.max(&child_bounds.bottom_right());
child_max = child_max.max(&child_bounds.lower_right());
state.child_bounds.push(child_bounds);
if let Some(requested) = requested.as_ref() {
@@ -1208,7 +1208,7 @@ impl Element for Div {
for child_layout_id in &request_layout.child_layout_ids {
let child_bounds = cx.layout_bounds(*child_layout_id);
child_min = child_min.min(&child_bounds.origin);
child_max = child_max.max(&child_bounds.bottom_right());
child_max = child_max.max(&child_bounds.lower_right());
}
(child_max - child_min).into()
};

View File

@@ -219,7 +219,7 @@ impl Element for UniformList {
let padded_bounds = Bounds::from_corners(
bounds.origin + point(border.left + padding.left, border.top + padding.top),
bounds.bottom_right()
bounds.lower_right()
- point(border.right + padding.right, border.bottom + padding.bottom),
);
@@ -261,7 +261,7 @@ impl Element for UniformList {
let padded_bounds = Bounds::from_corners(
bounds.origin + point(border.left + padding.left, border.top),
bounds.bottom_right() - point(border.right + padding.right, border.bottom),
bounds.lower_right() - point(border.right + padding.right, border.bottom),
);
if let Some(handle) = self.scroll_handle.as_mut() {

View File

@@ -50,10 +50,7 @@ pub struct ForegroundExecutor {
/// the task to continue running, but with no way to return a value.
#[must_use]
#[derive(Debug)]
pub struct Task<T>(TaskState<T>);
#[derive(Debug)]
enum TaskState<T> {
pub enum Task<T> {
/// A task that is ready to return a value
Ready(Option<T>),
@@ -64,14 +61,14 @@ enum TaskState<T> {
impl<T> Task<T> {
/// Creates a new task that will resolve with the value
pub fn ready(val: T) -> Self {
Task(TaskState::Ready(Some(val)))
Task::Ready(Some(val))
}
/// Detaching a task runs it to completion in the background
pub fn detach(self) {
match self {
Task(TaskState::Ready(_)) => {}
Task(TaskState::Spawned(task)) => task.detach(),
Task::Ready(_) => {}
Task::Spawned(task) => task.detach(),
}
}
}
@@ -97,8 +94,8 @@ impl<T> Future for Task<T> {
fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> {
match unsafe { self.get_unchecked_mut() } {
Task(TaskState::Ready(val)) => Poll::Ready(val.take().unwrap()),
Task(TaskState::Spawned(task)) => task.poll(cx),
Task::Ready(val) => Poll::Ready(val.take().unwrap()),
Task::Spawned(task) => task.poll(cx),
}
}
}
@@ -166,7 +163,7 @@ impl BackgroundExecutor {
let (runnable, task) =
async_task::spawn(future, move |runnable| dispatcher.dispatch(runnable, label));
runnable.schedule();
Task(TaskState::Spawned(task))
Task::Spawned(task)
}
/// Used by the test harness to run an async test in a synchronous fashion.
@@ -343,7 +340,7 @@ impl BackgroundExecutor {
move |runnable| dispatcher.dispatch_after(duration, runnable)
});
runnable.schedule();
Task(TaskState::Spawned(task))
Task::Spawned(task)
}
/// in tests, start_waiting lets you indicate which task is waiting (for debugging only)
@@ -463,7 +460,7 @@ impl ForegroundExecutor {
dispatcher.dispatch_on_main_thread(runnable)
});
runnable.schedule();
Task(TaskState::Spawned(task))
Task::Spawned(task)
}
inner::<R>(dispatcher, Box::pin(future))
}

View File

@@ -10,12 +10,12 @@ use std::{
cmp::{self, PartialOrd},
fmt,
hash::Hash,
ops::{Add, Div, Mul, MulAssign, Neg, Sub},
ops::{Add, Div, Mul, MulAssign, Sub},
};
use crate::{AppContext, DisplayId};
/// Axis in a 2D cartesian space.
/// An axis along which a measurement can be made.
#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize, Debug)]
pub enum Axis {
/// The y axis, or up and down
@@ -46,7 +46,7 @@ pub trait Along {
fn apply_along(&self, axis: Axis, f: impl FnOnce(Self::Unit) -> Self::Unit) -> Self;
}
/// Describes a location in a 2D cartesian space.
/// Describes a location in a 2D cartesian coordinate space.
///
/// It holds two public fields, `x` and `y`, which represent the coordinates in the space.
/// The type `T` for the coordinates can be any type that implements `Default`, `Clone`, and `Debug`.
@@ -54,7 +54,7 @@ pub trait Along {
/// # Examples
///
/// ```
/// # use gpui::Point;
/// # use zed::Point;
/// let point = Point { x: 10, y: 20 };
/// println!("{:?}", point); // Outputs: Point { x: 10, y: 20 }
/// ```
@@ -96,7 +96,7 @@ pub struct Point<T: Default + Clone + Debug> {
/// # Examples
///
/// ```
/// # use gpui::Point;
/// # use zed::Point;
/// let p = point(10, 20);
/// assert_eq!(p.x, 10);
/// assert_eq!(p.y, 20);
@@ -137,7 +137,7 @@ impl<T: Clone + Debug + Default> Point<T> {
/// # Examples
///
/// ```
/// # use gpui::Point;
/// # use zed::Point;
/// let p = Point { x: 3, y: 4 };
/// let p_float = p.map(|coord| coord as f32);
/// assert_eq!(p_float, Point { x: 3.0, y: 4.0 });
@@ -191,7 +191,7 @@ impl Point<Pixels> {
/// # Examples
///
/// ```
/// # use gpui::{Point, Pixels, ScaledPixels};
/// # use zed::{Point, Pixels, ScaledPixels};
/// let p = Point { x: Pixels(10.0), y: Pixels(20.0) };
/// let scaled_p = p.scale(1.5);
/// assert_eq!(scaled_p, Point { x: ScaledPixels(15.0), y: ScaledPixels(30.0) });
@@ -208,7 +208,8 @@ impl Point<Pixels> {
/// # Examples
///
/// ```
/// # use gpui::{Pixels, Point};
/// # use zed::Point;
/// # use zed::Pixels;
/// let p = Point { x: Pixels(3.0), y: Pixels(4.0) };
/// assert_eq!(p.magnitude(), 5.0);
/// ```
@@ -271,7 +272,7 @@ where
/// # Examples
///
/// ```
/// # use gpui::Point;
/// # use zed::Point;
/// let p1 = Point { x: 3, y: 7 };
/// let p2 = Point { x: 5, y: 2 };
/// let max_point = p1.max(&p2);
@@ -301,7 +302,7 @@ where
/// # Examples
///
/// ```
/// # use gpui::Point;
/// # use zed::Point;
/// let p1 = Point { x: 3, y: 7 };
/// let p2 = Point { x: 5, y: 2 };
/// let min_point = p1.min(&p2);
@@ -337,7 +338,7 @@ where
/// # Examples
///
/// ```
/// # use gpui::Point;
/// # use zed::Point;
/// let p = Point { x: 10, y: 20 };
/// let min = Point { x: 0, y: 5 };
/// let max = Point { x: 15, y: 25 };
@@ -386,7 +387,7 @@ pub struct Size<T: Clone + Default + Debug> {
/// # Examples
///
/// ```
/// # use gpui::Size;
/// # use zed::Size;
/// let my_size = size(10, 20);
/// assert_eq!(my_size.width, 10);
/// assert_eq!(my_size.height, 20);
@@ -415,7 +416,7 @@ where
/// # Examples
///
/// ```
/// # use gpui::Size;
/// # use zed::Size;
/// let my_size = Size { width: 10, height: 20 };
/// let my_new_size = my_size.map(|dimension| dimension as f32 * 1.5);
/// assert_eq!(my_new_size, Size { width: 15.0, height: 30.0 });
@@ -458,7 +459,7 @@ impl Size<Pixels> {
/// # Examples
///
/// ```
/// # use gpui::{Size, Pixels, ScaledPixels};
/// # use zed::{Size, Pixels, ScaledPixels};
/// let size = Size { width: Pixels(100.0), height: Pixels(50.0) };
/// let scaled_size = size.scale(2.0);
/// assert_eq!(scaled_size, Size { width: ScaledPixels(200.0), height: ScaledPixels(100.0) });
@@ -512,7 +513,7 @@ where
/// # Examples
///
/// ```
/// # use gpui::Size;
/// # use zed::Size;
/// let size1 = Size { width: 30, height: 40 };
/// let size2 = Size { width: 50, height: 20 };
/// let max_size = size1.max(&size2);
@@ -532,7 +533,6 @@ where
},
}
}
/// Returns a new `Size` with the minimum width and height from `self` and `other`.
///
/// # Arguments
@@ -542,7 +542,7 @@ where
/// # Examples
///
/// ```
/// # use gpui::Size;
/// # use zed::Size;
/// let size1 = Size { width: 30, height: 40 };
/// let size2 = Size { width: 50, height: 20 };
/// let min_size = size1.min(&size2);
@@ -694,13 +694,13 @@ impl Size<Length> {
/// Represents a rectangular area in a 2D space with an origin point and a size.
///
/// The `Bounds` struct is generic over a type `T` which represents the type of the coordinate system.
/// The origin is represented as a `Point<T>` which defines the top left corner of the rectangle,
/// The origin is represented as a `Point<T>` which defines the upper-left corner of the rectangle,
/// and the size is represented as a `Size<T>` which defines the width and height of the rectangle.
///
/// # Examples
///
/// ```
/// # use gpui::{Bounds, Point, Size};
/// # use zed::{Bounds, Point, Size};
/// let origin = Point { x: 0, y: 0 };
/// let size = Size { width: 10, height: 20 };
/// let bounds = Bounds::new(origin, size);
@@ -731,7 +731,13 @@ impl Bounds<Pixels> {
.or_else(|| cx.primary_display());
display
.map(|display| Bounds::centered_at(display.bounds().center(), size))
.map(|display| {
let center = display.bounds().center();
Bounds {
origin: point(center.x - size.width / 2., center.y - size.height / 2.),
size,
}
})
.unwrap_or_else(|| Bounds {
origin: point(px(0.), px(0.)),
size,
@@ -755,8 +761,47 @@ impl Bounds<Pixels> {
impl<T> Bounds<T>
where
T: Clone + Debug + Default,
T: Clone + Debug + Sub<Output = T> + Default,
{
/// Constructs a `Bounds` from two corner points: the upper-left and lower-right corners.
///
/// This function calculates the origin and size of the `Bounds` based on the provided corner points.
/// The origin is set to the upper-left corner, and the size is determined by the difference between
/// the x and y coordinates of the lower-right and upper-left points.
///
/// # Arguments
///
/// * `upper_left` - A `Point<T>` representing the upper-left corner of the rectangle.
/// * `lower_right` - A `Point<T>` representing the lower-right corner of the rectangle.
///
/// # Returns
///
/// Returns a `Bounds<T>` that encompasses the area defined by the two corner points.
///
/// # Examples
///
/// ```
/// # use zed::{Bounds, Point};
/// let upper_left = Point { x: 0, y: 0 };
/// let lower_right = Point { x: 10, y: 10 };
/// let bounds = Bounds::from_corners(upper_left, lower_right);
///
/// assert_eq!(bounds.origin, upper_left);
/// assert_eq!(bounds.size.width, 10);
/// assert_eq!(bounds.size.height, 10);
/// ```
pub fn from_corners(upper_left: Point<T>, lower_right: Point<T>) -> Self {
let origin = Point {
x: upper_left.x.clone(),
y: upper_left.y.clone(),
};
let size = Size {
width: lower_right.x - upper_left.x,
height: lower_right.y - upper_left.y,
};
Bounds { origin, size }
}
/// Creates a new `Bounds` with the specified origin and size.
///
/// # Arguments
@@ -774,93 +819,7 @@ where
impl<T> Bounds<T>
where
T: Clone + Debug + Sub<Output = T> + Default,
{
/// Constructs a `Bounds` from two corner points: the top left and bottom right corners.
///
/// This function calculates the origin and size of the `Bounds` based on the provided corner points.
/// The origin is set to the top left corner, and the size is determined by the difference between
/// the x and y coordinates of the bottom right and top left points.
///
/// # Arguments
///
/// * `upper_left` - A `Point<T>` representing the top left corner of the rectangle.
/// * `bottom_right` - A `Point<T>` representing the bottom right corner of the rectangle.
///
/// # Returns
///
/// Returns a `Bounds<T>` that encompasses the area defined by the two corner points.
///
/// # Examples
///
/// ```
/// # use gpui::{Bounds, Point};
/// let upper_left = Point { x: 0, y: 0 };
/// let bottom_right = Point { x: 10, y: 10 };
/// let bounds = Bounds::from_corners(upper_left, bottom_right);
///
/// assert_eq!(bounds.origin, upper_left);
/// assert_eq!(bounds.size.width, 10);
/// assert_eq!(bounds.size.height, 10);
/// ```
pub fn from_corners(upper_left: Point<T>, bottom_right: Point<T>) -> Self {
let origin = Point {
x: upper_left.x.clone(),
y: upper_left.y.clone(),
};
let size = Size {
width: bottom_right.x - upper_left.x,
height: bottom_right.y - upper_left.y,
};
Bounds { origin, size }
}
/// Constructs a `Bounds` from a corner point and size.
///
/// # Examples
///
/// ```
/// # use zed::{Bounds, Corner, Point};
/// todo!
/// ```
pub fn from_corner_and_size(corner: Corner, origin: Point<T>, size: Size<T>) -> Bounds<T> {
let origin = match corner {
Corner::TopLeft => origin,
Corner::TopRight => Point {
x: origin.x - size.width.clone(),
y: origin.y,
},
Corner::BottomLeft => Point {
x: origin.x,
y: origin.y - size.height.clone(),
},
Corner::BottomRight => Point {
x: origin.x - size.width.clone(),
y: origin.y - size.height.clone(),
},
};
Bounds { origin, size }
}
}
impl<T> Bounds<T>
where
T: Clone + Debug + Sub<T, Output = T> + Default + Half,
{
/// Creates a new bounds centered at the given point.
pub fn centered_at(center: Point<T>, size: Size<T>) -> Self {
let origin = Point {
x: center.x - size.width.half(),
y: center.y - size.height.half(),
};
Self::new(origin, size)
}
}
impl<T> Bounds<T>
where
T: Clone + Debug + PartialOrd + Add<T, Output = T> + Default,
T: Clone + Debug + PartialOrd + Add<T, Output = T> + Sub<Output = T> + Default + Half,
{
/// Checks if this `Bounds` intersects with another `Bounds`.
///
@@ -878,7 +837,7 @@ where
/// # Examples
///
/// ```
/// # use gpui::{Bounds, Point, Size};
/// # use zed::{Bounds, Point, Size};
/// let bounds1 = Bounds {
/// origin: Point { x: 0, y: 0 },
/// size: Size { width: 10, height: 10 },
@@ -896,80 +855,15 @@ where
/// assert_eq!(bounds1.intersects(&bounds3), false); // Non-overlapping bounds
/// ```
pub fn intersects(&self, other: &Bounds<T>) -> bool {
let my_lower_right = self.bottom_right();
let their_lower_right = other.bottom_right();
let my_lower_right = self.lower_right();
let their_lower_right = other.lower_right();
self.origin.x < their_lower_right.x
&& my_lower_right.x > other.origin.x
&& self.origin.y < their_lower_right.y
&& my_lower_right.y > other.origin.y
}
}
impl<T> Bounds<T>
where
T: Clone + Debug + Add<T, Output = T> + Default + Half,
{
/// Returns the center point of the bounds.
///
/// Calculates the center by taking the origin's x and y coordinates and adding half the width and height
/// of the bounds, respectively. The center is represented as a `Point<T>` where `T` is the type of the
/// coordinate system.
///
/// # Returns
///
/// A `Point<T>` representing the center of the bounds.
///
/// # Examples
///
/// ```
/// # use gpui::{Bounds, Point, Size};
/// let bounds = Bounds {
/// origin: Point { x: 0, y: 0 },
/// size: Size { width: 10, height: 20 },
/// };
/// let center = bounds.center();
/// assert_eq!(center, Point { x: 5, y: 10 });
/// ```
pub fn center(&self) -> Point<T> {
Point {
x: self.origin.x.clone() + self.size.width.clone().half(),
y: self.origin.y.clone() + self.size.height.clone().half(),
}
}
}
impl<T> Bounds<T>
where
T: Clone + Debug + Add<T, Output = T> + Default,
{
/// Calculates the half perimeter of a rectangle defined by the bounds.
///
/// The half perimeter is calculated as the sum of the width and the height of the rectangle.
/// This method is generic over the type `T` which must implement the `Sub` trait to allow
/// calculation of the width and height from the bounds' origin and size, as well as the `Add` trait
/// to sum the width and height for the half perimeter.
///
/// # Examples
///
/// ```
/// # use gpui::{Bounds, Point, Size};
/// let bounds = Bounds {
/// origin: Point { x: 0, y: 0 },
/// size: Size { width: 10, height: 20 },
/// };
/// let half_perimeter = bounds.half_perimeter();
/// assert_eq!(half_perimeter, 30);
/// ```
pub fn half_perimeter(&self) -> T {
self.size.width.clone() + self.size.height.clone()
}
}
impl<T> Bounds<T>
where
T: Clone + Debug + Add<T, Output = T> + Sub<Output = T> + Default,
{
/// Dilates the bounds by a specified amount in all directions.
///
/// This method expands the bounds by the given `amount`, increasing the size
@@ -985,7 +879,7 @@ where
/// # Examples
///
/// ```
/// # use gpui::{Bounds, Point, Size};
/// # use zed::{Bounds, Point, Size};
/// let mut bounds = Bounds {
/// origin: Point { x: 10, y: 10 },
/// size: Size { width: 10, height: 10 },
@@ -996,24 +890,79 @@ where
/// size: Size { width: 20, height: 20 },
/// });
/// ```
pub fn dilate(&self, amount: T) -> Bounds<T> {
let double_amount = amount.clone() + amount.clone();
Bounds {
origin: self.origin.clone() - point(amount.clone(), amount),
size: self.size.clone() + size(double_amount.clone(), double_amount),
pub fn dilate(&mut self, amount: T) {
self.origin.x = self.origin.x.clone() - amount.clone();
self.origin.y = self.origin.y.clone() - amount.clone();
let double_amount = amount.clone() + amount;
self.size.width = self.size.width.clone() + double_amount.clone();
self.size.height = self.size.height.clone() + double_amount;
}
/// inset the bounds by a specified amount
/// Note that this may panic if T does not support negative values
pub fn inset(&self, amount: T) -> Self {
let mut result = self.clone();
result.dilate(T::default() - amount);
result
}
/// Returns the center point of the bounds.
///
/// Calculates the center by taking the origin's x and y coordinates and adding half the width and height
/// of the bounds, respectively. The center is represented as a `Point<T>` where `T` is the type of the
/// coordinate system.
///
/// # Returns
///
/// A `Point<T>` representing the center of the bounds.
///
/// # Examples
///
/// ```
/// # use zed::{Bounds, Point, Size};
/// let bounds = Bounds {
/// origin: Point { x: 0, y: 0 },
/// size: Size { width: 10, height: 20 },
/// };
/// let center = bounds.center();
/// assert_eq!(center, Point { x: 5, y: 10 });
/// ```
pub fn center(&self) -> Point<T> {
Point {
x: self.origin.x.clone() + self.size.width.clone().half(),
y: self.origin.y.clone() + self.size.height.clone().half(),
}
}
}
impl<T> Bounds<T>
where
T: Clone + Debug + Add<T, Output = T> + Sub<T, Output = T> + Neg<Output = T> + Default,
{
/// Inset the bounds by a specified amount. Equivalent to `dilate` with the amount negated.
/// Calculates the half perimeter of a rectangle defined by the bounds.
///
/// Note that this may panic if T does not support negative values.
pub fn inset(&self, amount: T) -> Self {
self.dilate(-amount)
/// The half perimeter is calculated as the sum of the width and the height of the rectangle.
/// This method is generic over the type `T` which must implement the `Sub` trait to allow
/// calculation of the width and height from the bounds' origin and size, as well as the `Add` trait
/// to sum the width and height for the half perimeter.
///
/// # Examples
///
/// ```
/// # use zed::{Bounds, Point, Size};
/// let bounds = Bounds {
/// origin: Point { x: 0, y: 0 },
/// size: Size { width: 10, height: 20 },
/// };
/// let half_perimeter = bounds.half_perimeter();
/// assert_eq!(half_perimeter, 30);
/// ```
pub fn half_perimeter(&self) -> T {
self.size.width.clone() + self.size.height.clone()
}
/// centered_at creates a new bounds centered at the given point.
pub fn centered_at(center: Point<T>, size: Size<T>) -> Self {
let origin = Point {
x: center.x - size.width.half(),
y: center.y - size.height.half(),
};
Self::new(origin, size)
}
}
@@ -1035,7 +984,7 @@ impl<T: Clone + Default + Debug + PartialOrd + Add<T, Output = T> + Sub<Output =
/// # Examples
///
/// ```
/// # use gpui::{Bounds, Point, Size};
/// # use zed::{Bounds, Point, Size};
/// let bounds1 = Bounds {
/// origin: Point { x: 0, y: 0 },
/// size: Size { width: 10, height: 10 },
@@ -1053,8 +1002,8 @@ impl<T: Clone + Default + Debug + PartialOrd + Add<T, Output = T> + Sub<Output =
/// ```
pub fn intersect(&self, other: &Self) -> Self {
let upper_left = self.origin.max(&other.origin);
let bottom_right = self.bottom_right().min(&other.bottom_right());
Self::from_corners(upper_left, bottom_right)
let lower_right = self.lower_right().min(&other.lower_right());
Self::from_corners(upper_left, lower_right)
}
/// Computes the union of two `Bounds`.
@@ -1074,7 +1023,7 @@ impl<T: Clone + Default + Debug + PartialOrd + Add<T, Output = T> + Sub<Output =
/// # Examples
///
/// ```
/// # use gpui::{Bounds, Point, Size};
/// # use zed::{Bounds, Point, Size};
/// let bounds1 = Bounds {
/// origin: Point { x: 0, y: 0 },
/// size: Size { width: 10, height: 10 },
@@ -1092,7 +1041,7 @@ impl<T: Clone + Default + Debug + PartialOrd + Add<T, Output = T> + Sub<Output =
/// ```
pub fn union(&self, other: &Self) -> Self {
let top_left = self.origin.min(&other.origin);
let bottom_right = self.bottom_right().max(&other.bottom_right());
let bottom_right = self.lower_right().max(&other.lower_right());
Bounds::from_corners(top_left, bottom_right)
}
}
@@ -1140,34 +1089,6 @@ where
}
}
impl<T> Add<Point<T>> for Bounds<T>
where
T: Add<T, Output = T> + Default + Clone + Debug,
{
type Output = Self;
fn add(self, rhs: Point<T>) -> Self {
Self {
origin: self.origin + rhs,
size: self.size,
}
}
}
impl<T> Sub<Point<T>> for Bounds<T>
where
T: Sub<T, Output = T> + Default + Clone + Debug,
{
type Output = Self;
fn sub(self, rhs: Point<T>) -> Self {
Self {
origin: self.origin - rhs,
size: self.size,
}
}
}
impl<T> Bounds<T>
where
T: Add<T, Output = T> + Clone + Default + Debug,
@@ -1208,103 +1129,77 @@ where
self.origin.x.clone() + self.size.width.clone()
}
/// Returns the top right corner point of the bounds.
/// Returns the upper-right corner point of the bounds.
///
/// # Returns
///
/// A `Point<T>` representing the top right corner of the bounds.
/// A `Point<T>` representing the upper-right corner of the bounds.
///
/// # Examples
///
/// ```
/// # use gpui::{Bounds, Point, Size};
/// # use zed::{Bounds, Point, Size};
/// let bounds = Bounds {
/// origin: Point { x: 0, y: 0 },
/// size: Size { width: 10, height: 20 },
/// };
/// let top_right = bounds.top_right();
/// assert_eq!(top_right, Point { x: 10, y: 0 });
/// let upper_right = bounds.upper_right();
/// assert_eq!(upper_right, Point { x: 10, y: 0 });
/// ```
pub fn top_right(&self) -> Point<T> {
pub fn upper_right(&self) -> Point<T> {
Point {
x: self.origin.x.clone() + self.size.width.clone(),
y: self.origin.y.clone(),
}
}
/// Returns the bottom right corner point of the bounds.
/// Returns the lower-right corner point of the bounds.
///
/// # Returns
///
/// A `Point<T>` representing the bottom right corner of the bounds.
/// A `Point<T>` representing the lower-right corner of the bounds.
///
/// # Examples
///
/// ```
/// # use gpui::{Bounds, Point, Size};
/// # use zed::{Bounds, Point, Size};
/// let bounds = Bounds {
/// origin: Point { x: 0, y: 0 },
/// size: Size { width: 10, height: 20 },
/// };
/// let bottom_right = bounds.bottom_right();
/// assert_eq!(bottom_right, Point { x: 10, y: 20 });
/// let lower_right = bounds.lower_right();
/// assert_eq!(lower_right, Point { x: 10, y: 20 });
/// ```
pub fn bottom_right(&self) -> Point<T> {
pub fn lower_right(&self) -> Point<T> {
Point {
x: self.origin.x.clone() + self.size.width.clone(),
y: self.origin.y.clone() + self.size.height.clone(),
}
}
/// Returns the bottom left corner point of the bounds.
/// Returns the lower-left corner point of the bounds.
///
/// # Returns
///
/// A `Point<T>` representing the bottom left corner of the bounds.
/// A `Point<T>` representing the lower-left corner of the bounds.
///
/// # Examples
///
/// ```
/// # use gpui::{Bounds, Point, Size};
/// # use zed::{Bounds, Point, Size};
/// let bounds = Bounds {
/// origin: Point { x: 0, y: 0 },
/// size: Size { width: 10, height: 20 },
/// };
/// let bottom_left = bounds.bottom_left();
/// assert_eq!(bottom_left, Point { x: 0, y: 20 });
/// let lower_left = bounds.lower_left();
/// assert_eq!(lower_left, Point { x: 0, y: 20 });
/// ```
pub fn bottom_left(&self) -> Point<T> {
pub fn lower_left(&self) -> Point<T> {
Point {
x: self.origin.x.clone(),
y: self.origin.y.clone() + self.size.height.clone(),
}
}
/// Returns the requested corner point of the bounds.
///
/// # Returns
///
/// A `Point<T>` representing the corner of the bounds requested by the parameter.
///
/// # Examples
///
/// ```
/// # use zed::{Bounds, Corner, Point, Size};
/// let bounds = Bounds {
/// origin: Point { x: 0, y: 0 },
/// size: Size { width: 10, height: 20 },
/// };
/// let bottom_left = bounds.corner(Corner::BottomLeft);
/// assert_eq!(bottom_left, Point { x: 0, y: 20 });
/// ```
pub fn corner(&self, corner: Corner) -> Point<T> {
match corner {
Corner::TopLeft => self.origin.clone(),
Corner::TopRight => self.top_right(),
Corner::BottomLeft => self.bottom_left(),
Corner::BottomRight => self.bottom_right(),
}
}
}
impl<T> Bounds<T>
@@ -1329,7 +1224,7 @@ where
/// # Examples
///
/// ```
/// # use gpui::{Point, Bounds};
/// # use zed::{Point, Bounds};
/// let bounds = Bounds {
/// origin: Point { x: 0, y: 0 },
/// size: Size { width: 10, height: 10 },
@@ -1364,7 +1259,7 @@ where
/// # Examples
///
/// ```
/// # use gpui::{Bounds, Point, Size};
/// # use zed::{Bounds, Point, Size};
/// let bounds = Bounds {
/// origin: Point { x: 10.0, y: 10.0 },
/// size: Size { width: 10.0, height: 20.0 },
@@ -1391,7 +1286,7 @@ where
/// # Examples
///
/// ```
/// # use gpui::{Bounds, Point, Size};
/// # use zed::{Bounds, Point, Size};
/// let bounds = Bounds {
/// origin: Point { x: 10.0, y: 10.0 },
/// size: Size { width: 10.0, height: 20.0 },
@@ -1415,7 +1310,7 @@ where
/// # Examples
///
/// ```
/// # use gpui::{Bounds, Point, Size};
/// # use zed::{Bounds, Point, Size};
/// let bounds = Bounds {
/// origin: Point { x: 10.0, y: 10.0 },
/// size: Size { width: 10.0, height: 20.0 },
@@ -1490,7 +1385,7 @@ impl Bounds<Pixels> {
/// # Examples
///
/// ```
/// # use gpui::{Bounds, Point, Size, Pixels};
/// # use zed::{Bounds, Point, Size, Pixels};
/// let bounds = Bounds {
/// origin: Point { x: Pixels(10.0), y: Pixels(20.0) },
/// size: Size { width: Pixels(30.0), height: Pixels(40.0) },
@@ -1543,7 +1438,7 @@ impl<T: Clone + Debug + Copy + Default> Copy for Bounds<T> {}
/// # Examples
///
/// ```
/// # use gpui::Edges;
/// # use zed::Edges;
/// let edges = Edges {
/// top: 10.0,
/// right: 20.0,
@@ -1619,7 +1514,7 @@ impl<T: Clone + Default + Debug> Edges<T> {
/// # Examples
///
/// ```
/// # use gpui::Edges;
/// # use zed::Edges;
/// let uniform_edges = Edges::all(10.0);
/// assert_eq!(uniform_edges.top, 10.0);
/// assert_eq!(uniform_edges.right, 10.0);
@@ -1652,7 +1547,7 @@ impl<T: Clone + Default + Debug> Edges<T> {
/// # Examples
///
/// ```
/// # use gpui::Edges;
/// # use zed::Edges;
/// let edges = Edges { top: 10, right: 20, bottom: 30, left: 40 };
/// let edges_float = edges.map(|&value| value as f32 * 1.1);
/// assert_eq!(edges_float, Edges { top: 11.0, right: 22.0, bottom: 33.0, left: 44.0 });
@@ -1684,7 +1579,7 @@ impl<T: Clone + Default + Debug> Edges<T> {
/// # Examples
///
/// ```
/// # use gpui::Edges;
/// # use zed::Edges;
/// let edges = Edges {
/// top: 10,
/// right: 0,
@@ -1716,7 +1611,7 @@ impl Edges<Length> {
/// # Examples
///
/// ```
/// # use gpui::Edges;
/// # use zed::Edges;
/// let auto_edges = Edges::auto();
/// assert_eq!(auto_edges.top, Length::Auto);
/// assert_eq!(auto_edges.right, Length::Auto);
@@ -1744,7 +1639,7 @@ impl Edges<Length> {
/// # Examples
///
/// ```
/// # use gpui::Edges;
/// # use zed::Edges;
/// let no_edges = Edges::zero();
/// assert_eq!(no_edges.top, Length::Definite(DefiniteLength::from(Pixels(0.))));
/// assert_eq!(no_edges.right, Length::Definite(DefiniteLength::from(Pixels(0.))));
@@ -1774,12 +1669,12 @@ impl Edges<DefiniteLength> {
/// # Examples
///
/// ```
/// # use gpui::{px, Edges};
/// # use zed::Edges;
/// let no_edges = Edges::zero();
/// assert_eq!(no_edges.top, DefiniteLength::from(px(0.)));
/// assert_eq!(no_edges.right, DefiniteLength::from(px(0.)));
/// assert_eq!(no_edges.bottom, DefiniteLength::from(px(0.)));
/// assert_eq!(no_edges.left, DefiniteLength::from(px(0.)));
/// assert_eq!(no_edges.top, DefiniteLength::from(zed::px(0.)));
/// assert_eq!(no_edges.right, DefiniteLength::from(zed::px(0.)));
/// assert_eq!(no_edges.bottom, DefiniteLength::from(zed::px(0.)));
/// assert_eq!(no_edges.left, DefiniteLength::from(zed::px(0.)));
/// ```
pub fn zero() -> Self {
Self {
@@ -1807,7 +1702,7 @@ impl Edges<DefiniteLength> {
/// # Examples
///
/// ```
/// # use gpui::{Edges, DefiniteLength, px, AbsoluteLength, Size};
/// # use zed::{Edges, DefiniteLength, px, AbsoluteLength, Size};
/// let edges = Edges {
/// top: DefiniteLength::Absolute(AbsoluteLength::Pixels(px(10.0))),
/// right: DefiniteLength::Fraction(0.5),
@@ -1849,7 +1744,7 @@ impl Edges<AbsoluteLength> {
/// # Examples
///
/// ```
/// # use gpui::Edges;
/// # use zed::Edges;
/// let no_edges = Edges::zero();
/// assert_eq!(no_edges.top, AbsoluteLength::Pixels(Pixels(0.0)));
/// assert_eq!(no_edges.right, AbsoluteLength::Pixels(Pixels(0.0)));
@@ -1881,7 +1776,7 @@ impl Edges<AbsoluteLength> {
/// # Examples
///
/// ```
/// # use gpui::{Edges, AbsoluteLength, Pixels, px};
/// # use zed::{Edges, AbsoluteLength, Pixels, px};
/// let edges = Edges {
/// top: AbsoluteLength::Pixels(px(10.0)),
/// right: AbsoluteLength::Rems(rems(1.0)),
@@ -1922,7 +1817,7 @@ impl Edges<Pixels> {
/// # Examples
///
/// ```
/// # use gpui::{Edges, Pixels};
/// # use zed::{Edges, Pixels};
/// let edges = Edges {
/// top: Pixels(10.0),
/// right: Pixels(20.0),
@@ -1972,64 +1867,6 @@ impl From<Pixels> for Edges<Pixels> {
}
}
/// Identifies a corner of a 2d box.
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum Corner {
/// The top left corner
TopLeft,
/// The top right corner
TopRight,
/// The bottom left corner
BottomLeft,
/// The bottom right corner
BottomRight,
}
impl Corner {
/// Returns the directly opposite corner.
///
/// # Examples
///
/// ```
/// # use zed::Corner;
/// assert_eq!(Corner::TopLeft.opposite_corner(), Corner::BottomRight);
/// ```
pub fn opposite_corner(self) -> Self {
match self {
Corner::TopLeft => Corner::BottomRight,
Corner::TopRight => Corner::BottomLeft,
Corner::BottomLeft => Corner::TopRight,
Corner::BottomRight => Corner::TopLeft,
}
}
/// Returns the corner across from this corner, moving along the specified axis.
///
/// # Examples
///
/// ```
/// # use zed::Corner;
/// let result = Corner::TopLeft.other_side_corner_along(Axis::Horizontal);
/// assert_eq!(result, Corner::TopRight);
/// ```
pub fn other_side_corner_along(self, axis: Axis) -> Self {
match axis {
Axis::Vertical => match self {
Corner::TopLeft => Corner::BottomLeft,
Corner::TopRight => Corner::BottomRight,
Corner::BottomLeft => Corner::TopLeft,
Corner::BottomRight => Corner::TopRight,
},
Axis::Horizontal => match self {
Corner::TopLeft => Corner::TopRight,
Corner::TopRight => Corner::TopLeft,
Corner::BottomLeft => Corner::BottomRight,
Corner::BottomRight => Corner::BottomLeft,
},
}
}
}
/// Represents the corners of a box in a 2D space, such as border radius.
///
/// Each field represents the size of the corner on one side of the box: `top_left`, `top_right`, `bottom_right`, and `bottom_left`.
@@ -2068,7 +1905,7 @@ where
/// # Examples
///
/// ```
/// # use gpui::Corners;
/// # use zed::Corners;
/// let uniform_corners = Corners::all(5.0);
/// assert_eq!(uniform_corners.top_left, 5.0);
/// assert_eq!(uniform_corners.top_right, 5.0);
@@ -2083,33 +1920,6 @@ where
bottom_left: value,
}
}
/// Returns the requested corner.
///
/// # Returns
///
/// A `Point<T>` representing the corner requested by the parameter.
///
/// # Examples
///
/// ```
/// # use zed::{Corner, Corners};
/// let corners = Corners {
/// top_left: 1,
/// top_right: 2,
/// bottom_left: 3,
/// bottom_right: 4
/// };
/// assert_eq!(corners.corner(Corner::BottomLeft), 3);
/// ```
pub fn corner(&self, corner: Corner) -> T {
match corner {
Corner::TopLeft => self.top_left.clone(),
Corner::TopRight => self.top_right.clone(),
Corner::BottomLeft => self.bottom_left.clone(),
Corner::BottomRight => self.bottom_right.clone(),
}
}
}
impl Corners<AbsoluteLength> {
@@ -2132,7 +1942,7 @@ impl Corners<AbsoluteLength> {
/// # Examples
///
/// ```
/// # use gpui::{Corners, AbsoluteLength, Pixels, Size};
/// # use zed::{Corners, AbsoluteLength, Pixels, Size};
/// let corners = Corners {
/// top_left: AbsoluteLength::Pixels(Pixels(15.0)),
/// top_right: AbsoluteLength::Rems(Rems(1.0)),
@@ -2176,7 +1986,7 @@ impl Corners<Pixels> {
/// # Examples
///
/// ```
/// # use gpui::{Corners, Pixels};
/// # use zed::{Corners, Pixels};
/// let corners = Corners {
/// top_left: Pixels(10.0),
/// top_right: Pixels(20.0),
@@ -2229,7 +2039,7 @@ impl<T: Clone + Default + Debug> Corners<T> {
/// # Examples
///
/// ```
/// # use gpui::{Corners, Pixels};
/// # use zed::{Corners, Pixels};
/// let corners = Corners {
/// top_left: Pixels(10.0),
/// top_right: Pixels(20.0),
@@ -2383,7 +2193,7 @@ impl From<Percentage> for Radians {
/// # Examples
///
/// ```
/// use gpui::Pixels;
/// use zed::Pixels;
///
/// // Define a length of 10 pixels
/// let length = Pixels(10.0);
@@ -2695,7 +2505,7 @@ impl DevicePixels {
/// # Examples
///
/// ```
/// # use gpui::DevicePixels;
/// # use zed::DevicePixels;
/// let pixels = DevicePixels(10); // 10 device pixels
/// let bytes_per_pixel = 4; // Assume each pixel is represented by 4 bytes (e.g., RGBA)
/// let total_bytes = pixels.to_bytes(bytes_per_pixel);
@@ -2907,7 +2717,7 @@ impl AbsoluteLength {
/// # Examples
///
/// ```
/// # use gpui::{AbsoluteLength, Pixels};
/// # use zed::{AbsoluteLength, Pixels};
/// let length_in_pixels = AbsoluteLength::Pixels(Pixels(42.0));
/// let length_in_rems = AbsoluteLength::Rems(Rems(2.0));
/// let rem_size = Pixels(16.0);
@@ -2960,7 +2770,7 @@ impl DefiniteLength {
/// # Examples
///
/// ```
/// # use gpui::{DefiniteLength, AbsoluteLength, Pixels, px, rems};
/// # use zed::{DefiniteLength, AbsoluteLength, Pixels, px, rems};
/// let length_in_pixels = DefiniteLength::Absolute(AbsoluteLength::Pixels(px(42.0)));
/// let length_in_rems = DefiniteLength::Absolute(AbsoluteLength::Rems(rems(2.0)));
/// let length_as_fraction = DefiniteLength::Fraction(0.5);

View File

@@ -509,7 +509,7 @@ impl Style {
} => None,
_ => {
let mut min = bounds.origin;
let mut max = bounds.bottom_right();
let mut max = bounds.lower_right();
if self
.border_color
@@ -530,12 +530,12 @@ impl Style {
// x visible, y hidden
(true, false) => Bounds::from_corners(
point(min.x, bounds.origin.y),
point(max.x, bounds.bottom_right().y),
point(max.x, bounds.lower_right().y),
),
// x hidden, y visible
(false, true) => Bounds::from_corners(
point(bounds.origin.x, min.y),
point(bounds.bottom_right().x, max.y),
point(bounds.lower_right().x, max.y),
),
// both hidden
(false, false) => Bounds::from_corners(min, max),
@@ -604,19 +604,19 @@ impl Style {
let top_bounds = Bounds::from_corners(
bounds.origin,
bounds.top_right() + point(Pixels::ZERO, max_border_width.max(max_corner_radius)),
bounds.upper_right() + point(Pixels::ZERO, max_border_width.max(max_corner_radius)),
);
let bottom_bounds = Bounds::from_corners(
bounds.bottom_left() - point(Pixels::ZERO, max_border_width.max(max_corner_radius)),
bounds.bottom_right(),
bounds.lower_left() - point(Pixels::ZERO, max_border_width.max(max_corner_radius)),
bounds.lower_right(),
);
let left_bounds = Bounds::from_corners(
top_bounds.bottom_left(),
top_bounds.lower_left(),
bottom_bounds.origin + point(max_border_width, Pixels::ZERO),
);
let right_bounds = Bounds::from_corners(
top_bounds.bottom_right() - point(max_border_width, Pixels::ZERO),
bottom_bounds.top_right(),
top_bounds.lower_right() - point(max_border_width, Pixels::ZERO),
bottom_bounds.upper_right(),
);
let mut background = self.border_color.unwrap_or_default();

View File

@@ -2281,7 +2281,9 @@ impl<'a> WindowContext<'a> {
let content_mask = self.content_mask();
let opacity = self.element_opacity();
for shadow in shadows {
let shadow_bounds = (bounds + shadow.offset).dilate(shadow.spread_radius);
let mut shadow_bounds = bounds;
shadow_bounds.origin += shadow.offset;
shadow_bounds.dilate(shadow.spread_radius);
self.window.next_frame.scene.insert_primitive(Shadow {
order: 0,
blur_radius: shadow.blur_radius.scale(scale_factor),

View File

@@ -208,7 +208,7 @@ impl IndexedDocsStore {
let candidates = items
.iter()
.enumerate()
.map(|(ix, item_path)| StringMatchCandidate::new(ix, &item_path))
.map(|(ix, item_path)| StringMatchCandidate::new(ix, item_path.clone()))
.collect::<Vec<_>>();
let matches = fuzzy::match_strings(

View File

@@ -19,7 +19,6 @@ pub struct InlineCompletion {
pub trait InlineCompletionProvider: 'static + Sized {
fn name() -> &'static str;
fn display_name() -> &'static str;
fn is_enabled(
&self,
buffer: &Model<Buffer>,
@@ -52,7 +51,6 @@ pub trait InlineCompletionProvider: 'static + Sized {
pub trait InlineCompletionProviderHandle {
fn name(&self) -> &'static str;
fn display_name(&self) -> &'static str;
fn is_enabled(
&self,
buffer: &Model<Buffer>,
@@ -91,10 +89,6 @@ where
T::name()
}
fn display_name(&self) -> &'static str {
T::display_name()
}
fn is_enabled(
&self,
buffer: &Model<Buffer>,

View File

@@ -4,7 +4,7 @@ use editor::{scroll::Autoscroll, Editor};
use feature_flags::{FeatureFlagAppExt, ZetaFeatureFlag};
use fs::Fs;
use gpui::{
actions, div, Action, AppContext, AsyncWindowContext, Corner, Entity, IntoElement,
actions, div, Action, AnchorCorner, AppContext, AsyncWindowContext, Entity, IntoElement,
ParentElement, Render, Subscription, View, ViewContext, WeakView, WindowContext,
};
use language::{
@@ -123,7 +123,7 @@ impl Render for InlineCompletionButton {
_ => this.update(cx, |this, cx| this.build_copilot_start_menu(cx)),
})
})
.anchor(Corner::BottomRight)
.anchor(AnchorCorner::BottomRight)
.trigger(
IconButton::new("copilot-icon", icon)
.tooltip(|cx| Tooltip::text("GitHub Copilot", cx)),
@@ -191,7 +191,7 @@ impl Render for InlineCompletionButton {
),
_ => None,
})
.anchor(Corner::BottomRight)
.anchor(AnchorCorner::BottomRight)
.trigger(
IconButton::new("supermaven-icon", icon)
.tooltip(move |cx| Tooltip::text(tooltip_text.clone(), cx)),

View File

@@ -1678,10 +1678,6 @@ impl CodeLabel {
pub fn text(&self) -> &str {
self.text.as_str()
}
pub fn filter_text(&self) -> &str {
&self.text[self.filter_range.clone()]
}
}
impl From<String> for CodeLabel {

View File

@@ -73,8 +73,8 @@ impl<T> Outline<T> {
.map(|range| &item.text[range.start..range.end])
.collect::<String>();
path_candidates.push(StringMatchCandidate::new(id, &path_text));
candidates.push(StringMatchCandidate::new(id, &candidate_text));
path_candidates.push(StringMatchCandidate::new(id, path_text.clone()));
candidates.push(StringMatchCandidate::new(id, candidate_text));
}
Self {

View File

@@ -107,7 +107,7 @@ impl<T: PopoverTrigger> RenderOnce for LanguageModelSelectorPopoverMenu<T> {
PopoverMenu::new("model-switcher")
.menu(move |_cx| Some(language_model_selector.clone()))
.trigger(self.trigger)
.attach(gpui::Corner::BottomLeft)
.attach(gpui::AnchorCorner::BottomLeft)
.when_some(self.handle.clone(), |menu, handle| menu.with_handle(handle))
}
}

View File

@@ -112,7 +112,7 @@ impl LanguageSelectorDelegate {
.then_some(name)
})
.enumerate()
.map(|(candidate_id, name)| StringMatchCandidate::new(candidate_id, &name))
.map(|(candidate_id, name)| StringMatchCandidate::new(candidate_id, name))
.collect::<Vec<_>>();
Self {

View File

@@ -3,7 +3,7 @@ use copilot::Copilot;
use editor::{actions::MoveToEnd, Editor, EditorEvent};
use futures::{channel::mpsc, StreamExt};
use gpui::{
actions, div, AppContext, Context, Corner, EventEmitter, FocusHandle, FocusableView,
actions, div, AnchorCorner, AppContext, Context, EventEmitter, FocusHandle, FocusableView,
IntoElement, Model, ModelContext, ParentElement, Render, Styled, Subscription, View,
ViewContext, VisualContext, WeakModel, WindowContext,
};
@@ -1158,7 +1158,7 @@ impl Render for LspLogToolbarItemView {
.collect();
let log_toolbar_view = cx.view().clone();
let lsp_menu = PopoverMenu::new("LspLogView")
.anchor(Corner::TopLeft)
.anchor(AnchorCorner::TopLeft)
.trigger(Button::new(
"language_server_menu_header",
current_server
@@ -1214,7 +1214,7 @@ impl Render for LspLogToolbarItemView {
let rpc_trace_enabled = server.rpc_trace_enabled;
let log_view = log_view.clone();
PopoverMenu::new("LspViewSelector")
.anchor(Corner::TopLeft)
.anchor(AnchorCorner::TopLeft)
.trigger(Button::new(
"language_server_menu_header",
server.selected_entry.label(),
@@ -1301,7 +1301,7 @@ impl Render for LspLogToolbarItemView {
let log_view = log_view.clone();
div().child(
PopoverMenu::new("lsp-trace-level-menu")
.anchor(Corner::TopLeft)
.anchor(AnchorCorner::TopLeft)
.trigger(Button::new(
"language_server_trace_level_selector",
"Trace level",
@@ -1359,7 +1359,7 @@ impl Render for LspLogToolbarItemView {
let log_view = log_view.clone();
div().child(
PopoverMenu::new("lsp-log-level-menu")
.anchor(Corner::TopLeft)
.anchor(AnchorCorner::TopLeft)
.trigger(Button::new(
"language_server_log_level_selector",
"Log level",

View File

@@ -375,7 +375,7 @@ impl ContextProvider for PythonContextProvider {
args: vec![
"-m".to_owned(),
"unittest".to_owned(),
PYTHON_TEST_TARGET_TASK_VARIABLE.template_value_with_whitespace(),
PYTHON_TEST_TARGET_TASK_VARIABLE.template_value_with_whitespace()
],
tags: vec![
"python-unittest-class".to_owned(),

View File

@@ -443,7 +443,7 @@ impl LanguageServer {
let stderr_captures = stderr_capture.clone();
cx.spawn(|_| Self::handle_stderr(stderr, io_handlers, stderr_captures).log_err())
})
.unwrap_or_else(|| Task::ready(None));
.unwrap_or_else(|| Task::Ready(Some(None)));
let input_task = cx.spawn(|_| async move {
let (stdout, stderr) = futures::join!(stdout_input_task, stderr_input_task);
stdout.or(stderr)

View File

@@ -19,6 +19,5 @@ actions!(
SelectNext,
SelectFirst,
SelectLast,
Restart
]
);

View File

@@ -195,7 +195,6 @@ pub struct ExcerptInfo {
pub buffer: BufferSnapshot,
pub buffer_id: BufferId,
pub range: ExcerptRange<text::Anchor>,
pub text_summary: TextSummary,
}
impl std::fmt::Debug for ExcerptInfo {
@@ -1547,33 +1546,6 @@ impl MultiBuffer {
excerpts
}
pub fn excerpt_ranges_for_buffer(
&self,
buffer_id: BufferId,
cx: &AppContext,
) -> Vec<Range<Point>> {
let snapshot = self.read(cx);
let buffers = self.buffers.borrow();
let mut cursor = snapshot.excerpts.cursor::<(Option<&Locator>, Point)>(&());
buffers
.get(&buffer_id)
.into_iter()
.flat_map(|state| &state.excerpts)
.filter_map(move |locator| {
cursor.seek_forward(&Some(locator), Bias::Left, &());
cursor.item().and_then(|excerpt| {
if excerpt.locator == *locator {
let excerpt_start = cursor.start().1;
let excerpt_end = excerpt_start + excerpt.text_summary.lines;
Some(excerpt_start..excerpt_end)
} else {
None
}
})
})
.collect()
}
pub fn excerpt_buffer_ids(&self) -> Vec<BufferId> {
self.snapshot
.borrow()
@@ -3587,7 +3559,6 @@ impl MultiBufferSnapshot {
buffer: excerpt.buffer.clone(),
buffer_id: excerpt.buffer_id,
range: excerpt.range.clone(),
text_summary: excerpt.text_summary.clone(),
});
if next.is_none() {
@@ -3603,7 +3574,6 @@ impl MultiBufferSnapshot {
buffer: prev_excerpt.buffer.clone(),
buffer_id: prev_excerpt.buffer_id,
range: prev_excerpt.range.clone(),
text_summary: prev_excerpt.text_summary.clone(),
});
let row = MultiBufferRow(cursor.start().1.row);

File diff suppressed because it is too large Load Diff

View File

@@ -1211,7 +1211,7 @@ impl BufferStore {
return Task::ready(Err(anyhow!("buffer has no file")));
};
match file.worktree.read(cx) {
match file.worktree.clone().read(cx) {
Worktree::Local(worktree) => {
let Some(repo) = worktree.local_git_repo(file.path()) else {
return Task::ready(Err(anyhow!("no repository for buffer found")));

View File

@@ -52,7 +52,7 @@ use lsp::{
WillRenameFiles, WorkDoneProgressCancelParams, WorkspaceFolder,
};
use node_runtime::read_package_installed_version;
use parking_lot::Mutex;
use parking_lot::{Mutex, RwLock};
use postage::watch;
use rand::prelude::*;
@@ -65,14 +65,12 @@ use smol::channel::Sender;
use snippet::Snippet;
use std::{
any::Any,
cell::RefCell,
cmp::Ordering,
convert::TryInto,
ffi::OsStr,
iter, mem,
ops::{ControlFlow, Range},
path::{self, Path, PathBuf},
rc::Rc,
str,
sync::Arc,
time::{Duration, Instant},
@@ -4139,7 +4137,7 @@ impl LspStore {
&self,
buffer: Model<Buffer>,
completion_indices: Vec<usize>,
completions: Rc<RefCell<Box<[Completion]>>>,
completions: Arc<RwLock<Box<[Completion]>>>,
cx: &mut ModelContext<Self>,
) -> Task<Result<bool>> {
let client = self.upstream_client();
@@ -4153,8 +4151,8 @@ impl LspStore {
if let Some((client, project_id)) = client {
for completion_index in completion_indices {
let (server_id, completion) = {
let completions = completions.borrow_mut();
let completion = &completions[completion_index];
let completions_guard = completions.read();
let completion = &completions_guard[completion_index];
did_resolve = true;
let server_id = completion.server_id;
let completion = completion.lsp_completion.clone();
@@ -4177,8 +4175,8 @@ impl LspStore {
} else {
for completion_index in completion_indices {
let (server_id, completion) = {
let completions = completions.borrow_mut();
let completion = &completions[completion_index];
let completions_guard = completions.read();
let completion = &completions_guard[completion_index];
let server_id = completion.server_id;
let completion = completion.lsp_completion.clone();
@@ -4220,7 +4218,7 @@ impl LspStore {
server: Arc<lsp::LanguageServer>,
adapter: Arc<CachedLspAdapter>,
snapshot: &BufferSnapshot,
completions: Rc<RefCell<Box<[Completion]>>>,
completions: Arc<RwLock<Box<[Completion]>>>,
completion_index: usize,
completion: lsp::CompletionItem,
language_registry: Arc<LanguageRegistry>,
@@ -4248,11 +4246,11 @@ impl LspStore {
)
.await;
let mut completions = completions.borrow_mut();
let mut completions = completions.write();
let completion = &mut completions[completion_index];
completion.documentation = Some(documentation);
} else {
let mut completions = completions.borrow_mut();
let mut completions = completions.write();
let completion = &mut completions[completion_index];
completion.documentation = Some(Documentation::Undocumented);
}
@@ -4267,7 +4265,7 @@ impl LspStore {
if let Some((old_range, mut new_text)) = edit {
LineEnding::normalize(&mut new_text);
let mut completions = completions.borrow_mut();
let mut completions = completions.write();
let completion = &mut completions[completion_index];
completion.new_text = new_text;
@@ -4276,7 +4274,7 @@ impl LspStore {
}
if completion_item.insert_text_format == Some(InsertTextFormat::SNIPPET) {
// vtsls might change the type of completion after resolution.
let mut completions = completions.borrow_mut();
let mut completions = completions.write();
let completion = &mut completions[completion_index];
if completion_item.insert_text_format != completion.lsp_completion.insert_text_format {
completion.lsp_completion.insert_text_format = completion_item.insert_text_format;
@@ -4302,21 +4300,10 @@ impl LspStore {
)
});
let mut completions = completions.borrow_mut();
let mut completions = completions.write();
let completion = &mut completions[completion_index];
completion.lsp_completion = completion_item;
if completion.label.filter_text() == new_label.filter_text() {
completion.label = new_label;
} else {
log::error!(
"Resolved completion changed display label from {} to {}. \
Refusing to apply this because it changes the fuzzy match text from {} to {}",
completion.label.text(),
new_label.text(),
completion.label.filter_text(),
new_label.filter_text()
);
}
completion.label = new_label;
}
#[allow(clippy::too_many_arguments)]
@@ -4324,7 +4311,7 @@ impl LspStore {
project_id: u64,
server_id: LanguageServerId,
buffer_id: BufferId,
completions: Rc<RefCell<Box<[Completion]>>>,
completions: Arc<RwLock<Box<[Completion]>>>,
completion_index: usize,
completion: lsp::CompletionItem,
client: AnyProtoClient,
@@ -4362,7 +4349,7 @@ impl LspStore {
Documentation::MultiLinePlainText(response.documentation)
};
let mut completions = completions.borrow_mut();
let mut completions = completions.write();
let completion = &mut completions[completion_index];
completion.documentation = Some(documentation);
completion.lsp_completion = lsp_completion;

View File

@@ -57,7 +57,7 @@ use lsp::{
};
use lsp_command::*;
use node_runtime::NodeRuntime;
use parking_lot::Mutex;
use parking_lot::{Mutex, RwLock};
pub use prettier_store::PrettierStore;
use project_settings::{ProjectSettings, SettingsObserver, SettingsObserverEvent};
use remote::{SshConnectionOptions, SshRemoteClient};
@@ -73,10 +73,8 @@ use snippet::Snippet;
use snippet_provider::SnippetProvider;
use std::{
borrow::Cow,
cell::RefCell,
ops::Range,
path::{Component, Path, PathBuf},
rc::Rc,
str,
sync::Arc,
time::Duration,
@@ -2542,7 +2540,7 @@ impl Project {
.read(cx)
.list_toolchains(worktree_id, language_name, cx)
})
.ok()?
.unwrap_or(Task::Ready(None))
.await
})
} else {
@@ -2874,7 +2872,7 @@ impl Project {
&self,
buffer: Model<Buffer>,
completion_indices: Vec<usize>,
completions: Rc<RefCell<Box<[Completion]>>>,
completions: Arc<RwLock<Box<[Completion]>>>,
cx: &mut ModelContext<Self>,
) -> Task<Result<bool>> {
self.lsp_store.update(cx, |lsp_store, cx| {

View File

@@ -81,8 +81,7 @@ impl Inventory {
}
/// Pulls its task sources relevant to the worktree and the language given,
/// returns all task templates with their source kinds, worktree tasks first, language tasks second
/// and global tasks last. No specific order inside source kinds groups.
/// returns all task templates with their source kinds, in no specific order.
pub fn list_tasks(
&self,
file: Option<Arc<dyn File>>,
@@ -93,15 +92,13 @@ impl Inventory {
let task_source_kind = language.as_ref().map(|language| TaskSourceKind::Language {
name: language.name().0,
});
let global_tasks = self.global_templates_from_settings();
let language_tasks = language
.and_then(|language| language.context_provider()?.associated_tasks(file, cx))
.into_iter()
.flat_map(|tasks| tasks.0.into_iter())
.flat_map(|task| Some((task_source_kind.clone()?, task)))
.chain(global_tasks);
.flat_map(|task| Some((task_source_kind.clone()?, task)));
self.worktree_templates_from_settings(worktree)
self.templates_from_settings(worktree)
.chain(language_tasks)
.collect()
}
@@ -168,18 +165,14 @@ impl Inventory {
.collect::<Vec<_>>();
let not_used_score = post_inc(&mut lru_score);
let global_tasks = self.global_templates_from_settings();
let language_tasks = language
.and_then(|language| language.context_provider()?.associated_tasks(file, cx))
.into_iter()
.flat_map(|tasks| tasks.0.into_iter())
.flat_map(|task| Some((task_source_kind.clone()?, task)))
.chain(global_tasks);
let worktree_tasks = self
.worktree_templates_from_settings(worktree)
.chain(language_tasks);
let new_resolved_tasks = worktree_tasks
.flat_map(|task| Some((task_source_kind.clone()?, task)));
let new_resolved_tasks = self
.templates_from_settings(worktree)
.chain(language_tasks)
.filter_map(|(kind, task)| {
let id_base = kind.to_id_base();
Some((
@@ -242,8 +235,9 @@ impl Inventory {
self.last_scheduled_tasks.retain(|(_, task)| &task.id != id);
}
fn global_templates_from_settings(
fn templates_from_settings(
&self,
worktree: Option<WorktreeId>,
) -> impl '_ + Iterator<Item = (TaskSourceKind, TaskTemplate)> {
self.templates_from_settings
.global
@@ -258,34 +252,28 @@ impl Inventory {
template,
)
})
}
fn worktree_templates_from_settings(
&self,
worktree: Option<WorktreeId>,
) -> impl '_ + Iterator<Item = (TaskSourceKind, TaskTemplate)> {
worktree.into_iter().flat_map(|worktree| {
self.templates_from_settings
.worktree
.get(&worktree)
.into_iter()
.flatten()
.flat_map(|(directory, templates)| {
templates.iter().map(move |template| (directory, template))
})
.map(move |(directory, template)| {
(
TaskSourceKind::Worktree {
id: worktree,
directory_in_worktree: directory.to_path_buf(),
id_base: Cow::Owned(format!(
"local worktree tasks from directory {directory:?}"
)),
},
template.clone(),
)
})
})
.chain(worktree.into_iter().flat_map(|worktree| {
self.templates_from_settings
.worktree
.get(&worktree)
.into_iter()
.flatten()
.flat_map(|(directory, templates)| {
templates.iter().map(move |template| (directory, template))
})
.map(move |(directory, template)| {
(
TaskSourceKind::Worktree {
id: worktree,
directory_in_worktree: directory.to_path_buf(),
id_base: Cow::Owned(format!(
"local worktree tasks from directory {directory:?}"
)),
},
template.clone(),
)
})
}))
}
/// Updates in-memory task metadata from the JSON string given.
@@ -378,7 +366,7 @@ mod test_inventory {
use crate::Inventory;
use super::TaskSourceKind;
use super::{task_source_kind_preference, TaskSourceKind};
pub(super) fn task_template_names(
inventory: &Model<Inventory>,
@@ -420,18 +408,15 @@ mod test_inventory {
worktree: Option<WorktreeId>,
cx: &mut TestAppContext,
) -> Vec<(TaskSourceKind, String)> {
inventory.update(cx, |inventory, cx| {
let task_context = &TaskContext::default();
inventory
.list_tasks(None, None, worktree, cx)
.into_iter()
.filter_map(|(source_kind, task)| {
let id_base = source_kind.to_id_base();
Some((source_kind, task.resolve_task(&id_base, task_context)?))
})
.map(|(source_kind, resolved_task)| (source_kind, resolved_task.resolved_label))
.collect()
})
let (used, current) = inventory.update(cx, |inventory, cx| {
inventory.used_and_current_resolved_tasks(worktree, None, &TaskContext::default(), cx)
});
let mut all = used;
all.extend(current);
all.into_iter()
.map(|(source_kind, task)| (source_kind, task.resolved_label))
.sorted_by_key(|(kind, label)| (task_source_kind_preference(kind), label.clone()))
.collect()
}
}
@@ -804,30 +789,6 @@ mod tests {
.unwrap();
});
assert_eq!(
list_tasks_sorted_by_last_used(&inventory, None, cx).await,
worktree_independent_tasks,
"Without a worktree, only worktree-independent tasks should be listed"
);
assert_eq!(
list_tasks_sorted_by_last_used(&inventory, Some(worktree_1), cx).await,
worktree_1_tasks
.iter()
.chain(worktree_independent_tasks.iter())
.cloned()
.sorted_by_key(|(kind, label)| (task_source_kind_preference(kind), label.clone()))
.collect::<Vec<_>>(),
);
assert_eq!(
list_tasks_sorted_by_last_used(&inventory, Some(worktree_2), cx).await,
worktree_2_tasks
.iter()
.chain(worktree_independent_tasks.iter())
.cloned()
.sorted_by_key(|(kind, label)| (task_source_kind_preference(kind), label.clone()))
.collect::<Vec<_>>(),
);
assert_eq!(
list_tasks(&inventory, None, cx).await,
worktree_independent_tasks,
@@ -839,6 +800,7 @@ mod tests {
.iter()
.chain(worktree_independent_tasks.iter())
.cloned()
.sorted_by_key(|(kind, label)| (task_source_kind_preference(kind), label.clone()))
.collect::<Vec<_>>(),
);
assert_eq!(
@@ -847,6 +809,7 @@ mod tests {
.iter()
.chain(worktree_independent_tasks.iter())
.cloned()
.sorted_by_key(|(kind, label)| (task_source_kind_preference(kind), label.clone()))
.collect::<Vec<_>>(),
);
}
@@ -858,7 +821,7 @@ mod tests {
TaskStore::init(None);
}
async fn resolved_task_names(
pub(super) async fn resolved_task_names(
inventory: &Model<Inventory>,
worktree: Option<WorktreeId>,
cx: &mut TestAppContext,
@@ -886,20 +849,4 @@ mod tests {
))
.unwrap()
}
async fn list_tasks_sorted_by_last_used(
inventory: &Model<Inventory>,
worktree: Option<WorktreeId>,
cx: &mut TestAppContext,
) -> Vec<(TaskSourceKind, String)> {
let (used, current) = inventory.update(cx, |inventory, cx| {
inventory.used_and_current_resolved_tasks(worktree, None, &TaskContext::default(), cx)
});
let mut all = used;
all.extend(current);
all.into_iter()
.map(|(source_kind, task)| (source_kind, task.resolved_label))
.sorted_by_key(|(kind, label)| (task_source_kind_preference(kind), label.clone()))
.collect()
}
}

View File

@@ -331,7 +331,7 @@ fn local_task_context_for_location(
let worktree_id = location.buffer.read(cx).file().map(|f| f.worktree_id(cx));
let worktree_abs_path = worktree_id
.and_then(|worktree_id| worktree_store.read(cx).worktree_for_id(worktree_id, cx))
.and_then(|worktree| worktree.read(cx).root_dir());
.map(|worktree| worktree.read(cx).abs_path());
cx.spawn(|mut cx| async move {
let worktree_abs_path = worktree_abs_path.clone();

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