Merge branch 'main' into window_context_2

This commit is contained in:
Antonio Scandurra 2023-04-20 16:01:47 +02:00
commit c52b6328b7
47 changed files with 3046 additions and 1772 deletions

5
Cargo.lock generated
View file

@ -1213,6 +1213,7 @@ dependencies = [
"git", "git",
"gpui", "gpui",
"hyper", "hyper",
"indoc",
"language", "language",
"lazy_static", "lazy_static",
"lipsum", "lipsum",
@ -1340,14 +1341,17 @@ dependencies = [
"anyhow", "anyhow",
"async-compression", "async-compression",
"async-tar", "async-tar",
"clock",
"collections", "collections",
"context_menu", "context_menu",
"fs",
"futures 0.3.25", "futures 0.3.25",
"gpui", "gpui",
"language", "language",
"log", "log",
"lsp", "lsp",
"node_runtime", "node_runtime",
"rpc",
"serde", "serde",
"serde_derive", "serde_derive",
"settings", "settings",
@ -4687,6 +4691,7 @@ dependencies = [
"client", "client",
"clock", "clock",
"collections", "collections",
"copilot",
"ctor", "ctor",
"db", "db",
"env_logger", "env_logger",

File diff suppressed because it is too large Load diff

View file

@ -1,325 +1,325 @@
[ [
{ {
"context": "Editor && VimControl && !VimWaiting", "context": "Editor && VimControl && !VimWaiting",
"bindings": { "bindings": {
"g": [ "g": [
"vim::PushOperator", "vim::PushOperator",
{ {
"Namespace": "G" "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
]
} }
}, ],
{ "i": [
"context": "Editor && vim_mode == normal && vim_operator == none && !VimWaiting", "vim::PushOperator",
"bindings": { {
"c": [ "Object": {
"vim::PushOperator", "around": false
"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"
]
} }
}, ],
{ "a": [
"context": "Editor && vim_operator == n", "vim::PushOperator",
"bindings": { {
"0": [ "Object": {
"vim::Number", "around": true
0 }
]
} }
}, ],
{ "h": "vim::Left",
"context": "Editor && vim_operator == g", "backspace": "vim::Backspace",
"bindings": { "j": "vim::Down",
"g": "vim::StartOfDocument", "enter": "vim::NextLineStart",
"h": "editor::Hover", "k": "vim::Up",
"escape": [ "l": "vim::Right",
"vim::SwitchMode", "$": "vim::EndOfLine",
"Normal" "shift-g": "vim::EndOfDocument",
], "w": "vim::NextWordStart",
"d": "editor::GoToDefinition" "shift-w": [
"vim::NextWordStart",
{
"ignorePunctuation": true
} }
}, ],
{ "e": "vim::NextWordEnd",
"context": "Editor && vim_operator == c", "shift-e": [
"bindings": { "vim::NextWordEnd",
"c": "vim::CurrentLine" {
"ignorePunctuation": true
} }
}, ],
{ "b": "vim::PreviousWordStart",
"context": "Editor && vim_operator == d", "shift-b": [
"bindings": { "vim::PreviousWordStart",
"d": "vim::CurrentLine" {
"ignorePunctuation": true
} }
}, ],
{ "%": "vim::Matching",
"context": "Editor && vim_operator == y", "ctrl-y": [
"bindings": { "vim::Scroll",
"y": "vim::CurrentLine" "LineUp"
],
"f": [
"vim::PushOperator",
{
"FindForward": {
"before": false
}
} }
}, ],
{ "t": [
"context": "Editor && vim_operator == z", "vim::PushOperator",
"bindings": { {
"t": "editor::ScrollCursorTop", "FindForward": {
"z": "editor::ScrollCursorCenter", "before": true
"b": "editor::ScrollCursorBottom", }
"escape": [
"vim::SwitchMode",
"Normal"
]
} }
}, ],
{ "shift-f": [
"context": "Editor && VimObject", "vim::PushOperator",
"bindings": { {
"w": "vim::Word", "FindBackward": {
"shift-w": [ "after": false
"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-t": [
"context": "Editor && vim_mode == visual && !VimWaiting", "vim::PushOperator",
"bindings": { {
"u": "editor::Undo", "FindBackward": {
"c": "vim::VisualChange", "after": true
"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"
} }
],
"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
}
],
"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"
}
}
]

View file

@ -1,251 +1,257 @@
{ {
// The name of the Zed theme to use for the UI // The name of the Zed theme to use for the UI
"theme": "One Dark", "theme": "One Dark",
// The name of a font to use for rendering text in the editor // Features that can be globally enabled or disabled
"buffer_font_family": "Zed Mono", "features": {
// The OpenType features to enable for text in the editor. // Show Copilot icon in status bar
"buffer_font_features": { "copilot": true
// Disable ligatures: },
// "calt": false // The name of a font to use for rendering text in the editor
}, "buffer_font_family": "Zed Mono",
// The default font size for text in the editor // The OpenType features to enable for text in the editor.
"buffer_font_size": 15, "buffer_font_features": {
// The factor to grow the active pane by. Defaults to 1.0 // Disable ligatures:
// which gives the same size as all other panes. // "calt": false
"active_pane_magnification": 1.0, },
// Enable / disable copilot integration. // The default font size for text in the editor
"enable_copilot_integration": true, "buffer_font_size": 15,
// Controls whether copilot provides suggestion immediately // The factor to grow the active pane by. Defaults to 1.0
// or waits for a `copilot::Toggle` // which gives the same size as all other panes.
"copilot": "on", "active_pane_magnification": 1.0,
// Whether to enable vim modes and key bindings // Whether to enable vim modes and key bindings
"vim_mode": false, "vim_mode": false,
// Whether to show the informational hover box when moving the mouse // Whether to show the informational hover box when moving the mouse
// over symbols in the editor. // over symbols in the editor.
"hover_popover_enabled": true, "hover_popover_enabled": true,
// Whether to confirm before quitting Zed. // Whether to confirm before quitting Zed.
"confirm_quit": false, "confirm_quit": false,
// Whether the cursor blinks in the editor. // Whether the cursor blinks in the editor.
"cursor_blink": true, "cursor_blink": true,
// Whether to pop the completions menu while typing in an editor without // Whether to pop the completions menu while typing in an editor without
// explicitly requesting it. // explicitly requesting it.
"show_completions_on_input": true, "show_completions_on_input": true,
// Whether the screen sharing icon is shown in the os status bar. // Controls whether copilot provides suggestion immediately
"show_call_status_icon": true, // or waits for a `copilot::Toggle`
// Whether to use language servers to provide code intelligence. "show_copilot_suggestions": true,
"enable_language_server": true, // Whether the screen sharing icon is shown in the os status bar.
// When to automatically save edited buffers. This setting can "show_call_status_icon": true,
// take four values. // Whether to use language servers to provide code intelligence.
// "enable_language_server": true,
// 1. Never automatically save: // When to automatically save edited buffers. This setting can
// "autosave": "off", // take four values.
// 2. Save when changing focus away from the Zed window: //
// "autosave": "on_window_change", // 1. Never automatically save:
// 3. Save when changing focus away from a specific buffer: // "autosave": "off",
// "autosave": "on_focus_change", // 2. Save when changing focus away from the Zed window:
// 4. Save when idle for a certain amount of time: // "autosave": "on_window_change",
// "autosave": { "after_delay": {"milliseconds": 500} }, // 3. Save when changing focus away from a specific buffer:
"autosave": "off", // "autosave": "on_focus_change",
// Where to place the dock by default. This setting can take three // 4. Save when idle for a certain amount of time:
// values: // "autosave": { "after_delay": {"milliseconds": 500} },
// "autosave": "off",
// 1. Position the dock attached to the bottom of the workspace // Where to place the dock by default. This setting can take three
// "default_dock_anchor": "bottom" // values:
// 2. Position the dock to the right of the workspace like a side panel //
// "default_dock_anchor": "right" // 1. Position the dock attached to the bottom of the workspace
// 3. Position the dock full screen over the entire workspace" // "default_dock_anchor": "bottom"
// "default_dock_anchor": "expanded" // 2. Position the dock to the right of the workspace like a side panel
"default_dock_anchor": "bottom", // "default_dock_anchor": "right"
// Whether or not to remove any trailing whitespace from lines of a buffer // 3. Position the dock full screen over the entire workspace"
// before saving it. // "default_dock_anchor": "expanded"
"remove_trailing_whitespace_on_save": true, "default_dock_anchor": "bottom",
// Whether or not to ensure there's a single newline at the end of a buffer // Whether or not to remove any trailing whitespace from lines of a buffer
// when saving it. // before saving it.
"ensure_final_newline_on_save": true, "remove_trailing_whitespace_on_save": true,
// Whether or not to perform a buffer format before saving // Whether or not to ensure there's a single newline at the end of a buffer
"format_on_save": "on", // when saving it.
// How to perform a buffer format. This setting can take two values: "ensure_final_newline_on_save": true,
// // Whether or not to perform a buffer format before saving
// 1. Format code using the current language server: "format_on_save": "on",
// "format_on_save": "language_server" // How to perform a buffer format. This setting can take two values:
// 2. Format code using an external command: //
// "format_on_save": { // 1. Format code using the current language server:
// "external": { // "format_on_save": "language_server"
// "command": "prettier", // 2. Format code using an external command:
// "arguments": ["--stdin-filepath", "{buffer_path}"] // "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", "shell": "system",
// How to soft-wrap long lines of text. This setting can take // What working directory to use when launching the terminal.
// three values: // 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", "working_directory": "current_project_directory",
// 2. Soft wrap lines that overflow the editor: // Set the cursor blinking behavior in the terminal.
// "soft_wrap": "editor_width", // May take 4 values:
// 3. Soft wrap lines at the preferred line length // 1. Never blink the cursor, ignoring the terminal mode
// "soft_wrap": "preferred_line_length", // "blinking": "off",
"soft_wrap": "none", // 2. Default the cursor blink to off, but allow the terminal to
// The column at which to soft-wrap lines, for buffers where soft-wrap // set blinking
// is enabled. // "blinking": "terminal_controlled",
"preferred_line_length": 80, // 3. Always blink the cursor, ignoring the terminal mode
// Whether to indent lines using tab characters, as opposed to multiple // "blinking": "on",
// spaces. "blinking": "terminal_controlled",
"hard_tabs": false, // Set whether Alternate Scroll mode (code: ?1007) is active by default.
// How many columns a tab should occupy. // Alternate Scroll mode converts mouse scroll events into up / down key
"tab_size": 4, // presses when in the alternate screen (e.g. when running applications
// Control what info is collected by Zed. // like vim or less). The terminal can still set and unset this mode.
"telemetry": { // May take 2 values:
// Send debug info like crash reports. // 1. Default alternate scroll mode to on
"diagnostics": true, // "alternate_scroll": "on",
// Send anonymized usage data like what languages you're using Zed with. // 2. Default alternate scroll mode to off
"metrics": true // "alternate_scroll": "off",
}, "alternate_scroll": "off",
// Automatically update Zed // Set whether the option key behaves as the meta key.
"auto_update": true, // May take 2 values:
// Git gutter behavior configuration. // 1. Rely on default platform handling of option key, on macOS
"git": { // this means generating certain unicode characters
// Control whether the git gutter is shown. May take 2 values: // "option_to_meta": false,
// 1. Show the gutter // 2. Make the option keys behave as a 'meta' key, e.g. for emacs
// "git_gutter": "tracked_files" // "option_to_meta": true,
// 2. Hide the gutter "option_as_meta": false,
// "git_gutter": "hide" // Whether or not selecting text in the terminal will automatically
"git_gutter": "tracked_files" // copy to the system clipboard.
}, "copy_on_select": false,
// Settings specific to journaling // Any key-value pairs added to this list will be added to the terminal's
"journal": { // enviroment. Use `:` to seperate multiple values.
// The path of the directory where journal entries are stored "env": {
"path": "~", // "KEY": "value1:value2"
// What format to display the hours in
// May take 2 values:
// 1. hour12
// 2. hour24
"hour_format": "hour12"
},
// Settings specific to the terminal
"terminal": {
// What shell to use when opening a terminal. May take 3 values:
// 1. Use the system's default terminal configuration in /etc/passwd
// "shell": "system"
// 2. A program:
// "shell": {
// "program": "sh"
// }
// 3. A program with arguments:
// "shell": {
// "with_arguments": {
// "program": "/bin/bash",
// "arguments": ["--login"]
// }
// }
"shell": "system",
// What working directory to use when launching the terminal.
// May take 4 values:
// 1. Use the current file's project directory. Will Fallback to the
// first project directory strategy if unsuccessful
// "working_directory": "current_project_directory"
// 2. Use the first project in this workspace's directory
// "working_directory": "first_project_directory"
// 3. Always use this platform's home directory (if we can find it)
// "working_directory": "always_home"
// 4. Always use a specific directory. This value will be shell expanded.
// If this path is not a valid directory the terminal will default to
// this platform's home directory (if we can find it)
// "working_directory": {
// "always": {
// "directory": "~/zed/projects/"
// }
// }
//
//
"working_directory": "current_project_directory",
// Set the cursor blinking behavior in the terminal.
// May take 4 values:
// 1. Never blink the cursor, ignoring the terminal mode
// "blinking": "off",
// 2. Default the cursor blink to off, but allow the terminal to
// set blinking
// "blinking": "terminal_controlled",
// 3. Always blink the cursor, ignoring the terminal mode
// "blinking": "on",
"blinking": "terminal_controlled",
// Set whether Alternate Scroll mode (code: ?1007) is active by default.
// Alternate Scroll mode converts mouse scroll events into up / down key
// presses when in the alternate screen (e.g. when running applications
// like vim or less). The terminal can still set and unset this mode.
// May take 2 values:
// 1. Default alternate scroll mode to on
// "alternate_scroll": "on",
// 2. Default alternate scroll mode to off
// "alternate_scroll": "off",
"alternate_scroll": "off",
// Set whether the option key behaves as the meta key.
// May take 2 values:
// 1. Rely on default platform handling of option key, on macOS
// this means generating certain unicode characters
// "option_to_meta": false,
// 2. Make the option keys behave as a 'meta' key, e.g. for emacs
// "option_to_meta": true,
"option_as_meta": false,
// Whether or not selecting text in the terminal will automatically
// copy to the system clipboard.
"copy_on_select": false,
// Any key-value pairs added to this list will be added to the terminal's
// enviroment. Use `:` to seperate multiple values.
"env": {
// "KEY": "value1:value2"
}
// Set the terminal's font size. If this option is not included,
// the terminal will default to matching the buffer's font size.
// "font_size": "15"
// Set the terminal's font family. If this option is not included,
// the terminal will default to matching the buffer's font family.
// "font_family": "Zed Mono"
},
// Different settings for specific languages.
"languages": {
"Plain Text": {
"soft_wrap": "preferred_line_length"
},
"Elixir": {
"tab_size": 2
},
"Go": {
"tab_size": 4,
"hard_tabs": true
},
"Markdown": {
"soft_wrap": "preferred_line_length"
},
"JavaScript": {
"tab_size": 2
},
"TypeScript": {
"tab_size": 2
},
"TSX": {
"tab_size": 2
},
"YAML": {
"tab_size": 2
}
},
// LSP Specific settings.
"lsp": {
// Specify the LSP name as a key here.
// As of 8/10/22, supported LSPs are:
// pyright
// gopls
// rust-analyzer
// typescript-language-server
// vscode-json-languageserver
// "rust-analyzer": {
// //These initialization options are merged into Zed's defaults
// "initialization_options": {
// "checkOnSave": {
// "command": "clippy"
// }
// }
// }
} }
// Set the terminal's font size. If this option is not included,
// the terminal will default to matching the buffer's font size.
// "font_size": "15"
// Set the terminal's font family. If this option is not included,
// the terminal will default to matching the buffer's font family.
// "font_family": "Zed Mono"
},
// Different settings for specific languages.
"languages": {
"Plain Text": {
"soft_wrap": "preferred_line_length"
},
"Elixir": {
"tab_size": 2
},
"Go": {
"tab_size": 4,
"hard_tabs": true
},
"Markdown": {
"soft_wrap": "preferred_line_length"
},
"JavaScript": {
"tab_size": 2
},
"TypeScript": {
"tab_size": 2
},
"TSX": {
"tab_size": 2
},
"YAML": {
"tab_size": 2
},
"JSON": {
"tab_size": 2
}
},
// LSP Specific settings.
"lsp": {
// Specify the LSP name as a key here.
// As of 8/10/22, supported LSPs are:
// pyright
// gopls
// rust-analyzer
// typescript-language-server
// vscode-json-languageserver
// "rust-analyzer": {
// //These initialization options are merged into Zed's defaults
// "initialization_options": {
// "checkOnSave": {
// "command": "clippy"
// }
// }
// }
}
} }

View file

@ -7,5 +7,5 @@
// custom settings, run the `open default settings` command // custom settings, run the `open default settings` command
// from the command palette or from `Zed` application menu. // from the command palette or from `Zed` application menu.
{ {
"buffer_font_size": 15 "buffer_font_size": 15
} }

View file

@ -55,6 +55,7 @@ toml = "0.5.8"
tracing = "0.1.34" tracing = "0.1.34"
tracing-log = "0.1.3" tracing-log = "0.1.3"
tracing-subscriber = { version = "0.3.11", features = ["env-filter", "json"] } tracing-subscriber = { version = "0.3.11", features = ["env-filter", "json"] }
indoc = "1.0.4"
[dev-dependencies] [dev-dependencies]
collections = { path = "../collections", features = ["test-support"] } collections = { path = "../collections", features = ["test-support"] }

View file

@ -6,8 +6,9 @@ use call::{room, ActiveCall, ParticipantLocation, Room};
use client::{User, RECEIVE_TIMEOUT}; use client::{User, RECEIVE_TIMEOUT};
use collections::HashSet; use collections::HashSet;
use editor::{ use editor::{
ConfirmCodeAction, ConfirmCompletion, ConfirmRename, Editor, ExcerptRange, MultiBuffer, Redo, test::editor_test_context::EditorTestContext, ConfirmCodeAction, ConfirmCompletion,
Rename, ToOffset, ToggleCodeActions, Undo, ConfirmRename, Editor, ExcerptRange, MultiBuffer, Redo, Rename, ToOffset, ToggleCodeActions,
Undo,
}; };
use fs::{FakeFs, Fs as _, LineEnding, RemoveOptions}; use fs::{FakeFs, Fs as _, LineEnding, RemoveOptions};
use futures::StreamExt as _; use futures::StreamExt as _;
@ -15,6 +16,7 @@ use gpui::{
executor::Deterministic, geometry::vector::vec2f, test::EmptyView, ModelHandle, TestAppContext, executor::Deterministic, geometry::vector::vec2f, test::EmptyView, ModelHandle, TestAppContext,
ViewHandle, ViewHandle,
}; };
use indoc::indoc;
use language::{ use language::{
tree_sitter_rust, Anchor, Diagnostic, DiagnosticEntry, FakeLspAdapter, Language, tree_sitter_rust, Anchor, Diagnostic, DiagnosticEntry, FakeLspAdapter, Language,
LanguageConfig, OffsetRangeExt, Point, Rope, 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)); 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)] #[gpui::test(iterations = 10)]
async fn test_leaving_worktree_while_opening_buffer( async fn test_leaving_worktree_while_opening_buffer(
deterministic: Arc<Deterministic>, deterministic: Arc<Deterministic>,
@ -5860,10 +5960,17 @@ async fn test_basic_following(
// Client A updates their selections in those editors // Client A updates their selections in those editors
editor_a1.update(cx_a, |editor, cx| { editor_a1.update(cx_a, |editor, cx| {
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_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. // 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 .await
.unwrap(); .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(); cx_c.foreground().run_until_parked();
let active_call_c = cx_c.read(ActiveCall::global); let active_call_c = cx_c.read(ActiveCall::global);
let project_c = client_c.build_remote_project(project_id, cx_c).await; let project_c = client_c.build_remote_project(project_id, cx_c).await;
@ -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. // When client A activates a different editor, client B does so as well.
workspace_a.update(cx_a, |workspace, cx| { workspace_a.update(cx_a, |workspace, cx| {
workspace.activate_item(&editor_a1, cx) workspace.activate_item(&editor_a1, cx)

View file

@ -38,10 +38,13 @@ smol = "1.2.5"
futures = "0.3" futures = "0.3"
[dev-dependencies] [dev-dependencies]
clock = { path = "../clock" }
collections = { path = "../collections", features = ["test-support"] } collections = { path = "../collections", features = ["test-support"] }
fs = { path = "../fs", features = ["test-support"] }
gpui = { path = "../gpui", features = ["test-support"] } gpui = { path = "../gpui", features = ["test-support"] }
language = { path = "../language", features = ["test-support"] } language = { path = "../language", features = ["test-support"] }
settings = { path = "../settings", features = ["test-support"] }
lsp = { path = "../lsp", 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"] } util = { path = "../util", features = ["test-support"] }
workspace = { path = "../workspace", features = ["test-support"] } workspace = { path = "../workspace", features = ["test-support"] }

View file

@ -5,9 +5,14 @@ use anyhow::{anyhow, Context, Result};
use async_compression::futures::bufread::GzipDecoder; use async_compression::futures::bufread::GzipDecoder;
use async_tar::Archive; use async_tar::Archive;
use collections::HashMap; use collections::HashMap;
use futures::{future::Shared, Future, FutureExt, TryFutureExt}; use futures::{channel::oneshot, future::Shared, Future, FutureExt, TryFutureExt};
use gpui::{actions, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task}; use gpui::{
use language::{point_from_lsp, point_to_lsp, Anchor, Bias, Buffer, Language, ToPointUtf16}; 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 log::{debug, error};
use lsp::LanguageServer; use lsp::LanguageServer;
use node_runtime::NodeRuntime; use node_runtime::NodeRuntime;
@ -16,6 +21,7 @@ use settings::Settings;
use smol::{fs, io::BufReader, stream::StreamExt}; use smol::{fs, io::BufReader, stream::StreamExt};
use std::{ use std::{
ffi::OsString, ffi::OsString,
mem,
ops::Range, ops::Range,
path::{Path, PathBuf}, path::{Path, PathBuf},
sync::Arc, sync::Arc,
@ -29,7 +35,10 @@ const COPILOT_AUTH_NAMESPACE: &'static str = "copilot_auth";
actions!(copilot_auth, [SignIn, SignOut]); actions!(copilot_auth, [SignIn, SignOut]);
const COPILOT_NAMESPACE: &'static str = "copilot"; 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) { pub fn init(http: Arc<dyn HttpClient>, node_runtime: Arc<NodeRuntime>, cx: &mut AppContext) {
// Disable Copilot for stable releases. // Disable Copilot for stable releases.
@ -95,15 +104,38 @@ pub fn init(http: Arc<dyn HttpClient>, node_runtime: Arc<NodeRuntime>, cx: &mut
enum CopilotServer { enum CopilotServer {
Disabled, Disabled,
Starting { Starting { task: Shared<Task<()>> },
task: Shared<Task<()>>,
},
Error(Arc<str>), Error(Arc<str>),
Started { Running(RunningCopilotServer),
server: Arc<LanguageServer>, }
status: SignInStatus,
subscriptions_by_buffer_id: HashMap<usize, gpui::Subscription>, 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)] #[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 { pub struct Completion {
uuid: String,
pub range: Range<Anchor>, pub range: Range<Anchor>,
pub text: String, pub text: String,
} }
@ -148,6 +276,7 @@ pub struct Copilot {
http: Arc<dyn HttpClient>, http: Arc<dyn HttpClient>,
node_runtime: Arc<NodeRuntime>, node_runtime: Arc<NodeRuntime>,
server: CopilotServer, server: CopilotServer,
buffers: HashMap<usize, WeakModelHandle<Buffer>>,
} }
impl Entity for Copilot { impl Entity for Copilot {
@ -172,7 +301,7 @@ impl Copilot {
let http = http.clone(); let http = http.clone();
let node_runtime = node_runtime.clone(); let node_runtime = node_runtime.clone();
move |this, cx| { move |this, cx| {
if cx.global::<Settings>().enable_copilot_integration { if cx.global::<Settings>().features.copilot {
if matches!(this.server, CopilotServer::Disabled) { if matches!(this.server, CopilotServer::Disabled) {
let start_task = cx let start_task = cx
.spawn({ .spawn({
@ -194,12 +323,14 @@ impl Copilot {
}) })
.detach(); .detach();
if cx.global::<Settings>().enable_copilot_integration { if cx.global::<Settings>().features.copilot {
let start_task = cx let start_task = cx
.spawn({ .spawn({
let http = http.clone(); let http = http.clone();
let node_runtime = node_runtime.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(); .shared();
@ -207,12 +338,14 @@ impl Copilot {
http, http,
node_runtime, node_runtime,
server: CopilotServer::Starting { task: start_task }, server: CopilotServer::Starting { task: start_task },
buffers: Default::default(),
} }
} else { } else {
Self { Self {
http, http,
node_runtime, node_runtime,
server: CopilotServer::Disabled, server: CopilotServer::Disabled,
buffers: Default::default(),
} }
} }
} }
@ -225,11 +358,12 @@ impl Copilot {
let this = cx.add_model(|cx| Self { let this = cx.add_model(|cx| Self {
http: http.clone(), http: http.clone(),
node_runtime: NodeRuntime::new(http, cx.background().clone()), node_runtime: NodeRuntime::new(http, cx.background().clone()),
server: CopilotServer::Started { server: CopilotServer::Running(RunningCopilotServer {
server: Arc::new(server), lsp: Arc::new(server),
status: SignInStatus::Authorized, sign_in_status: SignInStatus::Authorized,
subscriptions_by_buffer_id: Default::default(), registered_buffers: Default::default(),
}, }),
buffers: Default::default(),
}); });
(this, fake_server) (this, fake_server)
} }
@ -281,6 +415,19 @@ impl Copilot {
) )
.detach(); .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)) anyhow::Ok((server, status))
}; };
@ -289,11 +436,11 @@ impl Copilot {
cx.notify(); cx.notify();
match server { match server {
Ok((server, status)) => { Ok((server, status)) => {
this.server = CopilotServer::Started { this.server = CopilotServer::Running(RunningCopilotServer {
server, lsp: server,
status: SignInStatus::SignedOut, sign_in_status: SignInStatus::SignedOut,
subscriptions_by_buffer_id: Default::default(), registered_buffers: Default::default(),
}; });
this.update_sign_in_status(status, cx); this.update_sign_in_status(status, cx);
} }
Err(error) => { Err(error) => {
@ -306,8 +453,8 @@ impl Copilot {
} }
fn sign_in(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> { fn sign_in(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
if let CopilotServer::Started { server, status, .. } = &mut self.server { if let CopilotServer::Running(server) = &mut self.server {
let task = match status { let task = match &server.sign_in_status {
SignInStatus::Authorized { .. } | SignInStatus::Unauthorized { .. } => { SignInStatus::Authorized { .. } | SignInStatus::Unauthorized { .. } => {
Task::ready(Ok(())).shared() Task::ready(Ok(())).shared()
} }
@ -316,11 +463,11 @@ impl Copilot {
task.clone() task.clone()
} }
SignInStatus::SignedOut => { SignInStatus::SignedOut => {
let server = server.clone(); let lsp = server.lsp.clone();
let task = cx let task = cx
.spawn(|this, mut cx| async move { .spawn(|this, mut cx| async move {
let sign_in = async { let sign_in = async {
let sign_in = server let sign_in = lsp
.request::<request::SignInInitiate>( .request::<request::SignInInitiate>(
request::SignInInitiateParams {}, request::SignInInitiateParams {},
) )
@ -331,8 +478,10 @@ impl Copilot {
} }
request::SignInInitiateResult::PromptUserDeviceFlow(flow) => { request::SignInInitiateResult::PromptUserDeviceFlow(flow) => {
this.update(&mut cx, |this, cx| { this.update(&mut cx, |this, cx| {
if let CopilotServer::Started { status, .. } = if let CopilotServer::Running(RunningCopilotServer {
&mut this.server sign_in_status: status,
..
}) = &mut this.server
{ {
if let SignInStatus::SigningIn { if let SignInStatus::SigningIn {
prompt: prompt_flow, prompt: prompt_flow,
@ -344,7 +493,7 @@ impl Copilot {
} }
} }
}); });
let response = server let response = lsp
.request::<request::SignInConfirm>( .request::<request::SignInConfirm>(
request::SignInConfirmParams { request::SignInConfirmParams {
user_code: flow.user_code, user_code: flow.user_code,
@ -372,7 +521,7 @@ impl Copilot {
}) })
}) })
.shared(); .shared();
*status = SignInStatus::SigningIn { server.sign_in_status = SignInStatus::SigningIn {
prompt: None, prompt: None,
task: task.clone(), task: task.clone(),
}; };
@ -391,10 +540,8 @@ impl Copilot {
} }
fn sign_out(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> { fn sign_out(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
if let CopilotServer::Started { server, status, .. } = &mut self.server { self.update_sign_in_status(request::SignInStatus::NotSignedIn, cx);
*status = SignInStatus::SignedOut; if let CopilotServer::Running(RunningCopilotServer { lsp: server, .. }) = &self.server {
cx.notify();
let server = server.clone(); let server = server.clone();
cx.background().spawn(async move { cx.background().spawn(async move {
server server
@ -428,6 +575,135 @@ impl Copilot {
cx.foreground().spawn(start_task) 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>( pub fn completions<T>(
&mut self, &mut self,
buffer: &ModelHandle<Buffer>, buffer: &ModelHandle<Buffer>,
@ -452,6 +728,51 @@ impl Copilot {
self.request_completions::<request::GetCompletionsCycling, _>(buffer, position, cx) 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>( fn request_completions<R, T>(
&mut self, &mut self,
buffer: &ModelHandle<Buffer>, buffer: &ModelHandle<Buffer>,
@ -459,116 +780,48 @@ impl Copilot {
cx: &mut ModelContext<Self>, cx: &mut ModelContext<Self>,
) -> Task<Result<Vec<Completion>>> ) -> Task<Result<Vec<Completion>>>
where where
R: lsp::request::Request< R: 'static
Params = request::GetCompletionsParams, + lsp::request::Request<
Result = request::GetCompletionsResult, Params = request::GetCompletionsParams,
>, Result = request::GetCompletionsResult,
>,
T: ToPointUtf16, T: ToPointUtf16,
{ {
let buffer_id = buffer.id(); self.register_buffer(buffer, cx);
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(); let server = match self.server.as_authenticated() {
cx.observe_release(buffer, move |this, _, _| { Ok(server) => server,
if let CopilotServer::Started { Err(error) => return Task::ready(Err(error)),
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);
}
})
});
server.clone()
} else {
return Task::ready(Err(anyhow!("must sign in before using copilot")));
}
}
}; };
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 settings = cx.global::<Settings>();
let position = position.to_point_utf16(&snapshot); let position = position.to_point_utf16(buffer);
let language = snapshot.language_at(position); let language = buffer.language_at(position);
let language_name = language.map(|language| language.name()); let language_name = language.map(|language| language.name());
let language_name = language_name.as_deref(); let language_name = language_name.as_deref();
let tab_size = settings.tab_size(language_name); let tab_size = settings.tab_size(language_name);
let hard_tabs = settings.hard_tabs(language_name); let hard_tabs = settings.hard_tabs(language_name);
let language_id = id_for_language(language); let relative_path = buffer
.file()
.map(|file| file.path().to_path_buf())
.unwrap_or_default();
let path; cx.foreground().spawn(async move {
let relative_path; let (version, snapshot) = snapshot.await?;
if let Some(file) = snapshot.file() { let result = lsp
if let Some(file) = file.as_local() {
path = file.abs_path(cx);
} else {
path = file.full_path(cx);
}
relative_path = file.path().to_path_buf();
} else {
path = PathBuf::new();
relative_path = PathBuf::new();
}
cx.background().spawn(async move {
let result = server
.request::<R>(request::GetCompletionsParams { .request::<R>(request::GetCompletionsParams {
doc: request::GetCompletionsDocument { doc: request::GetCompletionsDocument {
source: snapshot.text(), uri,
tab_size: tab_size.into(), tab_size: tab_size.into(),
indent_size: 1, indent_size: 1,
insert_spaces: !hard_tabs, insert_spaces: !hard_tabs,
uri,
path: path.to_string_lossy().into(),
relative_path: relative_path.to_string_lossy().into(), relative_path: relative_path.to_string_lossy().into(),
language_id,
position: point_to_lsp(position), position: point_to_lsp(position),
version: 0, version: version.try_into().unwrap(),
}, },
}) })
.await?; .await?;
@ -581,6 +834,7 @@ impl Copilot {
let end = let end =
snapshot.clip_point_utf16(point_from_lsp(completion.range.end), Bias::Left); snapshot.clip_point_utf16(point_from_lsp(completion.range.end), Bias::Left);
Completion { Completion {
uuid: completion.uuid,
range: snapshot.anchor_before(start)..snapshot.anchor_after(end), range: snapshot.anchor_before(start)..snapshot.anchor_after(end),
text: completion.text, text: completion.text,
} }
@ -595,14 +849,16 @@ impl Copilot {
CopilotServer::Starting { task } => Status::Starting { task: task.clone() }, CopilotServer::Starting { task } => Status::Starting { task: task.clone() },
CopilotServer::Disabled => Status::Disabled, CopilotServer::Disabled => Status::Disabled,
CopilotServer::Error(error) => Status::Error(error.clone()), CopilotServer::Error(error) => Status::Error(error.clone()),
CopilotServer::Started { status, .. } => match status { CopilotServer::Running(RunningCopilotServer { sign_in_status, .. }) => {
SignInStatus::Authorized { .. } => Status::Authorized, match sign_in_status {
SignInStatus::Unauthorized { .. } => Status::Unauthorized, SignInStatus::Authorized { .. } => Status::Authorized,
SignInStatus::SigningIn { prompt, .. } => Status::SigningIn { SignInStatus::Unauthorized { .. } => Status::Unauthorized,
prompt: prompt.clone(), SignInStatus::SigningIn { prompt, .. } => Status::SigningIn {
}, prompt: prompt.clone(),
SignInStatus::SignedOut => Status::SignedOut, },
}, SignInStatus::SignedOut => Status::SignedOut,
}
}
} }
} }
@ -611,14 +867,34 @@ impl Copilot {
lsp_status: request::SignInStatus, lsp_status: request::SignInStatus,
cx: &mut ModelContext<Self>, cx: &mut ModelContext<Self>,
) { ) {
if let CopilotServer::Started { status, .. } = &mut self.server { self.buffers.retain(|_, buffer| buffer.is_upgradable(cx));
*status = match lsp_status {
if let Ok(server) = self.server.as_running() {
match lsp_status {
request::SignInStatus::Ok { .. } request::SignInStatus::Ok { .. }
| request::SignInStatus::MaybeOk { .. } | request::SignInStatus::MaybeOk { .. }
| request::SignInStatus::AlreadySignedIn { .. } => SignInStatus::Authorized, | request::SignInStatus::AlreadySignedIn { .. } => {
request::SignInStatus::NotAuthorized { .. } => SignInStatus::Unauthorized, server.sign_in_status = SignInStatus::Authorized;
request::SignInStatus::NotSignedIn => SignInStatus::SignedOut, 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(); 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() { async fn clear_copilot_dir() {
remove_matching(&paths::COPILOT_DIR, |_| true).await 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!()
}
}
}

View file

@ -99,14 +99,11 @@ pub struct GetCompletionsParams {
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct GetCompletionsDocument { pub struct GetCompletionsDocument {
pub source: String,
pub tab_size: u32, pub tab_size: u32,
pub indent_size: u32, pub indent_size: u32,
pub insert_spaces: bool, pub insert_spaces: bool,
pub uri: lsp::Url, pub uri: lsp::Url,
pub path: String,
pub relative_path: String, pub relative_path: String,
pub language_id: String,
pub position: lsp::Position, pub position: lsp::Position,
pub version: usize, pub version: usize,
} }
@ -169,3 +166,60 @@ impl lsp::notification::Notification for StatusNotification {
type Params = StatusNotificationParams; type Params = StatusNotificationParams;
const METHOD: &'static str = "statusNotification"; 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";
}

View file

@ -2,6 +2,7 @@ use crate::{request::PromptUserDeviceFlow, Copilot, Status};
use gpui::{ use gpui::{
elements::*, elements::*,
geometry::rect::RectF, geometry::rect::RectF,
impl_internal_actions,
platform::{WindowBounds, WindowKind, WindowOptions}, platform::{WindowBounds, WindowKind, WindowOptions},
AnyViewHandle, AppContext, ClipboardItem, Drawable, Element, Entity, View, ViewContext, AnyViewHandle, AppContext, ClipboardItem, Drawable, Element, Entity, View, ViewContext,
ViewHandle, ViewHandle,
@ -9,6 +10,11 @@ use gpui::{
use settings::Settings; use settings::Settings;
use theme::ui::modal; use theme::ui::modal;
#[derive(PartialEq, Eq, Debug, Clone)]
struct ClickedConnect;
impl_internal_actions!(copilot_verification, [ClickedConnect]);
#[derive(PartialEq, Eq, Debug, Clone)] #[derive(PartialEq, Eq, Debug, Clone)]
struct CopyUserCode; struct CopyUserCode;
@ -61,6 +67,12 @@ pub fn init(cx: &mut AppContext) {
} }
}) })
.detach(); .detach();
cx.add_action(
|code_verification: &mut CopilotCodeVerification, _: &ClickedConnect, _| {
code_verification.connect_clicked = true;
},
);
} }
fn create_copilot_auth_window( fn create_copilot_auth_window(
@ -85,11 +97,15 @@ fn create_copilot_auth_window(
pub struct CopilotCodeVerification { pub struct CopilotCodeVerification {
status: Status, status: Status,
connect_clicked: bool,
} }
impl CopilotCodeVerification { impl CopilotCodeVerification {
pub fn new(status: Status) -> Self { 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>) { pub fn set_status(&mut self, status: Status, cx: &mut ViewContext<Self>) {
@ -147,6 +163,7 @@ impl CopilotCodeVerification {
} }
fn render_prompting_modal( fn render_prompting_modal(
connect_clicked: bool,
data: &PromptUserDeviceFlow, data: &PromptUserDeviceFlow,
style: &theme::Copilot, style: &theme::Copilot,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
@ -195,13 +212,20 @@ impl CopilotCodeVerification {
.with_style(style.auth.prompting.hint.container.clone()) .with_style(style.auth.prompting.hint.container.clone())
.boxed(), .boxed(),
theme::ui::cta_button_with_click::<ConnectButton, _, _, _>( 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.content_width,
&style.auth.cta_button, &style.auth.cta_button,
cx, cx,
{ {
let verification_uri = data.verification_uri.clone(); 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(), .boxed(),
@ -350,9 +374,20 @@ impl View for CopilotCodeVerification {
match &self.status { match &self.status {
Status::SigningIn { Status::SigningIn {
prompt: Some(prompt), prompt: Some(prompt),
} => Self::render_prompting_modal(&prompt, &style.copilot, cx), } => Self::render_prompting_modal(
Status::Unauthorized => Self::render_unauthorized_modal(&style.copilot, cx), self.connect_clicked,
Status::Authorized => Self::render_enabled_modal(&style.copilot, cx), &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(), _ => Empty::new().boxed(),
}, },
]) ])

View file

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

View file

@ -23,6 +23,7 @@ use settings::Settings;
use smallvec::SmallVec; use smallvec::SmallVec;
use std::{ use std::{
any::{Any, TypeId}, any::{Any, TypeId},
borrow::Cow,
cmp::Ordering, cmp::Ordering,
ops::Range, ops::Range,
path::PathBuf, path::PathBuf,
@ -530,6 +531,10 @@ impl Item for ProjectDiagnosticsEditor {
.update(cx, |editor, cx| editor.navigate(data, cx)) .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 { fn is_dirty(&self, cx: &AppContext) -> bool {
self.excerpts.read(cx).is_dirty(cx) self.excerpts.read(cx).is_dirty(cx)
} }

View file

@ -52,7 +52,7 @@ pub use language::{char_kind, CharKind};
use language::{ use language::{
AutoindentMode, BracketPair, Buffer, CodeAction, CodeLabel, Completion, CursorShape, AutoindentMode, BracketPair, Buffer, CodeAction, CodeLabel, Completion, CursorShape,
Diagnostic, DiagnosticSeverity, IndentKind, IndentSize, Language, OffsetRangeExt, OffsetUtf16, Diagnostic, DiagnosticSeverity, IndentKind, IndentSize, Language, OffsetRangeExt, OffsetUtf16,
Point, Rope, Selection, SelectionGoal, TransactionId, Point, Selection, SelectionGoal, TransactionId,
}; };
use link_go_to_definition::{ use link_go_to_definition::{
hide_link_definition, show_link_definition, LinkDefinitionKind, LinkGoToDefinitionState, hide_link_definition, show_link_definition, LinkDefinitionKind, LinkGoToDefinitionState,
@ -184,6 +184,7 @@ actions!(
Backspace, Backspace,
Delete, Delete,
Newline, Newline,
NewlineAbove,
NewlineBelow, NewlineBelow,
GoToDiagnostic, GoToDiagnostic,
GoToPrevDiagnostic, GoToPrevDiagnostic,
@ -301,6 +302,7 @@ pub fn init(cx: &mut AppContext) {
cx.add_action(Editor::select); cx.add_action(Editor::select);
cx.add_action(Editor::cancel); cx.add_action(Editor::cancel);
cx.add_action(Editor::newline); cx.add_action(Editor::newline);
cx.add_action(Editor::newline_above);
cx.add_action(Editor::newline_below); cx.add_action(Editor::newline_below);
cx.add_action(Editor::backspace); cx.add_action(Editor::backspace);
cx.add_action(Editor::delete); 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_async_action(Editor::find_all_references);
cx.add_action(Editor::next_copilot_suggestion); cx.add_action(Editor::next_copilot_suggestion);
cx.add_action(Editor::previous_copilot_suggestion); cx.add_action(Editor::previous_copilot_suggestion);
cx.add_action(Editor::copilot_suggest);
hover_popover::init(cx); hover_popover::init(cx);
link_go_to_definition::init(cx); link_go_to_definition::init(cx);
@ -1014,6 +1017,8 @@ impl CodeActionsMenu {
pub struct CopilotState { pub struct CopilotState {
excerpt_id: Option<ExcerptId>, excerpt_id: Option<ExcerptId>,
pending_refresh: Task<Option<()>>, pending_refresh: Task<Option<()>>,
pending_cycling_refresh: Task<Option<()>>,
cycled: bool,
completions: Vec<copilot::Completion>, completions: Vec<copilot::Completion>,
active_completion_index: usize, active_completion_index: usize,
} }
@ -1022,14 +1027,20 @@ impl Default for CopilotState {
fn default() -> Self { fn default() -> Self {
Self { Self {
excerpt_id: None, excerpt_id: None,
pending_cycling_refresh: Task::ready(Some(())),
pending_refresh: Task::ready(Some(())), pending_refresh: Task::ready(Some(())),
completions: Default::default(), completions: Default::default(),
active_completion_index: 0, active_completion_index: 0,
cycled: false,
} }
} }
} }
impl CopilotState { impl CopilotState {
fn active_completion(&self) -> Option<&copilot::Completion> {
self.completions.get(self.active_completion_index)
}
fn text_for_active_completion( fn text_for_active_completion(
&self, &self,
cursor: Anchor, cursor: Anchor,
@ -1037,7 +1048,7 @@ impl CopilotState {
) -> Option<&str> { ) -> Option<&str> {
use language::ToOffset as _; 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 excerpt_id = self.excerpt_id?;
let completion_buffer = buffer.buffer_for_excerpt(excerpt_id)?; let completion_buffer = buffer.buffer_for_excerpt(excerpt_id)?;
if excerpt_id != cursor.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) { fn push_completion(&mut self, new_completion: copilot::Completion) {
for completion in &self.completions { for completion in &self.completions {
if *completion == new_completion { if completion.text == new_completion.text && completion.range == new_completion.range {
return; return;
} }
} }
@ -1265,7 +1296,7 @@ impl Editor {
cx.subscribe(&buffer, Self::on_buffer_event), cx.subscribe(&buffer, Self::on_buffer_event),
cx.observe(&display_map, Self::on_display_map_changed), cx.observe(&display_map, Self::on_display_map_changed),
cx.observe(&blink_manager, |_, _, cx| cx.notify()), 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); this.end_selection(cx);
@ -1469,7 +1500,7 @@ impl Editor {
self.refresh_code_actions(cx); self.refresh_code_actions(cx);
self.refresh_document_highlights(cx); self.refresh_document_highlights(cx);
refresh_matching_bracket_highlights(self, 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); self.blink_manager.update(cx, BlinkManager::pause_blinking);
@ -1843,7 +1874,7 @@ impl Editor {
return; return;
} }
if self.hide_copilot_suggestion(cx).is_some() { if self.discard_copilot_suggestion(cx) {
return; return;
} }
@ -2026,13 +2057,13 @@ impl Editor {
this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(new_selections)); this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(new_selections));
if had_active_copilot_suggestion { if had_active_copilot_suggestion {
this.refresh_copilot_suggestions(cx); this.refresh_copilot_suggestions(true, cx);
if !this.has_active_copilot_suggestion(cx) { if !this.has_active_copilot_suggestion(cx) {
this.trigger_completion_on_input(&text, cx); this.trigger_completion_on_input(&text, cx);
} }
} else { } else {
this.trigger_completion_on_input(&text, cx); 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(); .collect();
this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(new_selections)); 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 cursor = selection.head();
let row = cursor.row; let row = cursor.row;
let end_of_line = snapshot let point = Point::new(row + 1, 0);
.clip_point(Point::new(row, snapshot.line_len(row)), Bias::Left) let start_of_line = snapshot.clip_point(point, Bias::Left);
.to_point(&snapshot);
let newline = "\n".to_string(); 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_inserted += 1;
rows.push(row + rows_inserted); rows.push(row + rows_inserted);
} }
self.transact(cx, |editor, cx| { self.transact(cx, |editor, cx| {
editor.edit_with_autoindent(edits, cx); editor.edit(edits, cx);
editor.change_selections(Some(Autoscroll::fit()), cx, |s| { editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
let mut index = 0; let mut index = 0;
@ -2157,6 +2246,25 @@ impl Editor {
(clipped, SelectionGoal::None) (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()?; let project = self.project.clone()?;
@ -2809,10 +2917,14 @@ impl Editor {
None 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)?; let copilot = Copilot::global(cx)?;
if self.mode != EditorMode::Full || !copilot.read(cx).status().is_authorized() { if self.mode != EditorMode::Full || !copilot.read(cx).status().is_authorized() {
self.hide_copilot_suggestion(cx); self.clear_copilot_suggestions(cx);
return None; return None;
} }
self.update_visible_copilot_suggestion(cx); self.update_visible_copilot_suggestion(cx);
@ -2820,29 +2932,36 @@ impl Editor {
let snapshot = self.buffer.read(cx).snapshot(cx); let snapshot = self.buffer.read(cx).snapshot(cx);
let cursor = self.selections.newest_anchor().head(); let cursor = self.selections.newest_anchor().head();
let language_name = snapshot.language_at(cursor).map(|language| language.name()); let language_name = snapshot.language_at(cursor).map(|language| language.name());
if !cx.global::<Settings>().copilot_on(language_name.as_deref()) { if !cx
self.hide_copilot_suggestion(cx); .global::<Settings>()
.show_copilot_suggestions(language_name.as_deref())
{
self.clear_copilot_suggestions(cx);
return None; return None;
} }
let (buffer, buffer_position) = let (buffer, buffer_position) =
self.buffer.read(cx).text_anchor_for_position(cursor, cx)?; self.buffer.read(cx).text_anchor_for_position(cursor, cx)?;
self.copilot_state.pending_refresh = cx.spawn_weak(|this, mut cx| async move { self.copilot_state.pending_refresh = cx.spawn_weak(|this, mut cx| async move {
cx.background().timer(COPILOT_DEBOUNCE_TIMEOUT).await; if debounce {
let (completion, completions_cycling) = copilot.update(&mut cx, |copilot, cx| { cx.background().timer(COPILOT_DEBOUNCE_TIMEOUT).await;
( }
copilot.completions(&buffer, buffer_position, cx),
copilot.completions_cycling(&buffer, buffer_position, cx), 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)? this.upgrade(&cx)?
.update(&mut cx, |this, cx| { .update(&mut cx, |this, cx| {
if !completions.is_empty() { 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.completions.clear();
this.copilot_state.active_completion_index = 0; this.copilot_state.active_completion_index = 0;
this.copilot_state.excerpt_id = Some(cursor.excerpt_id); this.copilot_state.excerpt_id = Some(cursor.excerpt_id);
@ -2853,46 +2972,116 @@ impl Editor {
} }
}) })
.log_err()?; .log_err()?;
Some(()) Some(())
}); });
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) { if !self.has_active_copilot_suggestion(cx) {
self.refresh_copilot_suggestions(cx); self.refresh_copilot_suggestions(false, cx);
return; 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); 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( fn previous_copilot_suggestion(
&mut self, &mut self,
_: &copilot::PreviousSuggestion, _: &copilot::PreviousSuggestion,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) { ) {
if !self.has_active_copilot_suggestion(cx) { if self.has_active_copilot_suggestion(cx) {
self.refresh_copilot_suggestions(cx); self.cycle_copilot_suggestions(Direction::Prev, cx);
return; } 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 { fn accept_copilot_suggestion(&mut self, cx: &mut ViewContext<Self>) -> bool {
if let Some(text) = self.hide_copilot_suggestion(cx) { if let Some(suggestion) = self
self.insert_with_autoindent_mode(&text.to_string(), None, cx); .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 discard_copilot_suggestion(&mut self, cx: &mut ViewContext<Self>) -> bool {
if self.has_active_copilot_suggestion(cx) {
if let Some(copilot) = Copilot::global(cx) {
copilot
.update(cx, |copilot, cx| {
copilot.discard_completions(&self.copilot_state.completions, cx)
})
.detach_and_log_err(cx);
}
self.display_map
.update(cx, |map, cx| map.replace_suggestion::<usize>(None, cx));
cx.notify();
true true
} else { } else {
false false
@ -2903,18 +3092,6 @@ impl Editor {
self.display_map.read(cx).has_suggestion() self.display_map.read(cx).has_suggestion()
} }
fn hide_copilot_suggestion(&mut self, cx: &mut ViewContext<Self>) -> Option<Rope> {
if self.has_active_copilot_suggestion(cx) {
let old_suggestion = self
.display_map
.update(cx, |map, cx| map.replace_suggestion::<usize>(None, cx));
cx.notify();
old_suggestion.map(|suggestion| suggestion.text)
} else {
None
}
}
fn update_visible_copilot_suggestion(&mut self, cx: &mut ViewContext<Self>) { fn update_visible_copilot_suggestion(&mut self, cx: &mut ViewContext<Self>) {
let snapshot = self.buffer.read(cx).snapshot(cx); let snapshot = self.buffer.read(cx).snapshot(cx);
let selection = self.selections.newest_anchor(); let selection = self.selections.newest_anchor();
@ -2924,26 +3101,31 @@ impl Editor {
|| !self.completion_tasks.is_empty() || !self.completion_tasks.is_empty()
|| selection.start != selection.end || selection.start != selection.end
{ {
self.hide_copilot_suggestion(cx); self.discard_copilot_suggestion(cx);
} else if let Some(text) = self } else if let Some(text) = self
.copilot_state .copilot_state
.text_for_active_completion(cursor, &snapshot) .text_for_active_completion(cursor, &snapshot)
{ {
self.display_map.update(cx, |map, cx| { self.display_map.update(cx, move |map, cx| {
map.replace_suggestion( map.replace_suggestion(
Some(Suggestion { Some(Suggestion {
position: cursor, position: cursor,
text: text.into(), text: text.trim_end().into(),
}), }),
cx, cx,
) )
}); });
cx.notify(); cx.notify();
} else { } 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( pub fn render_code_actions_indicator(
&self, &self,
style: &EditorStyle, style: &EditorStyle,
@ -3059,7 +3241,7 @@ impl Editor {
self.completion_tasks.clear(); self.completion_tasks.clear();
} }
self.context_menu = Some(menu); self.context_menu = Some(menu);
self.hide_copilot_suggestion(cx); self.discard_copilot_suggestion(cx);
cx.notify(); cx.notify();
} }
@ -3229,7 +3411,7 @@ impl Editor {
this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(selections)); this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(selections));
this.insert("", cx); this.insert("", cx);
this.refresh_copilot_suggestions(cx); this.refresh_copilot_suggestions(true, cx);
}); });
} }
@ -3245,7 +3427,7 @@ impl Editor {
}) })
}); });
this.insert("", cx); 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| { self.transact(cx, |this, cx| {
this.buffer.update(cx, |b, cx| b.edit(edits, None, cx)); this.buffer.update(cx, |b, cx| b.edit(edits, None, cx));
this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(selections)); 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.request_autoscroll(Autoscroll::fit(), cx);
self.unmark_text(cx); self.unmark_text(cx);
self.refresh_copilot_suggestions(cx); self.refresh_copilot_suggestions(true, cx);
cx.emit(Event::Edited); cx.emit(Event::Edited);
} }
} }
@ -4036,7 +4218,7 @@ impl Editor {
} }
self.request_autoscroll(Autoscroll::fit(), cx); self.request_autoscroll(Autoscroll::fit(), cx);
self.unmark_text(cx); self.unmark_text(cx);
self.refresh_copilot_suggestions(cx); self.refresh_copilot_suggestions(true, cx);
cx.emit(Event::Edited); cx.emit(Event::Edited);
} }
} }
@ -6490,6 +6672,7 @@ impl Editor {
multi_buffer::Event::DiagnosticsUpdated => { multi_buffer::Event::DiagnosticsUpdated => {
self.refresh_active_diagnostics(cx); self.refresh_active_diagnostics(cx);
} }
multi_buffer::Event::LanguageChanged => {}
} }
} }
@ -6497,8 +6680,8 @@ impl Editor {
cx.notify(); cx.notify();
} }
fn on_settings_changed(&mut self, cx: &mut ViewContext<Self>) { fn settings_changed(&mut self, cx: &mut ViewContext<Self>) {
self.refresh_copilot_suggestions(cx); self.refresh_copilot_suggestions(true, cx);
} }
pub fn set_searchable(&mut self, searchable: bool) { pub fn set_searchable(&mut self, searchable: bool) {

View file

@ -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«»et«»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] #[gpui::test]
async fn test_newline_below(cx: &mut gpui::TestAppContext) { async fn test_newline_below(cx: &mut gpui::TestAppContext) {
let mut cx = EditorTestContext::new(cx); let mut cx = EditorTestContext::new(cx);

View file

@ -3,12 +3,12 @@ use crate::{
movement::surrounding_word, persistence::DB, scroll::ScrollAnchor, Anchor, Autoscroll, Editor, movement::surrounding_word, persistence::DB, scroll::ScrollAnchor, Anchor, Autoscroll, Editor,
Event, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, NavigationData, ToPoint as _, Event, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, NavigationData, ToPoint as _,
}; };
use anyhow::{anyhow, Context, Result}; use anyhow::{Context, Result};
use collections::HashSet; use collections::HashSet;
use futures::future::try_join_all; use futures::future::try_join_all;
use gpui::{ use gpui::{
elements::*, geometry::vector::vec2f, AppContext, Entity, ModelHandle, Subscription, Task, elements::*, geometry::vector::vec2f, AppContext, AsyncAppContext, Entity, ModelHandle,
View, ViewContext, ViewHandle, WeakViewHandle, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle,
}; };
use language::{ use language::{
proto::serialize_anchor as serialize_text_anchor, Bias, Buffer, OffsetRangeExt, Point, 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 editor = pane.read_with(&cx, |pane, cx| {
let mut editors = pane.items_of_type::<Self>(); let mut editors = pane.items_of_type::<Self>();
editors.find(|editor| { editors.find(|editor| {
editor.remote_id(&client, cx) == Some(remote_id) let ids_match = editor.remote_id(&client, cx) == Some(remote_id);
|| state.singleton let singleton_buffer_matches = state.singleton
&& buffers.len() == 1 && buffers.first()
&& editor.read(cx).buffer.read(cx).as_singleton().as_ref() == editor.read(cx).buffer.read(cx).as_singleton().as_ref();
== Some(&buffers[0]) ids_match || singleton_buffer_matches
}) })
}); });
@ -117,46 +117,29 @@ impl FollowableItem for Editor {
multibuffer 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| { update_editor_from_message(
editor.remote_id = Some(remote_id); editor.clone(),
let buffer = editor.buffer.read(cx).read(cx); project,
let selections = state proto::update_view::Editor {
.selections selections: state.selections,
.into_iter() pending_selection: state.pending_selection,
.map(|selection| { scroll_top_anchor: state.scroll_top_anchor,
deserialize_selection(&buffer, selection) scroll_x: state.scroll_x,
.ok_or_else(|| anyhow!("invalid selection")) scroll_y: state.scroll_y,
}) ..Default::default()
.collect::<Result<Vec<_>>>()?; },
let pending_selection = state &mut cx,
.pending_selection )
.map(|selection| deserialize_selection(&buffer, selection)) .await?;
.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(())
})??;
Ok(editor) Ok(editor)
})) }))
@ -301,96 +284,9 @@ impl FollowableItem for Editor {
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) -> Task<Result<()>> { ) -> Task<Result<()>> {
let update_view::Variant::Editor(message) = message; let update_view::Variant::Editor(message) = message;
let multibuffer = self.buffer.read(cx);
let multibuffer = multibuffer.read(cx);
let buffer_ids = message
.inserted_excerpts
.iter()
.filter_map(|insertion| Some(insertion.excerpt.as_ref()?.buffer_id))
.collect::<HashSet<_>>();
let mut removals = message
.deleted_excerpts
.into_iter()
.map(ExcerptId::from_proto)
.collect::<Vec<_>>();
removals.sort_by(|a, b| a.cmp(&b, &multibuffer));
let selections = message
.selections
.into_iter()
.filter_map(|selection| deserialize_selection(&multibuffer, selection))
.collect::<Vec<_>>();
let pending_selection = message
.pending_selection
.and_then(|selection| deserialize_selection(&multibuffer, selection));
let scroll_top_anchor = message
.scroll_top_anchor
.and_then(|anchor| deserialize_anchor(&multibuffer, anchor));
drop(multibuffer);
let buffers = project.update(cx, |project, cx| {
buffer_ids
.into_iter()
.map(|id| project.open_buffer_by_id(id, cx))
.collect::<Vec<_>>()
});
let project = project.clone(); let project = project.clone();
cx.spawn(|this, mut cx| async move { cx.spawn(|this, mut cx| async move {
let _buffers = try_join_all(buffers).await?; update_editor_from_message(this, project, message, &mut cx).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(())
}) })
} }
@ -404,6 +300,128 @@ impl FollowableItem for Editor {
} }
} }
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(|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(&buffer, selection))
.collect::<Vec<_>>();
let pending_selection = message
.pending_selection
.and_then(|selection| deserialize_selection(&buffer, selection));
let scroll_top_anchor = message
.scroll_top_anchor
.and_then(|anchor| deserialize_anchor(&buffer, anchor));
anyhow::Ok((selections, pending_selection, scroll_top_anchor))
})??;
// Wait until the buffer has received all of the operations referenced by
// the editor's new state.
this.update(cx, |editor, cx| {
editor.buffer.update(cx, |buffer, cx| {
buffer.wait_for_anchors(
selections
.iter()
.chain(pending_selection.as_ref())
.flat_map(|selection| [selection.start, selection.end])
.chain(scroll_top_anchor),
cx,
)
})
})?
.await?;
// Update the editor's state.
this.update(cx, |editor, cx| {
if !selections.is_empty() || pending_selection.is_some() {
editor.set_selections_from_remote(selections, pending_selection, cx);
editor.request_autoscroll_remotely(Autoscroll::newest(), cx);
} else if let Some(scroll_top_anchor) = scroll_top_anchor {
editor.set_scroll_anchor_remote(
ScrollAnchor {
top_anchor: scroll_top_anchor,
offset: vec2f(message.scroll_x, message.scroll_y),
},
cx,
);
}
})?;
Ok(())
}
fn serialize_excerpt( fn serialize_excerpt(
buffer_id: u64, buffer_id: u64,
id: &ExcerptId, id: &ExcerptId,
@ -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)? { match path_for_buffer(&self.buffer, detail, true, cx)? {
Cow::Borrowed(path) => Some(path.to_string_lossy()), Cow::Borrowed(path) => Some(path.to_string_lossy()),
Cow::Owned(path) => Some(path.to_string_lossy().to_string().into()), Cow::Owned(path) => Some(path.to_string_lossy().to_string().into()),

View file

@ -1,6 +1,7 @@
mod anchor; mod anchor;
pub use anchor::{Anchor, AnchorRangeExt}; pub use anchor::{Anchor, AnchorRangeExt};
use anyhow::{anyhow, Result};
use clock::ReplicaId; use clock::ReplicaId;
use collections::{BTreeMap, Bound, HashMap, HashSet}; use collections::{BTreeMap, Bound, HashMap, HashSet};
use futures::{channel::mpsc, SinkExt}; use futures::{channel::mpsc, SinkExt};
@ -16,7 +17,9 @@ use language::{
use std::{ use std::{
borrow::Cow, borrow::Cow,
cell::{Ref, RefCell}, cell::{Ref, RefCell},
cmp, fmt, io, cmp, fmt,
future::Future,
io,
iter::{self, FromIterator}, iter::{self, FromIterator},
mem, mem,
ops::{Range, RangeBounds, Sub}, ops::{Range, RangeBounds, Sub},
@ -61,6 +64,7 @@ pub enum Event {
}, },
Edited, Edited,
Reloaded, Reloaded,
LanguageChanged,
Reparsed, Reparsed,
Saved, Saved,
FileHandleChanged, FileHandleChanged,
@ -1238,6 +1242,39 @@ impl MultiBuffer {
cx.notify(); 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>( pub fn text_anchor_for_position<T: ToOffset>(
&self, &self,
position: T, position: T,
@ -1266,6 +1303,7 @@ impl MultiBuffer {
language::Event::Saved => Event::Saved, language::Event::Saved => Event::Saved,
language::Event::FileHandleChanged => Event::FileHandleChanged, language::Event::FileHandleChanged => Event::FileHandleChanged,
language::Event::Reloaded => Event::Reloaded, language::Event::Reloaded => Event::Reloaded,
language::Event::LanguageChanged => Event::LanguageChanged,
language::Event::Reparsed => Event::Reparsed, language::Event::Reparsed => Event::Reparsed,
language::Event::DiagnosticsUpdated => Event::DiagnosticsUpdated, language::Event::DiagnosticsUpdated => Event::DiagnosticsUpdated,
language::Event::Closed => Event::Closed, language::Event::Closed => Event::Closed,

View file

@ -166,7 +166,7 @@ impl<'a> EditorTestContext<'a> {
/// ///
/// See the `util::test::marked_text_ranges` function for more information. /// See the `util::test::marked_text_ranges` function for more information.
pub fn set_state(&mut self, marked_text: &str) -> ContextHandle { 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: \"{}\"", "Initial Editor State: \"{}\"",
marked_text.escape_debug().to_string() marked_text.escape_debug().to_string()
)); ));
@ -177,7 +177,23 @@ impl<'a> EditorTestContext<'a> {
s.select_ranges(selection_ranges) 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 /// 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) { pub fn assert_editor_state(&mut self, marked_text: &str) {
let (unmarked_text, expected_selections) = marked_text_ranges(marked_text, true); let (unmarked_text, expected_selections) = marked_text_ranges(marked_text, true);
let buffer_text = self.buffer_text(); let buffer_text = self.buffer_text();
assert_eq!(
buffer_text, unmarked_text, if buffer_text != unmarked_text {
"Unmarked text doesn't match buffer 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()) self.assert_selections(expected_selections, marked_text.to_string())
} }

View file

@ -1,5 +1,6 @@
use std::{ use std::{
any::TypeId, any::TypeId,
borrow::Cow,
ops::{Range, RangeInclusive}, ops::{Range, RangeInclusive},
sync::Arc, sync::Arc,
}; };
@ -245,6 +246,10 @@ impl Entity for FeedbackEditor {
} }
impl Item 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> { fn tab_content(&self, _: Option<usize>, style: &theme::Tab, _: &AppContext) -> Element<Pane> {
Flex::row() Flex::row()
.with_child( .with_child(

View file

@ -37,7 +37,7 @@ pub struct TooltipStyle {
pub container: ContainerStyle, pub container: ContainerStyle,
pub text: TextStyle, pub text: TextStyle,
keystroke: KeystrokeStyle, keystroke: KeystrokeStyle,
pub max_text_width: f32, pub max_text_width: Option<f32>,
} }
#[derive(Clone, Deserialize, Default)] #[derive(Clone, Deserialize, Default)]
@ -135,9 +135,14 @@ impl<V: View> Tooltip<V> {
) -> impl Drawable<V> { ) -> impl Drawable<V> {
Flex::row() Flex::row()
.with_child({ .with_child({
let text = Text::new(text, style.text) let text = if let Some(max_text_width) = style.max_text_width {
.constrained() Text::new(text, style.text)
.with_max_width(style.max_text_width); .constrained()
.with_max_width(max_text_width)
} else {
Text::new(text, style.text).constrained()
};
if measure { if measure {
text.flex(1., false).boxed() text.flex(1., false).boxed()
} else { } else {

View file

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

View file

@ -187,6 +187,7 @@ pub enum Event {
Saved, Saved,
FileHandleChanged, FileHandleChanged,
Reloaded, Reloaded,
LanguageChanged,
Reparsed, Reparsed,
DiagnosticsUpdated, DiagnosticsUpdated,
Closed, Closed,
@ -536,6 +537,7 @@ impl Buffer {
self.syntax_map.lock().clear(); self.syntax_map.lock().clear();
self.language = language; self.language = language;
self.reparse(cx); self.reparse(cx);
cx.emit(Event::LanguageChanged);
} }
pub fn set_language_registry(&mut self, language_registry: Arc<LanguageRegistry>) { pub fn set_language_registry(&mut self, language_registry: Arc<LanguageRegistry>) {
@ -1313,10 +1315,10 @@ impl Buffer {
self.text.wait_for_edits(edit_ids) self.text.wait_for_edits(edit_ids)
} }
pub fn wait_for_anchors<'a>( pub fn wait_for_anchors(
&mut self, &mut self,
anchors: impl IntoIterator<Item = &'a Anchor>, anchors: impl IntoIterator<Item = Anchor>,
) -> impl Future<Output = Result<()>> { ) -> impl 'static + Future<Output = Result<()>> {
self.text.wait_for_anchors(anchors) self.text.wait_for_anchors(anchors)
} }

View file

@ -81,14 +81,14 @@ fn test_select_language() {
// matching file extension // matching file extension
assert_eq!( assert_eq!(
registry registry
.language_for_path("zed/lib.rs") .language_for_file("zed/lib.rs", None)
.now_or_never() .now_or_never()
.and_then(|l| Some(l.ok()?.name())), .and_then(|l| Some(l.ok()?.name())),
Some("Rust".into()) Some("Rust".into())
); );
assert_eq!( assert_eq!(
registry registry
.language_for_path("zed/lib.mk") .language_for_file("zed/lib.mk", None)
.now_or_never() .now_or_never()
.and_then(|l| Some(l.ok()?.name())), .and_then(|l| Some(l.ok()?.name())),
Some("Make".into()) Some("Make".into())
@ -97,7 +97,7 @@ fn test_select_language() {
// matching filename // matching filename
assert_eq!( assert_eq!(
registry registry
.language_for_path("zed/Makefile") .language_for_file("zed/Makefile", None)
.now_or_never() .now_or_never()
.and_then(|l| Some(l.ok()?.name())), .and_then(|l| Some(l.ok()?.name())),
Some("Make".into()) Some("Make".into())
@ -106,21 +106,21 @@ fn test_select_language() {
// matching suffix that is not the full file extension or filename // matching suffix that is not the full file extension or filename
assert_eq!( assert_eq!(
registry registry
.language_for_path("zed/cars") .language_for_file("zed/cars", None)
.now_or_never() .now_or_never()
.and_then(|l| Some(l.ok()?.name())), .and_then(|l| Some(l.ok()?.name())),
None None
); );
assert_eq!( assert_eq!(
registry registry
.language_for_path("zed/a.cars") .language_for_file("zed/a.cars", None)
.now_or_never() .now_or_never()
.and_then(|l| Some(l.ok()?.name())), .and_then(|l| Some(l.ok()?.name())),
None None
); );
assert_eq!( assert_eq!(
registry registry
.language_for_path("zed/sumk") .language_for_file("zed/sumk", None)
.now_or_never() .now_or_never()
.and_then(|l| Some(l.ok()?.name())), .and_then(|l| Some(l.ok()?.name())),
None None

View file

@ -262,6 +262,8 @@ pub struct LanguageConfig {
pub name: Arc<str>, pub name: Arc<str>,
pub path_suffixes: Vec<String>, pub path_suffixes: Vec<String>,
pub brackets: BracketPairConfig, 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")] #[serde(default = "auto_indent_using_last_non_empty_line_default")]
pub auto_indent_using_last_non_empty_line: bool, pub auto_indent_using_last_non_empty_line: bool,
#[serde(default, deserialize_with = "deserialize_regex")] #[serde(default, deserialize_with = "deserialize_regex")]
@ -334,6 +336,7 @@ impl Default for LanguageConfig {
path_suffixes: Default::default(), path_suffixes: Default::default(),
brackets: Default::default(), brackets: Default::default(),
auto_indent_using_last_non_empty_line: auto_indent_using_last_non_empty_line_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(), increase_indent_pattern: Default::default(),
decrease_indent_pattern: Default::default(), decrease_indent_pattern: Default::default(),
autoclose_before: 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>, self: &Arc<Self>,
path: impl AsRef<Path>, path: impl AsRef<Path>,
content: Option<&Rope>,
) -> UnwrapFuture<oneshot::Receiver<Result<Arc<Language>>>> { ) -> UnwrapFuture<oneshot::Receiver<Result<Arc<Language>>>> {
let path = path.as_ref(); let path = path.as_ref();
let filename = path.file_name().and_then(|name| name.to_str()); let filename = path.file_name().and_then(|name| name.to_str());
let extension = path.extension().and_then(|name| name.to_str()); let extension = path.extension().and_then(|name| name.to_str());
let path_suffixes = [extension, filename]; let path_suffixes = [extension, filename];
self.get_or_load_language(|config| { self.get_or_load_language(|config| {
config let path_matches = config
.path_suffixes .path_suffixes
.iter() .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)] #[cfg(test)]
mod tests { mod tests {
use super::*;
use gpui::TestAppContext; 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)] #[gpui::test(iterations = 10)]
async fn test_language_loading(cx: &mut TestAppContext) { async fn test_language_loading(cx: &mut TestAppContext) {

View file

@ -187,8 +187,6 @@ impl<D: PickerDelegate> Picker<D> {
confirmed: false, confirmed: false,
pending_update_matches: Task::ready(None), 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.update_matches(String::new(), cx);
this this
} }

View file

@ -19,6 +19,7 @@ test-support = [
[dependencies] [dependencies]
text = { path = "../text" } text = { path = "../text" }
copilot = { path = "../copilot" }
client = { path = "../client" } client = { path = "../client" }
clock = { path = "../clock" } clock = { path = "../clock" }
collections = { path = "../collections" } collections = { path = "../collections" }

View file

@ -572,7 +572,7 @@ async fn location_links_from_proto(
.and_then(deserialize_anchor) .and_then(deserialize_anchor)
.ok_or_else(|| anyhow!("missing origin end"))?; .ok_or_else(|| anyhow!("missing origin end"))?;
buffer buffer
.update(&mut cx, |buffer, _| buffer.wait_for_anchors([&start, &end])) .update(&mut cx, |buffer, _| buffer.wait_for_anchors([start, end]))
.await?; .await?;
Some(Location { Some(Location {
buffer, buffer,
@ -597,7 +597,7 @@ async fn location_links_from_proto(
.and_then(deserialize_anchor) .and_then(deserialize_anchor)
.ok_or_else(|| anyhow!("missing target end"))?; .ok_or_else(|| anyhow!("missing target end"))?;
buffer buffer
.update(&mut cx, |buffer, _| buffer.wait_for_anchors([&start, &end])) .update(&mut cx, |buffer, _| buffer.wait_for_anchors([start, end]))
.await?; .await?;
let target = Location { let target = Location {
buffer, buffer,
@ -868,7 +868,7 @@ impl LspCommand for GetReferences {
.and_then(deserialize_anchor) .and_then(deserialize_anchor)
.ok_or_else(|| anyhow!("missing target end"))?; .ok_or_else(|| anyhow!("missing target end"))?;
target_buffer target_buffer
.update(&mut cx, |buffer, _| buffer.wait_for_anchors([&start, &end])) .update(&mut cx, |buffer, _| buffer.wait_for_anchors([start, end]))
.await?; .await?;
locations.push(Location { locations.push(Location {
buffer: target_buffer, buffer: target_buffer,
@ -1012,7 +1012,7 @@ impl LspCommand for GetDocumentHighlights {
.and_then(deserialize_anchor) .and_then(deserialize_anchor)
.ok_or_else(|| anyhow!("missing target end"))?; .ok_or_else(|| anyhow!("missing target end"))?;
buffer buffer
.update(&mut cx, |buffer, _| buffer.wait_for_anchors([&start, &end])) .update(&mut cx, |buffer, _| buffer.wait_for_anchors([start, end]))
.await?; .await?;
let kind = match proto::document_highlight::Kind::from_i32(highlight.kind) { let kind = match proto::document_highlight::Kind::from_i32(highlight.kind) {
Some(proto::document_highlight::Kind::Text) => DocumentHighlightKind::TEXT, Some(proto::document_highlight::Kind::Text) => DocumentHighlightKind::TEXT,

View file

@ -12,6 +12,7 @@ use anyhow::{anyhow, Context, Result};
use client::{proto, Client, TypedEnvelope, UserStore}; use client::{proto, Client, TypedEnvelope, UserStore};
use clock::ReplicaId; use clock::ReplicaId;
use collections::{hash_map, BTreeMap, HashMap, HashSet}; use collections::{hash_map, BTreeMap, HashMap, HashSet};
use copilot::Copilot;
use futures::{ use futures::{
channel::mpsc::{self, UnboundedReceiver}, channel::mpsc::{self, UnboundedReceiver},
future::{try_join_all, Shared}, future::{try_join_all, Shared},
@ -129,6 +130,7 @@ pub struct Project {
_maintain_buffer_languages: Task<()>, _maintain_buffer_languages: Task<()>,
_maintain_workspace_config: Task<()>, _maintain_workspace_config: Task<()>,
terminals: Terminals, terminals: Terminals,
copilot_enabled: bool,
} }
enum BufferMessage { enum BufferMessage {
@ -472,6 +474,7 @@ impl Project {
terminals: Terminals { terminals: Terminals {
local_handles: Vec::new(), local_handles: Vec::new(),
}, },
copilot_enabled: Copilot::global(cx).is_some(),
} }
}) })
} }
@ -559,6 +562,7 @@ impl Project {
terminals: Terminals { terminals: Terminals {
local_handles: Vec::new(), local_handles: Vec::new(),
}, },
copilot_enabled: Copilot::global(cx).is_some(),
}; };
for worktree in worktrees { for worktree in worktrees {
let _ = this.add_worktree(&worktree, cx); let _ = this.add_worktree(&worktree, cx);
@ -664,6 +668,15 @@ impl Project {
self.start_language_server(worktree_id, worktree_path, language, cx); 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(); cx.notify();
} }
@ -1616,6 +1629,7 @@ impl Project {
self.detect_language_for_buffer(buffer, cx); self.detect_language_for_buffer(buffer, cx);
self.register_buffer_with_language_server(buffer, cx); self.register_buffer_with_language_server(buffer, cx);
self.register_buffer_with_copilot(buffer, cx);
cx.observe_release(buffer, |this, buffer, cx| { cx.observe_release(buffer, |this, buffer, cx| {
if let Some(file) = File::from_dyn(buffer.file()) { if let Some(file) = File::from_dyn(buffer.file()) {
if file.is_local() { 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( async fn send_buffer_messages(
this: WeakModelHandle<Self>, this: WeakModelHandle<Self>,
rx: UnboundedReceiver<BufferMessage>, rx: UnboundedReceiver<BufferMessage>,
@ -2013,17 +2037,19 @@ impl Project {
fn detect_language_for_buffer( fn detect_language_for_buffer(
&mut self, &mut self,
buffer: &ModelHandle<Buffer>, buffer_handle: &ModelHandle<Buffer>,
cx: &mut ModelContext<Self>, cx: &mut ModelContext<Self>,
) -> Option<()> { ) -> Option<()> {
// If the buffer has a language, set it and start the language server if we haven't already. // If the buffer has a language, set it and start the language server if we haven't already.
let full_path = buffer.read(cx).file()?.full_path(cx); let buffer = buffer_handle.read(cx);
let full_path = buffer.file()?.full_path(cx);
let content = buffer.as_rope();
let new_language = self let new_language = self
.languages .languages
.language_for_path(&full_path) .language_for_file(&full_path, Some(content))
.now_or_never()? .now_or_never()?
.ok()?; .ok()?;
self.set_language_for_buffer(buffer, new_language, cx); self.set_language_for_buffer(buffer_handle, new_language, cx);
None None
} }
@ -2434,26 +2460,23 @@ impl Project {
buffers: impl IntoIterator<Item = ModelHandle<Buffer>>, buffers: impl IntoIterator<Item = ModelHandle<Buffer>>,
cx: &mut ModelContext<Self>, cx: &mut ModelContext<Self>,
) -> Option<()> { ) -> 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() .into_iter()
.filter_map(|buffer| { .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 = 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); 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(); .collect();
for (worktree_id, worktree_abs_path, full_path) in language_server_lookup_info { for (worktree_id, worktree_abs_path, language) in language_server_lookup_info {
if let Some(language) = self self.restart_language_server(worktree_id, worktree_abs_path, language, cx);
.languages
.language_for_path(&full_path)
.now_or_never()
.and_then(|language| language.ok())
{
self.restart_language_server(worktree_id, worktree_abs_path, language, cx);
}
} }
None None
@ -3487,7 +3510,7 @@ impl Project {
let adapter_language = adapter_language.clone(); let adapter_language = adapter_language.clone();
let language = this let language = this
.languages .languages
.language_for_path(&project_path.path) .language_for_file(&project_path.path, None)
.unwrap_or_else(move |_| adapter_language); .unwrap_or_else(move |_| adapter_language);
let language_server_name = adapter.name.clone(); let language_server_name = adapter.name.clone();
Some(async move { Some(async move {
@ -5916,7 +5939,10 @@ impl Project {
worktree_id, worktree_id,
path: PathBuf::from(serialized_symbol.path).into(), path: PathBuf::from(serialized_symbol.path).into(),
}; };
let language = languages.language_for_path(&path.path).await.log_err(); let language = languages
.language_for_file(&path.path, None)
.await
.log_err();
Ok(Symbol { Ok(Symbol {
language_server_name: LanguageServerName( language_server_name: LanguageServerName(
serialized_symbol.language_server_name.into(), serialized_symbol.language_server_name.into(),

View file

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

View file

@ -21,6 +21,7 @@ use settings::Settings;
use smallvec::SmallVec; use smallvec::SmallVec;
use std::{ use std::{
any::{Any, TypeId}, any::{Any, TypeId},
borrow::Cow,
mem, mem,
ops::Range, ops::Range,
path::PathBuf, path::PathBuf,
@ -224,6 +225,10 @@ impl View for ProjectSearchView {
} }
impl Item 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>( fn act_as_type<'a>(
&'a self, &'a self,
type_id: TypeId, type_id: TypeId,

View file

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

View file

@ -3,6 +3,7 @@ pub mod terminal_button;
pub mod terminal_element; pub mod terminal_element;
use std::{ use std::{
borrow::Cow,
ops::RangeInclusive, ops::RangeInclusive,
path::{Path, PathBuf}, path::{Path, PathBuf},
time::Duration, time::Duration,
@ -541,6 +542,10 @@ impl View for TerminalView {
} }
impl Item 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( fn tab_content(
&self, &self,
_detail: Option<usize>, _detail: Option<usize>,

View file

@ -1331,15 +1331,15 @@ impl Buffer {
} }
} }
pub fn wait_for_anchors<'a>( pub fn wait_for_anchors(
&mut self, &mut self,
anchors: impl IntoIterator<Item = &'a Anchor>, anchors: impl IntoIterator<Item = Anchor>,
) -> impl 'static + Future<Output = Result<()>> { ) -> impl 'static + Future<Output = Result<()>> {
let mut futures = Vec::new(); let mut futures = Vec::new();
for anchor in anchors { for anchor in anchors {
if !self.version.observed(anchor.timestamp) if !self.version.observed(anchor.timestamp)
&& *anchor != Anchor::MAX && anchor != Anchor::MAX
&& *anchor != Anchor::MIN && anchor != Anchor::MIN
{ {
let (tx, rx) = oneshot::channel(); let (tx, rx) = oneshot::channel();
self.edit_id_resolvers self.edit_id_resolvers

View file

@ -1,6 +1,6 @@
mod base_keymap_picker; mod base_keymap_picker;
use std::sync::Arc; use std::{borrow::Cow, sync::Arc};
use db::kvp::KEY_VALUE_STORE; use db::kvp::KEY_VALUE_STORE;
use gpui::{ use gpui::{
@ -198,6 +198,10 @@ impl WelcomePage {
} }
impl Item for WelcomePage { impl Item for WelcomePage {
fn tab_tooltip_text(&self, _: &AppContext) -> Option<Cow<str>> {
Some("Welcome to Zed!".into())
}
fn tab_content( fn tab_content(
&self, &self,
_detail: Option<usize>, _detail: Option<usize>,

View file

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

View file

@ -1389,6 +1389,9 @@ impl Pane {
let detail = detail.clone(); let detail = detail.clone();
let theme = cx.global::<Settings>().theme.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| { move |mouse_state, cx| {
let tab_style = let tab_style =
@ -1396,39 +1399,56 @@ impl Pane {
let hovered = mouse_state.hovered(); let hovered = mouse_state.hovered();
enum Tab {} enum Tab {}
MouseEventHandler::<Tab, Pane>::new(ix, cx, |_, cx| { let mouse_event_handler =
Self::render_tab::<Pane>( MouseEventHandler::<Tab, Pane>::new(ix, cx, |_, cx| {
&item, Self::render_tab::<Pane>(
pane.clone(), &item,
ix == 0, pane.clone(),
detail, ix == 0,
hovered, detail,
tab_style, hovered,
cx, tab_style,
) cx,
}) )
.on_down(MouseButton::Left, move |_, _, cx| { })
cx.dispatch_action(ActivateItem(ix)); .on_down(MouseButton::Left, move |_, _, cx| {
}) cx.dispatch_action(ActivateItem(ix));
.on_click(MouseButton::Middle, { })
let item = item.clone(); .on_click(MouseButton::Middle, {
let pane = pane.clone(); let item = item.clone();
move |_, _, cx| { let pane = pane.clone();
cx.dispatch_action(CloseItemById { move |_, _, cx| {
item_id: item.id(), cx.dispatch_action(CloseItemById {
pane: pane.clone(), item_id: item.id(),
}) pane: pane.clone(),
} })
}) }
.on_down(MouseButton::Right, move |e, _, cx| { })
let item = item.clone(); .on_down(
cx.dispatch_action(DeployTabContextMenu { MouseButton::Right,
position: e.position, move |e, _, cx| {
item_id: item.id(), let item = item.clone();
pane: pane.clone(), cx.dispatch_action(DeployTabContextMenu {
}); position: e.position,
}) item_id: item.id(),
.boxed() 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()
} }
}); });

View file

@ -14,7 +14,10 @@ use gpui::{
}; };
use settings::Settings; use settings::Settings;
use smallvec::SmallVec; use smallvec::SmallVec;
use std::sync::{Arc, Weak}; use std::{
borrow::Cow,
sync::{Arc, Weak},
};
pub enum Event { pub enum Event {
Close, Close,
@ -94,6 +97,9 @@ impl View for SharedScreen {
} }
impl Item 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>) { fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
if let Some(nav_history) = self.nav_history.as_ref() { if let Some(nav_history) = self.nav_history.as_ref() {
nav_history.push::<()>(None, cx); nav_history.push::<()>(None, cx);

View file

@ -70,6 +70,7 @@ impl View for Toolbar {
for (item, position) in &self.items { for (item, position) in &self.items {
match *position { match *position {
ToolbarItemLocation::Hidden => {} ToolbarItemLocation::Hidden => {}
ToolbarItemLocation::PrimaryLeft { flex } => { ToolbarItemLocation::PrimaryLeft { flex } => {
let left_item = ChildView::new(item.as_any(), cx) let left_item = ChildView::new(item.as_any(), cx)
.aligned() .aligned()
@ -81,6 +82,7 @@ impl View for Toolbar {
primary_left_items.push(left_item.boxed()); primary_left_items.push(left_item.boxed());
} }
} }
ToolbarItemLocation::PrimaryRight { flex } => { ToolbarItemLocation::PrimaryRight { flex } => {
let right_item = ChildView::new(item.as_any(), cx) let right_item = ChildView::new(item.as_any(), cx)
.aligned() .aligned()
@ -93,6 +95,7 @@ impl View for Toolbar {
primary_right_items.push(right_item.boxed()); primary_right_items.push(right_item.boxed());
} }
} }
ToolbarItemLocation::Secondary => { ToolbarItemLocation::Secondary => {
secondary_item = Some( secondary_item = Some(
ChildView::new(item.as_any(), cx) 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) { 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();
});
} }
} }

View file

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

View file

@ -1,5 +1,6 @@
name = "JavaScript" name = "JavaScript"
path_suffixes = ["js", "jsx", "mjs"] path_suffixes = ["js", "jsx", "mjs"]
first_line_pattern = '^#!.*\bnode\b'
line_comment = "// " line_comment = "// "
autoclose_before = ";:.,=}])>" autoclose_before = ";:.,=}])>"
brackets = [ brackets = [

View file

@ -1,5 +1,6 @@
name = "Python" name = "Python"
path_suffixes = ["py", "pyi"] path_suffixes = ["py", "pyi"]
first_line_pattern = '^#!.*\bpython[0-9.]*\b'
line_comment = "# " line_comment = "# "
autoclose_before = ";:.,=}])>" autoclose_before = ";:.,=}])>"
brackets = [ brackets = [

View file

@ -1,5 +1,6 @@
name = "Ruby" name = "Ruby"
path_suffixes = ["rb", "Gemfile"] path_suffixes = ["rb", "Gemfile"]
first_line_pattern = '^#!.*\bruby\b'
line_comment = "# " line_comment = "# "
autoclose_before = ";:.,=}])>" autoclose_before = ";:.,=}])>"
brackets = [ brackets = [

View file

@ -219,7 +219,7 @@ fn main() {
cx.spawn(|cx| handle_cli_connection(connection, app_state.clone(), cx)) cx.spawn(|cx| handle_cli_connection(connection, app_state.clone(), cx))
.detach(); .detach();
} else if let Ok(Some(paths)) = open_paths_rx.try_next() { } else if let Ok(Some(paths)) = open_paths_rx.try_next() {
cx.update(|cx| workspace::open_paths(&paths, &app_state, cx)) cx.update(|cx| workspace::open_paths(&paths, &app_state, None, cx))
.detach(); .detach();
} else { } else {
cx.spawn({ cx.spawn({
@ -243,7 +243,7 @@ fn main() {
let app_state = app_state.clone(); let app_state = app_state.clone();
async move { async move {
while let Some(paths) = open_paths_rx.next().await { while let Some(paths) = open_paths_rx.next().await {
cx.update(|cx| workspace::open_paths(&paths, &app_state, cx)) cx.update(|cx| workspace::open_paths(&paths, &app_state, None, cx))
.detach(); .detach();
} }
} }
@ -609,7 +609,7 @@ async fn handle_cli_connection(
let mut errored = false; let mut errored = false;
match cx match cx
.update(|cx| workspace::open_paths(&paths, &app_state, cx)) .update(|cx| workspace::open_paths(&paths, &app_state, None, cx))
.await .await
{ {
Ok((workspace, items)) => { Ok((workspace, items)) => {

View file

@ -702,6 +702,7 @@ mod tests {
open_paths( open_paths(
&[PathBuf::from("/root/a"), PathBuf::from("/root/b")], &[PathBuf::from("/root/a"), PathBuf::from("/root/b")],
&app_state, &app_state,
None,
cx, cx,
) )
}) })
@ -709,7 +710,7 @@ mod tests {
.unwrap(); .unwrap();
assert_eq!(cx.window_ids().len(), 1); assert_eq!(cx.window_ids().len(), 1);
cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, cx)) cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, None, cx))
.await .await
.unwrap(); .unwrap();
assert_eq!(cx.window_ids().len(), 1); assert_eq!(cx.window_ids().len(), 1);
@ -728,6 +729,7 @@ mod tests {
open_paths( open_paths(
&[PathBuf::from("/root/b"), PathBuf::from("/root/c")], &[PathBuf::from("/root/b"), PathBuf::from("/root/c")],
&app_state, &app_state,
None,
cx, cx,
) )
}) })
@ -735,16 +737,36 @@ mod tests {
.unwrap(); .unwrap();
assert_eq!(cx.window_ids().len(), 2); assert_eq!(cx.window_ids().len(), 2);
// Replace existing windows
let window_id = cx.window_ids()[0];
cx.update(|cx| { cx.update(|cx| {
open_paths( open_paths(
&[PathBuf::from("/root/c"), PathBuf::from("/root/d")], &[PathBuf::from("/root/c"), PathBuf::from("/root/d")],
&app_state, &app_state,
Some(window_id),
cx, cx,
) )
}) })
.await .await
.unwrap(); .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] #[gpui::test]
@ -756,7 +778,7 @@ mod tests {
.insert_tree("/root", json!({"a": "hey"})) .insert_tree("/root", json!({"a": "hey"}))
.await; .await;
cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, cx)) cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, None, cx))
.await .await
.unwrap(); .unwrap();
assert_eq!(cx.window_ids().len(), 1); assert_eq!(cx.window_ids().len(), 1);
@ -799,7 +821,7 @@ mod tests {
assert!(!cx.is_window_edited(workspace.window_id())); assert!(!cx.is_window_edited(workspace.window_id()));
// Opening the buffer again doesn't impact the window's edited state. // Opening the buffer again doesn't impact the window's edited state.
cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, cx)) cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, None, cx))
.await .await
.unwrap(); .unwrap();
let editor = workspace.read_with(cx, |workspace, cx| { let editor = workspace.read_with(cx, |workspace, cx| {

View file

@ -56,6 +56,8 @@ async function main() {
headers: {"Content-Type": "application/json"}, headers: {"Content-Type": "application/json"},
body: JSON.stringify(body) body: JSON.stringify(body)
}) })
process.exit(1)
} }
function randomU64() { function randomU64() {

View file

@ -44,9 +44,7 @@ export default function editor(colorScheme: ColorScheme) {
activeLineBackground: withOpacity(background(layer, "on"), 0.75), activeLineBackground: withOpacity(background(layer, "on"), 0.75),
highlightedLineBackground: background(layer, "on"), highlightedLineBackground: background(layer, "on"),
// Inline autocomplete suggestions, Co-pilot suggestions, etc. // Inline autocomplete suggestions, Co-pilot suggestions, etc.
suggestion: { suggestion: syntax.predictive,
color: syntax.predictive.color,
},
codeActions: { codeActions: {
indicator: { indicator: {
color: foreground(layer, "variant"), color: foreground(layer, "variant"),

View file

@ -1,6 +1,7 @@
import deepmerge from "deepmerge" import deepmerge from "deepmerge"
import { FontWeight, fontWeights } from "../../common" import { FontWeight, fontWeights } from "../../common"
import { ColorScheme } from "./colorScheme" import { ColorScheme } from "./colorScheme"
import chroma from "chroma-js"
export interface SyntaxHighlightStyle { export interface SyntaxHighlightStyle {
color: string color: string
@ -128,6 +129,8 @@ function buildDefaultSyntax(colorScheme: ColorScheme): Syntax {
[key: string]: Omit<SyntaxHighlightStyle, "color"> [key: string]: Omit<SyntaxHighlightStyle, "color">
} = {} } = {}
const light = colorScheme.isLight
// then spread the default to each style // then spread the default to each style
for (const key of Object.keys({} as Syntax)) { for (const key of Object.keys({} as Syntax)) {
syntax[key as keyof 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 = { const color = {
primary: colorScheme.ramps.neutral(1).hex(), primary: colorScheme.ramps.neutral(1).hex(),
comment: colorScheme.ramps.neutral(0.71).hex(), comment: colorScheme.ramps.neutral(0.71).hex(),
punctuation: colorScheme.ramps.neutral(0.86).hex(), punctuation: colorScheme.ramps.neutral(0.86).hex(),
predictive: colorScheme.ramps.neutral(0.57).hex(), predictive: predictive,
emphasis: colorScheme.ramps.blue(0.5).hex(), emphasis: colorScheme.ramps.blue(0.5).hex(),
string: colorScheme.ramps.orange(0.5).hex(), string: colorScheme.ramps.orange(0.5).hex(),
function: colorScheme.ramps.yellow(0.5).hex(), function: colorScheme.ramps.yellow(0.5).hex(),
@ -169,6 +181,7 @@ function buildDefaultSyntax(colorScheme: ColorScheme): Syntax {
}, },
predictive: { predictive: {
color: color.predictive, color: color.predictive,
italic: true,
}, },
emphasis: { emphasis: {
color: color.emphasis, color: color.emphasis,