Merge branch 'main' into window_context_2
This commit is contained in:
commit
c52b6328b7
47 changed files with 3046 additions and 1772 deletions
5
Cargo.lock
generated
5
Cargo.lock
generated
|
@ -1213,6 +1213,7 @@ dependencies = [
|
||||||
"git",
|
"git",
|
||||||
"gpui",
|
"gpui",
|
||||||
"hyper",
|
"hyper",
|
||||||
|
"indoc",
|
||||||
"language",
|
"language",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"lipsum",
|
"lipsum",
|
||||||
|
@ -1340,14 +1341,17 @@ dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-compression",
|
"async-compression",
|
||||||
"async-tar",
|
"async-tar",
|
||||||
|
"clock",
|
||||||
"collections",
|
"collections",
|
||||||
"context_menu",
|
"context_menu",
|
||||||
|
"fs",
|
||||||
"futures 0.3.25",
|
"futures 0.3.25",
|
||||||
"gpui",
|
"gpui",
|
||||||
"language",
|
"language",
|
||||||
"log",
|
"log",
|
||||||
"lsp",
|
"lsp",
|
||||||
"node_runtime",
|
"node_runtime",
|
||||||
|
"rpc",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_derive",
|
"serde_derive",
|
||||||
"settings",
|
"settings",
|
||||||
|
@ -4687,6 +4691,7 @@ dependencies = [
|
||||||
"client",
|
"client",
|
||||||
"clock",
|
"clock",
|
||||||
"collections",
|
"collections",
|
||||||
|
"copilot",
|
||||||
"ctor",
|
"ctor",
|
||||||
"db",
|
"db",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
]
|
]
|
|
@ -1,251 +1,257 @@
|
||||||
{
|
{
|
||||||
// The name of the Zed theme to use for the UI
|
// The name of the Zed theme to use for the UI
|
||||||
"theme": "One Dark",
|
"theme": "One Dark",
|
||||||
// The name of a font to use for rendering text in the editor
|
// Features that can be globally enabled or disabled
|
||||||
"buffer_font_family": "Zed Mono",
|
"features": {
|
||||||
// The OpenType features to enable for text in the editor.
|
// Show Copilot icon in status bar
|
||||||
"buffer_font_features": {
|
"copilot": true
|
||||||
// Disable ligatures:
|
},
|
||||||
// "calt": false
|
// The name of a font to use for rendering text in the editor
|
||||||
},
|
"buffer_font_family": "Zed Mono",
|
||||||
// The default font size for text in the editor
|
// The OpenType features to enable for text in the editor.
|
||||||
"buffer_font_size": 15,
|
"buffer_font_features": {
|
||||||
// The factor to grow the active pane by. Defaults to 1.0
|
// Disable ligatures:
|
||||||
// which gives the same size as all other panes.
|
// "calt": false
|
||||||
"active_pane_magnification": 1.0,
|
},
|
||||||
// Enable / disable copilot integration.
|
// The default font size for text in the editor
|
||||||
"enable_copilot_integration": true,
|
"buffer_font_size": 15,
|
||||||
// Controls whether copilot provides suggestion immediately
|
// The factor to grow the active pane by. Defaults to 1.0
|
||||||
// or waits for a `copilot::Toggle`
|
// which gives the same size as all other panes.
|
||||||
"copilot": "on",
|
"active_pane_magnification": 1.0,
|
||||||
// Whether to enable vim modes and key bindings
|
// Whether to enable vim modes and key bindings
|
||||||
"vim_mode": false,
|
"vim_mode": false,
|
||||||
// Whether to show the informational hover box when moving the mouse
|
// Whether to show the informational hover box when moving the mouse
|
||||||
// over symbols in the editor.
|
// over symbols in the editor.
|
||||||
"hover_popover_enabled": true,
|
"hover_popover_enabled": true,
|
||||||
// Whether to confirm before quitting Zed.
|
// Whether to confirm before quitting Zed.
|
||||||
"confirm_quit": false,
|
"confirm_quit": false,
|
||||||
// Whether the cursor blinks in the editor.
|
// Whether the cursor blinks in the editor.
|
||||||
"cursor_blink": true,
|
"cursor_blink": true,
|
||||||
// Whether to pop the completions menu while typing in an editor without
|
// Whether to pop the completions menu while typing in an editor without
|
||||||
// explicitly requesting it.
|
// explicitly requesting it.
|
||||||
"show_completions_on_input": true,
|
"show_completions_on_input": true,
|
||||||
// Whether the screen sharing icon is shown in the os status bar.
|
// Controls whether copilot provides suggestion immediately
|
||||||
"show_call_status_icon": true,
|
// or waits for a `copilot::Toggle`
|
||||||
// Whether to use language servers to provide code intelligence.
|
"show_copilot_suggestions": true,
|
||||||
"enable_language_server": true,
|
// Whether the screen sharing icon is shown in the os status bar.
|
||||||
// When to automatically save edited buffers. This setting can
|
"show_call_status_icon": true,
|
||||||
// take four values.
|
// Whether to use language servers to provide code intelligence.
|
||||||
//
|
"enable_language_server": true,
|
||||||
// 1. Never automatically save:
|
// When to automatically save edited buffers. This setting can
|
||||||
// "autosave": "off",
|
// take four values.
|
||||||
// 2. Save when changing focus away from the Zed window:
|
//
|
||||||
// "autosave": "on_window_change",
|
// 1. Never automatically save:
|
||||||
// 3. Save when changing focus away from a specific buffer:
|
// "autosave": "off",
|
||||||
// "autosave": "on_focus_change",
|
// 2. Save when changing focus away from the Zed window:
|
||||||
// 4. Save when idle for a certain amount of time:
|
// "autosave": "on_window_change",
|
||||||
// "autosave": { "after_delay": {"milliseconds": 500} },
|
// 3. Save when changing focus away from a specific buffer:
|
||||||
"autosave": "off",
|
// "autosave": "on_focus_change",
|
||||||
// Where to place the dock by default. This setting can take three
|
// 4. Save when idle for a certain amount of time:
|
||||||
// values:
|
// "autosave": { "after_delay": {"milliseconds": 500} },
|
||||||
//
|
"autosave": "off",
|
||||||
// 1. Position the dock attached to the bottom of the workspace
|
// Where to place the dock by default. This setting can take three
|
||||||
// "default_dock_anchor": "bottom"
|
// values:
|
||||||
// 2. Position the dock to the right of the workspace like a side panel
|
//
|
||||||
// "default_dock_anchor": "right"
|
// 1. Position the dock attached to the bottom of the workspace
|
||||||
// 3. Position the dock full screen over the entire workspace"
|
// "default_dock_anchor": "bottom"
|
||||||
// "default_dock_anchor": "expanded"
|
// 2. Position the dock to the right of the workspace like a side panel
|
||||||
"default_dock_anchor": "bottom",
|
// "default_dock_anchor": "right"
|
||||||
// Whether or not to remove any trailing whitespace from lines of a buffer
|
// 3. Position the dock full screen over the entire workspace"
|
||||||
// before saving it.
|
// "default_dock_anchor": "expanded"
|
||||||
"remove_trailing_whitespace_on_save": true,
|
"default_dock_anchor": "bottom",
|
||||||
// Whether or not to ensure there's a single newline at the end of a buffer
|
// Whether or not to remove any trailing whitespace from lines of a buffer
|
||||||
// when saving it.
|
// before saving it.
|
||||||
"ensure_final_newline_on_save": true,
|
"remove_trailing_whitespace_on_save": true,
|
||||||
// Whether or not to perform a buffer format before saving
|
// Whether or not to ensure there's a single newline at the end of a buffer
|
||||||
"format_on_save": "on",
|
// when saving it.
|
||||||
// How to perform a buffer format. This setting can take two values:
|
"ensure_final_newline_on_save": true,
|
||||||
//
|
// Whether or not to perform a buffer format before saving
|
||||||
// 1. Format code using the current language server:
|
"format_on_save": "on",
|
||||||
// "format_on_save": "language_server"
|
// How to perform a buffer format. This setting can take two values:
|
||||||
// 2. Format code using an external command:
|
//
|
||||||
// "format_on_save": {
|
// 1. Format code using the current language server:
|
||||||
// "external": {
|
// "format_on_save": "language_server"
|
||||||
// "command": "prettier",
|
// 2. Format code using an external command:
|
||||||
// "arguments": ["--stdin-filepath", "{buffer_path}"]
|
// "format_on_save": {
|
||||||
// }
|
// "external": {
|
||||||
|
// "command": "prettier",
|
||||||
|
// "arguments": ["--stdin-filepath", "{buffer_path}"]
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
"formatter": "language_server",
|
||||||
|
// How to soft-wrap long lines of text. This setting can take
|
||||||
|
// three values:
|
||||||
|
//
|
||||||
|
// 1. Do not soft wrap.
|
||||||
|
// "soft_wrap": "none",
|
||||||
|
// 2. Soft wrap lines that overflow the editor:
|
||||||
|
// "soft_wrap": "editor_width",
|
||||||
|
// 3. Soft wrap lines at the preferred line length
|
||||||
|
// "soft_wrap": "preferred_line_length",
|
||||||
|
"soft_wrap": "none",
|
||||||
|
// The column at which to soft-wrap lines, for buffers where soft-wrap
|
||||||
|
// is enabled.
|
||||||
|
"preferred_line_length": 80,
|
||||||
|
// Whether to indent lines using tab characters, as opposed to multiple
|
||||||
|
// spaces.
|
||||||
|
"hard_tabs": false,
|
||||||
|
// How many columns a tab should occupy.
|
||||||
|
"tab_size": 4,
|
||||||
|
// Control what info is collected by Zed.
|
||||||
|
"telemetry": {
|
||||||
|
// Send debug info like crash reports.
|
||||||
|
"diagnostics": true,
|
||||||
|
// Send anonymized usage data like what languages you're using Zed with.
|
||||||
|
"metrics": true
|
||||||
|
},
|
||||||
|
// Automatically update Zed
|
||||||
|
"auto_update": true,
|
||||||
|
// Git gutter behavior configuration.
|
||||||
|
"git": {
|
||||||
|
// Control whether the git gutter is shown. May take 2 values:
|
||||||
|
// 1. Show the gutter
|
||||||
|
// "git_gutter": "tracked_files"
|
||||||
|
// 2. Hide the gutter
|
||||||
|
// "git_gutter": "hide"
|
||||||
|
"git_gutter": "tracked_files"
|
||||||
|
},
|
||||||
|
// Settings specific to journaling
|
||||||
|
"journal": {
|
||||||
|
// The path of the directory where journal entries are stored
|
||||||
|
"path": "~",
|
||||||
|
// What format to display the hours in
|
||||||
|
// May take 2 values:
|
||||||
|
// 1. hour12
|
||||||
|
// 2. hour24
|
||||||
|
"hour_format": "hour12"
|
||||||
|
},
|
||||||
|
// Settings specific to the terminal
|
||||||
|
"terminal": {
|
||||||
|
// What shell to use when opening a terminal. May take 3 values:
|
||||||
|
// 1. Use the system's default terminal configuration in /etc/passwd
|
||||||
|
// "shell": "system"
|
||||||
|
// 2. A program:
|
||||||
|
// "shell": {
|
||||||
|
// "program": "sh"
|
||||||
|
// }
|
||||||
|
// 3. A program with arguments:
|
||||||
|
// "shell": {
|
||||||
|
// "with_arguments": {
|
||||||
|
// "program": "/bin/bash",
|
||||||
|
// "arguments": ["--login"]
|
||||||
|
// }
|
||||||
// }
|
// }
|
||||||
"formatter": "language_server",
|
"shell": "system",
|
||||||
// How to soft-wrap long lines of text. This setting can take
|
// What working directory to use when launching the terminal.
|
||||||
// three values:
|
// May take 4 values:
|
||||||
|
// 1. Use the current file's project directory. Will Fallback to the
|
||||||
|
// first project directory strategy if unsuccessful
|
||||||
|
// "working_directory": "current_project_directory"
|
||||||
|
// 2. Use the first project in this workspace's directory
|
||||||
|
// "working_directory": "first_project_directory"
|
||||||
|
// 3. Always use this platform's home directory (if we can find it)
|
||||||
|
// "working_directory": "always_home"
|
||||||
|
// 4. Always use a specific directory. This value will be shell expanded.
|
||||||
|
// If this path is not a valid directory the terminal will default to
|
||||||
|
// this platform's home directory (if we can find it)
|
||||||
|
// "working_directory": {
|
||||||
|
// "always": {
|
||||||
|
// "directory": "~/zed/projects/"
|
||||||
|
// }
|
||||||
|
// }
|
||||||
//
|
//
|
||||||
// 1. Do not soft wrap.
|
//
|
||||||
// "soft_wrap": "none",
|
"working_directory": "current_project_directory",
|
||||||
// 2. Soft wrap lines that overflow the editor:
|
// Set the cursor blinking behavior in the terminal.
|
||||||
// "soft_wrap": "editor_width",
|
// May take 4 values:
|
||||||
// 3. Soft wrap lines at the preferred line length
|
// 1. Never blink the cursor, ignoring the terminal mode
|
||||||
// "soft_wrap": "preferred_line_length",
|
// "blinking": "off",
|
||||||
"soft_wrap": "none",
|
// 2. Default the cursor blink to off, but allow the terminal to
|
||||||
// The column at which to soft-wrap lines, for buffers where soft-wrap
|
// set blinking
|
||||||
// is enabled.
|
// "blinking": "terminal_controlled",
|
||||||
"preferred_line_length": 80,
|
// 3. Always blink the cursor, ignoring the terminal mode
|
||||||
// Whether to indent lines using tab characters, as opposed to multiple
|
// "blinking": "on",
|
||||||
// spaces.
|
"blinking": "terminal_controlled",
|
||||||
"hard_tabs": false,
|
// Set whether Alternate Scroll mode (code: ?1007) is active by default.
|
||||||
// How many columns a tab should occupy.
|
// Alternate Scroll mode converts mouse scroll events into up / down key
|
||||||
"tab_size": 4,
|
// presses when in the alternate screen (e.g. when running applications
|
||||||
// Control what info is collected by Zed.
|
// like vim or less). The terminal can still set and unset this mode.
|
||||||
"telemetry": {
|
// May take 2 values:
|
||||||
// Send debug info like crash reports.
|
// 1. Default alternate scroll mode to on
|
||||||
"diagnostics": true,
|
// "alternate_scroll": "on",
|
||||||
// Send anonymized usage data like what languages you're using Zed with.
|
// 2. Default alternate scroll mode to off
|
||||||
"metrics": true
|
// "alternate_scroll": "off",
|
||||||
},
|
"alternate_scroll": "off",
|
||||||
// Automatically update Zed
|
// Set whether the option key behaves as the meta key.
|
||||||
"auto_update": true,
|
// May take 2 values:
|
||||||
// Git gutter behavior configuration.
|
// 1. Rely on default platform handling of option key, on macOS
|
||||||
"git": {
|
// this means generating certain unicode characters
|
||||||
// Control whether the git gutter is shown. May take 2 values:
|
// "option_to_meta": false,
|
||||||
// 1. Show the gutter
|
// 2. Make the option keys behave as a 'meta' key, e.g. for emacs
|
||||||
// "git_gutter": "tracked_files"
|
// "option_to_meta": true,
|
||||||
// 2. Hide the gutter
|
"option_as_meta": false,
|
||||||
// "git_gutter": "hide"
|
// Whether or not selecting text in the terminal will automatically
|
||||||
"git_gutter": "tracked_files"
|
// copy to the system clipboard.
|
||||||
},
|
"copy_on_select": false,
|
||||||
// Settings specific to journaling
|
// Any key-value pairs added to this list will be added to the terminal's
|
||||||
"journal": {
|
// enviroment. Use `:` to seperate multiple values.
|
||||||
// The path of the directory where journal entries are stored
|
"env": {
|
||||||
"path": "~",
|
// "KEY": "value1:value2"
|
||||||
// What format to display the hours in
|
|
||||||
// May take 2 values:
|
|
||||||
// 1. hour12
|
|
||||||
// 2. hour24
|
|
||||||
"hour_format": "hour12"
|
|
||||||
},
|
|
||||||
// Settings specific to the terminal
|
|
||||||
"terminal": {
|
|
||||||
// What shell to use when opening a terminal. May take 3 values:
|
|
||||||
// 1. Use the system's default terminal configuration in /etc/passwd
|
|
||||||
// "shell": "system"
|
|
||||||
// 2. A program:
|
|
||||||
// "shell": {
|
|
||||||
// "program": "sh"
|
|
||||||
// }
|
|
||||||
// 3. A program with arguments:
|
|
||||||
// "shell": {
|
|
||||||
// "with_arguments": {
|
|
||||||
// "program": "/bin/bash",
|
|
||||||
// "arguments": ["--login"]
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
"shell": "system",
|
|
||||||
// What working directory to use when launching the terminal.
|
|
||||||
// May take 4 values:
|
|
||||||
// 1. Use the current file's project directory. Will Fallback to the
|
|
||||||
// first project directory strategy if unsuccessful
|
|
||||||
// "working_directory": "current_project_directory"
|
|
||||||
// 2. Use the first project in this workspace's directory
|
|
||||||
// "working_directory": "first_project_directory"
|
|
||||||
// 3. Always use this platform's home directory (if we can find it)
|
|
||||||
// "working_directory": "always_home"
|
|
||||||
// 4. Always use a specific directory. This value will be shell expanded.
|
|
||||||
// If this path is not a valid directory the terminal will default to
|
|
||||||
// this platform's home directory (if we can find it)
|
|
||||||
// "working_directory": {
|
|
||||||
// "always": {
|
|
||||||
// "directory": "~/zed/projects/"
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
//
|
|
||||||
"working_directory": "current_project_directory",
|
|
||||||
// Set the cursor blinking behavior in the terminal.
|
|
||||||
// May take 4 values:
|
|
||||||
// 1. Never blink the cursor, ignoring the terminal mode
|
|
||||||
// "blinking": "off",
|
|
||||||
// 2. Default the cursor blink to off, but allow the terminal to
|
|
||||||
// set blinking
|
|
||||||
// "blinking": "terminal_controlled",
|
|
||||||
// 3. Always blink the cursor, ignoring the terminal mode
|
|
||||||
// "blinking": "on",
|
|
||||||
"blinking": "terminal_controlled",
|
|
||||||
// Set whether Alternate Scroll mode (code: ?1007) is active by default.
|
|
||||||
// Alternate Scroll mode converts mouse scroll events into up / down key
|
|
||||||
// presses when in the alternate screen (e.g. when running applications
|
|
||||||
// like vim or less). The terminal can still set and unset this mode.
|
|
||||||
// May take 2 values:
|
|
||||||
// 1. Default alternate scroll mode to on
|
|
||||||
// "alternate_scroll": "on",
|
|
||||||
// 2. Default alternate scroll mode to off
|
|
||||||
// "alternate_scroll": "off",
|
|
||||||
"alternate_scroll": "off",
|
|
||||||
// Set whether the option key behaves as the meta key.
|
|
||||||
// May take 2 values:
|
|
||||||
// 1. Rely on default platform handling of option key, on macOS
|
|
||||||
// this means generating certain unicode characters
|
|
||||||
// "option_to_meta": false,
|
|
||||||
// 2. Make the option keys behave as a 'meta' key, e.g. for emacs
|
|
||||||
// "option_to_meta": true,
|
|
||||||
"option_as_meta": false,
|
|
||||||
// Whether or not selecting text in the terminal will automatically
|
|
||||||
// copy to the system clipboard.
|
|
||||||
"copy_on_select": false,
|
|
||||||
// Any key-value pairs added to this list will be added to the terminal's
|
|
||||||
// enviroment. Use `:` to seperate multiple values.
|
|
||||||
"env": {
|
|
||||||
// "KEY": "value1:value2"
|
|
||||||
}
|
|
||||||
// Set the terminal's font size. If this option is not included,
|
|
||||||
// the terminal will default to matching the buffer's font size.
|
|
||||||
// "font_size": "15"
|
|
||||||
// Set the terminal's font family. If this option is not included,
|
|
||||||
// the terminal will default to matching the buffer's font family.
|
|
||||||
// "font_family": "Zed Mono"
|
|
||||||
},
|
|
||||||
// Different settings for specific languages.
|
|
||||||
"languages": {
|
|
||||||
"Plain Text": {
|
|
||||||
"soft_wrap": "preferred_line_length"
|
|
||||||
},
|
|
||||||
"Elixir": {
|
|
||||||
"tab_size": 2
|
|
||||||
},
|
|
||||||
"Go": {
|
|
||||||
"tab_size": 4,
|
|
||||||
"hard_tabs": true
|
|
||||||
},
|
|
||||||
"Markdown": {
|
|
||||||
"soft_wrap": "preferred_line_length"
|
|
||||||
},
|
|
||||||
"JavaScript": {
|
|
||||||
"tab_size": 2
|
|
||||||
},
|
|
||||||
"TypeScript": {
|
|
||||||
"tab_size": 2
|
|
||||||
},
|
|
||||||
"TSX": {
|
|
||||||
"tab_size": 2
|
|
||||||
},
|
|
||||||
"YAML": {
|
|
||||||
"tab_size": 2
|
|
||||||
}
|
|
||||||
},
|
|
||||||
// LSP Specific settings.
|
|
||||||
"lsp": {
|
|
||||||
// Specify the LSP name as a key here.
|
|
||||||
// As of 8/10/22, supported LSPs are:
|
|
||||||
// pyright
|
|
||||||
// gopls
|
|
||||||
// rust-analyzer
|
|
||||||
// typescript-language-server
|
|
||||||
// vscode-json-languageserver
|
|
||||||
// "rust-analyzer": {
|
|
||||||
// //These initialization options are merged into Zed's defaults
|
|
||||||
// "initialization_options": {
|
|
||||||
// "checkOnSave": {
|
|
||||||
// "command": "clippy"
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
// Set the terminal's font size. If this option is not included,
|
||||||
|
// the terminal will default to matching the buffer's font size.
|
||||||
|
// "font_size": "15"
|
||||||
|
// Set the terminal's font family. If this option is not included,
|
||||||
|
// the terminal will default to matching the buffer's font family.
|
||||||
|
// "font_family": "Zed Mono"
|
||||||
|
},
|
||||||
|
// Different settings for specific languages.
|
||||||
|
"languages": {
|
||||||
|
"Plain Text": {
|
||||||
|
"soft_wrap": "preferred_line_length"
|
||||||
|
},
|
||||||
|
"Elixir": {
|
||||||
|
"tab_size": 2
|
||||||
|
},
|
||||||
|
"Go": {
|
||||||
|
"tab_size": 4,
|
||||||
|
"hard_tabs": true
|
||||||
|
},
|
||||||
|
"Markdown": {
|
||||||
|
"soft_wrap": "preferred_line_length"
|
||||||
|
},
|
||||||
|
"JavaScript": {
|
||||||
|
"tab_size": 2
|
||||||
|
},
|
||||||
|
"TypeScript": {
|
||||||
|
"tab_size": 2
|
||||||
|
},
|
||||||
|
"TSX": {
|
||||||
|
"tab_size": 2
|
||||||
|
},
|
||||||
|
"YAML": {
|
||||||
|
"tab_size": 2
|
||||||
|
},
|
||||||
|
"JSON": {
|
||||||
|
"tab_size": 2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// LSP Specific settings.
|
||||||
|
"lsp": {
|
||||||
|
// Specify the LSP name as a key here.
|
||||||
|
// As of 8/10/22, supported LSPs are:
|
||||||
|
// pyright
|
||||||
|
// gopls
|
||||||
|
// rust-analyzer
|
||||||
|
// typescript-language-server
|
||||||
|
// vscode-json-languageserver
|
||||||
|
// "rust-analyzer": {
|
||||||
|
// //These initialization options are merged into Zed's defaults
|
||||||
|
// "initialization_options": {
|
||||||
|
// "checkOnSave": {
|
||||||
|
// "command": "clippy"
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -55,6 +55,7 @@ toml = "0.5.8"
|
||||||
tracing = "0.1.34"
|
tracing = "0.1.34"
|
||||||
tracing-log = "0.1.3"
|
tracing-log = "0.1.3"
|
||||||
tracing-subscriber = { version = "0.3.11", features = ["env-filter", "json"] }
|
tracing-subscriber = { version = "0.3.11", features = ["env-filter", "json"] }
|
||||||
|
indoc = "1.0.4"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
collections = { path = "../collections", features = ["test-support"] }
|
collections = { path = "../collections", features = ["test-support"] }
|
||||||
|
|
|
@ -6,8 +6,9 @@ use call::{room, ActiveCall, ParticipantLocation, Room};
|
||||||
use client::{User, RECEIVE_TIMEOUT};
|
use client::{User, RECEIVE_TIMEOUT};
|
||||||
use collections::HashSet;
|
use collections::HashSet;
|
||||||
use editor::{
|
use editor::{
|
||||||
ConfirmCodeAction, ConfirmCompletion, ConfirmRename, Editor, ExcerptRange, MultiBuffer, Redo,
|
test::editor_test_context::EditorTestContext, ConfirmCodeAction, ConfirmCompletion,
|
||||||
Rename, ToOffset, ToggleCodeActions, Undo,
|
ConfirmRename, Editor, ExcerptRange, MultiBuffer, Redo, Rename, ToOffset, ToggleCodeActions,
|
||||||
|
Undo,
|
||||||
};
|
};
|
||||||
use fs::{FakeFs, Fs as _, LineEnding, RemoveOptions};
|
use fs::{FakeFs, Fs as _, LineEnding, RemoveOptions};
|
||||||
use futures::StreamExt as _;
|
use futures::StreamExt as _;
|
||||||
|
@ -15,6 +16,7 @@ use gpui::{
|
||||||
executor::Deterministic, geometry::vector::vec2f, test::EmptyView, ModelHandle, TestAppContext,
|
executor::Deterministic, geometry::vector::vec2f, test::EmptyView, ModelHandle, TestAppContext,
|
||||||
ViewHandle,
|
ViewHandle,
|
||||||
};
|
};
|
||||||
|
use indoc::indoc;
|
||||||
use language::{
|
use language::{
|
||||||
tree_sitter_rust, Anchor, Diagnostic, DiagnosticEntry, FakeLspAdapter, Language,
|
tree_sitter_rust, Anchor, Diagnostic, DiagnosticEntry, FakeLspAdapter, Language,
|
||||||
LanguageConfig, OffsetRangeExt, Point, Rope,
|
LanguageConfig, OffsetRangeExt, Point, Rope,
|
||||||
|
@ -3040,6 +3042,104 @@ async fn test_editing_while_guest_opens_buffer(
|
||||||
buffer_b.read_with(cx_b, |buf, _| assert_eq!(buf.text(), text));
|
buffer_b.read_with(cx_b, |buf, _| assert_eq!(buf.text(), text));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_newline_above_or_below_does_not_move_guest_cursor(
|
||||||
|
deterministic: Arc<Deterministic>,
|
||||||
|
cx_a: &mut TestAppContext,
|
||||||
|
cx_b: &mut TestAppContext,
|
||||||
|
) {
|
||||||
|
deterministic.forbid_parking();
|
||||||
|
let mut server = TestServer::start(&deterministic).await;
|
||||||
|
let client_a = server.create_client(cx_a, "user_a").await;
|
||||||
|
let client_b = server.create_client(cx_b, "user_b").await;
|
||||||
|
server
|
||||||
|
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
|
||||||
|
.await;
|
||||||
|
let active_call_a = cx_a.read(ActiveCall::global);
|
||||||
|
|
||||||
|
client_a
|
||||||
|
.fs
|
||||||
|
.insert_tree("/dir", json!({ "a.txt": "Some text\n" }))
|
||||||
|
.await;
|
||||||
|
let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
|
||||||
|
let project_id = active_call_a
|
||||||
|
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let project_b = client_b.build_remote_project(project_id, cx_b).await;
|
||||||
|
|
||||||
|
// Open a buffer as client A
|
||||||
|
let buffer_a = project_a
|
||||||
|
.update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let (_, window_a) = cx_a.add_window(|_| EmptyView);
|
||||||
|
let editor_a = cx_a.add_view(&window_a, |cx| {
|
||||||
|
Editor::for_buffer(buffer_a, Some(project_a), cx)
|
||||||
|
});
|
||||||
|
let mut editor_cx_a = EditorTestContext {
|
||||||
|
cx: cx_a,
|
||||||
|
window_id: window_a.id(),
|
||||||
|
editor: editor_a,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Open a buffer as client B
|
||||||
|
let buffer_b = project_b
|
||||||
|
.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let (_, window_b) = cx_b.add_window(|_| EmptyView);
|
||||||
|
let editor_b = cx_b.add_view(&window_b, |cx| {
|
||||||
|
Editor::for_buffer(buffer_b, Some(project_b), cx)
|
||||||
|
});
|
||||||
|
let mut editor_cx_b = EditorTestContext {
|
||||||
|
cx: cx_b,
|
||||||
|
window_id: window_b.id(),
|
||||||
|
editor: editor_b,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Test newline above
|
||||||
|
editor_cx_a.set_selections_state(indoc! {"
|
||||||
|
Some textˇ
|
||||||
|
"});
|
||||||
|
editor_cx_b.set_selections_state(indoc! {"
|
||||||
|
Some textˇ
|
||||||
|
"});
|
||||||
|
editor_cx_a.update_editor(|editor, cx| editor.newline_above(&editor::NewlineAbove, cx));
|
||||||
|
deterministic.run_until_parked();
|
||||||
|
editor_cx_a.assert_editor_state(indoc! {"
|
||||||
|
ˇ
|
||||||
|
Some text
|
||||||
|
"});
|
||||||
|
editor_cx_b.assert_editor_state(indoc! {"
|
||||||
|
|
||||||
|
Some textˇ
|
||||||
|
"});
|
||||||
|
|
||||||
|
// Test newline below
|
||||||
|
editor_cx_a.set_selections_state(indoc! {"
|
||||||
|
|
||||||
|
Some textˇ
|
||||||
|
"});
|
||||||
|
editor_cx_b.set_selections_state(indoc! {"
|
||||||
|
|
||||||
|
Some textˇ
|
||||||
|
"});
|
||||||
|
editor_cx_a.update_editor(|editor, cx| editor.newline_below(&editor::NewlineBelow, cx));
|
||||||
|
deterministic.run_until_parked();
|
||||||
|
editor_cx_a.assert_editor_state(indoc! {"
|
||||||
|
|
||||||
|
Some text
|
||||||
|
ˇ
|
||||||
|
"});
|
||||||
|
editor_cx_b.assert_editor_state(indoc! {"
|
||||||
|
|
||||||
|
Some textˇ
|
||||||
|
|
||||||
|
"});
|
||||||
|
}
|
||||||
|
|
||||||
#[gpui::test(iterations = 10)]
|
#[gpui::test(iterations = 10)]
|
||||||
async fn test_leaving_worktree_while_opening_buffer(
|
async fn test_leaving_worktree_while_opening_buffer(
|
||||||
deterministic: Arc<Deterministic>,
|
deterministic: Arc<Deterministic>,
|
||||||
|
@ -5860,10 +5960,17 @@ async fn test_basic_following(
|
||||||
|
|
||||||
// Client A updates their selections in those editors
|
// Client A updates their selections in those editors
|
||||||
editor_a1.update(cx_a, |editor, cx| {
|
editor_a1.update(cx_a, |editor, cx| {
|
||||||
editor.change_selections(None, cx, |s| s.select_ranges([0..1]))
|
editor.handle_input("a", cx);
|
||||||
|
editor.handle_input("b", cx);
|
||||||
|
editor.handle_input("c", cx);
|
||||||
|
editor.select_left(&Default::default(), cx);
|
||||||
|
assert_eq!(editor.selections.ranges(cx), vec![3..2]);
|
||||||
});
|
});
|
||||||
editor_a2.update(cx_a, |editor, cx| {
|
editor_a2.update(cx_a, |editor, cx| {
|
||||||
editor.change_selections(None, cx, |s| s.select_ranges([2..3]))
|
editor.handle_input("d", cx);
|
||||||
|
editor.handle_input("e", cx);
|
||||||
|
editor.select_left(&Default::default(), cx);
|
||||||
|
assert_eq!(editor.selections.ranges(cx), vec![2..1]);
|
||||||
});
|
});
|
||||||
|
|
||||||
// When client B starts following client A, all visible view states are replicated to client B.
|
// When client B starts following client A, all visible view states are replicated to client B.
|
||||||
|
@ -5876,6 +5983,27 @@ async fn test_basic_following(
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
cx_c.foreground().run_until_parked();
|
||||||
|
let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| {
|
||||||
|
workspace
|
||||||
|
.active_item(cx)
|
||||||
|
.unwrap()
|
||||||
|
.downcast::<Editor>()
|
||||||
|
.unwrap()
|
||||||
|
});
|
||||||
|
assert_eq!(
|
||||||
|
cx_b.read(|cx| editor_b2.project_path(cx)),
|
||||||
|
Some((worktree_id, "2.txt").into())
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
editor_b2.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
|
||||||
|
vec![2..1]
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
editor_b1.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
|
||||||
|
vec![3..2]
|
||||||
|
);
|
||||||
|
|
||||||
cx_c.foreground().run_until_parked();
|
cx_c.foreground().run_until_parked();
|
||||||
let active_call_c = cx_c.read(ActiveCall::global);
|
let active_call_c = cx_c.read(ActiveCall::global);
|
||||||
let project_c = client_c.build_remote_project(project_id, cx_c).await;
|
let project_c = client_c.build_remote_project(project_id, cx_c).await;
|
||||||
|
@ -6031,26 +6159,6 @@ async fn test_basic_following(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| {
|
|
||||||
workspace
|
|
||||||
.active_item(cx)
|
|
||||||
.unwrap()
|
|
||||||
.downcast::<Editor>()
|
|
||||||
.unwrap()
|
|
||||||
});
|
|
||||||
assert_eq!(
|
|
||||||
cx_b.read(|cx| editor_b2.project_path(cx)),
|
|
||||||
Some((worktree_id, "2.txt").into())
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
editor_b2.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
|
|
||||||
vec![2..3]
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
editor_b1.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
|
|
||||||
vec![0..1]
|
|
||||||
);
|
|
||||||
|
|
||||||
// When client A activates a different editor, client B does so as well.
|
// When client A activates a different editor, client B does so as well.
|
||||||
workspace_a.update(cx_a, |workspace, cx| {
|
workspace_a.update(cx_a, |workspace, cx| {
|
||||||
workspace.activate_item(&editor_a1, cx)
|
workspace.activate_item(&editor_a1, cx)
|
||||||
|
|
|
@ -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"] }
|
||||||
|
|
|
@ -5,9 +5,14 @@ use anyhow::{anyhow, Context, Result};
|
||||||
use async_compression::futures::bufread::GzipDecoder;
|
use async_compression::futures::bufread::GzipDecoder;
|
||||||
use async_tar::Archive;
|
use async_tar::Archive;
|
||||||
use collections::HashMap;
|
use collections::HashMap;
|
||||||
use futures::{future::Shared, Future, FutureExt, TryFutureExt};
|
use futures::{channel::oneshot, future::Shared, Future, FutureExt, TryFutureExt};
|
||||||
use gpui::{actions, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task};
|
use gpui::{
|
||||||
use language::{point_from_lsp, point_to_lsp, Anchor, Bias, Buffer, Language, ToPointUtf16};
|
actions, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task, WeakModelHandle,
|
||||||
|
};
|
||||||
|
use language::{
|
||||||
|
point_from_lsp, point_to_lsp, Anchor, Bias, Buffer, BufferSnapshot, Language, PointUtf16,
|
||||||
|
ToPointUtf16,
|
||||||
|
};
|
||||||
use log::{debug, error};
|
use log::{debug, error};
|
||||||
use lsp::LanguageServer;
|
use lsp::LanguageServer;
|
||||||
use node_runtime::NodeRuntime;
|
use node_runtime::NodeRuntime;
|
||||||
|
@ -16,6 +21,7 @@ use settings::Settings;
|
||||||
use smol::{fs, io::BufReader, stream::StreamExt};
|
use smol::{fs, io::BufReader, stream::StreamExt};
|
||||||
use std::{
|
use std::{
|
||||||
ffi::OsString,
|
ffi::OsString,
|
||||||
|
mem,
|
||||||
ops::Range,
|
ops::Range,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
|
@ -29,7 +35,10 @@ const COPILOT_AUTH_NAMESPACE: &'static str = "copilot_auth";
|
||||||
actions!(copilot_auth, [SignIn, SignOut]);
|
actions!(copilot_auth, [SignIn, SignOut]);
|
||||||
|
|
||||||
const COPILOT_NAMESPACE: &'static str = "copilot";
|
const COPILOT_NAMESPACE: &'static str = "copilot";
|
||||||
actions!(copilot, [NextSuggestion, PreviousSuggestion, Reinstall]);
|
actions!(
|
||||||
|
copilot,
|
||||||
|
[Suggest, NextSuggestion, PreviousSuggestion, Reinstall]
|
||||||
|
);
|
||||||
|
|
||||||
pub fn init(http: Arc<dyn HttpClient>, node_runtime: Arc<NodeRuntime>, cx: &mut AppContext) {
|
pub fn init(http: Arc<dyn HttpClient>, node_runtime: Arc<NodeRuntime>, cx: &mut AppContext) {
|
||||||
// Disable Copilot for stable releases.
|
// Disable Copilot for stable releases.
|
||||||
|
@ -95,15 +104,38 @@ pub fn init(http: Arc<dyn HttpClient>, node_runtime: Arc<NodeRuntime>, cx: &mut
|
||||||
|
|
||||||
enum CopilotServer {
|
enum CopilotServer {
|
||||||
Disabled,
|
Disabled,
|
||||||
Starting {
|
Starting { task: Shared<Task<()>> },
|
||||||
task: Shared<Task<()>>,
|
|
||||||
},
|
|
||||||
Error(Arc<str>),
|
Error(Arc<str>),
|
||||||
Started {
|
Running(RunningCopilotServer),
|
||||||
server: Arc<LanguageServer>,
|
}
|
||||||
status: SignInStatus,
|
|
||||||
subscriptions_by_buffer_id: HashMap<usize, gpui::Subscription>,
|
impl CopilotServer {
|
||||||
},
|
fn as_authenticated(&mut self) -> Result<&mut RunningCopilotServer> {
|
||||||
|
let server = self.as_running()?;
|
||||||
|
if matches!(server.sign_in_status, SignInStatus::Authorized { .. }) {
|
||||||
|
Ok(server)
|
||||||
|
} else {
|
||||||
|
Err(anyhow!("must sign in before using copilot"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_running(&mut self) -> Result<&mut RunningCopilotServer> {
|
||||||
|
match self {
|
||||||
|
CopilotServer::Starting { .. } => Err(anyhow!("copilot is still starting")),
|
||||||
|
CopilotServer::Disabled => Err(anyhow!("copilot is disabled")),
|
||||||
|
CopilotServer::Error(error) => Err(anyhow!(
|
||||||
|
"copilot was not started because of an error: {}",
|
||||||
|
error
|
||||||
|
)),
|
||||||
|
CopilotServer::Running(server) => Ok(server),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct RunningCopilotServer {
|
||||||
|
lsp: Arc<LanguageServer>,
|
||||||
|
sign_in_status: SignInStatus,
|
||||||
|
registered_buffers: HashMap<usize, RegisteredBuffer>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
|
@ -138,8 +170,104 @@ impl Status {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
struct RegisteredBuffer {
|
||||||
|
id: usize,
|
||||||
|
uri: lsp::Url,
|
||||||
|
language_id: String,
|
||||||
|
snapshot: BufferSnapshot,
|
||||||
|
snapshot_version: i32,
|
||||||
|
_subscriptions: [gpui::Subscription; 2],
|
||||||
|
pending_buffer_change: Task<Option<()>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RegisteredBuffer {
|
||||||
|
fn report_changes(
|
||||||
|
&mut self,
|
||||||
|
buffer: &ModelHandle<Buffer>,
|
||||||
|
cx: &mut ModelContext<Copilot>,
|
||||||
|
) -> oneshot::Receiver<(i32, BufferSnapshot)> {
|
||||||
|
let id = self.id;
|
||||||
|
let (done_tx, done_rx) = oneshot::channel();
|
||||||
|
|
||||||
|
if buffer.read(cx).version() == self.snapshot.version {
|
||||||
|
let _ = done_tx.send((self.snapshot_version, self.snapshot.clone()));
|
||||||
|
} else {
|
||||||
|
let buffer = buffer.downgrade();
|
||||||
|
let prev_pending_change =
|
||||||
|
mem::replace(&mut self.pending_buffer_change, Task::ready(None));
|
||||||
|
self.pending_buffer_change = cx.spawn_weak(|copilot, mut cx| async move {
|
||||||
|
prev_pending_change.await;
|
||||||
|
|
||||||
|
let old_version = copilot.upgrade(&cx)?.update(&mut cx, |copilot, _| {
|
||||||
|
let server = copilot.server.as_authenticated().log_err()?;
|
||||||
|
let buffer = server.registered_buffers.get_mut(&id)?;
|
||||||
|
Some(buffer.snapshot.version.clone())
|
||||||
|
})?;
|
||||||
|
let new_snapshot = buffer
|
||||||
|
.upgrade(&cx)?
|
||||||
|
.read_with(&cx, |buffer, _| buffer.snapshot());
|
||||||
|
|
||||||
|
let content_changes = cx
|
||||||
|
.background()
|
||||||
|
.spawn({
|
||||||
|
let new_snapshot = new_snapshot.clone();
|
||||||
|
async move {
|
||||||
|
new_snapshot
|
||||||
|
.edits_since::<(PointUtf16, usize)>(&old_version)
|
||||||
|
.map(|edit| {
|
||||||
|
let edit_start = edit.new.start.0;
|
||||||
|
let edit_end = edit_start + (edit.old.end.0 - edit.old.start.0);
|
||||||
|
let new_text = new_snapshot
|
||||||
|
.text_for_range(edit.new.start.1..edit.new.end.1)
|
||||||
|
.collect();
|
||||||
|
lsp::TextDocumentContentChangeEvent {
|
||||||
|
range: Some(lsp::Range::new(
|
||||||
|
point_to_lsp(edit_start),
|
||||||
|
point_to_lsp(edit_end),
|
||||||
|
)),
|
||||||
|
range_length: None,
|
||||||
|
text: new_text,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
copilot.upgrade(&cx)?.update(&mut cx, |copilot, _| {
|
||||||
|
let server = copilot.server.as_authenticated().log_err()?;
|
||||||
|
let buffer = server.registered_buffers.get_mut(&id)?;
|
||||||
|
if !content_changes.is_empty() {
|
||||||
|
buffer.snapshot_version += 1;
|
||||||
|
buffer.snapshot = new_snapshot;
|
||||||
|
server
|
||||||
|
.lsp
|
||||||
|
.notify::<lsp::notification::DidChangeTextDocument>(
|
||||||
|
lsp::DidChangeTextDocumentParams {
|
||||||
|
text_document: lsp::VersionedTextDocumentIdentifier::new(
|
||||||
|
buffer.uri.clone(),
|
||||||
|
buffer.snapshot_version,
|
||||||
|
),
|
||||||
|
content_changes,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.log_err();
|
||||||
|
}
|
||||||
|
let _ = done_tx.send((buffer.snapshot_version, buffer.snapshot.clone()));
|
||||||
|
Some(())
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Some(())
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
done_rx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
pub struct Completion {
|
pub struct Completion {
|
||||||
|
uuid: String,
|
||||||
pub range: Range<Anchor>,
|
pub range: Range<Anchor>,
|
||||||
pub text: String,
|
pub text: String,
|
||||||
}
|
}
|
||||||
|
@ -148,6 +276,7 @@ pub struct Copilot {
|
||||||
http: Arc<dyn HttpClient>,
|
http: Arc<dyn HttpClient>,
|
||||||
node_runtime: Arc<NodeRuntime>,
|
node_runtime: Arc<NodeRuntime>,
|
||||||
server: CopilotServer,
|
server: CopilotServer,
|
||||||
|
buffers: HashMap<usize, WeakModelHandle<Buffer>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Entity for Copilot {
|
impl Entity for Copilot {
|
||||||
|
@ -172,7 +301,7 @@ impl Copilot {
|
||||||
let http = http.clone();
|
let http = http.clone();
|
||||||
let node_runtime = node_runtime.clone();
|
let node_runtime = node_runtime.clone();
|
||||||
move |this, cx| {
|
move |this, cx| {
|
||||||
if cx.global::<Settings>().enable_copilot_integration {
|
if cx.global::<Settings>().features.copilot {
|
||||||
if matches!(this.server, CopilotServer::Disabled) {
|
if matches!(this.server, CopilotServer::Disabled) {
|
||||||
let start_task = cx
|
let start_task = cx
|
||||||
.spawn({
|
.spawn({
|
||||||
|
@ -194,12 +323,14 @@ impl Copilot {
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
|
|
||||||
if cx.global::<Settings>().enable_copilot_integration {
|
if cx.global::<Settings>().features.copilot {
|
||||||
let start_task = cx
|
let start_task = cx
|
||||||
.spawn({
|
.spawn({
|
||||||
let http = http.clone();
|
let http = http.clone();
|
||||||
let node_runtime = node_runtime.clone();
|
let node_runtime = node_runtime.clone();
|
||||||
move |this, cx| Self::start_language_server(http, node_runtime, this, cx)
|
move |this, cx| async {
|
||||||
|
Self::start_language_server(http, node_runtime, this, cx).await
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.shared();
|
.shared();
|
||||||
|
|
||||||
|
@ -207,12 +338,14 @@ impl Copilot {
|
||||||
http,
|
http,
|
||||||
node_runtime,
|
node_runtime,
|
||||||
server: CopilotServer::Starting { task: start_task },
|
server: CopilotServer::Starting { task: start_task },
|
||||||
|
buffers: Default::default(),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Self {
|
Self {
|
||||||
http,
|
http,
|
||||||
node_runtime,
|
node_runtime,
|
||||||
server: CopilotServer::Disabled,
|
server: CopilotServer::Disabled,
|
||||||
|
buffers: Default::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -225,11 +358,12 @@ impl Copilot {
|
||||||
let this = cx.add_model(|cx| Self {
|
let this = cx.add_model(|cx| Self {
|
||||||
http: http.clone(),
|
http: http.clone(),
|
||||||
node_runtime: NodeRuntime::new(http, cx.background().clone()),
|
node_runtime: NodeRuntime::new(http, cx.background().clone()),
|
||||||
server: CopilotServer::Started {
|
server: CopilotServer::Running(RunningCopilotServer {
|
||||||
server: Arc::new(server),
|
lsp: Arc::new(server),
|
||||||
status: SignInStatus::Authorized,
|
sign_in_status: SignInStatus::Authorized,
|
||||||
subscriptions_by_buffer_id: Default::default(),
|
registered_buffers: Default::default(),
|
||||||
},
|
}),
|
||||||
|
buffers: Default::default(),
|
||||||
});
|
});
|
||||||
(this, fake_server)
|
(this, fake_server)
|
||||||
}
|
}
|
||||||
|
@ -281,6 +415,19 @@ impl Copilot {
|
||||||
)
|
)
|
||||||
.detach();
|
.detach();
|
||||||
|
|
||||||
|
server
|
||||||
|
.request::<request::SetEditorInfo>(request::SetEditorInfoParams {
|
||||||
|
editor_info: request::EditorInfo {
|
||||||
|
name: "zed".into(),
|
||||||
|
version: env!("CARGO_PKG_VERSION").into(),
|
||||||
|
},
|
||||||
|
editor_plugin_info: request::EditorPluginInfo {
|
||||||
|
name: "zed-copilot".into(),
|
||||||
|
version: "0.0.1".into(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
anyhow::Ok((server, status))
|
anyhow::Ok((server, status))
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -289,11 +436,11 @@ impl Copilot {
|
||||||
cx.notify();
|
cx.notify();
|
||||||
match server {
|
match server {
|
||||||
Ok((server, status)) => {
|
Ok((server, status)) => {
|
||||||
this.server = CopilotServer::Started {
|
this.server = CopilotServer::Running(RunningCopilotServer {
|
||||||
server,
|
lsp: server,
|
||||||
status: SignInStatus::SignedOut,
|
sign_in_status: SignInStatus::SignedOut,
|
||||||
subscriptions_by_buffer_id: Default::default(),
|
registered_buffers: Default::default(),
|
||||||
};
|
});
|
||||||
this.update_sign_in_status(status, cx);
|
this.update_sign_in_status(status, cx);
|
||||||
}
|
}
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
|
@ -306,8 +453,8 @@ impl Copilot {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sign_in(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
|
fn sign_in(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
|
||||||
if let CopilotServer::Started { server, status, .. } = &mut self.server {
|
if let CopilotServer::Running(server) = &mut self.server {
|
||||||
let task = match status {
|
let task = match &server.sign_in_status {
|
||||||
SignInStatus::Authorized { .. } | SignInStatus::Unauthorized { .. } => {
|
SignInStatus::Authorized { .. } | SignInStatus::Unauthorized { .. } => {
|
||||||
Task::ready(Ok(())).shared()
|
Task::ready(Ok(())).shared()
|
||||||
}
|
}
|
||||||
|
@ -316,11 +463,11 @@ impl Copilot {
|
||||||
task.clone()
|
task.clone()
|
||||||
}
|
}
|
||||||
SignInStatus::SignedOut => {
|
SignInStatus::SignedOut => {
|
||||||
let server = server.clone();
|
let lsp = server.lsp.clone();
|
||||||
let task = cx
|
let task = cx
|
||||||
.spawn(|this, mut cx| async move {
|
.spawn(|this, mut cx| async move {
|
||||||
let sign_in = async {
|
let sign_in = async {
|
||||||
let sign_in = server
|
let sign_in = lsp
|
||||||
.request::<request::SignInInitiate>(
|
.request::<request::SignInInitiate>(
|
||||||
request::SignInInitiateParams {},
|
request::SignInInitiateParams {},
|
||||||
)
|
)
|
||||||
|
@ -331,8 +478,10 @@ impl Copilot {
|
||||||
}
|
}
|
||||||
request::SignInInitiateResult::PromptUserDeviceFlow(flow) => {
|
request::SignInInitiateResult::PromptUserDeviceFlow(flow) => {
|
||||||
this.update(&mut cx, |this, cx| {
|
this.update(&mut cx, |this, cx| {
|
||||||
if let CopilotServer::Started { status, .. } =
|
if let CopilotServer::Running(RunningCopilotServer {
|
||||||
&mut this.server
|
sign_in_status: status,
|
||||||
|
..
|
||||||
|
}) = &mut this.server
|
||||||
{
|
{
|
||||||
if let SignInStatus::SigningIn {
|
if let SignInStatus::SigningIn {
|
||||||
prompt: prompt_flow,
|
prompt: prompt_flow,
|
||||||
|
@ -344,7 +493,7 @@ impl Copilot {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
let response = server
|
let response = lsp
|
||||||
.request::<request::SignInConfirm>(
|
.request::<request::SignInConfirm>(
|
||||||
request::SignInConfirmParams {
|
request::SignInConfirmParams {
|
||||||
user_code: flow.user_code,
|
user_code: flow.user_code,
|
||||||
|
@ -372,7 +521,7 @@ impl Copilot {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.shared();
|
.shared();
|
||||||
*status = SignInStatus::SigningIn {
|
server.sign_in_status = SignInStatus::SigningIn {
|
||||||
prompt: None,
|
prompt: None,
|
||||||
task: task.clone(),
|
task: task.clone(),
|
||||||
};
|
};
|
||||||
|
@ -391,10 +540,8 @@ impl Copilot {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sign_out(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
|
fn sign_out(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
|
||||||
if let CopilotServer::Started { server, status, .. } = &mut self.server {
|
self.update_sign_in_status(request::SignInStatus::NotSignedIn, cx);
|
||||||
*status = SignInStatus::SignedOut;
|
if let CopilotServer::Running(RunningCopilotServer { lsp: server, .. }) = &self.server {
|
||||||
cx.notify();
|
|
||||||
|
|
||||||
let server = server.clone();
|
let server = server.clone();
|
||||||
cx.background().spawn(async move {
|
cx.background().spawn(async move {
|
||||||
server
|
server
|
||||||
|
@ -428,6 +575,135 @@ impl Copilot {
|
||||||
cx.foreground().spawn(start_task)
|
cx.foreground().spawn(start_task)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn register_buffer(&mut self, buffer: &ModelHandle<Buffer>, cx: &mut ModelContext<Self>) {
|
||||||
|
let buffer_id = buffer.id();
|
||||||
|
self.buffers.insert(buffer_id, buffer.downgrade());
|
||||||
|
|
||||||
|
if let CopilotServer::Running(RunningCopilotServer {
|
||||||
|
lsp: server,
|
||||||
|
sign_in_status: status,
|
||||||
|
registered_buffers,
|
||||||
|
..
|
||||||
|
}) = &mut self.server
|
||||||
|
{
|
||||||
|
if !matches!(status, SignInStatus::Authorized { .. }) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
registered_buffers.entry(buffer.id()).or_insert_with(|| {
|
||||||
|
let uri: lsp::Url = uri_for_buffer(buffer, cx);
|
||||||
|
let language_id = id_for_language(buffer.read(cx).language());
|
||||||
|
let snapshot = buffer.read(cx).snapshot();
|
||||||
|
server
|
||||||
|
.notify::<lsp::notification::DidOpenTextDocument>(
|
||||||
|
lsp::DidOpenTextDocumentParams {
|
||||||
|
text_document: lsp::TextDocumentItem {
|
||||||
|
uri: uri.clone(),
|
||||||
|
language_id: language_id.clone(),
|
||||||
|
version: 0,
|
||||||
|
text: snapshot.text(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.log_err();
|
||||||
|
|
||||||
|
RegisteredBuffer {
|
||||||
|
id: buffer_id,
|
||||||
|
uri,
|
||||||
|
language_id,
|
||||||
|
snapshot,
|
||||||
|
snapshot_version: 0,
|
||||||
|
pending_buffer_change: Task::ready(Some(())),
|
||||||
|
_subscriptions: [
|
||||||
|
cx.subscribe(buffer, |this, buffer, event, cx| {
|
||||||
|
this.handle_buffer_event(buffer, event, cx).log_err();
|
||||||
|
}),
|
||||||
|
cx.observe_release(buffer, move |this, _buffer, _cx| {
|
||||||
|
this.buffers.remove(&buffer_id);
|
||||||
|
this.unregister_buffer(buffer_id);
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_buffer_event(
|
||||||
|
&mut self,
|
||||||
|
buffer: ModelHandle<Buffer>,
|
||||||
|
event: &language::Event,
|
||||||
|
cx: &mut ModelContext<Self>,
|
||||||
|
) -> Result<()> {
|
||||||
|
if let Ok(server) = self.server.as_running() {
|
||||||
|
if let Some(registered_buffer) = server.registered_buffers.get_mut(&buffer.id()) {
|
||||||
|
match event {
|
||||||
|
language::Event::Edited => {
|
||||||
|
let _ = registered_buffer.report_changes(&buffer, cx);
|
||||||
|
}
|
||||||
|
language::Event::Saved => {
|
||||||
|
server
|
||||||
|
.lsp
|
||||||
|
.notify::<lsp::notification::DidSaveTextDocument>(
|
||||||
|
lsp::DidSaveTextDocumentParams {
|
||||||
|
text_document: lsp::TextDocumentIdentifier::new(
|
||||||
|
registered_buffer.uri.clone(),
|
||||||
|
),
|
||||||
|
text: None,
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
language::Event::FileHandleChanged | language::Event::LanguageChanged => {
|
||||||
|
let new_language_id = id_for_language(buffer.read(cx).language());
|
||||||
|
let new_uri = uri_for_buffer(&buffer, cx);
|
||||||
|
if new_uri != registered_buffer.uri
|
||||||
|
|| new_language_id != registered_buffer.language_id
|
||||||
|
{
|
||||||
|
let old_uri = mem::replace(&mut registered_buffer.uri, new_uri);
|
||||||
|
registered_buffer.language_id = new_language_id;
|
||||||
|
server
|
||||||
|
.lsp
|
||||||
|
.notify::<lsp::notification::DidCloseTextDocument>(
|
||||||
|
lsp::DidCloseTextDocumentParams {
|
||||||
|
text_document: lsp::TextDocumentIdentifier::new(old_uri),
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
server
|
||||||
|
.lsp
|
||||||
|
.notify::<lsp::notification::DidOpenTextDocument>(
|
||||||
|
lsp::DidOpenTextDocumentParams {
|
||||||
|
text_document: lsp::TextDocumentItem::new(
|
||||||
|
registered_buffer.uri.clone(),
|
||||||
|
registered_buffer.language_id.clone(),
|
||||||
|
registered_buffer.snapshot_version,
|
||||||
|
registered_buffer.snapshot.text(),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unregister_buffer(&mut self, buffer_id: usize) {
|
||||||
|
if let Ok(server) = self.server.as_running() {
|
||||||
|
if let Some(buffer) = server.registered_buffers.remove(&buffer_id) {
|
||||||
|
server
|
||||||
|
.lsp
|
||||||
|
.notify::<lsp::notification::DidCloseTextDocument>(
|
||||||
|
lsp::DidCloseTextDocumentParams {
|
||||||
|
text_document: lsp::TextDocumentIdentifier::new(buffer.uri),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.log_err();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn completions<T>(
|
pub fn completions<T>(
|
||||||
&mut self,
|
&mut self,
|
||||||
buffer: &ModelHandle<Buffer>,
|
buffer: &ModelHandle<Buffer>,
|
||||||
|
@ -452,6 +728,51 @@ impl Copilot {
|
||||||
self.request_completions::<request::GetCompletionsCycling, _>(buffer, position, cx)
|
self.request_completions::<request::GetCompletionsCycling, _>(buffer, position, cx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn accept_completion(
|
||||||
|
&mut self,
|
||||||
|
completion: &Completion,
|
||||||
|
cx: &mut ModelContext<Self>,
|
||||||
|
) -> Task<Result<()>> {
|
||||||
|
let server = match self.server.as_authenticated() {
|
||||||
|
Ok(server) => server,
|
||||||
|
Err(error) => return Task::ready(Err(error)),
|
||||||
|
};
|
||||||
|
let request =
|
||||||
|
server
|
||||||
|
.lsp
|
||||||
|
.request::<request::NotifyAccepted>(request::NotifyAcceptedParams {
|
||||||
|
uuid: completion.uuid.clone(),
|
||||||
|
});
|
||||||
|
cx.background().spawn(async move {
|
||||||
|
request.await?;
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn discard_completions(
|
||||||
|
&mut self,
|
||||||
|
completions: &[Completion],
|
||||||
|
cx: &mut ModelContext<Self>,
|
||||||
|
) -> Task<Result<()>> {
|
||||||
|
let server = match self.server.as_authenticated() {
|
||||||
|
Ok(server) => server,
|
||||||
|
Err(error) => return Task::ready(Err(error)),
|
||||||
|
};
|
||||||
|
let request =
|
||||||
|
server
|
||||||
|
.lsp
|
||||||
|
.request::<request::NotifyRejected>(request::NotifyRejectedParams {
|
||||||
|
uuids: completions
|
||||||
|
.iter()
|
||||||
|
.map(|completion| completion.uuid.clone())
|
||||||
|
.collect(),
|
||||||
|
});
|
||||||
|
cx.background().spawn(async move {
|
||||||
|
request.await?;
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn request_completions<R, T>(
|
fn request_completions<R, T>(
|
||||||
&mut self,
|
&mut self,
|
||||||
buffer: &ModelHandle<Buffer>,
|
buffer: &ModelHandle<Buffer>,
|
||||||
|
@ -459,116 +780,48 @@ impl Copilot {
|
||||||
cx: &mut ModelContext<Self>,
|
cx: &mut ModelContext<Self>,
|
||||||
) -> Task<Result<Vec<Completion>>>
|
) -> Task<Result<Vec<Completion>>>
|
||||||
where
|
where
|
||||||
R: lsp::request::Request<
|
R: 'static
|
||||||
Params = request::GetCompletionsParams,
|
+ lsp::request::Request<
|
||||||
Result = request::GetCompletionsResult,
|
Params = request::GetCompletionsParams,
|
||||||
>,
|
Result = request::GetCompletionsResult,
|
||||||
|
>,
|
||||||
T: ToPointUtf16,
|
T: ToPointUtf16,
|
||||||
{
|
{
|
||||||
let buffer_id = buffer.id();
|
self.register_buffer(buffer, cx);
|
||||||
let uri: lsp::Url = format!("buffer://{}", buffer_id).parse().unwrap();
|
|
||||||
let snapshot = buffer.read(cx).snapshot();
|
|
||||||
let server = match &mut self.server {
|
|
||||||
CopilotServer::Starting { .. } => {
|
|
||||||
return Task::ready(Err(anyhow!("copilot is still starting")))
|
|
||||||
}
|
|
||||||
CopilotServer::Disabled => return Task::ready(Err(anyhow!("copilot is disabled"))),
|
|
||||||
CopilotServer::Error(error) => {
|
|
||||||
return Task::ready(Err(anyhow!(
|
|
||||||
"copilot was not started because of an error: {}",
|
|
||||||
error
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
CopilotServer::Started {
|
|
||||||
server,
|
|
||||||
status,
|
|
||||||
subscriptions_by_buffer_id,
|
|
||||||
} => {
|
|
||||||
if matches!(status, SignInStatus::Authorized { .. }) {
|
|
||||||
subscriptions_by_buffer_id
|
|
||||||
.entry(buffer_id)
|
|
||||||
.or_insert_with(|| {
|
|
||||||
server
|
|
||||||
.notify::<lsp::notification::DidOpenTextDocument>(
|
|
||||||
lsp::DidOpenTextDocumentParams {
|
|
||||||
text_document: lsp::TextDocumentItem {
|
|
||||||
uri: uri.clone(),
|
|
||||||
language_id: id_for_language(
|
|
||||||
buffer.read(cx).language(),
|
|
||||||
),
|
|
||||||
version: 0,
|
|
||||||
text: snapshot.text(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.log_err();
|
|
||||||
|
|
||||||
let uri = uri.clone();
|
let server = match self.server.as_authenticated() {
|
||||||
cx.observe_release(buffer, move |this, _, _| {
|
Ok(server) => server,
|
||||||
if let CopilotServer::Started {
|
Err(error) => return Task::ready(Err(error)),
|
||||||
server,
|
|
||||||
subscriptions_by_buffer_id,
|
|
||||||
..
|
|
||||||
} = &mut this.server
|
|
||||||
{
|
|
||||||
server
|
|
||||||
.notify::<lsp::notification::DidCloseTextDocument>(
|
|
||||||
lsp::DidCloseTextDocumentParams {
|
|
||||||
text_document: lsp::TextDocumentIdentifier::new(
|
|
||||||
uri.clone(),
|
|
||||||
),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.log_err();
|
|
||||||
subscriptions_by_buffer_id.remove(&buffer_id);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
server.clone()
|
|
||||||
} else {
|
|
||||||
return Task::ready(Err(anyhow!("must sign in before using copilot")));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
let lsp = server.lsp.clone();
|
||||||
|
let registered_buffer = server.registered_buffers.get_mut(&buffer.id()).unwrap();
|
||||||
|
let snapshot = registered_buffer.report_changes(buffer, cx);
|
||||||
|
let buffer = buffer.read(cx);
|
||||||
|
let uri = registered_buffer.uri.clone();
|
||||||
let settings = cx.global::<Settings>();
|
let settings = cx.global::<Settings>();
|
||||||
let position = position.to_point_utf16(&snapshot);
|
let position = position.to_point_utf16(buffer);
|
||||||
let language = snapshot.language_at(position);
|
let language = buffer.language_at(position);
|
||||||
let language_name = language.map(|language| language.name());
|
let language_name = language.map(|language| language.name());
|
||||||
let language_name = language_name.as_deref();
|
let language_name = language_name.as_deref();
|
||||||
let tab_size = settings.tab_size(language_name);
|
let tab_size = settings.tab_size(language_name);
|
||||||
let hard_tabs = settings.hard_tabs(language_name);
|
let hard_tabs = settings.hard_tabs(language_name);
|
||||||
let language_id = id_for_language(language);
|
let relative_path = buffer
|
||||||
|
.file()
|
||||||
|
.map(|file| file.path().to_path_buf())
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
let path;
|
cx.foreground().spawn(async move {
|
||||||
let relative_path;
|
let (version, snapshot) = snapshot.await?;
|
||||||
if let Some(file) = snapshot.file() {
|
let result = lsp
|
||||||
if let Some(file) = file.as_local() {
|
|
||||||
path = file.abs_path(cx);
|
|
||||||
} else {
|
|
||||||
path = file.full_path(cx);
|
|
||||||
}
|
|
||||||
relative_path = file.path().to_path_buf();
|
|
||||||
} else {
|
|
||||||
path = PathBuf::new();
|
|
||||||
relative_path = PathBuf::new();
|
|
||||||
}
|
|
||||||
|
|
||||||
cx.background().spawn(async move {
|
|
||||||
let result = server
|
|
||||||
.request::<R>(request::GetCompletionsParams {
|
.request::<R>(request::GetCompletionsParams {
|
||||||
doc: request::GetCompletionsDocument {
|
doc: request::GetCompletionsDocument {
|
||||||
source: snapshot.text(),
|
uri,
|
||||||
tab_size: tab_size.into(),
|
tab_size: tab_size.into(),
|
||||||
indent_size: 1,
|
indent_size: 1,
|
||||||
insert_spaces: !hard_tabs,
|
insert_spaces: !hard_tabs,
|
||||||
uri,
|
|
||||||
path: path.to_string_lossy().into(),
|
|
||||||
relative_path: relative_path.to_string_lossy().into(),
|
relative_path: relative_path.to_string_lossy().into(),
|
||||||
language_id,
|
|
||||||
position: point_to_lsp(position),
|
position: point_to_lsp(position),
|
||||||
version: 0,
|
version: version.try_into().unwrap(),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
|
@ -581,6 +834,7 @@ impl Copilot {
|
||||||
let end =
|
let end =
|
||||||
snapshot.clip_point_utf16(point_from_lsp(completion.range.end), Bias::Left);
|
snapshot.clip_point_utf16(point_from_lsp(completion.range.end), Bias::Left);
|
||||||
Completion {
|
Completion {
|
||||||
|
uuid: completion.uuid,
|
||||||
range: snapshot.anchor_before(start)..snapshot.anchor_after(end),
|
range: snapshot.anchor_before(start)..snapshot.anchor_after(end),
|
||||||
text: completion.text,
|
text: completion.text,
|
||||||
}
|
}
|
||||||
|
@ -595,14 +849,16 @@ impl Copilot {
|
||||||
CopilotServer::Starting { task } => Status::Starting { task: task.clone() },
|
CopilotServer::Starting { task } => Status::Starting { task: task.clone() },
|
||||||
CopilotServer::Disabled => Status::Disabled,
|
CopilotServer::Disabled => Status::Disabled,
|
||||||
CopilotServer::Error(error) => Status::Error(error.clone()),
|
CopilotServer::Error(error) => Status::Error(error.clone()),
|
||||||
CopilotServer::Started { status, .. } => match status {
|
CopilotServer::Running(RunningCopilotServer { sign_in_status, .. }) => {
|
||||||
SignInStatus::Authorized { .. } => Status::Authorized,
|
match sign_in_status {
|
||||||
SignInStatus::Unauthorized { .. } => Status::Unauthorized,
|
SignInStatus::Authorized { .. } => Status::Authorized,
|
||||||
SignInStatus::SigningIn { prompt, .. } => Status::SigningIn {
|
SignInStatus::Unauthorized { .. } => Status::Unauthorized,
|
||||||
prompt: prompt.clone(),
|
SignInStatus::SigningIn { prompt, .. } => Status::SigningIn {
|
||||||
},
|
prompt: prompt.clone(),
|
||||||
SignInStatus::SignedOut => Status::SignedOut,
|
},
|
||||||
},
|
SignInStatus::SignedOut => Status::SignedOut,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -611,14 +867,34 @@ impl Copilot {
|
||||||
lsp_status: request::SignInStatus,
|
lsp_status: request::SignInStatus,
|
||||||
cx: &mut ModelContext<Self>,
|
cx: &mut ModelContext<Self>,
|
||||||
) {
|
) {
|
||||||
if let CopilotServer::Started { status, .. } = &mut self.server {
|
self.buffers.retain(|_, buffer| buffer.is_upgradable(cx));
|
||||||
*status = match lsp_status {
|
|
||||||
|
if let Ok(server) = self.server.as_running() {
|
||||||
|
match lsp_status {
|
||||||
request::SignInStatus::Ok { .. }
|
request::SignInStatus::Ok { .. }
|
||||||
| request::SignInStatus::MaybeOk { .. }
|
| request::SignInStatus::MaybeOk { .. }
|
||||||
| request::SignInStatus::AlreadySignedIn { .. } => SignInStatus::Authorized,
|
| request::SignInStatus::AlreadySignedIn { .. } => {
|
||||||
request::SignInStatus::NotAuthorized { .. } => SignInStatus::Unauthorized,
|
server.sign_in_status = SignInStatus::Authorized;
|
||||||
request::SignInStatus::NotSignedIn => SignInStatus::SignedOut,
|
for buffer in self.buffers.values().cloned().collect::<Vec<_>>() {
|
||||||
};
|
if let Some(buffer) = buffer.upgrade(cx) {
|
||||||
|
self.register_buffer(&buffer, cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
request::SignInStatus::NotAuthorized { .. } => {
|
||||||
|
server.sign_in_status = SignInStatus::Unauthorized;
|
||||||
|
for buffer_id in self.buffers.keys().copied().collect::<Vec<_>>() {
|
||||||
|
self.unregister_buffer(buffer_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
request::SignInStatus::NotSignedIn => {
|
||||||
|
server.sign_in_status = SignInStatus::SignedOut;
|
||||||
|
for buffer_id in self.buffers.keys().copied().collect::<Vec<_>>() {
|
||||||
|
self.unregister_buffer(buffer_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -633,6 +909,14 @@ fn id_for_language(language: Option<&Arc<Language>>) -> String {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn uri_for_buffer(buffer: &ModelHandle<Buffer>, cx: &AppContext) -> lsp::Url {
|
||||||
|
if let Some(file) = buffer.read(cx).file().and_then(|file| file.as_local()) {
|
||||||
|
lsp::Url::from_file_path(file.abs_path(cx)).unwrap()
|
||||||
|
} else {
|
||||||
|
format!("buffer://{}", buffer.id()).parse().unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn clear_copilot_dir() {
|
async fn clear_copilot_dir() {
|
||||||
remove_matching(&paths::COPILOT_DIR, |_| true).await
|
remove_matching(&paths::COPILOT_DIR, |_| true).await
|
||||||
}
|
}
|
||||||
|
@ -704,3 +988,226 @@ async fn get_copilot_lsp(http: Arc<dyn HttpClient>) -> anyhow::Result<PathBuf> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use gpui::{executor::Deterministic, TestAppContext};
|
||||||
|
|
||||||
|
#[gpui::test(iterations = 10)]
|
||||||
|
async fn test_buffer_management(deterministic: Arc<Deterministic>, cx: &mut TestAppContext) {
|
||||||
|
deterministic.forbid_parking();
|
||||||
|
let (copilot, mut lsp) = Copilot::fake(cx);
|
||||||
|
|
||||||
|
let buffer_1 = cx.add_model(|cx| Buffer::new(0, "Hello", cx));
|
||||||
|
let buffer_1_uri: lsp::Url = format!("buffer://{}", buffer_1.id()).parse().unwrap();
|
||||||
|
copilot.update(cx, |copilot, cx| copilot.register_buffer(&buffer_1, cx));
|
||||||
|
assert_eq!(
|
||||||
|
lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
|
||||||
|
.await,
|
||||||
|
lsp::DidOpenTextDocumentParams {
|
||||||
|
text_document: lsp::TextDocumentItem::new(
|
||||||
|
buffer_1_uri.clone(),
|
||||||
|
"plaintext".into(),
|
||||||
|
0,
|
||||||
|
"Hello".into()
|
||||||
|
),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
let buffer_2 = cx.add_model(|cx| Buffer::new(0, "Goodbye", cx));
|
||||||
|
let buffer_2_uri: lsp::Url = format!("buffer://{}", buffer_2.id()).parse().unwrap();
|
||||||
|
copilot.update(cx, |copilot, cx| copilot.register_buffer(&buffer_2, cx));
|
||||||
|
assert_eq!(
|
||||||
|
lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
|
||||||
|
.await,
|
||||||
|
lsp::DidOpenTextDocumentParams {
|
||||||
|
text_document: lsp::TextDocumentItem::new(
|
||||||
|
buffer_2_uri.clone(),
|
||||||
|
"plaintext".into(),
|
||||||
|
0,
|
||||||
|
"Goodbye".into()
|
||||||
|
),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
buffer_1.update(cx, |buffer, cx| buffer.edit([(5..5, " world")], None, cx));
|
||||||
|
assert_eq!(
|
||||||
|
lsp.receive_notification::<lsp::notification::DidChangeTextDocument>()
|
||||||
|
.await,
|
||||||
|
lsp::DidChangeTextDocumentParams {
|
||||||
|
text_document: lsp::VersionedTextDocumentIdentifier::new(buffer_1_uri.clone(), 1),
|
||||||
|
content_changes: vec![lsp::TextDocumentContentChangeEvent {
|
||||||
|
range: Some(lsp::Range::new(
|
||||||
|
lsp::Position::new(0, 5),
|
||||||
|
lsp::Position::new(0, 5)
|
||||||
|
)),
|
||||||
|
range_length: None,
|
||||||
|
text: " world".into(),
|
||||||
|
}],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Ensure updates to the file are reflected in the LSP.
|
||||||
|
buffer_1
|
||||||
|
.update(cx, |buffer, cx| {
|
||||||
|
buffer.file_updated(
|
||||||
|
Arc::new(File {
|
||||||
|
abs_path: "/root/child/buffer-1".into(),
|
||||||
|
path: Path::new("child/buffer-1").into(),
|
||||||
|
}),
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
assert_eq!(
|
||||||
|
lsp.receive_notification::<lsp::notification::DidCloseTextDocument>()
|
||||||
|
.await,
|
||||||
|
lsp::DidCloseTextDocumentParams {
|
||||||
|
text_document: lsp::TextDocumentIdentifier::new(buffer_1_uri),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
let buffer_1_uri = lsp::Url::from_file_path("/root/child/buffer-1").unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
|
||||||
|
.await,
|
||||||
|
lsp::DidOpenTextDocumentParams {
|
||||||
|
text_document: lsp::TextDocumentItem::new(
|
||||||
|
buffer_1_uri.clone(),
|
||||||
|
"plaintext".into(),
|
||||||
|
1,
|
||||||
|
"Hello world".into()
|
||||||
|
),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Ensure all previously-registered buffers are closed when signing out.
|
||||||
|
lsp.handle_request::<request::SignOut, _, _>(|_, _| async {
|
||||||
|
Ok(request::SignOutResult {})
|
||||||
|
});
|
||||||
|
copilot
|
||||||
|
.update(cx, |copilot, cx| copilot.sign_out(cx))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
lsp.receive_notification::<lsp::notification::DidCloseTextDocument>()
|
||||||
|
.await,
|
||||||
|
lsp::DidCloseTextDocumentParams {
|
||||||
|
text_document: lsp::TextDocumentIdentifier::new(buffer_2_uri.clone()),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
lsp.receive_notification::<lsp::notification::DidCloseTextDocument>()
|
||||||
|
.await,
|
||||||
|
lsp::DidCloseTextDocumentParams {
|
||||||
|
text_document: lsp::TextDocumentIdentifier::new(buffer_1_uri.clone()),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Ensure all previously-registered buffers are re-opened when signing in.
|
||||||
|
lsp.handle_request::<request::SignInInitiate, _, _>(|_, _| async {
|
||||||
|
Ok(request::SignInInitiateResult::AlreadySignedIn {
|
||||||
|
user: "user-1".into(),
|
||||||
|
})
|
||||||
|
});
|
||||||
|
copilot
|
||||||
|
.update(cx, |copilot, cx| copilot.sign_in(cx))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
|
||||||
|
.await,
|
||||||
|
lsp::DidOpenTextDocumentParams {
|
||||||
|
text_document: lsp::TextDocumentItem::new(
|
||||||
|
buffer_2_uri.clone(),
|
||||||
|
"plaintext".into(),
|
||||||
|
0,
|
||||||
|
"Goodbye".into()
|
||||||
|
),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
|
||||||
|
.await,
|
||||||
|
lsp::DidOpenTextDocumentParams {
|
||||||
|
text_document: lsp::TextDocumentItem::new(
|
||||||
|
buffer_1_uri.clone(),
|
||||||
|
"plaintext".into(),
|
||||||
|
0,
|
||||||
|
"Hello world".into()
|
||||||
|
),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Dropping a buffer causes it to be closed on the LSP side as well.
|
||||||
|
cx.update(|_| drop(buffer_2));
|
||||||
|
assert_eq!(
|
||||||
|
lsp.receive_notification::<lsp::notification::DidCloseTextDocument>()
|
||||||
|
.await,
|
||||||
|
lsp::DidCloseTextDocumentParams {
|
||||||
|
text_document: lsp::TextDocumentIdentifier::new(buffer_2_uri),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
struct File {
|
||||||
|
abs_path: PathBuf,
|
||||||
|
path: Arc<Path>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl language::File for File {
|
||||||
|
fn as_local(&self) -> Option<&dyn language::LocalFile> {
|
||||||
|
Some(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mtime(&self) -> std::time::SystemTime {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn path(&self) -> &Arc<Path> {
|
||||||
|
&self.path
|
||||||
|
}
|
||||||
|
|
||||||
|
fn full_path(&self, _: &AppContext) -> PathBuf {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn file_name<'a>(&'a self, _: &'a AppContext) -> &'a std::ffi::OsStr {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_deleted(&self) -> bool {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_any(&self) -> &dyn std::any::Any {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_proto(&self) -> rpc::proto::File {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl language::LocalFile for File {
|
||||||
|
fn abs_path(&self, _: &AppContext) -> PathBuf {
|
||||||
|
self.abs_path.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load(&self, _: &AppContext) -> Task<Result<String>> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn buffer_reloaded(
|
||||||
|
&self,
|
||||||
|
_: u64,
|
||||||
|
_: &clock::Global,
|
||||||
|
_: language::RopeFingerprint,
|
||||||
|
_: ::fs::LineEnding,
|
||||||
|
_: std::time::SystemTime,
|
||||||
|
_: &mut AppContext,
|
||||||
|
) {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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";
|
||||||
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ use crate::{request::PromptUserDeviceFlow, Copilot, Status};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
elements::*,
|
elements::*,
|
||||||
geometry::rect::RectF,
|
geometry::rect::RectF,
|
||||||
|
impl_internal_actions,
|
||||||
platform::{WindowBounds, WindowKind, WindowOptions},
|
platform::{WindowBounds, WindowKind, WindowOptions},
|
||||||
AnyViewHandle, AppContext, ClipboardItem, Drawable, Element, Entity, View, ViewContext,
|
AnyViewHandle, AppContext, ClipboardItem, Drawable, Element, Entity, View, ViewContext,
|
||||||
ViewHandle,
|
ViewHandle,
|
||||||
|
@ -9,6 +10,11 @@ use gpui::{
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
use theme::ui::modal;
|
use theme::ui::modal;
|
||||||
|
|
||||||
|
#[derive(PartialEq, Eq, Debug, Clone)]
|
||||||
|
struct ClickedConnect;
|
||||||
|
|
||||||
|
impl_internal_actions!(copilot_verification, [ClickedConnect]);
|
||||||
|
|
||||||
#[derive(PartialEq, Eq, Debug, Clone)]
|
#[derive(PartialEq, Eq, Debug, Clone)]
|
||||||
struct CopyUserCode;
|
struct CopyUserCode;
|
||||||
|
|
||||||
|
@ -61,6 +67,12 @@ pub fn init(cx: &mut AppContext) {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
|
|
||||||
|
cx.add_action(
|
||||||
|
|code_verification: &mut CopilotCodeVerification, _: &ClickedConnect, _| {
|
||||||
|
code_verification.connect_clicked = true;
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_copilot_auth_window(
|
fn create_copilot_auth_window(
|
||||||
|
@ -85,11 +97,15 @@ fn create_copilot_auth_window(
|
||||||
|
|
||||||
pub struct CopilotCodeVerification {
|
pub struct CopilotCodeVerification {
|
||||||
status: Status,
|
status: Status,
|
||||||
|
connect_clicked: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CopilotCodeVerification {
|
impl CopilotCodeVerification {
|
||||||
pub fn new(status: Status) -> Self {
|
pub fn new(status: Status) -> Self {
|
||||||
Self { status }
|
Self {
|
||||||
|
status,
|
||||||
|
connect_clicked: false,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_status(&mut self, status: Status, cx: &mut ViewContext<Self>) {
|
pub fn set_status(&mut self, status: Status, cx: &mut ViewContext<Self>) {
|
||||||
|
@ -147,6 +163,7 @@ impl CopilotCodeVerification {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_prompting_modal(
|
fn render_prompting_modal(
|
||||||
|
connect_clicked: bool,
|
||||||
data: &PromptUserDeviceFlow,
|
data: &PromptUserDeviceFlow,
|
||||||
style: &theme::Copilot,
|
style: &theme::Copilot,
|
||||||
cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
|
@ -195,13 +212,20 @@ impl CopilotCodeVerification {
|
||||||
.with_style(style.auth.prompting.hint.container.clone())
|
.with_style(style.auth.prompting.hint.container.clone())
|
||||||
.boxed(),
|
.boxed(),
|
||||||
theme::ui::cta_button_with_click::<ConnectButton, _, _, _>(
|
theme::ui::cta_button_with_click::<ConnectButton, _, _, _>(
|
||||||
"Connect to GitHub",
|
if connect_clicked {
|
||||||
|
"Waiting for connection..."
|
||||||
|
} else {
|
||||||
|
"Connect to GitHub"
|
||||||
|
},
|
||||||
style.auth.content_width,
|
style.auth.content_width,
|
||||||
&style.auth.cta_button,
|
&style.auth.cta_button,
|
||||||
cx,
|
cx,
|
||||||
{
|
{
|
||||||
let verification_uri = data.verification_uri.clone();
|
let verification_uri = data.verification_uri.clone();
|
||||||
move |_, _, cx| cx.platform().open_url(&verification_uri)
|
move |_, _, cx| {
|
||||||
|
cx.platform().open_url(&verification_uri);
|
||||||
|
cx.dispatch_action(ClickedConnect)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.boxed(),
|
.boxed(),
|
||||||
|
@ -350,9 +374,20 @@ impl View for CopilotCodeVerification {
|
||||||
match &self.status {
|
match &self.status {
|
||||||
Status::SigningIn {
|
Status::SigningIn {
|
||||||
prompt: Some(prompt),
|
prompt: Some(prompt),
|
||||||
} => Self::render_prompting_modal(&prompt, &style.copilot, cx),
|
} => Self::render_prompting_modal(
|
||||||
Status::Unauthorized => Self::render_unauthorized_modal(&style.copilot, cx),
|
self.connect_clicked,
|
||||||
Status::Authorized => Self::render_enabled_modal(&style.copilot, cx),
|
&prompt,
|
||||||
|
&style.copilot,
|
||||||
|
cx,
|
||||||
|
),
|
||||||
|
Status::Unauthorized => {
|
||||||
|
self.connect_clicked = false;
|
||||||
|
Self::render_unauthorized_modal(&style.copilot, cx)
|
||||||
|
}
|
||||||
|
Status::Authorized => {
|
||||||
|
self.connect_clicked = false;
|
||||||
|
Self::render_enabled_modal(&style.copilot, cx)
|
||||||
|
}
|
||||||
_ => Empty::new().boxed(),
|
_ => Empty::new().boxed(),
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|
|
@ -23,6 +23,15 @@ const COPILOT_ERROR_TOAST_ID: usize = 1338;
|
||||||
#[derive(Clone, PartialEq)]
|
#[derive(Clone, PartialEq)]
|
||||||
pub struct DeployCopilotMenu;
|
pub struct DeployCopilotMenu;
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq)]
|
||||||
|
pub struct DeployCopilotStartMenu;
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq)]
|
||||||
|
pub struct HideCopilot;
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq)]
|
||||||
|
pub struct InitiateSignIn;
|
||||||
|
|
||||||
#[derive(Clone, PartialEq)]
|
#[derive(Clone, PartialEq)]
|
||||||
pub struct ToggleCopilotForLanguage {
|
pub struct ToggleCopilotForLanguage {
|
||||||
language: Arc<str>,
|
language: Arc<str>,
|
||||||
|
@ -39,6 +48,9 @@ impl_internal_actions!(
|
||||||
copilot,
|
copilot,
|
||||||
[
|
[
|
||||||
DeployCopilotMenu,
|
DeployCopilotMenu,
|
||||||
|
DeployCopilotStartMenu,
|
||||||
|
HideCopilot,
|
||||||
|
InitiateSignIn,
|
||||||
DeployCopilotModal,
|
DeployCopilotModal,
|
||||||
ToggleCopilotForLanguage,
|
ToggleCopilotForLanguage,
|
||||||
ToggleCopilotGlobally,
|
ToggleCopilotGlobally,
|
||||||
|
@ -47,17 +59,19 @@ impl_internal_actions!(
|
||||||
|
|
||||||
pub fn init(cx: &mut AppContext) {
|
pub fn init(cx: &mut AppContext) {
|
||||||
cx.add_action(CopilotButton::deploy_copilot_menu);
|
cx.add_action(CopilotButton::deploy_copilot_menu);
|
||||||
|
cx.add_action(CopilotButton::deploy_copilot_start_menu);
|
||||||
cx.add_action(
|
cx.add_action(
|
||||||
|_: &mut CopilotButton, action: &ToggleCopilotForLanguage, cx| {
|
|_: &mut CopilotButton, action: &ToggleCopilotForLanguage, cx| {
|
||||||
let language = action.language.to_owned();
|
let language = action.language.clone();
|
||||||
|
let show_copilot_suggestions = cx
|
||||||
let current_langauge = cx.global::<Settings>().copilot_on(Some(&language));
|
.global::<Settings>()
|
||||||
|
.show_copilot_suggestions(Some(&language));
|
||||||
|
|
||||||
SettingsFile::update(cx, move |file_contents| {
|
SettingsFile::update(cx, move |file_contents| {
|
||||||
file_contents.languages.insert(
|
file_contents.languages.insert(
|
||||||
language.to_owned(),
|
language,
|
||||||
settings::EditorSettings {
|
settings::EditorSettings {
|
||||||
copilot: Some((!current_langauge).into()),
|
show_copilot_suggestions: Some((!show_copilot_suggestions).into()),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -66,12 +80,63 @@ pub fn init(cx: &mut AppContext) {
|
||||||
);
|
);
|
||||||
|
|
||||||
cx.add_action(|_: &mut CopilotButton, _: &ToggleCopilotGlobally, cx| {
|
cx.add_action(|_: &mut CopilotButton, _: &ToggleCopilotGlobally, cx| {
|
||||||
let copilot_on = cx.global::<Settings>().copilot_on(None);
|
let show_copilot_suggestions = cx.global::<Settings>().show_copilot_suggestions(None);
|
||||||
|
|
||||||
SettingsFile::update(cx, move |file_contents| {
|
SettingsFile::update(cx, move |file_contents| {
|
||||||
file_contents.editor.copilot = Some((!copilot_on).into())
|
file_contents.editor.show_copilot_suggestions = Some((!show_copilot_suggestions).into())
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
cx.add_action(|_: &mut CopilotButton, _: &HideCopilot, cx| {
|
||||||
|
SettingsFile::update(cx, move |file_contents| {
|
||||||
|
file_contents.features.copilot = Some(false)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.add_action(|_: &mut CopilotButton, _: &InitiateSignIn, cx| {
|
||||||
|
let Some(copilot) = Copilot::global(cx) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let status = copilot.read(cx).status();
|
||||||
|
|
||||||
|
match status {
|
||||||
|
Status::Starting { task } => {
|
||||||
|
cx.dispatch_action(workspace::Toast::new(
|
||||||
|
COPILOT_STARTING_TOAST_ID,
|
||||||
|
"Copilot is starting...",
|
||||||
|
));
|
||||||
|
let window_id = cx.window_id();
|
||||||
|
let task = task.to_owned();
|
||||||
|
cx.spawn(|handle, mut cx| async move {
|
||||||
|
task.await;
|
||||||
|
cx.update(|cx| {
|
||||||
|
if let Some(copilot) = Copilot::global(cx) {
|
||||||
|
let status = copilot.read(cx).status();
|
||||||
|
match status {
|
||||||
|
Status::Authorized => cx.dispatch_action_at(
|
||||||
|
window_id,
|
||||||
|
handle.id(),
|
||||||
|
workspace::Toast::new(
|
||||||
|
COPILOT_STARTING_TOAST_ID,
|
||||||
|
"Copilot has started!",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
_ => {
|
||||||
|
cx.dispatch_action_at(
|
||||||
|
window_id,
|
||||||
|
handle.id(),
|
||||||
|
DismissToast::new(COPILOT_STARTING_TOAST_ID),
|
||||||
|
);
|
||||||
|
cx.dispatch_action_at(window_id, handle.id(), SignIn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
_ => cx.dispatch_action(SignIn),
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct CopilotButton {
|
pub struct CopilotButton {
|
||||||
|
@ -93,7 +158,7 @@ impl View for CopilotButton {
|
||||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Element<Self> {
|
fn render(&mut self, cx: &mut ViewContext<Self>) -> Element<Self> {
|
||||||
let settings = cx.global::<Settings>();
|
let settings = cx.global::<Settings>();
|
||||||
|
|
||||||
if !settings.enable_copilot_integration {
|
if !settings.features.copilot {
|
||||||
return Empty::new().boxed();
|
return Empty::new().boxed();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -104,9 +169,9 @@ impl View for CopilotButton {
|
||||||
};
|
};
|
||||||
let status = copilot.read(cx).status();
|
let status = copilot.read(cx).status();
|
||||||
|
|
||||||
let enabled = self.editor_enabled.unwrap_or(settings.copilot_on(None));
|
let enabled = self
|
||||||
|
.editor_enabled
|
||||||
let view_id = cx.view_id();
|
.unwrap_or(settings.show_copilot_suggestions(None));
|
||||||
|
|
||||||
Stack::new()
|
Stack::new()
|
||||||
.with_child(
|
.with_child(
|
||||||
|
@ -154,48 +219,13 @@ impl View for CopilotButton {
|
||||||
let status = status.clone();
|
let status = status.clone();
|
||||||
move |_, _, cx| match status {
|
move |_, _, cx| match status {
|
||||||
Status::Authorized => cx.dispatch_action(DeployCopilotMenu),
|
Status::Authorized => cx.dispatch_action(DeployCopilotMenu),
|
||||||
Status::Starting { ref task } => {
|
|
||||||
cx.dispatch_action(workspace::Toast::new(
|
|
||||||
COPILOT_STARTING_TOAST_ID,
|
|
||||||
"Copilot is starting...",
|
|
||||||
));
|
|
||||||
let window_id = cx.window_id();
|
|
||||||
let task = task.to_owned();
|
|
||||||
cx.spawn_weak(|_this, mut cx| async move {
|
|
||||||
task.await;
|
|
||||||
cx.update(|cx| {
|
|
||||||
if let Some(copilot) = Copilot::global(cx) {
|
|
||||||
let status = copilot.read(cx).status();
|
|
||||||
match status {
|
|
||||||
Status::Authorized => cx.dispatch_action_at(
|
|
||||||
window_id,
|
|
||||||
view_id,
|
|
||||||
workspace::Toast::new(
|
|
||||||
COPILOT_STARTING_TOAST_ID,
|
|
||||||
"Copilot has started!",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
_ => {
|
|
||||||
cx.dispatch_action_at(
|
|
||||||
window_id,
|
|
||||||
view_id,
|
|
||||||
DismissToast::new(COPILOT_STARTING_TOAST_ID),
|
|
||||||
);
|
|
||||||
cx.dispatch_global_action(SignIn)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
}
|
|
||||||
Status::Error(ref e) => cx.dispatch_action(workspace::Toast::new_action(
|
Status::Error(ref e) => cx.dispatch_action(workspace::Toast::new_action(
|
||||||
COPILOT_ERROR_TOAST_ID,
|
COPILOT_ERROR_TOAST_ID,
|
||||||
format!("Copilot can't be started: {}", e),
|
format!("Copilot can't be started: {}", e),
|
||||||
"Reinstall Copilot",
|
"Reinstall Copilot",
|
||||||
Reinstall,
|
Reinstall,
|
||||||
)),
|
)),
|
||||||
_ => cx.dispatch_action(SignIn),
|
_ => cx.dispatch_action(DeployCopilotStartMenu),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.with_tooltip::<Self>(0, "GitHub Copilot".into(), None, theme.tooltip.clone(), cx)
|
.with_tooltip::<Self>(0, "GitHub Copilot".into(), None, theme.tooltip.clone(), cx)
|
||||||
|
@ -235,22 +265,38 @@ impl CopilotButton {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn deploy_copilot_start_menu(
|
||||||
|
&mut self,
|
||||||
|
_: &DeployCopilotStartMenu,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) {
|
||||||
|
let mut menu_options = Vec::with_capacity(2);
|
||||||
|
|
||||||
|
menu_options.push(ContextMenuItem::item("Sign In", InitiateSignIn));
|
||||||
|
menu_options.push(ContextMenuItem::item("Hide Copilot", HideCopilot));
|
||||||
|
|
||||||
|
self.popup_menu.update(cx, |menu, cx| {
|
||||||
|
menu.show(
|
||||||
|
Default::default(),
|
||||||
|
AnchorCorner::BottomRight,
|
||||||
|
menu_options,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
pub fn deploy_copilot_menu(&mut self, _: &DeployCopilotMenu, cx: &mut ViewContext<Self>) {
|
pub fn deploy_copilot_menu(&mut self, _: &DeployCopilotMenu, cx: &mut ViewContext<Self>) {
|
||||||
let settings = cx.global::<Settings>();
|
let settings = cx.global::<Settings>();
|
||||||
|
|
||||||
let mut menu_options = Vec::with_capacity(6);
|
let mut menu_options = Vec::with_capacity(6);
|
||||||
|
|
||||||
if let Some(language) = &self.language {
|
if let Some(language) = &self.language {
|
||||||
let language_enabled = settings.copilot_on(Some(language.as_ref()));
|
let language_enabled = settings.show_copilot_suggestions(Some(language.as_ref()));
|
||||||
|
|
||||||
menu_options.push(ContextMenuItem::item(
|
menu_options.push(ContextMenuItem::item(
|
||||||
format!(
|
format!(
|
||||||
"{} Copilot for {}",
|
"{} Suggestions for {}",
|
||||||
if language_enabled {
|
if language_enabled { "Hide" } else { "Show" },
|
||||||
"Disable"
|
|
||||||
} else {
|
|
||||||
"Enable"
|
|
||||||
},
|
|
||||||
language
|
language
|
||||||
),
|
),
|
||||||
ToggleCopilotForLanguage {
|
ToggleCopilotForLanguage {
|
||||||
|
@ -259,12 +305,12 @@ impl CopilotButton {
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let globally_enabled = cx.global::<Settings>().copilot_on(None);
|
let globally_enabled = cx.global::<Settings>().show_copilot_suggestions(None);
|
||||||
menu_options.push(ContextMenuItem::item(
|
menu_options.push(ContextMenuItem::item(
|
||||||
if globally_enabled {
|
if globally_enabled {
|
||||||
"Disable Copilot Globally"
|
"Hide Suggestions for All Files"
|
||||||
} else {
|
} else {
|
||||||
"Enable Copilot Globally"
|
"Show Suggestions for All Files"
|
||||||
},
|
},
|
||||||
ToggleCopilotGlobally,
|
ToggleCopilotGlobally,
|
||||||
));
|
));
|
||||||
|
@ -312,7 +358,7 @@ impl CopilotButton {
|
||||||
|
|
||||||
self.language = language_name.clone();
|
self.language = language_name.clone();
|
||||||
|
|
||||||
self.editor_enabled = Some(settings.copilot_on(language_name.as_deref()));
|
self.editor_enabled = Some(settings.show_copilot_suggestions(language_name.as_deref()));
|
||||||
|
|
||||||
cx.notify()
|
cx.notify()
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,7 @@ use settings::Settings;
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
use std::{
|
use std::{
|
||||||
any::{Any, TypeId},
|
any::{Any, TypeId},
|
||||||
|
borrow::Cow,
|
||||||
cmp::Ordering,
|
cmp::Ordering,
|
||||||
ops::Range,
|
ops::Range,
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
|
@ -530,6 +531,10 @@ impl Item for ProjectDiagnosticsEditor {
|
||||||
.update(cx, |editor, cx| editor.navigate(data, cx))
|
.update(cx, |editor, cx| editor.navigate(data, cx))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn tab_tooltip_text(&self, _: &AppContext) -> Option<Cow<str>> {
|
||||||
|
Some("Project Diagnostics".into())
|
||||||
|
}
|
||||||
|
|
||||||
fn is_dirty(&self, cx: &AppContext) -> bool {
|
fn is_dirty(&self, cx: &AppContext) -> bool {
|
||||||
self.excerpts.read(cx).is_dirty(cx)
|
self.excerpts.read(cx).is_dirty(cx)
|
||||||
}
|
}
|
||||||
|
|
|
@ -52,7 +52,7 @@ pub use language::{char_kind, CharKind};
|
||||||
use language::{
|
use language::{
|
||||||
AutoindentMode, BracketPair, Buffer, CodeAction, CodeLabel, Completion, CursorShape,
|
AutoindentMode, BracketPair, Buffer, CodeAction, CodeLabel, Completion, CursorShape,
|
||||||
Diagnostic, DiagnosticSeverity, IndentKind, IndentSize, Language, OffsetRangeExt, OffsetUtf16,
|
Diagnostic, DiagnosticSeverity, IndentKind, IndentSize, Language, OffsetRangeExt, OffsetUtf16,
|
||||||
Point, Rope, Selection, SelectionGoal, TransactionId,
|
Point, Selection, SelectionGoal, TransactionId,
|
||||||
};
|
};
|
||||||
use link_go_to_definition::{
|
use link_go_to_definition::{
|
||||||
hide_link_definition, show_link_definition, LinkDefinitionKind, LinkGoToDefinitionState,
|
hide_link_definition, show_link_definition, LinkDefinitionKind, LinkGoToDefinitionState,
|
||||||
|
@ -184,6 +184,7 @@ actions!(
|
||||||
Backspace,
|
Backspace,
|
||||||
Delete,
|
Delete,
|
||||||
Newline,
|
Newline,
|
||||||
|
NewlineAbove,
|
||||||
NewlineBelow,
|
NewlineBelow,
|
||||||
GoToDiagnostic,
|
GoToDiagnostic,
|
||||||
GoToPrevDiagnostic,
|
GoToPrevDiagnostic,
|
||||||
|
@ -301,6 +302,7 @@ pub fn init(cx: &mut AppContext) {
|
||||||
cx.add_action(Editor::select);
|
cx.add_action(Editor::select);
|
||||||
cx.add_action(Editor::cancel);
|
cx.add_action(Editor::cancel);
|
||||||
cx.add_action(Editor::newline);
|
cx.add_action(Editor::newline);
|
||||||
|
cx.add_action(Editor::newline_above);
|
||||||
cx.add_action(Editor::newline_below);
|
cx.add_action(Editor::newline_below);
|
||||||
cx.add_action(Editor::backspace);
|
cx.add_action(Editor::backspace);
|
||||||
cx.add_action(Editor::delete);
|
cx.add_action(Editor::delete);
|
||||||
|
@ -395,6 +397,7 @@ pub fn init(cx: &mut AppContext) {
|
||||||
cx.add_async_action(Editor::find_all_references);
|
cx.add_async_action(Editor::find_all_references);
|
||||||
cx.add_action(Editor::next_copilot_suggestion);
|
cx.add_action(Editor::next_copilot_suggestion);
|
||||||
cx.add_action(Editor::previous_copilot_suggestion);
|
cx.add_action(Editor::previous_copilot_suggestion);
|
||||||
|
cx.add_action(Editor::copilot_suggest);
|
||||||
|
|
||||||
hover_popover::init(cx);
|
hover_popover::init(cx);
|
||||||
link_go_to_definition::init(cx);
|
link_go_to_definition::init(cx);
|
||||||
|
@ -1014,6 +1017,8 @@ impl CodeActionsMenu {
|
||||||
pub struct CopilotState {
|
pub struct CopilotState {
|
||||||
excerpt_id: Option<ExcerptId>,
|
excerpt_id: Option<ExcerptId>,
|
||||||
pending_refresh: Task<Option<()>>,
|
pending_refresh: Task<Option<()>>,
|
||||||
|
pending_cycling_refresh: Task<Option<()>>,
|
||||||
|
cycled: bool,
|
||||||
completions: Vec<copilot::Completion>,
|
completions: Vec<copilot::Completion>,
|
||||||
active_completion_index: usize,
|
active_completion_index: usize,
|
||||||
}
|
}
|
||||||
|
@ -1022,14 +1027,20 @@ impl Default for CopilotState {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
excerpt_id: None,
|
excerpt_id: None,
|
||||||
|
pending_cycling_refresh: Task::ready(Some(())),
|
||||||
pending_refresh: Task::ready(Some(())),
|
pending_refresh: Task::ready(Some(())),
|
||||||
completions: Default::default(),
|
completions: Default::default(),
|
||||||
active_completion_index: 0,
|
active_completion_index: 0,
|
||||||
|
cycled: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CopilotState {
|
impl CopilotState {
|
||||||
|
fn active_completion(&self) -> Option<&copilot::Completion> {
|
||||||
|
self.completions.get(self.active_completion_index)
|
||||||
|
}
|
||||||
|
|
||||||
fn text_for_active_completion(
|
fn text_for_active_completion(
|
||||||
&self,
|
&self,
|
||||||
cursor: Anchor,
|
cursor: Anchor,
|
||||||
|
@ -1037,7 +1048,7 @@ impl CopilotState {
|
||||||
) -> Option<&str> {
|
) -> Option<&str> {
|
||||||
use language::ToOffset as _;
|
use language::ToOffset as _;
|
||||||
|
|
||||||
let completion = self.completions.get(self.active_completion_index)?;
|
let completion = self.active_completion()?;
|
||||||
let excerpt_id = self.excerpt_id?;
|
let excerpt_id = self.excerpt_id?;
|
||||||
let completion_buffer = buffer.buffer_for_excerpt(excerpt_id)?;
|
let completion_buffer = buffer.buffer_for_excerpt(excerpt_id)?;
|
||||||
if excerpt_id != cursor.excerpt_id
|
if excerpt_id != cursor.excerpt_id
|
||||||
|
@ -1068,9 +1079,29 @@ impl CopilotState {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn cycle_completions(&mut self, direction: Direction) {
|
||||||
|
match direction {
|
||||||
|
Direction::Prev => {
|
||||||
|
self.active_completion_index = if self.active_completion_index == 0 {
|
||||||
|
self.completions.len().saturating_sub(1)
|
||||||
|
} else {
|
||||||
|
self.active_completion_index - 1
|
||||||
|
};
|
||||||
|
}
|
||||||
|
Direction::Next => {
|
||||||
|
if self.completions.len() == 0 {
|
||||||
|
self.active_completion_index = 0
|
||||||
|
} else {
|
||||||
|
self.active_completion_index =
|
||||||
|
(self.active_completion_index + 1) % self.completions.len();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn push_completion(&mut self, new_completion: copilot::Completion) {
|
fn push_completion(&mut self, new_completion: copilot::Completion) {
|
||||||
for completion in &self.completions {
|
for completion in &self.completions {
|
||||||
if *completion == new_completion {
|
if completion.text == new_completion.text && completion.range == new_completion.range {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1265,7 +1296,7 @@ impl Editor {
|
||||||
cx.subscribe(&buffer, Self::on_buffer_event),
|
cx.subscribe(&buffer, Self::on_buffer_event),
|
||||||
cx.observe(&display_map, Self::on_display_map_changed),
|
cx.observe(&display_map, Self::on_display_map_changed),
|
||||||
cx.observe(&blink_manager, |_, _, cx| cx.notify()),
|
cx.observe(&blink_manager, |_, _, cx| cx.notify()),
|
||||||
cx.observe_global::<Settings, _>(Self::on_settings_changed),
|
cx.observe_global::<Settings, _>(Self::settings_changed),
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
this.end_selection(cx);
|
this.end_selection(cx);
|
||||||
|
@ -1469,7 +1500,7 @@ impl Editor {
|
||||||
self.refresh_code_actions(cx);
|
self.refresh_code_actions(cx);
|
||||||
self.refresh_document_highlights(cx);
|
self.refresh_document_highlights(cx);
|
||||||
refresh_matching_bracket_highlights(self, cx);
|
refresh_matching_bracket_highlights(self, cx);
|
||||||
self.hide_copilot_suggestion(cx);
|
self.discard_copilot_suggestion(cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.blink_manager.update(cx, BlinkManager::pause_blinking);
|
self.blink_manager.update(cx, BlinkManager::pause_blinking);
|
||||||
|
@ -1843,7 +1874,7 @@ impl Editor {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.hide_copilot_suggestion(cx).is_some() {
|
if self.discard_copilot_suggestion(cx) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2026,13 +2057,13 @@ impl Editor {
|
||||||
this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(new_selections));
|
this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(new_selections));
|
||||||
|
|
||||||
if had_active_copilot_suggestion {
|
if had_active_copilot_suggestion {
|
||||||
this.refresh_copilot_suggestions(cx);
|
this.refresh_copilot_suggestions(true, cx);
|
||||||
if !this.has_active_copilot_suggestion(cx) {
|
if !this.has_active_copilot_suggestion(cx) {
|
||||||
this.trigger_completion_on_input(&text, cx);
|
this.trigger_completion_on_input(&text, cx);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.trigger_completion_on_input(&text, cx);
|
this.trigger_completion_on_input(&text, cx);
|
||||||
this.refresh_copilot_suggestions(cx);
|
this.refresh_copilot_suggestions(true, cx);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -2114,7 +2145,66 @@ impl Editor {
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(new_selections));
|
this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(new_selections));
|
||||||
this.refresh_copilot_suggestions(cx);
|
this.refresh_copilot_suggestions(true, cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn newline_above(&mut self, _: &NewlineAbove, cx: &mut ViewContext<Self>) {
|
||||||
|
let buffer = self.buffer.read(cx);
|
||||||
|
let snapshot = buffer.snapshot(cx);
|
||||||
|
|
||||||
|
let mut edits = Vec::new();
|
||||||
|
let mut rows = Vec::new();
|
||||||
|
let mut rows_inserted = 0;
|
||||||
|
|
||||||
|
for selection in self.selections.all_adjusted(cx) {
|
||||||
|
let cursor = selection.head();
|
||||||
|
let row = cursor.row;
|
||||||
|
|
||||||
|
let start_of_line = snapshot.clip_point(Point::new(row, 0), Bias::Left);
|
||||||
|
|
||||||
|
let newline = "\n".to_string();
|
||||||
|
edits.push((start_of_line..start_of_line, newline));
|
||||||
|
|
||||||
|
rows.push(row + rows_inserted);
|
||||||
|
rows_inserted += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.transact(cx, |editor, cx| {
|
||||||
|
editor.edit(edits, cx);
|
||||||
|
|
||||||
|
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||||
|
let mut index = 0;
|
||||||
|
s.move_cursors_with(|map, _, _| {
|
||||||
|
let row = rows[index];
|
||||||
|
index += 1;
|
||||||
|
|
||||||
|
let point = Point::new(row, 0);
|
||||||
|
let boundary = map.next_line_boundary(point).1;
|
||||||
|
let clipped = map.clip_point(boundary, Bias::Left);
|
||||||
|
|
||||||
|
(clipped, SelectionGoal::None)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut indent_edits = Vec::new();
|
||||||
|
let multibuffer_snapshot = editor.buffer.read(cx).snapshot(cx);
|
||||||
|
for row in rows {
|
||||||
|
let indents = multibuffer_snapshot.suggested_indents(row..row + 1, cx);
|
||||||
|
for (row, indent) in indents {
|
||||||
|
if indent.len == 0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let text = match indent.kind {
|
||||||
|
IndentKind::Space => " ".repeat(indent.len as usize),
|
||||||
|
IndentKind::Tab => "\t".repeat(indent.len as usize),
|
||||||
|
};
|
||||||
|
let point = Point::new(row, 0);
|
||||||
|
indent_edits.push((point..point, text));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
editor.edit(indent_edits, cx);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2130,19 +2220,18 @@ impl Editor {
|
||||||
let cursor = selection.head();
|
let cursor = selection.head();
|
||||||
let row = cursor.row;
|
let row = cursor.row;
|
||||||
|
|
||||||
let end_of_line = snapshot
|
let point = Point::new(row + 1, 0);
|
||||||
.clip_point(Point::new(row, snapshot.line_len(row)), Bias::Left)
|
let start_of_line = snapshot.clip_point(point, Bias::Left);
|
||||||
.to_point(&snapshot);
|
|
||||||
|
|
||||||
let newline = "\n".to_string();
|
let newline = "\n".to_string();
|
||||||
edits.push((end_of_line..end_of_line, newline));
|
edits.push((start_of_line..start_of_line, newline));
|
||||||
|
|
||||||
rows_inserted += 1;
|
rows_inserted += 1;
|
||||||
rows.push(row + rows_inserted);
|
rows.push(row + rows_inserted);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.transact(cx, |editor, cx| {
|
self.transact(cx, |editor, cx| {
|
||||||
editor.edit_with_autoindent(edits, cx);
|
editor.edit(edits, cx);
|
||||||
|
|
||||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||||
let mut index = 0;
|
let mut index = 0;
|
||||||
|
@ -2157,6 +2246,25 @@ impl Editor {
|
||||||
(clipped, SelectionGoal::None)
|
(clipped, SelectionGoal::None)
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let mut indent_edits = Vec::new();
|
||||||
|
let multibuffer_snapshot = editor.buffer.read(cx).snapshot(cx);
|
||||||
|
for row in rows {
|
||||||
|
let indents = multibuffer_snapshot.suggested_indents(row..row + 1, cx);
|
||||||
|
for (row, indent) in indents {
|
||||||
|
if indent.len == 0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let text = match indent.kind {
|
||||||
|
IndentKind::Space => " ".repeat(indent.len as usize),
|
||||||
|
IndentKind::Tab => "\t".repeat(indent.len as usize),
|
||||||
|
};
|
||||||
|
let point = Point::new(row, 0);
|
||||||
|
indent_edits.push((point..point, text));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
editor.edit(indent_edits, cx);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2512,7 +2620,7 @@ impl Editor {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
this.refresh_copilot_suggestions(cx);
|
this.refresh_copilot_suggestions(true, cx);
|
||||||
});
|
});
|
||||||
|
|
||||||
let project = self.project.clone()?;
|
let project = self.project.clone()?;
|
||||||
|
@ -2809,10 +2917,14 @@ impl Editor {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
fn refresh_copilot_suggestions(&mut self, cx: &mut ViewContext<Self>) -> Option<()> {
|
fn refresh_copilot_suggestions(
|
||||||
|
&mut self,
|
||||||
|
debounce: bool,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) -> Option<()> {
|
||||||
let copilot = Copilot::global(cx)?;
|
let copilot = Copilot::global(cx)?;
|
||||||
if self.mode != EditorMode::Full || !copilot.read(cx).status().is_authorized() {
|
if self.mode != EditorMode::Full || !copilot.read(cx).status().is_authorized() {
|
||||||
self.hide_copilot_suggestion(cx);
|
self.clear_copilot_suggestions(cx);
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
self.update_visible_copilot_suggestion(cx);
|
self.update_visible_copilot_suggestion(cx);
|
||||||
|
@ -2820,29 +2932,36 @@ impl Editor {
|
||||||
let snapshot = self.buffer.read(cx).snapshot(cx);
|
let snapshot = self.buffer.read(cx).snapshot(cx);
|
||||||
let cursor = self.selections.newest_anchor().head();
|
let cursor = self.selections.newest_anchor().head();
|
||||||
let language_name = snapshot.language_at(cursor).map(|language| language.name());
|
let language_name = snapshot.language_at(cursor).map(|language| language.name());
|
||||||
if !cx.global::<Settings>().copilot_on(language_name.as_deref()) {
|
if !cx
|
||||||
self.hide_copilot_suggestion(cx);
|
.global::<Settings>()
|
||||||
|
.show_copilot_suggestions(language_name.as_deref())
|
||||||
|
{
|
||||||
|
self.clear_copilot_suggestions(cx);
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let (buffer, buffer_position) =
|
let (buffer, buffer_position) =
|
||||||
self.buffer.read(cx).text_anchor_for_position(cursor, cx)?;
|
self.buffer.read(cx).text_anchor_for_position(cursor, cx)?;
|
||||||
self.copilot_state.pending_refresh = cx.spawn_weak(|this, mut cx| async move {
|
self.copilot_state.pending_refresh = cx.spawn_weak(|this, mut cx| async move {
|
||||||
cx.background().timer(COPILOT_DEBOUNCE_TIMEOUT).await;
|
if debounce {
|
||||||
let (completion, completions_cycling) = copilot.update(&mut cx, |copilot, cx| {
|
cx.background().timer(COPILOT_DEBOUNCE_TIMEOUT).await;
|
||||||
(
|
}
|
||||||
copilot.completions(&buffer, buffer_position, cx),
|
|
||||||
copilot.completions_cycling(&buffer, buffer_position, cx),
|
let completions = copilot
|
||||||
)
|
.update(&mut cx, |copilot, cx| {
|
||||||
});
|
copilot.completions(&buffer, buffer_position, cx)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.log_err()
|
||||||
|
.into_iter()
|
||||||
|
.flatten()
|
||||||
|
.collect_vec();
|
||||||
|
|
||||||
let (completion, completions_cycling) = futures::join!(completion, completions_cycling);
|
|
||||||
let mut completions = Vec::new();
|
|
||||||
completions.extend(completion.log_err().into_iter().flatten());
|
|
||||||
completions.extend(completions_cycling.log_err().into_iter().flatten());
|
|
||||||
this.upgrade(&cx)?
|
this.upgrade(&cx)?
|
||||||
.update(&mut cx, |this, cx| {
|
.update(&mut cx, |this, cx| {
|
||||||
if !completions.is_empty() {
|
if !completions.is_empty() {
|
||||||
|
this.copilot_state.cycled = false;
|
||||||
|
this.copilot_state.pending_cycling_refresh = Task::ready(None);
|
||||||
this.copilot_state.completions.clear();
|
this.copilot_state.completions.clear();
|
||||||
this.copilot_state.active_completion_index = 0;
|
this.copilot_state.active_completion_index = 0;
|
||||||
this.copilot_state.excerpt_id = Some(cursor.excerpt_id);
|
this.copilot_state.excerpt_id = Some(cursor.excerpt_id);
|
||||||
|
@ -2853,46 +2972,116 @@ impl Editor {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.log_err()?;
|
.log_err()?;
|
||||||
|
|
||||||
Some(())
|
Some(())
|
||||||
});
|
});
|
||||||
|
|
||||||
Some(())
|
Some(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn next_copilot_suggestion(&mut self, _: &copilot::NextSuggestion, cx: &mut ViewContext<Self>) {
|
fn cycle_copilot_suggestions(
|
||||||
|
&mut self,
|
||||||
|
direction: Direction,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) -> Option<()> {
|
||||||
|
let copilot = Copilot::global(cx)?;
|
||||||
|
if self.mode != EditorMode::Full || !copilot.read(cx).status().is_authorized() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.copilot_state.cycled {
|
||||||
|
self.copilot_state.cycle_completions(direction);
|
||||||
|
self.update_visible_copilot_suggestion(cx);
|
||||||
|
} else {
|
||||||
|
let cursor = self.selections.newest_anchor().head();
|
||||||
|
let (buffer, buffer_position) =
|
||||||
|
self.buffer.read(cx).text_anchor_for_position(cursor, cx)?;
|
||||||
|
self.copilot_state.pending_cycling_refresh = cx.spawn_weak(|this, mut cx| async move {
|
||||||
|
let completions = copilot
|
||||||
|
.update(&mut cx, |copilot, cx| {
|
||||||
|
copilot.completions_cycling(&buffer, buffer_position, cx)
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
this.upgrade(&cx)?
|
||||||
|
.update(&mut cx, |this, cx| {
|
||||||
|
this.copilot_state.cycled = true;
|
||||||
|
for completion in completions.log_err().into_iter().flatten() {
|
||||||
|
this.copilot_state.push_completion(completion);
|
||||||
|
}
|
||||||
|
this.copilot_state.cycle_completions(direction);
|
||||||
|
this.update_visible_copilot_suggestion(cx);
|
||||||
|
})
|
||||||
|
.log_err()?;
|
||||||
|
|
||||||
|
Some(())
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn copilot_suggest(&mut self, _: &copilot::Suggest, cx: &mut ViewContext<Self>) {
|
||||||
if !self.has_active_copilot_suggestion(cx) {
|
if !self.has_active_copilot_suggestion(cx) {
|
||||||
self.refresh_copilot_suggestions(cx);
|
self.refresh_copilot_suggestions(false, cx);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.copilot_state.active_completion_index =
|
|
||||||
(self.copilot_state.active_completion_index + 1) % self.copilot_state.completions.len();
|
|
||||||
self.update_visible_copilot_suggestion(cx);
|
self.update_visible_copilot_suggestion(cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn next_copilot_suggestion(&mut self, _: &copilot::NextSuggestion, cx: &mut ViewContext<Self>) {
|
||||||
|
if self.has_active_copilot_suggestion(cx) {
|
||||||
|
self.cycle_copilot_suggestions(Direction::Next, cx);
|
||||||
|
} else {
|
||||||
|
self.refresh_copilot_suggestions(false, cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn previous_copilot_suggestion(
|
fn previous_copilot_suggestion(
|
||||||
&mut self,
|
&mut self,
|
||||||
_: &copilot::PreviousSuggestion,
|
_: &copilot::PreviousSuggestion,
|
||||||
cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
) {
|
) {
|
||||||
if !self.has_active_copilot_suggestion(cx) {
|
if self.has_active_copilot_suggestion(cx) {
|
||||||
self.refresh_copilot_suggestions(cx);
|
self.cycle_copilot_suggestions(Direction::Prev, cx);
|
||||||
return;
|
} else {
|
||||||
|
self.refresh_copilot_suggestions(false, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.copilot_state.active_completion_index =
|
|
||||||
if self.copilot_state.active_completion_index == 0 {
|
|
||||||
self.copilot_state.completions.len() - 1
|
|
||||||
} else {
|
|
||||||
self.copilot_state.active_completion_index - 1
|
|
||||||
};
|
|
||||||
self.update_visible_copilot_suggestion(cx);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn accept_copilot_suggestion(&mut self, cx: &mut ViewContext<Self>) -> bool {
|
fn accept_copilot_suggestion(&mut self, cx: &mut ViewContext<Self>) -> bool {
|
||||||
if let Some(text) = self.hide_copilot_suggestion(cx) {
|
if let Some(suggestion) = self
|
||||||
self.insert_with_autoindent_mode(&text.to_string(), None, cx);
|
.display_map
|
||||||
|
.update(cx, |map, cx| map.replace_suggestion::<usize>(None, cx))
|
||||||
|
{
|
||||||
|
if let Some((copilot, completion)) =
|
||||||
|
Copilot::global(cx).zip(self.copilot_state.active_completion())
|
||||||
|
{
|
||||||
|
copilot
|
||||||
|
.update(cx, |copilot, cx| copilot.accept_completion(completion, cx))
|
||||||
|
.detach_and_log_err(cx);
|
||||||
|
}
|
||||||
|
self.insert_with_autoindent_mode(&suggestion.text.to_string(), None, cx);
|
||||||
|
cx.notify();
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn discard_copilot_suggestion(&mut self, cx: &mut ViewContext<Self>) -> bool {
|
||||||
|
if self.has_active_copilot_suggestion(cx) {
|
||||||
|
if let Some(copilot) = Copilot::global(cx) {
|
||||||
|
copilot
|
||||||
|
.update(cx, |copilot, cx| {
|
||||||
|
copilot.discard_completions(&self.copilot_state.completions, cx)
|
||||||
|
})
|
||||||
|
.detach_and_log_err(cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.display_map
|
||||||
|
.update(cx, |map, cx| map.replace_suggestion::<usize>(None, cx));
|
||||||
|
cx.notify();
|
||||||
true
|
true
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
|
@ -2903,18 +3092,6 @@ impl Editor {
|
||||||
self.display_map.read(cx).has_suggestion()
|
self.display_map.read(cx).has_suggestion()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn hide_copilot_suggestion(&mut self, cx: &mut ViewContext<Self>) -> Option<Rope> {
|
|
||||||
if self.has_active_copilot_suggestion(cx) {
|
|
||||||
let old_suggestion = self
|
|
||||||
.display_map
|
|
||||||
.update(cx, |map, cx| map.replace_suggestion::<usize>(None, cx));
|
|
||||||
cx.notify();
|
|
||||||
old_suggestion.map(|suggestion| suggestion.text)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn update_visible_copilot_suggestion(&mut self, cx: &mut ViewContext<Self>) {
|
fn update_visible_copilot_suggestion(&mut self, cx: &mut ViewContext<Self>) {
|
||||||
let snapshot = self.buffer.read(cx).snapshot(cx);
|
let snapshot = self.buffer.read(cx).snapshot(cx);
|
||||||
let selection = self.selections.newest_anchor();
|
let selection = self.selections.newest_anchor();
|
||||||
|
@ -2924,26 +3101,31 @@ impl Editor {
|
||||||
|| !self.completion_tasks.is_empty()
|
|| !self.completion_tasks.is_empty()
|
||||||
|| selection.start != selection.end
|
|| selection.start != selection.end
|
||||||
{
|
{
|
||||||
self.hide_copilot_suggestion(cx);
|
self.discard_copilot_suggestion(cx);
|
||||||
} else if let Some(text) = self
|
} else if let Some(text) = self
|
||||||
.copilot_state
|
.copilot_state
|
||||||
.text_for_active_completion(cursor, &snapshot)
|
.text_for_active_completion(cursor, &snapshot)
|
||||||
{
|
{
|
||||||
self.display_map.update(cx, |map, cx| {
|
self.display_map.update(cx, move |map, cx| {
|
||||||
map.replace_suggestion(
|
map.replace_suggestion(
|
||||||
Some(Suggestion {
|
Some(Suggestion {
|
||||||
position: cursor,
|
position: cursor,
|
||||||
text: text.into(),
|
text: text.trim_end().into(),
|
||||||
}),
|
}),
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
cx.notify();
|
cx.notify();
|
||||||
} else {
|
} else {
|
||||||
self.hide_copilot_suggestion(cx);
|
self.discard_copilot_suggestion(cx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn clear_copilot_suggestions(&mut self, cx: &mut ViewContext<Self>) {
|
||||||
|
self.copilot_state = Default::default();
|
||||||
|
self.discard_copilot_suggestion(cx);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn render_code_actions_indicator(
|
pub fn render_code_actions_indicator(
|
||||||
&self,
|
&self,
|
||||||
style: &EditorStyle,
|
style: &EditorStyle,
|
||||||
|
@ -3059,7 +3241,7 @@ impl Editor {
|
||||||
self.completion_tasks.clear();
|
self.completion_tasks.clear();
|
||||||
}
|
}
|
||||||
self.context_menu = Some(menu);
|
self.context_menu = Some(menu);
|
||||||
self.hide_copilot_suggestion(cx);
|
self.discard_copilot_suggestion(cx);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3229,7 +3411,7 @@ impl Editor {
|
||||||
|
|
||||||
this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(selections));
|
this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(selections));
|
||||||
this.insert("", cx);
|
this.insert("", cx);
|
||||||
this.refresh_copilot_suggestions(cx);
|
this.refresh_copilot_suggestions(true, cx);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3245,7 +3427,7 @@ impl Editor {
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
this.insert("", cx);
|
this.insert("", cx);
|
||||||
this.refresh_copilot_suggestions(cx);
|
this.refresh_copilot_suggestions(true, cx);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3341,7 +3523,7 @@ impl Editor {
|
||||||
self.transact(cx, |this, cx| {
|
self.transact(cx, |this, cx| {
|
||||||
this.buffer.update(cx, |b, cx| b.edit(edits, None, cx));
|
this.buffer.update(cx, |b, cx| b.edit(edits, None, cx));
|
||||||
this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(selections));
|
this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(selections));
|
||||||
this.refresh_copilot_suggestions(cx);
|
this.refresh_copilot_suggestions(true, cx);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4021,7 +4203,7 @@ impl Editor {
|
||||||
}
|
}
|
||||||
self.request_autoscroll(Autoscroll::fit(), cx);
|
self.request_autoscroll(Autoscroll::fit(), cx);
|
||||||
self.unmark_text(cx);
|
self.unmark_text(cx);
|
||||||
self.refresh_copilot_suggestions(cx);
|
self.refresh_copilot_suggestions(true, cx);
|
||||||
cx.emit(Event::Edited);
|
cx.emit(Event::Edited);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4036,7 +4218,7 @@ impl Editor {
|
||||||
}
|
}
|
||||||
self.request_autoscroll(Autoscroll::fit(), cx);
|
self.request_autoscroll(Autoscroll::fit(), cx);
|
||||||
self.unmark_text(cx);
|
self.unmark_text(cx);
|
||||||
self.refresh_copilot_suggestions(cx);
|
self.refresh_copilot_suggestions(true, cx);
|
||||||
cx.emit(Event::Edited);
|
cx.emit(Event::Edited);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6490,6 +6672,7 @@ impl Editor {
|
||||||
multi_buffer::Event::DiagnosticsUpdated => {
|
multi_buffer::Event::DiagnosticsUpdated => {
|
||||||
self.refresh_active_diagnostics(cx);
|
self.refresh_active_diagnostics(cx);
|
||||||
}
|
}
|
||||||
|
multi_buffer::Event::LanguageChanged => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6497,8 +6680,8 @@ impl Editor {
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_settings_changed(&mut self, cx: &mut ViewContext<Self>) {
|
fn settings_changed(&mut self, cx: &mut ViewContext<Self>) {
|
||||||
self.refresh_copilot_suggestions(cx);
|
self.refresh_copilot_suggestions(true, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_searchable(&mut self, searchable: bool) {
|
pub fn set_searchable(&mut self, searchable: bool) {
|
||||||
|
|
|
@ -1488,6 +1488,55 @@ fn test_newline_with_old_selections(cx: &mut TestAppContext) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_newline_above(cx: &mut gpui::TestAppContext) {
|
||||||
|
let mut cx = EditorTestContext::new(cx);
|
||||||
|
cx.update(|cx| {
|
||||||
|
cx.update_global::<Settings, _, _>(|settings, _| {
|
||||||
|
settings.editor_overrides.tab_size = Some(NonZeroU32::new(4).unwrap());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
let language = Arc::new(
|
||||||
|
Language::new(
|
||||||
|
LanguageConfig::default(),
|
||||||
|
Some(tree_sitter_rust::language()),
|
||||||
|
)
|
||||||
|
.with_indents_query(r#"(_ "(" ")" @end) @indent"#)
|
||||||
|
.unwrap(),
|
||||||
|
);
|
||||||
|
cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
|
||||||
|
|
||||||
|
cx.set_state(indoc! {"
|
||||||
|
const a: ˇA = (
|
||||||
|
(ˇ
|
||||||
|
«const_functionˇ»(ˇ),
|
||||||
|
so«mˇ»et«hˇ»ing_ˇelse,ˇ
|
||||||
|
)ˇ
|
||||||
|
ˇ);ˇ
|
||||||
|
"});
|
||||||
|
cx.update_editor(|e, cx| e.newline_above(&NewlineAbove, cx));
|
||||||
|
cx.assert_editor_state(indoc! {"
|
||||||
|
ˇ
|
||||||
|
const a: A = (
|
||||||
|
ˇ
|
||||||
|
(
|
||||||
|
ˇ
|
||||||
|
ˇ
|
||||||
|
const_function(),
|
||||||
|
ˇ
|
||||||
|
ˇ
|
||||||
|
ˇ
|
||||||
|
ˇ
|
||||||
|
something_else,
|
||||||
|
ˇ
|
||||||
|
)
|
||||||
|
ˇ
|
||||||
|
ˇ
|
||||||
|
);
|
||||||
|
"});
|
||||||
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_newline_below(cx: &mut gpui::TestAppContext) {
|
async fn test_newline_below(cx: &mut gpui::TestAppContext) {
|
||||||
let mut cx = EditorTestContext::new(cx);
|
let mut cx = EditorTestContext::new(cx);
|
||||||
|
|
|
@ -3,12 +3,12 @@ use crate::{
|
||||||
movement::surrounding_word, persistence::DB, scroll::ScrollAnchor, Anchor, Autoscroll, Editor,
|
movement::surrounding_word, persistence::DB, scroll::ScrollAnchor, Anchor, Autoscroll, Editor,
|
||||||
Event, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, NavigationData, ToPoint as _,
|
Event, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, NavigationData, ToPoint as _,
|
||||||
};
|
};
|
||||||
use anyhow::{anyhow, Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use collections::HashSet;
|
use collections::HashSet;
|
||||||
use futures::future::try_join_all;
|
use futures::future::try_join_all;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
elements::*, geometry::vector::vec2f, AppContext, Entity, ModelHandle, Subscription, Task,
|
elements::*, geometry::vector::vec2f, AppContext, AsyncAppContext, Entity, ModelHandle,
|
||||||
View, ViewContext, ViewHandle, WeakViewHandle,
|
Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle,
|
||||||
};
|
};
|
||||||
use language::{
|
use language::{
|
||||||
proto::serialize_anchor as serialize_text_anchor, Bias, Buffer, OffsetRangeExt, Point,
|
proto::serialize_anchor as serialize_text_anchor, Bias, Buffer, OffsetRangeExt, Point,
|
||||||
|
@ -72,11 +72,11 @@ impl FollowableItem for Editor {
|
||||||
let editor = pane.read_with(&cx, |pane, cx| {
|
let editor = pane.read_with(&cx, |pane, cx| {
|
||||||
let mut editors = pane.items_of_type::<Self>();
|
let mut editors = pane.items_of_type::<Self>();
|
||||||
editors.find(|editor| {
|
editors.find(|editor| {
|
||||||
editor.remote_id(&client, cx) == Some(remote_id)
|
let ids_match = editor.remote_id(&client, cx) == Some(remote_id);
|
||||||
|| state.singleton
|
let singleton_buffer_matches = state.singleton
|
||||||
&& buffers.len() == 1
|
&& buffers.first()
|
||||||
&& editor.read(cx).buffer.read(cx).as_singleton().as_ref()
|
== editor.read(cx).buffer.read(cx).as_singleton().as_ref();
|
||||||
== Some(&buffers[0])
|
ids_match || singleton_buffer_matches
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -117,46 +117,29 @@ impl FollowableItem for Editor {
|
||||||
multibuffer
|
multibuffer
|
||||||
});
|
});
|
||||||
|
|
||||||
cx.add_view(|cx| Editor::for_multibuffer(multibuffer, Some(project), cx))
|
cx.add_view(|cx| {
|
||||||
|
let mut editor =
|
||||||
|
Editor::for_multibuffer(multibuffer, Some(project.clone()), cx);
|
||||||
|
editor.remote_id = Some(remote_id);
|
||||||
|
editor
|
||||||
|
})
|
||||||
})?
|
})?
|
||||||
};
|
};
|
||||||
|
|
||||||
editor.update(&mut cx, |editor, cx| {
|
update_editor_from_message(
|
||||||
editor.remote_id = Some(remote_id);
|
editor.clone(),
|
||||||
let buffer = editor.buffer.read(cx).read(cx);
|
project,
|
||||||
let selections = state
|
proto::update_view::Editor {
|
||||||
.selections
|
selections: state.selections,
|
||||||
.into_iter()
|
pending_selection: state.pending_selection,
|
||||||
.map(|selection| {
|
scroll_top_anchor: state.scroll_top_anchor,
|
||||||
deserialize_selection(&buffer, selection)
|
scroll_x: state.scroll_x,
|
||||||
.ok_or_else(|| anyhow!("invalid selection"))
|
scroll_y: state.scroll_y,
|
||||||
})
|
..Default::default()
|
||||||
.collect::<Result<Vec<_>>>()?;
|
},
|
||||||
let pending_selection = state
|
&mut cx,
|
||||||
.pending_selection
|
)
|
||||||
.map(|selection| deserialize_selection(&buffer, selection))
|
.await?;
|
||||||
.flatten();
|
|
||||||
let scroll_top_anchor = state
|
|
||||||
.scroll_top_anchor
|
|
||||||
.and_then(|anchor| deserialize_anchor(&buffer, anchor));
|
|
||||||
drop(buffer);
|
|
||||||
|
|
||||||
if !selections.is_empty() || pending_selection.is_some() {
|
|
||||||
editor.set_selections_from_remote(selections, pending_selection, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(scroll_top_anchor) = scroll_top_anchor {
|
|
||||||
editor.set_scroll_anchor_remote(
|
|
||||||
ScrollAnchor {
|
|
||||||
top_anchor: scroll_top_anchor,
|
|
||||||
offset: vec2f(state.scroll_x, state.scroll_y),
|
|
||||||
},
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
anyhow::Ok(())
|
|
||||||
})??;
|
|
||||||
|
|
||||||
Ok(editor)
|
Ok(editor)
|
||||||
}))
|
}))
|
||||||
|
@ -301,96 +284,9 @@ impl FollowableItem for Editor {
|
||||||
cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
) -> Task<Result<()>> {
|
) -> Task<Result<()>> {
|
||||||
let update_view::Variant::Editor(message) = message;
|
let update_view::Variant::Editor(message) = message;
|
||||||
let multibuffer = self.buffer.read(cx);
|
|
||||||
let multibuffer = multibuffer.read(cx);
|
|
||||||
|
|
||||||
let buffer_ids = message
|
|
||||||
.inserted_excerpts
|
|
||||||
.iter()
|
|
||||||
.filter_map(|insertion| Some(insertion.excerpt.as_ref()?.buffer_id))
|
|
||||||
.collect::<HashSet<_>>();
|
|
||||||
|
|
||||||
let mut removals = message
|
|
||||||
.deleted_excerpts
|
|
||||||
.into_iter()
|
|
||||||
.map(ExcerptId::from_proto)
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
removals.sort_by(|a, b| a.cmp(&b, &multibuffer));
|
|
||||||
|
|
||||||
let selections = message
|
|
||||||
.selections
|
|
||||||
.into_iter()
|
|
||||||
.filter_map(|selection| deserialize_selection(&multibuffer, selection))
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
let pending_selection = message
|
|
||||||
.pending_selection
|
|
||||||
.and_then(|selection| deserialize_selection(&multibuffer, selection));
|
|
||||||
|
|
||||||
let scroll_top_anchor = message
|
|
||||||
.scroll_top_anchor
|
|
||||||
.and_then(|anchor| deserialize_anchor(&multibuffer, anchor));
|
|
||||||
drop(multibuffer);
|
|
||||||
|
|
||||||
let buffers = project.update(cx, |project, cx| {
|
|
||||||
buffer_ids
|
|
||||||
.into_iter()
|
|
||||||
.map(|id| project.open_buffer_by_id(id, cx))
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
});
|
|
||||||
|
|
||||||
let project = project.clone();
|
let project = project.clone();
|
||||||
cx.spawn(|this, mut cx| async move {
|
cx.spawn(|this, mut cx| async move {
|
||||||
let _buffers = try_join_all(buffers).await?;
|
update_editor_from_message(this, project, message, &mut cx).await
|
||||||
this.update(&mut cx, |this, cx| {
|
|
||||||
this.buffer.update(cx, |multibuffer, cx| {
|
|
||||||
let mut insertions = message.inserted_excerpts.into_iter().peekable();
|
|
||||||
while let Some(insertion) = insertions.next() {
|
|
||||||
let Some(excerpt) = insertion.excerpt else { continue };
|
|
||||||
let Some(previous_excerpt_id) = insertion.previous_excerpt_id else { continue };
|
|
||||||
let buffer_id = excerpt.buffer_id;
|
|
||||||
let Some(buffer) = project.read(cx).buffer_for_id(buffer_id, cx) else { continue };
|
|
||||||
|
|
||||||
let adjacent_excerpts = iter::from_fn(|| {
|
|
||||||
let insertion = insertions.peek()?;
|
|
||||||
if insertion.previous_excerpt_id.is_none()
|
|
||||||
&& insertion.excerpt.as_ref()?.buffer_id == buffer_id
|
|
||||||
{
|
|
||||||
insertions.next()?.excerpt
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
multibuffer.insert_excerpts_with_ids_after(
|
|
||||||
ExcerptId::from_proto(previous_excerpt_id),
|
|
||||||
buffer,
|
|
||||||
[excerpt]
|
|
||||||
.into_iter()
|
|
||||||
.chain(adjacent_excerpts)
|
|
||||||
.filter_map(|excerpt| {
|
|
||||||
Some((
|
|
||||||
ExcerptId::from_proto(excerpt.id),
|
|
||||||
deserialize_excerpt_range(excerpt)?,
|
|
||||||
))
|
|
||||||
}),
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
multibuffer.remove_excerpts(removals, cx);
|
|
||||||
});
|
|
||||||
|
|
||||||
if !selections.is_empty() || pending_selection.is_some() {
|
|
||||||
this.set_selections_from_remote(selections, pending_selection, cx);
|
|
||||||
this.request_autoscroll_remotely(Autoscroll::newest(), cx);
|
|
||||||
} else if let Some(anchor) = scroll_top_anchor {
|
|
||||||
this.set_scroll_anchor_remote(ScrollAnchor {
|
|
||||||
top_anchor: anchor,
|
|
||||||
offset: vec2f(message.scroll_x, message.scroll_y)
|
|
||||||
}, cx);
|
|
||||||
}
|
|
||||||
})?;
|
|
||||||
Ok(())
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -404,6 +300,128 @@ impl FollowableItem for Editor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn update_editor_from_message(
|
||||||
|
this: ViewHandle<Editor>,
|
||||||
|
project: ModelHandle<Project>,
|
||||||
|
message: proto::update_view::Editor,
|
||||||
|
cx: &mut AsyncAppContext,
|
||||||
|
) -> Result<()> {
|
||||||
|
// Open all of the buffers of which excerpts were added to the editor.
|
||||||
|
let inserted_excerpt_buffer_ids = message
|
||||||
|
.inserted_excerpts
|
||||||
|
.iter()
|
||||||
|
.filter_map(|insertion| Some(insertion.excerpt.as_ref()?.buffer_id))
|
||||||
|
.collect::<HashSet<_>>();
|
||||||
|
let inserted_excerpt_buffers = project.update(cx, |project, cx| {
|
||||||
|
inserted_excerpt_buffer_ids
|
||||||
|
.into_iter()
|
||||||
|
.map(|id| project.open_buffer_by_id(id, cx))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
});
|
||||||
|
let _inserted_excerpt_buffers = try_join_all(inserted_excerpt_buffers).await?;
|
||||||
|
|
||||||
|
// Update the editor's excerpts.
|
||||||
|
this.update(cx, |editor, cx| {
|
||||||
|
editor.buffer.update(cx, |multibuffer, cx| {
|
||||||
|
let mut removed_excerpt_ids = message
|
||||||
|
.deleted_excerpts
|
||||||
|
.into_iter()
|
||||||
|
.map(ExcerptId::from_proto)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
removed_excerpt_ids.sort_by({
|
||||||
|
let multibuffer = multibuffer.read(cx);
|
||||||
|
move |a, b| a.cmp(&b, &multibuffer)
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut insertions = message.inserted_excerpts.into_iter().peekable();
|
||||||
|
while let Some(insertion) = insertions.next() {
|
||||||
|
let Some(excerpt) = insertion.excerpt else { continue };
|
||||||
|
let Some(previous_excerpt_id) = insertion.previous_excerpt_id else { continue };
|
||||||
|
let buffer_id = excerpt.buffer_id;
|
||||||
|
let Some(buffer) = project.read(cx).buffer_for_id(buffer_id, cx) else { continue };
|
||||||
|
|
||||||
|
let adjacent_excerpts = iter::from_fn(|| {
|
||||||
|
let insertion = insertions.peek()?;
|
||||||
|
if insertion.previous_excerpt_id.is_none()
|
||||||
|
&& insertion.excerpt.as_ref()?.buffer_id == buffer_id
|
||||||
|
{
|
||||||
|
insertions.next()?.excerpt
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
multibuffer.insert_excerpts_with_ids_after(
|
||||||
|
ExcerptId::from_proto(previous_excerpt_id),
|
||||||
|
buffer,
|
||||||
|
[excerpt]
|
||||||
|
.into_iter()
|
||||||
|
.chain(adjacent_excerpts)
|
||||||
|
.filter_map(|excerpt| {
|
||||||
|
Some((
|
||||||
|
ExcerptId::from_proto(excerpt.id),
|
||||||
|
deserialize_excerpt_range(excerpt)?,
|
||||||
|
))
|
||||||
|
}),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
multibuffer.remove_excerpts(removed_excerpt_ids, cx);
|
||||||
|
});
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Deserialize the editor state.
|
||||||
|
let (selections, pending_selection, scroll_top_anchor) = this.update(cx, |editor, cx| {
|
||||||
|
let buffer = editor.buffer.read(cx).read(cx);
|
||||||
|
let selections = message
|
||||||
|
.selections
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|selection| deserialize_selection(&buffer, selection))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let pending_selection = message
|
||||||
|
.pending_selection
|
||||||
|
.and_then(|selection| deserialize_selection(&buffer, selection));
|
||||||
|
let scroll_top_anchor = message
|
||||||
|
.scroll_top_anchor
|
||||||
|
.and_then(|anchor| deserialize_anchor(&buffer, anchor));
|
||||||
|
anyhow::Ok((selections, pending_selection, scroll_top_anchor))
|
||||||
|
})??;
|
||||||
|
|
||||||
|
// Wait until the buffer has received all of the operations referenced by
|
||||||
|
// the editor's new state.
|
||||||
|
this.update(cx, |editor, cx| {
|
||||||
|
editor.buffer.update(cx, |buffer, cx| {
|
||||||
|
buffer.wait_for_anchors(
|
||||||
|
selections
|
||||||
|
.iter()
|
||||||
|
.chain(pending_selection.as_ref())
|
||||||
|
.flat_map(|selection| [selection.start, selection.end])
|
||||||
|
.chain(scroll_top_anchor),
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})?
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Update the editor's state.
|
||||||
|
this.update(cx, |editor, cx| {
|
||||||
|
if !selections.is_empty() || pending_selection.is_some() {
|
||||||
|
editor.set_selections_from_remote(selections, pending_selection, cx);
|
||||||
|
editor.request_autoscroll_remotely(Autoscroll::newest(), cx);
|
||||||
|
} else if let Some(scroll_top_anchor) = scroll_top_anchor {
|
||||||
|
editor.set_scroll_anchor_remote(
|
||||||
|
ScrollAnchor {
|
||||||
|
top_anchor: scroll_top_anchor,
|
||||||
|
offset: vec2f(message.scroll_x, message.scroll_y),
|
||||||
|
},
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn serialize_excerpt(
|
fn serialize_excerpt(
|
||||||
buffer_id: u64,
|
buffer_id: u64,
|
||||||
id: &ExcerptId,
|
id: &ExcerptId,
|
||||||
|
@ -516,7 +534,24 @@ impl Item for Editor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn tab_description<'a>(&'a self, detail: usize, cx: &'a AppContext) -> Option<Cow<'a, str>> {
|
fn tab_tooltip_text(&self, cx: &AppContext) -> Option<Cow<str>> {
|
||||||
|
let file_path = self
|
||||||
|
.buffer()
|
||||||
|
.read(cx)
|
||||||
|
.as_singleton()?
|
||||||
|
.read(cx)
|
||||||
|
.file()
|
||||||
|
.and_then(|f| f.as_local())?
|
||||||
|
.abs_path(cx);
|
||||||
|
|
||||||
|
let file_path = util::paths::compact(&file_path)
|
||||||
|
.to_string_lossy()
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
Some(file_path.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tab_description<'a>(&'a self, detail: usize, cx: &'a AppContext) -> Option<Cow<str>> {
|
||||||
match path_for_buffer(&self.buffer, detail, true, cx)? {
|
match path_for_buffer(&self.buffer, detail, true, cx)? {
|
||||||
Cow::Borrowed(path) => Some(path.to_string_lossy()),
|
Cow::Borrowed(path) => Some(path.to_string_lossy()),
|
||||||
Cow::Owned(path) => Some(path.to_string_lossy().to_string().into()),
|
Cow::Owned(path) => Some(path.to_string_lossy().to_string().into()),
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
mod anchor;
|
mod anchor;
|
||||||
|
|
||||||
pub use anchor::{Anchor, AnchorRangeExt};
|
pub use anchor::{Anchor, AnchorRangeExt};
|
||||||
|
use anyhow::{anyhow, Result};
|
||||||
use clock::ReplicaId;
|
use clock::ReplicaId;
|
||||||
use collections::{BTreeMap, Bound, HashMap, HashSet};
|
use collections::{BTreeMap, Bound, HashMap, HashSet};
|
||||||
use futures::{channel::mpsc, SinkExt};
|
use futures::{channel::mpsc, SinkExt};
|
||||||
|
@ -16,7 +17,9 @@ use language::{
|
||||||
use std::{
|
use std::{
|
||||||
borrow::Cow,
|
borrow::Cow,
|
||||||
cell::{Ref, RefCell},
|
cell::{Ref, RefCell},
|
||||||
cmp, fmt, io,
|
cmp, fmt,
|
||||||
|
future::Future,
|
||||||
|
io,
|
||||||
iter::{self, FromIterator},
|
iter::{self, FromIterator},
|
||||||
mem,
|
mem,
|
||||||
ops::{Range, RangeBounds, Sub},
|
ops::{Range, RangeBounds, Sub},
|
||||||
|
@ -61,6 +64,7 @@ pub enum Event {
|
||||||
},
|
},
|
||||||
Edited,
|
Edited,
|
||||||
Reloaded,
|
Reloaded,
|
||||||
|
LanguageChanged,
|
||||||
Reparsed,
|
Reparsed,
|
||||||
Saved,
|
Saved,
|
||||||
FileHandleChanged,
|
FileHandleChanged,
|
||||||
|
@ -1238,6 +1242,39 @@ impl MultiBuffer {
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn wait_for_anchors<'a>(
|
||||||
|
&self,
|
||||||
|
anchors: impl 'a + Iterator<Item = Anchor>,
|
||||||
|
cx: &mut ModelContext<Self>,
|
||||||
|
) -> impl 'static + Future<Output = Result<()>> {
|
||||||
|
let borrow = self.buffers.borrow();
|
||||||
|
let mut error = None;
|
||||||
|
let mut futures = Vec::new();
|
||||||
|
for anchor in anchors {
|
||||||
|
if let Some(buffer_id) = anchor.buffer_id {
|
||||||
|
if let Some(buffer) = borrow.get(&buffer_id) {
|
||||||
|
buffer.buffer.update(cx, |buffer, _| {
|
||||||
|
futures.push(buffer.wait_for_anchors([anchor.text_anchor]))
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
error = Some(anyhow!(
|
||||||
|
"buffer {buffer_id} is not part of this multi-buffer"
|
||||||
|
));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async move {
|
||||||
|
if let Some(error) = error {
|
||||||
|
Err(error)?;
|
||||||
|
}
|
||||||
|
for future in futures {
|
||||||
|
future.await?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn text_anchor_for_position<T: ToOffset>(
|
pub fn text_anchor_for_position<T: ToOffset>(
|
||||||
&self,
|
&self,
|
||||||
position: T,
|
position: T,
|
||||||
|
@ -1266,6 +1303,7 @@ impl MultiBuffer {
|
||||||
language::Event::Saved => Event::Saved,
|
language::Event::Saved => Event::Saved,
|
||||||
language::Event::FileHandleChanged => Event::FileHandleChanged,
|
language::Event::FileHandleChanged => Event::FileHandleChanged,
|
||||||
language::Event::Reloaded => Event::Reloaded,
|
language::Event::Reloaded => Event::Reloaded,
|
||||||
|
language::Event::LanguageChanged => Event::LanguageChanged,
|
||||||
language::Event::Reparsed => Event::Reparsed,
|
language::Event::Reparsed => Event::Reparsed,
|
||||||
language::Event::DiagnosticsUpdated => Event::DiagnosticsUpdated,
|
language::Event::DiagnosticsUpdated => Event::DiagnosticsUpdated,
|
||||||
language::Event::Closed => Event::Closed,
|
language::Event::Closed => Event::Closed,
|
||||||
|
|
|
@ -166,7 +166,7 @@ impl<'a> EditorTestContext<'a> {
|
||||||
///
|
///
|
||||||
/// See the `util::test::marked_text_ranges` function for more information.
|
/// See the `util::test::marked_text_ranges` function for more information.
|
||||||
pub fn set_state(&mut self, marked_text: &str) -> ContextHandle {
|
pub fn set_state(&mut self, marked_text: &str) -> ContextHandle {
|
||||||
let _state_context = self.add_assertion_context(format!(
|
let state_context = self.add_assertion_context(format!(
|
||||||
"Initial Editor State: \"{}\"",
|
"Initial Editor State: \"{}\"",
|
||||||
marked_text.escape_debug().to_string()
|
marked_text.escape_debug().to_string()
|
||||||
));
|
));
|
||||||
|
@ -177,7 +177,23 @@ impl<'a> EditorTestContext<'a> {
|
||||||
s.select_ranges(selection_ranges)
|
s.select_ranges(selection_ranges)
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
_state_context
|
state_context
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Only change the editor's selections
|
||||||
|
pub fn set_selections_state(&mut self, marked_text: &str) -> ContextHandle {
|
||||||
|
let state_context = self.add_assertion_context(format!(
|
||||||
|
"Initial Editor State: \"{}\"",
|
||||||
|
marked_text.escape_debug().to_string()
|
||||||
|
));
|
||||||
|
let (unmarked_text, selection_ranges) = marked_text_ranges(marked_text, true);
|
||||||
|
self.editor.update(self.cx, |editor, cx| {
|
||||||
|
assert_eq!(editor.text(cx), unmarked_text);
|
||||||
|
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||||
|
s.select_ranges(selection_ranges)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
state_context
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Make an assertion about the editor's text and the ranges and directions
|
/// Make an assertion about the editor's text and the ranges and directions
|
||||||
|
@ -188,10 +204,11 @@ impl<'a> EditorTestContext<'a> {
|
||||||
pub fn assert_editor_state(&mut self, marked_text: &str) {
|
pub fn assert_editor_state(&mut self, marked_text: &str) {
|
||||||
let (unmarked_text, expected_selections) = marked_text_ranges(marked_text, true);
|
let (unmarked_text, expected_selections) = marked_text_ranges(marked_text, true);
|
||||||
let buffer_text = self.buffer_text();
|
let buffer_text = self.buffer_text();
|
||||||
assert_eq!(
|
|
||||||
buffer_text, unmarked_text,
|
if buffer_text != unmarked_text {
|
||||||
"Unmarked text doesn't match buffer text"
|
panic!("Unmarked text doesn't match buffer text\nBuffer text: {buffer_text:?}\nUnmarked text: {unmarked_text:?}\nRaw buffer text\n{buffer_text}Raw unmarked text\n{unmarked_text}");
|
||||||
);
|
}
|
||||||
|
|
||||||
self.assert_selections(expected_selections, marked_text.to_string())
|
self.assert_selections(expected_selections, marked_text.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
use std::{
|
use std::{
|
||||||
any::TypeId,
|
any::TypeId,
|
||||||
|
borrow::Cow,
|
||||||
ops::{Range, RangeInclusive},
|
ops::{Range, RangeInclusive},
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
};
|
};
|
||||||
|
@ -245,6 +246,10 @@ impl Entity for FeedbackEditor {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Item for FeedbackEditor {
|
impl Item for FeedbackEditor {
|
||||||
|
fn tab_tooltip_text(&self, _: &AppContext) -> Option<Cow<str>> {
|
||||||
|
Some("Send Feedback".into())
|
||||||
|
}
|
||||||
|
|
||||||
fn tab_content(&self, _: Option<usize>, style: &theme::Tab, _: &AppContext) -> Element<Pane> {
|
fn tab_content(&self, _: Option<usize>, style: &theme::Tab, _: &AppContext) -> Element<Pane> {
|
||||||
Flex::row()
|
Flex::row()
|
||||||
.with_child(
|
.with_child(
|
||||||
|
|
|
@ -37,7 +37,7 @@ pub struct TooltipStyle {
|
||||||
pub container: ContainerStyle,
|
pub container: ContainerStyle,
|
||||||
pub text: TextStyle,
|
pub text: TextStyle,
|
||||||
keystroke: KeystrokeStyle,
|
keystroke: KeystrokeStyle,
|
||||||
pub max_text_width: f32,
|
pub max_text_width: Option<f32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Deserialize, Default)]
|
#[derive(Clone, Deserialize, Default)]
|
||||||
|
@ -135,9 +135,14 @@ impl<V: View> Tooltip<V> {
|
||||||
) -> impl Drawable<V> {
|
) -> impl Drawable<V> {
|
||||||
Flex::row()
|
Flex::row()
|
||||||
.with_child({
|
.with_child({
|
||||||
let text = Text::new(text, style.text)
|
let text = if let Some(max_text_width) = style.max_text_width {
|
||||||
.constrained()
|
Text::new(text, style.text)
|
||||||
.with_max_width(style.max_text_width);
|
.constrained()
|
||||||
|
.with_max_width(max_text_width)
|
||||||
|
} else {
|
||||||
|
Text::new(text, style.text).constrained()
|
||||||
|
};
|
||||||
|
|
||||||
if measure {
|
if measure {
|
||||||
text.flex(1., false).boxed()
|
text.flex(1., false).boxed()
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -46,7 +46,7 @@ pub fn new_journal_entry(app_state: Arc<AppState>, cx: &mut AppContext) {
|
||||||
cx.spawn(|mut cx| async move {
|
cx.spawn(|mut cx| async move {
|
||||||
let (journal_dir, entry_path) = create_entry.await?;
|
let (journal_dir, entry_path) = create_entry.await?;
|
||||||
let (workspace, _) = cx
|
let (workspace, _) = cx
|
||||||
.update(|cx| workspace::open_paths(&[journal_dir], &app_state, cx))
|
.update(|cx| workspace::open_paths(&[journal_dir], &app_state, None, cx))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let opened = workspace
|
let opened = workspace
|
||||||
|
|
|
@ -187,6 +187,7 @@ pub enum Event {
|
||||||
Saved,
|
Saved,
|
||||||
FileHandleChanged,
|
FileHandleChanged,
|
||||||
Reloaded,
|
Reloaded,
|
||||||
|
LanguageChanged,
|
||||||
Reparsed,
|
Reparsed,
|
||||||
DiagnosticsUpdated,
|
DiagnosticsUpdated,
|
||||||
Closed,
|
Closed,
|
||||||
|
@ -536,6 +537,7 @@ impl Buffer {
|
||||||
self.syntax_map.lock().clear();
|
self.syntax_map.lock().clear();
|
||||||
self.language = language;
|
self.language = language;
|
||||||
self.reparse(cx);
|
self.reparse(cx);
|
||||||
|
cx.emit(Event::LanguageChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_language_registry(&mut self, language_registry: Arc<LanguageRegistry>) {
|
pub fn set_language_registry(&mut self, language_registry: Arc<LanguageRegistry>) {
|
||||||
|
@ -1313,10 +1315,10 @@ impl Buffer {
|
||||||
self.text.wait_for_edits(edit_ids)
|
self.text.wait_for_edits(edit_ids)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn wait_for_anchors<'a>(
|
pub fn wait_for_anchors(
|
||||||
&mut self,
|
&mut self,
|
||||||
anchors: impl IntoIterator<Item = &'a Anchor>,
|
anchors: impl IntoIterator<Item = Anchor>,
|
||||||
) -> impl Future<Output = Result<()>> {
|
) -> impl 'static + Future<Output = Result<()>> {
|
||||||
self.text.wait_for_anchors(anchors)
|
self.text.wait_for_anchors(anchors)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -81,14 +81,14 @@ fn test_select_language() {
|
||||||
// matching file extension
|
// matching file extension
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
registry
|
registry
|
||||||
.language_for_path("zed/lib.rs")
|
.language_for_file("zed/lib.rs", None)
|
||||||
.now_or_never()
|
.now_or_never()
|
||||||
.and_then(|l| Some(l.ok()?.name())),
|
.and_then(|l| Some(l.ok()?.name())),
|
||||||
Some("Rust".into())
|
Some("Rust".into())
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
registry
|
registry
|
||||||
.language_for_path("zed/lib.mk")
|
.language_for_file("zed/lib.mk", None)
|
||||||
.now_or_never()
|
.now_or_never()
|
||||||
.and_then(|l| Some(l.ok()?.name())),
|
.and_then(|l| Some(l.ok()?.name())),
|
||||||
Some("Make".into())
|
Some("Make".into())
|
||||||
|
@ -97,7 +97,7 @@ fn test_select_language() {
|
||||||
// matching filename
|
// matching filename
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
registry
|
registry
|
||||||
.language_for_path("zed/Makefile")
|
.language_for_file("zed/Makefile", None)
|
||||||
.now_or_never()
|
.now_or_never()
|
||||||
.and_then(|l| Some(l.ok()?.name())),
|
.and_then(|l| Some(l.ok()?.name())),
|
||||||
Some("Make".into())
|
Some("Make".into())
|
||||||
|
@ -106,21 +106,21 @@ fn test_select_language() {
|
||||||
// matching suffix that is not the full file extension or filename
|
// matching suffix that is not the full file extension or filename
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
registry
|
registry
|
||||||
.language_for_path("zed/cars")
|
.language_for_file("zed/cars", None)
|
||||||
.now_or_never()
|
.now_or_never()
|
||||||
.and_then(|l| Some(l.ok()?.name())),
|
.and_then(|l| Some(l.ok()?.name())),
|
||||||
None
|
None
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
registry
|
registry
|
||||||
.language_for_path("zed/a.cars")
|
.language_for_file("zed/a.cars", None)
|
||||||
.now_or_never()
|
.now_or_never()
|
||||||
.and_then(|l| Some(l.ok()?.name())),
|
.and_then(|l| Some(l.ok()?.name())),
|
||||||
None
|
None
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
registry
|
registry
|
||||||
.language_for_path("zed/sumk")
|
.language_for_file("zed/sumk", None)
|
||||||
.now_or_never()
|
.now_or_never()
|
||||||
.and_then(|l| Some(l.ok()?.name())),
|
.and_then(|l| Some(l.ok()?.name())),
|
||||||
None
|
None
|
||||||
|
|
|
@ -262,6 +262,8 @@ pub struct LanguageConfig {
|
||||||
pub name: Arc<str>,
|
pub name: Arc<str>,
|
||||||
pub path_suffixes: Vec<String>,
|
pub path_suffixes: Vec<String>,
|
||||||
pub brackets: BracketPairConfig,
|
pub brackets: BracketPairConfig,
|
||||||
|
#[serde(default, deserialize_with = "deserialize_regex")]
|
||||||
|
pub first_line_pattern: Option<Regex>,
|
||||||
#[serde(default = "auto_indent_using_last_non_empty_line_default")]
|
#[serde(default = "auto_indent_using_last_non_empty_line_default")]
|
||||||
pub auto_indent_using_last_non_empty_line: bool,
|
pub auto_indent_using_last_non_empty_line: bool,
|
||||||
#[serde(default, deserialize_with = "deserialize_regex")]
|
#[serde(default, deserialize_with = "deserialize_regex")]
|
||||||
|
@ -334,6 +336,7 @@ impl Default for LanguageConfig {
|
||||||
path_suffixes: Default::default(),
|
path_suffixes: Default::default(),
|
||||||
brackets: Default::default(),
|
brackets: Default::default(),
|
||||||
auto_indent_using_last_non_empty_line: auto_indent_using_last_non_empty_line_default(),
|
auto_indent_using_last_non_empty_line: auto_indent_using_last_non_empty_line_default(),
|
||||||
|
first_line_pattern: Default::default(),
|
||||||
increase_indent_pattern: Default::default(),
|
increase_indent_pattern: Default::default(),
|
||||||
decrease_indent_pattern: Default::default(),
|
decrease_indent_pattern: Default::default(),
|
||||||
autoclose_before: Default::default(),
|
autoclose_before: Default::default(),
|
||||||
|
@ -660,19 +663,30 @@ impl LanguageRegistry {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn language_for_path(
|
pub fn language_for_file(
|
||||||
self: &Arc<Self>,
|
self: &Arc<Self>,
|
||||||
path: impl AsRef<Path>,
|
path: impl AsRef<Path>,
|
||||||
|
content: Option<&Rope>,
|
||||||
) -> UnwrapFuture<oneshot::Receiver<Result<Arc<Language>>>> {
|
) -> UnwrapFuture<oneshot::Receiver<Result<Arc<Language>>>> {
|
||||||
let path = path.as_ref();
|
let path = path.as_ref();
|
||||||
let filename = path.file_name().and_then(|name| name.to_str());
|
let filename = path.file_name().and_then(|name| name.to_str());
|
||||||
let extension = path.extension().and_then(|name| name.to_str());
|
let extension = path.extension().and_then(|name| name.to_str());
|
||||||
let path_suffixes = [extension, filename];
|
let path_suffixes = [extension, filename];
|
||||||
self.get_or_load_language(|config| {
|
self.get_or_load_language(|config| {
|
||||||
config
|
let path_matches = config
|
||||||
.path_suffixes
|
.path_suffixes
|
||||||
.iter()
|
.iter()
|
||||||
.any(|suffix| path_suffixes.contains(&Some(suffix.as_str())))
|
.any(|suffix| path_suffixes.contains(&Some(suffix.as_str())));
|
||||||
|
let content_matches = content.zip(config.first_line_pattern.as_ref()).map_or(
|
||||||
|
false,
|
||||||
|
|(content, pattern)| {
|
||||||
|
let end = content.clip_point(Point::new(0, 256), Bias::Left);
|
||||||
|
let end = content.point_to_offset(end);
|
||||||
|
let text = content.chunks_in_range(0..end).collect::<String>();
|
||||||
|
pattern.is_match(&text)
|
||||||
|
},
|
||||||
|
);
|
||||||
|
path_matches || content_matches
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1528,9 +1542,45 @@ pub fn range_from_lsp(range: lsp::Range) -> Range<Unclipped<PointUtf16>> {
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use super::*;
|
||||||
use gpui::TestAppContext;
|
use gpui::TestAppContext;
|
||||||
|
|
||||||
use super::*;
|
#[gpui::test(iterations = 10)]
|
||||||
|
async fn test_first_line_pattern(cx: &mut TestAppContext) {
|
||||||
|
let mut languages = LanguageRegistry::test();
|
||||||
|
languages.set_executor(cx.background());
|
||||||
|
let languages = Arc::new(languages);
|
||||||
|
languages.register(
|
||||||
|
"/javascript",
|
||||||
|
LanguageConfig {
|
||||||
|
name: "JavaScript".into(),
|
||||||
|
path_suffixes: vec!["js".into()],
|
||||||
|
first_line_pattern: Some(Regex::new(r"\bnode\b").unwrap()),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
tree_sitter_javascript::language(),
|
||||||
|
None,
|
||||||
|
|_| Default::default(),
|
||||||
|
);
|
||||||
|
|
||||||
|
languages
|
||||||
|
.language_for_file("the/script", None)
|
||||||
|
.await
|
||||||
|
.unwrap_err();
|
||||||
|
languages
|
||||||
|
.language_for_file("the/script", Some(&"nothing".into()))
|
||||||
|
.await
|
||||||
|
.unwrap_err();
|
||||||
|
assert_eq!(
|
||||||
|
languages
|
||||||
|
.language_for_file("the/script", Some(&"#!/bin/env node".into()))
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.name()
|
||||||
|
.as_ref(),
|
||||||
|
"JavaScript"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[gpui::test(iterations = 10)]
|
#[gpui::test(iterations = 10)]
|
||||||
async fn test_language_loading(cx: &mut TestAppContext) {
|
async fn test_language_loading(cx: &mut TestAppContext) {
|
||||||
|
|
|
@ -187,8 +187,6 @@ impl<D: PickerDelegate> Picker<D> {
|
||||||
confirmed: false,
|
confirmed: false,
|
||||||
pending_update_matches: Task::ready(None),
|
pending_update_matches: Task::ready(None),
|
||||||
};
|
};
|
||||||
// TODO! How can the delegate notify the picker to update?
|
|
||||||
// cx.observe(&delegate, |_, _, cx| cx.notify()).detach();
|
|
||||||
this.update_matches(String::new(), cx);
|
this.update_matches(String::new(), cx);
|
||||||
this
|
this
|
||||||
}
|
}
|
||||||
|
|
|
@ -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" }
|
||||||
|
|
|
@ -572,7 +572,7 @@ async fn location_links_from_proto(
|
||||||
.and_then(deserialize_anchor)
|
.and_then(deserialize_anchor)
|
||||||
.ok_or_else(|| anyhow!("missing origin end"))?;
|
.ok_or_else(|| anyhow!("missing origin end"))?;
|
||||||
buffer
|
buffer
|
||||||
.update(&mut cx, |buffer, _| buffer.wait_for_anchors([&start, &end]))
|
.update(&mut cx, |buffer, _| buffer.wait_for_anchors([start, end]))
|
||||||
.await?;
|
.await?;
|
||||||
Some(Location {
|
Some(Location {
|
||||||
buffer,
|
buffer,
|
||||||
|
@ -597,7 +597,7 @@ async fn location_links_from_proto(
|
||||||
.and_then(deserialize_anchor)
|
.and_then(deserialize_anchor)
|
||||||
.ok_or_else(|| anyhow!("missing target end"))?;
|
.ok_or_else(|| anyhow!("missing target end"))?;
|
||||||
buffer
|
buffer
|
||||||
.update(&mut cx, |buffer, _| buffer.wait_for_anchors([&start, &end]))
|
.update(&mut cx, |buffer, _| buffer.wait_for_anchors([start, end]))
|
||||||
.await?;
|
.await?;
|
||||||
let target = Location {
|
let target = Location {
|
||||||
buffer,
|
buffer,
|
||||||
|
@ -868,7 +868,7 @@ impl LspCommand for GetReferences {
|
||||||
.and_then(deserialize_anchor)
|
.and_then(deserialize_anchor)
|
||||||
.ok_or_else(|| anyhow!("missing target end"))?;
|
.ok_or_else(|| anyhow!("missing target end"))?;
|
||||||
target_buffer
|
target_buffer
|
||||||
.update(&mut cx, |buffer, _| buffer.wait_for_anchors([&start, &end]))
|
.update(&mut cx, |buffer, _| buffer.wait_for_anchors([start, end]))
|
||||||
.await?;
|
.await?;
|
||||||
locations.push(Location {
|
locations.push(Location {
|
||||||
buffer: target_buffer,
|
buffer: target_buffer,
|
||||||
|
@ -1012,7 +1012,7 @@ impl LspCommand for GetDocumentHighlights {
|
||||||
.and_then(deserialize_anchor)
|
.and_then(deserialize_anchor)
|
||||||
.ok_or_else(|| anyhow!("missing target end"))?;
|
.ok_or_else(|| anyhow!("missing target end"))?;
|
||||||
buffer
|
buffer
|
||||||
.update(&mut cx, |buffer, _| buffer.wait_for_anchors([&start, &end]))
|
.update(&mut cx, |buffer, _| buffer.wait_for_anchors([start, end]))
|
||||||
.await?;
|
.await?;
|
||||||
let kind = match proto::document_highlight::Kind::from_i32(highlight.kind) {
|
let kind = match proto::document_highlight::Kind::from_i32(highlight.kind) {
|
||||||
Some(proto::document_highlight::Kind::Text) => DocumentHighlightKind::TEXT,
|
Some(proto::document_highlight::Kind::Text) => DocumentHighlightKind::TEXT,
|
||||||
|
|
|
@ -12,6 +12,7 @@ use anyhow::{anyhow, Context, Result};
|
||||||
use client::{proto, Client, TypedEnvelope, UserStore};
|
use client::{proto, Client, TypedEnvelope, UserStore};
|
||||||
use clock::ReplicaId;
|
use clock::ReplicaId;
|
||||||
use collections::{hash_map, BTreeMap, HashMap, HashSet};
|
use collections::{hash_map, BTreeMap, HashMap, HashSet};
|
||||||
|
use copilot::Copilot;
|
||||||
use futures::{
|
use futures::{
|
||||||
channel::mpsc::{self, UnboundedReceiver},
|
channel::mpsc::{self, UnboundedReceiver},
|
||||||
future::{try_join_all, Shared},
|
future::{try_join_all, Shared},
|
||||||
|
@ -129,6 +130,7 @@ pub struct Project {
|
||||||
_maintain_buffer_languages: Task<()>,
|
_maintain_buffer_languages: Task<()>,
|
||||||
_maintain_workspace_config: Task<()>,
|
_maintain_workspace_config: Task<()>,
|
||||||
terminals: Terminals,
|
terminals: Terminals,
|
||||||
|
copilot_enabled: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
enum BufferMessage {
|
enum BufferMessage {
|
||||||
|
@ -472,6 +474,7 @@ impl Project {
|
||||||
terminals: Terminals {
|
terminals: Terminals {
|
||||||
local_handles: Vec::new(),
|
local_handles: Vec::new(),
|
||||||
},
|
},
|
||||||
|
copilot_enabled: Copilot::global(cx).is_some(),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -559,6 +562,7 @@ impl Project {
|
||||||
terminals: Terminals {
|
terminals: Terminals {
|
||||||
local_handles: Vec::new(),
|
local_handles: Vec::new(),
|
||||||
},
|
},
|
||||||
|
copilot_enabled: Copilot::global(cx).is_some(),
|
||||||
};
|
};
|
||||||
for worktree in worktrees {
|
for worktree in worktrees {
|
||||||
let _ = this.add_worktree(&worktree, cx);
|
let _ = this.add_worktree(&worktree, cx);
|
||||||
|
@ -664,6 +668,15 @@ impl Project {
|
||||||
self.start_language_server(worktree_id, worktree_path, language, cx);
|
self.start_language_server(worktree_id, worktree_path, language, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !self.copilot_enabled && Copilot::global(cx).is_some() {
|
||||||
|
self.copilot_enabled = true;
|
||||||
|
for buffer in self.opened_buffers.values() {
|
||||||
|
if let Some(buffer) = buffer.upgrade(cx) {
|
||||||
|
self.register_buffer_with_copilot(&buffer, cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1616,6 +1629,7 @@ impl Project {
|
||||||
|
|
||||||
self.detect_language_for_buffer(buffer, cx);
|
self.detect_language_for_buffer(buffer, cx);
|
||||||
self.register_buffer_with_language_server(buffer, cx);
|
self.register_buffer_with_language_server(buffer, cx);
|
||||||
|
self.register_buffer_with_copilot(buffer, cx);
|
||||||
cx.observe_release(buffer, |this, buffer, cx| {
|
cx.observe_release(buffer, |this, buffer, cx| {
|
||||||
if let Some(file) = File::from_dyn(buffer.file()) {
|
if let Some(file) = File::from_dyn(buffer.file()) {
|
||||||
if file.is_local() {
|
if file.is_local() {
|
||||||
|
@ -1731,6 +1745,16 @@ impl Project {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn register_buffer_with_copilot(
|
||||||
|
&self,
|
||||||
|
buffer_handle: &ModelHandle<Buffer>,
|
||||||
|
cx: &mut ModelContext<Self>,
|
||||||
|
) {
|
||||||
|
if let Some(copilot) = Copilot::global(cx) {
|
||||||
|
copilot.update(cx, |copilot, cx| copilot.register_buffer(buffer_handle, cx));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn send_buffer_messages(
|
async fn send_buffer_messages(
|
||||||
this: WeakModelHandle<Self>,
|
this: WeakModelHandle<Self>,
|
||||||
rx: UnboundedReceiver<BufferMessage>,
|
rx: UnboundedReceiver<BufferMessage>,
|
||||||
|
@ -2013,17 +2037,19 @@ impl Project {
|
||||||
|
|
||||||
fn detect_language_for_buffer(
|
fn detect_language_for_buffer(
|
||||||
&mut self,
|
&mut self,
|
||||||
buffer: &ModelHandle<Buffer>,
|
buffer_handle: &ModelHandle<Buffer>,
|
||||||
cx: &mut ModelContext<Self>,
|
cx: &mut ModelContext<Self>,
|
||||||
) -> Option<()> {
|
) -> Option<()> {
|
||||||
// If the buffer has a language, set it and start the language server if we haven't already.
|
// If the buffer has a language, set it and start the language server if we haven't already.
|
||||||
let full_path = buffer.read(cx).file()?.full_path(cx);
|
let buffer = buffer_handle.read(cx);
|
||||||
|
let full_path = buffer.file()?.full_path(cx);
|
||||||
|
let content = buffer.as_rope();
|
||||||
let new_language = self
|
let new_language = self
|
||||||
.languages
|
.languages
|
||||||
.language_for_path(&full_path)
|
.language_for_file(&full_path, Some(content))
|
||||||
.now_or_never()?
|
.now_or_never()?
|
||||||
.ok()?;
|
.ok()?;
|
||||||
self.set_language_for_buffer(buffer, new_language, cx);
|
self.set_language_for_buffer(buffer_handle, new_language, cx);
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2434,26 +2460,23 @@ impl Project {
|
||||||
buffers: impl IntoIterator<Item = ModelHandle<Buffer>>,
|
buffers: impl IntoIterator<Item = ModelHandle<Buffer>>,
|
||||||
cx: &mut ModelContext<Self>,
|
cx: &mut ModelContext<Self>,
|
||||||
) -> Option<()> {
|
) -> Option<()> {
|
||||||
let language_server_lookup_info: HashSet<(WorktreeId, Arc<Path>, PathBuf)> = buffers
|
let language_server_lookup_info: HashSet<(WorktreeId, Arc<Path>, Arc<Language>)> = buffers
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|buffer| {
|
.filter_map(|buffer| {
|
||||||
let file = File::from_dyn(buffer.read(cx).file())?;
|
let buffer = buffer.read(cx);
|
||||||
|
let file = File::from_dyn(buffer.file())?;
|
||||||
let worktree = file.worktree.read(cx).as_local()?;
|
let worktree = file.worktree.read(cx).as_local()?;
|
||||||
let worktree_id = worktree.id();
|
|
||||||
let worktree_abs_path = worktree.abs_path().clone();
|
|
||||||
let full_path = file.full_path(cx);
|
let full_path = file.full_path(cx);
|
||||||
Some((worktree_id, worktree_abs_path, full_path))
|
let language = self
|
||||||
|
.languages
|
||||||
|
.language_for_file(&full_path, Some(buffer.as_rope()))
|
||||||
|
.now_or_never()?
|
||||||
|
.ok()?;
|
||||||
|
Some((worktree.id(), worktree.abs_path().clone(), language))
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
for (worktree_id, worktree_abs_path, full_path) in language_server_lookup_info {
|
for (worktree_id, worktree_abs_path, language) in language_server_lookup_info {
|
||||||
if let Some(language) = self
|
self.restart_language_server(worktree_id, worktree_abs_path, language, cx);
|
||||||
.languages
|
|
||||||
.language_for_path(&full_path)
|
|
||||||
.now_or_never()
|
|
||||||
.and_then(|language| language.ok())
|
|
||||||
{
|
|
||||||
self.restart_language_server(worktree_id, worktree_abs_path, language, cx);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
None
|
None
|
||||||
|
@ -3487,7 +3510,7 @@ impl Project {
|
||||||
let adapter_language = adapter_language.clone();
|
let adapter_language = adapter_language.clone();
|
||||||
let language = this
|
let language = this
|
||||||
.languages
|
.languages
|
||||||
.language_for_path(&project_path.path)
|
.language_for_file(&project_path.path, None)
|
||||||
.unwrap_or_else(move |_| adapter_language);
|
.unwrap_or_else(move |_| adapter_language);
|
||||||
let language_server_name = adapter.name.clone();
|
let language_server_name = adapter.name.clone();
|
||||||
Some(async move {
|
Some(async move {
|
||||||
|
@ -5916,7 +5939,10 @@ impl Project {
|
||||||
worktree_id,
|
worktree_id,
|
||||||
path: PathBuf::from(serialized_symbol.path).into(),
|
path: PathBuf::from(serialized_symbol.path).into(),
|
||||||
};
|
};
|
||||||
let language = languages.language_for_path(&path.path).await.log_err();
|
let language = languages
|
||||||
|
.language_for_file(&path.path, None)
|
||||||
|
.await
|
||||||
|
.log_err();
|
||||||
Ok(Symbol {
|
Ok(Symbol {
|
||||||
language_server_name: LanguageServerName(
|
language_server_name: LanguageServerName(
|
||||||
serialized_symbol.language_server_name.into(),
|
serialized_symbol.language_server_name.into(),
|
||||||
|
|
|
@ -141,7 +141,7 @@ impl PickerDelegate for RecentProjectsDelegate {
|
||||||
fn confirm(&mut self, cx: &mut ViewContext<RecentProjects>) {
|
fn confirm(&mut self, cx: &mut ViewContext<RecentProjects>) {
|
||||||
if let Some(selected_match) = &self.matches.get(self.selected_index()) {
|
if let Some(selected_match) = &self.matches.get(self.selected_index()) {
|
||||||
let workspace_location = &self.workspace_locations[selected_match.candidate_id];
|
let workspace_location = &self.workspace_locations[selected_match.candidate_id];
|
||||||
cx.dispatch_global_action(OpenPaths {
|
cx.dispatch_action(OpenPaths {
|
||||||
paths: workspace_location.paths().as_ref().clone(),
|
paths: workspace_location.paths().as_ref().clone(),
|
||||||
});
|
});
|
||||||
cx.emit(PickerEvent::Dismiss);
|
cx.emit(PickerEvent::Dismiss);
|
||||||
|
|
|
@ -21,6 +21,7 @@ use settings::Settings;
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
use std::{
|
use std::{
|
||||||
any::{Any, TypeId},
|
any::{Any, TypeId},
|
||||||
|
borrow::Cow,
|
||||||
mem,
|
mem,
|
||||||
ops::Range,
|
ops::Range,
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
|
@ -224,6 +225,10 @@ impl View for ProjectSearchView {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Item for ProjectSearchView {
|
impl Item for ProjectSearchView {
|
||||||
|
fn tab_tooltip_text(&self, cx: &AppContext) -> Option<Cow<str>> {
|
||||||
|
Some(self.query_editor.read(cx).text(cx).into())
|
||||||
|
}
|
||||||
|
|
||||||
fn act_as_type<'a>(
|
fn act_as_type<'a>(
|
||||||
&'a self,
|
&'a self,
|
||||||
type_id: TypeId,
|
type_id: TypeId,
|
||||||
|
|
|
@ -28,11 +28,11 @@ pub use watched_json::watch_files;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Settings {
|
pub struct Settings {
|
||||||
|
pub features: Features,
|
||||||
pub buffer_font_family_name: String,
|
pub buffer_font_family_name: String,
|
||||||
pub buffer_font_features: fonts::Features,
|
pub buffer_font_features: fonts::Features,
|
||||||
pub buffer_font_family: FamilyId,
|
pub buffer_font_family: FamilyId,
|
||||||
pub default_buffer_font_size: f32,
|
pub default_buffer_font_size: f32,
|
||||||
pub enable_copilot_integration: bool,
|
|
||||||
pub buffer_font_size: f32,
|
pub buffer_font_size: f32,
|
||||||
pub active_pane_magnification: f32,
|
pub active_pane_magnification: f32,
|
||||||
pub cursor_blink: bool,
|
pub cursor_blink: bool,
|
||||||
|
@ -177,43 +177,7 @@ pub struct EditorSettings {
|
||||||
pub ensure_final_newline_on_save: Option<bool>,
|
pub ensure_final_newline_on_save: Option<bool>,
|
||||||
pub formatter: Option<Formatter>,
|
pub formatter: Option<Formatter>,
|
||||||
pub enable_language_server: Option<bool>,
|
pub enable_language_server: Option<bool>,
|
||||||
#[schemars(skip)]
|
pub show_copilot_suggestions: Option<bool>,
|
||||||
pub copilot: Option<OnOff>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
|
||||||
#[serde(rename_all = "snake_case")]
|
|
||||||
pub enum OnOff {
|
|
||||||
On,
|
|
||||||
Off,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl OnOff {
|
|
||||||
pub fn as_bool(&self) -> bool {
|
|
||||||
match self {
|
|
||||||
OnOff::On => true,
|
|
||||||
OnOff::Off => false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn from_bool(value: bool) -> OnOff {
|
|
||||||
match value {
|
|
||||||
true => OnOff::On,
|
|
||||||
false => OnOff::Off,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<OnOff> for bool {
|
|
||||||
fn from(value: OnOff) -> bool {
|
|
||||||
value.as_bool()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<bool> for OnOff {
|
|
||||||
fn from(value: bool) -> OnOff {
|
|
||||||
OnOff::from_bool(value)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||||
|
@ -437,8 +401,7 @@ pub struct SettingsFileContent {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub base_keymap: Option<BaseKeymap>,
|
pub base_keymap: Option<BaseKeymap>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
#[schemars(skip)]
|
pub features: FeaturesContent,
|
||||||
pub enable_copilot_integration: Option<bool>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||||
|
@ -447,6 +410,18 @@ pub struct LspSettings {
|
||||||
pub initialization_options: Option<Value>,
|
pub initialization_options: Option<Value>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub struct Features {
|
||||||
|
pub copilot: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub struct FeaturesContent {
|
||||||
|
pub copilot: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
impl Settings {
|
impl Settings {
|
||||||
/// Fill out the settings corresponding to the default.json file, overrides will be set later
|
/// Fill out the settings corresponding to the default.json file, overrides will be set later
|
||||||
pub fn defaults(
|
pub fn defaults(
|
||||||
|
@ -500,7 +475,7 @@ impl Settings {
|
||||||
format_on_save: required(defaults.editor.format_on_save),
|
format_on_save: required(defaults.editor.format_on_save),
|
||||||
formatter: required(defaults.editor.formatter),
|
formatter: required(defaults.editor.formatter),
|
||||||
enable_language_server: required(defaults.editor.enable_language_server),
|
enable_language_server: required(defaults.editor.enable_language_server),
|
||||||
copilot: required(defaults.editor.copilot),
|
show_copilot_suggestions: required(defaults.editor.show_copilot_suggestions),
|
||||||
},
|
},
|
||||||
editor_overrides: Default::default(),
|
editor_overrides: Default::default(),
|
||||||
git: defaults.git.unwrap(),
|
git: defaults.git.unwrap(),
|
||||||
|
@ -517,7 +492,9 @@ impl Settings {
|
||||||
telemetry_overrides: Default::default(),
|
telemetry_overrides: Default::default(),
|
||||||
auto_update: defaults.auto_update.unwrap(),
|
auto_update: defaults.auto_update.unwrap(),
|
||||||
base_keymap: Default::default(),
|
base_keymap: Default::default(),
|
||||||
enable_copilot_integration: defaults.enable_copilot_integration.unwrap(),
|
features: Features {
|
||||||
|
copilot: defaults.features.copilot.unwrap(),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -569,10 +546,7 @@ impl Settings {
|
||||||
merge(&mut self.autosave, data.autosave);
|
merge(&mut self.autosave, data.autosave);
|
||||||
merge(&mut self.default_dock_anchor, data.default_dock_anchor);
|
merge(&mut self.default_dock_anchor, data.default_dock_anchor);
|
||||||
merge(&mut self.base_keymap, data.base_keymap);
|
merge(&mut self.base_keymap, data.base_keymap);
|
||||||
merge(
|
merge(&mut self.features.copilot, data.features.copilot);
|
||||||
&mut self.enable_copilot_integration,
|
|
||||||
data.enable_copilot_integration,
|
|
||||||
);
|
|
||||||
|
|
||||||
self.editor_overrides = data.editor;
|
self.editor_overrides = data.editor;
|
||||||
self.git_overrides = data.git.unwrap_or_default();
|
self.git_overrides = data.git.unwrap_or_default();
|
||||||
|
@ -596,12 +570,15 @@ impl Settings {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn copilot_on(&self, language: Option<&str>) -> bool {
|
pub fn features(&self) -> &Features {
|
||||||
if self.enable_copilot_integration {
|
&self.features
|
||||||
self.language_setting(language, |settings| settings.copilot.map(Into::into))
|
}
|
||||||
} else {
|
|
||||||
false
|
pub fn show_copilot_suggestions(&self, language: Option<&str>) -> bool {
|
||||||
}
|
self.features.copilot
|
||||||
|
&& self.language_setting(language, |settings| {
|
||||||
|
settings.show_copilot_suggestions.map(Into::into)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn tab_size(&self, language: Option<&str>) -> NonZeroU32 {
|
pub fn tab_size(&self, language: Option<&str>) -> NonZeroU32 {
|
||||||
|
@ -740,7 +717,7 @@ impl Settings {
|
||||||
format_on_save: Some(FormatOnSave::On),
|
format_on_save: Some(FormatOnSave::On),
|
||||||
formatter: Some(Formatter::LanguageServer),
|
formatter: Some(Formatter::LanguageServer),
|
||||||
enable_language_server: Some(true),
|
enable_language_server: Some(true),
|
||||||
copilot: Some(OnOff::On),
|
show_copilot_suggestions: Some(true),
|
||||||
},
|
},
|
||||||
editor_overrides: Default::default(),
|
editor_overrides: Default::default(),
|
||||||
journal_defaults: Default::default(),
|
journal_defaults: Default::default(),
|
||||||
|
@ -760,7 +737,7 @@ impl Settings {
|
||||||
telemetry_overrides: Default::default(),
|
telemetry_overrides: Default::default(),
|
||||||
auto_update: true,
|
auto_update: true,
|
||||||
base_keymap: Default::default(),
|
base_keymap: Default::default(),
|
||||||
enable_copilot_integration: true,
|
features: Features { copilot: true },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1125,7 +1102,7 @@ mod tests {
|
||||||
{
|
{
|
||||||
"language_overrides": {
|
"language_overrides": {
|
||||||
"JSON": {
|
"JSON": {
|
||||||
"copilot": "off"
|
"show_copilot_suggestions": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1135,7 +1112,7 @@ mod tests {
|
||||||
settings.languages.insert(
|
settings.languages.insert(
|
||||||
"Rust".into(),
|
"Rust".into(),
|
||||||
EditorSettings {
|
EditorSettings {
|
||||||
copilot: Some(OnOff::On),
|
show_copilot_suggestions: Some(true),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -1144,10 +1121,10 @@ mod tests {
|
||||||
{
|
{
|
||||||
"language_overrides": {
|
"language_overrides": {
|
||||||
"Rust": {
|
"Rust": {
|
||||||
"copilot": "on"
|
"show_copilot_suggestions": true
|
||||||
},
|
},
|
||||||
"JSON": {
|
"JSON": {
|
||||||
"copilot": "off"
|
"show_copilot_suggestions": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1163,21 +1140,21 @@ mod tests {
|
||||||
{
|
{
|
||||||
"languages": {
|
"languages": {
|
||||||
"JSON": {
|
"JSON": {
|
||||||
"copilot": "off"
|
"show_copilot_suggestions": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"#
|
"#
|
||||||
.unindent(),
|
.unindent(),
|
||||||
|settings| {
|
|settings| {
|
||||||
settings.editor.copilot = Some(OnOff::On);
|
settings.editor.show_copilot_suggestions = Some(true);
|
||||||
},
|
},
|
||||||
r#"
|
r#"
|
||||||
{
|
{
|
||||||
"copilot": "on",
|
"show_copilot_suggestions": true,
|
||||||
"languages": {
|
"languages": {
|
||||||
"JSON": {
|
"JSON": {
|
||||||
"copilot": "off"
|
"show_copilot_suggestions": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1187,13 +1164,13 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_update_langauge_copilot() {
|
fn test_update_language_copilot() {
|
||||||
assert_new_settings(
|
assert_new_settings(
|
||||||
r#"
|
r#"
|
||||||
{
|
{
|
||||||
"languages": {
|
"languages": {
|
||||||
"JSON": {
|
"JSON": {
|
||||||
"copilot": "off"
|
"show_copilot_suggestions": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1203,7 +1180,7 @@ mod tests {
|
||||||
settings.languages.insert(
|
settings.languages.insert(
|
||||||
"Rust".into(),
|
"Rust".into(),
|
||||||
EditorSettings {
|
EditorSettings {
|
||||||
copilot: Some(OnOff::On),
|
show_copilot_suggestions: Some(true),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -1212,10 +1189,10 @@ mod tests {
|
||||||
{
|
{
|
||||||
"languages": {
|
"languages": {
|
||||||
"Rust": {
|
"Rust": {
|
||||||
"copilot": "on"
|
"show_copilot_suggestions": true
|
||||||
},
|
},
|
||||||
"JSON": {
|
"JSON": {
|
||||||
"copilot": "off"
|
"show_copilot_suggestions": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ pub mod terminal_button;
|
||||||
pub mod terminal_element;
|
pub mod terminal_element;
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
|
borrow::Cow,
|
||||||
ops::RangeInclusive,
|
ops::RangeInclusive,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
time::Duration,
|
time::Duration,
|
||||||
|
@ -541,6 +542,10 @@ impl View for TerminalView {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Item for TerminalView {
|
impl Item for TerminalView {
|
||||||
|
fn tab_tooltip_text(&self, cx: &AppContext) -> Option<Cow<str>> {
|
||||||
|
Some(self.terminal().read(cx).title().into())
|
||||||
|
}
|
||||||
|
|
||||||
fn tab_content(
|
fn tab_content(
|
||||||
&self,
|
&self,
|
||||||
_detail: Option<usize>,
|
_detail: Option<usize>,
|
||||||
|
|
|
@ -1331,15 +1331,15 @@ impl Buffer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn wait_for_anchors<'a>(
|
pub fn wait_for_anchors(
|
||||||
&mut self,
|
&mut self,
|
||||||
anchors: impl IntoIterator<Item = &'a Anchor>,
|
anchors: impl IntoIterator<Item = Anchor>,
|
||||||
) -> impl 'static + Future<Output = Result<()>> {
|
) -> impl 'static + Future<Output = Result<()>> {
|
||||||
let mut futures = Vec::new();
|
let mut futures = Vec::new();
|
||||||
for anchor in anchors {
|
for anchor in anchors {
|
||||||
if !self.version.observed(anchor.timestamp)
|
if !self.version.observed(anchor.timestamp)
|
||||||
&& *anchor != Anchor::MAX
|
&& anchor != Anchor::MAX
|
||||||
&& *anchor != Anchor::MIN
|
&& anchor != Anchor::MIN
|
||||||
{
|
{
|
||||||
let (tx, rx) = oneshot::channel();
|
let (tx, rx) = oneshot::channel();
|
||||||
self.edit_id_resolvers
|
self.edit_id_resolvers
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
mod base_keymap_picker;
|
mod base_keymap_picker;
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::{borrow::Cow, sync::Arc};
|
||||||
|
|
||||||
use db::kvp::KEY_VALUE_STORE;
|
use db::kvp::KEY_VALUE_STORE;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
|
@ -198,6 +198,10 @@ impl WelcomePage {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Item for WelcomePage {
|
impl Item for WelcomePage {
|
||||||
|
fn tab_tooltip_text(&self, _: &AppContext) -> Option<Cow<str>> {
|
||||||
|
Some("Welcome to Zed!".into())
|
||||||
|
}
|
||||||
|
|
||||||
fn tab_content(
|
fn tab_content(
|
||||||
&self,
|
&self,
|
||||||
_detail: Option<usize>,
|
_detail: Option<usize>,
|
||||||
|
|
|
@ -48,7 +48,10 @@ pub trait Item: View {
|
||||||
fn navigate(&mut self, _: Box<dyn Any>, _: &mut ViewContext<Self>) -> bool {
|
fn navigate(&mut self, _: Box<dyn Any>, _: &mut ViewContext<Self>) -> bool {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
fn tab_description<'a>(&'a self, _: usize, _: &'a AppContext) -> Option<Cow<'a, str>> {
|
fn tab_tooltip_text(&self, _: &AppContext) -> Option<Cow<str>> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
fn tab_description<'a>(&'a self, _: usize, _: &'a AppContext) -> Option<Cow<str>> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
fn tab_content(
|
fn tab_content(
|
||||||
|
@ -170,7 +173,8 @@ pub trait ItemHandle: 'static + fmt::Debug {
|
||||||
cx: &mut WindowContext,
|
cx: &mut WindowContext,
|
||||||
handler: Box<dyn Fn(ItemEvent, &mut WindowContext)>,
|
handler: Box<dyn Fn(ItemEvent, &mut WindowContext)>,
|
||||||
) -> gpui::Subscription;
|
) -> gpui::Subscription;
|
||||||
fn tab_description<'a>(&self, detail: usize, cx: &'a AppContext) -> Option<Cow<'a, str>>;
|
fn tab_tooltip_text<'a>(&self, cx: &'a AppContext) -> Option<Cow<'a, str>>;
|
||||||
|
fn tab_description<'a>(&'a self, detail: usize, cx: &'a AppContext) -> Option<Cow<'a, str>>;
|
||||||
fn tab_content(
|
fn tab_content(
|
||||||
&self,
|
&self,
|
||||||
detail: Option<usize>,
|
detail: Option<usize>,
|
||||||
|
@ -260,7 +264,11 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn tab_description<'a>(&self, detail: usize, cx: &'a AppContext) -> Option<Cow<'a, str>> {
|
fn tab_tooltip_text<'a>(&self, cx: &'a AppContext) -> Option<Cow<'a, str>> {
|
||||||
|
self.read(cx).tab_tooltip_text(cx)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tab_description<'a>(&'a self, detail: usize, cx: &'a AppContext) -> Option<Cow<'a, str>> {
|
||||||
self.read(cx).tab_description(detail, cx)
|
self.read(cx).tab_description(detail, cx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -912,7 +920,7 @@ pub(crate) mod test {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Item for TestItem {
|
impl Item for TestItem {
|
||||||
fn tab_description<'a>(&'a self, detail: usize, _: &'a AppContext) -> Option<Cow<'a, str>> {
|
fn tab_description(&self, detail: usize, _: &AppContext) -> Option<Cow<str>> {
|
||||||
self.tab_descriptions.as_ref().and_then(|descriptions| {
|
self.tab_descriptions.as_ref().and_then(|descriptions| {
|
||||||
let description = *descriptions.get(detail).or_else(|| descriptions.last())?;
|
let description = *descriptions.get(detail).or_else(|| descriptions.last())?;
|
||||||
Some(description.into())
|
Some(description.into())
|
||||||
|
|
|
@ -1389,6 +1389,9 @@ impl Pane {
|
||||||
let detail = detail.clone();
|
let detail = detail.clone();
|
||||||
|
|
||||||
let theme = cx.global::<Settings>().theme.clone();
|
let theme = cx.global::<Settings>().theme.clone();
|
||||||
|
let mut tooltip_theme = theme.tooltip.clone();
|
||||||
|
tooltip_theme.max_text_width = None;
|
||||||
|
let tab_tooltip_text = item.tab_tooltip_text(cx).map(|a| a.to_string());
|
||||||
|
|
||||||
move |mouse_state, cx| {
|
move |mouse_state, cx| {
|
||||||
let tab_style =
|
let tab_style =
|
||||||
|
@ -1396,39 +1399,56 @@ impl Pane {
|
||||||
let hovered = mouse_state.hovered();
|
let hovered = mouse_state.hovered();
|
||||||
|
|
||||||
enum Tab {}
|
enum Tab {}
|
||||||
MouseEventHandler::<Tab, Pane>::new(ix, cx, |_, cx| {
|
let mouse_event_handler =
|
||||||
Self::render_tab::<Pane>(
|
MouseEventHandler::<Tab, Pane>::new(ix, cx, |_, cx| {
|
||||||
&item,
|
Self::render_tab::<Pane>(
|
||||||
pane.clone(),
|
&item,
|
||||||
ix == 0,
|
pane.clone(),
|
||||||
detail,
|
ix == 0,
|
||||||
hovered,
|
detail,
|
||||||
tab_style,
|
hovered,
|
||||||
cx,
|
tab_style,
|
||||||
)
|
cx,
|
||||||
})
|
)
|
||||||
.on_down(MouseButton::Left, move |_, _, cx| {
|
})
|
||||||
cx.dispatch_action(ActivateItem(ix));
|
.on_down(MouseButton::Left, move |_, _, cx| {
|
||||||
})
|
cx.dispatch_action(ActivateItem(ix));
|
||||||
.on_click(MouseButton::Middle, {
|
})
|
||||||
let item = item.clone();
|
.on_click(MouseButton::Middle, {
|
||||||
let pane = pane.clone();
|
let item = item.clone();
|
||||||
move |_, _, cx| {
|
let pane = pane.clone();
|
||||||
cx.dispatch_action(CloseItemById {
|
move |_, _, cx| {
|
||||||
item_id: item.id(),
|
cx.dispatch_action(CloseItemById {
|
||||||
pane: pane.clone(),
|
item_id: item.id(),
|
||||||
})
|
pane: pane.clone(),
|
||||||
}
|
})
|
||||||
})
|
}
|
||||||
.on_down(MouseButton::Right, move |e, _, cx| {
|
})
|
||||||
let item = item.clone();
|
.on_down(
|
||||||
cx.dispatch_action(DeployTabContextMenu {
|
MouseButton::Right,
|
||||||
position: e.position,
|
move |e, _, cx| {
|
||||||
item_id: item.id(),
|
let item = item.clone();
|
||||||
pane: pane.clone(),
|
cx.dispatch_action(DeployTabContextMenu {
|
||||||
});
|
position: e.position,
|
||||||
})
|
item_id: item.id(),
|
||||||
.boxed()
|
pane: pane.clone(),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if let Some(tab_tooltip_text) = tab_tooltip_text {
|
||||||
|
return mouse_event_handler
|
||||||
|
.with_tooltip::<Self>(
|
||||||
|
ix,
|
||||||
|
tab_tooltip_text,
|
||||||
|
None,
|
||||||
|
tooltip_theme,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
.boxed();
|
||||||
|
}
|
||||||
|
|
||||||
|
mouse_event_handler.boxed()
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,10 @@ use gpui::{
|
||||||
};
|
};
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
use std::sync::{Arc, Weak};
|
use std::{
|
||||||
|
borrow::Cow,
|
||||||
|
sync::{Arc, Weak},
|
||||||
|
};
|
||||||
|
|
||||||
pub enum Event {
|
pub enum Event {
|
||||||
Close,
|
Close,
|
||||||
|
@ -94,6 +97,9 @@ impl View for SharedScreen {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Item for SharedScreen {
|
impl Item for SharedScreen {
|
||||||
|
fn tab_tooltip_text(&self, _: &AppContext) -> Option<Cow<str>> {
|
||||||
|
Some(format!("{}'s screen", self.user.github_login).into())
|
||||||
|
}
|
||||||
fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
|
fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
|
||||||
if let Some(nav_history) = self.nav_history.as_ref() {
|
if let Some(nav_history) = self.nav_history.as_ref() {
|
||||||
nav_history.push::<()>(None, cx);
|
nav_history.push::<()>(None, cx);
|
||||||
|
|
|
@ -70,6 +70,7 @@ impl View for Toolbar {
|
||||||
for (item, position) in &self.items {
|
for (item, position) in &self.items {
|
||||||
match *position {
|
match *position {
|
||||||
ToolbarItemLocation::Hidden => {}
|
ToolbarItemLocation::Hidden => {}
|
||||||
|
|
||||||
ToolbarItemLocation::PrimaryLeft { flex } => {
|
ToolbarItemLocation::PrimaryLeft { flex } => {
|
||||||
let left_item = ChildView::new(item.as_any(), cx)
|
let left_item = ChildView::new(item.as_any(), cx)
|
||||||
.aligned()
|
.aligned()
|
||||||
|
@ -81,6 +82,7 @@ impl View for Toolbar {
|
||||||
primary_left_items.push(left_item.boxed());
|
primary_left_items.push(left_item.boxed());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ToolbarItemLocation::PrimaryRight { flex } => {
|
ToolbarItemLocation::PrimaryRight { flex } => {
|
||||||
let right_item = ChildView::new(item.as_any(), cx)
|
let right_item = ChildView::new(item.as_any(), cx)
|
||||||
.aligned()
|
.aligned()
|
||||||
|
@ -93,6 +95,7 @@ impl View for Toolbar {
|
||||||
primary_right_items.push(right_item.boxed());
|
primary_right_items.push(right_item.boxed());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ToolbarItemLocation::Secondary => {
|
ToolbarItemLocation::Secondary => {
|
||||||
secondary_item = Some(
|
secondary_item = Some(
|
||||||
ChildView::new(item.as_any(), cx)
|
ChildView::new(item.as_any(), cx)
|
||||||
|
@ -300,7 +303,10 @@ impl<T: ToolbarItemView> ToolbarItemViewHandle for ViewHandle<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn pane_focus_update(&mut self, pane_focused: bool, cx: &mut WindowContext) {
|
fn pane_focus_update(&mut self, pane_focused: bool, cx: &mut WindowContext) {
|
||||||
self.update(cx, |this, cx| this.pane_focus_update(pane_focused, cx));
|
self.update(cx, |this, cx| {
|
||||||
|
this.pane_focus_update(pane_focused, cx);
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -290,7 +290,7 @@ pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
|
||||||
let app_state = Arc::downgrade(&app_state);
|
let app_state = Arc::downgrade(&app_state);
|
||||||
move |action: &OpenPaths, cx: &mut AppContext| {
|
move |action: &OpenPaths, cx: &mut AppContext| {
|
||||||
if let Some(app_state) = app_state.upgrade() {
|
if let Some(app_state) = app_state.upgrade() {
|
||||||
open_paths(&action.paths, &app_state, cx).detach();
|
open_paths(&action.paths, &app_state, None, cx).detach();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -303,15 +303,28 @@ pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
|
||||||
}
|
}
|
||||||
|
|
||||||
let app_state = app_state.upgrade()?;
|
let app_state = app_state.upgrade()?;
|
||||||
|
let window_id = cx.window_id();
|
||||||
let action = action.clone();
|
let action = action.clone();
|
||||||
let close = workspace.prepare_to_close(false, cx);
|
let is_remote = workspace.project.read(cx).is_remote();
|
||||||
|
let has_worktree = workspace.project.read(cx).worktrees(cx).next().is_some();
|
||||||
|
let has_dirty_items = workspace.items(cx).any(|item| item.is_dirty(cx));
|
||||||
|
let close_task = if is_remote || has_worktree || has_dirty_items {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(workspace.prepare_to_close(false, cx))
|
||||||
|
};
|
||||||
|
|
||||||
Some(cx.spawn_weak(|_, mut cx| async move {
|
Some(cx.spawn_weak(|_, mut cx| async move {
|
||||||
let can_close = close.await?;
|
let window_id_to_replace = if let Some(close_task) = close_task {
|
||||||
if can_close {
|
if !close_task.await? {
|
||||||
cx.update(|cx| open_paths(&action.paths, &app_state, cx))
|
return Ok(());
|
||||||
.await?;
|
}
|
||||||
}
|
Some(window_id)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
cx.update(|cx| open_paths(&action.paths, &app_state, window_id_to_replace, cx))
|
||||||
|
.await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
@ -854,6 +867,7 @@ impl Workspace {
|
||||||
fn new_local(
|
fn new_local(
|
||||||
abs_paths: Vec<PathBuf>,
|
abs_paths: Vec<PathBuf>,
|
||||||
app_state: Arc<AppState>,
|
app_state: Arc<AppState>,
|
||||||
|
requesting_window_id: Option<usize>,
|
||||||
cx: &mut AppContext,
|
cx: &mut AppContext,
|
||||||
) -> Task<(
|
) -> Task<(
|
||||||
ViewHandle<Workspace>,
|
ViewHandle<Workspace>,
|
||||||
|
@ -868,7 +882,8 @@ impl Workspace {
|
||||||
);
|
);
|
||||||
|
|
||||||
cx.spawn(|mut cx| async move {
|
cx.spawn(|mut cx| async move {
|
||||||
let serialized_workspace = persistence::DB.workspace_for_roots(&abs_paths.as_slice());
|
let mut serialized_workspace =
|
||||||
|
persistence::DB.workspace_for_roots(&abs_paths.as_slice());
|
||||||
|
|
||||||
let paths_to_open = serialized_workspace
|
let paths_to_open = serialized_workspace
|
||||||
.as_ref()
|
.as_ref()
|
||||||
|
@ -915,7 +930,7 @@ impl Workspace {
|
||||||
let mut workspace = Workspace::new(
|
let mut workspace = Workspace::new(
|
||||||
serialized_workspace,
|
serialized_workspace,
|
||||||
workspace_id,
|
workspace_id,
|
||||||
project_handle,
|
project_handle.clone(),
|
||||||
app_state.dock_default_item_factory,
|
app_state.dock_default_item_factory,
|
||||||
app_state.background_actions,
|
app_state.background_actions,
|
||||||
cx,
|
cx,
|
||||||
|
@ -924,46 +939,54 @@ impl Workspace {
|
||||||
workspace
|
workspace
|
||||||
};
|
};
|
||||||
|
|
||||||
let workspace = {
|
let workspace = requesting_window_id
|
||||||
let (bounds, display) = if let Some(bounds) = window_bounds_override {
|
.and_then(|window_id| {
|
||||||
(Some(bounds), None)
|
cx.update(|cx| {
|
||||||
} else {
|
cx.replace_root_view(window_id, |cx| {
|
||||||
serialized_workspace
|
build_workspace(cx, serialized_workspace.take())
|
||||||
.as_ref()
|
|
||||||
.and_then(|serialized_workspace| {
|
|
||||||
let display = serialized_workspace.display?;
|
|
||||||
let mut bounds = serialized_workspace.bounds?;
|
|
||||||
|
|
||||||
// Stored bounds are relative to the containing display.
|
|
||||||
// So convert back to global coordinates if that screen still exists
|
|
||||||
if let WindowBounds::Fixed(mut window_bounds) = bounds {
|
|
||||||
if let Some(screen) = cx.platform().screen_by_id(display) {
|
|
||||||
let screen_bounds = screen.bounds();
|
|
||||||
window_bounds.set_origin_x(
|
|
||||||
window_bounds.origin_x() + screen_bounds.origin_x(),
|
|
||||||
);
|
|
||||||
window_bounds.set_origin_y(
|
|
||||||
window_bounds.origin_y() + screen_bounds.origin_y(),
|
|
||||||
);
|
|
||||||
bounds = WindowBounds::Fixed(window_bounds);
|
|
||||||
} else {
|
|
||||||
// Screen no longer exists. Return none here.
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Some((bounds, display))
|
|
||||||
})
|
})
|
||||||
.unzip()
|
})
|
||||||
};
|
})
|
||||||
|
.unwrap_or_else(|| {
|
||||||
|
let (bounds, display) = if let Some(bounds) = window_bounds_override {
|
||||||
|
(Some(bounds), None)
|
||||||
|
} else {
|
||||||
|
serialized_workspace
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|serialized_workspace| {
|
||||||
|
let display = serialized_workspace.display?;
|
||||||
|
let mut bounds = serialized_workspace.bounds?;
|
||||||
|
|
||||||
// Use the serialized workspace to construct the new window
|
// Stored bounds are relative to the containing display.
|
||||||
cx.add_window(
|
// So convert back to global coordinates if that screen still exists
|
||||||
(app_state.build_window_options)(bounds, display, cx.platform().as_ref()),
|
if let WindowBounds::Fixed(mut window_bounds) = bounds {
|
||||||
|cx| build_workspace(cx, serialized_workspace),
|
if let Some(screen) = cx.platform().screen_by_id(display) {
|
||||||
)
|
let screen_bounds = screen.bounds();
|
||||||
.1
|
window_bounds.set_origin_x(
|
||||||
};
|
window_bounds.origin_x() + screen_bounds.origin_x(),
|
||||||
|
);
|
||||||
|
window_bounds.set_origin_y(
|
||||||
|
window_bounds.origin_y() + screen_bounds.origin_y(),
|
||||||
|
);
|
||||||
|
bounds = WindowBounds::Fixed(window_bounds);
|
||||||
|
} else {
|
||||||
|
// Screen no longer exists. Return none here.
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Some((bounds, display))
|
||||||
|
})
|
||||||
|
.unzip()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Use the serialized workspace to construct the new window
|
||||||
|
cx.add_window(
|
||||||
|
(app_state.build_window_options)(bounds, display, cx.platform().as_ref()),
|
||||||
|
|cx| build_workspace(cx, serialized_workspace),
|
||||||
|
)
|
||||||
|
.1
|
||||||
|
});
|
||||||
|
|
||||||
notify_if_database_failed(&workspace, &mut cx);
|
notify_if_database_failed(&workspace, &mut cx);
|
||||||
|
|
||||||
|
@ -1056,7 +1079,7 @@ impl Workspace {
|
||||||
if self.project.read(cx).is_local() {
|
if self.project.read(cx).is_local() {
|
||||||
Task::Ready(Some(Ok(callback(self, cx))))
|
Task::Ready(Some(Ok(callback(self, cx))))
|
||||||
} else {
|
} else {
|
||||||
let task = Self::new_local(Vec::new(), app_state.clone(), cx);
|
let task = Self::new_local(Vec::new(), app_state.clone(), None, cx);
|
||||||
cx.spawn(|_vh, mut cx| async move {
|
cx.spawn(|_vh, mut cx| async move {
|
||||||
let (workspace, _) = task.await;
|
let (workspace, _) = task.await;
|
||||||
workspace.update(&mut cx, callback)
|
workspace.update(&mut cx, callback)
|
||||||
|
@ -3025,6 +3048,7 @@ pub async fn last_opened_workspace_paths() -> Option<WorkspaceLocation> {
|
||||||
pub fn open_paths(
|
pub fn open_paths(
|
||||||
abs_paths: &[PathBuf],
|
abs_paths: &[PathBuf],
|
||||||
app_state: &Arc<AppState>,
|
app_state: &Arc<AppState>,
|
||||||
|
requesting_window_id: Option<usize>,
|
||||||
cx: &mut AppContext,
|
cx: &mut AppContext,
|
||||||
) -> Task<
|
) -> Task<
|
||||||
Result<(
|
Result<(
|
||||||
|
@ -3057,7 +3081,8 @@ pub fn open_paths(
|
||||||
.contains(&false);
|
.contains(&false);
|
||||||
|
|
||||||
cx.update(|cx| {
|
cx.update(|cx| {
|
||||||
let task = Workspace::new_local(abs_paths, app_state.clone(), cx);
|
let task =
|
||||||
|
Workspace::new_local(abs_paths, app_state.clone(), requesting_window_id, cx);
|
||||||
|
|
||||||
cx.spawn(|mut cx| async move {
|
cx.spawn(|mut cx| async move {
|
||||||
let (workspace, items) = task.await;
|
let (workspace, items) = task.await;
|
||||||
|
@ -3081,7 +3106,7 @@ pub fn open_new(
|
||||||
cx: &mut AppContext,
|
cx: &mut AppContext,
|
||||||
init: impl FnOnce(&mut Workspace, &mut ViewContext<Workspace>) + 'static,
|
init: impl FnOnce(&mut Workspace, &mut ViewContext<Workspace>) + 'static,
|
||||||
) -> Task<()> {
|
) -> Task<()> {
|
||||||
let task = Workspace::new_local(Vec::new(), app_state.clone(), cx);
|
let task = Workspace::new_local(Vec::new(), app_state.clone(), None, cx);
|
||||||
cx.spawn(|mut cx| async move {
|
cx.spawn(|mut cx| async move {
|
||||||
let (workspace, opened_paths) = task.await;
|
let (workspace, opened_paths) = task.await;
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
name = "JavaScript"
|
name = "JavaScript"
|
||||||
path_suffixes = ["js", "jsx", "mjs"]
|
path_suffixes = ["js", "jsx", "mjs"]
|
||||||
|
first_line_pattern = '^#!.*\bnode\b'
|
||||||
line_comment = "// "
|
line_comment = "// "
|
||||||
autoclose_before = ";:.,=}])>"
|
autoclose_before = ";:.,=}])>"
|
||||||
brackets = [
|
brackets = [
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
name = "Python"
|
name = "Python"
|
||||||
path_suffixes = ["py", "pyi"]
|
path_suffixes = ["py", "pyi"]
|
||||||
|
first_line_pattern = '^#!.*\bpython[0-9.]*\b'
|
||||||
line_comment = "# "
|
line_comment = "# "
|
||||||
autoclose_before = ";:.,=}])>"
|
autoclose_before = ";:.,=}])>"
|
||||||
brackets = [
|
brackets = [
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
name = "Ruby"
|
name = "Ruby"
|
||||||
path_suffixes = ["rb", "Gemfile"]
|
path_suffixes = ["rb", "Gemfile"]
|
||||||
|
first_line_pattern = '^#!.*\bruby\b'
|
||||||
line_comment = "# "
|
line_comment = "# "
|
||||||
autoclose_before = ";:.,=}])>"
|
autoclose_before = ";:.,=}])>"
|
||||||
brackets = [
|
brackets = [
|
||||||
|
|
|
@ -219,7 +219,7 @@ fn main() {
|
||||||
cx.spawn(|cx| handle_cli_connection(connection, app_state.clone(), cx))
|
cx.spawn(|cx| handle_cli_connection(connection, app_state.clone(), cx))
|
||||||
.detach();
|
.detach();
|
||||||
} else if let Ok(Some(paths)) = open_paths_rx.try_next() {
|
} else if let Ok(Some(paths)) = open_paths_rx.try_next() {
|
||||||
cx.update(|cx| workspace::open_paths(&paths, &app_state, cx))
|
cx.update(|cx| workspace::open_paths(&paths, &app_state, None, cx))
|
||||||
.detach();
|
.detach();
|
||||||
} else {
|
} else {
|
||||||
cx.spawn({
|
cx.spawn({
|
||||||
|
@ -243,7 +243,7 @@ fn main() {
|
||||||
let app_state = app_state.clone();
|
let app_state = app_state.clone();
|
||||||
async move {
|
async move {
|
||||||
while let Some(paths) = open_paths_rx.next().await {
|
while let Some(paths) = open_paths_rx.next().await {
|
||||||
cx.update(|cx| workspace::open_paths(&paths, &app_state, cx))
|
cx.update(|cx| workspace::open_paths(&paths, &app_state, None, cx))
|
||||||
.detach();
|
.detach();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -609,7 +609,7 @@ async fn handle_cli_connection(
|
||||||
|
|
||||||
let mut errored = false;
|
let mut errored = false;
|
||||||
match cx
|
match cx
|
||||||
.update(|cx| workspace::open_paths(&paths, &app_state, cx))
|
.update(|cx| workspace::open_paths(&paths, &app_state, None, cx))
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok((workspace, items)) => {
|
Ok((workspace, items)) => {
|
||||||
|
|
|
@ -702,6 +702,7 @@ mod tests {
|
||||||
open_paths(
|
open_paths(
|
||||||
&[PathBuf::from("/root/a"), PathBuf::from("/root/b")],
|
&[PathBuf::from("/root/a"), PathBuf::from("/root/b")],
|
||||||
&app_state,
|
&app_state,
|
||||||
|
None,
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
@ -709,7 +710,7 @@ mod tests {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(cx.window_ids().len(), 1);
|
assert_eq!(cx.window_ids().len(), 1);
|
||||||
|
|
||||||
cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, cx))
|
cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, None, cx))
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(cx.window_ids().len(), 1);
|
assert_eq!(cx.window_ids().len(), 1);
|
||||||
|
@ -728,6 +729,7 @@ mod tests {
|
||||||
open_paths(
|
open_paths(
|
||||||
&[PathBuf::from("/root/b"), PathBuf::from("/root/c")],
|
&[PathBuf::from("/root/b"), PathBuf::from("/root/c")],
|
||||||
&app_state,
|
&app_state,
|
||||||
|
None,
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
@ -735,16 +737,36 @@ mod tests {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(cx.window_ids().len(), 2);
|
assert_eq!(cx.window_ids().len(), 2);
|
||||||
|
|
||||||
|
// Replace existing windows
|
||||||
|
let window_id = cx.window_ids()[0];
|
||||||
cx.update(|cx| {
|
cx.update(|cx| {
|
||||||
open_paths(
|
open_paths(
|
||||||
&[PathBuf::from("/root/c"), PathBuf::from("/root/d")],
|
&[PathBuf::from("/root/c"), PathBuf::from("/root/d")],
|
||||||
&app_state,
|
&app_state,
|
||||||
|
Some(window_id),
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(cx.window_ids().len(), 3);
|
assert_eq!(cx.window_ids().len(), 2);
|
||||||
|
let workspace_1 = cx
|
||||||
|
.read_window(cx.window_ids()[0], |cx| cx.root_view().clone())
|
||||||
|
.unwrap()
|
||||||
|
.clone()
|
||||||
|
.downcast::<Workspace>()
|
||||||
|
.unwrap();
|
||||||
|
workspace_1.update(cx, |workspace, cx| {
|
||||||
|
assert_eq!(
|
||||||
|
workspace
|
||||||
|
.worktrees(cx)
|
||||||
|
.map(|w| w.read(cx).abs_path())
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
&[Path::new("/root/c").into(), Path::new("/root/d").into()]
|
||||||
|
);
|
||||||
|
assert!(workspace.left_sidebar().read(cx).is_open());
|
||||||
|
assert!(workspace.active_pane().is_focused(cx));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
|
@ -756,7 +778,7 @@ mod tests {
|
||||||
.insert_tree("/root", json!({"a": "hey"}))
|
.insert_tree("/root", json!({"a": "hey"}))
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, cx))
|
cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, None, cx))
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(cx.window_ids().len(), 1);
|
assert_eq!(cx.window_ids().len(), 1);
|
||||||
|
@ -799,7 +821,7 @@ mod tests {
|
||||||
assert!(!cx.is_window_edited(workspace.window_id()));
|
assert!(!cx.is_window_edited(workspace.window_id()));
|
||||||
|
|
||||||
// Opening the buffer again doesn't impact the window's edited state.
|
// Opening the buffer again doesn't impact the window's edited state.
|
||||||
cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, cx))
|
cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, None, cx))
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let editor = workspace.read_with(cx, |workspace, cx| {
|
let editor = workspace.read_with(cx, |workspace, cx| {
|
||||||
|
|
|
@ -56,6 +56,8 @@ async function main() {
|
||||||
headers: {"Content-Type": "application/json"},
|
headers: {"Content-Type": "application/json"},
|
||||||
body: JSON.stringify(body)
|
body: JSON.stringify(body)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
process.exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
function randomU64() {
|
function randomU64() {
|
||||||
|
|
|
@ -44,9 +44,7 @@ export default function editor(colorScheme: ColorScheme) {
|
||||||
activeLineBackground: withOpacity(background(layer, "on"), 0.75),
|
activeLineBackground: withOpacity(background(layer, "on"), 0.75),
|
||||||
highlightedLineBackground: background(layer, "on"),
|
highlightedLineBackground: background(layer, "on"),
|
||||||
// Inline autocomplete suggestions, Co-pilot suggestions, etc.
|
// Inline autocomplete suggestions, Co-pilot suggestions, etc.
|
||||||
suggestion: {
|
suggestion: syntax.predictive,
|
||||||
color: syntax.predictive.color,
|
|
||||||
},
|
|
||||||
codeActions: {
|
codeActions: {
|
||||||
indicator: {
|
indicator: {
|
||||||
color: foreground(layer, "variant"),
|
color: foreground(layer, "variant"),
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import deepmerge from "deepmerge"
|
import deepmerge from "deepmerge"
|
||||||
import { FontWeight, fontWeights } from "../../common"
|
import { FontWeight, fontWeights } from "../../common"
|
||||||
import { ColorScheme } from "./colorScheme"
|
import { ColorScheme } from "./colorScheme"
|
||||||
|
import chroma from "chroma-js"
|
||||||
|
|
||||||
export interface SyntaxHighlightStyle {
|
export interface SyntaxHighlightStyle {
|
||||||
color: string
|
color: string
|
||||||
|
@ -128,6 +129,8 @@ function buildDefaultSyntax(colorScheme: ColorScheme): Syntax {
|
||||||
[key: string]: Omit<SyntaxHighlightStyle, "color">
|
[key: string]: Omit<SyntaxHighlightStyle, "color">
|
||||||
} = {}
|
} = {}
|
||||||
|
|
||||||
|
const light = colorScheme.isLight
|
||||||
|
|
||||||
// then spread the default to each style
|
// then spread the default to each style
|
||||||
for (const key of Object.keys({} as Syntax)) {
|
for (const key of Object.keys({} as Syntax)) {
|
||||||
syntax[key as keyof Syntax] = {
|
syntax[key as keyof Syntax] = {
|
||||||
|
@ -135,11 +138,20 @@ function buildDefaultSyntax(colorScheme: ColorScheme): Syntax {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Mix the neutral and blue colors to get a
|
||||||
|
// predictive color distinct from any other color in the theme
|
||||||
|
const predictive = chroma.mix(
|
||||||
|
colorScheme.ramps.neutral(0.4).hex(),
|
||||||
|
colorScheme.ramps.blue(0.4).hex(),
|
||||||
|
0.45,
|
||||||
|
"lch"
|
||||||
|
).hex()
|
||||||
|
|
||||||
const color = {
|
const color = {
|
||||||
primary: colorScheme.ramps.neutral(1).hex(),
|
primary: colorScheme.ramps.neutral(1).hex(),
|
||||||
comment: colorScheme.ramps.neutral(0.71).hex(),
|
comment: colorScheme.ramps.neutral(0.71).hex(),
|
||||||
punctuation: colorScheme.ramps.neutral(0.86).hex(),
|
punctuation: colorScheme.ramps.neutral(0.86).hex(),
|
||||||
predictive: colorScheme.ramps.neutral(0.57).hex(),
|
predictive: predictive,
|
||||||
emphasis: colorScheme.ramps.blue(0.5).hex(),
|
emphasis: colorScheme.ramps.blue(0.5).hex(),
|
||||||
string: colorScheme.ramps.orange(0.5).hex(),
|
string: colorScheme.ramps.orange(0.5).hex(),
|
||||||
function: colorScheme.ramps.yellow(0.5).hex(),
|
function: colorScheme.ramps.yellow(0.5).hex(),
|
||||||
|
@ -169,6 +181,7 @@ function buildDefaultSyntax(colorScheme: ColorScheme): Syntax {
|
||||||
},
|
},
|
||||||
predictive: {
|
predictive: {
|
||||||
color: color.predictive,
|
color: color.predictive,
|
||||||
|
italic: true,
|
||||||
},
|
},
|
||||||
emphasis: {
|
emphasis: {
|
||||||
color: color.emphasis,
|
color: color.emphasis,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue