Merge branch 'main' into joseph/z-226-add-terminal-popup-menu
This commit is contained in:
commit
bccc34c61a
159 changed files with 5447 additions and 1711 deletions
58
Cargo.lock
generated
58
Cargo.lock
generated
|
@ -1188,7 +1188,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "collab"
|
name = "collab"
|
||||||
version = "0.6.1"
|
version = "0.7.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-tungstenite",
|
"async-tungstenite",
|
||||||
|
@ -1259,6 +1259,7 @@ dependencies = [
|
||||||
"collections",
|
"collections",
|
||||||
"context_menu",
|
"context_menu",
|
||||||
"editor",
|
"editor",
|
||||||
|
"feedback",
|
||||||
"futures 0.3.25",
|
"futures 0.3.25",
|
||||||
"fuzzy",
|
"fuzzy",
|
||||||
"gpui",
|
"gpui",
|
||||||
|
@ -3018,6 +3019,17 @@ version = "1.0.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "adab1eaa3408fb7f0c777a73e7465fd5656136fc93b670eb6df3c88c2c1344e3"
|
checksum = "adab1eaa3408fb7f0c777a73e7465fd5656136fc93b670eb6df3c88c2c1344e3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "install_cli"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"gpui",
|
||||||
|
"log",
|
||||||
|
"smol",
|
||||||
|
"util",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "instant"
|
name = "instant"
|
||||||
version = "0.1.12"
|
version = "0.1.12"
|
||||||
|
@ -3157,6 +3169,7 @@ dependencies = [
|
||||||
name = "journal"
|
name = "journal"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
"chrono",
|
"chrono",
|
||||||
"dirs 4.0.0",
|
"dirs 4.0.0",
|
||||||
"editor",
|
"editor",
|
||||||
|
@ -3285,6 +3298,22 @@ dependencies = [
|
||||||
"util",
|
"util",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "language_selector"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"editor",
|
||||||
|
"fuzzy",
|
||||||
|
"gpui",
|
||||||
|
"language",
|
||||||
|
"picker",
|
||||||
|
"project",
|
||||||
|
"settings",
|
||||||
|
"theme",
|
||||||
|
"workspace",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lazy_static"
|
name = "lazy_static"
|
||||||
version = "1.4.0"
|
version = "1.4.0"
|
||||||
|
@ -8013,6 +8042,26 @@ version = "0.1.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9193164d4de03a926d909d3bc7c30543cecb35400c02114792c2cae20d5e2dbb"
|
checksum = "9193164d4de03a926d909d3bc7c30543cecb35400c02114792c2cae20d5e2dbb"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "welcome"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"db",
|
||||||
|
"editor",
|
||||||
|
"fuzzy",
|
||||||
|
"gpui",
|
||||||
|
"install_cli",
|
||||||
|
"log",
|
||||||
|
"picker",
|
||||||
|
"project",
|
||||||
|
"settings",
|
||||||
|
"theme",
|
||||||
|
"theme_selector",
|
||||||
|
"util",
|
||||||
|
"workspace",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wepoll-ffi"
|
name = "wepoll-ffi"
|
||||||
version = "0.1.2"
|
version = "0.1.2"
|
||||||
|
@ -8288,6 +8337,7 @@ dependencies = [
|
||||||
"futures 0.3.25",
|
"futures 0.3.25",
|
||||||
"gpui",
|
"gpui",
|
||||||
"indoc",
|
"indoc",
|
||||||
|
"install_cli",
|
||||||
"language",
|
"language",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"log",
|
"log",
|
||||||
|
@ -8359,7 +8409,7 @@ checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zed"
|
name = "zed"
|
||||||
version = "0.76.0"
|
version = "0.77.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"activity_indicator",
|
"activity_indicator",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
@ -8381,6 +8431,7 @@ dependencies = [
|
||||||
"command_palette",
|
"command_palette",
|
||||||
"context_menu",
|
"context_menu",
|
||||||
"ctor",
|
"ctor",
|
||||||
|
"db",
|
||||||
"diagnostics",
|
"diagnostics",
|
||||||
"easy-parallel",
|
"easy-parallel",
|
||||||
"editor",
|
"editor",
|
||||||
|
@ -8396,9 +8447,11 @@ dependencies = [
|
||||||
"ignore",
|
"ignore",
|
||||||
"image",
|
"image",
|
||||||
"indexmap",
|
"indexmap",
|
||||||
|
"install_cli",
|
||||||
"isahc",
|
"isahc",
|
||||||
"journal",
|
"journal",
|
||||||
"language",
|
"language",
|
||||||
|
"language_selector",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"libc",
|
"libc",
|
||||||
"log",
|
"log",
|
||||||
|
@ -8460,6 +8513,7 @@ dependencies = [
|
||||||
"util",
|
"util",
|
||||||
"uuid 1.2.2",
|
"uuid 1.2.2",
|
||||||
"vim",
|
"vim",
|
||||||
|
"welcome",
|
||||||
"workspace",
|
"workspace",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -26,8 +26,10 @@ members = [
|
||||||
"crates/go_to_line",
|
"crates/go_to_line",
|
||||||
"crates/gpui",
|
"crates/gpui",
|
||||||
"crates/gpui_macros",
|
"crates/gpui_macros",
|
||||||
|
"crates/install_cli",
|
||||||
"crates/journal",
|
"crates/journal",
|
||||||
"crates/language",
|
"crates/language",
|
||||||
|
"crates/language_selector",
|
||||||
"crates/live_kit_client",
|
"crates/live_kit_client",
|
||||||
"crates/live_kit_server",
|
"crates/live_kit_server",
|
||||||
"crates/lsp",
|
"crates/lsp",
|
||||||
|
@ -58,6 +60,7 @@ members = [
|
||||||
"crates/util",
|
"crates/util",
|
||||||
"crates/vim",
|
"crates/vim",
|
||||||
"crates/workspace",
|
"crates/workspace",
|
||||||
|
"crates/welcome",
|
||||||
"crates/zed",
|
"crates/zed",
|
||||||
]
|
]
|
||||||
default-members = ["crates/zed"]
|
default-members = ["crates/zed"]
|
||||||
|
|
3
assets/icons/logo_96.svg
Normal file
3
assets/icons/logo_96.svg
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<svg width="96" height="96" viewBox="0 0 96 96" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M9 6C7.34315 6 6 7.34315 6 9V75H0V9C0 4.02944 4.02944 0 9 0H89.3787C93.3878 0 95.3955 4.84715 92.5607 7.68198L43.0551 57.1875H57V51H63V58.6875C63 61.1728 60.9853 63.1875 58.5 63.1875H37.0551L26.7426 73.5H73.5V36H79.5V73.5C79.5 76.8137 76.8137 79.5 73.5 79.5H20.7426L10.2426 90H87C88.6569 90 90 88.6569 90 87V21H96V87C96 91.9706 91.9706 96 87 96H6.62132C2.61224 96 0.604504 91.1529 3.43934 88.318L52.7574 39H39V45H33V37.5C33 35.0147 35.0147 33 37.5 33H58.7574L69.2574 22.5H22.5V60H16.5V22.5C16.5 19.1863 19.1863 16.5 22.5 16.5H75.2574L85.7574 6H9Z" fill="white"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 715 B |
3
assets/icons/speech_bubble_12.svg
Normal file
3
assets/icons/speech_bubble_12.svg
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M10.6667 0.400196H1.33346C0.819658 0.400196 0.399658 0.820196 0.399658 1.3326V10.6658C0.399658 11.181 0.816998 11.5982 1.33206 11.5982C1.58966 11.5982 1.82206 11.4932 1.99146 11.3238L4.51706 8.79684H10.6639C11.1763 8.79684 11.5963 8.37544 11.5963 7.86304V1.3298C11.5963 0.815996 11.1749 0.395996 10.6625 0.395996L10.6667 0.400196ZM2.2667 2.2664H6.00008V3.1988H2.26628V2.265L2.2667 2.2664ZM7.8667 6.93316H2.2667V5.99936H7.8667V6.93176V6.93316ZM9.7329 5.06556H2.26488V4.13176H9.73164V5.06416L9.7329 5.06556Z" fill="#282C34"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 636 B |
68
assets/keymaps/atom.json
Normal file
68
assets/keymaps/atom.json
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"bindings": {
|
||||||
|
"cmd-k cmd-p": "workspace::ActivatePreviousPane",
|
||||||
|
"cmd-k cmd-n": "workspace::ActivateNextPane"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"context": "Editor",
|
||||||
|
"bindings": {
|
||||||
|
"cmd-b": "editor::GoToDefinition",
|
||||||
|
"cmd-<": "editor::ScrollCursorCenter",
|
||||||
|
"cmd-g": [
|
||||||
|
"editor::SelectNext",
|
||||||
|
{
|
||||||
|
"replace_newest": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"ctrl-shift-down": "editor::AddSelectionBelow",
|
||||||
|
"ctrl-shift-up": "editor::AddSelectionAbove",
|
||||||
|
"cmd-shift-backspace": "editor::DeleteToBeginningOfLine"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"context": "Editor && mode == full",
|
||||||
|
"bindings": {
|
||||||
|
"cmd-r": "outline::Toggle"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"context": "BufferSearchBar",
|
||||||
|
"bindings": {
|
||||||
|
"cmd-f3": "search::SelectNextMatch",
|
||||||
|
"cmd-shift-f3": "search::SelectPrevMatch"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"context": "Workspace",
|
||||||
|
"bindings": {
|
||||||
|
"cmd-\\": "workspace::ToggleLeftSidebar",
|
||||||
|
"cmd-k cmd-b": "workspace::ToggleLeftSidebar",
|
||||||
|
"cmd-t": "file_finder::Toggle",
|
||||||
|
"cmd-shift-r": "project_symbols::Toggle"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"context": "Pane",
|
||||||
|
"bindings": {
|
||||||
|
"alt-cmd-/": "search::ToggleRegex",
|
||||||
|
"ctrl-0": "project_panel::ToggleFocus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"context": "ProjectPanel",
|
||||||
|
"bindings": {
|
||||||
|
"ctrl-[": "project_panel::CollapseSelectedEntry",
|
||||||
|
"ctrl-b": "project_panel::CollapseSelectedEntry",
|
||||||
|
"h": "project_panel::CollapseSelectedEntry",
|
||||||
|
"ctrl-]": "project_panel::ExpandSelectedEntry",
|
||||||
|
"ctrl-f": "project_panel::ExpandSelectedEntry",
|
||||||
|
"ctrl-shift-c": "project_panel::CopyPath"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"context": "Dock",
|
||||||
|
"bindings": {}
|
||||||
|
}
|
||||||
|
]
|
|
@ -353,7 +353,8 @@
|
||||||
"cmd-shift-p": "command_palette::Toggle",
|
"cmd-shift-p": "command_palette::Toggle",
|
||||||
"cmd-shift-m": "diagnostics::Deploy",
|
"cmd-shift-m": "diagnostics::Deploy",
|
||||||
"cmd-shift-e": "project_panel::ToggleFocus",
|
"cmd-shift-e": "project_panel::ToggleFocus",
|
||||||
"cmd-alt-s": "workspace::SaveAll"
|
"cmd-alt-s": "workspace::SaveAll",
|
||||||
|
"cmd-k m": "language_selector::Toggle"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// Bindings from Sublime Text
|
// Bindings from Sublime Text
|
||||||
|
@ -537,4 +538,4 @@
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
78
assets/keymaps/jetbrains.json
Normal file
78
assets/keymaps/jetbrains.json
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"bindings": {
|
||||||
|
"cmd-shift-[": "pane::ActivatePrevItem",
|
||||||
|
"cmd-shift-]": "pane::ActivateNextItem"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"context": "Editor",
|
||||||
|
"bindings": {
|
||||||
|
"ctrl->": "zed::IncreaseBufferFontSize",
|
||||||
|
"ctrl-<": "zed::DecreaseBufferFontSize",
|
||||||
|
"cmd-d": "editor::DuplicateLine",
|
||||||
|
"cmd-pagedown": "editor::MovePageDown",
|
||||||
|
"cmd-pageup": "editor::MovePageUp",
|
||||||
|
"ctrl-alt-shift-b": "editor::SelectToPreviousWordStart",
|
||||||
|
"shift-enter": "editor::NewlineBelow",
|
||||||
|
"cmd--": "editor::Fold",
|
||||||
|
"cmd-=": "editor::UnfoldLines",
|
||||||
|
"alt-shift-g": "editor::SplitSelectionIntoLines",
|
||||||
|
"ctrl-g": [
|
||||||
|
"editor::SelectNext",
|
||||||
|
{
|
||||||
|
"replace_newest": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cmd-/": [
|
||||||
|
"editor::ToggleComments",
|
||||||
|
{
|
||||||
|
"advance_downwards": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"shift-alt-up": "editor::MoveLineUp",
|
||||||
|
"shift-alt-down": "editor::MoveLineDown",
|
||||||
|
"cmd-[": "pane::GoBack",
|
||||||
|
"cmd-]": "pane::GoForward",
|
||||||
|
"alt-f7": "editor::FindAllReferences",
|
||||||
|
"cmd-alt-f7": "editor::FindAllReferences",
|
||||||
|
"cmd-b": "editor::GoToDefinition",
|
||||||
|
"cmd-alt-b": "editor::GoToDefinition",
|
||||||
|
"cmd-shift-b": "editor::GoToTypeDefinition",
|
||||||
|
"alt-enter": "editor::ToggleCodeActions",
|
||||||
|
"f2": "editor::GoToDiagnostic",
|
||||||
|
"cmd-f2": "editor::GoToPrevDiagnostic",
|
||||||
|
"ctrl-alt-shift-down": "editor::GoToHunk",
|
||||||
|
"ctrl-alt-shift-up": "editor::GoToPrevHunk",
|
||||||
|
"cmd-home": "editor::MoveToBeginning",
|
||||||
|
"cmd-end": "editor::MoveToEnd",
|
||||||
|
"cmd-shift-home": "editor::SelectToBeginning",
|
||||||
|
"cmd-shift-end": "editor::SelectToEnd"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"context": "Editor && mode == full",
|
||||||
|
"bindings": {
|
||||||
|
"cmd-f12": "outline::Toggle",
|
||||||
|
"cmd-7": "outline::Toggle",
|
||||||
|
"cmd-shift-o": "file_finder::Toggle",
|
||||||
|
"cmd-l": "go_to_line::Toggle"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"context": "Workspace",
|
||||||
|
"bindings": {
|
||||||
|
"cmd-shift-a": "command_palette::Toggle",
|
||||||
|
"cmd-alt-o": "project_symbols::Toggle",
|
||||||
|
"cmd-1": "workspace::ToggleLeftSidebar",
|
||||||
|
"cmd-6": "diagnostics::Deploy",
|
||||||
|
"alt-f12": "dock::FocusDock"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"context": "Dock",
|
||||||
|
"bindings": {
|
||||||
|
"alt-f12": "dock::HideDock"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
60
assets/keymaps/sublime_text.json
Normal file
60
assets/keymaps/sublime_text.json
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"bindings": {
|
||||||
|
"cmd-shift-[": "pane::ActivatePrevItem",
|
||||||
|
"cmd-shift-]": "pane::ActivateNextItem",
|
||||||
|
"ctrl-pagedown": "pane::ActivatePrevItem",
|
||||||
|
"ctrl-pageup": "pane::ActivateNextItem",
|
||||||
|
"ctrl-shift-tab": "pane::ActivateNextItem",
|
||||||
|
"ctrl-tab": "pane::ActivatePrevItem",
|
||||||
|
"cmd-+": "zed::IncreaseBufferFontSize"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"context": "Editor",
|
||||||
|
"bindings": {
|
||||||
|
"ctrl-shift-up": "editor::AddSelectionAbove",
|
||||||
|
"ctrl-shift-down": "editor::AddSelectionBelow",
|
||||||
|
"cmd-shift-space": "editor::SelectAll",
|
||||||
|
"ctrl-shift-m": "editor::SelectLargerSyntaxNode",
|
||||||
|
"cmd-shift-a": "editor::SelectLargerSyntaxNode",
|
||||||
|
"shift-f12": "editor::FindAllReferences",
|
||||||
|
"alt-cmd-down": "editor::GoToDefinition",
|
||||||
|
"alt-shift-cmd-down": "editor::FindAllReferences",
|
||||||
|
"ctrl-.": "editor::GoToHunk",
|
||||||
|
"ctrl-,": "editor::GoToPrevHunk",
|
||||||
|
"ctrl-backspace": "editor::DeleteToPreviousWordStart",
|
||||||
|
"ctrl-delete": "editor::DeleteToNextWordEnd"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"context": "Editor && mode == full",
|
||||||
|
"bindings": {
|
||||||
|
"cmd-r": "outline::Toggle"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"context": "Pane",
|
||||||
|
"bindings": {
|
||||||
|
"f4": "search::SelectNextMatch",
|
||||||
|
"shift-f4": "search::SelectPrevMatch"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"context": "Workspace",
|
||||||
|
"bindings": {
|
||||||
|
"ctrl-`": "dock::FocusDock",
|
||||||
|
"cmd-k cmd-b": "workspace::ToggleLeftSidebar",
|
||||||
|
"cmd-t": "file_finder::Toggle",
|
||||||
|
"shift-cmd-r": "project_symbols::Toggle",
|
||||||
|
// Currently busted: https://github.com/zed-industries/feedback/issues/898
|
||||||
|
"ctrl-0": "project_panel::ToggleFocus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"context": "Dock",
|
||||||
|
"bindings": {
|
||||||
|
"ctrl-`": "dock::HideDock"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
|
@ -50,7 +50,7 @@
|
||||||
// "default_dock_anchor": "right"
|
// "default_dock_anchor": "right"
|
||||||
// 3. Position the dock full screen over the entire workspace"
|
// 3. Position the dock full screen over the entire workspace"
|
||||||
// "default_dock_anchor": "expanded"
|
// "default_dock_anchor": "expanded"
|
||||||
"default_dock_anchor": "right",
|
"default_dock_anchor": "bottom",
|
||||||
// Whether or not to remove any trailing whitespace from lines of a buffer
|
// Whether or not to remove any trailing whitespace from lines of a buffer
|
||||||
// before saving it.
|
// before saving it.
|
||||||
"remove_trailing_whitespace_on_save": true,
|
"remove_trailing_whitespace_on_save": true,
|
||||||
|
|
|
@ -20,7 +20,7 @@ use project::Project;
|
||||||
use std::{mem, sync::Arc, time::Duration};
|
use std::{mem, sync::Arc, time::Duration};
|
||||||
use util::{post_inc, ResultExt, TryFutureExt};
|
use util::{post_inc, ResultExt, TryFutureExt};
|
||||||
|
|
||||||
pub const RECONNECT_TIMEOUT: Duration = client::RECEIVE_TIMEOUT;
|
pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30);
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
pub enum Event {
|
pub enum Event {
|
||||||
|
|
|
@ -224,7 +224,7 @@ impl Telemetry {
|
||||||
.header("Content-Type", "application/json")
|
.header("Content-Type", "application/json")
|
||||||
.body(json_bytes.into())?;
|
.body(json_bytes.into())?;
|
||||||
this.http_client.send(request).await?;
|
this.http_client.send(request).await?;
|
||||||
Ok(())
|
anyhow::Ok(())
|
||||||
}
|
}
|
||||||
.log_err(),
|
.log_err(),
|
||||||
)
|
)
|
||||||
|
@ -320,7 +320,7 @@ impl Telemetry {
|
||||||
.header("Content-Type", "application/json")
|
.header("Content-Type", "application/json")
|
||||||
.body(json_bytes.into())?;
|
.body(json_bytes.into())?;
|
||||||
this.http_client.send(request).await?;
|
this.http_client.send(request).await?;
|
||||||
Ok(())
|
anyhow::Ok(())
|
||||||
}
|
}
|
||||||
.log_err(),
|
.log_err(),
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
DATABASE_URL = "postgres://postgres@localhost/zed"
|
DATABASE_URL = "postgres://postgres@localhost/zed"
|
||||||
|
DATABASE_MAX_CONNECTIONS = 5
|
||||||
HTTP_PORT = 8080
|
HTTP_PORT = 8080
|
||||||
API_TOKEN = "secret"
|
API_TOKEN = "secret"
|
||||||
INVITE_LINK_PREFIX = "http://localhost:3000/invites/"
|
INVITE_LINK_PREFIX = "http://localhost:3000/invites/"
|
||||||
|
|
|
@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathan@zed.dev>"]
|
||||||
default-run = "collab"
|
default-run = "collab"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
name = "collab"
|
name = "collab"
|
||||||
version = "0.6.1"
|
version = "0.7.1"
|
||||||
publish = false
|
publish = false
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
ZED_ENVIRONMENT=preview
|
ZED_ENVIRONMENT=preview
|
||||||
RUST_LOG=info
|
RUST_LOG=info
|
||||||
INVITE_LINK_PREFIX=https://zed.dev/invites/
|
INVITE_LINK_PREFIX=https://zed.dev/invites/
|
||||||
|
DATABASE_MAX_CONNECTIONS=10
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
ZED_ENVIRONMENT=production
|
ZED_ENVIRONMENT=production
|
||||||
RUST_LOG=info
|
RUST_LOG=info
|
||||||
INVITE_LINK_PREFIX=https://zed.dev/invites/
|
INVITE_LINK_PREFIX=https://zed.dev/invites/
|
||||||
|
DATABASE_MAX_CONNECTIONS=85
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
ZED_ENVIRONMENT=staging
|
ZED_ENVIRONMENT=staging
|
||||||
RUST_LOG=info
|
RUST_LOG=info
|
||||||
INVITE_LINK_PREFIX=https://staging.zed.dev/invites/
|
INVITE_LINK_PREFIX=https://staging.zed.dev/invites/
|
||||||
|
DATABASE_MAX_CONNECTIONS=5
|
||||||
|
|
|
@ -59,6 +59,13 @@ spec:
|
||||||
ports:
|
ports:
|
||||||
- containerPort: 8080
|
- containerPort: 8080
|
||||||
protocol: TCP
|
protocol: TCP
|
||||||
|
livenessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /healthz
|
||||||
|
port: 8080
|
||||||
|
initialDelaySeconds: 5
|
||||||
|
periodSeconds: 5
|
||||||
|
timeoutSeconds: 5
|
||||||
readinessProbe:
|
readinessProbe:
|
||||||
httpGet:
|
httpGet:
|
||||||
path: /
|
path: /
|
||||||
|
@ -73,6 +80,8 @@ spec:
|
||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
name: database
|
name: database
|
||||||
key: url
|
key: url
|
||||||
|
- name: DATABASE_MAX_CONNECTIONS
|
||||||
|
value: "${DATABASE_MAX_CONNECTIONS}"
|
||||||
- name: API_TOKEN
|
- name: API_TOKEN
|
||||||
valueFrom:
|
valueFrom:
|
||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
|
|
|
@ -1757,17 +1757,14 @@ impl Database {
|
||||||
.add(follower::Column::ProjectId.eq(project_id))
|
.add(follower::Column::ProjectId.eq(project_id))
|
||||||
.add(
|
.add(
|
||||||
follower::Column::LeaderConnectionServerId
|
follower::Column::LeaderConnectionServerId
|
||||||
.eq(leader_connection.owner_id)
|
.eq(leader_connection.owner_id),
|
||||||
.and(follower::Column::LeaderConnectionId.eq(leader_connection.id)),
|
|
||||||
)
|
)
|
||||||
|
.add(follower::Column::LeaderConnectionId.eq(leader_connection.id))
|
||||||
.add(
|
.add(
|
||||||
follower::Column::FollowerConnectionServerId
|
follower::Column::FollowerConnectionServerId
|
||||||
.eq(follower_connection.owner_id)
|
.eq(follower_connection.owner_id),
|
||||||
.and(
|
)
|
||||||
follower::Column::FollowerConnectionId
|
.add(follower::Column::FollowerConnectionId.eq(follower_connection.id)),
|
||||||
.eq(follower_connection.id),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
.exec(&*tx)
|
.exec(&*tx)
|
||||||
.await?;
|
.await?;
|
||||||
|
@ -2560,7 +2557,7 @@ impl Database {
|
||||||
&self,
|
&self,
|
||||||
project_id: ProjectId,
|
project_id: ProjectId,
|
||||||
connection: ConnectionId,
|
connection: ConnectionId,
|
||||||
) -> Result<RoomGuard<LeftProject>> {
|
) -> Result<RoomGuard<(proto::Room, LeftProject)>> {
|
||||||
let room_id = self.room_id_for_project(project_id).await?;
|
let room_id = self.room_id_for_project(project_id).await?;
|
||||||
self.room_transaction(room_id, |tx| async move {
|
self.room_transaction(room_id, |tx| async move {
|
||||||
let result = project_collaborator::Entity::delete_many()
|
let result = project_collaborator::Entity::delete_many()
|
||||||
|
@ -2592,13 +2589,39 @@ impl Database {
|
||||||
.map(|collaborator| collaborator.connection())
|
.map(|collaborator| collaborator.connection())
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
follower::Entity::delete_many()
|
||||||
|
.filter(
|
||||||
|
Condition::any()
|
||||||
|
.add(
|
||||||
|
Condition::all()
|
||||||
|
.add(follower::Column::ProjectId.eq(project_id))
|
||||||
|
.add(
|
||||||
|
follower::Column::LeaderConnectionServerId
|
||||||
|
.eq(connection.owner_id),
|
||||||
|
)
|
||||||
|
.add(follower::Column::LeaderConnectionId.eq(connection.id)),
|
||||||
|
)
|
||||||
|
.add(
|
||||||
|
Condition::all()
|
||||||
|
.add(follower::Column::ProjectId.eq(project_id))
|
||||||
|
.add(
|
||||||
|
follower::Column::FollowerConnectionServerId
|
||||||
|
.eq(connection.owner_id),
|
||||||
|
)
|
||||||
|
.add(follower::Column::FollowerConnectionId.eq(connection.id)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.exec(&*tx)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let room = self.get_room(project.room_id, &tx).await?;
|
||||||
let left_project = LeftProject {
|
let left_project = LeftProject {
|
||||||
id: project_id,
|
id: project_id,
|
||||||
host_user_id: project.host_user_id,
|
host_user_id: project.host_user_id,
|
||||||
host_connection_id: project.host_connection()?,
|
host_connection_id: project.host_connection()?,
|
||||||
connection_ids,
|
connection_ids,
|
||||||
};
|
};
|
||||||
Ok(left_project)
|
Ok((room, left_project))
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
|
@ -91,6 +91,7 @@ impl std::error::Error for Error {}
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
pub http_port: u16,
|
pub http_port: u16,
|
||||||
pub database_url: String,
|
pub database_url: String,
|
||||||
|
pub database_max_connections: u32,
|
||||||
pub api_token: String,
|
pub api_token: String,
|
||||||
pub invite_link_prefix: String,
|
pub invite_link_prefix: String,
|
||||||
pub live_kit_server: Option<String>,
|
pub live_kit_server: Option<String>,
|
||||||
|
@ -116,7 +117,7 @@ pub struct AppState {
|
||||||
impl AppState {
|
impl AppState {
|
||||||
pub async fn new(config: Config) -> Result<Arc<Self>> {
|
pub async fn new(config: Config) -> Result<Arc<Self>> {
|
||||||
let mut db_options = db::ConnectOptions::new(config.database_url.clone());
|
let mut db_options = db::ConnectOptions::new(config.database_url.clone());
|
||||||
db_options.max_connections(5);
|
db_options.max_connections(config.database_max_connections);
|
||||||
let db = Database::new(db_options).await?;
|
let db = Database::new(db_options).await?;
|
||||||
let live_kit_client = if let Some(((server, key), secret)) = config
|
let live_kit_client = if let Some(((server, key), secret)) = config
|
||||||
.live_kit_server
|
.live_kit_server
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
use axum::{routing::get, Router};
|
use axum::{routing::get, Extension, Router};
|
||||||
use collab::{db, env, executor::Executor, AppState, Config, MigrateConfig, Result};
|
use collab::{db, env, executor::Executor, AppState, Config, MigrateConfig, Result};
|
||||||
use db::Database;
|
use db::Database;
|
||||||
use std::{
|
use std::{
|
||||||
env::args,
|
env::args,
|
||||||
net::{SocketAddr, TcpListener},
|
net::{SocketAddr, TcpListener},
|
||||||
path::Path,
|
path::Path,
|
||||||
|
sync::Arc,
|
||||||
};
|
};
|
||||||
use tokio::signal::unix::SignalKind;
|
use tokio::signal::unix::SignalKind;
|
||||||
use tracing_log::LogTracer;
|
use tracing_log::LogTracer;
|
||||||
|
@ -66,7 +67,12 @@ async fn main() -> Result<()> {
|
||||||
|
|
||||||
let app = collab::api::routes(rpc_server.clone(), state.clone())
|
let app = collab::api::routes(rpc_server.clone(), state.clone())
|
||||||
.merge(collab::rpc::routes(rpc_server.clone()))
|
.merge(collab::rpc::routes(rpc_server.clone()))
|
||||||
.merge(Router::new().route("/", get(handle_root)));
|
.merge(
|
||||||
|
Router::new()
|
||||||
|
.route("/", get(handle_root))
|
||||||
|
.route("/healthz", get(handle_liveness_probe))
|
||||||
|
.layer(Extension(state.clone())),
|
||||||
|
);
|
||||||
|
|
||||||
axum::Server::from_tcp(listener)?
|
axum::Server::from_tcp(listener)?
|
||||||
.serve(app.into_make_service_with_connect_info::<SocketAddr>())
|
.serve(app.into_make_service_with_connect_info::<SocketAddr>())
|
||||||
|
@ -95,6 +101,11 @@ async fn handle_root() -> String {
|
||||||
format!("collab v{VERSION}")
|
format!("collab v{VERSION}")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn handle_liveness_probe(Extension(state): Extension<Arc<AppState>>) -> Result<String> {
|
||||||
|
state.db.get_all_users(0, 1).await?;
|
||||||
|
Ok("ok".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn init_tracing(config: &Config) -> Option<()> {
|
pub fn init_tracing(config: &Config) -> Option<()> {
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use tracing_subscriber::layer::SubscriberExt;
|
use tracing_subscriber::layer::SubscriberExt;
|
||||||
|
|
|
@ -53,11 +53,11 @@ use std::{
|
||||||
},
|
},
|
||||||
time::Duration,
|
time::Duration,
|
||||||
};
|
};
|
||||||
use tokio::sync::watch;
|
use tokio::sync::{watch, Semaphore};
|
||||||
use tower::ServiceBuilder;
|
use tower::ServiceBuilder;
|
||||||
use tracing::{info_span, instrument, Instrument};
|
use tracing::{info_span, instrument, Instrument};
|
||||||
|
|
||||||
pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(5);
|
pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30);
|
||||||
pub const CLEANUP_TIMEOUT: Duration = Duration::from_secs(10);
|
pub const CLEANUP_TIMEOUT: Duration = Duration::from_secs(10);
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
|
@ -542,8 +542,13 @@ impl Server {
|
||||||
// This arrangement ensures we will attempt to process earlier messages first, but fall
|
// This arrangement ensures we will attempt to process earlier messages first, but fall
|
||||||
// back to processing messages arrived later in the spirit of making progress.
|
// back to processing messages arrived later in the spirit of making progress.
|
||||||
let mut foreground_message_handlers = FuturesUnordered::new();
|
let mut foreground_message_handlers = FuturesUnordered::new();
|
||||||
|
let concurrent_handlers = Arc::new(Semaphore::new(256));
|
||||||
loop {
|
loop {
|
||||||
let next_message = incoming_rx.next().fuse();
|
let next_message = async {
|
||||||
|
let permit = concurrent_handlers.clone().acquire_owned().await.unwrap();
|
||||||
|
let message = incoming_rx.next().await;
|
||||||
|
(permit, message)
|
||||||
|
}.fuse();
|
||||||
futures::pin_mut!(next_message);
|
futures::pin_mut!(next_message);
|
||||||
futures::select_biased! {
|
futures::select_biased! {
|
||||||
_ = teardown.changed().fuse() => return Ok(()),
|
_ = teardown.changed().fuse() => return Ok(()),
|
||||||
|
@ -554,7 +559,8 @@ impl Server {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
_ = foreground_message_handlers.next() => {}
|
_ = foreground_message_handlers.next() => {}
|
||||||
message = next_message => {
|
next_message = next_message => {
|
||||||
|
let (permit, message) = next_message;
|
||||||
if let Some(message) = message {
|
if let Some(message) = message {
|
||||||
let type_name = message.payload_type_name();
|
let type_name = message.payload_type_name();
|
||||||
let span = tracing::info_span!("receive message", %user_id, %login, %connection_id, %address, type_name);
|
let span = tracing::info_span!("receive message", %user_id, %login, %connection_id, %address, type_name);
|
||||||
|
@ -564,7 +570,10 @@ impl Server {
|
||||||
let handle_message = (handler)(message, session.clone());
|
let handle_message = (handler)(message, session.clone());
|
||||||
drop(span_enter);
|
drop(span_enter);
|
||||||
|
|
||||||
let handle_message = handle_message.instrument(span);
|
let handle_message = async move {
|
||||||
|
handle_message.await;
|
||||||
|
drop(permit);
|
||||||
|
}.instrument(span);
|
||||||
if is_background {
|
if is_background {
|
||||||
executor.spawn_detached(handle_message);
|
executor.spawn_detached(handle_message);
|
||||||
} else {
|
} else {
|
||||||
|
@ -1408,7 +1417,7 @@ async fn leave_project(request: proto::LeaveProject, session: Session) -> Result
|
||||||
let sender_id = session.connection_id;
|
let sender_id = session.connection_id;
|
||||||
let project_id = ProjectId::from_proto(request.project_id);
|
let project_id = ProjectId::from_proto(request.project_id);
|
||||||
|
|
||||||
let project = session
|
let (room, project) = &*session
|
||||||
.db()
|
.db()
|
||||||
.await
|
.await
|
||||||
.leave_project(project_id, sender_id)
|
.leave_project(project_id, sender_id)
|
||||||
|
@ -1419,7 +1428,9 @@ async fn leave_project(request: proto::LeaveProject, session: Session) -> Result
|
||||||
host_connection_id = %project.host_connection_id,
|
host_connection_id = %project.host_connection_id,
|
||||||
"leave project"
|
"leave project"
|
||||||
);
|
);
|
||||||
|
|
||||||
project_left(&project, &session);
|
project_left(&project, &session);
|
||||||
|
room_updated(&room, &session.peer);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -197,7 +197,8 @@ impl TestServer {
|
||||||
fs: fs.clone(),
|
fs: fs.clone(),
|
||||||
build_window_options: |_, _, _| Default::default(),
|
build_window_options: |_, _, _| Default::default(),
|
||||||
initialize_workspace: |_, _, _| unimplemented!(),
|
initialize_workspace: |_, _, _| unimplemented!(),
|
||||||
dock_default_item_factory: |_, _| unimplemented!(),
|
dock_default_item_factory: |_, _| None,
|
||||||
|
background_actions: || &[],
|
||||||
});
|
});
|
||||||
|
|
||||||
Project::init(&client);
|
Project::init(&client);
|
||||||
|
@ -434,15 +435,7 @@ impl TestClient {
|
||||||
cx: &mut TestAppContext,
|
cx: &mut TestAppContext,
|
||||||
) -> ViewHandle<Workspace> {
|
) -> ViewHandle<Workspace> {
|
||||||
let (_, root_view) = cx.add_window(|_| EmptyView);
|
let (_, root_view) = cx.add_window(|_| EmptyView);
|
||||||
cx.add_view(&root_view, |cx| {
|
cx.add_view(&root_view, |cx| Workspace::test_new(project.clone(), cx))
|
||||||
Workspace::new(
|
|
||||||
Default::default(),
|
|
||||||
0,
|
|
||||||
project.clone(),
|
|
||||||
|_, _| unimplemented!(),
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_new_root_dir(&mut self) -> PathBuf {
|
fn create_new_root_dir(&mut self) -> PathBuf {
|
||||||
|
|
|
@ -829,7 +829,7 @@ async fn test_server_restarts(
|
||||||
|
|
||||||
// Users A and B reconnect to the call. User C has troubles reconnecting, so it leaves the room.
|
// Users A and B reconnect to the call. User C has troubles reconnecting, so it leaves the room.
|
||||||
client_c.override_establish_connection(|_, cx| cx.spawn(|_| future::pending()));
|
client_c.override_establish_connection(|_, cx| cx.spawn(|_| future::pending()));
|
||||||
deterministic.advance_clock(RECEIVE_TIMEOUT);
|
deterministic.advance_clock(RECONNECT_TIMEOUT);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
room_participants(&room_a, cx_a),
|
room_participants(&room_a, cx_a),
|
||||||
RoomParticipants {
|
RoomParticipants {
|
||||||
|
@ -1001,7 +1001,7 @@ async fn test_server_restarts(
|
||||||
client_a.override_establish_connection(|_, cx| cx.spawn(|_| future::pending()));
|
client_a.override_establish_connection(|_, cx| cx.spawn(|_| future::pending()));
|
||||||
client_b.override_establish_connection(|_, cx| cx.spawn(|_| future::pending()));
|
client_b.override_establish_connection(|_, cx| cx.spawn(|_| future::pending()));
|
||||||
client_c.override_establish_connection(|_, cx| cx.spawn(|_| future::pending()));
|
client_c.override_establish_connection(|_, cx| cx.spawn(|_| future::pending()));
|
||||||
deterministic.advance_clock(RECEIVE_TIMEOUT);
|
deterministic.advance_clock(RECONNECT_TIMEOUT);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
room_participants(&room_a, cx_a),
|
room_participants(&room_a, cx_a),
|
||||||
RoomParticipants {
|
RoomParticipants {
|
||||||
|
@ -1449,15 +1449,7 @@ async fn test_host_disconnect(
|
||||||
deterministic.run_until_parked();
|
deterministic.run_until_parked();
|
||||||
assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared()));
|
assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared()));
|
||||||
|
|
||||||
let (_, workspace_b) = cx_b.add_window(|cx| {
|
let (_, workspace_b) = cx_b.add_window(|cx| Workspace::test_new(project_b.clone(), cx));
|
||||||
Workspace::new(
|
|
||||||
Default::default(),
|
|
||||||
0,
|
|
||||||
project_b.clone(),
|
|
||||||
|_, _| unimplemented!(),
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
});
|
|
||||||
let editor_b = workspace_b
|
let editor_b = workspace_b
|
||||||
.update(cx_b, |workspace, cx| {
|
.update(cx_b, |workspace, cx| {
|
||||||
workspace.open_path((worktree_id, "b.txt"), None, true, cx)
|
workspace.open_path((worktree_id, "b.txt"), None, true, cx)
|
||||||
|
@ -4706,15 +4698,7 @@ async fn test_collaborating_with_code_actions(
|
||||||
|
|
||||||
// Join the project as client B.
|
// Join the project as client B.
|
||||||
let project_b = client_b.build_remote_project(project_id, cx_b).await;
|
let project_b = client_b.build_remote_project(project_id, cx_b).await;
|
||||||
let (_window_b, workspace_b) = cx_b.add_window(|cx| {
|
let (_window_b, workspace_b) = cx_b.add_window(|cx| Workspace::test_new(project_b.clone(), cx));
|
||||||
Workspace::new(
|
|
||||||
Default::default(),
|
|
||||||
0,
|
|
||||||
project_b.clone(),
|
|
||||||
|_, _| unimplemented!(),
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
});
|
|
||||||
let editor_b = workspace_b
|
let editor_b = workspace_b
|
||||||
.update(cx_b, |workspace, cx| {
|
.update(cx_b, |workspace, cx| {
|
||||||
workspace.open_path((worktree_id, "main.rs"), None, true, cx)
|
workspace.open_path((worktree_id, "main.rs"), None, true, cx)
|
||||||
|
@ -4937,15 +4921,7 @@ async fn test_collaborating_with_renames(
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let project_b = client_b.build_remote_project(project_id, cx_b).await;
|
let project_b = client_b.build_remote_project(project_id, cx_b).await;
|
||||||
|
|
||||||
let (_window_b, workspace_b) = cx_b.add_window(|cx| {
|
let (_window_b, workspace_b) = cx_b.add_window(|cx| Workspace::test_new(project_b.clone(), cx));
|
||||||
Workspace::new(
|
|
||||||
Default::default(),
|
|
||||||
0,
|
|
||||||
project_b.clone(),
|
|
||||||
|_, _| unimplemented!(),
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
});
|
|
||||||
let editor_b = workspace_b
|
let editor_b = workspace_b
|
||||||
.update(cx_b, |workspace, cx| {
|
.update(cx_b, |workspace, cx| {
|
||||||
workspace.open_path((worktree_id, "one.rs"), None, true, cx)
|
workspace.open_path((worktree_id, "one.rs"), None, true, cx)
|
||||||
|
@ -5792,11 +5768,12 @@ async fn test_contact_requests(
|
||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test(iterations = 10)]
|
#[gpui::test(iterations = 10)]
|
||||||
async fn test_following(
|
async fn test_basic_following(
|
||||||
deterministic: Arc<Deterministic>,
|
deterministic: Arc<Deterministic>,
|
||||||
cx_a: &mut TestAppContext,
|
cx_a: &mut TestAppContext,
|
||||||
cx_b: &mut TestAppContext,
|
cx_b: &mut TestAppContext,
|
||||||
cx_c: &mut TestAppContext,
|
cx_c: &mut TestAppContext,
|
||||||
|
cx_d: &mut TestAppContext,
|
||||||
) {
|
) {
|
||||||
deterministic.forbid_parking();
|
deterministic.forbid_parking();
|
||||||
cx_a.update(editor::init);
|
cx_a.update(editor::init);
|
||||||
|
@ -5806,11 +5783,14 @@ async fn test_following(
|
||||||
let client_a = server.create_client(cx_a, "user_a").await;
|
let client_a = server.create_client(cx_a, "user_a").await;
|
||||||
let client_b = server.create_client(cx_b, "user_b").await;
|
let client_b = server.create_client(cx_b, "user_b").await;
|
||||||
let client_c = server.create_client(cx_c, "user_c").await;
|
let client_c = server.create_client(cx_c, "user_c").await;
|
||||||
|
let client_d = server.create_client(cx_d, "user_d").await;
|
||||||
server
|
server
|
||||||
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
|
.create_room(&mut [
|
||||||
.await;
|
(&client_a, cx_a),
|
||||||
server
|
(&client_b, cx_b),
|
||||||
.make_contacts(&mut [(&client_a, cx_a), (&client_c, cx_c)])
|
(&client_c, cx_c),
|
||||||
|
(&client_d, cx_d),
|
||||||
|
])
|
||||||
.await;
|
.await;
|
||||||
let active_call_a = cx_a.read(ActiveCall::global);
|
let active_call_a = cx_a.read(ActiveCall::global);
|
||||||
let active_call_b = cx_b.read(ActiveCall::global);
|
let active_call_b = cx_b.read(ActiveCall::global);
|
||||||
|
@ -5877,6 +5857,7 @@ async fn test_following(
|
||||||
let peer_id_a = client_a.peer_id().unwrap();
|
let peer_id_a = client_a.peer_id().unwrap();
|
||||||
let peer_id_b = client_b.peer_id().unwrap();
|
let peer_id_b = client_b.peer_id().unwrap();
|
||||||
let peer_id_c = client_c.peer_id().unwrap();
|
let peer_id_c = client_c.peer_id().unwrap();
|
||||||
|
let peer_id_d = client_d.peer_id().unwrap();
|
||||||
|
|
||||||
// Client A updates their selections in those editors
|
// Client A updates their selections in those editors
|
||||||
editor_a1.update(cx_a, |editor, cx| {
|
editor_a1.update(cx_a, |editor, cx| {
|
||||||
|
@ -5896,25 +5877,15 @@ async fn test_following(
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
// Client A invites client C to the call.
|
|
||||||
active_call_a
|
|
||||||
.update(cx_a, |call, cx| {
|
|
||||||
call.invite(client_c.current_user_id(cx_c).to_proto(), None, cx)
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
cx_c.foreground().run_until_parked();
|
cx_c.foreground().run_until_parked();
|
||||||
let active_call_c = cx_c.read(ActiveCall::global);
|
let active_call_c = cx_c.read(ActiveCall::global);
|
||||||
active_call_c
|
|
||||||
.update(cx_c, |call, cx| call.accept_incoming(cx))
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
let project_c = client_c.build_remote_project(project_id, cx_c).await;
|
let project_c = client_c.build_remote_project(project_id, cx_c).await;
|
||||||
let workspace_c = client_c.build_workspace(&project_c, cx_c);
|
let workspace_c = client_c.build_workspace(&project_c, cx_c);
|
||||||
active_call_c
|
active_call_c
|
||||||
.update(cx_c, |call, cx| call.set_location(Some(&project_c), cx))
|
.update(cx_c, |call, cx| call.set_location(Some(&project_c), cx))
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
drop(project_c);
|
||||||
|
|
||||||
// Client C also follows client A.
|
// Client C also follows client A.
|
||||||
workspace_c
|
workspace_c
|
||||||
|
@ -5926,12 +5897,23 @@ async fn test_following(
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
cx_d.foreground().run_until_parked();
|
||||||
|
let active_call_d = cx_d.read(ActiveCall::global);
|
||||||
|
let project_d = client_d.build_remote_project(project_id, cx_d).await;
|
||||||
|
let workspace_d = client_d.build_workspace(&project_d, cx_d);
|
||||||
|
active_call_d
|
||||||
|
.update(cx_d, |call, cx| call.set_location(Some(&project_d), cx))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
drop(project_d);
|
||||||
|
|
||||||
// All clients see that clients B and C are following client A.
|
// All clients see that clients B and C are following client A.
|
||||||
cx_c.foreground().run_until_parked();
|
cx_c.foreground().run_until_parked();
|
||||||
for (name, active_call, cx) in [
|
for (name, active_call, cx) in [
|
||||||
("A", &active_call_a, &cx_a),
|
("A", &active_call_a, &cx_a),
|
||||||
("B", &active_call_b, &cx_b),
|
("B", &active_call_b, &cx_b),
|
||||||
("C", &active_call_c, &cx_c),
|
("C", &active_call_c, &cx_c),
|
||||||
|
("D", &active_call_d, &cx_d),
|
||||||
] {
|
] {
|
||||||
active_call.read_with(*cx, |call, cx| {
|
active_call.read_with(*cx, |call, cx| {
|
||||||
let room = call.room().unwrap().read(cx);
|
let room = call.room().unwrap().read(cx);
|
||||||
|
@ -5954,6 +5936,7 @@ async fn test_following(
|
||||||
("A", &active_call_a, &cx_a),
|
("A", &active_call_a, &cx_a),
|
||||||
("B", &active_call_b, &cx_b),
|
("B", &active_call_b, &cx_b),
|
||||||
("C", &active_call_c, &cx_c),
|
("C", &active_call_c, &cx_c),
|
||||||
|
("D", &active_call_d, &cx_d),
|
||||||
] {
|
] {
|
||||||
active_call.read_with(*cx, |call, cx| {
|
active_call.read_with(*cx, |call, cx| {
|
||||||
let room = call.room().unwrap().read(cx);
|
let room = call.room().unwrap().read(cx);
|
||||||
|
@ -5965,6 +5948,90 @@ async fn test_following(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Client C re-follows client A.
|
||||||
|
workspace_c.update(cx_c, |workspace, cx| {
|
||||||
|
workspace.toggle_follow(&ToggleFollow(peer_id_a), cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
// All clients see that clients B and C are following client A.
|
||||||
|
cx_c.foreground().run_until_parked();
|
||||||
|
for (name, active_call, cx) in [
|
||||||
|
("A", &active_call_a, &cx_a),
|
||||||
|
("B", &active_call_b, &cx_b),
|
||||||
|
("C", &active_call_c, &cx_c),
|
||||||
|
("D", &active_call_d, &cx_d),
|
||||||
|
] {
|
||||||
|
active_call.read_with(*cx, |call, cx| {
|
||||||
|
let room = call.room().unwrap().read(cx);
|
||||||
|
assert_eq!(
|
||||||
|
room.followers_for(peer_id_a, project_id),
|
||||||
|
&[peer_id_b, peer_id_c],
|
||||||
|
"checking followers for A as {name}"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Client D follows client C.
|
||||||
|
workspace_d
|
||||||
|
.update(cx_d, |workspace, cx| {
|
||||||
|
workspace
|
||||||
|
.toggle_follow(&ToggleFollow(peer_id_c), cx)
|
||||||
|
.unwrap()
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// All clients see that D is following C
|
||||||
|
cx_d.foreground().run_until_parked();
|
||||||
|
for (name, active_call, cx) in [
|
||||||
|
("A", &active_call_a, &cx_a),
|
||||||
|
("B", &active_call_b, &cx_b),
|
||||||
|
("C", &active_call_c, &cx_c),
|
||||||
|
("D", &active_call_d, &cx_d),
|
||||||
|
] {
|
||||||
|
active_call.read_with(*cx, |call, cx| {
|
||||||
|
let room = call.room().unwrap().read(cx);
|
||||||
|
assert_eq!(
|
||||||
|
room.followers_for(peer_id_c, project_id),
|
||||||
|
&[peer_id_d],
|
||||||
|
"checking followers for C as {name}"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Client C closes the project.
|
||||||
|
cx_c.drop_last(workspace_c);
|
||||||
|
|
||||||
|
// Clients A and B see that client B is following A, and client C is not present in the followers.
|
||||||
|
cx_c.foreground().run_until_parked();
|
||||||
|
for (name, active_call, cx) in [("A", &active_call_a, &cx_a), ("B", &active_call_b, &cx_b)] {
|
||||||
|
active_call.read_with(*cx, |call, cx| {
|
||||||
|
let room = call.room().unwrap().read(cx);
|
||||||
|
assert_eq!(
|
||||||
|
room.followers_for(peer_id_a, project_id),
|
||||||
|
&[peer_id_b],
|
||||||
|
"checking followers for A as {name}"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// All clients see that no-one is following C
|
||||||
|
for (name, active_call, cx) in [
|
||||||
|
("A", &active_call_a, &cx_a),
|
||||||
|
("B", &active_call_b, &cx_b),
|
||||||
|
("C", &active_call_c, &cx_c),
|
||||||
|
("D", &active_call_d, &cx_d),
|
||||||
|
] {
|
||||||
|
active_call.read_with(*cx, |call, cx| {
|
||||||
|
let room = call.room().unwrap().read(cx);
|
||||||
|
assert_eq!(
|
||||||
|
room.followers_for(peer_id_c, project_id),
|
||||||
|
&[],
|
||||||
|
"checking followers for C as {name}"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| {
|
let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| {
|
||||||
workspace
|
workspace
|
||||||
.active_item(cx)
|
.active_item(cx)
|
||||||
|
|
|
@ -29,6 +29,7 @@ clock = { path = "../clock" }
|
||||||
collections = { path = "../collections" }
|
collections = { path = "../collections" }
|
||||||
context_menu = { path = "../context_menu" }
|
context_menu = { path = "../context_menu" }
|
||||||
editor = { path = "../editor" }
|
editor = { path = "../editor" }
|
||||||
|
feedback = { path = "../feedback" }
|
||||||
fuzzy = { path = "../fuzzy" }
|
fuzzy = { path = "../fuzzy" }
|
||||||
gpui = { path = "../gpui" }
|
gpui = { path = "../gpui" }
|
||||||
menu = { path = "../menu" }
|
menu = { path = "../menu" }
|
||||||
|
|
|
@ -304,12 +304,22 @@ impl CollabTitlebarItem {
|
||||||
label: "Sign out".into(),
|
label: "Sign out".into(),
|
||||||
action: Box::new(SignOut),
|
action: Box::new(SignOut),
|
||||||
},
|
},
|
||||||
|
ContextMenuItem::Item {
|
||||||
|
label: "Give Feedback".into(),
|
||||||
|
action: Box::new(feedback::feedback_editor::GiveFeedback),
|
||||||
|
},
|
||||||
]
|
]
|
||||||
} else {
|
} else {
|
||||||
vec![ContextMenuItem::Item {
|
vec![
|
||||||
label: "Sign in".into(),
|
ContextMenuItem::Item {
|
||||||
action: Box::new(Authenticate),
|
label: "Sign in".into(),
|
||||||
}]
|
action: Box::new(Authenticate),
|
||||||
|
},
|
||||||
|
ContextMenuItem::Item {
|
||||||
|
label: "Give Feedback".into(),
|
||||||
|
action: Box::new(feedback::feedback_editor::GiveFeedback),
|
||||||
|
},
|
||||||
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
user_menu.show(
|
user_menu.show(
|
||||||
|
@ -572,15 +582,13 @@ impl CollabTitlebarItem {
|
||||||
room: &ModelHandle<Room>,
|
room: &ModelHandle<Room>,
|
||||||
cx: &mut RenderContext<Self>,
|
cx: &mut RenderContext<Self>,
|
||||||
) -> Vec<ElementBox> {
|
) -> Vec<ElementBox> {
|
||||||
let project = workspace.read(cx).project().read(cx);
|
|
||||||
|
|
||||||
let mut participants = room
|
let mut participants = room
|
||||||
.read(cx)
|
.read(cx)
|
||||||
.remote_participants()
|
.remote_participants()
|
||||||
.values()
|
.values()
|
||||||
.cloned()
|
.cloned()
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
participants.sort_by_key(|p| Some(project.collaborators().get(&p.peer_id)?.replica_id));
|
participants.sort_by_cached_key(|p| p.user.github_login.clone());
|
||||||
|
|
||||||
participants
|
participants
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
@ -823,7 +831,7 @@ impl CollabTitlebarItem {
|
||||||
avatar_style: AvatarStyle,
|
avatar_style: AvatarStyle,
|
||||||
background_color: Color,
|
background_color: Color,
|
||||||
) -> ElementBox {
|
) -> ElementBox {
|
||||||
Image::new(avatar)
|
Image::from_data(avatar)
|
||||||
.with_style(avatar_style.image)
|
.with_style(avatar_style.image)
|
||||||
.aligned()
|
.aligned()
|
||||||
.contained()
|
.contained()
|
||||||
|
|
|
@ -86,6 +86,7 @@ fn join_project(action: &JoinProject, app_state: Arc<AppState>, cx: &mut Mutable
|
||||||
0,
|
0,
|
||||||
project,
|
project,
|
||||||
app_state.dock_default_item_factory,
|
app_state.dock_default_item_factory,
|
||||||
|
app_state.background_actions,
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
(app_state.initialize_workspace)(&mut workspace, &app_state, cx);
|
(app_state.initialize_workspace)(&mut workspace, &app_state, cx);
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use client::{ContactRequestStatus, User, UserStore};
|
use client::{ContactRequestStatus, User, UserStore};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
elements::*, AnyViewHandle, Entity, ModelHandle, MouseState, MutableAppContext, RenderContext,
|
elements::*, AnyViewHandle, AppContext, Entity, ModelHandle, MouseState, MutableAppContext,
|
||||||
Task, View, ViewContext, ViewHandle,
|
RenderContext, Task, View, ViewContext, ViewHandle,
|
||||||
};
|
};
|
||||||
use picker::{Picker, PickerDelegate};
|
use picker::{Picker, PickerDelegate};
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
|
@ -68,7 +68,7 @@ impl PickerDelegate for ContactFinder {
|
||||||
this.potential_contacts = potential_contacts.into();
|
this.potential_contacts = potential_contacts.into();
|
||||||
cx.notify();
|
cx.notify();
|
||||||
});
|
});
|
||||||
Ok(())
|
anyhow::Ok(())
|
||||||
}
|
}
|
||||||
.log_err()
|
.log_err()
|
||||||
.await;
|
.await;
|
||||||
|
@ -128,7 +128,7 @@ impl PickerDelegate for ContactFinder {
|
||||||
.style_for(mouse_state, selected);
|
.style_for(mouse_state, selected);
|
||||||
Flex::row()
|
Flex::row()
|
||||||
.with_children(user.avatar.clone().map(|avatar| {
|
.with_children(user.avatar.clone().map(|avatar| {
|
||||||
Image::new(avatar)
|
Image::from_data(avatar)
|
||||||
.with_style(theme.contact_finder.contact_avatar)
|
.with_style(theme.contact_finder.contact_avatar)
|
||||||
.aligned()
|
.aligned()
|
||||||
.left()
|
.left()
|
||||||
|
@ -178,4 +178,14 @@ impl ContactFinder {
|
||||||
selected_index: 0,
|
selected_index: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn editor_text(&self, cx: &AppContext) -> String {
|
||||||
|
self.picker.read(cx).query(cx)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_editor_text(self, editor_text: String, cx: &mut ViewContext<Self>) -> Self {
|
||||||
|
self.picker
|
||||||
|
.update(cx, |picker, cx| picker.set_query(editor_text, cx));
|
||||||
|
self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -294,6 +294,16 @@ impl ContactList {
|
||||||
this
|
this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn editor_text(&self, cx: &AppContext) -> String {
|
||||||
|
self.filter_editor.read(cx).text(cx)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_editor_text(self, editor_text: String, cx: &mut ViewContext<Self>) -> Self {
|
||||||
|
self.filter_editor
|
||||||
|
.update(cx, |picker, cx| picker.set_text(editor_text, cx));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
fn remove_contact(&mut self, request: &RemoveContact, cx: &mut ViewContext<Self>) {
|
fn remove_contact(&mut self, request: &RemoveContact, cx: &mut ViewContext<Self>) {
|
||||||
let user_id = request.0;
|
let user_id = request.0;
|
||||||
let user_store = self.user_store.clone();
|
let user_store = self.user_store.clone();
|
||||||
|
@ -726,7 +736,7 @@ impl ContactList {
|
||||||
) -> ElementBox {
|
) -> ElementBox {
|
||||||
Flex::row()
|
Flex::row()
|
||||||
.with_children(user.avatar.clone().map(|avatar| {
|
.with_children(user.avatar.clone().map(|avatar| {
|
||||||
Image::new(avatar)
|
Image::from_data(avatar)
|
||||||
.with_style(theme.contact_avatar)
|
.with_style(theme.contact_avatar)
|
||||||
.aligned()
|
.aligned()
|
||||||
.left()
|
.left()
|
||||||
|
@ -1080,7 +1090,7 @@ impl ContactList {
|
||||||
};
|
};
|
||||||
Stack::new()
|
Stack::new()
|
||||||
.with_child(
|
.with_child(
|
||||||
Image::new(avatar)
|
Image::from_data(avatar)
|
||||||
.with_style(theme.contact_avatar)
|
.with_style(theme.contact_avatar)
|
||||||
.aligned()
|
.aligned()
|
||||||
.left()
|
.left()
|
||||||
|
@ -1173,7 +1183,7 @@ impl ContactList {
|
||||||
|
|
||||||
let mut row = Flex::row()
|
let mut row = Flex::row()
|
||||||
.with_children(user.avatar.clone().map(|avatar| {
|
.with_children(user.avatar.clone().map(|avatar| {
|
||||||
Image::new(avatar)
|
Image::from_data(avatar)
|
||||||
.with_style(theme.contact_avatar)
|
.with_style(theme.contact_avatar)
|
||||||
.aligned()
|
.aligned()
|
||||||
.left()
|
.left()
|
||||||
|
|
|
@ -43,19 +43,23 @@ impl ContactsPopover {
|
||||||
user_store,
|
user_store,
|
||||||
_subscription: None,
|
_subscription: None,
|
||||||
};
|
};
|
||||||
this.show_contact_list(cx);
|
this.show_contact_list(String::new(), cx);
|
||||||
this
|
this
|
||||||
}
|
}
|
||||||
|
|
||||||
fn toggle_contact_finder(&mut self, _: &ToggleContactFinder, cx: &mut ViewContext<Self>) {
|
fn toggle_contact_finder(&mut self, _: &ToggleContactFinder, cx: &mut ViewContext<Self>) {
|
||||||
match &self.child {
|
match &self.child {
|
||||||
Child::ContactList(_) => self.show_contact_finder(cx),
|
Child::ContactList(list) => self.show_contact_finder(list.read(cx).editor_text(cx), cx),
|
||||||
Child::ContactFinder(_) => self.show_contact_list(cx),
|
Child::ContactFinder(finder) => {
|
||||||
|
self.show_contact_list(finder.read(cx).editor_text(cx), cx)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn show_contact_finder(&mut self, cx: &mut ViewContext<ContactsPopover>) {
|
fn show_contact_finder(&mut self, editor_text: String, cx: &mut ViewContext<ContactsPopover>) {
|
||||||
let child = cx.add_view(|cx| ContactFinder::new(self.user_store.clone(), cx));
|
let child = cx.add_view(|cx| {
|
||||||
|
ContactFinder::new(self.user_store.clone(), cx).with_editor_text(editor_text, cx)
|
||||||
|
});
|
||||||
cx.focus(&child);
|
cx.focus(&child);
|
||||||
self._subscription = Some(cx.subscribe(&child, |_, _, event, cx| match event {
|
self._subscription = Some(cx.subscribe(&child, |_, _, event, cx| match event {
|
||||||
crate::contact_finder::Event::Dismissed => cx.emit(Event::Dismissed),
|
crate::contact_finder::Event::Dismissed => cx.emit(Event::Dismissed),
|
||||||
|
@ -64,9 +68,11 @@ impl ContactsPopover {
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn show_contact_list(&mut self, cx: &mut ViewContext<ContactsPopover>) {
|
fn show_contact_list(&mut self, editor_text: String, cx: &mut ViewContext<ContactsPopover>) {
|
||||||
let child =
|
let child = cx.add_view(|cx| {
|
||||||
cx.add_view(|cx| ContactList::new(self.project.clone(), self.user_store.clone(), cx));
|
ContactList::new(self.project.clone(), self.user_store.clone(), cx)
|
||||||
|
.with_editor_text(editor_text, cx)
|
||||||
|
});
|
||||||
cx.focus(&child);
|
cx.focus(&child);
|
||||||
self._subscription = Some(cx.subscribe(&child, |_, _, event, cx| match event {
|
self._subscription = Some(cx.subscribe(&child, |_, _, event, cx| match event {
|
||||||
crate::contact_list::Event::Dismissed => cx.emit(Event::Dismissed),
|
crate::contact_list::Event::Dismissed => cx.emit(Event::Dismissed),
|
||||||
|
|
|
@ -108,7 +108,7 @@ impl IncomingCallNotification {
|
||||||
.unwrap_or(&default_project);
|
.unwrap_or(&default_project);
|
||||||
Flex::row()
|
Flex::row()
|
||||||
.with_children(self.call.calling_user.avatar.clone().map(|avatar| {
|
.with_children(self.call.calling_user.avatar.clone().map(|avatar| {
|
||||||
Image::new(avatar)
|
Image::from_data(avatar)
|
||||||
.with_style(theme.caller_avatar)
|
.with_style(theme.caller_avatar)
|
||||||
.aligned()
|
.aligned()
|
||||||
.boxed()
|
.boxed()
|
||||||
|
|
|
@ -24,7 +24,7 @@ pub fn render_user_notification<V: View, A: Action + Clone>(
|
||||||
.with_child(
|
.with_child(
|
||||||
Flex::row()
|
Flex::row()
|
||||||
.with_children(user.avatar.clone().map(|avatar| {
|
.with_children(user.avatar.clone().map(|avatar| {
|
||||||
Image::new(avatar)
|
Image::from_data(avatar)
|
||||||
.with_style(theme.header_avatar)
|
.with_style(theme.header_avatar)
|
||||||
.aligned()
|
.aligned()
|
||||||
.constrained()
|
.constrained()
|
||||||
|
|
|
@ -108,7 +108,7 @@ impl ProjectSharedNotification {
|
||||||
let theme = &cx.global::<Settings>().theme.project_shared_notification;
|
let theme = &cx.global::<Settings>().theme.project_shared_notification;
|
||||||
Flex::row()
|
Flex::row()
|
||||||
.with_children(self.owner.avatar.clone().map(|avatar| {
|
.with_children(self.owner.avatar.clone().map(|avatar| {
|
||||||
Image::new(avatar)
|
Image::from_data(avatar)
|
||||||
.with_style(theme.owner_avatar)
|
.with_style(theme.owner_avatar)
|
||||||
.aligned()
|
.aligned()
|
||||||
.boxed()
|
.boxed()
|
||||||
|
|
|
@ -352,9 +352,7 @@ mod tests {
|
||||||
});
|
});
|
||||||
|
|
||||||
let project = Project::test(app_state.fs.clone(), [], cx).await;
|
let project = Project::test(app_state.fs.clone(), [], cx).await;
|
||||||
let (_, workspace) = cx.add_window(|cx| {
|
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
|
||||||
Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
|
|
||||||
});
|
|
||||||
let editor = cx.add_view(&workspace, |cx| {
|
let editor = cx.add_view(&workspace, |cx| {
|
||||||
let mut editor = Editor::single_line(None, cx);
|
let mut editor = Editor::single_line(None, cx);
|
||||||
editor.set_text("abc", cx);
|
editor.set_text("abc", cx);
|
||||||
|
|
|
@ -4,6 +4,7 @@ pub mod query;
|
||||||
// Re-export
|
// Re-export
|
||||||
pub use anyhow;
|
pub use anyhow;
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
|
use gpui::MutableAppContext;
|
||||||
pub use indoc::indoc;
|
pub use indoc::indoc;
|
||||||
pub use lazy_static;
|
pub use lazy_static;
|
||||||
use parking_lot::{Mutex, RwLock};
|
use parking_lot::{Mutex, RwLock};
|
||||||
|
@ -17,6 +18,7 @@ use sqlez::domain::Migrator;
|
||||||
use sqlez::thread_safe_connection::ThreadSafeConnection;
|
use sqlez::thread_safe_connection::ThreadSafeConnection;
|
||||||
use sqlez_macros::sql;
|
use sqlez_macros::sql;
|
||||||
use std::fs::create_dir_all;
|
use std::fs::create_dir_all;
|
||||||
|
use std::future::Future;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
@ -39,6 +41,7 @@ const FALLBACK_DB_NAME: &'static str = "FALLBACK_MEMORY_DB";
|
||||||
const DB_FILE_NAME: &'static str = "db.sqlite";
|
const DB_FILE_NAME: &'static str = "db.sqlite";
|
||||||
|
|
||||||
lazy_static::lazy_static! {
|
lazy_static::lazy_static! {
|
||||||
|
// !!!!!!! CHANGE BACK TO DEFAULT FALSE BEFORE SHIPPING
|
||||||
static ref ZED_STATELESS: bool = std::env::var("ZED_STATELESS").map_or(false, |v| !v.is_empty());
|
static ref ZED_STATELESS: bool = std::env::var("ZED_STATELESS").map_or(false, |v| !v.is_empty());
|
||||||
static ref DB_FILE_OPERATIONS: Mutex<()> = Mutex::new(());
|
static ref DB_FILE_OPERATIONS: Mutex<()> = Mutex::new(());
|
||||||
pub static ref BACKUP_DB_PATH: RwLock<Option<PathBuf>> = RwLock::new(None);
|
pub static ref BACKUP_DB_PATH: RwLock<Option<PathBuf>> = RwLock::new(None);
|
||||||
|
@ -63,11 +66,11 @@ pub async fn open_db<M: Migrator + 'static>(
|
||||||
let connection = async_iife!({
|
let connection = async_iife!({
|
||||||
// Note: This still has a race condition where 1 set of migrations succeeds
|
// Note: This still has a race condition where 1 set of migrations succeeds
|
||||||
// (e.g. (Workspace, Editor)) and another fails (e.g. (Workspace, Terminal))
|
// (e.g. (Workspace, Editor)) and another fails (e.g. (Workspace, Terminal))
|
||||||
// This will cause the first connection to have the database taken out
|
// This will cause the first connection to have the database taken out
|
||||||
// from under it. This *should* be fine though. The second dabatase failure will
|
// from under it. This *should* be fine though. The second dabatase failure will
|
||||||
// cause errors in the log and so should be observed by developers while writing
|
// cause errors in the log and so should be observed by developers while writing
|
||||||
// soon-to-be good migrations. If user databases are corrupted, we toss them out
|
// soon-to-be good migrations. If user databases are corrupted, we toss them out
|
||||||
// and try again from a blank. As long as running all migrations from start to end
|
// and try again from a blank. As long as running all migrations from start to end
|
||||||
// on a blank database is ok, this race condition will never be triggered.
|
// on a blank database is ok, this race condition will never be triggered.
|
||||||
//
|
//
|
||||||
// Basically: Don't ever push invalid migrations to stable or everyone will have
|
// Basically: Don't ever push invalid migrations to stable or everyone will have
|
||||||
|
@ -85,7 +88,7 @@ pub async fn open_db<M: Migrator + 'static>(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Take a lock in the failure case so that we move the db once per process instead
|
// Take a lock in the failure case so that we move the db once per process instead
|
||||||
// of potentially multiple times from different threads. This shouldn't happen in the
|
// of potentially multiple times from different threads. This shouldn't happen in the
|
||||||
// normal path
|
// normal path
|
||||||
let _lock = DB_FILE_OPERATIONS.lock();
|
let _lock = DB_FILE_OPERATIONS.lock();
|
||||||
|
@ -236,6 +239,15 @@ macro_rules! define_connection {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn write_and_log<F>(cx: &mut MutableAppContext, db_write: impl FnOnce() -> F + Send + 'static)
|
||||||
|
where
|
||||||
|
F: Future<Output = anyhow::Result<()>> + Send,
|
||||||
|
{
|
||||||
|
cx.background()
|
||||||
|
.spawn(async move { db_write().await.log_err() })
|
||||||
|
.detach()
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use std::{fs, thread};
|
use std::{fs, thread};
|
||||||
|
|
|
@ -805,15 +805,7 @@ mod tests {
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let project = Project::test(app_state.fs.clone(), ["/test".as_ref()], cx).await;
|
let project = Project::test(app_state.fs.clone(), ["/test".as_ref()], cx).await;
|
||||||
let (_, workspace) = cx.add_window(|cx| {
|
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
|
||||||
Workspace::new(
|
|
||||||
Default::default(),
|
|
||||||
0,
|
|
||||||
project.clone(),
|
|
||||||
|_, _| unimplemented!(),
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create some diagnostics
|
// Create some diagnostics
|
||||||
project.update(cx, |project, cx| {
|
project.update(cx, |project, cx| {
|
||||||
|
|
|
@ -1254,6 +1254,15 @@ impl Editor {
|
||||||
self.buffer.read(cx).language_at(point, cx)
|
self.buffer.read(cx).language_at(point, cx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn active_excerpt(
|
||||||
|
&self,
|
||||||
|
cx: &AppContext,
|
||||||
|
) -> Option<(ExcerptId, ModelHandle<Buffer>, Range<text::Anchor>)> {
|
||||||
|
self.buffer
|
||||||
|
.read(cx)
|
||||||
|
.excerpt_containing(self.selections.newest_anchor().head(), cx)
|
||||||
|
}
|
||||||
|
|
||||||
fn style(&self, cx: &AppContext) -> EditorStyle {
|
fn style(&self, cx: &AppContext) -> EditorStyle {
|
||||||
build_style(
|
build_style(
|
||||||
cx.global::<Settings>(),
|
cx.global::<Settings>(),
|
||||||
|
|
|
@ -484,7 +484,7 @@ fn test_navigation_history(cx: &mut gpui::MutableAppContext) {
|
||||||
cx.set_global(Settings::test(cx));
|
cx.set_global(Settings::test(cx));
|
||||||
cx.set_global(DragAndDrop::<Workspace>::default());
|
cx.set_global(DragAndDrop::<Workspace>::default());
|
||||||
use workspace::item::Item;
|
use workspace::item::Item;
|
||||||
let (_, pane) = cx.add_window(Default::default(), |cx| Pane::new(None, cx));
|
let (_, pane) = cx.add_window(Default::default(), |cx| Pane::new(0, None, || &[], cx));
|
||||||
let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
|
let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
|
||||||
|
|
||||||
cx.add_view(&pane, |cx| {
|
cx.add_view(&pane, |cx| {
|
||||||
|
@ -2353,12 +2353,16 @@ async fn test_clipboard(cx: &mut gpui::TestAppContext) {
|
||||||
e.paste(&Paste, cx);
|
e.paste(&Paste, cx);
|
||||||
e.handle_input(") ", cx);
|
e.handle_input(") ", cx);
|
||||||
});
|
});
|
||||||
cx.assert_editor_state(indoc! {"
|
cx.assert_editor_state(
|
||||||
( one✅
|
&([
|
||||||
three
|
"( one✅ ",
|
||||||
five ) ˇtwo one✅ four three six five ( one✅
|
"three ",
|
||||||
three
|
"five ) ˇtwo one✅ four three six five ( one✅ ",
|
||||||
five ) ˇ"});
|
"three ",
|
||||||
|
"five ) ˇ",
|
||||||
|
]
|
||||||
|
.join("\n")),
|
||||||
|
);
|
||||||
|
|
||||||
// Cut with three selections, one of which is full-line.
|
// Cut with three selections, one of which is full-line.
|
||||||
cx.set_state(indoc! {"
|
cx.set_state(indoc! {"
|
||||||
|
@ -5562,7 +5566,7 @@ async fn test_following_with_multiple_excerpts(cx: &mut gpui::TestAppContext) {
|
||||||
Settings::test_async(cx);
|
Settings::test_async(cx);
|
||||||
let fs = FakeFs::new(cx.background());
|
let fs = FakeFs::new(cx.background());
|
||||||
let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
|
let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
|
||||||
let (_, pane) = cx.add_window(|cx| Pane::new(None, cx));
|
let (_, pane) = cx.add_window(|cx| Pane::new(0, None, || &[], cx));
|
||||||
|
|
||||||
let leader = pane.update(cx, |_, cx| {
|
let leader = pane.update(cx, |_, cx| {
|
||||||
let multibuffer = cx.add_model(|_| MultiBuffer::new(0));
|
let multibuffer = cx.add_model(|_| MultiBuffer::new(0));
|
||||||
|
@ -5831,11 +5835,11 @@ async fn go_to_hunk(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppCon
|
||||||
cx.assert_editor_state(
|
cx.assert_editor_state(
|
||||||
&r#"
|
&r#"
|
||||||
ˇuse some::modified;
|
ˇuse some::modified;
|
||||||
|
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
println!("hello there");
|
println!("hello there");
|
||||||
|
|
||||||
println!("around the");
|
println!("around the");
|
||||||
println!("world");
|
println!("world");
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
use futures::FutureExt;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions,
|
actions,
|
||||||
elements::{Flex, MouseEventHandler, Padding, Text},
|
elements::{Flex, MouseEventHandler, Padding, Text},
|
||||||
|
@ -327,12 +328,10 @@ impl InfoPopover {
|
||||||
MouseEventHandler::<InfoPopover>::new(0, cx, |_, cx| {
|
MouseEventHandler::<InfoPopover>::new(0, cx, |_, cx| {
|
||||||
let mut flex = Flex::new(Axis::Vertical).scrollable::<HoverBlock, _>(1, None, cx);
|
let mut flex = Flex::new(Axis::Vertical).scrollable::<HoverBlock, _>(1, None, cx);
|
||||||
flex.extend(self.contents.iter().map(|content| {
|
flex.extend(self.contents.iter().map(|content| {
|
||||||
let project = self.project.read(cx);
|
let languages = self.project.read(cx).languages();
|
||||||
if let Some(language) = content
|
if let Some(language) = content.language.clone().and_then(|language| {
|
||||||
.language
|
languages.language_for_name(&language).now_or_never()?.ok()
|
||||||
.clone()
|
}) {
|
||||||
.and_then(|language| project.languages().language_for_name(&language))
|
|
||||||
{
|
|
||||||
let runs = language
|
let runs = language
|
||||||
.highlight_text(&content.text.as_str().into(), 0..content.text.len());
|
.highlight_text(&content.text.as_str().into(), 0..content.text.len());
|
||||||
|
|
||||||
|
|
|
@ -612,9 +612,34 @@ impl Item for Editor {
|
||||||
let buffers = self.buffer().clone().read(cx).all_buffers();
|
let buffers = self.buffer().clone().read(cx).all_buffers();
|
||||||
cx.as_mut().spawn(|mut cx| async move {
|
cx.as_mut().spawn(|mut cx| async move {
|
||||||
format.await?;
|
format.await?;
|
||||||
project
|
|
||||||
.update(&mut cx, |project, cx| project.save_buffers(buffers, cx))
|
if buffers.len() == 1 {
|
||||||
.await?;
|
project
|
||||||
|
.update(&mut cx, |project, cx| project.save_buffers(buffers, cx))
|
||||||
|
.await?;
|
||||||
|
} else {
|
||||||
|
// For multi-buffers, only save those ones that contain 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.read_with(&cx, |buffer, _| buffer.is_dirty() || buffer.has_conflict())
|
||||||
|
});
|
||||||
|
|
||||||
|
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(())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -1082,18 +1082,21 @@ impl MultiBuffer {
|
||||||
|
|
||||||
let mut cursor = snapshot.excerpts.cursor::<usize>();
|
let mut cursor = snapshot.excerpts.cursor::<usize>();
|
||||||
cursor.seek(&position, Bias::Right, &());
|
cursor.seek(&position, Bias::Right, &());
|
||||||
cursor.item().map(|excerpt| {
|
cursor
|
||||||
(
|
.item()
|
||||||
excerpt.id.clone(),
|
.or_else(|| snapshot.excerpts.last())
|
||||||
self.buffers
|
.map(|excerpt| {
|
||||||
.borrow()
|
(
|
||||||
.get(&excerpt.buffer_id)
|
excerpt.id.clone(),
|
||||||
.unwrap()
|
self.buffers
|
||||||
.buffer
|
.borrow()
|
||||||
.clone(),
|
.get(&excerpt.buffer_id)
|
||||||
excerpt.range.context.clone(),
|
.unwrap()
|
||||||
)
|
.buffer
|
||||||
})
|
.clone(),
|
||||||
|
excerpt.range.context.clone(),
|
||||||
|
)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// If point is at the end of the buffer, the last excerpt is returned
|
// If point is at the end of the buffer, the last excerpt is returned
|
||||||
|
|
|
@ -65,15 +65,7 @@ impl<'a> EditorLspTestContext<'a> {
|
||||||
.insert_tree("/root", json!({ "dir": { file_name.clone(): "" }}))
|
.insert_tree("/root", json!({ "dir": { file_name.clone(): "" }}))
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let (window_id, workspace) = cx.add_window(|cx| {
|
let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
|
||||||
Workspace::new(
|
|
||||||
Default::default(),
|
|
||||||
0,
|
|
||||||
project.clone(),
|
|
||||||
|_, _| unimplemented!(),
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
});
|
|
||||||
project
|
project
|
||||||
.update(cx, |project, cx| {
|
.update(cx, |project, cx| {
|
||||||
project.find_or_create_local_worktree("/root", true, cx)
|
project.find_or_create_local_worktree("/root", true, cx)
|
||||||
|
@ -134,7 +126,7 @@ impl<'a> EditorLspTestContext<'a> {
|
||||||
(let_chain)
|
(let_chain)
|
||||||
(await_expression)
|
(await_expression)
|
||||||
] @indent
|
] @indent
|
||||||
|
|
||||||
(_ "[" "]" @end) @indent
|
(_ "[" "]" @end) @indent
|
||||||
(_ "<" ">" @end) @indent
|
(_ "<" ">" @end) @indent
|
||||||
(_ "{" "}" @end) @indent
|
(_ "{" "}" @end) @indent
|
||||||
|
|
|
@ -1,34 +1,59 @@
|
||||||
use gpui::{
|
use gpui::{elements::*, CursorStyle, Entity, MouseButton, RenderContext, View, ViewContext};
|
||||||
elements::{MouseEventHandler, ParentElement, Stack, Text},
|
|
||||||
CursorStyle, Element, ElementBox, Entity, MouseButton, RenderContext, View, ViewContext,
|
|
||||||
};
|
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
use workspace::{item::ItemHandle, StatusItemView};
|
use workspace::{item::ItemHandle, StatusItemView};
|
||||||
|
|
||||||
use crate::feedback_editor::GiveFeedback;
|
use crate::feedback_editor::{FeedbackEditor, GiveFeedback};
|
||||||
|
|
||||||
pub struct DeployFeedbackButton;
|
pub struct DeployFeedbackButton {
|
||||||
|
active: bool,
|
||||||
|
}
|
||||||
|
|
||||||
impl Entity for DeployFeedbackButton {
|
impl Entity for DeployFeedbackButton {
|
||||||
type Event = ();
|
type Event = ();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl DeployFeedbackButton {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
DeployFeedbackButton { active: false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl View for DeployFeedbackButton {
|
impl View for DeployFeedbackButton {
|
||||||
fn ui_name() -> &'static str {
|
fn ui_name() -> &'static str {
|
||||||
"DeployFeedbackButton"
|
"DeployFeedbackButton"
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render(&mut self, cx: &mut RenderContext<'_, Self>) -> ElementBox {
|
fn render(&mut self, cx: &mut RenderContext<'_, Self>) -> ElementBox {
|
||||||
|
let active = self.active;
|
||||||
Stack::new()
|
Stack::new()
|
||||||
.with_child(
|
.with_child(
|
||||||
MouseEventHandler::<Self>::new(0, cx, |state, cx| {
|
MouseEventHandler::<Self>::new(0, cx, |state, cx| {
|
||||||
let theme = &cx.global::<Settings>().theme;
|
let theme = &cx.global::<Settings>().theme;
|
||||||
let theme = &theme.workspace.status_bar.feedback;
|
let style = &theme
|
||||||
|
.workspace
|
||||||
|
.status_bar
|
||||||
|
.sidebar_buttons
|
||||||
|
.item
|
||||||
|
.style_for(state, active);
|
||||||
|
|
||||||
Text::new("Give Feedback", theme.style_for(state, true).clone()).boxed()
|
Svg::new("icons/speech_bubble_12.svg")
|
||||||
|
.with_color(style.icon_color)
|
||||||
|
.constrained()
|
||||||
|
.with_width(style.icon_size)
|
||||||
|
.aligned()
|
||||||
|
.constrained()
|
||||||
|
.with_width(style.icon_size)
|
||||||
|
.with_height(style.icon_size)
|
||||||
|
.contained()
|
||||||
|
.with_style(style.container)
|
||||||
|
.boxed()
|
||||||
})
|
})
|
||||||
.with_cursor_style(CursorStyle::PointingHand)
|
.with_cursor_style(CursorStyle::PointingHand)
|
||||||
.on_click(MouseButton::Left, |_, cx| cx.dispatch_action(GiveFeedback))
|
.on_click(MouseButton::Left, move |_, cx| {
|
||||||
|
if !active {
|
||||||
|
cx.dispatch_action(GiveFeedback)
|
||||||
|
}
|
||||||
|
})
|
||||||
.boxed(),
|
.boxed(),
|
||||||
)
|
)
|
||||||
.boxed()
|
.boxed()
|
||||||
|
@ -36,5 +61,15 @@ impl View for DeployFeedbackButton {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StatusItemView for DeployFeedbackButton {
|
impl StatusItemView for DeployFeedbackButton {
|
||||||
fn set_active_pane_item(&mut self, _: Option<&dyn ItemHandle>, _: &mut ViewContext<Self>) {}
|
fn set_active_pane_item(&mut self, item: Option<&dyn ItemHandle>, cx: &mut ViewContext<Self>) {
|
||||||
|
if let Some(item) = item {
|
||||||
|
if let Some(_) = item.downcast::<FeedbackEditor>() {
|
||||||
|
self.active = true;
|
||||||
|
cx.notify();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.active = false;
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,7 @@ use postage::prelude::Stream;
|
||||||
|
|
||||||
use project::Project;
|
use project::Project;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
use util::ResultExt;
|
||||||
use workspace::{
|
use workspace::{
|
||||||
item::{Item, ItemHandle},
|
item::{Item, ItemHandle},
|
||||||
searchable::{SearchableItem, SearchableItemHandle},
|
searchable::{SearchableItem, SearchableItemHandle},
|
||||||
|
@ -200,24 +201,28 @@ impl FeedbackEditor {
|
||||||
impl FeedbackEditor {
|
impl FeedbackEditor {
|
||||||
pub fn deploy(
|
pub fn deploy(
|
||||||
system_specs: SystemSpecs,
|
system_specs: SystemSpecs,
|
||||||
workspace: &mut Workspace,
|
_: &mut Workspace,
|
||||||
app_state: Arc<AppState>,
|
app_state: Arc<AppState>,
|
||||||
cx: &mut ViewContext<Workspace>,
|
cx: &mut ViewContext<Workspace>,
|
||||||
) {
|
) {
|
||||||
workspace
|
let markdown = app_state.languages.language_for_name("Markdown");
|
||||||
.with_local_workspace(&app_state, cx, |workspace, cx| {
|
cx.spawn(|workspace, mut cx| async move {
|
||||||
let project = workspace.project().clone();
|
let markdown = markdown.await.log_err();
|
||||||
let markdown_language = project.read(cx).languages().language_for_name("Markdown");
|
workspace
|
||||||
let buffer = project
|
.update(&mut cx, |workspace, cx| {
|
||||||
.update(cx, |project, cx| {
|
workspace.with_local_workspace(&app_state, cx, |workspace, cx| {
|
||||||
project.create_buffer("", markdown_language, cx)
|
let project = workspace.project().clone();
|
||||||
|
let buffer = project
|
||||||
|
.update(cx, |project, cx| project.create_buffer("", markdown, cx))
|
||||||
|
.expect("creating buffers on a local workspace always succeeds");
|
||||||
|
let feedback_editor = cx
|
||||||
|
.add_view(|cx| FeedbackEditor::new(system_specs, project, buffer, cx));
|
||||||
|
workspace.add_item(Box::new(feedback_editor), cx);
|
||||||
})
|
})
|
||||||
.expect("creating buffers on a local workspace always succeeds");
|
})
|
||||||
let feedback_editor =
|
.await;
|
||||||
cx.add_view(|cx| FeedbackEditor::new(system_specs, project, buffer, cx));
|
})
|
||||||
workspace.add_item(Box::new(feedback_editor), cx);
|
.detach();
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -329,9 +329,7 @@ mod tests {
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
|
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
|
||||||
let (window_id, workspace) = cx.add_window(|cx| {
|
let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
|
||||||
Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
|
|
||||||
});
|
|
||||||
cx.dispatch_action(window_id, Toggle);
|
cx.dispatch_action(window_id, Toggle);
|
||||||
|
|
||||||
let finder = cx.read(|cx| workspace.read(cx).modal::<FileFinder>().unwrap());
|
let finder = cx.read(|cx| workspace.read(cx).modal::<FileFinder>().unwrap());
|
||||||
|
@ -385,9 +383,7 @@ mod tests {
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let project = Project::test(app_state.fs.clone(), ["/dir".as_ref()], cx).await;
|
let project = Project::test(app_state.fs.clone(), ["/dir".as_ref()], cx).await;
|
||||||
let (_, workspace) = cx.add_window(|cx| {
|
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
|
||||||
Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
|
|
||||||
});
|
|
||||||
let (_, finder) =
|
let (_, finder) =
|
||||||
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), None, cx));
|
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), None, cx));
|
||||||
|
|
||||||
|
@ -461,9 +457,7 @@ mod tests {
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
let (_, workspace) = cx.add_window(|cx| {
|
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
|
||||||
Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
|
|
||||||
});
|
|
||||||
let (_, finder) =
|
let (_, finder) =
|
||||||
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), None, cx));
|
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), None, cx));
|
||||||
finder
|
finder
|
||||||
|
@ -487,9 +481,7 @@ mod tests {
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
let (_, workspace) = cx.add_window(|cx| {
|
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
|
||||||
Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
|
|
||||||
});
|
|
||||||
let (_, finder) =
|
let (_, finder) =
|
||||||
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), None, cx));
|
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), None, cx));
|
||||||
|
|
||||||
|
@ -541,9 +533,7 @@ mod tests {
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
let (_, workspace) = cx.add_window(|cx| {
|
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
|
||||||
Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
|
|
||||||
});
|
|
||||||
|
|
||||||
let (_, finder) =
|
let (_, finder) =
|
||||||
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), None, cx));
|
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), None, cx));
|
||||||
|
@ -585,9 +575,7 @@ mod tests {
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
|
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
|
||||||
let (_, workspace) = cx.add_window(|cx| {
|
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
|
||||||
Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
|
|
||||||
});
|
|
||||||
|
|
||||||
// When workspace has an active item, sort items which are closer to that item
|
// When workspace has an active item, sort items which are closer to that item
|
||||||
// first when they have the same name. In this case, b.txt is closer to dir2's a.txt
|
// first when they have the same name. In this case, b.txt is closer to dir2's a.txt
|
||||||
|
@ -624,9 +612,7 @@ mod tests {
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
|
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
|
||||||
let (_, workspace) = cx.add_window(|cx| {
|
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
|
||||||
Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
|
|
||||||
});
|
|
||||||
let (_, finder) =
|
let (_, finder) =
|
||||||
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), None, cx));
|
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), None, cx));
|
||||||
finder
|
finder
|
||||||
|
|
|
@ -485,7 +485,9 @@ pub struct MutableAppContext {
|
||||||
cx: AppContext,
|
cx: AppContext,
|
||||||
action_deserializers: HashMap<&'static str, (TypeId, DeserializeActionCallback)>,
|
action_deserializers: HashMap<&'static str, (TypeId, DeserializeActionCallback)>,
|
||||||
capture_actions: HashMap<TypeId, HashMap<TypeId, Vec<Box<ActionCallback>>>>,
|
capture_actions: HashMap<TypeId, HashMap<TypeId, Vec<Box<ActionCallback>>>>,
|
||||||
|
// Entity Types -> { Action Types -> Action Handlers }
|
||||||
actions: HashMap<TypeId, HashMap<TypeId, Vec<Box<ActionCallback>>>>,
|
actions: HashMap<TypeId, HashMap<TypeId, Vec<Box<ActionCallback>>>>,
|
||||||
|
// Action Types -> Action Handlers
|
||||||
global_actions: HashMap<TypeId, Box<GlobalActionCallback>>,
|
global_actions: HashMap<TypeId, Box<GlobalActionCallback>>,
|
||||||
keystroke_matcher: KeymapMatcher,
|
keystroke_matcher: KeymapMatcher,
|
||||||
next_entity_id: usize,
|
next_entity_id: usize,
|
||||||
|
@ -1239,20 +1241,34 @@ impl MutableAppContext {
|
||||||
action: &dyn Action,
|
action: &dyn Action,
|
||||||
) -> Option<SmallVec<[Keystroke; 2]>> {
|
) -> Option<SmallVec<[Keystroke; 2]>> {
|
||||||
let mut contexts = Vec::new();
|
let mut contexts = Vec::new();
|
||||||
for view_id in self.ancestors(window_id, view_id) {
|
let mut handler_depth = None;
|
||||||
|
for (i, view_id) in self.ancestors(window_id, view_id).enumerate() {
|
||||||
if let Some(view) = self.views.get(&(window_id, view_id)) {
|
if let Some(view) = self.views.get(&(window_id, view_id)) {
|
||||||
|
if let Some(actions) = self.actions.get(&view.as_any().type_id()) {
|
||||||
|
if actions.contains_key(&action.as_any().type_id()) {
|
||||||
|
handler_depth = Some(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
contexts.push(view.keymap_context(self));
|
contexts.push(view.keymap_context(self));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if self.global_actions.contains_key(&action.as_any().type_id()) {
|
||||||
|
handler_depth = Some(contexts.len())
|
||||||
|
}
|
||||||
|
|
||||||
self.keystroke_matcher
|
self.keystroke_matcher
|
||||||
.bindings_for_action_type(action.as_any().type_id())
|
.bindings_for_action_type(action.as_any().type_id())
|
||||||
.find_map(|b| {
|
.find_map(|b| {
|
||||||
if b.match_context(&contexts) {
|
handler_depth
|
||||||
Some(b.keystrokes().into())
|
.map(|highest_handler| {
|
||||||
} else {
|
if (0..=highest_handler).any(|depth| b.match_context(&contexts[depth..])) {
|
||||||
None
|
Some(b.keystrokes().into())
|
||||||
}
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.flatten()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1261,29 +1277,42 @@ impl MutableAppContext {
|
||||||
window_id: usize,
|
window_id: usize,
|
||||||
view_id: usize,
|
view_id: usize,
|
||||||
) -> impl Iterator<Item = (&'static str, Box<dyn Action>, SmallVec<[&Binding; 1]>)> {
|
) -> impl Iterator<Item = (&'static str, Box<dyn Action>, SmallVec<[&Binding; 1]>)> {
|
||||||
let mut action_types: HashSet<_> = self.global_actions.keys().copied().collect();
|
|
||||||
|
|
||||||
let mut contexts = Vec::new();
|
let mut contexts = Vec::new();
|
||||||
for view_id in self.ancestors(window_id, view_id) {
|
let mut handler_depths_by_action_type = HashMap::<TypeId, usize>::default();
|
||||||
|
for (depth, view_id) in self.ancestors(window_id, view_id).enumerate() {
|
||||||
if let Some(view) = self.views.get(&(window_id, view_id)) {
|
if let Some(view) = self.views.get(&(window_id, view_id)) {
|
||||||
contexts.push(view.keymap_context(self));
|
contexts.push(view.keymap_context(self));
|
||||||
let view_type = view.as_any().type_id();
|
let view_type = view.as_any().type_id();
|
||||||
if let Some(actions) = self.actions.get(&view_type) {
|
if let Some(actions) = self.actions.get(&view_type) {
|
||||||
action_types.extend(actions.keys().copied());
|
handler_depths_by_action_type.extend(
|
||||||
|
actions
|
||||||
|
.keys()
|
||||||
|
.copied()
|
||||||
|
.map(|action_type| (action_type, depth)),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handler_depths_by_action_type.extend(
|
||||||
|
self.global_actions
|
||||||
|
.keys()
|
||||||
|
.copied()
|
||||||
|
.map(|action_type| (action_type, contexts.len())),
|
||||||
|
);
|
||||||
|
|
||||||
self.action_deserializers
|
self.action_deserializers
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(move |(name, (type_id, deserialize))| {
|
.filter_map(move |(name, (type_id, deserialize))| {
|
||||||
if action_types.contains(type_id) {
|
if let Some(action_depth) = handler_depths_by_action_type.get(type_id).copied() {
|
||||||
Some((
|
Some((
|
||||||
*name,
|
*name,
|
||||||
deserialize("{}").ok()?,
|
deserialize("{}").ok()?,
|
||||||
self.keystroke_matcher
|
self.keystroke_matcher
|
||||||
.bindings_for_action_type(*type_id)
|
.bindings_for_action_type(*type_id)
|
||||||
.filter(|b| b.match_context(&contexts))
|
.filter(|b| {
|
||||||
|
(0..=action_depth).any(|depth| b.match_context(&contexts[depth..]))
|
||||||
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
))
|
))
|
||||||
} else {
|
} else {
|
||||||
|
@ -1294,7 +1323,7 @@ impl MutableAppContext {
|
||||||
|
|
||||||
pub fn is_action_available(&self, action: &dyn Action) -> bool {
|
pub fn is_action_available(&self, action: &dyn Action) -> bool {
|
||||||
let action_type = action.as_any().type_id();
|
let action_type = action.as_any().type_id();
|
||||||
if let Some(window_id) = self.cx.platform.key_window_id() {
|
if let Some(window_id) = self.cx.platform.main_window_id() {
|
||||||
if let Some(focused_view_id) = self.focused_view_id(window_id) {
|
if let Some(focused_view_id) = self.focused_view_id(window_id) {
|
||||||
for view_id in self.ancestors(window_id, focused_view_id) {
|
for view_id in self.ancestors(window_id, focused_view_id) {
|
||||||
if let Some(view) = self.views.get(&(window_id, view_id)) {
|
if let Some(view) = self.views.get(&(window_id, view_id)) {
|
||||||
|
@ -5086,7 +5115,7 @@ impl<T: Entity> From<WeakModelHandle<T>> for AnyWeakModelHandle {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug, Copy)]
|
||||||
pub struct WeakViewHandle<T> {
|
pub struct WeakViewHandle<T> {
|
||||||
window_id: usize,
|
window_id: usize,
|
||||||
view_id: usize,
|
view_id: usize,
|
||||||
|
@ -5287,6 +5316,7 @@ impl Subscription {
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::{actions, elements::*, impl_actions, MouseButton, MouseButtonEvent};
|
use crate::{actions, elements::*, impl_actions, MouseButton, MouseButtonEvent};
|
||||||
|
use itertools::Itertools;
|
||||||
use postage::{sink::Sink, stream::Stream};
|
use postage::{sink::Sink, stream::Stream};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use smol::future::poll_once;
|
use smol::future::poll_once;
|
||||||
|
@ -6717,6 +6747,128 @@ mod tests {
|
||||||
actions.borrow_mut().clear();
|
actions.borrow_mut().clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[crate::test(self)]
|
||||||
|
fn test_keystrokes_for_action(cx: &mut MutableAppContext) {
|
||||||
|
actions!(test, [Action1, Action2, GlobalAction]);
|
||||||
|
|
||||||
|
struct View1 {}
|
||||||
|
struct View2 {}
|
||||||
|
|
||||||
|
impl Entity for View1 {
|
||||||
|
type Event = ();
|
||||||
|
}
|
||||||
|
impl Entity for View2 {
|
||||||
|
type Event = ();
|
||||||
|
}
|
||||||
|
|
||||||
|
impl super::View for View1 {
|
||||||
|
fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
|
||||||
|
Empty::new().boxed()
|
||||||
|
}
|
||||||
|
fn ui_name() -> &'static str {
|
||||||
|
"View1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl super::View for View2 {
|
||||||
|
fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
|
||||||
|
Empty::new().boxed()
|
||||||
|
}
|
||||||
|
fn ui_name() -> &'static str {
|
||||||
|
"View2"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let (window_id, view_1) = cx.add_window(Default::default(), |_| View1 {});
|
||||||
|
let view_2 = cx.add_view(&view_1, |cx| {
|
||||||
|
cx.focus_self();
|
||||||
|
View2 {}
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.add_action(|_: &mut View1, _: &Action1, _cx| {});
|
||||||
|
cx.add_action(|_: &mut View2, _: &Action2, _cx| {});
|
||||||
|
cx.add_global_action(|_: &GlobalAction, _| {});
|
||||||
|
|
||||||
|
cx.add_bindings(vec![
|
||||||
|
Binding::new("a", Action1, Some("View1")),
|
||||||
|
Binding::new("b", Action2, Some("View1 > View2")),
|
||||||
|
Binding::new("c", GlobalAction, Some("View3")), // View 3 does not exist
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Sanity check
|
||||||
|
assert_eq!(
|
||||||
|
cx.keystrokes_for_action(window_id, view_1.id(), &Action1)
|
||||||
|
.unwrap()
|
||||||
|
.as_slice(),
|
||||||
|
&[Keystroke::parse("a").unwrap()]
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
cx.keystrokes_for_action(window_id, view_2.id(), &Action2)
|
||||||
|
.unwrap()
|
||||||
|
.as_slice(),
|
||||||
|
&[Keystroke::parse("b").unwrap()]
|
||||||
|
);
|
||||||
|
|
||||||
|
// The 'a' keystroke propagates up the view tree from view_2
|
||||||
|
// to view_1. The action, Action1, is handled by view_1.
|
||||||
|
assert_eq!(
|
||||||
|
cx.keystrokes_for_action(window_id, view_2.id(), &Action1)
|
||||||
|
.unwrap()
|
||||||
|
.as_slice(),
|
||||||
|
&[Keystroke::parse("a").unwrap()]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Actions that are handled below the current view don't have bindings
|
||||||
|
assert_eq!(
|
||||||
|
cx.keystrokes_for_action(window_id, view_1.id(), &Action2),
|
||||||
|
None
|
||||||
|
);
|
||||||
|
|
||||||
|
// Actions that are handled in other branches of the tree should not have a binding
|
||||||
|
assert_eq!(
|
||||||
|
cx.keystrokes_for_action(window_id, view_2.id(), &GlobalAction),
|
||||||
|
None
|
||||||
|
);
|
||||||
|
|
||||||
|
// Produces a list of actions and keybindings
|
||||||
|
fn available_actions(
|
||||||
|
window_id: usize,
|
||||||
|
view_id: usize,
|
||||||
|
cx: &mut MutableAppContext,
|
||||||
|
) -> Vec<(&'static str, Vec<Keystroke>)> {
|
||||||
|
cx.available_actions(window_id, view_id)
|
||||||
|
.map(|(action_name, _, bindings)| {
|
||||||
|
(
|
||||||
|
action_name,
|
||||||
|
bindings
|
||||||
|
.iter()
|
||||||
|
.map(|binding| binding.keystrokes()[0].clone())
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.sorted_by(|(name1, _), (name2, _)| name1.cmp(name2))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that global actions do not have a binding, even if a binding does exist in another view
|
||||||
|
assert_eq!(
|
||||||
|
&available_actions(window_id, view_1.id(), cx),
|
||||||
|
&[
|
||||||
|
("test::Action1", vec![Keystroke::parse("a").unwrap()]),
|
||||||
|
("test::GlobalAction", vec![])
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check that view 1 actions and bindings are available even when called from view 2
|
||||||
|
assert_eq!(
|
||||||
|
&available_actions(window_id, view_2.id(), cx),
|
||||||
|
&[
|
||||||
|
("test::Action1", vec![Keystroke::parse("a").unwrap()]),
|
||||||
|
("test::Action2", vec![Keystroke::parse("b").unwrap()]),
|
||||||
|
("test::GlobalAction", vec![]),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[crate::test(self)]
|
#[crate::test(self)]
|
||||||
async fn test_model_condition(cx: &mut TestAppContext) {
|
async fn test_model_condition(cx: &mut TestAppContext) {
|
||||||
struct Counter(usize);
|
struct Counter(usize);
|
||||||
|
|
|
@ -77,9 +77,9 @@ pub(crate) fn setup_menu_handlers(foreground_platform: &dyn ForegroundPlatform,
|
||||||
let cx = app.0.clone();
|
let cx = app.0.clone();
|
||||||
move |action| {
|
move |action| {
|
||||||
let mut cx = cx.borrow_mut();
|
let mut cx = cx.borrow_mut();
|
||||||
if let Some(key_window_id) = cx.cx.platform.key_window_id() {
|
if let Some(main_window_id) = cx.cx.platform.main_window_id() {
|
||||||
if let Some(view_id) = cx.focused_view_id(key_window_id) {
|
if let Some(view_id) = cx.focused_view_id(main_window_id) {
|
||||||
cx.handle_dispatch_action_from_effect(key_window_id, Some(view_id), action);
|
cx.handle_dispatch_action_from_effect(main_window_id, Some(view_id), action);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,9 +18,10 @@ use smol::stream::StreamExt;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
executor, geometry::vector::Vector2F, keymap_matcher::Keystroke, platform, Action,
|
executor, geometry::vector::Vector2F, keymap_matcher::Keystroke, platform, Action,
|
||||||
AnyViewHandle, AppContext, Appearance, Entity, Event, FontCache, InputHandler, KeyDownEvent,
|
AnyViewHandle, AppContext, Appearance, Entity, Event, FontCache, Handle, InputHandler,
|
||||||
ModelContext, ModelHandle, MutableAppContext, Platform, ReadModelWith, ReadViewWith,
|
KeyDownEvent, ModelContext, ModelHandle, MutableAppContext, Platform, ReadModelWith,
|
||||||
RenderContext, Task, UpdateModel, UpdateView, View, ViewContext, ViewHandle, WeakHandle,
|
ReadViewWith, RenderContext, Task, UpdateModel, UpdateView, View, ViewContext, ViewHandle,
|
||||||
|
WeakHandle,
|
||||||
};
|
};
|
||||||
use collections::BTreeMap;
|
use collections::BTreeMap;
|
||||||
|
|
||||||
|
@ -329,6 +330,14 @@ impl TestAppContext {
|
||||||
.assert_dropped(handle.id())
|
.assert_dropped(handle.id())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Drop a handle, assuming it is the last. If it is not the last, panic with debug information about
|
||||||
|
/// where the stray handles were created.
|
||||||
|
pub fn drop_last<T, W: WeakHandle, H: Handle<T, Weak = W>>(&mut self, handle: H) {
|
||||||
|
let weak = handle.downgrade();
|
||||||
|
self.update(|_| drop(handle));
|
||||||
|
self.assert_dropped(weak);
|
||||||
|
}
|
||||||
|
|
||||||
fn window_mut(&self, window_id: usize) -> std::cell::RefMut<platform::test::Window> {
|
fn window_mut(&self, window_id: usize) -> std::cell::RefMut<platform::test::Window> {
|
||||||
std::cell::RefMut::map(self.cx.borrow_mut(), |state| {
|
std::cell::RefMut::map(self.cx.borrow_mut(), |state| {
|
||||||
let (_, window) = state
|
let (_, window) = state
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use std::{borrow::Cow, cell::RefCell, collections::HashMap};
|
use image::ImageFormat;
|
||||||
|
use std::{borrow::Cow, cell::RefCell, collections::HashMap, sync::Arc};
|
||||||
|
|
||||||
|
use crate::ImageData;
|
||||||
|
|
||||||
pub trait AssetSource: 'static + Send + Sync {
|
pub trait AssetSource: 'static + Send + Sync {
|
||||||
fn load(&self, path: &str) -> Result<Cow<[u8]>>;
|
fn load(&self, path: &str) -> Result<Cow<[u8]>>;
|
||||||
|
@ -22,6 +25,7 @@ impl AssetSource for () {
|
||||||
pub struct AssetCache {
|
pub struct AssetCache {
|
||||||
source: Box<dyn AssetSource>,
|
source: Box<dyn AssetSource>,
|
||||||
svgs: RefCell<HashMap<String, usvg::Tree>>,
|
svgs: RefCell<HashMap<String, usvg::Tree>>,
|
||||||
|
pngs: RefCell<HashMap<String, Arc<ImageData>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AssetCache {
|
impl AssetCache {
|
||||||
|
@ -29,6 +33,7 @@ impl AssetCache {
|
||||||
Self {
|
Self {
|
||||||
source: Box::new(source),
|
source: Box::new(source),
|
||||||
svgs: RefCell::new(HashMap::new()),
|
svgs: RefCell::new(HashMap::new()),
|
||||||
|
pngs: RefCell::new(HashMap::new()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -43,4 +48,18 @@ impl AssetCache {
|
||||||
Ok(svg)
|
Ok(svg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn png(&self, path: &str) -> Result<Arc<ImageData>> {
|
||||||
|
let mut pngs = self.pngs.borrow_mut();
|
||||||
|
if let Some(png) = pngs.get(path) {
|
||||||
|
Ok(png.clone())
|
||||||
|
} else {
|
||||||
|
let bytes = self.source.load(path)?;
|
||||||
|
let image = ImageData::new(
|
||||||
|
image::load_from_memory_with_format(&bytes, ImageFormat::Png)?.into_bgra8(),
|
||||||
|
);
|
||||||
|
pngs.insert(path.to_string(), image.clone());
|
||||||
|
Ok(image)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -153,7 +153,9 @@ impl Element for ConstrainedBox {
|
||||||
_: &mut Self::LayoutState,
|
_: &mut Self::LayoutState,
|
||||||
cx: &mut PaintContext,
|
cx: &mut PaintContext,
|
||||||
) -> Self::PaintState {
|
) -> Self::PaintState {
|
||||||
self.child.paint(bounds.origin(), visible_bounds, cx);
|
cx.paint_layer(Some(visible_bounds), |cx| {
|
||||||
|
self.child.paint(bounds.origin(), visible_bounds, cx);
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn rect_for_text_range(
|
fn rect_for_text_range(
|
||||||
|
|
|
@ -22,6 +22,7 @@ pub struct Flex {
|
||||||
axis: Axis,
|
axis: Axis,
|
||||||
children: Vec<ElementBox>,
|
children: Vec<ElementBox>,
|
||||||
scroll_state: Option<(ElementStateHandle<Rc<ScrollState>>, usize)>,
|
scroll_state: Option<(ElementStateHandle<Rc<ScrollState>>, usize)>,
|
||||||
|
child_alignment: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Flex {
|
impl Flex {
|
||||||
|
@ -30,6 +31,7 @@ impl Flex {
|
||||||
axis,
|
axis,
|
||||||
children: Default::default(),
|
children: Default::default(),
|
||||||
scroll_state: None,
|
scroll_state: None,
|
||||||
|
child_alignment: -1.,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -41,6 +43,15 @@ impl Flex {
|
||||||
Self::new(Axis::Vertical)
|
Self::new(Axis::Vertical)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Render children centered relative to the cross-axis of the parent flex.
|
||||||
|
///
|
||||||
|
/// If this is a flex row, children will be centered vertically. If this is a
|
||||||
|
/// flex column, children will be centered horizontally.
|
||||||
|
pub fn align_children_center(mut self) -> Self {
|
||||||
|
self.child_alignment = 0.;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub fn scrollable<Tag, V>(
|
pub fn scrollable<Tag, V>(
|
||||||
mut self,
|
mut self,
|
||||||
element_id: usize,
|
element_id: usize,
|
||||||
|
@ -309,7 +320,30 @@ impl Element for Flex {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
child.paint(child_origin, visible_bounds, cx);
|
// We use the child_alignment f32 to determine a point along the cross axis of the
|
||||||
|
// overall flex element and each child. We then align these points. So 0 would center
|
||||||
|
// each child relative to the overall height/width of the flex. -1 puts children at
|
||||||
|
// the start. 1 puts children at the end.
|
||||||
|
let aligned_child_origin = {
|
||||||
|
let cross_axis = self.axis.invert();
|
||||||
|
let my_center = bounds.size().along(cross_axis) / 2.;
|
||||||
|
let my_target = my_center + my_center * self.child_alignment;
|
||||||
|
|
||||||
|
let child_center = child.size().along(cross_axis) / 2.;
|
||||||
|
let child_target = child_center + child_center * self.child_alignment;
|
||||||
|
|
||||||
|
let mut aligned_child_origin = child_origin;
|
||||||
|
match self.axis {
|
||||||
|
Axis::Horizontal => aligned_child_origin
|
||||||
|
.set_y(aligned_child_origin.y() - (child_target - my_target)),
|
||||||
|
Axis::Vertical => aligned_child_origin
|
||||||
|
.set_x(aligned_child_origin.x() - (child_target - my_target)),
|
||||||
|
}
|
||||||
|
|
||||||
|
aligned_child_origin
|
||||||
|
};
|
||||||
|
|
||||||
|
child.paint(aligned_child_origin, visible_bounds, cx);
|
||||||
|
|
||||||
match self.axis {
|
match self.axis {
|
||||||
Axis::Horizontal => child_origin += vec2f(child.size().x(), 0.0),
|
Axis::Horizontal => child_origin += vec2f(child.size().x(), 0.0),
|
||||||
|
|
|
@ -11,8 +11,13 @@ use crate::{
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::{ops::Range, sync::Arc};
|
use std::{ops::Range, sync::Arc};
|
||||||
|
|
||||||
|
enum ImageSource {
|
||||||
|
Path(&'static str),
|
||||||
|
Data(Arc<ImageData>),
|
||||||
|
}
|
||||||
|
|
||||||
pub struct Image {
|
pub struct Image {
|
||||||
data: Arc<ImageData>,
|
source: ImageSource,
|
||||||
style: ImageStyle,
|
style: ImageStyle,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -31,9 +36,16 @@ pub struct ImageStyle {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Image {
|
impl Image {
|
||||||
pub fn new(data: Arc<ImageData>) -> Self {
|
pub fn new(asset_path: &'static str) -> Self {
|
||||||
Self {
|
Self {
|
||||||
data,
|
source: ImageSource::Path(asset_path),
|
||||||
|
style: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_data(data: Arc<ImageData>) -> Self {
|
||||||
|
Self {
|
||||||
|
source: ImageSource::Data(data),
|
||||||
style: Default::default(),
|
style: Default::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -45,39 +57,53 @@ impl Image {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Element for Image {
|
impl Element for Image {
|
||||||
type LayoutState = ();
|
type LayoutState = Option<Arc<ImageData>>;
|
||||||
type PaintState = ();
|
type PaintState = ();
|
||||||
|
|
||||||
fn layout(
|
fn layout(
|
||||||
&mut self,
|
&mut self,
|
||||||
constraint: SizeConstraint,
|
constraint: SizeConstraint,
|
||||||
_: &mut LayoutContext,
|
cx: &mut LayoutContext,
|
||||||
) -> (Vector2F, Self::LayoutState) {
|
) -> (Vector2F, Self::LayoutState) {
|
||||||
|
let data = match &self.source {
|
||||||
|
ImageSource::Path(path) => match cx.asset_cache.png(path) {
|
||||||
|
Ok(data) => data,
|
||||||
|
Err(error) => {
|
||||||
|
log::error!("could not load image: {}", error);
|
||||||
|
return (Vector2F::zero(), None);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ImageSource::Data(data) => data.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
let desired_size = vec2f(
|
let desired_size = vec2f(
|
||||||
self.style.width.unwrap_or_else(|| constraint.max.x()),
|
self.style.width.unwrap_or_else(|| constraint.max.x()),
|
||||||
self.style.height.unwrap_or_else(|| constraint.max.y()),
|
self.style.height.unwrap_or_else(|| constraint.max.y()),
|
||||||
);
|
);
|
||||||
let size = constrain_size_preserving_aspect_ratio(
|
let size = constrain_size_preserving_aspect_ratio(
|
||||||
constraint.constrain(desired_size),
|
constraint.constrain(desired_size),
|
||||||
self.data.size().to_f32(),
|
data.size().to_f32(),
|
||||||
);
|
);
|
||||||
(size, ())
|
|
||||||
|
(size, Some(data))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn paint(
|
fn paint(
|
||||||
&mut self,
|
&mut self,
|
||||||
bounds: RectF,
|
bounds: RectF,
|
||||||
_: RectF,
|
_: RectF,
|
||||||
_: &mut Self::LayoutState,
|
layout: &mut Self::LayoutState,
|
||||||
cx: &mut PaintContext,
|
cx: &mut PaintContext,
|
||||||
) -> Self::PaintState {
|
) -> Self::PaintState {
|
||||||
cx.scene.push_image(scene::Image {
|
if let Some(data) = layout {
|
||||||
bounds,
|
cx.scene.push_image(scene::Image {
|
||||||
border: self.style.border,
|
bounds,
|
||||||
corner_radius: self.style.corner_radius,
|
border: self.style.border,
|
||||||
grayscale: self.style.grayscale,
|
corner_radius: self.style.corner_radius,
|
||||||
data: self.data.clone(),
|
grayscale: self.style.grayscale,
|
||||||
});
|
data: data.clone(),
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn rect_for_text_range(
|
fn rect_for_text_range(
|
||||||
|
|
|
@ -58,7 +58,7 @@ pub trait Platform: Send + Sync {
|
||||||
options: WindowOptions,
|
options: WindowOptions,
|
||||||
executor: Rc<executor::Foreground>,
|
executor: Rc<executor::Foreground>,
|
||||||
) -> Box<dyn Window>;
|
) -> Box<dyn Window>;
|
||||||
fn key_window_id(&self) -> Option<usize>;
|
fn main_window_id(&self) -> Option<usize>;
|
||||||
|
|
||||||
fn add_status_item(&self) -> Box<dyn Window>;
|
fn add_status_item(&self) -> Box<dyn Window>;
|
||||||
|
|
||||||
|
|
|
@ -587,8 +587,8 @@ impl platform::Platform for MacPlatform {
|
||||||
Box::new(Window::open(id, options, executor, self.fonts()))
|
Box::new(Window::open(id, options, executor, self.fonts()))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn key_window_id(&self) -> Option<usize> {
|
fn main_window_id(&self) -> Option<usize> {
|
||||||
Window::key_window_id()
|
Window::main_window_id()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn add_status_item(&self) -> Box<dyn platform::Window> {
|
fn add_status_item(&self) -> Box<dyn platform::Window> {
|
||||||
|
|
|
@ -604,12 +604,12 @@ impl Window {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn key_window_id() -> Option<usize> {
|
pub fn main_window_id() -> Option<usize> {
|
||||||
unsafe {
|
unsafe {
|
||||||
let app = NSApplication::sharedApplication(nil);
|
let app = NSApplication::sharedApplication(nil);
|
||||||
let key_window: id = msg_send![app, keyWindow];
|
let main_window: id = msg_send![app, mainWindow];
|
||||||
if msg_send![key_window, isKindOfClass: WINDOW_CLASS] {
|
if msg_send![main_window, isKindOfClass: WINDOW_CLASS] {
|
||||||
let id = get_window_state(&*key_window).borrow().id;
|
let id = get_window_state(&*main_window).borrow().id;
|
||||||
Some(id)
|
Some(id)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
|
|
|
@ -157,7 +157,7 @@ impl super::Platform for Platform {
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn key_window_id(&self) -> Option<usize> {
|
fn main_window_id(&self) -> Option<usize> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
18
crates/install_cli/Cargo.toml
Normal file
18
crates/install_cli/Cargo.toml
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
[package]
|
||||||
|
name = "install_cli"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
publish = false
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
path = "src/install_cli.rs"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
test-support = []
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
smol = "1.2.5"
|
||||||
|
anyhow = "1.0.38"
|
||||||
|
log = "0.4"
|
||||||
|
gpui = { path = "../gpui" }
|
||||||
|
util = { path = "../util" }
|
55
crates/install_cli/src/install_cli.rs
Normal file
55
crates/install_cli/src/install_cli.rs
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use anyhow::{anyhow, Result};
|
||||||
|
use gpui::{actions, AsyncAppContext};
|
||||||
|
use util::ResultExt;
|
||||||
|
|
||||||
|
actions!(cli, [Install]);
|
||||||
|
|
||||||
|
pub async fn install_cli(cx: &AsyncAppContext) -> Result<()> {
|
||||||
|
let cli_path = cx.platform().path_for_auxiliary_executable("cli")?;
|
||||||
|
let link_path = Path::new("/usr/local/bin/zed");
|
||||||
|
let bin_dir_path = link_path.parent().unwrap();
|
||||||
|
|
||||||
|
// Don't re-create symlink if it points to the same CLI binary.
|
||||||
|
if smol::fs::read_link(link_path).await.ok().as_ref() == Some(&cli_path) {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the symlink is not there or is outdated, first try replacing it
|
||||||
|
// without escalating.
|
||||||
|
smol::fs::remove_file(link_path).await.log_err();
|
||||||
|
if smol::fs::unix::symlink(&cli_path, link_path)
|
||||||
|
.await
|
||||||
|
.log_err()
|
||||||
|
.is_some()
|
||||||
|
{
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// The symlink could not be created, so use osascript with admin privileges
|
||||||
|
// to create it.
|
||||||
|
let status = smol::process::Command::new("osascript")
|
||||||
|
.args([
|
||||||
|
"-e",
|
||||||
|
&format!(
|
||||||
|
"do shell script \" \
|
||||||
|
mkdir -p \'{}\' && \
|
||||||
|
ln -sf \'{}\' \'{}\' \
|
||||||
|
\" with administrator privileges",
|
||||||
|
bin_dir_path.to_string_lossy(),
|
||||||
|
cli_path.to_string_lossy(),
|
||||||
|
link_path.to_string_lossy(),
|
||||||
|
),
|
||||||
|
])
|
||||||
|
.stdout(smol::process::Stdio::inherit())
|
||||||
|
.stderr(smol::process::Stdio::inherit())
|
||||||
|
.output()
|
||||||
|
.await?
|
||||||
|
.status;
|
||||||
|
if status.success() {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(anyhow!("error running osascript"))
|
||||||
|
}
|
||||||
|
}
|
|
@ -13,6 +13,7 @@ editor = { path = "../editor" }
|
||||||
gpui = { path = "../gpui" }
|
gpui = { path = "../gpui" }
|
||||||
util = { path = "../util" }
|
util = { path = "../util" }
|
||||||
workspace = { path = "../workspace" }
|
workspace = { path = "../workspace" }
|
||||||
|
anyhow = "1.0"
|
||||||
chrono = "0.4"
|
chrono = "0.4"
|
||||||
dirs = "4.0"
|
dirs = "4.0"
|
||||||
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
|
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
|
||||||
|
|
|
@ -48,7 +48,7 @@ pub fn new_journal_entry(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
|
||||||
async move {
|
async move {
|
||||||
let (journal_dir, entry_path) = create_entry.await?;
|
let (journal_dir, entry_path) = create_entry.await?;
|
||||||
let (workspace, _) = cx
|
let (workspace, _) = cx
|
||||||
.update(|cx| workspace::open_paths(&[journal_dir], &app_state, cx))
|
.update(|cx| workspace::open_paths(&[journal_dir], &app_state, None, cx))
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let opened = workspace
|
let opened = workspace
|
||||||
|
@ -73,7 +73,7 @@ pub fn new_journal_entry(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
anyhow::Ok(())
|
||||||
}
|
}
|
||||||
.log_err()
|
.log_err()
|
||||||
})
|
})
|
||||||
|
|
|
@ -1353,7 +1353,13 @@ impl Buffer {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn remove_active_selections(&mut self, cx: &mut ModelContext<Self>) {
|
pub fn remove_active_selections(&mut self, cx: &mut ModelContext<Self>) {
|
||||||
self.set_active_selections(Arc::from([]), false, Default::default(), cx);
|
if self
|
||||||
|
.remote_selections
|
||||||
|
.get(&self.text.replica_id())
|
||||||
|
.map_or(true, |set| !set.selections.is_empty())
|
||||||
|
{
|
||||||
|
self.set_active_selections(Arc::from([]), false, Default::default(), cx);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_text<T>(&mut self, text: T, cx: &mut ModelContext<Self>) -> Option<clock::Local>
|
pub fn set_text<T>(&mut self, text: T, cx: &mut ModelContext<Self>) -> Option<clock::Local>
|
||||||
|
|
|
@ -80,31 +80,49 @@ fn test_select_language() {
|
||||||
|
|
||||||
// matching file extension
|
// matching file extension
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
registry.language_for_path("zed/lib.rs").map(|l| l.name()),
|
registry
|
||||||
|
.language_for_path("zed/lib.rs")
|
||||||
|
.now_or_never()
|
||||||
|
.and_then(|l| Some(l.ok()?.name())),
|
||||||
Some("Rust".into())
|
Some("Rust".into())
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
registry.language_for_path("zed/lib.mk").map(|l| l.name()),
|
registry
|
||||||
|
.language_for_path("zed/lib.mk")
|
||||||
|
.now_or_never()
|
||||||
|
.and_then(|l| Some(l.ok()?.name())),
|
||||||
Some("Make".into())
|
Some("Make".into())
|
||||||
);
|
);
|
||||||
|
|
||||||
// matching filename
|
// matching filename
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
registry.language_for_path("zed/Makefile").map(|l| l.name()),
|
registry
|
||||||
|
.language_for_path("zed/Makefile")
|
||||||
|
.now_or_never()
|
||||||
|
.and_then(|l| Some(l.ok()?.name())),
|
||||||
Some("Make".into())
|
Some("Make".into())
|
||||||
);
|
);
|
||||||
|
|
||||||
// matching suffix that is not the full file extension or filename
|
// matching suffix that is not the full file extension or filename
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
registry.language_for_path("zed/cars").map(|l| l.name()),
|
registry
|
||||||
|
.language_for_path("zed/cars")
|
||||||
|
.now_or_never()
|
||||||
|
.and_then(|l| Some(l.ok()?.name())),
|
||||||
None
|
None
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
registry.language_for_path("zed/a.cars").map(|l| l.name()),
|
registry
|
||||||
|
.language_for_path("zed/a.cars")
|
||||||
|
.now_or_never()
|
||||||
|
.and_then(|l| Some(l.ok()?.name())),
|
||||||
None
|
None
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
registry.language_for_path("zed/sumk").map(|l| l.name()),
|
registry
|
||||||
|
.language_for_path("zed/sumk")
|
||||||
|
.now_or_never()
|
||||||
|
.and_then(|l| Some(l.ok()?.name())),
|
||||||
None
|
None
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -666,14 +684,14 @@ fn test_enclosing_bracket_ranges(cx: &mut MutableAppContext) {
|
||||||
indoc! {"
|
indoc! {"
|
||||||
mod x {
|
mod x {
|
||||||
moˇd y {
|
moˇd y {
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let foo = 1;"},
|
let foo = 1;"},
|
||||||
vec![indoc! {"
|
vec![indoc! {"
|
||||||
mod x «{»
|
mod x «{»
|
||||||
mod y {
|
mod y {
|
||||||
|
|
||||||
}
|
}
|
||||||
«}»
|
«}»
|
||||||
let foo = 1;"}],
|
let foo = 1;"}],
|
||||||
|
@ -683,7 +701,7 @@ fn test_enclosing_bracket_ranges(cx: &mut MutableAppContext) {
|
||||||
indoc! {"
|
indoc! {"
|
||||||
mod x {
|
mod x {
|
||||||
mod y ˇ{
|
mod y ˇ{
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let foo = 1;"},
|
let foo = 1;"},
|
||||||
|
@ -691,14 +709,14 @@ fn test_enclosing_bracket_ranges(cx: &mut MutableAppContext) {
|
||||||
indoc! {"
|
indoc! {"
|
||||||
mod x «{»
|
mod x «{»
|
||||||
mod y {
|
mod y {
|
||||||
|
|
||||||
}
|
}
|
||||||
«}»
|
«}»
|
||||||
let foo = 1;"},
|
let foo = 1;"},
|
||||||
indoc! {"
|
indoc! {"
|
||||||
mod x {
|
mod x {
|
||||||
mod y «{»
|
mod y «{»
|
||||||
|
|
||||||
«}»
|
«}»
|
||||||
}
|
}
|
||||||
let foo = 1;"},
|
let foo = 1;"},
|
||||||
|
@ -709,7 +727,7 @@ fn test_enclosing_bracket_ranges(cx: &mut MutableAppContext) {
|
||||||
indoc! {"
|
indoc! {"
|
||||||
mod x {
|
mod x {
|
||||||
mod y {
|
mod y {
|
||||||
|
|
||||||
}ˇ
|
}ˇ
|
||||||
}
|
}
|
||||||
let foo = 1;"},
|
let foo = 1;"},
|
||||||
|
@ -717,14 +735,14 @@ fn test_enclosing_bracket_ranges(cx: &mut MutableAppContext) {
|
||||||
indoc! {"
|
indoc! {"
|
||||||
mod x «{»
|
mod x «{»
|
||||||
mod y {
|
mod y {
|
||||||
|
|
||||||
}
|
}
|
||||||
«}»
|
«}»
|
||||||
let foo = 1;"},
|
let foo = 1;"},
|
||||||
indoc! {"
|
indoc! {"
|
||||||
mod x {
|
mod x {
|
||||||
mod y «{»
|
mod y «{»
|
||||||
|
|
||||||
«}»
|
«}»
|
||||||
}
|
}
|
||||||
let foo = 1;"},
|
let foo = 1;"},
|
||||||
|
@ -735,14 +753,14 @@ fn test_enclosing_bracket_ranges(cx: &mut MutableAppContext) {
|
||||||
indoc! {"
|
indoc! {"
|
||||||
mod x {
|
mod x {
|
||||||
mod y {
|
mod y {
|
||||||
|
|
||||||
}
|
}
|
||||||
ˇ}
|
ˇ}
|
||||||
let foo = 1;"},
|
let foo = 1;"},
|
||||||
vec![indoc! {"
|
vec![indoc! {"
|
||||||
mod x «{»
|
mod x «{»
|
||||||
mod y {
|
mod y {
|
||||||
|
|
||||||
}
|
}
|
||||||
«}»
|
«}»
|
||||||
let foo = 1;"}],
|
let foo = 1;"}],
|
||||||
|
@ -752,7 +770,7 @@ fn test_enclosing_bracket_ranges(cx: &mut MutableAppContext) {
|
||||||
indoc! {"
|
indoc! {"
|
||||||
mod x {
|
mod x {
|
||||||
mod y {
|
mod y {
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let fˇoo = 1;"},
|
let fˇoo = 1;"},
|
||||||
|
@ -764,7 +782,7 @@ fn test_enclosing_bracket_ranges(cx: &mut MutableAppContext) {
|
||||||
indoc! {"
|
indoc! {"
|
||||||
mod x {
|
mod x {
|
||||||
mod y {
|
mod y {
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let foo = 1;ˇ"},
|
let foo = 1;ˇ"},
|
||||||
|
@ -1804,25 +1822,31 @@ fn test_random_collaboration(cx: &mut MutableAppContext, mut rng: StdRng) {
|
||||||
}
|
}
|
||||||
30..=39 if mutation_count != 0 => {
|
30..=39 if mutation_count != 0 => {
|
||||||
buffer.update(cx, |buffer, cx| {
|
buffer.update(cx, |buffer, cx| {
|
||||||
let mut selections = Vec::new();
|
if rng.gen_bool(0.2) {
|
||||||
for id in 0..rng.gen_range(1..=5) {
|
log::info!("peer {} clearing active selections", replica_id);
|
||||||
let range = buffer.random_byte_range(0, &mut rng);
|
active_selections.remove(&replica_id);
|
||||||
selections.push(Selection {
|
buffer.remove_active_selections(cx);
|
||||||
id,
|
} else {
|
||||||
start: buffer.anchor_before(range.start),
|
let mut selections = Vec::new();
|
||||||
end: buffer.anchor_before(range.end),
|
for id in 0..rng.gen_range(1..=5) {
|
||||||
reversed: false,
|
let range = buffer.random_byte_range(0, &mut rng);
|
||||||
goal: SelectionGoal::None,
|
selections.push(Selection {
|
||||||
});
|
id,
|
||||||
|
start: buffer.anchor_before(range.start),
|
||||||
|
end: buffer.anchor_before(range.end),
|
||||||
|
reversed: false,
|
||||||
|
goal: SelectionGoal::None,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let selections: Arc<[Selection<Anchor>]> = selections.into();
|
||||||
|
log::info!(
|
||||||
|
"peer {} setting active selections: {:?}",
|
||||||
|
replica_id,
|
||||||
|
selections
|
||||||
|
);
|
||||||
|
active_selections.insert(replica_id, selections.clone());
|
||||||
|
buffer.set_active_selections(selections, false, Default::default(), cx);
|
||||||
}
|
}
|
||||||
let selections: Arc<[Selection<Anchor>]> = selections.into();
|
|
||||||
log::info!(
|
|
||||||
"peer {} setting active selections: {:?}",
|
|
||||||
replica_id,
|
|
||||||
selections
|
|
||||||
);
|
|
||||||
active_selections.insert(replica_id, selections.clone());
|
|
||||||
buffer.set_active_selections(selections, false, Default::default(), cx);
|
|
||||||
});
|
});
|
||||||
mutation_count -= 1;
|
mutation_count -= 1;
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,8 +13,9 @@ use async_trait::async_trait;
|
||||||
use client::http::HttpClient;
|
use client::http::HttpClient;
|
||||||
use collections::HashMap;
|
use collections::HashMap;
|
||||||
use futures::{
|
use futures::{
|
||||||
|
channel::oneshot,
|
||||||
future::{BoxFuture, Shared},
|
future::{BoxFuture, Shared},
|
||||||
FutureExt, TryFutureExt,
|
FutureExt, TryFutureExt as _,
|
||||||
};
|
};
|
||||||
use gpui::{executor::Background, MutableAppContext, Task};
|
use gpui::{executor::Background, MutableAppContext, Task};
|
||||||
use highlight_map::HighlightMap;
|
use highlight_map::HighlightMap;
|
||||||
|
@ -43,7 +44,7 @@ use syntax_map::SyntaxSnapshot;
|
||||||
use theme::{SyntaxTheme, Theme};
|
use theme::{SyntaxTheme, Theme};
|
||||||
use tree_sitter::{self, Query};
|
use tree_sitter::{self, Query};
|
||||||
use unicase::UniCase;
|
use unicase::UniCase;
|
||||||
use util::ResultExt;
|
use util::{ResultExt, TryFutureExt as _, UnwrapFuture};
|
||||||
|
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
use futures::channel::mpsc;
|
use futures::channel::mpsc;
|
||||||
|
@ -484,7 +485,7 @@ impl LanguageRegistry {
|
||||||
let (lsp_binary_statuses_tx, lsp_binary_statuses_rx) = async_broadcast::broadcast(16);
|
let (lsp_binary_statuses_tx, lsp_binary_statuses_rx) = async_broadcast::broadcast(16);
|
||||||
Self {
|
Self {
|
||||||
language_server_download_dir: None,
|
language_server_download_dir: None,
|
||||||
languages: Default::default(),
|
languages: RwLock::new(vec![PLAIN_TEXT.clone()]),
|
||||||
available_languages: Default::default(),
|
available_languages: Default::default(),
|
||||||
lsp_binary_statuses_tx,
|
lsp_binary_statuses_tx,
|
||||||
lsp_binary_statuses_rx,
|
lsp_binary_statuses_rx,
|
||||||
|
@ -568,12 +569,18 @@ impl LanguageRegistry {
|
||||||
self.language_server_download_dir = Some(path.into());
|
self.language_server_download_dir = Some(path.into());
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn language_for_name(self: &Arc<Self>, name: &str) -> Option<Arc<Language>> {
|
pub fn language_for_name(
|
||||||
|
self: &Arc<Self>,
|
||||||
|
name: &str,
|
||||||
|
) -> UnwrapFuture<oneshot::Receiver<Result<Arc<Language>>>> {
|
||||||
let name = UniCase::new(name);
|
let name = UniCase::new(name);
|
||||||
self.get_or_load_language(|config| UniCase::new(config.name.as_ref()) == name)
|
self.get_or_load_language(|config| UniCase::new(config.name.as_ref()) == name)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn language_for_name_or_extension(self: &Arc<Self>, string: &str) -> Option<Arc<Language>> {
|
pub fn language_for_name_or_extension(
|
||||||
|
self: &Arc<Self>,
|
||||||
|
string: &str,
|
||||||
|
) -> UnwrapFuture<oneshot::Receiver<Result<Arc<Language>>>> {
|
||||||
let string = UniCase::new(string);
|
let string = UniCase::new(string);
|
||||||
self.get_or_load_language(|config| {
|
self.get_or_load_language(|config| {
|
||||||
UniCase::new(config.name.as_ref()) == string
|
UniCase::new(config.name.as_ref()) == string
|
||||||
|
@ -584,7 +591,10 @@ impl LanguageRegistry {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn language_for_path(self: &Arc<Self>, path: impl AsRef<Path>) -> Option<Arc<Language>> {
|
pub fn language_for_path(
|
||||||
|
self: &Arc<Self>,
|
||||||
|
path: impl AsRef<Path>,
|
||||||
|
) -> UnwrapFuture<oneshot::Receiver<Result<Arc<Language>>>> {
|
||||||
let path = path.as_ref();
|
let path = path.as_ref();
|
||||||
let filename = path.file_name().and_then(|name| name.to_str());
|
let filename = path.file_name().and_then(|name| name.to_str());
|
||||||
let extension = path.extension().and_then(|name| name.to_str());
|
let extension = path.extension().and_then(|name| name.to_str());
|
||||||
|
@ -600,17 +610,17 @@ impl LanguageRegistry {
|
||||||
fn get_or_load_language(
|
fn get_or_load_language(
|
||||||
self: &Arc<Self>,
|
self: &Arc<Self>,
|
||||||
callback: impl Fn(&LanguageConfig) -> bool,
|
callback: impl Fn(&LanguageConfig) -> bool,
|
||||||
) -> Option<Arc<Language>> {
|
) -> UnwrapFuture<oneshot::Receiver<Result<Arc<Language>>>> {
|
||||||
|
let (tx, rx) = oneshot::channel();
|
||||||
|
|
||||||
if let Some(language) = self
|
if let Some(language) = self
|
||||||
.languages
|
.languages
|
||||||
.read()
|
.read()
|
||||||
.iter()
|
.iter()
|
||||||
.find(|language| callback(&language.config))
|
.find(|language| callback(&language.config))
|
||||||
{
|
{
|
||||||
return Some(language.clone());
|
let _ = tx.send(Ok(language.clone()));
|
||||||
}
|
} else if let Some(executor) = self.executor.clone() {
|
||||||
|
|
||||||
if let Some(executor) = self.executor.clone() {
|
|
||||||
let mut available_languages = self.available_languages.write();
|
let mut available_languages = self.available_languages.write();
|
||||||
|
|
||||||
if let Some(ix) = available_languages.iter().position(|l| callback(&l.config)) {
|
if let Some(ix) = available_languages.iter().position(|l| callback(&l.config)) {
|
||||||
|
@ -625,18 +635,29 @@ impl LanguageRegistry {
|
||||||
.with_lsp_adapter(language.lsp_adapter)
|
.with_lsp_adapter(language.lsp_adapter)
|
||||||
.await;
|
.await;
|
||||||
match language.with_queries(queries) {
|
match language.with_queries(queries) {
|
||||||
Ok(language) => this.add(Arc::new(language)),
|
Ok(language) => {
|
||||||
|
let language = Arc::new(language);
|
||||||
|
this.add(language.clone());
|
||||||
|
let _ = tx.send(Ok(language));
|
||||||
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
log::error!("failed to load language {}: {}", name, err);
|
let _ = tx.send(Err(anyhow!(
|
||||||
return;
|
"failed to load language {}: {}",
|
||||||
|
name,
|
||||||
|
err
|
||||||
|
)));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
|
} else {
|
||||||
|
let _ = tx.send(Err(anyhow!("language not found")));
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
let _ = tx.send(Err(anyhow!("executor does not exist")));
|
||||||
}
|
}
|
||||||
|
|
||||||
None
|
rx.unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn to_vec(&self) -> Vec<Arc<Language>> {
|
pub fn to_vec(&self) -> Vec<Arc<Language>> {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
use crate::{Grammar, InjectionConfig, Language, LanguageRegistry};
|
use crate::{Grammar, InjectionConfig, Language, LanguageRegistry};
|
||||||
use collections::HashMap;
|
use collections::HashMap;
|
||||||
|
use futures::FutureExt;
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use std::{
|
use std::{
|
||||||
|
@ -382,11 +383,11 @@ impl SyntaxSnapshot {
|
||||||
cursor.next(text);
|
cursor.next(text);
|
||||||
while let Some(layer) = cursor.item() {
|
while let Some(layer) = cursor.item() {
|
||||||
let SyntaxLayerContent::Pending { language_name } = &layer.content else { unreachable!() };
|
let SyntaxLayerContent::Pending { language_name } = &layer.content else { unreachable!() };
|
||||||
if {
|
if registry
|
||||||
let language_registry = ®istry;
|
.language_for_name_or_extension(language_name)
|
||||||
language_registry.language_for_name_or_extension(language_name)
|
.now_or_never()
|
||||||
}
|
.and_then(|language| language.ok())
|
||||||
.is_some()
|
.is_some()
|
||||||
{
|
{
|
||||||
resolved_injection_ranges.push(layer.range.to_offset(text));
|
resolved_injection_ranges.push(layer.range.to_offset(text));
|
||||||
}
|
}
|
||||||
|
@ -1116,7 +1117,10 @@ fn get_injections(
|
||||||
combined_injection_ranges.clear();
|
combined_injection_ranges.clear();
|
||||||
for pattern in &config.patterns {
|
for pattern in &config.patterns {
|
||||||
if let (Some(language_name), true) = (pattern.language.as_ref(), pattern.combined) {
|
if let (Some(language_name), true) = (pattern.language.as_ref(), pattern.combined) {
|
||||||
if let Some(language) = language_registry.language_for_name_or_extension(language_name)
|
if let Some(language) = language_registry
|
||||||
|
.language_for_name_or_extension(language_name)
|
||||||
|
.now_or_never()
|
||||||
|
.and_then(|language| language.ok())
|
||||||
{
|
{
|
||||||
combined_injection_ranges.insert(language, Vec::new());
|
combined_injection_ranges.insert(language, Vec::new());
|
||||||
}
|
}
|
||||||
|
@ -1162,10 +1166,10 @@ fn get_injections(
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(language_name) = language_name {
|
if let Some(language_name) = language_name {
|
||||||
let language = {
|
let language = language_registry
|
||||||
let language_name: &str = &language_name;
|
.language_for_name_or_extension(&language_name)
|
||||||
language_registry.language_for_name_or_extension(language_name)
|
.now_or_never()
|
||||||
};
|
.and_then(|language| language.ok());
|
||||||
let range = text.anchor_before(step_range.start)..text.anchor_after(step_range.end);
|
let range = text.anchor_before(step_range.start)..text.anchor_after(step_range.end);
|
||||||
if let Some(language) = language {
|
if let Some(language) = language {
|
||||||
if combined {
|
if combined {
|
||||||
|
@ -2522,7 +2526,11 @@ mod tests {
|
||||||
registry.add(Arc::new(html_lang()));
|
registry.add(Arc::new(html_lang()));
|
||||||
registry.add(Arc::new(erb_lang()));
|
registry.add(Arc::new(erb_lang()));
|
||||||
registry.add(Arc::new(markdown_lang()));
|
registry.add(Arc::new(markdown_lang()));
|
||||||
let language = registry.language_for_name(language_name).unwrap();
|
let language = registry
|
||||||
|
.language_for_name(language_name)
|
||||||
|
.now_or_never()
|
||||||
|
.unwrap()
|
||||||
|
.unwrap();
|
||||||
let mut buffer = Buffer::new(0, 0, Default::default());
|
let mut buffer = Buffer::new(0, 0, Default::default());
|
||||||
|
|
||||||
let mut mutated_syntax_map = SyntaxMap::new();
|
let mut mutated_syntax_map = SyntaxMap::new();
|
||||||
|
|
21
crates/language_selector/Cargo.toml
Normal file
21
crates/language_selector/Cargo.toml
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
[package]
|
||||||
|
name = "language_selector"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
publish = false
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
path = "src/language_selector.rs"
|
||||||
|
doctest = false
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
editor = { path = "../editor" }
|
||||||
|
fuzzy = { path = "../fuzzy" }
|
||||||
|
language = { path = "../language" }
|
||||||
|
gpui = { path = "../gpui" }
|
||||||
|
picker = { path = "../picker" }
|
||||||
|
project = { path = "../project" }
|
||||||
|
theme = { path = "../theme" }
|
||||||
|
settings = { path = "../settings" }
|
||||||
|
workspace = { path = "../workspace" }
|
||||||
|
anyhow = "1.0"
|
87
crates/language_selector/src/active_buffer_language.rs
Normal file
87
crates/language_selector/src/active_buffer_language.rs
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
use editor::Editor;
|
||||||
|
use gpui::{
|
||||||
|
elements::*, CursorStyle, Entity, MouseButton, RenderContext, Subscription, View, ViewContext,
|
||||||
|
ViewHandle,
|
||||||
|
};
|
||||||
|
use settings::Settings;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use workspace::{item::ItemHandle, StatusItemView};
|
||||||
|
|
||||||
|
pub struct ActiveBufferLanguage {
|
||||||
|
active_language: Option<Arc<str>>,
|
||||||
|
_observe_active_editor: Option<Subscription>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ActiveBufferLanguage {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ActiveBufferLanguage {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
active_language: None,
|
||||||
|
_observe_active_editor: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_language(&mut self, editor: ViewHandle<Editor>, cx: &mut ViewContext<Self>) {
|
||||||
|
self.active_language.take();
|
||||||
|
|
||||||
|
let editor = editor.read(cx);
|
||||||
|
if let Some((_, buffer, _)) = editor.active_excerpt(cx) {
|
||||||
|
if let Some(language) = buffer.read(cx).language() {
|
||||||
|
self.active_language = Some(language.name());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Entity for ActiveBufferLanguage {
|
||||||
|
type Event = ();
|
||||||
|
}
|
||||||
|
|
||||||
|
impl View for ActiveBufferLanguage {
|
||||||
|
fn ui_name() -> &'static str {
|
||||||
|
"ActiveBufferLanguage"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||||
|
if let Some(active_language) = self.active_language.as_ref() {
|
||||||
|
MouseEventHandler::<Self>::new(0, cx, |state, cx| {
|
||||||
|
let theme = &cx.global::<Settings>().theme.workspace.status_bar;
|
||||||
|
let style = theme.active_language.style_for(state, false);
|
||||||
|
Label::new(active_language.to_string(), style.text.clone())
|
||||||
|
.contained()
|
||||||
|
.with_style(style.container)
|
||||||
|
.boxed()
|
||||||
|
})
|
||||||
|
.with_cursor_style(CursorStyle::PointingHand)
|
||||||
|
.on_click(MouseButton::Left, |_, cx| cx.dispatch_action(crate::Toggle))
|
||||||
|
.boxed()
|
||||||
|
} else {
|
||||||
|
Empty::new().boxed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StatusItemView for ActiveBufferLanguage {
|
||||||
|
fn set_active_pane_item(
|
||||||
|
&mut self,
|
||||||
|
active_pane_item: Option<&dyn ItemHandle>,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) {
|
||||||
|
if let Some(editor) = active_pane_item.and_then(|item| item.act_as::<Editor>(cx)) {
|
||||||
|
self._observe_active_editor = Some(cx.observe(&editor, Self::update_language));
|
||||||
|
self.update_language(editor, cx);
|
||||||
|
} else {
|
||||||
|
self.active_language = None;
|
||||||
|
self._observe_active_editor = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
}
|
230
crates/language_selector/src/language_selector.rs
Normal file
230
crates/language_selector/src/language_selector.rs
Normal file
|
@ -0,0 +1,230 @@
|
||||||
|
mod active_buffer_language;
|
||||||
|
|
||||||
|
pub use active_buffer_language::ActiveBufferLanguage;
|
||||||
|
use editor::Editor;
|
||||||
|
use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
|
||||||
|
use gpui::{
|
||||||
|
actions, elements::*, AnyViewHandle, AppContext, Entity, ModelHandle, MouseState,
|
||||||
|
MutableAppContext, RenderContext, View, ViewContext, ViewHandle,
|
||||||
|
};
|
||||||
|
use language::{Buffer, LanguageRegistry};
|
||||||
|
use picker::{Picker, PickerDelegate};
|
||||||
|
use project::Project;
|
||||||
|
use settings::Settings;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use workspace::{AppState, Workspace};
|
||||||
|
|
||||||
|
actions!(language_selector, [Toggle]);
|
||||||
|
|
||||||
|
pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
|
||||||
|
Picker::<LanguageSelector>::init(cx);
|
||||||
|
cx.add_action({
|
||||||
|
let language_registry = app_state.languages.clone();
|
||||||
|
move |workspace, _: &Toggle, cx| {
|
||||||
|
LanguageSelector::toggle(workspace, language_registry.clone(), cx)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum Event {
|
||||||
|
Dismissed,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct LanguageSelector {
|
||||||
|
buffer: ModelHandle<Buffer>,
|
||||||
|
project: ModelHandle<Project>,
|
||||||
|
language_registry: Arc<LanguageRegistry>,
|
||||||
|
candidates: Vec<StringMatchCandidate>,
|
||||||
|
matches: Vec<StringMatch>,
|
||||||
|
picker: ViewHandle<Picker<Self>>,
|
||||||
|
selected_index: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LanguageSelector {
|
||||||
|
fn new(
|
||||||
|
buffer: ModelHandle<Buffer>,
|
||||||
|
project: ModelHandle<Project>,
|
||||||
|
language_registry: Arc<LanguageRegistry>,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) -> Self {
|
||||||
|
let handle = cx.weak_handle();
|
||||||
|
let picker = cx.add_view(|cx| Picker::new("Select Language...", handle, cx));
|
||||||
|
|
||||||
|
let candidates = language_registry
|
||||||
|
.language_names()
|
||||||
|
.into_iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(candidate_id, name)| StringMatchCandidate::new(candidate_id, name))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let mut matches = candidates
|
||||||
|
.iter()
|
||||||
|
.map(|candidate| StringMatch {
|
||||||
|
candidate_id: candidate.id,
|
||||||
|
score: 0.,
|
||||||
|
positions: Default::default(),
|
||||||
|
string: candidate.string.clone(),
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
matches.sort_unstable_by(|mat1, mat2| mat1.string.cmp(&mat2.string));
|
||||||
|
|
||||||
|
Self {
|
||||||
|
buffer,
|
||||||
|
project,
|
||||||
|
language_registry,
|
||||||
|
candidates,
|
||||||
|
matches,
|
||||||
|
picker,
|
||||||
|
selected_index: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn toggle(
|
||||||
|
workspace: &mut Workspace,
|
||||||
|
registry: Arc<LanguageRegistry>,
|
||||||
|
cx: &mut ViewContext<Workspace>,
|
||||||
|
) {
|
||||||
|
if let Some((_, buffer, _)) = workspace
|
||||||
|
.active_item(cx)
|
||||||
|
.and_then(|active_item| active_item.act_as::<Editor>(cx))
|
||||||
|
.and_then(|editor| editor.read(cx).active_excerpt(cx))
|
||||||
|
{
|
||||||
|
workspace.toggle_modal(cx, |workspace, cx| {
|
||||||
|
let project = workspace.project().clone();
|
||||||
|
let this = cx.add_view(|cx| Self::new(buffer, project, registry, cx));
|
||||||
|
cx.subscribe(&this, Self::on_event).detach();
|
||||||
|
this
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_event(
|
||||||
|
workspace: &mut Workspace,
|
||||||
|
_: ViewHandle<LanguageSelector>,
|
||||||
|
event: &Event,
|
||||||
|
cx: &mut ViewContext<Workspace>,
|
||||||
|
) {
|
||||||
|
match event {
|
||||||
|
Event::Dismissed => {
|
||||||
|
workspace.dismiss_modal(cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Entity for LanguageSelector {
|
||||||
|
type Event = Event;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl View for LanguageSelector {
|
||||||
|
fn ui_name() -> &'static str {
|
||||||
|
"LanguageSelector"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||||
|
ChildView::new(self.picker.clone(), cx).boxed()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||||
|
if cx.is_self_focused() {
|
||||||
|
cx.focus(&self.picker);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PickerDelegate for LanguageSelector {
|
||||||
|
fn match_count(&self) -> usize {
|
||||||
|
self.matches.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn confirm(&mut self, cx: &mut ViewContext<Self>) {
|
||||||
|
if let Some(mat) = self.matches.get(self.selected_index) {
|
||||||
|
let language_name = &self.candidates[mat.candidate_id].string;
|
||||||
|
let language = self.language_registry.language_for_name(language_name);
|
||||||
|
cx.spawn(|this, mut cx| async move {
|
||||||
|
let language = language.await?;
|
||||||
|
this.update(&mut cx, |this, cx| {
|
||||||
|
this.project.update(cx, |project, cx| {
|
||||||
|
project.set_language_for_buffer(&this.buffer, language, cx);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
anyhow::Ok(())
|
||||||
|
})
|
||||||
|
.detach_and_log_err(cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
cx.emit(Event::Dismissed);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
|
||||||
|
cx.emit(Event::Dismissed);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn selected_index(&self) -> usize {
|
||||||
|
self.selected_index
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext<Self>) {
|
||||||
|
self.selected_index = ix;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Self>) -> gpui::Task<()> {
|
||||||
|
let background = cx.background().clone();
|
||||||
|
let candidates = self.candidates.clone();
|
||||||
|
cx.spawn(|this, mut cx| async move {
|
||||||
|
let matches = if query.is_empty() {
|
||||||
|
candidates
|
||||||
|
.into_iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(index, candidate)| StringMatch {
|
||||||
|
candidate_id: index,
|
||||||
|
string: candidate.string,
|
||||||
|
positions: Vec::new(),
|
||||||
|
score: 0.0,
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
} else {
|
||||||
|
match_strings(
|
||||||
|
&candidates,
|
||||||
|
&query,
|
||||||
|
false,
|
||||||
|
100,
|
||||||
|
&Default::default(),
|
||||||
|
background,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
};
|
||||||
|
|
||||||
|
this.update(&mut cx, |this, cx| {
|
||||||
|
this.matches = matches;
|
||||||
|
this.selected_index = this
|
||||||
|
.selected_index
|
||||||
|
.min(this.matches.len().saturating_sub(1));
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_match(
|
||||||
|
&self,
|
||||||
|
ix: usize,
|
||||||
|
mouse_state: &mut MouseState,
|
||||||
|
selected: bool,
|
||||||
|
cx: &AppContext,
|
||||||
|
) -> ElementBox {
|
||||||
|
let settings = cx.global::<Settings>();
|
||||||
|
let theme = &settings.theme;
|
||||||
|
let mat = &self.matches[ix];
|
||||||
|
let style = theme.picker.item.style_for(mouse_state, selected);
|
||||||
|
let buffer_language_name = self.buffer.read(cx).language().map(|l| l.name());
|
||||||
|
let mut label = mat.string.clone();
|
||||||
|
if buffer_language_name.as_deref() == Some(mat.string.as_str()) {
|
||||||
|
label.push_str(" (current)");
|
||||||
|
}
|
||||||
|
|
||||||
|
Label::new(label, style.label.clone())
|
||||||
|
.with_highlights(mat.positions.clone())
|
||||||
|
.contained()
|
||||||
|
.with_style(style.container)
|
||||||
|
.boxed()
|
||||||
|
}
|
||||||
|
}
|
|
@ -160,15 +160,13 @@ impl LanguageServer {
|
||||||
server: Option<Child>,
|
server: Option<Child>,
|
||||||
root_path: &Path,
|
root_path: &Path,
|
||||||
cx: AsyncAppContext,
|
cx: AsyncAppContext,
|
||||||
mut on_unhandled_notification: F,
|
on_unhandled_notification: F,
|
||||||
) -> Self
|
) -> Self
|
||||||
where
|
where
|
||||||
Stdin: AsyncWrite + Unpin + Send + 'static,
|
Stdin: AsyncWrite + Unpin + Send + 'static,
|
||||||
Stdout: AsyncRead + Unpin + Send + 'static,
|
Stdout: AsyncRead + Unpin + Send + 'static,
|
||||||
F: FnMut(AnyNotification) + 'static + Send,
|
F: FnMut(AnyNotification) + 'static + Send,
|
||||||
{
|
{
|
||||||
let mut stdin = BufWriter::new(stdin);
|
|
||||||
let mut stdout = BufReader::new(stdout);
|
|
||||||
let (outbound_tx, outbound_rx) = channel::unbounded::<Vec<u8>>();
|
let (outbound_tx, outbound_rx) = channel::unbounded::<Vec<u8>>();
|
||||||
let notification_handlers =
|
let notification_handlers =
|
||||||
Arc::new(Mutex::new(HashMap::<_, NotificationHandler>::default()));
|
Arc::new(Mutex::new(HashMap::<_, NotificationHandler>::default()));
|
||||||
|
@ -177,89 +175,19 @@ impl LanguageServer {
|
||||||
let input_task = cx.spawn(|cx| {
|
let input_task = cx.spawn(|cx| {
|
||||||
let notification_handlers = notification_handlers.clone();
|
let notification_handlers = notification_handlers.clone();
|
||||||
let response_handlers = response_handlers.clone();
|
let response_handlers = response_handlers.clone();
|
||||||
async move {
|
Self::handle_input(
|
||||||
let _clear_response_handlers = util::defer({
|
stdout,
|
||||||
let response_handlers = response_handlers.clone();
|
on_unhandled_notification,
|
||||||
move || {
|
notification_handlers,
|
||||||
response_handlers.lock().take();
|
response_handlers,
|
||||||
}
|
cx,
|
||||||
});
|
)
|
||||||
let mut buffer = Vec::new();
|
|
||||||
loop {
|
|
||||||
buffer.clear();
|
|
||||||
stdout.read_until(b'\n', &mut buffer).await?;
|
|
||||||
stdout.read_until(b'\n', &mut buffer).await?;
|
|
||||||
let message_len: usize = std::str::from_utf8(&buffer)?
|
|
||||||
.strip_prefix(CONTENT_LEN_HEADER)
|
|
||||||
.ok_or_else(|| anyhow!("invalid header"))?
|
|
||||||
.trim_end()
|
|
||||||
.parse()?;
|
|
||||||
|
|
||||||
buffer.resize(message_len, 0);
|
|
||||||
stdout.read_exact(&mut buffer).await?;
|
|
||||||
log::trace!("incoming message:{}", String::from_utf8_lossy(&buffer));
|
|
||||||
|
|
||||||
if let Ok(msg) = serde_json::from_slice::<AnyNotification>(&buffer) {
|
|
||||||
if let Some(handler) = notification_handlers.lock().get_mut(msg.method) {
|
|
||||||
handler(msg.id, msg.params.get(), cx.clone());
|
|
||||||
} else {
|
|
||||||
on_unhandled_notification(msg);
|
|
||||||
}
|
|
||||||
} else if let Ok(AnyResponse {
|
|
||||||
id, error, result, ..
|
|
||||||
}) = serde_json::from_slice(&buffer)
|
|
||||||
{
|
|
||||||
if let Some(handler) = response_handlers
|
|
||||||
.lock()
|
|
||||||
.as_mut()
|
|
||||||
.and_then(|handlers| handlers.remove(&id))
|
|
||||||
{
|
|
||||||
if let Some(error) = error {
|
|
||||||
handler(Err(error));
|
|
||||||
} else if let Some(result) = result {
|
|
||||||
handler(Ok(result.get()));
|
|
||||||
} else {
|
|
||||||
handler(Ok("null"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
warn!(
|
|
||||||
"Failed to deserialize message:\n{}",
|
|
||||||
std::str::from_utf8(&buffer)?
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Don't starve the main thread when receiving lots of messages at once.
|
|
||||||
smol::future::yield_now().await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.log_err()
|
.log_err()
|
||||||
});
|
});
|
||||||
let (output_done_tx, output_done_rx) = barrier::channel();
|
let (output_done_tx, output_done_rx) = barrier::channel();
|
||||||
let output_task = cx.background().spawn({
|
let output_task = cx.background().spawn({
|
||||||
let response_handlers = response_handlers.clone();
|
let response_handlers = response_handlers.clone();
|
||||||
async move {
|
Self::handle_output(stdin, outbound_rx, output_done_tx, response_handlers).log_err()
|
||||||
let _clear_response_handlers = util::defer({
|
|
||||||
let response_handlers = response_handlers.clone();
|
|
||||||
move || {
|
|
||||||
response_handlers.lock().take();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
let mut content_len_buffer = Vec::new();
|
|
||||||
while let Ok(message) = outbound_rx.recv().await {
|
|
||||||
log::trace!("outgoing message:{}", String::from_utf8_lossy(&message));
|
|
||||||
content_len_buffer.clear();
|
|
||||||
write!(content_len_buffer, "{}", message.len()).unwrap();
|
|
||||||
stdin.write_all(CONTENT_LEN_HEADER.as_bytes()).await?;
|
|
||||||
stdin.write_all(&content_len_buffer).await?;
|
|
||||||
stdin.write_all("\r\n\r\n".as_bytes()).await?;
|
|
||||||
stdin.write_all(&message).await?;
|
|
||||||
stdin.flush().await?;
|
|
||||||
}
|
|
||||||
drop(output_done_tx);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
.log_err()
|
|
||||||
});
|
});
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
|
@ -278,6 +206,105 @@ impl LanguageServer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn handle_input<Stdout, F>(
|
||||||
|
stdout: Stdout,
|
||||||
|
mut on_unhandled_notification: F,
|
||||||
|
notification_handlers: Arc<Mutex<HashMap<&'static str, NotificationHandler>>>,
|
||||||
|
response_handlers: Arc<Mutex<Option<HashMap<usize, ResponseHandler>>>>,
|
||||||
|
cx: AsyncAppContext,
|
||||||
|
) -> anyhow::Result<()>
|
||||||
|
where
|
||||||
|
Stdout: AsyncRead + Unpin + Send + 'static,
|
||||||
|
F: FnMut(AnyNotification) + 'static + Send,
|
||||||
|
{
|
||||||
|
let mut stdout = BufReader::new(stdout);
|
||||||
|
let _clear_response_handlers = util::defer({
|
||||||
|
let response_handlers = response_handlers.clone();
|
||||||
|
move || {
|
||||||
|
response_handlers.lock().take();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let mut buffer = Vec::new();
|
||||||
|
loop {
|
||||||
|
buffer.clear();
|
||||||
|
stdout.read_until(b'\n', &mut buffer).await?;
|
||||||
|
stdout.read_until(b'\n', &mut buffer).await?;
|
||||||
|
let message_len: usize = std::str::from_utf8(&buffer)?
|
||||||
|
.strip_prefix(CONTENT_LEN_HEADER)
|
||||||
|
.ok_or_else(|| anyhow!("invalid header"))?
|
||||||
|
.trim_end()
|
||||||
|
.parse()?;
|
||||||
|
|
||||||
|
buffer.resize(message_len, 0);
|
||||||
|
stdout.read_exact(&mut buffer).await?;
|
||||||
|
log::trace!("incoming message:{}", String::from_utf8_lossy(&buffer));
|
||||||
|
|
||||||
|
if let Ok(msg) = serde_json::from_slice::<AnyNotification>(&buffer) {
|
||||||
|
if let Some(handler) = notification_handlers.lock().get_mut(msg.method) {
|
||||||
|
handler(msg.id, msg.params.get(), cx.clone());
|
||||||
|
} else {
|
||||||
|
on_unhandled_notification(msg);
|
||||||
|
}
|
||||||
|
} else if let Ok(AnyResponse {
|
||||||
|
id, error, result, ..
|
||||||
|
}) = serde_json::from_slice(&buffer)
|
||||||
|
{
|
||||||
|
if let Some(handler) = response_handlers
|
||||||
|
.lock()
|
||||||
|
.as_mut()
|
||||||
|
.and_then(|handlers| handlers.remove(&id))
|
||||||
|
{
|
||||||
|
if let Some(error) = error {
|
||||||
|
handler(Err(error));
|
||||||
|
} else if let Some(result) = result {
|
||||||
|
handler(Ok(result.get()));
|
||||||
|
} else {
|
||||||
|
handler(Ok("null"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
warn!(
|
||||||
|
"Failed to deserialize message:\n{}",
|
||||||
|
std::str::from_utf8(&buffer)?
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't starve the main thread when receiving lots of messages at once.
|
||||||
|
smol::future::yield_now().await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_output<Stdin>(
|
||||||
|
stdin: Stdin,
|
||||||
|
outbound_rx: channel::Receiver<Vec<u8>>,
|
||||||
|
output_done_tx: barrier::Sender,
|
||||||
|
response_handlers: Arc<Mutex<Option<HashMap<usize, ResponseHandler>>>>,
|
||||||
|
) -> anyhow::Result<()>
|
||||||
|
where
|
||||||
|
Stdin: AsyncWrite + Unpin + Send + 'static,
|
||||||
|
{
|
||||||
|
let mut stdin = BufWriter::new(stdin);
|
||||||
|
let _clear_response_handlers = util::defer({
|
||||||
|
let response_handlers = response_handlers.clone();
|
||||||
|
move || {
|
||||||
|
response_handlers.lock().take();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let mut content_len_buffer = Vec::new();
|
||||||
|
while let Ok(message) = outbound_rx.recv().await {
|
||||||
|
log::trace!("outgoing message:{}", String::from_utf8_lossy(&message));
|
||||||
|
content_len_buffer.clear();
|
||||||
|
write!(content_len_buffer, "{}", message.len()).unwrap();
|
||||||
|
stdin.write_all(CONTENT_LEN_HEADER.as_bytes()).await?;
|
||||||
|
stdin.write_all(&content_len_buffer).await?;
|
||||||
|
stdin.write_all("\r\n\r\n".as_bytes()).await?;
|
||||||
|
stdin.write_all(&message).await?;
|
||||||
|
stdin.flush().await?;
|
||||||
|
}
|
||||||
|
drop(output_done_tx);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Initializes a language server.
|
/// Initializes a language server.
|
||||||
/// Note that `options` is used directly to construct [`InitializeParams`],
|
/// Note that `options` is used directly to construct [`InitializeParams`],
|
||||||
/// which is why it is owned.
|
/// which is why it is owned.
|
||||||
|
@ -389,7 +416,7 @@ impl LanguageServer {
|
||||||
output_done.recv().await;
|
output_done.recv().await;
|
||||||
log::debug!("language server shutdown finished");
|
log::debug!("language server shutdown finished");
|
||||||
drop(tasks);
|
drop(tasks);
|
||||||
Ok(())
|
anyhow::Ok(())
|
||||||
}
|
}
|
||||||
.log_err(),
|
.log_err(),
|
||||||
)
|
)
|
||||||
|
|
|
@ -102,7 +102,10 @@ impl<D: PickerDelegate> View for Picker<D> {
|
||||||
.read(cx)
|
.read(cx)
|
||||||
.render_match(ix, state, ix == selected_ix, cx)
|
.render_match(ix, state, ix == selected_ix, cx)
|
||||||
})
|
})
|
||||||
.on_down(MouseButton::Left, move |_, cx| {
|
// Capture mouse events
|
||||||
|
.on_down(MouseButton::Left, |_, _| {})
|
||||||
|
.on_up(MouseButton::Left, |_, _| {})
|
||||||
|
.on_click(MouseButton::Left, move |_, cx| {
|
||||||
cx.dispatch_action(SelectIndex(ix))
|
cx.dispatch_action(SelectIndex(ix))
|
||||||
})
|
})
|
||||||
.with_cursor_style(CursorStyle::PointingHand)
|
.with_cursor_style(CursorStyle::PointingHand)
|
||||||
|
@ -205,6 +208,11 @@ impl<D: PickerDelegate> Picker<D> {
|
||||||
self.query_editor.read(cx).text(cx)
|
self.query_editor.read(cx).text(cx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_query(&self, query: impl Into<Arc<str>>, cx: &mut ViewContext<Self>) {
|
||||||
|
self.query_editor
|
||||||
|
.update(cx, |editor, cx| editor.set_text(query, cx));
|
||||||
|
}
|
||||||
|
|
||||||
fn on_query_editor_event(
|
fn on_query_editor_event(
|
||||||
&mut self,
|
&mut self,
|
||||||
_: ViewHandle<Editor>,
|
_: ViewHandle<Editor>,
|
||||||
|
|
|
@ -1464,7 +1464,7 @@ impl Project {
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
this.update(&mut cx, |this, cx| {
|
this.update(&mut cx, |this, cx| {
|
||||||
this.assign_language_to_buffer(&buffer, cx);
|
this.detect_language_for_buffer(&buffer, cx);
|
||||||
this.register_buffer_with_language_server(&buffer, cx);
|
this.register_buffer_with_language_server(&buffer, cx);
|
||||||
});
|
});
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -1531,7 +1531,7 @@ impl Project {
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
|
|
||||||
self.assign_language_to_buffer(buffer, cx);
|
self.detect_language_for_buffer(buffer, cx);
|
||||||
self.register_buffer_with_language_server(buffer, cx);
|
self.register_buffer_with_language_server(buffer, cx);
|
||||||
cx.observe_release(buffer, |this, buffer, cx| {
|
cx.observe_release(buffer, |this, buffer, cx| {
|
||||||
if let Some(file) = File::from_dyn(buffer.file()) {
|
if let Some(file) = File::from_dyn(buffer.file()) {
|
||||||
|
@ -1818,7 +1818,7 @@ impl Project {
|
||||||
}
|
}
|
||||||
|
|
||||||
for buffer in plain_text_buffers {
|
for buffer in plain_text_buffers {
|
||||||
project.assign_language_to_buffer(&buffer, cx);
|
project.detect_language_for_buffer(&buffer, cx);
|
||||||
project.register_buffer_with_language_server(&buffer, cx);
|
project.register_buffer_with_language_server(&buffer, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1831,14 +1831,28 @@ impl Project {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn assign_language_to_buffer(
|
fn detect_language_for_buffer(
|
||||||
&mut self,
|
&mut self,
|
||||||
buffer: &ModelHandle<Buffer>,
|
buffer: &ModelHandle<Buffer>,
|
||||||
cx: &mut ModelContext<Self>,
|
cx: &mut ModelContext<Self>,
|
||||||
) -> Option<()> {
|
) -> Option<()> {
|
||||||
// If the buffer has a language, set it and start the language server if we haven't already.
|
// If the buffer has a language, set it and start the language server if we haven't already.
|
||||||
let full_path = buffer.read(cx).file()?.full_path(cx);
|
let full_path = buffer.read(cx).file()?.full_path(cx);
|
||||||
let new_language = self.languages.language_for_path(&full_path)?;
|
let new_language = self
|
||||||
|
.languages
|
||||||
|
.language_for_path(&full_path)
|
||||||
|
.now_or_never()?
|
||||||
|
.ok()?;
|
||||||
|
self.set_language_for_buffer(buffer, new_language, cx);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_language_for_buffer(
|
||||||
|
&mut self,
|
||||||
|
buffer: &ModelHandle<Buffer>,
|
||||||
|
new_language: Arc<Language>,
|
||||||
|
cx: &mut ModelContext<Self>,
|
||||||
|
) {
|
||||||
buffer.update(cx, |buffer, cx| {
|
buffer.update(cx, |buffer, cx| {
|
||||||
if buffer.language().map_or(true, |old_language| {
|
if buffer.language().map_or(true, |old_language| {
|
||||||
!Arc::ptr_eq(old_language, &new_language)
|
!Arc::ptr_eq(old_language, &new_language)
|
||||||
|
@ -1847,13 +1861,13 @@ impl Project {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let file = File::from_dyn(buffer.read(cx).file())?;
|
if let Some(file) = File::from_dyn(buffer.read(cx).file()) {
|
||||||
let worktree = file.worktree.read(cx).as_local()?;
|
if let Some(worktree) = file.worktree.read(cx).as_local() {
|
||||||
let worktree_id = worktree.id();
|
let worktree_id = worktree.id();
|
||||||
let worktree_abs_path = worktree.abs_path().clone();
|
let worktree_abs_path = worktree.abs_path().clone();
|
||||||
self.start_language_server(worktree_id, worktree_abs_path, new_language, cx);
|
self.start_language_server(worktree_id, worktree_abs_path, new_language, cx);
|
||||||
|
}
|
||||||
None
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn merge_json_value_into(source: serde_json::Value, target: &mut serde_json::Value) {
|
fn merge_json_value_into(source: serde_json::Value, target: &mut serde_json::Value) {
|
||||||
|
@ -2248,8 +2262,14 @@ impl Project {
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
for (worktree_id, worktree_abs_path, full_path) in language_server_lookup_info {
|
for (worktree_id, worktree_abs_path, full_path) in language_server_lookup_info {
|
||||||
let language = self.languages.language_for_path(&full_path)?;
|
if let Some(language) = self
|
||||||
self.restart_language_server(worktree_id, worktree_abs_path, language, cx);
|
.languages
|
||||||
|
.language_for_path(&full_path)
|
||||||
|
.now_or_never()
|
||||||
|
.and_then(|language| language.ok())
|
||||||
|
{
|
||||||
|
self.restart_language_server(worktree_id, worktree_abs_path, language, cx);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
None
|
None
|
||||||
|
@ -3278,12 +3298,14 @@ impl Project {
|
||||||
path: path.into(),
|
path: path.into(),
|
||||||
};
|
};
|
||||||
let signature = this.symbol_signature(&project_path);
|
let signature = this.symbol_signature(&project_path);
|
||||||
|
let adapter_language = adapter_language.clone();
|
||||||
let language = this
|
let language = this
|
||||||
.languages
|
.languages
|
||||||
.language_for_path(&project_path.path)
|
.language_for_path(&project_path.path)
|
||||||
.unwrap_or(adapter_language.clone());
|
.unwrap_or_else(move |_| adapter_language);
|
||||||
let language_server_name = adapter.name.clone();
|
let language_server_name = adapter.name.clone();
|
||||||
Some(async move {
|
Some(async move {
|
||||||
|
let language = language.await;
|
||||||
let label = language
|
let label = language
|
||||||
.label_for_symbol(&lsp_symbol.name, lsp_symbol.kind)
|
.label_for_symbol(&lsp_symbol.name, lsp_symbol.kind)
|
||||||
.await;
|
.await;
|
||||||
|
@ -4541,7 +4563,7 @@ impl Project {
|
||||||
|
|
||||||
for (buffer, old_path) in renamed_buffers {
|
for (buffer, old_path) in renamed_buffers {
|
||||||
self.unregister_buffer_from_language_server(&buffer, old_path, cx);
|
self.unregister_buffer_from_language_server(&buffer, old_path, cx);
|
||||||
self.assign_language_to_buffer(&buffer, cx);
|
self.detect_language_for_buffer(&buffer, cx);
|
||||||
self.register_buffer_with_language_server(&buffer, cx);
|
self.register_buffer_with_language_server(&buffer, cx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5210,7 +5232,7 @@ impl Project {
|
||||||
buffer.update(cx, |buffer, cx| {
|
buffer.update(cx, |buffer, cx| {
|
||||||
buffer.file_updated(Arc::new(file), cx).detach();
|
buffer.file_updated(Arc::new(file), cx).detach();
|
||||||
});
|
});
|
||||||
this.assign_language_to_buffer(&buffer, cx);
|
this.detect_language_for_buffer(&buffer, cx);
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
|
@ -5831,7 +5853,7 @@ impl Project {
|
||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
anyhow::Ok(())
|
||||||
}
|
}
|
||||||
.log_err(),
|
.log_err(),
|
||||||
)
|
)
|
||||||
|
@ -6060,7 +6082,7 @@ impl Project {
|
||||||
worktree_id,
|
worktree_id,
|
||||||
path: PathBuf::from(serialized_symbol.path).into(),
|
path: PathBuf::from(serialized_symbol.path).into(),
|
||||||
};
|
};
|
||||||
let language = languages.language_for_path(&path.path);
|
let language = languages.language_for_path(&path.path).await.log_err();
|
||||||
Ok(Symbol {
|
Ok(Symbol {
|
||||||
language_server_name: LanguageServerName(
|
language_server_name: LanguageServerName(
|
||||||
serialized_symbol.language_server_name.into(),
|
serialized_symbol.language_server_name.into(),
|
||||||
|
|
|
@ -867,7 +867,7 @@ impl LocalWorktree {
|
||||||
let old_path = self.entry_for_id(entry_id)?.path.clone();
|
let old_path = self.entry_for_id(entry_id)?.path.clone();
|
||||||
let new_path = new_path.into();
|
let new_path = new_path.into();
|
||||||
let abs_old_path = self.absolutize(&old_path);
|
let abs_old_path = self.absolutize(&old_path);
|
||||||
let abs_new_path = self.absolutize(&new_path);
|
let abs_new_path = self.absolutize(new_path.as_ref());
|
||||||
let rename = cx.background().spawn({
|
let rename = cx.background().spawn({
|
||||||
let fs = self.fs.clone();
|
let fs = self.fs.clone();
|
||||||
let abs_new_path = abs_new_path.clone();
|
let abs_new_path = abs_new_path.clone();
|
||||||
|
@ -2361,7 +2361,7 @@ impl BackgroundScanner {
|
||||||
job: &ScanJob,
|
job: &ScanJob,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let mut new_entries: Vec<Entry> = Vec::new();
|
let mut new_entries: Vec<Entry> = Vec::new();
|
||||||
let mut new_jobs: Vec<ScanJob> = Vec::new();
|
let mut new_jobs: Vec<Option<ScanJob>> = Vec::new();
|
||||||
let mut ignore_stack = job.ignore_stack.clone();
|
let mut ignore_stack = job.ignore_stack.clone();
|
||||||
let mut new_ignore = None;
|
let mut new_ignore = None;
|
||||||
|
|
||||||
|
@ -2374,6 +2374,7 @@ impl BackgroundScanner {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let child_name = child_abs_path.file_name().unwrap();
|
let child_name = child_abs_path.file_name().unwrap();
|
||||||
let child_path: Arc<Path> = job.path.join(child_name).into();
|
let child_path: Arc<Path> = job.path.join(child_name).into();
|
||||||
let child_metadata = match self.fs.metadata(&child_abs_path).await {
|
let child_metadata = match self.fs.metadata(&child_abs_path).await {
|
||||||
|
@ -2412,12 +2413,15 @@ impl BackgroundScanner {
|
||||||
let entry_abs_path = self.abs_path().join(&entry.path);
|
let entry_abs_path = self.abs_path().join(&entry.path);
|
||||||
entry.is_ignored =
|
entry.is_ignored =
|
||||||
ignore_stack.is_abs_path_ignored(&entry_abs_path, entry.is_dir());
|
ignore_stack.is_abs_path_ignored(&entry_abs_path, entry.is_dir());
|
||||||
|
|
||||||
if entry.is_dir() {
|
if entry.is_dir() {
|
||||||
new_jobs.next().unwrap().ignore_stack = if entry.is_ignored {
|
if let Some(job) = new_jobs.next().expect("Missing scan job for entry") {
|
||||||
IgnoreStack::all()
|
job.ignore_stack = if entry.is_ignored {
|
||||||
} else {
|
IgnoreStack::all()
|
||||||
ignore_stack.clone()
|
} else {
|
||||||
};
|
ignore_stack.clone()
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2433,10 +2437,12 @@ impl BackgroundScanner {
|
||||||
let is_ignored = ignore_stack.is_abs_path_ignored(&child_abs_path, true);
|
let is_ignored = ignore_stack.is_abs_path_ignored(&child_abs_path, true);
|
||||||
child_entry.is_ignored = is_ignored;
|
child_entry.is_ignored = is_ignored;
|
||||||
|
|
||||||
|
// Avoid recursing until crash in the case of a recursive symlink
|
||||||
if !job.ancestor_inodes.contains(&child_entry.inode) {
|
if !job.ancestor_inodes.contains(&child_entry.inode) {
|
||||||
let mut ancestor_inodes = job.ancestor_inodes.clone();
|
let mut ancestor_inodes = job.ancestor_inodes.clone();
|
||||||
ancestor_inodes.insert(child_entry.inode);
|
ancestor_inodes.insert(child_entry.inode);
|
||||||
new_jobs.push(ScanJob {
|
|
||||||
|
new_jobs.push(Some(ScanJob {
|
||||||
abs_path: child_abs_path,
|
abs_path: child_abs_path,
|
||||||
path: child_path,
|
path: child_path,
|
||||||
ignore_stack: if is_ignored {
|
ignore_stack: if is_ignored {
|
||||||
|
@ -2446,7 +2452,9 @@ impl BackgroundScanner {
|
||||||
},
|
},
|
||||||
ancestor_inodes,
|
ancestor_inodes,
|
||||||
scan_queue: job.scan_queue.clone(),
|
scan_queue: job.scan_queue.clone(),
|
||||||
});
|
}));
|
||||||
|
} else {
|
||||||
|
new_jobs.push(None);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
child_entry.is_ignored = ignore_stack.is_abs_path_ignored(&child_abs_path, false);
|
child_entry.is_ignored = ignore_stack.is_abs_path_ignored(&child_abs_path, false);
|
||||||
|
@ -2461,8 +2469,11 @@ impl BackgroundScanner {
|
||||||
new_ignore,
|
new_ignore,
|
||||||
self.fs.as_ref(),
|
self.fs.as_ref(),
|
||||||
);
|
);
|
||||||
|
|
||||||
for new_job in new_jobs {
|
for new_job in new_jobs {
|
||||||
job.scan_queue.send(new_job).await.unwrap();
|
if let Some(new_job) = new_job {
|
||||||
|
job.scan_queue.send(new_job).await.unwrap();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
@ -1262,54 +1262,89 @@ impl View for ProjectPanel {
|
||||||
let padding = std::mem::take(&mut container_style.padding);
|
let padding = std::mem::take(&mut container_style.padding);
|
||||||
let last_worktree_root_id = self.last_worktree_root_id;
|
let last_worktree_root_id = self.last_worktree_root_id;
|
||||||
|
|
||||||
Stack::new()
|
let has_worktree = self.visible_entries.len() != 0;
|
||||||
.with_child(
|
|
||||||
MouseEventHandler::<ProjectPanel>::new(0, cx, |_, cx| {
|
if has_worktree {
|
||||||
UniformList::new(
|
Stack::new()
|
||||||
self.list.clone(),
|
.with_child(
|
||||||
self.visible_entries
|
MouseEventHandler::<ProjectPanel>::new(0, cx, |_, cx| {
|
||||||
.iter()
|
UniformList::new(
|
||||||
.map(|(_, worktree_entries)| worktree_entries.len())
|
self.list.clone(),
|
||||||
.sum(),
|
self.visible_entries
|
||||||
cx,
|
.iter()
|
||||||
move |this, range, items, cx| {
|
.map(|(_, worktree_entries)| worktree_entries.len())
|
||||||
let theme = cx.global::<Settings>().theme.clone();
|
.sum(),
|
||||||
let mut dragged_entry_destination =
|
cx,
|
||||||
this.dragged_entry_destination.clone();
|
move |this, range, items, cx| {
|
||||||
this.for_each_visible_entry(range, cx, |id, details, cx| {
|
let theme = cx.global::<Settings>().theme.clone();
|
||||||
items.push(Self::render_entry(
|
let mut dragged_entry_destination =
|
||||||
id,
|
this.dragged_entry_destination.clone();
|
||||||
details,
|
this.for_each_visible_entry(range, cx, |id, details, cx| {
|
||||||
&this.filename_editor,
|
items.push(Self::render_entry(
|
||||||
&mut dragged_entry_destination,
|
id,
|
||||||
&theme.project_panel,
|
details,
|
||||||
cx,
|
&this.filename_editor,
|
||||||
));
|
&mut dragged_entry_destination,
|
||||||
});
|
&theme.project_panel,
|
||||||
this.dragged_entry_destination = dragged_entry_destination;
|
cx,
|
||||||
},
|
));
|
||||||
)
|
});
|
||||||
.with_padding_top(padding.top)
|
this.dragged_entry_destination = dragged_entry_destination;
|
||||||
.with_padding_bottom(padding.bottom)
|
},
|
||||||
.contained()
|
)
|
||||||
.with_style(container_style)
|
.with_padding_top(padding.top)
|
||||||
.expanded()
|
.with_padding_bottom(padding.bottom)
|
||||||
.boxed()
|
.contained()
|
||||||
})
|
.with_style(container_style)
|
||||||
.on_down(MouseButton::Right, move |e, cx| {
|
.expanded()
|
||||||
// When deploying the context menu anywhere below the last project entry,
|
.boxed()
|
||||||
// act as if the user clicked the root of the last worktree.
|
})
|
||||||
if let Some(entry_id) = last_worktree_root_id {
|
.on_down(MouseButton::Right, move |e, cx| {
|
||||||
cx.dispatch_action(DeployContextMenu {
|
// When deploying the context menu anywhere below the last project entry,
|
||||||
entry_id,
|
// act as if the user clicked the root of the last worktree.
|
||||||
position: e.position,
|
if let Some(entry_id) = last_worktree_root_id {
|
||||||
})
|
cx.dispatch_action(DeployContextMenu {
|
||||||
}
|
entry_id,
|
||||||
})
|
position: e.position,
|
||||||
.boxed(),
|
})
|
||||||
)
|
}
|
||||||
.with_child(ChildView::new(&self.context_menu, cx).boxed())
|
})
|
||||||
.boxed()
|
.boxed(),
|
||||||
|
)
|
||||||
|
.with_child(ChildView::new(&self.context_menu, cx).boxed())
|
||||||
|
.boxed()
|
||||||
|
} else {
|
||||||
|
Flex::column()
|
||||||
|
.with_child(
|
||||||
|
MouseEventHandler::<Self>::new(2, cx, {
|
||||||
|
let button_style = theme.open_project_button.clone();
|
||||||
|
let context_menu_item_style =
|
||||||
|
cx.global::<Settings>().theme.context_menu.item.clone();
|
||||||
|
move |state, cx| {
|
||||||
|
let button_style = button_style.style_for(state, false).clone();
|
||||||
|
let context_menu_item =
|
||||||
|
context_menu_item_style.style_for(state, true).clone();
|
||||||
|
|
||||||
|
theme::ui::keystroke_label(
|
||||||
|
"Open a project",
|
||||||
|
&button_style,
|
||||||
|
&context_menu_item.keystroke,
|
||||||
|
Box::new(workspace::Open),
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
.boxed()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.on_click(MouseButton::Left, move |_, cx| {
|
||||||
|
cx.dispatch_action(workspace::Open)
|
||||||
|
})
|
||||||
|
.with_cursor_style(CursorStyle::PointingHand)
|
||||||
|
.boxed(),
|
||||||
|
)
|
||||||
|
.contained()
|
||||||
|
.with_style(container_style)
|
||||||
|
.boxed()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn keymap_context(&self, _: &AppContext) -> KeymapContext {
|
fn keymap_context(&self, _: &AppContext) -> KeymapContext {
|
||||||
|
@ -1404,15 +1439,7 @@ mod tests {
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
|
let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
|
||||||
let (_, workspace) = cx.add_window(|cx| {
|
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
|
||||||
Workspace::new(
|
|
||||||
Default::default(),
|
|
||||||
0,
|
|
||||||
project.clone(),
|
|
||||||
|_, _| unimplemented!(),
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
});
|
|
||||||
let panel = workspace.update(cx, |_, cx| ProjectPanel::new(project, cx));
|
let panel = workspace.update(cx, |_, cx| ProjectPanel::new(project, cx));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
visible_entries_as_strings(&panel, 0..50, cx),
|
visible_entries_as_strings(&panel, 0..50, cx),
|
||||||
|
@ -1504,15 +1531,7 @@ mod tests {
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
|
let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
|
||||||
let (_, workspace) = cx.add_window(|cx| {
|
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
|
||||||
Workspace::new(
|
|
||||||
Default::default(),
|
|
||||||
0,
|
|
||||||
project.clone(),
|
|
||||||
|_, _| unimplemented!(),
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
});
|
|
||||||
let panel = workspace.update(cx, |_, cx| ProjectPanel::new(project, cx));
|
let panel = workspace.update(cx, |_, cx| ProjectPanel::new(project, cx));
|
||||||
|
|
||||||
select_path(&panel, "root1", cx);
|
select_path(&panel, "root1", cx);
|
||||||
|
|
|
@ -114,7 +114,7 @@ pub struct ConnectionState {
|
||||||
|
|
||||||
const KEEPALIVE_INTERVAL: Duration = Duration::from_secs(1);
|
const KEEPALIVE_INTERVAL: Duration = Duration::from_secs(1);
|
||||||
const WRITE_TIMEOUT: Duration = Duration::from_secs(2);
|
const WRITE_TIMEOUT: Duration = Duration::from_secs(2);
|
||||||
pub const RECEIVE_TIMEOUT: Duration = Duration::from_secs(5);
|
pub const RECEIVE_TIMEOUT: Duration = Duration::from_secs(10);
|
||||||
|
|
||||||
impl Peer {
|
impl Peer {
|
||||||
pub fn new(epoch: u32) -> Arc<Self> {
|
pub fn new(epoch: u32) -> Arc<Self> {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use crate::parse_json_with_comments;
|
use crate::{parse_json_with_comments, Settings};
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use assets::Assets;
|
use assets::Assets;
|
||||||
use collections::BTreeMap;
|
use collections::BTreeMap;
|
||||||
|
@ -45,6 +45,10 @@ impl KeymapFileContent {
|
||||||
for path in ["keymaps/default.json", "keymaps/vim.json"] {
|
for path in ["keymaps/default.json", "keymaps/vim.json"] {
|
||||||
Self::load(path, cx).unwrap();
|
Self::load(path, cx).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(asset_path) = cx.global::<Settings>().base_keymap.asset_path() {
|
||||||
|
Self::load(asset_path, cx).log_err();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn load(asset_path: &str, cx: &mut MutableAppContext) -> Result<()> {
|
pub fn load(asset_path: &str, cx: &mut MutableAppContext) -> Result<()> {
|
||||||
|
|
|
@ -18,12 +18,13 @@ use sqlez::{
|
||||||
bindable::{Bind, Column, StaticColumnCount},
|
bindable::{Bind, Column, StaticColumnCount},
|
||||||
statement::Statement,
|
statement::Statement,
|
||||||
};
|
};
|
||||||
use std::{collections::HashMap, fmt::Write as _, num::NonZeroU32, str, sync::Arc};
|
use std::{collections::HashMap, num::NonZeroU32, str, sync::Arc};
|
||||||
use theme::{Theme, ThemeRegistry};
|
use theme::{Theme, ThemeRegistry};
|
||||||
use tree_sitter::Query;
|
use tree_sitter::Query;
|
||||||
use util::ResultExt as _;
|
use util::ResultExt as _;
|
||||||
|
|
||||||
pub use keymap_file::{keymap_file_json_schema, KeymapFileContent};
|
pub use keymap_file::{keymap_file_json_schema, KeymapFileContent};
|
||||||
|
pub use watched_json::watch_files;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Settings {
|
pub struct Settings {
|
||||||
|
@ -54,6 +55,46 @@ pub struct Settings {
|
||||||
pub telemetry_defaults: TelemetrySettings,
|
pub telemetry_defaults: TelemetrySettings,
|
||||||
pub telemetry_overrides: TelemetrySettings,
|
pub telemetry_overrides: TelemetrySettings,
|
||||||
pub auto_update: bool,
|
pub auto_update: bool,
|
||||||
|
pub base_keymap: BaseKeymap,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)]
|
||||||
|
pub enum BaseKeymap {
|
||||||
|
#[default]
|
||||||
|
VSCode,
|
||||||
|
JetBrains,
|
||||||
|
Sublime,
|
||||||
|
Atom,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BaseKeymap {
|
||||||
|
pub const OPTIONS: [(&'static str, Self); 4] = [
|
||||||
|
("VSCode (Default)", Self::VSCode),
|
||||||
|
("Atom", Self::Atom),
|
||||||
|
("JetBrains", Self::JetBrains),
|
||||||
|
("Sublime", Self::Sublime),
|
||||||
|
];
|
||||||
|
|
||||||
|
pub fn asset_path(&self) -> Option<&'static str> {
|
||||||
|
match self {
|
||||||
|
BaseKeymap::JetBrains => Some("keymaps/jetbrains.json"),
|
||||||
|
BaseKeymap::Sublime => Some("keymaps/sublime_text.json"),
|
||||||
|
BaseKeymap::Atom => Some("keymaps/atom.json"),
|
||||||
|
BaseKeymap::VSCode => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn names() -> impl Iterator<Item = &'static str> {
|
||||||
|
Self::OPTIONS.iter().map(|(name, _)| *name)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_names(option: &str) -> BaseKeymap {
|
||||||
|
Self::OPTIONS
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.find_map(|(name, value)| (name == option).then(|| value))
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
|
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
|
||||||
|
@ -66,9 +107,18 @@ impl TelemetrySettings {
|
||||||
pub fn metrics(&self) -> bool {
|
pub fn metrics(&self) -> bool {
|
||||||
self.metrics.unwrap()
|
self.metrics.unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn diagnostics(&self) -> bool {
|
pub fn diagnostics(&self) -> bool {
|
||||||
self.diagnostics.unwrap()
|
self.diagnostics.unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_metrics(&mut self, value: bool) {
|
||||||
|
self.metrics = Some(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_diagnostics(&mut self, value: bool) {
|
||||||
|
self.diagnostics = Some(value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
|
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
|
||||||
|
@ -317,6 +367,8 @@ pub struct SettingsFileContent {
|
||||||
pub telemetry: TelemetrySettings,
|
pub telemetry: TelemetrySettings,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub auto_update: Option<bool>,
|
pub auto_update: Option<bool>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub base_keymap: Option<BaseKeymap>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||||
|
@ -387,6 +439,7 @@ impl Settings {
|
||||||
telemetry_defaults: defaults.telemetry,
|
telemetry_defaults: defaults.telemetry,
|
||||||
telemetry_overrides: Default::default(),
|
telemetry_overrides: Default::default(),
|
||||||
auto_update: defaults.auto_update.unwrap(),
|
auto_update: defaults.auto_update.unwrap(),
|
||||||
|
base_keymap: Default::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -424,6 +477,7 @@ impl Settings {
|
||||||
merge(&mut self.vim_mode, data.vim_mode);
|
merge(&mut self.vim_mode, data.vim_mode);
|
||||||
merge(&mut self.autosave, data.autosave);
|
merge(&mut self.autosave, data.autosave);
|
||||||
merge(&mut self.default_dock_anchor, data.default_dock_anchor);
|
merge(&mut self.default_dock_anchor, data.default_dock_anchor);
|
||||||
|
merge(&mut self.base_keymap, data.base_keymap);
|
||||||
|
|
||||||
// Ensure terminal font is loaded, so we can request it in terminal_element layout
|
// Ensure terminal font is loaded, so we can request it in terminal_element layout
|
||||||
if let Some(terminal_font) = &data.terminal.font_family {
|
if let Some(terminal_font) = &data.terminal.font_family {
|
||||||
|
@ -601,6 +655,7 @@ impl Settings {
|
||||||
},
|
},
|
||||||
telemetry_overrides: Default::default(),
|
telemetry_overrides: Default::default(),
|
||||||
auto_update: true,
|
auto_update: true,
|
||||||
|
base_keymap: Default::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -677,13 +732,19 @@ pub fn settings_file_json_schema(
|
||||||
serde_json::to_value(root_schema).unwrap()
|
serde_json::to_value(root_schema).unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Expects the key to be unquoted, and the value to be valid JSON
|
fn merge<T: Copy>(target: &mut T, value: Option<T>) {
|
||||||
/// (e.g. values should be unquoted for numbers and bools, quoted for strings)
|
if let Some(value) = value {
|
||||||
pub fn write_top_level_setting(
|
*target = value;
|
||||||
mut settings_content: String,
|
}
|
||||||
top_level_key: &str,
|
}
|
||||||
new_val: &str,
|
|
||||||
) -> String {
|
pub fn parse_json_with_comments<T: DeserializeOwned>(content: &str) -> Result<T> {
|
||||||
|
Ok(serde_json::from_reader(
|
||||||
|
json_comments::CommentSettings::c_style().strip_comments(content.as_bytes()),
|
||||||
|
)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_settings_key(settings_content: &mut String, key_path: &[&str], new_value: &Value) {
|
||||||
let mut parser = tree_sitter::Parser::new();
|
let mut parser = tree_sitter::Parser::new();
|
||||||
parser.set_language(tree_sitter_json::language()).unwrap();
|
parser.set_language(tree_sitter_json::language()).unwrap();
|
||||||
let tree = parser.parse(&settings_content, None).unwrap();
|
let tree = parser.parse(&settings_content, None).unwrap();
|
||||||
|
@ -693,56 +754,64 @@ pub fn write_top_level_setting(
|
||||||
let query = Query::new(
|
let query = Query::new(
|
||||||
tree_sitter_json::language(),
|
tree_sitter_json::language(),
|
||||||
"
|
"
|
||||||
(document
|
(pair
|
||||||
(object
|
key: (string) @key
|
||||||
(pair
|
value: (_) @value)
|
||||||
key: (string) @key
|
",
|
||||||
value: (_) @value)))
|
|
||||||
",
|
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
let mut depth = 0;
|
||||||
let mut first_key_start = None;
|
let mut first_key_start = None;
|
||||||
let mut existing_value_range = None;
|
let mut existing_value_range = 0..settings_content.len();
|
||||||
let matches = cursor.matches(&query, tree.root_node(), settings_content.as_bytes());
|
let matches = cursor.matches(&query, tree.root_node(), settings_content.as_bytes());
|
||||||
for mat in matches {
|
for mat in matches {
|
||||||
if mat.captures.len() != 2 {
|
if mat.captures.len() != 2 {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let key = mat.captures[0];
|
let key_range = mat.captures[0].node.byte_range();
|
||||||
let value = mat.captures[1];
|
let value_range = mat.captures[1].node.byte_range();
|
||||||
|
|
||||||
first_key_start.get_or_insert_with(|| key.node.start_byte());
|
if key_range.start > existing_value_range.end {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(key_text) = settings_content.get(key.node.byte_range()) {
|
first_key_start.get_or_insert_with(|| key_range.start);
|
||||||
if key_text == format!("\"{top_level_key}\"") {
|
|
||||||
existing_value_range = Some(value.node.byte_range());
|
let found_key = settings_content
|
||||||
|
.get(key_range.clone())
|
||||||
|
.map(|key_text| key_text == format!("\"{}\"", key_path[depth]))
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
|
if found_key {
|
||||||
|
existing_value_range = value_range;
|
||||||
|
depth += 1;
|
||||||
|
|
||||||
|
if depth == key_path.len() {
|
||||||
break;
|
break;
|
||||||
|
} else {
|
||||||
|
first_key_start = None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
match (first_key_start, existing_value_range) {
|
// We found the exact key we want, insert the new value
|
||||||
(None, None) => {
|
if depth == key_path.len() {
|
||||||
// No document, create a new object and overwrite
|
let new_val = serde_json::to_string_pretty(new_value)
|
||||||
settings_content.clear();
|
.expect("Could not serialize new json field to string");
|
||||||
write!(
|
settings_content.replace_range(existing_value_range, &new_val);
|
||||||
settings_content,
|
} else {
|
||||||
"{{\n \"{}\": {new_val}\n}}\n",
|
// We have key paths, construct the sub objects
|
||||||
top_level_key
|
let new_key = key_path[depth];
|
||||||
)
|
|
||||||
.unwrap();
|
// We don't have the key, construct the nested objects
|
||||||
|
let mut new_value = serde_json::to_value(new_value).unwrap();
|
||||||
|
for key in key_path[(depth + 1)..].iter().rev() {
|
||||||
|
new_value = serde_json::json!({ key.to_string(): new_value });
|
||||||
}
|
}
|
||||||
|
|
||||||
(_, Some(existing_value_range)) => {
|
if let Some(first_key_start) = first_key_start {
|
||||||
// Existing theme key, overwrite
|
|
||||||
settings_content.replace_range(existing_value_range, &new_val);
|
|
||||||
}
|
|
||||||
|
|
||||||
(Some(first_key_start), None) => {
|
|
||||||
// No existing theme key, but other settings. Prepend new theme settings and
|
|
||||||
// match style of first key
|
|
||||||
let mut row = 0;
|
let mut row = 0;
|
||||||
let mut column = 0;
|
let mut column = 0;
|
||||||
for (ix, char) in settings_content.char_indices() {
|
for (ix, char) in settings_content.char_indices() {
|
||||||
|
@ -757,142 +826,374 @@ pub fn write_top_level_setting(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let content = format!(r#""{top_level_key}": {new_val},"#);
|
|
||||||
settings_content.insert_str(first_key_start, &content);
|
|
||||||
|
|
||||||
if row > 0 {
|
if row > 0 {
|
||||||
|
let new_val = to_pretty_json(&new_value, column, column);
|
||||||
|
let content = format!(r#""{new_key}": {new_val},"#);
|
||||||
|
settings_content.insert_str(first_key_start, &content);
|
||||||
|
|
||||||
settings_content.insert_str(
|
settings_content.insert_str(
|
||||||
first_key_start + content.len(),
|
first_key_start + content.len(),
|
||||||
&format!("\n{:width$}", ' ', width = column),
|
&format!("\n{:width$}", ' ', width = column),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
settings_content.insert_str(first_key_start + content.len(), " ")
|
let new_val = serde_json::to_string(&new_value).unwrap();
|
||||||
|
let mut content = format!(r#""{new_key}": {new_val},"#);
|
||||||
|
content.push(' ');
|
||||||
|
settings_content.insert_str(first_key_start, &content);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
new_value = serde_json::json!({ new_key.to_string(): new_value });
|
||||||
|
let indent_prefix_len = 4 * depth;
|
||||||
|
let new_val = to_pretty_json(&new_value, 4, indent_prefix_len);
|
||||||
|
|
||||||
|
settings_content.replace_range(existing_value_range, &new_val);
|
||||||
|
if depth == 0 {
|
||||||
|
settings_content.push('\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_pretty_json(
|
||||||
|
value: &serde_json::Value,
|
||||||
|
indent_size: usize,
|
||||||
|
indent_prefix_len: usize,
|
||||||
|
) -> String {
|
||||||
|
const SPACES: [u8; 32] = [b' '; 32];
|
||||||
|
|
||||||
|
debug_assert!(indent_size <= SPACES.len());
|
||||||
|
debug_assert!(indent_prefix_len <= SPACES.len());
|
||||||
|
|
||||||
|
let mut output = Vec::new();
|
||||||
|
let mut ser = serde_json::Serializer::with_formatter(
|
||||||
|
&mut output,
|
||||||
|
serde_json::ser::PrettyFormatter::with_indent(&SPACES[0..indent_size.min(SPACES.len())]),
|
||||||
|
);
|
||||||
|
|
||||||
|
value.serialize(&mut ser).unwrap();
|
||||||
|
let text = String::from_utf8(output).unwrap();
|
||||||
|
|
||||||
|
let mut adjusted_text = String::new();
|
||||||
|
for (i, line) in text.split('\n').enumerate() {
|
||||||
|
if i > 0 {
|
||||||
|
adjusted_text.push_str(str::from_utf8(&SPACES[0..indent_prefix_len]).unwrap());
|
||||||
|
}
|
||||||
|
adjusted_text.push_str(line);
|
||||||
|
adjusted_text.push('\n');
|
||||||
|
}
|
||||||
|
adjusted_text.pop();
|
||||||
|
adjusted_text
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_settings_file(
|
||||||
|
mut text: String,
|
||||||
|
old_file_content: SettingsFileContent,
|
||||||
|
update: impl FnOnce(&mut SettingsFileContent),
|
||||||
|
) -> String {
|
||||||
|
let mut new_file_content = old_file_content.clone();
|
||||||
|
|
||||||
|
update(&mut new_file_content);
|
||||||
|
|
||||||
|
let old_object = to_json_object(old_file_content);
|
||||||
|
let new_object = to_json_object(new_file_content);
|
||||||
|
|
||||||
|
fn apply_changes_to_json_text(
|
||||||
|
old_object: &serde_json::Map<String, Value>,
|
||||||
|
new_object: &serde_json::Map<String, Value>,
|
||||||
|
current_key_path: Vec<&str>,
|
||||||
|
json_text: &mut String,
|
||||||
|
) {
|
||||||
|
for (key, old_value) in old_object.iter() {
|
||||||
|
// We know that these two are from the same shape of object, so we can just unwrap
|
||||||
|
let new_value = new_object.get(key).unwrap();
|
||||||
|
if old_value != new_value {
|
||||||
|
match new_value {
|
||||||
|
Value::Bool(_) | Value::Number(_) | Value::String(_) => {
|
||||||
|
let mut key_path = current_key_path.clone();
|
||||||
|
key_path.push(key);
|
||||||
|
write_settings_key(json_text, &key_path, &new_value);
|
||||||
|
}
|
||||||
|
Value::Object(new_sub_object) => {
|
||||||
|
let mut key_path = current_key_path.clone();
|
||||||
|
key_path.push(key);
|
||||||
|
if let Value::Object(old_sub_object) = old_value {
|
||||||
|
apply_changes_to_json_text(
|
||||||
|
old_sub_object,
|
||||||
|
new_sub_object,
|
||||||
|
key_path,
|
||||||
|
json_text,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
unimplemented!("This function doesn't support changing values from simple values to objects yet");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Value::Null | Value::Array(_) => {
|
||||||
|
unimplemented!("We only support objects and simple values");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
settings_content
|
apply_changes_to_json_text(&old_object, &new_object, vec![], &mut text);
|
||||||
|
|
||||||
|
text
|
||||||
}
|
}
|
||||||
|
|
||||||
fn merge<T: Copy>(target: &mut T, value: Option<T>) {
|
fn to_json_object(settings_file: SettingsFileContent) -> serde_json::Map<String, Value> {
|
||||||
if let Some(value) = value {
|
let tmp = serde_json::to_value(settings_file).unwrap();
|
||||||
*target = value;
|
match tmp {
|
||||||
|
Value::Object(map) => map,
|
||||||
|
_ => unreachable!("SettingsFileContent represents a JSON map"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse_json_with_comments<T: DeserializeOwned>(content: &str) -> Result<T> {
|
|
||||||
Ok(serde_json::from_reader(
|
|
||||||
json_comments::CommentSettings::c_style().strip_comments(content.as_bytes()),
|
|
||||||
)?)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::write_top_level_setting;
|
use super::*;
|
||||||
use unindent::Unindent;
|
use unindent::Unindent;
|
||||||
|
|
||||||
|
fn assert_new_settings<S1: Into<String>, S2: Into<String>>(
|
||||||
|
old_json: S1,
|
||||||
|
update: fn(&mut SettingsFileContent),
|
||||||
|
expected_new_json: S2,
|
||||||
|
) {
|
||||||
|
let old_json = old_json.into();
|
||||||
|
let old_content: SettingsFileContent = serde_json::from_str(&old_json).unwrap_or_default();
|
||||||
|
let new_json = update_settings_file(old_json, old_content, update);
|
||||||
|
assert_eq!(new_json, expected_new_json.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_update_telemetry_setting_multiple_fields() {
|
||||||
|
assert_new_settings(
|
||||||
|
r#"
|
||||||
|
{
|
||||||
|
"telemetry": {
|
||||||
|
"metrics": false,
|
||||||
|
"diagnostics": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"#
|
||||||
|
.unindent(),
|
||||||
|
|settings| {
|
||||||
|
settings.telemetry.set_diagnostics(true);
|
||||||
|
settings.telemetry.set_metrics(true);
|
||||||
|
},
|
||||||
|
r#"
|
||||||
|
{
|
||||||
|
"telemetry": {
|
||||||
|
"metrics": true,
|
||||||
|
"diagnostics": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"#
|
||||||
|
.unindent(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_update_telemetry_setting_weird_formatting() {
|
||||||
|
assert_new_settings(
|
||||||
|
r#"{
|
||||||
|
"telemetry": { "metrics": false, "diagnostics": true }
|
||||||
|
}"#
|
||||||
|
.unindent(),
|
||||||
|
|settings| settings.telemetry.set_diagnostics(false),
|
||||||
|
r#"{
|
||||||
|
"telemetry": { "metrics": false, "diagnostics": false }
|
||||||
|
}"#
|
||||||
|
.unindent(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_update_telemetry_setting_other_fields() {
|
||||||
|
assert_new_settings(
|
||||||
|
r#"
|
||||||
|
{
|
||||||
|
"telemetry": {
|
||||||
|
"metrics": false,
|
||||||
|
"diagnostics": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"#
|
||||||
|
.unindent(),
|
||||||
|
|settings| settings.telemetry.set_diagnostics(false),
|
||||||
|
r#"
|
||||||
|
{
|
||||||
|
"telemetry": {
|
||||||
|
"metrics": false,
|
||||||
|
"diagnostics": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"#
|
||||||
|
.unindent(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_update_telemetry_setting_empty_telemetry() {
|
||||||
|
assert_new_settings(
|
||||||
|
r#"
|
||||||
|
{
|
||||||
|
"telemetry": {}
|
||||||
|
}
|
||||||
|
"#
|
||||||
|
.unindent(),
|
||||||
|
|settings| settings.telemetry.set_diagnostics(false),
|
||||||
|
r#"
|
||||||
|
{
|
||||||
|
"telemetry": {
|
||||||
|
"diagnostics": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"#
|
||||||
|
.unindent(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_update_telemetry_setting_pre_existing() {
|
||||||
|
assert_new_settings(
|
||||||
|
r#"
|
||||||
|
{
|
||||||
|
"telemetry": {
|
||||||
|
"diagnostics": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"#
|
||||||
|
.unindent(),
|
||||||
|
|settings| settings.telemetry.set_diagnostics(false),
|
||||||
|
r#"
|
||||||
|
{
|
||||||
|
"telemetry": {
|
||||||
|
"diagnostics": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"#
|
||||||
|
.unindent(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_update_telemetry_setting() {
|
||||||
|
assert_new_settings(
|
||||||
|
"{}",
|
||||||
|
|settings| settings.telemetry.set_diagnostics(true),
|
||||||
|
r#"
|
||||||
|
{
|
||||||
|
"telemetry": {
|
||||||
|
"diagnostics": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"#
|
||||||
|
.unindent(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_update_object_empty_doc() {
|
||||||
|
assert_new_settings(
|
||||||
|
"",
|
||||||
|
|settings| settings.telemetry.set_diagnostics(true),
|
||||||
|
r#"
|
||||||
|
{
|
||||||
|
"telemetry": {
|
||||||
|
"diagnostics": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"#
|
||||||
|
.unindent(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_write_theme_into_settings_with_theme() {
|
fn test_write_theme_into_settings_with_theme() {
|
||||||
let settings = r#"
|
assert_new_settings(
|
||||||
{
|
r#"
|
||||||
"theme": "One Dark"
|
{
|
||||||
}
|
"theme": "One Dark"
|
||||||
"#
|
}
|
||||||
.unindent();
|
"#
|
||||||
|
.unindent(),
|
||||||
let new_settings = r#"
|
|settings| settings.theme = Some("summerfruit-light".to_string()),
|
||||||
{
|
r#"
|
||||||
"theme": "summerfruit-light"
|
{
|
||||||
}
|
"theme": "summerfruit-light"
|
||||||
"#
|
}
|
||||||
.unindent();
|
"#
|
||||||
|
.unindent(),
|
||||||
let settings_after_theme =
|
);
|
||||||
write_top_level_setting(settings, "theme", "\"summerfruit-light\"");
|
|
||||||
|
|
||||||
assert_eq!(settings_after_theme, new_settings)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_write_theme_into_empty_settings() {
|
fn test_write_theme_into_empty_settings() {
|
||||||
let settings = r#"
|
assert_new_settings(
|
||||||
{
|
r#"
|
||||||
}
|
{
|
||||||
"#
|
}
|
||||||
.unindent();
|
"#
|
||||||
|
.unindent(),
|
||||||
let new_settings = r#"
|
|settings| settings.theme = Some("summerfruit-light".to_string()),
|
||||||
{
|
r#"
|
||||||
"theme": "summerfruit-light"
|
{
|
||||||
}
|
"theme": "summerfruit-light"
|
||||||
"#
|
}
|
||||||
.unindent();
|
"#
|
||||||
|
.unindent(),
|
||||||
let settings_after_theme =
|
);
|
||||||
write_top_level_setting(settings, "theme", "\"summerfruit-light\"");
|
|
||||||
|
|
||||||
assert_eq!(settings_after_theme, new_settings)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_write_theme_into_no_settings() {
|
fn write_key_no_document() {
|
||||||
let settings = "".to_string();
|
assert_new_settings(
|
||||||
|
"",
|
||||||
let new_settings = r#"
|
|settings| settings.theme = Some("summerfruit-light".to_string()),
|
||||||
{
|
r#"
|
||||||
"theme": "summerfruit-light"
|
{
|
||||||
}
|
"theme": "summerfruit-light"
|
||||||
"#
|
}
|
||||||
.unindent();
|
"#
|
||||||
|
.unindent(),
|
||||||
let settings_after_theme =
|
);
|
||||||
write_top_level_setting(settings, "theme", "\"summerfruit-light\"");
|
|
||||||
|
|
||||||
assert_eq!(settings_after_theme, new_settings)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_write_theme_into_single_line_settings_without_theme() {
|
fn test_write_theme_into_single_line_settings_without_theme() {
|
||||||
let settings = r#"{ "a": "", "ok": true }"#.to_string();
|
assert_new_settings(
|
||||||
let new_settings = r#"{ "theme": "summerfruit-light", "a": "", "ok": true }"#;
|
r#"{ "a": "", "ok": true }"#,
|
||||||
|
|settings| settings.theme = Some("summerfruit-light".to_string()),
|
||||||
let settings_after_theme =
|
r#"{ "theme": "summerfruit-light", "a": "", "ok": true }"#,
|
||||||
write_top_level_setting(settings, "theme", "\"summerfruit-light\"");
|
);
|
||||||
|
|
||||||
assert_eq!(settings_after_theme, new_settings)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_write_theme_pre_object_whitespace() {
|
fn test_write_theme_pre_object_whitespace() {
|
||||||
let settings = r#" { "a": "", "ok": true }"#.to_string();
|
assert_new_settings(
|
||||||
let new_settings = r#" { "theme": "summerfruit-light", "a": "", "ok": true }"#;
|
r#" { "a": "", "ok": true }"#,
|
||||||
|
|settings| settings.theme = Some("summerfruit-light".to_string()),
|
||||||
let settings_after_theme =
|
r#" { "theme": "summerfruit-light", "a": "", "ok": true }"#.unindent(),
|
||||||
write_top_level_setting(settings, "theme", "\"summerfruit-light\"");
|
);
|
||||||
|
|
||||||
assert_eq!(settings_after_theme, new_settings)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_write_theme_into_multi_line_settings_without_theme() {
|
fn test_write_theme_into_multi_line_settings_without_theme() {
|
||||||
let settings = r#"
|
assert_new_settings(
|
||||||
{
|
r#"
|
||||||
"a": "b"
|
{
|
||||||
}
|
"a": "b"
|
||||||
"#
|
}
|
||||||
.unindent();
|
"#
|
||||||
|
.unindent(),
|
||||||
let new_settings = r#"
|
|settings| settings.theme = Some("summerfruit-light".to_string()),
|
||||||
{
|
r#"
|
||||||
"theme": "summerfruit-light",
|
{
|
||||||
"a": "b"
|
"theme": "summerfruit-light",
|
||||||
}
|
"a": "b"
|
||||||
"#
|
}
|
||||||
.unindent();
|
"#
|
||||||
|
.unindent(),
|
||||||
let settings_after_theme =
|
);
|
||||||
write_top_level_setting(settings, "theme", "\"summerfruit-light\"");
|
|
||||||
|
|
||||||
assert_eq!(settings_after_theme, new_settings)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
use crate::{watched_json::WatchedJsonFile, write_top_level_setting, SettingsFileContent};
|
use crate::{update_settings_file, watched_json::WatchedJsonFile, SettingsFileContent};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use fs::Fs;
|
use fs::Fs;
|
||||||
use gpui::MutableAppContext;
|
use gpui::MutableAppContext;
|
||||||
use serde_json::Value;
|
|
||||||
use std::{path::Path, sync::Arc};
|
use std::{path::Path, sync::Arc};
|
||||||
|
|
||||||
// TODO: Switch SettingsFile to open a worktree and buffer for synchronization
|
// TODO: Switch SettingsFile to open a worktree and buffer for synchronization
|
||||||
|
@ -27,57 +26,24 @@ impl SettingsFile {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update(cx: &mut MutableAppContext, update: impl FnOnce(&mut SettingsFileContent)) {
|
pub fn update(
|
||||||
|
cx: &mut MutableAppContext,
|
||||||
|
update: impl 'static + Send + FnOnce(&mut SettingsFileContent),
|
||||||
|
) {
|
||||||
let this = cx.global::<SettingsFile>();
|
let this = cx.global::<SettingsFile>();
|
||||||
|
|
||||||
let current_file_content = this.settings_file_content.current();
|
let current_file_content = this.settings_file_content.current();
|
||||||
let mut new_file_content = current_file_content.clone();
|
|
||||||
|
|
||||||
update(&mut new_file_content);
|
|
||||||
|
|
||||||
let fs = this.fs.clone();
|
let fs = this.fs.clone();
|
||||||
let path = this.path.clone();
|
let path = this.path.clone();
|
||||||
|
|
||||||
cx.background()
|
cx.background()
|
||||||
.spawn(async move {
|
.spawn(async move {
|
||||||
// Unwrap safety: These values are all guarnteed to be well formed, and we know
|
let old_text = fs.load(path).await?;
|
||||||
// that they will deserialize to our settings object. All of the following unwraps
|
|
||||||
// are therefore safe.
|
|
||||||
let tmp = serde_json::to_value(current_file_content).unwrap();
|
|
||||||
let old_json = tmp.as_object().unwrap();
|
|
||||||
|
|
||||||
let new_tmp = serde_json::to_value(new_file_content).unwrap();
|
let new_text = update_settings_file(old_text, current_file_content, update);
|
||||||
let new_json = new_tmp.as_object().unwrap();
|
|
||||||
|
|
||||||
// Find changed fields
|
fs.atomic_write(path.to_path_buf(), new_text).await?;
|
||||||
let mut diffs = vec![];
|
|
||||||
for (key, old_value) in old_json.iter() {
|
|
||||||
let new_value = new_json.get(key).unwrap();
|
|
||||||
if old_value != new_value {
|
|
||||||
if matches!(
|
|
||||||
new_value,
|
|
||||||
&Value::Null | &Value::Object(_) | &Value::Array(_)
|
|
||||||
) {
|
|
||||||
unimplemented!(
|
|
||||||
"We only support updating basic values at the top level"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let new_json = serde_json::to_string_pretty(new_value)
|
|
||||||
.expect("Could not serialize new json field to string");
|
|
||||||
|
|
||||||
diffs.push((key, new_json));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Have diffs, rewrite the settings file now.
|
|
||||||
let mut content = fs.load(path).await?;
|
|
||||||
|
|
||||||
for (key, new_value) in diffs {
|
|
||||||
content = write_top_level_setting(content, key, &new_value)
|
|
||||||
}
|
|
||||||
|
|
||||||
fs.atomic_write(path.to_path_buf(), content).await?;
|
|
||||||
|
|
||||||
Ok(()) as Result<()>
|
Ok(()) as Result<()>
|
||||||
})
|
})
|
||||||
|
@ -88,10 +54,164 @@ impl SettingsFile {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::{watched_json::watch_settings_file, EditorSettings, Settings, SoftWrap};
|
use crate::{
|
||||||
|
watch_files, watched_json::watch_settings_file, EditorSettings, Settings, SoftWrap,
|
||||||
|
};
|
||||||
use fs::FakeFs;
|
use fs::FakeFs;
|
||||||
|
use gpui::{actions, Action};
|
||||||
use theme::ThemeRegistry;
|
use theme::ThemeRegistry;
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_base_keymap(cx: &mut gpui::TestAppContext) {
|
||||||
|
let executor = cx.background();
|
||||||
|
let fs = FakeFs::new(executor.clone());
|
||||||
|
let font_cache = cx.font_cache();
|
||||||
|
|
||||||
|
actions!(test, [A, B]);
|
||||||
|
// From the Atom keymap
|
||||||
|
actions!(workspace, [ActivatePreviousPane]);
|
||||||
|
// From the JetBrains keymap
|
||||||
|
actions!(pane, [ActivatePrevItem]);
|
||||||
|
|
||||||
|
fs.save(
|
||||||
|
"/settings.json".as_ref(),
|
||||||
|
&r#"
|
||||||
|
{
|
||||||
|
"base_keymap": "Atom"
|
||||||
|
}
|
||||||
|
"#
|
||||||
|
.into(),
|
||||||
|
Default::default(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
fs.save(
|
||||||
|
"/keymap.json".as_ref(),
|
||||||
|
&r#"
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"bindings": {
|
||||||
|
"backspace": "test::A"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
"#
|
||||||
|
.into(),
|
||||||
|
Default::default(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let settings_file =
|
||||||
|
WatchedJsonFile::new(fs.clone(), &executor, "/settings.json".as_ref()).await;
|
||||||
|
let keymaps_file =
|
||||||
|
WatchedJsonFile::new(fs.clone(), &executor, "/keymap.json".as_ref()).await;
|
||||||
|
|
||||||
|
let default_settings = cx.read(Settings::test);
|
||||||
|
|
||||||
|
cx.update(|cx| {
|
||||||
|
cx.add_global_action(|_: &A, _cx| {});
|
||||||
|
cx.add_global_action(|_: &B, _cx| {});
|
||||||
|
cx.add_global_action(|_: &ActivatePreviousPane, _cx| {});
|
||||||
|
cx.add_global_action(|_: &ActivatePrevItem, _cx| {});
|
||||||
|
watch_files(
|
||||||
|
default_settings,
|
||||||
|
settings_file,
|
||||||
|
ThemeRegistry::new((), font_cache),
|
||||||
|
keymaps_file,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.foreground().run_until_parked();
|
||||||
|
|
||||||
|
// Test loading the keymap base at all
|
||||||
|
cx.update(|cx| {
|
||||||
|
assert_keybindings_for(
|
||||||
|
cx,
|
||||||
|
vec![("backspace", &A), ("k", &ActivatePreviousPane)],
|
||||||
|
line!(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test modifying the users keymap, while retaining the base keymap
|
||||||
|
fs.save(
|
||||||
|
"/keymap.json".as_ref(),
|
||||||
|
&r#"
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"bindings": {
|
||||||
|
"backspace": "test::B"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
"#
|
||||||
|
.into(),
|
||||||
|
Default::default(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
cx.foreground().run_until_parked();
|
||||||
|
|
||||||
|
cx.update(|cx| {
|
||||||
|
assert_keybindings_for(
|
||||||
|
cx,
|
||||||
|
vec![("backspace", &B), ("k", &ActivatePreviousPane)],
|
||||||
|
line!(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test modifying the base, while retaining the users keymap
|
||||||
|
fs.save(
|
||||||
|
"/settings.json".as_ref(),
|
||||||
|
&r#"
|
||||||
|
{
|
||||||
|
"base_keymap": "JetBrains"
|
||||||
|
}
|
||||||
|
"#
|
||||||
|
.into(),
|
||||||
|
Default::default(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
cx.foreground().run_until_parked();
|
||||||
|
|
||||||
|
cx.update(|cx| {
|
||||||
|
assert_keybindings_for(
|
||||||
|
cx,
|
||||||
|
vec![("backspace", &B), ("[", &ActivatePrevItem)],
|
||||||
|
line!(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn assert_keybindings_for<'a>(
|
||||||
|
cx: &mut MutableAppContext,
|
||||||
|
actions: Vec<(&'static str, &'a dyn Action)>,
|
||||||
|
line: u32,
|
||||||
|
) {
|
||||||
|
for (key, action) in actions {
|
||||||
|
// assert that...
|
||||||
|
assert!(
|
||||||
|
cx.available_actions(0, 0).any(|(_, bound_action, b)| {
|
||||||
|
// action names match...
|
||||||
|
bound_action.name() == action.name()
|
||||||
|
&& bound_action.namespace() == action.namespace()
|
||||||
|
// and key strokes contain the given key
|
||||||
|
&& b.iter()
|
||||||
|
.any(|binding| binding.keystrokes().iter().any(|k| k.key == key))
|
||||||
|
}),
|
||||||
|
"On {} Failed to find {} with keybinding {}",
|
||||||
|
line,
|
||||||
|
action.name(),
|
||||||
|
key
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_watch_settings_files(cx: &mut gpui::TestAppContext) {
|
async fn test_watch_settings_files(cx: &mut gpui::TestAppContext) {
|
||||||
let executor = cx.background();
|
let executor = cx.background();
|
||||||
|
|
|
@ -62,7 +62,18 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn watch_settings_file(
|
pub fn watch_files(
|
||||||
|
defaults: Settings,
|
||||||
|
settings_file: WatchedJsonFile<SettingsFileContent>,
|
||||||
|
theme_registry: Arc<ThemeRegistry>,
|
||||||
|
keymap_file: WatchedJsonFile<KeymapFileContent>,
|
||||||
|
cx: &mut MutableAppContext,
|
||||||
|
) {
|
||||||
|
watch_settings_file(defaults, settings_file, theme_registry, cx);
|
||||||
|
watch_keymap_file(keymap_file, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn watch_settings_file(
|
||||||
defaults: Settings,
|
defaults: Settings,
|
||||||
mut file: WatchedJsonFile<SettingsFileContent>,
|
mut file: WatchedJsonFile<SettingsFileContent>,
|
||||||
theme_registry: Arc<ThemeRegistry>,
|
theme_registry: Arc<ThemeRegistry>,
|
||||||
|
@ -77,13 +88,13 @@ pub fn watch_settings_file(
|
||||||
.detach();
|
.detach();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn keymap_updated(content: KeymapFileContent, cx: &mut MutableAppContext) {
|
fn keymap_updated(content: KeymapFileContent, cx: &mut MutableAppContext) {
|
||||||
cx.clear_bindings();
|
cx.clear_bindings();
|
||||||
KeymapFileContent::load_defaults(cx);
|
KeymapFileContent::load_defaults(cx);
|
||||||
content.add_to_cx(cx).log_err();
|
content.add_to_cx(cx).log_err();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn settings_updated(
|
fn settings_updated(
|
||||||
defaults: &Settings,
|
defaults: &Settings,
|
||||||
content: SettingsFileContent,
|
content: SettingsFileContent,
|
||||||
theme_registry: &Arc<ThemeRegistry>,
|
theme_registry: &Arc<ThemeRegistry>,
|
||||||
|
@ -95,10 +106,20 @@ pub fn settings_updated(
|
||||||
cx.refresh_windows();
|
cx.refresh_windows();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn watch_keymap_file(mut file: WatchedJsonFile<KeymapFileContent>, cx: &mut MutableAppContext) {
|
fn watch_keymap_file(mut file: WatchedJsonFile<KeymapFileContent>, cx: &mut MutableAppContext) {
|
||||||
cx.spawn(|mut cx| async move {
|
cx.spawn(|mut cx| async move {
|
||||||
|
let mut settings_subscription = None;
|
||||||
while let Some(content) = file.0.recv().await {
|
while let Some(content) = file.0.recv().await {
|
||||||
cx.update(|cx| keymap_updated(content, cx));
|
cx.update(|cx| {
|
||||||
|
let old_base_keymap = cx.global::<Settings>().base_keymap;
|
||||||
|
keymap_updated(content.clone(), cx);
|
||||||
|
settings_subscription = Some(cx.observe_global::<Settings, _>(move |cx| {
|
||||||
|
let settings = cx.global::<Settings>();
|
||||||
|
if settings.base_keymap != old_base_keymap {
|
||||||
|
keymap_updated(content.clone(), cx);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
|
|
|
@ -720,7 +720,7 @@ impl Element for TerminalElement {
|
||||||
cx.paint_layer(clip_bounds, |cx| {
|
cx.paint_layer(clip_bounds, |cx| {
|
||||||
let origin = bounds.origin() + vec2f(layout.size.cell_width, 0.);
|
let origin = bounds.origin() + vec2f(layout.size.cell_width, 0.);
|
||||||
|
|
||||||
//Elements are ephemeral, only at paint time do we know what could be clicked by a mouse
|
// Elements are ephemeral, only at paint time do we know what could be clicked by a mouse
|
||||||
self.attach_mouse_handlers(origin, self.view.id(), visible_bounds, layout.mode, cx);
|
self.attach_mouse_handlers(origin, self.view.id(), visible_bounds, layout.mode, cx);
|
||||||
|
|
||||||
cx.scene.push_cursor_region(gpui::CursorRegion {
|
cx.scene.push_cursor_region(gpui::CursorRegion {
|
||||||
|
|
|
@ -623,7 +623,7 @@ impl Item for TerminalView {
|
||||||
|
|
||||||
fn deserialize(
|
fn deserialize(
|
||||||
project: ModelHandle<Project>,
|
project: ModelHandle<Project>,
|
||||||
_workspace: WeakViewHandle<Workspace>,
|
workspace: WeakViewHandle<Workspace>,
|
||||||
workspace_id: workspace::WorkspaceId,
|
workspace_id: workspace::WorkspaceId,
|
||||||
item_id: workspace::ItemId,
|
item_id: workspace::ItemId,
|
||||||
cx: &mut ViewContext<Pane>,
|
cx: &mut ViewContext<Pane>,
|
||||||
|
@ -633,7 +633,18 @@ impl Item for TerminalView {
|
||||||
let cwd = TERMINAL_DB
|
let cwd = TERMINAL_DB
|
||||||
.get_working_directory(item_id, workspace_id)
|
.get_working_directory(item_id, workspace_id)
|
||||||
.log_err()
|
.log_err()
|
||||||
.flatten();
|
.flatten()
|
||||||
|
.or_else(|| {
|
||||||
|
cx.read(|cx| {
|
||||||
|
let strategy = cx.global::<Settings>().terminal_strategy();
|
||||||
|
workspace
|
||||||
|
.upgrade(cx)
|
||||||
|
.map(|workspace| {
|
||||||
|
get_working_directory(workspace.read(cx), cx, strategy)
|
||||||
|
})
|
||||||
|
.flatten()
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
cx.update(|cx| {
|
cx.update(|cx| {
|
||||||
let terminal = project.update(cx, |project, cx| {
|
let terminal = project.update(cx, |project, cx| {
|
||||||
|
@ -940,15 +951,7 @@ mod tests {
|
||||||
let params = cx.update(AppState::test);
|
let params = cx.update(AppState::test);
|
||||||
|
|
||||||
let project = Project::test(params.fs.clone(), [], cx).await;
|
let project = Project::test(params.fs.clone(), [], cx).await;
|
||||||
let (_, workspace) = cx.add_window(|cx| {
|
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
|
||||||
Workspace::new(
|
|
||||||
Default::default(),
|
|
||||||
0,
|
|
||||||
project.clone(),
|
|
||||||
|_, _| unimplemented!(),
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
});
|
|
||||||
|
|
||||||
(project, workspace)
|
(project, workspace)
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,9 @@ use gpui::{
|
||||||
use serde::{de::DeserializeOwned, Deserialize};
|
use serde::{de::DeserializeOwned, Deserialize};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use std::{collections::HashMap, sync::Arc};
|
use std::{collections::HashMap, sync::Arc};
|
||||||
|
use ui::{CheckboxStyle, IconStyle};
|
||||||
|
|
||||||
|
pub mod ui;
|
||||||
|
|
||||||
pub use theme_registry::*;
|
pub use theme_registry::*;
|
||||||
|
|
||||||
|
@ -37,6 +40,7 @@ pub struct Theme {
|
||||||
pub tooltip: TooltipStyle,
|
pub tooltip: TooltipStyle,
|
||||||
pub terminal: TerminalStyle,
|
pub terminal: TerminalStyle,
|
||||||
pub feedback: FeedbackStyle,
|
pub feedback: FeedbackStyle,
|
||||||
|
pub welcome: WelcomeStyle,
|
||||||
pub color_scheme: ColorScheme,
|
pub color_scheme: ColorScheme,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -49,6 +53,7 @@ pub struct ThemeMeta {
|
||||||
#[derive(Deserialize, Default)]
|
#[derive(Deserialize, Default)]
|
||||||
pub struct Workspace {
|
pub struct Workspace {
|
||||||
pub background: Color,
|
pub background: Color,
|
||||||
|
pub blank_pane: BlankPaneStyle,
|
||||||
pub titlebar: Titlebar,
|
pub titlebar: Titlebar,
|
||||||
pub tab_bar: TabBar,
|
pub tab_bar: TabBar,
|
||||||
pub pane_divider: Border,
|
pub pane_divider: Border,
|
||||||
|
@ -68,6 +73,16 @@ pub struct Workspace {
|
||||||
pub drop_target_overlay_color: Color,
|
pub drop_target_overlay_color: Color,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Deserialize, Default)]
|
||||||
|
pub struct BlankPaneStyle {
|
||||||
|
pub logo: IconStyle,
|
||||||
|
pub logo_shadow: IconStyle,
|
||||||
|
pub logo_container: ContainerStyle,
|
||||||
|
pub keyboard_hints: ContainerStyle,
|
||||||
|
pub keyboard_hint: Interactive<ContainedText>,
|
||||||
|
pub keyboard_hint_width: f32,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Deserialize, Default)]
|
#[derive(Clone, Deserialize, Default)]
|
||||||
pub struct Titlebar {
|
pub struct Titlebar {
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
|
@ -277,10 +292,10 @@ pub struct StatusBar {
|
||||||
pub height: f32,
|
pub height: f32,
|
||||||
pub item_spacing: f32,
|
pub item_spacing: f32,
|
||||||
pub cursor_position: TextStyle,
|
pub cursor_position: TextStyle,
|
||||||
|
pub active_language: Interactive<ContainedText>,
|
||||||
pub auto_update_progress_message: TextStyle,
|
pub auto_update_progress_message: TextStyle,
|
||||||
pub auto_update_done_message: TextStyle,
|
pub auto_update_done_message: TextStyle,
|
||||||
pub lsp_status: Interactive<StatusBarLspStatus>,
|
pub lsp_status: Interactive<StatusBarLspStatus>,
|
||||||
pub feedback: Interactive<TextStyle>,
|
|
||||||
pub sidebar_buttons: StatusBarSidebarButtons,
|
pub sidebar_buttons: StatusBarSidebarButtons,
|
||||||
pub diagnostic_summary: Interactive<StatusBarDiagnosticSummary>,
|
pub diagnostic_summary: Interactive<StatusBarDiagnosticSummary>,
|
||||||
pub diagnostic_message: Interactive<ContainedText>,
|
pub diagnostic_message: Interactive<ContainedText>,
|
||||||
|
@ -345,6 +360,7 @@ pub struct ProjectPanel {
|
||||||
pub cut_entry: Interactive<ProjectPanelEntry>,
|
pub cut_entry: Interactive<ProjectPanelEntry>,
|
||||||
pub filename_editor: FieldEditor,
|
pub filename_editor: FieldEditor,
|
||||||
pub indent_width: f32,
|
pub indent_width: f32,
|
||||||
|
pub open_project_button: Interactive<ContainedText>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, Default)]
|
#[derive(Clone, Debug, Deserialize, Default)]
|
||||||
|
@ -850,13 +866,25 @@ pub struct FeedbackStyle {
|
||||||
pub link_text_hover: ContainedText,
|
pub link_text_hover: ContainedText,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Deserialize, Default)]
|
||||||
|
pub struct WelcomeStyle {
|
||||||
|
pub page_width: f32,
|
||||||
|
pub logo: IconStyle,
|
||||||
|
pub logo_subheading: ContainedText,
|
||||||
|
pub usage_note: ContainedText,
|
||||||
|
pub checkbox: CheckboxStyle,
|
||||||
|
pub checkbox_container: ContainerStyle,
|
||||||
|
pub button: Interactive<ContainedText>,
|
||||||
|
pub button_group: ContainerStyle,
|
||||||
|
pub heading_group: ContainerStyle,
|
||||||
|
pub checkbox_group: ContainerStyle,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Deserialize, Default)]
|
#[derive(Clone, Deserialize, Default)]
|
||||||
pub struct ColorScheme {
|
pub struct ColorScheme {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub is_light: bool,
|
pub is_light: bool,
|
||||||
|
|
||||||
pub ramps: RampSet,
|
pub ramps: RampSet,
|
||||||
|
|
||||||
pub lowest: Layer,
|
pub lowest: Layer,
|
||||||
pub middle: Layer,
|
pub middle: Layer,
|
||||||
pub highest: Layer,
|
pub highest: Layer,
|
||||||
|
|
149
crates/theme/src/ui.rs
Normal file
149
crates/theme/src/ui.rs
Normal file
|
@ -0,0 +1,149 @@
|
||||||
|
use gpui::{
|
||||||
|
color::Color,
|
||||||
|
elements::{
|
||||||
|
ConstrainedBox, Container, ContainerStyle, Empty, Flex, KeystrokeLabel, Label,
|
||||||
|
MouseEventHandler, ParentElement, Svg,
|
||||||
|
},
|
||||||
|
Action, Element, ElementBox, EventContext, RenderContext, View,
|
||||||
|
};
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
use crate::ContainedText;
|
||||||
|
|
||||||
|
#[derive(Clone, Deserialize, Default)]
|
||||||
|
pub struct CheckboxStyle {
|
||||||
|
pub icon: IconStyle,
|
||||||
|
pub label: ContainedText,
|
||||||
|
pub default: ContainerStyle,
|
||||||
|
pub checked: ContainerStyle,
|
||||||
|
pub hovered: ContainerStyle,
|
||||||
|
pub hovered_and_checked: ContainerStyle,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn checkbox<T: 'static, V: View>(
|
||||||
|
label: &'static str,
|
||||||
|
style: &CheckboxStyle,
|
||||||
|
checked: bool,
|
||||||
|
cx: &mut RenderContext<V>,
|
||||||
|
change: fn(checked: bool, cx: &mut EventContext) -> (),
|
||||||
|
) -> MouseEventHandler<T> {
|
||||||
|
let label = Label::new(label, style.label.text.clone())
|
||||||
|
.contained()
|
||||||
|
.with_style(style.label.container)
|
||||||
|
.boxed();
|
||||||
|
|
||||||
|
checkbox_with_label(label, style, checked, cx, change)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn checkbox_with_label<T: 'static, V: View>(
|
||||||
|
label: ElementBox,
|
||||||
|
style: &CheckboxStyle,
|
||||||
|
checked: bool,
|
||||||
|
cx: &mut RenderContext<V>,
|
||||||
|
change: fn(checked: bool, cx: &mut EventContext) -> (),
|
||||||
|
) -> MouseEventHandler<T> {
|
||||||
|
MouseEventHandler::<T>::new(0, cx, |state, _| {
|
||||||
|
let indicator = if checked {
|
||||||
|
icon(&style.icon)
|
||||||
|
} else {
|
||||||
|
Empty::new()
|
||||||
|
.constrained()
|
||||||
|
.with_width(style.icon.dimensions.width)
|
||||||
|
.with_height(style.icon.dimensions.height)
|
||||||
|
};
|
||||||
|
|
||||||
|
Flex::row()
|
||||||
|
.with_children([
|
||||||
|
indicator
|
||||||
|
.contained()
|
||||||
|
.with_style(if checked {
|
||||||
|
if state.hovered() {
|
||||||
|
style.hovered_and_checked
|
||||||
|
} else {
|
||||||
|
style.checked
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if state.hovered() {
|
||||||
|
style.hovered
|
||||||
|
} else {
|
||||||
|
style.default
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.boxed(),
|
||||||
|
label,
|
||||||
|
])
|
||||||
|
.align_children_center()
|
||||||
|
.boxed()
|
||||||
|
})
|
||||||
|
.on_click(gpui::MouseButton::Left, move |_, cx| change(!checked, cx))
|
||||||
|
.with_cursor_style(gpui::CursorStyle::PointingHand)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Deserialize, Default)]
|
||||||
|
pub struct IconStyle {
|
||||||
|
pub color: Color,
|
||||||
|
pub icon: String,
|
||||||
|
pub dimensions: Dimensions,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Deserialize, Default)]
|
||||||
|
pub struct Dimensions {
|
||||||
|
pub width: f32,
|
||||||
|
pub height: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn icon(style: &IconStyle) -> ConstrainedBox {
|
||||||
|
Svg::new(style.icon.clone())
|
||||||
|
.with_color(style.color)
|
||||||
|
.constrained()
|
||||||
|
.with_width(style.dimensions.width)
|
||||||
|
.with_height(style.dimensions.height)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn keystroke_label<V: View>(
|
||||||
|
label_text: &'static str,
|
||||||
|
label_style: &ContainedText,
|
||||||
|
keystroke_style: &ContainedText,
|
||||||
|
action: Box<dyn Action>,
|
||||||
|
cx: &mut RenderContext<V>,
|
||||||
|
) -> Container {
|
||||||
|
// FIXME: Put the theme in it's own global so we can
|
||||||
|
// query the keystroke style on our own
|
||||||
|
keystroke_label_for(
|
||||||
|
cx.window_id(),
|
||||||
|
cx.handle().id(),
|
||||||
|
label_text,
|
||||||
|
label_style,
|
||||||
|
keystroke_style,
|
||||||
|
action,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn keystroke_label_for(
|
||||||
|
window_id: usize,
|
||||||
|
view_id: usize,
|
||||||
|
label_text: &'static str,
|
||||||
|
label_style: &ContainedText,
|
||||||
|
keystroke_style: &ContainedText,
|
||||||
|
action: Box<dyn Action>,
|
||||||
|
) -> Container {
|
||||||
|
Flex::row()
|
||||||
|
.with_child(
|
||||||
|
Label::new(label_text, label_style.text.clone())
|
||||||
|
.contained()
|
||||||
|
.boxed(),
|
||||||
|
)
|
||||||
|
.with_child({
|
||||||
|
KeystrokeLabel::new(
|
||||||
|
window_id,
|
||||||
|
view_id,
|
||||||
|
action,
|
||||||
|
keystroke_style.container,
|
||||||
|
keystroke_style.text.clone(),
|
||||||
|
)
|
||||||
|
.flex_float()
|
||||||
|
.boxed()
|
||||||
|
})
|
||||||
|
.contained()
|
||||||
|
.with_style(label_style.container)
|
||||||
|
}
|
|
@ -47,12 +47,7 @@ impl ThemeSelector {
|
||||||
let mut theme_names = registry
|
let mut theme_names = registry
|
||||||
.list(**cx.default_global::<StaffMode>())
|
.list(**cx.default_global::<StaffMode>())
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
theme_names.sort_unstable_by(|a, b| {
|
theme_names.sort_unstable_by(|a, b| a.is_light.cmp(&b.is_light).then(a.name.cmp(&b.name)));
|
||||||
a.is_light
|
|
||||||
.cmp(&b.is_light)
|
|
||||||
.reverse()
|
|
||||||
.then(a.name.cmp(&b.name))
|
|
||||||
});
|
|
||||||
let matches = theme_names
|
let matches = theme_names
|
||||||
.iter()
|
.iter()
|
||||||
.map(|meta| StringMatch {
|
.map(|meta| StringMatch {
|
||||||
|
|
|
@ -124,11 +124,15 @@ pub trait TryFutureExt {
|
||||||
fn warn_on_err(self) -> LogErrorFuture<Self>
|
fn warn_on_err(self) -> LogErrorFuture<Self>
|
||||||
where
|
where
|
||||||
Self: Sized;
|
Self: Sized;
|
||||||
|
fn unwrap(self) -> UnwrapFuture<Self>
|
||||||
|
where
|
||||||
|
Self: Sized;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<F, T> TryFutureExt for F
|
impl<F, T, E> TryFutureExt for F
|
||||||
where
|
where
|
||||||
F: Future<Output = anyhow::Result<T>>,
|
F: Future<Output = Result<T, E>>,
|
||||||
|
E: std::fmt::Debug,
|
||||||
{
|
{
|
||||||
fn log_err(self) -> LogErrorFuture<Self>
|
fn log_err(self) -> LogErrorFuture<Self>
|
||||||
where
|
where
|
||||||
|
@ -143,17 +147,25 @@ where
|
||||||
{
|
{
|
||||||
LogErrorFuture(self, log::Level::Warn)
|
LogErrorFuture(self, log::Level::Warn)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn unwrap(self) -> UnwrapFuture<Self>
|
||||||
|
where
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
UnwrapFuture(self)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct LogErrorFuture<F>(F, log::Level);
|
pub struct LogErrorFuture<F>(F, log::Level);
|
||||||
|
|
||||||
impl<F, T> Future for LogErrorFuture<F>
|
impl<F, T, E> Future for LogErrorFuture<F>
|
||||||
where
|
where
|
||||||
F: Future<Output = anyhow::Result<T>>,
|
F: Future<Output = Result<T, E>>,
|
||||||
|
E: std::fmt::Debug,
|
||||||
{
|
{
|
||||||
type Output = Option<T>;
|
type Output = Option<T>;
|
||||||
|
|
||||||
fn poll(self: std::pin::Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> {
|
||||||
let level = self.1;
|
let level = self.1;
|
||||||
let inner = unsafe { Pin::new_unchecked(&mut self.get_unchecked_mut().0) };
|
let inner = unsafe { Pin::new_unchecked(&mut self.get_unchecked_mut().0) };
|
||||||
match inner.poll(cx) {
|
match inner.poll(cx) {
|
||||||
|
@ -169,6 +181,24 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct UnwrapFuture<F>(F);
|
||||||
|
|
||||||
|
impl<F, T, E> Future for UnwrapFuture<F>
|
||||||
|
where
|
||||||
|
F: Future<Output = Result<T, E>>,
|
||||||
|
E: std::fmt::Debug,
|
||||||
|
{
|
||||||
|
type Output = T;
|
||||||
|
|
||||||
|
fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> {
|
||||||
|
let inner = unsafe { Pin::new_unchecked(&mut self.get_unchecked_mut().0) };
|
||||||
|
match inner.poll(cx) {
|
||||||
|
Poll::Ready(result) => Poll::Ready(result.unwrap()),
|
||||||
|
Poll::Pending => Poll::Pending,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct Defer<F: FnOnce()>(Option<F>);
|
struct Defer<F: FnOnce()>(Option<F>);
|
||||||
|
|
||||||
impl<F: FnOnce()> Drop for Defer<F> {
|
impl<F: FnOnce()> Drop for Defer<F> {
|
||||||
|
|
27
crates/welcome/Cargo.toml
Normal file
27
crates/welcome/Cargo.toml
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
[package]
|
||||||
|
name = "welcome"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
publish = false
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
path = "src/welcome.rs"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
test-support = []
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow = "1.0.38"
|
||||||
|
log = "0.4"
|
||||||
|
editor = { path = "../editor" }
|
||||||
|
fuzzy = { path = "../fuzzy" }
|
||||||
|
gpui = { path = "../gpui" }
|
||||||
|
db = { path = "../db" }
|
||||||
|
install_cli = { path = "../install_cli" }
|
||||||
|
project = { path = "../project" }
|
||||||
|
settings = { path = "../settings" }
|
||||||
|
theme = { path = "../theme" }
|
||||||
|
theme_selector = { path = "../theme_selector" }
|
||||||
|
util = { path = "../util" }
|
||||||
|
picker = { path = "../picker" }
|
||||||
|
workspace = { path = "../workspace" }
|
175
crates/welcome/src/base_keymap_picker.rs
Normal file
175
crates/welcome/src/base_keymap_picker.rs
Normal file
|
@ -0,0 +1,175 @@
|
||||||
|
use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
|
||||||
|
use gpui::{
|
||||||
|
actions,
|
||||||
|
elements::{ChildView, Element as _, Label},
|
||||||
|
AnyViewHandle, Entity, MutableAppContext, View, ViewContext, ViewHandle,
|
||||||
|
};
|
||||||
|
use picker::{Picker, PickerDelegate};
|
||||||
|
use settings::{settings_file::SettingsFile, BaseKeymap, Settings};
|
||||||
|
use workspace::Workspace;
|
||||||
|
|
||||||
|
pub struct BaseKeymapSelector {
|
||||||
|
matches: Vec<StringMatch>,
|
||||||
|
picker: ViewHandle<Picker<Self>>,
|
||||||
|
selected_index: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
actions!(welcome, [ToggleBaseKeymapSelector]);
|
||||||
|
|
||||||
|
pub fn init(cx: &mut MutableAppContext) {
|
||||||
|
Picker::<BaseKeymapSelector>::init(cx);
|
||||||
|
cx.add_action({
|
||||||
|
move |workspace, _: &ToggleBaseKeymapSelector, cx| BaseKeymapSelector::toggle(workspace, cx)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum Event {
|
||||||
|
Dismissed,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BaseKeymapSelector {
|
||||||
|
fn toggle(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
|
||||||
|
workspace.toggle_modal(cx, |_, cx| {
|
||||||
|
let this = cx.add_view(|cx| Self::new(cx));
|
||||||
|
cx.subscribe(&this, Self::on_event).detach();
|
||||||
|
this
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new(cx: &mut ViewContext<Self>) -> Self {
|
||||||
|
let base = cx.global::<Settings>().base_keymap;
|
||||||
|
let selected_index = BaseKeymap::OPTIONS
|
||||||
|
.iter()
|
||||||
|
.position(|(_, value)| *value == base)
|
||||||
|
.unwrap_or(0);
|
||||||
|
|
||||||
|
let this = cx.weak_handle();
|
||||||
|
Self {
|
||||||
|
picker: cx.add_view(|cx| Picker::new("Select a base keymap", this, cx)),
|
||||||
|
matches: Vec::new(),
|
||||||
|
selected_index,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_event(
|
||||||
|
workspace: &mut Workspace,
|
||||||
|
_: ViewHandle<BaseKeymapSelector>,
|
||||||
|
event: &Event,
|
||||||
|
cx: &mut ViewContext<Workspace>,
|
||||||
|
) {
|
||||||
|
match event {
|
||||||
|
Event::Dismissed => {
|
||||||
|
workspace.dismiss_modal(cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Entity for BaseKeymapSelector {
|
||||||
|
type Event = Event;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl View for BaseKeymapSelector {
|
||||||
|
fn ui_name() -> &'static str {
|
||||||
|
"BaseKeymapSelector"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> gpui::ElementBox {
|
||||||
|
ChildView::new(self.picker.clone(), cx).boxed()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||||
|
if cx.is_self_focused() {
|
||||||
|
cx.focus(&self.picker);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PickerDelegate for BaseKeymapSelector {
|
||||||
|
fn match_count(&self) -> usize {
|
||||||
|
self.matches.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn selected_index(&self) -> usize {
|
||||||
|
self.selected_index
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext<Self>) {
|
||||||
|
self.selected_index = ix;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Self>) -> gpui::Task<()> {
|
||||||
|
let background = cx.background().clone();
|
||||||
|
let candidates = BaseKeymap::names()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(id, name)| StringMatchCandidate {
|
||||||
|
id,
|
||||||
|
char_bag: name.into(),
|
||||||
|
string: name.into(),
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
cx.spawn(|this, mut cx| async move {
|
||||||
|
let matches = if query.is_empty() {
|
||||||
|
candidates
|
||||||
|
.into_iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(index, candidate)| StringMatch {
|
||||||
|
candidate_id: index,
|
||||||
|
string: candidate.string,
|
||||||
|
positions: Vec::new(),
|
||||||
|
score: 0.0,
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
} else {
|
||||||
|
match_strings(
|
||||||
|
&candidates,
|
||||||
|
&query,
|
||||||
|
false,
|
||||||
|
100,
|
||||||
|
&Default::default(),
|
||||||
|
background,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
};
|
||||||
|
|
||||||
|
this.update(&mut cx, |this, cx| {
|
||||||
|
this.matches = matches;
|
||||||
|
this.selected_index = this
|
||||||
|
.selected_index
|
||||||
|
.min(this.matches.len().saturating_sub(1));
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn confirm(&mut self, cx: &mut ViewContext<Self>) {
|
||||||
|
if let Some(selection) = self.matches.get(self.selected_index) {
|
||||||
|
let base_keymap = BaseKeymap::from_names(&selection.string);
|
||||||
|
SettingsFile::update(cx, move |settings| settings.base_keymap = Some(base_keymap));
|
||||||
|
}
|
||||||
|
cx.emit(Event::Dismissed);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
|
||||||
|
cx.emit(Event::Dismissed)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_match(
|
||||||
|
&self,
|
||||||
|
ix: usize,
|
||||||
|
mouse_state: &mut gpui::MouseState,
|
||||||
|
selected: bool,
|
||||||
|
cx: &gpui::AppContext,
|
||||||
|
) -> gpui::ElementBox {
|
||||||
|
let theme = &cx.global::<Settings>().theme;
|
||||||
|
let keymap_match = &self.matches[ix];
|
||||||
|
let style = theme.picker.item.style_for(mouse_state, selected);
|
||||||
|
|
||||||
|
Label::new(keymap_match.string.clone(), style.label.clone())
|
||||||
|
.with_highlights(keymap_match.positions.clone())
|
||||||
|
.contained()
|
||||||
|
.with_style(style.container)
|
||||||
|
.boxed()
|
||||||
|
}
|
||||||
|
}
|
316
crates/welcome/src/welcome.rs
Normal file
316
crates/welcome/src/welcome.rs
Normal file
|
@ -0,0 +1,316 @@
|
||||||
|
mod base_keymap_picker;
|
||||||
|
|
||||||
|
use std::{borrow::Cow, sync::Arc};
|
||||||
|
|
||||||
|
use db::kvp::KEY_VALUE_STORE;
|
||||||
|
use gpui::{
|
||||||
|
elements::{Flex, Label, MouseEventHandler, ParentElement},
|
||||||
|
Action, Element, ElementBox, Entity, MouseButton, MutableAppContext, RenderContext,
|
||||||
|
Subscription, View, ViewContext,
|
||||||
|
};
|
||||||
|
use settings::{settings_file::SettingsFile, Settings};
|
||||||
|
|
||||||
|
use workspace::{
|
||||||
|
item::Item, open_new, sidebar::SidebarSide, AppState, PaneBackdrop, Welcome, Workspace,
|
||||||
|
WorkspaceId,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::base_keymap_picker::ToggleBaseKeymapSelector;
|
||||||
|
|
||||||
|
pub const FIRST_OPEN: &str = "first_open";
|
||||||
|
|
||||||
|
pub fn init(cx: &mut MutableAppContext) {
|
||||||
|
cx.add_action(|workspace: &mut Workspace, _: &Welcome, cx| {
|
||||||
|
let welcome_page = cx.add_view(WelcomePage::new);
|
||||||
|
workspace.add_item(Box::new(welcome_page), cx)
|
||||||
|
});
|
||||||
|
|
||||||
|
base_keymap_picker::init(cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn show_welcome_experience(app_state: &Arc<AppState>, cx: &mut MutableAppContext) {
|
||||||
|
open_new(&app_state, cx, |workspace, cx| {
|
||||||
|
workspace.toggle_sidebar(SidebarSide::Left, cx);
|
||||||
|
let welcome_page = cx.add_view(|cx| WelcomePage::new(cx));
|
||||||
|
workspace.add_item_to_center(Box::new(welcome_page.clone()), cx);
|
||||||
|
cx.focus(welcome_page);
|
||||||
|
cx.notify();
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
|
||||||
|
db::write_and_log(cx, || {
|
||||||
|
KEY_VALUE_STORE.write_kvp(FIRST_OPEN.to_string(), "false".to_string())
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct WelcomePage {
|
||||||
|
_settings_subscription: Subscription,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Entity for WelcomePage {
|
||||||
|
type Event = ();
|
||||||
|
}
|
||||||
|
|
||||||
|
impl View for WelcomePage {
|
||||||
|
fn ui_name() -> &'static str {
|
||||||
|
"WelcomePage"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> ElementBox {
|
||||||
|
let self_handle = cx.handle();
|
||||||
|
let settings = cx.global::<Settings>();
|
||||||
|
let theme = settings.theme.clone();
|
||||||
|
|
||||||
|
let width = theme.welcome.page_width;
|
||||||
|
|
||||||
|
let (diagnostics, metrics) = {
|
||||||
|
let telemetry = settings.telemetry();
|
||||||
|
(telemetry.diagnostics(), telemetry.metrics())
|
||||||
|
};
|
||||||
|
|
||||||
|
enum Metrics {}
|
||||||
|
enum Diagnostics {}
|
||||||
|
|
||||||
|
PaneBackdrop::new(
|
||||||
|
self_handle.id(),
|
||||||
|
Flex::column()
|
||||||
|
.with_children([
|
||||||
|
Flex::column()
|
||||||
|
.with_children([
|
||||||
|
theme::ui::icon(&theme.welcome.logo)
|
||||||
|
.aligned()
|
||||||
|
.contained()
|
||||||
|
.aligned()
|
||||||
|
.boxed(),
|
||||||
|
Label::new(
|
||||||
|
"Code at the speed of thought",
|
||||||
|
theme.welcome.logo_subheading.text.clone(),
|
||||||
|
)
|
||||||
|
.aligned()
|
||||||
|
.contained()
|
||||||
|
.with_style(theme.welcome.logo_subheading.container)
|
||||||
|
.boxed(),
|
||||||
|
])
|
||||||
|
.contained()
|
||||||
|
.with_style(theme.welcome.heading_group)
|
||||||
|
.constrained()
|
||||||
|
.with_width(width)
|
||||||
|
.boxed(),
|
||||||
|
Flex::column()
|
||||||
|
.with_children([
|
||||||
|
self.render_cta_button(
|
||||||
|
"Choose a theme",
|
||||||
|
theme_selector::Toggle,
|
||||||
|
width,
|
||||||
|
cx,
|
||||||
|
),
|
||||||
|
self.render_cta_button(
|
||||||
|
"Choose a keymap",
|
||||||
|
ToggleBaseKeymapSelector,
|
||||||
|
width,
|
||||||
|
cx,
|
||||||
|
),
|
||||||
|
self.render_cta_button(
|
||||||
|
"Install the CLI",
|
||||||
|
install_cli::Install,
|
||||||
|
width,
|
||||||
|
cx,
|
||||||
|
),
|
||||||
|
])
|
||||||
|
.contained()
|
||||||
|
.with_style(theme.welcome.button_group)
|
||||||
|
.constrained()
|
||||||
|
.with_width(width)
|
||||||
|
.boxed(),
|
||||||
|
Flex::column()
|
||||||
|
.with_children([
|
||||||
|
theme::ui::checkbox_with_label::<Metrics, Self>(
|
||||||
|
Flex::column()
|
||||||
|
.with_children([
|
||||||
|
Label::new(
|
||||||
|
"Send anonymous usage data",
|
||||||
|
theme.welcome.checkbox.label.text.clone(),
|
||||||
|
)
|
||||||
|
.contained()
|
||||||
|
.with_style(theme.welcome.checkbox.label.container)
|
||||||
|
.boxed(),
|
||||||
|
Label::new(
|
||||||
|
"Help > View Telemetry",
|
||||||
|
theme.welcome.usage_note.text.clone(),
|
||||||
|
)
|
||||||
|
.contained()
|
||||||
|
.with_style(theme.welcome.usage_note.container)
|
||||||
|
.boxed(),
|
||||||
|
])
|
||||||
|
.boxed(),
|
||||||
|
&theme.welcome.checkbox,
|
||||||
|
metrics,
|
||||||
|
cx,
|
||||||
|
|checked, cx| {
|
||||||
|
SettingsFile::update(cx, move |file| {
|
||||||
|
file.telemetry.set_metrics(checked)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.contained()
|
||||||
|
.with_style(theme.welcome.checkbox_container)
|
||||||
|
.boxed(),
|
||||||
|
theme::ui::checkbox::<Diagnostics, Self>(
|
||||||
|
"Send crash reports",
|
||||||
|
&theme.welcome.checkbox,
|
||||||
|
diagnostics,
|
||||||
|
cx,
|
||||||
|
|checked, cx| {
|
||||||
|
SettingsFile::update(cx, move |file| {
|
||||||
|
file.telemetry.set_diagnostics(checked)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.contained()
|
||||||
|
.with_style(theme.welcome.checkbox_container)
|
||||||
|
.boxed(),
|
||||||
|
])
|
||||||
|
.contained()
|
||||||
|
.with_style(theme.welcome.checkbox_group)
|
||||||
|
.constrained()
|
||||||
|
.with_width(width)
|
||||||
|
.boxed(),
|
||||||
|
])
|
||||||
|
.constrained()
|
||||||
|
.with_max_width(width)
|
||||||
|
.contained()
|
||||||
|
.with_uniform_padding(10.)
|
||||||
|
.aligned()
|
||||||
|
.boxed(),
|
||||||
|
)
|
||||||
|
.boxed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WelcomePage {
|
||||||
|
pub fn new(cx: &mut ViewContext<Self>) -> Self {
|
||||||
|
let handle = cx.weak_handle();
|
||||||
|
|
||||||
|
let settings_subscription = cx.observe_global::<Settings, _>(move |cx| {
|
||||||
|
if let Some(handle) = handle.upgrade(cx) {
|
||||||
|
handle.update(cx, |_, cx| cx.notify())
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
WelcomePage {
|
||||||
|
_settings_subscription: settings_subscription,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_cta_button<L, A>(
|
||||||
|
&self,
|
||||||
|
label: L,
|
||||||
|
action: A,
|
||||||
|
width: f32,
|
||||||
|
cx: &mut RenderContext<Self>,
|
||||||
|
) -> ElementBox
|
||||||
|
where
|
||||||
|
L: Into<Cow<'static, str>>,
|
||||||
|
A: 'static + Action + Clone,
|
||||||
|
{
|
||||||
|
let theme = cx.global::<Settings>().theme.clone();
|
||||||
|
MouseEventHandler::<A>::new(0, cx, |state, _| {
|
||||||
|
let style = theme.welcome.button.style_for(state, false);
|
||||||
|
Label::new(label, style.text.clone())
|
||||||
|
.aligned()
|
||||||
|
.contained()
|
||||||
|
.with_style(style.container)
|
||||||
|
.constrained()
|
||||||
|
.with_max_width(width)
|
||||||
|
.boxed()
|
||||||
|
})
|
||||||
|
.on_click(MouseButton::Left, move |_, cx| {
|
||||||
|
cx.dispatch_action(action.clone())
|
||||||
|
})
|
||||||
|
.with_cursor_style(gpui::CursorStyle::PointingHand)
|
||||||
|
.boxed()
|
||||||
|
}
|
||||||
|
|
||||||
|
// fn render_settings_checkbox<T: 'static>(
|
||||||
|
// &self,
|
||||||
|
// label: &'static str,
|
||||||
|
// style: &CheckboxStyle,
|
||||||
|
// checked: bool,
|
||||||
|
// cx: &mut RenderContext<Self>,
|
||||||
|
// set_value: fn(&mut SettingsFileContent, checked: bool) -> (),
|
||||||
|
// ) -> ElementBox {
|
||||||
|
// MouseEventHandler::<T>::new(0, cx, |state, _| {
|
||||||
|
// let indicator = if checked {
|
||||||
|
// Svg::new(style.check_icon.clone())
|
||||||
|
// .with_color(style.check_icon_color)
|
||||||
|
// .constrained()
|
||||||
|
// } else {
|
||||||
|
// Empty::new().constrained()
|
||||||
|
// };
|
||||||
|
|
||||||
|
// Flex::row()
|
||||||
|
// .with_children([
|
||||||
|
// indicator
|
||||||
|
// .with_width(style.width)
|
||||||
|
// .with_height(style.height)
|
||||||
|
// .contained()
|
||||||
|
// .with_style(if checked {
|
||||||
|
// if state.hovered() {
|
||||||
|
// style.hovered_and_checked
|
||||||
|
// } else {
|
||||||
|
// style.checked
|
||||||
|
// }
|
||||||
|
// } else {
|
||||||
|
// if state.hovered() {
|
||||||
|
// style.hovered
|
||||||
|
// } else {
|
||||||
|
// style.default
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// .boxed(),
|
||||||
|
// Label::new(label, style.label.text.clone())
|
||||||
|
// .contained()
|
||||||
|
// .with_style(style.label.container)
|
||||||
|
// .boxed(),
|
||||||
|
// ])
|
||||||
|
// .align_children_center()
|
||||||
|
// .boxed()
|
||||||
|
// })
|
||||||
|
// .on_click(gpui::MouseButton::Left, move |_, cx| {
|
||||||
|
// SettingsFile::update(cx, move |content| set_value(content, !checked))
|
||||||
|
// })
|
||||||
|
// .with_cursor_style(gpui::CursorStyle::PointingHand)
|
||||||
|
// .contained()
|
||||||
|
// .with_style(style.container)
|
||||||
|
// .boxed()
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Item for WelcomePage {
|
||||||
|
fn tab_content(
|
||||||
|
&self,
|
||||||
|
_detail: Option<usize>,
|
||||||
|
style: &theme::Tab,
|
||||||
|
_cx: &gpui::AppContext,
|
||||||
|
) -> gpui::ElementBox {
|
||||||
|
Flex::row()
|
||||||
|
.with_child(
|
||||||
|
Label::new("Welcome to Zed!", style.label.clone())
|
||||||
|
.aligned()
|
||||||
|
.contained()
|
||||||
|
.boxed(),
|
||||||
|
)
|
||||||
|
.boxed()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn show_toolbar(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
fn clone_on_split(
|
||||||
|
&self,
|
||||||
|
_workspace_id: WorkspaceId,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) -> Option<Self> {
|
||||||
|
Some(WelcomePage::new(cx))
|
||||||
|
}
|
||||||
|
}
|
|
@ -27,6 +27,7 @@ context_menu = { path = "../context_menu" }
|
||||||
drag_and_drop = { path = "../drag_and_drop" }
|
drag_and_drop = { path = "../drag_and_drop" }
|
||||||
fs = { path = "../fs" }
|
fs = { path = "../fs" }
|
||||||
gpui = { path = "../gpui" }
|
gpui = { path = "../gpui" }
|
||||||
|
install_cli = { path = "../install_cli" }
|
||||||
language = { path = "../language" }
|
language = { path = "../language" }
|
||||||
menu = { path = "../menu" }
|
menu = { path = "../menu" }
|
||||||
project = { path = "../project" }
|
project = { path = "../project" }
|
||||||
|
|
|
@ -13,7 +13,7 @@ use gpui::{
|
||||||
use settings::{DockAnchor, Settings};
|
use settings::{DockAnchor, Settings};
|
||||||
use theme::Theme;
|
use theme::Theme;
|
||||||
|
|
||||||
use crate::{sidebar::SidebarSide, ItemHandle, Pane, Workspace};
|
use crate::{sidebar::SidebarSide, BackgroundActions, ItemHandle, Pane, Workspace};
|
||||||
pub use toggle_dock_button::ToggleDockButton;
|
pub use toggle_dock_button::ToggleDockButton;
|
||||||
|
|
||||||
#[derive(PartialEq, Clone, Deserialize)]
|
#[derive(PartialEq, Clone, Deserialize)]
|
||||||
|
@ -39,20 +39,24 @@ impl_internal_actions!(dock, [MoveDock, AddDefaultItemToDock]);
|
||||||
pub fn init(cx: &mut MutableAppContext) {
|
pub fn init(cx: &mut MutableAppContext) {
|
||||||
cx.add_action(Dock::focus_dock);
|
cx.add_action(Dock::focus_dock);
|
||||||
cx.add_action(Dock::hide_dock);
|
cx.add_action(Dock::hide_dock);
|
||||||
cx.add_action(Dock::move_dock);
|
cx.add_action(
|
||||||
|
|workspace: &mut Workspace, &MoveDock(dock_anchor), cx: &mut ViewContext<Workspace>| {
|
||||||
|
Dock::move_dock(workspace, dock_anchor, true, cx);
|
||||||
|
},
|
||||||
|
);
|
||||||
cx.add_action(
|
cx.add_action(
|
||||||
|workspace: &mut Workspace, _: &AnchorDockRight, cx: &mut ViewContext<Workspace>| {
|
|workspace: &mut Workspace, _: &AnchorDockRight, cx: &mut ViewContext<Workspace>| {
|
||||||
Dock::move_dock(workspace, &MoveDock(DockAnchor::Right), cx)
|
Dock::move_dock(workspace, DockAnchor::Right, true, cx);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
cx.add_action(
|
cx.add_action(
|
||||||
|workspace: &mut Workspace, _: &AnchorDockBottom, cx: &mut ViewContext<Workspace>| {
|
|workspace: &mut Workspace, _: &AnchorDockBottom, cx: &mut ViewContext<Workspace>| {
|
||||||
Dock::move_dock(workspace, &MoveDock(DockAnchor::Bottom), cx)
|
Dock::move_dock(workspace, DockAnchor::Bottom, true, cx)
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
cx.add_action(
|
cx.add_action(
|
||||||
|workspace: &mut Workspace, _: &ExpandDock, cx: &mut ViewContext<Workspace>| {
|
|workspace: &mut Workspace, _: &ExpandDock, cx: &mut ViewContext<Workspace>| {
|
||||||
Dock::move_dock(workspace, &MoveDock(DockAnchor::Expanded), cx)
|
Dock::move_dock(workspace, DockAnchor::Expanded, true, cx)
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
cx.add_action(
|
cx.add_action(
|
||||||
|
@ -177,12 +181,21 @@ pub struct Dock {
|
||||||
|
|
||||||
impl Dock {
|
impl Dock {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
|
workspace_id: usize,
|
||||||
default_item_factory: DockDefaultItemFactory,
|
default_item_factory: DockDefaultItemFactory,
|
||||||
|
background_actions: BackgroundActions,
|
||||||
cx: &mut ViewContext<Workspace>,
|
cx: &mut ViewContext<Workspace>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let position = DockPosition::Hidden(cx.global::<Settings>().default_dock_anchor);
|
let position = DockPosition::Hidden(cx.global::<Settings>().default_dock_anchor);
|
||||||
|
|
||||||
let pane = cx.add_view(|cx| Pane::new(Some(position.anchor()), cx));
|
let pane = cx.add_view(|cx| {
|
||||||
|
Pane::new(
|
||||||
|
workspace_id,
|
||||||
|
Some(position.anchor()),
|
||||||
|
background_actions,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
});
|
||||||
pane.update(cx, |pane, cx| {
|
pane.update(cx, |pane, cx| {
|
||||||
pane.set_active(false, cx);
|
pane.set_active(false, cx);
|
||||||
});
|
});
|
||||||
|
@ -215,6 +228,7 @@ impl Dock {
|
||||||
pub(crate) fn set_dock_position(
|
pub(crate) fn set_dock_position(
|
||||||
workspace: &mut Workspace,
|
workspace: &mut Workspace,
|
||||||
new_position: DockPosition,
|
new_position: DockPosition,
|
||||||
|
focus: bool,
|
||||||
cx: &mut ViewContext<Workspace>,
|
cx: &mut ViewContext<Workspace>,
|
||||||
) {
|
) {
|
||||||
workspace.dock.position = new_position;
|
workspace.dock.position = new_position;
|
||||||
|
@ -235,19 +249,23 @@ impl Dock {
|
||||||
let pane = workspace.dock.pane.clone();
|
let pane = workspace.dock.pane.clone();
|
||||||
if pane.read(cx).items().next().is_none() {
|
if pane.read(cx).items().next().is_none() {
|
||||||
if let Some(item_to_add) = (workspace.dock.default_item_factory)(workspace, cx) {
|
if let Some(item_to_add) = (workspace.dock.default_item_factory)(workspace, cx) {
|
||||||
Pane::add_item(workspace, &pane, item_to_add, true, true, None, cx);
|
Pane::add_item(workspace, &pane, item_to_add, focus, focus, None, cx);
|
||||||
} else {
|
} else {
|
||||||
workspace.dock.position = workspace.dock.position.hide();
|
workspace.dock.position = workspace.dock.position.hide();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
cx.focus(pane);
|
if focus {
|
||||||
|
cx.focus(pane);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if let Some(last_active_center_pane) = workspace
|
} else if let Some(last_active_center_pane) = workspace
|
||||||
.last_active_center_pane
|
.last_active_center_pane
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|pane| pane.upgrade(cx))
|
.and_then(|pane| pane.upgrade(cx))
|
||||||
{
|
{
|
||||||
cx.focus(last_active_center_pane);
|
if focus {
|
||||||
|
cx.focus(last_active_center_pane);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
cx.emit(crate::Event::DockAnchorChanged);
|
cx.emit(crate::Event::DockAnchorChanged);
|
||||||
workspace.serialize_workspace(cx);
|
workspace.serialize_workspace(cx);
|
||||||
|
@ -255,11 +273,11 @@ impl Dock {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn hide(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
|
pub fn hide(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
|
||||||
Self::set_dock_position(workspace, workspace.dock.position.hide(), cx);
|
Self::set_dock_position(workspace, workspace.dock.position.hide(), true, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn show(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
|
pub fn show(workspace: &mut Workspace, focus: bool, cx: &mut ViewContext<Workspace>) {
|
||||||
Self::set_dock_position(workspace, workspace.dock.position.show(), cx);
|
Self::set_dock_position(workspace, workspace.dock.position.show(), focus, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn hide_on_sidebar_shown(
|
pub fn hide_on_sidebar_shown(
|
||||||
|
@ -275,19 +293,20 @@ impl Dock {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn focus_dock(workspace: &mut Workspace, _: &FocusDock, cx: &mut ViewContext<Workspace>) {
|
fn focus_dock(workspace: &mut Workspace, _: &FocusDock, cx: &mut ViewContext<Workspace>) {
|
||||||
Self::set_dock_position(workspace, workspace.dock.position.show(), cx);
|
Self::set_dock_position(workspace, workspace.dock.position.show(), true, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn hide_dock(workspace: &mut Workspace, _: &HideDock, cx: &mut ViewContext<Workspace>) {
|
fn hide_dock(workspace: &mut Workspace, _: &HideDock, cx: &mut ViewContext<Workspace>) {
|
||||||
Self::set_dock_position(workspace, workspace.dock.position.hide(), cx);
|
Self::set_dock_position(workspace, workspace.dock.position.hide(), true, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn move_dock(
|
pub fn move_dock(
|
||||||
workspace: &mut Workspace,
|
workspace: &mut Workspace,
|
||||||
&MoveDock(new_anchor): &MoveDock,
|
new_anchor: DockAnchor,
|
||||||
|
focus: bool,
|
||||||
cx: &mut ViewContext<Workspace>,
|
cx: &mut ViewContext<Workspace>,
|
||||||
) {
|
) {
|
||||||
Self::set_dock_position(workspace, DockPosition::Shown(new_anchor), cx);
|
Self::set_dock_position(workspace, DockPosition::Shown(new_anchor), focus, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn render(
|
pub fn render(
|
||||||
|
@ -482,6 +501,7 @@ mod tests {
|
||||||
0,
|
0,
|
||||||
project.clone(),
|
project.clone(),
|
||||||
default_item_factory,
|
default_item_factory,
|
||||||
|
|| &[],
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
@ -610,7 +630,14 @@ mod tests {
|
||||||
cx.update(|cx| init(cx));
|
cx.update(|cx| init(cx));
|
||||||
let project = Project::test(fs, [], cx).await;
|
let project = Project::test(fs, [], cx).await;
|
||||||
let (window_id, workspace) = cx.add_window(|cx| {
|
let (window_id, workspace) = cx.add_window(|cx| {
|
||||||
Workspace::new(Default::default(), 0, project, default_item_factory, cx)
|
Workspace::new(
|
||||||
|
Default::default(),
|
||||||
|
0,
|
||||||
|
project,
|
||||||
|
default_item_factory,
|
||||||
|
|| &[],
|
||||||
|
cx,
|
||||||
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
workspace.update(cx, |workspace, cx| {
|
workspace.update(cx, |workspace, cx| {
|
||||||
|
|
|
@ -42,6 +42,7 @@ impl View for ToggleDockButton {
|
||||||
|
|
||||||
let workspace = workspace.unwrap();
|
let workspace = workspace.unwrap();
|
||||||
let dock_position = workspace.read(cx).dock.position;
|
let dock_position = workspace.read(cx).dock.position;
|
||||||
|
let dock_pane = workspace.read(cx.app).dock_pane().clone();
|
||||||
|
|
||||||
let theme = cx.global::<Settings>().theme.clone();
|
let theme = cx.global::<Settings>().theme.clone();
|
||||||
|
|
||||||
|
@ -67,7 +68,6 @@ impl View for ToggleDockButton {
|
||||||
})
|
})
|
||||||
.with_cursor_style(CursorStyle::PointingHand)
|
.with_cursor_style(CursorStyle::PointingHand)
|
||||||
.on_up(MouseButton::Left, move |event, cx| {
|
.on_up(MouseButton::Left, move |event, cx| {
|
||||||
let dock_pane = workspace.read(cx.app).dock_pane();
|
|
||||||
let drop_index = dock_pane.read(cx.app).items_len() + 1;
|
let drop_index = dock_pane.read(cx.app).items_len() + 1;
|
||||||
handle_dropped_item(event, &dock_pane.downgrade(), drop_index, false, None, cx);
|
handle_dropped_item(event, &dock_pane.downgrade(), drop_index, false, None, cx);
|
||||||
});
|
});
|
||||||
|
|
|
@ -151,6 +151,9 @@ pub trait Item: View {
|
||||||
"deserialize() must be implemented if serialized_item_kind() returns Some(_)"
|
"deserialize() must be implemented if serialized_item_kind() returns Some(_)"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
fn show_toolbar(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait ItemHandle: 'static + fmt::Debug {
|
pub trait ItemHandle: 'static + fmt::Debug {
|
||||||
|
@ -213,6 +216,7 @@ pub trait ItemHandle: 'static + fmt::Debug {
|
||||||
fn breadcrumb_location(&self, cx: &AppContext) -> ToolbarItemLocation;
|
fn breadcrumb_location(&self, cx: &AppContext) -> ToolbarItemLocation;
|
||||||
fn breadcrumbs(&self, theme: &Theme, cx: &AppContext) -> Option<Vec<ElementBox>>;
|
fn breadcrumbs(&self, theme: &Theme, cx: &AppContext) -> Option<Vec<ElementBox>>;
|
||||||
fn serialized_item_kind(&self) -> Option<&'static str>;
|
fn serialized_item_kind(&self) -> Option<&'static str>;
|
||||||
|
fn show_toolbar(&self, cx: &AppContext) -> bool;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait WeakItemHandle {
|
pub trait WeakItemHandle {
|
||||||
|
@ -591,6 +595,10 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
|
||||||
fn serialized_item_kind(&self) -> Option<&'static str> {
|
fn serialized_item_kind(&self) -> Option<&'static str> {
|
||||||
T::serialized_item_kind()
|
T::serialized_item_kind()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn show_toolbar(&self, cx: &AppContext) -> bool {
|
||||||
|
self.read(cx).show_toolbar()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<Box<dyn ItemHandle>> for AnyViewHandle {
|
impl From<Box<dyn ItemHandle>> for AnyViewHandle {
|
||||||
|
|
|
@ -122,6 +122,8 @@ impl Workspace {
|
||||||
|
|
||||||
pub mod simple_message_notification {
|
pub mod simple_message_notification {
|
||||||
|
|
||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions,
|
actions,
|
||||||
elements::{Flex, MouseEventHandler, Padding, ParentElement, Svg, Text},
|
elements::{Flex, MouseEventHandler, Padding, ParentElement, Svg, Text},
|
||||||
|
@ -153,9 +155,9 @@ pub mod simple_message_notification {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct MessageNotification {
|
pub struct MessageNotification {
|
||||||
message: String,
|
message: Cow<'static, str>,
|
||||||
click_action: Option<Box<dyn Action>>,
|
click_action: Option<Box<dyn Action>>,
|
||||||
click_message: Option<String>,
|
click_message: Option<Cow<'static, str>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum MessageNotificationEvent {
|
pub enum MessageNotificationEvent {
|
||||||
|
@ -167,23 +169,23 @@ pub mod simple_message_notification {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MessageNotification {
|
impl MessageNotification {
|
||||||
pub fn new_message<S: AsRef<str>>(message: S) -> MessageNotification {
|
pub fn new_message<S: Into<Cow<'static, str>>>(message: S) -> MessageNotification {
|
||||||
Self {
|
Self {
|
||||||
message: message.as_ref().to_string(),
|
message: message.into(),
|
||||||
click_action: None,
|
click_action: None,
|
||||||
click_message: None,
|
click_message: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new<S1: AsRef<str>, A: Action, S2: AsRef<str>>(
|
pub fn new<S1: Into<Cow<'static, str>>, A: Action, S2: Into<Cow<'static, str>>>(
|
||||||
message: S1,
|
message: S1,
|
||||||
click_action: A,
|
click_action: A,
|
||||||
click_message: S2,
|
click_message: S2,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
message: message.as_ref().to_string(),
|
message: message.into(),
|
||||||
click_action: Some(Box::new(click_action) as Box<dyn Action>),
|
click_action: Some(Box::new(click_action) as Box<dyn Action>),
|
||||||
click_message: Some(click_message.as_ref().to_string()),
|
click_message: Some(click_message.into()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -210,6 +212,8 @@ pub mod simple_message_notification {
|
||||||
let click_message = self.click_message.as_ref().map(|message| message.clone());
|
let click_message = self.click_message.as_ref().map(|message| message.clone());
|
||||||
let message = self.message.clone();
|
let message = self.message.clone();
|
||||||
|
|
||||||
|
let has_click_action = click_action.is_some();
|
||||||
|
|
||||||
MouseEventHandler::<MessageNotificationTag>::new(0, cx, |state, cx| {
|
MouseEventHandler::<MessageNotificationTag>::new(0, cx, |state, cx| {
|
||||||
Flex::column()
|
Flex::column()
|
||||||
.with_child(
|
.with_child(
|
||||||
|
@ -243,6 +247,7 @@ pub mod simple_message_notification {
|
||||||
.on_click(MouseButton::Left, move |_, cx| {
|
.on_click(MouseButton::Left, move |_, cx| {
|
||||||
cx.dispatch_action(CancelMessageNotification)
|
cx.dispatch_action(CancelMessageNotification)
|
||||||
})
|
})
|
||||||
|
.with_cursor_style(CursorStyle::PointingHand)
|
||||||
.aligned()
|
.aligned()
|
||||||
.constrained()
|
.constrained()
|
||||||
.with_height(
|
.with_height(
|
||||||
|
@ -272,12 +277,19 @@ pub mod simple_message_notification {
|
||||||
.contained()
|
.contained()
|
||||||
.boxed()
|
.boxed()
|
||||||
})
|
})
|
||||||
.with_cursor_style(CursorStyle::PointingHand)
|
// Since we're not using a proper overlay, we have to capture these extra events
|
||||||
|
.on_down(MouseButton::Left, |_, _| {})
|
||||||
|
.on_up(MouseButton::Left, |_, _| {})
|
||||||
.on_click(MouseButton::Left, move |_, cx| {
|
.on_click(MouseButton::Left, move |_, cx| {
|
||||||
if let Some(click_action) = click_action.as_ref() {
|
if let Some(click_action) = click_action.as_ref() {
|
||||||
cx.dispatch_any_action(click_action.boxed_clone())
|
cx.dispatch_any_action(click_action.boxed_clone())
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
.with_cursor_style(if has_click_action {
|
||||||
|
CursorStyle::PointingHand
|
||||||
|
} else {
|
||||||
|
CursorStyle::Arrow
|
||||||
|
})
|
||||||
.boxed()
|
.boxed()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,8 +24,8 @@ use gpui::{
|
||||||
keymap_matcher::KeymapContext,
|
keymap_matcher::KeymapContext,
|
||||||
platform::{CursorStyle, NavigationDirection},
|
platform::{CursorStyle, NavigationDirection},
|
||||||
Action, AnyViewHandle, AnyWeakViewHandle, AppContext, AsyncAppContext, Entity, EventContext,
|
Action, AnyViewHandle, AnyWeakViewHandle, AppContext, AsyncAppContext, Entity, EventContext,
|
||||||
ModelHandle, MouseButton, MutableAppContext, PromptLevel, Quad, RenderContext, Task, View,
|
ModelHandle, MouseButton, MouseRegion, MutableAppContext, PromptLevel, Quad, RenderContext,
|
||||||
ViewContext, ViewHandle, WeakViewHandle,
|
Task, View, ViewContext, ViewHandle, WeakViewHandle,
|
||||||
};
|
};
|
||||||
use project::{Project, ProjectEntryId, ProjectPath};
|
use project::{Project, ProjectEntryId, ProjectPath};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
@ -110,6 +110,8 @@ impl_internal_actions!(
|
||||||
|
|
||||||
const MAX_NAVIGATION_HISTORY_LEN: usize = 1024;
|
const MAX_NAVIGATION_HISTORY_LEN: usize = 1024;
|
||||||
|
|
||||||
|
pub type BackgroundActions = fn() -> &'static [(&'static str, &'static dyn Action)];
|
||||||
|
|
||||||
pub fn init(cx: &mut MutableAppContext) {
|
pub fn init(cx: &mut MutableAppContext) {
|
||||||
cx.add_action(|pane: &mut Pane, action: &ActivateItem, cx| {
|
cx.add_action(|pane: &mut Pane, action: &ActivateItem, cx| {
|
||||||
pane.activate_item(action.0, true, true, cx);
|
pane.activate_item(action.0, true, true, cx);
|
||||||
|
@ -215,6 +217,8 @@ pub struct Pane {
|
||||||
toolbar: ViewHandle<Toolbar>,
|
toolbar: ViewHandle<Toolbar>,
|
||||||
tab_bar_context_menu: ViewHandle<ContextMenu>,
|
tab_bar_context_menu: ViewHandle<ContextMenu>,
|
||||||
docked: Option<DockAnchor>,
|
docked: Option<DockAnchor>,
|
||||||
|
_background_actions: BackgroundActions,
|
||||||
|
_workspace_id: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ItemNavHistory {
|
pub struct ItemNavHistory {
|
||||||
|
@ -271,7 +275,12 @@ enum ItemType {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Pane {
|
impl Pane {
|
||||||
pub fn new(docked: Option<DockAnchor>, cx: &mut ViewContext<Self>) -> Self {
|
pub fn new(
|
||||||
|
workspace_id: usize,
|
||||||
|
docked: Option<DockAnchor>,
|
||||||
|
background_actions: BackgroundActions,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) -> Self {
|
||||||
let handle = cx.weak_handle();
|
let handle = cx.weak_handle();
|
||||||
let context_menu = cx.add_view(ContextMenu::new);
|
let context_menu = cx.add_view(ContextMenu::new);
|
||||||
Self {
|
Self {
|
||||||
|
@ -292,6 +301,8 @@ impl Pane {
|
||||||
toolbar: cx.add_view(|_| Toolbar::new(handle)),
|
toolbar: cx.add_view(|_| Toolbar::new(handle)),
|
||||||
tab_bar_context_menu: context_menu,
|
tab_bar_context_menu: context_menu,
|
||||||
docked,
|
docked,
|
||||||
|
_background_actions: background_actions,
|
||||||
|
_workspace_id: workspace_id,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1415,6 +1426,14 @@ impl Pane {
|
||||||
.flex(1., false)
|
.flex(1., false)
|
||||||
.boxed()
|
.boxed()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render_blank_pane(&mut self, theme: &Theme, _cx: &mut RenderContext<Self>) -> ElementBox {
|
||||||
|
let background = theme.workspace.background;
|
||||||
|
Empty::new()
|
||||||
|
.contained()
|
||||||
|
.with_background_color(background)
|
||||||
|
.boxed()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Entity for Pane {
|
impl Entity for Pane {
|
||||||
|
@ -1485,11 +1504,12 @@ impl View for Pane {
|
||||||
cx,
|
cx,
|
||||||
{
|
{
|
||||||
let toolbar = self.toolbar.clone();
|
let toolbar = self.toolbar.clone();
|
||||||
|
let toolbar_hidden = toolbar.read(cx).hidden();
|
||||||
move |_, cx| {
|
move |_, cx| {
|
||||||
Flex::column()
|
Flex::column()
|
||||||
.with_child(
|
.with_children((!toolbar_hidden).then(|| {
|
||||||
ChildView::new(&toolbar, cx).expanded().boxed(),
|
ChildView::new(&toolbar, cx).expanded().boxed()
|
||||||
)
|
}))
|
||||||
.with_child(
|
.with_child(
|
||||||
ChildView::new(active_item, cx)
|
ChildView::new(active_item, cx)
|
||||||
.flex(1., true)
|
.flex(1., true)
|
||||||
|
@ -1507,11 +1527,8 @@ impl View for Pane {
|
||||||
enum EmptyPane {}
|
enum EmptyPane {}
|
||||||
let theme = cx.global::<Settings>().theme.clone();
|
let theme = cx.global::<Settings>().theme.clone();
|
||||||
|
|
||||||
dragged_item_receiver::<EmptyPane, _>(0, 0, false, None, cx, |_, _| {
|
dragged_item_receiver::<EmptyPane, _>(0, 0, false, None, cx, |_, cx| {
|
||||||
Empty::new()
|
self.render_blank_pane(&theme, cx)
|
||||||
.contained()
|
|
||||||
.with_background_color(theme.workspace.background)
|
|
||||||
.boxed()
|
|
||||||
})
|
})
|
||||||
.on_down(MouseButton::Left, |_, cx| {
|
.on_down(MouseButton::Left, |_, cx| {
|
||||||
cx.focus_parent_view();
|
cx.focus_parent_view();
|
||||||
|
@ -1705,6 +1722,93 @@ impl NavHistory {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct PaneBackdrop {
|
||||||
|
child_view: usize,
|
||||||
|
child: ElementBox,
|
||||||
|
}
|
||||||
|
impl PaneBackdrop {
|
||||||
|
pub fn new(pane_item_view: usize, child: ElementBox) -> Self {
|
||||||
|
PaneBackdrop {
|
||||||
|
child,
|
||||||
|
child_view: pane_item_view,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Element for PaneBackdrop {
|
||||||
|
type LayoutState = ();
|
||||||
|
|
||||||
|
type PaintState = ();
|
||||||
|
|
||||||
|
fn layout(
|
||||||
|
&mut self,
|
||||||
|
constraint: gpui::SizeConstraint,
|
||||||
|
cx: &mut gpui::LayoutContext,
|
||||||
|
) -> (Vector2F, Self::LayoutState) {
|
||||||
|
let size = self.child.layout(constraint, cx);
|
||||||
|
(size, ())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn paint(
|
||||||
|
&mut self,
|
||||||
|
bounds: RectF,
|
||||||
|
visible_bounds: RectF,
|
||||||
|
_: &mut Self::LayoutState,
|
||||||
|
cx: &mut gpui::PaintContext,
|
||||||
|
) -> Self::PaintState {
|
||||||
|
let background = cx.global::<Settings>().theme.editor.background;
|
||||||
|
|
||||||
|
let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default();
|
||||||
|
|
||||||
|
cx.scene.push_quad(gpui::Quad {
|
||||||
|
bounds: RectF::new(bounds.origin(), bounds.size()),
|
||||||
|
background: Some(background),
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
|
||||||
|
let child_view_id = self.child_view;
|
||||||
|
cx.scene.push_mouse_region(
|
||||||
|
MouseRegion::new::<Self>(child_view_id, 0, visible_bounds).on_down(
|
||||||
|
gpui::MouseButton::Left,
|
||||||
|
move |_, cx| {
|
||||||
|
let window_id = cx.window_id;
|
||||||
|
cx.focus(window_id, Some(child_view_id))
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
cx.paint_layer(Some(bounds), |cx| {
|
||||||
|
self.child.paint(bounds.origin(), visible_bounds, cx)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rect_for_text_range(
|
||||||
|
&self,
|
||||||
|
range_utf16: std::ops::Range<usize>,
|
||||||
|
_bounds: RectF,
|
||||||
|
_visible_bounds: RectF,
|
||||||
|
_layout: &Self::LayoutState,
|
||||||
|
_paint: &Self::PaintState,
|
||||||
|
cx: &gpui::MeasurementContext,
|
||||||
|
) -> Option<RectF> {
|
||||||
|
self.child.rect_for_text_range(range_utf16, cx)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn debug(
|
||||||
|
&self,
|
||||||
|
_bounds: RectF,
|
||||||
|
_layout: &Self::LayoutState,
|
||||||
|
_paint: &Self::PaintState,
|
||||||
|
cx: &gpui::DebugContext,
|
||||||
|
) -> serde_json::Value {
|
||||||
|
gpui::json::json!({
|
||||||
|
"type": "Pane Back Drop",
|
||||||
|
"view": self.child_view,
|
||||||
|
"child": self.child.debug(cx),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
@ -1721,9 +1825,7 @@ mod tests {
|
||||||
let fs = FakeFs::new(cx.background());
|
let fs = FakeFs::new(cx.background());
|
||||||
|
|
||||||
let project = Project::test(fs, None, cx).await;
|
let project = Project::test(fs, None, cx).await;
|
||||||
let (_, workspace) = cx.add_window(|cx| {
|
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
|
||||||
Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
|
|
||||||
});
|
|
||||||
let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
|
let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
|
||||||
|
|
||||||
// 1. Add with a destination index
|
// 1. Add with a destination index
|
||||||
|
@ -1811,9 +1913,7 @@ mod tests {
|
||||||
let fs = FakeFs::new(cx.background());
|
let fs = FakeFs::new(cx.background());
|
||||||
|
|
||||||
let project = Project::test(fs, None, cx).await;
|
let project = Project::test(fs, None, cx).await;
|
||||||
let (_, workspace) = cx.add_window(|cx| {
|
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
|
||||||
Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
|
|
||||||
});
|
|
||||||
let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
|
let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
|
||||||
|
|
||||||
// 1. Add with a destination index
|
// 1. Add with a destination index
|
||||||
|
@ -1889,9 +1989,7 @@ mod tests {
|
||||||
let fs = FakeFs::new(cx.background());
|
let fs = FakeFs::new(cx.background());
|
||||||
|
|
||||||
let project = Project::test(fs, None, cx).await;
|
let project = Project::test(fs, None, cx).await;
|
||||||
let (_, workspace) = cx.add_window(|cx| {
|
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
|
||||||
Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
|
|
||||||
});
|
|
||||||
let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
|
let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
|
||||||
|
|
||||||
// singleton view
|
// singleton view
|
||||||
|
@ -2000,8 +2098,7 @@ mod tests {
|
||||||
let fs = FakeFs::new(cx.background());
|
let fs = FakeFs::new(cx.background());
|
||||||
|
|
||||||
let project = Project::test(fs, None, cx).await;
|
let project = Project::test(fs, None, cx).await;
|
||||||
let (_, workspace) =
|
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
|
||||||
cx.add_window(|cx| Workspace::new(None, 0, project, |_, _| unimplemented!(), cx));
|
|
||||||
let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
|
let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
|
||||||
|
|
||||||
add_labled_item(&workspace, &pane, "A", cx);
|
add_labled_item(&workspace, &pane, "A", cx);
|
||||||
|
|
|
@ -42,6 +42,7 @@ pub enum ToolbarItemLocation {
|
||||||
|
|
||||||
pub struct Toolbar {
|
pub struct Toolbar {
|
||||||
active_pane_item: Option<Box<dyn ItemHandle>>,
|
active_pane_item: Option<Box<dyn ItemHandle>>,
|
||||||
|
hidden: bool,
|
||||||
pane: WeakViewHandle<Pane>,
|
pane: WeakViewHandle<Pane>,
|
||||||
items: Vec<(Box<dyn ToolbarItemViewHandle>, ToolbarItemLocation)>,
|
items: Vec<(Box<dyn ToolbarItemViewHandle>, ToolbarItemLocation)>,
|
||||||
}
|
}
|
||||||
|
@ -211,6 +212,7 @@ impl Toolbar {
|
||||||
active_pane_item: None,
|
active_pane_item: None,
|
||||||
pane,
|
pane,
|
||||||
items: Default::default(),
|
items: Default::default(),
|
||||||
|
hidden: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -243,6 +245,12 @@ impl Toolbar {
|
||||||
cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
) {
|
) {
|
||||||
self.active_pane_item = pane_item.map(|item| item.boxed_clone());
|
self.active_pane_item = pane_item.map(|item| item.boxed_clone());
|
||||||
|
self.hidden = self
|
||||||
|
.active_pane_item
|
||||||
|
.as_ref()
|
||||||
|
.map(|item| !item.show_toolbar(cx))
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
for (toolbar_item, current_location) in self.items.iter_mut() {
|
for (toolbar_item, current_location) in self.items.iter_mut() {
|
||||||
let new_location = toolbar_item.set_active_pane_item(pane_item, cx);
|
let new_location = toolbar_item.set_active_pane_item(pane_item, cx);
|
||||||
if new_location != *current_location {
|
if new_location != *current_location {
|
||||||
|
@ -257,6 +265,10 @@ impl Toolbar {
|
||||||
.iter()
|
.iter()
|
||||||
.find_map(|(item, _)| item.to_any().downcast())
|
.find_map(|(item, _)| item.to_any().downcast())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn hidden(&self) -> bool {
|
||||||
|
self.hidden
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: ToolbarItemView> ToolbarItemViewHandle for ViewHandle<T> {
|
impl<T: ToolbarItemView> ToolbarItemViewHandle for ViewHandle<T> {
|
||||||
|
|
|
@ -16,7 +16,7 @@ mod toolbar;
|
||||||
|
|
||||||
pub use smallvec;
|
pub use smallvec;
|
||||||
|
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Context, Result};
|
||||||
use call::ActiveCall;
|
use call::ActiveCall;
|
||||||
use client::{
|
use client::{
|
||||||
proto::{self, PeerId},
|
proto::{self, PeerId},
|
||||||
|
@ -43,7 +43,8 @@ use gpui::{
|
||||||
platform::{CursorStyle, WindowOptions},
|
platform::{CursorStyle, WindowOptions},
|
||||||
AnyModelHandle, AnyViewHandle, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle,
|
AnyModelHandle, AnyViewHandle, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle,
|
||||||
MouseButton, MutableAppContext, PathPromptOptions, Platform, PromptLevel, RenderContext,
|
MouseButton, MutableAppContext, PathPromptOptions, Platform, PromptLevel, RenderContext,
|
||||||
SizeConstraint, Task, View, ViewContext, ViewHandle, WeakViewHandle, WindowBounds,
|
SizeConstraint, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle,
|
||||||
|
WindowBounds,
|
||||||
};
|
};
|
||||||
use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ProjectItem};
|
use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ProjectItem};
|
||||||
use language::LanguageRegistry;
|
use language::LanguageRegistry;
|
||||||
|
@ -63,7 +64,7 @@ use crate::{
|
||||||
};
|
};
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use log::{error, warn};
|
use log::{error, warn};
|
||||||
use notifications::NotificationHandle;
|
use notifications::{NotificationHandle, NotifyResultExt};
|
||||||
pub use pane::*;
|
pub use pane::*;
|
||||||
pub use pane_group::*;
|
pub use pane_group::*;
|
||||||
use persistence::{model::SerializedItem, DB};
|
use persistence::{model::SerializedItem, DB};
|
||||||
|
@ -116,7 +117,8 @@ actions!(
|
||||||
NewTerminal,
|
NewTerminal,
|
||||||
NewSearch,
|
NewSearch,
|
||||||
Feedback,
|
Feedback,
|
||||||
Restart
|
Restart,
|
||||||
|
Welcome
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -185,21 +187,66 @@ pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
|
||||||
dock::init(cx);
|
dock::init(cx);
|
||||||
notifications::init(cx);
|
notifications::init(cx);
|
||||||
|
|
||||||
cx.add_global_action(open);
|
cx.add_global_action(|_: &Open, cx: &mut MutableAppContext| {
|
||||||
|
let mut paths = cx.prompt_for_paths(PathPromptOptions {
|
||||||
|
files: true,
|
||||||
|
directories: true,
|
||||||
|
multiple: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.spawn(|mut cx| async move {
|
||||||
|
if let Some(paths) = paths.recv().await.flatten() {
|
||||||
|
cx.update(|cx| cx.dispatch_global_action(OpenPaths { paths }));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
});
|
||||||
|
cx.add_action(|_, _: &Open, cx: &mut ViewContext<Workspace>| {
|
||||||
|
let mut paths = cx.prompt_for_paths(PathPromptOptions {
|
||||||
|
files: true,
|
||||||
|
directories: true,
|
||||||
|
multiple: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
let handle = cx.handle().downgrade();
|
||||||
|
cx.spawn(|_, mut cx| async move {
|
||||||
|
if let Some(paths) = paths.recv().await.flatten() {
|
||||||
|
cx.update(|cx| {
|
||||||
|
cx.dispatch_action_at(handle.window_id(), handle.id(), OpenPaths { paths })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
});
|
||||||
cx.add_global_action({
|
cx.add_global_action({
|
||||||
let app_state = Arc::downgrade(&app_state);
|
let app_state = Arc::downgrade(&app_state);
|
||||||
move |action: &OpenPaths, cx: &mut MutableAppContext| {
|
move |action: &OpenPaths, cx: &mut MutableAppContext| {
|
||||||
if let Some(app_state) = app_state.upgrade() {
|
if let Some(app_state) = app_state.upgrade() {
|
||||||
open_paths(&action.paths, &app_state, cx).detach();
|
open_paths(&action.paths, &app_state, None, cx).detach();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
cx.add_global_action({
|
cx.add_async_action({
|
||||||
let app_state = Arc::downgrade(&app_state);
|
let app_state = Arc::downgrade(&app_state);
|
||||||
move |_: &NewFile, cx: &mut MutableAppContext| {
|
move |workspace, action: &OpenPaths, cx: &mut ViewContext<Workspace>| {
|
||||||
if let Some(app_state) = app_state.upgrade() {
|
if !workspace.project().read(cx).is_local() {
|
||||||
open_new(&app_state, cx).detach();
|
cx.propagate_action();
|
||||||
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let app_state = app_state.upgrade()?;
|
||||||
|
let window_id = cx.window_id();
|
||||||
|
let action = action.clone();
|
||||||
|
let close = workspace.prepare_to_close(false, cx);
|
||||||
|
|
||||||
|
Some(cx.spawn_weak(|_, mut cx| async move {
|
||||||
|
let can_close = close.await?;
|
||||||
|
if can_close {
|
||||||
|
cx.update(|cx| open_paths(&action.paths, &app_state, Some(window_id), cx))
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -207,7 +254,7 @@ pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
|
||||||
let app_state = Arc::downgrade(&app_state);
|
let app_state = Arc::downgrade(&app_state);
|
||||||
move |_: &NewWindow, cx: &mut MutableAppContext| {
|
move |_: &NewWindow, cx: &mut MutableAppContext| {
|
||||||
if let Some(app_state) = app_state.upgrade() {
|
if let Some(app_state) = app_state.upgrade() {
|
||||||
open_new(&app_state, cx).detach();
|
open_new(&app_state, cx, |_, cx| cx.dispatch_action(NewFile)).detach();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -273,6 +320,31 @@ pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
cx.add_action(|_: &mut Workspace, _: &install_cli::Install, cx| {
|
||||||
|
cx.spawn(|workspace, mut cx| async move {
|
||||||
|
let err = install_cli::install_cli(&cx)
|
||||||
|
.await
|
||||||
|
.context("Failed to create CLI symlink");
|
||||||
|
|
||||||
|
cx.update(|cx| {
|
||||||
|
workspace.update(cx, |workspace, cx| {
|
||||||
|
if matches!(err, Err(_)) {
|
||||||
|
err.notify_err(workspace, cx);
|
||||||
|
} else {
|
||||||
|
workspace.show_notification(1, cx, |cx| {
|
||||||
|
cx.add_view(|_| {
|
||||||
|
MessageNotification::new_message(
|
||||||
|
"Successfully installed the `zed` binary",
|
||||||
|
)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
});
|
||||||
|
|
||||||
let client = &app_state.client;
|
let client = &app_state.client;
|
||||||
client.add_view_request_handler(Workspace::handle_follow);
|
client.add_view_request_handler(Workspace::handle_follow);
|
||||||
client.add_view_message_handler(Workspace::handle_unfollow);
|
client.add_view_message_handler(Workspace::handle_unfollow);
|
||||||
|
@ -358,6 +430,7 @@ pub struct AppState {
|
||||||
fn(Option<WindowBounds>, Option<uuid::Uuid>, &dyn Platform) -> WindowOptions<'static>,
|
fn(Option<WindowBounds>, Option<uuid::Uuid>, &dyn Platform) -> WindowOptions<'static>,
|
||||||
pub initialize_workspace: fn(&mut Workspace, &Arc<AppState>, &mut ViewContext<Workspace>),
|
pub initialize_workspace: fn(&mut Workspace, &Arc<AppState>, &mut ViewContext<Workspace>),
|
||||||
pub dock_default_item_factory: DockDefaultItemFactory,
|
pub dock_default_item_factory: DockDefaultItemFactory,
|
||||||
|
pub background_actions: BackgroundActions,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AppState {
|
impl AppState {
|
||||||
|
@ -380,7 +453,8 @@ impl AppState {
|
||||||
user_store,
|
user_store,
|
||||||
initialize_workspace: |_, _, _| {},
|
initialize_workspace: |_, _, _| {},
|
||||||
build_window_options: |_, _, _| Default::default(),
|
build_window_options: |_, _, _| Default::default(),
|
||||||
dock_default_item_factory: |_, _| unimplemented!(),
|
dock_default_item_factory: |_, _| None,
|
||||||
|
background_actions: || &[],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -468,6 +542,8 @@ pub struct Workspace {
|
||||||
active_call: Option<(ModelHandle<ActiveCall>, Vec<gpui::Subscription>)>,
|
active_call: Option<(ModelHandle<ActiveCall>, Vec<gpui::Subscription>)>,
|
||||||
leader_updates_tx: mpsc::UnboundedSender<(PeerId, proto::UpdateFollowers)>,
|
leader_updates_tx: mpsc::UnboundedSender<(PeerId, proto::UpdateFollowers)>,
|
||||||
database_id: WorkspaceId,
|
database_id: WorkspaceId,
|
||||||
|
background_actions: BackgroundActions,
|
||||||
|
_window_subscriptions: [Subscription; 3],
|
||||||
_apply_leader_updates: Task<Result<()>>,
|
_apply_leader_updates: Task<Result<()>>,
|
||||||
_observe_current_user: Task<()>,
|
_observe_current_user: Task<()>,
|
||||||
}
|
}
|
||||||
|
@ -497,12 +573,9 @@ impl Workspace {
|
||||||
workspace_id: WorkspaceId,
|
workspace_id: WorkspaceId,
|
||||||
project: ModelHandle<Project>,
|
project: ModelHandle<Project>,
|
||||||
dock_default_factory: DockDefaultItemFactory,
|
dock_default_factory: DockDefaultItemFactory,
|
||||||
|
background_actions: BackgroundActions,
|
||||||
cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
cx.observe_fullscreen(|_, _, cx| cx.notify()).detach();
|
|
||||||
|
|
||||||
cx.observe_window_activation(Self::on_window_activation_changed)
|
|
||||||
.detach();
|
|
||||||
cx.observe(&project, |_, _, cx| cx.notify()).detach();
|
cx.observe(&project, |_, _, cx| cx.notify()).detach();
|
||||||
cx.subscribe(&project, move |this, _, event, cx| {
|
cx.subscribe(&project, move |this, _, event, cx| {
|
||||||
match event {
|
match event {
|
||||||
|
@ -531,7 +604,10 @@ impl Workspace {
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
|
|
||||||
let center_pane = cx.add_view(|cx| Pane::new(None, cx));
|
let weak_handle = cx.weak_handle();
|
||||||
|
|
||||||
|
let center_pane =
|
||||||
|
cx.add_view(|cx| Pane::new(weak_handle.id(), None, background_actions, cx));
|
||||||
let pane_id = center_pane.id();
|
let pane_id = center_pane.id();
|
||||||
cx.subscribe(¢er_pane, move |this, _, event, cx| {
|
cx.subscribe(¢er_pane, move |this, _, event, cx| {
|
||||||
this.handle_pane_event(pane_id, event, cx)
|
this.handle_pane_event(pane_id, event, cx)
|
||||||
|
@ -539,7 +615,12 @@ impl Workspace {
|
||||||
.detach();
|
.detach();
|
||||||
cx.focus(¢er_pane);
|
cx.focus(¢er_pane);
|
||||||
cx.emit(Event::PaneAdded(center_pane.clone()));
|
cx.emit(Event::PaneAdded(center_pane.clone()));
|
||||||
let dock = Dock::new(dock_default_factory, cx);
|
let dock = Dock::new(
|
||||||
|
weak_handle.id(),
|
||||||
|
dock_default_factory,
|
||||||
|
background_actions,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
let dock_pane = dock.pane().clone();
|
let dock_pane = dock.pane().clone();
|
||||||
|
|
||||||
let fs = project.read(cx).fs().clone();
|
let fs = project.read(cx).fs().clone();
|
||||||
|
@ -562,7 +643,6 @@ impl Workspace {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
let handle = cx.handle();
|
let handle = cx.handle();
|
||||||
let weak_handle = cx.weak_handle();
|
|
||||||
|
|
||||||
// All leader updates are enqueued and then processed in a single task, so
|
// All leader updates are enqueued and then processed in a single task, so
|
||||||
// that each asynchronous operation can be run in order.
|
// that each asynchronous operation can be run in order.
|
||||||
|
@ -607,6 +687,28 @@ impl Workspace {
|
||||||
active_call = Some((call, subscriptions));
|
active_call = Some((call, subscriptions));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let subscriptions = [
|
||||||
|
cx.observe_fullscreen(|_, _, cx| cx.notify()),
|
||||||
|
cx.observe_window_activation(Self::on_window_activation_changed),
|
||||||
|
cx.observe_window_bounds(move |_, mut bounds, display, cx| {
|
||||||
|
// Transform fixed bounds to be stored in terms of the containing display
|
||||||
|
if let WindowBounds::Fixed(mut window_bounds) = bounds {
|
||||||
|
if let Some(screen) = cx.platform().screen_by_id(display) {
|
||||||
|
let screen_bounds = screen.bounds();
|
||||||
|
window_bounds
|
||||||
|
.set_origin_x(window_bounds.origin_x() - screen_bounds.origin_x());
|
||||||
|
window_bounds
|
||||||
|
.set_origin_y(window_bounds.origin_y() - screen_bounds.origin_y());
|
||||||
|
bounds = WindowBounds::Fixed(window_bounds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cx.background()
|
||||||
|
.spawn(DB.set_window_bounds(workspace_id, bounds, display))
|
||||||
|
.detach_and_log_err(cx);
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
let mut this = Workspace {
|
let mut this = Workspace {
|
||||||
modal: None,
|
modal: None,
|
||||||
weak_self: weak_handle.clone(),
|
weak_self: weak_handle.clone(),
|
||||||
|
@ -635,9 +737,11 @@ impl Workspace {
|
||||||
window_edited: false,
|
window_edited: false,
|
||||||
active_call,
|
active_call,
|
||||||
database_id: workspace_id,
|
database_id: workspace_id,
|
||||||
|
background_actions,
|
||||||
_observe_current_user,
|
_observe_current_user,
|
||||||
_apply_leader_updates,
|
_apply_leader_updates,
|
||||||
leader_updates_tx,
|
leader_updates_tx,
|
||||||
|
_window_subscriptions: subscriptions,
|
||||||
};
|
};
|
||||||
this.project_remote_id_changed(project.read(cx).remote_id(), cx);
|
this.project_remote_id_changed(project.read(cx).remote_id(), cx);
|
||||||
cx.defer(|this, cx| this.update_window_title(cx));
|
cx.defer(|this, cx| this.update_window_title(cx));
|
||||||
|
@ -646,6 +750,10 @@ impl Workspace {
|
||||||
cx.defer(move |_, cx| {
|
cx.defer(move |_, cx| {
|
||||||
Self::load_from_serialized_workspace(weak_handle, serialized_workspace, cx)
|
Self::load_from_serialized_workspace(weak_handle, serialized_workspace, cx)
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
if cx.global::<Settings>().default_dock_anchor != DockAnchor::Expanded {
|
||||||
|
Dock::show(&mut this, false, cx);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this
|
this
|
||||||
|
@ -654,6 +762,7 @@ impl Workspace {
|
||||||
fn new_local(
|
fn new_local(
|
||||||
abs_paths: Vec<PathBuf>,
|
abs_paths: Vec<PathBuf>,
|
||||||
app_state: Arc<AppState>,
|
app_state: Arc<AppState>,
|
||||||
|
requesting_window_id: Option<usize>,
|
||||||
cx: &mut MutableAppContext,
|
cx: &mut MutableAppContext,
|
||||||
) -> Task<(
|
) -> Task<(
|
||||||
ViewHandle<Workspace>,
|
ViewHandle<Workspace>,
|
||||||
|
@ -709,73 +818,65 @@ impl Workspace {
|
||||||
))
|
))
|
||||||
});
|
});
|
||||||
|
|
||||||
let (bounds, display) = if let Some(bounds) = window_bounds_override {
|
let build_workspace =
|
||||||
(Some(bounds), None)
|
|cx: &mut ViewContext<Workspace>,
|
||||||
} else {
|
serialized_workspace: Option<SerializedWorkspace>| {
|
||||||
serialized_workspace
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|serialized_workspace| {
|
|
||||||
let display = serialized_workspace.display?;
|
|
||||||
let mut bounds = serialized_workspace.bounds?;
|
|
||||||
|
|
||||||
// Stored bounds are relative to the containing display.
|
|
||||||
// So convert back to global coordinates if that screen still exists
|
|
||||||
if let WindowBounds::Fixed(mut window_bounds) = bounds {
|
|
||||||
if let Some(screen) = cx.platform().screen_by_id(display) {
|
|
||||||
let screen_bounds = screen.bounds();
|
|
||||||
window_bounds.set_origin_x(
|
|
||||||
window_bounds.origin_x() + screen_bounds.origin_x(),
|
|
||||||
);
|
|
||||||
window_bounds.set_origin_y(
|
|
||||||
window_bounds.origin_y() + screen_bounds.origin_y(),
|
|
||||||
);
|
|
||||||
bounds = WindowBounds::Fixed(window_bounds);
|
|
||||||
} else {
|
|
||||||
// Screen no longer exists. Return none here.
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Some((bounds, display))
|
|
||||||
})
|
|
||||||
.unzip()
|
|
||||||
};
|
|
||||||
|
|
||||||
// Use the serialized workspace to construct the new window
|
|
||||||
let (_, workspace) = cx.add_window(
|
|
||||||
(app_state.build_window_options)(bounds, display, cx.platform().as_ref()),
|
|
||||||
|cx| {
|
|
||||||
let mut workspace = Workspace::new(
|
let mut workspace = Workspace::new(
|
||||||
serialized_workspace,
|
serialized_workspace,
|
||||||
workspace_id,
|
workspace_id,
|
||||||
project_handle,
|
project_handle,
|
||||||
app_state.dock_default_item_factory,
|
app_state.dock_default_item_factory,
|
||||||
|
app_state.background_actions,
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
(app_state.initialize_workspace)(&mut workspace, &app_state, cx);
|
(app_state.initialize_workspace)(&mut workspace, &app_state, cx);
|
||||||
cx.observe_window_bounds(move |_, mut bounds, display, cx| {
|
|
||||||
// Transform fixed bounds to be stored in terms of the containing display
|
|
||||||
if let WindowBounds::Fixed(mut window_bounds) = bounds {
|
|
||||||
if let Some(screen) = cx.platform().screen_by_id(display) {
|
|
||||||
let screen_bounds = screen.bounds();
|
|
||||||
window_bounds.set_origin_x(
|
|
||||||
window_bounds.origin_x() - screen_bounds.origin_x(),
|
|
||||||
);
|
|
||||||
window_bounds.set_origin_y(
|
|
||||||
window_bounds.origin_y() - screen_bounds.origin_y(),
|
|
||||||
);
|
|
||||||
bounds = WindowBounds::Fixed(window_bounds);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cx.background()
|
|
||||||
.spawn(DB.set_window_bounds(workspace_id, bounds, display))
|
|
||||||
.detach_and_log_err(cx);
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
workspace
|
workspace
|
||||||
},
|
};
|
||||||
);
|
|
||||||
|
let workspace = if let Some(window_id) = requesting_window_id {
|
||||||
|
cx.update(|cx| {
|
||||||
|
cx.replace_root_view(window_id, |cx| build_workspace(cx, serialized_workspace))
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
let (bounds, display) = if let Some(bounds) = window_bounds_override {
|
||||||
|
(Some(bounds), None)
|
||||||
|
} else {
|
||||||
|
serialized_workspace
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|serialized_workspace| {
|
||||||
|
let display = serialized_workspace.display?;
|
||||||
|
let mut bounds = serialized_workspace.bounds?;
|
||||||
|
|
||||||
|
// Stored bounds are relative to the containing display.
|
||||||
|
// So convert back to global coordinates if that screen still exists
|
||||||
|
if let WindowBounds::Fixed(mut window_bounds) = bounds {
|
||||||
|
if let Some(screen) = cx.platform().screen_by_id(display) {
|
||||||
|
let screen_bounds = screen.bounds();
|
||||||
|
window_bounds.set_origin_x(
|
||||||
|
window_bounds.origin_x() + screen_bounds.origin_x(),
|
||||||
|
);
|
||||||
|
window_bounds.set_origin_y(
|
||||||
|
window_bounds.origin_y() + screen_bounds.origin_y(),
|
||||||
|
);
|
||||||
|
bounds = WindowBounds::Fixed(window_bounds);
|
||||||
|
} else {
|
||||||
|
// Screen no longer exists. Return none here.
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Some((bounds, display))
|
||||||
|
})
|
||||||
|
.unzip()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Use the serialized workspace to construct the new window
|
||||||
|
cx.add_window(
|
||||||
|
(app_state.build_window_options)(bounds, display, cx.platform().as_ref()),
|
||||||
|
|cx| build_workspace(cx, serialized_workspace),
|
||||||
|
)
|
||||||
|
.1
|
||||||
|
};
|
||||||
|
|
||||||
notify_if_database_failed(&workspace, &mut cx);
|
notify_if_database_failed(&workspace, &mut cx);
|
||||||
|
|
||||||
|
@ -871,7 +972,7 @@ impl Workspace {
|
||||||
if self.project.read(cx).is_local() {
|
if self.project.read(cx).is_local() {
|
||||||
Task::Ready(Some(callback(self, cx)))
|
Task::Ready(Some(callback(self, cx)))
|
||||||
} else {
|
} else {
|
||||||
let task = Self::new_local(Vec::new(), app_state.clone(), cx);
|
let task = Self::new_local(Vec::new(), app_state.clone(), None, cx);
|
||||||
cx.spawn(|_vh, mut cx| async move {
|
cx.spawn(|_vh, mut cx| async move {
|
||||||
let (workspace, _) = task.await;
|
let (workspace, _) = task.await;
|
||||||
workspace.update(&mut cx, callback)
|
workspace.update(&mut cx, callback)
|
||||||
|
@ -1340,7 +1441,8 @@ impl Workspace {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn add_pane(&mut self, cx: &mut ViewContext<Self>) -> ViewHandle<Pane> {
|
fn add_pane(&mut self, cx: &mut ViewContext<Self>) -> ViewHandle<Pane> {
|
||||||
let pane = cx.add_view(|cx| Pane::new(None, cx));
|
let pane =
|
||||||
|
cx.add_view(|cx| Pane::new(self.weak_handle().id(), None, self.background_actions, cx));
|
||||||
let pane_id = pane.id();
|
let pane_id = pane.id();
|
||||||
cx.subscribe(&pane, move |this, _, event, cx| {
|
cx.subscribe(&pane, move |this, _, event, cx| {
|
||||||
this.handle_pane_event(pane_id, event, cx)
|
this.handle_pane_event(pane_id, event, cx)
|
||||||
|
@ -1352,6 +1454,23 @@ impl Workspace {
|
||||||
pane
|
pane
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn add_item_to_center(
|
||||||
|
&mut self,
|
||||||
|
item: Box<dyn ItemHandle>,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) -> bool {
|
||||||
|
if let Some(center_pane) = self.last_active_center_pane.clone() {
|
||||||
|
if let Some(center_pane) = center_pane.upgrade(cx) {
|
||||||
|
Pane::add_item(self, ¢er_pane, item, true, true, None, cx);
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn add_item(&mut self, item: Box<dyn ItemHandle>, cx: &mut ViewContext<Self>) {
|
pub fn add_item(&mut self, item: Box<dyn ItemHandle>, cx: &mut ViewContext<Self>) {
|
||||||
let active_pane = self.active_pane().clone();
|
let active_pane = self.active_pane().clone();
|
||||||
Pane::add_item(self, &active_pane, item, true, true, None, cx);
|
Pane::add_item(self, &active_pane, item, true, true, None, cx);
|
||||||
|
@ -1509,7 +1628,7 @@ impl Workspace {
|
||||||
self.active_item_path_changed(cx);
|
self.active_item_path_changed(cx);
|
||||||
|
|
||||||
if &pane == self.dock_pane() {
|
if &pane == self.dock_pane() {
|
||||||
Dock::show(self, cx);
|
Dock::show(self, true, cx);
|
||||||
} else {
|
} else {
|
||||||
self.last_active_center_pane = Some(pane.downgrade());
|
self.last_active_center_pane = Some(pane.downgrade());
|
||||||
if self.dock.is_anchored_at(DockAnchor::Expanded) {
|
if self.dock.is_anchored_at(DockAnchor::Expanded) {
|
||||||
|
@ -2522,7 +2641,12 @@ impl Workspace {
|
||||||
// the focus the dock generates start generating alternating
|
// the focus the dock generates start generating alternating
|
||||||
// focus due to the deferred execution each triggering each other
|
// focus due to the deferred execution each triggering each other
|
||||||
cx.after_window_update(move |workspace, cx| {
|
cx.after_window_update(move |workspace, cx| {
|
||||||
Dock::set_dock_position(workspace, serialized_workspace.dock_position, cx);
|
Dock::set_dock_position(
|
||||||
|
workspace,
|
||||||
|
serialized_workspace.dock_position,
|
||||||
|
true,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
cx.notify();
|
cx.notify();
|
||||||
|
@ -2534,6 +2658,11 @@ impl Workspace {
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
pub fn test_new(project: ModelHandle<Project>, cx: &mut ViewContext<Self>) -> Self {
|
||||||
|
Self::new(None, 0, project, |_, _| None, || &[], cx)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn notify_if_database_failed(workspace: &ViewHandle<Workspace>, cx: &mut AsyncAppContext) {
|
fn notify_if_database_failed(workspace: &ViewHandle<Workspace>, cx: &mut AsyncAppContext) {
|
||||||
|
@ -2765,20 +2894,6 @@ impl std::fmt::Debug for OpenPaths {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn open(_: &Open, cx: &mut MutableAppContext) {
|
|
||||||
let mut paths = cx.prompt_for_paths(PathPromptOptions {
|
|
||||||
files: true,
|
|
||||||
directories: true,
|
|
||||||
multiple: true,
|
|
||||||
});
|
|
||||||
cx.spawn(|mut cx| async move {
|
|
||||||
if let Some(paths) = paths.recv().await.flatten() {
|
|
||||||
cx.update(|cx| cx.dispatch_global_action(OpenPaths { paths }));
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct WorkspaceCreated(WeakViewHandle<Workspace>);
|
pub struct WorkspaceCreated(WeakViewHandle<Workspace>);
|
||||||
|
|
||||||
pub fn activate_workspace_for_project(
|
pub fn activate_workspace_for_project(
|
||||||
|
@ -2805,6 +2920,7 @@ pub async fn last_opened_workspace_paths() -> Option<WorkspaceLocation> {
|
||||||
pub fn open_paths(
|
pub fn open_paths(
|
||||||
abs_paths: &[PathBuf],
|
abs_paths: &[PathBuf],
|
||||||
app_state: &Arc<AppState>,
|
app_state: &Arc<AppState>,
|
||||||
|
requesting_window_id: Option<usize>,
|
||||||
cx: &mut MutableAppContext,
|
cx: &mut MutableAppContext,
|
||||||
) -> Task<(
|
) -> Task<(
|
||||||
ViewHandle<Workspace>,
|
ViewHandle<Workspace>,
|
||||||
|
@ -2835,7 +2951,8 @@ pub fn open_paths(
|
||||||
.contains(&false);
|
.contains(&false);
|
||||||
|
|
||||||
cx.update(|cx| {
|
cx.update(|cx| {
|
||||||
let task = Workspace::new_local(abs_paths, app_state.clone(), cx);
|
let task =
|
||||||
|
Workspace::new_local(abs_paths, app_state.clone(), requesting_window_id, cx);
|
||||||
|
|
||||||
cx.spawn(|mut cx| async move {
|
cx.spawn(|mut cx| async move {
|
||||||
let (workspace, items) = task.await;
|
let (workspace, items) = task.await;
|
||||||
|
@ -2854,14 +2971,18 @@ pub fn open_paths(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn open_new(app_state: &Arc<AppState>, cx: &mut MutableAppContext) -> Task<()> {
|
pub fn open_new(
|
||||||
let task = Workspace::new_local(Vec::new(), app_state.clone(), cx);
|
app_state: &Arc<AppState>,
|
||||||
|
cx: &mut MutableAppContext,
|
||||||
|
init: impl FnOnce(&mut Workspace, &mut ViewContext<Workspace>) + 'static,
|
||||||
|
) -> Task<()> {
|
||||||
|
let task = Workspace::new_local(Vec::new(), app_state.clone(), None, cx);
|
||||||
cx.spawn(|mut cx| async move {
|
cx.spawn(|mut cx| async move {
|
||||||
let (workspace, opened_paths) = task.await;
|
let (workspace, opened_paths) = task.await;
|
||||||
|
|
||||||
workspace.update(&mut cx, |_, cx| {
|
workspace.update(&mut cx, |workspace, cx| {
|
||||||
if opened_paths.is_empty() {
|
if opened_paths.is_empty() {
|
||||||
cx.dispatch_action(NewFile);
|
init(workspace, cx)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -2882,17 +3003,10 @@ mod tests {
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use fs::FakeFs;
|
use fs::FakeFs;
|
||||||
use gpui::{executor::Deterministic, TestAppContext, ViewContext};
|
use gpui::{executor::Deterministic, TestAppContext};
|
||||||
use project::{Project, ProjectEntryId};
|
use project::{Project, ProjectEntryId};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
pub fn default_item_factory(
|
|
||||||
_workspace: &mut Workspace,
|
|
||||||
_cx: &mut ViewContext<Workspace>,
|
|
||||||
) -> Option<Box<dyn ItemHandle>> {
|
|
||||||
unimplemented!()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_tab_disambiguation(cx: &mut TestAppContext) {
|
async fn test_tab_disambiguation(cx: &mut TestAppContext) {
|
||||||
cx.foreground().forbid_parking();
|
cx.foreground().forbid_parking();
|
||||||
|
@ -2905,7 +3019,8 @@ mod tests {
|
||||||
Default::default(),
|
Default::default(),
|
||||||
0,
|
0,
|
||||||
project.clone(),
|
project.clone(),
|
||||||
default_item_factory,
|
|_, _| None,
|
||||||
|
|| &[],
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
@ -2977,7 +3092,8 @@ mod tests {
|
||||||
Default::default(),
|
Default::default(),
|
||||||
0,
|
0,
|
||||||
project.clone(),
|
project.clone(),
|
||||||
default_item_factory,
|
|_, _| None,
|
||||||
|
|| &[],
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
@ -3077,7 +3193,8 @@ mod tests {
|
||||||
Default::default(),
|
Default::default(),
|
||||||
0,
|
0,
|
||||||
project.clone(),
|
project.clone(),
|
||||||
default_item_factory,
|
|_, _| None,
|
||||||
|
|| &[],
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
@ -3116,7 +3233,7 @@ mod tests {
|
||||||
|
|
||||||
let project = Project::test(fs, None, cx).await;
|
let project = Project::test(fs, None, cx).await;
|
||||||
let (window_id, workspace) = cx.add_window(|cx| {
|
let (window_id, workspace) = cx.add_window(|cx| {
|
||||||
Workspace::new(Default::default(), 0, project, default_item_factory, cx)
|
Workspace::new(Default::default(), 0, project, |_, _| None, || &[], cx)
|
||||||
});
|
});
|
||||||
|
|
||||||
let item1 = cx.add_view(&workspace, |cx| {
|
let item1 = cx.add_view(&workspace, |cx| {
|
||||||
|
@ -3225,7 +3342,7 @@ mod tests {
|
||||||
|
|
||||||
let project = Project::test(fs, [], cx).await;
|
let project = Project::test(fs, [], cx).await;
|
||||||
let (window_id, workspace) = cx.add_window(|cx| {
|
let (window_id, workspace) = cx.add_window(|cx| {
|
||||||
Workspace::new(Default::default(), 0, project, default_item_factory, cx)
|
Workspace::new(Default::default(), 0, project, |_, _| None, || &[], cx)
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create several workspace items with single project entries, and two
|
// Create several workspace items with single project entries, and two
|
||||||
|
@ -3334,7 +3451,7 @@ mod tests {
|
||||||
|
|
||||||
let project = Project::test(fs, [], cx).await;
|
let project = Project::test(fs, [], cx).await;
|
||||||
let (window_id, workspace) = cx.add_window(|cx| {
|
let (window_id, workspace) = cx.add_window(|cx| {
|
||||||
Workspace::new(Default::default(), 0, project, default_item_factory, cx)
|
Workspace::new(Default::default(), 0, project, |_, _| None, || &[], cx)
|
||||||
});
|
});
|
||||||
|
|
||||||
let item = cx.add_view(&workspace, |cx| {
|
let item = cx.add_view(&workspace, |cx| {
|
||||||
|
@ -3453,7 +3570,7 @@ mod tests {
|
||||||
|
|
||||||
let project = Project::test(fs, [], cx).await;
|
let project = Project::test(fs, [], cx).await;
|
||||||
let (_, workspace) = cx.add_window(|cx| {
|
let (_, workspace) = cx.add_window(|cx| {
|
||||||
Workspace::new(Default::default(), 0, project, default_item_factory, cx)
|
Workspace::new(Default::default(), 0, project, |_, _| None, || &[], cx)
|
||||||
});
|
});
|
||||||
|
|
||||||
let item = cx.add_view(&workspace, |cx| {
|
let item = cx.add_view(&workspace, |cx| {
|
||||||
|
|
|
@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathansobo@gmail.com>"]
|
||||||
description = "The fast, collaborative code editor."
|
description = "The fast, collaborative code editor."
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
name = "zed"
|
name = "zed"
|
||||||
version = "0.76.0"
|
version = "0.77.0"
|
||||||
publish = false
|
publish = false
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
|
@ -29,6 +29,7 @@ context_menu = { path = "../context_menu" }
|
||||||
client = { path = "../client" }
|
client = { path = "../client" }
|
||||||
clock = { path = "../clock" }
|
clock = { path = "../clock" }
|
||||||
diagnostics = { path = "../diagnostics" }
|
diagnostics = { path = "../diagnostics" }
|
||||||
|
db = { path = "../db" }
|
||||||
editor = { path = "../editor" }
|
editor = { path = "../editor" }
|
||||||
feedback = { path = "../feedback" }
|
feedback = { path = "../feedback" }
|
||||||
file_finder = { path = "../file_finder" }
|
file_finder = { path = "../file_finder" }
|
||||||
|
@ -38,8 +39,10 @@ fsevent = { path = "../fsevent" }
|
||||||
fuzzy = { path = "../fuzzy" }
|
fuzzy = { path = "../fuzzy" }
|
||||||
go_to_line = { path = "../go_to_line" }
|
go_to_line = { path = "../go_to_line" }
|
||||||
gpui = { path = "../gpui" }
|
gpui = { path = "../gpui" }
|
||||||
|
install_cli = { path = "../install_cli" }
|
||||||
journal = { path = "../journal" }
|
journal = { path = "../journal" }
|
||||||
language = { path = "../language" }
|
language = { path = "../language" }
|
||||||
|
language_selector = { path = "../language_selector" }
|
||||||
lsp = { path = "../lsp" }
|
lsp = { path = "../lsp" }
|
||||||
outline = { path = "../outline" }
|
outline = { path = "../outline" }
|
||||||
plugin_runtime = { path = "../plugin_runtime" }
|
plugin_runtime = { path = "../plugin_runtime" }
|
||||||
|
@ -58,6 +61,7 @@ theme_testbench = { path = "../theme_testbench" }
|
||||||
util = { path = "../util" }
|
util = { path = "../util" }
|
||||||
vim = { path = "../vim" }
|
vim = { path = "../vim" }
|
||||||
workspace = { path = "../workspace" }
|
workspace = { path = "../workspace" }
|
||||||
|
welcome = { path = "../welcome" }
|
||||||
anyhow = "1.0.38"
|
anyhow = "1.0.38"
|
||||||
async-compression = { version = "0.3", features = ["gzip", "futures-bufread"] }
|
async-compression = { version = "0.3", features = ["gzip", "futures-bufread"] }
|
||||||
async-tar = "0.4.2"
|
async-tar = "0.4.2"
|
||||||
|
|
|
@ -13,11 +13,12 @@ use client::{
|
||||||
http::{self, HttpClient},
|
http::{self, HttpClient},
|
||||||
UserStore, ZED_APP_VERSION, ZED_SECRET_CLIENT_TOKEN,
|
UserStore, ZED_APP_VERSION, ZED_SECRET_CLIENT_TOKEN,
|
||||||
};
|
};
|
||||||
|
use db::kvp::KEY_VALUE_STORE;
|
||||||
use futures::{
|
use futures::{
|
||||||
channel::{mpsc, oneshot},
|
channel::{mpsc, oneshot},
|
||||||
FutureExt, SinkExt, StreamExt,
|
FutureExt, SinkExt, StreamExt,
|
||||||
};
|
};
|
||||||
use gpui::{App, AssetSource, AsyncAppContext, MutableAppContext, Task, ViewContext};
|
use gpui::{Action, App, AssetSource, AsyncAppContext, MutableAppContext, Task, ViewContext};
|
||||||
use isahc::{config::Configurable, Request};
|
use isahc::{config::Configurable, Request};
|
||||||
use language::LanguageRegistry;
|
use language::LanguageRegistry;
|
||||||
use log::LevelFilter;
|
use log::LevelFilter;
|
||||||
|
@ -35,17 +36,19 @@ use std::{
|
||||||
path::PathBuf, sync::Arc, thread, time::Duration,
|
path::PathBuf, sync::Arc, thread, time::Duration,
|
||||||
};
|
};
|
||||||
use terminal_view::{get_working_directory, TerminalView};
|
use terminal_view::{get_working_directory, TerminalView};
|
||||||
|
use welcome::{show_welcome_experience, FIRST_OPEN};
|
||||||
|
|
||||||
use fs::RealFs;
|
use fs::RealFs;
|
||||||
use settings::watched_json::{watch_keymap_file, watch_settings_file, WatchedJsonFile};
|
use settings::watched_json::WatchedJsonFile;
|
||||||
use theme::ThemeRegistry;
|
use theme::ThemeRegistry;
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
use util::StaffMode;
|
use util::StaffMode;
|
||||||
use util::{channel::RELEASE_CHANNEL, paths, ResultExt, TryFutureExt};
|
use util::{channel::RELEASE_CHANNEL, paths, ResultExt, TryFutureExt};
|
||||||
use workspace::{
|
use workspace::{
|
||||||
self, item::ItemHandle, notifications::NotifyResultExt, AppState, NewFile, OpenPaths, Workspace,
|
self, dock::FocusDock, item::ItemHandle, notifications::NotifyResultExt, AppState, NewFile,
|
||||||
|
OpenPaths, Workspace,
|
||||||
};
|
};
|
||||||
use zed::{self, build_window_options, initialize_workspace, languages, menus};
|
use zed::{self, build_window_options, initialize_workspace, languages, menus, OpenSettings};
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let http = http::client();
|
let http = http::client();
|
||||||
|
@ -119,7 +122,14 @@ fn main() {
|
||||||
fs.clone(),
|
fs.clone(),
|
||||||
));
|
));
|
||||||
|
|
||||||
watch_settings_file(default_settings, settings_file_content, themes.clone(), cx);
|
settings::watch_files(
|
||||||
|
default_settings,
|
||||||
|
settings_file_content,
|
||||||
|
themes.clone(),
|
||||||
|
keymap_file,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
|
||||||
if !stdout_is_a_pty() {
|
if !stdout_is_a_pty() {
|
||||||
upload_previous_panics(http.clone(), cx);
|
upload_previous_panics(http.clone(), cx);
|
||||||
}
|
}
|
||||||
|
@ -132,8 +142,6 @@ fn main() {
|
||||||
languages::init(languages.clone());
|
languages::init(languages.clone());
|
||||||
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http.clone(), cx));
|
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http.clone(), cx));
|
||||||
|
|
||||||
watch_keymap_file(keymap_file, cx);
|
|
||||||
|
|
||||||
cx.set_global(client.clone());
|
cx.set_global(client.clone());
|
||||||
|
|
||||||
context_menu::init(cx);
|
context_menu::init(cx);
|
||||||
|
@ -179,16 +187,19 @@ fn main() {
|
||||||
build_window_options,
|
build_window_options,
|
||||||
initialize_workspace,
|
initialize_workspace,
|
||||||
dock_default_item_factory,
|
dock_default_item_factory,
|
||||||
|
background_actions,
|
||||||
});
|
});
|
||||||
auto_update::init(http, client::ZED_SERVER_URL.clone(), cx);
|
auto_update::init(http, client::ZED_SERVER_URL.clone(), cx);
|
||||||
|
|
||||||
workspace::init(app_state.clone(), cx);
|
workspace::init(app_state.clone(), cx);
|
||||||
|
|
||||||
journal::init(app_state.clone(), cx);
|
journal::init(app_state.clone(), cx);
|
||||||
|
language_selector::init(app_state.clone(), cx);
|
||||||
theme_selector::init(app_state.clone(), cx);
|
theme_selector::init(app_state.clone(), cx);
|
||||||
zed::init(&app_state, cx);
|
zed::init(&app_state, cx);
|
||||||
collab_ui::init(app_state.clone(), cx);
|
collab_ui::init(app_state.clone(), cx);
|
||||||
feedback::init(app_state.clone(), cx);
|
feedback::init(app_state.clone(), cx);
|
||||||
|
welcome::init(cx);
|
||||||
|
|
||||||
cx.set_menus(menus::menus());
|
cx.set_menus(menus::menus());
|
||||||
|
|
||||||
|
@ -196,7 +207,7 @@ fn main() {
|
||||||
cx.platform().activate(true);
|
cx.platform().activate(true);
|
||||||
let paths = collect_path_args();
|
let paths = collect_path_args();
|
||||||
if paths.is_empty() {
|
if paths.is_empty() {
|
||||||
cx.spawn(|cx| async move { restore_or_create_workspace(cx).await })
|
cx.spawn(|cx| async move { restore_or_create_workspace(&app_state, cx).await })
|
||||||
.detach()
|
.detach()
|
||||||
} else {
|
} else {
|
||||||
cx.dispatch_global_action(OpenPaths { paths });
|
cx.dispatch_global_action(OpenPaths { paths });
|
||||||
|
@ -206,11 +217,14 @@ fn main() {
|
||||||
cx.spawn(|cx| handle_cli_connection(connection, app_state.clone(), cx))
|
cx.spawn(|cx| handle_cli_connection(connection, app_state.clone(), cx))
|
||||||
.detach();
|
.detach();
|
||||||
} else if let Ok(Some(paths)) = open_paths_rx.try_next() {
|
} else if let Ok(Some(paths)) = open_paths_rx.try_next() {
|
||||||
cx.update(|cx| workspace::open_paths(&paths, &app_state, cx))
|
cx.update(|cx| workspace::open_paths(&paths, &app_state, None, cx))
|
||||||
.detach();
|
.detach();
|
||||||
} else {
|
} else {
|
||||||
cx.spawn(|cx| async move { restore_or_create_workspace(cx).await })
|
cx.spawn({
|
||||||
.detach()
|
let app_state = app_state.clone();
|
||||||
|
|cx| async move { restore_or_create_workspace(&app_state, cx).await }
|
||||||
|
})
|
||||||
|
.detach()
|
||||||
}
|
}
|
||||||
|
|
||||||
cx.spawn(|cx| {
|
cx.spawn(|cx| {
|
||||||
|
@ -227,8 +241,7 @@ fn main() {
|
||||||
let app_state = app_state.clone();
|
let app_state = app_state.clone();
|
||||||
async move {
|
async move {
|
||||||
while let Some(paths) = open_paths_rx.next().await {
|
while let Some(paths) = open_paths_rx.next().await {
|
||||||
log::error!("OPEN PATHS FROM HANDLE");
|
cx.update(|cx| workspace::open_paths(&paths, &app_state, None, cx))
|
||||||
cx.update(|cx| workspace::open_paths(&paths, &app_state, cx))
|
|
||||||
.detach();
|
.detach();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -250,13 +263,15 @@ fn main() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn restore_or_create_workspace(mut cx: AsyncAppContext) {
|
async fn restore_or_create_workspace(app_state: &Arc<AppState>, mut cx: AsyncAppContext) {
|
||||||
if let Some(location) = workspace::last_opened_workspace_paths().await {
|
if let Some(location) = workspace::last_opened_workspace_paths().await {
|
||||||
cx.update(|cx| {
|
cx.update(|cx| {
|
||||||
cx.dispatch_global_action(OpenPaths {
|
cx.dispatch_global_action(OpenPaths {
|
||||||
paths: location.paths().as_ref().clone(),
|
paths: location.paths().as_ref().clone(),
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
} else if matches!(KEY_VALUE_STORE.read_kvp(FIRST_OPEN), Ok(None)) {
|
||||||
|
cx.update(|cx| show_welcome_experience(app_state, cx));
|
||||||
} else {
|
} else {
|
||||||
cx.update(|cx| {
|
cx.update(|cx| {
|
||||||
cx.dispatch_global_action(NewFile);
|
cx.dispatch_global_action(NewFile);
|
||||||
|
@ -590,7 +605,7 @@ async fn handle_cli_connection(
|
||||||
paths
|
paths
|
||||||
};
|
};
|
||||||
let (workspace, items) = cx
|
let (workspace, items) = cx
|
||||||
.update(|cx| workspace::open_paths(&paths, &app_state, cx))
|
.update(|cx| workspace::open_paths(&paths, &app_state, None, cx))
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let mut errored = false;
|
let mut errored = false;
|
||||||
|
@ -691,3 +706,13 @@ pub fn dock_default_item_factory(
|
||||||
|
|
||||||
Some(Box::new(terminal_view))
|
Some(Box::new(terminal_view))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn background_actions() -> &'static [(&'static str, &'static dyn Action)] {
|
||||||
|
&[
|
||||||
|
("Go to file", &file_finder::Toggle),
|
||||||
|
("Open command palette", &command_palette::Toggle),
|
||||||
|
("Focus the dock", &FocusDock),
|
||||||
|
("Open recent projects", &recent_projects::OpenRecent),
|
||||||
|
("Change your settings", &OpenSettings),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
|
@ -19,7 +19,7 @@ pub fn menus() -> Vec<Menu<'static>> {
|
||||||
MenuItem::action("Select Theme", theme_selector::Toggle),
|
MenuItem::action("Select Theme", theme_selector::Toggle),
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
MenuItem::action("Install CLI", super::InstallCommandLineInterface),
|
MenuItem::action("Install CLI", install_cli::Install),
|
||||||
MenuItem::separator(),
|
MenuItem::separator(),
|
||||||
MenuItem::action("Hide Zed", super::Hide),
|
MenuItem::action("Hide Zed", super::Hide),
|
||||||
MenuItem::action("Hide Others", super::HideOthers),
|
MenuItem::action("Hide Others", super::HideOthers),
|
||||||
|
@ -137,9 +137,11 @@ pub fn menus() -> Vec<Menu<'static>> {
|
||||||
items: vec![
|
items: vec![
|
||||||
MenuItem::action("Command Palette", command_palette::Toggle),
|
MenuItem::action("Command Palette", command_palette::Toggle),
|
||||||
MenuItem::separator(),
|
MenuItem::separator(),
|
||||||
MenuItem::action("View Telemetry Log", crate::OpenTelemetryLog),
|
MenuItem::action("View Telemetry", crate::OpenTelemetryLog),
|
||||||
MenuItem::action("View Dependency Licenses", crate::OpenLicenses),
|
MenuItem::action("View Dependency Licenses", crate::OpenLicenses),
|
||||||
|
MenuItem::action("Show Welcome", workspace::Welcome),
|
||||||
MenuItem::separator(),
|
MenuItem::separator(),
|
||||||
|
MenuItem::action("Give us feedback", feedback::feedback_editor::GiveFeedback),
|
||||||
MenuItem::action(
|
MenuItem::action(
|
||||||
"Copy System Specs Into Clipboard",
|
"Copy System Specs Into Clipboard",
|
||||||
feedback::CopySystemSpecsIntoClipboard,
|
feedback::CopySystemSpecsIntoClipboard,
|
||||||
|
|
|
@ -2,7 +2,7 @@ pub mod languages;
|
||||||
pub mod menus;
|
pub mod menus;
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
pub mod test;
|
pub mod test;
|
||||||
use anyhow::{anyhow, Context, Result};
|
use anyhow::Context;
|
||||||
use assets::Assets;
|
use assets::Assets;
|
||||||
use breadcrumbs::Breadcrumbs;
|
use breadcrumbs::Breadcrumbs;
|
||||||
pub use client;
|
pub use client;
|
||||||
|
@ -20,7 +20,7 @@ use gpui::{
|
||||||
geometry::vector::vec2f,
|
geometry::vector::vec2f,
|
||||||
impl_actions,
|
impl_actions,
|
||||||
platform::{WindowBounds, WindowOptions},
|
platform::{WindowBounds, WindowOptions},
|
||||||
AssetSource, AsyncAppContext, Platform, PromptLevel, TitlebarOptions, ViewContext, WindowKind,
|
AssetSource, Platform, PromptLevel, TitlebarOptions, ViewContext, WindowKind,
|
||||||
};
|
};
|
||||||
use language::Rope;
|
use language::Rope;
|
||||||
pub use lsp;
|
pub use lsp;
|
||||||
|
@ -66,7 +66,6 @@ actions!(
|
||||||
IncreaseBufferFontSize,
|
IncreaseBufferFontSize,
|
||||||
DecreaseBufferFontSize,
|
DecreaseBufferFontSize,
|
||||||
ResetBufferFontSize,
|
ResetBufferFontSize,
|
||||||
InstallCommandLineInterface,
|
|
||||||
ResetDatabase,
|
ResetDatabase,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
@ -142,9 +141,13 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::MutableAppContext) {
|
||||||
cx.refresh_windows();
|
cx.refresh_windows();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
cx.add_global_action(move |_: &InstallCommandLineInterface, cx| {
|
cx.add_global_action(move |_: &install_cli::Install, cx| {
|
||||||
cx.spawn(|cx| async move { install_cli(&cx).await.context("error creating CLI symlink") })
|
cx.spawn(|cx| async move {
|
||||||
.detach_and_log_err(cx);
|
install_cli::install_cli(&cx)
|
||||||
|
.await
|
||||||
|
.context("error creating CLI symlink")
|
||||||
|
})
|
||||||
|
.detach_and_log_err(cx);
|
||||||
});
|
});
|
||||||
cx.add_action({
|
cx.add_action({
|
||||||
let app_state = app_state.clone();
|
let app_state = app_state.clone();
|
||||||
|
@ -169,9 +172,8 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::MutableAppContext) {
|
||||||
});
|
});
|
||||||
cx.add_action({
|
cx.add_action({
|
||||||
let app_state = app_state.clone();
|
let app_state = app_state.clone();
|
||||||
move |workspace: &mut Workspace, _: &OpenLicenses, cx: &mut ViewContext<Workspace>| {
|
move |_: &mut Workspace, _: &OpenLicenses, cx: &mut ViewContext<Workspace>| {
|
||||||
open_bundled_file(
|
open_bundled_file(
|
||||||
workspace,
|
|
||||||
app_state.clone(),
|
app_state.clone(),
|
||||||
"licenses.md",
|
"licenses.md",
|
||||||
"Open Source License Attribution",
|
"Open Source License Attribution",
|
||||||
|
@ -194,9 +196,8 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::MutableAppContext) {
|
||||||
});
|
});
|
||||||
cx.add_action({
|
cx.add_action({
|
||||||
let app_state = app_state.clone();
|
let app_state = app_state.clone();
|
||||||
move |workspace: &mut Workspace, _: &OpenDefaultKeymap, cx: &mut ViewContext<Workspace>| {
|
move |_: &mut Workspace, _: &OpenDefaultKeymap, cx: &mut ViewContext<Workspace>| {
|
||||||
open_bundled_file(
|
open_bundled_file(
|
||||||
workspace,
|
|
||||||
app_state.clone(),
|
app_state.clone(),
|
||||||
"keymaps/default.json",
|
"keymaps/default.json",
|
||||||
"Default Key Bindings",
|
"Default Key Bindings",
|
||||||
|
@ -207,11 +208,8 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::MutableAppContext) {
|
||||||
});
|
});
|
||||||
cx.add_action({
|
cx.add_action({
|
||||||
let app_state = app_state.clone();
|
let app_state = app_state.clone();
|
||||||
move |workspace: &mut Workspace,
|
move |_: &mut Workspace, _: &OpenDefaultSettings, cx: &mut ViewContext<Workspace>| {
|
||||||
_: &OpenDefaultSettings,
|
|
||||||
cx: &mut ViewContext<Workspace>| {
|
|
||||||
open_bundled_file(
|
open_bundled_file(
|
||||||
workspace,
|
|
||||||
app_state.clone(),
|
app_state.clone(),
|
||||||
"settings/default.json",
|
"settings/default.json",
|
||||||
"Default Settings",
|
"Default Settings",
|
||||||
|
@ -220,32 +218,41 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::MutableAppContext) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
cx.add_action(
|
cx.add_action({
|
||||||
|workspace: &mut Workspace, _: &DebugElements, cx: &mut ViewContext<Workspace>| {
|
let app_state = app_state.clone();
|
||||||
|
move |_: &mut Workspace, _: &DebugElements, cx: &mut ViewContext<Workspace>| {
|
||||||
|
let app_state = app_state.clone();
|
||||||
|
let markdown = app_state.languages.language_for_name("JSON");
|
||||||
let content = to_string_pretty(&cx.debug_elements()).unwrap();
|
let content = to_string_pretty(&cx.debug_elements()).unwrap();
|
||||||
let project = workspace.project().clone();
|
cx.spawn(|workspace, mut cx| async move {
|
||||||
let json_language = project
|
let markdown = markdown.await.log_err();
|
||||||
.read(cx)
|
workspace
|
||||||
.languages()
|
.update(&mut cx, |workspace, cx| {
|
||||||
.language_for_name("JSON")
|
workspace.with_local_workspace(&app_state, cx, move |workspace, cx| {
|
||||||
.unwrap();
|
let project = workspace.project().clone();
|
||||||
if project.read(cx).is_remote() {
|
|
||||||
cx.propagate_action();
|
let buffer = project
|
||||||
} else if let Some(buffer) = project
|
.update(cx, |project, cx| {
|
||||||
.update(cx, |project, cx| {
|
project.create_buffer(&content, markdown, cx)
|
||||||
project.create_buffer(&content, Some(json_language), cx)
|
})
|
||||||
})
|
.expect("creating buffers on a local workspace always succeeds");
|
||||||
.log_err()
|
let buffer = cx.add_model(|cx| {
|
||||||
{
|
MultiBuffer::singleton(buffer, cx)
|
||||||
workspace.add_item(
|
.with_title("Debug Elements".into())
|
||||||
Box::new(
|
});
|
||||||
cx.add_view(|cx| Editor::for_buffer(buffer, Some(project.clone()), cx)),
|
workspace.add_item(
|
||||||
),
|
Box::new(cx.add_view(|cx| {
|
||||||
cx,
|
Editor::for_multibuffer(buffer, Some(project.clone()), cx)
|
||||||
);
|
})),
|
||||||
}
|
cx,
|
||||||
},
|
);
|
||||||
);
|
})
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
});
|
||||||
cx.add_action(
|
cx.add_action(
|
||||||
|workspace: &mut Workspace,
|
|workspace: &mut Workspace,
|
||||||
_: &project_panel::ToggleFocus,
|
_: &project_panel::ToggleFocus,
|
||||||
|
@ -253,7 +260,6 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::MutableAppContext) {
|
||||||
workspace.toggle_sidebar_item_focus(SidebarSide::Left, 0, cx);
|
workspace.toggle_sidebar_item_focus(SidebarSide::Left, 0, cx);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
activity_indicator::init(cx);
|
activity_indicator::init(cx);
|
||||||
call::init(app_state.client.clone(), app_state.user_store.clone(), cx);
|
call::init(app_state.client.clone(), app_state.user_store.clone(), cx);
|
||||||
settings::KeymapFileContent::load_defaults(cx);
|
settings::KeymapFileContent::load_defaults(cx);
|
||||||
|
@ -337,18 +343,19 @@ pub fn initialize_workspace(
|
||||||
cx.add_view(|cx| diagnostics::items::DiagnosticIndicator::new(workspace.project(), cx));
|
cx.add_view(|cx| diagnostics::items::DiagnosticIndicator::new(workspace.project(), cx));
|
||||||
let activity_indicator =
|
let activity_indicator =
|
||||||
activity_indicator::ActivityIndicator::new(workspace, app_state.languages.clone(), cx);
|
activity_indicator::ActivityIndicator::new(workspace, app_state.languages.clone(), cx);
|
||||||
let cursor_position = cx.add_view(|_| editor::items::CursorPosition::new());
|
let active_buffer_language = cx.add_view(|_| language_selector::ActiveBufferLanguage::new());
|
||||||
let feedback_button =
|
let feedback_button =
|
||||||
cx.add_view(|_| feedback::deploy_feedback_button::DeployFeedbackButton {});
|
cx.add_view(|_| feedback::deploy_feedback_button::DeployFeedbackButton::new());
|
||||||
|
let cursor_position = cx.add_view(|_| editor::items::CursorPosition::new());
|
||||||
workspace.status_bar().update(cx, |status_bar, cx| {
|
workspace.status_bar().update(cx, |status_bar, cx| {
|
||||||
status_bar.add_left_item(diagnostic_summary, cx);
|
status_bar.add_left_item(diagnostic_summary, cx);
|
||||||
status_bar.add_left_item(activity_indicator, cx);
|
status_bar.add_left_item(activity_indicator, cx);
|
||||||
// TODO: Remove this when things are done
|
|
||||||
if **cx.default_global::<StaffMode>() {
|
if **cx.default_global::<StaffMode>() {
|
||||||
status_bar.add_right_item(toggle_terminal, cx);
|
status_bar.add_right_item(toggle_terminal, cx);
|
||||||
}
|
}
|
||||||
status_bar.add_right_item(cursor_position, cx);
|
|
||||||
status_bar.add_right_item(feedback_button, cx);
|
status_bar.add_right_item(feedback_button, cx);
|
||||||
|
status_bar.add_right_item(active_buffer_language, cx);
|
||||||
|
status_bar.add_right_item(cursor_position, cx);
|
||||||
});
|
});
|
||||||
|
|
||||||
auto_update::notify_of_any_new_update(cx.weak_handle(), cx);
|
auto_update::notify_of_any_new_update(cx.weak_handle(), cx);
|
||||||
|
@ -485,54 +492,6 @@ fn about(_: &mut Workspace, _: &About, cx: &mut gpui::ViewContext<Workspace>) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn install_cli(cx: &AsyncAppContext) -> Result<()> {
|
|
||||||
let cli_path = cx.platform().path_for_auxiliary_executable("cli")?;
|
|
||||||
let link_path = Path::new("/usr/local/bin/zed");
|
|
||||||
let bin_dir_path = link_path.parent().unwrap();
|
|
||||||
|
|
||||||
// Don't re-create symlink if it points to the same CLI binary.
|
|
||||||
if smol::fs::read_link(link_path).await.ok().as_ref() == Some(&cli_path) {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the symlink is not there or is outdated, first try replacing it
|
|
||||||
// without escalating.
|
|
||||||
smol::fs::remove_file(link_path).await.log_err();
|
|
||||||
if smol::fs::unix::symlink(&cli_path, link_path)
|
|
||||||
.await
|
|
||||||
.log_err()
|
|
||||||
.is_some()
|
|
||||||
{
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
// The symlink could not be created, so use osascript with admin privileges
|
|
||||||
// to create it.
|
|
||||||
let status = smol::process::Command::new("osascript")
|
|
||||||
.args([
|
|
||||||
"-e",
|
|
||||||
&format!(
|
|
||||||
"do shell script \" \
|
|
||||||
mkdir -p \'{}\' && \
|
|
||||||
ln -sf \'{}\' \'{}\' \
|
|
||||||
\" with administrator privileges",
|
|
||||||
bin_dir_path.to_string_lossy(),
|
|
||||||
cli_path.to_string_lossy(),
|
|
||||||
link_path.to_string_lossy(),
|
|
||||||
),
|
|
||||||
])
|
|
||||||
.stdout(smol::process::Stdio::inherit())
|
|
||||||
.stderr(smol::process::Stdio::inherit())
|
|
||||||
.output()
|
|
||||||
.await?
|
|
||||||
.status;
|
|
||||||
if status.success() {
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
Err(anyhow!("error running osascript"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn open_config_file(
|
fn open_config_file(
|
||||||
path: &'static Path,
|
path: &'static Path,
|
||||||
app_state: Arc<AppState>,
|
app_state: Arc<AppState>,
|
||||||
|
@ -626,8 +585,13 @@ fn open_telemetry_log_file(
|
||||||
workspace.with_local_workspace(&app_state.clone(), cx, move |_, cx| {
|
workspace.with_local_workspace(&app_state.clone(), cx, move |_, cx| {
|
||||||
cx.spawn_weak(|workspace, mut cx| async move {
|
cx.spawn_weak(|workspace, mut cx| async move {
|
||||||
let workspace = workspace.upgrade(&cx)?;
|
let workspace = workspace.upgrade(&cx)?;
|
||||||
let path = app_state.client.telemetry_log_file_path()?;
|
|
||||||
let log = app_state.fs.load(&path).await.log_err()?;
|
async fn fetch_log_string(app_state: &Arc<AppState>) -> Option<String> {
|
||||||
|
let path = app_state.client.telemetry_log_file_path()?;
|
||||||
|
app_state.fs.load(&path).await.log_err()
|
||||||
|
}
|
||||||
|
|
||||||
|
let log = fetch_log_string(&app_state).await.unwrap_or_else(|| "// No data has been collected yet".to_string());
|
||||||
|
|
||||||
const MAX_TELEMETRY_LOG_LEN: usize = 5 * 1024 * 1024;
|
const MAX_TELEMETRY_LOG_LEN: usize = 5 * 1024 * 1024;
|
||||||
let mut start_offset = log.len().saturating_sub(MAX_TELEMETRY_LOG_LEN);
|
let mut start_offset = log.len().saturating_sub(MAX_TELEMETRY_LOG_LEN);
|
||||||
|
@ -635,6 +599,7 @@ fn open_telemetry_log_file(
|
||||||
start_offset += newline_offset + 1;
|
start_offset += newline_offset + 1;
|
||||||
}
|
}
|
||||||
let log_suffix = &log[start_offset..];
|
let log_suffix = &log[start_offset..];
|
||||||
|
let json = app_state.languages.language_for_name("JSON").await.log_err();
|
||||||
|
|
||||||
workspace.update(&mut cx, |workspace, cx| {
|
workspace.update(&mut cx, |workspace, cx| {
|
||||||
let project = workspace.project().clone();
|
let project = workspace.project().clone();
|
||||||
|
@ -642,7 +607,7 @@ fn open_telemetry_log_file(
|
||||||
.update(cx, |project, cx| project.create_buffer("", None, cx))
|
.update(cx, |project, cx| project.create_buffer("", None, cx))
|
||||||
.expect("creating buffers on a local workspace always succeeds");
|
.expect("creating buffers on a local workspace always succeeds");
|
||||||
buffer.update(cx, |buffer, cx| {
|
buffer.update(cx, |buffer, cx| {
|
||||||
buffer.set_language(app_state.languages.language_for_name("JSON"), cx);
|
buffer.set_language(json, cx);
|
||||||
buffer.edit(
|
buffer.edit(
|
||||||
[(
|
[(
|
||||||
0..0,
|
0..0,
|
||||||
|
@ -675,35 +640,42 @@ fn open_telemetry_log_file(
|
||||||
}
|
}
|
||||||
|
|
||||||
fn open_bundled_file(
|
fn open_bundled_file(
|
||||||
workspace: &mut Workspace,
|
|
||||||
app_state: Arc<AppState>,
|
app_state: Arc<AppState>,
|
||||||
asset_path: &'static str,
|
asset_path: &'static str,
|
||||||
title: &'static str,
|
title: &'static str,
|
||||||
language: &'static str,
|
language: &'static str,
|
||||||
cx: &mut ViewContext<Workspace>,
|
cx: &mut ViewContext<Workspace>,
|
||||||
) {
|
) {
|
||||||
workspace
|
let language = app_state.languages.language_for_name(language);
|
||||||
.with_local_workspace(&app_state, cx, |workspace, cx| {
|
cx.spawn(|workspace, mut cx| async move {
|
||||||
let project = workspace.project().clone();
|
let language = language.await.log_err();
|
||||||
let buffer = project.update(cx, |project, cx| {
|
workspace
|
||||||
let text = Assets::get(asset_path)
|
.update(&mut cx, |workspace, cx| {
|
||||||
.map(|f| f.data)
|
workspace.with_local_workspace(&app_state, cx, |workspace, cx| {
|
||||||
.unwrap_or_else(|| Cow::Borrowed(b"File not found"));
|
let project = workspace.project();
|
||||||
let text = str::from_utf8(text.as_ref()).unwrap();
|
let buffer = project.update(cx, |project, cx| {
|
||||||
project
|
let text = Assets::get(asset_path)
|
||||||
.create_buffer(text, project.languages().language_for_name(language), cx)
|
.map(|f| f.data)
|
||||||
.expect("creating buffers on a local workspace always succeeds")
|
.unwrap_or_else(|| Cow::Borrowed(b"File not found"));
|
||||||
});
|
let text = str::from_utf8(text.as_ref()).unwrap();
|
||||||
let buffer =
|
project
|
||||||
cx.add_model(|cx| MultiBuffer::singleton(buffer, cx).with_title(title.into()));
|
.create_buffer(text, language, cx)
|
||||||
workspace.add_item(
|
.expect("creating buffers on a local workspace always succeeds")
|
||||||
Box::new(
|
});
|
||||||
cx.add_view(|cx| Editor::for_multibuffer(buffer, Some(project.clone()), cx)),
|
let buffer = cx.add_model(|cx| {
|
||||||
),
|
MultiBuffer::singleton(buffer, cx).with_title(title.into())
|
||||||
cx,
|
});
|
||||||
);
|
workspace.add_item(
|
||||||
})
|
Box::new(cx.add_view(|cx| {
|
||||||
.detach();
|
Editor::for_multibuffer(buffer, Some(project.clone()), cx)
|
||||||
|
})),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn schema_file_match(path: &Path) -> &Path {
|
fn schema_file_match(path: &Path) -> &Path {
|
||||||
|
@ -753,6 +725,10 @@ mod tests {
|
||||||
"ca": null,
|
"ca": null,
|
||||||
"cb": null,
|
"cb": null,
|
||||||
},
|
},
|
||||||
|
"d": {
|
||||||
|
"da": null,
|
||||||
|
"db": null,
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
@ -761,13 +737,14 @@ mod tests {
|
||||||
open_paths(
|
open_paths(
|
||||||
&[PathBuf::from("/root/a"), PathBuf::from("/root/b")],
|
&[PathBuf::from("/root/a"), PathBuf::from("/root/b")],
|
||||||
&app_state,
|
&app_state,
|
||||||
|
None,
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
assert_eq!(cx.window_ids().len(), 1);
|
assert_eq!(cx.window_ids().len(), 1);
|
||||||
|
|
||||||
cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, cx))
|
cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, None, cx))
|
||||||
.await;
|
.await;
|
||||||
assert_eq!(cx.window_ids().len(), 1);
|
assert_eq!(cx.window_ids().len(), 1);
|
||||||
let workspace_1 = cx.root_view::<Workspace>(cx.window_ids()[0]).unwrap();
|
let workspace_1 = cx.root_view::<Workspace>(cx.window_ids()[0]).unwrap();
|
||||||
|
@ -781,11 +758,37 @@ mod tests {
|
||||||
open_paths(
|
open_paths(
|
||||||
&[PathBuf::from("/root/b"), PathBuf::from("/root/c")],
|
&[PathBuf::from("/root/b"), PathBuf::from("/root/c")],
|
||||||
&app_state,
|
&app_state,
|
||||||
|
None,
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
assert_eq!(cx.window_ids().len(), 2);
|
assert_eq!(cx.window_ids().len(), 2);
|
||||||
|
|
||||||
|
// Replace existing windows
|
||||||
|
let window_id = cx.window_ids()[0];
|
||||||
|
cx.update(|cx| {
|
||||||
|
open_paths(
|
||||||
|
&[PathBuf::from("/root/c"), PathBuf::from("/root/d")],
|
||||||
|
&app_state,
|
||||||
|
Some(window_id),
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
assert_eq!(cx.window_ids().len(), 2);
|
||||||
|
let workspace_1 = cx.root_view::<Workspace>(window_id).unwrap();
|
||||||
|
workspace_1.read_with(cx, |workspace, cx| {
|
||||||
|
assert_eq!(
|
||||||
|
workspace
|
||||||
|
.worktrees(cx)
|
||||||
|
.map(|w| w.read(cx).abs_path())
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
&[Path::new("/root/c").into(), Path::new("/root/d").into()]
|
||||||
|
);
|
||||||
|
assert!(workspace.left_sidebar().read(cx).is_open());
|
||||||
|
assert!(workspace.active_pane().is_focused(cx));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
|
@ -797,7 +800,7 @@ mod tests {
|
||||||
.insert_tree("/root", json!({"a": "hey"}))
|
.insert_tree("/root", json!({"a": "hey"}))
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, cx))
|
cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, None, cx))
|
||||||
.await;
|
.await;
|
||||||
assert_eq!(cx.window_ids().len(), 1);
|
assert_eq!(cx.window_ids().len(), 1);
|
||||||
|
|
||||||
|
@ -835,7 +838,7 @@ mod tests {
|
||||||
assert!(!cx.is_window_edited(workspace.window_id()));
|
assert!(!cx.is_window_edited(workspace.window_id()));
|
||||||
|
|
||||||
// Opening the buffer again doesn't impact the window's edited state.
|
// Opening the buffer again doesn't impact the window's edited state.
|
||||||
cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, cx))
|
cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, None, cx))
|
||||||
.await;
|
.await;
|
||||||
let editor = workspace.read_with(cx, |workspace, cx| {
|
let editor = workspace.read_with(cx, |workspace, cx| {
|
||||||
workspace
|
workspace
|
||||||
|
@ -865,7 +868,8 @@ mod tests {
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_new_empty_workspace(cx: &mut TestAppContext) {
|
async fn test_new_empty_workspace(cx: &mut TestAppContext) {
|
||||||
let app_state = init(cx);
|
let app_state = init(cx);
|
||||||
cx.update(|cx| open_new(&app_state, cx)).await;
|
cx.update(|cx| open_new(&app_state, cx, |_, cx| cx.dispatch_action(NewFile)))
|
||||||
|
.await;
|
||||||
|
|
||||||
let window_id = *cx.window_ids().first().unwrap();
|
let window_id = *cx.window_ids().first().unwrap();
|
||||||
let workspace = cx.root_view::<Workspace>(window_id).unwrap();
|
let workspace = cx.root_view::<Workspace>(window_id).unwrap();
|
||||||
|
@ -910,9 +914,7 @@ mod tests {
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
|
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
|
||||||
let (_, workspace) = cx.add_window(|cx| {
|
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
|
||||||
Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
|
|
||||||
});
|
|
||||||
|
|
||||||
let entries = cx.read(|cx| workspace.file_project_paths(cx));
|
let entries = cx.read(|cx| workspace.file_project_paths(cx));
|
||||||
let file1 = entries[0].clone();
|
let file1 = entries[0].clone();
|
||||||
|
@ -1031,9 +1033,7 @@ mod tests {
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let project = Project::test(app_state.fs.clone(), ["/dir1".as_ref()], cx).await;
|
let project = Project::test(app_state.fs.clone(), ["/dir1".as_ref()], cx).await;
|
||||||
let (_, workspace) = cx.add_window(|cx| {
|
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
|
||||||
Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
|
|
||||||
});
|
|
||||||
|
|
||||||
// Open a file within an existing worktree.
|
// Open a file within an existing worktree.
|
||||||
cx.update(|cx| {
|
cx.update(|cx| {
|
||||||
|
@ -1192,9 +1192,7 @@ mod tests {
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
|
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
|
||||||
let (window_id, workspace) = cx.add_window(|cx| {
|
let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
|
||||||
Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
|
|
||||||
});
|
|
||||||
|
|
||||||
// Open a file within an existing worktree.
|
// Open a file within an existing worktree.
|
||||||
cx.update(|cx| {
|
cx.update(|cx| {
|
||||||
|
@ -1236,9 +1234,7 @@ mod tests {
|
||||||
|
|
||||||
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
|
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
|
||||||
project.update(cx, |project, _| project.languages().add(rust_lang()));
|
project.update(cx, |project, _| project.languages().add(rust_lang()));
|
||||||
let (window_id, workspace) = cx.add_window(|cx| {
|
let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
|
||||||
Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
|
|
||||||
});
|
|
||||||
let worktree = cx.read(|cx| workspace.read(cx).worktrees(cx).next().unwrap());
|
let worktree = cx.read(|cx| workspace.read(cx).worktrees(cx).next().unwrap());
|
||||||
|
|
||||||
// Create a new untitled buffer
|
// Create a new untitled buffer
|
||||||
|
@ -1327,9 +1323,7 @@ mod tests {
|
||||||
|
|
||||||
let project = Project::test(app_state.fs.clone(), [], cx).await;
|
let project = Project::test(app_state.fs.clone(), [], cx).await;
|
||||||
project.update(cx, |project, _| project.languages().add(rust_lang()));
|
project.update(cx, |project, _| project.languages().add(rust_lang()));
|
||||||
let (window_id, workspace) = cx.add_window(|cx| {
|
let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
|
||||||
Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create a new untitled buffer
|
// Create a new untitled buffer
|
||||||
cx.dispatch_action(window_id, NewFile);
|
cx.dispatch_action(window_id, NewFile);
|
||||||
|
@ -1382,9 +1376,7 @@ mod tests {
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
|
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
|
||||||
let (window_id, workspace) = cx.add_window(|cx| {
|
let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
|
||||||
Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
|
|
||||||
});
|
|
||||||
|
|
||||||
let entries = cx.read(|cx| workspace.file_project_paths(cx));
|
let entries = cx.read(|cx| workspace.file_project_paths(cx));
|
||||||
let file1 = entries[0].clone();
|
let file1 = entries[0].clone();
|
||||||
|
@ -1458,15 +1450,7 @@ mod tests {
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
|
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
|
||||||
let (_, workspace) = cx.add_window(|cx| {
|
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
|
||||||
Workspace::new(
|
|
||||||
Default::default(),
|
|
||||||
0,
|
|
||||||
project.clone(),
|
|
||||||
|_, _| unimplemented!(),
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
});
|
|
||||||
|
|
||||||
let entries = cx.read(|cx| workspace.file_project_paths(cx));
|
let entries = cx.read(|cx| workspace.file_project_paths(cx));
|
||||||
let file1 = entries[0].clone();
|
let file1 = entries[0].clone();
|
||||||
|
@ -1730,15 +1714,7 @@ mod tests {
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
|
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
|
||||||
let (_, workspace) = cx.add_window(|cx| {
|
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
|
||||||
Workspace::new(
|
|
||||||
Default::default(),
|
|
||||||
0,
|
|
||||||
project.clone(),
|
|
||||||
|_, _| unimplemented!(),
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
});
|
|
||||||
let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
|
let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
|
||||||
|
|
||||||
let entries = cx.read(|cx| workspace.file_project_paths(cx));
|
let entries = cx.read(|cx| workspace.file_project_paths(cx));
|
||||||
|
|
|
@ -10,7 +10,7 @@ echo -e "# ###### THEME LICENSES ######\n" >> $OUTPUT_FILE
|
||||||
|
|
||||||
echo "Generating theme licenses"
|
echo "Generating theme licenses"
|
||||||
cd styles
|
cd styles
|
||||||
npm ci
|
npm --silent ci
|
||||||
npm run --silent build-licenses >> $OUTPUT_FILE
|
npm run --silent build-licenses >> $OUTPUT_FILE
|
||||||
cd ..
|
cd ..
|
||||||
|
|
||||||
|
|
|
@ -39,7 +39,7 @@ function getLicenseText(
|
||||||
if (typeof meta.license.license_text == "string") {
|
if (typeof meta.license.license_text == "string") {
|
||||||
callback(meta, meta.license.license_text)
|
callback(meta, meta.license.license_text)
|
||||||
} else {
|
} else {
|
||||||
let license_text_obj: Verification = meta.license.license_text;
|
let license_text_obj: Verification = meta.license.license_text
|
||||||
// The following copied from the example code on nodejs.org:
|
// The following copied from the example code on nodejs.org:
|
||||||
// https://nodejs.org/api/http.html#httpgetoptions-callback
|
// https://nodejs.org/api/http.html#httpgetoptions-callback
|
||||||
https
|
https
|
||||||
|
|
|
@ -20,6 +20,7 @@ import contactList from "./contactList"
|
||||||
import incomingCallNotification from "./incomingCallNotification"
|
import incomingCallNotification from "./incomingCallNotification"
|
||||||
import { ColorScheme } from "../themes/common/colorScheme"
|
import { ColorScheme } from "../themes/common/colorScheme"
|
||||||
import feedback from "./feedback"
|
import feedback from "./feedback"
|
||||||
|
import welcome from "./welcome"
|
||||||
|
|
||||||
export default function app(colorScheme: ColorScheme): Object {
|
export default function app(colorScheme: ColorScheme): Object {
|
||||||
return {
|
return {
|
||||||
|
@ -33,6 +34,7 @@ export default function app(colorScheme: ColorScheme): Object {
|
||||||
incomingCallNotification: incomingCallNotification(colorScheme),
|
incomingCallNotification: incomingCallNotification(colorScheme),
|
||||||
picker: picker(colorScheme),
|
picker: picker(colorScheme),
|
||||||
workspace: workspace(colorScheme),
|
workspace: workspace(colorScheme),
|
||||||
|
welcome: welcome(colorScheme),
|
||||||
contextMenu: contextMenu(colorScheme),
|
contextMenu: contextMenu(colorScheme),
|
||||||
editor: editor(colorScheme),
|
editor: editor(colorScheme),
|
||||||
projectDiagnostics: projectDiagnostics(colorScheme),
|
projectDiagnostics: projectDiagnostics(colorScheme),
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue