Make BufferSearch less wide (#10459)

This also adds some "responsiveness" so that UI elements are hidden
before everything has to be occluded

Release Notes:

- Improved search UI. It now works in narrower panes, and avoids
scrolling the editor on open.

<img width="899" alt="Screenshot 2024-04-11 at 21 33 17"
src="https://github.com/zed-industries/zed/assets/94272/44b95d4f-08d6-4c40-a175-0e594402ca01">
<img width="508" alt="Screenshot 2024-04-11 at 21 33 45"
src="https://github.com/zed-industries/zed/assets/94272/baf4638d-427b-43e6-ad67-13d43f0f18a2">
<img width="361" alt="Screenshot 2024-04-11 at 21 34 00"
src="https://github.com/zed-industries/zed/assets/94272/ff60b561-2f77-49c0-9df7-e26227fe9225">
<img width="348" alt="Screenshot 2024-04-11 at 21 37 03"
src="https://github.com/zed-industries/zed/assets/94272/a2a700a2-ce99-41bd-bf47-9b14d7082b0e">
This commit is contained in:
Conrad Irwin 2024-04-11 23:07:29 -06:00 committed by GitHub
parent f2d61f3ea5
commit 08786fa7bf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 326 additions and 482 deletions

View file

@ -2,8 +2,6 @@
Thanks for your interest in contributing to Zed, the collaborative platform that is also a code editor!
We want to avoid anyone spending time on a pull request that may not be accepted, so we suggest you discuss your ideas with the team and community before starting on major changes. Bug fixes, however, are almost always welcome.
All activity in Zed forums is subject to our [Code of Conduct](https://zed.dev/docs/code-of-conduct). Additionally, contributors must sign our [Contributor License Agreement](https://zed.dev/cla) before their contributions can be merged.
## Contribution ideas

View file

@ -1,6 +1,6 @@
# Zed
[![CI](https://github.com/zed-industries/zed/actions/workflows/ci.yml/badge.svg)](https://github.com/zed-industries/zed/actions/workflows/ci.yml)
[![CI](https://github.com/zed-industries/zed/actions/workflows/ci.yml/badge.svg)](https://github.com/zed-industries/ze34actions/workflows/ci.yml)
Welcome to Zed, a high-performance, multiplayer code editor from the creators of [Atom](https://github.com/atom/atom) and [Tree-sitter](https://github.com/tree-sitter/tree-sitter).

4
assets/icons/regex.svg Normal file
View file

@ -0,0 +1,4 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="4" cy="11" r="1" fill="#787D87"/>
<path d="M9 2.5V5M9 5V7.5M9 5H11.5M9 5H6.5M9 5L10.6667 3.33333M9 5L7.33333 6.6667M9 5L10.6667 6.6667M9 5L7.33333 3.33333" stroke="#787D87" stroke-width="1.25" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 333 B

View file

@ -1,5 +1,5 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.5 7V9.5M9.5 12V9.5M12 9.5H9.5M7 9.5H9.5M9.5 9.5L11.1667 7.83333M9.5 9.5L7.83333 11.1667M9.5 9.5L11.1667 11.1667M9.5 9.5L7.83333 7.83333" stroke="#11181C" stroke-width="1.25" stroke-linecap="round"/>
<path d="M2.19366 3.84943C2.19188 4.26418 2.32864 4.59864 2.60673 4.84707C2.88052 5.09166 3.25136 5.26933 3.71609 5.3824C3.71616 5.38242 3.71623 5.38243 3.7163 5.38245L4.30919 5.53134L4.30919 5.53134L4.30965 5.53145C4.50649 5.57891 4.67124 5.63133 4.80447 5.68843L4.80469 5.68852C4.93838 5.74508 5.03564 5.81206 5.10001 5.8877L5.10001 5.8877L5.10041 5.88816C5.16432 5.96142 5.19716 6.05222 5.19716 6.16389C5.19716 6.28412 5.1609 6.38933 5.0882 6.48141C5.01496 6.57418 4.91031 6.64838 4.77141 6.70259L4.77121 6.70266C4.63472 6.75659 4.47185 6.7843 4.28146 6.7843C4.08801 6.7843 3.91607 6.75496 3.76491 6.69726C3.61654 6.6382 3.49924 6.55209 3.41132 6.43942C3.3502 6.35821 3.30747 6.26204 3.28375 6.14992C3.26238 6.04888 3.1772 5.96225 3.06518 5.96225H2.26366C2.14682 5.96225 2.04842 6.05919 2.0592 6.18012C2.08842 6.50802 2.1826 6.79102 2.34331 7.02735L2.34352 7.02767C2.53217 7.30057 2.79377 7.50587 3.12633 7.64399L3.12642 7.64402C3.46009 7.78185 3.84993 7.85 4.29476 7.85C4.74293 7.85 5.12859 7.7828 5.45023 7.64651L5.45036 7.64646C5.77328 7.50857 6.02259 7.31417 6.19551 7.06217C6.37037 6.80817 6.4579 6.50901 6.45972 6.16682L6.45972 6.16616C6.4579 5.9333 6.41513 5.72482 6.33012 5.54178C6.2474 5.35987 6.13061 5.20175 5.98007 5.06773C5.83038 4.93448 5.65389 4.82273 5.4511 4.7322C5.24919 4.64206 5.02795 4.57016 4.78757 4.51632L4.29841 4.39935L4.29841 4.39934L4.29771 4.39919C4.18081 4.37301 4.07116 4.34168 3.9687 4.30523C3.86715 4.26734 3.77847 4.22375 3.70232 4.17471C3.62796 4.12508 3.57037 4.06717 3.52849 4.00124C3.49012 3.93815 3.47157 3.86312 3.47481 3.77407L3.47484 3.77407V3.77225C3.47484 3.66563 3.50527 3.57146 3.56612 3.48808C3.6287 3.40475 3.71977 3.33801 3.84235 3.28931L3.84235 3.28932L3.84289 3.28909C3.96465 3.23906 4.1165 3.21304 4.30008 3.21304C4.57006 3.21304 4.77746 3.27105 4.92754 3.38154C5.04235 3.46608 5.11838 3.57594 5.15673 3.71259C5.18352 3.80802 5.26636 3.89142 5.37611 3.89142H6.17259C6.28852 3.89142 6.38806 3.7953 6.37515 3.67382C6.34686 3.4077 6.26051 3.16831 6.1158 2.95658C5.94159 2.70169 5.6982 2.50368 5.38762 2.36201L5.36687 2.4075M2.19366 3.84943C2.19187 3.51004 2.28242 3.21139 2.46644 2.9556L2.46658 2.9554C2.65148 2.70093 2.90447 2.50326 3.22368 2.36179C3.54316 2.2202 3.90494 2.15 4.30807 2.15C4.71809 2.15 5.07841 2.22014 5.38773 2.36206L5.36687 2.4075M2.19366 3.84943C2.19366 3.84951 2.19366 3.84959 2.19366 3.84967L2.24366 3.8494L2.19366 3.84918C2.19366 3.84926 2.19366 3.84935 2.19366 3.84943ZM5.36687 2.4075C5.06537 2.26917 4.71244 2.2 4.30807 2.2C3.91079 2.2 3.55608 2.26917 3.24394 2.4075C2.93179 2.54584 2.68616 2.73827 2.50703 2.9848L3.82389 3.24285L3.82389 3.24285C3.95336 3.18964 4.11209 3.16304 4.30008 3.16304C4.57676 3.16304 4.79579 3.22245 4.95718 3.34128C5.08094 3.43239 5.1635 3.55166 5.20487 3.69908C5.2271 3.77827 5.29386 3.84142 5.37611 3.84142H6.17259C6.26198 3.84142 6.33488 3.76799 6.32543 3.6791C6.29797 3.4208 6.21433 3.18936 6.07452 2.9848C5.90603 2.73827 5.67015 2.54584 5.36687 2.4075ZM4.78958 6.74917C4.64593 6.80592 4.47655 6.8343 4.28146 6.8343C4.08283 6.8343 3.90458 6.80415 3.74674 6.74384C3.59067 6.68177 3.46563 6.59043 3.37163 6.46983L4.78958 6.74917ZM4.78958 6.74917C4.93502 6.69241 5.04764 6.61349 5.12745 6.5124M4.78958 6.74917L5.12745 6.5124M5.12745 6.5124C5.20726 6.4113 5.24716 6.29514 5.24716 6.16389M5.12745 6.5124L5.24716 6.16389M5.24716 6.16389C5.24716 6.04152 5.2108 5.93865 5.13809 5.85529L5.24716 6.16389Z" fill="#687076" stroke="#687076" stroke-width="0.1"/>
<path d="M9.5 7V9.5M9.5 9.5V12M9.5 9.5H12M9.5 9.5H7M9.5 9.5L11.1667 7.83333M9.5 9.5L7.83333 11.1667M9.5 9.5L11.1667 11.1667M9.5 9.5L7.83333 7.83333" stroke="#687076" stroke-width="1.25" stroke-linecap="round"/>
<path d="M2.19368 3.84945C2.1919 4.2642 2.32866 4.59866 2.60675 4.84709C2.88054 5.09168 3.25138 5.26935 3.71611 5.38242L4.30921 5.53136C4.50605 5.57882 4.67126 5.63135 4.80449 5.68845C4.93818 5.74501 5.03566 5.81208 5.10003 5.88772C5.16394 5.96098 5.19718 6.05224 5.19718 6.16391C5.19718 6.28414 5.16092 6.38935 5.08822 6.48143C5.01498 6.5742 4.91033 6.6484 4.77143 6.70261C4.63494 6.75654 4.47187 6.78432 4.28148 6.78432C4.08803 6.78432 3.91609 6.75498 3.76493 6.69728C3.61656 6.63822 3.49926 6.55211 3.41134 6.43944C3.35022 6.35823 3.30749 6.26206 3.28377 6.14994C3.2624 6.0489 3.17722 5.96227 3.0652 5.96227H2.26368C2.14684 5.96227 2.04844 6.05921 2.05922 6.18014C2.08844 6.50804 2.18262 6.79104 2.34333 7.02737C2.53198 7.30027 2.79379 7.50589 3.12635 7.64401C3.46002 7.78184 3.84995 7.85002 4.29478 7.85002C4.74295 7.85002 5.12861 7.78282 5.45025 7.64653C5.77317 7.50864 6.02261 7.31419 6.19553 7.06219C6.37039 6.80819 6.45792 6.50903 6.45974 6.16684C6.45792 5.93398 6.41515 5.72484 6.33014 5.5418C6.24742 5.35989 6.13063 5.20177 5.98009 5.06775C5.8304 4.9345 5.65391 4.82275 5.45112 4.73222C5.24921 4.64208 5.02797 4.57018 4.78759 4.51634L4.29843 4.39937C4.18153 4.37319 4.07118 4.3417 3.96872 4.30525C3.86717 4.26736 3.77849 4.22377 3.70234 4.17473C3.62798 4.1251 3.57039 4.06719 3.52851 4.00126C3.49014 3.93817 3.47159 3.86314 3.47483 3.77409L3.47486 3.77227C3.47486 3.66565 3.50529 3.57148 3.56614 3.4881C3.62872 3.40477 3.71979 3.33803 3.84237 3.28933C3.96413 3.2393 4.11652 3.21306 4.3001 3.21306C4.57008 3.21306 4.77748 3.27107 4.92756 3.38156C5.04237 3.4661 5.1184 3.57596 5.15675 3.71261C5.18354 3.80804 5.26638 3.89144 5.37613 3.89144H6.17261C6.28854 3.89144 6.38808 3.79532 6.37517 3.67384C6.34688 3.40772 6.26053 3.16833 6.11582 2.9566C5.94161 2.70171 5.69822 2.5037 5.38764 2.36203L5.36689 2.40752M2.19368 3.84945C2.19189 3.51006 2.28244 3.21141 2.46646 2.95562C2.65136 2.70115 2.90449 2.50328 3.2237 2.36181C3.54318 2.22022 3.90496 2.15002 4.30809 2.15002C4.71811 2.15002 5.07832 2.22011 5.38764 2.36203L5.36689 2.40752M4.7896 6.74919C4.93504 6.69243 5.04766 6.61351 5.12747 6.51242ZM4.7896 6.74919L5.12747 6.51242ZM5.12747 6.51242C5.20728 6.41132 5.24718 6.29516 5.24718 6.16391ZM5.12747 6.51242L5.24718 6.16391ZM5.24718 6.16391C5.24718 6.04154 5.21082 5.93867 5.13811 5.85531L5.24718 6.16391Z" fill="#687076"/>
<path d="M2.19368 3.84945C2.1919 4.2642 2.32866 4.59866 2.60675 4.84709C2.88054 5.09168 3.25138 5.26935 3.71611 5.38242L4.30921 5.53136C4.50605 5.57882 4.67126 5.63135 4.80449 5.68845C4.93818 5.74501 5.03566 5.81208 5.10003 5.88772C5.16394 5.96098 5.19718 6.05224 5.19718 6.16391C5.19718 6.28414 5.16092 6.38935 5.08822 6.48143C5.01498 6.5742 4.91033 6.6484 4.77143 6.70261C4.63494 6.75654 4.47187 6.78432 4.28148 6.78432C4.08803 6.78432 3.91609 6.75498 3.76493 6.69728C3.61656 6.63822 3.49926 6.55211 3.41134 6.43944C3.35022 6.35823 3.30749 6.26206 3.28377 6.14994C3.2624 6.0489 3.17722 5.96227 3.0652 5.96227H2.26368C2.14684 5.96227 2.04844 6.05921 2.05922 6.18014C2.08844 6.50804 2.18262 6.79104 2.34333 7.02737C2.53198 7.30027 2.79379 7.50589 3.12635 7.64401C3.46002 7.78184 3.84995 7.85002 4.29478 7.85002C4.74295 7.85002 5.12861 7.78282 5.45025 7.64653C5.77317 7.50864 6.02261 7.31419 6.19553 7.06219C6.37039 6.80819 6.45792 6.50903 6.45974 6.16684C6.45792 5.93398 6.41515 5.72484 6.33014 5.5418C6.24742 5.35989 6.13063 5.20177 5.98009 5.06775C5.8304 4.9345 5.65391 4.82275 5.45112 4.73222C5.24921 4.64208 5.02797 4.57018 4.78759 4.51634L4.29843 4.39937C4.18153 4.37319 4.07118 4.3417 3.96872 4.30525C3.86717 4.26736 3.77849 4.22377 3.70234 4.17473C3.62798 4.1251 3.57039 4.06719 3.52851 4.00126C3.49014 3.93817 3.47159 3.86314 3.47483 3.77409L3.47486 3.77227C3.47486 3.66565 3.50529 3.57148 3.56614 3.4881C3.62872 3.40477 3.71979 3.33803 3.84237 3.28933C3.96413 3.2393 4.11652 3.21306 4.3001 3.21306C4.57008 3.21306 4.77748 3.27107 4.92756 3.38156C5.04237 3.4661 5.1184 3.57596 5.15675 3.71261C5.18354 3.80804 5.26638 3.89144 5.37613 3.89144H6.17261C6.28854 3.89144 6.38808 3.79532 6.37517 3.67384C6.34688 3.40772 6.26053 3.16833 6.11582 2.9566C5.94161 2.70171 5.69822 2.5037 5.38764 2.36203M2.19368 3.84945C2.19189 3.51006 2.28244 3.21141 2.46646 2.95562C2.65136 2.70115 2.90449 2.50328 3.2237 2.36181C3.54318 2.22022 3.90496 2.15002 4.30809 2.15002C4.71811 2.15002 5.07832 2.22011 5.38764 2.36203M2.19368 3.84945L2.24368 3.84942M5.38764 2.36203L5.36689 2.40752M5.36689 2.40752C5.06539 2.26919 4.71246 2.20002 4.30809 2.20002C3.91081 2.20002 3.5561 2.26919 3.24396 2.40752C2.93181 2.54586 2.68618 2.73829 2.50705 2.98482M5.36689 2.40752C5.67017 2.54586 5.90605 2.73829 6.07454 2.98482C6.21435 3.18938 6.29799 3.42082 6.32545 3.67912C6.3349 3.76801 6.262 3.84144 6.17261 3.84144H5.37613C5.29388 3.84144 5.22712 3.77829 5.20489 3.6991C5.16352 3.55168 5.08096 3.43241 4.9572 3.3413C4.79581 3.22247 4.57678 3.16306 4.3001 3.16306M4.7896 6.74919C4.64595 6.80594 4.47657 6.83432 4.28148 6.83432C4.08285 6.83432 3.9046 6.80417 3.74676 6.74386C3.59069 6.68179 3.46565 6.59045 3.37165 6.46985M4.7896 6.74919C4.93504 6.69243 5.04766 6.61351 5.12747 6.51242M4.7896 6.74919L5.12747 6.51242M5.12747 6.51242C5.20728 6.41132 5.24718 6.29516 5.24718 6.16391M5.12747 6.51242L5.24718 6.16391M5.24718 6.16391C5.24718 6.04154 5.21082 5.93867 5.13811 5.85531L5.24718 6.16391Z" stroke="#687076" stroke-width="0.1"/>
</svg>

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

Before After
Before After

View file

@ -212,7 +212,6 @@
"enter": "search::SelectNextMatch",
"shift-enter": "search::SelectPrevMatch",
"alt-enter": "search::SelectAllMatches",
"alt-tab": "search::CycleMode",
"ctrl-f": "search::FocusSearch",
"ctrl-h": "search::ToggleReplace"
}
@ -235,11 +234,10 @@
"context": "ProjectSearchBar",
"bindings": {
"escape": "project_search::ToggleFocus",
"alt-tab": "search::CycleMode",
"ctrl-shift-f": "search::FocusSearch",
"ctrl-shift-h": "search::ToggleReplace",
"alt-ctrl-g": "search::ActivateRegexMode",
"alt-ctrl-x": "search::ActivateTextMode"
"alt-ctrl-g": "search::ToggleRegex",
"alt-ctrl-x": "search::ToggleRegex"
}
},
{
@ -260,10 +258,9 @@
"context": "ProjectSearchView",
"bindings": {
"escape": "project_search::ToggleFocus",
"alt-tab": "search::CycleMode",
"ctrl-shift-h": "search::ToggleReplace",
"alt-ctrl-g": "search::ActivateRegexMode",
"alt-ctrl-x": "search::ActivateTextMode"
"alt-ctrl-g": "search::ToggleRegex",
"alt-ctrl-x": "search::ToggleRegex"
}
},
{
@ -283,10 +280,10 @@
"alt-enter": "search::SelectAllMatches",
"alt-c": "search::ToggleCaseSensitive",
"alt-w": "search::ToggleWholeWord",
"alt-r": "search::CycleMode",
"alt-r": "search::ToggleRegex",
"alt-ctrl-f": "project_search::ToggleFilters",
"ctrl-alt-shift-r": "search::ActivateRegexMode",
"ctrl-alt-shift-x": "search::ActivateTextMode"
"ctrl-alt-shift-r": "search::ToggleRegex",
"ctrl-alt-shift-x": "search::ToggleRegex"
}
},
// Bindings from VS Code

View file

@ -233,7 +233,6 @@
"enter": "search::SelectNextMatch",
"shift-enter": "search::SelectPrevMatch",
"alt-enter": "search::SelectAllMatches",
"alt-tab": "search::CycleMode",
"cmd-f": "search::FocusSearch",
"cmd-alt-f": "search::ToggleReplace"
}
@ -256,11 +255,10 @@
"context": "ProjectSearchBar",
"bindings": {
"escape": "project_search::ToggleFocus",
"alt-tab": "search::CycleMode",
"cmd-shift-f": "search::FocusSearch",
"cmd-shift-h": "search::ToggleReplace",
"alt-cmd-g": "search::ActivateRegexMode",
"alt-cmd-x": "search::ActivateTextMode"
"alt-cmd-g": "search::ToggleRegex",
"alt-cmd-x": "search::ToggleRegex"
}
},
{
@ -281,10 +279,9 @@
"context": "ProjectSearchView",
"bindings": {
"escape": "project_search::ToggleFocus",
"alt-tab": "search::CycleMode",
"cmd-shift-h": "search::ToggleReplace",
"alt-cmd-g": "search::ActivateRegexMode",
"alt-cmd-x": "search::ActivateTextMode"
"alt-cmd-g": "search::ToggleRegex",
"alt-cmd-x": "search::ToggleRegex"
}
},
{
@ -306,10 +303,9 @@
"alt-enter": "search::SelectAllMatches",
"alt-cmd-c": "search::ToggleCaseSensitive",
"alt-cmd-w": "search::ToggleWholeWord",
"alt-tab": "search::CycleMode",
"alt-cmd-f": "project_search::ToggleFilters",
"alt-cmd-g": "search::ActivateRegexMode",
"alt-cmd-x": "search::ActivateTextMode"
"alt-cmd-g": "search::ToggleRegex",
"alt-cmd-x": "search::ToggleRegex"
}
},
// Bindings from VS Code

View file

@ -471,6 +471,8 @@ pub struct Editor {
+ Fn(&mut Self, DisplayPoint, &mut ViewContext<Self>) -> Option<View<ui::ContextMenu>>,
>,
>,
last_bounds: Option<Bounds<Pixels>>,
expect_bounds_change: Option<Bounds<Pixels>>,
}
#[derive(Clone)]
@ -1485,6 +1487,8 @@ impl Editor {
inlay_hint_cache: InlayHintCache::new(inlay_hint_settings),
gutter_hovered: false,
pixel_position_of_newest_cursor: None,
last_bounds: None,
expect_bounds_change: None,
gutter_width: Default::default(),
style: None,
show_cursor_names: false,

View file

@ -3371,6 +3371,7 @@ impl Element for EditorElement {
let overscroll = size(em_width, px(0.));
snapshot = self.editor.update(cx, |editor, cx| {
editor.last_bounds = Some(bounds);
editor.gutter_width = gutter_dimensions.width;
editor.set_visible_line_count(bounds.size.height / line_height, cx);
@ -3419,7 +3420,7 @@ impl Element for EditorElement {
let autoscroll_horizontally = self.editor.update(cx, |editor, cx| {
let autoscroll_horizontally =
editor.autoscroll_vertically(bounds.size.height, line_height, cx);
editor.autoscroll_vertically(bounds, line_height, cx);
snapshot = editor.snapshot(cx);
autoscroll_horizontally
});

View file

@ -1167,6 +1167,10 @@ impl SearchableItem for Editor {
&self.buffer().read(cx).snapshot(cx),
)
}
fn search_bar_visibility_changed(&mut self, _visible: bool, _cx: &mut ViewContext<Self>) {
self.expect_bounds_change = self.last_bounds;
}
}
pub fn active_match_index(

View file

@ -1,6 +1,6 @@
use std::{cmp, f32};
use gpui::{px, Pixels, ViewContext};
use gpui::{px, Bounds, Pixels, ViewContext};
use language::Point;
use crate::{display_map::ToDisplayPoint, Editor, EditorMode, LineWithInvisibles};
@ -63,13 +63,23 @@ impl AutoscrollStrategy {
impl Editor {
pub fn autoscroll_vertically(
&mut self,
viewport_height: Pixels,
bounds: Bounds<Pixels>,
line_height: Pixels,
cx: &mut ViewContext<Editor>,
) -> bool {
let viewport_height = bounds.size.height;
let visible_lines = viewport_height / line_height;
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
let mut scroll_position = self.scroll_manager.scroll_position(&display_map);
let original_y = scroll_position.y;
if let Some(last_bounds) = self.expect_bounds_change.take() {
if scroll_position.y != 0. {
scroll_position.y += (bounds.top() - last_bounds.top()) / line_height;
if scroll_position.y < 0. {
scroll_position.y = 0.;
}
}
}
let max_scroll_top = if matches!(self.mode, EditorMode::AutoHeight { .. }) {
(display_map.max_point().row() as f32 - visible_lines + 1.).max(0.)
} else {
@ -77,6 +87,9 @@ impl Editor {
};
if scroll_position.y > max_scroll_top {
scroll_position.y = max_scroll_top;
}
if original_y != scroll_position.y {
self.set_scroll_position(scroll_position, cx);
}

View file

@ -2381,6 +2381,11 @@ impl ScrollHandle {
}
}
/// Return the bounds into which this child is painted
pub fn bounds(&self) -> Bounds<Pixels> {
self.0.borrow().bounds
}
/// Get the bounds for a specific child.
pub fn bounds_for_item(&self, ix: usize) -> Option<Bounds<Pixels>> {
self.0.borrow().child_bounds.get(ix).cloned()

View file

@ -1,24 +1,22 @@
mod registrar;
use crate::{
mode::{next_mode, SearchMode},
search_bar::render_nav_button,
ActivateRegexMode, ActivateTextMode, CycleMode, FocusSearch, NextHistoryQuery,
PreviousHistoryQuery, ReplaceAll, ReplaceNext, SearchOptions, SelectAllMatches,
SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleReplace, ToggleWholeWord,
search_bar::render_nav_button, FocusSearch, NextHistoryQuery, PreviousHistoryQuery, ReplaceAll,
ReplaceNext, SearchOptions, SelectAllMatches, SelectNextMatch, SelectPrevMatch,
ToggleCaseSensitive, ToggleRegex, ToggleReplace, ToggleWholeWord,
};
use any_vec::AnyVec;
use collections::HashMap;
use editor::{
actions::{Tab, TabPrev},
Editor, EditorElement, EditorStyle,
DisplayPoint, Editor, EditorElement, EditorStyle,
};
use futures::channel::oneshot;
use gpui::{
actions, div, impl_actions, Action, AppContext, ClickEvent, EventEmitter, FocusableView,
FontStyle, FontWeight, Hsla, InteractiveElement as _, IntoElement, KeyContext,
ParentElement as _, Render, Styled, Subscription, Task, TextStyle, View, ViewContext,
VisualContext as _, WhiteSpace, WindowContext,
ParentElement as _, Render, ScrollHandle, Styled, Subscription, Task, TextStyle, View,
ViewContext, VisualContext as _, WhiteSpace, WindowContext,
};
use project::{
search::SearchQuery,
@ -29,7 +27,7 @@ use settings::Settings;
use std::sync::Arc;
use theme::ThemeSettings;
use ui::{h_flex, prelude::*, IconButton, IconName, ToggleButton, Tooltip};
use ui::{h_flex, prelude::*, IconButton, IconName, Tooltip, BASE_REM_SIZE_IN_PX};
use util::ResultExt;
use workspace::{
item::ItemHandle,
@ -40,7 +38,7 @@ use workspace::{
pub use registrar::DivRegistrar;
use registrar::{ForDeployed, ForDismissed, SearchActionsRegistrar, WithResults};
const MIN_INPUT_WIDTH_REMS: f32 = 15.;
const MIN_INPUT_WIDTH_REMS: f32 = 10.;
const MAX_INPUT_WIDTH_REMS: f32 = 30.;
const MAX_BUFFER_SEARCH_HISTORY_SIZE: usize = 50;
@ -95,8 +93,10 @@ pub struct BufferSearchBar {
dismissed: bool,
search_history: SearchHistory,
search_history_cursor: SearchHistoryCursor,
current_mode: SearchMode,
replace_enabled: bool,
scroll_handle: ScrollHandle,
editor_scroll_handle: ScrollHandle,
editor_needed_width: Pixels,
}
impl BufferSearchBar {
@ -142,61 +142,29 @@ impl EventEmitter<workspace::ToolbarItemEvent> for BufferSearchBar {}
impl Render for BufferSearchBar {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
if self.dismissed {
return div();
return div().id("search_bar");
}
let narrow_mode =
self.scroll_handle.bounds().size.width / cx.rem_size() < 340. / BASE_REM_SIZE_IN_PX;
let hide_inline_icons = self.editor_needed_width
> self.editor_scroll_handle.bounds().size.width - cx.rem_size() * 6.;
let supported_options = self.supported_options();
if self.query_editor.update(cx, |query_editor, cx| {
query_editor.placeholder_text(cx).is_none()
}) {
let query_focus_handle = self.query_editor.focus_handle(cx);
let up_keystrokes = cx
.bindings_for_action_in(&PreviousHistoryQuery {}, &query_focus_handle)
.into_iter()
.next()
.map(|binding| {
binding
.keystrokes()
.iter()
.map(|k| k.to_string())
.collect::<Vec<_>>()
});
let down_keystrokes = cx
.bindings_for_action_in(&NextHistoryQuery {}, &query_focus_handle)
.into_iter()
.next()
.map(|binding| {
binding
.keystrokes()
.iter()
.map(|k| k.to_string())
.collect::<Vec<_>>()
});
let placeholder_text =
up_keystrokes
.zip(down_keystrokes)
.map(|(up_keystrokes, down_keystrokes)| {
Arc::from(format!(
"Search ({}/{} for previous/next query)",
up_keystrokes.join(" "),
down_keystrokes.join(" ")
))
});
if let Some(placeholder_text) = placeholder_text {
self.query_editor.update(cx, |editor, cx| {
editor.set_placeholder_text(placeholder_text, cx);
});
}
self.query_editor.update(cx, |editor, cx| {
editor.set_placeholder_text("Search", cx);
});
}
self.replacement_editor.update(cx, |editor, cx| {
editor.set_placeholder_text("Replace with...", cx);
});
let mut match_color = Color::Default;
let mut text_color = Color::Default;
let match_text = self
.active_searchable_item
.as_ref()
@ -212,12 +180,11 @@ impl Render for BufferSearchBar {
if let Some(match_ix) = self.active_match_index {
Some(format!("{}/{}", match_ix + 1, matches_count))
} else {
match_color = Color::Error; // No matches found
text_color = Color::Error; // No matches found
None
}
})
.unwrap_or_else(|| "No matches".to_string());
let match_count = Label::new(match_text).color(match_color);
.unwrap_or_else(|| "0/0".to_string());
let should_show_replace_input = self.replace_enabled && supported_options.replacement;
let in_replace = self.replacement_editor.focus_handle(cx).is_focused(cx);
@ -233,111 +200,94 @@ impl Render for BufferSearchBar {
};
let search_line = h_flex()
.gap_2()
.child(
h_flex()
.id("editor-scroll")
.track_scroll(&self.editor_scroll_handle)
.flex_1()
.h_8()
.px_2()
.mr_2()
.py_1()
.border_1()
.border_color(editor_border)
.min_w(rems(MIN_INPUT_WIDTH_REMS))
.max_w(rems(MAX_INPUT_WIDTH_REMS))
.rounded_lg()
.child(self.render_text_input(&self.query_editor, match_color.color(cx), cx))
.children(supported_options.case.then(|| {
self.render_search_option_button(
SearchOptions::CASE_SENSITIVE,
cx.listener(|this, _, cx| {
this.toggle_case_sensitive(&ToggleCaseSensitive, cx)
}),
)
}))
.children(supported_options.word.then(|| {
self.render_search_option_button(
SearchOptions::WHOLE_WORD,
cx.listener(|this, _, cx| this.toggle_whole_word(&ToggleWholeWord, cx)),
)
})),
)
.child(
h_flex()
.gap_2()
.flex_none()
.child(
h_flex()
.child(
ToggleButton::new("search-mode-text", SearchMode::Text.label())
.style(ButtonStyle::Filled)
.size(ButtonSize::Large)
.selected(self.current_mode == SearchMode::Text)
.on_click(cx.listener(move |_, _event, cx| {
cx.dispatch_action(SearchMode::Text.action())
}))
.tooltip(|cx| {
Tooltip::for_action(
SearchMode::Text.tooltip(),
&*SearchMode::Text.action(),
cx,
)
})
.first(),
.child(self.render_text_input(&self.query_editor, text_color.color(cx), cx))
.when(!hide_inline_icons, |div| {
div.children(supported_options.case.then(|| {
self.render_search_option_button(
SearchOptions::CASE_SENSITIVE,
cx.listener(|this, _, cx| {
this.toggle_case_sensitive(&ToggleCaseSensitive, cx)
}),
)
.child(
ToggleButton::new("search-mode-regex", SearchMode::Regex.label())
.style(ButtonStyle::Filled)
.size(ButtonSize::Large)
.selected(self.current_mode == SearchMode::Regex)
.on_click(cx.listener(move |_, _event, cx| {
cx.dispatch_action(SearchMode::Regex.action())
}))
.tooltip(|cx| {
Tooltip::for_action(
SearchMode::Regex.tooltip(),
&*SearchMode::Regex.action(),
cx,
)
})
.last(),
),
)
.when(supported_options.replacement, |this| {
this.child(
IconButton::new(
"buffer-search-bar-toggle-replace-button",
IconName::Replace,
}))
.children(supported_options.word.then(|| {
self.render_search_option_button(
SearchOptions::WHOLE_WORD,
cx.listener(|this, _, cx| {
this.toggle_whole_word(&ToggleWholeWord, cx)
}),
)
.style(ButtonStyle::Subtle)
.when(self.replace_enabled, |button| {
button.style(ButtonStyle::Filled)
})
.on_click(cx.listener(|this, _: &ClickEvent, cx| {
this.toggle_replace(&ToggleReplace, cx);
}))
.tooltip(|cx| {
Tooltip::for_action("Toggle replace", &ToggleReplace, cx)
}),
)
}))
.children(supported_options.word.then(|| {
self.render_search_option_button(
SearchOptions::REGEX,
cx.listener(|this, _, cx| this.toggle_regex(&ToggleRegex, cx)),
)
}))
}),
)
.when(supported_options.replacement, |this| {
this.child(
IconButton::new("buffer-search-bar-toggle-replace-button", IconName::Replace)
.style(ButtonStyle::Subtle)
.when(self.replace_enabled, |button| {
button.style(ButtonStyle::Filled)
})
.on_click(cx.listener(|this, _: &ClickEvent, cx| {
this.toggle_replace(&ToggleReplace, cx);
}))
.selected(self.replace_enabled)
.size(ButtonSize::Compact)
.tooltip(|cx| Tooltip::for_action("Toggle replace", &ToggleReplace, cx)),
)
})
.child(
h_flex()
.gap_2()
.flex_none()
.child(
IconButton::new("select-all", ui::IconName::SelectAll)
.on_click(|_, cx| cx.dispatch_action(SelectAllMatches.boxed_clone()))
.size(ButtonSize::Compact)
.tooltip(|cx| {
Tooltip::for_action("Select all matches", &SelectAllMatches, cx)
}),
)
.child(div().min_w(rems(6.)).child(match_count))
.child(render_nav_button(
ui::IconName::ChevronLeft,
self.active_match_index.is_some(),
"Select previous match",
&SelectPrevMatch,
))
.when(!narrow_mode, |this| {
this.child(
h_flex()
.mx(rems_from_px(-4.0))
.min_w(rems_from_px(40.))
.justify_center()
.items_center()
.child(Label::new(match_text).color(
if self.active_match_index.is_some() {
Color::Default
} else {
Color::Disabled
},
)),
)
})
.child(render_nav_button(
ui::IconName::ChevronRight,
self.active_match_index.is_some(),
@ -394,6 +344,8 @@ impl Render for BufferSearchBar {
});
v_flex()
.id("buffer_search")
.track_scroll(&self.scroll_handle)
.key_context(key_context)
.capture_action(cx.listener(Self::tab))
.capture_action(cx.listener(Self::tab_prev))
@ -402,12 +354,6 @@ impl Render for BufferSearchBar {
.on_action(cx.listener(Self::dismiss))
.on_action(cx.listener(Self::select_next_match))
.on_action(cx.listener(Self::select_prev_match))
.on_action(cx.listener(|this, _: &ActivateRegexMode, cx| {
this.activate_search_mode(SearchMode::Regex, cx);
}))
.on_action(cx.listener(|this, _: &ActivateTextMode, cx| {
this.activate_search_mode(SearchMode::Text, cx);
}))
.when(self.supported_options().replacement, |this| {
this.on_action(cx.listener(Self::toggle_replace))
.when(in_replace, |this| {
@ -421,15 +367,24 @@ impl Render for BufferSearchBar {
.when(self.supported_options().word, |this| {
this.on_action(cx.listener(Self::toggle_whole_word))
})
.when(self.supported_options().regex, |this| {
this.on_action(cx.listener(Self::toggle_regex))
})
.gap_2()
.child(
h_flex().child(search_line.w_full()).child(
IconButton::new(SharedString::from("Close"), IconName::Close)
.tooltip(move |cx| Tooltip::for_action("Close search bar", &Dismiss, cx))
.on_click(
cx.listener(|this, _: &ClickEvent, cx| this.dismiss(&Dismiss, cx)),
),
),
h_flex()
.child(search_line.w_full())
.when(!narrow_mode, |div| {
div.child(
IconButton::new(SharedString::from("Close"), IconName::Close)
.tooltip(move |cx| {
Tooltip::for_action("Close search bar", &Dismiss, cx)
})
.on_click(cx.listener(|this, _: &ClickEvent, cx| {
this.dismiss(&Dismiss, cx)
})),
)
}),
)
.children(replace_line)
}
@ -504,21 +459,6 @@ impl BufferSearchBar {
this.toggle_replace(action, cx);
}
}));
registrar.register_handler(ForDeployed(|this, _: &ActivateRegexMode, cx| {
if this.supported_options().regex {
this.activate_search_mode(SearchMode::Regex, cx);
}
}));
registrar.register_handler(ForDeployed(|this, _: &ActivateTextMode, cx| {
this.activate_search_mode(SearchMode::Text, cx);
}));
registrar.register_handler(ForDeployed(|this, action: &CycleMode, cx| {
if this.supported_options().regex {
// If regex is not supported then search has just one mode (text) - in that case there's no point in supporting
// cycling.
this.cycle_mode(action, cx)
}
}));
registrar.register_handler(WithResults(|this, action: &SelectNextMatch, cx| {
this.select_next_match(action, cx);
}));
@ -569,9 +509,11 @@ impl BufferSearchBar {
project::search_history::QueryInsertionBehavior::ReplacePreviousIfContains,
),
search_history_cursor: Default::default(),
current_mode: SearchMode::default(),
active_search: None,
replace_enabled: false,
scroll_handle: ScrollHandle::new(),
editor_scroll_handle: ScrollHandle::new(),
editor_needed_width: px(0.),
}
}
@ -589,6 +531,7 @@ impl BufferSearchBar {
}
}
if let Some(active_editor) = self.active_searchable_item.as_ref() {
active_editor.search_bar_visibility_changed(false, cx);
let handle = active_editor.focus_handle(cx);
cx.focus(&handle);
}
@ -630,10 +573,12 @@ impl BufferSearchBar {
}
pub fn show(&mut self, cx: &mut ViewContext<Self>) -> bool {
if self.active_searchable_item.is_none() {
let Some(handle) = self.active_searchable_item.as_ref() else {
return false;
}
};
self.dismissed = false;
handle.search_bar_visibility_changed(true, cx);
cx.notify();
cx.emit(Event::UpdateLocation);
cx.emit(ToolbarItemEvent::ChangeLocation(
@ -740,14 +685,6 @@ impl BufferSearchBar {
let is_active = self.search_options.contains(option);
option.as_button(is_active, action)
}
pub fn activate_search_mode(&mut self, mode: SearchMode, cx: &mut ViewContext<Self>) {
if mode == self.current_mode {
return;
}
self.current_mode = mode;
let _ = self.update_matches(cx);
cx.notify();
}
pub fn focus_editor(&mut self, _: &FocusEditor, cx: &mut ViewContext<Self>) {
if let Some(active_editor) = self.active_searchable_item.as_ref() {
@ -763,6 +700,16 @@ impl BufferSearchBar {
cx.notify();
}
pub fn enable_search_option(
&mut self,
search_option: SearchOptions,
cx: &mut ViewContext<Self>,
) {
if !self.search_options.contains(search_option) {
self.toggle_search_option(search_option, cx)
}
}
pub fn set_search_options(
&mut self,
search_options: SearchOptions,
@ -829,7 +776,7 @@ impl BufferSearchBar {
fn on_query_editor_event(
&mut self,
_: View<Editor>,
editor: View<Editor>,
event: &editor::EditorEvent,
cx: &mut ViewContext<Self>,
) {
@ -839,6 +786,17 @@ impl BufferSearchBar {
editor::EditorEvent::Edited => {
self.clear_matches(cx);
let search = self.update_matches(cx);
let width = editor.update(cx, |editor, cx| {
let text_layout_details = editor.text_layout_details(cx);
let snapshot = editor.snapshot(cx).display_snapshot;
snapshot.x_for_display_point(snapshot.max_point(), &text_layout_details)
- snapshot.x_for_display_point(DisplayPoint::zero(), &text_layout_details)
});
self.editor_needed_width = width;
cx.notify();
cx.spawn(|this, mut cx| async move {
search.await?;
this.update(&mut cx, |this, cx| this.activate_current_match(cx))
@ -874,10 +832,15 @@ impl BufferSearchBar {
fn toggle_case_sensitive(&mut self, _: &ToggleCaseSensitive, cx: &mut ViewContext<Self>) {
self.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx)
}
fn toggle_whole_word(&mut self, _: &ToggleWholeWord, cx: &mut ViewContext<Self>) {
self.toggle_search_option(SearchOptions::WHOLE_WORD, cx)
}
fn toggle_regex(&mut self, _: &ToggleRegex, cx: &mut ViewContext<Self>) {
self.toggle_search_option(SearchOptions::REGEX, cx)
}
fn clear_active_searchable_item_matches(&mut self, cx: &mut WindowContext) {
if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
self.active_match_index = None;
@ -917,7 +880,7 @@ impl BufferSearchBar {
let _ = done_tx.send(());
cx.notify();
} else {
let query: Arc<_> = if self.current_mode == SearchMode::Regex {
let query: Arc<_> = if self.search_options.contains(SearchOptions::REGEX) {
match SearchQuery::regex(
query,
self.search_options.contains(SearchOptions::WHOLE_WORD),
@ -1065,9 +1028,7 @@ impl BufferSearchBar {
let _ = self.search(&new_query, Some(self.search_options), cx);
}
}
fn cycle_mode(&mut self, _: &CycleMode, cx: &mut ViewContext<Self>) {
self.activate_search_mode(next_mode(&self.current_mode), cx);
}
fn toggle_replace(&mut self, _: &ToggleReplace, cx: &mut ViewContext<Self>) {
if let Some(_) = &self.active_searchable_item {
self.replace_enabled = !self.replace_enabled;
@ -1910,8 +1871,7 @@ mod tests {
// Let's turn on regex mode.
search_bar
.update(cx, |search_bar, cx| {
search_bar.activate_search_mode(SearchMode::Regex, cx);
search_bar.search("\\[([^\\]]+)\\]", None, cx)
search_bar.search("\\[([^\\]]+)\\]", Some(SearchOptions::REGEX), cx)
})
.await
.unwrap();
@ -1934,8 +1894,11 @@ mod tests {
// Now with a whole-word twist.
search_bar
.update(cx, |search_bar, cx| {
search_bar.activate_search_mode(SearchMode::Regex, cx);
search_bar.search("a\\w+s", Some(SearchOptions::WHOLE_WORD), cx)
search_bar.search(
"a\\w+s",
Some(SearchOptions::REGEX | SearchOptions::WHOLE_WORD),
cx,
)
})
.await
.unwrap();
@ -1963,7 +1926,6 @@ mod tests {
editor: &'a View<Editor>,
search_bar: &'a View<BufferSearchBar>,
cx: &'a mut VisualTestContext,
search_mode: SearchMode,
search_text: &'static str,
search_options: Option<SearchOptions>,
replacement_text: &'static str,
@ -1975,7 +1937,9 @@ mod tests {
options
.search_bar
.update(options.cx, |search_bar, cx| {
search_bar.activate_search_mode(options.search_mode, cx);
if let Some(options) = options.search_options {
search_bar.set_search_options(options, cx);
}
search_bar.search(options.search_text, options.search_options, cx)
})
.await
@ -2009,7 +1973,6 @@ mod tests {
editor: &editor,
search_bar: &search_bar,
cx,
search_mode: SearchMode::Text,
search_text: "expression",
search_options: None,
replacement_text: r"\n",
@ -2028,9 +1991,8 @@ mod tests {
editor: &editor,
search_bar: &search_bar,
cx,
search_mode: SearchMode::Regex,
search_text: "or",
search_options: Some(SearchOptions::WHOLE_WORD),
search_options: Some(SearchOptions::WHOLE_WORD | SearchOptions::REGEX),
replacement_text: r"\\\n\\\\",
replace_all: false,
expected_text: r#"
@ -2048,9 +2010,8 @@ mod tests {
editor: &editor,
search_bar: &search_bar,
cx,
search_mode: SearchMode::Regex,
search_text: r"(that|used) ",
search_options: None,
search_options: Some(SearchOptions::REGEX),
replacement_text: r"$1\n",
replace_all: true,
expected_text: r#"
@ -2079,7 +2040,7 @@ mod tests {
// Search using valid regexp
search_bar
.update(cx, |search_bar, cx| {
search_bar.activate_search_mode(SearchMode::Regex, cx);
search_bar.enable_search_option(SearchOptions::REGEX, cx);
search_bar.search("expression", None, cx)
})
.await

View file

@ -1,8 +1,7 @@
use crate::{
mode::SearchMode, ActivateRegexMode, ActivateTextMode, CycleMode, FocusSearch,
NextHistoryQuery, PreviousHistoryQuery, ReplaceAll, ReplaceNext, SearchOptions,
SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleIncludeIgnored, ToggleReplace,
ToggleWholeWord,
FocusSearch, NextHistoryQuery, PreviousHistoryQuery, ReplaceAll, ReplaceNext, SearchOptions,
SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleIncludeIgnored, ToggleRegex,
ToggleReplace, ToggleWholeWord,
};
use anyhow::Context as _;
use collections::{HashMap, HashSet};
@ -15,7 +14,7 @@ use editor::{
use gpui::{
actions, div, Action, AnyElement, AnyView, AppContext, Context as _, Element, EntityId,
EventEmitter, FocusHandle, FocusableView, FontStyle, FontWeight, Global, Hsla,
InteractiveElement, IntoElement, KeyContext, Model, ModelContext, ParentElement, Point, Render,
InteractiveElement, IntoElement, Model, ModelContext, ParentElement, Point, Render,
SharedString, Styled, Subscription, Task, TextStyle, View, ViewContext, VisualContext,
WeakModel, WeakView, WhiteSpace, WindowContext,
};
@ -32,7 +31,7 @@ use std::{
use theme::ThemeSettings;
use ui::{
h_flex, prelude::*, v_flex, Icon, IconButton, IconName, Label, LabelCommon, LabelSize,
Selectable, ToggleButton, Tooltip,
Selectable, Tooltip,
};
use util::paths::PathMatcher;
use workspace::{
@ -72,18 +71,12 @@ pub fn init(cx: &mut AppContext) {
register_workspace_action(workspace, move |search_bar, _: &ToggleWholeWord, cx| {
search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, cx);
});
register_workspace_action(workspace, move |search_bar, _: &ToggleRegex, cx| {
search_bar.toggle_search_option(SearchOptions::REGEX, cx);
});
register_workspace_action(workspace, move |search_bar, action: &ToggleReplace, cx| {
search_bar.toggle_replace(action, cx)
});
register_workspace_action(workspace, move |search_bar, _: &ActivateRegexMode, cx| {
search_bar.activate_search_mode(SearchMode::Regex, cx)
});
register_workspace_action(workspace, move |search_bar, _: &ActivateTextMode, cx| {
search_bar.activate_search_mode(SearchMode::Text, cx)
});
register_workspace_action(workspace, move |search_bar, action: &CycleMode, cx| {
search_bar.cycle_mode(action, cx)
});
register_workspace_action(
workspace,
move |search_bar, action: &SelectPrevMatch, cx| {
@ -158,7 +151,6 @@ pub struct ProjectSearchView {
excluded_files_editor: View<Editor>,
filters_enabled: bool,
replace_enabled: bool,
current_mode: SearchMode,
_subscriptions: Vec<Subscription>,
}
@ -166,7 +158,6 @@ pub struct ProjectSearchView {
struct ProjectSearchSettings {
search_options: SearchOptions,
filters_enabled: bool,
current_mode: SearchMode,
}
pub struct ProjectSearchBar {
@ -302,7 +293,7 @@ impl Render for ProjectSearchView {
} else if has_no_results {
Label::new("No results")
} else {
Label::new(format!("{} search all files", self.current_mode.label()))
Label::new("Search all files")
};
let major_text = div().justify_center().max_w_96().child(major_text);
@ -549,7 +540,6 @@ impl ProjectSearchView {
ProjectSearchSettings {
search_options: self.search_options,
filters_enabled: self.filters_enabled,
current_mode: self.current_mode,
}
}
fn toggle_search_option(&mut self, option: SearchOptions, cx: &mut ViewContext<Self>) {
@ -562,39 +552,6 @@ impl ProjectSearchView {
});
}
fn clear_search(&mut self, cx: &mut ViewContext<Self>) {
self.model.update(cx, |model, cx| {
model.pending_search = None;
model.no_results = None;
model.limit_reached = false;
model.match_ranges.clear();
model.excerpts.update(cx, |excerpts, cx| {
excerpts.clear(cx);
});
});
}
fn activate_search_mode(&mut self, mode: SearchMode, cx: &mut ViewContext<Self>) {
let previous_mode = self.current_mode;
if previous_mode == mode {
return;
}
self.clear_search(cx);
self.current_mode = mode;
self.active_match_index = None;
self.search(cx);
cx.update_global(|state: &mut ActiveSettings, cx| {
state.0.insert(
self.model.read(cx).project.downgrade(),
self.current_settings(),
);
});
cx.notify();
}
fn replace_next(&mut self, _: &ReplaceNext, cx: &mut ViewContext<Self>) {
if self.model.read(cx).match_ranges.is_empty() {
return;
@ -658,14 +615,10 @@ impl ProjectSearchView {
let mut subscriptions = Vec::new();
// Read in settings if available
let (mut options, current_mode, filters_enabled) = if let Some(settings) = settings {
(
settings.search_options,
settings.current_mode,
settings.filters_enabled,
)
let (mut options, filters_enabled) = if let Some(settings) = settings {
(settings.search_options, settings.filters_enabled)
} else {
(SearchOptions::NONE, Default::default(), false)
(SearchOptions::NONE, false)
};
{
@ -682,7 +635,7 @@ impl ProjectSearchView {
let query_editor = cx.new_view(|cx| {
let mut editor = Editor::single_line(cx);
editor.set_placeholder_text("Text search all files", cx);
editor.set_placeholder_text("Search all files..", cx);
editor.set_text(query_text, cx);
editor
});
@ -769,7 +722,6 @@ impl ProjectSearchView {
included_files_editor,
excluded_files_editor,
filters_enabled,
current_mode,
replace_enabled: false,
_subscriptions: subscriptions,
};
@ -946,37 +898,8 @@ impl ProjectSearchView {
}
};
let current_mode = self.current_mode;
let query = match current_mode {
SearchMode::Regex => {
match SearchQuery::regex(
text,
self.search_options.contains(SearchOptions::WHOLE_WORD),
self.search_options.contains(SearchOptions::CASE_SENSITIVE),
self.search_options.contains(SearchOptions::INCLUDE_IGNORED),
included_files,
excluded_files,
) {
Ok(query) => {
let should_unmark_error =
self.panels_with_errors.remove(&InputPanel::Query);
if should_unmark_error {
cx.notify();
}
Some(query)
}
Err(_e) => {
let should_mark_error = self.panels_with_errors.insert(InputPanel::Query);
if should_mark_error {
cx.notify();
}
None
}
}
}
_ => match SearchQuery::text(
let query = if self.search_options.contains(SearchOptions::REGEX) {
match SearchQuery::regex(
text,
self.search_options.contains(SearchOptions::WHOLE_WORD),
self.search_options.contains(SearchOptions::CASE_SENSITIVE),
@ -1000,7 +923,33 @@ impl ProjectSearchView {
None
}
},
}
} else {
match SearchQuery::text(
text,
self.search_options.contains(SearchOptions::WHOLE_WORD),
self.search_options.contains(SearchOptions::CASE_SENSITIVE),
self.search_options.contains(SearchOptions::INCLUDE_IGNORED),
included_files,
excluded_files,
) {
Ok(query) => {
let should_unmark_error = self.panels_with_errors.remove(&InputPanel::Query);
if should_unmark_error {
cx.notify();
}
Some(query)
}
Err(_e) => {
let should_mark_error = self.panels_with_errors.insert(InputPanel::Query);
if should_mark_error {
cx.notify();
}
None
}
}
};
if !self.panels_with_errors.is_empty() {
return None;
@ -1116,10 +1065,9 @@ impl ProjectSearchView {
}
fn landing_text_minor(&self) -> SharedString {
match self.current_mode {
SearchMode::Text | SearchMode::Regex => "Include/exclude specific paths with the filter option. Matching exact word and/or casing is available too.".into(),
}
"Include/exclude specific paths with the filter option. Matching exact word and/or casing is available too.".into()
}
fn border_color_for(&self, panel: InputPanel, cx: &WindowContext) -> Hsla {
if self.panels_with_errors.contains(&panel) {
Color::Error.color(cx)
@ -1127,6 +1075,7 @@ impl ProjectSearchView {
cx.theme().colors().border
}
}
fn move_focus_to_results(&mut self, cx: &mut ViewContext<Self>) {
if !self.results_editor.focus_handle(cx).is_focused(cx)
&& !self.model.read(cx).match_ranges.is_empty()
@ -1145,17 +1094,6 @@ impl ProjectSearchBar {
}
}
fn cycle_mode(&self, _: &CycleMode, cx: &mut ViewContext<Self>) {
if let Some(view) = self.active_project_search.as_ref() {
view.update(cx, |this, cx| {
let new_mode = crate::mode::next_mode(&this.current_mode);
this.activate_search_mode(new_mode, cx);
let editor_handle = this.query_editor.focus_handle(cx);
cx.focus(&editor_handle);
});
}
}
fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
if let Some(search_view) = self.active_project_search.as_ref() {
search_view.update(cx, |search_view, cx| {
@ -1285,16 +1223,6 @@ impl ProjectSearchBar {
}
}
fn activate_search_mode(&self, mode: SearchMode, cx: &mut ViewContext<Self>) {
// Update Current Mode
if let Some(search_view) = self.active_project_search.as_ref() {
search_view.update(cx, |search_view, cx| {
search_view.activate_search_mode(mode, cx);
});
cx.notify();
}
}
fn is_option_enabled(&self, option: SearchOptions, cx: &AppContext) -> bool {
if let Some(search) = self.active_project_search.as_ref() {
search.read(cx).search_options.contains(option)
@ -1372,48 +1300,6 @@ impl ProjectSearchBar {
}
}
fn new_placeholder_text(&self, cx: &mut ViewContext<Self>) -> Option<String> {
let previous_query_keystrokes = cx
.bindings_for_action(&PreviousHistoryQuery {})
.into_iter()
.next()
.map(|binding| {
binding
.keystrokes()
.iter()
.map(|k| k.to_string())
.collect::<Vec<_>>()
});
let next_query_keystrokes = cx
.bindings_for_action(&NextHistoryQuery {})
.into_iter()
.next()
.map(|binding| {
binding
.keystrokes()
.iter()
.map(|k| k.to_string())
.collect::<Vec<_>>()
});
let new_placeholder_text = match (previous_query_keystrokes, next_query_keystrokes) {
(Some(previous_query_keystrokes), Some(next_query_keystrokes)) => Some(format!(
"Search ({}/{} for previous/next query)",
previous_query_keystrokes.join(" "),
next_query_keystrokes.join(" ")
)),
(None, Some(next_query_keystrokes)) => Some(format!(
"Search ({} for next query)",
next_query_keystrokes.join(" ")
)),
(Some(previous_query_keystrokes), None) => Some(format!(
"Search ({} for previous query)",
previous_query_keystrokes.join(" ")
)),
(None, None) => None,
};
new_placeholder_text
}
fn render_text_input(&self, editor: &View<Editor>, cx: &ViewContext<Self>) -> impl IntoElement {
let settings = ThemeSettings::get_global(cx);
let text_style = TextStyle {
@ -1451,19 +1337,12 @@ impl Render for ProjectSearchBar {
let Some(search) = self.active_project_search.clone() else {
return div();
};
let mut key_context = KeyContext::default();
key_context.add("ProjectSearchBar");
if let Some(placeholder_text) = self.new_placeholder_text(cx) {
search.update(cx, |search, cx| {
search.query_editor.update(cx, |this, cx| {
this.set_placeholder_text(placeholder_text, cx)
})
});
}
let search = search.read(cx);
let query_column = h_flex()
.flex_1()
.h_8()
.mr_2()
.px_2()
.py_1()
.border_1()
@ -1477,79 +1356,39 @@ impl Render for ProjectSearchBar {
.child(self.render_text_input(&search.query_editor, cx))
.child(
h_flex()
.child(
IconButton::new("project-search-filter-button", IconName::Filter)
.tooltip(|cx| Tooltip::for_action("Toggle filters", &ToggleFilters, cx))
.on_click(cx.listener(|this, _, cx| {
this.toggle_filters(cx);
}))
.selected(
self.active_project_search
.as_ref()
.map(|search| search.read(cx).filters_enabled)
.unwrap_or_default(),
),
)
.child(
IconButton::new("project-search-case-sensitive", IconName::CaseSensitive)
.tooltip(|cx| {
Tooltip::for_action(
"Toggle case sensitive",
&ToggleCaseSensitive,
cx,
)
})
.selected(self.is_option_enabled(SearchOptions::CASE_SENSITIVE, cx))
.on_click(cx.listener(|this, _, cx| {
this.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx);
})),
)
.child(
IconButton::new("project-search-whole-word", IconName::WholeWord)
.tooltip(|cx| {
Tooltip::for_action("Toggle whole word", &ToggleWholeWord, cx)
})
.selected(self.is_option_enabled(SearchOptions::WHOLE_WORD, cx))
.on_click(cx.listener(|this, _, cx| {
this.toggle_search_option(SearchOptions::WHOLE_WORD, cx);
})),
),
.child(SearchOptions::CASE_SENSITIVE.as_button(
self.is_option_enabled(SearchOptions::CASE_SENSITIVE, cx),
cx.listener(|this, _, cx| {
this.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx);
}),
))
.child(SearchOptions::WHOLE_WORD.as_button(
self.is_option_enabled(SearchOptions::WHOLE_WORD, cx),
cx.listener(|this, _, cx| {
this.toggle_search_option(SearchOptions::WHOLE_WORD, cx);
}),
))
.child(SearchOptions::REGEX.as_button(
self.is_option_enabled(SearchOptions::REGEX, cx),
cx.listener(|this, _, cx| {
this.toggle_search_option(SearchOptions::REGEX, cx);
}),
)),
);
let mode_column = v_flex().items_start().justify_start().child(
h_flex()
.gap_2()
.child(
h_flex()
.child(
ToggleButton::new("project-search-text-button", "Text")
.style(ButtonStyle::Filled)
.size(ButtonSize::Large)
.selected(search.current_mode == SearchMode::Text)
.on_click(cx.listener(|this, _, cx| {
this.activate_search_mode(SearchMode::Text, cx)
}))
.tooltip(|cx| {
Tooltip::for_action("Toggle text search", &ActivateTextMode, cx)
})
.first(),
)
.child(
ToggleButton::new("project-search-regex-button", "Regex")
.style(ButtonStyle::Filled)
.size(ButtonSize::Large)
.selected(search.current_mode == SearchMode::Regex)
.on_click(cx.listener(|this, _, cx| {
this.activate_search_mode(SearchMode::Regex, cx)
}))
.tooltip(|cx| {
Tooltip::for_action(
"Toggle regular expression search",
&ActivateRegexMode,
cx,
)
})
.last(),
IconButton::new("project-search-filter-button", IconName::Filter)
.tooltip(|cx| Tooltip::for_action("Toggle filters", &ToggleFilters, cx))
.on_click(cx.listener(|this, _, cx| {
this.toggle_filters(cx);
}))
.selected(
self.active_project_search
.as_ref()
.map(|search| search.read(cx).filters_enabled)
.unwrap_or_default(),
),
)
.child(
@ -1557,6 +1396,12 @@ impl Render for ProjectSearchBar {
.on_click(cx.listener(|this, _, cx| {
this.toggle_replace(&ToggleReplace, cx);
}))
.selected(
self.active_project_search
.as_ref()
.map(|search| search.read(cx).replace_enabled)
.unwrap_or_default(),
)
.tooltip(|cx| Tooltip::for_action("Toggle replace", &ToggleReplace, cx)),
),
);
@ -1573,12 +1418,11 @@ impl Render for ProjectSearchBar {
None
}
})
.unwrap_or_else(|| "No matches".to_string());
.unwrap_or_else(|| "0/0".to_string());
let limit_reached = search.model.read(cx).limit_reached;
let matches_column = h_flex()
.child(div().min_w(rems(6.)).child(Label::new(match_text)))
.child(
IconButton::new("project-search-prev-match", IconName::ChevronLeft)
.disabled(search.active_match_index.is_none())
@ -1593,6 +1437,20 @@ impl Render for ProjectSearchBar {
Tooltip::for_action("Go to previous match", &SelectPrevMatch, cx)
}),
)
.child(
h_flex()
.mx(rems_from_px(-4.0))
.min_w(rems_from_px(40.))
.justify_center()
.items_center()
.child(
Label::new(match_text).color(if search.active_match_index.is_some() {
Color::Default
} else {
Color::Disabled
}),
),
)
.child(
IconButton::new("project-search-next-match", IconName::ChevronRight)
.disabled(search.active_match_index.is_none())
@ -1614,7 +1472,6 @@ impl Render for ProjectSearchBar {
});
let search_line = h_flex()
.gap_2()
.flex_1()
.child(query_column)
.child(mode_column)
@ -1705,17 +1562,11 @@ impl Render for ProjectSearchBar {
});
v_flex()
.key_context(key_context)
.key_context("ProjectSearchBar")
.on_action(cx.listener(|this, _: &ToggleFocus, cx| this.move_focus_to_results(cx)))
.on_action(cx.listener(|this, _: &ToggleFilters, cx| {
this.toggle_filters(cx);
}))
.on_action(cx.listener(|this, _: &ActivateTextMode, cx| {
this.activate_search_mode(SearchMode::Text, cx)
}))
.on_action(cx.listener(|this, _: &ActivateRegexMode, cx| {
this.activate_search_mode(SearchMode::Regex, cx)
}))
.capture_action(cx.listener(|this, action, cx| {
this.tab(action, cx);
cx.stop_propagation();
@ -1725,9 +1576,6 @@ impl Render for ProjectSearchBar {
cx.stop_propagation();
}))
.on_action(cx.listener(|this, action, cx| this.confirm(action, cx)))
.on_action(cx.listener(|this, action, cx| {
this.cycle_mode(action, cx);
}))
.on_action(cx.listener(|this, action, cx| {
this.toggle_replace(action, cx);
}))

View file

@ -1,14 +1,12 @@
use bitflags::bitflags;
pub use buffer_search::BufferSearchBar;
use gpui::{actions, Action, AppContext, IntoElement};
pub use mode::SearchMode;
use project::search::SearchQuery;
pub use project_search::ProjectSearchView;
use ui::{prelude::*, Tooltip};
use ui::{ButtonStyle, IconButton};
pub mod buffer_search;
mod mode;
pub mod project_search;
pub(crate) mod search_bar;
@ -21,19 +19,17 @@ pub fn init(cx: &mut AppContext) {
actions!(
search,
[
CycleMode,
FocusSearch,
ToggleWholeWord,
ToggleCaseSensitive,
ToggleIncludeIgnored,
ToggleRegex,
ToggleReplace,
SelectNextMatch,
SelectPrevMatch,
SelectAllMatches,
NextHistoryQuery,
PreviousHistoryQuery,
ActivateTextMode,
ActivateRegexMode,
ReplaceAll,
ReplaceNext,
]
@ -46,15 +42,17 @@ bitflags! {
const WHOLE_WORD = 0b001;
const CASE_SENSITIVE = 0b010;
const INCLUDE_IGNORED = 0b100;
const REGEX = 0b1000;
}
}
impl SearchOptions {
pub fn label(&self) -> &'static str {
match *self {
SearchOptions::WHOLE_WORD => "Match Whole Word",
SearchOptions::CASE_SENSITIVE => "Match Case",
SearchOptions::INCLUDE_IGNORED => "Include ignored",
SearchOptions::WHOLE_WORD => "whole word",
SearchOptions::CASE_SENSITIVE => "match case",
SearchOptions::INCLUDE_IGNORED => "include Ignored",
SearchOptions::REGEX => "regular expression",
_ => panic!("{:?} is not a named SearchOption", self),
}
}
@ -64,6 +62,7 @@ impl SearchOptions {
SearchOptions::WHOLE_WORD => ui::IconName::WholeWord,
SearchOptions::CASE_SENSITIVE => ui::IconName::CaseSensitive,
SearchOptions::INCLUDE_IGNORED => ui::IconName::FileGit,
SearchOptions::REGEX => ui::IconName::Regex,
_ => panic!("{:?} is not a named SearchOption", self),
}
}
@ -73,6 +72,7 @@ impl SearchOptions {
SearchOptions::WHOLE_WORD => Box::new(ToggleWholeWord),
SearchOptions::CASE_SENSITIVE => Box::new(ToggleCaseSensitive),
SearchOptions::INCLUDE_IGNORED => Box::new(ToggleIncludeIgnored),
SearchOptions::REGEX => Box::new(ToggleRegex),
_ => panic!("{:?} is not a named SearchOption", self),
}
}
@ -86,6 +86,7 @@ impl SearchOptions {
options.set(SearchOptions::WHOLE_WORD, query.whole_word());
options.set(SearchOptions::CASE_SENSITIVE, query.case_sensitive());
options.set(SearchOptions::INCLUDE_IGNORED, query.include_ignored());
options.set(SearchOptions::REGEX, query.is_regex());
options
}

View file

@ -98,6 +98,7 @@ pub enum IconName {
Plus,
Public,
Quote,
Regex,
Replace,
ReplaceAll,
ReplaceNext,
@ -196,6 +197,7 @@ impl IconName {
IconName::Plus => "icons/plus.svg",
IconName::Public => "icons/public.svg",
IconName::Quote => "icons/quote.svg",
IconName::Regex => "icons/regex.svg",
IconName::Replace => "icons/replace.svg",
IconName::ReplaceAll => "icons/replace_all.svg",
IconName::ReplaceNext => "icons/replace_next.svg",

View file

@ -1,7 +1,7 @@
use gpui::{rems, Length, Rems, WindowContext};
/// The base size of a rem, in pixels.
pub(crate) const BASE_REM_SIZE_IN_PX: f32 = 16.;
pub const BASE_REM_SIZE_IN_PX: f32 = 16.;
/// Returns a rem value derived from the provided pixel value and the base rem size (16px).
///

View file

@ -1,5 +1,5 @@
use gpui::{actions, impl_actions, ViewContext};
use search::{buffer_search, BufferSearchBar, SearchMode, SearchOptions};
use search::{buffer_search, BufferSearchBar, SearchOptions};
use serde_derive::Deserialize;
use workspace::{searchable::Direction, Workspace};
@ -115,7 +115,7 @@ fn search(workspace: &mut Workspace, action: &Search, cx: &mut ViewContext<Works
if query.is_empty() {
search_bar.set_replacement(None, cx);
search_bar.activate_search_mode(SearchMode::Regex, cx);
search_bar.set_search_options(SearchOptions::REGEX, cx);
}
vim.workspace_state.search = SearchState {
direction,
@ -228,7 +228,7 @@ pub fn move_to_internal(
pane.update(cx, |pane, cx| {
if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
let search = search_bar.update(cx, |search_bar, cx| {
let options = SearchOptions::CASE_SENSITIVE;
let options = SearchOptions::CASE_SENSITIVE | SearchOptions::REGEX;
if !search_bar.show(cx) {
return None;
}
@ -241,7 +241,6 @@ pub fn move_to_internal(
if whole_word {
query = format!(r"\b{}\b", query);
}
search_bar.activate_search_mode(SearchMode::Regex, cx);
Some(search_bar.search(&query, Some(options), cx))
});
@ -288,8 +287,11 @@ fn find_command(workspace: &mut Workspace, action: &FindCommand, cx: &mut ViewCo
query = search_bar.query(cx);
};
search_bar.activate_search_mode(SearchMode::Regex, cx);
Some(search_bar.search(&query, Some(SearchOptions::CASE_SENSITIVE), cx))
Some(search_bar.search(
&query,
Some(SearchOptions::CASE_SENSITIVE | SearchOptions::REGEX),
cx,
))
});
let Some(search) = search else { return };
let search_bar = search_bar.downgrade();
@ -326,7 +328,7 @@ fn replace_command(
return None;
}
let mut options = SearchOptions::default();
let mut options = SearchOptions::REGEX;
if replacement.is_case_sensitive {
options.set(SearchOptions::CASE_SENSITIVE, true)
}
@ -337,7 +339,6 @@ fn replace_command(
};
search_bar.set_replacement(Some(&replacement.replacement), cx);
search_bar.activate_search_mode(SearchMode::Regex, cx);
Some(search_bar.search(&search, Some(options), cx))
});
let Some(search) = search else { return };

View file

@ -55,6 +55,8 @@ pub trait SearchableItem: Item + EventEmitter<SearchEvent> {
}
}
fn search_bar_visibility_changed(&mut self, _visible: bool, _cx: &mut ViewContext<Self>) {}
fn clear_matches(&mut self, cx: &mut ViewContext<Self>);
fn update_matches(&mut self, matches: &[Self::Match], cx: &mut ViewContext<Self>);
fn query_suggestion(&mut self, cx: &mut ViewContext<Self>) -> String;
@ -131,6 +133,7 @@ pub trait SearchableItemHandle: ItemHandle {
matches: &AnyVec<dyn Send>,
cx: &mut WindowContext,
) -> Option<usize>;
fn search_bar_visibility_changed(&self, visible: bool, cx: &mut WindowContext);
}
impl<T: SearchableItem> SearchableItemHandle for View<T> {
@ -227,6 +230,12 @@ impl<T: SearchableItem> SearchableItemHandle for View<T> {
let mat = mat.downcast_ref().unwrap();
self.update(cx, |this, cx| this.replace(mat, query, cx))
}
fn search_bar_visibility_changed(&self, visible: bool, cx: &mut WindowContext) {
self.update(cx, |this, cx| {
this.search_bar_visibility_changed(visible, cx)
});
}
}
impl From<Box<dyn SearchableItemHandle>> for AnyView {