Merge branch 'main' into user-timeline
This commit is contained in:
commit
d3b9eca791
50 changed files with 3076 additions and 1109 deletions
224
Cargo.lock
generated
224
Cargo.lock
generated
|
@ -2,6 +2,22 @@
|
||||||
# It is not intended for manual editing.
|
# It is not intended for manual editing.
|
||||||
version = 3
|
version = 3
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "activity_indicator"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"auto_update",
|
||||||
|
"editor",
|
||||||
|
"futures",
|
||||||
|
"gpui",
|
||||||
|
"language",
|
||||||
|
"project",
|
||||||
|
"settings",
|
||||||
|
"smallvec",
|
||||||
|
"util",
|
||||||
|
"workspace",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "addr2line"
|
name = "addr2line"
|
||||||
version = "0.17.0"
|
version = "0.17.0"
|
||||||
|
@ -43,6 +59,45 @@ dependencies = [
|
||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "alacritty_config_derive"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "77044c45bdb871e501b5789ad16293ecb619e5733b60f4bb01d1cb31c463c336"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "alacritty_terminal"
|
||||||
|
version = "0.16.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "02fb5d4af84e39f9754d039ff6de2233c8996dbae0af74910156e559e5766e2f"
|
||||||
|
dependencies = [
|
||||||
|
"alacritty_config_derive",
|
||||||
|
"base64 0.13.0",
|
||||||
|
"bitflags",
|
||||||
|
"dirs 3.0.2",
|
||||||
|
"libc",
|
||||||
|
"log",
|
||||||
|
"mio 0.6.23",
|
||||||
|
"mio-anonymous-pipes",
|
||||||
|
"mio-extras",
|
||||||
|
"miow 0.3.7",
|
||||||
|
"nix",
|
||||||
|
"parking_lot 0.11.2",
|
||||||
|
"regex-automata",
|
||||||
|
"serde",
|
||||||
|
"serde_yaml",
|
||||||
|
"signal-hook",
|
||||||
|
"signal-hook-mio",
|
||||||
|
"unicode-width",
|
||||||
|
"vte",
|
||||||
|
"winapi 0.3.9",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ansi_term"
|
name = "ansi_term"
|
||||||
version = "0.12.1"
|
version = "0.12.1"
|
||||||
|
@ -2500,6 +2555,12 @@ dependencies = [
|
||||||
"safemem",
|
"safemem",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "linked-hash-map"
|
||||||
|
version = "0.5.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lipsum"
|
name = "lipsum"
|
||||||
version = "0.8.2"
|
version = "0.8.2"
|
||||||
|
@ -2566,21 +2627,6 @@ dependencies = [
|
||||||
"url",
|
"url",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "lsp_status"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"editor",
|
|
||||||
"futures",
|
|
||||||
"gpui",
|
|
||||||
"language",
|
|
||||||
"project",
|
|
||||||
"settings",
|
|
||||||
"smallvec",
|
|
||||||
"util",
|
|
||||||
"workspace",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "malloc_buf"
|
name = "malloc_buf"
|
||||||
version = "0.0.6"
|
version = "0.0.6"
|
||||||
|
@ -2724,7 +2770,7 @@ dependencies = [
|
||||||
"kernel32-sys",
|
"kernel32-sys",
|
||||||
"libc",
|
"libc",
|
||||||
"log",
|
"log",
|
||||||
"miow",
|
"miow 0.2.2",
|
||||||
"net2",
|
"net2",
|
||||||
"slab",
|
"slab",
|
||||||
"winapi 0.2.8",
|
"winapi 0.2.8",
|
||||||
|
@ -2742,6 +2788,42 @@ dependencies = [
|
||||||
"windows-sys",
|
"windows-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mio-anonymous-pipes"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6bc513025fe5005a3aa561b50fdb2cda5a150b84800ae02acd8aa9ed62ca1a6b"
|
||||||
|
dependencies = [
|
||||||
|
"mio 0.6.23",
|
||||||
|
"miow 0.3.7",
|
||||||
|
"parking_lot 0.11.2",
|
||||||
|
"spsc-buffer",
|
||||||
|
"winapi 0.3.9",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mio-extras"
|
||||||
|
version = "2.0.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "52403fe290012ce777c4626790c8951324a2b9e3316b3143779c72b029742f19"
|
||||||
|
dependencies = [
|
||||||
|
"lazycell",
|
||||||
|
"log",
|
||||||
|
"mio 0.6.23",
|
||||||
|
"slab",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mio-uds"
|
||||||
|
version = "0.6.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "afcb699eb26d4332647cc848492bbc15eafb26f08d0304550d5aa1f612e066f0"
|
||||||
|
dependencies = [
|
||||||
|
"iovec",
|
||||||
|
"libc",
|
||||||
|
"mio 0.6.23",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "miow"
|
name = "miow"
|
||||||
version = "0.2.2"
|
version = "0.2.2"
|
||||||
|
@ -2754,6 +2836,15 @@ dependencies = [
|
||||||
"ws2_32-sys",
|
"ws2_32-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "miow"
|
||||||
|
version = "0.3.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21"
|
||||||
|
dependencies = [
|
||||||
|
"winapi 0.3.9",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "multimap"
|
name = "multimap"
|
||||||
version = "0.8.3"
|
version = "0.8.3"
|
||||||
|
@ -2798,6 +2889,19 @@ dependencies = [
|
||||||
"winapi 0.3.9",
|
"winapi 0.3.9",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nix"
|
||||||
|
version = "0.22.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e4916f159ed8e5de0082076562152a76b7a1f64a01fd9d1e0fea002c37624faf"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"cc",
|
||||||
|
"cfg-if 1.0.0",
|
||||||
|
"libc",
|
||||||
|
"memoffset",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nom"
|
name = "nom"
|
||||||
version = "7.1.1"
|
version = "7.1.1"
|
||||||
|
@ -4252,6 +4356,18 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_yaml"
|
||||||
|
version = "0.8.24"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "707d15895415db6628332b737c838b88c598522e4dc70647e59b72312924aebc"
|
||||||
|
dependencies = [
|
||||||
|
"indexmap",
|
||||||
|
"ryu",
|
||||||
|
"serde",
|
||||||
|
"yaml-rust",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "servo-fontconfig"
|
name = "servo-fontconfig"
|
||||||
version = "0.5.1"
|
version = "0.5.1"
|
||||||
|
@ -4364,6 +4480,18 @@ dependencies = [
|
||||||
"signal-hook-registry",
|
"signal-hook-registry",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "signal-hook-mio"
|
||||||
|
version = "0.2.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"mio 0.6.23",
|
||||||
|
"mio-uds",
|
||||||
|
"signal-hook",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "signal-hook-registry"
|
name = "signal-hook-registry"
|
||||||
version = "1.4.0"
|
version = "1.4.0"
|
||||||
|
@ -4492,6 +4620,12 @@ version = "0.5.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
|
checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "spsc-buffer"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "be6c3f39c37a4283ee4b43d1311c828f2e1fb0541e76ea0cb1a2abd9ef2f5b3b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sqlformat"
|
name = "sqlformat"
|
||||||
version = "0.1.8"
|
version = "0.1.8"
|
||||||
|
@ -4739,6 +4873,25 @@ dependencies = [
|
||||||
"winapi-util",
|
"winapi-util",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "terminal"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"alacritty_terminal",
|
||||||
|
"editor",
|
||||||
|
"futures",
|
||||||
|
"gpui",
|
||||||
|
"itertools",
|
||||||
|
"mio-extras",
|
||||||
|
"ordered-float",
|
||||||
|
"project",
|
||||||
|
"settings",
|
||||||
|
"smallvec",
|
||||||
|
"theme",
|
||||||
|
"util",
|
||||||
|
"workspace",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "text"
|
name = "text"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
@ -5531,6 +5684,12 @@ version = "0.7.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
|
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "utf8parse"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "936e4b492acfd135421d8dca4b1aa80a7bfc26e702ef3af710e0752684df5372"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "util"
|
name = "util"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
@ -5610,12 +5769,33 @@ dependencies = [
|
||||||
"language",
|
"language",
|
||||||
"log",
|
"log",
|
||||||
"project",
|
"project",
|
||||||
|
"search",
|
||||||
"serde",
|
"serde",
|
||||||
"settings",
|
"settings",
|
||||||
"util",
|
"util",
|
||||||
"workspace",
|
"workspace",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "vte"
|
||||||
|
version = "0.10.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6cbce692ab4ca2f1f3047fcf732430249c0e971bfdd2b234cf2c47ad93af5983"
|
||||||
|
dependencies = [
|
||||||
|
"utf8parse",
|
||||||
|
"vte_generate_state_changes",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "vte_generate_state_changes"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d257817081c7dffcdbab24b9e62d2def62e2ff7d00b1c20062551e6cccc145ff"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "waker-fn"
|
name = "waker-fn"
|
||||||
version = "1.1.0"
|
version = "1.1.0"
|
||||||
|
@ -5967,10 +6147,20 @@ version = "0.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9"
|
checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "yaml-rust"
|
||||||
|
version = "0.4.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85"
|
||||||
|
dependencies = [
|
||||||
|
"linked-hash-map",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zed"
|
name = "zed"
|
||||||
version = "0.42.0"
|
version = "0.42.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"activity_indicator",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"assets",
|
"assets",
|
||||||
"async-compression",
|
"async-compression",
|
||||||
|
@ -6011,7 +6201,6 @@ dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"log",
|
"log",
|
||||||
"lsp",
|
"lsp",
|
||||||
"lsp_status",
|
|
||||||
"num_cpus",
|
"num_cpus",
|
||||||
"outline",
|
"outline",
|
||||||
"parking_lot 0.11.2",
|
"parking_lot 0.11.2",
|
||||||
|
@ -6034,6 +6223,7 @@ dependencies = [
|
||||||
"smol",
|
"smol",
|
||||||
"sum_tree",
|
"sum_tree",
|
||||||
"tempdir",
|
"tempdir",
|
||||||
|
"terminal",
|
||||||
"text",
|
"text",
|
||||||
"theme",
|
"theme",
|
||||||
"theme_selector",
|
"theme_selector",
|
||||||
|
|
|
@ -13,6 +13,8 @@
|
||||||
"ctrl-c": "menu::Cancel",
|
"ctrl-c": "menu::Cancel",
|
||||||
"shift-cmd-{": "pane::ActivatePrevItem",
|
"shift-cmd-{": "pane::ActivatePrevItem",
|
||||||
"shift-cmd-}": "pane::ActivateNextItem",
|
"shift-cmd-}": "pane::ActivateNextItem",
|
||||||
|
"alt-cmd-left": "pane::ActivatePrevItem",
|
||||||
|
"alt-cmd-right": "pane::ActivateNextItem",
|
||||||
"cmd-w": "pane::CloseActiveItem",
|
"cmd-w": "pane::CloseActiveItem",
|
||||||
"cmd-shift-W": "workspace::CloseWindow",
|
"cmd-shift-W": "workspace::CloseWindow",
|
||||||
"alt-cmd-t": "pane::CloseInactiveItems",
|
"alt-cmd-t": "pane::CloseInactiveItems",
|
||||||
|
@ -210,6 +212,43 @@
|
||||||
{
|
{
|
||||||
"context": "Pane",
|
"context": "Pane",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
|
"ctrl-1": [
|
||||||
|
"pane::ActivateItem",
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"ctrl-2": [
|
||||||
|
"pane::ActivateItem",
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"ctrl-3": [
|
||||||
|
"pane::ActivateItem",
|
||||||
|
2
|
||||||
|
],
|
||||||
|
"ctrl-4": [
|
||||||
|
"pane::ActivateItem",
|
||||||
|
3
|
||||||
|
],
|
||||||
|
"ctrl-5": [
|
||||||
|
"pane::ActivateItem",
|
||||||
|
4
|
||||||
|
],
|
||||||
|
"ctrl-6": [
|
||||||
|
"pane::ActivateItem",
|
||||||
|
5
|
||||||
|
],
|
||||||
|
"ctrl-7": [
|
||||||
|
"pane::ActivateItem",
|
||||||
|
6
|
||||||
|
],
|
||||||
|
"ctrl-8": [
|
||||||
|
"pane::ActivateItem",
|
||||||
|
7
|
||||||
|
],
|
||||||
|
"ctrl-9": [
|
||||||
|
"pane::ActivateItem",
|
||||||
|
8
|
||||||
|
],
|
||||||
|
"ctrl-0": "pane::ActivateLastItem",
|
||||||
"ctrl--": "pane::GoBack",
|
"ctrl--": "pane::GoBack",
|
||||||
"shift-ctrl-_": "pane::GoForward",
|
"shift-ctrl-_": "pane::GoForward",
|
||||||
"cmd-shift-T": "pane::ReopenClosedItem",
|
"cmd-shift-T": "pane::ReopenClosedItem",
|
||||||
|
@ -219,6 +258,43 @@
|
||||||
{
|
{
|
||||||
"context": "Workspace",
|
"context": "Workspace",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
|
"cmd-1": [
|
||||||
|
"workspace::ActivatePane",
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"cmd-2": [
|
||||||
|
"workspace::ActivatePane",
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"cmd-3": [
|
||||||
|
"workspace::ActivatePane",
|
||||||
|
2
|
||||||
|
],
|
||||||
|
"cmd-4": [
|
||||||
|
"workspace::ActivatePane",
|
||||||
|
3
|
||||||
|
],
|
||||||
|
"cmd-5": [
|
||||||
|
"workspace::ActivatePane",
|
||||||
|
4
|
||||||
|
],
|
||||||
|
"cmd-6": [
|
||||||
|
"workspace::ActivatePane",
|
||||||
|
5
|
||||||
|
],
|
||||||
|
"cmd-7": [
|
||||||
|
"workspace::ActivatePane",
|
||||||
|
6
|
||||||
|
],
|
||||||
|
"cmd-8": [
|
||||||
|
"workspace::ActivatePane",
|
||||||
|
7
|
||||||
|
],
|
||||||
|
"cmd-9": [
|
||||||
|
"workspace::ActivatePane",
|
||||||
|
8
|
||||||
|
],
|
||||||
|
"cmd-b": "workspace::ToggleLeftSidebar",
|
||||||
"cmd-shift-F": "project_search::Deploy",
|
"cmd-shift-F": "project_search::Deploy",
|
||||||
"cmd-k cmd-t": "theme_selector::Toggle",
|
"cmd-k cmd-t": "theme_selector::Toggle",
|
||||||
"cmd-k cmd-s": "zed::OpenKeymap",
|
"cmd-k cmd-s": "zed::OpenKeymap",
|
||||||
|
@ -226,6 +302,7 @@
|
||||||
"cmd-p": "file_finder::Toggle",
|
"cmd-p": "file_finder::Toggle",
|
||||||
"cmd-shift-P": "command_palette::Toggle",
|
"cmd-shift-P": "command_palette::Toggle",
|
||||||
"cmd-shift-M": "diagnostics::Deploy",
|
"cmd-shift-M": "diagnostics::Deploy",
|
||||||
|
"cmd-shift-E": "project_panel::Toggle",
|
||||||
"cmd-alt-s": "workspace::SaveAll"
|
"cmd-alt-s": "workspace::SaveAll"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -310,34 +387,8 @@
|
||||||
{
|
{
|
||||||
"context": "Workspace",
|
"context": "Workspace",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"cmd-1": [
|
"cmd-shift-C": "contacts_panel::Toggle",
|
||||||
"workspace::ToggleSidebarItemFocus",
|
"cmd-shift-B": "workspace::ToggleRightSidebar"
|
||||||
{
|
|
||||||
"side": "Left",
|
|
||||||
"item_index": 0
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"cmd-shift-!": [
|
|
||||||
"workspace::ToggleSidebarItem",
|
|
||||||
{
|
|
||||||
"side": "Left",
|
|
||||||
"item_index": 0
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"cmd-9": [
|
|
||||||
"workspace::ToggleSidebarItemFocus",
|
|
||||||
{
|
|
||||||
"side": "Right",
|
|
||||||
"item_index": 0
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"cmd-shift-(": [
|
|
||||||
"workspace::ToggleSidebarItem",
|
|
||||||
{
|
|
||||||
"side": "Right",
|
|
||||||
"item_index": 0
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -352,5 +403,21 @@
|
||||||
"f2": "project_panel::Rename",
|
"f2": "project_panel::Rename",
|
||||||
"backspace": "project_panel::Delete"
|
"backspace": "project_panel::Delete"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"context": "Terminal",
|
||||||
|
"bindings": {
|
||||||
|
"ctrl-c": "terminal::Sigint",
|
||||||
|
"escape": "terminal::Escape",
|
||||||
|
"ctrl-d": "terminal::Quit",
|
||||||
|
"backspace": "terminal::Del",
|
||||||
|
"enter": "terminal::Return",
|
||||||
|
"left": "terminal::Left",
|
||||||
|
"right": "terminal::Right",
|
||||||
|
"up": "terminal::Up",
|
||||||
|
"down": "terminal::Down",
|
||||||
|
"tab": "terminal::Tab",
|
||||||
|
"cmd-v": "terminal::Paste"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
|
@ -37,16 +37,12 @@
|
||||||
"ignorePunctuation": true
|
"ignorePunctuation": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"escape": [
|
"escape": "editor::Cancel"
|
||||||
"vim::SwitchMode",
|
|
||||||
"Normal"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "Editor && vim_mode == normal",
|
"context": "Editor && vim_mode == normal && vim_operator == none",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"escape": "editor::Cancel",
|
|
||||||
"c": [
|
"c": [
|
||||||
"vim::PushOperator",
|
"vim::PushOperator",
|
||||||
"Change"
|
"Change"
|
||||||
|
@ -92,7 +88,13 @@
|
||||||
"p": "vim::Paste",
|
"p": "vim::Paste",
|
||||||
"u": "editor::Undo",
|
"u": "editor::Undo",
|
||||||
"ctrl-r": "editor::Redo",
|
"ctrl-r": "editor::Redo",
|
||||||
"ctrl-o": "pane::GoBack"
|
"ctrl-o": "pane::GoBack",
|
||||||
|
"/": [
|
||||||
|
"buffer_search::Deploy",
|
||||||
|
{
|
||||||
|
"focus": true
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -146,11 +148,5 @@
|
||||||
"escape": "vim::NormalBefore",
|
"escape": "vim::NormalBefore",
|
||||||
"ctrl-c": "vim::NormalBefore"
|
"ctrl-c": "vim::NormalBefore"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
{
|
|
||||||
"context": "Editor && mode == singleline",
|
|
||||||
"bindings": {
|
|
||||||
"escape": "editor::Cancel"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
]
|
]
|
|
@ -1,13 +1,14 @@
|
||||||
[package]
|
[package]
|
||||||
name = "lsp_status"
|
name = "activity_indicator"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
path = "src/lsp_status.rs"
|
path = "src/activity_indicator.rs"
|
||||||
doctest = false
|
doctest = false
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
auto_update = { path = "../auto_update" }
|
||||||
editor = { path = "../editor" }
|
editor = { path = "../editor" }
|
||||||
language = { path = "../language" }
|
language = { path = "../language" }
|
||||||
gpui = { path = "../gpui" }
|
gpui = { path = "../gpui" }
|
|
@ -1,7 +1,8 @@
|
||||||
|
use auto_update::{AutoUpdateStatus, AutoUpdater, DismissErrorMessage};
|
||||||
use editor::Editor;
|
use editor::Editor;
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions, elements::*, platform::CursorStyle, AppContext, Entity, EventContext, ModelHandle,
|
actions, elements::*, platform::CursorStyle, Action, AppContext, Entity, ModelHandle,
|
||||||
MutableAppContext, RenderContext, View, ViewContext, ViewHandle,
|
MutableAppContext, RenderContext, View, ViewContext, ViewHandle,
|
||||||
};
|
};
|
||||||
use language::{LanguageRegistry, LanguageServerBinaryStatus};
|
use language::{LanguageRegistry, LanguageServerBinaryStatus};
|
||||||
|
@ -14,13 +15,18 @@ use workspace::{ItemHandle, StatusItemView, Workspace};
|
||||||
|
|
||||||
actions!(lsp_status, [ShowErrorMessage]);
|
actions!(lsp_status, [ShowErrorMessage]);
|
||||||
|
|
||||||
|
const DOWNLOAD_ICON: &'static str = "icons/download-solid-14.svg";
|
||||||
|
const WARNING_ICON: &'static str = "icons/warning-solid-14.svg";
|
||||||
|
const DONE_ICON: &'static str = "icons/accept.svg";
|
||||||
|
|
||||||
pub enum Event {
|
pub enum Event {
|
||||||
ShowError { lsp_name: Arc<str>, error: String },
|
ShowError { lsp_name: Arc<str>, error: String },
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct LspStatusItem {
|
pub struct ActivityIndicator {
|
||||||
statuses: Vec<LspStatus>,
|
statuses: Vec<LspStatus>,
|
||||||
project: ModelHandle<Project>,
|
project: ModelHandle<Project>,
|
||||||
|
auto_updater: Option<ModelHandle<AutoUpdater>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct LspStatus {
|
struct LspStatus {
|
||||||
|
@ -29,15 +35,16 @@ struct LspStatus {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn init(cx: &mut MutableAppContext) {
|
pub fn init(cx: &mut MutableAppContext) {
|
||||||
cx.add_action(LspStatusItem::show_error_message);
|
cx.add_action(ActivityIndicator::show_error_message);
|
||||||
|
cx.add_action(ActivityIndicator::dismiss_error_message);
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LspStatusItem {
|
impl ActivityIndicator {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
workspace: &mut Workspace,
|
workspace: &mut Workspace,
|
||||||
languages: Arc<LanguageRegistry>,
|
languages: Arc<LanguageRegistry>,
|
||||||
cx: &mut ViewContext<Workspace>,
|
cx: &mut ViewContext<Workspace>,
|
||||||
) -> ViewHandle<LspStatusItem> {
|
) -> ViewHandle<ActivityIndicator> {
|
||||||
let project = workspace.project().clone();
|
let project = workspace.project().clone();
|
||||||
let this = cx.add_view(|cx: &mut ViewContext<Self>| {
|
let this = cx.add_view(|cx: &mut ViewContext<Self>| {
|
||||||
let mut status_events = languages.language_server_binary_statuses();
|
let mut status_events = languages.language_server_binary_statuses();
|
||||||
|
@ -63,6 +70,7 @@ impl LspStatusItem {
|
||||||
Self {
|
Self {
|
||||||
statuses: Default::default(),
|
statuses: Default::default(),
|
||||||
project: project.clone(),
|
project: project.clone(),
|
||||||
|
auto_updater: AutoUpdater::get(cx),
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
cx.subscribe(&this, move |workspace, _, event, cx| match event {
|
cx.subscribe(&this, move |workspace, _, event, cx| match event {
|
||||||
|
@ -106,6 +114,15 @@ impl LspStatusItem {
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn dismiss_error_message(&mut self, _: &DismissErrorMessage, cx: &mut ViewContext<Self>) {
|
||||||
|
if let Some(updater) = &self.auto_updater {
|
||||||
|
updater.update(cx, |updater, cx| {
|
||||||
|
updater.dismiss_error(cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
fn pending_language_server_work<'a>(
|
fn pending_language_server_work<'a>(
|
||||||
&self,
|
&self,
|
||||||
cx: &'a AppContext,
|
cx: &'a AppContext,
|
||||||
|
@ -129,25 +146,15 @@ impl LspStatusItem {
|
||||||
})
|
})
|
||||||
.flatten()
|
.flatten()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl Entity for LspStatusItem {
|
|
||||||
type Event = Event;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl View for LspStatusItem {
|
|
||||||
fn ui_name() -> &'static str {
|
|
||||||
"LspStatus"
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
|
|
||||||
let mut message;
|
|
||||||
let mut icon = None;
|
|
||||||
let mut handler = None;
|
|
||||||
|
|
||||||
|
fn content_to_render(
|
||||||
|
&mut self,
|
||||||
|
cx: &mut RenderContext<Self>,
|
||||||
|
) -> (Option<&'static str>, String, Option<Box<dyn Action>>) {
|
||||||
|
// Show any language server has pending activity.
|
||||||
let mut pending_work = self.pending_language_server_work(cx);
|
let mut pending_work = self.pending_language_server_work(cx);
|
||||||
if let Some((lang_server_name, progress_token, progress)) = pending_work.next() {
|
if let Some((lang_server_name, progress_token, progress)) = pending_work.next() {
|
||||||
message = lang_server_name.to_string();
|
let mut message = lang_server_name.to_string();
|
||||||
|
|
||||||
message.push_str(": ");
|
message.push_str(": ");
|
||||||
if let Some(progress_message) = progress.message.as_ref() {
|
if let Some(progress_message) = progress.message.as_ref() {
|
||||||
|
@ -164,38 +171,43 @@ impl View for LspStatusItem {
|
||||||
if additional_work_count > 0 {
|
if additional_work_count > 0 {
|
||||||
write!(&mut message, " + {} more", additional_work_count).unwrap();
|
write!(&mut message, " + {} more", additional_work_count).unwrap();
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
drop(pending_work);
|
|
||||||
|
|
||||||
let mut downloading = SmallVec::<[_; 3]>::new();
|
return (None, message, None);
|
||||||
let mut checking_for_update = SmallVec::<[_; 3]>::new();
|
}
|
||||||
let mut failed = SmallVec::<[_; 3]>::new();
|
|
||||||
for status in &self.statuses {
|
// Show any language server installation info.
|
||||||
match status.status {
|
let mut downloading = SmallVec::<[_; 3]>::new();
|
||||||
LanguageServerBinaryStatus::CheckingForUpdate => {
|
let mut checking_for_update = SmallVec::<[_; 3]>::new();
|
||||||
checking_for_update.push(status.name.clone());
|
let mut failed = SmallVec::<[_; 3]>::new();
|
||||||
}
|
for status in &self.statuses {
|
||||||
LanguageServerBinaryStatus::Downloading => {
|
match status.status {
|
||||||
downloading.push(status.name.clone());
|
LanguageServerBinaryStatus::CheckingForUpdate => {
|
||||||
}
|
checking_for_update.push(status.name.clone());
|
||||||
LanguageServerBinaryStatus::Failed { .. } => {
|
|
||||||
failed.push(status.name.clone());
|
|
||||||
}
|
|
||||||
LanguageServerBinaryStatus::Downloaded | LanguageServerBinaryStatus::Cached => {
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
LanguageServerBinaryStatus::Downloading => {
|
||||||
|
downloading.push(status.name.clone());
|
||||||
|
}
|
||||||
|
LanguageServerBinaryStatus::Failed { .. } => {
|
||||||
|
failed.push(status.name.clone());
|
||||||
|
}
|
||||||
|
LanguageServerBinaryStatus::Downloaded | LanguageServerBinaryStatus::Cached => {}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if !downloading.is_empty() {
|
if !downloading.is_empty() {
|
||||||
icon = Some("icons/download-solid-14.svg");
|
return (
|
||||||
message = format!(
|
Some(DOWNLOAD_ICON),
|
||||||
|
format!(
|
||||||
"Downloading {} language server{}...",
|
"Downloading {} language server{}...",
|
||||||
downloading.join(", "),
|
downloading.join(", "),
|
||||||
if downloading.len() > 1 { "s" } else { "" }
|
if downloading.len() > 1 { "s" } else { "" }
|
||||||
);
|
),
|
||||||
} else if !checking_for_update.is_empty() {
|
None,
|
||||||
icon = Some("icons/download-solid-14.svg");
|
);
|
||||||
message = format!(
|
} else if !checking_for_update.is_empty() {
|
||||||
|
return (
|
||||||
|
Some(DOWNLOAD_ICON),
|
||||||
|
format!(
|
||||||
"Checking for updates to {} language server{}...",
|
"Checking for updates to {} language server{}...",
|
||||||
checking_for_update.join(", "),
|
checking_for_update.join(", "),
|
||||||
if checking_for_update.len() > 1 {
|
if checking_for_update.len() > 1 {
|
||||||
|
@ -203,20 +215,68 @@ impl View for LspStatusItem {
|
||||||
} else {
|
} else {
|
||||||
""
|
""
|
||||||
}
|
}
|
||||||
);
|
),
|
||||||
} else if !failed.is_empty() {
|
None,
|
||||||
icon = Some("icons/warning-solid-14.svg");
|
);
|
||||||
message = format!(
|
} else if !failed.is_empty() {
|
||||||
|
return (
|
||||||
|
Some(WARNING_ICON),
|
||||||
|
format!(
|
||||||
"Failed to download {} language server{}. Click to show error.",
|
"Failed to download {} language server{}. Click to show error.",
|
||||||
failed.join(", "),
|
failed.join(", "),
|
||||||
if failed.len() > 1 { "s" } else { "" }
|
if failed.len() > 1 { "s" } else { "" }
|
||||||
);
|
),
|
||||||
handler = Some(|_, _, cx: &mut EventContext| cx.dispatch_action(ShowErrorMessage));
|
Some(Box::new(ShowErrorMessage)),
|
||||||
} else {
|
);
|
||||||
return Empty::new().boxed();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Show any application auto-update info.
|
||||||
|
if let Some(updater) = &self.auto_updater {
|
||||||
|
// let theme = &cx.global::<Settings>().theme.workspace.status_bar;
|
||||||
|
match &updater.read(cx).status() {
|
||||||
|
AutoUpdateStatus::Checking => (
|
||||||
|
Some(DOWNLOAD_ICON),
|
||||||
|
"Checking for Zed updates…".to_string(),
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
AutoUpdateStatus::Downloading => (
|
||||||
|
Some(DOWNLOAD_ICON),
|
||||||
|
"Downloading Zed update…".to_string(),
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
AutoUpdateStatus::Installing => (
|
||||||
|
Some(DOWNLOAD_ICON),
|
||||||
|
"Installing Zed update…".to_string(),
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
AutoUpdateStatus::Updated => {
|
||||||
|
(Some(DONE_ICON), "Restart to update Zed".to_string(), None)
|
||||||
|
}
|
||||||
|
AutoUpdateStatus::Errored => (
|
||||||
|
Some(WARNING_ICON),
|
||||||
|
"Auto update failed".to_string(),
|
||||||
|
Some(Box::new(DismissErrorMessage)),
|
||||||
|
),
|
||||||
|
AutoUpdateStatus::Idle => Default::default(),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Entity for ActivityIndicator {
|
||||||
|
type Event = Event;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl View for ActivityIndicator {
|
||||||
|
fn ui_name() -> &'static str {
|
||||||
|
"ActivityIndicator"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||||
|
let (icon, message, action) = self.content_to_render(cx);
|
||||||
|
|
||||||
let mut element = MouseEventHandler::new::<Self, _, _>(0, cx, |state, cx| {
|
let mut element = MouseEventHandler::new::<Self, _, _>(0, cx, |state, cx| {
|
||||||
let theme = &cx
|
let theme = &cx
|
||||||
.global::<Settings>()
|
.global::<Settings>()
|
||||||
|
@ -224,7 +284,7 @@ impl View for LspStatusItem {
|
||||||
.workspace
|
.workspace
|
||||||
.status_bar
|
.status_bar
|
||||||
.lsp_status;
|
.lsp_status;
|
||||||
let style = if state.hovered && handler.is_some() {
|
let style = if state.hovered && action.is_some() {
|
||||||
theme.hover.as_ref().unwrap_or(&theme.default)
|
theme.hover.as_ref().unwrap_or(&theme.default)
|
||||||
} else {
|
} else {
|
||||||
&theme.default
|
&theme.default
|
||||||
|
@ -238,9 +298,14 @@ impl View for LspStatusItem {
|
||||||
.contained()
|
.contained()
|
||||||
.with_margin_right(style.icon_spacing)
|
.with_margin_right(style.icon_spacing)
|
||||||
.aligned()
|
.aligned()
|
||||||
.named("warning-icon")
|
.named("activity-icon")
|
||||||
}))
|
}))
|
||||||
.with_child(Label::new(message, style.message.clone()).aligned().boxed())
|
.with_child(
|
||||||
|
Text::new(message, style.message.clone())
|
||||||
|
.with_soft_wrap(false)
|
||||||
|
.aligned()
|
||||||
|
.boxed(),
|
||||||
|
)
|
||||||
.constrained()
|
.constrained()
|
||||||
.with_height(style.height)
|
.with_height(style.height)
|
||||||
.contained()
|
.contained()
|
||||||
|
@ -249,16 +314,16 @@ impl View for LspStatusItem {
|
||||||
.boxed()
|
.boxed()
|
||||||
});
|
});
|
||||||
|
|
||||||
if let Some(handler) = handler {
|
if let Some(action) = action {
|
||||||
element = element
|
element = element
|
||||||
.with_cursor_style(CursorStyle::PointingHand)
|
.with_cursor_style(CursorStyle::PointingHand)
|
||||||
.on_click(handler);
|
.on_click(move |_, _, cx| cx.dispatch_any_action(action.boxed_clone()));
|
||||||
}
|
}
|
||||||
|
|
||||||
element.boxed()
|
element.boxed()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StatusItemView for LspStatusItem {
|
impl StatusItemView for ActivityIndicator {
|
||||||
fn set_active_pane_item(&mut self, _: Option<&dyn ItemHandle>, _: &mut ViewContext<Self>) {}
|
fn set_active_pane_item(&mut self, _: Option<&dyn ItemHandle>, _: &mut ViewContext<Self>) {}
|
||||||
}
|
}
|
|
@ -3,19 +3,15 @@ mod update_notification;
|
||||||
use anyhow::{anyhow, Context, Result};
|
use anyhow::{anyhow, Context, Result};
|
||||||
use client::{http::HttpClient, ZED_SECRET_CLIENT_TOKEN};
|
use client::{http::HttpClient, ZED_SECRET_CLIENT_TOKEN};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions,
|
actions, platform::AppVersion, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle,
|
||||||
elements::{Empty, MouseEventHandler, Text},
|
MutableAppContext, Task, WeakViewHandle,
|
||||||
platform::AppVersion,
|
|
||||||
AppContext, AsyncAppContext, Element, Entity, ModelContext, ModelHandle, MutableAppContext,
|
|
||||||
Task, View, ViewContext, WeakViewHandle,
|
|
||||||
};
|
};
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use settings::Settings;
|
|
||||||
use smol::{fs::File, io::AsyncReadExt, process::Command};
|
use smol::{fs::File, io::AsyncReadExt, process::Command};
|
||||||
use std::{env, ffi::OsString, path::PathBuf, sync::Arc, time::Duration};
|
use std::{env, ffi::OsString, path::PathBuf, sync::Arc, time::Duration};
|
||||||
use update_notification::UpdateNotification;
|
use update_notification::UpdateNotification;
|
||||||
use workspace::{ItemHandle, StatusItemView, Workspace};
|
use workspace::Workspace;
|
||||||
|
|
||||||
const SHOULD_SHOW_UPDATE_NOTIFICATION_KEY: &'static str =
|
const SHOULD_SHOW_UPDATE_NOTIFICATION_KEY: &'static str =
|
||||||
"auto-updater-should-show-updated-notification";
|
"auto-updater-should-show-updated-notification";
|
||||||
|
@ -30,7 +26,7 @@ lazy_static! {
|
||||||
|
|
||||||
actions!(auto_update, [Check, DismissErrorMessage, ViewReleaseNotes]);
|
actions!(auto_update, [Check, DismissErrorMessage, ViewReleaseNotes]);
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Eq)]
|
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum AutoUpdateStatus {
|
pub enum AutoUpdateStatus {
|
||||||
Idle,
|
Idle,
|
||||||
Checking,
|
Checking,
|
||||||
|
@ -49,10 +45,6 @@ pub struct AutoUpdater {
|
||||||
server_url: String,
|
server_url: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct AutoUpdateIndicator {
|
|
||||||
updater: Option<ModelHandle<AutoUpdater>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
struct JsonRelease {
|
struct JsonRelease {
|
||||||
version: String,
|
version: String,
|
||||||
|
@ -84,7 +76,6 @@ pub fn init(
|
||||||
cx.add_global_action(move |_: &ViewReleaseNotes, cx| {
|
cx.add_global_action(move |_: &ViewReleaseNotes, cx| {
|
||||||
cx.platform().open_url(&format!("{server_url}/releases"));
|
cx.platform().open_url(&format!("{server_url}/releases"));
|
||||||
});
|
});
|
||||||
cx.add_action(AutoUpdateIndicator::dismiss_error_message);
|
|
||||||
cx.add_action(UpdateNotification::dismiss);
|
cx.add_action(UpdateNotification::dismiss);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -120,7 +111,7 @@ pub fn notify_of_any_new_update(
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AutoUpdater {
|
impl AutoUpdater {
|
||||||
fn get(cx: &mut MutableAppContext) -> Option<ModelHandle<Self>> {
|
pub fn get(cx: &mut MutableAppContext) -> Option<ModelHandle<Self>> {
|
||||||
cx.default_global::<Option<ModelHandle<Self>>>().clone()
|
cx.default_global::<Option<ModelHandle<Self>>>().clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -170,6 +161,15 @@ impl AutoUpdater {
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn status(&self) -> AutoUpdateStatus {
|
||||||
|
self.status
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dismiss_error(&mut self, cx: &mut ModelContext<Self>) {
|
||||||
|
self.status = AutoUpdateStatus::Idle;
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
async fn update(this: ModelHandle<Self>, mut cx: AsyncAppContext) -> Result<()> {
|
async fn update(this: ModelHandle<Self>, mut cx: AsyncAppContext) -> Result<()> {
|
||||||
let (client, server_url, current_version) = this.read_with(&cx, |this, _| {
|
let (client, server_url, current_version) = this.read_with(&cx, |this, _| {
|
||||||
(
|
(
|
||||||
|
@ -299,79 +299,3 @@ impl AutoUpdater {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Entity for AutoUpdateIndicator {
|
|
||||||
type Event = ();
|
|
||||||
}
|
|
||||||
|
|
||||||
impl View for AutoUpdateIndicator {
|
|
||||||
fn ui_name() -> &'static str {
|
|
||||||
"AutoUpdateIndicator"
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> gpui::ElementBox {
|
|
||||||
if let Some(updater) = &self.updater {
|
|
||||||
let theme = &cx.global::<Settings>().theme.workspace.status_bar;
|
|
||||||
match &updater.read(cx).status {
|
|
||||||
AutoUpdateStatus::Checking => Text::new(
|
|
||||||
"Checking for updates…".to_string(),
|
|
||||||
theme.auto_update_progress_message.clone(),
|
|
||||||
)
|
|
||||||
.boxed(),
|
|
||||||
AutoUpdateStatus::Downloading => Text::new(
|
|
||||||
"Downloading update…".to_string(),
|
|
||||||
theme.auto_update_progress_message.clone(),
|
|
||||||
)
|
|
||||||
.boxed(),
|
|
||||||
AutoUpdateStatus::Installing => Text::new(
|
|
||||||
"Installing update…".to_string(),
|
|
||||||
theme.auto_update_progress_message.clone(),
|
|
||||||
)
|
|
||||||
.boxed(),
|
|
||||||
AutoUpdateStatus::Updated => Text::new(
|
|
||||||
"Restart to update Zed".to_string(),
|
|
||||||
theme.auto_update_done_message.clone(),
|
|
||||||
)
|
|
||||||
.boxed(),
|
|
||||||
AutoUpdateStatus::Errored => {
|
|
||||||
MouseEventHandler::new::<Self, _, _>(0, cx, |_, cx| {
|
|
||||||
let theme = &cx.global::<Settings>().theme.workspace.status_bar;
|
|
||||||
Text::new(
|
|
||||||
"Auto update failed".to_string(),
|
|
||||||
theme.auto_update_done_message.clone(),
|
|
||||||
)
|
|
||||||
.boxed()
|
|
||||||
})
|
|
||||||
.on_click(|_, _, cx| cx.dispatch_action(DismissErrorMessage))
|
|
||||||
.boxed()
|
|
||||||
}
|
|
||||||
AutoUpdateStatus::Idle => Empty::new().boxed(),
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Empty::new().boxed()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl StatusItemView for AutoUpdateIndicator {
|
|
||||||
fn set_active_pane_item(&mut self, _: Option<&dyn ItemHandle>, _: &mut ViewContext<Self>) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AutoUpdateIndicator {
|
|
||||||
pub fn new(cx: &mut ViewContext<Self>) -> Self {
|
|
||||||
let updater = AutoUpdater::get(cx);
|
|
||||||
if let Some(updater) = &updater {
|
|
||||||
cx.observe(updater, |_, _, cx| cx.notify()).detach();
|
|
||||||
}
|
|
||||||
Self { updater }
|
|
||||||
}
|
|
||||||
|
|
||||||
fn dismiss_error_message(&mut self, _: &DismissErrorMessage, cx: &mut ViewContext<Self>) {
|
|
||||||
if let Some(updater) = &self.updater {
|
|
||||||
updater.update(cx, |updater, cx| {
|
|
||||||
updater.status = AutoUpdateStatus::Idle;
|
|
||||||
cx.notify();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -28,10 +28,7 @@ use std::{
|
||||||
convert::TryFrom,
|
convert::TryFrom,
|
||||||
fmt::Write as _,
|
fmt::Write as _,
|
||||||
future::Future,
|
future::Future,
|
||||||
sync::{
|
sync::{Arc, Weak},
|
||||||
atomic::{AtomicUsize, Ordering},
|
|
||||||
Arc, Weak,
|
|
||||||
},
|
|
||||||
time::{Duration, Instant},
|
time::{Duration, Instant},
|
||||||
};
|
};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
@ -232,12 +229,8 @@ impl Drop for Subscription {
|
||||||
|
|
||||||
impl Client {
|
impl Client {
|
||||||
pub fn new(http: Arc<dyn HttpClient>) -> Arc<Self> {
|
pub fn new(http: Arc<dyn HttpClient>) -> Arc<Self> {
|
||||||
lazy_static! {
|
|
||||||
static ref NEXT_CLIENT_ID: AtomicUsize = AtomicUsize::default();
|
|
||||||
}
|
|
||||||
|
|
||||||
Arc::new(Self {
|
Arc::new(Self {
|
||||||
id: NEXT_CLIENT_ID.fetch_add(1, Ordering::SeqCst),
|
id: 0,
|
||||||
peer: Peer::new(),
|
peer: Peer::new(),
|
||||||
http,
|
http,
|
||||||
state: Default::default(),
|
state: Default::default(),
|
||||||
|
@ -257,6 +250,12 @@ impl Client {
|
||||||
self.http.clone()
|
self.http.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
pub fn set_id(&mut self, id: usize) -> &Self {
|
||||||
|
self.id = id;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
pub fn tear_down(&self) {
|
pub fn tear_down(&self) {
|
||||||
let mut state = self.state.write();
|
let mut state = self.state.write();
|
||||||
|
|
|
@ -2282,7 +2282,7 @@ pub mod tests {
|
||||||
Self {
|
Self {
|
||||||
background,
|
background,
|
||||||
users: Default::default(),
|
users: Default::default(),
|
||||||
next_user_id: Mutex::new(1),
|
next_user_id: Mutex::new(0),
|
||||||
projects: Default::default(),
|
projects: Default::default(),
|
||||||
worktree_extensions: Default::default(),
|
worktree_extensions: Default::default(),
|
||||||
next_project_id: Mutex::new(1),
|
next_project_id: Mutex::new(1),
|
||||||
|
@ -2346,6 +2346,7 @@ pub mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_user_by_id(&self, id: UserId) -> Result<Option<User>> {
|
async fn get_user_by_id(&self, id: UserId) -> Result<Option<User>> {
|
||||||
|
self.background.simulate_random_delay().await;
|
||||||
Ok(self.get_users_by_ids(vec![id]).await?.into_iter().next())
|
Ok(self.get_users_by_ids(vec![id]).await?.into_iter().next())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2360,6 +2361,7 @@ pub mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_user_by_github_login(&self, github_login: &str) -> Result<Option<User>> {
|
async fn get_user_by_github_login(&self, github_login: &str) -> Result<Option<User>> {
|
||||||
|
self.background.simulate_random_delay().await;
|
||||||
Ok(self
|
Ok(self
|
||||||
.users
|
.users
|
||||||
.lock()
|
.lock()
|
||||||
|
@ -2393,6 +2395,7 @@ pub mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_invite_code_for_user(&self, _id: UserId) -> Result<Option<(String, u32)>> {
|
async fn get_invite_code_for_user(&self, _id: UserId) -> Result<Option<(String, u32)>> {
|
||||||
|
self.background.simulate_random_delay().await;
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2430,6 +2433,7 @@ pub mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn unregister_project(&self, project_id: ProjectId) -> Result<()> {
|
async fn unregister_project(&self, project_id: ProjectId) -> Result<()> {
|
||||||
|
self.background.simulate_random_delay().await;
|
||||||
self.projects
|
self.projects
|
||||||
.lock()
|
.lock()
|
||||||
.get_mut(&project_id)
|
.get_mut(&project_id)
|
||||||
|
@ -2543,6 +2547,7 @@ pub mod tests {
|
||||||
requester_id: UserId,
|
requester_id: UserId,
|
||||||
responder_id: UserId,
|
responder_id: UserId,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
|
self.background.simulate_random_delay().await;
|
||||||
let mut contacts = self.contacts.lock();
|
let mut contacts = self.contacts.lock();
|
||||||
for contact in contacts.iter_mut() {
|
for contact in contacts.iter_mut() {
|
||||||
if contact.requester_id == requester_id && contact.responder_id == responder_id {
|
if contact.requester_id == requester_id && contact.responder_id == responder_id {
|
||||||
|
@ -2572,6 +2577,7 @@ pub mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn remove_contact(&self, requester_id: UserId, responder_id: UserId) -> Result<()> {
|
async fn remove_contact(&self, requester_id: UserId, responder_id: UserId) -> Result<()> {
|
||||||
|
self.background.simulate_random_delay().await;
|
||||||
self.contacts.lock().retain(|contact| {
|
self.contacts.lock().retain(|contact| {
|
||||||
!(contact.requester_id == requester_id && contact.responder_id == responder_id)
|
!(contact.requester_id == requester_id && contact.responder_id == responder_id)
|
||||||
});
|
});
|
||||||
|
@ -2583,6 +2589,7 @@ pub mod tests {
|
||||||
user_id: UserId,
|
user_id: UserId,
|
||||||
contact_user_id: UserId,
|
contact_user_id: UserId,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
|
self.background.simulate_random_delay().await;
|
||||||
let mut contacts = self.contacts.lock();
|
let mut contacts = self.contacts.lock();
|
||||||
for contact in contacts.iter_mut() {
|
for contact in contacts.iter_mut() {
|
||||||
if contact.requester_id == contact_user_id
|
if contact.requester_id == contact_user_id
|
||||||
|
@ -2609,6 +2616,7 @@ pub mod tests {
|
||||||
requester_id: UserId,
|
requester_id: UserId,
|
||||||
accept: bool,
|
accept: bool,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
|
self.background.simulate_random_delay().await;
|
||||||
let mut contacts = self.contacts.lock();
|
let mut contacts = self.contacts.lock();
|
||||||
for (ix, contact) in contacts.iter_mut().enumerate() {
|
for (ix, contact) in contacts.iter_mut().enumerate() {
|
||||||
if contact.requester_id == requester_id && contact.responder_id == responder_id {
|
if contact.requester_id == requester_id && contact.responder_id == responder_id {
|
||||||
|
@ -2804,6 +2812,7 @@ pub mod tests {
|
||||||
count: usize,
|
count: usize,
|
||||||
before_id: Option<MessageId>,
|
before_id: Option<MessageId>,
|
||||||
) -> Result<Vec<ChannelMessage>> {
|
) -> Result<Vec<ChannelMessage>> {
|
||||||
|
self.background.simulate_random_delay().await;
|
||||||
let mut messages = self
|
let mut messages = self
|
||||||
.channel_messages
|
.channel_messages
|
||||||
.lock()
|
.lock()
|
||||||
|
|
|
@ -50,7 +50,6 @@ use std::{
|
||||||
time::Duration,
|
time::Duration,
|
||||||
};
|
};
|
||||||
use theme::ThemeRegistry;
|
use theme::ThemeRegistry;
|
||||||
use tokio::sync::RwLockReadGuard;
|
|
||||||
use workspace::{Item, SplitDirection, ToggleFollow, Workspace};
|
use workspace::{Item, SplitDirection, ToggleFollow, Workspace};
|
||||||
|
|
||||||
#[ctor::ctor]
|
#[ctor::ctor]
|
||||||
|
@ -596,7 +595,7 @@ async fn test_offline_projects(
|
||||||
deterministic.run_until_parked();
|
deterministic.run_until_parked();
|
||||||
assert!(server
|
assert!(server
|
||||||
.store
|
.store
|
||||||
.read()
|
.lock()
|
||||||
.await
|
.await
|
||||||
.project_metadata_for_user(user_a)
|
.project_metadata_for_user(user_a)
|
||||||
.is_empty());
|
.is_empty());
|
||||||
|
@ -630,7 +629,7 @@ async fn test_offline_projects(
|
||||||
cx_a.foreground().advance_clock(rpc::RECEIVE_TIMEOUT);
|
cx_a.foreground().advance_clock(rpc::RECEIVE_TIMEOUT);
|
||||||
assert!(server
|
assert!(server
|
||||||
.store
|
.store
|
||||||
.read()
|
.lock()
|
||||||
.await
|
.await
|
||||||
.project_metadata_for_user(user_a)
|
.project_metadata_for_user(user_a)
|
||||||
.is_empty());
|
.is_empty());
|
||||||
|
@ -1491,7 +1490,7 @@ async fn test_collaborating_with_diagnostics(
|
||||||
// Wait for server to see the diagnostics update.
|
// Wait for server to see the diagnostics update.
|
||||||
deterministic.run_until_parked();
|
deterministic.run_until_parked();
|
||||||
{
|
{
|
||||||
let store = server.store.read().await;
|
let store = server.store.lock().await;
|
||||||
let project = store.project(ProjectId::from_proto(project_id)).unwrap();
|
let project = store.project(ProjectId::from_proto(project_id)).unwrap();
|
||||||
let worktree = project.worktrees.get(&worktree_id.to_proto()).unwrap();
|
let worktree = project.worktrees.get(&worktree_id.to_proto()).unwrap();
|
||||||
assert!(!worktree.diagnostic_summaries.is_empty());
|
assert!(!worktree.diagnostic_summaries.is_empty());
|
||||||
|
@ -1517,6 +1516,7 @@ async fn test_collaborating_with_diagnostics(
|
||||||
|
|
||||||
// Join project as client C and observe the diagnostics.
|
// Join project as client C and observe the diagnostics.
|
||||||
let project_c = client_c.build_remote_project(&project_a, cx_a, cx_c).await;
|
let project_c = client_c.build_remote_project(&project_a, cx_a, cx_c).await;
|
||||||
|
deterministic.run_until_parked();
|
||||||
project_c.read_with(cx_c, |project, cx| {
|
project_c.read_with(cx_c, |project, cx| {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
project.diagnostic_summaries(cx).collect::<Vec<_>>(),
|
project.diagnostic_summaries(cx).collect::<Vec<_>>(),
|
||||||
|
@ -3216,7 +3216,7 @@ async fn test_basic_chat(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
server
|
server
|
||||||
.state()
|
.store()
|
||||||
.await
|
.await
|
||||||
.channel(channel_id)
|
.channel(channel_id)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
|
@ -4470,8 +4470,16 @@ async fn test_random_collaboration(
|
||||||
let mut server = TestServer::start(cx.foreground(), cx.background()).await;
|
let mut server = TestServer::start(cx.foreground(), cx.background()).await;
|
||||||
let db = server.app_state.db.clone();
|
let db = server.app_state.db.clone();
|
||||||
let host_user_id = db.create_user("host", None, false).await.unwrap();
|
let host_user_id = db.create_user("host", None, false).await.unwrap();
|
||||||
for username in ["guest-1", "guest-2", "guest-3", "guest-4"] {
|
let mut available_guests = vec![
|
||||||
|
"guest-1".to_string(),
|
||||||
|
"guest-2".to_string(),
|
||||||
|
"guest-3".to_string(),
|
||||||
|
"guest-4".to_string(),
|
||||||
|
];
|
||||||
|
|
||||||
|
for username in &available_guests {
|
||||||
let guest_user_id = db.create_user(username, None, false).await.unwrap();
|
let guest_user_id = db.create_user(username, None, false).await.unwrap();
|
||||||
|
assert_eq!(*username, format!("guest-{}", guest_user_id));
|
||||||
server
|
server
|
||||||
.app_state
|
.app_state
|
||||||
.db
|
.db
|
||||||
|
@ -4665,12 +4673,7 @@ async fn test_random_collaboration(
|
||||||
} else {
|
} else {
|
||||||
max_operations
|
max_operations
|
||||||
};
|
};
|
||||||
let mut available_guests = vec![
|
|
||||||
"guest-1".to_string(),
|
|
||||||
"guest-2".to_string(),
|
|
||||||
"guest-3".to_string(),
|
|
||||||
"guest-4".to_string(),
|
|
||||||
];
|
|
||||||
let mut operations = 0;
|
let mut operations = 0;
|
||||||
while operations < max_operations {
|
while operations < max_operations {
|
||||||
if operations == disconnect_host_at {
|
if operations == disconnect_host_at {
|
||||||
|
@ -4701,7 +4704,7 @@ async fn test_random_collaboration(
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let contacts = server
|
let contacts = server
|
||||||
.store
|
.store
|
||||||
.read()
|
.lock()
|
||||||
.await
|
.await
|
||||||
.build_initial_contacts_update(contacts)
|
.build_initial_contacts_update(contacts)
|
||||||
.contacts;
|
.contacts;
|
||||||
|
@ -4773,6 +4776,7 @@ async fn test_random_collaboration(
|
||||||
server.disconnect_client(removed_guest_id);
|
server.disconnect_client(removed_guest_id);
|
||||||
deterministic.advance_clock(RECEIVE_TIMEOUT);
|
deterministic.advance_clock(RECEIVE_TIMEOUT);
|
||||||
deterministic.start_waiting();
|
deterministic.start_waiting();
|
||||||
|
log::info!("Waiting for guest {} to exit...", removed_guest_id);
|
||||||
let (guest, guest_project, mut guest_cx, guest_err) = guest.await;
|
let (guest, guest_project, mut guest_cx, guest_err) = guest.await;
|
||||||
deterministic.finish_waiting();
|
deterministic.finish_waiting();
|
||||||
server.allow_connections();
|
server.allow_connections();
|
||||||
|
@ -4785,7 +4789,7 @@ async fn test_random_collaboration(
|
||||||
let contacts = server.app_state.db.get_contacts(*user_id).await.unwrap();
|
let contacts = server.app_state.db.get_contacts(*user_id).await.unwrap();
|
||||||
let contacts = server
|
let contacts = server
|
||||||
.store
|
.store
|
||||||
.read()
|
.lock()
|
||||||
.await
|
.await
|
||||||
.build_initial_contacts_update(contacts)
|
.build_initial_contacts_update(contacts)
|
||||||
.contacts;
|
.contacts;
|
||||||
|
@ -4989,6 +4993,7 @@ impl TestServer {
|
||||||
|
|
||||||
Arc::get_mut(&mut client)
|
Arc::get_mut(&mut client)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
|
.set_id(user_id.0 as usize)
|
||||||
.override_authenticate(move |cx| {
|
.override_authenticate(move |cx| {
|
||||||
cx.spawn(|_| async move {
|
cx.spawn(|_| async move {
|
||||||
let access_token = "the-token".to_string();
|
let access_token = "the-token".to_string();
|
||||||
|
@ -5116,10 +5121,6 @@ impl TestServer {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn state<'a>(&'a self) -> RwLockReadGuard<'a, Store> {
|
|
||||||
self.server.store.read().await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn condition<F>(&mut self, mut predicate: F)
|
async fn condition<F>(&mut self, mut predicate: F)
|
||||||
where
|
where
|
||||||
F: FnMut(&Store) -> bool,
|
F: FnMut(&Store) -> bool,
|
||||||
|
@ -5128,7 +5129,7 @@ impl TestServer {
|
||||||
self.foreground.parking_forbidden(),
|
self.foreground.parking_forbidden(),
|
||||||
"you must call forbid_parking to use server conditions so we don't block indefinitely"
|
"you must call forbid_parking to use server conditions so we don't block indefinitely"
|
||||||
);
|
);
|
||||||
while !(predicate)(&*self.server.store.read().await) {
|
while !(predicate)(&*self.server.store.lock().await) {
|
||||||
self.foreground.start_waiting();
|
self.foreground.start_waiting();
|
||||||
self.notifications.next().await;
|
self.notifications.next().await;
|
||||||
self.foreground.finish_waiting();
|
self.foreground.finish_waiting();
|
||||||
|
|
|
@ -51,7 +51,7 @@ use std::{
|
||||||
};
|
};
|
||||||
use time::OffsetDateTime;
|
use time::OffsetDateTime;
|
||||||
use tokio::{
|
use tokio::{
|
||||||
sync::{RwLock, RwLockReadGuard, RwLockWriteGuard},
|
sync::{Mutex, MutexGuard},
|
||||||
time::Sleep,
|
time::Sleep,
|
||||||
};
|
};
|
||||||
use tower::ServiceBuilder;
|
use tower::ServiceBuilder;
|
||||||
|
@ -97,7 +97,7 @@ impl<R: RequestMessage> Response<R> {
|
||||||
|
|
||||||
pub struct Server {
|
pub struct Server {
|
||||||
peer: Arc<Peer>,
|
peer: Arc<Peer>,
|
||||||
pub(crate) store: RwLock<Store>,
|
pub(crate) store: Mutex<Store>,
|
||||||
app_state: Arc<AppState>,
|
app_state: Arc<AppState>,
|
||||||
handlers: HashMap<TypeId, MessageHandler>,
|
handlers: HashMap<TypeId, MessageHandler>,
|
||||||
notifications: Option<mpsc::UnboundedSender<()>>,
|
notifications: Option<mpsc::UnboundedSender<()>>,
|
||||||
|
@ -115,13 +115,8 @@ pub struct RealExecutor;
|
||||||
const MESSAGE_COUNT_PER_PAGE: usize = 100;
|
const MESSAGE_COUNT_PER_PAGE: usize = 100;
|
||||||
const MAX_MESSAGE_LEN: usize = 1024;
|
const MAX_MESSAGE_LEN: usize = 1024;
|
||||||
|
|
||||||
struct StoreReadGuard<'a> {
|
pub(crate) struct StoreGuard<'a> {
|
||||||
guard: RwLockReadGuard<'a, Store>,
|
guard: MutexGuard<'a, Store>,
|
||||||
_not_send: PhantomData<Rc<()>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
struct StoreWriteGuard<'a> {
|
|
||||||
guard: RwLockWriteGuard<'a, Store>,
|
|
||||||
_not_send: PhantomData<Rc<()>>,
|
_not_send: PhantomData<Rc<()>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -129,7 +124,7 @@ struct StoreWriteGuard<'a> {
|
||||||
pub struct ServerSnapshot<'a> {
|
pub struct ServerSnapshot<'a> {
|
||||||
peer: &'a Peer,
|
peer: &'a Peer,
|
||||||
#[serde(serialize_with = "serialize_deref")]
|
#[serde(serialize_with = "serialize_deref")]
|
||||||
store: RwLockReadGuard<'a, Store>,
|
store: StoreGuard<'a>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn serialize_deref<S, T, U>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
|
pub fn serialize_deref<S, T, U>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
@ -385,7 +380,7 @@ impl Server {
|
||||||
).await?;
|
).await?;
|
||||||
|
|
||||||
{
|
{
|
||||||
let mut store = this.store_mut().await;
|
let mut store = this.store().await;
|
||||||
store.add_connection(connection_id, user_id, user.admin);
|
store.add_connection(connection_id, user_id, user.admin);
|
||||||
this.peer.send(connection_id, store.build_initial_contacts_update(contacts))?;
|
this.peer.send(connection_id, store.build_initial_contacts_update(contacts))?;
|
||||||
|
|
||||||
|
@ -472,7 +467,7 @@ impl Server {
|
||||||
let mut projects_to_unregister = Vec::new();
|
let mut projects_to_unregister = Vec::new();
|
||||||
let removed_user_id;
|
let removed_user_id;
|
||||||
{
|
{
|
||||||
let mut store = self.store_mut().await;
|
let mut store = self.store().await;
|
||||||
let removed_connection = store.remove_connection(connection_id)?;
|
let removed_connection = store.remove_connection(connection_id)?;
|
||||||
|
|
||||||
for (project_id, project) in removed_connection.hosted_projects {
|
for (project_id, project) in removed_connection.hosted_projects {
|
||||||
|
@ -606,7 +601,7 @@ impl Server {
|
||||||
.await
|
.await
|
||||||
.user_id_for_connection(request.sender_id)?;
|
.user_id_for_connection(request.sender_id)?;
|
||||||
let project_id = self.app_state.db.register_project(user_id).await?;
|
let project_id = self.app_state.db.register_project(user_id).await?;
|
||||||
self.store_mut().await.register_project(
|
self.store().await.register_project(
|
||||||
request.sender_id,
|
request.sender_id,
|
||||||
project_id,
|
project_id,
|
||||||
request.payload.online,
|
request.payload.online,
|
||||||
|
@ -626,7 +621,7 @@ impl Server {
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let project_id = ProjectId::from_proto(request.payload.project_id);
|
let project_id = ProjectId::from_proto(request.payload.project_id);
|
||||||
let (user_id, project) = {
|
let (user_id, project) = {
|
||||||
let mut state = self.store_mut().await;
|
let mut state = self.store().await;
|
||||||
let project = state.unregister_project(project_id, request.sender_id)?;
|
let project = state.unregister_project(project_id, request.sender_id)?;
|
||||||
(state.user_id_for_connection(request.sender_id)?, project)
|
(state.user_id_for_connection(request.sender_id)?, project)
|
||||||
};
|
};
|
||||||
|
@ -728,7 +723,7 @@ impl Server {
|
||||||
return Err(anyhow!("no such project"))?;
|
return Err(anyhow!("no such project"))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.store_mut().await.request_join_project(
|
self.store().await.request_join_project(
|
||||||
guest_user_id,
|
guest_user_id,
|
||||||
project_id,
|
project_id,
|
||||||
response.into_receipt(),
|
response.into_receipt(),
|
||||||
|
@ -750,7 +745,7 @@ impl Server {
|
||||||
let host_user_id;
|
let host_user_id;
|
||||||
|
|
||||||
{
|
{
|
||||||
let mut state = self.store_mut().await;
|
let mut state = self.store().await;
|
||||||
let project_id = ProjectId::from_proto(request.payload.project_id);
|
let project_id = ProjectId::from_proto(request.payload.project_id);
|
||||||
let project = state.project(project_id)?;
|
let project = state.project(project_id)?;
|
||||||
if project.host_connection_id != request.sender_id {
|
if project.host_connection_id != request.sender_id {
|
||||||
|
@ -794,20 +789,10 @@ impl Server {
|
||||||
let worktrees = project
|
let worktrees = project
|
||||||
.worktrees
|
.worktrees
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|(id, shared_worktree)| {
|
.map(|(id, worktree)| proto::WorktreeMetadata {
|
||||||
let worktree = project.worktrees.get(&id)?;
|
id: *id,
|
||||||
Some(proto::Worktree {
|
root_name: worktree.root_name.clone(),
|
||||||
id: *id,
|
visible: worktree.visible,
|
||||||
root_name: worktree.root_name.clone(),
|
|
||||||
entries: shared_worktree.entries.values().cloned().collect(),
|
|
||||||
diagnostic_summaries: shared_worktree
|
|
||||||
.diagnostic_summaries
|
|
||||||
.values()
|
|
||||||
.cloned()
|
|
||||||
.collect(),
|
|
||||||
visible: worktree.visible,
|
|
||||||
scan_id: shared_worktree.scan_id,
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
@ -843,14 +828,15 @@ impl Server {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (receipt, replica_id) in receipts_with_replica_ids {
|
// First, we send the metadata associated with each worktree.
|
||||||
|
for (receipt, replica_id) in &receipts_with_replica_ids {
|
||||||
self.peer.respond(
|
self.peer.respond(
|
||||||
receipt,
|
receipt.clone(),
|
||||||
proto::JoinProjectResponse {
|
proto::JoinProjectResponse {
|
||||||
variant: Some(proto::join_project_response::Variant::Accept(
|
variant: Some(proto::join_project_response::Variant::Accept(
|
||||||
proto::join_project_response::Accept {
|
proto::join_project_response::Accept {
|
||||||
worktrees: worktrees.clone(),
|
worktrees: worktrees.clone(),
|
||||||
replica_id: replica_id as u32,
|
replica_id: *replica_id as u32,
|
||||||
collaborators: collaborators.clone(),
|
collaborators: collaborators.clone(),
|
||||||
language_servers: project.language_servers.clone(),
|
language_servers: project.language_servers.clone(),
|
||||||
},
|
},
|
||||||
|
@ -858,6 +844,43 @@ impl Server {
|
||||||
},
|
},
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (worktree_id, worktree) in &project.worktrees {
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
const MAX_CHUNK_SIZE: usize = 2;
|
||||||
|
#[cfg(not(any(test, feature = "test-support")))]
|
||||||
|
const MAX_CHUNK_SIZE: usize = 256;
|
||||||
|
|
||||||
|
// Stream this worktree's entries.
|
||||||
|
let message = proto::UpdateWorktree {
|
||||||
|
project_id: project_id.to_proto(),
|
||||||
|
worktree_id: *worktree_id,
|
||||||
|
root_name: worktree.root_name.clone(),
|
||||||
|
updated_entries: worktree.entries.values().cloned().collect(),
|
||||||
|
removed_entries: Default::default(),
|
||||||
|
scan_id: worktree.scan_id,
|
||||||
|
is_last_update: worktree.is_complete,
|
||||||
|
};
|
||||||
|
for update in proto::split_worktree_update(message, MAX_CHUNK_SIZE) {
|
||||||
|
for (receipt, _) in &receipts_with_replica_ids {
|
||||||
|
self.peer.send(receipt.sender_id, update.clone())?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stream this worktree's diagnostics.
|
||||||
|
for summary in worktree.diagnostic_summaries.values() {
|
||||||
|
for (receipt, _) in &receipts_with_replica_ids {
|
||||||
|
self.peer.send(
|
||||||
|
receipt.sender_id,
|
||||||
|
proto::UpdateDiagnosticSummary {
|
||||||
|
project_id: project_id.to_proto(),
|
||||||
|
worktree_id: *worktree_id,
|
||||||
|
summary: Some(summary.clone()),
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.update_user_contacts(host_user_id).await?;
|
self.update_user_contacts(host_user_id).await?;
|
||||||
|
@ -872,7 +895,7 @@ impl Server {
|
||||||
let project_id = ProjectId::from_proto(request.payload.project_id);
|
let project_id = ProjectId::from_proto(request.payload.project_id);
|
||||||
let project;
|
let project;
|
||||||
{
|
{
|
||||||
let mut store = self.store_mut().await;
|
let mut store = self.store().await;
|
||||||
project = store.leave_project(sender_id, project_id)?;
|
project = store.leave_project(sender_id, project_id)?;
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
%project_id,
|
%project_id,
|
||||||
|
@ -923,7 +946,7 @@ impl Server {
|
||||||
let project_id = ProjectId::from_proto(request.payload.project_id);
|
let project_id = ProjectId::from_proto(request.payload.project_id);
|
||||||
let user_id;
|
let user_id;
|
||||||
{
|
{
|
||||||
let mut state = self.store_mut().await;
|
let mut state = self.store().await;
|
||||||
user_id = state.user_id_for_connection(request.sender_id)?;
|
user_id = state.user_id_for_connection(request.sender_id)?;
|
||||||
let guest_connection_ids = state
|
let guest_connection_ids = state
|
||||||
.read_project(project_id, request.sender_id)?
|
.read_project(project_id, request.sender_id)?
|
||||||
|
@ -983,7 +1006,7 @@ impl Server {
|
||||||
self: Arc<Server>,
|
self: Arc<Server>,
|
||||||
request: TypedEnvelope<proto::RegisterProjectActivity>,
|
request: TypedEnvelope<proto::RegisterProjectActivity>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
self.store_mut().await.register_project_activity(
|
self.store().await.register_project_activity(
|
||||||
ProjectId::from_proto(request.payload.project_id),
|
ProjectId::from_proto(request.payload.project_id),
|
||||||
request.sender_id,
|
request.sender_id,
|
||||||
)?;
|
)?;
|
||||||
|
@ -998,7 +1021,7 @@ impl Server {
|
||||||
let project_id = ProjectId::from_proto(request.payload.project_id);
|
let project_id = ProjectId::from_proto(request.payload.project_id);
|
||||||
let worktree_id = request.payload.worktree_id;
|
let worktree_id = request.payload.worktree_id;
|
||||||
let (connection_ids, metadata_changed) = {
|
let (connection_ids, metadata_changed) = {
|
||||||
let mut store = self.store_mut().await;
|
let mut store = self.store().await;
|
||||||
let (connection_ids, metadata_changed) = store.update_worktree(
|
let (connection_ids, metadata_changed) = store.update_worktree(
|
||||||
request.sender_id,
|
request.sender_id,
|
||||||
project_id,
|
project_id,
|
||||||
|
@ -1007,6 +1030,7 @@ impl Server {
|
||||||
&request.payload.removed_entries,
|
&request.payload.removed_entries,
|
||||||
&request.payload.updated_entries,
|
&request.payload.updated_entries,
|
||||||
request.payload.scan_id,
|
request.payload.scan_id,
|
||||||
|
request.payload.is_last_update,
|
||||||
)?;
|
)?;
|
||||||
(connection_ids, metadata_changed)
|
(connection_ids, metadata_changed)
|
||||||
};
|
};
|
||||||
|
@ -1054,7 +1078,7 @@ impl Server {
|
||||||
.summary
|
.summary
|
||||||
.clone()
|
.clone()
|
||||||
.ok_or_else(|| anyhow!("invalid summary"))?;
|
.ok_or_else(|| anyhow!("invalid summary"))?;
|
||||||
let receiver_ids = self.store_mut().await.update_diagnostic_summary(
|
let receiver_ids = self.store().await.update_diagnostic_summary(
|
||||||
ProjectId::from_proto(request.payload.project_id),
|
ProjectId::from_proto(request.payload.project_id),
|
||||||
request.payload.worktree_id,
|
request.payload.worktree_id,
|
||||||
request.sender_id,
|
request.sender_id,
|
||||||
|
@ -1072,7 +1096,7 @@ impl Server {
|
||||||
self: Arc<Server>,
|
self: Arc<Server>,
|
||||||
request: TypedEnvelope<proto::StartLanguageServer>,
|
request: TypedEnvelope<proto::StartLanguageServer>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let receiver_ids = self.store_mut().await.start_language_server(
|
let receiver_ids = self.store().await.start_language_server(
|
||||||
ProjectId::from_proto(request.payload.project_id),
|
ProjectId::from_proto(request.payload.project_id),
|
||||||
request.sender_id,
|
request.sender_id,
|
||||||
request
|
request
|
||||||
|
@ -1111,20 +1135,23 @@ impl Server {
|
||||||
where
|
where
|
||||||
T: EntityMessage + RequestMessage,
|
T: EntityMessage + RequestMessage,
|
||||||
{
|
{
|
||||||
|
let project_id = ProjectId::from_proto(request.payload.remote_entity_id());
|
||||||
let host_connection_id = self
|
let host_connection_id = self
|
||||||
.store()
|
.store()
|
||||||
.await
|
.await
|
||||||
.read_project(
|
.read_project(project_id, request.sender_id)?
|
||||||
ProjectId::from_proto(request.payload.remote_entity_id()),
|
|
||||||
request.sender_id,
|
|
||||||
)?
|
|
||||||
.host_connection_id;
|
.host_connection_id;
|
||||||
|
let payload = self
|
||||||
|
.peer
|
||||||
|
.forward_request(request.sender_id, host_connection_id, request.payload)
|
||||||
|
.await?;
|
||||||
|
|
||||||
response.send(
|
// Ensure project still exists by the time we get the response from the host.
|
||||||
self.peer
|
self.store()
|
||||||
.forward_request(request.sender_id, host_connection_id, request.payload)
|
.await
|
||||||
.await?,
|
.read_project(project_id, request.sender_id)?;
|
||||||
)?;
|
|
||||||
|
response.send(payload)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1165,7 +1192,7 @@ impl Server {
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let project_id = ProjectId::from_proto(request.payload.project_id);
|
let project_id = ProjectId::from_proto(request.payload.project_id);
|
||||||
let receiver_ids = {
|
let receiver_ids = {
|
||||||
let mut store = self.store_mut().await;
|
let mut store = self.store().await;
|
||||||
store.register_project_activity(project_id, request.sender_id)?;
|
store.register_project_activity(project_id, request.sender_id)?;
|
||||||
store.project_connection_ids(project_id, request.sender_id)?
|
store.project_connection_ids(project_id, request.sender_id)?
|
||||||
};
|
};
|
||||||
|
@ -1232,7 +1259,7 @@ impl Server {
|
||||||
let leader_id = ConnectionId(request.payload.leader_id);
|
let leader_id = ConnectionId(request.payload.leader_id);
|
||||||
let follower_id = request.sender_id;
|
let follower_id = request.sender_id;
|
||||||
{
|
{
|
||||||
let mut store = self.store_mut().await;
|
let mut store = self.store().await;
|
||||||
if !store
|
if !store
|
||||||
.project_connection_ids(project_id, follower_id)?
|
.project_connection_ids(project_id, follower_id)?
|
||||||
.contains(&leader_id)
|
.contains(&leader_id)
|
||||||
|
@ -1257,7 +1284,7 @@ impl Server {
|
||||||
async fn unfollow(self: Arc<Self>, request: TypedEnvelope<proto::Unfollow>) -> Result<()> {
|
async fn unfollow(self: Arc<Self>, request: TypedEnvelope<proto::Unfollow>) -> Result<()> {
|
||||||
let project_id = ProjectId::from_proto(request.payload.project_id);
|
let project_id = ProjectId::from_proto(request.payload.project_id);
|
||||||
let leader_id = ConnectionId(request.payload.leader_id);
|
let leader_id = ConnectionId(request.payload.leader_id);
|
||||||
let mut store = self.store_mut().await;
|
let mut store = self.store().await;
|
||||||
if !store
|
if !store
|
||||||
.project_connection_ids(project_id, request.sender_id)?
|
.project_connection_ids(project_id, request.sender_id)?
|
||||||
.contains(&leader_id)
|
.contains(&leader_id)
|
||||||
|
@ -1275,7 +1302,7 @@ impl Server {
|
||||||
request: TypedEnvelope<proto::UpdateFollowers>,
|
request: TypedEnvelope<proto::UpdateFollowers>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let project_id = ProjectId::from_proto(request.payload.project_id);
|
let project_id = ProjectId::from_proto(request.payload.project_id);
|
||||||
let mut store = self.store_mut().await;
|
let mut store = self.store().await;
|
||||||
store.register_project_activity(project_id, request.sender_id)?;
|
store.register_project_activity(project_id, request.sender_id)?;
|
||||||
let connection_ids = store.project_connection_ids(project_id, request.sender_id)?;
|
let connection_ids = store.project_connection_ids(project_id, request.sender_id)?;
|
||||||
let leader_id = request
|
let leader_id = request
|
||||||
|
@ -1533,7 +1560,7 @@ impl Server {
|
||||||
Err(anyhow!("access denied"))?;
|
Err(anyhow!("access denied"))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.store_mut()
|
self.store()
|
||||||
.await
|
.await
|
||||||
.join_channel(request.sender_id, channel_id);
|
.join_channel(request.sender_id, channel_id);
|
||||||
let messages = self
|
let messages = self
|
||||||
|
@ -1575,7 +1602,7 @@ impl Server {
|
||||||
Err(anyhow!("access denied"))?;
|
Err(anyhow!("access denied"))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.store_mut()
|
self.store()
|
||||||
.await
|
.await
|
||||||
.leave_channel(request.sender_id, channel_id);
|
.leave_channel(request.sender_id, channel_id);
|
||||||
|
|
||||||
|
@ -1683,25 +1710,13 @@ impl Server {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn store<'a>(self: &'a Arc<Self>) -> StoreReadGuard<'a> {
|
pub(crate) async fn store<'a>(&'a self) -> StoreGuard<'a> {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
tokio::task::yield_now().await;
|
tokio::task::yield_now().await;
|
||||||
let guard = self.store.read().await;
|
let guard = self.store.lock().await;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
tokio::task::yield_now().await;
|
tokio::task::yield_now().await;
|
||||||
StoreReadGuard {
|
StoreGuard {
|
||||||
guard,
|
|
||||||
_not_send: PhantomData,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn store_mut<'a>(self: &'a Arc<Self>) -> StoreWriteGuard<'a> {
|
|
||||||
#[cfg(test)]
|
|
||||||
tokio::task::yield_now().await;
|
|
||||||
let guard = self.store.write().await;
|
|
||||||
#[cfg(test)]
|
|
||||||
tokio::task::yield_now().await;
|
|
||||||
StoreWriteGuard {
|
|
||||||
guard,
|
guard,
|
||||||
_not_send: PhantomData,
|
_not_send: PhantomData,
|
||||||
}
|
}
|
||||||
|
@ -1709,13 +1724,13 @@ impl Server {
|
||||||
|
|
||||||
pub async fn snapshot<'a>(self: &'a Arc<Self>) -> ServerSnapshot<'a> {
|
pub async fn snapshot<'a>(self: &'a Arc<Self>) -> ServerSnapshot<'a> {
|
||||||
ServerSnapshot {
|
ServerSnapshot {
|
||||||
store: self.store.read().await,
|
store: self.store().await,
|
||||||
peer: &self.peer,
|
peer: &self.peer,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Deref for StoreReadGuard<'a> {
|
impl<'a> Deref for StoreGuard<'a> {
|
||||||
type Target = Store;
|
type Target = Store;
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
fn deref(&self) -> &Self::Target {
|
||||||
|
@ -1723,21 +1738,13 @@ impl<'a> Deref for StoreReadGuard<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Deref for StoreWriteGuard<'a> {
|
impl<'a> DerefMut for StoreGuard<'a> {
|
||||||
type Target = Store;
|
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
&*self.guard
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> DerefMut for StoreWriteGuard<'a> {
|
|
||||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||||
&mut *self.guard
|
&mut *self.guard
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Drop for StoreWriteGuard<'a> {
|
impl<'a> Drop for StoreGuard<'a> {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
self.check_invariants();
|
self.check_invariants();
|
||||||
|
|
|
@ -56,6 +56,7 @@ pub struct Worktree {
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
pub diagnostic_summaries: BTreeMap<PathBuf, proto::DiagnosticSummary>,
|
pub diagnostic_summaries: BTreeMap<PathBuf, proto::DiagnosticSummary>,
|
||||||
pub scan_id: u64,
|
pub scan_id: u64,
|
||||||
|
pub is_complete: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
|
@ -646,6 +647,7 @@ impl Store {
|
||||||
removed_entries: &[u64],
|
removed_entries: &[u64],
|
||||||
updated_entries: &[proto::Entry],
|
updated_entries: &[proto::Entry],
|
||||||
scan_id: u64,
|
scan_id: u64,
|
||||||
|
is_last_update: bool,
|
||||||
) -> Result<(Vec<ConnectionId>, bool)> {
|
) -> Result<(Vec<ConnectionId>, bool)> {
|
||||||
let project = self.write_project(project_id, connection_id)?;
|
let project = self.write_project(project_id, connection_id)?;
|
||||||
if !project.online {
|
if !project.online {
|
||||||
|
@ -666,6 +668,7 @@ impl Store {
|
||||||
}
|
}
|
||||||
|
|
||||||
worktree.scan_id = scan_id;
|
worktree.scan_id = scan_id;
|
||||||
|
worktree.is_complete = is_last_update;
|
||||||
Ok((connection_ids, metadata_changed))
|
Ok((connection_ids, metadata_changed))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@ use contact_notification::ContactNotification;
|
||||||
use editor::{Cancel, Editor};
|
use editor::{Cancel, Editor};
|
||||||
use fuzzy::{match_strings, StringMatchCandidate};
|
use fuzzy::{match_strings, StringMatchCandidate};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
|
actions,
|
||||||
elements::*,
|
elements::*,
|
||||||
geometry::{rect::RectF, vector::vec2f},
|
geometry::{rect::RectF, vector::vec2f},
|
||||||
impl_actions, impl_internal_actions,
|
impl_actions, impl_internal_actions,
|
||||||
|
@ -24,6 +25,8 @@ use std::{ops::DerefMut, sync::Arc};
|
||||||
use theme::IconButton;
|
use theme::IconButton;
|
||||||
use workspace::{sidebar::SidebarItem, JoinProject, ToggleProjectOnline, Workspace};
|
use workspace::{sidebar::SidebarItem, JoinProject, ToggleProjectOnline, Workspace};
|
||||||
|
|
||||||
|
actions!(contacts_panel, [Toggle]);
|
||||||
|
|
||||||
impl_actions!(
|
impl_actions!(
|
||||||
contacts_panel,
|
contacts_panel,
|
||||||
[RequestContact, RemoveContact, RespondToContactRequest]
|
[RequestContact, RemoveContact, RespondToContactRequest]
|
||||||
|
|
|
@ -490,7 +490,7 @@ impl EditorElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
let block_text =
|
let block_text =
|
||||||
if matches!(self.cursor_shape, CursorShape::Block) {
|
if let CursorShape::Block = self.cursor_shape {
|
||||||
layout.snapshot.chars_at(cursor_position).next().and_then(
|
layout.snapshot.chars_at(cursor_position).next().and_then(
|
||||||
|character| {
|
|character| {
|
||||||
let font_id =
|
let font_id =
|
||||||
|
@ -520,7 +520,7 @@ impl EditorElement {
|
||||||
cursors.push(Cursor {
|
cursors.push(Cursor {
|
||||||
color: selection_style.cursor,
|
color: selection_style.cursor,
|
||||||
block_width,
|
block_width,
|
||||||
origin: content_origin + vec2f(x, y),
|
origin: vec2f(x, y),
|
||||||
line_height: layout.line_height,
|
line_height: layout.line_height,
|
||||||
shape: self.cursor_shape,
|
shape: self.cursor_shape,
|
||||||
block_text,
|
block_text,
|
||||||
|
@ -546,13 +546,12 @@ impl EditorElement {
|
||||||
|
|
||||||
cx.scene.push_layer(Some(bounds));
|
cx.scene.push_layer(Some(bounds));
|
||||||
for cursor in cursors {
|
for cursor in cursors {
|
||||||
cursor.paint(cx);
|
cursor.paint(content_origin, cx);
|
||||||
}
|
}
|
||||||
cx.scene.pop_layer();
|
cx.scene.pop_layer();
|
||||||
|
|
||||||
if let Some((position, context_menu)) = layout.context_menu.as_mut() {
|
if let Some((position, context_menu)) = layout.context_menu.as_mut() {
|
||||||
cx.scene.push_stacking_context(None);
|
cx.scene.push_stacking_context(None);
|
||||||
|
|
||||||
let cursor_row_layout = &layout.line_layouts[(position.row() - start_row) as usize];
|
let cursor_row_layout = &layout.line_layouts[(position.row() - start_row) as usize];
|
||||||
let x = cursor_row_layout.x_for_index(position.column() as usize) - scroll_left;
|
let x = cursor_row_layout.x_for_index(position.column() as usize) - scroll_left;
|
||||||
let y = (position.row() + 1) as f32 * layout.line_height - scroll_top;
|
let y = (position.row() + 1) as f32 * layout.line_height - scroll_top;
|
||||||
|
@ -1630,7 +1629,7 @@ impl Default for CursorShape {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Cursor {
|
pub struct Cursor {
|
||||||
origin: Vector2F,
|
origin: Vector2F,
|
||||||
block_width: f32,
|
block_width: f32,
|
||||||
line_height: f32,
|
line_height: f32,
|
||||||
|
@ -1640,14 +1639,33 @@ struct Cursor {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Cursor {
|
impl Cursor {
|
||||||
fn paint(&self, cx: &mut PaintContext) {
|
pub fn new(
|
||||||
|
origin: Vector2F,
|
||||||
|
block_width: f32,
|
||||||
|
line_height: f32,
|
||||||
|
color: Color,
|
||||||
|
shape: CursorShape,
|
||||||
|
block_text: Option<Line>,
|
||||||
|
) -> Cursor {
|
||||||
|
Cursor {
|
||||||
|
origin,
|
||||||
|
block_width,
|
||||||
|
line_height,
|
||||||
|
color,
|
||||||
|
shape,
|
||||||
|
block_text,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn paint(&self, origin: Vector2F, cx: &mut PaintContext) {
|
||||||
let bounds = match self.shape {
|
let bounds = match self.shape {
|
||||||
CursorShape::Bar => RectF::new(self.origin, vec2f(2.0, self.line_height)),
|
CursorShape::Bar => RectF::new(self.origin + origin, vec2f(2.0, self.line_height)),
|
||||||
CursorShape::Block => {
|
CursorShape::Block => RectF::new(
|
||||||
RectF::new(self.origin, vec2f(self.block_width, self.line_height))
|
self.origin + origin,
|
||||||
}
|
vec2f(self.block_width, self.line_height),
|
||||||
|
),
|
||||||
CursorShape::Underscore => RectF::new(
|
CursorShape::Underscore => RectF::new(
|
||||||
self.origin + Vector2F::new(0.0, self.line_height - 2.0),
|
self.origin + origin + Vector2F::new(0.0, self.line_height - 2.0),
|
||||||
vec2f(self.block_width, 2.0),
|
vec2f(self.block_width, 2.0),
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
@ -1660,7 +1678,7 @@ impl Cursor {
|
||||||
});
|
});
|
||||||
|
|
||||||
if let Some(block_text) = &self.block_text {
|
if let Some(block_text) = &self.block_text {
|
||||||
block_text.paint(self.origin, bounds, self.line_height, cx);
|
block_text.paint(self.origin + origin, bounds, self.line_height, cx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1634,14 +1634,10 @@ impl MutableAppContext {
|
||||||
pub fn default_global<T: 'static + Default>(&mut self) -> &T {
|
pub fn default_global<T: 'static + Default>(&mut self) -> &T {
|
||||||
let type_id = TypeId::of::<T>();
|
let type_id = TypeId::of::<T>();
|
||||||
self.update(|this| {
|
self.update(|this| {
|
||||||
if !this.globals.contains_key(&type_id) {
|
if let Entry::Vacant(entry) = this.cx.globals.entry(type_id) {
|
||||||
|
entry.insert(Box::new(T::default()));
|
||||||
this.notify_global(type_id);
|
this.notify_global(type_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.cx
|
|
||||||
.globals
|
|
||||||
.entry(type_id)
|
|
||||||
.or_insert_with(|| Box::new(T::default()));
|
|
||||||
});
|
});
|
||||||
self.globals.get(&type_id).unwrap().downcast_ref().unwrap()
|
self.globals.get(&type_id).unwrap().downcast_ref().unwrap()
|
||||||
}
|
}
|
||||||
|
|
|
@ -703,6 +703,20 @@ impl<'a> EventContext<'a> {
|
||||||
self.view_stack.last().copied()
|
self.view_stack.last().copied()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn is_parent_view_focused(&self) -> bool {
|
||||||
|
if let Some(parent_view_id) = self.view_stack.last() {
|
||||||
|
self.app.focused_view_id(self.window_id) == Some(*parent_view_id)
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn focus_parent_view(&mut self) {
|
||||||
|
if let Some(parent_view_id) = self.view_stack.last() {
|
||||||
|
self.app.focus(self.window_id, Some(*parent_view_id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn dispatch_any_action(&mut self, action: Box<dyn Action>) {
|
pub fn dispatch_any_action(&mut self, action: Box<dyn Action>) {
|
||||||
self.dispatched_actions.push(DispatchDirective {
|
self.dispatched_actions.push(DispatchDirective {
|
||||||
dispatcher_view_id: self.view_stack.last().copied(),
|
dispatcher_view_id: self.view_stack.last().copied(),
|
||||||
|
|
|
@ -164,7 +164,7 @@ impl<'a> Hash for CacheKeyRef<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Debug)]
|
#[derive(Default, Debug, Clone)]
|
||||||
pub struct Line {
|
pub struct Line {
|
||||||
layout: Arc<LineLayout>,
|
layout: Arc<LineLayout>,
|
||||||
style_runs: SmallVec<[(u32, Color, Underline); 32]>,
|
style_runs: SmallVec<[(u32, Color, Underline); 32]>,
|
||||||
|
|
|
@ -11,7 +11,7 @@ use serde_json::{json, value::RawValue, Value};
|
||||||
use smol::{
|
use smol::{
|
||||||
channel,
|
channel,
|
||||||
io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader},
|
io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader},
|
||||||
process,
|
process::{self, Child},
|
||||||
};
|
};
|
||||||
use std::{
|
use std::{
|
||||||
future::Future,
|
future::Future,
|
||||||
|
@ -44,6 +44,7 @@ pub struct LanguageServer {
|
||||||
io_tasks: Mutex<Option<(Task<Option<()>>, Task<Option<()>>)>>,
|
io_tasks: Mutex<Option<(Task<Option<()>>, Task<Option<()>>)>>,
|
||||||
output_done_rx: Mutex<Option<barrier::Receiver>>,
|
output_done_rx: Mutex<Option<barrier::Receiver>>,
|
||||||
root_path: PathBuf,
|
root_path: PathBuf,
|
||||||
|
_server: Option<Child>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Subscription {
|
pub struct Subscription {
|
||||||
|
@ -118,11 +119,20 @@ impl LanguageServer {
|
||||||
.stdin(Stdio::piped())
|
.stdin(Stdio::piped())
|
||||||
.stdout(Stdio::piped())
|
.stdout(Stdio::piped())
|
||||||
.stderr(Stdio::inherit())
|
.stderr(Stdio::inherit())
|
||||||
|
.kill_on_drop(true)
|
||||||
.spawn()?;
|
.spawn()?;
|
||||||
|
|
||||||
let stdin = server.stdin.take().unwrap();
|
let stdin = server.stdin.take().unwrap();
|
||||||
let stdout = server.stdout.take().unwrap();
|
let stout = server.stdout.take().unwrap();
|
||||||
let mut server =
|
|
||||||
Self::new_internal(server_id, stdin, stdout, root_path, cx, |notification| {
|
let mut server = Self::new_internal(
|
||||||
|
server_id,
|
||||||
|
stdin,
|
||||||
|
stout,
|
||||||
|
Some(server),
|
||||||
|
root_path,
|
||||||
|
cx,
|
||||||
|
|notification| {
|
||||||
log::info!(
|
log::info!(
|
||||||
"unhandled notification {}:\n{}",
|
"unhandled notification {}:\n{}",
|
||||||
notification.method,
|
notification.method,
|
||||||
|
@ -131,7 +141,8 @@ impl LanguageServer {
|
||||||
)
|
)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
);
|
);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
if let Some(name) = binary_path.file_name() {
|
if let Some(name) = binary_path.file_name() {
|
||||||
server.name = name.to_string_lossy().to_string();
|
server.name = name.to_string_lossy().to_string();
|
||||||
}
|
}
|
||||||
|
@ -142,6 +153,7 @@ impl LanguageServer {
|
||||||
server_id: usize,
|
server_id: usize,
|
||||||
stdin: Stdin,
|
stdin: Stdin,
|
||||||
stdout: Stdout,
|
stdout: Stdout,
|
||||||
|
server: Option<Child>,
|
||||||
root_path: &Path,
|
root_path: &Path,
|
||||||
cx: AsyncAppContext,
|
cx: AsyncAppContext,
|
||||||
mut on_unhandled_notification: F,
|
mut on_unhandled_notification: F,
|
||||||
|
@ -242,6 +254,7 @@ impl LanguageServer {
|
||||||
io_tasks: Mutex::new(Some((input_task, output_task))),
|
io_tasks: Mutex::new(Some((input_task, output_task))),
|
||||||
output_done_rx: Mutex::new(Some(output_done_rx)),
|
output_done_rx: Mutex::new(Some(output_done_rx)),
|
||||||
root_path: root_path.to_path_buf(),
|
root_path: root_path.to_path_buf(),
|
||||||
|
_server: server,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -480,6 +493,10 @@ impl LanguageServer {
|
||||||
self.server_id
|
self.server_id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn root_path(&self) -> &PathBuf {
|
||||||
|
&self.root_path
|
||||||
|
}
|
||||||
|
|
||||||
pub fn request<T: request::Request>(
|
pub fn request<T: request::Request>(
|
||||||
&self,
|
&self,
|
||||||
params: T::Params,
|
params: T::Params,
|
||||||
|
@ -608,6 +625,7 @@ impl LanguageServer {
|
||||||
0,
|
0,
|
||||||
stdin_writer,
|
stdin_writer,
|
||||||
stdout_reader,
|
stdout_reader,
|
||||||
|
None,
|
||||||
Path::new("/"),
|
Path::new("/"),
|
||||||
cx.clone(),
|
cx.clone(),
|
||||||
|_| {},
|
|_| {},
|
||||||
|
@ -617,6 +635,7 @@ impl LanguageServer {
|
||||||
0,
|
0,
|
||||||
stdout_writer,
|
stdout_writer,
|
||||||
stdin_reader,
|
stdin_reader,
|
||||||
|
None,
|
||||||
Path::new("/"),
|
Path::new("/"),
|
||||||
cx.clone(),
|
cx.clone(),
|
||||||
move |msg| {
|
move |msg| {
|
||||||
|
|
|
@ -242,7 +242,7 @@ impl LspCommand for PerformRename {
|
||||||
.read_with(&cx, |project, cx| {
|
.read_with(&cx, |project, cx| {
|
||||||
project
|
project
|
||||||
.language_server_for_buffer(buffer.read(cx), cx)
|
.language_server_for_buffer(buffer.read(cx), cx)
|
||||||
.cloned()
|
.map(|(adapter, server)| (adapter.clone(), server.clone()))
|
||||||
})
|
})
|
||||||
.ok_or_else(|| anyhow!("no language server found for buffer"))?;
|
.ok_or_else(|| anyhow!("no language server found for buffer"))?;
|
||||||
Project::deserialize_workspace_edit(
|
Project::deserialize_workspace_edit(
|
||||||
|
@ -359,7 +359,7 @@ impl LspCommand for GetDefinition {
|
||||||
.read_with(&cx, |project, cx| {
|
.read_with(&cx, |project, cx| {
|
||||||
project
|
project
|
||||||
.language_server_for_buffer(buffer.read(cx), cx)
|
.language_server_for_buffer(buffer.read(cx), cx)
|
||||||
.cloned()
|
.map(|(adapter, server)| (adapter.clone(), server.clone()))
|
||||||
})
|
})
|
||||||
.ok_or_else(|| anyhow!("no language server found for buffer"))?;
|
.ok_or_else(|| anyhow!("no language server found for buffer"))?;
|
||||||
|
|
||||||
|
@ -388,8 +388,8 @@ impl LspCommand for GetDefinition {
|
||||||
.update(&mut cx, |this, cx| {
|
.update(&mut cx, |this, cx| {
|
||||||
this.open_local_buffer_via_lsp(
|
this.open_local_buffer_via_lsp(
|
||||||
target_uri,
|
target_uri,
|
||||||
lsp_adapter.clone(),
|
language_server.server_id(),
|
||||||
language_server.clone(),
|
lsp_adapter.name(),
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
@ -599,7 +599,7 @@ impl LspCommand for GetReferences {
|
||||||
.read_with(&cx, |project, cx| {
|
.read_with(&cx, |project, cx| {
|
||||||
project
|
project
|
||||||
.language_server_for_buffer(buffer.read(cx), cx)
|
.language_server_for_buffer(buffer.read(cx), cx)
|
||||||
.cloned()
|
.map(|(adapter, server)| (adapter.clone(), server.clone()))
|
||||||
})
|
})
|
||||||
.ok_or_else(|| anyhow!("no language server found for buffer"))?;
|
.ok_or_else(|| anyhow!("no language server found for buffer"))?;
|
||||||
|
|
||||||
|
@ -609,8 +609,8 @@ impl LspCommand for GetReferences {
|
||||||
.update(&mut cx, |this, cx| {
|
.update(&mut cx, |this, cx| {
|
||||||
this.open_local_buffer_via_lsp(
|
this.open_local_buffer_via_lsp(
|
||||||
lsp_location.uri,
|
lsp_location.uri,
|
||||||
lsp_adapter.clone(),
|
language_server.server_id(),
|
||||||
language_server.clone(),
|
lsp_adapter.name(),
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -7,9 +7,9 @@ use super::{
|
||||||
};
|
};
|
||||||
use ::ignore::gitignore::{Gitignore, GitignoreBuilder};
|
use ::ignore::gitignore::{Gitignore, GitignoreBuilder};
|
||||||
use anyhow::{anyhow, Context, Result};
|
use anyhow::{anyhow, Context, Result};
|
||||||
use client::{proto, Client, TypedEnvelope};
|
use client::{proto, Client};
|
||||||
use clock::ReplicaId;
|
use clock::ReplicaId;
|
||||||
use collections::HashMap;
|
use collections::{HashMap, VecDeque};
|
||||||
use futures::{
|
use futures::{
|
||||||
channel::{
|
channel::{
|
||||||
mpsc::{self, UnboundedSender},
|
mpsc::{self, UnboundedSender},
|
||||||
|
@ -40,11 +40,11 @@ use std::{
|
||||||
ffi::{OsStr, OsString},
|
ffi::{OsStr, OsString},
|
||||||
fmt,
|
fmt,
|
||||||
future::Future,
|
future::Future,
|
||||||
mem,
|
|
||||||
ops::{Deref, DerefMut},
|
ops::{Deref, DerefMut},
|
||||||
os::unix::prelude::{OsStrExt, OsStringExt},
|
os::unix::prelude::{OsStrExt, OsStringExt},
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
sync::{atomic::AtomicUsize, Arc},
|
sync::{atomic::AtomicUsize, Arc},
|
||||||
|
task::Poll,
|
||||||
time::{Duration, SystemTime},
|
time::{Duration, SystemTime},
|
||||||
};
|
};
|
||||||
use sum_tree::{Bias, Edit, SeekTarget, SumTree, TreeMap};
|
use sum_tree::{Bias, Edit, SeekTarget, SumTree, TreeMap};
|
||||||
|
@ -82,7 +82,7 @@ pub struct RemoteWorktree {
|
||||||
project_id: u64,
|
project_id: u64,
|
||||||
client: Arc<Client>,
|
client: Arc<Client>,
|
||||||
updates_tx: Option<UnboundedSender<proto::UpdateWorktree>>,
|
updates_tx: Option<UnboundedSender<proto::UpdateWorktree>>,
|
||||||
last_scan_id_rx: watch::Receiver<usize>,
|
snapshot_subscriptions: VecDeque<(usize, oneshot::Sender<()>)>,
|
||||||
replica_id: ReplicaId,
|
replica_id: ReplicaId,
|
||||||
diagnostic_summaries: TreeMap<PathKey, DiagnosticSummary>,
|
diagnostic_summaries: TreeMap<PathKey, DiagnosticSummary>,
|
||||||
visible: bool,
|
visible: bool,
|
||||||
|
@ -96,6 +96,7 @@ pub struct Snapshot {
|
||||||
entries_by_path: SumTree<Entry>,
|
entries_by_path: SumTree<Entry>,
|
||||||
entries_by_id: SumTree<PathEntry>,
|
entries_by_id: SumTree<PathEntry>,
|
||||||
scan_id: usize,
|
scan_id: usize,
|
||||||
|
is_complete: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
|
@ -125,13 +126,16 @@ impl DerefMut for LocalSnapshot {
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
enum ScanState {
|
enum ScanState {
|
||||||
Idle,
|
Idle,
|
||||||
Scanning,
|
/// The worktree is performing its initial scan of the filesystem.
|
||||||
|
Initializing,
|
||||||
|
/// The worktree is updating in response to filesystem events.
|
||||||
|
Updating,
|
||||||
Err(Arc<anyhow::Error>),
|
Err(Arc<anyhow::Error>),
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ShareState {
|
struct ShareState {
|
||||||
project_id: u64,
|
project_id: u64,
|
||||||
snapshots_tx: Sender<LocalSnapshot>,
|
snapshots_tx: watch::Sender<LocalSnapshot>,
|
||||||
_maintain_remote_snapshot: Option<Task<Option<()>>>,
|
_maintain_remote_snapshot: Option<Task<Option<()>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -172,10 +176,10 @@ impl Worktree {
|
||||||
pub fn remote(
|
pub fn remote(
|
||||||
project_remote_id: u64,
|
project_remote_id: u64,
|
||||||
replica_id: ReplicaId,
|
replica_id: ReplicaId,
|
||||||
worktree: proto::Worktree,
|
worktree: proto::WorktreeMetadata,
|
||||||
client: Arc<Client>,
|
client: Arc<Client>,
|
||||||
cx: &mut MutableAppContext,
|
cx: &mut MutableAppContext,
|
||||||
) -> (ModelHandle<Self>, Task<()>) {
|
) -> ModelHandle<Self> {
|
||||||
let remote_id = worktree.id;
|
let remote_id = worktree.id;
|
||||||
let root_char_bag: CharBag = worktree
|
let root_char_bag: CharBag = worktree
|
||||||
.root_name
|
.root_name
|
||||||
|
@ -190,13 +194,13 @@ impl Worktree {
|
||||||
root_char_bag,
|
root_char_bag,
|
||||||
entries_by_path: Default::default(),
|
entries_by_path: Default::default(),
|
||||||
entries_by_id: Default::default(),
|
entries_by_id: Default::default(),
|
||||||
scan_id: worktree.scan_id as usize,
|
scan_id: 0,
|
||||||
|
is_complete: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
let (updates_tx, mut updates_rx) = mpsc::unbounded();
|
let (updates_tx, mut updates_rx) = mpsc::unbounded();
|
||||||
let background_snapshot = Arc::new(Mutex::new(snapshot.clone()));
|
let background_snapshot = Arc::new(Mutex::new(snapshot.clone()));
|
||||||
let (mut snapshot_updated_tx, mut snapshot_updated_rx) = watch::channel();
|
let (mut snapshot_updated_tx, mut snapshot_updated_rx) = watch::channel();
|
||||||
let (mut last_scan_id_tx, last_scan_id_rx) = watch::channel_with(worktree.scan_id as usize);
|
|
||||||
let worktree_handle = cx.add_model(|_: &mut ModelContext<Worktree>| {
|
let worktree_handle = cx.add_model(|_: &mut ModelContext<Worktree>| {
|
||||||
Worktree::Remote(RemoteWorktree {
|
Worktree::Remote(RemoteWorktree {
|
||||||
project_id: project_remote_id,
|
project_id: project_remote_id,
|
||||||
|
@ -204,96 +208,50 @@ impl Worktree {
|
||||||
snapshot: snapshot.clone(),
|
snapshot: snapshot.clone(),
|
||||||
background_snapshot: background_snapshot.clone(),
|
background_snapshot: background_snapshot.clone(),
|
||||||
updates_tx: Some(updates_tx),
|
updates_tx: Some(updates_tx),
|
||||||
last_scan_id_rx,
|
snapshot_subscriptions: Default::default(),
|
||||||
client: client.clone(),
|
client: client.clone(),
|
||||||
diagnostic_summaries: TreeMap::from_ordered_entries(
|
diagnostic_summaries: Default::default(),
|
||||||
worktree.diagnostic_summaries.into_iter().map(|summary| {
|
|
||||||
(
|
|
||||||
PathKey(PathBuf::from(summary.path).into()),
|
|
||||||
DiagnosticSummary {
|
|
||||||
language_server_id: summary.language_server_id as usize,
|
|
||||||
error_count: summary.error_count as usize,
|
|
||||||
warning_count: summary.warning_count as usize,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
visible,
|
visible,
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
let deserialize_task = cx.spawn({
|
cx.background()
|
||||||
let worktree_handle = worktree_handle.clone();
|
.spawn(async move {
|
||||||
|cx| async move {
|
while let Some(update) = updates_rx.next().await {
|
||||||
let (entries_by_path, entries_by_id) = cx
|
if let Err(error) = background_snapshot.lock().apply_remote_update(update) {
|
||||||
.background()
|
log::error!("error applying worktree update: {}", error);
|
||||||
.spawn(async move {
|
}
|
||||||
let mut entries_by_path_edits = Vec::new();
|
|
||||||
let mut entries_by_id_edits = Vec::new();
|
|
||||||
for entry in worktree.entries {
|
|
||||||
match Entry::try_from((&root_char_bag, entry)) {
|
|
||||||
Ok(entry) => {
|
|
||||||
entries_by_id_edits.push(Edit::Insert(PathEntry {
|
|
||||||
id: entry.id,
|
|
||||||
path: entry.path.clone(),
|
|
||||||
is_ignored: entry.is_ignored,
|
|
||||||
scan_id: 0,
|
|
||||||
}));
|
|
||||||
entries_by_path_edits.push(Edit::Insert(entry));
|
|
||||||
}
|
|
||||||
Err(err) => log::warn!("error for remote worktree entry {:?}", err),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut entries_by_path = SumTree::new();
|
|
||||||
let mut entries_by_id = SumTree::new();
|
|
||||||
entries_by_path.edit(entries_by_path_edits, &());
|
|
||||||
entries_by_id.edit(entries_by_id_edits, &());
|
|
||||||
|
|
||||||
(entries_by_path, entries_by_id)
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
|
|
||||||
{
|
|
||||||
let mut snapshot = background_snapshot.lock();
|
|
||||||
snapshot.entries_by_path = entries_by_path;
|
|
||||||
snapshot.entries_by_id = entries_by_id;
|
|
||||||
snapshot_updated_tx.send(()).await.ok();
|
snapshot_updated_tx.send(()).await.ok();
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
|
||||||
cx.background()
|
cx.spawn(|mut cx| {
|
||||||
.spawn(async move {
|
let this = worktree_handle.downgrade();
|
||||||
while let Some(update) = updates_rx.next().await {
|
async move {
|
||||||
if let Err(error) =
|
while let Some(_) = snapshot_updated_rx.recv().await {
|
||||||
background_snapshot.lock().apply_remote_update(update)
|
if let Some(this) = this.upgrade(&cx) {
|
||||||
{
|
this.update(&mut cx, |this, cx| {
|
||||||
log::error!("error applying worktree update: {}", error);
|
this.poll_snapshot(cx);
|
||||||
|
let this = this.as_remote_mut().unwrap();
|
||||||
|
while let Some((scan_id, _)) = this.snapshot_subscriptions.front() {
|
||||||
|
if this.observed_snapshot(*scan_id) {
|
||||||
|
let (_, tx) = this.snapshot_subscriptions.pop_front().unwrap();
|
||||||
|
let _ = tx.send(());
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
snapshot_updated_tx.send(()).await.ok();
|
});
|
||||||
}
|
} else {
|
||||||
})
|
break;
|
||||||
.detach();
|
|
||||||
|
|
||||||
cx.spawn(|mut cx| {
|
|
||||||
let this = worktree_handle.downgrade();
|
|
||||||
async move {
|
|
||||||
while let Some(_) = snapshot_updated_rx.recv().await {
|
|
||||||
if let Some(this) = this.upgrade(&cx) {
|
|
||||||
this.update(&mut cx, |this, cx| {
|
|
||||||
this.poll_snapshot(cx);
|
|
||||||
let this = this.as_remote_mut().unwrap();
|
|
||||||
*last_scan_id_tx.borrow_mut() = this.snapshot.scan_id;
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
.detach();
|
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
(worktree_handle, deserialize_task)
|
.detach();
|
||||||
|
|
||||||
|
worktree_handle
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn as_local(&self) -> Option<&LocalWorktree> {
|
pub fn as_local(&self) -> Option<&LocalWorktree> {
|
||||||
|
@ -377,38 +335,9 @@ impl Worktree {
|
||||||
|
|
||||||
fn poll_snapshot(&mut self, cx: &mut ModelContext<Self>) {
|
fn poll_snapshot(&mut self, cx: &mut ModelContext<Self>) {
|
||||||
match self {
|
match self {
|
||||||
Self::Local(worktree) => {
|
Self::Local(worktree) => worktree.poll_snapshot(false, cx),
|
||||||
let is_fake_fs = worktree.fs.is_fake();
|
Self::Remote(worktree) => worktree.poll_snapshot(cx),
|
||||||
worktree.snapshot = worktree.background_snapshot.lock().clone();
|
|
||||||
if worktree.is_scanning() {
|
|
||||||
if worktree.poll_task.is_none() {
|
|
||||||
worktree.poll_task = Some(cx.spawn_weak(|this, mut cx| async move {
|
|
||||||
if is_fake_fs {
|
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
|
||||||
cx.background().simulate_random_delay().await;
|
|
||||||
} else {
|
|
||||||
smol::Timer::after(Duration::from_millis(100)).await;
|
|
||||||
}
|
|
||||||
if let Some(this) = this.upgrade(&cx) {
|
|
||||||
this.update(&mut cx, |this, cx| {
|
|
||||||
this.as_local_mut().unwrap().poll_task = None;
|
|
||||||
this.poll_snapshot(cx);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
worktree.poll_task.take();
|
|
||||||
cx.emit(Event::UpdatedEntries);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Self::Remote(worktree) => {
|
|
||||||
worktree.snapshot = worktree.background_snapshot.lock().clone();
|
|
||||||
cx.emit(Event::UpdatedEntries);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
cx.notify();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -436,7 +365,8 @@ impl LocalWorktree {
|
||||||
.context("failed to stat worktree path")?;
|
.context("failed to stat worktree path")?;
|
||||||
|
|
||||||
let (scan_states_tx, mut scan_states_rx) = mpsc::unbounded();
|
let (scan_states_tx, mut scan_states_rx) = mpsc::unbounded();
|
||||||
let (mut last_scan_state_tx, last_scan_state_rx) = watch::channel_with(ScanState::Scanning);
|
let (mut last_scan_state_tx, last_scan_state_rx) =
|
||||||
|
watch::channel_with(ScanState::Initializing);
|
||||||
let tree = cx.add_model(move |cx: &mut ModelContext<Worktree>| {
|
let tree = cx.add_model(move |cx: &mut ModelContext<Worktree>| {
|
||||||
let mut snapshot = LocalSnapshot {
|
let mut snapshot = LocalSnapshot {
|
||||||
abs_path,
|
abs_path,
|
||||||
|
@ -450,6 +380,7 @@ impl LocalWorktree {
|
||||||
entries_by_path: Default::default(),
|
entries_by_path: Default::default(),
|
||||||
entries_by_id: Default::default(),
|
entries_by_id: Default::default(),
|
||||||
scan_id: 0,
|
scan_id: 0,
|
||||||
|
is_complete: true,
|
||||||
},
|
},
|
||||||
extension_counts: Default::default(),
|
extension_counts: Default::default(),
|
||||||
};
|
};
|
||||||
|
@ -481,11 +412,7 @@ impl LocalWorktree {
|
||||||
while let Some(scan_state) = scan_states_rx.next().await {
|
while let Some(scan_state) = scan_states_rx.next().await {
|
||||||
if let Some(this) = this.upgrade(&cx) {
|
if let Some(this) = this.upgrade(&cx) {
|
||||||
last_scan_state_tx.blocking_send(scan_state).ok();
|
last_scan_state_tx.blocking_send(scan_state).ok();
|
||||||
this.update(&mut cx, |this, cx| {
|
this.update(&mut cx, |this, cx| this.poll_snapshot(cx));
|
||||||
this.poll_snapshot(cx);
|
|
||||||
this.as_local().unwrap().broadcast_snapshot()
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
} else {
|
} else {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -569,22 +496,53 @@ impl LocalWorktree {
|
||||||
Ok(updated)
|
Ok(updated)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn poll_snapshot(&mut self, force: bool, cx: &mut ModelContext<Worktree>) {
|
||||||
|
self.poll_task.take();
|
||||||
|
match self.scan_state() {
|
||||||
|
ScanState::Idle => {
|
||||||
|
self.snapshot = self.background_snapshot.lock().clone();
|
||||||
|
if let Some(share) = self.share.as_mut() {
|
||||||
|
*share.snapshots_tx.borrow_mut() = self.snapshot.clone();
|
||||||
|
}
|
||||||
|
cx.emit(Event::UpdatedEntries);
|
||||||
|
}
|
||||||
|
ScanState::Initializing => {
|
||||||
|
let is_fake_fs = self.fs.is_fake();
|
||||||
|
self.snapshot = self.background_snapshot.lock().clone();
|
||||||
|
self.poll_task = Some(cx.spawn_weak(|this, mut cx| async move {
|
||||||
|
if is_fake_fs {
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
cx.background().simulate_random_delay().await;
|
||||||
|
} else {
|
||||||
|
smol::Timer::after(Duration::from_millis(100)).await;
|
||||||
|
}
|
||||||
|
if let Some(this) = this.upgrade(&cx) {
|
||||||
|
this.update(&mut cx, |this, cx| this.poll_snapshot(cx));
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
cx.emit(Event::UpdatedEntries);
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
if force {
|
||||||
|
self.snapshot = self.background_snapshot.lock().clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
pub fn scan_complete(&self) -> impl Future<Output = ()> {
|
pub fn scan_complete(&self) -> impl Future<Output = ()> {
|
||||||
let mut scan_state_rx = self.last_scan_state_rx.clone();
|
let mut scan_state_rx = self.last_scan_state_rx.clone();
|
||||||
async move {
|
async move {
|
||||||
let mut scan_state = Some(scan_state_rx.borrow().clone());
|
let mut scan_state = Some(scan_state_rx.borrow().clone());
|
||||||
while let Some(ScanState::Scanning) = scan_state {
|
while let Some(ScanState::Initializing | ScanState::Updating) = scan_state {
|
||||||
scan_state = scan_state_rx.recv().await;
|
scan_state = scan_state_rx.recv().await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_scanning(&self) -> bool {
|
fn scan_state(&self) -> ScanState {
|
||||||
if let ScanState::Scanning = *self.last_scan_state_rx.borrow() {
|
self.last_scan_state_rx.borrow().clone()
|
||||||
true
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn snapshot(&self) -> LocalSnapshot {
|
pub fn snapshot(&self) -> LocalSnapshot {
|
||||||
|
@ -614,7 +572,6 @@ impl LocalWorktree {
|
||||||
.refresh_entry(path, abs_path, None, cx)
|
.refresh_entry(path, abs_path, None, cx)
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
this.update(&mut cx, |this, cx| this.poll_snapshot(cx));
|
|
||||||
Ok((
|
Ok((
|
||||||
File {
|
File {
|
||||||
entry_id: Some(entry.id),
|
entry_id: Some(entry.id),
|
||||||
|
@ -712,16 +669,14 @@ impl LocalWorktree {
|
||||||
|
|
||||||
Some(cx.spawn(|this, mut cx| async move {
|
Some(cx.spawn(|this, mut cx| async move {
|
||||||
delete.await?;
|
delete.await?;
|
||||||
this.update(&mut cx, |this, _| {
|
|
||||||
let this = this.as_local_mut().unwrap();
|
|
||||||
let mut snapshot = this.background_snapshot.lock();
|
|
||||||
snapshot.delete_entry(entry_id);
|
|
||||||
});
|
|
||||||
this.update(&mut cx, |this, cx| {
|
this.update(&mut cx, |this, cx| {
|
||||||
this.poll_snapshot(cx);
|
let this = this.as_local_mut().unwrap();
|
||||||
this.as_local().unwrap().broadcast_snapshot()
|
{
|
||||||
})
|
let mut snapshot = this.background_snapshot.lock();
|
||||||
.await;
|
snapshot.delete_entry(entry_id);
|
||||||
|
}
|
||||||
|
this.poll_snapshot(true, cx);
|
||||||
|
});
|
||||||
Ok(())
|
Ok(())
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
@ -757,11 +712,6 @@ impl LocalWorktree {
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
this.update(&mut cx, |this, cx| {
|
|
||||||
this.poll_snapshot(cx);
|
|
||||||
this.as_local().unwrap().broadcast_snapshot()
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
Ok(entry)
|
Ok(entry)
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
@ -797,11 +747,6 @@ impl LocalWorktree {
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
this.update(&mut cx, |this, cx| {
|
|
||||||
this.poll_snapshot(cx);
|
|
||||||
this.as_local().unwrap().broadcast_snapshot()
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
Ok(entry)
|
Ok(entry)
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
@ -835,11 +780,6 @@ impl LocalWorktree {
|
||||||
.refresh_entry(path, abs_path, None, cx)
|
.refresh_entry(path, abs_path, None, cx)
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
this.update(&mut cx, |this, cx| {
|
|
||||||
this.poll_snapshot(cx);
|
|
||||||
this.as_local().unwrap().broadcast_snapshot()
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
Ok(entry)
|
Ok(entry)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -872,61 +812,55 @@ impl LocalWorktree {
|
||||||
let this = this
|
let this = this
|
||||||
.upgrade(&cx)
|
.upgrade(&cx)
|
||||||
.ok_or_else(|| anyhow!("worktree was dropped"))?;
|
.ok_or_else(|| anyhow!("worktree was dropped"))?;
|
||||||
let (entry, snapshot, snapshots_tx) = this.read_with(&cx, |this, _| {
|
this.update(&mut cx, |this, cx| {
|
||||||
let this = this.as_local().unwrap();
|
let this = this.as_local_mut().unwrap();
|
||||||
let mut snapshot = this.background_snapshot.lock();
|
let inserted_entry;
|
||||||
entry.is_ignored = snapshot
|
{
|
||||||
.ignore_stack_for_path(&path, entry.is_dir())
|
let mut snapshot = this.background_snapshot.lock();
|
||||||
.is_path_ignored(&path, entry.is_dir());
|
entry.is_ignored = snapshot
|
||||||
if let Some(old_path) = old_path {
|
.ignore_stack_for_path(&path, entry.is_dir())
|
||||||
snapshot.remove_path(&old_path);
|
.is_path_ignored(&path, entry.is_dir());
|
||||||
|
if let Some(old_path) = old_path {
|
||||||
|
snapshot.remove_path(&old_path);
|
||||||
|
}
|
||||||
|
inserted_entry = snapshot.insert_entry(entry, fs.as_ref());
|
||||||
|
snapshot.scan_id += 1;
|
||||||
}
|
}
|
||||||
let entry = snapshot.insert_entry(entry, fs.as_ref());
|
this.poll_snapshot(true, cx);
|
||||||
snapshot.scan_id += 1;
|
Ok(inserted_entry)
|
||||||
let snapshots_tx = this.share.as_ref().map(|s| s.snapshots_tx.clone());
|
})
|
||||||
(entry, snapshot.clone(), snapshots_tx)
|
|
||||||
});
|
|
||||||
this.update(&mut cx, |this, cx| this.poll_snapshot(cx));
|
|
||||||
|
|
||||||
if let Some(snapshots_tx) = snapshots_tx {
|
|
||||||
snapshots_tx.send(snapshot).await.ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(entry)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn share(&mut self, project_id: u64, cx: &mut ModelContext<Worktree>) -> Task<Result<()>> {
|
pub fn share(&mut self, project_id: u64, cx: &mut ModelContext<Worktree>) -> Task<Result<()>> {
|
||||||
let (share_tx, share_rx) = oneshot::channel();
|
let (share_tx, share_rx) = oneshot::channel();
|
||||||
let (snapshots_to_send_tx, snapshots_to_send_rx) =
|
|
||||||
smol::channel::unbounded::<LocalSnapshot>();
|
|
||||||
if self.share.is_some() {
|
if self.share.is_some() {
|
||||||
let _ = share_tx.send(Ok(()));
|
let _ = share_tx.send(Ok(()));
|
||||||
} else {
|
} else {
|
||||||
|
let (snapshots_tx, mut snapshots_rx) = watch::channel_with(self.snapshot());
|
||||||
let rpc = self.client.clone();
|
let rpc = self.client.clone();
|
||||||
let worktree_id = cx.model_id() as u64;
|
let worktree_id = cx.model_id() as u64;
|
||||||
let maintain_remote_snapshot = cx.background().spawn({
|
let maintain_remote_snapshot = cx.background().spawn({
|
||||||
let rpc = rpc.clone();
|
let rpc = rpc.clone();
|
||||||
let diagnostic_summaries = self.diagnostic_summaries.clone();
|
let diagnostic_summaries = self.diagnostic_summaries.clone();
|
||||||
async move {
|
async move {
|
||||||
let mut prev_snapshot = match snapshots_to_send_rx.recv().await {
|
let mut prev_snapshot = match snapshots_rx.recv().await {
|
||||||
Ok(snapshot) => {
|
Some(snapshot) => {
|
||||||
if let Err(error) = rpc
|
let update = proto::UpdateWorktree {
|
||||||
.request(proto::UpdateWorktree {
|
project_id,
|
||||||
project_id,
|
worktree_id,
|
||||||
worktree_id,
|
root_name: snapshot.root_name().to_string(),
|
||||||
root_name: snapshot.root_name().to_string(),
|
updated_entries: snapshot
|
||||||
updated_entries: snapshot
|
.entries_by_path
|
||||||
.entries_by_path
|
.iter()
|
||||||
.iter()
|
.map(Into::into)
|
||||||
.filter(|e| !e.is_ignored)
|
.collect(),
|
||||||
.map(Into::into)
|
removed_entries: Default::default(),
|
||||||
.collect(),
|
scan_id: snapshot.scan_id as u64,
|
||||||
removed_entries: Default::default(),
|
is_last_update: true,
|
||||||
scan_id: snapshot.scan_id as u64,
|
};
|
||||||
})
|
if let Err(error) = send_worktree_update(&rpc, update).await {
|
||||||
.await
|
|
||||||
{
|
|
||||||
let _ = share_tx.send(Err(error));
|
let _ = share_tx.send(Err(error));
|
||||||
return Err(anyhow!("failed to send initial update worktree"));
|
return Err(anyhow!("failed to send initial update worktree"));
|
||||||
} else {
|
} else {
|
||||||
|
@ -934,8 +868,10 @@ impl LocalWorktree {
|
||||||
snapshot
|
snapshot
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(error) => {
|
None => {
|
||||||
let _ = share_tx.send(Err(error.into()));
|
share_tx
|
||||||
|
.send(Err(anyhow!("worktree dropped before share completed")))
|
||||||
|
.ok();
|
||||||
return Err(anyhow!("failed to send initial update worktree"));
|
return Err(anyhow!("failed to send initial update worktree"));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -948,44 +884,12 @@ impl LocalWorktree {
|
||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stream ignored entries in chunks.
|
while let Some(snapshot) = snapshots_rx.recv().await {
|
||||||
{
|
send_worktree_update(
|
||||||
let mut ignored_entries = prev_snapshot
|
&rpc,
|
||||||
.entries_by_path
|
snapshot.build_update(&prev_snapshot, project_id, worktree_id, true),
|
||||||
.iter()
|
)
|
||||||
.filter(|e| e.is_ignored);
|
.await?;
|
||||||
let mut ignored_entries_to_send = Vec::new();
|
|
||||||
loop {
|
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
|
||||||
const CHUNK_SIZE: usize = 2;
|
|
||||||
#[cfg(not(any(test, feature = "test-support")))]
|
|
||||||
const CHUNK_SIZE: usize = 256;
|
|
||||||
|
|
||||||
let entry = ignored_entries.next();
|
|
||||||
if ignored_entries_to_send.len() >= CHUNK_SIZE || entry.is_none() {
|
|
||||||
rpc.request(proto::UpdateWorktree {
|
|
||||||
project_id,
|
|
||||||
worktree_id,
|
|
||||||
root_name: prev_snapshot.root_name().to_string(),
|
|
||||||
updated_entries: mem::take(&mut ignored_entries_to_send),
|
|
||||||
removed_entries: Default::default(),
|
|
||||||
scan_id: prev_snapshot.scan_id as u64,
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(entry) = entry {
|
|
||||||
ignored_entries_to_send.push(entry.into());
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
while let Ok(snapshot) = snapshots_to_send_rx.recv().await {
|
|
||||||
let message =
|
|
||||||
snapshot.build_update(&prev_snapshot, project_id, worktree_id, true);
|
|
||||||
rpc.request(message).await?;
|
|
||||||
prev_snapshot = snapshot;
|
prev_snapshot = snapshot;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -995,18 +899,12 @@ impl LocalWorktree {
|
||||||
});
|
});
|
||||||
self.share = Some(ShareState {
|
self.share = Some(ShareState {
|
||||||
project_id,
|
project_id,
|
||||||
snapshots_tx: snapshots_to_send_tx.clone(),
|
snapshots_tx,
|
||||||
_maintain_remote_snapshot: Some(maintain_remote_snapshot),
|
_maintain_remote_snapshot: Some(maintain_remote_snapshot),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
cx.spawn_weak(|this, cx| async move {
|
cx.foreground().spawn(async move {
|
||||||
if let Some(this) = this.upgrade(&cx) {
|
|
||||||
this.read_with(&cx, |this, _| {
|
|
||||||
let this = this.as_local().unwrap();
|
|
||||||
let _ = snapshots_to_send_tx.try_send(this.snapshot());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
share_rx
|
share_rx
|
||||||
.await
|
.await
|
||||||
.unwrap_or_else(|_| Err(anyhow!("share ended")))
|
.unwrap_or_else(|_| Err(anyhow!("share ended")))
|
||||||
|
@ -1021,23 +919,6 @@ impl LocalWorktree {
|
||||||
self.share.is_some()
|
self.share.is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn broadcast_snapshot(&self) -> impl Future<Output = ()> {
|
|
||||||
let mut to_send = None;
|
|
||||||
if !self.is_scanning() {
|
|
||||||
if let Some(share) = self.share.as_ref() {
|
|
||||||
to_send = Some((self.snapshot(), share.snapshots_tx.clone()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async move {
|
|
||||||
if let Some((snapshot, snapshots_to_send_tx)) = to_send {
|
|
||||||
if let Err(err) = snapshots_to_send_tx.send(snapshot).await {
|
|
||||||
log::error!("error submitting snapshot to send {}", err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn send_extension_counts(&self, project_id: u64) {
|
pub fn send_extension_counts(&self, project_id: u64) {
|
||||||
let mut extensions = Vec::new();
|
let mut extensions = Vec::new();
|
||||||
let mut counts = Vec::new();
|
let mut counts = Vec::new();
|
||||||
|
@ -1063,31 +944,45 @@ impl RemoteWorktree {
|
||||||
self.snapshot.clone()
|
self.snapshot.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn poll_snapshot(&mut self, cx: &mut ModelContext<Worktree>) {
|
||||||
|
self.snapshot = self.background_snapshot.lock().clone();
|
||||||
|
cx.emit(Event::UpdatedEntries);
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
pub fn disconnected_from_host(&mut self) {
|
pub fn disconnected_from_host(&mut self) {
|
||||||
self.updates_tx.take();
|
self.updates_tx.take();
|
||||||
|
self.snapshot_subscriptions.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_from_remote(
|
pub fn update_from_remote(&mut self, update: proto::UpdateWorktree) {
|
||||||
&mut self,
|
|
||||||
envelope: TypedEnvelope<proto::UpdateWorktree>,
|
|
||||||
) -> Result<()> {
|
|
||||||
if let Some(updates_tx) = &self.updates_tx {
|
if let Some(updates_tx) = &self.updates_tx {
|
||||||
updates_tx
|
updates_tx
|
||||||
.unbounded_send(envelope.payload)
|
.unbounded_send(update)
|
||||||
.expect("consumer runs to completion");
|
.expect("consumer runs to completion");
|
||||||
}
|
}
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn wait_for_snapshot(&self, scan_id: usize) -> impl Future<Output = ()> {
|
fn observed_snapshot(&self, scan_id: usize) -> bool {
|
||||||
let mut rx = self.last_scan_id_rx.clone();
|
self.scan_id > scan_id || (self.scan_id == scan_id && self.is_complete)
|
||||||
async move {
|
}
|
||||||
while let Some(applied_scan_id) = rx.next().await {
|
|
||||||
if applied_scan_id >= scan_id {
|
fn wait_for_snapshot(&mut self, scan_id: usize) -> impl Future<Output = ()> {
|
||||||
return;
|
let (tx, rx) = oneshot::channel();
|
||||||
}
|
if self.observed_snapshot(scan_id) {
|
||||||
|
let _ = tx.send(());
|
||||||
|
} else {
|
||||||
|
match self
|
||||||
|
.snapshot_subscriptions
|
||||||
|
.binary_search_by_key(&scan_id, |probe| probe.0)
|
||||||
|
{
|
||||||
|
Ok(ix) | Err(ix) => self.snapshot_subscriptions.insert(ix, (scan_id, tx)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async move {
|
||||||
|
let _ = rx.await;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_diagnostic_summary(
|
pub fn update_diagnostic_summary(
|
||||||
|
@ -1109,7 +1004,7 @@ impl RemoteWorktree {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn insert_entry(
|
pub fn insert_entry(
|
||||||
&self,
|
&mut self,
|
||||||
entry: proto::Entry,
|
entry: proto::Entry,
|
||||||
scan_id: usize,
|
scan_id: usize,
|
||||||
cx: &mut ModelContext<Worktree>,
|
cx: &mut ModelContext<Worktree>,
|
||||||
|
@ -1128,7 +1023,7 @@ impl RemoteWorktree {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn delete_entry(
|
pub(crate) fn delete_entry(
|
||||||
&self,
|
&mut self,
|
||||||
id: ProjectEntryId,
|
id: ProjectEntryId,
|
||||||
scan_id: usize,
|
scan_id: usize,
|
||||||
cx: &mut ModelContext<Worktree>,
|
cx: &mut ModelContext<Worktree>,
|
||||||
|
@ -1204,7 +1099,7 @@ impl Snapshot {
|
||||||
for entry_id in update.removed_entries {
|
for entry_id in update.removed_entries {
|
||||||
let entry = self
|
let entry = self
|
||||||
.entry_for_id(ProjectEntryId::from_proto(entry_id))
|
.entry_for_id(ProjectEntryId::from_proto(entry_id))
|
||||||
.ok_or_else(|| anyhow!("unknown entry"))?;
|
.ok_or_else(|| anyhow!("unknown entry {}", entry_id))?;
|
||||||
entries_by_path_edits.push(Edit::Remove(PathKey(entry.path.clone())));
|
entries_by_path_edits.push(Edit::Remove(PathKey(entry.path.clone())));
|
||||||
entries_by_id_edits.push(Edit::Remove(entry.id));
|
entries_by_id_edits.push(Edit::Remove(entry.id));
|
||||||
}
|
}
|
||||||
|
@ -1226,6 +1121,7 @@ impl Snapshot {
|
||||||
self.entries_by_path.edit(entries_by_path_edits, &());
|
self.entries_by_path.edit(entries_by_path_edits, &());
|
||||||
self.entries_by_id.edit(entries_by_id_edits, &());
|
self.entries_by_id.edit(entries_by_id_edits, &());
|
||||||
self.scan_id = update.scan_id as usize;
|
self.scan_id = update.scan_id as usize;
|
||||||
|
self.is_complete = update.is_last_update;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -1351,27 +1247,16 @@ impl LocalSnapshot {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub(crate) fn to_proto(
|
pub(crate) fn build_initial_update(&self, project_id: u64) -> proto::UpdateWorktree {
|
||||||
&self,
|
|
||||||
diagnostic_summaries: &TreeMap<PathKey, DiagnosticSummary>,
|
|
||||||
visible: bool,
|
|
||||||
) -> proto::Worktree {
|
|
||||||
let root_name = self.root_name.clone();
|
let root_name = self.root_name.clone();
|
||||||
proto::Worktree {
|
proto::UpdateWorktree {
|
||||||
id: self.id.0 as u64,
|
project_id,
|
||||||
|
worktree_id: self.id().to_proto(),
|
||||||
root_name,
|
root_name,
|
||||||
entries: self
|
updated_entries: self.entries_by_path.iter().map(Into::into).collect(),
|
||||||
.entries_by_path
|
removed_entries: Default::default(),
|
||||||
.iter()
|
|
||||||
.filter(|e| !e.is_ignored)
|
|
||||||
.map(Into::into)
|
|
||||||
.collect(),
|
|
||||||
diagnostic_summaries: diagnostic_summaries
|
|
||||||
.iter()
|
|
||||||
.map(|(path, summary)| summary.to_proto(&path.0))
|
|
||||||
.collect(),
|
|
||||||
visible,
|
|
||||||
scan_id: self.scan_id as u64,
|
scan_id: self.scan_id as u64,
|
||||||
|
is_last_update: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1438,6 +1323,7 @@ impl LocalSnapshot {
|
||||||
updated_entries,
|
updated_entries,
|
||||||
removed_entries,
|
removed_entries,
|
||||||
scan_id: self.scan_id as u64,
|
scan_id: self.scan_id as u64,
|
||||||
|
is_last_update: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2109,7 +1995,7 @@ impl BackgroundScanner {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn run(mut self, events_rx: impl Stream<Item = Vec<fsevent::Event>>) {
|
async fn run(mut self, events_rx: impl Stream<Item = Vec<fsevent::Event>>) {
|
||||||
if self.notify.unbounded_send(ScanState::Scanning).is_err() {
|
if self.notify.unbounded_send(ScanState::Initializing).is_err() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2128,8 +2014,13 @@ impl BackgroundScanner {
|
||||||
}
|
}
|
||||||
|
|
||||||
futures::pin_mut!(events_rx);
|
futures::pin_mut!(events_rx);
|
||||||
while let Some(events) = events_rx.next().await {
|
|
||||||
if self.notify.unbounded_send(ScanState::Scanning).is_err() {
|
while let Some(mut events) = events_rx.next().await {
|
||||||
|
while let Poll::Ready(Some(additional_events)) = futures::poll!(events_rx.next()) {
|
||||||
|
events.extend(additional_events);
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.notify.unbounded_send(ScanState::Updating).is_err() {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2781,6 +2672,19 @@ impl<'a> TryFrom<(&'a CharBag, proto::Entry)> for Entry {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn send_worktree_update(client: &Arc<Client>, update: proto::UpdateWorktree) -> Result<()> {
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
const MAX_CHUNK_SIZE: usize = 2;
|
||||||
|
#[cfg(not(any(test, feature = "test-support")))]
|
||||||
|
const MAX_CHUNK_SIZE: usize = 256;
|
||||||
|
|
||||||
|
for update in proto::split_worktree_update(update, MAX_CHUNK_SIZE) {
|
||||||
|
client.request(update).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
@ -2990,6 +2894,7 @@ mod tests {
|
||||||
root_name: Default::default(),
|
root_name: Default::default(),
|
||||||
root_char_bag: Default::default(),
|
root_char_bag: Default::default(),
|
||||||
scan_id: 0,
|
scan_id: 0,
|
||||||
|
is_complete: true,
|
||||||
},
|
},
|
||||||
extension_counts: Default::default(),
|
extension_counts: Default::default(),
|
||||||
};
|
};
|
||||||
|
|
|
@ -108,7 +108,8 @@ actions!(
|
||||||
Cut,
|
Cut,
|
||||||
Paste,
|
Paste,
|
||||||
Delete,
|
Delete,
|
||||||
Rename
|
Rename,
|
||||||
|
Toggle
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
impl_internal_actions!(project_panel, [Open, ToggleExpanded, DeployContextMenu]);
|
impl_internal_actions!(project_panel, [Open, ToggleExpanded, DeployContextMenu]);
|
||||||
|
|
|
@ -172,7 +172,7 @@ message JoinProjectResponse {
|
||||||
|
|
||||||
message Accept {
|
message Accept {
|
||||||
uint32 replica_id = 1;
|
uint32 replica_id = 1;
|
||||||
repeated Worktree worktrees = 2;
|
repeated WorktreeMetadata worktrees = 2;
|
||||||
repeated Collaborator collaborators = 3;
|
repeated Collaborator collaborators = 3;
|
||||||
repeated LanguageServer language_servers = 4;
|
repeated LanguageServer language_servers = 4;
|
||||||
}
|
}
|
||||||
|
@ -199,6 +199,7 @@ message UpdateWorktree {
|
||||||
repeated Entry updated_entries = 4;
|
repeated Entry updated_entries = 4;
|
||||||
repeated uint64 removed_entries = 5;
|
repeated uint64 removed_entries = 5;
|
||||||
uint64 scan_id = 6;
|
uint64 scan_id = 6;
|
||||||
|
bool is_last_update = 7;
|
||||||
}
|
}
|
||||||
|
|
||||||
message UpdateWorktreeExtensions {
|
message UpdateWorktreeExtensions {
|
||||||
|
@ -776,15 +777,6 @@ message User {
|
||||||
string avatar_url = 3;
|
string avatar_url = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
message Worktree {
|
|
||||||
uint64 id = 1;
|
|
||||||
string root_name = 2;
|
|
||||||
repeated Entry entries = 3;
|
|
||||||
repeated DiagnosticSummary diagnostic_summaries = 4;
|
|
||||||
bool visible = 5;
|
|
||||||
uint64 scan_id = 6;
|
|
||||||
}
|
|
||||||
|
|
||||||
message File {
|
message File {
|
||||||
uint64 worktree_id = 1;
|
uint64 worktree_id = 1;
|
||||||
optional uint64 entry_id = 2;
|
optional uint64 entry_id = 2;
|
||||||
|
|
|
@ -5,6 +5,7 @@ use futures::{SinkExt as _, StreamExt as _};
|
||||||
use prost::Message as _;
|
use prost::Message as _;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use std::any::{Any, TypeId};
|
use std::any::{Any, TypeId};
|
||||||
|
use std::{cmp, iter, mem};
|
||||||
use std::{
|
use std::{
|
||||||
fmt::Debug,
|
fmt::Debug,
|
||||||
io,
|
io,
|
||||||
|
@ -392,6 +393,31 @@ impl From<Nonce> for u128 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn split_worktree_update(
|
||||||
|
mut message: UpdateWorktree,
|
||||||
|
max_chunk_size: usize,
|
||||||
|
) -> impl Iterator<Item = UpdateWorktree> {
|
||||||
|
let mut done = false;
|
||||||
|
iter::from_fn(move || {
|
||||||
|
if done {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let chunk_size = cmp::min(message.updated_entries.len(), max_chunk_size);
|
||||||
|
let updated_entries = message.updated_entries.drain(..chunk_size).collect();
|
||||||
|
done = message.updated_entries.is_empty();
|
||||||
|
Some(UpdateWorktree {
|
||||||
|
project_id: message.project_id,
|
||||||
|
worktree_id: message.worktree_id,
|
||||||
|
root_name: message.root_name.clone(),
|
||||||
|
updated_entries,
|
||||||
|
removed_entries: mem::take(&mut message.removed_entries),
|
||||||
|
scan_id: message.scan_id,
|
||||||
|
is_last_update: done && message.is_last_update,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
|
@ -58,7 +58,7 @@ fn add_toggle_option_action<A: Action>(option: SearchOption, cx: &mut MutableApp
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct BufferSearchBar {
|
pub struct BufferSearchBar {
|
||||||
query_editor: ViewHandle<Editor>,
|
pub query_editor: ViewHandle<Editor>,
|
||||||
active_editor: Option<ViewHandle<Editor>>,
|
active_editor: Option<ViewHandle<Editor>>,
|
||||||
active_match_index: Option<usize>,
|
active_match_index: Option<usize>,
|
||||||
active_editor_subscription: Option<Subscription>,
|
active_editor_subscription: Option<Subscription>,
|
||||||
|
|
27
crates/terminal/Cargo.toml
Normal file
27
crates/terminal/Cargo.toml
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
[package]
|
||||||
|
name = "terminal"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
path = "src/terminal.rs"
|
||||||
|
doctest = false
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
alacritty_terminal = "0.16.1"
|
||||||
|
editor = { path = "../editor" }
|
||||||
|
util = { path = "../util" }
|
||||||
|
gpui = { path = "../gpui" }
|
||||||
|
theme = { path = "../theme" }
|
||||||
|
settings = { path = "../settings" }
|
||||||
|
workspace = { path = "../workspace" }
|
||||||
|
project = { path = "../project" }
|
||||||
|
smallvec = { version = "1.6", features = ["union"] }
|
||||||
|
mio-extras = "2.0.6"
|
||||||
|
futures = "0.3"
|
||||||
|
ordered-float = "2.1.1"
|
||||||
|
itertools = "0.10"
|
||||||
|
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
gpui = { path = "../gpui", features = ["test-support"] }
|
96
crates/terminal/print256color.sh
Executable file
96
crates/terminal/print256color.sh
Executable file
|
@ -0,0 +1,96 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Tom Hale, 2016. MIT Licence.
|
||||||
|
# Print out 256 colours, with each number printed in its corresponding colour
|
||||||
|
# See http://askubuntu.com/questions/821157/print-a-256-color-test-pattern-in-the-terminal/821163#821163
|
||||||
|
|
||||||
|
set -eu # Fail on errors or undeclared variables
|
||||||
|
|
||||||
|
printable_colours=256
|
||||||
|
|
||||||
|
# Return a colour that contrasts with the given colour
|
||||||
|
# Bash only does integer division, so keep it integral
|
||||||
|
function contrast_colour {
|
||||||
|
local r g b luminance
|
||||||
|
colour="$1"
|
||||||
|
|
||||||
|
if (( colour < 16 )); then # Initial 16 ANSI colours
|
||||||
|
(( colour == 0 )) && printf "15" || printf "0"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Greyscale # rgb_R = rgb_G = rgb_B = (number - 232) * 10 + 8
|
||||||
|
if (( colour > 231 )); then # Greyscale ramp
|
||||||
|
(( colour < 244 )) && printf "15" || printf "0"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
# All other colours:
|
||||||
|
# 6x6x6 colour cube = 16 + 36*R + 6*G + B # Where RGB are [0..5]
|
||||||
|
# See http://stackoverflow.com/a/27165165/5353461
|
||||||
|
|
||||||
|
# r=$(( (colour-16) / 36 ))
|
||||||
|
g=$(( ((colour-16) % 36) / 6 ))
|
||||||
|
# b=$(( (colour-16) % 6 ))
|
||||||
|
|
||||||
|
# If luminance is bright, print number in black, white otherwise.
|
||||||
|
# Green contributes 587/1000 to human perceived luminance - ITU R-REC-BT.601
|
||||||
|
(( g > 2)) && printf "0" || printf "15"
|
||||||
|
return
|
||||||
|
|
||||||
|
# Uncomment the below for more precise luminance calculations
|
||||||
|
|
||||||
|
# # Calculate percieved brightness
|
||||||
|
# # See https://www.w3.org/TR/AERT#color-contrast
|
||||||
|
# # and http://www.itu.int/rec/R-REC-BT.601
|
||||||
|
# # Luminance is in range 0..5000 as each value is 0..5
|
||||||
|
# luminance=$(( (r * 299) + (g * 587) + (b * 114) ))
|
||||||
|
# (( $luminance > 2500 )) && printf "0" || printf "15"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Print a coloured block with the number of that colour
|
||||||
|
function print_colour {
|
||||||
|
local colour="$1" contrast
|
||||||
|
contrast=$(contrast_colour "$1")
|
||||||
|
printf "\e[48;5;%sm" "$colour" # Start block of colour
|
||||||
|
printf "\e[38;5;%sm%3d" "$contrast" "$colour" # In contrast, print number
|
||||||
|
printf "\e[0m " # Reset colour
|
||||||
|
}
|
||||||
|
|
||||||
|
# Starting at $1, print a run of $2 colours
|
||||||
|
function print_run {
|
||||||
|
local i
|
||||||
|
for (( i = "$1"; i < "$1" + "$2" && i < printable_colours; i++ )) do
|
||||||
|
print_colour "$i"
|
||||||
|
done
|
||||||
|
printf " "
|
||||||
|
}
|
||||||
|
|
||||||
|
# Print blocks of colours
|
||||||
|
function print_blocks {
|
||||||
|
local start="$1" i
|
||||||
|
local end="$2" # inclusive
|
||||||
|
local block_cols="$3"
|
||||||
|
local block_rows="$4"
|
||||||
|
local blocks_per_line="$5"
|
||||||
|
local block_length=$((block_cols * block_rows))
|
||||||
|
|
||||||
|
# Print sets of blocks
|
||||||
|
for (( i = start; i <= end; i += (blocks_per_line-1) * block_length )) do
|
||||||
|
printf "\n" # Space before each set of blocks
|
||||||
|
# For each block row
|
||||||
|
for (( row = 0; row < block_rows; row++ )) do
|
||||||
|
# Print block columns for all blocks on the line
|
||||||
|
for (( block = 0; block < blocks_per_line; block++ )) do
|
||||||
|
print_run $(( i + (block * block_length) )) "$block_cols"
|
||||||
|
done
|
||||||
|
(( i += block_cols )) # Prepare to print the next row
|
||||||
|
printf "\n"
|
||||||
|
done
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
print_run 0 16 # The first 16 colours are spread over the whole spectrum
|
||||||
|
printf "\n"
|
||||||
|
print_blocks 16 231 6 6 3 # 6x6x6 colour cube between 16 and 231 inclusive
|
||||||
|
print_blocks 232 255 12 2 1 # Not 50, but 24 Shades of Grey
|
494
crates/terminal/src/terminal.rs
Normal file
494
crates/terminal/src/terminal.rs
Normal file
|
@ -0,0 +1,494 @@
|
||||||
|
use alacritty_terminal::{
|
||||||
|
config::{Config, Program, PtyConfig},
|
||||||
|
event::{Event as AlacTermEvent, EventListener, Notify},
|
||||||
|
event_loop::{EventLoop, Msg, Notifier},
|
||||||
|
grid::Scroll,
|
||||||
|
sync::FairMutex,
|
||||||
|
term::{color::Rgb as AlacRgb, SizeInfo},
|
||||||
|
tty, Term,
|
||||||
|
};
|
||||||
|
|
||||||
|
use futures::{
|
||||||
|
channel::mpsc::{unbounded, UnboundedSender},
|
||||||
|
StreamExt,
|
||||||
|
};
|
||||||
|
use gpui::{
|
||||||
|
actions, color::Color, elements::*, impl_internal_actions, platform::CursorStyle,
|
||||||
|
ClipboardItem, Entity, MutableAppContext, View, ViewContext,
|
||||||
|
};
|
||||||
|
use project::{Project, ProjectPath};
|
||||||
|
use settings::Settings;
|
||||||
|
use smallvec::SmallVec;
|
||||||
|
use std::{path::PathBuf, sync::Arc};
|
||||||
|
use workspace::{Item, Workspace};
|
||||||
|
|
||||||
|
use crate::terminal_element::{get_color_at_index, TerminalEl};
|
||||||
|
|
||||||
|
//ASCII Control characters on a keyboard
|
||||||
|
const ETX_CHAR: char = 3_u8 as char; //'End of text', the control code for 'ctrl-c'
|
||||||
|
const TAB_CHAR: char = 9_u8 as char;
|
||||||
|
const CARRIAGE_RETURN_CHAR: char = 13_u8 as char;
|
||||||
|
const ESC_CHAR: char = 27_u8 as char;
|
||||||
|
const DEL_CHAR: char = 127_u8 as char;
|
||||||
|
const LEFT_SEQ: &str = "\x1b[D";
|
||||||
|
const RIGHT_SEQ: &str = "\x1b[C";
|
||||||
|
const UP_SEQ: &str = "\x1b[A";
|
||||||
|
const DOWN_SEQ: &str = "\x1b[B";
|
||||||
|
const DEFAULT_TITLE: &str = "Terminal";
|
||||||
|
|
||||||
|
pub mod terminal_element;
|
||||||
|
|
||||||
|
///Action for carrying the input to the PTY
|
||||||
|
#[derive(Clone, Default, Debug, PartialEq, Eq)]
|
||||||
|
pub struct Input(pub String);
|
||||||
|
|
||||||
|
///Event to transmit the scroll from the element to the view
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub struct ScrollTerminal(pub i32);
|
||||||
|
|
||||||
|
actions!(
|
||||||
|
terminal,
|
||||||
|
[Sigint, Escape, Del, Return, Left, Right, Up, Down, Tab, Clear, Paste, Deploy, Quit]
|
||||||
|
);
|
||||||
|
impl_internal_actions!(terminal, [Input, ScrollTerminal]);
|
||||||
|
|
||||||
|
///Initialize and register all of our action handlers
|
||||||
|
pub fn init(cx: &mut MutableAppContext) {
|
||||||
|
cx.add_action(Terminal::deploy);
|
||||||
|
cx.add_action(Terminal::write_to_pty);
|
||||||
|
cx.add_action(Terminal::send_sigint);
|
||||||
|
cx.add_action(Terminal::escape);
|
||||||
|
cx.add_action(Terminal::quit);
|
||||||
|
cx.add_action(Terminal::del);
|
||||||
|
cx.add_action(Terminal::carriage_return); //TODO figure out how to do this properly. Should we be checking the terminal mode?
|
||||||
|
cx.add_action(Terminal::left);
|
||||||
|
cx.add_action(Terminal::right);
|
||||||
|
cx.add_action(Terminal::up);
|
||||||
|
cx.add_action(Terminal::down);
|
||||||
|
cx.add_action(Terminal::tab);
|
||||||
|
cx.add_action(Terminal::paste);
|
||||||
|
cx.add_action(Terminal::scroll_terminal);
|
||||||
|
}
|
||||||
|
|
||||||
|
///A translation struct for Alacritty to communicate with us from their event loop
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct ZedListener(UnboundedSender<AlacTermEvent>);
|
||||||
|
|
||||||
|
impl EventListener for ZedListener {
|
||||||
|
fn send_event(&self, event: AlacTermEvent) {
|
||||||
|
self.0.unbounded_send(event).ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
///A terminal view, maintains the PTY's file handles and communicates with the terminal
|
||||||
|
pub struct Terminal {
|
||||||
|
pty_tx: Notifier,
|
||||||
|
term: Arc<FairMutex<Term<ZedListener>>>,
|
||||||
|
title: String,
|
||||||
|
has_new_content: bool,
|
||||||
|
has_bell: bool, //Currently using iTerm bell, show bell emoji in tab until input is received
|
||||||
|
cur_size: SizeInfo,
|
||||||
|
}
|
||||||
|
|
||||||
|
///Upward flowing events, for changing the title and such
|
||||||
|
pub enum Event {
|
||||||
|
TitleChanged,
|
||||||
|
CloseTerminal,
|
||||||
|
Activate,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Entity for Terminal {
|
||||||
|
type Event = Event;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Terminal {
|
||||||
|
///Create a new Terminal view. This spawns a task, a thread, and opens the TTY devices
|
||||||
|
fn new(cx: &mut ViewContext<Self>, working_directory: Option<PathBuf>) -> Self {
|
||||||
|
//Spawn a task so the Alacritty EventLoop can communicate with us in a view context
|
||||||
|
let (events_tx, mut events_rx) = unbounded();
|
||||||
|
cx.spawn_weak(|this, mut cx| async move {
|
||||||
|
while let Some(event) = events_rx.next().await {
|
||||||
|
match this.upgrade(&cx) {
|
||||||
|
Some(handle) => {
|
||||||
|
handle.update(&mut cx, |this, cx| {
|
||||||
|
this.process_terminal_event(event, cx);
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
None => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
|
||||||
|
let pty_config = PtyConfig {
|
||||||
|
shell: Some(Program::Just("zsh".to_string())),
|
||||||
|
working_directory,
|
||||||
|
hold: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
let config = Config {
|
||||||
|
pty_config: pty_config.clone(),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
//The details here don't matter, the terminal will be resized on the first layout
|
||||||
|
//Set to something small for easier debugging
|
||||||
|
let size_info = SizeInfo::new(200., 100.0, 5., 5., 0., 0., false);
|
||||||
|
|
||||||
|
//Set up the terminal...
|
||||||
|
let term = Term::new(&config, size_info, ZedListener(events_tx.clone()));
|
||||||
|
let term = Arc::new(FairMutex::new(term));
|
||||||
|
|
||||||
|
//Setup the pty...
|
||||||
|
let pty = tty::new(&pty_config, &size_info, None).expect("Could not create tty");
|
||||||
|
|
||||||
|
//And connect them together
|
||||||
|
let event_loop = EventLoop::new(
|
||||||
|
term.clone(),
|
||||||
|
ZedListener(events_tx.clone()),
|
||||||
|
pty,
|
||||||
|
pty_config.hold,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
|
//Kick things off
|
||||||
|
let pty_tx = Notifier(event_loop.channel());
|
||||||
|
let _io_thread = event_loop.spawn();
|
||||||
|
Terminal {
|
||||||
|
title: DEFAULT_TITLE.to_string(),
|
||||||
|
term,
|
||||||
|
pty_tx,
|
||||||
|
has_new_content: false,
|
||||||
|
has_bell: false,
|
||||||
|
cur_size: size_info,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
///Takes events from Alacritty and translates them to behavior on this view
|
||||||
|
fn process_terminal_event(
|
||||||
|
&mut self,
|
||||||
|
event: alacritty_terminal::event::Event,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) {
|
||||||
|
match event {
|
||||||
|
AlacTermEvent::Wakeup => {
|
||||||
|
if !cx.is_self_focused() {
|
||||||
|
self.has_new_content = true; //Change tab content
|
||||||
|
cx.emit(Event::TitleChanged);
|
||||||
|
} else {
|
||||||
|
cx.notify()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AlacTermEvent::PtyWrite(out) => self.write_to_pty(&Input(out), cx),
|
||||||
|
AlacTermEvent::MouseCursorDirty => {
|
||||||
|
//Calculate new cursor style.
|
||||||
|
//TODO
|
||||||
|
//Check on correctly handling mouse events for terminals
|
||||||
|
cx.platform().set_cursor_style(CursorStyle::Arrow); //???
|
||||||
|
}
|
||||||
|
AlacTermEvent::Title(title) => {
|
||||||
|
self.title = title;
|
||||||
|
cx.emit(Event::TitleChanged);
|
||||||
|
}
|
||||||
|
AlacTermEvent::ResetTitle => {
|
||||||
|
self.title = DEFAULT_TITLE.to_string();
|
||||||
|
cx.emit(Event::TitleChanged);
|
||||||
|
}
|
||||||
|
AlacTermEvent::ClipboardStore(_, data) => {
|
||||||
|
cx.write_to_clipboard(ClipboardItem::new(data))
|
||||||
|
}
|
||||||
|
AlacTermEvent::ClipboardLoad(_, format) => self.write_to_pty(
|
||||||
|
&Input(format(
|
||||||
|
&cx.read_from_clipboard()
|
||||||
|
.map(|ci| ci.text().to_string())
|
||||||
|
.unwrap_or("".to_string()),
|
||||||
|
)),
|
||||||
|
cx,
|
||||||
|
),
|
||||||
|
AlacTermEvent::ColorRequest(index, format) => {
|
||||||
|
let color = self.term.lock().colors()[index].unwrap_or_else(|| {
|
||||||
|
let term_style = &cx.global::<Settings>().theme.terminal;
|
||||||
|
match index {
|
||||||
|
0..=255 => to_alac_rgb(get_color_at_index(&(index as u8), term_style)),
|
||||||
|
//These additional values are required to match the Alacritty Colors object's behavior
|
||||||
|
256 => to_alac_rgb(term_style.foreground),
|
||||||
|
257 => to_alac_rgb(term_style.background),
|
||||||
|
258 => to_alac_rgb(term_style.cursor),
|
||||||
|
259 => to_alac_rgb(term_style.dim_black),
|
||||||
|
260 => to_alac_rgb(term_style.dim_red),
|
||||||
|
261 => to_alac_rgb(term_style.dim_green),
|
||||||
|
262 => to_alac_rgb(term_style.dim_yellow),
|
||||||
|
263 => to_alac_rgb(term_style.dim_blue),
|
||||||
|
264 => to_alac_rgb(term_style.dim_magenta),
|
||||||
|
265 => to_alac_rgb(term_style.dim_cyan),
|
||||||
|
266 => to_alac_rgb(term_style.dim_white),
|
||||||
|
267 => to_alac_rgb(term_style.bright_foreground),
|
||||||
|
268 => to_alac_rgb(term_style.black), //Dim Background, non-standard
|
||||||
|
_ => AlacRgb { r: 0, g: 0, b: 0 },
|
||||||
|
}
|
||||||
|
});
|
||||||
|
self.write_to_pty(&Input(format(color)), cx)
|
||||||
|
}
|
||||||
|
AlacTermEvent::CursorBlinkingChange => {
|
||||||
|
//TODO: Set a timer to blink the cursor on and off
|
||||||
|
}
|
||||||
|
AlacTermEvent::Bell => {
|
||||||
|
self.has_bell = true;
|
||||||
|
cx.emit(Event::TitleChanged);
|
||||||
|
}
|
||||||
|
AlacTermEvent::Exit => self.quit(&Quit, cx),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
///Resize the terminal and the PTY. This locks the terminal.
|
||||||
|
fn set_size(&mut self, new_size: SizeInfo) {
|
||||||
|
if new_size != self.cur_size {
|
||||||
|
self.pty_tx.0.send(Msg::Resize(new_size)).ok();
|
||||||
|
self.term.lock().resize(new_size);
|
||||||
|
self.cur_size = new_size;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
///Scroll the terminal. This locks the terminal
|
||||||
|
fn scroll_terminal(&mut self, scroll: &ScrollTerminal, _: &mut ViewContext<Self>) {
|
||||||
|
self.term.lock().scroll_display(Scroll::Delta(scroll.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
///Create a new Terminal in the current working directory or the user's home directory
|
||||||
|
fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext<Workspace>) {
|
||||||
|
let project = workspace.project().read(cx);
|
||||||
|
let abs_path = project
|
||||||
|
.active_entry()
|
||||||
|
.and_then(|entry_id| project.worktree_for_entry(entry_id, cx))
|
||||||
|
.and_then(|worktree_handle| worktree_handle.read(cx).as_local())
|
||||||
|
.map(|wt| wt.abs_path().to_path_buf());
|
||||||
|
|
||||||
|
workspace.add_item(Box::new(cx.add_view(|cx| Terminal::new(cx, abs_path))), cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
///Send the shutdown message to Alacritty
|
||||||
|
fn shutdown_pty(&mut self) {
|
||||||
|
self.pty_tx.0.send(Msg::Shutdown).ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
///Tell Zed to close us
|
||||||
|
fn quit(&mut self, _: &Quit, cx: &mut ViewContext<Self>) {
|
||||||
|
cx.emit(Event::CloseTerminal);
|
||||||
|
}
|
||||||
|
|
||||||
|
///Attempt to paste the clipboard into the terminal
|
||||||
|
fn paste(&mut self, _: &Paste, cx: &mut ViewContext<Self>) {
|
||||||
|
if let Some(item) = cx.read_from_clipboard() {
|
||||||
|
self.write_to_pty(&Input(item.text().to_owned()), cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
///Write the Input payload to the tty. This locks the terminal so we can scroll it.
|
||||||
|
fn write_to_pty(&mut self, input: &Input, cx: &mut ViewContext<Self>) {
|
||||||
|
//iTerm bell behavior, bell stays until terminal is interacted with
|
||||||
|
self.has_bell = false;
|
||||||
|
self.term.lock().scroll_display(Scroll::Bottom);
|
||||||
|
cx.emit(Event::TitleChanged);
|
||||||
|
self.pty_tx.notify(input.0.clone().into_bytes());
|
||||||
|
}
|
||||||
|
|
||||||
|
///Send the `up` key
|
||||||
|
fn up(&mut self, _: &Up, cx: &mut ViewContext<Self>) {
|
||||||
|
self.write_to_pty(&Input(UP_SEQ.to_string()), cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
///Send the `down` key
|
||||||
|
fn down(&mut self, _: &Down, cx: &mut ViewContext<Self>) {
|
||||||
|
self.write_to_pty(&Input(DOWN_SEQ.to_string()), cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
///Send the `tab` key
|
||||||
|
fn tab(&mut self, _: &Tab, cx: &mut ViewContext<Self>) {
|
||||||
|
self.write_to_pty(&Input(TAB_CHAR.to_string()), cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
///Send `SIGINT` (`ctrl-c`)
|
||||||
|
fn send_sigint(&mut self, _: &Sigint, cx: &mut ViewContext<Self>) {
|
||||||
|
self.write_to_pty(&Input(ETX_CHAR.to_string()), cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
///Send the `escape` key
|
||||||
|
fn escape(&mut self, _: &Escape, cx: &mut ViewContext<Self>) {
|
||||||
|
self.write_to_pty(&Input(ESC_CHAR.to_string()), cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
///Send the `delete` key. TODO: Difference between this and backspace?
|
||||||
|
fn del(&mut self, _: &Del, cx: &mut ViewContext<Self>) {
|
||||||
|
self.write_to_pty(&Input(DEL_CHAR.to_string()), cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
///Send a carriage return. TODO: May need to check the terminal mode.
|
||||||
|
fn carriage_return(&mut self, _: &Return, cx: &mut ViewContext<Self>) {
|
||||||
|
self.write_to_pty(&Input(CARRIAGE_RETURN_CHAR.to_string()), cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
//Send the `left` key
|
||||||
|
fn left(&mut self, _: &Left, cx: &mut ViewContext<Self>) {
|
||||||
|
self.write_to_pty(&Input(LEFT_SEQ.to_string()), cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
//Send the `right` key
|
||||||
|
fn right(&mut self, _: &Right, cx: &mut ViewContext<Self>) {
|
||||||
|
self.write_to_pty(&Input(RIGHT_SEQ.to_string()), cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for Terminal {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
self.shutdown_pty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl View for Terminal {
|
||||||
|
fn ui_name() -> &'static str {
|
||||||
|
"Terminal"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> ElementBox {
|
||||||
|
TerminalEl::new(cx.handle()).contained().boxed()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
|
||||||
|
cx.emit(Event::Activate);
|
||||||
|
self.has_new_content = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Item for Terminal {
|
||||||
|
fn tab_content(&self, tab_theme: &theme::Tab, cx: &gpui::AppContext) -> ElementBox {
|
||||||
|
let settings = cx.global::<Settings>();
|
||||||
|
let search_theme = &settings.theme.search; //TODO properly integrate themes
|
||||||
|
|
||||||
|
let mut flex = Flex::row();
|
||||||
|
|
||||||
|
if self.has_bell {
|
||||||
|
flex.add_child(
|
||||||
|
Svg::new("icons/zap.svg") //TODO: Swap out for a better icon, or at least resize this
|
||||||
|
.with_color(tab_theme.label.text.color)
|
||||||
|
.constrained()
|
||||||
|
.with_width(search_theme.tab_icon_width)
|
||||||
|
.aligned()
|
||||||
|
.boxed(),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
flex.with_child(
|
||||||
|
Label::new(self.title.clone(), tab_theme.label.clone())
|
||||||
|
.aligned()
|
||||||
|
.contained()
|
||||||
|
.with_margin_left(if self.has_bell {
|
||||||
|
search_theme.tab_icon_spacing
|
||||||
|
} else {
|
||||||
|
0.
|
||||||
|
})
|
||||||
|
.boxed(),
|
||||||
|
)
|
||||||
|
.boxed()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn project_path(&self, _cx: &gpui::AppContext) -> Option<ProjectPath> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn project_entry_ids(&self, _cx: &gpui::AppContext) -> SmallVec<[project::ProjectEntryId; 3]> {
|
||||||
|
SmallVec::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_singleton(&self, _cx: &gpui::AppContext) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_nav_history(&mut self, _: workspace::ItemNavHistory, _: &mut ViewContext<Self>) {}
|
||||||
|
|
||||||
|
fn can_save(&self, _cx: &gpui::AppContext) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save(
|
||||||
|
&mut self,
|
||||||
|
_project: gpui::ModelHandle<Project>,
|
||||||
|
_cx: &mut ViewContext<Self>,
|
||||||
|
) -> gpui::Task<gpui::anyhow::Result<()>> {
|
||||||
|
unreachable!("save should not have been called");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_as(
|
||||||
|
&mut self,
|
||||||
|
_project: gpui::ModelHandle<Project>,
|
||||||
|
_abs_path: std::path::PathBuf,
|
||||||
|
_cx: &mut ViewContext<Self>,
|
||||||
|
) -> gpui::Task<gpui::anyhow::Result<()>> {
|
||||||
|
unreachable!("save_as should not have been called");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reload(
|
||||||
|
&mut self,
|
||||||
|
_project: gpui::ModelHandle<Project>,
|
||||||
|
_cx: &mut ViewContext<Self>,
|
||||||
|
) -> gpui::Task<gpui::anyhow::Result<()>> {
|
||||||
|
gpui::Task::ready(Ok(()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_dirty(&self, _: &gpui::AppContext) -> bool {
|
||||||
|
self.has_new_content
|
||||||
|
}
|
||||||
|
|
||||||
|
fn should_update_tab_on_event(event: &Self::Event) -> bool {
|
||||||
|
matches!(event, &Event::TitleChanged)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn should_close_item_on_event(event: &Self::Event) -> bool {
|
||||||
|
matches!(event, &Event::CloseTerminal)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn should_activate_item_on_event(event: &Self::Event) -> bool {
|
||||||
|
matches!(event, &Event::Activate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Convenience method for less lines
|
||||||
|
fn to_alac_rgb(color: Color) -> AlacRgb {
|
||||||
|
AlacRgb {
|
||||||
|
r: color.r,
|
||||||
|
g: color.g,
|
||||||
|
b: color.g,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::terminal_element::{build_chunks, BuiltChunks};
|
||||||
|
use gpui::TestAppContext;
|
||||||
|
|
||||||
|
///Basic integration test, can we get the terminal to show up, execute a command,
|
||||||
|
//and produce noticable output?
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_terminal(cx: &mut TestAppContext) {
|
||||||
|
let terminal = cx.add_view(Default::default(), |cx| Terminal::new(cx, None));
|
||||||
|
|
||||||
|
terminal.update(cx, |terminal, cx| {
|
||||||
|
terminal.write_to_pty(&Input(("expr 3 + 4".to_string()).to_string()), cx);
|
||||||
|
terminal.carriage_return(&Return, cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
terminal
|
||||||
|
.condition(cx, |terminal, _cx| {
|
||||||
|
let term = terminal.term.clone();
|
||||||
|
let BuiltChunks { chunks, .. } = build_chunks(
|
||||||
|
term.lock().renderable_content().display_iter,
|
||||||
|
&Default::default(),
|
||||||
|
Default::default(),
|
||||||
|
);
|
||||||
|
let content = chunks.iter().map(|e| e.0.trim()).collect::<String>();
|
||||||
|
content.contains("7")
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
}
|
621
crates/terminal/src/terminal_element.rs
Normal file
621
crates/terminal/src/terminal_element.rs
Normal file
|
@ -0,0 +1,621 @@
|
||||||
|
use alacritty_terminal::{
|
||||||
|
ansi::Color as AnsiColor,
|
||||||
|
grid::{GridIterator, Indexed},
|
||||||
|
index::Point,
|
||||||
|
term::{
|
||||||
|
cell::{Cell, Flags},
|
||||||
|
SizeInfo,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
use editor::{Cursor, CursorShape};
|
||||||
|
use gpui::{
|
||||||
|
color::Color,
|
||||||
|
elements::*,
|
||||||
|
fonts::{HighlightStyle, TextStyle, Underline},
|
||||||
|
geometry::{
|
||||||
|
rect::RectF,
|
||||||
|
vector::{vec2f, Vector2F},
|
||||||
|
},
|
||||||
|
json::json,
|
||||||
|
text_layout::{Line, RunStyle},
|
||||||
|
Event, FontCache, MouseRegion, PaintContext, Quad, SizeConstraint, WeakViewHandle,
|
||||||
|
};
|
||||||
|
use itertools::Itertools;
|
||||||
|
use ordered_float::OrderedFloat;
|
||||||
|
use settings::Settings;
|
||||||
|
use std::{iter, rc::Rc};
|
||||||
|
use theme::TerminalStyle;
|
||||||
|
|
||||||
|
use crate::{Input, ScrollTerminal, Terminal};
|
||||||
|
|
||||||
|
///Scrolling is unbearably sluggish by default. Alacritty supports a configurable
|
||||||
|
///Scroll multiplier that is set to 3 by default. This will be removed when I
|
||||||
|
///Implement scroll bars.
|
||||||
|
const ALACRITTY_SCROLL_MULTIPLIER: f32 = 3.;
|
||||||
|
|
||||||
|
///Used to display the grid as passed to Alacritty and the TTY.
|
||||||
|
///Useful for debugging inconsistencies between behavior and display
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
const DEBUG_GRID: bool = false;
|
||||||
|
|
||||||
|
///The GPUI element that paints the terminal.
|
||||||
|
pub struct TerminalEl {
|
||||||
|
view: WeakViewHandle<Terminal>,
|
||||||
|
}
|
||||||
|
|
||||||
|
///Represents a span of cells in a single line in the terminal's grid.
|
||||||
|
///This is used for drawing background rectangles
|
||||||
|
#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, PartialOrd, Ord)]
|
||||||
|
pub struct RectSpan {
|
||||||
|
start: i32,
|
||||||
|
end: i32,
|
||||||
|
line: usize,
|
||||||
|
color: Color,
|
||||||
|
}
|
||||||
|
|
||||||
|
///A background color span
|
||||||
|
impl RectSpan {
|
||||||
|
///Creates a new LineSpan. `start` must be <= `end`.
|
||||||
|
///If `start` == `end`, then this span is considered to be over a
|
||||||
|
/// single cell
|
||||||
|
fn new(start: i32, end: i32, line: usize, color: Color) -> RectSpan {
|
||||||
|
debug_assert!(start <= end);
|
||||||
|
RectSpan {
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
line,
|
||||||
|
color,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
///Helper types so I don't mix these two up
|
||||||
|
struct CellWidth(f32);
|
||||||
|
struct LineHeight(f32);
|
||||||
|
|
||||||
|
///The information generated during layout that is nescessary for painting
|
||||||
|
pub struct LayoutState {
|
||||||
|
lines: Vec<Line>,
|
||||||
|
line_height: LineHeight,
|
||||||
|
em_width: CellWidth,
|
||||||
|
cursor: Option<Cursor>,
|
||||||
|
cur_size: SizeInfo,
|
||||||
|
background_color: Color,
|
||||||
|
background_rects: Vec<(RectF, Color)>, //Vec index == Line index for the LineSpan
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TerminalEl {
|
||||||
|
pub fn new(view: WeakViewHandle<Terminal>) -> TerminalEl {
|
||||||
|
TerminalEl { view }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Element for TerminalEl {
|
||||||
|
type LayoutState = LayoutState;
|
||||||
|
type PaintState = ();
|
||||||
|
|
||||||
|
fn layout(
|
||||||
|
&mut self,
|
||||||
|
constraint: gpui::SizeConstraint,
|
||||||
|
cx: &mut gpui::LayoutContext,
|
||||||
|
) -> (gpui::geometry::vector::Vector2F, Self::LayoutState) {
|
||||||
|
//Settings immutably borrows cx here for the settings and font cache
|
||||||
|
//and we need to modify the cx to resize the terminal. So instead of
|
||||||
|
//storing Settings or the font_cache(), we toss them ASAP and then reborrow later
|
||||||
|
let text_style = make_text_style(cx.font_cache(), cx.global::<Settings>());
|
||||||
|
let line_height = LineHeight(cx.font_cache().line_height(text_style.font_size));
|
||||||
|
let cell_width = CellWidth(
|
||||||
|
cx.font_cache()
|
||||||
|
.em_advance(text_style.font_id, text_style.font_size),
|
||||||
|
);
|
||||||
|
let view_handle = self.view.upgrade(cx).unwrap();
|
||||||
|
|
||||||
|
//Tell the view our new size. Requires a mutable borrow of cx and the view
|
||||||
|
let cur_size = make_new_size(constraint, &cell_width, &line_height);
|
||||||
|
//Note that set_size locks and mutates the terminal.
|
||||||
|
//TODO: Would be nice to lock once for the whole of layout
|
||||||
|
view_handle.update(cx.app, |view, _cx| view.set_size(cur_size));
|
||||||
|
|
||||||
|
//Now that we're done with the mutable portion, grab the immutable settings and view again
|
||||||
|
let terminal_theme = &(cx.global::<Settings>()).theme.terminal;
|
||||||
|
let term = view_handle.read(cx).term.lock();
|
||||||
|
|
||||||
|
let grid = term.grid();
|
||||||
|
let cursor_point = grid.cursor.point;
|
||||||
|
let cursor_text = grid[cursor_point.line][cursor_point.column].c.to_string();
|
||||||
|
|
||||||
|
let content = term.renderable_content();
|
||||||
|
|
||||||
|
//And we're off! Begin layouting
|
||||||
|
let BuiltChunks {
|
||||||
|
chunks,
|
||||||
|
line_count,
|
||||||
|
cursor_index,
|
||||||
|
} = build_chunks(content.display_iter, &terminal_theme, cursor_point);
|
||||||
|
|
||||||
|
let shaped_lines = layout_highlighted_chunks(
|
||||||
|
chunks
|
||||||
|
.iter()
|
||||||
|
.map(|(text, style, _)| (text.as_str(), *style)),
|
||||||
|
&text_style,
|
||||||
|
cx.text_layout_cache,
|
||||||
|
cx.font_cache(),
|
||||||
|
usize::MAX,
|
||||||
|
line_count,
|
||||||
|
);
|
||||||
|
|
||||||
|
let backgrounds = chunks
|
||||||
|
.iter()
|
||||||
|
.filter(|(_, _, line_span)| line_span != &RectSpan::default())
|
||||||
|
.map(|(_, _, line_span)| *line_span)
|
||||||
|
.collect();
|
||||||
|
let background_rects = make_background_rects(backgrounds, &shaped_lines, &line_height);
|
||||||
|
|
||||||
|
let block_text = cx.text_layout_cache.layout_str(
|
||||||
|
&cursor_text,
|
||||||
|
text_style.font_size,
|
||||||
|
&[(
|
||||||
|
cursor_text.len(),
|
||||||
|
RunStyle {
|
||||||
|
font_id: text_style.font_id,
|
||||||
|
color: terminal_theme.background,
|
||||||
|
underline: Default::default(),
|
||||||
|
},
|
||||||
|
)],
|
||||||
|
);
|
||||||
|
|
||||||
|
let cursor = get_cursor_position(
|
||||||
|
content.cursor.point.line.0 as usize,
|
||||||
|
cursor_index,
|
||||||
|
&shaped_lines,
|
||||||
|
content.display_offset,
|
||||||
|
&line_height,
|
||||||
|
)
|
||||||
|
.map(move |(cursor_position, block_width)| {
|
||||||
|
let block_width = if block_width != 0.0 {
|
||||||
|
block_width
|
||||||
|
} else {
|
||||||
|
cell_width.0
|
||||||
|
};
|
||||||
|
|
||||||
|
Cursor::new(
|
||||||
|
cursor_position,
|
||||||
|
block_width,
|
||||||
|
line_height.0,
|
||||||
|
terminal_theme.cursor,
|
||||||
|
CursorShape::Block,
|
||||||
|
Some(block_text.clone()),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
(
|
||||||
|
constraint.max,
|
||||||
|
LayoutState {
|
||||||
|
lines: shaped_lines,
|
||||||
|
line_height,
|
||||||
|
em_width: cell_width,
|
||||||
|
cursor,
|
||||||
|
cur_size,
|
||||||
|
background_rects,
|
||||||
|
background_color: terminal_theme.background,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn paint(
|
||||||
|
&mut self,
|
||||||
|
bounds: gpui::geometry::rect::RectF,
|
||||||
|
visible_bounds: gpui::geometry::rect::RectF,
|
||||||
|
layout: &mut Self::LayoutState,
|
||||||
|
cx: &mut gpui::PaintContext,
|
||||||
|
) -> Self::PaintState {
|
||||||
|
//Setup element stuff
|
||||||
|
cx.scene.push_layer(Some(visible_bounds));
|
||||||
|
|
||||||
|
//Elements are ephemeral, only at paint time do we know what could be clicked by a mouse
|
||||||
|
cx.scene.push_mouse_region(MouseRegion {
|
||||||
|
view_id: self.view.id(),
|
||||||
|
mouse_down: Some(Rc::new(|_, cx| cx.focus_parent_view())),
|
||||||
|
bounds: visible_bounds,
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
|
||||||
|
let origin = bounds.origin() + vec2f(layout.em_width.0, 0.);
|
||||||
|
|
||||||
|
//Start us off with a nice simple background color
|
||||||
|
cx.scene.push_layer(Some(visible_bounds));
|
||||||
|
cx.scene.push_quad(Quad {
|
||||||
|
bounds: RectF::new(bounds.origin(), bounds.size()),
|
||||||
|
background: Some(layout.background_color),
|
||||||
|
border: Default::default(),
|
||||||
|
corner_radius: 0.,
|
||||||
|
});
|
||||||
|
|
||||||
|
//Draw cell backgrounds
|
||||||
|
for background_rect in &layout.background_rects {
|
||||||
|
let new_origin = origin + background_rect.0.origin();
|
||||||
|
cx.scene.push_quad(Quad {
|
||||||
|
bounds: RectF::new(new_origin, background_rect.0.size()),
|
||||||
|
background: Some(background_rect.1),
|
||||||
|
border: Default::default(),
|
||||||
|
corner_radius: 0.,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
cx.scene.pop_layer();
|
||||||
|
|
||||||
|
//Draw text
|
||||||
|
cx.scene.push_layer(Some(visible_bounds));
|
||||||
|
let mut line_origin = origin.clone();
|
||||||
|
for line in &layout.lines {
|
||||||
|
let boundaries = RectF::new(line_origin, vec2f(bounds.width(), layout.line_height.0));
|
||||||
|
if boundaries.intersects(visible_bounds) {
|
||||||
|
line.paint(line_origin, visible_bounds, layout.line_height.0, cx);
|
||||||
|
}
|
||||||
|
line_origin.set_y(boundaries.max_y());
|
||||||
|
}
|
||||||
|
cx.scene.pop_layer();
|
||||||
|
|
||||||
|
//Draw cursor
|
||||||
|
if let Some(cursor) = &layout.cursor {
|
||||||
|
cx.scene.push_layer(Some(visible_bounds));
|
||||||
|
cursor.paint(origin, cx);
|
||||||
|
cx.scene.pop_layer();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
if DEBUG_GRID {
|
||||||
|
draw_debug_grid(bounds, layout, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
cx.scene.pop_layer();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dispatch_event(
|
||||||
|
&mut self,
|
||||||
|
event: &gpui::Event,
|
||||||
|
_bounds: gpui::geometry::rect::RectF,
|
||||||
|
visible_bounds: gpui::geometry::rect::RectF,
|
||||||
|
layout: &mut Self::LayoutState,
|
||||||
|
_paint: &mut Self::PaintState,
|
||||||
|
cx: &mut gpui::EventContext,
|
||||||
|
) -> bool {
|
||||||
|
match event {
|
||||||
|
Event::ScrollWheel {
|
||||||
|
delta, position, ..
|
||||||
|
} => visible_bounds
|
||||||
|
.contains_point(*position)
|
||||||
|
.then(|| {
|
||||||
|
let vertical_scroll =
|
||||||
|
(delta.y() / layout.line_height.0) * ALACRITTY_SCROLL_MULTIPLIER;
|
||||||
|
cx.dispatch_action(ScrollTerminal(vertical_scroll.round() as i32));
|
||||||
|
})
|
||||||
|
.is_some(),
|
||||||
|
Event::KeyDown {
|
||||||
|
input: Some(input), ..
|
||||||
|
} => cx
|
||||||
|
.is_parent_view_focused()
|
||||||
|
.then(|| {
|
||||||
|
cx.dispatch_action(Input(input.to_string()));
|
||||||
|
})
|
||||||
|
.is_some(),
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn debug(
|
||||||
|
&self,
|
||||||
|
_bounds: gpui::geometry::rect::RectF,
|
||||||
|
_layout: &Self::LayoutState,
|
||||||
|
_paint: &Self::PaintState,
|
||||||
|
_cx: &gpui::DebugContext,
|
||||||
|
) -> gpui::serde_json::Value {
|
||||||
|
json!({
|
||||||
|
"type": "TerminalElement",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
///Configures a text style from the current settings.
|
||||||
|
fn make_text_style(font_cache: &FontCache, settings: &Settings) -> TextStyle {
|
||||||
|
TextStyle {
|
||||||
|
color: settings.theme.editor.text_color,
|
||||||
|
font_family_id: settings.buffer_font_family,
|
||||||
|
font_family_name: font_cache.family_name(settings.buffer_font_family).unwrap(),
|
||||||
|
font_id: font_cache
|
||||||
|
.select_font(settings.buffer_font_family, &Default::default())
|
||||||
|
.unwrap(),
|
||||||
|
font_size: settings.buffer_font_size,
|
||||||
|
font_properties: Default::default(),
|
||||||
|
underline: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
///Configures a size info object from the given information.
|
||||||
|
fn make_new_size(
|
||||||
|
constraint: SizeConstraint,
|
||||||
|
cell_width: &CellWidth,
|
||||||
|
line_height: &LineHeight,
|
||||||
|
) -> SizeInfo {
|
||||||
|
SizeInfo::new(
|
||||||
|
constraint.max.x() - cell_width.0,
|
||||||
|
constraint.max.y(),
|
||||||
|
cell_width.0,
|
||||||
|
line_height.0,
|
||||||
|
0.,
|
||||||
|
0.,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct BuiltChunks {
|
||||||
|
pub chunks: Vec<(String, Option<HighlightStyle>, RectSpan)>,
|
||||||
|
pub line_count: usize,
|
||||||
|
pub cursor_index: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
///In a single pass, this function generates the background and foreground color info for every item in the grid.
|
||||||
|
pub(crate) fn build_chunks(
|
||||||
|
grid_iterator: GridIterator<Cell>,
|
||||||
|
theme: &TerminalStyle,
|
||||||
|
cursor_point: Point,
|
||||||
|
) -> BuiltChunks {
|
||||||
|
let mut line_count: usize = 0;
|
||||||
|
let mut cursor_index: usize = 0;
|
||||||
|
//Every `group_by()` -> `into_iter()` pair needs to be seperated by a local variable so
|
||||||
|
//rust knows where to put everything.
|
||||||
|
//Start by grouping by lines
|
||||||
|
let lines = grid_iterator.group_by(|i| i.point.line.0);
|
||||||
|
let result = lines
|
||||||
|
.into_iter()
|
||||||
|
.map(|(_line_grid_index, line)| {
|
||||||
|
line_count += 1;
|
||||||
|
let mut col_index = 0;
|
||||||
|
//Setup a variable
|
||||||
|
|
||||||
|
//Then group by style
|
||||||
|
let chunks = line.group_by(|i| cell_style(&i, theme));
|
||||||
|
chunks
|
||||||
|
.into_iter()
|
||||||
|
.map(|(style, fragment)| {
|
||||||
|
//And assemble the styled fragment into it's background and foreground information
|
||||||
|
let mut str_fragment = String::new();
|
||||||
|
for indexed_cell in fragment {
|
||||||
|
if cursor_point.line.0 == indexed_cell.point.line.0
|
||||||
|
&& indexed_cell.point.column < cursor_point.column.0
|
||||||
|
{
|
||||||
|
cursor_index += indexed_cell.c.to_string().len();
|
||||||
|
}
|
||||||
|
str_fragment.push(indexed_cell.c);
|
||||||
|
}
|
||||||
|
|
||||||
|
let start = col_index;
|
||||||
|
let end = start + str_fragment.len() as i32;
|
||||||
|
|
||||||
|
//munge it here
|
||||||
|
col_index = end;
|
||||||
|
(
|
||||||
|
str_fragment,
|
||||||
|
Some(style.0),
|
||||||
|
RectSpan::new(start, end, line_count - 1, style.1), //Line count -> Line index
|
||||||
|
)
|
||||||
|
})
|
||||||
|
//Add a \n to the end, as we're using text layouting rather than grid layouts
|
||||||
|
.chain(iter::once(("\n".to_string(), None, Default::default())))
|
||||||
|
.collect::<Vec<(String, Option<HighlightStyle>, RectSpan)>>()
|
||||||
|
})
|
||||||
|
.flatten()
|
||||||
|
//We have a Vec<Vec<>> (Vec of lines of styled chunks), flatten to just Vec<> (the styled chunks)
|
||||||
|
.collect::<Vec<(String, Option<HighlightStyle>, RectSpan)>>();
|
||||||
|
|
||||||
|
BuiltChunks {
|
||||||
|
chunks: result,
|
||||||
|
line_count,
|
||||||
|
cursor_index,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
///Convert a RectSpan in terms of character offsets, into RectFs of exact offsets
|
||||||
|
fn make_background_rects(
|
||||||
|
backgrounds: Vec<RectSpan>,
|
||||||
|
shaped_lines: &Vec<Line>,
|
||||||
|
line_height: &LineHeight,
|
||||||
|
) -> Vec<(RectF, Color)> {
|
||||||
|
backgrounds
|
||||||
|
.into_iter()
|
||||||
|
.map(|line_span| {
|
||||||
|
//This should always be safe, as the shaped lines and backgrounds where derived
|
||||||
|
//At the same time earlier
|
||||||
|
let line = shaped_lines
|
||||||
|
.get(line_span.line)
|
||||||
|
.expect("Background line_num did not correspond to a line number");
|
||||||
|
let x = line.x_for_index(line_span.start as usize);
|
||||||
|
let width = line.x_for_index(line_span.end as usize) - x;
|
||||||
|
(
|
||||||
|
RectF::new(
|
||||||
|
vec2f(x, line_span.line as f32 * line_height.0),
|
||||||
|
vec2f(width, line_height.0),
|
||||||
|
),
|
||||||
|
line_span.color,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect::<Vec<(RectF, Color)>>()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute the cursor position and expected block width, may return a zero width if x_for_index returns
|
||||||
|
// the same position for sequential indexes. Use em_width instead
|
||||||
|
fn get_cursor_position(
|
||||||
|
line: usize,
|
||||||
|
line_index: usize,
|
||||||
|
shaped_lines: &Vec<Line>,
|
||||||
|
display_offset: usize,
|
||||||
|
line_height: &LineHeight,
|
||||||
|
) -> Option<(Vector2F, f32)> {
|
||||||
|
let cursor_line = line + display_offset;
|
||||||
|
shaped_lines.get(cursor_line).map(|layout_line| {
|
||||||
|
let cursor_x = layout_line.x_for_index(line_index);
|
||||||
|
let next_char_x = layout_line.x_for_index(line_index + 1);
|
||||||
|
(
|
||||||
|
vec2f(cursor_x, cursor_line as f32 * line_height.0),
|
||||||
|
next_char_x - cursor_x,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
///Convert the Alacritty cell styles to GPUI text styles and background color
|
||||||
|
fn cell_style(indexed: &Indexed<&Cell>, style: &TerminalStyle) -> (HighlightStyle, Color) {
|
||||||
|
let flags = indexed.cell.flags;
|
||||||
|
let fg = Some(convert_color(&indexed.cell.fg, style));
|
||||||
|
let bg = convert_color(&indexed.cell.bg, style);
|
||||||
|
|
||||||
|
let underline = flags.contains(Flags::UNDERLINE).then(|| Underline {
|
||||||
|
color: fg,
|
||||||
|
squiggly: false,
|
||||||
|
thickness: OrderedFloat(1.),
|
||||||
|
});
|
||||||
|
|
||||||
|
(
|
||||||
|
HighlightStyle {
|
||||||
|
color: fg,
|
||||||
|
underline,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
bg,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
///Converts a 2, 8, or 24 bit color ANSI color to the GPUI equivalent
|
||||||
|
fn convert_color(alac_color: &AnsiColor, style: &TerminalStyle) -> Color {
|
||||||
|
match alac_color {
|
||||||
|
//Named and theme defined colors
|
||||||
|
alacritty_terminal::ansi::Color::Named(n) => match n {
|
||||||
|
alacritty_terminal::ansi::NamedColor::Black => style.black,
|
||||||
|
alacritty_terminal::ansi::NamedColor::Red => style.red,
|
||||||
|
alacritty_terminal::ansi::NamedColor::Green => style.green,
|
||||||
|
alacritty_terminal::ansi::NamedColor::Yellow => style.yellow,
|
||||||
|
alacritty_terminal::ansi::NamedColor::Blue => style.blue,
|
||||||
|
alacritty_terminal::ansi::NamedColor::Magenta => style.magenta,
|
||||||
|
alacritty_terminal::ansi::NamedColor::Cyan => style.cyan,
|
||||||
|
alacritty_terminal::ansi::NamedColor::White => style.white,
|
||||||
|
alacritty_terminal::ansi::NamedColor::BrightBlack => style.bright_black,
|
||||||
|
alacritty_terminal::ansi::NamedColor::BrightRed => style.bright_red,
|
||||||
|
alacritty_terminal::ansi::NamedColor::BrightGreen => style.bright_green,
|
||||||
|
alacritty_terminal::ansi::NamedColor::BrightYellow => style.bright_yellow,
|
||||||
|
alacritty_terminal::ansi::NamedColor::BrightBlue => style.bright_blue,
|
||||||
|
alacritty_terminal::ansi::NamedColor::BrightMagenta => style.bright_magenta,
|
||||||
|
alacritty_terminal::ansi::NamedColor::BrightCyan => style.bright_cyan,
|
||||||
|
alacritty_terminal::ansi::NamedColor::BrightWhite => style.bright_white,
|
||||||
|
alacritty_terminal::ansi::NamedColor::Foreground => style.foreground,
|
||||||
|
alacritty_terminal::ansi::NamedColor::Background => style.background,
|
||||||
|
alacritty_terminal::ansi::NamedColor::Cursor => style.cursor,
|
||||||
|
alacritty_terminal::ansi::NamedColor::DimBlack => style.dim_black,
|
||||||
|
alacritty_terminal::ansi::NamedColor::DimRed => style.dim_red,
|
||||||
|
alacritty_terminal::ansi::NamedColor::DimGreen => style.dim_green,
|
||||||
|
alacritty_terminal::ansi::NamedColor::DimYellow => style.dim_yellow,
|
||||||
|
alacritty_terminal::ansi::NamedColor::DimBlue => style.dim_blue,
|
||||||
|
alacritty_terminal::ansi::NamedColor::DimMagenta => style.dim_magenta,
|
||||||
|
alacritty_terminal::ansi::NamedColor::DimCyan => style.dim_cyan,
|
||||||
|
alacritty_terminal::ansi::NamedColor::DimWhite => style.dim_white,
|
||||||
|
alacritty_terminal::ansi::NamedColor::BrightForeground => style.bright_foreground,
|
||||||
|
alacritty_terminal::ansi::NamedColor::DimForeground => style.dim_foreground,
|
||||||
|
},
|
||||||
|
//'True' colors
|
||||||
|
alacritty_terminal::ansi::Color::Spec(rgb) => Color::new(rgb.r, rgb.g, rgb.b, u8::MAX),
|
||||||
|
//8 bit, indexed colors
|
||||||
|
alacritty_terminal::ansi::Color::Indexed(i) => get_color_at_index(i, style),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
///Converts an 8 bit ANSI color to it's GPUI equivalent.
|
||||||
|
pub fn get_color_at_index(index: &u8, style: &TerminalStyle) -> Color {
|
||||||
|
match index {
|
||||||
|
//0-15 are the same as the named colors above
|
||||||
|
0 => style.black,
|
||||||
|
1 => style.red,
|
||||||
|
2 => style.green,
|
||||||
|
3 => style.yellow,
|
||||||
|
4 => style.blue,
|
||||||
|
5 => style.magenta,
|
||||||
|
6 => style.cyan,
|
||||||
|
7 => style.white,
|
||||||
|
8 => style.bright_black,
|
||||||
|
9 => style.bright_red,
|
||||||
|
10 => style.bright_green,
|
||||||
|
11 => style.bright_yellow,
|
||||||
|
12 => style.bright_blue,
|
||||||
|
13 => style.bright_magenta,
|
||||||
|
14 => style.bright_cyan,
|
||||||
|
15 => style.bright_white,
|
||||||
|
//16-231 are mapped to their RGB colors on a 0-5 range per channel
|
||||||
|
16..=231 => {
|
||||||
|
let (r, g, b) = rgb_for_index(index); //Split the index into it's ANSI-RGB components
|
||||||
|
let step = (u8::MAX as f32 / 5.).floor() as u8; //Split the RGB range into 5 chunks, with floor so no overflow
|
||||||
|
Color::new(r * step, g * step, b * step, u8::MAX) //Map the ANSI-RGB components to an RGB color
|
||||||
|
}
|
||||||
|
//232-255 are a 24 step grayscale from black to white
|
||||||
|
232..=255 => {
|
||||||
|
let i = index - 232; //Align index to 0..24
|
||||||
|
let step = (u8::MAX as f32 / 24.).floor() as u8; //Split the RGB grayscale values into 24 chunks
|
||||||
|
Color::new(i * step, i * step, i * step, u8::MAX) //Map the ANSI-grayscale components to the RGB-grayscale
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
///Generates the rgb channels in [0, 5] for a given index into the 6x6x6 ANSI color cube
|
||||||
|
///See: [8 bit ansi color](https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit).
|
||||||
|
///
|
||||||
|
///Wikipedia gives a formula for calculating the index for a given color:
|
||||||
|
///
|
||||||
|
///index = 16 + 36 × r + 6 × g + b (0 ≤ r, g, b ≤ 5)
|
||||||
|
///
|
||||||
|
///This function does the reverse, calculating the r, g, and b components from a given index.
|
||||||
|
fn rgb_for_index(i: &u8) -> (u8, u8, u8) {
|
||||||
|
debug_assert!(i >= &16 && i <= &231);
|
||||||
|
let i = i - 16;
|
||||||
|
let r = (i - (i % 36)) / 36;
|
||||||
|
let g = ((i % 36) - (i % 6)) / 6;
|
||||||
|
let b = (i % 36) % 6;
|
||||||
|
(r, g, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
///Draws the grid as Alacritty sees it. Useful for checking if there is an inconsistency between
|
||||||
|
///Display and conceptual grid.
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
fn draw_debug_grid(bounds: RectF, layout: &mut LayoutState, cx: &mut PaintContext) {
|
||||||
|
let width = layout.cur_size.width();
|
||||||
|
let height = layout.cur_size.height();
|
||||||
|
//Alacritty uses 'as usize', so shall we.
|
||||||
|
for col in 0..(width / layout.em_width.0).round() as usize {
|
||||||
|
cx.scene.push_quad(Quad {
|
||||||
|
bounds: RectF::new(
|
||||||
|
bounds.origin() + vec2f((col + 1) as f32 * layout.em_width.0, 0.),
|
||||||
|
vec2f(1., height),
|
||||||
|
),
|
||||||
|
background: Some(Color::green()),
|
||||||
|
border: Default::default(),
|
||||||
|
corner_radius: 0.,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
for row in 0..((height / layout.line_height.0) + 1.0).round() as usize {
|
||||||
|
cx.scene.push_quad(Quad {
|
||||||
|
bounds: RectF::new(
|
||||||
|
bounds.origin() + vec2f(layout.em_width.0, row as f32 * layout.line_height.0),
|
||||||
|
vec2f(width, 1.),
|
||||||
|
),
|
||||||
|
background: Some(Color::green()),
|
||||||
|
border: Default::default(),
|
||||||
|
corner_radius: 0.,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
#[test]
|
||||||
|
fn test_rgb_for_index() {
|
||||||
|
//Test every possible value in the color cube
|
||||||
|
for i in 16..=231 {
|
||||||
|
let (r, g, b) = crate::terminal_element::rgb_for_index(&(i as u8));
|
||||||
|
assert_eq!(i, 16 + 36 * r + 6 * g + b);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
19
crates/terminal/truecolor.sh
Executable file
19
crates/terminal/truecolor.sh
Executable file
|
@ -0,0 +1,19 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# Copied from: https://unix.stackexchange.com/a/696756
|
||||||
|
# Based on: https://gist.github.com/XVilka/8346728 and https://unix.stackexchange.com/a/404415/395213
|
||||||
|
|
||||||
|
awk -v term_cols="${width:-$(tput cols || echo 80)}" -v term_lines="${height:-1}" 'BEGIN{
|
||||||
|
s="/\\";
|
||||||
|
total_cols=term_cols*term_lines;
|
||||||
|
for (colnum = 0; colnum<total_cols; colnum++) {
|
||||||
|
r = 255-(colnum*255/total_cols);
|
||||||
|
g = (colnum*510/total_cols);
|
||||||
|
b = (colnum*255/total_cols);
|
||||||
|
if (g>255) g = 510-g;
|
||||||
|
printf "\033[48;2;%d;%d;%dm", r,g,b;
|
||||||
|
printf "\033[38;2;%d;%d;%dm", 255-r,255-g,255-b;
|
||||||
|
printf "%s\033[0m", substr(s,colnum%2+1,1);
|
||||||
|
if (colnum%term_cols==term_cols) printf "\n";
|
||||||
|
}
|
||||||
|
printf "\n";
|
||||||
|
}'
|
|
@ -33,6 +33,7 @@ pub struct Theme {
|
||||||
pub contact_notification: ContactNotification,
|
pub contact_notification: ContactNotification,
|
||||||
pub update_notification: UpdateNotification,
|
pub update_notification: UpdateNotification,
|
||||||
pub tooltip: TooltipStyle,
|
pub tooltip: TooltipStyle,
|
||||||
|
pub terminal: TerminalStyle,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Default)]
|
#[derive(Deserialize, Default)]
|
||||||
|
@ -633,3 +634,36 @@ pub struct HoverPopover {
|
||||||
pub prose: TextStyle,
|
pub prose: TextStyle,
|
||||||
pub highlight: Color,
|
pub highlight: Color,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Deserialize, Default)]
|
||||||
|
pub struct TerminalStyle {
|
||||||
|
pub black: Color,
|
||||||
|
pub red: Color,
|
||||||
|
pub green: Color,
|
||||||
|
pub yellow: Color,
|
||||||
|
pub blue: Color,
|
||||||
|
pub magenta: Color,
|
||||||
|
pub cyan: Color,
|
||||||
|
pub white: Color,
|
||||||
|
pub bright_black: Color,
|
||||||
|
pub bright_red: Color,
|
||||||
|
pub bright_green: Color,
|
||||||
|
pub bright_yellow: Color,
|
||||||
|
pub bright_blue: Color,
|
||||||
|
pub bright_magenta: Color,
|
||||||
|
pub bright_cyan: Color,
|
||||||
|
pub bright_white: Color,
|
||||||
|
pub foreground: Color,
|
||||||
|
pub background: Color,
|
||||||
|
pub cursor: Color,
|
||||||
|
pub dim_black: Color,
|
||||||
|
pub dim_red: Color,
|
||||||
|
pub dim_green: Color,
|
||||||
|
pub dim_yellow: Color,
|
||||||
|
pub dim_blue: Color,
|
||||||
|
pub dim_magenta: Color,
|
||||||
|
pub dim_cyan: Color,
|
||||||
|
pub dim_white: Color,
|
||||||
|
pub bright_foreground: Color,
|
||||||
|
pub dim_foreground: Color,
|
||||||
|
}
|
||||||
|
|
|
@ -14,6 +14,7 @@ command_palette = { path = "../command_palette" }
|
||||||
editor = { path = "../editor" }
|
editor = { path = "../editor" }
|
||||||
gpui = { path = "../gpui" }
|
gpui = { path = "../gpui" }
|
||||||
language = { path = "../language" }
|
language = { path = "../language" }
|
||||||
|
search = { path = "../search" }
|
||||||
serde = { version = "1.0", features = ["derive", "rc"] }
|
serde = { version = "1.0", features = ["derive", "rc"] }
|
||||||
settings = { path = "../settings" }
|
settings = { path = "../settings" }
|
||||||
workspace = { path = "../workspace" }
|
workspace = { path = "../workspace" }
|
||||||
|
|
|
@ -13,7 +13,7 @@ pub fn init(cx: &mut MutableAppContext) {
|
||||||
fn editor_created(EditorCreated(editor): &EditorCreated, cx: &mut MutableAppContext) {
|
fn editor_created(EditorCreated(editor): &EditorCreated, cx: &mut MutableAppContext) {
|
||||||
cx.update_default_global(|vim: &mut Vim, cx| {
|
cx.update_default_global(|vim: &mut Vim, cx| {
|
||||||
vim.editors.insert(editor.id(), editor.downgrade());
|
vim.editors.insert(editor.id(), editor.downgrade());
|
||||||
vim.sync_editor_options(cx);
|
vim.sync_vim_settings(cx);
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -29,8 +29,17 @@ fn editor_focused(EditorFocused(editor): &EditorFocused, cx: &mut MutableAppCont
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
if editor.read(cx).mode() != EditorMode::Full {
|
if !vim.enabled {
|
||||||
vim.switch_mode(Mode::Insert, cx);
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let editor = editor.read(cx);
|
||||||
|
if editor.selections.newest::<usize>(cx).is_empty() {
|
||||||
|
if editor.mode() != EditorMode::Full {
|
||||||
|
vim.switch_mode(Mode::Insert, cx);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
vim.switch_mode(Mode::Visual { line: false }, cx);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -42,7 +51,7 @@ fn editor_blurred(EditorBlurred(editor): &EditorBlurred, cx: &mut MutableAppCont
|
||||||
vim.active_editor = None;
|
vim.active_editor = None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
vim.sync_editor_options(cx);
|
vim.sync_vim_settings(cx);
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1165,7 +1165,7 @@ mod test {
|
||||||
The quick brown
|
The quick brown
|
||||||
fox [jump}s over
|
fox [jump}s over
|
||||||
the lazy dog"},
|
the lazy dog"},
|
||||||
Mode::Normal,
|
Mode::Visual { line: false },
|
||||||
);
|
);
|
||||||
cx.simulate_keystroke("y");
|
cx.simulate_keystroke("y");
|
||||||
cx.set_state(
|
cx.set_state(
|
||||||
|
|
|
@ -40,7 +40,7 @@ pub fn delete_over(vim: &mut Vim, motion: Motion, cx: &mut MutableAppContext) {
|
||||||
mod test {
|
mod test {
|
||||||
use indoc::indoc;
|
use indoc::indoc;
|
||||||
|
|
||||||
use crate::vim_test_context::VimTestContext;
|
use crate::{state::Mode, vim_test_context::VimTestContext};
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_delete_h(cx: &mut gpui::TestAppContext) {
|
async fn test_delete_h(cx: &mut gpui::TestAppContext) {
|
||||||
|
@ -390,4 +390,42 @@ mod test {
|
||||||
the lazy"},
|
the lazy"},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_cancel_delete_operator(cx: &mut gpui::TestAppContext) {
|
||||||
|
let mut cx = VimTestContext::new(cx, true).await;
|
||||||
|
cx.set_state(
|
||||||
|
indoc! {"
|
||||||
|
The quick brown
|
||||||
|
fox ju|mps over
|
||||||
|
the lazy dog"},
|
||||||
|
Mode::Normal,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Canceling operator twice reverts to normal mode with no active operator
|
||||||
|
cx.simulate_keystrokes(["d", "escape", "k"]);
|
||||||
|
assert_eq!(cx.active_operator(), None);
|
||||||
|
assert_eq!(cx.mode(), Mode::Normal);
|
||||||
|
cx.assert_editor_state(indoc! {"
|
||||||
|
The qu|ick brown
|
||||||
|
fox jumps over
|
||||||
|
the lazy dog"});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_unbound_command_cancels_pending_operator(cx: &mut gpui::TestAppContext) {
|
||||||
|
let mut cx = VimTestContext::new(cx, true).await;
|
||||||
|
cx.set_state(
|
||||||
|
indoc! {"
|
||||||
|
The quick brown
|
||||||
|
fox ju|mps over
|
||||||
|
the lazy dog"},
|
||||||
|
Mode::Normal,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Canceling operator twice reverts to normal mode with no active operator
|
||||||
|
cx.simulate_keystrokes(["d", "y"]);
|
||||||
|
assert_eq!(cx.active_operator(), None);
|
||||||
|
assert_eq!(cx.mode(), Mode::Normal);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,7 +37,14 @@ pub struct VimState {
|
||||||
impl VimState {
|
impl VimState {
|
||||||
pub fn cursor_shape(&self) -> CursorShape {
|
pub fn cursor_shape(&self) -> CursorShape {
|
||||||
match self.mode {
|
match self.mode {
|
||||||
Mode::Normal | Mode::Visual { .. } => CursorShape::Block,
|
Mode::Normal => {
|
||||||
|
if self.operator_stack.is_empty() {
|
||||||
|
CursorShape::Block
|
||||||
|
} else {
|
||||||
|
CursorShape::Underscore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Mode::Visual { .. } => CursorShape::Block,
|
||||||
Mode::Insert => CursorShape::Bar,
|
Mode::Insert => CursorShape::Bar,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -73,20 +80,20 @@ impl VimState {
|
||||||
context.set.insert("VimControl".to_string());
|
context.set.insert("VimControl".to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(operator) = &self.operator_stack.last() {
|
Operator::set_context(self.operator_stack.last(), &mut context);
|
||||||
operator.set_context(&mut context);
|
|
||||||
}
|
|
||||||
context
|
context
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Operator {
|
impl Operator {
|
||||||
pub fn set_context(&self, context: &mut Context) {
|
pub fn set_context(operator: Option<&Operator>, context: &mut Context) {
|
||||||
let operator_context = match self {
|
let operator_context = match operator {
|
||||||
Operator::Namespace(Namespace::G) => "g",
|
Some(Operator::Namespace(Namespace::G)) => "g",
|
||||||
Operator::Change => "c",
|
Some(Operator::Change) => "c",
|
||||||
Operator::Delete => "d",
|
Some(Operator::Delete) => "d",
|
||||||
Operator::Yank => "y",
|
Some(Operator::Yank) => "y",
|
||||||
|
None => "none",
|
||||||
}
|
}
|
||||||
.to_owned();
|
.to_owned();
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,7 @@ mod visual;
|
||||||
|
|
||||||
use collections::HashMap;
|
use collections::HashMap;
|
||||||
use command_palette::CommandPaletteFilter;
|
use command_palette::CommandPaletteFilter;
|
||||||
use editor::{Bias, CursorShape, Editor, Input};
|
use editor::{Bias, Cancel, CursorShape, Editor, Input};
|
||||||
use gpui::{impl_actions, MutableAppContext, Subscription, ViewContext, WeakViewHandle};
|
use gpui::{impl_actions, MutableAppContext, Subscription, ViewContext, WeakViewHandle};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
@ -34,6 +34,7 @@ pub fn init(cx: &mut MutableAppContext) {
|
||||||
insert::init(cx);
|
insert::init(cx);
|
||||||
motion::init(cx);
|
motion::init(cx);
|
||||||
|
|
||||||
|
// Vim Actions
|
||||||
cx.add_action(|_: &mut Workspace, &SwitchMode(mode): &SwitchMode, cx| {
|
cx.add_action(|_: &mut Workspace, &SwitchMode(mode): &SwitchMode, cx| {
|
||||||
Vim::update(cx, |vim, cx| vim.switch_mode(mode, cx))
|
Vim::update(cx, |vim, cx| vim.switch_mode(mode, cx))
|
||||||
});
|
});
|
||||||
|
@ -42,7 +43,11 @@ pub fn init(cx: &mut MutableAppContext) {
|
||||||
Vim::update(cx, |vim, cx| vim.push_operator(operator, cx))
|
Vim::update(cx, |vim, cx| vim.push_operator(operator, cx))
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Editor Actions
|
||||||
cx.add_action(|_: &mut Editor, _: &Input, cx| {
|
cx.add_action(|_: &mut Editor, _: &Input, cx| {
|
||||||
|
// If we have an unbound input with an active operator, cancel that operator. Otherwise forward
|
||||||
|
// the input to the editor
|
||||||
if Vim::read(cx).active_operator().is_some() {
|
if Vim::read(cx).active_operator().is_some() {
|
||||||
// Defer without updating editor
|
// Defer without updating editor
|
||||||
MutableAppContext::defer(cx, |cx| Vim::update(cx, |vim, cx| vim.clear_operator(cx)))
|
MutableAppContext::defer(cx, |cx| Vim::update(cx, |vim, cx| vim.clear_operator(cx)))
|
||||||
|
@ -50,7 +55,25 @@ pub fn init(cx: &mut MutableAppContext) {
|
||||||
cx.propagate_action()
|
cx.propagate_action()
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
cx.add_action(|_: &mut Editor, _: &Cancel, cx| {
|
||||||
|
// If we are in a non normal mode or have an active operator, swap to normal mode
|
||||||
|
// Otherwise forward cancel on to the editor
|
||||||
|
let vim = Vim::read(cx);
|
||||||
|
if vim.state.mode != Mode::Normal || vim.active_operator().is_some() {
|
||||||
|
MutableAppContext::defer(cx, |cx| {
|
||||||
|
Vim::update(cx, |state, cx| {
|
||||||
|
state.switch_mode(Mode::Normal, cx);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
cx.propagate_action();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sync initial settings with the rest of the app
|
||||||
|
Vim::update(cx, |state, cx| state.sync_vim_settings(cx));
|
||||||
|
|
||||||
|
// Any time settings change, update vim mode to match
|
||||||
cx.observe_global::<Settings, _>(|cx| {
|
cx.observe_global::<Settings, _>(|cx| {
|
||||||
Vim::update(cx, |state, cx| {
|
Vim::update(cx, |state, cx| {
|
||||||
state.set_enabled(cx.global::<Settings>().vim_mode, cx)
|
state.set_enabled(cx.global::<Settings>().vim_mode, cx)
|
||||||
|
@ -93,25 +116,62 @@ impl Vim {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn switch_mode(&mut self, mode: Mode, cx: &mut MutableAppContext) {
|
fn switch_mode(&mut self, mode: Mode, cx: &mut MutableAppContext) {
|
||||||
|
let previous_mode = self.state.mode;
|
||||||
self.state.mode = mode;
|
self.state.mode = mode;
|
||||||
self.state.operator_stack.clear();
|
self.state.operator_stack.clear();
|
||||||
self.sync_editor_options(cx);
|
|
||||||
|
// Sync editor settings like clip mode
|
||||||
|
self.sync_vim_settings(cx);
|
||||||
|
|
||||||
|
// Adjust selections
|
||||||
|
for editor in self.editors.values() {
|
||||||
|
if let Some(editor) = editor.upgrade(cx) {
|
||||||
|
editor.update(cx, |editor, cx| {
|
||||||
|
editor.change_selections(None, cx, |s| {
|
||||||
|
s.move_with(|map, selection| {
|
||||||
|
// If empty selections
|
||||||
|
if self.state.empty_selections_only() {
|
||||||
|
let new_head = map.clip_point(selection.head(), Bias::Left);
|
||||||
|
selection.collapse_to(new_head, selection.goal)
|
||||||
|
} else {
|
||||||
|
if matches!(mode, Mode::Visual { line: false })
|
||||||
|
&& !matches!(previous_mode, Mode::Visual { .. })
|
||||||
|
&& !selection.reversed
|
||||||
|
&& !selection.is_empty()
|
||||||
|
{
|
||||||
|
// Mode wasn't visual mode before, but is now. We need to move the end
|
||||||
|
// back by one character so that the region to be modifed stays the same
|
||||||
|
*selection.end.column_mut() =
|
||||||
|
selection.end.column().saturating_sub(1);
|
||||||
|
selection.end = map.clip_point(selection.end, Bias::Left);
|
||||||
|
}
|
||||||
|
|
||||||
|
selection.set_head(
|
||||||
|
map.clip_point(selection.head(), Bias::Left),
|
||||||
|
selection.goal,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn push_operator(&mut self, operator: Operator, cx: &mut MutableAppContext) {
|
fn push_operator(&mut self, operator: Operator, cx: &mut MutableAppContext) {
|
||||||
self.state.operator_stack.push(operator);
|
self.state.operator_stack.push(operator);
|
||||||
self.sync_editor_options(cx);
|
self.sync_vim_settings(cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn pop_operator(&mut self, cx: &mut MutableAppContext) -> Operator {
|
fn pop_operator(&mut self, cx: &mut MutableAppContext) -> Operator {
|
||||||
let popped_operator = self.state.operator_stack.pop().expect("Operator popped when no operator was on the stack. This likely means there is an invalid keymap config");
|
let popped_operator = self.state.operator_stack.pop().expect("Operator popped when no operator was on the stack. This likely means there is an invalid keymap config");
|
||||||
self.sync_editor_options(cx);
|
self.sync_vim_settings(cx);
|
||||||
popped_operator
|
popped_operator
|
||||||
}
|
}
|
||||||
|
|
||||||
fn clear_operator(&mut self, cx: &mut MutableAppContext) {
|
fn clear_operator(&mut self, cx: &mut MutableAppContext) {
|
||||||
self.state.operator_stack.clear();
|
self.state.operator_stack.clear();
|
||||||
self.sync_editor_options(cx);
|
self.sync_vim_settings(cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn active_operator(&self) -> Option<Operator> {
|
fn active_operator(&self) -> Option<Operator> {
|
||||||
|
@ -123,23 +183,24 @@ impl Vim {
|
||||||
self.enabled = enabled;
|
self.enabled = enabled;
|
||||||
self.state = Default::default();
|
self.state = Default::default();
|
||||||
if enabled {
|
if enabled {
|
||||||
self.state.mode = Mode::Normal;
|
self.switch_mode(Mode::Normal, cx);
|
||||||
}
|
}
|
||||||
cx.update_default_global::<CommandPaletteFilter, _, _>(|filter, _| {
|
self.sync_vim_settings(cx);
|
||||||
if enabled {
|
|
||||||
filter.filtered_namespaces.remove("vim");
|
|
||||||
} else {
|
|
||||||
filter.filtered_namespaces.insert("vim");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
self.sync_editor_options(cx);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sync_editor_options(&self, cx: &mut MutableAppContext) {
|
fn sync_vim_settings(&self, cx: &mut MutableAppContext) {
|
||||||
let state = &self.state;
|
let state = &self.state;
|
||||||
let cursor_shape = state.cursor_shape();
|
let cursor_shape = state.cursor_shape();
|
||||||
|
|
||||||
|
cx.update_default_global::<CommandPaletteFilter, _, _>(|filter, _| {
|
||||||
|
if self.enabled {
|
||||||
|
filter.filtered_namespaces.remove("vim");
|
||||||
|
} else {
|
||||||
|
filter.filtered_namespaces.insert("vim");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
for editor in self.editors.values() {
|
for editor in self.editors.values() {
|
||||||
if let Some(editor) = editor.upgrade(cx) {
|
if let Some(editor) = editor.upgrade(cx) {
|
||||||
editor.update(cx, |editor, cx| {
|
editor.update(cx, |editor, cx| {
|
||||||
|
@ -151,17 +212,6 @@ impl Vim {
|
||||||
matches!(state.mode, Mode::Visual { line: true });
|
matches!(state.mode, Mode::Visual { line: true });
|
||||||
let context_layer = state.keymap_context_layer();
|
let context_layer = state.keymap_context_layer();
|
||||||
editor.set_keymap_context_layer::<Self>(context_layer);
|
editor.set_keymap_context_layer::<Self>(context_layer);
|
||||||
editor.change_selections(None, cx, |s| {
|
|
||||||
s.move_with(|map, selection| {
|
|
||||||
selection.set_head(
|
|
||||||
map.clip_point(selection.head(), Bias::Left),
|
|
||||||
selection.goal,
|
|
||||||
);
|
|
||||||
if state.empty_selections_only() {
|
|
||||||
selection.collapse_to(selection.head(), selection.goal)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
})
|
|
||||||
} else {
|
} else {
|
||||||
editor.set_cursor_shape(CursorShape::Bar, cx);
|
editor.set_cursor_shape(CursorShape::Bar, cx);
|
||||||
editor.set_clip_at_line_ends(false, cx);
|
editor.set_clip_at_line_ends(false, cx);
|
||||||
|
@ -177,6 +227,9 @@ impl Vim {
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
|
use indoc::indoc;
|
||||||
|
use search::BufferSearchBar;
|
||||||
|
|
||||||
use crate::{state::Mode, vim_test_context::VimTestContext};
|
use crate::{state::Mode, vim_test_context::VimTestContext};
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
|
@ -221,4 +274,34 @@ mod test {
|
||||||
cx.enable_vim();
|
cx.enable_vim();
|
||||||
assert_eq!(cx.mode(), Mode::Normal);
|
assert_eq!(cx.mode(), Mode::Normal);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_buffer_search_switches_mode(cx: &mut gpui::TestAppContext) {
|
||||||
|
let mut cx = VimTestContext::new(cx, true).await;
|
||||||
|
|
||||||
|
cx.set_state(
|
||||||
|
indoc! {"
|
||||||
|
The quick brown
|
||||||
|
fox ju|mps over
|
||||||
|
the lazy dog"},
|
||||||
|
Mode::Normal,
|
||||||
|
);
|
||||||
|
cx.simulate_keystroke("/");
|
||||||
|
|
||||||
|
assert_eq!(cx.mode(), Mode::Visual { line: false });
|
||||||
|
|
||||||
|
let search_bar = cx.workspace(|workspace, cx| {
|
||||||
|
workspace
|
||||||
|
.active_pane()
|
||||||
|
.read(cx)
|
||||||
|
.toolbar()
|
||||||
|
.read(cx)
|
||||||
|
.item_of_type::<BufferSearchBar>()
|
||||||
|
.expect("Buffer search bar should be deployed")
|
||||||
|
});
|
||||||
|
|
||||||
|
search_bar.read_with(cx.cx, |bar, cx| {
|
||||||
|
assert_eq!(bar.query_editor.read(cx).text(cx), "jumps");
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,16 @@
|
||||||
use std::ops::{Deref, DerefMut};
|
use std::ops::{Deref, DerefMut};
|
||||||
|
|
||||||
use editor::test::EditorTestContext;
|
use editor::test::EditorTestContext;
|
||||||
use gpui::json::json;
|
use gpui::{json::json, AppContext, ViewHandle};
|
||||||
use project::Project;
|
use project::Project;
|
||||||
|
use search::{BufferSearchBar, ProjectSearchBar};
|
||||||
use workspace::{pane, AppState, WorkspaceHandle};
|
use workspace::{pane, AppState, WorkspaceHandle};
|
||||||
|
|
||||||
use crate::{state::Operator, *};
|
use crate::{state::Operator, *};
|
||||||
|
|
||||||
pub struct VimTestContext<'a> {
|
pub struct VimTestContext<'a> {
|
||||||
cx: EditorTestContext<'a>,
|
cx: EditorTestContext<'a>,
|
||||||
|
workspace: ViewHandle<Workspace>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> VimTestContext<'a> {
|
impl<'a> VimTestContext<'a> {
|
||||||
|
@ -16,6 +18,7 @@ impl<'a> VimTestContext<'a> {
|
||||||
cx.update(|cx| {
|
cx.update(|cx| {
|
||||||
editor::init(cx);
|
editor::init(cx);
|
||||||
pane::init(cx);
|
pane::init(cx);
|
||||||
|
search::init(cx);
|
||||||
crate::init(cx);
|
crate::init(cx);
|
||||||
|
|
||||||
settings::KeymapFileContent::load("keymaps/vim.json", cx).unwrap();
|
settings::KeymapFileContent::load("keymaps/vim.json", cx).unwrap();
|
||||||
|
@ -37,6 +40,19 @@ impl<'a> VimTestContext<'a> {
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project.clone(), cx));
|
let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project.clone(), cx));
|
||||||
|
|
||||||
|
// Setup search toolbars
|
||||||
|
workspace.update(cx, |workspace, cx| {
|
||||||
|
workspace.active_pane().update(cx, |pane, cx| {
|
||||||
|
pane.toolbar().update(cx, |toolbar, cx| {
|
||||||
|
let buffer_search_bar = cx.add_view(|cx| BufferSearchBar::new(cx));
|
||||||
|
toolbar.add_item(buffer_search_bar, cx);
|
||||||
|
let project_search_bar = cx.add_view(|_| ProjectSearchBar::new());
|
||||||
|
toolbar.add_item(project_search_bar, cx);
|
||||||
|
})
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
project
|
project
|
||||||
.update(cx, |project, cx| {
|
.update(cx, |project, cx| {
|
||||||
project.find_or_create_local_worktree("/root", true, cx)
|
project.find_or_create_local_worktree("/root", true, cx)
|
||||||
|
@ -64,9 +80,17 @@ impl<'a> VimTestContext<'a> {
|
||||||
window_id,
|
window_id,
|
||||||
editor,
|
editor,
|
||||||
},
|
},
|
||||||
|
workspace,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn workspace<F, T>(&mut self, read: F) -> T
|
||||||
|
where
|
||||||
|
F: FnOnce(&Workspace, &AppContext) -> T,
|
||||||
|
{
|
||||||
|
self.workspace.read_with(self.cx.cx, read)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn enable_vim(&mut self) {
|
pub fn enable_vim(&mut self) {
|
||||||
self.cx.update(|cx| {
|
self.cx.update(|cx| {
|
||||||
cx.update_global(|settings: &mut Settings, _| {
|
cx.update_global(|settings: &mut Settings, _| {
|
||||||
|
|
|
@ -18,11 +18,15 @@ use settings::Settings;
|
||||||
use std::{any::Any, cell::RefCell, mem, path::Path, rc::Rc};
|
use std::{any::Any, cell::RefCell, mem, path::Path, rc::Rc};
|
||||||
use util::ResultExt;
|
use util::ResultExt;
|
||||||
|
|
||||||
|
#[derive(Clone, Deserialize, PartialEq)]
|
||||||
|
pub struct ActivateItem(pub usize);
|
||||||
|
|
||||||
actions!(
|
actions!(
|
||||||
pane,
|
pane,
|
||||||
[
|
[
|
||||||
ActivatePrevItem,
|
ActivatePrevItem,
|
||||||
ActivateNextItem,
|
ActivateNextItem,
|
||||||
|
ActivateLastItem,
|
||||||
CloseActiveItem,
|
CloseActiveItem,
|
||||||
CloseInactiveItems,
|
CloseInactiveItems,
|
||||||
ReopenClosedItem,
|
ReopenClosedItem,
|
||||||
|
@ -39,9 +43,6 @@ pub struct CloseItem {
|
||||||
pub pane: WeakViewHandle<Pane>,
|
pub pane: WeakViewHandle<Pane>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Deserialize, PartialEq)]
|
|
||||||
pub struct ActivateItem(pub usize);
|
|
||||||
|
|
||||||
#[derive(Clone, Deserialize, PartialEq)]
|
#[derive(Clone, Deserialize, PartialEq)]
|
||||||
pub struct GoBack {
|
pub struct GoBack {
|
||||||
#[serde(skip_deserializing)]
|
#[serde(skip_deserializing)]
|
||||||
|
@ -54,8 +55,8 @@ pub struct GoForward {
|
||||||
pub pane: Option<WeakViewHandle<Pane>>,
|
pub pane: Option<WeakViewHandle<Pane>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl_actions!(pane, [GoBack, GoForward]);
|
impl_actions!(pane, [GoBack, GoForward, ActivateItem]);
|
||||||
impl_internal_actions!(pane, [CloseItem, ActivateItem]);
|
impl_internal_actions!(pane, [CloseItem]);
|
||||||
|
|
||||||
const MAX_NAVIGATION_HISTORY_LEN: usize = 1024;
|
const MAX_NAVIGATION_HISTORY_LEN: usize = 1024;
|
||||||
|
|
||||||
|
@ -63,6 +64,9 @@ pub fn init(cx: &mut MutableAppContext) {
|
||||||
cx.add_action(|pane: &mut Pane, action: &ActivateItem, cx| {
|
cx.add_action(|pane: &mut Pane, action: &ActivateItem, cx| {
|
||||||
pane.activate_item(action.0, true, true, cx);
|
pane.activate_item(action.0, true, true, cx);
|
||||||
});
|
});
|
||||||
|
cx.add_action(|pane: &mut Pane, _: &ActivateLastItem, cx| {
|
||||||
|
pane.activate_item(pane.items.len() - 1, true, true, cx);
|
||||||
|
});
|
||||||
cx.add_action(|pane: &mut Pane, _: &ActivatePrevItem, cx| {
|
cx.add_action(|pane: &mut Pane, _: &ActivatePrevItem, cx| {
|
||||||
pane.activate_prev_item(cx);
|
pane.activate_prev_item(cx);
|
||||||
});
|
});
|
||||||
|
|
|
@ -55,7 +55,8 @@ impl Into<AnyViewHandle> for &dyn SidebarItemHandle {
|
||||||
pub struct Sidebar {
|
pub struct Sidebar {
|
||||||
side: Side,
|
side: Side,
|
||||||
items: Vec<Item>,
|
items: Vec<Item>,
|
||||||
active_item_ix: Option<usize>,
|
is_open: bool,
|
||||||
|
active_item_ix: usize,
|
||||||
actual_width: Rc<RefCell<f32>>,
|
actual_width: Rc<RefCell<f32>>,
|
||||||
custom_width: Rc<RefCell<f32>>,
|
custom_width: Rc<RefCell<f32>>,
|
||||||
}
|
}
|
||||||
|
@ -83,25 +84,41 @@ pub struct ToggleSidebarItem {
|
||||||
pub item_index: usize,
|
pub item_index: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, PartialEq)]
|
impl_actions!(workspace, [ToggleSidebarItem]);
|
||||||
pub struct ToggleSidebarItemFocus {
|
|
||||||
pub side: Side,
|
|
||||||
pub item_index: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl_actions!(workspace, [ToggleSidebarItem, ToggleSidebarItemFocus]);
|
|
||||||
|
|
||||||
impl Sidebar {
|
impl Sidebar {
|
||||||
pub fn new(side: Side) -> Self {
|
pub fn new(side: Side) -> Self {
|
||||||
Self {
|
Self {
|
||||||
side,
|
side,
|
||||||
items: Default::default(),
|
items: Default::default(),
|
||||||
active_item_ix: None,
|
active_item_ix: 0,
|
||||||
|
is_open: false,
|
||||||
actual_width: Rc::new(RefCell::new(260.)),
|
actual_width: Rc::new(RefCell::new(260.)),
|
||||||
custom_width: Rc::new(RefCell::new(260.)),
|
custom_width: Rc::new(RefCell::new(260.)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn is_open(&self) -> bool {
|
||||||
|
self.is_open
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn active_item_ix(&self) -> usize {
|
||||||
|
self.active_item_ix
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_open(&mut self, open: bool, cx: &mut ViewContext<Self>) {
|
||||||
|
if open != self.is_open {
|
||||||
|
self.is_open = open;
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn toggle_open(&mut self, cx: &mut ViewContext<Self>) {
|
||||||
|
if self.is_open {}
|
||||||
|
self.is_open = !self.is_open;
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
pub fn add_item<T: SidebarItem>(
|
pub fn add_item<T: SidebarItem>(
|
||||||
&mut self,
|
&mut self,
|
||||||
icon_path: &'static str,
|
icon_path: &'static str,
|
||||||
|
@ -133,23 +150,25 @@ impl Sidebar {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn activate_item(&mut self, item_ix: usize, cx: &mut ViewContext<Self>) {
|
pub fn activate_item(&mut self, item_ix: usize, cx: &mut ViewContext<Self>) {
|
||||||
self.active_item_ix = Some(item_ix);
|
self.active_item_ix = item_ix;
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn toggle_item(&mut self, item_ix: usize, cx: &mut ViewContext<Self>) {
|
pub fn toggle_item(&mut self, item_ix: usize, cx: &mut ViewContext<Self>) {
|
||||||
if self.active_item_ix == Some(item_ix) {
|
if self.active_item_ix == item_ix {
|
||||||
self.active_item_ix = None;
|
self.is_open = false;
|
||||||
} else {
|
} else {
|
||||||
self.active_item_ix = Some(item_ix);
|
self.active_item_ix = item_ix;
|
||||||
}
|
}
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn active_item(&self) -> Option<&Rc<dyn SidebarItemHandle>> {
|
pub fn active_item(&self) -> Option<&Rc<dyn SidebarItemHandle>> {
|
||||||
self.active_item_ix
|
if self.is_open {
|
||||||
.and_then(|ix| self.items.get(ix))
|
self.items.get(self.active_item_ix).map(|item| &item.view)
|
||||||
.map(|item| &item.view)
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_resize_handle(&self, theme: &Theme, cx: &mut RenderContext<Self>) -> ElementBox {
|
fn render_resize_handle(&self, theme: &Theme, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||||
|
@ -249,6 +268,7 @@ impl View for SidebarButtons {
|
||||||
let item_style = theme.item;
|
let item_style = theme.item;
|
||||||
let badge_style = theme.badge;
|
let badge_style = theme.badge;
|
||||||
let active_ix = sidebar.active_item_ix;
|
let active_ix = sidebar.active_item_ix;
|
||||||
|
let is_open = sidebar.is_open;
|
||||||
let side = sidebar.side;
|
let side = sidebar.side;
|
||||||
let group_style = match side {
|
let group_style = match side {
|
||||||
Side::Left => theme.group_left,
|
Side::Left => theme.group_left,
|
||||||
|
@ -267,7 +287,7 @@ impl View for SidebarButtons {
|
||||||
item_index: ix,
|
item_index: ix,
|
||||||
};
|
};
|
||||||
MouseEventHandler::new::<Self, _, _>(ix, cx, move |state, cx| {
|
MouseEventHandler::new::<Self, _, _>(ix, cx, move |state, cx| {
|
||||||
let is_active = Some(ix) == active_ix;
|
let is_active = is_open && ix == active_ix;
|
||||||
let style = item_style.style_for(state, is_active);
|
let style = item_style.style_for(state, is_active);
|
||||||
Stack::new()
|
Stack::new()
|
||||||
.with_child(Svg::new(icon_path).with_color(style.icon_color).boxed())
|
.with_child(Svg::new(icon_path).with_color(style.icon_color).boxed())
|
||||||
|
|
|
@ -1,7 +1,4 @@
|
||||||
use crate::{
|
use crate::{sidebar::Side, AppState, ToggleFollow, Workspace};
|
||||||
sidebar::{Side, ToggleSidebarItem},
|
|
||||||
AppState, ToggleFollow, Workspace,
|
|
||||||
};
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use client::{proto, Client, Contact};
|
use client::{proto, Client, Contact};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
|
@ -104,13 +101,7 @@ impl WaitingRoom {
|
||||||
&app_state,
|
&app_state,
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
workspace.toggle_sidebar_item(
|
workspace.toggle_sidebar(Side::Left, cx);
|
||||||
&ToggleSidebarItem {
|
|
||||||
side: Side::Left,
|
|
||||||
item_index: 0,
|
|
||||||
},
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
if let Some((host_peer_id, _)) =
|
if let Some((host_peer_id, _)) =
|
||||||
workspace.project.read(cx).collaborators().iter().find(
|
workspace.project.read(cx).collaborators().iter().find(
|
||||||
|(_, collaborator)| collaborator.replica_id == 0,
|
|(_, collaborator)| collaborator.replica_id == 0,
|
||||||
|
|
|
@ -31,7 +31,7 @@ use postage::prelude::Stream;
|
||||||
use project::{fs, Fs, Project, ProjectEntryId, ProjectPath, ProjectStore, Worktree, WorktreeId};
|
use project::{fs, Fs, Project, ProjectEntryId, ProjectPath, ProjectStore, Worktree, WorktreeId};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
use sidebar::{Side, Sidebar, SidebarButtons, ToggleSidebarItem, ToggleSidebarItemFocus};
|
use sidebar::{Side, Sidebar, SidebarButtons, ToggleSidebarItem};
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
use status_bar::StatusBar;
|
use status_bar::StatusBar;
|
||||||
pub use status_bar::StatusItemView;
|
pub use status_bar::StatusItemView;
|
||||||
|
@ -90,6 +90,8 @@ actions!(
|
||||||
ActivatePreviousPane,
|
ActivatePreviousPane,
|
||||||
ActivateNextPane,
|
ActivateNextPane,
|
||||||
FollowNextCollaborator,
|
FollowNextCollaborator,
|
||||||
|
ToggleLeftSidebar,
|
||||||
|
ToggleRightSidebar,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -104,6 +106,9 @@ pub struct ToggleProjectOnline {
|
||||||
pub project: Option<ModelHandle<Project>>,
|
pub project: Option<ModelHandle<Project>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Deserialize, PartialEq)]
|
||||||
|
pub struct ActivatePane(pub usize);
|
||||||
|
|
||||||
#[derive(Clone, PartialEq)]
|
#[derive(Clone, PartialEq)]
|
||||||
pub struct ToggleFollow(pub PeerId);
|
pub struct ToggleFollow(pub PeerId);
|
||||||
|
|
||||||
|
@ -122,7 +127,7 @@ impl_internal_actions!(
|
||||||
RemoveWorktreeFromProject
|
RemoveWorktreeFromProject
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
impl_actions!(workspace, [ToggleProjectOnline]);
|
impl_actions!(workspace, [ToggleProjectOnline, ActivatePane]);
|
||||||
|
|
||||||
pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
|
pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
|
||||||
pane::init(cx);
|
pane::init(cx);
|
||||||
|
@ -185,7 +190,6 @@ pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
cx.add_action(Workspace::toggle_sidebar_item);
|
cx.add_action(Workspace::toggle_sidebar_item);
|
||||||
cx.add_action(Workspace::toggle_sidebar_item_focus);
|
|
||||||
cx.add_action(Workspace::focus_center);
|
cx.add_action(Workspace::focus_center);
|
||||||
cx.add_action(|workspace: &mut Workspace, _: &ActivatePreviousPane, cx| {
|
cx.add_action(|workspace: &mut Workspace, _: &ActivatePreviousPane, cx| {
|
||||||
workspace.activate_previous_pane(cx)
|
workspace.activate_previous_pane(cx)
|
||||||
|
@ -193,6 +197,13 @@ pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
|
||||||
cx.add_action(|workspace: &mut Workspace, _: &ActivateNextPane, cx| {
|
cx.add_action(|workspace: &mut Workspace, _: &ActivateNextPane, cx| {
|
||||||
workspace.activate_next_pane(cx)
|
workspace.activate_next_pane(cx)
|
||||||
});
|
});
|
||||||
|
cx.add_action(|workspace: &mut Workspace, _: &ToggleLeftSidebar, cx| {
|
||||||
|
workspace.toggle_sidebar(Side::Left, cx);
|
||||||
|
});
|
||||||
|
cx.add_action(|workspace: &mut Workspace, _: &ToggleRightSidebar, cx| {
|
||||||
|
workspace.toggle_sidebar(Side::Right, cx);
|
||||||
|
});
|
||||||
|
cx.add_action(Workspace::activate_pane_at_index);
|
||||||
|
|
||||||
let client = &app_state.client;
|
let client = &app_state.client;
|
||||||
client.add_view_request_handler(Workspace::handle_follow);
|
client.add_view_request_handler(Workspace::handle_follow);
|
||||||
|
@ -1248,17 +1259,39 @@ impl Workspace {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn toggle_sidebar(&mut self, side: Side, cx: &mut ViewContext<Self>) {
|
||||||
|
let sidebar = match side {
|
||||||
|
Side::Left => &mut self.left_sidebar,
|
||||||
|
Side::Right => &mut self.right_sidebar,
|
||||||
|
};
|
||||||
|
sidebar.update(cx, |sidebar, cx| {
|
||||||
|
sidebar.set_open(!sidebar.is_open(), cx);
|
||||||
|
});
|
||||||
|
cx.focus_self();
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
pub fn toggle_sidebar_item(&mut self, action: &ToggleSidebarItem, cx: &mut ViewContext<Self>) {
|
pub fn toggle_sidebar_item(&mut self, action: &ToggleSidebarItem, cx: &mut ViewContext<Self>) {
|
||||||
let sidebar = match action.side {
|
let sidebar = match action.side {
|
||||||
Side::Left => &mut self.left_sidebar,
|
Side::Left => &mut self.left_sidebar,
|
||||||
Side::Right => &mut self.right_sidebar,
|
Side::Right => &mut self.right_sidebar,
|
||||||
};
|
};
|
||||||
let active_item = sidebar.update(cx, |sidebar, cx| {
|
let active_item = sidebar.update(cx, |sidebar, cx| {
|
||||||
sidebar.toggle_item(action.item_index, cx);
|
if sidebar.is_open() && sidebar.active_item_ix() == action.item_index {
|
||||||
sidebar.active_item().map(|item| item.to_any())
|
sidebar.set_open(false, cx);
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
sidebar.set_open(true, cx);
|
||||||
|
sidebar.activate_item(action.item_index, cx);
|
||||||
|
sidebar.active_item().cloned()
|
||||||
|
}
|
||||||
});
|
});
|
||||||
if let Some(active_item) = active_item {
|
if let Some(active_item) = active_item {
|
||||||
cx.focus(active_item);
|
if active_item.is_focused(cx) {
|
||||||
|
cx.focus_self();
|
||||||
|
} else {
|
||||||
|
cx.focus(active_item.to_any());
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
cx.focus_self();
|
cx.focus_self();
|
||||||
}
|
}
|
||||||
|
@ -1267,15 +1300,17 @@ impl Workspace {
|
||||||
|
|
||||||
pub fn toggle_sidebar_item_focus(
|
pub fn toggle_sidebar_item_focus(
|
||||||
&mut self,
|
&mut self,
|
||||||
action: &ToggleSidebarItemFocus,
|
side: Side,
|
||||||
|
item_index: usize,
|
||||||
cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
) {
|
) {
|
||||||
let sidebar = match action.side {
|
let sidebar = match side {
|
||||||
Side::Left => &mut self.left_sidebar,
|
Side::Left => &mut self.left_sidebar,
|
||||||
Side::Right => &mut self.right_sidebar,
|
Side::Right => &mut self.right_sidebar,
|
||||||
};
|
};
|
||||||
let active_item = sidebar.update(cx, |sidebar, cx| {
|
let active_item = sidebar.update(cx, |sidebar, cx| {
|
||||||
sidebar.activate_item(action.item_index, cx);
|
sidebar.set_open(true, cx);
|
||||||
|
sidebar.activate_item(item_index, cx);
|
||||||
sidebar.active_item().cloned()
|
sidebar.active_item().cloned()
|
||||||
});
|
});
|
||||||
if let Some(active_item) = active_item {
|
if let Some(active_item) = active_item {
|
||||||
|
@ -1405,6 +1440,15 @@ impl Workspace {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn activate_pane_at_index(&mut self, action: &ActivatePane, cx: &mut ViewContext<Self>) {
|
||||||
|
let panes = self.center.panes();
|
||||||
|
if let Some(pane) = panes.get(action.0).map(|p| (*p).clone()) {
|
||||||
|
self.activate_pane(pane, cx);
|
||||||
|
} else {
|
||||||
|
self.split_pane(self.active_pane.clone(), SplitDirection::Right, cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn activate_next_pane(&mut self, cx: &mut ViewContext<Self>) {
|
pub fn activate_next_pane(&mut self, cx: &mut ViewContext<Self>) {
|
||||||
let next_pane = {
|
let next_pane = {
|
||||||
let panes = self.center.panes();
|
let panes = self.center.panes();
|
||||||
|
@ -2481,13 +2525,7 @@ pub fn open_paths(
|
||||||
let mut workspace = Workspace::new(project, cx);
|
let mut workspace = Workspace::new(project, cx);
|
||||||
(app_state.initialize_workspace)(&mut workspace, &app_state, cx);
|
(app_state.initialize_workspace)(&mut workspace, &app_state, cx);
|
||||||
if contains_directory {
|
if contains_directory {
|
||||||
workspace.toggle_sidebar_item(
|
workspace.toggle_sidebar(Side::Left, cx);
|
||||||
&ToggleSidebarItem {
|
|
||||||
side: Side::Left,
|
|
||||||
item_index: 0,
|
|
||||||
},
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
workspace
|
workspace
|
||||||
})
|
})
|
||||||
|
|
|
@ -15,6 +15,7 @@ name = "Zed"
|
||||||
path = "src/main.rs"
|
path = "src/main.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
activity_indicator = { path = "../activity_indicator" }
|
||||||
assets = { path = "../assets" }
|
assets = { path = "../assets" }
|
||||||
auto_update = { path = "../auto_update" }
|
auto_update = { path = "../auto_update" }
|
||||||
breadcrumbs = { path = "../breadcrumbs" }
|
breadcrumbs = { path = "../breadcrumbs" }
|
||||||
|
@ -37,7 +38,6 @@ gpui = { path = "../gpui" }
|
||||||
journal = { path = "../journal" }
|
journal = { path = "../journal" }
|
||||||
language = { path = "../language" }
|
language = { path = "../language" }
|
||||||
lsp = { path = "../lsp" }
|
lsp = { path = "../lsp" }
|
||||||
lsp_status = { path = "../lsp_status" }
|
|
||||||
outline = { path = "../outline" }
|
outline = { path = "../outline" }
|
||||||
project = { path = "../project" }
|
project = { path = "../project" }
|
||||||
project_panel = { path = "../project_panel" }
|
project_panel = { path = "../project_panel" }
|
||||||
|
@ -46,6 +46,7 @@ rpc = { path = "../rpc" }
|
||||||
settings = { path = "../settings" }
|
settings = { path = "../settings" }
|
||||||
sum_tree = { path = "../sum_tree" }
|
sum_tree = { path = "../sum_tree" }
|
||||||
text = { path = "../text" }
|
text = { path = "../text" }
|
||||||
|
terminal = { path = "../terminal" }
|
||||||
theme = { path = "../theme" }
|
theme = { path = "../theme" }
|
||||||
theme_selector = { path = "../theme_selector" }
|
theme_selector = { path = "../theme_selector" }
|
||||||
util = { path = "../util" }
|
util = { path = "../util" }
|
||||||
|
|
|
@ -36,6 +36,7 @@ use std::{
|
||||||
thread,
|
thread,
|
||||||
time::Duration,
|
time::Duration,
|
||||||
};
|
};
|
||||||
|
use terminal;
|
||||||
use theme::{ThemeRegistry, DEFAULT_THEME_NAME};
|
use theme::{ThemeRegistry, DEFAULT_THEME_NAME};
|
||||||
use util::{ResultExt, TryFutureExt};
|
use util::{ResultExt, TryFutureExt};
|
||||||
use workspace::{self, AppState, NewFile, OpenPaths};
|
use workspace::{self, AppState, NewFile, OpenPaths};
|
||||||
|
@ -181,6 +182,7 @@ fn main() {
|
||||||
diagnostics::init(cx);
|
diagnostics::init(cx);
|
||||||
search::init(cx);
|
search::init(cx);
|
||||||
vim::init(cx);
|
vim::init(cx);
|
||||||
|
terminal::init(cx);
|
||||||
|
|
||||||
let db = cx.background().block(db);
|
let db = cx.background().block(db);
|
||||||
let (settings_file, keymap_file) = cx.background().block(config_files).unwrap();
|
let (settings_file, keymap_file) = cx.background().block(config_files).unwrap();
|
||||||
|
|
|
@ -187,11 +187,42 @@ pub fn menus() -> Vec<Menu<'static>> {
|
||||||
},
|
},
|
||||||
MenuItem::Separator,
|
MenuItem::Separator,
|
||||||
MenuItem::Action {
|
MenuItem::Action {
|
||||||
name: "Project Browser",
|
name: "Toggle Left Sidebar",
|
||||||
action: Box::new(workspace::sidebar::ToggleSidebarItemFocus {
|
action: Box::new(workspace::ToggleLeftSidebar),
|
||||||
side: workspace::sidebar::Side::Left,
|
},
|
||||||
item_index: 0,
|
MenuItem::Action {
|
||||||
}),
|
name: "Toggle Right Sidebar",
|
||||||
|
action: Box::new(workspace::ToggleRightSidebar),
|
||||||
|
},
|
||||||
|
MenuItem::Submenu(Menu {
|
||||||
|
name: "Editor Layout",
|
||||||
|
items: vec![
|
||||||
|
MenuItem::Action {
|
||||||
|
name: "Split Up",
|
||||||
|
action: Box::new(workspace::SplitUp),
|
||||||
|
},
|
||||||
|
MenuItem::Action {
|
||||||
|
name: "Split Down",
|
||||||
|
action: Box::new(workspace::SplitDown),
|
||||||
|
},
|
||||||
|
MenuItem::Action {
|
||||||
|
name: "Split Left",
|
||||||
|
action: Box::new(workspace::SplitLeft),
|
||||||
|
},
|
||||||
|
MenuItem::Action {
|
||||||
|
name: "Split Right",
|
||||||
|
action: Box::new(workspace::SplitRight),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
MenuItem::Separator,
|
||||||
|
MenuItem::Action {
|
||||||
|
name: "Project Panel",
|
||||||
|
action: Box::new(project_panel::Toggle),
|
||||||
|
},
|
||||||
|
MenuItem::Action {
|
||||||
|
name: "Contacts Panel",
|
||||||
|
action: Box::new(contacts_panel::Toggle),
|
||||||
},
|
},
|
||||||
MenuItem::Action {
|
MenuItem::Action {
|
||||||
name: "Command Palette",
|
name: "Command Palette",
|
||||||
|
|
|
@ -34,7 +34,7 @@ use std::{
|
||||||
};
|
};
|
||||||
use util::ResultExt;
|
use util::ResultExt;
|
||||||
pub use workspace;
|
pub use workspace;
|
||||||
use workspace::{AppState, Workspace};
|
use workspace::{sidebar::Side, AppState, Workspace};
|
||||||
|
|
||||||
#[derive(Deserialize, Clone, PartialEq)]
|
#[derive(Deserialize, Clone, PartialEq)]
|
||||||
struct OpenBrowser {
|
struct OpenBrowser {
|
||||||
|
@ -97,6 +97,7 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::MutableAppContext) {
|
||||||
cx.add_action({
|
cx.add_action({
|
||||||
let app_state = app_state.clone();
|
let app_state = app_state.clone();
|
||||||
move |_: &mut Workspace, _: &OpenSettings, cx: &mut ViewContext<Workspace>| {
|
move |_: &mut Workspace, _: &OpenSettings, cx: &mut ViewContext<Workspace>| {
|
||||||
|
println!("open settings");
|
||||||
open_config_file(&SETTINGS_PATH, app_state.clone(), cx);
|
open_config_file(&SETTINGS_PATH, app_state.clone(), cx);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -128,8 +129,18 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::MutableAppContext) {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
cx.add_action(
|
||||||
|
|workspace: &mut Workspace, _: &project_panel::Toggle, cx: &mut ViewContext<Workspace>| {
|
||||||
|
workspace.toggle_sidebar_item_focus(Side::Left, 0, cx);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
cx.add_action(
|
||||||
|
|workspace: &mut Workspace, _: &contacts_panel::Toggle, cx: &mut ViewContext<Workspace>| {
|
||||||
|
workspace.toggle_sidebar_item_focus(Side::Right, 0, cx);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
lsp_status::init(cx);
|
activity_indicator::init(cx);
|
||||||
settings::KeymapFileContent::load_defaults(cx);
|
settings::KeymapFileContent::load_defaults(cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -212,15 +223,14 @@ pub fn initialize_workspace(
|
||||||
|
|
||||||
let diagnostic_summary =
|
let diagnostic_summary =
|
||||||
cx.add_view(|cx| diagnostics::items::DiagnosticIndicator::new(workspace.project(), cx));
|
cx.add_view(|cx| diagnostics::items::DiagnosticIndicator::new(workspace.project(), cx));
|
||||||
let lsp_status = lsp_status::LspStatusItem::new(workspace, app_state.languages.clone(), cx);
|
let activity_indicator =
|
||||||
|
activity_indicator::ActivityIndicator::new(workspace, app_state.languages.clone(), cx);
|
||||||
let cursor_position = cx.add_view(|_| editor::items::CursorPosition::new());
|
let cursor_position = cx.add_view(|_| editor::items::CursorPosition::new());
|
||||||
let auto_update = cx.add_view(|cx| auto_update::AutoUpdateIndicator::new(cx));
|
|
||||||
let feedback_link = cx.add_view(|_| feedback::FeedbackLink);
|
let feedback_link = cx.add_view(|_| feedback::FeedbackLink);
|
||||||
workspace.status_bar().update(cx, |status_bar, cx| {
|
workspace.status_bar().update(cx, |status_bar, cx| {
|
||||||
status_bar.add_left_item(diagnostic_summary, cx);
|
status_bar.add_left_item(diagnostic_summary, cx);
|
||||||
status_bar.add_left_item(lsp_status, cx);
|
status_bar.add_left_item(activity_indicator, cx);
|
||||||
status_bar.add_right_item(cursor_position, cx);
|
status_bar.add_right_item(cursor_position, cx);
|
||||||
status_bar.add_right_item(auto_update, cx);
|
|
||||||
status_bar.add_right_item(feedback_link, cx);
|
status_bar.add_right_item(feedback_link, cx);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -429,7 +439,7 @@ mod tests {
|
||||||
let workspace_1 = cx.root_view::<Workspace>(cx.window_ids()[0]).unwrap();
|
let workspace_1 = cx.root_view::<Workspace>(cx.window_ids()[0]).unwrap();
|
||||||
workspace_1.update(cx, |workspace, cx| {
|
workspace_1.update(cx, |workspace, cx| {
|
||||||
assert_eq!(workspace.worktrees(cx).count(), 2);
|
assert_eq!(workspace.worktrees(cx).count(), 2);
|
||||||
assert!(workspace.left_sidebar().read(cx).active_item().is_some());
|
assert!(workspace.left_sidebar().read(cx).is_open());
|
||||||
assert!(workspace.active_pane().is_focused(cx));
|
assert!(workspace.active_pane().is_focused(cx));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -14,6 +14,7 @@ import projectDiagnostics from "./projectDiagnostics";
|
||||||
import contactNotification from "./contactNotification";
|
import contactNotification from "./contactNotification";
|
||||||
import updateNotification from "./updateNotification";
|
import updateNotification from "./updateNotification";
|
||||||
import tooltip from "./tooltip";
|
import tooltip from "./tooltip";
|
||||||
|
import terminal from "./terminal";
|
||||||
|
|
||||||
export const panel = {
|
export const panel = {
|
||||||
padding: { top: 12, bottom: 12 },
|
padding: { top: 12, bottom: 12 },
|
||||||
|
@ -41,5 +42,6 @@ export default function app(theme: Theme): Object {
|
||||||
contactNotification: contactNotification(theme),
|
contactNotification: contactNotification(theme),
|
||||||
updateNotification: updateNotification(theme),
|
updateNotification: updateNotification(theme),
|
||||||
tooltip: tooltip(theme),
|
tooltip: tooltip(theme),
|
||||||
|
terminal: terminal(theme),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
35
styles/src/styleTree/terminal.ts
Normal file
35
styles/src/styleTree/terminal.ts
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
import Theme from "../themes/common/theme";
|
||||||
|
|
||||||
|
export default function terminal(theme: Theme) {
|
||||||
|
return {
|
||||||
|
black: theme.ramps.neutral(0).hex(),
|
||||||
|
red: theme.ramps.red(0.5).hex(),
|
||||||
|
green: theme.ramps.green(0.5).hex(),
|
||||||
|
yellow: theme.ramps.yellow(0.5).hex(),
|
||||||
|
blue: theme.ramps.blue(0.5).hex(),
|
||||||
|
magenta: theme.ramps.magenta(0.5).hex(),
|
||||||
|
cyan: theme.ramps.cyan(0.5).hex(),
|
||||||
|
white: theme.ramps.neutral(7).hex(),
|
||||||
|
brightBlack: theme.ramps.neutral(2).hex(),
|
||||||
|
brightRed: theme.ramps.red(0.25).hex(),
|
||||||
|
brightGreen: theme.ramps.green(0.25).hex(),
|
||||||
|
brightYellow: theme.ramps.yellow(0.25).hex(),
|
||||||
|
brightBlue: theme.ramps.blue(0.25).hex(),
|
||||||
|
brightMagenta: theme.ramps.magenta(0.25).hex(),
|
||||||
|
brightCyan: theme.ramps.cyan(0.25).hex(),
|
||||||
|
brightWhite: theme.ramps.neutral(7).hex(),
|
||||||
|
foreground: theme.ramps.neutral(7).hex(),
|
||||||
|
background: theme.ramps.neutral(0).hex(),
|
||||||
|
cursor: theme.ramps.neutral(7).hex(),
|
||||||
|
dimBlack: theme.ramps.neutral(7).hex(),
|
||||||
|
dimRed: theme.ramps.red(0.75).hex(),
|
||||||
|
dimGreen: theme.ramps.green(0.75).hex(),
|
||||||
|
dimYellow: theme.ramps.yellow(0.75).hex(),
|
||||||
|
dimBlue: theme.ramps.blue(0.75).hex(),
|
||||||
|
dimMagenta: theme.ramps.magenta(0.75).hex(),
|
||||||
|
dimCyan: theme.ramps.cyan(0.75).hex(),
|
||||||
|
dimWhite: theme.ramps.neutral(5).hex(),
|
||||||
|
brightForeground: theme.ramps.neutral(7).hex(),
|
||||||
|
dimForeground: theme.ramps.neutral(0).hex(),
|
||||||
|
};
|
||||||
|
}
|
|
@ -13,15 +13,25 @@ export function colorRamp(color: Color): Scale {
|
||||||
export function createTheme(
|
export function createTheme(
|
||||||
name: string,
|
name: string,
|
||||||
isLight: boolean,
|
isLight: boolean,
|
||||||
ramps: { [rampName: string]: Scale },
|
color_ramps: { [rampName: string]: Scale },
|
||||||
): Theme {
|
): Theme {
|
||||||
|
let ramps: typeof color_ramps = {};
|
||||||
|
// Chromajs mutates the underlying ramp when you call domain. This causes problems because
|
||||||
|
// we now store the ramps object in the theme so that we can pull colors out of them.
|
||||||
|
// So instead of calling domain and storing the result, we have to construct new ramps for each
|
||||||
|
// theme so that we don't modify the passed in ramps.
|
||||||
|
// This combined with an error in the type definitions for chroma js means we have to cast the colors
|
||||||
|
// function to any in order to get the colors back out from the original ramps.
|
||||||
if (isLight) {
|
if (isLight) {
|
||||||
for (var rampName in ramps) {
|
for (var rampName in color_ramps) {
|
||||||
ramps[rampName] = ramps[rampName].domain([1, 0]);
|
ramps[rampName] = chroma.scale((color_ramps[rampName].colors as any)()).domain([1, 0]);
|
||||||
}
|
}
|
||||||
ramps.neutral = ramps.neutral.domain([7, 0]);
|
ramps.neutral = chroma.scale((color_ramps.neutral.colors as any)()).domain([7, 0]);
|
||||||
} else {
|
} else {
|
||||||
ramps.neutral = ramps.neutral.domain([0, 7]);
|
for (var rampName in color_ramps) {
|
||||||
|
ramps[rampName] = chroma.scale((color_ramps[rampName].colors as any)()).domain([0, 1]);
|
||||||
|
}
|
||||||
|
ramps.neutral = chroma.scale((color_ramps.neutral.colors as any)()).domain([0, 7]);
|
||||||
}
|
}
|
||||||
|
|
||||||
let blend = isLight ? 0.12 : 0.24;
|
let blend = isLight ? 0.12 : 0.24;
|
||||||
|
@ -237,6 +247,7 @@ export function createTheme(
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name,
|
name,
|
||||||
|
isLight,
|
||||||
backgroundColor,
|
backgroundColor,
|
||||||
borderColor,
|
borderColor,
|
||||||
textColor,
|
textColor,
|
||||||
|
@ -245,5 +256,6 @@ export function createTheme(
|
||||||
syntax,
|
syntax,
|
||||||
player,
|
player,
|
||||||
shadow,
|
shadow,
|
||||||
|
ramps,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { Scale } from "chroma-js";
|
||||||
import { FontWeight } from "../../common";
|
import { FontWeight } from "../../common";
|
||||||
import { withOpacity } from "../../utils/color";
|
import { withOpacity } from "../../utils/color";
|
||||||
|
|
||||||
|
@ -60,6 +61,7 @@ export interface Syntax {
|
||||||
|
|
||||||
export default interface Theme {
|
export default interface Theme {
|
||||||
name: string;
|
name: string;
|
||||||
|
isLight: boolean,
|
||||||
backgroundColor: {
|
backgroundColor: {
|
||||||
// Basically just Title Bar
|
// Basically just Title Bar
|
||||||
// Lowest background level
|
// Lowest background level
|
||||||
|
@ -155,4 +157,5 @@ export default interface Theme {
|
||||||
8: Player;
|
8: Player;
|
||||||
},
|
},
|
||||||
shadow: string;
|
shadow: string;
|
||||||
|
ramps: { [rampName: string]: Scale };
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue