Detailed changes
@@ -1213,6 +1213,7 @@ dependencies = [
"git",
"gpui",
"hyper",
+ "indoc",
"language",
"lazy_static",
"lipsum",
@@ -1340,14 +1341,17 @@ dependencies = [
"anyhow",
"async-compression",
"async-tar",
+ "clock",
"collections",
"context_menu",
+ "fs",
"futures 0.3.25",
"gpui",
"language",
"log",
"lsp",
"node_runtime",
+ "rpc",
"serde",
"serde_derive",
"settings",
@@ -4687,6 +4691,7 @@ dependencies = [
"client",
"clock",
"collections",
+ "copilot",
"ctor",
"db",
"env_logger",
@@ -1,547 +1,548 @@
[
- // Standard macOS bindings
- {
- "bindings": {
- "up": "menu::SelectPrev",
- "pageup": "menu::SelectFirst",
- "shift-pageup": "menu::SelectFirst",
- "ctrl-p": "menu::SelectPrev",
- "down": "menu::SelectNext",
- "pagedown": "menu::SelectLast",
- "shift-pagedown": "menu::SelectFirst",
- "ctrl-n": "menu::SelectNext",
- "cmd-up": "menu::SelectFirst",
- "cmd-down": "menu::SelectLast",
- "enter": "menu::Confirm",
- "escape": "menu::Cancel",
- "ctrl-c": "menu::Cancel",
- "cmd-{": "pane::ActivatePrevItem",
- "cmd-}": "pane::ActivateNextItem",
- "alt-cmd-left": "pane::ActivatePrevItem",
- "alt-cmd-right": "pane::ActivateNextItem",
- "cmd-w": "pane::CloseActiveItem",
- "alt-cmd-t": "pane::CloseInactiveItems",
- "cmd-k u": "pane::CloseCleanItems",
- "cmd-k cmd-w": "pane::CloseAllItems",
- "cmd-shift-w": "workspace::CloseWindow",
- "cmd-s": "workspace::Save",
- "cmd-shift-s": "workspace::SaveAs",
- "cmd-=": "zed::IncreaseBufferFontSize",
- "cmd--": "zed::DecreaseBufferFontSize",
- "cmd-0": "zed::ResetBufferFontSize",
- "cmd-,": "zed::OpenSettings",
- "cmd-q": "zed::Quit",
- "cmd-h": "zed::Hide",
- "alt-cmd-h": "zed::HideOthers",
- "cmd-m": "zed::Minimize",
- "ctrl-cmd-f": "zed::ToggleFullScreen",
- "cmd-n": "workspace::NewFile",
- "cmd-shift-n": "workspace::NewWindow",
- "cmd-o": "workspace::Open",
- "alt-cmd-o": "projects::OpenRecent",
- "ctrl-`": "workspace::NewTerminal"
- }
- },
- {
- "context": "Editor",
- "bindings": {
- "escape": "editor::Cancel",
- "backspace": "editor::Backspace",
- "shift-backspace": "editor::Backspace",
- "ctrl-h": "editor::Backspace",
- "delete": "editor::Delete",
- "ctrl-d": "editor::Delete",
- "tab": "editor::Tab",
- "shift-tab": "editor::TabPrev",
- "ctrl-k": "editor::CutToEndOfLine",
- "ctrl-t": "editor::Transpose",
- "cmd-backspace": "editor::DeleteToBeginningOfLine",
- "cmd-delete": "editor::DeleteToEndOfLine",
- "alt-backspace": "editor::DeleteToPreviousWordStart",
- "alt-delete": "editor::DeleteToNextWordEnd",
- "alt-h": "editor::DeleteToPreviousWordStart",
- "alt-d": "editor::DeleteToNextWordEnd",
- "cmd-x": "editor::Cut",
- "cmd-c": "editor::Copy",
- "cmd-v": "editor::Paste",
- "cmd-z": "editor::Undo",
- "cmd-shift-z": "editor::Redo",
- "up": "editor::MoveUp",
- "pageup": "editor::PageUp",
- "shift-pageup": "editor::MovePageUp",
- "home": "editor::MoveToBeginningOfLine",
- "down": "editor::MoveDown",
- "pagedown": "editor::PageDown",
- "shift-pagedown": "editor::MovePageDown",
- "end": "editor::MoveToEndOfLine",
- "left": "editor::MoveLeft",
- "right": "editor::MoveRight",
- "ctrl-p": "editor::MoveUp",
- "ctrl-n": "editor::MoveDown",
- "ctrl-b": "editor::MoveLeft",
- "ctrl-f": "editor::MoveRight",
- "ctrl-l": "editor::NextScreen",
- "alt-left": "editor::MoveToPreviousWordStart",
- "alt-b": "editor::MoveToPreviousWordStart",
- "alt-right": "editor::MoveToNextWordEnd",
- "alt-f": "editor::MoveToNextWordEnd",
- "cmd-left": "editor::MoveToBeginningOfLine",
- "ctrl-a": "editor::MoveToBeginningOfLine",
- "cmd-right": "editor::MoveToEndOfLine",
- "ctrl-e": "editor::MoveToEndOfLine",
- "cmd-up": "editor::MoveToBeginning",
- "cmd-down": "editor::MoveToEnd",
- "shift-up": "editor::SelectUp",
- "ctrl-shift-p": "editor::SelectUp",
- "shift-down": "editor::SelectDown",
- "ctrl-shift-n": "editor::SelectDown",
- "shift-left": "editor::SelectLeft",
- "ctrl-shift-b": "editor::SelectLeft",
- "shift-right": "editor::SelectRight",
- "ctrl-shift-f": "editor::SelectRight",
- "alt-shift-left": "editor::SelectToPreviousWordStart",
- "alt-shift-b": "editor::SelectToPreviousWordStart",
- "alt-shift-right": "editor::SelectToNextWordEnd",
- "alt-shift-f": "editor::SelectToNextWordEnd",
- "cmd-shift-up": "editor::SelectToBeginning",
- "cmd-shift-down": "editor::SelectToEnd",
- "cmd-a": "editor::SelectAll",
- "cmd-l": "editor::SelectLine",
- "cmd-shift-i": "editor::Format",
- "cmd-shift-left": [
- "editor::SelectToBeginningOfLine",
- {
- "stop_at_soft_wraps": true
- }
- ],
- "shift-home": [
- "editor::SelectToBeginningOfLine",
- {
- "stop_at_soft_wraps": true
- }
- ],
- "ctrl-shift-a": [
- "editor::SelectToBeginningOfLine",
- {
- "stop_at_soft_wraps": true
- }
- ],
- "cmd-shift-right": [
- "editor::SelectToEndOfLine",
- {
- "stop_at_soft_wraps": true
- }
- ],
- "shift-end": [
- "editor::SelectToEndOfLine",
- {
- "stop_at_soft_wraps": true
- }
- ],
- "ctrl-shift-e": [
- "editor::SelectToEndOfLine",
- {
- "stop_at_soft_wraps": true
- }
- ],
- "ctrl-v": [
- "editor::MovePageDown",
- {
- "center_cursor": true
- }
- ],
- "alt-v": [
- "editor::MovePageUp",
- {
- "center_cursor": true
- }
- ],
- "ctrl-cmd-space": "editor::ShowCharacterPalette"
- }
- },
- {
- "context": "Editor && mode == full",
- "bindings": {
- "enter": "editor::Newline",
- "cmd-enter": "editor::NewlineBelow",
- "alt-z": "editor::ToggleSoftWrap",
- "cmd-f": [
- "buffer_search::Deploy",
- {
- "focus": true
- }
- ],
- "cmd-e": [
- "buffer_search::Deploy",
- {
- "focus": false
- }
- ],
- "alt-\\": "copilot::NextSuggestion",
- "alt-]": "copilot::NextSuggestion",
- "alt-[": "copilot::PreviousSuggestion"
- }
- },
- {
- "context": "Editor && mode == auto_height",
- "bindings": {
- "alt-enter": "editor::Newline",
- "cmd-alt-enter": "editor::NewlineBelow"
- }
- },
- {
- "context": "BufferSearchBar > Editor",
- "bindings": {
- "escape": "buffer_search::Dismiss",
- "tab": "buffer_search::FocusEditor",
- "enter": "search::SelectNextMatch",
- "shift-enter": "search::SelectPrevMatch"
- }
- },
- {
- "context": "Pane",
- "bindings": {
- "cmd-f": "project_search::ToggleFocus",
- "cmd-g": "search::SelectNextMatch",
- "cmd-shift-g": "search::SelectPrevMatch",
- "alt-cmd-c": "search::ToggleCaseSensitive",
- "alt-cmd-w": "search::ToggleWholeWord",
- "alt-cmd-r": "search::ToggleRegex"
- }
- },
- // Bindings from VS Code
- {
- "context": "Editor",
- "bindings": {
- "cmd-[": "editor::Outdent",
- "cmd-]": "editor::Indent",
- "cmd-alt-up": "editor::AddSelectionAbove",
- "cmd-ctrl-p": "editor::AddSelectionAbove",
- "cmd-alt-down": "editor::AddSelectionBelow",
- "cmd-ctrl-n": "editor::AddSelectionBelow",
- "cmd-d": [
- "editor::SelectNext",
- {
- "replace_newest": false
- }
- ],
- "cmd-k cmd-d": [
- "editor::SelectNext",
- {
- "replace_newest": true
- }
- ],
- "cmd-k cmd-i": "editor::Hover",
- "cmd-/": [
- "editor::ToggleComments",
- {
- "advance_downwards": false
- }
- ],
- "alt-up": "editor::SelectLargerSyntaxNode",
- "alt-down": "editor::SelectSmallerSyntaxNode",
- "cmd-u": "editor::UndoSelection",
- "cmd-shift-u": "editor::RedoSelection",
- "f8": "editor::GoToDiagnostic",
- "shift-f8": "editor::GoToPrevDiagnostic",
- "f2": "editor::Rename",
- "f12": "editor::GoToDefinition",
- "cmd-f12": "editor::GoToTypeDefinition",
- "alt-shift-f12": "editor::FindAllReferences",
- "ctrl-m": "editor::MoveToEnclosingBracket",
- "alt-cmd-[": "editor::Fold",
- "alt-cmd-]": "editor::UnfoldLines",
- "ctrl-space": "editor::ShowCompletions",
- "cmd-.": "editor::ToggleCodeActions",
- "alt-cmd-r": "editor::RevealInFinder"
- }
- },
- {
- "context": "Editor && mode == full",
- "bindings": {
- "cmd-shift-o": "outline::Toggle",
- "ctrl-g": "go_to_line::Toggle"
- }
- },
- {
- "context": "Pane",
- "bindings": {
- "ctrl-1": [
- "pane::ActivateItem",
- 0
- ],
- "ctrl-2": [
- "pane::ActivateItem",
- 1
- ],
- "ctrl-3": [
- "pane::ActivateItem",
- 2
- ],
- "ctrl-4": [
- "pane::ActivateItem",
- 3
- ],
- "ctrl-5": [
- "pane::ActivateItem",
- 4
- ],
- "ctrl-6": [
- "pane::ActivateItem",
- 5
- ],
- "ctrl-7": [
- "pane::ActivateItem",
- 6
- ],
- "ctrl-8": [
- "pane::ActivateItem",
- 7
- ],
- "ctrl-9": [
- "pane::ActivateItem",
- 8
- ],
- "ctrl-0": "pane::ActivateLastItem",
- "ctrl--": "pane::GoBack",
- "ctrl-_": "pane::GoForward",
- "cmd-shift-t": "pane::ReopenClosedItem",
- "cmd-shift-f": "project_search::ToggleFocus"
- }
- },
- {
- "context": "Workspace",
- "bindings": {
- "cmd-1": [
- "workspace::ActivatePane",
- 0
- ],
- "cmd-2": [
- "workspace::ActivatePane",
- 1
- ],
- "cmd-3": [
- "workspace::ActivatePane",
- 2
- ],
- "cmd-4": [
- "workspace::ActivatePane",
- 3
- ],
- "cmd-5": [
- "workspace::ActivatePane",
- 4
- ],
- "cmd-6": [
- "workspace::ActivatePane",
- 5
- ],
- "cmd-7": [
- "workspace::ActivatePane",
- 6
- ],
- "cmd-8": [
- "workspace::ActivatePane",
- 7
- ],
- "cmd-9": [
- "workspace::ActivatePane",
- 8
- ],
- "cmd-b": "workspace::ToggleLeftSidebar",
- "cmd-shift-f": "workspace::NewSearch",
- "cmd-k cmd-t": "theme_selector::Toggle",
- "cmd-k cmd-s": "zed::OpenKeymap",
- "cmd-t": "project_symbols::Toggle",
- "cmd-p": "file_finder::Toggle",
- "cmd-shift-p": "command_palette::Toggle",
- "cmd-shift-m": "diagnostics::Deploy",
- "cmd-shift-e": "project_panel::ToggleFocus",
- "cmd-alt-s": "workspace::SaveAll",
- "cmd-k m": "language_selector::Toggle"
- }
- },
- // Bindings from Sublime Text
- {
- "context": "Editor",
- "bindings": {
- "ctrl-shift-k": "editor::DeleteLine",
- "cmd-shift-d": "editor::DuplicateLine",
- "cmd-shift-l": "editor::SplitSelectionIntoLines",
- "ctrl-cmd-up": "editor::MoveLineUp",
- "ctrl-cmd-down": "editor::MoveLineDown",
- "ctrl-alt-backspace": "editor::DeleteToPreviousSubwordStart",
- "ctrl-alt-h": "editor::DeleteToPreviousSubwordStart",
- "ctrl-alt-delete": "editor::DeleteToNextSubwordEnd",
- "ctrl-alt-d": "editor::DeleteToNextSubwordEnd",
- "ctrl-alt-left": "editor::MoveToPreviousSubwordStart",
- "ctrl-alt-b": "editor::MoveToPreviousSubwordStart",
- "ctrl-alt-right": "editor::MoveToNextSubwordEnd",
- "ctrl-alt-f": "editor::MoveToNextSubwordEnd",
- "ctrl-alt-shift-left": "editor::SelectToPreviousSubwordStart",
- "ctrl-alt-shift-b": "editor::SelectToPreviousSubwordStart",
- "ctrl-alt-shift-right": "editor::SelectToNextSubwordEnd",
- "ctrl-alt-shift-f": "editor::SelectToNextSubwordEnd"
- }
- },
- {
- "bindings": {
- "cmd-k cmd-left": "workspace::ActivatePreviousPane",
- "cmd-k cmd-right": "workspace::ActivateNextPane"
- }
- },
- // Bindings from Atom
- {
- "context": "Pane",
- "bindings": {
- "cmd-k up": "pane::SplitUp",
- "cmd-k down": "pane::SplitDown",
- "cmd-k left": "pane::SplitLeft",
- "cmd-k right": "pane::SplitRight"
+ // Standard macOS bindings
+ {
+ "bindings": {
+ "up": "menu::SelectPrev",
+ "pageup": "menu::SelectFirst",
+ "shift-pageup": "menu::SelectFirst",
+ "ctrl-p": "menu::SelectPrev",
+ "down": "menu::SelectNext",
+ "pagedown": "menu::SelectLast",
+ "shift-pagedown": "menu::SelectFirst",
+ "ctrl-n": "menu::SelectNext",
+ "cmd-up": "menu::SelectFirst",
+ "cmd-down": "menu::SelectLast",
+ "enter": "menu::Confirm",
+ "escape": "menu::Cancel",
+ "ctrl-c": "menu::Cancel",
+ "cmd-{": "pane::ActivatePrevItem",
+ "cmd-}": "pane::ActivateNextItem",
+ "alt-cmd-left": "pane::ActivatePrevItem",
+ "alt-cmd-right": "pane::ActivateNextItem",
+ "cmd-w": "pane::CloseActiveItem",
+ "alt-cmd-t": "pane::CloseInactiveItems",
+ "cmd-k u": "pane::CloseCleanItems",
+ "cmd-k cmd-w": "pane::CloseAllItems",
+ "cmd-shift-w": "workspace::CloseWindow",
+ "cmd-s": "workspace::Save",
+ "cmd-shift-s": "workspace::SaveAs",
+ "cmd-=": "zed::IncreaseBufferFontSize",
+ "cmd--": "zed::DecreaseBufferFontSize",
+ "cmd-0": "zed::ResetBufferFontSize",
+ "cmd-,": "zed::OpenSettings",
+ "cmd-q": "zed::Quit",
+ "cmd-h": "zed::Hide",
+ "alt-cmd-h": "zed::HideOthers",
+ "cmd-m": "zed::Minimize",
+ "ctrl-cmd-f": "zed::ToggleFullScreen",
+ "cmd-n": "workspace::NewFile",
+ "cmd-shift-n": "workspace::NewWindow",
+ "cmd-o": "workspace::Open",
+ "alt-cmd-o": "projects::OpenRecent",
+ "ctrl-`": "workspace::NewTerminal"
+ }
+ },
+ {
+ "context": "Editor",
+ "bindings": {
+ "escape": "editor::Cancel",
+ "backspace": "editor::Backspace",
+ "shift-backspace": "editor::Backspace",
+ "ctrl-h": "editor::Backspace",
+ "delete": "editor::Delete",
+ "ctrl-d": "editor::Delete",
+ "tab": "editor::Tab",
+ "shift-tab": "editor::TabPrev",
+ "ctrl-k": "editor::CutToEndOfLine",
+ "ctrl-t": "editor::Transpose",
+ "cmd-backspace": "editor::DeleteToBeginningOfLine",
+ "cmd-delete": "editor::DeleteToEndOfLine",
+ "alt-backspace": "editor::DeleteToPreviousWordStart",
+ "alt-delete": "editor::DeleteToNextWordEnd",
+ "alt-h": "editor::DeleteToPreviousWordStart",
+ "alt-d": "editor::DeleteToNextWordEnd",
+ "cmd-x": "editor::Cut",
+ "cmd-c": "editor::Copy",
+ "cmd-v": "editor::Paste",
+ "cmd-z": "editor::Undo",
+ "cmd-shift-z": "editor::Redo",
+ "up": "editor::MoveUp",
+ "pageup": "editor::PageUp",
+ "shift-pageup": "editor::MovePageUp",
+ "home": "editor::MoveToBeginningOfLine",
+ "down": "editor::MoveDown",
+ "pagedown": "editor::PageDown",
+ "shift-pagedown": "editor::MovePageDown",
+ "end": "editor::MoveToEndOfLine",
+ "left": "editor::MoveLeft",
+ "right": "editor::MoveRight",
+ "ctrl-p": "editor::MoveUp",
+ "ctrl-n": "editor::MoveDown",
+ "ctrl-b": "editor::MoveLeft",
+ "ctrl-f": "editor::MoveRight",
+ "ctrl-l": "editor::NextScreen",
+ "alt-left": "editor::MoveToPreviousWordStart",
+ "alt-b": "editor::MoveToPreviousWordStart",
+ "alt-right": "editor::MoveToNextWordEnd",
+ "alt-f": "editor::MoveToNextWordEnd",
+ "cmd-left": "editor::MoveToBeginningOfLine",
+ "ctrl-a": "editor::MoveToBeginningOfLine",
+ "cmd-right": "editor::MoveToEndOfLine",
+ "ctrl-e": "editor::MoveToEndOfLine",
+ "cmd-up": "editor::MoveToBeginning",
+ "cmd-down": "editor::MoveToEnd",
+ "shift-up": "editor::SelectUp",
+ "ctrl-shift-p": "editor::SelectUp",
+ "shift-down": "editor::SelectDown",
+ "ctrl-shift-n": "editor::SelectDown",
+ "shift-left": "editor::SelectLeft",
+ "ctrl-shift-b": "editor::SelectLeft",
+ "shift-right": "editor::SelectRight",
+ "ctrl-shift-f": "editor::SelectRight",
+ "alt-shift-left": "editor::SelectToPreviousWordStart",
+ "alt-shift-b": "editor::SelectToPreviousWordStart",
+ "alt-shift-right": "editor::SelectToNextWordEnd",
+ "alt-shift-f": "editor::SelectToNextWordEnd",
+ "cmd-shift-up": "editor::SelectToBeginning",
+ "cmd-shift-down": "editor::SelectToEnd",
+ "cmd-a": "editor::SelectAll",
+ "cmd-l": "editor::SelectLine",
+ "cmd-shift-i": "editor::Format",
+ "cmd-shift-left": [
+ "editor::SelectToBeginningOfLine",
+ {
+ "stop_at_soft_wraps": true
}
- },
- // Bindings that should be unified with bindings for more general actions
- {
- "context": "Editor && renaming",
- "bindings": {
- "enter": "editor::ConfirmRename"
+ ],
+ "shift-home": [
+ "editor::SelectToBeginningOfLine",
+ {
+ "stop_at_soft_wraps": true
}
- },
- {
- "context": "Editor && showing_completions",
- "bindings": {
- "enter": "editor::ConfirmCompletion",
- "tab": "editor::ConfirmCompletion"
+ ],
+ "ctrl-shift-a": [
+ "editor::SelectToBeginningOfLine",
+ {
+ "stop_at_soft_wraps": true
}
- },
- {
- "context": "Editor && showing_code_actions",
- "bindings": {
- "enter": "editor::ConfirmCodeAction"
+ ],
+ "cmd-shift-right": [
+ "editor::SelectToEndOfLine",
+ {
+ "stop_at_soft_wraps": true
}
- },
- // Custom bindings
- {
- "bindings": {
- "ctrl-alt-cmd-f": "workspace::FollowNextCollaborator",
- "cmd-shift-c": "collab::ToggleContactsMenu",
- "cmd-alt-i": "zed::DebugElements"
+ ],
+ "shift-end": [
+ "editor::SelectToEndOfLine",
+ {
+ "stop_at_soft_wraps": true
}
- },
- {
- "context": "Editor",
- "bindings": {
- "alt-enter": "editor::OpenExcerpts",
- "cmd-f8": "editor::GoToHunk",
- "cmd-shift-f8": "editor::GoToPrevHunk"
+ ],
+ "ctrl-shift-e": [
+ "editor::SelectToEndOfLine",
+ {
+ "stop_at_soft_wraps": true
}
- },
- {
- "context": "ProjectSearchBar",
- "bindings": {
- "cmd-enter": "project_search::SearchInNew"
+ ],
+ "ctrl-v": [
+ "editor::MovePageDown",
+ {
+ "center_cursor": true
}
- },
- {
- "context": "Workspace",
- "bindings": {
- "shift-escape": "dock::FocusDock"
+ ],
+ "alt-v": [
+ "editor::MovePageUp",
+ {
+ "center_cursor": true
}
- },
- {
- "bindings": {
- "cmd-shift-k cmd-shift-right": "dock::AnchorDockRight",
- "cmd-shift-k cmd-shift-down": "dock::AnchorDockBottom",
- "cmd-shift-k cmd-shift-up": "dock::ExpandDock"
+ ],
+ "ctrl-cmd-space": "editor::ShowCharacterPalette"
+ }
+ },
+ {
+ "context": "Editor && mode == full",
+ "bindings": {
+ "enter": "editor::Newline",
+ "cmd-shift-enter": "editor::NewlineAbove",
+ "cmd-enter": "editor::NewlineBelow",
+ "alt-z": "editor::ToggleSoftWrap",
+ "cmd-f": [
+ "buffer_search::Deploy",
+ {
+ "focus": true
}
- },
- {
- "context": "Pane",
- "bindings": {
- "cmd-escape": "dock::AddTabToDock"
+ ],
+ "cmd-e": [
+ "buffer_search::Deploy",
+ {
+ "focus": false
}
- },
- {
- "context": "Pane && docked",
- "bindings": {
- "shift-escape": "dock::HideDock",
- "cmd-escape": "dock::RemoveTabFromDock"
+ ],
+ "alt-\\": "copilot::Suggest",
+ "alt-]": "copilot::NextSuggestion",
+ "alt-[": "copilot::PreviousSuggestion"
+ }
+ },
+ {
+ "context": "Editor && mode == auto_height",
+ "bindings": {
+ "alt-enter": "editor::Newline",
+ "cmd-alt-enter": "editor::NewlineBelow"
+ }
+ },
+ {
+ "context": "BufferSearchBar > Editor",
+ "bindings": {
+ "escape": "buffer_search::Dismiss",
+ "tab": "buffer_search::FocusEditor",
+ "enter": "search::SelectNextMatch",
+ "shift-enter": "search::SelectPrevMatch"
+ }
+ },
+ {
+ "context": "Pane",
+ "bindings": {
+ "cmd-f": "project_search::ToggleFocus",
+ "cmd-g": "search::SelectNextMatch",
+ "cmd-shift-g": "search::SelectPrevMatch",
+ "alt-cmd-c": "search::ToggleCaseSensitive",
+ "alt-cmd-w": "search::ToggleWholeWord",
+ "alt-cmd-r": "search::ToggleRegex"
+ }
+ },
+ // Bindings from VS Code
+ {
+ "context": "Editor",
+ "bindings": {
+ "cmd-[": "editor::Outdent",
+ "cmd-]": "editor::Indent",
+ "cmd-alt-up": "editor::AddSelectionAbove",
+ "cmd-ctrl-p": "editor::AddSelectionAbove",
+ "cmd-alt-down": "editor::AddSelectionBelow",
+ "cmd-ctrl-n": "editor::AddSelectionBelow",
+ "cmd-d": [
+ "editor::SelectNext",
+ {
+ "replace_newest": false
}
- },
- {
- "context": "ProjectPanel",
- "bindings": {
- "left": "project_panel::CollapseSelectedEntry",
- "right": "project_panel::ExpandSelectedEntry",
- "cmd-x": "project_panel::Cut",
- "cmd-c": "project_panel::Copy",
- "cmd-v": "project_panel::Paste",
- "cmd-alt-c": "project_panel::CopyPath",
- "alt-cmd-shift-c": "project_panel::CopyRelativePath",
- "f2": "project_panel::Rename",
- "backspace": "project_panel::Delete",
- "alt-cmd-r": "project_panel::RevealInFinder"
+ ],
+ "cmd-k cmd-d": [
+ "editor::SelectNext",
+ {
+ "replace_newest": true
}
- },
- {
- "context": "Terminal",
- "bindings": {
- "ctrl-cmd-space": "terminal::ShowCharacterPalette",
- "cmd-c": "terminal::Copy",
- "cmd-v": "terminal::Paste",
- "cmd-k": "terminal::Clear",
- // Some nice conveniences
- "cmd-backspace": [
- "terminal::SendText",
- "\u0015"
- ],
- "cmd-right": [
- "terminal::SendText",
- "\u0005"
- ],
- "cmd-left": [
- "terminal::SendText",
- "\u0001"
- ],
- // Terminal.app compatability
- "alt-left": [
- "terminal::SendText",
- "\u001bb"
- ],
- "alt-right": [
- "terminal::SendText",
- "\u001bf"
- ],
- // There are conflicting bindings for these keys in the global context.
- // these bindings override them, remove at your own risk:
- "up": [
- "terminal::SendKeystroke",
- "up"
- ],
- "pageup": [
- "terminal::SendKeystroke",
- "pageup"
- ],
- "down": [
- "terminal::SendKeystroke",
- "down"
- ],
- "pagedown": [
- "terminal::SendKeystroke",
- "pagedown"
- ],
- "escape": [
- "terminal::SendKeystroke",
- "escape"
- ],
- "enter": [
- "terminal::SendKeystroke",
- "enter"
- ],
- "ctrl-c": [
- "terminal::SendKeystroke",
- "ctrl-c"
- ]
+ ],
+ "cmd-k cmd-i": "editor::Hover",
+ "cmd-/": [
+ "editor::ToggleComments",
+ {
+ "advance_downwards": false
}
+ ],
+ "alt-up": "editor::SelectLargerSyntaxNode",
+ "alt-down": "editor::SelectSmallerSyntaxNode",
+ "cmd-u": "editor::UndoSelection",
+ "cmd-shift-u": "editor::RedoSelection",
+ "f8": "editor::GoToDiagnostic",
+ "shift-f8": "editor::GoToPrevDiagnostic",
+ "f2": "editor::Rename",
+ "f12": "editor::GoToDefinition",
+ "cmd-f12": "editor::GoToTypeDefinition",
+ "alt-shift-f12": "editor::FindAllReferences",
+ "ctrl-m": "editor::MoveToEnclosingBracket",
+ "alt-cmd-[": "editor::Fold",
+ "alt-cmd-]": "editor::UnfoldLines",
+ "ctrl-space": "editor::ShowCompletions",
+ "cmd-.": "editor::ToggleCodeActions",
+ "alt-cmd-r": "editor::RevealInFinder"
+ }
+ },
+ {
+ "context": "Editor && mode == full",
+ "bindings": {
+ "cmd-shift-o": "outline::Toggle",
+ "ctrl-g": "go_to_line::Toggle"
+ }
+ },
+ {
+ "context": "Pane",
+ "bindings": {
+ "ctrl-1": [
+ "pane::ActivateItem",
+ 0
+ ],
+ "ctrl-2": [
+ "pane::ActivateItem",
+ 1
+ ],
+ "ctrl-3": [
+ "pane::ActivateItem",
+ 2
+ ],
+ "ctrl-4": [
+ "pane::ActivateItem",
+ 3
+ ],
+ "ctrl-5": [
+ "pane::ActivateItem",
+ 4
+ ],
+ "ctrl-6": [
+ "pane::ActivateItem",
+ 5
+ ],
+ "ctrl-7": [
+ "pane::ActivateItem",
+ 6
+ ],
+ "ctrl-8": [
+ "pane::ActivateItem",
+ 7
+ ],
+ "ctrl-9": [
+ "pane::ActivateItem",
+ 8
+ ],
+ "ctrl-0": "pane::ActivateLastItem",
+ "ctrl--": "pane::GoBack",
+ "ctrl-_": "pane::GoForward",
+ "cmd-shift-t": "pane::ReopenClosedItem",
+ "cmd-shift-f": "project_search::ToggleFocus"
+ }
+ },
+ {
+ "context": "Workspace",
+ "bindings": {
+ "cmd-1": [
+ "workspace::ActivatePane",
+ 0
+ ],
+ "cmd-2": [
+ "workspace::ActivatePane",
+ 1
+ ],
+ "cmd-3": [
+ "workspace::ActivatePane",
+ 2
+ ],
+ "cmd-4": [
+ "workspace::ActivatePane",
+ 3
+ ],
+ "cmd-5": [
+ "workspace::ActivatePane",
+ 4
+ ],
+ "cmd-6": [
+ "workspace::ActivatePane",
+ 5
+ ],
+ "cmd-7": [
+ "workspace::ActivatePane",
+ 6
+ ],
+ "cmd-8": [
+ "workspace::ActivatePane",
+ 7
+ ],
+ "cmd-9": [
+ "workspace::ActivatePane",
+ 8
+ ],
+ "cmd-b": "workspace::ToggleLeftSidebar",
+ "cmd-shift-f": "workspace::NewSearch",
+ "cmd-k cmd-t": "theme_selector::Toggle",
+ "cmd-k cmd-s": "zed::OpenKeymap",
+ "cmd-t": "project_symbols::Toggle",
+ "cmd-p": "file_finder::Toggle",
+ "cmd-shift-p": "command_palette::Toggle",
+ "cmd-shift-m": "diagnostics::Deploy",
+ "cmd-shift-e": "project_panel::ToggleFocus",
+ "cmd-alt-s": "workspace::SaveAll",
+ "cmd-k m": "language_selector::Toggle"
+ }
+ },
+ // Bindings from Sublime Text
+ {
+ "context": "Editor",
+ "bindings": {
+ "ctrl-shift-k": "editor::DeleteLine",
+ "cmd-shift-d": "editor::DuplicateLine",
+ "cmd-shift-l": "editor::SplitSelectionIntoLines",
+ "ctrl-cmd-up": "editor::MoveLineUp",
+ "ctrl-cmd-down": "editor::MoveLineDown",
+ "ctrl-alt-backspace": "editor::DeleteToPreviousSubwordStart",
+ "ctrl-alt-h": "editor::DeleteToPreviousSubwordStart",
+ "ctrl-alt-delete": "editor::DeleteToNextSubwordEnd",
+ "ctrl-alt-d": "editor::DeleteToNextSubwordEnd",
+ "ctrl-alt-left": "editor::MoveToPreviousSubwordStart",
+ "ctrl-alt-b": "editor::MoveToPreviousSubwordStart",
+ "ctrl-alt-right": "editor::MoveToNextSubwordEnd",
+ "ctrl-alt-f": "editor::MoveToNextSubwordEnd",
+ "ctrl-alt-shift-left": "editor::SelectToPreviousSubwordStart",
+ "ctrl-alt-shift-b": "editor::SelectToPreviousSubwordStart",
+ "ctrl-alt-shift-right": "editor::SelectToNextSubwordEnd",
+ "ctrl-alt-shift-f": "editor::SelectToNextSubwordEnd"
+ }
+ },
+ {
+ "bindings": {
+ "cmd-k cmd-left": "workspace::ActivatePreviousPane",
+ "cmd-k cmd-right": "workspace::ActivateNextPane"
+ }
+ },
+ // Bindings from Atom
+ {
+ "context": "Pane",
+ "bindings": {
+ "cmd-k up": "pane::SplitUp",
+ "cmd-k down": "pane::SplitDown",
+ "cmd-k left": "pane::SplitLeft",
+ "cmd-k right": "pane::SplitRight"
+ }
+ },
+ // Bindings that should be unified with bindings for more general actions
+ {
+ "context": "Editor && renaming",
+ "bindings": {
+ "enter": "editor::ConfirmRename"
+ }
+ },
+ {
+ "context": "Editor && showing_completions",
+ "bindings": {
+ "enter": "editor::ConfirmCompletion",
+ "tab": "editor::ConfirmCompletion"
+ }
+ },
+ {
+ "context": "Editor && showing_code_actions",
+ "bindings": {
+ "enter": "editor::ConfirmCodeAction"
+ }
+ },
+ // Custom bindings
+ {
+ "bindings": {
+ "ctrl-alt-cmd-f": "workspace::FollowNextCollaborator",
+ "cmd-shift-c": "collab::ToggleContactsMenu",
+ "cmd-alt-i": "zed::DebugElements"
+ }
+ },
+ {
+ "context": "Editor",
+ "bindings": {
+ "alt-enter": "editor::OpenExcerpts",
+ "cmd-f8": "editor::GoToHunk",
+ "cmd-shift-f8": "editor::GoToPrevHunk"
+ }
+ },
+ {
+ "context": "ProjectSearchBar",
+ "bindings": {
+ "cmd-enter": "project_search::SearchInNew"
+ }
+ },
+ {
+ "context": "Workspace",
+ "bindings": {
+ "shift-escape": "dock::FocusDock"
+ }
+ },
+ {
+ "bindings": {
+ "cmd-shift-k cmd-shift-right": "dock::AnchorDockRight",
+ "cmd-shift-k cmd-shift-down": "dock::AnchorDockBottom",
+ "cmd-shift-k cmd-shift-up": "dock::ExpandDock"
+ }
+ },
+ {
+ "context": "Pane",
+ "bindings": {
+ "cmd-escape": "dock::AddTabToDock"
+ }
+ },
+ {
+ "context": "Pane && docked",
+ "bindings": {
+ "shift-escape": "dock::HideDock",
+ "cmd-escape": "dock::RemoveTabFromDock"
+ }
+ },
+ {
+ "context": "ProjectPanel",
+ "bindings": {
+ "left": "project_panel::CollapseSelectedEntry",
+ "right": "project_panel::ExpandSelectedEntry",
+ "cmd-x": "project_panel::Cut",
+ "cmd-c": "project_panel::Copy",
+ "cmd-v": "project_panel::Paste",
+ "cmd-alt-c": "project_panel::CopyPath",
+ "alt-cmd-shift-c": "project_panel::CopyRelativePath",
+ "f2": "project_panel::Rename",
+ "backspace": "project_panel::Delete",
+ "alt-cmd-r": "project_panel::RevealInFinder"
+ }
+ },
+ {
+ "context": "Terminal",
+ "bindings": {
+ "ctrl-cmd-space": "terminal::ShowCharacterPalette",
+ "cmd-c": "terminal::Copy",
+ "cmd-v": "terminal::Paste",
+ "cmd-k": "terminal::Clear",
+ // Some nice conveniences
+ "cmd-backspace": [
+ "terminal::SendText",
+ "\u0015"
+ ],
+ "cmd-right": [
+ "terminal::SendText",
+ "\u0005"
+ ],
+ "cmd-left": [
+ "terminal::SendText",
+ "\u0001"
+ ],
+ // Terminal.app compatability
+ "alt-left": [
+ "terminal::SendText",
+ "\u001bb"
+ ],
+ "alt-right": [
+ "terminal::SendText",
+ "\u001bf"
+ ],
+ // There are conflicting bindings for these keys in the global context.
+ // these bindings override them, remove at your own risk:
+ "up": [
+ "terminal::SendKeystroke",
+ "up"
+ ],
+ "pageup": [
+ "terminal::SendKeystroke",
+ "pageup"
+ ],
+ "down": [
+ "terminal::SendKeystroke",
+ "down"
+ ],
+ "pagedown": [
+ "terminal::SendKeystroke",
+ "pagedown"
+ ],
+ "escape": [
+ "terminal::SendKeystroke",
+ "escape"
+ ],
+ "enter": [
+ "terminal::SendKeystroke",
+ "enter"
+ ],
+ "ctrl-c": [
+ "terminal::SendKeystroke",
+ "ctrl-c"
+ ]
}
+ }
]
@@ -1,325 +1,325 @@
[
- {
- "context": "Editor && VimControl && !VimWaiting",
- "bindings": {
- "g": [
- "vim::PushOperator",
- {
- "Namespace": "G"
- }
- ],
- "i": [
- "vim::PushOperator",
- {
- "Object": {
- "around": false
- }
- }
- ],
- "a": [
- "vim::PushOperator",
- {
- "Object": {
- "around": true
- }
- }
- ],
- "h": "vim::Left",
- "backspace": "vim::Backspace",
- "j": "vim::Down",
- "enter": "vim::NextLineStart",
- "k": "vim::Up",
- "l": "vim::Right",
- "$": "vim::EndOfLine",
- "shift-g": "vim::EndOfDocument",
- "w": "vim::NextWordStart",
- "shift-w": [
- "vim::NextWordStart",
- {
- "ignorePunctuation": true
- }
- ],
- "e": "vim::NextWordEnd",
- "shift-e": [
- "vim::NextWordEnd",
- {
- "ignorePunctuation": true
- }
- ],
- "b": "vim::PreviousWordStart",
- "shift-b": [
- "vim::PreviousWordStart",
- {
- "ignorePunctuation": true
- }
- ],
- "%": "vim::Matching",
- "ctrl-y": [
- "vim::Scroll",
- "LineUp"
- ],
- "f": [
- "vim::PushOperator",
- {
- "FindForward": {
- "before": false
- }
- }
- ],
- "t": [
- "vim::PushOperator",
- {
- "FindForward": {
- "before": true
- }
- }
- ],
- "shift-f": [
- "vim::PushOperator",
- {
- "FindBackward": {
- "after": false
- }
- }
- ],
- "shift-t": [
- "vim::PushOperator",
- {
- "FindBackward": {
- "after": true
- }
- }
- ],
- "escape": "editor::Cancel",
- "0": "vim::StartOfLine", // When no number operator present, use start of line motion
- "1": [
- "vim::Number",
- 1
- ],
- "2": [
- "vim::Number",
- 2
- ],
- "3": [
- "vim::Number",
- 3
- ],
- "4": [
- "vim::Number",
- 4
- ],
- "5": [
- "vim::Number",
- 5
- ],
- "6": [
- "vim::Number",
- 6
- ],
- "7": [
- "vim::Number",
- 7
- ],
- "8": [
- "vim::Number",
- 8
- ],
- "9": [
- "vim::Number",
- 9
- ]
+ {
+ "context": "Editor && VimControl && !VimWaiting",
+ "bindings": {
+ "g": [
+ "vim::PushOperator",
+ {
+ "Namespace": "G"
}
- },
- {
- "context": "Editor && vim_mode == normal && vim_operator == none && !VimWaiting",
- "bindings": {
- "c": [
- "vim::PushOperator",
- "Change"
- ],
- "shift-c": "vim::ChangeToEndOfLine",
- "d": [
- "vim::PushOperator",
- "Delete"
- ],
- "shift-d": "vim::DeleteToEndOfLine",
- "y": [
- "vim::PushOperator",
- "Yank"
- ],
- "z": [
- "vim::PushOperator",
- {
- "Namespace": "Z"
- }
- ],
- "i": [
- "vim::SwitchMode",
- "Insert"
- ],
- "shift-i": "vim::InsertFirstNonWhitespace",
- "a": "vim::InsertAfter",
- "shift-a": "vim::InsertEndOfLine",
- "x": "vim::DeleteRight",
- "shift-x": "vim::DeleteLeft",
- "^": "vim::FirstNonWhitespace",
- "o": "vim::InsertLineBelow",
- "shift-o": "vim::InsertLineAbove",
- "v": [
- "vim::SwitchMode",
- {
- "Visual": {
- "line": false
- }
- }
- ],
- "shift-v": [
- "vim::SwitchMode",
- {
- "Visual": {
- "line": true
- }
- }
- ],
- "p": "vim::Paste",
- "u": "editor::Undo",
- "ctrl-r": "editor::Redo",
- "ctrl-o": "pane::GoBack",
- "/": [
- "buffer_search::Deploy",
- {
- "focus": true
- }
- ],
- "ctrl-f": [
- "vim::Scroll",
- "PageDown"
- ],
- "ctrl-b": [
- "vim::Scroll",
- "PageUp"
- ],
- "ctrl-d": [
- "vim::Scroll",
- "HalfPageDown"
- ],
- "ctrl-u": [
- "vim::Scroll",
- "HalfPageUp"
- ],
- "ctrl-e": [
- "vim::Scroll",
- "LineDown"
- ],
- "r": [
- "vim::PushOperator",
- "Replace"
- ]
+ ],
+ "i": [
+ "vim::PushOperator",
+ {
+ "Object": {
+ "around": false
+ }
}
- },
- {
- "context": "Editor && vim_operator == n",
- "bindings": {
- "0": [
- "vim::Number",
- 0
- ]
+ ],
+ "a": [
+ "vim::PushOperator",
+ {
+ "Object": {
+ "around": true
+ }
}
- },
- {
- "context": "Editor && vim_operator == g",
- "bindings": {
- "g": "vim::StartOfDocument",
- "h": "editor::Hover",
- "escape": [
- "vim::SwitchMode",
- "Normal"
- ],
- "d": "editor::GoToDefinition"
+ ],
+ "h": "vim::Left",
+ "backspace": "vim::Backspace",
+ "j": "vim::Down",
+ "enter": "vim::NextLineStart",
+ "k": "vim::Up",
+ "l": "vim::Right",
+ "$": "vim::EndOfLine",
+ "shift-g": "vim::EndOfDocument",
+ "w": "vim::NextWordStart",
+ "shift-w": [
+ "vim::NextWordStart",
+ {
+ "ignorePunctuation": true
}
- },
- {
- "context": "Editor && vim_operator == c",
- "bindings": {
- "c": "vim::CurrentLine"
+ ],
+ "e": "vim::NextWordEnd",
+ "shift-e": [
+ "vim::NextWordEnd",
+ {
+ "ignorePunctuation": true
}
- },
- {
- "context": "Editor && vim_operator == d",
- "bindings": {
- "d": "vim::CurrentLine"
+ ],
+ "b": "vim::PreviousWordStart",
+ "shift-b": [
+ "vim::PreviousWordStart",
+ {
+ "ignorePunctuation": true
}
- },
- {
- "context": "Editor && vim_operator == y",
- "bindings": {
- "y": "vim::CurrentLine"
+ ],
+ "%": "vim::Matching",
+ "ctrl-y": [
+ "vim::Scroll",
+ "LineUp"
+ ],
+ "f": [
+ "vim::PushOperator",
+ {
+ "FindForward": {
+ "before": false
+ }
}
- },
- {
- "context": "Editor && vim_operator == z",
- "bindings": {
- "t": "editor::ScrollCursorTop",
- "z": "editor::ScrollCursorCenter",
- "b": "editor::ScrollCursorBottom",
- "escape": [
- "vim::SwitchMode",
- "Normal"
- ]
+ ],
+ "t": [
+ "vim::PushOperator",
+ {
+ "FindForward": {
+ "before": true
+ }
}
- },
- {
- "context": "Editor && VimObject",
- "bindings": {
- "w": "vim::Word",
- "shift-w": [
- "vim::Word",
- {
- "ignorePunctuation": true
- }
- ],
- "s": "vim::Sentence",
- "'": "vim::Quotes",
- "`": "vim::BackQuotes",
- "\"": "vim::DoubleQuotes",
- "(": "vim::Parentheses",
- ")": "vim::Parentheses",
- "[": "vim::SquareBrackets",
- "]": "vim::SquareBrackets",
- "{": "vim::CurlyBrackets",
- "}": "vim::CurlyBrackets",
- "<": "vim::AngleBrackets",
- ">": "vim::AngleBrackets"
+ ],
+ "shift-f": [
+ "vim::PushOperator",
+ {
+ "FindBackward": {
+ "after": false
+ }
}
- },
- {
- "context": "Editor && vim_mode == visual && !VimWaiting",
- "bindings": {
- "u": "editor::Undo",
- "c": "vim::VisualChange",
- "d": "vim::VisualDelete",
- "x": "vim::VisualDelete",
- "y": "vim::VisualYank",
- "p": "vim::VisualPaste",
- "r": [
- "vim::PushOperator",
- "Replace"
- ]
+ ],
+ "shift-t": [
+ "vim::PushOperator",
+ {
+ "FindBackward": {
+ "after": true
+ }
}
- },
- {
- "context": "Editor && vim_mode == insert",
- "bindings": {
- "escape": "vim::NormalBefore",
- "ctrl-c": "vim::NormalBefore"
+ ],
+ "escape": "editor::Cancel",
+ "0": "vim::StartOfLine", // When no number operator present, use start of line motion
+ "1": [
+ "vim::Number",
+ 1
+ ],
+ "2": [
+ "vim::Number",
+ 2
+ ],
+ "3": [
+ "vim::Number",
+ 3
+ ],
+ "4": [
+ "vim::Number",
+ 4
+ ],
+ "5": [
+ "vim::Number",
+ 5
+ ],
+ "6": [
+ "vim::Number",
+ 6
+ ],
+ "7": [
+ "vim::Number",
+ 7
+ ],
+ "8": [
+ "vim::Number",
+ 8
+ ],
+ "9": [
+ "vim::Number",
+ 9
+ ]
+ }
+ },
+ {
+ "context": "Editor && vim_mode == normal && vim_operator == none && !VimWaiting",
+ "bindings": {
+ "c": [
+ "vim::PushOperator",
+ "Change"
+ ],
+ "shift-c": "vim::ChangeToEndOfLine",
+ "d": [
+ "vim::PushOperator",
+ "Delete"
+ ],
+ "shift-d": "vim::DeleteToEndOfLine",
+ "y": [
+ "vim::PushOperator",
+ "Yank"
+ ],
+ "z": [
+ "vim::PushOperator",
+ {
+ "Namespace": "Z"
+ }
+ ],
+ "i": [
+ "vim::SwitchMode",
+ "Insert"
+ ],
+ "shift-i": "vim::InsertFirstNonWhitespace",
+ "a": "vim::InsertAfter",
+ "shift-a": "vim::InsertEndOfLine",
+ "x": "vim::DeleteRight",
+ "shift-x": "vim::DeleteLeft",
+ "^": "vim::FirstNonWhitespace",
+ "o": "vim::InsertLineBelow",
+ "shift-o": "vim::InsertLineAbove",
+ "v": [
+ "vim::SwitchMode",
+ {
+ "Visual": {
+ "line": false
+ }
+ }
+ ],
+ "shift-v": [
+ "vim::SwitchMode",
+ {
+ "Visual": {
+ "line": true
+ }
+ }
+ ],
+ "p": "vim::Paste",
+ "u": "editor::Undo",
+ "ctrl-r": "editor::Redo",
+ "ctrl-o": "pane::GoBack",
+ "/": [
+ "buffer_search::Deploy",
+ {
+ "focus": true
}
- },
- {
- "context": "Editor && VimWaiting",
- "bindings": {
- "tab": "vim::Tab",
- "enter": "vim::Enter",
- "escape": "editor::Cancel"
+ ],
+ "ctrl-f": [
+ "vim::Scroll",
+ "PageDown"
+ ],
+ "ctrl-b": [
+ "vim::Scroll",
+ "PageUp"
+ ],
+ "ctrl-d": [
+ "vim::Scroll",
+ "HalfPageDown"
+ ],
+ "ctrl-u": [
+ "vim::Scroll",
+ "HalfPageUp"
+ ],
+ "ctrl-e": [
+ "vim::Scroll",
+ "LineDown"
+ ],
+ "r": [
+ "vim::PushOperator",
+ "Replace"
+ ]
+ }
+ },
+ {
+ "context": "Editor && vim_operator == n",
+ "bindings": {
+ "0": [
+ "vim::Number",
+ 0
+ ]
+ }
+ },
+ {
+ "context": "Editor && vim_operator == g",
+ "bindings": {
+ "g": "vim::StartOfDocument",
+ "h": "editor::Hover",
+ "escape": [
+ "vim::SwitchMode",
+ "Normal"
+ ],
+ "d": "editor::GoToDefinition"
+ }
+ },
+ {
+ "context": "Editor && vim_operator == c",
+ "bindings": {
+ "c": "vim::CurrentLine"
+ }
+ },
+ {
+ "context": "Editor && vim_operator == d",
+ "bindings": {
+ "d": "vim::CurrentLine"
+ }
+ },
+ {
+ "context": "Editor && vim_operator == y",
+ "bindings": {
+ "y": "vim::CurrentLine"
+ }
+ },
+ {
+ "context": "Editor && vim_operator == z",
+ "bindings": {
+ "t": "editor::ScrollCursorTop",
+ "z": "editor::ScrollCursorCenter",
+ "b": "editor::ScrollCursorBottom",
+ "escape": [
+ "vim::SwitchMode",
+ "Normal"
+ ]
+ }
+ },
+ {
+ "context": "Editor && VimObject",
+ "bindings": {
+ "w": "vim::Word",
+ "shift-w": [
+ "vim::Word",
+ {
+ "ignorePunctuation": true
}
+ ],
+ "s": "vim::Sentence",
+ "'": "vim::Quotes",
+ "`": "vim::BackQuotes",
+ "\"": "vim::DoubleQuotes",
+ "(": "vim::Parentheses",
+ ")": "vim::Parentheses",
+ "[": "vim::SquareBrackets",
+ "]": "vim::SquareBrackets",
+ "{": "vim::CurlyBrackets",
+ "}": "vim::CurlyBrackets",
+ "<": "vim::AngleBrackets",
+ ">": "vim::AngleBrackets"
+ }
+ },
+ {
+ "context": "Editor && vim_mode == visual && !VimWaiting",
+ "bindings": {
+ "u": "editor::Undo",
+ "c": "vim::VisualChange",
+ "d": "vim::VisualDelete",
+ "x": "vim::VisualDelete",
+ "y": "vim::VisualYank",
+ "p": "vim::VisualPaste",
+ "r": [
+ "vim::PushOperator",
+ "Replace"
+ ]
+ }
+ },
+ {
+ "context": "Editor && vim_mode == insert",
+ "bindings": {
+ "escape": "vim::NormalBefore",
+ "ctrl-c": "vim::NormalBefore"
+ }
+ },
+ {
+ "context": "Editor && VimWaiting",
+ "bindings": {
+ "tab": "vim::Tab",
+ "enter": "vim::Enter",
+ "escape": "editor::Cancel"
}
-]
+ }
+]
@@ -1,251 +1,257 @@
{
- // The name of the Zed theme to use for the UI
- "theme": "One Dark",
- // The name of a font to use for rendering text in the editor
- "buffer_font_family": "Zed Mono",
- // The OpenType features to enable for text in the editor.
- "buffer_font_features": {
- // Disable ligatures:
- // "calt": false
- },
- // The default font size for text in the editor
- "buffer_font_size": 15,
- // The factor to grow the active pane by. Defaults to 1.0
- // which gives the same size as all other panes.
- "active_pane_magnification": 1.0,
- // Enable / disable copilot integration.
- "enable_copilot_integration": true,
- // Controls whether copilot provides suggestion immediately
- // or waits for a `copilot::Toggle`
- "copilot": "on",
- // Whether to enable vim modes and key bindings
- "vim_mode": false,
- // Whether to show the informational hover box when moving the mouse
- // over symbols in the editor.
- "hover_popover_enabled": true,
- // Whether to confirm before quitting Zed.
- "confirm_quit": false,
- // Whether the cursor blinks in the editor.
- "cursor_blink": true,
- // Whether to pop the completions menu while typing in an editor without
- // explicitly requesting it.
- "show_completions_on_input": true,
- // Whether the screen sharing icon is shown in the os status bar.
- "show_call_status_icon": true,
- // Whether to use language servers to provide code intelligence.
- "enable_language_server": true,
- // When to automatically save edited buffers. This setting can
- // take four values.
- //
- // 1. Never automatically save:
- // "autosave": "off",
- // 2. Save when changing focus away from the Zed window:
- // "autosave": "on_window_change",
- // 3. Save when changing focus away from a specific buffer:
- // "autosave": "on_focus_change",
- // 4. Save when idle for a certain amount of time:
- // "autosave": { "after_delay": {"milliseconds": 500} },
- "autosave": "off",
- // Where to place the dock by default. This setting can take three
- // values:
- //
- // 1. Position the dock attached to the bottom of the workspace
- // "default_dock_anchor": "bottom"
- // 2. Position the dock to the right of the workspace like a side panel
- // "default_dock_anchor": "right"
- // 3. Position the dock full screen over the entire workspace"
- // "default_dock_anchor": "expanded"
- "default_dock_anchor": "bottom",
- // Whether or not to remove any trailing whitespace from lines of a buffer
- // before saving it.
- "remove_trailing_whitespace_on_save": true,
- // Whether or not to ensure there's a single newline at the end of a buffer
- // when saving it.
- "ensure_final_newline_on_save": true,
- // Whether or not to perform a buffer format before saving
- "format_on_save": "on",
- // How to perform a buffer format. This setting can take two values:
- //
- // 1. Format code using the current language server:
- // "format_on_save": "language_server"
- // 2. Format code using an external command:
- // "format_on_save": {
- // "external": {
- // "command": "prettier",
- // "arguments": ["--stdin-filepath", "{buffer_path}"]
- // }
+ // The name of the Zed theme to use for the UI
+ "theme": "One Dark",
+ // Features that can be globally enabled or disabled
+ "features": {
+ // Show Copilot icon in status bar
+ "copilot": true
+ },
+ // The name of a font to use for rendering text in the editor
+ "buffer_font_family": "Zed Mono",
+ // The OpenType features to enable for text in the editor.
+ "buffer_font_features": {
+ // Disable ligatures:
+ // "calt": false
+ },
+ // The default font size for text in the editor
+ "buffer_font_size": 15,
+ // The factor to grow the active pane by. Defaults to 1.0
+ // which gives the same size as all other panes.
+ "active_pane_magnification": 1.0,
+ // Whether to enable vim modes and key bindings
+ "vim_mode": false,
+ // Whether to show the informational hover box when moving the mouse
+ // over symbols in the editor.
+ "hover_popover_enabled": true,
+ // Whether to confirm before quitting Zed.
+ "confirm_quit": false,
+ // Whether the cursor blinks in the editor.
+ "cursor_blink": true,
+ // Whether to pop the completions menu while typing in an editor without
+ // explicitly requesting it.
+ "show_completions_on_input": true,
+ // Controls whether copilot provides suggestion immediately
+ // or waits for a `copilot::Toggle`
+ "show_copilot_suggestions": true,
+ // Whether the screen sharing icon is shown in the os status bar.
+ "show_call_status_icon": true,
+ // Whether to use language servers to provide code intelligence.
+ "enable_language_server": true,
+ // When to automatically save edited buffers. This setting can
+ // take four values.
+ //
+ // 1. Never automatically save:
+ // "autosave": "off",
+ // 2. Save when changing focus away from the Zed window:
+ // "autosave": "on_window_change",
+ // 3. Save when changing focus away from a specific buffer:
+ // "autosave": "on_focus_change",
+ // 4. Save when idle for a certain amount of time:
+ // "autosave": { "after_delay": {"milliseconds": 500} },
+ "autosave": "off",
+ // Where to place the dock by default. This setting can take three
+ // values:
+ //
+ // 1. Position the dock attached to the bottom of the workspace
+ // "default_dock_anchor": "bottom"
+ // 2. Position the dock to the right of the workspace like a side panel
+ // "default_dock_anchor": "right"
+ // 3. Position the dock full screen over the entire workspace"
+ // "default_dock_anchor": "expanded"
+ "default_dock_anchor": "bottom",
+ // Whether or not to remove any trailing whitespace from lines of a buffer
+ // before saving it.
+ "remove_trailing_whitespace_on_save": true,
+ // Whether or not to ensure there's a single newline at the end of a buffer
+ // when saving it.
+ "ensure_final_newline_on_save": true,
+ // Whether or not to perform a buffer format before saving
+ "format_on_save": "on",
+ // How to perform a buffer format. This setting can take two values:
+ //
+ // 1. Format code using the current language server:
+ // "format_on_save": "language_server"
+ // 2. Format code using an external command:
+ // "format_on_save": {
+ // "external": {
+ // "command": "prettier",
+ // "arguments": ["--stdin-filepath", "{buffer_path}"]
+ // }
+ // }
+ "formatter": "language_server",
+ // How to soft-wrap long lines of text. This setting can take
+ // three values:
+ //
+ // 1. Do not soft wrap.
+ // "soft_wrap": "none",
+ // 2. Soft wrap lines that overflow the editor:
+ // "soft_wrap": "editor_width",
+ // 3. Soft wrap lines at the preferred line length
+ // "soft_wrap": "preferred_line_length",
+ "soft_wrap": "none",
+ // The column at which to soft-wrap lines, for buffers where soft-wrap
+ // is enabled.
+ "preferred_line_length": 80,
+ // Whether to indent lines using tab characters, as opposed to multiple
+ // spaces.
+ "hard_tabs": false,
+ // How many columns a tab should occupy.
+ "tab_size": 4,
+ // Control what info is collected by Zed.
+ "telemetry": {
+ // Send debug info like crash reports.
+ "diagnostics": true,
+ // Send anonymized usage data like what languages you're using Zed with.
+ "metrics": true
+ },
+ // Automatically update Zed
+ "auto_update": true,
+ // Git gutter behavior configuration.
+ "git": {
+ // Control whether the git gutter is shown. May take 2 values:
+ // 1. Show the gutter
+ // "git_gutter": "tracked_files"
+ // 2. Hide the gutter
+ // "git_gutter": "hide"
+ "git_gutter": "tracked_files"
+ },
+ // Settings specific to journaling
+ "journal": {
+ // The path of the directory where journal entries are stored
+ "path": "~",
+ // What format to display the hours in
+ // May take 2 values:
+ // 1. hour12
+ // 2. hour24
+ "hour_format": "hour12"
+ },
+ // Settings specific to the terminal
+ "terminal": {
+ // What shell to use when opening a terminal. May take 3 values:
+ // 1. Use the system's default terminal configuration in /etc/passwd
+ // "shell": "system"
+ // 2. A program:
+ // "shell": {
+ // "program": "sh"
+ // }
+ // 3. A program with arguments:
+ // "shell": {
+ // "with_arguments": {
+ // "program": "/bin/bash",
+ // "arguments": ["--login"]
+ // }
// }
- "formatter": "language_server",
- // How to soft-wrap long lines of text. This setting can take
- // three values:
+ "shell": "system",
+ // What working directory to use when launching the terminal.
+ // May take 4 values:
+ // 1. Use the current file's project directory. Will Fallback to the
+ // first project directory strategy if unsuccessful
+ // "working_directory": "current_project_directory"
+ // 2. Use the first project in this workspace's directory
+ // "working_directory": "first_project_directory"
+ // 3. Always use this platform's home directory (if we can find it)
+ // "working_directory": "always_home"
+ // 4. Always use a specific directory. This value will be shell expanded.
+ // If this path is not a valid directory the terminal will default to
+ // this platform's home directory (if we can find it)
+ // "working_directory": {
+ // "always": {
+ // "directory": "~/zed/projects/"
+ // }
+ // }
+ //
//
- // 1. Do not soft wrap.
- // "soft_wrap": "none",
- // 2. Soft wrap lines that overflow the editor:
- // "soft_wrap": "editor_width",
- // 3. Soft wrap lines at the preferred line length
- // "soft_wrap": "preferred_line_length",
- "soft_wrap": "none",
- // The column at which to soft-wrap lines, for buffers where soft-wrap
- // is enabled.
- "preferred_line_length": 80,
- // Whether to indent lines using tab characters, as opposed to multiple
- // spaces.
- "hard_tabs": false,
- // How many columns a tab should occupy.
- "tab_size": 4,
- // Control what info is collected by Zed.
- "telemetry": {
- // Send debug info like crash reports.
- "diagnostics": true,
- // Send anonymized usage data like what languages you're using Zed with.
- "metrics": true
+ "working_directory": "current_project_directory",
+ // Set the cursor blinking behavior in the terminal.
+ // May take 4 values:
+ // 1. Never blink the cursor, ignoring the terminal mode
+ // "blinking": "off",
+ // 2. Default the cursor blink to off, but allow the terminal to
+ // set blinking
+ // "blinking": "terminal_controlled",
+ // 3. Always blink the cursor, ignoring the terminal mode
+ // "blinking": "on",
+ "blinking": "terminal_controlled",
+ // Set whether Alternate Scroll mode (code: ?1007) is active by default.
+ // Alternate Scroll mode converts mouse scroll events into up / down key
+ // presses when in the alternate screen (e.g. when running applications
+ // like vim or less). The terminal can still set and unset this mode.
+ // May take 2 values:
+ // 1. Default alternate scroll mode to on
+ // "alternate_scroll": "on",
+ // 2. Default alternate scroll mode to off
+ // "alternate_scroll": "off",
+ "alternate_scroll": "off",
+ // Set whether the option key behaves as the meta key.
+ // May take 2 values:
+ // 1. Rely on default platform handling of option key, on macOS
+ // this means generating certain unicode characters
+ // "option_to_meta": false,
+ // 2. Make the option keys behave as a 'meta' key, e.g. for emacs
+ // "option_to_meta": true,
+ "option_as_meta": false,
+ // Whether or not selecting text in the terminal will automatically
+ // copy to the system clipboard.
+ "copy_on_select": false,
+ // Any key-value pairs added to this list will be added to the terminal's
+ // enviroment. Use `:` to seperate multiple values.
+ "env": {
+ // "KEY": "value1:value2"
+ }
+ // Set the terminal's font size. If this option is not included,
+ // the terminal will default to matching the buffer's font size.
+ // "font_size": "15"
+ // Set the terminal's font family. If this option is not included,
+ // the terminal will default to matching the buffer's font family.
+ // "font_family": "Zed Mono"
+ },
+ // Different settings for specific languages.
+ "languages": {
+ "Plain Text": {
+ "soft_wrap": "preferred_line_length"
+ },
+ "Elixir": {
+ "tab_size": 2
+ },
+ "Go": {
+ "tab_size": 4,
+ "hard_tabs": true
},
- // Automatically update Zed
- "auto_update": true,
- // Git gutter behavior configuration.
- "git": {
- // Control whether the git gutter is shown. May take 2 values:
- // 1. Show the gutter
- // "git_gutter": "tracked_files"
- // 2. Hide the gutter
- // "git_gutter": "hide"
- "git_gutter": "tracked_files"
+ "Markdown": {
+ "soft_wrap": "preferred_line_length"
},
- // Settings specific to journaling
- "journal": {
- // The path of the directory where journal entries are stored
- "path": "~",
- // What format to display the hours in
- // May take 2 values:
- // 1. hour12
- // 2. hour24
- "hour_format": "hour12"
+ "JavaScript": {
+ "tab_size": 2
},
- // Settings specific to the terminal
- "terminal": {
- // What shell to use when opening a terminal. May take 3 values:
- // 1. Use the system's default terminal configuration in /etc/passwd
- // "shell": "system"
- // 2. A program:
- // "shell": {
- // "program": "sh"
- // }
- // 3. A program with arguments:
- // "shell": {
- // "with_arguments": {
- // "program": "/bin/bash",
- // "arguments": ["--login"]
- // }
- // }
- "shell": "system",
- // What working directory to use when launching the terminal.
- // May take 4 values:
- // 1. Use the current file's project directory. Will Fallback to the
- // first project directory strategy if unsuccessful
- // "working_directory": "current_project_directory"
- // 2. Use the first project in this workspace's directory
- // "working_directory": "first_project_directory"
- // 3. Always use this platform's home directory (if we can find it)
- // "working_directory": "always_home"
- // 4. Always use a specific directory. This value will be shell expanded.
- // If this path is not a valid directory the terminal will default to
- // this platform's home directory (if we can find it)
- // "working_directory": {
- // "always": {
- // "directory": "~/zed/projects/"
- // }
- // }
- //
- //
- "working_directory": "current_project_directory",
- // Set the cursor blinking behavior in the terminal.
- // May take 4 values:
- // 1. Never blink the cursor, ignoring the terminal mode
- // "blinking": "off",
- // 2. Default the cursor blink to off, but allow the terminal to
- // set blinking
- // "blinking": "terminal_controlled",
- // 3. Always blink the cursor, ignoring the terminal mode
- // "blinking": "on",
- "blinking": "terminal_controlled",
- // Set whether Alternate Scroll mode (code: ?1007) is active by default.
- // Alternate Scroll mode converts mouse scroll events into up / down key
- // presses when in the alternate screen (e.g. when running applications
- // like vim or less). The terminal can still set and unset this mode.
- // May take 2 values:
- // 1. Default alternate scroll mode to on
- // "alternate_scroll": "on",
- // 2. Default alternate scroll mode to off
- // "alternate_scroll": "off",
- "alternate_scroll": "off",
- // Set whether the option key behaves as the meta key.
- // May take 2 values:
- // 1. Rely on default platform handling of option key, on macOS
- // this means generating certain unicode characters
- // "option_to_meta": false,
- // 2. Make the option keys behave as a 'meta' key, e.g. for emacs
- // "option_to_meta": true,
- "option_as_meta": false,
- // Whether or not selecting text in the terminal will automatically
- // copy to the system clipboard.
- "copy_on_select": false,
- // Any key-value pairs added to this list will be added to the terminal's
- // enviroment. Use `:` to seperate multiple values.
- "env": {
- // "KEY": "value1:value2"
- }
- // Set the terminal's font size. If this option is not included,
- // the terminal will default to matching the buffer's font size.
- // "font_size": "15"
- // Set the terminal's font family. If this option is not included,
- // the terminal will default to matching the buffer's font family.
- // "font_family": "Zed Mono"
+ "TypeScript": {
+ "tab_size": 2
},
- // Different settings for specific languages.
- "languages": {
- "Plain Text": {
- "soft_wrap": "preferred_line_length"
- },
- "Elixir": {
- "tab_size": 2
- },
- "Go": {
- "tab_size": 4,
- "hard_tabs": true
- },
- "Markdown": {
- "soft_wrap": "preferred_line_length"
- },
- "JavaScript": {
- "tab_size": 2
- },
- "TypeScript": {
- "tab_size": 2
- },
- "TSX": {
- "tab_size": 2
- },
- "YAML": {
- "tab_size": 2
- }
+ "TSX": {
+ "tab_size": 2
},
- // LSP Specific settings.
- "lsp": {
- // Specify the LSP name as a key here.
- // As of 8/10/22, supported LSPs are:
- // pyright
- // gopls
- // rust-analyzer
- // typescript-language-server
- // vscode-json-languageserver
- // "rust-analyzer": {
- // //These initialization options are merged into Zed's defaults
- // "initialization_options": {
- // "checkOnSave": {
- // "command": "clippy"
- // }
- // }
- // }
+ "YAML": {
+ "tab_size": 2
+ },
+ "JSON": {
+ "tab_size": 2
}
+ },
+ // LSP Specific settings.
+ "lsp": {
+ // Specify the LSP name as a key here.
+ // As of 8/10/22, supported LSPs are:
+ // pyright
+ // gopls
+ // rust-analyzer
+ // typescript-language-server
+ // vscode-json-languageserver
+ // "rust-analyzer": {
+ // //These initialization options are merged into Zed's defaults
+ // "initialization_options": {
+ // "checkOnSave": {
+ // "command": "clippy"
+ // }
+ // }
+ // }
+ }
}
@@ -7,5 +7,5 @@
// custom settings, run the `open default settings` command
// from the command palette or from `Zed` application menu.
{
- "buffer_font_size": 15
+ "buffer_font_size": 15
}
@@ -55,6 +55,7 @@ toml = "0.5.8"
tracing = "0.1.34"
tracing-log = "0.1.3"
tracing-subscriber = { version = "0.3.11", features = ["env-filter", "json"] }
+indoc = "1.0.4"
[dev-dependencies]
collections = { path = "../collections", features = ["test-support"] }
@@ -6,8 +6,9 @@ use call::{room, ActiveCall, ParticipantLocation, Room};
use client::{User, RECEIVE_TIMEOUT};
use collections::HashSet;
use editor::{
- ConfirmCodeAction, ConfirmCompletion, ConfirmRename, Editor, ExcerptRange, MultiBuffer, Redo,
- Rename, ToOffset, ToggleCodeActions, Undo,
+ test::editor_test_context::EditorTestContext, ConfirmCodeAction, ConfirmCompletion,
+ ConfirmRename, Editor, ExcerptRange, MultiBuffer, Redo, Rename, ToOffset, ToggleCodeActions,
+ Undo,
};
use fs::{FakeFs, Fs as _, LineEnding, RemoveOptions};
use futures::StreamExt as _;
@@ -15,6 +16,7 @@ use gpui::{
executor::Deterministic, geometry::vector::vec2f, test::EmptyView, ModelHandle, TestAppContext,
ViewHandle,
};
+use indoc::indoc;
use language::{
tree_sitter_rust, Anchor, Diagnostic, DiagnosticEntry, FakeLspAdapter, Language,
LanguageConfig, OffsetRangeExt, Point, Rope,
@@ -3040,6 +3042,104 @@ async fn test_editing_while_guest_opens_buffer(
buffer_b.read_with(cx_b, |buf, _| assert_eq!(buf.text(), text));
}
+#[gpui::test]
+async fn test_newline_above_or_below_does_not_move_guest_cursor(
+ deterministic: Arc<Deterministic>,
+ cx_a: &mut TestAppContext,
+ cx_b: &mut TestAppContext,
+) {
+ deterministic.forbid_parking();
+ let mut server = TestServer::start(&deterministic).await;
+ let client_a = server.create_client(cx_a, "user_a").await;
+ let client_b = server.create_client(cx_b, "user_b").await;
+ server
+ .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
+ .await;
+ let active_call_a = cx_a.read(ActiveCall::global);
+
+ client_a
+ .fs
+ .insert_tree("/dir", json!({ "a.txt": "Some text\n" }))
+ .await;
+ let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
+ let project_id = active_call_a
+ .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
+ .await
+ .unwrap();
+
+ let project_b = client_b.build_remote_project(project_id, cx_b).await;
+
+ // Open a buffer as client A
+ let buffer_a = project_a
+ .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
+ .await
+ .unwrap();
+ let (_, window_a) = cx_a.add_window(|_| EmptyView);
+ let editor_a = cx_a.add_view(&window_a, |cx| {
+ Editor::for_buffer(buffer_a, Some(project_a), cx)
+ });
+ let mut editor_cx_a = EditorTestContext {
+ cx: cx_a,
+ window_id: window_a.id(),
+ editor: editor_a,
+ };
+
+ // Open a buffer as client B
+ let buffer_b = project_b
+ .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
+ .await
+ .unwrap();
+ let (_, window_b) = cx_b.add_window(|_| EmptyView);
+ let editor_b = cx_b.add_view(&window_b, |cx| {
+ Editor::for_buffer(buffer_b, Some(project_b), cx)
+ });
+ let mut editor_cx_b = EditorTestContext {
+ cx: cx_b,
+ window_id: window_b.id(),
+ editor: editor_b,
+ };
+
+ // Test newline above
+ editor_cx_a.set_selections_state(indoc! {"
+ Some textˇ
+ "});
+ editor_cx_b.set_selections_state(indoc! {"
+ Some textˇ
+ "});
+ editor_cx_a.update_editor(|editor, cx| editor.newline_above(&editor::NewlineAbove, cx));
+ deterministic.run_until_parked();
+ editor_cx_a.assert_editor_state(indoc! {"
+ ˇ
+ Some text
+ "});
+ editor_cx_b.assert_editor_state(indoc! {"
+
+ Some textˇ
+ "});
+
+ // Test newline below
+ editor_cx_a.set_selections_state(indoc! {"
+
+ Some textˇ
+ "});
+ editor_cx_b.set_selections_state(indoc! {"
+
+ Some textˇ
+ "});
+ editor_cx_a.update_editor(|editor, cx| editor.newline_below(&editor::NewlineBelow, cx));
+ deterministic.run_until_parked();
+ editor_cx_a.assert_editor_state(indoc! {"
+
+ Some text
+ ˇ
+ "});
+ editor_cx_b.assert_editor_state(indoc! {"
+
+ Some textˇ
+
+ "});
+}
+
#[gpui::test(iterations = 10)]
async fn test_leaving_worktree_while_opening_buffer(
deterministic: Arc<Deterministic>,
@@ -5860,10 +5960,17 @@ async fn test_basic_following(
// Client A updates their selections in those editors
editor_a1.update(cx_a, |editor, cx| {
- editor.change_selections(None, cx, |s| s.select_ranges([0..1]))
+ editor.handle_input("a", cx);
+ editor.handle_input("b", cx);
+ editor.handle_input("c", cx);
+ editor.select_left(&Default::default(), cx);
+ assert_eq!(editor.selections.ranges(cx), vec![3..2]);
});
editor_a2.update(cx_a, |editor, cx| {
- editor.change_selections(None, cx, |s| s.select_ranges([2..3]))
+ editor.handle_input("d", cx);
+ editor.handle_input("e", cx);
+ editor.select_left(&Default::default(), cx);
+ assert_eq!(editor.selections.ranges(cx), vec![2..1]);
});
// When client B starts following client A, all visible view states are replicated to client B.
@@ -5876,6 +5983,27 @@ async fn test_basic_following(
.await
.unwrap();
+ cx_c.foreground().run_until_parked();
+ let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| {
+ workspace
+ .active_item(cx)
+ .unwrap()
+ .downcast::<Editor>()
+ .unwrap()
+ });
+ assert_eq!(
+ cx_b.read(|cx| editor_b2.project_path(cx)),
+ Some((worktree_id, "2.txt").into())
+ );
+ assert_eq!(
+ editor_b2.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
+ vec![2..1]
+ );
+ assert_eq!(
+ editor_b1.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
+ vec![3..2]
+ );
+
cx_c.foreground().run_until_parked();
let active_call_c = cx_c.read(ActiveCall::global);
let project_c = client_c.build_remote_project(project_id, cx_c).await;
@@ -6031,26 +6159,6 @@ async fn test_basic_following(
});
}
- let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| {
- workspace
- .active_item(cx)
- .unwrap()
- .downcast::<Editor>()
- .unwrap()
- });
- assert_eq!(
- cx_b.read(|cx| editor_b2.project_path(cx)),
- Some((worktree_id, "2.txt").into())
- );
- assert_eq!(
- editor_b2.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
- vec![2..3]
- );
- assert_eq!(
- editor_b1.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
- vec![0..1]
- );
-
// When client A activates a different editor, client B does so as well.
workspace_a.update(cx_a, |workspace, cx| {
workspace.activate_item(&editor_a1, cx)
@@ -38,10 +38,13 @@ smol = "1.2.5"
futures = "0.3"
[dev-dependencies]
+clock = { path = "../clock" }
collections = { path = "../collections", features = ["test-support"] }
+fs = { path = "../fs", features = ["test-support"] }
gpui = { path = "../gpui", features = ["test-support"] }
language = { path = "../language", features = ["test-support"] }
-settings = { path = "../settings", features = ["test-support"] }
lsp = { path = "../lsp", features = ["test-support"] }
+rpc = { path = "../rpc", features = ["test-support"] }
+settings = { path = "../settings", features = ["test-support"] }
util = { path = "../util", features = ["test-support"] }
workspace = { path = "../workspace", features = ["test-support"] }
@@ -5,9 +5,14 @@ use anyhow::{anyhow, Context, Result};
use async_compression::futures::bufread::GzipDecoder;
use async_tar::Archive;
use collections::HashMap;
-use futures::{future::Shared, Future, FutureExt, TryFutureExt};
-use gpui::{actions, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task};
-use language::{point_from_lsp, point_to_lsp, Anchor, Bias, Buffer, Language, ToPointUtf16};
+use futures::{channel::oneshot, future::Shared, Future, FutureExt, TryFutureExt};
+use gpui::{
+ actions, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task, WeakModelHandle,
+};
+use language::{
+ point_from_lsp, point_to_lsp, Anchor, Bias, Buffer, BufferSnapshot, Language, PointUtf16,
+ ToPointUtf16,
+};
use log::{debug, error};
use lsp::LanguageServer;
use node_runtime::NodeRuntime;
@@ -16,6 +21,7 @@ use settings::Settings;
use smol::{fs, io::BufReader, stream::StreamExt};
use std::{
ffi::OsString,
+ mem,
ops::Range,
path::{Path, PathBuf},
sync::Arc,
@@ -29,7 +35,10 @@ const COPILOT_AUTH_NAMESPACE: &'static str = "copilot_auth";
actions!(copilot_auth, [SignIn, SignOut]);
const COPILOT_NAMESPACE: &'static str = "copilot";
-actions!(copilot, [NextSuggestion, PreviousSuggestion, Reinstall]);
+actions!(
+ copilot,
+ [Suggest, NextSuggestion, PreviousSuggestion, Reinstall]
+);
pub fn init(http: Arc<dyn HttpClient>, node_runtime: Arc<NodeRuntime>, cx: &mut AppContext) {
// Disable Copilot for stable releases.
@@ -95,15 +104,38 @@ pub fn init(http: Arc<dyn HttpClient>, node_runtime: Arc<NodeRuntime>, cx: &mut
enum CopilotServer {
Disabled,
- Starting {
- task: Shared<Task<()>>,
- },
+ Starting { task: Shared<Task<()>> },
Error(Arc<str>),
- Started {
- server: Arc<LanguageServer>,
- status: SignInStatus,
- subscriptions_by_buffer_id: HashMap<usize, gpui::Subscription>,
- },
+ Running(RunningCopilotServer),
+}
+
+impl CopilotServer {
+ fn as_authenticated(&mut self) -> Result<&mut RunningCopilotServer> {
+ let server = self.as_running()?;
+ if matches!(server.sign_in_status, SignInStatus::Authorized { .. }) {
+ Ok(server)
+ } else {
+ Err(anyhow!("must sign in before using copilot"))
+ }
+ }
+
+ fn as_running(&mut self) -> Result<&mut RunningCopilotServer> {
+ match self {
+ CopilotServer::Starting { .. } => Err(anyhow!("copilot is still starting")),
+ CopilotServer::Disabled => Err(anyhow!("copilot is disabled")),
+ CopilotServer::Error(error) => Err(anyhow!(
+ "copilot was not started because of an error: {}",
+ error
+ )),
+ CopilotServer::Running(server) => Ok(server),
+ }
+ }
+}
+
+struct RunningCopilotServer {
+ lsp: Arc<LanguageServer>,
+ sign_in_status: SignInStatus,
+ registered_buffers: HashMap<usize, RegisteredBuffer>,
}
#[derive(Clone, Debug)]
@@ -138,8 +170,104 @@ impl Status {
}
}
-#[derive(Debug, PartialEq, Eq)]
+struct RegisteredBuffer {
+ id: usize,
+ uri: lsp::Url,
+ language_id: String,
+ snapshot: BufferSnapshot,
+ snapshot_version: i32,
+ _subscriptions: [gpui::Subscription; 2],
+ pending_buffer_change: Task<Option<()>>,
+}
+
+impl RegisteredBuffer {
+ fn report_changes(
+ &mut self,
+ buffer: &ModelHandle<Buffer>,
+ cx: &mut ModelContext<Copilot>,
+ ) -> oneshot::Receiver<(i32, BufferSnapshot)> {
+ let id = self.id;
+ let (done_tx, done_rx) = oneshot::channel();
+
+ if buffer.read(cx).version() == self.snapshot.version {
+ let _ = done_tx.send((self.snapshot_version, self.snapshot.clone()));
+ } else {
+ let buffer = buffer.downgrade();
+ let prev_pending_change =
+ mem::replace(&mut self.pending_buffer_change, Task::ready(None));
+ self.pending_buffer_change = cx.spawn_weak(|copilot, mut cx| async move {
+ prev_pending_change.await;
+
+ let old_version = copilot.upgrade(&cx)?.update(&mut cx, |copilot, _| {
+ let server = copilot.server.as_authenticated().log_err()?;
+ let buffer = server.registered_buffers.get_mut(&id)?;
+ Some(buffer.snapshot.version.clone())
+ })?;
+ let new_snapshot = buffer
+ .upgrade(&cx)?
+ .read_with(&cx, |buffer, _| buffer.snapshot());
+
+ let content_changes = cx
+ .background()
+ .spawn({
+ let new_snapshot = new_snapshot.clone();
+ async move {
+ new_snapshot
+ .edits_since::<(PointUtf16, usize)>(&old_version)
+ .map(|edit| {
+ let edit_start = edit.new.start.0;
+ let edit_end = edit_start + (edit.old.end.0 - edit.old.start.0);
+ let new_text = new_snapshot
+ .text_for_range(edit.new.start.1..edit.new.end.1)
+ .collect();
+ lsp::TextDocumentContentChangeEvent {
+ range: Some(lsp::Range::new(
+ point_to_lsp(edit_start),
+ point_to_lsp(edit_end),
+ )),
+ range_length: None,
+ text: new_text,
+ }
+ })
+ .collect::<Vec<_>>()
+ }
+ })
+ .await;
+
+ copilot.upgrade(&cx)?.update(&mut cx, |copilot, _| {
+ let server = copilot.server.as_authenticated().log_err()?;
+ let buffer = server.registered_buffers.get_mut(&id)?;
+ if !content_changes.is_empty() {
+ buffer.snapshot_version += 1;
+ buffer.snapshot = new_snapshot;
+ server
+ .lsp
+ .notify::<lsp::notification::DidChangeTextDocument>(
+ lsp::DidChangeTextDocumentParams {
+ text_document: lsp::VersionedTextDocumentIdentifier::new(
+ buffer.uri.clone(),
+ buffer.snapshot_version,
+ ),
+ content_changes,
+ },
+ )
+ .log_err();
+ }
+ let _ = done_tx.send((buffer.snapshot_version, buffer.snapshot.clone()));
+ Some(())
+ })?;
+
+ Some(())
+ });
+ }
+
+ done_rx
+ }
+}
+
+#[derive(Debug)]
pub struct Completion {
+ uuid: String,
pub range: Range<Anchor>,
pub text: String,
}
@@ -148,6 +276,7 @@ pub struct Copilot {
http: Arc<dyn HttpClient>,
node_runtime: Arc<NodeRuntime>,
server: CopilotServer,
+ buffers: HashMap<usize, WeakModelHandle<Buffer>>,
}
impl Entity for Copilot {
@@ -172,7 +301,7 @@ impl Copilot {
let http = http.clone();
let node_runtime = node_runtime.clone();
move |this, cx| {
- if cx.global::<Settings>().enable_copilot_integration {
+ if cx.global::<Settings>().features.copilot {
if matches!(this.server, CopilotServer::Disabled) {
let start_task = cx
.spawn({
@@ -194,12 +323,14 @@ impl Copilot {
})
.detach();
- if cx.global::<Settings>().enable_copilot_integration {
+ if cx.global::<Settings>().features.copilot {
let start_task = cx
.spawn({
let http = http.clone();
let node_runtime = node_runtime.clone();
- move |this, cx| Self::start_language_server(http, node_runtime, this, cx)
+ move |this, cx| async {
+ Self::start_language_server(http, node_runtime, this, cx).await
+ }
})
.shared();
@@ -207,12 +338,14 @@ impl Copilot {
http,
node_runtime,
server: CopilotServer::Starting { task: start_task },
+ buffers: Default::default(),
}
} else {
Self {
http,
node_runtime,
server: CopilotServer::Disabled,
+ buffers: Default::default(),
}
}
}
@@ -225,11 +358,12 @@ impl Copilot {
let this = cx.add_model(|cx| Self {
http: http.clone(),
node_runtime: NodeRuntime::new(http, cx.background().clone()),
- server: CopilotServer::Started {
- server: Arc::new(server),
- status: SignInStatus::Authorized,
- subscriptions_by_buffer_id: Default::default(),
- },
+ server: CopilotServer::Running(RunningCopilotServer {
+ lsp: Arc::new(server),
+ sign_in_status: SignInStatus::Authorized,
+ registered_buffers: Default::default(),
+ }),
+ buffers: Default::default(),
});
(this, fake_server)
}
@@ -281,6 +415,19 @@ impl Copilot {
)
.detach();
+ server
+ .request::<request::SetEditorInfo>(request::SetEditorInfoParams {
+ editor_info: request::EditorInfo {
+ name: "zed".into(),
+ version: env!("CARGO_PKG_VERSION").into(),
+ },
+ editor_plugin_info: request::EditorPluginInfo {
+ name: "zed-copilot".into(),
+ version: "0.0.1".into(),
+ },
+ })
+ .await?;
+
anyhow::Ok((server, status))
};
@@ -289,11 +436,11 @@ impl Copilot {
cx.notify();
match server {
Ok((server, status)) => {
- this.server = CopilotServer::Started {
- server,
- status: SignInStatus::SignedOut,
- subscriptions_by_buffer_id: Default::default(),
- };
+ this.server = CopilotServer::Running(RunningCopilotServer {
+ lsp: server,
+ sign_in_status: SignInStatus::SignedOut,
+ registered_buffers: Default::default(),
+ });
this.update_sign_in_status(status, cx);
}
Err(error) => {
@@ -306,8 +453,8 @@ impl Copilot {
}
fn sign_in(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
- if let CopilotServer::Started { server, status, .. } = &mut self.server {
- let task = match status {
+ if let CopilotServer::Running(server) = &mut self.server {
+ let task = match &server.sign_in_status {
SignInStatus::Authorized { .. } | SignInStatus::Unauthorized { .. } => {
Task::ready(Ok(())).shared()
}
@@ -316,11 +463,11 @@ impl Copilot {
task.clone()
}
SignInStatus::SignedOut => {
- let server = server.clone();
+ let lsp = server.lsp.clone();
let task = cx
.spawn(|this, mut cx| async move {
let sign_in = async {
- let sign_in = server
+ let sign_in = lsp
.request::<request::SignInInitiate>(
request::SignInInitiateParams {},
)
@@ -331,8 +478,10 @@ impl Copilot {
}
request::SignInInitiateResult::PromptUserDeviceFlow(flow) => {
this.update(&mut cx, |this, cx| {
- if let CopilotServer::Started { status, .. } =
- &mut this.server
+ if let CopilotServer::Running(RunningCopilotServer {
+ sign_in_status: status,
+ ..
+ }) = &mut this.server
{
if let SignInStatus::SigningIn {
prompt: prompt_flow,
@@ -344,7 +493,7 @@ impl Copilot {
}
}
});
- let response = server
+ let response = lsp
.request::<request::SignInConfirm>(
request::SignInConfirmParams {
user_code: flow.user_code,
@@ -372,7 +521,7 @@ impl Copilot {
})
})
.shared();
- *status = SignInStatus::SigningIn {
+ server.sign_in_status = SignInStatus::SigningIn {
prompt: None,
task: task.clone(),
};
@@ -391,10 +540,8 @@ impl Copilot {
}
fn sign_out(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
- if let CopilotServer::Started { server, status, .. } = &mut self.server {
- *status = SignInStatus::SignedOut;
- cx.notify();
-
+ self.update_sign_in_status(request::SignInStatus::NotSignedIn, cx);
+ if let CopilotServer::Running(RunningCopilotServer { lsp: server, .. }) = &self.server {
let server = server.clone();
cx.background().spawn(async move {
server
@@ -428,6 +575,135 @@ impl Copilot {
cx.foreground().spawn(start_task)
}
+ pub fn register_buffer(&mut self, buffer: &ModelHandle<Buffer>, cx: &mut ModelContext<Self>) {
+ let buffer_id = buffer.id();
+ self.buffers.insert(buffer_id, buffer.downgrade());
+
+ if let CopilotServer::Running(RunningCopilotServer {
+ lsp: server,
+ sign_in_status: status,
+ registered_buffers,
+ ..
+ }) = &mut self.server
+ {
+ if !matches!(status, SignInStatus::Authorized { .. }) {
+ return;
+ }
+
+ registered_buffers.entry(buffer.id()).or_insert_with(|| {
+ let uri: lsp::Url = uri_for_buffer(buffer, cx);
+ let language_id = id_for_language(buffer.read(cx).language());
+ let snapshot = buffer.read(cx).snapshot();
+ server
+ .notify::<lsp::notification::DidOpenTextDocument>(
+ lsp::DidOpenTextDocumentParams {
+ text_document: lsp::TextDocumentItem {
+ uri: uri.clone(),
+ language_id: language_id.clone(),
+ version: 0,
+ text: snapshot.text(),
+ },
+ },
+ )
+ .log_err();
+
+ RegisteredBuffer {
+ id: buffer_id,
+ uri,
+ language_id,
+ snapshot,
+ snapshot_version: 0,
+ pending_buffer_change: Task::ready(Some(())),
+ _subscriptions: [
+ cx.subscribe(buffer, |this, buffer, event, cx| {
+ this.handle_buffer_event(buffer, event, cx).log_err();
+ }),
+ cx.observe_release(buffer, move |this, _buffer, _cx| {
+ this.buffers.remove(&buffer_id);
+ this.unregister_buffer(buffer_id);
+ }),
+ ],
+ }
+ });
+ }
+ }
+
+ fn handle_buffer_event(
+ &mut self,
+ buffer: ModelHandle<Buffer>,
+ event: &language::Event,
+ cx: &mut ModelContext<Self>,
+ ) -> Result<()> {
+ if let Ok(server) = self.server.as_running() {
+ if let Some(registered_buffer) = server.registered_buffers.get_mut(&buffer.id()) {
+ match event {
+ language::Event::Edited => {
+ let _ = registered_buffer.report_changes(&buffer, cx);
+ }
+ language::Event::Saved => {
+ server
+ .lsp
+ .notify::<lsp::notification::DidSaveTextDocument>(
+ lsp::DidSaveTextDocumentParams {
+ text_document: lsp::TextDocumentIdentifier::new(
+ registered_buffer.uri.clone(),
+ ),
+ text: None,
+ },
+ )?;
+ }
+ language::Event::FileHandleChanged | language::Event::LanguageChanged => {
+ let new_language_id = id_for_language(buffer.read(cx).language());
+ let new_uri = uri_for_buffer(&buffer, cx);
+ if new_uri != registered_buffer.uri
+ || new_language_id != registered_buffer.language_id
+ {
+ let old_uri = mem::replace(&mut registered_buffer.uri, new_uri);
+ registered_buffer.language_id = new_language_id;
+ server
+ .lsp
+ .notify::<lsp::notification::DidCloseTextDocument>(
+ lsp::DidCloseTextDocumentParams {
+ text_document: lsp::TextDocumentIdentifier::new(old_uri),
+ },
+ )?;
+ server
+ .lsp
+ .notify::<lsp::notification::DidOpenTextDocument>(
+ lsp::DidOpenTextDocumentParams {
+ text_document: lsp::TextDocumentItem::new(
+ registered_buffer.uri.clone(),
+ registered_buffer.language_id.clone(),
+ registered_buffer.snapshot_version,
+ registered_buffer.snapshot.text(),
+ ),
+ },
+ )?;
+ }
+ }
+ _ => {}
+ }
+ }
+ }
+
+ Ok(())
+ }
+
+ fn unregister_buffer(&mut self, buffer_id: usize) {
+ if let Ok(server) = self.server.as_running() {
+ if let Some(buffer) = server.registered_buffers.remove(&buffer_id) {
+ server
+ .lsp
+ .notify::<lsp::notification::DidCloseTextDocument>(
+ lsp::DidCloseTextDocumentParams {
+ text_document: lsp::TextDocumentIdentifier::new(buffer.uri),
+ },
+ )
+ .log_err();
+ }
+ }
+ }
+
pub fn completions<T>(
&mut self,
buffer: &ModelHandle<Buffer>,
@@ -452,6 +728,51 @@ impl Copilot {
self.request_completions::<request::GetCompletionsCycling, _>(buffer, position, cx)
}
+ pub fn accept_completion(
+ &mut self,
+ completion: &Completion,
+ cx: &mut ModelContext<Self>,
+ ) -> Task<Result<()>> {
+ let server = match self.server.as_authenticated() {
+ Ok(server) => server,
+ Err(error) => return Task::ready(Err(error)),
+ };
+ let request =
+ server
+ .lsp
+ .request::<request::NotifyAccepted>(request::NotifyAcceptedParams {
+ uuid: completion.uuid.clone(),
+ });
+ cx.background().spawn(async move {
+ request.await?;
+ Ok(())
+ })
+ }
+
+ pub fn discard_completions(
+ &mut self,
+ completions: &[Completion],
+ cx: &mut ModelContext<Self>,
+ ) -> Task<Result<()>> {
+ let server = match self.server.as_authenticated() {
+ Ok(server) => server,
+ Err(error) => return Task::ready(Err(error)),
+ };
+ let request =
+ server
+ .lsp
+ .request::<request::NotifyRejected>(request::NotifyRejectedParams {
+ uuids: completions
+ .iter()
+ .map(|completion| completion.uuid.clone())
+ .collect(),
+ });
+ cx.background().spawn(async move {
+ request.await?;
+ Ok(())
+ })
+ }
+
fn request_completions<R, T>(
&mut self,
buffer: &ModelHandle<Buffer>,
@@ -459,116 +780,48 @@ impl Copilot {
cx: &mut ModelContext<Self>,
) -> Task<Result<Vec<Completion>>>
where
- R: lsp::request::Request<
- Params = request::GetCompletionsParams,
- Result = request::GetCompletionsResult,
- >,
+ R: 'static
+ + lsp::request::Request<
+ Params = request::GetCompletionsParams,
+ Result = request::GetCompletionsResult,
+ >,
T: ToPointUtf16,
{
- let buffer_id = buffer.id();
- let uri: lsp::Url = format!("buffer://{}", buffer_id).parse().unwrap();
- let snapshot = buffer.read(cx).snapshot();
- let server = match &mut self.server {
- CopilotServer::Starting { .. } => {
- return Task::ready(Err(anyhow!("copilot is still starting")))
- }
- CopilotServer::Disabled => return Task::ready(Err(anyhow!("copilot is disabled"))),
- CopilotServer::Error(error) => {
- return Task::ready(Err(anyhow!(
- "copilot was not started because of an error: {}",
- error
- )))
- }
- CopilotServer::Started {
- server,
- status,
- subscriptions_by_buffer_id,
- } => {
- if matches!(status, SignInStatus::Authorized { .. }) {
- subscriptions_by_buffer_id
- .entry(buffer_id)
- .or_insert_with(|| {
- server
- .notify::<lsp::notification::DidOpenTextDocument>(
- lsp::DidOpenTextDocumentParams {
- text_document: lsp::TextDocumentItem {
- uri: uri.clone(),
- language_id: id_for_language(
- buffer.read(cx).language(),
- ),
- version: 0,
- text: snapshot.text(),
- },
- },
- )
- .log_err();
-
- let uri = uri.clone();
- cx.observe_release(buffer, move |this, _, _| {
- if let CopilotServer::Started {
- server,
- subscriptions_by_buffer_id,
- ..
- } = &mut this.server
- {
- server
- .notify::<lsp::notification::DidCloseTextDocument>(
- lsp::DidCloseTextDocumentParams {
- text_document: lsp::TextDocumentIdentifier::new(
- uri.clone(),
- ),
- },
- )
- .log_err();
- subscriptions_by_buffer_id.remove(&buffer_id);
- }
- })
- });
+ self.register_buffer(buffer, cx);
- server.clone()
- } else {
- return Task::ready(Err(anyhow!("must sign in before using copilot")));
- }
- }
+ let server = match self.server.as_authenticated() {
+ Ok(server) => server,
+ Err(error) => return Task::ready(Err(error)),
};
-
+ let lsp = server.lsp.clone();
+ let registered_buffer = server.registered_buffers.get_mut(&buffer.id()).unwrap();
+ let snapshot = registered_buffer.report_changes(buffer, cx);
+ let buffer = buffer.read(cx);
+ let uri = registered_buffer.uri.clone();
let settings = cx.global::<Settings>();
- let position = position.to_point_utf16(&snapshot);
- let language = snapshot.language_at(position);
+ let position = position.to_point_utf16(buffer);
+ let language = buffer.language_at(position);
let language_name = language.map(|language| language.name());
let language_name = language_name.as_deref();
let tab_size = settings.tab_size(language_name);
let hard_tabs = settings.hard_tabs(language_name);
- let language_id = id_for_language(language);
-
- let path;
- let relative_path;
- if let Some(file) = snapshot.file() {
- if let Some(file) = file.as_local() {
- path = file.abs_path(cx);
- } else {
- path = file.full_path(cx);
- }
- relative_path = file.path().to_path_buf();
- } else {
- path = PathBuf::new();
- relative_path = PathBuf::new();
- }
+ let relative_path = buffer
+ .file()
+ .map(|file| file.path().to_path_buf())
+ .unwrap_or_default();
- cx.background().spawn(async move {
- let result = server
+ cx.foreground().spawn(async move {
+ let (version, snapshot) = snapshot.await?;
+ let result = lsp
.request::<R>(request::GetCompletionsParams {
doc: request::GetCompletionsDocument {
- source: snapshot.text(),
+ uri,
tab_size: tab_size.into(),
indent_size: 1,
insert_spaces: !hard_tabs,
- uri,
- path: path.to_string_lossy().into(),
relative_path: relative_path.to_string_lossy().into(),
- language_id,
position: point_to_lsp(position),
- version: 0,
+ version: version.try_into().unwrap(),
},
})
.await?;
@@ -581,6 +834,7 @@ impl Copilot {
let end =
snapshot.clip_point_utf16(point_from_lsp(completion.range.end), Bias::Left);
Completion {
+ uuid: completion.uuid,
range: snapshot.anchor_before(start)..snapshot.anchor_after(end),
text: completion.text,
}
@@ -595,14 +849,16 @@ impl Copilot {
CopilotServer::Starting { task } => Status::Starting { task: task.clone() },
CopilotServer::Disabled => Status::Disabled,
CopilotServer::Error(error) => Status::Error(error.clone()),
- CopilotServer::Started { status, .. } => match status {
- SignInStatus::Authorized { .. } => Status::Authorized,
- SignInStatus::Unauthorized { .. } => Status::Unauthorized,
- SignInStatus::SigningIn { prompt, .. } => Status::SigningIn {
- prompt: prompt.clone(),
- },
- SignInStatus::SignedOut => Status::SignedOut,
- },
+ CopilotServer::Running(RunningCopilotServer { sign_in_status, .. }) => {
+ match sign_in_status {
+ SignInStatus::Authorized { .. } => Status::Authorized,
+ SignInStatus::Unauthorized { .. } => Status::Unauthorized,
+ SignInStatus::SigningIn { prompt, .. } => Status::SigningIn {
+ prompt: prompt.clone(),
+ },
+ SignInStatus::SignedOut => Status::SignedOut,
+ }
+ }
}
}
@@ -611,14 +867,34 @@ impl Copilot {
lsp_status: request::SignInStatus,
cx: &mut ModelContext<Self>,
) {
- if let CopilotServer::Started { status, .. } = &mut self.server {
- *status = match lsp_status {
+ self.buffers.retain(|_, buffer| buffer.is_upgradable(cx));
+
+ if let Ok(server) = self.server.as_running() {
+ match lsp_status {
request::SignInStatus::Ok { .. }
| request::SignInStatus::MaybeOk { .. }
- | request::SignInStatus::AlreadySignedIn { .. } => SignInStatus::Authorized,
- request::SignInStatus::NotAuthorized { .. } => SignInStatus::Unauthorized,
- request::SignInStatus::NotSignedIn => SignInStatus::SignedOut,
- };
+ | request::SignInStatus::AlreadySignedIn { .. } => {
+ server.sign_in_status = SignInStatus::Authorized;
+ for buffer in self.buffers.values().cloned().collect::<Vec<_>>() {
+ if let Some(buffer) = buffer.upgrade(cx) {
+ self.register_buffer(&buffer, cx);
+ }
+ }
+ }
+ request::SignInStatus::NotAuthorized { .. } => {
+ server.sign_in_status = SignInStatus::Unauthorized;
+ for buffer_id in self.buffers.keys().copied().collect::<Vec<_>>() {
+ self.unregister_buffer(buffer_id);
+ }
+ }
+ request::SignInStatus::NotSignedIn => {
+ server.sign_in_status = SignInStatus::SignedOut;
+ for buffer_id in self.buffers.keys().copied().collect::<Vec<_>>() {
+ self.unregister_buffer(buffer_id);
+ }
+ }
+ }
+
cx.notify();
}
}
@@ -633,6 +909,14 @@ fn id_for_language(language: Option<&Arc<Language>>) -> String {
}
}
+fn uri_for_buffer(buffer: &ModelHandle<Buffer>, cx: &AppContext) -> lsp::Url {
+ if let Some(file) = buffer.read(cx).file().and_then(|file| file.as_local()) {
+ lsp::Url::from_file_path(file.abs_path(cx)).unwrap()
+ } else {
+ format!("buffer://{}", buffer.id()).parse().unwrap()
+ }
+}
+
async fn clear_copilot_dir() {
remove_matching(&paths::COPILOT_DIR, |_| true).await
}
@@ -704,3 +988,226 @@ async fn get_copilot_lsp(http: Arc<dyn HttpClient>) -> anyhow::Result<PathBuf> {
}
}
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use gpui::{executor::Deterministic, TestAppContext};
+
+ #[gpui::test(iterations = 10)]
+ async fn test_buffer_management(deterministic: Arc<Deterministic>, cx: &mut TestAppContext) {
+ deterministic.forbid_parking();
+ let (copilot, mut lsp) = Copilot::fake(cx);
+
+ let buffer_1 = cx.add_model(|cx| Buffer::new(0, "Hello", cx));
+ let buffer_1_uri: lsp::Url = format!("buffer://{}", buffer_1.id()).parse().unwrap();
+ copilot.update(cx, |copilot, cx| copilot.register_buffer(&buffer_1, cx));
+ assert_eq!(
+ lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
+ .await,
+ lsp::DidOpenTextDocumentParams {
+ text_document: lsp::TextDocumentItem::new(
+ buffer_1_uri.clone(),
+ "plaintext".into(),
+ 0,
+ "Hello".into()
+ ),
+ }
+ );
+
+ let buffer_2 = cx.add_model(|cx| Buffer::new(0, "Goodbye", cx));
+ let buffer_2_uri: lsp::Url = format!("buffer://{}", buffer_2.id()).parse().unwrap();
+ copilot.update(cx, |copilot, cx| copilot.register_buffer(&buffer_2, cx));
+ assert_eq!(
+ lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
+ .await,
+ lsp::DidOpenTextDocumentParams {
+ text_document: lsp::TextDocumentItem::new(
+ buffer_2_uri.clone(),
+ "plaintext".into(),
+ 0,
+ "Goodbye".into()
+ ),
+ }
+ );
+
+ buffer_1.update(cx, |buffer, cx| buffer.edit([(5..5, " world")], None, cx));
+ assert_eq!(
+ lsp.receive_notification::<lsp::notification::DidChangeTextDocument>()
+ .await,
+ lsp::DidChangeTextDocumentParams {
+ text_document: lsp::VersionedTextDocumentIdentifier::new(buffer_1_uri.clone(), 1),
+ content_changes: vec![lsp::TextDocumentContentChangeEvent {
+ range: Some(lsp::Range::new(
+ lsp::Position::new(0, 5),
+ lsp::Position::new(0, 5)
+ )),
+ range_length: None,
+ text: " world".into(),
+ }],
+ }
+ );
+
+ // Ensure updates to the file are reflected in the LSP.
+ buffer_1
+ .update(cx, |buffer, cx| {
+ buffer.file_updated(
+ Arc::new(File {
+ abs_path: "/root/child/buffer-1".into(),
+ path: Path::new("child/buffer-1").into(),
+ }),
+ cx,
+ )
+ })
+ .await;
+ assert_eq!(
+ lsp.receive_notification::<lsp::notification::DidCloseTextDocument>()
+ .await,
+ lsp::DidCloseTextDocumentParams {
+ text_document: lsp::TextDocumentIdentifier::new(buffer_1_uri),
+ }
+ );
+ let buffer_1_uri = lsp::Url::from_file_path("/root/child/buffer-1").unwrap();
+ assert_eq!(
+ lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
+ .await,
+ lsp::DidOpenTextDocumentParams {
+ text_document: lsp::TextDocumentItem::new(
+ buffer_1_uri.clone(),
+ "plaintext".into(),
+ 1,
+ "Hello world".into()
+ ),
+ }
+ );
+
+ // Ensure all previously-registered buffers are closed when signing out.
+ lsp.handle_request::<request::SignOut, _, _>(|_, _| async {
+ Ok(request::SignOutResult {})
+ });
+ copilot
+ .update(cx, |copilot, cx| copilot.sign_out(cx))
+ .await
+ .unwrap();
+ assert_eq!(
+ lsp.receive_notification::<lsp::notification::DidCloseTextDocument>()
+ .await,
+ lsp::DidCloseTextDocumentParams {
+ text_document: lsp::TextDocumentIdentifier::new(buffer_2_uri.clone()),
+ }
+ );
+ assert_eq!(
+ lsp.receive_notification::<lsp::notification::DidCloseTextDocument>()
+ .await,
+ lsp::DidCloseTextDocumentParams {
+ text_document: lsp::TextDocumentIdentifier::new(buffer_1_uri.clone()),
+ }
+ );
+
+ // Ensure all previously-registered buffers are re-opened when signing in.
+ lsp.handle_request::<request::SignInInitiate, _, _>(|_, _| async {
+ Ok(request::SignInInitiateResult::AlreadySignedIn {
+ user: "user-1".into(),
+ })
+ });
+ copilot
+ .update(cx, |copilot, cx| copilot.sign_in(cx))
+ .await
+ .unwrap();
+ assert_eq!(
+ lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
+ .await,
+ lsp::DidOpenTextDocumentParams {
+ text_document: lsp::TextDocumentItem::new(
+ buffer_2_uri.clone(),
+ "plaintext".into(),
+ 0,
+ "Goodbye".into()
+ ),
+ }
+ );
+ assert_eq!(
+ lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
+ .await,
+ lsp::DidOpenTextDocumentParams {
+ text_document: lsp::TextDocumentItem::new(
+ buffer_1_uri.clone(),
+ "plaintext".into(),
+ 0,
+ "Hello world".into()
+ ),
+ }
+ );
+
+ // Dropping a buffer causes it to be closed on the LSP side as well.
+ cx.update(|_| drop(buffer_2));
+ assert_eq!(
+ lsp.receive_notification::<lsp::notification::DidCloseTextDocument>()
+ .await,
+ lsp::DidCloseTextDocumentParams {
+ text_document: lsp::TextDocumentIdentifier::new(buffer_2_uri),
+ }
+ );
+ }
+
+ struct File {
+ abs_path: PathBuf,
+ path: Arc<Path>,
+ }
+
+ impl language::File for File {
+ fn as_local(&self) -> Option<&dyn language::LocalFile> {
+ Some(self)
+ }
+
+ fn mtime(&self) -> std::time::SystemTime {
+ todo!()
+ }
+
+ fn path(&self) -> &Arc<Path> {
+ &self.path
+ }
+
+ fn full_path(&self, _: &AppContext) -> PathBuf {
+ todo!()
+ }
+
+ fn file_name<'a>(&'a self, _: &'a AppContext) -> &'a std::ffi::OsStr {
+ todo!()
+ }
+
+ fn is_deleted(&self) -> bool {
+ todo!()
+ }
+
+ fn as_any(&self) -> &dyn std::any::Any {
+ todo!()
+ }
+
+ fn to_proto(&self) -> rpc::proto::File {
+ todo!()
+ }
+ }
+
+ impl language::LocalFile for File {
+ fn abs_path(&self, _: &AppContext) -> PathBuf {
+ self.abs_path.clone()
+ }
+
+ fn load(&self, _: &AppContext) -> Task<Result<String>> {
+ todo!()
+ }
+
+ fn buffer_reloaded(
+ &self,
+ _: u64,
+ _: &clock::Global,
+ _: language::RopeFingerprint,
+ _: ::fs::LineEnding,
+ _: std::time::SystemTime,
+ _: &mut AppContext,
+ ) {
+ todo!()
+ }
+ }
+}
@@ -99,14 +99,11 @@ pub struct GetCompletionsParams {
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GetCompletionsDocument {
- pub source: String,
pub tab_size: u32,
pub indent_size: u32,
pub insert_spaces: bool,
pub uri: lsp::Url,
- pub path: String,
pub relative_path: String,
- pub language_id: String,
pub position: lsp::Position,
pub version: usize,
}
@@ -169,3 +166,60 @@ impl lsp::notification::Notification for StatusNotification {
type Params = StatusNotificationParams;
const METHOD: &'static str = "statusNotification";
}
+
+pub enum SetEditorInfo {}
+
+#[derive(Debug, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct SetEditorInfoParams {
+ pub editor_info: EditorInfo,
+ pub editor_plugin_info: EditorPluginInfo,
+}
+
+impl lsp::request::Request for SetEditorInfo {
+ type Params = SetEditorInfoParams;
+ type Result = String;
+ const METHOD: &'static str = "setEditorInfo";
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct EditorInfo {
+ pub name: String,
+ pub version: String,
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct EditorPluginInfo {
+ pub name: String,
+ pub version: String,
+}
+
+pub enum NotifyAccepted {}
+
+#[derive(Debug, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct NotifyAcceptedParams {
+ pub uuid: String,
+}
+
+impl lsp::request::Request for NotifyAccepted {
+ type Params = NotifyAcceptedParams;
+ type Result = String;
+ const METHOD: &'static str = "notifyAccepted";
+}
+
+pub enum NotifyRejected {}
+
+#[derive(Debug, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct NotifyRejectedParams {
+ pub uuids: Vec<String>,
+}
+
+impl lsp::request::Request for NotifyRejected {
+ type Params = NotifyRejectedParams;
+ type Result = String;
+ const METHOD: &'static str = "notifyRejected";
+}
@@ -2,6 +2,7 @@ use crate::{request::PromptUserDeviceFlow, Copilot, Status};
use gpui::{
elements::*,
geometry::rect::RectF,
+ impl_internal_actions,
platform::{WindowBounds, WindowKind, WindowOptions},
AnyViewHandle, AppContext, ClipboardItem, Drawable, Element, Entity, View, ViewContext,
ViewHandle,
@@ -9,6 +10,11 @@ use gpui::{
use settings::Settings;
use theme::ui::modal;
+#[derive(PartialEq, Eq, Debug, Clone)]
+struct ClickedConnect;
+
+impl_internal_actions!(copilot_verification, [ClickedConnect]);
+
#[derive(PartialEq, Eq, Debug, Clone)]
struct CopyUserCode;
@@ -61,6 +67,12 @@ pub fn init(cx: &mut AppContext) {
}
})
.detach();
+
+ cx.add_action(
+ |code_verification: &mut CopilotCodeVerification, _: &ClickedConnect, _| {
+ code_verification.connect_clicked = true;
+ },
+ );
}
fn create_copilot_auth_window(
@@ -85,11 +97,15 @@ fn create_copilot_auth_window(
pub struct CopilotCodeVerification {
status: Status,
+ connect_clicked: bool,
}
impl CopilotCodeVerification {
pub fn new(status: Status) -> Self {
- Self { status }
+ Self {
+ status,
+ connect_clicked: false,
+ }
}
pub fn set_status(&mut self, status: Status, cx: &mut ViewContext<Self>) {
@@ -147,6 +163,7 @@ impl CopilotCodeVerification {
}
fn render_prompting_modal(
+ connect_clicked: bool,
data: &PromptUserDeviceFlow,
style: &theme::Copilot,
cx: &mut ViewContext<Self>,
@@ -195,13 +212,20 @@ impl CopilotCodeVerification {
.with_style(style.auth.prompting.hint.container.clone())
.boxed(),
theme::ui::cta_button_with_click::<ConnectButton, _, _, _>(
- "Connect to GitHub",
+ if connect_clicked {
+ "Waiting for connection..."
+ } else {
+ "Connect to GitHub"
+ },
style.auth.content_width,
&style.auth.cta_button,
cx,
{
let verification_uri = data.verification_uri.clone();
- move |_, _, cx| cx.platform().open_url(&verification_uri)
+ move |_, _, cx| {
+ cx.platform().open_url(&verification_uri);
+ cx.dispatch_action(ClickedConnect)
+ }
},
)
.boxed(),
@@ -350,9 +374,20 @@ impl View for CopilotCodeVerification {
match &self.status {
Status::SigningIn {
prompt: Some(prompt),
- } => Self::render_prompting_modal(&prompt, &style.copilot, cx),
- Status::Unauthorized => Self::render_unauthorized_modal(&style.copilot, cx),
- Status::Authorized => Self::render_enabled_modal(&style.copilot, cx),
+ } => Self::render_prompting_modal(
+ self.connect_clicked,
+ &prompt,
+ &style.copilot,
+ cx,
+ ),
+ Status::Unauthorized => {
+ self.connect_clicked = false;
+ Self::render_unauthorized_modal(&style.copilot, cx)
+ }
+ Status::Authorized => {
+ self.connect_clicked = false;
+ Self::render_enabled_modal(&style.copilot, cx)
+ }
_ => Empty::new().boxed(),
},
])
@@ -23,6 +23,15 @@ const COPILOT_ERROR_TOAST_ID: usize = 1338;
#[derive(Clone, PartialEq)]
pub struct DeployCopilotMenu;
+#[derive(Clone, PartialEq)]
+pub struct DeployCopilotStartMenu;
+
+#[derive(Clone, PartialEq)]
+pub struct HideCopilot;
+
+#[derive(Clone, PartialEq)]
+pub struct InitiateSignIn;
+
#[derive(Clone, PartialEq)]
pub struct ToggleCopilotForLanguage {
language: Arc<str>,
@@ -39,6 +48,9 @@ impl_internal_actions!(
copilot,
[
DeployCopilotMenu,
+ DeployCopilotStartMenu,
+ HideCopilot,
+ InitiateSignIn,
DeployCopilotModal,
ToggleCopilotForLanguage,
ToggleCopilotGlobally,
@@ -47,17 +59,19 @@ impl_internal_actions!(
pub fn init(cx: &mut AppContext) {
cx.add_action(CopilotButton::deploy_copilot_menu);
+ cx.add_action(CopilotButton::deploy_copilot_start_menu);
cx.add_action(
|_: &mut CopilotButton, action: &ToggleCopilotForLanguage, cx| {
- let language = action.language.to_owned();
-
- let current_langauge = cx.global::<Settings>().copilot_on(Some(&language));
+ let language = action.language.clone();
+ let show_copilot_suggestions = cx
+ .global::<Settings>()
+ .show_copilot_suggestions(Some(&language));
SettingsFile::update(cx, move |file_contents| {
file_contents.languages.insert(
- language.to_owned(),
+ language,
settings::EditorSettings {
- copilot: Some((!current_langauge).into()),
+ show_copilot_suggestions: Some((!show_copilot_suggestions).into()),
..Default::default()
},
);
@@ -66,12 +80,63 @@ pub fn init(cx: &mut AppContext) {
);
cx.add_action(|_: &mut CopilotButton, _: &ToggleCopilotGlobally, cx| {
- let copilot_on = cx.global::<Settings>().copilot_on(None);
+ let show_copilot_suggestions = cx.global::<Settings>().show_copilot_suggestions(None);
+ SettingsFile::update(cx, move |file_contents| {
+ file_contents.editor.show_copilot_suggestions = Some((!show_copilot_suggestions).into())
+ })
+ });
+ cx.add_action(|_: &mut CopilotButton, _: &HideCopilot, cx| {
SettingsFile::update(cx, move |file_contents| {
- file_contents.editor.copilot = Some((!copilot_on).into())
+ file_contents.features.copilot = Some(false)
})
});
+
+ cx.add_action(|_: &mut CopilotButton, _: &InitiateSignIn, cx| {
+ let Some(copilot) = Copilot::global(cx) else {
+ return;
+ };
+ let status = copilot.read(cx).status();
+
+ match status {
+ Status::Starting { task } => {
+ cx.dispatch_action(workspace::Toast::new(
+ COPILOT_STARTING_TOAST_ID,
+ "Copilot is starting...",
+ ));
+ let window_id = cx.window_id();
+ let task = task.to_owned();
+ cx.spawn(|handle, mut cx| async move {
+ task.await;
+ cx.update(|cx| {
+ if let Some(copilot) = Copilot::global(cx) {
+ let status = copilot.read(cx).status();
+ match status {
+ Status::Authorized => cx.dispatch_action_at(
+ window_id,
+ handle.id(),
+ workspace::Toast::new(
+ COPILOT_STARTING_TOAST_ID,
+ "Copilot has started!",
+ ),
+ ),
+ _ => {
+ cx.dispatch_action_at(
+ window_id,
+ handle.id(),
+ DismissToast::new(COPILOT_STARTING_TOAST_ID),
+ );
+ cx.dispatch_action_at(window_id, handle.id(), SignIn)
+ }
+ }
+ }
+ })
+ })
+ .detach();
+ }
+ _ => cx.dispatch_action(SignIn),
+ }
+ })
}
pub struct CopilotButton {
@@ -93,7 +158,7 @@ impl View for CopilotButton {
fn render(&mut self, cx: &mut ViewContext<Self>) -> Element<Self> {
let settings = cx.global::<Settings>();
- if !settings.enable_copilot_integration {
+ if !settings.features.copilot {
return Empty::new().boxed();
}
@@ -104,9 +169,9 @@ impl View for CopilotButton {
};
let status = copilot.read(cx).status();
- let enabled = self.editor_enabled.unwrap_or(settings.copilot_on(None));
-
- let view_id = cx.view_id();
+ let enabled = self
+ .editor_enabled
+ .unwrap_or(settings.show_copilot_suggestions(None));
Stack::new()
.with_child(
@@ -154,48 +219,13 @@ impl View for CopilotButton {
let status = status.clone();
move |_, _, cx| match status {
Status::Authorized => cx.dispatch_action(DeployCopilotMenu),
- Status::Starting { ref task } => {
- cx.dispatch_action(workspace::Toast::new(
- COPILOT_STARTING_TOAST_ID,
- "Copilot is starting...",
- ));
- let window_id = cx.window_id();
- let task = task.to_owned();
- cx.spawn_weak(|_this, mut cx| async move {
- task.await;
- cx.update(|cx| {
- if let Some(copilot) = Copilot::global(cx) {
- let status = copilot.read(cx).status();
- match status {
- Status::Authorized => cx.dispatch_action_at(
- window_id,
- view_id,
- workspace::Toast::new(
- COPILOT_STARTING_TOAST_ID,
- "Copilot has started!",
- ),
- ),
- _ => {
- cx.dispatch_action_at(
- window_id,
- view_id,
- DismissToast::new(COPILOT_STARTING_TOAST_ID),
- );
- cx.dispatch_global_action(SignIn)
- }
- }
- }
- })
- })
- .detach();
- }
Status::Error(ref e) => cx.dispatch_action(workspace::Toast::new_action(
COPILOT_ERROR_TOAST_ID,
format!("Copilot can't be started: {}", e),
"Reinstall Copilot",
Reinstall,
)),
- _ => cx.dispatch_action(SignIn),
+ _ => cx.dispatch_action(DeployCopilotStartMenu),
}
})
.with_tooltip::<Self>(0, "GitHub Copilot".into(), None, theme.tooltip.clone(), cx)
@@ -235,22 +265,38 @@ impl CopilotButton {
}
}
+ pub fn deploy_copilot_start_menu(
+ &mut self,
+ _: &DeployCopilotStartMenu,
+ cx: &mut ViewContext<Self>,
+ ) {
+ let mut menu_options = Vec::with_capacity(2);
+
+ menu_options.push(ContextMenuItem::item("Sign In", InitiateSignIn));
+ menu_options.push(ContextMenuItem::item("Hide Copilot", HideCopilot));
+
+ self.popup_menu.update(cx, |menu, cx| {
+ menu.show(
+ Default::default(),
+ AnchorCorner::BottomRight,
+ menu_options,
+ cx,
+ );
+ });
+ }
+
pub fn deploy_copilot_menu(&mut self, _: &DeployCopilotMenu, cx: &mut ViewContext<Self>) {
let settings = cx.global::<Settings>();
let mut menu_options = Vec::with_capacity(6);
if let Some(language) = &self.language {
- let language_enabled = settings.copilot_on(Some(language.as_ref()));
+ let language_enabled = settings.show_copilot_suggestions(Some(language.as_ref()));
menu_options.push(ContextMenuItem::item(
format!(
- "{} Copilot for {}",
- if language_enabled {
- "Disable"
- } else {
- "Enable"
- },
+ "{} Suggestions for {}",
+ if language_enabled { "Hide" } else { "Show" },
language
),
ToggleCopilotForLanguage {
@@ -259,12 +305,12 @@ impl CopilotButton {
));
}
- let globally_enabled = cx.global::<Settings>().copilot_on(None);
+ let globally_enabled = cx.global::<Settings>().show_copilot_suggestions(None);
menu_options.push(ContextMenuItem::item(
if globally_enabled {
- "Disable Copilot Globally"
+ "Hide Suggestions for All Files"
} else {
- "Enable Copilot Globally"
+ "Show Suggestions for All Files"
},
ToggleCopilotGlobally,
));
@@ -312,7 +358,7 @@ impl CopilotButton {
self.language = language_name.clone();
- self.editor_enabled = Some(settings.copilot_on(language_name.as_deref()));
+ self.editor_enabled = Some(settings.show_copilot_suggestions(language_name.as_deref()));
cx.notify()
}
@@ -23,6 +23,7 @@ use settings::Settings;
use smallvec::SmallVec;
use std::{
any::{Any, TypeId},
+ borrow::Cow,
cmp::Ordering,
ops::Range,
path::PathBuf,
@@ -530,6 +531,10 @@ impl Item for ProjectDiagnosticsEditor {
.update(cx, |editor, cx| editor.navigate(data, cx))
}
+ fn tab_tooltip_text(&self, _: &AppContext) -> Option<Cow<str>> {
+ Some("Project Diagnostics".into())
+ }
+
fn is_dirty(&self, cx: &AppContext) -> bool {
self.excerpts.read(cx).is_dirty(cx)
}
@@ -52,7 +52,7 @@ pub use language::{char_kind, CharKind};
use language::{
AutoindentMode, BracketPair, Buffer, CodeAction, CodeLabel, Completion, CursorShape,
Diagnostic, DiagnosticSeverity, IndentKind, IndentSize, Language, OffsetRangeExt, OffsetUtf16,
- Point, Rope, Selection, SelectionGoal, TransactionId,
+ Point, Selection, SelectionGoal, TransactionId,
};
use link_go_to_definition::{
hide_link_definition, show_link_definition, LinkDefinitionKind, LinkGoToDefinitionState,
@@ -184,6 +184,7 @@ actions!(
Backspace,
Delete,
Newline,
+ NewlineAbove,
NewlineBelow,
GoToDiagnostic,
GoToPrevDiagnostic,
@@ -301,6 +302,7 @@ pub fn init(cx: &mut AppContext) {
cx.add_action(Editor::select);
cx.add_action(Editor::cancel);
cx.add_action(Editor::newline);
+ cx.add_action(Editor::newline_above);
cx.add_action(Editor::newline_below);
cx.add_action(Editor::backspace);
cx.add_action(Editor::delete);
@@ -395,6 +397,7 @@ pub fn init(cx: &mut AppContext) {
cx.add_async_action(Editor::find_all_references);
cx.add_action(Editor::next_copilot_suggestion);
cx.add_action(Editor::previous_copilot_suggestion);
+ cx.add_action(Editor::copilot_suggest);
hover_popover::init(cx);
link_go_to_definition::init(cx);
@@ -1014,6 +1017,8 @@ impl CodeActionsMenu {
pub struct CopilotState {
excerpt_id: Option<ExcerptId>,
pending_refresh: Task<Option<()>>,
+ pending_cycling_refresh: Task<Option<()>>,
+ cycled: bool,
completions: Vec<copilot::Completion>,
active_completion_index: usize,
}
@@ -1022,14 +1027,20 @@ impl Default for CopilotState {
fn default() -> Self {
Self {
excerpt_id: None,
+ pending_cycling_refresh: Task::ready(Some(())),
pending_refresh: Task::ready(Some(())),
completions: Default::default(),
active_completion_index: 0,
+ cycled: false,
}
}
}
impl CopilotState {
+ fn active_completion(&self) -> Option<&copilot::Completion> {
+ self.completions.get(self.active_completion_index)
+ }
+
fn text_for_active_completion(
&self,
cursor: Anchor,
@@ -1037,7 +1048,7 @@ impl CopilotState {
) -> Option<&str> {
use language::ToOffset as _;
- let completion = self.completions.get(self.active_completion_index)?;
+ let completion = self.active_completion()?;
let excerpt_id = self.excerpt_id?;
let completion_buffer = buffer.buffer_for_excerpt(excerpt_id)?;
if excerpt_id != cursor.excerpt_id
@@ -1068,9 +1079,29 @@ impl CopilotState {
}
}
+ fn cycle_completions(&mut self, direction: Direction) {
+ match direction {
+ Direction::Prev => {
+ self.active_completion_index = if self.active_completion_index == 0 {
+ self.completions.len().saturating_sub(1)
+ } else {
+ self.active_completion_index - 1
+ };
+ }
+ Direction::Next => {
+ if self.completions.len() == 0 {
+ self.active_completion_index = 0
+ } else {
+ self.active_completion_index =
+ (self.active_completion_index + 1) % self.completions.len();
+ }
+ }
+ }
+ }
+
fn push_completion(&mut self, new_completion: copilot::Completion) {
for completion in &self.completions {
- if *completion == new_completion {
+ if completion.text == new_completion.text && completion.range == new_completion.range {
return;
}
}
@@ -1265,7 +1296,7 @@ impl Editor {
cx.subscribe(&buffer, Self::on_buffer_event),
cx.observe(&display_map, Self::on_display_map_changed),
cx.observe(&blink_manager, |_, _, cx| cx.notify()),
- cx.observe_global::<Settings, _>(Self::on_settings_changed),
+ cx.observe_global::<Settings, _>(Self::settings_changed),
],
};
this.end_selection(cx);
@@ -1469,7 +1500,7 @@ impl Editor {
self.refresh_code_actions(cx);
self.refresh_document_highlights(cx);
refresh_matching_bracket_highlights(self, cx);
- self.hide_copilot_suggestion(cx);
+ self.discard_copilot_suggestion(cx);
}
self.blink_manager.update(cx, BlinkManager::pause_blinking);
@@ -1843,7 +1874,7 @@ impl Editor {
return;
}
- if self.hide_copilot_suggestion(cx).is_some() {
+ if self.discard_copilot_suggestion(cx) {
return;
}
@@ -2026,13 +2057,13 @@ impl Editor {
this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(new_selections));
if had_active_copilot_suggestion {
- this.refresh_copilot_suggestions(cx);
+ this.refresh_copilot_suggestions(true, cx);
if !this.has_active_copilot_suggestion(cx) {
this.trigger_completion_on_input(&text, cx);
}
} else {
this.trigger_completion_on_input(&text, cx);
- this.refresh_copilot_suggestions(cx);
+ this.refresh_copilot_suggestions(true, cx);
}
});
}
@@ -2114,7 +2145,66 @@ impl Editor {
.collect();
this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(new_selections));
- this.refresh_copilot_suggestions(cx);
+ this.refresh_copilot_suggestions(true, cx);
+ });
+ }
+
+ pub fn newline_above(&mut self, _: &NewlineAbove, cx: &mut ViewContext<Self>) {
+ let buffer = self.buffer.read(cx);
+ let snapshot = buffer.snapshot(cx);
+
+ let mut edits = Vec::new();
+ let mut rows = Vec::new();
+ let mut rows_inserted = 0;
+
+ for selection in self.selections.all_adjusted(cx) {
+ let cursor = selection.head();
+ let row = cursor.row;
+
+ let start_of_line = snapshot.clip_point(Point::new(row, 0), Bias::Left);
+
+ let newline = "\n".to_string();
+ edits.push((start_of_line..start_of_line, newline));
+
+ rows.push(row + rows_inserted);
+ rows_inserted += 1;
+ }
+
+ self.transact(cx, |editor, cx| {
+ editor.edit(edits, cx);
+
+ editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
+ let mut index = 0;
+ s.move_cursors_with(|map, _, _| {
+ let row = rows[index];
+ index += 1;
+
+ let point = Point::new(row, 0);
+ let boundary = map.next_line_boundary(point).1;
+ let clipped = map.clip_point(boundary, Bias::Left);
+
+ (clipped, SelectionGoal::None)
+ });
+ });
+
+ let mut indent_edits = Vec::new();
+ let multibuffer_snapshot = editor.buffer.read(cx).snapshot(cx);
+ for row in rows {
+ let indents = multibuffer_snapshot.suggested_indents(row..row + 1, cx);
+ for (row, indent) in indents {
+ if indent.len == 0 {
+ continue;
+ }
+
+ let text = match indent.kind {
+ IndentKind::Space => " ".repeat(indent.len as usize),
+ IndentKind::Tab => "\t".repeat(indent.len as usize),
+ };
+ let point = Point::new(row, 0);
+ indent_edits.push((point..point, text));
+ }
+ }
+ editor.edit(indent_edits, cx);
});
}
@@ -2130,19 +2220,18 @@ impl Editor {
let cursor = selection.head();
let row = cursor.row;
- let end_of_line = snapshot
- .clip_point(Point::new(row, snapshot.line_len(row)), Bias::Left)
- .to_point(&snapshot);
+ let point = Point::new(row + 1, 0);
+ let start_of_line = snapshot.clip_point(point, Bias::Left);
let newline = "\n".to_string();
- edits.push((end_of_line..end_of_line, newline));
+ edits.push((start_of_line..start_of_line, newline));
rows_inserted += 1;
rows.push(row + rows_inserted);
}
self.transact(cx, |editor, cx| {
- editor.edit_with_autoindent(edits, cx);
+ editor.edit(edits, cx);
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
let mut index = 0;
@@ -2157,6 +2246,25 @@ impl Editor {
(clipped, SelectionGoal::None)
});
});
+
+ let mut indent_edits = Vec::new();
+ let multibuffer_snapshot = editor.buffer.read(cx).snapshot(cx);
+ for row in rows {
+ let indents = multibuffer_snapshot.suggested_indents(row..row + 1, cx);
+ for (row, indent) in indents {
+ if indent.len == 0 {
+ continue;
+ }
+
+ let text = match indent.kind {
+ IndentKind::Space => " ".repeat(indent.len as usize),
+ IndentKind::Tab => "\t".repeat(indent.len as usize),
+ };
+ let point = Point::new(row, 0);
+ indent_edits.push((point..point, text));
+ }
+ }
+ editor.edit(indent_edits, cx);
});
}
@@ -2512,7 +2620,7 @@ impl Editor {
});
}
- this.refresh_copilot_suggestions(cx);
+ this.refresh_copilot_suggestions(true, cx);
});
let project = self.project.clone()?;
@@ -2809,10 +2917,14 @@ impl Editor {
None
}
- fn refresh_copilot_suggestions(&mut self, cx: &mut ViewContext<Self>) -> Option<()> {
+ fn refresh_copilot_suggestions(
+ &mut self,
+ debounce: bool,
+ cx: &mut ViewContext<Self>,
+ ) -> Option<()> {
let copilot = Copilot::global(cx)?;
if self.mode != EditorMode::Full || !copilot.read(cx).status().is_authorized() {
- self.hide_copilot_suggestion(cx);
+ self.clear_copilot_suggestions(cx);
return None;
}
self.update_visible_copilot_suggestion(cx);
@@ -2820,29 +2932,36 @@ impl Editor {
let snapshot = self.buffer.read(cx).snapshot(cx);
let cursor = self.selections.newest_anchor().head();
let language_name = snapshot.language_at(cursor).map(|language| language.name());
- if !cx.global::<Settings>().copilot_on(language_name.as_deref()) {
- self.hide_copilot_suggestion(cx);
+ if !cx
+ .global::<Settings>()
+ .show_copilot_suggestions(language_name.as_deref())
+ {
+ self.clear_copilot_suggestions(cx);
return None;
}
let (buffer, buffer_position) =
self.buffer.read(cx).text_anchor_for_position(cursor, cx)?;
self.copilot_state.pending_refresh = cx.spawn_weak(|this, mut cx| async move {
- cx.background().timer(COPILOT_DEBOUNCE_TIMEOUT).await;
- let (completion, completions_cycling) = copilot.update(&mut cx, |copilot, cx| {
- (
- copilot.completions(&buffer, buffer_position, cx),
- copilot.completions_cycling(&buffer, buffer_position, cx),
- )
- });
+ if debounce {
+ cx.background().timer(COPILOT_DEBOUNCE_TIMEOUT).await;
+ }
+
+ let completions = copilot
+ .update(&mut cx, |copilot, cx| {
+ copilot.completions(&buffer, buffer_position, cx)
+ })
+ .await
+ .log_err()
+ .into_iter()
+ .flatten()
+ .collect_vec();
- let (completion, completions_cycling) = futures::join!(completion, completions_cycling);
- let mut completions = Vec::new();
- completions.extend(completion.log_err().into_iter().flatten());
- completions.extend(completions_cycling.log_err().into_iter().flatten());
this.upgrade(&cx)?
.update(&mut cx, |this, cx| {
if !completions.is_empty() {
+ this.copilot_state.cycled = false;
+ this.copilot_state.pending_cycling_refresh = Task::ready(None);
this.copilot_state.completions.clear();
this.copilot_state.active_completion_index = 0;
this.copilot_state.excerpt_id = Some(cursor.excerpt_id);
@@ -2853,68 +2972,126 @@ impl Editor {
}
})
.log_err()?;
-
Some(())
});
Some(())
}
- fn next_copilot_suggestion(&mut self, _: &copilot::NextSuggestion, cx: &mut ViewContext<Self>) {
+ fn cycle_copilot_suggestions(
+ &mut self,
+ direction: Direction,
+ cx: &mut ViewContext<Self>,
+ ) -> Option<()> {
+ let copilot = Copilot::global(cx)?;
+ if self.mode != EditorMode::Full || !copilot.read(cx).status().is_authorized() {
+ return None;
+ }
+
+ if self.copilot_state.cycled {
+ self.copilot_state.cycle_completions(direction);
+ self.update_visible_copilot_suggestion(cx);
+ } else {
+ let cursor = self.selections.newest_anchor().head();
+ let (buffer, buffer_position) =
+ self.buffer.read(cx).text_anchor_for_position(cursor, cx)?;
+ self.copilot_state.pending_cycling_refresh = cx.spawn_weak(|this, mut cx| async move {
+ let completions = copilot
+ .update(&mut cx, |copilot, cx| {
+ copilot.completions_cycling(&buffer, buffer_position, cx)
+ })
+ .await;
+
+ this.upgrade(&cx)?
+ .update(&mut cx, |this, cx| {
+ this.copilot_state.cycled = true;
+ for completion in completions.log_err().into_iter().flatten() {
+ this.copilot_state.push_completion(completion);
+ }
+ this.copilot_state.cycle_completions(direction);
+ this.update_visible_copilot_suggestion(cx);
+ })
+ .log_err()?;
+
+ Some(())
+ });
+ }
+
+ Some(())
+ }
+
+ fn copilot_suggest(&mut self, _: &copilot::Suggest, cx: &mut ViewContext<Self>) {
if !self.has_active_copilot_suggestion(cx) {
- self.refresh_copilot_suggestions(cx);
+ self.refresh_copilot_suggestions(false, cx);
return;
}
- self.copilot_state.active_completion_index =
- (self.copilot_state.active_completion_index + 1) % self.copilot_state.completions.len();
self.update_visible_copilot_suggestion(cx);
}
+ fn next_copilot_suggestion(&mut self, _: &copilot::NextSuggestion, cx: &mut ViewContext<Self>) {
+ if self.has_active_copilot_suggestion(cx) {
+ self.cycle_copilot_suggestions(Direction::Next, cx);
+ } else {
+ self.refresh_copilot_suggestions(false, cx);
+ }
+ }
+
fn previous_copilot_suggestion(
&mut self,
_: &copilot::PreviousSuggestion,
cx: &mut ViewContext<Self>,
) {
- if !self.has_active_copilot_suggestion(cx) {
- self.refresh_copilot_suggestions(cx);
- return;
+ if self.has_active_copilot_suggestion(cx) {
+ self.cycle_copilot_suggestions(Direction::Prev, cx);
+ } else {
+ self.refresh_copilot_suggestions(false, cx);
}
-
- self.copilot_state.active_completion_index =
- if self.copilot_state.active_completion_index == 0 {
- self.copilot_state.completions.len() - 1
- } else {
- self.copilot_state.active_completion_index - 1
- };
- self.update_visible_copilot_suggestion(cx);
}
fn accept_copilot_suggestion(&mut self, cx: &mut ViewContext<Self>) -> bool {
- if let Some(text) = self.hide_copilot_suggestion(cx) {
- self.insert_with_autoindent_mode(&text.to_string(), None, cx);
+ if let Some(suggestion) = self
+ .display_map
+ .update(cx, |map, cx| map.replace_suggestion::<usize>(None, cx))
+ {
+ if let Some((copilot, completion)) =
+ Copilot::global(cx).zip(self.copilot_state.active_completion())
+ {
+ copilot
+ .update(cx, |copilot, cx| copilot.accept_completion(completion, cx))
+ .detach_and_log_err(cx);
+ }
+ self.insert_with_autoindent_mode(&suggestion.text.to_string(), None, cx);
+ cx.notify();
true
} else {
false
}
}
- fn has_active_copilot_suggestion(&self, cx: &AppContext) -> bool {
- self.display_map.read(cx).has_suggestion()
- }
-
- fn hide_copilot_suggestion(&mut self, cx: &mut ViewContext<Self>) -> Option<Rope> {
+ fn discard_copilot_suggestion(&mut self, cx: &mut ViewContext<Self>) -> bool {
if self.has_active_copilot_suggestion(cx) {
- let old_suggestion = self
- .display_map
+ if let Some(copilot) = Copilot::global(cx) {
+ copilot
+ .update(cx, |copilot, cx| {
+ copilot.discard_completions(&self.copilot_state.completions, cx)
+ })
+ .detach_and_log_err(cx);
+ }
+
+ self.display_map
.update(cx, |map, cx| map.replace_suggestion::<usize>(None, cx));
cx.notify();
- old_suggestion.map(|suggestion| suggestion.text)
+ true
} else {
- None
+ false
}
}
+ fn has_active_copilot_suggestion(&self, cx: &AppContext) -> bool {
+ self.display_map.read(cx).has_suggestion()
+ }
+
fn update_visible_copilot_suggestion(&mut self, cx: &mut ViewContext<Self>) {
let snapshot = self.buffer.read(cx).snapshot(cx);
let selection = self.selections.newest_anchor();
@@ -2924,26 +3101,31 @@ impl Editor {
|| !self.completion_tasks.is_empty()
|| selection.start != selection.end
{
- self.hide_copilot_suggestion(cx);
+ self.discard_copilot_suggestion(cx);
} else if let Some(text) = self
.copilot_state
.text_for_active_completion(cursor, &snapshot)
{
- self.display_map.update(cx, |map, cx| {
+ self.display_map.update(cx, move |map, cx| {
map.replace_suggestion(
Some(Suggestion {
position: cursor,
- text: text.into(),
+ text: text.trim_end().into(),
}),
cx,
)
});
cx.notify();
} else {
- self.hide_copilot_suggestion(cx);
+ self.discard_copilot_suggestion(cx);
}
}
+ fn clear_copilot_suggestions(&mut self, cx: &mut ViewContext<Self>) {
+ self.copilot_state = Default::default();
+ self.discard_copilot_suggestion(cx);
+ }
+
pub fn render_code_actions_indicator(
&self,
style: &EditorStyle,
@@ -3059,7 +3241,7 @@ impl Editor {
self.completion_tasks.clear();
}
self.context_menu = Some(menu);
- self.hide_copilot_suggestion(cx);
+ self.discard_copilot_suggestion(cx);
cx.notify();
}
@@ -3229,7 +3411,7 @@ impl Editor {
this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(selections));
this.insert("", cx);
- this.refresh_copilot_suggestions(cx);
+ this.refresh_copilot_suggestions(true, cx);
});
}
@@ -3245,7 +3427,7 @@ impl Editor {
})
});
this.insert("", cx);
- this.refresh_copilot_suggestions(cx);
+ this.refresh_copilot_suggestions(true, cx);
});
}
@@ -3341,7 +3523,7 @@ impl Editor {
self.transact(cx, |this, cx| {
this.buffer.update(cx, |b, cx| b.edit(edits, None, cx));
this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(selections));
- this.refresh_copilot_suggestions(cx);
+ this.refresh_copilot_suggestions(true, cx);
});
}
@@ -4021,7 +4203,7 @@ impl Editor {
}
self.request_autoscroll(Autoscroll::fit(), cx);
self.unmark_text(cx);
- self.refresh_copilot_suggestions(cx);
+ self.refresh_copilot_suggestions(true, cx);
cx.emit(Event::Edited);
}
}
@@ -4036,7 +4218,7 @@ impl Editor {
}
self.request_autoscroll(Autoscroll::fit(), cx);
self.unmark_text(cx);
- self.refresh_copilot_suggestions(cx);
+ self.refresh_copilot_suggestions(true, cx);
cx.emit(Event::Edited);
}
}
@@ -6490,6 +6672,7 @@ impl Editor {
multi_buffer::Event::DiagnosticsUpdated => {
self.refresh_active_diagnostics(cx);
}
+ multi_buffer::Event::LanguageChanged => {}
}
}
@@ -6497,8 +6680,8 @@ impl Editor {
cx.notify();
}
- fn on_settings_changed(&mut self, cx: &mut ViewContext<Self>) {
- self.refresh_copilot_suggestions(cx);
+ fn settings_changed(&mut self, cx: &mut ViewContext<Self>) {
+ self.refresh_copilot_suggestions(true, cx);
}
pub fn set_searchable(&mut self, searchable: bool) {
@@ -1488,6 +1488,55 @@ fn test_newline_with_old_selections(cx: &mut TestAppContext) {
});
}
+#[gpui::test]
+async fn test_newline_above(cx: &mut gpui::TestAppContext) {
+ let mut cx = EditorTestContext::new(cx);
+ cx.update(|cx| {
+ cx.update_global::<Settings, _, _>(|settings, _| {
+ settings.editor_overrides.tab_size = Some(NonZeroU32::new(4).unwrap());
+ });
+ });
+
+ let language = Arc::new(
+ Language::new(
+ LanguageConfig::default(),
+ Some(tree_sitter_rust::language()),
+ )
+ .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
+ .unwrap(),
+ );
+ cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
+
+ cx.set_state(indoc! {"
+ const a: ˇA = (
+ (ˇ
+ «const_functionˇ»(ˇ),
+ so«mˇ»et«hˇ»ing_ˇelse,ˇ
+ )ˇ
+ ˇ);ˇ
+ "});
+ cx.update_editor(|e, cx| e.newline_above(&NewlineAbove, cx));
+ cx.assert_editor_state(indoc! {"
+ ˇ
+ const a: A = (
+ ˇ
+ (
+ ˇ
+ ˇ
+ const_function(),
+ ˇ
+ ˇ
+ ˇ
+ ˇ
+ something_else,
+ ˇ
+ )
+ ˇ
+ ˇ
+ );
+ "});
+}
+
#[gpui::test]
async fn test_newline_below(cx: &mut gpui::TestAppContext) {
let mut cx = EditorTestContext::new(cx);
@@ -3,12 +3,12 @@ use crate::{
movement::surrounding_word, persistence::DB, scroll::ScrollAnchor, Anchor, Autoscroll, Editor,
Event, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, NavigationData, ToPoint as _,
};
-use anyhow::{anyhow, Context, Result};
+use anyhow::{Context, Result};
use collections::HashSet;
use futures::future::try_join_all;
use gpui::{
- elements::*, geometry::vector::vec2f, AppContext, Entity, ModelHandle, Subscription, Task,
- View, ViewContext, ViewHandle, WeakViewHandle,
+ elements::*, geometry::vector::vec2f, AppContext, AsyncAppContext, Entity, ModelHandle,
+ Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle,
};
use language::{
proto::serialize_anchor as serialize_text_anchor, Bias, Buffer, OffsetRangeExt, Point,
@@ -72,11 +72,11 @@ impl FollowableItem for Editor {
let editor = pane.read_with(&cx, |pane, cx| {
let mut editors = pane.items_of_type::<Self>();
editors.find(|editor| {
- editor.remote_id(&client, cx) == Some(remote_id)
- || state.singleton
- && buffers.len() == 1
- && editor.read(cx).buffer.read(cx).as_singleton().as_ref()
- == Some(&buffers[0])
+ let ids_match = editor.remote_id(&client, cx) == Some(remote_id);
+ let singleton_buffer_matches = state.singleton
+ && buffers.first()
+ == editor.read(cx).buffer.read(cx).as_singleton().as_ref();
+ ids_match || singleton_buffer_matches
})
});
@@ -117,46 +117,29 @@ impl FollowableItem for Editor {
multibuffer
});
- cx.add_view(|cx| Editor::for_multibuffer(multibuffer, Some(project), cx))
+ cx.add_view(|cx| {
+ let mut editor =
+ Editor::for_multibuffer(multibuffer, Some(project.clone()), cx);
+ editor.remote_id = Some(remote_id);
+ editor
+ })
})?
};
- editor.update(&mut cx, |editor, cx| {
- editor.remote_id = Some(remote_id);
- let buffer = editor.buffer.read(cx).read(cx);
- let selections = state
- .selections
- .into_iter()
- .map(|selection| {
- deserialize_selection(&buffer, selection)
- .ok_or_else(|| anyhow!("invalid selection"))
- })
- .collect::<Result<Vec<_>>>()?;
- let pending_selection = state
- .pending_selection
- .map(|selection| deserialize_selection(&buffer, selection))
- .flatten();
- let scroll_top_anchor = state
- .scroll_top_anchor
- .and_then(|anchor| deserialize_anchor(&buffer, anchor));
- drop(buffer);
-
- if !selections.is_empty() || pending_selection.is_some() {
- editor.set_selections_from_remote(selections, pending_selection, cx);
- }
-
- if let Some(scroll_top_anchor) = scroll_top_anchor {
- editor.set_scroll_anchor_remote(
- ScrollAnchor {
- top_anchor: scroll_top_anchor,
- offset: vec2f(state.scroll_x, state.scroll_y),
- },
- cx,
- );
- }
-
- anyhow::Ok(())
- })??;
+ update_editor_from_message(
+ editor.clone(),
+ project,
+ proto::update_view::Editor {
+ selections: state.selections,
+ pending_selection: state.pending_selection,
+ scroll_top_anchor: state.scroll_top_anchor,
+ scroll_x: state.scroll_x,
+ scroll_y: state.scroll_y,
+ ..Default::default()
+ },
+ &mut cx,
+ )
+ .await?;
Ok(editor)
}))
@@ -301,107 +284,142 @@ impl FollowableItem for Editor {
cx: &mut ViewContext<Self>,
) -> Task<Result<()>> {
let update_view::Variant::Editor(message) = message;
- let multibuffer = self.buffer.read(cx);
- let multibuffer = multibuffer.read(cx);
+ let project = project.clone();
+ cx.spawn(|this, mut cx| async move {
+ update_editor_from_message(this, project, message, &mut cx).await
+ })
+ }
- let buffer_ids = message
- .inserted_excerpts
- .iter()
- .filter_map(|insertion| Some(insertion.excerpt.as_ref()?.buffer_id))
- .collect::<HashSet<_>>();
+ fn should_unfollow_on_event(event: &Self::Event, _: &AppContext) -> bool {
+ match event {
+ Event::Edited => true,
+ Event::SelectionsChanged { local } => *local,
+ Event::ScrollPositionChanged { local } => *local,
+ _ => false,
+ }
+ }
+}
- let mut removals = message
- .deleted_excerpts
+async fn update_editor_from_message(
+ this: ViewHandle<Editor>,
+ project: ModelHandle<Project>,
+ message: proto::update_view::Editor,
+ cx: &mut AsyncAppContext,
+) -> Result<()> {
+ // Open all of the buffers of which excerpts were added to the editor.
+ let inserted_excerpt_buffer_ids = message
+ .inserted_excerpts
+ .iter()
+ .filter_map(|insertion| Some(insertion.excerpt.as_ref()?.buffer_id))
+ .collect::<HashSet<_>>();
+ let inserted_excerpt_buffers = project.update(cx, |project, cx| {
+ inserted_excerpt_buffer_ids
.into_iter()
- .map(ExcerptId::from_proto)
- .collect::<Vec<_>>();
- removals.sort_by(|a, b| a.cmp(&b, &multibuffer));
+ .map(|id| project.open_buffer_by_id(id, cx))
+ .collect::<Vec<_>>()
+ });
+ let _inserted_excerpt_buffers = try_join_all(inserted_excerpt_buffers).await?;
+
+ // Update the editor's excerpts.
+ this.update(cx, |editor, cx| {
+ editor.buffer.update(cx, |multibuffer, cx| {
+ let mut removed_excerpt_ids = message
+ .deleted_excerpts
+ .into_iter()
+ .map(ExcerptId::from_proto)
+ .collect::<Vec<_>>();
+ removed_excerpt_ids.sort_by({
+ let multibuffer = multibuffer.read(cx);
+ move |a, b| a.cmp(&b, &multibuffer)
+ });
+
+ let mut insertions = message.inserted_excerpts.into_iter().peekable();
+ while let Some(insertion) = insertions.next() {
+ let Some(excerpt) = insertion.excerpt else { continue };
+ let Some(previous_excerpt_id) = insertion.previous_excerpt_id else { continue };
+ let buffer_id = excerpt.buffer_id;
+ let Some(buffer) = project.read(cx).buffer_for_id(buffer_id, cx) else { continue };
+
+ let adjacent_excerpts = iter::from_fn(|| {
+ let insertion = insertions.peek()?;
+ if insertion.previous_excerpt_id.is_none()
+ && insertion.excerpt.as_ref()?.buffer_id == buffer_id
+ {
+ insertions.next()?.excerpt
+ } else {
+ None
+ }
+ });
+ multibuffer.insert_excerpts_with_ids_after(
+ ExcerptId::from_proto(previous_excerpt_id),
+ buffer,
+ [excerpt]
+ .into_iter()
+ .chain(adjacent_excerpts)
+ .filter_map(|excerpt| {
+ Some((
+ ExcerptId::from_proto(excerpt.id),
+ deserialize_excerpt_range(excerpt)?,
+ ))
+ }),
+ cx,
+ );
+ }
+
+ multibuffer.remove_excerpts(removed_excerpt_ids, cx);
+ });
+ })?;
+
+ // Deserialize the editor state.
+ let (selections, pending_selection, scroll_top_anchor) = this.update(cx, |editor, cx| {
+ let buffer = editor.buffer.read(cx).read(cx);
let selections = message
.selections
.into_iter()
- .filter_map(|selection| deserialize_selection(&multibuffer, selection))
+ .filter_map(|selection| deserialize_selection(&buffer, selection))
.collect::<Vec<_>>();
let pending_selection = message
.pending_selection
- .and_then(|selection| deserialize_selection(&multibuffer, selection));
-
+ .and_then(|selection| deserialize_selection(&buffer, selection));
let scroll_top_anchor = message
.scroll_top_anchor
- .and_then(|anchor| deserialize_anchor(&multibuffer, anchor));
- drop(multibuffer);
-
- let buffers = project.update(cx, |project, cx| {
- buffer_ids
- .into_iter()
- .map(|id| project.open_buffer_by_id(id, cx))
- .collect::<Vec<_>>()
- });
-
- let project = project.clone();
- cx.spawn(|this, mut cx| async move {
- let _buffers = try_join_all(buffers).await?;
- this.update(&mut cx, |this, cx| {
- this.buffer.update(cx, |multibuffer, cx| {
- let mut insertions = message.inserted_excerpts.into_iter().peekable();
- while let Some(insertion) = insertions.next() {
- let Some(excerpt) = insertion.excerpt else { continue };
- let Some(previous_excerpt_id) = insertion.previous_excerpt_id else { continue };
- let buffer_id = excerpt.buffer_id;
- let Some(buffer) = project.read(cx).buffer_for_id(buffer_id, cx) else { continue };
-
- let adjacent_excerpts = iter::from_fn(|| {
- let insertion = insertions.peek()?;
- if insertion.previous_excerpt_id.is_none()
- && insertion.excerpt.as_ref()?.buffer_id == buffer_id
- {
- insertions.next()?.excerpt
- } else {
- None
- }
- });
-
- multibuffer.insert_excerpts_with_ids_after(
- ExcerptId::from_proto(previous_excerpt_id),
- buffer,
- [excerpt]
- .into_iter()
- .chain(adjacent_excerpts)
- .filter_map(|excerpt| {
- Some((
- ExcerptId::from_proto(excerpt.id),
- deserialize_excerpt_range(excerpt)?,
- ))
- }),
- cx,
- );
- }
-
- multibuffer.remove_excerpts(removals, cx);
- });
-
- if !selections.is_empty() || pending_selection.is_some() {
- this.set_selections_from_remote(selections, pending_selection, cx);
- this.request_autoscroll_remotely(Autoscroll::newest(), cx);
- } else if let Some(anchor) = scroll_top_anchor {
- this.set_scroll_anchor_remote(ScrollAnchor {
- top_anchor: anchor,
- offset: vec2f(message.scroll_x, message.scroll_y)
- }, cx);
- }
- })?;
- Ok(())
+ .and_then(|anchor| deserialize_anchor(&buffer, anchor));
+ anyhow::Ok((selections, pending_selection, scroll_top_anchor))
+ })??;
+
+ // Wait until the buffer has received all of the operations referenced by
+ // the editor's new state.
+ this.update(cx, |editor, cx| {
+ editor.buffer.update(cx, |buffer, cx| {
+ buffer.wait_for_anchors(
+ selections
+ .iter()
+ .chain(pending_selection.as_ref())
+ .flat_map(|selection| [selection.start, selection.end])
+ .chain(scroll_top_anchor),
+ cx,
+ )
})
- }
-
- fn should_unfollow_on_event(event: &Self::Event, _: &AppContext) -> bool {
- match event {
- Event::Edited => true,
- Event::SelectionsChanged { local } => *local,
- Event::ScrollPositionChanged { local } => *local,
- _ => false,
+ })?
+ .await?;
+
+ // Update the editor's state.
+ this.update(cx, |editor, cx| {
+ if !selections.is_empty() || pending_selection.is_some() {
+ editor.set_selections_from_remote(selections, pending_selection, cx);
+ editor.request_autoscroll_remotely(Autoscroll::newest(), cx);
+ } else if let Some(scroll_top_anchor) = scroll_top_anchor {
+ editor.set_scroll_anchor_remote(
+ ScrollAnchor {
+ top_anchor: scroll_top_anchor,
+ offset: vec2f(message.scroll_x, message.scroll_y),
+ },
+ cx,
+ );
}
- }
+ })?;
+ Ok(())
}
fn serialize_excerpt(
@@ -516,7 +534,24 @@ impl Item for Editor {
}
}
- fn tab_description<'a>(&'a self, detail: usize, cx: &'a AppContext) -> Option<Cow<'a, str>> {
+ fn tab_tooltip_text(&self, cx: &AppContext) -> Option<Cow<str>> {
+ let file_path = self
+ .buffer()
+ .read(cx)
+ .as_singleton()?
+ .read(cx)
+ .file()
+ .and_then(|f| f.as_local())?
+ .abs_path(cx);
+
+ let file_path = util::paths::compact(&file_path)
+ .to_string_lossy()
+ .to_string();
+
+ Some(file_path.into())
+ }
+
+ fn tab_description<'a>(&'a self, detail: usize, cx: &'a AppContext) -> Option<Cow<str>> {
match path_for_buffer(&self.buffer, detail, true, cx)? {
Cow::Borrowed(path) => Some(path.to_string_lossy()),
Cow::Owned(path) => Some(path.to_string_lossy().to_string().into()),
@@ -1,6 +1,7 @@
mod anchor;
pub use anchor::{Anchor, AnchorRangeExt};
+use anyhow::{anyhow, Result};
use clock::ReplicaId;
use collections::{BTreeMap, Bound, HashMap, HashSet};
use futures::{channel::mpsc, SinkExt};
@@ -16,7 +17,9 @@ use language::{
use std::{
borrow::Cow,
cell::{Ref, RefCell},
- cmp, fmt, io,
+ cmp, fmt,
+ future::Future,
+ io,
iter::{self, FromIterator},
mem,
ops::{Range, RangeBounds, Sub},
@@ -61,6 +64,7 @@ pub enum Event {
},
Edited,
Reloaded,
+ LanguageChanged,
Reparsed,
Saved,
FileHandleChanged,
@@ -1238,6 +1242,39 @@ impl MultiBuffer {
cx.notify();
}
+ pub fn wait_for_anchors<'a>(
+ &self,
+ anchors: impl 'a + Iterator<Item = Anchor>,
+ cx: &mut ModelContext<Self>,
+ ) -> impl 'static + Future<Output = Result<()>> {
+ let borrow = self.buffers.borrow();
+ let mut error = None;
+ let mut futures = Vec::new();
+ for anchor in anchors {
+ if let Some(buffer_id) = anchor.buffer_id {
+ if let Some(buffer) = borrow.get(&buffer_id) {
+ buffer.buffer.update(cx, |buffer, _| {
+ futures.push(buffer.wait_for_anchors([anchor.text_anchor]))
+ });
+ } else {
+ error = Some(anyhow!(
+ "buffer {buffer_id} is not part of this multi-buffer"
+ ));
+ break;
+ }
+ }
+ }
+ async move {
+ if let Some(error) = error {
+ Err(error)?;
+ }
+ for future in futures {
+ future.await?;
+ }
+ Ok(())
+ }
+ }
+
pub fn text_anchor_for_position<T: ToOffset>(
&self,
position: T,
@@ -1266,6 +1303,7 @@ impl MultiBuffer {
language::Event::Saved => Event::Saved,
language::Event::FileHandleChanged => Event::FileHandleChanged,
language::Event::Reloaded => Event::Reloaded,
+ language::Event::LanguageChanged => Event::LanguageChanged,
language::Event::Reparsed => Event::Reparsed,
language::Event::DiagnosticsUpdated => Event::DiagnosticsUpdated,
language::Event::Closed => Event::Closed,
@@ -166,7 +166,7 @@ impl<'a> EditorTestContext<'a> {
///
/// See the `util::test::marked_text_ranges` function for more information.
pub fn set_state(&mut self, marked_text: &str) -> ContextHandle {
- let _state_context = self.add_assertion_context(format!(
+ let state_context = self.add_assertion_context(format!(
"Initial Editor State: \"{}\"",
marked_text.escape_debug().to_string()
));
@@ -177,7 +177,23 @@ impl<'a> EditorTestContext<'a> {
s.select_ranges(selection_ranges)
})
});
- _state_context
+ state_context
+ }
+
+ /// Only change the editor's selections
+ pub fn set_selections_state(&mut self, marked_text: &str) -> ContextHandle {
+ let state_context = self.add_assertion_context(format!(
+ "Initial Editor State: \"{}\"",
+ marked_text.escape_debug().to_string()
+ ));
+ let (unmarked_text, selection_ranges) = marked_text_ranges(marked_text, true);
+ self.editor.update(self.cx, |editor, cx| {
+ assert_eq!(editor.text(cx), unmarked_text);
+ editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
+ s.select_ranges(selection_ranges)
+ })
+ });
+ state_context
}
/// Make an assertion about the editor's text and the ranges and directions
@@ -188,10 +204,11 @@ impl<'a> EditorTestContext<'a> {
pub fn assert_editor_state(&mut self, marked_text: &str) {
let (unmarked_text, expected_selections) = marked_text_ranges(marked_text, true);
let buffer_text = self.buffer_text();
- assert_eq!(
- buffer_text, unmarked_text,
- "Unmarked text doesn't match buffer text"
- );
+
+ if buffer_text != unmarked_text {
+ panic!("Unmarked text doesn't match buffer text\nBuffer text: {buffer_text:?}\nUnmarked text: {unmarked_text:?}\nRaw buffer text\n{buffer_text}Raw unmarked text\n{unmarked_text}");
+ }
+
self.assert_selections(expected_selections, marked_text.to_string())
}
@@ -1,5 +1,6 @@
use std::{
any::TypeId,
+ borrow::Cow,
ops::{Range, RangeInclusive},
sync::Arc,
};
@@ -245,6 +246,10 @@ impl Entity for FeedbackEditor {
}
impl Item for FeedbackEditor {
+ fn tab_tooltip_text(&self, _: &AppContext) -> Option<Cow<str>> {
+ Some("Send Feedback".into())
+ }
+
fn tab_content(&self, _: Option<usize>, style: &theme::Tab, _: &AppContext) -> Element<Pane> {
Flex::row()
.with_child(
@@ -37,7 +37,7 @@ pub struct TooltipStyle {
pub container: ContainerStyle,
pub text: TextStyle,
keystroke: KeystrokeStyle,
- pub max_text_width: f32,
+ pub max_text_width: Option<f32>,
}
#[derive(Clone, Deserialize, Default)]
@@ -135,9 +135,14 @@ impl<V: View> Tooltip<V> {
) -> impl Drawable<V> {
Flex::row()
.with_child({
- let text = Text::new(text, style.text)
- .constrained()
- .with_max_width(style.max_text_width);
+ let text = if let Some(max_text_width) = style.max_text_width {
+ Text::new(text, style.text)
+ .constrained()
+ .with_max_width(max_text_width)
+ } else {
+ Text::new(text, style.text).constrained()
+ };
+
if measure {
text.flex(1., false).boxed()
} else {
@@ -46,7 +46,7 @@ pub fn new_journal_entry(app_state: Arc<AppState>, cx: &mut AppContext) {
cx.spawn(|mut cx| async move {
let (journal_dir, entry_path) = create_entry.await?;
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?;
let opened = workspace
@@ -187,6 +187,7 @@ pub enum Event {
Saved,
FileHandleChanged,
Reloaded,
+ LanguageChanged,
Reparsed,
DiagnosticsUpdated,
Closed,
@@ -536,6 +537,7 @@ impl Buffer {
self.syntax_map.lock().clear();
self.language = language;
self.reparse(cx);
+ cx.emit(Event::LanguageChanged);
}
pub fn set_language_registry(&mut self, language_registry: Arc<LanguageRegistry>) {
@@ -1313,10 +1315,10 @@ impl Buffer {
self.text.wait_for_edits(edit_ids)
}
- pub fn wait_for_anchors<'a>(
+ pub fn wait_for_anchors(
&mut self,
- anchors: impl IntoIterator<Item = &'a Anchor>,
- ) -> impl Future<Output = Result<()>> {
+ anchors: impl IntoIterator<Item = Anchor>,
+ ) -> impl 'static + Future<Output = Result<()>> {
self.text.wait_for_anchors(anchors)
}
@@ -81,14 +81,14 @@ fn test_select_language() {
// matching file extension
assert_eq!(
registry
- .language_for_path("zed/lib.rs")
+ .language_for_file("zed/lib.rs", None)
.now_or_never()
.and_then(|l| Some(l.ok()?.name())),
Some("Rust".into())
);
assert_eq!(
registry
- .language_for_path("zed/lib.mk")
+ .language_for_file("zed/lib.mk", None)
.now_or_never()
.and_then(|l| Some(l.ok()?.name())),
Some("Make".into())
@@ -97,7 +97,7 @@ fn test_select_language() {
// matching filename
assert_eq!(
registry
- .language_for_path("zed/Makefile")
+ .language_for_file("zed/Makefile", None)
.now_or_never()
.and_then(|l| Some(l.ok()?.name())),
Some("Make".into())
@@ -106,21 +106,21 @@ fn test_select_language() {
// matching suffix that is not the full file extension or filename
assert_eq!(
registry
- .language_for_path("zed/cars")
+ .language_for_file("zed/cars", None)
.now_or_never()
.and_then(|l| Some(l.ok()?.name())),
None
);
assert_eq!(
registry
- .language_for_path("zed/a.cars")
+ .language_for_file("zed/a.cars", None)
.now_or_never()
.and_then(|l| Some(l.ok()?.name())),
None
);
assert_eq!(
registry
- .language_for_path("zed/sumk")
+ .language_for_file("zed/sumk", None)
.now_or_never()
.and_then(|l| Some(l.ok()?.name())),
None
@@ -262,6 +262,8 @@ pub struct LanguageConfig {
pub name: Arc<str>,
pub path_suffixes: Vec<String>,
pub brackets: BracketPairConfig,
+ #[serde(default, deserialize_with = "deserialize_regex")]
+ pub first_line_pattern: Option<Regex>,
#[serde(default = "auto_indent_using_last_non_empty_line_default")]
pub auto_indent_using_last_non_empty_line: bool,
#[serde(default, deserialize_with = "deserialize_regex")]
@@ -334,6 +336,7 @@ impl Default for LanguageConfig {
path_suffixes: Default::default(),
brackets: Default::default(),
auto_indent_using_last_non_empty_line: auto_indent_using_last_non_empty_line_default(),
+ first_line_pattern: Default::default(),
increase_indent_pattern: Default::default(),
decrease_indent_pattern: Default::default(),
autoclose_before: Default::default(),
@@ -660,19 +663,30 @@ impl LanguageRegistry {
})
}
- pub fn language_for_path(
+ pub fn language_for_file(
self: &Arc<Self>,
path: impl AsRef<Path>,
+ content: Option<&Rope>,
) -> UnwrapFuture<oneshot::Receiver<Result<Arc<Language>>>> {
let path = path.as_ref();
let filename = path.file_name().and_then(|name| name.to_str());
let extension = path.extension().and_then(|name| name.to_str());
let path_suffixes = [extension, filename];
self.get_or_load_language(|config| {
- config
+ let path_matches = config
.path_suffixes
.iter()
- .any(|suffix| path_suffixes.contains(&Some(suffix.as_str())))
+ .any(|suffix| path_suffixes.contains(&Some(suffix.as_str())));
+ let content_matches = content.zip(config.first_line_pattern.as_ref()).map_or(
+ false,
+ |(content, pattern)| {
+ let end = content.clip_point(Point::new(0, 256), Bias::Left);
+ let end = content.point_to_offset(end);
+ let text = content.chunks_in_range(0..end).collect::<String>();
+ pattern.is_match(&text)
+ },
+ );
+ path_matches || content_matches
})
}
@@ -1528,9 +1542,45 @@ pub fn range_from_lsp(range: lsp::Range) -> Range<Unclipped<PointUtf16>> {
#[cfg(test)]
mod tests {
+ use super::*;
use gpui::TestAppContext;
- use super::*;
+ #[gpui::test(iterations = 10)]
+ async fn test_first_line_pattern(cx: &mut TestAppContext) {
+ let mut languages = LanguageRegistry::test();
+ languages.set_executor(cx.background());
+ let languages = Arc::new(languages);
+ languages.register(
+ "/javascript",
+ LanguageConfig {
+ name: "JavaScript".into(),
+ path_suffixes: vec!["js".into()],
+ first_line_pattern: Some(Regex::new(r"\bnode\b").unwrap()),
+ ..Default::default()
+ },
+ tree_sitter_javascript::language(),
+ None,
+ |_| Default::default(),
+ );
+
+ languages
+ .language_for_file("the/script", None)
+ .await
+ .unwrap_err();
+ languages
+ .language_for_file("the/script", Some(&"nothing".into()))
+ .await
+ .unwrap_err();
+ assert_eq!(
+ languages
+ .language_for_file("the/script", Some(&"#!/bin/env node".into()))
+ .await
+ .unwrap()
+ .name()
+ .as_ref(),
+ "JavaScript"
+ );
+ }
#[gpui::test(iterations = 10)]
async fn test_language_loading(cx: &mut TestAppContext) {
@@ -187,8 +187,6 @@ impl<D: PickerDelegate> Picker<D> {
confirmed: false,
pending_update_matches: Task::ready(None),
};
- // TODO! How can the delegate notify the picker to update?
- // cx.observe(&delegate, |_, _, cx| cx.notify()).detach();
this.update_matches(String::new(), cx);
this
}
@@ -19,6 +19,7 @@ test-support = [
[dependencies]
text = { path = "../text" }
+copilot = { path = "../copilot" }
client = { path = "../client" }
clock = { path = "../clock" }
collections = { path = "../collections" }
@@ -572,7 +572,7 @@ async fn location_links_from_proto(
.and_then(deserialize_anchor)
.ok_or_else(|| anyhow!("missing origin end"))?;
buffer
- .update(&mut cx, |buffer, _| buffer.wait_for_anchors([&start, &end]))
+ .update(&mut cx, |buffer, _| buffer.wait_for_anchors([start, end]))
.await?;
Some(Location {
buffer,
@@ -597,7 +597,7 @@ async fn location_links_from_proto(
.and_then(deserialize_anchor)
.ok_or_else(|| anyhow!("missing target end"))?;
buffer
- .update(&mut cx, |buffer, _| buffer.wait_for_anchors([&start, &end]))
+ .update(&mut cx, |buffer, _| buffer.wait_for_anchors([start, end]))
.await?;
let target = Location {
buffer,
@@ -868,7 +868,7 @@ impl LspCommand for GetReferences {
.and_then(deserialize_anchor)
.ok_or_else(|| anyhow!("missing target end"))?;
target_buffer
- .update(&mut cx, |buffer, _| buffer.wait_for_anchors([&start, &end]))
+ .update(&mut cx, |buffer, _| buffer.wait_for_anchors([start, end]))
.await?;
locations.push(Location {
buffer: target_buffer,
@@ -1012,7 +1012,7 @@ impl LspCommand for GetDocumentHighlights {
.and_then(deserialize_anchor)
.ok_or_else(|| anyhow!("missing target end"))?;
buffer
- .update(&mut cx, |buffer, _| buffer.wait_for_anchors([&start, &end]))
+ .update(&mut cx, |buffer, _| buffer.wait_for_anchors([start, end]))
.await?;
let kind = match proto::document_highlight::Kind::from_i32(highlight.kind) {
Some(proto::document_highlight::Kind::Text) => DocumentHighlightKind::TEXT,
@@ -12,6 +12,7 @@ use anyhow::{anyhow, Context, Result};
use client::{proto, Client, TypedEnvelope, UserStore};
use clock::ReplicaId;
use collections::{hash_map, BTreeMap, HashMap, HashSet};
+use copilot::Copilot;
use futures::{
channel::mpsc::{self, UnboundedReceiver},
future::{try_join_all, Shared},
@@ -129,6 +130,7 @@ pub struct Project {
_maintain_buffer_languages: Task<()>,
_maintain_workspace_config: Task<()>,
terminals: Terminals,
+ copilot_enabled: bool,
}
enum BufferMessage {
@@ -472,6 +474,7 @@ impl Project {
terminals: Terminals {
local_handles: Vec::new(),
},
+ copilot_enabled: Copilot::global(cx).is_some(),
}
})
}
@@ -559,6 +562,7 @@ impl Project {
terminals: Terminals {
local_handles: Vec::new(),
},
+ copilot_enabled: Copilot::global(cx).is_some(),
};
for worktree in worktrees {
let _ = this.add_worktree(&worktree, cx);
@@ -664,6 +668,15 @@ impl Project {
self.start_language_server(worktree_id, worktree_path, language, cx);
}
+ if !self.copilot_enabled && Copilot::global(cx).is_some() {
+ self.copilot_enabled = true;
+ for buffer in self.opened_buffers.values() {
+ if let Some(buffer) = buffer.upgrade(cx) {
+ self.register_buffer_with_copilot(&buffer, cx);
+ }
+ }
+ }
+
cx.notify();
}
@@ -1616,6 +1629,7 @@ impl Project {
self.detect_language_for_buffer(buffer, cx);
self.register_buffer_with_language_server(buffer, cx);
+ self.register_buffer_with_copilot(buffer, cx);
cx.observe_release(buffer, |this, buffer, cx| {
if let Some(file) = File::from_dyn(buffer.file()) {
if file.is_local() {
@@ -1731,6 +1745,16 @@ impl Project {
});
}
+ fn register_buffer_with_copilot(
+ &self,
+ buffer_handle: &ModelHandle<Buffer>,
+ cx: &mut ModelContext<Self>,
+ ) {
+ if let Some(copilot) = Copilot::global(cx) {
+ copilot.update(cx, |copilot, cx| copilot.register_buffer(buffer_handle, cx));
+ }
+ }
+
async fn send_buffer_messages(
this: WeakModelHandle<Self>,
rx: UnboundedReceiver<BufferMessage>,
@@ -2013,17 +2037,19 @@ impl Project {
fn detect_language_for_buffer(
&mut self,
- buffer: &ModelHandle<Buffer>,
+ buffer_handle: &ModelHandle<Buffer>,
cx: &mut ModelContext<Self>,
) -> Option<()> {
// 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 buffer = buffer_handle.read(cx);
+ let full_path = buffer.file()?.full_path(cx);
+ let content = buffer.as_rope();
let new_language = self
.languages
- .language_for_path(&full_path)
+ .language_for_file(&full_path, Some(content))
.now_or_never()?
.ok()?;
- self.set_language_for_buffer(buffer, new_language, cx);
+ self.set_language_for_buffer(buffer_handle, new_language, cx);
None
}
@@ -2434,26 +2460,23 @@ impl Project {
buffers: impl IntoIterator<Item = ModelHandle<Buffer>>,
cx: &mut ModelContext<Self>,
) -> Option<()> {
- let language_server_lookup_info: HashSet<(WorktreeId, Arc<Path>, PathBuf)> = buffers
+ let language_server_lookup_info: HashSet<(WorktreeId, Arc<Path>, Arc<Language>)> = buffers
.into_iter()
.filter_map(|buffer| {
- let file = File::from_dyn(buffer.read(cx).file())?;
+ let buffer = buffer.read(cx);
+ let file = File::from_dyn(buffer.file())?;
let worktree = file.worktree.read(cx).as_local()?;
- let worktree_id = worktree.id();
- let worktree_abs_path = worktree.abs_path().clone();
let full_path = file.full_path(cx);
- Some((worktree_id, worktree_abs_path, full_path))
+ let language = self
+ .languages
+ .language_for_file(&full_path, Some(buffer.as_rope()))
+ .now_or_never()?
+ .ok()?;
+ Some((worktree.id(), worktree.abs_path().clone(), language))
})
.collect();
- for (worktree_id, worktree_abs_path, full_path) in language_server_lookup_info {
- if let Some(language) = self
- .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);
- }
+ for (worktree_id, worktree_abs_path, language) in language_server_lookup_info {
+ self.restart_language_server(worktree_id, worktree_abs_path, language, cx);
}
None
@@ -3487,7 +3510,7 @@ impl Project {
let adapter_language = adapter_language.clone();
let language = this
.languages
- .language_for_path(&project_path.path)
+ .language_for_file(&project_path.path, None)
.unwrap_or_else(move |_| adapter_language);
let language_server_name = adapter.name.clone();
Some(async move {
@@ -5916,7 +5939,10 @@ impl Project {
worktree_id,
path: PathBuf::from(serialized_symbol.path).into(),
};
- let language = languages.language_for_path(&path.path).await.log_err();
+ let language = languages
+ .language_for_file(&path.path, None)
+ .await
+ .log_err();
Ok(Symbol {
language_server_name: LanguageServerName(
serialized_symbol.language_server_name.into(),
@@ -141,7 +141,7 @@ impl PickerDelegate for RecentProjectsDelegate {
fn confirm(&mut self, cx: &mut ViewContext<RecentProjects>) {
if let Some(selected_match) = &self.matches.get(self.selected_index()) {
let workspace_location = &self.workspace_locations[selected_match.candidate_id];
- cx.dispatch_global_action(OpenPaths {
+ cx.dispatch_action(OpenPaths {
paths: workspace_location.paths().as_ref().clone(),
});
cx.emit(PickerEvent::Dismiss);
@@ -21,6 +21,7 @@ use settings::Settings;
use smallvec::SmallVec;
use std::{
any::{Any, TypeId},
+ borrow::Cow,
mem,
ops::Range,
path::PathBuf,
@@ -224,6 +225,10 @@ impl View for ProjectSearchView {
}
impl Item for ProjectSearchView {
+ fn tab_tooltip_text(&self, cx: &AppContext) -> Option<Cow<str>> {
+ Some(self.query_editor.read(cx).text(cx).into())
+ }
+
fn act_as_type<'a>(
&'a self,
type_id: TypeId,
@@ -28,11 +28,11 @@ pub use watched_json::watch_files;
#[derive(Clone)]
pub struct Settings {
+ pub features: Features,
pub buffer_font_family_name: String,
pub buffer_font_features: fonts::Features,
pub buffer_font_family: FamilyId,
pub default_buffer_font_size: f32,
- pub enable_copilot_integration: bool,
pub buffer_font_size: f32,
pub active_pane_magnification: f32,
pub cursor_blink: bool,
@@ -177,43 +177,7 @@ pub struct EditorSettings {
pub ensure_final_newline_on_save: Option<bool>,
pub formatter: Option<Formatter>,
pub enable_language_server: Option<bool>,
- #[schemars(skip)]
- pub copilot: Option<OnOff>,
-}
-
-#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
-#[serde(rename_all = "snake_case")]
-pub enum OnOff {
- On,
- Off,
-}
-
-impl OnOff {
- pub fn as_bool(&self) -> bool {
- match self {
- OnOff::On => true,
- OnOff::Off => false,
- }
- }
-
- pub fn from_bool(value: bool) -> OnOff {
- match value {
- true => OnOff::On,
- false => OnOff::Off,
- }
- }
-}
-
-impl From<OnOff> for bool {
- fn from(value: OnOff) -> bool {
- value.as_bool()
- }
-}
-
-impl From<bool> for OnOff {
- fn from(value: bool) -> OnOff {
- OnOff::from_bool(value)
- }
+ pub show_copilot_suggestions: Option<bool>,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
@@ -437,8 +401,7 @@ pub struct SettingsFileContent {
#[serde(default)]
pub base_keymap: Option<BaseKeymap>,
#[serde(default)]
- #[schemars(skip)]
- pub enable_copilot_integration: Option<bool>,
+ pub features: FeaturesContent,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
@@ -447,6 +410,18 @@ pub struct LspSettings {
pub initialization_options: Option<Value>,
}
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub struct Features {
+ pub copilot: bool,
+}
+
+#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub struct FeaturesContent {
+ pub copilot: Option<bool>,
+}
+
impl Settings {
/// Fill out the settings corresponding to the default.json file, overrides will be set later
pub fn defaults(
@@ -500,7 +475,7 @@ impl Settings {
format_on_save: required(defaults.editor.format_on_save),
formatter: required(defaults.editor.formatter),
enable_language_server: required(defaults.editor.enable_language_server),
- copilot: required(defaults.editor.copilot),
+ show_copilot_suggestions: required(defaults.editor.show_copilot_suggestions),
},
editor_overrides: Default::default(),
git: defaults.git.unwrap(),
@@ -517,7 +492,9 @@ impl Settings {
telemetry_overrides: Default::default(),
auto_update: defaults.auto_update.unwrap(),
base_keymap: Default::default(),
- enable_copilot_integration: defaults.enable_copilot_integration.unwrap(),
+ features: Features {
+ copilot: defaults.features.copilot.unwrap(),
+ },
}
}
@@ -569,10 +546,7 @@ impl Settings {
merge(&mut self.autosave, data.autosave);
merge(&mut self.default_dock_anchor, data.default_dock_anchor);
merge(&mut self.base_keymap, data.base_keymap);
- merge(
- &mut self.enable_copilot_integration,
- data.enable_copilot_integration,
- );
+ merge(&mut self.features.copilot, data.features.copilot);
self.editor_overrides = data.editor;
self.git_overrides = data.git.unwrap_or_default();
@@ -596,12 +570,15 @@ impl Settings {
self
}
- pub fn copilot_on(&self, language: Option<&str>) -> bool {
- if self.enable_copilot_integration {
- self.language_setting(language, |settings| settings.copilot.map(Into::into))
- } else {
- false
- }
+ pub fn features(&self) -> &Features {
+ &self.features
+ }
+
+ pub fn show_copilot_suggestions(&self, language: Option<&str>) -> bool {
+ self.features.copilot
+ && self.language_setting(language, |settings| {
+ settings.show_copilot_suggestions.map(Into::into)
+ })
}
pub fn tab_size(&self, language: Option<&str>) -> NonZeroU32 {
@@ -740,7 +717,7 @@ impl Settings {
format_on_save: Some(FormatOnSave::On),
formatter: Some(Formatter::LanguageServer),
enable_language_server: Some(true),
- copilot: Some(OnOff::On),
+ show_copilot_suggestions: Some(true),
},
editor_overrides: Default::default(),
journal_defaults: Default::default(),
@@ -760,7 +737,7 @@ impl Settings {
telemetry_overrides: Default::default(),
auto_update: true,
base_keymap: Default::default(),
- enable_copilot_integration: true,
+ features: Features { copilot: true },
}
}
@@ -1125,7 +1102,7 @@ mod tests {
{
"language_overrides": {
"JSON": {
- "copilot": "off"
+ "show_copilot_suggestions": false
}
}
}
@@ -1135,7 +1112,7 @@ mod tests {
settings.languages.insert(
"Rust".into(),
EditorSettings {
- copilot: Some(OnOff::On),
+ show_copilot_suggestions: Some(true),
..Default::default()
},
);
@@ -1144,10 +1121,10 @@ mod tests {
{
"language_overrides": {
"Rust": {
- "copilot": "on"
+ "show_copilot_suggestions": true
},
"JSON": {
- "copilot": "off"
+ "show_copilot_suggestions": false
}
}
}
@@ -1163,21 +1140,21 @@ mod tests {
{
"languages": {
"JSON": {
- "copilot": "off"
+ "show_copilot_suggestions": false
}
}
}
"#
.unindent(),
|settings| {
- settings.editor.copilot = Some(OnOff::On);
+ settings.editor.show_copilot_suggestions = Some(true);
},
r#"
{
- "copilot": "on",
+ "show_copilot_suggestions": true,
"languages": {
"JSON": {
- "copilot": "off"
+ "show_copilot_suggestions": false
}
}
}
@@ -1187,13 +1164,13 @@ mod tests {
}
#[test]
- fn test_update_langauge_copilot() {
+ fn test_update_language_copilot() {
assert_new_settings(
r#"
{
"languages": {
"JSON": {
- "copilot": "off"
+ "show_copilot_suggestions": false
}
}
}
@@ -1203,7 +1180,7 @@ mod tests {
settings.languages.insert(
"Rust".into(),
EditorSettings {
- copilot: Some(OnOff::On),
+ show_copilot_suggestions: Some(true),
..Default::default()
},
);
@@ -1212,10 +1189,10 @@ mod tests {
{
"languages": {
"Rust": {
- "copilot": "on"
+ "show_copilot_suggestions": true
},
"JSON": {
- "copilot": "off"
+ "show_copilot_suggestions": false
}
}
}
@@ -3,6 +3,7 @@ pub mod terminal_button;
pub mod terminal_element;
use std::{
+ borrow::Cow,
ops::RangeInclusive,
path::{Path, PathBuf},
time::Duration,
@@ -541,6 +542,10 @@ impl View for TerminalView {
}
impl Item for TerminalView {
+ fn tab_tooltip_text(&self, cx: &AppContext) -> Option<Cow<str>> {
+ Some(self.terminal().read(cx).title().into())
+ }
+
fn tab_content(
&self,
_detail: Option<usize>,
@@ -1331,15 +1331,15 @@ impl Buffer {
}
}
- pub fn wait_for_anchors<'a>(
+ pub fn wait_for_anchors(
&mut self,
- anchors: impl IntoIterator<Item = &'a Anchor>,
+ anchors: impl IntoIterator<Item = Anchor>,
) -> impl 'static + Future<Output = Result<()>> {
let mut futures = Vec::new();
for anchor in anchors {
if !self.version.observed(anchor.timestamp)
- && *anchor != Anchor::MAX
- && *anchor != Anchor::MIN
+ && anchor != Anchor::MAX
+ && anchor != Anchor::MIN
{
let (tx, rx) = oneshot::channel();
self.edit_id_resolvers
@@ -1,6 +1,6 @@
mod base_keymap_picker;
-use std::sync::Arc;
+use std::{borrow::Cow, sync::Arc};
use db::kvp::KEY_VALUE_STORE;
use gpui::{
@@ -198,6 +198,10 @@ impl WelcomePage {
}
impl Item for WelcomePage {
+ fn tab_tooltip_text(&self, _: &AppContext) -> Option<Cow<str>> {
+ Some("Welcome to Zed!".into())
+ }
+
fn tab_content(
&self,
_detail: Option<usize>,
@@ -48,7 +48,10 @@ pub trait Item: View {
fn navigate(&mut self, _: Box<dyn Any>, _: &mut ViewContext<Self>) -> bool {
false
}
- fn tab_description<'a>(&'a self, _: usize, _: &'a AppContext) -> Option<Cow<'a, str>> {
+ fn tab_tooltip_text(&self, _: &AppContext) -> Option<Cow<str>> {
+ None
+ }
+ fn tab_description<'a>(&'a self, _: usize, _: &'a AppContext) -> Option<Cow<str>> {
None
}
fn tab_content(
@@ -170,7 +173,8 @@ pub trait ItemHandle: 'static + fmt::Debug {
cx: &mut WindowContext,
handler: Box<dyn Fn(ItemEvent, &mut WindowContext)>,
) -> gpui::Subscription;
- fn tab_description<'a>(&self, detail: usize, cx: &'a AppContext) -> Option<Cow<'a, str>>;
+ fn tab_tooltip_text<'a>(&self, cx: &'a AppContext) -> Option<Cow<'a, str>>;
+ fn tab_description<'a>(&'a self, detail: usize, cx: &'a AppContext) -> Option<Cow<'a, str>>;
fn tab_content(
&self,
detail: Option<usize>,
@@ -260,7 +264,11 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
})
}
- fn tab_description<'a>(&self, detail: usize, cx: &'a AppContext) -> Option<Cow<'a, str>> {
+ fn tab_tooltip_text<'a>(&self, cx: &'a AppContext) -> Option<Cow<'a, str>> {
+ self.read(cx).tab_tooltip_text(cx)
+ }
+
+ fn tab_description<'a>(&'a self, detail: usize, cx: &'a AppContext) -> Option<Cow<'a, str>> {
self.read(cx).tab_description(detail, cx)
}
@@ -912,7 +920,7 @@ pub(crate) mod test {
}
impl Item for TestItem {
- fn tab_description<'a>(&'a self, detail: usize, _: &'a AppContext) -> Option<Cow<'a, str>> {
+ fn tab_description(&self, detail: usize, _: &AppContext) -> Option<Cow<str>> {
self.tab_descriptions.as_ref().and_then(|descriptions| {
let description = *descriptions.get(detail).or_else(|| descriptions.last())?;
Some(description.into())
@@ -1389,6 +1389,9 @@ impl Pane {
let detail = detail.clone();
let theme = cx.global::<Settings>().theme.clone();
+ let mut tooltip_theme = theme.tooltip.clone();
+ tooltip_theme.max_text_width = None;
+ let tab_tooltip_text = item.tab_tooltip_text(cx).map(|a| a.to_string());
move |mouse_state, cx| {
let tab_style =
@@ -1396,39 +1399,56 @@ impl Pane {
let hovered = mouse_state.hovered();
enum Tab {}
- MouseEventHandler::<Tab, Pane>::new(ix, cx, |_, cx| {
- Self::render_tab::<Pane>(
- &item,
- pane.clone(),
- ix == 0,
- detail,
- hovered,
- tab_style,
- cx,
- )
- })
- .on_down(MouseButton::Left, move |_, _, cx| {
- cx.dispatch_action(ActivateItem(ix));
- })
- .on_click(MouseButton::Middle, {
- let item = item.clone();
- let pane = pane.clone();
- move |_, _, cx| {
- cx.dispatch_action(CloseItemById {
- item_id: item.id(),
- pane: pane.clone(),
- })
- }
- })
- .on_down(MouseButton::Right, move |e, _, cx| {
- let item = item.clone();
- cx.dispatch_action(DeployTabContextMenu {
- position: e.position,
- item_id: item.id(),
- pane: pane.clone(),
- });
- })
- .boxed()
+ let mouse_event_handler =
+ MouseEventHandler::<Tab, Pane>::new(ix, cx, |_, cx| {
+ Self::render_tab::<Pane>(
+ &item,
+ pane.clone(),
+ ix == 0,
+ detail,
+ hovered,
+ tab_style,
+ cx,
+ )
+ })
+ .on_down(MouseButton::Left, move |_, _, cx| {
+ cx.dispatch_action(ActivateItem(ix));
+ })
+ .on_click(MouseButton::Middle, {
+ let item = item.clone();
+ let pane = pane.clone();
+ move |_, _, cx| {
+ cx.dispatch_action(CloseItemById {
+ item_id: item.id(),
+ pane: pane.clone(),
+ })
+ }
+ })
+ .on_down(
+ MouseButton::Right,
+ move |e, _, cx| {
+ let item = item.clone();
+ cx.dispatch_action(DeployTabContextMenu {
+ position: e.position,
+ item_id: item.id(),
+ pane: pane.clone(),
+ });
+ },
+ );
+
+ if let Some(tab_tooltip_text) = tab_tooltip_text {
+ return mouse_event_handler
+ .with_tooltip::<Self>(
+ ix,
+ tab_tooltip_text,
+ None,
+ tooltip_theme,
+ cx,
+ )
+ .boxed();
+ }
+
+ mouse_event_handler.boxed()
}
});
@@ -14,7 +14,10 @@ use gpui::{
};
use settings::Settings;
use smallvec::SmallVec;
-use std::sync::{Arc, Weak};
+use std::{
+ borrow::Cow,
+ sync::{Arc, Weak},
+};
pub enum Event {
Close,
@@ -94,6 +97,9 @@ impl View for SharedScreen {
}
impl Item for SharedScreen {
+ fn tab_tooltip_text(&self, _: &AppContext) -> Option<Cow<str>> {
+ Some(format!("{}'s screen", self.user.github_login).into())
+ }
fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
if let Some(nav_history) = self.nav_history.as_ref() {
nav_history.push::<()>(None, cx);
@@ -70,6 +70,7 @@ impl View for Toolbar {
for (item, position) in &self.items {
match *position {
ToolbarItemLocation::Hidden => {}
+
ToolbarItemLocation::PrimaryLeft { flex } => {
let left_item = ChildView::new(item.as_any(), cx)
.aligned()
@@ -81,6 +82,7 @@ impl View for Toolbar {
primary_left_items.push(left_item.boxed());
}
}
+
ToolbarItemLocation::PrimaryRight { flex } => {
let right_item = ChildView::new(item.as_any(), cx)
.aligned()
@@ -93,6 +95,7 @@ impl View for Toolbar {
primary_right_items.push(right_item.boxed());
}
}
+
ToolbarItemLocation::Secondary => {
secondary_item = Some(
ChildView::new(item.as_any(), cx)
@@ -300,7 +303,10 @@ impl<T: ToolbarItemView> ToolbarItemViewHandle for ViewHandle<T> {
}
fn pane_focus_update(&mut self, pane_focused: bool, cx: &mut WindowContext) {
- self.update(cx, |this, cx| this.pane_focus_update(pane_focused, cx));
+ self.update(cx, |this, cx| {
+ this.pane_focus_update(pane_focused, cx);
+ cx.notify();
+ });
}
}
@@ -290,7 +290,7 @@ pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
let app_state = Arc::downgrade(&app_state);
move |action: &OpenPaths, cx: &mut AppContext| {
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();
}
}
});
@@ -303,15 +303,28 @@ pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
}
let app_state = app_state.upgrade()?;
+ let window_id = cx.window_id();
let action = action.clone();
- let close = workspace.prepare_to_close(false, cx);
+ let is_remote = workspace.project.read(cx).is_remote();
+ let has_worktree = workspace.project.read(cx).worktrees(cx).next().is_some();
+ let has_dirty_items = workspace.items(cx).any(|item| item.is_dirty(cx));
+ let close_task = if is_remote || has_worktree || has_dirty_items {
+ None
+ } else {
+ Some(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, cx))
- .await?;
- }
+ let window_id_to_replace = if let Some(close_task) = close_task {
+ if !close_task.await? {
+ return Ok(());
+ }
+ Some(window_id)
+ } else {
+ None
+ };
+ cx.update(|cx| open_paths(&action.paths, &app_state, window_id_to_replace, cx))
+ .await?;
Ok(())
}))
}
@@ -854,6 +867,7 @@ impl Workspace {
fn new_local(
abs_paths: Vec<PathBuf>,
app_state: Arc<AppState>,
+ requesting_window_id: Option<usize>,
cx: &mut AppContext,
) -> Task<(
ViewHandle<Workspace>,
@@ -868,7 +882,8 @@ impl Workspace {
);
cx.spawn(|mut cx| async move {
- let serialized_workspace = persistence::DB.workspace_for_roots(&abs_paths.as_slice());
+ let mut serialized_workspace =
+ persistence::DB.workspace_for_roots(&abs_paths.as_slice());
let paths_to_open = serialized_workspace
.as_ref()
@@ -915,7 +930,7 @@ impl Workspace {
let mut workspace = Workspace::new(
serialized_workspace,
workspace_id,
- project_handle,
+ project_handle.clone(),
app_state.dock_default_item_factory,
app_state.background_actions,
cx,
@@ -924,46 +939,54 @@ impl Workspace {
workspace
};
- let workspace = {
- 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;
+ let workspace = requesting_window_id
+ .and_then(|window_id| {
+ cx.update(|cx| {
+ cx.replace_root_view(window_id, |cx| {
+ build_workspace(cx, serialized_workspace.take())
+ })
+ })
+ })
+ .unwrap_or_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()
- };
+ 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
- };
+ // 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);
@@ -1056,7 +1079,7 @@ impl Workspace {
if self.project.read(cx).is_local() {
Task::Ready(Some(Ok(callback(self, cx))))
} 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 {
let (workspace, _) = task.await;
workspace.update(&mut cx, callback)
@@ -3025,6 +3048,7 @@ pub async fn last_opened_workspace_paths() -> Option<WorkspaceLocation> {
pub fn open_paths(
abs_paths: &[PathBuf],
app_state: &Arc<AppState>,
+ requesting_window_id: Option<usize>,
cx: &mut AppContext,
) -> Task<
Result<(
@@ -3057,7 +3081,8 @@ pub fn open_paths(
.contains(&false);
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 {
let (workspace, items) = task.await;
@@ -3081,7 +3106,7 @@ pub fn open_new(
cx: &mut AppContext,
init: impl FnOnce(&mut Workspace, &mut ViewContext<Workspace>) + 'static,
) -> Task<()> {
- let task = Workspace::new_local(Vec::new(), app_state.clone(), cx);
+ let task = Workspace::new_local(Vec::new(), app_state.clone(), None, cx);
cx.spawn(|mut cx| async move {
let (workspace, opened_paths) = task.await;
@@ -1,5 +1,6 @@
name = "JavaScript"
path_suffixes = ["js", "jsx", "mjs"]
+first_line_pattern = '^#!.*\bnode\b'
line_comment = "// "
autoclose_before = ";:.,=}])>"
brackets = [
@@ -1,5 +1,6 @@
name = "Python"
path_suffixes = ["py", "pyi"]
+first_line_pattern = '^#!.*\bpython[0-9.]*\b'
line_comment = "# "
autoclose_before = ";:.,=}])>"
brackets = [
@@ -1,5 +1,6 @@
name = "Ruby"
path_suffixes = ["rb", "Gemfile"]
+first_line_pattern = '^#!.*\bruby\b'
line_comment = "# "
autoclose_before = ";:.,=}])>"
brackets = [
@@ -219,7 +219,7 @@ fn main() {
cx.spawn(|cx| handle_cli_connection(connection, app_state.clone(), cx))
.detach();
} 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();
} else {
cx.spawn({
@@ -243,7 +243,7 @@ fn main() {
let app_state = app_state.clone();
async move {
while let Some(paths) = open_paths_rx.next().await {
- cx.update(|cx| workspace::open_paths(&paths, &app_state, cx))
+ cx.update(|cx| workspace::open_paths(&paths, &app_state, None, cx))
.detach();
}
}
@@ -609,7 +609,7 @@ async fn handle_cli_connection(
let mut errored = false;
match cx
- .update(|cx| workspace::open_paths(&paths, &app_state, cx))
+ .update(|cx| workspace::open_paths(&paths, &app_state, None, cx))
.await
{
Ok((workspace, items)) => {
@@ -702,6 +702,7 @@ mod tests {
open_paths(
&[PathBuf::from("/root/a"), PathBuf::from("/root/b")],
&app_state,
+ None,
cx,
)
})
@@ -709,7 +710,7 @@ mod tests {
.unwrap();
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
.unwrap();
assert_eq!(cx.window_ids().len(), 1);
@@ -728,6 +729,7 @@ mod tests {
open_paths(
&[PathBuf::from("/root/b"), PathBuf::from("/root/c")],
&app_state,
+ None,
cx,
)
})
@@ -735,16 +737,36 @@ mod tests {
.unwrap();
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
.unwrap();
- assert_eq!(cx.window_ids().len(), 3);
+ assert_eq!(cx.window_ids().len(), 2);
+ let workspace_1 = cx
+ .read_window(cx.window_ids()[0], |cx| cx.root_view().clone())
+ .unwrap()
+ .clone()
+ .downcast::<Workspace>()
+ .unwrap();
+ workspace_1.update(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]
@@ -756,7 +778,7 @@ mod tests {
.insert_tree("/root", json!({"a": "hey"}))
.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
.unwrap();
assert_eq!(cx.window_ids().len(), 1);
@@ -799,7 +821,7 @@ mod tests {
assert!(!cx.is_window_edited(workspace.window_id()));
// 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
.unwrap();
let editor = workspace.read_with(cx, |workspace, cx| {
@@ -56,6 +56,8 @@ async function main() {
headers: {"Content-Type": "application/json"},
body: JSON.stringify(body)
})
+
+ process.exit(1)
}
function randomU64() {
@@ -44,9 +44,7 @@ export default function editor(colorScheme: ColorScheme) {
activeLineBackground: withOpacity(background(layer, "on"), 0.75),
highlightedLineBackground: background(layer, "on"),
// Inline autocomplete suggestions, Co-pilot suggestions, etc.
- suggestion: {
- color: syntax.predictive.color,
- },
+ suggestion: syntax.predictive,
codeActions: {
indicator: {
color: foreground(layer, "variant"),
@@ -1,6 +1,7 @@
import deepmerge from "deepmerge"
import { FontWeight, fontWeights } from "../../common"
import { ColorScheme } from "./colorScheme"
+import chroma from "chroma-js"
export interface SyntaxHighlightStyle {
color: string
@@ -128,6 +129,8 @@ function buildDefaultSyntax(colorScheme: ColorScheme): Syntax {
[key: string]: Omit<SyntaxHighlightStyle, "color">
} = {}
+ const light = colorScheme.isLight
+
// then spread the default to each style
for (const key of Object.keys({} as Syntax)) {
syntax[key as keyof Syntax] = {
@@ -135,11 +138,20 @@ function buildDefaultSyntax(colorScheme: ColorScheme): Syntax {
}
}
+ // Mix the neutral and blue colors to get a
+ // predictive color distinct from any other color in the theme
+ const predictive = chroma.mix(
+ colorScheme.ramps.neutral(0.4).hex(),
+ colorScheme.ramps.blue(0.4).hex(),
+ 0.45,
+ "lch"
+ ).hex()
+
const color = {
primary: colorScheme.ramps.neutral(1).hex(),
comment: colorScheme.ramps.neutral(0.71).hex(),
punctuation: colorScheme.ramps.neutral(0.86).hex(),
- predictive: colorScheme.ramps.neutral(0.57).hex(),
+ predictive: predictive,
emphasis: colorScheme.ramps.blue(0.5).hex(),
string: colorScheme.ramps.orange(0.5).hex(),
function: colorScheme.ramps.yellow(0.5).hex(),
@@ -169,6 +181,7 @@ function buildDefaultSyntax(colorScheme: ColorScheme): Syntax {
},
predictive: {
color: color.predictive,
+ italic: true,
},
emphasis: {
color: color.emphasis,