Merge branch 'main' into window_context_2

Antonio Scandurra created

Change summary

Cargo.lock                                      |    5 
assets/keymaps/default.json                     | 1063 +++++++++---------
assets/keymaps/vim.json                         |  622 +++++-----
assets/settings/default.json                    |  488 ++++----
assets/settings/initial_user_settings.json      |    2 
crates/collab/Cargo.toml                        |    1 
crates/collab/src/tests/integration_tests.rs    |  156 ++
crates/copilot/Cargo.toml                       |    5 
crates/copilot/src/copilot.rs                   |  797 +++++++++++--
crates/copilot/src/request.rs                   |   60 +
crates/copilot/src/sign_in.rs                   |   47 
crates/copilot_button/src/copilot_button.rs     |  162 +-
crates/diagnostics/src/diagnostics.rs           |    5 
crates/editor/src/editor.rs                     |  321 ++++-
crates/editor/src/editor_tests.rs               |   49 
crates/editor/src/items.rs                      |  301 +++--
crates/editor/src/multi_buffer.rs               |   40 
crates/editor/src/test/editor_test_context.rs   |   29 
crates/feedback/src/feedback_editor.rs          |    5 
crates/gpui/src/elements/tooltip.rs             |   13 
crates/journal/src/journal.rs                   |    2 
crates/language/src/buffer.rs                   |    8 
crates/language/src/buffer_tests.rs             |   12 
crates/language/src/language.rs                 |   58 
crates/picker/src/picker.rs                     |    2 
crates/project/Cargo.toml                       |    1 
crates/project/src/lsp_command.rs               |    8 
crates/project/src/project.rs                   |   66 
crates/recent_projects/src/recent_projects.rs   |    2 
crates/search/src/project_search.rs             |    5 
crates/settings/src/settings.rs                 |  111 -
crates/terminal_view/src/terminal_view.rs       |    5 
crates/text/src/text.rs                         |    8 
crates/welcome/src/welcome.rs                   |    6 
crates/workspace/src/item.rs                    |   16 
crates/workspace/src/pane.rs                    |   86 
crates/workspace/src/shared_screen.rs           |    8 
crates/workspace/src/toolbar.rs                 |    8 
crates/workspace/src/workspace.rs               |  123 +
crates/zed/src/languages/javascript/config.toml |    1 
crates/zed/src/languages/python/config.toml     |    1 
crates/zed/src/languages/ruby/config.toml       |    1 
crates/zed/src/main.rs                          |    6 
crates/zed/src/zed.rs                           |   30 
script/randomized-test-ci                       |    2 
styles/src/styleTree/editor.ts                  |    4 
styles/src/themes/common/syntax.ts              |   15 
47 files changed, 3,020 insertions(+), 1,746 deletions(-)

