Compare commits
39 Commits
paint-orde
...
bounds-tre
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
74ed2f7f70 | ||
|
|
eba5db3a6e | ||
|
|
cd6bdd8b1c | ||
|
|
a3ce933b04 | ||
|
|
816c48b7d6 | ||
|
|
42ac9880c6 | ||
|
|
65318cb6ac | ||
|
|
71557f3eb3 | ||
|
|
a588f674db | ||
|
|
50dd38bd02 | ||
|
|
caa156ab13 | ||
|
|
a82f4857f4 | ||
|
|
0de8672044 | ||
|
|
cc8e3c2286 | ||
|
|
347f68887f | ||
|
|
a475d8640f | ||
|
|
991c9ec441 | ||
|
|
250df707bf | ||
|
|
ba6b319046 | ||
|
|
bd94a0e921 | ||
|
|
40bbd0031d | ||
|
|
946f4a312a | ||
|
|
af06063d31 | ||
|
|
5c4f3c0cea | ||
|
|
c6826a61a0 | ||
|
|
fa2c92d190 | ||
|
|
20b10fdca9 | ||
|
|
4f40d3c801 | ||
|
|
b716035d02 | ||
|
|
94bc216bbd | ||
|
|
95d5ea7edc | ||
|
|
aff858bd00 | ||
|
|
583d85cf66 | ||
|
|
36586b77ec | ||
|
|
587788b9a0 | ||
|
|
6f36527bc6 | ||
|
|
aa34e306f7 | ||
|
|
e5d971f4c7 | ||
|
|
38c3a93f0c |
64
Cargo.lock
generated
64
Cargo.lock
generated
@@ -770,10 +770,12 @@ dependencies = [
|
||||
"anyhow",
|
||||
"client",
|
||||
"db",
|
||||
"editor",
|
||||
"gpui",
|
||||
"isahc",
|
||||
"lazy_static",
|
||||
"log",
|
||||
"markdown_preview",
|
||||
"menu",
|
||||
"project",
|
||||
"release_channel",
|
||||
@@ -1349,7 +1351,7 @@ dependencies = [
|
||||
"rustc-hash",
|
||||
"shlex",
|
||||
"syn 2.0.48",
|
||||
"which",
|
||||
"which 4.4.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1909,6 +1911,7 @@ dependencies = [
|
||||
"async-recursion 0.3.2",
|
||||
"async-tungstenite",
|
||||
"chrono",
|
||||
"clock",
|
||||
"collections",
|
||||
"db",
|
||||
"feature_flags",
|
||||
@@ -1946,6 +1949,8 @@ dependencies = [
|
||||
name = "clock"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"parking_lot 0.11.2",
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
@@ -2091,6 +2096,7 @@ dependencies = [
|
||||
"collections",
|
||||
"db",
|
||||
"editor",
|
||||
"extensions_ui",
|
||||
"feature_flags",
|
||||
"feedback",
|
||||
"futures 0.3.28",
|
||||
@@ -3675,6 +3681,7 @@ dependencies = [
|
||||
"text",
|
||||
"time",
|
||||
"util",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4118,6 +4125,7 @@ dependencies = [
|
||||
"pathfinder_geometry",
|
||||
"png",
|
||||
"postage",
|
||||
"profiling",
|
||||
"rand 0.8.5",
|
||||
"raw-window-handle 0.5.2",
|
||||
"raw-window-handle 0.6.0",
|
||||
@@ -4340,11 +4348,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "home"
|
||||
version = "0.5.5"
|
||||
version = "0.5.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5444c27eef6923071f7ebcc33e3444508466a76f7a2b93da00ed6e19f30c1ddb"
|
||||
checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5"
|
||||
dependencies = [
|
||||
"windows-sys 0.48.0",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6860,6 +6868,25 @@ dependencies = [
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "profiling"
|
||||
version = "1.0.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "43d84d1d7a6ac92673717f9f6d1518374ef257669c24ebc5ac25d5033828be58"
|
||||
dependencies = [
|
||||
"profiling-procmacros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "profiling-procmacros"
|
||||
version = "1.0.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8021cf59c8ec9c432cfc2526ac6b8aa508ecaf29cd415f271b8406c1b851c3fd"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn 2.0.48",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
@@ -6915,6 +6942,7 @@ dependencies = [
|
||||
"toml 0.8.10",
|
||||
"unindent",
|
||||
"util",
|
||||
"which 6.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7024,7 +7052,7 @@ dependencies = [
|
||||
"prost-types 0.9.0",
|
||||
"regex",
|
||||
"tempfile",
|
||||
"which",
|
||||
"which 4.4.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7316,6 +7344,7 @@ dependencies = [
|
||||
"fuzzy",
|
||||
"gpui",
|
||||
"language",
|
||||
"menu",
|
||||
"ordered-float 2.10.0",
|
||||
"picker",
|
||||
"postage",
|
||||
@@ -8519,9 +8548,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "1.3.0"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||
checksum = "a7cee0529a6d40f580e7a5e6c495c8fbfe21b7b52795ed4bb5e62cdf92bc6380"
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook"
|
||||
@@ -10374,8 +10403,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tree-sitter-purescript"
|
||||
version = "1.0.0"
|
||||
source = "git+https://github.com/ivanmoreau/tree-sitter-purescript?rev=a37140f0c7034977b90faa73c94fcb8a5e45ed08#a37140f0c7034977b90faa73c94fcb8a5e45ed08"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/postsolar/tree-sitter-purescript?rev=v0.1.0#0554811a512b9cec08b5a83ce9096eb22da18213"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"tree-sitter",
|
||||
@@ -10900,6 +10929,7 @@ dependencies = [
|
||||
"project",
|
||||
"regex",
|
||||
"release_channel",
|
||||
"schemars",
|
||||
"search",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
@@ -11396,6 +11426,19 @@ dependencies = [
|
||||
"rustix 0.38.30",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "which"
|
||||
version = "6.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7fa5e0c10bf77f44aac573e498d1a82d5fbd5e91f6fc0a99e7be4b38e85e101c"
|
||||
dependencies = [
|
||||
"either",
|
||||
"home",
|
||||
"once_cell",
|
||||
"rustix 0.38.30",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "whoami"
|
||||
version = "1.4.1"
|
||||
@@ -11707,6 +11750,7 @@ dependencies = [
|
||||
"bincode",
|
||||
"call",
|
||||
"client",
|
||||
"clock",
|
||||
"collections",
|
||||
"db",
|
||||
"derive_more",
|
||||
@@ -11931,6 +11975,7 @@ dependencies = [
|
||||
"chrono",
|
||||
"cli",
|
||||
"client",
|
||||
"clock",
|
||||
"collab_ui",
|
||||
"collections",
|
||||
"command_palette",
|
||||
@@ -11974,6 +12019,7 @@ dependencies = [
|
||||
"outline",
|
||||
"parking_lot 0.11.2",
|
||||
"postage",
|
||||
"profiling",
|
||||
"project",
|
||||
"project_panel",
|
||||
"project_symbols",
|
||||
|
||||
@@ -203,6 +203,7 @@ linkify = "0.10.0"
|
||||
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
|
||||
ordered-float = "2.1.1"
|
||||
parking_lot = "0.11.1"
|
||||
profiling = "1"
|
||||
postage = { version = "0.5", features = ["futures-traits"] }
|
||||
pretty_assertions = "1.3.0"
|
||||
prost = "0.8"
|
||||
@@ -261,7 +262,7 @@ tree-sitter-ocaml = { git = "https://github.com/tree-sitter/tree-sitter-ocaml",
|
||||
tree-sitter-php = "0.21.1"
|
||||
tree-sitter-prisma-io = { git = "https://github.com/victorhqc/tree-sitter-prisma" }
|
||||
tree-sitter-proto = { git = "https://github.com/rewinfrey/tree-sitter-proto", rev = "36d54f288aee112f13a67b550ad32634d0c2cb52" }
|
||||
tree-sitter-purescript = { git = "https://github.com/ivanmoreau/tree-sitter-purescript", rev = "a37140f0c7034977b90faa73c94fcb8a5e45ed08" }
|
||||
tree-sitter-purescript = { git = "https://github.com/postsolar/tree-sitter-purescript", rev = "v0.1.0" }
|
||||
tree-sitter-python = "0.20.2"
|
||||
tree-sitter-racket = { git = "https://github.com/zed-industries/tree-sitter-racket", rev = "eb010cf2c674c6fd9a6316a84e28ef90190fe51a" }
|
||||
tree-sitter-ruby = "0.20.0"
|
||||
@@ -278,6 +279,7 @@ unindent = "0.1.7"
|
||||
url = "2.2"
|
||||
uuid = { version = "1.1.2", features = ["v4"] }
|
||||
wasmtime = "16"
|
||||
which = "6.0.0"
|
||||
sys-locale = "0.3.1"
|
||||
|
||||
[patch.crates-io]
|
||||
|
||||
@@ -531,7 +531,8 @@
|
||||
"alt-cmd-shift-c": "project_panel::CopyRelativePath",
|
||||
"f2": "project_panel::Rename",
|
||||
"enter": "project_panel::Rename",
|
||||
"backspace": "project_panel::Delete",
|
||||
"delete": "project_panel::Delete",
|
||||
"cmd-backspace": "project_panel::Delete",
|
||||
"alt-cmd-r": "project_panel::RevealInFinder",
|
||||
"alt-shift-f": "project_panel::NewSearchInDirectory"
|
||||
}
|
||||
|
||||
23
assets/keymaps/storybook.json
Normal file
23
assets/keymaps/storybook.json
Normal file
@@ -0,0 +1,23 @@
|
||||
[
|
||||
// Standard macOS bindings
|
||||
{
|
||||
"bindings": {
|
||||
"up": "menu::SelectPrev",
|
||||
"pageup": "menu::SelectFirst",
|
||||
"shift-pageup": "menu::SelectFirst",
|
||||
"ctrl-p": "menu::SelectPrev",
|
||||
"down": "menu::SelectNext",
|
||||
"pagedown": "menu::SelectLast",
|
||||
"shift-pagedown": "menu::SelectFirst",
|
||||
"ctrl-n": "menu::SelectNext",
|
||||
"cmd-up": "menu::SelectFirst",
|
||||
"cmd-down": "menu::SelectLast",
|
||||
"enter": "menu::Confirm",
|
||||
"ctrl-enter": "menu::ShowContextMenu",
|
||||
"cmd-enter": "menu::SecondaryConfirm",
|
||||
"escape": "menu::Cancel",
|
||||
"ctrl-c": "menu::Cancel",
|
||||
"cmd-q": "storybook::Quit"
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -101,8 +101,14 @@
|
||||
"ctrl-o": "pane::GoBack",
|
||||
"ctrl-i": "pane::GoForward",
|
||||
"ctrl-]": "editor::GoToDefinition",
|
||||
"escape": ["vim::SwitchMode", "Normal"],
|
||||
"ctrl-[": ["vim::SwitchMode", "Normal"],
|
||||
"escape": [
|
||||
"vim::SwitchMode",
|
||||
"Normal"
|
||||
],
|
||||
"ctrl-[": [
|
||||
"vim::SwitchMode",
|
||||
"Normal"
|
||||
],
|
||||
"v": "vim::ToggleVisual",
|
||||
"shift-v": "vim::ToggleVisualLine",
|
||||
"ctrl-v": "vim::ToggleVisualBlock",
|
||||
@@ -235,36 +241,123 @@
|
||||
}
|
||||
],
|
||||
// Count support
|
||||
"1": ["vim::Number", 1],
|
||||
"2": ["vim::Number", 2],
|
||||
"3": ["vim::Number", 3],
|
||||
"4": ["vim::Number", 4],
|
||||
"5": ["vim::Number", 5],
|
||||
"6": ["vim::Number", 6],
|
||||
"7": ["vim::Number", 7],
|
||||
"8": ["vim::Number", 8],
|
||||
"9": ["vim::Number", 9],
|
||||
"1": [
|
||||
"vim::Number",
|
||||
1
|
||||
],
|
||||
"2": [
|
||||
"vim::Number",
|
||||
2
|
||||
],
|
||||
"3": [
|
||||
"vim::Number",
|
||||
3
|
||||
],
|
||||
"4": [
|
||||
"vim::Number",
|
||||
4
|
||||
],
|
||||
"5": [
|
||||
"vim::Number",
|
||||
5
|
||||
],
|
||||
"6": [
|
||||
"vim::Number",
|
||||
6
|
||||
],
|
||||
"7": [
|
||||
"vim::Number",
|
||||
7
|
||||
],
|
||||
"8": [
|
||||
"vim::Number",
|
||||
8
|
||||
],
|
||||
"9": [
|
||||
"vim::Number",
|
||||
9
|
||||
],
|
||||
// window related commands (ctrl-w X)
|
||||
"ctrl-w left": ["workspace::ActivatePaneInDirection", "Left"],
|
||||
"ctrl-w right": ["workspace::ActivatePaneInDirection", "Right"],
|
||||
"ctrl-w up": ["workspace::ActivatePaneInDirection", "Up"],
|
||||
"ctrl-w down": ["workspace::ActivatePaneInDirection", "Down"],
|
||||
"ctrl-w h": ["workspace::ActivatePaneInDirection", "Left"],
|
||||
"ctrl-w l": ["workspace::ActivatePaneInDirection", "Right"],
|
||||
"ctrl-w k": ["workspace::ActivatePaneInDirection", "Up"],
|
||||
"ctrl-w j": ["workspace::ActivatePaneInDirection", "Down"],
|
||||
"ctrl-w ctrl-h": ["workspace::ActivatePaneInDirection", "Left"],
|
||||
"ctrl-w ctrl-l": ["workspace::ActivatePaneInDirection", "Right"],
|
||||
"ctrl-w ctrl-k": ["workspace::ActivatePaneInDirection", "Up"],
|
||||
"ctrl-w ctrl-j": ["workspace::ActivatePaneInDirection", "Down"],
|
||||
"ctrl-w shift-left": ["workspace::SwapPaneInDirection", "Left"],
|
||||
"ctrl-w shift-right": ["workspace::SwapPaneInDirection", "Right"],
|
||||
"ctrl-w shift-up": ["workspace::SwapPaneInDirection", "Up"],
|
||||
"ctrl-w shift-down": ["workspace::SwapPaneInDirection", "Down"],
|
||||
"ctrl-w shift-h": ["workspace::SwapPaneInDirection", "Left"],
|
||||
"ctrl-w shift-l": ["workspace::SwapPaneInDirection", "Right"],
|
||||
"ctrl-w shift-k": ["workspace::SwapPaneInDirection", "Up"],
|
||||
"ctrl-w shift-j": ["workspace::SwapPaneInDirection", "Down"],
|
||||
"ctrl-w left": [
|
||||
"workspace::ActivatePaneInDirection",
|
||||
"Left"
|
||||
],
|
||||
"ctrl-w right": [
|
||||
"workspace::ActivatePaneInDirection",
|
||||
"Right"
|
||||
],
|
||||
"ctrl-w up": [
|
||||
"workspace::ActivatePaneInDirection",
|
||||
"Up"
|
||||
],
|
||||
"ctrl-w down": [
|
||||
"workspace::ActivatePaneInDirection",
|
||||
"Down"
|
||||
],
|
||||
"ctrl-w h": [
|
||||
"workspace::ActivatePaneInDirection",
|
||||
"Left"
|
||||
],
|
||||
"ctrl-w l": [
|
||||
"workspace::ActivatePaneInDirection",
|
||||
"Right"
|
||||
],
|
||||
"ctrl-w k": [
|
||||
"workspace::ActivatePaneInDirection",
|
||||
"Up"
|
||||
],
|
||||
"ctrl-w j": [
|
||||
"workspace::ActivatePaneInDirection",
|
||||
"Down"
|
||||
],
|
||||
"ctrl-w ctrl-h": [
|
||||
"workspace::ActivatePaneInDirection",
|
||||
"Left"
|
||||
],
|
||||
"ctrl-w ctrl-l": [
|
||||
"workspace::ActivatePaneInDirection",
|
||||
"Right"
|
||||
],
|
||||
"ctrl-w ctrl-k": [
|
||||
"workspace::ActivatePaneInDirection",
|
||||
"Up"
|
||||
],
|
||||
"ctrl-w ctrl-j": [
|
||||
"workspace::ActivatePaneInDirection",
|
||||
"Down"
|
||||
],
|
||||
"ctrl-w shift-left": [
|
||||
"workspace::SwapPaneInDirection",
|
||||
"Left"
|
||||
],
|
||||
"ctrl-w shift-right": [
|
||||
"workspace::SwapPaneInDirection",
|
||||
"Right"
|
||||
],
|
||||
"ctrl-w shift-up": [
|
||||
"workspace::SwapPaneInDirection",
|
||||
"Up"
|
||||
],
|
||||
"ctrl-w shift-down": [
|
||||
"workspace::SwapPaneInDirection",
|
||||
"Down"
|
||||
],
|
||||
"ctrl-w shift-h": [
|
||||
"workspace::SwapPaneInDirection",
|
||||
"Left"
|
||||
],
|
||||
"ctrl-w shift-l": [
|
||||
"workspace::SwapPaneInDirection",
|
||||
"Right"
|
||||
],
|
||||
"ctrl-w shift-k": [
|
||||
"workspace::SwapPaneInDirection",
|
||||
"Up"
|
||||
],
|
||||
"ctrl-w shift-j": [
|
||||
"workspace::SwapPaneInDirection",
|
||||
"Down"
|
||||
],
|
||||
"ctrl-w g t": "pane::ActivateNextItem",
|
||||
"ctrl-w ctrl-g t": "pane::ActivateNextItem",
|
||||
"ctrl-w g shift-t": "pane::ActivatePrevItem",
|
||||
@@ -286,8 +379,14 @@
|
||||
"ctrl-w ctrl-q": "pane::CloseAllItems",
|
||||
"ctrl-w o": "workspace::CloseInactiveTabsAndPanes",
|
||||
"ctrl-w ctrl-o": "workspace::CloseInactiveTabsAndPanes",
|
||||
"ctrl-w n": ["workspace::NewFileInDirection", "Up"],
|
||||
"ctrl-w ctrl-n": ["workspace::NewFileInDirection", "Up"],
|
||||
"ctrl-w n": [
|
||||
"workspace::NewFileInDirection",
|
||||
"Up"
|
||||
],
|
||||
"ctrl-w ctrl-n": [
|
||||
"workspace::NewFileInDirection",
|
||||
"Up"
|
||||
],
|
||||
"-": "pane::RevealInProjectPanel"
|
||||
}
|
||||
},
|
||||
@@ -303,12 +402,21 @@
|
||||
"context": "Editor && vim_mode == normal && vim_operator == none && !VimWaiting",
|
||||
"bindings": {
|
||||
".": "vim::Repeat",
|
||||
"c": ["vim::PushOperator", "Change"],
|
||||
"c": [
|
||||
"vim::PushOperator",
|
||||
"Change"
|
||||
],
|
||||
"shift-c": "vim::ChangeToEndOfLine",
|
||||
"d": ["vim::PushOperator", "Delete"],
|
||||
"d": [
|
||||
"vim::PushOperator",
|
||||
"Delete"
|
||||
],
|
||||
"shift-d": "vim::DeleteToEndOfLine",
|
||||
"shift-j": "vim::JoinLines",
|
||||
"y": ["vim::PushOperator", "Yank"],
|
||||
"y": [
|
||||
"vim::PushOperator",
|
||||
"Yank"
|
||||
],
|
||||
"shift-y": "vim::YankLine",
|
||||
"i": "vim::InsertBefore",
|
||||
"shift-i": "vim::InsertFirstNonWhitespace",
|
||||
@@ -339,7 +447,10 @@
|
||||
],
|
||||
"*": "vim::MoveToNext",
|
||||
"#": "vim::MoveToPrev",
|
||||
"r": ["vim::PushOperator", "Replace"],
|
||||
"r": [
|
||||
"vim::PushOperator",
|
||||
"Replace"
|
||||
],
|
||||
"s": "vim::Substitute",
|
||||
"shift-s": "vim::SubstituteLine",
|
||||
"> >": "editor::Indent",
|
||||
@@ -351,7 +462,10 @@
|
||||
{
|
||||
"context": "Editor && VimCount",
|
||||
"bindings": {
|
||||
"0": ["vim::Number", 0]
|
||||
"0": [
|
||||
"vim::Number",
|
||||
0
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -454,10 +568,22 @@
|
||||
"shift-i": "vim::InsertBefore",
|
||||
"shift-a": "vim::InsertAfter",
|
||||
"shift-j": "vim::JoinLines",
|
||||
"r": ["vim::PushOperator", "Replace"],
|
||||
"ctrl-c": ["vim::SwitchMode", "Normal"],
|
||||
"escape": ["vim::SwitchMode", "Normal"],
|
||||
"ctrl-[": ["vim::SwitchMode", "Normal"],
|
||||
"r": [
|
||||
"vim::PushOperator",
|
||||
"Replace"
|
||||
],
|
||||
"ctrl-c": [
|
||||
"vim::SwitchMode",
|
||||
"Normal"
|
||||
],
|
||||
"escape": [
|
||||
"vim::SwitchMode",
|
||||
"Normal"
|
||||
],
|
||||
"ctrl-[": [
|
||||
"vim::SwitchMode",
|
||||
"Normal"
|
||||
],
|
||||
">": "editor::Indent",
|
||||
"<": "editor::Outdent",
|
||||
"i": [
|
||||
@@ -498,8 +624,14 @@
|
||||
"bindings": {
|
||||
"tab": "vim::Tab",
|
||||
"enter": "vim::Enter",
|
||||
"escape": ["vim::SwitchMode", "Normal"],
|
||||
"ctrl-[": ["vim::SwitchMode", "Normal"]
|
||||
"escape": [
|
||||
"vim::SwitchMode",
|
||||
"Normal"
|
||||
],
|
||||
"ctrl-[": [
|
||||
"vim::SwitchMode",
|
||||
"Normal"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -140,6 +140,14 @@
|
||||
// Whether to show diagnostic indicators in the scrollbar.
|
||||
"diagnostics": true
|
||||
},
|
||||
"gutter": {
|
||||
// Whether to show line numbers in the gutter.
|
||||
"line_numbers": true,
|
||||
// Whether to show code action buttons in the gutter.
|
||||
"code_actions": true,
|
||||
// Whether to show fold buttons in the gutter.
|
||||
"folds": true
|
||||
},
|
||||
// The number of lines to keep above/below the cursor when scrolling.
|
||||
"vertical_scroll_margin": 3,
|
||||
"relative_line_numbers": false,
|
||||
@@ -331,7 +339,9 @@
|
||||
"copilot": {
|
||||
// The set of glob patterns for which copilot should be disabled
|
||||
// in any matching file.
|
||||
"disabled_globs": [".env"]
|
||||
"disabled_globs": [
|
||||
".env"
|
||||
]
|
||||
},
|
||||
// Settings specific to journaling
|
||||
"journal": {
|
||||
@@ -440,7 +450,12 @@
|
||||
// Default directories to search for virtual environments, relative
|
||||
// to the current working directory. We recommend overriding this
|
||||
// in your project's settings, rather than globally.
|
||||
"directories": [".env", "env", ".venv", "venv"],
|
||||
"directories": [
|
||||
".env",
|
||||
"env",
|
||||
".venv",
|
||||
"venv"
|
||||
],
|
||||
// Can also be 'csh', 'fish', and `nushell`
|
||||
"activate_script": "default"
|
||||
}
|
||||
@@ -555,6 +570,10 @@
|
||||
// }
|
||||
// }
|
||||
},
|
||||
// Vim settings
|
||||
"vim": {
|
||||
"use_system_clipboard": "always"
|
||||
},
|
||||
// The server to connect to. If the environment variable
|
||||
// ZED_SERVER_URL is set, it will override this setting.
|
||||
"server_url": "https://zed.dev",
|
||||
|
||||
@@ -122,16 +122,13 @@ impl AssistantPanel {
|
||||
.await
|
||||
.log_err()
|
||||
.unwrap_or_default();
|
||||
let (api_url, model_name) = cx
|
||||
.update(|cx| {
|
||||
let settings = AssistantSettings::get_global(cx);
|
||||
(
|
||||
settings.openai_api_url.clone(),
|
||||
settings.default_open_ai_model.full_name().to_string(),
|
||||
)
|
||||
})
|
||||
.log_err()
|
||||
.unwrap();
|
||||
let (api_url, model_name) = cx.update(|cx| {
|
||||
let settings = AssistantSettings::get_global(cx);
|
||||
(
|
||||
settings.openai_api_url.clone(),
|
||||
settings.default_open_ai_model.full_name().to_string(),
|
||||
)
|
||||
})?;
|
||||
let completion_provider = OpenAiCompletionProvider::new(
|
||||
api_url,
|
||||
model_name,
|
||||
@@ -365,7 +362,7 @@ impl AssistantPanel {
|
||||
move |cx: &mut BlockContext| {
|
||||
measurements.set(BlockMeasurements {
|
||||
anchor_x: cx.anchor_x,
|
||||
gutter_width: cx.gutter_width,
|
||||
gutter_width: cx.gutter_dimensions.width,
|
||||
});
|
||||
inline_assistant.clone().into_any_element()
|
||||
}
|
||||
|
||||
@@ -13,10 +13,12 @@ doctest = false
|
||||
anyhow.workspace = true
|
||||
client.workspace = true
|
||||
db.workspace = true
|
||||
editor.workspace = true
|
||||
gpui.workspace = true
|
||||
isahc.workspace = true
|
||||
lazy_static.workspace = true
|
||||
log.workspace = true
|
||||
markdown_preview.workspace = true
|
||||
menu.workspace = true
|
||||
project.workspace = true
|
||||
release_channel.workspace = true
|
||||
|
||||
@@ -4,12 +4,14 @@ use anyhow::{anyhow, Context, Result};
|
||||
use client::{Client, TelemetrySettings, ZED_APP_PATH};
|
||||
use db::kvp::KEY_VALUE_STORE;
|
||||
use db::RELEASE_CHANNEL;
|
||||
use editor::{Editor, MultiBuffer};
|
||||
use gpui::{
|
||||
actions, AppContext, AsyncAppContext, Context as _, Global, Model, ModelContext,
|
||||
SemanticVersion, Task, ViewContext, VisualContext, WindowContext,
|
||||
SemanticVersion, SharedString, Task, View, ViewContext, VisualContext, WindowContext,
|
||||
};
|
||||
use isahc::AsyncBody;
|
||||
|
||||
use markdown_preview::markdown_preview_view::MarkdownPreviewView;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde_derive::Serialize;
|
||||
@@ -26,13 +28,24 @@ use std::{
|
||||
time::Duration,
|
||||
};
|
||||
use update_notification::UpdateNotification;
|
||||
use util::http::{HttpClient, ZedHttpClient};
|
||||
use util::{
|
||||
http::{HttpClient, ZedHttpClient},
|
||||
ResultExt,
|
||||
};
|
||||
use workspace::Workspace;
|
||||
|
||||
const SHOULD_SHOW_UPDATE_NOTIFICATION_KEY: &str = "auto-updater-should-show-updated-notification";
|
||||
const POLL_INTERVAL: Duration = Duration::from_secs(60 * 60);
|
||||
|
||||
actions!(auto_update, [Check, DismissErrorMessage, ViewReleaseNotes]);
|
||||
actions!(
|
||||
auto_update,
|
||||
[
|
||||
Check,
|
||||
DismissErrorMessage,
|
||||
ViewReleaseNotes,
|
||||
ViewReleaseNotesLocally
|
||||
]
|
||||
);
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct UpdateRequestBody {
|
||||
@@ -96,6 +109,12 @@ struct GlobalAutoUpdate(Option<Model<AutoUpdater>>);
|
||||
|
||||
impl Global for GlobalAutoUpdate {}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct ReleaseNotesBody {
|
||||
title: String,
|
||||
release_notes: String,
|
||||
}
|
||||
|
||||
pub fn init(http_client: Arc<ZedHttpClient>, cx: &mut AppContext) {
|
||||
AutoUpdateSetting::register(cx);
|
||||
|
||||
@@ -105,6 +124,10 @@ pub fn init(http_client: Arc<ZedHttpClient>, cx: &mut AppContext) {
|
||||
workspace.register_action(|_, action, cx| {
|
||||
view_release_notes(action, cx);
|
||||
});
|
||||
|
||||
workspace.register_action(|workspace, _: &ViewReleaseNotesLocally, cx| {
|
||||
view_release_notes_locally(workspace, cx);
|
||||
});
|
||||
})
|
||||
.detach();
|
||||
|
||||
@@ -165,6 +188,71 @@ pub fn view_release_notes(_: &ViewReleaseNotes, cx: &mut AppContext) -> Option<(
|
||||
None
|
||||
}
|
||||
|
||||
fn view_release_notes_locally(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
|
||||
let release_channel = ReleaseChannel::global(cx);
|
||||
let version = env!("CARGO_PKG_VERSION");
|
||||
|
||||
let client = client::Client::global(cx).http_client();
|
||||
let url = client.zed_url(&format!(
|
||||
"/api/release_notes/{}/{}",
|
||||
release_channel.dev_name(),
|
||||
version
|
||||
));
|
||||
|
||||
let markdown = workspace
|
||||
.app_state()
|
||||
.languages
|
||||
.language_for_name("Markdown");
|
||||
|
||||
workspace
|
||||
.with_local_workspace(cx, move |_, cx| {
|
||||
cx.spawn(|workspace, mut cx| async move {
|
||||
let markdown = markdown.await.log_err();
|
||||
let response = client.get(&url, Default::default(), true).await;
|
||||
let Some(mut response) = response.log_err() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let mut body = Vec::new();
|
||||
response.body_mut().read_to_end(&mut body).await.ok();
|
||||
|
||||
let body: serde_json::Result<ReleaseNotesBody> =
|
||||
serde_json::from_slice(body.as_slice());
|
||||
|
||||
if let Ok(body) = body {
|
||||
workspace
|
||||
.update(&mut cx, |workspace, cx| {
|
||||
let project = workspace.project().clone();
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| project.create_buffer("", markdown, cx))
|
||||
.expect("creating buffers on a local workspace always succeeds");
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.edit([(0..0, body.release_notes)], None, cx)
|
||||
});
|
||||
|
||||
let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
|
||||
let tab_description = SharedString::from(body.title.to_string());
|
||||
let editor = cx
|
||||
.new_view(|cx| Editor::for_multibuffer(buffer, Some(project), cx));
|
||||
let workspace_handle = workspace.weak_handle();
|
||||
let view: View<MarkdownPreviewView> = MarkdownPreviewView::new(
|
||||
editor,
|
||||
workspace_handle,
|
||||
Some(tab_description),
|
||||
cx,
|
||||
);
|
||||
workspace.add_item(Box::new(view.clone()), cx);
|
||||
cx.notify();
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
pub fn notify_of_any_new_update(cx: &mut ViewContext<Workspace>) -> Option<()> {
|
||||
let updater = AutoUpdater::get(cx)?;
|
||||
let version = updater.read(cx).current_version;
|
||||
|
||||
@@ -156,7 +156,7 @@ impl Room {
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
connect.await?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
if !this.read_only() {
|
||||
if this.can_use_microphone() {
|
||||
if let Some(live_kit) = &this.live_kit {
|
||||
if !live_kit.muted_by_user && !live_kit.deafened {
|
||||
return this.share_microphone(cx);
|
||||
@@ -1322,11 +1322,6 @@ impl Room {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn read_only(&self) -> bool {
|
||||
!(self.local_participant().role == proto::ChannelRole::Member
|
||||
|| self.local_participant().role == proto::ChannelRole::Admin)
|
||||
}
|
||||
|
||||
pub fn is_speaking(&self) -> bool {
|
||||
self.live_kit
|
||||
.as_ref()
|
||||
@@ -1337,6 +1332,22 @@ impl Room {
|
||||
self.live_kit.as_ref().map(|live_kit| live_kit.deafened)
|
||||
}
|
||||
|
||||
pub fn can_use_microphone(&self) -> bool {
|
||||
use proto::ChannelRole::*;
|
||||
match self.local_participant.role {
|
||||
Admin | Member | Talker => true,
|
||||
Guest | Banned => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn can_share_projects(&self) -> bool {
|
||||
use proto::ChannelRole::*;
|
||||
match self.local_participant.role {
|
||||
Admin | Member => true,
|
||||
Guest | Banned | Talker => false,
|
||||
}
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn share_microphone(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
|
||||
if self.status.is_offline() {
|
||||
|
||||
@@ -120,7 +120,8 @@ impl ChannelMembership {
|
||||
proto::ChannelRole::Admin => 0,
|
||||
proto::ChannelRole::Member => 1,
|
||||
proto::ChannelRole::Banned => 2,
|
||||
proto::ChannelRole::Guest => 3,
|
||||
proto::ChannelRole::Talker => 3,
|
||||
proto::ChannelRole::Guest => 4,
|
||||
},
|
||||
kind_order: match self.kind {
|
||||
proto::channel_member::Kind::Member => 0,
|
||||
|
||||
@@ -2,6 +2,7 @@ use crate::channel_chat::ChannelChatEvent;
|
||||
|
||||
use super::*;
|
||||
use client::{test::FakeServer, Client, UserStore};
|
||||
use clock::FakeSystemClock;
|
||||
use gpui::{AppContext, Context, Model, TestAppContext};
|
||||
use rpc::proto::{self};
|
||||
use settings::SettingsStore;
|
||||
@@ -337,8 +338,9 @@ fn init_test(cx: &mut AppContext) -> Model<ChannelStore> {
|
||||
release_channel::init("0.0.0", cx);
|
||||
client::init_settings(cx);
|
||||
|
||||
let clock = Arc::new(FakeSystemClock::default());
|
||||
let http = FakeHttpClient::with_404_response();
|
||||
let client = Client::new(http.clone(), cx);
|
||||
let client = Client::new(clock, http.clone(), cx);
|
||||
let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx));
|
||||
|
||||
client::init(&client, cx);
|
||||
|
||||
@@ -10,10 +10,11 @@ path = "src/client.rs"
|
||||
doctest = false
|
||||
|
||||
[features]
|
||||
test-support = ["collections/test-support", "gpui/test-support", "rpc/test-support"]
|
||||
test-support = ["clock/test-support", "collections/test-support", "gpui/test-support", "rpc/test-support"]
|
||||
|
||||
[dependencies]
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
clock.workspace = true
|
||||
collections.workspace = true
|
||||
db.workspace = true
|
||||
gpui.workspace = true
|
||||
@@ -51,6 +52,7 @@ uuid.workspace = true
|
||||
url.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
clock = { workspace = true, features = ["test-support"] }
|
||||
collections = { workspace = true, features = ["test-support"] }
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
rpc = { workspace = true, features = ["test-support"] }
|
||||
|
||||
@@ -10,6 +10,7 @@ use async_tungstenite::tungstenite::{
|
||||
error::Error as WebsocketError,
|
||||
http::{Request, StatusCode},
|
||||
};
|
||||
use clock::SystemClock;
|
||||
use collections::HashMap;
|
||||
use futures::{
|
||||
channel::oneshot, future::LocalBoxFuture, AsyncReadExt, FutureExt, SinkExt, StreamExt,
|
||||
@@ -421,11 +422,15 @@ impl settings::Settings for TelemetrySettings {
|
||||
}
|
||||
|
||||
impl Client {
|
||||
pub fn new(http: Arc<ZedHttpClient>, cx: &mut AppContext) -> Arc<Self> {
|
||||
pub fn new(
|
||||
clock: Arc<dyn SystemClock>,
|
||||
http: Arc<ZedHttpClient>,
|
||||
cx: &mut AppContext,
|
||||
) -> Arc<Self> {
|
||||
let client = Arc::new(Self {
|
||||
id: AtomicU64::new(0),
|
||||
peer: Peer::new(0),
|
||||
telemetry: Telemetry::new(http.clone(), cx),
|
||||
telemetry: Telemetry::new(clock, http.clone(), cx),
|
||||
http,
|
||||
state: Default::default(),
|
||||
|
||||
@@ -1455,6 +1460,7 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::test::FakeServer;
|
||||
|
||||
use clock::FakeSystemClock;
|
||||
use gpui::{BackgroundExecutor, Context, TestAppContext};
|
||||
use parking_lot::Mutex;
|
||||
use settings::SettingsStore;
|
||||
@@ -1465,7 +1471,13 @@ mod tests {
|
||||
async fn test_reconnection(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let user_id = 5;
|
||||
let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
|
||||
let client = cx.update(|cx| {
|
||||
Client::new(
|
||||
Arc::new(FakeSystemClock::default()),
|
||||
FakeHttpClient::with_404_response(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let server = FakeServer::for_client(user_id, &client, cx).await;
|
||||
let mut status = client.status();
|
||||
assert!(matches!(
|
||||
@@ -1500,7 +1512,13 @@ mod tests {
|
||||
async fn test_connection_timeout(executor: BackgroundExecutor, cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let user_id = 5;
|
||||
let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
|
||||
let client = cx.update(|cx| {
|
||||
Client::new(
|
||||
Arc::new(FakeSystemClock::default()),
|
||||
FakeHttpClient::with_404_response(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let mut status = client.status();
|
||||
|
||||
// Time out when client tries to connect.
|
||||
@@ -1573,7 +1591,13 @@ mod tests {
|
||||
init_test(cx);
|
||||
let auth_count = Arc::new(Mutex::new(0));
|
||||
let dropped_auth_count = Arc::new(Mutex::new(0));
|
||||
let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
|
||||
let client = cx.update(|cx| {
|
||||
Client::new(
|
||||
Arc::new(FakeSystemClock::default()),
|
||||
FakeHttpClient::with_404_response(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
client.override_authenticate({
|
||||
let auth_count = auth_count.clone();
|
||||
let dropped_auth_count = dropped_auth_count.clone();
|
||||
@@ -1621,7 +1645,13 @@ mod tests {
|
||||
async fn test_subscribing_to_entity(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let user_id = 5;
|
||||
let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
|
||||
let client = cx.update(|cx| {
|
||||
Client::new(
|
||||
Arc::new(FakeSystemClock::default()),
|
||||
FakeHttpClient::with_404_response(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let server = FakeServer::for_client(user_id, &client, cx).await;
|
||||
|
||||
let (done_tx1, mut done_rx1) = smol::channel::unbounded();
|
||||
@@ -1675,7 +1705,13 @@ mod tests {
|
||||
async fn test_subscribing_after_dropping_subscription(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let user_id = 5;
|
||||
let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
|
||||
let client = cx.update(|cx| {
|
||||
Client::new(
|
||||
Arc::new(FakeSystemClock::default()),
|
||||
FakeHttpClient::with_404_response(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let server = FakeServer::for_client(user_id, &client, cx).await;
|
||||
|
||||
let model = cx.new_model(|_| TestModel::default());
|
||||
@@ -1704,7 +1740,13 @@ mod tests {
|
||||
async fn test_dropping_subscription_in_handler(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let user_id = 5;
|
||||
let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
|
||||
let client = cx.update(|cx| {
|
||||
Client::new(
|
||||
Arc::new(FakeSystemClock::default()),
|
||||
FakeHttpClient::with_404_response(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let server = FakeServer::for_client(user_id, &client, cx).await;
|
||||
|
||||
let model = cx.new_model(|_| TestModel::default());
|
||||
|
||||
@@ -2,6 +2,7 @@ mod event_coalescer;
|
||||
|
||||
use crate::TelemetrySettings;
|
||||
use chrono::{DateTime, Utc};
|
||||
use clock::SystemClock;
|
||||
use futures::Future;
|
||||
use gpui::{AppContext, AppMetadata, BackgroundExecutor, Task};
|
||||
use once_cell::sync::Lazy;
|
||||
@@ -24,6 +25,7 @@ use util::TryFutureExt;
|
||||
use self::event_coalescer::EventCoalescer;
|
||||
|
||||
pub struct Telemetry {
|
||||
clock: Arc<dyn SystemClock>,
|
||||
http_client: Arc<ZedHttpClient>,
|
||||
executor: BackgroundExecutor,
|
||||
state: Arc<Mutex<TelemetryState>>,
|
||||
@@ -156,7 +158,11 @@ static ZED_CLIENT_CHECKSUM_SEED: Lazy<Option<Vec<u8>>> = Lazy::new(|| {
|
||||
});
|
||||
|
||||
impl Telemetry {
|
||||
pub fn new(client: Arc<ZedHttpClient>, cx: &mut AppContext) -> Arc<Self> {
|
||||
pub fn new(
|
||||
clock: Arc<dyn SystemClock>,
|
||||
client: Arc<ZedHttpClient>,
|
||||
cx: &mut AppContext,
|
||||
) -> Arc<Self> {
|
||||
let release_channel =
|
||||
ReleaseChannel::try_global(cx).map(|release_channel| release_channel.display_name());
|
||||
|
||||
@@ -205,6 +211,7 @@ impl Telemetry {
|
||||
|
||||
// TODO: Replace all hardware stuff with nested SystemSpecs json
|
||||
let this = Arc::new(Self {
|
||||
clock,
|
||||
http_client: client,
|
||||
executor: cx.background_executor().clone(),
|
||||
state,
|
||||
@@ -317,7 +324,8 @@ impl Telemetry {
|
||||
operation,
|
||||
copilot_enabled,
|
||||
copilot_enabled_for_language,
|
||||
milliseconds_since_first_event: self.milliseconds_since_first_event(Utc::now()),
|
||||
milliseconds_since_first_event: self
|
||||
.milliseconds_since_first_event(self.clock.utc_now()),
|
||||
};
|
||||
|
||||
self.report_event(event)
|
||||
@@ -333,7 +341,8 @@ impl Telemetry {
|
||||
suggestion_id,
|
||||
suggestion_accepted,
|
||||
file_extension,
|
||||
milliseconds_since_first_event: self.milliseconds_since_first_event(Utc::now()),
|
||||
milliseconds_since_first_event: self
|
||||
.milliseconds_since_first_event(self.clock.utc_now()),
|
||||
};
|
||||
|
||||
self.report_event(event)
|
||||
@@ -349,7 +358,8 @@ impl Telemetry {
|
||||
conversation_id,
|
||||
kind,
|
||||
model,
|
||||
milliseconds_since_first_event: self.milliseconds_since_first_event(Utc::now()),
|
||||
milliseconds_since_first_event: self
|
||||
.milliseconds_since_first_event(self.clock.utc_now()),
|
||||
};
|
||||
|
||||
self.report_event(event)
|
||||
@@ -365,7 +375,8 @@ impl Telemetry {
|
||||
operation,
|
||||
room_id,
|
||||
channel_id,
|
||||
milliseconds_since_first_event: self.milliseconds_since_first_event(Utc::now()),
|
||||
milliseconds_since_first_event: self
|
||||
.milliseconds_since_first_event(self.clock.utc_now()),
|
||||
};
|
||||
|
||||
self.report_event(event)
|
||||
@@ -375,7 +386,8 @@ impl Telemetry {
|
||||
let event = Event::Cpu {
|
||||
usage_as_percentage,
|
||||
core_count,
|
||||
milliseconds_since_first_event: self.milliseconds_since_first_event(Utc::now()),
|
||||
milliseconds_since_first_event: self
|
||||
.milliseconds_since_first_event(self.clock.utc_now()),
|
||||
};
|
||||
|
||||
self.report_event(event)
|
||||
@@ -389,24 +401,18 @@ impl Telemetry {
|
||||
let event = Event::Memory {
|
||||
memory_in_bytes,
|
||||
virtual_memory_in_bytes,
|
||||
milliseconds_since_first_event: self.milliseconds_since_first_event(Utc::now()),
|
||||
milliseconds_since_first_event: self
|
||||
.milliseconds_since_first_event(self.clock.utc_now()),
|
||||
};
|
||||
|
||||
self.report_event(event)
|
||||
}
|
||||
|
||||
pub fn report_app_event(self: &Arc<Self>, operation: String) {
|
||||
self.report_app_event_with_date_time(operation, Utc::now());
|
||||
}
|
||||
|
||||
fn report_app_event_with_date_time(
|
||||
self: &Arc<Self>,
|
||||
operation: String,
|
||||
date_time: DateTime<Utc>,
|
||||
) -> Event {
|
||||
pub fn report_app_event(self: &Arc<Self>, operation: String) -> Event {
|
||||
let event = Event::App {
|
||||
operation,
|
||||
milliseconds_since_first_event: self.milliseconds_since_first_event(date_time),
|
||||
milliseconds_since_first_event: self
|
||||
.milliseconds_since_first_event(self.clock.utc_now()),
|
||||
};
|
||||
|
||||
self.report_event(event.clone());
|
||||
@@ -418,7 +424,8 @@ impl Telemetry {
|
||||
let event = Event::Setting {
|
||||
setting,
|
||||
value,
|
||||
milliseconds_since_first_event: self.milliseconds_since_first_event(Utc::now()),
|
||||
milliseconds_since_first_event: self
|
||||
.milliseconds_since_first_event(self.clock.utc_now()),
|
||||
};
|
||||
|
||||
self.report_event(event)
|
||||
@@ -433,7 +440,8 @@ impl Telemetry {
|
||||
let event = Event::Edit {
|
||||
duration: end.timestamp_millis() - start.timestamp_millis(),
|
||||
environment,
|
||||
milliseconds_since_first_event: self.milliseconds_since_first_event(Utc::now()),
|
||||
milliseconds_since_first_event: self
|
||||
.milliseconds_since_first_event(self.clock.utc_now()),
|
||||
};
|
||||
|
||||
self.report_event(event);
|
||||
@@ -444,7 +452,8 @@ impl Telemetry {
|
||||
let event = Event::Action {
|
||||
source,
|
||||
action,
|
||||
milliseconds_since_first_event: self.milliseconds_since_first_event(Utc::now()),
|
||||
milliseconds_since_first_event: self
|
||||
.milliseconds_since_first_event(self.clock.utc_now()),
|
||||
};
|
||||
|
||||
self.report_event(event)
|
||||
@@ -590,29 +599,32 @@ impl Telemetry {
|
||||
mod tests {
|
||||
use super::*;
|
||||
use chrono::TimeZone;
|
||||
use clock::FakeSystemClock;
|
||||
use gpui::TestAppContext;
|
||||
use util::http::FakeHttpClient;
|
||||
|
||||
#[gpui::test]
|
||||
fn test_telemetry_flush_on_max_queue_size(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let clock = Arc::new(FakeSystemClock::new(
|
||||
Utc.with_ymd_and_hms(1990, 4, 12, 12, 0, 0).unwrap(),
|
||||
));
|
||||
let http = FakeHttpClient::with_200_response();
|
||||
let installation_id = Some("installation_id".to_string());
|
||||
let session_id = "session_id".to_string();
|
||||
|
||||
cx.update(|cx| {
|
||||
let telemetry = Telemetry::new(http, cx);
|
||||
let telemetry = Telemetry::new(clock.clone(), http, cx);
|
||||
|
||||
telemetry.state.lock().max_queue_size = 4;
|
||||
telemetry.start(installation_id, session_id, cx);
|
||||
|
||||
assert!(is_empty_state(&telemetry));
|
||||
|
||||
let first_date_time = Utc.with_ymd_and_hms(1990, 4, 12, 12, 0, 0).unwrap();
|
||||
let first_date_time = clock.utc_now();
|
||||
let operation = "test".to_string();
|
||||
|
||||
let event =
|
||||
telemetry.report_app_event_with_date_time(operation.clone(), first_date_time);
|
||||
let event = telemetry.report_app_event(operation.clone());
|
||||
assert_eq!(
|
||||
event,
|
||||
Event::App {
|
||||
@@ -627,9 +639,9 @@ mod tests {
|
||||
Some(first_date_time)
|
||||
);
|
||||
|
||||
let mut date_time = first_date_time + chrono::Duration::milliseconds(100);
|
||||
clock.advance(chrono::Duration::milliseconds(100));
|
||||
|
||||
let event = telemetry.report_app_event_with_date_time(operation.clone(), date_time);
|
||||
let event = telemetry.report_app_event(operation.clone());
|
||||
assert_eq!(
|
||||
event,
|
||||
Event::App {
|
||||
@@ -644,9 +656,9 @@ mod tests {
|
||||
Some(first_date_time)
|
||||
);
|
||||
|
||||
date_time += chrono::Duration::milliseconds(100);
|
||||
clock.advance(chrono::Duration::milliseconds(100));
|
||||
|
||||
let event = telemetry.report_app_event_with_date_time(operation.clone(), date_time);
|
||||
let event = telemetry.report_app_event(operation.clone());
|
||||
assert_eq!(
|
||||
event,
|
||||
Event::App {
|
||||
@@ -661,10 +673,10 @@ mod tests {
|
||||
Some(first_date_time)
|
||||
);
|
||||
|
||||
date_time += chrono::Duration::milliseconds(100);
|
||||
clock.advance(chrono::Duration::milliseconds(100));
|
||||
|
||||
// Adding a 4th event should cause a flush
|
||||
let event = telemetry.report_app_event_with_date_time(operation.clone(), date_time);
|
||||
let event = telemetry.report_app_event(operation.clone());
|
||||
assert_eq!(
|
||||
event,
|
||||
Event::App {
|
||||
@@ -680,22 +692,24 @@ mod tests {
|
||||
#[gpui::test]
|
||||
async fn test_connection_timeout(executor: BackgroundExecutor, cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let clock = Arc::new(FakeSystemClock::new(
|
||||
Utc.with_ymd_and_hms(1990, 4, 12, 12, 0, 0).unwrap(),
|
||||
));
|
||||
let http = FakeHttpClient::with_200_response();
|
||||
let installation_id = Some("installation_id".to_string());
|
||||
let session_id = "session_id".to_string();
|
||||
|
||||
cx.update(|cx| {
|
||||
let telemetry = Telemetry::new(http, cx);
|
||||
let telemetry = Telemetry::new(clock.clone(), http, cx);
|
||||
telemetry.state.lock().max_queue_size = 4;
|
||||
telemetry.start(installation_id, session_id, cx);
|
||||
|
||||
assert!(is_empty_state(&telemetry));
|
||||
|
||||
let first_date_time = Utc.with_ymd_and_hms(1990, 4, 12, 12, 0, 0).unwrap();
|
||||
let first_date_time = clock.utc_now();
|
||||
let operation = "test".to_string();
|
||||
|
||||
let event =
|
||||
telemetry.report_app_event_with_date_time(operation.clone(), first_date_time);
|
||||
let event = telemetry.report_app_event(operation.clone());
|
||||
assert_eq!(
|
||||
event,
|
||||
Event::App {
|
||||
|
||||
@@ -9,5 +9,10 @@ license = "GPL-3.0-or-later"
|
||||
path = "src/clock.rs"
|
||||
doctest = false
|
||||
|
||||
[features]
|
||||
test-support = ["dep:parking_lot"]
|
||||
|
||||
[dependencies]
|
||||
chrono.workspace = true
|
||||
parking_lot = { workspace = true, optional = true }
|
||||
smallvec.workspace = true
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
mod system_clock;
|
||||
|
||||
use smallvec::SmallVec;
|
||||
use std::{
|
||||
cmp::{self, Ordering},
|
||||
fmt, iter,
|
||||
};
|
||||
|
||||
/// A unique identifier for each distributed node
|
||||
pub use system_clock::*;
|
||||
|
||||
/// A unique identifier for each distributed node.
|
||||
pub type ReplicaId = u16;
|
||||
|
||||
/// A [Lamport sequence number](https://en.wikipedia.org/wiki/Lamport_timestamp),
|
||||
/// A [Lamport sequence number](https://en.wikipedia.org/wiki/Lamport_timestamp).
|
||||
pub type Seq = u32;
|
||||
|
||||
/// A [Lamport timestamp](https://en.wikipedia.org/wiki/Lamport_timestamp),
|
||||
@@ -18,7 +22,7 @@ pub struct Lamport {
|
||||
pub value: Seq,
|
||||
}
|
||||
|
||||
/// A [vector clock](https://en.wikipedia.org/wiki/Vector_clock)
|
||||
/// A [vector clock](https://en.wikipedia.org/wiki/Vector_clock).
|
||||
#[derive(Clone, Default, Hash, Eq, PartialEq)]
|
||||
pub struct Global(SmallVec<[u32; 8]>);
|
||||
|
||||
|
||||
59
crates/clock/src/system_clock.rs
Normal file
59
crates/clock/src/system_clock.rs
Normal file
@@ -0,0 +1,59 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
|
||||
pub trait SystemClock: Send + Sync {
|
||||
/// Returns the current date and time in UTC.
|
||||
fn utc_now(&self) -> DateTime<Utc>;
|
||||
}
|
||||
|
||||
pub struct RealSystemClock;
|
||||
|
||||
impl SystemClock for RealSystemClock {
|
||||
fn utc_now(&self) -> DateTime<Utc> {
|
||||
Utc::now()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub struct FakeSystemClockState {
|
||||
now: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub struct FakeSystemClock {
|
||||
// Use an unfair lock to ensure tests are deterministic.
|
||||
state: parking_lot::Mutex<FakeSystemClockState>,
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
impl Default for FakeSystemClock {
|
||||
fn default() -> Self {
|
||||
Self::new(Utc::now())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
impl FakeSystemClock {
|
||||
pub fn new(now: DateTime<Utc>) -> Self {
|
||||
let state = FakeSystemClockState { now };
|
||||
|
||||
Self {
|
||||
state: parking_lot::Mutex::new(state),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_now(&self, now: DateTime<Utc>) {
|
||||
self.state.lock().now = now;
|
||||
}
|
||||
|
||||
/// Advances the [`FakeSystemClock`] by the specified [`Duration`](chrono::Duration).
|
||||
pub fn advance(&self, duration: chrono::Duration) {
|
||||
self.state.lock().now += duration;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
impl SystemClock for FakeSystemClock {
|
||||
fn utc_now(&self) -> DateTime<Utc> {
|
||||
self.state.lock().now
|
||||
}
|
||||
}
|
||||
@@ -56,7 +56,7 @@ async fn get_extensions(
|
||||
Extension(app): Extension<Arc<AppState>>,
|
||||
Query(params): Query<GetExtensionsParams>,
|
||||
) -> Result<Json<GetExtensionsResponse>> {
|
||||
let extensions = app.db.get_extensions(params.filter.as_deref(), 30).await?;
|
||||
let extensions = app.db.get_extensions(params.filter.as_deref(), 500).await?;
|
||||
Ok(Json(GetExtensionsResponse { data: extensions }))
|
||||
}
|
||||
|
||||
|
||||
@@ -100,8 +100,12 @@ pub enum ChannelRole {
|
||||
#[sea_orm(string_value = "member")]
|
||||
#[default]
|
||||
Member,
|
||||
/// Talker can read, but not write.
|
||||
/// They can use microphones and the channel chat
|
||||
#[sea_orm(string_value = "talker")]
|
||||
Talker,
|
||||
/// Guest can read, but not write.
|
||||
/// (thought they can use the channel chat)
|
||||
/// They can not use microphones but can use the chat.
|
||||
#[sea_orm(string_value = "guest")]
|
||||
Guest,
|
||||
/// Banned may not read.
|
||||
@@ -114,8 +118,9 @@ impl ChannelRole {
|
||||
pub fn should_override(&self, other: Self) -> bool {
|
||||
use ChannelRole::*;
|
||||
match self {
|
||||
Admin => matches!(other, Member | Banned | Guest),
|
||||
Member => matches!(other, Banned | Guest),
|
||||
Admin => matches!(other, Member | Banned | Talker | Guest),
|
||||
Member => matches!(other, Banned | Talker | Guest),
|
||||
Talker => matches!(other, Guest),
|
||||
Banned => matches!(other, Guest),
|
||||
Guest => false,
|
||||
}
|
||||
@@ -134,7 +139,7 @@ impl ChannelRole {
|
||||
use ChannelRole::*;
|
||||
match self {
|
||||
Admin | Member => true,
|
||||
Guest => visibility == ChannelVisibility::Public,
|
||||
Guest | Talker => visibility == ChannelVisibility::Public,
|
||||
Banned => false,
|
||||
}
|
||||
}
|
||||
@@ -144,7 +149,7 @@ impl ChannelRole {
|
||||
use ChannelRole::*;
|
||||
match self {
|
||||
Admin | Member => true,
|
||||
Guest | Banned => false,
|
||||
Guest | Talker | Banned => false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,16 +157,16 @@ impl ChannelRole {
|
||||
pub fn can_only_see_public_descendants(&self) -> bool {
|
||||
use ChannelRole::*;
|
||||
match self {
|
||||
Guest => true,
|
||||
Guest | Talker => true,
|
||||
Admin | Member | Banned => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// True if the role can share screen/microphone/projects into rooms.
|
||||
pub fn can_publish_to_rooms(&self) -> bool {
|
||||
pub fn can_use_microphone(&self) -> bool {
|
||||
use ChannelRole::*;
|
||||
match self {
|
||||
Admin | Member => true,
|
||||
Admin | Member | Talker => true,
|
||||
Guest | Banned => false,
|
||||
}
|
||||
}
|
||||
@@ -171,7 +176,7 @@ impl ChannelRole {
|
||||
use ChannelRole::*;
|
||||
match self {
|
||||
Admin | Member => true,
|
||||
Guest | Banned => false,
|
||||
Talker | Guest | Banned => false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,7 +184,7 @@ impl ChannelRole {
|
||||
pub fn can_read_projects(&self) -> bool {
|
||||
use ChannelRole::*;
|
||||
match self {
|
||||
Admin | Member | Guest => true,
|
||||
Admin | Member | Guest | Talker => true,
|
||||
Banned => false,
|
||||
}
|
||||
}
|
||||
@@ -188,7 +193,7 @@ impl ChannelRole {
|
||||
use ChannelRole::*;
|
||||
match self {
|
||||
Admin | Member => true,
|
||||
Banned | Guest => false,
|
||||
Banned | Guest | Talker => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -198,6 +203,7 @@ impl From<proto::ChannelRole> for ChannelRole {
|
||||
match value {
|
||||
proto::ChannelRole::Admin => ChannelRole::Admin,
|
||||
proto::ChannelRole::Member => ChannelRole::Member,
|
||||
proto::ChannelRole::Talker => ChannelRole::Talker,
|
||||
proto::ChannelRole::Guest => ChannelRole::Guest,
|
||||
proto::ChannelRole::Banned => ChannelRole::Banned,
|
||||
}
|
||||
@@ -209,6 +215,7 @@ impl Into<proto::ChannelRole> for ChannelRole {
|
||||
match self {
|
||||
ChannelRole::Admin => proto::ChannelRole::Admin,
|
||||
ChannelRole::Member => proto::ChannelRole::Member,
|
||||
ChannelRole::Talker => proto::ChannelRole::Talker,
|
||||
ChannelRole::Guest => proto::ChannelRole::Guest,
|
||||
ChannelRole::Banned => proto::ChannelRole::Banned,
|
||||
}
|
||||
|
||||
@@ -795,6 +795,7 @@ impl Database {
|
||||
match role {
|
||||
Some(ChannelRole::Admin) => Ok(role.unwrap()),
|
||||
Some(ChannelRole::Member)
|
||||
| Some(ChannelRole::Talker)
|
||||
| Some(ChannelRole::Banned)
|
||||
| Some(ChannelRole::Guest)
|
||||
| None => Err(anyhow!(
|
||||
@@ -813,7 +814,10 @@ impl Database {
|
||||
let channel_role = self.channel_role_for_user(channel, user_id, tx).await?;
|
||||
match channel_role {
|
||||
Some(ChannelRole::Admin) | Some(ChannelRole::Member) => Ok(channel_role.unwrap()),
|
||||
Some(ChannelRole::Banned) | Some(ChannelRole::Guest) | None => Err(anyhow!(
|
||||
Some(ChannelRole::Banned)
|
||||
| Some(ChannelRole::Guest)
|
||||
| Some(ChannelRole::Talker)
|
||||
| None => Err(anyhow!(
|
||||
"user is not a channel member or channel does not exist"
|
||||
))?,
|
||||
}
|
||||
@@ -828,9 +832,10 @@ impl Database {
|
||||
) -> Result<ChannelRole> {
|
||||
let role = self.channel_role_for_user(channel, user_id, tx).await?;
|
||||
match role {
|
||||
Some(ChannelRole::Admin) | Some(ChannelRole::Member) | Some(ChannelRole::Guest) => {
|
||||
Ok(role.unwrap())
|
||||
}
|
||||
Some(ChannelRole::Admin)
|
||||
| Some(ChannelRole::Member)
|
||||
| Some(ChannelRole::Guest)
|
||||
| Some(ChannelRole::Talker) => Ok(role.unwrap()),
|
||||
Some(ChannelRole::Banned) | None => Err(anyhow!(
|
||||
"user is not a channel participant or channel does not exist"
|
||||
))?,
|
||||
|
||||
@@ -51,7 +51,7 @@ impl Database {
|
||||
if !participant
|
||||
.role
|
||||
.unwrap_or(ChannelRole::Member)
|
||||
.can_publish_to_rooms()
|
||||
.can_edit_projects()
|
||||
{
|
||||
return Err(anyhow!("guests cannot share projects"))?;
|
||||
}
|
||||
|
||||
@@ -169,7 +169,7 @@ impl Database {
|
||||
|
||||
let called_user_role = match caller.role.unwrap_or(ChannelRole::Member) {
|
||||
ChannelRole::Admin | ChannelRole::Member => ChannelRole::Member,
|
||||
ChannelRole::Guest => ChannelRole::Guest,
|
||||
ChannelRole::Guest | ChannelRole::Talker => ChannelRole::Guest,
|
||||
ChannelRole::Banned => return Err(anyhow!("banned users cannot invite").into()),
|
||||
};
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ use axum::{
|
||||
Extension, Router, TypedHeader,
|
||||
};
|
||||
use collections::{HashMap, HashSet};
|
||||
pub use connection_pool::ConnectionPool;
|
||||
pub use connection_pool::{ConnectionPool, ZedVersion};
|
||||
use futures::{
|
||||
channel::oneshot,
|
||||
future::{self, BoxFuture},
|
||||
@@ -558,6 +558,7 @@ impl Server {
|
||||
connection: Connection,
|
||||
address: String,
|
||||
user: User,
|
||||
zed_version: ZedVersion,
|
||||
impersonator: Option<User>,
|
||||
mut send_connection_id: Option<oneshot::Sender<ConnectionId>>,
|
||||
executor: Executor,
|
||||
@@ -599,7 +600,7 @@ impl Server {
|
||||
|
||||
{
|
||||
let mut pool = this.connection_pool.lock();
|
||||
pool.add_connection(connection_id, user_id, user.admin);
|
||||
pool.add_connection(connection_id, user_id, user.admin, zed_version);
|
||||
this.peer.send(connection_id, build_initial_contacts_update(contacts, &pool))?;
|
||||
this.peer.send(connection_id, build_update_user_channels(&channels_for_user))?;
|
||||
this.peer.send(connection_id, build_channels_update(
|
||||
@@ -879,17 +880,20 @@ pub async fn handle_websocket_request(
|
||||
.into_response();
|
||||
}
|
||||
|
||||
// the first version of zed that sent this header was 0.121.x
|
||||
if let Some(version) = app_version_header.map(|header| header.0 .0) {
|
||||
// 0.123.0 was a nightly version with incompatible collab changes
|
||||
// that were reverted.
|
||||
if version == "0.123.0".parse().unwrap() {
|
||||
return (
|
||||
StatusCode::UPGRADE_REQUIRED,
|
||||
"client must be upgraded".to_string(),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
let Some(version) = app_version_header.map(|header| ZedVersion(header.0 .0)) else {
|
||||
return (
|
||||
StatusCode::UPGRADE_REQUIRED,
|
||||
"no version header found".to_string(),
|
||||
)
|
||||
.into_response();
|
||||
};
|
||||
|
||||
if !version.is_supported() {
|
||||
return (
|
||||
StatusCode::UPGRADE_REQUIRED,
|
||||
"client must be upgraded".to_string(),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
let socket_address = socket_address.to_string();
|
||||
@@ -906,6 +910,7 @@ pub async fn handle_websocket_request(
|
||||
connection,
|
||||
socket_address,
|
||||
user,
|
||||
version,
|
||||
impersonator.0,
|
||||
None,
|
||||
Executor::Production,
|
||||
@@ -1311,6 +1316,22 @@ async fn set_room_participant_role(
|
||||
response: Response<proto::SetRoomParticipantRole>,
|
||||
session: Session,
|
||||
) -> Result<()> {
|
||||
let user_id = UserId::from_proto(request.user_id);
|
||||
let role = ChannelRole::from(request.role());
|
||||
|
||||
if role == ChannelRole::Talker {
|
||||
let pool = session.connection_pool().await;
|
||||
|
||||
for connection in pool.user_connections(user_id) {
|
||||
if !connection.zed_version.supports_talker_role() {
|
||||
Err(anyhow!(
|
||||
"This user is on zed {} which does not support unmute",
|
||||
connection.zed_version
|
||||
))?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let (live_kit_room, can_publish) = {
|
||||
let room = session
|
||||
.db()
|
||||
@@ -1318,13 +1339,13 @@ async fn set_room_participant_role(
|
||||
.set_room_participant_role(
|
||||
session.user_id,
|
||||
RoomId::from_proto(request.room_id),
|
||||
UserId::from_proto(request.user_id),
|
||||
ChannelRole::from(request.role()),
|
||||
user_id,
|
||||
role,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let live_kit_room = room.live_kit_room.clone();
|
||||
let can_publish = ChannelRole::from(request.role()).can_publish_to_rooms();
|
||||
let can_publish = ChannelRole::from(request.role()).can_use_microphone();
|
||||
room_updated(&room, &session.peer);
|
||||
(live_kit_room, can_publish)
|
||||
};
|
||||
|
||||
@@ -4,6 +4,7 @@ use collections::{BTreeMap, HashSet};
|
||||
use rpc::ConnectionId;
|
||||
use serde::Serialize;
|
||||
use tracing::instrument;
|
||||
use util::SemanticVersion;
|
||||
|
||||
#[derive(Default, Serialize)]
|
||||
pub struct ConnectionPool {
|
||||
@@ -16,10 +17,30 @@ struct ConnectedUser {
|
||||
connection_ids: HashSet<ConnectionId>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct ZedVersion(pub SemanticVersion);
|
||||
use std::fmt;
|
||||
|
||||
impl fmt::Display for ZedVersion {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl ZedVersion {
|
||||
pub fn is_supported(&self) -> bool {
|
||||
self.0 != SemanticVersion::new(0, 123, 0)
|
||||
}
|
||||
pub fn supports_talker_role(&self) -> bool {
|
||||
self.0 >= SemanticVersion::new(0, 125, 0)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct Connection {
|
||||
pub user_id: UserId,
|
||||
pub admin: bool,
|
||||
pub zed_version: ZedVersion,
|
||||
}
|
||||
|
||||
impl ConnectionPool {
|
||||
@@ -29,9 +50,21 @@ impl ConnectionPool {
|
||||
}
|
||||
|
||||
#[instrument(skip(self))]
|
||||
pub fn add_connection(&mut self, connection_id: ConnectionId, user_id: UserId, admin: bool) {
|
||||
self.connections
|
||||
.insert(connection_id, Connection { user_id, admin });
|
||||
pub fn add_connection(
|
||||
&mut self,
|
||||
connection_id: ConnectionId,
|
||||
user_id: UserId,
|
||||
admin: bool,
|
||||
zed_version: ZedVersion,
|
||||
) {
|
||||
self.connections.insert(
|
||||
connection_id,
|
||||
Connection {
|
||||
user_id,
|
||||
admin,
|
||||
zed_version,
|
||||
},
|
||||
);
|
||||
let connected_user = self.connected_users.entry(user_id).or_default();
|
||||
connected_user.connection_ids.insert(connection_id);
|
||||
}
|
||||
@@ -57,6 +90,19 @@ impl ConnectionPool {
|
||||
self.connections.values()
|
||||
}
|
||||
|
||||
pub fn user_connections(&self, user_id: UserId) -> impl Iterator<Item = &Connection> + '_ {
|
||||
self.connected_users
|
||||
.get(&user_id)
|
||||
.into_iter()
|
||||
.map(|state| {
|
||||
state
|
||||
.connection_ids
|
||||
.iter()
|
||||
.flat_map(|cid| self.connections.get(cid))
|
||||
})
|
||||
.flatten()
|
||||
}
|
||||
|
||||
pub fn user_connection_ids(&self, user_id: UserId) -> impl Iterator<Item = ConnectionId> + '_ {
|
||||
self.connected_users
|
||||
.get(&user_id)
|
||||
|
||||
@@ -104,7 +104,7 @@ async fn test_channel_guest_promotion(cx_a: &mut TestAppContext, cx_b: &mut Test
|
||||
});
|
||||
assert!(project_b.read_with(cx_b, |project, _| project.is_read_only()));
|
||||
assert!(editor_b.update(cx_b, |e, cx| e.read_only(cx)));
|
||||
assert!(room_b.read_with(cx_b, |room, _| room.read_only()));
|
||||
assert!(room_b.read_with(cx_b, |room, _| !room.can_use_microphone()));
|
||||
assert!(room_b
|
||||
.update(cx_b, |room, cx| room.share_microphone(cx))
|
||||
.await
|
||||
@@ -130,7 +130,7 @@ async fn test_channel_guest_promotion(cx_a: &mut TestAppContext, cx_b: &mut Test
|
||||
assert!(editor_b.update(cx_b, |editor, cx| !editor.read_only(cx)));
|
||||
|
||||
// B sees themselves as muted, and can unmute.
|
||||
assert!(room_b.read_with(cx_b, |room, _| !room.read_only()));
|
||||
assert!(room_b.read_with(cx_b, |room, _| room.can_use_microphone()));
|
||||
room_b.read_with(cx_b, |room, _| assert!(room.is_muted()));
|
||||
room_b.update(cx_b, |room, cx| room.toggle_mute(cx));
|
||||
cx_a.run_until_parked();
|
||||
@@ -223,7 +223,7 @@ async fn test_channel_requires_zed_cla(cx_a: &mut TestAppContext, cx_b: &mut Tes
|
||||
let room_b = cx_b
|
||||
.read(ActiveCall::global)
|
||||
.update(cx_b, |call, _| call.room().unwrap().clone());
|
||||
assert!(room_b.read_with(cx_b, |room, _| room.read_only()));
|
||||
assert!(room_b.read_with(cx_b, |room, _| !room.can_use_microphone()));
|
||||
|
||||
// A tries to grant write access to B, but cannot because B has not
|
||||
// yet signed the zed CLA.
|
||||
@@ -240,7 +240,26 @@ async fn test_channel_requires_zed_cla(cx_a: &mut TestAppContext, cx_b: &mut Tes
|
||||
.await
|
||||
.unwrap_err();
|
||||
cx_a.run_until_parked();
|
||||
assert!(room_b.read_with(cx_b, |room, _| room.read_only()));
|
||||
assert!(room_b.read_with(cx_b, |room, _| !room.can_share_projects()));
|
||||
assert!(room_b.read_with(cx_b, |room, _| !room.can_use_microphone()));
|
||||
|
||||
// A tries to grant write access to B, but cannot because B has not
|
||||
// yet signed the zed CLA.
|
||||
active_call_a
|
||||
.update(cx_a, |call, cx| {
|
||||
call.room().unwrap().update(cx, |room, cx| {
|
||||
room.set_participant_role(
|
||||
client_b.user_id().unwrap(),
|
||||
proto::ChannelRole::Talker,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
cx_a.run_until_parked();
|
||||
assert!(room_b.read_with(cx_b, |room, _| !room.can_share_projects()));
|
||||
assert!(room_b.read_with(cx_b, |room, _| room.can_use_microphone()));
|
||||
|
||||
// User B signs the zed CLA.
|
||||
server
|
||||
@@ -264,5 +283,6 @@ async fn test_channel_requires_zed_cla(cx_a: &mut TestAppContext, cx_b: &mut Tes
|
||||
.await
|
||||
.unwrap();
|
||||
cx_a.run_until_parked();
|
||||
assert!(room_b.read_with(cx_b, |room, _| !room.read_only()));
|
||||
assert!(room_b.read_with(cx_b, |room, _| room.can_share_projects()));
|
||||
assert!(room_b.read_with(cx_b, |room, _| room.can_use_microphone()));
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::{
|
||||
db::{tests::TestDb, NewUserParams, UserId},
|
||||
executor::Executor,
|
||||
rpc::{Server, CLEANUP_TIMEOUT, RECONNECT_TIMEOUT},
|
||||
rpc::{Server, ZedVersion, CLEANUP_TIMEOUT, RECONNECT_TIMEOUT},
|
||||
AppState, Config,
|
||||
};
|
||||
use anyhow::anyhow;
|
||||
@@ -10,6 +10,7 @@ use channel::{ChannelBuffer, ChannelStore};
|
||||
use client::{
|
||||
self, proto::PeerId, Client, Connection, Credentials, EstablishConnectionError, UserStore,
|
||||
};
|
||||
use clock::FakeSystemClock;
|
||||
use collab_ui::channel_view::ChannelView;
|
||||
use collections::{HashMap, HashSet};
|
||||
use fs::FakeFs;
|
||||
@@ -37,7 +38,7 @@ use std::{
|
||||
Arc,
|
||||
},
|
||||
};
|
||||
use util::http::FakeHttpClient;
|
||||
use util::{http::FakeHttpClient, SemanticVersion};
|
||||
use workspace::{Workspace, WorkspaceStore};
|
||||
|
||||
pub struct TestServer {
|
||||
@@ -163,6 +164,7 @@ impl TestServer {
|
||||
client::init_settings(cx);
|
||||
});
|
||||
|
||||
let clock = Arc::new(FakeSystemClock::default());
|
||||
let http = FakeHttpClient::with_404_response();
|
||||
let user_id = if let Ok(Some(user)) = self.app_state.db.get_user_by_github_login(name).await
|
||||
{
|
||||
@@ -185,7 +187,7 @@ impl TestServer {
|
||||
.user_id
|
||||
};
|
||||
let client_name = name.to_string();
|
||||
let mut client = cx.update(|cx| Client::new(http.clone(), cx));
|
||||
let mut client = cx.update(|cx| Client::new(clock, http.clone(), cx));
|
||||
let server = self.server.clone();
|
||||
let db = self.app_state.db.clone();
|
||||
let connection_killers = self.connection_killers.clone();
|
||||
@@ -231,6 +233,7 @@ impl TestServer {
|
||||
server_conn,
|
||||
client_name,
|
||||
user,
|
||||
ZedVersion(SemanticVersion::new(1, 0, 0)),
|
||||
None,
|
||||
Some(connection_id_tx),
|
||||
Executor::Deterministic(cx.background_executor().clone()),
|
||||
|
||||
@@ -34,6 +34,7 @@ clock.workspace = true
|
||||
collections.workspace = true
|
||||
db.workspace = true
|
||||
editor.workspace = true
|
||||
extensions_ui.workspace = true
|
||||
feature_flags.workspace = true
|
||||
feedback.workspace = true
|
||||
futures.workspace = true
|
||||
|
||||
@@ -385,6 +385,7 @@ impl Render for MessageEditor {
|
||||
mod tests {
|
||||
use super::*;
|
||||
use client::{Client, User, UserStore};
|
||||
use clock::FakeSystemClock;
|
||||
use gpui::TestAppContext;
|
||||
use language::{Language, LanguageConfig};
|
||||
use rpc::proto;
|
||||
@@ -455,8 +456,9 @@ mod tests {
|
||||
let settings = SettingsStore::test(cx);
|
||||
cx.set_global(settings);
|
||||
|
||||
let clock = Arc::new(FakeSystemClock::default());
|
||||
let http = FakeHttpClient::with_404_response();
|
||||
let client = Client::new(http.clone(), cx);
|
||||
let client = Client::new(clock, http.clone(), cx);
|
||||
let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx));
|
||||
theme::init(theme::LoadThemes::JustBase, cx);
|
||||
language::init(cx);
|
||||
|
||||
@@ -34,7 +34,7 @@ use std::{mem, sync::Arc};
|
||||
use theme::{ActiveTheme, ThemeSettings};
|
||||
use ui::{
|
||||
prelude::*, tooltip_container, Avatar, AvatarAvailabilityIndicator, Button, Color, ContextMenu,
|
||||
Icon, IconButton, IconName, IconSize, Label, ListHeader, ListItem, Tooltip,
|
||||
Icon, IconButton, IconName, IconSize, Indicator, Label, ListHeader, ListItem, Tooltip,
|
||||
};
|
||||
use util::{maybe, ResultExt, TryFutureExt};
|
||||
use workspace::{
|
||||
@@ -854,6 +854,10 @@ impl CollabPanel {
|
||||
.into_any_element()
|
||||
} else if role == proto::ChannelRole::Guest {
|
||||
Label::new("Guest").color(Color::Muted).into_any_element()
|
||||
} else if role == proto::ChannelRole::Talker {
|
||||
Label::new("Mic only")
|
||||
.color(Color::Muted)
|
||||
.into_any_element()
|
||||
} else {
|
||||
div().into_any_element()
|
||||
})
|
||||
@@ -959,6 +963,8 @@ impl CollabPanel {
|
||||
is_selected: bool,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> impl IntoElement {
|
||||
let channel_store = self.channel_store.read(cx);
|
||||
let has_channel_buffer_changed = channel_store.has_channel_buffer_changed(channel_id);
|
||||
ListItem::new("channel-notes")
|
||||
.selected(is_selected)
|
||||
.on_click(cx.listener(move |this, _, cx| {
|
||||
@@ -966,9 +972,19 @@ impl CollabPanel {
|
||||
}))
|
||||
.start_slot(
|
||||
h_flex()
|
||||
.relative()
|
||||
.gap_1()
|
||||
.child(render_tree_branch(false, true, cx))
|
||||
.child(IconButton::new(0, IconName::File)),
|
||||
.child(IconButton::new(0, IconName::File))
|
||||
.children(has_channel_buffer_changed.then(|| {
|
||||
div()
|
||||
.w_1p5()
|
||||
.z_index(1)
|
||||
.absolute()
|
||||
.right(px(2.))
|
||||
.top(px(2.))
|
||||
.child(Indicator::dot().color(Color::Info))
|
||||
})),
|
||||
)
|
||||
.child(Label::new("notes"))
|
||||
.tooltip(move |cx| Tooltip::text("Open Channel Notes", cx))
|
||||
@@ -980,6 +996,8 @@ impl CollabPanel {
|
||||
is_selected: bool,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> impl IntoElement {
|
||||
let channel_store = self.channel_store.read(cx);
|
||||
let has_messages_notification = channel_store.has_new_messages(channel_id);
|
||||
ListItem::new("channel-chat")
|
||||
.selected(is_selected)
|
||||
.on_click(cx.listener(move |this, _, cx| {
|
||||
@@ -987,9 +1005,19 @@ impl CollabPanel {
|
||||
}))
|
||||
.start_slot(
|
||||
h_flex()
|
||||
.relative()
|
||||
.gap_1()
|
||||
.child(render_tree_branch(false, false, cx))
|
||||
.child(IconButton::new(0, IconName::MessageBubbles)),
|
||||
.child(IconButton::new(0, IconName::MessageBubbles))
|
||||
.children(has_messages_notification.then(|| {
|
||||
div()
|
||||
.w_1p5()
|
||||
.z_index(1)
|
||||
.absolute()
|
||||
.right(px(2.))
|
||||
.top(px(4.))
|
||||
.child(Indicator::dot().color(Color::Info))
|
||||
})),
|
||||
)
|
||||
.child(Label::new("chat"))
|
||||
.tooltip(move |cx| Tooltip::text("Open Chat", cx))
|
||||
@@ -1013,13 +1041,38 @@ impl CollabPanel {
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let this = cx.view().clone();
|
||||
if !(role == proto::ChannelRole::Guest || role == proto::ChannelRole::Member) {
|
||||
if !(role == proto::ChannelRole::Guest
|
||||
|| role == proto::ChannelRole::Talker
|
||||
|| role == proto::ChannelRole::Member)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
let context_menu = ContextMenu::build(cx, |context_menu, cx| {
|
||||
let context_menu = ContextMenu::build(cx, |mut context_menu, cx| {
|
||||
if role == proto::ChannelRole::Guest {
|
||||
context_menu.entry(
|
||||
context_menu = context_menu.entry(
|
||||
"Grant Mic Access",
|
||||
None,
|
||||
cx.handler_for(&this, move |_, cx| {
|
||||
ActiveCall::global(cx)
|
||||
.update(cx, |call, cx| {
|
||||
let Some(room) = call.room() else {
|
||||
return Task::ready(Ok(()));
|
||||
};
|
||||
room.update(cx, |room, cx| {
|
||||
room.set_participant_role(
|
||||
user_id,
|
||||
proto::ChannelRole::Talker,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
})
|
||||
.detach_and_prompt_err("Failed to grant mic access", cx, |_, _| None)
|
||||
}),
|
||||
);
|
||||
}
|
||||
if role == proto::ChannelRole::Guest || role == proto::ChannelRole::Talker {
|
||||
context_menu = context_menu.entry(
|
||||
"Grant Write Access",
|
||||
None,
|
||||
cx.handler_for(&this, move |_, cx| {
|
||||
@@ -1043,10 +1096,16 @@ impl CollabPanel {
|
||||
}
|
||||
})
|
||||
}),
|
||||
)
|
||||
} else if role == proto::ChannelRole::Member {
|
||||
context_menu.entry(
|
||||
"Revoke Write Access",
|
||||
);
|
||||
}
|
||||
if role == proto::ChannelRole::Member || role == proto::ChannelRole::Talker {
|
||||
let label = if role == proto::ChannelRole::Talker {
|
||||
"Mute"
|
||||
} else {
|
||||
"Revoke Access"
|
||||
};
|
||||
context_menu = context_menu.entry(
|
||||
label,
|
||||
None,
|
||||
cx.handler_for(&this, move |_, cx| {
|
||||
ActiveCall::global(cx)
|
||||
@@ -1062,12 +1121,12 @@ impl CollabPanel {
|
||||
)
|
||||
})
|
||||
})
|
||||
.detach_and_prompt_err("Failed to revoke write access", cx, |_, _| None)
|
||||
.detach_and_prompt_err("Failed to revoke access", cx, |_, _| None)
|
||||
}),
|
||||
)
|
||||
} else {
|
||||
unreachable!()
|
||||
);
|
||||
}
|
||||
|
||||
context_menu
|
||||
});
|
||||
|
||||
cx.focus_view(&context_menu);
|
||||
@@ -2490,13 +2549,26 @@ impl CollabPanel {
|
||||
},
|
||||
))
|
||||
.start_slot(
|
||||
Icon::new(if is_public {
|
||||
IconName::Public
|
||||
} else {
|
||||
IconName::Hash
|
||||
})
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted),
|
||||
div()
|
||||
.relative()
|
||||
.child(
|
||||
Icon::new(if is_public {
|
||||
IconName::Public
|
||||
} else {
|
||||
IconName::Hash
|
||||
})
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.children(has_notes_notification.then(|| {
|
||||
div()
|
||||
.w_1p5()
|
||||
.z_index(1)
|
||||
.absolute()
|
||||
.right(px(-1.))
|
||||
.top(px(-1.))
|
||||
.child(Indicator::dot().color(Color::Info))
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
@@ -2530,9 +2602,7 @@ impl CollabPanel {
|
||||
this.join_channel_chat(channel_id, cx)
|
||||
}))
|
||||
.tooltip(|cx| Tooltip::text("Open channel chat", cx))
|
||||
.when(!has_messages_notification, |this| {
|
||||
this.visible_on_hover("")
|
||||
}),
|
||||
.visible_on_hover(""),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("channel_notes", IconName::File)
|
||||
@@ -2548,9 +2618,7 @@ impl CollabPanel {
|
||||
this.open_channel_notes(channel_id, cx)
|
||||
}))
|
||||
.tooltip(|cx| Tooltip::text("Open channel notes", cx))
|
||||
.when(!has_notes_notification, |this| {
|
||||
this.visible_on_hover("")
|
||||
}),
|
||||
.visible_on_hover(""),
|
||||
),
|
||||
),
|
||||
)
|
||||
@@ -2560,6 +2628,7 @@ impl CollabPanel {
|
||||
cx.new_view(|_| JoinChannelTooltip {
|
||||
channel_store: channel_store.clone(),
|
||||
channel_id,
|
||||
has_notes_notification,
|
||||
})
|
||||
.into()
|
||||
}
|
||||
@@ -2845,17 +2914,25 @@ impl Render for DraggedChannelView {
|
||||
struct JoinChannelTooltip {
|
||||
channel_store: Model<ChannelStore>,
|
||||
channel_id: ChannelId,
|
||||
has_notes_notification: bool,
|
||||
}
|
||||
|
||||
impl Render for JoinChannelTooltip {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
tooltip_container(cx, |div, cx| {
|
||||
tooltip_container(cx, |container, cx| {
|
||||
let participants = self
|
||||
.channel_store
|
||||
.read(cx)
|
||||
.channel_participants(self.channel_id);
|
||||
|
||||
div.child(Label::new("Join Channel"))
|
||||
container
|
||||
.child(Label::new("Join channel"))
|
||||
.children(self.has_notes_notification.then(|| {
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(Indicator::dot().color(Color::Info))
|
||||
.child(Label::new("Unread notes"))
|
||||
}))
|
||||
.children(participants.iter().map(|participant| {
|
||||
h_flex()
|
||||
.gap_2()
|
||||
|
||||
@@ -187,9 +187,10 @@ impl Render for CollabTitlebarItem {
|
||||
let is_muted = room.is_muted();
|
||||
let is_deafened = room.is_deafened().unwrap_or(false);
|
||||
let is_screen_sharing = room.is_screen_sharing();
|
||||
let read_only = room.read_only();
|
||||
let can_use_microphone = room.can_use_microphone();
|
||||
let can_share_projects = room.can_share_projects();
|
||||
|
||||
this.when(is_local && !read_only, |this| {
|
||||
this.when(is_local && can_share_projects, |this| {
|
||||
this.child(
|
||||
Button::new(
|
||||
"toggle_sharing",
|
||||
@@ -235,7 +236,7 @@ impl Render for CollabTitlebarItem {
|
||||
)
|
||||
.pr_2(),
|
||||
)
|
||||
.when(!read_only, |this| {
|
||||
.when(can_use_microphone, |this| {
|
||||
this.child(
|
||||
IconButton::new(
|
||||
"mute-microphone",
|
||||
@@ -276,7 +277,7 @@ impl Render for CollabTitlebarItem {
|
||||
.icon_size(IconSize::Small)
|
||||
.selected(is_deafened)
|
||||
.tooltip(move |cx| {
|
||||
if !read_only {
|
||||
if can_use_microphone {
|
||||
Tooltip::with_meta(
|
||||
"Deafen Audio",
|
||||
None,
|
||||
@@ -289,7 +290,7 @@ impl Render for CollabTitlebarItem {
|
||||
})
|
||||
.on_click(move |_, cx| crate::toggle_deafen(&Default::default(), cx)),
|
||||
)
|
||||
.when(!read_only, |this| {
|
||||
.when(can_share_projects, |this| {
|
||||
this.child(
|
||||
IconButton::new("screen-share", ui::IconName::Screen)
|
||||
.style(ButtonStyle::Subtle)
|
||||
@@ -695,6 +696,7 @@ impl CollabTitlebarItem {
|
||||
.menu(|cx| {
|
||||
ContextMenu::build(cx, |menu, _| {
|
||||
menu.action("Settings", zed_actions::OpenSettings.boxed_clone())
|
||||
.action("Extensions", extensions_ui::Extensions.boxed_clone())
|
||||
.action("Theme", theme_selector::Toggle.boxed_clone())
|
||||
.separator()
|
||||
.action("Share Feedback", feedback::GiveFeedback.boxed_clone())
|
||||
@@ -720,6 +722,7 @@ impl CollabTitlebarItem {
|
||||
ContextMenu::build(cx, |menu, _| {
|
||||
menu.action("Settings", zed_actions::OpenSettings.boxed_clone())
|
||||
.action("Theme", theme_selector::Toggle.boxed_clone())
|
||||
.action("Extensions", extensions_ui::Extensions.boxed_clone())
|
||||
.separator()
|
||||
.action("Share Feedback", feedback::GiveFeedback.boxed_clone())
|
||||
})
|
||||
|
||||
@@ -428,6 +428,8 @@ impl Copilot {
|
||||
let binary = LanguageServerBinary {
|
||||
path: node_path,
|
||||
arguments,
|
||||
// TODO: We could set HTTP_PROXY etc here and fix the copilot issue.
|
||||
env: None,
|
||||
};
|
||||
|
||||
let server = LanguageServer::new(
|
||||
|
||||
@@ -885,7 +885,7 @@ mod tests {
|
||||
use super::*;
|
||||
use editor::{
|
||||
display_map::{BlockContext, TransformBlock},
|
||||
DisplayPoint,
|
||||
DisplayPoint, GutterDimensions,
|
||||
};
|
||||
use gpui::{px, TestAppContext, VisualTestContext, WindowContext};
|
||||
use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, PointUtf16, Unclipped};
|
||||
@@ -1599,8 +1599,7 @@ mod tests {
|
||||
.render(&mut BlockContext {
|
||||
context: cx,
|
||||
anchor_x: px(0.),
|
||||
gutter_padding: px(0.),
|
||||
gutter_width: px(0.),
|
||||
gutter_dimensions: &GutterDimensions::default(),
|
||||
line_height: px(0.),
|
||||
em_width: px(0.),
|
||||
max_width: px(0.),
|
||||
|
||||
@@ -165,6 +165,8 @@ gpui::actions!(
|
||||
GoToPrevHunk,
|
||||
GoToTypeDefinition,
|
||||
GoToTypeDefinitionSplit,
|
||||
GoToImplementation,
|
||||
GoToImplementationSplit,
|
||||
OpenUrl,
|
||||
HalfPageDown,
|
||||
HalfPageUp,
|
||||
|
||||
@@ -2,7 +2,7 @@ use super::{
|
||||
wrap_map::{self, WrapEdit, WrapPoint, WrapSnapshot},
|
||||
Highlights,
|
||||
};
|
||||
use crate::{Anchor, Editor, EditorStyle, ExcerptId, ExcerptRange, ToPoint as _};
|
||||
use crate::{Anchor, Editor, EditorStyle, ExcerptId, ExcerptRange, GutterDimensions, ToPoint as _};
|
||||
use collections::{Bound, HashMap, HashSet};
|
||||
use gpui::{AnyElement, ElementContext, Pixels, View};
|
||||
use language::{BufferSnapshot, Chunk, Patch, Point};
|
||||
@@ -88,8 +88,7 @@ pub struct BlockContext<'a, 'b> {
|
||||
pub view: View<Editor>,
|
||||
pub anchor_x: Pixels,
|
||||
pub max_width: Pixels,
|
||||
pub gutter_width: Pixels,
|
||||
pub gutter_padding: Pixels,
|
||||
pub gutter_dimensions: &'b GutterDimensions,
|
||||
pub em_width: Pixels,
|
||||
pub line_height: Pixels,
|
||||
pub block_id: usize,
|
||||
|
||||
@@ -88,6 +88,7 @@ pub use multi_buffer::{
|
||||
};
|
||||
use ordered_float::OrderedFloat;
|
||||
use parking_lot::{Mutex, RwLock};
|
||||
use project::project_settings::{GitGutterSetting, ProjectSettings};
|
||||
use project::{FormatTrigger, Location, Project, ProjectPath, ProjectTransaction};
|
||||
use rand::prelude::*;
|
||||
use rpc::proto::*;
|
||||
@@ -443,7 +444,8 @@ pub struct EditorSnapshot {
|
||||
}
|
||||
|
||||
pub struct GutterDimensions {
|
||||
pub padding: Pixels,
|
||||
pub left_padding: Pixels,
|
||||
pub right_padding: Pixels,
|
||||
pub width: Pixels,
|
||||
pub margin: Pixels,
|
||||
}
|
||||
@@ -451,7 +453,8 @@ pub struct GutterDimensions {
|
||||
impl Default for GutterDimensions {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
padding: Pixels::ZERO,
|
||||
left_padding: Pixels::ZERO,
|
||||
right_padding: Pixels::ZERO,
|
||||
width: Pixels::ZERO,
|
||||
margin: Pixels::ZERO,
|
||||
}
|
||||
@@ -1346,6 +1349,7 @@ pub(crate) struct NavigationData {
|
||||
enum GotoDefinitionKind {
|
||||
Symbol,
|
||||
Type,
|
||||
Implementation,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -4057,7 +4061,8 @@ impl Editor {
|
||||
if self.available_code_actions.is_some() {
|
||||
Some(
|
||||
IconButton::new("code_actions_indicator", ui::IconName::Bolt)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.size(ui::ButtonSize::None)
|
||||
.icon_color(Color::Muted)
|
||||
.selected(is_active)
|
||||
.on_click(cx.listener(|editor, _e, cx| {
|
||||
@@ -4206,8 +4211,43 @@ impl Editor {
|
||||
active_index: 0,
|
||||
ranges: tabstops,
|
||||
});
|
||||
}
|
||||
|
||||
// Check whether the just-entered snippet ends with an auto-closable bracket.
|
||||
if self.autoclose_regions.is_empty() {
|
||||
let snapshot = self.buffer.read(cx).snapshot(cx);
|
||||
for selection in &mut self.selections.all::<Point>(cx) {
|
||||
let selection_head = selection.head();
|
||||
let Some(scope) = snapshot.language_scope_at(selection_head) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let mut bracket_pair = None;
|
||||
let next_chars = snapshot.chars_at(selection_head).collect::<String>();
|
||||
let prev_chars = snapshot
|
||||
.reversed_chars_at(selection_head)
|
||||
.collect::<String>();
|
||||
for (pair, enabled) in scope.brackets() {
|
||||
if enabled
|
||||
&& pair.close
|
||||
&& prev_chars.starts_with(pair.start.as_str())
|
||||
&& next_chars.starts_with(pair.end.as_str())
|
||||
{
|
||||
bracket_pair = Some(pair.clone());
|
||||
break;
|
||||
}
|
||||
}
|
||||
if let Some(pair) = bracket_pair {
|
||||
let start = snapshot.anchor_after(selection_head);
|
||||
let end = snapshot.anchor_after(selection_head);
|
||||
self.autoclose_regions.push(AutocloseRegion {
|
||||
selection_id: selection.id,
|
||||
range: start..end,
|
||||
pair,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -7317,6 +7357,18 @@ impl Editor {
|
||||
self.go_to_definition_of_kind(GotoDefinitionKind::Symbol, false, cx);
|
||||
}
|
||||
|
||||
pub fn go_to_implementation(&mut self, _: &GoToImplementation, cx: &mut ViewContext<Self>) {
|
||||
self.go_to_definition_of_kind(GotoDefinitionKind::Implementation, false, cx);
|
||||
}
|
||||
|
||||
pub fn go_to_implementation_split(
|
||||
&mut self,
|
||||
_: &GoToImplementationSplit,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
self.go_to_definition_of_kind(GotoDefinitionKind::Implementation, true, cx);
|
||||
}
|
||||
|
||||
pub fn go_to_type_definition(&mut self, _: &GoToTypeDefinition, cx: &mut ViewContext<Self>) {
|
||||
self.go_to_definition_of_kind(GotoDefinitionKind::Type, false, cx);
|
||||
}
|
||||
@@ -7354,12 +7406,14 @@ impl Editor {
|
||||
let definitions = project.update(cx, |project, cx| match kind {
|
||||
GotoDefinitionKind::Symbol => project.definition(&buffer, head, cx),
|
||||
GotoDefinitionKind::Type => project.type_definition(&buffer, head, cx),
|
||||
GotoDefinitionKind::Implementation => project.implementation(&buffer, head, cx),
|
||||
});
|
||||
|
||||
cx.spawn(|editor, mut cx| async move {
|
||||
let definitions = definitions.await?;
|
||||
editor.update(&mut cx, |editor, cx| {
|
||||
editor.navigate_to_hover_links(
|
||||
Some(kind),
|
||||
definitions.into_iter().map(HoverLink::Text).collect(),
|
||||
split,
|
||||
cx,
|
||||
@@ -7392,8 +7446,9 @@ impl Editor {
|
||||
.detach();
|
||||
}
|
||||
|
||||
pub fn navigate_to_hover_links(
|
||||
pub(crate) fn navigate_to_hover_links(
|
||||
&mut self,
|
||||
kind: Option<GotoDefinitionKind>,
|
||||
mut definitions: Vec<HoverLink>,
|
||||
split: bool,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
@@ -7462,13 +7517,18 @@ impl Editor {
|
||||
cx.spawn(|editor, mut cx| async move {
|
||||
let (title, location_tasks, workspace) = editor
|
||||
.update(&mut cx, |editor, cx| {
|
||||
let tab_kind = match kind {
|
||||
Some(GotoDefinitionKind::Implementation) => "Implementations",
|
||||
_ => "Definitions",
|
||||
};
|
||||
let title = definitions
|
||||
.iter()
|
||||
.find_map(|definition| match definition {
|
||||
HoverLink::Text(link) => link.origin.as_ref().map(|origin| {
|
||||
let buffer = origin.buffer.read(cx);
|
||||
format!(
|
||||
"Definitions for {}",
|
||||
"{} for {}",
|
||||
tab_kind,
|
||||
buffer
|
||||
.text_for_range(origin.range.clone())
|
||||
.collect::<String>()
|
||||
@@ -7477,7 +7537,7 @@ impl Editor {
|
||||
HoverLink::InlayHint(_, _) => None,
|
||||
HoverLink::Url(_) => None,
|
||||
})
|
||||
.unwrap_or("Definitions".to_string());
|
||||
.unwrap_or(tab_kind.to_string());
|
||||
let location_tasks = definitions
|
||||
.into_iter()
|
||||
.map(|definition| match definition {
|
||||
@@ -9580,23 +9640,50 @@ impl EditorSnapshot {
|
||||
max_line_number_width: Pixels,
|
||||
cx: &AppContext,
|
||||
) -> GutterDimensions {
|
||||
if self.show_gutter {
|
||||
let descent = cx.text_system().descent(font_id, font_size);
|
||||
let gutter_padding_factor = 4.0;
|
||||
let gutter_padding = (em_width * gutter_padding_factor).round();
|
||||
if !self.show_gutter {
|
||||
return GutterDimensions::default();
|
||||
}
|
||||
let descent = cx.text_system().descent(font_id, font_size);
|
||||
|
||||
let show_git_gutter = matches!(
|
||||
ProjectSettings::get_global(cx).git.git_gutter,
|
||||
Some(GitGutterSetting::TrackedFiles)
|
||||
);
|
||||
let gutter_settings = EditorSettings::get_global(cx).gutter;
|
||||
|
||||
let line_gutter_width = if gutter_settings.line_numbers {
|
||||
// Avoid flicker-like gutter resizes when the line number gains another digit and only resize the gutter on files with N*10^5 lines.
|
||||
let min_width_for_number_on_gutter = em_width * 4.0;
|
||||
let gutter_width =
|
||||
max_line_number_width.max(min_width_for_number_on_gutter) + gutter_padding * 2.0;
|
||||
let gutter_margin = -descent;
|
||||
|
||||
GutterDimensions {
|
||||
padding: gutter_padding,
|
||||
width: gutter_width,
|
||||
margin: gutter_margin,
|
||||
}
|
||||
max_line_number_width.max(min_width_for_number_on_gutter)
|
||||
} else {
|
||||
GutterDimensions::default()
|
||||
0.0.into()
|
||||
};
|
||||
|
||||
let left_padding = if gutter_settings.code_actions {
|
||||
em_width * 3.0
|
||||
} else if show_git_gutter && gutter_settings.line_numbers {
|
||||
em_width * 2.0
|
||||
} else if show_git_gutter || gutter_settings.line_numbers {
|
||||
em_width
|
||||
} else {
|
||||
px(0.)
|
||||
};
|
||||
|
||||
let right_padding = if gutter_settings.folds && gutter_settings.line_numbers {
|
||||
em_width * 4.0
|
||||
} else if gutter_settings.folds {
|
||||
em_width * 3.0
|
||||
} else if gutter_settings.line_numbers {
|
||||
em_width
|
||||
} else {
|
||||
px(0.)
|
||||
};
|
||||
|
||||
GutterDimensions {
|
||||
left_padding,
|
||||
right_padding,
|
||||
width: line_gutter_width + left_padding + right_padding,
|
||||
margin: -descent,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10103,9 +10190,14 @@ pub fn diagnostic_block_renderer(diagnostic: Diagnostic, _is_valid: bool) -> Ren
|
||||
.group(group_id.clone())
|
||||
.relative()
|
||||
.size_full()
|
||||
.pl(cx.gutter_width)
|
||||
.w(cx.max_width + cx.gutter_width)
|
||||
.child(div().flex().w(cx.anchor_x - cx.gutter_width).flex_shrink())
|
||||
.pl(cx.gutter_dimensions.width)
|
||||
.w(cx.max_width + cx.gutter_dimensions.width)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.w(cx.anchor_x - cx.gutter_dimensions.width)
|
||||
.flex_shrink(),
|
||||
)
|
||||
.child(div().flex().flex_shrink_0().child(
|
||||
StyledText::new(text_without_backticks.clone()).with_highlights(
|
||||
&text_style,
|
||||
|
||||
@@ -12,6 +12,7 @@ pub struct EditorSettings {
|
||||
pub use_on_type_format: bool,
|
||||
pub toolbar: Toolbar,
|
||||
pub scrollbar: Scrollbar,
|
||||
pub gutter: Gutter,
|
||||
pub vertical_scroll_margin: f32,
|
||||
pub relative_line_numbers: bool,
|
||||
pub seed_search_query_from_cursor: SeedQuerySetting,
|
||||
@@ -45,6 +46,13 @@ pub struct Scrollbar {
|
||||
pub diagnostics: bool,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
|
||||
pub struct Gutter {
|
||||
pub line_numbers: bool,
|
||||
pub code_actions: bool,
|
||||
pub folds: bool,
|
||||
}
|
||||
|
||||
/// When to show the scrollbar in the editor.
|
||||
///
|
||||
/// Default: auto
|
||||
@@ -97,6 +105,8 @@ pub struct EditorSettingsContent {
|
||||
pub toolbar: Option<ToolbarContent>,
|
||||
/// Scrollbar related settings
|
||||
pub scrollbar: Option<ScrollbarContent>,
|
||||
/// Gutter related settings
|
||||
pub gutter: Option<GutterContent>,
|
||||
|
||||
/// The number of lines to keep above/below the cursor when auto-scrolling.
|
||||
///
|
||||
@@ -157,6 +167,23 @@ pub struct ScrollbarContent {
|
||||
pub diagnostics: Option<bool>,
|
||||
}
|
||||
|
||||
/// Gutter related settings
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
|
||||
pub struct GutterContent {
|
||||
/// Whether to show line numbers in the gutter.
|
||||
///
|
||||
/// Default: true
|
||||
pub line_numbers: Option<bool>,
|
||||
/// Whether to show code action buttons in the gutter.
|
||||
///
|
||||
/// Default: true
|
||||
pub code_actions: Option<bool>,
|
||||
/// Whether to show fold buttons in the gutter.
|
||||
///
|
||||
/// Default: true
|
||||
pub folds: Option<bool>,
|
||||
}
|
||||
|
||||
impl Settings for EditorSettings {
|
||||
const KEY: Option<&'static str> = None;
|
||||
|
||||
|
||||
@@ -12,9 +12,9 @@ use crate::{
|
||||
mouse_context_menu,
|
||||
scroll::scroll_amount::ScrollAmount,
|
||||
CursorShape, DisplayPoint, DocumentHighlightRead, DocumentHighlightWrite, Editor, EditorMode,
|
||||
EditorSettings, EditorSnapshot, EditorStyle, HalfPageDown, HalfPageUp, HoveredCursor, LineDown,
|
||||
LineUp, OpenExcerpts, PageDown, PageUp, Point, SelectPhase, Selection, SoftWrap, ToPoint,
|
||||
CURSORS_VISIBLE_FOR, MAX_LINE_LEN,
|
||||
EditorSettings, EditorSnapshot, EditorStyle, GutterDimensions, HalfPageDown, HalfPageUp,
|
||||
HoveredCursor, LineDown, LineUp, OpenExcerpts, PageDown, PageUp, Point, SelectPhase, Selection,
|
||||
SoftWrap, ToPoint, CURSORS_VISIBLE_FOR, MAX_LINE_LEN,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use collections::{BTreeMap, HashMap};
|
||||
@@ -260,6 +260,8 @@ impl EditorElement {
|
||||
register_action(view, cx, Editor::go_to_prev_hunk);
|
||||
register_action(view, cx, Editor::go_to_definition);
|
||||
register_action(view, cx, Editor::go_to_definition_split);
|
||||
register_action(view, cx, Editor::go_to_implementation);
|
||||
register_action(view, cx, Editor::go_to_implementation_split);
|
||||
register_action(view, cx, Editor::go_to_type_definition);
|
||||
register_action(view, cx, Editor::go_to_type_definition_split);
|
||||
register_action(view, cx, Editor::open_url);
|
||||
@@ -714,68 +716,73 @@ impl EditorElement {
|
||||
let scroll_position = layout.position_map.snapshot.scroll_position();
|
||||
let scroll_top = scroll_position.y * line_height;
|
||||
|
||||
let show_gutter = matches!(
|
||||
let show_git_gutter = matches!(
|
||||
ProjectSettings::get_global(cx).git.git_gutter,
|
||||
Some(GitGutterSetting::TrackedFiles)
|
||||
);
|
||||
|
||||
if show_gutter {
|
||||
if show_git_gutter {
|
||||
Self::paint_diff_hunks(bounds, layout, cx);
|
||||
}
|
||||
|
||||
let gutter_settings = EditorSettings::get_global(cx).gutter;
|
||||
|
||||
if let Some(indicator) = layout.code_actions_indicator.take() {
|
||||
debug_assert!(gutter_settings.code_actions);
|
||||
let mut button = indicator.button.into_any_element();
|
||||
let available_space = size(
|
||||
AvailableSpace::MinContent,
|
||||
AvailableSpace::Definite(line_height),
|
||||
);
|
||||
let indicator_size = button.measure(available_space, cx);
|
||||
|
||||
let mut x = Pixels::ZERO;
|
||||
let mut y = indicator.row as f32 * line_height - scroll_top;
|
||||
// Center indicator.
|
||||
x += (layout.gutter_dimensions.margin + layout.gutter_dimensions.left_padding
|
||||
- indicator_size.width)
|
||||
/ 2.;
|
||||
y += (line_height - indicator_size.height) / 2.;
|
||||
|
||||
button.draw(bounds.origin + point(x, y), available_space, cx);
|
||||
}
|
||||
|
||||
for (ix, fold_indicator) in layout.fold_indicators.drain(..).enumerate() {
|
||||
if let Some(fold_indicator) = fold_indicator {
|
||||
debug_assert!(gutter_settings.folds);
|
||||
let mut fold_indicator = fold_indicator.into_any_element();
|
||||
let available_space = size(
|
||||
AvailableSpace::MinContent,
|
||||
AvailableSpace::Definite(line_height * 0.55),
|
||||
);
|
||||
let fold_indicator_size = fold_indicator.measure(available_space, cx);
|
||||
|
||||
let position = point(
|
||||
bounds.size.width - layout.gutter_dimensions.right_padding,
|
||||
ix as f32 * line_height - (scroll_top % line_height),
|
||||
);
|
||||
let centering_offset = point(
|
||||
(layout.gutter_dimensions.right_padding + layout.gutter_dimensions.margin
|
||||
- fold_indicator_size.width)
|
||||
/ 2.,
|
||||
(line_height - fold_indicator_size.height) / 2.,
|
||||
);
|
||||
let origin = bounds.origin + position + centering_offset;
|
||||
fold_indicator.draw(origin, available_space, cx);
|
||||
}
|
||||
}
|
||||
|
||||
for (ix, line) in layout.line_numbers.iter().enumerate() {
|
||||
if let Some(line) = line {
|
||||
let line_origin = bounds.origin
|
||||
+ point(
|
||||
bounds.size.width - line.width - layout.gutter_padding,
|
||||
bounds.size.width - line.width - layout.gutter_dimensions.right_padding,
|
||||
ix as f32 * line_height - (scroll_top % line_height),
|
||||
);
|
||||
|
||||
line.paint(line_origin, line_height, cx).log_err();
|
||||
}
|
||||
}
|
||||
|
||||
cx.with_z_index(1, |cx| {
|
||||
for (ix, fold_indicator) in layout.fold_indicators.drain(..).enumerate() {
|
||||
if let Some(fold_indicator) = fold_indicator {
|
||||
let mut fold_indicator = fold_indicator.into_any_element();
|
||||
let available_space = size(
|
||||
AvailableSpace::MinContent,
|
||||
AvailableSpace::Definite(line_height * 0.55),
|
||||
);
|
||||
let fold_indicator_size = fold_indicator.measure(available_space, cx);
|
||||
|
||||
let position = point(
|
||||
bounds.size.width - layout.gutter_padding,
|
||||
ix as f32 * line_height - (scroll_top % line_height),
|
||||
);
|
||||
let centering_offset = point(
|
||||
(layout.gutter_padding + layout.gutter_margin - fold_indicator_size.width)
|
||||
/ 2.,
|
||||
(line_height - fold_indicator_size.height) / 2.,
|
||||
);
|
||||
let origin = bounds.origin + position + centering_offset;
|
||||
fold_indicator.draw(origin, available_space, cx);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(indicator) = layout.code_actions_indicator.take() {
|
||||
let mut button = indicator.button.into_any_element();
|
||||
let available_space = size(
|
||||
AvailableSpace::MinContent,
|
||||
AvailableSpace::Definite(line_height),
|
||||
);
|
||||
let indicator_size = button.measure(available_space, cx);
|
||||
|
||||
let mut x = Pixels::ZERO;
|
||||
let mut y = indicator.row as f32 * line_height - scroll_top;
|
||||
// Center indicator.
|
||||
x += ((layout.gutter_padding + layout.gutter_margin) - indicator_size.width) / 2.;
|
||||
y += (line_height - indicator_size.height) / 2.;
|
||||
|
||||
button.draw(bounds.origin + point(x, y), available_space, cx);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn paint_diff_hunks(bounds: Bounds<Pixels>, layout: &LayoutState, cx: &mut ElementContext) {
|
||||
@@ -885,7 +892,8 @@ impl EditorElement {
|
||||
cx: &mut ElementContext,
|
||||
) {
|
||||
let start_row = layout.visible_display_row_range.start;
|
||||
let content_origin = text_bounds.origin + point(layout.gutter_margin, Pixels::ZERO);
|
||||
let content_origin =
|
||||
text_bounds.origin + point(layout.gutter_dimensions.margin, Pixels::ZERO);
|
||||
let line_end_overshoot = 0.15 * layout.position_map.line_height;
|
||||
let whitespace_setting = self
|
||||
.editor
|
||||
@@ -918,6 +926,153 @@ impl EditorElement {
|
||||
}
|
||||
}
|
||||
|
||||
let mut invisible_display_ranges = SmallVec::<[Range<DisplayPoint>; 32]>::new();
|
||||
for (participant_ix, (player_color, selections)) in
|
||||
layout.selections.iter().enumerate()
|
||||
{
|
||||
for selection in selections {
|
||||
if selection.is_local && !selection.range.is_empty() {
|
||||
invisible_display_ranges.push(selection.range.clone());
|
||||
}
|
||||
|
||||
if !selection.is_local || self.editor.read(cx).show_local_cursors(cx) {
|
||||
let cursor_position = selection.head;
|
||||
if layout
|
||||
.visible_display_row_range
|
||||
.contains(&cursor_position.row())
|
||||
{
|
||||
let cursor_row_layout = &layout.position_map.line_layouts
|
||||
[(cursor_position.row() - start_row) as usize]
|
||||
.line;
|
||||
let cursor_column = cursor_position.column() as usize;
|
||||
|
||||
let cursor_character_x =
|
||||
cursor_row_layout.x_for_index(cursor_column);
|
||||
let mut block_width = cursor_row_layout
|
||||
.x_for_index(cursor_column + 1)
|
||||
- cursor_character_x;
|
||||
if block_width == Pixels::ZERO {
|
||||
block_width = layout.position_map.em_width;
|
||||
}
|
||||
let block_text = if let CursorShape::Block = selection.cursor_shape
|
||||
{
|
||||
layout
|
||||
.position_map
|
||||
.snapshot
|
||||
.chars_at(cursor_position)
|
||||
.next()
|
||||
.and_then(|(character, _)| {
|
||||
let text = if character == '\n' {
|
||||
SharedString::from(" ")
|
||||
} else {
|
||||
SharedString::from(character.to_string())
|
||||
};
|
||||
let len = text.len();
|
||||
cx.text_system()
|
||||
.shape_line(
|
||||
text,
|
||||
cursor_row_layout.font_size,
|
||||
&[TextRun {
|
||||
len,
|
||||
font: self.style.text.font(),
|
||||
color: self.style.background,
|
||||
background_color: None,
|
||||
strikethrough: None,
|
||||
underline: None,
|
||||
}],
|
||||
)
|
||||
.log_err()
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let x = cursor_character_x - layout.position_map.scroll_position.x;
|
||||
let y = cursor_position.row() as f32
|
||||
* layout.position_map.line_height
|
||||
- layout.position_map.scroll_position.y;
|
||||
if selection.is_newest {
|
||||
self.editor.update(cx, |editor, _| {
|
||||
editor.pixel_position_of_newest_cursor = Some(point(
|
||||
text_bounds.origin.x + x + block_width / 2.,
|
||||
text_bounds.origin.y
|
||||
+ y
|
||||
+ layout.position_map.line_height / 2.,
|
||||
))
|
||||
});
|
||||
}
|
||||
|
||||
let cursor = Cursor {
|
||||
color: player_color.cursor,
|
||||
block_width,
|
||||
origin: point(x, y),
|
||||
line_height: layout.position_map.line_height,
|
||||
shape: selection.cursor_shape,
|
||||
block_text,
|
||||
cursor_name: selection.user_name.clone().map(|name| {
|
||||
CursorName {
|
||||
string: name,
|
||||
color: self.style.background,
|
||||
is_top_row: cursor_position.row() == 0,
|
||||
z_index: (participant_ix % 256).try_into().unwrap(),
|
||||
}
|
||||
}),
|
||||
};
|
||||
cursor.paint(content_origin, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.paint_redactions(text_bounds, &layout, cx);
|
||||
|
||||
for (ix, line_with_invisibles) in
|
||||
layout.position_map.line_layouts.iter().enumerate()
|
||||
{
|
||||
let row = start_row + ix as u32;
|
||||
line_with_invisibles.draw(
|
||||
layout,
|
||||
row,
|
||||
content_origin,
|
||||
whitespace_setting,
|
||||
&invisible_display_ranges,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
|
||||
let corner_radius = 0.15 * layout.position_map.line_height;
|
||||
for (player_color, selections) in &layout.selections {
|
||||
for selection in selections.into_iter() {
|
||||
self.paint_highlighted_range(
|
||||
selection.range.clone(),
|
||||
player_color.selection,
|
||||
corner_radius,
|
||||
corner_radius * 2.,
|
||||
layout,
|
||||
content_origin,
|
||||
text_bounds,
|
||||
cx,
|
||||
);
|
||||
|
||||
if selection.is_local && !selection.range.is_empty() {
|
||||
invisible_display_ranges.push(selection.range.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (range, color) in &layout.highlighted_ranges {
|
||||
self.paint_highlighted_range(
|
||||
range.clone(),
|
||||
*color,
|
||||
Pixels::ZERO,
|
||||
line_end_overshoot,
|
||||
layout,
|
||||
content_origin,
|
||||
text_bounds,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
let fold_corner_radius = 0.15 * layout.position_map.line_height;
|
||||
cx.with_element_id(Some("folds"), |cx| {
|
||||
let snapshot = &layout.position_map.snapshot;
|
||||
@@ -998,152 +1153,6 @@ impl EditorElement {
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
for (range, color) in &layout.highlighted_ranges {
|
||||
self.paint_highlighted_range(
|
||||
range.clone(),
|
||||
*color,
|
||||
Pixels::ZERO,
|
||||
line_end_overshoot,
|
||||
layout,
|
||||
content_origin,
|
||||
text_bounds,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
let mut cursors = SmallVec::<[Cursor; 32]>::new();
|
||||
let corner_radius = 0.15 * layout.position_map.line_height;
|
||||
let mut invisible_display_ranges = SmallVec::<[Range<DisplayPoint>; 32]>::new();
|
||||
|
||||
for (participant_ix, (player_color, selections)) in
|
||||
layout.selections.iter().enumerate()
|
||||
{
|
||||
for selection in selections.into_iter() {
|
||||
self.paint_highlighted_range(
|
||||
selection.range.clone(),
|
||||
player_color.selection,
|
||||
corner_radius,
|
||||
corner_radius * 2.,
|
||||
layout,
|
||||
content_origin,
|
||||
text_bounds,
|
||||
cx,
|
||||
);
|
||||
|
||||
if selection.is_local && !selection.range.is_empty() {
|
||||
invisible_display_ranges.push(selection.range.clone());
|
||||
}
|
||||
|
||||
if !selection.is_local || self.editor.read(cx).show_local_cursors(cx) {
|
||||
let cursor_position = selection.head;
|
||||
if layout
|
||||
.visible_display_row_range
|
||||
.contains(&cursor_position.row())
|
||||
{
|
||||
let cursor_row_layout = &layout.position_map.line_layouts
|
||||
[(cursor_position.row() - start_row) as usize]
|
||||
.line;
|
||||
let cursor_column = cursor_position.column() as usize;
|
||||
|
||||
let cursor_character_x =
|
||||
cursor_row_layout.x_for_index(cursor_column);
|
||||
let mut block_width = cursor_row_layout
|
||||
.x_for_index(cursor_column + 1)
|
||||
- cursor_character_x;
|
||||
if block_width == Pixels::ZERO {
|
||||
block_width = layout.position_map.em_width;
|
||||
}
|
||||
let block_text = if let CursorShape::Block = selection.cursor_shape
|
||||
{
|
||||
layout
|
||||
.position_map
|
||||
.snapshot
|
||||
.chars_at(cursor_position)
|
||||
.next()
|
||||
.and_then(|(character, _)| {
|
||||
let text = if character == '\n' {
|
||||
SharedString::from(" ")
|
||||
} else {
|
||||
SharedString::from(character.to_string())
|
||||
};
|
||||
let len = text.len();
|
||||
cx.text_system()
|
||||
.shape_line(
|
||||
text,
|
||||
cursor_row_layout.font_size,
|
||||
&[TextRun {
|
||||
len,
|
||||
font: self.style.text.font(),
|
||||
color: self.style.background,
|
||||
background_color: None,
|
||||
strikethrough: None,
|
||||
underline: None,
|
||||
}],
|
||||
)
|
||||
.log_err()
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let x = cursor_character_x - layout.position_map.scroll_position.x;
|
||||
let y = cursor_position.row() as f32
|
||||
* layout.position_map.line_height
|
||||
- layout.position_map.scroll_position.y;
|
||||
if selection.is_newest {
|
||||
self.editor.update(cx, |editor, _| {
|
||||
editor.pixel_position_of_newest_cursor = Some(point(
|
||||
text_bounds.origin.x + x + block_width / 2.,
|
||||
text_bounds.origin.y
|
||||
+ y
|
||||
+ layout.position_map.line_height / 2.,
|
||||
))
|
||||
});
|
||||
}
|
||||
|
||||
cursors.push(Cursor {
|
||||
color: player_color.cursor,
|
||||
block_width,
|
||||
origin: point(x, y),
|
||||
line_height: layout.position_map.line_height,
|
||||
shape: selection.cursor_shape,
|
||||
block_text,
|
||||
cursor_name: selection.user_name.clone().map(|name| {
|
||||
CursorName {
|
||||
string: name,
|
||||
color: self.style.background,
|
||||
is_top_row: cursor_position.row() == 0,
|
||||
z_index: (participant_ix % 256).try_into().unwrap(),
|
||||
}
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (ix, line_with_invisibles) in
|
||||
layout.position_map.line_layouts.iter().enumerate()
|
||||
{
|
||||
let row = start_row + ix as u32;
|
||||
line_with_invisibles.draw(
|
||||
layout,
|
||||
row,
|
||||
content_origin,
|
||||
whitespace_setting,
|
||||
&invisible_display_ranges,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
|
||||
cx.with_z_index(0, |cx| self.paint_redactions(text_bounds, &layout, cx));
|
||||
|
||||
cx.with_z_index(1, |cx| {
|
||||
for cursor in cursors {
|
||||
cursor.paint(content_origin, cx);
|
||||
}
|
||||
});
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -1154,7 +1163,8 @@ impl EditorElement {
|
||||
layout: &LayoutState,
|
||||
cx: &mut ElementContext,
|
||||
) {
|
||||
let content_origin = text_bounds.origin + point(layout.gutter_margin, Pixels::ZERO);
|
||||
let content_origin =
|
||||
text_bounds.origin + point(layout.gutter_dimensions.margin, Pixels::ZERO);
|
||||
let line_end_overshoot = layout.line_end_overshoot();
|
||||
|
||||
// A softer than perfect black
|
||||
@@ -1180,7 +1190,8 @@ impl EditorElement {
|
||||
layout: &mut LayoutState,
|
||||
cx: &mut ElementContext,
|
||||
) {
|
||||
let content_origin = text_bounds.origin + point(layout.gutter_margin, Pixels::ZERO);
|
||||
let content_origin =
|
||||
text_bounds.origin + point(layout.gutter_dimensions.margin, Pixels::ZERO);
|
||||
let start_row = layout.visible_display_row_range.start;
|
||||
if let Some((position, mut context_menu)) = layout.context_menu.take() {
|
||||
let available_space = size(AvailableSpace::MinContent, AvailableSpace::MinContent);
|
||||
@@ -1334,132 +1345,20 @@ impl EditorElement {
|
||||
let thumb_bounds = Bounds::from_corners(point(left, thumb_top), point(right, thumb_bottom));
|
||||
|
||||
if layout.show_scrollbars {
|
||||
let scrollbar_settings = EditorSettings::get_global(cx).scrollbar;
|
||||
|
||||
cx.paint_quad(quad(
|
||||
track_bounds,
|
||||
thumb_bounds,
|
||||
Corners::default(),
|
||||
cx.theme().colors().scrollbar_track_background,
|
||||
cx.theme().colors().scrollbar_thumb_background,
|
||||
Edges {
|
||||
top: Pixels::ZERO,
|
||||
right: Pixels::ZERO,
|
||||
right: px(1.),
|
||||
bottom: Pixels::ZERO,
|
||||
left: px(1.),
|
||||
},
|
||||
cx.theme().colors().scrollbar_track_border,
|
||||
cx.theme().colors().scrollbar_thumb_border,
|
||||
));
|
||||
let scrollbar_settings = EditorSettings::get_global(cx).scrollbar;
|
||||
if layout.is_singleton && scrollbar_settings.selections {
|
||||
let start_anchor = Anchor::min();
|
||||
let end_anchor = Anchor::max();
|
||||
let background_ranges = self
|
||||
.editor
|
||||
.read(cx)
|
||||
.background_highlight_row_ranges::<BufferSearchHighlights>(
|
||||
start_anchor..end_anchor,
|
||||
&layout.position_map.snapshot,
|
||||
50000,
|
||||
);
|
||||
for range in background_ranges {
|
||||
let start_y = y_for_row(range.start().row() as f32);
|
||||
let mut end_y = y_for_row(range.end().row() as f32);
|
||||
if end_y - start_y < px(1.) {
|
||||
end_y = start_y + px(1.);
|
||||
}
|
||||
let bounds = Bounds::from_corners(point(left, start_y), point(right, end_y));
|
||||
cx.paint_quad(quad(
|
||||
bounds,
|
||||
Corners::default(),
|
||||
cx.theme().status().info,
|
||||
Edges {
|
||||
top: Pixels::ZERO,
|
||||
right: px(1.),
|
||||
bottom: Pixels::ZERO,
|
||||
left: px(1.),
|
||||
},
|
||||
cx.theme().colors().scrollbar_thumb_border,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if layout.is_singleton && scrollbar_settings.symbols_selections {
|
||||
let selection_ranges = self.editor.read(cx).background_highlights_in_range(
|
||||
Anchor::min()..Anchor::max(),
|
||||
&layout.position_map.snapshot,
|
||||
cx.theme().colors(),
|
||||
);
|
||||
for hunk in selection_ranges {
|
||||
let start_display = Point::new(hunk.0.start.row(), 0)
|
||||
.to_display_point(&layout.position_map.snapshot.display_snapshot);
|
||||
let end_display = Point::new(hunk.0.end.row(), 0)
|
||||
.to_display_point(&layout.position_map.snapshot.display_snapshot);
|
||||
let start_y = y_for_row(start_display.row() as f32);
|
||||
let mut end_y = if hunk.0.start == hunk.0.end {
|
||||
y_for_row((end_display.row() + 1) as f32)
|
||||
} else {
|
||||
y_for_row((end_display.row()) as f32)
|
||||
};
|
||||
|
||||
if end_y - start_y < px(1.) {
|
||||
end_y = start_y + px(1.);
|
||||
}
|
||||
let bounds = Bounds::from_corners(point(left, start_y), point(right, end_y));
|
||||
|
||||
cx.paint_quad(quad(
|
||||
bounds,
|
||||
Corners::default(),
|
||||
cx.theme().status().info,
|
||||
Edges {
|
||||
top: Pixels::ZERO,
|
||||
right: px(1.),
|
||||
bottom: Pixels::ZERO,
|
||||
left: px(1.),
|
||||
},
|
||||
cx.theme().colors().scrollbar_thumb_border,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if layout.is_singleton && scrollbar_settings.git_diff {
|
||||
for hunk in layout
|
||||
.position_map
|
||||
.snapshot
|
||||
.buffer_snapshot
|
||||
.git_diff_hunks_in_range(0..(max_row.floor() as u32))
|
||||
{
|
||||
let start_display = Point::new(hunk.buffer_range.start, 0)
|
||||
.to_display_point(&layout.position_map.snapshot.display_snapshot);
|
||||
let end_display = Point::new(hunk.buffer_range.end, 0)
|
||||
.to_display_point(&layout.position_map.snapshot.display_snapshot);
|
||||
let start_y = y_for_row(start_display.row() as f32);
|
||||
let mut end_y = if hunk.buffer_range.start == hunk.buffer_range.end {
|
||||
y_for_row((end_display.row() + 1) as f32)
|
||||
} else {
|
||||
y_for_row((end_display.row()) as f32)
|
||||
};
|
||||
|
||||
if end_y - start_y < px(1.) {
|
||||
end_y = start_y + px(1.);
|
||||
}
|
||||
let bounds = Bounds::from_corners(point(left, start_y), point(right, end_y));
|
||||
|
||||
let color = match hunk.status() {
|
||||
DiffHunkStatus::Added => cx.theme().status().created,
|
||||
DiffHunkStatus::Modified => cx.theme().status().modified,
|
||||
DiffHunkStatus::Removed => cx.theme().status().deleted,
|
||||
};
|
||||
cx.paint_quad(quad(
|
||||
bounds,
|
||||
Corners::default(),
|
||||
color,
|
||||
Edges {
|
||||
top: Pixels::ZERO,
|
||||
right: px(1.),
|
||||
bottom: Pixels::ZERO,
|
||||
left: px(1.),
|
||||
},
|
||||
cx.theme().colors().scrollbar_thumb_border,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if layout.is_singleton && scrollbar_settings.diagnostics {
|
||||
let max_point = layout
|
||||
@@ -1519,17 +1418,131 @@ impl EditorElement {
|
||||
}
|
||||
}
|
||||
|
||||
if layout.is_singleton && scrollbar_settings.git_diff {
|
||||
for hunk in layout
|
||||
.position_map
|
||||
.snapshot
|
||||
.buffer_snapshot
|
||||
.git_diff_hunks_in_range(0..(max_row.floor() as u32))
|
||||
{
|
||||
let start_display = Point::new(hunk.buffer_range.start, 0)
|
||||
.to_display_point(&layout.position_map.snapshot.display_snapshot);
|
||||
let end_display = Point::new(hunk.buffer_range.end, 0)
|
||||
.to_display_point(&layout.position_map.snapshot.display_snapshot);
|
||||
let start_y = y_for_row(start_display.row() as f32);
|
||||
let mut end_y = if hunk.buffer_range.start == hunk.buffer_range.end {
|
||||
y_for_row((end_display.row() + 1) as f32)
|
||||
} else {
|
||||
y_for_row((end_display.row()) as f32)
|
||||
};
|
||||
|
||||
if end_y - start_y < px(1.) {
|
||||
end_y = start_y + px(1.);
|
||||
}
|
||||
let bounds = Bounds::from_corners(point(left, start_y), point(right, end_y));
|
||||
|
||||
let color = match hunk.status() {
|
||||
DiffHunkStatus::Added => cx.theme().status().created,
|
||||
DiffHunkStatus::Modified => cx.theme().status().modified,
|
||||
DiffHunkStatus::Removed => cx.theme().status().deleted,
|
||||
};
|
||||
cx.paint_quad(quad(
|
||||
bounds,
|
||||
Corners::default(),
|
||||
color,
|
||||
Edges {
|
||||
top: Pixels::ZERO,
|
||||
right: px(1.),
|
||||
bottom: Pixels::ZERO,
|
||||
left: px(1.),
|
||||
},
|
||||
cx.theme().colors().scrollbar_thumb_border,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if layout.is_singleton && scrollbar_settings.symbols_selections {
|
||||
let selection_ranges = self.editor.read(cx).background_highlights_in_range(
|
||||
Anchor::min()..Anchor::max(),
|
||||
&layout.position_map.snapshot,
|
||||
cx.theme().colors(),
|
||||
);
|
||||
for hunk in selection_ranges {
|
||||
let start_display = Point::new(hunk.0.start.row(), 0)
|
||||
.to_display_point(&layout.position_map.snapshot.display_snapshot);
|
||||
let end_display = Point::new(hunk.0.end.row(), 0)
|
||||
.to_display_point(&layout.position_map.snapshot.display_snapshot);
|
||||
let start_y = y_for_row(start_display.row() as f32);
|
||||
let mut end_y = if hunk.0.start == hunk.0.end {
|
||||
y_for_row((end_display.row() + 1) as f32)
|
||||
} else {
|
||||
y_for_row((end_display.row()) as f32)
|
||||
};
|
||||
|
||||
if end_y - start_y < px(1.) {
|
||||
end_y = start_y + px(1.);
|
||||
}
|
||||
let bounds = Bounds::from_corners(point(left, start_y), point(right, end_y));
|
||||
|
||||
cx.paint_quad(quad(
|
||||
bounds,
|
||||
Corners::default(),
|
||||
cx.theme().status().info,
|
||||
Edges {
|
||||
top: Pixels::ZERO,
|
||||
right: px(1.),
|
||||
bottom: Pixels::ZERO,
|
||||
left: px(1.),
|
||||
},
|
||||
cx.theme().colors().scrollbar_thumb_border,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if layout.is_singleton && scrollbar_settings.selections {
|
||||
let start_anchor = Anchor::min();
|
||||
let end_anchor = Anchor::max();
|
||||
let background_ranges = self
|
||||
.editor
|
||||
.read(cx)
|
||||
.background_highlight_row_ranges::<BufferSearchHighlights>(
|
||||
start_anchor..end_anchor,
|
||||
&layout.position_map.snapshot,
|
||||
50000,
|
||||
);
|
||||
for range in background_ranges {
|
||||
let start_y = y_for_row(range.start().row() as f32);
|
||||
let mut end_y = y_for_row(range.end().row() as f32);
|
||||
if end_y - start_y < px(1.) {
|
||||
end_y = start_y + px(1.);
|
||||
}
|
||||
let bounds = Bounds::from_corners(point(left, start_y), point(right, end_y));
|
||||
cx.paint_quad(quad(
|
||||
bounds,
|
||||
Corners::default(),
|
||||
cx.theme().status().info,
|
||||
Edges {
|
||||
top: Pixels::ZERO,
|
||||
right: px(1.),
|
||||
bottom: Pixels::ZERO,
|
||||
left: px(1.),
|
||||
},
|
||||
cx.theme().colors().scrollbar_thumb_border,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
cx.paint_quad(quad(
|
||||
thumb_bounds,
|
||||
track_bounds,
|
||||
Corners::default(),
|
||||
cx.theme().colors().scrollbar_thumb_background,
|
||||
cx.theme().colors().scrollbar_track_background,
|
||||
Edges {
|
||||
top: Pixels::ZERO,
|
||||
right: px(1.),
|
||||
right: Pixels::ZERO,
|
||||
bottom: Pixels::ZERO,
|
||||
left: px(1.),
|
||||
},
|
||||
cx.theme().colors().scrollbar_thumb_border,
|
||||
cx.theme().colors().scrollbar_track_border,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -1817,7 +1830,10 @@ impl EditorElement {
|
||||
Vec<Option<(FoldStatus, BufferRow, bool)>>,
|
||||
) {
|
||||
let font_size = self.style.text.font_size.to_pixels(cx.rem_size());
|
||||
let include_line_numbers = snapshot.mode == EditorMode::Full;
|
||||
let include_line_numbers =
|
||||
EditorSettings::get_global(cx).gutter.line_numbers && snapshot.mode == EditorMode::Full;
|
||||
let include_fold_statuses =
|
||||
EditorSettings::get_global(cx).gutter.folds && snapshot.mode == EditorMode::Full;
|
||||
let mut shaped_line_numbers = Vec::with_capacity(rows.len());
|
||||
let mut fold_statuses = Vec::with_capacity(rows.len());
|
||||
let mut line_number = String::new();
|
||||
@@ -1862,6 +1878,8 @@ impl EditorElement {
|
||||
.shape_line(line_number.clone().into(), font_size, &[run])
|
||||
.unwrap();
|
||||
shaped_line_numbers.push(Some(shaped_line));
|
||||
}
|
||||
if include_fold_statuses {
|
||||
fold_statuses.push(
|
||||
is_singleton
|
||||
.then(|| {
|
||||
@@ -1958,7 +1976,13 @@ impl EditorElement {
|
||||
.unwrap()
|
||||
.width;
|
||||
|
||||
let gutter_dimensions = snapshot.gutter_dimensions(font_id, font_size, em_width, self.max_line_number_width(&snapshot, cx), cx);
|
||||
let gutter_dimensions = snapshot.gutter_dimensions(
|
||||
font_id,
|
||||
font_size,
|
||||
em_width,
|
||||
self.max_line_number_width(&snapshot, cx),
|
||||
cx,
|
||||
);
|
||||
|
||||
editor.gutter_width = gutter_dimensions.width;
|
||||
|
||||
@@ -2211,8 +2235,7 @@ impl EditorElement {
|
||||
bounds.size.width,
|
||||
scroll_width,
|
||||
text_width,
|
||||
gutter_dimensions.padding,
|
||||
gutter_dimensions.width,
|
||||
&gutter_dimensions,
|
||||
em_width,
|
||||
gutter_dimensions.width + gutter_dimensions.margin,
|
||||
line_height,
|
||||
@@ -2249,6 +2272,8 @@ impl EditorElement {
|
||||
snapshot = editor.snapshot(cx);
|
||||
}
|
||||
|
||||
let gutter_settings = EditorSettings::get_global(cx).gutter;
|
||||
|
||||
let mut context_menu = None;
|
||||
let mut code_actions_indicator = None;
|
||||
if let Some(newest_selection_head) = newest_selection_head {
|
||||
@@ -2270,12 +2295,14 @@ impl EditorElement {
|
||||
Some(crate::ContextMenu::CodeActions(_))
|
||||
);
|
||||
|
||||
code_actions_indicator = editor
|
||||
.render_code_actions_indicator(&style, active, cx)
|
||||
.map(|element| CodeActionsIndicator {
|
||||
row: newest_selection_head.row(),
|
||||
button: element,
|
||||
});
|
||||
if gutter_settings.code_actions {
|
||||
code_actions_indicator = editor
|
||||
.render_code_actions_indicator(&style, active, cx)
|
||||
.map(|element| CodeActionsIndicator {
|
||||
row: newest_selection_head.row(),
|
||||
button: element,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2293,29 +2320,32 @@ impl EditorElement {
|
||||
None
|
||||
} else {
|
||||
editor.hover_state.render(
|
||||
&snapshot,
|
||||
&style,
|
||||
visible_rows,
|
||||
max_size,
|
||||
editor.workspace.as_ref().map(|(w, _)| w.clone()),
|
||||
cx,
|
||||
)
|
||||
&snapshot,
|
||||
&style,
|
||||
visible_rows,
|
||||
max_size,
|
||||
editor.workspace.as_ref().map(|(w, _)| w.clone()),
|
||||
cx,
|
||||
)
|
||||
};
|
||||
|
||||
let editor_view = cx.view().clone();
|
||||
let fold_indicators = cx.with_element_context(|cx| {
|
||||
|
||||
cx.with_element_id(Some("gutter_fold_indicators"), |_cx| {
|
||||
editor.render_fold_indicators(
|
||||
fold_statuses,
|
||||
&style,
|
||||
editor.gutter_hovered,
|
||||
line_height,
|
||||
gutter_dimensions.margin,
|
||||
editor_view,
|
||||
)
|
||||
})
|
||||
});
|
||||
let fold_indicators = if gutter_settings.folds {
|
||||
cx.with_element_context(|cx| {
|
||||
cx.with_element_id(Some("gutter_fold_indicators"), |_cx| {
|
||||
editor.render_fold_indicators(
|
||||
fold_statuses,
|
||||
&style,
|
||||
editor.gutter_hovered,
|
||||
line_height,
|
||||
gutter_dimensions.margin,
|
||||
editor_view,
|
||||
)
|
||||
})
|
||||
})
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
let invisible_symbol_font_size = font_size / 2.;
|
||||
let tab_invisible = cx
|
||||
@@ -2368,13 +2398,12 @@ impl EditorElement {
|
||||
visible_display_row_range: start_row..end_row,
|
||||
wrap_guides,
|
||||
gutter_size,
|
||||
gutter_padding: gutter_dimensions.padding,
|
||||
gutter_dimensions,
|
||||
text_size,
|
||||
scrollbar_row_range,
|
||||
show_scrollbars,
|
||||
is_singleton,
|
||||
max_row,
|
||||
gutter_margin: gutter_dimensions.margin,
|
||||
active_rows,
|
||||
highlighted_rows,
|
||||
highlighted_ranges,
|
||||
@@ -2401,8 +2430,7 @@ impl EditorElement {
|
||||
editor_width: Pixels,
|
||||
scroll_width: Pixels,
|
||||
text_width: Pixels,
|
||||
gutter_padding: Pixels,
|
||||
gutter_width: Pixels,
|
||||
gutter_dimensions: &GutterDimensions,
|
||||
em_width: Pixels,
|
||||
text_x: Pixels,
|
||||
line_height: Pixels,
|
||||
@@ -2445,9 +2473,8 @@ impl EditorElement {
|
||||
block.render(&mut BlockContext {
|
||||
context: cx,
|
||||
anchor_x,
|
||||
gutter_padding,
|
||||
gutter_dimensions,
|
||||
line_height,
|
||||
gutter_width,
|
||||
em_width,
|
||||
block_id,
|
||||
max_width: scroll_width.max(text_width),
|
||||
@@ -2551,12 +2578,14 @@ impl EditorElement {
|
||||
h_flex()
|
||||
.id(("collapsed context", block_id))
|
||||
.size_full()
|
||||
.gap(gutter_padding)
|
||||
.gap(gutter_dimensions.left_padding + gutter_dimensions.right_padding)
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_end()
|
||||
.flex_none()
|
||||
.w(gutter_width - gutter_padding)
|
||||
.w(gutter_dimensions.width
|
||||
- (gutter_dimensions.left_padding
|
||||
+ gutter_dimensions.right_padding))
|
||||
.h_full()
|
||||
.text_buffer(cx)
|
||||
.text_color(cx.theme().colors().editor_line_number)
|
||||
@@ -2617,7 +2646,7 @@ impl EditorElement {
|
||||
BlockStyle::Sticky => editor_width,
|
||||
BlockStyle::Flex => editor_width
|
||||
.max(fixed_block_max_width)
|
||||
.max(gutter_width + scroll_width),
|
||||
.max(gutter_dimensions.width + scroll_width),
|
||||
BlockStyle::Fixed => unreachable!(),
|
||||
};
|
||||
let available_space = size(
|
||||
@@ -2634,7 +2663,7 @@ impl EditorElement {
|
||||
});
|
||||
}
|
||||
(
|
||||
scroll_width.max(fixed_block_max_width - gutter_width),
|
||||
scroll_width.max(fixed_block_max_width - gutter_dimensions.width),
|
||||
blocks,
|
||||
)
|
||||
}
|
||||
@@ -3098,34 +3127,25 @@ impl Element for EditorElement {
|
||||
ElementInputHandler::new(bounds, self.editor.clone()),
|
||||
);
|
||||
|
||||
self.paint_background(gutter_bounds, text_bounds, &layout, cx);
|
||||
self.paint_scrollbar(bounds, &mut layout, cx);
|
||||
self.paint_overlays(text_bounds, &mut layout, cx);
|
||||
if !layout.blocks.is_empty() {
|
||||
cx.with_element_id(Some("editor_blocks"), |cx| {
|
||||
self.paint_blocks(bounds, &mut layout, cx);
|
||||
});
|
||||
}
|
||||
self.paint_mouse_listeners(
|
||||
bounds,
|
||||
gutter_bounds,
|
||||
text_bounds,
|
||||
&layout,
|
||||
cx,
|
||||
);
|
||||
self.paint_text(text_bounds, &mut layout, cx);
|
||||
if layout.gutter_size.width > Pixels::ZERO {
|
||||
self.paint_gutter(gutter_bounds, &mut layout, cx);
|
||||
}
|
||||
self.paint_text(text_bounds, &mut layout, cx);
|
||||
|
||||
cx.with_z_index(0, |cx| {
|
||||
self.paint_mouse_listeners(
|
||||
bounds,
|
||||
gutter_bounds,
|
||||
text_bounds,
|
||||
&layout,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
if !layout.blocks.is_empty() {
|
||||
cx.with_z_index(0, |cx| {
|
||||
cx.with_element_id(Some("editor_blocks"), |cx| {
|
||||
self.paint_blocks(bounds, &mut layout, cx);
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
cx.with_z_index(1, |cx| {
|
||||
self.paint_overlays(text_bounds, &mut layout, cx);
|
||||
});
|
||||
|
||||
cx.with_z_index(2, |cx| self.paint_scrollbar(bounds, &mut layout, cx));
|
||||
self.paint_background(gutter_bounds, text_bounds, &layout, cx);
|
||||
});
|
||||
})
|
||||
},
|
||||
@@ -3151,8 +3171,7 @@ type BufferRow = u32;
|
||||
pub struct LayoutState {
|
||||
position_map: Arc<PositionMap>,
|
||||
gutter_size: Size<Pixels>,
|
||||
gutter_padding: Pixels,
|
||||
gutter_margin: Pixels,
|
||||
gutter_dimensions: GutterDimensions,
|
||||
text_size: gpui::Size<Pixels>,
|
||||
mode: EditorMode,
|
||||
wrap_guides: SmallVec<[(Pixels, bool); 2]>,
|
||||
|
||||
@@ -138,7 +138,7 @@ impl Editor {
|
||||
cx.focus(&self.focus_handle);
|
||||
}
|
||||
|
||||
self.navigate_to_hover_links(hovered_link_state.links, modifiers.alt, cx);
|
||||
self.navigate_to_hover_links(None, hovered_link_state.links, modifiers.alt, cx);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::{
|
||||
DisplayPoint, Editor, EditorMode, FindAllReferences, GoToDefinition, GoToTypeDefinition,
|
||||
Rename, RevealInFinder, SelectMode, ToggleCodeActions,
|
||||
DisplayPoint, Editor, EditorMode, FindAllReferences, GoToDefinition, GoToImplementation,
|
||||
GoToTypeDefinition, Rename, RevealInFinder, SelectMode, ToggleCodeActions,
|
||||
};
|
||||
use gpui::{DismissEvent, Pixels, Point, Subscription, View, ViewContext};
|
||||
|
||||
@@ -48,6 +48,7 @@ pub fn deploy_context_menu(
|
||||
menu.action("Rename Symbol", Box::new(Rename))
|
||||
.action("Go to Definition", Box::new(GoToDefinition))
|
||||
.action("Go to Type Definition", Box::new(GoToTypeDefinition))
|
||||
.action("Go to Implementation", Box::new(GoToImplementation))
|
||||
.action("Find All References", Box::new(FindAllReferences))
|
||||
.action(
|
||||
"Code Actions",
|
||||
|
||||
@@ -34,7 +34,7 @@ pub struct ExtensionsApiResponse {
|
||||
pub data: Vec<Extension>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[derive(Clone, Deserialize)]
|
||||
pub struct Extension {
|
||||
pub id: Arc<str>,
|
||||
pub version: Arc<str>,
|
||||
|
||||
@@ -11,7 +11,7 @@ use settings::Settings;
|
||||
use std::time::Duration;
|
||||
use std::{ops::Range, sync::Arc};
|
||||
use theme::ThemeSettings;
|
||||
use ui::prelude::*;
|
||||
use ui::{prelude::*, CheckboxWithLabel, Tooltip};
|
||||
|
||||
use workspace::{
|
||||
item::{Item, ItemEvent},
|
||||
@@ -34,7 +34,8 @@ pub struct ExtensionsPage {
|
||||
list: UniformListScrollHandle,
|
||||
telemetry: Arc<Telemetry>,
|
||||
is_fetching_extensions: bool,
|
||||
extensions_entries: Vec<Extension>,
|
||||
is_only_showing_installed_extensions: bool,
|
||||
extension_entries: Vec<Extension>,
|
||||
query_editor: View<Editor>,
|
||||
query_contains_error: bool,
|
||||
_subscription: gpui::Subscription,
|
||||
@@ -54,7 +55,8 @@ impl ExtensionsPage {
|
||||
list: UniformListScrollHandle::new(),
|
||||
telemetry: workspace.client().telemetry().clone(),
|
||||
is_fetching_extensions: false,
|
||||
extensions_entries: Vec::new(),
|
||||
is_only_showing_installed_extensions: false,
|
||||
extension_entries: Vec::new(),
|
||||
query_contains_error: false,
|
||||
extension_fetch_task: None,
|
||||
_subscription: subscription,
|
||||
@@ -65,6 +67,24 @@ impl ExtensionsPage {
|
||||
})
|
||||
}
|
||||
|
||||
fn filtered_extension_entries(&self, cx: &mut ViewContext<Self>) -> Vec<Extension> {
|
||||
let extension_store = ExtensionStore::global(cx).read(cx);
|
||||
|
||||
self.extension_entries
|
||||
.iter()
|
||||
.filter(|extension| {
|
||||
if self.is_only_showing_installed_extensions {
|
||||
let status = extension_store.extension_status(&extension.id);
|
||||
|
||||
matches!(status, ExtensionStatus::Installed(_))
|
||||
} else {
|
||||
true
|
||||
}
|
||||
})
|
||||
.cloned()
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
|
||||
fn install_extension(
|
||||
&self,
|
||||
extension_id: Arc<str>,
|
||||
@@ -94,7 +114,7 @@ impl ExtensionsPage {
|
||||
let fetch_result = extensions.await;
|
||||
match fetch_result {
|
||||
Ok(extensions) => this.update(&mut cx, |this, cx| {
|
||||
this.extensions_entries = extensions;
|
||||
this.extension_entries = extensions;
|
||||
this.is_fetching_extensions = false;
|
||||
cx.notify();
|
||||
}),
|
||||
@@ -113,7 +133,7 @@ impl ExtensionsPage {
|
||||
}
|
||||
|
||||
fn render_extensions(&mut self, range: Range<usize>, cx: &mut ViewContext<Self>) -> Vec<Div> {
|
||||
self.extensions_entries[range]
|
||||
self.filtered_extension_entries(cx)[range]
|
||||
.iter()
|
||||
.map(|extension| self.render_entry(extension, cx))
|
||||
.collect()
|
||||
@@ -195,6 +215,7 @@ impl ExtensionsPage {
|
||||
.color(Color::Accent);
|
||||
|
||||
let repository_url = extension.repository.clone();
|
||||
let tooltip_text = Tooltip::text(repository_url.clone(), cx);
|
||||
|
||||
div().w_full().child(
|
||||
v_flex()
|
||||
@@ -269,7 +290,8 @@ impl ExtensionsPage {
|
||||
.style(ButtonStyle::Filled)
|
||||
.on_click(cx.listener(move |_, _, cx| {
|
||||
cx.open_url(&repository_url);
|
||||
})),
|
||||
}))
|
||||
.tooltip(move |_| tooltip_text.clone()),
|
||||
),
|
||||
),
|
||||
)
|
||||
@@ -379,10 +401,32 @@ impl ExtensionsPage {
|
||||
Some(search)
|
||||
}
|
||||
}
|
||||
|
||||
fn render_empty_state(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let has_search = self.search_query(cx).is_some();
|
||||
|
||||
let message = if self.is_fetching_extensions {
|
||||
"Loading extensions..."
|
||||
} else if self.is_only_showing_installed_extensions {
|
||||
if has_search {
|
||||
"No installed extensions that match your search."
|
||||
} else {
|
||||
"No installed extensions."
|
||||
}
|
||||
} else {
|
||||
if has_search {
|
||||
"No extensions that match your search."
|
||||
} else {
|
||||
"No extensions."
|
||||
}
|
||||
};
|
||||
|
||||
Label::new(message)
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ExtensionsPage {
|
||||
fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> impl IntoElement {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
v_flex()
|
||||
.size_full()
|
||||
.p_4()
|
||||
@@ -393,25 +437,39 @@ impl Render for ExtensionsPage {
|
||||
.w_full()
|
||||
.child(Headline::new("Extensions").size(HeadlineSize::XLarge)),
|
||||
)
|
||||
.child(h_flex().w_56().child(self.render_search(cx)))
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.gap_2()
|
||||
.child(h_flex().child(self.render_search(cx)))
|
||||
.child(CheckboxWithLabel::new(
|
||||
"installed",
|
||||
Label::new("Only show installed"),
|
||||
if self.is_only_showing_installed_extensions {
|
||||
Selection::Selected
|
||||
} else {
|
||||
Selection::Unselected
|
||||
},
|
||||
cx.listener(|this, selection, _cx| {
|
||||
this.is_only_showing_installed_extensions = match selection {
|
||||
Selection::Selected => true,
|
||||
Selection::Unselected => false,
|
||||
Selection::Indeterminate => return,
|
||||
}
|
||||
}),
|
||||
)),
|
||||
)
|
||||
.child(v_flex().size_full().overflow_y_hidden().map(|this| {
|
||||
if self.extensions_entries.is_empty() {
|
||||
let message = if self.is_fetching_extensions {
|
||||
"Loading extensions..."
|
||||
} else if self.search_query(cx).is_some() {
|
||||
"No extensions that match your search."
|
||||
} else {
|
||||
"No extensions."
|
||||
};
|
||||
|
||||
return this.child(Label::new(message));
|
||||
let entries = self.filtered_extension_entries(cx);
|
||||
if entries.is_empty() {
|
||||
return this.child(self.render_empty_state(cx));
|
||||
}
|
||||
|
||||
this.child(
|
||||
canvas({
|
||||
let view = cx.view().clone();
|
||||
let scroll_handle = self.list.clone();
|
||||
let item_count = self.extensions_entries.len();
|
||||
let item_count = entries.len();
|
||||
move |bounds, cx| {
|
||||
uniform_list::<_, Div, _>(
|
||||
view,
|
||||
|
||||
@@ -32,11 +32,17 @@ log.workspace = true
|
||||
libc = "0.2"
|
||||
time.workspace = true
|
||||
|
||||
gpui = { workspace = true, optional = true}
|
||||
gpui = { workspace = true, optional = true }
|
||||
|
||||
[target.'cfg(not(target_os = "macos"))'.dependencies]
|
||||
notify = "6.1.1"
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
windows-sys = { version = "0.52", features = [
|
||||
"Win32_Foundation",
|
||||
"Win32_Storage_FileSystem",
|
||||
] }
|
||||
|
||||
[dev-dependencies]
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
|
||||
|
||||
@@ -245,9 +245,8 @@ impl Fs for RealFs {
|
||||
#[cfg(unix)]
|
||||
let inode = metadata.ino();
|
||||
|
||||
// todo!("windows")
|
||||
#[cfg(windows)]
|
||||
let inode = 0;
|
||||
let inode = file_id(path).await?;
|
||||
|
||||
Ok(Some(Metadata {
|
||||
inode,
|
||||
@@ -1337,6 +1336,41 @@ pub fn copy_recursive<'a>(
|
||||
.boxed()
|
||||
}
|
||||
|
||||
// todo!(windows)
|
||||
// can we get file id not open the file twice?
|
||||
// https://github.com/rust-lang/rust/issues/63010
|
||||
#[cfg(target_os = "windows")]
|
||||
async fn file_id(path: impl AsRef<Path>) -> Result<u64> {
|
||||
use std::os::windows::io::AsRawHandle;
|
||||
|
||||
use smol::fs::windows::OpenOptionsExt;
|
||||
use windows_sys::Win32::{
|
||||
Foundation::HANDLE,
|
||||
Storage::FileSystem::{
|
||||
GetFileInformationByHandle, BY_HANDLE_FILE_INFORMATION, FILE_FLAG_BACKUP_SEMANTICS,
|
||||
},
|
||||
};
|
||||
|
||||
let file = smol::fs::OpenOptions::new()
|
||||
.read(true)
|
||||
.custom_flags(FILE_FLAG_BACKUP_SEMANTICS)
|
||||
.open(path)
|
||||
.await?;
|
||||
|
||||
let mut info: BY_HANDLE_FILE_INFORMATION = unsafe { std::mem::zeroed() };
|
||||
// https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getfileinformationbyhandle
|
||||
// This function supports Windows XP+
|
||||
smol::unblock(move || {
|
||||
let ret = unsafe { GetFileInformationByHandle(file.as_raw_handle() as HANDLE, &mut info) };
|
||||
if ret == 0 {
|
||||
return Err(anyhow!(format!("{}", std::io::Error::last_os_error())));
|
||||
};
|
||||
|
||||
Ok(((info.nFileIndexHigh as u64) << 32) | (info.nFileIndexLow as u64))
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -51,6 +51,7 @@ parking = "2.0.0"
|
||||
parking_lot.workspace = true
|
||||
pathfinder_geometry = "0.5"
|
||||
postage.workspace = true
|
||||
profiling.workspace = true
|
||||
rand.workspace = true
|
||||
raw-window-handle = "0.6"
|
||||
refineable.workspace = true
|
||||
@@ -115,3 +116,11 @@ blade-macros.workspace = true
|
||||
blade-rwh.workspace = true
|
||||
bytemuck = "1"
|
||||
cosmic-text = "0.10.0"
|
||||
|
||||
[[example]]
|
||||
name = "hello_world"
|
||||
path = "examples/hello_world.rs"
|
||||
|
||||
[[example]]
|
||||
name = "image"
|
||||
path = "examples/image/image.rs"
|
||||
|
||||
BIN
crates/gpui/examples/image/app-icon.png
Normal file
BIN
crates/gpui/examples/image/app-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 164 KiB |
@@ -64,9 +64,8 @@ fn main() {
|
||||
App::new().run(|cx: &mut AppContext| {
|
||||
cx.open_window(WindowOptions::default(), |cx| {
|
||||
cx.new_view(|_cx| ImageShowcase {
|
||||
local_resource: Arc::new(
|
||||
PathBuf::from_str("crates/zed/resources/app-icon.png").unwrap(),
|
||||
),
|
||||
// Relative path to your root project path
|
||||
local_resource: Arc::new(PathBuf::from_str("examples/image/app-icon.png").unwrap()),
|
||||
remote_resource: "https://picsum.photos/512/512".into(),
|
||||
})
|
||||
});
|
||||
@@ -1106,8 +1106,14 @@ impl AppContext {
|
||||
for window in self.windows() {
|
||||
window
|
||||
.update(self, |_, cx| {
|
||||
cx.window.rendered_frame.clear_pending_keystrokes();
|
||||
cx.window.next_frame.clear_pending_keystrokes();
|
||||
cx.window
|
||||
.rendered_frame
|
||||
.dispatch_tree
|
||||
.clear_pending_keystrokes();
|
||||
cx.window
|
||||
.next_frame
|
||||
.dispatch_tree
|
||||
.clear_pending_keystrokes();
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
435
crates/gpui/src/bounds_tree.rs
Normal file
435
crates/gpui/src/bounds_tree.rs
Normal file
@@ -0,0 +1,435 @@
|
||||
use crate::{Bounds, Half};
|
||||
use std::{
|
||||
cmp,
|
||||
fmt::Debug,
|
||||
ops::{Add, Sub},
|
||||
};
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct BoundsTree<U: Default + Clone + Debug> {
|
||||
root: Option<usize>,
|
||||
nodes: Vec<Node<U>>,
|
||||
stack: Vec<usize>,
|
||||
}
|
||||
|
||||
impl<U> BoundsTree<U>
|
||||
where
|
||||
U: Clone + Debug + PartialOrd + Add<U, Output = U> + Sub<Output = U> + Half + Default,
|
||||
{
|
||||
pub fn new() -> Self {
|
||||
BoundsTree::default()
|
||||
}
|
||||
|
||||
pub fn clear(&mut self) {
|
||||
self.root = None;
|
||||
self.nodes.clear();
|
||||
self.stack.clear();
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, new_bounds: Bounds<U>) -> u32 {
|
||||
// If the tree is empty, make the root the new leaf.
|
||||
if self.root.is_none() {
|
||||
let new_node = self.push_leaf(new_bounds, 1);
|
||||
self.root = Some(new_node);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Search for the best place to add the new leaf based on heuristics.
|
||||
let mut max_intersecting_ordering = 0;
|
||||
let mut index = self.root.unwrap();
|
||||
while let Node::Internal {
|
||||
left,
|
||||
right,
|
||||
bounds: node_bounds,
|
||||
..
|
||||
} = self.node_mut(index)
|
||||
{
|
||||
let left = *left;
|
||||
let right = *right;
|
||||
*node_bounds = node_bounds.union(&new_bounds);
|
||||
self.stack.push(index);
|
||||
|
||||
// Descend to the best-fit child, based on which one would increase
|
||||
// the surface area the least. This attempts to keep the tree balanced
|
||||
// in terms of surface area. If there is an intersection with the other child,
|
||||
// add its keys to the intersections vector.
|
||||
let left_cost = new_bounds.union(self.node(left).bounds()).half_perimeter();
|
||||
let right_cost = new_bounds.union(self.node(right).bounds()).half_perimeter();
|
||||
if left_cost < right_cost {
|
||||
max_intersecting_ordering =
|
||||
self.find_max_ordering(right, &new_bounds, max_intersecting_ordering);
|
||||
index = left;
|
||||
} else {
|
||||
max_intersecting_ordering =
|
||||
self.find_max_ordering(left, &new_bounds, max_intersecting_ordering);
|
||||
index = right;
|
||||
}
|
||||
}
|
||||
|
||||
// We've found a leaf ('index' now refers to a leaf node).
|
||||
// We'll insert a new parent node above the leaf and attach our new leaf to it.
|
||||
let sibling = index;
|
||||
|
||||
// Check for collision with the located leaf node
|
||||
let Node::Leaf {
|
||||
bounds: sibling_bounds,
|
||||
order: sibling_ordering,
|
||||
..
|
||||
} = self.node(index)
|
||||
else {
|
||||
unreachable!();
|
||||
};
|
||||
if sibling_bounds.intersects(&new_bounds) {
|
||||
max_intersecting_ordering = cmp::max(max_intersecting_ordering, *sibling_ordering);
|
||||
}
|
||||
|
||||
let ordering = max_intersecting_ordering + 1;
|
||||
let new_node = self.push_leaf(new_bounds, ordering);
|
||||
let new_parent = self.push_internal(sibling, new_node);
|
||||
|
||||
// If there was an old parent, we need to update its children indices.
|
||||
if let Some(old_parent) = self.stack.last().copied() {
|
||||
let Node::Internal { left, right, .. } = self.node_mut(old_parent) else {
|
||||
unreachable!();
|
||||
};
|
||||
|
||||
if *left == sibling {
|
||||
*left = new_parent;
|
||||
} else {
|
||||
*right = new_parent;
|
||||
}
|
||||
} else {
|
||||
// If the old parent was the root, the new parent is the new root.
|
||||
self.root = Some(new_parent);
|
||||
}
|
||||
|
||||
for node_index in self.stack.drain(..) {
|
||||
let Node::Internal { max_ordering, .. } = &mut self.nodes[node_index] else {
|
||||
unreachable!()
|
||||
};
|
||||
*max_ordering = cmp::max(*max_ordering, ordering);
|
||||
}
|
||||
|
||||
ordering
|
||||
}
|
||||
|
||||
fn find_max_ordering(&self, index: usize, bounds: &Bounds<U>, mut max_ordering: u32) -> u32 {
|
||||
match self.node(index) {
|
||||
Node::Leaf {
|
||||
bounds: node_bounds,
|
||||
order: ordering,
|
||||
..
|
||||
} => {
|
||||
if bounds.intersects(node_bounds) {
|
||||
max_ordering = cmp::max(*ordering, max_ordering);
|
||||
}
|
||||
}
|
||||
Node::Internal {
|
||||
left,
|
||||
right,
|
||||
bounds: node_bounds,
|
||||
max_ordering: node_max_ordering,
|
||||
..
|
||||
} => {
|
||||
if bounds.intersects(node_bounds) && max_ordering < *node_max_ordering {
|
||||
let left_max_ordering = self.node(*left).max_ordering();
|
||||
let right_max_ordering = self.node(*right).max_ordering();
|
||||
if left_max_ordering > right_max_ordering {
|
||||
max_ordering = self.find_max_ordering(*left, bounds, max_ordering);
|
||||
max_ordering = self.find_max_ordering(*right, bounds, max_ordering);
|
||||
} else {
|
||||
max_ordering = self.find_max_ordering(*right, bounds, max_ordering);
|
||||
max_ordering = self.find_max_ordering(*left, bounds, max_ordering);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
max_ordering
|
||||
}
|
||||
|
||||
fn push_leaf(&mut self, bounds: Bounds<U>, order: u32) -> usize {
|
||||
self.nodes.push(Node::Leaf { bounds, order });
|
||||
self.nodes.len() - 1
|
||||
}
|
||||
|
||||
fn push_internal(&mut self, left: usize, right: usize) -> usize {
|
||||
let left_node = self.node(left);
|
||||
let right_node = self.node(right);
|
||||
let new_bounds = left_node.bounds().union(right_node.bounds());
|
||||
let max_ordering = cmp::max(left_node.max_ordering(), right_node.max_ordering());
|
||||
self.nodes.push(Node::Internal {
|
||||
bounds: new_bounds,
|
||||
left,
|
||||
right,
|
||||
max_ordering,
|
||||
});
|
||||
self.nodes.len() - 1
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn node(&self, index: usize) -> &Node<U> {
|
||||
&self.nodes[index]
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn node_mut(&mut self, index: usize) -> &mut Node<U> {
|
||||
&mut self.nodes[index]
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq)]
|
||||
pub struct Primitive<U: Clone + Default + Debug> {
|
||||
bounds: Bounds<U>,
|
||||
order: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum Node<U: Clone + Default + Debug> {
|
||||
Leaf {
|
||||
bounds: Bounds<U>,
|
||||
order: u32,
|
||||
},
|
||||
Internal {
|
||||
left: usize,
|
||||
right: usize,
|
||||
bounds: Bounds<U>,
|
||||
max_ordering: u32,
|
||||
},
|
||||
}
|
||||
|
||||
impl<U> Node<U>
|
||||
where
|
||||
U: Clone + Default + Debug,
|
||||
{
|
||||
fn bounds(&self) -> &Bounds<U> {
|
||||
match self {
|
||||
Node::Leaf { bounds, .. } => bounds,
|
||||
Node::Internal { bounds, .. } => bounds,
|
||||
}
|
||||
}
|
||||
|
||||
fn max_ordering(&self) -> u32 {
|
||||
match self {
|
||||
Node::Leaf {
|
||||
order: ordering, ..
|
||||
} => *ordering,
|
||||
Node::Internal { max_ordering, .. } => *max_ordering,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{Point, Size};
|
||||
use rand::{Rng, SeedableRng};
|
||||
use std::{fs, path::Path};
|
||||
|
||||
#[test]
|
||||
fn test_bounds_insertion_with_two_bounds() {
|
||||
let mut tree = BoundsTree::new();
|
||||
let bounds1 = Bounds {
|
||||
origin: Point { x: 0.0, y: 0.0 },
|
||||
size: Size {
|
||||
width: 10.0,
|
||||
height: 10.0,
|
||||
},
|
||||
};
|
||||
let bounds2 = Bounds {
|
||||
origin: Point { x: 5.0, y: 5.0 },
|
||||
size: Size {
|
||||
width: 10.0,
|
||||
height: 10.0,
|
||||
},
|
||||
};
|
||||
|
||||
// Insert the first Bounds.
|
||||
assert_eq!(tree.insert(bounds1), 1);
|
||||
|
||||
// Insert the second Bounds, which overlaps with the first.
|
||||
assert_eq!(tree.insert(bounds2), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_adjacent_bounds() {
|
||||
let mut tree = BoundsTree::new();
|
||||
let bounds1 = Bounds {
|
||||
origin: Point { x: 0.0, y: 0.0 },
|
||||
size: Size {
|
||||
width: 10.0,
|
||||
height: 10.0,
|
||||
},
|
||||
};
|
||||
let bounds2 = Bounds {
|
||||
origin: Point { x: 10.0, y: 0.0 },
|
||||
size: Size {
|
||||
width: 10.0,
|
||||
height: 10.0,
|
||||
},
|
||||
};
|
||||
|
||||
// Insert the first bounds.
|
||||
assert_eq!(tree.insert(bounds1), 1);
|
||||
|
||||
// Insert the second bounds, which is adjacent to the first but not overlapping.
|
||||
assert_eq!(tree.insert(bounds2), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_random_iterations() {
|
||||
let max_bounds = 100;
|
||||
|
||||
let mut actual_intersections: Vec<usize> = Vec::new();
|
||||
for seed in 1..=1000 {
|
||||
// let seed = 44;
|
||||
let debug = false;
|
||||
if debug {
|
||||
let svg_path = Path::new("./svg");
|
||||
if svg_path.exists() {
|
||||
fs::remove_dir_all("./svg").unwrap();
|
||||
}
|
||||
fs::create_dir_all("./svg").unwrap();
|
||||
}
|
||||
|
||||
dbg!(seed);
|
||||
|
||||
let mut tree = BoundsTree::new();
|
||||
let mut rng = rand::rngs::StdRng::seed_from_u64(seed as u64);
|
||||
let mut expected_quads: Vec<Primitive<f32>> = Vec::new();
|
||||
|
||||
let mut insert_time = std::time::Duration::ZERO;
|
||||
|
||||
// Insert a random number of random Bounds into the tree.
|
||||
let num_bounds = rng.gen_range(1..=max_bounds);
|
||||
for quad_id in 0..num_bounds {
|
||||
let min_x: f32 = rng.gen_range(-100.0..100.0);
|
||||
let min_y: f32 = rng.gen_range(-100.0..100.0);
|
||||
let max_x: f32 = rng.gen_range(min_x..min_x + 50.0);
|
||||
let max_y: f32 = rng.gen_range(min_y..min_y + 50.0);
|
||||
let bounds = Bounds {
|
||||
origin: Point { x: min_x, y: min_y },
|
||||
size: Size {
|
||||
width: max_x - min_x,
|
||||
height: max_y - min_y,
|
||||
},
|
||||
};
|
||||
|
||||
let expected_ordering = expected_quads
|
||||
.iter()
|
||||
.filter_map(|quad| {
|
||||
(quad.bounds.origin.x < bounds.origin.x + bounds.size.width
|
||||
&& quad.bounds.origin.x + quad.bounds.size.width > bounds.origin.x
|
||||
&& quad.bounds.origin.y < bounds.origin.y + bounds.size.height
|
||||
&& quad.bounds.origin.y + quad.bounds.size.height > bounds.origin.y)
|
||||
.then_some(quad.order)
|
||||
})
|
||||
.max()
|
||||
.unwrap_or(0)
|
||||
+ 1;
|
||||
expected_quads.push(Primitive {
|
||||
bounds,
|
||||
order: expected_ordering,
|
||||
});
|
||||
if debug {
|
||||
println!("inserting {} with Bounds: {:?}", quad_id, bounds);
|
||||
draw_bounds(
|
||||
format!("./svg/expected_bounds_after_{}.svg", quad_id),
|
||||
&expected_quads,
|
||||
);
|
||||
}
|
||||
|
||||
// Insert the Bounds into the tree and collect intersections.
|
||||
actual_intersections.clear();
|
||||
let t0 = std::time::Instant::now();
|
||||
let actual_ordering = tree.insert(bounds);
|
||||
insert_time += t0.elapsed();
|
||||
assert_eq!(actual_ordering, expected_ordering);
|
||||
|
||||
if debug {
|
||||
tree.draw(format!("./svg/bounds_tree_after_{}.svg", quad_id));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_bounds(svg_path: impl AsRef<Path>, bounds: &[Primitive<f32>]) {
|
||||
let mut svg_content = String::from(
|
||||
r#"<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="-100 -100 200 200" style="border:1px solid black;">"#,
|
||||
);
|
||||
|
||||
for quad in bounds {
|
||||
svg_content.push_str(&format!(
|
||||
r#"<rect x="{}" y="{}" width="{}" height="{}" style="fill:none;stroke:black;stroke-width:1" />"#,
|
||||
quad.bounds.origin.x,
|
||||
quad.bounds.origin.y,
|
||||
quad.bounds.size.width,
|
||||
quad.bounds.size.height
|
||||
));
|
||||
svg_content.push_str(&format!(
|
||||
r#"<text x="{}" y="{}" font-size="3" text-anchor="middle" alignment-baseline="central"></text>"#,
|
||||
quad.bounds.origin.x + quad.bounds.size.width / 2.0,
|
||||
quad.bounds.origin.y + quad.bounds.size.height / 2.0,
|
||||
));
|
||||
}
|
||||
|
||||
svg_content.push_str("</svg>");
|
||||
fs::write(svg_path, &svg_content).unwrap();
|
||||
}
|
||||
|
||||
impl BoundsTree<f32> {
|
||||
fn draw(&self, svg_path: impl AsRef<std::path::Path>) {
|
||||
let root_bounds = self.node(self.root.unwrap()).bounds();
|
||||
|
||||
let mut svg_content = format!(
|
||||
r#"<svg xmlns="http://www.w3.org/2000/svg" version="1.1" style="border:1px solid black;" viewBox="{} {} {} {}">"#,
|
||||
root_bounds.origin.x,
|
||||
root_bounds.origin.y,
|
||||
root_bounds.size.width,
|
||||
root_bounds.size.height
|
||||
);
|
||||
|
||||
fn draw_node(svg_content: &mut String, nodes: &[Node<f32>], index: usize) {
|
||||
match &nodes[index] {
|
||||
Node::Internal {
|
||||
bounds,
|
||||
left,
|
||||
right,
|
||||
..
|
||||
} => {
|
||||
svg_content.push_str(&format!(
|
||||
r#"<rect x="{}" y="{}" width="{}" height="{}" style="fill:rgba({},{},{},0.1);stroke:rgba({},{},{},1);stroke-width:1" />"#,
|
||||
bounds.origin.x,
|
||||
bounds.origin.y,
|
||||
bounds.size.width,
|
||||
bounds.size.height,
|
||||
(index * 50) % 255, // Red component
|
||||
(index * 120) % 255, // Green component
|
||||
(index * 180) % 255, // Blue component
|
||||
(index * 50) % 255, // Red stroke
|
||||
(index * 120) % 255, // Green stroke
|
||||
(index * 180) % 255 // Blue stroke
|
||||
));
|
||||
draw_node(svg_content, nodes, *left);
|
||||
draw_node(svg_content, nodes, *right);
|
||||
}
|
||||
Node::Leaf { bounds, .. } => {
|
||||
svg_content.push_str(&format!(
|
||||
r#"<rect x="{}" y="{}" width="{}" height="{}" style="fill:none;stroke:black;stroke-width:1" />"#,
|
||||
bounds.origin.x,
|
||||
bounds.origin.y,
|
||||
bounds.size.width,
|
||||
bounds.size.height
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(root) = self.root {
|
||||
draw_node(&mut svg_content, &self.nodes, root);
|
||||
}
|
||||
|
||||
svg_content.push_str("</svg>");
|
||||
std::fs::write(svg_path, &svg_content).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1127,7 +1127,7 @@ impl Element for Div {
|
||||
cx,
|
||||
|_style, scroll_offset, cx| {
|
||||
cx.with_element_offset(scroll_offset, |cx| {
|
||||
for child in &mut self.children {
|
||||
for child in self.children.iter_mut().rev() {
|
||||
child.paint(cx);
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,457 +0,0 @@
|
||||
use crate::{
|
||||
Action, ActionRegistry, AnyTooltip, Bounds, ContentMask, CursorStyle, DispatchPhase,
|
||||
ElementContext, EntityId, FocusId, GlobalElementId, KeyBinding, KeyContext, KeyEvent, Keymap,
|
||||
KeymatchResult, Keystroke, KeystrokeMatcher, MouseEvent, Pixels, PlatformInputHandler,
|
||||
Primitive, Scene, SceneIndex, SmallVec, WindowContext,
|
||||
};
|
||||
use collections::FxHashMap;
|
||||
use std::{
|
||||
any::{Any, TypeId},
|
||||
cell::RefCell,
|
||||
iter,
|
||||
ops::Range,
|
||||
rc::Rc,
|
||||
};
|
||||
|
||||
// pub(crate) struct Frame {
|
||||
// pub(crate) window_active: bool,
|
||||
|
||||
// #[cfg(any(test, feature = "test-support"))]
|
||||
// pub(crate) debug_bounds: FxHashMap<String, Bounds<Pixels>>,
|
||||
// }
|
||||
|
||||
pub struct Frame {
|
||||
elements: Vec<PaintedElement>,
|
||||
pub(crate) scene: Scene,
|
||||
focus: Option<FocusId>,
|
||||
pub(crate) window_active: bool,
|
||||
mouse_listeners: Vec<AnyMouseListener>,
|
||||
key_listeners: Vec<KeyListener>,
|
||||
action_listeners: Vec<ActionListener>,
|
||||
element_states: FxHashMap<GlobalElementId, ElementStateBox>,
|
||||
|
||||
element_stack: Vec<PaintedElementId>,
|
||||
context_stack: Vec<KeyContext>,
|
||||
content_mask_stack: Vec<ContentMask<Pixels>>,
|
||||
focusable_node_ids: FxHashMap<FocusId, PaintedElementId>,
|
||||
view_node_ids: FxHashMap<EntityId, PaintedElementId>,
|
||||
keystroke_matchers: FxHashMap<SmallVec<[KeyContext; 4]>, KeystrokeMatcher>,
|
||||
keymap: Rc<RefCell<Keymap>>,
|
||||
action_registry: Rc<ActionRegistry>,
|
||||
}
|
||||
|
||||
impl Frame {
|
||||
pub fn new(keymap: Rc<RefCell<Keymap>>, action_registry: Rc<ActionRegistry>) -> Self {
|
||||
Frame {
|
||||
keymap,
|
||||
action_registry,
|
||||
elements: Vec::new(),
|
||||
scene: Scene::default(),
|
||||
focus: None,
|
||||
window_active: false,
|
||||
mouse_listeners: Vec::new(),
|
||||
key_listeners: Vec::new(),
|
||||
action_listeners: Vec::new(),
|
||||
element_states: FxHashMap::default(),
|
||||
element_stack: Vec::new(),
|
||||
context_stack: Vec::new(),
|
||||
content_mask_stack: Vec::new(),
|
||||
focusable_node_ids: FxHashMap::default(),
|
||||
view_node_ids: FxHashMap::default(),
|
||||
keystroke_matchers: FxHashMap::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clear(&mut self) {
|
||||
self.elements.clear();
|
||||
self.scene.clear();
|
||||
self.focus = None;
|
||||
self.mouse_listeners.clear();
|
||||
self.key_listeners.clear();
|
||||
self.action_listeners.clear();
|
||||
self.element_states.clear();
|
||||
self.element_stack.clear();
|
||||
self.context_stack.clear();
|
||||
self.content_mask_stack.clear();
|
||||
self.focusable_node_ids.clear();
|
||||
self.view_node_ids.clear();
|
||||
self.keystroke_matchers.clear();
|
||||
}
|
||||
|
||||
pub fn clear_pending_keystrokes(&mut self) {
|
||||
self.keystroke_matchers.clear();
|
||||
}
|
||||
|
||||
/// Preserve keystroke matchers from previous frames to support multi-stroke
|
||||
/// bindings across multiple frames.
|
||||
pub fn preserve_pending_keystrokes(
|
||||
&mut self,
|
||||
prev_frame: &mut Self,
|
||||
focus_id: Option<FocusId>,
|
||||
) {
|
||||
self.context_stack.clear();
|
||||
for element in self.dispatch_path(focus_id) {
|
||||
if let Some(context) = element.key_context.clone() {
|
||||
self.context_stack.push(context);
|
||||
}
|
||||
|
||||
if let Some((context_stack, matcher)) = prev_frame
|
||||
.keystroke_matchers
|
||||
.remove_entry(self.context_stack.as_slice())
|
||||
{
|
||||
self.keystroke_matchers.insert(context_stack, matcher);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_focus(&mut self, focus_id: Option<FocusId>) {
|
||||
self.focus = focus_id;
|
||||
}
|
||||
|
||||
pub fn set_window_active(&mut self, active: bool) {
|
||||
self.window_active = active;
|
||||
}
|
||||
|
||||
pub fn window_active(&self) -> bool {
|
||||
self.window_active
|
||||
}
|
||||
|
||||
pub fn focus_contains(&self, parent: FocusId, child: FocusId) -> bool {
|
||||
if parent == child {
|
||||
return true;
|
||||
}
|
||||
|
||||
if let Some(parent_node_id) = self.focusable_node_ids.get(&parent) {
|
||||
let mut current_node_id = self.focusable_node_ids.get(&child).copied();
|
||||
while let Some(node_id) = current_node_id {
|
||||
if node_id == *parent_node_id {
|
||||
return true;
|
||||
}
|
||||
current_node_id = self.elements[node_id.0].parent;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
pub fn focus_path(&self) -> SmallVec<[FocusId; 8]> {
|
||||
let Some(focus_id) = self.focus else {
|
||||
return SmallVec::new();
|
||||
};
|
||||
|
||||
let mut focus_path = self
|
||||
.dispatch_path(Some(focus_id))
|
||||
.flat_map(|element| element.focus_id)
|
||||
.collect::<SmallVec<[FocusId; 8]>>();
|
||||
focus_path.reverse(); // Reverse the path so it goes from the root to the focused node.
|
||||
focus_path
|
||||
}
|
||||
|
||||
pub fn view_path(&self, view_id: EntityId) -> SmallVec<[EntityId; 8]> {
|
||||
let Some(element_id) = self.view_node_ids.get(&view_id) else {
|
||||
return SmallVec::new();
|
||||
};
|
||||
|
||||
let mut view_path = self
|
||||
.ancestors(Some(*element_id))
|
||||
.flat_map(|element| element.view_id)
|
||||
.collect::<SmallVec<[EntityId; 8]>>();
|
||||
view_path.reverse(); // Reverse the path so it goes from the root to the focused node.
|
||||
view_path
|
||||
}
|
||||
|
||||
pub fn action_dispatch_path(&self, focus_id: Option<FocusId>) -> SmallVec<[ActionListener; 8]> {
|
||||
let mut action_dispatch_path = self
|
||||
.dispatch_path(focus_id)
|
||||
.flat_map(|element| {
|
||||
self.action_listeners[element.action_listeners.clone()]
|
||||
.iter()
|
||||
.cloned()
|
||||
})
|
||||
.collect::<SmallVec<[ActionListener; 8]>>();
|
||||
action_dispatch_path.reverse(); // Reverse the path so it goes from the root to the focused node.
|
||||
action_dispatch_path
|
||||
}
|
||||
|
||||
pub fn key_dispatch_path(&self, focus_id: Option<FocusId>) -> SmallVec<[KeyListener; 8]> {
|
||||
let mut key_dispatch_path: SmallVec<[KeyListener; 8]> = self
|
||||
.dispatch_path(focus_id)
|
||||
.flat_map(|element| {
|
||||
self.key_listeners[element.key_listeners.clone()]
|
||||
.iter()
|
||||
.cloned()
|
||||
})
|
||||
.collect::<SmallVec<[KeyListener; 8]>>();
|
||||
key_dispatch_path.reverse(); // Reverse the path so it goes from the root to the focused node.
|
||||
key_dispatch_path
|
||||
}
|
||||
|
||||
pub fn available_actions(&self, focus_id: Option<FocusId>) -> Vec<Box<dyn Action>> {
|
||||
let mut actions = Vec::<Box<dyn Action>>::new();
|
||||
for ActionListener { action_type, .. } in self.action_dispatch_path(focus_id) {
|
||||
if let Err(ix) = actions.binary_search_by_key(&action_type, |a| a.as_any().type_id()) {
|
||||
// Intentionally silence these errors without logging.
|
||||
// If an action cannot be built by default, it's not available.
|
||||
let action = self.action_registry.build_action_type(&action_type).ok();
|
||||
if let Some(action) = action {
|
||||
actions.insert(ix, action);
|
||||
}
|
||||
}
|
||||
}
|
||||
actions
|
||||
}
|
||||
|
||||
pub fn bindings_for_action(
|
||||
&self,
|
||||
action: &dyn Action,
|
||||
focus_id: Option<FocusId>,
|
||||
) -> Vec<KeyBinding> {
|
||||
let context_stack = self
|
||||
.dispatch_path(focus_id)
|
||||
.flat_map(|element| element.key_context.clone())
|
||||
.collect::<SmallVec<[KeyContext; 8]>>();
|
||||
|
||||
let keymap = self.keymap.borrow();
|
||||
keymap
|
||||
.bindings_for_action(action)
|
||||
.filter(|binding| {
|
||||
for i in 0..context_stack.len() {
|
||||
let context = &context_stack[0..=i];
|
||||
if keymap.binding_enabled(binding, context) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
})
|
||||
.cloned()
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn is_action_available(&self, action: &dyn Action, focus_id: Option<FocusId>) -> bool {
|
||||
for element in self.dispatch_path(focus_id) {
|
||||
if self.action_listeners[element.action_listeners.clone()]
|
||||
.iter()
|
||||
.any(|listener| listener.action_type == action.as_any().type_id())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
pub fn match_keystroke(
|
||||
&mut self,
|
||||
keystroke: &Keystroke,
|
||||
focus_id: Option<FocusId>,
|
||||
) -> KeymatchResult {
|
||||
let mut bindings = SmallVec::<[KeyBinding; 1]>::new();
|
||||
let mut pending = false;
|
||||
|
||||
let mut context_stack: SmallVec<[KeyContext; 4]> = SmallVec::new();
|
||||
|
||||
for element in self.dispatch_path(focus_id) {
|
||||
if let Some(context) = element.key_context.clone() {
|
||||
context_stack.push(context);
|
||||
}
|
||||
}
|
||||
|
||||
while !context_stack.is_empty() {
|
||||
let keystroke_matcher = self
|
||||
.keystroke_matchers
|
||||
.entry(context_stack.clone())
|
||||
.or_insert_with(|| KeystrokeMatcher::new(self.keymap.clone()));
|
||||
|
||||
let result = keystroke_matcher.match_keystroke(keystroke, &context_stack);
|
||||
if result.pending && !pending && !bindings.is_empty() {
|
||||
context_stack.pop();
|
||||
continue;
|
||||
}
|
||||
|
||||
pending = result.pending || pending;
|
||||
for new_binding in result.bindings {
|
||||
match bindings
|
||||
.iter()
|
||||
.position(|el| el.keystrokes.len() < new_binding.keystrokes.len())
|
||||
{
|
||||
Some(idx) => {
|
||||
bindings.insert(idx, new_binding);
|
||||
}
|
||||
None => bindings.push(new_binding),
|
||||
}
|
||||
}
|
||||
context_stack.pop();
|
||||
}
|
||||
|
||||
KeymatchResult { bindings, pending }
|
||||
}
|
||||
|
||||
pub fn has_pending_keystrokes(&self) -> bool {
|
||||
self.keystroke_matchers
|
||||
.iter()
|
||||
.any(|(_, matcher)| matcher.has_pending_keystrokes())
|
||||
}
|
||||
|
||||
fn dispatch_path(&self, focus_id: Option<FocusId>) -> impl Iterator<Item = &PaintedElement> {
|
||||
let mut current_node_id = focus_id
|
||||
.and_then(|focus_id| self.focusable_node_ids.get(&focus_id).copied())
|
||||
.or_else(|| self.elements.is_empty().then(|| PaintedElementId(0)));
|
||||
self.ancestors(current_node_id)
|
||||
}
|
||||
|
||||
fn ancestors(
|
||||
&self,
|
||||
mut current_node_id: Option<PaintedElementId>,
|
||||
) -> impl Iterator<Item = &PaintedElement> {
|
||||
iter::from_fn(move || {
|
||||
let node_id = current_node_id?;
|
||||
current_node_id = self.elements[node_id.0].parent;
|
||||
Some(&self.elements[node_id.0])
|
||||
})
|
||||
}
|
||||
|
||||
pub fn push_element(&mut self) {
|
||||
let parent = self.element_stack.last().copied();
|
||||
let element_id = PaintedElementId(self.elements.len());
|
||||
let scene_index = self.scene.current_index();
|
||||
self.elements.push(PaintedElement {
|
||||
parent,
|
||||
scene_primitives: scene_index.clone()..scene_index,
|
||||
mouse_listeners: self.mouse_listeners.len()..self.mouse_listeners.len(),
|
||||
key_listeners: self.key_listeners.len()..self.key_listeners.len(),
|
||||
action_listeners: self.action_listeners.len()..self.action_listeners.len(),
|
||||
..Default::default()
|
||||
});
|
||||
self.element_stack.push(element_id);
|
||||
}
|
||||
|
||||
pub fn pop_element(&mut self) {
|
||||
let element = &self.elements[self.active_element_id().0];
|
||||
if element.key_context.is_some() {
|
||||
self.context_stack.pop();
|
||||
}
|
||||
self.element_stack.pop();
|
||||
}
|
||||
|
||||
pub fn set_key_context(&mut self, context: KeyContext) {
|
||||
let element_id = self.active_element_id();
|
||||
self.elements[element_id.0].key_context = Some(context.clone());
|
||||
self.context_stack.push(context);
|
||||
}
|
||||
|
||||
pub fn set_focus_id(&mut self, focus_id: FocusId) {
|
||||
let element_id = self.active_element_id();
|
||||
self.elements[element_id.0].focus_id = Some(focus_id);
|
||||
self.focusable_node_ids.insert(focus_id, element_id);
|
||||
}
|
||||
|
||||
pub fn set_view_id(&mut self, view_id: EntityId) {
|
||||
let element_id = self.active_element_id();
|
||||
self.elements[element_id.0].view_id = Some(view_id);
|
||||
self.view_node_ids.insert(view_id, element_id);
|
||||
}
|
||||
|
||||
pub fn paint_primitive<P: Into<Primitive>>(&mut self, build_primitive: impl FnOnce(u32) -> P) {
|
||||
self.scene.paint_primitive(build_primitive);
|
||||
let element_id = self.active_element_id();
|
||||
self.elements[element_id.0].scene_primitives.end = self.scene.current_index();
|
||||
}
|
||||
|
||||
pub fn on_mouse_event<E: MouseEvent>(
|
||||
&mut self,
|
||||
mut listener: impl 'static + FnMut(&E, DispatchPhase, &mut WindowContext),
|
||||
) {
|
||||
self.mouse_listeners.push(Rc::new(move |event, phase, cx| {
|
||||
if let Some(event) = event.downcast_ref::<E>() {
|
||||
listener(event, phase, cx);
|
||||
}
|
||||
}));
|
||||
self.active_element().mouse_listeners.end += 1;
|
||||
}
|
||||
|
||||
pub fn on_key_event<E: KeyEvent>(
|
||||
&mut self,
|
||||
listener: impl Fn(&E, DispatchPhase, &mut WindowContext) + 'static,
|
||||
) {
|
||||
self.key_listeners.push(Rc::new(|event, phase, cx| {
|
||||
if let Some(event) = event.downcast_ref::<E>() {
|
||||
listener(event, phase, cx);
|
||||
}
|
||||
}));
|
||||
self.active_element().key_listeners.end += 1;
|
||||
}
|
||||
|
||||
pub fn on_action(
|
||||
&mut self,
|
||||
action_type: TypeId,
|
||||
listener: Rc<dyn Fn(&dyn Any, DispatchPhase, &mut WindowContext) + 'static>,
|
||||
) {
|
||||
self.action_listeners.push(ActionListener {
|
||||
action_type,
|
||||
listener: Rc::new(|event, phase, cx| listener(event, phase, cx)),
|
||||
});
|
||||
|
||||
self.active_element().action_listeners.end += 1;
|
||||
}
|
||||
|
||||
pub fn set_input_handler(&mut self, handler: Option<PlatformInputHandler>) {
|
||||
self.active_element().input_handler = handler;
|
||||
}
|
||||
|
||||
pub fn set_tooltip(&mut self, tooltip: Option<AnyTooltip>) {
|
||||
self.active_element().tooltip = tooltip;
|
||||
}
|
||||
|
||||
pub fn set_cursor_style(&mut self, cursor_style: Option<CursorStyle>) {
|
||||
self.active_element().cursor_style = cursor_style;
|
||||
}
|
||||
|
||||
fn active_element_id(&self) -> PaintedElementId {
|
||||
self.element_stack
|
||||
.last()
|
||||
.copied()
|
||||
.expect("There should be an active element")
|
||||
}
|
||||
|
||||
fn active_element(&mut self) -> &mut PaintedElement {
|
||||
let element_id = self.active_element_id();
|
||||
&mut self.elements[element_id.0]
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct PaintedElement {
|
||||
id: Option<GlobalElementId>,
|
||||
bounds: Bounds<Pixels>,
|
||||
content_mask: ContentMask<Pixels>,
|
||||
opaque: bool,
|
||||
scene_primitives: Range<SceneIndex>,
|
||||
mouse_listeners: Range<usize>,
|
||||
key_listeners: Range<usize>,
|
||||
action_listeners: Range<usize>,
|
||||
input_handler: Option<PlatformInputHandler>,
|
||||
tooltip: Option<AnyTooltip>,
|
||||
cursor_style: Option<CursorStyle>,
|
||||
key_context: Option<KeyContext>,
|
||||
focus_id: Option<FocusId>,
|
||||
view_id: Option<EntityId>,
|
||||
parent: Option<PaintedElementId>,
|
||||
}
|
||||
|
||||
pub(crate) struct ElementStateBox {
|
||||
pub(crate) inner: Box<dyn Any>,
|
||||
pub(crate) parent_view_id: EntityId,
|
||||
#[cfg(debug_assertions)]
|
||||
pub(crate) type_name: &'static str,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Eq, PartialEq)]
|
||||
struct PaintedElementId(usize);
|
||||
|
||||
type AnyMouseListener = Rc<dyn Fn(&dyn Any, DispatchPhase, &mut ElementContext) + 'static>;
|
||||
|
||||
type KeyListener = Rc<dyn Fn(&dyn Any, DispatchPhase, &mut ElementContext)>;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct ActionListener {
|
||||
pub(crate) action_type: TypeId,
|
||||
pub(crate) listener: Rc<dyn Fn(&dyn Any, DispatchPhase, &mut WindowContext)>,
|
||||
}
|
||||
@@ -828,6 +828,28 @@ where
|
||||
y: self.origin.y.clone() + self.size.height.clone().half(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculates the half perimeter of a rectangle defined by the bounds.
|
||||
///
|
||||
/// The half perimeter is calculated as the sum of the width and the height of the rectangle.
|
||||
/// This method is generic over the type `T` which must implement the `Sub` trait to allow
|
||||
/// calculation of the width and height from the bounds' origin and size, as well as the `Add` trait
|
||||
/// to sum the width and height for the half perimeter.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use zed::{Bounds, Point, Size};
|
||||
/// let bounds = Bounds {
|
||||
/// origin: Point { x: 0, y: 0 },
|
||||
/// size: Size { width: 10, height: 20 },
|
||||
/// };
|
||||
/// let half_perimeter = bounds.half_perimeter();
|
||||
/// assert_eq!(half_perimeter, 30);
|
||||
/// ```
|
||||
pub fn half_perimeter(&self) -> T {
|
||||
self.size.width.clone() + self.size.height.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Clone + Default + Debug + PartialOrd + Add<T, Output = T> + Sub<Output = T>> Bounds<T> {
|
||||
|
||||
@@ -70,11 +70,11 @@ mod app;
|
||||
|
||||
mod arena;
|
||||
mod assets;
|
||||
mod bounds_tree;
|
||||
mod color;
|
||||
mod element;
|
||||
mod elements;
|
||||
mod executor;
|
||||
mod frame;
|
||||
mod geometry;
|
||||
mod image_cache;
|
||||
mod input;
|
||||
@@ -118,12 +118,12 @@ pub use anyhow::Result;
|
||||
pub use app::*;
|
||||
pub(crate) use arena::*;
|
||||
pub use assets::*;
|
||||
pub(crate) use bounds_tree::*;
|
||||
pub use color::*;
|
||||
pub use ctor::ctor;
|
||||
pub use element::*;
|
||||
pub use elements::*;
|
||||
pub use executor::*;
|
||||
pub use frame::*;
|
||||
pub use geometry::*;
|
||||
pub use gpui_macros::{register_action, test, IntoElement, Render};
|
||||
use image_cache::*;
|
||||
|
||||
@@ -68,22 +68,26 @@ impl<V: 'static> ElementInputHandler<V> {
|
||||
}
|
||||
|
||||
impl<V: ViewInputHandler> InputHandler for ElementInputHandler<V> {
|
||||
fn selected_text_range(&self, cx: &mut WindowContext) -> Option<Range<usize>> {
|
||||
fn selected_text_range(&mut self, cx: &mut WindowContext) -> Option<Range<usize>> {
|
||||
self.view
|
||||
.update(cx, |view, cx| view.selected_text_range(cx))
|
||||
}
|
||||
|
||||
fn marked_text_range(&self, cx: &mut WindowContext) -> Option<Range<usize>> {
|
||||
fn marked_text_range(&mut self, cx: &mut WindowContext) -> Option<Range<usize>> {
|
||||
self.view.update(cx, |view, cx| view.marked_text_range(cx))
|
||||
}
|
||||
|
||||
fn text_for_range(&self, range_utf16: Range<usize>, cx: &mut WindowContext) -> Option<String> {
|
||||
fn text_for_range(
|
||||
&mut self,
|
||||
range_utf16: Range<usize>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Option<String> {
|
||||
self.view
|
||||
.update(cx, |view, cx| view.text_for_range(range_utf16, cx))
|
||||
}
|
||||
|
||||
fn replace_text_in_range(
|
||||
&self,
|
||||
&mut self,
|
||||
replacement_range: Option<Range<usize>>,
|
||||
text: &str,
|
||||
cx: &mut WindowContext,
|
||||
@@ -94,7 +98,7 @@ impl<V: ViewInputHandler> InputHandler for ElementInputHandler<V> {
|
||||
}
|
||||
|
||||
fn replace_and_mark_text_in_range(
|
||||
&self,
|
||||
&mut self,
|
||||
range_utf16: Option<Range<usize>>,
|
||||
new_text: &str,
|
||||
new_selected_range: Option<Range<usize>>,
|
||||
@@ -105,12 +109,12 @@ impl<V: ViewInputHandler> InputHandler for ElementInputHandler<V> {
|
||||
});
|
||||
}
|
||||
|
||||
fn unmark_text(&self, cx: &mut WindowContext) {
|
||||
fn unmark_text(&mut self, cx: &mut WindowContext) {
|
||||
self.view.update(cx, |view, cx| view.unmark_text(cx));
|
||||
}
|
||||
|
||||
fn bounds_for_range(
|
||||
&self,
|
||||
&mut self,
|
||||
range_utf16: Range<usize>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Option<Bounds<Pixels>> {
|
||||
|
||||
@@ -108,10 +108,6 @@ impl DispatchTree {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.nodes.len()
|
||||
}
|
||||
|
||||
pub fn clear(&mut self) {
|
||||
self.node_stack.clear();
|
||||
self.context_stack.clear();
|
||||
|
||||
@@ -430,26 +430,30 @@ pub trait InputHandler: 'static {
|
||||
/// Corresponds to [selectedRange()](https://developer.apple.com/documentation/appkit/nstextinputclient/1438242-selectedrange)
|
||||
///
|
||||
/// Return value is in terms of UTF-16 characters, from 0 to the length of the document
|
||||
fn selected_text_range(&self, cx: &mut WindowContext) -> Option<Range<usize>>;
|
||||
fn selected_text_range(&mut self, cx: &mut WindowContext) -> Option<Range<usize>>;
|
||||
|
||||
/// Get the range of the currently marked text, if any
|
||||
/// Corresponds to [markedRange()](https://developer.apple.com/documentation/appkit/nstextinputclient/1438250-markedrange)
|
||||
///
|
||||
/// Return value is in terms of UTF-16 characters, from 0 to the length of the document
|
||||
fn marked_text_range(&self, cx: &mut WindowContext) -> Option<Range<usize>>;
|
||||
fn marked_text_range(&mut self, cx: &mut WindowContext) -> Option<Range<usize>>;
|
||||
|
||||
/// Get the text for the given document range in UTF-16 characters
|
||||
/// Corresponds to [attributedSubstring(forProposedRange: actualRange:)](https://developer.apple.com/documentation/appkit/nstextinputclient/1438238-attributedsubstring)
|
||||
///
|
||||
/// range_utf16 is in terms of UTF-16 characters
|
||||
fn text_for_range(&self, range_utf16: Range<usize>, cx: &mut WindowContext) -> Option<String>;
|
||||
fn text_for_range(
|
||||
&mut self,
|
||||
range_utf16: Range<usize>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Option<String>;
|
||||
|
||||
/// Replace the text in the given document range with the given text
|
||||
/// Corresponds to [insertText(_:replacementRange:)](https://developer.apple.com/documentation/appkit/nstextinputclient/1438258-inserttext)
|
||||
///
|
||||
/// replacement_range is in terms of UTF-16 characters
|
||||
fn replace_text_in_range(
|
||||
&self,
|
||||
&mut self,
|
||||
replacement_range: Option<Range<usize>>,
|
||||
text: &str,
|
||||
cx: &mut WindowContext,
|
||||
@@ -462,7 +466,7 @@ pub trait InputHandler: 'static {
|
||||
/// range_utf16 is in terms of UTF-16 characters
|
||||
/// new_selected_range is in terms of UTF-16 characters
|
||||
fn replace_and_mark_text_in_range(
|
||||
&self,
|
||||
&mut self,
|
||||
range_utf16: Option<Range<usize>>,
|
||||
new_text: &str,
|
||||
new_selected_range: Option<Range<usize>>,
|
||||
@@ -471,14 +475,14 @@ pub trait InputHandler: 'static {
|
||||
|
||||
/// Remove the IME 'composing' state from the document
|
||||
/// Corresponds to [unmarkText()](https://developer.apple.com/documentation/appkit/nstextinputclient/1438239-unmarktext)
|
||||
fn unmark_text(&self, cx: &mut WindowContext);
|
||||
fn unmark_text(&mut self, cx: &mut WindowContext);
|
||||
|
||||
/// Get the bounds of the given document range in screen coordinates
|
||||
/// Corresponds to [firstRect(forCharacterRange:actualRange:)](https://developer.apple.com/documentation/appkit/nstextinputclient/1438240-firstrect)
|
||||
///
|
||||
/// This is used for positioning the IME candidate window
|
||||
fn bounds_for_range(
|
||||
&self,
|
||||
&mut self,
|
||||
range_utf16: Range<usize>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Option<Bounds<Pixels>>;
|
||||
|
||||
@@ -117,6 +117,7 @@ impl PlatformAtlas for BladeAtlas {
|
||||
if let Some(tile) = lock.tiles_by_key.get(key) {
|
||||
Ok(tile.clone())
|
||||
} else {
|
||||
profiling::scope!("new tile");
|
||||
let (size, bytes) = build()?;
|
||||
let tile = lock.allocate(size, key.texture_kind());
|
||||
lock.upload_texture(tile.texture_id, tile.bounds, &bytes);
|
||||
|
||||
@@ -39,6 +39,7 @@ impl BladeBelt {
|
||||
}
|
||||
}
|
||||
|
||||
#[profiling::function]
|
||||
pub fn alloc(&mut self, size: u64, gpu: &gpu::Context) -> gpu::BufferPiece {
|
||||
for &mut (ref rb, ref mut offset) in self.active.iter_mut() {
|
||||
let aligned = offset.next_multiple_of(self.desc.alignment);
|
||||
|
||||
@@ -444,6 +444,7 @@ impl BladeRenderer {
|
||||
self.gpu.metal_layer().unwrap().as_ptr()
|
||||
}
|
||||
|
||||
#[profiling::function]
|
||||
fn rasterize_paths(&mut self, paths: &[Path<ScaledPixels>]) {
|
||||
self.path_tiles.clear();
|
||||
let mut vertices_by_texture_id = HashMap::default();
|
||||
@@ -506,7 +507,10 @@ impl BladeRenderer {
|
||||
}
|
||||
|
||||
pub fn draw(&mut self, scene: &Scene) {
|
||||
let frame = self.gpu.acquire_frame();
|
||||
let frame = {
|
||||
profiling::scope!("acquire frame");
|
||||
self.gpu.acquire_frame()
|
||||
};
|
||||
self.command_encoder.start();
|
||||
self.command_encoder.init_texture(frame.texture());
|
||||
|
||||
@@ -529,6 +533,7 @@ impl BladeRenderer {
|
||||
}],
|
||||
depth_stencil: None,
|
||||
}) {
|
||||
profiling::scope!("render pass");
|
||||
for batch in scene.batches() {
|
||||
match batch {
|
||||
PrimitiveBatch::Quads(quads) => {
|
||||
@@ -718,6 +723,7 @@ impl BladeRenderer {
|
||||
self.command_encoder.present(frame);
|
||||
let sync_point = self.gpu.submit(&mut self.command_encoder);
|
||||
|
||||
profiling::scope!("finish");
|
||||
self.instance_belt.flush(&sync_point);
|
||||
self.atlas.after_frame(&sync_point);
|
||||
self.atlas.clear_textures(AtlasTextureKind::Path);
|
||||
|
||||
@@ -33,6 +33,7 @@ impl LinuxDispatcher {
|
||||
) -> Self {
|
||||
let (background_sender, background_receiver) = flume::unbounded::<Runnable>();
|
||||
let background_thread = thread::spawn(move || {
|
||||
profiling::register_thread!("background");
|
||||
for runnable in background_receiver {
|
||||
let _ignore_panic = panic::catch_unwind(|| runnable.run());
|
||||
}
|
||||
|
||||
@@ -294,7 +294,13 @@ impl Platform for LinuxPlatform {
|
||||
}
|
||||
|
||||
fn reveal_path(&self, path: &Path) {
|
||||
open::that(path);
|
||||
if path.is_dir() {
|
||||
open::that(path);
|
||||
return;
|
||||
}
|
||||
// If `path` is a file, the system may try to open it in a text editor
|
||||
let dir = path.parent().unwrap_or(Path::new(""));
|
||||
open::that(dir);
|
||||
}
|
||||
|
||||
fn on_become_active(&self, callback: Box<dyn FnMut()>) {
|
||||
|
||||
@@ -17,7 +17,9 @@ impl Keystroke {
|
||||
|
||||
// Ignore control characters (and DEL) for the purposes of ime_key,
|
||||
// but if key_utf32 is 0 then assume it isn't one
|
||||
let ime_key = (key_utf32 == 0 || (key_utf32 >= 32 && key_utf32 != 127)).then_some(key_utf8);
|
||||
let ime_key = ((key_utf32 == 0 || (key_utf32 >= 32 && key_utf32 != 127))
|
||||
&& !key_utf8.is_empty())
|
||||
.then_some(key_utf8);
|
||||
|
||||
Keystroke {
|
||||
modifiers,
|
||||
|
||||
@@ -421,12 +421,29 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClientState {
|
||||
state.keymap_state = Some(xkb::State::new(&keymap));
|
||||
}
|
||||
wl_keyboard::Event::Enter { surface, .. } => {
|
||||
for window in &state.windows {
|
||||
if window.1.surface.id() == surface.id() {
|
||||
state.keyboard_focused_window = Some(Rc::clone(&window.1));
|
||||
}
|
||||
state.keyboard_focused_window = state
|
||||
.windows
|
||||
.iter()
|
||||
.find(|&w| w.1.surface.id() == surface.id())
|
||||
.map(|w| w.1.clone());
|
||||
|
||||
if let Some(window) = &state.keyboard_focused_window {
|
||||
window.set_focused(true);
|
||||
}
|
||||
}
|
||||
wl_keyboard::Event::Leave { surface, .. } => {
|
||||
let keyboard_focused_window = state
|
||||
.windows
|
||||
.iter()
|
||||
.find(|&w| w.1.surface.id() == surface.id())
|
||||
.map(|w| w.1.clone());
|
||||
|
||||
if let Some(window) = keyboard_focused_window {
|
||||
window.set_focused(false);
|
||||
}
|
||||
|
||||
state.keyboard_focused_window = None;
|
||||
}
|
||||
wl_keyboard::Event::Modifiers {
|
||||
mods_depressed,
|
||||
mods_latched,
|
||||
@@ -479,7 +496,6 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClientState {
|
||||
}
|
||||
}
|
||||
}
|
||||
wl_keyboard::Event::Leave { .. } => {}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -228,6 +228,12 @@ impl WaylandWindowState {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_focused(&self, focus: bool) {
|
||||
if let Some(ref mut fun) = self.callbacks.lock().active_status_change {
|
||||
fun(focus);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -349,7 +355,7 @@ impl PlatformWindow for WaylandWindow {
|
||||
}
|
||||
|
||||
fn on_active_status_change(&self, callback: Box<dyn FnMut(bool)>) {
|
||||
//todo!(linux)
|
||||
self.0.callbacks.lock().active_status_change = Some(callback);
|
||||
}
|
||||
|
||||
fn on_resize(&self, callback: Box<dyn FnMut(Size<Pixels>, f32)>) {
|
||||
|
||||
@@ -71,7 +71,10 @@ impl Client for X11Client {
|
||||
// into window functions as they may invoke callbacks that need
|
||||
// to immediately access the platform (self).
|
||||
while !self.platform_inner.state.lock().quit_requested {
|
||||
let event = self.xcb_connection.wait_for_event().unwrap();
|
||||
let event = {
|
||||
profiling::scope!("Wait for event");
|
||||
self.xcb_connection.wait_for_event().unwrap()
|
||||
};
|
||||
match event {
|
||||
xcb::Event::X(x::Event::ClientMessage(ev)) => {
|
||||
if let x::ClientMessageData::Data32([atom, ..]) = ev.data() {
|
||||
@@ -210,6 +213,7 @@ impl Client for X11Client {
|
||||
_ => {}
|
||||
}
|
||||
|
||||
profiling::scope!("Runnables");
|
||||
if let Ok(runnable) = self.platform_inner.main_receiver.try_recv() {
|
||||
runnable.run();
|
||||
}
|
||||
@@ -219,6 +223,7 @@ impl Client for X11Client {
|
||||
fun();
|
||||
}
|
||||
}
|
||||
|
||||
fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>> {
|
||||
let setup = self.xcb_connection.get_setup();
|
||||
setup
|
||||
@@ -230,6 +235,7 @@ impl Client for X11Client {
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn display(&self, id: DisplayId) -> Option<Rc<dyn PlatformDisplay>> {
|
||||
Some(Rc::new(X11Display::new(&self.xcb_connection, id.0 as i32)))
|
||||
}
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
use crate::{
|
||||
point, AtlasTextureId, AtlasTile, Bounds, ContentMask, Corners, Edges, EntityId, Hsla, Pixels,
|
||||
Point, ScaledPixels,
|
||||
point, AtlasTextureId, AtlasTile, Bounds, BoundsTree, ContentMask, Corners, Edges, EntityId,
|
||||
Hsla, Pixels, Point, ScaledPixels, StackingOrder,
|
||||
};
|
||||
use collections::{BTreeMap, FxHashSet};
|
||||
use std::{fmt::Debug, iter::Peekable, slice};
|
||||
|
||||
#[allow(non_camel_case_types, unused)]
|
||||
pub(crate) type PathVertex_ScaledPixels = PathVertex<ScaledPixels>;
|
||||
|
||||
pub(crate) type LayerId = u32;
|
||||
pub(crate) type DrawOrder = u32;
|
||||
|
||||
#[derive(Default, Copy, Clone, Debug, Eq, PartialEq, Hash)]
|
||||
#[repr(C)]
|
||||
pub(crate) struct ViewId {
|
||||
@@ -31,18 +35,6 @@ impl From<ViewId> for EntityId {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
/// An index into all the geometry in a `Scene` at a point in time.
|
||||
pub(crate) struct SceneIndex {
|
||||
pub(crate) shadows: usize,
|
||||
pub(crate) quads: usize,
|
||||
pub(crate) paths: usize,
|
||||
pub(crate) underlines: usize,
|
||||
pub(crate) monochrome_sprites: usize,
|
||||
pub(crate) polychrome_sprites: usize,
|
||||
pub(crate) surfaces: usize,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct Scene {
|
||||
pub(crate) shadows: Vec<Shadow>,
|
||||
@@ -52,7 +44,7 @@ pub(crate) struct Scene {
|
||||
pub(crate) monochrome_sprites: Vec<MonochromeSprite>,
|
||||
pub(crate) polychrome_sprites: Vec<PolychromeSprite>,
|
||||
pub(crate) surfaces: Vec<Surface>,
|
||||
pub(crate) primitive_count: u32,
|
||||
bounds_tree: BoundsTree<ScaledPixels>,
|
||||
}
|
||||
|
||||
impl Scene {
|
||||
@@ -64,19 +56,7 @@ impl Scene {
|
||||
self.monochrome_sprites.clear();
|
||||
self.polychrome_sprites.clear();
|
||||
self.surfaces.clear();
|
||||
self.primitive_count = 0;
|
||||
}
|
||||
|
||||
pub fn current_index(&self) -> SceneIndex {
|
||||
SceneIndex {
|
||||
shadows: self.shadows.len(),
|
||||
quads: self.quads.len(),
|
||||
paths: self.paths.len(),
|
||||
underlines: self.underlines.len(),
|
||||
monochrome_sprites: self.monochrome_sprites.len(),
|
||||
polychrome_sprites: self.polychrome_sprites.len(),
|
||||
surfaces: self.surfaces.len(),
|
||||
}
|
||||
self.bounds_tree.clear();
|
||||
}
|
||||
|
||||
pub fn paths(&self) -> &[Path<ScaledPixels>] {
|
||||
@@ -109,72 +89,120 @@ impl Scene {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn paint_primitive<T: Into<Primitive>>(
|
||||
pub(crate) fn insert(
|
||||
&mut self,
|
||||
build_primitive: impl FnOnce(u32) -> T,
|
||||
) {
|
||||
let primitive = build_primitive(self.primitive_count).into();
|
||||
|
||||
order: &StackingOrder,
|
||||
primitive: impl Into<Primitive>,
|
||||
) -> Option<u32> {
|
||||
let primitive = primitive.into();
|
||||
let clipped_bounds = primitive
|
||||
.bounds()
|
||||
.intersect(&primitive.content_mask().bounds);
|
||||
if clipped_bounds.size.width <= ScaledPixels(0.)
|
||||
|| clipped_bounds.size.height <= ScaledPixels(0.)
|
||||
{
|
||||
return;
|
||||
return None;
|
||||
}
|
||||
|
||||
let order = u32::MAX - self.bounds_tree.insert(clipped_bounds);
|
||||
match primitive {
|
||||
Primitive::Shadow(mut shadow) => {
|
||||
shadow.order = order;
|
||||
self.shadows.push(shadow);
|
||||
}
|
||||
Primitive::Quad(mut quad) => {
|
||||
quad.order = order;
|
||||
self.quads.push(quad);
|
||||
}
|
||||
Primitive::Path(mut path) => {
|
||||
path.order = order;
|
||||
path.id = PathId(self.paths.len());
|
||||
self.paths.push(path);
|
||||
}
|
||||
Primitive::Underline(mut underline) => {
|
||||
underline.order = order;
|
||||
self.underlines.push(underline);
|
||||
}
|
||||
Primitive::MonochromeSprite(mut sprite) => {
|
||||
sprite.order = order;
|
||||
self.monochrome_sprites.push(sprite);
|
||||
}
|
||||
Primitive::PolychromeSprite(mut sprite) => {
|
||||
sprite.order = order;
|
||||
self.polychrome_sprites.push(sprite);
|
||||
}
|
||||
Primitive::Surface(mut surface) => {
|
||||
surface.order = order;
|
||||
self.surfaces.push(surface);
|
||||
}
|
||||
}
|
||||
|
||||
self.primitive_count += 1;
|
||||
Some(order)
|
||||
}
|
||||
|
||||
pub fn reuse_subscene(&mut self, prev_scene: &mut Self, start: SceneIndex, end: SceneIndex) {
|
||||
self.shadows
|
||||
.extend(prev_scene.shadows.drain(start.shadows..end.shadows));
|
||||
self.quads
|
||||
.extend(prev_scene.quads.drain(start.quads..end.quads));
|
||||
self.paths
|
||||
.extend(prev_scene.paths.drain(start.paths..end.paths));
|
||||
self.underlines.extend(
|
||||
prev_scene
|
||||
.underlines
|
||||
.drain(start.underlines..end.underlines),
|
||||
);
|
||||
self.monochrome_sprites.extend(
|
||||
prev_scene
|
||||
.monochrome_sprites
|
||||
.drain(start.monochrome_sprites..end.monochrome_sprites),
|
||||
);
|
||||
self.polychrome_sprites.extend(
|
||||
prev_scene
|
||||
.polychrome_sprites
|
||||
.drain(start.polychrome_sprites..end.polychrome_sprites),
|
||||
);
|
||||
self.surfaces
|
||||
.extend(prev_scene.surfaces.drain(start.surfaces..end.surfaces));
|
||||
pub fn reuse_views(&mut self, views: &FxHashSet<EntityId>, prev_scene: &mut Self) {
|
||||
// todo!()
|
||||
// for shadow in prev_scene.shadows.drain(..) {
|
||||
// if views.contains(&shadow.view_id.into()) {
|
||||
// let order = &prev_scene.orders_by_layer[&shadow.layer_id];
|
||||
// self.insert(order, shadow);
|
||||
// }
|
||||
// }
|
||||
|
||||
// for quad in prev_scene.quads.drain(..) {
|
||||
// if views.contains(&quad.view_id.into()) {
|
||||
// let order = &prev_scene.orders_by_layer[&quad.layer_id];
|
||||
// self.insert(order, quad);
|
||||
// }
|
||||
// }
|
||||
|
||||
// for path in prev_scene.paths.drain(..) {
|
||||
// if views.contains(&path.view_id.into()) {
|
||||
// let order = &prev_scene.orders_by_layer[&path.layer_id];
|
||||
// self.insert(order, path);
|
||||
// }
|
||||
// }
|
||||
|
||||
// for underline in prev_scene.underlines.drain(..) {
|
||||
// if views.contains(&underline.view_id.into()) {
|
||||
// let order = &prev_scene.orders_by_layer[&underline.layer_id];
|
||||
// self.insert(order, underline);
|
||||
// }
|
||||
// }
|
||||
|
||||
// for sprite in prev_scene.monochrome_sprites.drain(..) {
|
||||
// if views.contains(&sprite.view_id.into()) {
|
||||
// let order = &prev_scene.orders_by_layer[&sprite.layer_id];
|
||||
// self.insert(order, sprite);
|
||||
// }
|
||||
// }
|
||||
|
||||
// for sprite in prev_scene.polychrome_sprites.drain(..) {
|
||||
// if views.contains(&sprite.view_id.into()) {
|
||||
// let order = &prev_scene.orders_by_layer[&sprite.layer_id];
|
||||
// self.insert(order, sprite);
|
||||
// }
|
||||
// }
|
||||
|
||||
// for surface in prev_scene.surfaces.drain(..) {
|
||||
// if views.contains(&surface.view_id.into()) {
|
||||
// let order = &prev_scene.orders_by_layer[&surface.layer_id];
|
||||
// self.insert(order, surface);
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
pub fn finish(&mut self) {
|
||||
self.shadows.sort_unstable_by_key(|shadow| shadow.order);
|
||||
self.quads.sort_unstable_by_key(|quad| quad.order);
|
||||
self.paths.sort_unstable_by_key(|path| path.order);
|
||||
self.underlines
|
||||
.sort_unstable_by_key(|underline| underline.order);
|
||||
self.monochrome_sprites
|
||||
.sort_unstable_by_key(|sprite| sprite.order);
|
||||
self.polychrome_sprites
|
||||
.sort_unstable_by_key(|sprite| sprite.order);
|
||||
self.surfaces.sort_unstable_by_key(|surface| surface.order);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,31 +236,25 @@ impl<'a> Iterator for BatchIterator<'a> {
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let mut orders_and_kinds = [
|
||||
(
|
||||
self.shadows_iter.peek().map(|s| s.draw_order),
|
||||
self.shadows_iter.peek().map(|s| s.order),
|
||||
PrimitiveKind::Shadow,
|
||||
),
|
||||
(self.quads_iter.peek().map(|q| q.order), PrimitiveKind::Quad),
|
||||
(self.paths_iter.peek().map(|q| q.order), PrimitiveKind::Path),
|
||||
(
|
||||
self.quads_iter.peek().map(|q| q.draw_order),
|
||||
PrimitiveKind::Quad,
|
||||
),
|
||||
(
|
||||
self.paths_iter.peek().map(|q| q.draw_order),
|
||||
PrimitiveKind::Path,
|
||||
),
|
||||
(
|
||||
self.underlines_iter.peek().map(|u| u.draw_order),
|
||||
self.underlines_iter.peek().map(|u| u.order),
|
||||
PrimitiveKind::Underline,
|
||||
),
|
||||
(
|
||||
self.monochrome_sprites_iter.peek().map(|s| s.draw_order),
|
||||
self.monochrome_sprites_iter.peek().map(|s| s.order),
|
||||
PrimitiveKind::MonochromeSprite,
|
||||
),
|
||||
(
|
||||
self.polychrome_sprites_iter.peek().map(|s| s.draw_order),
|
||||
self.polychrome_sprites_iter.peek().map(|s| s.order),
|
||||
PrimitiveKind::PolychromeSprite,
|
||||
),
|
||||
(
|
||||
self.surfaces_iter.peek().map(|s| s.draw_order),
|
||||
self.surfaces_iter.peek().map(|s| s.order),
|
||||
PrimitiveKind::Surface,
|
||||
),
|
||||
];
|
||||
@@ -253,7 +275,7 @@ impl<'a> Iterator for BatchIterator<'a> {
|
||||
self.shadows_iter.next();
|
||||
while self
|
||||
.shadows_iter
|
||||
.next_if(|shadow| (shadow.draw_order, batch_kind) < max_order_and_kind)
|
||||
.next_if(|shadow| (shadow.order, batch_kind) < max_order_and_kind)
|
||||
.is_some()
|
||||
{
|
||||
shadows_end += 1;
|
||||
@@ -269,7 +291,7 @@ impl<'a> Iterator for BatchIterator<'a> {
|
||||
self.quads_iter.next();
|
||||
while self
|
||||
.quads_iter
|
||||
.next_if(|quad| (quad.draw_order, batch_kind) < max_order_and_kind)
|
||||
.next_if(|quad| (quad.order, batch_kind) < max_order_and_kind)
|
||||
.is_some()
|
||||
{
|
||||
quads_end += 1;
|
||||
@@ -283,7 +305,7 @@ impl<'a> Iterator for BatchIterator<'a> {
|
||||
self.paths_iter.next();
|
||||
while self
|
||||
.paths_iter
|
||||
.next_if(|path| (path.draw_order, batch_kind) < max_order_and_kind)
|
||||
.next_if(|path| (path.order, batch_kind) < max_order_and_kind)
|
||||
.is_some()
|
||||
{
|
||||
paths_end += 1;
|
||||
@@ -297,7 +319,7 @@ impl<'a> Iterator for BatchIterator<'a> {
|
||||
self.underlines_iter.next();
|
||||
while self
|
||||
.underlines_iter
|
||||
.next_if(|underline| (underline.draw_order, batch_kind) < max_order_and_kind)
|
||||
.next_if(|underline| (underline.order, batch_kind) < max_order_and_kind)
|
||||
.is_some()
|
||||
{
|
||||
underlines_end += 1;
|
||||
@@ -315,7 +337,7 @@ impl<'a> Iterator for BatchIterator<'a> {
|
||||
while self
|
||||
.monochrome_sprites_iter
|
||||
.next_if(|sprite| {
|
||||
(sprite.draw_order, batch_kind) < max_order_and_kind
|
||||
(sprite.order, batch_kind) < max_order_and_kind
|
||||
&& sprite.tile.texture_id == texture_id
|
||||
})
|
||||
.is_some()
|
||||
@@ -336,7 +358,7 @@ impl<'a> Iterator for BatchIterator<'a> {
|
||||
while self
|
||||
.polychrome_sprites_iter
|
||||
.next_if(|sprite| {
|
||||
(sprite.draw_order, batch_kind) < max_order_and_kind
|
||||
(sprite.order, batch_kind) < max_order_and_kind
|
||||
&& sprite.tile.texture_id == texture_id
|
||||
})
|
||||
.is_some()
|
||||
@@ -355,7 +377,7 @@ impl<'a> Iterator for BatchIterator<'a> {
|
||||
self.surfaces_iter.next();
|
||||
while self
|
||||
.surfaces_iter
|
||||
.next_if(|surface| (surface.draw_order, batch_kind) < max_order_and_kind)
|
||||
.next_if(|surface| (surface.order, batch_kind) < max_order_and_kind)
|
||||
.is_some()
|
||||
{
|
||||
surfaces_end += 1;
|
||||
@@ -437,7 +459,9 @@ pub(crate) enum PrimitiveBatch<'a> {
|
||||
#[derive(Default, Debug, Clone, Eq, PartialEq)]
|
||||
#[repr(C)]
|
||||
pub(crate) struct Quad {
|
||||
pub draw_order: u32,
|
||||
pub view_id: ViewId,
|
||||
pub layer_id: LayerId,
|
||||
pub order: DrawOrder,
|
||||
pub bounds: Bounds<ScaledPixels>,
|
||||
pub content_mask: ContentMask<ScaledPixels>,
|
||||
pub background: Hsla,
|
||||
@@ -448,7 +472,7 @@ pub(crate) struct Quad {
|
||||
|
||||
impl Ord for Quad {
|
||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||
self.draw_order.cmp(&other.draw_order)
|
||||
self.order.cmp(&other.order)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -467,7 +491,9 @@ impl From<Quad> for Primitive {
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
#[repr(C)]
|
||||
pub(crate) struct Underline {
|
||||
pub draw_order: u32,
|
||||
pub view_id: ViewId,
|
||||
pub layer_id: LayerId,
|
||||
pub order: DrawOrder,
|
||||
pub bounds: Bounds<ScaledPixels>,
|
||||
pub content_mask: ContentMask<ScaledPixels>,
|
||||
pub color: Hsla,
|
||||
@@ -477,7 +503,7 @@ pub(crate) struct Underline {
|
||||
|
||||
impl Ord for Underline {
|
||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||
self.draw_order.cmp(&other.draw_order)
|
||||
self.order.cmp(&other.order)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -496,7 +522,9 @@ impl From<Underline> for Primitive {
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
#[repr(C)]
|
||||
pub(crate) struct Shadow {
|
||||
pub draw_order: u32,
|
||||
pub view_id: ViewId,
|
||||
pub layer_id: LayerId,
|
||||
pub order: DrawOrder,
|
||||
pub bounds: Bounds<ScaledPixels>,
|
||||
pub corner_radii: Corners<ScaledPixels>,
|
||||
pub content_mask: ContentMask<ScaledPixels>,
|
||||
@@ -507,7 +535,7 @@ pub(crate) struct Shadow {
|
||||
|
||||
impl Ord for Shadow {
|
||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||
self.draw_order.cmp(&other.draw_order)
|
||||
self.order.cmp(&other.order)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -526,13 +554,30 @@ impl From<Shadow> for Primitive {
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
#[repr(C)]
|
||||
pub(crate) struct MonochromeSprite {
|
||||
pub draw_order: u32,
|
||||
pub view_id: ViewId,
|
||||
pub layer_id: LayerId,
|
||||
pub order: DrawOrder,
|
||||
pub bounds: Bounds<ScaledPixels>,
|
||||
pub content_mask: ContentMask<ScaledPixels>,
|
||||
pub color: Hsla,
|
||||
pub tile: AtlasTile,
|
||||
}
|
||||
|
||||
impl Ord for MonochromeSprite {
|
||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||
match self.order.cmp(&other.order) {
|
||||
std::cmp::Ordering::Equal => self.tile.tile_id.cmp(&other.tile.tile_id),
|
||||
order => order,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for MonochromeSprite {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<MonochromeSprite> for Primitive {
|
||||
fn from(sprite: MonochromeSprite) -> Self {
|
||||
Primitive::MonochromeSprite(sprite)
|
||||
@@ -542,7 +587,9 @@ impl From<MonochromeSprite> for Primitive {
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
#[repr(C)]
|
||||
pub(crate) struct PolychromeSprite {
|
||||
pub draw_order: u32,
|
||||
pub view_id: ViewId,
|
||||
pub layer_id: LayerId,
|
||||
pub order: DrawOrder,
|
||||
pub bounds: Bounds<ScaledPixels>,
|
||||
pub content_mask: ContentMask<ScaledPixels>,
|
||||
pub corner_radii: Corners<ScaledPixels>,
|
||||
@@ -551,6 +598,21 @@ pub(crate) struct PolychromeSprite {
|
||||
pub pad: u32, // align to 8 bytes
|
||||
}
|
||||
|
||||
impl Ord for PolychromeSprite {
|
||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||
match self.order.cmp(&other.order) {
|
||||
std::cmp::Ordering::Equal => self.tile.tile_id.cmp(&other.tile.tile_id),
|
||||
order => order,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for PolychromeSprite {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PolychromeSprite> for Primitive {
|
||||
fn from(sprite: PolychromeSprite) -> Self {
|
||||
Primitive::PolychromeSprite(sprite)
|
||||
@@ -559,13 +621,27 @@ impl From<PolychromeSprite> for Primitive {
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub(crate) struct Surface {
|
||||
pub draw_order: u32,
|
||||
pub view_id: ViewId,
|
||||
pub layer_id: LayerId,
|
||||
pub order: DrawOrder,
|
||||
pub bounds: Bounds<ScaledPixels>,
|
||||
pub content_mask: ContentMask<ScaledPixels>,
|
||||
#[cfg(target_os = "macos")]
|
||||
pub image_buffer: media::core_video::CVImageBuffer,
|
||||
}
|
||||
|
||||
impl Ord for Surface {
|
||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||
self.order.cmp(&other.order)
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for Surface {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Surface> for Primitive {
|
||||
fn from(surface: Surface) -> Self {
|
||||
Primitive::Surface(surface)
|
||||
@@ -578,8 +654,10 @@ pub(crate) struct PathId(pub(crate) usize);
|
||||
/// A line made up of a series of vertices and control points.
|
||||
#[derive(Debug)]
|
||||
pub struct Path<P: Clone + Default + Debug> {
|
||||
pub(crate) draw_order: u32,
|
||||
pub(crate) id: PathId,
|
||||
pub(crate) view_id: ViewId,
|
||||
layer_id: LayerId,
|
||||
order: DrawOrder,
|
||||
pub(crate) bounds: Bounds<P>,
|
||||
pub(crate) content_mask: ContentMask<P>,
|
||||
pub(crate) vertices: Vec<PathVertex<P>>,
|
||||
@@ -593,8 +671,10 @@ impl Path<Pixels> {
|
||||
/// Create a new path with the given starting point.
|
||||
pub fn new(start: Point<Pixels>) -> Self {
|
||||
Self {
|
||||
draw_order: 0,
|
||||
id: PathId(0),
|
||||
view_id: ViewId::default(),
|
||||
layer_id: LayerId::default(),
|
||||
order: DrawOrder::default(),
|
||||
vertices: Vec::new(),
|
||||
start,
|
||||
current: start,
|
||||
@@ -611,8 +691,10 @@ impl Path<Pixels> {
|
||||
/// Scale this path by the given factor.
|
||||
pub fn scale(&self, factor: f32) -> Path<ScaledPixels> {
|
||||
Path {
|
||||
draw_order: self.draw_order,
|
||||
id: self.id,
|
||||
view_id: self.view_id,
|
||||
layer_id: self.layer_id,
|
||||
order: self.order,
|
||||
bounds: self.bounds.scale(factor),
|
||||
content_mask: self.content_mask.scale(factor),
|
||||
vertices: self
|
||||
@@ -698,13 +780,13 @@ impl Eq for Path<ScaledPixels> {}
|
||||
|
||||
impl PartialEq for Path<ScaledPixels> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.draw_order == other.draw_order
|
||||
self.order == other.order
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for Path<ScaledPixels> {
|
||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||
self.draw_order.cmp(&other.draw_order)
|
||||
self.order.cmp(&other.order)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -402,98 +402,88 @@ impl Style {
|
||||
|
||||
let rem_size = cx.rem_size();
|
||||
|
||||
cx.with_z_index(0, |cx| {
|
||||
cx.paint_shadows(
|
||||
bounds,
|
||||
self.corner_radii.to_pixels(bounds.size, rem_size),
|
||||
&self.box_shadow,
|
||||
if self.is_border_visible() {
|
||||
let corner_radii = self.corner_radii.to_pixels(bounds.size, rem_size);
|
||||
let border_widths = self.border_widths.to_pixels(rem_size);
|
||||
let max_border_width = border_widths.max();
|
||||
let max_corner_radius = corner_radii.max();
|
||||
|
||||
let top_bounds = Bounds::from_corners(
|
||||
bounds.origin,
|
||||
bounds.upper_right() + point(Pixels::ZERO, max_border_width.max(max_corner_radius)),
|
||||
);
|
||||
});
|
||||
let bottom_bounds = Bounds::from_corners(
|
||||
bounds.lower_left() - point(Pixels::ZERO, max_border_width.max(max_corner_radius)),
|
||||
bounds.lower_right(),
|
||||
);
|
||||
let left_bounds = Bounds::from_corners(
|
||||
top_bounds.lower_left(),
|
||||
bottom_bounds.origin + point(max_border_width, Pixels::ZERO),
|
||||
);
|
||||
let right_bounds = Bounds::from_corners(
|
||||
top_bounds.lower_right() - point(max_border_width, Pixels::ZERO),
|
||||
bottom_bounds.upper_right(),
|
||||
);
|
||||
|
||||
let mut background = self.border_color.unwrap_or_default();
|
||||
background.a = 0.;
|
||||
let quad = quad(
|
||||
bounds,
|
||||
corner_radii,
|
||||
background,
|
||||
border_widths,
|
||||
self.border_color.unwrap_or_default(),
|
||||
);
|
||||
|
||||
cx.with_content_mask(Some(ContentMask { bounds: top_bounds }), |cx| {
|
||||
cx.paint_quad(quad.clone());
|
||||
});
|
||||
cx.with_content_mask(
|
||||
Some(ContentMask {
|
||||
bounds: right_bounds,
|
||||
}),
|
||||
|cx| {
|
||||
cx.paint_quad(quad.clone());
|
||||
},
|
||||
);
|
||||
cx.with_content_mask(
|
||||
Some(ContentMask {
|
||||
bounds: bottom_bounds,
|
||||
}),
|
||||
|cx| {
|
||||
cx.paint_quad(quad.clone());
|
||||
},
|
||||
);
|
||||
cx.with_content_mask(
|
||||
Some(ContentMask {
|
||||
bounds: left_bounds,
|
||||
}),
|
||||
|cx| {
|
||||
cx.paint_quad(quad);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
continuation(cx);
|
||||
|
||||
let background_color = self.background.as_ref().and_then(Fill::color);
|
||||
if background_color.map_or(false, |color| !color.is_transparent()) {
|
||||
cx.with_z_index(1, |cx| {
|
||||
let mut border_color = background_color.unwrap_or_default();
|
||||
border_color.a = 0.;
|
||||
cx.paint_quad(quad(
|
||||
bounds,
|
||||
self.corner_radii.to_pixels(bounds.size, rem_size),
|
||||
background_color.unwrap_or_default(),
|
||||
Edges::default(),
|
||||
border_color,
|
||||
));
|
||||
});
|
||||
let mut border_color = background_color.unwrap_or_default();
|
||||
border_color.a = 0.;
|
||||
cx.paint_quad(quad(
|
||||
bounds,
|
||||
self.corner_radii.to_pixels(bounds.size, rem_size),
|
||||
background_color.unwrap_or_default(),
|
||||
Edges::default(),
|
||||
border_color,
|
||||
));
|
||||
}
|
||||
|
||||
cx.with_z_index(2, |cx| {
|
||||
continuation(cx);
|
||||
});
|
||||
|
||||
if self.is_border_visible() {
|
||||
cx.with_z_index(3, |cx| {
|
||||
let corner_radii = self.corner_radii.to_pixels(bounds.size, rem_size);
|
||||
let border_widths = self.border_widths.to_pixels(rem_size);
|
||||
let max_border_width = border_widths.max();
|
||||
let max_corner_radius = corner_radii.max();
|
||||
|
||||
let top_bounds = Bounds::from_corners(
|
||||
bounds.origin,
|
||||
bounds.upper_right()
|
||||
+ point(Pixels::ZERO, max_border_width.max(max_corner_radius)),
|
||||
);
|
||||
let bottom_bounds = Bounds::from_corners(
|
||||
bounds.lower_left()
|
||||
- point(Pixels::ZERO, max_border_width.max(max_corner_radius)),
|
||||
bounds.lower_right(),
|
||||
);
|
||||
let left_bounds = Bounds::from_corners(
|
||||
top_bounds.lower_left(),
|
||||
bottom_bounds.origin + point(max_border_width, Pixels::ZERO),
|
||||
);
|
||||
let right_bounds = Bounds::from_corners(
|
||||
top_bounds.lower_right() - point(max_border_width, Pixels::ZERO),
|
||||
bottom_bounds.upper_right(),
|
||||
);
|
||||
|
||||
let mut background = self.border_color.unwrap_or_default();
|
||||
background.a = 0.;
|
||||
let quad = quad(
|
||||
bounds,
|
||||
corner_radii,
|
||||
background,
|
||||
border_widths,
|
||||
self.border_color.unwrap_or_default(),
|
||||
);
|
||||
|
||||
cx.with_content_mask(Some(ContentMask { bounds: top_bounds }), |cx| {
|
||||
cx.paint_quad(quad.clone());
|
||||
});
|
||||
cx.with_content_mask(
|
||||
Some(ContentMask {
|
||||
bounds: right_bounds,
|
||||
}),
|
||||
|cx| {
|
||||
cx.paint_quad(quad.clone());
|
||||
},
|
||||
);
|
||||
cx.with_content_mask(
|
||||
Some(ContentMask {
|
||||
bounds: bottom_bounds,
|
||||
}),
|
||||
|cx| {
|
||||
cx.paint_quad(quad.clone());
|
||||
},
|
||||
);
|
||||
cx.with_content_mask(
|
||||
Some(ContentMask {
|
||||
bounds: left_bounds,
|
||||
}),
|
||||
|cx| {
|
||||
cx.paint_quad(quad);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
cx.paint_shadows(
|
||||
bounds,
|
||||
self.corner_radii.to_pixels(bounds.size, rem_size),
|
||||
&self.box_shadow,
|
||||
);
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
if self.debug_below {
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
use crate::{
|
||||
seal::Sealed, AnyElement, AnyModel, AnyWeakModel, AppContext, AvailableSpace, Bounds,
|
||||
ContentMask, Element, ElementContext, ElementId, Entity, EntityId, Flatten, FocusHandle,
|
||||
FocusableView, FrameIndex, IntoElement, LayoutId, Model, Pixels, Point, Render, Size,
|
||||
StackingOrder, Style, TextStyle, ViewContext, VisualContext, WeakModel,
|
||||
FocusableView, IntoElement, LayoutId, Model, Pixels, Point, Render, Size, StackingOrder, Style,
|
||||
TextStyle, ViewContext, VisualContext, WeakModel,
|
||||
};
|
||||
use anyhow::{Context, Result};
|
||||
use std::{
|
||||
any::{type_name, TypeId},
|
||||
fmt,
|
||||
hash::{Hash, Hasher},
|
||||
ops::Range,
|
||||
};
|
||||
|
||||
/// A view is a piece of state that can be presented on screen by implementing the [Render] trait.
|
||||
@@ -25,16 +24,15 @@ impl<V> Sealed for View<V> {}
|
||||
pub struct AnyViewState {
|
||||
root_style: Style,
|
||||
next_stacking_order_id: u16,
|
||||
cache_state: Option<ViewCacheState>,
|
||||
cache_key: Option<ViewCacheKey>,
|
||||
element: Option<AnyElement>,
|
||||
}
|
||||
|
||||
struct ViewCacheState {
|
||||
struct ViewCacheKey {
|
||||
bounds: Bounds<Pixels>,
|
||||
stacking_order: StackingOrder,
|
||||
content_mask: ContentMask<Pixels>,
|
||||
text_style: TextStyle,
|
||||
subframe_range: Range<FrameIndex>,
|
||||
}
|
||||
|
||||
impl<V: 'static> Entity<V> for View<V> {
|
||||
@@ -214,7 +212,8 @@ impl AnyView {
|
||||
/// When using this method, the view's previous layout and paint will be recycled from the previous frame if [ViewContext::notify] has not been called since it was rendered.
|
||||
/// The one exception is when [WindowContext::refresh] is called, in which case caching is ignored.
|
||||
pub fn cached(mut self) -> Self {
|
||||
self.cache = true;
|
||||
// TODO!: ENABLE ME!
|
||||
// self.cache = true;
|
||||
self
|
||||
}
|
||||
|
||||
@@ -299,7 +298,7 @@ impl Element for AnyView {
|
||||
let state = AnyViewState {
|
||||
root_style,
|
||||
next_stacking_order_id: 0,
|
||||
cache_state: None,
|
||||
cache_key: None,
|
||||
element: Some(element),
|
||||
};
|
||||
(layout_id, state)
|
||||
@@ -313,25 +312,23 @@ impl Element for AnyView {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(cache_state) = state.cache_state.as_mut() {
|
||||
if cache_state.bounds == bounds
|
||||
&& cache_state.content_mask == cx.content_mask()
|
||||
&& cache_state.stacking_order == *cx.stacking_order()
|
||||
&& cache_state.text_style == cx.text_style()
|
||||
if let Some(cache_key) = state.cache_key.as_mut() {
|
||||
if cache_key.bounds == bounds
|
||||
&& cache_key.content_mask == cx.content_mask()
|
||||
&& cache_key.stacking_order == *cx.stacking_order()
|
||||
&& cache_key.text_style == cx.text_style()
|
||||
{
|
||||
cx.reuse_view(cache_state.subframe_range.clone());
|
||||
cx.reuse_view(state.next_stacking_order_id);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let subframe_start = cx.window.next_frame.current_index();
|
||||
if let Some(mut element) = state.element.take() {
|
||||
element.paint(cx);
|
||||
} else {
|
||||
let mut element = (self.request_layout)(self, cx).1;
|
||||
element.draw(bounds.origin, bounds.size.into(), cx);
|
||||
}
|
||||
let subframe_end = cx.window.next_frame.current_index();
|
||||
|
||||
state.next_stacking_order_id = cx
|
||||
.window
|
||||
@@ -340,12 +337,11 @@ impl Element for AnyView {
|
||||
.last()
|
||||
.copied()
|
||||
.unwrap();
|
||||
state.cache_state = Some(ViewCacheState {
|
||||
state.cache_key = Some(ViewCacheKey {
|
||||
bounds,
|
||||
stacking_order: cx.stacking_order().clone(),
|
||||
content_mask: cx.content_mask(),
|
||||
text_style: cx.text_style(),
|
||||
subframe_range: subframe_start..subframe_end,
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use crate::{
|
||||
frame::ActionListener, px, size, transparent_black, Action, AnyDrag, AnyView, AppContext,
|
||||
Arena, AsyncWindowContext, AvailableSpace, Bounds, Context, Corners, CursorStyle, DisplayId,
|
||||
Edges, Effect, Entity, EntityId, EventEmitter, FileDropEvent, Flatten, Frame, Global,
|
||||
GlobalElementId, Hsla, KeyBinding, KeyContext, KeyDownEvent, KeyMatch, KeymatchResult,
|
||||
px, size, transparent_black, Action, AnyDrag, AnyView, AppContext, Arena, AsyncWindowContext,
|
||||
AvailableSpace, Bounds, Context, Corners, CursorStyle, DispatchActionListener, DispatchNodeId,
|
||||
DispatchTree, DisplayId, Edges, Effect, Entity, EntityId, EventEmitter, FileDropEvent, Flatten,
|
||||
Global, GlobalElementId, Hsla, KeyBinding, KeyContext, KeyDownEvent, KeyMatch, KeymatchResult,
|
||||
Keystroke, KeystrokeEvent, Model, ModelContext, Modifiers, MouseButton, MouseMoveEvent,
|
||||
MouseUpEvent, Pixels, PlatformAtlas, PlatformDisplay, PlatformInput, PlatformWindow, Point,
|
||||
PromptLevel, Render, ScaledPixels, SharedString, Size, SubscriberSet, Subscription,
|
||||
@@ -126,7 +126,10 @@ impl FocusId {
|
||||
|
||||
/// Obtains whether this handle contains the given handle in the most recently rendered frame.
|
||||
pub(crate) fn contains(&self, other: Self, cx: &WindowContext) -> bool {
|
||||
cx.window.rendered_frame.focus_contains(*self, other)
|
||||
cx.window
|
||||
.rendered_frame
|
||||
.dispatch_tree
|
||||
.focus_contains(*self, other)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -311,6 +314,13 @@ impl PendingInput {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct ElementStateBox {
|
||||
pub(crate) inner: Box<dyn Any>,
|
||||
pub(crate) parent_view_id: EntityId,
|
||||
#[cfg(debug_assertions)]
|
||||
pub(crate) type_name: &'static str,
|
||||
}
|
||||
|
||||
impl Window {
|
||||
pub(crate) fn new(
|
||||
handle: AnyWindowHandle,
|
||||
@@ -439,8 +449,8 @@ impl Window {
|
||||
layout_engine: Some(TaffyLayoutEngine::new()),
|
||||
root_view: None,
|
||||
element_id_stack: GlobalElementId::default(),
|
||||
rendered_frame: Frame::new(cx.keymap.clone(), cx.actions.clone()),
|
||||
next_frame: Frame::new(cx.keymap.clone(), cx.actions.clone()),
|
||||
rendered_frame: Frame::new(DispatchTree::new(cx.keymap.clone(), cx.actions.clone())),
|
||||
next_frame: Frame::new(DispatchTree::new(cx.keymap.clone(), cx.actions.clone())),
|
||||
next_frame_callbacks,
|
||||
dirty_views: FxHashSet::default(),
|
||||
focus_handles: Arc::new(RwLock::new(SlotMap::with_key())),
|
||||
@@ -551,7 +561,10 @@ impl<'a> WindowContext<'a> {
|
||||
}
|
||||
|
||||
self.window.focus = Some(handle.id);
|
||||
self.window.rendered_frame.clear_pending_keystrokes();
|
||||
self.window
|
||||
.rendered_frame
|
||||
.dispatch_tree
|
||||
.clear_pending_keystrokes();
|
||||
self.refresh();
|
||||
}
|
||||
|
||||
@@ -578,10 +591,20 @@ impl<'a> WindowContext<'a> {
|
||||
|
||||
/// Dispatch the given action on the currently focused element.
|
||||
pub fn dispatch_action(&mut self, action: Box<dyn Action>) {
|
||||
let focus_id = self.window.focus;
|
||||
let focus_handle = self.focused();
|
||||
|
||||
self.defer(move |cx| {
|
||||
let node_id = focus_handle
|
||||
.and_then(|handle| {
|
||||
cx.window
|
||||
.rendered_frame
|
||||
.dispatch_tree
|
||||
.focusable_node_id(handle.id)
|
||||
})
|
||||
.unwrap_or_else(|| cx.window.rendered_frame.dispatch_tree.root_node_id());
|
||||
|
||||
cx.propagate_event = true;
|
||||
cx.dispatch_action_on(focus_id, action);
|
||||
cx.dispatch_action_on_node(node_id, action);
|
||||
})
|
||||
}
|
||||
|
||||
@@ -609,8 +632,14 @@ impl<'a> WindowContext<'a> {
|
||||
}
|
||||
|
||||
pub(crate) fn clear_pending_keystrokes(&mut self) {
|
||||
self.window.rendered_frame.clear_pending_keystrokes();
|
||||
self.window.next_frame.clear_pending_keystrokes();
|
||||
self.window
|
||||
.rendered_frame
|
||||
.dispatch_tree
|
||||
.clear_pending_keystrokes();
|
||||
self.window
|
||||
.next_frame
|
||||
.dispatch_tree
|
||||
.clear_pending_keystrokes();
|
||||
}
|
||||
|
||||
/// Schedules the given function to be run at the end of the current effect cycle, allowing entities
|
||||
@@ -805,9 +834,19 @@ impl<'a> WindowContext<'a> {
|
||||
|
||||
/// Determine whether the given action is available along the dispatch path to the currently focused element.
|
||||
pub fn is_action_available(&self, action: &dyn Action) -> bool {
|
||||
let target = self
|
||||
.focused()
|
||||
.and_then(|focused_handle| {
|
||||
self.window
|
||||
.rendered_frame
|
||||
.dispatch_tree
|
||||
.focusable_node_id(focused_handle.id)
|
||||
})
|
||||
.unwrap_or_else(|| self.window.rendered_frame.dispatch_tree.root_node_id());
|
||||
self.window
|
||||
.rendered_frame
|
||||
.is_action_available(action, self.window.focus)
|
||||
.dispatch_tree
|
||||
.is_action_available(action, target)
|
||||
}
|
||||
|
||||
/// The position of the mouse relative to the window.
|
||||
@@ -824,38 +863,36 @@ impl<'a> WindowContext<'a> {
|
||||
/// on top of the given level. Layers who are extensions of the queried layer
|
||||
/// are not considered to be on top of queried layer.
|
||||
pub fn was_top_layer(&self, point: &Point<Pixels>, layer: &StackingOrder) -> bool {
|
||||
todo!()
|
||||
// Precondition: the depth map is ordered from topmost to bottomost.
|
||||
|
||||
// // Precondition: the depth map is ordered from topmost to bottomost.
|
||||
for (opaque_layer, _, bounds) in self.window.rendered_frame.depth_map.iter() {
|
||||
if layer >= opaque_layer {
|
||||
// The queried layer is either above or is the same as the this opaque layer.
|
||||
// Anything after this point is guaranteed to be below the queried layer.
|
||||
return true;
|
||||
}
|
||||
|
||||
// for (opaque_layer, _, bounds) in self.window.rendered_frame.depth_map.iter() {
|
||||
// if layer >= opaque_layer {
|
||||
// // The queried layer is either above or is the same as the this opaque layer.
|
||||
// // Anything after this point is guaranteed to be below the queried layer.
|
||||
// return true;
|
||||
// }
|
||||
if !bounds.contains(point) {
|
||||
// This opaque layer is above the queried layer but it doesn't contain
|
||||
// the given position, so we can ignore it even if it's above.
|
||||
continue;
|
||||
}
|
||||
|
||||
// if !bounds.contains(point) {
|
||||
// // This opaque layer is above the queried layer but it doesn't contain
|
||||
// // the given position, so we can ignore it even if it's above.
|
||||
// continue;
|
||||
// }
|
||||
// At this point, we've established that this opaque layer is on top of the queried layer
|
||||
// and contains the position:
|
||||
// If neither the opaque layer or the queried layer is an extension of the other then
|
||||
// we know they are on different stacking orders, and return false.
|
||||
let is_on_same_layer = opaque_layer
|
||||
.iter()
|
||||
.zip(layer.iter())
|
||||
.all(|(a, b)| a.z_index == b.z_index);
|
||||
|
||||
// // At this point, we've established that this opaque layer is on top of the queried layer
|
||||
// // and contains the position:
|
||||
// // If neither the opaque layer or the queried layer is an extension of the other then
|
||||
// // we know they are on different stacking orders, and return false.
|
||||
// let is_on_same_layer = opaque_layer
|
||||
// .iter()
|
||||
// .zip(layer.iter())
|
||||
// .all(|(a, b)| a.z_index == b.z_index);
|
||||
if !is_on_same_layer {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// if !is_on_same_layer {
|
||||
// return false;
|
||||
// }
|
||||
// }
|
||||
|
||||
// true
|
||||
true
|
||||
}
|
||||
|
||||
pub(crate) fn was_top_layer_under_active_drag(
|
||||
@@ -863,61 +900,67 @@ impl<'a> WindowContext<'a> {
|
||||
point: &Point<Pixels>,
|
||||
layer: &StackingOrder,
|
||||
) -> bool {
|
||||
todo!()
|
||||
// Precondition: the depth map is ordered from topmost to bottomost.
|
||||
|
||||
// for (opaque_layer, _, bounds) in self.window.rendered_frame.depth_map.iter() {
|
||||
// if layer >= opaque_layer {
|
||||
// // The queried layer is either above or is the same as the this opaque layer.
|
||||
// // Anything after this point is guaranteed to be below the queried layer.
|
||||
// return true;
|
||||
// }
|
||||
for (opaque_layer, _, bounds) in self.window.rendered_frame.depth_map.iter() {
|
||||
if layer >= opaque_layer {
|
||||
// The queried layer is either above or is the same as the this opaque layer.
|
||||
// Anything after this point is guaranteed to be below the queried layer.
|
||||
return true;
|
||||
}
|
||||
|
||||
// if !bounds.contains(point) {
|
||||
// // This opaque layer is above the queried layer but it doesn't contain
|
||||
// // the given position, so we can ignore it even if it's above.
|
||||
// continue;
|
||||
// }
|
||||
if !bounds.contains(point) {
|
||||
// This opaque layer is above the queried layer but it doesn't contain
|
||||
// the given position, so we can ignore it even if it's above.
|
||||
continue;
|
||||
}
|
||||
|
||||
// // All normal content is rendered with a base z-index of 0, we know that if the root of this opaque layer
|
||||
// // equals `ACTIVE_DRAG_Z_INDEX` then it must be the drag layer and we can ignore it as we are
|
||||
// // looking to see if the queried layer was the topmost underneath the drag layer.
|
||||
// if opaque_layer
|
||||
// .first()
|
||||
// .map(|c| c.z_index == ACTIVE_DRAG_Z_INDEX)
|
||||
// .unwrap_or(false)
|
||||
// {
|
||||
// continue;
|
||||
// }
|
||||
// All normal content is rendered with a base z-index of 0, we know that if the root of this opaque layer
|
||||
// equals `ACTIVE_DRAG_Z_INDEX` then it must be the drag layer and we can ignore it as we are
|
||||
// looking to see if the queried layer was the topmost underneath the drag layer.
|
||||
if opaque_layer
|
||||
.first()
|
||||
.map(|c| c.z_index == ACTIVE_DRAG_Z_INDEX)
|
||||
.unwrap_or(false)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// // At this point, we've established that this opaque layer is on top of the queried layer
|
||||
// // and contains the position:
|
||||
// // If neither the opaque layer or the queried layer is an extension of the other then
|
||||
// // we know they are on different stacking orders, and return false.
|
||||
// let is_on_same_layer = opaque_layer
|
||||
// .iter()
|
||||
// .zip(layer.iter())
|
||||
// .all(|(a, b)| a.z_index == b.z_index);
|
||||
// At this point, we've established that this opaque layer is on top of the queried layer
|
||||
// and contains the position:
|
||||
// If neither the opaque layer or the queried layer is an extension of the other then
|
||||
// we know they are on different stacking orders, and return false.
|
||||
let is_on_same_layer = opaque_layer
|
||||
.iter()
|
||||
.zip(layer.iter())
|
||||
.all(|(a, b)| a.z_index == b.z_index);
|
||||
|
||||
// if !is_on_same_layer {
|
||||
// return false;
|
||||
// }
|
||||
// }
|
||||
if !is_on_same_layer {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// true
|
||||
true
|
||||
}
|
||||
|
||||
/// Called during painting to get the current stacking order.
|
||||
pub fn stacking_order(&self) -> &StackingOrder {
|
||||
todo!()
|
||||
&self.window.next_frame.z_index_stack
|
||||
}
|
||||
|
||||
/// Produces a new frame and assigns it to `rendered_frame`. To actually show
|
||||
/// the contents of the new [Scene], use [present].
|
||||
#[profiling::function]
|
||||
pub fn draw(&mut self) {
|
||||
self.window.dirty.set(false);
|
||||
self.window.drawing = true;
|
||||
|
||||
if let Some(requested_handler) = self.window.rendered_frame.requested_input_handler.as_mut()
|
||||
{
|
||||
let input_handler = self.window.platform_window.take_input_handler();
|
||||
requested_handler.handler = input_handler;
|
||||
}
|
||||
|
||||
let root_view = self.window.root_view.take().unwrap();
|
||||
self.with_element_context(|cx| {
|
||||
cx.with_z_index(0, |cx| {
|
||||
@@ -925,7 +968,7 @@ impl<'a> WindowContext<'a> {
|
||||
// We need to use cx.cx here so we can utilize borrow splitting
|
||||
for (action_type, action_listeners) in &cx.cx.app.global_action_listeners {
|
||||
for action_listener in action_listeners.iter().cloned() {
|
||||
cx.cx.window.next_frame.on_action(
|
||||
cx.cx.window.next_frame.dispatch_tree.on_action(
|
||||
*action_type,
|
||||
Rc::new(
|
||||
move |action: &dyn Any, phase, cx: &mut WindowContext<'_>| {
|
||||
@@ -968,14 +1011,15 @@ impl<'a> WindowContext<'a> {
|
||||
}
|
||||
self.window.dirty_views.clear();
|
||||
|
||||
self.window.next_frame.preserve_pending_keystrokes(
|
||||
&mut self.window.rendered_frame.dispatch_tree,
|
||||
self.window.focus,
|
||||
);
|
||||
self.window.next_frame.set_focus(self.window.focus);
|
||||
self.window
|
||||
.next_frame
|
||||
.set_window_active(self.window.active.get());
|
||||
.dispatch_tree
|
||||
.preserve_pending_keystrokes(
|
||||
&mut self.window.rendered_frame.dispatch_tree,
|
||||
self.window.focus,
|
||||
);
|
||||
self.window.next_frame.focus = self.window.focus;
|
||||
self.window.next_frame.window_active = self.window.active.get();
|
||||
self.window.root_view = Some(root_view);
|
||||
|
||||
// Set the cursor only if we're the active window.
|
||||
@@ -999,10 +1043,9 @@ impl<'a> WindowContext<'a> {
|
||||
self.window.layout_engine.as_mut().unwrap().clear();
|
||||
self.text_system()
|
||||
.finish_frame(&self.window.next_frame.reused_views);
|
||||
// todo!()
|
||||
// self.window
|
||||
// .next_frame
|
||||
// .finish(&mut self.window.rendered_frame);
|
||||
self.window
|
||||
.next_frame
|
||||
.finish(&mut self.window.rendered_frame);
|
||||
ELEMENT_ARENA.with_borrow_mut(|element_arena| {
|
||||
let percentage = (element_arena.len() as f32 / element_arena.capacity() as f32) * 100.;
|
||||
if percentage >= 80. {
|
||||
@@ -1050,11 +1093,13 @@ impl<'a> WindowContext<'a> {
|
||||
self.window.needs_present.set(true);
|
||||
}
|
||||
|
||||
#[profiling::function]
|
||||
fn present(&self) {
|
||||
self.window
|
||||
.platform_window
|
||||
.draw(&self.window.rendered_frame.scene);
|
||||
self.window.needs_present.set(false);
|
||||
profiling::finish_frame!();
|
||||
}
|
||||
|
||||
/// Dispatch a given keystroke as though the user had typed it.
|
||||
@@ -1090,6 +1135,7 @@ impl<'a> WindowContext<'a> {
|
||||
}
|
||||
|
||||
/// Dispatch a mouse or keyboard event on the window.
|
||||
#[profiling::function]
|
||||
pub fn dispatch_event(&mut self, event: PlatformInput) -> bool {
|
||||
self.window.last_input_timestamp.set(Instant::now());
|
||||
// Handlers may set this to false by calling `stop_propagation`.
|
||||
@@ -1184,54 +1230,56 @@ impl<'a> WindowContext<'a> {
|
||||
}
|
||||
|
||||
fn dispatch_mouse_event(&mut self, event: &dyn Any) {
|
||||
todo!()
|
||||
// if let Some(mut handlers) = self
|
||||
// .window
|
||||
// .rendered_frame
|
||||
// .mouse_listeners
|
||||
// .remove(&event.type_id())
|
||||
// {
|
||||
// // Capture phase, events bubble from back to front. Handlers for this phase are used for
|
||||
// // special purposes, such as detecting events outside of a given Bounds.
|
||||
// for (_, _, handler) in &mut handlers {
|
||||
// self.with_element_context(|cx| {
|
||||
// handler(event, DispatchPhase::Capture, cx);
|
||||
// });
|
||||
// if !self.app.propagate_event {
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
if let Some(mut handlers) = self
|
||||
.window
|
||||
.rendered_frame
|
||||
.mouse_listeners
|
||||
.remove(&event.type_id())
|
||||
{
|
||||
// Because handlers may add other handlers, we sort every time.
|
||||
handlers.sort_by(|(a, _, _), (b, _, _)| a.cmp(b));
|
||||
|
||||
// // Bubble phase, where most normal handlers do their work.
|
||||
// if self.app.propagate_event {
|
||||
// for (_, _, handler) in handlers.iter_mut().rev() {
|
||||
// self.with_element_context(|cx| {
|
||||
// handler(event, DispatchPhase::Bubble, cx);
|
||||
// });
|
||||
// if !self.app.propagate_event {
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// Capture phase, events bubble from back to front. Handlers for this phase are used for
|
||||
// special purposes, such as detecting events outside of a given Bounds.
|
||||
for (_, _, handler) in &mut handlers {
|
||||
self.with_element_context(|cx| {
|
||||
handler(event, DispatchPhase::Capture, cx);
|
||||
});
|
||||
if !self.app.propagate_event {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// self.window
|
||||
// .rendered_frame
|
||||
// .mouse_listeners
|
||||
// .insert(event.type_id(), handlers);
|
||||
// }
|
||||
// Bubble phase, where most normal handlers do their work.
|
||||
if self.app.propagate_event {
|
||||
for (_, _, handler) in handlers.iter_mut().rev() {
|
||||
self.with_element_context(|cx| {
|
||||
handler(event, DispatchPhase::Bubble, cx);
|
||||
});
|
||||
if !self.app.propagate_event {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if self.app.propagate_event && self.has_active_drag() {
|
||||
// if event.is::<MouseMoveEvent>() {
|
||||
// // If this was a mouse move event, redraw the window so that the
|
||||
// // active drag can follow the mouse cursor.
|
||||
// self.refresh();
|
||||
// } else if event.is::<MouseUpEvent>() {
|
||||
// // If this was a mouse up event, cancel the active drag and redraw
|
||||
// // the window.
|
||||
// self.active_drag = None;
|
||||
// self.refresh();
|
||||
// }
|
||||
// }
|
||||
self.window
|
||||
.rendered_frame
|
||||
.mouse_listeners
|
||||
.insert(event.type_id(), handlers);
|
||||
}
|
||||
|
||||
if self.app.propagate_event && self.has_active_drag() {
|
||||
if event.is::<MouseMoveEvent>() {
|
||||
// If this was a mouse move event, redraw the window so that the
|
||||
// active drag can follow the mouse cursor.
|
||||
self.refresh();
|
||||
} else if event.is::<MouseUpEvent>() {
|
||||
// If this was a mouse up event, cancel the active drag and redraw
|
||||
// the window.
|
||||
self.active_drag = None;
|
||||
self.refresh();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn dispatch_key_event(&mut self, event: &dyn Any) {
|
||||
@@ -1239,14 +1287,29 @@ impl<'a> WindowContext<'a> {
|
||||
self.draw();
|
||||
}
|
||||
|
||||
let focus_id = self.window.focus;
|
||||
let dispatch_path = self.window.rendered_frame.key_dispatch_path(focus_id);
|
||||
let node_id = self
|
||||
.window
|
||||
.focus
|
||||
.and_then(|focus_id| {
|
||||
self.window
|
||||
.rendered_frame
|
||||
.dispatch_tree
|
||||
.focusable_node_id(focus_id)
|
||||
})
|
||||
.unwrap_or_else(|| self.window.rendered_frame.dispatch_tree.root_node_id());
|
||||
|
||||
let dispatch_path = self
|
||||
.window
|
||||
.rendered_frame
|
||||
.dispatch_tree
|
||||
.dispatch_path(node_id);
|
||||
|
||||
if let Some(key_down_event) = event.downcast_ref::<KeyDownEvent>() {
|
||||
let KeymatchResult { bindings, pending } = self
|
||||
.window
|
||||
.rendered_frame
|
||||
.match_keystroke(&key_down_event.keystroke, focus_id);
|
||||
.dispatch_tree
|
||||
.dispatch_key(&key_down_event.keystroke, &dispatch_path);
|
||||
|
||||
if pending {
|
||||
let mut currently_pending = self.window.pending_input.take().unwrap_or_default();
|
||||
@@ -1276,11 +1339,8 @@ impl<'a> WindowContext<'a> {
|
||||
self.window.pending_input = Some(currently_pending);
|
||||
|
||||
self.propagate_event = false;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(currently_pending) = self.window.pending_input.take() {
|
||||
} else if let Some(currently_pending) = self.window.pending_input.take() {
|
||||
if bindings
|
||||
.iter()
|
||||
.all(|binding| !currently_pending.used_by_binding(binding))
|
||||
@@ -1295,7 +1355,7 @@ impl<'a> WindowContext<'a> {
|
||||
|
||||
self.propagate_event = true;
|
||||
for binding in bindings {
|
||||
self.dispatch_action_on(focus_id, binding.action.boxed_clone());
|
||||
self.dispatch_action_on_node(node_id, binding.action.boxed_clone());
|
||||
if !self.propagate_event {
|
||||
self.dispatch_keystroke_observers(event, Some(binding.action));
|
||||
return;
|
||||
@@ -1303,7 +1363,7 @@ impl<'a> WindowContext<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
self.dispatch_key_down_up_event(event, focus_id);
|
||||
self.dispatch_key_down_up_event(event, &dispatch_path);
|
||||
if !self.propagate_event {
|
||||
return;
|
||||
}
|
||||
@@ -1311,37 +1371,61 @@ impl<'a> WindowContext<'a> {
|
||||
self.dispatch_keystroke_observers(event, None);
|
||||
}
|
||||
|
||||
fn dispatch_key_down_up_event(&mut self, event: &dyn Any, focus_id: Option<FocusId>) {
|
||||
fn dispatch_key_down_up_event(
|
||||
&mut self,
|
||||
event: &dyn Any,
|
||||
dispatch_path: &SmallVec<[DispatchNodeId; 32]>,
|
||||
) {
|
||||
// Capture phase
|
||||
let key_dispatch_path = self.window.rendered_frame.key_dispatch_path(focus_id);
|
||||
for key_listener in &key_dispatch_path {
|
||||
self.with_element_context(|cx| {
|
||||
key_listener(event, DispatchPhase::Capture, cx);
|
||||
});
|
||||
if !self.propagate_event {
|
||||
return;
|
||||
for node_id in dispatch_path {
|
||||
let node = self.window.rendered_frame.dispatch_tree.node(*node_id);
|
||||
|
||||
for key_listener in node.key_listeners.clone() {
|
||||
self.with_element_context(|cx| {
|
||||
key_listener(event, DispatchPhase::Capture, cx);
|
||||
});
|
||||
if !self.propagate_event {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Bubble phase
|
||||
for key_listener in key_dispatch_path.iter().rev() {
|
||||
self.with_element_context(|cx| {
|
||||
key_listener(event, DispatchPhase::Bubble, cx);
|
||||
});
|
||||
if !self.propagate_event {
|
||||
return;
|
||||
for node_id in dispatch_path.iter().rev() {
|
||||
// Handle low level key events
|
||||
let node = self.window.rendered_frame.dispatch_tree.node(*node_id);
|
||||
for key_listener in node.key_listeners.clone() {
|
||||
self.with_element_context(|cx| {
|
||||
key_listener(event, DispatchPhase::Bubble, cx);
|
||||
});
|
||||
if !self.propagate_event {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Determine whether a potential multi-stroke key binding is in progress on this window.
|
||||
pub fn has_pending_keystrokes(&self) -> bool {
|
||||
self.window.rendered_frame.has_pending_keystrokes()
|
||||
self.window
|
||||
.rendered_frame
|
||||
.dispatch_tree
|
||||
.has_pending_keystrokes()
|
||||
}
|
||||
|
||||
fn replay_pending_input(&mut self, currently_pending: PendingInput) {
|
||||
let focus_id = self.window.focus;
|
||||
if focus_id != currently_pending.focus {
|
||||
let node_id = self
|
||||
.window
|
||||
.focus
|
||||
.and_then(|focus_id| {
|
||||
self.window
|
||||
.rendered_frame
|
||||
.dispatch_tree
|
||||
.focusable_node_id(focus_id)
|
||||
})
|
||||
.unwrap_or_else(|| self.window.rendered_frame.dispatch_tree.root_node_id());
|
||||
|
||||
if self.window.focus != currently_pending.focus {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1349,19 +1433,25 @@ impl<'a> WindowContext<'a> {
|
||||
|
||||
self.propagate_event = true;
|
||||
for binding in currently_pending.bindings {
|
||||
self.dispatch_action_on(focus_id, binding.action.boxed_clone());
|
||||
self.dispatch_action_on_node(node_id, binding.action.boxed_clone());
|
||||
if !self.propagate_event {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let dispatch_path = self
|
||||
.window
|
||||
.rendered_frame
|
||||
.dispatch_tree
|
||||
.dispatch_path(node_id);
|
||||
|
||||
for keystroke in currently_pending.keystrokes {
|
||||
let event = KeyDownEvent {
|
||||
keystroke,
|
||||
is_held: false,
|
||||
};
|
||||
|
||||
self.dispatch_key_down_up_event(&event, focus_id);
|
||||
self.dispatch_key_down_up_event(&event, &dispatch_path);
|
||||
if !self.propagate_event {
|
||||
return;
|
||||
}
|
||||
@@ -1375,43 +1465,52 @@ impl<'a> WindowContext<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
fn dispatch_action_on(&mut self, focus_id: Option<FocusId>, action: Box<dyn Action>) {
|
||||
let dispatch_path = self.window.rendered_frame.action_dispatch_path(focus_id);
|
||||
fn dispatch_action_on_node(&mut self, node_id: DispatchNodeId, action: Box<dyn Action>) {
|
||||
let dispatch_path = self
|
||||
.window
|
||||
.rendered_frame
|
||||
.dispatch_tree
|
||||
.dispatch_path(node_id);
|
||||
|
||||
// Capture phase
|
||||
for ActionListener {
|
||||
action_type,
|
||||
listener,
|
||||
} in &dispatch_path
|
||||
{
|
||||
let any_action = action.as_any();
|
||||
if *action_type == any_action.type_id() {
|
||||
self.with_element_context(|cx| {
|
||||
listener(any_action, DispatchPhase::Capture, cx);
|
||||
});
|
||||
for node_id in &dispatch_path {
|
||||
let node = self.window.rendered_frame.dispatch_tree.node(*node_id);
|
||||
for DispatchActionListener {
|
||||
action_type,
|
||||
listener,
|
||||
} in node.action_listeners.clone()
|
||||
{
|
||||
let any_action = action.as_any();
|
||||
if action_type == any_action.type_id() {
|
||||
self.with_element_context(|cx| {
|
||||
listener(any_action, DispatchPhase::Capture, cx);
|
||||
});
|
||||
|
||||
if !self.propagate_event {
|
||||
return;
|
||||
if !self.propagate_event {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Bubble phase
|
||||
for ActionListener {
|
||||
action_type,
|
||||
listener,
|
||||
} in dispatch_path.iter().rev()
|
||||
{
|
||||
let any_action = action.as_any();
|
||||
if *action_type == any_action.type_id() {
|
||||
self.propagate_event = false; // Actions stop propagation by default during the bubble phase
|
||||
for node_id in dispatch_path.iter().rev() {
|
||||
let node = self.window.rendered_frame.dispatch_tree.node(*node_id);
|
||||
for DispatchActionListener {
|
||||
action_type,
|
||||
listener,
|
||||
} in node.action_listeners.clone()
|
||||
{
|
||||
let any_action = action.as_any();
|
||||
if action_type == any_action.type_id() {
|
||||
self.propagate_event = false; // Actions stop propagation by default during the bubble phase
|
||||
|
||||
self.with_element_context(|cx| {
|
||||
listener(any_action, DispatchPhase::Bubble, cx);
|
||||
});
|
||||
self.with_element_context(|cx| {
|
||||
listener(any_action, DispatchPhase::Bubble, cx);
|
||||
});
|
||||
|
||||
if !self.propagate_event {
|
||||
return;
|
||||
if !self.propagate_event {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1472,16 +1571,32 @@ impl<'a> WindowContext<'a> {
|
||||
|
||||
/// Returns all available actions for the focused element.
|
||||
pub fn available_actions(&self) -> Vec<Box<dyn Action>> {
|
||||
let node_id = self
|
||||
.window
|
||||
.focus
|
||||
.and_then(|focus_id| {
|
||||
self.window
|
||||
.rendered_frame
|
||||
.dispatch_tree
|
||||
.focusable_node_id(focus_id)
|
||||
})
|
||||
.unwrap_or_else(|| self.window.rendered_frame.dispatch_tree.root_node_id());
|
||||
|
||||
self.window
|
||||
.rendered_frame
|
||||
.available_actions(self.window.focus)
|
||||
.dispatch_tree
|
||||
.available_actions(node_id)
|
||||
}
|
||||
|
||||
/// Returns key bindings that invoke the given action on the currently focused element.
|
||||
pub fn bindings_for_action(&self, action: &dyn Action) -> Vec<KeyBinding> {
|
||||
self.window
|
||||
.rendered_frame
|
||||
.bindings_for_action(action, self.window.focus)
|
||||
.dispatch_tree
|
||||
.bindings_for_action(
|
||||
action,
|
||||
&self.window.rendered_frame.dispatch_tree.context_stack,
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns any bindings that would invoke the given action on the given focus handle if it were focused.
|
||||
@@ -1490,9 +1605,17 @@ impl<'a> WindowContext<'a> {
|
||||
action: &dyn Action,
|
||||
focus_handle: &FocusHandle,
|
||||
) -> Vec<KeyBinding> {
|
||||
self.window
|
||||
.rendered_frame
|
||||
.bindings_for_action(action, Some(focus_handle.id))
|
||||
let dispatch_tree = &self.window.rendered_frame.dispatch_tree;
|
||||
|
||||
let Some(node_id) = dispatch_tree.focusable_node_id(focus_handle.id) else {
|
||||
return vec![];
|
||||
};
|
||||
let context_stack: Vec<_> = dispatch_tree
|
||||
.dispatch_path(node_id)
|
||||
.into_iter()
|
||||
.filter_map(|node_id| dispatch_tree.node(node_id).context.clone())
|
||||
.collect();
|
||||
dispatch_tree.bindings_for_action(action, &context_stack)
|
||||
}
|
||||
|
||||
/// Returns a generic event listener that invokes the given listener with the view and context associated with the given view handle.
|
||||
@@ -1528,6 +1651,15 @@ impl<'a> WindowContext<'a> {
|
||||
.on_should_close(Box::new(move || this.update(|cx| f(cx)).unwrap_or(true)))
|
||||
}
|
||||
|
||||
pub(crate) fn parent_view_id(&self) -> EntityId {
|
||||
*self
|
||||
.window
|
||||
.next_frame
|
||||
.view_stack
|
||||
.last()
|
||||
.expect("a view should always be on the stack while drawing")
|
||||
}
|
||||
|
||||
/// Register an action listener on the window for the next frame. The type of action
|
||||
/// is determined by the first parameter of the given listener. When the next frame is rendered
|
||||
/// the listener will be cleared.
|
||||
@@ -1541,6 +1673,7 @@ impl<'a> WindowContext<'a> {
|
||||
) {
|
||||
self.window
|
||||
.next_frame
|
||||
.dispatch_tree
|
||||
.on_action(action_type, Rc::new(listener));
|
||||
}
|
||||
}
|
||||
@@ -1952,6 +2085,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
|
||||
for view_id in self
|
||||
.window
|
||||
.rendered_frame
|
||||
.dispatch_tree
|
||||
.view_path(self.view.entity_id())
|
||||
.into_iter()
|
||||
.rev()
|
||||
|
||||
@@ -16,13 +16,12 @@ use std::{
|
||||
any::{Any, TypeId},
|
||||
borrow::{Borrow, BorrowMut, Cow},
|
||||
mem,
|
||||
ops::Range,
|
||||
rc::Rc,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use anyhow::Result;
|
||||
use collections::FxHashMap;
|
||||
use collections::{FxHashMap, FxHashSet};
|
||||
use derive_more::{Deref, DerefMut};
|
||||
#[cfg(target_os = "macos")]
|
||||
use media::core_video::CVImageBuffer;
|
||||
@@ -35,135 +34,132 @@ use crate::{
|
||||
EntityId, FocusHandle, FocusId, FontId, GlobalElementId, GlyphId, Hsla, ImageData,
|
||||
InputHandler, IsZero, KeyContext, KeyEvent, LayoutId, MonochromeSprite, MouseEvent, PaintQuad,
|
||||
Path, Pixels, PlatformInputHandler, Point, PolychromeSprite, Quad, RenderGlyphParams,
|
||||
RenderImageParams, RenderSvgParams, Scene, SceneIndex, Shadow, SharedString, Size,
|
||||
StackingContext, StackingOrder, StrikethroughStyle, Style, TextStyleRefinement, Underline,
|
||||
UnderlineStyle, Window, WindowContext, SUBPIXEL_VARIANTS,
|
||||
RenderImageParams, RenderSvgParams, Scene, Shadow, SharedString, Size, StackingContext,
|
||||
StackingOrder, StrikethroughStyle, Style, TextStyleRefinement, Underline, UnderlineStyle,
|
||||
Window, WindowContext, SUBPIXEL_VARIANTS,
|
||||
};
|
||||
|
||||
type AnyMouseListener = Box<dyn FnMut(&dyn Any, DispatchPhase, &mut ElementContext) + 'static>;
|
||||
|
||||
pub(crate) struct RequestedInputHandler {
|
||||
pub(crate) view_id: EntityId,
|
||||
pub(crate) handler: Option<PlatformInputHandler>,
|
||||
}
|
||||
|
||||
pub(crate) struct TooltipRequest {
|
||||
pub(crate) view_id: EntityId,
|
||||
pub(crate) tooltip: AnyTooltip,
|
||||
}
|
||||
|
||||
/// Identifies a moment in time during construction of a frame. Used for reusing cached subsets of a previous frame.
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct FrameIndex {
|
||||
mouse_listeners: usize,
|
||||
dispatch_nodes: usize,
|
||||
scene_index: SceneIndex,
|
||||
pub(crate) struct Frame {
|
||||
pub(crate) focus: Option<FocusId>,
|
||||
pub(crate) window_active: bool,
|
||||
pub(crate) element_states: FxHashMap<GlobalElementId, ElementStateBox>,
|
||||
pub(crate) mouse_listeners: FxHashMap<TypeId, Vec<(StackingOrder, EntityId, AnyMouseListener)>>,
|
||||
pub(crate) dispatch_tree: DispatchTree,
|
||||
pub(crate) scene: Scene,
|
||||
pub(crate) depth_map: Vec<(StackingOrder, EntityId, Bounds<Pixels>)>,
|
||||
pub(crate) z_index_stack: StackingOrder,
|
||||
pub(crate) next_stacking_order_ids: Vec<u16>,
|
||||
pub(crate) next_root_z_index: u16,
|
||||
pub(crate) content_mask_stack: Vec<ContentMask<Pixels>>,
|
||||
pub(crate) element_offset_stack: Vec<Point<Pixels>>,
|
||||
pub(crate) requested_input_handler: Option<RequestedInputHandler>,
|
||||
pub(crate) tooltip_request: Option<TooltipRequest>,
|
||||
pub(crate) cursor_styles: FxHashMap<EntityId, CursorStyle>,
|
||||
pub(crate) requested_cursor_style: Option<CursorStyle>,
|
||||
pub(crate) view_stack: Vec<EntityId>,
|
||||
pub(crate) reused_views: FxHashSet<EntityId>,
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub(crate) debug_bounds: FxHashMap<String, Bounds<Pixels>>,
|
||||
}
|
||||
|
||||
// pub(crate) struct Frame {
|
||||
// pub(crate) focus: Option<FocusId>,
|
||||
// pub(crate) window_active: bool,
|
||||
// pub(crate) element_states: FxHashMap<GlobalElementId, ElementStateBox>,
|
||||
// pub(crate) mouse_listeners: FxHashMap<TypeId, Vec<(StackingOrder, EntityId, AnyMouseListener)>>,
|
||||
// pub(crate) input_handlers: Vec<PlatformInputHandler>,
|
||||
// pub(crate) dispatch_tree: DispatchTree,
|
||||
// pub(crate) scene: Scene,
|
||||
// pub(crate) depth_map: Vec<(StackingOrder, EntityId, Bounds<Pixels>)>,
|
||||
// pub(crate) z_index_stack: StackingOrder,
|
||||
// pub(crate) next_stacking_order_ids: Vec<u16>,
|
||||
// pub(crate) next_root_z_index: u16,
|
||||
// pub(crate) content_mask_stack: Vec<ContentMask<Pixels>>,
|
||||
// pub(crate) element_offset_stack: Vec<Point<Pixels>>,
|
||||
// pub(crate) tooltip_request: Option<TooltipRequest>,
|
||||
// pub(crate) cursor_styles: FxHashMap<EntityId, CursorStyle>,
|
||||
// pub(crate) requested_cursor_style: Option<CursorStyle>,
|
||||
// pub(crate) view_stack: Vec<EntityId>,
|
||||
impl Frame {
|
||||
pub(crate) fn new(dispatch_tree: DispatchTree) -> Self {
|
||||
Frame {
|
||||
focus: None,
|
||||
window_active: false,
|
||||
element_states: FxHashMap::default(),
|
||||
mouse_listeners: FxHashMap::default(),
|
||||
dispatch_tree,
|
||||
scene: Scene::default(),
|
||||
depth_map: Vec::new(),
|
||||
z_index_stack: StackingOrder::default(),
|
||||
next_stacking_order_ids: vec![0],
|
||||
next_root_z_index: 0,
|
||||
content_mask_stack: Vec::new(),
|
||||
element_offset_stack: Vec::new(),
|
||||
requested_input_handler: None,
|
||||
tooltip_request: None,
|
||||
cursor_styles: FxHashMap::default(),
|
||||
requested_cursor_style: None,
|
||||
view_stack: Vec::new(),
|
||||
reused_views: FxHashSet::default(),
|
||||
|
||||
// #[cfg(any(test, feature = "test-support"))]
|
||||
// pub(crate) debug_bounds: FxHashMap<String, Bounds<Pixels>>,
|
||||
// }
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
debug_bounds: FxHashMap::default(),
|
||||
}
|
||||
}
|
||||
|
||||
// impl Frame {
|
||||
// pub(crate) fn new(dispatch_tree: DispatchTree) -> Self {
|
||||
// Frame {
|
||||
// focus: None,
|
||||
// window_active: false,
|
||||
// element_states: FxHashMap::default(),
|
||||
// mouse_listeners: FxHashMap::default(),
|
||||
// input_handlers: Vec::new(),
|
||||
// dispatch_tree,
|
||||
// scene: Scene::default(),
|
||||
// depth_map: Vec::new(),
|
||||
// z_index_stack: StackingOrder::default(),
|
||||
// next_stacking_order_ids: vec![0],
|
||||
// next_root_z_index: 0,
|
||||
// content_mask_stack: Vec::new(),
|
||||
// element_offset_stack: Vec::new(),
|
||||
// tooltip_request: None,
|
||||
// cursor_styles: FxHashMap::default(),
|
||||
// requested_cursor_style: None,
|
||||
// view_stack: Vec::new(),
|
||||
pub(crate) fn clear(&mut self) {
|
||||
self.element_states.clear();
|
||||
self.mouse_listeners.values_mut().for_each(Vec::clear);
|
||||
self.dispatch_tree.clear();
|
||||
self.depth_map.clear();
|
||||
self.next_stacking_order_ids = vec![0];
|
||||
self.next_root_z_index = 0;
|
||||
self.reused_views.clear();
|
||||
self.scene.clear();
|
||||
self.requested_input_handler.take();
|
||||
self.tooltip_request.take();
|
||||
self.cursor_styles.clear();
|
||||
self.requested_cursor_style.take();
|
||||
debug_assert_eq!(self.view_stack.len(), 0);
|
||||
}
|
||||
|
||||
// #[cfg(any(test, feature = "test-support"))]
|
||||
// debug_bounds: FxHashMap::default(),
|
||||
// }
|
||||
// }
|
||||
pub(crate) fn focus_path(&self) -> SmallVec<[FocusId; 8]> {
|
||||
self.focus
|
||||
.map(|focus_id| self.dispatch_tree.focus_path(focus_id))
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
// pub(crate) fn clear(&mut self) {
|
||||
// self.element_states.clear();
|
||||
// self.mouse_listeners.values_mut().for_each(Vec::clear);
|
||||
// self.input_handlers.clear();
|
||||
// self.dispatch_tree.clear();
|
||||
// self.depth_map.clear();
|
||||
// self.next_stacking_order_ids = vec![0];
|
||||
// self.next_root_z_index = 0;
|
||||
// self.scene.clear();
|
||||
// self.tooltip_request.take();
|
||||
// self.cursor_styles.clear();
|
||||
// self.requested_cursor_style.take();
|
||||
// debug_assert_eq!(self.view_stack.len(), 0);
|
||||
// }
|
||||
pub(crate) fn finish(&mut self, prev_frame: &mut Self) {
|
||||
// Reuse mouse listeners that didn't change since the last frame.
|
||||
for (type_id, listeners) in &mut prev_frame.mouse_listeners {
|
||||
let next_listeners = self.mouse_listeners.entry(*type_id).or_default();
|
||||
for (order, view_id, listener) in listeners.drain(..) {
|
||||
if self.reused_views.contains(&view_id) {
|
||||
next_listeners.push((order, view_id, listener));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// pub(crate) fn focus_path(&self) -> SmallVec<[FocusId; 8]> {
|
||||
// self.focus
|
||||
// .map(|focus_id| self.dispatch_tree.focus_path(focus_id))
|
||||
// .unwrap_or_default()
|
||||
// }
|
||||
// Reuse entries in the depth map that didn't change since the last frame.
|
||||
for (order, view_id, bounds) in prev_frame.depth_map.drain(..) {
|
||||
if self.reused_views.contains(&view_id) {
|
||||
match self
|
||||
.depth_map
|
||||
.binary_search_by(|(level, _, _)| order.cmp(level))
|
||||
{
|
||||
Ok(i) | Err(i) => self.depth_map.insert(i, (order, view_id, bounds)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// pub(crate) fn current_index(&self) -> FrameIndex {
|
||||
// FrameIndex {
|
||||
// scene_index: self.scene.current_index(),
|
||||
// mouse_listeners: self.mouse_listeners.len(),
|
||||
// dispatch_nodes: self.dispatch_tree.len(),
|
||||
// }
|
||||
// }
|
||||
// Retain element states for views that didn't change since the last frame.
|
||||
for (element_id, state) in prev_frame.element_states.drain() {
|
||||
if self.reused_views.contains(&state.parent_view_id) {
|
||||
self.element_states.entry(element_id).or_insert(state);
|
||||
}
|
||||
}
|
||||
|
||||
// pub(crate) fn finish(&mut self, prev_frame: &mut Self) {
|
||||
// // Reuse mouse listeners that didn't change since the last frame.
|
||||
// for (type_id, listeners) in &mut prev_frame.mouse_listeners {
|
||||
// let next_listeners = self.mouse_listeners.entry(*type_id).or_default();
|
||||
// for (order, view_id, listener) in listeners.drain(..) {
|
||||
// if self.reused_views.contains(&view_id) {
|
||||
// next_listeners.push((order, view_id, listener));
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// // Reuse entries in the depth map that didn't change since the last frame.
|
||||
// for (order, view_id, bounds) in prev_frame.depth_map.drain(..) {
|
||||
// if self.reused_views.contains(&view_id) {
|
||||
// match self
|
||||
// .depth_map
|
||||
// .binary_search_by(|(level, _, _)| order.cmp(level))
|
||||
// {
|
||||
// Ok(i) | Err(i) => self.depth_map.insert(i, (order, view_id, bounds)),
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// // Retain element states for views that didn't change since the last frame.
|
||||
// for (element_id, state) in prev_frame.element_states.drain() {
|
||||
// if self.reused_views.contains(&state.parent_view_id) {
|
||||
// self.element_states.entry(element_id).or_insert(state);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// Reuse geometry that didn't change since the last frame.
|
||||
self.scene
|
||||
.reuse_views(&self.reused_views, &mut prev_frame.scene);
|
||||
self.scene.finish();
|
||||
}
|
||||
}
|
||||
|
||||
/// This context is used for assisting in the implementation of the element trait
|
||||
#[derive(Deref, DerefMut)]
|
||||
@@ -314,9 +310,8 @@ impl<'a> VisualContext for ElementContext<'a> {
|
||||
}
|
||||
|
||||
impl<'a> ElementContext<'a> {
|
||||
pub(crate) fn reuse_view(&mut self, subframe_range: Range<FrameIndex>) {
|
||||
pub(crate) fn reuse_view(&mut self, next_stacking_order_id: u16) {
|
||||
let view_id = self.parent_view_id();
|
||||
|
||||
let grafted_view_ids = self
|
||||
.cx
|
||||
.window
|
||||
@@ -656,7 +651,6 @@ impl<'a> ElementContext<'a> {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Paint one or more drop shadows into the scene for the next frame at the current z-index.
|
||||
pub fn paint_shadows(
|
||||
&mut self,
|
||||
@@ -672,18 +666,20 @@ impl<'a> ElementContext<'a> {
|
||||
let mut shadow_bounds = bounds;
|
||||
shadow_bounds.origin += shadow.offset;
|
||||
shadow_bounds.dilate(shadow.spread_radius);
|
||||
window
|
||||
.next_frame
|
||||
.scene
|
||||
.paint_primitive(|draw_order| Shadow {
|
||||
draw_order,
|
||||
window.next_frame.scene.insert(
|
||||
&window.next_frame.z_index_stack,
|
||||
Shadow {
|
||||
view_id: view_id.into(),
|
||||
layer_id: 0,
|
||||
order: 0,
|
||||
bounds: shadow_bounds.scale(scale_factor),
|
||||
content_mask: content_mask.scale(scale_factor),
|
||||
corner_radii: corner_radii.scale(scale_factor),
|
||||
color: shadow.color,
|
||||
blur_radius: shadow.blur_radius.scale(scale_factor),
|
||||
pad: 0,
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -696,15 +692,20 @@ impl<'a> ElementContext<'a> {
|
||||
let view_id = self.parent_view_id();
|
||||
|
||||
let window = &mut *self.window;
|
||||
window.next_frame.scene.paint_primitive(|draw_order| Quad {
|
||||
draw_order,
|
||||
bounds: quad.bounds.scale(scale_factor),
|
||||
content_mask: content_mask.scale(scale_factor),
|
||||
background: quad.background,
|
||||
border_color: quad.border_color,
|
||||
corner_radii: quad.corner_radii.scale(scale_factor),
|
||||
border_widths: quad.border_widths.scale(scale_factor),
|
||||
});
|
||||
window.next_frame.scene.insert(
|
||||
&window.next_frame.z_index_stack,
|
||||
Quad {
|
||||
view_id: view_id.into(),
|
||||
layer_id: 0,
|
||||
order: 0,
|
||||
bounds: quad.bounds.scale(scale_factor),
|
||||
content_mask: content_mask.scale(scale_factor),
|
||||
background: quad.background,
|
||||
border_color: quad.border_color,
|
||||
corner_radii: quad.corner_radii.scale(scale_factor),
|
||||
border_widths: quad.border_widths.scale(scale_factor),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Paint the given `Path` into the scene for the next frame at the current z-index.
|
||||
@@ -715,11 +716,12 @@ impl<'a> ElementContext<'a> {
|
||||
|
||||
path.content_mask = content_mask;
|
||||
path.color = color.into();
|
||||
path.view_id = view_id.into();
|
||||
let window = &mut *self.window;
|
||||
window.next_frame.scene.paint_primitive(|draw_order| {
|
||||
path.draw_order = draw_order;
|
||||
path.scale(scale_factor)
|
||||
});
|
||||
window
|
||||
.next_frame
|
||||
.scene
|
||||
.insert(&window.next_frame.z_index_stack, path.scale(scale_factor));
|
||||
}
|
||||
|
||||
/// Paint an underline into the scene for the next frame at the current z-index.
|
||||
@@ -743,17 +745,19 @@ impl<'a> ElementContext<'a> {
|
||||
let view_id = self.parent_view_id();
|
||||
|
||||
let window = &mut *self.window;
|
||||
window
|
||||
.next_frame
|
||||
.scene
|
||||
.paint_primitive(|draw_order| Underline {
|
||||
draw_order,
|
||||
window.next_frame.scene.insert(
|
||||
&window.next_frame.z_index_stack,
|
||||
Underline {
|
||||
view_id: view_id.into(),
|
||||
layer_id: 0,
|
||||
order: 0,
|
||||
bounds: bounds.scale(scale_factor),
|
||||
content_mask: content_mask.scale(scale_factor),
|
||||
color: style.color.unwrap_or_default(),
|
||||
thickness: style.thickness.scale(scale_factor),
|
||||
wavy: style.wavy,
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Paint a strikethrough into the scene for the next frame at the current z-index.
|
||||
@@ -773,17 +777,19 @@ impl<'a> ElementContext<'a> {
|
||||
let view_id = self.parent_view_id();
|
||||
|
||||
let window = &mut *self.window;
|
||||
window
|
||||
.next_frame
|
||||
.scene
|
||||
.paint_primitive(|draw_order| Underline {
|
||||
draw_order,
|
||||
window.next_frame.scene.insert(
|
||||
&window.next_frame.z_index_stack,
|
||||
Underline {
|
||||
view_id: view_id.into(),
|
||||
layer_id: 0,
|
||||
order: 0,
|
||||
bounds: bounds.scale(scale_factor),
|
||||
content_mask: content_mask.scale(scale_factor),
|
||||
thickness: style.thickness.scale(scale_factor),
|
||||
color: style.color.unwrap_or_default(),
|
||||
wavy: false,
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Paints a monochrome (non-emoji) glyph into the scene for the next frame at the current z-index.
|
||||
@@ -831,16 +837,18 @@ impl<'a> ElementContext<'a> {
|
||||
let content_mask = self.content_mask().scale(scale_factor);
|
||||
let view_id = self.parent_view_id();
|
||||
let window = &mut *self.window;
|
||||
window
|
||||
.next_frame
|
||||
.scene
|
||||
.paint_primitive(|draw_order| MonochromeSprite {
|
||||
draw_order,
|
||||
window.next_frame.scene.insert(
|
||||
&window.next_frame.z_index_stack,
|
||||
MonochromeSprite {
|
||||
view_id: view_id.into(),
|
||||
layer_id: 0,
|
||||
order: 0,
|
||||
bounds,
|
||||
content_mask,
|
||||
color,
|
||||
tile,
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -887,18 +895,20 @@ impl<'a> ElementContext<'a> {
|
||||
let view_id = self.parent_view_id();
|
||||
let window = &mut *self.window;
|
||||
|
||||
window
|
||||
.next_frame
|
||||
.scene
|
||||
.paint_primitive(|draw_order| PolychromeSprite {
|
||||
draw_order,
|
||||
window.next_frame.scene.insert(
|
||||
&window.next_frame.z_index_stack,
|
||||
PolychromeSprite {
|
||||
view_id: view_id.into(),
|
||||
layer_id: 0,
|
||||
order: 0,
|
||||
bounds,
|
||||
corner_radii: Default::default(),
|
||||
content_mask,
|
||||
tile,
|
||||
grayscale: false,
|
||||
pad: 0,
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -931,16 +941,18 @@ impl<'a> ElementContext<'a> {
|
||||
let view_id = self.parent_view_id();
|
||||
|
||||
let window = &mut *self.window;
|
||||
window
|
||||
.next_frame
|
||||
.scene
|
||||
.paint_primitive(|draw_order| MonochromeSprite {
|
||||
draw_order,
|
||||
window.next_frame.scene.insert(
|
||||
&window.next_frame.z_index_stack,
|
||||
MonochromeSprite {
|
||||
view_id: view_id.into(),
|
||||
layer_id: 0,
|
||||
order: 0,
|
||||
bounds,
|
||||
content_mask,
|
||||
color,
|
||||
tile,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -968,18 +980,20 @@ impl<'a> ElementContext<'a> {
|
||||
let view_id = self.parent_view_id();
|
||||
|
||||
let window = &mut *self.window;
|
||||
window
|
||||
.next_frame
|
||||
.scene
|
||||
.paint_primitive(|draw_order| PolychromeSprite {
|
||||
draw_order,
|
||||
window.next_frame.scene.insert(
|
||||
&window.next_frame.z_index_stack,
|
||||
PolychromeSprite {
|
||||
view_id: view_id.into(),
|
||||
layer_id: 0,
|
||||
order: 0,
|
||||
bounds,
|
||||
content_mask,
|
||||
corner_radii,
|
||||
tile,
|
||||
grayscale,
|
||||
pad: 0,
|
||||
});
|
||||
},
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -991,15 +1005,17 @@ impl<'a> ElementContext<'a> {
|
||||
let content_mask = self.content_mask().scale(scale_factor);
|
||||
let view_id = self.parent_view_id();
|
||||
let window = &mut *self.window;
|
||||
window
|
||||
.next_frame
|
||||
.scene
|
||||
.paint_primitive(|draw_order| crate::Surface {
|
||||
draw_order,
|
||||
window.next_frame.scene.insert(
|
||||
&window.next_frame.z_index_stack,
|
||||
crate::Surface {
|
||||
view_id: view_id.into(),
|
||||
layer_id: 0,
|
||||
order: 0,
|
||||
bounds,
|
||||
content_mask,
|
||||
image_buffer,
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
|
||||
@@ -38,6 +38,7 @@ use serde_json::Value;
|
||||
use std::{
|
||||
any::Any,
|
||||
cell::RefCell,
|
||||
ffi::OsString,
|
||||
fmt::Debug,
|
||||
hash::Hash,
|
||||
mem,
|
||||
@@ -140,6 +141,14 @@ impl CachedLspAdapter {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn check_if_user_installed(
|
||||
&self,
|
||||
delegate: &Arc<dyn LspAdapterDelegate>,
|
||||
cx: &mut AsyncAppContext,
|
||||
) -> Option<Task<Option<LanguageServerBinary>>> {
|
||||
self.adapter.check_if_user_installed(delegate, cx)
|
||||
}
|
||||
|
||||
pub async fn fetch_latest_server_version(
|
||||
&self,
|
||||
delegate: &dyn LspAdapterDelegate,
|
||||
@@ -240,6 +249,11 @@ impl CachedLspAdapter {
|
||||
pub trait LspAdapterDelegate: Send + Sync {
|
||||
fn show_notification(&self, message: &str, cx: &mut AppContext);
|
||||
fn http_client(&self) -> Arc<dyn HttpClient>;
|
||||
fn which_command(
|
||||
&self,
|
||||
command: OsString,
|
||||
cx: &AppContext,
|
||||
) -> Task<Option<(PathBuf, HashMap<String, String>)>>;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
@@ -248,6 +262,14 @@ pub trait LspAdapter: 'static + Send + Sync {
|
||||
|
||||
fn short_name(&self) -> &'static str;
|
||||
|
||||
fn check_if_user_installed(
|
||||
&self,
|
||||
_: &Arc<dyn LspAdapterDelegate>,
|
||||
_: &mut AsyncAppContext,
|
||||
) -> Option<Task<Option<LanguageServerBinary>>> {
|
||||
None
|
||||
}
|
||||
|
||||
async fn fetch_latest_server_version(
|
||||
&self,
|
||||
delegate: &dyn LspAdapterDelegate,
|
||||
|
||||
@@ -558,34 +558,41 @@ impl LanguageRegistry {
|
||||
let task = {
|
||||
let container_dir = container_dir.clone();
|
||||
cx.spawn(move |mut cx| async move {
|
||||
login_shell_env_loaded.await;
|
||||
// First we check whether the adapter can give us a user-installed binary.
|
||||
// If so, we do *not* want to cache that, because each worktree might give us a different
|
||||
// binary:
|
||||
//
|
||||
// worktree 1: user-installed at `.bin/gopls`
|
||||
// worktree 2: user-installed at `~/bin/gopls`
|
||||
// worktree 3: no gopls found in PATH -> fallback to Zed installation
|
||||
//
|
||||
// We only want to cache when we fall back to the global one,
|
||||
// because we don't want to download and overwrite our global one
|
||||
// for each worktree we might have open.
|
||||
|
||||
let entry = this
|
||||
.lsp_binary_paths
|
||||
.lock()
|
||||
.entry(adapter.name.clone())
|
||||
.or_insert_with(|| {
|
||||
let adapter = adapter.clone();
|
||||
let language = language.clone();
|
||||
let delegate = delegate.clone();
|
||||
cx.spawn(|cx| {
|
||||
get_binary(
|
||||
adapter,
|
||||
language,
|
||||
delegate,
|
||||
container_dir,
|
||||
lsp_binary_statuses,
|
||||
cx,
|
||||
)
|
||||
.map_err(Arc::new)
|
||||
})
|
||||
.shared()
|
||||
})
|
||||
.clone();
|
||||
let user_binary_task = check_user_installed_binary(
|
||||
adapter.clone(),
|
||||
language.clone(),
|
||||
delegate.clone(),
|
||||
&mut cx,
|
||||
);
|
||||
let binary = if let Some(user_binary) = user_binary_task.await {
|
||||
user_binary
|
||||
} else {
|
||||
// If we want to install a binary globally, we need to wait for
|
||||
// the login shell to be set on our process.
|
||||
login_shell_env_loaded.await;
|
||||
|
||||
let binary = match entry.await {
|
||||
Ok(binary) => binary,
|
||||
Err(err) => anyhow::bail!("{err}"),
|
||||
get_or_install_binary(
|
||||
this,
|
||||
&adapter,
|
||||
language,
|
||||
&delegate,
|
||||
&cx,
|
||||
container_dir,
|
||||
lsp_binary_statuses,
|
||||
)
|
||||
.await?
|
||||
};
|
||||
|
||||
if let Some(task) = adapter.will_start_server(&delegate, &mut cx) {
|
||||
@@ -724,6 +731,62 @@ impl LspBinaryStatusSender {
|
||||
}
|
||||
}
|
||||
|
||||
async fn check_user_installed_binary(
|
||||
adapter: Arc<CachedLspAdapter>,
|
||||
language: Arc<Language>,
|
||||
delegate: Arc<dyn LspAdapterDelegate>,
|
||||
cx: &mut AsyncAppContext,
|
||||
) -> Option<LanguageServerBinary> {
|
||||
let Some(task) = adapter.check_if_user_installed(&delegate, cx) else {
|
||||
return None;
|
||||
};
|
||||
|
||||
task.await.and_then(|binary| {
|
||||
log::info!(
|
||||
"found user-installed language server for {}. path: {:?}, arguments: {:?}",
|
||||
language.name(),
|
||||
binary.path,
|
||||
binary.arguments
|
||||
);
|
||||
Some(binary)
|
||||
})
|
||||
}
|
||||
|
||||
async fn get_or_install_binary(
|
||||
registry: Arc<LanguageRegistry>,
|
||||
adapter: &Arc<CachedLspAdapter>,
|
||||
language: Arc<Language>,
|
||||
delegate: &Arc<dyn LspAdapterDelegate>,
|
||||
cx: &AsyncAppContext,
|
||||
container_dir: Arc<Path>,
|
||||
lsp_binary_statuses: LspBinaryStatusSender,
|
||||
) -> Result<LanguageServerBinary> {
|
||||
let entry = registry
|
||||
.lsp_binary_paths
|
||||
.lock()
|
||||
.entry(adapter.name.clone())
|
||||
.or_insert_with(|| {
|
||||
let adapter = adapter.clone();
|
||||
let language = language.clone();
|
||||
let delegate = delegate.clone();
|
||||
cx.spawn(|cx| {
|
||||
get_binary(
|
||||
adapter,
|
||||
language,
|
||||
delegate,
|
||||
container_dir,
|
||||
lsp_binary_statuses,
|
||||
cx,
|
||||
)
|
||||
.map_err(Arc::new)
|
||||
})
|
||||
.shared()
|
||||
})
|
||||
.clone();
|
||||
|
||||
entry.await.map_err(|err| anyhow!("{:?}", err))
|
||||
}
|
||||
|
||||
async fn get_binary(
|
||||
adapter: Arc<CachedLspAdapter>,
|
||||
language: Arc<Language>,
|
||||
@@ -757,15 +820,20 @@ async fn get_binary(
|
||||
.await
|
||||
{
|
||||
statuses.send(language.clone(), LanguageServerBinaryStatus::Cached);
|
||||
return Ok(binary);
|
||||
} else {
|
||||
statuses.send(
|
||||
language.clone(),
|
||||
LanguageServerBinaryStatus::Failed {
|
||||
error: format!("{:?}", error),
|
||||
},
|
||||
log::info!(
|
||||
"failed to fetch newest version of language server {:?}. falling back to using {:?}",
|
||||
adapter.name,
|
||||
binary.path.display()
|
||||
);
|
||||
return Ok(binary);
|
||||
}
|
||||
|
||||
statuses.send(
|
||||
language.clone(),
|
||||
LanguageServerBinaryStatus::Failed {
|
||||
error: format!("{:?}", error),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
binary
|
||||
@@ -779,14 +847,23 @@ async fn fetch_latest_binary(
|
||||
lsp_binary_statuses_tx: LspBinaryStatusSender,
|
||||
) -> Result<LanguageServerBinary> {
|
||||
let container_dir: Arc<Path> = container_dir.into();
|
||||
|
||||
lsp_binary_statuses_tx.send(
|
||||
language.clone(),
|
||||
LanguageServerBinaryStatus::CheckingForUpdate,
|
||||
);
|
||||
|
||||
log::info!(
|
||||
"querying GitHub for latest version of language server {:?}",
|
||||
adapter.name.0
|
||||
);
|
||||
let version_info = adapter.fetch_latest_server_version(delegate).await?;
|
||||
lsp_binary_statuses_tx.send(language.clone(), LanguageServerBinaryStatus::Downloading);
|
||||
|
||||
log::info!(
|
||||
"checking if Zed already installed or fetching version for language server {:?}",
|
||||
adapter.name.0
|
||||
);
|
||||
let binary = adapter
|
||||
.fetch_server_binary(version_info, container_dir.to_path_buf(), delegate)
|
||||
.await?;
|
||||
|
||||
@@ -55,6 +55,7 @@ pub enum IoKind {
|
||||
pub struct LanguageServerBinary {
|
||||
pub path: PathBuf,
|
||||
pub arguments: Vec<OsString>,
|
||||
pub env: Option<HashMap<String, String>>,
|
||||
}
|
||||
|
||||
/// A running language server process.
|
||||
@@ -189,6 +190,7 @@ impl LanguageServer {
|
||||
let mut server = process::Command::new(&binary.path)
|
||||
.current_dir(working_dir)
|
||||
.args(binary.arguments)
|
||||
.envs(binary.env.unwrap_or_default())
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
@@ -1136,6 +1138,7 @@ impl LanguageServer {
|
||||
document_formatting_provider: Some(OneOf::Left(true)),
|
||||
document_range_formatting_provider: Some(OneOf::Left(true)),
|
||||
definition_provider: Some(OneOf::Left(true)),
|
||||
implementation_provider: Some(ImplementationProviderCapability::Simple(true)),
|
||||
type_definition_provider: Some(TypeDefinitionProviderCapability::Simple(true)),
|
||||
..Default::default()
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ use gpui::{
|
||||
IntoElement, ListState, ParentElement, Render, Styled, View, ViewContext, WeakView,
|
||||
};
|
||||
use ui::prelude::*;
|
||||
use workspace::item::Item;
|
||||
use workspace::item::{Item, ItemHandle};
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::{
|
||||
@@ -22,6 +22,7 @@ pub struct MarkdownPreviewView {
|
||||
contents: ParsedMarkdown,
|
||||
selected_block: usize,
|
||||
list_state: ListState,
|
||||
tab_description: String,
|
||||
}
|
||||
|
||||
impl MarkdownPreviewView {
|
||||
@@ -34,8 +35,9 @@ impl MarkdownPreviewView {
|
||||
|
||||
if let Some(editor) = workspace.active_item_as::<Editor>(cx) {
|
||||
let workspace_handle = workspace.weak_handle();
|
||||
let tab_description = editor.tab_description(0, cx);
|
||||
let view: View<MarkdownPreviewView> =
|
||||
MarkdownPreviewView::new(editor, workspace_handle, cx);
|
||||
MarkdownPreviewView::new(editor, workspace_handle, tab_description, cx);
|
||||
workspace.split_item(workspace::SplitDirection::Right, Box::new(view.clone()), cx);
|
||||
cx.notify();
|
||||
}
|
||||
@@ -45,6 +47,7 @@ impl MarkdownPreviewView {
|
||||
pub fn new(
|
||||
active_editor: View<Editor>,
|
||||
workspace: WeakView<Workspace>,
|
||||
tab_description: Option<SharedString>,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) -> View<Self> {
|
||||
cx.new_view(|cx: &mut ViewContext<Self>| {
|
||||
@@ -119,12 +122,17 @@ impl MarkdownPreviewView {
|
||||
},
|
||||
);
|
||||
|
||||
let tab_description = tab_description
|
||||
.map(|tab_description| format!("Preview {}", tab_description))
|
||||
.unwrap_or("Markdown preview".to_string());
|
||||
|
||||
Self {
|
||||
selected_block: 0,
|
||||
focus_handle: cx.focus_handle(),
|
||||
workspace,
|
||||
contents,
|
||||
list_state,
|
||||
tab_description: tab_description.into(),
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -188,11 +196,13 @@ impl Item for MarkdownPreviewView {
|
||||
} else {
|
||||
Color::Muted
|
||||
}))
|
||||
.child(Label::new("Markdown preview").color(if selected {
|
||||
Color::Default
|
||||
} else {
|
||||
Color::Muted
|
||||
}))
|
||||
.child(
|
||||
Label::new(self.tab_description.to_string()).color(if selected {
|
||||
Color::Default
|
||||
} else {
|
||||
Color::Muted
|
||||
}),
|
||||
)
|
||||
.into_any()
|
||||
}
|
||||
|
||||
|
||||
@@ -192,6 +192,7 @@ impl Prettier {
|
||||
LanguageServerBinary {
|
||||
path: node_path,
|
||||
arguments: vec![prettier_server.into(), prettier_dir.as_path().into()],
|
||||
env: None,
|
||||
},
|
||||
Path::new("/"),
|
||||
None,
|
||||
|
||||
@@ -65,6 +65,7 @@ text.workspace = true
|
||||
thiserror.workspace = true
|
||||
toml.workspace = true
|
||||
util.workspace = true
|
||||
which.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
client = { workspace = true, features = ["test-support"] }
|
||||
|
||||
@@ -105,6 +105,10 @@ pub(crate) struct GetTypeDefinition {
|
||||
pub position: PointUtf16,
|
||||
}
|
||||
|
||||
pub(crate) struct GetImplementation {
|
||||
pub position: PointUtf16,
|
||||
}
|
||||
|
||||
pub(crate) struct GetReferences {
|
||||
pub position: PointUtf16,
|
||||
}
|
||||
@@ -492,6 +496,99 @@ impl LspCommand for GetDefinition {
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
impl LspCommand for GetImplementation {
|
||||
type Response = Vec<LocationLink>;
|
||||
type LspRequest = lsp::request::GotoImplementation;
|
||||
type ProtoRequest = proto::GetImplementation;
|
||||
|
||||
fn to_lsp(
|
||||
&self,
|
||||
path: &Path,
|
||||
_: &Buffer,
|
||||
_: &Arc<LanguageServer>,
|
||||
_: &AppContext,
|
||||
) -> lsp::GotoImplementationParams {
|
||||
lsp::GotoImplementationParams {
|
||||
text_document_position_params: lsp::TextDocumentPositionParams {
|
||||
text_document: lsp::TextDocumentIdentifier {
|
||||
uri: lsp::Url::from_file_path(path).unwrap(),
|
||||
},
|
||||
position: point_to_lsp(self.position),
|
||||
},
|
||||
work_done_progress_params: Default::default(),
|
||||
partial_result_params: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn response_from_lsp(
|
||||
self,
|
||||
message: Option<lsp::GotoImplementationResponse>,
|
||||
project: Model<Project>,
|
||||
buffer: Model<Buffer>,
|
||||
server_id: LanguageServerId,
|
||||
cx: AsyncAppContext,
|
||||
) -> Result<Vec<LocationLink>> {
|
||||
location_links_from_lsp(message, project, buffer, server_id, cx).await
|
||||
}
|
||||
|
||||
fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::GetImplementation {
|
||||
proto::GetImplementation {
|
||||
project_id,
|
||||
buffer_id: buffer.remote_id().into(),
|
||||
position: Some(language::proto::serialize_anchor(
|
||||
&buffer.anchor_before(self.position),
|
||||
)),
|
||||
version: serialize_version(&buffer.version()),
|
||||
}
|
||||
}
|
||||
|
||||
async fn from_proto(
|
||||
message: proto::GetImplementation,
|
||||
_: Model<Project>,
|
||||
buffer: Model<Buffer>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<Self> {
|
||||
let position = message
|
||||
.position
|
||||
.and_then(deserialize_anchor)
|
||||
.ok_or_else(|| anyhow!("invalid position"))?;
|
||||
buffer
|
||||
.update(&mut cx, |buffer, _| {
|
||||
buffer.wait_for_version(deserialize_version(&message.version))
|
||||
})?
|
||||
.await?;
|
||||
Ok(Self {
|
||||
position: buffer.update(&mut cx, |buffer, _| position.to_point_utf16(buffer))?,
|
||||
})
|
||||
}
|
||||
|
||||
fn response_to_proto(
|
||||
response: Vec<LocationLink>,
|
||||
project: &mut Project,
|
||||
peer_id: PeerId,
|
||||
_: &clock::Global,
|
||||
cx: &mut AppContext,
|
||||
) -> proto::GetImplementationResponse {
|
||||
let links = location_links_to_proto(response, project, peer_id, cx);
|
||||
proto::GetImplementationResponse { links }
|
||||
}
|
||||
|
||||
async fn response_from_proto(
|
||||
self,
|
||||
message: proto::GetImplementationResponse,
|
||||
project: Model<Project>,
|
||||
_: Model<Buffer>,
|
||||
cx: AsyncAppContext,
|
||||
) -> Result<Vec<LocationLink>> {
|
||||
location_links_from_proto(message.links, project, cx).await
|
||||
}
|
||||
|
||||
fn buffer_id_from_proto(message: &proto::GetImplementation) -> Result<BufferId> {
|
||||
BufferId::new(message.buffer_id)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
impl LspCommand for GetTypeDefinition {
|
||||
type Response = Vec<LocationLink>;
|
||||
|
||||
@@ -70,9 +70,14 @@ pub(super) async fn format_with_prettier(
|
||||
match prettier.format(buffer, buffer_path, cx).await {
|
||||
Ok(new_diff) => return Some(FormatOperation::Prettier(new_diff)),
|
||||
Err(e) => {
|
||||
log::error!(
|
||||
"Prettier instance from {prettier_path:?} failed to format a buffer: {e:#}"
|
||||
);
|
||||
match prettier_path {
|
||||
Some(prettier_path) => log::error!(
|
||||
"Prettier instance from path {prettier_path:?} failed to format a buffer: {e:#}"
|
||||
),
|
||||
None => log::error!(
|
||||
"Default prettier instance failed to format a buffer: {e:#}"
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -366,6 +371,7 @@ fn register_new_prettier(
|
||||
}
|
||||
|
||||
async fn install_prettier_packages(
|
||||
fs: &dyn Fs,
|
||||
plugins_to_install: HashSet<&'static str>,
|
||||
node: Arc<dyn NodeRuntime>,
|
||||
) -> anyhow::Result<()> {
|
||||
@@ -385,18 +391,32 @@ async fn install_prettier_packages(
|
||||
.await
|
||||
.context("fetching latest npm versions")?;
|
||||
|
||||
log::info!("Fetching default prettier and plugins: {packages_to_versions:?}");
|
||||
let default_prettier_dir = DEFAULT_PRETTIER_DIR.as_path();
|
||||
match fs.metadata(default_prettier_dir).await.with_context(|| {
|
||||
format!("fetching FS metadata for default prettier dir {default_prettier_dir:?}")
|
||||
})? {
|
||||
Some(prettier_dir_metadata) => anyhow::ensure!(
|
||||
prettier_dir_metadata.is_dir,
|
||||
"default prettier dir {default_prettier_dir:?} is not a directory"
|
||||
),
|
||||
None => fs
|
||||
.create_dir(default_prettier_dir)
|
||||
.await
|
||||
.with_context(|| format!("creating default prettier dir {default_prettier_dir:?}"))?,
|
||||
}
|
||||
|
||||
log::info!("Installing default prettier and plugins: {packages_to_versions:?}");
|
||||
let borrowed_packages = packages_to_versions
|
||||
.iter()
|
||||
.map(|(package, version)| (package.as_str(), version.as_str()))
|
||||
.collect::<Vec<_>>();
|
||||
node.npm_install_packages(DEFAULT_PRETTIER_DIR.as_path(), &borrowed_packages)
|
||||
node.npm_install_packages(default_prettier_dir, &borrowed_packages)
|
||||
.await
|
||||
.context("fetching formatter packages")?;
|
||||
anyhow::Ok(())
|
||||
}
|
||||
|
||||
async fn save_prettier_server_file(fs: &dyn Fs) -> Result<(), anyhow::Error> {
|
||||
async fn save_prettier_server_file(fs: &dyn Fs) -> anyhow::Result<()> {
|
||||
let prettier_wrapper_path = DEFAULT_PRETTIER_DIR.join(prettier::PRETTIER_SERVER_FILE);
|
||||
fs.save(
|
||||
&prettier_wrapper_path,
|
||||
@@ -413,6 +433,17 @@ async fn save_prettier_server_file(fs: &dyn Fs) -> Result<(), anyhow::Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn should_write_prettier_server_file(fs: &dyn Fs) -> bool {
|
||||
let prettier_wrapper_path = DEFAULT_PRETTIER_DIR.join(prettier::PRETTIER_SERVER_FILE);
|
||||
if !fs.is_file(&prettier_wrapper_path).await {
|
||||
return true;
|
||||
}
|
||||
let Ok(prettier_server_file_contents) = fs.load(&prettier_wrapper_path).await else {
|
||||
return true;
|
||||
};
|
||||
prettier_server_file_contents != prettier::PRETTIER_SERVER_JS
|
||||
}
|
||||
|
||||
impl Project {
|
||||
pub fn update_prettier_settings(
|
||||
&self,
|
||||
@@ -623,6 +654,7 @@ impl Project {
|
||||
_cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
// suppress unused code warnings
|
||||
let _ = should_write_prettier_server_file;
|
||||
let _ = install_prettier_packages;
|
||||
let _ = save_prettier_server_file;
|
||||
|
||||
@@ -643,7 +675,6 @@ impl Project {
|
||||
let Some(node) = self.node.as_ref().cloned() else {
|
||||
return;
|
||||
};
|
||||
log::info!("Initializing default prettier with plugins {new_plugins:?}");
|
||||
let fs = Arc::clone(&self.fs);
|
||||
let locate_prettier_installation = match worktree.and_then(|worktree_id| {
|
||||
self.worktree_for_id(worktree_id, cx)
|
||||
@@ -689,6 +720,7 @@ impl Project {
|
||||
}
|
||||
};
|
||||
|
||||
log::info!("Initializing default prettier with plugins {new_plugins:?}");
|
||||
let plugins_to_install = new_plugins.clone();
|
||||
let fs = Arc::clone(&self.fs);
|
||||
let new_installation_task = cx
|
||||
@@ -703,7 +735,7 @@ impl Project {
|
||||
if prettier_path.is_some() {
|
||||
new_plugins.clear();
|
||||
}
|
||||
let mut needs_install = false;
|
||||
let mut needs_install = should_write_prettier_server_file(fs.as_ref()).await;
|
||||
if let Some(previous_installation_task) = previous_installation_task {
|
||||
if let Err(e) = previous_installation_task.await {
|
||||
log::error!("Failed to install default prettier: {e:#}");
|
||||
@@ -744,8 +776,10 @@ impl Project {
|
||||
let installed_plugins = new_plugins.clone();
|
||||
cx.background_executor()
|
||||
.spawn(async move {
|
||||
install_prettier_packages(fs.as_ref(), new_plugins, node).await?;
|
||||
// Save the server file last, so the reinstall need could be determined by the absence of the file.
|
||||
save_prettier_server_file(fs.as_ref()).await?;
|
||||
install_prettier_packages(new_plugins, node).await
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.await
|
||||
.context("prettier & plugins install")
|
||||
|
||||
@@ -71,6 +71,8 @@ use smol::lock::Semaphore;
|
||||
use std::{
|
||||
cmp::{self, Ordering},
|
||||
convert::TryInto,
|
||||
env,
|
||||
ffi::OsString,
|
||||
hash::Hash,
|
||||
mem,
|
||||
num::NonZeroU32,
|
||||
@@ -504,11 +506,6 @@ pub enum FormatTrigger {
|
||||
Manual,
|
||||
}
|
||||
|
||||
struct ProjectLspAdapterDelegate {
|
||||
project: Model<Project>,
|
||||
http_client: Arc<dyn HttpClient>,
|
||||
}
|
||||
|
||||
// Currently, formatting operations are represented differently depending on
|
||||
// whether they come from a language server or an external command.
|
||||
enum FormatOperation {
|
||||
@@ -853,10 +850,13 @@ impl Project {
|
||||
root_paths: impl IntoIterator<Item = &Path>,
|
||||
cx: &mut gpui::TestAppContext,
|
||||
) -> Model<Project> {
|
||||
use clock::FakeSystemClock;
|
||||
|
||||
let mut languages = LanguageRegistry::test();
|
||||
languages.set_executor(cx.executor());
|
||||
let clock = Arc::new(FakeSystemClock::default());
|
||||
let http_client = util::http::FakeHttpClient::with_404_response();
|
||||
let client = cx.update(|cx| client::Client::new(http_client.clone(), cx));
|
||||
let client = cx.update(|cx| client::Client::new(clock, http_client.clone(), cx));
|
||||
let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx));
|
||||
let project = cx.update(|cx| {
|
||||
Project::local(
|
||||
@@ -2800,7 +2800,7 @@ impl Project {
|
||||
|
||||
fn start_language_server(
|
||||
&mut self,
|
||||
worktree: &Model<Worktree>,
|
||||
worktree_handle: &Model<Worktree>,
|
||||
adapter: Arc<CachedLspAdapter>,
|
||||
language: Arc<Language>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
@@ -2809,7 +2809,7 @@ impl Project {
|
||||
return;
|
||||
}
|
||||
|
||||
let worktree = worktree.read(cx);
|
||||
let worktree = worktree_handle.read(cx);
|
||||
let worktree_id = worktree.id();
|
||||
let worktree_path = worktree.abs_path();
|
||||
let key = (worktree_id, adapter.name.clone());
|
||||
@@ -2823,7 +2823,7 @@ impl Project {
|
||||
language.clone(),
|
||||
adapter.clone(),
|
||||
Arc::clone(&worktree_path),
|
||||
ProjectLspAdapterDelegate::new(self, cx),
|
||||
ProjectLspAdapterDelegate::new(self, worktree_handle, cx),
|
||||
cx,
|
||||
) {
|
||||
Some(pending_server) => pending_server,
|
||||
@@ -4646,6 +4646,7 @@ impl Project {
|
||||
cx,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn type_definition<T: ToPointUtf16>(
|
||||
&self,
|
||||
buffer: &Model<Buffer>,
|
||||
@@ -4653,10 +4654,33 @@ impl Project {
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<Vec<LocationLink>>> {
|
||||
let position = position.to_point_utf16(buffer.read(cx));
|
||||
|
||||
self.type_definition_impl(buffer, position, cx)
|
||||
}
|
||||
|
||||
fn implementation_impl(
|
||||
&self,
|
||||
buffer: &Model<Buffer>,
|
||||
position: PointUtf16,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<Vec<LocationLink>>> {
|
||||
self.request_lsp(
|
||||
buffer.clone(),
|
||||
LanguageServerToQuery::Primary,
|
||||
GetImplementation { position },
|
||||
cx,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn implementation<T: ToPointUtf16>(
|
||||
&self,
|
||||
buffer: &Model<Buffer>,
|
||||
position: T,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<Vec<LocationLink>>> {
|
||||
let position = position.to_point_utf16(buffer.read(cx));
|
||||
self.implementation_impl(buffer, position, cx)
|
||||
}
|
||||
|
||||
fn references_impl(
|
||||
&self,
|
||||
buffer: &Model<Buffer>,
|
||||
@@ -9271,10 +9295,17 @@ impl<P: AsRef<Path>> From<(WorktreeId, P)> for ProjectPath {
|
||||
}
|
||||
}
|
||||
|
||||
struct ProjectLspAdapterDelegate {
|
||||
project: Model<Project>,
|
||||
worktree: Model<Worktree>,
|
||||
http_client: Arc<dyn HttpClient>,
|
||||
}
|
||||
|
||||
impl ProjectLspAdapterDelegate {
|
||||
fn new(project: &Project, cx: &ModelContext<Project>) -> Arc<Self> {
|
||||
fn new(project: &Project, worktree: &Model<Worktree>, cx: &ModelContext<Project>) -> Arc<Self> {
|
||||
Arc::new(Self {
|
||||
project: cx.handle(),
|
||||
worktree: worktree.clone(),
|
||||
http_client: project.client.http_client(),
|
||||
})
|
||||
}
|
||||
@@ -9289,6 +9320,41 @@ impl LspAdapterDelegate for ProjectLspAdapterDelegate {
|
||||
fn http_client(&self) -> Arc<dyn HttpClient> {
|
||||
self.http_client.clone()
|
||||
}
|
||||
|
||||
fn which_command(
|
||||
&self,
|
||||
command: OsString,
|
||||
cx: &AppContext,
|
||||
) -> Task<Option<(PathBuf, HashMap<String, String>)>> {
|
||||
let worktree_abs_path = self.worktree.read(cx).abs_path();
|
||||
let command = command.to_owned();
|
||||
|
||||
cx.background_executor().spawn(async move {
|
||||
let shell_env = load_shell_environment(&worktree_abs_path)
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"failed to determine load login shell environment in {worktree_abs_path:?}"
|
||||
)
|
||||
})
|
||||
.log_err();
|
||||
|
||||
if let Some(shell_env) = shell_env {
|
||||
let shell_path = shell_env.get("PATH");
|
||||
match which::which_in(&command, shell_path, &worktree_abs_path) {
|
||||
Ok(command_path) => Some((command_path, shell_env)),
|
||||
Err(error) => {
|
||||
log::warn!(
|
||||
"failed to determine path for command {:?} in env {shell_env:?}: {error}", command.to_string_lossy()
|
||||
);
|
||||
None
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn serialize_symbol(symbol: &Symbol) -> proto::Symbol {
|
||||
@@ -9396,3 +9462,55 @@ fn include_text(server: &lsp::LanguageServer) -> bool {
|
||||
})
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
async fn load_shell_environment(dir: &Path) -> Result<HashMap<String, String>> {
|
||||
let marker = "ZED_SHELL_START";
|
||||
let shell = env::var("SHELL").context(
|
||||
"SHELL environment variable is not assigned so we can't source login environment variables",
|
||||
)?;
|
||||
let output = smol::process::Command::new(&shell)
|
||||
.args([
|
||||
"-i",
|
||||
"-c",
|
||||
// What we're doing here is to spawn a shell and then `cd` into
|
||||
// the project directory to get the env in there as if the user
|
||||
// `cd`'d into it. We do that because tools like direnv, asdf, ...
|
||||
// hook into `cd` and only set up the env after that.
|
||||
//
|
||||
// The `exit 0` is the result of hours of debugging, trying to find out
|
||||
// why running this command here, without `exit 0`, would mess
|
||||
// up signal process for our process so that `ctrl-c` doesn't work
|
||||
// anymore.
|
||||
// We still don't know why `$SHELL -l -i -c '/usr/bin/env -0'` would
|
||||
// do that, but it does, and `exit 0` helps.
|
||||
&format!("cd {dir:?}; echo {marker}; /usr/bin/env -0; exit 0;"),
|
||||
])
|
||||
.output()
|
||||
.await
|
||||
.context("failed to spawn login shell to source login environment variables")?;
|
||||
|
||||
anyhow::ensure!(
|
||||
output.status.success(),
|
||||
"login shell exited with error {:?}",
|
||||
output.status
|
||||
);
|
||||
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
let env_output_start = stdout.find(marker).ok_or_else(|| {
|
||||
anyhow!(
|
||||
"failed to parse output of `env` command in login shell: {}",
|
||||
stdout
|
||||
)
|
||||
})?;
|
||||
|
||||
let mut parsed_env = HashMap::default();
|
||||
let env_output = &stdout[env_output_start + marker.len()..];
|
||||
for line in env_output.split_terminator('\0') {
|
||||
if let Some(separator_index) = line.find('=') {
|
||||
let key = line[..separator_index].to_string();
|
||||
let value = line[separator_index + 1..].to_string();
|
||||
parsed_env.insert(key, value);
|
||||
}
|
||||
}
|
||||
Ok(parsed_env)
|
||||
}
|
||||
|
||||
@@ -2252,11 +2252,16 @@ impl LocalSnapshot {
|
||||
|
||||
fn ignore_stack_for_abs_path(&self, abs_path: &Path, is_dir: bool) -> Arc<IgnoreStack> {
|
||||
let mut new_ignores = Vec::new();
|
||||
for ancestor in abs_path.ancestors().skip(1) {
|
||||
if let Some((ignore, _)) = self.ignores_by_parent_abs_path.get(ancestor) {
|
||||
new_ignores.push((ancestor, Some(ignore.clone())));
|
||||
} else {
|
||||
new_ignores.push((ancestor, None));
|
||||
for (index, ancestor) in abs_path.ancestors().enumerate() {
|
||||
if index > 0 {
|
||||
if let Some((ignore, _)) = self.ignores_by_parent_abs_path.get(ancestor) {
|
||||
new_ignores.push((ancestor, Some(ignore.clone())));
|
||||
} else {
|
||||
new_ignores.push((ancestor, None));
|
||||
}
|
||||
}
|
||||
if ancestor.join(&*DOT_GIT).is_dir() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3319,14 +3324,21 @@ impl BackgroundScanner {
|
||||
|
||||
// Populate ignores above the root.
|
||||
let root_abs_path = self.state.lock().snapshot.abs_path.clone();
|
||||
for ancestor in root_abs_path.ancestors().skip(1) {
|
||||
if let Ok(ignore) = build_gitignore(&ancestor.join(&*GITIGNORE), self.fs.as_ref()).await
|
||||
{
|
||||
self.state
|
||||
.lock()
|
||||
.snapshot
|
||||
.ignores_by_parent_abs_path
|
||||
.insert(ancestor.into(), (ignore.into(), false));
|
||||
for (index, ancestor) in root_abs_path.ancestors().enumerate() {
|
||||
if index != 0 {
|
||||
if let Ok(ignore) =
|
||||
build_gitignore(&ancestor.join(&*GITIGNORE), self.fs.as_ref()).await
|
||||
{
|
||||
self.state
|
||||
.lock()
|
||||
.snapshot
|
||||
.ignores_by_parent_abs_path
|
||||
.insert(ancestor.into(), (ignore.into(), false));
|
||||
}
|
||||
}
|
||||
if ancestor.join(&*DOT_GIT).is_dir() {
|
||||
// Reached root of git repository.
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ use crate::{
|
||||
};
|
||||
use anyhow::Result;
|
||||
use client::Client;
|
||||
use clock::FakeSystemClock;
|
||||
use fs::{repository::GitFileStatus, FakeFs, Fs, RealFs, RemoveOptions};
|
||||
use git::GITIGNORE;
|
||||
use gpui::{ModelContext, Task, TestAppContext};
|
||||
@@ -1263,7 +1264,13 @@ async fn test_create_directory_during_initial_scan(cx: &mut TestAppContext) {
|
||||
async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
cx.executor().allow_parking();
|
||||
let client_fake = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
|
||||
let client_fake = cx.update(|cx| {
|
||||
Client::new(
|
||||
Arc::new(FakeSystemClock::default()),
|
||||
FakeHttpClient::with_404_response(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let fs_fake = FakeFs::new(cx.background_executor.clone());
|
||||
fs_fake
|
||||
@@ -1304,7 +1311,13 @@ async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) {
|
||||
assert!(tree.entry_for_path("a/b/").unwrap().is_dir());
|
||||
});
|
||||
|
||||
let client_real = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
|
||||
let client_real = cx.update(|cx| {
|
||||
Client::new(
|
||||
Arc::new(FakeSystemClock::default()),
|
||||
FakeHttpClient::with_404_response(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let fs_real = Arc::new(RealFs);
|
||||
let temp_root = temp_tree(json!({
|
||||
@@ -2396,8 +2409,9 @@ async fn test_propagate_git_statuses(cx: &mut TestAppContext) {
|
||||
}
|
||||
|
||||
fn build_client(cx: &mut TestAppContext) -> Arc<Client> {
|
||||
let clock = Arc::new(FakeSystemClock::default());
|
||||
let http_client = FakeHttpClient::with_404_response();
|
||||
cx.update(|cx| Client::new(http_client, cx))
|
||||
cx.update(|cx| Client::new(clock, http_client, cx))
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
|
||||
@@ -15,6 +15,7 @@ futures.workspace = true
|
||||
fuzzy.workspace = true
|
||||
gpui.workspace = true
|
||||
language.workspace = true
|
||||
menu.workspace = true
|
||||
ordered-float.workspace = true
|
||||
picker.workspace = true
|
||||
postage.workspace = true
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
gpui::actions!(projects, [OpenRecent]);
|
||||
@@ -1,5 +1,4 @@
|
||||
mod highlighted_workspace_location;
|
||||
mod projects;
|
||||
|
||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||
use gpui::{
|
||||
@@ -14,7 +13,7 @@ use ui::{prelude::*, tooltip_container, HighlightedLabel, ListItem, ListItemSpac
|
||||
use util::paths::PathExt;
|
||||
use workspace::{ModalView, Workspace, WorkspaceId, WorkspaceLocation, WORKSPACE_DB};
|
||||
|
||||
pub use projects::OpenRecent;
|
||||
gpui::actions!(projects, [OpenRecent]);
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
cx.observe_new_views(RecentProjects::register).detach();
|
||||
@@ -94,6 +93,7 @@ impl RecentProjects {
|
||||
Ok(())
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn open_popover(workspace: WeakView<Workspace>, cx: &mut WindowContext<'_>) -> View<Self> {
|
||||
cx.new_view(|cx| Self::new(RecentProjectsDelegate::new(workspace, false), 20., cx))
|
||||
}
|
||||
@@ -147,7 +147,11 @@ impl PickerDelegate for RecentProjectsDelegate {
|
||||
type ListItem = ListItem;
|
||||
|
||||
fn placeholder_text(&self) -> Arc<str> {
|
||||
"Search recent projects...".into()
|
||||
Arc::from(format!(
|
||||
"`{:?}` reuses the window, `{:?}` opens in new",
|
||||
menu::Confirm,
|
||||
menu::SecondaryConfirm,
|
||||
))
|
||||
}
|
||||
|
||||
fn match_count(&self) -> usize {
|
||||
@@ -207,17 +211,26 @@ impl PickerDelegate for RecentProjectsDelegate {
|
||||
Task::ready(())
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<Self>>) {
|
||||
fn confirm(&mut self, secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
|
||||
if let Some((selected_match, workspace)) = self
|
||||
.matches
|
||||
.get(self.selected_index())
|
||||
.zip(self.workspace.upgrade())
|
||||
{
|
||||
let (_, workspace_location) = &self.workspaces[selected_match.candidate_id];
|
||||
let (candidate_workspace_id, candidate_workspace_location) =
|
||||
&self.workspaces[selected_match.candidate_id];
|
||||
let replace_current_window = !secondary;
|
||||
workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace
|
||||
.open_workspace_for_paths(workspace_location.paths().as_ref().clone(), cx)
|
||||
if workspace.database_id() != *candidate_workspace_id {
|
||||
workspace.open_workspace_for_paths(
|
||||
replace_current_window,
|
||||
candidate_workspace_location.paths().as_ref().clone(),
|
||||
cx,
|
||||
)
|
||||
} else {
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
cx.emit(DismissEvent);
|
||||
|
||||
@@ -12,6 +12,14 @@ message Envelope {
|
||||
uint32 id = 1;
|
||||
optional uint32 responding_to = 2;
|
||||
optional PeerId original_sender_id = 3;
|
||||
|
||||
/*
|
||||
When you are adding a new message type, instead of adding it in semantic order
|
||||
and bumping the message ID's of everything that follows, add it at the end of the
|
||||
file and bump the max number. See this
|
||||
https://github.com/zed-industries/zed/pull/7890#discussion_r1496621823
|
||||
|
||||
*/
|
||||
oneof payload {
|
||||
Hello hello = 4;
|
||||
Ack ack = 5;
|
||||
@@ -48,6 +56,7 @@ message Envelope {
|
||||
GetDefinitionResponse get_definition_response = 33;
|
||||
GetTypeDefinition get_type_definition = 34;
|
||||
GetTypeDefinitionResponse get_type_definition_response = 35;
|
||||
|
||||
GetReferences get_references = 36;
|
||||
GetReferencesResponse get_references_response = 37;
|
||||
GetDocumentHighlights get_document_highlights = 38;
|
||||
@@ -183,7 +192,10 @@ message Envelope {
|
||||
LspExtExpandMacroResponse lsp_ext_expand_macro_response = 155;
|
||||
SetRoomParticipantRole set_room_participant_role = 156;
|
||||
|
||||
UpdateUserChannels update_user_channels = 157;
|
||||
UpdateUserChannels update_user_channels = 157;
|
||||
|
||||
GetImplementation get_implementation = 162;
|
||||
GetImplementationResponse get_implementation_response = 163;
|
||||
}
|
||||
|
||||
reserved 158 to 161;
|
||||
@@ -503,6 +515,16 @@ message GetTypeDefinition {
|
||||
message GetTypeDefinitionResponse {
|
||||
repeated LocationLink links = 1;
|
||||
}
|
||||
message GetImplementation {
|
||||
uint64 project_id = 1;
|
||||
uint64 buffer_id = 2;
|
||||
Anchor position = 3;
|
||||
repeated VectorClockEntry version = 4;
|
||||
}
|
||||
|
||||
message GetImplementationResponse {
|
||||
repeated LocationLink links = 1;
|
||||
}
|
||||
|
||||
message GetReferences {
|
||||
uint64 project_id = 1;
|
||||
@@ -1085,6 +1107,7 @@ enum ChannelRole {
|
||||
Member = 1;
|
||||
Guest = 2;
|
||||
Banned = 3;
|
||||
Talker = 4;
|
||||
}
|
||||
|
||||
message SetChannelMemberRole {
|
||||
|
||||
@@ -192,6 +192,8 @@ messages!(
|
||||
(GetReferencesResponse, Background),
|
||||
(GetTypeDefinition, Background),
|
||||
(GetTypeDefinitionResponse, Background),
|
||||
(GetImplementation, Background),
|
||||
(GetImplementationResponse, Background),
|
||||
(GetUsers, Foreground),
|
||||
(Hello, Foreground),
|
||||
(IncomingCall, Foreground),
|
||||
@@ -312,6 +314,7 @@ request_messages!(
|
||||
(GetCodeActions, GetCodeActionsResponse),
|
||||
(GetCompletions, GetCompletionsResponse),
|
||||
(GetDefinition, GetDefinitionResponse),
|
||||
(GetImplementation, GetImplementationResponse),
|
||||
(GetDocumentHighlights, GetDocumentHighlightsResponse),
|
||||
(GetHover, GetHoverResponse),
|
||||
(GetNotifications, GetNotificationsResponse),
|
||||
@@ -388,6 +391,7 @@ entity_messages!(
|
||||
GetCodeActions,
|
||||
GetCompletions,
|
||||
GetDefinition,
|
||||
GetImplementation,
|
||||
GetDocumentHighlights,
|
||||
GetHover,
|
||||
GetProjectSymbols,
|
||||
|
||||
2
crates/storybook/src/actions.rs
Normal file
2
crates/storybook/src/actions.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
use gpui::actions;
|
||||
actions!(storybook, [Quit]);
|
||||
10
crates/storybook/src/app_menus.rs
Normal file
10
crates/storybook/src/app_menus.rs
Normal file
@@ -0,0 +1,10 @@
|
||||
use gpui::{Menu, MenuItem};
|
||||
|
||||
pub fn app_menus() -> Vec<Menu<'static>> {
|
||||
use crate::actions::Quit;
|
||||
|
||||
vec![Menu {
|
||||
name: "Storybook",
|
||||
items: vec![MenuItem::action("Quit", Quit)],
|
||||
}]
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
mod actions;
|
||||
mod app_menus;
|
||||
mod assets;
|
||||
mod stories;
|
||||
mod story_selector;
|
||||
@@ -9,14 +11,16 @@ use gpui::{
|
||||
WindowOptions,
|
||||
};
|
||||
use log::LevelFilter;
|
||||
use settings::{default_settings, Settings, SettingsStore};
|
||||
use settings::{default_settings, KeymapFile, Settings, SettingsStore};
|
||||
use simplelog::SimpleLogger;
|
||||
use strum::IntoEnumIterator;
|
||||
use theme::{ThemeRegistry, ThemeSettings};
|
||||
use ui::prelude::*;
|
||||
|
||||
use crate::app_menus::app_menus;
|
||||
use crate::assets::Assets;
|
||||
use crate::story_selector::{ComponentStory, StorySelector};
|
||||
use actions::Quit;
|
||||
pub use indoc::indoc;
|
||||
|
||||
#[derive(Parser)]
|
||||
@@ -37,6 +41,7 @@ fn main() {
|
||||
|
||||
SimpleLogger::init(LevelFilter::Info, Default::default()).expect("could not initialize logger");
|
||||
|
||||
menu::init();
|
||||
let args = Args::parse();
|
||||
|
||||
let story_selector = args.story.clone().unwrap_or_else(|| {
|
||||
@@ -78,6 +83,9 @@ fn main() {
|
||||
|
||||
language::init(cx);
|
||||
editor::init(cx);
|
||||
init(cx);
|
||||
load_storybook_keymap(cx);
|
||||
cx.set_menus(app_menus());
|
||||
|
||||
let _window = cx.open_window(
|
||||
WindowOptions {
|
||||
@@ -133,3 +141,19 @@ fn load_embedded_fonts(cx: &AppContext) -> gpui::Result<()> {
|
||||
|
||||
cx.text_system().add_fonts(embedded_fonts)
|
||||
}
|
||||
|
||||
fn load_storybook_keymap(cx: &mut AppContext) {
|
||||
KeymapFile::load_asset("keymaps/storybook.json", cx).unwrap();
|
||||
}
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
cx.on_action(quit);
|
||||
}
|
||||
|
||||
fn quit(_: &Quit, cx: &mut AppContext) {
|
||||
cx.spawn(|cx| async move {
|
||||
cx.update(|cx| cx.quit())?;
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
@@ -180,16 +180,21 @@ impl PickerDelegate for TasksModalDelegate {
|
||||
|
||||
fn confirm(&mut self, secondary: bool, cx: &mut ViewContext<picker::Picker<Self>>) {
|
||||
let current_match_index = self.selected_index();
|
||||
let Some(task) = secondary
|
||||
.then(|| self.spawn_oneshot(cx))
|
||||
.flatten()
|
||||
.or_else(|| {
|
||||
self.matches.get(current_match_index).map(|current_match| {
|
||||
let ix = current_match.candidate_id;
|
||||
self.candidates[ix].clone()
|
||||
})
|
||||
|
||||
let task = if secondary {
|
||||
if !self.last_prompt.trim().is_empty() {
|
||||
self.spawn_oneshot(cx)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
self.matches.get(current_match_index).map(|current_match| {
|
||||
let ix = current_match.candidate_id;
|
||||
self.candidates[ix].clone()
|
||||
})
|
||||
else {
|
||||
};
|
||||
|
||||
let Some(task) = task else {
|
||||
return;
|
||||
};
|
||||
|
||||
|
||||
@@ -14,9 +14,18 @@ pub struct SemanticVersion {
|
||||
pub patch: usize,
|
||||
}
|
||||
|
||||
impl SemanticVersion {
|
||||
pub fn new(major: usize, minor: usize, patch: usize) -> Self {
|
||||
Self {
|
||||
major,
|
||||
minor,
|
||||
patch,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for SemanticVersion {
|
||||
type Err = anyhow::Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self> {
|
||||
let mut components = s.trim().split('.');
|
||||
let major = components
|
||||
|
||||
@@ -39,6 +39,7 @@ tokio = { version = "1.15", "optional" = true }
|
||||
ui.workspace = true
|
||||
workspace.workspace = true
|
||||
zed_actions.workspace = true
|
||||
schemars.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
editor = { workspace = true, features = ["test-support"] }
|
||||
|
||||
@@ -15,7 +15,7 @@ fn normal_before(_: &mut Workspace, action: &NormalBefore, cx: &mut ViewContext<
|
||||
let count = vim.take_count(cx).unwrap_or(1);
|
||||
vim.stop_recording_immediately(action.boxed_clone());
|
||||
if count <= 1 || vim.workspace_state.replaying {
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
vim.update_active_editor(cx, |_, editor, cx| {
|
||||
editor.cancel(&Default::default(), cx);
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.move_cursors_with(|map, mut cursor, _| {
|
||||
|
||||
@@ -119,7 +119,7 @@ pub(crate) fn register(workspace: &mut Workspace, cx: &mut ViewContext<Workspace
|
||||
times -= 1;
|
||||
}
|
||||
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
vim.update_active_editor(cx, |_, editor, cx| {
|
||||
editor.transact(cx, |editor, cx| {
|
||||
for _ in 0..times {
|
||||
editor.join_lines(&Default::default(), cx)
|
||||
@@ -182,7 +182,7 @@ pub(crate) fn move_cursor(
|
||||
times: Option<usize>,
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
vim.update_active_editor(cx, |_, editor, cx| {
|
||||
let text_layout_details = editor.text_layout_details(cx);
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.move_cursors_with(|map, cursor, goal| {
|
||||
@@ -198,7 +198,7 @@ fn insert_after(_: &mut Workspace, _: &InsertAfter, cx: &mut ViewContext<Workspa
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.start_recording(cx);
|
||||
vim.switch_mode(Mode::Insert, false, cx);
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
vim.update_active_editor(cx, |_, editor, cx| {
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.move_cursors_with(|map, cursor, _| (right(map, cursor, 1), SelectionGoal::None));
|
||||
});
|
||||
@@ -221,7 +221,7 @@ fn insert_first_non_whitespace(
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.start_recording(cx);
|
||||
vim.switch_mode(Mode::Insert, false, cx);
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
vim.update_active_editor(cx, |_, editor, cx| {
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.move_cursors_with(|map, cursor, _| {
|
||||
(
|
||||
@@ -238,7 +238,7 @@ fn insert_end_of_line(_: &mut Workspace, _: &InsertEndOfLine, cx: &mut ViewConte
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.start_recording(cx);
|
||||
vim.switch_mode(Mode::Insert, false, cx);
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
vim.update_active_editor(cx, |_, editor, cx| {
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.move_cursors_with(|map, cursor, _| {
|
||||
(next_line_end(map, cursor, 1), SelectionGoal::None)
|
||||
@@ -252,7 +252,7 @@ fn insert_line_above(_: &mut Workspace, _: &InsertLineAbove, cx: &mut ViewContex
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.start_recording(cx);
|
||||
vim.switch_mode(Mode::Insert, false, cx);
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
vim.update_active_editor(cx, |_, editor, cx| {
|
||||
editor.transact(cx, |editor, cx| {
|
||||
let (map, old_selections) = editor.selections.all_display(cx);
|
||||
let selection_start_rows: HashSet<u32> = old_selections
|
||||
@@ -285,7 +285,7 @@ fn insert_line_below(_: &mut Workspace, _: &InsertLineBelow, cx: &mut ViewContex
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.start_recording(cx);
|
||||
vim.switch_mode(Mode::Insert, false, cx);
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
vim.update_active_editor(cx, |_, editor, cx| {
|
||||
let text_layout_details = editor.text_layout_details(cx);
|
||||
editor.transact(cx, |editor, cx| {
|
||||
let (map, old_selections) = editor.selections.all_display(cx);
|
||||
@@ -330,7 +330,7 @@ fn yank_line(_: &mut Workspace, _: &YankLine, cx: &mut ViewContext<Workspace>) {
|
||||
pub(crate) fn normal_replace(text: Arc<str>, cx: &mut WindowContext) {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.stop_recording();
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
vim.update_active_editor(cx, |_, editor, cx| {
|
||||
editor.transact(cx, |editor, cx| {
|
||||
editor.set_clip_at_line_ends(false, cx);
|
||||
let (map, display_selections) = editor.selections.all_display(cx);
|
||||
|
||||
@@ -40,7 +40,7 @@ where
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.record_current_action(cx);
|
||||
let count = vim.take_count(cx).unwrap_or(1) as u32;
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
vim.update_active_editor(cx, |vim, editor, cx| {
|
||||
let mut ranges = Vec::new();
|
||||
let mut cursor_positions = Vec::new();
|
||||
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
||||
|
||||
@@ -24,7 +24,7 @@ pub fn change_motion(vim: &mut Vim, motion: Motion, times: Option<usize>, cx: &m
|
||||
| Motion::Backspace
|
||||
| Motion::StartOfLine { .. }
|
||||
);
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
vim.update_active_editor(cx, |vim, editor, cx| {
|
||||
let text_layout_details = editor.text_layout_details(cx);
|
||||
editor.transact(cx, |editor, cx| {
|
||||
// We are swapping to insert mode anyway. Just set the line end clipping behavior now
|
||||
@@ -45,7 +45,7 @@ pub fn change_motion(vim: &mut Vim, motion: Motion, times: Option<usize>, cx: &m
|
||||
};
|
||||
});
|
||||
});
|
||||
copy_selections_content(editor, motion.linewise(), cx);
|
||||
copy_selections_content(vim, editor, motion.linewise(), cx);
|
||||
editor.insert("", cx);
|
||||
});
|
||||
});
|
||||
@@ -59,7 +59,7 @@ pub fn change_motion(vim: &mut Vim, motion: Motion, times: Option<usize>, cx: &m
|
||||
|
||||
pub fn change_object(vim: &mut Vim, object: Object, around: bool, cx: &mut WindowContext) {
|
||||
let mut objects_found = false;
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
vim.update_active_editor(cx, |vim, editor, cx| {
|
||||
// We are swapping to insert mode anyway. Just set the line end clipping behavior now
|
||||
editor.set_clip_at_line_ends(false, cx);
|
||||
editor.transact(cx, |editor, cx| {
|
||||
@@ -69,7 +69,7 @@ pub fn change_object(vim: &mut Vim, object: Object, around: bool, cx: &mut Windo
|
||||
});
|
||||
});
|
||||
if objects_found {
|
||||
copy_selections_content(editor, false, cx);
|
||||
copy_selections_content(vim, editor, false, cx);
|
||||
editor.insert("", cx);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -6,7 +6,7 @@ use language::Point;
|
||||
|
||||
pub fn delete_motion(vim: &mut Vim, motion: Motion, times: Option<usize>, cx: &mut WindowContext) {
|
||||
vim.stop_recording();
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
vim.update_active_editor(cx, |vim, editor, cx| {
|
||||
let text_layout_details = editor.text_layout_details(cx);
|
||||
editor.transact(cx, |editor, cx| {
|
||||
editor.set_clip_at_line_ends(false, cx);
|
||||
@@ -39,7 +39,7 @@ pub fn delete_motion(vim: &mut Vim, motion: Motion, times: Option<usize>, cx: &m
|
||||
}
|
||||
});
|
||||
});
|
||||
copy_selections_content(editor, motion.linewise(), cx);
|
||||
copy_selections_content(vim, editor, motion.linewise(), cx);
|
||||
editor.insert("", cx);
|
||||
|
||||
// Fixup cursor position after the deletion
|
||||
@@ -62,7 +62,7 @@ pub fn delete_motion(vim: &mut Vim, motion: Motion, times: Option<usize>, cx: &m
|
||||
|
||||
pub fn delete_object(vim: &mut Vim, object: Object, around: bool, cx: &mut WindowContext) {
|
||||
vim.stop_recording();
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
vim.update_active_editor(cx, |vim, editor, cx| {
|
||||
editor.transact(cx, |editor, cx| {
|
||||
editor.set_clip_at_line_ends(false, cx);
|
||||
// Emulates behavior in vim where if we expanded backwards to include a newline
|
||||
@@ -98,7 +98,7 @@ pub fn delete_object(vim: &mut Vim, object: Object, around: bool, cx: &mut Windo
|
||||
}
|
||||
});
|
||||
});
|
||||
copy_selections_content(editor, false, cx);
|
||||
copy_selections_content(vim, editor, false, cx);
|
||||
editor.insert("", cx);
|
||||
|
||||
// Fixup cursor position after the deletion
|
||||
|
||||
@@ -44,7 +44,7 @@ pub fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
|
||||
}
|
||||
|
||||
fn increment(vim: &mut Vim, mut delta: i32, step: i32, cx: &mut WindowContext) {
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
vim.update_active_editor(cx, |vim, editor, cx| {
|
||||
let mut edits = Vec::new();
|
||||
let mut new_anchors = Vec::new();
|
||||
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
use std::{borrow::Cow, cmp};
|
||||
use std::cmp;
|
||||
|
||||
use editor::{
|
||||
display_map::ToDisplayPoint, movement, scroll::Autoscroll, ClipboardSelection, DisplayPoint,
|
||||
};
|
||||
use gpui::{impl_actions, ViewContext};
|
||||
use gpui::{impl_actions, AppContext, ViewContext};
|
||||
use language::{Bias, SelectionGoal};
|
||||
use serde::Deserialize;
|
||||
use settings::Settings;
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::{state::Mode, utils::copy_selections_content, Vim};
|
||||
use crate::{state::Mode, utils::copy_selections_content, UseSystemClipboard, Vim, VimSettings};
|
||||
|
||||
#[derive(Clone, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@@ -25,34 +26,60 @@ pub(crate) fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>
|
||||
workspace.register_action(paste);
|
||||
}
|
||||
|
||||
fn system_clipboard_is_newer(vim: &Vim, cx: &mut AppContext) -> bool {
|
||||
cx.read_from_clipboard().is_some_and(|item| {
|
||||
if let Some(last_state) = vim.workspace_state.registers.get(".system.") {
|
||||
last_state != item.text()
|
||||
} else {
|
||||
true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn paste(_: &mut Workspace, action: &Paste, cx: &mut ViewContext<Workspace>) {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.record_current_action(cx);
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
vim.update_active_editor(cx, |vim, editor, cx| {
|
||||
let text_layout_details = editor.text_layout_details(cx);
|
||||
editor.transact(cx, |editor, cx| {
|
||||
editor.set_clip_at_line_ends(false, cx);
|
||||
|
||||
let Some(item) = cx.read_from_clipboard() else {
|
||||
return;
|
||||
};
|
||||
let clipboard_text = Cow::Borrowed(item.text());
|
||||
let (clipboard_text, clipboard_selections): (String, Option<_>) =
|
||||
if VimSettings::get_global(cx).use_system_clipboard == UseSystemClipboard::Never
|
||||
|| VimSettings::get_global(cx).use_system_clipboard
|
||||
== UseSystemClipboard::OnYank
|
||||
&& !system_clipboard_is_newer(vim, cx)
|
||||
{
|
||||
(
|
||||
vim.workspace_state
|
||||
.registers
|
||||
.get("\"")
|
||||
.cloned()
|
||||
.unwrap_or_else(|| "".to_string()),
|
||||
None,
|
||||
)
|
||||
} else {
|
||||
if let Some(item) = cx.read_from_clipboard() {
|
||||
let clipboard_selections = item
|
||||
.metadata::<Vec<ClipboardSelection>>()
|
||||
.filter(|clipboard_selections| {
|
||||
clipboard_selections.len() > 1
|
||||
&& vim.state().mode != Mode::VisualLine
|
||||
});
|
||||
(item.text().clone(), clipboard_selections)
|
||||
} else {
|
||||
("".into(), None)
|
||||
}
|
||||
};
|
||||
|
||||
if clipboard_text.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
if !action.preserve_clipboard && vim.state().mode.is_visual() {
|
||||
copy_selections_content(editor, vim.state().mode == Mode::VisualLine, cx);
|
||||
copy_selections_content(vim, editor, vim.state().mode == Mode::VisualLine, cx);
|
||||
}
|
||||
|
||||
// if we are copying from multi-cursor (of visual block mode), we want
|
||||
// to
|
||||
let clipboard_selections =
|
||||
item.metadata::<Vec<ClipboardSelection>>()
|
||||
.filter(|clipboard_selections| {
|
||||
clipboard_selections.len() > 1 && vim.state().mode != Mode::VisualLine
|
||||
});
|
||||
|
||||
let (display_map, current_selections) = editor.selections.all_adjusted_display(cx);
|
||||
|
||||
// unlike zed, if you have a multi-cursor selection from vim block mode,
|
||||
@@ -201,8 +228,11 @@ mod test {
|
||||
use crate::{
|
||||
state::Mode,
|
||||
test::{NeovimBackedTestContext, VimTestContext},
|
||||
UseSystemClipboard, VimSettings,
|
||||
};
|
||||
use gpui::ClipboardItem;
|
||||
use indoc::indoc;
|
||||
use settings::SettingsStore;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_paste(cx: &mut gpui::TestAppContext) {
|
||||
@@ -291,6 +321,103 @@ mod test {
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_yank_system_clipboard_never(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
|
||||
cx.update_global(|store: &mut SettingsStore, cx| {
|
||||
store.update_user_settings::<VimSettings>(cx, |s| {
|
||||
s.use_system_clipboard = Some(UseSystemClipboard::Never)
|
||||
});
|
||||
});
|
||||
|
||||
cx.set_state(
|
||||
indoc! {"
|
||||
The quick brown
|
||||
fox jˇumps over
|
||||
the lazy dog"},
|
||||
Mode::Normal,
|
||||
);
|
||||
cx.simulate_keystrokes(["v", "i", "w", "y"]);
|
||||
cx.assert_state(
|
||||
indoc! {"
|
||||
The quick brown
|
||||
fox ˇjumps over
|
||||
the lazy dog"},
|
||||
Mode::Normal,
|
||||
);
|
||||
cx.simulate_keystroke("p");
|
||||
cx.assert_state(
|
||||
indoc! {"
|
||||
The quick brown
|
||||
fox jjumpˇsumps over
|
||||
the lazy dog"},
|
||||
Mode::Normal,
|
||||
);
|
||||
assert_eq!(cx.read_from_clipboard(), None);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_yank_system_clipboard_on_yank(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
|
||||
cx.update_global(|store: &mut SettingsStore, cx| {
|
||||
store.update_user_settings::<VimSettings>(cx, |s| {
|
||||
s.use_system_clipboard = Some(UseSystemClipboard::OnYank)
|
||||
});
|
||||
});
|
||||
|
||||
// copy in visual mode
|
||||
cx.set_state(
|
||||
indoc! {"
|
||||
The quick brown
|
||||
fox jˇumps over
|
||||
the lazy dog"},
|
||||
Mode::Normal,
|
||||
);
|
||||
cx.simulate_keystrokes(["v", "i", "w", "y"]);
|
||||
cx.assert_state(
|
||||
indoc! {"
|
||||
The quick brown
|
||||
fox ˇjumps over
|
||||
the lazy dog"},
|
||||
Mode::Normal,
|
||||
);
|
||||
cx.simulate_keystroke("p");
|
||||
cx.assert_state(
|
||||
indoc! {"
|
||||
The quick brown
|
||||
fox jjumpˇsumps over
|
||||
the lazy dog"},
|
||||
Mode::Normal,
|
||||
);
|
||||
assert_eq!(
|
||||
cx.read_from_clipboard().map(|item| item.text().clone()),
|
||||
Some("jumps".into())
|
||||
);
|
||||
cx.simulate_keystrokes(["d", "d", "p"]);
|
||||
cx.assert_state(
|
||||
indoc! {"
|
||||
The quick brown
|
||||
the lazy dog
|
||||
ˇfox jjumpsumps over"},
|
||||
Mode::Normal,
|
||||
);
|
||||
assert_eq!(
|
||||
cx.read_from_clipboard().map(|item| item.text().clone()),
|
||||
Some("jumps".into())
|
||||
);
|
||||
cx.write_to_clipboard(ClipboardItem::new("test-copy".to_string()));
|
||||
cx.simulate_keystroke("shift-p");
|
||||
cx.assert_state(
|
||||
indoc! {"
|
||||
The quick brown
|
||||
the lazy dog
|
||||
test-copˇyfox jjumpsumps over"},
|
||||
Mode::Normal,
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_paste_visual(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
@@ -52,7 +52,7 @@ fn scroll(
|
||||
) {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
let amount = by(vim.take_count(cx).map(|c| c as f32));
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
vim.update_active_editor(cx, |_, editor, cx| {
|
||||
scroll_editor(editor, move_cursor, &amount, cx)
|
||||
});
|
||||
})
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user