Merge pull request #1078 from zed-industries/lsp-hover

Keith Simmons created

LSP Hover Information

Change summary

Cargo.lock                                 | 393 ++++++++++-------------
assets/keymaps/vim.json                    |   1 
crates/collab/src/integration_tests.rs     |  98 +++++
crates/collab/src/rpc.rs                   |   1 
crates/editor/src/editor.rs                | 307 ++++++++++++++++++
crates/editor/src/element.rs               | 141 +++++++
crates/editor/src/selections_collection.rs |   8 
crates/gpui/src/elements/flex.rs           |  11 
crates/gpui/src/presenter.rs               | 144 +++++---
crates/language/src/language.rs            |   2 
crates/lsp/src/lsp.rs                      |   4 
crates/project/Cargo.toml                  |   1 
crates/project/src/lsp_command.rs          | 224 +++++++++++++
crates/project/src/project.rs              |  48 ++
crates/rpc/proto/zed.proto                 |  90 +++--
crates/rpc/src/proto.rs                    |   4 
crates/theme/src/theme.rs                  |   9 
styles/src/styleTree/chatPanel.ts          |   7 
styles/src/styleTree/components.ts         |  13 
styles/src/styleTree/contextMenu.ts        |   4 
styles/src/styleTree/editor.ts             |   4 
styles/src/styleTree/hoverPopover.ts       |  27 +
styles/src/styleTree/picker.ts             |   4 
styles/src/styleTree/tooltip.ts            |   4 
styles/src/styleTree/workspace.ts          |   4 
styles/src/themes/common/base16.ts         | 173 +++++-----
styles/src/themes/common/theme.ts          |   4 
27 files changed, 1,287 insertions(+), 443 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -25,11 +25,11 @@ checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234"
 
 [[package]]
 name = "ahash"
-version = "0.7.4"
+version = "0.7.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "43bb833f0bf979d8475d38fbf09ed3b8a55e1885fe93ad3f93239fc6a4f17b98"
+checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47"
 dependencies = [
- "getrandom 0.2.2",
+ "getrandom 0.2.6",
  "once_cell",
  "version_check",
 ]
@@ -43,15 +43,6 @@ dependencies = [
  "memchr",
 ]
 
-[[package]]
-name = "ansi_term"
-version = "0.11.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b"
-dependencies = [
- "winapi 0.3.9",
-]
-
 [[package]]
 name = "ansi_term"
 version = "0.12.1"
@@ -81,9 +72,9 @@ checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b"
 
 [[package]]
 name = "arrayvec"
-version = "0.7.1"
+version = "0.7.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "be4dc07131ffa69b8072d35f5007352af944213cde02545e2103680baed38fcd"
+checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6"
 
 [[package]]
 name = "ascii"
@@ -124,9 +115,9 @@ dependencies = [
 
 [[package]]
 name = "async-compression"
-version = "0.3.12"
+version = "0.3.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f2bf394cfbbe876f0ac67b13b6ca819f9c9f2fb9ec67223cceb1555fbab1c31a"
+checksum = "345fd392ab01f746c717b1357165b76f0b67a60192007b234058c9045fdcf695"
 dependencies = [
  "flate2",
  "futures-core",
@@ -137,16 +128,16 @@ dependencies = [
 
 [[package]]
 name = "async-executor"
-version = "1.4.0"
+version = "1.4.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "eb877970c7b440ead138f6321a3b5395d6061183af779340b65e20c0fede9146"
+checksum = "871f9bb5e0a22eeb7e8cf16641feb87c9dc67032ccf8ff49e772eb9941d3a965"
 dependencies = [
  "async-task",
  "concurrent-queue",
  "fastrand",
  "futures-lite",
  "once_cell",
- "vec-arena",
+ "slab",
 ]
 
 [[package]]
@@ -162,42 +153,40 @@ dependencies = [
 
 [[package]]
 name = "async-io"
-version = "1.3.1"
+version = "1.7.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9315f8f07556761c3e48fec2e6b276004acf426e6dc068b2c2251854d65ee0fd"
+checksum = "e5e18f61464ae81cde0a23e713ae8fd299580c54d697a35820cfd0625b8b0e07"
 dependencies = [
  "concurrent-queue",
- "fastrand",
  "futures-lite",
  "libc",
  "log",
- "nb-connect",
  "once_cell",
  "parking",
  "polling",
- "vec-arena",
+ "slab",
+ "socket2",
  "waker-fn",
  "winapi 0.3.9",
 ]
 
 [[package]]
 name = "async-lock"
-version = "2.4.0"
+version = "2.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e6a8ea61bf9947a1007c5cada31e647dbc77b103c679858150003ba697ea798b"
+checksum = "e97a171d191782fba31bb902b14ad94e24a68145032b7eedf871ab0bc0d077b6"
 dependencies = [
  "event-listener",
 ]
 
 [[package]]
 name = "async-net"
-version = "1.5.0"
+version = "1.6.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "06de475c85affe184648202401d7622afb32f0f74e02192857d0201a16defbe5"
+checksum = "5373304df79b9b4395068fb080369ec7178608827306ce4d081cba51cac551df"
 dependencies = [
  "async-io",
  "blocking",
- "fastrand",
  "futures-lite",
 ]
 
@@ -212,15 +201,16 @@ dependencies = [
 
 [[package]]
 name = "async-process"
-version = "1.0.2"
+version = "1.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ef37b86e2fa961bae5a4d212708ea0154f904ce31d1a4a7f47e1bbc33a0c040b"
+checksum = "cf2c06e30a24e8c78a3987d07f0930edf76ef35e027e7bdb063fccafdad1f60c"
 dependencies = [
  "async-io",
  "blocking",
  "cfg-if 1.0.0",
  "event-listener",
  "futures-lite",
+ "libc",
  "once_cell",
  "signal-hook",
  "winapi 0.3.9",
@@ -278,9 +268,9 @@ dependencies = [
 
 [[package]]
 name = "async-trait"
-version = "0.1.50"
+version = "0.1.56"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0b98e84bbb4cbcdd97da190ba0c58a1bb0de2c1fdf67d159e192ed766aeca722"
+checksum = "96cf8829f67d2eab0b2dfa42c5d0ef737e0724e4a82b01b3e292456202b19716"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -312,11 +302,11 @@ dependencies = [
 
 [[package]]
 name = "atomic"
-version = "0.5.0"
+version = "0.5.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c3410529e8288c463bedb5930f82833bc0c90e5d2fe639a56582a4d09220b281"
+checksum = "b88d82667eca772c4aa12f0f1348b3ae643424c8876448f3f7bd5787032e234c"
 dependencies = [
- "autocfg 1.0.1",
+ "autocfg 1.1.0",
 ]
 
 [[package]]
@@ -360,21 +350,24 @@ dependencies = [
 
 [[package]]
 name = "autocfg"
-version = "0.1.7"
+version = "0.1.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1d49d90015b3c36167a20fe2810c5cd875ad504b39cff3d4eae7977e6b7c1cb2"
+checksum = "0dde43e75fd43e8a1bf86103336bc699aa8d17ad1be60c76c0bdfd4828e19b78"
+dependencies = [
+ "autocfg 1.1.0",
+]
 
 [[package]]
 name = "autocfg"
-version = "1.0.1"
+version = "1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
+checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
 
 [[package]]
 name = "axum"
-version = "0.5.4"
+version = "0.5.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f4af7447fc1214c1f3a1ace861d0216a6c8bb13965b64bbad9650f375b67689a"
+checksum = "ab2504b827a8bef941ba3dd64bdffe9cf56ca182908a147edd6189c95fbcae7d"
 dependencies = [
  "async-trait",
  "axum-core",
@@ -386,7 +379,7 @@ dependencies = [
  "http",
  "http-body",
  "hyper",
- "itoa 1.0.1",
+ "itoa",
  "matchit",
  "memchr",
  "mime",
@@ -407,9 +400,9 @@ dependencies = [
 
 [[package]]
 name = "axum-core"
-version = "0.2.3"
+version = "0.2.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3bdc19781b16e32f8a7200368a336fa4509d4b72ef15dd4e41df5290855ee1e6"
+checksum = "da31c0ed7b4690e2c78fe4b880d21cd7db04a346ebc658b4270251b695437f17"
 dependencies = [
  "async-trait",
  "bytes",
@@ -440,24 +433,24 @@ dependencies = [
 
 [[package]]
 name = "backtrace"
-version = "0.3.64"
+version = "0.3.65"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5e121dee8023ce33ab248d9ce1493df03c3b38a659b240096fcbd7048ff9c31f"
+checksum = "11a17d453482a265fd5f8479f2a3f405566e6ca627837aaddb85af8b1ab8ef61"
 dependencies = [
  "addr2line",
  "cc",
  "cfg-if 1.0.0",
  "libc",
- "miniz_oxide 0.4.4",
+ "miniz_oxide 0.5.3",
  "object",
  "rustc-demangle",
 ]
 
 [[package]]
 name = "base-x"
-version = "0.2.8"
+version = "0.2.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a4521f3e3d031370679b3b140beb36dfe4801b09ac77e30c61941f97df3ef28b"
+checksum = "dc19a4937b4fbd3fe3379793130e42060d10627a360f2127802b10b87e7baf74"
 
 [[package]]
 name = "base64"
@@ -473,9 +466,9 @@ checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
 
 [[package]]
 name = "base64ct"
-version = "1.0.0"
+version = "1.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d0d27fb6b6f1e43147af148af49d49329413ba781aa0d5e10979831c210173b5"
+checksum = "dea908e7347a8c64e378c17e30ef880ad73e3b4498346b055c2c00ea342f3179"
 
 [[package]]
 name = "bincode"
@@ -495,7 +488,7 @@ dependencies = [
  "bitflags",
  "cexpr",
  "clang-sys",
- "clap 2.33.3",
+ "clap 2.34.0",
  "env_logger",
  "lazy_static",
  "lazycell",
@@ -515,18 +508,6 @@ version = "1.3.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
 
-[[package]]
-name = "bitvec"
-version = "0.19.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8942c8d352ae1838c9dda0b0ca2ab657696ef2232a20147cf1b30ae1a9cb4321"
-dependencies = [
- "funty",
- "radium",
- "tap",
- "wyz",
-]
-
 [[package]]
 name = "block"
 version = "0.1.6"
@@ -553,9 +534,9 @@ dependencies = [
 
 [[package]]
 name = "blocking"
-version = "1.0.2"
+version = "1.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c5e170dbede1f740736619b776d7251cb1b9095c435c34d8ca9f57fcd2f335e9"
+checksum = "c6ccb65d468978a086b69884437ded69a90faab3bbe6e67f242173ea728acccc"
 dependencies = [
  "async-channel",
  "async-task",
@@ -582,36 +563,30 @@ dependencies = [
 
 [[package]]
 name = "bstr"
-version = "0.2.15"
+version = "0.2.17"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a40b47ad93e1a5404e6c18dec46b628214fee441c70f4ab5d6942142cc268a3d"
+checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223"
 dependencies = [
  "memchr",
 ]
 
-[[package]]
-name = "build_const"
-version = "0.2.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b4ae4235e6dac0694637c763029ecea1a2ec9e4e06ec2729bd21ba4d9c863eb7"
-
 [[package]]
 name = "bumpalo"
-version = "3.7.0"
+version = "3.10.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9c59e7af012c713f529e7a3ee57ce9b31ddd858d4b512923602f74608b009631"
+checksum = "37ccbd214614c6783386c1af30caf03192f17891059cecc394b4fb119e363de3"
 
 [[package]]
 name = "bytemuck"
-version = "1.5.1"
+version = "1.9.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bed57e2090563b83ba8f83366628ce535a7584c9afa4c9fc0612a03925c6df58"
+checksum = "cdead85bdec19c194affaeeb670c0e41fe23de31459efd1c174d049269cf02cc"
 
 [[package]]
 name = "byteorder"
-version = "1.4.2"
+version = "1.4.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ae44d1a3d5a19df61dd0c8beb138458ac2a53a7ac09eba97d55592540004306b"
+checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
 
 [[package]]
 name = "bytes"
@@ -632,9 +607,9 @@ dependencies = [
 
 [[package]]
 name = "cache-padded"
-version = "1.1.1"
+version = "1.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "631ae5198c9be5e753e5cc215e1bd73c2b466a3565173db433f52bb9d3e66dba"
+checksum = "c1db59621ec70f09c5e9b597b220c7a2b43611f4710dc03ceb8748637775692c"
 
 [[package]]
 name = "castaway"
@@ -644,9 +619,9 @@ checksum = "a2698f953def977c68f935bb0dfa959375ad4638570e969e2f1e9f433cbf1af6"
 
 [[package]]
 name = "cc"
-version = "1.0.67"
+version = "1.0.73"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e3c69b077ad434294d3ce9f1f6143a2a4b89a8a2d54ef813d85003a4fd1137fd"
+checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11"
 dependencies = [
  "jobserver",
 ]
@@ -657,7 +632,7 @@ version = "0.6.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
 dependencies = [
- "nom 7.1.1",
+ "nom",
 ]
 
 [[package]]
@@ -683,7 +658,7 @@ dependencies = [
  "postage",
  "settings",
  "theme",
- "time 0.3.7",
+ "time 0.3.9",
  "util",
  "workspace",
 ]
@@ -697,7 +672,7 @@ dependencies = [
  "libc",
  "num-integer",
  "num-traits",
- "time 0.1.44",
+ "time 0.1.43",
  "winapi 0.3.9",
 ]
 
@@ -718,9 +693,9 @@ dependencies = [
 
 [[package]]
 name = "clang-sys"
-version = "1.1.1"
+version = "1.3.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f54d78e30b388d4815220c8dd03fea5656b6c6d32adb59e89061552a102f8da1"
+checksum = "5a050e2153c5be08febd6734e29298e844fdb0fa21aeddd63b4eb7baa106c69b"
 dependencies = [
  "glob",
  "libc",
@@ -729,11 +704,11 @@ dependencies = [
 
 [[package]]
 name = "clap"
-version = "2.33.3"
+version = "2.34.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002"
+checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c"
 dependencies = [
- "ansi_term 0.11.0",
+ "ansi_term",
  "atty",
  "bitflags",
  "strsim 0.8.0",
@@ -744,9 +719,9 @@ dependencies = [
 
 [[package]]
 name = "clap"
-version = "3.1.12"
+version = "3.1.18"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7c167e37342afc5f33fd87bbc870cedd020d2a6dffa05d45ccd9241fbdd146db"
+checksum = "d2dbdf4bdacb33466e854ce889eee8dfd5729abf7ccd7664d0a2d60cd384440b"
 dependencies = [
  "atty",
  "bitflags",
@@ -761,9 +736,9 @@ dependencies = [
 
 [[package]]
 name = "clap_derive"
-version = "3.1.7"
+version = "3.1.18"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a3aab4734e083b809aaf5794e14e756d1c798d2c69c7f7de7a09a2f5214993c1"
+checksum = "25320346e922cffe59c0bbc5410c8d8784509efb321488971081313cb1e1a33c"
 dependencies = [
  "heck 0.4.0",
  "proc-macro-error",
@@ -774,9 +749,9 @@ dependencies = [
 
 [[package]]
 name = "clap_lex"
-version = "0.1.1"
+version = "0.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "189ddd3b5d32a70b35e7686054371742a937b0d99128e76dde6340210e966669"
+checksum = "a37c35f1112dad5e6e0b1adaff798507497a18fceeb30cceb3bae7d1427b9213"
 dependencies = [
  "os_str_bytes",
 ]
@@ -786,10 +761,10 @@ name = "cli"
 version = "0.1.0"
 dependencies = [
  "anyhow",
- "clap 3.1.12",
+ "clap 3.1.18",
  "core-foundation",
  "core-services",
- "dirs 3.0.1",
+ "dirs 3.0.2",
  "ipc-channel",
  "plist",
  "serde",
@@ -809,14 +784,14 @@ dependencies = [
  "isahc",
  "lazy_static",
  "log",
- "parking_lot",
+ "parking_lot 0.11.2",
  "postage",
- "rand 0.8.3",
+ "rand 0.8.5",
  "rpc",
  "smol",
  "sum_tree",
  "thiserror",
- "time 0.3.7",
+ "time 0.3.9",
  "tiny_http",
  "url",
  "util",
@@ -831,9 +806,9 @@ dependencies = [
 
 [[package]]
 name = "cmake"
-version = "0.1.45"
+version = "0.1.48"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "eb6210b637171dfba4cda12e579ac6dc73f5165ad56133e5d72ef3131f320855"
+checksum = "e8ad8cef104ac57b68b89df3208164d228503abbdce70f6880ffa3d970e7443a"
 dependencies = [
  "cc",
 ]
@@ -877,7 +852,7 @@ dependencies = [
  "axum",
  "axum-extra",
  "base64 0.13.0",
- "clap 3.1.12",
+ "clap 3.1.18",
  "client",
  "collections",
  "ctor",
@@ -893,16 +868,16 @@ dependencies = [
  "log",
  "lsp",
  "nanoid",
- "parking_lot",
+ "parking_lot 0.11.2",
  "project",
- "rand 0.8.3",
+ "rand 0.8.5",
  "reqwest",
  "rpc",
  "scrypt",
  "serde",
  "serde_json",
  "settings",
- "sha-1 0.9.6",
+ "sha-1 0.9.8",
  "sqlx",
  "theme",
  "time 0.2.27",
@@ -960,9 +935,9 @@ dependencies = [
 
 [[package]]
 name = "const_fn"
-version = "0.4.8"
+version = "0.4.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f92cfa0fd5690b3cf8c1ef2cabbd9b7ef22fa53cf5e1f92b05103f6d5d1cf6e7"
+checksum = "fbdcdcb6d86f71c5e97409ad45898af11cbc995b4ee8112d59095a28d376c935"
 
 [[package]]
 name = "contacts_panel"
@@ -1059,36 +1034,33 @@ dependencies = [
 
 [[package]]
 name = "cpufeatures"
-version = "0.1.4"
+version = "0.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ed00c67cb5d0a7d64a44f6ad2668db7e7530311dd53ea79bcd4fb022c64911c8"
+checksum = "59a6001667ab124aebae2a495118e11d30984c3a653e99d86d58971708cf5e4b"
 dependencies = [
  "libc",
 ]
 
 [[package]]
-name = "cpufeatures"
-version = "0.2.1"
+name = "crc"
+version = "2.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "95059428f66df56b63431fdb4e1947ed2190586af5c5a8a8b71122bdf5a7f469"
+checksum = "49fc9a695bca7f35f5f4c15cddc84415f66a74ea78eef08e90c5024f2b540e23"
 dependencies = [
- "libc",
+ "crc-catalog",
 ]
 
 [[package]]
-name = "crc"
-version = "1.8.1"
+name = "crc-catalog"
+version = "1.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d663548de7f5cca343f1e0a48d14dcfb0e9eb4e079ec58883b7251539fa10aeb"
-dependencies = [
- "build_const",
-]
+checksum = "ccaeedb56da03b09f598226e25e80088cb4cd25f316e6e4df7d695f0feeb1403"
 
 [[package]]
 name = "crc32fast"
-version = "1.2.1"
+version = "1.3.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "81156fece84ab6a9f2afdb109ce3ae577e42b1228441eded99bd77f627953b1a"
+checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d"
 dependencies = [
  "cfg-if 1.0.0",
 ]
@@ -1105,47 +1077,47 @@ dependencies = [
 
 [[package]]
 name = "crossbeam-channel"
-version = "0.5.0"
+version = "0.5.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dca26ee1f8d361640700bde38b2c37d8c22b3ce2d360e1fc1c74ea4b0aa7d775"
+checksum = "5aaa7bd5fb665c6864b5f963dd9097905c54125909c7aa94c9e18507cdbe6c53"
 dependencies = [
  "cfg-if 1.0.0",
- "crossbeam-utils 0.8.2",
+ "crossbeam-utils 0.8.8",
 ]
 
 [[package]]
 name = "crossbeam-deque"
-version = "0.8.0"
+version = "0.8.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "94af6efb46fef72616855b036a624cf27ba656ffc9be1b9a3c931cfc7749a9a9"
+checksum = "6455c0ca19f0d2fbf751b908d5c55c1f5cbc65e03c4225427254b46890bdde1e"
 dependencies = [
  "cfg-if 1.0.0",
  "crossbeam-epoch",
- "crossbeam-utils 0.8.2",
+ "crossbeam-utils 0.8.8",
 ]
 
 [[package]]
 name = "crossbeam-epoch"
-version = "0.9.2"
+version = "0.9.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d60ab4a8dba064f2fbb5aa270c28da5cf4bbd0e72dae1140a6b0353a779dbe00"
+checksum = "1145cf131a2c6ba0615079ab6a638f7e1973ac9c2634fcbeaaad6114246efe8c"
 dependencies = [
+ "autocfg 1.1.0",
  "cfg-if 1.0.0",
- "crossbeam-utils 0.8.2",
+ "crossbeam-utils 0.8.8",
  "lazy_static",
- "loom",
  "memoffset",
  "scopeguard",
 ]
 
 [[package]]
 name = "crossbeam-queue"
-version = "0.3.1"
+version = "0.3.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0f6cb3c7f5b8e51bc3ebb73a2327ad4abdbd119dc13223f14f961d2f38486756"
+checksum = "1f25d8400f4a7a5778f0e4e52384a48cbd9b5c495d110786187fc750075277a2"
 dependencies = [
  "cfg-if 1.0.0",
- "crossbeam-utils 0.8.2",
+ "crossbeam-utils 0.8.8",
 ]
 
 [[package]]
@@ -1154,21 +1126,19 @@ version = "0.7.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8"
 dependencies = [
- "autocfg 1.0.1",
+ "autocfg 1.1.0",
  "cfg-if 0.1.10",
  "lazy_static",
 ]
 
 [[package]]
 name = "crossbeam-utils"
-version = "0.8.2"
+version = "0.8.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bae8f328835f8f5a6ceb6a7842a7f2d0c03692adb5c889347235d59194731fe3"
+checksum = "0bf124c720b7686e3c2663cf54062ab0f68a88af2fb6a030e87e30bf721fcb38"
 dependencies = [
- "autocfg 1.0.1",
  "cfg-if 1.0.0",
  "lazy_static",
- "loom",
 ]
 
 [[package]]
@@ -1183,19 +1153,9 @@ dependencies = [
 
 [[package]]
 name = "crypto-mac"
-version = "0.10.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4857fd85a0c34b3c3297875b747c1e02e06b6a0ea32dd892d8192b9ce0813ea6"
-dependencies = [
- "generic-array",
- "subtle",
-]
-
-[[package]]
-name = "crypto-mac"
-version = "0.11.0"
+version = "0.11.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "25fab6889090c8133f3deb8f73ba3c65a7f456f66436fc012a1b1e272b1e103e"
+checksum = "b1d1a86f49236c215f271d40892d5fc950490551400b02ef360692c29815c714"
 dependencies = [
  "generic-array",
  "subtle",
@@ -1203,9 +1163,9 @@ dependencies = [
 
 [[package]]
 name = "ctor"
-version = "0.1.20"
+version = "0.1.22"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5e98e2ad1a782e33928b96fc3948e7c355e5af34ba4de7670fe8bac2a3b2006d"
+checksum = "f877be4f7c9f246b183111634f75baa039715e3f46ce860677d3b19a69fb229c"
 dependencies = [
  "quote",
  "syn",
@@ -1213,24 +1173,24 @@ dependencies = [
 
 [[package]]
 name = "curl"
-version = "0.4.42"
+version = "0.4.43"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7de97b894edd5b5bcceef8b78d7da9b75b1d2f2f9a910569d0bde3dd31d84939"
+checksum = "37d855aeef205b43f65a5001e0997d81f8efca7badad4fad7d897aa7f0d0651f"
 dependencies = [
  "curl-sys",
  "libc",
  "openssl-probe",
  "openssl-sys",
  "schannel",
- "socket2 0.4.0",
+ "socket2",
  "winapi 0.3.9",
 ]
 
 [[package]]
 name = "curl-sys"
-version = "0.4.52+curl-7.81.0"
+version = "0.4.55+curl-7.83.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "14b8c2d1023ea5fded5b7b892e4b8e95f70038a421126a056761a84246a28971"
+checksum = "23734ec77368ec583c2e61dd3f0b0e5c98b93abe6d2a004ca06b91dd7e3e2762"
 dependencies = [
  "cc",
  "libc",
@@ -1244,9 +1204,9 @@ dependencies = [
 
 [[package]]
 name = "data-url"
-version = "0.1.0"
+version = "0.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d33fe99ccedd6e84bc035f1931bb2e6be79739d6242bd895e7311c886c50dc9c"
+checksum = "3a30bfce702bcfa94e906ef82421f2c0e61c076ad76030c16ee5d2e9a32fe193"
 dependencies = [
  "matches",
 ]
@@ -1269,7 +1229,7 @@ checksum = "47003dc9f6368a88e85956c3b2573a7e6872746a3e5d762a8885da3a136a0381"
 dependencies = [
  "backtrace",
  "lazy_static",
- "parking_lot",
+ "parking_lot 0.11.2",
  "rustc-hash",
  "serde",
  "serde_json",
@@ -1314,13 +1274,14 @@ checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506"
 dependencies = [
  "block-buffer 0.10.2",
  "crypto-common",
+ "subtle",
 ]
 
 [[package]]
 name = "dirs"
-version = "3.0.1"
+version = "3.0.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "142995ed02755914747cc6ca76fc7e4583cd18578746716d0508ea6ed558b9ff"
+checksum = "30baa043103c9d0c2a57cf537cc2f35623889dc0d405e6c3cccfadbc81c71309"
 dependencies = [
  "dirs-sys",
 ]
@@ -1346,9 +1307,9 @@ dependencies = [
 
 [[package]]
 name = "dirs-sys"
-version = "0.3.6"
+version = "0.3.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "03d86534ed367a67548dc68113a0f5db55432fdfbb6e6f9d77704397d95d5780"
+checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6"
 dependencies = [
  "libc",
  "redox_users",
@@ -1392,15 +1353,15 @@ dependencies = [
 
 [[package]]
 name = "dyn-clone"
-version = "1.0.4"
+version = "1.0.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ee2626afccd7561a06cf1367e2950c4718ea04565e20fb5029b6c7d8ad09abcf"
+checksum = "21e50f3adc76d6a43f5ed73b698a87d0760ca74617f60f7c3b879003536fdd28"
 
 [[package]]
 name = "easy-parallel"
-version = "3.1.0"
+version = "3.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1dd4afd79212583ff429b913ad6605242ed7eec277e950b1438f300748f948f4"
+checksum = "6907e25393cdcc1f4f3f513d9aac1e840eb1cc341a0fccb01171f7d14d10b946"
 
 [[package]]
 name = "editor"
@@ -1422,10 +1383,10 @@ dependencies = [
  "log",
  "lsp",
  "ordered-float",
- "parking_lot",
+ "parking_lot 0.11.2",
  "postage",
  "project",
- "rand 0.8.3",
+ "rand 0.8.5",
  "rpc",
  "serde",
  "settings",
@@ -1450,9 +1411,9 @@ checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
 
 [[package]]
 name = "encoding_rs"
-version = "0.8.28"
+version = "0.8.31"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "80df024fbc5ac80f87dfef0d9f5209a252f2a497f7f42944cff24d8253cac065"
+checksum = "9852635589dc9f9ea1b6fe9f05b50ef208c85c834a562f0c6abb1c475736ec2b"
 dependencies = [
  "cfg-if 1.0.0",
 ]
@@ -1490,9 +1451,9 @@ dependencies = [
 
 [[package]]
 name = "etagere"
-version = "0.2.4"
+version = "0.2.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "520d7de540904fd09b11c03a47d50a7ce4ff37d1aa763f454fa60d9088ef8356"
+checksum = "6301151a318f367f392c31395beb1cfba5ccd9abc44d1db0db3a4b27b9601c89"
 dependencies = [
  "euclid",
  "svg_fmt",
@@ -1500,18 +1461,18 @@ dependencies = [
 
 [[package]]
 name = "euclid"
-version = "0.22.2"
+version = "0.22.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "51e5bac4ec41ece6346fd867815a57a221abdf48f4eb931b033789b5b4b6fc70"
+checksum = "b52c2ef4a78da0ba68fbe1fd920627411096d2ac478f7f4c9f3a54ba6705bade"
 dependencies = [
  "num-traits",
 ]
 
 [[package]]
 name = "event-listener"
-version = "2.5.1"
+version = "2.5.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f7531096570974c3a9dcf9e4b8e1cede1ec26cf5046219fb3b9d897503b9be59"
+checksum = "77f3309417938f28bf8228fcff79a4a37103981e3e186d2ccd19c74b38f4eb71"
 
 [[package]]
 name = "expat-sys"
@@ -1560,14 +1521,12 @@ checksum = "279fb028e20b3c4c320317955b77c5e0c9701f05a1d309905d6fc702cdc5053e"
 
 [[package]]
 name = "flate2"
-version = "1.0.20"
+version = "1.0.24"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cd3aec53de10fe96d7d8c565eb17f2c687bb5518a2ec453b5b1252964526abe0"
+checksum = "f82b0f4c27ad9f8bfd1f3208d882da2b09c301bc1c828fd3a00d0216d2fbbff6"
 dependencies = [
- "cfg-if 1.0.0",
  "crc32fast",
- "libc",
- "miniz_oxide 0.4.4",
+ "miniz_oxide 0.5.3",
 ]
 
 [[package]]
@@ -1614,13 +1573,13 @@ dependencies = [
 
 [[package]]
 name = "fontdb"
-version = "0.5.1"
+version = "0.5.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "428948a0f39fb83fe55991d4423e35a793cdbb0322ebe23853f6024124a330d7"
+checksum = "e58903f4f8d5b58c7d300908e4ebe5289c1bfdf5587964330f12023b8ff17fd1"
 dependencies = [
  "log",
- "memmap2 0.1.0",
- "ttf-parser 0.9.0",
+ "memmap2",
+ "ttf-parser 0.12.3",
 ]
 
 [[package]]
@@ -1675,15 +1634,15 @@ version = "2.0.2"
 dependencies = [
  "bitflags",
  "fsevent-sys",
- "parking_lot",
+ "parking_lot 0.11.2",
  "tempdir",
 ]
 
 [[package]]
 name = "fsevent-sys"
-version = "3.0.2"
+version = "3.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "77a29c77f1ca394c3e73a9a5d24cfcabb734682d9634fc398f2204a63c994120"
+checksum = "ca6f5e6817058771c10f0eb0f05ddf1e35844266f972004fe8e4b21fda295bd5"
 dependencies = [
  "libc",
 ]
@@ -1710,17 +1669,11 @@ version = "0.3.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7"
 
-[[package]]
-name = "funty"
-version = "1.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fed34cd105917e91daa4da6b3728c47b068749d6a62c59811f06ed2ac71d9da7"
-
 [[package]]
 name = "futures"
-version = "0.3.12"
+version = "0.3.21"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "da9052a1a50244d8d5aa9bf55cbc2fb6f357c86cc52e46c62ed390a7180cf150"
+checksum = "f73fe65f54d1e12b726f517d3e2135ca3125a437b6d998caf1962961f7172d9e"
 dependencies = [
  "futures-channel",
  "futures-core",
@@ -1749,15 +1702,26 @@ checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3"
 
 [[package]]
 name = "futures-executor"
-version = "0.3.12"
+version = "0.3.21"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e9e59fdc009a4b3096bf94f740a0f2424c082521f20a9b08c5c07c48d90fd9b9"
+checksum = "9420b90cfa29e327d0429f19be13e7ddb68fa1cccb09d65e5706b8c7a749b8a6"
 dependencies = [
  "futures-core",
  "futures-task",
  "futures-util",
 ]
 
+[[package]]
+name = "futures-intrusive"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62007592ac46aa7c2b6416f7deb9a8a8f63a01e0f1d6e1787d5630170db2b63e"
+dependencies = [
+ "futures-core",
+ "lock_api",
+ "parking_lot 0.11.2",
+]
+
 [[package]]
 name = "futures-io"
 version = "0.3.21"
@@ -1828,24 +1792,11 @@ dependencies = [
  "util",
 ]
 
-[[package]]
-name = "generator"
-version = "0.6.23"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8cdc09201b2e8ca1b19290cf7e65de2246b8e91fb6874279722189c4de7b94dc"
-dependencies = [
- "cc",
- "libc",
- "log",
- "rustc_version",
- "winapi 0.3.9",
-]
-
 [[package]]
 name = "generic-array"
-version = "0.14.4"
+version = "0.14.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817"
+checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803"
 dependencies = [
  "typenum",
  "version_check",

assets/keymaps/vim.json 🔗

@@ -99,6 +99,7 @@
         "context": "Editor && vim_operator == g",
         "bindings": {
             "g": "vim::StartOfDocument",
+            "h": "editor::Hover",
             "escape": [
                 "vim::SwitchMode",
                 "Normal"

crates/collab/src/integration_tests.rs 🔗

@@ -2281,6 +2281,104 @@ async fn test_document_highlights(cx_a: &mut TestAppContext, cx_b: &mut TestAppC
     });
 }
 
+#[gpui::test(iterations = 10)]
+async fn test_lsp_hover(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
+    cx_a.foreground().forbid_parking();
+    let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
+    let client_a = server.create_client(cx_a, "user_a").await;
+    let client_b = server.create_client(cx_b, "user_b").await;
+    server
+        .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
+        .await;
+
+    client_a
+        .fs
+        .insert_tree(
+            "/root-1",
+            json!({
+                "main.rs": "use std::collections::HashMap;",
+            }),
+        )
+        .await;
+
+    // Set up a fake language server.
+    let mut language = Language::new(
+        LanguageConfig {
+            name: "Rust".into(),
+            path_suffixes: vec!["rs".to_string()],
+            ..Default::default()
+        },
+        Some(tree_sitter_rust::language()),
+    );
+    let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default());
+    client_a.language_registry.add(Arc::new(language));
+
+    let (project_a, worktree_id) = client_a.build_local_project("/root-1", cx_a).await;
+    let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
+
+    // Open the file as the guest
+    let buffer_b = cx_b
+        .background()
+        .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)))
+        .await
+        .unwrap();
+
+    // Request hover information as the guest.
+    let fake_language_server = fake_language_servers.next().await.unwrap();
+    fake_language_server.handle_request::<lsp::request::HoverRequest, _, _>(
+        |params, _| async move {
+            assert_eq!(
+                params
+                    .text_document_position_params
+                    .text_document
+                    .uri
+                    .as_str(),
+                "file:///root-1/main.rs"
+            );
+            assert_eq!(
+                params.text_document_position_params.position,
+                lsp::Position::new(0, 22)
+            );
+            Ok(Some(lsp::Hover {
+                contents: lsp::HoverContents::Array(vec![
+                    lsp::MarkedString::String("Test hover content.".to_string()),
+                    lsp::MarkedString::LanguageString(lsp::LanguageString {
+                        language: "Rust".to_string(),
+                        value: "let foo = 42;".to_string(),
+                    }),
+                ]),
+                range: Some(lsp::Range::new(
+                    lsp::Position::new(0, 22),
+                    lsp::Position::new(0, 29),
+                )),
+            }))
+        },
+    );
+
+    let hover_info = project_b
+        .update(cx_b, |p, cx| p.hover(&buffer_b, 22, cx))
+        .await
+        .unwrap()
+        .unwrap();
+    buffer_b.read_with(cx_b, |buffer, _| {
+        let snapshot = buffer.snapshot();
+        assert_eq!(hover_info.range.unwrap().to_offset(&snapshot), 22..29);
+        assert_eq!(
+            hover_info.contents,
+            vec![
+                project::HoverBlock {
+                    text: "Test hover content.".to_string(),
+                    language: None,
+                },
+                project::HoverBlock {
+                    text: "let foo = 42;".to_string(),
+                    language: Some("Rust".to_string()),
+                }
+            ]
+        );
+    });
+}
+
 #[gpui::test(iterations = 10)]
 async fn test_project_symbols(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
     cx_a.foreground().forbid_parking();

crates/collab/src/rpc.rs 🔗

@@ -150,6 +150,7 @@ impl Server {
             .add_message_handler(Server::start_language_server)
             .add_message_handler(Server::update_language_server)
             .add_message_handler(Server::update_diagnostic_summary)
+            .add_request_handler(Server::forward_project_request::<proto::GetHover>)
             .add_request_handler(Server::forward_project_request::<proto::GetDefinition>)
             .add_request_handler(Server::forward_project_request::<proto::GetReferences>)
             .add_request_handler(Server::forward_project_request::<proto::SearchProject>)

crates/editor/src/editor.rs 🔗

@@ -25,7 +25,7 @@ use gpui::{
     geometry::vector::{vec2f, Vector2F},
     impl_actions, impl_internal_actions,
     platform::CursorStyle,
-    text_layout, AppContext, AsyncAppContext, ClipboardItem, Element, ElementBox, Entity,
+    text_layout, AppContext, AsyncAppContext, Axis, ClipboardItem, Element, ElementBox, Entity,
     ModelHandle, MutableAppContext, RenderContext, Task, View, ViewContext, ViewHandle,
     WeakViewHandle,
 };
@@ -39,7 +39,7 @@ pub use multi_buffer::{
     Anchor, AnchorRangeExt, ExcerptId, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint,
 };
 use ordered_float::OrderedFloat;
-use project::{Project, ProjectTransaction};
+use project::{HoverBlock, Project, ProjectTransaction};
 use selections_collection::{resolve_multiple, MutableSelectionsCollection, SelectionsCollection};
 use serde::{Deserialize, Serialize};
 use settings::Settings;
@@ -80,6 +80,11 @@ pub struct Scroll(pub Vector2F);
 #[derive(Clone, PartialEq)]
 pub struct Select(pub SelectPhase);
 
+#[derive(Clone, PartialEq)]
+pub struct HoverAt {
+    point: Option<DisplayPoint>,
+}
+
 #[derive(Clone, Deserialize, PartialEq)]
 pub struct Input(pub String);
 
@@ -113,6 +118,11 @@ pub struct ConfirmCodeAction {
     pub item_ix: Option<usize>,
 }
 
+#[derive(Clone, Default)]
+pub struct GoToDefinitionAt {
+    pub location: Option<DisplayPoint>,
+}
+
 actions!(
     editor,
     [
@@ -173,10 +183,10 @@ actions!(
         ToggleComments,
         SelectLargerSyntaxNode,
         SelectSmallerSyntaxNode,
+        GoToDefinition,
         MoveToEnclosingBracket,
         UndoSelection,
         RedoSelection,
-        GoToDefinition,
         FindAllReferences,
         Rename,
         ConfirmRename,
@@ -188,6 +198,7 @@ actions!(
         ShowCompletions,
         OpenExcerpts,
         RestartLanguageServer,
+        Hover,
     ]
 );
 
@@ -204,7 +215,7 @@ impl_actions!(
     ]
 );
 
-impl_internal_actions!(editor, [Scroll, Select]);
+impl_internal_actions!(editor, [Scroll, Select, HoverAt]);
 
 enum DocumentHighlightRead {}
 enum DocumentHighlightWrite {}
@@ -291,6 +302,8 @@ pub fn init(cx: &mut MutableAppContext) {
     cx.add_action(Editor::fold_selected_ranges);
     cx.add_action(Editor::show_completions);
     cx.add_action(Editor::toggle_code_actions);
+    cx.add_action(Editor::hover);
+    cx.add_action(Editor::hover_at);
     cx.add_action(Editor::open_excerpts);
     cx.add_action(Editor::restart_language_server);
     cx.add_async_action(Editor::confirm_completion);
@@ -408,6 +421,7 @@ pub struct Editor {
     next_completion_id: CompletionId,
     available_code_actions: Option<(ModelHandle<Buffer>, Arc<[CodeAction]>)>,
     code_actions_task: Option<Task<()>>,
+    hover_task: Option<Task<Option<()>>>,
     document_highlights_task: Option<Task<()>>,
     pending_rename: Option<RenameState>,
     searchable: bool,
@@ -415,6 +429,40 @@ pub struct Editor {
     keymap_context_layers: BTreeMap<TypeId, gpui::keymap::Context>,
     input_enabled: bool,
     leader_replica_id: Option<u16>,
+    hover_state: HoverState,
+}
+
+/// Keeps track of the state of the [`HoverPopover`].
+/// Times out the initial delay and the grace period.
+pub struct HoverState {
+    popover: Option<HoverPopover>,
+    last_hover: std::time::Instant,
+    start_grace: std::time::Instant,
+}
+
+impl HoverState {
+    /// Takes whether the cursor is currently hovering over a symbol,
+    /// and returns a tuple containing whether there was a recent hover,
+    /// and whether the hover is still in the grace period.
+    pub fn determine_state(&mut self, hovering: bool) -> (bool, bool) {
+        // NOTE: We use some sane defaults, but it might be
+        //       nice to make these values configurable.
+        let recent_hover = self.last_hover.elapsed() < std::time::Duration::from_millis(500);
+        if !hovering {
+            self.last_hover = std::time::Instant::now();
+        }
+
+        let in_grace = self.start_grace.elapsed() < std::time::Duration::from_millis(250);
+        if hovering && !recent_hover {
+            self.start_grace = std::time::Instant::now();
+        }
+
+        return (recent_hover, in_grace);
+    }
+
+    pub fn close(&mut self) {
+        self.popover.take();
+    }
 }
 
 pub struct EditorSnapshot {
@@ -841,6 +889,67 @@ impl CodeActionsMenu {
     }
 }
 
+#[derive(Clone)]
+pub(crate) struct HoverPopover {
+    pub project: ModelHandle<Project>,
+    pub hover_point: DisplayPoint,
+    pub range: Range<DisplayPoint>,
+    pub contents: Vec<HoverBlock>,
+}
+
+impl HoverPopover {
+    fn render(
+        &self,
+        style: EditorStyle,
+        cx: &mut RenderContext<Editor>,
+    ) -> (DisplayPoint, ElementBox) {
+        let element = MouseEventHandler::new::<HoverPopover, _, _>(0, cx, |_, cx| {
+            let mut flex = Flex::new(Axis::Vertical).scrollable::<HoverBlock, _>(1, None, cx);
+            flex.extend(self.contents.iter().map(|content| {
+                let project = self.project.read(cx);
+                if let Some(language) = content
+                    .language
+                    .clone()
+                    .and_then(|language| project.languages().get_language(&language))
+                {
+                    let runs = language
+                        .highlight_text(&content.text.as_str().into(), 0..content.text.len());
+
+                    Text::new(content.text.clone(), style.text.clone())
+                        .with_soft_wrap(true)
+                        .with_highlights(
+                            runs.iter()
+                                .filter_map(|(range, id)| {
+                                    id.style(style.theme.syntax.as_ref())
+                                        .map(|style| (range.clone(), style))
+                                })
+                                .collect(),
+                        )
+                        .boxed()
+                } else {
+                    Text::new(content.text.clone(), style.hover_popover.prose.clone())
+                        .with_soft_wrap(true)
+                        .contained()
+                        .with_style(style.hover_popover.block_style)
+                        .boxed()
+                }
+            }));
+            flex.contained()
+                .with_style(style.hover_popover.container)
+                .boxed()
+        })
+        .with_cursor_style(CursorStyle::Arrow)
+        .with_padding(Padding {
+            bottom: 5.,
+            top: 5.,
+            ..Default::default()
+        })
+        .boxed();
+
+        (self.range.start, element)
+    }
+}
+
 #[derive(Debug)]
 struct ActiveDiagnosticGroup {
     primary_range: Range<Anchor>,
@@ -998,6 +1107,7 @@ impl Editor {
             next_completion_id: 0,
             available_code_actions: Default::default(),
             code_actions_task: Default::default(),
+            hover_task: Default::default(),
             document_highlights_task: Default::default(),
             pending_rename: Default::default(),
             searchable: true,
@@ -1006,6 +1116,11 @@ impl Editor {
             keymap_context_layers: Default::default(),
             input_enabled: true,
             leader_replica_id: None,
+            hover_state: HoverState {
+                popover: None,
+                last_hover: std::time::Instant::now(),
+                start_grace: std::time::Instant::now(),
+            },
         };
         this.end_selection(cx);
 
@@ -1391,6 +1506,8 @@ impl Editor {
                 }
             }
 
+            self.hide_hover(cx);
+
             if old_cursor_position.to_display_point(&display_map).row()
                 != new_cursor_position.to_display_point(&display_map).row()
             {
@@ -1752,6 +1869,10 @@ impl Editor {
             return;
         }
 
+        if self.hide_hover(cx) {
+            return;
+        }
+
         if self.hide_context_menu(cx).is_some() {
             return;
         }
@@ -2380,6 +2501,179 @@ impl Editor {
         }))
     }
 
+    /// Bindable action which uses the most recent selection head to trigger a hover
+    fn hover(&mut self, _: &Hover, cx: &mut ViewContext<Self>) {
+        let head = self.selections.newest_display(cx).head();
+        self.show_hover(head, true, cx);
+    }
+
+    /// The internal hover action dispatches between `show_hover` or `hide_hover`
+    /// depending on whether a point to hover over is provided.
+    fn hover_at(&mut self, action: &HoverAt, cx: &mut ViewContext<Self>) {
+        if let Some(point) = action.point {
+            self.show_hover(point, false, cx);
+        } else {
+            self.hide_hover(cx);
+        }
+    }
+
+    /// Hides the type information popup.
+    /// Triggered by the `Hover` action when the cursor is not over a symbol or when the
+    /// selecitons changed.
+    fn hide_hover(&mut self, cx: &mut ViewContext<Self>) -> bool {
+        // consistently keep track of state to make handoff smooth
+        self.hover_state.determine_state(false);
+
+        let mut did_hide = false;
+
+        // only notify the context once
+        if self.hover_state.popover.is_some() {
+            self.hover_state.popover = None;
+            did_hide = true;
+            cx.notify();
+        }
+
+        self.clear_background_highlights::<HoverState>(cx);
+
+        self.hover_task = None;
+
+        did_hide
+    }
+
+    /// Queries the LSP and shows type info and documentation
+    /// about the symbol the mouse is currently hovering over.
+    /// Triggered by the `Hover` action when the cursor may be over a symbol.
+    fn show_hover(
+        &mut self,
+        point: DisplayPoint,
+        ignore_timeout: bool,
+        cx: &mut ViewContext<Self>,
+    ) {
+        if self.pending_rename.is_some() {
+            return;
+        }
+
+        if let Some(hover) = &self.hover_state.popover {
+            if hover.hover_point == point {
+                // Hover triggered from same location as last time. Don't show again.
+                return;
+            }
+        }
+
+        let snapshot = self.snapshot(cx);
+        let (buffer, buffer_position) = if let Some(output) = self
+            .buffer
+            .read(cx)
+            .text_anchor_for_position(point.to_point(&snapshot.display_snapshot), cx)
+        {
+            output
+        } else {
+            return;
+        };
+
+        let project = if let Some(project) = self.project.clone() {
+            project
+        } else {
+            return;
+        };
+
+        // query the LSP for hover info
+        let hover_request = project.update(cx, |project, cx| {
+            project.hover(&buffer, buffer_position.clone(), cx)
+        });
+
+        let buffer_snapshot = buffer.read(cx).snapshot();
+
+        let task = cx.spawn_weak(|this, mut cx| {
+            async move {
+                // Construct new hover popover from hover request
+                let hover_popover = hover_request.await.ok().flatten().and_then(|hover_result| {
+                    if hover_result.contents.is_empty() {
+                        return None;
+                    }
+
+                    let range = if let Some(range) = hover_result.range {
+                        let offset_range = range.to_offset(&buffer_snapshot);
+                        if !offset_range
+                            .contains(&point.to_offset(&snapshot.display_snapshot, Bias::Left))
+                        {
+                            return None;
+                        }
+
+                        offset_range
+                            .start
+                            .to_display_point(&snapshot.display_snapshot)
+                            ..offset_range
+                                .end
+                                .to_display_point(&snapshot.display_snapshot)
+                    } else {
+                        point..point
+                    };
+
+                    Some(HoverPopover {
+                        project: project.clone(),
+                        hover_point: point,
+                        range,
+                        contents: hover_result.contents,
+                    })
+                });
+
+                if let Some(this) = this.upgrade(&cx) {
+                    this.update(&mut cx, |this, cx| {
+                        // this was trickier than expected, trying to do a couple things:
+                        //
+                        // 1. if you hover over a symbol, there should be a slight delay
+                        //    before the popover shows
+                        // 2. if you move to another symbol when the popover is showing,
+                        //    the popover should switch right away, and you should
+                        //    not have to wait for it to come up again
+                        let (recent_hover, in_grace) =
+                            this.hover_state.determine_state(hover_popover.is_some());
+                        let smooth_handoff =
+                            this.hover_state.popover.is_some() && hover_popover.is_some();
+                        let visible = this.hover_state.popover.is_some() || hover_popover.is_some();
+
+                        // `smooth_handoff` and `in_grace` determine whether to switch right away.
+                        // `recent_hover` will activate the handoff after the initial delay.
+                        // `ignore_timeout` is set when the user manually sent the hover action.
+                        if (ignore_timeout || smooth_handoff || !recent_hover || in_grace)
+                            && visible
+                        {
+                            // Highlight the selected symbol using a background highlight
+                            if let Some(display_range) =
+                                hover_popover.as_ref().map(|popover| popover.range.clone())
+                            {
+                                let start = snapshot.display_snapshot.buffer_snapshot.anchor_after(
+                                    display_range
+                                        .start
+                                        .to_offset(&snapshot.display_snapshot, Bias::Right),
+                                );
+                                let end = snapshot.display_snapshot.buffer_snapshot.anchor_before(
+                                    display_range
+                                        .end
+                                        .to_offset(&snapshot.display_snapshot, Bias::Left),
+                                );
+
+                                this.highlight_background::<HoverState>(
+                                    vec![start..end],
+                                    |theme| theme.editor.hover_popover.highlight,
+                                    cx,
+                                );
+                            }
+
+                            this.hover_state.popover = hover_popover;
+                            cx.notify();
+                        }
+                    });
+                }
+                Ok::<_, anyhow::Error>(())
+            }
+            .log_err()
+        });
+
+        self.hover_task = Some(task);
+    }
+
     async fn open_project_transaction(
         this: ViewHandle<Editor>,
         workspace: ViewHandle<Workspace>,
@@ -2626,6 +2920,10 @@ impl Editor {
             .map(|menu| menu.render(cursor_position, style, cx))
     }
 
+    pub(crate) fn hover_popover(&self) -> Option<HoverPopover> {
+        self.hover_state.popover.clone()
+    }
+
     fn show_context_menu(&mut self, menu: ContextMenu, cx: &mut ViewContext<Self>) {
         if !matches!(menu, ContextMenu::Completions(_)) {
             self.completion_tasks.clear();
@@ -4706,7 +5004,6 @@ impl Editor {
                     // Position the selection in the rename editor so that it matches the current selection.
                     this.show_local_selections = false;
                     let rename_editor = cx.add_view(|cx| {
-                        println!("Rename editor created.");
                         let mut editor = Editor::single_line(None, cx);
                         if let Some(old_highlight_id) = old_highlight_id {
                             editor.override_text_style =

crates/editor/src/element.rs 🔗

@@ -5,7 +5,7 @@ use super::{
 };
 use crate::{
     display_map::{DisplaySnapshot, TransformBlock},
-    EditorStyle,
+    EditorStyle, HoverAt,
 };
 use clock::ReplicaId;
 use collections::{BTreeMap, HashMap};
@@ -102,6 +102,7 @@ impl EditorElement {
     fn mouse_down(
         &self,
         position: Vector2F,
+        _: bool,
         alt: bool,
         shift: bool,
         mut click_count: usize,
@@ -121,7 +122,7 @@ impl EditorElement {
         if shift && alt {
             cx.dispatch_action(Select(SelectPhase::BeginColumnar {
                 position,
-                overshoot,
+                overshoot: overshoot.column(),
             }));
         } else if shift {
             cx.dispatch_action(Select(SelectPhase::Extend {
@@ -190,7 +191,7 @@ impl EditorElement {
 
             cx.dispatch_action(Select(SelectPhase::Update {
                 position,
-                overshoot,
+                overshoot: overshoot.column(),
                 scroll_position: (snapshot.scroll_position() + scroll_delta)
                     .clamp(Vector2F::zero(), layout.scroll_max),
             }));
@@ -349,6 +350,7 @@ impl EditorElement {
         bounds: RectF,
         visible_bounds: RectF,
         layout: &mut LayoutState,
+        paint: &mut PaintState,
         cx: &mut PaintContext,
     ) {
         let view = self.view(cx.app);
@@ -490,7 +492,7 @@ impl EditorElement {
             let mut list_origin = content_origin + vec2f(x, y);
             let list_height = context_menu.size().y();
 
-            if list_origin.y() + list_height > bounds.lower_left().y() {
+            if list_origin.y() + list_height > bounds.max_y() {
                 list_origin.set_y(list_origin.y() - layout.line_height - list_height);
             }
 
@@ -503,6 +505,38 @@ impl EditorElement {
             cx.scene.pop_stacking_context();
         }
 
+        if let Some((position, hover_popover)) = layout.hover.as_mut() {
+            cx.scene.push_stacking_context(None);
+
+            // This is safe because we check on layout whether the required row is available
+            let hovered_row_layout = &layout.line_layouts[(position.row() - start_row) as usize];
+            let size = hover_popover.size();
+            let x = hovered_row_layout.x_for_index(position.column() as usize) - scroll_left;
+            let y = position.row() as f32 * layout.line_height - scroll_top - size.y();
+            let mut popover_origin = content_origin + vec2f(x, y);
+
+            if popover_origin.y() < 0.0 {
+                popover_origin.set_y(popover_origin.y() + layout.line_height + size.y());
+            }
+
+            let x_out_of_bounds = bounds.max_x() - (popover_origin.x() + size.x());
+            if x_out_of_bounds < 0.0 {
+                popover_origin.set_x(popover_origin.x() + x_out_of_bounds);
+            }
+
+            hover_popover.paint(
+                popover_origin,
+                RectF::from_points(Vector2F::zero(), vec2f(f32::MAX, f32::MAX)), // Let content bleed outside of editor
+                cx,
+            );
+
+            paint.hover_bounds = Some(
+                RectF::new(popover_origin, hover_popover.size()).dilate(Vector2F::new(0., 5.)),
+            );
+
+            cx.scene.pop_stacking_context();
+        }
+
         cx.scene.pop_layer();
     }
 
@@ -1076,6 +1110,7 @@ impl Element for EditorElement {
 
         let mut context_menu = None;
         let mut code_actions_indicator = None;
+        let mut hover = None;
         cx.render(&self.view.upgrade(cx).unwrap(), |view, cx| {
             let newest_selection_head = view
                 .selections
@@ -1083,8 +1118,8 @@ impl Element for EditorElement {
                 .head()
                 .to_display_point(&snapshot);
 
+            let style = view.style(cx);
             if (start_row..end_row).contains(&newest_selection_head.row()) {
-                let style = view.style(cx);
                 if view.context_menu_visible() {
                     context_menu =
                         view.render_context_menu(newest_selection_head, style.clone(), cx);
@@ -1094,6 +1129,17 @@ impl Element for EditorElement {
                     .render_code_actions_indicator(&style, cx)
                     .map(|indicator| (newest_selection_head.row(), indicator));
             }
+
+            hover = view.hover_popover().and_then(|hover| {
+                let (point, rendered) = hover.render(style.clone(), cx);
+                if point.row() >= snapshot.scroll_position().y() as u32 {
+                    if line_layouts.len() > (point.row() - start_row) as usize {
+                        return Some((point, rendered));
+                    }
+                }
+
+                None
+            });
         });
 
         if let Some((_, context_menu)) = context_menu.as_mut() {
@@ -1116,6 +1162,19 @@ impl Element for EditorElement {
             );
         }
 
+        if let Some((_, hover)) = hover.as_mut() {
+            hover.layout(
+                SizeConstraint {
+                    min: Vector2F::zero(),
+                    max: vec2f(
+                        (120. * em_width).min(size.x()),
+                        (size.y() - line_height) * 1. / 2.,
+                    ),
+                },
+                cx,
+            );
+        }
+
         let blocks = self.layout_blocks(
             start_row..end_row,
             &snapshot,
@@ -1152,6 +1211,7 @@ impl Element for EditorElement {
                 selections,
                 context_menu,
                 code_actions_indicator,
+                hover,
             },
         )
     }
@@ -1171,11 +1231,18 @@ impl Element for EditorElement {
             layout.text_size,
         );
 
+        let mut paint_state = PaintState {
+            bounds,
+            gutter_bounds,
+            text_bounds,
+            hover_bounds: None,
+        };
+
         self.paint_background(gutter_bounds, text_bounds, layout, cx);
         if layout.gutter_size.x() > 0. {
             self.paint_gutter(gutter_bounds, visible_bounds, layout, cx);
         }
-        self.paint_text(text_bounds, visible_bounds, layout, cx);
+        self.paint_text(text_bounds, visible_bounds, layout, &mut paint_state, cx);
 
         if !layout.blocks.is_empty() {
             cx.scene.push_layer(Some(bounds));
@@ -1185,11 +1252,7 @@ impl Element for EditorElement {
 
         cx.scene.pop_layer();
 
-        PaintState {
-            bounds,
-            gutter_bounds,
-            text_bounds,
-        }
+        paint_state
     }
 
     fn dispatch_event(
@@ -1213,6 +1276,12 @@ impl Element for EditorElement {
             }
         }
 
+        if let Some((_, hover)) = &mut layout.hover {
+            if hover.dispatch_event(event, cx) {
+                return true;
+            }
+        }
+
         for (_, block) in &mut layout.blocks {
             if block.dispatch_event(event, cx) {
                 return true;
@@ -1222,11 +1291,21 @@ impl Element for EditorElement {
         match event {
             Event::LeftMouseDown {
                 position,
+                cmd,
                 alt,
                 shift,
                 click_count,
                 ..
-            } => self.mouse_down(*position, *alt, *shift, *click_count, layout, paint, cx),
+            } => self.mouse_down(
+                *position,
+                *cmd,
+                *alt,
+                *shift,
+                *click_count,
+                layout,
+                paint,
+                cx,
+            ),
             Event::LeftMouseUp { position, .. } => self.mouse_up(*position, cx),
             Event::LeftMouseDragged { position } => {
                 self.mouse_dragged(*position, layout, paint, cx)
@@ -1237,6 +1316,29 @@ impl Element for EditorElement {
                 precise,
             } => self.scroll(*position, *delta, *precise, layout, paint, cx),
             Event::KeyDown { input, .. } => self.key_down(input.as_deref(), cx),
+            Event::MouseMoved { position, .. } => {
+                if paint
+                    .hover_bounds
+                    .map_or(false, |hover_bounds| hover_bounds.contains_point(*position))
+                {
+                    return false;
+                }
+
+                let point = if paint.text_bounds.contains_point(*position) {
+                    let (point, overshoot) =
+                        paint.point_for_position(&self.snapshot(cx), layout, *position);
+                    if overshoot.is_zero() {
+                        Some(point)
+                    } else {
+                        None
+                    }
+                } else {
+                    None
+                };
+
+                cx.dispatch_action(HoverAt { point });
+                true
+            }
             _ => false,
         }
     }
@@ -1275,6 +1377,7 @@ pub struct LayoutState {
     selections: Vec<(ReplicaId, Vec<SelectionLayout>)>,
     context_menu: Option<(DisplayPoint, ElementBox)>,
     code_actions_indicator: Option<(u32, ElementBox)>,
+    hover: Option<(DisplayPoint, ElementBox)>,
 }
 
 fn layout_line(
@@ -1312,19 +1415,24 @@ pub struct PaintState {
     bounds: RectF,
     gutter_bounds: RectF,
     text_bounds: RectF,
+    hover_bounds: Option<RectF>,
 }
 
 impl PaintState {
+    /// Returns two display points. The first is the nearest valid
+    /// position in the current buffer and the second is the distance to the
+    /// nearest valid position if there was overshoot.
     fn point_for_position(
         &self,
         snapshot: &EditorSnapshot,
         layout: &LayoutState,
         position: Vector2F,
-    ) -> (DisplayPoint, u32) {
+    ) -> (DisplayPoint, DisplayPoint) {
         let scroll_position = snapshot.scroll_position();
         let position = position - self.text_bounds.origin();
         let y = position.y().max(0.0).min(layout.size.y());
         let row = ((y / layout.line_height) + scroll_position.y()) as u32;
+        let row_overshoot = row.saturating_sub(snapshot.max_point().row());
         let row = cmp::min(row, snapshot.max_point().row());
         let line = &layout.line_layouts[(row - scroll_position.y() as u32) as usize];
         let x = position.x() + (scroll_position.x() * layout.em_width);
@@ -1336,9 +1444,12 @@ impl PaintState {
         } else {
             0
         };
-        let overshoot = (0f32.max(x - line.width()) / layout.em_advance) as u32;
+        let column_overshoot = (0f32.max(x - line.width()) / layout.em_advance) as u32;
 
-        (DisplayPoint::new(row, column), overshoot)
+        (
+            DisplayPoint::new(row, column),
+            DisplayPoint::new(row_overshoot, column_overshoot),
+        )
     }
 }
 

crates/editor/src/selections_collection.rs 🔗

@@ -195,6 +195,14 @@ impl SelectionsCollection {
         resolve(self.newest_anchor(), &self.buffer(cx))
     }
 
+    pub fn newest_display(&self, cx: &mut MutableAppContext) -> Selection<DisplayPoint> {
+        let display_map = self.display_map(cx);
+        let selection = self
+            .newest_anchor()
+            .map(|point| point.to_display_point(&display_map));
+        selection
+    }
+
     pub fn oldest_anchor(&self) -> &Selection<Anchor> {
         self.disjoint
             .iter()

crates/gpui/src/elements/flex.rs 🔗

@@ -319,6 +319,17 @@ impl Element for Flex {
                 }
             }
         }
+
+        if !handled {
+            if let &Event::MouseMoved { position, .. } = event {
+                // If this is a scrollable flex, and the mouse is over it, eat the scroll event to prevent
+                // propogating it to the element below.
+                if self.scroll_state.is_some() && bounds.contains_point(position) {
+                    handled = true;
+                }
+            }
+        }
+
         handled
     }
 

crates/gpui/src/presenter.rs 🔗

@@ -160,7 +160,12 @@ impl Presenter {
 
             if cx.window_is_active(self.window_id) {
                 if let Some(event) = self.last_mouse_moved_event.clone() {
-                    self.dispatch_event(event, cx)
+                    let mut invalidated_views = Vec::new();
+                    self.handle_hover_events(&event, &mut invalidated_views, cx);
+
+                    for view_id in invalidated_views {
+                        cx.notify_view(self.window_id, view_id);
+                    }
                 }
             }
         } else {
@@ -222,8 +227,6 @@ impl Presenter {
     pub fn dispatch_event(&mut self, event: Event, cx: &mut MutableAppContext) {
         if let Some(root_view_id) = cx.root_view_id(self.window_id) {
             let mut invalidated_views = Vec::new();
-            let mut hovered_regions = Vec::new();
-            let mut unhovered_regions = Vec::new();
             let mut mouse_down_out_handlers = Vec::new();
             let mut mouse_down_region = None;
             let mut clicked_region = None;
@@ -288,46 +291,8 @@ impl Presenter {
                         }
                     }
                 }
-                Event::MouseMoved {
-                    position,
-                    left_mouse_down,
-                } => {
+                Event::MouseMoved { .. } => {
                     self.last_mouse_moved_event = Some(event.clone());
-
-                    if !left_mouse_down {
-                        let mut style_to_assign = CursorStyle::Arrow;
-                        for region in self.cursor_regions.iter().rev() {
-                            if region.bounds.contains_point(position) {
-                                style_to_assign = region.style;
-                                break;
-                            }
-                        }
-                        cx.platform().set_cursor_style(style_to_assign);
-
-                        let mut hover_depth = None;
-                        for (region, depth) in self.mouse_regions.iter().rev() {
-                            if region.bounds.contains_point(position)
-                                && hover_depth.map_or(true, |hover_depth| hover_depth == *depth)
-                            {
-                                hover_depth = Some(*depth);
-                                if let Some(region_id) = region.id() {
-                                    if !self.hovered_region_ids.contains(&region_id) {
-                                        invalidated_views.push(region.view_id);
-                                        hovered_regions.push((region.clone(), position));
-                                        self.hovered_region_ids.insert(region_id);
-                                    }
-                                }
-                            } else {
-                                if let Some(region_id) = region.id() {
-                                    if self.hovered_region_ids.contains(&region_id) {
-                                        invalidated_views.push(region.view_id);
-                                        unhovered_regions.push((region.clone(), position));
-                                        self.hovered_region_ids.remove(&region_id);
-                                    }
-                                }
-                            }
-                        }
-                    }
                 }
                 Event::LeftMouseDragged { position } => {
                     if let Some((clicked_region, prev_drag_position)) = self
@@ -348,25 +313,8 @@ impl Presenter {
                 _ => {}
             }
 
-            let mut event_cx = self.build_event_context(cx);
-            let mut handled = false;
-            for (unhovered_region, position) in unhovered_regions {
-                handled = true;
-                if let Some(hover_callback) = unhovered_region.hover {
-                    event_cx.with_current_view(unhovered_region.view_id, |event_cx| {
-                        hover_callback(position, false, event_cx);
-                    })
-                }
-            }
-
-            for (hovered_region, position) in hovered_regions {
-                handled = true;
-                if let Some(hover_callback) = hovered_region.hover {
-                    event_cx.with_current_view(hovered_region.view_id, |event_cx| {
-                        hover_callback(position, true, event_cx);
-                    })
-                }
-            }
+            let (mut handled, mut event_cx) =
+                self.handle_hover_events(&event, &mut invalidated_views, cx);
 
             for (handler, view_id, position) in mouse_down_out_handlers {
                 event_cx.with_current_view(view_id, |event_cx| handler(position, event_cx))
@@ -439,6 +387,80 @@ impl Presenter {
         }
     }
 
+    fn handle_hover_events<'a>(
+        &'a mut self,
+        event: &Event,
+        invalidated_views: &mut Vec<usize>,
+        cx: &'a mut MutableAppContext,
+    ) -> (bool, EventContext<'a>) {
+        let mut unhovered_regions = Vec::new();
+        let mut hovered_regions = Vec::new();
+
+        if let Event::MouseMoved {
+            position,
+            left_mouse_down,
+        } = event
+        {
+            if !left_mouse_down {
+                let mut style_to_assign = CursorStyle::Arrow;
+                for region in self.cursor_regions.iter().rev() {
+                    if region.bounds.contains_point(*position) {
+                        style_to_assign = region.style;
+                        break;
+                    }
+                }
+                cx.platform().set_cursor_style(style_to_assign);
+
+                let mut hover_depth = None;
+                for (region, depth) in self.mouse_regions.iter().rev() {
+                    if region.bounds.contains_point(*position)
+                        && hover_depth.map_or(true, |hover_depth| hover_depth == *depth)
+                    {
+                        hover_depth = Some(*depth);
+                        if let Some(region_id) = region.id() {
+                            if !self.hovered_region_ids.contains(&region_id) {
+                                invalidated_views.push(region.view_id);
+                                hovered_regions.push((region.clone(), position));
+                                self.hovered_region_ids.insert(region_id);
+                            }
+                        }
+                    } else {
+                        if let Some(region_id) = region.id() {
+                            if self.hovered_region_ids.contains(&region_id) {
+                                invalidated_views.push(region.view_id);
+                                unhovered_regions.push((region.clone(), position));
+                                self.hovered_region_ids.remove(&region_id);
+                            }
+                        }
+                    }
+                }
+            }
+        }
+
+        let mut event_cx = self.build_event_context(cx);
+        let mut handled = false;
+
+        for (unhovered_region, position) in unhovered_regions {
+            handled = true;
+            if let Some(hover_callback) = unhovered_region.hover {
+                event_cx.with_current_view(unhovered_region.view_id, |event_cx| {
+                    hover_callback(*position, false, event_cx);
+                })
+            }
+        }
+
+        for (hovered_region, position) in hovered_regions {
+            handled = true;
+            if let Some(hover_callback) = hovered_region.hover {
+                event_cx.with_current_view(hovered_region.view_id, |event_cx| {
+                    hover_callback(*position, true, event_cx);
+                })
+            }
+        }
+
+        (handled, event_cx)
+    }
+
     pub fn build_event_context<'a>(
         &'a mut self,
         cx: &'a mut MutableAppContext,

crates/language/src/language.rs 🔗

@@ -236,7 +236,7 @@ impl LanguageRegistry {
         self.languages
             .read()
             .iter()
-            .find(|language| language.name().as_ref() == name)
+            .find(|language| language.name().to_lowercase() == name.to_lowercase())
             .cloned()
     }
 

crates/lsp/src/lsp.rs 🔗

@@ -296,6 +296,10 @@ impl LanguageServer {
                         prepare_support: Some(true),
                         ..Default::default()
                     }),
+                    hover: Some(HoverClientCapabilities {
+                        content_format: Some(vec![MarkupKind::Markdown]),
+                        ..Default::default()
+                    }),
                     ..Default::default()
                 }),
                 experimental: Some(json!({

crates/project/Cargo.toml 🔗

@@ -38,6 +38,7 @@ libc = "0.2"
 log = { version = "0.4.16", features = ["kv_unstable_serde"] }
 parking_lot = "0.11.1"
 postage = { version = "0.4.1", features = ["futures-traits"] }
+pulldown-cmark = { version = "0.9.1", default-features = false }
 rand = "0.8.3"
 regex = "1.5"
 serde = { version = "1.0", features = ["derive", "rc"] }

crates/project/src/lsp_command.rs 🔗

@@ -1,4 +1,4 @@
-use crate::{DocumentHighlight, Location, Project, ProjectTransaction};
+use crate::{DocumentHighlight, Hover, HoverBlock, Location, Project, ProjectTransaction};
 use anyhow::{anyhow, Result};
 use async_trait::async_trait;
 use client::{proto, PeerId};
@@ -9,6 +9,7 @@ use language::{
     range_from_lsp, Anchor, Bias, Buffer, PointUtf16, ToPointUtf16,
 };
 use lsp::{DocumentHighlightKind, ServerCapabilities};
+use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag};
 use std::{cmp::Reverse, ops::Range, path::Path};
 
 #[async_trait(?Send)]
@@ -80,6 +81,10 @@ pub(crate) struct GetDocumentHighlights {
     pub position: PointUtf16,
 }
 
+pub(crate) struct GetHover {
+    pub position: PointUtf16,
+}
+
 #[async_trait(?Send)]
 impl LspCommand for PrepareRename {
     type Response = Option<Range<Anchor>>;
@@ -794,3 +799,220 @@ impl LspCommand for GetDocumentHighlights {
         message.buffer_id
     }
 }
+
+#[async_trait(?Send)]
+impl LspCommand for GetHover {
+    type Response = Option<Hover>;
+    type LspRequest = lsp::request::HoverRequest;
+    type ProtoRequest = proto::GetHover;
+
+    fn to_lsp(&self, path: &Path, _: &AppContext) -> lsp::HoverParams {
+        lsp::HoverParams {
+            text_document_position_params: lsp::TextDocumentPositionParams {
+                text_document: lsp::TextDocumentIdentifier {
+                    uri: lsp::Url::from_file_path(path).unwrap(),
+                },
+                position: point_to_lsp(self.position),
+            },
+            work_done_progress_params: Default::default(),
+        }
+    }
+
+    async fn response_from_lsp(
+        self,
+        message: Option<lsp::Hover>,
+        _: ModelHandle<Project>,
+        buffer: ModelHandle<Buffer>,
+        mut cx: AsyncAppContext,
+    ) -> Result<Self::Response> {
+        Ok(message.and_then(|hover| {
+            let range = hover.range.map(|range| {
+                cx.read(|cx| {
+                    let buffer = buffer.read(cx);
+                    let token_start =
+                        buffer.clip_point_utf16(point_from_lsp(range.start), Bias::Left);
+                    let token_end = buffer.clip_point_utf16(point_from_lsp(range.end), Bias::Left);
+                    buffer.anchor_after(token_start)..buffer.anchor_before(token_end)
+                })
+            });
+
+            let contents = cx.read(|_| match hover.contents {
+                lsp::HoverContents::Scalar(marked_string) => {
+                    HoverBlock::try_new(marked_string).map(|contents| vec![contents])
+                }
+                lsp::HoverContents::Array(marked_strings) => {
+                    let content: Vec<HoverBlock> = marked_strings
+                        .into_iter()
+                        .filter_map(|marked_string| HoverBlock::try_new(marked_string))
+                        .collect();
+                    if content.is_empty() {
+                        None
+                    } else {
+                        Some(content)
+                    }
+                }
+                lsp::HoverContents::Markup(markup_content) => {
+                    let mut contents = Vec::new();
+                    let mut language = None;
+                    let mut current_text = String::new();
+                    for event in Parser::new_ext(&markup_content.value, Options::all()) {
+                        match event {
+                            Event::SoftBreak => {
+                                current_text.push(' ');
+                            }
+                            Event::Text(text) | Event::Code(text) => {
+                                current_text.push_str(&text.to_string());
+                            }
+                            Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(new_language))) => {
+                                if !current_text.is_empty() {
+                                    let text = std::mem::replace(&mut current_text, String::new());
+                                    contents.push(HoverBlock { text, language });
+                                }
+
+                                language = if new_language.is_empty() {
+                                    None
+                                } else {
+                                    Some(new_language.to_string())
+                                };
+                            }
+                            Event::End(Tag::CodeBlock(_))
+                            | Event::End(Tag::Paragraph)
+                            | Event::End(Tag::Heading(_, _, _))
+                            | Event::End(Tag::BlockQuote)
+                            | Event::HardBreak => {
+                                if !current_text.is_empty() {
+                                    let text = std::mem::replace(&mut current_text, String::new());
+                                    contents.push(HoverBlock { text, language });
+                                    current_text.clear();
+                                }
+                                language = None;
+                            }
+                            _ => {}
+                        }
+                    }
+
+                    if !current_text.is_empty() {
+                        contents.push(HoverBlock {
+                            text: current_text,
+                            language,
+                        });
+                    }
+
+                    if contents.is_empty() {
+                        None
+                    } else {
+                        Some(contents)
+                    }
+                }
+            });
+
+            contents.map(|contents| Hover { contents, range })
+        }))
+    }
+
+    fn to_proto(&self, project_id: u64, buffer: &Buffer) -> Self::ProtoRequest {
+        proto::GetHover {
+            project_id,
+            buffer_id: buffer.remote_id(),
+            position: Some(language::proto::serialize_anchor(
+                &buffer.anchor_before(self.position),
+            )),
+            version: serialize_version(&buffer.version),
+        }
+    }
+
+    async fn from_proto(
+        message: Self::ProtoRequest,
+        _: ModelHandle<Project>,
+        buffer: ModelHandle<Buffer>,
+        mut cx: AsyncAppContext,
+    ) -> Result<Self> {
+        let position = message
+            .position
+            .and_then(deserialize_anchor)
+            .ok_or_else(|| anyhow!("invalid position"))?;
+        buffer
+            .update(&mut cx, |buffer, _| {
+                buffer.wait_for_version(deserialize_version(message.version))
+            })
+            .await;
+        Ok(Self {
+            position: buffer.read_with(&cx, |buffer, _| position.to_point_utf16(buffer)),
+        })
+    }
+
+    fn response_to_proto(
+        response: Self::Response,
+        _: &mut Project,
+        _: PeerId,
+        _: &clock::Global,
+        _: &AppContext,
+    ) -> proto::GetHoverResponse {
+        if let Some(response) = response {
+            let (start, end) = if let Some(range) = response.range {
+                (
+                    Some(language::proto::serialize_anchor(&range.start)),
+                    Some(language::proto::serialize_anchor(&range.end)),
+                )
+            } else {
+                (None, None)
+            };
+
+            let contents = response
+                .contents
+                .into_iter()
+                .map(|block| proto::HoverBlock {
+                    text: block.text,
+                    language: block.language,
+                })
+                .collect();
+
+            proto::GetHoverResponse {
+                start,
+                end,
+                contents,
+            }
+        } else {
+            proto::GetHoverResponse {
+                start: None,
+                end: None,
+                contents: Vec::new(),
+            }
+        }
+    }
+
+    async fn response_from_proto(
+        self,
+        message: proto::GetHoverResponse,
+        _: ModelHandle<Project>,
+        _: ModelHandle<Buffer>,
+        _: AsyncAppContext,
+    ) -> Result<Self::Response> {
+        println!("Response from proto");
+        let range = if let (Some(start), Some(end)) = (message.start, message.end) {
+            language::proto::deserialize_anchor(start)
+                .and_then(|start| language::proto::deserialize_anchor(end).map(|end| start..end))
+        } else {
+            None
+        };
+
+        let contents: Vec<_> = message
+            .contents
+            .into_iter()
+            .map(|block| HoverBlock {
+                text: block.text,
+                language: block.language,
+            })
+            .collect();
+
+        Ok(if contents.is_empty() {
+            None
+        } else {
+            Some(Hover { contents, range })
+        })
+    }
+
+    fn buffer_id_from_proto(message: &Self::ProtoRequest) -> u64 {
+        message.buffer_id
+    }
+}

crates/project/src/project.rs 🔗

@@ -23,7 +23,10 @@ use language::{
     LanguageRegistry, LanguageServerName, LocalFile, LspAdapter, OffsetRangeExt, Operation, Patch,
     PointUtf16, TextBufferSnapshot, ToOffset, ToPointUtf16, Transaction,
 };
-use lsp::{DiagnosticSeverity, DiagnosticTag, DocumentHighlightKind, LanguageServer};
+use lsp::{
+    DiagnosticSeverity, DiagnosticTag, DocumentHighlightKind, LanguageServer, LanguageString,
+    MarkedString,
+};
 use lsp_command::*;
 use parking_lot::Mutex;
 use postage::stream::Stream;
@@ -223,6 +226,38 @@ pub struct Symbol {
     pub signature: [u8; 32],
 }
 
+#[derive(Clone, Debug, PartialEq)]
+pub struct HoverBlock {
+    pub text: String,
+    pub language: Option<String>,
+}
+
+impl HoverBlock {
+    fn try_new(marked_string: MarkedString) -> Option<Self> {
+        let result = match marked_string {
+            MarkedString::LanguageString(LanguageString { language, value }) => HoverBlock {
+                text: value,
+                language: Some(language),
+            },
+            MarkedString::String(text) => HoverBlock {
+                text,
+                language: None,
+            },
+        };
+        if result.text.is_empty() {
+            None
+        } else {
+            Some(result)
+        }
+    }
+}
+
+#[derive(Debug)]
+pub struct Hover {
+    pub contents: Vec<HoverBlock>,
+    pub range: Option<Range<language::Anchor>>,
+}
+
 #[derive(Default)]
 pub struct ProjectTransaction(pub HashMap<ModelHandle<Buffer>, language::Transaction>);
 
@@ -314,6 +349,7 @@ impl Project {
         client.add_model_request_handler(Self::handle_format_buffers);
         client.add_model_request_handler(Self::handle_get_code_actions);
         client.add_model_request_handler(Self::handle_get_completions);
+        client.add_model_request_handler(Self::handle_lsp_command::<GetHover>);
         client.add_model_request_handler(Self::handle_lsp_command::<GetDefinition>);
         client.add_model_request_handler(Self::handle_lsp_command::<GetDocumentHighlights>);
         client.add_model_request_handler(Self::handle_lsp_command::<GetReferences>);
@@ -2912,6 +2948,16 @@ impl Project {
         }
     }
 
+    pub fn hover<T: ToPointUtf16>(
+        &self,
+        buffer: &ModelHandle<Buffer>,
+        position: T,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<Option<Hover>>> {
+        let position = position.to_point_utf16(buffer.read(cx));
+        self.request_lsp(buffer.clone(), GetHover { position }, cx)
+    }
+
     pub fn completions<T: ToPointUtf16>(
         &self,
         source_buffer_handle: &ModelHandle<Buffer>,

crates/rpc/proto/zed.proto 🔗

@@ -66,41 +66,43 @@ message Envelope {
         ApplyCompletionAdditionalEditsResponse apply_completion_additional_edits_response = 56;
         GetCodeActions get_code_actions = 57;
         GetCodeActionsResponse get_code_actions_response = 58;
-        ApplyCodeAction apply_code_action = 59;
-        ApplyCodeActionResponse apply_code_action_response = 60;
-        PrepareRename prepare_rename = 61;
-        PrepareRenameResponse prepare_rename_response = 62;
-        PerformRename perform_rename = 63;
-        PerformRenameResponse perform_rename_response = 64;
-        SearchProject search_project = 65;
-        SearchProjectResponse search_project_response = 66;
-
-        GetChannels get_channels = 67;
-        GetChannelsResponse get_channels_response = 68;
-        JoinChannel join_channel = 69;
-        JoinChannelResponse join_channel_response = 70;
-        LeaveChannel leave_channel = 71;
-        SendChannelMessage send_channel_message = 72;
-        SendChannelMessageResponse send_channel_message_response = 73;
-        ChannelMessageSent channel_message_sent = 74;
-        GetChannelMessages get_channel_messages = 75;
-        GetChannelMessagesResponse get_channel_messages_response = 76;
-
-        UpdateContacts update_contacts = 77;
-        UpdateInviteInfo update_invite_info = 78;
-        ShowContacts show_contacts = 79;
-
-        GetUsers get_users = 80;
-        FuzzySearchUsers fuzzy_search_users = 81;
-        UsersResponse users_response = 82;
-        RequestContact request_contact = 83;
-        RespondToContactRequest respond_to_contact_request = 84;
-        RemoveContact remove_contact = 85;
-
-        Follow follow = 86;
-        FollowResponse follow_response = 87;
-        UpdateFollowers update_followers = 88;
-        Unfollow unfollow = 89;
+        GetHover get_hover = 59;
+        GetHoverResponse get_hover_response = 60;
+        ApplyCodeAction apply_code_action = 61;
+        ApplyCodeActionResponse apply_code_action_response = 62;
+        PrepareRename prepare_rename = 63;
+        PrepareRenameResponse prepare_rename_response = 64;
+        PerformRename perform_rename = 65;
+        PerformRenameResponse perform_rename_response = 66;
+        SearchProject search_project = 67;
+        SearchProjectResponse search_project_response = 68;
+
+        GetChannels get_channels = 69;
+        GetChannelsResponse get_channels_response = 70;
+        JoinChannel join_channel = 71;
+        JoinChannelResponse join_channel_response = 72;
+        LeaveChannel leave_channel = 73;
+        SendChannelMessage send_channel_message = 74;
+        SendChannelMessageResponse send_channel_message_response = 75;
+        ChannelMessageSent channel_message_sent = 76;
+        GetChannelMessages get_channel_messages = 77;
+        GetChannelMessagesResponse get_channel_messages_response = 78;
+
+        UpdateContacts update_contacts = 79;
+        UpdateInviteInfo update_invite_info = 80;
+        ShowContacts show_contacts = 81;
+
+        GetUsers get_users = 82;
+        FuzzySearchUsers fuzzy_search_users = 83;
+        UsersResponse users_response = 84;
+        RequestContact request_contact = 85;
+        RespondToContactRequest respond_to_contact_request = 86;
+        RemoveContact remove_contact = 87;
+
+        Follow follow = 88;
+        FollowResponse follow_response = 89;
+        UpdateFollowers update_followers = 90;
+        Unfollow unfollow = 91;
     }
 }
 
@@ -426,6 +428,24 @@ message GetCodeActionsResponse {
     repeated VectorClockEntry version = 2;
 }
 
+message GetHover {
+    uint64 project_id = 1;
+    uint64 buffer_id = 2;
+    Anchor position = 3;
+    repeated VectorClockEntry version = 5;
+}
+
+message GetHoverResponse {
+    optional Anchor start = 1;
+    optional Anchor end = 2;
+    repeated HoverBlock contents = 3;
+}
+
+message HoverBlock {
+    string text = 1;
+    optional string language = 2;
+}
+
 message ApplyCodeAction {
     uint64 project_id = 1;
     uint64 buffer_id = 2;

crates/rpc/src/proto.rs 🔗

@@ -99,6 +99,8 @@ messages!(
     (GetChannelsResponse, Foreground),
     (GetCodeActions, Background),
     (GetCodeActionsResponse, Background),
+    (GetHover, Background),
+    (GetHoverResponse, Background),
     (GetCompletions, Background),
     (GetCompletionsResponse, Background),
     (GetDefinition, Background),
@@ -175,6 +177,7 @@ request_messages!(
     (GetChannelMessages, GetChannelMessagesResponse),
     (GetChannels, GetChannelsResponse),
     (GetCodeActions, GetCodeActionsResponse),
+    (GetHover, GetHoverResponse),
     (GetCompletions, GetCompletionsResponse),
     (GetDefinition, GetDefinitionResponse),
     (GetDocumentHighlights, GetDocumentHighlightsResponse),
@@ -221,6 +224,7 @@ entity_messages!(
     GetCompletions,
     GetDefinition,
     GetDocumentHighlights,
+    GetHover,
     GetReferences,
     GetProjectSymbols,
     JoinProject,

crates/theme/src/theme.rs 🔗

@@ -452,6 +452,7 @@ pub struct Editor {
     pub autocomplete: AutocompleteStyle,
     pub code_actions_indicator: Color,
     pub unnecessary_code_fade: f32,
+    pub hover_popover: HoverPopover,
 }
 
 #[derive(Clone, Deserialize, Default)]
@@ -630,3 +631,11 @@ impl<'de> Deserialize<'de> for SyntaxTheme {
         Ok(result)
     }
 }
+
+#[derive(Clone, Deserialize, Default)]
+pub struct HoverPopover {
+    pub container: ContainerStyle,
+    pub block_style: ContainerStyle,
+    pub prose: TextStyle,
+    pub highlight: Color,
+}

styles/src/styleTree/chatPanel.ts 🔗

@@ -4,9 +4,10 @@ import {
   backgroundColor,
   border,
   player,
-  shadow,
+  modalShadow,
   text,
-  TextColor
+  TextColor,
+  popoverShadow
 } from "./components";
 
 export default function chatPanel(theme: Theme) {
@@ -69,7 +70,7 @@ export default function chatPanel(theme: Theme) {
         cornerRadius: 6,
         padding: 4,
         border: border(theme, "primary"),
-        shadow: shadow(theme),
+        shadow: popoverShadow(theme),
       },
     },
     signInPrompt: text(theme, "sans", "secondary", { underline: true }),

styles/src/styleTree/components.ts 🔗

@@ -1,4 +1,5 @@
 import chroma from "chroma-js";
+import { isIPv4 } from "net";
 import Theme, { BackgroundColorSet } from "../themes/common/theme";
 import { fontFamilies, fontSizes, FontWeight } from "../tokens";
 import { Color } from "../utils/color";
@@ -84,10 +85,18 @@ export function backgroundColor(
   return theme.backgroundColor[name][state || "base"].value;
 }
 
-export function shadow(theme: Theme) {
+export function modalShadow(theme: Theme) {
   return {
     blur: 16,
-    color: chroma("black").alpha(theme.shadowAlpha.value).hex(),
+    color: theme.shadow.value,
     offset: [0, 2],
   };
 }
+
+export function popoverShadow(theme: Theme) {
+  return {
+    blur: 4,
+    color: theme.shadow.value,
+    offset: [1, 2],
+  };
+}

styles/src/styleTree/contextMenu.ts 🔗

@@ -1,12 +1,12 @@
 import Theme from "../themes/common/theme";
-import { backgroundColor, border, borderColor, shadow, text } from "./components";
+import { backgroundColor, border, borderColor, popoverShadow, text } from "./components";
 
 export default function contextMenu(theme: Theme) {
   return {
     background: backgroundColor(theme, 300, "base"),
     cornerRadius: 6,
     padding: 6,
-    shadow: shadow(theme),
+    shadow: popoverShadow(theme),
     border: border(theme, "primary"),
     item: {
       padding: { left: 4, right: 4, top: 2, bottom: 2 },

styles/src/styleTree/editor.ts 🔗

@@ -4,9 +4,11 @@ import {
   border,
   iconColor,
   player,
+  popoverShadow,
   text,
   TextColor
 } from "./components";
+import hoverPopover from "./hoverPopover";
 
 export default function editor(theme: Theme) {
   const autocompleteItem = {
@@ -80,6 +82,7 @@ export default function editor(theme: Theme) {
       cornerRadius: 8,
       padding: 4,
       border: border(theme, "secondary"),
+      shadow: popoverShadow(theme),
       item: autocompleteItem,
       hoveredItem: {
         ...autocompleteItem,
@@ -143,6 +146,7 @@ export default function editor(theme: Theme) {
     invalidHintDiagnostic: diagnostic(theme, "muted"),
     invalidInformationDiagnostic: diagnostic(theme, "muted"),
     invalidWarningDiagnostic: diagnostic(theme, "muted"),
+    hover_popover: hoverPopover(theme),
     syntax,
   };
 }

styles/src/styleTree/hoverPopover.ts 🔗

@@ -0,0 +1,27 @@
+import Theme from "../themes/common/theme";
+import { backgroundColor, border, popoverShadow, text } from "./components";
+
+export default function HoverPopover(theme: Theme) {
+  return {
+    container: {
+      background: backgroundColor(theme, "on500"),
+      cornerRadius: 8,
+      padding: {
+        left: 8,
+        right: 8,
+        top: 4,
+        bottom: 4
+      },
+      shadow: popoverShadow(theme),
+      border: border(theme, "primary"),
+      margin: {
+        left: -8,
+      },
+    },
+    block_style: {
+      padding: { top: 4 },
+    },
+    prose: text(theme, "sans", "primary", { "size": "sm" }),
+    highlight: theme.editor.highlight.occurrence.value,
+  }
+}

styles/src/styleTree/picker.ts 🔗

@@ -1,5 +1,5 @@
 import Theme from "../themes/common/theme";
-import { backgroundColor, border, player, shadow, text } from "./components";
+import { backgroundColor, border, player, modalShadow, text } from "./components";
 
 export default function picker(theme: Theme) {
   return {
@@ -48,6 +48,6 @@ export default function picker(theme: Theme) {
         top: 7,
       },
     },
-    shadow: shadow(theme),
+    shadow: modalShadow(theme),
   };
 }

styles/src/styleTree/tooltip.ts 🔗

@@ -1,5 +1,5 @@
 import Theme from "../themes/common/theme";
-import { backgroundColor, border, shadow, text } from "./components";
+import { backgroundColor, border, popoverShadow, text } from "./components";
 
 export default function tooltip(theme: Theme) {
   return {
@@ -7,7 +7,7 @@ export default function tooltip(theme: Theme) {
     border: border(theme, "secondary"),
     padding: { top: 4, bottom: 4, left: 8, right: 8 },
     margin: { top: 6, left: 6 },
-    shadow: shadow(theme),
+    shadow: popoverShadow(theme),
     cornerRadius: 6,
     text: text(theme, "sans", "secondary", { size: "xs", weight: "bold" }),
     keystroke: {

styles/src/styleTree/workspace.ts 🔗

@@ -1,6 +1,6 @@
 import Theme from "../themes/common/theme";
 import { withOpacity } from "../utils/color";
-import { backgroundColor, border, iconColor, shadow, text } from "./components";
+import { backgroundColor, border, iconColor, modalShadow, text } from "./components";
 import statusBar from "./statusBar";
 
 export function workspaceBackground(theme: Theme) {
@@ -164,7 +164,7 @@ export default function workspace(theme: Theme) {
       cornerRadius: 6,
       padding: 12,
       border: border(theme, "primary"),
-      shadow: shadow(theme),
+      shadow: modalShadow(theme),
     },
     notifications: {
       width: 380,

styles/src/themes/common/base16.ts 🔗

@@ -14,7 +14,6 @@ export function createTheme(
   name: string,
   isLight: boolean,
   ramps: { [rampName: string]: Scale },
-  blend?: number
 ): Theme {
   if (isLight) {
     for (var rampName in ramps) {
@@ -25,100 +24,99 @@ export function createTheme(
     ramps.neutral = ramps.neutral.domain([0, 7]);
   }
 
-  if (blend === undefined) {
-    blend = isLight ? 0.12 : 0.24;
-  }
+  let blend = isLight ? 0.12 : 0.24;
 
-  function rampColor(ramp: Scale, index: number): ColorToken {
+  function sample(ramp: Scale, index: number): ColorToken {
     return color(ramp(index).hex());
   }
+  const darkest = color(ramps.neutral(isLight ? 7 : 0).hex());
 
   const backgroundColor = {
     // Title bar
     100: {
-      base: rampColor(ramps.neutral, 1.25),
-      hovered: rampColor(ramps.neutral, 1.5),
-      active: rampColor(ramps.neutral, 1.75),
+      base: sample(ramps.neutral, 1.25),
+      hovered: sample(ramps.neutral, 1.5),
+      active: sample(ramps.neutral, 1.75),
     },
     // Midground (panels, etc)
     300: {
-      base: rampColor(ramps.neutral, 1),
-      hovered: rampColor(ramps.neutral, 1.25),
-      active: rampColor(ramps.neutral, 1.5),
+      base: sample(ramps.neutral, 1),
+      hovered: sample(ramps.neutral, 1.25),
+      active: sample(ramps.neutral, 1.5),
     },
     // Editor
     500: {
-      base: rampColor(ramps.neutral, 0),
-      hovered: rampColor(ramps.neutral, 0.25),
-      active: rampColor(ramps.neutral, 0.5),
+      base: sample(ramps.neutral, 0),
+      hovered: sample(ramps.neutral, 0.25),
+      active: sample(ramps.neutral, 0.5),
     },
     on300: {
-      base: rampColor(ramps.neutral, 0),
-      hovered: rampColor(ramps.neutral, 0.25),
-      active: rampColor(ramps.neutral, 0.5),
+      base: sample(ramps.neutral, 0),
+      hovered: sample(ramps.neutral, 0.25),
+      active: sample(ramps.neutral, 0.5),
     },
     on500: {
-      base: rampColor(ramps.neutral, 1.25),
-      hovered: rampColor(ramps.neutral, 1.5),
-      active: rampColor(ramps.neutral, 1.75),
+      base: sample(ramps.neutral, 1.25),
+      hovered: sample(ramps.neutral, 1.5),
+      active: sample(ramps.neutral, 1.75),
     },
     ok: {
-      base: withOpacity(rampColor(ramps.green, 0.5), 0.15),
-      hovered: withOpacity(rampColor(ramps.green, 0.5), 0.2),
-      active: withOpacity(rampColor(ramps.green, 0.5), 0.25),
+      base: withOpacity(sample(ramps.green, 0.5), 0.15),
+      hovered: withOpacity(sample(ramps.green, 0.5), 0.2),
+      active: withOpacity(sample(ramps.green, 0.5), 0.25),
     },
     error: {
-      base: withOpacity(rampColor(ramps.red, 0.5), 0.15),
-      hovered: withOpacity(rampColor(ramps.red, 0.5), 0.2),
-      active: withOpacity(rampColor(ramps.red, 0.5), 0.25),
+      base: withOpacity(sample(ramps.red, 0.5), 0.15),
+      hovered: withOpacity(sample(ramps.red, 0.5), 0.2),
+      active: withOpacity(sample(ramps.red, 0.5), 0.25),
     },
     warning: {
-      base: withOpacity(rampColor(ramps.yellow, 0.5), 0.15),
-      hovered: withOpacity(rampColor(ramps.yellow, 0.5), 0.2),
-      active: withOpacity(rampColor(ramps.yellow, 0.5), 0.25),
+      base: withOpacity(sample(ramps.yellow, 0.5), 0.15),
+      hovered: withOpacity(sample(ramps.yellow, 0.5), 0.2),
+      active: withOpacity(sample(ramps.yellow, 0.5), 0.25),
     },
     info: {
-      base: withOpacity(rampColor(ramps.blue, 0.5), 0.15),
-      hovered: withOpacity(rampColor(ramps.blue, 0.5), 0.2),
-      active: withOpacity(rampColor(ramps.blue, 0.5), 0.25),
+      base: withOpacity(sample(ramps.blue, 0.5), 0.15),
+      hovered: withOpacity(sample(ramps.blue, 0.5), 0.2),
+      active: withOpacity(sample(ramps.blue, 0.5), 0.25),
     },
   };
 
   const borderColor = {
-    primary: rampColor(ramps.neutral, isLight ? 1.5 : 0),
-    secondary: rampColor(ramps.neutral, isLight ? 1.25 : 1),
-    muted: rampColor(ramps.neutral, isLight ? 1 : 3),
-    active: rampColor(ramps.neutral, isLight ? 4 : 3),
-    onMedia: withOpacity(rampColor(ramps.neutral, 0), 0.1),
-    ok: withOpacity(rampColor(ramps.green, 0.5), 0.15),
-    error: withOpacity(rampColor(ramps.red, 0.5), 0.15),
-    warning: withOpacity(rampColor(ramps.yellow, 0.5), 0.15),
-    info: withOpacity(rampColor(ramps.blue, 0.5), 0.15),
+    primary: sample(ramps.neutral, isLight ? 1.5 : 0),
+    secondary: sample(ramps.neutral, isLight ? 1.25 : 1),
+    muted: sample(ramps.neutral, isLight ? 1 : 3),
+    active: sample(ramps.neutral, isLight ? 4 : 3),
+    onMedia: withOpacity(darkest, 0.1),
+    ok: withOpacity(sample(ramps.green, 0.5), 0.15),
+    error: withOpacity(sample(ramps.red, 0.5), 0.15),
+    warning: withOpacity(sample(ramps.yellow, 0.5), 0.15),
+    info: withOpacity(sample(ramps.blue, 0.5), 0.15),
   };
 
   const textColor = {
-    primary: rampColor(ramps.neutral, 6),
-    secondary: rampColor(ramps.neutral, 5),
-    muted: rampColor(ramps.neutral, 5),
-    placeholder: rampColor(ramps.neutral, 4),
-    active: rampColor(ramps.neutral, 7),
-    feature: rampColor(ramps.blue, 0.5),
-    ok: rampColor(ramps.green, 0.5),
-    error: rampColor(ramps.red, 0.5),
-    warning: rampColor(ramps.yellow, 0.5),
-    info: rampColor(ramps.blue, 0.5),
-    onMedia: rampColor(ramps.neutral, isLight ? 0 : 7),
+    primary: sample(ramps.neutral, 6),
+    secondary: sample(ramps.neutral, 5),
+    muted: sample(ramps.neutral, 5),
+    placeholder: sample(ramps.neutral, 4),
+    active: sample(ramps.neutral, 7),
+    feature: sample(ramps.blue, 0.5),
+    ok: sample(ramps.green, 0.5),
+    error: sample(ramps.red, 0.5),
+    warning: sample(ramps.yellow, 0.5),
+    info: sample(ramps.blue, 0.5),
+    onMedia: darkest,
   };
 
   const player = {
-    1: buildPlayer(rampColor(ramps.blue, 0.5)),
-    2: buildPlayer(rampColor(ramps.green, 0.5)),
-    3: buildPlayer(rampColor(ramps.magenta, 0.5)),
-    4: buildPlayer(rampColor(ramps.orange, 0.5)),
-    5: buildPlayer(rampColor(ramps.violet, 0.5)),
-    6: buildPlayer(rampColor(ramps.cyan, 0.5)),
-    7: buildPlayer(rampColor(ramps.red, 0.5)),
-    8: buildPlayer(rampColor(ramps.yellow, 0.5)),
+    1: buildPlayer(sample(ramps.blue, 0.5)),
+    2: buildPlayer(sample(ramps.green, 0.5)),
+    3: buildPlayer(sample(ramps.magenta, 0.5)),
+    4: buildPlayer(sample(ramps.orange, 0.5)),
+    5: buildPlayer(sample(ramps.violet, 0.5)),
+    6: buildPlayer(sample(ramps.cyan, 0.5)),
+    7: buildPlayer(sample(ramps.red, 0.5)),
+    8: buildPlayer(sample(ramps.yellow, 0.5)),
   };
 
   const editor = {
@@ -126,16 +124,16 @@ export function createTheme(
     indent_guide: borderColor.muted,
     indent_guide_active: borderColor.secondary,
     line: {
-      active: rampColor(ramps.neutral, 1),
-      highlighted: rampColor(ramps.neutral, 1.25), // TODO: Where is this used?
+      active: sample(ramps.neutral, 1),
+      highlighted: sample(ramps.neutral, 1.25), // TODO: Where is this used?
     },
     highlight: {
       selection: player[1].selectionColor,
-      occurrence: withOpacity(rampColor(ramps.neutral, 3.5), blend),
-      activeOccurrence: withOpacity(rampColor(ramps.neutral, 3.5), blend * 2), // TODO: Not hooked up - https://github.com/zed-industries/zed/issues/751
+      occurrence: withOpacity(sample(ramps.neutral, 3.5), blend),
+      activeOccurrence: withOpacity(sample(ramps.neutral, 3.5), blend * 2), // TODO: Not hooked up - https://github.com/zed-industries/zed/issues/751
       matchingBracket: backgroundColor[500].active, // TODO: Not hooked up
-      match: rampColor(ramps.violet, 0.15),
-      activeMatch: withOpacity(rampColor(ramps.violet, 0.4), blend * 2), // TODO: Not hooked up - https://github.com/zed-industries/zed/issues/751
+      match: sample(ramps.violet, 0.15),
+      activeMatch: withOpacity(sample(ramps.violet, 0.4), blend * 2), // TODO: Not hooked up - https://github.com/zed-industries/zed/issues/751
       related: backgroundColor[500].hovered,
     },
     gutter: {
@@ -146,59 +144,59 @@ export function createTheme(
 
   const syntax: Syntax = {
     primary: {
-      color: rampColor(ramps.neutral, 7),
+      color: sample(ramps.neutral, 7),
       weight: fontWeights.normal,
     },
     comment: {
-      color: rampColor(ramps.neutral, 5),
+      color: sample(ramps.neutral, 5),
       weight: fontWeights.normal,
     },
     punctuation: {
-      color: rampColor(ramps.neutral, 6),
+      color: sample(ramps.neutral, 6),
       weight: fontWeights.normal,
     },
     constant: {
-      color: rampColor(ramps.neutral, 4),
+      color: sample(ramps.neutral, 4),
       weight: fontWeights.normal,
     },
     keyword: {
-      color: rampColor(ramps.blue, 0.5),
+      color: sample(ramps.blue, 0.5),
       weight: fontWeights.normal,
     },
     function: {
-      color: rampColor(ramps.yellow, 0.5),
+      color: sample(ramps.yellow, 0.5),
       weight: fontWeights.normal,
     },
     type: {
-      color: rampColor(ramps.cyan, 0.5),
+      color: sample(ramps.cyan, 0.5),
       weight: fontWeights.normal,
     },
     variant: {
-      color: rampColor(ramps.blue, 0.5),
+      color: sample(ramps.blue, 0.5),
       weight: fontWeights.normal,
     },
     property: {
-      color: rampColor(ramps.blue, 0.5),
+      color: sample(ramps.blue, 0.5),
       weight: fontWeights.normal,
     },
     enum: {
-      color: rampColor(ramps.orange, 0.5),
+      color: sample(ramps.orange, 0.5),
       weight: fontWeights.normal,
     },
     operator: {
-      color: rampColor(ramps.orange, 0.5),
+      color: sample(ramps.orange, 0.5),
       weight: fontWeights.normal,
     },
     string: {
-      color: rampColor(ramps.orange, 0.5),
+      color: sample(ramps.orange, 0.5),
       weight: fontWeights.normal,
     },
     number: {
-      color: rampColor(ramps.green, 0.5),
+      color: sample(ramps.green, 0.5),
       weight: fontWeights.normal,
     },
     boolean: {
-      color: rampColor(ramps.green, 0.5),
+      color: sample(ramps.green, 0.5),
       weight: fontWeights.normal,
     },
     predictive: {
@@ -206,7 +204,7 @@ export function createTheme(
       weight: fontWeights.normal,
     },
     title: {
-      color: rampColor(ramps.yellow, 0.5),
+      color: sample(ramps.yellow, 0.5),
       weight: fontWeights.bold,
     },
     emphasis: {
@@ -218,21 +216,20 @@ export function createTheme(
       weight: fontWeights.bold,
     },
     linkUri: {
-      color: rampColor(ramps.green, 0.5),
+      color: sample(ramps.green, 0.5),
       weight: fontWeights.normal,
       underline: true,
     },
     linkText: {
-      color: rampColor(ramps.orange, 0.5),
+      color: sample(ramps.orange, 0.5),
       weight: fontWeights.normal,
       italic: true,
     },
   };
 
-  const shadowAlpha: NumberToken = {
-    value: blend,
-    type: "number",
-  };
+  const shadow = withOpacity(
+    color(ramps.neutral(isLight ? 7 : 0).darken().hex()),
+    blend);
 
   return {
     name,
@@ -243,6 +240,6 @@ export function createTheme(
     editor,
     syntax,
     player,
-    shadowAlpha,
+    shadow,
   };
 }

styles/src/themes/common/theme.ts 🔗

@@ -153,6 +153,6 @@ export default interface Theme {
     6: Player;
     7: Player;
     8: Player;
-  };
-  shadowAlpha: NumberToken;
+  },
+  shadow: ColorToken;
 }