diff --git a/Cargo.lock b/Cargo.lock index 966b5b49e2..6498133b69 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index 14e3056e6a..b89940a751 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -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" - } - }, - // 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" - ] - } + // 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-shift-enter": "editor::NewlineAbove", + "cmd-enter": "editor::NewlineBelow", + "alt-z": "editor::ToggleSoftWrap", + "cmd-f": [ + "buffer_search::Deploy", + { + "focus": true + } + ], + "cmd-e": [ + "buffer_search::Deploy", + { + "focus": false + } + ], + "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 + } + ], + "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" + } + }, + // 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" + ] + } + } ] diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 332e3a7414..47c5f8c458 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -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" - ] - } - }, - { - "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" + ], + "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 + ] } -] \ No newline at end of file + }, + { + "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" + ] + } + }, + { + "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" + } + } +] diff --git a/assets/settings/default.json b/assets/settings/default.json index 0f663e1b81..f3ddba1e42 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -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 - }, - // 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"] - // } - // } - "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" - }, - // 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 - } - }, - // 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" - // } - // } - // } + // + "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 + }, + "Markdown": { + "soft_wrap": "preferred_line_length" + }, + "JavaScript": { + "tab_size": 2 + }, + "TypeScript": { + "tab_size": 2 + }, + "TSX": { + "tab_size": 2 + }, + "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" + // } + // } + // } + } } diff --git a/assets/settings/initial_user_settings.json b/assets/settings/initial_user_settings.json index 4c90b8a002..dc79fd7911 100644 --- a/assets/settings/initial_user_settings.json +++ b/assets/settings/initial_user_settings.json @@ -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 } diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index 2891fe3010..58fc602c94 100644 --- a/crates/collab/Cargo.toml +++ b/crates/collab/Cargo.toml @@ -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"] } diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index 5d7e358700..dada6dd8fb 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -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, + 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, @@ -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::() + .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::() - .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) diff --git a/crates/copilot/Cargo.toml b/crates/copilot/Cargo.toml index 9d68edd6b5..bfafdbc0ca 100644 --- a/crates/copilot/Cargo.toml +++ b/crates/copilot/Cargo.toml @@ -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"] } diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index 43fcf55e95..c3ec63c43c 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -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, node_runtime: Arc, cx: &mut AppContext) { // Disable Copilot for stable releases. @@ -95,15 +104,38 @@ pub fn init(http: Arc, node_runtime: Arc, cx: &mut enum CopilotServer { Disabled, - Starting { - task: Shared>, - }, + Starting { task: Shared> }, Error(Arc), - Started { - server: Arc, - status: SignInStatus, - subscriptions_by_buffer_id: HashMap, - }, + 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, + sign_in_status: SignInStatus, + registered_buffers: HashMap, } #[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>, +} + +impl RegisteredBuffer { + fn report_changes( + &mut self, + buffer: &ModelHandle, + cx: &mut ModelContext, + ) -> 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::>() + } + }) + .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::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, pub text: String, } @@ -148,6 +276,7 @@ pub struct Copilot { http: Arc, node_runtime: Arc, server: CopilotServer, + buffers: HashMap>, } 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::().enable_copilot_integration { + if cx.global::().features.copilot { if matches!(this.server, CopilotServer::Disabled) { let start_task = cx .spawn({ @@ -194,12 +323,14 @@ impl Copilot { }) .detach(); - if cx.global::().enable_copilot_integration { + if cx.global::().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::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) -> Task> { - 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::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::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) -> Task> { - 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, cx: &mut ModelContext) { + 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::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, + event: &language::Event, + cx: &mut ModelContext, + ) -> 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::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::DidCloseTextDocumentParams { + text_document: lsp::TextDocumentIdentifier::new(old_uri), + }, + )?; + server + .lsp + .notify::( + 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::DidCloseTextDocumentParams { + text_document: lsp::TextDocumentIdentifier::new(buffer.uri), + }, + ) + .log_err(); + } + } + } + pub fn completions( &mut self, buffer: &ModelHandle, @@ -452,6 +728,51 @@ impl Copilot { self.request_completions::(buffer, position, cx) } + pub fn accept_completion( + &mut self, + completion: &Completion, + cx: &mut ModelContext, + ) -> Task> { + let server = match self.server.as_authenticated() { + Ok(server) => server, + Err(error) => return Task::ready(Err(error)), + }; + let request = + server + .lsp + .request::(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, + ) -> Task> { + let server = match self.server.as_authenticated() { + Ok(server) => server, + Err(error) => return Task::ready(Err(error)), + }; + let request = + server + .lsp + .request::(request::NotifyRejectedParams { + uuids: completions + .iter() + .map(|completion| completion.uuid.clone()) + .collect(), + }); + cx.background().spawn(async move { + request.await?; + Ok(()) + }) + } + fn request_completions( &mut self, buffer: &ModelHandle, @@ -459,116 +780,48 @@ impl Copilot { cx: &mut ModelContext, ) -> Task>> 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::DidOpenTextDocumentParams { - text_document: lsp::TextDocumentItem { - uri: uri.clone(), - language_id: id_for_language( - buffer.read(cx).language(), - ), - version: 0, - text: snapshot.text(), - }, - }, - ) - .log_err(); + self.register_buffer(buffer, cx); - 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::DidCloseTextDocumentParams { - text_document: lsp::TextDocumentIdentifier::new( - uri.clone(), - ), - }, - ) - .log_err(); - subscriptions_by_buffer_id.remove(&buffer_id); - } - }) - }); - - 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::(); - 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 relative_path = buffer + .file() + .map(|file| file.path().to_path_buf()) + .unwrap_or_default(); - 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(); - } - - cx.background().spawn(async move { - let result = server + cx.foreground().spawn(async move { + let (version, snapshot) = snapshot.await?; + let result = lsp .request::(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, ) { - 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::>() { + 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::>() { + self.unregister_buffer(buffer_id); + } + } + request::SignInStatus::NotSignedIn => { + server.sign_in_status = SignInStatus::SignedOut; + for buffer_id in self.buffers.keys().copied().collect::>() { + self.unregister_buffer(buffer_id); + } + } + } + cx.notify(); } } @@ -633,6 +909,14 @@ fn id_for_language(language: Option<&Arc>) -> String { } } +fn uri_for_buffer(buffer: &ModelHandle, 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) -> anyhow::Result { } } } + +#[cfg(test)] +mod tests { + use super::*; + use gpui::{executor::Deterministic, TestAppContext}; + + #[gpui::test(iterations = 10)] + async fn test_buffer_management(deterministic: Arc, 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::() + .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::() + .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::() + .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::() + .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::() + .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::(|_, _| async { + Ok(request::SignOutResult {}) + }); + copilot + .update(cx, |copilot, cx| copilot.sign_out(cx)) + .await + .unwrap(); + assert_eq!( + lsp.receive_notification::() + .await, + lsp::DidCloseTextDocumentParams { + text_document: lsp::TextDocumentIdentifier::new(buffer_2_uri.clone()), + } + ); + assert_eq!( + lsp.receive_notification::() + .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::(|_, _| 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::() + .await, + lsp::DidOpenTextDocumentParams { + text_document: lsp::TextDocumentItem::new( + buffer_2_uri.clone(), + "plaintext".into(), + 0, + "Goodbye".into() + ), + } + ); + assert_eq!( + lsp.receive_notification::() + .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::() + .await, + lsp::DidCloseTextDocumentParams { + text_document: lsp::TextDocumentIdentifier::new(buffer_2_uri), + } + ); + } + + struct File { + abs_path: PathBuf, + path: Arc, + } + + 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 { + &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> { + todo!() + } + + fn buffer_reloaded( + &self, + _: u64, + _: &clock::Global, + _: language::RopeFingerprint, + _: ::fs::LineEnding, + _: std::time::SystemTime, + _: &mut AppContext, + ) { + todo!() + } + } +} diff --git a/crates/copilot/src/request.rs b/crates/copilot/src/request.rs index 415f160ea3..43b5109d02 100644 --- a/crates/copilot/src/request.rs +++ b/crates/copilot/src/request.rs @@ -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, +} + +impl lsp::request::Request for NotifyRejected { + type Params = NotifyRejectedParams; + type Result = String; + const METHOD: &'static str = "notifyRejected"; +} diff --git a/crates/copilot/src/sign_in.rs b/crates/copilot/src/sign_in.rs index c08fc1be53..e67f1b3e43 100644 --- a/crates/copilot/src/sign_in.rs +++ b/crates/copilot/src/sign_in.rs @@ -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) { @@ -147,6 +163,7 @@ impl CopilotCodeVerification { } fn render_prompting_modal( + connect_clicked: bool, data: &PromptUserDeviceFlow, style: &theme::Copilot, cx: &mut ViewContext, @@ -195,13 +212,20 @@ impl CopilotCodeVerification { .with_style(style.auth.prompting.hint.container.clone()) .boxed(), theme::ui::cta_button_with_click::( - "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(), }, ]) diff --git a/crates/copilot_button/src/copilot_button.rs b/crates/copilot_button/src/copilot_button.rs index a1fff79942..a3560ab5a1 100644 --- a/crates/copilot_button/src/copilot_button.rs +++ b/crates/copilot_button/src/copilot_button.rs @@ -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, @@ -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::().copilot_on(Some(&language)); + let language = action.language.clone(); + let show_copilot_suggestions = cx + .global::() + .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::().copilot_on(None); - + let show_copilot_suggestions = cx.global::().show_copilot_suggestions(None); SettingsFile::update(cx, move |file_contents| { - file_contents.editor.copilot = Some((!copilot_on).into()) + 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.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) -> Element { let settings = cx.global::(); - 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::(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, + ) { + 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) { let settings = cx.global::(); 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::().copilot_on(None); + let globally_enabled = cx.global::().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() } diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index 42f3bbadf0..42f97ae348 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -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> { + Some("Project Diagnostics".into()) + } + fn is_dirty(&self, cx: &AppContext) -> bool { self.excerpts.read(cx).is_dirty(cx) } diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index d70ed96725..4f88e91375 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -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, pending_refresh: Task>, + pending_cycling_refresh: Task>, + cycled: bool, completions: Vec, 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::(Self::on_settings_changed), + cx.observe_global::(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) { + 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) -> Option<()> { + fn refresh_copilot_suggestions( + &mut self, + debounce: bool, + cx: &mut ViewContext, + ) -> 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::().copilot_on(language_name.as_deref()) { - self.hide_copilot_suggestion(cx); + if !cx + .global::() + .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,46 +2972,116 @@ impl Editor { } }) .log_err()?; - Some(()) }); Some(()) } - fn next_copilot_suggestion(&mut self, _: &copilot::NextSuggestion, cx: &mut ViewContext) { + fn cycle_copilot_suggestions( + &mut self, + direction: Direction, + cx: &mut ViewContext, + ) -> 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) { 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) { + 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, ) { - 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) -> 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::(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 discard_copilot_suggestion(&mut self, cx: &mut ViewContext) -> bool { + if self.has_active_copilot_suggestion(cx) { + 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::(None, cx)); + cx.notify(); true } else { false @@ -2903,18 +3092,6 @@ impl Editor { self.display_map.read(cx).has_suggestion() } - fn hide_copilot_suggestion(&mut self, cx: &mut ViewContext) -> Option { - if self.has_active_copilot_suggestion(cx) { - let old_suggestion = self - .display_map - .update(cx, |map, cx| map.replace_suggestion::(None, cx)); - cx.notify(); - old_suggestion.map(|suggestion| suggestion.text) - } else { - None - } - } - fn update_visible_copilot_suggestion(&mut self, cx: &mut ViewContext) { 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.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.refresh_copilot_suggestions(cx); + fn settings_changed(&mut self, cx: &mut ViewContext) { + self.refresh_copilot_suggestions(true, cx); } pub fn set_searchable(&mut self, searchable: bool) { diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 81bcfbc7a1..05b3a17fc7 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -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.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); diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index a6b8665350..dd7249b6db 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -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::(); 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::>>()?; - 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,96 +284,9 @@ impl FollowableItem for Editor { cx: &mut ViewContext, ) -> Task> { let update_view::Variant::Editor(message) = message; - let multibuffer = self.buffer.read(cx); - let multibuffer = multibuffer.read(cx); - - let buffer_ids = message - .inserted_excerpts - .iter() - .filter_map(|insertion| Some(insertion.excerpt.as_ref()?.buffer_id)) - .collect::>(); - - let mut removals = message - .deleted_excerpts - .into_iter() - .map(ExcerptId::from_proto) - .collect::>(); - removals.sort_by(|a, b| a.cmp(&b, &multibuffer)); - - let selections = message - .selections - .into_iter() - .filter_map(|selection| deserialize_selection(&multibuffer, selection)) - .collect::>(); - let pending_selection = message - .pending_selection - .and_then(|selection| deserialize_selection(&multibuffer, 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::>() - }); - 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(()) + update_editor_from_message(this, project, message, &mut cx).await }) } @@ -404,6 +300,128 @@ impl FollowableItem for Editor { } } +async fn update_editor_from_message( + this: ViewHandle, + project: ModelHandle, + 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::>(); + let inserted_excerpt_buffers = project.update(cx, |project, cx| { + inserted_excerpt_buffer_ids + .into_iter() + .map(|id| project.open_buffer_by_id(id, cx)) + .collect::>() + }); + 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::>(); + 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(&buffer, selection)) + .collect::>(); + let pending_selection = message + .pending_selection + .and_then(|selection| deserialize_selection(&buffer, selection)); + let scroll_top_anchor = message + .scroll_top_anchor + .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, + ) + }) + })? + .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( buffer_id: u64, id: &ExcerptId, @@ -516,7 +534,24 @@ impl Item for Editor { } } - fn tab_description<'a>(&'a self, detail: usize, cx: &'a AppContext) -> Option> { + fn tab_tooltip_text(&self, cx: &AppContext) -> Option> { + 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> { 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()), diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index bec9d5967c..824c108e46 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -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, + cx: &mut ModelContext, + ) -> impl 'static + Future> { + 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( &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, diff --git a/crates/editor/src/test/editor_test_context.rs b/crates/editor/src/test/editor_test_context.rs index 63d3b49a99..65ff11b77a 100644 --- a/crates/editor/src/test/editor_test_context.rs +++ b/crates/editor/src/test/editor_test_context.rs @@ -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()) } diff --git a/crates/feedback/src/feedback_editor.rs b/crates/feedback/src/feedback_editor.rs index a8a1031123..8c06b1ecbd 100644 --- a/crates/feedback/src/feedback_editor.rs +++ b/crates/feedback/src/feedback_editor.rs @@ -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> { + Some("Send Feedback".into()) + } + fn tab_content(&self, _: Option, style: &theme::Tab, _: &AppContext) -> Element { Flex::row() .with_child( diff --git a/crates/gpui/src/elements/tooltip.rs b/crates/gpui/src/elements/tooltip.rs index 1d17b62ba7..6d2d8d2cc4 100644 --- a/crates/gpui/src/elements/tooltip.rs +++ b/crates/gpui/src/elements/tooltip.rs @@ -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, } #[derive(Clone, Deserialize, Default)] @@ -135,9 +135,14 @@ impl Tooltip { ) -> impl Drawable { 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 { diff --git a/crates/journal/src/journal.rs b/crates/journal/src/journal.rs index f79f912ab8..c439edd87d 100644 --- a/crates/journal/src/journal.rs +++ b/crates/journal/src/journal.rs @@ -46,7 +46,7 @@ pub fn new_journal_entry(app_state: Arc, 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 diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index fa8368f20b..7325ca9af5 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -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) { @@ -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, - ) -> impl Future> { + anchors: impl IntoIterator, + ) -> impl 'static + Future> { self.text.wait_for_anchors(anchors) } diff --git a/crates/language/src/buffer_tests.rs b/crates/language/src/buffer_tests.rs index 8d99b9bad7..4675e4e9dc 100644 --- a/crates/language/src/buffer_tests.rs +++ b/crates/language/src/buffer_tests.rs @@ -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 diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 60bb2cfddf..81aa1de7bd 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -262,6 +262,8 @@ pub struct LanguageConfig { pub name: Arc, pub path_suffixes: Vec, pub brackets: BracketPairConfig, + #[serde(default, deserialize_with = "deserialize_regex")] + pub first_line_pattern: Option, #[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, path: impl AsRef, + content: Option<&Rope>, ) -> UnwrapFuture>>> { 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::(); + pattern.is_match(&text) + }, + ); + path_matches || content_matches }) } @@ -1528,9 +1542,45 @@ pub fn range_from_lsp(range: lsp::Range) -> Range> { #[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) { diff --git a/crates/picker/src/picker.rs b/crates/picker/src/picker.rs index 9d3061edd3..f973dddb3c 100644 --- a/crates/picker/src/picker.rs +++ b/crates/picker/src/picker.rs @@ -187,8 +187,6 @@ impl Picker { 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 } diff --git a/crates/project/Cargo.toml b/crates/project/Cargo.toml index f5c144a3ad..e30ab56e45 100644 --- a/crates/project/Cargo.toml +++ b/crates/project/Cargo.toml @@ -19,6 +19,7 @@ test-support = [ [dependencies] text = { path = "../text" } +copilot = { path = "../copilot" } client = { path = "../client" } clock = { path = "../clock" } collections = { path = "../collections" } diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index d9fafceab0..fb69df8766 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -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, diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 2deada6a5c..d5b7ac3f3f 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -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, + cx: &mut ModelContext, + ) { + 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, rx: UnboundedReceiver, @@ -2013,17 +2037,19 @@ impl Project { fn detect_language_for_buffer( &mut self, - buffer: &ModelHandle, + buffer_handle: &ModelHandle, cx: &mut ModelContext, ) -> 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>, cx: &mut ModelContext, ) -> Option<()> { - let language_server_lookup_info: HashSet<(WorktreeId, Arc, PathBuf)> = buffers + let language_server_lookup_info: HashSet<(WorktreeId, Arc, Arc)> = 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(), diff --git a/crates/recent_projects/src/recent_projects.rs b/crates/recent_projects/src/recent_projects.rs index afa04f414d..a055177a99 100644 --- a/crates/recent_projects/src/recent_projects.rs +++ b/crates/recent_projects/src/recent_projects.rs @@ -141,7 +141,7 @@ impl PickerDelegate for RecentProjectsDelegate { fn confirm(&mut self, cx: &mut ViewContext) { 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); diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 3cbafefa5a..680191b161 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -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> { + Some(self.query_editor.read(cx).text(cx).into()) + } + fn act_as_type<'a>( &'a self, type_id: TypeId, diff --git a/crates/settings/src/settings.rs b/crates/settings/src/settings.rs index feb4017018..6942a6e57b 100644 --- a/crates/settings/src/settings.rs +++ b/crates/settings/src/settings.rs @@ -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, pub formatter: Option, pub enable_language_server: Option, - #[schemars(skip)] - pub copilot: Option, -} - -#[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 for bool { - fn from(value: OnOff) -> bool { - value.as_bool() - } -} - -impl From for OnOff { - fn from(value: bool) -> OnOff { - OnOff::from_bool(value) - } + pub show_copilot_suggestions: Option, } #[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] @@ -437,8 +401,7 @@ pub struct SettingsFileContent { #[serde(default)] pub base_keymap: Option, #[serde(default)] - #[schemars(skip)] - pub enable_copilot_integration: Option, + pub features: FeaturesContent, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] @@ -447,6 +410,18 @@ pub struct LspSettings { pub initialization_options: Option, } +#[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, +} + 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 } } } diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index f25bbbfcf5..54e1d412aa 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -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> { + Some(self.terminal().read(cx).title().into()) + } + fn tab_content( &self, _detail: Option, diff --git a/crates/text/src/text.rs b/crates/text/src/text.rs index b857ec5d5e..3df83fa305 100644 --- a/crates/text/src/text.rs +++ b/crates/text/src/text.rs @@ -1331,15 +1331,15 @@ impl Buffer { } } - pub fn wait_for_anchors<'a>( + pub fn wait_for_anchors( &mut self, - anchors: impl IntoIterator, + anchors: impl IntoIterator, ) -> impl 'static + Future> { 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 diff --git a/crates/welcome/src/welcome.rs b/crates/welcome/src/welcome.rs index d610668882..3501dd52a0 100644 --- a/crates/welcome/src/welcome.rs +++ b/crates/welcome/src/welcome.rs @@ -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> { + Some("Welcome to Zed!".into()) + } + fn tab_content( &self, _detail: Option, diff --git a/crates/workspace/src/item.rs b/crates/workspace/src/item.rs index 6a7416849e..6b908d97eb 100644 --- a/crates/workspace/src/item.rs +++ b/crates/workspace/src/item.rs @@ -48,7 +48,10 @@ pub trait Item: View { fn navigate(&mut self, _: Box, _: &mut ViewContext) -> bool { false } - fn tab_description<'a>(&'a self, _: usize, _: &'a AppContext) -> Option> { + fn tab_tooltip_text(&self, _: &AppContext) -> Option> { + None + } + fn tab_description<'a>(&'a self, _: usize, _: &'a AppContext) -> Option> { None } fn tab_content( @@ -170,7 +173,8 @@ pub trait ItemHandle: 'static + fmt::Debug { cx: &mut WindowContext, handler: Box, ) -> gpui::Subscription; - fn tab_description<'a>(&self, detail: usize, cx: &'a AppContext) -> Option>; + fn tab_tooltip_text<'a>(&self, cx: &'a AppContext) -> Option>; + fn tab_description<'a>(&'a self, detail: usize, cx: &'a AppContext) -> Option>; fn tab_content( &self, detail: Option, @@ -260,7 +264,11 @@ impl ItemHandle for ViewHandle { }) } - fn tab_description<'a>(&self, detail: usize, cx: &'a AppContext) -> Option> { + fn tab_tooltip_text<'a>(&self, cx: &'a AppContext) -> Option> { + self.read(cx).tab_tooltip_text(cx) + } + + fn tab_description<'a>(&'a self, detail: usize, cx: &'a AppContext) -> Option> { 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> { + fn tab_description(&self, detail: usize, _: &AppContext) -> Option> { self.tab_descriptions.as_ref().and_then(|descriptions| { let description = *descriptions.get(detail).or_else(|| descriptions.last())?; Some(description.into()) diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 2adc1e0a32..4e003d65d8 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -1389,6 +1389,9 @@ impl Pane { let detail = detail.clone(); let theme = cx.global::().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::::new(ix, cx, |_, cx| { - Self::render_tab::( - &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::::new(ix, cx, |_, cx| { + Self::render_tab::( + &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::( + ix, + tab_tooltip_text, + None, + tooltip_theme, + cx, + ) + .boxed(); + } + + mouse_event_handler.boxed() } }); diff --git a/crates/workspace/src/shared_screen.rs b/crates/workspace/src/shared_screen.rs index 17f7c061c5..51d9672390 100644 --- a/crates/workspace/src/shared_screen.rs +++ b/crates/workspace/src/shared_screen.rs @@ -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> { + Some(format!("{}'s screen", self.user.github_login).into()) + } fn deactivated(&mut self, cx: &mut ViewContext) { if let Some(nav_history) = self.nav_history.as_ref() { nav_history.push::<()>(None, cx); diff --git a/crates/workspace/src/toolbar.rs b/crates/workspace/src/toolbar.rs index c47b2718ff..291936aaa5 100644 --- a/crates/workspace/src/toolbar.rs +++ b/crates/workspace/src/toolbar.rs @@ -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 ToolbarItemViewHandle for ViewHandle { } 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(); + }); } } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 90fc3f2f07..6df269ff27 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -290,7 +290,7 @@ pub fn init(app_state: Arc, 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, 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, app_state: Arc, + requesting_window_id: Option, cx: &mut AppContext, ) -> Task<( ViewHandle, @@ -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; - } - } - - Some((bounds, display)) + 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()) }) - .unzip() - }; + }) + }) + .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?; - // 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 - }; + // Stored bounds are relative to the containing display. + // So convert back to global coordinates if that screen still exists + if let WindowBounds::Fixed(mut window_bounds) = bounds { + if let Some(screen) = cx.platform().screen_by_id(display) { + let screen_bounds = screen.bounds(); + window_bounds.set_origin_x( + window_bounds.origin_x() + screen_bounds.origin_x(), + ); + window_bounds.set_origin_y( + window_bounds.origin_y() + screen_bounds.origin_y(), + ); + bounds = WindowBounds::Fixed(window_bounds); + } else { + // Screen no longer exists. Return none here. + return None; + } + } + + Some((bounds, display)) + }) + .unzip() + }; + + // Use the serialized workspace to construct the new window + cx.add_window( + (app_state.build_window_options)(bounds, display, cx.platform().as_ref()), + |cx| build_workspace(cx, serialized_workspace), + ) + .1 + }); notify_if_database_failed(&workspace, &mut cx); @@ -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 { pub fn open_paths( abs_paths: &[PathBuf], app_state: &Arc, + requesting_window_id: Option, 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) + '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; diff --git a/crates/zed/src/languages/javascript/config.toml b/crates/zed/src/languages/javascript/config.toml index 7c49ac9513..c23ddcd6e7 100644 --- a/crates/zed/src/languages/javascript/config.toml +++ b/crates/zed/src/languages/javascript/config.toml @@ -1,5 +1,6 @@ name = "JavaScript" path_suffixes = ["js", "jsx", "mjs"] +first_line_pattern = '^#!.*\bnode\b' line_comment = "// " autoclose_before = ";:.,=}])>" brackets = [ diff --git a/crates/zed/src/languages/python/config.toml b/crates/zed/src/languages/python/config.toml index e733676d89..80609de0ba 100644 --- a/crates/zed/src/languages/python/config.toml +++ b/crates/zed/src/languages/python/config.toml @@ -1,5 +1,6 @@ name = "Python" path_suffixes = ["py", "pyi"] +first_line_pattern = '^#!.*\bpython[0-9.]*\b' line_comment = "# " autoclose_before = ";:.,=}])>" brackets = [ diff --git a/crates/zed/src/languages/ruby/config.toml b/crates/zed/src/languages/ruby/config.toml index 329e080740..a0b26bff92 100644 --- a/crates/zed/src/languages/ruby/config.toml +++ b/crates/zed/src/languages/ruby/config.toml @@ -1,5 +1,6 @@ name = "Ruby" path_suffixes = ["rb", "Gemfile"] +first_line_pattern = '^#!.*\bruby\b' line_comment = "# " autoclose_before = ";:.,=}])>" brackets = [ diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 4cd7ae32df..4b9916f861 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -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)) => { diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 4f9b8fca6c..c6cfd8c109 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -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::() + .unwrap(); + workspace_1.update(cx, |workspace, cx| { + assert_eq!( + workspace + .worktrees(cx) + .map(|w| w.read(cx).abs_path()) + .collect::>(), + &[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| { diff --git a/script/randomized-test-ci b/script/randomized-test-ci index 7816ebfbbf..4d3f85aef4 100755 --- a/script/randomized-test-ci +++ b/script/randomized-test-ci @@ -56,6 +56,8 @@ async function main() { headers: {"Content-Type": "application/json"}, body: JSON.stringify(body) }) + + process.exit(1) } function randomU64() { diff --git a/styles/src/styleTree/editor.ts b/styles/src/styleTree/editor.ts index 0c2435ed5a..84ef51406e 100644 --- a/styles/src/styleTree/editor.ts +++ b/styles/src/styleTree/editor.ts @@ -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"), diff --git a/styles/src/themes/common/syntax.ts b/styles/src/themes/common/syntax.ts index dcfd93628e..925ed7e5c1 100644 --- a/styles/src/themes/common/syntax.ts +++ b/styles/src/themes/common/syntax.ts @@ -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 } = {} + 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,