diff --git a/Cargo.lock b/Cargo.lock index ded64052c8e165e4b4ca3955e3382a38874d88bf..49f37fb0429b0d2ddd44b32099e1a1544adc6fbc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1275,11 +1275,10 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.83" +version = "1.0.84" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +checksum = "0f8e7c90afad890484a21653d08b6e209ae34770fb5ee298f9c699fcc1e5c856" dependencies = [ - "jobserver", "libc", ] @@ -1880,6 +1879,30 @@ dependencies = [ "zed-actions", ] +[[package]] +name = "command_palette2" +version = "0.1.0" +dependencies = [ + "anyhow", + "collections", + "ctor", + "editor2", + "env_logger 0.9.3", + "fuzzy2", + "gpui2", + "language2", + "picker2", + "project2", + "serde", + "serde_json", + "settings2", + "theme2", + "ui2", + "util", + "workspace2", + "zed_actions2", +] + [[package]] name = "component_test" version = "0.1.0" @@ -2781,6 +2804,7 @@ dependencies = [ "tree-sitter-html", "tree-sitter-rust", "tree-sitter-typescript", + "ui2", "unindent", "util", "workspace2", @@ -4370,15 +4394,6 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" -[[package]] -name = "jobserver" -version = "0.1.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "936cfd212a0155903bcbc060e316fb6cc7cbf2e1907329391ebadc1fe0ce77c2" -dependencies = [ - "libc", -] - [[package]] name = "journal" version = "0.1.0" @@ -4433,6 +4448,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "json_comments" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dbbfed4e59ba9750e15ba154fdfd9329cee16ff3df539c2666b70f58cc32105" + [[package]] name = "jwt" version = "0.16.0" @@ -6129,6 +6150,7 @@ dependencies = [ "serde_json", "settings2", "theme2", + "ui2", "util", ] @@ -9155,10 +9177,13 @@ dependencies = [ "anyhow", "convert_case 0.6.0", "gpui2", + "indexmap 1.9.3", + "json_comments", "log", "rust-embed", "serde", "simplelog", + "strum", "theme2", "uuid 1.4.1", ] @@ -11362,6 +11387,7 @@ dependencies = [ "cli", "client2", "collections", + "command_palette2", "copilot2", "ctor", "db2", @@ -11448,6 +11474,15 @@ dependencies = [ "util", "uuid 1.4.1", "workspace2", + "zed_actions2", +] + +[[package]] +name = "zed_actions2" +version = "0.1.0" +dependencies = [ + "gpui2", + "serde", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 1b8081d06639c8ff611e2b5228cbfee1e6005b4b..905750f8352b02422fa0815b7a51e17d74b0daff 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ members = [ "crates/collab_ui", "crates/collections", "crates/command_palette", + "crates/command_palette2", "crates/component_test", "crates/context_menu", "crates/copilot", @@ -110,7 +111,8 @@ members = [ "crates/xtask", "crates/zed", "crates/zed2", - "crates/zed-actions" + "crates/zed-actions", + "crates/zed_actions2" ] default-members = ["crates/zed"] resolver = "2" diff --git a/assets/keymaps/jetbrains.json b/assets/keymaps/jetbrains.json index ab093a8deb49059659addb9255564d8a7106412f..b2ed144a3f364822fd5e0375a75e07bb50d0ab20 100644 --- a/assets/keymaps/jetbrains.json +++ b/assets/keymaps/jetbrains.json @@ -10,6 +10,7 @@ "bindings": { "ctrl->": "zed::IncreaseBufferFontSize", "ctrl-<": "zed::DecreaseBufferFontSize", + "ctrl-shift-j": "editor::JoinLines", "cmd-d": "editor::DuplicateLine", "cmd-backspace": "editor::DeleteLine", "cmd-pagedown": "editor::MovePageDown", @@ -18,7 +19,7 @@ "cmd-alt-enter": "editor::NewlineAbove", "shift-enter": "editor::NewlineBelow", "cmd--": "editor::Fold", - "cmd-=": "editor::UnfoldLines", + "cmd-+": "editor::UnfoldLines", "alt-shift-g": "editor::SplitSelectionIntoLines", "ctrl-g": [ "editor::SelectNext", diff --git a/assets/settings/default.json b/assets/settings/default.json index 19c73ca0212016bf61b7fcf519d62fee33a4417b..42f3b3128666e84274a472fadd498652eadaa8a5 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -102,6 +102,16 @@ "selections": true }, "relative_line_numbers": false, + // When to populate a new search's query based on the text under the cursor. + // This setting can take the following three values: + // + // 1. Always populate the search query with the word under the cursor (default). + // "always" + // 2. Only populate the search query when there is text selected + // "selection" + // 3. Never populate the search query + // "never" + "seed_search_query_from_cursor": "always", // Inlay hint related settings "inlay_hints": { // Global switch to toggle hints on and off, switched off by default. @@ -199,7 +209,7 @@ "ensure_final_newline_on_save": true, // Whether or not to perform a buffer format before saving "format_on_save": "on", - // How to perform a buffer format. This setting can take two values: + // How to perform a buffer format. This setting can take 4 values: // // 1. Format code using the current language server: // "formatter": "language_server" diff --git a/assets/themes/src/vscode/gruvbox/gruvbox-dark-hard.json b/assets/themes/src/vscode/gruvbox/gruvbox-dark-hard.json index 4c1a7c9e8aaa100b2fbbb37cad882e4f9a29caf7..1b7eb8eef644fa6bd283270f2770b60fceab6f2a 100644 --- a/assets/themes/src/vscode/gruvbox/gruvbox-dark-hard.json +++ b/assets/themes/src/vscode/gruvbox/gruvbox-dark-hard.json @@ -423,7 +423,10 @@ } }, { - "scope": ["string.interpolated.dollar.shell", "string.interpolated.backtick.shell"], + "scope": [ + "string.interpolated.dollar.shell", + "string.interpolated.backtick.shell" + ], "settings": { "foreground": "#8ec07c" } @@ -489,13 +492,19 @@ { "name": "coloring of the Java import and package identifiers", - "scope": ["storage.modifier.import.java", "storage.modifier.package.java"], + "scope": [ + "storage.modifier.import.java", + "storage.modifier.package.java" + ], "settings": { "foreground": "#bdae93" } }, { - "scope": ["keyword.other.import.java", "keyword.other.package.java"], + "scope": [ + "keyword.other.import.java", + "keyword.other.package.java" + ], "settings": { "foreground": "#8ec07c" } @@ -628,7 +637,9 @@ }, { "name": "JSON Level 0", - "scope": ["source.json meta.structure.dictionary.json support.type.property-name.json"], + "scope": [ + "source.json meta.structure.dictionary.json support.type.property-name.json" + ], "settings": { "foreground": "#b8bb26" } @@ -761,7 +772,10 @@ } }, { - "scope": ["source.go keyword.interface", "source.go keyword.struct"], + "scope": [ + "source.go keyword.interface", + "source.go keyword.struct" + ], "settings": { "foreground": "#83a598" } @@ -788,7 +802,10 @@ { "name": "ReasonML String", - "scope": ["source.reason string.double", "source.reason string.regexp"], + "scope": [ + "source.reason string.double", + "source.reason string.regexp" + ], "settings": { "foreground": "#b8bb26" } @@ -809,7 +826,10 @@ }, { "name": "ReasonML property", - "scope": ["source.reason support.property-value", "source.reason entity.name.filename"], + "scope": [ + "source.reason support.property-value", + "source.reason entity.name.filename" + ], "settings": { "foreground": "#fe8019" } @@ -831,7 +851,9 @@ }, { "name": "Powershell function attribute", - "scope": ["source.powershell support.function.attribute.powershell"], + "scope": [ + "source.powershell support.function.attribute.powershell" + ], "settings": { "foreground": "#bdae93" } diff --git a/assets/themes/src/vscode/gruvbox/gruvbox-dark-medium.json b/assets/themes/src/vscode/gruvbox/gruvbox-dark-medium.json index e9ef916193b471785a17a461c7063a5325252ba7..0f22d6fc219e4cb89a77a54b833d34fe2fd2a59d 100644 --- a/assets/themes/src/vscode/gruvbox/gruvbox-dark-medium.json +++ b/assets/themes/src/vscode/gruvbox/gruvbox-dark-medium.json @@ -423,7 +423,10 @@ } }, { - "scope": ["string.interpolated.dollar.shell", "string.interpolated.backtick.shell"], + "scope": [ + "string.interpolated.dollar.shell", + "string.interpolated.backtick.shell" + ], "settings": { "foreground": "#8ec07c" } @@ -489,13 +492,19 @@ { "name": "coloring of the Java import and package identifiers", - "scope": ["storage.modifier.import.java", "storage.modifier.package.java"], + "scope": [ + "storage.modifier.import.java", + "storage.modifier.package.java" + ], "settings": { "foreground": "#bdae93" } }, { - "scope": ["keyword.other.import.java", "keyword.other.package.java"], + "scope": [ + "keyword.other.import.java", + "keyword.other.package.java" + ], "settings": { "foreground": "#8ec07c" } @@ -628,7 +637,9 @@ }, { "name": "JSON Level 0", - "scope": ["source.json meta.structure.dictionary.json support.type.property-name.json"], + "scope": [ + "source.json meta.structure.dictionary.json support.type.property-name.json" + ], "settings": { "foreground": "#b8bb26" } @@ -761,7 +772,10 @@ } }, { - "scope": ["source.go keyword.interface", "source.go keyword.struct"], + "scope": [ + "source.go keyword.interface", + "source.go keyword.struct" + ], "settings": { "foreground": "#83a598" } @@ -788,7 +802,10 @@ { "name": "ReasonML String", - "scope": ["source.reason string.double", "source.reason string.regexp"], + "scope": [ + "source.reason string.double", + "source.reason string.regexp" + ], "settings": { "foreground": "#b8bb26" } @@ -809,7 +826,10 @@ }, { "name": "ReasonML property", - "scope": ["source.reason support.property-value", "source.reason entity.name.filename"], + "scope": [ + "source.reason support.property-value", + "source.reason entity.name.filename" + ], "settings": { "foreground": "#fe8019" } @@ -831,7 +851,9 @@ }, { "name": "Powershell function attribute", - "scope": ["source.powershell support.function.attribute.powershell"], + "scope": [ + "source.powershell support.function.attribute.powershell" + ], "settings": { "foreground": "#bdae93" } diff --git a/assets/themes/src/vscode/gruvbox/gruvbox-dark-soft.json b/assets/themes/src/vscode/gruvbox/gruvbox-dark-soft.json index 8581447f498ad332d9ac74de6fb20ccaed76a1c3..e03a9f0f5c2caa5124c8e5e98b969b30a9ad2150 100644 --- a/assets/themes/src/vscode/gruvbox/gruvbox-dark-soft.json +++ b/assets/themes/src/vscode/gruvbox/gruvbox-dark-soft.json @@ -423,7 +423,10 @@ } }, { - "scope": ["string.interpolated.dollar.shell", "string.interpolated.backtick.shell"], + "scope": [ + "string.interpolated.dollar.shell", + "string.interpolated.backtick.shell" + ], "settings": { "foreground": "#8ec07c" } @@ -489,13 +492,19 @@ { "name": "coloring of the Java import and package identifiers", - "scope": ["storage.modifier.import.java", "storage.modifier.package.java"], + "scope": [ + "storage.modifier.import.java", + "storage.modifier.package.java" + ], "settings": { "foreground": "#bdae93" } }, { - "scope": ["keyword.other.import.java", "keyword.other.package.java"], + "scope": [ + "keyword.other.import.java", + "keyword.other.package.java" + ], "settings": { "foreground": "#8ec07c" } @@ -628,7 +637,9 @@ }, { "name": "JSON Level 0", - "scope": ["source.json meta.structure.dictionary.json support.type.property-name.json"], + "scope": [ + "source.json meta.structure.dictionary.json support.type.property-name.json" + ], "settings": { "foreground": "#b8bb26" } @@ -761,7 +772,10 @@ } }, { - "scope": ["source.go keyword.interface", "source.go keyword.struct"], + "scope": [ + "source.go keyword.interface", + "source.go keyword.struct" + ], "settings": { "foreground": "#83a598" } @@ -788,7 +802,10 @@ { "name": "ReasonML String", - "scope": ["source.reason string.double", "source.reason string.regexp"], + "scope": [ + "source.reason string.double", + "source.reason string.regexp" + ], "settings": { "foreground": "#b8bb26" } @@ -809,7 +826,10 @@ }, { "name": "ReasonML property", - "scope": ["source.reason support.property-value", "source.reason entity.name.filename"], + "scope": [ + "source.reason support.property-value", + "source.reason entity.name.filename" + ], "settings": { "foreground": "#fe8019" } @@ -831,7 +851,9 @@ }, { "name": "Powershell function attribute", - "scope": ["source.powershell support.function.attribute.powershell"], + "scope": [ + "source.powershell support.function.attribute.powershell" + ], "settings": { "foreground": "#bdae93" } diff --git a/assets/themes/src/vscode/gruvbox/gruvbox-light-hard.json b/assets/themes/src/vscode/gruvbox/gruvbox-light-hard.json index bd0f60eac99bb62216a58fb90932016741fb172f..71af6c807ef5c74c8d99118cb7afb527f44b5d79 100644 --- a/assets/themes/src/vscode/gruvbox/gruvbox-light-hard.json +++ b/assets/themes/src/vscode/gruvbox/gruvbox-light-hard.json @@ -422,7 +422,10 @@ } }, { - "scope": ["string.interpolated.dollar.shell", "string.interpolated.backtick.shell"], + "scope": [ + "string.interpolated.dollar.shell", + "string.interpolated.backtick.shell" + ], "settings": { "foreground": "#427b58" } @@ -488,13 +491,19 @@ { "name": "coloring of the Java import and package identifiers", - "scope": ["storage.modifier.import.java", "storage.modifier.package.java"], + "scope": [ + "storage.modifier.import.java", + "storage.modifier.package.java" + ], "settings": { "foreground": "#665c54" } }, { - "scope": ["keyword.other.import.java", "keyword.other.package.java"], + "scope": [ + "keyword.other.import.java", + "keyword.other.package.java" + ], "settings": { "foreground": "#427b58" } @@ -627,7 +636,9 @@ }, { "name": "JSON Level 0", - "scope": ["source.json meta.structure.dictionary.json support.type.property-name.json"], + "scope": [ + "source.json meta.structure.dictionary.json support.type.property-name.json" + ], "settings": { "foreground": "#79740e" } @@ -760,7 +771,10 @@ } }, { - "scope": ["source.go keyword.interface", "source.go keyword.struct"], + "scope": [ + "source.go keyword.interface", + "source.go keyword.struct" + ], "settings": { "foreground": "#076678" } @@ -787,7 +801,10 @@ { "name": "ReasonML String", - "scope": ["source.reason string.double", "source.reason string.regexp"], + "scope": [ + "source.reason string.double", + "source.reason string.regexp" + ], "settings": { "foreground": "#79740e" } @@ -808,7 +825,10 @@ }, { "name": "ReasonML property", - "scope": ["source.reason support.property-value", "source.reason entity.name.filename"], + "scope": [ + "source.reason support.property-value", + "source.reason entity.name.filename" + ], "settings": { "foreground": "#af3a03" } @@ -830,7 +850,9 @@ }, { "name": "Powershell function attribute", - "scope": ["source.powershell support.function.attribute.powershell"], + "scope": [ + "source.powershell support.function.attribute.powershell" + ], "settings": { "foreground": "#665c54" } diff --git a/assets/themes/src/vscode/gruvbox/gruvbox-light-medium.json b/assets/themes/src/vscode/gruvbox/gruvbox-light-medium.json index 6cd742090e8bcfbb7daad2d87a063edec8775e93..39de91c9ec25b3a50ad21f955693d2cfdb1eb8fe 100644 --- a/assets/themes/src/vscode/gruvbox/gruvbox-light-medium.json +++ b/assets/themes/src/vscode/gruvbox/gruvbox-light-medium.json @@ -422,7 +422,10 @@ } }, { - "scope": ["string.interpolated.dollar.shell", "string.interpolated.backtick.shell"], + "scope": [ + "string.interpolated.dollar.shell", + "string.interpolated.backtick.shell" + ], "settings": { "foreground": "#427b58" } @@ -488,13 +491,19 @@ { "name": "coloring of the Java import and package identifiers", - "scope": ["storage.modifier.import.java", "storage.modifier.package.java"], + "scope": [ + "storage.modifier.import.java", + "storage.modifier.package.java" + ], "settings": { "foreground": "#665c54" } }, { - "scope": ["keyword.other.import.java", "keyword.other.package.java"], + "scope": [ + "keyword.other.import.java", + "keyword.other.package.java" + ], "settings": { "foreground": "#427b58" } @@ -627,7 +636,9 @@ }, { "name": "JSON Level 0", - "scope": ["source.json meta.structure.dictionary.json support.type.property-name.json"], + "scope": [ + "source.json meta.structure.dictionary.json support.type.property-name.json" + ], "settings": { "foreground": "#79740e" } @@ -760,7 +771,10 @@ } }, { - "scope": ["source.go keyword.interface", "source.go keyword.struct"], + "scope": [ + "source.go keyword.interface", + "source.go keyword.struct" + ], "settings": { "foreground": "#076678" } @@ -787,7 +801,10 @@ { "name": "ReasonML String", - "scope": ["source.reason string.double", "source.reason string.regexp"], + "scope": [ + "source.reason string.double", + "source.reason string.regexp" + ], "settings": { "foreground": "#79740e" } @@ -808,7 +825,10 @@ }, { "name": "ReasonML property", - "scope": ["source.reason support.property-value", "source.reason entity.name.filename"], + "scope": [ + "source.reason support.property-value", + "source.reason entity.name.filename" + ], "settings": { "foreground": "#af3a03" } @@ -830,7 +850,9 @@ }, { "name": "Powershell function attribute", - "scope": ["source.powershell support.function.attribute.powershell"], + "scope": [ + "source.powershell support.function.attribute.powershell" + ], "settings": { "foreground": "#665c54" } diff --git a/assets/themes/src/vscode/gruvbox/gruvbox-light-soft.json b/assets/themes/src/vscode/gruvbox/gruvbox-light-soft.json index a101704eed4ed97a68ea33e59f051969d8b9c4a2..997dcfe1544267bcc01d447968eed8c12192e67d 100644 --- a/assets/themes/src/vscode/gruvbox/gruvbox-light-soft.json +++ b/assets/themes/src/vscode/gruvbox/gruvbox-light-soft.json @@ -422,7 +422,10 @@ } }, { - "scope": ["string.interpolated.dollar.shell", "string.interpolated.backtick.shell"], + "scope": [ + "string.interpolated.dollar.shell", + "string.interpolated.backtick.shell" + ], "settings": { "foreground": "#427b58" } @@ -488,13 +491,19 @@ { "name": "coloring of the Java import and package identifiers", - "scope": ["storage.modifier.import.java", "storage.modifier.package.java"], + "scope": [ + "storage.modifier.import.java", + "storage.modifier.package.java" + ], "settings": { "foreground": "#665c54" } }, { - "scope": ["keyword.other.import.java", "keyword.other.package.java"], + "scope": [ + "keyword.other.import.java", + "keyword.other.package.java" + ], "settings": { "foreground": "#427b58" } @@ -627,7 +636,9 @@ }, { "name": "JSON Level 0", - "scope": ["source.json meta.structure.dictionary.json support.type.property-name.json"], + "scope": [ + "source.json meta.structure.dictionary.json support.type.property-name.json" + ], "settings": { "foreground": "#79740e" } @@ -760,7 +771,10 @@ } }, { - "scope": ["source.go keyword.interface", "source.go keyword.struct"], + "scope": [ + "source.go keyword.interface", + "source.go keyword.struct" + ], "settings": { "foreground": "#076678" } @@ -787,7 +801,10 @@ { "name": "ReasonML String", - "scope": ["source.reason string.double", "source.reason string.regexp"], + "scope": [ + "source.reason string.double", + "source.reason string.regexp" + ], "settings": { "foreground": "#79740e" } @@ -808,7 +825,10 @@ }, { "name": "ReasonML property", - "scope": ["source.reason support.property-value", "source.reason entity.name.filename"], + "scope": [ + "source.reason support.property-value", + "source.reason entity.name.filename" + ], "settings": { "foreground": "#af3a03" } @@ -830,7 +850,9 @@ }, { "name": "Powershell function attribute", - "scope": ["source.powershell support.function.attribute.powershell"], + "scope": [ + "source.powershell support.function.attribute.powershell" + ], "settings": { "foreground": "#665c54" } diff --git a/assets/themes/src/vscode/noctis/family.json b/assets/themes/src/vscode/noctis/family.json index 26aba7e21357fb566a46f5d3f4ed8d59ba1226a7..525cb160f52e48ba839e6d1d51b81b50abad40cb 100644 --- a/assets/themes/src/vscode/noctis/family.json +++ b/assets/themes/src/vscode/noctis/family.json @@ -1,61 +1,61 @@ { - "name": "Notctis", - "author": "Liviu Schera (liviuschera)", - "themes": [ - { - "name": "Noctis Azureus", - "file_name": "azureus.json", - "appearance": "dark" - }, - { - "name": "Noctis Bordo", - "file_name": "bordo.json", - "appearance": "dark" - }, - { - "name": "Noctus Hibernus", - "file_name": "hibernus.json", - "appearance": "light" - }, - { - "name": "Noctis Lilac", - "file_name": "lilac.json", - "appearance": "dark" - }, - { - "name": "Noctis Lux", - "file_name": "lux.json", - "appearance": "light" - }, - { - "name": "Noctis Minimus", - "file_name": "minimus.json", - "appearance": "dark" - }, - { - "name": "Noctis", - "file_name": "noctis.json", - "appearance": "dark" - }, - { - "name": "Noctis Obscuro", - "file_name": "obscuro.json", - "appearance": "dark" - }, - { - "name": "Noctis Sereno", - "file_name": "obscuro.json", - "appearance": "dark" - }, - { - "name": "Noctis Uva", - "file_name": "uva.json", - "appearance": "dark" - }, - { - "name": "Noctis Viola", - "file_name": "viola.json", - "appearance": "dark" - } - ] + "name": "Noctis", + "author": "Liviu Schera (liviuschera)", + "themes": [ + { + "name": "Noctis Azureus", + "file_name": "azureus.json", + "appearance": "dark" + }, + { + "name": "Noctis Bordo", + "file_name": "bordo.json", + "appearance": "dark" + }, + { + "name": "Noctus Hibernus", + "file_name": "hibernus.json", + "appearance": "light" + }, + { + "name": "Noctis Lilac", + "file_name": "lilac.json", + "appearance": "dark" + }, + { + "name": "Noctis Lux", + "file_name": "lux.json", + "appearance": "light" + }, + { + "name": "Noctis Minimus", + "file_name": "minimus.json", + "appearance": "dark" + }, + { + "name": "Noctis", + "file_name": "noctis.json", + "appearance": "dark" + }, + { + "name": "Noctis Obscuro", + "file_name": "obscuro.json", + "appearance": "dark" + }, + { + "name": "Noctis Sereno", + "file_name": "obscuro.json", + "appearance": "dark" + }, + { + "name": "Noctis Uva", + "file_name": "uva.json", + "appearance": "dark" + }, + { + "name": "Noctis Viola", + "file_name": "viola.json", + "appearance": "dark" + } + ] } diff --git a/assets/themes/src/vscode/rose-pine/family.json b/assets/themes/src/vscode/rose-pine/family.json index 5b72edd300fe585f6c92121813c0baf61a5778e8..1cdcab78420a9560eb0d45de2e0d0097703d6154 100644 --- a/assets/themes/src/vscode/rose-pine/family.json +++ b/assets/themes/src/vscode/rose-pine/family.json @@ -1,21 +1,21 @@ { - "name": "Rose Pine", - "author": "Rosé Pine", - "themes": [ - { - "name": "Rose Pine", - "file_name": "rose-pine.json", - "appearance": "dark" - }, - { - "name": "Rose Moon", - "file_name": "rose-pine-moon.json", - "appearance": "dark" - }, - { - "name": "Rose Pine Dawn", - "file_name": "rose-pine-dawn.json", - "appearance": "light" - } - ] + "name": "Rose Pine", + "author": "Rosé Pine", + "themes": [ + { + "name": "Rose Pine", + "file_name": "rose-pine.json", + "appearance": "dark" + }, + { + "name": "Rose Pine Moon", + "file_name": "rose-pine-moon.json", + "appearance": "dark" + }, + { + "name": "Rose Pine Dawn", + "file_name": "rose-pine-dawn.json", + "appearance": "light" + } + ] } diff --git a/assets/themes/src/vscode/synthwave-84/synthwave.json b/assets/themes/src/vscode/synthwave-84/synthwave.json index 5b38ef46fa70a932b94933b61c176c6842104a9c..9b23270d8c5f439bebc205ed218a820129f69ba3 100644 --- a/assets/themes/src/vscode/synthwave-84/synthwave.json +++ b/assets/themes/src/vscode/synthwave-84/synthwave.json @@ -1,822 +1,841 @@ { - "name": "SynthWave 84", - "type": "dark", - "semanticHighlighting": true, - "colors": { - "focusBorder": "#1f212b", - "foreground": "#ffffff", - "widget.shadow": "#2a2139", - "selection.background": "#ffffff20", - "errorForeground": "#fe4450", - "textLink.activeForeground": "#ff7edb", - "textLink.foreground": "#f97e72", - "button.background": "#614D85", - "dropdown.background": "#232530", - "dropdown.listBackground": "#2a2139", - "input.background": "#2a2139", - "inputOption.activeBorder": "#ff7edb99", - "inputValidation.errorBackground": "#fe445080", - "inputValidation.errorBorder": "#fe445000", - "scrollbar.shadow": "#2a2139", - "scrollbarSlider.activeBackground": "#9d8bca20", - "scrollbarSlider.background": "#9d8bca30", - "scrollbarSlider.hoverBackground": "#9d8bca50", - "badge.foreground": "#ffffff", - "badge.background": "#2a2139", - "progressBar.background": "#f97e72", - "list.activeSelectionBackground": "#ffffff20", - "list.activeSelectionForeground": "#ffffff", - "list.dropBackground": "#34294f66", - "list.focusBackground": "#ffffff20", - "list.focusForeground": "#ffffff", - "list.highlightForeground": "#f97e72", - "list.hoverBackground": "#37294d99", - "list.hoverForeground": "#ffffff", - "list.inactiveSelectionBackground": "#ffffff20", - "list.inactiveSelectionForeground": "#ffffff", - "list.inactiveFocusBackground": "#2a213999", - "list.errorForeground": "#fe4450E6", - "list.warningForeground": "#72f1b8bb", - "activityBar.background": "#171520", - "activityBar.dropBackground": "#34294f66", - "activityBar.foreground": "#ffffffCC", - "activityBarBadge.background": "#f97e72", - "activityBarBadge.foreground": "#2a2139", - "sideBar.background": "#241b2f", - "sideBar.foreground": "#ffffff99", - "sideBar.dropBackground": "#34294f4c", - "sideBarSectionHeader.background": "#241b2f", - "sideBarSectionHeader.foreground": "#ffffffca", - "menu.background": "#463465", - "editorGroup.border": "#495495", - "editorGroup.dropBackground": "#4954954a", - "editorGroupHeader.tabsBackground": "#241b2f", - "tab.border": "#241b2f00", - "tab.activeBorder": "#880088", - "tab.inactiveBackground": "#262335", - "editor.background": "#262335", - "editorLineNumber.foreground": "#ffffff73", - "editorLineNumber.activeForeground": "#ffffffcc", - "editorCursor.background": "#241b2f", - "editorCursor.foreground": "#f97e72", - "editor.selectionBackground": "#ffffff20", - "editor.selectionHighlightBackground": "#ffffff20", - "editor.wordHighlightBackground": "#34294f88", - "editor.wordHighlightStrongBackground": "#34294f88", - "editor.findMatchBackground": "#D18616bb", - "editor.findMatchHighlightBackground": "#D1861655", - "editor.findRangeHighlightBackground": "#34294f1a", - "editor.hoverHighlightBackground": "#463564", - "editor.lineHighlightBorder": "#7059AB66", - "editor.rangeHighlightBackground": "#49549539", - "editorIndentGuide.background": "#444251", - "editorIndentGuide.activeBackground": "#A148AB80", - "editorRuler.foreground": "#A148AB80", - "editorCodeLens.foreground": "#ffffff7c", - "editorBracketMatch.background": "#34294f66", - "editorBracketMatch.border": "#495495", - "editorOverviewRuler.border": "#34294fb3", - "editorOverviewRuler.findMatchForeground": "#D1861699", - "editorOverviewRuler.modifiedForeground": "#b893ce99", - "editorOverviewRuler.addedForeground": "#09f7a099", - "editorOverviewRuler.deletedForeground": "#fe445099", - "editorOverviewRuler.errorForeground": "#fe4450dd", - "editorOverviewRuler.warningForeground": "#72f1b8cc", - "editorError.foreground": "#fe4450", - "editorWarning.foreground": "#72f1b8cc", - "editorGutter.modifiedBackground": "#b893ce8f", - "editorGutter.addedBackground": "#206d4bd6", - "editorGutter.deletedBackground": "#fa2e46a4", - "diffEditor.insertedTextBackground": "#0beb9935", - "diffEditor.removedTextBackground": "#fe445035", - "editorWidget.background": "#171520DC", - "editorWidget.border": "#ffffff22", - "editorWidget.resizeBorder": "#ffffff44", - "editorSuggestWidget.highlightForeground": "#f97e72", - "editorSuggestWidget.selectedBackground": "#ffffff36", - "peekView.border": "#495495", - "peekViewEditor.background": "#232530", - "peekViewEditor.matchHighlightBackground": "#D18616bb", - "peekViewResult.background": "#232530", - "peekViewResult.matchHighlightBackground": "#D1861655", - "peekViewResult.selectionBackground": "#2a213980", - "peekViewTitle.background": "#232530", - "panelTitle.activeBorder": "#f97e72", - "statusBar.background": "#241b2f", - "statusBar.foreground": "#ffffff80", - "statusBar.debuggingBackground": "#f97e72", - "statusBar.debuggingForeground": "#08080f", - "statusBar.noFolderBackground": "#241b2f", - "statusBarItem.prominentBackground": "#2a2139", - "statusBarItem.prominentHoverBackground": "#34294f", - "titleBar.activeBackground": "#241b2f", - "titleBar.inactiveBackground": "#241b2f", - "extensionButton.prominentBackground": "#f97e72", - "extensionButton.prominentHoverBackground": "#ff7edb", - "pickerGroup.foreground": "#f97e72ea", - "terminal.foreground": "#ffffff", - "terminal.ansiBlue": "#03edf9", - "terminal.ansiBrightBlue": "#03edf9", - "terminal.ansiBrightCyan": "#03edf9", - "terminal.ansiBrightGreen": "#72f1b8", - "terminal.ansiBrightMagenta": "#ff7edb", - "terminal.ansiBrightRed": "#fe4450", - "terminal.ansiBrightYellow": "#fede5d", - "terminal.ansiCyan": "#03edf9", - "terminal.ansiGreen": "#72f1b8", - "terminal.ansiMagenta": "#ff7edb", - "terminal.ansiRed": "#fe4450", - "terminal.ansiYellow": "#f3e70f", - "terminal.selectionBackground": "#ffffff20", - "terminalCursor.background": "#ffffff", - "terminalCursor.foreground": "#03edf9", - "debugToolBar.background": "#463465", - "walkThrough.embeddedEditorBackground": "#232530", - "gitDecoration.modifiedResourceForeground": "#b893ceee", - "gitDecoration.deletedResourceForeground": "#fe4450", - "gitDecoration.addedResourceForeground": "#72f1b8cc", - "gitDecoration.untrackedResourceForeground": "#72f1b8", - "gitDecoration.ignoredResourceForeground": "#ffffff59", - "minimapGutter.addedBackground": "#09f7a099", - "minimapGutter.modifiedBackground": "#b893ce", - "minimapGutter.deletedBackground": "#fe4450", - "breadcrumbPicker.background": "#232530" - }, - "tokenColors": [ - { - "name": "Comment", - "scope": [ - "comment", - "string.quoted.docstring.multi.python", - "string.quoted.docstring.multi.python punctuation.definition.string.begin.python", - "string.quoted.docstring.multi.python punctuation.definition.string.end.python" - ], - "settings": { - "foreground": "#848bbd", - "fontStyle": "italic" - } - }, - { - "name": "String", - "scope": ["string.quoted", "string.template", "punctuation.definition.string"], - "settings": { - "foreground": "#ff8b39" - } - }, - { - "name": "Punctuation within templates", - "scope": "string.template meta.embedded.line", - "settings": { - "foreground": "#b6b1b1" - } - }, - { - "name": "Variable", - "scope": ["variable", "entity.name.variable"], - "settings": { - "foreground": "#ff7edb" - } - }, - { - "name": "Language variable", - "scope": "variable.language", - "settings": { - "foreground": "#fe4450", - "fontStyle": "bold" - } - }, - { - "name": "Parameter", - "scope": "variable.parameter", - "settings": { - "fontStyle": "italic" - } - }, - { - "name": "Storage (declaration or modifier keyword)", - "scope": ["storage.type", "storage.modifier"], - "settings": { - "foreground": "#fede5d" - } - }, - { - "name": "Constant", - "scope": "constant", - "settings": { - "foreground": "#f97e72" - } - }, - { - "name": "Regex", - "scope": "string.regexp", - "settings": { - "foreground": "#f97e72" - } - }, - { - "name": "Number", - "scope": "constant.numeric", - "settings": { - "foreground": "#f97e72" - } - }, - { - "name": "Language constant (boolean, null)", - "scope": "constant.language", - "settings": { - "foreground": "#f97e72" - } - }, - { - "name": "Character escape", - "scope": "constant.character.escape", - "settings": { - "foreground": "#36f9f6" - } - }, - { - "name": "Entity", - "scope": "entity.name", - "settings": { - "foreground": "#fe4450" - } - }, - { - "name": "HTML or XML tag", - "scope": "entity.name.tag", - "settings": { - "foreground": "#72f1b8" - } - }, - { - "name": "HTML or XML tag brackets", - "scope": ["punctuation.definition.tag"], - "settings": { - "foreground": "#36f9f6" - } - }, - { - "name": "Tag attribute", - "scope": "entity.other.attribute-name", - "settings": { - "foreground": "#fede5d" - } - }, - { - "name": "Tag attribute HTML", - "scope": "entity.other.attribute-name.html", - "settings": { - "foreground": "#fede5d", - "fontStyle": "italic" - } - }, - { - "name": "Class", - "scope": ["entity.name.type", "meta.attribute.class.html"], - "settings": { - "foreground": "#fe4450" - } - }, - { - "name": "Inherited class", - "scope": "entity.other.inherited-class", - "settings": { - "foreground": "#D50" - } - }, - { - "name": "Function", - "scope": ["entity.name.function", "variable.function"], - "settings": { - "foreground": "#36f9f6" - } - }, - { - "name": "JS Export", - "scope": ["keyword.control.export.js", "keyword.control.import.js"], - "settings": { - "foreground": "#72f1b8" - } - }, - { - "name": "JS Numerics", - "scope": ["constant.numeric.decimal.js"], - "settings": { - "foreground": "#2EE2FA" - } - }, - { - "name": "Keyword", - "scope": "keyword", - "settings": { - "foreground": "#fede5d" - } - }, - { - "name": "Control keyword", - "scope": "keyword.control", - "settings": { - "foreground": "#fede5d" - } - }, - { - "name": "Operator", - "scope": "keyword.operator", - "settings": { - "foreground": "#fede5d" - } - }, - { - "name": "Special operator", - "scope": [ - "keyword.operator.new", - "keyword.operator.expression", - "keyword.operator.logical" - ], - "settings": { - "foreground": "#fede5d" - } - }, - { - "name": "Unit", - "scope": "keyword.other.unit", - "settings": { - "foreground": "#f97e72" - } - }, - { - "name": "Support", - "scope": "support", - "settings": { - "foreground": "#fe4450" - } - }, - { - "name": "Support function", - "scope": "support.function", - "settings": { - "foreground": "#36f9f6" - } - }, - { - "name": "Support variable", - "scope": "support.variable", - "settings": { - "foreground": "#ff7edb" - } - }, - { - "name": "Object literal key / property", - "scope": ["meta.object-literal.key", "support.type.property-name"], - "settings": { - "foreground": "#ff7edb" - } - }, - { - "name": "Key-value separator", - "scope": "punctuation.separator.key-value", - "settings": { - "foreground": "#b6b1b1" - } - }, - { - "name": "Embedded punctuation", - "scope": "punctuation.section.embedded", - "settings": { - "foreground": "#fede5d" - } - }, - { - "name": "Template expression", - "scope": [ - "punctuation.definition.template-expression.begin", - "punctuation.definition.template-expression.end" - ], - "settings": { - "foreground": "#72f1b8" - } - }, - { - "name": "CSS property", - "scope": ["support.type.property-name.css", "support.type.property-name.json"], - "settings": { - "foreground": "#72f1b8" - } - }, - { - "name": "JS Switch control", - "scope": "switch-block.expr.js", - "settings": { - "foreground": "#72f1b8" - } - }, - { - "name": "JS object path", - "scope": "variable.other.constant.property.js, variable.other.property.js", - "settings": { - "foreground": "#2ee2fa" - } - }, - { - "name": "Color", - "scope": "constant.other.color", - "settings": { - "foreground": "#f97e72" - } - }, - { - "name": "Font names", - "scope": "support.constant.font-name", - "settings": { - "foreground": "#f97e72" - } - }, - { - "name": "CSS #id", - "scope": "entity.other.attribute-name.id", - "settings": { - "foreground": "#36f9f6" - } - }, - { - "name": "Pseudo CSS", - "scope": [ - "entity.other.attribute-name.pseudo-element", - "entity.other.attribute-name.pseudo-class" - ], - "settings": { - "foreground": "#D50" - } - }, - { - "name": "CSS support functions (rgb)", - "scope": "support.function.misc.css", - "settings": { - "foreground": "#fe4450" - } - }, - { - "name": "Markup heading", - "scope": ["markup.heading", "entity.name.section"], - "settings": { - "foreground": "#ff7edb" - } - }, - { - "name": "Markup text", - "scope": ["text.html", "keyword.operator.assignment"], - "settings": { - "foreground": "#ffffffee" - } - }, - { - "name": "Markup quote", - "scope": "markup.quote", - "settings": { - "foreground": "#b6b1b1cc", - "fontStyle": "italic" - } - }, - { - "name": "Markup list", - "scope": "beginning.punctuation.definition.list", - "settings": { - "foreground": "#ff7edb" - } - }, - { - "name": "Markup link", - "scope": "markup.underline.link", - "settings": { - "foreground": "#D50" - } - }, - { - "name": "Markup link description", - "scope": "string.other.link.description", - "settings": { - "foreground": "#f97e72" - } - }, - { - "name": "Python function call", - "scope": "meta.function-call.generic.python", - "settings": { - "foreground": "#36f9f6" - } - }, - { - "name": "Python variable params", - "scope": "variable.parameter.function-call.python", - "settings": { - "foreground": "#72f1b8" - } - }, - { - "name": "C# storage type", - "scope": "storage.type.cs", - "settings": { - "foreground": "#fe4450" - } - }, - { - "name": "C# local variable", - "scope": "entity.name.variable.local.cs", - "settings": { - "foreground": "#ff7edb" - } - }, - { - "name": "C# properties and fields", - "scope": ["entity.name.variable.field.cs", "entity.name.variable.property.cs"], - "settings": { - "foreground": "#ff7edb" - } - }, - { - "name": "C placeholder", - "scope": "constant.other.placeholder.c", - "settings": { - "foreground": "#72f1b8", - "fontStyle": "italic" - } - }, - { - "name": "C preprocessors", - "scope": ["keyword.control.directive.include.c", "keyword.control.directive.define.c"], - "settings": { - "foreground": "#72f1b8" - } - }, - { - "name": "C storage modifier", - "scope": "storage.modifier.c", - "settings": { - "foreground": "#fe4450" - } - }, - { - "name": "C++ operators", - "scope": "source.cpp keyword.operator", - "settings": { - "foreground": "#fede5d" - } - }, - { - "name": "C++ placeholder", - "scope": "constant.other.placeholder.cpp", - "settings": { - "foreground": "#72f1b8", - "fontStyle": "italic" - } - }, - { - "name": "C++ include", - "scope": [ - "keyword.control.directive.include.cpp", - "keyword.control.directive.define.cpp" - ], - "settings": { - "foreground": "#72f1b8" - } - }, - { - "name": "C++ constant modifier", - "scope": "storage.modifier.specifier.const.cpp", - "settings": { - "foreground": "#fe4450" - } - }, - { - "name": "Elixir Classes", - "scope": [ - "source.elixir support.type.elixir", - "source.elixir meta.module.elixir entity.name.class.elixir" - ], - "settings": { - "foreground": "#36f9f6" - } - }, - { - "name": "Elixir Functions", - "scope": "source.elixir entity.name.function", - "settings": { - "foreground": "#72f1b8" - } - }, - { - "name": "Elixir Constants", - "scope": [ - "source.elixir constant.other.symbol.elixir", - "source.elixir constant.other.keywords.elixir" - ], - "settings": { - "foreground": "#36f9f6" - } - }, - { - "name": "Elixir String Punctuation", - "scope": "source.elixir punctuation.definition.string", - "settings": { - "foreground": "#72f1b8" - } - }, - { - "name": "Elixir", - "scope": [ - "source.elixir variable.other.readwrite.module.elixir", - "source.elixir variable.other.readwrite.module.elixir punctuation.definition.variable.elixir" - ], - "settings": { - "foreground": "#72f1b8" - } - }, - { - "name": "Elixir Binary Punctuation", - "scope": "source.elixir .punctuation.binary.elixir", - "settings": { - "foreground": "#ff7edb", - "fontStyle": "italic" - } - }, - { - "name": "Clojure Globals", - "scope": ["entity.global.clojure"], - "settings": { - "foreground": "#36f9f6", - "fontStyle": "bold" - } - }, - { - "name": "Clojure Storage", - "scope": ["storage.control.clojure"], - "settings": { - "foreground": "#36f9f6", - "fontStyle": "italic" - } - }, - { - "name": "Clojure Metadata", - "scope": ["meta.metadata.simple.clojure", "meta.metadata.map.clojure"], - "settings": { - "foreground": "#fe4450", - "fontStyle": "italic" - } - }, - { - "name": "Clojure Macros, Quoted", - "scope": ["meta.quoted-expression.clojure"], - "settings": { - "fontStyle": "italic" - } - }, - { - "name": "Clojure Symbols", - "scope": ["meta.symbol.clojure"], - "settings": { - "foreground": "#ff7edbff" - } - }, - { - "name": "Go basic", - "scope": "source.go", - "settings": { - "foreground": "#ff7edbff" - } - }, - { - "name": "Go Function Calls", - "scope": "source.go meta.function-call.go", - "settings": { - "foreground": "#36f9f6" - } - }, - { - "name": "Go Keywords", - "scope": [ - "source.go keyword.package.go", - "source.go keyword.import.go", - "source.go keyword.function.go", - "source.go keyword.type.go", - "source.go keyword.const.go", - "source.go keyword.var.go", - "source.go keyword.map.go", - "source.go keyword.channel.go", - "source.go keyword.control.go" - ], - "settings": { - "foreground": "#fede5d" - } - }, - { - "name": "Go interfaces", - "scope": [ - "source.go storage.type", - "source.go keyword.struct.go", - "source.go keyword.interface.go" - ], - "settings": { - "foreground": "#72f1b8" - } - }, - { - "name": "Go Constants e.g. nil, string format (%s, %d, etc.)", - "scope": [ - "source.go constant.language.go", - "source.go constant.other.placeholder.go", - "source.go variable" - ], - "settings": { - "foreground": "#2EE2FA" - } - }, - { - "name": "Markdown links and image paths", - "scope": ["markup.underline.link.markdown", "markup.inline.raw.string.markdown"], - "settings": { - "foreground": "#72f1b8", - "fontStyle": "italic" - } - }, - { - "name": "Markdown links and image paths", - "scope": ["string.other.link.title.markdown"], - "settings": { - "foreground": "#fede5d" - } - }, - { - "name": "Markdown headings", - "scope": ["markup.heading.markdown", "entity.name.section.markdown"], - "settings": { - "foreground": "#ff7edb", - "fontStyle": "bold" - } - }, - { - "name": "Markdown italic", - "scope": ["markup.italic.markdown"], - "settings": { - "foreground": "#2EE2FA", - "fontStyle": "italic" - } - }, - { - "name": "Markdown bold", - "scope": ["markup.bold.markdown"], - "settings": { - "foreground": "#2EE2FA", - "fontStyle": "bold" - } - }, - { - "name": "Markdown quotes", - "scope": ["punctuation.definition.quote.begin.markdown", "markup.quote.markdown"], - "settings": { - "foreground": "#72f1b8" - } - }, - { - "name": "Basic source colours", - "scope": ["source.dart", "source.python", "source.scala"], - "settings": { - "foreground": "#ff7edbff" - } - }, - { - "name": "Dart strings", - "scope": ["string.interpolated.single.dart"], - "settings": { - "foreground": "#f97e72" - } - }, - { - "name": "Dart variable params", - "scope": ["variable.parameter.dart"], - "settings": { - "foreground": "#72f1b8" - } - }, - { - "name": "Dart numerics", - "scope": ["constant.numeric.dart"], - "settings": { - "foreground": "#2EE2FA" - } - }, - { - "name": "Scala variable params", - "scope": ["variable.parameter.scala"], - "settings": { - "foreground": "#2EE2FA" - } - }, - { - "name": "Scala", - "scope": ["meta.template.expression.scala"], - "settings": { - "foreground": "#72f1b8" - } - } - ] + "name": "SynthWave 84", + "type": "dark", + "semanticHighlighting": true, + "colors": { + "focusBorder": "#1f212b", + "foreground": "#ffffff", + "widget.shadow": "#2a2139", + "selection.background": "#ffffff20", + "errorForeground": "#fe4450", + "textLink.activeForeground": "#ff7edb", + "textLink.foreground": "#f97e72", + "button.background": "#614D85", + "dropdown.background": "#232530", + "dropdown.listBackground": "#2a2139", + "input.background": "#2a2139", + "inputOption.activeBorder": "#ff7edb99", + "inputValidation.errorBackground": "#fe445080", + "inputValidation.errorBorder": "#fe445000", + "scrollbar.shadow": "#2a2139", + "scrollbarSlider.activeBackground": "#9d8bca20", + "scrollbarSlider.background": "#9d8bca30", + "scrollbarSlider.hoverBackground": "#9d8bca50", + "badge.foreground": "#ffffff", + "badge.background": "#2a2139", + "progressBar.background": "#f97e72", + "list.activeSelectionBackground": "#ffffff20", + "list.activeSelectionForeground": "#ffffff", + "list.dropBackground": "#34294f66", + "list.focusBackground": "#ffffff20", + "list.focusForeground": "#ffffff", + "list.highlightForeground": "#f97e72", + "list.hoverBackground": "#37294d99", + "list.hoverForeground": "#ffffff", + "list.inactiveSelectionBackground": "#ffffff20", + "list.inactiveSelectionForeground": "#ffffff", + "list.inactiveFocusBackground": "#2a213999", + "list.errorForeground": "#fe4450E6", + "list.warningForeground": "#72f1b8bb", + "activityBar.background": "#171520", + "activityBar.dropBackground": "#34294f66", + "activityBar.foreground": "#ffffffCC", + "activityBarBadge.background": "#f97e72", + "activityBarBadge.foreground": "#2a2139", + "sideBar.background": "#241b2f", + "sideBar.foreground": "#ffffff99", + "sideBar.dropBackground": "#34294f4c", + "sideBarSectionHeader.background": "#241b2f", + "sideBarSectionHeader.foreground": "#ffffffca", + "menu.background": "#463465", + "editorGroup.border": "#495495", + "editorGroup.dropBackground": "#4954954a", + "editorGroupHeader.tabsBackground": "#241b2f", + "tab.border": "#241b2f00", + "tab.activeBorder": "#880088", + "tab.inactiveBackground": "#262335", + "editor.background": "#262335", + "editorLineNumber.foreground": "#ffffff73", + "editorLineNumber.activeForeground": "#ffffffcc", + "editorCursor.background": "#241b2f", + "editorCursor.foreground": "#f97e72", + "editor.selectionBackground": "#ffffff20", + "editor.selectionHighlightBackground": "#ffffff20", + "editor.wordHighlightBackground": "#34294f88", + "editor.wordHighlightStrongBackground": "#34294f88", + "editor.findMatchBackground": "#D18616bb", + "editor.findMatchHighlightBackground": "#D1861655", + "editor.findRangeHighlightBackground": "#34294f1a", + "editor.hoverHighlightBackground": "#463564", + "editor.lineHighlightBorder": "#7059AB66", + "editor.rangeHighlightBackground": "#49549539", + "editorIndentGuide.background": "#444251", + "editorIndentGuide.activeBackground": "#A148AB80", + "editorRuler.foreground": "#A148AB80", + "editorCodeLens.foreground": "#ffffff7c", + "editorBracketMatch.background": "#34294f66", + "editorBracketMatch.border": "#495495", + "editorOverviewRuler.border": "#34294fb3", + "editorOverviewRuler.findMatchForeground": "#D1861699", + "editorOverviewRuler.modifiedForeground": "#b893ce99", + "editorOverviewRuler.addedForeground": "#09f7a099", + "editorOverviewRuler.deletedForeground": "#fe445099", + "editorOverviewRuler.errorForeground": "#fe4450dd", + "editorOverviewRuler.warningForeground": "#72f1b8cc", + "editorError.foreground": "#fe4450", + "editorWarning.foreground": "#72f1b8cc", + "editorGutter.modifiedBackground": "#b893ce8f", + "editorGutter.addedBackground": "#206d4bd6", + "editorGutter.deletedBackground": "#fa2e46a4", + "diffEditor.insertedTextBackground": "#0beb9935", + "diffEditor.removedTextBackground": "#fe445035", + "editorWidget.background": "#171520DC", + "editorWidget.border": "#ffffff22", + "editorWidget.resizeBorder": "#ffffff44", + "editorSuggestWidget.highlightForeground": "#f97e72", + "editorSuggestWidget.selectedBackground": "#ffffff36", + "peekView.border": "#495495", + "peekViewEditor.background": "#232530", + "peekViewEditor.matchHighlightBackground": "#D18616bb", + "peekViewResult.background": "#232530", + "peekViewResult.matchHighlightBackground": "#D1861655", + "peekViewResult.selectionBackground": "#2a213980", + "peekViewTitle.background": "#232530", + "panelTitle.activeBorder": "#f97e72", + "statusBar.background": "#241b2f", + "statusBar.foreground": "#ffffff80", + "statusBar.debuggingBackground": "#f97e72", + "statusBar.debuggingForeground": "#08080f", + "statusBar.noFolderBackground": "#241b2f", + "statusBarItem.prominentBackground": "#2a2139", + "statusBarItem.prominentHoverBackground": "#34294f", + "titleBar.activeBackground": "#241b2f", + "titleBar.inactiveBackground": "#241b2f", + "extensionButton.prominentBackground": "#f97e72", + "extensionButton.prominentHoverBackground": "#ff7edb", + "pickerGroup.foreground": "#f97e72ea", + "terminal.foreground": "#ffffff", + "terminal.ansiBlue": "#03edf9", + "terminal.ansiBrightBlue": "#03edf9", + "terminal.ansiBrightCyan": "#03edf9", + "terminal.ansiBrightGreen": "#72f1b8", + "terminal.ansiBrightMagenta": "#ff7edb", + "terminal.ansiBrightRed": "#fe4450", + "terminal.ansiBrightYellow": "#fede5d", + "terminal.ansiCyan": "#03edf9", + "terminal.ansiGreen": "#72f1b8", + "terminal.ansiMagenta": "#ff7edb", + "terminal.ansiRed": "#fe4450", + "terminal.ansiYellow": "#f3e70f", + "terminal.selectionBackground": "#ffffff20", + "terminalCursor.background": "#ffffff", + "terminalCursor.foreground": "#03edf9", + "debugToolBar.background": "#463465", + "walkThrough.embeddedEditorBackground": "#232530", + "gitDecoration.modifiedResourceForeground": "#b893ceee", + "gitDecoration.deletedResourceForeground": "#fe4450", + "gitDecoration.addedResourceForeground": "#72f1b8cc", + "gitDecoration.untrackedResourceForeground": "#72f1b8", + "gitDecoration.ignoredResourceForeground": "#ffffff59", + "minimapGutter.addedBackground": "#09f7a099", + "minimapGutter.modifiedBackground": "#b893ce", + "minimapGutter.deletedBackground": "#fe4450", + "breadcrumbPicker.background": "#232530" + }, + "tokenColors": [ + { + "name": "Comment", + "scope": [ + "comment", + "string.quoted.docstring.multi.python", + "string.quoted.docstring.multi.python punctuation.definition.string.begin.python", + "string.quoted.docstring.multi.python punctuation.definition.string.end.python" + ], + "settings": { + "foreground": "#848bbd", + "fontStyle": "italic" + } + }, + { + "name": "String", + "scope": [ + "string.quoted", + "string.template", + "punctuation.definition.string" + ], + "settings": { + "foreground": "#ff8b39" + } + }, + { + "name": "Punctuation within templates", + "scope": "string.template meta.embedded.line", + "settings": { + "foreground": "#b6b1b1" + } + }, + { + "name": "Variable", + "scope": ["variable", "entity.name.variable"], + "settings": { + "foreground": "#ff7edb" + } + }, + { + "name": "Language variable", + "scope": "variable.language", + "settings": { + "foreground": "#fe4450", + "fontStyle": "bold" + } + }, + { + "name": "Parameter", + "scope": "variable.parameter", + "settings": { + "fontStyle": "italic" + } + }, + { + "name": "Storage (declaration or modifier keyword)", + "scope": ["storage.type", "storage.modifier"], + "settings": { + "foreground": "#fede5d" + } + }, + { + "name": "Constant", + "scope": "constant", + "settings": { + "foreground": "#f97e72" + } + }, + { + "name": "Regex", + "scope": "string.regexp", + "settings": { + "foreground": "#f97e72" + } + }, + { + "name": "Number", + "scope": "constant.numeric", + "settings": { + "foreground": "#f97e72" + } + }, + { + "name": "Language constant (boolean, null)", + "scope": "constant.language", + "settings": { + "foreground": "#f97e72" + } + }, + { + "name": "Character escape", + "scope": "constant.character.escape", + "settings": { + "foreground": "#36f9f6" + } + }, + { + "name": "Entity", + "scope": "entity.name", + "settings": { + "foreground": "#fe4450" + } + }, + { + "name": "HTML or XML tag", + "scope": "entity.name.tag", + "settings": { + "foreground": "#72f1b8" + } + }, + { + "name": "HTML or XML tag brackets", + "scope": ["punctuation.definition.tag"], + "settings": { + "foreground": "#36f9f6" + } + }, + { + "name": "Tag attribute", + "scope": "entity.other.attribute-name", + "settings": { + "foreground": "#fede5d" + } + }, + { + "name": "Tag attribute HTML", + "scope": "entity.other.attribute-name.html", + "settings": { + "foreground": "#fede5d", + "fontStyle": "italic" + } + }, + { + "name": "Class", + "scope": ["entity.name.type", "meta.attribute.class.html"], + "settings": { + "foreground": "#fe4450" + } + }, + { + "name": "Inherited class", + "scope": "entity.other.inherited-class", + "settings": { + "foreground": "#D50" + } + }, + { + "name": "Function", + "scope": ["entity.name.function", "variable.function"], + "settings": { + "foreground": "#36f9f6" + } + }, + { + "name": "JS Export", + "scope": ["keyword.control.export.js", "keyword.control.import.js"], + "settings": { + "foreground": "#72f1b8" + } + }, + { + "name": "JS Numerics", + "scope": ["constant.numeric.decimal.js"], + "settings": { + "foreground": "#2EE2FA" + } + }, + { + "name": "Keyword", + "scope": "keyword", + "settings": { + "foreground": "#fede5d" + } + }, + { + "name": "Control keyword", + "scope": "keyword.control", + "settings": { + "foreground": "#fede5d" + } + }, + { + "name": "Operator", + "scope": "keyword.operator", + "settings": { + "foreground": "#fede5d" + } + }, + { + "name": "Special operator", + "scope": [ + "keyword.operator.new", + "keyword.operator.expression", + "keyword.operator.logical" + ], + "settings": { + "foreground": "#fede5d" + } + }, + { + "name": "Unit", + "scope": "keyword.other.unit", + "settings": { + "foreground": "#f97e72" + } + }, + { + "name": "Support", + "scope": "support", + "settings": { + "foreground": "#fe4450" + } + }, + { + "name": "Support function", + "scope": "support.function", + "settings": { + "foreground": "#36f9f6" + } + }, + { + "name": "Support variable", + "scope": "support.variable", + "settings": { + "foreground": "#ff7edb" + } + }, + { + "name": "Object literal key / property", + "scope": ["meta.object-literal.key", "support.type.property-name"], + "settings": { + "foreground": "#ff7edb" + } + }, + { + "name": "Key-value separator", + "scope": "punctuation.separator.key-value", + "settings": { + "foreground": "#b6b1b1" + } + }, + { + "name": "Embedded punctuation", + "scope": "punctuation.section.embedded", + "settings": { + "foreground": "#fede5d" + } + }, + { + "name": "Template expression", + "scope": [ + "punctuation.definition.template-expression.begin", + "punctuation.definition.template-expression.end" + ], + "settings": { + "foreground": "#72f1b8" + } + }, + { + "name": "CSS property", + "scope": [ + "support.type.property-name.css", + "support.type.property-name.json" + ], + "settings": { + "foreground": "#72f1b8" + } + }, + { + "name": "JS Switch control", + "scope": "switch-block.expr.js", + "settings": { + "foreground": "#72f1b8" + } + }, + { + "name": "JS object path", + "scope": "variable.other.constant.property.js, variable.other.property.js", + "settings": { + "foreground": "#2ee2fa" + } + }, + { + "name": "Color", + "scope": "constant.other.color", + "settings": { + "foreground": "#f97e72" + } + }, + { + "name": "Font names", + "scope": "support.constant.font-name", + "settings": { + "foreground": "#f97e72" + } + }, + { + "name": "CSS #id", + "scope": "entity.other.attribute-name.id", + "settings": { + "foreground": "#36f9f6" + } + }, + { + "name": "Pseudo CSS", + "scope": [ + "entity.other.attribute-name.pseudo-element", + "entity.other.attribute-name.pseudo-class" + ], + "settings": { + "foreground": "#D50" + } + }, + { + "name": "CSS support functions (rgb)", + "scope": "support.function.misc.css", + "settings": { + "foreground": "#fe4450" + } + }, + { + "name": "Markup heading", + "scope": ["markup.heading", "entity.name.section"], + "settings": { + "foreground": "#ff7edb" + } + }, + { + "name": "Markup text", + "scope": ["text.html", "keyword.operator.assignment"], + "settings": { + "foreground": "#ffffffee" + } + }, + { + "name": "Markup quote", + "scope": "markup.quote", + "settings": { + "foreground": "#b6b1b1cc", + "fontStyle": "italic" + } + }, + { + "name": "Markup list", + "scope": "beginning.punctuation.definition.list", + "settings": { + "foreground": "#ff7edb" + } + }, + { + "name": "Markup link", + "scope": "markup.underline.link", + "settings": { + "foreground": "#D50" + } + }, + { + "name": "Markup link description", + "scope": "string.other.link.description", + "settings": { + "foreground": "#f97e72" + } + }, + { + "name": "Python function call", + "scope": "meta.function-call.generic.python", + "settings": { + "foreground": "#36f9f6" + } + }, + { + "name": "Python variable params", + "scope": "variable.parameter.function-call.python", + "settings": { + "foreground": "#72f1b8" + } + }, + { + "name": "C# storage type", + "scope": "storage.type.cs", + "settings": { + "foreground": "#fe4450" + } + }, + { + "name": "C# local variable", + "scope": "entity.name.variable.local.cs", + "settings": { + "foreground": "#ff7edb" + } + }, + { + "name": "C# properties and fields", + "scope": [ + "entity.name.variable.field.cs", + "entity.name.variable.property.cs" + ], + "settings": { + "foreground": "#ff7edb" + } + }, + { + "name": "C placeholder", + "scope": "constant.other.placeholder.c", + "settings": { + "foreground": "#72f1b8", + "fontStyle": "italic" + } + }, + { + "name": "C preprocessors", + "scope": [ + "keyword.control.directive.include.c", + "keyword.control.directive.define.c" + ], + "settings": { + "foreground": "#72f1b8" + } + }, + { + "name": "C storage modifier", + "scope": "storage.modifier.c", + "settings": { + "foreground": "#fe4450" + } + }, + { + "name": "C++ operators", + "scope": "source.cpp keyword.operator", + "settings": { + "foreground": "#fede5d" + } + }, + { + "name": "C++ placeholder", + "scope": "constant.other.placeholder.cpp", + "settings": { + "foreground": "#72f1b8", + "fontStyle": "italic" + } + }, + { + "name": "C++ include", + "scope": [ + "keyword.control.directive.include.cpp", + "keyword.control.directive.define.cpp" + ], + "settings": { + "foreground": "#72f1b8" + } + }, + { + "name": "C++ constant modifier", + "scope": "storage.modifier.specifier.const.cpp", + "settings": { + "foreground": "#fe4450" + } + }, + { + "name": "Elixir Classes", + "scope": [ + "source.elixir support.type.elixir", + "source.elixir meta.module.elixir entity.name.class.elixir" + ], + "settings": { + "foreground": "#36f9f6" + } + }, + { + "name": "Elixir Functions", + "scope": "source.elixir entity.name.function", + "settings": { + "foreground": "#72f1b8" + } + }, + { + "name": "Elixir Constants", + "scope": [ + "source.elixir constant.other.symbol.elixir", + "source.elixir constant.other.keywords.elixir" + ], + "settings": { + "foreground": "#36f9f6" + } + }, + { + "name": "Elixir String Punctuation", + "scope": "source.elixir punctuation.definition.string", + "settings": { + "foreground": "#72f1b8" + } + }, + { + "name": "Elixir", + "scope": [ + "source.elixir variable.other.readwrite.module.elixir", + "source.elixir variable.other.readwrite.module.elixir punctuation.definition.variable.elixir" + ], + "settings": { + "foreground": "#72f1b8" + } + }, + { + "name": "Elixir Binary Punctuation", + "scope": "source.elixir .punctuation.binary.elixir", + "settings": { + "foreground": "#ff7edb", + "fontStyle": "italic" + } + }, + { + "name": "Clojure Globals", + "scope": ["entity.global.clojure"], + "settings": { + "foreground": "#36f9f6", + "fontStyle": "bold" + } + }, + { + "name": "Clojure Storage", + "scope": ["storage.control.clojure"], + "settings": { + "foreground": "#36f9f6", + "fontStyle": "italic" + } + }, + { + "name": "Clojure Metadata", + "scope": ["meta.metadata.simple.clojure", "meta.metadata.map.clojure"], + "settings": { + "foreground": "#fe4450", + "fontStyle": "italic" + } + }, + { + "name": "Clojure Macros, Quoted", + "scope": ["meta.quoted-expression.clojure"], + "settings": { + "fontStyle": "italic" + } + }, + { + "name": "Clojure Symbols", + "scope": ["meta.symbol.clojure"], + "settings": { + "foreground": "#ff7edbff" + } + }, + { + "name": "Go basic", + "scope": "source.go", + "settings": { + "foreground": "#ff7edbff" + } + }, + { + "name": "Go Function Calls", + "scope": "source.go meta.function-call.go", + "settings": { + "foreground": "#36f9f6" + } + }, + { + "name": "Go Keywords", + "scope": [ + "source.go keyword.package.go", + "source.go keyword.import.go", + "source.go keyword.function.go", + "source.go keyword.type.go", + "source.go keyword.const.go", + "source.go keyword.var.go", + "source.go keyword.map.go", + "source.go keyword.channel.go", + "source.go keyword.control.go" + ], + "settings": { + "foreground": "#fede5d" + } + }, + { + "name": "Go interfaces", + "scope": [ + "source.go storage.type", + "source.go keyword.struct.go", + "source.go keyword.interface.go" + ], + "settings": { + "foreground": "#72f1b8" + } + }, + { + "name": "Go Constants e.g. nil, string format (%s, %d, etc.)", + "scope": [ + "source.go constant.language.go", + "source.go constant.other.placeholder.go", + "source.go variable" + ], + "settings": { + "foreground": "#2EE2FA" + } + }, + { + "name": "Markdown links and image paths", + "scope": [ + "markup.underline.link.markdown", + "markup.inline.raw.string.markdown" + ], + "settings": { + "foreground": "#72f1b8", + "fontStyle": "italic" + } + }, + { + "name": "Markdown links and image paths", + "scope": ["string.other.link.title.markdown"], + "settings": { + "foreground": "#fede5d" + } + }, + { + "name": "Markdown headings", + "scope": ["markup.heading.markdown", "entity.name.section.markdown"], + "settings": { + "foreground": "#ff7edb", + "fontStyle": "bold" + } + }, + { + "name": "Markdown italic", + "scope": ["markup.italic.markdown"], + "settings": { + "foreground": "#2EE2FA", + "fontStyle": "italic" + } + }, + { + "name": "Markdown bold", + "scope": ["markup.bold.markdown"], + "settings": { + "foreground": "#2EE2FA", + "fontStyle": "bold" + } + }, + { + "name": "Markdown quotes", + "scope": [ + "punctuation.definition.quote.begin.markdown", + "markup.quote.markdown" + ], + "settings": { + "foreground": "#72f1b8" + } + }, + { + "name": "Basic source colours", + "scope": ["source.dart", "source.python", "source.scala"], + "settings": { + "foreground": "#ff7edbff" + } + }, + { + "name": "Dart strings", + "scope": ["string.interpolated.single.dart"], + "settings": { + "foreground": "#f97e72" + } + }, + { + "name": "Dart variable params", + "scope": ["variable.parameter.dart"], + "settings": { + "foreground": "#72f1b8" + } + }, + { + "name": "Dart numerics", + "scope": ["constant.numeric.dart"], + "settings": { + "foreground": "#2EE2FA" + } + }, + { + "name": "Scala variable params", + "scope": ["variable.parameter.scala"], + "settings": { + "foreground": "#2EE2FA" + } + }, + { + "name": "Scala", + "scope": ["meta.template.expression.scala"], + "settings": { + "foreground": "#72f1b8" + } + } + ] } diff --git a/crates/command_palette2/Cargo.toml b/crates/command_palette2/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..bcc0099c2086f03fac0f13cd6541071fe91fb8f8 --- /dev/null +++ b/crates/command_palette2/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "command_palette2" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +path = "src/command_palette.rs" +doctest = false + +[dependencies] +collections = { path = "../collections" } +editor = { package = "editor2", path = "../editor2" } +fuzzy = { package = "fuzzy2", path = "../fuzzy2" } +gpui = { package = "gpui2", path = "../gpui2" } +picker = { package = "picker2", path = "../picker2" } +project = { package = "project2", path = "../project2" } +settings = { package = "settings2", path = "../settings2" } +ui = { package = "ui2", path = "../ui2" } +util = { path = "../util" } +theme = { package = "theme2", path = "../theme2" } +workspace = { package="workspace2", path = "../workspace2" } +zed_actions = { package = "zed_actions2", path = "../zed_actions2" } +anyhow.workspace = true +serde.workspace = true +[dev-dependencies] +gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] } +editor = { package = "editor2", path = "../editor2", features = ["test-support"] } +language = { package="language2", path = "../language2", features = ["test-support"] } +project = { package="project2", path = "../project2", features = ["test-support"] } +serde_json.workspace = true +workspace = { package="workspace2", path = "../workspace2", features = ["test-support"] } +ctor.workspace = true +env_logger.workspace = true diff --git a/crates/command_palette2/src/command_palette.rs b/crates/command_palette2/src/command_palette.rs new file mode 100644 index 0000000000000000000000000000000000000000..bf9f9fa94b9691405f4ff9f682e6abcf9c6b0b18 --- /dev/null +++ b/crates/command_palette2/src/command_palette.rs @@ -0,0 +1,532 @@ +use collections::{CommandPaletteFilter, HashMap}; +use fuzzy::{StringMatch, StringMatchCandidate}; +use gpui::{ + actions, div, Action, AppContext, Component, Div, EventEmitter, FocusHandle, Keystroke, + ParentElement, Render, StatelessInteractive, Styled, View, ViewContext, VisualContext, + WeakView, WindowContext, +}; +use picker::{Picker, PickerDelegate}; +use std::cmp::{self, Reverse}; +use theme::ActiveTheme; +use ui::{v_stack, Label, StyledExt}; +use util::{ + channel::{parse_zed_link, ReleaseChannel, RELEASE_CHANNEL}, + ResultExt, +}; +use workspace::{Modal, ModalEvent, Workspace}; +use zed_actions::OpenZedURL; + +actions!(Toggle); + +pub fn init(cx: &mut AppContext) { + cx.set_global(HitCounts::default()); + cx.observe_new_views(CommandPalette::register).detach(); +} + +pub struct CommandPalette { + picker: View>, +} + +impl CommandPalette { + fn register(workspace: &mut Workspace, _: &mut ViewContext) { + workspace.register_action(|workspace, _: &Toggle, cx| { + let Some(previous_focus_handle) = cx.focused() else { + return; + }; + workspace.toggle_modal(cx, move |cx| CommandPalette::new(previous_focus_handle, cx)); + }); + } + + fn new(previous_focus_handle: FocusHandle, cx: &mut ViewContext) -> Self { + let filter = cx.try_global::(); + + let commands = cx + .available_actions() + .into_iter() + .filter_map(|action| { + let name = action.name(); + let namespace = name.split("::").next().unwrap_or("malformed action name"); + if filter.is_some_and(|f| f.filtered_namespaces.contains(namespace)) { + return None; + } + + Some(Command { + name: humanize_action_name(&name), + action, + keystrokes: vec![], // todo!() + }) + }) + .collect(); + + let delegate = + CommandPaletteDelegate::new(cx.view().downgrade(), commands, previous_focus_handle); + + let picker = cx.build_view(|cx| Picker::new(delegate, cx)); + Self { picker } + } +} + +impl EventEmitter for CommandPalette {} +impl Modal for CommandPalette { + fn focus(&self, cx: &mut WindowContext) { + self.picker.update(cx, |picker, cx| picker.focus(cx)); + } +} + +impl Render for CommandPalette { + type Element = Div; + + fn render(&mut self, _cx: &mut ViewContext) -> Self::Element { + v_stack().w_96().child(self.picker.clone()) + } +} + +pub type CommandPaletteInterceptor = + Box Option>; + +pub struct CommandInterceptResult { + pub action: Box, + pub string: String, + pub positions: Vec, +} + +pub struct CommandPaletteDelegate { + command_palette: WeakView, + commands: Vec, + matches: Vec, + selected_ix: usize, + previous_focus_handle: FocusHandle, +} + +struct Command { + name: String, + action: Box, + keystrokes: Vec, +} + +impl Clone for Command { + fn clone(&self) -> Self { + Self { + name: self.name.clone(), + action: self.action.boxed_clone(), + keystrokes: self.keystrokes.clone(), + } + } +} +/// Hit count for each command in the palette. +/// We only account for commands triggered directly via command palette and not by e.g. keystrokes because +/// if an user already knows a keystroke for a command, they are unlikely to use a command palette to look for it. +#[derive(Default)] +struct HitCounts(HashMap); + +impl CommandPaletteDelegate { + fn new( + command_palette: WeakView, + commands: Vec, + previous_focus_handle: FocusHandle, + ) -> Self { + Self { + command_palette, + matches: commands + .iter() + .enumerate() + .map(|(i, command)| StringMatch { + candidate_id: i, + string: command.name.clone(), + positions: Vec::new(), + score: 0.0, + }) + .collect(), + commands, + selected_ix: 0, + previous_focus_handle, + } + } +} + +impl PickerDelegate for CommandPaletteDelegate { + type ListItem = Div>; + + fn match_count(&self) -> usize { + self.matches.len() + } + + fn selected_index(&self) -> usize { + self.selected_ix + } + + fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext>) { + self.selected_ix = ix; + } + + fn update_matches( + &mut self, + query: String, + cx: &mut ViewContext>, + ) -> gpui::Task<()> { + let mut commands = self.commands.clone(); + + cx.spawn(move |picker, mut cx| async move { + cx.read_global::(|hit_counts, _| { + commands.sort_by_key(|action| { + ( + Reverse(hit_counts.0.get(&action.name).cloned()), + action.name.clone(), + ) + }); + }) + .ok(); + + let candidates = commands + .iter() + .enumerate() + .map(|(ix, command)| StringMatchCandidate { + id: ix, + string: command.name.to_string(), + char_bag: command.name.chars().collect(), + }) + .collect::>(); + let mut matches = if query.is_empty() { + candidates + .into_iter() + .enumerate() + .map(|(index, candidate)| StringMatch { + candidate_id: index, + string: candidate.string, + positions: Vec::new(), + score: 0.0, + }) + .collect() + } else { + fuzzy::match_strings( + &candidates, + &query, + true, + 10000, + &Default::default(), + cx.background_executor().clone(), + ) + .await + }; + + let mut intercept_result = cx + .try_read_global(|interceptor: &CommandPaletteInterceptor, cx| { + (interceptor)(&query, cx) + }) + .flatten(); + + if *RELEASE_CHANNEL == ReleaseChannel::Dev { + if parse_zed_link(&query).is_some() { + intercept_result = Some(CommandInterceptResult { + action: OpenZedURL { url: query.clone() }.boxed_clone(), + string: query.clone(), + positions: vec![], + }) + } + } + if let Some(CommandInterceptResult { + action, + string, + positions, + }) = intercept_result + { + if let Some(idx) = matches + .iter() + .position(|m| commands[m.candidate_id].action.type_id() == action.type_id()) + { + matches.remove(idx); + } + commands.push(Command { + name: string.clone(), + action, + keystrokes: vec![], + }); + matches.insert( + 0, + StringMatch { + candidate_id: commands.len() - 1, + string, + positions, + score: 0.0, + }, + ) + } + picker + .update(&mut cx, |picker, _| { + let delegate = &mut picker.delegate; + delegate.commands = commands; + delegate.matches = matches; + if delegate.matches.is_empty() { + delegate.selected_ix = 0; + } else { + delegate.selected_ix = + cmp::min(delegate.selected_ix, delegate.matches.len() - 1); + } + }) + .log_err(); + }) + } + + fn dismissed(&mut self, cx: &mut ViewContext>) { + self.command_palette + .update(cx, |_, cx| cx.emit(ModalEvent::Dismissed)) + .log_err(); + } + + fn confirm(&mut self, _: bool, cx: &mut ViewContext>) { + if self.matches.is_empty() { + self.dismissed(cx); + return; + } + let action_ix = self.matches[self.selected_ix].candidate_id; + let command = self.commands.swap_remove(action_ix); + cx.update_global(|hit_counts: &mut HitCounts, _| { + *hit_counts.0.entry(command.name).or_default() += 1; + }); + let action = command.action; + cx.focus(&self.previous_focus_handle); + cx.dispatch_action(action); + self.dismissed(cx); + } + + fn render_match( + &self, + ix: usize, + selected: bool, + cx: &mut ViewContext>, + ) -> Self::ListItem { + let colors = cx.theme().colors(); + let Some(command) = self + .matches + .get(ix) + .and_then(|m| self.commands.get(m.candidate_id)) + else { + return div(); + }; + + div() + .px_1() + .text_color(colors.text) + .text_ui() + .bg(colors.ghost_element_background) + .rounded_md() + .when(selected, |this| this.bg(colors.ghost_element_selected)) + .hover(|this| this.bg(colors.ghost_element_hover)) + .child(Label::new(command.name.clone())) + } + + // fn render_match( + // &self, + // ix: usize, + // mouse_state: &mut MouseState, + // selected: bool, + // cx: &gpui::AppContext, + // ) -> AnyElement> { + // let mat = &self.matches[ix]; + // let command = &self.actions[mat.candidate_id]; + // let theme = theme::current(cx); + // let style = theme.picker.item.in_state(selected).style_for(mouse_state); + // let key_style = &theme.command_palette.key.in_state(selected); + // let keystroke_spacing = theme.command_palette.keystroke_spacing; + + // Flex::row() + // .with_child( + // Label::new(mat.string.clone(), style.label.clone()) + // .with_highlights(mat.positions.clone()), + // ) + // .with_children(command.keystrokes.iter().map(|keystroke| { + // Flex::row() + // .with_children( + // [ + // (keystroke.ctrl, "^"), + // (keystroke.alt, "⌥"), + // (keystroke.cmd, "⌘"), + // (keystroke.shift, "⇧"), + // ] + // .into_iter() + // .filter_map(|(modifier, label)| { + // if modifier { + // Some( + // Label::new(label, key_style.label.clone()) + // .contained() + // .with_style(key_style.container), + // ) + // } else { + // None + // } + // }), + // ) + // .with_child( + // Label::new(keystroke.key.clone(), key_style.label.clone()) + // .contained() + // .with_style(key_style.container), + // ) + // .contained() + // .with_margin_left(keystroke_spacing) + // .flex_float() + // })) + // .contained() + // .with_style(style.container) + // .into_any() + // } +} + +fn humanize_action_name(name: &str) -> String { + let capacity = name.len() + name.chars().filter(|c| c.is_uppercase()).count(); + let mut result = String::with_capacity(capacity); + for char in name.chars() { + if char == ':' { + if result.ends_with(':') { + result.push(' '); + } else { + result.push(':'); + } + } else if char == '_' { + result.push(' '); + } else if char.is_uppercase() { + if !result.ends_with(' ') { + result.push(' '); + } + result.extend(char.to_lowercase()); + } else { + result.push(char); + } + } + result +} + +impl std::fmt::Debug for Command { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Command") + .field("name", &self.name) + .field("keystrokes", &self.keystrokes) + .finish() + } +} + +// #[cfg(test)] +// mod tests { +// use std::sync::Arc; + +// use super::*; +// use editor::Editor; +// use gpui::{executor::Deterministic, TestAppContext}; +// use project::Project; +// use workspace::{AppState, Workspace}; + +// #[test] +// fn test_humanize_action_name() { +// assert_eq!( +// humanize_action_name("editor::GoToDefinition"), +// "editor: go to definition" +// ); +// assert_eq!( +// humanize_action_name("editor::Backspace"), +// "editor: backspace" +// ); +// assert_eq!( +// humanize_action_name("go_to_line::Deploy"), +// "go to line: deploy" +// ); +// } + +// #[gpui::test] +// async fn test_command_palette(deterministic: Arc, cx: &mut TestAppContext) { +// let app_state = init_test(cx); + +// let project = Project::test(app_state.fs.clone(), [], cx).await; +// let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); +// let workspace = window.root(cx); +// let editor = window.add_view(cx, |cx| { +// let mut editor = Editor::single_line(None, cx); +// editor.set_text("abc", cx); +// editor +// }); + +// workspace.update(cx, |workspace, cx| { +// cx.focus(&editor); +// workspace.add_item(Box::new(editor.clone()), cx) +// }); + +// workspace.update(cx, |workspace, cx| { +// toggle_command_palette(workspace, &Toggle, cx); +// }); + +// let palette = workspace.read_with(cx, |workspace, _| { +// workspace.modal::().unwrap() +// }); + +// palette +// .update(cx, |palette, cx| { +// // Fill up palette's command list by running an empty query; +// // we only need it to subsequently assert that the palette is initially +// // sorted by command's name. +// palette.delegate_mut().update_matches("".to_string(), cx) +// }) +// .await; + +// palette.update(cx, |palette, _| { +// let is_sorted = +// |actions: &[Command]| actions.windows(2).all(|pair| pair[0].name <= pair[1].name); +// assert!(is_sorted(&palette.delegate().actions)); +// }); + +// palette +// .update(cx, |palette, cx| { +// palette +// .delegate_mut() +// .update_matches("bcksp".to_string(), cx) +// }) +// .await; + +// palette.update(cx, |palette, cx| { +// assert_eq!(palette.delegate().matches[0].string, "editor: backspace"); +// palette.confirm(&Default::default(), cx); +// }); +// deterministic.run_until_parked(); +// editor.read_with(cx, |editor, cx| { +// assert_eq!(editor.text(cx), "ab"); +// }); + +// // Add namespace filter, and redeploy the palette +// cx.update(|cx| { +// cx.update_default_global::(|filter, _| { +// filter.filtered_namespaces.insert("editor"); +// }) +// }); + +// workspace.update(cx, |workspace, cx| { +// toggle_command_palette(workspace, &Toggle, cx); +// }); + +// // Assert editor command not present +// let palette = workspace.read_with(cx, |workspace, _| { +// workspace.modal::().unwrap() +// }); + +// palette +// .update(cx, |palette, cx| { +// palette +// .delegate_mut() +// .update_matches("bcksp".to_string(), cx) +// }) +// .await; + +// palette.update(cx, |palette, _| { +// assert!(palette.delegate().matches.is_empty()) +// }); +// } + +// fn init_test(cx: &mut TestAppContext) -> Arc { +// cx.update(|cx| { +// let app_state = AppState::test(cx); +// theme::init(cx); +// language::init(cx); +// editor::init(cx); +// workspace::init(app_state.clone(), cx); +// init(cx); +// Project::init_settings(cx); +// app_state +// }) +// } +// } diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index e794771434a0981fe5af2efa1ae19d9db7b0f523..4748f63e5d37e29174777ea326ce8158787acd09 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -171,10 +171,9 @@ impl ProjectDiagnosticsEditor { .entry(*language_server_id) .or_default() .insert(path.clone()); - let no_multiselections = this.editor.update(cx, |editor, cx| { - editor.selections.all::(cx).len() <= 1 - }); - if no_multiselections && !this.is_dirty(cx) { + if this.editor.read(cx).selections.all::(cx).is_empty() + && !this.is_dirty(cx) + { this.update_excerpts(Some(*language_server_id), cx); } } diff --git a/crates/editor/src/editor_settings.rs b/crates/editor/src/editor_settings.rs index 75f8b800f93757bec3bcbbf01b68226880267d57..b885e065a1a722564314d157a097a97a990ac971 100644 --- a/crates/editor/src/editor_settings.rs +++ b/crates/editor/src/editor_settings.rs @@ -2,7 +2,7 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use settings::Setting; -#[derive(Deserialize)] +#[derive(Clone, Deserialize)] pub struct EditorSettings { pub cursor_blink: bool, pub hover_popover_enabled: bool, @@ -11,6 +11,15 @@ pub struct EditorSettings { pub use_on_type_format: bool, pub scrollbar: Scrollbar, pub relative_line_numbers: bool, + pub seed_search_query_from_cursor: SeedQuerySetting, +} + +#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum SeedQuerySetting { + Always, + Selection, + Never, } #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] @@ -38,6 +47,7 @@ pub struct EditorSettingsContent { pub use_on_type_format: Option, pub scrollbar: Option, pub relative_line_numbers: Option, + pub seed_search_query_from_cursor: Option, } #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 1b922848e061b8b162fc5c1f25babf5c3ce96c0e..4c45904c502285cb217b7e61193ef8fdc95dc99b 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -1,7 +1,7 @@ use crate::{ - display_map::ToDisplayPoint, link_go_to_definition::hide_link_definition, - movement::surrounding_word, persistence::DB, scroll::ScrollAnchor, Anchor, Autoscroll, Editor, - Event, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, NavigationData, ToPoint as _, + editor_settings::SeedQuerySetting, link_go_to_definition::hide_link_definition, + persistence::DB, scroll::ScrollAnchor, Anchor, Autoscroll, Editor, EditorSettings, Event, + ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, NavigationData, ToPoint as _, }; use anyhow::{Context, Result}; use collections::HashSet; @@ -13,8 +13,8 @@ use gpui::{ ViewHandle, WeakViewHandle, }; use language::{ - proto::serialize_anchor as serialize_text_anchor, Bias, Buffer, OffsetRangeExt, Point, - SelectionGoal, + proto::serialize_anchor as serialize_text_anchor, Bias, Buffer, CharKind, OffsetRangeExt, + Point, SelectionGoal, }; use project::{search::SearchQuery, FormatTrigger, Item as _, Project, ProjectPath}; use rpc::proto::{self, update_view, PeerId}; @@ -937,24 +937,28 @@ impl SearchableItem for Editor { } fn query_suggestion(&mut self, cx: &mut ViewContext) -> String { - let display_map = self.snapshot(cx).display_snapshot; + let setting = settings::get::(cx).seed_search_query_from_cursor; + let snapshot = &self.snapshot(cx).buffer_snapshot; let selection = self.selections.newest::(cx); - if selection.start == selection.end { - let point = selection.start.to_display_point(&display_map); - let range = surrounding_word(&display_map, point); - let range = range.start.to_offset(&display_map, Bias::Left) - ..range.end.to_offset(&display_map, Bias::Right); - let text: String = display_map.buffer_snapshot.text_for_range(range).collect(); - if text.trim().is_empty() { + + match setting { + SeedQuerySetting::Never => String::new(), + SeedQuerySetting::Selection | SeedQuerySetting::Always if !selection.is_empty() => { + snapshot + .text_for_range(selection.start..selection.end) + .collect() + } + SeedQuerySetting::Selection => String::new(), + SeedQuerySetting::Always => { + let (range, kind) = snapshot.surrounding_word(selection.start); + if kind == Some(CharKind::Word) { + let text: String = snapshot.text_for_range(range).collect(); + if !text.trim().is_empty() { + return text; + } + } String::new() - } else { - text } - } else { - display_map - .buffer_snapshot - .text_for_range(selection.start..selection.end) - .collect() } } diff --git a/crates/editor2/Cargo.toml b/crates/editor2/Cargo.toml index b897110966709ecef886caf8adef36f1973cc4bd..e45c33d91759f2e7d21283e45d1011cdd26abcb8 100644 --- a/crates/editor2/Cargo.toml +++ b/crates/editor2/Cargo.toml @@ -44,6 +44,7 @@ snippet = { path = "../snippet" } sum_tree = { path = "../sum_tree" } text = { package="text2", path = "../text2" } theme = { package="theme2", path = "../theme2" } +ui = { package = "ui2", path = "../ui2" } util = { path = "../util" } sqlez = { path = "../sqlez" } workspace = { package = "workspace2", path = "../workspace2" } diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index 51cd549923827ce21f9220e153f1f983bf5aa95d..b1f0d2678683fc95bbce9d0feac29896e9d23630 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -39,10 +39,12 @@ use futures::FutureExt; use fuzzy::{StringMatch, StringMatchCandidate}; use git::diff_hunk_to_display; use gpui::{ - action, actions, point, px, relative, rems, size, AnyElement, AppContext, BackgroundExecutor, - Bounds, ClipboardItem, Context, DispatchContext, EventEmitter, FocusHandle, FontFeatures, - FontStyle, FontWeight, HighlightStyle, Hsla, InputHandler, Model, Pixels, Render, Subscription, - Task, TextStyle, View, ViewContext, VisualContext, WeakView, WindowContext, + action, actions, div, point, px, relative, rems, size, uniform_list, AnyElement, AppContext, + AsyncWindowContext, BackgroundExecutor, Bounds, ClipboardItem, Component, Context, + DispatchContext, EventEmitter, FocusHandle, FontFeatures, FontStyle, FontWeight, + HighlightStyle, Hsla, InputHandler, Model, MouseButton, ParentElement, Pixels, Render, + StatelessInteractive, Styled, Subscription, Task, TextStyle, UniformListScrollHandle, View, + ViewContext, VisualContext, WeakView, WindowContext, }; use highlight_matching_bracket::refresh_matching_bracket_highlights; use hover_popover::{hide_hover, HoverState}; @@ -67,7 +69,7 @@ pub use multi_buffer::{ }; use ordered_float::OrderedFloat; use parking_lot::{Mutex, RwLock}; -use project::{FormatTrigger, Location, Project}; +use project::{FormatTrigger, Location, Project, ProjectTransaction}; use rand::prelude::*; use rpc::proto::*; use scroll::{ @@ -95,6 +97,7 @@ use text::{OffsetUtf16, Rope}; use theme::{ ActiveTheme, DiagnosticStyle, PlayerColor, SyntaxTheme, Theme, ThemeColors, ThemeSettings, }; +use ui::{IconButton, StyledExt}; use util::{post_inc, RangeExt, ResultExt, TryFutureExt}; use workspace::{ item::ItemEvent, searchable::SearchEvent, ItemNavHistory, SplitDirection, ViewId, Workspace, @@ -384,26 +387,6 @@ actions!( UnfoldLines, ); -// impl_actions!( -// editor, -// [ -// SelectNext, -// SelectPrevious, -// SelectAllMatches, -// SelectToBeginningOfLine, -// SelectToEndOfLine, -// ToggleCodeActions, -// MovePageUp, -// MovePageDown, -// ConfirmCompletion, -// ConfirmCodeAction, -// ToggleComments, -// FoldAt, -// UnfoldAt, -// GutterHover -// ] -// ); - enum DocumentHighlightRead {} enum DocumentHighlightWrite {} enum InputComposition {} @@ -919,15 +902,14 @@ impl ContextMenu { fn render( &self, cursor_position: DisplayPoint, - style: EditorStyle, + style: &EditorStyle, workspace: Option>, cx: &mut ViewContext, ) -> (DisplayPoint, AnyElement) { - todo!() - // match self { - // ContextMenu::Completions(menu) => (cursor_position, menu.render(style, workspace, cx)), - // ContextMenu::CodeActions(menu) => menu.render(cursor_position, style, cx), - // } + match self { + ContextMenu::Completions(menu) => (cursor_position, menu.render(style, workspace, cx)), + ContextMenu::CodeActions(menu) => menu.render(cursor_position, style, cx), + } } } @@ -940,29 +922,13 @@ struct CompletionsMenu { match_candidates: Arc<[StringMatchCandidate]>, matches: Arc<[StringMatch]>, selected_item: usize, - list: UniformListState, -} - -// todo!(this is fake) -#[derive(Clone, Default)] -struct UniformListState; - -// todo!(this is fake) -impl UniformListState { - pub fn scroll_to(&mut self, target: ScrollTarget) {} -} - -// todo!(this is somewhat fake) -#[derive(Debug)] -pub enum ScrollTarget { - Show(usize), - Center(usize), + scroll_handle: UniformListScrollHandle, } impl CompletionsMenu { fn select_first(&mut self, project: Option<&Model>, cx: &mut ViewContext) { self.selected_item = 0; - self.list.scroll_to(ScrollTarget::Show(self.selected_item)); + self.scroll_handle.scroll_to_item(self.selected_item); self.attempt_resolve_selected_completion_documentation(project, cx); cx.notify(); } @@ -973,7 +939,7 @@ impl CompletionsMenu { } else { self.selected_item = self.matches.len() - 1; } - self.list.scroll_to(ScrollTarget::Show(self.selected_item)); + self.scroll_handle.scroll_to_item(self.selected_item); self.attempt_resolve_selected_completion_documentation(project, cx); cx.notify(); } @@ -984,14 +950,14 @@ impl CompletionsMenu { } else { self.selected_item = 0; } - self.list.scroll_to(ScrollTarget::Show(self.selected_item)); + self.scroll_handle.scroll_to_item(self.selected_item); self.attempt_resolve_selected_completion_documentation(project, cx); cx.notify(); } fn select_last(&mut self, project: Option<&Model>, cx: &mut ViewContext) { self.selected_item = self.matches.len() - 1; - self.list.scroll_to(ScrollTarget::Show(self.selected_item)); + self.scroll_handle.scroll_to_item(self.selected_item); self.attempt_resolve_selected_completion_documentation(project, cx); cx.notify(); } @@ -1252,13 +1218,13 @@ impl CompletionsMenu { fn render( &self, - style: EditorStyle, + style: &EditorStyle, workspace: Option>, cx: &mut ViewContext, - ) { + ) -> AnyElement { todo!("old implementation below") } - // ) -> AnyElement { + // enum CompletionTag {} // let settings = EditorSettings>(cx); @@ -1527,14 +1493,14 @@ struct CodeActionsMenu { actions: Arc<[CodeAction]>, buffer: Model, selected_item: usize, - list: UniformListState, + scroll_handle: UniformListScrollHandle, deployed_from_indicator: bool, } impl CodeActionsMenu { fn select_first(&mut self, cx: &mut ViewContext) { self.selected_item = 0; - self.list.scroll_to(ScrollTarget::Show(self.selected_item)); + self.scroll_handle.scroll_to_item(self.selected_item); cx.notify() } @@ -1544,7 +1510,7 @@ impl CodeActionsMenu { } else { self.selected_item = self.actions.len() - 1; } - self.list.scroll_to(ScrollTarget::Show(self.selected_item)); + self.scroll_handle.scroll_to_item(self.selected_item); cx.notify(); } @@ -1554,13 +1520,13 @@ impl CodeActionsMenu { } else { self.selected_item = 0; } - self.list.scroll_to(ScrollTarget::Show(self.selected_item)); + self.scroll_handle.scroll_to_item(self.selected_item); cx.notify(); } fn select_last(&mut self, cx: &mut ViewContext) { self.selected_item = self.actions.len() - 1; - self.list.scroll_to(ScrollTarget::Show(self.selected_item)); + self.scroll_handle.scroll_to_item(self.selected_item); cx.notify() } @@ -1571,83 +1537,70 @@ impl CodeActionsMenu { fn render( &self, mut cursor_position: DisplayPoint, - style: EditorStyle, + style: &EditorStyle, cx: &mut ViewContext, ) -> (DisplayPoint, AnyElement) { - todo!("old version below") - } - // enum ActionTag {} + let actions = self.actions.clone(); + let selected_item = self.selected_item; + let element = uniform_list( + "code_actions_menu", + self.actions.len(), + move |editor, range, cx| { + actions[range.clone()] + .iter() + .enumerate() + .map(|(ix, action)| { + let item_ix = range.start + ix; + let selected = selected_item == item_ix; + let colors = cx.theme().colors(); + div() + .px_2() + .text_ui() + .text_color(colors.text) + .when(selected, |style| { + style + .bg(colors.element_active) + .text_color(colors.text_accent) + }) + .hover(|style| { + style + .bg(colors.element_hover) + .text_color(colors.text_accent) + }) + .on_mouse_down(MouseButton::Left, move |editor: &mut Editor, _, cx| { + cx.stop_propagation(); + editor + .confirm_code_action( + &ConfirmCodeAction { + item_ix: Some(item_ix), + }, + cx, + ) + .map(|task| task.detach_and_log_err(cx)); + }) + .child(action.lsp_action.title.clone()) + }) + .collect() + }, + ) + .elevation_1(cx) + .px_2() + .py_1() + .with_width_from_item( + self.actions + .iter() + .enumerate() + .max_by_key(|(_, action)| action.lsp_action.title.chars().count()) + .map(|(ix, _)| ix), + ) + .render(); - // let container_style = style.autocomplete.container; - // let actions = self.actions.clone(); - // let selected_item = self.selected_item; - // let element = UniformList::new( - // self.list.clone(), - // actions.len(), - // cx, - // move |_, range, items, cx| { - // let start_ix = range.start; - // for (ix, action) in actions[range].iter().enumerate() { - // let item_ix = start_ix + ix; - // items.push( - // MouseEventHandler::new::(item_ix, cx, |state, _| { - // let item_style = if item_ix == selected_item { - // style.autocomplete.selected_item - // } else if state.hovered() { - // style.autocomplete.hovered_item - // } else { - // style.autocomplete.item - // }; - - // Text::new(action.lsp_action.title.clone(), style.text.clone()) - // .with_soft_wrap(false) - // .contained() - // .with_style(item_style) - // }) - // .with_cursor_style(CursorStyle::PointingHand) - // .on_down(MouseButton::Left, move |_, this, cx| { - // let workspace = this - // .workspace - // .as_ref() - // .and_then(|(workspace, _)| workspace.upgrade(cx)); - // cx.window_context().defer(move |cx| { - // if let Some(workspace) = workspace { - // workspace.update(cx, |workspace, cx| { - // if let Some(task) = Editor::confirm_code_action( - // workspace, - // &ConfirmCodeAction { - // item_ix: Some(item_ix), - // }, - // cx, - // ) { - // task.detach_and_log_err(cx); - // } - // }); - // } - // }); - // }) - // .into_any(), - // ); - // } - // }, - // ) - // .with_width_from_item( - // self.actions - // .iter() - // .enumerate() - // .max_by_key(|(_, action)| action.lsp_action.title.chars().count()) - // .map(|(ix, _)| ix), - // ) - // .contained() - // .with_style(container_style) - // .into_any(); - - // if self.deployed_from_indicator { - // *cursor_position.column_mut() = 0; - // } + if self.deployed_from_indicator { + *cursor_position.column_mut() = 0; + } - // (cursor_position, element) - // } + (cursor_position, element) + } } pub struct CopilotState { @@ -3660,7 +3613,7 @@ impl Editor { completions: Arc::new(RwLock::new(completions.into())), matches: Vec::new().into(), selected_item: 0, - list: Default::default(), + scroll_handle: UniformListScrollHandle::new(), }; menu.filter(query.as_deref(), cx.background_executor().clone()) .await; @@ -3846,156 +3799,161 @@ impl Editor { // })) // } - // pub fn toggle_code_actions(&mut self, action: &ToggleCodeActions, cx: &mut ViewContext) { - // let mut context_menu = self.context_menu.write(); - // if matches!(context_menu.as_ref(), Some(ContextMenu::CodeActions(_))) { - // *context_menu = None; - // cx.notify(); - // return; - // } - // drop(context_menu); - - // let deployed_from_indicator = action.deployed_from_indicator; - // let mut task = self.code_actions_task.take(); - // cx.spawn(|this, mut cx| async move { - // while let Some(prev_task) = task { - // prev_task.await; - // task = this.update(&mut cx, |this, _| this.code_actions_task.take())?; - // } + pub fn toggle_code_actions(&mut self, action: &ToggleCodeActions, cx: &mut ViewContext) { + let mut context_menu = self.context_menu.write(); + if matches!(context_menu.as_ref(), Some(ContextMenu::CodeActions(_))) { + *context_menu = None; + cx.notify(); + return; + } + drop(context_menu); - // this.update(&mut cx, |this, cx| { - // if this.focused { - // if let Some((buffer, actions)) = this.available_code_actions.clone() { - // this.completion_tasks.clear(); - // this.discard_copilot_suggestion(cx); - // *this.context_menu.write() = - // Some(ContextMenu::CodeActions(CodeActionsMenu { - // buffer, - // actions, - // selected_item: Default::default(), - // list: Default::default(), - // deployed_from_indicator, - // })); - // } - // } - // })?; + let deployed_from_indicator = action.deployed_from_indicator; + let mut task = self.code_actions_task.take(); + cx.spawn(|this, mut cx| async move { + while let Some(prev_task) = task { + prev_task.await; + task = this.update(&mut cx, |this, _| this.code_actions_task.take())?; + } - // Ok::<_, anyhow::Error>(()) - // }) - // .detach_and_log_err(cx); - // } + this.update(&mut cx, |this, cx| { + if this.focus_handle.is_focused(cx) { + if let Some((buffer, actions)) = this.available_code_actions.clone() { + this.completion_tasks.clear(); + this.discard_copilot_suggestion(cx); + *this.context_menu.write() = + Some(ContextMenu::CodeActions(CodeActionsMenu { + buffer, + actions, + selected_item: Default::default(), + scroll_handle: UniformListScrollHandle::default(), + deployed_from_indicator, + })); + cx.notify(); + } + } + })?; - // pub fn confirm_code_action( - // workspace: &mut Workspace, - // action: &ConfirmCodeAction, - // cx: &mut ViewContext, - // ) -> Option>> { - // let editor = workspace.active_item(cx)?.act_as::(cx)?; - // let actions_menu = if let ContextMenu::CodeActions(menu) = - // editor.update(cx, |editor, cx| editor.hide_context_menu(cx))? - // { - // menu - // } else { - // return None; - // }; - // let action_ix = action.item_ix.unwrap_or(actions_menu.selected_item); - // let action = actions_menu.actions.get(action_ix)?.clone(); - // let title = action.lsp_action.title.clone(); - // let buffer = actions_menu.buffer; + Ok::<_, anyhow::Error>(()) + }) + .detach_and_log_err(cx); + } - // let apply_code_actions = workspace.project().clone().update(cx, |project, cx| { - // project.apply_code_action(buffer, action, true, cx) - // }); - // let editor = editor.downgrade(); - // Some(cx.spawn(|workspace, cx| async move { - // let project_transaction = apply_code_actions.await?; - // Self::open_project_transaction(&editor, workspace, project_transaction, title, cx).await - // })) - // } + pub fn confirm_code_action( + &mut self, + action: &ConfirmCodeAction, + cx: &mut ViewContext, + ) -> Option>> { + let actions_menu = if let ContextMenu::CodeActions(menu) = self.hide_context_menu(cx)? { + menu + } else { + return None; + }; + let action_ix = action.item_ix.unwrap_or(actions_menu.selected_item); + let action = actions_menu.actions.get(action_ix)?.clone(); + let title = action.lsp_action.title.clone(); + let buffer = actions_menu.buffer; + let workspace = self.workspace()?; - // async fn open_project_transaction( - // this: &WeakViewHandle Result<()> { - // let replica_id = this.read_with(&cx, |this, cx| this.replica_id(cx))?; - - // let mut entries = transaction.0.into_iter().collect::>(); - // entries.sort_unstable_by_key(|(buffer, _)| { - // buffer.read_with(&cx, |buffer, _| buffer.file().map(|f| f.path().clone())) - // }); + let apply_code_actions = workspace + .read(cx) + .project() + .clone() + .update(cx, |project, cx| { + project.apply_code_action(buffer, action, true, cx) + }); + let workspace = workspace.downgrade(); + Some(cx.spawn(|editor, cx| async move { + let project_transaction = apply_code_actions.await?; + Self::open_project_transaction(&editor, workspace, project_transaction, title, cx).await + })) + } - // // If the project transaction's edits are all contained within this editor, then - // // avoid opening a new editor to display them. + async fn open_project_transaction( + this: &WeakView, + workspace: WeakView, + transaction: ProjectTransaction, + title: String, + mut cx: AsyncWindowContext, + ) -> Result<()> { + let replica_id = this.update(&mut cx, |this, cx| this.replica_id(cx))?; - // if let Some((buffer, transaction)) = entries.first() { - // if entries.len() == 1 { - // let excerpt = this.read_with(&cx, |editor, cx| { - // editor - // .buffer() - // .read(cx) - // .excerpt_containing(editor.selections.newest_anchor().head(), cx) - // })?; - // if let Some((_, excerpted_buffer, excerpt_range)) = excerpt { - // if excerpted_buffer == *buffer { - // let all_edits_within_excerpt = buffer.read_with(&cx, |buffer, _| { - // let excerpt_range = excerpt_range.to_offset(buffer); - // buffer - // .edited_ranges_for_transaction::(transaction) - // .all(|range| { - // excerpt_range.start <= range.start - // && excerpt_range.end >= range.end - // }) - // }); - - // if all_edits_within_excerpt { - // return Ok(()); - // } - // } - // } - // } - // } else { - // return Ok(()); - // } + let mut entries = transaction.0.into_iter().collect::>(); + cx.update(|_, cx| { + entries.sort_unstable_by_key(|(buffer, _)| { + buffer.read(cx).file().map(|f| f.path().clone()) + }); + })?; + + // If the project transaction's edits are all contained within this editor, then + // avoid opening a new editor to display them. + + if let Some((buffer, transaction)) = entries.first() { + if entries.len() == 1 { + let excerpt = this.update(&mut cx, |editor, cx| { + editor + .buffer() + .read(cx) + .excerpt_containing(editor.selections.newest_anchor().head(), cx) + })?; + if let Some((_, excerpted_buffer, excerpt_range)) = excerpt { + if excerpted_buffer == *buffer { + let all_edits_within_excerpt = buffer.read_with(&cx, |buffer, _| { + let excerpt_range = excerpt_range.to_offset(buffer); + buffer + .edited_ranges_for_transaction::(transaction) + .all(|range| { + excerpt_range.start <= range.start + && excerpt_range.end >= range.end + }) + })?; - // let mut ranges_to_highlight = Vec::new(); - // let excerpt_buffer = cx.build_model(|cx| { - // let mut multibuffer = MultiBuffer::new(replica_id).with_title(title); - // for (buffer_handle, transaction) in &entries { - // let buffer = buffer_handle.read(cx); - // ranges_to_highlight.extend( - // multibuffer.push_excerpts_with_context_lines( - // buffer_handle.clone(), - // buffer - // .edited_ranges_for_transaction::(transaction) - // .collect(), - // 1, - // cx, - // ), - // ); - // } - // multibuffer.push_transaction(entries.iter().map(|(b, t)| (b, t)), cx); - // multibuffer - // }); + if all_edits_within_excerpt { + return Ok(()); + } + } + } + } + } else { + return Ok(()); + } - // workspace.update(&mut cx, |workspace, cx| { - // let project = workspace.project().clone(); - // let editor = - // cx.add_view(|cx| Editor::for_multibuffer(excerpt_buffer, Some(project), cx)); - // workspace.add_item(Box::new(editor.clone()), cx); - // editor.update(cx, |editor, cx| { - // editor.highlight_background::( - // ranges_to_highlight, - // |theme| theme.editor.highlighted_line_background, - // cx, - // ); - // }); - // })?; + let mut ranges_to_highlight = Vec::new(); + let excerpt_buffer = cx.build_model(|cx| { + let mut multibuffer = MultiBuffer::new(replica_id).with_title(title); + for (buffer_handle, transaction) in &entries { + let buffer = buffer_handle.read(cx); + ranges_to_highlight.extend( + multibuffer.push_excerpts_with_context_lines( + buffer_handle.clone(), + buffer + .edited_ranges_for_transaction::(transaction) + .collect(), + 1, + cx, + ), + ); + } + multibuffer.push_transaction(entries.iter().map(|(b, t)| (b, t)), cx); + multibuffer + })?; + + workspace.update(&mut cx, |workspace, cx| { + let project = workspace.project().clone(); + let editor = + cx.build_view(|cx| Editor::for_multibuffer(excerpt_buffer, Some(project), cx)); + workspace.add_item(Box::new(editor.clone()), cx); + editor.update(cx, |editor, cx| { + editor.highlight_background::( + ranges_to_highlight, + |theme| theme.editor_highlighted_line_background, + cx, + ); + }); + })?; - // Ok(()) - // } + Ok(()) + } fn refresh_code_actions(&mut self, cx: &mut ViewContext) -> Option<()> { let project = self.project.clone()?; @@ -4390,41 +4348,29 @@ impl Editor { self.discard_copilot_suggestion(cx); } - // pub fn render_code_actions_indicator( - // &self, - // style: &EditorStyle, - // is_active: bool, - // cx: &mut ViewContext, - // ) -> Option> { - // if self.available_code_actions.is_some() { - // enum CodeActions {} - // Some( - // MouseEventHandler::new::(0, cx, |state, _| { - // Svg::new("icons/bolt.svg").with_color( - // style - // .code_actions - // .indicator - // .in_state(is_active) - // .style_for(state) - // .color, - // ) - // }) - // .with_cursor_style(CursorStyle::PointingHand) - // .with_padding(Padding::uniform(3.)) - // .on_down(MouseButton::Left, |_, this, cx| { - // this.toggle_code_actions( - // &ToggleCodeActions { - // deployed_from_indicator: true, - // }, - // cx, - // ); - // }) - // .into_any(), - // ) - // } else { - // None - // } - // } + pub fn render_code_actions_indicator( + &self, + style: &EditorStyle, + is_active: bool, + cx: &mut ViewContext, + ) -> Option> { + if self.available_code_actions.is_some() { + Some( + IconButton::new("code_actions_indicator", ui::Icon::Bolt) + .on_click(|editor: &mut Editor, cx| { + editor.toggle_code_actions( + &ToggleCodeActions { + deployed_from_indicator: true, + }, + cx, + ); + }) + .render(), + ) + } else { + None + } + } // pub fn render_fold_indicators( // &self, @@ -4491,29 +4437,27 @@ impl Editor { // } pub fn context_menu_visible(&self) -> bool { - false - // todo!("context menu") - // self.context_menu - // .read() - // .as_ref() - // .map_or(false, |menu| menu.visible()) + self.context_menu + .read() + .as_ref() + .map_or(false, |menu| menu.visible()) } - // pub fn render_context_menu( - // &self, - // cursor_position: DisplayPoint, - // style: EditorStyle, - // cx: &mut ViewContext, - // ) -> Option<(DisplayPoint, AnyElement)> { - // self.context_menu.read().as_ref().map(|menu| { - // menu.render( - // cursor_position, - // style, - // self.workspace.as_ref().map(|(w, _)| w.clone()), - // cx, - // ) - // }) - // } + pub fn render_context_menu( + &self, + cursor_position: DisplayPoint, + style: &EditorStyle, + cx: &mut ViewContext, + ) -> Option<(DisplayPoint, AnyElement)> { + self.context_menu.read().as_ref().map(|menu| { + menu.render( + cursor_position, + style, + self.workspace.as_ref().map(|(w, _)| w.clone()), + cx, + ) + }) + } fn hide_context_menu(&mut self, cx: &mut ViewContext) -> Option { cx.notify(); @@ -5954,29 +5898,29 @@ impl Editor { }); } - // pub fn context_menu_first(&mut self, _: &ContextMenuFirst, cx: &mut ViewContext) { - // if let Some(context_menu) = self.context_menu.write().as_mut() { - // context_menu.select_first(self.project.as_ref(), cx); - // } - // } + pub fn context_menu_first(&mut self, _: &ContextMenuFirst, cx: &mut ViewContext) { + if let Some(context_menu) = self.context_menu.write().as_mut() { + context_menu.select_first(self.project.as_ref(), cx); + } + } - // pub fn context_menu_prev(&mut self, _: &ContextMenuPrev, cx: &mut ViewContext) { - // if let Some(context_menu) = self.context_menu.write().as_mut() { - // context_menu.select_prev(self.project.as_ref(), cx); - // } - // } + pub fn context_menu_prev(&mut self, _: &ContextMenuPrev, cx: &mut ViewContext) { + if let Some(context_menu) = self.context_menu.write().as_mut() { + context_menu.select_prev(self.project.as_ref(), cx); + } + } - // pub fn context_menu_next(&mut self, _: &ContextMenuNext, cx: &mut ViewContext) { - // if let Some(context_menu) = self.context_menu.write().as_mut() { - // context_menu.select_next(self.project.as_ref(), cx); - // } - // } + pub fn context_menu_next(&mut self, _: &ContextMenuNext, cx: &mut ViewContext) { + if let Some(context_menu) = self.context_menu.write().as_mut() { + context_menu.select_next(self.project.as_ref(), cx); + } + } - // pub fn context_menu_last(&mut self, _: &ContextMenuLast, cx: &mut ViewContext) { - // if let Some(context_menu) = self.context_menu.write().as_mut() { - // context_menu.select_last(self.project.as_ref(), cx); - // } - // } + pub fn context_menu_last(&mut self, _: &ContextMenuLast, cx: &mut ViewContext) { + if let Some(context_menu) = self.context_menu.write().as_mut() { + context_menu.select_last(self.project.as_ref(), cx); + } + } pub fn move_to_previous_word_start( &mut self, diff --git a/crates/editor2/src/editor_settings.rs b/crates/editor2/src/editor_settings.rs index 45c797598f51c4bde2f0008d02ac62d55accb3c7..fd7e2feea31a07f0b35184087508c76e652b9c26 100644 --- a/crates/editor2/src/editor_settings.rs +++ b/crates/editor2/src/editor_settings.rs @@ -11,6 +11,15 @@ pub struct EditorSettings { pub use_on_type_format: bool, pub scrollbar: Scrollbar, pub relative_line_numbers: bool, + pub seed_search_query_from_cursor: SeedQuerySetting, +} + +#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum SeedQuerySetting { + Always, + Selection, + Never, } #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] @@ -38,6 +47,7 @@ pub struct EditorSettingsContent { pub use_on_type_format: Option, pub scrollbar: Option, pub relative_line_numbers: Option, + pub seed_search_query_from_cursor: Option, } #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] diff --git a/crates/editor2/src/element.rs b/crates/editor2/src/element.rs index 3e77a66936443aa872fc6f196fb71257bfede82f..67fcbaa4ba11acf260ffad4c29f7f9c217d1f727 100644 --- a/crates/editor2/src/element.rs +++ b/crates/editor2/src/element.rs @@ -15,7 +15,7 @@ use crate::{ use anyhow::Result; use collections::{BTreeMap, HashMap}; use gpui::{ - black, hsla, point, px, relative, size, transparent_black, Action, AnyElement, + black, hsla, point, px, relative, size, transparent_black, Action, AnyElement, AvailableSpace, BorrowAppContext, BorrowWindow, Bounds, ContentMask, Corners, DispatchContext, DispatchPhase, Edges, Element, ElementId, ElementInputHandler, Entity, FocusHandle, GlobalElementId, Hsla, InputHandler, KeyDownEvent, KeyListener, KeyMatch, Line, LineLayout, Modifiers, MouseButton, @@ -447,7 +447,7 @@ impl EditorElement { fn paint_gutter( &mut self, bounds: Bounds, - layout: &LayoutState, + layout: &mut LayoutState, editor: &mut Editor, cx: &mut ViewContext, ) { @@ -495,14 +495,21 @@ impl EditorElement { // } // } - // todo!("code actions indicator") - // if let Some((row, indicator)) = layout.code_actions_indicator.as_mut() { - // let mut x = 0.; - // let mut y = *row as f32 * line_height - scroll_top; - // x += ((layout.gutter_padding + layout.gutter_margin) - indicator.size().x) / 2.; - // y += (line_height - indicator.size().y) / 2.; - // indicator.paint(bounds.origin + point(x, y), visible_bounds, editor, cx); - // } + if let Some(indicator) = layout.code_actions_indicator.as_mut() { + let available_space = size( + AvailableSpace::MinContent, + AvailableSpace::Definite(line_height), + ); + let indicator_size = indicator.element.measure(available_space, editor, cx); + let mut x = Pixels::ZERO; + let mut y = indicator.row as f32 * line_height - scroll_top; + // Center indicator. + x += ((layout.gutter_padding + layout.gutter_margin) - indicator_size.width) / 2.; + y += (line_height - indicator_size.height) / 2.; + indicator + .element + .draw(bounds.origin + point(x, y), available_space, editor, cx); + } } fn paint_diff_hunks( @@ -596,7 +603,7 @@ impl EditorElement { fn paint_text( &mut self, bounds: Bounds, - layout: &LayoutState, + layout: &mut LayoutState, editor: &mut Editor, cx: &mut ViewContext, ) { @@ -787,48 +794,46 @@ impl EditorElement { ) } - cx.stack(0, |cx| { + cx.with_z_index(0, |cx| { for cursor in cursors { cursor.paint(content_origin, cx); } }); - // cx.scene().push_layer(Some(bounds)); - - // cx.scene().pop_layer(); - // if let Some((position, context_menu)) = layout.context_menu.as_mut() { - // cx.scene().push_stacking_context(None, None); - // let cursor_row_layout = - // &layout.position_map.line_layouts[(position.row() - start_row) as usize].line; - // let x = cursor_row_layout.x_for_index(position.column() as usize) - scroll_left; - // let y = (position.row() + 1) as f32 * layout.position_map.line_height - scroll_top; - // let mut list_origin = content_origin + point(x, y); - // let list_width = context_menu.size().x; - // let list_height = context_menu.size().y; - - // // Snap the right edge of the list to the right edge of the window if - // // its horizontal bounds overflow. - // if list_origin.x + list_width > cx.window_size().x { - // list_origin.set_x((cx.window_size().x - list_width).max(0.)); - // } - - // if list_origin.y + list_height > bounds.max_y { - // list_origin - // .set_y(list_origin.y - layout.position_map.line_height - list_height); - // } + if let Some((position, context_menu)) = layout.context_menu.as_mut() { + cx.with_z_index(1, |cx| { + let line_height = self.style.text.line_height_in_pixels(cx.rem_size()); + let available_space = size( + AvailableSpace::MinContent, + AvailableSpace::Definite( + (12. * line_height).min((bounds.size.height - line_height) / 2.), + ), + ); + let context_menu_size = context_menu.measure(available_space, editor, cx); + + let cursor_row_layout = &layout.position_map.line_layouts + [(position.row() - start_row) as usize] + .line; + let x = cursor_row_layout.x_for_index(position.column() as usize) - scroll_left; + let y = + (position.row() + 1) as f32 * layout.position_map.line_height - scroll_top; + let mut list_origin = content_origin + point(x, y); + let list_width = context_menu_size.width; + let list_height = context_menu_size.height; + + // Snap the right edge of the list to the right edge of the window if + // its horizontal bounds overflow. + if list_origin.x + list_width > cx.viewport_size().width { + list_origin.x = (cx.viewport_size().width - list_width).max(Pixels::ZERO); + } - // context_menu.paint( - // list_origin, - // Bounds::::from_points( - // gpui::Point::::zero(), - // point(f32::MAX, f32::MAX), - // ), // Let content bleed outside of editor - // editor, - // cx, - // ); + if list_origin.y + list_height > bounds.lower_right().y { + list_origin.y -= layout.position_map.line_height - list_height; + } - // cx.scene().pop_stacking_context(); - // } + context_menu.draw(list_origin, available_space, editor, cx); + }) + } // if let Some((position, hover_popovers)) = layout.hover_popovers.as_mut() { // cx.scene().push_stacking_context(None, None); @@ -1774,26 +1779,28 @@ impl EditorElement { snapshot = editor.snapshot(cx); } - // todo!("context menu") - // let mut context_menu = None; - // let mut code_actions_indicator = None; - // if let Some(newest_selection_head) = newest_selection_head { - // if (start_row..end_row).contains(&newest_selection_head.row()) { - // if editor.context_menu_visible() { - // context_menu = - // editor.render_context_menu(newest_selection_head, style.clone(), cx); - // } + let mut context_menu = None; + let mut code_actions_indicator = None; + if let Some(newest_selection_head) = newest_selection_head { + if (start_row..end_row).contains(&newest_selection_head.row()) { + if editor.context_menu_visible() { + context_menu = + editor.render_context_menu(newest_selection_head, &self.style, cx); + } - // let active = matches!( - // editor.context_menu.read().as_ref(), - // Some(crate::ContextMenu::CodeActions(_)) - // ); + let active = matches!( + editor.context_menu.read().as_ref(), + Some(crate::ContextMenu::CodeActions(_)) + ); - // code_actions_indicator = editor - // .render_code_actions_indicator(&style, active, cx) - // .map(|indicator| (newest_selection_head.row(), indicator)); - // } - // } + code_actions_indicator = editor + .render_code_actions_indicator(&style, active, cx) + .map(|element| CodeActionsIndicator { + row: newest_selection_head.row(), + element, + }); + } + } let visible_rows = start_row..start_row + line_layouts.len() as u32; // todo!("hover") @@ -1831,18 +1838,6 @@ impl EditorElement { // ); // } - // todo!("code actions") - // if let Some((_, indicator)) = code_actions_indicator.as_mut() { - // indicator.layout( - // SizeConstraint::strict_along( - // Axis::Vertical, - // line_height * style.code_actions.vertical_scale, - // ), - // editor, - // cx, - // ); - // } - // todo!("fold indicators") // for fold_indicator in fold_indicators.iter_mut() { // if let Some(indicator) = fold_indicator.as_mut() { @@ -1941,8 +1936,8 @@ impl EditorElement { display_hunks, // blocks, selections, - // context_menu, - // code_actions_indicator, + context_menu, + code_actions_indicator, // fold_indicators, tab_invisible, space_invisible, @@ -2493,7 +2488,7 @@ impl Element for EditorElement { element_state: &mut Self::ElementState, cx: &mut gpui::ViewContext, ) { - let layout = self.compute_layout(editor, cx, bounds); + let mut layout = self.compute_layout(editor, cx, bounds); let gutter_bounds = Bounds { origin: bounds.origin, size: layout.gutter_size, @@ -2503,21 +2498,24 @@ impl Element for EditorElement { size: layout.text_size, }; - cx.with_content_mask(ContentMask { bounds }, |cx| { - self.paint_mouse_listeners( - bounds, - gutter_bounds, - text_bounds, - &layout.position_map, - cx, - ); - self.paint_background(gutter_bounds, text_bounds, &layout, cx); - if layout.gutter_size.width > Pixels::ZERO { - self.paint_gutter(gutter_bounds, &layout, editor, cx); - } - self.paint_text(text_bounds, &layout, editor, cx); - let input_handler = ElementInputHandler::new(bounds, cx); - cx.handle_input(&editor.focus_handle, input_handler); + // We call with_z_index to establish a new stacking context. + cx.with_z_index(0, |cx| { + cx.with_content_mask(ContentMask { bounds }, |cx| { + self.paint_mouse_listeners( + bounds, + gutter_bounds, + text_bounds, + &layout.position_map, + cx, + ); + self.paint_background(gutter_bounds, text_bounds, &layout, cx); + if layout.gutter_size.width > Pixels::ZERO { + self.paint_gutter(gutter_bounds, &mut layout, editor, cx); + } + self.paint_text(text_bounds, &mut layout, editor, cx); + let input_handler = ElementInputHandler::new(bounds, cx); + cx.handle_input(&editor.focus_handle, input_handler); + }); }); } } @@ -3143,14 +3141,19 @@ pub struct LayoutState { show_scrollbars: bool, is_singleton: bool, max_row: u32, - // context_menu: Option<(DisplayPoint, AnyElement)>, - // code_actions_indicator: Option<(u32, AnyElement)>, + context_menu: Option<(DisplayPoint, AnyElement)>, + code_actions_indicator: Option, // hover_popovers: Option<(DisplayPoint, Vec>)>, // fold_indicators: Vec>>, tab_invisible: Line, space_invisible: Line, } +struct CodeActionsIndicator { + row: u32, + element: AnyElement, +} + struct PositionMap { size: Size, line_height: Pixels, @@ -4123,7 +4126,7 @@ fn build_key_listeners( build_action_listener(Editor::unfold_at), build_action_listener(Editor::fold_selected_ranges), build_action_listener(Editor::show_completions), - // build_action_listener(Editor::toggle_code_actions), todo!() + build_action_listener(Editor::toggle_code_actions), // build_action_listener(Editor::open_excerpts), todo!() build_action_listener(Editor::toggle_soft_wrap), build_action_listener(Editor::toggle_inlay_hints), @@ -4139,13 +4142,21 @@ fn build_key_listeners( build_action_listener(Editor::restart_language_server), build_action_listener(Editor::show_character_palette), // build_action_listener(Editor::confirm_completion), todo!() - // build_action_listener(Editor::confirm_code_action), todo!() + build_action_listener(|editor, action, cx| { + editor + .confirm_code_action(action, cx) + .map(|task| task.detach_and_log_err(cx)); + }), // build_action_listener(Editor::rename), todo!() // build_action_listener(Editor::confirm_rename), todo!() // build_action_listener(Editor::find_all_references), todo!() build_action_listener(Editor::next_copilot_suggestion), build_action_listener(Editor::previous_copilot_suggestion), build_action_listener(Editor::copilot_suggest), + build_action_listener(Editor::context_menu_first), + build_action_listener(Editor::context_menu_prev), + build_action_listener(Editor::context_menu_next), + build_action_listener(Editor::context_menu_last), build_key_listener( move |editor, key_down: &KeyDownEvent, dispatch_context, phase, cx| { if phase == DispatchPhase::Bubble { diff --git a/crates/editor2/src/inlay_hint_cache.rs b/crates/editor2/src/inlay_hint_cache.rs index addd3bf3acb74a11b2eaab5cec44fe9a2a68f495..af9febf376b2e74eb9ccac99d84fbe588ece612b 100644 --- a/crates/editor2/src/inlay_hint_cache.rs +++ b/crates/editor2/src/inlay_hint_cache.rs @@ -553,18 +553,17 @@ impl InlayHintCache { let mut resolved_hint = resolved_hint_task.await.context("hint resolve task")?; editor.update(&mut cx, |editor, _| { - todo!() - // if let Some(excerpt_hints) = - // editor.inlay_hint_cache.hints.get(&excerpt_id) - // { - // let mut guard = excerpt_hints.write(); - // if let Some(cached_hint) = guard.hints_by_id.get_mut(&id) { - // if cached_hint.resolve_state == ResolveState::Resolving { - // resolved_hint.resolve_state = ResolveState::Resolved; - // *cached_hint = resolved_hint; - // } - // } - // } + if let Some(excerpt_hints) = + editor.inlay_hint_cache.hints.get(&excerpt_id) + { + let mut guard = excerpt_hints.write(); + if let Some(cached_hint) = guard.hints_by_id.get_mut(&id) { + if cached_hint.resolve_state == ResolveState::Resolving { + resolved_hint.resolve_state = ResolveState::Resolved; + *cached_hint = resolved_hint; + } + } + } })?; } @@ -585,91 +584,89 @@ fn spawn_new_update_tasks( update_cache_version: usize, cx: &mut ViewContext<'_, Editor>, ) { - todo!("old version below"); + let visible_hints = Arc::new(editor.visible_inlay_hints(cx)); + for (excerpt_id, (excerpt_buffer, new_task_buffer_version, excerpt_visible_range)) in + excerpts_to_query + { + if excerpt_visible_range.is_empty() { + continue; + } + let buffer = excerpt_buffer.read(cx); + let buffer_id = buffer.remote_id(); + let buffer_snapshot = buffer.snapshot(); + if buffer_snapshot + .version() + .changed_since(&new_task_buffer_version) + { + continue; + } + + let cached_excerpt_hints = editor.inlay_hint_cache.hints.get(&excerpt_id).cloned(); + if let Some(cached_excerpt_hints) = &cached_excerpt_hints { + let cached_excerpt_hints = cached_excerpt_hints.read(); + let cached_buffer_version = &cached_excerpt_hints.buffer_version; + if cached_excerpt_hints.version > update_cache_version + || cached_buffer_version.changed_since(&new_task_buffer_version) + { + continue; + } + }; + + let (multi_buffer_snapshot, Some(query_ranges)) = + editor.buffer.update(cx, |multi_buffer, cx| { + ( + multi_buffer.snapshot(cx), + determine_query_ranges( + multi_buffer, + excerpt_id, + &excerpt_buffer, + excerpt_visible_range, + cx, + ), + ) + }) + else { + return; + }; + let query = ExcerptQuery { + buffer_id, + excerpt_id, + cache_version: update_cache_version, + invalidate, + reason, + }; + + let new_update_task = |query_ranges| { + new_update_task( + query, + query_ranges, + multi_buffer_snapshot, + buffer_snapshot.clone(), + Arc::clone(&visible_hints), + cached_excerpt_hints, + Arc::clone(&editor.inlay_hint_cache.lsp_request_limiter), + cx, + ) + }; + + match editor.inlay_hint_cache.update_tasks.entry(excerpt_id) { + hash_map::Entry::Occupied(mut o) => { + o.get_mut().update_cached_tasks( + &buffer_snapshot, + query_ranges, + invalidate, + new_update_task, + ); + } + hash_map::Entry::Vacant(v) => { + v.insert(TasksForRanges::new( + query_ranges.clone(), + new_update_task(query_ranges), + )); + } + } + } } -// let visible_hints = Arc::new(editor.visible_inlay_hints(cx)); -// for (excerpt_id, (excerpt_buffer, new_task_buffer_version, excerpt_visible_range)) in -// excerpts_to_query -// { -// if excerpt_visible_range.is_empty() { -// continue; -// } -// let buffer = excerpt_buffer.read(cx); -// let buffer_id = buffer.remote_id(); -// let buffer_snapshot = buffer.snapshot(); -// if buffer_snapshot -// .version() -// .changed_since(&new_task_buffer_version) -// { -// continue; -// } - -// let cached_excerpt_hints = editor.inlay_hint_cache.hints.get(&excerpt_id).cloned(); -// if let Some(cached_excerpt_hints) = &cached_excerpt_hints { -// let cached_excerpt_hints = cached_excerpt_hints.read(); -// let cached_buffer_version = &cached_excerpt_hints.buffer_version; -// if cached_excerpt_hints.version > update_cache_version -// || cached_buffer_version.changed_since(&new_task_buffer_version) -// { -// continue; -// } -// }; - -// let (multi_buffer_snapshot, Some(query_ranges)) = -// editor.buffer.update(cx, |multi_buffer, cx| { -// ( -// multi_buffer.snapshot(cx), -// determine_query_ranges( -// multi_buffer, -// excerpt_id, -// &excerpt_buffer, -// excerpt_visible_range, -// cx, -// ), -// ) -// }) -// else { -// return; -// }; -// let query = ExcerptQuery { -// buffer_id, -// excerpt_id, -// cache_version: update_cache_version, -// invalidate, -// reason, -// }; - -// let new_update_task = |query_ranges| { -// new_update_task( -// query, -// query_ranges, -// multi_buffer_snapshot, -// buffer_snapshot.clone(), -// Arc::clone(&visible_hints), -// cached_excerpt_hints, -// Arc::clone(&editor.inlay_hint_cache.lsp_request_limiter), -// cx, -// ) -// }; - -// match editor.inlay_hint_cache.update_tasks.entry(excerpt_id) { -// hash_map::Entry::Occupied(mut o) => { -// o.get_mut().update_cached_tasks( -// &buffer_snapshot, -// query_ranges, -// invalidate, -// new_update_task, -// ); -// } -// hash_map::Entry::Vacant(v) => { -// v.insert(TasksForRanges::new( -// query_ranges.clone(), -// new_update_task(query_ranges), -// )); -// } -// } -// } -// } #[derive(Debug, Clone)] struct QueryRanges { @@ -765,209 +762,208 @@ fn new_update_task( lsp_request_limiter: Arc, cx: &mut ViewContext<'_, Editor>, ) -> Task<()> { - todo!() - // cx.spawn(|editor, mut cx| async move { - // let closure_cx = cx.clone(); - // let fetch_and_update_hints = |invalidate, range| { - // fetch_and_update_hints( - // editor.clone(), - // multi_buffer_snapshot.clone(), - // buffer_snapshot.clone(), - // Arc::clone(&visible_hints), - // cached_excerpt_hints.as_ref().map(Arc::clone), - // query, - // invalidate, - // range, - // Arc::clone(&lsp_request_limiter), - // closure_cx.clone(), - // ) - // }; - // let visible_range_update_results = future::join_all(query_ranges.visible.into_iter().map( - // |visible_range| async move { - // ( - // visible_range.clone(), - // fetch_and_update_hints(query.invalidate.should_invalidate(), visible_range) - // .await, - // ) - // }, - // )) - // .await; - - // let hint_delay = cx.background().timer(Duration::from_millis( - // INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS, - // )); - - // let mut query_range_failed = |range: &Range, e: anyhow::Error| { - // log::error!("inlay hint update task for range {range:?} failed: {e:#}"); - // editor - // .update(&mut cx, |editor, _| { - // if let Some(task_ranges) = editor - // .inlay_hint_cache - // .update_tasks - // .get_mut(&query.excerpt_id) - // { - // task_ranges.invalidate_range(&buffer_snapshot, &range); - // } - // }) - // .ok() - // }; - - // for (range, result) in visible_range_update_results { - // if let Err(e) = result { - // query_range_failed(&range, e); - // } - // } - - // hint_delay.await; - // let invisible_range_update_results = future::join_all( - // query_ranges - // .before_visible - // .into_iter() - // .chain(query_ranges.after_visible.into_iter()) - // .map(|invisible_range| async move { - // ( - // invisible_range.clone(), - // fetch_and_update_hints(false, invisible_range).await, - // ) - // }), - // ) - // .await; - // for (range, result) in invisible_range_update_results { - // if let Err(e) = result { - // query_range_failed(&range, e); - // } - // } - // }) + cx.spawn(|editor, mut cx| async move { + let closure_cx = cx.clone(); + let fetch_and_update_hints = |invalidate, range| { + fetch_and_update_hints( + editor.clone(), + multi_buffer_snapshot.clone(), + buffer_snapshot.clone(), + Arc::clone(&visible_hints), + cached_excerpt_hints.as_ref().map(Arc::clone), + query, + invalidate, + range, + Arc::clone(&lsp_request_limiter), + closure_cx.clone(), + ) + }; + let visible_range_update_results = future::join_all(query_ranges.visible.into_iter().map( + |visible_range| async move { + ( + visible_range.clone(), + fetch_and_update_hints(query.invalidate.should_invalidate(), visible_range) + .await, + ) + }, + )) + .await; + + let hint_delay = cx.background_executor().timer(Duration::from_millis( + INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS, + )); + + let mut query_range_failed = |range: &Range, e: anyhow::Error| { + log::error!("inlay hint update task for range {range:?} failed: {e:#}"); + editor + .update(&mut cx, |editor, _| { + if let Some(task_ranges) = editor + .inlay_hint_cache + .update_tasks + .get_mut(&query.excerpt_id) + { + task_ranges.invalidate_range(&buffer_snapshot, &range); + } + }) + .ok() + }; + + for (range, result) in visible_range_update_results { + if let Err(e) = result { + query_range_failed(&range, e); + } + } + + hint_delay.await; + let invisible_range_update_results = future::join_all( + query_ranges + .before_visible + .into_iter() + .chain(query_ranges.after_visible.into_iter()) + .map(|invisible_range| async move { + ( + invisible_range.clone(), + fetch_and_update_hints(false, invisible_range).await, + ) + }), + ) + .await; + for (range, result) in invisible_range_update_results { + if let Err(e) = result { + query_range_failed(&range, e); + } + } + }) } -// async fn fetch_and_update_hints( -// editor: gpui::WeakView, -// multi_buffer_snapshot: MultiBufferSnapshot, -// buffer_snapshot: BufferSnapshot, -// visible_hints: Arc>, -// cached_excerpt_hints: Option>>, -// query: ExcerptQuery, -// invalidate: bool, -// fetch_range: Range, -// lsp_request_limiter: Arc, -// mut cx: gpui::AsyncAppContext, -// ) -> anyhow::Result<()> { -// let (lsp_request_guard, got_throttled) = if query.invalidate.should_invalidate() { -// (None, false) -// } else { -// match lsp_request_limiter.try_acquire() { -// Some(guard) => (Some(guard), false), -// None => (Some(lsp_request_limiter.acquire().await), true), -// } -// }; -// let fetch_range_to_log = -// fetch_range.start.to_point(&buffer_snapshot)..fetch_range.end.to_point(&buffer_snapshot); -// let inlay_hints_fetch_task = editor -// .update(&mut cx, |editor, cx| { -// if got_throttled { -// let query_not_around_visible_range = match editor.excerpt_visible_offsets(None, cx).remove(&query.excerpt_id) { -// Some((_, _, current_visible_range)) => { -// let visible_offset_length = current_visible_range.len(); -// let double_visible_range = current_visible_range -// .start -// .saturating_sub(visible_offset_length) -// ..current_visible_range -// .end -// .saturating_add(visible_offset_length) -// .min(buffer_snapshot.len()); -// !double_visible_range -// .contains(&fetch_range.start.to_offset(&buffer_snapshot)) -// && !double_visible_range -// .contains(&fetch_range.end.to_offset(&buffer_snapshot)) -// }, -// None => true, -// }; -// if query_not_around_visible_range { -// log::trace!("Fetching inlay hints for range {fetch_range_to_log:?} got throttled and fell off the current visible range, skipping."); -// if let Some(task_ranges) = editor -// .inlay_hint_cache -// .update_tasks -// .get_mut(&query.excerpt_id) -// { -// task_ranges.invalidate_range(&buffer_snapshot, &fetch_range); -// } -// return None; -// } -// } -// editor -// .buffer() -// .read(cx) -// .buffer(query.buffer_id) -// .and_then(|buffer| { -// let project = editor.project.as_ref()?; -// Some(project.update(cx, |project, cx| { -// project.inlay_hints(buffer, fetch_range.clone(), cx) -// })) -// }) -// }) -// .ok() -// .flatten(); -// let new_hints = match inlay_hints_fetch_task { -// Some(fetch_task) => { -// log::debug!( -// "Fetching inlay hints for range {fetch_range_to_log:?}, reason: {query_reason}, invalidate: {invalidate}", -// query_reason = query.reason, -// ); -// log::trace!( -// "Currently visible hints: {visible_hints:?}, cached hints present: {}", -// cached_excerpt_hints.is_some(), -// ); -// fetch_task.await.context("inlay hint fetch task")? -// } -// None => return Ok(()), -// }; -// drop(lsp_request_guard); -// log::debug!( -// "Fetched {} hints for range {fetch_range_to_log:?}", -// new_hints.len() -// ); -// log::trace!("Fetched hints: {new_hints:?}"); - -// let background_task_buffer_snapshot = buffer_snapshot.clone(); -// let backround_fetch_range = fetch_range.clone(); -// let new_update = cx -// .background() -// .spawn(async move { -// calculate_hint_updates( -// query.excerpt_id, -// invalidate, -// backround_fetch_range, -// new_hints, -// &background_task_buffer_snapshot, -// cached_excerpt_hints, -// &visible_hints, -// ) -// }) -// .await; -// if let Some(new_update) = new_update { -// log::debug!( -// "Applying update for range {fetch_range_to_log:?}: remove from editor: {}, remove from cache: {}, add to cache: {}", -// new_update.remove_from_visible.len(), -// new_update.remove_from_cache.len(), -// new_update.add_to_cache.len() -// ); -// log::trace!("New update: {new_update:?}"); -// editor -// .update(&mut cx, |editor, cx| { -// apply_hint_update( -// editor, -// new_update, -// query, -// invalidate, -// buffer_snapshot, -// multi_buffer_snapshot, -// cx, -// ); -// }) -// .ok(); -// } -// Ok(()) -// } +async fn fetch_and_update_hints( + editor: gpui::WeakView, + multi_buffer_snapshot: MultiBufferSnapshot, + buffer_snapshot: BufferSnapshot, + visible_hints: Arc>, + cached_excerpt_hints: Option>>, + query: ExcerptQuery, + invalidate: bool, + fetch_range: Range, + lsp_request_limiter: Arc, + mut cx: gpui::AsyncWindowContext, +) -> anyhow::Result<()> { + let (lsp_request_guard, got_throttled) = if query.invalidate.should_invalidate() { + (None, false) + } else { + match lsp_request_limiter.try_acquire() { + Some(guard) => (Some(guard), false), + None => (Some(lsp_request_limiter.acquire().await), true), + } + }; + let fetch_range_to_log = + fetch_range.start.to_point(&buffer_snapshot)..fetch_range.end.to_point(&buffer_snapshot); + let inlay_hints_fetch_task = editor + .update(&mut cx, |editor, cx| { + if got_throttled { + let query_not_around_visible_range = match editor.excerpt_visible_offsets(None, cx).remove(&query.excerpt_id) { + Some((_, _, current_visible_range)) => { + let visible_offset_length = current_visible_range.len(); + let double_visible_range = current_visible_range + .start + .saturating_sub(visible_offset_length) + ..current_visible_range + .end + .saturating_add(visible_offset_length) + .min(buffer_snapshot.len()); + !double_visible_range + .contains(&fetch_range.start.to_offset(&buffer_snapshot)) + && !double_visible_range + .contains(&fetch_range.end.to_offset(&buffer_snapshot)) + }, + None => true, + }; + if query_not_around_visible_range { + log::trace!("Fetching inlay hints for range {fetch_range_to_log:?} got throttled and fell off the current visible range, skipping."); + if let Some(task_ranges) = editor + .inlay_hint_cache + .update_tasks + .get_mut(&query.excerpt_id) + { + task_ranges.invalidate_range(&buffer_snapshot, &fetch_range); + } + return None; + } + } + editor + .buffer() + .read(cx) + .buffer(query.buffer_id) + .and_then(|buffer| { + let project = editor.project.as_ref()?; + Some(project.update(cx, |project, cx| { + project.inlay_hints(buffer, fetch_range.clone(), cx) + })) + }) + }) + .ok() + .flatten(); + let new_hints = match inlay_hints_fetch_task { + Some(fetch_task) => { + log::debug!( + "Fetching inlay hints for range {fetch_range_to_log:?}, reason: {query_reason}, invalidate: {invalidate}", + query_reason = query.reason, + ); + log::trace!( + "Currently visible hints: {visible_hints:?}, cached hints present: {}", + cached_excerpt_hints.is_some(), + ); + fetch_task.await.context("inlay hint fetch task")? + } + None => return Ok(()), + }; + drop(lsp_request_guard); + log::debug!( + "Fetched {} hints for range {fetch_range_to_log:?}", + new_hints.len() + ); + log::trace!("Fetched hints: {new_hints:?}"); + + let background_task_buffer_snapshot = buffer_snapshot.clone(); + let backround_fetch_range = fetch_range.clone(); + let new_update = cx + .background_executor() + .spawn(async move { + calculate_hint_updates( + query.excerpt_id, + invalidate, + backround_fetch_range, + new_hints, + &background_task_buffer_snapshot, + cached_excerpt_hints, + &visible_hints, + ) + }) + .await; + if let Some(new_update) = new_update { + log::debug!( + "Applying update for range {fetch_range_to_log:?}: remove from editor: {}, remove from cache: {}, add to cache: {}", + new_update.remove_from_visible.len(), + new_update.remove_from_cache.len(), + new_update.add_to_cache.len() + ); + log::trace!("New update: {new_update:?}"); + editor + .update(&mut cx, |editor, cx| { + apply_hint_update( + editor, + new_update, + query, + invalidate, + buffer_snapshot, + multi_buffer_snapshot, + cx, + ); + }) + .ok(); + } + Ok(()) +} fn calculate_hint_updates( excerpt_id: ExcerptId, @@ -1077,2279 +1073,2196 @@ fn apply_hint_update( multi_buffer_snapshot: MultiBufferSnapshot, cx: &mut ViewContext<'_, Editor>, ) { - todo!("old implementation commented below") + let cached_excerpt_hints = editor + .inlay_hint_cache + .hints + .entry(new_update.excerpt_id) + .or_insert_with(|| { + Arc::new(RwLock::new(CachedExcerptHints { + version: query.cache_version, + buffer_version: buffer_snapshot.version().clone(), + buffer_id: query.buffer_id, + ordered_hints: Vec::new(), + hints_by_id: HashMap::default(), + })) + }); + let mut cached_excerpt_hints = cached_excerpt_hints.write(); + match query.cache_version.cmp(&cached_excerpt_hints.version) { + cmp::Ordering::Less => return, + cmp::Ordering::Greater | cmp::Ordering::Equal => { + cached_excerpt_hints.version = query.cache_version; + } + } + + let mut cached_inlays_changed = !new_update.remove_from_cache.is_empty(); + cached_excerpt_hints + .ordered_hints + .retain(|hint_id| !new_update.remove_from_cache.contains(hint_id)); + cached_excerpt_hints + .hints_by_id + .retain(|hint_id, _| !new_update.remove_from_cache.contains(hint_id)); + let mut splice = InlaySplice { + to_remove: new_update.remove_from_visible, + to_insert: Vec::new(), + }; + for new_hint in new_update.add_to_cache { + let insert_position = match cached_excerpt_hints + .ordered_hints + .binary_search_by(|probe| { + cached_excerpt_hints.hints_by_id[probe] + .position + .cmp(&new_hint.position, &buffer_snapshot) + }) { + Ok(i) => { + let mut insert_position = Some(i); + for id in &cached_excerpt_hints.ordered_hints[i..] { + let cached_hint = &cached_excerpt_hints.hints_by_id[id]; + if new_hint + .position + .cmp(&cached_hint.position, &buffer_snapshot) + .is_gt() + { + break; + } + if cached_hint.text() == new_hint.text() { + insert_position = None; + break; + } + } + insert_position + } + Err(i) => Some(i), + }; + + if let Some(insert_position) = insert_position { + let new_inlay_id = post_inc(&mut editor.next_inlay_id); + if editor + .inlay_hint_cache + .allowed_hint_kinds + .contains(&new_hint.kind) + { + let new_hint_position = + multi_buffer_snapshot.anchor_in_excerpt(query.excerpt_id, new_hint.position); + splice + .to_insert + .push(Inlay::hint(new_inlay_id, new_hint_position, &new_hint)); + } + let new_id = InlayId::Hint(new_inlay_id); + cached_excerpt_hints.hints_by_id.insert(new_id, new_hint); + cached_excerpt_hints + .ordered_hints + .insert(insert_position, new_id); + cached_inlays_changed = true; + } + } + cached_excerpt_hints.buffer_version = buffer_snapshot.version().clone(); + drop(cached_excerpt_hints); + + if invalidate { + let mut outdated_excerpt_caches = HashSet::default(); + for (excerpt_id, excerpt_hints) in &editor.inlay_hint_cache().hints { + let excerpt_hints = excerpt_hints.read(); + if excerpt_hints.buffer_id == query.buffer_id + && excerpt_id != &query.excerpt_id + && buffer_snapshot + .version() + .changed_since(&excerpt_hints.buffer_version) + { + outdated_excerpt_caches.insert(*excerpt_id); + splice + .to_remove + .extend(excerpt_hints.ordered_hints.iter().copied()); + } + } + cached_inlays_changed |= !outdated_excerpt_caches.is_empty(); + editor + .inlay_hint_cache + .hints + .retain(|excerpt_id, _| !outdated_excerpt_caches.contains(excerpt_id)); + } + + let InlaySplice { + to_remove, + to_insert, + } = splice; + let displayed_inlays_changed = !to_remove.is_empty() || !to_insert.is_empty(); + if cached_inlays_changed || displayed_inlays_changed { + editor.inlay_hint_cache.version += 1; + } + if displayed_inlays_changed { + editor.splice_inlay_hints(to_remove, to_insert, cx) + } +} + +#[cfg(test)] +pub mod tests { + use std::sync::atomic::{AtomicBool, AtomicU32, AtomicUsize, Ordering}; + + use crate::{ + scroll::{autoscroll::Autoscroll, scroll_amount::ScrollAmount}, + ExcerptRange, + }; + use futures::StreamExt; + use gpui::{Context, TestAppContext, View, WindowHandle}; + use itertools::Itertools; + use language::{ + language_settings::AllLanguageSettingsContent, FakeLspAdapter, Language, LanguageConfig, + }; + use lsp::FakeLanguageServer; + use parking_lot::Mutex; + use project::{FakeFs, Project}; + use serde_json::json; + use settings::SettingsStore; + use text::{Point, ToPoint}; + use workspace::Workspace; + + use crate::editor_tests::update_test_language_settings; + + use super::*; + + #[gpui::test] + async fn test_basic_cache_update_with_duplicate_hints(cx: &mut gpui::TestAppContext) { + let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]); + init_test(cx, |settings| { + settings.defaults.inlay_hints = Some(InlayHintSettings { + enabled: true, + show_type_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Type)), + show_parameter_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)), + show_other_hints: allowed_hint_kinds.contains(&None), + }) + }); + + let (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await; + let lsp_request_count = Arc::new(AtomicU32::new(0)); + fake_server + .handle_request::(move |params, _| { + let task_lsp_request_count = Arc::clone(&lsp_request_count); + async move { + assert_eq!( + params.text_document.uri, + lsp::Url::from_file_path(file_with_hints).unwrap(), + ); + let current_call_id = + Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst); + let mut new_hints = Vec::with_capacity(2 * current_call_id as usize); + for _ in 0..2 { + let mut i = current_call_id; + loop { + new_hints.push(lsp::InlayHint { + position: lsp::Position::new(0, i), + label: lsp::InlayHintLabel::String(i.to_string()), + kind: None, + text_edits: None, + tooltip: None, + padding_left: None, + padding_right: None, + data: None, + }); + if i == 0 { + break; + } + i -= 1; + } + } + + Ok(Some(new_hints)) + } + }) + .next() + .await; + cx.executor().run_until_parked(); + + let mut edits_made = 1; + editor.update(cx, |editor, cx| { + let expected_hints = vec!["0".to_string()]; + assert_eq!( + expected_hints, + cached_hint_labels(editor), + "Should get its first hints when opening the editor" + ); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + let inlay_cache = editor.inlay_hint_cache(); + assert_eq!( + inlay_cache.allowed_hint_kinds, allowed_hint_kinds, + "Cache should use editor settings to get the allowed hint kinds" + ); + assert_eq!( + inlay_cache.version, edits_made, + "The editor update the cache version after every cache/view change" + ); + }); + + editor.update(cx, |editor, cx| { + editor.change_selections(None, cx, |s| s.select_ranges([13..13])); + editor.handle_input("some change", cx); + edits_made += 1; + }); + cx.executor().run_until_parked(); + editor.update(cx, |editor, cx| { + let expected_hints = vec!["0".to_string(), "1".to_string()]; + assert_eq!( + expected_hints, + cached_hint_labels(editor), + "Should get new hints after an edit" + ); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + let inlay_cache = editor.inlay_hint_cache(); + assert_eq!( + inlay_cache.allowed_hint_kinds, allowed_hint_kinds, + "Cache should use editor settings to get the allowed hint kinds" + ); + assert_eq!( + inlay_cache.version, edits_made, + "The editor update the cache version after every cache/view change" + ); + }); + + fake_server + .request::(()) + .await + .expect("inlay refresh request failed"); + edits_made += 1; + cx.executor().run_until_parked(); + editor.update(cx, |editor, cx| { + let expected_hints = vec!["0".to_string(), "1".to_string(), "2".to_string()]; + assert_eq!( + expected_hints, + cached_hint_labels(editor), + "Should get new hints after hint refresh/ request" + ); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + let inlay_cache = editor.inlay_hint_cache(); + assert_eq!( + inlay_cache.allowed_hint_kinds, allowed_hint_kinds, + "Cache should use editor settings to get the allowed hint kinds" + ); + assert_eq!( + inlay_cache.version, edits_made, + "The editor update the cache version after every cache/view change" + ); + }); + } + + #[gpui::test] + async fn test_cache_update_on_lsp_completion_tasks(cx: &mut gpui::TestAppContext) { + init_test(cx, |settings| { + settings.defaults.inlay_hints = Some(InlayHintSettings { + enabled: true, + show_type_hints: true, + show_parameter_hints: true, + show_other_hints: true, + }) + }); + + let (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await; + let lsp_request_count = Arc::new(AtomicU32::new(0)); + fake_server + .handle_request::(move |params, _| { + let task_lsp_request_count = Arc::clone(&lsp_request_count); + async move { + assert_eq!( + params.text_document.uri, + lsp::Url::from_file_path(file_with_hints).unwrap(), + ); + let current_call_id = + Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst); + Ok(Some(vec![lsp::InlayHint { + position: lsp::Position::new(0, current_call_id), + label: lsp::InlayHintLabel::String(current_call_id.to_string()), + kind: None, + text_edits: None, + tooltip: None, + padding_left: None, + padding_right: None, + data: None, + }])) + } + }) + .next() + .await; + cx.executor().run_until_parked(); + + let mut edits_made = 1; + editor.update(cx, |editor, cx| { + let expected_hints = vec!["0".to_string()]; + assert_eq!( + expected_hints, + cached_hint_labels(editor), + "Should get its first hints when opening the editor" + ); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + assert_eq!( + editor.inlay_hint_cache().version, + edits_made, + "The editor update the cache version after every cache/view change" + ); + }); + + let progress_token = "test_progress_token"; + fake_server + .request::(lsp::WorkDoneProgressCreateParams { + token: lsp::ProgressToken::String(progress_token.to_string()), + }) + .await + .expect("work done progress create request failed"); + cx.executor().run_until_parked(); + fake_server.notify::(lsp::ProgressParams { + token: lsp::ProgressToken::String(progress_token.to_string()), + value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Begin( + lsp::WorkDoneProgressBegin::default(), + )), + }); + cx.executor().run_until_parked(); + + editor.update(cx, |editor, cx| { + let expected_hints = vec!["0".to_string()]; + assert_eq!( + expected_hints, + cached_hint_labels(editor), + "Should not update hints while the work task is running" + ); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + assert_eq!( + editor.inlay_hint_cache().version, + edits_made, + "Should not update the cache while the work task is running" + ); + }); + + fake_server.notify::(lsp::ProgressParams { + token: lsp::ProgressToken::String(progress_token.to_string()), + value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::End( + lsp::WorkDoneProgressEnd::default(), + )), + }); + cx.executor().run_until_parked(); + + edits_made += 1; + editor.update(cx, |editor, cx| { + let expected_hints = vec!["1".to_string()]; + assert_eq!( + expected_hints, + cached_hint_labels(editor), + "New hints should be queried after the work task is done" + ); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + assert_eq!( + editor.inlay_hint_cache().version, + edits_made, + "Cache version should udpate once after the work task is done" + ); + }); + } + + #[gpui::test] + async fn test_no_hint_updates_for_unrelated_language_files(cx: &mut gpui::TestAppContext) { + init_test(cx, |settings| { + settings.defaults.inlay_hints = Some(InlayHintSettings { + enabled: true, + show_type_hints: true, + show_parameter_hints: true, + show_other_hints: true, + }) + }); + + let fs = FakeFs::new(cx.background_executor.clone()); + fs.insert_tree( + "/a", + json!({ + "main.rs": "fn main() { a } // and some long comment to ensure inlays are not trimmed out", + "other.md": "Test md file with some text", + }), + ) + .await; + let project = Project::test(fs, ["/a".as_ref()], cx).await; + + let mut rs_fake_servers = None; + let mut md_fake_servers = None; + for (name, path_suffix) in [("Rust", "rs"), ("Markdown", "md")] { + let mut language = Language::new( + LanguageConfig { + name: name.into(), + path_suffixes: vec![path_suffix.to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ); + let fake_servers = language + .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + name, + capabilities: lsp::ServerCapabilities { + inlay_hint_provider: Some(lsp::OneOf::Left(true)), + ..Default::default() + }, + ..Default::default() + })) + .await; + match name { + "Rust" => rs_fake_servers = Some(fake_servers), + "Markdown" => md_fake_servers = Some(fake_servers), + _ => unreachable!(), + } + project.update(cx, |project, _| { + project.languages().add(Arc::new(language)); + }); + } + + let rs_buffer = project + .update(cx, |project, cx| { + project.open_local_buffer("/a/main.rs", cx) + }) + .await + .unwrap(); + cx.executor().run_until_parked(); + cx.executor().start_waiting(); + let rs_fake_server = rs_fake_servers.unwrap().next().await.unwrap(); + let rs_editor = + cx.add_window(|cx| Editor::for_buffer(rs_buffer, Some(project.clone()), cx)); + let rs_lsp_request_count = Arc::new(AtomicU32::new(0)); + rs_fake_server + .handle_request::(move |params, _| { + let task_lsp_request_count = Arc::clone(&rs_lsp_request_count); + async move { + assert_eq!( + params.text_document.uri, + lsp::Url::from_file_path("/a/main.rs").unwrap(), + ); + let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst); + Ok(Some(vec![lsp::InlayHint { + position: lsp::Position::new(0, i), + label: lsp::InlayHintLabel::String(i.to_string()), + kind: None, + text_edits: None, + tooltip: None, + padding_left: None, + padding_right: None, + data: None, + }])) + } + }) + .next() + .await; + cx.executor().run_until_parked(); + rs_editor.update(cx, |editor, cx| { + let expected_hints = vec!["0".to_string()]; + assert_eq!( + expected_hints, + cached_hint_labels(editor), + "Should get its first hints when opening the editor" + ); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + assert_eq!( + editor.inlay_hint_cache().version, + 1, + "Rust editor update the cache version after every cache/view change" + ); + }); + + cx.executor().run_until_parked(); + let md_buffer = project + .update(cx, |project, cx| { + project.open_local_buffer("/a/other.md", cx) + }) + .await + .unwrap(); + cx.executor().run_until_parked(); + cx.executor().start_waiting(); + let md_fake_server = md_fake_servers.unwrap().next().await.unwrap(); + let md_editor = cx.add_window(|cx| Editor::for_buffer(md_buffer, Some(project), cx)); + let md_lsp_request_count = Arc::new(AtomicU32::new(0)); + md_fake_server + .handle_request::(move |params, _| { + let task_lsp_request_count = Arc::clone(&md_lsp_request_count); + async move { + assert_eq!( + params.text_document.uri, + lsp::Url::from_file_path("/a/other.md").unwrap(), + ); + let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst); + Ok(Some(vec![lsp::InlayHint { + position: lsp::Position::new(0, i), + label: lsp::InlayHintLabel::String(i.to_string()), + kind: None, + text_edits: None, + tooltip: None, + padding_left: None, + padding_right: None, + data: None, + }])) + } + }) + .next() + .await; + cx.executor().run_until_parked(); + md_editor.update(cx, |editor, cx| { + let expected_hints = vec!["0".to_string()]; + assert_eq!( + expected_hints, + cached_hint_labels(editor), + "Markdown editor should have a separate verison, repeating Rust editor rules" + ); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + assert_eq!(editor.inlay_hint_cache().version, 1); + }); + + rs_editor.update(cx, |editor, cx| { + editor.change_selections(None, cx, |s| s.select_ranges([13..13])); + editor.handle_input("some rs change", cx); + }); + cx.executor().run_until_parked(); + rs_editor.update(cx, |editor, cx| { + let expected_hints = vec!["1".to_string()]; + assert_eq!( + expected_hints, + cached_hint_labels(editor), + "Rust inlay cache should change after the edit" + ); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + assert_eq!( + editor.inlay_hint_cache().version, + 2, + "Every time hint cache changes, cache version should be incremented" + ); + }); + md_editor.update(cx, |editor, cx| { + let expected_hints = vec!["0".to_string()]; + assert_eq!( + expected_hints, + cached_hint_labels(editor), + "Markdown editor should not be affected by Rust editor changes" + ); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + assert_eq!(editor.inlay_hint_cache().version, 1); + }); + + md_editor.update(cx, |editor, cx| { + editor.change_selections(None, cx, |s| s.select_ranges([13..13])); + editor.handle_input("some md change", cx); + }); + cx.executor().run_until_parked(); + md_editor.update(cx, |editor, cx| { + let expected_hints = vec!["1".to_string()]; + assert_eq!( + expected_hints, + cached_hint_labels(editor), + "Rust editor should not be affected by Markdown editor changes" + ); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + assert_eq!(editor.inlay_hint_cache().version, 2); + }); + rs_editor.update(cx, |editor, cx| { + let expected_hints = vec!["1".to_string()]; + assert_eq!( + expected_hints, + cached_hint_labels(editor), + "Markdown editor should also change independently" + ); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + assert_eq!(editor.inlay_hint_cache().version, 2); + }); + } + + #[gpui::test] + async fn test_hint_setting_changes(cx: &mut gpui::TestAppContext) { + let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]); + init_test(cx, |settings| { + settings.defaults.inlay_hints = Some(InlayHintSettings { + enabled: true, + show_type_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Type)), + show_parameter_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)), + show_other_hints: allowed_hint_kinds.contains(&None), + }) + }); + + let (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await; + let lsp_request_count = Arc::new(AtomicU32::new(0)); + let another_lsp_request_count = Arc::clone(&lsp_request_count); + fake_server + .handle_request::(move |params, _| { + let task_lsp_request_count = Arc::clone(&another_lsp_request_count); + async move { + Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst); + assert_eq!( + params.text_document.uri, + lsp::Url::from_file_path(file_with_hints).unwrap(), + ); + Ok(Some(vec![ + lsp::InlayHint { + position: lsp::Position::new(0, 1), + label: lsp::InlayHintLabel::String("type hint".to_string()), + kind: Some(lsp::InlayHintKind::TYPE), + text_edits: None, + tooltip: None, + padding_left: None, + padding_right: None, + data: None, + }, + lsp::InlayHint { + position: lsp::Position::new(0, 2), + label: lsp::InlayHintLabel::String("parameter hint".to_string()), + kind: Some(lsp::InlayHintKind::PARAMETER), + text_edits: None, + tooltip: None, + padding_left: None, + padding_right: None, + data: None, + }, + lsp::InlayHint { + position: lsp::Position::new(0, 3), + label: lsp::InlayHintLabel::String("other hint".to_string()), + kind: None, + text_edits: None, + tooltip: None, + padding_left: None, + padding_right: None, + data: None, + }, + ])) + } + }) + .next() + .await; + cx.executor().run_until_parked(); + + let mut edits_made = 1; + editor.update(cx, |editor, cx| { + assert_eq!( + lsp_request_count.load(Ordering::Relaxed), + 1, + "Should query new hints once" + ); + assert_eq!( + vec![ + "other hint".to_string(), + "parameter hint".to_string(), + "type hint".to_string(), + ], + cached_hint_labels(editor), + "Should get its first hints when opening the editor" + ); + assert_eq!( + vec!["other hint".to_string(), "type hint".to_string()], + visible_hint_labels(editor, cx) + ); + let inlay_cache = editor.inlay_hint_cache(); + assert_eq!( + inlay_cache.allowed_hint_kinds, allowed_hint_kinds, + "Cache should use editor settings to get the allowed hint kinds" + ); + assert_eq!( + inlay_cache.version, edits_made, + "The editor update the cache version after every cache/view change" + ); + }); + + fake_server + .request::(()) + .await + .expect("inlay refresh request failed"); + cx.executor().run_until_parked(); + editor.update(cx, |editor, cx| { + assert_eq!( + lsp_request_count.load(Ordering::Relaxed), + 2, + "Should load new hints twice" + ); + assert_eq!( + vec![ + "other hint".to_string(), + "parameter hint".to_string(), + "type hint".to_string(), + ], + cached_hint_labels(editor), + "Cached hints should not change due to allowed hint kinds settings update" + ); + assert_eq!( + vec!["other hint".to_string(), "type hint".to_string()], + visible_hint_labels(editor, cx) + ); + assert_eq!( + editor.inlay_hint_cache().version, + edits_made, + "Should not update cache version due to new loaded hints being the same" + ); + }); + + for (new_allowed_hint_kinds, expected_visible_hints) in [ + (HashSet::from_iter([None]), vec!["other hint".to_string()]), + ( + HashSet::from_iter([Some(InlayHintKind::Type)]), + vec!["type hint".to_string()], + ), + ( + HashSet::from_iter([Some(InlayHintKind::Parameter)]), + vec!["parameter hint".to_string()], + ), + ( + HashSet::from_iter([None, Some(InlayHintKind::Type)]), + vec!["other hint".to_string(), "type hint".to_string()], + ), + ( + HashSet::from_iter([None, Some(InlayHintKind::Parameter)]), + vec!["other hint".to_string(), "parameter hint".to_string()], + ), + ( + HashSet::from_iter([Some(InlayHintKind::Type), Some(InlayHintKind::Parameter)]), + vec!["parameter hint".to_string(), "type hint".to_string()], + ), + ( + HashSet::from_iter([ + None, + Some(InlayHintKind::Type), + Some(InlayHintKind::Parameter), + ]), + vec![ + "other hint".to_string(), + "parameter hint".to_string(), + "type hint".to_string(), + ], + ), + ] { + edits_made += 1; + update_test_language_settings(cx, |settings| { + settings.defaults.inlay_hints = Some(InlayHintSettings { + enabled: true, + show_type_hints: new_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)), + show_parameter_hints: new_allowed_hint_kinds + .contains(&Some(InlayHintKind::Parameter)), + show_other_hints: new_allowed_hint_kinds.contains(&None), + }) + }); + cx.executor().run_until_parked(); + editor.update(cx, |editor, cx| { + assert_eq!( + lsp_request_count.load(Ordering::Relaxed), + 2, + "Should not load new hints on allowed hint kinds change for hint kinds {new_allowed_hint_kinds:?}" + ); + assert_eq!( + vec![ + "other hint".to_string(), + "parameter hint".to_string(), + "type hint".to_string(), + ], + cached_hint_labels(editor), + "Should get its cached hints unchanged after the settings change for hint kinds {new_allowed_hint_kinds:?}" + ); + assert_eq!( + expected_visible_hints, + visible_hint_labels(editor, cx), + "Should get its visible hints filtered after the settings change for hint kinds {new_allowed_hint_kinds:?}" + ); + let inlay_cache = editor.inlay_hint_cache(); + assert_eq!( + inlay_cache.allowed_hint_kinds, new_allowed_hint_kinds, + "Cache should use editor settings to get the allowed hint kinds for hint kinds {new_allowed_hint_kinds:?}" + ); + assert_eq!( + inlay_cache.version, edits_made, + "The editor should update the cache version after every cache/view change for hint kinds {new_allowed_hint_kinds:?} due to visible hints change" + ); + }); + } + + edits_made += 1; + let another_allowed_hint_kinds = HashSet::from_iter([Some(InlayHintKind::Type)]); + update_test_language_settings(cx, |settings| { + settings.defaults.inlay_hints = Some(InlayHintSettings { + enabled: false, + show_type_hints: another_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)), + show_parameter_hints: another_allowed_hint_kinds + .contains(&Some(InlayHintKind::Parameter)), + show_other_hints: another_allowed_hint_kinds.contains(&None), + }) + }); + cx.executor().run_until_parked(); + editor.update(cx, |editor, cx| { + assert_eq!( + lsp_request_count.load(Ordering::Relaxed), + 2, + "Should not load new hints when hints got disabled" + ); + assert!( + cached_hint_labels(editor).is_empty(), + "Should clear the cache when hints got disabled" + ); + assert!( + visible_hint_labels(editor, cx).is_empty(), + "Should clear visible hints when hints got disabled" + ); + let inlay_cache = editor.inlay_hint_cache(); + assert_eq!( + inlay_cache.allowed_hint_kinds, another_allowed_hint_kinds, + "Should update its allowed hint kinds even when hints got disabled" + ); + assert_eq!( + inlay_cache.version, edits_made, + "The editor should update the cache version after hints got disabled" + ); + }); + + fake_server + .request::(()) + .await + .expect("inlay refresh request failed"); + cx.executor().run_until_parked(); + editor.update(cx, |editor, cx| { + assert_eq!( + lsp_request_count.load(Ordering::Relaxed), + 2, + "Should not load new hints when they got disabled" + ); + assert!(cached_hint_labels(editor).is_empty()); + assert!(visible_hint_labels(editor, cx).is_empty()); + assert_eq!( + editor.inlay_hint_cache().version, edits_made, + "The editor should not update the cache version after /refresh query without updates" + ); + }); + + let final_allowed_hint_kinds = HashSet::from_iter([Some(InlayHintKind::Parameter)]); + edits_made += 1; + update_test_language_settings(cx, |settings| { + settings.defaults.inlay_hints = Some(InlayHintSettings { + enabled: true, + show_type_hints: final_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)), + show_parameter_hints: final_allowed_hint_kinds + .contains(&Some(InlayHintKind::Parameter)), + show_other_hints: final_allowed_hint_kinds.contains(&None), + }) + }); + cx.executor().run_until_parked(); + editor.update(cx, |editor, cx| { + assert_eq!( + lsp_request_count.load(Ordering::Relaxed), + 3, + "Should query for new hints when they got reenabled" + ); + assert_eq!( + vec![ + "other hint".to_string(), + "parameter hint".to_string(), + "type hint".to_string(), + ], + cached_hint_labels(editor), + "Should get its cached hints fully repopulated after the hints got reenabled" + ); + assert_eq!( + vec!["parameter hint".to_string()], + visible_hint_labels(editor, cx), + "Should get its visible hints repopulated and filtered after the h" + ); + let inlay_cache = editor.inlay_hint_cache(); + assert_eq!( + inlay_cache.allowed_hint_kinds, final_allowed_hint_kinds, + "Cache should update editor settings when hints got reenabled" + ); + assert_eq!( + inlay_cache.version, edits_made, + "Cache should update its version after hints got reenabled" + ); + }); + + fake_server + .request::(()) + .await + .expect("inlay refresh request failed"); + cx.executor().run_until_parked(); + editor.update(cx, |editor, cx| { + assert_eq!( + lsp_request_count.load(Ordering::Relaxed), + 4, + "Should query for new hints again" + ); + assert_eq!( + vec![ + "other hint".to_string(), + "parameter hint".to_string(), + "type hint".to_string(), + ], + cached_hint_labels(editor), + ); + assert_eq!( + vec!["parameter hint".to_string()], + visible_hint_labels(editor, cx), + ); + assert_eq!(editor.inlay_hint_cache().version, edits_made); + }); + } + + #[gpui::test] + async fn test_hint_request_cancellation(cx: &mut gpui::TestAppContext) { + init_test(cx, |settings| { + settings.defaults.inlay_hints = Some(InlayHintSettings { + enabled: true, + show_type_hints: true, + show_parameter_hints: true, + show_other_hints: true, + }) + }); + + let (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await; + let fake_server = Arc::new(fake_server); + let lsp_request_count = Arc::new(AtomicU32::new(0)); + let another_lsp_request_count = Arc::clone(&lsp_request_count); + fake_server + .handle_request::(move |params, _| { + let task_lsp_request_count = Arc::clone(&another_lsp_request_count); + async move { + let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst) + 1; + assert_eq!( + params.text_document.uri, + lsp::Url::from_file_path(file_with_hints).unwrap(), + ); + Ok(Some(vec![lsp::InlayHint { + position: lsp::Position::new(0, i), + label: lsp::InlayHintLabel::String(i.to_string()), + kind: None, + text_edits: None, + tooltip: None, + padding_left: None, + padding_right: None, + data: None, + }])) + } + }) + .next() + .await; + + let mut expected_changes = Vec::new(); + for change_after_opening in [ + "initial change #1", + "initial change #2", + "initial change #3", + ] { + editor.update(cx, |editor, cx| { + editor.change_selections(None, cx, |s| s.select_ranges([13..13])); + editor.handle_input(change_after_opening, cx); + }); + expected_changes.push(change_after_opening); + } + + cx.executor().run_until_parked(); + + editor.update(cx, |editor, cx| { + let current_text = editor.text(cx); + for change in &expected_changes { + assert!( + current_text.contains(change), + "Should apply all changes made" + ); + } + assert_eq!( + lsp_request_count.load(Ordering::Relaxed), + 2, + "Should query new hints twice: for editor init and for the last edit that interrupted all others" + ); + let expected_hints = vec!["2".to_string()]; + assert_eq!( + expected_hints, + cached_hint_labels(editor), + "Should get hints from the last edit landed only" + ); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + assert_eq!( + editor.inlay_hint_cache().version, 1, + "Only one update should be registered in the cache after all cancellations" + ); + }); + + let mut edits = Vec::new(); + for async_later_change in [ + "another change #1", + "another change #2", + "another change #3", + ] { + expected_changes.push(async_later_change); + let task_editor = editor.clone(); + edits.push(cx.spawn(|mut cx| async move { + task_editor.update(&mut cx, |editor, cx| { + editor.change_selections(None, cx, |s| s.select_ranges([13..13])); + editor.handle_input(async_later_change, cx); + }); + })); + } + let _ = future::join_all(edits).await; + cx.executor().run_until_parked(); + + editor.update(cx, |editor, cx| { + let current_text = editor.text(cx); + for change in &expected_changes { + assert!( + current_text.contains(change), + "Should apply all changes made" + ); + } + assert_eq!( + lsp_request_count.load(Ordering::SeqCst), + 3, + "Should query new hints one more time, for the last edit only" + ); + let expected_hints = vec!["3".to_string()]; + assert_eq!( + expected_hints, + cached_hint_labels(editor), + "Should get hints from the last edit landed only" + ); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + assert_eq!( + editor.inlay_hint_cache().version, + 2, + "Should update the cache version once more, for the new change" + ); + }); + } + + #[gpui::test(iterations = 10)] + async fn test_large_buffer_inlay_requests_split(cx: &mut gpui::TestAppContext) { + init_test(cx, |settings| { + settings.defaults.inlay_hints = Some(InlayHintSettings { + enabled: true, + show_type_hints: true, + show_parameter_hints: true, + show_other_hints: true, + }) + }); + + let mut language = Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ); + let mut fake_servers = language + .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + capabilities: lsp::ServerCapabilities { + inlay_hint_provider: Some(lsp::OneOf::Left(true)), + ..Default::default() + }, + ..Default::default() + })) + .await; + let fs = FakeFs::new(cx.background_executor.clone()); + fs.insert_tree( + "/a", + json!({ + "main.rs": format!("fn main() {{\n{}\n}}", "let i = 5;\n".repeat(500)), + "other.rs": "// Test file", + }), + ) + .await; + let project = Project::test(fs, ["/a".as_ref()], cx).await; + project.update(cx, |project, _| project.languages().add(Arc::new(language))); + let buffer = project + .update(cx, |project, cx| { + project.open_local_buffer("/a/main.rs", cx) + }) + .await + .unwrap(); + cx.executor().run_until_parked(); + cx.executor().start_waiting(); + let fake_server = fake_servers.next().await.unwrap(); + let editor = cx.add_window(|cx| Editor::for_buffer(buffer, Some(project), cx)); + let lsp_request_ranges = Arc::new(Mutex::new(Vec::new())); + let lsp_request_count = Arc::new(AtomicUsize::new(0)); + let closure_lsp_request_ranges = Arc::clone(&lsp_request_ranges); + let closure_lsp_request_count = Arc::clone(&lsp_request_count); + fake_server + .handle_request::(move |params, _| { + let task_lsp_request_ranges = Arc::clone(&closure_lsp_request_ranges); + let task_lsp_request_count = Arc::clone(&closure_lsp_request_count); + async move { + assert_eq!( + params.text_document.uri, + lsp::Url::from_file_path("/a/main.rs").unwrap(), + ); + + task_lsp_request_ranges.lock().push(params.range); + let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::Release) + 1; + Ok(Some(vec![lsp::InlayHint { + position: params.range.end, + label: lsp::InlayHintLabel::String(i.to_string()), + kind: None, + text_edits: None, + tooltip: None, + padding_left: None, + padding_right: None, + data: None, + }])) + } + }) + .next() + .await; + + fn editor_visible_range( + editor: &WindowHandle, + cx: &mut gpui::TestAppContext, + ) -> Range { + let ranges = editor + .update(cx, |editor, cx| editor.excerpt_visible_offsets(None, cx)) + .unwrap(); + assert_eq!( + ranges.len(), + 1, + "Single buffer should produce a single excerpt with visible range" + ); + let (_, (excerpt_buffer, _, excerpt_visible_range)) = + ranges.into_iter().next().unwrap(); + excerpt_buffer.update(cx, |buffer, _| { + let snapshot = buffer.snapshot(); + let start = buffer + .anchor_before(excerpt_visible_range.start) + .to_point(&snapshot); + let end = buffer + .anchor_after(excerpt_visible_range.end) + .to_point(&snapshot); + start..end + }) + } + + // in large buffers, requests are made for more than visible range of a buffer. + // invisible parts are queried later, to avoid excessive requests on quick typing. + // wait the timeout needed to get all requests. + cx.executor().advance_clock(Duration::from_millis( + INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100, + )); + cx.executor().run_until_parked(); + let initial_visible_range = editor_visible_range(&editor, cx); + let lsp_initial_visible_range = lsp::Range::new( + lsp::Position::new( + initial_visible_range.start.row, + initial_visible_range.start.column, + ), + lsp::Position::new( + initial_visible_range.end.row, + initial_visible_range.end.column, + ), + ); + let expected_initial_query_range_end = + lsp::Position::new(initial_visible_range.end.row * 2, 2); + let mut expected_invisible_query_start = lsp_initial_visible_range.end; + expected_invisible_query_start.character += 1; + editor.update(cx, |editor, cx| { + let ranges = lsp_request_ranges.lock().drain(..).collect::>(); + assert_eq!(ranges.len(), 2, + "When scroll is at the edge of a big document, its visible part and the same range further should be queried in order, but got: {ranges:?}"); + let visible_query_range = &ranges[0]; + assert_eq!(visible_query_range.start, lsp_initial_visible_range.start); + assert_eq!(visible_query_range.end, lsp_initial_visible_range.end); + let invisible_query_range = &ranges[1]; + + assert_eq!(invisible_query_range.start, expected_invisible_query_start, "Should initially query visible edge of the document"); + assert_eq!(invisible_query_range.end, expected_initial_query_range_end, "Should initially query visible edge of the document"); + + let requests_count = lsp_request_count.load(Ordering::Acquire); + assert_eq!(requests_count, 2, "Visible + invisible request"); + let expected_hints = vec!["1".to_string(), "2".to_string()]; + assert_eq!( + expected_hints, + cached_hint_labels(editor), + "Should have hints from both LSP requests made for a big file" + ); + assert_eq!(expected_hints, visible_hint_labels(editor, cx), "Should display only hints from the visible range"); + assert_eq!( + editor.inlay_hint_cache().version, requests_count, + "LSP queries should've bumped the cache version" + ); + }); + + editor.update(cx, |editor, cx| { + editor.scroll_screen(&ScrollAmount::Page(1.0), cx); + editor.scroll_screen(&ScrollAmount::Page(1.0), cx); + }); + cx.executor().advance_clock(Duration::from_millis( + INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100, + )); + cx.executor().run_until_parked(); + let visible_range_after_scrolls = editor_visible_range(&editor, cx); + let visible_line_count = editor + .update(cx, |editor, _| editor.visible_line_count().unwrap()) + .unwrap(); + let selection_in_cached_range = editor + .update(cx, |editor, cx| { + let ranges = lsp_request_ranges + .lock() + .drain(..) + .sorted_by_key(|r| r.start) + .collect::>(); + assert_eq!( + ranges.len(), + 2, + "Should query 2 ranges after both scrolls, but got: {ranges:?}" + ); + let first_scroll = &ranges[0]; + let second_scroll = &ranges[1]; + assert_eq!( + first_scroll.end, second_scroll.start, + "Should query 2 adjacent ranges after the scrolls, but got: {ranges:?}" + ); + assert_eq!( + first_scroll.start, expected_initial_query_range_end, + "First scroll should start the query right after the end of the original scroll", + ); + assert_eq!( + second_scroll.end, + lsp::Position::new( + visible_range_after_scrolls.end.row + + visible_line_count.ceil() as u32, + 1, + ), + "Second scroll should query one more screen down after the end of the visible range" + ); + + let lsp_requests = lsp_request_count.load(Ordering::Acquire); + assert_eq!(lsp_requests, 4, "Should query for hints after every scroll"); + let expected_hints = vec![ + "1".to_string(), + "2".to_string(), + "3".to_string(), + "4".to_string(), + ]; + assert_eq!( + expected_hints, + cached_hint_labels(editor), + "Should have hints from the new LSP response after the edit" + ); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + assert_eq!( + editor.inlay_hint_cache().version, + lsp_requests, + "Should update the cache for every LSP response with hints added" + ); + + let mut selection_in_cached_range = visible_range_after_scrolls.end; + selection_in_cached_range.row -= visible_line_count.ceil() as u32; + selection_in_cached_range + }) + .unwrap(); + + editor.update(cx, |editor, cx| { + editor.change_selections(Some(Autoscroll::center()), cx, |s| { + s.select_ranges([selection_in_cached_range..selection_in_cached_range]) + }); + }); + cx.executor().advance_clock(Duration::from_millis( + INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100, + )); + cx.executor().run_until_parked(); + editor.update(cx, |_, _| { + let ranges = lsp_request_ranges + .lock() + .drain(..) + .sorted_by_key(|r| r.start) + .collect::>(); + assert!(ranges.is_empty(), "No new ranges or LSP queries should be made after returning to the selection with cached hints"); + assert_eq!(lsp_request_count.load(Ordering::Acquire), 4); + }); + + editor.update(cx, |editor, cx| { + editor.handle_input("++++more text++++", cx); + }); + cx.executor().advance_clock(Duration::from_millis( + INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100, + )); + cx.executor().run_until_parked(); + editor.update(cx, |editor, cx| { + let mut ranges = lsp_request_ranges.lock().drain(..).collect::>(); + ranges.sort_by_key(|r| r.start); + + assert_eq!(ranges.len(), 3, + "On edit, should scroll to selection and query a range around it: visible + same range above and below. Instead, got query ranges {ranges:?}"); + let above_query_range = &ranges[0]; + let visible_query_range = &ranges[1]; + let below_query_range = &ranges[2]; + assert!(above_query_range.end.character < visible_query_range.start.character || above_query_range.end.line + 1 == visible_query_range.start.line, + "Above range {above_query_range:?} should be before visible range {visible_query_range:?}"); + assert!(visible_query_range.end.character < below_query_range.start.character || visible_query_range.end.line + 1 == below_query_range.start.line, + "Visible range {visible_query_range:?} should be before below range {below_query_range:?}"); + assert!(above_query_range.start.line < selection_in_cached_range.row, + "Hints should be queried with the selected range after the query range start"); + assert!(below_query_range.end.line > selection_in_cached_range.row, + "Hints should be queried with the selected range before the query range end"); + assert!(above_query_range.start.line <= selection_in_cached_range.row - (visible_line_count * 3.0 / 2.0) as u32, + "Hints query range should contain one more screen before"); + assert!(below_query_range.end.line >= selection_in_cached_range.row + (visible_line_count * 3.0 / 2.0) as u32, + "Hints query range should contain one more screen after"); + + let lsp_requests = lsp_request_count.load(Ordering::Acquire); + assert_eq!(lsp_requests, 7, "There should be a visible range and two ranges above and below it queried"); + let expected_hints = vec!["5".to_string(), "6".to_string(), "7".to_string()]; + assert_eq!(expected_hints, cached_hint_labels(editor), + "Should have hints from the new LSP response after the edit"); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + assert_eq!(editor.inlay_hint_cache().version, lsp_requests, "Should update the cache for every LSP response with hints added"); + }); + } + + #[gpui::test(iterations = 10)] + async fn test_multiple_excerpts_large_multibuffer(cx: &mut gpui::TestAppContext) { + init_test(cx, |settings| { + settings.defaults.inlay_hints = Some(InlayHintSettings { + enabled: true, + show_type_hints: true, + show_parameter_hints: true, + show_other_hints: true, + }) + }); + + let mut language = Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ); + let mut fake_servers = language + .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + capabilities: lsp::ServerCapabilities { + inlay_hint_provider: Some(lsp::OneOf::Left(true)), + ..Default::default() + }, + ..Default::default() + })) + .await; + let language = Arc::new(language); + let fs = FakeFs::new(cx.background_executor.clone()); + fs.insert_tree( + "/a", + json!({ + "main.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|i| format!("let i = {i};\n")).collect::>().join("")), + "other.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|j| format!("let j = {j};\n")).collect::>().join("")), + }), + ) + .await; + let project = Project::test(fs, ["/a".as_ref()], cx).await; + project.update(cx, |project, _| { + project.languages().add(Arc::clone(&language)) + }); + let worktree_id = project.update(cx, |project, cx| { + project.worktrees().next().unwrap().read(cx).id() + }); + + let buffer_1 = project + .update(cx, |project, cx| { + project.open_buffer((worktree_id, "main.rs"), cx) + }) + .await + .unwrap(); + let buffer_2 = project + .update(cx, |project, cx| { + project.open_buffer((worktree_id, "other.rs"), cx) + }) + .await + .unwrap(); + let multibuffer = cx.build_model(|cx| { + let mut multibuffer = MultiBuffer::new(0); + multibuffer.push_excerpts( + buffer_1.clone(), + [ + ExcerptRange { + context: Point::new(0, 0)..Point::new(2, 0), + primary: None, + }, + ExcerptRange { + context: Point::new(4, 0)..Point::new(11, 0), + primary: None, + }, + ExcerptRange { + context: Point::new(22, 0)..Point::new(33, 0), + primary: None, + }, + ExcerptRange { + context: Point::new(44, 0)..Point::new(55, 0), + primary: None, + }, + ExcerptRange { + context: Point::new(56, 0)..Point::new(66, 0), + primary: None, + }, + ExcerptRange { + context: Point::new(67, 0)..Point::new(77, 0), + primary: None, + }, + ], + cx, + ); + multibuffer.push_excerpts( + buffer_2.clone(), + [ + ExcerptRange { + context: Point::new(0, 1)..Point::new(2, 1), + primary: None, + }, + ExcerptRange { + context: Point::new(4, 1)..Point::new(11, 1), + primary: None, + }, + ExcerptRange { + context: Point::new(22, 1)..Point::new(33, 1), + primary: None, + }, + ExcerptRange { + context: Point::new(44, 1)..Point::new(55, 1), + primary: None, + }, + ExcerptRange { + context: Point::new(56, 1)..Point::new(66, 1), + primary: None, + }, + ExcerptRange { + context: Point::new(67, 1)..Point::new(77, 1), + primary: None, + }, + ], + cx, + ); + multibuffer + }); + + cx.executor().run_until_parked(); + let editor = + cx.add_window(|cx| Editor::for_multibuffer(multibuffer, Some(project.clone()), cx)); + let editor_edited = Arc::new(AtomicBool::new(false)); + let fake_server = fake_servers.next().await.unwrap(); + let closure_editor_edited = Arc::clone(&editor_edited); + fake_server + .handle_request::(move |params, _| { + let task_editor_edited = Arc::clone(&closure_editor_edited); + async move { + let hint_text = if params.text_document.uri + == lsp::Url::from_file_path("/a/main.rs").unwrap() + { + "main hint" + } else if params.text_document.uri + == lsp::Url::from_file_path("/a/other.rs").unwrap() + { + "other hint" + } else { + panic!("unexpected uri: {:?}", params.text_document.uri); + }; + + // one hint per excerpt + let positions = [ + lsp::Position::new(0, 2), + lsp::Position::new(4, 2), + lsp::Position::new(22, 2), + lsp::Position::new(44, 2), + lsp::Position::new(56, 2), + lsp::Position::new(67, 2), + ]; + let out_of_range_hint = lsp::InlayHint { + position: lsp::Position::new( + params.range.start.line + 99, + params.range.start.character + 99, + ), + label: lsp::InlayHintLabel::String( + "out of excerpt range, should be ignored".to_string(), + ), + kind: None, + text_edits: None, + tooltip: None, + padding_left: None, + padding_right: None, + data: None, + }; + + let edited = task_editor_edited.load(Ordering::Acquire); + Ok(Some( + std::iter::once(out_of_range_hint) + .chain(positions.into_iter().enumerate().map(|(i, position)| { + lsp::InlayHint { + position, + label: lsp::InlayHintLabel::String(format!( + "{hint_text}{} #{i}", + if edited { "(edited)" } else { "" }, + )), + kind: None, + text_edits: None, + tooltip: None, + padding_left: None, + padding_right: None, + data: None, + } + })) + .collect(), + )) + } + }) + .next() + .await; + cx.executor().run_until_parked(); + + editor.update(cx, |editor, cx| { + let expected_hints = vec![ + "main hint #0".to_string(), + "main hint #1".to_string(), + "main hint #2".to_string(), + "main hint #3".to_string(), + // todo!() there used to be no these hints, but new gpui2 presumably scrolls a bit farther + // (or renders less?) note that tests below pass + "main hint #4".to_string(), + "main hint #5".to_string(), + ]; + assert_eq!( + expected_hints, + cached_hint_labels(editor), + "When scroll is at the edge of a multibuffer, its visible excerpts only should be queried for inlay hints" + ); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + assert_eq!(editor.inlay_hint_cache().version, expected_hints.len(), "Every visible excerpt hints should bump the verison"); + }); + + editor.update(cx, |editor, cx| { + editor.change_selections(Some(Autoscroll::Next), cx, |s| { + s.select_ranges([Point::new(4, 0)..Point::new(4, 0)]) + }); + editor.change_selections(Some(Autoscroll::Next), cx, |s| { + s.select_ranges([Point::new(22, 0)..Point::new(22, 0)]) + }); + editor.change_selections(Some(Autoscroll::Next), cx, |s| { + s.select_ranges([Point::new(50, 0)..Point::new(50, 0)]) + }); + }); + cx.executor().run_until_parked(); + editor.update(cx, |editor, cx| { + let expected_hints = vec![ + "main hint #0".to_string(), + "main hint #1".to_string(), + "main hint #2".to_string(), + "main hint #3".to_string(), + "main hint #4".to_string(), + "main hint #5".to_string(), + "other hint #0".to_string(), + "other hint #1".to_string(), + "other hint #2".to_string(), + ]; + assert_eq!(expected_hints, cached_hint_labels(editor), + "With more scrolls of the multibuffer, more hints should be added into the cache and nothing invalidated without edits"); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + assert_eq!(editor.inlay_hint_cache().version, expected_hints.len(), + "Due to every excerpt having one hint, we update cache per new excerpt scrolled"); + }); + + editor.update(cx, |editor, cx| { + editor.change_selections(Some(Autoscroll::Next), cx, |s| { + s.select_ranges([Point::new(100, 0)..Point::new(100, 0)]) + }); + }); + cx.executor().advance_clock(Duration::from_millis( + INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100, + )); + cx.executor().run_until_parked(); + let last_scroll_update_version = editor.update(cx, |editor, cx| { + let expected_hints = vec![ + "main hint #0".to_string(), + "main hint #1".to_string(), + "main hint #2".to_string(), + "main hint #3".to_string(), + "main hint #4".to_string(), + "main hint #5".to_string(), + "other hint #0".to_string(), + "other hint #1".to_string(), + "other hint #2".to_string(), + "other hint #3".to_string(), + "other hint #4".to_string(), + "other hint #5".to_string(), + ]; + assert_eq!(expected_hints, cached_hint_labels(editor), + "After multibuffer was scrolled to the end, all hints for all excerpts should be fetched"); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + assert_eq!(editor.inlay_hint_cache().version, expected_hints.len()); + expected_hints.len() + }).unwrap(); + + editor.update(cx, |editor, cx| { + editor.change_selections(Some(Autoscroll::Next), cx, |s| { + s.select_ranges([Point::new(4, 0)..Point::new(4, 0)]) + }); + }); + cx.executor().run_until_parked(); + editor.update(cx, |editor, cx| { + let expected_hints = vec![ + "main hint #0".to_string(), + "main hint #1".to_string(), + "main hint #2".to_string(), + "main hint #3".to_string(), + "main hint #4".to_string(), + "main hint #5".to_string(), + "other hint #0".to_string(), + "other hint #1".to_string(), + "other hint #2".to_string(), + "other hint #3".to_string(), + "other hint #4".to_string(), + "other hint #5".to_string(), + ]; + assert_eq!(expected_hints, cached_hint_labels(editor), + "After multibuffer was scrolled to the end, further scrolls up should not bring more hints"); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + assert_eq!(editor.inlay_hint_cache().version, last_scroll_update_version, "No updates should happen during scrolling already scolled buffer"); + }); + + editor_edited.store(true, Ordering::Release); + editor.update(cx, |editor, cx| { + editor.change_selections(None, cx, |s| { + s.select_ranges([Point::new(56, 0)..Point::new(56, 0)]) + }); + editor.handle_input("++++more text++++", cx); + }); + cx.executor().run_until_parked(); + editor.update(cx, |editor, cx| { + let expected_hints = vec![ + "main hint(edited) #0".to_string(), + "main hint(edited) #1".to_string(), + "main hint(edited) #2".to_string(), + "main hint(edited) #3".to_string(), + "main hint(edited) #4".to_string(), + "main hint(edited) #5".to_string(), + "other hint(edited) #0".to_string(), + "other hint(edited) #1".to_string(), + ]; + assert_eq!( + expected_hints, + cached_hint_labels(editor), + "After multibuffer edit, editor gets scolled back to the last selection; \ +all hints should be invalidated and requeried for all of its visible excerpts" + ); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + + let current_cache_version = editor.inlay_hint_cache().version; + let minimum_expected_version = last_scroll_update_version + expected_hints.len(); + assert!( + current_cache_version == minimum_expected_version || current_cache_version == minimum_expected_version + 1, + "Due to every excerpt having one hint, cache should update per new excerpt received + 1 potential sporadic update" + ); + }); + } + + #[gpui::test] + async fn test_excerpts_removed(cx: &mut gpui::TestAppContext) { + init_test(cx, |settings| { + settings.defaults.inlay_hints = Some(InlayHintSettings { + enabled: true, + show_type_hints: false, + show_parameter_hints: false, + show_other_hints: false, + }) + }); + + let mut language = Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ); + let mut fake_servers = language + .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + capabilities: lsp::ServerCapabilities { + inlay_hint_provider: Some(lsp::OneOf::Left(true)), + ..Default::default() + }, + ..Default::default() + })) + .await; + let language = Arc::new(language); + let fs = FakeFs::new(cx.background_executor.clone()); + fs.insert_tree( + "/a", + json!({ + "main.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|i| format!("let i = {i};\n")).collect::>().join("")), + "other.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|j| format!("let j = {j};\n")).collect::>().join("")), + }), + ) + .await; + let project = Project::test(fs, ["/a".as_ref()], cx).await; + project.update(cx, |project, _| { + project.languages().add(Arc::clone(&language)) + }); + let worktree_id = project.update(cx, |project, cx| { + project.worktrees().next().unwrap().read(cx).id() + }); + + let buffer_1 = project + .update(cx, |project, cx| { + project.open_buffer((worktree_id, "main.rs"), cx) + }) + .await + .unwrap(); + let buffer_2 = project + .update(cx, |project, cx| { + project.open_buffer((worktree_id, "other.rs"), cx) + }) + .await + .unwrap(); + let multibuffer = cx.build_model(|_| MultiBuffer::new(0)); + let (buffer_1_excerpts, buffer_2_excerpts) = multibuffer.update(cx, |multibuffer, cx| { + let buffer_1_excerpts = multibuffer.push_excerpts( + buffer_1.clone(), + [ExcerptRange { + context: Point::new(0, 0)..Point::new(2, 0), + primary: None, + }], + cx, + ); + let buffer_2_excerpts = multibuffer.push_excerpts( + buffer_2.clone(), + [ExcerptRange { + context: Point::new(0, 1)..Point::new(2, 1), + primary: None, + }], + cx, + ); + (buffer_1_excerpts, buffer_2_excerpts) + }); + + assert!(!buffer_1_excerpts.is_empty()); + assert!(!buffer_2_excerpts.is_empty()); + + cx.executor().run_until_parked(); + let editor = + cx.add_window(|cx| Editor::for_multibuffer(multibuffer, Some(project.clone()), cx)); + let editor_edited = Arc::new(AtomicBool::new(false)); + let fake_server = fake_servers.next().await.unwrap(); + let closure_editor_edited = Arc::clone(&editor_edited); + fake_server + .handle_request::(move |params, _| { + let task_editor_edited = Arc::clone(&closure_editor_edited); + async move { + let hint_text = if params.text_document.uri + == lsp::Url::from_file_path("/a/main.rs").unwrap() + { + "main hint" + } else if params.text_document.uri + == lsp::Url::from_file_path("/a/other.rs").unwrap() + { + "other hint" + } else { + panic!("unexpected uri: {:?}", params.text_document.uri); + }; + + let positions = [ + lsp::Position::new(0, 2), + lsp::Position::new(4, 2), + lsp::Position::new(22, 2), + lsp::Position::new(44, 2), + lsp::Position::new(56, 2), + lsp::Position::new(67, 2), + ]; + let out_of_range_hint = lsp::InlayHint { + position: lsp::Position::new( + params.range.start.line + 99, + params.range.start.character + 99, + ), + label: lsp::InlayHintLabel::String( + "out of excerpt range, should be ignored".to_string(), + ), + kind: None, + text_edits: None, + tooltip: None, + padding_left: None, + padding_right: None, + data: None, + }; + + let edited = task_editor_edited.load(Ordering::Acquire); + Ok(Some( + std::iter::once(out_of_range_hint) + .chain(positions.into_iter().enumerate().map(|(i, position)| { + lsp::InlayHint { + position, + label: lsp::InlayHintLabel::String(format!( + "{hint_text}{} #{i}", + if edited { "(edited)" } else { "" }, + )), + kind: None, + text_edits: None, + tooltip: None, + padding_left: None, + padding_right: None, + data: None, + } + })) + .collect(), + )) + } + }) + .next() + .await; + cx.executor().run_until_parked(); + + editor.update(cx, |editor, cx| { + assert_eq!( + vec!["main hint #0".to_string(), "other hint #0".to_string()], + cached_hint_labels(editor), + "Cache should update for both excerpts despite hints display was disabled" + ); + assert!( + visible_hint_labels(editor, cx).is_empty(), + "All hints are disabled and should not be shown despite being present in the cache" + ); + assert_eq!( + editor.inlay_hint_cache().version, + 2, + "Cache should update once per excerpt query" + ); + }); + + editor.update(cx, |editor, cx| { + editor.buffer().update(cx, |multibuffer, cx| { + multibuffer.remove_excerpts(buffer_2_excerpts, cx) + }) + }); + cx.executor().run_until_parked(); + editor.update(cx, |editor, cx| { + assert_eq!( + vec!["main hint #0".to_string()], + cached_hint_labels(editor), + "For the removed excerpt, should clean corresponding cached hints" + ); + assert!( + visible_hint_labels(editor, cx).is_empty(), + "All hints are disabled and should not be shown despite being present in the cache" + ); + assert_eq!( + editor.inlay_hint_cache().version, + 3, + "Excerpt removal should trigger a cache update" + ); + }); + + update_test_language_settings(cx, |settings| { + settings.defaults.inlay_hints = Some(InlayHintSettings { + enabled: true, + show_type_hints: true, + show_parameter_hints: true, + show_other_hints: true, + }) + }); + cx.executor().run_until_parked(); + editor.update(cx, |editor, cx| { + let expected_hints = vec!["main hint #0".to_string()]; + assert_eq!( + expected_hints, + cached_hint_labels(editor), + "Hint display settings change should not change the cache" + ); + assert_eq!( + expected_hints, + visible_hint_labels(editor, cx), + "Settings change should make cached hints visible" + ); + assert_eq!( + editor.inlay_hint_cache().version, + 4, + "Settings change should trigger a cache update" + ); + }); + } + + #[gpui::test] + async fn test_inside_char_boundary_range_hints(cx: &mut gpui::TestAppContext) { + init_test(cx, |settings| { + settings.defaults.inlay_hints = Some(InlayHintSettings { + enabled: true, + show_type_hints: true, + show_parameter_hints: true, + show_other_hints: true, + }) + }); + + let mut language = Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ); + let mut fake_servers = language + .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + capabilities: lsp::ServerCapabilities { + inlay_hint_provider: Some(lsp::OneOf::Left(true)), + ..Default::default() + }, + ..Default::default() + })) + .await; + let fs = FakeFs::new(cx.background_executor.clone()); + fs.insert_tree( + "/a", + json!({ + "main.rs": format!(r#"fn main() {{\n{}\n}}"#, format!("let i = {};\n", "√".repeat(10)).repeat(500)), + "other.rs": "// Test file", + }), + ) + .await; + let project = Project::test(fs, ["/a".as_ref()], cx).await; + project.update(cx, |project, _| project.languages().add(Arc::new(language))); + let buffer = project + .update(cx, |project, cx| { + project.open_local_buffer("/a/main.rs", cx) + }) + .await + .unwrap(); + cx.executor().run_until_parked(); + cx.executor().start_waiting(); + let fake_server = fake_servers.next().await.unwrap(); + let editor = cx.add_window(|cx| Editor::for_buffer(buffer, Some(project), cx)); + let lsp_request_count = Arc::new(AtomicU32::new(0)); + let closure_lsp_request_count = Arc::clone(&lsp_request_count); + fake_server + .handle_request::(move |params, _| { + let task_lsp_request_count = Arc::clone(&closure_lsp_request_count); + async move { + assert_eq!( + params.text_document.uri, + lsp::Url::from_file_path("/a/main.rs").unwrap(), + ); + let query_start = params.range.start; + let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::Release) + 1; + Ok(Some(vec![lsp::InlayHint { + position: query_start, + label: lsp::InlayHintLabel::String(i.to_string()), + kind: None, + text_edits: None, + tooltip: None, + padding_left: None, + padding_right: None, + data: None, + }])) + } + }) + .next() + .await; + + cx.executor().run_until_parked(); + editor.update(cx, |editor, cx| { + editor.change_selections(None, cx, |s| { + s.select_ranges([Point::new(10, 0)..Point::new(10, 0)]) + }) + }); + cx.executor().run_until_parked(); + editor.update(cx, |editor, cx| { + let expected_hints = vec!["1".to_string()]; + assert_eq!(expected_hints, cached_hint_labels(editor)); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + assert_eq!(editor.inlay_hint_cache().version, 1); + }); + } + + #[gpui::test] + async fn test_toggle_inlay_hints(cx: &mut gpui::TestAppContext) { + init_test(cx, |settings| { + settings.defaults.inlay_hints = Some(InlayHintSettings { + enabled: false, + show_type_hints: true, + show_parameter_hints: true, + show_other_hints: true, + }) + }); + + let (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await; + + editor.update(cx, |editor, cx| { + editor.toggle_inlay_hints(&crate::ToggleInlayHints, cx) + }); + cx.executor().start_waiting(); + let lsp_request_count = Arc::new(AtomicU32::new(0)); + let closure_lsp_request_count = Arc::clone(&lsp_request_count); + fake_server + .handle_request::(move |params, _| { + let task_lsp_request_count = Arc::clone(&closure_lsp_request_count); + async move { + assert_eq!( + params.text_document.uri, + lsp::Url::from_file_path(file_with_hints).unwrap(), + ); + + let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst) + 1; + Ok(Some(vec![lsp::InlayHint { + position: lsp::Position::new(0, i), + label: lsp::InlayHintLabel::String(i.to_string()), + kind: None, + text_edits: None, + tooltip: None, + padding_left: None, + padding_right: None, + data: None, + }])) + } + }) + .next() + .await; + cx.executor().run_until_parked(); + editor.update(cx, |editor, cx| { + let expected_hints = vec!["1".to_string()]; + assert_eq!( + expected_hints, + cached_hint_labels(editor), + "Should display inlays after toggle despite them disabled in settings" + ); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + assert_eq!( + editor.inlay_hint_cache().version, + 1, + "First toggle should be cache's first update" + ); + }); + + editor.update(cx, |editor, cx| { + editor.toggle_inlay_hints(&crate::ToggleInlayHints, cx) + }); + cx.executor().run_until_parked(); + editor.update(cx, |editor, cx| { + assert!( + cached_hint_labels(editor).is_empty(), + "Should clear hints after 2nd toggle" + ); + assert!(visible_hint_labels(editor, cx).is_empty()); + assert_eq!(editor.inlay_hint_cache().version, 2); + }); + + update_test_language_settings(cx, |settings| { + settings.defaults.inlay_hints = Some(InlayHintSettings { + enabled: true, + show_type_hints: true, + show_parameter_hints: true, + show_other_hints: true, + }) + }); + cx.executor().run_until_parked(); + editor.update(cx, |editor, cx| { + let expected_hints = vec!["2".to_string()]; + assert_eq!( + expected_hints, + cached_hint_labels(editor), + "Should query LSP hints for the 2nd time after enabling hints in settings" + ); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + assert_eq!(editor.inlay_hint_cache().version, 3); + }); + + editor.update(cx, |editor, cx| { + editor.toggle_inlay_hints(&crate::ToggleInlayHints, cx) + }); + cx.executor().run_until_parked(); + editor.update(cx, |editor, cx| { + assert!( + cached_hint_labels(editor).is_empty(), + "Should clear hints after enabling in settings and a 3rd toggle" + ); + assert!(visible_hint_labels(editor, cx).is_empty()); + assert_eq!(editor.inlay_hint_cache().version, 4); + }); + + editor.update(cx, |editor, cx| { + editor.toggle_inlay_hints(&crate::ToggleInlayHints, cx) + }); + cx.executor().run_until_parked(); + editor.update(cx, |editor, cx| { + let expected_hints = vec!["3".to_string()]; + assert_eq!( + expected_hints, + cached_hint_labels(editor), + "Should query LSP hints for the 3rd time after enabling hints in settings and toggling them back on" + ); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + assert_eq!(editor.inlay_hint_cache().version, 5); + }); + } + + pub(crate) fn init_test(cx: &mut TestAppContext, f: impl Fn(&mut AllLanguageSettingsContent)) { + cx.update(|cx| { + let settings_store = SettingsStore::test(cx); + cx.set_global(settings_store); + theme::init(cx); + client::init_settings(cx); + language::init(cx); + Project::init_settings(cx); + workspace::init_settings(cx); + crate::init(cx); + }); + + update_test_language_settings(cx, f); + } + + async fn prepare_test_objects( + cx: &mut TestAppContext, + ) -> (&'static str, WindowHandle, FakeLanguageServer) { + let mut language = Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ); + let mut fake_servers = language + .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + capabilities: lsp::ServerCapabilities { + inlay_hint_provider: Some(lsp::OneOf::Left(true)), + ..Default::default() + }, + ..Default::default() + })) + .await; + + let fs = FakeFs::new(cx.background_executor.clone()); + fs.insert_tree( + "/a", + json!({ + "main.rs": "fn main() { a } // and some long comment to ensure inlays are not trimmed out", + "other.rs": "// Test file", + }), + ) + .await; + + let project = Project::test(fs, ["/a".as_ref()], cx).await; + project.update(cx, |project, _| project.languages().add(Arc::new(language))); + let buffer = project + .update(cx, |project, cx| { + project.open_local_buffer("/a/main.rs", cx) + }) + .await + .unwrap(); + cx.executor().run_until_parked(); + cx.executor().start_waiting(); + let fake_server = fake_servers.next().await.unwrap(); + let editor = cx.add_window(|cx| Editor::for_buffer(buffer, Some(project), cx)); + + editor.update(cx, |editor, cx| { + assert!(cached_hint_labels(editor).is_empty()); + assert!(visible_hint_labels(editor, cx).is_empty()); + assert_eq!(editor.inlay_hint_cache().version, 0); + }); + + ("/a/main.rs", editor, fake_server) + } + + pub fn cached_hint_labels(editor: &Editor) -> Vec { + let mut labels = Vec::new(); + for (_, excerpt_hints) in &editor.inlay_hint_cache().hints { + let excerpt_hints = excerpt_hints.read(); + for id in &excerpt_hints.ordered_hints { + labels.push(excerpt_hints.hints_by_id[id].text()); + } + } + + labels.sort(); + labels + } + + pub fn visible_hint_labels(editor: &Editor, cx: &ViewContext<'_, Editor>) -> Vec { + let mut hints = editor + .visible_inlay_hints(cx) + .into_iter() + .map(|hint| hint.text.to_string()) + .collect::>(); + hints.sort(); + hints + } } -// let cached_excerpt_hints = editor -// .inlay_hint_cache -// .hints -// .entry(new_update.excerpt_id) -// .or_insert_with(|| { -// Arc::new(RwLock::new(CachedExcerptHints { -// version: query.cache_version, -// buffer_version: buffer_snapshot.version().clone(), -// buffer_id: query.buffer_id, -// ordered_hints: Vec::new(), -// hints_by_id: HashMap::default(), -// })) -// }); -// let mut cached_excerpt_hints = cached_excerpt_hints.write(); -// match query.cache_version.cmp(&cached_excerpt_hints.version) { -// cmp::Ordering::Less => return, -// cmp::Ordering::Greater | cmp::Ordering::Equal => { -// cached_excerpt_hints.version = query.cache_version; -// } -// } - -// let mut cached_inlays_changed = !new_update.remove_from_cache.is_empty(); -// cached_excerpt_hints -// .ordered_hints -// .retain(|hint_id| !new_update.remove_from_cache.contains(hint_id)); -// cached_excerpt_hints -// .hints_by_id -// .retain(|hint_id, _| !new_update.remove_from_cache.contains(hint_id)); -// let mut splice = InlaySplice { -// to_remove: new_update.remove_from_visible, -// to_insert: Vec::new(), -// }; -// for new_hint in new_update.add_to_cache { -// let insert_position = match cached_excerpt_hints -// .ordered_hints -// .binary_search_by(|probe| { -// cached_excerpt_hints.hints_by_id[probe] -// .position -// .cmp(&new_hint.position, &buffer_snapshot) -// }) { -// Ok(i) => { -// let mut insert_position = Some(i); -// for id in &cached_excerpt_hints.ordered_hints[i..] { -// let cached_hint = &cached_excerpt_hints.hints_by_id[id]; -// if new_hint -// .position -// .cmp(&cached_hint.position, &buffer_snapshot) -// .is_gt() -// { -// break; -// } -// if cached_hint.text() == new_hint.text() { -// insert_position = None; -// break; -// } -// } -// insert_position -// } -// Err(i) => Some(i), -// }; - -// if let Some(insert_position) = insert_position { -// let new_inlay_id = post_inc(&mut editor.next_inlay_id); -// if editor -// .inlay_hint_cache -// .allowed_hint_kinds -// .contains(&new_hint.kind) -// { -// let new_hint_position = -// multi_buffer_snapshot.anchor_in_excerpt(query.excerpt_id, new_hint.position); -// splice -// .to_insert -// .push(Inlay::hint(new_inlay_id, new_hint_position, &new_hint)); -// } -// let new_id = InlayId::Hint(new_inlay_id); -// cached_excerpt_hints.hints_by_id.insert(new_id, new_hint); -// cached_excerpt_hints -// .ordered_hints -// .insert(insert_position, new_id); -// cached_inlays_changed = true; -// } -// } -// cached_excerpt_hints.buffer_version = buffer_snapshot.version().clone(); -// drop(cached_excerpt_hints); - -// if invalidate { -// let mut outdated_excerpt_caches = HashSet::default(); -// for (excerpt_id, excerpt_hints) in &editor.inlay_hint_cache().hints { -// let excerpt_hints = excerpt_hints.read(); -// if excerpt_hints.buffer_id == query.buffer_id -// && excerpt_id != &query.excerpt_id -// && buffer_snapshot -// .version() -// .changed_since(&excerpt_hints.buffer_version) -// { -// outdated_excerpt_caches.insert(*excerpt_id); -// splice -// .to_remove -// .extend(excerpt_hints.ordered_hints.iter().copied()); -// } -// } -// cached_inlays_changed |= !outdated_excerpt_caches.is_empty(); -// editor -// .inlay_hint_cache -// .hints -// .retain(|excerpt_id, _| !outdated_excerpt_caches.contains(excerpt_id)); -// } - -// let InlaySplice { -// to_remove, -// to_insert, -// } = splice; -// let displayed_inlays_changed = !to_remove.is_empty() || !to_insert.is_empty(); -// if cached_inlays_changed || displayed_inlays_changed { -// editor.inlay_hint_cache.version += 1; -// } -// if displayed_inlays_changed { -// editor.splice_inlay_hints(to_remove, to_insert, cx) -// } -// } - -// #[cfg(test)] -// pub mod tests { -// use std::sync::atomic::{AtomicBool, AtomicU32, AtomicUsize, Ordering}; - -// use crate::{ -// scroll::{autoscroll::Autoscroll, scroll_amount::ScrollAmount}, -// serde_json::json, -// ExcerptRange, -// }; -// use futures::StreamExt; -// use gpui::{executor::Deterministic, TestAppContext, View}; -// use itertools::Itertools; -// use language::{ -// language_settings::AllLanguageSettingsContent, FakeLspAdapter, Language, LanguageConfig, -// }; -// use lsp::FakeLanguageServer; -// use parking_lot::Mutex; -// use project::{FakeFs, Project}; -// use settings::SettingsStore; -// use text::{Point, ToPoint}; -// use workspace::Workspace; - -// use crate::editor_tests::update_test_language_settings; - -// use super::*; - -// #[gpui::test] -// async fn test_basic_cache_update_with_duplicate_hints(cx: &mut gpui::TestAppContext) { -// let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]); -// init_test(cx, |settings| { -// settings.defaults.inlay_hints = Some(InlayHintSettings { -// enabled: true, -// show_type_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Type)), -// show_parameter_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)), -// show_other_hints: allowed_hint_kinds.contains(&None), -// }) -// }); - -// let (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await; -// let lsp_request_count = Arc::new(AtomicU32::new(0)); -// fake_server -// .handle_request::(move |params, _| { -// let task_lsp_request_count = Arc::clone(&lsp_request_count); -// async move { -// assert_eq!( -// params.text_document.uri, -// lsp::Url::from_file_path(file_with_hints).unwrap(), -// ); -// let current_call_id = -// Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst); -// let mut new_hints = Vec::with_capacity(2 * current_call_id as usize); -// for _ in 0..2 { -// let mut i = current_call_id; -// loop { -// new_hints.push(lsp::InlayHint { -// position: lsp::Position::new(0, i), -// label: lsp::InlayHintLabel::String(i.to_string()), -// kind: None, -// text_edits: None, -// tooltip: None, -// padding_left: None, -// padding_right: None, -// data: None, -// }); -// if i == 0 { -// break; -// } -// i -= 1; -// } -// } - -// Ok(Some(new_hints)) -// } -// }) -// .next() -// .await; -// cx.foreground().run_until_parked(); - -// let mut edits_made = 1; -// editor.update(cx, |editor, cx| { -// let expected_hints = vec!["0".to_string()]; -// assert_eq!( -// expected_hints, -// cached_hint_labels(editor), -// "Should get its first hints when opening the editor" -// ); -// assert_eq!(expected_hints, visible_hint_labels(editor, cx)); -// let inlay_cache = editor.inlay_hint_cache(); -// assert_eq!( -// inlay_cache.allowed_hint_kinds, allowed_hint_kinds, -// "Cache should use editor settings to get the allowed hint kinds" -// ); -// assert_eq!( -// inlay_cache.version, edits_made, -// "The editor update the cache version after every cache/view change" -// ); -// }); - -// editor.update(cx, |editor, cx| { -// editor.change_selections(None, cx, |s| s.select_ranges([13..13])); -// editor.handle_input("some change", cx); -// edits_made += 1; -// }); -// cx.foreground().run_until_parked(); -// editor.update(cx, |editor, cx| { -// let expected_hints = vec!["0".to_string(), "1".to_string()]; -// assert_eq!( -// expected_hints, -// cached_hint_labels(editor), -// "Should get new hints after an edit" -// ); -// assert_eq!(expected_hints, visible_hint_labels(editor, cx)); -// let inlay_cache = editor.inlay_hint_cache(); -// assert_eq!( -// inlay_cache.allowed_hint_kinds, allowed_hint_kinds, -// "Cache should use editor settings to get the allowed hint kinds" -// ); -// assert_eq!( -// inlay_cache.version, edits_made, -// "The editor update the cache version after every cache/view change" -// ); -// }); - -// fake_server -// .request::(()) -// .await -// .expect("inlay refresh request failed"); -// edits_made += 1; -// cx.foreground().run_until_parked(); -// editor.update(cx, |editor, cx| { -// let expected_hints = vec!["0".to_string(), "1".to_string(), "2".to_string()]; -// assert_eq!( -// expected_hints, -// cached_hint_labels(editor), -// "Should get new hints after hint refresh/ request" -// ); -// assert_eq!(expected_hints, visible_hint_labels(editor, cx)); -// let inlay_cache = editor.inlay_hint_cache(); -// assert_eq!( -// inlay_cache.allowed_hint_kinds, allowed_hint_kinds, -// "Cache should use editor settings to get the allowed hint kinds" -// ); -// assert_eq!( -// inlay_cache.version, edits_made, -// "The editor update the cache version after every cache/view change" -// ); -// }); -// } - -// #[gpui::test] -// async fn test_cache_update_on_lsp_completion_tasks(cx: &mut gpui::TestAppContext) { -// init_test(cx, |settings| { -// settings.defaults.inlay_hints = Some(InlayHintSettings { -// enabled: true, -// show_type_hints: true, -// show_parameter_hints: true, -// show_other_hints: true, -// }) -// }); - -// let (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await; -// let lsp_request_count = Arc::new(AtomicU32::new(0)); -// fake_server -// .handle_request::(move |params, _| { -// let task_lsp_request_count = Arc::clone(&lsp_request_count); -// async move { -// assert_eq!( -// params.text_document.uri, -// lsp::Url::from_file_path(file_with_hints).unwrap(), -// ); -// let current_call_id = -// Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst); -// Ok(Some(vec![lsp::InlayHint { -// position: lsp::Position::new(0, current_call_id), -// label: lsp::InlayHintLabel::String(current_call_id.to_string()), -// kind: None, -// text_edits: None, -// tooltip: None, -// padding_left: None, -// padding_right: None, -// data: None, -// }])) -// } -// }) -// .next() -// .await; -// cx.foreground().run_until_parked(); - -// let mut edits_made = 1; -// editor.update(cx, |editor, cx| { -// let expected_hints = vec!["0".to_string()]; -// assert_eq!( -// expected_hints, -// cached_hint_labels(editor), -// "Should get its first hints when opening the editor" -// ); -// assert_eq!(expected_hints, visible_hint_labels(editor, cx)); -// assert_eq!( -// editor.inlay_hint_cache().version, -// edits_made, -// "The editor update the cache version after every cache/view change" -// ); -// }); - -// let progress_token = "test_progress_token"; -// fake_server -// .request::(lsp::WorkDoneProgressCreateParams { -// token: lsp::ProgressToken::String(progress_token.to_string()), -// }) -// .await -// .expect("work done progress create request failed"); -// cx.foreground().run_until_parked(); -// fake_server.notify::(lsp::ProgressParams { -// token: lsp::ProgressToken::String(progress_token.to_string()), -// value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Begin( -// lsp::WorkDoneProgressBegin::default(), -// )), -// }); -// cx.foreground().run_until_parked(); - -// editor.update(cx, |editor, cx| { -// let expected_hints = vec!["0".to_string()]; -// assert_eq!( -// expected_hints, -// cached_hint_labels(editor), -// "Should not update hints while the work task is running" -// ); -// assert_eq!(expected_hints, visible_hint_labels(editor, cx)); -// assert_eq!( -// editor.inlay_hint_cache().version, -// edits_made, -// "Should not update the cache while the work task is running" -// ); -// }); - -// fake_server.notify::(lsp::ProgressParams { -// token: lsp::ProgressToken::String(progress_token.to_string()), -// value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::End( -// lsp::WorkDoneProgressEnd::default(), -// )), -// }); -// cx.foreground().run_until_parked(); - -// edits_made += 1; -// editor.update(cx, |editor, cx| { -// let expected_hints = vec!["1".to_string()]; -// assert_eq!( -// expected_hints, -// cached_hint_labels(editor), -// "New hints should be queried after the work task is done" -// ); -// assert_eq!(expected_hints, visible_hint_labels(editor, cx)); -// assert_eq!( -// editor.inlay_hint_cache().version, -// edits_made, -// "Cache version should udpate once after the work task is done" -// ); -// }); -// } - -// #[gpui::test] -// async fn test_no_hint_updates_for_unrelated_language_files(cx: &mut gpui::TestAppContext) { -// init_test(cx, |settings| { -// settings.defaults.inlay_hints = Some(InlayHintSettings { -// enabled: true, -// show_type_hints: true, -// show_parameter_hints: true, -// show_other_hints: true, -// }) -// }); - -// let fs = FakeFs::new(cx.background()); -// fs.insert_tree( -// "/a", -// json!({ -// "main.rs": "fn main() { a } // and some long comment to ensure inlays are not trimmed out", -// "other.md": "Test md file with some text", -// }), -// ) -// .await; -// let project = Project::test(fs, ["/a".as_ref()], cx).await; -// let workspace = cx -// .add_window(|cx| Workspace::test_new(project.clone(), cx)) -// .root(cx); -// let worktree_id = workspace.update(cx, |workspace, cx| { -// workspace.project().read_with(cx, |project, cx| { -// project.worktrees(cx).next().unwrap().read(cx).id() -// }) -// }); - -// let mut rs_fake_servers = None; -// let mut md_fake_servers = None; -// for (name, path_suffix) in [("Rust", "rs"), ("Markdown", "md")] { -// let mut language = Language::new( -// LanguageConfig { -// name: name.into(), -// path_suffixes: vec![path_suffix.to_string()], -// ..Default::default() -// }, -// Some(tree_sitter_rust::language()), -// ); -// let fake_servers = language -// .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { -// name, -// capabilities: lsp::ServerCapabilities { -// inlay_hint_provider: Some(lsp::OneOf::Left(true)), -// ..Default::default() -// }, -// ..Default::default() -// })) -// .await; -// match name { -// "Rust" => rs_fake_servers = Some(fake_servers), -// "Markdown" => md_fake_servers = Some(fake_servers), -// _ => unreachable!(), -// } -// project.update(cx, |project, _| { -// project.languages().add(Arc::new(language)); -// }); -// } - -// let _rs_buffer = project -// .update(cx, |project, cx| { -// project.open_local_buffer("/a/main.rs", cx) -// }) -// .await -// .unwrap(); -// cx.foreground().run_until_parked(); -// cx.foreground().start_waiting(); -// let rs_fake_server = rs_fake_servers.unwrap().next().await.unwrap(); -// let rs_editor = workspace -// .update(cx, |workspace, cx| { -// workspace.open_path((worktree_id, "main.rs"), None, true, cx) -// }) -// .await -// .unwrap() -// .downcast::() -// .unwrap(); -// let rs_lsp_request_count = Arc::new(AtomicU32::new(0)); -// rs_fake_server -// .handle_request::(move |params, _| { -// let task_lsp_request_count = Arc::clone(&rs_lsp_request_count); -// async move { -// assert_eq!( -// params.text_document.uri, -// lsp::Url::from_file_path("/a/main.rs").unwrap(), -// ); -// let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst); -// Ok(Some(vec![lsp::InlayHint { -// position: lsp::Position::new(0, i), -// label: lsp::InlayHintLabel::String(i.to_string()), -// kind: None, -// text_edits: None, -// tooltip: None, -// padding_left: None, -// padding_right: None, -// data: None, -// }])) -// } -// }) -// .next() -// .await; -// cx.foreground().run_until_parked(); -// rs_editor.update(cx, |editor, cx| { -// let expected_hints = vec!["0".to_string()]; -// assert_eq!( -// expected_hints, -// cached_hint_labels(editor), -// "Should get its first hints when opening the editor" -// ); -// assert_eq!(expected_hints, visible_hint_labels(editor, cx)); -// assert_eq!( -// editor.inlay_hint_cache().version, -// 1, -// "Rust editor update the cache version after every cache/view change" -// ); -// }); - -// cx.foreground().run_until_parked(); -// let _md_buffer = project -// .update(cx, |project, cx| { -// project.open_local_buffer("/a/other.md", cx) -// }) -// .await -// .unwrap(); -// cx.foreground().run_until_parked(); -// cx.foreground().start_waiting(); -// let md_fake_server = md_fake_servers.unwrap().next().await.unwrap(); -// let md_editor = workspace -// .update(cx, |workspace, cx| { -// workspace.open_path((worktree_id, "other.md"), None, true, cx) -// }) -// .await -// .unwrap() -// .downcast::() -// .unwrap(); -// let md_lsp_request_count = Arc::new(AtomicU32::new(0)); -// md_fake_server -// .handle_request::(move |params, _| { -// let task_lsp_request_count = Arc::clone(&md_lsp_request_count); -// async move { -// assert_eq!( -// params.text_document.uri, -// lsp::Url::from_file_path("/a/other.md").unwrap(), -// ); -// let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst); -// Ok(Some(vec![lsp::InlayHint { -// position: lsp::Position::new(0, i), -// label: lsp::InlayHintLabel::String(i.to_string()), -// kind: None, -// text_edits: None, -// tooltip: None, -// padding_left: None, -// padding_right: None, -// data: None, -// }])) -// } -// }) -// .next() -// .await; -// cx.foreground().run_until_parked(); -// md_editor.update(cx, |editor, cx| { -// let expected_hints = vec!["0".to_string()]; -// assert_eq!( -// expected_hints, -// cached_hint_labels(editor), -// "Markdown editor should have a separate verison, repeating Rust editor rules" -// ); -// assert_eq!(expected_hints, visible_hint_labels(editor, cx)); -// assert_eq!(editor.inlay_hint_cache().version, 1); -// }); - -// rs_editor.update(cx, |editor, cx| { -// editor.change_selections(None, cx, |s| s.select_ranges([13..13])); -// editor.handle_input("some rs change", cx); -// }); -// cx.foreground().run_until_parked(); -// rs_editor.update(cx, |editor, cx| { -// let expected_hints = vec!["1".to_string()]; -// assert_eq!( -// expected_hints, -// cached_hint_labels(editor), -// "Rust inlay cache should change after the edit" -// ); -// assert_eq!(expected_hints, visible_hint_labels(editor, cx)); -// assert_eq!( -// editor.inlay_hint_cache().version, -// 2, -// "Every time hint cache changes, cache version should be incremented" -// ); -// }); -// md_editor.update(cx, |editor, cx| { -// let expected_hints = vec!["0".to_string()]; -// assert_eq!( -// expected_hints, -// cached_hint_labels(editor), -// "Markdown editor should not be affected by Rust editor changes" -// ); -// assert_eq!(expected_hints, visible_hint_labels(editor, cx)); -// assert_eq!(editor.inlay_hint_cache().version, 1); -// }); - -// md_editor.update(cx, |editor, cx| { -// editor.change_selections(None, cx, |s| s.select_ranges([13..13])); -// editor.handle_input("some md change", cx); -// }); -// cx.foreground().run_until_parked(); -// md_editor.update(cx, |editor, cx| { -// let expected_hints = vec!["1".to_string()]; -// assert_eq!( -// expected_hints, -// cached_hint_labels(editor), -// "Rust editor should not be affected by Markdown editor changes" -// ); -// assert_eq!(expected_hints, visible_hint_labels(editor, cx)); -// assert_eq!(editor.inlay_hint_cache().version, 2); -// }); -// rs_editor.update(cx, |editor, cx| { -// let expected_hints = vec!["1".to_string()]; -// assert_eq!( -// expected_hints, -// cached_hint_labels(editor), -// "Markdown editor should also change independently" -// ); -// assert_eq!(expected_hints, visible_hint_labels(editor, cx)); -// assert_eq!(editor.inlay_hint_cache().version, 2); -// }); -// } - -// #[gpui::test] -// async fn test_hint_setting_changes(cx: &mut gpui::TestAppContext) { -// let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]); -// init_test(cx, |settings| { -// settings.defaults.inlay_hints = Some(InlayHintSettings { -// enabled: true, -// show_type_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Type)), -// show_parameter_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)), -// show_other_hints: allowed_hint_kinds.contains(&None), -// }) -// }); - -// let (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await; -// let lsp_request_count = Arc::new(AtomicU32::new(0)); -// let another_lsp_request_count = Arc::clone(&lsp_request_count); -// fake_server -// .handle_request::(move |params, _| { -// let task_lsp_request_count = Arc::clone(&another_lsp_request_count); -// async move { -// Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst); -// assert_eq!( -// params.text_document.uri, -// lsp::Url::from_file_path(file_with_hints).unwrap(), -// ); -// Ok(Some(vec![ -// lsp::InlayHint { -// position: lsp::Position::new(0, 1), -// label: lsp::InlayHintLabel::String("type hint".to_string()), -// kind: Some(lsp::InlayHintKind::TYPE), -// text_edits: None, -// tooltip: None, -// padding_left: None, -// padding_right: None, -// data: None, -// }, -// lsp::InlayHint { -// position: lsp::Position::new(0, 2), -// label: lsp::InlayHintLabel::String("parameter hint".to_string()), -// kind: Some(lsp::InlayHintKind::PARAMETER), -// text_edits: None, -// tooltip: None, -// padding_left: None, -// padding_right: None, -// data: None, -// }, -// lsp::InlayHint { -// position: lsp::Position::new(0, 3), -// label: lsp::InlayHintLabel::String("other hint".to_string()), -// kind: None, -// text_edits: None, -// tooltip: None, -// padding_left: None, -// padding_right: None, -// data: None, -// }, -// ])) -// } -// }) -// .next() -// .await; -// cx.foreground().run_until_parked(); - -// let mut edits_made = 1; -// editor.update(cx, |editor, cx| { -// assert_eq!( -// lsp_request_count.load(Ordering::Relaxed), -// 1, -// "Should query new hints once" -// ); -// assert_eq!( -// vec![ -// "other hint".to_string(), -// "parameter hint".to_string(), -// "type hint".to_string(), -// ], -// cached_hint_labels(editor), -// "Should get its first hints when opening the editor" -// ); -// assert_eq!( -// vec!["other hint".to_string(), "type hint".to_string()], -// visible_hint_labels(editor, cx) -// ); -// let inlay_cache = editor.inlay_hint_cache(); -// assert_eq!( -// inlay_cache.allowed_hint_kinds, allowed_hint_kinds, -// "Cache should use editor settings to get the allowed hint kinds" -// ); -// assert_eq!( -// inlay_cache.version, edits_made, -// "The editor update the cache version after every cache/view change" -// ); -// }); - -// fake_server -// .request::(()) -// .await -// .expect("inlay refresh request failed"); -// cx.foreground().run_until_parked(); -// editor.update(cx, |editor, cx| { -// assert_eq!( -// lsp_request_count.load(Ordering::Relaxed), -// 2, -// "Should load new hints twice" -// ); -// assert_eq!( -// vec![ -// "other hint".to_string(), -// "parameter hint".to_string(), -// "type hint".to_string(), -// ], -// cached_hint_labels(editor), -// "Cached hints should not change due to allowed hint kinds settings update" -// ); -// assert_eq!( -// vec!["other hint".to_string(), "type hint".to_string()], -// visible_hint_labels(editor, cx) -// ); -// assert_eq!( -// editor.inlay_hint_cache().version, -// edits_made, -// "Should not update cache version due to new loaded hints being the same" -// ); -// }); - -// for (new_allowed_hint_kinds, expected_visible_hints) in [ -// (HashSet::from_iter([None]), vec!["other hint".to_string()]), -// ( -// HashSet::from_iter([Some(InlayHintKind::Type)]), -// vec!["type hint".to_string()], -// ), -// ( -// HashSet::from_iter([Some(InlayHintKind::Parameter)]), -// vec!["parameter hint".to_string()], -// ), -// ( -// HashSet::from_iter([None, Some(InlayHintKind::Type)]), -// vec!["other hint".to_string(), "type hint".to_string()], -// ), -// ( -// HashSet::from_iter([None, Some(InlayHintKind::Parameter)]), -// vec!["other hint".to_string(), "parameter hint".to_string()], -// ), -// ( -// HashSet::from_iter([Some(InlayHintKind::Type), Some(InlayHintKind::Parameter)]), -// vec!["parameter hint".to_string(), "type hint".to_string()], -// ), -// ( -// HashSet::from_iter([ -// None, -// Some(InlayHintKind::Type), -// Some(InlayHintKind::Parameter), -// ]), -// vec![ -// "other hint".to_string(), -// "parameter hint".to_string(), -// "type hint".to_string(), -// ], -// ), -// ] { -// edits_made += 1; -// update_test_language_settings(cx, |settings| { -// settings.defaults.inlay_hints = Some(InlayHintSettings { -// enabled: true, -// show_type_hints: new_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)), -// show_parameter_hints: new_allowed_hint_kinds -// .contains(&Some(InlayHintKind::Parameter)), -// show_other_hints: new_allowed_hint_kinds.contains(&None), -// }) -// }); -// cx.foreground().run_until_parked(); -// editor.update(cx, |editor, cx| { -// assert_eq!( -// lsp_request_count.load(Ordering::Relaxed), -// 2, -// "Should not load new hints on allowed hint kinds change for hint kinds {new_allowed_hint_kinds:?}" -// ); -// assert_eq!( -// vec![ -// "other hint".to_string(), -// "parameter hint".to_string(), -// "type hint".to_string(), -// ], -// cached_hint_labels(editor), -// "Should get its cached hints unchanged after the settings change for hint kinds {new_allowed_hint_kinds:?}" -// ); -// assert_eq!( -// expected_visible_hints, -// visible_hint_labels(editor, cx), -// "Should get its visible hints filtered after the settings change for hint kinds {new_allowed_hint_kinds:?}" -// ); -// let inlay_cache = editor.inlay_hint_cache(); -// assert_eq!( -// inlay_cache.allowed_hint_kinds, new_allowed_hint_kinds, -// "Cache should use editor settings to get the allowed hint kinds for hint kinds {new_allowed_hint_kinds:?}" -// ); -// assert_eq!( -// inlay_cache.version, edits_made, -// "The editor should update the cache version after every cache/view change for hint kinds {new_allowed_hint_kinds:?} due to visible hints change" -// ); -// }); -// } - -// edits_made += 1; -// let another_allowed_hint_kinds = HashSet::from_iter([Some(InlayHintKind::Type)]); -// update_test_language_settings(cx, |settings| { -// settings.defaults.inlay_hints = Some(InlayHintSettings { -// enabled: false, -// show_type_hints: another_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)), -// show_parameter_hints: another_allowed_hint_kinds -// .contains(&Some(InlayHintKind::Parameter)), -// show_other_hints: another_allowed_hint_kinds.contains(&None), -// }) -// }); -// cx.foreground().run_until_parked(); -// editor.update(cx, |editor, cx| { -// assert_eq!( -// lsp_request_count.load(Ordering::Relaxed), -// 2, -// "Should not load new hints when hints got disabled" -// ); -// assert!( -// cached_hint_labels(editor).is_empty(), -// "Should clear the cache when hints got disabled" -// ); -// assert!( -// visible_hint_labels(editor, cx).is_empty(), -// "Should clear visible hints when hints got disabled" -// ); -// let inlay_cache = editor.inlay_hint_cache(); -// assert_eq!( -// inlay_cache.allowed_hint_kinds, another_allowed_hint_kinds, -// "Should update its allowed hint kinds even when hints got disabled" -// ); -// assert_eq!( -// inlay_cache.version, edits_made, -// "The editor should update the cache version after hints got disabled" -// ); -// }); - -// fake_server -// .request::(()) -// .await -// .expect("inlay refresh request failed"); -// cx.foreground().run_until_parked(); -// editor.update(cx, |editor, cx| { -// assert_eq!( -// lsp_request_count.load(Ordering::Relaxed), -// 2, -// "Should not load new hints when they got disabled" -// ); -// assert!(cached_hint_labels(editor).is_empty()); -// assert!(visible_hint_labels(editor, cx).is_empty()); -// assert_eq!( -// editor.inlay_hint_cache().version, edits_made, -// "The editor should not update the cache version after /refresh query without updates" -// ); -// }); - -// let final_allowed_hint_kinds = HashSet::from_iter([Some(InlayHintKind::Parameter)]); -// edits_made += 1; -// update_test_language_settings(cx, |settings| { -// settings.defaults.inlay_hints = Some(InlayHintSettings { -// enabled: true, -// show_type_hints: final_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)), -// show_parameter_hints: final_allowed_hint_kinds -// .contains(&Some(InlayHintKind::Parameter)), -// show_other_hints: final_allowed_hint_kinds.contains(&None), -// }) -// }); -// cx.foreground().run_until_parked(); -// editor.update(cx, |editor, cx| { -// assert_eq!( -// lsp_request_count.load(Ordering::Relaxed), -// 3, -// "Should query for new hints when they got reenabled" -// ); -// assert_eq!( -// vec![ -// "other hint".to_string(), -// "parameter hint".to_string(), -// "type hint".to_string(), -// ], -// cached_hint_labels(editor), -// "Should get its cached hints fully repopulated after the hints got reenabled" -// ); -// assert_eq!( -// vec!["parameter hint".to_string()], -// visible_hint_labels(editor, cx), -// "Should get its visible hints repopulated and filtered after the h" -// ); -// let inlay_cache = editor.inlay_hint_cache(); -// assert_eq!( -// inlay_cache.allowed_hint_kinds, final_allowed_hint_kinds, -// "Cache should update editor settings when hints got reenabled" -// ); -// assert_eq!( -// inlay_cache.version, edits_made, -// "Cache should update its version after hints got reenabled" -// ); -// }); - -// fake_server -// .request::(()) -// .await -// .expect("inlay refresh request failed"); -// cx.foreground().run_until_parked(); -// editor.update(cx, |editor, cx| { -// assert_eq!( -// lsp_request_count.load(Ordering::Relaxed), -// 4, -// "Should query for new hints again" -// ); -// assert_eq!( -// vec![ -// "other hint".to_string(), -// "parameter hint".to_string(), -// "type hint".to_string(), -// ], -// cached_hint_labels(editor), -// ); -// assert_eq!( -// vec!["parameter hint".to_string()], -// visible_hint_labels(editor, cx), -// ); -// assert_eq!(editor.inlay_hint_cache().version, edits_made); -// }); -// } - -// #[gpui::test] -// async fn test_hint_request_cancellation(cx: &mut gpui::TestAppContext) { -// init_test(cx, |settings| { -// settings.defaults.inlay_hints = Some(InlayHintSettings { -// enabled: true, -// show_type_hints: true, -// show_parameter_hints: true, -// show_other_hints: true, -// }) -// }); - -// let (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await; -// let fake_server = Arc::new(fake_server); -// let lsp_request_count = Arc::new(AtomicU32::new(0)); -// let another_lsp_request_count = Arc::clone(&lsp_request_count); -// fake_server -// .handle_request::(move |params, _| { -// let task_lsp_request_count = Arc::clone(&another_lsp_request_count); -// async move { -// let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst) + 1; -// assert_eq!( -// params.text_document.uri, -// lsp::Url::from_file_path(file_with_hints).unwrap(), -// ); -// Ok(Some(vec![lsp::InlayHint { -// position: lsp::Position::new(0, i), -// label: lsp::InlayHintLabel::String(i.to_string()), -// kind: None, -// text_edits: None, -// tooltip: None, -// padding_left: None, -// padding_right: None, -// data: None, -// }])) -// } -// }) -// .next() -// .await; - -// let mut expected_changes = Vec::new(); -// for change_after_opening in [ -// "initial change #1", -// "initial change #2", -// "initial change #3", -// ] { -// editor.update(cx, |editor, cx| { -// editor.change_selections(None, cx, |s| s.select_ranges([13..13])); -// editor.handle_input(change_after_opening, cx); -// }); -// expected_changes.push(change_after_opening); -// } - -// cx.foreground().run_until_parked(); - -// editor.update(cx, |editor, cx| { -// let current_text = editor.text(cx); -// for change in &expected_changes { -// assert!( -// current_text.contains(change), -// "Should apply all changes made" -// ); -// } -// assert_eq!( -// lsp_request_count.load(Ordering::Relaxed), -// 2, -// "Should query new hints twice: for editor init and for the last edit that interrupted all others" -// ); -// let expected_hints = vec!["2".to_string()]; -// assert_eq!( -// expected_hints, -// cached_hint_labels(editor), -// "Should get hints from the last edit landed only" -// ); -// assert_eq!(expected_hints, visible_hint_labels(editor, cx)); -// assert_eq!( -// editor.inlay_hint_cache().version, 1, -// "Only one update should be registered in the cache after all cancellations" -// ); -// }); - -// let mut edits = Vec::new(); -// for async_later_change in [ -// "another change #1", -// "another change #2", -// "another change #3", -// ] { -// expected_changes.push(async_later_change); -// let task_editor = editor.clone(); -// let mut task_cx = cx.clone(); -// edits.push(cx.foreground().spawn(async move { -// task_editor.update(&mut task_cx, |editor, cx| { -// editor.change_selections(None, cx, |s| s.select_ranges([13..13])); -// editor.handle_input(async_later_change, cx); -// }); -// })); -// } -// let _ = future::join_all(edits).await; -// cx.foreground().run_until_parked(); - -// editor.update(cx, |editor, cx| { -// let current_text = editor.text(cx); -// for change in &expected_changes { -// assert!( -// current_text.contains(change), -// "Should apply all changes made" -// ); -// } -// assert_eq!( -// lsp_request_count.load(Ordering::SeqCst), -// 3, -// "Should query new hints one more time, for the last edit only" -// ); -// let expected_hints = vec!["3".to_string()]; -// assert_eq!( -// expected_hints, -// cached_hint_labels(editor), -// "Should get hints from the last edit landed only" -// ); -// assert_eq!(expected_hints, visible_hint_labels(editor, cx)); -// assert_eq!( -// editor.inlay_hint_cache().version, -// 2, -// "Should update the cache version once more, for the new change" -// ); -// }); -// } - -// #[gpui::test(iterations = 10)] -// async fn test_large_buffer_inlay_requests_split(cx: &mut gpui::TestAppContext) { -// init_test(cx, |settings| { -// settings.defaults.inlay_hints = Some(InlayHintSettings { -// enabled: true, -// show_type_hints: true, -// show_parameter_hints: true, -// show_other_hints: true, -// }) -// }); - -// let mut language = Language::new( -// LanguageConfig { -// name: "Rust".into(), -// path_suffixes: vec!["rs".to_string()], -// ..Default::default() -// }, -// Some(tree_sitter_rust::language()), -// ); -// let mut fake_servers = language -// .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { -// capabilities: lsp::ServerCapabilities { -// inlay_hint_provider: Some(lsp::OneOf::Left(true)), -// ..Default::default() -// }, -// ..Default::default() -// })) -// .await; -// let fs = FakeFs::new(cx.background()); -// fs.insert_tree( -// "/a", -// json!({ -// "main.rs": format!("fn main() {{\n{}\n}}", "let i = 5;\n".repeat(500)), -// "other.rs": "// Test file", -// }), -// ) -// .await; -// let project = Project::test(fs, ["/a".as_ref()], cx).await; -// project.update(cx, |project, _| project.languages().add(Arc::new(language))); -// let workspace = cx -// .add_window(|cx| Workspace::test_new(project.clone(), cx)) -// .root(cx); -// let worktree_id = workspace.update(cx, |workspace, cx| { -// workspace.project().read_with(cx, |project, cx| { -// project.worktrees(cx).next().unwrap().read(cx).id() -// }) -// }); - -// let _buffer = project -// .update(cx, |project, cx| { -// project.open_local_buffer("/a/main.rs", cx) -// }) -// .await -// .unwrap(); -// cx.foreground().run_until_parked(); -// cx.foreground().start_waiting(); -// let fake_server = fake_servers.next().await.unwrap(); -// let editor = workspace -// .update(cx, |workspace, cx| { -// workspace.open_path((worktree_id, "main.rs"), None, true, cx) -// }) -// .await -// .unwrap() -// .downcast::() -// .unwrap(); -// let lsp_request_ranges = Arc::new(Mutex::new(Vec::new())); -// let lsp_request_count = Arc::new(AtomicUsize::new(0)); -// let closure_lsp_request_ranges = Arc::clone(&lsp_request_ranges); -// let closure_lsp_request_count = Arc::clone(&lsp_request_count); -// fake_server -// .handle_request::(move |params, _| { -// let task_lsp_request_ranges = Arc::clone(&closure_lsp_request_ranges); -// let task_lsp_request_count = Arc::clone(&closure_lsp_request_count); -// async move { -// assert_eq!( -// params.text_document.uri, -// lsp::Url::from_file_path("/a/main.rs").unwrap(), -// ); - -// task_lsp_request_ranges.lock().push(params.range); -// let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::Release) + 1; -// Ok(Some(vec![lsp::InlayHint { -// position: params.range.end, -// label: lsp::InlayHintLabel::String(i.to_string()), -// kind: None, -// text_edits: None, -// tooltip: None, -// padding_left: None, -// padding_right: None, -// data: None, -// }])) -// } -// }) -// .next() -// .await; -// fn editor_visible_range( -// editor: &ViewHandle, -// cx: &mut gpui::TestAppContext, -// ) -> Range { -// let ranges = editor.update(cx, |editor, cx| editor.excerpt_visible_offsets(None, cx)); -// assert_eq!( -// ranges.len(), -// 1, -// "Single buffer should produce a single excerpt with visible range" -// ); -// let (_, (excerpt_buffer, _, excerpt_visible_range)) = -// ranges.into_iter().next().unwrap(); -// excerpt_buffer.update(cx, |buffer, _| { -// let snapshot = buffer.snapshot(); -// let start = buffer -// .anchor_before(excerpt_visible_range.start) -// .to_point(&snapshot); -// let end = buffer -// .anchor_after(excerpt_visible_range.end) -// .to_point(&snapshot); -// start..end -// }) -// } - -// // in large buffers, requests are made for more than visible range of a buffer. -// // invisible parts are queried later, to avoid excessive requests on quick typing. -// // wait the timeout needed to get all requests. -// cx.foreground().advance_clock(Duration::from_millis( -// INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100, -// )); -// cx.foreground().run_until_parked(); -// let initial_visible_range = editor_visible_range(&editor, cx); -// let lsp_initial_visible_range = lsp::Range::new( -// lsp::Position::new( -// initial_visible_range.start.row, -// initial_visible_range.start.column, -// ), -// lsp::Position::new( -// initial_visible_range.end.row, -// initial_visible_range.end.column, -// ), -// ); -// let expected_initial_query_range_end = -// lsp::Position::new(initial_visible_range.end.row * 2, 2); -// let mut expected_invisible_query_start = lsp_initial_visible_range.end; -// expected_invisible_query_start.character += 1; -// editor.update(cx, |editor, cx| { -// let ranges = lsp_request_ranges.lock().drain(..).collect::>(); -// assert_eq!(ranges.len(), 2, -// "When scroll is at the edge of a big document, its visible part and the same range further should be queried in order, but got: {ranges:?}"); -// let visible_query_range = &ranges[0]; -// assert_eq!(visible_query_range.start, lsp_initial_visible_range.start); -// assert_eq!(visible_query_range.end, lsp_initial_visible_range.end); -// let invisible_query_range = &ranges[1]; - -// assert_eq!(invisible_query_range.start, expected_invisible_query_start, "Should initially query visible edge of the document"); -// assert_eq!(invisible_query_range.end, expected_initial_query_range_end, "Should initially query visible edge of the document"); - -// let requests_count = lsp_request_count.load(Ordering::Acquire); -// assert_eq!(requests_count, 2, "Visible + invisible request"); -// let expected_hints = vec!["1".to_string(), "2".to_string()]; -// assert_eq!( -// expected_hints, -// cached_hint_labels(editor), -// "Should have hints from both LSP requests made for a big file" -// ); -// assert_eq!(expected_hints, visible_hint_labels(editor, cx), "Should display only hints from the visible range"); -// assert_eq!( -// editor.inlay_hint_cache().version, requests_count, -// "LSP queries should've bumped the cache version" -// ); -// }); - -// editor.update(cx, |editor, cx| { -// editor.scroll_screen(&ScrollAmount::Page(1.0), cx); -// editor.scroll_screen(&ScrollAmount::Page(1.0), cx); -// }); -// cx.foreground().advance_clock(Duration::from_millis( -// INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100, -// )); -// cx.foreground().run_until_parked(); -// let visible_range_after_scrolls = editor_visible_range(&editor, cx); -// let visible_line_count = -// editor.update(cx, |editor, _| editor.visible_line_count().unwrap()); -// let selection_in_cached_range = editor.update(cx, |editor, cx| { -// let ranges = lsp_request_ranges -// .lock() -// .drain(..) -// .sorted_by_key(|r| r.start) -// .collect::>(); -// assert_eq!( -// ranges.len(), -// 2, -// "Should query 2 ranges after both scrolls, but got: {ranges:?}" -// ); -// let first_scroll = &ranges[0]; -// let second_scroll = &ranges[1]; -// assert_eq!( -// first_scroll.end, second_scroll.start, -// "Should query 2 adjacent ranges after the scrolls, but got: {ranges:?}" -// ); -// assert_eq!( -// first_scroll.start, expected_initial_query_range_end, -// "First scroll should start the query right after the end of the original scroll", -// ); -// assert_eq!( -// second_scroll.end, -// lsp::Position::new( -// visible_range_after_scrolls.end.row -// + visible_line_count.ceil() as u32, -// 1, -// ), -// "Second scroll should query one more screen down after the end of the visible range" -// ); - -// let lsp_requests = lsp_request_count.load(Ordering::Acquire); -// assert_eq!(lsp_requests, 4, "Should query for hints after every scroll"); -// let expected_hints = vec![ -// "1".to_string(), -// "2".to_string(), -// "3".to_string(), -// "4".to_string(), -// ]; -// assert_eq!( -// expected_hints, -// cached_hint_labels(editor), -// "Should have hints from the new LSP response after the edit" -// ); -// assert_eq!(expected_hints, visible_hint_labels(editor, cx)); -// assert_eq!( -// editor.inlay_hint_cache().version, -// lsp_requests, -// "Should update the cache for every LSP response with hints added" -// ); - -// let mut selection_in_cached_range = visible_range_after_scrolls.end; -// selection_in_cached_range.row -= visible_line_count.ceil() as u32; -// selection_in_cached_range -// }); - -// editor.update(cx, |editor, cx| { -// editor.change_selections(Some(Autoscroll::center()), cx, |s| { -// s.select_ranges([selection_in_cached_range..selection_in_cached_range]) -// }); -// }); -// cx.foreground().advance_clock(Duration::from_millis( -// INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100, -// )); -// cx.foreground().run_until_parked(); -// editor.update(cx, |_, _| { -// let ranges = lsp_request_ranges -// .lock() -// .drain(..) -// .sorted_by_key(|r| r.start) -// .collect::>(); -// assert!(ranges.is_empty(), "No new ranges or LSP queries should be made after returning to the selection with cached hints"); -// assert_eq!(lsp_request_count.load(Ordering::Acquire), 4); -// }); - -// editor.update(cx, |editor, cx| { -// editor.handle_input("++++more text++++", cx); -// }); -// cx.foreground().advance_clock(Duration::from_millis( -// INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100, -// )); -// cx.foreground().run_until_parked(); -// editor.update(cx, |editor, cx| { -// let mut ranges = lsp_request_ranges.lock().drain(..).collect::>(); -// ranges.sort_by_key(|r| r.start); - -// assert_eq!(ranges.len(), 3, -// "On edit, should scroll to selection and query a range around it: visible + same range above and below. Instead, got query ranges {ranges:?}"); -// let above_query_range = &ranges[0]; -// let visible_query_range = &ranges[1]; -// let below_query_range = &ranges[2]; -// assert!(above_query_range.end.character < visible_query_range.start.character || above_query_range.end.line + 1 == visible_query_range.start.line, -// "Above range {above_query_range:?} should be before visible range {visible_query_range:?}"); -// assert!(visible_query_range.end.character < below_query_range.start.character || visible_query_range.end.line + 1 == below_query_range.start.line, -// "Visible range {visible_query_range:?} should be before below range {below_query_range:?}"); -// assert!(above_query_range.start.line < selection_in_cached_range.row, -// "Hints should be queried with the selected range after the query range start"); -// assert!(below_query_range.end.line > selection_in_cached_range.row, -// "Hints should be queried with the selected range before the query range end"); -// assert!(above_query_range.start.line <= selection_in_cached_range.row - (visible_line_count * 3.0 / 2.0) as u32, -// "Hints query range should contain one more screen before"); -// assert!(below_query_range.end.line >= selection_in_cached_range.row + (visible_line_count * 3.0 / 2.0) as u32, -// "Hints query range should contain one more screen after"); - -// let lsp_requests = lsp_request_count.load(Ordering::Acquire); -// assert_eq!(lsp_requests, 7, "There should be a visible range and two ranges above and below it queried"); -// let expected_hints = vec!["5".to_string(), "6".to_string(), "7".to_string()]; -// assert_eq!(expected_hints, cached_hint_labels(editor), -// "Should have hints from the new LSP response after the edit"); -// assert_eq!(expected_hints, visible_hint_labels(editor, cx)); -// assert_eq!(editor.inlay_hint_cache().version, lsp_requests, "Should update the cache for every LSP response with hints added"); -// }); -// } - -// #[gpui::test(iterations = 10)] -// async fn test_multiple_excerpts_large_multibuffer( -// deterministic: Arc, -// cx: &mut gpui::TestAppContext, -// ) { -// init_test(cx, |settings| { -// settings.defaults.inlay_hints = Some(InlayHintSettings { -// enabled: true, -// show_type_hints: true, -// show_parameter_hints: true, -// show_other_hints: true, -// }) -// }); - -// let mut language = Language::new( -// LanguageConfig { -// name: "Rust".into(), -// path_suffixes: vec!["rs".to_string()], -// ..Default::default() -// }, -// Some(tree_sitter_rust::language()), -// ); -// let mut fake_servers = language -// .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { -// capabilities: lsp::ServerCapabilities { -// inlay_hint_provider: Some(lsp::OneOf::Left(true)), -// ..Default::default() -// }, -// ..Default::default() -// })) -// .await; -// let language = Arc::new(language); -// let fs = FakeFs::new(cx.background()); -// fs.insert_tree( -// "/a", -// json!({ -// "main.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|i| format!("let i = {i};\n")).collect::>().join("")), -// "other.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|j| format!("let j = {j};\n")).collect::>().join("")), -// }), -// ) -// .await; -// let project = Project::test(fs, ["/a".as_ref()], cx).await; -// project.update(cx, |project, _| { -// project.languages().add(Arc::clone(&language)) -// }); -// let workspace = cx -// .add_window(|cx| Workspace::test_new(project.clone(), cx)) -// .root(cx); -// let worktree_id = workspace.update(cx, |workspace, cx| { -// workspace.project().read_with(cx, |project, cx| { -// project.worktrees(cx).next().unwrap().read(cx).id() -// }) -// }); - -// let buffer_1 = project -// .update(cx, |project, cx| { -// project.open_buffer((worktree_id, "main.rs"), cx) -// }) -// .await -// .unwrap(); -// let buffer_2 = project -// .update(cx, |project, cx| { -// project.open_buffer((worktree_id, "other.rs"), cx) -// }) -// .await -// .unwrap(); -// let multibuffer = cx.add_model(|cx| { -// let mut multibuffer = MultiBuffer::new(0); -// multibuffer.push_excerpts( -// buffer_1.clone(), -// [ -// ExcerptRange { -// context: Point::new(0, 0)..Point::new(2, 0), -// primary: None, -// }, -// ExcerptRange { -// context: Point::new(4, 0)..Point::new(11, 0), -// primary: None, -// }, -// ExcerptRange { -// context: Point::new(22, 0)..Point::new(33, 0), -// primary: None, -// }, -// ExcerptRange { -// context: Point::new(44, 0)..Point::new(55, 0), -// primary: None, -// }, -// ExcerptRange { -// context: Point::new(56, 0)..Point::new(66, 0), -// primary: None, -// }, -// ExcerptRange { -// context: Point::new(67, 0)..Point::new(77, 0), -// primary: None, -// }, -// ], -// cx, -// ); -// multibuffer.push_excerpts( -// buffer_2.clone(), -// [ -// ExcerptRange { -// context: Point::new(0, 1)..Point::new(2, 1), -// primary: None, -// }, -// ExcerptRange { -// context: Point::new(4, 1)..Point::new(11, 1), -// primary: None, -// }, -// ExcerptRange { -// context: Point::new(22, 1)..Point::new(33, 1), -// primary: None, -// }, -// ExcerptRange { -// context: Point::new(44, 1)..Point::new(55, 1), -// primary: None, -// }, -// ExcerptRange { -// context: Point::new(56, 1)..Point::new(66, 1), -// primary: None, -// }, -// ExcerptRange { -// context: Point::new(67, 1)..Point::new(77, 1), -// primary: None, -// }, -// ], -// cx, -// ); -// multibuffer -// }); - -// deterministic.run_until_parked(); -// cx.foreground().run_until_parked(); -// let editor = cx -// .add_window(|cx| Editor::for_multibuffer(multibuffer, Some(project.clone()), cx)) -// .root(cx); -// let editor_edited = Arc::new(AtomicBool::new(false)); -// let fake_server = fake_servers.next().await.unwrap(); -// let closure_editor_edited = Arc::clone(&editor_edited); -// fake_server -// .handle_request::(move |params, _| { -// let task_editor_edited = Arc::clone(&closure_editor_edited); -// async move { -// let hint_text = if params.text_document.uri -// == lsp::Url::from_file_path("/a/main.rs").unwrap() -// { -// "main hint" -// } else if params.text_document.uri -// == lsp::Url::from_file_path("/a/other.rs").unwrap() -// { -// "other hint" -// } else { -// panic!("unexpected uri: {:?}", params.text_document.uri); -// }; - -// // one hint per excerpt -// let positions = [ -// lsp::Position::new(0, 2), -// lsp::Position::new(4, 2), -// lsp::Position::new(22, 2), -// lsp::Position::new(44, 2), -// lsp::Position::new(56, 2), -// lsp::Position::new(67, 2), -// ]; -// let out_of_range_hint = lsp::InlayHint { -// position: lsp::Position::new( -// params.range.start.line + 99, -// params.range.start.character + 99, -// ), -// label: lsp::InlayHintLabel::String( -// "out of excerpt range, should be ignored".to_string(), -// ), -// kind: None, -// text_edits: None, -// tooltip: None, -// padding_left: None, -// padding_right: None, -// data: None, -// }; - -// let edited = task_editor_edited.load(Ordering::Acquire); -// Ok(Some( -// std::iter::once(out_of_range_hint) -// .chain(positions.into_iter().enumerate().map(|(i, position)| { -// lsp::InlayHint { -// position, -// label: lsp::InlayHintLabel::String(format!( -// "{hint_text}{} #{i}", -// if edited { "(edited)" } else { "" }, -// )), -// kind: None, -// text_edits: None, -// tooltip: None, -// padding_left: None, -// padding_right: None, -// data: None, -// } -// })) -// .collect(), -// )) -// } -// }) -// .next() -// .await; -// cx.foreground().run_until_parked(); - -// editor.update(cx, |editor, cx| { -// let expected_hints = vec![ -// "main hint #0".to_string(), -// "main hint #1".to_string(), -// "main hint #2".to_string(), -// "main hint #3".to_string(), -// ]; -// assert_eq!( -// expected_hints, -// cached_hint_labels(editor), -// "When scroll is at the edge of a multibuffer, its visible excerpts only should be queried for inlay hints" -// ); -// assert_eq!(expected_hints, visible_hint_labels(editor, cx)); -// assert_eq!(editor.inlay_hint_cache().version, expected_hints.len(), "Every visible excerpt hints should bump the verison"); -// }); - -// editor.update(cx, |editor, cx| { -// editor.change_selections(Some(Autoscroll::Next), cx, |s| { -// s.select_ranges([Point::new(4, 0)..Point::new(4, 0)]) -// }); -// editor.change_selections(Some(Autoscroll::Next), cx, |s| { -// s.select_ranges([Point::new(22, 0)..Point::new(22, 0)]) -// }); -// editor.change_selections(Some(Autoscroll::Next), cx, |s| { -// s.select_ranges([Point::new(50, 0)..Point::new(50, 0)]) -// }); -// }); -// cx.foreground().run_until_parked(); -// editor.update(cx, |editor, cx| { -// let expected_hints = vec![ -// "main hint #0".to_string(), -// "main hint #1".to_string(), -// "main hint #2".to_string(), -// "main hint #3".to_string(), -// "main hint #4".to_string(), -// "main hint #5".to_string(), -// "other hint #0".to_string(), -// "other hint #1".to_string(), -// "other hint #2".to_string(), -// ]; -// assert_eq!(expected_hints, cached_hint_labels(editor), -// "With more scrolls of the multibuffer, more hints should be added into the cache and nothing invalidated without edits"); -// assert_eq!(expected_hints, visible_hint_labels(editor, cx)); -// assert_eq!(editor.inlay_hint_cache().version, expected_hints.len(), -// "Due to every excerpt having one hint, we update cache per new excerpt scrolled"); -// }); - -// editor.update(cx, |editor, cx| { -// editor.change_selections(Some(Autoscroll::Next), cx, |s| { -// s.select_ranges([Point::new(100, 0)..Point::new(100, 0)]) -// }); -// }); -// cx.foreground().advance_clock(Duration::from_millis( -// INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100, -// )); -// cx.foreground().run_until_parked(); -// let last_scroll_update_version = editor.update(cx, |editor, cx| { -// let expected_hints = vec![ -// "main hint #0".to_string(), -// "main hint #1".to_string(), -// "main hint #2".to_string(), -// "main hint #3".to_string(), -// "main hint #4".to_string(), -// "main hint #5".to_string(), -// "other hint #0".to_string(), -// "other hint #1".to_string(), -// "other hint #2".to_string(), -// "other hint #3".to_string(), -// "other hint #4".to_string(), -// "other hint #5".to_string(), -// ]; -// assert_eq!(expected_hints, cached_hint_labels(editor), -// "After multibuffer was scrolled to the end, all hints for all excerpts should be fetched"); -// assert_eq!(expected_hints, visible_hint_labels(editor, cx)); -// assert_eq!(editor.inlay_hint_cache().version, expected_hints.len()); -// expected_hints.len() -// }); - -// editor.update(cx, |editor, cx| { -// editor.change_selections(Some(Autoscroll::Next), cx, |s| { -// s.select_ranges([Point::new(4, 0)..Point::new(4, 0)]) -// }); -// }); -// cx.foreground().run_until_parked(); -// editor.update(cx, |editor, cx| { -// let expected_hints = vec![ -// "main hint #0".to_string(), -// "main hint #1".to_string(), -// "main hint #2".to_string(), -// "main hint #3".to_string(), -// "main hint #4".to_string(), -// "main hint #5".to_string(), -// "other hint #0".to_string(), -// "other hint #1".to_string(), -// "other hint #2".to_string(), -// "other hint #3".to_string(), -// "other hint #4".to_string(), -// "other hint #5".to_string(), -// ]; -// assert_eq!(expected_hints, cached_hint_labels(editor), -// "After multibuffer was scrolled to the end, further scrolls up should not bring more hints"); -// assert_eq!(expected_hints, visible_hint_labels(editor, cx)); -// assert_eq!(editor.inlay_hint_cache().version, last_scroll_update_version, "No updates should happen during scrolling already scolled buffer"); -// }); - -// editor_edited.store(true, Ordering::Release); -// editor.update(cx, |editor, cx| { -// editor.change_selections(None, cx, |s| { -// s.select_ranges([Point::new(56, 0)..Point::new(56, 0)]) -// }); -// editor.handle_input("++++more text++++", cx); -// }); -// cx.foreground().run_until_parked(); -// editor.update(cx, |editor, cx| { -// let expected_hints = vec![ -// "main hint(edited) #0".to_string(), -// "main hint(edited) #1".to_string(), -// "main hint(edited) #2".to_string(), -// "main hint(edited) #3".to_string(), -// "main hint(edited) #4".to_string(), -// "main hint(edited) #5".to_string(), -// "other hint(edited) #0".to_string(), -// "other hint(edited) #1".to_string(), -// ]; -// assert_eq!( -// expected_hints, -// cached_hint_labels(editor), -// "After multibuffer edit, editor gets scolled back to the last selection; \ -// all hints should be invalidated and requeried for all of its visible excerpts" -// ); -// assert_eq!(expected_hints, visible_hint_labels(editor, cx)); - -// let current_cache_version = editor.inlay_hint_cache().version; -// let minimum_expected_version = last_scroll_update_version + expected_hints.len(); -// assert!( -// current_cache_version == minimum_expected_version || current_cache_version == minimum_expected_version + 1, -// "Due to every excerpt having one hint, cache should update per new excerpt received + 1 potential sporadic update" -// ); -// }); -// } - -// #[gpui::test] -// async fn test_excerpts_removed( -// deterministic: Arc, -// cx: &mut gpui::TestAppContext, -// ) { -// init_test(cx, |settings| { -// settings.defaults.inlay_hints = Some(InlayHintSettings { -// enabled: true, -// show_type_hints: false, -// show_parameter_hints: false, -// show_other_hints: false, -// }) -// }); - -// let mut language = Language::new( -// LanguageConfig { -// name: "Rust".into(), -// path_suffixes: vec!["rs".to_string()], -// ..Default::default() -// }, -// Some(tree_sitter_rust::language()), -// ); -// let mut fake_servers = language -// .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { -// capabilities: lsp::ServerCapabilities { -// inlay_hint_provider: Some(lsp::OneOf::Left(true)), -// ..Default::default() -// }, -// ..Default::default() -// })) -// .await; -// let language = Arc::new(language); -// let fs = FakeFs::new(cx.background()); -// fs.insert_tree( -// "/a", -// json!({ -// "main.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|i| format!("let i = {i};\n")).collect::>().join("")), -// "other.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|j| format!("let j = {j};\n")).collect::>().join("")), -// }), -// ) -// .await; -// let project = Project::test(fs, ["/a".as_ref()], cx).await; -// project.update(cx, |project, _| { -// project.languages().add(Arc::clone(&language)) -// }); -// let workspace = cx -// .add_window(|cx| Workspace::test_new(project.clone(), cx)) -// .root(cx); -// let worktree_id = workspace.update(cx, |workspace, cx| { -// workspace.project().read_with(cx, |project, cx| { -// project.worktrees(cx).next().unwrap().read(cx).id() -// }) -// }); - -// let buffer_1 = project -// .update(cx, |project, cx| { -// project.open_buffer((worktree_id, "main.rs"), cx) -// }) -// .await -// .unwrap(); -// let buffer_2 = project -// .update(cx, |project, cx| { -// project.open_buffer((worktree_id, "other.rs"), cx) -// }) -// .await -// .unwrap(); -// let multibuffer = cx.add_model(|_| MultiBuffer::new(0)); -// let (buffer_1_excerpts, buffer_2_excerpts) = multibuffer.update(cx, |multibuffer, cx| { -// let buffer_1_excerpts = multibuffer.push_excerpts( -// buffer_1.clone(), -// [ExcerptRange { -// context: Point::new(0, 0)..Point::new(2, 0), -// primary: None, -// }], -// cx, -// ); -// let buffer_2_excerpts = multibuffer.push_excerpts( -// buffer_2.clone(), -// [ExcerptRange { -// context: Point::new(0, 1)..Point::new(2, 1), -// primary: None, -// }], -// cx, -// ); -// (buffer_1_excerpts, buffer_2_excerpts) -// }); - -// assert!(!buffer_1_excerpts.is_empty()); -// assert!(!buffer_2_excerpts.is_empty()); - -// deterministic.run_until_parked(); -// cx.foreground().run_until_parked(); -// let editor = cx -// .add_window(|cx| Editor::for_multibuffer(multibuffer, Some(project.clone()), cx)) -// .root(cx); -// let editor_edited = Arc::new(AtomicBool::new(false)); -// let fake_server = fake_servers.next().await.unwrap(); -// let closure_editor_edited = Arc::clone(&editor_edited); -// fake_server -// .handle_request::(move |params, _| { -// let task_editor_edited = Arc::clone(&closure_editor_edited); -// async move { -// let hint_text = if params.text_document.uri -// == lsp::Url::from_file_path("/a/main.rs").unwrap() -// { -// "main hint" -// } else if params.text_document.uri -// == lsp::Url::from_file_path("/a/other.rs").unwrap() -// { -// "other hint" -// } else { -// panic!("unexpected uri: {:?}", params.text_document.uri); -// }; - -// let positions = [ -// lsp::Position::new(0, 2), -// lsp::Position::new(4, 2), -// lsp::Position::new(22, 2), -// lsp::Position::new(44, 2), -// lsp::Position::new(56, 2), -// lsp::Position::new(67, 2), -// ]; -// let out_of_range_hint = lsp::InlayHint { -// position: lsp::Position::new( -// params.range.start.line + 99, -// params.range.start.character + 99, -// ), -// label: lsp::InlayHintLabel::String( -// "out of excerpt range, should be ignored".to_string(), -// ), -// kind: None, -// text_edits: None, -// tooltip: None, -// padding_left: None, -// padding_right: None, -// data: None, -// }; - -// let edited = task_editor_edited.load(Ordering::Acquire); -// Ok(Some( -// std::iter::once(out_of_range_hint) -// .chain(positions.into_iter().enumerate().map(|(i, position)| { -// lsp::InlayHint { -// position, -// label: lsp::InlayHintLabel::String(format!( -// "{hint_text}{} #{i}", -// if edited { "(edited)" } else { "" }, -// )), -// kind: None, -// text_edits: None, -// tooltip: None, -// padding_left: None, -// padding_right: None, -// data: None, -// } -// })) -// .collect(), -// )) -// } -// }) -// .next() -// .await; -// cx.foreground().run_until_parked(); - -// editor.update(cx, |editor, cx| { -// assert_eq!( -// vec!["main hint #0".to_string(), "other hint #0".to_string()], -// cached_hint_labels(editor), -// "Cache should update for both excerpts despite hints display was disabled" -// ); -// assert!( -// visible_hint_labels(editor, cx).is_empty(), -// "All hints are disabled and should not be shown despite being present in the cache" -// ); -// assert_eq!( -// editor.inlay_hint_cache().version, -// 2, -// "Cache should update once per excerpt query" -// ); -// }); - -// editor.update(cx, |editor, cx| { -// editor.buffer().update(cx, |multibuffer, cx| { -// multibuffer.remove_excerpts(buffer_2_excerpts, cx) -// }) -// }); -// cx.foreground().run_until_parked(); -// editor.update(cx, |editor, cx| { -// assert_eq!( -// vec!["main hint #0".to_string()], -// cached_hint_labels(editor), -// "For the removed excerpt, should clean corresponding cached hints" -// ); -// assert!( -// visible_hint_labels(editor, cx).is_empty(), -// "All hints are disabled and should not be shown despite being present in the cache" -// ); -// assert_eq!( -// editor.inlay_hint_cache().version, -// 3, -// "Excerpt removal should trigger a cache update" -// ); -// }); - -// update_test_language_settings(cx, |settings| { -// settings.defaults.inlay_hints = Some(InlayHintSettings { -// enabled: true, -// show_type_hints: true, -// show_parameter_hints: true, -// show_other_hints: true, -// }) -// }); -// cx.foreground().run_until_parked(); -// editor.update(cx, |editor, cx| { -// let expected_hints = vec!["main hint #0".to_string()]; -// assert_eq!( -// expected_hints, -// cached_hint_labels(editor), -// "Hint display settings change should not change the cache" -// ); -// assert_eq!( -// expected_hints, -// visible_hint_labels(editor, cx), -// "Settings change should make cached hints visible" -// ); -// assert_eq!( -// editor.inlay_hint_cache().version, -// 4, -// "Settings change should trigger a cache update" -// ); -// }); -// } - -// #[gpui::test] -// async fn test_inside_char_boundary_range_hints(cx: &mut gpui::TestAppContext) { -// init_test(cx, |settings| { -// settings.defaults.inlay_hints = Some(InlayHintSettings { -// enabled: true, -// show_type_hints: true, -// show_parameter_hints: true, -// show_other_hints: true, -// }) -// }); - -// let mut language = Language::new( -// LanguageConfig { -// name: "Rust".into(), -// path_suffixes: vec!["rs".to_string()], -// ..Default::default() -// }, -// Some(tree_sitter_rust::language()), -// ); -// let mut fake_servers = language -// .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { -// capabilities: lsp::ServerCapabilities { -// inlay_hint_provider: Some(lsp::OneOf::Left(true)), -// ..Default::default() -// }, -// ..Default::default() -// })) -// .await; -// let fs = FakeFs::new(cx.background()); -// fs.insert_tree( -// "/a", -// json!({ -// "main.rs": format!(r#"fn main() {{\n{}\n}}"#, format!("let i = {};\n", "√".repeat(10)).repeat(500)), -// "other.rs": "// Test file", -// }), -// ) -// .await; -// let project = Project::test(fs, ["/a".as_ref()], cx).await; -// project.update(cx, |project, _| project.languages().add(Arc::new(language))); -// let workspace = cx -// .add_window(|cx| Workspace::test_new(project.clone(), cx)) -// .root(cx); -// let worktree_id = workspace.update(cx, |workspace, cx| { -// workspace.project().read_with(cx, |project, cx| { -// project.worktrees(cx).next().unwrap().read(cx).id() -// }) -// }); - -// let _buffer = project -// .update(cx, |project, cx| { -// project.open_local_buffer("/a/main.rs", cx) -// }) -// .await -// .unwrap(); -// cx.foreground().run_until_parked(); -// cx.foreground().start_waiting(); -// let fake_server = fake_servers.next().await.unwrap(); -// let editor = workspace -// .update(cx, |workspace, cx| { -// workspace.open_path((worktree_id, "main.rs"), None, true, cx) -// }) -// .await -// .unwrap() -// .downcast::() -// .unwrap(); -// let lsp_request_count = Arc::new(AtomicU32::new(0)); -// let closure_lsp_request_count = Arc::clone(&lsp_request_count); -// fake_server -// .handle_request::(move |params, _| { -// let task_lsp_request_count = Arc::clone(&closure_lsp_request_count); -// async move { -// assert_eq!( -// params.text_document.uri, -// lsp::Url::from_file_path("/a/main.rs").unwrap(), -// ); -// let query_start = params.range.start; -// let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::Release) + 1; -// Ok(Some(vec![lsp::InlayHint { -// position: query_start, -// label: lsp::InlayHintLabel::String(i.to_string()), -// kind: None, -// text_edits: None, -// tooltip: None, -// padding_left: None, -// padding_right: None, -// data: None, -// }])) -// } -// }) -// .next() -// .await; - -// cx.foreground().run_until_parked(); -// editor.update(cx, |editor, cx| { -// editor.change_selections(None, cx, |s| { -// s.select_ranges([Point::new(10, 0)..Point::new(10, 0)]) -// }) -// }); -// cx.foreground().run_until_parked(); -// editor.update(cx, |editor, cx| { -// let expected_hints = vec!["1".to_string()]; -// assert_eq!(expected_hints, cached_hint_labels(editor)); -// assert_eq!(expected_hints, visible_hint_labels(editor, cx)); -// assert_eq!(editor.inlay_hint_cache().version, 1); -// }); -// } - -// #[gpui::test] -// async fn test_toggle_inlay_hints(cx: &mut gpui::TestAppContext) { -// init_test(cx, |settings| { -// settings.defaults.inlay_hints = Some(InlayHintSettings { -// enabled: false, -// show_type_hints: true, -// show_parameter_hints: true, -// show_other_hints: true, -// }) -// }); - -// let (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await; - -// editor.update(cx, |editor, cx| { -// editor.toggle_inlay_hints(&crate::ToggleInlayHints, cx) -// }); -// cx.foreground().start_waiting(); -// let lsp_request_count = Arc::new(AtomicU32::new(0)); -// let closure_lsp_request_count = Arc::clone(&lsp_request_count); -// fake_server -// .handle_request::(move |params, _| { -// let task_lsp_request_count = Arc::clone(&closure_lsp_request_count); -// async move { -// assert_eq!( -// params.text_document.uri, -// lsp::Url::from_file_path(file_with_hints).unwrap(), -// ); - -// let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst) + 1; -// Ok(Some(vec![lsp::InlayHint { -// position: lsp::Position::new(0, i), -// label: lsp::InlayHintLabel::String(i.to_string()), -// kind: None, -// text_edits: None, -// tooltip: None, -// padding_left: None, -// padding_right: None, -// data: None, -// }])) -// } -// }) -// .next() -// .await; -// cx.foreground().run_until_parked(); -// editor.update(cx, |editor, cx| { -// let expected_hints = vec!["1".to_string()]; -// assert_eq!( -// expected_hints, -// cached_hint_labels(editor), -// "Should display inlays after toggle despite them disabled in settings" -// ); -// assert_eq!(expected_hints, visible_hint_labels(editor, cx)); -// assert_eq!( -// editor.inlay_hint_cache().version, -// 1, -// "First toggle should be cache's first update" -// ); -// }); - -// editor.update(cx, |editor, cx| { -// editor.toggle_inlay_hints(&crate::ToggleInlayHints, cx) -// }); -// cx.foreground().run_until_parked(); -// editor.update(cx, |editor, cx| { -// assert!( -// cached_hint_labels(editor).is_empty(), -// "Should clear hints after 2nd toggle" -// ); -// assert!(visible_hint_labels(editor, cx).is_empty()); -// assert_eq!(editor.inlay_hint_cache().version, 2); -// }); - -// update_test_language_settings(cx, |settings| { -// settings.defaults.inlay_hints = Some(InlayHintSettings { -// enabled: true, -// show_type_hints: true, -// show_parameter_hints: true, -// show_other_hints: true, -// }) -// }); -// cx.foreground().run_until_parked(); -// editor.update(cx, |editor, cx| { -// let expected_hints = vec!["2".to_string()]; -// assert_eq!( -// expected_hints, -// cached_hint_labels(editor), -// "Should query LSP hints for the 2nd time after enabling hints in settings" -// ); -// assert_eq!(expected_hints, visible_hint_labels(editor, cx)); -// assert_eq!(editor.inlay_hint_cache().version, 3); -// }); - -// editor.update(cx, |editor, cx| { -// editor.toggle_inlay_hints(&crate::ToggleInlayHints, cx) -// }); -// cx.foreground().run_until_parked(); -// editor.update(cx, |editor, cx| { -// assert!( -// cached_hint_labels(editor).is_empty(), -// "Should clear hints after enabling in settings and a 3rd toggle" -// ); -// assert!(visible_hint_labels(editor, cx).is_empty()); -// assert_eq!(editor.inlay_hint_cache().version, 4); -// }); - -// editor.update(cx, |editor, cx| { -// editor.toggle_inlay_hints(&crate::ToggleInlayHints, cx) -// }); -// cx.foreground().run_until_parked(); -// editor.update(cx, |editor, cx| { -// let expected_hints = vec!["3".to_string()]; -// assert_eq!( -// expected_hints, -// cached_hint_labels(editor), -// "Should query LSP hints for the 3rd time after enabling hints in settings and toggling them back on" -// ); -// assert_eq!(expected_hints, visible_hint_labels(editor, cx)); -// assert_eq!(editor.inlay_hint_cache().version, 5); -// }); -// } - -// pub(crate) fn init_test(cx: &mut TestAppContext, f: impl Fn(&mut AllLanguageSettingsContent)) { -// cx.foreground().forbid_parking(); - -// cx.update(|cx| { -// cx.set_global(SettingsStore::test(cx)); -// theme::init(cx); -// client::init_settings(cx); -// language::init(cx); -// Project::init_settings(cx); -// workspace::init_settings(cx); -// crate::init(cx); -// }); - -// update_test_language_settings(cx, f); -// } - -// async fn prepare_test_objects( -// cx: &mut TestAppContext, -// ) -> (&'static str, ViewHandle, FakeLanguageServer) { -// let mut language = Language::new( -// LanguageConfig { -// name: "Rust".into(), -// path_suffixes: vec!["rs".to_string()], -// ..Default::default() -// }, -// Some(tree_sitter_rust::language()), -// ); -// let mut fake_servers = language -// .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { -// capabilities: lsp::ServerCapabilities { -// inlay_hint_provider: Some(lsp::OneOf::Left(true)), -// ..Default::default() -// }, -// ..Default::default() -// })) -// .await; - -// let fs = FakeFs::new(cx.background()); -// fs.insert_tree( -// "/a", -// json!({ -// "main.rs": "fn main() { a } // and some long comment to ensure inlays are not trimmed out", -// "other.rs": "// Test file", -// }), -// ) -// .await; - -// let project = Project::test(fs, ["/a".as_ref()], cx).await; -// project.update(cx, |project, _| project.languages().add(Arc::new(language))); -// let workspace = cx -// .add_window(|cx| Workspace::test_new(project.clone(), cx)) -// .root(cx); -// let worktree_id = workspace.update(cx, |workspace, cx| { -// workspace.project().read_with(cx, |project, cx| { -// project.worktrees(cx).next().unwrap().read(cx).id() -// }) -// }); - -// let _buffer = project -// .update(cx, |project, cx| { -// project.open_local_buffer("/a/main.rs", cx) -// }) -// .await -// .unwrap(); -// cx.foreground().run_until_parked(); -// cx.foreground().start_waiting(); -// let fake_server = fake_servers.next().await.unwrap(); -// let editor = workspace -// .update(cx, |workspace, cx| { -// workspace.open_path((worktree_id, "main.rs"), None, true, cx) -// }) -// .await -// .unwrap() -// .downcast::() -// .unwrap(); - -// editor.update(cx, |editor, cx| { -// assert!(cached_hint_labels(editor).is_empty()); -// assert!(visible_hint_labels(editor, cx).is_empty()); -// assert_eq!(editor.inlay_hint_cache().version, 0); -// }); - -// ("/a/main.rs", editor, fake_server) -// } - -// pub fn cached_hint_labels(editor: &Editor) -> Vec { -// let mut labels = Vec::new(); -// for (_, excerpt_hints) in &editor.inlay_hint_cache().hints { -// let excerpt_hints = excerpt_hints.read(); -// for id in &excerpt_hints.ordered_hints { -// labels.push(excerpt_hints.hints_by_id[id].text()); -// } -// } - -// labels.sort(); -// labels -// } - -// pub fn visible_hint_labels(editor: &Editor, cx: &ViewContext<'_, '_, Editor>) -> Vec { -// let mut hints = editor -// .visible_inlay_hints(cx) -// .into_iter() -// .map(|hint| hint.text.to_string()) -// .collect::>(); -// hints.sort(); -// hints -// } -// } diff --git a/crates/editor2/src/items.rs b/crates/editor2/src/items.rs index 1081a329c6145ab6e6cea620aeaff1ec006cf017..25e9f91608fc857ea30191f25bbfc75aac6117ef 100644 --- a/crates/editor2/src/items.rs +++ b/crates/editor2/src/items.rs @@ -1,7 +1,8 @@ use crate::{ - display_map::ToDisplayPoint, link_go_to_definition::hide_link_definition, + editor_settings::SeedQuerySetting, link_go_to_definition::hide_link_definition, movement::surrounding_word, persistence::DB, scroll::ScrollAnchor, Anchor, Autoscroll, Editor, - Event, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, NavigationData, ToPoint as _, + EditorSettings, Event, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, + NavigationData, ToPoint as _, }; use anyhow::{anyhow, Context, Result}; use collections::HashSet; @@ -12,11 +13,12 @@ use gpui::{ ViewContext, VisualContext, WeakView, }; use language::{ - proto::serialize_anchor as serialize_text_anchor, Bias, Buffer, OffsetRangeExt, Point, - SelectionGoal, + proto::serialize_anchor as serialize_text_anchor, Bias, Buffer, CharKind, OffsetRangeExt, + Point, SelectionGoal, }; use project::{search::SearchQuery, FormatTrigger, Item as _, Project, ProjectPath}; use rpc::proto::{self, update_view, PeerId}; +use settings::Settings; use smallvec::SmallVec; use std::{ borrow::Cow, @@ -918,24 +920,28 @@ impl SearchableItem for Editor { } fn query_suggestion(&mut self, cx: &mut ViewContext) -> String { - let display_map = self.snapshot(cx).display_snapshot; + let setting = EditorSettings::get_global(cx).seed_search_query_from_cursor; + let snapshot = &self.snapshot(cx).buffer_snapshot; let selection = self.selections.newest::(cx); - if selection.start == selection.end { - let point = selection.start.to_display_point(&display_map); - let range = surrounding_word(&display_map, point); - let range = range.start.to_offset(&display_map, Bias::Left) - ..range.end.to_offset(&display_map, Bias::Right); - let text: String = display_map.buffer_snapshot.text_for_range(range).collect(); - if text.trim().is_empty() { + + match setting { + SeedQuerySetting::Never => String::new(), + SeedQuerySetting::Selection | SeedQuerySetting::Always if !selection.is_empty() => { + snapshot + .text_for_range(selection.start..selection.end) + .collect() + } + SeedQuerySetting::Selection => String::new(), + SeedQuerySetting::Always => { + let (range, kind) = snapshot.surrounding_word(selection.start); + if kind == Some(CharKind::Word) { + let text: String = snapshot.text_for_range(range).collect(); + if !text.trim().is_empty() { + return text; + } + } String::new() - } else { - text } - } else { - display_map - .buffer_snapshot - .text_for_range(selection.start..selection.end) - .collect() } } diff --git a/crates/editor2/src/scroll/scroll_amount.rs b/crates/editor2/src/scroll/scroll_amount.rs index 89d188e324e6aa83cc2bfab3fd0444133636f27b..2cb22d15163323eae5f396e2415b973d099aae74 100644 --- a/crates/editor2/src/scroll/scroll_amount.rs +++ b/crates/editor2/src/scroll/scroll_amount.rs @@ -11,19 +11,18 @@ pub enum ScrollAmount { impl ScrollAmount { pub fn lines(&self, editor: &mut Editor) -> f32 { - todo!() - // match self { - // Self::Line(count) => *count, - // Self::Page(count) => editor - // .visible_line_count() - // .map(|mut l| { - // // for full pages subtract one to leave an anchor line - // if count.abs() == 1.0 { - // l -= 1.0 - // } - // (l * count).trunc() - // }) - // .unwrap_or(0.), - // } + match self { + Self::Line(count) => *count, + Self::Page(count) => editor + .visible_line_count() + .map(|mut l| { + // for full pages subtract one to leave an anchor line + if count.abs() == 1.0 { + l -= 1.0 + } + (l * count).trunc() + }) + .unwrap_or(0.), + } } } diff --git a/crates/go_to_line2/src/go_to_line.rs b/crates/go_to_line2/src/go_to_line.rs index c65373e6acc0c1af750173caa8a648eb9881be7b..9ec770e05cdb73fb3b3ddc172f9949d5217ddabf 100644 --- a/crates/go_to_line2/src/go_to_line.rs +++ b/crates/go_to_line2/src/go_to_line.rs @@ -8,25 +8,12 @@ use text::{Bias, Point}; use theme::ActiveTheme; use ui::{h_stack, modal, v_stack, Label, LabelColor}; use util::paths::FILE_ROW_COLUMN_DELIMITER; -use workspace::{ModalEvent, Workspace}; +use workspace::{Modal, ModalEvent, Workspace}; actions!(Toggle); pub fn init(cx: &mut AppContext) { - cx.observe_new_views( - |workspace: &mut Workspace, _: &mut ViewContext| { - workspace - .modal_layer() - .register_modal(Toggle, |workspace, cx| { - let editor = workspace - .active_item(cx) - .and_then(|active_item| active_item.downcast::())?; - - Some(cx.build_view(|cx| GoToLine::new(editor, cx))) - }); - }, - ) - .detach(); + cx.observe_new_views(GoToLine::register).detach(); } pub struct GoToLine { @@ -37,21 +24,29 @@ pub struct GoToLine { _subscriptions: Vec, } -pub enum Event { - Dismissed, -} - -impl EventEmitter for GoToLine {} - impl EventEmitter for GoToLine {} +impl Modal for GoToLine { + fn focus(&self, cx: &mut WindowContext) { + self.line_editor.update(cx, |editor, cx| editor.focus(cx)) + } +} impl GoToLine { - pub fn new(active_editor: View, cx: &mut ViewContext) -> Self { - let line_editor = cx.build_view(|cx| { - let editor = Editor::single_line(cx); - editor.focus(cx); - editor + fn register(workspace: &mut Workspace, _: &mut ViewContext) { + workspace.register_action(|workspace, _: &Toggle, cx| { + let Some(editor) = workspace + .active_item(cx) + .and_then(|active_item| active_item.downcast::()) + else { + return; + }; + + workspace.toggle_modal(cx, move |cx| GoToLine::new(editor, cx)); }); + } + + pub fn new(active_editor: View, cx: &mut ViewContext) -> Self { + let line_editor = cx.build_view(|cx| Editor::single_line(cx)); let line_editor_change = cx.subscribe(&line_editor, Self::on_line_editor_event); let editor = active_editor.read(cx); @@ -78,7 +73,6 @@ impl GoToLine { fn release(&mut self, cx: &mut WindowContext) { let scroll_position = self.prev_scroll_position.take(); self.active_editor.update(cx, |editor, cx| { - editor.focus(cx); editor.highlight_rows(None); if let Some(scroll_position) = scroll_position { editor.set_scroll_position(scroll_position, cx); @@ -95,7 +89,7 @@ impl GoToLine { ) { match event { // todo!() this isn't working... - editor::Event::Blurred => cx.emit(Event::Dismissed), + editor::Event::Blurred => cx.emit(ModalEvent::Dismissed), editor::Event::BufferEdited { .. } => self.highlight_current_line(cx), _ => {} } @@ -130,22 +124,24 @@ impl GoToLine { } fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext) { - cx.emit(Event::Dismissed); + cx.emit(ModalEvent::Dismissed); } fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext) { if let Some(point) = self.point_from_query(cx) { - self.active_editor.update(cx, |active_editor, cx| { - let snapshot = active_editor.snapshot(cx).display_snapshot; + self.active_editor.update(cx, |editor, cx| { + let snapshot = editor.snapshot(cx).display_snapshot; let point = snapshot.buffer_snapshot.clip_point(point, Bias::Left); - active_editor.change_selections(Some(Autoscroll::center()), cx, |s| { + editor.change_selections(Some(Autoscroll::center()), cx, |s| { s.select_ranges([point..point]) }); + editor.focus(cx); + cx.notify(); }); self.prev_scroll_position.take(); } - cx.emit(Event::Dismissed); + cx.emit(ModalEvent::Dismissed); } } diff --git a/crates/gpui/src/elements/flex.rs b/crates/gpui/src/elements/flex.rs index ba387c5e48a7068856e4f8bfb31bdf36443d9c68..f4b8578d17b3c560125c132a520f8859f82f3715 100644 --- a/crates/gpui/src/elements/flex.rs +++ b/crates/gpui/src/elements/flex.rs @@ -67,14 +67,21 @@ impl Flex { where Tag: 'static, { + // Don't assume that this initialization is what scroll_state really is in other panes: + // `element_state` is shared and there could be init races. let scroll_state = cx.element_state::>( element_id, Rc::new(ScrollState { - scroll_to: Cell::new(scroll_to), - scroll_position: Default::default(), type_tag: TypeTag::new::(), + scroll_to: Default::default(), + scroll_position: Default::default(), }), ); + // Set scroll_to separately, because the default state is already picked as `None` by other panes + // by the time we start setting it here, hence update all others' state too. + scroll_state.update(cx, |this, _| { + this.scroll_to.set(scroll_to); + }); self.scroll_state = Some((scroll_state, cx.handle().id())); self } diff --git a/crates/gpui/src/geometry.rs b/crates/gpui/src/geometry.rs index 40b06827872edec469d82cfc1cc79c377a00633b..fe197af5d2670146de94198abbc0db3dccc02135 100644 --- a/crates/gpui/src/geometry.rs +++ b/crates/gpui/src/geometry.rs @@ -136,7 +136,7 @@ impl ToJson for RectF { } #[derive(Refineable, Debug)] -#[refineable(debug)] +#[refineable(Debug)] pub struct Point { pub x: T, pub y: T, @@ -161,7 +161,7 @@ impl Into> for Point { } #[derive(Refineable, Clone, Debug)] -#[refineable(debug)] +#[refineable(Debug)] pub struct Size { pub width: T, pub height: T, @@ -227,7 +227,7 @@ impl Size { } #[derive(Clone, Default, Refineable, Debug)] -#[refineable(debug)] +#[refineable(Debug)] pub struct Edges { pub top: T, pub right: T, diff --git a/crates/gpui2/src/action.rs b/crates/gpui2/src/action.rs index 85149f5d55cc971844621e3acb3ba52d1d7c1a74..170ddf942f2bfcdacda710ef89094cd8aef726ec 100644 --- a/crates/gpui2/src/action.rs +++ b/crates/gpui2/src/action.rs @@ -4,7 +4,7 @@ use collections::{HashMap, HashSet}; use lazy_static::lazy_static; use parking_lot::{MappedRwLockReadGuard, RwLock, RwLockReadGuard}; use serde::Deserialize; -use std::any::{type_name, Any}; +use std::any::{type_name, Any, TypeId}; /// Actions are used to implement keyboard-driven UI. /// When you declare an action, you can bind keys to the action in the keymap and @@ -100,6 +100,21 @@ where } } +impl dyn Action { + pub fn type_id(&self) -> TypeId { + self.as_any().type_id() + } + + pub fn name(&self) -> SharedString { + ACTION_REGISTRY + .read() + .names_by_type_id + .get(&self.type_id()) + .expect("type is not a registered action") + .clone() + } +} + type ActionBuilder = fn(json: Option) -> anyhow::Result>; lazy_static! { @@ -109,6 +124,7 @@ lazy_static! { #[derive(Default)] struct ActionRegistry { builders_by_name: HashMap, + names_by_type_id: HashMap, all_names: Vec, // So we can return a static slice. } @@ -117,9 +133,24 @@ pub fn register_action() { let name = A::qualified_name(); let mut lock = ACTION_REGISTRY.write(); lock.builders_by_name.insert(name.clone(), A::build); + lock.names_by_type_id + .insert(TypeId::of::(), name.clone()); lock.all_names.push(name); } +/// Construct an action based on its name and optional JSON parameters sourced from the keymap. +pub fn build_action_from_type(type_id: &TypeId) -> Result> { + let lock = ACTION_REGISTRY.read(); + let name = lock + .names_by_type_id + .get(type_id) + .ok_or_else(|| anyhow!("no action type registered for {:?}", type_id))? + .clone(); + drop(lock); + + build_action(&name, None) +} + /// Construct an action based on its name and optional JSON parameters sourced from the keymap. pub fn build_action(name: &str, params: Option) -> Result> { let lock = ACTION_REGISTRY.read(); diff --git a/crates/gpui2/src/color.rs b/crates/gpui2/src/color.rs index 3f751ae45aa15c5c58ae4e510c0211f67fb5484a..edf416ae7dcb384ffd270933d796e71e731ac612 100644 --- a/crates/gpui2/src/color.rs +++ b/crates/gpui2/src/color.rs @@ -1,8 +1,8 @@ #![allow(dead_code)] +use anyhow::bail; use serde::de::{self, Deserialize, Deserializer, Visitor}; use std::fmt; -use std::num::ParseIntError; pub fn rgb>(hex: u32) -> C { let r = ((hex >> 16) & 0xFF) as f32 / 255.0; @@ -19,7 +19,7 @@ pub fn rgba(hex: u32) -> Rgba { Rgba { r, g, b, a } } -#[derive(Clone, Copy, Default)] +#[derive(PartialEq, Clone, Copy, Default)] pub struct Rgba { pub r: f32, pub g: f32, @@ -70,21 +70,7 @@ impl<'de> Visitor<'de> for RgbaVisitor { } fn visit_str(self, value: &str) -> Result { - if value.len() == 7 || value.len() == 9 { - let r = u8::from_str_radix(&value[1..3], 16).unwrap() as f32 / 255.0; - let g = u8::from_str_radix(&value[3..5], 16).unwrap() as f32 / 255.0; - let b = u8::from_str_radix(&value[5..7], 16).unwrap() as f32 / 255.0; - let a = if value.len() == 9 { - u8::from_str_radix(&value[7..9], 16).unwrap() as f32 / 255.0 - } else { - 1.0 - }; - Ok(Rgba { r, g, b, a }) - } else { - Err(E::custom( - "Bad format for RGBA. Expected #rrggbb or #rrggbbaa.", - )) - } + Rgba::try_from(value).map_err(E::custom) } } @@ -125,19 +111,59 @@ impl From for Rgba { } impl TryFrom<&'_ str> for Rgba { - type Error = ParseIntError; + type Error = anyhow::Error; fn try_from(value: &'_ str) -> Result { - let r = u8::from_str_radix(&value[1..3], 16)? as f32 / 255.0; - let g = u8::from_str_radix(&value[3..5], 16)? as f32 / 255.0; - let b = u8::from_str_radix(&value[5..7], 16)? as f32 / 255.0; - let a = if value.len() > 7 { - u8::from_str_radix(&value[7..9], 16)? as f32 / 255.0 - } else { - 1.0 + const RGB: usize = "rgb".len(); + const RGBA: usize = "rgba".len(); + const RRGGBB: usize = "rrggbb".len(); + const RRGGBBAA: usize = "rrggbbaa".len(); + + const EXPECTED_FORMATS: &'static str = "Expected #rgb, #rgba, #rrggbb, or #rrggbbaa"; + + let Some(("", hex)) = value.trim().split_once('#') else { + bail!("invalid RGBA hex color: '{value}'. {EXPECTED_FORMATS}"); + }; + + let (r, g, b, a) = match hex.len() { + RGB | RGBA => { + let r = u8::from_str_radix(&hex[0..1], 16)?; + let g = u8::from_str_radix(&hex[1..2], 16)?; + let b = u8::from_str_radix(&hex[2..3], 16)?; + let a = if hex.len() == RGBA { + u8::from_str_radix(&hex[3..4], 16)? + } else { + 0xf + }; + + /// Duplicates a given hex digit. + /// E.g., `0xf` -> `0xff`. + const fn duplicate(value: u8) -> u8 { + value << 4 | value + } + + (duplicate(r), duplicate(g), duplicate(b), duplicate(a)) + } + RRGGBB | RRGGBBAA => { + let r = u8::from_str_radix(&hex[0..2], 16)?; + let g = u8::from_str_radix(&hex[2..4], 16)?; + let b = u8::from_str_radix(&hex[4..6], 16)?; + let a = if hex.len() == RRGGBBAA { + u8::from_str_radix(&hex[6..8], 16)? + } else { + 0xff + }; + (r, g, b, a) + } + _ => bail!("invalid RGBA hex color: '{value}'. {EXPECTED_FORMATS}"), }; - Ok(Rgba { r, g, b, a }) + Ok(Rgba { + r: r as f32 / 255., + g: g as f32 / 255., + b: b as f32 / 255., + a: a as f32 / 255., + }) } } @@ -344,3 +370,52 @@ impl<'de> Deserialize<'de> for Hsla { Ok(Hsla::from(rgba)) } } + +#[cfg(test)] +mod tests { + use serde_json::json; + + use super::*; + + #[test] + fn test_deserialize_three_value_hex_to_rgba() { + let actual: Rgba = serde_json::from_value(json!("#f09")).unwrap(); + + assert_eq!(actual, rgba(0xff0099ff)) + } + + #[test] + fn test_deserialize_four_value_hex_to_rgba() { + let actual: Rgba = serde_json::from_value(json!("#f09f")).unwrap(); + + assert_eq!(actual, rgba(0xff0099ff)) + } + + #[test] + fn test_deserialize_six_value_hex_to_rgba() { + let actual: Rgba = serde_json::from_value(json!("#ff0099")).unwrap(); + + assert_eq!(actual, rgba(0xff0099ff)) + } + + #[test] + fn test_deserialize_eight_value_hex_to_rgba() { + let actual: Rgba = serde_json::from_value(json!("#ff0099ff")).unwrap(); + + assert_eq!(actual, rgba(0xff0099ff)) + } + + #[test] + fn test_deserialize_eight_value_hex_with_padding_to_rgba() { + let actual: Rgba = serde_json::from_value(json!(" #f5f5f5ff ")).unwrap(); + + assert_eq!(actual, rgba(0xf5f5f5ff)) + } + + #[test] + fn test_deserialize_eight_value_hex_with_mixed_case_to_rgba() { + let actual: Rgba = serde_json::from_value(json!("#DeAdbEeF")).unwrap(); + + assert_eq!(actual, rgba(0xdeadbeef)) + } +} diff --git a/crates/gpui2/src/element.rs b/crates/gpui2/src/element.rs index 8fdc17de07296d95def915f3a605f3988913eb2a..9ee9eaa7c335960f3e3c6974b0a8798c3d13f9c4 100644 --- a/crates/gpui2/src/element.rs +++ b/crates/gpui2/src/element.rs @@ -1,4 +1,6 @@ -use crate::{BorrowWindow, Bounds, ElementId, LayoutId, Pixels, ViewContext}; +use crate::{ + AvailableSpace, BorrowWindow, Bounds, ElementId, LayoutId, Pixels, Point, Size, ViewContext, +}; use derive_more::{Deref, DerefMut}; pub(crate) use smallvec::SmallVec; use std::{any::Any, mem}; @@ -61,6 +63,19 @@ trait ElementObject { fn initialize(&mut self, view_state: &mut V, cx: &mut ViewContext); fn layout(&mut self, view_state: &mut V, cx: &mut ViewContext) -> LayoutId; fn paint(&mut self, view_state: &mut V, cx: &mut ViewContext); + fn measure( + &mut self, + available_space: Size, + view_state: &mut V, + cx: &mut ViewContext, + ) -> Size; + fn draw( + &mut self, + origin: Point, + available_space: Size, + view_state: &mut V, + cx: &mut ViewContext, + ); } struct RenderedElement> { @@ -79,6 +94,11 @@ enum ElementRenderPhase { layout_id: LayoutId, frame_state: Option, }, + LayoutComputed { + layout_id: LayoutId, + available_space: Size, + frame_state: Option, + }, Painted, } @@ -135,7 +155,9 @@ where } } ElementRenderPhase::Start => panic!("must call initialize before layout"), - ElementRenderPhase::LayoutRequested { .. } | ElementRenderPhase::Painted => { + ElementRenderPhase::LayoutRequested { .. } + | ElementRenderPhase::LayoutComputed { .. } + | ElementRenderPhase::Painted => { panic!("element rendered twice") } }; @@ -152,6 +174,11 @@ where ElementRenderPhase::LayoutRequested { layout_id, mut frame_state, + } + | ElementRenderPhase::LayoutComputed { + layout_id, + mut frame_state, + .. } => { let bounds = cx.layout_bounds(layout_id); if let Some(id) = self.element.id() { @@ -171,6 +198,65 @@ where _ => panic!("must call layout before paint"), }; } + + fn measure( + &mut self, + available_space: Size, + view_state: &mut V, + cx: &mut ViewContext, + ) -> Size { + if matches!(&self.phase, ElementRenderPhase::Start) { + self.initialize(view_state, cx); + } + + if matches!(&self.phase, ElementRenderPhase::Initialized { .. }) { + self.layout(view_state, cx); + } + + let layout_id = match &mut self.phase { + ElementRenderPhase::LayoutRequested { + layout_id, + frame_state, + } => { + cx.compute_layout(*layout_id, available_space); + let layout_id = *layout_id; + self.phase = ElementRenderPhase::LayoutComputed { + layout_id, + available_space, + frame_state: frame_state.take(), + }; + layout_id + } + ElementRenderPhase::LayoutComputed { + layout_id, + available_space: prev_available_space, + .. + } => { + if available_space != *prev_available_space { + cx.compute_layout(*layout_id, available_space); + *prev_available_space = available_space; + } + *layout_id + } + _ => panic!("cannot measure after painting"), + }; + + cx.layout_bounds(layout_id).size + } + + fn draw( + &mut self, + mut origin: Point, + available_space: Size, + view_state: &mut V, + cx: &mut ViewContext, + ) { + self.measure(available_space, view_state, cx); + // Ignore the element offset when drawing this element, as the origin is already specified + // in absolute terms. + origin -= cx.element_offset(); + cx.with_element_offset(Some(origin), |cx| self.paint(view_state, cx)) + } } pub struct AnyElement(Box>); @@ -196,6 +282,27 @@ impl AnyElement { pub fn paint(&mut self, view_state: &mut V, cx: &mut ViewContext) { self.0.paint(view_state, cx) } + + /// Initializes this element and performs layout within the given available space to determine its size. + pub fn measure( + &mut self, + available_space: Size, + view_state: &mut V, + cx: &mut ViewContext, + ) -> Size { + self.0.measure(available_space, view_state, cx) + } + + /// Initializes this element and performs layout in the available space, then paints it at the given origin. + pub fn draw( + &mut self, + origin: Point, + available_space: Size, + view_state: &mut V, + cx: &mut ViewContext, + ) { + self.0.draw(origin, available_space, view_state, cx) + } } pub trait Component { diff --git a/crates/gpui2/src/elements/text.rs b/crates/gpui2/src/elements/text.rs index e258d3e7dc6dba8c7d7c625981b7ef340c1dc96c..5c5709d32e5a12e247726789a35dfd21806c6c7c 100644 --- a/crates/gpui2/src/elements/text.rs +++ b/crates/gpui2/src/elements/text.rs @@ -101,7 +101,12 @@ impl Element for Text { .map(|line| line.wrap_count() + 1) .sum::(); let size = Size { - width: lines.iter().map(|line| line.layout.width).max().unwrap(), + width: lines + .iter() + .map(|line| line.layout.width) + .max() + .unwrap() + .ceil(), height: line_height * line_count, }; diff --git a/crates/gpui2/src/elements/uniform_list.rs b/crates/gpui2/src/elements/uniform_list.rs index e1160227637c8374fa47e922a0fabb509308ec1e..6687559d1c811349a3f18a5585f2d2d4110eb16e 100644 --- a/crates/gpui2/src/elements/uniform_list.rs +++ b/crates/gpui2/src/elements/uniform_list.rs @@ -1,6 +1,6 @@ use crate::{ - point, px, AnyElement, AvailableSpace, BorrowWindow, Bounds, Component, Element, ElementId, - ElementInteractivity, InteractiveElementState, LayoutId, Pixels, Point, Size, + point, px, size, AnyElement, AvailableSpace, BorrowWindow, Bounds, Component, Element, + ElementId, ElementInteractivity, InteractiveElementState, LayoutId, Pixels, Point, Size, StatefulInteractive, StatefulInteractivity, StatelessInteractive, StatelessInteractivity, StyleRefinement, Styled, ViewContext, }; @@ -9,6 +9,9 @@ use smallvec::SmallVec; use std::{cmp, ops::Range, sync::Arc}; use taffy::style::Overflow; +/// uniform_list provides lazy rendering for a set of items that are of uniform height. +/// When rendered into a container with overflow-y: hidden and a fixed (or max) height, +/// uniform_list will only render the visibile subset of items. pub fn uniform_list( id: Id, item_count: usize, @@ -20,10 +23,14 @@ where C: Component, { let id = id.into(); + let mut style = StyleRefinement::default(); + style.overflow.y = Some(Overflow::Hidden); + UniformList { id: id.clone(), - style: Default::default(), + style, item_count, + item_to_measure_index: 0, render_items: Box::new(move |view, visible_range, cx| { f(view, visible_range, cx) .into_iter() @@ -39,6 +46,7 @@ pub struct UniformList { id: ElementId, style: StyleRefinement, item_count: usize, + item_to_measure_index: usize, render_items: Box< dyn for<'a> Fn( &'a mut V, @@ -50,7 +58,7 @@ pub struct UniformList { scroll_handle: Option, } -#[derive(Clone)] +#[derive(Clone, Default)] pub struct UniformListScrollHandle(Arc>>); #[derive(Clone, Debug)] @@ -86,8 +94,14 @@ impl Styled for UniformList { } } +#[derive(Default)] +pub struct UniformListState { + interactive: InteractiveElementState, + item_size: Size, +} + impl Element for UniformList { - type ElementState = InteractiveElementState; + type ElementState = UniformListState; fn id(&self) -> Option { Some(self.id.clone()) @@ -95,20 +109,47 @@ impl Element for UniformList { fn initialize( &mut self, - _: &mut V, + view_state: &mut V, element_state: Option, - _: &mut ViewContext, + cx: &mut ViewContext, ) -> Self::ElementState { - element_state.unwrap_or_default() + element_state.unwrap_or_else(|| { + let item_size = self.measure_item(view_state, None, cx); + UniformListState { + interactive: InteractiveElementState::default(), + item_size, + } + }) } fn layout( &mut self, _view_state: &mut V, - _element_state: &mut Self::ElementState, + element_state: &mut Self::ElementState, cx: &mut ViewContext, ) -> LayoutId { - cx.request_layout(&self.computed_style(), None) + let max_items = self.item_count; + let item_size = element_state.item_size; + let rem_size = cx.rem_size(); + + cx.request_measured_layout( + self.computed_style(), + rem_size, + move |known_dimensions: Size>, available_space: Size| { + let desired_height = item_size.height * max_items; + let width = known_dimensions + .width + .unwrap_or(match available_space.width { + AvailableSpace::Definite(x) => x, + AvailableSpace::MinContent | AvailableSpace::MaxContent => item_size.width, + }); + let height = match available_space.height { + AvailableSpace::Definite(x) => desired_height.min(x), + AvailableSpace::MinContent | AvailableSpace::MaxContent => desired_height, + }; + size(width, height) + }, + ) } fn paint( @@ -119,7 +160,6 @@ impl Element for UniformList { cx: &mut ViewContext, ) { let style = self.computed_style(); - style.paint(bounds, cx); let border = style.border_widths.to_pixels(cx.rem_size()); let padding = style.padding.to_pixels(bounds.size.into(), cx.rem_size()); @@ -131,14 +171,18 @@ impl Element for UniformList { ); cx.with_z_index(style.z_index.unwrap_or(0), |cx| { + style.paint(bounds, cx); + let content_size; if self.item_count > 0 { - let item_height = self.measure_item_height(view_state, padded_bounds, cx); + let item_height = self + .measure_item(view_state, Some(padded_bounds.size.width), cx) + .height; if let Some(scroll_handle) = self.scroll_handle.clone() { scroll_handle.0.lock().replace(ScrollHandleState { item_height, list_height: padded_bounds.size.height, - scroll_offset: element_state.track_scroll_offset(), + scroll_offset: element_state.interactive.track_scroll_offset(), }); } let visible_item_count = if item_height > px(0.) { @@ -147,6 +191,7 @@ impl Element for UniformList { 0 }; let scroll_offset = element_state + .interactive .scroll_offset() .map_or((0.0).into(), |offset| offset.y); let first_visible_element_ix = (-scroll_offset / item_height).floor() as usize; @@ -165,19 +210,13 @@ impl Element for UniformList { cx.with_z_index(1, |cx| { for (item, ix) in items.iter_mut().zip(visible_range) { - item.initialize(view_state, cx); - - let layout_id = item.layout(view_state, cx); - cx.compute_layout( - layout_id, - Size { - width: AvailableSpace::Definite(bounds.size.width), - height: AvailableSpace::Definite(item_height), - }, - ); - let offset = + let item_origin = padded_bounds.origin + point(px(0.), item_height * ix + scroll_offset); - cx.with_element_offset(Some(offset), |cx| item.paint(view_state, cx)) + let available_space = size( + AvailableSpace::Definite(padded_bounds.size.width), + AvailableSpace::Definite(item_height), + ); + item.draw(item_origin, available_space, view_state, cx); } }); } else { @@ -190,33 +229,44 @@ impl Element for UniformList { let overflow = point(style.overflow.x, Overflow::Scroll); cx.with_z_index(0, |cx| { - self.interactivity - .paint(bounds, content_size, overflow, element_state, cx); + self.interactivity.paint( + bounds, + content_size, + overflow, + &mut element_state.interactive, + cx, + ); }); }) } } impl UniformList { - fn measure_item_height( + pub fn with_width_from_item(mut self, item_index: Option) -> Self { + self.item_to_measure_index = item_index.unwrap_or(0); + self + } + + fn measure_item( &self, view_state: &mut V, - list_bounds: Bounds, + list_width: Option, cx: &mut ViewContext, - ) -> Pixels { - let mut items = (self.render_items)(view_state, 0..1, cx); - debug_assert!(items.len() == 1); + ) -> Size { + if self.item_count == 0 { + return Size::default(); + } + + let item_ix = cmp::min(self.item_to_measure_index, self.item_count - 1); + let mut items = (self.render_items)(view_state, item_ix..item_ix + 1, cx); let mut item_to_measure = items.pop().unwrap(); - item_to_measure.initialize(view_state, cx); - let layout_id = item_to_measure.layout(view_state, cx); - cx.compute_layout( - layout_id, - Size { - width: AvailableSpace::Definite(list_bounds.size.width), - height: AvailableSpace::MinContent, - }, + let available_space = size( + list_width.map_or(AvailableSpace::MinContent, |width| { + AvailableSpace::Definite(width) + }), + AvailableSpace::MinContent, ); - cx.layout_bounds(layout_id).size.height + item_to_measure.measure(available_space, view_state, cx) } pub fn track_scroll(mut self, handle: UniformListScrollHandle) -> Self { diff --git a/crates/gpui2/src/geometry.rs b/crates/gpui2/src/geometry.rs index c5209239aea6ed84bc43d715d52b6b1e0a895c7c..e07300951ec61429ba617927649406409e74b531 100644 --- a/crates/gpui2/src/geometry.rs +++ b/crates/gpui2/src/geometry.rs @@ -9,7 +9,7 @@ use std::{ }; #[derive(Refineable, Default, Add, AddAssign, Sub, SubAssign, Copy, Debug, PartialEq, Eq, Hash)] -#[refineable(debug)] +#[refineable(Debug)] #[repr(C)] pub struct Point { pub x: T, @@ -140,7 +140,7 @@ impl Clone for Point { } #[derive(Refineable, Default, Clone, Copy, PartialEq, Div, Hash, Serialize, Deserialize)] -#[refineable(debug)] +#[refineable(Debug)] #[repr(C)] pub struct Size { pub width: T, @@ -313,7 +313,7 @@ impl Size { } #[derive(Refineable, Clone, Default, Debug, Eq, PartialEq)] -#[refineable(debug)] +#[refineable(Debug)] #[repr(C)] pub struct Bounds { pub origin: Point, @@ -477,7 +477,7 @@ impl Bounds { impl Copy for Bounds {} #[derive(Refineable, Clone, Default, Debug, Eq, PartialEq)] -#[refineable(debug)] +#[refineable(Debug)] #[repr(C)] pub struct Edges { pub top: T, @@ -619,7 +619,7 @@ impl Edges { } #[derive(Refineable, Clone, Default, Debug, Eq, PartialEq)] -#[refineable(debug)] +#[refineable(Debug)] #[repr(C)] pub struct Corners { pub top_left: T, @@ -785,6 +785,10 @@ impl Pixels { Self(self.0.round()) } + pub fn ceil(&self) -> Self { + Self(self.0.ceil()) + } + pub fn scale(&self, factor: f32) -> ScaledPixels { ScaledPixels(self.0 * factor) } diff --git a/crates/gpui2/src/interactive.rs b/crates/gpui2/src/interactive.rs index a546c1b40b9cbb073f3539133c29b1714fffd951..243eb3cb07844a2fa558d6c1bf2555f75cf1af95 100644 --- a/crates/gpui2/src/interactive.rs +++ b/crates/gpui2/src/interactive.rs @@ -94,7 +94,6 @@ pub trait StatelessInteractive: Element { fn on_mouse_down_out( mut self, - button: MouseButton, handler: impl Fn(&mut V, &MouseDownEvent, &mut ViewContext) + 'static, ) -> Self where @@ -103,10 +102,7 @@ pub trait StatelessInteractive: Element { self.stateless_interactivity() .mouse_down_listeners .push(Box::new(move |view, event, bounds, phase, cx| { - if phase == DispatchPhase::Capture - && event.button == button - && !bounds.contains_point(&event.position) - { + if phase == DispatchPhase::Capture && !bounds.contains_point(&event.position) { handler(view, event, cx) } })); diff --git a/crates/gpui2/src/platform/test/platform.rs b/crates/gpui2/src/platform/test/platform.rs index 37978e7ad7fe7e4b4df812cf3116f15239d6d2a8..4afcc4fc1ae436ef0b089809ac5e1b5b86207076 100644 --- a/crates/gpui2/src/platform/test/platform.rs +++ b/crates/gpui2/src/platform/test/platform.rs @@ -182,7 +182,8 @@ impl Platform for TestPlatform { } fn should_auto_hide_scrollbars(&self) -> bool { - unimplemented!() + // todo() + true } fn write_to_clipboard(&self, _item: crate::ClipboardItem) { diff --git a/crates/gpui2/src/platform/test/window.rs b/crates/gpui2/src/platform/test/window.rs index f1327196556dbf93871ec86da50f4108d9a32846..289ecf7e6b2b5d0231ef99d56a2364ca61695c7a 100644 --- a/crates/gpui2/src/platform/test/window.rs +++ b/crates/gpui2/src/platform/test/window.rs @@ -1,10 +1,14 @@ -use std::{rc::Rc, sync::Arc}; +use std::{ + rc::Rc, + sync::{self, Arc}, +}; +use collections::HashMap; use parking_lot::Mutex; use crate::{ - px, Pixels, PlatformAtlas, PlatformDisplay, PlatformWindow, Point, Scene, Size, - WindowAppearance, WindowBounds, WindowOptions, + px, AtlasKey, AtlasTextureId, AtlasTile, Pixels, PlatformAtlas, PlatformDisplay, + PlatformWindow, Point, Scene, Size, TileId, WindowAppearance, WindowBounds, WindowOptions, }; #[derive(Default)] @@ -30,7 +34,7 @@ impl TestWindow { current_scene: Default::default(), display, - sprite_atlas: Arc::new(TestAtlas), + sprite_atlas: Arc::new(TestAtlas::new()), handlers: Default::default(), } } @@ -154,26 +158,71 @@ impl PlatformWindow for TestWindow { self.current_scene.lock().replace(scene); } - fn sprite_atlas(&self) -> std::sync::Arc { + fn sprite_atlas(&self) -> sync::Arc { self.sprite_atlas.clone() } } -pub struct TestAtlas; +pub struct TestAtlasState { + next_id: u32, + tiles: HashMap, +} + +pub struct TestAtlas(Mutex); + +impl TestAtlas { + pub fn new() -> Self { + TestAtlas(Mutex::new(TestAtlasState { + next_id: 0, + tiles: HashMap::default(), + })) + } +} impl PlatformAtlas for TestAtlas { fn get_or_insert_with<'a>( &self, - _key: &crate::AtlasKey, - _build: &mut dyn FnMut() -> anyhow::Result<( + key: &crate::AtlasKey, + build: &mut dyn FnMut() -> anyhow::Result<( Size, std::borrow::Cow<'a, [u8]>, )>, ) -> anyhow::Result { - todo!() + let mut state = self.0.lock(); + if let Some(tile) = state.tiles.get(key) { + return Ok(tile.clone()); + } + + state.next_id += 1; + let texture_id = state.next_id; + state.next_id += 1; + let tile_id = state.next_id; + + drop(state); + let (size, _) = build()?; + let mut state = self.0.lock(); + + state.tiles.insert( + key.clone(), + crate::AtlasTile { + texture_id: AtlasTextureId { + index: texture_id, + kind: crate::AtlasTextureKind::Path, + }, + tile_id: TileId(tile_id), + bounds: crate::Bounds { + origin: Point::zero(), + size, + }, + }, + ); + + Ok(state.tiles[key].clone()) } fn clear(&self) { - todo!() + let mut state = self.0.lock(); + state.tiles = HashMap::default(); + state.next_id = 0; } } diff --git a/crates/gpui2/src/style.rs b/crates/gpui2/src/style.rs index 6d3b31c1f7a2817cbf45b6b8c4221d045f6992ca..664cc61f8a1bf100678bc13d2cf796019380f9e5 100644 --- a/crates/gpui2/src/style.rs +++ b/crates/gpui2/src/style.rs @@ -14,7 +14,7 @@ pub use taffy::style::{ pub type StyleCascade = Cascade