Improve outline panel performance (#17183)

Kirill Bulatov created

Part of https://github.com/zed-industries/zed/issues/14235

* moved search results highlight calculation into the background thread,
with highlight-less representation as a fallback
* show only a part of the line per search result, stop uniting them into
a single line if possible, always trim left trailing whitespaces
* highlight results in batches
* better cache all search result data, related to rendering
* add test infra and fix folding-related issues
* improve entry displays when multi buffer has a buffer search (find
references one has)
* fix cloud notes not showing search matches

Release Notes:

- Improved outline panel performance

Change summary

Cargo.lock                                | 295 ++++++-----
crates/outline_panel/Cargo.toml           |   5 
crates/outline_panel/src/outline_panel.rs | 623 +++++++++++++++++-------
crates/search/Cargo.toml                  |   8 
crates/search/src/project_search.rs       |  37 +
crates/workspace/src/pane.rs              |   2 
6 files changed, 631 insertions(+), 339 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -34,6 +34,12 @@ version = "1.0.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
 
+[[package]]
+name = "adler2"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
+
 [[package]]
 name = "aes"
 version = "0.8.4"
@@ -93,7 +99,7 @@ dependencies = [
  "miow",
  "parking_lot",
  "piper",
- "polling 3.7.2",
+ "polling 3.7.3",
  "regex-automata 0.4.7",
  "rustix-openpty",
  "serde",
@@ -123,12 +129,13 @@ checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f"
 
 [[package]]
 name = "alsa"
-version = "0.9.0"
+version = "0.9.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "37fe60779335388a88c01ac6c3be40304d1e349de3ada3b15f7808bb90fa9dce"
+checksum = "ed7572b7ba83a31e20d1b48970ee402d2e3e0537dcfe0a3ff4d6eb7508617d43"
 dependencies = [
  "alsa-sys",
  "bitflags 2.6.0",
+ "cfg-if",
  "libc",
 ]
 
@@ -284,7 +291,7 @@ checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.72",
+ "syn 2.0.76",
 ]
 
 [[package]]
@@ -535,7 +542,7 @@ checksum = "d7ebdfa2ebdab6b1760375fa7d6f382b9f486eac35fc994625a00e89280bdbb7"
 dependencies = [
  "async-task",
  "concurrent-queue",
- "fastrand 2.1.0",
+ "fastrand 2.1.1",
  "futures-lite 2.3.0",
  "slab",
 ]
@@ -571,7 +578,7 @@ checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c"
 dependencies = [
  "async-channel 2.3.1",
  "async-executor",
- "async-io 2.3.3",
+ "async-io 2.3.4",
  "async-lock 3.4.0",
  "blocking",
  "futures-lite 2.3.0",
@@ -600,9 +607,9 @@ dependencies = [
 
 [[package]]
 name = "async-io"
-version = "2.3.3"
+version = "2.3.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0d6baa8f0178795da0e71bc42c9e5d13261aac7ee549853162e66a241ba17964"
+checksum = "444b0228950ee6501b3568d3c93bf1176a1fdbc3b758dcd9475046d30f4dc7e8"
 dependencies = [
  "async-lock 3.4.0",
  "cfg-if",
@@ -610,11 +617,11 @@ dependencies = [
  "futures-io",
  "futures-lite 2.3.0",
  "parking",
- "polling 3.7.2",
- "rustix 0.38.34",
+ "polling 3.7.3",
+ "rustix 0.38.35",
  "slab",
  "tracing",
- "windows-sys 0.52.0",
+ "windows-sys 0.59.0",
 ]
 
 [[package]]
@@ -666,7 +673,7 @@ version = "2.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b948000fad4873c1c9339d60f2623323a0cfd3816e5181033c6a5cb68b2accf7"
 dependencies = [
- "async-io 2.3.3",
+ "async-io 2.3.4",
  "blocking",
  "futures-lite 2.3.0",
 ]
@@ -693,18 +700,18 @@ dependencies = [
  "cfg-if",
  "event-listener 3.1.0",
  "futures-lite 1.13.0",
- "rustix 0.38.34",
+ "rustix 0.38.35",
  "windows-sys 0.48.0",
 ]
 
 [[package]]
 name = "async-process"
-version = "2.2.3"
+version = "2.2.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f7eda79bbd84e29c2b308d1dc099d7de8dcc7035e48f4bf5dc4a531a44ff5e2a"
+checksum = "a8a07789659a4d385b79b18b9127fc27e1a59e1e89117c78c5ea3b806f016374"
 dependencies = [
  "async-channel 2.3.1",
- "async-io 2.3.3",
+ "async-io 2.3.4",
  "async-lock 3.4.0",
  "async-signal",
  "async-task",
@@ -712,9 +719,9 @@ dependencies = [
  "cfg-if",
  "event-listener 5.3.1",
  "futures-lite 2.3.0",
- "rustix 0.38.34",
+ "rustix 0.38.35",
  "tracing",
- "windows-sys 0.52.0",
+ "windows-sys 0.59.0",
 ]
 
 [[package]]
@@ -736,25 +743,25 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.72",
+ "syn 2.0.76",
 ]
 
 [[package]]
 name = "async-signal"
-version = "0.2.9"
+version = "0.2.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dfb3634b73397aa844481f814fad23bbf07fdb0eabec10f2eb95e58944b1ec32"
+checksum = "637e00349800c0bdf8bfc21ebbc0b6524abea702b0da4168ac00d070d0c0b9f3"
 dependencies = [
- "async-io 2.3.3",
+ "async-io 2.3.4",
  "async-lock 3.4.0",
  "atomic-waker",
  "cfg-if",
  "futures-core",
  "futures-io",
- "rustix 0.38.34",
+ "rustix 0.38.35",
  "signal-hook-registry",
  "slab",
- "windows-sys 0.52.0",
+ "windows-sys 0.59.0",
 ]
 
 [[package]]
@@ -804,7 +811,7 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.72",
+ "syn 2.0.76",
 ]
 
 [[package]]
@@ -856,7 +863,7 @@ checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.72",
+ "syn 2.0.76",
 ]
 
 [[package]]
@@ -1018,7 +1025,7 @@ dependencies = [
  "aws-smithy-types",
  "aws-types",
  "bytes 1.7.1",
- "fastrand 2.1.0",
+ "fastrand 2.1.1",
  "hex",
  "http 0.2.12",
  "ring",
@@ -1031,9 +1038,9 @@ dependencies = [
 
 [[package]]
 name = "aws-credential-types"
-version = "1.2.0"
+version = "1.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e16838e6c9e12125face1c1eff1343c75e3ff540de98ff7ebd61874a89bcfeb9"
+checksum = "60e8f6b615cb5fc60a98132268508ad104310f0cfb25a1c22eee76efdf9154da"
 dependencies = [
  "aws-smithy-async",
  "aws-smithy-runtime-api",
@@ -1043,20 +1050,21 @@ dependencies = [
 
 [[package]]
 name = "aws-runtime"
-version = "1.4.0"
+version = "1.4.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f42c2d4218de4dcd890a109461e2f799a1a2ba3bcd2cde9af88360f5df9266c6"
+checksum = "2424565416eef55906f9f8cece2072b6b6a76075e3ff81483ebe938a89a4c05f"
 dependencies = [
  "aws-credential-types",
  "aws-sigv4",
  "aws-smithy-async",
  "aws-smithy-eventstream",
  "aws-smithy-http",
+ "aws-smithy-runtime",
  "aws-smithy-runtime-api",
  "aws-smithy-types",
  "aws-types",
  "bytes 1.7.1",
- "fastrand 2.1.0",
+ "fastrand 2.1.1",
  "http 0.2.12",
  "http-body 0.4.6",
  "once_cell",
@@ -1068,9 +1076,9 @@ dependencies = [
 
 [[package]]
 name = "aws-sdk-s3"
-version = "1.46.0"
+version = "1.47.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4abf69a87be33b6f125a93d5046b5f7395c26d1f449bf8d3927f5577463b6de0"
+checksum = "cca49303c05d2a740b8a4552fac63a4db6ead84f7e7eeed04761fd3014c26f25"
 dependencies = [
  "ahash 0.8.11",
  "aws-credential-types",
@@ -1087,7 +1095,7 @@ dependencies = [
  "aws-smithy-xml",
  "aws-types",
  "bytes 1.7.1",
- "fastrand 2.1.0",
+ "fastrand 2.1.1",
  "hex",
  "hmac",
  "http 0.2.12",
@@ -1103,9 +1111,9 @@ dependencies = [
 
 [[package]]
 name = "aws-sdk-sso"
-version = "1.37.0"
+version = "1.40.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1074e818fbe4f9169242d78448b15be8916a79daa38ea1231f2e2e10d993fcd2"
+checksum = "e5879bec6e74b648ce12f6085e7245417bc5f6d672781028384d2e494be3eb6d"
 dependencies = [
  "aws-credential-types",
  "aws-runtime",
@@ -1125,9 +1133,9 @@ dependencies = [
 
 [[package]]
 name = "aws-sdk-ssooidc"
-version = "1.38.0"
+version = "1.41.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "29755c51e33fa3f678598f64324a169cf4b7d3c4865d2709d4308f53366a92a4"
+checksum = "4ef4cd9362f638c22a3b959fd8df292e7e47fdf170270f86246b97109b5f2f7d"
 dependencies = [
  "aws-credential-types",
  "aws-runtime",
@@ -1147,9 +1155,9 @@ dependencies = [
 
 [[package]]
 name = "aws-sdk-sts"
-version = "1.37.0"
+version = "1.40.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6e52dc3fd7dfa6c01a69cf3903e00aa467261639138a05b06cd92314d2c8fb07"
+checksum = "0b1e2735d2ab28b35ecbb5496c9d41857f52a0d6a0075bbf6a8af306045ea6f6"
 dependencies = [
  "aws-credential-types",
  "aws-runtime",
@@ -1242,9 +1250,9 @@ dependencies = [
 
 [[package]]
 name = "aws-smithy-http"
-version = "0.60.9"
+version = "0.60.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d9cd0ae3d97daa0a2bf377a4d8e8e1362cae590c4a1aad0d40058ebca18eb91e"
+checksum = "01dbcb6e2588fd64cfb6d7529661b06466419e4c54ed1c62d6510d2d0350a728"
 dependencies = [
  "aws-smithy-eventstream",
  "aws-smithy-runtime-api",
@@ -1282,16 +1290,16 @@ dependencies = [
 
 [[package]]
 name = "aws-smithy-runtime"
-version = "1.6.3"
+version = "1.7.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0abbf454960d0db2ad12684a1640120e7557294b0ff8e2f11236290a1b293225"
+checksum = "d1ce695746394772e7000b39fe073095db6d45a862d0767dd5ad0ac0d7f8eb87"
 dependencies = [
  "aws-smithy-async",
  "aws-smithy-http",
  "aws-smithy-runtime-api",
  "aws-smithy-types",
  "bytes 1.7.1",
- "fastrand 2.1.0",
+ "fastrand 2.1.1",
  "h2",
  "http 0.2.12",
  "http-body 0.4.6",
@@ -1326,9 +1334,9 @@ dependencies = [
 
 [[package]]
 name = "aws-smithy-types"
-version = "1.2.2"
+version = "1.2.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6cee7cadb433c781d3299b916fbf620fea813bf38f49db282fb6858141a05cc8"
+checksum = "273dcdfd762fae3e1650b8024624e7cd50e484e37abdab73a7a706188ad34543"
 dependencies = [
  "base64-simd",
  "bytes 1.7.1",
@@ -1457,8 +1465,8 @@ dependencies = [
  "cc",
  "cfg-if",
  "libc",
- "miniz_oxide",
- "object 0.36.2",
+ "miniz_oxide 0.7.4",
+ "object 0.36.4",
  "rustc-demangle",
 ]
 
@@ -1541,7 +1549,7 @@ dependencies = [
  "regex",
  "rustc-hash",
  "shlex",
- "syn 2.0.72",
+ "syn 2.0.76",
  "which 4.4.2",
 ]
 
@@ -1562,7 +1570,7 @@ dependencies = [
  "regex",
  "rustc-hash",
  "shlex",
- "syn 2.0.72",
+ "syn 2.0.76",
 ]
 
 [[package]]
@@ -1618,9 +1626,9 @@ dependencies = [
 
 [[package]]
 name = "bitstream-io"
-version = "2.5.0"
+version = "2.5.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3dcde5f311c85b8ca30c2e4198d4326bc342c76541590106f5fa4a50946ea499"
+checksum = "b81e1519b0d82120d2fd469d5bfb2919a9361c48b02d82d04befc1cdd2002452"
 
 [[package]]
 name = "bitvec"
@@ -1671,7 +1679,7 @@ source = "git+https://github.com/kvark/blade?rev=b37a9a994709d256f4634efd29281c7
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.72",
+ "syn 2.0.76",
 ]
 
 [[package]]
@@ -1742,7 +1750,7 @@ dependencies = [
  "proc-macro-crate",
  "proc-macro2",
  "quote",
- "syn 2.0.72",
+ "syn 2.0.76",
  "syn_derive",
 ]
 
@@ -1812,22 +1820,22 @@ dependencies = [
 
 [[package]]
 name = "bytemuck"
-version = "1.16.3"
+version = "1.17.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "102087e286b4677862ea56cf8fc58bb2cdfa8725c40ffb80fe3a008eb7f2fc83"
+checksum = "773d90827bc3feecfb67fab12e24de0749aad83c74b9504ecde46237b5cd24e2"
 dependencies = [
  "bytemuck_derive",
 ]
 
 [[package]]
 name = "bytemuck_derive"
-version = "1.7.0"
+version = "1.7.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1ee891b04274a59bd38b412188e24b849617b2e45a0fd8d057deb63e7403761b"
+checksum = "0cc8b54b395f2fcfbb3d90c47b01c7f444d94d05bdeb775811dec868ac3bbc26"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.72",
+ "syn 2.0.76",
 ]
 
 [[package]]
@@ -1900,8 +1908,8 @@ checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec"
 dependencies = [
  "bitflags 2.6.0",
  "log",
- "polling 3.7.2",
- "rustix 0.38.34",
+ "polling 3.7.3",
+ "rustix 0.38.35",
  "slab",
  "thiserror",
 ]
@@ -1913,16 +1921,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20"
 dependencies = [
  "calloop",
- "rustix 0.38.34",
+ "rustix 0.38.35",
  "wayland-backend",
  "wayland-client",
 ]
 
 [[package]]
 name = "camino"
-version = "1.1.7"
+version = "1.1.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e0ec6b951b160caa93cc0c7b209e5a3bff7aae9062213451ac99493cd844c239"
+checksum = "8b96ec4966b5813e2c0507c1f86115c8c5abaadc3980879c3424042a02fd1ad3"
 dependencies = [
  "serde",
 ]
@@ -1947,7 +1955,7 @@ checksum = "f83ae11f116bcbafc5327c6af250341db96b5930046732e1905f7dc65887e0e1"
 dependencies = [
  "cap-primitives",
  "cap-std",
- "rustix 0.38.34",
+ "rustix 0.38.35",
  "smallvec",
 ]
 
@@ -1963,7 +1971,7 @@ dependencies = [
  "io-lifetimes 2.0.3",
  "ipnet",
  "maybe-owned",
- "rustix 0.38.34",
+ "rustix 0.38.35",
  "windows-sys 0.52.0",
  "winx",
 ]
@@ -1987,7 +1995,7 @@ dependencies = [
  "cap-primitives",
  "io-extras",
  "io-lifetimes 2.0.3",
- "rustix 0.38.34",
+ "rustix 0.38.35",
 ]
 
 [[package]]
@@ -2000,7 +2008,7 @@ dependencies = [
  "cap-primitives",
  "iana-time-zone",
  "once_cell",
- "rustix 0.38.34",
+ "rustix 0.38.35",
  "winx",
 ]
 
@@ -2078,12 +2086,13 @@ dependencies = [
 
 [[package]]
 name = "cc"
-version = "1.1.7"
+version = "1.1.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "26a5c3fd7bfa1ce3897a3a3501d362b2d87b7f2583ebcb4a949ec25911025cbc"
+checksum = "57b6a275aa2903740dc87da01c62040406b8812552e97129a63ea8850a17c6e6"
 dependencies = [
  "jobserver",
  "libc",
+ "shlex",
 ]
 
 [[package]]
@@ -2247,9 +2256,9 @@ dependencies = [
 
 [[package]]
 name = "clap_complete"
-version = "4.5.23"
+version = "4.5.24"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "531d7959c5bbb6e266cecdd0f20213639c3a5c3e4d615f97db87661745f781ff"
+checksum = "6d7db6eca8c205649e8d3ccd05aa5042b1800a784e56bc7c43524fde8abbfa9b"
 dependencies = [
  "clap",
 ]
@@ -2263,7 +2272,7 @@ dependencies = [
  "heck 0.5.0",
  "proc-macro2",
  "quote",
- "syn 2.0.72",
+ "syn 2.0.76",
 ]
 
 [[package]]
@@ -2901,18 +2910,18 @@ dependencies = [
 
 [[package]]
 name = "cpp_demangle"
-version = "0.4.3"
+version = "0.4.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7e8227005286ec39567949b33df9896bcadfa6051bccca2488129f108ca23119"
+checksum = "96e58d342ad113c2b878f16d5d034c03be492ae460cdbc02b7f0f2284d310c7d"
 dependencies = [
  "cfg-if",
 ]
 
 [[package]]
 name = "cpufeatures"
-version = "0.2.12"
+version = "0.2.13"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504"
+checksum = "51e852e6dc9a5bed1fae92dd2375037bf2b768725bf3be87811edee3249d09ad"
 dependencies = [
  "libc",
 ]
@@ -3185,7 +3194,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "edb49164822f3ee45b17acd4a208cfc1251410cf0cad9a833234c9890774dd9f"
 dependencies = [
  "quote",
- "syn 2.0.72",
+ "syn 2.0.76",
 ]
 
 [[package]]
@@ -3365,7 +3374,7 @@ dependencies = [
  "proc-macro2",
  "quote",
  "rustc_version",
- "syn 2.0.72",
+ "syn 2.0.76",
 ]
 
 [[package]]
@@ -3537,9 +3546,9 @@ dependencies = [
 
 [[package]]
 name = "dwrote"
-version = "0.11.0"
+version = "0.11.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "439a1c2ba5611ad3ed731280541d36d2e9c4ac5e7fb818a27b604bdc5a6aa65b"
+checksum = "2da3498378ed373237bdef1eddcc64e7be2d3ba4841f4c22a998e81cadeea83c"
 dependencies = [
  "lazy_static",
  "libc",
@@ -3685,6 +3694,12 @@ version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced"
 
+[[package]]
+name = "embedded-io"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d"
+
 [[package]]
 name = "emojis"
 version = "0.6.3"
@@ -3733,7 +3748,7 @@ checksum = "de0d48a183585823424a4ce1aa132d174a6a81bd540895822eb4c8373a8e49e8"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.72",
+ "syn 2.0.76",
 ]
 
 [[package]]
@@ -3838,9 +3853,9 @@ dependencies = [
 
 [[package]]
 name = "euclid"
-version = "0.22.10"
+version = "0.22.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e0f0eb73b934648cd7a4a61f1b15391cd95dab0b4da6e2e66c2a072c144b4a20"
+checksum = "ad9cdb4b747e485a12abb0e6566612956c7a1bafa3bdb8d682c5b6d403589e48"
 dependencies = [
  "num-traits",
 ]
@@ -3903,7 +3918,7 @@ dependencies = [
  "flume",
  "half",
  "lebe",
- "miniz_oxide",
+ "miniz_oxide 0.7.4",
  "rayon-core",
  "smallvec",
  "zune-inflate",
@@ -4042,9 +4057,9 @@ dependencies = [
 
 [[package]]
 name = "fastrand"
-version = "2.1.0"
+version = "2.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a"
+checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6"
 
 [[package]]
 name = "fd-lock"
@@ -4053,7 +4068,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7e5768da2206272c81ef0b5e951a41862938a6070da63bcea197899942d3b947"
 dependencies = [
  "cfg-if",
- "rustix 0.38.34",
+ "rustix 0.38.35",
  "windows-sys 0.52.0",
 ]
 
@@ -4165,14 +4180,14 @@ dependencies = [
 
 [[package]]
 name = "filetime"
-version = "0.2.23"
+version = "0.2.25"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1ee447700ac8aa0b2f2bd7bc4462ad686ba06baa6727ac149a2d6277f0d240fd"
+checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586"
 dependencies = [
  "cfg-if",
  "libc",
- "redox_syscall 0.4.1",
- "windows-sys 0.52.0",
+ "libredox",
+ "windows-sys 0.59.0",
 ]
 
 [[package]]
@@ -4183,12 +4198,12 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
 
 [[package]]
 name = "flate2"
-version = "1.0.31"
+version = "1.0.33"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7f211bbe8e69bbd0cfdea405084f128ae8b4aaa6b0b522fc8f2b009084797920"
+checksum = "324a1be68054ef05ad64b861cc9eaf1d623d2d8cb25b4bf2cb9cdd902b4bf253"
 dependencies = [
  "crc32fast",
- "miniz_oxide",
+ "miniz_oxide 0.8.0",
 ]
 
 [[package]]
@@ -4304,7 +4319,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.72",
+ "syn 2.0.76",
 ]
 
 [[package]]
@@ -4386,7 +4401,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "033b337d725b97690d86893f9de22b67b80dcc4e9ad815f348254c38119db8fb"
 dependencies = [
  "io-lifetimes 2.0.3",
- "rustix 0.38.34",
+ "rustix 0.38.35",
  "windows-sys 0.52.0",
 ]
 
@@ -4532,7 +4547,7 @@ version = "2.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "52527eb5074e35e9339c6b4e8d12600c7128b68fb25dcb9fa9dec18f7c25f3a5"
 dependencies = [
- "fastrand 2.1.0",
+ "fastrand 2.1.1",
  "futures-core",
  "futures-io",
  "parking",
@@ -4547,7 +4562,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.72",
+ "syn 2.0.76",
 ]
 
 [[package]]
@@ -4666,7 +4681,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253"
 dependencies = [
  "fallible-iterator",
- "indexmap 2.3.0",
+ "indexmap 2.4.0",
  "stable_deref_trait",
 ]
 
@@ -4972,7 +4987,7 @@ dependencies = [
  "futures-sink",
  "futures-util",
  "http 0.2.12",
- "indexmap 2.3.0",
+ "indexmap 2.4.0",
  "slab",
  "tokio",
  "tokio-util",
@@ -5241,7 +5256,7 @@ dependencies = [
  "markup5ever",
  "proc-macro2",
  "quote",
- "syn 2.0.72",
+ "syn 2.0.76",
 ]
 
 [[package]]
@@ -5595,9 +5610,9 @@ dependencies = [
 
 [[package]]
 name = "indexmap"
-version = "2.3.0"
+version = "2.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "de3fc2e30ba82dd1b3911c8de1ffc143c74a914a14e99514d7637e3099df5ea0"
+checksum = "93ead53efc7ea8ed3cfb0c79fc8023fbb782a5432b52830b6518941cebe6505c"
 dependencies = [
  "equivalent",
  "hashbrown 0.14.5",
@@ -5624,7 +5639,7 @@ checksum = "0122b7114117e64a63ac49f752a5ca4624d534c7b1c7de796ac196381cd2d947"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.72",
+ "syn 2.0.76",
 ]
 
 [[package]]
@@ -5709,7 +5724,7 @@ checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.72",
+ "syn 2.0.76",
 ]
 
 [[package]]
@@ -5759,7 +5774,7 @@ dependencies = [
  "fnv",
  "lazy_static",
  "libc",
- "mio 1.0.1",
+ "mio 1.0.2",
  "rand 0.8.5",
  "serde",
  "tempfile",
@@ -5784,11 +5799,11 @@ dependencies = [
 
 [[package]]
 name = "is-terminal"
-version = "0.4.12"
+version = "0.4.13"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f23ff5ef2b80d608d61efee834934d862cd92461afc0560dedf493e4c033738b"
+checksum = "261f68e344040fbd0edea105bef17c66edf46f984ddb1115b775ce31be948f4b"
 dependencies = [
- "hermit-abi 0.3.9",
+ "hermit-abi 0.4.0",
  "libc",
  "windows-sys 0.52.0",
 ]
@@ -5924,9 +5939,9 @@ checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0"
 
 [[package]]
 name = "js-sys"
-version = "0.3.69"
+version = "0.3.70"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d"
+checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a"
 dependencies = [
  "wasm-bindgen",
 ]
@@ -6285,6 +6300,7 @@ checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
 dependencies = [
  "bitflags 2.6.0",
  "libc",
+ "redox_syscall 0.5.3",
 ]
 
 [[package]]
@@ -6300,9 +6316,9 @@ dependencies = [
 
 [[package]]
 name = "libz-sys"
-version = "1.1.18"
+version = "1.1.20"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c15da26e5af7e25c90b37a2d75cdbf940cf4a55316de9d84c679c9b8bfabf82e"
+checksum = "d2d16453e800a8cf6dd2fc3eb4bc99b786a9b90c663b8559a5b1a041bf89e472"
 dependencies = [
  "cc",
  "libc",
@@ -6336,7 +6352,7 @@ checksum = "cb26336e6dc7cc76e7927d2c9e7e3bb376d7af65a6f56a0b16c47d18a9b1abc5"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.72",
+ "syn 2.0.76",
 ]
 
 [[package]]
@@ -6689,7 +6705,7 @@ version = "0.6.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b2cffa4ad52c6f791f4f8b15f0c05f9824b2ced1160e88cc393d64fff9a8ac64"
 dependencies = [
- "rustix 0.38.34",
+ "rustix 0.38.35",
 ]
 
 [[package]]
@@ -6774,6 +6790,15 @@ dependencies = [
  "simd-adler32",
 ]
 
+[[package]]
+name = "miniz_oxide"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1"
+dependencies = [
+ "adler2",
+]
+
 [[package]]
 name = "mint"
 version = "0.5.9"
@@ -6794,9 +6819,9 @@ dependencies = [
 
 [[package]]
 name = "mio"
-version = "1.0.1"
+version = "1.0.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4569e456d394deccd22ce1c1913e6ea0e54519f577285001215d33557431afe4"
+checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec"
 dependencies = [
  "hermit-abi 0.3.9",
  "libc",
@@ -6858,7 +6883,7 @@ dependencies = [
  "cfg_aliases 0.1.1",
  "codespan-reporting",
  "hexf-parse",
- "indexmap 2.3.0",
+ "indexmap 2.4.0",
  "log",
  "rustc-hash",
  "spirv",
@@ -7142,7 +7167,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.72",
+ "syn 2.0.76",
 ]
 
 [[package]]
@@ -7224,7 +7249,7 @@ dependencies = [
  "proc-macro-crate",
  "proc-macro2",
  "quote",
- "syn 2.0.72",
+ "syn 2.0.76",
 ]
 
 [[package]]
@@ -7269,15 +7294,15 @@ checksum = "d8dd6c0cdf9429bce006e1362bfce61fa1bfd8c898a643ed8d2b471934701d3d"
 dependencies = [
  "crc32fast",
  "hashbrown 0.14.5",
- "indexmap 2.3.0",
+ "indexmap 2.4.0",
  "memchr",
 ]
 
 [[package]]
 name = "object"
-version = "0.36.2"
+version = "0.36.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3f203fa8daa7bb185f760ae12bd8e097f63d17041dcdcaf675ac54cdf863170e"
+checksum = "084f1a5821ac4c651660a94a7153d27ac9d8a53736203f58b31945ded098070a"
 dependencies = [
  "memchr",
 ]
@@ -7332,7 +7357,7 @@ checksum = "8fc6ce4692fbfd044ce22ca07dcab1a30fa12432ca2aa5b1294eca50d3332a24"
 dependencies = [
  "aes",
  "async-fs 2.1.2",
- "async-io 2.3.3",
+ "async-io 2.3.4",
  "async-lock 3.4.0",
  "async-net 2.0.0",
  "blocking",
@@ -7423,7 +7448,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.72",
+ "syn 2.0.76",
 ]
 
 [[package]]
@@ -7509,7 +7534,7 @@ dependencies = [
  "proc-macro-error",
  "proc-macro2",
  "quote",
- "syn 2.0.72",
+ "syn 2.0.76",
 ]
 
 [[package]]
@@ -7551,12 +7576,14 @@ dependencies = [
  "language",
  "log",
  "menu",
+ "pretty_assertions",
  "project",
  "schemars",
  "search",
  "serde",
  "serde_json",
  "settings",
+ "smol",
  "theme",
  "util",
  "workspace",
@@ -7616,7 +7643,7 @@ dependencies = [
  "by_address",
  "proc-macro2",
  "quote",
- "syn 2.0.72",
+ "syn 2.0.76",
 ]
 
 [[package]]
@@ -7807,7 +7834,7 @@ dependencies = [
  "pest_meta",
  "proc-macro2",
  "quote",
- "syn 2.0.72",
+ "syn 2.0.76",
 ]
 
 [[package]]
@@ -7828,7 +7855,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db"
 dependencies = [
  "fixedbitset",
- "indexmap 2.3.0",
+ "indexmap 2.4.0",
 ]
 
 [[package]]
@@ -7881,7 +7908,7 @@ dependencies = [
  "phf_shared 0.11.2",
  "proc-macro2",
  "quote",
- "syn 2.0.72",
+ "syn 2.0.76",
 ]
 
 [[package]]
@@ -7941,7 +7968,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.72",
+ "syn 2.0.76",
 ]
 
 [[package]]

crates/outline_panel/Cargo.toml 🔗

@@ -30,10 +30,15 @@ search.workspace = true
 serde.workspace = true
 serde_json.workspace = true
 settings.workspace = true
+smol.workspace = true
 theme.workspace = true
 util.workspace = true
 worktree.workspace = true
 workspace.workspace = true
 
+[dev-dependencies]
+search = { workspace = true, features = ["test-support"] }
+pretty_assertions.workspace = true
+
 [package.metadata.cargo-machete]
 ignored = ["log"]

crates/outline_panel/src/outline_panel.rs 🔗

@@ -3,9 +3,10 @@ mod outline_panel_settings;
 use std::{
     cell::OnceCell,
     cmp,
+    hash::Hash,
     ops::Range,
     path::{Path, PathBuf},
-    sync::{atomic::AtomicBool, Arc},
+    sync::{atomic::AtomicBool, Arc, OnceLock},
     time::Duration,
     u32,
 };
@@ -16,7 +17,7 @@ use db::kvp::KEY_VALUE_STORE;
 use editor::{
     display_map::ToDisplayPoint,
     items::{entry_git_aware_label_color, entry_label_color},
-    scroll::{Autoscroll, ScrollAnchor},
+    scroll::{Autoscroll, AutoscrollStrategy, ScrollAnchor},
     AnchorRangeExt, Bias, DisplayPoint, Editor, EditorEvent, EditorMode, ExcerptId, ExcerptRange,
     MultiBufferSnapshot, RangeToAnchorExt,
 };
@@ -39,8 +40,9 @@ use project::{File, Fs, Item, Project};
 use search::{BufferSearchBar, ProjectSearchView};
 use serde::{Deserialize, Serialize};
 use settings::{Settings, SettingsStore};
+use smol::channel;
 use theme::SyntaxTheme;
-use util::{RangeExt, ResultExt, TryFutureExt};
+use util::{debug_panic, RangeExt, ResultExt, TryFutureExt};
 use workspace::{
     dock::{DockPosition, Panel, PanelEvent},
     item::ItemHandle,
@@ -50,7 +52,7 @@ use workspace::{
         HighlightedLabel, Icon, IconButton, IconButtonShape, IconName, IconSize, Label,
         LabelCommon, ListItem, Selectable, Spacing, StyledExt, StyledTypography, Tooltip,
     },
-    OpenInTerminal, Workspace,
+    OpenInTerminal, WeakItemHandle, Workspace,
 };
 use worktree::{Entry, ProjectEntryId, WorktreeId};
 
@@ -83,6 +85,7 @@ const OUTLINE_PANEL_KEY: &str = "OutlinePanel";
 const UPDATE_DEBOUNCE: Duration = Duration::from_millis(50);
 
 type Outline = OutlineItem<language::Anchor>;
+type HighlightStyleData = Arc<OnceLock<Vec<(Range<usize>, HighlightStyle)>>>;
 
 pub struct OutlinePanel {
     fs: Arc<dyn Fs>,
@@ -112,8 +115,140 @@ pub struct OutlinePanel {
     cached_entries: Vec<CachedEntry>,
     filter_editor: View<Editor>,
     mode: ItemsDisplayMode,
-    search: Option<(SearchKind, String)>,
-    search_matches: Vec<Range<editor::Anchor>>,
+}
+
+enum ItemsDisplayMode {
+    Search(SearchState),
+    Outline,
+}
+
+struct SearchState {
+    kind: SearchKind,
+    query: String,
+    matches: Vec<(Range<editor::Anchor>, OnceCell<Arc<SearchData>>)>,
+    highlight_search_match_tx: channel::Sender<HighlightArguments>,
+    _search_match_highlighter: Task<()>,
+    _search_match_notify: Task<()>,
+}
+
+struct HighlightArguments {
+    multi_buffer_snapshot: MultiBufferSnapshot,
+    search_data: Arc<SearchData>,
+}
+
+impl SearchState {
+    fn new(
+        kind: SearchKind,
+        query: String,
+        new_matches: Vec<Range<editor::Anchor>>,
+        theme: Arc<SyntaxTheme>,
+        cx: &mut ViewContext<'_, OutlinePanel>,
+    ) -> Self {
+        let (highlight_search_match_tx, highlight_search_match_rx) = channel::unbounded();
+        let (notify_tx, notify_rx) = channel::bounded::<()>(1);
+        Self {
+            kind,
+            query,
+            matches: new_matches
+                .into_iter()
+                .map(|range| (range, OnceCell::new()))
+                .collect(),
+            highlight_search_match_tx,
+            _search_match_highlighter: cx.background_executor().spawn(async move {
+                while let Some(highlight_arguments) = highlight_search_match_rx.recv().await.ok() {
+                    let highlight_data = &highlight_arguments.search_data.highlights_data;
+                    if highlight_data.get().is_some() {
+                        continue;
+                    }
+                    let mut left_whitespaces_count = 0;
+                    let mut non_whitespace_symbol_occurred = false;
+                    let context_offset_range = highlight_arguments
+                        .search_data
+                        .context_range
+                        .to_offset(&highlight_arguments.multi_buffer_snapshot);
+                    let mut offset = context_offset_range.start;
+                    let mut context_text = String::new();
+                    let mut highlight_ranges = Vec::new();
+                    for mut chunk in highlight_arguments
+                        .multi_buffer_snapshot
+                        .chunks(context_offset_range.start..context_offset_range.end, true)
+                    {
+                        if !non_whitespace_symbol_occurred {
+                            for c in chunk.text.chars() {
+                                if c.is_whitespace() {
+                                    left_whitespaces_count += c.len_utf8();
+                                } else {
+                                    non_whitespace_symbol_occurred = true;
+                                    break;
+                                }
+                            }
+                        }
+
+                        if chunk.text.len() > context_offset_range.end - offset {
+                            chunk.text = &chunk.text[0..(context_offset_range.end - offset)];
+                            offset = context_offset_range.end;
+                        } else {
+                            offset += chunk.text.len();
+                        }
+                        let style = chunk
+                            .syntax_highlight_id
+                            .and_then(|highlight| highlight.style(&theme));
+                        if let Some(style) = style {
+                            let start = context_text.len();
+                            let end = start + chunk.text.len();
+                            highlight_ranges.push((start..end, style));
+                        }
+                        context_text.push_str(chunk.text);
+                        if offset >= context_offset_range.end {
+                            break;
+                        }
+                    }
+
+                    highlight_ranges.iter_mut().for_each(|(range, _)| {
+                        range.start = range.start.saturating_sub(left_whitespaces_count);
+                        range.end = range.end.saturating_sub(left_whitespaces_count);
+                    });
+                    if highlight_data.set(highlight_ranges).ok().is_some() {
+                        notify_tx.try_send(()).ok();
+                    }
+
+                    let trimmed_text = context_text[left_whitespaces_count..].to_owned();
+                    debug_assert_eq!(
+                        trimmed_text, highlight_arguments.search_data.context_text,
+                        "Highlighted text that does not match the buffer text"
+                    );
+                }
+            }),
+            _search_match_notify: cx.spawn(|outline_panel, mut cx| async move {
+                while let Some(()) = notify_rx.recv().await.ok() {
+                    let update_result = outline_panel.update(&mut cx, |_, cx| {
+                        cx.notify();
+                    });
+                    if update_result.is_err() {
+                        break;
+                    }
+                }
+            }),
+        }
+    }
+
+    fn highlight_search_match(
+        &mut self,
+        match_range: &Range<editor::Anchor>,
+        multi_buffer_snapshot: &MultiBufferSnapshot,
+    ) {
+        if let Some((_, search_data)) = self.matches.iter().find(|(range, _)| range == match_range)
+        {
+            let search_data = search_data
+                .get_or_init(|| Arc::new(SearchData::new(match_range, multi_buffer_snapshot)));
+            self.highlight_search_match_tx
+                .send_blocking(HighlightArguments {
+                    multi_buffer_snapshot: multi_buffer_snapshot.clone(),
+                    search_data: Arc::clone(search_data),
+                })
+                .ok();
+        }
+    }
 }
 
 #[derive(Debug)]
@@ -137,12 +272,6 @@ impl SelectedEntry {
     }
 }
 
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-enum ItemsDisplayMode {
-    Search,
-    Outline,
-}
-
 #[derive(Debug, Clone, Copy, Default)]
 struct FsChildren {
     files: usize,
@@ -218,12 +347,11 @@ enum PanelEntry {
 #[derive(Clone, Debug)]
 struct SearchEntry {
     match_range: Range<editor::Anchor>,
-    same_line_matches: Vec<Range<editor::Anchor>>,
     kind: SearchKind,
-    render_data: Option<OnceCell<SearchData>>,
+    render_data: Arc<SearchData>,
 }
 
-#[derive(Copy, Clone, Debug, PartialEq, Eq)]
+#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
 enum SearchKind {
     Project,
     Buffer,
@@ -233,8 +361,10 @@ enum SearchKind {
 struct SearchData {
     context_range: Range<editor::Anchor>,
     context_text: String,
-    highlight_ranges: Vec<(Range<usize>, HighlightStyle)>,
+    truncated_left: bool,
+    truncated_right: bool,
     search_match_indices: Vec<Range<usize>>,
+    highlights_data: HighlightStyleData,
 }
 
 impl PartialEq for PanelEntry {
@@ -262,87 +392,103 @@ impl PartialEq for PanelEntry {
 
 impl Eq for PanelEntry {}
 
+const SEARCH_MATCH_CONTEXT_SIZE: u32 = 40;
+const TRUNCATED_CONTEXT_MARK: &str = "…";
+
 impl SearchData {
     fn new(
-        kind: SearchKind,
         match_range: &Range<editor::Anchor>,
         multi_buffer_snapshot: &MultiBufferSnapshot,
-        theme: &SyntaxTheme,
     ) -> Self {
         let match_point_range = match_range.to_point(&multi_buffer_snapshot);
-        let entire_row_range_start = language::Point::new(match_point_range.start.row, 0);
-        let entire_row_range_end = multi_buffer_snapshot.clip_point(
-            language::Point::new(match_point_range.end.row, u32::MAX),
+        let context_left_border = multi_buffer_snapshot.clip_point(
+            language::Point::new(
+                match_point_range.start.row,
+                match_point_range
+                    .start
+                    .column
+                    .saturating_sub(SEARCH_MATCH_CONTEXT_SIZE),
+            ),
+            Bias::Left,
+        );
+        let context_right_border = multi_buffer_snapshot.clip_point(
+            language::Point::new(
+                match_point_range.end.row,
+                match_point_range.end.column + SEARCH_MATCH_CONTEXT_SIZE,
+            ),
             Bias::Right,
         );
-        let entire_row_range =
-            (entire_row_range_start..entire_row_range_end).to_anchors(&multi_buffer_snapshot);
-        let entire_row_offset_range = entire_row_range.to_offset(&multi_buffer_snapshot);
+
+        let context_anchor_range =
+            (context_left_border..context_right_border).to_anchors(&multi_buffer_snapshot);
+        let context_offset_range = context_anchor_range.to_offset(&multi_buffer_snapshot);
         let match_offset_range = match_range.to_offset(&multi_buffer_snapshot);
+
         let mut search_match_indices = vec![
-            match_offset_range.start - entire_row_offset_range.start
-                ..match_offset_range.end - entire_row_offset_range.start,
+            multi_buffer_snapshot.clip_offset(
+                match_offset_range.start - context_offset_range.start,
+                Bias::Left,
+            )
+                ..multi_buffer_snapshot.clip_offset(
+                    match_offset_range.end - context_offset_range.start,
+                    Bias::Right,
+                ),
         ];
 
-        let mut left_whitespaces_count = 0;
-        let mut non_whitespace_symbol_occurred = false;
-        let mut offset = entire_row_offset_range.start;
-        let mut entire_row_text = String::new();
-        let mut highlight_ranges = Vec::new();
-        for mut chunk in multi_buffer_snapshot.chunks(
-            entire_row_offset_range.start..entire_row_offset_range.end,
-            true,
-        ) {
-            if !non_whitespace_symbol_occurred {
-                for c in chunk.text.chars() {
-                    if c.is_whitespace() {
-                        left_whitespaces_count += 1;
-                    } else {
-                        non_whitespace_symbol_occurred = true;
-                        break;
-                    }
-                }
-            }
-
-            if chunk.text.len() > entire_row_offset_range.end - offset {
-                chunk.text = &chunk.text[0..(entire_row_offset_range.end - offset)];
-                offset = entire_row_offset_range.end;
-            } else {
-                offset += chunk.text.len();
-            }
-            let style = chunk
-                .syntax_highlight_id
-                .and_then(|highlight| highlight.style(theme));
-            if let Some(style) = style {
-                let start = entire_row_text.len();
-                let end = start + chunk.text.len();
-                highlight_ranges.push((start..end, style));
-            }
-            entire_row_text.push_str(chunk.text);
-            if offset >= entire_row_offset_range.end {
-                break;
-            }
-        }
-
-        if let SearchKind::Buffer = kind {
-            left_whitespaces_count = 0;
-        }
-        highlight_ranges.iter_mut().for_each(|(range, _)| {
-            range.start = range.start.saturating_sub(left_whitespaces_count);
-            range.end = range.end.saturating_sub(left_whitespaces_count);
-        });
+        let entire_context_text = multi_buffer_snapshot
+            .text_for_range(context_offset_range.clone())
+            .collect::<String>();
+        let left_whitespaces_offset = entire_context_text
+            .chars()
+            .take_while(|c| c.is_whitespace())
+            .map(|c| c.len_utf8())
+            .sum::<usize>();
+
+        let mut extended_context_left_border = context_left_border;
+        extended_context_left_border.column = extended_context_left_border.column.saturating_sub(1);
+        let extended_context_left_border =
+            multi_buffer_snapshot.clip_point(extended_context_left_border, Bias::Left);
+        let mut extended_context_right_border = context_right_border;
+        extended_context_right_border.column += 1;
+        let extended_context_right_border =
+            multi_buffer_snapshot.clip_point(extended_context_right_border, Bias::Right);
+
+        let truncated_left = left_whitespaces_offset == 0
+            && extended_context_left_border < context_left_border
+            && multi_buffer_snapshot
+                .chars_at(extended_context_left_border)
+                .last()
+                .map_or(false, |c| !c.is_whitespace());
+        let truncated_right = entire_context_text
+            .chars()
+            .last()
+            .map_or(true, |c| !c.is_whitespace())
+            && extended_context_right_border > context_right_border
+            && multi_buffer_snapshot
+                .chars_at(extended_context_right_border)
+                .next()
+                .map_or(false, |c| !c.is_whitespace());
         search_match_indices.iter_mut().for_each(|range| {
-            range.start = range.start.saturating_sub(left_whitespaces_count);
-            range.end = range.end.saturating_sub(left_whitespaces_count);
+            range.start = multi_buffer_snapshot.clip_offset(
+                range.start.saturating_sub(left_whitespaces_offset),
+                Bias::Left,
+            );
+            range.end = multi_buffer_snapshot.clip_offset(
+                range.end.saturating_sub(left_whitespaces_offset),
+                Bias::Right,
+            );
         });
+
         let trimmed_row_offset_range =
-            entire_row_offset_range.start + left_whitespaces_count..entire_row_offset_range.end;
-        let trimmed_text = entire_row_text[left_whitespaces_count..].to_owned();
+            context_offset_range.start + left_whitespaces_offset..context_offset_range.end;
+        let trimmed_text = entire_context_text[left_whitespaces_offset..].to_owned();
         Self {
-            highlight_ranges,
+            highlights_data: Arc::default(),
             search_match_indices,
             context_range: trimmed_row_offset_range.to_anchors(&multi_buffer_snapshot),
             context_text: trimmed_text,
+            truncated_left,
+            truncated_right,
         }
     }
 }
@@ -376,7 +522,27 @@ impl PartialEq for FsEntry {
     }
 }
 
+impl Hash for FsEntry {
+    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
+        match self {
+            Self::ExternalFile(buffer_id, _) => {
+                buffer_id.hash(state);
+            }
+            Self::Directory(worktree_id, entry) => {
+                worktree_id.hash(state);
+                entry.id.hash(state);
+            }
+            Self::File(worktree_id, entry, buffer_id, _) => {
+                worktree_id.hash(state);
+                entry.id.hash(state);
+                buffer_id.hash(state);
+            }
+        }
+    }
+}
+
 struct ActiveItem {
+    item_handle: Box<dyn WeakItemHandle>,
     active_editor: WeakView<Editor>,
     _buffer_search_subscription: Subscription,
     _editor_subscrpiption: Subscription,
@@ -464,11 +630,15 @@ impl OutlinePanel {
                     .expect("have a &mut Workspace"),
                 move |outline_panel, workspace, event, cx| {
                     if let workspace::Event::ActiveItemChanged = event {
-                        if let Some(new_active_editor) =
+                        if let Some((new_active_item, new_active_editor)) =
                             workspace_active_editor(workspace.read(cx), cx)
                         {
-                            if outline_panel.should_replace_active_editor(&new_active_editor) {
-                                outline_panel.replace_active_editor(new_active_editor, cx);
+                            if outline_panel.should_replace_active_item(new_active_item.as_ref()) {
+                                outline_panel.replace_active_editor(
+                                    new_active_item,
+                                    new_active_editor,
+                                    cx,
+                                );
                             }
                         } else {
                             outline_panel.clear_previous(cx);
@@ -502,8 +672,6 @@ impl OutlinePanel {
                 focus_handle,
                 filter_editor,
                 fs_entries: Vec::new(),
-                search_matches: Vec::new(),
-                search: None,
                 fs_entries_depth: HashMap::default(),
                 fs_children_count: HashMap::default(),
                 collapsed_entries: HashSet::default(),
@@ -528,8 +696,8 @@ impl OutlinePanel {
                     filter_update_subscription,
                 ],
             };
-            if let Some(editor) = workspace_active_editor(workspace, cx) {
-                outline_panel.replace_active_editor(editor, cx);
+            if let Some((item, editor)) = workspace_active_editor(workspace, cx) {
+                outline_panel.replace_active_editor(item, editor, cx);
             }
             outline_panel
         });
@@ -691,12 +859,22 @@ impl OutlinePanel {
         };
 
         if let Some((offset, anchor)) = scroll_target {
+            self.workspace
+                .update(cx, |workspace, cx| match self.active_item() {
+                    Some(active_item) => {
+                        workspace.activate_item(active_item.as_ref(), true, change_selection, cx)
+                    }
+                    None => workspace.activate_item(&active_editor, true, change_selection, cx),
+                });
+
             self.select_entry(entry.clone(), true, cx);
             if change_selection {
                 active_editor.update(cx, |editor, cx| {
-                    editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
-                        s.select_ranges(Some(anchor..anchor))
-                    });
+                    editor.change_selections(
+                        Some(Autoscroll::Strategy(AutoscrollStrategy::Top)),
+                        cx,
+                        |s| s.select_ranges(Some(anchor..anchor)),
+                    );
                 });
                 active_editor.focus_handle(cx).focus(cx);
             } else {
@@ -705,20 +883,6 @@ impl OutlinePanel {
                 });
                 self.focus_handle.focus(cx);
             }
-
-            if let PanelEntry::Search(_) = entry {
-                if let Some(active_project_search) =
-                    self.active_project_search(Some(&active_editor), cx)
-                {
-                    self.workspace.update(cx, |workspace, cx| {
-                        workspace.activate_item(&active_project_search, true, change_selection, cx)
-                    });
-                }
-            } else {
-                self.workspace.update(cx, |workspace, cx| {
-                    workspace.activate_item(&active_editor, true, change_selection, cx)
-                });
-            };
         }
     }
 
@@ -751,7 +915,7 @@ impl OutlinePanel {
         }) {
             self.select_entry(entry_to_select, true, cx);
         } else {
-            self.select_first(&SelectFirst {}, cx)
+            self.select_last(&SelectLast, cx)
         }
     }
 
@@ -778,7 +942,7 @@ impl OutlinePanel {
                                 PanelEntry::FoldedDirs(dirs_worktree_id, dirs) => {
                                     dirs_worktree_id == worktree_id
                                         && dirs
-                                            .first()
+                                            .last()
                                             .map_or(false, |dir| dir.path.as_ref() == parent_path)
                                 }
                                 _ => false,
@@ -1672,15 +1836,23 @@ impl OutlinePanel {
         )
     }
 
+    #[allow(clippy::too_many_arguments)]
     fn render_search_match(
-        &self,
+        &mut self,
+        multi_buffer_snapshot: Option<&MultiBufferSnapshot>,
         match_range: &Range<editor::Anchor>,
-        search_data: &SearchData,
+        search_data: &Arc<SearchData>,
         kind: SearchKind,
         depth: usize,
         string_match: Option<&StringMatch>,
         cx: &mut ViewContext<Self>,
     ) -> Stateful<Div> {
+        if let ItemsDisplayMode::Search(search_state) = &mut self.mode {
+            if let Some(multi_buffer_snapshot) = multi_buffer_snapshot {
+                search_state.highlight_search_match(match_range, multi_buffer_snapshot);
+            }
+        }
+
         let search_matches = string_match
             .iter()
             .flat_map(|string_match| string_match.ranges())
@@ -1696,14 +1868,29 @@ impl OutlinePanel {
                 annotation_range: None,
                 range: search_data.context_range.clone(),
                 text: search_data.context_text.clone(),
-                highlight_ranges: search_data.highlight_ranges.clone(),
+                highlight_ranges: search_data
+                    .highlights_data
+                    .get()
+                    .cloned()
+                    .unwrap_or_default(),
                 name_ranges: search_data.search_match_indices.clone(),
                 body_range: Some(search_data.context_range.clone()),
             },
             match_ranges.into_iter().cloned(),
             cx,
-        )
-        .into_any_element();
+        );
+        let truncated_contents_label = || Label::new(TRUNCATED_CONTEXT_MARK);
+        let entire_label = h_flex()
+            .justify_center()
+            .p_0()
+            .when(search_data.truncated_left, |parent| {
+                parent.child(truncated_contents_label())
+            })
+            .child(label_element)
+            .when(search_data.truncated_right, |parent| {
+                parent.child(truncated_contents_label())
+            })
+            .into_any_element();
 
         let is_active = match self.selected_entry() {
             Some(PanelEntry::Search(SearchEntry {
@@ -1716,14 +1903,13 @@ impl OutlinePanel {
             PanelEntry::Search(SearchEntry {
                 kind,
                 match_range: match_range.clone(),
-                same_line_matches: Vec::new(),
-                render_data: Some(OnceCell::new()),
+                render_data: Arc::clone(search_data),
             }),
             ElementId::from(SharedString::from(format!("search-{match_range:?}"))),
             depth,
             None,
             is_active,
-            label_element,
+            entire_label,
             cx,
         )
     }
@@ -2156,20 +2342,24 @@ impl OutlinePanel {
 
     fn replace_active_editor(
         &mut self,
+        new_active_item: Box<dyn ItemHandle>,
         new_active_editor: View<Editor>,
         cx: &mut ViewContext<Self>,
     ) {
         self.clear_previous(cx);
         let buffer_search_subscription = cx.subscribe(
             &new_active_editor,
-            |outline_panel: &mut Self, _, _: &SearchEvent, cx: &mut ViewContext<'_, Self>| {
-                outline_panel.update_search_matches(cx);
+            |outline_panel: &mut Self, _, e: &SearchEvent, cx: &mut ViewContext<'_, Self>| {
+                if matches!(e, SearchEvent::MatchesInvalidated) {
+                    outline_panel.update_search_matches(cx);
+                };
                 outline_panel.autoscroll(cx);
             },
         );
         self.active_item = Some(ActiveItem {
             _buffer_search_subscription: buffer_search_subscription,
             _editor_subscrpiption: subscribe_for_editor_events(&new_active_editor, cx),
+            item_handle: new_active_item.downgrade_item(),
             active_editor: new_active_editor.downgrade(),
         });
         let new_entries =
@@ -2192,9 +2382,8 @@ impl OutlinePanel {
         self.outline_fetch_tasks.clear();
         self.excerpts.clear();
         self.cached_entries = Vec::new();
-        self.search_matches.clear();
-        self.search = None;
         self.pinned = false;
+        self.mode = ItemsDisplayMode::Outline;
     }
 
     fn location_for_editor_selection(
@@ -2218,12 +2407,12 @@ impl OutlinePanel {
         let buffer_id = buffer.read(cx).remote_id();
         let selection_display_point = selection.to_display_point(&editor_snapshot);
 
-        match self.mode {
-            ItemsDisplayMode::Search => self
-                .search_matches
+        match &self.mode {
+            ItemsDisplayMode::Search(search_state) => search_state
+                .matches
                 .iter()
                 .rev()
-                .min_by_key(|&match_range| {
+                .min_by_key(|&(match_range, _)| {
                     let match_display_range =
                         match_range.clone().to_display_points(&editor_snapshot);
                     let start_distance = if selection_display_point < match_display_range.start {
@@ -2238,17 +2427,12 @@ impl OutlinePanel {
                     };
                     start_distance + end_distance
                 })
-                .and_then(|closest_range| {
+                .and_then(|(closest_range, _)| {
                     self.cached_entries.iter().find_map(|cached_entry| {
-                        if let PanelEntry::Search(SearchEntry {
-                            match_range,
-                            same_line_matches,
-                            ..
-                        }) = &cached_entry.entry
+                        if let PanelEntry::Search(SearchEntry { match_range, .. }) =
+                            &cached_entry.entry
                         {
-                            if match_range == closest_range
-                                || same_line_matches.contains(&closest_range)
-                            {
+                            if match_range == closest_range {
                                 Some(cached_entry.entry.clone())
                             } else {
                                 None
@@ -2623,17 +2807,26 @@ impl OutlinePanel {
         cx.spawn(|outline_panel, mut cx| async move {
             let mut entries = Vec::new();
             let mut match_candidates = Vec::new();
+            let mut added_contexts = HashSet::default();
 
             let Ok(()) = outline_panel.update(&mut cx, |outline_panel, cx| {
                 let auto_fold_dirs = OutlinePanelSettings::get_global(cx).auto_fold_dirs;
                 let mut folded_dirs_entry = None::<(usize, WorktreeId, Vec<Entry>)>;
                 let track_matches = query.is_some();
-                let mut parent_dirs = Vec::<(&Path, bool, bool, usize)>::new();
 
-                for entry in &outline_panel.fs_entries {
-                    let is_expanded = outline_panel.is_expanded(entry);
-                    let (depth, should_add) = match entry {
+                #[derive(Debug)]
+                struct ParentStats {
+                    path: Arc<Path>,
+                    folded: bool,
+                    expanded: bool,
+                    depth: usize,
+                }
+                let mut parent_dirs = Vec::<ParentStats>::new();
+                for entry in outline_panel.fs_entries.clone() {
+                    let is_expanded = outline_panel.is_expanded(&entry);
+                    let (depth, should_add) = match &entry {
                         FsEntry::Directory(worktree_id, dir_entry) => {
+                            let mut should_add = true;
                             let is_root = project
                                 .read(cx)
                                 .worktree_for_id(*worktree_id, cx)
@@ -2653,16 +2846,16 @@ impl OutlinePanel {
                                 .get(&(*worktree_id, dir_entry.id))
                                 .copied()
                                 .unwrap_or(0);
-                            while let Some(&(previous_path, ..)) = parent_dirs.last() {
-                                if dir_entry.path.starts_with(previous_path) {
+                            while let Some(parent) = parent_dirs.last() {
+                                if dir_entry.path.starts_with(&parent.path) {
                                     break;
                                 }
                                 parent_dirs.pop();
                             }
                             let auto_fold = match parent_dirs.last() {
-                                Some((parent_path, parent_folded, _, _)) => {
-                                    *parent_folded
-                                        && Some(*parent_path) == dir_entry.path.parent()
+                                Some(parent) => {
+                                    parent.folded
+                                        && Some(parent.path.as_ref()) == dir_entry.path.parent()
                                         && outline_panel
                                             .fs_children_count
                                             .get(worktree_id)
@@ -2674,29 +2867,31 @@ impl OutlinePanel {
                                 None => false,
                             };
                             let folded = folded || auto_fold;
-                            let (depth, parent_expanded) = match parent_dirs.last() {
-                                Some(&(_, previous_folded, previous_expanded, previous_depth)) => {
-                                    let new_depth = if folded && previous_folded {
-                                        previous_depth
+                            let (depth, parent_expanded, parent_folded) = match parent_dirs.last() {
+                                Some(parent) => {
+                                    let parent_folded = parent.folded;
+                                    let parent_expanded = parent.expanded;
+                                    let new_depth = if parent_folded {
+                                        parent.depth
                                     } else {
-                                        previous_depth + 1
+                                        parent.depth + 1
                                     };
-                                    parent_dirs.push((
-                                        &dir_entry.path,
+                                    parent_dirs.push(ParentStats {
+                                        path: dir_entry.path.clone(),
                                         folded,
-                                        previous_expanded && is_expanded,
-                                        new_depth,
-                                    ));
-                                    (new_depth, previous_expanded)
+                                        expanded: parent_expanded && is_expanded,
+                                        depth: new_depth,
+                                    });
+                                    (new_depth, parent_expanded, parent_folded)
                                 }
                                 None => {
-                                    parent_dirs.push((
-                                        &dir_entry.path,
+                                    parent_dirs.push(ParentStats {
+                                        path: dir_entry.path.clone(),
                                         folded,
-                                        is_expanded,
-                                        fs_depth,
-                                    ));
-                                    (fs_depth, true)
+                                        expanded: is_expanded,
+                                        depth: fs_depth,
+                                    });
+                                    (fs_depth, true, false)
                                 }
                             };
 
@@ -2712,27 +2907,51 @@ impl OutlinePanel {
                                     folded_dirs_entry =
                                         Some((folded_depth, folded_worktree_id, folded_dirs))
                                 } else {
-                                    if !is_singleton && (parent_expanded || query.is_some()) {
-                                        let new_folded_dirs =
-                                            PanelEntry::FoldedDirs(folded_worktree_id, folded_dirs);
-                                        outline_panel.push_entry(
-                                            &mut entries,
-                                            &mut match_candidates,
-                                            track_matches,
-                                            new_folded_dirs,
-                                            folded_depth,
-                                            cx,
-                                        );
+                                    if !is_singleton {
+                                        let start_of_collapsed_dir_sequence = !parent_expanded
+                                            && parent_dirs
+                                                .iter()
+                                                .rev()
+                                                .skip(folded_dirs.len() + 1)
+                                                .next()
+                                                .map_or(true, |parent| parent.expanded);
+                                        if start_of_collapsed_dir_sequence
+                                            || parent_expanded
+                                            || query.is_some()
+                                        {
+                                            if parent_folded {
+                                                folded_dirs.push(dir_entry.clone());
+                                                should_add = false;
+                                            }
+                                            let new_folded_dirs = PanelEntry::FoldedDirs(
+                                                folded_worktree_id,
+                                                folded_dirs,
+                                            );
+                                            outline_panel.push_entry(
+                                                &mut entries,
+                                                &mut match_candidates,
+                                                &mut added_contexts,
+                                                track_matches,
+                                                new_folded_dirs,
+                                                folded_depth,
+                                                cx,
+                                            );
+                                        }
                                     }
-                                    folded_dirs_entry =
+
+                                    folded_dirs_entry = if parent_folded {
+                                        None
+                                    } else {
                                         Some((depth, *worktree_id, vec![dir_entry.clone()]))
+                                    };
                                 }
                             } else if folded {
                                 folded_dirs_entry =
                                     Some((depth, *worktree_id, vec![dir_entry.clone()]));
                             }
 
-                            let should_add = parent_expanded && folded_dirs_entry.is_none();
+                            let should_add =
+                                should_add && parent_expanded && folded_dirs_entry.is_none();
                             (depth, should_add)
                         }
                         FsEntry::ExternalFile(..) => {
@@ -2742,16 +2961,15 @@ impl OutlinePanel {
                                 let parent_expanded = parent_dirs
                                     .iter()
                                     .rev()
-                                    .find(|(parent_path, ..)| {
-                                        folded_dirs
-                                            .iter()
-                                            .all(|entry| entry.path.as_ref() != *parent_path)
+                                    .find(|parent| {
+                                        folded_dirs.iter().all(|entry| entry.path != parent.path)
                                     })
-                                    .map_or(true, |&(_, _, parent_expanded, _)| parent_expanded);
+                                    .map_or(true, |parent| parent.expanded);
                                 if !is_singleton && (parent_expanded || query.is_some()) {
                                     outline_panel.push_entry(
                                         &mut entries,
                                         &mut match_candidates,
+                                        &mut added_contexts,
                                         track_matches,
                                         PanelEntry::FoldedDirs(worktree_id, folded_dirs),
                                         folded_depth,
@@ -2769,16 +2987,15 @@ impl OutlinePanel {
                                 let parent_expanded = parent_dirs
                                     .iter()
                                     .rev()
-                                    .find(|(parent_path, ..)| {
-                                        folded_dirs
-                                            .iter()
-                                            .all(|entry| entry.path.as_ref() != *parent_path)
+                                    .find(|parent| {
+                                        folded_dirs.iter().all(|entry| entry.path != parent.path)
                                     })
-                                    .map_or(true, |&(_, _, parent_expanded, _)| parent_expanded);
+                                    .map_or(true, |parent| parent.expanded);
                                 if !is_singleton && (parent_expanded || query.is_some()) {
                                     outline_panel.push_entry(
                                         &mut entries,
                                         &mut match_candidates,
+                                        &mut added_contexts,
                                         track_matches,
                                         PanelEntry::FoldedDirs(worktree_id, folded_dirs),
                                         folded_depth,
@@ -2792,16 +3009,16 @@ impl OutlinePanel {
                                 .get(&(*worktree_id, file_entry.id))
                                 .copied()
                                 .unwrap_or(0);
-                            while let Some(&(previous_path, ..)) = parent_dirs.last() {
-                                if file_entry.path.starts_with(previous_path) {
+                            while let Some(parent) = parent_dirs.last() {
+                                if file_entry.path.starts_with(&parent.path) {
                                     break;
                                 }
                                 parent_dirs.pop();
                             }
                             let (depth, should_add) = match parent_dirs.last() {
-                                Some(&(_, _, previous_expanded, previous_depth)) => {
-                                    let new_depth = previous_depth + 1;
-                                    (new_depth, previous_expanded)
+                                Some(parent) => {
+                                    let new_depth = parent.depth + 1;
+                                    (new_depth, parent.expanded)
                                 }
                                 None => (fs_depth, true),
                             };
@@ -2815,6 +3032,7 @@ impl OutlinePanel {
                         outline_panel.push_entry(
                             &mut entries,
                             &mut match_candidates,
+                            &mut added_contexts,
                             track_matches,
                             PanelEntry::Fs(entry.clone()),
                             depth,
@@ -2823,15 +3041,16 @@ impl OutlinePanel {
                     }
 
                     match outline_panel.mode {
-                        ItemsDisplayMode::Search => {
+                        ItemsDisplayMode::Search(_) => {
                             if is_singleton || query.is_some() || (should_add && is_expanded) {
                                 outline_panel.add_search_entries(
-                                    entry,
-                                    depth,
-                                    track_matches,
-                                    is_singleton,
                                     &mut entries,
                                     &mut match_candidates,
+                                    &mut added_contexts,
+                                    entry.clone(),
+                                    depth,
+                                    query.clone(),
+                                    is_singleton,
                                     cx,
                                 );
                             }
@@ -2839,7 +3058,7 @@ impl OutlinePanel {
                         ItemsDisplayMode::Outline => {
                             let excerpts_to_consider =
                                 if is_singleton || query.is_some() || (should_add && is_expanded) {
-                                    match entry {
+                                    match &entry {
                                         FsEntry::File(_, _, buffer_id, entry_excerpts) => {
                                             Some((*buffer_id, entry_excerpts))
                                         }
@@ -2854,13 +3073,14 @@ impl OutlinePanel {
                             if let Some((buffer_id, entry_excerpts)) = excerpts_to_consider {
                                 outline_panel.add_excerpt_entries(
                                     buffer_id,
-                                    entry_excerpts,
+                                    &entry_excerpts,
                                     depth,
                                     track_matches,
                                     is_singleton,
                                     query.as_deref(),
                                     &mut entries,
                                     &mut match_candidates,
+                                    &mut added_contexts,
                                     cx,
                                 );
                             }
@@ -2876,6 +3096,7 @@ impl OutlinePanel {
                         outline_panel.push_entry(
                             &mut entries,
                             &mut match_candidates,
+                            &mut added_contexts,
                             track_matches,
                             PanelEntry::Fs(entry.clone()),
                             0,
@@ -2888,16 +3109,13 @@ impl OutlinePanel {
                     let parent_expanded = parent_dirs
                         .iter()
                         .rev()
-                        .find(|(parent_path, ..)| {
-                            folded_dirs
-                                .iter()
-                                .all(|entry| entry.path.as_ref() != *parent_path)
-                        })
-                        .map_or(true, |&(_, _, parent_expanded, _)| parent_expanded);
+                        .find(|parent| folded_dirs.iter().all(|entry| entry.path != parent.path))
+                        .map_or(true, |parent| parent.expanded);
                     if parent_expanded || query.is_some() {
                         outline_panel.push_entry(
                             &mut entries,
                             &mut match_candidates,
+                            &mut added_contexts,
                             track_matches,
                             PanelEntry::FoldedDirs(worktree_id, folded_dirs),
                             folded_depth,
@@ -2909,6 +3127,19 @@ impl OutlinePanel {
                 return Vec::new();
             };
 
+            outline_panel
+                .update(&mut cx, |outline_panel, _| {
+                    if matches!(outline_panel.mode, ItemsDisplayMode::Search(_)) {
+                        cleanup_fs_entries_without_search_children(
+                            &outline_panel.collapsed_entries,
+                            &mut entries,
+                            &mut match_candidates,
+                            &mut added_contexts,
+                        );
+                    }
+                })
+                .ok();
+
             let Some(query) = query else {
                 return entries;
             };

crates/search/Cargo.toml 🔗

@@ -5,6 +5,14 @@ edition = "2021"
 publish = false
 license = "GPL-3.0-or-later"
 
+[features]
+test-support = [
+    "client/test-support",
+    "editor/test-support",
+    "gpui/test-support",
+    "workspace/test-support",
+]
+
 [lints]
 workspace = true
 

crates/search/src/project_search.rs 🔗

@@ -113,7 +113,7 @@ pub fn init(cx: &mut AppContext) {
     .detach();
 }
 
-struct ProjectSearch {
+pub struct ProjectSearch {
     project: Model<Project>,
     excerpts: Model<MultiBuffer>,
     pending_search: Option<Task<Option<()>>>,
@@ -151,7 +151,7 @@ pub struct ProjectSearchView {
 }
 
 #[derive(Debug, Clone)]
-struct ProjectSearchSettings {
+pub struct ProjectSearchSettings {
     search_options: SearchOptions,
     filters_enabled: bool,
 }
@@ -162,7 +162,7 @@ pub struct ProjectSearchBar {
 }
 
 impl ProjectSearch {
-    fn new(project: Model<Project>, cx: &mut ModelContext<Self>) -> Self {
+    pub fn new(project: Model<Project>, cx: &mut ModelContext<Self>) -> Self {
         let replica_id = project.read(cx).replica_id();
         let capability = project.read(cx).capability();
 
@@ -605,7 +605,7 @@ impl ProjectSearchView {
         });
     }
 
-    fn new(
+    pub fn new(
         model: Model<ProjectSearch>,
         cx: &mut ViewContext<Self>,
         settings: Option<ProjectSearchSettings>,
@@ -751,9 +751,9 @@ impl ProjectSearchView {
         });
     }
 
-    // Re-activate the most recently activated search in this pane or the most recent if it has been closed.
-    // If no search exists in the workspace, create a new one.
-    fn deploy_search(
+    /// Re-activate the most recently activated search in this pane or the most recent if it has been closed.
+    /// If no search exists in the workspace, create a new one.
+    pub fn deploy_search(
         workspace: &mut Workspace,
         action: &workspace::DeploySearch,
         cx: &mut ViewContext<Workspace>,
@@ -1140,6 +1140,11 @@ impl ProjectSearchView {
             return self.focus_results_editor(cx);
         }
     }
+
+    #[cfg(any(test, feature = "test-support"))]
+    pub fn results_editor(&self) -> &View<Editor> {
+        &self.results_editor
+    }
 }
 
 impl ProjectSearchBar {
@@ -1752,15 +1757,31 @@ fn register_workspace_action_for_present_search<A: Action>(
     });
 }
 
+#[cfg(any(test, feature = "test-support"))]
+pub fn perform_project_search(
+    search_view: &View<ProjectSearchView>,
+    text: impl Into<std::sync::Arc<str>>,
+    cx: &mut gpui::VisualTestContext,
+) {
+    search_view.update(cx, |search_view, cx| {
+        search_view
+            .query_editor
+            .update(cx, |query_editor, cx| query_editor.set_text(text, cx));
+        search_view.search(cx);
+    });
+    cx.run_until_parked();
+}
+
 #[cfg(test)]
 pub mod tests {
+    use std::sync::Arc;
+
     use super::*;
     use editor::{display_map::DisplayRow, DisplayPoint};
     use gpui::{Action, TestAppContext, WindowHandle};
     use project::FakeFs;
     use serde_json::json;
     use settings::SettingsStore;
-    use std::sync::Arc;
     use workspace::DeploySearch;
 
     #[gpui::test]

crates/workspace/src/pane.rs 🔗

@@ -117,7 +117,7 @@ pub struct RevealInProjectPanel {
     pub entry_id: Option<u64>,
 }
 
-#[derive(PartialEq, Clone, Deserialize)]
+#[derive(Default, PartialEq, Clone, Deserialize)]
 pub struct DeploySearch {
     #[serde(default)]
     pub replace_enabled: bool,