Detailed changes

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",

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"
+  // Standard macOS bindings
+  {
+    "bindings": {
+      "up": "menu::SelectPrev",
+      "pageup": "menu::SelectFirst",
+      "shift-pageup": "menu::SelectFirst",
+      "ctrl-p": "menu::SelectPrev",
+      "down": "menu::SelectNext",
+      "pagedown": "menu::SelectLast",
+      "shift-pagedown": "menu::SelectFirst",
+      "ctrl-n": "menu::SelectNext",
+      "cmd-up": "menu::SelectFirst",
+      "cmd-down": "menu::SelectLast",
+      "enter": "menu::Confirm",
+      "escape": "menu::Cancel",
+      "ctrl-c": "menu::Cancel",
+      "cmd-{": "pane::ActivatePrevItem",
+      "cmd-}": "pane::ActivateNextItem",
+      "alt-cmd-left": "pane::ActivatePrevItem",
+      "alt-cmd-right": "pane::ActivateNextItem",
+      "cmd-w": "pane::CloseActiveItem",
+      "alt-cmd-t": "pane::CloseInactiveItems",
+      "cmd-k u": "pane::CloseCleanItems",
+      "cmd-k cmd-w": "pane::CloseAllItems",
+      "cmd-shift-w": "workspace::CloseWindow",
+      "cmd-s": "workspace::Save",
+      "cmd-shift-s": "workspace::SaveAs",
+      "cmd-=": "zed::IncreaseBufferFontSize",
+      "cmd--": "zed::DecreaseBufferFontSize",
+      "cmd-0": "zed::ResetBufferFontSize",
+      "cmd-,": "zed::OpenSettings",
+      "cmd-q": "zed::Quit",
+      "cmd-h": "zed::Hide",
+      "alt-cmd-h": "zed::HideOthers",
+      "cmd-m": "zed::Minimize",
+      "ctrl-cmd-f": "zed::ToggleFullScreen",
+      "cmd-n": "workspace::NewFile",
+      "cmd-shift-n": "workspace::NewWindow",
+      "cmd-o": "workspace::Open",
+      "alt-cmd-o": "projects::OpenRecent",
+      "ctrl-`": "workspace::NewTerminal"
+    }
+  },
+  {
+    "context": "Editor",
+    "bindings": {
+      "escape": "editor::Cancel",
+      "backspace": "editor::Backspace",
+      "shift-backspace": "editor::Backspace",
+      "ctrl-h": "editor::Backspace",
+      "delete": "editor::Delete",
+      "ctrl-d": "editor::Delete",
+      "tab": "editor::Tab",
+      "shift-tab": "editor::TabPrev",
+      "ctrl-k": "editor::CutToEndOfLine",
+      "ctrl-t": "editor::Transpose",
+      "cmd-backspace": "editor::DeleteToBeginningOfLine",
+      "cmd-delete": "editor::DeleteToEndOfLine",
+      "alt-backspace": "editor::DeleteToPreviousWordStart",
+      "alt-delete": "editor::DeleteToNextWordEnd",
+      "alt-h": "editor::DeleteToPreviousWordStart",
+      "alt-d": "editor::DeleteToNextWordEnd",
+      "cmd-x": "editor::Cut",
+      "cmd-c": "editor::Copy",
+      "cmd-v": "editor::Paste",
+      "cmd-z": "editor::Undo",
+      "cmd-shift-z": "editor::Redo",
+      "up": "editor::MoveUp",
+      "pageup": "editor::PageUp",
+      "shift-pageup": "editor::MovePageUp",
+      "home": "editor::MoveToBeginningOfLine",
+      "down": "editor::MoveDown",
+      "pagedown": "editor::PageDown",
+      "shift-pagedown": "editor::MovePageDown",
+      "end": "editor::MoveToEndOfLine",
+      "left": "editor::MoveLeft",
+      "right": "editor::MoveRight",
+      "ctrl-p": "editor::MoveUp",
+      "ctrl-n": "editor::MoveDown",
+      "ctrl-b": "editor::MoveLeft",
+      "ctrl-f": "editor::MoveRight",
+      "ctrl-l": "editor::NextScreen",
+      "alt-left": "editor::MoveToPreviousWordStart",
+      "alt-b": "editor::MoveToPreviousWordStart",
+      "alt-right": "editor::MoveToNextWordEnd",
+      "alt-f": "editor::MoveToNextWordEnd",
+      "cmd-left": "editor::MoveToBeginningOfLine",
+      "ctrl-a": "editor::MoveToBeginningOfLine",
+      "cmd-right": "editor::MoveToEndOfLine",
+      "ctrl-e": "editor::MoveToEndOfLine",
+      "cmd-up": "editor::MoveToBeginning",
+      "cmd-down": "editor::MoveToEnd",
+      "shift-up": "editor::SelectUp",
+      "ctrl-shift-p": "editor::SelectUp",
+      "shift-down": "editor::SelectDown",
+      "ctrl-shift-n": "editor::SelectDown",
+      "shift-left": "editor::SelectLeft",
+      "ctrl-shift-b": "editor::SelectLeft",
+      "shift-right": "editor::SelectRight",
+      "ctrl-shift-f": "editor::SelectRight",
+      "alt-shift-left": "editor::SelectToPreviousWordStart",
+      "alt-shift-b": "editor::SelectToPreviousWordStart",
+      "alt-shift-right": "editor::SelectToNextWordEnd",
+      "alt-shift-f": "editor::SelectToNextWordEnd",
+      "cmd-shift-up": "editor::SelectToBeginning",
+      "cmd-shift-down": "editor::SelectToEnd",
+      "cmd-a": "editor::SelectAll",
+      "cmd-l": "editor::SelectLine",
+      "cmd-shift-i": "editor::Format",
+      "cmd-shift-left": [
+        "editor::SelectToBeginningOfLine",
+        {
+          "stop_at_soft_wraps": true
         }
-    },
-    // Bindings that should be unified with bindings for more general actions
-    {
-        "context": "Editor && renaming",
-        "bindings": {
-            "enter": "editor::ConfirmRename"
+      ],
+      "shift-home": [
+        "editor::SelectToBeginningOfLine",
+        {
+          "stop_at_soft_wraps": true
         }
-    },
-    {
-        "context": "Editor && showing_completions",
-        "bindings": {
-            "enter": "editor::ConfirmCompletion",
-            "tab": "editor::ConfirmCompletion"
+      ],
+      "ctrl-shift-a": [
+        "editor::SelectToBeginningOfLine",
+        {
+          "stop_at_soft_wraps": true
         }
-    },
-    {
-        "context": "Editor && showing_code_actions",
-        "bindings": {
-            "enter": "editor::ConfirmCodeAction"
+      ],
+      "cmd-shift-right": [
+        "editor::SelectToEndOfLine",
+        {
+          "stop_at_soft_wraps": true
         }
-    },
-    // Custom bindings
-    {
-        "bindings": {
-            "ctrl-alt-cmd-f": "workspace::FollowNextCollaborator",
-            "cmd-shift-c": "collab::ToggleContactsMenu",
-            "cmd-alt-i": "zed::DebugElements"
+      ],
+      "shift-end": [
+        "editor::SelectToEndOfLine",
+        {
+          "stop_at_soft_wraps": true
         }
-    },
-    {
-        "context": "Editor",
-        "bindings": {
-            "alt-enter": "editor::OpenExcerpts",
-            "cmd-f8": "editor::GoToHunk",
-            "cmd-shift-f8": "editor::GoToPrevHunk"
+      ],
+      "ctrl-shift-e": [
+        "editor::SelectToEndOfLine",
+        {
+          "stop_at_soft_wraps": true
         }
-    },
-    {
-        "context": "ProjectSearchBar",
-        "bindings": {
-            "cmd-enter": "project_search::SearchInNew"
+      ],
+      "ctrl-v": [
+        "editor::MovePageDown",
+        {
+          "center_cursor": true
         }
-    },
-    {
-        "context": "Workspace",
-        "bindings": {
-            "shift-escape": "dock::FocusDock"
+      ],
+      "alt-v": [
+        "editor::MovePageUp",
+        {
+          "center_cursor": true
         }
-    },
-    {
-        "bindings": {
-            "cmd-shift-k cmd-shift-right": "dock::AnchorDockRight",
-            "cmd-shift-k cmd-shift-down": "dock::AnchorDockBottom",
-            "cmd-shift-k cmd-shift-up": "dock::ExpandDock"
+      ],
+      "ctrl-cmd-space": "editor::ShowCharacterPalette"
+    }
+  },
+  {
+    "context": "Editor && mode == full",
+    "bindings": {
+      "enter": "editor::Newline",
+      "cmd-shift-enter": "editor::NewlineAbove",
+      "cmd-enter": "editor::NewlineBelow",
+      "alt-z": "editor::ToggleSoftWrap",
+      "cmd-f": [
+        "buffer_search::Deploy",
+        {
+          "focus": true
         }
-    },
-    {
-        "context": "Pane",
-        "bindings": {
-            "cmd-escape": "dock::AddTabToDock"
+      ],
+      "cmd-e": [
+        "buffer_search::Deploy",
+        {
+          "focus": false
         }
-    },
-    {
-        "context": "Pane && docked",
-        "bindings": {
-            "shift-escape": "dock::HideDock",
-            "cmd-escape": "dock::RemoveTabFromDock"
+      ],
+      "alt-\\": "copilot::Suggest",
+      "alt-]": "copilot::NextSuggestion",
+      "alt-[": "copilot::PreviousSuggestion"
+    }
+  },
+  {
+    "context": "Editor && mode == auto_height",
+    "bindings": {
+      "alt-enter": "editor::Newline",
+      "cmd-alt-enter": "editor::NewlineBelow"
+    }
+  },
+  {
+    "context": "BufferSearchBar > Editor",
+    "bindings": {
+      "escape": "buffer_search::Dismiss",
+      "tab": "buffer_search::FocusEditor",
+      "enter": "search::SelectNextMatch",
+      "shift-enter": "search::SelectPrevMatch"
+    }
+  },
+  {
+    "context": "Pane",
+    "bindings": {
+      "cmd-f": "project_search::ToggleFocus",
+      "cmd-g": "search::SelectNextMatch",
+      "cmd-shift-g": "search::SelectPrevMatch",
+      "alt-cmd-c": "search::ToggleCaseSensitive",
+      "alt-cmd-w": "search::ToggleWholeWord",
+      "alt-cmd-r": "search::ToggleRegex"
+    }
+  },
+  // Bindings from VS Code
+  {
+    "context": "Editor",
+    "bindings": {
+      "cmd-[": "editor::Outdent",
+      "cmd-]": "editor::Indent",
+      "cmd-alt-up": "editor::AddSelectionAbove",
+      "cmd-ctrl-p": "editor::AddSelectionAbove",
+      "cmd-alt-down": "editor::AddSelectionBelow",
+      "cmd-ctrl-n": "editor::AddSelectionBelow",
+      "cmd-d": [
+        "editor::SelectNext",
+        {
+          "replace_newest": false
         }
-    },
-    {
-        "context": "ProjectPanel",
-        "bindings": {
-            "left": "project_panel::CollapseSelectedEntry",
-            "right": "project_panel::ExpandSelectedEntry",
-            "cmd-x": "project_panel::Cut",
-            "cmd-c": "project_panel::Copy",
-            "cmd-v": "project_panel::Paste",
-            "cmd-alt-c": "project_panel::CopyPath",
-            "alt-cmd-shift-c": "project_panel::CopyRelativePath",
-            "f2": "project_panel::Rename",
-            "backspace": "project_panel::Delete",
-            "alt-cmd-r": "project_panel::RevealInFinder"
+      ],
+      "cmd-k cmd-d": [
+        "editor::SelectNext",
+        {
+          "replace_newest": true
         }
-    },
-    {
-        "context": "Terminal",
-        "bindings": {
-            "ctrl-cmd-space": "terminal::ShowCharacterPalette",
-            "cmd-c": "terminal::Copy",
-            "cmd-v": "terminal::Paste",
-            "cmd-k": "terminal::Clear",
-            // Some nice conveniences
-            "cmd-backspace": [
-                "terminal::SendText",
-                "\u0015"
-            ],
-            "cmd-right": [
-                "terminal::SendText",
-                "\u0005"
-            ],
-            "cmd-left": [
-                "terminal::SendText",
-                "\u0001"
-            ],
-            // Terminal.app compatability
-            "alt-left": [
-                "terminal::SendText",
-                "\u001bb"
-            ],
-            "alt-right": [
-                "terminal::SendText",
-                "\u001bf"
-            ],
-            // There are conflicting bindings for these keys in the global context.
-            // these bindings override them, remove at your own risk:
-            "up": [
-                "terminal::SendKeystroke",
-                "up"
-            ],
-            "pageup": [
-                "terminal::SendKeystroke",
-                "pageup"
-            ],
-            "down": [
-                "terminal::SendKeystroke",
-                "down"
-            ],
-            "pagedown": [
-                "terminal::SendKeystroke",
-                "pagedown"
-            ],
-            "escape": [
-                "terminal::SendKeystroke",
-                "escape"
-            ],
-            "enter": [
-                "terminal::SendKeystroke",
-                "enter"
-            ],
-            "ctrl-c": [
-                "terminal::SendKeystroke",
-                "ctrl-c"
-            ]
+      ],
+      "cmd-k cmd-i": "editor::Hover",
+      "cmd-/": [
+        "editor::ToggleComments",
+        {
+          "advance_downwards": false
         }
+      ],
+      "alt-up": "editor::SelectLargerSyntaxNode",
+      "alt-down": "editor::SelectSmallerSyntaxNode",
+      "cmd-u": "editor::UndoSelection",
+      "cmd-shift-u": "editor::RedoSelection",
+      "f8": "editor::GoToDiagnostic",
+      "shift-f8": "editor::GoToPrevDiagnostic",
+      "f2": "editor::Rename",
+      "f12": "editor::GoToDefinition",
+      "cmd-f12": "editor::GoToTypeDefinition",
+      "alt-shift-f12": "editor::FindAllReferences",
+      "ctrl-m": "editor::MoveToEnclosingBracket",
+      "alt-cmd-[": "editor::Fold",
+      "alt-cmd-]": "editor::UnfoldLines",
+      "ctrl-space": "editor::ShowCompletions",
+      "cmd-.": "editor::ToggleCodeActions",
+      "alt-cmd-r": "editor::RevealInFinder"
+    }
+  },
+  {
+    "context": "Editor && mode == full",
+    "bindings": {
+      "cmd-shift-o": "outline::Toggle",
+      "ctrl-g": "go_to_line::Toggle"
+    }
+  },
+  {
+    "context": "Pane",
+    "bindings": {
+      "ctrl-1": [
+        "pane::ActivateItem",
+        0
+      ],
+      "ctrl-2": [
+        "pane::ActivateItem",
+        1
+      ],
+      "ctrl-3": [
+        "pane::ActivateItem",
+        2
+      ],
+      "ctrl-4": [
+        "pane::ActivateItem",
+        3
+      ],
+      "ctrl-5": [
+        "pane::ActivateItem",
+        4
+      ],
+      "ctrl-6": [
+        "pane::ActivateItem",
+        5
+      ],
+      "ctrl-7": [
+        "pane::ActivateItem",
+        6
+      ],
+      "ctrl-8": [
+        "pane::ActivateItem",
+        7
+      ],
+      "ctrl-9": [
+        "pane::ActivateItem",
+        8
+      ],
+      "ctrl-0": "pane::ActivateLastItem",
+      "ctrl--": "pane::GoBack",
+      "ctrl-_": "pane::GoForward",
+      "cmd-shift-t": "pane::ReopenClosedItem",
+      "cmd-shift-f": "project_search::ToggleFocus"
+    }
+  },
+  {
+    "context": "Workspace",
+    "bindings": {
+      "cmd-1": [
+        "workspace::ActivatePane",
+        0
+      ],
+      "cmd-2": [
+        "workspace::ActivatePane",
+        1
+      ],
+      "cmd-3": [
+        "workspace::ActivatePane",
+        2
+      ],
+      "cmd-4": [
+        "workspace::ActivatePane",
+        3
+      ],
+      "cmd-5": [
+        "workspace::ActivatePane",
+        4
+      ],
+      "cmd-6": [
+        "workspace::ActivatePane",
+        5
+      ],
+      "cmd-7": [
+        "workspace::ActivatePane",
+        6
+      ],
+      "cmd-8": [
+        "workspace::ActivatePane",
+        7
+      ],
+      "cmd-9": [
+        "workspace::ActivatePane",
+        8
+      ],
+      "cmd-b": "workspace::ToggleLeftSidebar",
+      "cmd-shift-f": "workspace::NewSearch",
+      "cmd-k cmd-t": "theme_selector::Toggle",
+      "cmd-k cmd-s": "zed::OpenKeymap",
+      "cmd-t": "project_symbols::Toggle",
+      "cmd-p": "file_finder::Toggle",
+      "cmd-shift-p": "command_palette::Toggle",
+      "cmd-shift-m": "diagnostics::Deploy",
+      "cmd-shift-e": "project_panel::ToggleFocus",
+      "cmd-alt-s": "workspace::SaveAll",
+      "cmd-k m": "language_selector::Toggle"
+    }
+  },
+  // Bindings from Sublime Text
+  {
+    "context": "Editor",
+    "bindings": {
+      "ctrl-shift-k": "editor::DeleteLine",
+      "cmd-shift-d": "editor::DuplicateLine",
+      "cmd-shift-l": "editor::SplitSelectionIntoLines",
+      "ctrl-cmd-up": "editor::MoveLineUp",
+      "ctrl-cmd-down": "editor::MoveLineDown",
+      "ctrl-alt-backspace": "editor::DeleteToPreviousSubwordStart",
+      "ctrl-alt-h": "editor::DeleteToPreviousSubwordStart",
+      "ctrl-alt-delete": "editor::DeleteToNextSubwordEnd",
+      "ctrl-alt-d": "editor::DeleteToNextSubwordEnd",
+      "ctrl-alt-left": "editor::MoveToPreviousSubwordStart",
+      "ctrl-alt-b": "editor::MoveToPreviousSubwordStart",
+      "ctrl-alt-right": "editor::MoveToNextSubwordEnd",
+      "ctrl-alt-f": "editor::MoveToNextSubwordEnd",
+      "ctrl-alt-shift-left": "editor::SelectToPreviousSubwordStart",
+      "ctrl-alt-shift-b": "editor::SelectToPreviousSubwordStart",
+      "ctrl-alt-shift-right": "editor::SelectToNextSubwordEnd",
+      "ctrl-alt-shift-f": "editor::SelectToNextSubwordEnd"
+    }
+  },
+  {
+    "bindings": {
+      "cmd-k cmd-left": "workspace::ActivatePreviousPane",
+      "cmd-k cmd-right": "workspace::ActivateNextPane"
+    }
+  },
+  // Bindings from Atom
+  {
+    "context": "Pane",
+    "bindings": {
+      "cmd-k up": "pane::SplitUp",
+      "cmd-k down": "pane::SplitDown",
+      "cmd-k left": "pane::SplitLeft",
+      "cmd-k right": "pane::SplitRight"
+    }
+  },
+  // Bindings that should be unified with bindings for more general actions
+  {
+    "context": "Editor && renaming",
+    "bindings": {
+      "enter": "editor::ConfirmRename"
+    }
+  },
+  {
+    "context": "Editor && showing_completions",
+    "bindings": {
+      "enter": "editor::ConfirmCompletion",
+      "tab": "editor::ConfirmCompletion"
+    }
+  },
+  {
+    "context": "Editor && showing_code_actions",
+    "bindings": {
+      "enter": "editor::ConfirmCodeAction"
+    }
+  },
+  // Custom bindings
+  {
+    "bindings": {
+      "ctrl-alt-cmd-f": "workspace::FollowNextCollaborator",
+      "cmd-shift-c": "collab::ToggleContactsMenu",
+      "cmd-alt-i": "zed::DebugElements"
+    }
+  },
+  {
+    "context": "Editor",
+    "bindings": {
+      "alt-enter": "editor::OpenExcerpts",
+      "cmd-f8": "editor::GoToHunk",
+      "cmd-shift-f8": "editor::GoToPrevHunk"
+    }
+  },
+  {
+    "context": "ProjectSearchBar",
+    "bindings": {
+      "cmd-enter": "project_search::SearchInNew"
+    }
+  },
+  {
+    "context": "Workspace",
+    "bindings": {
+      "shift-escape": "dock::FocusDock"
+    }
+  },
+  {
+    "bindings": {
+      "cmd-shift-k cmd-shift-right": "dock::AnchorDockRight",
+      "cmd-shift-k cmd-shift-down": "dock::AnchorDockBottom",
+      "cmd-shift-k cmd-shift-up": "dock::ExpandDock"
+    }
+  },
+  {
+    "context": "Pane",
+    "bindings": {
+      "cmd-escape": "dock::AddTabToDock"
+    }
+  },
+  {
+    "context": "Pane && docked",
+    "bindings": {
+      "shift-escape": "dock::HideDock",
+      "cmd-escape": "dock::RemoveTabFromDock"
+    }
+  },
+  {
+    "context": "ProjectPanel",
+    "bindings": {
+      "left": "project_panel::CollapseSelectedEntry",
+      "right": "project_panel::ExpandSelectedEntry",
+      "cmd-x": "project_panel::Cut",
+      "cmd-c": "project_panel::Copy",
+      "cmd-v": "project_panel::Paste",
+      "cmd-alt-c": "project_panel::CopyPath",
+      "alt-cmd-shift-c": "project_panel::CopyRelativePath",
+      "f2": "project_panel::Rename",
+      "backspace": "project_panel::Delete",
+      "alt-cmd-r": "project_panel::RevealInFinder"
+    }
+  },
+  {
+    "context": "Terminal",
+    "bindings": {
+      "ctrl-cmd-space": "terminal::ShowCharacterPalette",
+      "cmd-c": "terminal::Copy",
+      "cmd-v": "terminal::Paste",
+      "cmd-k": "terminal::Clear",
+      // Some nice conveniences
+      "cmd-backspace": [
+        "terminal::SendText",
+        "\u0015"
+      ],
+      "cmd-right": [
+        "terminal::SendText",
+        "\u0005"
+      ],
+      "cmd-left": [
+        "terminal::SendText",
+        "\u0001"
+      ],
+      // Terminal.app compatability
+      "alt-left": [
+        "terminal::SendText",
+        "\u001bb"
+      ],
+      "alt-right": [
+        "terminal::SendText",
+        "\u001bf"
+      ],
+      // There are conflicting bindings for these keys in the global context.
+      // these bindings override them, remove at your own risk:
+      "up": [
+        "terminal::SendKeystroke",
+        "up"
+      ],
+      "pageup": [
+        "terminal::SendKeystroke",
+        "pageup"
+      ],
+      "down": [
+        "terminal::SendKeystroke",
+        "down"
+      ],
+      "pagedown": [
+        "terminal::SendKeystroke",
+        "pagedown"
+      ],
+      "escape": [
+        "terminal::SendKeystroke",
+        "escape"
+      ],
+      "enter": [
+        "terminal::SendKeystroke",
+        "enter"
+      ],
+      "ctrl-c": [
+        "terminal::SendKeystroke",
+        "ctrl-c"
+      ]
     }
+  }
 ]

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"
-            ]
+      ],
+      "shift-t": [
+        "vim::PushOperator",
+        {
+          "FindBackward": {
+            "after": true
+          }
         }
-    },
-    {
-        "context": "Editor && vim_mode == insert",
-        "bindings": {
-            "escape": "vim::NormalBefore",
-            "ctrl-c": "vim::NormalBefore"
+      ],
+      "escape": "editor::Cancel",
+      "0": "vim::StartOfLine", // When no number operator present, use start of line motion
+      "1": [
+        "vim::Number",
+        1
+      ],
+      "2": [
+        "vim::Number",
+        2
+      ],
+      "3": [
+        "vim::Number",
+        3
+      ],
+      "4": [
+        "vim::Number",
+        4
+      ],
+      "5": [
+        "vim::Number",
+        5
+      ],
+      "6": [
+        "vim::Number",
+        6
+      ],
+      "7": [
+        "vim::Number",
+        7
+      ],
+      "8": [
+        "vim::Number",
+        8
+      ],
+      "9": [
+        "vim::Number",
+        9
+      ]
+    }
+  },
+  {
+    "context": "Editor && vim_mode == normal && vim_operator == none && !VimWaiting",
+    "bindings": {
+      "c": [
+        "vim::PushOperator",
+        "Change"
+      ],
+      "shift-c": "vim::ChangeToEndOfLine",
+      "d": [
+        "vim::PushOperator",
+        "Delete"
+      ],
+      "shift-d": "vim::DeleteToEndOfLine",
+      "y": [
+        "vim::PushOperator",
+        "Yank"
+      ],
+      "z": [
+        "vim::PushOperator",
+        {
+          "Namespace": "Z"
+        }
+      ],
+      "i": [
+        "vim::SwitchMode",
+        "Insert"
+      ],
+      "shift-i": "vim::InsertFirstNonWhitespace",
+      "a": "vim::InsertAfter",
+      "shift-a": "vim::InsertEndOfLine",
+      "x": "vim::DeleteRight",
+      "shift-x": "vim::DeleteLeft",
+      "^": "vim::FirstNonWhitespace",
+      "o": "vim::InsertLineBelow",
+      "shift-o": "vim::InsertLineAbove",
+      "v": [
+        "vim::SwitchMode",
+        {
+          "Visual": {
+            "line": false
+          }
+        }
+      ],
+      "shift-v": [
+        "vim::SwitchMode",
+        {
+          "Visual": {
+            "line": true
+          }
+        }
+      ],
+      "p": "vim::Paste",
+      "u": "editor::Undo",
+      "ctrl-r": "editor::Redo",
+      "ctrl-o": "pane::GoBack",
+      "/": [
+        "buffer_search::Deploy",
+        {
+          "focus": true
         }
-    },
-    {
-        "context": "Editor && VimWaiting",
-        "bindings": {
-            "tab": "vim::Tab",
-            "enter": "vim::Enter",
-            "escape": "editor::Cancel"
+      ],
+      "ctrl-f": [
+        "vim::Scroll",
+        "PageDown"
+      ],
+      "ctrl-b": [
+        "vim::Scroll",
+        "PageUp"
+      ],
+      "ctrl-d": [
+        "vim::Scroll",
+        "HalfPageDown"
+      ],
+      "ctrl-u": [
+        "vim::Scroll",
+        "HalfPageUp"
+      ],
+      "ctrl-e": [
+        "vim::Scroll",
+        "LineDown"
+      ],
+      "r": [
+        "vim::PushOperator",
+        "Replace"
+      ]
+    }
+  },
+  {
+    "context": "Editor && vim_operator == n",
+    "bindings": {
+      "0": [
+        "vim::Number",
+        0
+      ]
+    }
+  },
+  {
+    "context": "Editor && vim_operator == g",
+    "bindings": {
+      "g": "vim::StartOfDocument",
+      "h": "editor::Hover",
+      "escape": [
+        "vim::SwitchMode",
+        "Normal"
+      ],
+      "d": "editor::GoToDefinition"
+    }
+  },
+  {
+    "context": "Editor && vim_operator == c",
+    "bindings": {
+      "c": "vim::CurrentLine"
+    }
+  },
+  {
+    "context": "Editor && vim_operator == d",
+    "bindings": {
+      "d": "vim::CurrentLine"
+    }
+  },
+  {
+    "context": "Editor && vim_operator == y",
+    "bindings": {
+      "y": "vim::CurrentLine"
+    }
+  },
+  {
+    "context": "Editor && vim_operator == z",
+    "bindings": {
+      "t": "editor::ScrollCursorTop",
+      "z": "editor::ScrollCursorCenter",
+      "b": "editor::ScrollCursorBottom",
+      "escape": [
+        "vim::SwitchMode",
+        "Normal"
+      ]
+    }
+  },
+  {
+    "context": "Editor && VimObject",
+    "bindings": {
+      "w": "vim::Word",
+      "shift-w": [
+        "vim::Word",
+        {
+          "ignorePunctuation": true
         }
+      ],
+      "s": "vim::Sentence",
+      "'": "vim::Quotes",
+      "`": "vim::BackQuotes",
+      "\"": "vim::DoubleQuotes",
+      "(": "vim::Parentheses",
+      ")": "vim::Parentheses",
+      "[": "vim::SquareBrackets",
+      "]": "vim::SquareBrackets",
+      "{": "vim::CurlyBrackets",
+      "}": "vim::CurlyBrackets",
+      "<": "vim::AngleBrackets",
+      ">": "vim::AngleBrackets"
+    }
+  },
+  {
+    "context": "Editor && vim_mode == visual && !VimWaiting",
+    "bindings": {
+      "u": "editor::Undo",
+      "c": "vim::VisualChange",
+      "d": "vim::VisualDelete",
+      "x": "vim::VisualDelete",
+      "y": "vim::VisualYank",
+      "p": "vim::VisualPaste",
+      "r": [
+        "vim::PushOperator",
+        "Replace"
+      ]
+    }
+  },
+  {
+    "context": "Editor && vim_mode == insert",
+    "bindings": {
+      "escape": "vim::NormalBefore",
+      "ctrl-c": "vim::NormalBefore"
+    }
+  },
+  {
+    "context": "Editor && VimWaiting",
+    "bindings": {
+      "tab": "vim::Tab",
+      "enter": "vim::Enter",
+      "escape": "editor::Cancel"
     }
-]
+  }
+]

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
+    "working_directory": "current_project_directory",
+    // Set the cursor blinking behavior in the terminal.
+    // May take 4 values:
+    //  1. Never blink the cursor, ignoring the terminal mode
+    //         "blinking": "off",
+    //  2. Default the cursor blink to off, but allow the terminal to
+    //     set blinking
+    //         "blinking": "terminal_controlled",
+    //  3. Always blink the cursor, ignoring the terminal mode
+    //         "blinking": "on",
+    "blinking": "terminal_controlled",
+    // Set whether Alternate Scroll mode (code: ?1007) is active by default.
+    // Alternate Scroll mode converts mouse scroll events into up / down key
+    // presses when in the alternate screen (e.g. when running applications
+    // like vim or  less). The terminal can still set and unset this mode.
+    // May take 2 values:
+    //  1. Default alternate scroll mode to on
+    //         "alternate_scroll": "on",
+    //  2. Default alternate scroll mode to off
+    //         "alternate_scroll": "off",
+    "alternate_scroll": "off",
+    // Set whether the option key behaves as the meta key.
+    // May take 2 values:
+    //  1. Rely on default platform handling of option key, on macOS
+    //     this means generating certain unicode characters
+    //         "option_to_meta": false,
+    //  2. Make the option keys behave as a 'meta' key, e.g. for emacs
+    //         "option_to_meta": true,
+    "option_as_meta": false,
+    // Whether or not selecting text in the terminal will automatically
+    // copy to the system clipboard.
+    "copy_on_select": false,
+    // Any key-value pairs added to this list will be added to the terminal's
+    // enviroment. Use `:` to seperate multiple values.
+    "env": {
+      // "KEY": "value1:value2"
+    }
+    // Set the terminal's font size. If this option is not included,
+    // the terminal will default to matching the buffer's font size.
+    // "font_size": "15"
+    // Set the terminal's font family. If this option is not included,
+    // the terminal will default to matching the buffer's font family.
+    // "font_family": "Zed Mono"
+  },
+  // Different settings for specific languages.
+  "languages": {
+    "Plain Text": {
+      "soft_wrap": "preferred_line_length"
+    },
+    "Elixir": {
+      "tab_size": 2
+    },
+    "Go": {
+      "tab_size": 4,
+      "hard_tabs": true
     },
-    // Automatically update Zed
-    "auto_update": true,
-    // Git gutter behavior configuration.
-    "git": {
-        // Control whether the git gutter is shown. May take 2 values:
-        // 1. Show the gutter
-        //      "git_gutter": "tracked_files"
-        // 2. Hide the gutter
-        //      "git_gutter": "hide"
-        "git_gutter": "tracked_files"
+    "Markdown": {
+      "soft_wrap": "preferred_line_length"
     },
-    // Settings specific to journaling
-    "journal": {
-        // The path of the directory where journal entries are stored
-        "path": "~",
-        // What format to display the hours in
-        // May take 2 values:
-        // 1. hour12
-        // 2. hour24
-        "hour_format": "hour12"
+    "JavaScript": {
+      "tab_size": 2
     },
-    // Settings specific to the terminal
-    "terminal": {
-        // What shell to use when opening a terminal. May take 3 values:
-        // 1. Use the system's default terminal configuration in /etc/passwd
-        //      "shell": "system"
-        // 2. A program:
-        //      "shell": {
-        //        "program": "sh"
-        //      }
-        // 3. A program with arguments:
-        //     "shell": {
-        //         "with_arguments": {
-        //           "program": "/bin/bash",
-        //           "arguments": ["--login"]
-        //         }
-        //     }
-        "shell": "system",
-        // What working directory to use when launching the terminal.
-        // May take 4 values:
-        // 1. Use the current file's project directory.  Will Fallback to the
-        //    first project directory strategy if unsuccessful
-        //      "working_directory": "current_project_directory"
-        // 2. Use the first project in this workspace's directory
-        //      "working_directory": "first_project_directory"
-        // 3. Always use this platform's home directory (if we can find it)
-        //     "working_directory": "always_home"
-        // 4. Always use a specific directory. This value will be shell expanded.
-        //    If this path is not a valid directory the terminal will default to
-        //    this platform's home directory  (if we can find it)
-        //      "working_directory": {
-        //        "always": {
-        //          "directory": "~/zed/projects/"
-        //        }
-        //      }
-        //
-        //
-        "working_directory": "current_project_directory",
-        // Set the cursor blinking behavior in the terminal.
-        // May take 4 values:
-        //  1. Never blink the cursor, ignoring the terminal mode
-        //         "blinking": "off",
-        //  2. Default the cursor blink to off, but allow the terminal to
-        //     set blinking
-        //         "blinking": "terminal_controlled",
-        //  3. Always blink the cursor, ignoring the terminal mode
-        //         "blinking": "on",
-        "blinking": "terminal_controlled",
-        // Set whether Alternate Scroll mode (code: ?1007) is active by default.
-        // Alternate Scroll mode converts mouse scroll events into up / down key
-        // presses when in the alternate screen (e.g. when running applications
-        // like vim or  less). The terminal can still set and unset this mode.
-        // May take 2 values:
-        //  1. Default alternate scroll mode to on
-        //         "alternate_scroll": "on",
-        //  2. Default alternate scroll mode to off
-        //         "alternate_scroll": "off",
-        "alternate_scroll": "off",
-        // Set whether the option key behaves as the meta key.
-        // May take 2 values:
-        //  1. Rely on default platform handling of option key, on macOS
-        //     this means generating certain unicode characters
-        //         "option_to_meta": false,
-        //  2. Make the option keys behave as a 'meta' key, e.g. for emacs
-        //         "option_to_meta": true,
-        "option_as_meta": false,
-        // Whether or not selecting text in the terminal will automatically
-        // copy to the system clipboard.
-        "copy_on_select": false,
-        // Any key-value pairs added to this list will be added to the terminal's
-        // enviroment. Use `:` to seperate multiple values.
-        "env": {
-            // "KEY": "value1:value2"
-        }
-        // Set the terminal's font size. If this option is not included,
-        // the terminal will default to matching the buffer's font size.
-        // "font_size": "15"
-        // Set the terminal's font family. If this option is not included,
-        // the terminal will default to matching the buffer's font family.
-        // "font_family": "Zed Mono"
+    "TypeScript": {
+      "tab_size": 2
     },
-    // Different settings for specific languages.
-    "languages": {
-        "Plain Text": {
-            "soft_wrap": "preferred_line_length"
-        },
-        "Elixir": {
-            "tab_size": 2
-        },
-        "Go": {
-            "tab_size": 4,
-            "hard_tabs": true
-        },
-        "Markdown": {
-            "soft_wrap": "preferred_line_length"
-        },
-        "JavaScript": {
-            "tab_size": 2
-        },
-        "TypeScript": {
-            "tab_size": 2
-        },
-        "TSX": {
-            "tab_size": 2
-        },
-        "YAML": {
-            "tab_size": 2
-        }
+    "TSX": {
+      "tab_size": 2
     },
-    // LSP Specific settings.
-    "lsp": {
-        // Specify the LSP name as a key here.
-        // As of 8/10/22, supported LSPs are:
-        // pyright
-        // gopls
-        // rust-analyzer
-        // typescript-language-server
-        // vscode-json-languageserver
-        // "rust-analyzer": {
-        //     //These initialization options are merged into Zed's defaults
-        //     "initialization_options": {
-        //         "checkOnSave": {
-        //             "command": "clippy"
-        //         }
-        //     }
-        // }
+    "YAML": {
+      "tab_size": 2
+    },
+    "JSON": {
+      "tab_size": 2
     }
+  },
+  // LSP Specific settings.
+  "lsp": {
+    // Specify the LSP name as a key here.
+    // As of 8/10/22, supported LSPs are:
+    // pyright
+    // gopls
+    // rust-analyzer
+    // typescript-language-server
+    // vscode-json-languageserver
+    // "rust-analyzer": {
+    //     //These initialization options are merged into Zed's defaults
+    //     "initialization_options": {
+    //         "checkOnSave": {
+    //             "command": "clippy"
+    //         }
+    //     }
+    // }
+  }
 }

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
 }

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"] }

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<Deterministic>,
+    cx_a: &mut TestAppContext,
+    cx_b: &mut TestAppContext,
+) {
+    deterministic.forbid_parking();
+    let mut server = TestServer::start(&deterministic).await;
+    let client_a = server.create_client(cx_a, "user_a").await;
+    let client_b = server.create_client(cx_b, "user_b").await;
+    server
+        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
+        .await;
+    let active_call_a = cx_a.read(ActiveCall::global);
+
+    client_a
+        .fs
+        .insert_tree("/dir", json!({ "a.txt": "Some text\n" }))
+        .await;
+    let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
+    let project_id = active_call_a
+        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
+        .await
+        .unwrap();
+
+    let project_b = client_b.build_remote_project(project_id, cx_b).await;
+
+    // Open a buffer as client A
+    let buffer_a = project_a
+        .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
+        .await
+        .unwrap();
+    let (_, window_a) = cx_a.add_window(|_| EmptyView);
+    let editor_a = cx_a.add_view(&window_a, |cx| {
+        Editor::for_buffer(buffer_a, Some(project_a), cx)
+    });
+    let mut editor_cx_a = EditorTestContext {
+        cx: cx_a,
+        window_id: window_a.id(),
+        editor: editor_a,
+    };
+
+    // Open a buffer as client B
+    let buffer_b = project_b
+        .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
+        .await
+        .unwrap();
+    let (_, window_b) = cx_b.add_window(|_| EmptyView);
+    let editor_b = cx_b.add_view(&window_b, |cx| {
+        Editor::for_buffer(buffer_b, Some(project_b), cx)
+    });
+    let mut editor_cx_b = EditorTestContext {
+        cx: cx_b,
+        window_id: window_b.id(),
+        editor: editor_b,
+    };
+
+    // Test newline above
+    editor_cx_a.set_selections_state(indoc! {"
+        Some textˇ
+    "});
+    editor_cx_b.set_selections_state(indoc! {"
+        Some textˇ
+    "});
+    editor_cx_a.update_editor(|editor, cx| editor.newline_above(&editor::NewlineAbove, cx));
+    deterministic.run_until_parked();
+    editor_cx_a.assert_editor_state(indoc! {"
+        ˇ
+        Some text
+    "});
+    editor_cx_b.assert_editor_state(indoc! {"
+
+        Some textˇ
+    "});
+
+    // Test newline below
+    editor_cx_a.set_selections_state(indoc! {"
+
+        Some textˇ
+    "});
+    editor_cx_b.set_selections_state(indoc! {"
+
+        Some textˇ
+    "});
+    editor_cx_a.update_editor(|editor, cx| editor.newline_below(&editor::NewlineBelow, cx));
+    deterministic.run_until_parked();
+    editor_cx_a.assert_editor_state(indoc! {"
+
+        Some text
+        ˇ
+    "});
+    editor_cx_b.assert_editor_state(indoc! {"
+
+        Some textˇ
+
+    "});
+}
+
 #[gpui::test(iterations = 10)]
 async fn test_leaving_worktree_while_opening_buffer(
     deterministic: Arc<Deterministic>,
@@ -5860,10 +5960,17 @@ async fn test_basic_following(
 
     // Client A updates their selections in those editors
     editor_a1.update(cx_a, |editor, cx| {
-        editor.change_selections(None, cx, |s| s.select_ranges([0..1]))
+        editor.handle_input("a", cx);
+        editor.handle_input("b", cx);
+        editor.handle_input("c", cx);
+        editor.select_left(&Default::default(), cx);
+        assert_eq!(editor.selections.ranges(cx), vec![3..2]);
     });
     editor_a2.update(cx_a, |editor, cx| {
-        editor.change_selections(None, cx, |s| s.select_ranges([2..3]))
+        editor.handle_input("d", cx);
+        editor.handle_input("e", cx);
+        editor.select_left(&Default::default(), cx);
+        assert_eq!(editor.selections.ranges(cx), vec![2..1]);
     });
 
     // When client B starts following client A, all visible view states are replicated to client B.
@@ -5876,6 +5983,27 @@ async fn test_basic_following(
         .await
         .unwrap();
 
+    cx_c.foreground().run_until_parked();
+    let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| {
+        workspace
+            .active_item(cx)
+            .unwrap()
+            .downcast::<Editor>()
+            .unwrap()
+    });
+    assert_eq!(
+        cx_b.read(|cx| editor_b2.project_path(cx)),
+        Some((worktree_id, "2.txt").into())
+    );
+    assert_eq!(
+        editor_b2.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
+        vec![2..1]
+    );
+    assert_eq!(
+        editor_b1.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
+        vec![3..2]
+    );
+
     cx_c.foreground().run_until_parked();
     let active_call_c = cx_c.read(ActiveCall::global);
     let project_c = client_c.build_remote_project(project_id, cx_c).await;
@@ -6031,26 +6159,6 @@ async fn test_basic_following(
         });
     }
 
-    let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| {
-        workspace
-            .active_item(cx)
-            .unwrap()
-            .downcast::<Editor>()
-            .unwrap()
-    });
-    assert_eq!(
-        cx_b.read(|cx| editor_b2.project_path(cx)),
-        Some((worktree_id, "2.txt").into())
-    );
-    assert_eq!(
-        editor_b2.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
-        vec![2..3]
-    );
-    assert_eq!(
-        editor_b1.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
-        vec![0..1]
-    );
-
     // When client A activates a different editor, client B does so as well.
     workspace_a.update(cx_a, |workspace, cx| {
         workspace.activate_item(&editor_a1, cx)

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"] }

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<dyn HttpClient>, node_runtime: Arc<NodeRuntime>, cx: &mut AppContext) {
     // Disable Copilot for stable releases.
@@ -95,15 +104,38 @@ pub fn init(http: Arc<dyn HttpClient>, node_runtime: Arc<NodeRuntime>, cx: &mut
 
 enum CopilotServer {
     Disabled,
-    Starting {
-        task: Shared<Task<()>>,
-    },
+    Starting { task: Shared<Task<()>> },
     Error(Arc<str>),
-    Started {
-        server: Arc<LanguageServer>,
-        status: SignInStatus,
-        subscriptions_by_buffer_id: HashMap<usize, gpui::Subscription>,
-    },
+    Running(RunningCopilotServer),
+}
+
+impl CopilotServer {
+    fn as_authenticated(&mut self) -> Result<&mut RunningCopilotServer> {
+        let server = self.as_running()?;
+        if matches!(server.sign_in_status, SignInStatus::Authorized { .. }) {
+            Ok(server)
+        } else {
+            Err(anyhow!("must sign in before using copilot"))
+        }
+    }
+
+    fn as_running(&mut self) -> Result<&mut RunningCopilotServer> {
+        match self {
+            CopilotServer::Starting { .. } => Err(anyhow!("copilot is still starting")),
+            CopilotServer::Disabled => Err(anyhow!("copilot is disabled")),
+            CopilotServer::Error(error) => Err(anyhow!(
+                "copilot was not started because of an error: {}",
+                error
+            )),
+            CopilotServer::Running(server) => Ok(server),
+        }
+    }
+}
+
+struct RunningCopilotServer {
+    lsp: Arc<LanguageServer>,
+    sign_in_status: SignInStatus,
+    registered_buffers: HashMap<usize, RegisteredBuffer>,
 }
 
 #[derive(Clone, Debug)]
@@ -138,8 +170,104 @@ impl Status {
     }
 }
 
-#[derive(Debug, PartialEq, Eq)]
+struct RegisteredBuffer {
+    id: usize,
+    uri: lsp::Url,
+    language_id: String,
+    snapshot: BufferSnapshot,
+    snapshot_version: i32,
+    _subscriptions: [gpui::Subscription; 2],
+    pending_buffer_change: Task<Option<()>>,
+}
+
+impl RegisteredBuffer {
+    fn report_changes(
+        &mut self,
+        buffer: &ModelHandle<Buffer>,
+        cx: &mut ModelContext<Copilot>,
+    ) -> oneshot::Receiver<(i32, BufferSnapshot)> {
+        let id = self.id;
+        let (done_tx, done_rx) = oneshot::channel();
+
+        if buffer.read(cx).version() == self.snapshot.version {
+            let _ = done_tx.send((self.snapshot_version, self.snapshot.clone()));
+        } else {
+            let buffer = buffer.downgrade();
+            let prev_pending_change =
+                mem::replace(&mut self.pending_buffer_change, Task::ready(None));
+            self.pending_buffer_change = cx.spawn_weak(|copilot, mut cx| async move {
+                prev_pending_change.await;
+
+                let old_version = copilot.upgrade(&cx)?.update(&mut cx, |copilot, _| {
+                    let server = copilot.server.as_authenticated().log_err()?;
+                    let buffer = server.registered_buffers.get_mut(&id)?;
+                    Some(buffer.snapshot.version.clone())
+                })?;
+                let new_snapshot = buffer
+                    .upgrade(&cx)?
+                    .read_with(&cx, |buffer, _| buffer.snapshot());
+
+                let content_changes = cx
+                    .background()
+                    .spawn({
+                        let new_snapshot = new_snapshot.clone();
+                        async move {
+                            new_snapshot
+                                .edits_since::<(PointUtf16, usize)>(&old_version)
+                                .map(|edit| {
+                                    let edit_start = edit.new.start.0;
+                                    let edit_end = edit_start + (edit.old.end.0 - edit.old.start.0);
+                                    let new_text = new_snapshot
+                                        .text_for_range(edit.new.start.1..edit.new.end.1)
+                                        .collect();
+                                    lsp::TextDocumentContentChangeEvent {
+                                        range: Some(lsp::Range::new(
+                                            point_to_lsp(edit_start),
+                                            point_to_lsp(edit_end),
+                                        )),
+                                        range_length: None,
+                                        text: new_text,
+                                    }
+                                })
+                                .collect::<Vec<_>>()
+                        }
+                    })
+                    .await;
+
+                copilot.upgrade(&cx)?.update(&mut cx, |copilot, _| {
+                    let server = copilot.server.as_authenticated().log_err()?;
+                    let buffer = server.registered_buffers.get_mut(&id)?;
+                    if !content_changes.is_empty() {
+                        buffer.snapshot_version += 1;
+                        buffer.snapshot = new_snapshot;
+                        server
+                            .lsp
+                            .notify::<lsp::notification::DidChangeTextDocument>(
+                                lsp::DidChangeTextDocumentParams {
+                                    text_document: lsp::VersionedTextDocumentIdentifier::new(
+                                        buffer.uri.clone(),
+                                        buffer.snapshot_version,
+                                    ),
+                                    content_changes,
+                                },
+                            )
+                            .log_err();
+                    }
+                    let _ = done_tx.send((buffer.snapshot_version, buffer.snapshot.clone()));
+                    Some(())
+                })?;
+
+                Some(())
+            });
+        }
+
+        done_rx
+    }
+}
+
+#[derive(Debug)]
 pub struct Completion {
+    uuid: String,
     pub range: Range<Anchor>,
     pub text: String,
 }
@@ -148,6 +276,7 @@ pub struct Copilot {
     http: Arc<dyn HttpClient>,
     node_runtime: Arc<NodeRuntime>,
     server: CopilotServer,
+    buffers: HashMap<usize, WeakModelHandle<Buffer>>,
 }
 
 impl Entity for Copilot {
@@ -172,7 +301,7 @@ impl Copilot {
             let http = http.clone();
             let node_runtime = node_runtime.clone();
             move |this, cx| {
-                if cx.global::<Settings>().enable_copilot_integration {
+                if cx.global::<Settings>().features.copilot {
                     if matches!(this.server, CopilotServer::Disabled) {
                         let start_task = cx
                             .spawn({
@@ -194,12 +323,14 @@ impl Copilot {
         })
         .detach();
 
-        if cx.global::<Settings>().enable_copilot_integration {
+        if cx.global::<Settings>().features.copilot {
             let start_task = cx
                 .spawn({
                     let http = http.clone();
                     let node_runtime = node_runtime.clone();
-                    move |this, cx| Self::start_language_server(http, node_runtime, this, cx)
+                    move |this, cx| async {
+                        Self::start_language_server(http, node_runtime, this, cx).await
+                    }
                 })
                 .shared();
 
@@ -207,12 +338,14 @@ impl Copilot {
                 http,
                 node_runtime,
                 server: CopilotServer::Starting { task: start_task },
+                buffers: Default::default(),
             }
         } else {
             Self {
                 http,
                 node_runtime,
                 server: CopilotServer::Disabled,
+                buffers: Default::default(),
             }
         }
     }
@@ -225,11 +358,12 @@ impl Copilot {
         let this = cx.add_model(|cx| Self {
             http: http.clone(),
             node_runtime: NodeRuntime::new(http, cx.background().clone()),
-            server: CopilotServer::Started {
-                server: Arc::new(server),
-                status: SignInStatus::Authorized,
-                subscriptions_by_buffer_id: Default::default(),
-            },
+            server: CopilotServer::Running(RunningCopilotServer {
+                lsp: Arc::new(server),
+                sign_in_status: SignInStatus::Authorized,
+                registered_buffers: Default::default(),
+            }),
+            buffers: Default::default(),
         });
         (this, fake_server)
     }
@@ -281,6 +415,19 @@ impl Copilot {
                     )
                     .detach();
 
+                server
+                    .request::<request::SetEditorInfo>(request::SetEditorInfoParams {
+                        editor_info: request::EditorInfo {
+                            name: "zed".into(),
+                            version: env!("CARGO_PKG_VERSION").into(),
+                        },
+                        editor_plugin_info: request::EditorPluginInfo {
+                            name: "zed-copilot".into(),
+                            version: "0.0.1".into(),
+                        },
+                    })
+                    .await?;
+
                 anyhow::Ok((server, status))
             };
 
@@ -289,11 +436,11 @@ impl Copilot {
                 cx.notify();
                 match server {
                     Ok((server, status)) => {
-                        this.server = CopilotServer::Started {
-                            server,
-                            status: SignInStatus::SignedOut,
-                            subscriptions_by_buffer_id: Default::default(),
-                        };
+                        this.server = CopilotServer::Running(RunningCopilotServer {
+                            lsp: server,
+                            sign_in_status: SignInStatus::SignedOut,
+                            registered_buffers: Default::default(),
+                        });
                         this.update_sign_in_status(status, cx);
                     }
                     Err(error) => {
@@ -306,8 +453,8 @@ impl Copilot {
     }
 
     fn sign_in(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
-        if let CopilotServer::Started { server, status, .. } = &mut self.server {
-            let task = match status {
+        if let CopilotServer::Running(server) = &mut self.server {
+            let task = match &server.sign_in_status {
                 SignInStatus::Authorized { .. } | SignInStatus::Unauthorized { .. } => {
                     Task::ready(Ok(())).shared()
                 }
@@ -316,11 +463,11 @@ impl Copilot {
                     task.clone()
                 }
                 SignInStatus::SignedOut => {
-                    let server = server.clone();
+                    let lsp = server.lsp.clone();
                     let task = cx
                         .spawn(|this, mut cx| async move {
                             let sign_in = async {
-                                let sign_in = server
+                                let sign_in = lsp
                                     .request::<request::SignInInitiate>(
                                         request::SignInInitiateParams {},
                                     )
@@ -331,8 +478,10 @@ impl Copilot {
                                     }
                                     request::SignInInitiateResult::PromptUserDeviceFlow(flow) => {
                                         this.update(&mut cx, |this, cx| {
-                                            if let CopilotServer::Started { status, .. } =
-                                                &mut this.server
+                                            if let CopilotServer::Running(RunningCopilotServer {
+                                                sign_in_status: status,
+                                                ..
+                                            }) = &mut this.server
                                             {
                                                 if let SignInStatus::SigningIn {
                                                     prompt: prompt_flow,
@@ -344,7 +493,7 @@ impl Copilot {
                                                 }
                                             }
                                         });
-                                        let response = server
+                                        let response = lsp
                                             .request::<request::SignInConfirm>(
                                                 request::SignInConfirmParams {
                                                     user_code: flow.user_code,
@@ -372,7 +521,7 @@ impl Copilot {
                             })
                         })
                         .shared();
-                    *status = SignInStatus::SigningIn {
+                    server.sign_in_status = SignInStatus::SigningIn {
                         prompt: None,
                         task: task.clone(),
                     };
@@ -391,10 +540,8 @@ impl Copilot {
     }
 
     fn sign_out(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
-        if let CopilotServer::Started { server, status, .. } = &mut self.server {
-            *status = SignInStatus::SignedOut;
-            cx.notify();
-
+        self.update_sign_in_status(request::SignInStatus::NotSignedIn, cx);
+        if let CopilotServer::Running(RunningCopilotServer { lsp: server, .. }) = &self.server {
             let server = server.clone();
             cx.background().spawn(async move {
                 server
@@ -428,6 +575,135 @@ impl Copilot {
         cx.foreground().spawn(start_task)
     }
 
+    pub fn register_buffer(&mut self, buffer: &ModelHandle<Buffer>, cx: &mut ModelContext<Self>) {
+        let buffer_id = buffer.id();
+        self.buffers.insert(buffer_id, buffer.downgrade());
+
+        if let CopilotServer::Running(RunningCopilotServer {
+            lsp: server,
+            sign_in_status: status,
+            registered_buffers,
+            ..
+        }) = &mut self.server
+        {
+            if !matches!(status, SignInStatus::Authorized { .. }) {
+                return;
+            }
+
+            registered_buffers.entry(buffer.id()).or_insert_with(|| {
+                let uri: lsp::Url = uri_for_buffer(buffer, cx);
+                let language_id = id_for_language(buffer.read(cx).language());
+                let snapshot = buffer.read(cx).snapshot();
+                server
+                    .notify::<lsp::notification::DidOpenTextDocument>(
+                        lsp::DidOpenTextDocumentParams {
+                            text_document: lsp::TextDocumentItem {
+                                uri: uri.clone(),
+                                language_id: language_id.clone(),
+                                version: 0,
+                                text: snapshot.text(),
+                            },
+                        },
+                    )
+                    .log_err();
+
+                RegisteredBuffer {
+                    id: buffer_id,
+                    uri,
+                    language_id,
+                    snapshot,
+                    snapshot_version: 0,
+                    pending_buffer_change: Task::ready(Some(())),
+                    _subscriptions: [
+                        cx.subscribe(buffer, |this, buffer, event, cx| {
+                            this.handle_buffer_event(buffer, event, cx).log_err();
+                        }),
+                        cx.observe_release(buffer, move |this, _buffer, _cx| {
+                            this.buffers.remove(&buffer_id);
+                            this.unregister_buffer(buffer_id);
+                        }),
+                    ],
+                }
+            });
+        }
+    }
+
+    fn handle_buffer_event(
+        &mut self,
+        buffer: ModelHandle<Buffer>,
+        event: &language::Event,
+        cx: &mut ModelContext<Self>,
+    ) -> Result<()> {
+        if let Ok(server) = self.server.as_running() {
+            if let Some(registered_buffer) = server.registered_buffers.get_mut(&buffer.id()) {
+                match event {
+                    language::Event::Edited => {
+                        let _ = registered_buffer.report_changes(&buffer, cx);
+                    }
+                    language::Event::Saved => {
+                        server
+                            .lsp
+                            .notify::<lsp::notification::DidSaveTextDocument>(
+                                lsp::DidSaveTextDocumentParams {
+                                    text_document: lsp::TextDocumentIdentifier::new(
+                                        registered_buffer.uri.clone(),
+                                    ),
+                                    text: None,
+                                },
+                            )?;
+                    }
+                    language::Event::FileHandleChanged | language::Event::LanguageChanged => {
+                        let new_language_id = id_for_language(buffer.read(cx).language());
+                        let new_uri = uri_for_buffer(&buffer, cx);
+                        if new_uri != registered_buffer.uri
+                            || new_language_id != registered_buffer.language_id
+                        {
+                            let old_uri = mem::replace(&mut registered_buffer.uri, new_uri);
+                            registered_buffer.language_id = new_language_id;
+                            server
+                                .lsp
+                                .notify::<lsp::notification::DidCloseTextDocument>(
+                                    lsp::DidCloseTextDocumentParams {
+                                        text_document: lsp::TextDocumentIdentifier::new(old_uri),
+                                    },
+                                )?;
+                            server
+                                .lsp
+                                .notify::<lsp::notification::DidOpenTextDocument>(
+                                    lsp::DidOpenTextDocumentParams {
+                                        text_document: lsp::TextDocumentItem::new(
+                                            registered_buffer.uri.clone(),
+                                            registered_buffer.language_id.clone(),
+                                            registered_buffer.snapshot_version,
+                                            registered_buffer.snapshot.text(),
+                                        ),
+                                    },
+                                )?;
+                        }
+                    }
+                    _ => {}
+                }
+            }
+        }
+
+        Ok(())
+    }
+
+    fn unregister_buffer(&mut self, buffer_id: usize) {
+        if let Ok(server) = self.server.as_running() {
+            if let Some(buffer) = server.registered_buffers.remove(&buffer_id) {
+                server
+                    .lsp
+                    .notify::<lsp::notification::DidCloseTextDocument>(
+                        lsp::DidCloseTextDocumentParams {
+                            text_document: lsp::TextDocumentIdentifier::new(buffer.uri),
+                        },
+                    )
+                    .log_err();
+            }
+        }
+    }
+
     pub fn completions<T>(
         &mut self,
         buffer: &ModelHandle<Buffer>,
@@ -452,6 +728,51 @@ impl Copilot {
         self.request_completions::<request::GetCompletionsCycling, _>(buffer, position, cx)
     }
 
+    pub fn accept_completion(
+        &mut self,
+        completion: &Completion,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<()>> {
+        let server = match self.server.as_authenticated() {
+            Ok(server) => server,
+            Err(error) => return Task::ready(Err(error)),
+        };
+        let request =
+            server
+                .lsp
+                .request::<request::NotifyAccepted>(request::NotifyAcceptedParams {
+                    uuid: completion.uuid.clone(),
+                });
+        cx.background().spawn(async move {
+            request.await?;
+            Ok(())
+        })
+    }
+
+    pub fn discard_completions(
+        &mut self,
+        completions: &[Completion],
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<()>> {
+        let server = match self.server.as_authenticated() {
+            Ok(server) => server,
+            Err(error) => return Task::ready(Err(error)),
+        };
+        let request =
+            server
+                .lsp
+                .request::<request::NotifyRejected>(request::NotifyRejectedParams {
+                    uuids: completions
+                        .iter()
+                        .map(|completion| completion.uuid.clone())
+                        .collect(),
+                });
+        cx.background().spawn(async move {
+            request.await?;
+            Ok(())
+        })
+    }
+
     fn request_completions<R, T>(
         &mut self,
         buffer: &ModelHandle<Buffer>,
@@ -459,116 +780,48 @@ impl Copilot {
         cx: &mut ModelContext<Self>,
     ) -> Task<Result<Vec<Completion>>>
     where
-        R: lsp::request::Request<
-            Params = request::GetCompletionsParams,
-            Result = request::GetCompletionsResult,
-        >,
+        R: 'static
+            + lsp::request::Request<
+                Params = request::GetCompletionsParams,
+                Result = request::GetCompletionsResult,
+            >,
         T: ToPointUtf16,
     {
-        let buffer_id = buffer.id();
-        let uri: lsp::Url = format!("buffer://{}", buffer_id).parse().unwrap();
-        let snapshot = buffer.read(cx).snapshot();
-        let server = match &mut self.server {
-            CopilotServer::Starting { .. } => {
-                return Task::ready(Err(anyhow!("copilot is still starting")))
-            }
-            CopilotServer::Disabled => return Task::ready(Err(anyhow!("copilot is disabled"))),
-            CopilotServer::Error(error) => {
-                return Task::ready(Err(anyhow!(
-                    "copilot was not started because of an error: {}",
-                    error
-                )))
-            }
-            CopilotServer::Started {
-                server,
-                status,
-                subscriptions_by_buffer_id,
-            } => {
-                if matches!(status, SignInStatus::Authorized { .. }) {
-                    subscriptions_by_buffer_id
-                        .entry(buffer_id)
-                        .or_insert_with(|| {
-                            server
-                                .notify::<lsp::notification::DidOpenTextDocument>(
-                                    lsp::DidOpenTextDocumentParams {
-                                        text_document: lsp::TextDocumentItem {
-                                            uri: uri.clone(),
-                                            language_id: id_for_language(
-                                                buffer.read(cx).language(),
-                                            ),
-                                            version: 0,
-                                            text: snapshot.text(),
-                                        },
-                                    },
-                                )
-                                .log_err();
-
-                            let uri = uri.clone();
-                            cx.observe_release(buffer, move |this, _, _| {
-                                if let CopilotServer::Started {
-                                    server,
-                                    subscriptions_by_buffer_id,
-                                    ..
-                                } = &mut this.server
-                                {
-                                    server
-                                        .notify::<lsp::notification::DidCloseTextDocument>(
-                                            lsp::DidCloseTextDocumentParams {
-                                                text_document: lsp::TextDocumentIdentifier::new(
-                                                    uri.clone(),
-                                                ),
-                                            },
-                                        )
-                                        .log_err();
-                                    subscriptions_by_buffer_id.remove(&buffer_id);
-                                }
-                            })
-                        });
+        self.register_buffer(buffer, cx);
 
-                    server.clone()
-                } else {
-                    return Task::ready(Err(anyhow!("must sign in before using copilot")));
-                }
-            }
+        let server = match self.server.as_authenticated() {
+            Ok(server) => server,
+            Err(error) => return Task::ready(Err(error)),
         };
-
+        let lsp = server.lsp.clone();
+        let registered_buffer = server.registered_buffers.get_mut(&buffer.id()).unwrap();
+        let snapshot = registered_buffer.report_changes(buffer, cx);
+        let buffer = buffer.read(cx);
+        let uri = registered_buffer.uri.clone();
         let settings = cx.global::<Settings>();
-        let position = position.to_point_utf16(&snapshot);
-        let language = snapshot.language_at(position);
+        let position = position.to_point_utf16(buffer);
+        let language = buffer.language_at(position);
         let language_name = language.map(|language| language.name());
         let language_name = language_name.as_deref();
         let tab_size = settings.tab_size(language_name);
         let hard_tabs = settings.hard_tabs(language_name);
-        let language_id = id_for_language(language);
-
-        let path;
-        let relative_path;
-        if let Some(file) = snapshot.file() {
-            if let Some(file) = file.as_local() {
-                path = file.abs_path(cx);
-            } else {
-                path = file.full_path(cx);
-            }
-            relative_path = file.path().to_path_buf();
-        } else {
-            path = PathBuf::new();
-            relative_path = PathBuf::new();
-        }
+        let relative_path = buffer
+            .file()
+            .map(|file| file.path().to_path_buf())
+            .unwrap_or_default();
 
-        cx.background().spawn(async move {
-            let result = server
+        cx.foreground().spawn(async move {
+            let (version, snapshot) = snapshot.await?;
+            let result = lsp
                 .request::<R>(request::GetCompletionsParams {
                     doc: request::GetCompletionsDocument {
-                        source: snapshot.text(),
+                        uri,
                         tab_size: tab_size.into(),
                         indent_size: 1,
                         insert_spaces: !hard_tabs,
-                        uri,
-                        path: path.to_string_lossy().into(),
                         relative_path: relative_path.to_string_lossy().into(),
-                        language_id,
                         position: point_to_lsp(position),
-                        version: 0,
+                        version: version.try_into().unwrap(),
                     },
                 })
                 .await?;
@@ -581,6 +834,7 @@ impl Copilot {
                     let end =
                         snapshot.clip_point_utf16(point_from_lsp(completion.range.end), Bias::Left);
                     Completion {
+                        uuid: completion.uuid,
                         range: snapshot.anchor_before(start)..snapshot.anchor_after(end),
                         text: completion.text,
                     }
@@ -595,14 +849,16 @@ impl Copilot {
             CopilotServer::Starting { task } => Status::Starting { task: task.clone() },
             CopilotServer::Disabled => Status::Disabled,
             CopilotServer::Error(error) => Status::Error(error.clone()),
-            CopilotServer::Started { status, .. } => match status {
-                SignInStatus::Authorized { .. } => Status::Authorized,
-                SignInStatus::Unauthorized { .. } => Status::Unauthorized,
-                SignInStatus::SigningIn { prompt, .. } => Status::SigningIn {
-                    prompt: prompt.clone(),
-                },
-                SignInStatus::SignedOut => Status::SignedOut,
-            },
+            CopilotServer::Running(RunningCopilotServer { sign_in_status, .. }) => {
+                match sign_in_status {
+                    SignInStatus::Authorized { .. } => Status::Authorized,
+                    SignInStatus::Unauthorized { .. } => Status::Unauthorized,
+                    SignInStatus::SigningIn { prompt, .. } => Status::SigningIn {
+                        prompt: prompt.clone(),
+                    },
+                    SignInStatus::SignedOut => Status::SignedOut,
+                }
+            }
         }
     }
 
@@ -611,14 +867,34 @@ impl Copilot {
         lsp_status: request::SignInStatus,
         cx: &mut ModelContext<Self>,
     ) {
-        if let CopilotServer::Started { status, .. } = &mut self.server {
-            *status = match lsp_status {
+        self.buffers.retain(|_, buffer| buffer.is_upgradable(cx));
+
+        if let Ok(server) = self.server.as_running() {
+            match lsp_status {
                 request::SignInStatus::Ok { .. }
                 | request::SignInStatus::MaybeOk { .. }
-                | request::SignInStatus::AlreadySignedIn { .. } => SignInStatus::Authorized,
-                request::SignInStatus::NotAuthorized { .. } => SignInStatus::Unauthorized,
-                request::SignInStatus::NotSignedIn => SignInStatus::SignedOut,
-            };
+                | request::SignInStatus::AlreadySignedIn { .. } => {
+                    server.sign_in_status = SignInStatus::Authorized;
+                    for buffer in self.buffers.values().cloned().collect::<Vec<_>>() {
+                        if let Some(buffer) = buffer.upgrade(cx) {
+                            self.register_buffer(&buffer, cx);
+                        }
+                    }
+                }
+                request::SignInStatus::NotAuthorized { .. } => {
+                    server.sign_in_status = SignInStatus::Unauthorized;
+                    for buffer_id in self.buffers.keys().copied().collect::<Vec<_>>() {
+                        self.unregister_buffer(buffer_id);
+                    }
+                }
+                request::SignInStatus::NotSignedIn => {
+                    server.sign_in_status = SignInStatus::SignedOut;
+                    for buffer_id in self.buffers.keys().copied().collect::<Vec<_>>() {
+                        self.unregister_buffer(buffer_id);
+                    }
+                }
+            }
+
             cx.notify();
         }
     }
@@ -633,6 +909,14 @@ fn id_for_language(language: Option<&Arc<Language>>) -> String {
     }
 }
 
+fn uri_for_buffer(buffer: &ModelHandle<Buffer>, cx: &AppContext) -> lsp::Url {
+    if let Some(file) = buffer.read(cx).file().and_then(|file| file.as_local()) {
+        lsp::Url::from_file_path(file.abs_path(cx)).unwrap()
+    } else {
+        format!("buffer://{}", buffer.id()).parse().unwrap()
+    }
+}
+
 async fn clear_copilot_dir() {
     remove_matching(&paths::COPILOT_DIR, |_| true).await
 }
@@ -704,3 +988,226 @@ async fn get_copilot_lsp(http: Arc<dyn HttpClient>) -> anyhow::Result<PathBuf> {
         }
     }
 }
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use gpui::{executor::Deterministic, TestAppContext};
+
+    #[gpui::test(iterations = 10)]
+    async fn test_buffer_management(deterministic: Arc<Deterministic>, cx: &mut TestAppContext) {
+        deterministic.forbid_parking();
+        let (copilot, mut lsp) = Copilot::fake(cx);
+
+        let buffer_1 = cx.add_model(|cx| Buffer::new(0, "Hello", cx));
+        let buffer_1_uri: lsp::Url = format!("buffer://{}", buffer_1.id()).parse().unwrap();
+        copilot.update(cx, |copilot, cx| copilot.register_buffer(&buffer_1, cx));
+        assert_eq!(
+            lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
+                .await,
+            lsp::DidOpenTextDocumentParams {
+                text_document: lsp::TextDocumentItem::new(
+                    buffer_1_uri.clone(),
+                    "plaintext".into(),
+                    0,
+                    "Hello".into()
+                ),
+            }
+        );
+
+        let buffer_2 = cx.add_model(|cx| Buffer::new(0, "Goodbye", cx));
+        let buffer_2_uri: lsp::Url = format!("buffer://{}", buffer_2.id()).parse().unwrap();
+        copilot.update(cx, |copilot, cx| copilot.register_buffer(&buffer_2, cx));
+        assert_eq!(
+            lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
+                .await,
+            lsp::DidOpenTextDocumentParams {
+                text_document: lsp::TextDocumentItem::new(
+                    buffer_2_uri.clone(),
+                    "plaintext".into(),
+                    0,
+                    "Goodbye".into()
+                ),
+            }
+        );
+
+        buffer_1.update(cx, |buffer, cx| buffer.edit([(5..5, " world")], None, cx));
+        assert_eq!(
+            lsp.receive_notification::<lsp::notification::DidChangeTextDocument>()
+                .await,
+            lsp::DidChangeTextDocumentParams {
+                text_document: lsp::VersionedTextDocumentIdentifier::new(buffer_1_uri.clone(), 1),
+                content_changes: vec![lsp::TextDocumentContentChangeEvent {
+                    range: Some(lsp::Range::new(
+                        lsp::Position::new(0, 5),
+                        lsp::Position::new(0, 5)
+                    )),
+                    range_length: None,
+                    text: " world".into(),
+                }],
+            }
+        );
+
+        // Ensure updates to the file are reflected in the LSP.
+        buffer_1
+            .update(cx, |buffer, cx| {
+                buffer.file_updated(
+                    Arc::new(File {
+                        abs_path: "/root/child/buffer-1".into(),
+                        path: Path::new("child/buffer-1").into(),
+                    }),
+                    cx,
+                )
+            })
+            .await;
+        assert_eq!(
+            lsp.receive_notification::<lsp::notification::DidCloseTextDocument>()
+                .await,
+            lsp::DidCloseTextDocumentParams {
+                text_document: lsp::TextDocumentIdentifier::new(buffer_1_uri),
+            }
+        );
+        let buffer_1_uri = lsp::Url::from_file_path("/root/child/buffer-1").unwrap();
+        assert_eq!(
+            lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
+                .await,
+            lsp::DidOpenTextDocumentParams {
+                text_document: lsp::TextDocumentItem::new(
+                    buffer_1_uri.clone(),
+                    "plaintext".into(),
+                    1,
+                    "Hello world".into()
+                ),
+            }
+        );
+
+        // Ensure all previously-registered buffers are closed when signing out.
+        lsp.handle_request::<request::SignOut, _, _>(|_, _| async {
+            Ok(request::SignOutResult {})
+        });
+        copilot
+            .update(cx, |copilot, cx| copilot.sign_out(cx))
+            .await
+            .unwrap();
+        assert_eq!(
+            lsp.receive_notification::<lsp::notification::DidCloseTextDocument>()
+                .await,
+            lsp::DidCloseTextDocumentParams {
+                text_document: lsp::TextDocumentIdentifier::new(buffer_2_uri.clone()),
+            }
+        );
+        assert_eq!(
+            lsp.receive_notification::<lsp::notification::DidCloseTextDocument>()
+                .await,
+            lsp::DidCloseTextDocumentParams {
+                text_document: lsp::TextDocumentIdentifier::new(buffer_1_uri.clone()),
+            }
+        );
+
+        // Ensure all previously-registered buffers are re-opened when signing in.
+        lsp.handle_request::<request::SignInInitiate, _, _>(|_, _| async {
+            Ok(request::SignInInitiateResult::AlreadySignedIn {
+                user: "user-1".into(),
+            })
+        });
+        copilot
+            .update(cx, |copilot, cx| copilot.sign_in(cx))
+            .await
+            .unwrap();
+        assert_eq!(
+            lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
+                .await,
+            lsp::DidOpenTextDocumentParams {
+                text_document: lsp::TextDocumentItem::new(
+                    buffer_2_uri.clone(),
+                    "plaintext".into(),
+                    0,
+                    "Goodbye".into()
+                ),
+            }
+        );
+        assert_eq!(
+            lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
+                .await,
+            lsp::DidOpenTextDocumentParams {
+                text_document: lsp::TextDocumentItem::new(
+                    buffer_1_uri.clone(),
+                    "plaintext".into(),
+                    0,
+                    "Hello world".into()
+                ),
+            }
+        );
+
+        // Dropping a buffer causes it to be closed on the LSP side as well.
+        cx.update(|_| drop(buffer_2));
+        assert_eq!(
+            lsp.receive_notification::<lsp::notification::DidCloseTextDocument>()
+                .await,
+            lsp::DidCloseTextDocumentParams {
+                text_document: lsp::TextDocumentIdentifier::new(buffer_2_uri),
+            }
+        );
+    }
+
+    struct File {
+        abs_path: PathBuf,
+        path: Arc<Path>,
+    }
+
+    impl language::File for File {
+        fn as_local(&self) -> Option<&dyn language::LocalFile> {
+            Some(self)
+        }
+
+        fn mtime(&self) -> std::time::SystemTime {
+            todo!()
+        }
+
+        fn path(&self) -> &Arc<Path> {
+            &self.path
+        }
+
+        fn full_path(&self, _: &AppContext) -> PathBuf {
+            todo!()
+        }
+
+        fn file_name<'a>(&'a self, _: &'a AppContext) -> &'a std::ffi::OsStr {
+            todo!()
+        }
+
+        fn is_deleted(&self) -> bool {
+            todo!()
+        }
+
+        fn as_any(&self) -> &dyn std::any::Any {
+            todo!()
+        }
+
+        fn to_proto(&self) -> rpc::proto::File {
+            todo!()
+        }
+    }
+
+    impl language::LocalFile for File {
+        fn abs_path(&self, _: &AppContext) -> PathBuf {
+            self.abs_path.clone()
+        }
+
+        fn load(&self, _: &AppContext) -> Task<Result<String>> {
+            todo!()
+        }
+
+        fn buffer_reloaded(
+            &self,
+            _: u64,
+            _: &clock::Global,
+            _: language::RopeFingerprint,
+            _: ::fs::LineEnding,
+            _: std::time::SystemTime,
+            _: &mut AppContext,
+        ) {
+            todo!()
+        }
+    }
+}

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<String>,
+}
+
+impl lsp::request::Request for NotifyRejected {
+    type Params = NotifyRejectedParams;
+    type Result = String;
+    const METHOD: &'static str = "notifyRejected";
+}

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<Self>) {
@@ -147,6 +163,7 @@ impl CopilotCodeVerification {
     }
 
     fn render_prompting_modal(
+        connect_clicked: bool,
         data: &PromptUserDeviceFlow,
         style: &theme::Copilot,
         cx: &mut ViewContext<Self>,
@@ -195,13 +212,20 @@ impl CopilotCodeVerification {
                     .with_style(style.auth.prompting.hint.container.clone())
                     .boxed(),
                 theme::ui::cta_button_with_click::<ConnectButton, _, _, _>(
-                    "Connect to GitHub",
+                    if connect_clicked {
+                        "Waiting for connection..."
+                    } else {
+                        "Connect to GitHub"
+                    },
                     style.auth.content_width,
                     &style.auth.cta_button,
                     cx,
                     {
                         let verification_uri = data.verification_uri.clone();
-                        move |_, _, cx| cx.platform().open_url(&verification_uri)
+                        move |_, _, cx| {
+                            cx.platform().open_url(&verification_uri);
+                            cx.dispatch_action(ClickedConnect)
+                        }
                     },
                 )
                 .boxed(),
@@ -350,9 +374,20 @@ impl View for CopilotCodeVerification {
                     match &self.status {
                         Status::SigningIn {
                             prompt: Some(prompt),
-                        } => Self::render_prompting_modal(&prompt, &style.copilot, cx),
-                        Status::Unauthorized => Self::render_unauthorized_modal(&style.copilot, cx),
-                        Status::Authorized => Self::render_enabled_modal(&style.copilot, cx),
+                        } => Self::render_prompting_modal(
+                            self.connect_clicked,
+                            &prompt,
+                            &style.copilot,
+                            cx,
+                        ),
+                        Status::Unauthorized => {
+                            self.connect_clicked = false;
+                            Self::render_unauthorized_modal(&style.copilot, cx)
+                        }
+                        Status::Authorized => {
+                            self.connect_clicked = false;
+                            Self::render_enabled_modal(&style.copilot, cx)
+                        }
                         _ => Empty::new().boxed(),
                     },
                 ])

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<str>,
@@ -39,6 +48,9 @@ impl_internal_actions!(
     copilot,
     [
         DeployCopilotMenu,
+        DeployCopilotStartMenu,
+        HideCopilot,
+        InitiateSignIn,
         DeployCopilotModal,
         ToggleCopilotForLanguage,
         ToggleCopilotGlobally,
@@ -47,17 +59,19 @@ impl_internal_actions!(
 
 pub fn init(cx: &mut AppContext) {
     cx.add_action(CopilotButton::deploy_copilot_menu);
+    cx.add_action(CopilotButton::deploy_copilot_start_menu);
     cx.add_action(
         |_: &mut CopilotButton, action: &ToggleCopilotForLanguage, cx| {
-            let language = action.language.to_owned();
-
-            let current_langauge = cx.global::<Settings>().copilot_on(Some(&language));
+            let language = action.language.clone();
+            let show_copilot_suggestions = cx
+                .global::<Settings>()
+                .show_copilot_suggestions(Some(&language));
 
             SettingsFile::update(cx, move |file_contents| {
                 file_contents.languages.insert(
-                    language.to_owned(),
+                    language,
                     settings::EditorSettings {
-                        copilot: Some((!current_langauge).into()),
+                        show_copilot_suggestions: Some((!show_copilot_suggestions).into()),
                         ..Default::default()
                     },
                 );
@@ -66,12 +80,63 @@ pub fn init(cx: &mut AppContext) {
     );
 
     cx.add_action(|_: &mut CopilotButton, _: &ToggleCopilotGlobally, cx| {
-        let copilot_on = cx.global::<Settings>().copilot_on(None);
+        let show_copilot_suggestions = cx.global::<Settings>().show_copilot_suggestions(None);
+        SettingsFile::update(cx, move |file_contents| {
+            file_contents.editor.show_copilot_suggestions = Some((!show_copilot_suggestions).into())
+        })
+    });
 
+    cx.add_action(|_: &mut CopilotButton, _: &HideCopilot, cx| {
         SettingsFile::update(cx, move |file_contents| {
-            file_contents.editor.copilot = Some((!copilot_on).into())
+            file_contents.features.copilot = Some(false)
         })
     });
+
+    cx.add_action(|_: &mut CopilotButton, _: &InitiateSignIn, cx| {
+        let Some(copilot) = Copilot::global(cx) else {
+            return;
+        };
+        let status = copilot.read(cx).status();
+
+        match status {
+            Status::Starting { task } => {
+                cx.dispatch_action(workspace::Toast::new(
+                    COPILOT_STARTING_TOAST_ID,
+                    "Copilot is starting...",
+                ));
+                let window_id = cx.window_id();
+                let task = task.to_owned();
+                cx.spawn(|handle, mut cx| async move {
+                    task.await;
+                    cx.update(|cx| {
+                        if let Some(copilot) = Copilot::global(cx) {
+                            let status = copilot.read(cx).status();
+                            match status {
+                                Status::Authorized => cx.dispatch_action_at(
+                                    window_id,
+                                    handle.id(),
+                                    workspace::Toast::new(
+                                        COPILOT_STARTING_TOAST_ID,
+                                        "Copilot has started!",
+                                    ),
+                                ),
+                                _ => {
+                                    cx.dispatch_action_at(
+                                        window_id,
+                                        handle.id(),
+                                        DismissToast::new(COPILOT_STARTING_TOAST_ID),
+                                    );
+                                    cx.dispatch_action_at(window_id, handle.id(), SignIn)
+                                }
+                            }
+                        }
+                    })
+                })
+                .detach();
+            }
+            _ => cx.dispatch_action(SignIn),
+        }
+    })
 }
 
 pub struct CopilotButton {
@@ -93,7 +158,7 @@ impl View for CopilotButton {
     fn render(&mut self, cx: &mut ViewContext<Self>) -> Element<Self> {
         let settings = cx.global::<Settings>();
 
-        if !settings.enable_copilot_integration {
+        if !settings.features.copilot {
             return Empty::new().boxed();
         }
 
@@ -104,9 +169,9 @@ impl View for CopilotButton {
         };
         let status = copilot.read(cx).status();
 
-        let enabled = self.editor_enabled.unwrap_or(settings.copilot_on(None));
-
-        let view_id = cx.view_id();
+        let enabled = self
+            .editor_enabled
+            .unwrap_or(settings.show_copilot_suggestions(None));
 
         Stack::new()
             .with_child(
@@ -154,48 +219,13 @@ impl View for CopilotButton {
                     let status = status.clone();
                     move |_, _, cx| match status {
                         Status::Authorized => cx.dispatch_action(DeployCopilotMenu),
-                        Status::Starting { ref task } => {
-                            cx.dispatch_action(workspace::Toast::new(
-                                COPILOT_STARTING_TOAST_ID,
-                                "Copilot is starting...",
-                            ));
-                            let window_id = cx.window_id();
-                            let task = task.to_owned();
-                            cx.spawn_weak(|_this, mut cx| async move {
-                                task.await;
-                                cx.update(|cx| {
-                                    if let Some(copilot) = Copilot::global(cx) {
-                                        let status = copilot.read(cx).status();
-                                        match status {
-                                            Status::Authorized => cx.dispatch_action_at(
-                                                window_id,
-                                                view_id,
-                                                workspace::Toast::new(
-                                                    COPILOT_STARTING_TOAST_ID,
-                                                    "Copilot has started!",
-                                                ),
-                                            ),
-                                            _ => {
-                                                cx.dispatch_action_at(
-                                                    window_id,
-                                                    view_id,
-                                                    DismissToast::new(COPILOT_STARTING_TOAST_ID),
-                                                );
-                                                cx.dispatch_global_action(SignIn)
-                                            }
-                                        }
-                                    }
-                                })
-                            })
-                            .detach();
-                        }
                         Status::Error(ref e) => cx.dispatch_action(workspace::Toast::new_action(
                             COPILOT_ERROR_TOAST_ID,
                             format!("Copilot can't be started: {}", e),
                             "Reinstall Copilot",
                             Reinstall,
                         )),
-                        _ => cx.dispatch_action(SignIn),
+                        _ => cx.dispatch_action(DeployCopilotStartMenu),
                     }
                 })
                 .with_tooltip::<Self>(0, "GitHub Copilot".into(), None, theme.tooltip.clone(), cx)
@@ -235,22 +265,38 @@ impl CopilotButton {
         }
     }
 
+    pub fn deploy_copilot_start_menu(
+        &mut self,
+        _: &DeployCopilotStartMenu,
+        cx: &mut ViewContext<Self>,
+    ) {
+        let mut menu_options = Vec::with_capacity(2);
+
+        menu_options.push(ContextMenuItem::item("Sign In", InitiateSignIn));
+        menu_options.push(ContextMenuItem::item("Hide Copilot", HideCopilot));
+
+        self.popup_menu.update(cx, |menu, cx| {
+            menu.show(
+                Default::default(),
+                AnchorCorner::BottomRight,
+                menu_options,
+                cx,
+            );
+        });
+    }
+
     pub fn deploy_copilot_menu(&mut self, _: &DeployCopilotMenu, cx: &mut ViewContext<Self>) {
         let settings = cx.global::<Settings>();
 
         let mut menu_options = Vec::with_capacity(6);
 
         if let Some(language) = &self.language {
-            let language_enabled = settings.copilot_on(Some(language.as_ref()));
+            let language_enabled = settings.show_copilot_suggestions(Some(language.as_ref()));
 
             menu_options.push(ContextMenuItem::item(
                 format!(
-                    "{} Copilot for {}",
-                    if language_enabled {
-                        "Disable"
-                    } else {
-                        "Enable"
-                    },
+                    "{} Suggestions for {}",
+                    if language_enabled { "Hide" } else { "Show" },
                     language
                 ),
                 ToggleCopilotForLanguage {
@@ -259,12 +305,12 @@ impl CopilotButton {
             ));
         }
 
-        let globally_enabled = cx.global::<Settings>().copilot_on(None);
+        let globally_enabled = cx.global::<Settings>().show_copilot_suggestions(None);
         menu_options.push(ContextMenuItem::item(
             if globally_enabled {
-                "Disable Copilot Globally"
+                "Hide Suggestions for All Files"
             } else {
-                "Enable Copilot Globally"
+                "Show Suggestions for All Files"
             },
             ToggleCopilotGlobally,
         ));
@@ -312,7 +358,7 @@ impl CopilotButton {
 
         self.language = language_name.clone();
 
-        self.editor_enabled = Some(settings.copilot_on(language_name.as_deref()));
+        self.editor_enabled = Some(settings.show_copilot_suggestions(language_name.as_deref()));
 
         cx.notify()
     }

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<Cow<str>> {
+        Some("Project Diagnostics".into())
+    }
+
     fn is_dirty(&self, cx: &AppContext) -> bool {
         self.excerpts.read(cx).is_dirty(cx)
     }

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<ExcerptId>,
     pending_refresh: Task<Option<()>>,
+    pending_cycling_refresh: Task<Option<()>>,
+    cycled: bool,
     completions: Vec<copilot::Completion>,
     active_completion_index: usize,
 }
@@ -1022,14 +1027,20 @@ impl Default for CopilotState {
     fn default() -> Self {
         Self {
             excerpt_id: None,
+            pending_cycling_refresh: Task::ready(Some(())),
             pending_refresh: Task::ready(Some(())),
             completions: Default::default(),
             active_completion_index: 0,
+            cycled: false,
         }
     }
 }
 
 impl CopilotState {
+    fn active_completion(&self) -> Option<&copilot::Completion> {
+        self.completions.get(self.active_completion_index)
+    }
+
     fn text_for_active_completion(
         &self,
         cursor: Anchor,
@@ -1037,7 +1048,7 @@ impl CopilotState {
     ) -> Option<&str> {
         use language::ToOffset as _;
 
-        let completion = self.completions.get(self.active_completion_index)?;
+        let completion = self.active_completion()?;
         let excerpt_id = self.excerpt_id?;
         let completion_buffer = buffer.buffer_for_excerpt(excerpt_id)?;
         if excerpt_id != cursor.excerpt_id
@@ -1068,9 +1079,29 @@ impl CopilotState {
         }
     }
 
+    fn cycle_completions(&mut self, direction: Direction) {
+        match direction {
+            Direction::Prev => {
+                self.active_completion_index = if self.active_completion_index == 0 {
+                    self.completions.len().saturating_sub(1)
+                } else {
+                    self.active_completion_index - 1
+                };
+            }
+            Direction::Next => {
+                if self.completions.len() == 0 {
+                    self.active_completion_index = 0
+                } else {
+                    self.active_completion_index =
+                        (self.active_completion_index + 1) % self.completions.len();
+                }
+            }
+        }
+    }
+
     fn push_completion(&mut self, new_completion: copilot::Completion) {
         for completion in &self.completions {
-            if *completion == new_completion {
+            if completion.text == new_completion.text && completion.range == new_completion.range {
                 return;
             }
         }
@@ -1265,7 +1296,7 @@ impl Editor {
                 cx.subscribe(&buffer, Self::on_buffer_event),
                 cx.observe(&display_map, Self::on_display_map_changed),
                 cx.observe(&blink_manager, |_, _, cx| cx.notify()),
-                cx.observe_global::<Settings, _>(Self::on_settings_changed),
+                cx.observe_global::<Settings, _>(Self::settings_changed),
             ],
         };
         this.end_selection(cx);
@@ -1469,7 +1500,7 @@ impl Editor {
             self.refresh_code_actions(cx);
             self.refresh_document_highlights(cx);
             refresh_matching_bracket_highlights(self, cx);
-            self.hide_copilot_suggestion(cx);
+            self.discard_copilot_suggestion(cx);
         }
 
         self.blink_manager.update(cx, BlinkManager::pause_blinking);
@@ -1843,7 +1874,7 @@ impl Editor {
             return;
         }
 
-        if self.hide_copilot_suggestion(cx).is_some() {
+        if self.discard_copilot_suggestion(cx) {
             return;
         }
 
@@ -2026,13 +2057,13 @@ impl Editor {
             this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(new_selections));
 
             if had_active_copilot_suggestion {
-                this.refresh_copilot_suggestions(cx);
+                this.refresh_copilot_suggestions(true, cx);
                 if !this.has_active_copilot_suggestion(cx) {
                     this.trigger_completion_on_input(&text, cx);
                 }
             } else {
                 this.trigger_completion_on_input(&text, cx);
-                this.refresh_copilot_suggestions(cx);
+                this.refresh_copilot_suggestions(true, cx);
             }
         });
     }
@@ -2114,7 +2145,66 @@ impl Editor {
                 .collect();
 
             this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(new_selections));
-            this.refresh_copilot_suggestions(cx);
+            this.refresh_copilot_suggestions(true, cx);
+        });
+    }
+
+    pub fn newline_above(&mut self, _: &NewlineAbove, cx: &mut ViewContext<Self>) {
+        let buffer = self.buffer.read(cx);
+        let snapshot = buffer.snapshot(cx);
+
+        let mut edits = Vec::new();
+        let mut rows = Vec::new();
+        let mut rows_inserted = 0;
+
+        for selection in self.selections.all_adjusted(cx) {
+            let cursor = selection.head();
+            let row = cursor.row;
+
+            let start_of_line = snapshot.clip_point(Point::new(row, 0), Bias::Left);
+
+            let newline = "\n".to_string();
+            edits.push((start_of_line..start_of_line, newline));
+
+            rows.push(row + rows_inserted);
+            rows_inserted += 1;
+        }
+
+        self.transact(cx, |editor, cx| {
+            editor.edit(edits, cx);
+
+            editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
+                let mut index = 0;
+                s.move_cursors_with(|map, _, _| {
+                    let row = rows[index];
+                    index += 1;
+
+                    let point = Point::new(row, 0);
+                    let boundary = map.next_line_boundary(point).1;
+                    let clipped = map.clip_point(boundary, Bias::Left);
+
+                    (clipped, SelectionGoal::None)
+                });
+            });
+
+            let mut indent_edits = Vec::new();
+            let multibuffer_snapshot = editor.buffer.read(cx).snapshot(cx);
+            for row in rows {
+                let indents = multibuffer_snapshot.suggested_indents(row..row + 1, cx);
+                for (row, indent) in indents {
+                    if indent.len == 0 {
+                        continue;
+                    }
+
+                    let text = match indent.kind {
+                        IndentKind::Space => " ".repeat(indent.len as usize),
+                        IndentKind::Tab => "\t".repeat(indent.len as usize),
+                    };
+                    let point = Point::new(row, 0);
+                    indent_edits.push((point..point, text));
+                }
+            }
+            editor.edit(indent_edits, cx);
         });
     }
 
@@ -2130,19 +2220,18 @@ impl Editor {
             let cursor = selection.head();
             let row = cursor.row;
 
-            let end_of_line = snapshot
-                .clip_point(Point::new(row, snapshot.line_len(row)), Bias::Left)
-                .to_point(&snapshot);
+            let point = Point::new(row + 1, 0);
+            let start_of_line = snapshot.clip_point(point, Bias::Left);
 
             let newline = "\n".to_string();
-            edits.push((end_of_line..end_of_line, newline));
+            edits.push((start_of_line..start_of_line, newline));
 
             rows_inserted += 1;
             rows.push(row + rows_inserted);
         }
 
         self.transact(cx, |editor, cx| {
-            editor.edit_with_autoindent(edits, cx);
+            editor.edit(edits, cx);
 
             editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
                 let mut index = 0;
@@ -2157,6 +2246,25 @@ impl Editor {
                     (clipped, SelectionGoal::None)
                 });
             });
+
+            let mut indent_edits = Vec::new();
+            let multibuffer_snapshot = editor.buffer.read(cx).snapshot(cx);
+            for row in rows {
+                let indents = multibuffer_snapshot.suggested_indents(row..row + 1, cx);
+                for (row, indent) in indents {
+                    if indent.len == 0 {
+                        continue;
+                    }
+
+                    let text = match indent.kind {
+                        IndentKind::Space => " ".repeat(indent.len as usize),
+                        IndentKind::Tab => "\t".repeat(indent.len as usize),
+                    };
+                    let point = Point::new(row, 0);
+                    indent_edits.push((point..point, text));
+                }
+            }
+            editor.edit(indent_edits, cx);
         });
     }
 
@@ -2512,7 +2620,7 @@ impl Editor {
                 });
             }
 
-            this.refresh_copilot_suggestions(cx);
+            this.refresh_copilot_suggestions(true, cx);
         });
 
         let project = self.project.clone()?;
@@ -2809,10 +2917,14 @@ impl Editor {
         None
     }
 
-    fn refresh_copilot_suggestions(&mut self, cx: &mut ViewContext<Self>) -> Option<()> {
+    fn refresh_copilot_suggestions(
+        &mut self,
+        debounce: bool,
+        cx: &mut ViewContext<Self>,
+    ) -> Option<()> {
         let copilot = Copilot::global(cx)?;
         if self.mode != EditorMode::Full || !copilot.read(cx).status().is_authorized() {
-            self.hide_copilot_suggestion(cx);
+            self.clear_copilot_suggestions(cx);
             return None;
         }
         self.update_visible_copilot_suggestion(cx);
@@ -2820,29 +2932,36 @@ impl Editor {
         let snapshot = self.buffer.read(cx).snapshot(cx);
         let cursor = self.selections.newest_anchor().head();
         let language_name = snapshot.language_at(cursor).map(|language| language.name());
-        if !cx.global::<Settings>().copilot_on(language_name.as_deref()) {
-            self.hide_copilot_suggestion(cx);
+        if !cx
+            .global::<Settings>()
+            .show_copilot_suggestions(language_name.as_deref())
+        {
+            self.clear_copilot_suggestions(cx);
             return None;
         }
 
         let (buffer, buffer_position) =
             self.buffer.read(cx).text_anchor_for_position(cursor, cx)?;
         self.copilot_state.pending_refresh = cx.spawn_weak(|this, mut cx| async move {
-            cx.background().timer(COPILOT_DEBOUNCE_TIMEOUT).await;
-            let (completion, completions_cycling) = copilot.update(&mut cx, |copilot, cx| {
-                (
-                    copilot.completions(&buffer, buffer_position, cx),
-                    copilot.completions_cycling(&buffer, buffer_position, cx),
-                )
-            });
+            if debounce {
+                cx.background().timer(COPILOT_DEBOUNCE_TIMEOUT).await;
+            }
+
+            let completions = copilot
+                .update(&mut cx, |copilot, cx| {
+                    copilot.completions(&buffer, buffer_position, cx)
+                })
+                .await
+                .log_err()
+                .into_iter()
+                .flatten()
+                .collect_vec();
 
-            let (completion, completions_cycling) = futures::join!(completion, completions_cycling);
-            let mut completions = Vec::new();
-            completions.extend(completion.log_err().into_iter().flatten());
-            completions.extend(completions_cycling.log_err().into_iter().flatten());
             this.upgrade(&cx)?
                 .update(&mut cx, |this, cx| {
                     if !completions.is_empty() {
+                        this.copilot_state.cycled = false;
+                        this.copilot_state.pending_cycling_refresh = Task::ready(None);
                         this.copilot_state.completions.clear();
                         this.copilot_state.active_completion_index = 0;
                         this.copilot_state.excerpt_id = Some(cursor.excerpt_id);
@@ -2853,68 +2972,126 @@ impl Editor {
                     }
                 })
                 .log_err()?;
-
             Some(())
         });
 
         Some(())
     }
 
-    fn next_copilot_suggestion(&mut self, _: &copilot::NextSuggestion, cx: &mut ViewContext<Self>) {
+    fn cycle_copilot_suggestions(
+        &mut self,
+        direction: Direction,
+        cx: &mut ViewContext<Self>,
+    ) -> Option<()> {
+        let copilot = Copilot::global(cx)?;
+        if self.mode != EditorMode::Full || !copilot.read(cx).status().is_authorized() {
+            return None;
+        }
+
+        if self.copilot_state.cycled {
+            self.copilot_state.cycle_completions(direction);
+            self.update_visible_copilot_suggestion(cx);
+        } else {
+            let cursor = self.selections.newest_anchor().head();
+            let (buffer, buffer_position) =
+                self.buffer.read(cx).text_anchor_for_position(cursor, cx)?;
+            self.copilot_state.pending_cycling_refresh = cx.spawn_weak(|this, mut cx| async move {
+                let completions = copilot
+                    .update(&mut cx, |copilot, cx| {
+                        copilot.completions_cycling(&buffer, buffer_position, cx)
+                    })
+                    .await;
+
+                this.upgrade(&cx)?
+                    .update(&mut cx, |this, cx| {
+                        this.copilot_state.cycled = true;
+                        for completion in completions.log_err().into_iter().flatten() {
+                            this.copilot_state.push_completion(completion);
+                        }
+                        this.copilot_state.cycle_completions(direction);
+                        this.update_visible_copilot_suggestion(cx);
+                    })
+                    .log_err()?;
+
+                Some(())
+            });
+        }
+
+        Some(())
+    }
+
+    fn copilot_suggest(&mut self, _: &copilot::Suggest, cx: &mut ViewContext<Self>) {
         if !self.has_active_copilot_suggestion(cx) {
-            self.refresh_copilot_suggestions(cx);
+            self.refresh_copilot_suggestions(false, cx);
             return;
         }
 
-        self.copilot_state.active_completion_index =
-            (self.copilot_state.active_completion_index + 1) % self.copilot_state.completions.len();
         self.update_visible_copilot_suggestion(cx);
     }
 
+    fn next_copilot_suggestion(&mut self, _: &copilot::NextSuggestion, cx: &mut ViewContext<Self>) {
+        if self.has_active_copilot_suggestion(cx) {
+            self.cycle_copilot_suggestions(Direction::Next, cx);
+        } else {
+            self.refresh_copilot_suggestions(false, cx);
+        }
+    }
+
     fn previous_copilot_suggestion(
         &mut self,
         _: &copilot::PreviousSuggestion,
         cx: &mut ViewContext<Self>,
     ) {
-        if !self.has_active_copilot_suggestion(cx) {
-            self.refresh_copilot_suggestions(cx);
-            return;
+        if self.has_active_copilot_suggestion(cx) {
+            self.cycle_copilot_suggestions(Direction::Prev, cx);
+        } else {
+            self.refresh_copilot_suggestions(false, cx);
         }
-
-        self.copilot_state.active_completion_index =
-            if self.copilot_state.active_completion_index == 0 {
-                self.copilot_state.completions.len() - 1
-            } else {
-                self.copilot_state.active_completion_index - 1
-            };
-        self.update_visible_copilot_suggestion(cx);
     }
 
     fn accept_copilot_suggestion(&mut self, cx: &mut ViewContext<Self>) -> bool {
-        if let Some(text) = self.hide_copilot_suggestion(cx) {
-            self.insert_with_autoindent_mode(&text.to_string(), None, cx);
+        if let Some(suggestion) = self
+            .display_map
+            .update(cx, |map, cx| map.replace_suggestion::<usize>(None, cx))
+        {
+            if let Some((copilot, completion)) =
+                Copilot::global(cx).zip(self.copilot_state.active_completion())
+            {
+                copilot
+                    .update(cx, |copilot, cx| copilot.accept_completion(completion, cx))
+                    .detach_and_log_err(cx);
+            }
+            self.insert_with_autoindent_mode(&suggestion.text.to_string(), None, cx);
+            cx.notify();
             true
         } else {
             false
         }
     }
 
-    fn has_active_copilot_suggestion(&self, cx: &AppContext) -> bool {
-        self.display_map.read(cx).has_suggestion()
-    }
-
-    fn hide_copilot_suggestion(&mut self, cx: &mut ViewContext<Self>) -> Option<Rope> {
+    fn discard_copilot_suggestion(&mut self, cx: &mut ViewContext<Self>) -> bool {
         if self.has_active_copilot_suggestion(cx) {
-            let old_suggestion = self
-                .display_map
+            if let Some(copilot) = Copilot::global(cx) {
+                copilot
+                    .update(cx, |copilot, cx| {
+                        copilot.discard_completions(&self.copilot_state.completions, cx)
+                    })
+                    .detach_and_log_err(cx);
+            }
+
+            self.display_map
                 .update(cx, |map, cx| map.replace_suggestion::<usize>(None, cx));
             cx.notify();
-            old_suggestion.map(|suggestion| suggestion.text)
+            true
         } else {
-            None
+            false
         }
     }
 
+    fn has_active_copilot_suggestion(&self, cx: &AppContext) -> bool {
+        self.display_map.read(cx).has_suggestion()
+    }
+
     fn update_visible_copilot_suggestion(&mut self, cx: &mut ViewContext<Self>) {
         let snapshot = self.buffer.read(cx).snapshot(cx);
         let selection = self.selections.newest_anchor();
@@ -2924,26 +3101,31 @@ impl Editor {
             || !self.completion_tasks.is_empty()
             || selection.start != selection.end
         {
-            self.hide_copilot_suggestion(cx);
+            self.discard_copilot_suggestion(cx);
         } else if let Some(text) = self
             .copilot_state
             .text_for_active_completion(cursor, &snapshot)
         {
-            self.display_map.update(cx, |map, cx| {
+            self.display_map.update(cx, move |map, cx| {
                 map.replace_suggestion(
                     Some(Suggestion {
                         position: cursor,
-                        text: text.into(),
+                        text: text.trim_end().into(),
                     }),
                     cx,
                 )
             });
             cx.notify();
         } else {
-            self.hide_copilot_suggestion(cx);
+            self.discard_copilot_suggestion(cx);
         }
     }
 
+    fn clear_copilot_suggestions(&mut self, cx: &mut ViewContext<Self>) {
+        self.copilot_state = Default::default();
+        self.discard_copilot_suggestion(cx);
+    }
+
     pub fn render_code_actions_indicator(
         &self,
         style: &EditorStyle,
@@ -3059,7 +3241,7 @@ impl Editor {
             self.completion_tasks.clear();
         }
         self.context_menu = Some(menu);
-        self.hide_copilot_suggestion(cx);
+        self.discard_copilot_suggestion(cx);
         cx.notify();
     }
 
@@ -3229,7 +3411,7 @@ impl Editor {
 
             this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(selections));
             this.insert("", cx);
-            this.refresh_copilot_suggestions(cx);
+            this.refresh_copilot_suggestions(true, cx);
         });
     }
 
@@ -3245,7 +3427,7 @@ impl Editor {
                 })
             });
             this.insert("", cx);
-            this.refresh_copilot_suggestions(cx);
+            this.refresh_copilot_suggestions(true, cx);
         });
     }
 
@@ -3341,7 +3523,7 @@ impl Editor {
         self.transact(cx, |this, cx| {
             this.buffer.update(cx, |b, cx| b.edit(edits, None, cx));
             this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(selections));
-            this.refresh_copilot_suggestions(cx);
+            this.refresh_copilot_suggestions(true, cx);
         });
     }
 
@@ -4021,7 +4203,7 @@ impl Editor {
             }
             self.request_autoscroll(Autoscroll::fit(), cx);
             self.unmark_text(cx);
-            self.refresh_copilot_suggestions(cx);
+            self.refresh_copilot_suggestions(true, cx);
             cx.emit(Event::Edited);
         }
     }
@@ -4036,7 +4218,7 @@ impl Editor {
             }
             self.request_autoscroll(Autoscroll::fit(), cx);
             self.unmark_text(cx);
-            self.refresh_copilot_suggestions(cx);
+            self.refresh_copilot_suggestions(true, cx);
             cx.emit(Event::Edited);
         }
     }
@@ -6490,6 +6672,7 @@ impl Editor {
             multi_buffer::Event::DiagnosticsUpdated => {
                 self.refresh_active_diagnostics(cx);
             }
+            multi_buffer::Event::LanguageChanged => {}
         }
     }
 
@@ -6497,8 +6680,8 @@ impl Editor {
         cx.notify();
     }
 
-    fn on_settings_changed(&mut self, cx: &mut ViewContext<Self>) {
-        self.refresh_copilot_suggestions(cx);
+    fn settings_changed(&mut self, cx: &mut ViewContext<Self>) {
+        self.refresh_copilot_suggestions(true, cx);
     }
 
     pub fn set_searchable(&mut self, searchable: bool) {

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, _| {
+            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);

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::<Self>();
                 editors.find(|editor| {
-                    editor.remote_id(&client, cx) == Some(remote_id)
-                        || state.singleton
-                            && buffers.len() == 1
-                            && editor.read(cx).buffer.read(cx).as_singleton().as_ref()
-                                == Some(&buffers[0])
+                    let ids_match = editor.remote_id(&client, cx) == Some(remote_id);
+                    let singleton_buffer_matches = state.singleton
+                        && buffers.first()
+                            == editor.read(cx).buffer.read(cx).as_singleton().as_ref();
+                    ids_match || singleton_buffer_matches
                 })
             });
 
@@ -117,46 +117,29 @@ impl FollowableItem for Editor {
                         multibuffer
                     });
 
-                    cx.add_view(|cx| Editor::for_multibuffer(multibuffer, Some(project), cx))
+                    cx.add_view(|cx| {
+                        let mut editor =
+                            Editor::for_multibuffer(multibuffer, Some(project.clone()), cx);
+                        editor.remote_id = Some(remote_id);
+                        editor
+                    })
                 })?
             };
 
-            editor.update(&mut cx, |editor, cx| {
-                editor.remote_id = Some(remote_id);
-                let buffer = editor.buffer.read(cx).read(cx);
-                let selections = state
-                    .selections
-                    .into_iter()
-                    .map(|selection| {
-                        deserialize_selection(&buffer, selection)
-                            .ok_or_else(|| anyhow!("invalid selection"))
-                    })
-                    .collect::<Result<Vec<_>>>()?;
-                let pending_selection = state
-                    .pending_selection
-                    .map(|selection| deserialize_selection(&buffer, selection))
-                    .flatten();
-                let scroll_top_anchor = state
-                    .scroll_top_anchor
-                    .and_then(|anchor| deserialize_anchor(&buffer, anchor));
-                drop(buffer);
-
-                if !selections.is_empty() || pending_selection.is_some() {
-                    editor.set_selections_from_remote(selections, pending_selection, cx);
-                }
-
-                if let Some(scroll_top_anchor) = scroll_top_anchor {
-                    editor.set_scroll_anchor_remote(
-                        ScrollAnchor {
-                            top_anchor: scroll_top_anchor,
-                            offset: vec2f(state.scroll_x, state.scroll_y),
-                        },
-                        cx,
-                    );
-                }
-
-                anyhow::Ok(())
-            })??;
+            update_editor_from_message(
+                editor.clone(),
+                project,
+                proto::update_view::Editor {
+                    selections: state.selections,
+                    pending_selection: state.pending_selection,
+                    scroll_top_anchor: state.scroll_top_anchor,
+                    scroll_x: state.scroll_x,
+                    scroll_y: state.scroll_y,
+                    ..Default::default()
+                },
+                &mut cx,
+            )
+            .await?;
 
             Ok(editor)
         }))
@@ -301,107 +284,142 @@ impl FollowableItem for Editor {
         cx: &mut ViewContext<Self>,
     ) -> Task<Result<()>> {
         let update_view::Variant::Editor(message) = message;
-        let multibuffer = self.buffer.read(cx);
-        let multibuffer = multibuffer.read(cx);
+        let project = project.clone();
+        cx.spawn(|this, mut cx| async move {
+            update_editor_from_message(this, project, message, &mut cx).await
+        })
+    }
 
-        let buffer_ids = message
-            .inserted_excerpts
-            .iter()
-            .filter_map(|insertion| Some(insertion.excerpt.as_ref()?.buffer_id))
-            .collect::<HashSet<_>>();
+    fn should_unfollow_on_event(event: &Self::Event, _: &AppContext) -> bool {
+        match event {
+            Event::Edited => true,
+            Event::SelectionsChanged { local } => *local,
+            Event::ScrollPositionChanged { local } => *local,
+            _ => false,
+        }
+    }
+}
 
-        let mut removals = message
-            .deleted_excerpts
+async fn update_editor_from_message(
+    this: ViewHandle<Editor>,
+    project: ModelHandle<Project>,
+    message: proto::update_view::Editor,
+    cx: &mut AsyncAppContext,
+) -> Result<()> {
+    // Open all of the buffers of which excerpts were added to the editor.
+    let inserted_excerpt_buffer_ids = message
+        .inserted_excerpts
+        .iter()
+        .filter_map(|insertion| Some(insertion.excerpt.as_ref()?.buffer_id))
+        .collect::<HashSet<_>>();
+    let inserted_excerpt_buffers = project.update(cx, |project, cx| {
+        inserted_excerpt_buffer_ids
             .into_iter()
-            .map(ExcerptId::from_proto)
-            .collect::<Vec<_>>();
-        removals.sort_by(|a, b| a.cmp(&b, &multibuffer));
+            .map(|id| project.open_buffer_by_id(id, cx))
+            .collect::<Vec<_>>()
+    });
+    let _inserted_excerpt_buffers = try_join_all(inserted_excerpt_buffers).await?;
+
+    // Update the editor's excerpts.
+    this.update(cx, |editor, cx| {
+        editor.buffer.update(cx, |multibuffer, cx| {
+            let mut removed_excerpt_ids = message
+                .deleted_excerpts
+                .into_iter()
+                .map(ExcerptId::from_proto)
+                .collect::<Vec<_>>();
+            removed_excerpt_ids.sort_by({
+                let multibuffer = multibuffer.read(cx);
+                move |a, b| a.cmp(&b, &multibuffer)
+            });
+
+            let mut insertions = message.inserted_excerpts.into_iter().peekable();
+            while let Some(insertion) = insertions.next() {
+                let Some(excerpt) = insertion.excerpt else { continue };
+                let Some(previous_excerpt_id) = insertion.previous_excerpt_id else { continue };
+                let buffer_id = excerpt.buffer_id;
+                let Some(buffer) = project.read(cx).buffer_for_id(buffer_id, cx) else { continue };
+
+                let adjacent_excerpts = iter::from_fn(|| {
+                    let insertion = insertions.peek()?;
+                    if insertion.previous_excerpt_id.is_none()
+                        && insertion.excerpt.as_ref()?.buffer_id == buffer_id
+                    {
+                        insertions.next()?.excerpt
+                    } else {
+                        None
+                    }
+                });
 
+                multibuffer.insert_excerpts_with_ids_after(
+                    ExcerptId::from_proto(previous_excerpt_id),
+                    buffer,
+                    [excerpt]
+                        .into_iter()
+                        .chain(adjacent_excerpts)
+                        .filter_map(|excerpt| {
+                            Some((
+                                ExcerptId::from_proto(excerpt.id),
+                                deserialize_excerpt_range(excerpt)?,
+                            ))
+                        }),
+                    cx,
+                );
+            }
+
+            multibuffer.remove_excerpts(removed_excerpt_ids, cx);
+        });
+    })?;
+
+    // Deserialize the editor state.
+    let (selections, pending_selection, scroll_top_anchor) = this.update(cx, |editor, cx| {
+        let buffer = editor.buffer.read(cx).read(cx);
         let selections = message
             .selections
             .into_iter()
-            .filter_map(|selection| deserialize_selection(&multibuffer, selection))
+            .filter_map(|selection| deserialize_selection(&buffer, selection))
             .collect::<Vec<_>>();
         let pending_selection = message
             .pending_selection
-            .and_then(|selection| deserialize_selection(&multibuffer, selection));
-
+            .and_then(|selection| deserialize_selection(&buffer, selection));
         let scroll_top_anchor = message
             .scroll_top_anchor
-            .and_then(|anchor| deserialize_anchor(&multibuffer, anchor));
-        drop(multibuffer);
-
-        let buffers = project.update(cx, |project, cx| {
-            buffer_ids
-                .into_iter()
-                .map(|id| project.open_buffer_by_id(id, cx))
-                .collect::<Vec<_>>()
-        });
-
-        let project = project.clone();
-        cx.spawn(|this, mut cx| async move {
-            let _buffers = try_join_all(buffers).await?;
-            this.update(&mut cx, |this, cx| {
-                this.buffer.update(cx, |multibuffer, cx| {
-                    let mut insertions = message.inserted_excerpts.into_iter().peekable();
-                    while let Some(insertion) = insertions.next() {
-                        let Some(excerpt) = insertion.excerpt else { continue };
-                        let Some(previous_excerpt_id) = insertion.previous_excerpt_id else { continue };
-                        let buffer_id = excerpt.buffer_id;
-                        let Some(buffer) = project.read(cx).buffer_for_id(buffer_id, cx) else { continue };
-
-                        let adjacent_excerpts = iter::from_fn(|| {
-                            let insertion = insertions.peek()?;
-                            if insertion.previous_excerpt_id.is_none()
-                                && insertion.excerpt.as_ref()?.buffer_id == buffer_id
-                            {
-                                insertions.next()?.excerpt
-                            } else {
-                                None
-                            }
-                        });
-
-                        multibuffer.insert_excerpts_with_ids_after(
-                            ExcerptId::from_proto(previous_excerpt_id),
-                            buffer,
-                            [excerpt]
-                                .into_iter()
-                                .chain(adjacent_excerpts)
-                                .filter_map(|excerpt| {
-                                    Some((
-                                        ExcerptId::from_proto(excerpt.id),
-                                        deserialize_excerpt_range(excerpt)?,
-                                    ))
-                                }),
-                            cx,
-                        );
-                    }
-
-                    multibuffer.remove_excerpts(removals, cx);
-                });
-
-                if !selections.is_empty() || pending_selection.is_some() {
-                    this.set_selections_from_remote(selections, pending_selection, cx);
-                    this.request_autoscroll_remotely(Autoscroll::newest(), cx);
-                } else if let Some(anchor) = scroll_top_anchor {
-                    this.set_scroll_anchor_remote(ScrollAnchor {
-                        top_anchor: anchor,
-                        offset: vec2f(message.scroll_x, message.scroll_y)
-                    }, cx);
-                }
-            })?;
-            Ok(())
+            .and_then(|anchor| deserialize_anchor(&buffer, anchor));
+        anyhow::Ok((selections, pending_selection, scroll_top_anchor))
+    })??;
+
+    // Wait until the buffer has received all of the operations referenced by
+    // the editor's new state.
+    this.update(cx, |editor, cx| {
+        editor.buffer.update(cx, |buffer, cx| {
+            buffer.wait_for_anchors(
+                selections
+                    .iter()
+                    .chain(pending_selection.as_ref())
+                    .flat_map(|selection| [selection.start, selection.end])
+                    .chain(scroll_top_anchor),
+                cx,
+            )
         })
-    }
-
-    fn should_unfollow_on_event(event: &Self::Event, _: &AppContext) -> bool {
-        match event {
-            Event::Edited => true,
-            Event::SelectionsChanged { local } => *local,
-            Event::ScrollPositionChanged { local } => *local,
-            _ => false,
+    })?
+    .await?;
+
+    // Update the editor's state.
+    this.update(cx, |editor, cx| {
+        if !selections.is_empty() || pending_selection.is_some() {
+            editor.set_selections_from_remote(selections, pending_selection, cx);
+            editor.request_autoscroll_remotely(Autoscroll::newest(), cx);
+        } else if let Some(scroll_top_anchor) = scroll_top_anchor {
+            editor.set_scroll_anchor_remote(
+                ScrollAnchor {
+                    top_anchor: scroll_top_anchor,
+                    offset: vec2f(message.scroll_x, message.scroll_y),
+                },
+                cx,
+            );
         }
-    }
+    })?;
+    Ok(())
 }
 
 fn serialize_excerpt(
@@ -516,7 +534,24 @@ impl Item for Editor {
         }
     }
 
-    fn tab_description<'a>(&'a self, detail: usize, cx: &'a AppContext) -> Option<Cow<'a, str>> {
+    fn tab_tooltip_text(&self, cx: &AppContext) -> Option<Cow<str>> {
+        let file_path = self
+            .buffer()
+            .read(cx)
+            .as_singleton()?
+            .read(cx)
+            .file()
+            .and_then(|f| f.as_local())?
+            .abs_path(cx);
+
+        let file_path = util::paths::compact(&file_path)
+            .to_string_lossy()
+            .to_string();
+
+        Some(file_path.into())
+    }
+
+    fn tab_description<'a>(&'a self, detail: usize, cx: &'a AppContext) -> Option<Cow<str>> {
         match path_for_buffer(&self.buffer, detail, true, cx)? {
             Cow::Borrowed(path) => Some(path.to_string_lossy()),
             Cow::Owned(path) => Some(path.to_string_lossy().to_string().into()),

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<Item = Anchor>,
+        cx: &mut ModelContext<Self>,
+    ) -> impl 'static + Future<Output = Result<()>> {
+        let borrow = self.buffers.borrow();
+        let mut error = None;
+        let mut futures = Vec::new();
+        for anchor in anchors {
+            if let Some(buffer_id) = anchor.buffer_id {
+                if let Some(buffer) = borrow.get(&buffer_id) {
+                    buffer.buffer.update(cx, |buffer, _| {
+                        futures.push(buffer.wait_for_anchors([anchor.text_anchor]))
+                    });
+                } else {
+                    error = Some(anyhow!(
+                        "buffer {buffer_id} is not part of this multi-buffer"
+                    ));
+                    break;
+                }
+            }
+        }
+        async move {
+            if let Some(error) = error {
+                Err(error)?;
+            }
+            for future in futures {
+                future.await?;
+            }
+            Ok(())
+        }
+    }
+
     pub fn text_anchor_for_position<T: ToOffset>(
         &self,
         position: T,
@@ -1266,6 +1303,7 @@ impl MultiBuffer {
             language::Event::Saved => Event::Saved,
             language::Event::FileHandleChanged => Event::FileHandleChanged,
             language::Event::Reloaded => Event::Reloaded,
+            language::Event::LanguageChanged => Event::LanguageChanged,
             language::Event::Reparsed => Event::Reparsed,
             language::Event::DiagnosticsUpdated => Event::DiagnosticsUpdated,
             language::Event::Closed => Event::Closed,

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())
     }
 

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<Cow<str>> {
+        Some("Send Feedback".into())
+    }
+
     fn tab_content(&self, _: Option<usize>, style: &theme::Tab, _: &AppContext) -> Element<Pane> {
         Flex::row()
             .with_child(

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<f32>,
 }
 
 #[derive(Clone, Deserialize, Default)]
@@ -135,9 +135,14 @@ impl<V: View> Tooltip<V> {
     ) -> impl Drawable<V> {
         Flex::row()
             .with_child({
-                let text = Text::new(text, style.text)
-                    .constrained()
-                    .with_max_width(style.max_text_width);
+                let text = if let Some(max_text_width) = style.max_text_width {
+                    Text::new(text, style.text)
+                        .constrained()
+                        .with_max_width(max_text_width)
+                } else {
+                    Text::new(text, style.text).constrained()
+                };
+
                 if measure {
                     text.flex(1., false).boxed()
                 } else {

crates/journal/src/journal.rs 🔗

@@ -46,7 +46,7 @@ pub fn new_journal_entry(app_state: Arc<AppState>, cx: &mut AppContext) {
     cx.spawn(|mut cx| async move {
         let (journal_dir, entry_path) = create_entry.await?;
         let (workspace, _) = cx
-            .update(|cx| workspace::open_paths(&[journal_dir], &app_state, cx))
+            .update(|cx| workspace::open_paths(&[journal_dir], &app_state, None, cx))
             .await?;
 
         let opened = workspace

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<LanguageRegistry>) {
@@ -1313,10 +1315,10 @@ impl Buffer {
         self.text.wait_for_edits(edit_ids)
     }
 
-    pub fn wait_for_anchors<'a>(
+    pub fn wait_for_anchors(
         &mut self,
-        anchors: impl IntoIterator<Item = &'a Anchor>,
-    ) -> impl Future<Output = Result<()>> {
+        anchors: impl IntoIterator<Item = Anchor>,
+    ) -> impl 'static + Future<Output = Result<()>> {
         self.text.wait_for_anchors(anchors)
     }
 

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

crates/language/src/language.rs 🔗

@@ -262,6 +262,8 @@ pub struct LanguageConfig {
     pub name: Arc<str>,
     pub path_suffixes: Vec<String>,
     pub brackets: BracketPairConfig,
+    #[serde(default, deserialize_with = "deserialize_regex")]
+    pub first_line_pattern: Option<Regex>,
     #[serde(default = "auto_indent_using_last_non_empty_line_default")]
     pub auto_indent_using_last_non_empty_line: bool,
     #[serde(default, deserialize_with = "deserialize_regex")]
@@ -334,6 +336,7 @@ impl Default for LanguageConfig {
             path_suffixes: Default::default(),
             brackets: Default::default(),
             auto_indent_using_last_non_empty_line: auto_indent_using_last_non_empty_line_default(),
+            first_line_pattern: Default::default(),
             increase_indent_pattern: Default::default(),
             decrease_indent_pattern: Default::default(),
             autoclose_before: Default::default(),
@@ -660,19 +663,30 @@ impl LanguageRegistry {
         })
     }
 
-    pub fn language_for_path(
+    pub fn language_for_file(
         self: &Arc<Self>,
         path: impl AsRef<Path>,
+        content: Option<&Rope>,
     ) -> UnwrapFuture<oneshot::Receiver<Result<Arc<Language>>>> {
         let path = path.as_ref();
         let filename = path.file_name().and_then(|name| name.to_str());
         let extension = path.extension().and_then(|name| name.to_str());
         let path_suffixes = [extension, filename];
         self.get_or_load_language(|config| {
-            config
+            let path_matches = config
                 .path_suffixes
                 .iter()
-                .any(|suffix| path_suffixes.contains(&Some(suffix.as_str())))
+                .any(|suffix| path_suffixes.contains(&Some(suffix.as_str())));
+            let content_matches = content.zip(config.first_line_pattern.as_ref()).map_or(
+                false,
+                |(content, pattern)| {
+                    let end = content.clip_point(Point::new(0, 256), Bias::Left);
+                    let end = content.point_to_offset(end);
+                    let text = content.chunks_in_range(0..end).collect::<String>();
+                    pattern.is_match(&text)
+                },
+            );
+            path_matches || content_matches
         })
     }
 
@@ -1528,9 +1542,45 @@ pub fn range_from_lsp(range: lsp::Range) -> Range<Unclipped<PointUtf16>> {
 
 #[cfg(test)]
 mod tests {
+    use super::*;
     use gpui::TestAppContext;
 
-    use super::*;
+    #[gpui::test(iterations = 10)]
+    async fn test_first_line_pattern(cx: &mut TestAppContext) {
+        let mut languages = LanguageRegistry::test();
+        languages.set_executor(cx.background());
+        let languages = Arc::new(languages);
+        languages.register(
+            "/javascript",
+            LanguageConfig {
+                name: "JavaScript".into(),
+                path_suffixes: vec!["js".into()],
+                first_line_pattern: Some(Regex::new(r"\bnode\b").unwrap()),
+                ..Default::default()
+            },
+            tree_sitter_javascript::language(),
+            None,
+            |_| Default::default(),
+        );
+
+        languages
+            .language_for_file("the/script", None)
+            .await
+            .unwrap_err();
+        languages
+            .language_for_file("the/script", Some(&"nothing".into()))
+            .await
+            .unwrap_err();
+        assert_eq!(
+            languages
+                .language_for_file("the/script", Some(&"#!/bin/env node".into()))
+                .await
+                .unwrap()
+                .name()
+                .as_ref(),
+            "JavaScript"
+        );
+    }
 
     #[gpui::test(iterations = 10)]
     async fn test_language_loading(cx: &mut TestAppContext) {

crates/picker/src/picker.rs 🔗

@@ -187,8 +187,6 @@ impl<D: PickerDelegate> Picker<D> {
             confirmed: false,
             pending_update_matches: Task::ready(None),
         };
-        // TODO! How can the delegate notify the picker to update?
-        // cx.observe(&delegate, |_, _, cx| cx.notify()).detach();
         this.update_matches(String::new(), cx);
         this
     }

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" }

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,

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<Buffer>,
+        cx: &mut ModelContext<Self>,
+    ) {
+        if let Some(copilot) = Copilot::global(cx) {
+            copilot.update(cx, |copilot, cx| copilot.register_buffer(buffer_handle, cx));
+        }
+    }
+
     async fn send_buffer_messages(
         this: WeakModelHandle<Self>,
         rx: UnboundedReceiver<BufferMessage>,
@@ -2013,17 +2037,19 @@ impl Project {
 
     fn detect_language_for_buffer(
         &mut self,
-        buffer: &ModelHandle<Buffer>,
+        buffer_handle: &ModelHandle<Buffer>,
         cx: &mut ModelContext<Self>,
     ) -> Option<()> {
         // If the buffer has a language, set it and start the language server if we haven't already.
-        let full_path = buffer.read(cx).file()?.full_path(cx);
+        let buffer = buffer_handle.read(cx);
+        let full_path = buffer.file()?.full_path(cx);
+        let content = buffer.as_rope();
         let new_language = self
             .languages
-            .language_for_path(&full_path)
+            .language_for_file(&full_path, Some(content))
             .now_or_never()?
             .ok()?;
-        self.set_language_for_buffer(buffer, new_language, cx);
+        self.set_language_for_buffer(buffer_handle, new_language, cx);
         None
     }
 
@@ -2434,26 +2460,23 @@ impl Project {
         buffers: impl IntoIterator<Item = ModelHandle<Buffer>>,
         cx: &mut ModelContext<Self>,
     ) -> Option<()> {
-        let language_server_lookup_info: HashSet<(WorktreeId, Arc<Path>, PathBuf)> = buffers
+        let language_server_lookup_info: HashSet<(WorktreeId, Arc<Path>, Arc<Language>)> = buffers
             .into_iter()
             .filter_map(|buffer| {
-                let file = File::from_dyn(buffer.read(cx).file())?;
+                let buffer = buffer.read(cx);
+                let file = File::from_dyn(buffer.file())?;
                 let worktree = file.worktree.read(cx).as_local()?;
-                let worktree_id = worktree.id();
-                let worktree_abs_path = worktree.abs_path().clone();
                 let full_path = file.full_path(cx);
-                Some((worktree_id, worktree_abs_path, full_path))
+                let language = self
+                    .languages
+                    .language_for_file(&full_path, Some(buffer.as_rope()))
+                    .now_or_never()?
+                    .ok()?;
+                Some((worktree.id(), worktree.abs_path().clone(), language))
             })
             .collect();
-        for (worktree_id, worktree_abs_path, full_path) in language_server_lookup_info {
-            if let Some(language) = self
-                .languages
-                .language_for_path(&full_path)
-                .now_or_never()
-                .and_then(|language| language.ok())
-            {
-                self.restart_language_server(worktree_id, worktree_abs_path, language, cx);
-            }
+        for (worktree_id, worktree_abs_path, language) in language_server_lookup_info {
+            self.restart_language_server(worktree_id, worktree_abs_path, language, cx);
         }
 
         None
@@ -3487,7 +3510,7 @@ impl Project {
                             let adapter_language = adapter_language.clone();
                             let language = this
                                 .languages
-                                .language_for_path(&project_path.path)
+                                .language_for_file(&project_path.path, None)
                                 .unwrap_or_else(move |_| adapter_language);
                             let language_server_name = adapter.name.clone();
                             Some(async move {
@@ -5916,7 +5939,10 @@ impl Project {
                 worktree_id,
                 path: PathBuf::from(serialized_symbol.path).into(),
             };
-            let language = languages.language_for_path(&path.path).await.log_err();
+            let language = languages
+                .language_for_file(&path.path, None)
+                .await
+                .log_err();
             Ok(Symbol {
                 language_server_name: LanguageServerName(
                     serialized_symbol.language_server_name.into(),

crates/recent_projects/src/recent_projects.rs 🔗

@@ -141,7 +141,7 @@ impl PickerDelegate for RecentProjectsDelegate {
     fn confirm(&mut self, cx: &mut ViewContext<RecentProjects>) {
         if let Some(selected_match) = &self.matches.get(self.selected_index()) {
             let workspace_location = &self.workspace_locations[selected_match.candidate_id];
-            cx.dispatch_global_action(OpenPaths {
+            cx.dispatch_action(OpenPaths {
                 paths: workspace_location.paths().as_ref().clone(),
             });
             cx.emit(PickerEvent::Dismiss);

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<Cow<str>> {
+        Some(self.query_editor.read(cx).text(cx).into())
+    }
+
     fn act_as_type<'a>(
         &'a self,
         type_id: TypeId,

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<bool>,
     pub formatter: Option<Formatter>,
     pub enable_language_server: Option<bool>,
-    #[schemars(skip)]
-    pub copilot: Option<OnOff>,
-}
-
-#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
-#[serde(rename_all = "snake_case")]
-pub enum OnOff {
-    On,
-    Off,
-}
-
-impl OnOff {
-    pub fn as_bool(&self) -> bool {
-        match self {
-            OnOff::On => true,
-            OnOff::Off => false,
-        }
-    }
-
-    pub fn from_bool(value: bool) -> OnOff {
-        match value {
-            true => OnOff::On,
-            false => OnOff::Off,
-        }
-    }
-}
-
-impl From<OnOff> for bool {
-    fn from(value: OnOff) -> bool {
-        value.as_bool()
-    }
-}
-
-impl From<bool> for OnOff {
-    fn from(value: bool) -> OnOff {
-        OnOff::from_bool(value)
-    }
+    pub show_copilot_suggestions: Option<bool>,
 }
 
 #[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
@@ -437,8 +401,7 @@ pub struct SettingsFileContent {
     #[serde(default)]
     pub base_keymap: Option<BaseKeymap>,
     #[serde(default)]
-    #[schemars(skip)]
-    pub enable_copilot_integration: Option<bool>,
+    pub features: FeaturesContent,
 }
 
 #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
@@ -447,6 +410,18 @@ pub struct LspSettings {
     pub initialization_options: Option<Value>,
 }
 
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub struct Features {
+    pub copilot: bool,
+}
+
+#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub struct FeaturesContent {
+    pub copilot: Option<bool>,
+}
+
 impl Settings {
     /// Fill out the settings corresponding to the default.json file, overrides will be set later
     pub fn defaults(
@@ -500,7 +475,7 @@ impl Settings {
                 format_on_save: required(defaults.editor.format_on_save),
                 formatter: required(defaults.editor.formatter),
                 enable_language_server: required(defaults.editor.enable_language_server),
-                copilot: required(defaults.editor.copilot),
+                show_copilot_suggestions: required(defaults.editor.show_copilot_suggestions),
             },
             editor_overrides: Default::default(),
             git: defaults.git.unwrap(),
@@ -517,7 +492,9 @@ impl Settings {
             telemetry_overrides: Default::default(),
             auto_update: defaults.auto_update.unwrap(),
             base_keymap: Default::default(),
-            enable_copilot_integration: defaults.enable_copilot_integration.unwrap(),
+            features: Features {
+                copilot: defaults.features.copilot.unwrap(),
+            },
         }
     }
 
@@ -569,10 +546,7 @@ impl Settings {
         merge(&mut self.autosave, data.autosave);
         merge(&mut self.default_dock_anchor, data.default_dock_anchor);
         merge(&mut self.base_keymap, data.base_keymap);
-        merge(
-            &mut self.enable_copilot_integration,
-            data.enable_copilot_integration,
-        );
+        merge(&mut self.features.copilot, data.features.copilot);
 
         self.editor_overrides = data.editor;
         self.git_overrides = data.git.unwrap_or_default();
@@ -596,12 +570,15 @@ impl Settings {
         self
     }
 
-    pub fn copilot_on(&self, language: Option<&str>) -> bool {
-        if self.enable_copilot_integration {
-            self.language_setting(language, |settings| settings.copilot.map(Into::into))
-        } else {
-            false
-        }
+    pub fn features(&self) -> &Features {
+        &self.features
+    }
+
+    pub fn show_copilot_suggestions(&self, language: Option<&str>) -> bool {
+        self.features.copilot
+            && self.language_setting(language, |settings| {
+                settings.show_copilot_suggestions.map(Into::into)
+            })
     }
 
     pub fn tab_size(&self, language: Option<&str>) -> NonZeroU32 {
@@ -740,7 +717,7 @@ impl Settings {
                 format_on_save: Some(FormatOnSave::On),
                 formatter: Some(Formatter::LanguageServer),
                 enable_language_server: Some(true),
-                copilot: Some(OnOff::On),
+                show_copilot_suggestions: Some(true),
             },
             editor_overrides: Default::default(),
             journal_defaults: Default::default(),
@@ -760,7 +737,7 @@ impl Settings {
             telemetry_overrides: Default::default(),
             auto_update: true,
             base_keymap: Default::default(),
-            enable_copilot_integration: true,
+            features: Features { copilot: true },
         }
     }
 
@@ -1125,7 +1102,7 @@ mod tests {
                 {
                     "language_overrides": {
                         "JSON": {
-                            "copilot": "off"
+                            "show_copilot_suggestions": false
                         }
                     }
                 }
@@ -1135,7 +1112,7 @@ mod tests {
                 settings.languages.insert(
                     "Rust".into(),
                     EditorSettings {
-                        copilot: Some(OnOff::On),
+                        show_copilot_suggestions: Some(true),
                         ..Default::default()
                     },
                 );
@@ -1144,10 +1121,10 @@ mod tests {
                 {
                     "language_overrides": {
                         "Rust": {
-                            "copilot": "on"
+                            "show_copilot_suggestions": true
                         },
                         "JSON": {
-                            "copilot": "off"
+                            "show_copilot_suggestions": false
                         }
                     }
                 }
@@ -1163,21 +1140,21 @@ mod tests {
                 {
                     "languages": {
                         "JSON": {
-                            "copilot": "off"
+                            "show_copilot_suggestions": false
                         }
                     }
                 }
             "#
             .unindent(),
             |settings| {
-                settings.editor.copilot = Some(OnOff::On);
+                settings.editor.show_copilot_suggestions = Some(true);
             },
             r#"
                 {
-                    "copilot": "on",
+                    "show_copilot_suggestions": true,
                     "languages": {
                         "JSON": {
-                            "copilot": "off"
+                            "show_copilot_suggestions": false
                         }
                     }
                 }
@@ -1187,13 +1164,13 @@ mod tests {
     }
 
     #[test]
-    fn test_update_langauge_copilot() {
+    fn test_update_language_copilot() {
         assert_new_settings(
             r#"
                 {
                     "languages": {
                         "JSON": {
-                            "copilot": "off"
+                            "show_copilot_suggestions": false
                         }
                     }
                 }
@@ -1203,7 +1180,7 @@ mod tests {
                 settings.languages.insert(
                     "Rust".into(),
                     EditorSettings {
-                        copilot: Some(OnOff::On),
+                        show_copilot_suggestions: Some(true),
                         ..Default::default()
                     },
                 );
@@ -1212,10 +1189,10 @@ mod tests {
                 {
                     "languages": {
                         "Rust": {
-                            "copilot": "on"
+                            "show_copilot_suggestions": true
                         },
                         "JSON": {
-                            "copilot": "off"
+                            "show_copilot_suggestions": false
                         }
                     }
                 }

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<Cow<str>> {
+        Some(self.terminal().read(cx).title().into())
+    }
+
     fn tab_content(
         &self,
         _detail: Option<usize>,

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<Item = &'a Anchor>,
+        anchors: impl IntoIterator<Item = Anchor>,
     ) -> impl 'static + Future<Output = Result<()>> {
         let mut futures = Vec::new();
         for anchor in anchors {
             if !self.version.observed(anchor.timestamp)
-                && *anchor != Anchor::MAX
-                && *anchor != Anchor::MIN
+                && anchor != Anchor::MAX
+                && anchor != Anchor::MIN
             {
                 let (tx, rx) = oneshot::channel();
                 self.edit_id_resolvers

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<Cow<str>> {
+        Some("Welcome to Zed!".into())
+    }
+
     fn tab_content(
         &self,
         _detail: Option<usize>,

crates/workspace/src/item.rs 🔗

@@ -48,7 +48,10 @@ pub trait Item: View {
     fn navigate(&mut self, _: Box<dyn Any>, _: &mut ViewContext<Self>) -> bool {
         false
     }
-    fn tab_description<'a>(&'a self, _: usize, _: &'a AppContext) -> Option<Cow<'a, str>> {
+    fn tab_tooltip_text(&self, _: &AppContext) -> Option<Cow<str>> {
+        None
+    }
+    fn tab_description<'a>(&'a self, _: usize, _: &'a AppContext) -> Option<Cow<str>> {
         None
     }
     fn tab_content(
@@ -170,7 +173,8 @@ pub trait ItemHandle: 'static + fmt::Debug {
         cx: &mut WindowContext,
         handler: Box<dyn Fn(ItemEvent, &mut WindowContext)>,
     ) -> gpui::Subscription;
-    fn tab_description<'a>(&self, detail: usize, cx: &'a AppContext) -> Option<Cow<'a, str>>;
+    fn tab_tooltip_text<'a>(&self, cx: &'a AppContext) -> Option<Cow<'a, str>>;
+    fn tab_description<'a>(&'a self, detail: usize, cx: &'a AppContext) -> Option<Cow<'a, str>>;
     fn tab_content(
         &self,
         detail: Option<usize>,
@@ -260,7 +264,11 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
         })
     }
 
-    fn tab_description<'a>(&self, detail: usize, cx: &'a AppContext) -> Option<Cow<'a, str>> {
+    fn tab_tooltip_text<'a>(&self, cx: &'a AppContext) -> Option<Cow<'a, str>> {
+        self.read(cx).tab_tooltip_text(cx)
+    }
+
+    fn tab_description<'a>(&'a self, detail: usize, cx: &'a AppContext) -> Option<Cow<'a, str>> {
         self.read(cx).tab_description(detail, cx)
     }
 
@@ -912,7 +920,7 @@ pub(crate) mod test {
     }
 
     impl Item for TestItem {
-        fn tab_description<'a>(&'a self, detail: usize, _: &'a AppContext) -> Option<Cow<'a, str>> {
+        fn tab_description(&self, detail: usize, _: &AppContext) -> Option<Cow<str>> {
             self.tab_descriptions.as_ref().and_then(|descriptions| {
                 let description = *descriptions.get(detail).or_else(|| descriptions.last())?;
                 Some(description.into())

crates/workspace/src/pane.rs 🔗

@@ -1389,6 +1389,9 @@ impl Pane {
                         let detail = detail.clone();
 
                         let theme = cx.global::<Settings>().theme.clone();
+                        let mut tooltip_theme = theme.tooltip.clone();
+                        tooltip_theme.max_text_width = None;
+                        let tab_tooltip_text = item.tab_tooltip_text(cx).map(|a| a.to_string());
 
                         move |mouse_state, cx| {
                             let tab_style =
@@ -1396,39 +1399,56 @@ impl Pane {
                             let hovered = mouse_state.hovered();
 
                             enum Tab {}
-                            MouseEventHandler::<Tab, Pane>::new(ix, cx, |_, cx| {
-                                Self::render_tab::<Pane>(
-                                    &item,
-                                    pane.clone(),
-                                    ix == 0,
-                                    detail,
-                                    hovered,
-                                    tab_style,
-                                    cx,
-                                )
-                            })
-                            .on_down(MouseButton::Left, move |_, _, cx| {
-                                cx.dispatch_action(ActivateItem(ix));
-                            })
-                            .on_click(MouseButton::Middle, {
-                                let item = item.clone();
-                                let pane = pane.clone();
-                                move |_, _, cx| {
-                                    cx.dispatch_action(CloseItemById {
-                                        item_id: item.id(),
-                                        pane: pane.clone(),
-                                    })
-                                }
-                            })
-                            .on_down(MouseButton::Right, move |e, _, cx| {
-                                let item = item.clone();
-                                cx.dispatch_action(DeployTabContextMenu {
-                                    position: e.position,
-                                    item_id: item.id(),
-                                    pane: pane.clone(),
-                                });
-                            })
-                            .boxed()
+                            let mouse_event_handler =
+                                MouseEventHandler::<Tab, Pane>::new(ix, cx, |_, cx| {
+                                    Self::render_tab::<Pane>(
+                                        &item,
+                                        pane.clone(),
+                                        ix == 0,
+                                        detail,
+                                        hovered,
+                                        tab_style,
+                                        cx,
+                                    )
+                                })
+                                .on_down(MouseButton::Left, move |_, _, cx| {
+                                    cx.dispatch_action(ActivateItem(ix));
+                                })
+                                .on_click(MouseButton::Middle, {
+                                    let item = item.clone();
+                                    let pane = pane.clone();
+                                    move |_, _, cx| {
+                                        cx.dispatch_action(CloseItemById {
+                                            item_id: item.id(),
+                                            pane: pane.clone(),
+                                        })
+                                    }
+                                })
+                                .on_down(
+                                    MouseButton::Right,
+                                    move |e, _, cx| {
+                                        let item = item.clone();
+                                        cx.dispatch_action(DeployTabContextMenu {
+                                            position: e.position,
+                                            item_id: item.id(),
+                                            pane: pane.clone(),
+                                        });
+                                    },
+                                );
+
+                            if let Some(tab_tooltip_text) = tab_tooltip_text {
+                                return mouse_event_handler
+                                    .with_tooltip::<Self>(
+                                        ix,
+                                        tab_tooltip_text,
+                                        None,
+                                        tooltip_theme,
+                                        cx,
+                                    )
+                                    .boxed();
+                            }
+
+                            mouse_event_handler.boxed()
                         }
                     });
 

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<Cow<str>> {
+        Some(format!("{}'s screen", self.user.github_login).into())
+    }
     fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
         if let Some(nav_history) = self.nav_history.as_ref() {
             nav_history.push::<()>(None, cx);

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<T: ToolbarItemView> ToolbarItemViewHandle for ViewHandle<T> {
     }
 
     fn pane_focus_update(&mut self, pane_focused: bool, cx: &mut WindowContext) {
-        self.update(cx, |this, cx| this.pane_focus_update(pane_focused, cx));
+        self.update(cx, |this, cx| {
+            this.pane_focus_update(pane_focused, cx);
+            cx.notify();
+        });
     }
 }
 

crates/workspace/src/workspace.rs 🔗

@@ -290,7 +290,7 @@ pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
         let app_state = Arc::downgrade(&app_state);
         move |action: &OpenPaths, cx: &mut AppContext| {
             if let Some(app_state) = app_state.upgrade() {
-                open_paths(&action.paths, &app_state, cx).detach();
+                open_paths(&action.paths, &app_state, None, cx).detach();
             }
         }
     });
@@ -303,15 +303,28 @@ pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
             }
 
             let app_state = app_state.upgrade()?;
+            let window_id = cx.window_id();
             let action = action.clone();
-            let close = workspace.prepare_to_close(false, cx);
+            let is_remote = workspace.project.read(cx).is_remote();
+            let has_worktree = workspace.project.read(cx).worktrees(cx).next().is_some();
+            let has_dirty_items = workspace.items(cx).any(|item| item.is_dirty(cx));
+            let close_task = if is_remote || has_worktree || has_dirty_items {
+                None
+            } else {
+                Some(workspace.prepare_to_close(false, cx))
+            };
 
             Some(cx.spawn_weak(|_, mut cx| async move {
-                let can_close = close.await?;
-                if can_close {
-                    cx.update(|cx| open_paths(&action.paths, &app_state, cx))
-                        .await?;
-                }
+                let window_id_to_replace = if let Some(close_task) = close_task {
+                    if !close_task.await? {
+                        return Ok(());
+                    }
+                    Some(window_id)
+                } else {
+                    None
+                };
+                cx.update(|cx| open_paths(&action.paths, &app_state, window_id_to_replace, cx))
+                    .await?;
                 Ok(())
             }))
         }
@@ -854,6 +867,7 @@ impl Workspace {
     fn new_local(
         abs_paths: Vec<PathBuf>,
         app_state: Arc<AppState>,
+        requesting_window_id: Option<usize>,
         cx: &mut AppContext,
     ) -> Task<(
         ViewHandle<Workspace>,
@@ -868,7 +882,8 @@ impl Workspace {
         );
 
         cx.spawn(|mut cx| async move {
-            let serialized_workspace = persistence::DB.workspace_for_roots(&abs_paths.as_slice());
+            let mut serialized_workspace =
+                persistence::DB.workspace_for_roots(&abs_paths.as_slice());
 
             let paths_to_open = serialized_workspace
                 .as_ref()
@@ -915,7 +930,7 @@ impl Workspace {
                     let mut workspace = Workspace::new(
                         serialized_workspace,
                         workspace_id,
-                        project_handle,
+                        project_handle.clone(),
                         app_state.dock_default_item_factory,
                         app_state.background_actions,
                         cx,
@@ -924,46 +939,54 @@ impl Workspace {
                     workspace
                 };
 
-            let workspace = {
-                let (bounds, display) = if let Some(bounds) = window_bounds_override {
-                    (Some(bounds), None)
-                } else {
-                    serialized_workspace
-                        .as_ref()
-                        .and_then(|serialized_workspace| {
-                            let display = serialized_workspace.display?;
-                            let mut bounds = serialized_workspace.bounds?;
-
-                            // Stored bounds are relative to the containing display.
-                            // So convert back to global coordinates if that screen still exists
-                            if let WindowBounds::Fixed(mut window_bounds) = bounds {
-                                if let Some(screen) = cx.platform().screen_by_id(display) {
-                                    let screen_bounds = screen.bounds();
-                                    window_bounds.set_origin_x(
-                                        window_bounds.origin_x() + screen_bounds.origin_x(),
-                                    );
-                                    window_bounds.set_origin_y(
-                                        window_bounds.origin_y() + screen_bounds.origin_y(),
-                                    );
-                                    bounds = WindowBounds::Fixed(window_bounds);
-                                } else {
-                                    // Screen no longer exists. Return none here.
-                                    return None;
+            let workspace = requesting_window_id
+                .and_then(|window_id| {
+                    cx.update(|cx| {
+                        cx.replace_root_view(window_id, |cx| {
+                            build_workspace(cx, serialized_workspace.take())
+                        })
+                    })
+                })
+                .unwrap_or_else(|| {
+                    let (bounds, display) = if let Some(bounds) = window_bounds_override {
+                        (Some(bounds), None)
+                    } else {
+                        serialized_workspace
+                            .as_ref()
+                            .and_then(|serialized_workspace| {
+                                let display = serialized_workspace.display?;
+                                let mut bounds = serialized_workspace.bounds?;
+
+                                // Stored bounds are relative to the containing display.
+                                // So convert back to global coordinates if that screen still exists
+                                if let WindowBounds::Fixed(mut window_bounds) = bounds {
+                                    if let Some(screen) = cx.platform().screen_by_id(display) {
+                                        let screen_bounds = screen.bounds();
+                                        window_bounds.set_origin_x(
+                                            window_bounds.origin_x() + screen_bounds.origin_x(),
+                                        );
+                                        window_bounds.set_origin_y(
+                                            window_bounds.origin_y() + screen_bounds.origin_y(),
+                                        );
+                                        bounds = WindowBounds::Fixed(window_bounds);
+                                    } else {
+                                        // Screen no longer exists. Return none here.
+                                        return None;
+                                    }
                                 }
-                            }
 
-                            Some((bounds, display))
-                        })
-                        .unzip()
-                };
+                                Some((bounds, display))
+                            })
+                            .unzip()
+                    };
 
-                // Use the serialized workspace to construct the new window
-                cx.add_window(
-                    (app_state.build_window_options)(bounds, display, cx.platform().as_ref()),
-                    |cx| build_workspace(cx, serialized_workspace),
-                )
-                .1
-            };
+                    // Use the serialized workspace to construct the new window
+                    cx.add_window(
+                        (app_state.build_window_options)(bounds, display, cx.platform().as_ref()),
+                        |cx| build_workspace(cx, serialized_workspace),
+                    )
+                    .1
+                });
 
             notify_if_database_failed(&workspace, &mut cx);
 
@@ -1056,7 +1079,7 @@ impl Workspace {
         if self.project.read(cx).is_local() {
             Task::Ready(Some(Ok(callback(self, cx))))
         } else {
-            let task = Self::new_local(Vec::new(), app_state.clone(), cx);
+            let task = Self::new_local(Vec::new(), app_state.clone(), None, cx);
             cx.spawn(|_vh, mut cx| async move {
                 let (workspace, _) = task.await;
                 workspace.update(&mut cx, callback)
@@ -3025,6 +3048,7 @@ pub async fn last_opened_workspace_paths() -> Option<WorkspaceLocation> {
 pub fn open_paths(
     abs_paths: &[PathBuf],
     app_state: &Arc<AppState>,
+    requesting_window_id: Option<usize>,
     cx: &mut AppContext,
 ) -> Task<
     Result<(
@@ -3057,7 +3081,8 @@ pub fn open_paths(
                     .contains(&false);
 
             cx.update(|cx| {
-                let task = Workspace::new_local(abs_paths, app_state.clone(), cx);
+                let task =
+                    Workspace::new_local(abs_paths, app_state.clone(), requesting_window_id, cx);
 
                 cx.spawn(|mut cx| async move {
                     let (workspace, items) = task.await;
@@ -3081,7 +3106,7 @@ pub fn open_new(
     cx: &mut AppContext,
     init: impl FnOnce(&mut Workspace, &mut ViewContext<Workspace>) + 'static,
 ) -> Task<()> {
-    let task = Workspace::new_local(Vec::new(), app_state.clone(), cx);
+    let task = Workspace::new_local(Vec::new(), app_state.clone(), None, cx);
     cx.spawn(|mut cx| async move {
         let (workspace, opened_paths) = task.await;
 

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)) => {

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::<Workspace>()
+            .unwrap();
+        workspace_1.update(cx, |workspace, cx| {
+            assert_eq!(
+                workspace
+                    .worktrees(cx)
+                    .map(|w| w.read(cx).abs_path())
+                    .collect::<Vec<_>>(),
+                &[Path::new("/root/c").into(), Path::new("/root/d").into()]
+            );
+            assert!(workspace.left_sidebar().read(cx).is_open());
+            assert!(workspace.active_pane().is_focused(cx));
+        });
     }
 
     #[gpui::test]
@@ -756,7 +778,7 @@ mod tests {
             .insert_tree("/root", json!({"a": "hey"}))
             .await;
 
-        cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, cx))
+        cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, None, cx))
             .await
             .unwrap();
         assert_eq!(cx.window_ids().len(), 1);
@@ -799,7 +821,7 @@ mod tests {
         assert!(!cx.is_window_edited(workspace.window_id()));
 
         // Opening the buffer again doesn't impact the window's edited state.
-        cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, cx))
+        cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, None, cx))
             .await
             .unwrap();
         let editor = workspace.read_with(cx, |workspace, cx| {

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() {

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"),

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<SyntaxHighlightStyle, "color">
     } = {}
 
+    const light = colorScheme.isLight
+
     // then spread the default to each style
     for (const key of Object.keys({} as Syntax)) {
         syntax[key as keyof Syntax] = {
@@ -135,11 +138,20 @@ function buildDefaultSyntax(colorScheme: ColorScheme): Syntax {
         }
     }
 
+    // Mix the neutral and blue colors to get a
+    // predictive color distinct from any other color in the theme
+    const predictive = chroma.mix(
+        colorScheme.ramps.neutral(0.4).hex(),
+        colorScheme.ramps.blue(0.4).hex(),
+        0.45,
+        "lch"
+    ).hex()
+
     const color = {
         primary: colorScheme.ramps.neutral(1).hex(),
         comment: colorScheme.ramps.neutral(0.71).hex(),
         punctuation: colorScheme.ramps.neutral(0.86).hex(),
-        predictive: colorScheme.ramps.neutral(0.57).hex(),
+        predictive: predictive,
         emphasis: colorScheme.ramps.blue(0.5).hex(),
         string: colorScheme.ramps.orange(0.5).hex(),
         function: colorScheme.ramps.yellow(0.5).hex(),
@@ -169,6 +181,7 @@ function buildDefaultSyntax(colorScheme: ColorScheme): Syntax {
         },
         predictive: {
             color: color.predictive,
+            italic: true,
         },
         emphasis: {
             color: color.emphasis,