Merge branch 'main' into finally-fix-terminal-line-height

This commit is contained in:
Mikayla Maki 2023-04-21 12:26:32 +12:00 committed by GitHub
commit f54ab73b47
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
50 changed files with 3598 additions and 2148 deletions

9
Cargo.lock generated
View file

@ -1192,7 +1192,7 @@ dependencies = [
[[package]] [[package]]
name = "collab" name = "collab"
version = "0.8.3" version = "0.9.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-tungstenite", "async-tungstenite",
@ -1341,14 +1341,17 @@ dependencies = [
"anyhow", "anyhow",
"async-compression", "async-compression",
"async-tar", "async-tar",
"clock",
"collections", "collections",
"context_menu", "context_menu",
"fs",
"futures 0.3.25", "futures 0.3.25",
"gpui", "gpui",
"language", "language",
"log", "log",
"lsp", "lsp",
"node_runtime", "node_runtime",
"rpc",
"serde", "serde",
"serde_derive", "serde_derive",
"settings", "settings",
@ -1831,6 +1834,7 @@ dependencies = [
"editor", "editor",
"gpui", "gpui",
"language", "language",
"lsp",
"postage", "postage",
"project", "project",
"serde_json", "serde_json",
@ -4687,6 +4691,7 @@ dependencies = [
"client", "client",
"clock", "clock",
"collections", "collections",
"copilot",
"ctor", "ctor",
"db", "db",
"env_logger", "env_logger",
@ -8517,7 +8522,7 @@ checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec"
[[package]] [[package]]
name = "zed" name = "zed"
version = "0.83.0" version = "0.84.0"
dependencies = [ dependencies = [
"activity_indicator", "activity_indicator",
"anyhow", "anyhow",

View file

@ -76,6 +76,7 @@ serde_derive = { version = "1.0", features = ["deserialize_in_place"] }
serde_json = { version = "1.0", features = ["preserve_order", "raw_value"] } serde_json = { version = "1.0", features = ["preserve_order", "raw_value"] }
rand = { version = "0.8" } rand = { version = "0.8" }
postage = { version = "0.5", features = ["futures-traits"] } postage = { version = "0.5", features = ["futures-traits"] }
smallvec = { version = "1.6", features = ["union"] }
[patch.crates-io] [patch.crates-io]
tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "c51896d32dcc11a38e41f36e3deb1a6a9c4f4b14" } tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "c51896d32dcc11a38e41f36e3deb1a6a9c4f4b14" }

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

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

View file

@ -18,4 +18,4 @@ settings = { path = "../settings" }
util = { path = "../util" } util = { path = "../util" }
workspace = { path = "../workspace" } workspace = { path = "../workspace" }
futures = "0.3" futures = "0.3"
smallvec = { version = "1.6", features = ["union"] } smallvec = { workspace = true }

View file

@ -9,4 +9,4 @@ path = "src/clock.rs"
doctest = false doctest = false
[dependencies] [dependencies]
smallvec = { version = "1.6", features = ["union"] } smallvec = { workspace = true }

View file

@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathan@zed.dev>"]
default-run = "collab" default-run = "collab"
edition = "2021" edition = "2021"
name = "collab" name = "collab"
version = "0.8.3" version = "0.9.0"
publish = false publish = false
[[bin]] [[bin]]

View file

@ -22,6 +22,7 @@ use language::{
LanguageConfig, OffsetRangeExt, Point, Rope, LanguageConfig, OffsetRangeExt, Point, Rope,
}; };
use live_kit_client::MacOSDisplay; use live_kit_client::MacOSDisplay;
use lsp::LanguageServerId;
use project::{search::SearchQuery, DiagnosticSummary, Project, ProjectPath}; use project::{search::SearchQuery, DiagnosticSummary, Project, ProjectPath};
use rand::prelude::*; use rand::prelude::*;
use serde_json::json; use serde_json::json;
@ -3477,6 +3478,7 @@ async fn test_collaborating_with_diagnostics(
worktree_id, worktree_id,
path: Arc::from(Path::new("a.rs")), path: Arc::from(Path::new("a.rs")),
}, },
LanguageServerId(0),
DiagnosticSummary { DiagnosticSummary {
error_count: 1, error_count: 1,
warning_count: 0, warning_count: 0,
@ -3512,6 +3514,7 @@ async fn test_collaborating_with_diagnostics(
worktree_id, worktree_id,
path: Arc::from(Path::new("a.rs")), path: Arc::from(Path::new("a.rs")),
}, },
LanguageServerId(0),
DiagnosticSummary { DiagnosticSummary {
error_count: 1, error_count: 1,
warning_count: 0, warning_count: 0,
@ -3552,10 +3555,10 @@ async fn test_collaborating_with_diagnostics(
worktree_id, worktree_id,
path: Arc::from(Path::new("a.rs")), path: Arc::from(Path::new("a.rs")),
}, },
LanguageServerId(0),
DiagnosticSummary { DiagnosticSummary {
error_count: 1, error_count: 1,
warning_count: 1, warning_count: 1,
..Default::default()
}, },
)] )]
); );
@ -3568,10 +3571,10 @@ async fn test_collaborating_with_diagnostics(
worktree_id, worktree_id,
path: Arc::from(Path::new("a.rs")), path: Arc::from(Path::new("a.rs")),
}, },
LanguageServerId(0),
DiagnosticSummary { DiagnosticSummary {
error_count: 1, error_count: 1,
warning_count: 1, warning_count: 1,
..Default::default()
}, },
)] )]
); );

View file

@ -13,4 +13,4 @@ gpui = { path = "../gpui" }
menu = { path = "../menu" } menu = { path = "../menu" }
settings = { path = "../settings" } settings = { path = "../settings" }
theme = { path = "../theme" } theme = { path = "../theme" }
smallvec = "1.6" smallvec = { workspace = true }

View file

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

View file

