Compare commits
80 Commits
v0.130.4-p
...
multibuffe
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f5da886564 | ||
|
|
3a6887db53 | ||
|
|
b19ad92a1e | ||
|
|
2a47df9d3b | ||
|
|
935e0d547e | ||
|
|
cc367d43d6 | ||
|
|
a4566c36a3 | ||
|
|
843aad80c6 | ||
|
|
def87a8d76 | ||
|
|
ee1642a50f | ||
|
|
7c5bc3c26f | ||
|
|
4a3032c5e5 | ||
|
|
f327118e06 | ||
|
|
f9bf60f017 | ||
|
|
0390df27d4 | ||
|
|
cf5a113751 | ||
|
|
7dccbd8e3b | ||
|
|
d009d84ead | ||
|
|
5e44748677 | ||
|
|
d2bf80ca3d | ||
|
|
44aed4a0cb | ||
|
|
e826ef83e2 | ||
|
|
56c0345cf3 | ||
|
|
f1428fea4e | ||
|
|
9b88259b1f | ||
|
|
4d68bf2fa6 | ||
|
|
87c282d8f1 | ||
|
|
134decb75e | ||
|
|
f0d4d71e97 | ||
|
|
bcdae9fefa | ||
|
|
7aef447f47 | ||
|
|
4bdfc12b79 | ||
|
|
4ce5b22989 | ||
|
|
ce5bc399df | ||
|
|
4f9ad300a7 | ||
|
|
3e6a9f6890 | ||
|
|
4944dc9d78 | ||
|
|
c7961b9054 | ||
|
|
c64c2758c0 | ||
|
|
0325bda89a | ||
|
|
3aa242e076 | ||
|
|
518cfdbd56 | ||
|
|
bf9b443b4a | ||
|
|
fe4b345603 | ||
|
|
7b636d9774 | ||
|
|
c851e6edba | ||
|
|
4aaf3459c4 | ||
|
|
b05aa381aa | ||
|
|
ec6efe262f | ||
|
|
6c45bc2b3d | ||
|
|
83364c709b | ||
|
|
4cab4e8a10 | ||
|
|
1737329e84 | ||
|
|
3ae6463869 | ||
|
|
773a3e83ad | ||
|
|
cedbfac844 | ||
|
|
73d8a43c81 | ||
|
|
4a325614f0 | ||
|
|
5d88d9c0d7 | ||
|
|
dde87f6468 | ||
|
|
d306b531c7 | ||
|
|
0f1c2e6f2b | ||
|
|
0861ceaac2 | ||
|
|
1c485a0d05 | ||
|
|
7d1a5d2ddf | ||
|
|
27165e9927 | ||
|
|
1085642c88 | ||
|
|
ee1b1779f1 | ||
|
|
5b4ff74dca | ||
|
|
8e9543aefe | ||
|
|
c0d117182f | ||
|
|
9cbde74274 | ||
|
|
879f361966 | ||
|
|
79272b75e3 | ||
|
|
0ddec2753a | ||
|
|
ccb2d02ce0 | ||
|
|
fc08ea9b0d | ||
|
|
49c53bc0ec | ||
|
|
256b446bdf | ||
|
|
ef3d04efe6 |
@@ -9,10 +9,10 @@ jobs:
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.10.5"
|
||||
python-version: "3.11"
|
||||
architecture: "x64"
|
||||
cache: "pip"
|
||||
- run: pip install -r script/update_top_ranking_issues/requirements.txt
|
||||
- run: python script/update_top_ranking_issues/main.py 5393 --github-token ${{ secrets.GITHUB_TOKEN }} --prod
|
||||
- run: python script/update_top_ranking_issues/main.py --github-token ${{ secrets.GITHUB_TOKEN }} --issue-reference-number 5393
|
||||
|
||||
@@ -9,10 +9,10 @@ jobs:
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.10.5"
|
||||
python-version: "3.11"
|
||||
architecture: "x64"
|
||||
cache: "pip"
|
||||
- run: pip install -r script/update_top_ranking_issues/requirements.txt
|
||||
- run: python script/update_top_ranking_issues/main.py 6952 --github-token ${{ secrets.GITHUB_TOKEN }} --prod --query-day-interval 7
|
||||
- run: python script/update_top_ranking_issues/main.py --github-token ${{ secrets.GITHUB_TOKEN }} --issue-reference-number 6952 --query-day-interval 7
|
||||
|
||||
70
Cargo.lock
generated
70
Cargo.lock
generated
@@ -1434,7 +1434,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "blade-graphics"
|
||||
version = "0.3.0"
|
||||
source = "git+https://github.com/kvark/blade?rev=61cbd6b2c224791d52b150fe535cee665cc91bb2#61cbd6b2c224791d52b150fe535cee665cc91bb2"
|
||||
source = "git+https://github.com/zed-industries/blade?rev=85981c0f4890a5fcd08da2a53cc4a0459247af44#85981c0f4890a5fcd08da2a53cc4a0459247af44"
|
||||
dependencies = [
|
||||
"ash",
|
||||
"ash-window",
|
||||
@@ -1464,7 +1464,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "blade-macros"
|
||||
version = "0.2.1"
|
||||
source = "git+https://github.com/kvark/blade?rev=61cbd6b2c224791d52b150fe535cee665cc91bb2#61cbd6b2c224791d52b150fe535cee665cc91bb2"
|
||||
source = "git+https://github.com/zed-industries/blade?rev=85981c0f4890a5fcd08da2a53cc4a0459247af44#85981c0f4890a5fcd08da2a53cc4a0459247af44"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -5338,7 +5338,6 @@ dependencies = [
|
||||
"tree-sitter-c",
|
||||
"tree-sitter-cpp",
|
||||
"tree-sitter-css",
|
||||
"tree-sitter-dart",
|
||||
"tree-sitter-elixir",
|
||||
"tree-sitter-elm",
|
||||
"tree-sitter-embedded-template",
|
||||
@@ -5348,7 +5347,6 @@ dependencies = [
|
||||
"tree-sitter-gowork",
|
||||
"tree-sitter-hcl",
|
||||
"tree-sitter-heex",
|
||||
"tree-sitter-html",
|
||||
"tree-sitter-jsdoc",
|
||||
"tree-sitter-json 0.20.0",
|
||||
"tree-sitter-lua",
|
||||
@@ -5677,10 +5675,13 @@ dependencies = [
|
||||
name = "markdown_preview"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion 1.0.5",
|
||||
"editor",
|
||||
"gpui",
|
||||
"language",
|
||||
"linkify",
|
||||
"log",
|
||||
"pretty_assertions",
|
||||
"pulldown-cmark",
|
||||
"theme",
|
||||
@@ -9497,16 +9498,19 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"editor",
|
||||
"file_icons",
|
||||
"fuzzy",
|
||||
"gpui",
|
||||
"itertools 0.11.0",
|
||||
"language",
|
||||
"menu",
|
||||
"picker",
|
||||
"project",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"settings",
|
||||
"task",
|
||||
"terminal",
|
||||
"tree-sitter-rust",
|
||||
"tree-sitter-typescript",
|
||||
"ui",
|
||||
@@ -10218,15 +10222,6 @@ dependencies = [
|
||||
"tree-sitter",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tree-sitter-dart"
|
||||
version = "0.0.1"
|
||||
source = "git+https://github.com/agent3bood/tree-sitter-dart?rev=48934e3bf757a9b78f17bdfaa3e2b4284656fdc7#48934e3bf757a9b78f17bdfaa3e2b4284656fdc7"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"tree-sitter",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tree-sitter-elixir"
|
||||
version = "0.1.0"
|
||||
@@ -12379,7 +12374,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed"
|
||||
version = "0.130.0"
|
||||
version = "0.131.0"
|
||||
dependencies = [
|
||||
"activity_indicator",
|
||||
"anyhow",
|
||||
@@ -12491,6 +12486,20 @@ dependencies = [
|
||||
"zed_extension_api 0.0.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zed_dart"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"zed_extension_api 0.0.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zed_emmet"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"zed_extension_api 0.0.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zed_erlang"
|
||||
version = "0.0.1"
|
||||
@@ -12507,13 +12516,6 @@ dependencies = [
|
||||
"wit-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zed_extension_api"
|
||||
version = "0.0.5"
|
||||
dependencies = [
|
||||
"wit-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zed_extension_api"
|
||||
version = "0.0.5"
|
||||
@@ -12523,16 +12525,32 @@ dependencies = [
|
||||
"wit-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zed_extension_api"
|
||||
version = "0.0.6"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"wit-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zed_gleam"
|
||||
version = "0.0.2"
|
||||
dependencies = [
|
||||
"zed_extension_api 0.0.4",
|
||||
"zed_extension_api 0.0.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zed_haskell"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"zed_extension_api 0.0.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zed_html"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"zed_extension_api 0.0.4",
|
||||
]
|
||||
@@ -12562,14 +12580,14 @@ dependencies = [
|
||||
name = "zed_svelte"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"zed_extension_api 0.0.4",
|
||||
"zed_extension_api 0.0.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zed_toml"
|
||||
version = "0.0.2"
|
||||
dependencies = [
|
||||
"zed_extension_api 0.0.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"zed_extension_api 0.0.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -12583,7 +12601,7 @@ dependencies = [
|
||||
name = "zed_zig"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"zed_extension_api 0.0.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"zed_extension_api 0.0.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -102,9 +102,12 @@ members = [
|
||||
"extensions/astro",
|
||||
"extensions/clojure",
|
||||
"extensions/csharp",
|
||||
"extensions/dart",
|
||||
"extensions/emmet",
|
||||
"extensions/erlang",
|
||||
"extensions/gleam",
|
||||
"extensions/haskell",
|
||||
"extensions/html",
|
||||
"extensions/php",
|
||||
"extensions/prisma",
|
||||
"extensions/purescript",
|
||||
@@ -227,8 +230,9 @@ async-recursion = "1.0.0"
|
||||
async-tar = "0.4.2"
|
||||
async-trait = "0.1"
|
||||
bitflags = "2.4.2"
|
||||
blade-graphics = { git = "https://github.com/kvark/blade", rev = "61cbd6b2c224791d52b150fe535cee665cc91bb2" }
|
||||
blade-macros = { git = "https://github.com/kvark/blade", rev = "61cbd6b2c224791d52b150fe535cee665cc91bb2" }
|
||||
# todo(linux): Remove these once https://github.com/kvark/blade/pull/107 is merged and we've upgraded our renderer
|
||||
blade-graphics = { git = "https://github.com/zed-industries/blade", rev = "85981c0f4890a5fcd08da2a53cc4a0459247af44" }
|
||||
blade-macros = { git = "https://github.com/zed-industries/blade", rev = "85981c0f4890a5fcd08da2a53cc4a0459247af44" }
|
||||
blade-rwh = { package = "raw-window-handle", version = "0.5" }
|
||||
cap-std = "3.0"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
@@ -306,7 +310,6 @@ tree-sitter-bash = { git = "https://github.com/tree-sitter/tree-sitter-bash", re
|
||||
tree-sitter-c = "0.20.1"
|
||||
tree-sitter-cpp = { git = "https://github.com/tree-sitter/tree-sitter-cpp", rev = "f44509141e7e483323d2ec178f2d2e6c0fc041c1" }
|
||||
tree-sitter-css = { git = "https://github.com/tree-sitter/tree-sitter-css", rev = "769203d0f9abe1a9a691ac2b9fe4bb4397a73c51" }
|
||||
tree-sitter-dart = { git = "https://github.com/agent3bood/tree-sitter-dart", rev = "48934e3bf757a9b78f17bdfaa3e2b4284656fdc7" }
|
||||
tree-sitter-elixir = { git = "https://github.com/elixir-lang/tree-sitter-elixir", rev = "a2861e88a730287a60c11ea9299c033c7d076e30" }
|
||||
tree-sitter-elm = { git = "https://github.com/elm-tooling/tree-sitter-elm", rev = "692c50c0b961364c40299e73c1306aecb5d20f40" }
|
||||
tree-sitter-embedded-template = "0.20.0"
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
"ctrl-0": "zed::ResetBufferFontSize",
|
||||
"ctrl-,": "zed::OpenSettings",
|
||||
"ctrl-q": "zed::Quit",
|
||||
"ctrl-h": "zed::Hide",
|
||||
"alt-f9": "zed::Hide",
|
||||
"f11": "zed::ToggleFullScreen"
|
||||
}
|
||||
},
|
||||
@@ -38,7 +38,6 @@
|
||||
"escape": "editor::Cancel",
|
||||
"backspace": "editor::Backspace",
|
||||
"shift-backspace": "editor::Backspace",
|
||||
"ctrl-h": "editor::Backspace",
|
||||
"delete": "editor::Delete",
|
||||
"ctrl-d": "editor::Delete",
|
||||
"tab": "editor::Tab",
|
||||
@@ -150,10 +149,11 @@
|
||||
"ctrl-shift-enter": "editor::NewlineBelow",
|
||||
"ctrl-enter": "editor::NewlineAbove",
|
||||
"alt-z": "editor::ToggleSoftWrap",
|
||||
"ctrl-f": [
|
||||
"ctrl-f": "buffer_search::Deploy",
|
||||
"ctrl-h": [
|
||||
"buffer_search::Deploy",
|
||||
{
|
||||
"focus": true
|
||||
"replace_enabled": true
|
||||
}
|
||||
],
|
||||
// "cmd-e": [
|
||||
@@ -212,7 +212,9 @@
|
||||
"enter": "search::SelectNextMatch",
|
||||
"shift-enter": "search::SelectPrevMatch",
|
||||
"alt-enter": "search::SelectAllMatches",
|
||||
"alt-tab": "search::CycleMode"
|
||||
"alt-tab": "search::CycleMode",
|
||||
"ctrl-f": "search::FocusSearch",
|
||||
"ctrl-h": "search::ToggleReplace"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -234,6 +236,7 @@
|
||||
"bindings": {
|
||||
"escape": "project_search::ToggleFocus",
|
||||
"alt-tab": "search::CycleMode",
|
||||
"ctrl-shift-f": "search::FocusSearch",
|
||||
"ctrl-shift-h": "search::ToggleReplace",
|
||||
"alt-ctrl-g": "search::ActivateRegexMode",
|
||||
"alt-ctrl-x": "search::ActivateTextMode"
|
||||
@@ -419,6 +422,12 @@
|
||||
"ctrl-j": "workspace::ToggleBottomDock",
|
||||
"ctrl-alt-y": "workspace::CloseAllDocks",
|
||||
"ctrl-shift-f": "pane::DeploySearch",
|
||||
"ctrl-shift-h": [
|
||||
"pane::DeploySearch",
|
||||
{
|
||||
"replace_enabled": true
|
||||
}
|
||||
],
|
||||
"ctrl-k ctrl-s": "zed::OpenKeymap",
|
||||
"ctrl-k ctrl-t": "theme_selector::Toggle",
|
||||
"ctrl-shift-t": "project_symbols::Toggle",
|
||||
|
||||
@@ -170,10 +170,11 @@
|
||||
"cmd-shift-enter": "editor::NewlineAbove",
|
||||
"cmd-enter": "editor::NewlineBelow",
|
||||
"alt-z": "editor::ToggleSoftWrap",
|
||||
"cmd-f": [
|
||||
"cmd-f": "buffer_search::Deploy",
|
||||
"cmd-alt-f": [
|
||||
"buffer_search::Deploy",
|
||||
{
|
||||
"focus": true
|
||||
"replace_enabled": true
|
||||
}
|
||||
],
|
||||
"cmd-e": [
|
||||
@@ -232,7 +233,9 @@
|
||||
"enter": "search::SelectNextMatch",
|
||||
"shift-enter": "search::SelectPrevMatch",
|
||||
"alt-enter": "search::SelectAllMatches",
|
||||
"alt-tab": "search::CycleMode"
|
||||
"alt-tab": "search::CycleMode",
|
||||
"cmd-f": "search::FocusSearch",
|
||||
"cmd-alt-f": "search::ToggleReplace"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -254,6 +257,7 @@
|
||||
"bindings": {
|
||||
"escape": "project_search::ToggleFocus",
|
||||
"alt-tab": "search::CycleMode",
|
||||
"cmd-shift-f": "search::FocusSearch",
|
||||
"cmd-shift-h": "search::ToggleReplace",
|
||||
"alt-cmd-g": "search::ActivateRegexMode",
|
||||
"alt-cmd-x": "search::ActivateTextMode"
|
||||
@@ -436,6 +440,12 @@
|
||||
"cmd-j": "workspace::ToggleBottomDock",
|
||||
"alt-cmd-y": "workspace::CloseAllDocks",
|
||||
"cmd-shift-f": "pane::DeploySearch",
|
||||
"cmd-shift-h": [
|
||||
"pane::DeploySearch",
|
||||
{
|
||||
"replace_enabled": true
|
||||
}
|
||||
],
|
||||
"cmd-k cmd-s": "zed::OpenKeymap",
|
||||
"cmd-k cmd-t": "theme_selector::Toggle",
|
||||
"cmd-t": "project_symbols::Toggle",
|
||||
|
||||
@@ -73,8 +73,17 @@
|
||||
],
|
||||
"g shift-e": ["vim::PreviousWordEnd", { "ignorePunctuation": true }],
|
||||
|
||||
"n": "search::SelectNextMatch",
|
||||
"shift-n": "search::SelectPrevMatch",
|
||||
"/": "vim::Search",
|
||||
"?": [
|
||||
"vim::Search",
|
||||
{
|
||||
"backwards": true
|
||||
}
|
||||
],
|
||||
"*": "vim::MoveToNext",
|
||||
"#": "vim::MoveToPrev",
|
||||
"n": "vim::MoveToNextMatch",
|
||||
"shift-n": "vim::MoveToPrevMatch",
|
||||
"%": "vim::Matching",
|
||||
"f": [
|
||||
"vim::PushOperator",
|
||||
@@ -137,8 +146,10 @@
|
||||
"g d": "editor::GoToDefinition",
|
||||
"g shift-d": "editor::GoToTypeDefinition",
|
||||
"g x": "editor::OpenUrl",
|
||||
"g n": "vim::SelectNext",
|
||||
"g shift-n": "vim::SelectPrevious",
|
||||
"g n": "vim::SelectNextMatch",
|
||||
"g shift-n": "vim::SelectPreviousMatch",
|
||||
"g l": "vim::SelectNext",
|
||||
"g shift-l": "vim::SelectPrevious",
|
||||
"g >": [
|
||||
"editor::SelectNext",
|
||||
{
|
||||
@@ -349,15 +360,6 @@
|
||||
],
|
||||
"u": "editor::Undo",
|
||||
"ctrl-r": "editor::Redo",
|
||||
"/": "vim::Search",
|
||||
"?": [
|
||||
"vim::Search",
|
||||
{
|
||||
"backwards": true
|
||||
}
|
||||
],
|
||||
"*": "vim::MoveToNext",
|
||||
"#": "vim::MoveToPrev",
|
||||
"r": ["vim::PushOperator", "Replace"],
|
||||
"s": "vim::Substitute",
|
||||
"shift-s": "vim::SubstituteLine",
|
||||
@@ -382,18 +384,46 @@
|
||||
"d": "editor::Rename" // zed specific
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && vim_mode == normal && vim_operator == c",
|
||||
"bindings": {
|
||||
"s": [
|
||||
"vim::PushOperator",
|
||||
{
|
||||
"ChangeSurrounds": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && vim_operator == d",
|
||||
"bindings": {
|
||||
"d": "vim::CurrentLine"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && vim_mode == normal && vim_operator == d",
|
||||
"bindings": {
|
||||
"s": ["vim::PushOperator", "DeleteSurrounds"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && vim_operator == y",
|
||||
"bindings": {
|
||||
"y": "vim::CurrentLine"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && vim_mode == normal && vim_operator == y",
|
||||
"bindings": {
|
||||
"s": [
|
||||
"vim::PushOperator",
|
||||
{
|
||||
"AddSurrounds": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && VimObject",
|
||||
"bindings": {
|
||||
@@ -546,6 +576,12 @@
|
||||
"escape": "buffer_search::Dismiss"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "EmptyPane || SharedScreen",
|
||||
"bindings": {
|
||||
":": "command_palette::Toggle"
|
||||
}
|
||||
},
|
||||
{
|
||||
// netrw compatibility
|
||||
"context": "ProjectPanel && not_editing",
|
||||
|
||||
@@ -70,7 +70,7 @@
|
||||
// documentation when not included in original completion list.
|
||||
"completion_documentation_secondary_query_debounce": 300,
|
||||
// Whether to show wrap guides in the editor. Setting this to true will
|
||||
// show a guide at the 'preferred_line_length' value if softwrap is set to
|
||||
// show a guide at the 'preferred_line_length' value if 'soft_wrap' is set to
|
||||
// 'preferred_line_length', and will show any additional guides as specified
|
||||
// by the 'wrap_guides' setting.
|
||||
"show_wrap_guides": true,
|
||||
@@ -284,6 +284,11 @@
|
||||
// 4. Save when idle for a certain amount of time:
|
||||
// "autosave": { "after_delay": {"milliseconds": 500} },
|
||||
"autosave": "off",
|
||||
// Settings related to the editor's tab bar.
|
||||
"tab_bar": {
|
||||
// Whether or not to show the navigation history buttons.
|
||||
"show_nav_history_buttons": true
|
||||
},
|
||||
// Settings related to the editor's tabs
|
||||
"tabs": {
|
||||
// Show git status colors in the editor tabs.
|
||||
@@ -545,18 +550,16 @@
|
||||
"file_types": {},
|
||||
// Different settings for specific languages.
|
||||
"languages": {
|
||||
"Plain Text": {
|
||||
"soft_wrap": "preferred_line_length"
|
||||
"C++": {
|
||||
"format_on_save": "off"
|
||||
},
|
||||
"Elixir": {
|
||||
"tab_size": 2
|
||||
"C": {
|
||||
"format_on_save": "off"
|
||||
},
|
||||
"Gleam": {
|
||||
"tab_size": 2
|
||||
},
|
||||
"Go": {
|
||||
"tab_size": 4,
|
||||
"hard_tabs": true,
|
||||
"code_actions_on_format": {
|
||||
"source.organizeImports": true
|
||||
}
|
||||
@@ -564,40 +567,12 @@
|
||||
"Make": {
|
||||
"hard_tabs": true
|
||||
},
|
||||
"Markdown": {
|
||||
"tab_size": 2,
|
||||
"soft_wrap": "preferred_line_length"
|
||||
},
|
||||
"JavaScript": {
|
||||
"tab_size": 2
|
||||
},
|
||||
"Terraform": {
|
||||
"tab_size": 2
|
||||
},
|
||||
"TypeScript": {
|
||||
"tab_size": 2
|
||||
},
|
||||
"TSX": {
|
||||
"tab_size": 2
|
||||
},
|
||||
"YAML": {
|
||||
"tab_size": 2
|
||||
},
|
||||
"JSON": {
|
||||
"tab_size": 2
|
||||
},
|
||||
"OCaml": {
|
||||
"tab_size": 2
|
||||
},
|
||||
"OCaml Interface": {
|
||||
"tab_size": 2
|
||||
},
|
||||
"Prisma": {
|
||||
"tab_size": 2
|
||||
}
|
||||
},
|
||||
// Zed's Prettier integration settings.
|
||||
// If Prettier is enabled, Zed will use this its Prettier instance for any applicable file, if
|
||||
// If Prettier is enabled, Zed will use this for its Prettier instance for any applicable file, if
|
||||
// project has no other Prettier installed.
|
||||
"prettier": {
|
||||
// Use regular Prettier json configuration:
|
||||
@@ -646,5 +621,10 @@
|
||||
// Mostly useful for developers who are managing multiple instances of Zed.
|
||||
"dev": {
|
||||
// "theme": "Andromeda"
|
||||
},
|
||||
// Task-related settings.
|
||||
"task": {
|
||||
// Whether to show task status indicator in the status bar. Default: true
|
||||
"show_status_indicator": true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ use serde::{
|
||||
de::{self, Visitor},
|
||||
Deserialize, Deserializer, Serialize, Serializer,
|
||||
};
|
||||
use settings::Settings;
|
||||
use settings::{Settings, SettingsSources};
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub enum ZedDotDevModel {
|
||||
@@ -332,13 +332,12 @@ impl Settings for AssistantSettings {
|
||||
type FileContent = AssistantSettingsContent;
|
||||
|
||||
fn load(
|
||||
default_value: &Self::FileContent,
|
||||
user_values: &[&Self::FileContent],
|
||||
sources: SettingsSources<Self::FileContent>,
|
||||
_: &mut gpui::AppContext,
|
||||
) -> anyhow::Result<Self> {
|
||||
let mut settings = AssistantSettings::default();
|
||||
|
||||
for value in [default_value].iter().chain(user_values) {
|
||||
for value in sources.defaults_and_customizations() {
|
||||
let value = value.upgrade();
|
||||
merge(&mut settings.enabled, value.enabled);
|
||||
merge(&mut settings.button, value.button);
|
||||
|
||||
@@ -11,13 +11,13 @@ use gpui::{
|
||||
};
|
||||
use isahc::AsyncBody;
|
||||
|
||||
use markdown_preview::markdown_preview_view::MarkdownPreviewView;
|
||||
use markdown_preview::markdown_preview_view::{MarkdownPreviewMode, MarkdownPreviewView};
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde_derive::Serialize;
|
||||
use smol::io::AsyncReadExt;
|
||||
|
||||
use settings::{Settings, SettingsStore};
|
||||
use settings::{Settings, SettingsSources, SettingsStore};
|
||||
use smol::{fs::File, process::Command};
|
||||
|
||||
use release_channel::{AppCommitSha, AppVersion, ReleaseChannel};
|
||||
@@ -82,25 +82,22 @@ struct AutoUpdateSetting(bool);
|
||||
/// Whether or not to automatically check for updates.
|
||||
///
|
||||
/// Default: true
|
||||
#[derive(Clone, Default, JsonSchema, Deserialize, Serialize)]
|
||||
#[derive(Clone, Copy, Default, JsonSchema, Deserialize, Serialize)]
|
||||
#[serde(transparent)]
|
||||
struct AutoUpdateSettingOverride(Option<bool>);
|
||||
struct AutoUpdateSettingContent(bool);
|
||||
|
||||
impl Settings for AutoUpdateSetting {
|
||||
const KEY: Option<&'static str> = Some("auto_update");
|
||||
|
||||
type FileContent = AutoUpdateSettingOverride;
|
||||
type FileContent = Option<AutoUpdateSettingContent>;
|
||||
|
||||
fn load(
|
||||
default_value: &Self::FileContent,
|
||||
user_values: &[&Self::FileContent],
|
||||
_: &mut AppContext,
|
||||
) -> Result<Self> {
|
||||
Ok(Self(
|
||||
Self::json_merge(default_value, user_values)?
|
||||
.0
|
||||
.ok_or_else(Self::missing_default)?,
|
||||
))
|
||||
fn load(sources: SettingsSources<Self::FileContent>, _: &mut AppContext) -> Result<Self> {
|
||||
let auto_update = [sources.release_channel, sources.user]
|
||||
.into_iter()
|
||||
.find_map(|value| value.copied().flatten())
|
||||
.unwrap_or(sources.default.ok_or_else(Self::missing_default)?);
|
||||
|
||||
Ok(Self(auto_update.0))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -238,10 +235,11 @@ fn view_release_notes_locally(workspace: &mut Workspace, cx: &mut ViewContext<Wo
|
||||
.new_view(|cx| Editor::for_multibuffer(buffer, Some(project), cx));
|
||||
let workspace_handle = workspace.weak_handle();
|
||||
let view: View<MarkdownPreviewView> = MarkdownPreviewView::new(
|
||||
MarkdownPreviewMode::Default,
|
||||
editor,
|
||||
workspace_handle,
|
||||
Some(tab_description),
|
||||
language_registry,
|
||||
Some(tab_description),
|
||||
cx,
|
||||
);
|
||||
workspace.add_item_to_active_pane(Box::new(view.clone()), cx);
|
||||
|
||||
@@ -373,7 +373,10 @@ impl ActiveCall {
|
||||
self.report_call_event("hang up", cx);
|
||||
|
||||
Audio::end_call(cx);
|
||||
|
||||
let channel_id = self.channel_id(cx);
|
||||
if let Some((room, _)) = self.room.take() {
|
||||
cx.emit(Event::RoomLeft { channel_id });
|
||||
room.update(cx, |room, cx| room.leave(cx))
|
||||
} else {
|
||||
Task::ready(Ok(()))
|
||||
|
||||
@@ -2,7 +2,7 @@ use anyhow::Result;
|
||||
use gpui::AppContext;
|
||||
use schemars::JsonSchema;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use settings::Settings;
|
||||
use settings::{Settings, SettingsSources};
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct CallSettings {
|
||||
@@ -29,14 +29,7 @@ impl Settings for CallSettings {
|
||||
|
||||
type FileContent = CallSettingsContent;
|
||||
|
||||
fn load(
|
||||
default_value: &Self::FileContent,
|
||||
user_values: &[&Self::FileContent],
|
||||
_cx: &mut AppContext,
|
||||
) -> Result<Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
Self::load_via_json_merge(default_value, user_values)
|
||||
fn load(sources: SettingsSources<Self::FileContent>, _: &mut AppContext) -> Result<Self> {
|
||||
sources.json_merge()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ pub enum Event {
|
||||
RemoteProjectInvitationDiscarded {
|
||||
project_id: u64,
|
||||
},
|
||||
Left {
|
||||
RoomLeft {
|
||||
channel_id: Option<ChannelId>,
|
||||
},
|
||||
}
|
||||
@@ -366,9 +366,6 @@ impl Room {
|
||||
|
||||
pub(crate) fn leave(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
|
||||
cx.notify();
|
||||
cx.emit(Event::Left {
|
||||
channel_id: self.channel_id(),
|
||||
});
|
||||
self.leave_internal(cx)
|
||||
}
|
||||
|
||||
|
||||
@@ -222,6 +222,9 @@ impl ChannelChat {
|
||||
let message = ChannelMessage::from_proto(response, &user_store, &mut cx).await?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.insert_messages(SumTree::from_item(message, &()), cx);
|
||||
if this.first_loaded_message_id.is_none() {
|
||||
this.first_loaded_message_id = Some(id);
|
||||
}
|
||||
})?;
|
||||
Ok(id)
|
||||
}))
|
||||
|
||||
@@ -28,7 +28,7 @@ use release_channel::{AppVersion, ReleaseChannel};
|
||||
use rpc::proto::{AnyTypedEnvelope, EntityMessage, EnvelopedMessage, PeerId, RequestMessage};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsStore};
|
||||
use settings::{Settings, SettingsSources, SettingsStore};
|
||||
use std::fmt;
|
||||
use std::{
|
||||
any::TypeId,
|
||||
@@ -97,15 +97,8 @@ impl Settings for ClientSettings {
|
||||
|
||||
type FileContent = ClientSettingsContent;
|
||||
|
||||
fn load(
|
||||
default_value: &Self::FileContent,
|
||||
user_values: &[&Self::FileContent],
|
||||
_: &mut AppContext,
|
||||
) -> Result<Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
let mut result = Self::load_via_json_merge(default_value, user_values)?;
|
||||
fn load(sources: SettingsSources<Self::FileContent>, _: &mut AppContext) -> Result<Self> {
|
||||
let mut result = sources.json_merge::<Self>()?;
|
||||
if let Some(server_url) = &*ZED_SERVER_URL {
|
||||
result.server_url = server_url.clone()
|
||||
}
|
||||
@@ -427,21 +420,19 @@ impl settings::Settings for TelemetrySettings {
|
||||
|
||||
type FileContent = TelemetrySettingsContent;
|
||||
|
||||
fn load(
|
||||
default_value: &Self::FileContent,
|
||||
user_values: &[&Self::FileContent],
|
||||
_: &mut AppContext,
|
||||
) -> Result<Self> {
|
||||
fn load(sources: SettingsSources<Self::FileContent>, _: &mut AppContext) -> Result<Self> {
|
||||
Ok(Self {
|
||||
diagnostics: user_values.first().and_then(|v| v.diagnostics).unwrap_or(
|
||||
default_value
|
||||
diagnostics: sources.user.as_ref().and_then(|v| v.diagnostics).unwrap_or(
|
||||
sources
|
||||
.default
|
||||
.diagnostics
|
||||
.ok_or_else(Self::missing_default)?,
|
||||
),
|
||||
metrics: user_values
|
||||
.first()
|
||||
metrics: sources
|
||||
.user
|
||||
.as_ref()
|
||||
.and_then(|v| v.metrics)
|
||||
.unwrap_or(default_value.metrics.ok_or_else(Self::missing_default)?),
|
||||
.unwrap_or(sources.default.metrics.ok_or_else(Self::missing_default)?),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -790,7 +781,6 @@ impl Client {
|
||||
}
|
||||
Status::UpgradeRequired => return Err(EstablishConnectionError::UpgradeRequired)?,
|
||||
};
|
||||
|
||||
if was_disconnected {
|
||||
self.set_status(Status::Authenticating, cx);
|
||||
} else {
|
||||
|
||||
@@ -47,19 +47,6 @@ spec:
|
||||
metadata:
|
||||
labels:
|
||||
app: ${ZED_SERVICE_NAME}
|
||||
annotations:
|
||||
ad.datadoghq.com/collab.check_names: |
|
||||
["openmetrics"]
|
||||
ad.datadoghq.com/collab.init_configs: |
|
||||
[{}]
|
||||
ad.datadoghq.com/collab.instances: |
|
||||
[
|
||||
{
|
||||
"openmetrics_endpoint": "http://%%host%%:%%port%%/metrics",
|
||||
"namespace": "collab_${ZED_KUBE_NAMESPACE}",
|
||||
"metrics": [".*"]
|
||||
}
|
||||
]
|
||||
spec:
|
||||
containers:
|
||||
- name: ${ZED_SERVICE_NAME}
|
||||
|
||||
@@ -1866,6 +1866,24 @@ async fn test_active_call_events(
|
||||
executor.run_until_parked();
|
||||
assert_eq!(mem::take(&mut *events_a.borrow_mut()), vec![]);
|
||||
assert_eq!(mem::take(&mut *events_b.borrow_mut()), vec![]);
|
||||
|
||||
// Unsharing a project should dispatch the RemoteProjectUnshared event.
|
||||
active_call_a
|
||||
.update(cx_a, |call, cx| call.hang_up(cx))
|
||||
.await
|
||||
.unwrap();
|
||||
executor.run_until_parked();
|
||||
|
||||
assert_eq!(
|
||||
mem::take(&mut *events_a.borrow_mut()),
|
||||
vec![room::Event::RoomLeft { channel_id: None }]
|
||||
);
|
||||
assert_eq!(
|
||||
mem::take(&mut *events_b.borrow_mut()),
|
||||
vec![room::Event::RemoteProjectUnshared {
|
||||
project_id: project_a_id,
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
fn active_call_events(cx: &mut TestAppContext) -> Rc<RefCell<Vec<room::Event>>> {
|
||||
@@ -4641,9 +4659,16 @@ async fn test_references(
|
||||
let active_call_a = cx_a.read(ActiveCall::global);
|
||||
|
||||
client_a.language_registry().add(rust_lang());
|
||||
let mut fake_language_servers = client_a
|
||||
.language_registry()
|
||||
.register_fake_lsp_adapter("Rust", Default::default());
|
||||
let mut fake_language_servers = client_a.language_registry().register_fake_lsp_adapter(
|
||||
"Rust",
|
||||
FakeLspAdapter {
|
||||
capabilities: lsp::ServerCapabilities {
|
||||
references_provider: Some(lsp::OneOf::Left(true)),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
client_a
|
||||
.fs()
|
||||
|
||||
@@ -156,7 +156,7 @@ impl ChatPanel {
|
||||
}
|
||||
}
|
||||
}
|
||||
room::Event::Left { channel_id } => {
|
||||
room::Event::RoomLeft { channel_id } => {
|
||||
if channel_id == &this.channel_id(cx) {
|
||||
cx.emit(PanelEvent::Close)
|
||||
}
|
||||
@@ -615,6 +615,8 @@ impl ChatPanel {
|
||||
.child(
|
||||
IconButton::new(("reply", message_id), IconName::ReplyArrowRight)
|
||||
.on_click(cx.listener(move |this, _, cx| {
|
||||
this.cancel_edit_message(cx);
|
||||
|
||||
this.message_editor.update(cx, |editor, cx| {
|
||||
editor.set_reply_to_message_id(message_id);
|
||||
editor.focus_handle(cx).focus(cx);
|
||||
@@ -636,6 +638,8 @@ impl ChatPanel {
|
||||
IconButton::new(("edit", message_id), IconName::Pencil)
|
||||
.on_click(cx.listener(move |this, _, cx| {
|
||||
this.message_editor.update(cx, |editor, cx| {
|
||||
editor.clear_reply_to_message_id();
|
||||
|
||||
let message = this
|
||||
.active_chat()
|
||||
.and_then(|active_chat| {
|
||||
|
||||
@@ -9,12 +9,12 @@ use gpui::{
|
||||
Render, SharedString, Task, TextStyle, View, ViewContext, WeakView, WhiteSpace,
|
||||
};
|
||||
use language::{
|
||||
language_settings::SoftWrap, Anchor, Buffer, BufferSnapshot, CodeLabel, Completion,
|
||||
LanguageRegistry, LanguageServerId, ToOffset,
|
||||
language_settings::SoftWrap, Anchor, Buffer, BufferSnapshot, CodeLabel, LanguageRegistry,
|
||||
LanguageServerId, ToOffset,
|
||||
};
|
||||
use lazy_static::lazy_static;
|
||||
use parking_lot::RwLock;
|
||||
use project::search::SearchQuery;
|
||||
use project::{search::SearchQuery, Completion};
|
||||
use settings::Settings;
|
||||
use std::{ops::Range, sync::Arc, time::Duration};
|
||||
use theme::ThemeSettings;
|
||||
@@ -48,7 +48,7 @@ impl CompletionProvider for MessageEditorCompletionProvider {
|
||||
buffer: &Model<Buffer>,
|
||||
buffer_position: language::Anchor,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) -> Task<anyhow::Result<Vec<language::Completion>>> {
|
||||
) -> Task<anyhow::Result<Vec<Completion>>> {
|
||||
let Some(handle) = self.0.upgrade() else {
|
||||
return Task::ready(Ok(Vec::new()));
|
||||
};
|
||||
@@ -60,7 +60,7 @@ impl CompletionProvider for MessageEditorCompletionProvider {
|
||||
fn resolve_completions(
|
||||
&self,
|
||||
_completion_indices: Vec<usize>,
|
||||
_completions: Arc<RwLock<Box<[language::Completion]>>>,
|
||||
_completions: Arc<RwLock<Box<[Completion]>>>,
|
||||
_cx: &mut ViewContext<Editor>,
|
||||
) -> Task<anyhow::Result<bool>> {
|
||||
Task::ready(Ok(false))
|
||||
|
||||
@@ -58,7 +58,7 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
|
||||
}
|
||||
}
|
||||
|
||||
room::Event::Left { .. } => {
|
||||
room::Event::RoomLeft { .. } => {
|
||||
for (_, windows) in notification_windows.drain() {
|
||||
for window in windows {
|
||||
window
|
||||
|
||||
@@ -2,7 +2,7 @@ use anyhow;
|
||||
use gpui::Pixels;
|
||||
use schemars::JsonSchema;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use settings::Settings;
|
||||
use settings::{Settings, SettingsSources};
|
||||
use workspace::dock::DockPosition;
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
@@ -53,48 +53,52 @@ pub struct MessageEditorSettings {
|
||||
|
||||
impl Settings for CollaborationPanelSettings {
|
||||
const KEY: Option<&'static str> = Some("collaboration_panel");
|
||||
|
||||
type FileContent = PanelSettingsContent;
|
||||
|
||||
fn load(
|
||||
default_value: &Self::FileContent,
|
||||
user_values: &[&Self::FileContent],
|
||||
sources: SettingsSources<Self::FileContent>,
|
||||
_: &mut gpui::AppContext,
|
||||
) -> anyhow::Result<Self> {
|
||||
Self::load_via_json_merge(default_value, user_values)
|
||||
sources.json_merge()
|
||||
}
|
||||
}
|
||||
|
||||
impl Settings for ChatPanelSettings {
|
||||
const KEY: Option<&'static str> = Some("chat_panel");
|
||||
|
||||
type FileContent = PanelSettingsContent;
|
||||
|
||||
fn load(
|
||||
default_value: &Self::FileContent,
|
||||
user_values: &[&Self::FileContent],
|
||||
sources: SettingsSources<Self::FileContent>,
|
||||
_: &mut gpui::AppContext,
|
||||
) -> anyhow::Result<Self> {
|
||||
Self::load_via_json_merge(default_value, user_values)
|
||||
sources.json_merge()
|
||||
}
|
||||
}
|
||||
|
||||
impl Settings for NotificationPanelSettings {
|
||||
const KEY: Option<&'static str> = Some("notification_panel");
|
||||
|
||||
type FileContent = PanelSettingsContent;
|
||||
|
||||
fn load(
|
||||
default_value: &Self::FileContent,
|
||||
user_values: &[&Self::FileContent],
|
||||
sources: SettingsSources<Self::FileContent>,
|
||||
_: &mut gpui::AppContext,
|
||||
) -> anyhow::Result<Self> {
|
||||
Self::load_via_json_merge(default_value, user_values)
|
||||
sources.json_merge()
|
||||
}
|
||||
}
|
||||
|
||||
impl Settings for MessageEditorSettings {
|
||||
const KEY: Option<&'static str> = Some("message_editor");
|
||||
|
||||
type FileContent = MessageEditorSettings;
|
||||
|
||||
fn load(
|
||||
default_value: &Self::FileContent,
|
||||
user_values: &[&Self::FileContent],
|
||||
sources: SettingsSources<Self::FileContent>,
|
||||
_: &mut gpui::AppContext,
|
||||
) -> anyhow::Result<Self> {
|
||||
Self::load_via_json_merge(default_value, user_values)
|
||||
sources.json_merge()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
use anyhow::Result;
|
||||
use gpui::AppContext;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsSources};
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct ProjectDiagnosticsSettings {
|
||||
@@ -15,18 +18,11 @@ pub struct ProjectDiagnosticsSettingsContent {
|
||||
include_warnings: Option<bool>,
|
||||
}
|
||||
|
||||
impl settings::Settings for ProjectDiagnosticsSettings {
|
||||
impl Settings for ProjectDiagnosticsSettings {
|
||||
const KEY: Option<&'static str> = Some("diagnostics");
|
||||
type FileContent = ProjectDiagnosticsSettingsContent;
|
||||
|
||||
fn load(
|
||||
default_value: &Self::FileContent,
|
||||
user_values: &[&Self::FileContent],
|
||||
_cx: &mut gpui::AppContext,
|
||||
) -> anyhow::Result<Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
Self::load_via_json_merge(default_value, user_values)
|
||||
fn load(sources: SettingsSources<Self::FileContent>, _: &mut AppContext) -> Result<Self> {
|
||||
sources.json_merge()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,7 +157,7 @@ impl DisplayMap {
|
||||
);
|
||||
}
|
||||
|
||||
pub fn fold<T: ToOffset>(
|
||||
pub fn fold<T: ToOffset<MultiBuffer>>(
|
||||
&mut self,
|
||||
ranges: impl IntoIterator<Item = Range<T>>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
@@ -180,7 +180,7 @@ impl DisplayMap {
|
||||
self.block_map.read(snapshot, edits);
|
||||
}
|
||||
|
||||
pub fn unfold<T: ToOffset>(
|
||||
pub fn unfold<T: ToOffset<MultiBuffer>>(
|
||||
&mut self,
|
||||
ranges: impl IntoIterator<Item = Range<T>>,
|
||||
inclusive: bool,
|
||||
@@ -433,7 +433,10 @@ impl DisplaySnapshot {
|
||||
} else if range.start.row == self.max_buffer_row()
|
||||
|| (range.end.column > 0 && range.end.row == self.max_buffer_row())
|
||||
{
|
||||
Point::new(range.start.row - 1, self.line_len(range.start.row - 1))
|
||||
Point::new(
|
||||
range.start.row - 1,
|
||||
self.buffer_snapshot.line_len(range.start.row - 1),
|
||||
)
|
||||
} else {
|
||||
self.prev_line_boundary(range.start).0
|
||||
};
|
||||
@@ -690,7 +693,10 @@ impl DisplaySnapshot {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn buffer_chars_at(&self, mut offset: usize) -> impl Iterator<Item = (char, usize)> + '_ {
|
||||
pub fn buffer_chars_at(
|
||||
&self,
|
||||
mut offset: Offset<MultiBuffer>,
|
||||
) -> impl Iterator<Item = (char, usize)> + '_ {
|
||||
self.buffer_snapshot.chars_at(offset).map(move |ch| {
|
||||
let ret = (ch, offset);
|
||||
offset += ch.len_utf8();
|
||||
@@ -700,7 +706,7 @@ impl DisplaySnapshot {
|
||||
|
||||
pub fn reverse_buffer_chars_at(
|
||||
&self,
|
||||
mut offset: usize,
|
||||
mut offset: Offset<MultiBuffer>,
|
||||
) -> impl Iterator<Item = (char, usize)> + '_ {
|
||||
self.buffer_snapshot
|
||||
.reversed_chars_at(offset)
|
||||
@@ -729,7 +735,7 @@ impl DisplaySnapshot {
|
||||
|
||||
pub fn folds_in_range<T>(&self, range: Range<T>) -> impl Iterator<Item = &Fold>
|
||||
where
|
||||
T: ToOffset,
|
||||
T: ToOffset<MultiBuffer>,
|
||||
{
|
||||
self.fold_snapshot.folds_in_range(range)
|
||||
}
|
||||
@@ -741,7 +747,7 @@ impl DisplaySnapshot {
|
||||
self.block_snapshot.blocks_in_range(rows)
|
||||
}
|
||||
|
||||
pub fn intersects_fold<T: ToOffset>(&self, offset: T) -> bool {
|
||||
pub fn intersects_fold<T: ToOffset<MultiBuffer>>(&self, offset: T) -> bool {
|
||||
self.fold_snapshot.intersects_fold(offset)
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ use super::{
|
||||
};
|
||||
use gpui::{ElementId, HighlightStyle, Hsla};
|
||||
use language::{Chunk, Edit, Point, TextSummary};
|
||||
use multi_buffer::{Anchor, AnchorRangeExt, MultiBufferSnapshot, ToOffset};
|
||||
use multi_buffer::{Anchor, AnchorRangeExt, MultiBuffer, MultiBufferSnapshot, ToOffset};
|
||||
use std::{
|
||||
any::TypeId,
|
||||
cmp::{self, Ordering},
|
||||
@@ -74,7 +74,7 @@ impl<'a> sum_tree::Dimension<'a, TransformSummary> for FoldPoint {
|
||||
pub(crate) struct FoldMapWriter<'a>(&'a mut FoldMap);
|
||||
|
||||
impl<'a> FoldMapWriter<'a> {
|
||||
pub(crate) fn fold<T: ToOffset>(
|
||||
pub(crate) fn fold<T: ToOffset<MultiBuffer>>(
|
||||
&mut self,
|
||||
ranges: impl IntoIterator<Item = Range<T>>,
|
||||
) -> (FoldSnapshot, Vec<FoldEdit>) {
|
||||
@@ -129,7 +129,7 @@ impl<'a> FoldMapWriter<'a> {
|
||||
(self.0.snapshot.clone(), edits)
|
||||
}
|
||||
|
||||
pub(crate) fn unfold<T: ToOffset>(
|
||||
pub(crate) fn unfold<T: ToOffset<MultiBuffer>>(
|
||||
&mut self,
|
||||
ranges: impl IntoIterator<Item = Range<T>>,
|
||||
inclusive: bool,
|
||||
@@ -609,7 +609,7 @@ impl FoldSnapshot {
|
||||
|
||||
pub fn folds_in_range<T>(&self, range: Range<T>) -> impl Iterator<Item = &Fold>
|
||||
where
|
||||
T: ToOffset,
|
||||
T: ToOffset<MultiBuffer>,
|
||||
{
|
||||
let mut folds = intersecting_folds(&self.inlay_snapshot, &self.folds, range, false);
|
||||
iter::from_fn(move || {
|
||||
@@ -621,7 +621,7 @@ impl FoldSnapshot {
|
||||
|
||||
pub fn intersects_fold<T>(&self, offset: T) -> bool
|
||||
where
|
||||
T: ToOffset,
|
||||
T: ToOffset<MultiBuffer>,
|
||||
{
|
||||
let buffer_offset = offset.to_offset(&self.inlay_snapshot.buffer);
|
||||
let inlay_offset = self.inlay_snapshot.to_inlay_offset(buffer_offset);
|
||||
@@ -741,7 +741,7 @@ fn intersecting_folds<'a, T>(
|
||||
inclusive: bool,
|
||||
) -> FilterCursor<'a, impl 'a + FnMut(&FoldSummary) -> bool, Fold, usize>
|
||||
where
|
||||
T: ToOffset,
|
||||
T: ToOffset<MultiBuffer>,
|
||||
{
|
||||
let buffer = &inlay_snapshot.buffer;
|
||||
let start = buffer.anchor_before(range.start.to_offset(buffer));
|
||||
|
||||
@@ -74,12 +74,12 @@ use inlay_hint_cache::{InlayHintCache, InlaySplice, InvalidationStrategy};
|
||||
pub use inline_completion_provider::*;
|
||||
pub use items::MAX_TAB_TITLE_LEN;
|
||||
use itertools::Itertools;
|
||||
use language::{char_kind, CharKind};
|
||||
use language::{
|
||||
char_kind,
|
||||
language_settings::{self, all_language_settings, InlayHintSettings},
|
||||
markdown, point_from_lsp, AutoindentMode, BracketPair, Buffer, Capability, CodeAction,
|
||||
CodeLabel, Completion, CursorShape, Diagnostic, Documentation, IndentKind, IndentSize,
|
||||
Language, OffsetRangeExt, Point, Selection, SelectionGoal, TransactionId,
|
||||
markdown, point_from_lsp, AutoindentMode, BracketPair, Buffer, Capability, CharKind, CodeLabel,
|
||||
CursorShape, Diagnostic, Documentation, IndentKind, IndentSize, Language, OffsetRangeExt,
|
||||
Point, Selection, SelectionGoal, TransactionId,
|
||||
};
|
||||
|
||||
use hover_links::{HoverLink, HoveredLinkState, InlayHighlight};
|
||||
@@ -94,7 +94,9 @@ pub use multi_buffer::{
|
||||
use ordered_float::OrderedFloat;
|
||||
use parking_lot::{Mutex, RwLock};
|
||||
use project::project_settings::{GitGutterSetting, ProjectSettings};
|
||||
use project::{FormatTrigger, Item, Location, Project, ProjectPath, ProjectTransaction};
|
||||
use project::{
|
||||
CodeAction, Completion, FormatTrigger, Item, Location, Project, ProjectPath, ProjectTransaction,
|
||||
};
|
||||
use rand::prelude::*;
|
||||
use rpc::proto::*;
|
||||
use scroll::{Autoscroll, OngoingScroll, ScrollAnchor, ScrollManager, ScrollbarAutoHide};
|
||||
@@ -1668,11 +1670,15 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn language_at<T: ToOffset>(&self, point: T, cx: &AppContext) -> Option<Arc<Language>> {
|
||||
pub fn language_at<T: ToOffset<MultiBuffer>>(
|
||||
&self,
|
||||
point: T,
|
||||
cx: &AppContext,
|
||||
) -> Option<Arc<Language>> {
|
||||
self.buffer.read(cx).language_at(point, cx)
|
||||
}
|
||||
|
||||
pub fn file_at<T: ToOffset>(
|
||||
pub fn file_at<T: ToOffset<MultiBuffer>>(
|
||||
&self,
|
||||
point: T,
|
||||
cx: &AppContext,
|
||||
@@ -1961,7 +1967,7 @@ impl Editor {
|
||||
pub fn edit<I, S, T>(&mut self, edits: I, cx: &mut ViewContext<Self>)
|
||||
where
|
||||
I: IntoIterator<Item = (Range<S>, T)>,
|
||||
S: ToOffset,
|
||||
S: ToOffset<MultiBuffer>,
|
||||
T: Into<Arc<str>>,
|
||||
{
|
||||
if self.read_only(cx) {
|
||||
@@ -1975,7 +1981,7 @@ impl Editor {
|
||||
pub fn edit_with_autoindent<I, S, T>(&mut self, edits: I, cx: &mut ViewContext<Self>)
|
||||
where
|
||||
I: IntoIterator<Item = (Range<S>, T)>,
|
||||
S: ToOffset,
|
||||
S: ToOffset<MultiBuffer>,
|
||||
T: Into<Arc<str>>,
|
||||
{
|
||||
if self.read_only(cx) {
|
||||
@@ -1994,7 +2000,7 @@ impl Editor {
|
||||
cx: &mut ViewContext<Self>,
|
||||
) where
|
||||
I: IntoIterator<Item = (Range<S>, T)>,
|
||||
S: ToOffset,
|
||||
S: ToOffset<MultiBuffer>,
|
||||
T: Into<Arc<str>>,
|
||||
{
|
||||
if self.read_only(cx) {
|
||||
@@ -3065,7 +3071,7 @@ impl Editor {
|
||||
/// Iterate the given selections, and for each one, find the smallest surrounding
|
||||
/// autoclose region. This uses the ordering of the selections and the autoclose
|
||||
/// regions to avoid repeated comparisons.
|
||||
fn selections_with_autoclose_regions<'a, D: ToOffset + Clone>(
|
||||
fn selections_with_autoclose_regions<'a, D: ToOffset<MultiBuffer> + Clone>(
|
||||
&'a self,
|
||||
selections: impl IntoIterator<Item = Selection<D>>,
|
||||
buffer: &'a MultiBufferSnapshot,
|
||||
@@ -3120,7 +3126,10 @@ impl Editor {
|
||||
});
|
||||
}
|
||||
|
||||
fn completion_query(buffer: &MultiBufferSnapshot, position: impl ToOffset) -> Option<String> {
|
||||
fn completion_query(
|
||||
buffer: &MultiBufferSnapshot,
|
||||
position: impl ToOffset<MultiBuffer>,
|
||||
) -> Option<String> {
|
||||
let offset = position.to_offset(buffer);
|
||||
let (word_range, kind) = buffer.surrounding_word(offset);
|
||||
if offset > word_range.start && kind == Some(CharKind::Word) {
|
||||
@@ -8608,7 +8617,7 @@ impl Editor {
|
||||
self.fold_ranges(ranges, true, cx);
|
||||
}
|
||||
|
||||
pub fn fold_ranges<T: ToOffset + Clone>(
|
||||
pub fn fold_ranges<T: ToOffset<MultiBuffer> + Clone>(
|
||||
&mut self,
|
||||
ranges: impl IntoIterator<Item = Range<T>>,
|
||||
auto_scroll: bool,
|
||||
@@ -8626,7 +8635,7 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn unfold_ranges<T: ToOffset + Clone>(
|
||||
pub fn unfold_ranges<T: ToOffset<MultiBuffer> + Clone>(
|
||||
&mut self,
|
||||
ranges: impl IntoIterator<Item = Range<T>>,
|
||||
inclusive: bool,
|
||||
@@ -10017,7 +10026,7 @@ impl EditorSnapshot {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn language_at<T: ToOffset>(&self, position: T) -> Option<&Arc<Language>> {
|
||||
pub fn language_at<T: ToOffset<MultiBuffer>>(&self, position: T) -> Option<&Arc<Language>> {
|
||||
self.display_snapshot.buffer_snapshot.language_at(position)
|
||||
}
|
||||
|
||||
@@ -10198,7 +10207,7 @@ impl Render for Editor {
|
||||
background,
|
||||
local_player: cx.theme().players().local(),
|
||||
text: text_style,
|
||||
scrollbar_width: px(12.),
|
||||
scrollbar_width: px(13.),
|
||||
syntax: cx.theme().syntax().clone(),
|
||||
status: cx.theme().status().clone(),
|
||||
inlay_hints_style: HighlightStyle {
|
||||
@@ -10480,7 +10489,7 @@ trait SelectionExt {
|
||||
-> Range<u32>;
|
||||
}
|
||||
|
||||
impl<T: ToPoint + ToOffset> SelectionExt for Selection<T> {
|
||||
impl<T: ToPoint + ToOffset<MultiBuffer>> SelectionExt for Selection<T> {
|
||||
fn point_range(&self, buffer: &MultiBufferSnapshot) -> Range<Point> {
|
||||
let start = self.start.to_point(buffer);
|
||||
let end = self.end.to_point(buffer);
|
||||
@@ -10537,7 +10546,7 @@ impl<T: ToPoint + ToOffset> SelectionExt for Selection<T> {
|
||||
impl<T: InvalidationRegion> InvalidationStack<T> {
|
||||
fn invalidate<S>(&mut self, selections: &[Selection<S>], buffer: &MultiBufferSnapshot)
|
||||
where
|
||||
S: Clone + ToOffset,
|
||||
S: Clone + ToOffset<MultiBuffer>,
|
||||
{
|
||||
while let Some(region) = self.last() {
|
||||
let all_selections_inside_invalidation_ranges =
|
||||
@@ -10778,7 +10787,7 @@ trait RangeToAnchorExt {
|
||||
fn to_anchors(self, snapshot: &MultiBufferSnapshot) -> Range<Anchor>;
|
||||
}
|
||||
|
||||
impl<T: ToOffset> RangeToAnchorExt for Range<T> {
|
||||
impl<T: ToOffset<MultiBuffer>> RangeToAnchorExt for Range<T> {
|
||||
fn to_anchors(self, snapshot: &MultiBufferSnapshot) -> Range<Anchor> {
|
||||
snapshot.anchor_after(self.start)..snapshot.anchor_before(self.end)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use gpui::AppContext;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::Settings;
|
||||
use settings::{Settings, SettingsSources};
|
||||
|
||||
#[derive(Deserialize, Clone)]
|
||||
pub struct EditorSettings {
|
||||
@@ -224,10 +225,9 @@ impl Settings for EditorSettings {
|
||||
type FileContent = EditorSettingsContent;
|
||||
|
||||
fn load(
|
||||
default_value: &Self::FileContent,
|
||||
user_values: &[&Self::FileContent],
|
||||
_: &mut gpui::AppContext,
|
||||
sources: SettingsSources<Self::FileContent>,
|
||||
_: &mut AppContext,
|
||||
) -> anyhow::Result<Self> {
|
||||
Self::load_via_json_merge(default_value, user_values)
|
||||
sources.json_merge()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2685,6 +2685,65 @@ fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_join_lines_with_git_diff_base(
|
||||
executor: BackgroundExecutor,
|
||||
cx: &mut gpui::TestAppContext,
|
||||
) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
|
||||
let diff_base = r#"
|
||||
Line 0
|
||||
Line 1
|
||||
Line 2
|
||||
Line 3
|
||||
"#
|
||||
.unindent();
|
||||
|
||||
cx.set_state(
|
||||
&r#"
|
||||
ˇLine 0
|
||||
Line 1
|
||||
Line 2
|
||||
Line 3
|
||||
"#
|
||||
.unindent(),
|
||||
);
|
||||
|
||||
cx.set_diff_base(Some(&diff_base));
|
||||
executor.run_until_parked();
|
||||
|
||||
// Join lines
|
||||
cx.update_editor(|editor, cx| {
|
||||
editor.join_lines(&JoinLines, cx);
|
||||
});
|
||||
executor.run_until_parked();
|
||||
|
||||
cx.assert_editor_state(
|
||||
&r#"
|
||||
Line 0ˇ Line 1
|
||||
Line 2
|
||||
Line 3
|
||||
"#
|
||||
.unindent(),
|
||||
);
|
||||
// Join again
|
||||
cx.update_editor(|editor, cx| {
|
||||
editor.join_lines(&JoinLines, cx);
|
||||
});
|
||||
executor.run_until_parked();
|
||||
|
||||
cx.assert_editor_state(
|
||||
&r#"
|
||||
Line 0 Line 1ˇ Line 2
|
||||
Line 3
|
||||
"#
|
||||
.unindent(),
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_manipulate_lines_with_single_selection(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
@@ -884,13 +884,21 @@ impl EditorElement {
|
||||
SharedString::from(character.to_string())
|
||||
};
|
||||
let len = text.len();
|
||||
|
||||
let font = cursor_row_layout
|
||||
.font_id_for_index(cursor_column)
|
||||
.and_then(|cursor_font_id| {
|
||||
cx.text_system().get_font_for_id(cursor_font_id)
|
||||
})
|
||||
.unwrap_or(self.style.text.font());
|
||||
|
||||
cx.text_system()
|
||||
.shape_line(
|
||||
text,
|
||||
cursor_row_layout.font_size,
|
||||
&[TextRun {
|
||||
len,
|
||||
font: self.style.text.font(),
|
||||
font: font,
|
||||
color: self.style.background,
|
||||
background_color: None,
|
||||
strikethrough: None,
|
||||
@@ -3875,10 +3883,7 @@ impl ScrollbarLayout {
|
||||
.into_iter()
|
||||
.map(|range| {
|
||||
let start_y = self.first_row_y_offset + range.start as f32 * self.row_height;
|
||||
let mut end_y = self.first_row_y_offset + (range.end + 1) as f32 * self.row_height;
|
||||
if end_y - start_y < Self::MIN_MARKER_HEIGHT {
|
||||
end_y = start_y + Self::MIN_MARKER_HEIGHT;
|
||||
}
|
||||
let end_y = self.first_row_y_offset + (range.end + 1) as f32 * self.row_height;
|
||||
ColoredRange {
|
||||
start: start_y,
|
||||
end: end_y,
|
||||
@@ -3889,11 +3894,14 @@ impl ScrollbarLayout {
|
||||
|
||||
let mut quads = Vec::new();
|
||||
while let Some(mut pixel_range) = background_pixel_ranges.next() {
|
||||
pixel_range.end = pixel_range
|
||||
.end
|
||||
.max(pixel_range.start + Self::MIN_MARKER_HEIGHT);
|
||||
while let Some(next_pixel_range) = background_pixel_ranges.peek() {
|
||||
if pixel_range.end >= next_pixel_range.start
|
||||
&& pixel_range.color == next_pixel_range.color
|
||||
{
|
||||
pixel_range.end = next_pixel_range.end;
|
||||
pixel_range.end = next_pixel_range.end.max(pixel_range.end);
|
||||
background_pixel_ranges.next();
|
||||
} else {
|
||||
break;
|
||||
|
||||
@@ -375,12 +375,12 @@ async fn parse_blocks(
|
||||
match &block.kind {
|
||||
HoverBlockKind::PlainText => {
|
||||
markdown::new_paragraph(&mut text, &mut Vec::new());
|
||||
text.push_str(&block.text);
|
||||
text.push_str(&block.text.replace("\\n", "\n"));
|
||||
}
|
||||
|
||||
HoverBlockKind::Markdown => {
|
||||
markdown::parse_markdown_block(
|
||||
&block.text,
|
||||
&block.text.replace("\\n", "\n"),
|
||||
language_registry,
|
||||
language.clone(),
|
||||
&mut text,
|
||||
|
||||
@@ -705,31 +705,38 @@ impl Item for Editor {
|
||||
.await?;
|
||||
}
|
||||
|
||||
// Only format and save the buffers with changes. For clean buffers,
|
||||
// we simulate saving by calling `Buffer::did_save`, so that language servers or
|
||||
// other downstream listeners of save events get notified.
|
||||
let (dirty_buffers, clean_buffers) = buffers.into_iter().partition(|buffer| {
|
||||
buffer
|
||||
.update(&mut cx, |buffer, _| {
|
||||
buffer.is_dirty() || buffer.has_conflict()
|
||||
})
|
||||
.unwrap_or(false)
|
||||
});
|
||||
if buffers.len() == 1 {
|
||||
// Apply full save routine for singleton buffers, to allow to `touch` the file via the editor.
|
||||
project
|
||||
.update(&mut cx, |project, cx| project.save_buffers(buffers, cx))?
|
||||
.await?;
|
||||
} else {
|
||||
// For multi-buffers, only format and save the buffers with changes.
|
||||
// For clean buffers, we simulate saving by calling `Buffer::did_save`,
|
||||
// so that language servers or other downstream listeners of save events get notified.
|
||||
let (dirty_buffers, clean_buffers) = buffers.into_iter().partition(|buffer| {
|
||||
buffer
|
||||
.update(&mut cx, |buffer, _| {
|
||||
buffer.is_dirty() || buffer.has_conflict()
|
||||
})
|
||||
.unwrap_or(false)
|
||||
});
|
||||
|
||||
project
|
||||
.update(&mut cx, |project, cx| {
|
||||
project.save_buffers(dirty_buffers, cx)
|
||||
})?
|
||||
.await?;
|
||||
for buffer in clean_buffers {
|
||||
buffer
|
||||
.update(&mut cx, |buffer, cx| {
|
||||
let version = buffer.saved_version().clone();
|
||||
let fingerprint = buffer.saved_version_fingerprint();
|
||||
let mtime = buffer.saved_mtime();
|
||||
buffer.did_save(version, fingerprint, mtime, cx);
|
||||
})
|
||||
.ok();
|
||||
project
|
||||
.update(&mut cx, |project, cx| {
|
||||
project.save_buffers(dirty_buffers, cx)
|
||||
})?
|
||||
.await?;
|
||||
for buffer in clean_buffers {
|
||||
buffer
|
||||
.update(&mut cx, |buffer, cx| {
|
||||
let version = buffer.saved_version().clone();
|
||||
let fingerprint = buffer.saved_version_fingerprint();
|
||||
let mtime = buffer.saved_mtime();
|
||||
buffer.did_save(version, fingerprint, mtime, cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -5,14 +5,15 @@ use super::{Bias, DisplayPoint, DisplaySnapshot, SelectionGoal, ToDisplayPoint};
|
||||
use crate::{char_kind, scroll::ScrollAnchor, CharKind, EditorStyle, ToOffset, ToPoint};
|
||||
use gpui::{px, Pixels, WindowTextSystem};
|
||||
use language::Point;
|
||||
use multi_buffer::MultiBufferSnapshot;
|
||||
use multi_buffer::{MultiBufferSnapshot, Offset};
|
||||
use serde::Deserialize;
|
||||
|
||||
use std::{ops::Range, sync::Arc};
|
||||
|
||||
/// Defines search strategy for items in `movement` module.
|
||||
/// `FindRange::SingeLine` only looks for a match on a single line at a time, whereas
|
||||
/// `FindRange::MultiLine` keeps going until the end of a string.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize)]
|
||||
pub enum FindRange {
|
||||
SingleLine,
|
||||
MultiLine,
|
||||
@@ -471,8 +472,8 @@ pub fn find_boundary_exclusive(
|
||||
/// the [`DisplaySnapshot`]. The offsets are relative to the start of a buffer.
|
||||
pub fn chars_after(
|
||||
map: &DisplaySnapshot,
|
||||
mut offset: usize,
|
||||
) -> impl Iterator<Item = (char, Range<usize>)> + '_ {
|
||||
mut offset: Offset<multi_buffer::MultiBuffer>,
|
||||
) -> impl Iterator<Item = (char, Range<Offset<multi_buffer::MultiBuffer>>)> + '_ {
|
||||
map.buffer_snapshot.chars_at(offset).map(move |ch| {
|
||||
let before = offset;
|
||||
offset = offset + ch.len_utf8();
|
||||
@@ -485,13 +486,13 @@ pub fn chars_after(
|
||||
/// the [`DisplaySnapshot`]. The offsets are relative to the start of a buffer.
|
||||
pub fn chars_before(
|
||||
map: &DisplaySnapshot,
|
||||
mut offset: usize,
|
||||
) -> impl Iterator<Item = (char, Range<usize>)> + '_ {
|
||||
mut offset: Offset<multi_buffer::MultiBuffer>,
|
||||
) -> impl Iterator<Item = (char, Range<Offset<multi_buffer::MultiBuffer>>)> + '_ {
|
||||
map.buffer_snapshot
|
||||
.reversed_chars_at(offset)
|
||||
.map(move |ch| {
|
||||
let after = offset;
|
||||
offset = offset - ch.len_utf8();
|
||||
offset = Offset::new(offset.0 - ch.len_utf8());
|
||||
(ch, offset..after)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -490,7 +490,13 @@ impl<'a> MutableSelectionsCollection<'a> {
|
||||
|
||||
pub fn insert_range<T>(&mut self, range: Range<T>)
|
||||
where
|
||||
T: 'a + ToOffset + ToPoint + TextDimension + Ord + Sub<T, Output = T> + std::marker::Copy,
|
||||
T: 'a
|
||||
+ ToOffset<multi_buffer::MultiBuffer>
|
||||
+ ToPoint
|
||||
+ TextDimension
|
||||
+ Ord
|
||||
+ Sub<T, Output = T>
|
||||
+ std::marker::Copy,
|
||||
{
|
||||
let mut selections = self.all(self.cx);
|
||||
let mut start = range.start.to_offset(&self.buffer());
|
||||
@@ -513,7 +519,11 @@ impl<'a> MutableSelectionsCollection<'a> {
|
||||
|
||||
pub fn select<T>(&mut self, mut selections: Vec<Selection<T>>)
|
||||
where
|
||||
T: ToOffset + ToPoint + Ord + std::marker::Copy + std::fmt::Debug,
|
||||
T: ToOffset<multi_buffer::MultiBuffer>
|
||||
+ ToPoint
|
||||
+ Ord
|
||||
+ std::marker::Copy
|
||||
+ std::fmt::Debug,
|
||||
{
|
||||
let buffer = self.buffer.read(self.cx).snapshot(self.cx);
|
||||
selections.sort_unstable_by_key(|s| s.start);
|
||||
@@ -562,7 +572,7 @@ impl<'a> MutableSelectionsCollection<'a> {
|
||||
pub fn select_ranges<I, T>(&mut self, ranges: I)
|
||||
where
|
||||
I: IntoIterator<Item = Range<T>>,
|
||||
T: ToOffset,
|
||||
T: ToOffset<multi_buffer::MultiBuffer>,
|
||||
{
|
||||
let buffer = self.buffer.read(self.cx).snapshot(self.cx);
|
||||
let ranges = ranges
|
||||
|
||||
@@ -1,21 +1,31 @@
|
||||
use crate::wasm_host::{wit::LanguageServerConfig, WasmExtension, WasmHost};
|
||||
use crate::wasm_host::{
|
||||
wit::{self, LanguageServerConfig},
|
||||
WasmExtension, WasmHost,
|
||||
};
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use async_trait::async_trait;
|
||||
use collections::HashMap;
|
||||
use futures::{Future, FutureExt};
|
||||
use gpui::AsyncAppContext;
|
||||
use language::{Language, LanguageServerName, LspAdapter, LspAdapterDelegate};
|
||||
use language::{
|
||||
CodeLabel, HighlightId, Language, LanguageServerName, LspAdapter, LspAdapterDelegate,
|
||||
};
|
||||
use lsp::LanguageServerBinary;
|
||||
use serde::Serialize;
|
||||
use serde_json::Value;
|
||||
use std::ops::Range;
|
||||
use std::{
|
||||
any::Any,
|
||||
path::{Path, PathBuf},
|
||||
pin::Pin,
|
||||
sync::Arc,
|
||||
};
|
||||
use util::{maybe, ResultExt};
|
||||
use wasmtime_wasi::WasiView as _;
|
||||
|
||||
pub struct ExtensionLspAdapter {
|
||||
pub(crate) extension: WasmExtension,
|
||||
pub(crate) language_server_id: LanguageServerName,
|
||||
pub(crate) config: LanguageServerConfig,
|
||||
pub(crate) host: Arc<WasmHost>,
|
||||
}
|
||||
@@ -43,7 +53,12 @@ impl LspAdapter for ExtensionLspAdapter {
|
||||
async move {
|
||||
let resource = store.data_mut().table().push(delegate)?;
|
||||
let command = extension
|
||||
.call_language_server_command(store, &this.config, resource)
|
||||
.call_language_server_command(
|
||||
store,
|
||||
&this.language_server_id,
|
||||
&this.config,
|
||||
resource,
|
||||
)
|
||||
.await?
|
||||
.map_err(|e| anyhow!("{}", e))?;
|
||||
anyhow::Ok(command)
|
||||
@@ -146,6 +161,7 @@ impl LspAdapter for ExtensionLspAdapter {
|
||||
let options = extension
|
||||
.call_language_server_initialization_options(
|
||||
store,
|
||||
&this.language_server_id,
|
||||
&this.config,
|
||||
resource,
|
||||
)
|
||||
@@ -165,4 +181,394 @@ impl LspAdapter for ExtensionLspAdapter {
|
||||
None
|
||||
})
|
||||
}
|
||||
|
||||
async fn workspace_configuration(
|
||||
self: Arc<Self>,
|
||||
delegate: &Arc<dyn LspAdapterDelegate>,
|
||||
_cx: &mut AsyncAppContext,
|
||||
) -> Result<Value> {
|
||||
let delegate = delegate.clone();
|
||||
let json_options: Option<String> = self
|
||||
.extension
|
||||
.call({
|
||||
let this = self.clone();
|
||||
|extension, store| {
|
||||
async move {
|
||||
let resource = store.data_mut().table().push(delegate)?;
|
||||
let options = extension
|
||||
.call_language_server_workspace_configuration(
|
||||
store,
|
||||
&this.language_server_id,
|
||||
resource,
|
||||
)
|
||||
.await?
|
||||
.map_err(|e| anyhow!("{}", e))?;
|
||||
anyhow::Ok(options)
|
||||
}
|
||||
.boxed()
|
||||
}
|
||||
})
|
||||
.await?;
|
||||
Ok(if let Some(json_options) = json_options {
|
||||
serde_json::from_str(&json_options).with_context(|| {
|
||||
format!("failed to parse initialization_options from extension: {json_options}")
|
||||
})?
|
||||
} else {
|
||||
serde_json::json!({})
|
||||
})
|
||||
}
|
||||
|
||||
async fn labels_for_completions(
|
||||
self: Arc<Self>,
|
||||
completions: &[lsp::CompletionItem],
|
||||
language: &Arc<Language>,
|
||||
) -> Result<Vec<Option<CodeLabel>>> {
|
||||
let completions = completions
|
||||
.into_iter()
|
||||
.map(|completion| wit::Completion::from(completion.clone()))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let labels = self
|
||||
.extension
|
||||
.call({
|
||||
let this = self.clone();
|
||||
|extension, store| {
|
||||
async move {
|
||||
extension
|
||||
.call_labels_for_completions(
|
||||
store,
|
||||
&this.language_server_id,
|
||||
completions,
|
||||
)
|
||||
.await?
|
||||
.map_err(|e| anyhow!("{}", e))
|
||||
}
|
||||
.boxed()
|
||||
}
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(labels_from_wit(labels, language))
|
||||
}
|
||||
|
||||
async fn labels_for_symbols(
|
||||
self: Arc<Self>,
|
||||
symbols: &[(String, lsp::SymbolKind)],
|
||||
language: &Arc<Language>,
|
||||
) -> Result<Vec<Option<CodeLabel>>> {
|
||||
let symbols = symbols
|
||||
.into_iter()
|
||||
.cloned()
|
||||
.map(|(name, kind)| wit::Symbol {
|
||||
name,
|
||||
kind: kind.into(),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let labels = self
|
||||
.extension
|
||||
.call({
|
||||
let this = self.clone();
|
||||
|extension, store| {
|
||||
async move {
|
||||
extension
|
||||
.call_labels_for_symbols(store, &this.language_server_id, symbols)
|
||||
.await?
|
||||
.map_err(|e| anyhow!("{}", e))
|
||||
}
|
||||
.boxed()
|
||||
}
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(labels_from_wit(labels, language))
|
||||
}
|
||||
}
|
||||
|
||||
fn labels_from_wit(
|
||||
labels: Vec<Option<wit::CodeLabel>>,
|
||||
language: &Arc<Language>,
|
||||
) -> Vec<Option<CodeLabel>> {
|
||||
labels
|
||||
.into_iter()
|
||||
.map(|label| {
|
||||
let label = label?;
|
||||
let runs = if !label.code.is_empty() {
|
||||
language.highlight_text(&label.code.as_str().into(), 0..label.code.len())
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
build_code_label(&label, &runs, &language)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn build_code_label(
|
||||
label: &wit::CodeLabel,
|
||||
parsed_runs: &[(Range<usize>, HighlightId)],
|
||||
language: &Arc<Language>,
|
||||
) -> Option<CodeLabel> {
|
||||
let mut text = String::new();
|
||||
let mut runs = vec![];
|
||||
|
||||
for span in &label.spans {
|
||||
match span {
|
||||
wit::CodeLabelSpan::CodeRange(range) => {
|
||||
let range = Range::from(*range);
|
||||
let code_span = &label.code.get(range.clone())?;
|
||||
let mut input_ix = range.start;
|
||||
let mut output_ix = text.len();
|
||||
for (run_range, id) in parsed_runs {
|
||||
if run_range.start >= range.end {
|
||||
break;
|
||||
}
|
||||
if run_range.end <= input_ix {
|
||||
continue;
|
||||
}
|
||||
|
||||
if run_range.start > input_ix {
|
||||
let len = run_range.start - input_ix;
|
||||
output_ix += len;
|
||||
input_ix += len;
|
||||
}
|
||||
|
||||
let len = range.end.min(run_range.end) - input_ix;
|
||||
runs.push((output_ix..output_ix + len, *id));
|
||||
output_ix += len;
|
||||
input_ix += len;
|
||||
}
|
||||
|
||||
text.push_str(code_span);
|
||||
}
|
||||
wit::CodeLabelSpan::Literal(span) => {
|
||||
let highlight_id = language
|
||||
.grammar()
|
||||
.zip(span.highlight_name.as_ref())
|
||||
.and_then(|(grammar, highlight_name)| {
|
||||
grammar.highlight_id_for_name(&highlight_name)
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let ix = text.len();
|
||||
runs.push((ix..ix + span.text.len(), highlight_id));
|
||||
text.push_str(&span.text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let filter_range = Range::from(label.filter_range);
|
||||
text.get(filter_range.clone())?;
|
||||
Some(CodeLabel {
|
||||
text,
|
||||
runs,
|
||||
filter_range,
|
||||
})
|
||||
}
|
||||
|
||||
impl From<wit::Range> for Range<usize> {
|
||||
fn from(range: wit::Range) -> Self {
|
||||
let start = range.start as usize;
|
||||
let end = range.end as usize;
|
||||
start..end
|
||||
}
|
||||
}
|
||||
|
||||
impl From<lsp::CompletionItem> for wit::Completion {
|
||||
fn from(value: lsp::CompletionItem) -> Self {
|
||||
Self {
|
||||
label: value.label,
|
||||
detail: value.detail,
|
||||
kind: value.kind.map(Into::into),
|
||||
insert_text_format: value.insert_text_format.map(Into::into),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<lsp::CompletionItemKind> for wit::CompletionKind {
|
||||
fn from(value: lsp::CompletionItemKind) -> Self {
|
||||
match value {
|
||||
lsp::CompletionItemKind::TEXT => Self::Text,
|
||||
lsp::CompletionItemKind::METHOD => Self::Method,
|
||||
lsp::CompletionItemKind::FUNCTION => Self::Function,
|
||||
lsp::CompletionItemKind::CONSTRUCTOR => Self::Constructor,
|
||||
lsp::CompletionItemKind::FIELD => Self::Field,
|
||||
lsp::CompletionItemKind::VARIABLE => Self::Variable,
|
||||
lsp::CompletionItemKind::CLASS => Self::Class,
|
||||
lsp::CompletionItemKind::INTERFACE => Self::Interface,
|
||||
lsp::CompletionItemKind::MODULE => Self::Module,
|
||||
lsp::CompletionItemKind::PROPERTY => Self::Property,
|
||||
lsp::CompletionItemKind::UNIT => Self::Unit,
|
||||
lsp::CompletionItemKind::VALUE => Self::Value,
|
||||
lsp::CompletionItemKind::ENUM => Self::Enum,
|
||||
lsp::CompletionItemKind::KEYWORD => Self::Keyword,
|
||||
lsp::CompletionItemKind::SNIPPET => Self::Snippet,
|
||||
lsp::CompletionItemKind::COLOR => Self::Color,
|
||||
lsp::CompletionItemKind::FILE => Self::File,
|
||||
lsp::CompletionItemKind::REFERENCE => Self::Reference,
|
||||
lsp::CompletionItemKind::FOLDER => Self::Folder,
|
||||
lsp::CompletionItemKind::ENUM_MEMBER => Self::EnumMember,
|
||||
lsp::CompletionItemKind::CONSTANT => Self::Constant,
|
||||
lsp::CompletionItemKind::STRUCT => Self::Struct,
|
||||
lsp::CompletionItemKind::EVENT => Self::Event,
|
||||
lsp::CompletionItemKind::OPERATOR => Self::Operator,
|
||||
lsp::CompletionItemKind::TYPE_PARAMETER => Self::TypeParameter,
|
||||
_ => Self::Other(extract_int(value)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<lsp::InsertTextFormat> for wit::InsertTextFormat {
|
||||
fn from(value: lsp::InsertTextFormat) -> Self {
|
||||
match value {
|
||||
lsp::InsertTextFormat::PLAIN_TEXT => Self::PlainText,
|
||||
lsp::InsertTextFormat::SNIPPET => Self::Snippet,
|
||||
_ => Self::Other(extract_int(value)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<lsp::SymbolKind> for wit::SymbolKind {
|
||||
fn from(value: lsp::SymbolKind) -> Self {
|
||||
match value {
|
||||
lsp::SymbolKind::FILE => Self::File,
|
||||
lsp::SymbolKind::MODULE => Self::Module,
|
||||
lsp::SymbolKind::NAMESPACE => Self::Namespace,
|
||||
lsp::SymbolKind::PACKAGE => Self::Package,
|
||||
lsp::SymbolKind::CLASS => Self::Class,
|
||||
lsp::SymbolKind::METHOD => Self::Method,
|
||||
lsp::SymbolKind::PROPERTY => Self::Property,
|
||||
lsp::SymbolKind::FIELD => Self::Field,
|
||||
lsp::SymbolKind::CONSTRUCTOR => Self::Constructor,
|
||||
lsp::SymbolKind::ENUM => Self::Enum,
|
||||
lsp::SymbolKind::INTERFACE => Self::Interface,
|
||||
lsp::SymbolKind::FUNCTION => Self::Function,
|
||||
lsp::SymbolKind::VARIABLE => Self::Variable,
|
||||
lsp::SymbolKind::CONSTANT => Self::Constant,
|
||||
lsp::SymbolKind::STRING => Self::String,
|
||||
lsp::SymbolKind::NUMBER => Self::Number,
|
||||
lsp::SymbolKind::BOOLEAN => Self::Boolean,
|
||||
lsp::SymbolKind::ARRAY => Self::Array,
|
||||
lsp::SymbolKind::OBJECT => Self::Object,
|
||||
lsp::SymbolKind::KEY => Self::Key,
|
||||
lsp::SymbolKind::NULL => Self::Null,
|
||||
lsp::SymbolKind::ENUM_MEMBER => Self::EnumMember,
|
||||
lsp::SymbolKind::STRUCT => Self::Struct,
|
||||
lsp::SymbolKind::EVENT => Self::Event,
|
||||
lsp::SymbolKind::OPERATOR => Self::Operator,
|
||||
lsp::SymbolKind::TYPE_PARAMETER => Self::TypeParameter,
|
||||
_ => Self::Other(extract_int(value)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn extract_int<T: Serialize>(value: T) -> i32 {
|
||||
maybe!({
|
||||
let kind = serde_json::to_value(&value)?;
|
||||
serde_json::from_value(kind)
|
||||
})
|
||||
.log_err()
|
||||
.unwrap_or(-1)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_code_label() {
|
||||
use util::test::marked_text_ranges;
|
||||
|
||||
let (code, code_ranges) = marked_text_ranges(
|
||||
"«const» «a»: «fn»(«Bcd»(«Efgh»)) -> «Ijklm» = pqrs.tuv",
|
||||
false,
|
||||
);
|
||||
let code_runs = code_ranges
|
||||
.into_iter()
|
||||
.map(|range| (range, HighlightId(0)))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let label = build_code_label(
|
||||
&wit::CodeLabel {
|
||||
spans: vec![
|
||||
wit::CodeLabelSpan::CodeRange(wit::Range {
|
||||
start: code.find("pqrs").unwrap() as u32,
|
||||
end: code.len() as u32,
|
||||
}),
|
||||
wit::CodeLabelSpan::CodeRange(wit::Range {
|
||||
start: code.find(": fn").unwrap() as u32,
|
||||
end: code.find(" = ").unwrap() as u32,
|
||||
}),
|
||||
],
|
||||
filter_range: wit::Range {
|
||||
start: 0,
|
||||
end: "pqrs.tuv".len() as u32,
|
||||
},
|
||||
code,
|
||||
},
|
||||
&code_runs,
|
||||
&language::PLAIN_TEXT,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let (label_text, label_ranges) =
|
||||
marked_text_ranges("pqrs.tuv: «fn»(«Bcd»(«Efgh»)) -> «Ijklm»", false);
|
||||
let label_runs = label_ranges
|
||||
.into_iter()
|
||||
.map(|range| (range, HighlightId(0)))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(
|
||||
label,
|
||||
CodeLabel {
|
||||
text: label_text,
|
||||
runs: label_runs,
|
||||
filter_range: label.filter_range.clone()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_code_label_with_invalid_ranges() {
|
||||
use util::test::marked_text_ranges;
|
||||
|
||||
let (code, code_ranges) = marked_text_ranges("const «a»: «B» = '🏀'", false);
|
||||
let code_runs = code_ranges
|
||||
.into_iter()
|
||||
.map(|range| (range, HighlightId(0)))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// A span uses a code range that is invalid because it starts inside of
|
||||
// a multi-byte character.
|
||||
let label = build_code_label(
|
||||
&wit::CodeLabel {
|
||||
spans: vec![
|
||||
wit::CodeLabelSpan::CodeRange(wit::Range {
|
||||
start: code.find('B').unwrap() as u32,
|
||||
end: code.find(" = ").unwrap() as u32,
|
||||
}),
|
||||
wit::CodeLabelSpan::CodeRange(wit::Range {
|
||||
start: code.find('🏀').unwrap() as u32 + 1,
|
||||
end: code.len() as u32,
|
||||
}),
|
||||
],
|
||||
filter_range: wit::Range {
|
||||
start: 0,
|
||||
end: "B".len() as u32,
|
||||
},
|
||||
code,
|
||||
},
|
||||
&code_runs,
|
||||
&language::PLAIN_TEXT,
|
||||
);
|
||||
assert!(label.is_none());
|
||||
|
||||
// Filter range extends beyond actual text
|
||||
let label = build_code_label(
|
||||
&wit::CodeLabel {
|
||||
spans: vec![wit::CodeLabelSpan::Literal(wit::CodeLabelSpanLiteral {
|
||||
text: "abc".into(),
|
||||
highlight_name: Some("type".into()),
|
||||
})],
|
||||
filter_range: wit::Range { start: 0, end: 5 },
|
||||
code: String::new(),
|
||||
},
|
||||
&code_runs,
|
||||
&language::PLAIN_TEXT,
|
||||
);
|
||||
assert!(label.is_none());
|
||||
}
|
||||
|
||||
@@ -98,11 +98,34 @@ pub struct GrammarManifestEntry {
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)]
|
||||
pub struct LanguageServerManifestEntry {
|
||||
pub language: Arc<str>,
|
||||
/// Deprecated in favor of `languages`.
|
||||
#[serde(default)]
|
||||
language: Option<Arc<str>>,
|
||||
/// The list of languages this language server should work with.
|
||||
#[serde(default)]
|
||||
languages: Vec<Arc<str>>,
|
||||
#[serde(default)]
|
||||
pub language_ids: HashMap<String, String>,
|
||||
}
|
||||
|
||||
impl LanguageServerManifestEntry {
|
||||
/// Returns the list of languages for the language server.
|
||||
///
|
||||
/// Prefer this over accessing the `language` or `languages` fields directly,
|
||||
/// as we currently support both.
|
||||
///
|
||||
/// We can replace this with just field access for the `languages` field once
|
||||
/// we have removed `language`.
|
||||
pub fn languages(&self) -> impl IntoIterator<Item = Arc<str>> + '_ {
|
||||
let language = if self.languages.is_empty() {
|
||||
self.language.clone()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
self.languages.iter().cloned().chain(language)
|
||||
}
|
||||
}
|
||||
|
||||
impl ExtensionManifest {
|
||||
pub async fn load(fs: Arc<dyn Fs>, extension_dir: &Path) -> Result<Self> {
|
||||
let extension_name = extension_dir
|
||||
|
||||
@@ -3,7 +3,7 @@ use collections::HashMap;
|
||||
use gpui::AppContext;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::Settings;
|
||||
use settings::{Settings, SettingsSources};
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Default, Clone, JsonSchema)]
|
||||
@@ -26,14 +26,7 @@ impl Settings for ExtensionSettings {
|
||||
|
||||
type FileContent = Self;
|
||||
|
||||
fn load(
|
||||
_default_value: &Self::FileContent,
|
||||
user_values: &[&Self::FileContent],
|
||||
_cx: &mut AppContext,
|
||||
) -> Result<Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
Ok(user_values.get(0).copied().cloned().unwrap_or_default())
|
||||
fn load(sources: SettingsSources<Self::FileContent>, _cx: &mut AppContext) -> Result<Self> {
|
||||
Ok(sources.user.cloned().unwrap_or_default())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -237,6 +237,7 @@ impl ExtensionStore {
|
||||
node_runtime,
|
||||
language_registry.clone(),
|
||||
work_dir,
|
||||
cx,
|
||||
),
|
||||
wasm_extensions: Vec::new(),
|
||||
fs,
|
||||
@@ -961,8 +962,10 @@ impl ExtensionStore {
|
||||
};
|
||||
grammars_to_remove.extend(extension.manifest.grammars.keys().cloned());
|
||||
for (language_server_name, config) in extension.manifest.language_servers.iter() {
|
||||
self.language_registry
|
||||
.remove_lsp_adapter(config.language.as_ref(), language_server_name);
|
||||
for language in config.languages() {
|
||||
self.language_registry
|
||||
.remove_lsp_adapter(&language, language_server_name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1101,19 +1104,21 @@ impl ExtensionStore {
|
||||
this.reload_complete_senders.clear();
|
||||
|
||||
for (manifest, wasm_extension) in &wasm_extensions {
|
||||
for (language_server_name, language_server_config) in &manifest.language_servers
|
||||
{
|
||||
this.language_registry.register_lsp_adapter(
|
||||
language_server_config.language.clone(),
|
||||
Arc::new(ExtensionLspAdapter {
|
||||
extension: wasm_extension.clone(),
|
||||
host: this.wasm_host.clone(),
|
||||
config: wit::LanguageServerConfig {
|
||||
name: language_server_name.0.to_string(),
|
||||
language_name: language_server_config.language.to_string(),
|
||||
},
|
||||
}),
|
||||
);
|
||||
for (language_server_id, language_server_config) in &manifest.language_servers {
|
||||
for language in language_server_config.languages() {
|
||||
this.language_registry.register_lsp_adapter(
|
||||
language.clone(),
|
||||
Arc::new(ExtensionLspAdapter {
|
||||
extension: wasm_extension.clone(),
|
||||
host: this.wasm_host.clone(),
|
||||
language_server_id: language_server_id.clone(),
|
||||
config: wit::LanguageServerConfig {
|
||||
name: language_server_id.0.to_string(),
|
||||
language_name: language.to_string(),
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
this.wasm_extensions.extend(wasm_extensions);
|
||||
|
||||
@@ -619,6 +619,53 @@ async fn test_extension_store_with_gleam_extension(cx: &mut TestAppContext) {
|
||||
]
|
||||
);
|
||||
|
||||
// The extension creates custom labels for completion items.
|
||||
fake_server.handle_request::<lsp::request::Completion, _, _>(|_, _| async move {
|
||||
Ok(Some(lsp::CompletionResponse::Array(vec![
|
||||
lsp::CompletionItem {
|
||||
label: "foo".into(),
|
||||
kind: Some(lsp::CompletionItemKind::FUNCTION),
|
||||
detail: Some("fn() -> Result(Nil, Error)".into()),
|
||||
..Default::default()
|
||||
},
|
||||
lsp::CompletionItem {
|
||||
label: "bar.baz".into(),
|
||||
kind: Some(lsp::CompletionItemKind::FUNCTION),
|
||||
detail: Some("fn(List(a)) -> a".into()),
|
||||
..Default::default()
|
||||
},
|
||||
lsp::CompletionItem {
|
||||
label: "Quux".into(),
|
||||
kind: Some(lsp::CompletionItemKind::CONSTRUCTOR),
|
||||
detail: Some("fn(String) -> T".into()),
|
||||
..Default::default()
|
||||
},
|
||||
lsp::CompletionItem {
|
||||
label: "my_string".into(),
|
||||
kind: Some(lsp::CompletionItemKind::CONSTANT),
|
||||
detail: Some("String".into()),
|
||||
..Default::default()
|
||||
},
|
||||
])))
|
||||
});
|
||||
|
||||
let completion_labels = project
|
||||
.update(cx, |project, cx| project.completions(&buffer, 0, cx))
|
||||
.await
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.map(|c| c.label.text)
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
completion_labels,
|
||||
[
|
||||
"foo: fn() -> Result(Nil, Error)".to_string(),
|
||||
"bar.baz: fn(List(a)) -> a".to_string(),
|
||||
"Quux: fn(String) -> T".to_string(),
|
||||
"my_string: String".to_string(),
|
||||
]
|
||||
);
|
||||
|
||||
// Simulate a new version of the language server being released
|
||||
language_server_version.lock().version = "v2.0.0".into();
|
||||
language_server_version.lock().binary_contents = "the-new-binary-contents".into();
|
||||
|
||||
@@ -3,6 +3,7 @@ pub(crate) mod wit;
|
||||
use crate::ExtensionManifest;
|
||||
use anyhow::{anyhow, bail, Context as _, Result};
|
||||
use fs::{normalize_path, Fs};
|
||||
use futures::future::LocalBoxFuture;
|
||||
use futures::{
|
||||
channel::{
|
||||
mpsc::{self, UnboundedSender},
|
||||
@@ -11,7 +12,7 @@ use futures::{
|
||||
future::BoxFuture,
|
||||
Future, FutureExt, StreamExt as _,
|
||||
};
|
||||
use gpui::BackgroundExecutor;
|
||||
use gpui::{AppContext, AsyncAppContext, BackgroundExecutor, Task};
|
||||
use language::LanguageRegistry;
|
||||
use node_runtime::NodeRuntime;
|
||||
use semantic_version::SemanticVersion;
|
||||
@@ -34,6 +35,8 @@ pub(crate) struct WasmHost {
|
||||
pub(crate) language_registry: Arc<LanguageRegistry>,
|
||||
fs: Arc<dyn Fs>,
|
||||
pub(crate) work_dir: PathBuf,
|
||||
_main_thread_message_task: Task<()>,
|
||||
main_thread_message_tx: mpsc::UnboundedSender<MainThreadCall>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -51,6 +54,9 @@ pub(crate) struct WasmState {
|
||||
pub(crate) host: Arc<WasmHost>,
|
||||
}
|
||||
|
||||
type MainThreadCall =
|
||||
Box<dyn Send + for<'a> FnOnce(&'a mut AsyncAppContext) -> LocalBoxFuture<'a, ()>>;
|
||||
|
||||
type ExtensionCall = Box<
|
||||
dyn Send + for<'a> FnOnce(&'a mut Extension, &'a mut Store<WasmState>) -> BoxFuture<'a, ()>,
|
||||
>;
|
||||
@@ -75,7 +81,14 @@ impl WasmHost {
|
||||
node_runtime: Arc<dyn NodeRuntime>,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
work_dir: PathBuf,
|
||||
cx: &mut AppContext,
|
||||
) -> Arc<Self> {
|
||||
let (tx, mut rx) = mpsc::unbounded::<MainThreadCall>();
|
||||
let task = cx.spawn(|mut cx| async move {
|
||||
while let Some(message) = rx.next().await {
|
||||
message(&mut cx).await;
|
||||
}
|
||||
});
|
||||
Arc::new(Self {
|
||||
engine: wasm_engine(),
|
||||
fs,
|
||||
@@ -83,6 +96,8 @@ impl WasmHost {
|
||||
http_client,
|
||||
node_runtime,
|
||||
language_registry,
|
||||
_main_thread_message_task: task,
|
||||
main_thread_message_tx: tx,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -238,6 +253,26 @@ impl WasmExtension {
|
||||
}
|
||||
|
||||
impl WasmState {
|
||||
fn on_main_thread<T, Fn>(&self, f: Fn) -> impl 'static + Future<Output = T>
|
||||
where
|
||||
T: 'static + Send,
|
||||
Fn: 'static + Send + for<'a> FnOnce(&'a mut AsyncAppContext) -> LocalBoxFuture<'a, T>,
|
||||
{
|
||||
let (return_tx, return_rx) = oneshot::channel();
|
||||
self.host
|
||||
.main_thread_message_tx
|
||||
.clone()
|
||||
.unbounded_send(Box::new(move |cx| {
|
||||
async {
|
||||
let result = f(cx).await;
|
||||
return_tx.send(result).ok();
|
||||
}
|
||||
.boxed_local()
|
||||
}))
|
||||
.expect("main thread message channel should not be closed yet");
|
||||
async move { return_rx.await.expect("main thread message channel") }
|
||||
}
|
||||
|
||||
fn work_dir(&self) -> PathBuf {
|
||||
self.host.work_dir.join(self.manifest.id.as_ref())
|
||||
}
|
||||
|
||||
@@ -1,20 +1,25 @@
|
||||
mod since_v0_0_1;
|
||||
mod since_v0_0_4;
|
||||
mod since_v0_0_6;
|
||||
use since_v0_0_6 as latest;
|
||||
|
||||
use super::{wasm_engine, WasmState};
|
||||
use anyhow::{Context, Result};
|
||||
use language::LspAdapterDelegate;
|
||||
use language::{LanguageServerName, LspAdapterDelegate};
|
||||
use semantic_version::SemanticVersion;
|
||||
use std::ops::RangeInclusive;
|
||||
use std::sync::Arc;
|
||||
use std::{ops::RangeInclusive, sync::Arc};
|
||||
use wasmtime::{
|
||||
component::{Component, Instance, Linker, Resource},
|
||||
Store,
|
||||
};
|
||||
|
||||
use since_v0_0_4 as latest;
|
||||
|
||||
pub use latest::{Command, LanguageServerConfig};
|
||||
#[cfg(test)]
|
||||
pub use latest::CodeLabelSpanLiteral;
|
||||
pub use latest::{
|
||||
zed::extension::lsp::{Completion, CompletionKind, InsertTextFormat, Symbol, SymbolKind},
|
||||
CodeLabel, CodeLabelSpan, Command, Range,
|
||||
};
|
||||
pub use since_v0_0_4::LanguageServerConfig;
|
||||
|
||||
pub fn new_linker(
|
||||
f: impl Fn(&mut Linker<WasmState>, fn(&mut WasmState) -> &mut WasmState) -> Result<()>,
|
||||
@@ -41,6 +46,7 @@ pub fn wasm_api_version_range() -> RangeInclusive<SemanticVersion> {
|
||||
}
|
||||
|
||||
pub enum Extension {
|
||||
V006(since_v0_0_6::Extension),
|
||||
V004(since_v0_0_4::Extension),
|
||||
V001(since_v0_0_1::Extension),
|
||||
}
|
||||
@@ -51,16 +57,13 @@ impl Extension {
|
||||
version: SemanticVersion,
|
||||
component: &Component,
|
||||
) -> Result<(Self, Instance)> {
|
||||
if version < latest::MIN_VERSION {
|
||||
let (extension, instance) = since_v0_0_1::Extension::instantiate_async(
|
||||
store,
|
||||
&component,
|
||||
since_v0_0_1::linker(),
|
||||
)
|
||||
.await
|
||||
.context("failed to instantiate wasm extension")?;
|
||||
Ok((Self::V001(extension), instance))
|
||||
} else {
|
||||
if version >= latest::MIN_VERSION {
|
||||
let (extension, instance) =
|
||||
latest::Extension::instantiate_async(store, &component, latest::linker())
|
||||
.await
|
||||
.context("failed to instantiate wasm extension")?;
|
||||
Ok((Self::V006(extension), instance))
|
||||
} else if version >= since_v0_0_4::MIN_VERSION {
|
||||
let (extension, instance) = since_v0_0_4::Extension::instantiate_async(
|
||||
store,
|
||||
&component,
|
||||
@@ -69,11 +72,21 @@ impl Extension {
|
||||
.await
|
||||
.context("failed to instantiate wasm extension")?;
|
||||
Ok((Self::V004(extension), instance))
|
||||
} else {
|
||||
let (extension, instance) = since_v0_0_1::Extension::instantiate_async(
|
||||
store,
|
||||
&component,
|
||||
since_v0_0_1::linker(),
|
||||
)
|
||||
.await
|
||||
.context("failed to instantiate wasm extension")?;
|
||||
Ok((Self::V001(extension), instance))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn call_init_extension(&self, store: &mut Store<WasmState>) -> Result<()> {
|
||||
match self {
|
||||
Extension::V006(ext) => ext.call_init_extension(store).await,
|
||||
Extension::V004(ext) => ext.call_init_extension(store).await,
|
||||
Extension::V001(ext) => ext.call_init_extension(store).await,
|
||||
}
|
||||
@@ -82,14 +95,19 @@ impl Extension {
|
||||
pub async fn call_language_server_command(
|
||||
&self,
|
||||
store: &mut Store<WasmState>,
|
||||
language_server_id: &LanguageServerName,
|
||||
config: &LanguageServerConfig,
|
||||
resource: Resource<Arc<dyn LspAdapterDelegate>>,
|
||||
) -> Result<Result<Command, String>> {
|
||||
match self {
|
||||
Extension::V004(ext) => {
|
||||
ext.call_language_server_command(store, config, resource)
|
||||
Extension::V006(ext) => {
|
||||
ext.call_language_server_command(store, &language_server_id.0, resource)
|
||||
.await
|
||||
}
|
||||
Extension::V004(ext) => Ok(ext
|
||||
.call_language_server_command(store, config, resource)
|
||||
.await?
|
||||
.map(|command| command.into())),
|
||||
Extension::V001(ext) => Ok(ext
|
||||
.call_language_server_command(store, &config.clone().into(), resource)
|
||||
.await?
|
||||
@@ -100,10 +118,19 @@ impl Extension {
|
||||
pub async fn call_language_server_initialization_options(
|
||||
&self,
|
||||
store: &mut Store<WasmState>,
|
||||
language_server_id: &LanguageServerName,
|
||||
config: &LanguageServerConfig,
|
||||
resource: Resource<Arc<dyn LspAdapterDelegate>>,
|
||||
) -> Result<Result<Option<String>, String>> {
|
||||
match self {
|
||||
Extension::V006(ext) => {
|
||||
ext.call_language_server_initialization_options(
|
||||
store,
|
||||
&language_server_id.0,
|
||||
resource,
|
||||
)
|
||||
.await
|
||||
}
|
||||
Extension::V004(ext) => {
|
||||
ext.call_language_server_initialization_options(store, config, resource)
|
||||
.await
|
||||
@@ -118,6 +145,55 @@ impl Extension {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn call_language_server_workspace_configuration(
|
||||
&self,
|
||||
store: &mut Store<WasmState>,
|
||||
language_server_id: &LanguageServerName,
|
||||
resource: Resource<Arc<dyn LspAdapterDelegate>>,
|
||||
) -> Result<Result<Option<String>, String>> {
|
||||
match self {
|
||||
Extension::V006(ext) => {
|
||||
ext.call_language_server_workspace_configuration(
|
||||
store,
|
||||
&language_server_id.0,
|
||||
resource,
|
||||
)
|
||||
.await
|
||||
}
|
||||
Extension::V004(_) | Extension::V001(_) => Ok(Ok(None)),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn call_labels_for_completions(
|
||||
&self,
|
||||
store: &mut Store<WasmState>,
|
||||
language_server_id: &LanguageServerName,
|
||||
completions: Vec<latest::Completion>,
|
||||
) -> Result<Result<Vec<Option<CodeLabel>>, String>> {
|
||||
match self {
|
||||
Extension::V001(_) | Extension::V004(_) => Ok(Ok(Vec::new())),
|
||||
Extension::V006(ext) => {
|
||||
ext.call_labels_for_completions(store, &language_server_id.0, &completions)
|
||||
.await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn call_labels_for_symbols(
|
||||
&self,
|
||||
store: &mut Store<WasmState>,
|
||||
language_server_id: &LanguageServerName,
|
||||
symbols: Vec<latest::Symbol>,
|
||||
) -> Result<Result<Vec<Option<CodeLabel>>, String>> {
|
||||
match self {
|
||||
Extension::V001(_) | Extension::V004(_) => Ok(Ok(Vec::new())),
|
||||
Extension::V006(ext) => {
|
||||
ext.call_labels_for_symbols(store, &language_server_id.0, &symbols)
|
||||
.await
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
trait ToWasmtimeResult<T> {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use super::latest;
|
||||
use crate::wasm_host::wit::since_v0_0_4;
|
||||
use crate::wasm_host::WasmState;
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
@@ -14,6 +15,8 @@ wasmtime::component::bindgen!({
|
||||
path: "../extension_api/wit/since_v0.0.1",
|
||||
with: {
|
||||
"worktree": ExtensionWorktree,
|
||||
"zed:extension/github": latest::zed::extension::github,
|
||||
"zed:extension/platform": latest::zed::extension::platform,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -24,53 +27,6 @@ pub fn linker() -> &'static Linker<WasmState> {
|
||||
LINKER.get_or_init(|| super::new_linker(Extension::add_to_linker))
|
||||
}
|
||||
|
||||
impl From<latest::Os> for Os {
|
||||
fn from(value: latest::Os) -> Self {
|
||||
match value {
|
||||
latest::Os::Mac => Os::Mac,
|
||||
latest::Os::Linux => Os::Linux,
|
||||
latest::Os::Windows => Os::Windows,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<latest::Architecture> for Architecture {
|
||||
fn from(value: latest::Architecture) -> Self {
|
||||
match value {
|
||||
latest::Architecture::Aarch64 => Self::Aarch64,
|
||||
latest::Architecture::X86 => Self::X86,
|
||||
latest::Architecture::X8664 => Self::X8664,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<latest::GithubRelease> for GithubRelease {
|
||||
fn from(value: latest::GithubRelease) -> Self {
|
||||
Self {
|
||||
version: value.version,
|
||||
assets: value.assets.into_iter().map(|asset| asset.into()).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<latest::GithubReleaseAsset> for GithubReleaseAsset {
|
||||
fn from(value: latest::GithubReleaseAsset) -> Self {
|
||||
Self {
|
||||
name: value.name,
|
||||
download_url: value.download_url,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<GithubReleaseOptions> for latest::GithubReleaseOptions {
|
||||
fn from(value: GithubReleaseOptions) -> Self {
|
||||
Self {
|
||||
require_assets: value.require_assets,
|
||||
pre_release: value.pre_release,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DownloadedFileType> for latest::DownloadedFileType {
|
||||
fn from(value: DownloadedFileType) -> Self {
|
||||
match value {
|
||||
@@ -82,8 +38,8 @@ impl From<DownloadedFileType> for latest::DownloadedFileType {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<latest::LanguageServerConfig> for LanguageServerConfig {
|
||||
fn from(value: latest::LanguageServerConfig) -> Self {
|
||||
impl From<since_v0_0_4::LanguageServerConfig> for LanguageServerConfig {
|
||||
fn from(value: since_v0_0_4::LanguageServerConfig) -> Self {
|
||||
Self {
|
||||
name: value.name,
|
||||
language_name: value.language_name,
|
||||
@@ -134,21 +90,21 @@ impl HostWorktree for WasmState {
|
||||
#[async_trait]
|
||||
impl ExtensionImports for WasmState {
|
||||
async fn node_binary_path(&mut self) -> wasmtime::Result<Result<String, String>> {
|
||||
latest::ExtensionImports::node_binary_path(self).await
|
||||
latest::nodejs::Host::node_binary_path(self).await
|
||||
}
|
||||
|
||||
async fn npm_package_latest_version(
|
||||
&mut self,
|
||||
package_name: String,
|
||||
) -> wasmtime::Result<Result<String, String>> {
|
||||
latest::ExtensionImports::npm_package_latest_version(self, package_name).await
|
||||
latest::nodejs::Host::npm_package_latest_version(self, package_name).await
|
||||
}
|
||||
|
||||
async fn npm_package_installed_version(
|
||||
&mut self,
|
||||
package_name: String,
|
||||
) -> wasmtime::Result<Result<Option<String>, String>> {
|
||||
latest::ExtensionImports::npm_package_installed_version(self, package_name).await
|
||||
latest::nodejs::Host::npm_package_installed_version(self, package_name).await
|
||||
}
|
||||
|
||||
async fn npm_install_package(
|
||||
@@ -156,7 +112,7 @@ impl ExtensionImports for WasmState {
|
||||
package_name: String,
|
||||
version: String,
|
||||
) -> wasmtime::Result<Result<(), String>> {
|
||||
latest::ExtensionImports::npm_install_package(self, package_name, version).await
|
||||
latest::nodejs::Host::npm_install_package(self, package_name, version).await
|
||||
}
|
||||
|
||||
async fn latest_github_release(
|
||||
@@ -164,17 +120,11 @@ impl ExtensionImports for WasmState {
|
||||
repo: String,
|
||||
options: GithubReleaseOptions,
|
||||
) -> wasmtime::Result<Result<GithubRelease, String>> {
|
||||
Ok(
|
||||
latest::ExtensionImports::latest_github_release(self, repo, options.into())
|
||||
.await?
|
||||
.map(|github| github.into()),
|
||||
)
|
||||
latest::zed::extension::github::Host::latest_github_release(self, repo, options).await
|
||||
}
|
||||
|
||||
async fn current_platform(&mut self) -> Result<(Os, Architecture)> {
|
||||
latest::ExtensionImports::current_platform(self)
|
||||
.await
|
||||
.map(|(os, arch)| (os.into(), arch.into()))
|
||||
latest::zed::extension::platform::Host::current_platform(self).await
|
||||
}
|
||||
|
||||
async fn set_language_server_installation_status(
|
||||
|
||||
@@ -1,29 +1,21 @@
|
||||
use crate::wasm_host::wit::ToWasmtimeResult;
|
||||
use super::latest;
|
||||
use crate::wasm_host::WasmState;
|
||||
use anyhow::{anyhow, Result};
|
||||
use async_compression::futures::bufread::GzipDecoder;
|
||||
use async_tar::Archive;
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use futures::io::BufReader;
|
||||
use language::{LanguageServerBinaryStatus, LspAdapterDelegate};
|
||||
use language::LspAdapterDelegate;
|
||||
use semantic_version::SemanticVersion;
|
||||
use std::path::Path;
|
||||
use std::{
|
||||
env,
|
||||
path::PathBuf,
|
||||
sync::{Arc, OnceLock},
|
||||
};
|
||||
use util::maybe;
|
||||
use std::sync::{Arc, OnceLock};
|
||||
use wasmtime::component::{Linker, Resource};
|
||||
|
||||
pub const MIN_VERSION: SemanticVersion = SemanticVersion::new(0, 0, 4);
|
||||
pub const MAX_VERSION: SemanticVersion = SemanticVersion::new(0, 0, 5);
|
||||
|
||||
wasmtime::component::bindgen!({
|
||||
async: true,
|
||||
path: "../extension_api/wit/since_v0.0.4",
|
||||
with: {
|
||||
"worktree": ExtensionWorktree,
|
||||
"zed:extension/github": latest::zed::extension::github,
|
||||
"zed:extension/platform": latest::zed::extension::platform,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -34,6 +26,46 @@ pub fn linker() -> &'static Linker<WasmState> {
|
||||
LINKER.get_or_init(|| super::new_linker(Extension::add_to_linker))
|
||||
}
|
||||
|
||||
impl From<DownloadedFileType> for latest::DownloadedFileType {
|
||||
fn from(value: DownloadedFileType) -> Self {
|
||||
match value {
|
||||
DownloadedFileType::Gzip => latest::DownloadedFileType::Gzip,
|
||||
DownloadedFileType::GzipTar => latest::DownloadedFileType::GzipTar,
|
||||
DownloadedFileType::Zip => latest::DownloadedFileType::Zip,
|
||||
DownloadedFileType::Uncompressed => latest::DownloadedFileType::Uncompressed,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<LanguageServerInstallationStatus> for latest::LanguageServerInstallationStatus {
|
||||
fn from(value: LanguageServerInstallationStatus) -> Self {
|
||||
match value {
|
||||
LanguageServerInstallationStatus::None => {
|
||||
latest::LanguageServerInstallationStatus::None
|
||||
}
|
||||
LanguageServerInstallationStatus::Downloading => {
|
||||
latest::LanguageServerInstallationStatus::Downloading
|
||||
}
|
||||
LanguageServerInstallationStatus::CheckingForUpdate => {
|
||||
latest::LanguageServerInstallationStatus::CheckingForUpdate
|
||||
}
|
||||
LanguageServerInstallationStatus::Failed(error) => {
|
||||
latest::LanguageServerInstallationStatus::Failed(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Command> for latest::Command {
|
||||
fn from(value: Command) -> Self {
|
||||
Self {
|
||||
command: value.command,
|
||||
args: value.args,
|
||||
env: value.env,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl HostWorktree for WasmState {
|
||||
async fn read_text_file(
|
||||
@@ -41,19 +73,14 @@ impl HostWorktree for WasmState {
|
||||
delegate: Resource<Arc<dyn LspAdapterDelegate>>,
|
||||
path: String,
|
||||
) -> wasmtime::Result<Result<String, String>> {
|
||||
let delegate = self.table.get(&delegate)?;
|
||||
Ok(delegate
|
||||
.read_text_file(path.into())
|
||||
.await
|
||||
.map_err(|error| error.to_string()))
|
||||
latest::HostWorktree::read_text_file(self, delegate, path).await
|
||||
}
|
||||
|
||||
async fn shell_env(
|
||||
&mut self,
|
||||
delegate: Resource<Arc<dyn LspAdapterDelegate>>,
|
||||
) -> wasmtime::Result<EnvVars> {
|
||||
let delegate = self.table.get(&delegate)?;
|
||||
Ok(delegate.shell_env().await.into_iter().collect())
|
||||
latest::HostWorktree::shell_env(self, delegate).await
|
||||
}
|
||||
|
||||
async fn which(
|
||||
@@ -61,15 +88,11 @@ impl HostWorktree for WasmState {
|
||||
delegate: Resource<Arc<dyn LspAdapterDelegate>>,
|
||||
binary_name: String,
|
||||
) -> wasmtime::Result<Option<String>> {
|
||||
let delegate = self.table.get(&delegate)?;
|
||||
Ok(delegate
|
||||
.which(binary_name.as_ref())
|
||||
.await
|
||||
.map(|path| path.to_string_lossy().to_string()))
|
||||
latest::HostWorktree::which(self, delegate, binary_name).await
|
||||
}
|
||||
|
||||
fn drop(&mut self, _worktree: Resource<Worktree>) -> Result<()> {
|
||||
// we only ever hand out borrows of worktrees
|
||||
// We only ever hand out borrows of worktrees.
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -77,34 +100,21 @@ impl HostWorktree for WasmState {
|
||||
#[async_trait]
|
||||
impl ExtensionImports for WasmState {
|
||||
async fn node_binary_path(&mut self) -> wasmtime::Result<Result<String, String>> {
|
||||
self.host
|
||||
.node_runtime
|
||||
.binary_path()
|
||||
.await
|
||||
.map(|path| path.to_string_lossy().to_string())
|
||||
.to_wasmtime_result()
|
||||
latest::nodejs::Host::node_binary_path(self).await
|
||||
}
|
||||
|
||||
async fn npm_package_latest_version(
|
||||
&mut self,
|
||||
package_name: String,
|
||||
) -> wasmtime::Result<Result<String, String>> {
|
||||
self.host
|
||||
.node_runtime
|
||||
.npm_package_latest_version(&package_name)
|
||||
.await
|
||||
.to_wasmtime_result()
|
||||
latest::nodejs::Host::npm_package_latest_version(self, package_name).await
|
||||
}
|
||||
|
||||
async fn npm_package_installed_version(
|
||||
&mut self,
|
||||
package_name: String,
|
||||
) -> wasmtime::Result<Result<Option<String>, String>> {
|
||||
self.host
|
||||
.node_runtime
|
||||
.npm_package_installed_version(&self.work_dir(), &package_name)
|
||||
.await
|
||||
.to_wasmtime_result()
|
||||
latest::nodejs::Host::npm_package_installed_version(self, package_name).await
|
||||
}
|
||||
|
||||
async fn npm_install_package(
|
||||
@@ -112,11 +122,7 @@ impl ExtensionImports for WasmState {
|
||||
package_name: String,
|
||||
version: String,
|
||||
) -> wasmtime::Result<Result<(), String>> {
|
||||
self.host
|
||||
.node_runtime
|
||||
.npm_install_packages(&self.work_dir(), &[(&package_name, &version)])
|
||||
.await
|
||||
.to_wasmtime_result()
|
||||
latest::nodejs::Host::npm_install_package(self, package_name, version).await
|
||||
}
|
||||
|
||||
async fn latest_github_release(
|
||||
@@ -124,45 +130,11 @@ impl ExtensionImports for WasmState {
|
||||
repo: String,
|
||||
options: GithubReleaseOptions,
|
||||
) -> wasmtime::Result<Result<GithubRelease, String>> {
|
||||
maybe!(async {
|
||||
let release = util::github::latest_github_release(
|
||||
&repo,
|
||||
options.require_assets,
|
||||
options.pre_release,
|
||||
self.host.http_client.clone(),
|
||||
)
|
||||
.await?;
|
||||
Ok(GithubRelease {
|
||||
version: release.tag_name,
|
||||
assets: release
|
||||
.assets
|
||||
.into_iter()
|
||||
.map(|asset| GithubReleaseAsset {
|
||||
name: asset.name,
|
||||
download_url: asset.browser_download_url,
|
||||
})
|
||||
.collect(),
|
||||
})
|
||||
})
|
||||
.await
|
||||
.to_wasmtime_result()
|
||||
latest::zed::extension::github::Host::latest_github_release(self, repo, options).await
|
||||
}
|
||||
|
||||
async fn current_platform(&mut self) -> Result<(Os, Architecture)> {
|
||||
Ok((
|
||||
match env::consts::OS {
|
||||
"macos" => Os::Mac,
|
||||
"linux" => Os::Linux,
|
||||
"windows" => Os::Windows,
|
||||
_ => panic!("unsupported os"),
|
||||
},
|
||||
match env::consts::ARCH {
|
||||
"aarch64" => Architecture::Aarch64,
|
||||
"x86" => Architecture::X86,
|
||||
"x86_64" => Architecture::X8664,
|
||||
_ => panic!("unsupported architecture"),
|
||||
},
|
||||
))
|
||||
latest::zed::extension::platform::Host::current_platform(self).await
|
||||
}
|
||||
|
||||
async fn set_language_server_installation_status(
|
||||
@@ -170,23 +142,12 @@ impl ExtensionImports for WasmState {
|
||||
server_name: String,
|
||||
status: LanguageServerInstallationStatus,
|
||||
) -> wasmtime::Result<()> {
|
||||
let status = match status {
|
||||
LanguageServerInstallationStatus::CheckingForUpdate => {
|
||||
LanguageServerBinaryStatus::CheckingForUpdate
|
||||
}
|
||||
LanguageServerInstallationStatus::Downloading => {
|
||||
LanguageServerBinaryStatus::Downloading
|
||||
}
|
||||
LanguageServerInstallationStatus::None => LanguageServerBinaryStatus::None,
|
||||
LanguageServerInstallationStatus::Failed(error) => {
|
||||
LanguageServerBinaryStatus::Failed { error }
|
||||
}
|
||||
};
|
||||
|
||||
self.host
|
||||
.language_registry
|
||||
.update_lsp_status(language::LanguageServerName(server_name.into()), status);
|
||||
Ok(())
|
||||
latest::ExtensionImports::set_language_server_installation_status(
|
||||
self,
|
||||
server_name,
|
||||
status.into(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn download_file(
|
||||
@@ -195,103 +156,10 @@ impl ExtensionImports for WasmState {
|
||||
path: String,
|
||||
file_type: DownloadedFileType,
|
||||
) -> wasmtime::Result<Result<(), String>> {
|
||||
maybe!(async {
|
||||
let path = PathBuf::from(path);
|
||||
let extension_work_dir = self.host.work_dir.join(self.manifest.id.as_ref());
|
||||
|
||||
self.host.fs.create_dir(&extension_work_dir).await?;
|
||||
|
||||
let destination_path = self
|
||||
.host
|
||||
.writeable_path_from_extension(&self.manifest.id, &path)?;
|
||||
|
||||
let mut response = self
|
||||
.host
|
||||
.http_client
|
||||
.get(&url, Default::default(), true)
|
||||
.await
|
||||
.map_err(|err| anyhow!("error downloading release: {}", err))?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
Err(anyhow!(
|
||||
"download failed with status {}",
|
||||
response.status().to_string()
|
||||
))?;
|
||||
}
|
||||
let body = BufReader::new(response.body_mut());
|
||||
|
||||
match file_type {
|
||||
DownloadedFileType::Uncompressed => {
|
||||
futures::pin_mut!(body);
|
||||
self.host
|
||||
.fs
|
||||
.create_file_with(&destination_path, body)
|
||||
.await?;
|
||||
}
|
||||
DownloadedFileType::Gzip => {
|
||||
let body = GzipDecoder::new(body);
|
||||
futures::pin_mut!(body);
|
||||
self.host
|
||||
.fs
|
||||
.create_file_with(&destination_path, body)
|
||||
.await?;
|
||||
}
|
||||
DownloadedFileType::GzipTar => {
|
||||
let body = GzipDecoder::new(body);
|
||||
futures::pin_mut!(body);
|
||||
self.host
|
||||
.fs
|
||||
.extract_tar_file(&destination_path, Archive::new(body))
|
||||
.await?;
|
||||
}
|
||||
DownloadedFileType::Zip => {
|
||||
let file_name = destination_path
|
||||
.file_name()
|
||||
.ok_or_else(|| anyhow!("invalid download path"))?
|
||||
.to_string_lossy();
|
||||
let zip_filename = format!("{file_name}.zip");
|
||||
let mut zip_path = destination_path.clone();
|
||||
zip_path.set_file_name(zip_filename);
|
||||
|
||||
futures::pin_mut!(body);
|
||||
self.host.fs.create_file_with(&zip_path, body).await?;
|
||||
|
||||
let unzip_status = std::process::Command::new("unzip")
|
||||
.current_dir(&extension_work_dir)
|
||||
.arg("-d")
|
||||
.arg(&destination_path)
|
||||
.arg(&zip_path)
|
||||
.output()?
|
||||
.status;
|
||||
if !unzip_status.success() {
|
||||
Err(anyhow!("failed to unzip {} archive", path.display()))?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
.to_wasmtime_result()
|
||||
latest::ExtensionImports::download_file(self, url, path, file_type.into()).await
|
||||
}
|
||||
|
||||
async fn make_file_executable(&mut self, path: String) -> wasmtime::Result<Result<(), String>> {
|
||||
#[allow(unused)]
|
||||
let path = self
|
||||
.host
|
||||
.writeable_path_from_extension(&self.manifest.id, Path::new(&path))?;
|
||||
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::fs::{self, Permissions};
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
return fs::set_permissions(&path, Permissions::from_mode(0o755))
|
||||
.map_err(|error| anyhow!("failed to set permissions for path {path:?}: {error}"))
|
||||
.to_wasmtime_result();
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
Ok(Ok(()))
|
||||
latest::ExtensionImports::make_file_executable(self, path).await
|
||||
}
|
||||
}
|
||||
|
||||
385
crates/extension/src/wasm_host/wit/since_v0_0_6.rs
Normal file
385
crates/extension/src/wasm_host/wit/since_v0_0_6.rs
Normal file
@@ -0,0 +1,385 @@
|
||||
use crate::wasm_host::{wit::ToWasmtimeResult, WasmState};
|
||||
use ::settings::Settings;
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use async_compression::futures::bufread::GzipDecoder;
|
||||
use async_tar::Archive;
|
||||
use async_trait::async_trait;
|
||||
use futures::{io::BufReader, FutureExt as _};
|
||||
use language::{
|
||||
language_settings::AllLanguageSettings, LanguageServerBinaryStatus, LspAdapterDelegate,
|
||||
};
|
||||
use project::project_settings::ProjectSettings;
|
||||
use semantic_version::SemanticVersion;
|
||||
use std::{
|
||||
env,
|
||||
path::{Path, PathBuf},
|
||||
sync::{Arc, OnceLock},
|
||||
};
|
||||
use util::maybe;
|
||||
use wasmtime::component::{Linker, Resource};
|
||||
|
||||
pub const MIN_VERSION: SemanticVersion = SemanticVersion::new(0, 0, 6);
|
||||
pub const MAX_VERSION: SemanticVersion = SemanticVersion::new(0, 0, 6);
|
||||
|
||||
wasmtime::component::bindgen!({
|
||||
async: true,
|
||||
path: "../extension_api/wit/since_v0.0.6",
|
||||
with: {
|
||||
"worktree": ExtensionWorktree,
|
||||
},
|
||||
});
|
||||
|
||||
pub use self::zed::extension::*;
|
||||
|
||||
mod settings {
|
||||
include!("../../../../extension_api/wit/since_v0.0.6/settings.rs");
|
||||
}
|
||||
|
||||
pub type ExtensionWorktree = Arc<dyn LspAdapterDelegate>;
|
||||
|
||||
pub fn linker() -> &'static Linker<WasmState> {
|
||||
static LINKER: OnceLock<Linker<WasmState>> = OnceLock::new();
|
||||
LINKER.get_or_init(|| super::new_linker(Extension::add_to_linker))
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl HostWorktree for WasmState {
|
||||
async fn id(
|
||||
&mut self,
|
||||
delegate: Resource<Arc<dyn LspAdapterDelegate>>,
|
||||
) -> wasmtime::Result<u64> {
|
||||
let delegate = self.table.get(&delegate)?;
|
||||
Ok(delegate.worktree_id())
|
||||
}
|
||||
|
||||
async fn root_path(
|
||||
&mut self,
|
||||
delegate: Resource<Arc<dyn LspAdapterDelegate>>,
|
||||
) -> wasmtime::Result<String> {
|
||||
let delegate = self.table.get(&delegate)?;
|
||||
Ok(delegate.worktree_root_path().to_string_lossy().to_string())
|
||||
}
|
||||
|
||||
async fn read_text_file(
|
||||
&mut self,
|
||||
delegate: Resource<Arc<dyn LspAdapterDelegate>>,
|
||||
path: String,
|
||||
) -> wasmtime::Result<Result<String, String>> {
|
||||
let delegate = self.table.get(&delegate)?;
|
||||
Ok(delegate
|
||||
.read_text_file(path.into())
|
||||
.await
|
||||
.map_err(|error| error.to_string()))
|
||||
}
|
||||
|
||||
async fn shell_env(
|
||||
&mut self,
|
||||
delegate: Resource<Arc<dyn LspAdapterDelegate>>,
|
||||
) -> wasmtime::Result<EnvVars> {
|
||||
let delegate = self.table.get(&delegate)?;
|
||||
Ok(delegate.shell_env().await.into_iter().collect())
|
||||
}
|
||||
|
||||
async fn which(
|
||||
&mut self,
|
||||
delegate: Resource<Arc<dyn LspAdapterDelegate>>,
|
||||
binary_name: String,
|
||||
) -> wasmtime::Result<Option<String>> {
|
||||
let delegate = self.table.get(&delegate)?;
|
||||
Ok(delegate
|
||||
.which(binary_name.as_ref())
|
||||
.await
|
||||
.map(|path| path.to_string_lossy().to_string()))
|
||||
}
|
||||
|
||||
fn drop(&mut self, _worktree: Resource<Worktree>) -> Result<()> {
|
||||
// We only ever hand out borrows of worktrees.
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl nodejs::Host for WasmState {
|
||||
async fn node_binary_path(&mut self) -> wasmtime::Result<Result<String, String>> {
|
||||
self.host
|
||||
.node_runtime
|
||||
.binary_path()
|
||||
.await
|
||||
.map(|path| path.to_string_lossy().to_string())
|
||||
.to_wasmtime_result()
|
||||
}
|
||||
|
||||
async fn npm_package_latest_version(
|
||||
&mut self,
|
||||
package_name: String,
|
||||
) -> wasmtime::Result<Result<String, String>> {
|
||||
self.host
|
||||
.node_runtime
|
||||
.npm_package_latest_version(&package_name)
|
||||
.await
|
||||
.to_wasmtime_result()
|
||||
}
|
||||
|
||||
async fn npm_package_installed_version(
|
||||
&mut self,
|
||||
package_name: String,
|
||||
) -> wasmtime::Result<Result<Option<String>, String>> {
|
||||
self.host
|
||||
.node_runtime
|
||||
.npm_package_installed_version(&self.work_dir(), &package_name)
|
||||
.await
|
||||
.to_wasmtime_result()
|
||||
}
|
||||
|
||||
async fn npm_install_package(
|
||||
&mut self,
|
||||
package_name: String,
|
||||
version: String,
|
||||
) -> wasmtime::Result<Result<(), String>> {
|
||||
self.host
|
||||
.node_runtime
|
||||
.npm_install_packages(&self.work_dir(), &[(&package_name, &version)])
|
||||
.await
|
||||
.to_wasmtime_result()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl lsp::Host for WasmState {}
|
||||
|
||||
#[async_trait]
|
||||
impl github::Host for WasmState {
|
||||
async fn latest_github_release(
|
||||
&mut self,
|
||||
repo: String,
|
||||
options: github::GithubReleaseOptions,
|
||||
) -> wasmtime::Result<Result<github::GithubRelease, String>> {
|
||||
maybe!(async {
|
||||
let release = util::github::latest_github_release(
|
||||
&repo,
|
||||
options.require_assets,
|
||||
options.pre_release,
|
||||
self.host.http_client.clone(),
|
||||
)
|
||||
.await?;
|
||||
Ok(github::GithubRelease {
|
||||
version: release.tag_name,
|
||||
assets: release
|
||||
.assets
|
||||
.into_iter()
|
||||
.map(|asset| github::GithubReleaseAsset {
|
||||
name: asset.name,
|
||||
download_url: asset.browser_download_url,
|
||||
})
|
||||
.collect(),
|
||||
})
|
||||
})
|
||||
.await
|
||||
.to_wasmtime_result()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl platform::Host for WasmState {
|
||||
async fn current_platform(&mut self) -> Result<(platform::Os, platform::Architecture)> {
|
||||
Ok((
|
||||
match env::consts::OS {
|
||||
"macos" => platform::Os::Mac,
|
||||
"linux" => platform::Os::Linux,
|
||||
"windows" => platform::Os::Windows,
|
||||
_ => panic!("unsupported os"),
|
||||
},
|
||||
match env::consts::ARCH {
|
||||
"aarch64" => platform::Architecture::Aarch64,
|
||||
"x86" => platform::Architecture::X86,
|
||||
"x86_64" => platform::Architecture::X8664,
|
||||
_ => panic!("unsupported architecture"),
|
||||
},
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ExtensionImports for WasmState {
|
||||
async fn get_settings(
|
||||
&mut self,
|
||||
location: Option<self::SettingsLocation>,
|
||||
category: String,
|
||||
key: Option<String>,
|
||||
) -> wasmtime::Result<Result<String, String>> {
|
||||
self.on_main_thread(|cx| {
|
||||
async move {
|
||||
let location = location
|
||||
.as_ref()
|
||||
.map(|location| ::settings::SettingsLocation {
|
||||
worktree_id: location.worktree_id as usize,
|
||||
path: Path::new(&location.path),
|
||||
});
|
||||
|
||||
cx.update(|cx| match category.as_str() {
|
||||
"language" => {
|
||||
let settings =
|
||||
AllLanguageSettings::get(location, cx).language(key.as_deref());
|
||||
Ok(serde_json::to_string(&settings::LanguageSettings {
|
||||
tab_size: settings.tab_size,
|
||||
})?)
|
||||
}
|
||||
"lsp" => {
|
||||
let settings = key
|
||||
.and_then(|key| {
|
||||
ProjectSettings::get_global(cx)
|
||||
.lsp
|
||||
.get(&Arc::<str>::from(key))
|
||||
})
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
Ok(serde_json::to_string(&settings::LspSettings {
|
||||
binary: settings.binary.map(|binary| settings::BinarySettings {
|
||||
path: binary.path,
|
||||
arguments: binary.arguments,
|
||||
}),
|
||||
settings: settings.settings,
|
||||
initialization_options: settings.initialization_options,
|
||||
})?)
|
||||
}
|
||||
_ => {
|
||||
bail!("Unknown settings category: {}", category);
|
||||
}
|
||||
})
|
||||
}
|
||||
.boxed_local()
|
||||
})
|
||||
.await?
|
||||
.to_wasmtime_result()
|
||||
}
|
||||
|
||||
async fn set_language_server_installation_status(
|
||||
&mut self,
|
||||
server_name: String,
|
||||
status: LanguageServerInstallationStatus,
|
||||
) -> wasmtime::Result<()> {
|
||||
let status = match status {
|
||||
LanguageServerInstallationStatus::CheckingForUpdate => {
|
||||
LanguageServerBinaryStatus::CheckingForUpdate
|
||||
}
|
||||
LanguageServerInstallationStatus::Downloading => {
|
||||
LanguageServerBinaryStatus::Downloading
|
||||
}
|
||||
LanguageServerInstallationStatus::None => LanguageServerBinaryStatus::None,
|
||||
LanguageServerInstallationStatus::Failed(error) => {
|
||||
LanguageServerBinaryStatus::Failed { error }
|
||||
}
|
||||
};
|
||||
|
||||
self.host
|
||||
.language_registry
|
||||
.update_lsp_status(language::LanguageServerName(server_name.into()), status);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn download_file(
|
||||
&mut self,
|
||||
url: String,
|
||||
path: String,
|
||||
file_type: DownloadedFileType,
|
||||
) -> wasmtime::Result<Result<(), String>> {
|
||||
maybe!(async {
|
||||
let path = PathBuf::from(path);
|
||||
let extension_work_dir = self.host.work_dir.join(self.manifest.id.as_ref());
|
||||
|
||||
self.host.fs.create_dir(&extension_work_dir).await?;
|
||||
|
||||
let destination_path = self
|
||||
.host
|
||||
.writeable_path_from_extension(&self.manifest.id, &path)?;
|
||||
|
||||
let mut response = self
|
||||
.host
|
||||
.http_client
|
||||
.get(&url, Default::default(), true)
|
||||
.await
|
||||
.map_err(|err| anyhow!("error downloading release: {}", err))?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
Err(anyhow!(
|
||||
"download failed with status {}",
|
||||
response.status().to_string()
|
||||
))?;
|
||||
}
|
||||
let body = BufReader::new(response.body_mut());
|
||||
|
||||
match file_type {
|
||||
DownloadedFileType::Uncompressed => {
|
||||
futures::pin_mut!(body);
|
||||
self.host
|
||||
.fs
|
||||
.create_file_with(&destination_path, body)
|
||||
.await?;
|
||||
}
|
||||
DownloadedFileType::Gzip => {
|
||||
let body = GzipDecoder::new(body);
|
||||
futures::pin_mut!(body);
|
||||
self.host
|
||||
.fs
|
||||
.create_file_with(&destination_path, body)
|
||||
.await?;
|
||||
}
|
||||
DownloadedFileType::GzipTar => {
|
||||
let body = GzipDecoder::new(body);
|
||||
futures::pin_mut!(body);
|
||||
self.host
|
||||
.fs
|
||||
.extract_tar_file(&destination_path, Archive::new(body))
|
||||
.await?;
|
||||
}
|
||||
DownloadedFileType::Zip => {
|
||||
let file_name = destination_path
|
||||
.file_name()
|
||||
.ok_or_else(|| anyhow!("invalid download path"))?
|
||||
.to_string_lossy();
|
||||
let zip_filename = format!("{file_name}.zip");
|
||||
let mut zip_path = destination_path.clone();
|
||||
zip_path.set_file_name(zip_filename);
|
||||
|
||||
futures::pin_mut!(body);
|
||||
self.host.fs.create_file_with(&zip_path, body).await?;
|
||||
|
||||
let unzip_status = std::process::Command::new("unzip")
|
||||
.current_dir(&extension_work_dir)
|
||||
.arg("-d")
|
||||
.arg(&destination_path)
|
||||
.arg(&zip_path)
|
||||
.output()?
|
||||
.status;
|
||||
if !unzip_status.success() {
|
||||
Err(anyhow!("failed to unzip {} archive", path.display()))?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
.to_wasmtime_result()
|
||||
}
|
||||
|
||||
async fn make_file_executable(&mut self, path: String) -> wasmtime::Result<Result<(), String>> {
|
||||
#[allow(unused)]
|
||||
let path = self
|
||||
.host
|
||||
.writeable_path_from_extension(&self.manifest.id, Path::new(&path))?;
|
||||
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::fs::{self, Permissions};
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
return fs::set_permissions(&path, Permissions::from_mode(0o755))
|
||||
.map_err(|error| anyhow!("failed to set permissions for path {path:?}: {error}"))
|
||||
.to_wasmtime_result();
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
Ok(Ok(()))
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "zed_extension_api"
|
||||
version = "0.0.5"
|
||||
version = "0.0.6"
|
||||
description = "APIs for creating Zed extensions in Rust"
|
||||
repository = "https://github.com/zed-industries/zed"
|
||||
documentation = "https://docs.rs/zed_extension_api"
|
||||
@@ -15,6 +15,8 @@ workspace = true
|
||||
path = "src/extension_api.rs"
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
wit-bindgen = "0.22"
|
||||
|
||||
[package.metadata.component]
|
||||
|
||||
@@ -54,3 +54,16 @@ To run your extension in Zed as you're developing it:
|
||||
- Open the extensions view using the `zed: extensions` action in the command palette.
|
||||
- Click the `Install Dev Extension` button in the top right
|
||||
- Choose the path to your extension directory.
|
||||
|
||||
## Compatible Zed versions
|
||||
|
||||
Extensions created using newer versions of the Zed extension API won't be compatible with older versions of Zed.
|
||||
|
||||
Here is the compatibility of the `zed_extension_api` with versions of Zed:
|
||||
|
||||
| Zed version | `zed_extension_api` version |
|
||||
| ----------- | --------------------------- |
|
||||
| `0.131.x` | `0.0.1` - `0.0.6` |
|
||||
| `0.130.x` | `0.0.1` - `0.0.5` |
|
||||
| `0.129.x` | `0.0.1` - `0.0.4` |
|
||||
| `0.128.x` | `0.0.1` |
|
||||
|
||||
@@ -1,26 +1,112 @@
|
||||
pub use wit::*;
|
||||
//! The Zed Rust Extension API allows you write extensions for [Zed](https://zed.dev/) in Rust.
|
||||
|
||||
pub mod settings;
|
||||
|
||||
use core::fmt;
|
||||
|
||||
use wit::*;
|
||||
|
||||
pub use serde_json;
|
||||
|
||||
// WIT re-exports.
|
||||
//
|
||||
// We explicitly enumerate the symbols we want to re-export, as there are some
|
||||
// that we may want to shadow to provide a cleaner Rust API.
|
||||
pub use wit::{
|
||||
download_file, make_file_executable,
|
||||
zed::extension::github::{
|
||||
latest_github_release, GithubRelease, GithubReleaseAsset, GithubReleaseOptions,
|
||||
},
|
||||
zed::extension::nodejs::{
|
||||
node_binary_path, npm_install_package, npm_package_installed_version,
|
||||
npm_package_latest_version,
|
||||
},
|
||||
zed::extension::platform::{current_platform, Architecture, Os},
|
||||
CodeLabel, CodeLabelSpan, CodeLabelSpanLiteral, Command, DownloadedFileType, EnvVars,
|
||||
LanguageServerInstallationStatus, Range, Worktree,
|
||||
};
|
||||
|
||||
// Undocumented WIT re-exports.
|
||||
//
|
||||
// These are symbols that need to be public for the purposes of implementing
|
||||
// the extension host, but aren't relevant to extension authors.
|
||||
#[doc(hidden)]
|
||||
pub use wit::Guest;
|
||||
|
||||
/// Constructs for interacting with language servers over the
|
||||
/// Language Server Protocol (LSP).
|
||||
pub mod lsp {
|
||||
pub use crate::wit::zed::extension::lsp::{
|
||||
Completion, CompletionKind, InsertTextFormat, Symbol, SymbolKind,
|
||||
};
|
||||
}
|
||||
|
||||
/// A result returned from a Zed extension.
|
||||
pub type Result<T, E = String> = core::result::Result<T, E>;
|
||||
|
||||
/// Updates the installation status for the given language server.
|
||||
pub fn set_language_server_installation_status(
|
||||
language_server_id: &LanguageServerId,
|
||||
status: &LanguageServerInstallationStatus,
|
||||
) {
|
||||
wit::set_language_server_installation_status(&language_server_id.0, status)
|
||||
}
|
||||
|
||||
/// A Zed extension.
|
||||
pub trait Extension: Send + Sync {
|
||||
/// Returns a new instance of the extension.
|
||||
fn new() -> Self
|
||||
where
|
||||
Self: Sized;
|
||||
|
||||
/// Returns the command used to start the language server for the specified
|
||||
/// language.
|
||||
fn language_server_command(
|
||||
&mut self,
|
||||
config: LanguageServerConfig,
|
||||
language_server_id: &LanguageServerId,
|
||||
worktree: &Worktree,
|
||||
) -> Result<Command>;
|
||||
|
||||
/// Returns the initialization options to pass to the specified language server.
|
||||
fn language_server_initialization_options(
|
||||
&mut self,
|
||||
_config: LanguageServerConfig,
|
||||
_language_server_id: &LanguageServerId,
|
||||
_worktree: &Worktree,
|
||||
) -> Result<Option<String>> {
|
||||
) -> Result<Option<serde_json::Value>> {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Returns the workspace configuration options to pass to the language server.
|
||||
fn language_server_workspace_configuration(
|
||||
&mut self,
|
||||
_language_server_id: &LanguageServerId,
|
||||
_worktree: &Worktree,
|
||||
) -> Result<Option<serde_json::Value>> {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Returns the label for the given completion.
|
||||
fn label_for_completion(
|
||||
&self,
|
||||
_language_server_id: &LanguageServerId,
|
||||
_completion: Completion,
|
||||
) -> Option<CodeLabel> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Returns the label for the given symbol.
|
||||
fn label_for_symbol(
|
||||
&self,
|
||||
_language_server_id: &LanguageServerId,
|
||||
_symbol: Symbol,
|
||||
) -> Option<CodeLabel> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Registers the provided type as a Zed extension.
|
||||
///
|
||||
/// The type must implement the [`Extension`] trait.
|
||||
#[macro_export]
|
||||
macro_rules! register_extension {
|
||||
($extension_type:ty) => {
|
||||
@@ -53,7 +139,7 @@ pub static ZED_API_VERSION: [u8; 6] = *include_bytes!(concat!(env!("OUT_DIR"), "
|
||||
mod wit {
|
||||
wit_bindgen::generate!({
|
||||
skip: ["init-extension"],
|
||||
path: "./wit/since_v0.0.4",
|
||||
path: "./wit/since_v0.0.6",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -63,16 +149,111 @@ struct Component;
|
||||
|
||||
impl wit::Guest for Component {
|
||||
fn language_server_command(
|
||||
config: wit::LanguageServerConfig,
|
||||
language_server_id: String,
|
||||
worktree: &wit::Worktree,
|
||||
) -> Result<wit::Command> {
|
||||
extension().language_server_command(config, worktree)
|
||||
let language_server_id = LanguageServerId(language_server_id);
|
||||
extension().language_server_command(&language_server_id, worktree)
|
||||
}
|
||||
|
||||
fn language_server_initialization_options(
|
||||
config: LanguageServerConfig,
|
||||
language_server_id: String,
|
||||
worktree: &Worktree,
|
||||
) -> Result<Option<String>, String> {
|
||||
extension().language_server_initialization_options(config, worktree)
|
||||
let language_server_id = LanguageServerId(language_server_id);
|
||||
Ok(extension()
|
||||
.language_server_initialization_options(&language_server_id, worktree)?
|
||||
.and_then(|value| serde_json::to_string(&value).ok()))
|
||||
}
|
||||
|
||||
fn language_server_workspace_configuration(
|
||||
language_server_id: String,
|
||||
worktree: &Worktree,
|
||||
) -> Result<Option<String>, String> {
|
||||
let language_server_id = LanguageServerId(language_server_id);
|
||||
Ok(extension()
|
||||
.language_server_workspace_configuration(&language_server_id, worktree)?
|
||||
.and_then(|value| serde_json::to_string(&value).ok()))
|
||||
}
|
||||
|
||||
fn labels_for_completions(
|
||||
language_server_id: String,
|
||||
completions: Vec<Completion>,
|
||||
) -> Result<Vec<Option<CodeLabel>>, String> {
|
||||
let language_server_id = LanguageServerId(language_server_id);
|
||||
let mut labels = Vec::new();
|
||||
for (ix, completion) in completions.into_iter().enumerate() {
|
||||
let label = extension().label_for_completion(&language_server_id, completion);
|
||||
if let Some(label) = label {
|
||||
labels.resize(ix + 1, None);
|
||||
*labels.last_mut().unwrap() = Some(label);
|
||||
}
|
||||
}
|
||||
Ok(labels)
|
||||
}
|
||||
|
||||
fn labels_for_symbols(
|
||||
language_server_id: String,
|
||||
symbols: Vec<Symbol>,
|
||||
) -> Result<Vec<Option<CodeLabel>>, String> {
|
||||
let language_server_id = LanguageServerId(language_server_id);
|
||||
let mut labels = Vec::new();
|
||||
for (ix, symbol) in symbols.into_iter().enumerate() {
|
||||
let label = extension().label_for_symbol(&language_server_id, symbol);
|
||||
if let Some(label) = label {
|
||||
labels.resize(ix + 1, None);
|
||||
*labels.last_mut().unwrap() = Some(label);
|
||||
}
|
||||
}
|
||||
Ok(labels)
|
||||
}
|
||||
}
|
||||
|
||||
/// The ID of a language server.
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)]
|
||||
pub struct LanguageServerId(String);
|
||||
|
||||
impl AsRef<str> for LanguageServerId {
|
||||
fn as_ref(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for LanguageServerId {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl CodeLabelSpan {
|
||||
/// Returns a [`CodeLabelSpan::CodeRange`].
|
||||
pub fn code_range(range: impl Into<wit::Range>) -> Self {
|
||||
Self::CodeRange(range.into())
|
||||
}
|
||||
|
||||
/// Returns a [`CodeLabelSpan::Literal`].
|
||||
pub fn literal(text: impl Into<String>, highlight_name: Option<String>) -> Self {
|
||||
Self::Literal(CodeLabelSpanLiteral {
|
||||
text: text.into(),
|
||||
highlight_name,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::ops::Range<u32>> for wit::Range {
|
||||
fn from(value: std::ops::Range<u32>) -> Self {
|
||||
Self {
|
||||
start: value.start,
|
||||
end: value.end,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::ops::Range<usize>> for wit::Range {
|
||||
fn from(value: std::ops::Range<usize>) -> Self {
|
||||
Self {
|
||||
start: value.start as u32,
|
||||
end: value.end as u32,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
30
crates/extension_api/src/settings.rs
Normal file
30
crates/extension_api/src/settings.rs
Normal file
@@ -0,0 +1,30 @@
|
||||
#[path = "../wit/since_v0.0.6/settings.rs"]
|
||||
mod types;
|
||||
|
||||
use crate::{wit, Result, SettingsLocation, Worktree};
|
||||
use serde_json;
|
||||
pub use types::*;
|
||||
|
||||
impl LanguageSettings {
|
||||
pub fn for_worktree(language: Option<&str>, worktree: &Worktree) -> Result<Self> {
|
||||
let location = SettingsLocation {
|
||||
worktree_id: worktree.id(),
|
||||
path: worktree.root_path(),
|
||||
};
|
||||
let settings_json = wit::get_settings(Some(&location), "language", language)?;
|
||||
let settings: Self = serde_json::from_str(&settings_json).map_err(|err| err.to_string())?;
|
||||
Ok(settings)
|
||||
}
|
||||
}
|
||||
|
||||
impl LspSettings {
|
||||
pub fn for_worktree(language_server_name: &str, worktree: &Worktree) -> Result<Self> {
|
||||
let location = SettingsLocation {
|
||||
worktree_id: worktree.id(),
|
||||
path: worktree.root_path(),
|
||||
};
|
||||
let settings_json = wit::get_settings(Some(&location), "lsp", Some(language_server_name))?;
|
||||
let settings: Self = serde_json::from_str(&settings_json).map_err(|err| err.to_string())?;
|
||||
Ok(settings)
|
||||
}
|
||||
}
|
||||
@@ -1,35 +1,11 @@
|
||||
package zed:extension;
|
||||
|
||||
world extension {
|
||||
use github.{github-release, github-release-options};
|
||||
use platform.{os, architecture};
|
||||
|
||||
export init-extension: func();
|
||||
|
||||
record github-release {
|
||||
version: string,
|
||||
assets: list<github-release-asset>,
|
||||
}
|
||||
|
||||
record github-release-asset {
|
||||
name: string,
|
||||
download-url: string,
|
||||
}
|
||||
|
||||
record github-release-options {
|
||||
require-assets: bool,
|
||||
pre-release: bool,
|
||||
}
|
||||
|
||||
enum os {
|
||||
mac,
|
||||
linux,
|
||||
windows,
|
||||
}
|
||||
|
||||
enum architecture {
|
||||
aarch64,
|
||||
x86,
|
||||
x8664,
|
||||
}
|
||||
|
||||
enum downloaded-file-type {
|
||||
gzip,
|
||||
gzip-tar,
|
||||
|
||||
28
crates/extension_api/wit/since_v0.0.1/github.wit
Normal file
28
crates/extension_api/wit/since_v0.0.1/github.wit
Normal file
@@ -0,0 +1,28 @@
|
||||
interface github {
|
||||
/// A GitHub release.
|
||||
record github-release {
|
||||
/// The version of the release.
|
||||
version: string,
|
||||
/// The list of assets attached to the release.
|
||||
assets: list<github-release-asset>,
|
||||
}
|
||||
|
||||
/// An asset from a GitHub release.
|
||||
record github-release-asset {
|
||||
/// The name of the asset.
|
||||
name: string,
|
||||
/// The download URL for the asset.
|
||||
download-url: string,
|
||||
}
|
||||
|
||||
/// The options used to filter down GitHub releases.
|
||||
record github-release-options {
|
||||
/// Whether releases without assets should be included.
|
||||
require-assets: bool,
|
||||
/// Whether pre-releases should be included.
|
||||
pre-release: bool,
|
||||
}
|
||||
|
||||
/// Returns the latest release for the given GitHub repository.
|
||||
latest-github-release: func(repo: string, options: github-release-options) -> result<github-release, string>;
|
||||
}
|
||||
24
crates/extension_api/wit/since_v0.0.1/platform.wit
Normal file
24
crates/extension_api/wit/since_v0.0.1/platform.wit
Normal file
@@ -0,0 +1,24 @@
|
||||
interface platform {
|
||||
/// An operating system.
|
||||
enum os {
|
||||
/// macOS.
|
||||
mac,
|
||||
/// Linux.
|
||||
linux,
|
||||
/// Windows.
|
||||
windows,
|
||||
}
|
||||
|
||||
/// A platform architecture.
|
||||
enum architecture {
|
||||
/// AArch64 (e.g., Apple Silicon).
|
||||
aarch64,
|
||||
/// x86.
|
||||
x86,
|
||||
/// x86-64.
|
||||
x8664,
|
||||
}
|
||||
|
||||
/// Gets the current operating system and architecture.
|
||||
current-platform: func() -> tuple<os, architecture>;
|
||||
}
|
||||
@@ -1,35 +1,11 @@
|
||||
package zed:extension;
|
||||
|
||||
world extension {
|
||||
use github.{github-release, github-release-options};
|
||||
use platform.{os, architecture};
|
||||
|
||||
export init-extension: func();
|
||||
|
||||
record github-release {
|
||||
version: string,
|
||||
assets: list<github-release-asset>,
|
||||
}
|
||||
|
||||
record github-release-asset {
|
||||
name: string,
|
||||
download-url: string,
|
||||
}
|
||||
|
||||
record github-release-options {
|
||||
require-assets: bool,
|
||||
pre-release: bool,
|
||||
}
|
||||
|
||||
enum os {
|
||||
mac,
|
||||
linux,
|
||||
windows,
|
||||
}
|
||||
|
||||
enum architecture {
|
||||
aarch64,
|
||||
x86,
|
||||
x8664,
|
||||
}
|
||||
|
||||
enum downloaded-file-type {
|
||||
gzip,
|
||||
gzip-tar,
|
||||
|
||||
28
crates/extension_api/wit/since_v0.0.4/github.wit
Normal file
28
crates/extension_api/wit/since_v0.0.4/github.wit
Normal file
@@ -0,0 +1,28 @@
|
||||
interface github {
|
||||
/// A GitHub release.
|
||||
record github-release {
|
||||
/// The version of the release.
|
||||
version: string,
|
||||
/// The list of assets attached to the release.
|
||||
assets: list<github-release-asset>,
|
||||
}
|
||||
|
||||
/// An asset from a GitHub release.
|
||||
record github-release-asset {
|
||||
/// The name of the asset.
|
||||
name: string,
|
||||
/// The download URL for the asset.
|
||||
download-url: string,
|
||||
}
|
||||
|
||||
/// The options used to filter down GitHub releases.
|
||||
record github-release-options {
|
||||
/// Whether releases without assets should be included.
|
||||
require-assets: bool,
|
||||
/// Whether pre-releases should be included.
|
||||
pre-release: bool,
|
||||
}
|
||||
|
||||
/// Returns the latest release for the given GitHub repository.
|
||||
latest-github-release: func(repo: string, options: github-release-options) -> result<github-release, string>;
|
||||
}
|
||||
24
crates/extension_api/wit/since_v0.0.4/platform.wit
Normal file
24
crates/extension_api/wit/since_v0.0.4/platform.wit
Normal file
@@ -0,0 +1,24 @@
|
||||
interface platform {
|
||||
/// An operating system.
|
||||
enum os {
|
||||
/// macOS.
|
||||
mac,
|
||||
/// Linux.
|
||||
linux,
|
||||
/// Windows.
|
||||
windows,
|
||||
}
|
||||
|
||||
/// A platform architecture.
|
||||
enum architecture {
|
||||
/// AArch64 (e.g., Apple Silicon).
|
||||
aarch64,
|
||||
/// x86.
|
||||
x86,
|
||||
/// x86-64.
|
||||
x8664,
|
||||
}
|
||||
|
||||
/// Gets the current operating system and architecture.
|
||||
current-platform: func() -> tuple<os, architecture>;
|
||||
}
|
||||
119
crates/extension_api/wit/since_v0.0.6/extension.wit
Normal file
119
crates/extension_api/wit/since_v0.0.6/extension.wit
Normal file
@@ -0,0 +1,119 @@
|
||||
package zed:extension;
|
||||
|
||||
world extension {
|
||||
import github;
|
||||
import platform;
|
||||
import nodejs;
|
||||
|
||||
use lsp.{completion, symbol};
|
||||
|
||||
/// Initializes the extension.
|
||||
export init-extension: func();
|
||||
|
||||
/// The type of a downloaded file.
|
||||
enum downloaded-file-type {
|
||||
/// A gzipped file (`.gz`).
|
||||
gzip,
|
||||
/// A gzipped tar archive (`.tar.gz`).
|
||||
gzip-tar,
|
||||
/// A ZIP file (`.zip`).
|
||||
zip,
|
||||
/// An uncompressed file.
|
||||
uncompressed,
|
||||
}
|
||||
|
||||
/// The installation status for a language server.
|
||||
variant language-server-installation-status {
|
||||
/// The language server has no installation status.
|
||||
none,
|
||||
/// The language server is being downloaded.
|
||||
downloading,
|
||||
/// The language server is checking for updates.
|
||||
checking-for-update,
|
||||
/// The language server installation failed for specified reason.
|
||||
failed(string),
|
||||
}
|
||||
|
||||
record settings-location {
|
||||
worktree-id: u64,
|
||||
path: string,
|
||||
}
|
||||
|
||||
import get-settings: func(path: option<settings-location>, category: string, key: option<string>) -> result<string, string>;
|
||||
|
||||
/// Downloads a file from the given URL and saves it to the given path within the extension's
|
||||
/// working directory.
|
||||
///
|
||||
/// The file will be extracted according to the given file type.
|
||||
import download-file: func(url: string, file-path: string, file-type: downloaded-file-type) -> result<_, string>;
|
||||
|
||||
/// Makes the file at the given path executable.
|
||||
import make-file-executable: func(filepath: string) -> result<_, string>;
|
||||
|
||||
/// Updates the installation status for the given language server.
|
||||
import set-language-server-installation-status: func(language-server-name: string, status: language-server-installation-status);
|
||||
|
||||
/// A list of environment variables.
|
||||
type env-vars = list<tuple<string, string>>;
|
||||
|
||||
/// A command.
|
||||
record command {
|
||||
/// The command to execute.
|
||||
command: string,
|
||||
/// The arguments to pass to the command.
|
||||
args: list<string>,
|
||||
/// The environment variables to set for the command.
|
||||
env: env-vars,
|
||||
}
|
||||
|
||||
/// A Zed worktree.
|
||||
resource worktree {
|
||||
/// Returns the ID of the worktree.
|
||||
id: func() -> u64;
|
||||
/// Returns the root path of the worktree.
|
||||
root-path: func() -> string;
|
||||
/// Returns the textual contents of the specified file in the worktree.
|
||||
read-text-file: func(path: string) -> result<string, string>;
|
||||
/// Returns the path to the given binary name, if one is present on the `$PATH`.
|
||||
which: func(binary-name: string) -> option<string>;
|
||||
/// Returns the current shell environment.
|
||||
shell-env: func() -> env-vars;
|
||||
}
|
||||
|
||||
/// Returns the command used to start up the language server.
|
||||
export language-server-command: func(language-server-id: string, worktree: borrow<worktree>) -> result<command, string>;
|
||||
|
||||
/// Returns the initialization options to pass to the language server on startup.
|
||||
///
|
||||
/// The initialization options are represented as a JSON string.
|
||||
export language-server-initialization-options: func(language-server-id: string, worktree: borrow<worktree>) -> result<option<string>, string>;
|
||||
|
||||
/// Returns the workspace configuration options to pass to the language server.
|
||||
export language-server-workspace-configuration: func(language-server-id: string, worktree: borrow<worktree>) -> result<option<string>, string>;
|
||||
|
||||
record code-label {
|
||||
/// The source code to parse with Tree-sitter.
|
||||
code: string,
|
||||
spans: list<code-label-span>,
|
||||
filter-range: range,
|
||||
}
|
||||
|
||||
variant code-label-span {
|
||||
/// A range into the parsed code.
|
||||
code-range(range),
|
||||
literal(code-label-span-literal),
|
||||
}
|
||||
|
||||
record code-label-span-literal {
|
||||
text: string,
|
||||
highlight-name: option<string>,
|
||||
}
|
||||
|
||||
record range {
|
||||
start: u32,
|
||||
end: u32,
|
||||
}
|
||||
|
||||
export labels-for-completions: func(language-server-id: string, completions: list<completion>) -> result<list<option<code-label>>, string>;
|
||||
export labels-for-symbols: func(language-server-id: string, symbols: list<symbol>) -> result<list<option<code-label>>, string>;
|
||||
}
|
||||
28
crates/extension_api/wit/since_v0.0.6/github.wit
Normal file
28
crates/extension_api/wit/since_v0.0.6/github.wit
Normal file
@@ -0,0 +1,28 @@
|
||||
interface github {
|
||||
/// A GitHub release.
|
||||
record github-release {
|
||||
/// The version of the release.
|
||||
version: string,
|
||||
/// The list of assets attached to the release.
|
||||
assets: list<github-release-asset>,
|
||||
}
|
||||
|
||||
/// An asset from a GitHub release.
|
||||
record github-release-asset {
|
||||
/// The name of the asset.
|
||||
name: string,
|
||||
/// The download URL for the asset.
|
||||
download-url: string,
|
||||
}
|
||||
|
||||
/// The options used to filter down GitHub releases.
|
||||
record github-release-options {
|
||||
/// Whether releases without assets should be included.
|
||||
require-assets: bool,
|
||||
/// Whether pre-releases should be included.
|
||||
pre-release: bool,
|
||||
}
|
||||
|
||||
/// Returns the latest release for the given GitHub repository.
|
||||
latest-github-release: func(repo: string, options: github-release-options) -> result<github-release, string>;
|
||||
}
|
||||
81
crates/extension_api/wit/since_v0.0.6/lsp.wit
Normal file
81
crates/extension_api/wit/since_v0.0.6/lsp.wit
Normal file
@@ -0,0 +1,81 @@
|
||||
interface lsp {
|
||||
/// An LSP completion.
|
||||
record completion {
|
||||
label: string,
|
||||
detail: option<string>,
|
||||
kind: option<completion-kind>,
|
||||
insert-text-format: option<insert-text-format>,
|
||||
}
|
||||
|
||||
/// The kind of an LSP completion.
|
||||
variant completion-kind {
|
||||
text,
|
||||
method,
|
||||
function,
|
||||
%constructor,
|
||||
field,
|
||||
variable,
|
||||
class,
|
||||
%interface,
|
||||
module,
|
||||
property,
|
||||
unit,
|
||||
value,
|
||||
%enum,
|
||||
keyword,
|
||||
snippet,
|
||||
color,
|
||||
file,
|
||||
reference,
|
||||
folder,
|
||||
enum-member,
|
||||
constant,
|
||||
struct,
|
||||
event,
|
||||
operator,
|
||||
type-parameter,
|
||||
other(s32),
|
||||
}
|
||||
|
||||
/// Defines how to interpret the insert text in a completion item.
|
||||
variant insert-text-format {
|
||||
plain-text,
|
||||
snippet,
|
||||
other(s32),
|
||||
}
|
||||
|
||||
record symbol {
|
||||
kind: symbol-kind,
|
||||
name: string,
|
||||
}
|
||||
|
||||
variant symbol-kind {
|
||||
file,
|
||||
module,
|
||||
namespace,
|
||||
%package,
|
||||
class,
|
||||
method,
|
||||
property,
|
||||
field,
|
||||
%constructor,
|
||||
%enum,
|
||||
%interface,
|
||||
function,
|
||||
variable,
|
||||
constant,
|
||||
%string,
|
||||
number,
|
||||
boolean,
|
||||
array,
|
||||
object,
|
||||
key,
|
||||
null,
|
||||
enum-member,
|
||||
struct,
|
||||
event,
|
||||
operator,
|
||||
type-parameter,
|
||||
other(s32),
|
||||
}
|
||||
}
|
||||
13
crates/extension_api/wit/since_v0.0.6/nodejs.wit
Normal file
13
crates/extension_api/wit/since_v0.0.6/nodejs.wit
Normal file
@@ -0,0 +1,13 @@
|
||||
interface nodejs {
|
||||
/// Returns the path to the Node binary used by Zed.
|
||||
node-binary-path: func() -> result<string, string>;
|
||||
|
||||
/// Returns the latest version of the given NPM package.
|
||||
npm-package-latest-version: func(package-name: string) -> result<string, string>;
|
||||
|
||||
/// Returns the installed version of the given NPM package, if it exists.
|
||||
npm-package-installed-version: func(package-name: string) -> result<option<string>, string>;
|
||||
|
||||
/// Installs the specified NPM package.
|
||||
npm-install-package: func(package-name: string, version: string) -> result<_, string>;
|
||||
}
|
||||
24
crates/extension_api/wit/since_v0.0.6/platform.wit
Normal file
24
crates/extension_api/wit/since_v0.0.6/platform.wit
Normal file
@@ -0,0 +1,24 @@
|
||||
interface platform {
|
||||
/// An operating system.
|
||||
enum os {
|
||||
/// macOS.
|
||||
mac,
|
||||
/// Linux.
|
||||
linux,
|
||||
/// Windows.
|
||||
windows,
|
||||
}
|
||||
|
||||
/// A platform architecture.
|
||||
enum architecture {
|
||||
/// AArch64 (e.g., Apple Silicon).
|
||||
aarch64,
|
||||
/// x86.
|
||||
x86,
|
||||
/// x86-64.
|
||||
x8664,
|
||||
}
|
||||
|
||||
/// Gets the current operating system and architecture.
|
||||
current-platform: func() -> tuple<os, architecture>;
|
||||
}
|
||||
20
crates/extension_api/wit/since_v0.0.6/settings.rs
Normal file
20
crates/extension_api/wit/since_v0.0.6/settings.rs
Normal file
@@ -0,0 +1,20 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::num::NonZeroU32;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct LanguageSettings {
|
||||
pub tab_size: NonZeroU32,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Serialize, Deserialize)]
|
||||
pub struct LspSettings {
|
||||
pub binary: Option<BinarySettings>,
|
||||
pub initialization_options: Option<serde_json::Value>,
|
||||
pub settings: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct BinarySettings {
|
||||
pub path: Option<String>,
|
||||
pub arguments: Option<Vec<String>>,
|
||||
}
|
||||
@@ -22,6 +22,7 @@ fn suggested_extensions() -> &'static HashMap<&'static str, Arc<str>> {
|
||||
("clojure", "cljs"),
|
||||
("clojure", "edn"),
|
||||
("csharp", "cs"),
|
||||
("dart", "dart"),
|
||||
("dockerfile", "Dockerfile"),
|
||||
("elisp", "el"),
|
||||
("erlang", "erl"),
|
||||
@@ -39,6 +40,9 @@ fn suggested_extensions() -> &'static HashMap<&'static str, Arc<str>> {
|
||||
("graphql", "gql"),
|
||||
("graphql", "graphql"),
|
||||
("haskell", "hs"),
|
||||
("html", "htm"),
|
||||
("html", "html"),
|
||||
("html", "shtml"),
|
||||
("java", "java"),
|
||||
("kotlin", "kt"),
|
||||
("latex", "tex"),
|
||||
|
||||
@@ -54,18 +54,20 @@ impl FileIcons {
|
||||
let suffix = path.icon_stem_or_suffix()?;
|
||||
|
||||
if let Some(type_str) = this.stems.get(suffix) {
|
||||
return this
|
||||
.types
|
||||
.get(type_str)
|
||||
.map(|type_config| type_config.icon.clone());
|
||||
return this.get_type_icon(type_str);
|
||||
}
|
||||
|
||||
this.suffixes
|
||||
.get(suffix)
|
||||
.and_then(|type_str| this.types.get(type_str))
|
||||
.map(|type_config| type_config.icon.clone())
|
||||
.and_then(|type_str| this.get_type_icon(type_str))
|
||||
})
|
||||
.or_else(|| this.types.get("default").map(|config| config.icon.clone()))
|
||||
.or_else(|| this.get_type_icon("default"))
|
||||
}
|
||||
|
||||
pub fn get_type_icon(&self, typ: &str) -> Option<Arc<str>> {
|
||||
self.types
|
||||
.get(typ)
|
||||
.map(|type_config| type_config.icon.clone())
|
||||
}
|
||||
|
||||
pub fn get_folder_icon(expanded: bool, cx: &AppContext) -> Option<Arc<str>> {
|
||||
@@ -77,9 +79,7 @@ impl FileIcons {
|
||||
COLLAPSED_DIRECTORY_TYPE
|
||||
};
|
||||
|
||||
this.types
|
||||
.get(key)
|
||||
.map(|type_config| type_config.icon.clone())
|
||||
this.get_type_icon(key)
|
||||
}
|
||||
|
||||
pub fn get_chevron_icon(expanded: bool, cx: &AppContext) -> Option<Arc<str>> {
|
||||
@@ -91,8 +91,6 @@ impl FileIcons {
|
||||
COLLAPSED_CHEVRON_TYPE
|
||||
};
|
||||
|
||||
this.types
|
||||
.get(key)
|
||||
.map(|type_config| type_config.icon.clone())
|
||||
this.get_type_icon(key)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,6 +130,7 @@ impl BufferDiff {
|
||||
|
||||
if end_point.column > 0 {
|
||||
end_point.row += 1;
|
||||
end_point.column = 0;
|
||||
}
|
||||
|
||||
Some(DiffHunk {
|
||||
|
||||
@@ -99,6 +99,8 @@ pub enum ObjectFit {
|
||||
Contain,
|
||||
/// The image will be scaled to cover the bounds of the element.
|
||||
Cover,
|
||||
/// The image will be scaled down to fit within the bounds of the element.
|
||||
ScaleDown,
|
||||
/// The image will maintain its original size.
|
||||
None,
|
||||
}
|
||||
@@ -114,7 +116,7 @@ impl ObjectFit {
|
||||
let image_ratio = image_size.width / image_size.height;
|
||||
let bounds_ratio = bounds.size.width / bounds.size.height;
|
||||
|
||||
match self {
|
||||
let result_bounds = match self {
|
||||
ObjectFit::Fill => bounds,
|
||||
ObjectFit::Contain => {
|
||||
let new_size = if bounds_ratio > image_ratio {
|
||||
@@ -137,6 +139,42 @@ impl ObjectFit {
|
||||
size: new_size,
|
||||
}
|
||||
}
|
||||
ObjectFit::ScaleDown => {
|
||||
// Check if the image is larger than the bounds in either dimension.
|
||||
if image_size.width > bounds.size.width || image_size.height > bounds.size.height {
|
||||
// If the image is larger, use the same logic as Contain to scale it down.
|
||||
let new_size = if bounds_ratio > image_ratio {
|
||||
size(
|
||||
image_size.width * (bounds.size.height / image_size.height),
|
||||
bounds.size.height,
|
||||
)
|
||||
} else {
|
||||
size(
|
||||
bounds.size.width,
|
||||
image_size.height * (bounds.size.width / image_size.width),
|
||||
)
|
||||
};
|
||||
|
||||
Bounds {
|
||||
origin: point(
|
||||
bounds.origin.x + (bounds.size.width - new_size.width) / 2.0,
|
||||
bounds.origin.y + (bounds.size.height - new_size.height) / 2.0,
|
||||
),
|
||||
size: new_size,
|
||||
}
|
||||
} else {
|
||||
// If the image is smaller than or equal to the container, display it at its original size,
|
||||
// centered within the container.
|
||||
let original_size = size(image_size.width, image_size.height);
|
||||
Bounds {
|
||||
origin: point(
|
||||
bounds.origin.x + (bounds.size.width - original_size.width) / 2.0,
|
||||
bounds.origin.y + (bounds.size.height - original_size.height) / 2.0,
|
||||
),
|
||||
size: original_size,
|
||||
}
|
||||
}
|
||||
}
|
||||
ObjectFit::Cover => {
|
||||
let new_size = if bounds_ratio > image_ratio {
|
||||
size(
|
||||
@@ -162,7 +200,9 @@ impl ObjectFit {
|
||||
origin: bounds.origin,
|
||||
size: image_size,
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
result_bounds
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -307,7 +307,6 @@ impl ScrollDelta {
|
||||
}
|
||||
|
||||
/// A mouse exit event from the platform, generated when the mouse leaves the window.
|
||||
/// The position generated should be just outside of the window's bounds.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct MouseExitEvent {
|
||||
/// The position of the mouse relative to the window.
|
||||
|
||||
@@ -66,7 +66,14 @@ pub(crate) fn current_platform() -> Rc<dyn Platform> {
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
pub(crate) fn current_platform() -> Rc<dyn Platform> {
|
||||
Rc::new(LinuxPlatform::new())
|
||||
let wayland_display = std::env::var_os("WAYLAND_DISPLAY");
|
||||
let use_wayland = wayland_display.is_some_and(|display| !display.is_empty());
|
||||
|
||||
if use_wayland {
|
||||
Rc::new(WaylandClient::new())
|
||||
} else {
|
||||
Rc::new(X11Client::new())
|
||||
}
|
||||
}
|
||||
// todo("windows")
|
||||
#[cfg(target_os = "windows")]
|
||||
@@ -207,6 +214,7 @@ pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
|
||||
fn on_appearance_changed(&self, callback: Box<dyn FnMut()>);
|
||||
fn is_topmost_for_position(&self, position: Point<Pixels>) -> bool;
|
||||
fn draw(&self, scene: &Scene);
|
||||
fn completed_frame(&self) {}
|
||||
fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas>;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
mod client;
|
||||
// todo(linux): remove
|
||||
#![allow(unused)]
|
||||
|
||||
mod dispatcher;
|
||||
mod platform;
|
||||
mod text_system;
|
||||
mod util;
|
||||
mod wayland;
|
||||
mod x11;
|
||||
|
||||
pub(crate) use dispatcher::*;
|
||||
pub(crate) use platform::*;
|
||||
pub(crate) use text_system::*;
|
||||
// pub(crate) use x11::*;
|
||||
pub(crate) use wayland::*;
|
||||
pub(crate) use x11::*;
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
|
||||
use copypasta::ClipboardProvider;
|
||||
|
||||
use crate::platform::PlatformWindow;
|
||||
use crate::{AnyWindowHandle, CursorStyle, DisplayId, PlatformDisplay, WindowParams};
|
||||
|
||||
pub trait Client {
|
||||
fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>>;
|
||||
fn primary_display(&self) -> Option<Rc<dyn PlatformDisplay>>;
|
||||
fn display(&self, id: DisplayId) -> Option<Rc<dyn PlatformDisplay>>;
|
||||
fn open_window(
|
||||
&self,
|
||||
handle: AnyWindowHandle,
|
||||
options: WindowParams,
|
||||
) -> Box<dyn PlatformWindow>;
|
||||
fn set_cursor_style(&self, style: CursorStyle);
|
||||
fn get_clipboard(&self) -> Rc<RefCell<dyn ClipboardProvider>>;
|
||||
fn get_primary(&self) -> Rc<RefCell<dyn ClipboardProvider>>;
|
||||
}
|
||||
@@ -101,15 +101,13 @@ impl PlatformDispatcher for LinuxDispatcher {
|
||||
}
|
||||
|
||||
fn dispatch_on_main_thread(&self, runnable: Runnable) {
|
||||
self.main_sender
|
||||
.send(runnable)
|
||||
.expect("Main thread is gone");
|
||||
self.main_sender.send(runnable).ok();
|
||||
}
|
||||
|
||||
fn dispatch_after(&self, duration: Duration, runnable: Runnable) {
|
||||
self.timer_sender
|
||||
.send(TimerAfter { duration, runnable })
|
||||
.expect("Timer thread has died");
|
||||
.ok();
|
||||
}
|
||||
|
||||
fn tick(&self, background_only: bool) -> bool {
|
||||
@@ -117,7 +115,7 @@ impl PlatformDispatcher for LinuxDispatcher {
|
||||
}
|
||||
|
||||
fn park(&self) {
|
||||
self.parker.lock().park()
|
||||
self.parker.lock().park();
|
||||
}
|
||||
|
||||
fn unparker(&self) -> Unparker {
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
#![allow(unused)]
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::any::{type_name, Any};
|
||||
use std::cell::{self, RefCell};
|
||||
use std::env;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use std::panic::Location;
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
process::Command,
|
||||
@@ -13,140 +16,176 @@ use std::{
|
||||
use anyhow::anyhow;
|
||||
use ashpd::desktop::file_chooser::{OpenFileRequest, SaveFileRequest};
|
||||
use async_task::Runnable;
|
||||
use calloop::channel::Channel;
|
||||
use calloop::{EventLoop, LoopHandle, LoopSignal};
|
||||
use copypasta::ClipboardProvider;
|
||||
use flume::{Receiver, Sender};
|
||||
use futures::channel::oneshot;
|
||||
use parking_lot::Mutex;
|
||||
use time::UtcOffset;
|
||||
use wayland_client::Connection;
|
||||
use xkbcommon::xkb::{self, Keycode, Keysym, State};
|
||||
|
||||
use crate::platform::linux::client::Client;
|
||||
use crate::platform::linux::wayland::WaylandClient;
|
||||
use crate::{
|
||||
px, Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DisplayId,
|
||||
ForegroundExecutor, Keymap, LinuxDispatcher, LinuxTextSystem, Menu, PathPromptOptions, Pixels,
|
||||
Platform, PlatformDisplay, PlatformInput, PlatformTextSystem, PlatformWindow, Result,
|
||||
SemanticVersion, Task, WindowOptions, WindowParams,
|
||||
ForegroundExecutor, Keymap, Keystroke, LinuxDispatcher, LinuxTextSystem, Menu, Modifiers,
|
||||
PathPromptOptions, Pixels, Platform, PlatformDisplay, PlatformInput, PlatformInputHandler,
|
||||
PlatformTextSystem, PlatformWindow, Point, PromptLevel, Result, SemanticVersion, Size, Task,
|
||||
WindowAppearance, WindowOptions, WindowParams,
|
||||
};
|
||||
|
||||
use super::x11::X11Client;
|
||||
|
||||
pub(super) const SCROLL_LINES: f64 = 3.0;
|
||||
pub(crate) const SCROLL_LINES: f64 = 3.0;
|
||||
|
||||
// Values match the defaults on GTK.
|
||||
// Taken from https://github.com/GNOME/gtk/blob/main/gtk/gtksettings.c#L320
|
||||
pub(super) const DOUBLE_CLICK_INTERVAL: Duration = Duration::from_millis(400);
|
||||
pub(super) const DOUBLE_CLICK_DISTANCE: Pixels = px(5.0);
|
||||
pub(crate) const DOUBLE_CLICK_INTERVAL: Duration = Duration::from_millis(400);
|
||||
pub(crate) const DOUBLE_CLICK_DISTANCE: Pixels = px(5.0);
|
||||
pub(crate) const KEYRING_LABEL: &str = "zed-github-account";
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct Callbacks {
|
||||
open_urls: Option<Box<dyn FnMut(Vec<String>)>>,
|
||||
become_active: Option<Box<dyn FnMut()>>,
|
||||
resign_active: Option<Box<dyn FnMut()>>,
|
||||
quit: Option<Box<dyn FnMut()>>,
|
||||
reopen: Option<Box<dyn FnMut()>>,
|
||||
event: Option<Box<dyn FnMut(PlatformInput) -> bool>>,
|
||||
app_menu_action: Option<Box<dyn FnMut(&dyn Action)>>,
|
||||
will_open_app_menu: Option<Box<dyn FnMut()>>,
|
||||
validate_app_menu_command: Option<Box<dyn FnMut(&dyn Action) -> bool>>,
|
||||
}
|
||||
pub struct RcRefCell<T>(Rc<RefCell<T>>);
|
||||
|
||||
pub(crate) struct LinuxPlatformInner {
|
||||
pub(crate) event_loop: RefCell<EventLoop<'static, ()>>,
|
||||
pub(crate) loop_handle: Rc<LoopHandle<'static, ()>>,
|
||||
pub(crate) loop_signal: LoopSignal,
|
||||
pub(crate) background_executor: BackgroundExecutor,
|
||||
pub(crate) foreground_executor: ForegroundExecutor,
|
||||
pub(crate) text_system: Arc<LinuxTextSystem>,
|
||||
pub(crate) callbacks: RefCell<Callbacks>,
|
||||
}
|
||||
impl<T> RcRefCell<T> {
|
||||
pub fn new(value: T) -> Self {
|
||||
RcRefCell(Rc::new(RefCell::new(value)))
|
||||
}
|
||||
|
||||
pub(crate) struct LinuxPlatform {
|
||||
client: Rc<dyn Client>,
|
||||
inner: Rc<LinuxPlatformInner>,
|
||||
}
|
||||
#[inline]
|
||||
#[track_caller]
|
||||
pub fn borrow_mut(&self) -> std::cell::RefMut<'_, T> {
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
if option_env!("TRACK_BORROW_MUT").is_some() {
|
||||
eprintln!(
|
||||
"borrow_mut-ing {} at {}",
|
||||
type_name::<T>(),
|
||||
Location::caller()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for LinuxPlatform {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
self.0.borrow_mut()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[track_caller]
|
||||
pub fn borrow(&self) -> std::cell::Ref<'_, T> {
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
if option_env!("TRACK_BORROW_MUT").is_some() {
|
||||
eprintln!("borrow-ing {} at {}", type_name::<T>(), Location::caller());
|
||||
}
|
||||
}
|
||||
|
||||
self.0.borrow()
|
||||
}
|
||||
}
|
||||
|
||||
impl LinuxPlatform {
|
||||
pub(crate) fn new() -> Self {
|
||||
let wayland_display = env::var_os("WAYLAND_DISPLAY");
|
||||
let use_wayland = wayland_display.is_some_and(|display| !display.is_empty());
|
||||
impl<T> Deref for RcRefCell<T> {
|
||||
type Target = Rc<RefCell<T>>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
impl<T> DerefMut for RcRefCell<T> {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Clone for RcRefCell<T> {
|
||||
fn clone(&self) -> Self {
|
||||
RcRefCell(self.0.clone())
|
||||
}
|
||||
}
|
||||
|
||||
pub trait LinuxClient {
|
||||
fn with_common<R>(&self, f: impl FnOnce(&mut LinuxCommon) -> R) -> R;
|
||||
fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>>;
|
||||
fn primary_display(&self) -> Option<Rc<dyn PlatformDisplay>>;
|
||||
fn display(&self, id: DisplayId) -> Option<Rc<dyn PlatformDisplay>>;
|
||||
fn open_window(
|
||||
&self,
|
||||
handle: AnyWindowHandle,
|
||||
options: WindowParams,
|
||||
) -> Box<dyn PlatformWindow>;
|
||||
fn set_cursor_style(&self, style: CursorStyle);
|
||||
fn write_to_clipboard(&self, item: ClipboardItem);
|
||||
fn read_from_clipboard(&self) -> Option<ClipboardItem>;
|
||||
fn run(&self);
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct PlatformHandlers {
|
||||
pub(crate) open_urls: Option<Box<dyn FnMut(Vec<String>)>>,
|
||||
pub(crate) become_active: Option<Box<dyn FnMut()>>,
|
||||
pub(crate) resign_active: Option<Box<dyn FnMut()>>,
|
||||
pub(crate) quit: Option<Box<dyn FnMut()>>,
|
||||
pub(crate) reopen: Option<Box<dyn FnMut()>>,
|
||||
pub(crate) event: Option<Box<dyn FnMut(PlatformInput) -> bool>>,
|
||||
pub(crate) app_menu_action: Option<Box<dyn FnMut(&dyn Action)>>,
|
||||
pub(crate) will_open_app_menu: Option<Box<dyn FnMut()>>,
|
||||
pub(crate) validate_app_menu_command: Option<Box<dyn FnMut(&dyn Action) -> bool>>,
|
||||
}
|
||||
|
||||
pub(crate) struct LinuxCommon {
|
||||
pub(crate) background_executor: BackgroundExecutor,
|
||||
pub(crate) foreground_executor: ForegroundExecutor,
|
||||
pub(crate) text_system: Arc<LinuxTextSystem>,
|
||||
pub(crate) callbacks: PlatformHandlers,
|
||||
pub(crate) signal: LoopSignal,
|
||||
}
|
||||
|
||||
impl LinuxCommon {
|
||||
pub fn new(signal: LoopSignal) -> (Self, Channel<Runnable>) {
|
||||
let (main_sender, main_receiver) = calloop::channel::channel::<Runnable>();
|
||||
let text_system = Arc::new(LinuxTextSystem::new());
|
||||
let callbacks = RefCell::new(Callbacks::default());
|
||||
|
||||
let event_loop = EventLoop::try_new().unwrap();
|
||||
event_loop
|
||||
.handle()
|
||||
.insert_source(main_receiver, |event, _, _| {
|
||||
if let calloop::channel::Event::Msg(runnable) = event {
|
||||
runnable.run();
|
||||
}
|
||||
});
|
||||
let callbacks = PlatformHandlers::default();
|
||||
|
||||
let dispatcher = Arc::new(LinuxDispatcher::new(main_sender));
|
||||
|
||||
let inner = Rc::new(LinuxPlatformInner {
|
||||
loop_handle: Rc::new(event_loop.handle()),
|
||||
loop_signal: event_loop.get_signal(),
|
||||
event_loop: RefCell::new(event_loop),
|
||||
let common = LinuxCommon {
|
||||
background_executor: BackgroundExecutor::new(dispatcher.clone()),
|
||||
foreground_executor: ForegroundExecutor::new(dispatcher.clone()),
|
||||
text_system,
|
||||
callbacks,
|
||||
});
|
||||
signal,
|
||||
};
|
||||
|
||||
if use_wayland {
|
||||
Self {
|
||||
client: Rc::new(WaylandClient::new(Rc::clone(&inner))),
|
||||
inner,
|
||||
}
|
||||
} else {
|
||||
Self {
|
||||
client: X11Client::new(Rc::clone(&inner)),
|
||||
inner,
|
||||
}
|
||||
}
|
||||
(common, main_receiver)
|
||||
}
|
||||
}
|
||||
|
||||
const KEYRING_LABEL: &str = "zed-github-account";
|
||||
|
||||
impl Platform for LinuxPlatform {
|
||||
impl<P: LinuxClient + 'static> Platform for P {
|
||||
fn background_executor(&self) -> BackgroundExecutor {
|
||||
self.inner.background_executor.clone()
|
||||
self.with_common(|common| common.background_executor.clone())
|
||||
}
|
||||
|
||||
fn foreground_executor(&self) -> ForegroundExecutor {
|
||||
self.inner.foreground_executor.clone()
|
||||
self.with_common(|common| common.foreground_executor.clone())
|
||||
}
|
||||
|
||||
fn text_system(&self) -> Arc<dyn PlatformTextSystem> {
|
||||
self.inner.text_system.clone()
|
||||
self.with_common(|common| common.text_system.clone())
|
||||
}
|
||||
|
||||
fn run(&self, on_finish_launching: Box<dyn FnOnce()>) {
|
||||
on_finish_launching();
|
||||
|
||||
self.inner
|
||||
.event_loop
|
||||
.borrow_mut()
|
||||
.run(None, &mut (), |&mut ()| {})
|
||||
.expect("Run loop failed");
|
||||
LinuxClient::run(self);
|
||||
|
||||
if let Some(mut fun) = self.inner.callbacks.borrow_mut().quit.take() {
|
||||
fun();
|
||||
}
|
||||
self.with_common(|common| {
|
||||
if let Some(mut fun) = common.callbacks.quit.take() {
|
||||
fun();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn quit(&self) {
|
||||
self.inner.loop_signal.stop();
|
||||
self.with_common(|common| common.signal.stop());
|
||||
}
|
||||
|
||||
fn restart(&self) {
|
||||
@@ -194,22 +233,23 @@ impl Platform for LinuxPlatform {
|
||||
// todo(linux)
|
||||
fn hide(&self) {}
|
||||
|
||||
// todo(linux)
|
||||
fn hide_other_apps(&self) {}
|
||||
fn hide_other_apps(&self) {
|
||||
log::warn!("hide_other_apps is not implemented on Linux, ignoring the call")
|
||||
}
|
||||
|
||||
// todo(linux)
|
||||
fn unhide_other_apps(&self) {}
|
||||
|
||||
fn primary_display(&self) -> Option<Rc<dyn PlatformDisplay>> {
|
||||
self.client.primary_display()
|
||||
self.primary_display()
|
||||
}
|
||||
|
||||
fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>> {
|
||||
self.client.displays()
|
||||
self.displays()
|
||||
}
|
||||
|
||||
fn display(&self, id: DisplayId) -> Option<Rc<dyn PlatformDisplay>> {
|
||||
self.client.display(id)
|
||||
self.display(id)
|
||||
}
|
||||
|
||||
// todo(linux)
|
||||
@@ -222,7 +262,7 @@ impl Platform for LinuxPlatform {
|
||||
handle: AnyWindowHandle,
|
||||
options: WindowParams,
|
||||
) -> Box<dyn PlatformWindow> {
|
||||
self.client.open_window(handle, options)
|
||||
self.open_window(handle, options)
|
||||
}
|
||||
|
||||
fn open_url(&self, url: &str) {
|
||||
@@ -230,7 +270,7 @@ impl Platform for LinuxPlatform {
|
||||
}
|
||||
|
||||
fn on_open_urls(&self, callback: Box<dyn FnMut(Vec<String>)>) {
|
||||
self.inner.callbacks.borrow_mut().open_urls = Some(callback);
|
||||
self.with_common(|common| common.callbacks.open_urls = Some(callback));
|
||||
}
|
||||
|
||||
fn prompt_for_paths(
|
||||
@@ -238,8 +278,7 @@ impl Platform for LinuxPlatform {
|
||||
options: PathPromptOptions,
|
||||
) -> oneshot::Receiver<Option<Vec<PathBuf>>> {
|
||||
let (done_tx, done_rx) = oneshot::channel();
|
||||
self.inner
|
||||
.foreground_executor
|
||||
self.foreground_executor()
|
||||
.spawn(async move {
|
||||
let title = if options.multiple {
|
||||
if !options.files {
|
||||
@@ -282,8 +321,7 @@ impl Platform for LinuxPlatform {
|
||||
fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver<Option<PathBuf>> {
|
||||
let (done_tx, done_rx) = oneshot::channel();
|
||||
let directory = directory.to_owned();
|
||||
self.inner
|
||||
.foreground_executor
|
||||
self.foreground_executor()
|
||||
.spawn(async move {
|
||||
let result = SaveFileRequest::default()
|
||||
.modal(true)
|
||||
@@ -303,6 +341,7 @@ impl Platform for LinuxPlatform {
|
||||
done_tx.send(result);
|
||||
})
|
||||
.detach();
|
||||
|
||||
done_rx
|
||||
}
|
||||
|
||||
@@ -317,35 +356,51 @@ impl Platform for LinuxPlatform {
|
||||
}
|
||||
|
||||
fn on_become_active(&self, callback: Box<dyn FnMut()>) {
|
||||
self.inner.callbacks.borrow_mut().become_active = Some(callback);
|
||||
self.with_common(|common| {
|
||||
common.callbacks.become_active = Some(callback);
|
||||
});
|
||||
}
|
||||
|
||||
fn on_resign_active(&self, callback: Box<dyn FnMut()>) {
|
||||
self.inner.callbacks.borrow_mut().resign_active = Some(callback);
|
||||
self.with_common(|common| {
|
||||
common.callbacks.resign_active = Some(callback);
|
||||
});
|
||||
}
|
||||
|
||||
fn on_quit(&self, callback: Box<dyn FnMut()>) {
|
||||
self.inner.callbacks.borrow_mut().quit = Some(callback);
|
||||
self.with_common(|common| {
|
||||
common.callbacks.quit = Some(callback);
|
||||
});
|
||||
}
|
||||
|
||||
fn on_reopen(&self, callback: Box<dyn FnMut()>) {
|
||||
self.inner.callbacks.borrow_mut().reopen = Some(callback);
|
||||
self.with_common(|common| {
|
||||
common.callbacks.reopen = Some(callback);
|
||||
});
|
||||
}
|
||||
|
||||
fn on_event(&self, callback: Box<dyn FnMut(PlatformInput) -> bool>) {
|
||||
self.inner.callbacks.borrow_mut().event = Some(callback);
|
||||
self.with_common(|common| {
|
||||
common.callbacks.event = Some(callback);
|
||||
});
|
||||
}
|
||||
|
||||
fn on_app_menu_action(&self, callback: Box<dyn FnMut(&dyn Action)>) {
|
||||
self.inner.callbacks.borrow_mut().app_menu_action = Some(callback);
|
||||
self.with_common(|common| {
|
||||
common.callbacks.app_menu_action = Some(callback);
|
||||
});
|
||||
}
|
||||
|
||||
fn on_will_open_app_menu(&self, callback: Box<dyn FnMut()>) {
|
||||
self.inner.callbacks.borrow_mut().will_open_app_menu = Some(callback);
|
||||
self.with_common(|common| {
|
||||
common.callbacks.will_open_app_menu = Some(callback);
|
||||
});
|
||||
}
|
||||
|
||||
fn on_validate_app_menu_command(&self, callback: Box<dyn FnMut(&dyn Action) -> bool>) {
|
||||
self.inner.callbacks.borrow_mut().validate_app_menu_command = Some(callback);
|
||||
self.with_common(|common| {
|
||||
common.callbacks.validate_app_menu_command = Some(callback);
|
||||
});
|
||||
}
|
||||
|
||||
fn os_name(&self) -> &'static str {
|
||||
@@ -381,7 +436,7 @@ impl Platform for LinuxPlatform {
|
||||
}
|
||||
|
||||
fn set_cursor_style(&self, style: CursorStyle) {
|
||||
self.client.set_cursor_style(style)
|
||||
self.set_cursor_style(style)
|
||||
}
|
||||
|
||||
// todo(linux)
|
||||
@@ -389,23 +444,6 @@ impl Platform for LinuxPlatform {
|
||||
false
|
||||
}
|
||||
|
||||
fn write_to_clipboard(&self, item: ClipboardItem) {
|
||||
let clipboard = self.client.get_clipboard();
|
||||
clipboard.borrow_mut().set_contents(item.text);
|
||||
}
|
||||
|
||||
fn read_from_clipboard(&self) -> Option<ClipboardItem> {
|
||||
let clipboard = self.client.get_clipboard();
|
||||
let contents = clipboard.borrow_mut().get_contents();
|
||||
match contents {
|
||||
Ok(text) => Some(ClipboardItem {
|
||||
metadata: None,
|
||||
text,
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Task<Result<()>> {
|
||||
let url = url.to_string();
|
||||
let username = username.to_string();
|
||||
@@ -479,14 +517,136 @@ impl Platform for LinuxPlatform {
|
||||
fn register_url_scheme(&self, _: &str) -> Task<anyhow::Result<()>> {
|
||||
Task::ready(Err(anyhow!("register_url_scheme unimplemented")))
|
||||
}
|
||||
|
||||
fn write_to_clipboard(&self, item: ClipboardItem) {
|
||||
self.write_to_clipboard(item)
|
||||
}
|
||||
|
||||
fn read_from_clipboard(&self) -> Option<ClipboardItem> {
|
||||
self.read_from_clipboard()
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn is_within_click_distance(a: Point<Pixels>, b: Point<Pixels>) -> bool {
|
||||
let diff = a - b;
|
||||
diff.x.abs() <= DOUBLE_CLICK_DISTANCE && diff.y.abs() <= DOUBLE_CLICK_DISTANCE
|
||||
}
|
||||
|
||||
impl Keystroke {
|
||||
pub(super) fn from_xkb(state: &State, modifiers: Modifiers, keycode: Keycode) -> Self {
|
||||
let mut modifiers = modifiers;
|
||||
|
||||
let key_utf32 = state.key_get_utf32(keycode);
|
||||
let key_utf8 = state.key_get_utf8(keycode);
|
||||
let key_sym = state.key_get_one_sym(keycode);
|
||||
|
||||
// The logic here tries to replicate the logic in `../mac/events.rs`
|
||||
// "Consumed" modifiers are modifiers that have been used to translate a key, for example
|
||||
// pressing "shift" and "1" on US layout produces the key `!` but "consumes" the shift.
|
||||
// Notes:
|
||||
// - macOS gets the key character directly ("."), xkb gives us the key name ("period")
|
||||
// - macOS logic removes consumed shift modifier for symbols: "{", not "shift-{"
|
||||
// - macOS logic keeps consumed shift modifiers for letters: "shift-a", not "a" or "A"
|
||||
|
||||
let mut handle_consumed_modifiers = true;
|
||||
let key = match key_sym {
|
||||
Keysym::Return => "enter".to_owned(),
|
||||
Keysym::Prior => "pageup".to_owned(),
|
||||
Keysym::Next => "pagedown".to_owned(),
|
||||
|
||||
Keysym::comma => ",".to_owned(),
|
||||
Keysym::period => ".".to_owned(),
|
||||
Keysym::less => "<".to_owned(),
|
||||
Keysym::greater => ">".to_owned(),
|
||||
Keysym::slash => "/".to_owned(),
|
||||
Keysym::question => "?".to_owned(),
|
||||
|
||||
Keysym::semicolon => ";".to_owned(),
|
||||
Keysym::colon => ":".to_owned(),
|
||||
Keysym::apostrophe => "'".to_owned(),
|
||||
Keysym::quotedbl => "\"".to_owned(),
|
||||
|
||||
Keysym::bracketleft => "[".to_owned(),
|
||||
Keysym::braceleft => "{".to_owned(),
|
||||
Keysym::bracketright => "]".to_owned(),
|
||||
Keysym::braceright => "}".to_owned(),
|
||||
Keysym::backslash => "\\".to_owned(),
|
||||
Keysym::bar => "|".to_owned(),
|
||||
|
||||
Keysym::grave => "`".to_owned(),
|
||||
Keysym::asciitilde => "~".to_owned(),
|
||||
Keysym::exclam => "!".to_owned(),
|
||||
Keysym::at => "@".to_owned(),
|
||||
Keysym::numbersign => "#".to_owned(),
|
||||
Keysym::dollar => "$".to_owned(),
|
||||
Keysym::percent => "%".to_owned(),
|
||||
Keysym::asciicircum => "^".to_owned(),
|
||||
Keysym::ampersand => "&".to_owned(),
|
||||
Keysym::asterisk => "*".to_owned(),
|
||||
Keysym::parenleft => "(".to_owned(),
|
||||
Keysym::parenright => ")".to_owned(),
|
||||
Keysym::minus => "-".to_owned(),
|
||||
Keysym::underscore => "_".to_owned(),
|
||||
Keysym::equal => "=".to_owned(),
|
||||
Keysym::plus => "+".to_owned(),
|
||||
|
||||
Keysym::ISO_Left_Tab => {
|
||||
handle_consumed_modifiers = false;
|
||||
"tab".to_owned()
|
||||
}
|
||||
|
||||
_ => {
|
||||
handle_consumed_modifiers = false;
|
||||
xkb::keysym_get_name(key_sym).to_lowercase()
|
||||
}
|
||||
};
|
||||
|
||||
// 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))
|
||||
&& !key_utf8.is_empty())
|
||||
.then_some(key_utf8);
|
||||
|
||||
if handle_consumed_modifiers {
|
||||
let mod_shift_index = state.get_keymap().mod_get_index(xkb::MOD_NAME_SHIFT);
|
||||
let is_shift_consumed = state.mod_index_is_consumed(keycode, mod_shift_index);
|
||||
|
||||
if modifiers.shift && is_shift_consumed {
|
||||
modifiers.shift = false;
|
||||
}
|
||||
}
|
||||
|
||||
Keystroke {
|
||||
modifiers,
|
||||
key,
|
||||
ime_key,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{px, Point};
|
||||
|
||||
fn build_platform() -> LinuxPlatform {
|
||||
let platform = LinuxPlatform::new();
|
||||
platform
|
||||
#[test]
|
||||
fn test_is_within_click_distance() {
|
||||
let zero = Point::new(px(0.0), px(0.0));
|
||||
assert_eq!(
|
||||
is_within_click_distance(zero, Point::new(px(5.0), px(5.0))),
|
||||
true
|
||||
);
|
||||
assert_eq!(
|
||||
is_within_click_distance(zero, Point::new(px(-4.9), px(5.0))),
|
||||
true
|
||||
);
|
||||
assert_eq!(
|
||||
is_within_click_distance(Point::new(px(3.0), px(2.0)), Point::new(px(-2.0), px(-2.0))),
|
||||
true
|
||||
);
|
||||
assert_eq!(
|
||||
is_within_click_distance(zero, Point::new(px(5.0), px(5.1))),
|
||||
false
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,128 +0,0 @@
|
||||
use xkbcommon::xkb::{self, Keycode, Keysym, State};
|
||||
|
||||
use super::DOUBLE_CLICK_DISTANCE;
|
||||
use crate::{Keystroke, Modifiers, Pixels, Point};
|
||||
|
||||
impl Keystroke {
|
||||
pub(super) fn from_xkb(state: &State, modifiers: Modifiers, keycode: Keycode) -> Self {
|
||||
let mut modifiers = modifiers;
|
||||
|
||||
let key_utf32 = state.key_get_utf32(keycode);
|
||||
let key_utf8 = state.key_get_utf8(keycode);
|
||||
let key_sym = state.key_get_one_sym(keycode);
|
||||
|
||||
// The logic here tries to replicate the logic in `../mac/events.rs`
|
||||
// "Consumed" modifiers are modifiers that have been used to translate a key, for example
|
||||
// pressing "shift" and "1" on US layout produces the key `!` but "consumes" the shift.
|
||||
// Notes:
|
||||
// - macOS gets the key character directly ("."), xkb gives us the key name ("period")
|
||||
// - macOS logic removes consumed shift modifier for symbols: "{", not "shift-{"
|
||||
// - macOS logic keeps consumed shift modifiers for letters: "shift-a", not "a" or "A"
|
||||
|
||||
let mut handle_consumed_modifiers = true;
|
||||
let key = match key_sym {
|
||||
Keysym::Return => "enter".to_owned(),
|
||||
Keysym::Prior => "pageup".to_owned(),
|
||||
Keysym::Next => "pagedown".to_owned(),
|
||||
|
||||
Keysym::comma => ",".to_owned(),
|
||||
Keysym::period => ".".to_owned(),
|
||||
Keysym::less => "<".to_owned(),
|
||||
Keysym::greater => ">".to_owned(),
|
||||
Keysym::slash => "/".to_owned(),
|
||||
Keysym::question => "?".to_owned(),
|
||||
|
||||
Keysym::semicolon => ";".to_owned(),
|
||||
Keysym::colon => ":".to_owned(),
|
||||
Keysym::apostrophe => "'".to_owned(),
|
||||
Keysym::quotedbl => "\"".to_owned(),
|
||||
|
||||
Keysym::bracketleft => "[".to_owned(),
|
||||
Keysym::braceleft => "{".to_owned(),
|
||||
Keysym::bracketright => "]".to_owned(),
|
||||
Keysym::braceright => "}".to_owned(),
|
||||
Keysym::backslash => "\\".to_owned(),
|
||||
Keysym::bar => "|".to_owned(),
|
||||
|
||||
Keysym::grave => "`".to_owned(),
|
||||
Keysym::asciitilde => "~".to_owned(),
|
||||
Keysym::exclam => "!".to_owned(),
|
||||
Keysym::at => "@".to_owned(),
|
||||
Keysym::numbersign => "#".to_owned(),
|
||||
Keysym::dollar => "$".to_owned(),
|
||||
Keysym::percent => "%".to_owned(),
|
||||
Keysym::asciicircum => "^".to_owned(),
|
||||
Keysym::ampersand => "&".to_owned(),
|
||||
Keysym::asterisk => "*".to_owned(),
|
||||
Keysym::parenleft => "(".to_owned(),
|
||||
Keysym::parenright => ")".to_owned(),
|
||||
Keysym::minus => "-".to_owned(),
|
||||
Keysym::underscore => "_".to_owned(),
|
||||
Keysym::equal => "=".to_owned(),
|
||||
Keysym::plus => "+".to_owned(),
|
||||
|
||||
Keysym::ISO_Left_Tab => {
|
||||
handle_consumed_modifiers = false;
|
||||
"tab".to_owned()
|
||||
}
|
||||
|
||||
_ => {
|
||||
handle_consumed_modifiers = false;
|
||||
xkb::keysym_get_name(key_sym).to_lowercase()
|
||||
}
|
||||
};
|
||||
|
||||
// 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))
|
||||
&& !key_utf8.is_empty())
|
||||
.then_some(key_utf8);
|
||||
|
||||
if handle_consumed_modifiers {
|
||||
let mod_shift_index = state.get_keymap().mod_get_index(xkb::MOD_NAME_SHIFT);
|
||||
let is_shift_consumed = state.mod_index_is_consumed(keycode, mod_shift_index);
|
||||
|
||||
if modifiers.shift && is_shift_consumed {
|
||||
modifiers.shift = false;
|
||||
}
|
||||
}
|
||||
|
||||
Keystroke {
|
||||
modifiers,
|
||||
key,
|
||||
ime_key,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn is_within_click_distance(a: Point<Pixels>, b: Point<Pixels>) -> bool {
|
||||
let diff = a - b;
|
||||
diff.x.abs() <= DOUBLE_CLICK_DISTANCE && diff.y.abs() <= DOUBLE_CLICK_DISTANCE
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{px, Point};
|
||||
|
||||
#[test]
|
||||
fn test_is_within_click_distance() {
|
||||
let zero = Point::new(px(0.0), px(0.0));
|
||||
assert_eq!(
|
||||
is_within_click_distance(zero, Point::new(px(5.0), px(5.0))),
|
||||
true
|
||||
);
|
||||
assert_eq!(
|
||||
is_within_click_distance(zero, Point::new(px(-4.9), px(5.0))),
|
||||
true
|
||||
);
|
||||
assert_eq!(
|
||||
is_within_click_distance(Point::new(px(3.0), px(2.0)), Point::new(px(-2.0), px(-2.0))),
|
||||
true
|
||||
);
|
||||
assert_eq!(
|
||||
is_within_click_distance(zero, Point::new(px(5.0), px(5.1))),
|
||||
false
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,6 @@
|
||||
// todo(linux): remove this once the relevant functionality has been implemented
|
||||
#![allow(unused_variables)]
|
||||
|
||||
pub(crate) use client::*;
|
||||
|
||||
mod client;
|
||||
mod cursor;
|
||||
mod display;
|
||||
mod window;
|
||||
|
||||
pub(crate) use client::*;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,31 +1,31 @@
|
||||
use crate::platform::linux::wayland::WaylandClientState;
|
||||
use wayland_backend::client::InvalidId;
|
||||
use wayland_client::protocol::wl_compositor::WlCompositor;
|
||||
use crate::Globals;
|
||||
use util::ResultExt;
|
||||
|
||||
use wayland_client::protocol::wl_pointer::WlPointer;
|
||||
use wayland_client::protocol::wl_shm::WlShm;
|
||||
use wayland_client::protocol::wl_surface::WlSurface;
|
||||
use wayland_client::{Connection, QueueHandle};
|
||||
use wayland_client::Connection;
|
||||
use wayland_cursor::{CursorImageBuffer, CursorTheme};
|
||||
|
||||
pub(crate) struct Cursor {
|
||||
theme: Result<CursorTheme, InvalidId>,
|
||||
theme: Option<CursorTheme>,
|
||||
current_icon_name: String,
|
||||
surface: WlSurface,
|
||||
serial_id: u32,
|
||||
}
|
||||
|
||||
impl Drop for Cursor {
|
||||
fn drop(&mut self) {
|
||||
self.theme.take();
|
||||
self.surface.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
impl Cursor {
|
||||
pub fn new(
|
||||
connection: &Connection,
|
||||
compositor: &WlCompositor,
|
||||
qh: &QueueHandle<WaylandClientState>,
|
||||
shm: &WlShm,
|
||||
size: u32,
|
||||
) -> Self {
|
||||
pub fn new(connection: &Connection, globals: &Globals, size: u32) -> Self {
|
||||
Self {
|
||||
theme: CursorTheme::load(&connection, shm.clone(), size),
|
||||
current_icon_name: "".to_string(),
|
||||
surface: compositor.create_surface(qh, ()),
|
||||
theme: CursorTheme::load(&connection, globals.shm.clone(), size).log_err(),
|
||||
current_icon_name: "default".to_string(),
|
||||
surface: globals.compositor.create_surface(&globals.qh, ()),
|
||||
serial_id: 0,
|
||||
}
|
||||
}
|
||||
@@ -34,17 +34,17 @@ impl Cursor {
|
||||
self.serial_id = serial_id;
|
||||
}
|
||||
|
||||
pub fn set_icon(&mut self, wl_pointer: &WlPointer, cursor_icon_name: String) {
|
||||
let mut cursor_icon_name = cursor_icon_name.clone();
|
||||
pub fn set_icon(&mut self, wl_pointer: &WlPointer, mut cursor_icon_name: Option<&str>) {
|
||||
let mut cursor_icon_name = cursor_icon_name.unwrap_or("default");
|
||||
if self.current_icon_name != cursor_icon_name {
|
||||
if let Ok(theme) = &mut self.theme {
|
||||
if let Some(theme) = &mut self.theme {
|
||||
let mut buffer: Option<&CursorImageBuffer>;
|
||||
|
||||
if let Some(cursor) = theme.get_cursor(&cursor_icon_name) {
|
||||
buffer = Some(&cursor[0]);
|
||||
} else if let Some(cursor) = theme.get_cursor("default") {
|
||||
buffer = Some(&cursor[0]);
|
||||
cursor_icon_name = "default".to_string();
|
||||
cursor_icon_name = "default";
|
||||
log::warn!(
|
||||
"Linux: Wayland: Unable to get cursor icon: {}. Using default cursor icon",
|
||||
cursor_icon_name
|
||||
@@ -68,7 +68,7 @@ impl Cursor {
|
||||
self.surface.damage(0, 0, width as i32, height as i32);
|
||||
self.surface.commit();
|
||||
|
||||
self.current_icon_name = cursor_icon_name;
|
||||
self.current_icon_name = cursor_icon_name.to_string();
|
||||
}
|
||||
} else {
|
||||
log::warn!("Linux: Wayland: Unable to load cursor themes");
|
||||
|
||||
@@ -7,23 +7,28 @@ use std::sync::Arc;
|
||||
|
||||
use blade_graphics as gpu;
|
||||
use blade_rwh::{HasRawDisplayHandle, HasRawWindowHandle, RawDisplayHandle, RawWindowHandle};
|
||||
use collections::HashSet;
|
||||
use collections::{HashMap, HashSet};
|
||||
use futures::channel::oneshot::Receiver;
|
||||
use raw_window_handle::{
|
||||
DisplayHandle, HandleError, HasDisplayHandle, HasWindowHandle, WindowHandle,
|
||||
};
|
||||
use wayland_backend::client::ObjectId;
|
||||
use wayland_client::WEnum;
|
||||
use wayland_client::{protocol::wl_surface, Proxy};
|
||||
use wayland_protocols::wp::fractional_scale::v1::client::wp_fractional_scale_v1;
|
||||
use wayland_protocols::wp::viewporter::client::wp_viewport;
|
||||
use wayland_protocols::xdg::shell::client::xdg_toplevel;
|
||||
use wayland_protocols::xdg::decoration::zv1::client::zxdg_toplevel_decoration_v1;
|
||||
use wayland_protocols::xdg::shell::client::xdg_surface;
|
||||
use wayland_protocols::xdg::shell::client::xdg_toplevel::{self, WmCapabilities};
|
||||
|
||||
use crate::platform::blade::BladeRenderer;
|
||||
use crate::platform::linux::wayland::display::WaylandDisplay;
|
||||
use crate::platform::{PlatformAtlas, PlatformInputHandler, PlatformWindow};
|
||||
use crate::scene::Scene;
|
||||
use crate::{
|
||||
px, size, Bounds, DevicePixels, Modifiers, Pixels, PlatformDisplay, PlatformInput, Point,
|
||||
PromptLevel, Size, WindowAppearance, WindowBackgroundAppearance, WindowParams,
|
||||
px, size, Bounds, DevicePixels, Globals, Modifiers, Pixels, PlatformDisplay, PlatformInput,
|
||||
Point, PromptLevel, RcRefCell, Size, WindowAppearance, WindowBackgroundAppearance,
|
||||
WindowParams,
|
||||
};
|
||||
|
||||
#[derive(Default)]
|
||||
@@ -39,14 +44,6 @@ pub(crate) struct Callbacks {
|
||||
appearance_changed: Option<Box<dyn FnMut()>>,
|
||||
}
|
||||
|
||||
struct WaylandWindowInner {
|
||||
renderer: BladeRenderer,
|
||||
bounds: Bounds<u32>,
|
||||
scale: f32,
|
||||
input_handler: Option<PlatformInputHandler>,
|
||||
decoration_state: WaylandDecorationState,
|
||||
}
|
||||
|
||||
struct RawWindow {
|
||||
window: *mut c_void,
|
||||
display: *mut c_void,
|
||||
@@ -68,11 +65,36 @@ unsafe impl HasRawDisplayHandle for RawWindow {
|
||||
}
|
||||
}
|
||||
|
||||
impl WaylandWindowInner {
|
||||
fn new(wl_surf: &Arc<wl_surface::WlSurface>, bounds: Bounds<u32>) -> Self {
|
||||
pub struct WaylandWindowState {
|
||||
xdg_surface: xdg_surface::XdgSurface,
|
||||
surface: wl_surface::WlSurface,
|
||||
toplevel: xdg_toplevel::XdgToplevel,
|
||||
viewport: Option<wp_viewport::WpViewport>,
|
||||
outputs: HashSet<ObjectId>,
|
||||
globals: Globals,
|
||||
renderer: BladeRenderer,
|
||||
bounds: Bounds<u32>,
|
||||
scale: f32,
|
||||
input_handler: Option<PlatformInputHandler>,
|
||||
decoration_state: WaylandDecorationState,
|
||||
fullscreen: bool,
|
||||
maximized: bool,
|
||||
}
|
||||
|
||||
impl WaylandWindowState {
|
||||
pub(crate) fn new(
|
||||
surface: wl_surface::WlSurface,
|
||||
xdg_surface: xdg_surface::XdgSurface,
|
||||
viewport: Option<wp_viewport::WpViewport>,
|
||||
toplevel: xdg_toplevel::XdgToplevel,
|
||||
globals: Globals,
|
||||
options: WindowParams,
|
||||
) -> Self {
|
||||
let bounds = options.bounds.map(|p| p.0 as u32);
|
||||
|
||||
let raw = RawWindow {
|
||||
window: wl_surf.id().as_ptr().cast::<c_void>(),
|
||||
display: wl_surf
|
||||
window: surface.id().as_ptr().cast::<c_void>(),
|
||||
display: surface
|
||||
.backend()
|
||||
.upgrade()
|
||||
.unwrap()
|
||||
@@ -97,54 +119,213 @@ impl WaylandWindowInner {
|
||||
height: bounds.size.height,
|
||||
depth: 1,
|
||||
};
|
||||
|
||||
Self {
|
||||
xdg_surface,
|
||||
surface,
|
||||
toplevel,
|
||||
viewport,
|
||||
globals,
|
||||
|
||||
outputs: HashSet::default(),
|
||||
|
||||
renderer: BladeRenderer::new(gpu, extent),
|
||||
bounds,
|
||||
scale: 1.0,
|
||||
input_handler: None,
|
||||
|
||||
// On wayland, decorations are by default provided by the client
|
||||
decoration_state: WaylandDecorationState::Client,
|
||||
fullscreen: false,
|
||||
maximized: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct WaylandWindowState {
|
||||
inner: RefCell<WaylandWindowInner>,
|
||||
pub(crate) callbacks: RefCell<Callbacks>,
|
||||
pub(crate) surface: Arc<wl_surface::WlSurface>,
|
||||
pub(crate) toplevel: Arc<xdg_toplevel::XdgToplevel>,
|
||||
pub(crate) outputs: RefCell<HashSet<ObjectId>>,
|
||||
viewport: Option<wp_viewport::WpViewport>,
|
||||
fullscreen: RefCell<bool>,
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct WaylandWindow {
|
||||
pub(crate) state: RcRefCell<WaylandWindowState>,
|
||||
pub(crate) callbacks: Rc<RefCell<Callbacks>>,
|
||||
}
|
||||
|
||||
impl WaylandWindowState {
|
||||
pub(crate) fn new(
|
||||
wl_surf: Arc<wl_surface::WlSurface>,
|
||||
viewport: Option<wp_viewport::WpViewport>,
|
||||
toplevel: Arc<xdg_toplevel::XdgToplevel>,
|
||||
options: WindowParams,
|
||||
) -> Self {
|
||||
let bounds = options.bounds.map(|p| p.0 as u32);
|
||||
impl WaylandWindow {
|
||||
pub fn ptr_eq(&self, other: &Self) -> bool {
|
||||
Rc::ptr_eq(&self.state, &other.state)
|
||||
}
|
||||
|
||||
Self {
|
||||
surface: Arc::clone(&wl_surf),
|
||||
inner: RefCell::new(WaylandWindowInner::new(&wl_surf, bounds)),
|
||||
callbacks: RefCell::new(Callbacks::default()),
|
||||
outputs: RefCell::new(HashSet::default()),
|
||||
toplevel,
|
||||
pub fn new(globals: Globals, params: WindowParams) -> (Self, ObjectId) {
|
||||
let surface = globals.compositor.create_surface(&globals.qh, ());
|
||||
let xdg_surface = globals
|
||||
.wm_base
|
||||
.get_xdg_surface(&surface, &globals.qh, surface.id());
|
||||
let toplevel = xdg_surface.get_toplevel(&globals.qh, surface.id());
|
||||
|
||||
if let Some(fractional_scale_manager) = globals.fractional_scale_manager.as_ref() {
|
||||
fractional_scale_manager.get_fractional_scale(&surface, &globals.qh, surface.id());
|
||||
}
|
||||
|
||||
// Attempt to set up window decorations based on the requested configuration
|
||||
if let Some(decoration_manager) = globals.decoration_manager.as_ref() {
|
||||
let decoration =
|
||||
decoration_manager.get_toplevel_decoration(&toplevel, &globals.qh, surface.id());
|
||||
|
||||
// Request client side decorations if possible
|
||||
decoration.set_mode(zxdg_toplevel_decoration_v1::Mode::ClientSide);
|
||||
}
|
||||
|
||||
let viewport = globals
|
||||
.viewporter
|
||||
.as_ref()
|
||||
.map(|viewporter| viewporter.get_viewport(&surface, &globals.qh, ()));
|
||||
|
||||
surface.frame(&globals.qh, surface.id());
|
||||
|
||||
let window_state = RcRefCell::new(WaylandWindowState::new(
|
||||
surface.clone(),
|
||||
xdg_surface,
|
||||
viewport,
|
||||
fullscreen: RefCell::new(false),
|
||||
toplevel,
|
||||
globals,
|
||||
params,
|
||||
));
|
||||
|
||||
let this = Self {
|
||||
state: window_state,
|
||||
callbacks: Rc::new(RefCell::new(Callbacks::default())),
|
||||
};
|
||||
|
||||
// Kick things off
|
||||
surface.commit();
|
||||
|
||||
(this, surface.id())
|
||||
}
|
||||
|
||||
pub fn frame(&self) {
|
||||
let state = self.state.borrow_mut();
|
||||
state.surface.frame(&state.globals.qh, state.surface.id());
|
||||
drop(state);
|
||||
|
||||
let mut cb = self.callbacks.borrow_mut();
|
||||
if let Some(fun) = cb.request_frame.as_mut() {
|
||||
fun();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(&self) {
|
||||
let mut cb = self.callbacks.borrow_mut();
|
||||
if let Some(mut fun) = cb.request_frame.take() {
|
||||
drop(cb);
|
||||
fun();
|
||||
self.callbacks.borrow_mut().request_frame = Some(fun);
|
||||
pub fn handle_toplevel_decoration_event(&self, event: zxdg_toplevel_decoration_v1::Event) {
|
||||
match event {
|
||||
zxdg_toplevel_decoration_v1::Event::Configure { mode } => match mode {
|
||||
WEnum::Value(zxdg_toplevel_decoration_v1::Mode::ServerSide) => {
|
||||
self.set_decoration_state(WaylandDecorationState::Server)
|
||||
}
|
||||
WEnum::Value(zxdg_toplevel_decoration_v1::Mode::ClientSide) => {
|
||||
self.set_decoration_state(WaylandDecorationState::Server)
|
||||
}
|
||||
WEnum::Value(_) => {
|
||||
log::warn!("Unknown decoration mode");
|
||||
}
|
||||
WEnum::Unknown(v) => {
|
||||
log::warn!("Unknown decoration mode: {}", v);
|
||||
}
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_fractional_scale_event(&self, event: wp_fractional_scale_v1::Event) {
|
||||
match event {
|
||||
wp_fractional_scale_v1::Event::PreferredScale { scale } => {
|
||||
self.rescale(scale as f32 / 120.0);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_toplevel_event(&self, event: xdg_toplevel::Event) -> bool {
|
||||
match event {
|
||||
xdg_toplevel::Event::Configure {
|
||||
width,
|
||||
height,
|
||||
states,
|
||||
} => {
|
||||
let width = NonZeroU32::new(width as u32);
|
||||
let height = NonZeroU32::new(height as u32);
|
||||
let fullscreen = states.contains(&(xdg_toplevel::State::Fullscreen as u8));
|
||||
let maximized = states.contains(&(xdg_toplevel::State::Maximized as u8));
|
||||
self.resize(width, height);
|
||||
self.set_fullscreen(fullscreen);
|
||||
let mut state = self.state.borrow_mut();
|
||||
state.maximized = true;
|
||||
false
|
||||
}
|
||||
xdg_toplevel::Event::Close => {
|
||||
let mut cb = self.callbacks.borrow_mut();
|
||||
if let Some(mut should_close) = cb.should_close.take() {
|
||||
let result = (should_close)();
|
||||
cb.should_close = Some(should_close);
|
||||
result
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_surface_event(
|
||||
&self,
|
||||
event: wl_surface::Event,
|
||||
output_scales: HashMap<ObjectId, i32>,
|
||||
) {
|
||||
let mut state = self.state.borrow_mut();
|
||||
|
||||
// We use `WpFractionalScale` instead to set the scale if it's available
|
||||
if state.globals.fractional_scale_manager.is_some() {
|
||||
return;
|
||||
}
|
||||
|
||||
match event {
|
||||
wl_surface::Event::Enter { output } => {
|
||||
// We use `PreferredBufferScale` instead to set the scale if it's available
|
||||
if state.surface.version() >= wl_surface::EVT_PREFERRED_BUFFER_SCALE_SINCE {
|
||||
return;
|
||||
}
|
||||
|
||||
state.outputs.insert(output.id());
|
||||
|
||||
let mut scale = 1;
|
||||
for output in state.outputs.iter() {
|
||||
if let Some(s) = output_scales.get(output) {
|
||||
scale = scale.max(*s)
|
||||
}
|
||||
}
|
||||
|
||||
state.surface.set_buffer_scale(scale);
|
||||
drop(state);
|
||||
self.rescale(scale as f32);
|
||||
}
|
||||
wl_surface::Event::Leave { output } => {
|
||||
// We use `PreferredBufferScale` instead to set the scale if it's available
|
||||
if state.surface.version() >= wl_surface::EVT_PREFERRED_BUFFER_SCALE_SINCE {
|
||||
return;
|
||||
}
|
||||
|
||||
state.outputs.remove(&output.id());
|
||||
|
||||
let mut scale = 1;
|
||||
for output in state.outputs.iter() {
|
||||
if let Some(s) = output_scales.get(output) {
|
||||
scale = scale.max(*s)
|
||||
}
|
||||
}
|
||||
|
||||
state.surface.set_buffer_scale(scale);
|
||||
drop(state);
|
||||
self.rescale(scale as f32);
|
||||
}
|
||||
wl_surface::Event::PreferredBufferScale { factor } => {
|
||||
state.surface.set_buffer_scale(factor);
|
||||
drop(state);
|
||||
self.rescale(factor as f32);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,26 +336,26 @@ impl WaylandWindowState {
|
||||
scale: Option<f32>,
|
||||
) {
|
||||
let (width, height, scale) = {
|
||||
let mut inner = self.inner.borrow_mut();
|
||||
if width.map_or(true, |width| width.get() == inner.bounds.size.width)
|
||||
&& height.map_or(true, |height| height.get() == inner.bounds.size.height)
|
||||
&& scale.map_or(true, |scale| scale == inner.scale)
|
||||
let mut state = self.state.borrow_mut();
|
||||
if width.map_or(true, |width| width.get() == state.bounds.size.width)
|
||||
&& height.map_or(true, |height| height.get() == state.bounds.size.height)
|
||||
&& scale.map_or(true, |scale| scale == state.scale)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if let Some(width) = width {
|
||||
inner.bounds.size.width = width.get();
|
||||
state.bounds.size.width = width.get();
|
||||
}
|
||||
if let Some(height) = height {
|
||||
inner.bounds.size.height = height.get();
|
||||
state.bounds.size.height = height.get();
|
||||
}
|
||||
if let Some(scale) = scale {
|
||||
inner.scale = scale;
|
||||
state.scale = scale;
|
||||
}
|
||||
let width = inner.bounds.size.width;
|
||||
let height = inner.bounds.size.height;
|
||||
let scale = inner.scale;
|
||||
inner.renderer.update_drawable_size(size(
|
||||
let width = state.bounds.size.width;
|
||||
let height = state.bounds.size.height;
|
||||
let scale = state.scale;
|
||||
state.renderer.update_drawable_size(size(
|
||||
width as f64 * scale as f64,
|
||||
height as f64 * scale as f64,
|
||||
));
|
||||
@@ -191,8 +372,11 @@ impl WaylandWindowState {
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(viewport) = &self.viewport {
|
||||
viewport.set_destination(width as i32, height as i32);
|
||||
{
|
||||
let state = self.state.borrow();
|
||||
if let Some(viewport) = &state.viewport {
|
||||
viewport.set_destination(width as i32, height as i32);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -205,11 +389,13 @@ impl WaylandWindowState {
|
||||
}
|
||||
|
||||
pub fn set_fullscreen(&self, fullscreen: bool) {
|
||||
let mut state = self.state.borrow_mut();
|
||||
state.fullscreen = fullscreen;
|
||||
|
||||
let mut callbacks = self.callbacks.borrow_mut();
|
||||
if let Some(ref mut fun) = callbacks.fullscreen {
|
||||
fun(fullscreen)
|
||||
}
|
||||
self.fullscreen.replace(fullscreen);
|
||||
}
|
||||
|
||||
/// Notifies the window of the state of the decorations.
|
||||
@@ -221,9 +407,7 @@ impl WaylandWindowState {
|
||||
/// of the decorations. This is because the state of the decorations
|
||||
/// is managed by the compositor and not the client.
|
||||
pub fn set_decoration_state(&self, state: WaylandDecorationState) {
|
||||
self.inner.borrow_mut().decoration_state = state;
|
||||
log::trace!("Window decorations are now handled by {:?}", state);
|
||||
// todo(linux) - Handle this properly
|
||||
self.state.borrow_mut().decoration_state = state;
|
||||
}
|
||||
|
||||
pub fn close(&self) {
|
||||
@@ -231,7 +415,7 @@ impl WaylandWindowState {
|
||||
if let Some(fun) = callbacks.close.take() {
|
||||
fun()
|
||||
}
|
||||
self.toplevel.destroy();
|
||||
self.state.borrow_mut().toplevel.destroy();
|
||||
}
|
||||
|
||||
pub fn handle_input(&self, input: PlatformInput) {
|
||||
@@ -241,10 +425,13 @@ impl WaylandWindowState {
|
||||
}
|
||||
}
|
||||
if let PlatformInput::KeyDown(event) = input {
|
||||
let mut inner = self.inner.borrow_mut();
|
||||
if let Some(ref mut input_handler) = inner.input_handler {
|
||||
let mut state = self.state.borrow_mut();
|
||||
if let Some(mut input_handler) = state.input_handler.take() {
|
||||
if let Some(ime_key) = &event.keystroke.ime_key {
|
||||
drop(state);
|
||||
input_handler.replace_text_in_range(None, ime_key);
|
||||
let mut state = self.state.borrow_mut();
|
||||
state.input_handler = Some(input_handler);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -257,9 +444,6 @@ impl WaylandWindowState {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct WaylandWindow(pub(crate) Rc<WaylandWindowState>);
|
||||
|
||||
impl HasWindowHandle for WaylandWindow {
|
||||
fn window_handle(&self) -> Result<WindowHandle<'_>, HandleError> {
|
||||
unimplemented!()
|
||||
@@ -273,31 +457,29 @@ impl HasDisplayHandle for WaylandWindow {
|
||||
}
|
||||
|
||||
impl PlatformWindow for WaylandWindow {
|
||||
// todo(linux)
|
||||
fn bounds(&self) -> Bounds<DevicePixels> {
|
||||
unimplemented!()
|
||||
self.state.borrow().bounds.map(|p| DevicePixels(p as i32))
|
||||
}
|
||||
|
||||
// todo(linux)
|
||||
fn is_maximized(&self) -> bool {
|
||||
false
|
||||
self.state.borrow().maximized
|
||||
}
|
||||
|
||||
// todo(linux)
|
||||
fn is_minimized(&self) -> bool {
|
||||
// This cannot be determined by the client
|
||||
false
|
||||
}
|
||||
|
||||
fn content_size(&self) -> Size<Pixels> {
|
||||
let inner = self.0.inner.borrow();
|
||||
let state = self.state.borrow();
|
||||
Size {
|
||||
width: Pixels(inner.bounds.size.width as f32),
|
||||
height: Pixels(inner.bounds.size.height as f32),
|
||||
width: Pixels(state.bounds.size.width as f32),
|
||||
height: Pixels(state.bounds.size.height as f32),
|
||||
}
|
||||
}
|
||||
|
||||
fn scale_factor(&self) -> f32 {
|
||||
self.0.inner.borrow().scale
|
||||
self.state.borrow().scale
|
||||
}
|
||||
|
||||
// todo(linux)
|
||||
@@ -325,11 +507,11 @@ impl PlatformWindow for WaylandWindow {
|
||||
}
|
||||
|
||||
fn set_input_handler(&mut self, input_handler: PlatformInputHandler) {
|
||||
self.0.inner.borrow_mut().input_handler = Some(input_handler);
|
||||
self.state.borrow_mut().input_handler = Some(input_handler);
|
||||
}
|
||||
|
||||
fn take_input_handler(&mut self) -> Option<PlatformInputHandler> {
|
||||
self.0.inner.borrow_mut().input_handler.take()
|
||||
self.state.borrow_mut().input_handler.take()
|
||||
}
|
||||
|
||||
fn prompt(
|
||||
@@ -352,7 +534,10 @@ impl PlatformWindow for WaylandWindow {
|
||||
}
|
||||
|
||||
fn set_title(&mut self, title: &str) {
|
||||
self.0.toplevel.set_title(title.to_string());
|
||||
self.state
|
||||
.borrow_mut()
|
||||
.toplevel
|
||||
.set_title(title.to_string());
|
||||
}
|
||||
|
||||
fn set_background_appearance(&mut self, _background_appearance: WindowBackgroundAppearance) {
|
||||
@@ -368,7 +553,7 @@ impl PlatformWindow for WaylandWindow {
|
||||
}
|
||||
|
||||
fn minimize(&self) {
|
||||
self.0.toplevel.set_minimized();
|
||||
self.state.borrow_mut().toplevel.set_minimized();
|
||||
}
|
||||
|
||||
fn zoom(&self) {
|
||||
@@ -376,47 +561,48 @@ impl PlatformWindow for WaylandWindow {
|
||||
}
|
||||
|
||||
fn toggle_fullscreen(&self) {
|
||||
if !(*self.0.fullscreen.borrow()) {
|
||||
self.0.toplevel.set_fullscreen(None);
|
||||
let state = self.state.borrow_mut();
|
||||
if !state.fullscreen {
|
||||
state.toplevel.set_fullscreen(None);
|
||||
} else {
|
||||
self.0.toplevel.unset_fullscreen();
|
||||
state.toplevel.unset_fullscreen();
|
||||
}
|
||||
}
|
||||
|
||||
fn is_fullscreen(&self) -> bool {
|
||||
*self.0.fullscreen.borrow()
|
||||
self.state.borrow().fullscreen
|
||||
}
|
||||
|
||||
fn on_request_frame(&self, callback: Box<dyn FnMut()>) {
|
||||
self.0.callbacks.borrow_mut().request_frame = Some(callback);
|
||||
self.callbacks.borrow_mut().request_frame = Some(callback);
|
||||
}
|
||||
|
||||
fn on_input(&self, callback: Box<dyn FnMut(PlatformInput) -> crate::DispatchEventResult>) {
|
||||
self.0.callbacks.borrow_mut().input = Some(callback);
|
||||
self.callbacks.borrow_mut().input = Some(callback);
|
||||
}
|
||||
|
||||
fn on_active_status_change(&self, callback: Box<dyn FnMut(bool)>) {
|
||||
self.0.callbacks.borrow_mut().active_status_change = Some(callback);
|
||||
self.callbacks.borrow_mut().active_status_change = Some(callback);
|
||||
}
|
||||
|
||||
fn on_resize(&self, callback: Box<dyn FnMut(Size<Pixels>, f32)>) {
|
||||
self.0.callbacks.borrow_mut().resize = Some(callback);
|
||||
self.callbacks.borrow_mut().resize = Some(callback);
|
||||
}
|
||||
|
||||
fn on_fullscreen(&self, callback: Box<dyn FnMut(bool)>) {
|
||||
self.0.callbacks.borrow_mut().fullscreen = Some(callback);
|
||||
self.callbacks.borrow_mut().fullscreen = Some(callback);
|
||||
}
|
||||
|
||||
fn on_moved(&self, callback: Box<dyn FnMut()>) {
|
||||
self.0.callbacks.borrow_mut().moved = Some(callback);
|
||||
self.callbacks.borrow_mut().moved = Some(callback);
|
||||
}
|
||||
|
||||
fn on_should_close(&self, callback: Box<dyn FnMut() -> bool>) {
|
||||
self.0.callbacks.borrow_mut().should_close = Some(callback);
|
||||
self.callbacks.borrow_mut().should_close = Some(callback);
|
||||
}
|
||||
|
||||
fn on_close(&self, callback: Box<dyn FnOnce()>) {
|
||||
self.0.callbacks.borrow_mut().close = Some(callback);
|
||||
self.callbacks.borrow_mut().close = Some(callback);
|
||||
}
|
||||
|
||||
fn on_appearance_changed(&self, callback: Box<dyn FnMut()>) {
|
||||
@@ -429,12 +615,18 @@ impl PlatformWindow for WaylandWindow {
|
||||
}
|
||||
|
||||
fn draw(&self, scene: &Scene) {
|
||||
self.0.inner.borrow_mut().renderer.draw(scene);
|
||||
let mut state = self.state.borrow_mut();
|
||||
state.renderer.draw(scene);
|
||||
}
|
||||
|
||||
fn completed_frame(&self) {
|
||||
let mut state = self.state.borrow_mut();
|
||||
state.surface.commit();
|
||||
}
|
||||
|
||||
fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas> {
|
||||
let inner = self.0.inner.borrow();
|
||||
inner.renderer.sprite_atlas().clone()
|
||||
let state = self.state.borrow();
|
||||
state.renderer.sprite_atlas().clone()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
use std::cell::RefCell;
|
||||
use std::ops::Deref;
|
||||
use std::rc::Rc;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use calloop::{EventLoop, LoopHandle};
|
||||
use collections::HashMap;
|
||||
use copypasta::x11_clipboard::{Clipboard, Primary, X11ClipboardContext};
|
||||
use copypasta::ClipboardProvider;
|
||||
|
||||
use util::ResultExt;
|
||||
use x11rb::connection::{Connection, RequestConnection};
|
||||
use x11rb::errors::ConnectionError;
|
||||
use x11rb::protocol::randr::ConnectionExt as _;
|
||||
@@ -16,50 +19,71 @@ use x11rb::xcb_ffi::XCBConnection;
|
||||
use xkbc::x11::ffi::{XKB_X11_MIN_MAJOR_XKB_VERSION, XKB_X11_MIN_MINOR_XKB_VERSION};
|
||||
use xkbcommon::xkb as xkbc;
|
||||
|
||||
use crate::platform::linux::client::Client;
|
||||
use crate::platform::{LinuxPlatformInner, PlatformWindow};
|
||||
use crate::platform::linux::LinuxClient;
|
||||
use crate::platform::{LinuxCommon, PlatformWindow};
|
||||
use crate::{
|
||||
px, AnyWindowHandle, Bounds, CursorStyle, DisplayId, Pixels, PlatformDisplay, PlatformInput,
|
||||
Point, ScrollDelta, Size, TouchPhase, WindowParams,
|
||||
};
|
||||
|
||||
use super::{super::SCROLL_LINES, X11Display, X11Window, X11WindowState, XcbAtoms};
|
||||
use super::{super::SCROLL_LINES, X11Display, X11Window, XcbAtoms};
|
||||
use super::{button_of_key, modifiers_from_state};
|
||||
use crate::platform::linux::is_within_click_distance;
|
||||
use crate::platform::linux::platform::DOUBLE_CLICK_INTERVAL;
|
||||
use crate::platform::linux::util::is_within_click_distance;
|
||||
use calloop::{
|
||||
generic::{FdWrapper, Generic},
|
||||
RegistrationToken,
|
||||
};
|
||||
|
||||
struct WindowRef {
|
||||
state: Rc<X11WindowState>,
|
||||
pub(crate) struct WindowRef {
|
||||
window: X11Window,
|
||||
refresh_event_token: RegistrationToken,
|
||||
}
|
||||
|
||||
struct X11ClientState {
|
||||
windows: HashMap<xproto::Window, WindowRef>,
|
||||
xkb: xkbc::State,
|
||||
clipboard: Rc<RefCell<X11ClipboardContext<Clipboard>>>,
|
||||
primary: Rc<RefCell<X11ClipboardContext<Primary>>>,
|
||||
click_state: ClickState,
|
||||
impl Deref for WindowRef {
|
||||
type Target = X11Window;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.window
|
||||
}
|
||||
}
|
||||
|
||||
struct ClickState {
|
||||
last_click: Instant,
|
||||
last_location: Point<Pixels>,
|
||||
current_count: usize,
|
||||
pub struct X11ClientState {
|
||||
pub(crate) loop_handle: LoopHandle<'static, X11Client>,
|
||||
pub(crate) event_loop: Option<calloop::EventLoop<'static, X11Client>>,
|
||||
|
||||
pub(crate) last_click: Instant,
|
||||
pub(crate) last_location: Point<Pixels>,
|
||||
pub(crate) current_count: usize,
|
||||
|
||||
pub(crate) xcb_connection: Rc<XCBConnection>,
|
||||
pub(crate) x_root_index: usize,
|
||||
pub(crate) atoms: XcbAtoms,
|
||||
pub(crate) windows: HashMap<xproto::Window, WindowRef>,
|
||||
pub(crate) xkb: xkbc::State,
|
||||
|
||||
pub(crate) common: LinuxCommon,
|
||||
pub(crate) clipboard: X11ClipboardContext<Clipboard>,
|
||||
pub(crate) primary: X11ClipboardContext<Primary>,
|
||||
}
|
||||
|
||||
pub(crate) struct X11Client {
|
||||
platform_inner: Rc<LinuxPlatformInner>,
|
||||
xcb_connection: Rc<XCBConnection>,
|
||||
x_root_index: usize,
|
||||
atoms: XcbAtoms,
|
||||
state: RefCell<X11ClientState>,
|
||||
}
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct X11Client(Rc<RefCell<X11ClientState>>);
|
||||
|
||||
impl X11Client {
|
||||
pub(crate) fn new(inner: Rc<LinuxPlatformInner>) -> Rc<Self> {
|
||||
pub(crate) fn new() -> Self {
|
||||
let event_loop = EventLoop::try_new().unwrap();
|
||||
|
||||
let (common, main_receiver) = LinuxCommon::new(event_loop.get_signal());
|
||||
|
||||
let handle = event_loop.handle();
|
||||
|
||||
handle.insert_source(main_receiver, |event, _, _: &mut X11Client| {
|
||||
if let calloop::channel::Event::Msg(runnable) = event {
|
||||
runnable.run();
|
||||
}
|
||||
});
|
||||
|
||||
let (xcb_connection, x_root_index) = XCBConnection::connect(None).unwrap();
|
||||
xcb_connection
|
||||
.prefetch_extension_information(xkb::X11_EXTENSION_NAME)
|
||||
@@ -94,30 +118,10 @@ impl X11Client {
|
||||
|
||||
let xcb_connection = Rc::new(xcb_connection);
|
||||
|
||||
let click_state = ClickState {
|
||||
last_click: Instant::now(),
|
||||
last_location: Point::new(px(0.0), px(0.0)),
|
||||
current_count: 0,
|
||||
};
|
||||
let client: Rc<X11Client> = Rc::new(Self {
|
||||
platform_inner: inner.clone(),
|
||||
xcb_connection: Rc::clone(&xcb_connection),
|
||||
x_root_index,
|
||||
atoms,
|
||||
state: RefCell::new(X11ClientState {
|
||||
windows: HashMap::default(),
|
||||
xkb: xkb_state,
|
||||
clipboard: Rc::new(RefCell::new(clipboard)),
|
||||
primary: Rc::new(RefCell::new(primary)),
|
||||
click_state,
|
||||
}),
|
||||
});
|
||||
|
||||
// Safety: Safe if xcb::Connection always returns a valid fd
|
||||
let fd = unsafe { FdWrapper::new(Rc::clone(&xcb_connection)) };
|
||||
|
||||
inner
|
||||
.loop_handle
|
||||
handle
|
||||
.insert_source(
|
||||
Generic::new_with_error::<ConnectionError>(
|
||||
fd,
|
||||
@@ -125,8 +129,8 @@ impl X11Client {
|
||||
calloop::Mode::Level,
|
||||
),
|
||||
{
|
||||
let client = Rc::clone(&client);
|
||||
move |_readiness, _, _| {
|
||||
let xcb_connection = xcb_connection.clone();
|
||||
move |_readiness, _, client| {
|
||||
while let Some(event) = xcb_connection.poll_for_event()? {
|
||||
client.handle_event(event);
|
||||
}
|
||||
@@ -136,34 +140,47 @@ impl X11Client {
|
||||
)
|
||||
.expect("Failed to initialize x11 event source");
|
||||
|
||||
client
|
||||
X11Client(Rc::new(RefCell::new(X11ClientState {
|
||||
event_loop: Some(event_loop),
|
||||
loop_handle: handle,
|
||||
common,
|
||||
last_click: Instant::now(),
|
||||
last_location: Point::new(px(0.0), px(0.0)),
|
||||
current_count: 0,
|
||||
|
||||
xcb_connection,
|
||||
x_root_index,
|
||||
atoms,
|
||||
windows: HashMap::default(),
|
||||
xkb: xkb_state,
|
||||
clipboard,
|
||||
primary,
|
||||
})))
|
||||
}
|
||||
|
||||
fn get_window(&self, win: xproto::Window) -> Option<Rc<X11WindowState>> {
|
||||
let state = self.state.borrow();
|
||||
state.windows.get(&win).map(|wr| Rc::clone(&wr.state))
|
||||
fn get_window(&self, win: xproto::Window) -> Option<X11Window> {
|
||||
let state = self.0.borrow();
|
||||
state
|
||||
.windows
|
||||
.get(&win)
|
||||
.map(|window_reference| window_reference.window.clone())
|
||||
}
|
||||
|
||||
fn handle_event(&self, event: Event) -> Option<()> {
|
||||
match event {
|
||||
Event::ClientMessage(event) => {
|
||||
let [atom, ..] = event.data.as_data32();
|
||||
if atom == self.atoms.WM_DELETE_WINDOW {
|
||||
let mut state = self.0.borrow_mut();
|
||||
|
||||
if atom == state.atoms.WM_DELETE_WINDOW {
|
||||
// window "x" button clicked by user, we gracefully exit
|
||||
let window_ref = self
|
||||
.state
|
||||
.borrow_mut()
|
||||
.windows
|
||||
.remove(&event.window)
|
||||
.unwrap();
|
||||
let window_ref = state.windows.remove(&event.window)?;
|
||||
|
||||
self.platform_inner
|
||||
.loop_handle
|
||||
.remove(window_ref.refresh_event_token);
|
||||
window_ref.state.destroy();
|
||||
state.loop_handle.remove(window_ref.refresh_event_token);
|
||||
window_ref.window.destroy();
|
||||
|
||||
if self.state.borrow().windows.is_empty() {
|
||||
self.platform_inner.loop_signal.stop();
|
||||
if state.windows.is_empty() {
|
||||
state.common.signal.stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -195,15 +212,17 @@ impl X11Client {
|
||||
}
|
||||
Event::KeyPress(event) => {
|
||||
let window = self.get_window(event.event)?;
|
||||
let modifiers = super::modifiers_from_state(event.state);
|
||||
let mut state = self.0.borrow_mut();
|
||||
|
||||
let modifiers = modifiers_from_state(event.state);
|
||||
let keystroke = {
|
||||
let code = event.detail.into();
|
||||
let mut state = self.state.borrow_mut();
|
||||
let keystroke = crate::Keystroke::from_xkb(&state.xkb, modifiers, code);
|
||||
state.xkb.update_key(code, xkbc::KeyDirection::Down);
|
||||
keystroke
|
||||
};
|
||||
|
||||
drop(state);
|
||||
window.handle_input(PlatformInput::KeyDown(crate::KeyDownEvent {
|
||||
keystroke,
|
||||
is_held: false,
|
||||
@@ -211,48 +230,54 @@ impl X11Client {
|
||||
}
|
||||
Event::KeyRelease(event) => {
|
||||
let window = self.get_window(event.event)?;
|
||||
let modifiers = super::modifiers_from_state(event.state);
|
||||
let mut state = self.0.borrow_mut();
|
||||
|
||||
let modifiers = modifiers_from_state(event.state);
|
||||
let keystroke = {
|
||||
let code = event.detail.into();
|
||||
let mut state = self.state.borrow_mut();
|
||||
let keystroke = crate::Keystroke::from_xkb(&state.xkb, modifiers, code);
|
||||
state.xkb.update_key(code, xkbc::KeyDirection::Up);
|
||||
keystroke
|
||||
};
|
||||
|
||||
drop(state);
|
||||
window.handle_input(PlatformInput::KeyUp(crate::KeyUpEvent { keystroke }));
|
||||
}
|
||||
Event::ButtonPress(event) => {
|
||||
let window = self.get_window(event.event)?;
|
||||
let modifiers = super::modifiers_from_state(event.state);
|
||||
let mut state = self.0.borrow_mut();
|
||||
|
||||
let modifiers = modifiers_from_state(event.state);
|
||||
let position =
|
||||
Point::new((event.event_x as f32).into(), (event.event_y as f32).into());
|
||||
if let Some(button) = super::button_of_key(event.detail) {
|
||||
let mut state = self.state.borrow_mut();
|
||||
let click_elapsed = state.click_state.last_click.elapsed();
|
||||
if let Some(button) = button_of_key(event.detail) {
|
||||
let click_elapsed = state.last_click.elapsed();
|
||||
|
||||
if click_elapsed < DOUBLE_CLICK_INTERVAL
|
||||
&& is_within_click_distance(state.click_state.last_location, position)
|
||||
&& is_within_click_distance(state.last_location, position)
|
||||
{
|
||||
state.click_state.current_count += 1;
|
||||
state.current_count += 1;
|
||||
} else {
|
||||
state.click_state.current_count = 1;
|
||||
state.current_count = 1;
|
||||
}
|
||||
|
||||
state.click_state.last_click = Instant::now();
|
||||
state.click_state.last_location = position;
|
||||
state.last_click = Instant::now();
|
||||
state.last_location = position;
|
||||
let current_count = state.current_count;
|
||||
|
||||
drop(state);
|
||||
window.handle_input(PlatformInput::MouseDown(crate::MouseDownEvent {
|
||||
button,
|
||||
position,
|
||||
modifiers,
|
||||
click_count: state.click_state.current_count,
|
||||
click_count: current_count,
|
||||
first_mouse: false,
|
||||
}));
|
||||
} else if event.detail >= 4 && event.detail <= 5 {
|
||||
// https://stackoverflow.com/questions/15510472/scrollwheel-event-in-x11
|
||||
let scroll_direction = if event.detail == 4 { 1.0 } else { -1.0 };
|
||||
let scroll_y = SCROLL_LINES * scroll_direction;
|
||||
|
||||
drop(state);
|
||||
window.handle_input(PlatformInput::ScrollWheel(crate::ScrollWheelEvent {
|
||||
position,
|
||||
delta: ScrollDelta::Lines(Point::new(0.0, scroll_y as f32)),
|
||||
@@ -265,16 +290,18 @@ impl X11Client {
|
||||
}
|
||||
Event::ButtonRelease(event) => {
|
||||
let window = self.get_window(event.event)?;
|
||||
let modifiers = super::modifiers_from_state(event.state);
|
||||
let state = self.0.borrow();
|
||||
let modifiers = modifiers_from_state(event.state);
|
||||
let position =
|
||||
Point::new((event.event_x as f32).into(), (event.event_y as f32).into());
|
||||
let state = self.state.borrow();
|
||||
if let Some(button) = super::button_of_key(event.detail) {
|
||||
if let Some(button) = button_of_key(event.detail) {
|
||||
let click_count = state.current_count;
|
||||
drop(state);
|
||||
window.handle_input(PlatformInput::MouseUp(crate::MouseUpEvent {
|
||||
button,
|
||||
position,
|
||||
modifiers,
|
||||
click_count: state.click_state.current_count,
|
||||
click_count,
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -283,7 +310,7 @@ impl X11Client {
|
||||
let pressed_button = super::button_from_state(event.state);
|
||||
let position =
|
||||
Point::new((event.event_x as f32).into(), (event.event_y as f32).into());
|
||||
let modifiers = super::modifiers_from_state(event.state);
|
||||
let modifiers = modifiers_from_state(event.state);
|
||||
window.handle_input(PlatformInput::MouseMove(crate::MouseMoveEvent {
|
||||
pressed_button,
|
||||
position,
|
||||
@@ -295,7 +322,7 @@ impl X11Client {
|
||||
let pressed_button = super::button_from_state(event.state);
|
||||
let position =
|
||||
Point::new((event.event_x as f32).into(), (event.event_y as f32).into());
|
||||
let modifiers = super::modifiers_from_state(event.state);
|
||||
let modifiers = modifiers_from_state(event.state);
|
||||
window.handle_input(PlatformInput::MouseExited(crate::MouseExitEvent {
|
||||
pressed_button,
|
||||
position,
|
||||
@@ -309,61 +336,71 @@ impl X11Client {
|
||||
}
|
||||
}
|
||||
|
||||
impl Client for X11Client {
|
||||
impl LinuxClient for X11Client {
|
||||
fn with_common<R>(&self, f: impl FnOnce(&mut LinuxCommon) -> R) -> R {
|
||||
f(&mut self.0.borrow_mut().common)
|
||||
}
|
||||
|
||||
fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>> {
|
||||
let setup = self.xcb_connection.setup();
|
||||
let state = self.0.borrow();
|
||||
let setup = state.xcb_connection.setup();
|
||||
setup
|
||||
.roots
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(root_id, _)| {
|
||||
Some(Rc::new(X11Display::new(&self.xcb_connection, root_id)?)
|
||||
Some(Rc::new(X11Display::new(&state.xcb_connection, root_id)?)
|
||||
as Rc<dyn PlatformDisplay>)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn display(&self, id: DisplayId) -> Option<Rc<dyn PlatformDisplay>> {
|
||||
Some(Rc::new(X11Display::new(
|
||||
&self.xcb_connection,
|
||||
id.0 as usize,
|
||||
)?))
|
||||
}
|
||||
|
||||
fn primary_display(&self) -> Option<Rc<dyn PlatformDisplay>> {
|
||||
let state = self.0.borrow();
|
||||
|
||||
Some(Rc::new(
|
||||
X11Display::new(&self.xcb_connection, self.x_root_index)
|
||||
X11Display::new(&state.xcb_connection, state.x_root_index)
|
||||
.expect("There should always be a root index"),
|
||||
))
|
||||
}
|
||||
|
||||
fn display(&self, id: DisplayId) -> Option<Rc<dyn PlatformDisplay>> {
|
||||
let state = self.0.borrow();
|
||||
|
||||
Some(Rc::new(X11Display::new(
|
||||
&state.xcb_connection,
|
||||
id.0 as usize,
|
||||
)?))
|
||||
}
|
||||
|
||||
fn open_window(
|
||||
&self,
|
||||
_handle: AnyWindowHandle,
|
||||
options: WindowParams,
|
||||
params: WindowParams,
|
||||
) -> Box<dyn PlatformWindow> {
|
||||
let x_window = self.xcb_connection.generate_id().unwrap();
|
||||
let mut state = self.0.borrow_mut();
|
||||
let x_window = state.xcb_connection.generate_id().unwrap();
|
||||
|
||||
let window_ptr = Rc::new(X11WindowState::new(
|
||||
options,
|
||||
&self.xcb_connection,
|
||||
self.x_root_index,
|
||||
let window = X11Window::new(
|
||||
params,
|
||||
&state.xcb_connection,
|
||||
state.x_root_index,
|
||||
x_window,
|
||||
&self.atoms,
|
||||
));
|
||||
&state.atoms,
|
||||
);
|
||||
|
||||
let screen_resources = self
|
||||
let screen_resources = state
|
||||
.xcb_connection
|
||||
.randr_get_screen_resources(x_window)
|
||||
.unwrap()
|
||||
.reply()
|
||||
.expect("TODO");
|
||||
.expect("Could not find available screens");
|
||||
|
||||
let mode = screen_resources
|
||||
.crtcs
|
||||
.iter()
|
||||
.find_map(|crtc| {
|
||||
let crtc_info = self
|
||||
let crtc_info = state
|
||||
.xcb_connection
|
||||
.randr_get_crtc_info(*crtc, x11rb::CURRENT_TIME)
|
||||
.ok()?
|
||||
@@ -377,16 +414,14 @@ impl Client for X11Client {
|
||||
})
|
||||
.expect("Unable to find screen refresh rate");
|
||||
|
||||
// .expect("Missing screen mode for crtc specified mode id");
|
||||
|
||||
let refresh_event_token = self
|
||||
.platform_inner
|
||||
let refresh_event_token = state
|
||||
.loop_handle
|
||||
.insert_source(calloop::timer::Timer::immediate(), {
|
||||
let refresh_duration = mode_refresh_rate(mode);
|
||||
let xcb_connection = Rc::clone(&self.xcb_connection);
|
||||
move |mut instant, (), _| {
|
||||
xcb_connection
|
||||
move |mut instant, (), client| {
|
||||
let state = client.0.borrow_mut();
|
||||
state
|
||||
.xcb_connection
|
||||
.send_event(
|
||||
false,
|
||||
x_window,
|
||||
@@ -403,7 +438,7 @@ impl Client for X11Client {
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let _ = xcb_connection.flush().unwrap();
|
||||
let _ = state.xcb_connection.flush().unwrap();
|
||||
// Take into account that some frames have been skipped
|
||||
let now = time::Instant::now();
|
||||
while instant < now {
|
||||
@@ -415,22 +450,42 @@ impl Client for X11Client {
|
||||
.expect("Failed to initialize refresh timer");
|
||||
|
||||
let window_ref = WindowRef {
|
||||
state: Rc::clone(&window_ptr),
|
||||
window: window.clone(),
|
||||
refresh_event_token,
|
||||
};
|
||||
self.state.borrow_mut().windows.insert(x_window, window_ref);
|
||||
Box::new(X11Window(window_ptr))
|
||||
|
||||
state.windows.insert(x_window, window_ref);
|
||||
Box::new(window)
|
||||
}
|
||||
|
||||
//todo(linux)
|
||||
fn set_cursor_style(&self, _style: CursorStyle) {}
|
||||
|
||||
fn get_clipboard(&self) -> Rc<RefCell<dyn ClipboardProvider>> {
|
||||
self.state.borrow().clipboard.clone()
|
||||
fn write_to_clipboard(&self, item: crate::ClipboardItem) {
|
||||
self.0.borrow_mut().clipboard.set_contents(item.text);
|
||||
}
|
||||
|
||||
fn get_primary(&self) -> Rc<RefCell<dyn ClipboardProvider>> {
|
||||
self.state.borrow().primary.clone()
|
||||
fn read_from_clipboard(&self) -> Option<crate::ClipboardItem> {
|
||||
self.0
|
||||
.borrow_mut()
|
||||
.clipboard
|
||||
.get_contents()
|
||||
.ok()
|
||||
.map(|text| crate::ClipboardItem {
|
||||
text,
|
||||
metadata: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn run(&self) {
|
||||
let mut event_loop = self
|
||||
.0
|
||||
.borrow_mut()
|
||||
.event_loop
|
||||
.take()
|
||||
.expect("App is already running");
|
||||
|
||||
event_loop.run(None, &mut self.clone(), |_| {}).log_err();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,10 +5,12 @@ use crate::{
|
||||
platform::blade::BladeRenderer, size, Bounds, DevicePixels, Modifiers, Pixels, PlatformAtlas,
|
||||
PlatformDisplay, PlatformInput, PlatformInputHandler, PlatformWindow, Point, PromptLevel,
|
||||
Scene, Size, WindowAppearance, WindowBackgroundAppearance, WindowOptions, WindowParams,
|
||||
X11Client, X11ClientState,
|
||||
};
|
||||
use blade_graphics as gpu;
|
||||
use parking_lot::Mutex;
|
||||
use raw_window_handle as rwh;
|
||||
use util::ResultExt;
|
||||
use x11rb::{
|
||||
connection::Connection,
|
||||
protocol::xproto::{self, ConnectionExt as _, CreateWindowAux},
|
||||
@@ -17,8 +19,9 @@ use x11rb::{
|
||||
};
|
||||
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
cell::{Ref, RefCell, RefMut},
|
||||
ffi::c_void,
|
||||
iter::Zip,
|
||||
mem,
|
||||
num::NonZeroU32,
|
||||
ptr::NonNull,
|
||||
@@ -28,19 +31,6 @@ use std::{
|
||||
|
||||
use super::X11Display;
|
||||
|
||||
#[derive(Default)]
|
||||
struct Callbacks {
|
||||
request_frame: Option<Box<dyn FnMut()>>,
|
||||
input: Option<Box<dyn FnMut(PlatformInput) -> crate::DispatchEventResult>>,
|
||||
active_status_change: Option<Box<dyn FnMut(bool)>>,
|
||||
resize: Option<Box<dyn FnMut(Size<Pixels>, f32)>>,
|
||||
fullscreen: Option<Box<dyn FnMut(bool)>>,
|
||||
moved: Option<Box<dyn FnMut()>>,
|
||||
should_close: Option<Box<dyn FnMut() -> bool>>,
|
||||
close: Option<Box<dyn FnOnce()>>,
|
||||
appearance_changed: Option<Box<dyn FnMut()>>,
|
||||
}
|
||||
|
||||
x11rb::atom_manager! {
|
||||
pub XcbAtoms: AtomsCookie {
|
||||
WM_PROTOCOLS,
|
||||
@@ -51,23 +41,6 @@ x11rb::atom_manager! {
|
||||
}
|
||||
}
|
||||
|
||||
struct LinuxWindowInner {
|
||||
bounds: Bounds<i32>,
|
||||
scale_factor: f32,
|
||||
renderer: BladeRenderer,
|
||||
input_handler: Option<PlatformInputHandler>,
|
||||
}
|
||||
|
||||
impl LinuxWindowInner {
|
||||
fn content_size(&self) -> Size<Pixels> {
|
||||
let size = self.renderer.viewport_size();
|
||||
Size {
|
||||
width: size.width.into(),
|
||||
height: size.height.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn query_render_extent(xcb_connection: &XCBConnection, x_window: xproto::Window) -> gpu::Extent {
|
||||
let reply = xcb_connection
|
||||
.get_geometry(x_window)
|
||||
@@ -88,17 +61,37 @@ struct RawWindow {
|
||||
visual_id: u32,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Callbacks {
|
||||
request_frame: Option<Box<dyn FnMut()>>,
|
||||
input: Option<Box<dyn FnMut(PlatformInput) -> crate::DispatchEventResult>>,
|
||||
active_status_change: Option<Box<dyn FnMut(bool)>>,
|
||||
resize: Option<Box<dyn FnMut(Size<Pixels>, f32)>>,
|
||||
fullscreen: Option<Box<dyn FnMut(bool)>>,
|
||||
moved: Option<Box<dyn FnMut()>>,
|
||||
should_close: Option<Box<dyn FnMut() -> bool>>,
|
||||
close: Option<Box<dyn FnOnce()>>,
|
||||
appearance_changed: Option<Box<dyn FnMut()>>,
|
||||
}
|
||||
|
||||
pub(crate) struct X11WindowState {
|
||||
xcb_connection: Rc<XCBConnection>,
|
||||
display: Rc<dyn PlatformDisplay>,
|
||||
raw: RawWindow,
|
||||
x_window: xproto::Window,
|
||||
callbacks: RefCell<Callbacks>,
|
||||
inner: RefCell<LinuxWindowInner>,
|
||||
|
||||
bounds: Bounds<i32>,
|
||||
scale_factor: f32,
|
||||
renderer: BladeRenderer,
|
||||
display: Rc<dyn PlatformDisplay>,
|
||||
|
||||
input_handler: Option<PlatformInputHandler>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct X11Window(pub(crate) Rc<X11WindowState>);
|
||||
pub(crate) struct X11Window {
|
||||
pub(crate) state: Rc<RefCell<X11WindowState>>,
|
||||
pub(crate) callbacks: Rc<RefCell<Callbacks>>,
|
||||
xcb_connection: Rc<XCBConnection>,
|
||||
x_window: xproto::Window,
|
||||
}
|
||||
|
||||
// todo(linux): Remove other RawWindowHandle implementation
|
||||
unsafe impl blade_rwh::HasRawWindowHandle for RawWindow {
|
||||
@@ -121,7 +114,7 @@ unsafe impl blade_rwh::HasRawDisplayHandle for RawWindow {
|
||||
impl rwh::HasWindowHandle for X11Window {
|
||||
fn window_handle(&self) -> Result<rwh::WindowHandle, rwh::HandleError> {
|
||||
Ok(unsafe {
|
||||
let non_zero = NonZeroU32::new(self.0.raw.window_id).unwrap();
|
||||
let non_zero = NonZeroU32::new(self.state.borrow().raw.window_id).unwrap();
|
||||
let handle = rwh::XcbWindowHandle::new(non_zero);
|
||||
rwh::WindowHandle::borrow_raw(handle.into())
|
||||
})
|
||||
@@ -130,8 +123,9 @@ impl rwh::HasWindowHandle for X11Window {
|
||||
impl rwh::HasDisplayHandle for X11Window {
|
||||
fn display_handle(&self) -> Result<rwh::DisplayHandle, rwh::HandleError> {
|
||||
Ok(unsafe {
|
||||
let non_zero = NonNull::new(self.0.raw.connection).unwrap();
|
||||
let handle = rwh::XcbDisplayHandle::new(Some(non_zero), self.0.raw.screen_id as i32);
|
||||
let this = self.state.borrow();
|
||||
let non_zero = NonNull::new(this.raw.connection).unwrap();
|
||||
let handle = rwh::XcbDisplayHandle::new(Some(non_zero), this.raw.screen_id as i32);
|
||||
rwh::DisplayHandle::borrow_raw(handle.into())
|
||||
})
|
||||
}
|
||||
@@ -239,22 +233,52 @@ impl X11WindowState {
|
||||
let gpu_extent = query_render_extent(xcb_connection, x_window);
|
||||
|
||||
Self {
|
||||
xcb_connection: xcb_connection.clone(),
|
||||
display: Rc::new(X11Display::new(xcb_connection, x_screen_index).unwrap()),
|
||||
raw,
|
||||
bounds: params.bounds.map(|v| v.0),
|
||||
scale_factor: 1.0,
|
||||
renderer: BladeRenderer::new(gpu, gpu_extent),
|
||||
|
||||
input_handler: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn content_size(&self) -> Size<Pixels> {
|
||||
let size = self.renderer.viewport_size();
|
||||
Size {
|
||||
width: size.width.into(),
|
||||
height: size.height.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl X11Window {
|
||||
pub fn new(
|
||||
params: WindowParams,
|
||||
xcb_connection: &Rc<XCBConnection>,
|
||||
x_main_screen_index: usize,
|
||||
x_window: xproto::Window,
|
||||
atoms: &XcbAtoms,
|
||||
) -> Self {
|
||||
X11Window {
|
||||
state: Rc::new(RefCell::new(X11WindowState::new(
|
||||
params,
|
||||
xcb_connection,
|
||||
x_main_screen_index,
|
||||
x_window,
|
||||
atoms,
|
||||
))),
|
||||
callbacks: Rc::new(RefCell::new(Callbacks::default())),
|
||||
xcb_connection: xcb_connection.clone(),
|
||||
x_window,
|
||||
callbacks: RefCell::new(Callbacks::default()),
|
||||
inner: RefCell::new(LinuxWindowInner {
|
||||
bounds: params.bounds.map(|v| v.0),
|
||||
scale_factor: 1.0,
|
||||
renderer: BladeRenderer::new(gpu, gpu_extent),
|
||||
input_handler: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn destroy(&self) {
|
||||
self.inner.borrow_mut().renderer.destroy();
|
||||
let mut state = self.state.borrow_mut();
|
||||
state.renderer.destroy();
|
||||
drop(state);
|
||||
|
||||
self.xcb_connection.unmap_window(self.x_window).unwrap();
|
||||
self.xcb_connection.destroy_window(self.x_window).unwrap();
|
||||
if let Some(fun) = self.callbacks.borrow_mut().close.take() {
|
||||
@@ -270,21 +294,40 @@ impl X11WindowState {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_input(&self, input: PlatformInput) {
|
||||
if let Some(ref mut fun) = self.callbacks.borrow_mut().input {
|
||||
if !fun(input.clone()).propagate {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if let PlatformInput::KeyDown(event) = input {
|
||||
let mut state = self.state.borrow_mut();
|
||||
if let Some(mut input_handler) = state.input_handler.take() {
|
||||
if let Some(ime_key) = &event.keystroke.ime_key {
|
||||
drop(state);
|
||||
input_handler.replace_text_in_range(None, ime_key);
|
||||
state = self.state.borrow_mut();
|
||||
}
|
||||
state.input_handler = Some(input_handler);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn configure(&self, bounds: Bounds<i32>) {
|
||||
let mut resize_args = None;
|
||||
let do_move;
|
||||
{
|
||||
let mut inner = self.inner.borrow_mut();
|
||||
let old_bounds = mem::replace(&mut inner.bounds, bounds);
|
||||
let mut state = self.state.borrow_mut();
|
||||
let old_bounds = mem::replace(&mut state.bounds, bounds);
|
||||
do_move = old_bounds.origin != bounds.origin;
|
||||
// todo(linux): use normal GPUI types here, refactor out the double
|
||||
// viewport check and extra casts ( )
|
||||
let gpu_size = query_render_extent(&self.xcb_connection, self.x_window);
|
||||
if inner.renderer.viewport_size() != gpu_size {
|
||||
inner
|
||||
if state.renderer.viewport_size() != gpu_size {
|
||||
state
|
||||
.renderer
|
||||
.update_drawable_size(size(gpu_size.width as f64, gpu_size.height as f64));
|
||||
resize_args = Some((inner.content_size(), inner.scale_factor));
|
||||
resize_args = Some((state.content_size(), state.scale_factor));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -301,22 +344,6 @@ impl X11WindowState {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_input(&self, input: PlatformInput) {
|
||||
if let Some(ref mut fun) = self.callbacks.borrow_mut().input {
|
||||
if !fun(input.clone()).propagate {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if let PlatformInput::KeyDown(event) = input {
|
||||
let mut inner = self.inner.borrow_mut();
|
||||
if let Some(ref mut input_handler) = inner.input_handler {
|
||||
if let Some(ime_key) = &event.keystroke.ime_key {
|
||||
input_handler.replace_text_in_range(None, ime_key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_focused(&self, focus: bool) {
|
||||
if let Some(ref mut fun) = self.callbacks.borrow_mut().active_status_change {
|
||||
fun(focus);
|
||||
@@ -326,7 +353,7 @@ impl X11WindowState {
|
||||
|
||||
impl PlatformWindow for X11Window {
|
||||
fn bounds(&self) -> Bounds<DevicePixels> {
|
||||
self.0.inner.borrow_mut().bounds.map(|v| v.into())
|
||||
self.state.borrow_mut().bounds.map(|v| v.into())
|
||||
}
|
||||
|
||||
// todo(linux)
|
||||
@@ -340,11 +367,11 @@ impl PlatformWindow for X11Window {
|
||||
}
|
||||
|
||||
fn content_size(&self) -> Size<Pixels> {
|
||||
self.0.inner.borrow_mut().content_size()
|
||||
self.state.borrow_mut().content_size()
|
||||
}
|
||||
|
||||
fn scale_factor(&self) -> f32 {
|
||||
self.0.inner.borrow_mut().scale_factor
|
||||
self.state.borrow_mut().scale_factor
|
||||
}
|
||||
|
||||
// todo(linux)
|
||||
@@ -353,14 +380,13 @@ impl PlatformWindow for X11Window {
|
||||
}
|
||||
|
||||
fn display(&self) -> Rc<dyn PlatformDisplay> {
|
||||
Rc::clone(&self.0.display)
|
||||
self.state.borrow().display.clone()
|
||||
}
|
||||
|
||||
fn mouse_position(&self) -> Point<Pixels> {
|
||||
let reply = self
|
||||
.0
|
||||
.xcb_connection
|
||||
.query_pointer(self.0.x_window)
|
||||
.query_pointer(self.x_window)
|
||||
.unwrap()
|
||||
.reply()
|
||||
.unwrap();
|
||||
@@ -377,11 +403,11 @@ impl PlatformWindow for X11Window {
|
||||
}
|
||||
|
||||
fn set_input_handler(&mut self, input_handler: PlatformInputHandler) {
|
||||
self.0.inner.borrow_mut().input_handler = Some(input_handler);
|
||||
self.state.borrow_mut().input_handler = Some(input_handler);
|
||||
}
|
||||
|
||||
fn take_input_handler(&mut self) -> Option<PlatformInputHandler> {
|
||||
self.0.inner.borrow_mut().input_handler.take()
|
||||
self.state.borrow_mut().input_handler.take()
|
||||
}
|
||||
|
||||
fn prompt(
|
||||
@@ -396,10 +422,9 @@ impl PlatformWindow for X11Window {
|
||||
|
||||
fn activate(&self) {
|
||||
let win_aux = xproto::ConfigureWindowAux::new().stack_mode(xproto::StackMode::ABOVE);
|
||||
self.0
|
||||
.xcb_connection
|
||||
.configure_window(self.0.x_window, &win_aux)
|
||||
.unwrap();
|
||||
self.xcb_connection
|
||||
.configure_window(self.x_window, &win_aux)
|
||||
.log_err();
|
||||
}
|
||||
|
||||
// todo(linux)
|
||||
@@ -408,11 +433,10 @@ impl PlatformWindow for X11Window {
|
||||
}
|
||||
|
||||
fn set_title(&mut self, title: &str) {
|
||||
self.0
|
||||
.xcb_connection
|
||||
self.xcb_connection
|
||||
.change_property8(
|
||||
xproto::PropMode::REPLACE,
|
||||
self.0.x_window,
|
||||
self.x_window,
|
||||
xproto::AtomEnum::WM_NAME,
|
||||
xproto::AtomEnum::STRING,
|
||||
title.as_bytes(),
|
||||
@@ -458,39 +482,39 @@ impl PlatformWindow for X11Window {
|
||||
}
|
||||
|
||||
fn on_request_frame(&self, callback: Box<dyn FnMut()>) {
|
||||
self.0.callbacks.borrow_mut().request_frame = Some(callback);
|
||||
self.callbacks.borrow_mut().request_frame = Some(callback);
|
||||
}
|
||||
|
||||
fn on_input(&self, callback: Box<dyn FnMut(PlatformInput) -> crate::DispatchEventResult>) {
|
||||
self.0.callbacks.borrow_mut().input = Some(callback);
|
||||
self.callbacks.borrow_mut().input = Some(callback);
|
||||
}
|
||||
|
||||
fn on_active_status_change(&self, callback: Box<dyn FnMut(bool)>) {
|
||||
self.0.callbacks.borrow_mut().active_status_change = Some(callback);
|
||||
self.callbacks.borrow_mut().active_status_change = Some(callback);
|
||||
}
|
||||
|
||||
fn on_resize(&self, callback: Box<dyn FnMut(Size<Pixels>, f32)>) {
|
||||
self.0.callbacks.borrow_mut().resize = Some(callback);
|
||||
self.callbacks.borrow_mut().resize = Some(callback);
|
||||
}
|
||||
|
||||
fn on_fullscreen(&self, callback: Box<dyn FnMut(bool)>) {
|
||||
self.0.callbacks.borrow_mut().fullscreen = Some(callback);
|
||||
self.callbacks.borrow_mut().fullscreen = Some(callback);
|
||||
}
|
||||
|
||||
fn on_moved(&self, callback: Box<dyn FnMut()>) {
|
||||
self.0.callbacks.borrow_mut().moved = Some(callback);
|
||||
self.callbacks.borrow_mut().moved = Some(callback);
|
||||
}
|
||||
|
||||
fn on_should_close(&self, callback: Box<dyn FnMut() -> bool>) {
|
||||
self.0.callbacks.borrow_mut().should_close = Some(callback);
|
||||
self.callbacks.borrow_mut().should_close = Some(callback);
|
||||
}
|
||||
|
||||
fn on_close(&self, callback: Box<dyn FnOnce()>) {
|
||||
self.0.callbacks.borrow_mut().close = Some(callback);
|
||||
self.callbacks.borrow_mut().close = Some(callback);
|
||||
}
|
||||
|
||||
fn on_appearance_changed(&self, callback: Box<dyn FnMut()>) {
|
||||
self.0.callbacks.borrow_mut().appearance_changed = Some(callback);
|
||||
self.callbacks.borrow_mut().appearance_changed = Some(callback);
|
||||
}
|
||||
|
||||
// todo(linux)
|
||||
@@ -499,12 +523,12 @@ impl PlatformWindow for X11Window {
|
||||
}
|
||||
|
||||
fn draw(&self, scene: &Scene) {
|
||||
let mut inner = self.0.inner.borrow_mut();
|
||||
let mut inner = self.state.borrow_mut();
|
||||
inner.renderer.draw(scene);
|
||||
}
|
||||
|
||||
fn sprite_atlas(&self) -> sync::Arc<dyn PlatformAtlas> {
|
||||
let inner = self.0.inner.borrow_mut();
|
||||
let inner = self.state.borrow();
|
||||
inner.renderer.sprite_atlas().clone()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1848,16 +1848,17 @@ extern "C" fn accepts_first_mouse(this: &Object, _: Sel, _: id) -> BOOL {
|
||||
|
||||
extern "C" fn dragging_entered(this: &Object, _: Sel, dragging_info: id) -> NSDragOperation {
|
||||
let window_state = unsafe { get_window_state(this) };
|
||||
if send_new_event(&window_state, {
|
||||
let position = drag_event_position(&window_state, dragging_info);
|
||||
let paths = external_paths_from_event(dragging_info);
|
||||
PlatformInput::FileDrop(FileDropEvent::Entered { position, paths })
|
||||
}) {
|
||||
window_state.lock().external_files_dragged = true;
|
||||
NSDragOperationCopy
|
||||
} else {
|
||||
NSDragOperationNone
|
||||
let position = drag_event_position(&window_state, dragging_info);
|
||||
let paths = external_paths_from_event(dragging_info);
|
||||
if let Some(event) =
|
||||
paths.map(|paths| PlatformInput::FileDrop(FileDropEvent::Entered { position, paths }))
|
||||
{
|
||||
if send_new_event(&window_state, event) {
|
||||
window_state.lock().external_files_dragged = true;
|
||||
return NSDragOperationCopy;
|
||||
}
|
||||
}
|
||||
NSDragOperationNone
|
||||
}
|
||||
|
||||
extern "C" fn dragging_updated(this: &Object, _: Sel, dragging_info: id) -> NSDragOperation {
|
||||
@@ -1895,10 +1896,13 @@ extern "C" fn perform_drag_operation(this: &Object, _: Sel, dragging_info: id) -
|
||||
}
|
||||
}
|
||||
|
||||
fn external_paths_from_event(dragging_info: *mut Object) -> ExternalPaths {
|
||||
fn external_paths_from_event(dragging_info: *mut Object) -> Option<ExternalPaths> {
|
||||
let mut paths = SmallVec::new();
|
||||
let pasteboard: id = unsafe { msg_send![dragging_info, draggingPasteboard] };
|
||||
let filenames = unsafe { NSPasteboard::propertyListForType(pasteboard, NSFilenamesPboardType) };
|
||||
if filenames == nil {
|
||||
return None;
|
||||
}
|
||||
for file in unsafe { filenames.iter() } {
|
||||
let path = unsafe {
|
||||
let f = NSString::UTF8String(file);
|
||||
@@ -1906,7 +1910,7 @@ fn external_paths_from_event(dragging_info: *mut Object) -> ExternalPaths {
|
||||
};
|
||||
paths.push(PathBuf::from(path))
|
||||
}
|
||||
ExternalPaths(paths)
|
||||
Some(ExternalPaths(paths))
|
||||
}
|
||||
|
||||
extern "C" fn conclude_drag_operation(this: &Object, _: Sel, _: id) {
|
||||
|
||||
@@ -117,6 +117,17 @@ impl TextSystem {
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the Font for the Font Id.
|
||||
pub fn get_font_for_id(&self, id: FontId) -> Option<Font> {
|
||||
let lock = self.font_ids_by_font.read();
|
||||
lock.iter()
|
||||
.filter_map(|(font, result)| match result {
|
||||
Ok(font_id) if *font_id == id => Some(font.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.next()
|
||||
}
|
||||
|
||||
/// Resolves the specified font, falling back to the default font stack if
|
||||
/// the font fails to load.
|
||||
///
|
||||
|
||||
@@ -103,6 +103,18 @@ impl LineLayout {
|
||||
self.width
|
||||
}
|
||||
|
||||
/// The corresponding Font at the given index
|
||||
pub fn font_id_for_index(&self, index: usize) -> Option<FontId> {
|
||||
for run in &self.runs {
|
||||
for glyph in &run.glyphs {
|
||||
if glyph.index >= index {
|
||||
return Some(run.font_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn compute_wrap_boundaries(
|
||||
&self,
|
||||
text: &str,
|
||||
|
||||
@@ -476,6 +476,12 @@ impl Window {
|
||||
} else if needs_present {
|
||||
handle.update(&mut cx, |_, cx| cx.present()).log_err();
|
||||
}
|
||||
|
||||
handle
|
||||
.update(&mut cx, |_, cx| {
|
||||
cx.complete_frame();
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
}));
|
||||
platform_window.on_resize(Box::new({
|
||||
@@ -1004,6 +1010,10 @@ impl<'a> WindowContext<'a> {
|
||||
self.window.modifiers
|
||||
}
|
||||
|
||||
fn complete_frame(&self) {
|
||||
self.window.platform_window.completed_frame();
|
||||
}
|
||||
|
||||
/// Produces a new frame and assigns it to `rendered_frame`. To actually show
|
||||
/// the contents of the new [Scene], use [present].
|
||||
#[profiling::function]
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
use gpui::{
|
||||
canvas, div, fill, img, opaque_grey, point, size, AnyElement, AppContext, Bounds, Context,
|
||||
EventEmitter, FocusHandle, FocusableView, Img, InteractiveElement, IntoElement, Model,
|
||||
ParentElement, Render, Styled, Task, View, ViewContext, VisualContext, WeakView, WindowContext,
|
||||
ObjectFit, ParentElement, Render, Styled, Task, View, ViewContext, VisualContext, WeakView,
|
||||
WindowContext,
|
||||
};
|
||||
use persistence::IMAGE_VIEWER;
|
||||
use ui::{h_flex, prelude::*};
|
||||
use ui::prelude::*;
|
||||
|
||||
use project::{Project, ProjectEntryId, ProjectPath};
|
||||
use std::{ffi::OsStr, path::PathBuf};
|
||||
@@ -155,64 +156,67 @@ impl FocusableView for ImageView {
|
||||
|
||||
impl Render for ImageView {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let checkered_background = |bounds: Bounds<Pixels>, _, cx: &mut ElementContext| {
|
||||
let square_size = 32.0;
|
||||
|
||||
let start_y = bounds.origin.y.0;
|
||||
let height = bounds.size.height.0;
|
||||
let start_x = bounds.origin.x.0;
|
||||
let width = bounds.size.width.0;
|
||||
|
||||
let mut y = start_y;
|
||||
let mut x = start_x;
|
||||
let mut color_swapper = true;
|
||||
// draw checkerboard pattern
|
||||
while y <= start_y + height {
|
||||
// Keeping track of the grid in order to be resilient to resizing
|
||||
let start_swap = color_swapper;
|
||||
while x <= start_x + width {
|
||||
let rect =
|
||||
Bounds::new(point(px(x), px(y)), size(px(square_size), px(square_size)));
|
||||
|
||||
let color = if color_swapper {
|
||||
opaque_grey(0.6, 0.4)
|
||||
} else {
|
||||
opaque_grey(0.7, 0.4)
|
||||
};
|
||||
|
||||
cx.paint_quad(fill(rect, color));
|
||||
color_swapper = !color_swapper;
|
||||
x += square_size;
|
||||
}
|
||||
x = start_x;
|
||||
color_swapper = !start_swap;
|
||||
y += square_size;
|
||||
}
|
||||
};
|
||||
|
||||
let checkered_background = canvas(|_, _| (), checkered_background)
|
||||
.border_2()
|
||||
.border_color(cx.theme().styles.colors.border)
|
||||
.size_full()
|
||||
.absolute()
|
||||
.top_0()
|
||||
.left_0();
|
||||
|
||||
div()
|
||||
.track_focus(&self.focus_handle)
|
||||
.size_full()
|
||||
.child(checkered_background)
|
||||
.child(
|
||||
// Checkered background behind the image
|
||||
canvas(
|
||||
|_, _| (),
|
||||
|bounds, _, cx| {
|
||||
let square_size = 32.0;
|
||||
|
||||
let start_y = bounds.origin.y.0;
|
||||
let height = bounds.size.height.0;
|
||||
let start_x = bounds.origin.x.0;
|
||||
let width = bounds.size.width.0;
|
||||
|
||||
let mut y = start_y;
|
||||
let mut x = start_x;
|
||||
let mut color_swapper = true;
|
||||
// draw checkerboard pattern
|
||||
while y <= start_y + height {
|
||||
// Keeping track of the grid in order to be resilient to resizing
|
||||
let start_swap = color_swapper;
|
||||
while x <= start_x + width {
|
||||
let rect = Bounds::new(
|
||||
point(px(x), px(y)),
|
||||
size(px(square_size), px(square_size)),
|
||||
);
|
||||
|
||||
let color = if color_swapper {
|
||||
opaque_grey(0.6, 0.4)
|
||||
} else {
|
||||
opaque_grey(0.7, 0.4)
|
||||
};
|
||||
|
||||
cx.paint_quad(fill(rect, color));
|
||||
color_swapper = !color_swapper;
|
||||
x += square_size;
|
||||
}
|
||||
x = start_x;
|
||||
color_swapper = !start_swap;
|
||||
y += square_size;
|
||||
}
|
||||
},
|
||||
)
|
||||
.border_2()
|
||||
.border_color(cx.theme().styles.colors.border)
|
||||
.size_full()
|
||||
.absolute()
|
||||
.top_0()
|
||||
.left_0(),
|
||||
)
|
||||
.child(
|
||||
v_flex().h_full().justify_around().child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.justify_around()
|
||||
.child(img(self.path.clone())),
|
||||
),
|
||||
div()
|
||||
.flex()
|
||||
.justify_center()
|
||||
.items_center()
|
||||
.w_full()
|
||||
// TODO: In browser based Tailwind & Flex this would be h-screen and we'd use w-full
|
||||
.h_full()
|
||||
.child(
|
||||
img(self.path.clone())
|
||||
.object_fit(ObjectFit::ScaleDown)
|
||||
.max_w_full()
|
||||
.max_h_full(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ use editor::Editor;
|
||||
use gpui::{actions, AppContext, ViewContext, WindowContext};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::Settings;
|
||||
use settings::{Settings, SettingsSources};
|
||||
use std::{
|
||||
fs::OpenOptions,
|
||||
path::{Path, PathBuf},
|
||||
@@ -50,12 +50,8 @@ impl settings::Settings for JournalSettings {
|
||||
|
||||
type FileContent = Self;
|
||||
|
||||
fn load(
|
||||
defaults: &Self::FileContent,
|
||||
user_values: &[&Self::FileContent],
|
||||
_: &mut AppContext,
|
||||
) -> Result<Self> {
|
||||
Self::load_via_json_merge(defaults, user_values)
|
||||
fn load(sources: SettingsSources<Self::FileContent>, _: &mut AppContext) -> Result<Self> {
|
||||
sources.json_merge()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ use crate::{
|
||||
SyntaxLayer, SyntaxMap, SyntaxMapCapture, SyntaxMapCaptures, SyntaxMapMatches,
|
||||
SyntaxSnapshot, ToTreeSitterPoint,
|
||||
},
|
||||
CodeLabel, LanguageScope, Outline,
|
||||
LanguageScope, Outline,
|
||||
};
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
pub use clock::ReplicaId;
|
||||
@@ -250,34 +250,6 @@ pub enum Documentation {
|
||||
MultiLineMarkdown(ParsedMarkdown),
|
||||
}
|
||||
|
||||
/// A completion provided by a language server
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Completion {
|
||||
/// The range of the buffer that will be replaced.
|
||||
pub old_range: Range<Anchor>,
|
||||
/// The new text that will be inserted.
|
||||
pub new_text: String,
|
||||
/// A label for this completion that is shown in the menu.
|
||||
pub label: CodeLabel,
|
||||
/// The id of the language server that produced this completion.
|
||||
pub server_id: LanguageServerId,
|
||||
/// The documentation for this completion.
|
||||
pub documentation: Option<Documentation>,
|
||||
/// The raw completion provided by the language server.
|
||||
pub lsp_completion: lsp::CompletionItem,
|
||||
}
|
||||
|
||||
/// A code action provided by a language server.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct CodeAction {
|
||||
/// The id of the language server that produced this code action.
|
||||
pub server_id: LanguageServerId,
|
||||
/// The range of the buffer where this code action is applicable.
|
||||
pub range: Range<Anchor>,
|
||||
/// The raw code action provided by the language server.
|
||||
pub lsp_action: lsp::CodeAction,
|
||||
}
|
||||
|
||||
/// An operation used to synchronize this buffer with its other replicas.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum Operation {
|
||||
@@ -2526,6 +2498,11 @@ impl BufferSnapshot {
|
||||
.last()
|
||||
}
|
||||
|
||||
/// Returns the main [Language]
|
||||
pub fn language(&self) -> Option<&Arc<Language>> {
|
||||
self.language.as_ref()
|
||||
}
|
||||
|
||||
/// Returns the [Language] at the given location.
|
||||
pub fn language_at<D: ToOffset>(&self, position: D) -> Option<&Arc<Language>> {
|
||||
self.syntax_layer_at(position)
|
||||
@@ -2777,10 +2754,13 @@ impl BufferSnapshot {
|
||||
range.start + self.line_len(start.row as u32) as usize - start.column;
|
||||
}
|
||||
|
||||
buffer_ranges.push((range, node_is_name));
|
||||
if !range.is_empty() {
|
||||
buffer_ranges.push((range, node_is_name));
|
||||
}
|
||||
}
|
||||
|
||||
if buffer_ranges.is_empty() {
|
||||
matches.advance();
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -3508,24 +3488,6 @@ impl IndentSize {
|
||||
}
|
||||
}
|
||||
|
||||
impl Completion {
|
||||
/// A key that can be used to sort completions when displaying
|
||||
/// them to the user.
|
||||
pub fn sort_key(&self) -> (usize, &str) {
|
||||
let kind_key = match self.lsp_completion.kind {
|
||||
Some(lsp::CompletionItemKind::KEYWORD) => 0,
|
||||
Some(lsp::CompletionItemKind::VARIABLE) => 1,
|
||||
_ => 2,
|
||||
};
|
||||
(kind_key, &self.label.text[self.label.filter_range.clone()])
|
||||
}
|
||||
|
||||
/// Whether this completion is a snippet.
|
||||
pub fn is_snippet(&self) -> bool {
|
||||
self.lsp_completion.insert_text_format == Some(lsp::InsertTextFormat::SNIPPET)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub struct TestFile {
|
||||
pub path: Arc<Path>,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::Diagnostic;
|
||||
use crate::{range_to_lsp, Diagnostic};
|
||||
use collections::HashMap;
|
||||
use lsp::LanguageServerId;
|
||||
use std::{
|
||||
@@ -51,7 +51,7 @@ pub struct Summary {
|
||||
count: usize,
|
||||
}
|
||||
|
||||
impl<T> DiagnosticEntry<T> {
|
||||
impl DiagnosticEntry<PointUtf16> {
|
||||
/// Returns a raw LSP diagnostic ssed to provide diagnostic context to LSP
|
||||
/// codeAction request
|
||||
pub fn to_lsp_diagnostic_stub(&self) -> lsp::Diagnostic {
|
||||
@@ -61,9 +61,14 @@ impl<T> DiagnosticEntry<T> {
|
||||
.clone()
|
||||
.map(lsp::NumberOrString::String);
|
||||
|
||||
let range = range_to_lsp(self.range.clone());
|
||||
|
||||
lsp::Diagnostic {
|
||||
code,
|
||||
range,
|
||||
severity: Some(self.diagnostic.severity),
|
||||
source: self.diagnostic.source.clone(),
|
||||
message: self.diagnostic.message.clone(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ use schemars::{
|
||||
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
|
||||
use serde_json::Value;
|
||||
use smol::future::FutureExt as _;
|
||||
use std::num::NonZeroU32;
|
||||
use std::{
|
||||
any::Any,
|
||||
cell::RefCell,
|
||||
@@ -55,9 +56,7 @@ use std::{
|
||||
},
|
||||
};
|
||||
use syntax_map::SyntaxSnapshot;
|
||||
pub use task_context::{
|
||||
ContextProvider, ContextProviderWithTasks, LanguageSource, SymbolContextProvider,
|
||||
};
|
||||
pub use task_context::{ContextProvider, ContextProviderWithTasks, SymbolContextProvider};
|
||||
use theme::SyntaxTheme;
|
||||
use tree_sitter::{self, wasmtime, Query, WasmStore};
|
||||
use util::http::HttpClient;
|
||||
@@ -75,6 +74,8 @@ pub use syntax_map::{OwnedSyntaxLayer, SyntaxLayer};
|
||||
pub use text::LineEnding;
|
||||
pub use tree_sitter::{Parser, Tree};
|
||||
|
||||
use crate::language_settings::SoftWrap;
|
||||
|
||||
/// Initializes the `language` crate.
|
||||
///
|
||||
/// This should be called before making use of items from the create.
|
||||
@@ -101,6 +102,7 @@ lazy_static! {
|
||||
pub static ref PLAIN_TEXT: Arc<Language> = Arc::new(Language::new(
|
||||
LanguageConfig {
|
||||
name: "Plain Text".into(),
|
||||
soft_wrap: Some(SoftWrap::PreferredLineLength),
|
||||
..Default::default()
|
||||
},
|
||||
None,
|
||||
@@ -197,35 +199,34 @@ impl CachedLspAdapter {
|
||||
self.adapter.code_action_kinds()
|
||||
}
|
||||
|
||||
pub fn workspace_configuration(&self, workspace_root: &Path, cx: &mut AppContext) -> Value {
|
||||
self.adapter.workspace_configuration(workspace_root, cx)
|
||||
}
|
||||
|
||||
pub fn process_diagnostics(&self, params: &mut lsp::PublishDiagnosticsParams) {
|
||||
self.adapter.process_diagnostics(params)
|
||||
}
|
||||
|
||||
pub async fn process_completion(&self, completion_item: &mut lsp::CompletionItem) {
|
||||
self.adapter.process_completion(completion_item).await
|
||||
pub async fn process_completions(&self, completion_items: &mut [lsp::CompletionItem]) {
|
||||
self.adapter.process_completions(completion_items).await
|
||||
}
|
||||
|
||||
pub async fn label_for_completion(
|
||||
pub async fn labels_for_completions(
|
||||
&self,
|
||||
completion_item: &lsp::CompletionItem,
|
||||
completion_items: &[lsp::CompletionItem],
|
||||
language: &Arc<Language>,
|
||||
) -> Option<CodeLabel> {
|
||||
) -> Result<Vec<Option<CodeLabel>>> {
|
||||
self.adapter
|
||||
.label_for_completion(completion_item, language)
|
||||
.clone()
|
||||
.labels_for_completions(completion_items, language)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn label_for_symbol(
|
||||
pub async fn labels_for_symbols(
|
||||
&self,
|
||||
name: &str,
|
||||
kind: lsp::SymbolKind,
|
||||
symbols: &[(String, lsp::SymbolKind)],
|
||||
language: &Arc<Language>,
|
||||
) -> Option<CodeLabel> {
|
||||
self.adapter.label_for_symbol(name, kind, language).await
|
||||
) -> Result<Vec<Option<CodeLabel>>> {
|
||||
self.adapter
|
||||
.clone()
|
||||
.labels_for_symbols(symbols, language)
|
||||
.await
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
@@ -240,6 +241,8 @@ impl CachedLspAdapter {
|
||||
pub trait LspAdapterDelegate: Send + Sync {
|
||||
fn show_notification(&self, message: &str, cx: &mut AppContext);
|
||||
fn http_client(&self) -> Arc<dyn HttpClient>;
|
||||
fn worktree_id(&self) -> u64;
|
||||
fn worktree_root_path(&self) -> &Path;
|
||||
fn update_status(&self, language: LanguageServerName, status: LanguageServerBinaryStatus);
|
||||
|
||||
async fn which(&self, command: &OsStr) -> Option<PathBuf>;
|
||||
@@ -382,10 +385,24 @@ pub trait LspAdapter: 'static + Send + Sync {
|
||||
|
||||
fn process_diagnostics(&self, _: &mut lsp::PublishDiagnosticsParams) {}
|
||||
|
||||
/// A callback called for each [`lsp::CompletionItem`] obtained from LSP server.
|
||||
/// Some LspAdapter implementations might want to modify the obtained item to
|
||||
/// change how it's displayed.
|
||||
async fn process_completion(&self, _: &mut lsp::CompletionItem) {}
|
||||
/// Post-processes completions provided by the language server.
|
||||
async fn process_completions(&self, _: &mut [lsp::CompletionItem]) {}
|
||||
|
||||
async fn labels_for_completions(
|
||||
self: Arc<Self>,
|
||||
completions: &[lsp::CompletionItem],
|
||||
language: &Arc<Language>,
|
||||
) -> Result<Vec<Option<CodeLabel>>> {
|
||||
let mut labels = Vec::new();
|
||||
for (ix, completion) in completions.into_iter().enumerate() {
|
||||
let label = self.label_for_completion(completion, language).await;
|
||||
if let Some(label) = label {
|
||||
labels.resize(ix + 1, None);
|
||||
*labels.last_mut().unwrap() = Some(label);
|
||||
}
|
||||
}
|
||||
Ok(labels)
|
||||
}
|
||||
|
||||
async fn label_for_completion(
|
||||
&self,
|
||||
@@ -395,6 +412,22 @@ pub trait LspAdapter: 'static + Send + Sync {
|
||||
None
|
||||
}
|
||||
|
||||
async fn labels_for_symbols(
|
||||
self: Arc<Self>,
|
||||
symbols: &[(String, lsp::SymbolKind)],
|
||||
language: &Arc<Language>,
|
||||
) -> Result<Vec<Option<CodeLabel>>> {
|
||||
let mut labels = Vec::new();
|
||||
for (ix, (name, kind)) in symbols.into_iter().enumerate() {
|
||||
let label = self.label_for_symbol(name, *kind, language).await;
|
||||
if let Some(label) = label {
|
||||
labels.resize(ix + 1, None);
|
||||
*labels.last_mut().unwrap() = Some(label);
|
||||
}
|
||||
}
|
||||
Ok(labels)
|
||||
}
|
||||
|
||||
async fn label_for_symbol(
|
||||
&self,
|
||||
_: &str,
|
||||
@@ -412,8 +445,12 @@ pub trait LspAdapter: 'static + Send + Sync {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn workspace_configuration(&self, _workspace_root: &Path, _cx: &mut AppContext) -> Value {
|
||||
serde_json::json!({})
|
||||
async fn workspace_configuration(
|
||||
self: Arc<Self>,
|
||||
_: &Arc<dyn LspAdapterDelegate>,
|
||||
_cx: &mut AsyncAppContext,
|
||||
) -> Result<Value> {
|
||||
Ok(serde_json::json!({}))
|
||||
}
|
||||
|
||||
/// Returns a list of code actions supported by a given LspAdapter
|
||||
@@ -543,6 +580,17 @@ pub struct LanguageConfig {
|
||||
/// The names of any Prettier plugins that should be used for this language.
|
||||
#[serde(default)]
|
||||
pub prettier_plugins: Vec<Arc<str>>,
|
||||
|
||||
/// Whether to indent lines using tab characters, as opposed to multiple
|
||||
/// spaces.
|
||||
#[serde(default)]
|
||||
pub hard_tabs: Option<bool>,
|
||||
/// How many columns a tab should occupy.
|
||||
#[serde(default)]
|
||||
pub tab_size: Option<NonZeroU32>,
|
||||
/// How to soft-wrap long lines of text.
|
||||
#[serde(default)]
|
||||
pub soft_wrap: Option<SoftWrap>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, Default, JsonSchema)]
|
||||
@@ -627,6 +675,9 @@ impl Default for LanguageConfig {
|
||||
prettier_parser_name: None,
|
||||
prettier_plugins: Default::default(),
|
||||
collapsed_placeholder: Default::default(),
|
||||
hard_tabs: Default::default(),
|
||||
tab_size: Default::default(),
|
||||
soft_wrap: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::language_settings::{AllLanguageSettingsContent, LanguageSettingsContent};
|
||||
use crate::{
|
||||
language_settings::all_language_settings, task_context::ContextProvider, CachedLspAdapter,
|
||||
File, Language, LanguageConfig, LanguageId, LanguageMatcher, LanguageServerName, LspAdapter,
|
||||
@@ -38,6 +39,7 @@ pub struct LanguageRegistry {
|
||||
struct LanguageRegistryState {
|
||||
next_language_server_id: usize,
|
||||
languages: Vec<Arc<Language>>,
|
||||
language_settings: AllLanguageSettingsContent,
|
||||
available_languages: Vec<AvailableLanguage>,
|
||||
grammars: HashMap<Arc<str>, AvailableGrammar>,
|
||||
lsp_adapters: HashMap<Arc<str>, Vec<Arc<CachedLspAdapter>>>,
|
||||
@@ -145,6 +147,7 @@ impl LanguageRegistry {
|
||||
languages: Vec::new(),
|
||||
available_languages: Vec::new(),
|
||||
grammars: Default::default(),
|
||||
language_settings: Default::default(),
|
||||
loading_languages: Default::default(),
|
||||
lsp_adapters: Default::default(),
|
||||
subscription: watch::channel(),
|
||||
@@ -338,6 +341,10 @@ impl LanguageRegistry {
|
||||
*state.subscription.0.borrow_mut() = ();
|
||||
}
|
||||
|
||||
pub fn language_settings(&self) -> AllLanguageSettingsContent {
|
||||
self.state.read().language_settings.clone()
|
||||
}
|
||||
|
||||
pub fn language_names(&self) -> Vec<String> {
|
||||
let state = self.state.read();
|
||||
let mut result = state
|
||||
@@ -746,7 +753,10 @@ impl LanguageRegistry {
|
||||
let capabilities = adapter
|
||||
.as_fake()
|
||||
.map(|fake_adapter| fake_adapter.capabilities.clone())
|
||||
.unwrap_or_default();
|
||||
.unwrap_or_else(|| lsp::ServerCapabilities {
|
||||
completion_provider: Some(Default::default()),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let (server, mut fake_server) = lsp::FakeLanguageServer::new(
|
||||
server_id,
|
||||
@@ -851,6 +861,16 @@ impl LanguageRegistryState {
|
||||
if let Some(theme) = self.theme.as_ref() {
|
||||
language.set_theme(theme.syntax());
|
||||
}
|
||||
self.language_settings.languages.insert(
|
||||
language.name(),
|
||||
LanguageSettingsContent {
|
||||
tab_size: language.config.tab_size,
|
||||
hard_tabs: language.config.hard_tabs,
|
||||
soft_wrap: language.config.soft_wrap,
|
||||
..Default::default()
|
||||
}
|
||||
.clone(),
|
||||
);
|
||||
self.languages.push(language);
|
||||
self.version += 1;
|
||||
*self.subscription.0.borrow_mut() = ();
|
||||
|
||||
@@ -10,7 +10,7 @@ use schemars::{
|
||||
JsonSchema,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsLocation};
|
||||
use settings::{Settings, SettingsLocation, SettingsSources};
|
||||
use std::{num::NonZeroU32, path::Path, sync::Arc};
|
||||
|
||||
impl<'a> Into<SettingsLocation<'a>> for &'a dyn File {
|
||||
@@ -87,7 +87,7 @@ pub struct LanguageSettings {
|
||||
/// How to perform a buffer format.
|
||||
pub formatter: Formatter,
|
||||
/// Zed's Prettier integration settings.
|
||||
/// If Prettier is enabled, Zed will use this its Prettier instance for any applicable file, if
|
||||
/// If Prettier is enabled, Zed will use this for its Prettier instance for any applicable file, if
|
||||
/// the project has no other Prettier installed.
|
||||
pub prettier: HashMap<String, serde_json::Value>,
|
||||
/// Whether to use language servers to provide code intelligence.
|
||||
@@ -119,7 +119,7 @@ pub struct CopilotSettings {
|
||||
}
|
||||
|
||||
/// The settings for all languages.
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
|
||||
#[derive(Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct AllLanguageSettingsContent {
|
||||
/// The settings for enabling/disabling features.
|
||||
#[serde(default)]
|
||||
@@ -140,7 +140,7 @@ pub struct AllLanguageSettingsContent {
|
||||
}
|
||||
|
||||
/// The settings for a particular language.
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
|
||||
#[derive(Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct LanguageSettingsContent {
|
||||
/// How many columns a tab should occupy.
|
||||
///
|
||||
@@ -200,7 +200,7 @@ pub struct LanguageSettingsContent {
|
||||
#[serde(default)]
|
||||
pub formatter: Option<Formatter>,
|
||||
/// Zed's Prettier integration settings.
|
||||
/// If Prettier is enabled, Zed will use this its Prettier instance for any applicable file, if
|
||||
/// If Prettier is enabled, Zed will use this for its Prettier instance for any applicable file, if
|
||||
/// the project has no other Prettier installed.
|
||||
///
|
||||
/// Default: {}
|
||||
@@ -249,7 +249,7 @@ pub struct LanguageSettingsContent {
|
||||
}
|
||||
|
||||
/// The contents of the GitHub Copilot settings.
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
|
||||
#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct CopilotSettingsContent {
|
||||
/// A list of globs representing files that Copilot should be disabled for.
|
||||
#[serde(default)]
|
||||
@@ -257,7 +257,7 @@ pub struct CopilotSettingsContent {
|
||||
}
|
||||
|
||||
/// The settings for enabling/disabling features.
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
|
||||
#[derive(Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub struct FeaturesContent {
|
||||
/// Whether the GitHub Copilot feature is enabled.
|
||||
@@ -473,11 +473,9 @@ impl settings::Settings for AllLanguageSettings {
|
||||
|
||||
type FileContent = AllLanguageSettingsContent;
|
||||
|
||||
fn load(
|
||||
default_value: &Self::FileContent,
|
||||
user_settings: &[&Self::FileContent],
|
||||
_: &mut AppContext,
|
||||
) -> Result<Self> {
|
||||
fn load(sources: SettingsSources<Self::FileContent>, _: &mut AppContext) -> Result<Self> {
|
||||
let default_value = sources.default;
|
||||
|
||||
// A default is provided for all settings.
|
||||
let mut defaults: LanguageSettings =
|
||||
serde_json::from_value(serde_json::to_value(&default_value.defaults)?)?;
|
||||
@@ -500,7 +498,8 @@ impl settings::Settings for AllLanguageSettings {
|
||||
.and_then(|c| c.disabled_globs.as_ref())
|
||||
.ok_or_else(Self::missing_default)?;
|
||||
|
||||
for user_settings in user_settings {
|
||||
let mut file_types: HashMap<Arc<str>, Vec<String>> = HashMap::default();
|
||||
for user_settings in sources.customizations() {
|
||||
if let Some(copilot) = user_settings.features.as_ref().and_then(|f| f.copilot) {
|
||||
copilot_enabled = copilot;
|
||||
}
|
||||
@@ -528,11 +527,8 @@ impl settings::Settings for AllLanguageSettings {
|
||||
user_language_settings,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let mut file_types: HashMap<Arc<str>, Vec<String>> = HashMap::default();
|
||||
for user_file_types in user_settings.iter().map(|s| &s.file_types) {
|
||||
for (language, suffixes) in user_file_types {
|
||||
for (language, suffixes) in &user_settings.file_types {
|
||||
file_types
|
||||
.entry(language.clone())
|
||||
.or_default()
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
//! Handles conversions of `language` items to and from the [`rpc`] protocol.
|
||||
|
||||
use crate::{
|
||||
diagnostic_set::DiagnosticEntry, CodeAction, CodeLabel, Completion, CursorShape, Diagnostic,
|
||||
Language, LanguageRegistry,
|
||||
};
|
||||
use crate::{diagnostic_set::DiagnosticEntry, CursorShape, Diagnostic};
|
||||
use anyhow::{anyhow, Result};
|
||||
use clock::ReplicaId;
|
||||
use lsp::{DiagnosticSeverity, LanguageServerId};
|
||||
@@ -466,85 +463,6 @@ pub fn lamport_timestamp_for_operation(operation: &proto::Operation) -> Option<c
|
||||
})
|
||||
}
|
||||
|
||||
/// Serializes a [`Completion`] to be sent over RPC.
|
||||
pub fn serialize_completion(completion: &Completion) -> proto::Completion {
|
||||
proto::Completion {
|
||||
old_start: Some(serialize_anchor(&completion.old_range.start)),
|
||||
old_end: Some(serialize_anchor(&completion.old_range.end)),
|
||||
new_text: completion.new_text.clone(),
|
||||
server_id: completion.server_id.0 as u64,
|
||||
lsp_completion: serde_json::to_vec(&completion.lsp_completion).unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Deserializes a [`Completion`] from the RPC representation.
|
||||
pub async fn deserialize_completion(
|
||||
completion: proto::Completion,
|
||||
language: Option<Arc<Language>>,
|
||||
language_registry: &Arc<LanguageRegistry>,
|
||||
) -> Result<Completion> {
|
||||
let old_start = completion
|
||||
.old_start
|
||||
.and_then(deserialize_anchor)
|
||||
.ok_or_else(|| anyhow!("invalid old start"))?;
|
||||
let old_end = completion
|
||||
.old_end
|
||||
.and_then(deserialize_anchor)
|
||||
.ok_or_else(|| anyhow!("invalid old end"))?;
|
||||
let lsp_completion = serde_json::from_slice(&completion.lsp_completion)?;
|
||||
|
||||
let mut label = None;
|
||||
if let Some(language) = language {
|
||||
if let Some(adapter) = language_registry.lsp_adapters(&language).first() {
|
||||
label = adapter
|
||||
.label_for_completion(&lsp_completion, &language)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Completion {
|
||||
old_range: old_start..old_end,
|
||||
new_text: completion.new_text,
|
||||
label: label.unwrap_or_else(|| {
|
||||
CodeLabel::plain(
|
||||
lsp_completion.label.clone(),
|
||||
lsp_completion.filter_text.as_deref(),
|
||||
)
|
||||
}),
|
||||
documentation: None,
|
||||
server_id: LanguageServerId(completion.server_id as usize),
|
||||
lsp_completion,
|
||||
})
|
||||
}
|
||||
|
||||
/// Serializes a [`CodeAction`] to be sent over RPC.
|
||||
pub fn serialize_code_action(action: &CodeAction) -> proto::CodeAction {
|
||||
proto::CodeAction {
|
||||
server_id: action.server_id.0 as u64,
|
||||
start: Some(serialize_anchor(&action.range.start)),
|
||||
end: Some(serialize_anchor(&action.range.end)),
|
||||
lsp_action: serde_json::to_vec(&action.lsp_action).unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Deserializes a [`CodeAction`] from the RPC representation.
|
||||
pub fn deserialize_code_action(action: proto::CodeAction) -> Result<CodeAction> {
|
||||
let start = action
|
||||
.start
|
||||
.and_then(deserialize_anchor)
|
||||
.ok_or_else(|| anyhow!("invalid start"))?;
|
||||
let end = action
|
||||
.end
|
||||
.and_then(deserialize_anchor)
|
||||
.ok_or_else(|| anyhow!("invalid end"))?;
|
||||
let lsp_action = serde_json::from_slice(&action.lsp_action)?;
|
||||
Ok(CodeAction {
|
||||
server_id: LanguageServerId(action.server_id as usize),
|
||||
range: start..end,
|
||||
lsp_action,
|
||||
})
|
||||
}
|
||||
|
||||
/// Serializes a [`Transaction`] to be sent over RPC.
|
||||
pub fn serialize_transaction(transaction: &Transaction) -> proto::Transaction {
|
||||
proto::Transaction {
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
use crate::{LanguageRegistry, Location};
|
||||
use crate::Location;
|
||||
|
||||
use anyhow::Result;
|
||||
use gpui::{AppContext, Context, Model};
|
||||
use std::sync::Arc;
|
||||
use task::{static_source::tasks_for, static_source::TaskDefinitions, TaskSource, TaskVariables};
|
||||
use gpui::AppContext;
|
||||
use task::{static_source::TaskDefinitions, TaskVariables, VariableName};
|
||||
|
||||
/// Language Contexts are used by Zed tasks to extract information about source file.
|
||||
pub trait ContextProvider: Send + Sync {
|
||||
fn build_context(&self, _: Location, _: &mut AppContext) -> Result<TaskVariables> {
|
||||
Ok(TaskVariables::default())
|
||||
}
|
||||
|
||||
fn associated_tasks(&self) -> Option<TaskDefinitions> {
|
||||
None
|
||||
}
|
||||
@@ -29,18 +29,16 @@ impl ContextProvider for SymbolContextProvider {
|
||||
.read(cx)
|
||||
.snapshot()
|
||||
.symbols_containing(location.range.start, None);
|
||||
let symbol = symbols.and_then(|symbols| {
|
||||
symbols.last().map(|symbol| {
|
||||
let range = symbol
|
||||
.name_ranges
|
||||
.last()
|
||||
.cloned()
|
||||
.unwrap_or(0..symbol.text.len());
|
||||
symbol.text[range].to_string()
|
||||
})
|
||||
let symbol = symbols.unwrap_or_default().last().map(|symbol| {
|
||||
let range = symbol
|
||||
.name_ranges
|
||||
.last()
|
||||
.cloned()
|
||||
.unwrap_or(0..symbol.text.len());
|
||||
symbol.text[range].to_string()
|
||||
});
|
||||
Ok(TaskVariables::from_iter(
|
||||
symbol.map(|symbol| ("ZED_SYMBOL".to_string(), symbol)),
|
||||
Some(VariableName::Symbol).zip(symbol),
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -65,45 +63,3 @@ impl ContextProvider for ContextProviderWithTasks {
|
||||
SymbolContextProvider.build_context(location, cx)
|
||||
}
|
||||
}
|
||||
|
||||
/// A source that pulls in the tasks from language registry.
|
||||
pub struct LanguageSource {
|
||||
languages: Arc<LanguageRegistry>,
|
||||
}
|
||||
|
||||
impl LanguageSource {
|
||||
pub fn new(
|
||||
languages: Arc<LanguageRegistry>,
|
||||
cx: &mut AppContext,
|
||||
) -> Model<Box<dyn TaskSource>> {
|
||||
cx.new_model(|_| Box::new(Self { languages }) as Box<_>)
|
||||
}
|
||||
}
|
||||
|
||||
impl TaskSource for LanguageSource {
|
||||
fn as_any(&mut self) -> &mut dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
|
||||
fn tasks_for_path(
|
||||
&mut self,
|
||||
_: Option<&std::path::Path>,
|
||||
_: &mut gpui::ModelContext<Box<dyn TaskSource>>,
|
||||
) -> Vec<Arc<dyn task::Task>> {
|
||||
self.languages
|
||||
.to_vec()
|
||||
.into_iter()
|
||||
.filter_map(|language| {
|
||||
language
|
||||
.context_provider()?
|
||||
.associated_tasks()
|
||||
.map(|tasks| (tasks, language))
|
||||
})
|
||||
.flat_map(|(tasks, language)| {
|
||||
let language_name = language.name();
|
||||
let id_base = format!("buffer_source_{language_name}");
|
||||
tasks_for(tasks, &id_base)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +40,6 @@ tree-sitter-bash.workspace = true
|
||||
tree-sitter-c.workspace = true
|
||||
tree-sitter-cpp.workspace = true
|
||||
tree-sitter-css.workspace = true
|
||||
tree-sitter-dart.workspace = true
|
||||
tree-sitter-elixir.workspace = true
|
||||
tree-sitter-elm.workspace = true
|
||||
tree-sitter-embedded-template.workspace = true
|
||||
@@ -50,7 +49,6 @@ tree-sitter-gomod.workspace = true
|
||||
tree-sitter-gowork.workspace = true
|
||||
tree-sitter-hcl.workspace = true
|
||||
tree-sitter-heex.workspace = true
|
||||
tree-sitter-html.workspace = true
|
||||
tree-sitter-jsdoc.workspace = true
|
||||
tree-sitter-json.workspace = true
|
||||
tree-sitter-lua.workspace = true
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use async_trait::async_trait;
|
||||
use gpui::AppContext;
|
||||
use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
|
||||
use lsp::LanguageServerBinary;
|
||||
use project::project_settings::ProjectSettings;
|
||||
use serde_json::Value;
|
||||
use settings::Settings;
|
||||
use std::{
|
||||
any::Any,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
pub struct DartLanguageServer;
|
||||
|
||||
#[async_trait(?Send)]
|
||||
impl LspAdapter for DartLanguageServer {
|
||||
fn name(&self) -> LanguageServerName {
|
||||
LanguageServerName("dart".into())
|
||||
}
|
||||
|
||||
async fn fetch_latest_server_version(
|
||||
&self,
|
||||
_: &dyn LspAdapterDelegate,
|
||||
) -> Result<Box<dyn 'static + Send + Any>> {
|
||||
Ok(Box::new(()))
|
||||
}
|
||||
|
||||
async fn fetch_server_binary(
|
||||
&self,
|
||||
_: Box<dyn 'static + Send + Any>,
|
||||
_: PathBuf,
|
||||
_: &dyn LspAdapterDelegate,
|
||||
) -> Result<LanguageServerBinary> {
|
||||
Err(anyhow!("dart must me installed from dart.dev/get-dart"))
|
||||
}
|
||||
|
||||
async fn cached_server_binary(
|
||||
&self,
|
||||
_: PathBuf,
|
||||
_: &dyn LspAdapterDelegate,
|
||||
) -> Option<LanguageServerBinary> {
|
||||
Some(LanguageServerBinary {
|
||||
path: "dart".into(),
|
||||
env: None,
|
||||
arguments: vec!["language-server".into(), "--protocol=lsp".into()],
|
||||
})
|
||||
}
|
||||
|
||||
fn can_be_reinstalled(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
async fn installation_test_binary(&self, _: PathBuf) -> Option<LanguageServerBinary> {
|
||||
None
|
||||
}
|
||||
|
||||
fn workspace_configuration(&self, _workspace_root: &Path, cx: &mut AppContext) -> Value {
|
||||
let settings = ProjectSettings::get_global(cx)
|
||||
.lsp
|
||||
.get("dart")
|
||||
.and_then(|s| s.settings.clone())
|
||||
.unwrap_or_default();
|
||||
|
||||
serde_json::json!({
|
||||
"dart": settings
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -2,12 +2,13 @@ use anyhow::{anyhow, bail, Context, Result};
|
||||
use async_trait::async_trait;
|
||||
use collections::HashMap;
|
||||
use futures::StreamExt;
|
||||
use gpui::AppContext;
|
||||
use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
|
||||
use lsp::{CodeActionKind, LanguageServerBinary};
|
||||
use schemars::JsonSchema;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use settings::Settings;
|
||||
use settings::{Settings, SettingsSources};
|
||||
use smol::{fs, fs::File};
|
||||
use std::{any::Any, env::consts, ffi::OsString, path::PathBuf, sync::Arc};
|
||||
use util::{
|
||||
@@ -31,15 +32,8 @@ impl Settings for DenoSettings {
|
||||
|
||||
type FileContent = DenoSettingsContent;
|
||||
|
||||
fn load(
|
||||
default_value: &Self::FileContent,
|
||||
user_values: &[&Self::FileContent],
|
||||
_: &mut gpui::AppContext,
|
||||
) -> Result<Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
Self::load_via_json_merge(default_value, user_values)
|
||||
fn load(sources: SettingsSources<Self::FileContent>, _: &mut AppContext) -> Result<Self> {
|
||||
sources.json_merge()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,19 +8,22 @@ use project::project_settings::ProjectSettings;
|
||||
use schemars::JsonSchema;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use settings::Settings;
|
||||
use settings::{Settings, SettingsSources};
|
||||
use smol::fs::{self, File};
|
||||
use std::{
|
||||
any::Any,
|
||||
env::consts,
|
||||
ops::Deref,
|
||||
path::{Path, PathBuf},
|
||||
path::PathBuf,
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering::SeqCst},
|
||||
Arc,
|
||||
},
|
||||
};
|
||||
use task::static_source::{Definition, TaskDefinitions};
|
||||
use task::{
|
||||
static_source::{Definition, TaskDefinitions},
|
||||
VariableName,
|
||||
};
|
||||
use util::{
|
||||
fs::remove_matching,
|
||||
github::{latest_github_release, GitHubLspBinaryVersion},
|
||||
@@ -53,15 +56,8 @@ impl Settings for ElixirSettings {
|
||||
|
||||
type FileContent = ElixirSettingsContent;
|
||||
|
||||
fn load(
|
||||
default_value: &Self::FileContent,
|
||||
user_values: &[&Self::FileContent],
|
||||
_: &mut gpui::AppContext,
|
||||
) -> Result<Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
Self::load_via_json_merge(default_value, user_values)
|
||||
fn load(sources: SettingsSources<Self::FileContent>, _: &mut AppContext) -> Result<Self> {
|
||||
sources.json_merge()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -275,16 +271,22 @@ impl LspAdapter for ElixirLspAdapter {
|
||||
})
|
||||
}
|
||||
|
||||
fn workspace_configuration(&self, _workspace_root: &Path, cx: &mut AppContext) -> Value {
|
||||
let settings = ProjectSettings::get_global(cx)
|
||||
.lsp
|
||||
.get("elixir-ls")
|
||||
.and_then(|s| s.settings.clone())
|
||||
.unwrap_or_default();
|
||||
async fn workspace_configuration(
|
||||
self: Arc<Self>,
|
||||
_: &Arc<dyn LspAdapterDelegate>,
|
||||
cx: &mut AsyncAppContext,
|
||||
) -> Result<Value> {
|
||||
let settings = cx.update(|cx| {
|
||||
ProjectSettings::get_global(cx)
|
||||
.lsp
|
||||
.get("elixir-ls")
|
||||
.and_then(|s| s.settings.clone())
|
||||
.unwrap_or_default()
|
||||
})?;
|
||||
|
||||
serde_json::json!({
|
||||
Ok(serde_json::json!({
|
||||
"elixirLS": settings
|
||||
})
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -557,25 +559,32 @@ pub(super) fn elixir_task_context() -> ContextProviderWithTasks {
|
||||
label: "Elixir: test suite".to_owned(),
|
||||
command: "mix".to_owned(),
|
||||
args: vec!["test".to_owned()],
|
||||
..Default::default()
|
||||
..Definition::default()
|
||||
},
|
||||
Definition {
|
||||
label: "Elixir: failed tests suite".to_owned(),
|
||||
command: "mix".to_owned(),
|
||||
args: vec!["test".to_owned(), "--failed".to_owned()],
|
||||
..Default::default()
|
||||
..Definition::default()
|
||||
},
|
||||
Definition {
|
||||
label: "Elixir: test file".to_owned(),
|
||||
command: "mix".to_owned(),
|
||||
args: vec!["test".to_owned(), "$ZED_FILE".to_owned()],
|
||||
..Default::default()
|
||||
args: vec!["test".to_owned(), VariableName::Symbol.template_value()],
|
||||
..Definition::default()
|
||||
},
|
||||
Definition {
|
||||
label: "Elixir: test at current line".to_owned(),
|
||||
command: "mix".to_owned(),
|
||||
args: vec!["test".to_owned(), "$ZED_FILE:$ZED_ROW".to_owned()],
|
||||
..Default::default()
|
||||
args: vec![
|
||||
"test".to_owned(),
|
||||
format!(
|
||||
"{}:{}",
|
||||
VariableName::File.template_value(),
|
||||
VariableName::Row.template_value()
|
||||
),
|
||||
],
|
||||
..Definition::default()
|
||||
},
|
||||
Definition {
|
||||
label: "Elixir: break line".to_owned(),
|
||||
@@ -585,9 +594,13 @@ pub(super) fn elixir_task_context() -> ContextProviderWithTasks {
|
||||
"mix".to_owned(),
|
||||
"test".to_owned(),
|
||||
"-b".to_owned(),
|
||||
"$ZED_FILE:$ZED_ROW".to_owned(),
|
||||
format!(
|
||||
"{}:{}",
|
||||
VariableName::File.template_value(),
|
||||
VariableName::Row.template_value()
|
||||
),
|
||||
],
|
||||
..Default::default()
|
||||
..Definition::default()
|
||||
},
|
||||
]))
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ brackets = [
|
||||
{ start = "\"", end = "\"", close = true, newline = false, not_in = ["string", "comment"] },
|
||||
{ start = "'", end = "'", close = true, newline = false, not_in = ["string", "comment"] },
|
||||
]
|
||||
tab_size = 2
|
||||
scope_opt_in_language_servers = ["tailwindcss-language-server"]
|
||||
|
||||
[overrides.string]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use async_trait::async_trait;
|
||||
use futures::StreamExt;
|
||||
use gpui::AppContext;
|
||||
use gpui::AsyncAppContext;
|
||||
use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
|
||||
use lsp::LanguageServerBinary;
|
||||
use node_runtime::NodeRuntime;
|
||||
@@ -94,16 +94,22 @@ impl LspAdapter for ElmLspAdapter {
|
||||
get_cached_server_binary(container_dir, &*self.node).await
|
||||
}
|
||||
|
||||
fn workspace_configuration(&self, _workspace_root: &Path, cx: &mut AppContext) -> Value {
|
||||
async fn workspace_configuration(
|
||||
self: Arc<Self>,
|
||||
_: &Arc<dyn LspAdapterDelegate>,
|
||||
cx: &mut AsyncAppContext,
|
||||
) -> Result<Value> {
|
||||
// elm-language-server expects workspace didChangeConfiguration notification
|
||||
// params to be the same as lsp initialization_options
|
||||
let override_options = ProjectSettings::get_global(cx)
|
||||
.lsp
|
||||
.get(SERVER_NAME)
|
||||
.and_then(|s| s.initialization_options.clone())
|
||||
.unwrap_or_default();
|
||||
let override_options = cx.update(|cx| {
|
||||
ProjectSettings::get_global(cx)
|
||||
.lsp
|
||||
.get(SERVER_NAME)
|
||||
.and_then(|s| s.initialization_options.clone())
|
||||
.unwrap_or_default()
|
||||
})?;
|
||||
|
||||
match override_options.clone().as_object_mut() {
|
||||
Ok(match override_options.clone().as_object_mut() {
|
||||
Some(op) => {
|
||||
// elm-language-server requests workspace configuration
|
||||
// for the `elmLS` section, so we have to nest
|
||||
@@ -112,7 +118,7 @@ impl LspAdapter for ElmLspAdapter {
|
||||
serde_json::to_value(op).unwrap_or_default()
|
||||
}
|
||||
None => override_options,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,3 +11,5 @@ brackets = [
|
||||
{ start = "'", end = "'", close = true, newline = false, not_in = ["comment", "string"] },
|
||||
{ start = "/*", end = " */", close = true, newline = false, not_in = ["comment", "string"] },
|
||||
]
|
||||
tab_size = 4
|
||||
hard_tabs = true
|
||||
|
||||
@@ -1,134 +0,0 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use async_trait::async_trait;
|
||||
use futures::StreamExt;
|
||||
use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
|
||||
use lsp::LanguageServerBinary;
|
||||
use node_runtime::NodeRuntime;
|
||||
use serde_json::json;
|
||||
use smol::fs;
|
||||
use std::{
|
||||
any::Any,
|
||||
ffi::OsString,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
use util::{maybe, ResultExt};
|
||||
|
||||
const SERVER_PATH: &str =
|
||||
"node_modules/vscode-langservers-extracted/bin/vscode-html-language-server";
|
||||
|
||||
fn server_binary_arguments(server_path: &Path) -> Vec<OsString> {
|
||||
vec![server_path.into(), "--stdio".into()]
|
||||
}
|
||||
|
||||
pub struct HtmlLspAdapter {
|
||||
node: Arc<dyn NodeRuntime>,
|
||||
}
|
||||
|
||||
impl HtmlLspAdapter {
|
||||
pub fn new(node: Arc<dyn NodeRuntime>) -> Self {
|
||||
HtmlLspAdapter { node }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
impl LspAdapter for HtmlLspAdapter {
|
||||
fn name(&self) -> LanguageServerName {
|
||||
LanguageServerName("vscode-html-language-server".into())
|
||||
}
|
||||
|
||||
async fn fetch_latest_server_version(
|
||||
&self,
|
||||
_: &dyn LspAdapterDelegate,
|
||||
) -> Result<Box<dyn 'static + Any + Send>> {
|
||||
Ok(Box::new(
|
||||
self.node
|
||||
.npm_package_latest_version("vscode-langservers-extracted")
|
||||
.await?,
|
||||
) as Box<_>)
|
||||
}
|
||||
|
||||
async fn fetch_server_binary(
|
||||
&self,
|
||||
latest_version: Box<dyn 'static + Send + Any>,
|
||||
container_dir: PathBuf,
|
||||
_: &dyn LspAdapterDelegate,
|
||||
) -> Result<LanguageServerBinary> {
|
||||
let latest_version = latest_version.downcast::<String>().unwrap();
|
||||
let server_path = container_dir.join(SERVER_PATH);
|
||||
let package_name = "vscode-langservers-extracted";
|
||||
|
||||
let should_install_language_server = self
|
||||
.node
|
||||
.should_install_npm_package(package_name, &server_path, &container_dir, &latest_version)
|
||||
.await;
|
||||
|
||||
if should_install_language_server {
|
||||
self.node
|
||||
.npm_install_packages(&container_dir, &[(package_name, latest_version.as_str())])
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(LanguageServerBinary {
|
||||
path: self.node.binary_path().await?,
|
||||
env: None,
|
||||
arguments: server_binary_arguments(&server_path),
|
||||
})
|
||||
}
|
||||
|
||||
async fn cached_server_binary(
|
||||
&self,
|
||||
container_dir: PathBuf,
|
||||
_: &dyn LspAdapterDelegate,
|
||||
) -> Option<LanguageServerBinary> {
|
||||
get_cached_server_binary(container_dir, &*self.node).await
|
||||
}
|
||||
|
||||
async fn installation_test_binary(
|
||||
&self,
|
||||
container_dir: PathBuf,
|
||||
) -> Option<LanguageServerBinary> {
|
||||
get_cached_server_binary(container_dir, &*self.node).await
|
||||
}
|
||||
|
||||
async fn initialization_options(
|
||||
self: Arc<Self>,
|
||||
_: &Arc<dyn LspAdapterDelegate>,
|
||||
) -> Result<Option<serde_json::Value>> {
|
||||
Ok(Some(json!({
|
||||
"provideFormatter": true
|
||||
})))
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_cached_server_binary(
|
||||
container_dir: PathBuf,
|
||||
node: &dyn NodeRuntime,
|
||||
) -> Option<LanguageServerBinary> {
|
||||
maybe!(async {
|
||||
let mut last_version_dir = None;
|
||||
let mut entries = fs::read_dir(&container_dir).await?;
|
||||
while let Some(entry) = entries.next().await {
|
||||
let entry = entry?;
|
||||
if entry.file_type().await?.is_dir() {
|
||||
last_version_dir = Some(entry.path());
|
||||
}
|
||||
}
|
||||
let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?;
|
||||
let server_path = last_version_dir.join(SERVER_PATH);
|
||||
if server_path.exists() {
|
||||
Ok(LanguageServerBinary {
|
||||
path: node.binary_path().await?,
|
||||
env: None,
|
||||
arguments: server_binary_arguments(&server_path),
|
||||
})
|
||||
} else {
|
||||
Err(anyhow!(
|
||||
"missing executable in directory {:?}",
|
||||
last_version_dir
|
||||
))
|
||||
}
|
||||
})
|
||||
.await
|
||||
.log_err()
|
||||
}
|
||||
@@ -15,6 +15,7 @@ brackets = [
|
||||
{ start = "/*", end = " */", close = true, newline = false, not_in = ["comment", "string"] },
|
||||
]
|
||||
word_characters = ["$", "#"]
|
||||
tab_size = 2
|
||||
scope_opt_in_language_servers = ["tailwindcss-language-server"]
|
||||
prettier_parser_name = "babel"
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ use async_trait::async_trait;
|
||||
use collections::HashMap;
|
||||
use feature_flags::FeatureFlagAppExt;
|
||||
use futures::StreamExt;
|
||||
use gpui::AppContext;
|
||||
use gpui::{AppContext, AsyncAppContext};
|
||||
use language::{LanguageRegistry, LanguageServerName, LspAdapter, LspAdapterDelegate};
|
||||
use lsp::LanguageServerBinary;
|
||||
use node_runtime::NodeRuntime;
|
||||
@@ -152,10 +152,16 @@ impl LspAdapter for JsonLspAdapter {
|
||||
})))
|
||||
}
|
||||
|
||||
fn workspace_configuration(&self, _workspace_root: &Path, cx: &mut AppContext) -> Value {
|
||||
self.workspace_config
|
||||
.get_or_init(|| Self::get_workspace_config(self.languages.language_names(), cx))
|
||||
.clone()
|
||||
async fn workspace_configuration(
|
||||
self: Arc<Self>,
|
||||
_: &Arc<dyn LspAdapterDelegate>,
|
||||
cx: &mut AsyncAppContext,
|
||||
) -> Result<Value> {
|
||||
cx.update(|cx| {
|
||||
self.workspace_config
|
||||
.get_or_init(|| Self::get_workspace_config(self.languages.language_names(), cx))
|
||||
.clone()
|
||||
})
|
||||
}
|
||||
|
||||
fn language_ids(&self) -> HashMap<String, String> {
|
||||
|
||||
@@ -9,3 +9,4 @@ brackets = [
|
||||
{ start = "\"", end = "\"", close = true, newline = false, not_in = ["string"] },
|
||||
]
|
||||
prettier_parser_name = "json"
|
||||
tab_size = 2
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
use anyhow::Context;
|
||||
use gpui::AppContext;
|
||||
use gpui::{AppContext, BorrowAppContext};
|
||||
pub use language::*;
|
||||
use node_runtime::NodeRuntime;
|
||||
use rust_embed::RustEmbed;
|
||||
use settings::Settings;
|
||||
use settings::{Settings, SettingsStore};
|
||||
use smol::stream::StreamExt;
|
||||
use std::{str, sync::Arc};
|
||||
use util::asset_str;
|
||||
use util::{asset_str, ResultExt};
|
||||
|
||||
use crate::{elixir::elixir_task_context, rust::RustContextProvider};
|
||||
|
||||
@@ -13,12 +14,10 @@ use self::{deno::DenoSettings, elixir::ElixirSettings};
|
||||
|
||||
mod c;
|
||||
mod css;
|
||||
mod dart;
|
||||
mod deno;
|
||||
mod elixir;
|
||||
mod elm;
|
||||
mod go;
|
||||
mod html;
|
||||
mod json;
|
||||
mod lua;
|
||||
mod nu;
|
||||
@@ -71,7 +70,6 @@ pub fn init(
|
||||
("gowork", tree_sitter_gowork::language()),
|
||||
("hcl", tree_sitter_hcl::language()),
|
||||
("heex", tree_sitter_heex::language()),
|
||||
("html", tree_sitter_html::language()),
|
||||
("jsdoc", tree_sitter_jsdoc::language()),
|
||||
("json", tree_sitter_json::language()),
|
||||
("lua", tree_sitter_lua::language()),
|
||||
@@ -94,7 +92,6 @@ pub fn init(
|
||||
("typescript", tree_sitter_typescript::language_typescript()),
|
||||
("vue", tree_sitter_vue::language()),
|
||||
("yaml", tree_sitter_yaml::language()),
|
||||
("dart", tree_sitter_dart::language()),
|
||||
]);
|
||||
|
||||
macro_rules! language {
|
||||
@@ -275,13 +272,6 @@ pub fn init(
|
||||
}
|
||||
}
|
||||
|
||||
language!(
|
||||
"html",
|
||||
vec![
|
||||
Arc::new(html::HtmlLspAdapter::new(node_runtime.clone())),
|
||||
Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())),
|
||||
]
|
||||
);
|
||||
language!("ruby", vec![Arc::new(ruby::RubyLanguageServer)]);
|
||||
language!(
|
||||
"erb",
|
||||
@@ -321,12 +311,15 @@ pub fn init(
|
||||
vec![Arc::new(terraform::TerraformLspAdapter)]
|
||||
);
|
||||
language!("hcl", vec![]);
|
||||
language!("dart", vec![Arc::new(dart::DartLanguageServer {})]);
|
||||
|
||||
languages.register_secondary_lsp_adapter(
|
||||
"Astro".into(),
|
||||
Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())),
|
||||
);
|
||||
languages.register_secondary_lsp_adapter(
|
||||
"HTML".into(),
|
||||
Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())),
|
||||
);
|
||||
languages.register_secondary_lsp_adapter(
|
||||
"PHP".into(),
|
||||
Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())),
|
||||
@@ -335,6 +328,27 @@ pub fn init(
|
||||
"Svelte".into(),
|
||||
Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())),
|
||||
);
|
||||
|
||||
let mut subscription = languages.subscribe();
|
||||
let mut prev_language_settings = languages.language_settings();
|
||||
|
||||
cx.spawn(|cx| async move {
|
||||
while subscription.next().await.is_some() {
|
||||
let language_settings = languages.language_settings();
|
||||
if language_settings != prev_language_settings {
|
||||
cx.update(|cx| {
|
||||
cx.update_global(|settings: &mut SettingsStore, cx| {
|
||||
settings
|
||||
.set_extension_settings(language_settings.clone(), cx)
|
||||
.log_err();
|
||||
});
|
||||
})?;
|
||||
prev_language_settings = language_settings;
|
||||
}
|
||||
}
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user