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

File diff suppressed because it is too large Load diff

View file

@ -1,325 +1,325 @@
[
{
"context": "Editor && VimControl && !VimWaiting",
"bindings": {
"g": [
"vim::PushOperator",
{
"Namespace": "G"
}
],
"i": [
"vim::PushOperator",
{
"Object": {
"around": false
}
}
],
"a": [
"vim::PushOperator",
{
"Object": {
"around": true
}
}
],
"h": "vim::Left",
"backspace": "vim::Backspace",
"j": "vim::Down",
"enter": "vim::NextLineStart",
"k": "vim::Up",
"l": "vim::Right",
"$": "vim::EndOfLine",
"shift-g": "vim::EndOfDocument",
"w": "vim::NextWordStart",
"shift-w": [
"vim::NextWordStart",
{
"ignorePunctuation": true
}
],
"e": "vim::NextWordEnd",
"shift-e": [
"vim::NextWordEnd",
{
"ignorePunctuation": true
}
],
"b": "vim::PreviousWordStart",
"shift-b": [
"vim::PreviousWordStart",
{
"ignorePunctuation": true
}
],
"%": "vim::Matching",
"ctrl-y": [
"vim::Scroll",
"LineUp"
],
"f": [
"vim::PushOperator",
{
"FindForward": {
"before": false
}
}
],
"t": [
"vim::PushOperator",
{
"FindForward": {
"before": true
}
}
],
"shift-f": [
"vim::PushOperator",
{
"FindBackward": {
"after": false
}
}
],
"shift-t": [
"vim::PushOperator",
{
"FindBackward": {
"after": true
}
}
],
"escape": "editor::Cancel",
"0": "vim::StartOfLine", // When no number operator present, use start of line motion
"1": [
"vim::Number",
1
],
"2": [
"vim::Number",
2
],
"3": [
"vim::Number",
3
],
"4": [
"vim::Number",
4
],
"5": [
"vim::Number",
5
],
"6": [
"vim::Number",
6
],
"7": [
"vim::Number",
7
],
"8": [
"vim::Number",
8
],
"9": [
"vim::Number",
9
]
{
"context": "Editor && VimControl && !VimWaiting",
"bindings": {
"g": [
"vim::PushOperator",
{
"Namespace": "G"
}
},
{
"context": "Editor && vim_mode == normal && vim_operator == none && !VimWaiting",
"bindings": {
"c": [
"vim::PushOperator",
"Change"
],
"shift-c": "vim::ChangeToEndOfLine",
"d": [
"vim::PushOperator",
"Delete"
],
"shift-d": "vim::DeleteToEndOfLine",
"y": [
"vim::PushOperator",
"Yank"
],
"z": [
"vim::PushOperator",
{
"Namespace": "Z"
}
],
"i": [
"vim::SwitchMode",
"Insert"
],
"shift-i": "vim::InsertFirstNonWhitespace",
"a": "vim::InsertAfter",
"shift-a": "vim::InsertEndOfLine",
"x": "vim::DeleteRight",
"shift-x": "vim::DeleteLeft",
"^": "vim::FirstNonWhitespace",
"o": "vim::InsertLineBelow",
"shift-o": "vim::InsertLineAbove",
"v": [
"vim::SwitchMode",
{
"Visual": {
"line": false
}
}
],
"shift-v": [
"vim::SwitchMode",
{
"Visual": {
"line": true
}
}
],
"p": "vim::Paste",
"u": "editor::Undo",
"ctrl-r": "editor::Redo",
"ctrl-o": "pane::GoBack",
"/": [
"buffer_search::Deploy",
{
"focus": true
}
],
"ctrl-f": [
"vim::Scroll",
"PageDown"
],
"ctrl-b": [
"vim::Scroll",
"PageUp"
],
"ctrl-d": [
"vim::Scroll",
"HalfPageDown"
],
"ctrl-u": [
"vim::Scroll",
"HalfPageUp"
],
"ctrl-e": [
"vim::Scroll",
"LineDown"
],
"r": [
"vim::PushOperator",
"Replace"
]
],
"i": [
"vim::PushOperator",
{
"Object": {
"around": false
}
}
},
{
"context": "Editor && vim_operator == n",
"bindings": {
"0": [
"vim::Number",
0
]
],
"a": [
"vim::PushOperator",
{
"Object": {
"around": true
}
}
},
{
"context": "Editor && vim_operator == g",
"bindings": {
"g": "vim::StartOfDocument",
"h": "editor::Hover",
"escape": [
"vim::SwitchMode",
"Normal"
],
"d": "editor::GoToDefinition"
],
"h": "vim::Left",
"backspace": "vim::Backspace",
"j": "vim::Down",
"enter": "vim::NextLineStart",
"k": "vim::Up",
"l": "vim::Right",
"$": "vim::EndOfLine",
"shift-g": "vim::EndOfDocument",
"w": "vim::NextWordStart",
"shift-w": [
"vim::NextWordStart",
{
"ignorePunctuation": true
}
},
{
"context": "Editor && vim_operator == c",
"bindings": {
"c": "vim::CurrentLine"
],
"e": "vim::NextWordEnd",
"shift-e": [
"vim::NextWordEnd",
{
"ignorePunctuation": true
}
},
{
"context": "Editor && vim_operator == d",
"bindings": {
"d": "vim::CurrentLine"
],
"b": "vim::PreviousWordStart",
"shift-b": [
"vim::PreviousWordStart",
{
"ignorePunctuation": true
}
},
{
"context": "Editor && vim_operator == y",
"bindings": {
"y": "vim::CurrentLine"
],
"%": "vim::Matching",
"ctrl-y": [
"vim::Scroll",
"LineUp"
],
"f": [
"vim::PushOperator",
{
"FindForward": {
"before": false
}
}
},
{
"context": "Editor && vim_operator == z",
"bindings": {
"t": "editor::ScrollCursorTop",
"z": "editor::ScrollCursorCenter",
"b": "editor::ScrollCursorBottom",
"escape": [
"vim::SwitchMode",
"Normal"
]
],
"t": [
"vim::PushOperator",
{
"FindForward": {
"before": true
}
}
},
{
"context": "Editor && VimObject",
"bindings": {
"w": "vim::Word",
"shift-w": [
"vim::Word",
{
"ignorePunctuation": true
}
],
"s": "vim::Sentence",
"'": "vim::Quotes",
"`": "vim::BackQuotes",
"\"": "vim::DoubleQuotes",
"(": "vim::Parentheses",
")": "vim::Parentheses",
"[": "vim::SquareBrackets",
"]": "vim::SquareBrackets",
"{": "vim::CurlyBrackets",
"}": "vim::CurlyBrackets",
"<": "vim::AngleBrackets",
">": "vim::AngleBrackets"
],
"shift-f": [
"vim::PushOperator",
{
"FindBackward": {
"after": false
}
}
},
{
"context": "Editor && vim_mode == visual && !VimWaiting",
"bindings": {
"u": "editor::Undo",
"c": "vim::VisualChange",
"d": "vim::VisualDelete",
"x": "vim::VisualDelete",
"y": "vim::VisualYank",
"p": "vim::VisualPaste",
"r": [
"vim::PushOperator",
"Replace"
]
}
},
{
"context": "Editor && vim_mode == insert",
"bindings": {
"escape": "vim::NormalBefore",
"ctrl-c": "vim::NormalBefore"
}
},
{
"context": "Editor && VimWaiting",
"bindings": {
"tab": "vim::Tab",
"enter": "vim::Enter",
"escape": "editor::Cancel"
],
"shift-t": [
"vim::PushOperator",
{
"FindBackward": {
"after": true
}
}
],
"escape": "editor::Cancel",
"0": "vim::StartOfLine", // When no number operator present, use start of line motion
"1": [
"vim::Number",
1
],
"2": [
"vim::Number",
2
],
"3": [
"vim::Number",
3
],
"4": [
"vim::Number",
4
],
"5": [
"vim::Number",
5
],
"6": [
"vim::Number",
6
],
"7": [
"vim::Number",
7
],
"8": [
"vim::Number",
8
],
"9": [
"vim::Number",
9
]
}
]
},
{
"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
"theme": "One Dark",
// The name of a font to use for rendering text in the editor
"buffer_font_family": "Zed Mono",
// The OpenType features to enable for text in the editor.
"buffer_font_features": {
// Disable ligatures:
// "calt": false
},
// The default font size for text in the editor
"buffer_font_size": 15,
// The factor to grow the active pane by. Defaults to 1.0
// which gives the same size as all other panes.
"active_pane_magnification": 1.0,
// Enable / disable copilot integration.
"enable_copilot_integration": true,
// Controls whether copilot provides suggestion immediately
// or waits for a `copilot::Toggle`
"copilot": "on",
// Whether to enable vim modes and key bindings
"vim_mode": false,
// Whether to show the informational hover box when moving the mouse
// over symbols in the editor.
"hover_popover_enabled": true,
// Whether to confirm before quitting Zed.
"confirm_quit": false,
// Whether the cursor blinks in the editor.
"cursor_blink": true,
// Whether to pop the completions menu while typing in an editor without
// explicitly requesting it.
"show_completions_on_input": true,
// Whether the screen sharing icon is shown in the os status bar.
"show_call_status_icon": true,
// Whether to use language servers to provide code intelligence.
"enable_language_server": true,
// When to automatically save edited buffers. This setting can
// take four values.
//
// 1. Never automatically save:
// "autosave": "off",
// 2. Save when changing focus away from the Zed window:
// "autosave": "on_window_change",
// 3. Save when changing focus away from a specific buffer:
// "autosave": "on_focus_change",
// 4. Save when idle for a certain amount of time:
// "autosave": { "after_delay": {"milliseconds": 500} },
"autosave": "off",
// Where to place the dock by default. This setting can take three
// values:
//
// 1. Position the dock attached to the bottom of the workspace
// "default_dock_anchor": "bottom"
// 2. Position the dock to the right of the workspace like a side panel
// "default_dock_anchor": "right"
// 3. Position the dock full screen over the entire workspace"
// "default_dock_anchor": "expanded"
"default_dock_anchor": "bottom",
// Whether or not to remove any trailing whitespace from lines of a buffer
// before saving it.
"remove_trailing_whitespace_on_save": true,
// Whether or not to ensure there's a single newline at the end of a buffer
// when saving it.
"ensure_final_newline_on_save": true,
// Whether or not to perform a buffer format before saving
"format_on_save": "on",
// How to perform a buffer format. This setting can take two values:
//
// 1. Format code using the current language server:
// "format_on_save": "language_server"
// 2. Format code using an external command:
// "format_on_save": {
// "external": {
// "command": "prettier",
// "arguments": ["--stdin-filepath", "{buffer_path}"]
// }
// The name of the Zed theme to use for the UI
"theme": "One Dark",
// Features that can be globally enabled or disabled
"features": {
// Show Copilot icon in status bar
"copilot": true
},
// The name of a font to use for rendering text in the editor
"buffer_font_family": "Zed Mono",
// The OpenType features to enable for text in the editor.
"buffer_font_features": {
// Disable ligatures:
// "calt": false
},
// The default font size for text in the editor
"buffer_font_size": 15,
// The factor to grow the active pane by. Defaults to 1.0
// which gives the same size as all other panes.
"active_pane_magnification": 1.0,
// Whether to enable vim modes and key bindings
"vim_mode": false,
// Whether to show the informational hover box when moving the mouse
// over symbols in the editor.
"hover_popover_enabled": true,
// Whether to confirm before quitting Zed.
"confirm_quit": false,
// Whether the cursor blinks in the editor.
"cursor_blink": true,
// Whether to pop the completions menu while typing in an editor without
// explicitly requesting it.
"show_completions_on_input": true,
// Controls whether copilot provides suggestion immediately
// or waits for a `copilot::Toggle`
"show_copilot_suggestions": true,
// Whether the screen sharing icon is shown in the os status bar.
"show_call_status_icon": true,
// Whether to use language servers to provide code intelligence.
"enable_language_server": true,
// When to automatically save edited buffers. This setting can
// take four values.
//
// 1. Never automatically save:
// "autosave": "off",
// 2. Save when changing focus away from the Zed window:
// "autosave": "on_window_change",
// 3. Save when changing focus away from a specific buffer:
// "autosave": "on_focus_change",
// 4. Save when idle for a certain amount of time:
// "autosave": { "after_delay": {"milliseconds": 500} },
"autosave": "off",
// Where to place the dock by default. This setting can take three
// values:
//
// 1. Position the dock attached to the bottom of the workspace
// "default_dock_anchor": "bottom"
// 2. Position the dock to the right of the workspace like a side panel
// "default_dock_anchor": "right"
// 3. Position the dock full screen over the entire workspace"
// "default_dock_anchor": "expanded"
"default_dock_anchor": "bottom",
// Whether or not to remove any trailing whitespace from lines of a buffer
// before saving it.
"remove_trailing_whitespace_on_save": true,
// Whether or not to ensure there's a single newline at the end of a buffer
// when saving it.
"ensure_final_newline_on_save": true,
// Whether or not to perform a buffer format before saving
"format_on_save": "on",
// How to perform a buffer format. This setting can take two values:
//
// 1. Format code using the current language server:
// "format_on_save": "language_server"
// 2. Format code using an external command:
// "format_on_save": {
// "external": {
// "command": "prettier",
// "arguments": ["--stdin-filepath", "{buffer_path}"]
// }
// }
"formatter": "language_server",
// How to soft-wrap long lines of text. This setting can take
// three values:
//
// 1. Do not soft wrap.
// "soft_wrap": "none",
// 2. Soft wrap lines that overflow the editor:
// "soft_wrap": "editor_width",
// 3. Soft wrap lines at the preferred line length
// "soft_wrap": "preferred_line_length",
"soft_wrap": "none",
// The column at which to soft-wrap lines, for buffers where soft-wrap
// is enabled.
"preferred_line_length": 80,
// Whether to indent lines using tab characters, as opposed to multiple
// spaces.
"hard_tabs": false,
// How many columns a tab should occupy.
"tab_size": 4,
// Control what info is collected by Zed.
"telemetry": {
// Send debug info like crash reports.
"diagnostics": true,
// Send anonymized usage data like what languages you're using Zed with.
"metrics": true
},
// Automatically update Zed
"auto_update": true,
// Git gutter behavior configuration.
"git": {
// Control whether the git gutter is shown. May take 2 values:
// 1. Show the gutter
// "git_gutter": "tracked_files"
// 2. Hide the gutter
// "git_gutter": "hide"
"git_gutter": "tracked_files"
},
// Settings specific to journaling
"journal": {
// The path of the directory where journal entries are stored
"path": "~",
// What format to display the hours in
// May take 2 values:
// 1. hour12
// 2. hour24
"hour_format": "hour12"
},
// Settings specific to the terminal
"terminal": {
// What shell to use when opening a terminal. May take 3 values:
// 1. Use the system's default terminal configuration in /etc/passwd
// "shell": "system"
// 2. A program:
// "shell": {
// "program": "sh"
// }
// 3. A program with arguments:
// "shell": {
// "with_arguments": {
// "program": "/bin/bash",
// "arguments": ["--login"]
// }
// }
"formatter": "language_server",
// How to soft-wrap long lines of text. This setting can take
// three values:
"shell": "system",
// What working directory to use when launching the terminal.
// May take 4 values:
// 1. Use the current file's project directory. Will Fallback to the
// first project directory strategy if unsuccessful
// "working_directory": "current_project_directory"
// 2. Use the first project in this workspace's directory
// "working_directory": "first_project_directory"
// 3. Always use this platform's home directory (if we can find it)
// "working_directory": "always_home"
// 4. Always use a specific directory. This value will be shell expanded.
// If this path is not a valid directory the terminal will default to
// this platform's home directory (if we can find it)
// "working_directory": {
// "always": {
// "directory": "~/zed/projects/"
// }
// }
//
// 1. Do not soft wrap.
// "soft_wrap": "none",
// 2. Soft wrap lines that overflow the editor:
// "soft_wrap": "editor_width",
// 3. Soft wrap lines at the preferred line length
// "soft_wrap": "preferred_line_length",
"soft_wrap": "none",
// The column at which to soft-wrap lines, for buffers where soft-wrap
// is enabled.
"preferred_line_length": 80,
// Whether to indent lines using tab characters, as opposed to multiple
// spaces.
"hard_tabs": false,
// How many columns a tab should occupy.
"tab_size": 4,
// Control what info is collected by Zed.
"telemetry": {
// Send debug info like crash reports.
"diagnostics": true,
// Send anonymized usage data like what languages you're using Zed with.
"metrics": true
},
// Automatically update Zed
"auto_update": true,
// Git gutter behavior configuration.
"git": {
// Control whether the git gutter is shown. May take 2 values:
// 1. Show the gutter
// "git_gutter": "tracked_files"
// 2. Hide the gutter
// "git_gutter": "hide"
"git_gutter": "tracked_files"
},
// Settings specific to journaling
"journal": {
// The path of the directory where journal entries are stored
"path": "~",
// What format to display the hours in
// May take 2 values:
// 1. hour12
// 2. hour24
"hour_format": "hour12"
},
// Settings specific to the terminal
"terminal": {
// What shell to use when opening a terminal. May take 3 values:
// 1. Use the system's default terminal configuration in /etc/passwd
// "shell": "system"
// 2. A program:
// "shell": {
// "program": "sh"
// }
// 3. A program with arguments:
// "shell": {
// "with_arguments": {
// "program": "/bin/bash",
// "arguments": ["--login"]
// }
// }
"shell": "system",
// What working directory to use when launching the terminal.
// May take 4 values:
// 1. Use the current file's project directory. Will Fallback to the
// first project directory strategy if unsuccessful
// "working_directory": "current_project_directory"
// 2. Use the first project in this workspace's directory
// "working_directory": "first_project_directory"
// 3. Always use this platform's home directory (if we can find it)
// "working_directory": "always_home"
// 4. Always use a specific directory. This value will be shell expanded.
// If this path is not a valid directory the terminal will default to
// this platform's home directory (if we can find it)
// "working_directory": {
// "always": {
// "directory": "~/zed/projects/"
// }
// }
//
//
"working_directory": "current_project_directory",
// Set the cursor blinking behavior in the terminal.
// May take 4 values:
// 1. Never blink the cursor, ignoring the terminal mode
// "blinking": "off",
// 2. Default the cursor blink to off, but allow the terminal to
// set blinking
// "blinking": "terminal_controlled",
// 3. Always blink the cursor, ignoring the terminal mode
// "blinking": "on",
"blinking": "terminal_controlled",
// Set whether Alternate Scroll mode (code: ?1007) is active by default.
// Alternate Scroll mode converts mouse scroll events into up / down key
// presses when in the alternate screen (e.g. when running applications
// like vim or less). The terminal can still set and unset this mode.
// May take 2 values:
// 1. Default alternate scroll mode to on
// "alternate_scroll": "on",
// 2. Default alternate scroll mode to off
// "alternate_scroll": "off",
"alternate_scroll": "off",
// Set whether the option key behaves as the meta key.
// May take 2 values:
// 1. Rely on default platform handling of option key, on macOS
// this means generating certain unicode characters
// "option_to_meta": false,
// 2. Make the option keys behave as a 'meta' key, e.g. for emacs
// "option_to_meta": true,
"option_as_meta": false,
// Whether or not selecting text in the terminal will automatically
// copy to the system clipboard.
"copy_on_select": false,
// Any key-value pairs added to this list will be added to the terminal's
// enviroment. Use `:` to seperate multiple values.
"env": {
// "KEY": "value1:value2"
}
// Set the terminal's font size. If this option is not included,
// the terminal will default to matching the buffer's font size.
// "font_size": "15"
// Set the terminal's font family. If this option is not included,
// the terminal will default to matching the buffer's font family.
// "font_family": "Zed Mono"
},
// Different settings for specific languages.
"languages": {
"Plain Text": {
"soft_wrap": "preferred_line_length"
},
"Elixir": {
"tab_size": 2
},
"Go": {
"tab_size": 4,
"hard_tabs": true
},
"Markdown": {
"soft_wrap": "preferred_line_length"
},
"JavaScript": {
"tab_size": 2
},
"TypeScript": {
"tab_size": 2
},
"TSX": {
"tab_size": 2
},
"YAML": {
"tab_size": 2
}
},
// LSP Specific settings.
"lsp": {
// Specify the LSP name as a key here.
// As of 8/10/22, supported LSPs are:
// pyright
// gopls
// rust-analyzer
// typescript-language-server
// vscode-json-languageserver
// "rust-analyzer": {
// //These initialization options are merged into Zed's defaults
// "initialization_options": {
// "checkOnSave": {
// "command": "clippy"
// }
// }
// }
//
"working_directory": "current_project_directory",
// Set the cursor blinking behavior in the terminal.
// May take 4 values:
// 1. Never blink the cursor, ignoring the terminal mode
// "blinking": "off",
// 2. Default the cursor blink to off, but allow the terminal to
// set blinking
// "blinking": "terminal_controlled",
// 3. Always blink the cursor, ignoring the terminal mode
// "blinking": "on",
"blinking": "terminal_controlled",
// Set whether Alternate Scroll mode (code: ?1007) is active by default.
// Alternate Scroll mode converts mouse scroll events into up / down key
// presses when in the alternate screen (e.g. when running applications
// like vim or less). The terminal can still set and unset this mode.
// May take 2 values:
// 1. Default alternate scroll mode to on
// "alternate_scroll": "on",
// 2. Default alternate scroll mode to off
// "alternate_scroll": "off",
"alternate_scroll": "off",
// Set whether the option key behaves as the meta key.
// May take 2 values:
// 1. Rely on default platform handling of option key, on macOS
// this means generating certain unicode characters
// "option_to_meta": false,
// 2. Make the option keys behave as a 'meta' key, e.g. for emacs
// "option_to_meta": true,
"option_as_meta": false,
// Whether or not selecting text in the terminal will automatically
// copy to the system clipboard.
"copy_on_select": false,
// Any key-value pairs added to this list will be added to the terminal's
// enviroment. Use `:` to seperate multiple values.
"env": {
// "KEY": "value1:value2"
}
// Set the terminal's font size. If this option is not included,
// the terminal will default to matching the buffer's font size.
// "font_size": "15"
// Set the terminal's font family. If this option is not included,
// the terminal will default to matching the buffer's font family.
// "font_family": "Zed Mono"
},
// Different settings for specific languages.
"languages": {
"Plain Text": {
"soft_wrap": "preferred_line_length"
},
"Elixir": {
"tab_size": 2
},
"Go": {
"tab_size": 4,
"hard_tabs": true
},
"Markdown": {
"soft_wrap": "preferred_line_length"
},
"JavaScript": {
"tab_size": 2
},
"TypeScript": {
"tab_size": 2
},
"TSX": {
"tab_size": 2
},
"YAML": {
"tab_size": 2
},
"JSON": {
"tab_size": 2
}
},
// LSP Specific settings.
"lsp": {
// Specify the LSP name as a key here.
// As of 8/10/22, supported LSPs are:
// pyright
// gopls
// rust-analyzer
// typescript-language-server
// vscode-json-languageserver
// "rust-analyzer": {
// //These initialization options are merged into Zed's defaults
// "initialization_options": {
// "checkOnSave": {
// "command": "clippy"
// }
// }
// }
}
}

View file

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

View file

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

View file

@ -6,8 +6,9 @@ use call::{room, ActiveCall, ParticipantLocation, Room};
use client::{User, RECEIVE_TIMEOUT};
use collections::HashSet;
use editor::{
ConfirmCodeAction, ConfirmCompletion, ConfirmRename, Editor, ExcerptRange, MultiBuffer, Redo,
Rename, ToOffset, ToggleCodeActions, Undo,
test::editor_test_context::EditorTestContext, ConfirmCodeAction, ConfirmCompletion,
ConfirmRename, Editor, ExcerptRange, MultiBuffer, Redo, Rename, ToOffset, ToggleCodeActions,
Undo,
};
use fs::{FakeFs, Fs as _, LineEnding, RemoveOptions};
use futures::StreamExt as _;
@ -15,6 +16,7 @@ use gpui::{
executor::Deterministic, geometry::vector::vec2f, test::EmptyView, ModelHandle, TestAppContext,
ViewHandle,
};
use indoc::indoc;
use language::{
tree_sitter_rust, Anchor, Diagnostic, DiagnosticEntry, FakeLspAdapter, Language,
LanguageConfig, OffsetRangeExt, Point, Rope,
@ -3040,6 +3042,104 @@ async fn test_editing_while_guest_opens_buffer(
buffer_b.read_with(cx_b, |buf, _| assert_eq!(buf.text(), text));
}
#[gpui::test]
async fn test_newline_above_or_below_does_not_move_guest_cursor(
deterministic: Arc<Deterministic>,
cx_a: &mut TestAppContext,
cx_b: &mut TestAppContext,
) {
deterministic.forbid_parking();
let mut server = TestServer::start(&deterministic).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
server
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
.await;
let active_call_a = cx_a.read(ActiveCall::global);
client_a
.fs
.insert_tree("/dir", json!({ "a.txt": "Some text\n" }))
.await;
let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
let project_id = active_call_a
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
.await
.unwrap();
let project_b = client_b.build_remote_project(project_id, cx_b).await;
// Open a buffer as client A
let buffer_a = project_a
.update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
.await
.unwrap();
let (_, window_a) = cx_a.add_window(|_| EmptyView);
let editor_a = cx_a.add_view(&window_a, |cx| {
Editor::for_buffer(buffer_a, Some(project_a), cx)
});
let mut editor_cx_a = EditorTestContext {
cx: cx_a,
window_id: window_a.id(),
editor: editor_a,
};
// Open a buffer as client B
let buffer_b = project_b
.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
.await
.unwrap();
let (_, window_b) = cx_b.add_window(|_| EmptyView);
let editor_b = cx_b.add_view(&window_b, |cx| {
Editor::for_buffer(buffer_b, Some(project_b), cx)
});
let mut editor_cx_b = EditorTestContext {
cx: cx_b,
window_id: window_b.id(),
editor: editor_b,
};
// Test newline above
editor_cx_a.set_selections_state(indoc! {"
Some textˇ
"});
editor_cx_b.set_selections_state(indoc! {"
Some textˇ
"});
editor_cx_a.update_editor(|editor, cx| editor.newline_above(&editor::NewlineAbove, cx));
deterministic.run_until_parked();
editor_cx_a.assert_editor_state(indoc! {"
ˇ
Some text
"});
editor_cx_b.assert_editor_state(indoc! {"
Some textˇ
"});
// Test newline below
editor_cx_a.set_selections_state(indoc! {"
Some textˇ
"});
editor_cx_b.set_selections_state(indoc! {"
Some textˇ
"});
editor_cx_a.update_editor(|editor, cx| editor.newline_below(&editor::NewlineBelow, cx));
deterministic.run_until_parked();
editor_cx_a.assert_editor_state(indoc! {"
Some text
ˇ
"});
editor_cx_b.assert_editor_state(indoc! {"
Some textˇ
"});
}
#[gpui::test(iterations = 10)]
async fn test_leaving_worktree_while_opening_buffer(
deterministic: Arc<Deterministic>,
@ -5860,10 +5960,17 @@ async fn test_basic_following(
// Client A updates their selections in those editors
editor_a1.update(cx_a, |editor, cx| {
editor.change_selections(None, cx, |s| s.select_ranges([0..1]))
editor.handle_input("a", cx);
editor.handle_input("b", cx);
editor.handle_input("c", cx);
editor.select_left(&Default::default(), cx);
assert_eq!(editor.selections.ranges(cx), vec![3..2]);
});
editor_a2.update(cx_a, |editor, cx| {
editor.change_selections(None, cx, |s| s.select_ranges([2..3]))
editor.handle_input("d", cx);
editor.handle_input("e", cx);
editor.select_left(&Default::default(), cx);
assert_eq!(editor.selections.ranges(cx), vec![2..1]);
});
// When client B starts following client A, all visible view states are replicated to client B.
@ -5876,6 +5983,27 @@ async fn test_basic_following(
.await
.unwrap();
cx_c.foreground().run_until_parked();
let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| {
workspace
.active_item(cx)
.unwrap()
.downcast::<Editor>()
.unwrap()
});
assert_eq!(
cx_b.read(|cx| editor_b2.project_path(cx)),
Some((worktree_id, "2.txt").into())
);
assert_eq!(
editor_b2.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
vec![2..1]
);
assert_eq!(
editor_b1.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
vec![3..2]
);
cx_c.foreground().run_until_parked();
let active_call_c = cx_c.read(ActiveCall::global);
let project_c = client_c.build_remote_project(project_id, cx_c).await;
@ -6031,26 +6159,6 @@ async fn test_basic_following(
});
}
let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| {
workspace
.active_item(cx)
.unwrap()
.downcast::<Editor>()
.unwrap()
});
assert_eq!(
cx_b.read(|cx| editor_b2.project_path(cx)),
Some((worktree_id, "2.txt").into())
);
assert_eq!(
editor_b2.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
vec![2..3]
);
assert_eq!(
editor_b1.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
vec![0..1]
);
// When client A activates a different editor, client B does so as well.
workspace_a.update(cx_a, |workspace, cx| {
workspace.activate_item(&editor_a1, cx)

View file

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

View file

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

View file

@ -99,14 +99,11 @@ pub struct GetCompletionsParams {
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GetCompletionsDocument {
pub source: String,
pub tab_size: u32,
pub indent_size: u32,
pub insert_spaces: bool,
pub uri: lsp::Url,
pub path: String,
pub relative_path: String,
pub language_id: String,
pub position: lsp::Position,
pub version: usize,
}
@ -169,3 +166,60 @@ impl lsp::notification::Notification for StatusNotification {
type Params = StatusNotificationParams;
const METHOD: &'static str = "statusNotification";
}
pub enum SetEditorInfo {}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SetEditorInfoParams {
pub editor_info: EditorInfo,
pub editor_plugin_info: EditorPluginInfo,
}
impl lsp::request::Request for SetEditorInfo {
type Params = SetEditorInfoParams;
type Result = String;
const METHOD: &'static str = "setEditorInfo";
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EditorInfo {
pub name: String,
pub version: String,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EditorPluginInfo {
pub name: String,
pub version: String,
}
pub enum NotifyAccepted {}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct NotifyAcceptedParams {
pub uuid: String,
}
impl lsp::request::Request for NotifyAccepted {
type Params = NotifyAcceptedParams;
type Result = String;
const METHOD: &'static str = "notifyAccepted";
}
pub enum NotifyRejected {}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct NotifyRejectedParams {
pub uuids: Vec<String>,
}
impl lsp::request::Request for NotifyRejected {
type Params = NotifyRejectedParams;
type Result = String;
const METHOD: &'static str = "notifyRejected";
}

View file

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

View file

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

View file

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

View file

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

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]
async fn test_newline_below(cx: &mut gpui::TestAppContext) {
let mut cx = EditorTestContext::new(cx);

View file

@ -3,12 +3,12 @@ use crate::{
movement::surrounding_word, persistence::DB, scroll::ScrollAnchor, Anchor, Autoscroll, Editor,
Event, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, NavigationData, ToPoint as _,
};
use anyhow::{anyhow, Context, Result};
use anyhow::{Context, Result};
use collections::HashSet;
use futures::future::try_join_all;
use gpui::{
elements::*, geometry::vector::vec2f, AppContext, Entity, ModelHandle, Subscription, Task,
View, ViewContext, ViewHandle, WeakViewHandle,
elements::*, geometry::vector::vec2f, AppContext, AsyncAppContext, Entity, ModelHandle,
Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle,
};
use language::{
proto::serialize_anchor as serialize_text_anchor, Bias, Buffer, OffsetRangeExt, Point,
@ -72,11 +72,11 @@ impl FollowableItem for Editor {
let editor = pane.read_with(&cx, |pane, cx| {
let mut editors = pane.items_of_type::<Self>();
editors.find(|editor| {
editor.remote_id(&client, cx) == Some(remote_id)
|| state.singleton
&& buffers.len() == 1
&& editor.read(cx).buffer.read(cx).as_singleton().as_ref()
== Some(&buffers[0])
let ids_match = editor.remote_id(&client, cx) == Some(remote_id);
let singleton_buffer_matches = state.singleton
&& buffers.first()
== editor.read(cx).buffer.read(cx).as_singleton().as_ref();
ids_match || singleton_buffer_matches
})
});
@ -117,46 +117,29 @@ impl FollowableItem for Editor {
multibuffer
});
cx.add_view(|cx| Editor::for_multibuffer(multibuffer, Some(project), cx))
cx.add_view(|cx| {
let mut editor =
Editor::for_multibuffer(multibuffer, Some(project.clone()), cx);
editor.remote_id = Some(remote_id);
editor
})
})?
};
editor.update(&mut cx, |editor, cx| {
editor.remote_id = Some(remote_id);
let buffer = editor.buffer.read(cx).read(cx);
let selections = state
.selections
.into_iter()
.map(|selection| {
deserialize_selection(&buffer, selection)
.ok_or_else(|| anyhow!("invalid selection"))
})
.collect::<Result<Vec<_>>>()?;
let pending_selection = state
.pending_selection
.map(|selection| deserialize_selection(&buffer, selection))
.flatten();
let scroll_top_anchor = state
.scroll_top_anchor
.and_then(|anchor| deserialize_anchor(&buffer, anchor));
drop(buffer);
if !selections.is_empty() || pending_selection.is_some() {
editor.set_selections_from_remote(selections, pending_selection, cx);
}
if let Some(scroll_top_anchor) = scroll_top_anchor {
editor.set_scroll_anchor_remote(
ScrollAnchor {
top_anchor: scroll_top_anchor,
offset: vec2f(state.scroll_x, state.scroll_y),
},
cx,
);
}
anyhow::Ok(())
})??;
update_editor_from_message(
editor.clone(),
project,
proto::update_view::Editor {
selections: state.selections,
pending_selection: state.pending_selection,
scroll_top_anchor: state.scroll_top_anchor,
scroll_x: state.scroll_x,
scroll_y: state.scroll_y,
..Default::default()
},
&mut cx,
)
.await?;
Ok(editor)
}))
@ -301,96 +284,9 @@ impl FollowableItem for Editor {
cx: &mut ViewContext<Self>,
) -> Task<Result<()>> {
let update_view::Variant::Editor(message) = message;
let multibuffer = self.buffer.read(cx);
let multibuffer = multibuffer.read(cx);
let 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();
cx.spawn(|this, mut cx| async move {
let _buffers = try_join_all(buffers).await?;
this.update(&mut cx, |this, cx| {
this.buffer.update(cx, |multibuffer, cx| {
let mut insertions = message.inserted_excerpts.into_iter().peekable();
while let Some(insertion) = insertions.next() {
let Some(excerpt) = insertion.excerpt else { continue };
let Some(previous_excerpt_id) = insertion.previous_excerpt_id else { continue };
let buffer_id = excerpt.buffer_id;
let Some(buffer) = project.read(cx).buffer_for_id(buffer_id, cx) else { continue };
let adjacent_excerpts = iter::from_fn(|| {
let insertion = insertions.peek()?;
if insertion.previous_excerpt_id.is_none()
&& insertion.excerpt.as_ref()?.buffer_id == buffer_id
{
insertions.next()?.excerpt
} else {
None
}
});
multibuffer.insert_excerpts_with_ids_after(
ExcerptId::from_proto(previous_excerpt_id),
buffer,
[excerpt]
.into_iter()
.chain(adjacent_excerpts)
.filter_map(|excerpt| {
Some((
ExcerptId::from_proto(excerpt.id),
deserialize_excerpt_range(excerpt)?,
))
}),
cx,
);
}
multibuffer.remove_excerpts(removals, cx);
});
if !selections.is_empty() || pending_selection.is_some() {
this.set_selections_from_remote(selections, pending_selection, cx);
this.request_autoscroll_remotely(Autoscroll::newest(), cx);
} else if let Some(anchor) = scroll_top_anchor {
this.set_scroll_anchor_remote(ScrollAnchor {
top_anchor: anchor,
offset: vec2f(message.scroll_x, message.scroll_y)
}, cx);
}
})?;
Ok(())
update_editor_from_message(this, project, message, &mut cx).await
})
}
@ -404,6 +300,128 @@ impl FollowableItem for Editor {
}
}
async fn update_editor_from_message(
this: ViewHandle<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(
buffer_id: u64,
id: &ExcerptId,
@ -516,7 +534,24 @@ impl Item for Editor {
}
}
fn tab_description<'a>(&'a self, detail: usize, cx: &'a AppContext) -> Option<Cow<'a, str>> {
fn tab_tooltip_text(&self, cx: &AppContext) -> Option<Cow<str>> {
let file_path = self
.buffer()
.read(cx)
.as_singleton()?
.read(cx)
.file()
.and_then(|f| f.as_local())?
.abs_path(cx);
let file_path = util::paths::compact(&file_path)
.to_string_lossy()
.to_string();
Some(file_path.into())
}
fn tab_description<'a>(&'a self, detail: usize, cx: &'a AppContext) -> Option<Cow<str>> {
match path_for_buffer(&self.buffer, detail, true, cx)? {
Cow::Borrowed(path) => Some(path.to_string_lossy()),
Cow::Owned(path) => Some(path.to_string_lossy().to_string().into()),

View file

@ -1,6 +1,7 @@
mod anchor;
pub use anchor::{Anchor, AnchorRangeExt};
use anyhow::{anyhow, Result};
use clock::ReplicaId;
use collections::{BTreeMap, Bound, HashMap, HashSet};
use futures::{channel::mpsc, SinkExt};
@ -16,7 +17,9 @@ use language::{
use std::{
borrow::Cow,
cell::{Ref, RefCell},
cmp, fmt, io,
cmp, fmt,
future::Future,
io,
iter::{self, FromIterator},
mem,
ops::{Range, RangeBounds, Sub},
@ -61,6 +64,7 @@ pub enum Event {
},
Edited,
Reloaded,
LanguageChanged,
Reparsed,
Saved,
FileHandleChanged,
@ -1238,6 +1242,39 @@ impl MultiBuffer {
cx.notify();
}
pub fn wait_for_anchors<'a>(
&self,
anchors: impl 'a + Iterator<Item = Anchor>,
cx: &mut ModelContext<Self>,
) -> impl 'static + Future<Output = Result<()>> {
let borrow = self.buffers.borrow();
let mut error = None;
let mut futures = Vec::new();
for anchor in anchors {
if let Some(buffer_id) = anchor.buffer_id {
if let Some(buffer) = borrow.get(&buffer_id) {
buffer.buffer.update(cx, |buffer, _| {
futures.push(buffer.wait_for_anchors([anchor.text_anchor]))
});
} else {
error = Some(anyhow!(
"buffer {buffer_id} is not part of this multi-buffer"
));
break;
}
}
}
async move {
if let Some(error) = error {
Err(error)?;
}
for future in futures {
future.await?;
}
Ok(())
}
}
pub fn text_anchor_for_position<T: ToOffset>(
&self,
position: T,
@ -1266,6 +1303,7 @@ impl MultiBuffer {
language::Event::Saved => Event::Saved,
language::Event::FileHandleChanged => Event::FileHandleChanged,
language::Event::Reloaded => Event::Reloaded,
language::Event::LanguageChanged => Event::LanguageChanged,
language::Event::Reparsed => Event::Reparsed,
language::Event::DiagnosticsUpdated => Event::DiagnosticsUpdated,
language::Event::Closed => Event::Closed,

View file

@ -166,7 +166,7 @@ impl<'a> EditorTestContext<'a> {
///
/// See the `util::test::marked_text_ranges` function for more information.
pub fn set_state(&mut self, marked_text: &str) -> ContextHandle {
let _state_context = self.add_assertion_context(format!(
let state_context = self.add_assertion_context(format!(
"Initial Editor State: \"{}\"",
marked_text.escape_debug().to_string()
));
@ -177,7 +177,23 @@ impl<'a> EditorTestContext<'a> {
s.select_ranges(selection_ranges)
})
});
_state_context
state_context
}
/// Only change the editor's selections
pub fn set_selections_state(&mut self, marked_text: &str) -> ContextHandle {
let state_context = self.add_assertion_context(format!(
"Initial Editor State: \"{}\"",
marked_text.escape_debug().to_string()
));
let (unmarked_text, selection_ranges) = marked_text_ranges(marked_text, true);
self.editor.update(self.cx, |editor, cx| {
assert_eq!(editor.text(cx), unmarked_text);
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.select_ranges(selection_ranges)
})
});
state_context
}
/// Make an assertion about the editor's text and the ranges and directions
@ -188,10 +204,11 @@ impl<'a> EditorTestContext<'a> {
pub fn assert_editor_state(&mut self, marked_text: &str) {
let (unmarked_text, expected_selections) = marked_text_ranges(marked_text, true);
let buffer_text = self.buffer_text();
assert_eq!(
buffer_text, unmarked_text,
"Unmarked text doesn't match buffer text"
);
if buffer_text != unmarked_text {
panic!("Unmarked text doesn't match buffer text\nBuffer text: {buffer_text:?}\nUnmarked text: {unmarked_text:?}\nRaw buffer text\n{buffer_text}Raw unmarked text\n{unmarked_text}");
}
self.assert_selections(expected_selections, marked_text.to_string())
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -21,6 +21,7 @@ use settings::Settings;
use smallvec::SmallVec;
use std::{
any::{Any, TypeId},
borrow::Cow,
mem,
ops::Range,
path::PathBuf,
@ -224,6 +225,10 @@ impl View for ProjectSearchView {
}
impl Item for ProjectSearchView {
fn tab_tooltip_text(&self, cx: &AppContext) -> Option<Cow<str>> {
Some(self.query_editor.read(cx).text(cx).into())
}
fn act_as_type<'a>(
&'a self,
type_id: TypeId,

View file

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

View file

@ -3,6 +3,7 @@ pub mod terminal_button;
pub mod terminal_element;
use std::{
borrow::Cow,
ops::RangeInclusive,
path::{Path, PathBuf},
time::Duration,
@ -541,6 +542,10 @@ impl View for TerminalView {
}
impl Item for TerminalView {
fn tab_tooltip_text(&self, cx: &AppContext) -> Option<Cow<str>> {
Some(self.terminal().read(cx).title().into())
}
fn tab_content(
&self,
_detail: Option<usize>,

View file

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

View file

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

View file

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

View file

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

View file

@ -14,7 +14,10 @@ use gpui::{
};
use settings::Settings;
use smallvec::SmallVec;
use std::sync::{Arc, Weak};
use std::{
borrow::Cow,
sync::{Arc, Weak},
};
pub enum Event {
Close,
@ -94,6 +97,9 @@ impl View for SharedScreen {
}
impl Item for SharedScreen {
fn tab_tooltip_text(&self, _: &AppContext) -> Option<Cow<str>> {
Some(format!("{}'s screen", self.user.github_login).into())
}
fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
if let Some(nav_history) = self.nav_history.as_ref() {
nav_history.push::<()>(None, cx);

View file

@ -70,6 +70,7 @@ impl View for Toolbar {
for (item, position) in &self.items {
match *position {
ToolbarItemLocation::Hidden => {}
ToolbarItemLocation::PrimaryLeft { flex } => {
let left_item = ChildView::new(item.as_any(), cx)
.aligned()
@ -81,6 +82,7 @@ impl View for Toolbar {
primary_left_items.push(left_item.boxed());
}
}
ToolbarItemLocation::PrimaryRight { flex } => {
let right_item = ChildView::new(item.as_any(), cx)
.aligned()
@ -93,6 +95,7 @@ impl View for Toolbar {
primary_right_items.push(right_item.boxed());
}
}
ToolbarItemLocation::Secondary => {
secondary_item = Some(
ChildView::new(item.as_any(), cx)
@ -300,7 +303,10 @@ impl<T: ToolbarItemView> ToolbarItemViewHandle for ViewHandle<T> {
}
fn pane_focus_update(&mut self, pane_focused: bool, cx: &mut WindowContext) {
self.update(cx, |this, cx| this.pane_focus_update(pane_focused, cx));
self.update(cx, |this, cx| {
this.pane_focus_update(pane_focused, cx);
cx.notify();
});
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -702,6 +702,7 @@ mod tests {
open_paths(
&[PathBuf::from("/root/a"), PathBuf::from("/root/b")],
&app_state,
None,
cx,
)
})
@ -709,7 +710,7 @@ mod tests {
.unwrap();
assert_eq!(cx.window_ids().len(), 1);
cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, cx))
cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, None, cx))
.await
.unwrap();
assert_eq!(cx.window_ids().len(), 1);
@ -728,6 +729,7 @@ mod tests {
open_paths(
&[PathBuf::from("/root/b"), PathBuf::from("/root/c")],
&app_state,
None,
cx,
)
})
@ -735,16 +737,36 @@ mod tests {
.unwrap();
assert_eq!(cx.window_ids().len(), 2);
// Replace existing windows
let window_id = cx.window_ids()[0];
cx.update(|cx| {
open_paths(
&[PathBuf::from("/root/c"), PathBuf::from("/root/d")],
&app_state,
Some(window_id),
cx,
)
})
.await
.unwrap();
assert_eq!(cx.window_ids().len(), 3);
assert_eq!(cx.window_ids().len(), 2);
let workspace_1 = cx
.read_window(cx.window_ids()[0], |cx| cx.root_view().clone())
.unwrap()
.clone()
.downcast::<Workspace>()
.unwrap();
workspace_1.update(cx, |workspace, cx| {
assert_eq!(
workspace
.worktrees(cx)
.map(|w| w.read(cx).abs_path())
.collect::<Vec<_>>(),
&[Path::new("/root/c").into(), Path::new("/root/d").into()]
);
assert!(workspace.left_sidebar().read(cx).is_open());
assert!(workspace.active_pane().is_focused(cx));
});
}
#[gpui::test]
@ -756,7 +778,7 @@ mod tests {
.insert_tree("/root", json!({"a": "hey"}))
.await;
cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, cx))
cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, None, cx))
.await
.unwrap();
assert_eq!(cx.window_ids().len(), 1);
@ -799,7 +821,7 @@ mod tests {
assert!(!cx.is_window_edited(workspace.window_id()));
// Opening the buffer again doesn't impact the window's edited state.
cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, cx))
cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, None, cx))
.await
.unwrap();
let editor = workspace.read_with(cx, |workspace, cx| {

View file

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

View file

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

View file

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