@ -5,24 +5,29 @@ use anyhow::{anyhow, Context, Result};
use async_compression::futures::bufread::GzipDecoder; use async_compression::futures::bufread::GzipDecoder;
use async_tar::Archive; use async_tar::Archive;
use collections::HashMap; use collections::HashMap;
use futures::{future::Shared, Future, FutureExt, TryFutureExt}; use futures::{channel::oneshot, future::Shared, Future, FutureExt, TryFutureExt};
use gpui::{actions, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task}; use gpui::{
use language::{point_from_lsp, point_to_lsp, Anchor, Bias, Buffer, Language, ToPointUtf16}; actions, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task, WeakModelHandle,
};
use language::{
point_from_lsp, point_to_lsp, Anchor, Bias, Buffer, BufferSnapshot, Language, PointUtf16,
ToPointUtf16,
};
use log::{debug, error}; use log::{debug, error};
use lsp::LanguageServer; use lsp::{LanguageServer, LanguageServerId};
use node_runtime::NodeRuntime; use node_runtime::NodeRuntime;
use request::{LogMessage, StatusNotification}; use request::{LogMessage, StatusNotification};
use settings::Settings; use settings::Settings;
use smol::{fs, io::BufReader, stream::StreamExt}; use smol::{fs, io::BufReader, stream::StreamExt};
use std::{ use std::{
ffi::OsString, ffi::OsString,
mem,
ops::Range, ops::Range,
path::{Path, PathBuf}, path::{Path, PathBuf},
sync::Arc, sync::Arc,
}; };
use util::{ use util::{
channel::ReleaseChannel, fs::remove_matching, github::latest_github_release, http::HttpClient, fs::remove_matching, github::latest_github_release, http::HttpClient, paths, ResultExt,
paths, ResultExt,
}; };
const COPILOT_AUTH_NAMESPACE: &'static str = "copilot_auth"; const COPILOT_AUTH_NAMESPACE: &'static str = "copilot_auth";
@ -35,15 +40,6 @@ actions!(
); );
pub fn init(http: Arc<dyn HttpClient>, node_runtime: Arc<NodeRuntime>, cx: &mut AppContext) { pub fn init(http: Arc<dyn HttpClient>, node_runtime: Arc<NodeRuntime>, cx: &mut AppContext) {
// Disable Copilot for stable releases.
if *cx.global::<ReleaseChannel>() == ReleaseChannel::Stable {
cx.update_global::<collections::CommandPaletteFilter, _, _>(|filter, _cx| {
filter.filtered_namespaces.insert(COPILOT_NAMESPACE);
filter.filtered_namespaces.insert(COPILOT_AUTH_NAMESPACE);
});
return;
}
let copilot = cx.add_model({ let copilot = cx.add_model({
let node_runtime = node_runtime.clone(); let node_runtime = node_runtime.clone();
move |cx| Copilot::start(http, node_runtime, cx) move |cx| Copilot::start(http, node_runtime, cx)
@ -98,15 +94,38 @@ pub fn init(http: Arc<dyn HttpClient>, node_runtime: Arc<NodeRuntime>, cx: &mut
enum CopilotServer { enum CopilotServer {
Disabled, Disabled,
Starting { Starting { task: Shared<Task<()>> },
task: Shared<Task<()>>,
},
Error(Arc<str>), Error(Arc<str>),
Started { Running(RunningCopilotServer),
server: Arc<LanguageServer>, }
status: SignInStatus,
subscriptions_by_buffer_id: HashMap<usize, gpui::Subscription>, impl CopilotServer {
}, fn as_authenticated(&mut self) -> Result<&mut RunningCopilotServer> {
let server = self.as_running()?;
if matches!(server.sign_in_status, SignInStatus::Authorized { .. }) {
Ok(server)
} else {
Err(anyhow!("must sign in before using copilot"))
}
}
fn as_running(&mut self) -> Result<&mut RunningCopilotServer> {
match self {
CopilotServer::Starting { .. } => Err(anyhow!("copilot is still starting")),
CopilotServer::Disabled => Err(anyhow!("copilot is disabled")),
CopilotServer::Error(error) => Err(anyhow!(
"copilot was not started because of an error: {}",
error
)),
CopilotServer::Running(server) => Ok(server),
}
}
}
struct RunningCopilotServer {
lsp: Arc<LanguageServer>,
sign_in_status: SignInStatus,
registered_buffers: HashMap<usize, RegisteredBuffer>,
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
@ -141,8 +160,104 @@ impl Status {
} }
} }
#[derive(Debug, PartialEq, Eq)] struct RegisteredBuffer {
id: usize,
uri: lsp::Url,
language_id: String,
snapshot: BufferSnapshot,
snapshot_version: i32,
_subscriptions: [gpui::Subscription; 2],
pending_buffer_change: Task<Option<()>>,
}
impl RegisteredBuffer {
fn report_changes(
&mut self,
buffer: &ModelHandle<Buffer>,
cx: &mut ModelContext<Copilot>,
) -> oneshot::Receiver<(i32, BufferSnapshot)> {
let id = self.id;
let (done_tx, done_rx) = oneshot::channel();
if buffer.read(cx).version() == self.snapshot.version {
let _ = done_tx.send((self.snapshot_version, self.snapshot.clone()));
} else {
let buffer = buffer.downgrade();
let prev_pending_change =
mem::replace(&mut self.pending_buffer_change, Task::ready(None));
self.pending_buffer_change = cx.spawn_weak(|copilot, mut cx| async move {
prev_pending_change.await;
let old_version = copilot.upgrade(&cx)?.update(&mut cx, |copilot, _| {
let server = copilot.server.as_authenticated().log_err()?;
let buffer = server.registered_buffers.get_mut(&id)?;
Some(buffer.snapshot.version.clone())
})?;
let new_snapshot = buffer
.upgrade(&cx)?
.read_with(&cx, |buffer, _| buffer.snapshot());
let content_changes = cx
.background()
.spawn({
let new_snapshot = new_snapshot.clone();
async move {
new_snapshot
.edits_since::<(PointUtf16, usize)>(&old_version)
.map(|edit| {
let edit_start = edit.new.start.0;
let edit_end = edit_start + (edit.old.end.0 - edit.old.start.0);
let new_text = new_snapshot
.text_for_range(edit.new.start.1..edit.new.end.1)
.collect();
lsp::TextDocumentContentChangeEvent {
range: Some(lsp::Range::new(
point_to_lsp(edit_start),
point_to_lsp(edit_end),
)),
range_length: None,
text: new_text,
}
})
.collect::<Vec<_>>()
}
})
.await;
copilot.upgrade(&cx)?.update(&mut cx, |copilot, _| {
let server = copilot.server.as_authenticated().log_err()?;
let buffer = server.registered_buffers.get_mut(&id)?;
if !content_changes.is_empty() {
buffer.snapshot_version += 1;
buffer.snapshot = new_snapshot;
server
.lsp
.notify::<lsp::notification::DidChangeTextDocument>(
lsp::DidChangeTextDocumentParams {
text_document: lsp::VersionedTextDocumentIdentifier::new(
buffer.uri.clone(),
buffer.snapshot_version,
),
content_changes,
},
)
.log_err();
}
let _ = done_tx.send((buffer.snapshot_version, buffer.snapshot.clone()));
Some(())
})?;
Some(())
});
}
done_rx
}
}
#[derive(Debug)]
pub struct Completion { pub struct Completion {
uuid: String,
pub range: Range<Anchor>, pub range: Range<Anchor>,
pub text: String, pub text: String,
} }
@ -151,6 +266,7 @@ pub struct Copilot {
http: Arc<dyn HttpClient>, http: Arc<dyn HttpClient>,
node_runtime: Arc<NodeRuntime>, node_runtime: Arc<NodeRuntime>,
server: CopilotServer, server: CopilotServer,
buffers: HashMap<usize, WeakModelHandle<Buffer>>,
} }
impl Entity for Copilot { impl Entity for Copilot {
@ -212,12 +328,14 @@ impl Copilot {
http, http,
node_runtime, node_runtime,
server: CopilotServer::Starting { task: start_task }, server: CopilotServer::Starting { task: start_task },
buffers: Default::default(),
} }
} else { } else {
Self { Self {
http, http,
node_runtime, node_runtime,
server: CopilotServer::Disabled, server: CopilotServer::Disabled,
buffers: Default::default(),
} }
} }
} }
@ -230,11 +348,12 @@ impl Copilot {
let this = cx.add_model(|cx| Self { let this = cx.add_model(|cx| Self {
http: http.clone(), http: http.clone(),
node_runtime: NodeRuntime::new(http, cx.background().clone()), node_runtime: NodeRuntime::new(http, cx.background().clone()),
server: CopilotServer::Started { server: CopilotServer::Running(RunningCopilotServer {
server: Arc::new(server), lsp: Arc::new(server),
status: SignInStatus::Authorized, sign_in_status: SignInStatus::Authorized,
subscriptions_by_buffer_id: Default::default(), registered_buffers: Default::default(),
}, }),
buffers: Default::default(),
}); });
(this, fake_server) (this, fake_server)
} }
@ -251,7 +370,7 @@ impl Copilot {
let node_path = node_runtime.binary_path().await?; let node_path = node_runtime.binary_path().await?;
let arguments: &[OsString] = &[server_path.into(), "--stdio".into()]; let arguments: &[OsString] = &[server_path.into(), "--stdio".into()];
let server = LanguageServer::new( let server = LanguageServer::new(
0, LanguageServerId(0),
&node_path, &node_path,
arguments, arguments,
Path::new("/"), Path::new("/"),
@ -286,6 +405,19 @@ impl Copilot {
) )
.detach(); .detach();
server
.request::<request::SetEditorInfo>(request::SetEditorInfoParams {
editor_info: request::EditorInfo {
name: "zed".into(),
version: env!("CARGO_PKG_VERSION").into(),
},
editor_plugin_info: request::EditorPluginInfo {
name: "zed-copilot".into(),
version: "0.0.1".into(),
},
})
.await?;
anyhow::Ok((server, status)) anyhow::Ok((server, status))
}; };
@ -294,11 +426,11 @@ impl Copilot {
cx.notify(); cx.notify();
match server { match server {
Ok((server, status)) => { Ok((server, status)) => {
this.server = CopilotServer::Started { this.server = CopilotServer::Running(RunningCopilotServer {
server, lsp: server,
status: SignInStatus::SignedOut, sign_in_status: SignInStatus::SignedOut,
subscriptions_by_buffer_id: Default::default(), registered_buffers: Default::default(),
}; });
this.update_sign_in_status(status, cx); this.update_sign_in_status(status, cx);
} }
Err(error) => { Err(error) => {
@ -311,8 +443,8 @@ impl Copilot {
} }
fn sign_in(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> { fn sign_in(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
if let CopilotServer::Started { server, status, .. } = &mut self.server { if let CopilotServer::Running(server) = &mut self.server {
let task = match status { let task = match &server.sign_in_status {
SignInStatus::Authorized { .. } | SignInStatus::Unauthorized { .. } => { SignInStatus::Authorized { .. } | SignInStatus::Unauthorized { .. } => {
Task::ready(Ok(())).shared() Task::ready(Ok(())).shared()
} }
@ -321,11 +453,11 @@ impl Copilot {
task.clone() task.clone()
} }
SignInStatus::SignedOut => { SignInStatus::SignedOut => {
let server = server.clone(); let lsp = server.lsp.clone();
let task = cx let task = cx
.spawn(|this, mut cx| async move { .spawn(|this, mut cx| async move {
let sign_in = async { let sign_in = async {
let sign_in = server let sign_in = lsp
.request::<request::SignInInitiate>( .request::<request::SignInInitiate>(
request::SignInInitiateParams {}, request::SignInInitiateParams {},
) )
@ -336,8 +468,10 @@ impl Copilot {
} }
request::SignInInitiateResult::PromptUserDeviceFlow(flow) => { request::SignInInitiateResult::PromptUserDeviceFlow(flow) => {
this.update(&mut cx, |this, cx| { this.update(&mut cx, |this, cx| {
if let CopilotServer::Started { status, .. } = if let CopilotServer::Running(RunningCopilotServer {
&mut this.server sign_in_status: status,
..
}) = &mut this.server
{ {
if let SignInStatus::SigningIn { if let SignInStatus::SigningIn {
prompt: prompt_flow, prompt: prompt_flow,
@ -349,7 +483,7 @@ impl Copilot {
} }
} }
}); });
let response = server let response = lsp
.request::<request::SignInConfirm>( .request::<request::SignInConfirm>(
request::SignInConfirmParams { request::SignInConfirmParams {
user_code: flow.user_code, user_code: flow.user_code,
@ -377,7 +511,7 @@ impl Copilot {
}) })
}) })
.shared(); .shared();
*status = SignInStatus::SigningIn { server.sign_in_status = SignInStatus::SigningIn {
prompt: None, prompt: None,
task: task.clone(), task: task.clone(),
}; };
@ -396,10 +530,8 @@ impl Copilot {
} }
fn sign_out(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> { fn sign_out(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
if let CopilotServer::Started { server, status, .. } = &mut self.server { self.update_sign_in_status(request::SignInStatus::NotSignedIn, cx);
*status = SignInStatus::SignedOut; if let CopilotServer::Running(RunningCopilotServer { lsp: server, .. }) = &self.server {
cx.notify();
let server = server.clone(); let server = server.clone();
cx.background().spawn(async move { cx.background().spawn(async move {
server server
@ -433,6 +565,135 @@ impl Copilot {
cx.foreground().spawn(start_task) cx.foreground().spawn(start_task)
} }
pub fn register_buffer(&mut self, buffer: &ModelHandle<Buffer>, cx: &mut ModelContext<Self>) {
let buffer_id = buffer.id();
self.buffers.insert(buffer_id, buffer.downgrade());
if let CopilotServer::Running(RunningCopilotServer {
lsp: server,
sign_in_status: status,
registered_buffers,
..
}) = &mut self.server
{
if !matches!(status, SignInStatus::Authorized { .. }) {
return;
}
registered_buffers.entry(buffer.id()).or_insert_with(|| {
let uri: lsp::Url = uri_for_buffer(buffer, cx);
let language_id = id_for_language(buffer.read(cx).language());
let snapshot = buffer.read(cx).snapshot();
server
.notify::<lsp::notification::DidOpenTextDocument>(
lsp::DidOpenTextDocumentParams {
text_document: lsp::TextDocumentItem {
uri: uri.clone(),
language_id: language_id.clone(),
version: 0,
text: snapshot.text(),
},
},
)
.log_err();
RegisteredBuffer {
id: buffer_id,
uri,
language_id,
snapshot,
snapshot_version: 0,
pending_buffer_change: Task::ready(Some(())),
_subscriptions: [
cx.subscribe(buffer, |this, buffer, event, cx| {
this.handle_buffer_event(buffer, event, cx).log_err();
}),
cx.observe_release(buffer, move |this, _buffer, _cx| {
this.buffers.remove(&buffer_id);
this.unregister_buffer(buffer_id);
}),
],
}
});
}
}
fn handle_buffer_event(
&mut self,
buffer: ModelHandle<Buffer>,
event: &language::Event,
cx: &mut ModelContext<Self>,
) -> Result<()> {
if let Ok(server) = self.server.as_running() {
if let Some(registered_buffer) = server.registered_buffers.get_mut(&buffer.id()) {
match event {
language::Event::Edited => {
let _ = registered_buffer.report_changes(&buffer, cx);
}
language::Event::Saved => {
server
.lsp
.notify::<lsp::notification::DidSaveTextDocument>(
lsp::DidSaveTextDocumentParams {
text_document: lsp::TextDocumentIdentifier::new(
registered_buffer.uri.clone(),
),
text: None,
},
)?;
}
language::Event::FileHandleChanged | language::Event::LanguageChanged => {
let new_language_id = id_for_language(buffer.read(cx).language());
let new_uri = uri_for_buffer(&buffer, cx);
if new_uri != registered_buffer.uri
|| new_language_id != registered_buffer.language_id
{
let old_uri = mem::replace(&mut registered_buffer.uri, new_uri);
registered_buffer.language_id = new_language_id;
server
.lsp
.notify::<lsp::notification::DidCloseTextDocument>(
lsp::DidCloseTextDocumentParams {
text_document: lsp::TextDocumentIdentifier::new(old_uri),
},
)?;
server
.lsp
.notify::<lsp::notification::DidOpenTextDocument>(
lsp::DidOpenTextDocumentParams {
text_document: lsp::TextDocumentItem::new(
registered_buffer.uri.clone(),
registered_buffer.language_id.clone(),
registered_buffer.snapshot_version,
registered_buffer.snapshot.text(),
),
},
)?;
}
}
_ => {}
}
}
}
Ok(())
}
fn unregister_buffer(&mut self, buffer_id: usize) {
if let Ok(server) = self.server.as_running() {
if let Some(buffer) = server.registered_buffers.remove(&buffer_id) {
server
.lsp
.notify::<lsp::notification::DidCloseTextDocument>(
lsp::DidCloseTextDocumentParams {
text_document: lsp::TextDocumentIdentifier::new(buffer.uri),
},
)
.log_err();
}
}
}
pub fn completions<T>( pub fn completions<T>(
&mut self, &mut self,
buffer: &ModelHandle<Buffer>, buffer: &ModelHandle<Buffer>,
@ -457,6 +718,51 @@ impl Copilot {
self.request_completions::<request::GetCompletionsCycling, _>(buffer, position, cx) self.request_completions::<request::GetCompletionsCycling, _>(buffer, position, cx)
} }
pub fn accept_completion(
&mut self,
completion: &Completion,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
let server = match self.server.as_authenticated() {
Ok(server) => server,
Err(error) => return Task::ready(Err(error)),
};
let request =
server
.lsp
.request::<request::NotifyAccepted>(request::NotifyAcceptedParams {
uuid: completion.uuid.clone(),
});
cx.background().spawn(async move {
request.await?;
Ok(())
})
}
pub fn discard_completions(
&mut self,
completions: &[Completion],
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
let server = match self.server.as_authenticated() {
Ok(server) => server,
Err(error) => return Task::ready(Err(error)),
};
let request =
server
.lsp
.request::<request::NotifyRejected>(request::NotifyRejectedParams {
uuids: completions
.iter()
.map(|completion| completion.uuid.clone())
.collect(),
});
cx.background().spawn(async move {
request.await?;
Ok(())
})
}
fn request_completions<R, T>( fn request_completions<R, T>(
&mut self, &mut self,
buffer: &ModelHandle<Buffer>, buffer: &ModelHandle<Buffer>,
@ -464,116 +770,48 @@ impl Copilot {
cx: &mut ModelContext<Self>, cx: &mut ModelContext<Self>,
) -> Task<Result<Vec<Completion>>> ) -> Task<Result<Vec<Completion>>>
where where
R: lsp::request::Request< R: 'static
Params = request::GetCompletionsParams, + lsp::request::Request<
Result = request::GetCompletionsResult, Params = request::GetCompletionsParams,
>, Result = request::GetCompletionsResult,
>,
T: ToPointUtf16, T: ToPointUtf16,
{ {
let buffer_id = buffer.id(); self.register_buffer(buffer, cx);
let uri: lsp::Url = format!("buffer://{}", buffer_id).parse().unwrap();
let snapshot = buffer.read(cx).snapshot();
let server = match &mut self.server {
CopilotServer::Starting { .. } => {
return Task::ready(Err(anyhow!("copilot is still starting")))
}
CopilotServer::Disabled => return Task::ready(Err(anyhow!("copilot is disabled"))),
CopilotServer::Error(error) => {
return Task::ready(Err(anyhow!(
"copilot was not started because of an error: {}",
error
)))
}
CopilotServer::Started {
server,
status,
subscriptions_by_buffer_id,
} => {
if matches!(status, SignInStatus::Authorized { .. }) {
subscriptions_by_buffer_id
.entry(buffer_id)
.or_insert_with(|| {
server
.notify::<lsp::notification::DidOpenTextDocument>(
lsp::DidOpenTextDocumentParams {
text_document: lsp::TextDocumentItem {
uri: uri.clone(),
language_id: id_for_language(
buffer.read(cx).language(),
),
version: 0,
text: snapshot.text(),
},
},
)
.log_err();
let uri = uri.clone(); let server = match self.server.as_authenticated() {
cx.observe_release(buffer, move |this, _, _| { Ok(server) => server,
if let CopilotServer::Started { Err(error) => return Task::ready(Err(error)),
server,
subscriptions_by_buffer_id,
..
} = &mut this.server
{
server
.notify::<lsp::notification::DidCloseTextDocument>(
lsp::DidCloseTextDocumentParams {
text_document: lsp::TextDocumentIdentifier::new(
uri.clone(),
),
},
)
.log_err();
subscriptions_by_buffer_id.remove(&buffer_id);
}
})
});
server.clone()
} else {
return Task::ready(Err(anyhow!("must sign in before using copilot")));
}
}
}; };
let lsp = server.lsp.clone();
let registered_buffer = server.registered_buffers.get_mut(&buffer.id()).unwrap();
let snapshot = registered_buffer.report_changes(buffer, cx);
let buffer = buffer.read(cx);
let uri = registered_buffer.uri.clone();
let settings = cx.global::<Settings>(); let settings = cx.global::<Settings>();
let position = position.to_point_utf16(&snapshot); let position = position.to_point_utf16(buffer);
let language = snapshot.language_at(position); let language = buffer.language_at(position);
let language_name = language.map(|language| language.name()); let language_name = language.map(|language| language.name());
let language_name = language_name.as_deref(); let language_name = language_name.as_deref();
let tab_size = settings.tab_size(language_name); let tab_size = settings.tab_size(language_name);
let hard_tabs = settings.hard_tabs(language_name); let hard_tabs = settings.hard_tabs(language_name);
let language_id = id_for_language(language); let relative_path = buffer
.file()
.map(|file| file.path().to_path_buf())
.unwrap_or_default();
let path; cx.foreground().spawn(async move {
let relative_path; let (version, snapshot) = snapshot.await?;
if let Some(file) = snapshot.file() { let result = lsp
if let Some(file) = file.as_local() {
path = file.abs_path(cx);
} else {
path = file.full_path(cx);
}
relative_path = file.path().to_path_buf();
} else {
path = PathBuf::new();
relative_path = PathBuf::new();
}
cx.background().spawn(async move {
let result = server
.request::<R>(request::GetCompletionsParams { .request::<R>(request::GetCompletionsParams {
doc: request::GetCompletionsDocument { doc: request::GetCompletionsDocument {
source: snapshot.text(), uri,
tab_size: tab_size.into(), tab_size: tab_size.into(),
indent_size: 1, indent_size: 1,
insert_spaces: !hard_tabs, insert_spaces: !hard_tabs,
uri,
path: path.to_string_lossy().into(),
relative_path: relative_path.to_string_lossy().into(), relative_path: relative_path.to_string_lossy().into(),
language_id,
position: point_to_lsp(position), position: point_to_lsp(position),
version: 0, version: version.try_into().unwrap(),
}, },
}) })
.await?; .await?;
@ -586,6 +824,7 @@ impl Copilot {
let end = let end =
snapshot.clip_point_utf16(point_from_lsp(completion.range.end), Bias::Left); snapshot.clip_point_utf16(point_from_lsp(completion.range.end), Bias::Left);
Completion { Completion {
uuid: completion.uuid,
range: snapshot.anchor_before(start)..snapshot.anchor_after(end), range: snapshot.anchor_before(start)..snapshot.anchor_after(end),
text: completion.text, text: completion.text,
} }
@ -600,14 +839,16 @@ impl Copilot {
CopilotServer::Starting { task } => Status::Starting { task: task.clone() }, CopilotServer::Starting { task } => Status::Starting { task: task.clone() },
CopilotServer::Disabled => Status::Disabled, CopilotServer::Disabled => Status::Disabled,
CopilotServer::Error(error) => Status::Error(error.clone()), CopilotServer::Error(error) => Status::Error(error.clone()),
CopilotServer::Started { status, .. } => match status { CopilotServer::Running(RunningCopilotServer { sign_in_status, .. }) => {
SignInStatus::Authorized { .. } => Status::Authorized, match sign_in_status {
SignInStatus::Unauthorized { .. } => Status::Unauthorized, SignInStatus::Authorized { .. } => Status::Authorized,
SignInStatus::SigningIn { prompt, .. } => Status::SigningIn { SignInStatus::Unauthorized { .. } => Status::Unauthorized,
prompt: prompt.clone(), SignInStatus::SigningIn { prompt, .. } => Status::SigningIn {
}, prompt: prompt.clone(),
SignInStatus::SignedOut => Status::SignedOut, },
}, SignInStatus::SignedOut => Status::SignedOut,
}
}
} }
} }
@ -616,14 +857,34 @@ impl Copilot {
lsp_status: request::SignInStatus, lsp_status: request::SignInStatus,
cx: &mut ModelContext<Self>, cx: &mut ModelContext<Self>,
) { ) {
if let CopilotServer::Started { status, .. } = &mut self.server { self.buffers.retain(|_, buffer| buffer.is_upgradable(cx));
*status = match lsp_status {
if let Ok(server) = self.server.as_running() {
match lsp_status {
request::SignInStatus::Ok { .. } request::SignInStatus::Ok { .. }
| request::SignInStatus::MaybeOk { .. } | request::SignInStatus::MaybeOk { .. }
| request::SignInStatus::AlreadySignedIn { .. } => SignInStatus::Authorized, | request::SignInStatus::AlreadySignedIn { .. } => {
request::SignInStatus::NotAuthorized { .. } => SignInStatus::Unauthorized, server.sign_in_status = SignInStatus::Authorized;
request::SignInStatus::NotSignedIn => SignInStatus::SignedOut, for buffer in self.buffers.values().cloned().collect::<Vec<_>>() {
}; if let Some(buffer) = buffer.upgrade(cx) {
self.register_buffer(&buffer, cx);
}
}
}
request::SignInStatus::NotAuthorized { .. } => {
server.sign_in_status = SignInStatus::Unauthorized;
for buffer_id in self.buffers.keys().copied().collect::<Vec<_>>() {
self.unregister_buffer(buffer_id);
}
}
request::SignInStatus::NotSignedIn => {
server.sign_in_status = SignInStatus::SignedOut;
for buffer_id in self.buffers.keys().copied().collect::<Vec<_>>() {
self.unregister_buffer(buffer_id);
}
}
}
cx.notify(); cx.notify();
} }
} }
@ -638,6 +899,14 @@ fn id_for_language(language: Option<&Arc<Language>>) -> String {
} }
} }
fn uri_for_buffer(buffer: &ModelHandle<Buffer>, cx: &AppContext) -> lsp::Url {
if let Some(file) = buffer.read(cx).file().and_then(|file| file.as_local()) {
lsp::Url::from_file_path(file.abs_path(cx)).unwrap()
} else {
format!("buffer://{}", buffer.id()).parse().unwrap()
}
}
async fn clear_copilot_dir() { async fn clear_copilot_dir() {
remove_matching(&paths::COPILOT_DIR, |_| true).await remove_matching(&paths::COPILOT_DIR, |_| true).await
} }
@ -709,3 +978,226 @@ async fn get_copilot_lsp(http: Arc<dyn HttpClient>) -> anyhow::Result<PathBuf> {
} }
} }
} }
#[cfg(test)]
mod tests {
use super::*;
use gpui::{executor::Deterministic, TestAppContext};
#[gpui::test(iterations = 10)]
async fn test_buffer_management(deterministic: Arc<Deterministic>, cx: &mut TestAppContext) {
deterministic.forbid_parking();
let (copilot, mut lsp) = Copilot::fake(cx);
let buffer_1 = cx.add_model(|cx| Buffer::new(0, "Hello", cx));
let buffer_1_uri: lsp::Url = format!("buffer://{}", buffer_1.id()).parse().unwrap();
copilot.update(cx, |copilot, cx| copilot.register_buffer(&buffer_1, cx));
assert_eq!(
lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
.await,
lsp::DidOpenTextDocumentParams {
text_document: lsp::TextDocumentItem::new(
buffer_1_uri.clone(),
"plaintext".into(),
0,
"Hello".into()
),
}
);
let buffer_2 = cx.add_model(|cx| Buffer::new(0, "Goodbye", cx));
let buffer_2_uri: lsp::Url = format!("buffer://{}", buffer_2.id()).parse().unwrap();
copilot.update(cx, |copilot, cx| copilot.register_buffer(&buffer_2, cx));
assert_eq!(
lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
.await,
lsp::DidOpenTextDocumentParams {
text_document: lsp::TextDocumentItem::new(
buffer_2_uri.clone(),
"plaintext".into(),
0,
"Goodbye".into()
),
}
);
buffer_1.update(cx, |buffer, cx| buffer.edit([(5..5, " world")], None, cx));
assert_eq!(
lsp.receive_notification::<lsp::notification::DidChangeTextDocument>()
.await,
lsp::DidChangeTextDocumentParams {
text_document: lsp::VersionedTextDocumentIdentifier::new(buffer_1_uri.clone(), 1),
content_changes: vec![lsp::TextDocumentContentChangeEvent {
range: Some(lsp::Range::new(
lsp::Position::new(0, 5),
lsp::Position::new(0, 5)
)),
range_length: None,
text: " world".into(),
}],
}
);
// Ensure updates to the file are reflected in the LSP.
buffer_1
.update(cx, |buffer, cx| {
buffer.file_updated(
Arc::new(File {
abs_path: "/root/child/buffer-1".into(),
path: Path::new("child/buffer-1").into(),
}),
cx,
)
})
.await;
assert_eq!(
lsp.receive_notification::<lsp::notification::DidCloseTextDocument>()
.await,
lsp::DidCloseTextDocumentParams {
text_document: lsp::TextDocumentIdentifier::new(buffer_1_uri),
}
);
let buffer_1_uri = lsp::Url::from_file_path("/root/child/buffer-1").unwrap();
assert_eq!(
lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
.await,
lsp::DidOpenTextDocumentParams {
text_document: lsp::TextDocumentItem::new(
buffer_1_uri.clone(),
"plaintext".into(),
1,
"Hello world".into()
),
}
);
// Ensure all previously-registered buffers are closed when signing out.
lsp.handle_request::<request::SignOut, _, _>(|_, _| async {
Ok(request::SignOutResult {})
});
copilot
.update(cx, |copilot, cx| copilot.sign_out(cx))
.await
.unwrap();
assert_eq!(
lsp.receive_notification::<lsp::notification::DidCloseTextDocument>()
.await,
lsp::DidCloseTextDocumentParams {
text_document: lsp::TextDocumentIdentifier::new(buffer_2_uri.clone()),
}
);
assert_eq!(
lsp.receive_notification::<lsp::notification::DidCloseTextDocument>()
.await,
lsp::DidCloseTextDocumentParams {
text_document: lsp::TextDocumentIdentifier::new(buffer_1_uri.clone()),
}
);
// Ensure all previously-registered buffers are re-opened when signing in.
lsp.handle_request::<request::SignInInitiate, _, _>(|_, _| async {
Ok(request::SignInInitiateResult::AlreadySignedIn {
user: "user-1".into(),
})
});
copilot
.update(cx, |copilot, cx| copilot.sign_in(cx))
.await
.unwrap();
assert_eq!(
lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
.await,
lsp::DidOpenTextDocumentParams {
text_document: lsp::TextDocumentItem::new(
buffer_2_uri.clone(),
"plaintext".into(),
0,
"Goodbye".into()
),
}
);
assert_eq!(
lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
.await,
lsp::DidOpenTextDocumentParams {
text_document: lsp::TextDocumentItem::new(
buffer_1_uri.clone(),
"plaintext".into(),
0,
"Hello world".into()
),
}
);
// Dropping a buffer causes it to be closed on the LSP side as well.
cx.update(|_| drop(buffer_2));
assert_eq!(
lsp.receive_notification::<lsp::notification::DidCloseTextDocument>()
.await,
lsp::DidCloseTextDocumentParams {
text_document: lsp::TextDocumentIdentifier::new(buffer_2_uri),
}
);
}
struct File {
abs_path: PathBuf,
path: Arc<Path>,
}
impl language::File for File {
fn as_local(&self) -> Option<&dyn language::LocalFile> {
Some(self)
}
fn mtime(&self) -> std::time::SystemTime {
todo!()
}
fn path(&self) -> &Arc<Path> {
&self.path
}
fn full_path(&self, _: &AppContext) -> PathBuf {
todo!()
}
fn file_name<'a>(&'a self, _: &'a AppContext) -> &'a std::ffi::OsStr {
todo!()
}
fn is_deleted(&self) -> bool {
todo!()
}
fn as_any(&self) -> &dyn std::any::Any {
todo!()
}
fn to_proto(&self) -> rpc::proto::File {
todo!()
}
}
impl language::LocalFile for File {
fn abs_path(&self, _: &AppContext) -> PathBuf {
self.abs_path.clone()
}
fn load(&self, _: &AppContext) -> Task<Result<String>> {
todo!()
}
fn buffer_reloaded(
&self,
_: u64,
_: &clock::Global,
_: language::RopeFingerprint,
_: ::fs::LineEnding,
_: std::time::SystemTime,
_: &mut AppContext,
) {
todo!()
}
}
}

View file

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

View file

@ -23,51 +23,51 @@ struct OpenGithub;
const COPILOT_SIGN_UP_URL: &'static str = "https://github.com/features/copilot"; const COPILOT_SIGN_UP_URL: &'static str = "https://github.com/features/copilot";
pub fn init(cx: &mut AppContext) { pub fn init(cx: &mut AppContext) {
let copilot = Copilot::global(cx).unwrap(); if let Some(copilot) = Copilot::global(cx) {
let mut code_verification: Option<ViewHandle<CopilotCodeVerification>> = None;
cx.observe(&copilot, move |copilot, cx| {
let status = copilot.read(cx).status();
let mut code_verification: Option<ViewHandle<CopilotCodeVerification>> = None; match &status {
cx.observe(&copilot, move |copilot, cx| { crate::Status::SigningIn { prompt } => {
let status = copilot.read(cx).status(); if let Some(code_verification_handle) = code_verification.as_mut() {
if cx.has_window(code_verification_handle.window_id()) {
match &status { code_verification_handle.update(cx, |code_verification_view, cx| {
crate::Status::SigningIn { prompt } => { code_verification_view.set_status(status, cx)
if let Some(code_verification_handle) = code_verification.as_mut() { });
if cx.has_window(code_verification_handle.window_id()) { cx.activate_window(code_verification_handle.window_id());
code_verification_handle.update(cx, |code_verification_view, cx| { } else {
code_verification_view.set_status(status, cx) create_copilot_auth_window(cx, &status, &mut code_verification);
}); }
cx.activate_window(code_verification_handle.window_id()); } else if let Some(_prompt) = prompt {
} else {
create_copilot_auth_window(cx, &status, &mut code_verification); create_copilot_auth_window(cx, &status, &mut code_verification);
} }
} else if let Some(_prompt) = prompt {
create_copilot_auth_window(cx, &status, &mut code_verification);
} }
} Status::Authorized | Status::Unauthorized => {
Status::Authorized | Status::Unauthorized => { if let Some(code_verification) = code_verification.as_ref() {
if let Some(code_verification) = code_verification.as_ref() { code_verification.update(cx, |code_verification, cx| {
code_verification.update(cx, |code_verification, cx| { code_verification.set_status(status, cx)
code_verification.set_status(status, cx) });
});
cx.platform().activate(true); cx.platform().activate(true);
cx.activate_window(code_verification.window_id()); cx.activate_window(code_verification.window_id());
}
}
_ => {
if let Some(code_verification) = code_verification.take() {
cx.remove_window(code_verification.window_id());
}
} }
} }
_ => { })
if let Some(code_verification) = code_verification.take() { .detach();
cx.remove_window(code_verification.window_id());
}
}
}
})
.detach();
cx.add_action( cx.add_action(
|code_verification: &mut CopilotCodeVerification, _: &ClickedConnect, _| { |code_verification: &mut CopilotCodeVerification, _: &ClickedConnect, _| {
code_verification.connect_clicked = true; code_verification.connect_clicked = true;
}, },
); );
}
} }
fn create_copilot_auth_window( fn create_copilot_auth_window(

View file

@ -10,10 +10,11 @@ doctest = false
[dependencies] [dependencies]
anyhow = "1.0" anyhow = "1.0"
smallvec = { version = "1.6", features = ["union"] } smallvec = { workspace = true }
collections = { path = "../collections" } collections = { path = "../collections" }
editor = { path = "../editor" } editor = { path = "../editor" }
language = { path = "../language" } language = { path = "../language" }
lsp = { path = "../lsp" }
gpui = { path = "../gpui" } gpui = { path = "../gpui" }
project = { path = "../project" } project = { path = "../project" }
settings = { path = "../settings" } settings = { path = "../settings" }
@ -27,6 +28,7 @@ unindent = "0.1"
client = { path = "../client", features = ["test-support"] } client = { path = "../client", features = ["test-support"] }
editor = { path = "../editor", features = ["test-support"] } editor = { path = "../editor", features = ["test-support"] }
language = { path = "../language", features = ["test-support"] } language = { path = "../language", features = ["test-support"] }
lsp = { path = "../lsp", features = ["test-support"] }
gpui = { path = "../gpui", features = ["test-support"] } gpui = { path = "../gpui", features = ["test-support"] }
workspace = { path = "../workspace", features = ["test-support"] } workspace = { path = "../workspace", features = ["test-support"] }
serde_json = { workspace = true } serde_json = { workspace = true }

View file

@ -1,7 +1,7 @@
pub mod items; pub mod items;
use anyhow::Result; use anyhow::Result;
use collections::{BTreeMap, HashSet}; use collections::{BTreeSet, HashSet};
use editor::{ use editor::{
diagnostic_block_renderer, diagnostic_block_renderer,
display_map::{BlockDisposition, BlockId, BlockProperties, BlockStyle, RenderBlock}, display_map::{BlockDisposition, BlockId, BlockProperties, BlockStyle, RenderBlock},
@ -18,6 +18,7 @@ use language::{
Anchor, Bias, Buffer, Diagnostic, DiagnosticEntry, DiagnosticSeverity, Point, Selection, Anchor, Bias, Buffer, Diagnostic, DiagnosticEntry, DiagnosticSeverity, Point, Selection,
SelectionGoal, SelectionGoal,
}; };
use lsp::LanguageServerId;
use project::{DiagnosticSummary, Project, ProjectPath}; use project::{DiagnosticSummary, Project, ProjectPath};
use serde_json::json; use serde_json::json;
use settings::Settings; use settings::Settings;
@ -56,7 +57,7 @@ struct ProjectDiagnosticsEditor {
summary: DiagnosticSummary, summary: DiagnosticSummary,
excerpts: ModelHandle<MultiBuffer>, excerpts: ModelHandle<MultiBuffer>,
path_states: Vec<PathState>, path_states: Vec<PathState>,
paths_to_update: BTreeMap<ProjectPath, usize>, paths_to_update: BTreeSet<(ProjectPath, LanguageServerId)>,
} }
struct PathState { struct PathState {
@ -72,6 +73,7 @@ struct Jump {
} }
struct DiagnosticGroupState { struct DiagnosticGroupState {
language_server_id: LanguageServerId,
primary_diagnostic: DiagnosticEntry<language::Anchor>, primary_diagnostic: DiagnosticEntry<language::Anchor>,
primary_excerpt_ix: usize, primary_excerpt_ix: usize,
excerpts: Vec<ExcerptId>, excerpts: Vec<ExcerptId>,
@ -116,7 +118,7 @@ impl View for ProjectDiagnosticsEditor {
}), }),
"summary": self.summary, "summary": self.summary,
"paths_to_update": self.paths_to_update.iter().map(|(path, server_id)| "paths_to_update": self.paths_to_update.iter().map(|(path, server_id)|
(path.path.to_string_lossy(), server_id) (path.path.to_string_lossy(), server_id.0)
).collect::<Vec<_>>(), ).collect::<Vec<_>>(),
"paths_states": self.path_states.iter().map(|state| "paths_states": self.path_states.iter().map(|state|
json!({ json!({
@ -149,7 +151,7 @@ impl ProjectDiagnosticsEditor {
path, path,
} => { } => {
this.paths_to_update this.paths_to_update
.insert(path.clone(), *language_server_id); .insert((path.clone(), *language_server_id));
} }
_ => {} _ => {}
}) })
@ -168,7 +170,7 @@ impl ProjectDiagnosticsEditor {
let project = project_handle.read(cx); let project = project_handle.read(cx);
let paths_to_update = project let paths_to_update = project
.diagnostic_summaries(cx) .diagnostic_summaries(cx)
.map(|e| (e.0, e.1.language_server_id)) .map(|(path, server_id, _)| (path, server_id))
.collect(); .collect();
let summary = project.diagnostic_summary(cx); let summary = project.diagnostic_summary(cx);
let mut this = Self { let mut this = Self {
@ -196,9 +198,13 @@ impl ProjectDiagnosticsEditor {
} }
} }
fn update_excerpts(&mut self, language_server_id: Option<usize>, cx: &mut ViewContext<Self>) { fn update_excerpts(
&mut self,
language_server_id: Option<LanguageServerId>,
cx: &mut ViewContext<Self>,
) {
let mut paths = Vec::new(); let mut paths = Vec::new();
self.paths_to_update.retain(|path, server_id| { self.paths_to_update.retain(|(path, server_id)| {
if language_server_id if language_server_id
.map_or(true, |language_server_id| language_server_id == *server_id) .map_or(true, |language_server_id| language_server_id == *server_id)
{ {
@ -215,7 +221,9 @@ impl ProjectDiagnosticsEditor {
let buffer = project let buffer = project
.update(&mut cx, |project, cx| project.open_buffer(path.clone(), cx)) .update(&mut cx, |project, cx| project.open_buffer(path.clone(), cx))
.await?; .await?;
this.update(&mut cx, |this, cx| this.populate_excerpts(path, buffer, cx)) this.update(&mut cx, |this, cx| {
this.populate_excerpts(path, language_server_id, buffer, cx)
})
} }
Result::<_, anyhow::Error>::Ok(()) Result::<_, anyhow::Error>::Ok(())
} }
@ -227,6 +235,7 @@ impl ProjectDiagnosticsEditor {
fn populate_excerpts( fn populate_excerpts(
&mut self, &mut self,
path: ProjectPath, path: ProjectPath,
language_server_id: Option<LanguageServerId>,
buffer: ModelHandle<Buffer>, buffer: ModelHandle<Buffer>,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) { ) {
@ -265,9 +274,9 @@ impl ProjectDiagnosticsEditor {
let excerpts_snapshot = self.excerpts.update(cx, |excerpts, excerpts_cx| { let excerpts_snapshot = self.excerpts.update(cx, |excerpts, excerpts_cx| {
let mut old_groups = path_state.diagnostic_groups.iter().enumerate().peekable(); let mut old_groups = path_state.diagnostic_groups.iter().enumerate().peekable();
let mut new_groups = snapshot let mut new_groups = snapshot
.diagnostic_groups() .diagnostic_groups(language_server_id)
.into_iter() .into_iter()
.filter(|group| { .filter(|(_, group)| {
group.entries[group.primary_ix].diagnostic.severity group.entries[group.primary_ix].diagnostic.severity
<= DiagnosticSeverity::WARNING <= DiagnosticSeverity::WARNING
}) })
@ -279,12 +288,27 @@ impl ProjectDiagnosticsEditor {
match (old_groups.peek(), new_groups.peek()) { match (old_groups.peek(), new_groups.peek()) {
(None, None) => break, (None, None) => break,
(None, Some(_)) => to_insert = new_groups.next(), (None, Some(_)) => to_insert = new_groups.next(),
(Some(_), None) => to_remove = old_groups.next(), (Some((_, old_group)), None) => {
(Some((_, old_group)), Some(new_group)) => { if language_server_id.map_or(true, |id| id == old_group.language_server_id)
{
to_remove = old_groups.next();
} else {
to_keep = old_groups.next();
}
}
(Some((_, old_group)), Some((_, new_group))) => {
let old_primary = &old_group.primary_diagnostic; let old_primary = &old_group.primary_diagnostic;
let new_primary = &new_group.entries[new_group.primary_ix]; let new_primary = &new_group.entries[new_group.primary_ix];
match compare_diagnostics(old_primary, new_primary, &snapshot) { match compare_diagnostics(old_primary, new_primary, &snapshot) {
Ordering::Less => to_remove = old_groups.next(), Ordering::Less => {
if language_server_id
.map_or(true, |id| id == old_group.language_server_id)
{
to_remove = old_groups.next();
} else {
to_keep = old_groups.next();
}
}
Ordering::Equal => { Ordering::Equal => {
to_keep = old_groups.next(); to_keep = old_groups.next();
new_groups.next(); new_groups.next();
@ -294,8 +318,9 @@ impl ProjectDiagnosticsEditor {
} }
} }
if let Some(group) = to_insert { if let Some((language_server_id, group)) = to_insert {
let mut group_state = DiagnosticGroupState { let mut group_state = DiagnosticGroupState {
language_server_id,
primary_diagnostic: group.entries[group.primary_ix].clone(), primary_diagnostic: group.entries[group.primary_ix].clone(),
primary_excerpt_ix: 0, primary_excerpt_ix: 0,
excerpts: Default::default(), excerpts: Default::default(),
@ -773,26 +798,24 @@ mod tests {
}; };
use gpui::TestAppContext; use gpui::TestAppContext;
use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, PointUtf16, Unclipped}; use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, PointUtf16, Unclipped};
use project::FakeFs;
use serde_json::json; use serde_json::json;
use unindent::Unindent as _; use unindent::Unindent as _;
use workspace::AppState;
#[gpui::test] #[gpui::test]
async fn test_diagnostics(cx: &mut TestAppContext) { async fn test_diagnostics(cx: &mut TestAppContext) {
let app_state = cx.update(AppState::test); Settings::test_async(cx);
app_state let fs = FakeFs::new(cx.background());
.fs fs.insert_tree(
.as_fake() "/test",
.insert_tree( json!({
"/test", "consts.rs": "
json!({
"consts.rs": "
const a: i32 = 'a'; const a: i32 = 'a';
const b: i32 = c; const b: i32 = c;
" "
.unindent(), .unindent(),
"main.rs": " "main.rs": "
fn main() { fn main() {
let x = vec![]; let x = vec![];
let y = vec![]; let y = vec![];
@ -804,19 +827,20 @@ mod tests {
d(x); d(x);
} }
" "
.unindent(), .unindent(),
}), }),
) )
.await; .await;
let project = Project::test(app_state.fs.clone(), ["/test".as_ref()], cx).await; let language_server_id = LanguageServerId(0);
let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
// Create some diagnostics // Create some diagnostics
project.update(cx, |project, cx| { project.update(cx, |project, cx| {
project project
.update_diagnostic_entries( .update_diagnostic_entries(
0, language_server_id,
PathBuf::from("/test/main.rs"), PathBuf::from("/test/main.rs"),
None, None,
vec![ vec![
@ -965,10 +989,10 @@ mod tests {
// Diagnostics are added for another earlier path. // Diagnostics are added for another earlier path.
project.update(cx, |project, cx| { project.update(cx, |project, cx| {
project.disk_based_diagnostics_started(0, cx); project.disk_based_diagnostics_started(language_server_id, cx);
project project
.update_diagnostic_entries( .update_diagnostic_entries(
0, language_server_id,
PathBuf::from("/test/consts.rs"), PathBuf::from("/test/consts.rs"),
None, None,
vec![DiagnosticEntry { vec![DiagnosticEntry {
@ -985,7 +1009,7 @@ mod tests {
cx, cx,
) )
.unwrap(); .unwrap();
project.disk_based_diagnostics_finished(0, cx); project.disk_based_diagnostics_finished(language_server_id, cx);
}); });
view.next_notification(cx).await; view.next_notification(cx).await;
@ -1065,10 +1089,10 @@ mod tests {
// Diagnostics are added to the first path // Diagnostics are added to the first path
project.update(cx, |project, cx| { project.update(cx, |project, cx| {
project.disk_based_diagnostics_started(0, cx); project.disk_based_diagnostics_started(language_server_id, cx);
project project
.update_diagnostic_entries( .update_diagnostic_entries(
0, language_server_id,
PathBuf::from("/test/consts.rs"), PathBuf::from("/test/consts.rs"),
None, None,
vec![ vec![
@ -1101,7 +1125,7 @@ mod tests {
cx, cx,
) )
.unwrap(); .unwrap();
project.disk_based_diagnostics_finished(0, cx); project.disk_based_diagnostics_finished(language_server_id, cx);
}); });
view.next_notification(cx).await; view.next_notification(cx).await;
@ -1181,6 +1205,272 @@ mod tests {
}); });
} }
#[gpui::test]
async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
Settings::test_async(cx);
let fs = FakeFs::new(cx.background());
fs.insert_tree(
"/test",
json!({
"main.js": "
a();
b();
c();
d();
e();
".unindent()
}),
)
.await;
let server_id_1 = LanguageServerId(100);
let server_id_2 = LanguageServerId(101);
let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
let view = cx.add_view(&workspace, |cx| {
ProjectDiagnosticsEditor::new(project.clone(), workspace.downgrade(), cx)
});
// Two language servers start updating diagnostics
project.update(cx, |project, cx| {
project.disk_based_diagnostics_started(server_id_1, cx);
project.disk_based_diagnostics_started(server_id_2, cx);
project
.update_diagnostic_entries(
server_id_1,
PathBuf::from("/test/main.js"),
None,
vec![DiagnosticEntry {
range: Unclipped(PointUtf16::new(0, 0))..Unclipped(PointUtf16::new(0, 1)),
diagnostic: Diagnostic {
message: "error 1".to_string(),
severity: DiagnosticSeverity::WARNING,
is_primary: true,
is_disk_based: true,
group_id: 1,
..Default::default()
},
}],
cx,
)
.unwrap();
project
.update_diagnostic_entries(
server_id_2,
PathBuf::from("/test/main.js"),
None,
vec![DiagnosticEntry {
range: Unclipped(PointUtf16::new(1, 0))..Unclipped(PointUtf16::new(1, 1)),
diagnostic: Diagnostic {
message: "warning 1".to_string(),
severity: DiagnosticSeverity::ERROR,
is_primary: true,
is_disk_based: true,
group_id: 2,
..Default::default()
},
}],
cx,
)
.unwrap();
});
// The first language server finishes
project.update(cx, |project, cx| {
project.disk_based_diagnostics_finished(server_id_1, cx);
});
// Only the first language server's diagnostics are shown.
cx.foreground().run_until_parked();
view.update(cx, |view, cx| {
assert_eq!(
editor_blocks(&view.editor, cx),
[
(0, "path header block".into()),
(2, "diagnostic header".into()),
]
);
assert_eq!(
view.editor.update(cx, |editor, cx| editor.display_text(cx)),
concat!(
"\n", // filename
"\n", // padding
// diagnostic group 1
"\n", // primary message
"\n", // padding
"a();\n", //
"b();",
)
);
});
// The second language server finishes
project.update(cx, |project, cx| {
project.disk_based_diagnostics_finished(server_id_2, cx);
});
// Both language server's diagnostics are shown.
cx.foreground().run_until_parked();
view.update(cx, |view, cx| {
assert_eq!(
editor_blocks(&view.editor, cx),
[
(0, "path header block".into()),
(2, "diagnostic header".into()),
(6, "collapsed context".into()),
(7, "diagnostic header".into()),
]
);
assert_eq!(
view.editor.update(cx, |editor, cx| editor.display_text(cx)),
concat!(
"\n", // filename
"\n", // padding
// diagnostic group 1
"\n", // primary message
"\n", // padding
"a();\n", // location
"b();\n", //
"\n", // collapsed context
// diagnostic group 2
"\n", // primary message
"\n", // padding
"a();\n", // context
"b();\n", //
"c();", // context
)
);
});
// Both language servers start updating diagnostics, and the first server finishes.
project.update(cx, |project, cx| {
project.disk_based_diagnostics_started(server_id_1, cx);
project.disk_based_diagnostics_started(server_id_2, cx);
project
.update_diagnostic_entries(
server_id_1,
PathBuf::from("/test/main.js"),
None,
vec![DiagnosticEntry {
range: Unclipped(PointUtf16::new(2, 0))..Unclipped(PointUtf16::new(2, 1)),
diagnostic: Diagnostic {
message: "warning 2".to_string(),
severity: DiagnosticSeverity::WARNING,
is_primary: true,
is_disk_based: true,
group_id: 1,
..Default::default()
},
}],
cx,
)
.unwrap();
project
.update_diagnostic_entries(
server_id_2,
PathBuf::from("/test/main.rs"),
None,
vec![],
cx,
)
.unwrap();
project.disk_based_diagnostics_finished(server_id_1, cx);
});
// Only the first language server's diagnostics are updated.
cx.foreground().run_until_parked();
view.update(cx, |view, cx| {
assert_eq!(
editor_blocks(&view.editor, cx),
[
(0, "path header block".into()),
(2, "diagnostic header".into()),
(7, "collapsed context".into()),
(8, "diagnostic header".into()),
]
);
assert_eq!(
view.editor.update(cx, |editor, cx| editor.display_text(cx)),
concat!(
"\n", // filename
"\n", // padding
// diagnostic group 1
"\n", // primary message
"\n", // padding
"a();\n", // location
"b();\n", //
"c();\n", // context
"\n", // collapsed context
// diagnostic group 2
"\n", // primary message
"\n", // padding
"b();\n", // context
"c();\n", //
"d();", // context
)
);
});
// The second language server finishes.
project.update(cx, |project, cx| {
project
.update_diagnostic_entries(
server_id_2,
PathBuf::from("/test/main.js"),
None,
vec![DiagnosticEntry {
range: Unclipped(PointUtf16::new(3, 0))..Unclipped(PointUtf16::new(3, 1)),
diagnostic: Diagnostic {
message: "warning 2".to_string(),
severity: DiagnosticSeverity::WARNING,
is_primary: true,
is_disk_based: true,
group_id: 1,
..Default::default()
},
}],
cx,
)
.unwrap();
project.disk_based_diagnostics_finished(server_id_2, cx);
});
// Both language servers' diagnostics are updated.
cx.foreground().run_until_parked();
view.update(cx, |view, cx| {
assert_eq!(
editor_blocks(&view.editor, cx),
[
(0, "path header block".into()),
(2, "diagnostic header".into()),
(7, "collapsed context".into()),
(8, "diagnostic header".into()),
]
);
assert_eq!(
view.editor.update(cx, |editor, cx| editor.display_text(cx)),
concat!(
"\n", // filename
"\n", // padding
// diagnostic group 1
"\n", // primary message
"\n", // padding
"b();\n", // location
"c();\n", //
"d();\n", // context
"\n", // collapsed context
// diagnostic group 2
"\n", // primary message
"\n", // padding
"c();\n", // context
"d();\n", //
"e();", // context
)
);
});
}
fn editor_blocks(editor: &ViewHandle<Editor>, cx: &mut AppContext) -> Vec<(u32, String)> { fn editor_blocks(editor: &ViewHandle<Editor>, cx: &mut AppContext) -> Vec<(u32, String)> {
let mut presenter = cx.build_presenter(editor.id(), 0., Default::default()); let mut presenter = cx.build_presenter(editor.id(), 0., Default::default());
let mut cx = presenter.build_layout_context(Default::default(), false, cx); let mut cx = presenter.build_layout_context(Default::default(), false, cx);

View file

@ -7,6 +7,7 @@ use gpui::{
ViewHandle, WeakViewHandle, ViewHandle, WeakViewHandle,
}; };
use language::Diagnostic; use language::Diagnostic;
use lsp::LanguageServerId;
use project::Project; use project::Project;
use settings::Settings; use settings::Settings;
use workspace::{item::ItemHandle, StatusItemView}; use workspace::{item::ItemHandle, StatusItemView};
@ -15,7 +16,7 @@ pub struct DiagnosticIndicator {
summary: project::DiagnosticSummary, summary: project::DiagnosticSummary,
active_editor: Option<WeakViewHandle<Editor>>, active_editor: Option<WeakViewHandle<Editor>>,
current_diagnostic: Option<Diagnostic>, current_diagnostic: Option<Diagnostic>,
in_progress_checks: HashSet<usize>, in_progress_checks: HashSet<LanguageServerId>,
_observe_active_editor: Option<Subscription>, _observe_active_editor: Option<Subscription>,
} }

View file

@ -58,7 +58,7 @@ postage = { workspace = true }
rand = { version = "0.8.3", optional = true } rand = { version = "0.8.3", optional = true }
serde = { workspace = true } serde = { workspace = true }
serde_derive = { workspace = true } serde_derive = { workspace = true }
smallvec = { version = "1.6", features = ["union"] } smallvec = { workspace = true }
smol = "1.2" smol = "1.2"
tree-sitter-rust = { version = "*", optional = true } tree-sitter-rust = { version = "*", optional = true }
tree-sitter-html = { version = "*", optional = true } tree-sitter-html = { version = "*", optional = true }

View file

@ -52,7 +52,7 @@ pub use language::{char_kind, CharKind};
use language::{ use language::{
AutoindentMode, BracketPair, Buffer, CodeAction, CodeLabel, Completion, CursorShape, AutoindentMode, BracketPair, Buffer, CodeAction, CodeLabel, Completion, CursorShape,
Diagnostic, DiagnosticSeverity, IndentKind, IndentSize, Language, OffsetRangeExt, OffsetUtf16, Diagnostic, DiagnosticSeverity, IndentKind, IndentSize, Language, OffsetRangeExt, OffsetUtf16,
Point, Rope, Selection, SelectionGoal, TransactionId, Point, Selection, SelectionGoal, TransactionId,
}; };
use link_go_to_definition::{ use link_go_to_definition::{
hide_link_definition, show_link_definition, LinkDefinitionKind, LinkGoToDefinitionState, hide_link_definition, show_link_definition, LinkDefinitionKind, LinkGoToDefinitionState,
@ -1037,6 +1037,10 @@ impl Default for CopilotState {
} }
impl CopilotState { impl CopilotState {
fn active_completion(&self) -> Option<&copilot::Completion> {
self.completions.get(self.active_completion_index)
}
fn text_for_active_completion( fn text_for_active_completion(
&self, &self,
cursor: Anchor, cursor: Anchor,
@ -1044,7 +1048,7 @@ impl CopilotState {
) -> Option<&str> { ) -> Option<&str> {
use language::ToOffset as _; use language::ToOffset as _;
let completion = self.completions.get(self.active_completion_index)?; let completion = self.active_completion()?;
let excerpt_id = self.excerpt_id?; let excerpt_id = self.excerpt_id?;
let completion_buffer = buffer.buffer_for_excerpt(excerpt_id)?; let completion_buffer = buffer.buffer_for_excerpt(excerpt_id)?;
if excerpt_id != cursor.excerpt_id if excerpt_id != cursor.excerpt_id
@ -1097,7 +1101,7 @@ impl CopilotState {
fn push_completion(&mut self, new_completion: copilot::Completion) { fn push_completion(&mut self, new_completion: copilot::Completion) {
for completion in &self.completions { for completion in &self.completions {
if *completion == new_completion { if completion.text == new_completion.text && completion.range == new_completion.range {
return; return;
} }
} }
@ -1496,7 +1500,7 @@ impl Editor {
self.refresh_code_actions(cx); self.refresh_code_actions(cx);
self.refresh_document_highlights(cx); self.refresh_document_highlights(cx);
refresh_matching_bracket_highlights(self, cx); refresh_matching_bracket_highlights(self, cx);
self.hide_copilot_suggestion(cx); self.discard_copilot_suggestion(cx);
} }
self.blink_manager.update(cx, BlinkManager::pause_blinking); self.blink_manager.update(cx, BlinkManager::pause_blinking);
@ -1870,7 +1874,7 @@ impl Editor {
return; return;
} }
if self.hide_copilot_suggestion(cx).is_some() { if self.discard_copilot_suggestion(cx) {
return; return;
} }
@ -2969,7 +2973,7 @@ impl Editor {
Some(()) Some(())
} }
fn cycle_suggestions( fn cycle_copilot_suggestions(
&mut self, &mut self,
direction: Direction, direction: Direction,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
@ -3020,7 +3024,7 @@ impl Editor {
fn next_copilot_suggestion(&mut self, _: &copilot::NextSuggestion, cx: &mut ViewContext<Self>) { fn next_copilot_suggestion(&mut self, _: &copilot::NextSuggestion, cx: &mut ViewContext<Self>) {
if self.has_active_copilot_suggestion(cx) { if self.has_active_copilot_suggestion(cx) {
self.cycle_suggestions(Direction::Next, cx); self.cycle_copilot_suggestions(Direction::Next, cx);
} else { } else {
self.refresh_copilot_suggestions(false, cx); self.refresh_copilot_suggestions(false, cx);
} }
@ -3032,15 +3036,45 @@ impl Editor {
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) { ) {
if self.has_active_copilot_suggestion(cx) { if self.has_active_copilot_suggestion(cx) {
self.cycle_suggestions(Direction::Prev, cx); self.cycle_copilot_suggestions(Direction::Prev, cx);
} else { } else {
self.refresh_copilot_suggestions(false, cx); self.refresh_copilot_suggestions(false, cx);
} }
} }
fn accept_copilot_suggestion(&mut self, cx: &mut ViewContext<Self>) -> bool { fn accept_copilot_suggestion(&mut self, cx: &mut ViewContext<Self>) -> bool {
if let Some(text) = self.hide_copilot_suggestion(cx) { if let Some(suggestion) = self
self.insert_with_autoindent_mode(&text.to_string(), None, cx); .display_map
.update(cx, |map, cx| map.replace_suggestion::<usize>(None, cx))
{
if let Some((copilot, completion)) =
Copilot::global(cx).zip(self.copilot_state.active_completion())
{
copilot
.update(cx, |copilot, cx| copilot.accept_completion(completion, cx))
.detach_and_log_err(cx);
}
self.insert_with_autoindent_mode(&suggestion.text.to_string(), None, cx);
cx.notify();
true
} else {
false
}
}
fn discard_copilot_suggestion(&mut self, cx: &mut ViewContext<Self>) -> bool {
if self.has_active_copilot_suggestion(cx) {
if let Some(copilot) = Copilot::global(cx) {
copilot
.update(cx, |copilot, cx| {
copilot.discard_completions(&self.copilot_state.completions, cx)
})
.detach_and_log_err(cx);
}
self.display_map
.update(cx, |map, cx| map.replace_suggestion::<usize>(None, cx));
cx.notify();
true true
} else { } else {
false false
@ -3051,18 +3085,6 @@ impl Editor {
self.display_map.read(cx).has_suggestion() self.display_map.read(cx).has_suggestion()
} }
fn hide_copilot_suggestion(&mut self, cx: &mut ViewContext<Self>) -> Option<Rope> {
if self.has_active_copilot_suggestion(cx) {
let old_suggestion = self
.display_map
.update(cx, |map, cx| map.replace_suggestion::<usize>(None, cx));
cx.notify();
old_suggestion.map(|suggestion| suggestion.text)
} else {
None
}
}
fn update_visible_copilot_suggestion(&mut self, cx: &mut ViewContext<Self>) { fn update_visible_copilot_suggestion(&mut self, cx: &mut ViewContext<Self>) {
let snapshot = self.buffer.read(cx).snapshot(cx); let snapshot = self.buffer.read(cx).snapshot(cx);
let selection = self.selections.newest_anchor(); let selection = self.selections.newest_anchor();
@ -3072,7 +3094,7 @@ impl Editor {
|| !self.completion_tasks.is_empty() || !self.completion_tasks.is_empty()
|| selection.start != selection.end || selection.start != selection.end
{ {
self.hide_copilot_suggestion(cx); self.discard_copilot_suggestion(cx);
} else if let Some(text) = self } else if let Some(text) = self
.copilot_state .copilot_state
.text_for_active_completion(cursor, &snapshot) .text_for_active_completion(cursor, &snapshot)
@ -3088,13 +3110,13 @@ impl Editor {
}); });
cx.notify(); cx.notify();
} else { } else {
self.hide_copilot_suggestion(cx); self.discard_copilot_suggestion(cx);
} }
} }
fn clear_copilot_suggestions(&mut self, cx: &mut ViewContext<Self>) { fn clear_copilot_suggestions(&mut self, cx: &mut ViewContext<Self>) {
self.copilot_state = Default::default(); self.copilot_state = Default::default();
self.hide_copilot_suggestion(cx); self.discard_copilot_suggestion(cx);
} }
pub fn render_code_actions_indicator( pub fn render_code_actions_indicator(
@ -3212,7 +3234,7 @@ impl Editor {
self.completion_tasks.clear(); self.completion_tasks.clear();
} }
self.context_menu = Some(menu); self.context_menu = Some(menu);
self.hide_copilot_suggestion(cx); self.discard_copilot_suggestion(cx);
cx.notify(); cx.notify();
} }
@ -6643,6 +6665,7 @@ impl Editor {
multi_buffer::Event::DiagnosticsUpdated => { multi_buffer::Event::DiagnosticsUpdated => {
self.refresh_active_diagnostics(cx); self.refresh_active_diagnostics(cx);
} }
multi_buffer::Event::LanguageChanged => {}
} }
} }

View file

@ -4371,7 +4371,7 @@ async fn test_strip_whitespace_and_format_via_lsp(cx: &mut gpui::TestAppContext)
cx.set_state( cx.set_state(
&[ &[
"one ", // "one ", //
"twoˇ", // "twoˇ", //
"three ", // "three ", //
"four", // "four", //
] ]
@ -4446,7 +4446,7 @@ async fn test_strip_whitespace_and_format_via_lsp(cx: &mut gpui::TestAppContext)
&[ &[
"one", // "one", //
"", // "", //
"twoˇ", // "twoˇ", //
"", // "", //
"three", // "three", //
"four", // "four", //
@ -4461,7 +4461,7 @@ async fn test_strip_whitespace_and_format_via_lsp(cx: &mut gpui::TestAppContext)
cx.assert_editor_state( cx.assert_editor_state(
&[ &[
"one ", // "one ", //
"twoˇ", // "twoˇ", //
"three ", // "three ", //
"four", // "four", //
] ]

View file

@ -436,6 +436,7 @@ mod tests {
use indoc::indoc; use indoc::indoc;
use language::{Diagnostic, DiagnosticSet}; use language::{Diagnostic, DiagnosticSet};
use lsp::LanguageServerId;
use project::HoverBlock; use project::HoverBlock;
use smol::stream::StreamExt; use smol::stream::StreamExt;
@ -620,7 +621,7 @@ mod tests {
}], }],
&snapshot, &snapshot,
); );
buffer.update_diagnostics(set, cx); buffer.update_diagnostics(LanguageServerId(0), set, cx);
}); });
// Hover pops diagnostic immediately // Hover pops diagnostic immediately

View file

@ -64,6 +64,7 @@ pub enum Event {
}, },
Edited, Edited,
Reloaded, Reloaded,
LanguageChanged,
Reparsed, Reparsed,
Saved, Saved,
FileHandleChanged, FileHandleChanged,
@ -1302,6 +1303,7 @@ impl MultiBuffer {
language::Event::Saved => Event::Saved, language::Event::Saved => Event::Saved,
language::Event::FileHandleChanged => Event::FileHandleChanged, language::Event::FileHandleChanged => Event::FileHandleChanged,
language::Event::Reloaded => Event::Reloaded, language::Event::Reloaded => Event::Reloaded,
language::Event::LanguageChanged => Event::LanguageChanged,
language::Event::Reparsed => Event::Reparsed, language::Event::Reparsed => Event::Reparsed,
language::Event::DiagnosticsUpdated => Event::DiagnosticsUpdated, language::Event::DiagnosticsUpdated => Event::DiagnosticsUpdated,
language::Event::Closed => Event::Closed, language::Event::Closed => Event::Closed,
@ -2762,6 +2764,15 @@ impl MultiBufferSnapshot {
.and_then(|(buffer, offset)| buffer.language_scope_at(offset)) .and_then(|(buffer, offset)| buffer.language_scope_at(offset))
} }
pub fn language_indent_size_at<T: ToOffset>(
&self,
position: T,
cx: &AppContext,
) -> Option<IndentSize> {
let (buffer_snapshot, offset) = self.point_to_buffer_offset(position)?;
Some(buffer_snapshot.language_indent_size_at(offset, cx))
}
pub fn is_dirty(&self) -> bool { pub fn is_dirty(&self) -> bool {
self.is_dirty self.is_dirty
} }
@ -2789,7 +2800,7 @@ impl MultiBufferSnapshot {
) -> impl Iterator<Item = DiagnosticEntry<O>> + 'a ) -> impl Iterator<Item = DiagnosticEntry<O>> + 'a
where where
T: 'a + ToOffset, T: 'a + ToOffset,
O: 'a + text::FromAnchor, O: 'a + text::FromAnchor + Ord,
{ {
self.as_singleton() self.as_singleton()
.into_iter() .into_iter()

View file

@ -44,7 +44,7 @@ seahash = "4.1"
serde = { workspace = true } serde = { workspace = true }
serde_derive = { workspace = true } serde_derive = { workspace = true }
serde_json = { workspace = true } serde_json = { workspace = true }
smallvec = { version = "1.6", features = ["union"] } smallvec = { workspace = true }
smol = "1.2" smol = "1.2"
time = { version = "0.3", features = ["serde", "serde-well-known"] } time = { version = "0.3", features = ["serde", "serde-well-known"] }
tiny-skia = "0.5" tiny-skia = "0.5"

View file

@ -50,7 +50,7 @@ serde = { workspace = true }
serde_derive = { workspace = true } serde_derive = { workspace = true }
serde_json = { workspace = true } serde_json = { workspace = true }
similar = "1.3" similar = "1.3"
smallvec = { version = "1.6", features = ["union"] } smallvec = { workspace = true }
smol = "1.2" smol = "1.2"
tree-sitter = "0.20" tree-sitter = "0.20"
tree-sitter-rust = { version = "*", optional = true } tree-sitter-rust = { version = "*", optional = true }

View file

@ -16,9 +16,11 @@ use clock::ReplicaId;
use fs::LineEnding; use fs::LineEnding;
use futures::FutureExt as _; use futures::FutureExt as _;
use gpui::{fonts::HighlightStyle, AppContext, Entity, ModelContext, Task}; use gpui::{fonts::HighlightStyle, AppContext, Entity, ModelContext, Task};
use lsp::LanguageServerId;
use parking_lot::Mutex; use parking_lot::Mutex;
use settings::Settings; use settings::Settings;
use similar::{ChangeTag, TextDiff}; use similar::{ChangeTag, TextDiff};
use smallvec::SmallVec;
use smol::future::yield_now; use smol::future::yield_now;
use std::{ use std::{
any::Any, any::Any,
@ -71,7 +73,7 @@ pub struct Buffer {
syntax_map: Mutex<SyntaxMap>, syntax_map: Mutex<SyntaxMap>,
parsing_in_background: bool, parsing_in_background: bool,
parse_count: usize, parse_count: usize,
diagnostics: DiagnosticSet, diagnostics: SmallVec<[(LanguageServerId, DiagnosticSet); 2]>,
remote_selections: TreeMap<ReplicaId, SelectionSet>, remote_selections: TreeMap<ReplicaId, SelectionSet>,
selections_update_count: usize, selections_update_count: usize,
diagnostics_update_count: usize, diagnostics_update_count: usize,
@ -88,7 +90,7 @@ pub struct BufferSnapshot {
pub git_diff: git::diff::BufferDiff, pub git_diff: git::diff::BufferDiff,
pub(crate) syntax: SyntaxSnapshot, pub(crate) syntax: SyntaxSnapshot,
file: Option<Arc<dyn File>>, file: Option<Arc<dyn File>>,
diagnostics: DiagnosticSet, diagnostics: SmallVec<[(LanguageServerId, DiagnosticSet); 2]>,
diagnostics_update_count: usize, diagnostics_update_count: usize,
file_update_count: usize, file_update_count: usize,
git_diff_update_count: usize, git_diff_update_count: usize,
@ -156,6 +158,7 @@ pub struct Completion {
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct CodeAction { pub struct CodeAction {
pub server_id: LanguageServerId,
pub range: Range<Anchor>, pub range: Range<Anchor>,
pub lsp_action: lsp::CodeAction, pub lsp_action: lsp::CodeAction,
} }
@ -163,16 +166,20 @@ pub struct CodeAction {
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
pub enum Operation { pub enum Operation {
Buffer(text::Operation), Buffer(text::Operation),
UpdateDiagnostics { UpdateDiagnostics {
server_id: LanguageServerId,
diagnostics: Arc<[DiagnosticEntry<Anchor>]>, diagnostics: Arc<[DiagnosticEntry<Anchor>]>,
lamport_timestamp: clock::Lamport, lamport_timestamp: clock::Lamport,
}, },
UpdateSelections { UpdateSelections {
selections: Arc<[Selection<Anchor>]>, selections: Arc<[Selection<Anchor>]>,
lamport_timestamp: clock::Lamport, lamport_timestamp: clock::Lamport,
line_mode: bool, line_mode: bool,
cursor_shape: CursorShape, cursor_shape: CursorShape,
}, },
UpdateCompletionTriggers { UpdateCompletionTriggers {
triggers: Vec<String>, triggers: Vec<String>,
lamport_timestamp: clock::Lamport, lamport_timestamp: clock::Lamport,
@ -187,6 +194,7 @@ pub enum Event {
Saved, Saved,
FileHandleChanged, FileHandleChanged,
Reloaded, Reloaded,
LanguageChanged,
Reparsed, Reparsed,
DiagnosticsUpdated, DiagnosticsUpdated,
Closed, Closed,
@ -407,6 +415,7 @@ impl Buffer {
) -> Task<Vec<proto::Operation>> { ) -> Task<Vec<proto::Operation>> {
let mut operations = Vec::new(); let mut operations = Vec::new();
operations.extend(self.deferred_ops.iter().map(proto::serialize_operation)); operations.extend(self.deferred_ops.iter().map(proto::serialize_operation));
operations.extend(self.remote_selections.iter().map(|(_, set)| { operations.extend(self.remote_selections.iter().map(|(_, set)| {
proto::serialize_operation(&Operation::UpdateSelections { proto::serialize_operation(&Operation::UpdateSelections {
selections: set.selections.clone(), selections: set.selections.clone(),
@ -415,10 +424,15 @@ impl Buffer {
cursor_shape: set.cursor_shape, cursor_shape: set.cursor_shape,
}) })
})); }));
operations.push(proto::serialize_operation(&Operation::UpdateDiagnostics {
diagnostics: self.diagnostics.iter().cloned().collect(), for (server_id, diagnostics) in &self.diagnostics {
lamport_timestamp: self.diagnostics_timestamp, operations.push(proto::serialize_operation(&Operation::UpdateDiagnostics {
})); lamport_timestamp: self.diagnostics_timestamp,
server_id: *server_id,
diagnostics: diagnostics.iter().cloned().collect(),
}));
}
operations.push(proto::serialize_operation( operations.push(proto::serialize_operation(
&Operation::UpdateCompletionTriggers { &Operation::UpdateCompletionTriggers {
triggers: self.completion_triggers.clone(), triggers: self.completion_triggers.clone(),
@ -536,6 +550,7 @@ impl Buffer {
self.syntax_map.lock().clear(); self.syntax_map.lock().clear();
self.language = language; self.language = language;
self.reparse(cx); self.reparse(cx);
cx.emit(Event::LanguageChanged);
} }
pub fn set_language_registry(&mut self, language_registry: Arc<LanguageRegistry>) { pub fn set_language_registry(&mut self, language_registry: Arc<LanguageRegistry>) {
@ -863,13 +878,19 @@ impl Buffer {
cx.notify(); cx.notify();
} }
pub fn update_diagnostics(&mut self, diagnostics: DiagnosticSet, cx: &mut ModelContext<Self>) { pub fn update_diagnostics(
&mut self,
server_id: LanguageServerId,
diagnostics: DiagnosticSet,
cx: &mut ModelContext<Self>,
) {
let lamport_timestamp = self.text.lamport_clock.tick(); let lamport_timestamp = self.text.lamport_clock.tick();
let op = Operation::UpdateDiagnostics { let op = Operation::UpdateDiagnostics {
server_id,
diagnostics: diagnostics.iter().cloned().collect(), diagnostics: diagnostics.iter().cloned().collect(),
lamport_timestamp, lamport_timestamp,
}; };
self.apply_diagnostic_update(diagnostics, lamport_timestamp, cx); self.apply_diagnostic_update(server_id, diagnostics, lamport_timestamp, cx);
self.send_operation(op, cx); self.send_operation(op, cx);
} }
@ -1577,11 +1598,13 @@ impl Buffer {
unreachable!("buffer operations should never be applied at this layer") unreachable!("buffer operations should never be applied at this layer")
} }
Operation::UpdateDiagnostics { Operation::UpdateDiagnostics {
server_id,
diagnostics: diagnostic_set, diagnostics: diagnostic_set,
lamport_timestamp, lamport_timestamp,
} => { } => {
let snapshot = self.snapshot(); let snapshot = self.snapshot();
self.apply_diagnostic_update( self.apply_diagnostic_update(
server_id,
DiagnosticSet::from_sorted_entries(diagnostic_set.iter().cloned(), &snapshot), DiagnosticSet::from_sorted_entries(diagnostic_set.iter().cloned(), &snapshot),
lamport_timestamp, lamport_timestamp,
cx, cx,
@ -1623,12 +1646,16 @@ impl Buffer {
fn apply_diagnostic_update( fn apply_diagnostic_update(
&mut self, &mut self,
server_id: LanguageServerId,
diagnostics: DiagnosticSet, diagnostics: DiagnosticSet,
lamport_timestamp: clock::Lamport, lamport_timestamp: clock::Lamport,
cx: &mut ModelContext<Self>, cx: &mut ModelContext<Self>,
) { ) {
if lamport_timestamp > self.diagnostics_timestamp { if lamport_timestamp > self.diagnostics_timestamp {
self.diagnostics = diagnostics; match self.diagnostics.binary_search_by_key(&server_id, |e| e.0) {
Err(ix) => self.diagnostics.insert(ix, (server_id, diagnostics)),
Ok(ix) => self.diagnostics[ix].1 = diagnostics,
};
self.diagnostics_timestamp = lamport_timestamp; self.diagnostics_timestamp = lamport_timestamp;
self.diagnostics_update_count += 1; self.diagnostics_update_count += 1;
self.text.lamport_clock.observe(lamport_timestamp); self.text.lamport_clock.observe(lamport_timestamp);
@ -2502,14 +2529,55 @@ impl BufferSnapshot {
) -> impl 'a + Iterator<Item = DiagnosticEntry<O>> ) -> impl 'a + Iterator<Item = DiagnosticEntry<O>>
where where
T: 'a + Clone + ToOffset, T: 'a + Clone + ToOffset,
O: 'a + FromAnchor, O: 'a + FromAnchor + Ord,
{ {
self.diagnostics.range(search_range, self, true, reversed) let mut iterators: Vec<_> = self
.diagnostics
.iter()
.map(|(_, collection)| {
collection
.range::<T, O>(search_range.clone(), self, true, reversed)
.peekable()
})
.collect();
std::iter::from_fn(move || {
let (next_ix, _) = iterators
.iter_mut()
.enumerate()
.flat_map(|(ix, iter)| Some((ix, iter.peek()?)))
.min_by(|(_, a), (_, b)| a.range.start.cmp(&b.range.start))?;
iterators[next_ix].next()
})
} }
pub fn diagnostic_groups(&self) -> Vec<DiagnosticGroup<Anchor>> { pub fn diagnostic_groups(
&self,
language_server_id: Option<LanguageServerId>,
) -> Vec<(LanguageServerId, DiagnosticGroup<Anchor>)> {
let mut groups = Vec::new(); let mut groups = Vec::new();
self.diagnostics.groups(&mut groups, self);
if let Some(language_server_id) = language_server_id {
if let Ok(ix) = self
.diagnostics
.binary_search_by_key(&language_server_id, |e| e.0)
{
self.diagnostics[ix]
.1
.groups(language_server_id, &mut groups, self);
}
} else {
for (language_server_id, diagnostics) in self.diagnostics.iter() {
diagnostics.groups(*language_server_id, &mut groups, self);
}
}
groups.sort_by(|(id_a, group_a), (id_b, group_b)| {
let a_start = &group_a.entries[group_a.primary_ix].range.start;
let b_start = &group_b.entries[group_b.primary_ix].range.start;
a_start.cmp(b_start, self).then_with(|| id_a.cmp(&id_b))
});
groups groups
} }
@ -2520,7 +2588,9 @@ impl BufferSnapshot {
where where
O: 'a + FromAnchor, O: 'a + FromAnchor,
{ {
self.diagnostics.group(group_id, self) self.diagnostics
.iter()
.flat_map(move |(_, set)| set.group(group_id, self))
} }
pub fn diagnostics_update_count(&self) -> usize { pub fn diagnostics_update_count(&self) -> usize {

View file

@ -1866,7 +1866,7 @@ fn test_random_collaboration(cx: &mut AppContext, mut rng: StdRng) {
buffer, buffer,
); );
log::info!("peer {} setting diagnostics: {:?}", replica_id, diagnostics); log::info!("peer {} setting diagnostics: {:?}", replica_id, diagnostics);
buffer.update_diagnostics(diagnostics, cx); buffer.update_diagnostics(LanguageServerId(0), diagnostics, cx);
}); });
mutation_count -= 1; mutation_count -= 1;
} }

View file

@ -1,5 +1,6 @@
use crate::Diagnostic; use crate::Diagnostic;
use collections::HashMap; use collections::HashMap;
use lsp::LanguageServerId;
use std::{ use std::{
cmp::{Ordering, Reverse}, cmp::{Ordering, Reverse},
iter, iter,
@ -129,7 +130,12 @@ impl DiagnosticSet {
}) })
} }
pub fn groups(&self, output: &mut Vec<DiagnosticGroup<Anchor>>, buffer: &text::BufferSnapshot) { pub fn groups(
&self,
language_server_id: LanguageServerId,
output: &mut Vec<(LanguageServerId, DiagnosticGroup<Anchor>)>,
buffer: &text::BufferSnapshot,
) {
let mut groups = HashMap::default(); let mut groups = HashMap::default();
for entry in self.diagnostics.iter() { for entry in self.diagnostics.iter() {
groups groups
@ -144,16 +150,22 @@ impl DiagnosticSet {
entries entries
.iter() .iter()
.position(|entry| entry.diagnostic.is_primary) .position(|entry| entry.diagnostic.is_primary)
.map(|primary_ix| DiagnosticGroup { .map(|primary_ix| {
entries, (
primary_ix, language_server_id,
DiagnosticGroup {
entries,
primary_ix,
},
)
}) })
})); }));
output[start_ix..].sort_unstable_by(|a, b| { output[start_ix..].sort_unstable_by(|(id_a, group_a), (id_b, group_b)| {
a.entries[a.primary_ix] group_a.entries[group_a.primary_ix]
.range .range
.start .start
.cmp(&b.entries[b.primary_ix].range.start, buffer) .cmp(&group_b.entries[group_b.primary_ix].range.start, buffer)
.then_with(|| id_a.cmp(&id_b))
}); });
} }

View file

@ -54,6 +54,7 @@ use futures::channel::mpsc;
pub use buffer::Operation; pub use buffer::Operation;
pub use buffer::*; pub use buffer::*;
pub use diagnostic_set::DiagnosticEntry; pub use diagnostic_set::DiagnosticEntry;
pub use lsp::LanguageServerId;
pub use outline::{Outline, OutlineItem}; pub use outline::{Outline, OutlineItem};
pub use tree_sitter::{Parser, Tree}; pub use tree_sitter::{Parser, Tree};
@ -414,7 +415,7 @@ pub struct BracketPair {
pub struct Language { pub struct Language {
pub(crate) config: LanguageConfig, pub(crate) config: LanguageConfig,
pub(crate) grammar: Option<Arc<Grammar>>, pub(crate) grammar: Option<Arc<Grammar>>,
pub(crate) adapter: Option<Arc<CachedLspAdapter>>, pub(crate) adapters: Vec<Arc<CachedLspAdapter>>,
#[cfg(any(test, feature = "test-support"))] #[cfg(any(test, feature = "test-support"))]
fake_adapter: Option<( fake_adapter: Option<(
@ -492,7 +493,7 @@ struct AvailableLanguage {
path: &'static str, path: &'static str,
config: LanguageConfig, config: LanguageConfig,
grammar: tree_sitter::Language, grammar: tree_sitter::Language,
lsp_adapter: Option<Arc<dyn LspAdapter>>, lsp_adapters: Vec<Arc<dyn LspAdapter>>,
get_queries: fn(&str) -> LanguageQueries, get_queries: fn(&str) -> LanguageQueries,
} }
@ -513,6 +514,7 @@ pub struct LanguageRegistry {
} }
struct LanguageRegistryState { struct LanguageRegistryState {
next_language_server_id: usize,
languages: Vec<Arc<Language>>, languages: Vec<Arc<Language>>,
available_languages: Vec<AvailableLanguage>, available_languages: Vec<AvailableLanguage>,
next_available_language_id: AvailableLanguageId, next_available_language_id: AvailableLanguageId,
@ -522,11 +524,17 @@ struct LanguageRegistryState {
version: usize, version: usize,
} }
pub struct PendingLanguageServer {
pub server_id: LanguageServerId,
pub task: Task<Result<lsp::LanguageServer>>,
}
impl LanguageRegistry { impl LanguageRegistry {
pub fn new(login_shell_env_loaded: Task<()>) -> Self { pub fn new(login_shell_env_loaded: Task<()>) -> Self {
let (lsp_binary_statuses_tx, lsp_binary_statuses_rx) = async_broadcast::broadcast(16); let (lsp_binary_statuses_tx, lsp_binary_statuses_rx) = async_broadcast::broadcast(16);
Self { Self {
state: RwLock::new(LanguageRegistryState { state: RwLock::new(LanguageRegistryState {
next_language_server_id: 0,
languages: vec![PLAIN_TEXT.clone()], languages: vec![PLAIN_TEXT.clone()],
available_languages: Default::default(), available_languages: Default::default(),
next_available_language_id: 0, next_available_language_id: 0,
@ -558,7 +566,7 @@ impl LanguageRegistry {
path: &'static str, path: &'static str,
config: LanguageConfig, config: LanguageConfig,
grammar: tree_sitter::Language, grammar: tree_sitter::Language,
lsp_adapter: Option<Arc<dyn LspAdapter>>, lsp_adapters: Vec<Arc<dyn LspAdapter>>,
get_queries: fn(&str) -> LanguageQueries, get_queries: fn(&str) -> LanguageQueries,
) { ) {
let state = &mut *self.state.write(); let state = &mut *self.state.write();
@ -567,7 +575,7 @@ impl LanguageRegistry {
path, path,
config, config,
grammar, grammar,
lsp_adapter, lsp_adapters,
get_queries, get_queries,
}); });
} }
@ -590,12 +598,13 @@ impl LanguageRegistry {
state state
.available_languages .available_languages
.iter() .iter()
.filter_map(|l| l.lsp_adapter.clone()) .flat_map(|l| l.lsp_adapters.clone())
.chain( .chain(
state state
.languages .languages
.iter() .iter()
.filter_map(|l| l.adapter.as_ref().map(|a| a.adapter.clone())), .flat_map(|language| &language.adapters)
.map(|adapter| adapter.adapter.clone()),
) )
.collect::<Vec<_>>() .collect::<Vec<_>>()
}; };
@ -721,7 +730,7 @@ impl LanguageRegistry {
let queries = (language.get_queries)(&language.path); let queries = (language.get_queries)(&language.path);
let language = let language =
Language::new(language.config, Some(language.grammar)) Language::new(language.config, Some(language.grammar))
.with_lsp_adapter(language.lsp_adapter) .with_lsp_adapters(language.lsp_adapters)
.await; .await;
let name = language.name(); let name = language.name();
match language.with_queries(queries) { match language.with_queries(queries) {
@ -776,16 +785,15 @@ impl LanguageRegistry {
pub fn start_language_server( pub fn start_language_server(
self: &Arc<Self>, self: &Arc<Self>,
server_id: usize,
language: Arc<Language>, language: Arc<Language>,
adapter: Arc<CachedLspAdapter>,
root_path: Arc<Path>, root_path: Arc<Path>,
http_client: Arc<dyn HttpClient>, http_client: Arc<dyn HttpClient>,
cx: &mut AppContext, cx: &mut AppContext,
) -> Option<Task<Result<lsp::LanguageServer>>> { ) -> Option<PendingLanguageServer> {
#[cfg(any(test, feature = "test-support"))] #[cfg(any(test, feature = "test-support"))]
if language.fake_adapter.is_some() { if language.fake_adapter.is_some() {
let language = language; let task = cx.spawn(|cx| async move {
return Some(cx.spawn(|cx| async move {
let (servers_tx, fake_adapter) = language.fake_adapter.as_ref().unwrap(); let (servers_tx, fake_adapter) = language.fake_adapter.as_ref().unwrap();
let (server, mut fake_server) = lsp::LanguageServer::fake( let (server, mut fake_server) = lsp::LanguageServer::fake(
fake_adapter.name.to_string(), fake_adapter.name.to_string(),
@ -810,7 +818,10 @@ impl LanguageRegistry {
}) })
.detach(); .detach();
Ok(server) Ok(server)
})); });
let server_id = self.state.write().next_language_server_id();
return Some(PendingLanguageServer { server_id, task });
} }
let download_dir = self let download_dir = self
@ -820,11 +831,16 @@ impl LanguageRegistry {
.log_err()?; .log_err()?;
let this = self.clone(); let this = self.clone();
let adapter = language.adapter.clone()?; let language = language.clone();
let http_client = http_client.clone();
let download_dir = download_dir.clone();
let root_path = root_path.clone();
let adapter = adapter.clone();
let lsp_binary_statuses = self.lsp_binary_statuses_tx.clone(); let lsp_binary_statuses = self.lsp_binary_statuses_tx.clone();
let login_shell_env_loaded = self.login_shell_env_loaded.clone(); let login_shell_env_loaded = self.login_shell_env_loaded.clone();
let server_id = self.state.write().next_language_server_id();
Some(cx.spawn(|cx| async move { let task = cx.spawn(|cx| async move {
login_shell_env_loaded.await; login_shell_env_loaded.await;
let mut lock = this.lsp_binary_paths.lock(); let mut lock = this.lsp_binary_paths.lock();
@ -856,7 +872,9 @@ impl LanguageRegistry {
)?; )?;
Ok(server) Ok(server)
})) });
Some(PendingLanguageServer { server_id, task })
} }
pub fn language_server_binary_statuses( pub fn language_server_binary_statuses(
@ -867,6 +885,10 @@ impl LanguageRegistry {
} }
impl LanguageRegistryState { impl LanguageRegistryState {
fn next_language_server_id(&mut self) -> LanguageServerId {
LanguageServerId(post_inc(&mut self.next_language_server_id))
}
fn add(&mut self, language: Arc<Language>) { fn add(&mut self, language: Arc<Language>) {
if let Some(theme) = self.theme.as_ref() { if let Some(theme) = self.theme.as_ref() {
language.set_theme(&theme.editor.syntax); language.set_theme(&theme.editor.syntax);
@ -974,15 +996,15 @@ impl Language {
highlight_map: Default::default(), highlight_map: Default::default(),
}) })
}), }),
adapter: None, adapters: Vec::new(),
#[cfg(any(test, feature = "test-support"))] #[cfg(any(test, feature = "test-support"))]
fake_adapter: None, fake_adapter: None,
} }
} }
pub fn lsp_adapter(&self) -> Option<Arc<CachedLspAdapter>> { pub fn lsp_adapters(&self) -> &[Arc<CachedLspAdapter>] {
self.adapter.clone() &self.adapters
} }
pub fn id(&self) -> Option<usize> { pub fn id(&self) -> Option<usize> {
@ -1209,9 +1231,9 @@ impl Language {
Arc::get_mut(self.grammar.as_mut().unwrap()).unwrap() Arc::get_mut(self.grammar.as_mut().unwrap()).unwrap()
} }
pub async fn with_lsp_adapter(mut self, lsp_adapter: Option<Arc<dyn LspAdapter>>) -> Self { pub async fn with_lsp_adapters(mut self, lsp_adapters: Vec<Arc<dyn LspAdapter>>) -> Self {
if let Some(adapter) = lsp_adapter { for adapter in lsp_adapters {
self.adapter = Some(CachedLspAdapter::new(adapter).await); self.adapters.push(CachedLspAdapter::new(adapter).await);
} }
self self
} }
@ -1224,7 +1246,7 @@ impl Language {
let (servers_tx, servers_rx) = mpsc::unbounded(); let (servers_tx, servers_rx) = mpsc::unbounded();
self.fake_adapter = Some((servers_tx, fake_lsp_adapter.clone())); self.fake_adapter = Some((servers_tx, fake_lsp_adapter.clone()));
let adapter = CachedLspAdapter::new(Arc::new(fake_lsp_adapter)).await; let adapter = CachedLspAdapter::new(Arc::new(fake_lsp_adapter)).await;
self.adapter = Some(adapter); self.adapters = vec![adapter];
servers_rx servers_rx
} }
@ -1233,28 +1255,31 @@ impl Language {
} }
pub async fn disk_based_diagnostic_sources(&self) -> &[String] { pub async fn disk_based_diagnostic_sources(&self) -> &[String] {
match self.adapter.as_ref() { match self.adapters.first().as_ref() {
Some(adapter) => &adapter.disk_based_diagnostic_sources, Some(adapter) => &adapter.disk_based_diagnostic_sources,
None => &[], None => &[],
} }
} }
pub async fn disk_based_diagnostics_progress_token(&self) -> Option<&str> { pub async fn disk_based_diagnostics_progress_token(&self) -> Option<&str> {
if let Some(adapter) = self.adapter.as_ref() { for adapter in &self.adapters {
adapter.disk_based_diagnostics_progress_token.as_deref() let token = adapter.disk_based_diagnostics_progress_token.as_deref();
} else { if token.is_some() {
None return token;
}
} }
None
} }
pub async fn process_diagnostics(&self, diagnostics: &mut lsp::PublishDiagnosticsParams) { pub async fn process_diagnostics(&self, diagnostics: &mut lsp::PublishDiagnosticsParams) {
if let Some(processor) = self.adapter.as_ref() { for adapter in &self.adapters {
processor.process_diagnostics(diagnostics).await; adapter.process_diagnostics(diagnostics).await;
} }
} }
pub async fn process_completion(self: &Arc<Self>, completion: &mut lsp::CompletionItem) { pub async fn process_completion(self: &Arc<Self>, completion: &mut lsp::CompletionItem) {
if let Some(adapter) = self.adapter.as_ref() { for adapter in &self.adapters {
adapter.process_completion(completion).await; adapter.process_completion(completion).await;
} }
} }
@ -1263,7 +1288,8 @@ impl Language {
self: &Arc<Self>, self: &Arc<Self>,
completion: &lsp::CompletionItem, completion: &lsp::CompletionItem,
) -> Option<CodeLabel> { ) -> Option<CodeLabel> {
self.adapter self.adapters
.first()
.as_ref()? .as_ref()?
.label_for_completion(completion, self) .label_for_completion(completion, self)
.await .await
@ -1274,7 +1300,8 @@ impl Language {
name: &str, name: &str,
kind: lsp::SymbolKind, kind: lsp::SymbolKind,
) -> Option<CodeLabel> { ) -> Option<CodeLabel> {
self.adapter self.adapters
.first()
.as_ref()? .as_ref()?
.label_for_symbol(name, kind, self) .label_for_symbol(name, kind, self)
.await .await
@ -1559,7 +1586,7 @@ mod tests {
..Default::default() ..Default::default()
}, },
tree_sitter_javascript::language(), tree_sitter_javascript::language(),
None, vec![],
|_| Default::default(), |_| Default::default(),
); );
@ -1595,7 +1622,7 @@ mod tests {
..Default::default() ..Default::default()
}, },
tree_sitter_json::language(), tree_sitter_json::language(),
None, vec![],
|_| Default::default(), |_| Default::default(),
); );
languages.register( languages.register(
@ -1606,7 +1633,7 @@ mod tests {
..Default::default() ..Default::default()
}, },
tree_sitter_rust::language(), tree_sitter_rust::language(),
None, vec![],
|_| Default::default(), |_| Default::default(),
); );
assert_eq!( assert_eq!(

View file

@ -4,7 +4,7 @@ use crate::{
}; };
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use clock::ReplicaId; use clock::ReplicaId;
use lsp::DiagnosticSeverity; use lsp::{DiagnosticSeverity, LanguageServerId};
use rpc::proto; use rpc::proto;
use std::{ops::Range, sync::Arc}; use std::{ops::Range, sync::Arc};
use text::*; use text::*;
@ -40,6 +40,7 @@ pub fn serialize_operation(operation: &crate::Operation) -> proto::Operation {
crate::Operation::Buffer(text::Operation::Edit(edit)) => { crate::Operation::Buffer(text::Operation::Edit(edit)) => {
proto::operation::Variant::Edit(serialize_edit_operation(edit)) proto::operation::Variant::Edit(serialize_edit_operation(edit))
} }
crate::Operation::Buffer(text::Operation::Undo { crate::Operation::Buffer(text::Operation::Undo {
undo, undo,
lamport_timestamp, lamport_timestamp,
@ -58,6 +59,7 @@ pub fn serialize_operation(operation: &crate::Operation) -> proto::Operation {
}) })
.collect(), .collect(),
}), }),
crate::Operation::UpdateSelections { crate::Operation::UpdateSelections {
selections, selections,
line_mode, line_mode,
@ -70,14 +72,18 @@ pub fn serialize_operation(operation: &crate::Operation) -> proto::Operation {
line_mode: *line_mode, line_mode: *line_mode,
cursor_shape: serialize_cursor_shape(cursor_shape) as i32, cursor_shape: serialize_cursor_shape(cursor_shape) as i32,
}), }),
crate::Operation::UpdateDiagnostics { crate::Operation::UpdateDiagnostics {
diagnostics,
lamport_timestamp, lamport_timestamp,
server_id,
diagnostics,
} => proto::operation::Variant::UpdateDiagnostics(proto::UpdateDiagnostics { } => proto::operation::Variant::UpdateDiagnostics(proto::UpdateDiagnostics {
replica_id: lamport_timestamp.replica_id as u32, replica_id: lamport_timestamp.replica_id as u32,
lamport_timestamp: lamport_timestamp.value, lamport_timestamp: lamport_timestamp.value,
server_id: server_id.0 as u64,
diagnostics: serialize_diagnostics(diagnostics.iter()), diagnostics: serialize_diagnostics(diagnostics.iter()),
}), }),
crate::Operation::UpdateCompletionTriggers { crate::Operation::UpdateCompletionTriggers {
triggers, triggers,
lamport_timestamp, lamport_timestamp,
@ -267,11 +273,12 @@ pub fn deserialize_operation(message: proto::Operation) -> Result<crate::Operati
} }
proto::operation::Variant::UpdateDiagnostics(message) => { proto::operation::Variant::UpdateDiagnostics(message) => {
crate::Operation::UpdateDiagnostics { crate::Operation::UpdateDiagnostics {
diagnostics: deserialize_diagnostics(message.diagnostics),
lamport_timestamp: clock::Lamport { lamport_timestamp: clock::Lamport {
replica_id: message.replica_id as ReplicaId, replica_id: message.replica_id as ReplicaId,
value: message.lamport_timestamp, value: message.lamport_timestamp,
}, },
server_id: LanguageServerId(message.server_id as usize),
diagnostics: deserialize_diagnostics(message.diagnostics),
} }
} }
proto::operation::Variant::UpdateCompletionTriggers(message) => { proto::operation::Variant::UpdateCompletionTriggers(message) => {
@ -462,6 +469,7 @@ pub async fn deserialize_completion(
pub fn serialize_code_action(action: &CodeAction) -> proto::CodeAction { pub fn serialize_code_action(action: &CodeAction) -> proto::CodeAction {
proto::CodeAction { proto::CodeAction {
server_id: action.server_id.0 as u64,
start: Some(serialize_anchor(&action.range.start)), start: Some(serialize_anchor(&action.range.start)),
end: Some(serialize_anchor(&action.range.end)), end: Some(serialize_anchor(&action.range.end)),
lsp_action: serde_json::to_vec(&action.lsp_action).unwrap(), lsp_action: serde_json::to_vec(&action.lsp_action).unwrap(),
@ -479,6 +487,7 @@ pub fn deserialize_code_action(action: proto::CodeAction) -> Result<CodeAction>
.ok_or_else(|| anyhow!("invalid end"))?; .ok_or_else(|| anyhow!("invalid end"))?;
let lsp_action = serde_json::from_slice(&action.lsp_action)?; let lsp_action = serde_json::from_slice(&action.lsp_action)?;
Ok(CodeAction { Ok(CodeAction {
server_id: LanguageServerId(action.server_id as usize),
range: start..end, range: start..end,
lsp_action, lsp_action,
}) })

View file

@ -16,6 +16,7 @@ use smol::{
process::{self, Child}, process::{self, Child},
}; };
use std::{ use std::{
fmt,
future::Future, future::Future,
io::Write, io::Write,
path::PathBuf, path::PathBuf,
@ -35,7 +36,7 @@ type NotificationHandler = Box<dyn Send + FnMut(Option<usize>, &str, AsyncAppCon
type ResponseHandler = Box<dyn Send + FnOnce(Result<&str, Error>)>; type ResponseHandler = Box<dyn Send + FnOnce(Result<&str, Error>)>;
pub struct LanguageServer { pub struct LanguageServer {
server_id: usize, server_id: LanguageServerId,
next_id: AtomicUsize, next_id: AtomicUsize,
outbound_tx: channel::Sender<Vec<u8>>, outbound_tx: channel::Sender<Vec<u8>>,
name: String, name: String,
@ -51,6 +52,10 @@ pub struct LanguageServer {
_server: Option<Child>, _server: Option<Child>,
} }
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(transparent)]
pub struct LanguageServerId(pub usize);
pub struct Subscription { pub struct Subscription {
method: &'static str, method: &'static str,
notification_handlers: Arc<Mutex<HashMap<&'static str, NotificationHandler>>>, notification_handlers: Arc<Mutex<HashMap<&'static str, NotificationHandler>>>,
@ -107,7 +112,7 @@ struct Error {
impl LanguageServer { impl LanguageServer {
pub fn new<T: AsRef<std::ffi::OsStr>>( pub fn new<T: AsRef<std::ffi::OsStr>>(
server_id: usize, server_id: LanguageServerId,
binary_path: &Path, binary_path: &Path,
arguments: &[T], arguments: &[T],
root_path: &Path, root_path: &Path,
@ -158,7 +163,7 @@ impl LanguageServer {
} }
fn new_internal<Stdin, Stdout, F>( fn new_internal<Stdin, Stdout, F>(
server_id: usize, server_id: LanguageServerId,
stdin: Stdin, stdin: Stdin,
stdout: Stdout, stdout: Stdout,
server: Option<Child>, server: Option<Child>,
@ -581,7 +586,7 @@ impl LanguageServer {
&self.capabilities &self.capabilities
} }
pub fn server_id(&self) -> usize { pub fn server_id(&self) -> LanguageServerId {
self.server_id self.server_id
} }
@ -685,6 +690,12 @@ impl Subscription {
} }
} }
impl fmt::Display for LanguageServerId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
impl Drop for Subscription { impl Drop for Subscription {
fn drop(&mut self) { fn drop(&mut self) {
self.notification_handlers.lock().remove(self.method); self.notification_handlers.lock().remove(self.method);
@ -720,7 +731,7 @@ impl LanguageServer {
let (notifications_tx, notifications_rx) = channel::unbounded(); let (notifications_tx, notifications_rx) = channel::unbounded();
let server = Self::new_internal( let server = Self::new_internal(
0, LanguageServerId(0),
stdin_writer, stdin_writer,
stdout_reader, stdout_reader,
None, None,
@ -731,7 +742,7 @@ impl LanguageServer {
); );
let fake = FakeLanguageServer { let fake = FakeLanguageServer {
server: Arc::new(Self::new_internal( server: Arc::new(Self::new_internal(
0, LanguageServerId(0),
stdout_writer, stdout_writer,
stdin_reader, stdin_reader,
None, None,

View file

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

View file

@ -12,7 +12,7 @@ use language::{
range_from_lsp, range_to_lsp, Anchor, Bias, Buffer, CachedLspAdapter, CharKind, CodeAction, range_from_lsp, range_to_lsp, Anchor, Bias, Buffer, CachedLspAdapter, CharKind, CodeAction,
Completion, OffsetRangeExt, PointUtf16, ToOffset, ToPointUtf16, Unclipped, Completion, OffsetRangeExt, PointUtf16, ToOffset, ToPointUtf16, Unclipped,
}; };
use lsp::{DocumentHighlightKind, LanguageServer, ServerCapabilities}; use lsp::{DocumentHighlightKind, LanguageServer, LanguageServerId, ServerCapabilities};
use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag}; use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag};
use std::{cmp::Reverse, ops::Range, path::Path, sync::Arc}; use std::{cmp::Reverse, ops::Range, path::Path, sync::Arc};
@ -33,21 +33,25 @@ pub(crate) trait LspCommand: 'static + Sized {
language_server: &Arc<LanguageServer>, language_server: &Arc<LanguageServer>,
cx: &AppContext, cx: &AppContext,
) -> <Self::LspRequest as lsp::request::Request>::Params; ) -> <Self::LspRequest as lsp::request::Request>::Params;
async fn response_from_lsp( async fn response_from_lsp(
self, self,
message: <Self::LspRequest as lsp::request::Request>::Result, message: <Self::LspRequest as lsp::request::Request>::Result,
project: ModelHandle<Project>, project: ModelHandle<Project>,
buffer: ModelHandle<Buffer>, buffer: ModelHandle<Buffer>,
server_id: LanguageServerId,
cx: AsyncAppContext, cx: AsyncAppContext,
) -> Result<Self::Response>; ) -> Result<Self::Response>;
fn to_proto(&self, project_id: u64, buffer: &Buffer) -> Self::ProtoRequest; fn to_proto(&self, project_id: u64, buffer: &Buffer) -> Self::ProtoRequest;
async fn from_proto( async fn from_proto(
message: Self::ProtoRequest, message: Self::ProtoRequest,
project: ModelHandle<Project>, project: ModelHandle<Project>,
buffer: ModelHandle<Buffer>, buffer: ModelHandle<Buffer>,
cx: AsyncAppContext, cx: AsyncAppContext,
) -> Result<Self>; ) -> Result<Self>;
fn response_to_proto( fn response_to_proto(
response: Self::Response, response: Self::Response,
project: &mut Project, project: &mut Project,
@ -55,6 +59,7 @@ pub(crate) trait LspCommand: 'static + Sized {
buffer_version: &clock::Global, buffer_version: &clock::Global,
cx: &mut AppContext, cx: &mut AppContext,
) -> <Self::ProtoRequest as proto::RequestMessage>::Response; ) -> <Self::ProtoRequest as proto::RequestMessage>::Response;
async fn response_from_proto( async fn response_from_proto(
self, self,
message: <Self::ProtoRequest as proto::RequestMessage>::Response, message: <Self::ProtoRequest as proto::RequestMessage>::Response,
@ -62,6 +67,7 @@ pub(crate) trait LspCommand: 'static + Sized {
buffer: ModelHandle<Buffer>, buffer: ModelHandle<Buffer>,
cx: AsyncAppContext, cx: AsyncAppContext,
) -> Result<Self::Response>; ) -> Result<Self::Response>;
fn buffer_id_from_proto(message: &Self::ProtoRequest) -> u64; fn buffer_id_from_proto(message: &Self::ProtoRequest) -> u64;
} }
@ -137,6 +143,7 @@ impl LspCommand for PrepareRename {
message: Option<lsp::PrepareRenameResponse>, message: Option<lsp::PrepareRenameResponse>,
_: ModelHandle<Project>, _: ModelHandle<Project>,
buffer: ModelHandle<Buffer>, buffer: ModelHandle<Buffer>,
_: LanguageServerId,
cx: AsyncAppContext, cx: AsyncAppContext,
) -> Result<Option<Range<Anchor>>> { ) -> Result<Option<Range<Anchor>>> {
buffer.read_with(&cx, |buffer, _| { buffer.read_with(&cx, |buffer, _| {
@ -263,10 +270,12 @@ impl LspCommand for PerformRename {
message: Option<lsp::WorkspaceEdit>, message: Option<lsp::WorkspaceEdit>,
project: ModelHandle<Project>, project: ModelHandle<Project>,
buffer: ModelHandle<Buffer>, buffer: ModelHandle<Buffer>,
server_id: LanguageServerId,
mut cx: AsyncAppContext, mut cx: AsyncAppContext,
) -> Result<ProjectTransaction> { ) -> Result<ProjectTransaction> {
if let Some(edit) = message { if let Some(edit) = message {
let (lsp_adapter, lsp_server) = language_server_for_buffer(&project, &buffer, &mut cx)?; let (lsp_adapter, lsp_server) =
language_server_for_buffer(&project, &buffer, server_id, &mut cx)?;
Project::deserialize_workspace_edit( Project::deserialize_workspace_edit(
project, project,
edit, edit,
@ -380,9 +389,10 @@ impl LspCommand for GetDefinition {
message: Option<lsp::GotoDefinitionResponse>, message: Option<lsp::GotoDefinitionResponse>,
project: ModelHandle<Project>, project: ModelHandle<Project>,
buffer: ModelHandle<Buffer>, buffer: ModelHandle<Buffer>,
server_id: LanguageServerId,
cx: AsyncAppContext, cx: AsyncAppContext,
) -> Result<Vec<LocationLink>> { ) -> Result<Vec<LocationLink>> {
location_links_from_lsp(message, project, buffer, cx).await location_links_from_lsp(message, project, buffer, server_id, cx).await
} }
fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::GetDefinition { fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::GetDefinition {
@ -472,9 +482,10 @@ impl LspCommand for GetTypeDefinition {
message: Option<lsp::GotoTypeDefinitionResponse>, message: Option<lsp::GotoTypeDefinitionResponse>,
project: ModelHandle<Project>, project: ModelHandle<Project>,
buffer: ModelHandle<Buffer>, buffer: ModelHandle<Buffer>,
server_id: LanguageServerId,
cx: AsyncAppContext, cx: AsyncAppContext,
) -> Result<Vec<LocationLink>> { ) -> Result<Vec<LocationLink>> {
location_links_from_lsp(message, project, buffer, cx).await location_links_from_lsp(message, project, buffer, server_id, cx).await
} }
fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::GetTypeDefinition { fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::GetTypeDefinition {
@ -537,12 +548,13 @@ impl LspCommand for GetTypeDefinition {
fn language_server_for_buffer( fn language_server_for_buffer(
project: &ModelHandle<Project>, project: &ModelHandle<Project>,
buffer: &ModelHandle<Buffer>, buffer: &ModelHandle<Buffer>,
server_id: LanguageServerId,
cx: &mut AsyncAppContext, cx: &mut AsyncAppContext,
) -> Result<(Arc<CachedLspAdapter>, Arc<LanguageServer>)> { ) -> Result<(Arc<CachedLspAdapter>, Arc<LanguageServer>)> {
project project
.read_with(cx, |project, cx| { .read_with(cx, |project, cx| {
project project
.language_server_for_buffer(buffer.read(cx), cx) .language_server_for_buffer(buffer.read(cx), server_id, cx)
.map(|(adapter, server)| (adapter.clone(), server.clone())) .map(|(adapter, server)| (adapter.clone(), server.clone()))
}) })
.ok_or_else(|| anyhow!("no language server found for buffer")) .ok_or_else(|| anyhow!("no language server found for buffer"))
@ -614,6 +626,7 @@ async fn location_links_from_lsp(
message: Option<lsp::GotoDefinitionResponse>, message: Option<lsp::GotoDefinitionResponse>,
project: ModelHandle<Project>, project: ModelHandle<Project>,
buffer: ModelHandle<Buffer>, buffer: ModelHandle<Buffer>,
server_id: LanguageServerId,
mut cx: AsyncAppContext, mut cx: AsyncAppContext,
) -> Result<Vec<LocationLink>> { ) -> Result<Vec<LocationLink>> {
let message = match message { let message = match message {
@ -642,7 +655,8 @@ async fn location_links_from_lsp(
} }
} }
let (lsp_adapter, language_server) = language_server_for_buffer(&project, &buffer, &mut cx)?; let (lsp_adapter, language_server) =
language_server_for_buffer(&project, &buffer, server_id, &mut cx)?;
let mut definitions = Vec::new(); let mut definitions = Vec::new();
for (origin_range, target_uri, target_range) in unresolved_links { for (origin_range, target_uri, target_range) in unresolved_links {
let target_buffer_handle = project let target_buffer_handle = project
@ -756,11 +770,12 @@ impl LspCommand for GetReferences {
locations: Option<Vec<lsp::Location>>, locations: Option<Vec<lsp::Location>>,
project: ModelHandle<Project>, project: ModelHandle<Project>,
buffer: ModelHandle<Buffer>, buffer: ModelHandle<Buffer>,
server_id: LanguageServerId,
mut cx: AsyncAppContext, mut cx: AsyncAppContext,
) -> Result<Vec<Location>> { ) -> Result<Vec<Location>> {
let mut references = Vec::new(); let mut references = Vec::new();
let (lsp_adapter, language_server) = let (lsp_adapter, language_server) =
language_server_for_buffer(&project, &buffer, &mut cx)?; language_server_for_buffer(&project, &buffer, server_id, &mut cx)?;
if let Some(locations) = locations { if let Some(locations) = locations {
for lsp_location in locations { for lsp_location in locations {
@ -917,6 +932,7 @@ impl LspCommand for GetDocumentHighlights {
lsp_highlights: Option<Vec<lsp::DocumentHighlight>>, lsp_highlights: Option<Vec<lsp::DocumentHighlight>>,
_: ModelHandle<Project>, _: ModelHandle<Project>,
buffer: ModelHandle<Buffer>, buffer: ModelHandle<Buffer>,
_: LanguageServerId,
cx: AsyncAppContext, cx: AsyncAppContext,
) -> Result<Vec<DocumentHighlight>> { ) -> Result<Vec<DocumentHighlight>> {
buffer.read_with(&cx, |buffer, _| { buffer.read_with(&cx, |buffer, _| {
@ -1062,6 +1078,7 @@ impl LspCommand for GetHover {
message: Option<lsp::Hover>, message: Option<lsp::Hover>,
_: ModelHandle<Project>, _: ModelHandle<Project>,
buffer: ModelHandle<Buffer>, buffer: ModelHandle<Buffer>,
_: LanguageServerId,
cx: AsyncAppContext, cx: AsyncAppContext,
) -> Result<Self::Response> { ) -> Result<Self::Response> {
Ok(message.and_then(|hover| { Ok(message.and_then(|hover| {
@ -1283,6 +1300,7 @@ impl LspCommand for GetCompletions {
completions: Option<lsp::CompletionResponse>, completions: Option<lsp::CompletionResponse>,
_: ModelHandle<Project>, _: ModelHandle<Project>,
buffer: ModelHandle<Buffer>, buffer: ModelHandle<Buffer>,
_: LanguageServerId,
cx: AsyncAppContext, cx: AsyncAppContext,
) -> Result<Vec<Completion>> { ) -> Result<Vec<Completion>> {
let completions = if let Some(completions) = completions { let completions = if let Some(completions) = completions {
@ -1502,6 +1520,7 @@ impl LspCommand for GetCodeActions {
actions: Option<lsp::CodeActionResponse>, actions: Option<lsp::CodeActionResponse>,
_: ModelHandle<Project>, _: ModelHandle<Project>,
_: ModelHandle<Buffer>, _: ModelHandle<Buffer>,
server_id: LanguageServerId,
_: AsyncAppContext, _: AsyncAppContext,
) -> Result<Vec<CodeAction>> { ) -> Result<Vec<CodeAction>> {
Ok(actions Ok(actions
@ -1510,6 +1529,7 @@ impl LspCommand for GetCodeActions {
.filter_map(|entry| { .filter_map(|entry| {
if let lsp::CodeActionOrCommand::CodeAction(lsp_action) = entry { if let lsp::CodeActionOrCommand::CodeAction(lsp_action) = entry {
Some(CodeAction { Some(CodeAction {
server_id,
range: self.range.clone(), range: self.range.clone(),
lsp_action, lsp_action,
}) })

File diff suppressed because it is too large Load diff

View file

@ -303,6 +303,7 @@ async fn test_managing_language_servers(
rust_buffer2.update(cx, |buffer, cx| { rust_buffer2.update(cx, |buffer, cx| {
buffer.update_diagnostics( buffer.update_diagnostics(
LanguageServerId(0),
DiagnosticSet::from_sorted_entries( DiagnosticSet::from_sorted_entries(
vec![DiagnosticEntry { vec![DiagnosticEntry {
diagnostic: Default::default(), diagnostic: Default::default(),
@ -399,7 +400,7 @@ async fn test_managing_language_servers(
.text_document, .text_document,
lsp::TextDocumentItem { lsp::TextDocumentItem {
uri: lsp::Url::from_file_path("/the-root/test.rs").unwrap(), uri: lsp::Url::from_file_path("/the-root/test.rs").unwrap(),
version: 1, version: 0,
text: rust_buffer.read_with(cx, |buffer, _| buffer.text()), text: rust_buffer.read_with(cx, |buffer, _| buffer.text()),
language_id: Default::default() language_id: Default::default()
} }
@ -426,7 +427,7 @@ async fn test_managing_language_servers(
}, },
lsp::TextDocumentItem { lsp::TextDocumentItem {
uri: lsp::Url::from_file_path("/the-root/test3.json").unwrap(), uri: lsp::Url::from_file_path("/the-root/test3.json").unwrap(),
version: 1, version: 0,
text: rust_buffer2.read_with(cx, |buffer, _| buffer.text()), text: rust_buffer2.read_with(cx, |buffer, _| buffer.text()),
language_id: Default::default() language_id: Default::default()
} }
@ -581,7 +582,7 @@ async fn test_single_file_worktrees_diagnostics(cx: &mut gpui::TestAppContext) {
project.update(cx, |project, cx| { project.update(cx, |project, cx| {
project project
.update_diagnostics( .update_diagnostics(
0, LanguageServerId(0),
lsp::PublishDiagnosticsParams { lsp::PublishDiagnosticsParams {
uri: Url::from_file_path("/dir/a.rs").unwrap(), uri: Url::from_file_path("/dir/a.rs").unwrap(),
version: None, version: None,
@ -598,7 +599,7 @@ async fn test_single_file_worktrees_diagnostics(cx: &mut gpui::TestAppContext) {
.unwrap(); .unwrap();
project project
.update_diagnostics( .update_diagnostics(
0, LanguageServerId(0),
lsp::PublishDiagnosticsParams { lsp::PublishDiagnosticsParams {
uri: Url::from_file_path("/dir/b.rs").unwrap(), uri: Url::from_file_path("/dir/b.rs").unwrap(),
version: None, version: None,
@ -674,7 +675,7 @@ async fn test_hidden_worktrees_diagnostics(cx: &mut gpui::TestAppContext) {
project.update(cx, |project, cx| { project.update(cx, |project, cx| {
project project
.update_diagnostics( .update_diagnostics(
0, LanguageServerId(0),
lsp::PublishDiagnosticsParams { lsp::PublishDiagnosticsParams {
uri: Url::from_file_path("/root/other.rs").unwrap(), uri: Url::from_file_path("/root/other.rs").unwrap(),
version: None, version: None,
@ -766,7 +767,7 @@ async fn test_disk_based_diagnostics_progress(cx: &mut gpui::TestAppContext) {
assert_eq!( assert_eq!(
events.next().await.unwrap(), events.next().await.unwrap(),
Event::DiskBasedDiagnosticsStarted { Event::DiskBasedDiagnosticsStarted {
language_server_id: 0, language_server_id: LanguageServerId(0),
} }
); );
@ -783,7 +784,7 @@ async fn test_disk_based_diagnostics_progress(cx: &mut gpui::TestAppContext) {
assert_eq!( assert_eq!(
events.next().await.unwrap(), events.next().await.unwrap(),
Event::DiagnosticsUpdated { Event::DiagnosticsUpdated {
language_server_id: 0, language_server_id: LanguageServerId(0),
path: (worktree_id, Path::new("a.rs")).into() path: (worktree_id, Path::new("a.rs")).into()
} }
); );
@ -792,7 +793,7 @@ async fn test_disk_based_diagnostics_progress(cx: &mut gpui::TestAppContext) {
assert_eq!( assert_eq!(
events.next().await.unwrap(), events.next().await.unwrap(),
Event::DiskBasedDiagnosticsFinished { Event::DiskBasedDiagnosticsFinished {
language_server_id: 0 language_server_id: LanguageServerId(0)
} }
); );
@ -830,7 +831,7 @@ async fn test_disk_based_diagnostics_progress(cx: &mut gpui::TestAppContext) {
assert_eq!( assert_eq!(
events.next().await.unwrap(), events.next().await.unwrap(),
Event::DiagnosticsUpdated { Event::DiagnosticsUpdated {
language_server_id: 0, language_server_id: LanguageServerId(0),
path: (worktree_id, Path::new("a.rs")).into() path: (worktree_id, Path::new("a.rs")).into()
} }
); );
@ -891,7 +892,7 @@ async fn test_restarting_server_with_diagnostics_running(cx: &mut gpui::TestAppC
assert_eq!( assert_eq!(
events.next().await.unwrap(), events.next().await.unwrap(),
Event::DiskBasedDiagnosticsStarted { Event::DiskBasedDiagnosticsStarted {
language_server_id: 1 language_server_id: LanguageServerId(1)
} }
); );
project.read_with(cx, |project, _| { project.read_with(cx, |project, _| {
@ -899,7 +900,7 @@ async fn test_restarting_server_with_diagnostics_running(cx: &mut gpui::TestAppC
project project
.language_servers_running_disk_based_diagnostics() .language_servers_running_disk_based_diagnostics()
.collect::<Vec<_>>(), .collect::<Vec<_>>(),
[1] [LanguageServerId(1)]
); );
}); });
@ -909,7 +910,7 @@ async fn test_restarting_server_with_diagnostics_running(cx: &mut gpui::TestAppC
assert_eq!( assert_eq!(
events.next().await.unwrap(), events.next().await.unwrap(),
Event::DiskBasedDiagnosticsFinished { Event::DiskBasedDiagnosticsFinished {
language_server_id: 1 language_server_id: LanguageServerId(1)
} }
); );
project.read_with(cx, |project, _| { project.read_with(cx, |project, _| {
@ -917,7 +918,7 @@ async fn test_restarting_server_with_diagnostics_running(cx: &mut gpui::TestAppC
project project
.language_servers_running_disk_based_diagnostics() .language_servers_running_disk_based_diagnostics()
.collect::<Vec<_>>(), .collect::<Vec<_>>(),
[0; 0] [LanguageServerId(0); 0]
); );
}); });
} }
@ -1402,6 +1403,8 @@ async fn test_empty_diagnostic_ranges(cx: &mut gpui::TestAppContext) {
project project
.update_buffer_diagnostics( .update_buffer_diagnostics(
&buffer, &buffer,
LanguageServerId(0),
None,
vec![ vec![
DiagnosticEntry { DiagnosticEntry {
range: Unclipped(PointUtf16::new(0, 10))..Unclipped(PointUtf16::new(0, 10)), range: Unclipped(PointUtf16::new(0, 10))..Unclipped(PointUtf16::new(0, 10)),
@ -1420,7 +1423,6 @@ async fn test_empty_diagnostic_ranges(cx: &mut gpui::TestAppContext) {
}, },
}, },
], ],
None,
cx, cx,
) )
.unwrap(); .unwrap();
@ -1447,6 +1449,64 @@ async fn test_empty_diagnostic_ranges(cx: &mut gpui::TestAppContext) {
}); });
} }
#[gpui::test]
async fn test_diagnostics_from_multiple_language_servers(cx: &mut gpui::TestAppContext) {
println!("hello from stdout");
eprintln!("hello from stderr");
cx.foreground().forbid_parking();
let fs = FakeFs::new(cx.background());
fs.insert_tree("/dir", json!({ "a.rs": "one two three" }))
.await;
let project = Project::test(fs, ["/dir".as_ref()], cx).await;
project.update(cx, |project, cx| {
project
.update_diagnostic_entries(
LanguageServerId(0),
Path::new("/dir/a.rs").to_owned(),
None,
vec![DiagnosticEntry {
range: Unclipped(PointUtf16::new(0, 0))..Unclipped(PointUtf16::new(0, 3)),
diagnostic: Diagnostic {
severity: DiagnosticSeverity::ERROR,
is_primary: true,
message: "syntax error a1".to_string(),
..Default::default()
},
}],
cx,
)
.unwrap();
project
.update_diagnostic_entries(
LanguageServerId(1),
Path::new("/dir/a.rs").to_owned(),
None,
vec![DiagnosticEntry {
range: Unclipped(PointUtf16::new(0, 0))..Unclipped(PointUtf16::new(0, 3)),
diagnostic: Diagnostic {
severity: DiagnosticSeverity::ERROR,
is_primary: true,
message: "syntax error b1".to_string(),
..Default::default()
},
}],
cx,
)
.unwrap();
assert_eq!(
project.diagnostic_summary(cx),
DiagnosticSummary {
error_count: 2,
warning_count: 0,
}
);
});
}
#[gpui::test] #[gpui::test]
async fn test_edits_from_lsp_with_past_version(cx: &mut gpui::TestAppContext) { async fn test_edits_from_lsp_with_past_version(cx: &mut gpui::TestAppContext) {
cx.foreground().forbid_parking(); cx.foreground().forbid_parking();
@ -1573,6 +1633,7 @@ async fn test_edits_from_lsp_with_past_version(cx: &mut gpui::TestAppContext) {
new_text: "".into(), new_text: "".into(),
}, },
], ],
LanguageServerId(0),
Some(lsp_document_version), Some(lsp_document_version),
cx, cx,
) )
@ -1667,6 +1728,7 @@ async fn test_edits_from_lsp_with_edits_on_adjacent_lines(cx: &mut gpui::TestApp
new_text: "".into(), new_text: "".into(),
}, },
], ],
LanguageServerId(0),
None, None,
cx, cx,
) )
@ -1770,6 +1832,7 @@ async fn test_invalid_edits_from_lsp(cx: &mut gpui::TestAppContext) {
.unindent(), .unindent(),
}, },
], ],
LanguageServerId(0),
None, None,
cx, cx,
) )
@ -2258,7 +2321,7 @@ async fn test_save_as(cx: &mut gpui::TestAppContext) {
..Default::default() ..Default::default()
}, },
tree_sitter_rust::language(), tree_sitter_rust::language(),
None, vec![],
|_| Default::default(), |_| Default::default(),
); );
@ -2948,7 +3011,9 @@ async fn test_grouped_diagnostics(cx: &mut gpui::TestAppContext) {
}; };
project project
.update(cx, |p, cx| p.update_diagnostics(0, message, &[], cx)) .update(cx, |p, cx| {
p.update_diagnostics(LanguageServerId(0), message, &[], cx)
})
.unwrap(); .unwrap();
let buffer = buffer.read_with(cx, |buffer, _| buffer.snapshot()); let buffer = buffer.read_with(cx, |buffer, _| buffer.snapshot());

View file

@ -26,6 +26,7 @@ use language::{
}, },
Buffer, DiagnosticEntry, File as _, PointUtf16, Rope, RopeFingerprint, Unclipped, Buffer, DiagnosticEntry, File as _, PointUtf16, Rope, RopeFingerprint, Unclipped,
}; };
use lsp::LanguageServerId;
use parking_lot::Mutex; use parking_lot::Mutex;
use postage::{ use postage::{
barrier, barrier,
@ -50,7 +51,7 @@ use std::{
}, },
time::{Duration, SystemTime}, time::{Duration, SystemTime},
}; };
use sum_tree::{Bias, Edit, SeekTarget, SumTree, TreeMap, TreeSet}; use sum_tree::{Bias, Edit, SeekTarget, SumTree, TreeSet};
use util::{paths::HOME, ResultExt, TryFutureExt}; use util::{paths::HOME, ResultExt, TryFutureExt};
#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash, PartialOrd, Ord)] #[derive(Copy, Clone, PartialEq, Eq, Debug, Hash, PartialOrd, Ord)]
@ -67,8 +68,14 @@ pub struct LocalWorktree {
is_scanning: (watch::Sender<bool>, watch::Receiver<bool>), is_scanning: (watch::Sender<bool>, watch::Receiver<bool>),
_background_scanner_task: Task<()>, _background_scanner_task: Task<()>,
share: Option<ShareState>, share: Option<ShareState>,
diagnostics: HashMap<Arc<Path>, Vec<DiagnosticEntry<Unclipped<PointUtf16>>>>, diagnostics: HashMap<
diagnostic_summaries: TreeMap<PathKey, DiagnosticSummary>, Arc<Path>,
Vec<(
LanguageServerId,
Vec<DiagnosticEntry<Unclipped<PointUtf16>>>,
)>,
>,
diagnostic_summaries: HashMap<Arc<Path>, HashMap<LanguageServerId, DiagnosticSummary>>,
client: Arc<Client>, client: Arc<Client>,
fs: Arc<dyn Fs>, fs: Arc<dyn Fs>,
visible: bool, visible: bool,
@ -82,7 +89,7 @@ pub struct RemoteWorktree {
updates_tx: Option<UnboundedSender<proto::UpdateWorktree>>, updates_tx: Option<UnboundedSender<proto::UpdateWorktree>>,
snapshot_subscriptions: VecDeque<(usize, oneshot::Sender<()>)>, snapshot_subscriptions: VecDeque<(usize, oneshot::Sender<()>)>,
replica_id: ReplicaId, replica_id: ReplicaId,
diagnostic_summaries: TreeMap<PathKey, DiagnosticSummary>, diagnostic_summaries: HashMap<Arc<Path>, HashMap<LanguageServerId, DiagnosticSummary>>,
visible: bool, visible: bool,
disconnected: bool, disconnected: bool,
} }
@ -463,13 +470,17 @@ impl Worktree {
pub fn diagnostic_summaries( pub fn diagnostic_summaries(
&self, &self,
) -> impl Iterator<Item = (Arc<Path>, DiagnosticSummary)> + '_ { ) -> impl Iterator<Item = (Arc<Path>, LanguageServerId, DiagnosticSummary)> + '_ {
match self { match self {
Worktree::Local(worktree) => &worktree.diagnostic_summaries, Worktree::Local(worktree) => &worktree.diagnostic_summaries,
Worktree::Remote(worktree) => &worktree.diagnostic_summaries, Worktree::Remote(worktree) => &worktree.diagnostic_summaries,
} }
.iter() .iter()
.map(|(path, summary)| (path.0.clone(), *summary)) .flat_map(|(path, summaries)| {
summaries
.iter()
.map(move |(&server_id, &summary)| (path.clone(), server_id, summary))
})
} }
pub fn abs_path(&self) -> Arc<Path> { pub fn abs_path(&self) -> Arc<Path> {
@ -514,31 +525,54 @@ impl LocalWorktree {
pub fn diagnostics_for_path( pub fn diagnostics_for_path(
&self, &self,
path: &Path, path: &Path,
) -> Option<Vec<DiagnosticEntry<Unclipped<PointUtf16>>>> { ) -> Vec<(
self.diagnostics.get(path).cloned() LanguageServerId,
Vec<DiagnosticEntry<Unclipped<PointUtf16>>>,
)> {
self.diagnostics.get(path).cloned().unwrap_or_default()
} }
pub fn update_diagnostics( pub fn update_diagnostics(
&mut self, &mut self,
language_server_id: usize, server_id: LanguageServerId,
worktree_path: Arc<Path>, worktree_path: Arc<Path>,
diagnostics: Vec<DiagnosticEntry<Unclipped<PointUtf16>>>, diagnostics: Vec<DiagnosticEntry<Unclipped<PointUtf16>>>,
_: &mut ModelContext<Worktree>, _: &mut ModelContext<Worktree>,
) -> Result<bool> { ) -> Result<bool> {
self.diagnostics.remove(&worktree_path); let summaries_by_server_id = self
let old_summary = self
.diagnostic_summaries .diagnostic_summaries
.remove(&PathKey(worktree_path.clone())) .entry(worktree_path.clone())
.or_default();
let old_summary = summaries_by_server_id
.remove(&server_id)
.unwrap_or_default(); .unwrap_or_default();
let new_summary = DiagnosticSummary::new(language_server_id, &diagnostics);
if !new_summary.is_empty() { let new_summary = DiagnosticSummary::new(&diagnostics);
self.diagnostic_summaries if new_summary.is_empty() {
.insert(PathKey(worktree_path.clone()), new_summary); if let Some(diagnostics_by_server_id) = self.diagnostics.get_mut(&worktree_path) {
self.diagnostics.insert(worktree_path.clone(), diagnostics); if let Ok(ix) = diagnostics_by_server_id.binary_search_by_key(&server_id, |e| e.0) {
diagnostics_by_server_id.remove(ix);
}
if diagnostics_by_server_id.is_empty() {
self.diagnostics.remove(&worktree_path);
}
}
} else {
summaries_by_server_id.insert(server_id, new_summary);
let diagnostics_by_server_id =
self.diagnostics.entry(worktree_path.clone()).or_default();
match diagnostics_by_server_id.binary_search_by_key(&server_id, |e| e.0) {
Ok(ix) => {
diagnostics_by_server_id[ix] = (server_id, diagnostics);
}
Err(ix) => {
diagnostics_by_server_id.insert(ix, (server_id, diagnostics));
}
}
} }
let updated = !old_summary.is_empty() || !new_summary.is_empty(); if !old_summary.is_empty() || !new_summary.is_empty() {
if updated {
if let Some(share) = self.share.as_ref() { if let Some(share) = self.share.as_ref() {
self.client self.client
.send(proto::UpdateDiagnosticSummary { .send(proto::UpdateDiagnosticSummary {
@ -546,7 +580,7 @@ impl LocalWorktree {
worktree_id: self.id().to_proto(), worktree_id: self.id().to_proto(),
summary: Some(proto::DiagnosticSummary { summary: Some(proto::DiagnosticSummary {
path: worktree_path.to_string_lossy().to_string(), path: worktree_path.to_string_lossy().to_string(),
language_server_id: language_server_id as u64, language_server_id: server_id.0 as u64,
error_count: new_summary.error_count as u32, error_count: new_summary.error_count as u32,
warning_count: new_summary.warning_count as u32, warning_count: new_summary.warning_count as u32,
}), }),
@ -555,7 +589,7 @@ impl LocalWorktree {
} }
} }
Ok(updated) Ok(!old_summary.is_empty() || !new_summary.is_empty())
} }
fn set_snapshot(&mut self, new_snapshot: LocalSnapshot, cx: &mut ModelContext<Worktree>) { fn set_snapshot(&mut self, new_snapshot: LocalSnapshot, cx: &mut ModelContext<Worktree>) {
@ -945,13 +979,15 @@ impl LocalWorktree {
let (resume_updates_tx, mut resume_updates_rx) = watch::channel(); let (resume_updates_tx, mut resume_updates_rx) = watch::channel();
let worktree_id = cx.model_id() as u64; let worktree_id = cx.model_id() as u64;
for (path, summary) in self.diagnostic_summaries.iter() { for (path, summaries) in &self.diagnostic_summaries {
if let Err(e) = self.client.send(proto::UpdateDiagnosticSummary { for (&server_id, summary) in summaries {
project_id, if let Err(e) = self.client.send(proto::UpdateDiagnosticSummary {
worktree_id, project_id,
summary: Some(summary.to_proto(&path.0)), worktree_id,
}) { summary: Some(summary.to_proto(server_id, &path)),
return Task::ready(Err(e)); }) {
return Task::ready(Err(e));
}
} }
} }
@ -1109,15 +1145,24 @@ impl RemoteWorktree {
path: Arc<Path>, path: Arc<Path>,
summary: &proto::DiagnosticSummary, summary: &proto::DiagnosticSummary,
) { ) {
let server_id = LanguageServerId(summary.language_server_id as usize);
let summary = DiagnosticSummary { let summary = DiagnosticSummary {
language_server_id: summary.language_server_id as usize,
error_count: summary.error_count as usize, error_count: summary.error_count as usize,
warning_count: summary.warning_count as usize, warning_count: summary.warning_count as usize,
}; };
if summary.is_empty() { if summary.is_empty() {
self.diagnostic_summaries.remove(&PathKey(path)); if let Some(summaries) = self.diagnostic_summaries.get_mut(&path) {
summaries.remove(&server_id);
if summaries.is_empty() {
self.diagnostic_summaries.remove(&path);
}
}
} else { } else {
self.diagnostic_summaries.insert(PathKey(path), summary); self.diagnostic_summaries
.entry(path)
.or_default()
.insert(server_id, summary);
} }
} }

View file

@ -9,7 +9,7 @@ path = "src/rope.rs"
[dependencies] [dependencies]
bromberg_sl2 = { git = "https://github.com/zed-industries/bromberg_sl2", rev = "950bc5482c216c395049ae33ae4501e08975f17f" } bromberg_sl2 = { git = "https://github.com/zed-industries/bromberg_sl2", rev = "950bc5482c216c395049ae33ae4501e08975f17f" }
smallvec = { version = "1.6", features = ["union"] } smallvec = { workspace = true }
sum_tree = { path = "../sum_tree" } sum_tree = { path = "../sum_tree" }
arrayvec = "0.7.1" arrayvec = "0.7.1"
log = { version = "0.4.16", features = ["kv_unstable_serde"] } log = { version = "0.4.16", features = ["kv_unstable_serde"] }

View file

@ -684,9 +684,10 @@ message SearchProjectResponse {
} }
message CodeAction { message CodeAction {
Anchor start = 1; uint64 server_id = 1;
Anchor end = 2; Anchor start = 2;
bytes lsp_action = 3; Anchor end = 3;
bytes lsp_action = 4;
} }
message ProjectTransaction { message ProjectTransaction {
@ -860,7 +861,8 @@ message IncomingContactRequest {
message UpdateDiagnostics { message UpdateDiagnostics {
uint32 replica_id = 1; uint32 replica_id = 1;
uint32 lamport_timestamp = 2; uint32 lamport_timestamp = 2;
repeated Diagnostic diagnostics = 3; uint64 server_id = 3;
repeated Diagnostic diagnostics = 4;
} }
message Follow { message Follow {

View file

@ -6,4 +6,4 @@ pub use conn::Connection;
pub use peer::*; pub use peer::*;
mod macros; mod macros;
pub const PROTOCOL_VERSION: u32 = 51; pub const PROTOCOL_VERSION: u32 = 52;

View file

@ -25,7 +25,7 @@ log = { version = "0.4.16", features = ["kv_unstable_serde"] }
postage = { workspace = true } postage = { workspace = true }
serde = { workspace = true } serde = { workspace = true }
serde_derive = { workspace = true } serde_derive = { workspace = true }
smallvec = { version = "1.6", features = ["union"] } smallvec = { workspace = true }
smol = "1.2" smol = "1.2"
[dev-dependencies] [dev-dependencies]

View file

@ -10,4 +10,4 @@ doctest = false
[dependencies] [dependencies]
anyhow = "1.0" anyhow = "1.0"
smallvec = { version = "1.6", features = ["union"] } smallvec = { workspace = true }

View file

@ -17,7 +17,7 @@ theme = { path = "../theme" }
util = { path = "../util" } util = { path = "../util" }
alacritty_terminal = { git = "https://github.com/zed-industries/alacritty", rev = "a51dbe25d67e84d6ed4261e640d3954fbdd9be45" } alacritty_terminal = { git = "https://github.com/zed-industries/alacritty", rev = "a51dbe25d67e84d6ed4261e640d3954fbdd9be45" }
procinfo = { git = "https://github.com/zed-industries/wezterm", rev = "5cd757e5f2eb039ed0c6bb6512223e69d5efc64d", default-features = false } procinfo = { git = "https://github.com/zed-industries/wezterm", rev = "5cd757e5f2eb039ed0c6bb6512223e69d5efc64d", default-features = false }
smallvec = { version = "1.6", features = ["union"] } smallvec = { workspace = true }
smol = "1.2.5" smol = "1.2.5"
mio-extras = "2.0.6" mio-extras = "2.0.6"
futures = "0.3" futures = "0.3"

View file

@ -21,7 +21,7 @@ workspace = { path = "../workspace" }
db = { path = "../db" } db = { path = "../db" }
procinfo = { git = "https://github.com/zed-industries/wezterm", rev = "5cd757e5f2eb039ed0c6bb6512223e69d5efc64d", default-features = false } procinfo = { git = "https://github.com/zed-industries/wezterm", rev = "5cd757e5f2eb039ed0c6bb6512223e69d5efc64d", default-features = false }
terminal = { path = "../terminal" } terminal = { path = "../terminal" }
smallvec = { version = "1.6", features = ["union"] } smallvec = { workspace = true }
smol = "1.2.5" smol = "1.2.5"
mio-extras = "2.0.6" mio-extras = "2.0.6"
futures = "0.3" futures = "0.3"

View file

@ -24,7 +24,7 @@ log = { version = "0.4.16", features = ["kv_unstable_serde"] }
parking_lot = "0.11" parking_lot = "0.11"
postage = { workspace = true } postage = { workspace = true }
rand = { version = "0.8.3", optional = true } rand = { version = "0.8.3", optional = true }
smallvec = { version = "1.6", features = ["union"] } smallvec = { workspace = true }
util = { path = "../util" } util = { path = "../util" }
regex = "1.5" regex = "1.5"

View file

@ -16,4 +16,4 @@ settings = { path = "../settings" }
workspace = { path = "../workspace" } workspace = { path = "../workspace" }
project = { path = "../project" } project = { path = "../project" }
smallvec = { version = "1.6", features = ["union"] } smallvec = { workspace = true }

View file

@ -47,7 +47,7 @@ postage = { workspace = true }
serde = { workspace = true } serde = { workspace = true }
serde_derive = { workspace = true } serde_derive = { workspace = true }
serde_json = { workspace = true } serde_json = { workspace = true }
smallvec = { version = "1.6", features = ["union"] } smallvec = { workspace = true }
indoc = "1.0.4" indoc = "1.0.4"
uuid = { version = "1.1.2", features = ["v4"] } uuid = { version = "1.1.2", features = ["v4"] }

View file

@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathansobo@gmail.com>"]
description = "The fast, collaborative code editor." description = "The fast, collaborative code editor."
edition = "2021" edition = "2021"
name = "zed" name = "zed"
version = "0.83.0" version = "0.84.0"
publish = false publish = false
[lib] [lib]
@ -96,7 +96,7 @@ serde_derive = { workspace = true }
serde_json = { workspace = true } serde_json = { workspace = true }
serde_path_to_error = "0.1.4" serde_path_to_error = "0.1.4"
simplelog = "0.9" simplelog = "0.9"
smallvec = { version = "1.6", features = ["union"] } smallvec = { workspace = true }
smol = "1.2.5" smol = "1.2.5"
tempdir = { version = "0.3.7" } tempdir = { version = "0.3.7" }
thiserror = "1.0.29" thiserror = "1.0.29"

View file

@ -37,121 +37,107 @@ pub fn init(
themes: Arc<ThemeRegistry>, themes: Arc<ThemeRegistry>,
node_runtime: Arc<NodeRuntime>, node_runtime: Arc<NodeRuntime>,
) { ) {
for (name, grammar, lsp_adapter) in [ fn adapter_arc(adapter: impl LspAdapter) -> Arc<dyn LspAdapter> {
Arc::new(adapter)
}
let languages_list = [
( (
"c", "c",
tree_sitter_c::language(), tree_sitter_c::language(),
Some(Arc::new(c::CLspAdapter) as Arc<dyn LspAdapter>), vec![adapter_arc(c::CLspAdapter)],
), ),
( (
"cpp", "cpp",
tree_sitter_cpp::language(), tree_sitter_cpp::language(),
Some(Arc::new(c::CLspAdapter)), vec![adapter_arc(c::CLspAdapter)],
),
(
"css",
tree_sitter_css::language(),
None, //
), ),
("css", tree_sitter_css::language(), vec![]),
( (
"elixir", "elixir",
tree_sitter_elixir::language(), tree_sitter_elixir::language(),
Some(Arc::new(elixir::ElixirLspAdapter)), vec![adapter_arc(elixir::ElixirLspAdapter)],
), ),
( (
"go", "go",
tree_sitter_go::language(), tree_sitter_go::language(),
Some(Arc::new(go::GoLspAdapter)), vec![adapter_arc(go::GoLspAdapter)],
), ),
( (
"json", "json",
tree_sitter_json::language(), tree_sitter_json::language(),
Some(Arc::new(json::JsonLspAdapter::new( vec![adapter_arc(json::JsonLspAdapter::new(
node_runtime.clone(), node_runtime.clone(),
languages.clone(), languages.clone(),
themes.clone(), themes.clone(),
))), ))],
),
(
"markdown",
tree_sitter_markdown::language(),
None, //
), ),
("markdown", tree_sitter_markdown::language(), vec![]),
( (
"python", "python",
tree_sitter_python::language(), tree_sitter_python::language(),
Some(Arc::new(python::PythonLspAdapter::new( vec![adapter_arc(python::PythonLspAdapter::new(
node_runtime.clone(), node_runtime.clone(),
))), ))],
), ),
( (
"rust", "rust",
tree_sitter_rust::language(), tree_sitter_rust::language(),
Some(Arc::new(rust::RustLspAdapter)), vec![adapter_arc(rust::RustLspAdapter)],
),
(
"toml",
tree_sitter_toml::language(),
None, //
), ),
("toml", tree_sitter_toml::language(), vec![]),
( (
"tsx", "tsx",
tree_sitter_typescript::language_tsx(), tree_sitter_typescript::language_tsx(),
Some(Arc::new(typescript::TypeScriptLspAdapter::new( vec![adapter_arc(typescript::TypeScriptLspAdapter::new(
node_runtime.clone(), node_runtime.clone(),
))), ))],
), ),
( (
"typescript", "typescript",
tree_sitter_typescript::language_typescript(), tree_sitter_typescript::language_typescript(),
Some(Arc::new(typescript::TypeScriptLspAdapter::new( vec![adapter_arc(typescript::TypeScriptLspAdapter::new(
node_runtime.clone(), node_runtime.clone(),
))), ))],
), ),
( (
"javascript", "javascript",
tree_sitter_typescript::language_tsx(), tree_sitter_typescript::language_tsx(),
Some(Arc::new(typescript::TypeScriptLspAdapter::new( vec![adapter_arc(typescript::TypeScriptLspAdapter::new(
node_runtime.clone(), node_runtime.clone(),
))), ))],
), ),
( (
"html", "html",
tree_sitter_html::language(), tree_sitter_html::language(),
Some(Arc::new(html::HtmlLspAdapter::new(node_runtime.clone()))), vec![adapter_arc(html::HtmlLspAdapter::new(node_runtime.clone()))],
), ),
( (
"ruby", "ruby",
tree_sitter_ruby::language(), tree_sitter_ruby::language(),
Some(Arc::new(ruby::RubyLanguageServer)), vec![adapter_arc(ruby::RubyLanguageServer)],
), ),
( (
"erb", "erb",
tree_sitter_embedded_template::language(), tree_sitter_embedded_template::language(),
Some(Arc::new(ruby::RubyLanguageServer)), vec![adapter_arc(ruby::RubyLanguageServer)],
),
(
"scheme",
tree_sitter_scheme::language(),
None, //
),
(
"racket",
tree_sitter_racket::language(),
None, //
), ),
("scheme", tree_sitter_scheme::language(), vec![]),
("racket", tree_sitter_racket::language(), vec![]),
( (
"lua", "lua",
tree_sitter_lua::language(), tree_sitter_lua::language(),
Some(Arc::new(lua::LuaLspAdapter)), vec![adapter_arc(lua::LuaLspAdapter)],
), ),
( (
"yaml", "yaml",
tree_sitter_yaml::language(), tree_sitter_yaml::language(),
Some(Arc::new(yaml::YamlLspAdapter::new(node_runtime.clone()))), vec![adapter_arc(yaml::YamlLspAdapter::new(node_runtime.clone()))],
), ),
] { ];
languages.register(name, load_config(name), grammar, lsp_adapter, load_queries);
for (name, grammar, lsp_adapters) in languages_list {
languages.register(name, load_config(name), grammar, lsp_adapters, load_queries);
} }
} }
@ -163,7 +149,7 @@ pub async fn language(
) -> Arc<Language> { ) -> Arc<Language> {
Arc::new( Arc::new(
Language::new(load_config(name), Some(grammar)) Language::new(load_config(name), Some(grammar))
.with_lsp_adapter(lsp_adapter) .with_lsp_adapters(lsp_adapter.into_iter().collect())
.await .await
.with_queries(load_queries(name)) .with_queries(load_queries(name))
.unwrap(), .unwrap(),

View file

@ -1,21 +1,23 @@
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use async_trait::async_trait; use async_trait::async_trait;
use futures::StreamExt; use futures::{future::BoxFuture, FutureExt};
use gpui::AppContext;
use language::{LanguageServerBinary, LanguageServerName, LspAdapter}; use language::{LanguageServerBinary, LanguageServerName, LspAdapter};
use lsp::CodeActionKind; use lsp::CodeActionKind;
use node_runtime::NodeRuntime; use node_runtime::NodeRuntime;
use serde_json::json; use serde_json::{json, Value};
use smol::fs; use smol::fs;
use std::{ use std::{
any::Any, any::Any,
ffi::OsString, ffi::OsString,
future,
path::{Path, PathBuf}, path::{Path, PathBuf},
sync::Arc, sync::Arc,
}; };
use util::http::HttpClient; use util::http::HttpClient;
use util::ResultExt; use util::ResultExt;
fn server_binary_arguments(server_path: &Path) -> Vec<OsString> { fn typescript_server_binary_arguments(server_path: &Path) -> Vec<OsString> {
vec![ vec![
server_path.into(), server_path.into(),
"--stdio".into(), "--stdio".into(),
@ -24,6 +26,10 @@ fn server_binary_arguments(server_path: &Path) -> Vec<OsString> {
] ]
} }
fn eslint_server_binary_arguments(server_path: &Path) -> Vec<OsString> {
vec![server_path.into(), "--stdio".into()]
}
pub struct TypeScriptLspAdapter { pub struct TypeScriptLspAdapter {
node: Arc<NodeRuntime>, node: Arc<NodeRuntime>,
} }
@ -37,7 +43,7 @@ impl TypeScriptLspAdapter {
} }
} }
struct Versions { struct TypeScriptVersions {
typescript_version: String, typescript_version: String,
server_version: String, server_version: String,
} }
@ -52,7 +58,7 @@ impl LspAdapter for TypeScriptLspAdapter {
&self, &self,
_: Arc<dyn HttpClient>, _: Arc<dyn HttpClient>,
) -> Result<Box<dyn 'static + Send + Any>> { ) -> Result<Box<dyn 'static + Send + Any>> {
Ok(Box::new(Versions { Ok(Box::new(TypeScriptVersions {
typescript_version: self.node.npm_package_latest_version("typescript").await?, typescript_version: self.node.npm_package_latest_version("typescript").await?,
server_version: self server_version: self
.node .node
@ -67,7 +73,7 @@ impl LspAdapter for TypeScriptLspAdapter {
_: Arc<dyn HttpClient>, _: Arc<dyn HttpClient>,
container_dir: PathBuf, container_dir: PathBuf,
) -> Result<LanguageServerBinary> { ) -> Result<LanguageServerBinary> {
let versions = versions.downcast::<Versions>().unwrap(); let versions = versions.downcast::<TypeScriptVersions>().unwrap();
let server_path = container_dir.join(Self::NEW_SERVER_PATH); let server_path = container_dir.join(Self::NEW_SERVER_PATH);
if fs::metadata(&server_path).await.is_err() { if fs::metadata(&server_path).await.is_err() {
@ -87,37 +93,28 @@ impl LspAdapter for TypeScriptLspAdapter {
Ok(LanguageServerBinary { Ok(LanguageServerBinary {
path: self.node.binary_path().await?, path: self.node.binary_path().await?,
arguments: server_binary_arguments(&server_path), arguments: typescript_server_binary_arguments(&server_path),
}) })
} }
async fn cached_server_binary(&self, container_dir: PathBuf) -> Option<LanguageServerBinary> { async fn cached_server_binary(&self, container_dir: PathBuf) -> Option<LanguageServerBinary> {
(|| async move { (|| async move {
let mut last_version_dir = None; let old_server_path = container_dir.join(Self::OLD_SERVER_PATH);
let mut entries = fs::read_dir(&container_dir).await?; let new_server_path = container_dir.join(Self::NEW_SERVER_PATH);
while let Some(entry) = entries.next().await {
let entry = entry?;
if entry.file_type().await?.is_dir() {
last_version_dir = Some(entry.path());
}
}
let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?;
let old_server_path = last_version_dir.join(Self::OLD_SERVER_PATH);
let new_server_path = last_version_dir.join(Self::NEW_SERVER_PATH);
if new_server_path.exists() { if new_server_path.exists() {
Ok(LanguageServerBinary { Ok(LanguageServerBinary {
path: self.node.binary_path().await?, path: self.node.binary_path().await?,
arguments: server_binary_arguments(&new_server_path), arguments: typescript_server_binary_arguments(&new_server_path),
}) })
} else if old_server_path.exists() { } else if old_server_path.exists() {
Ok(LanguageServerBinary { Ok(LanguageServerBinary {
path: self.node.binary_path().await?, path: self.node.binary_path().await?,
arguments: server_binary_arguments(&old_server_path), arguments: typescript_server_binary_arguments(&old_server_path),
}) })
} else { } else {
Err(anyhow!( Err(anyhow!(
"missing executable in directory {:?}", "missing executable in directory {:?}",
last_version_dir container_dir
)) ))
} }
})() })()
@ -170,6 +167,136 @@ impl LspAdapter for TypeScriptLspAdapter {
} }
} }
pub struct EsLintLspAdapter {
node: Arc<NodeRuntime>,
}
impl EsLintLspAdapter {
const SERVER_PATH: &'static str =
"node_modules/vscode-langservers-extracted/lib/eslint-language-server/eslintServer.js";
#[allow(unused)]
pub fn new(node: Arc<NodeRuntime>) -> Self {
EsLintLspAdapter { node }
}
}
#[async_trait]
impl LspAdapter for EsLintLspAdapter {
fn workspace_configuration(&self, _: &mut AppContext) -> Option<BoxFuture<'static, Value>> {
Some(
future::ready(json!({
"": {
"validate": "on",
"packageManager": "npm",
"useESLintClass": false,
"experimental": {
"useFlatConfig": false
},
"codeActionOnSave": {
"mode": "all"
},
"format": false,
"quiet": false,
"onIgnoredFiles": "off",
"options": {},
"rulesCustomizations": [],
"run": "onType",
"problems": {
"shortenToSingleLine": false
},
"nodePath": null,
"workspaceFolder": {
"name": "testing_ts",
"uri": "file:///Users/julia/Stuff/testing_ts"
},
"codeAction": {
"disableRuleComment": {
"enable": true,
"location": "separateLine",
"commentStyle": "line"
},
"showDocumentation": {
"enable": true
}
}
}
}))
.boxed(),
)
}
async fn name(&self) -> LanguageServerName {
LanguageServerName("eslint".into())
}
async fn fetch_latest_server_version(
&self,
_: Arc<dyn HttpClient>,
) -> Result<Box<dyn 'static + Send + Any>> {
Ok(Box::new(
self.node
.npm_package_latest_version("vscode-langservers-extracted")
.await?,
))
}
async fn fetch_server_binary(
&self,
versions: Box<dyn 'static + Send + Any>,
_: Arc<dyn HttpClient>,
container_dir: PathBuf,
) -> Result<LanguageServerBinary> {
let version = versions.downcast::<String>().unwrap();
let server_path = container_dir.join(Self::SERVER_PATH);
if fs::metadata(&server_path).await.is_err() {
self.node
.npm_install_packages(
[("vscode-langservers-extracted", version.as_str())],
&container_dir,
)
.await?;
}
Ok(LanguageServerBinary {
path: self.node.binary_path().await?,
arguments: eslint_server_binary_arguments(&server_path),
})
}
async fn cached_server_binary(&self, container_dir: PathBuf) -> Option<LanguageServerBinary> {
(|| async move {
let server_path = container_dir.join(Self::SERVER_PATH);
if server_path.exists() {
Ok(LanguageServerBinary {
path: self.node.binary_path().await?,
arguments: eslint_server_binary_arguments(&server_path),
})
} else {
Err(anyhow!(
"missing executable in directory {:?}",
container_dir
))
}
})()
.await
.log_err()
}
async fn label_for_completion(
&self,
_item: &lsp::CompletionItem,
_language: &Arc<language::Language>,
) -> Option<language::CodeLabel> {
None
}
async fn initialization_options(&self) -> Option<serde_json::Value> {
None
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use gpui::TestAppContext; use gpui::TestAppContext;

View file

@ -21,7 +21,7 @@ use log::LevelFilter;
use node_runtime::NodeRuntime; use node_runtime::NodeRuntime;
use parking_lot::Mutex; use parking_lot::Mutex;
use project::Fs; use project::Fs;
use serde_json::json; use serde::{Deserialize, Serialize};
use settings::{ use settings::{
self, settings_file::SettingsFile, KeymapFileContent, Settings, SettingsFileContent, self, settings_file::SettingsFile, KeymapFileContent, Settings, SettingsFileContent,
WorkingDirectory, WorkingDirectory,
@ -317,6 +317,30 @@ fn init_logger() {
} }
} }
#[derive(Serialize, Deserialize)]
struct LocationData {
file: String,
line: u32,
}
#[derive(Serialize, Deserialize)]
struct Panic {
thread: String,
payload: String,
#[serde(skip_serializing_if = "Option::is_none")]
location_data: Option<LocationData>,
backtrace: Vec<String>,
// TODO
// stripped_backtrace: String,
}
#[derive(Serialize)]
struct PanicRequest {
panic: Panic,
version: String,
token: String,
}
fn init_panic_hook(app_version: String) { fn init_panic_hook(app_version: String) {
let is_pty = stdout_is_a_pty(); let is_pty = stdout_is_a_pty();
panic::set_hook(Box::new(move |info| { panic::set_hook(Box::new(move |info| {
@ -333,39 +357,38 @@ fn init_panic_hook(app_version: String) {
}, },
}; };
let message = match info.location() { let panic_data = Panic {
Some(location) => { thread: thread.into(),
format!( payload: payload.into(),
"thread '{}' panicked at '{}'\n{}:{}\n{:?}", location_data: info.location().map(|location| LocationData {
thread, file: location.file().into(),
payload, line: location.line(),
location.file(), }),
location.line(), backtrace: format!("{:?}", backtrace)
backtrace .split("\n")
) .map(|line| line.to_string())
} .collect(),
None => format!( // modified_backtrace: None,
"thread '{}' panicked at '{}'\n{:?}",
thread, payload, backtrace
),
}; };
if is_pty { if let Some(panic_data_json) = serde_json::to_string_pretty(&panic_data).log_err() {
eprintln!("{}", message); if is_pty {
return; eprintln!("{}", panic_data_json);
} return;
}
let timestamp = chrono::Utc::now().format("%Y_%m_%d %H_%M_%S").to_string(); let timestamp = chrono::Utc::now().format("%Y_%m_%d %H_%M_%S").to_string();
let panic_file_path = let panic_file_path =
paths::LOGS_DIR.join(format!("zed-{}-{}.panic", app_version, timestamp)); paths::LOGS_DIR.join(format!("zed-{}-{}.panic", app_version, timestamp));
let panic_file = std::fs::OpenOptions::new() let panic_file = std::fs::OpenOptions::new()
.append(true) .append(true)
.create(true) .create(true)
.open(&panic_file_path) .open(&panic_file_path)
.log_err(); .log_err();
if let Some(mut panic_file) = panic_file { if let Some(mut panic_file) = panic_file {
write!(&mut panic_file, "{}", message).log_err(); write!(&mut panic_file, "{}", panic_data_json).log_err();
panic_file.flush().log_err(); panic_file.flush().log_err();
}
} }
})); }));
} }
@ -402,15 +425,17 @@ fn upload_previous_panics(http: Arc<dyn HttpClient>, cx: &mut AppContext) {
}; };
if diagnostics_telemetry { if diagnostics_telemetry {
let text = smol::fs::read_to_string(&child_path) let panic_data_text = smol::fs::read_to_string(&child_path)
.await .await
.context("error reading panic file")?; .context("error reading panic file")?;
let body = serde_json::to_string(&json!({
"text": text, let body = serde_json::to_string(&PanicRequest {
"version": version, panic: serde_json::from_str(&panic_data_text)?,
"token": ZED_SECRET_CLIENT_TOKEN, version: version.to_string(),
})) token: ZED_SECRET_CLIENT_TOKEN.into(),
})
.unwrap(); .unwrap();
let request = Request::post(&panic_report_url) let request = Request::post(&panic_report_url)
.redirect_policy(isahc::config::RedirectPolicy::Follow) .redirect_policy(isahc::config::RedirectPolicy::Follow)
.header("Content-Type", "application/json") .header("Content-Type", "application/json")