Compare commits

..

1 Commits

Author SHA1 Message Date
Nia Espera
bbd9b9f2e9 add insane test 2025-09-30 17:53:38 +02:00
158 changed files with 4117 additions and 5563 deletions

33
.github/workflows/issue_response.yml vendored Normal file
View File

@@ -0,0 +1,33 @@
name: Issue Response
on:
schedule:
- cron: "0 12 * * 2"
workflow_dispatch:
jobs:
issue-response:
if: github.repository_owner == 'zed-industries'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 # v4.0.0
with:
version: 9
- name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: "20"
cache: "pnpm"
cache-dependency-path: "script/issue_response/pnpm-lock.yaml"
- run: pnpm install --dir script/issue_response
- name: Run Issue Response
run: pnpm run --dir script/issue_response start
env:
ISSUE_RESPONSE_GITHUB_TOKEN: ${{ secrets.ISSUE_RESPONSE_GITHUB_TOKEN }}
SLACK_ISSUE_RESPONSE_WEBHOOK_URL: ${{ secrets.SLACK_ISSUE_RESPONSE_WEBHOOK_URL }}

88
Cargo.lock generated
View File

@@ -516,7 +516,7 @@ dependencies = [
"rustix-openpty",
"serde",
"signal-hook",
"unicode-width",
"unicode-width 0.2.0",
"vte",
"windows-sys 0.59.0",
]
@@ -1411,6 +1411,7 @@ dependencies = [
"log",
"parking_lot",
"rodio",
"rubato",
"serde",
"settings",
"smol",
@@ -2307,15 +2308,14 @@ dependencies = [
[[package]]
name = "blade-graphics"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e4deb8f595ce7f00dee3543ebf6fd9a20ea86fc421ab79600dac30876250bdae"
version = "0.6.0"
source = "git+https://github.com/kvark/blade?rev=bfa594ea697d4b6326ea29f747525c85ecf933b9#bfa594ea697d4b6326ea29f747525c85ecf933b9"
dependencies = [
"ash",
"ash-window",
"bitflags 2.9.0",
"bytemuck",
"codespan-reporting",
"codespan-reporting 0.11.1",
"glow",
"gpu-alloc",
"gpu-alloc-ash",
@@ -2333,7 +2333,6 @@ dependencies = [
"objc2-metal",
"objc2-quartz-core",
"objc2-ui-kit",
"once_cell",
"raw-window-handle",
"slab",
"wasm-bindgen",
@@ -2343,8 +2342,7 @@ dependencies = [
[[package]]
name = "blade-macros"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "27142319e2f4c264581067eaccb9f80acccdde60d8b4bf57cc50cd3152f109ca"
source = "git+https://github.com/kvark/blade?rev=bfa594ea697d4b6326ea29f747525c85ecf933b9#bfa594ea697d4b6326ea29f747525c85ecf933b9"
dependencies = [
"proc-macro2",
"quote",
@@ -2353,9 +2351,8 @@ dependencies = [
[[package]]
name = "blade-util"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a6be3a82c001ba7a17b6f8e413ede5d1004e6047213f8efaf0ffc15b5c4904c"
version = "0.2.0"
source = "git+https://github.com/kvark/blade?rev=bfa594ea697d4b6326ea29f747525c85ecf933b9#bfa594ea697d4b6326ea29f747525c85ecf933b9"
dependencies = [
"blade-graphics",
"bytemuck",
@@ -3084,7 +3081,6 @@ name = "cli"
version = "0.1.0"
dependencies = [
"anyhow",
"askpass",
"clap",
"collections",
"core-foundation 0.10.0",
@@ -3221,7 +3217,6 @@ dependencies = [
"indoc",
"ordered-float 2.10.1",
"rustc-hash 2.1.1",
"serde",
"strum 0.27.1",
"workspace-hack",
]
@@ -3301,6 +3296,16 @@ dependencies = [
"objc",
]
[[package]]
name = "codespan-reporting"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e"
dependencies = [
"termcolor",
"unicode-width 0.1.14",
]
[[package]]
name = "codespan-reporting"
version = "0.12.0"
@@ -3309,7 +3314,7 @@ checksum = "fe6d2e5af09e8c8ad56c969f2157a3d4238cebc7c55f0a517728c38f7b200f81"
dependencies = [
"serde",
"termcolor",
"unicode-width",
"unicode-width 0.2.0",
]
[[package]]
@@ -3385,6 +3390,7 @@ dependencies = [
"reqwest 0.11.27",
"reqwest_client",
"rpc",
"rustc-demangle",
"scrypt",
"sea-orm",
"semantic_version",
@@ -3577,7 +3583,7 @@ dependencies = [
"encode_unicode",
"libc",
"once_cell",
"unicode-width",
"unicode-width 0.2.0",
"windows-sys 0.59.0",
]
@@ -4106,9 +4112,9 @@ dependencies = [
[[package]]
name = "crc"
version = "3.3.0"
version = "3.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675"
checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636"
dependencies = [
"crc-catalog",
]
@@ -4367,7 +4373,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b4400e26ea4b99417e4263b1ce2d8452404d750ba0809a7bd043072593d430d"
dependencies = [
"cc",
"codespan-reporting",
"codespan-reporting 0.12.0",
"proc-macro2",
"quote",
"scratch",
@@ -4381,7 +4387,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "31860c98f69fc14da5742c5deaf78983e846c7b27804ca8c8319e32eef421bde"
dependencies = [
"clap",
"codespan-reporting",
"codespan-reporting 0.12.0",
"proc-macro2",
"quote",
"syn 2.0.101",
@@ -5172,7 +5178,6 @@ dependencies = [
"language",
"log",
"ordered-float 2.10.1",
"postage",
"pretty_assertions",
"project",
"regex",
@@ -6938,9 +6943,9 @@ dependencies = [
[[package]]
name = "glow"
version = "0.16.0"
version = "0.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c5e5ea60d70410161c8bf5da3fdfeaa1c72ed2c15f8bbb9d19fe3a4fad085f08"
checksum = "d51fa363f025f5c111e03f13eda21162faeacb6911fe8caa0c0349f9cf0c4483"
dependencies = [
"js-sys",
"slotmap",
@@ -9922,7 +9927,7 @@ dependencies = [
"bit-set 0.8.0",
"bitflags 2.9.0",
"cfg_aliases 0.2.1",
"codespan-reporting",
"codespan-reporting 0.12.0",
"half",
"hashbrown 0.15.3",
"hexf-parse",
@@ -13506,6 +13511,18 @@ version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad8388ea1a9e0ea807e442e8263a699e7edcb320ecbcd21b4fa8ff859acce3ba"
[[package]]
name = "rubato"
version = "0.16.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5258099699851cfd0082aeb645feb9c084d9a5e1f1b8d5372086b989fc5e56a1"
dependencies = [
"num-complex",
"num-integer",
"num-traits",
"realfft",
]
[[package]]
name = "rules_library"
version = "0.1.0"
@@ -14532,7 +14549,6 @@ dependencies = [
"feature_flags",
"fs",
"futures 0.3.31",
"fuzzy",
"gpui",
"language",
"menu",
@@ -14570,9 +14586,9 @@ checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d"
[[package]]
name = "sha2"
version = "0.10.9"
version = "0.10.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8"
dependencies = [
"cfg-if",
"cpufeatures",
@@ -16899,9 +16915,9 @@ dependencies = [
[[package]]
name = "tree-sitter"
version = "0.25.10"
version = "0.25.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78f873475d258561b06f1c595d93308a7ed124d9977cb26b148c2084a4a3cc87"
checksum = "a7cf18d43cbf0bfca51f657132cc616a5097edc4424d538bae6fa60142eaf9f0"
dependencies = [
"cc",
"regex",
@@ -17424,6 +17440,12 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94"
[[package]]
name = "unicode-width"
version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
[[package]]
name = "unicode-width"
version = "0.2.0"
@@ -17698,7 +17720,6 @@ dependencies = [
"language",
"log",
"lsp",
"menu",
"multi_buffer",
"nvim-rs",
"parking_lot",
@@ -19676,7 +19697,7 @@ dependencies = [
"cipher",
"clap",
"clap_builder",
"codespan-reporting",
"codespan-reporting 0.12.0",
"concurrent-queue",
"core-foundation 0.9.4",
"core-foundation-sys",
@@ -20196,7 +20217,7 @@ dependencies = [
[[package]]
name = "zed"
version = "0.207.1"
version = "0.207.0"
dependencies = [
"acp_tools",
"activity_indicator",
@@ -20205,6 +20226,7 @@ dependencies = [
"agent_ui",
"anyhow",
"ashpd 0.11.0",
"askpass",
"assets",
"assistant_tools",
"audio",
@@ -20330,6 +20352,7 @@ dependencies = [
"url",
"urlencoding",
"util",
"util_macros",
"uuid",
"vim",
"vim_mode_setting",
@@ -20718,9 +20741,7 @@ dependencies = [
"language_model",
"language_models",
"languages",
"log",
"node_runtime",
"ordered-float 2.10.1",
"paths",
"project",
"prompt_store",
@@ -20737,7 +20758,6 @@ dependencies = [
"workspace-hack",
"zeta",
"zeta2",
"zlog",
]
[[package]]

View File

@@ -474,9 +474,9 @@ backtrace = "0.3"
base64 = "0.22"
bincode = "1.2.1"
bitflags = "2.6.0"
blade-graphics = { version = "0.7.0" }
blade-macros = { version = "0.3.0" }
blade-util = { version = "0.3.0" }
blade-graphics = { git = "https://github.com/kvark/blade", rev = "bfa594ea697d4b6326ea29f747525c85ecf933b9" }
blade-macros = { git = "https://github.com/kvark/blade", rev = "bfa594ea697d4b6326ea29f747525c85ecf933b9" }
blade-util = { git = "https://github.com/kvark/blade", rev = "bfa594ea697d4b6326ea29f747525c85ecf933b9" }
blake3 = "1.5.3"
bytes = "1.0"
cargo_metadata = "0.19"
@@ -620,6 +620,7 @@ runtimelib = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c804
"async-dispatcher-runtime",
] }
rust-embed = { version = "8.4", features = ["include-exclude"] }
rustc-demangle = "0.1.23"
rustc-hash = "2.1.0"
rustls = { version = "0.23.26" }
rustls-platform-verifier = "0.5.0"
@@ -668,7 +669,7 @@ tokio = { version = "1" }
tokio-tungstenite = { version = "0.26", features = ["__rustls-tls"] }
toml = "0.8"
tower-http = "0.4.4"
tree-sitter = { version = "0.25.10", features = ["wasm"] }
tree-sitter = { version = "0.25.6", features = ["wasm"] }
tree-sitter-bash = "0.25.0"
tree-sitter-c = "0.23"
tree-sitter-cpp = { git = "https://github.com/tree-sitter/tree-sitter-cpp", rev = "5cb9b693cfd7bfacab1d9ff4acac1a4150700609" }

View File

@@ -1,3 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.1645 4.45825L5.20344 9.52074C4.98225 9.74193 4.85798 10.0419 4.85798 10.3548C4.85798 10.6676 4.98225 10.9676 5.20344 11.1888C5.42464 11.41 5.72464 11.5342 6.03746 11.5342C6.35028 11.5342 6.65028 11.41 6.87148 11.1888L11.8326 6.12629C12.2749 5.68397 12.5234 5.08407 12.5234 4.45854C12.5234 3.83302 12.2749 3.23311 11.8326 2.7908C11.3902 2.34849 10.7903 2.1 10.1648 2.1C9.53928 2.1 8.93938 2.34849 8.49707 2.7908L3.55663 7.83265C3.22373 8.16017 2.95897 8.55037 2.77762 8.98072C2.59628 9.41108 2.50193 9.87308 2.50003 10.3401C2.49813 10.8071 2.58871 11.2698 2.76654 11.7017C2.94438 12.1335 3.20595 12.5258 3.53618 12.856C3.8664 13.1863 4.25873 13.4478 4.69055 13.6257C5.12237 13.8035 5.58513 13.8941 6.05213 13.8922C6.51913 13.8903 6.98114 13.7959 7.41149 13.6146C7.84185 13.4332 8.23204 13.1685 8.55957 12.8356L13.5 7.79373" stroke="#C4CAD4" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -250,7 +250,7 @@
"alt-enter": "agent::ContinueWithBurnMode",
"ctrl-y": "agent::AllowOnce",
"ctrl-alt-y": "agent::AllowAlways",
"ctrl-alt-z": "agent::RejectOnce"
"ctrl-d": "agent::RejectOnce"
}
},
{

View File

@@ -289,7 +289,7 @@
"alt-enter": "agent::ContinueWithBurnMode",
"cmd-y": "agent::AllowOnce",
"cmd-alt-y": "agent::AllowAlways",
"cmd-alt-z": "agent::RejectOnce"
"cmd-d": "agent::RejectOnce"
}
},
{

View File

@@ -251,7 +251,7 @@
"alt-enter": "agent::ContinueWithBurnMode",
"ctrl-y": "agent::AllowOnce",
"ctrl-alt-y": "agent::AllowAlways",
"ctrl-alt-z": "agent::RejectOnce"
"ctrl-d": "agent::RejectOnce"
}
},
{

View File

@@ -240,7 +240,6 @@
"delete": "vim::DeleteRight",
"g shift-j": "vim::JoinLinesNoWhitespace",
"y": "vim::PushYank",
"shift-y": "vim::YankToEndOfLine",
"x": "vim::DeleteRight",
"shift-x": "vim::DeleteLeft",
"ctrl-a": "vim::Increment",
@@ -393,7 +392,7 @@
"escape": "editor::Cancel",
"shift-d": "vim::DeleteToEndOfLine",
"shift-j": "vim::JoinLines",
"shift-y": "vim::YankToEndOfLine",
"shift-y": "vim::YankLine",
"shift-i": "vim::InsertFirstNonWhitespace",
"shift-a": "vim::InsertEndOfLine",
"o": "vim::InsertLineBelow",
@@ -884,12 +883,10 @@
"/": "project_panel::NewSearchInDirectory",
"d": "project_panel::NewDirectory",
"enter": "project_panel::OpenPermanent",
"escape": "vim::ToggleProjectPanelFocus",
"escape": "project_panel::ToggleFocus",
"h": "project_panel::CollapseSelectedEntry",
"j": "vim::MenuSelectNext",
"k": "vim::MenuSelectPrevious",
"down": "vim::MenuSelectNext",
"up": "vim::MenuSelectPrevious",
"j": "menu::SelectNext",
"k": "menu::SelectPrevious",
"l": "project_panel::ExpandSelectedEntry",
"shift-d": "project_panel::Delete",
"shift-r": "project_panel::Rename",
@@ -908,22 +905,7 @@
"{": "project_panel::SelectPrevDirectory",
"shift-g": "menu::SelectLast",
"g g": "menu::SelectFirst",
"-": "project_panel::SelectParent",
"ctrl-u": "project_panel::ScrollUp",
"ctrl-d": "project_panel::ScrollDown",
"z t": "project_panel::ScrollCursorTop",
"z z": "project_panel::ScrollCursorCenter",
"z b": "project_panel::ScrollCursorBottom",
"0": ["vim::Number", 0],
"1": ["vim::Number", 1],
"2": ["vim::Number", 2],
"3": ["vim::Number", 3],
"4": ["vim::Number", 4],
"5": ["vim::Number", 5],
"6": ["vim::Number", 6],
"7": ["vim::Number", 7],
"8": ["vim::Number", 8],
"9": ["vim::Number", 9]
"-": "project_panel::SelectParent"
}
},
{

View File

@@ -29,9 +29,7 @@ Generate {{content_type}} based on the following prompt:
Match the indentation in the original file in the inserted {{content_type}}, don't include any indentation on blank lines.
Return ONLY the {{content_type}} to insert. Do NOT include any XML tags like <document>, <insert_here>, or any surrounding markup from the input.
Respond with a code block containing the {{content_type}} to insert. Replace \{{INSERTED_CODE}} with your actual {{content_type}}:
Immediately start with the following format with no remarks:
```
\{{INSERTED_CODE}}
@@ -68,9 +66,7 @@ Only make changes that are necessary to fulfill the prompt, leave everything els
Start at the indentation level in the original file in the rewritten {{content_type}}. Don't stop until you've rewritten the entire section, even if you have no more changes to make, always write out the whole section with no unnecessary elisions.
Return ONLY the rewritten {{content_type}}. Do NOT include any XML tags like <document>, <rewrite_this>, or any surrounding markup from the input.
Respond with a code block containing the rewritten {{content_type}}. Replace \{{REWRITTEN_CODE}} with your actual rewritten {{content_type}}:
Immediately start with the following format with no remarks:
```
\{{REWRITTEN_CODE}}

View File

@@ -1,7 +1,5 @@
{
/// The displayed name of this project. If not set or empty, the root directory name
/// will be displayed.
"project_name": "",
"project_name": null,
// The name of the Zed theme to use for the UI.
//
// `mode` is one of:
@@ -1244,9 +1242,6 @@
// The minimum column number to show the inline blame information at
"min_column": 0
},
"blame": {
"show_avatar": true
},
// Control which information is shown in the branch picker.
"branch_picker": {
"show_author_name": true

View File

@@ -192,7 +192,7 @@
"font_weight": null
},
"comment": {
"color": "#5c6773ff",
"color": "#abb5be8c",
"font_style": null,
"font_weight": null
},
@@ -583,7 +583,7 @@
"font_weight": null
},
"comment": {
"color": "#abb0b6ff",
"color": "#787b8099",
"font_style": null,
"font_weight": null
},
@@ -630,7 +630,7 @@
"hint": {
"color": "#8ca7c2ff",
"font_style": null,
"font_weight": null
"font_weight": 700
},
"keyword": {
"color": "#fa8d3eff",
@@ -974,7 +974,7 @@
"font_weight": null
},
"comment": {
"color": "#5c6773ff",
"color": "#b8cfe680",
"font_style": null,
"font_weight": null
},
@@ -1021,7 +1021,7 @@
"hint": {
"color": "#7399a3ff",
"font_style": null,
"font_weight": null
"font_weight": 700
},
"keyword": {
"color": "#ffad65ff",

View File

@@ -653,7 +653,7 @@
"hint": {
"color": "#8c957dff",
"font_style": null,
"font_weight": null
"font_weight": 700
},
"keyword": {
"color": "#fb4833ff",
@@ -1058,7 +1058,7 @@
"hint": {
"color": "#8c957dff",
"font_style": null,
"font_weight": null
"font_weight": 700
},
"keyword": {
"color": "#fb4833ff",
@@ -1463,7 +1463,7 @@
"hint": {
"color": "#677562ff",
"font_style": null,
"font_weight": null
"font_weight": 700
},
"keyword": {
"color": "#9d0006ff",
@@ -1868,7 +1868,7 @@
"hint": {
"color": "#677562ff",
"font_style": null,
"font_weight": null
"font_weight": 700
},
"keyword": {
"color": "#9d0006ff",
@@ -2273,7 +2273,7 @@
"hint": {
"color": "#677562ff",
"font_style": null,
"font_weight": null
"font_weight": 700
},
"keyword": {
"color": "#9d0006ff",

View File

@@ -643,7 +643,7 @@
"hint": {
"color": "#7274a7ff",
"font_style": null,
"font_weight": null
"font_weight": 700
},
"keyword": {
"color": "#a449abff",

View File

@@ -1968,8 +1968,9 @@ impl AcpThread {
let env = cx.spawn(async move |_, _| {
let mut env = env.await.unwrap_or_default();
// Disables paging for `git` and hopefully other commands
env.insert("PAGER".into(), "".into());
if cfg!(unix) {
env.insert("PAGER".into(), "cat".into());
}
for var in extra_env {
env.insert(var.name, var.value);
}

View File

@@ -31,7 +31,7 @@ impl Diff {
let buffer = new_buffer.clone();
async move |_, cx| {
let language = language_registry
.load_language_for_file_path(Path::new(&path))
.language_for_file_path(Path::new(&path))
.await
.log_err();

View File

@@ -3712,10 +3712,13 @@ impl AcpThreadView {
None
} else {
Some(
Label::new(format!("{}{separator}", parent.display(path_style)))
.color(Color::Muted)
.size(LabelSize::XSmall)
.buffer_font(cx),
Label::new(format!(
"{separator}{}{separator}",
parent.display(path_style)
))
.color(Color::Muted)
.size(LabelSize::XSmall)
.buffer_font(cx),
)
}
});

View File

@@ -317,8 +317,6 @@ impl ManageProfilesModal {
window: &mut Window,
cx: &mut Context<Self>,
) -> impl IntoElement + use<> {
let is_focused = profile.navigation.focus_handle.contains_focused(window, cx);
div()
.id(SharedString::from(format!("profile-{}", profile.id)))
.track_focus(&profile.navigation.focus_handle)
@@ -330,27 +328,25 @@ impl ManageProfilesModal {
})
.child(
ListItem::new(SharedString::from(format!("profile-{}", profile.id)))
.toggle_state(is_focused)
.toggle_state(profile.navigation.focus_handle.contains_focused(window, cx))
.inset(true)
.spacing(ListItemSpacing::Sparse)
.child(Label::new(profile.name.clone()))
.when(is_focused, |this| {
this.end_slot(
h_flex()
.gap_1()
.child(
Label::new("Customize")
.size(LabelSize::Small)
.color(Color::Muted),
)
.children(KeyBinding::for_action_in(
&menu::Confirm,
&self.focus_handle,
window,
cx,
)),
)
})
.end_slot(
h_flex()
.gap_1()
.child(
Label::new("Customize")
.size(LabelSize::Small)
.color(Color::Muted),
)
.children(KeyBinding::for_action_in(
&menu::Confirm,
&self.focus_handle,
window,
cx,
)),
)
.on_click({
let profile_id = profile.id.clone();
cx.listener(move |this, _, window, cx| {

View File

@@ -3,19 +3,12 @@ use agent_settings::{
AgentProfile, AgentProfileId, AgentSettings, AvailableProfiles, builtin_profiles,
};
use fs::Fs;
use fuzzy::{StringMatch, StringMatchCandidate, match_strings};
use gpui::{
Action, AnyElement, App, BackgroundExecutor, Context, DismissEvent, Entity, FocusHandle,
Focusable, SharedString, Subscription, Task, Window,
};
use picker::{Picker, PickerDelegate, popover_menu::PickerPopoverMenu};
use settings::{Settings as _, SettingsStore, update_settings_file};
use std::{
sync::atomic::Ordering,
sync::{Arc, atomic::AtomicBool},
};
use gpui::{Action, Entity, FocusHandle, Subscription, prelude::*};
use settings::{DockPosition, Settings as _, SettingsStore, update_settings_file};
use std::sync::Arc;
use ui::{
HighlightedLabel, ListItem, ListItemSpacing, PopoverMenuHandle, TintColor, Tooltip, prelude::*,
ContextMenu, ContextMenuEntry, DocumentationEdge, DocumentationSide, PopoverMenu,
PopoverMenuHandle, TintColor, Tooltip, prelude::*,
};
/// Trait for types that can provide and manage agent profiles
@@ -32,11 +25,9 @@ pub trait ProfileProvider {
pub struct ProfileSelector {
profiles: AvailableProfiles,
pending_refresh: bool,
fs: Arc<dyn Fs>,
provider: Arc<dyn ProfileProvider>,
picker: Option<Entity<Picker<ProfilePickerDelegate>>>,
picker_handle: PopoverMenuHandle<Picker<ProfilePickerDelegate>>,
menu_handle: PopoverMenuHandle<ContextMenu>,
focus_handle: FocusHandle,
_subscriptions: Vec<Subscription>,
}
@@ -49,91 +40,125 @@ impl ProfileSelector {
cx: &mut Context<Self>,
) -> Self {
let settings_subscription = cx.observe_global::<SettingsStore>(move |this, cx| {
this.pending_refresh = true;
cx.notify();
this.refresh_profiles(cx);
});
Self {
profiles: AgentProfile::available_profiles(cx),
pending_refresh: false,
fs,
provider,
picker: None,
picker_handle: PopoverMenuHandle::default(),
menu_handle: PopoverMenuHandle::default(),
focus_handle,
_subscriptions: vec![settings_subscription],
}
}
pub fn menu_handle(&self) -> PopoverMenuHandle<Picker<ProfilePickerDelegate>> {
self.picker_handle.clone()
pub fn menu_handle(&self) -> PopoverMenuHandle<ContextMenu> {
self.menu_handle.clone()
}
fn ensure_picker(
&mut self,
fn refresh_profiles(&mut self, cx: &mut Context<Self>) {
self.profiles = AgentProfile::available_profiles(cx);
}
fn build_context_menu(
&self,
window: &mut Window,
cx: &mut Context<Self>,
) -> Entity<Picker<ProfilePickerDelegate>> {
if self.picker.is_none() {
let delegate = ProfilePickerDelegate::new(
self.fs.clone(),
self.provider.clone(),
self.profiles.clone(),
cx.background_executor().clone(),
cx,
);
) -> Entity<ContextMenu> {
ContextMenu::build(window, cx, |mut menu, _window, cx| {
let settings = AgentSettings::get_global(cx);
let picker = cx.new(|cx| {
Picker::list(delegate, window, cx)
.show_scrollbar(true)
.width(rems(20.))
.max_height(Some(rems(16.).into()))
});
self.picker = Some(picker);
}
if self.pending_refresh {
if let Some(picker) = &self.picker {
let profiles = AgentProfile::available_profiles(cx);
self.profiles = profiles.clone();
picker.update(cx, |picker, cx| {
let query = picker.query(cx);
picker
.delegate
.refresh_profiles(profiles.clone(), query, cx);
});
let mut found_non_builtin = false;
for (profile_id, profile_name) in self.profiles.iter() {
if !builtin_profiles::is_builtin(profile_id) {
found_non_builtin = true;
continue;
}
menu = menu.item(self.menu_entry_for_profile(
profile_id.clone(),
profile_name,
settings,
cx,
));
}
self.pending_refresh = false;
}
self.picker.as_ref().unwrap().clone()
if found_non_builtin {
menu = menu.separator().header("Custom Profiles");
for (profile_id, profile_name) in self.profiles.iter() {
if builtin_profiles::is_builtin(profile_id) {
continue;
}
menu = menu.item(self.menu_entry_for_profile(
profile_id.clone(),
profile_name,
settings,
cx,
));
}
}
menu = menu.separator();
menu = menu.item(ContextMenuEntry::new("Configure Profiles…").handler(
move |window, cx| {
window.dispatch_action(ManageProfiles::default().boxed_clone(), cx);
},
));
menu
})
}
}
impl Focusable for ProfileSelector {
fn focus_handle(&self, cx: &App) -> FocusHandle {
if let Some(picker) = &self.picker {
picker.focus_handle(cx)
fn menu_entry_for_profile(
&self,
profile_id: AgentProfileId,
profile_name: &SharedString,
settings: &AgentSettings,
cx: &App,
) -> ContextMenuEntry {
let documentation = match profile_name.to_lowercase().as_str() {
builtin_profiles::WRITE => Some("Get help to write anything."),
builtin_profiles::ASK => Some("Chat about your codebase."),
builtin_profiles::MINIMAL => Some("Chat about anything with no tools."),
_ => None,
};
let thread_profile_id = self.provider.profile_id(cx);
let entry = ContextMenuEntry::new(profile_name.clone())
.toggleable(IconPosition::End, profile_id == thread_profile_id);
let entry = if let Some(doc_text) = documentation {
entry.documentation_aside(
documentation_side(settings.dock),
DocumentationEdge::Top,
move |_| Label::new(doc_text).into_any_element(),
)
} else {
self.focus_handle.clone()
}
entry
};
entry.handler({
let fs = self.fs.clone();
let provider = self.provider.clone();
move |_window, cx| {
update_settings_file(fs.clone(), cx, {
let profile_id = profile_id.clone();
move |settings, _cx| {
settings
.agent
.get_or_insert_default()
.set_profile(profile_id.0);
}
});
provider.set_profile(profile_id.clone(), cx);
}
})
}
}
impl Render for ProfileSelector {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
if !self.provider.profiles_supported(cx) {
return Button::new("tools-not-supported-button", "Tools Unsupported")
.disabled(true)
.label_size(LabelSize::Small)
.color(Color::Muted)
.tooltip(Tooltip::text("This model does not support tools."))
.into_any_element();
}
let picker = self.ensure_picker(window, cx);
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let settings = AgentSettings::get_global(cx);
let profile_id = self.provider.profile_id(cx);
let profile = settings.profiles.get(&profile_id);
@@ -141,572 +166,62 @@ impl Render for ProfileSelector {
let selected_profile = profile
.map(|profile| profile.name.clone())
.unwrap_or_else(|| "Unknown".into());
let focus_handle = self.focus_handle.clone();
let trigger_button = Button::new("profile-selector", selected_profile)
.label_size(LabelSize::Small)
.color(Color::Muted)
.icon(IconName::ChevronDown)
.icon_size(IconSize::XSmall)
.icon_position(IconPosition::End)
.icon_color(Color::Muted)
.selected_style(ButtonStyle::Tinted(TintColor::Accent));
if self.provider.profiles_supported(cx) {
let this = cx.entity();
let focus_handle = self.focus_handle.clone();
let trigger_button = Button::new("profile-selector-model", selected_profile)
.label_size(LabelSize::Small)
.color(Color::Muted)
.icon(IconName::ChevronDown)
.icon_size(IconSize::XSmall)
.icon_position(IconPosition::End)
.icon_color(Color::Muted)
.selected_style(ButtonStyle::Tinted(TintColor::Accent));
PickerPopoverMenu::new(
picker,
trigger_button,
move |window, cx| {
Tooltip::for_action_in(
"Toggle Profile Menu",
&ToggleProfileSelector,
&focus_handle,
window,
cx,
)
},
gpui::Corner::BottomRight,
cx,
)
.with_handle(self.picker_handle.clone())
.render(window, cx)
.into_any_element()
}
}
#[derive(Clone)]
struct ProfileCandidate {
id: AgentProfileId,
name: SharedString,
is_builtin: bool,
}
#[derive(Clone)]
struct ProfileMatchEntry {
candidate_index: usize,
positions: Vec<usize>,
}
enum ProfilePickerEntry {
Header(SharedString),
Profile(ProfileMatchEntry),
}
pub(crate) struct ProfilePickerDelegate {
fs: Arc<dyn Fs>,
provider: Arc<dyn ProfileProvider>,
background: BackgroundExecutor,
candidates: Vec<ProfileCandidate>,
string_candidates: Arc<Vec<StringMatchCandidate>>,
filtered_entries: Vec<ProfilePickerEntry>,
selected_index: usize,
query: String,
cancel: Option<Arc<AtomicBool>>,
}
impl ProfilePickerDelegate {
fn new(
fs: Arc<dyn Fs>,
provider: Arc<dyn ProfileProvider>,
profiles: AvailableProfiles,
background: BackgroundExecutor,
cx: &mut Context<ProfileSelector>,
) -> Self {
let candidates = Self::candidates_from(profiles);
let string_candidates = Arc::new(Self::string_candidates(&candidates));
let filtered_entries = Self::entries_from_candidates(&candidates);
let mut this = Self {
fs,
provider,
background,
candidates,
string_candidates,
filtered_entries,
selected_index: 0,
query: String::new(),
cancel: None,
};
this.selected_index = this
.index_of_profile(&this.provider.profile_id(cx))
.unwrap_or_else(|| this.first_selectable_index().unwrap_or(0));
this
}
fn refresh_profiles(
&mut self,
profiles: AvailableProfiles,
query: String,
cx: &mut Context<Picker<Self>>,
) {
self.candidates = Self::candidates_from(profiles);
self.string_candidates = Arc::new(Self::string_candidates(&self.candidates));
self.query = query;
if self.query.is_empty() {
self.filtered_entries = Self::entries_from_candidates(&self.candidates);
} else {
let matches = self.search_blocking(&self.query);
self.filtered_entries = self.entries_from_matches(matches);
}
self.selected_index = self
.index_of_profile(&self.provider.profile_id(cx))
.unwrap_or_else(|| self.first_selectable_index().unwrap_or(0));
cx.notify();
}
fn candidates_from(profiles: AvailableProfiles) -> Vec<ProfileCandidate> {
profiles
.into_iter()
.map(|(id, name)| ProfileCandidate {
is_builtin: builtin_profiles::is_builtin(&id),
id,
name,
})
.collect()
}
fn string_candidates(candidates: &[ProfileCandidate]) -> Vec<StringMatchCandidate> {
candidates
.iter()
.enumerate()
.map(|(index, candidate)| StringMatchCandidate::new(index, candidate.name.as_ref()))
.collect()
}
fn documentation(candidate: &ProfileCandidate) -> Option<&'static str> {
match candidate.id.as_str() {
builtin_profiles::WRITE => Some("Get help to write anything."),
builtin_profiles::ASK => Some("Chat about your codebase."),
builtin_profiles::MINIMAL => Some("Chat about anything with no tools."),
_ => None,
}
}
fn entries_from_candidates(candidates: &[ProfileCandidate]) -> Vec<ProfilePickerEntry> {
let mut entries = Vec::new();
let mut inserted_custom_header = false;
for (idx, candidate) in candidates.iter().enumerate() {
if !candidate.is_builtin && !inserted_custom_header {
if !entries.is_empty() {
entries.push(ProfilePickerEntry::Header("Custom Profiles".into()));
}
inserted_custom_header = true;
}
entries.push(ProfilePickerEntry::Profile(ProfileMatchEntry {
candidate_index: idx,
positions: Vec::new(),
}));
}
entries
}
fn entries_from_matches(&self, matches: Vec<StringMatch>) -> Vec<ProfilePickerEntry> {
let mut entries = Vec::new();
for mat in matches {
if self.candidates.get(mat.candidate_id).is_some() {
entries.push(ProfilePickerEntry::Profile(ProfileMatchEntry {
candidate_index: mat.candidate_id,
positions: mat.positions,
}));
}
}
entries
}
fn first_selectable_index(&self) -> Option<usize> {
self.filtered_entries
.iter()
.position(|entry| matches!(entry, ProfilePickerEntry::Profile(_)))
}
fn index_of_profile(&self, profile_id: &AgentProfileId) -> Option<usize> {
self.filtered_entries.iter().position(|entry| {
matches!(entry, ProfilePickerEntry::Profile(profile) if self
.candidates
.get(profile.candidate_index)
.map(|candidate| &candidate.id == profile_id)
.unwrap_or(false))
})
}
fn search_blocking(&self, query: &str) -> Vec<StringMatch> {
if query.is_empty() {
return self
.string_candidates
.iter()
.map(|candidate| StringMatch {
candidate_id: candidate.id,
score: 0.0,
positions: Vec::new(),
string: candidate.string.clone(),
})
.collect();
}
let cancel_flag = AtomicBool::new(false);
self.background.block(match_strings(
self.string_candidates.as_ref(),
query,
false,
true,
100,
&cancel_flag,
self.background.clone(),
))
}
}
impl PickerDelegate for ProfilePickerDelegate {
type ListItem = AnyElement;
fn placeholder_text(&self, _: &mut Window, _: &mut App) -> Arc<str> {
"Search profiles…".into()
}
fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> Option<SharedString> {
let text = if self.candidates.is_empty() {
"No profiles.".into()
} else {
"No profiles match your search.".into()
};
Some(text)
}
fn match_count(&self) -> usize {
self.filtered_entries.len()
}
fn selected_index(&self) -> usize {
self.selected_index
}
fn set_selected_index(&mut self, ix: usize, _: &mut Window, cx: &mut Context<Picker<Self>>) {
self.selected_index = ix.min(self.filtered_entries.len().saturating_sub(1));
cx.notify();
}
fn can_select(
&mut self,
ix: usize,
_window: &mut Window,
_cx: &mut Context<Picker<Self>>,
) -> bool {
match self.filtered_entries.get(ix) {
Some(ProfilePickerEntry::Profile(_)) => true,
Some(ProfilePickerEntry::Header(_)) | None => false,
}
}
fn update_matches(
&mut self,
query: String,
window: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Task<()> {
if query.is_empty() {
self.query.clear();
self.filtered_entries = Self::entries_from_candidates(&self.candidates);
self.selected_index = self
.index_of_profile(&self.provider.profile_id(cx))
.unwrap_or_else(|| self.first_selectable_index().unwrap_or(0));
cx.notify();
return Task::ready(());
}
if let Some(prev) = &self.cancel {
prev.store(true, Ordering::Relaxed);
}
let cancel = Arc::new(AtomicBool::new(false));
self.cancel = Some(cancel.clone());
let string_candidates = self.string_candidates.clone();
let background = self.background.clone();
let provider = self.provider.clone();
self.query = query.clone();
let cancel_for_future = cancel;
cx.spawn_in(window, async move |this, cx| {
let matches = match_strings(
string_candidates.as_ref(),
&query,
false,
true,
100,
cancel_for_future.as_ref(),
background,
)
.await;
this.update_in(cx, |this, _, cx| {
if this.delegate.query != query {
return;
}
this.delegate.filtered_entries = this.delegate.entries_from_matches(matches);
this.delegate.selected_index = this
.delegate
.index_of_profile(&provider.profile_id(cx))
.unwrap_or_else(|| this.delegate.first_selectable_index().unwrap_or(0));
cx.notify();
})
.ok();
})
}
fn confirm(&mut self, _: bool, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
match self.filtered_entries.get(self.selected_index) {
Some(ProfilePickerEntry::Profile(entry)) => {
if let Some(candidate) = self.candidates.get(entry.candidate_index) {
let profile_id = candidate.id.clone();
let fs = self.fs.clone();
let provider = self.provider.clone();
update_settings_file(fs, cx, {
let profile_id = profile_id.clone();
move |settings, _cx| {
settings
.agent
.get_or_insert_default()
.set_profile(profile_id.0);
}
});
provider.set_profile(profile_id.clone(), cx);
telemetry::event!(
"agent_profile_switched",
profile_id = profile_id.as_str(),
source = "picker"
);
}
cx.emit(DismissEvent);
}
_ => {}
}
}
fn dismissed(&mut self, window: &mut Window, cx: &mut Context<Picker<Self>>) {
cx.defer_in(window, |picker, window, cx| {
picker.set_query("", window, cx);
});
cx.emit(DismissEvent);
}
fn render_match(
&self,
ix: usize,
selected: bool,
_: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Option<Self::ListItem> {
match self.filtered_entries.get(ix)? {
ProfilePickerEntry::Header(label) => Some(
div()
.px_2p5()
.pb_0p5()
.when(ix > 0, |this| {
this.mt_1p5()
.pt_2()
.border_t_1()
.border_color(cx.theme().colors().border_variant)
})
.child(
Label::new(label.clone())
.size(LabelSize::XSmall)
.color(Color::Muted),
)
.into_any_element(),
),
ProfilePickerEntry::Profile(entry) => {
let candidate = self.candidates.get(entry.candidate_index)?;
let active_id = self.provider.profile_id(cx);
let is_active = active_id == candidate.id;
Some(
ListItem::new(SharedString::from(candidate.id.0.clone()))
.inset(true)
.spacing(ListItemSpacing::Sparse)
.toggle_state(selected)
.child(
v_flex()
.child(HighlightedLabel::new(
candidate.name.clone(),
entry.positions.clone(),
))
.when_some(Self::documentation(candidate), |this, doc| {
this.child(
Label::new(doc).size(LabelSize::Small).color(Color::Muted),
)
}),
PopoverMenu::new("profile-selector")
.trigger_with_tooltip(trigger_button, {
move |window, cx| {
Tooltip::for_action_in(
"Toggle Profile Menu",
&ToggleProfileSelector,
&focus_handle,
window,
cx,
)
.when(is_active, |this| {
this.end_slot(
div()
.pr_2()
.child(Icon::new(IconName::Check).color(Color::Accent)),
)
})
.into_any_element(),
}
})
.anchor(
if documentation_side(settings.dock) == DocumentationSide::Left {
gpui::Corner::BottomRight
} else {
gpui::Corner::BottomLeft
},
)
}
.with_handle(self.menu_handle.clone())
.menu(move |window, cx| {
Some(this.update(cx, |this, cx| this.build_context_menu(window, cx)))
})
.offset(gpui::Point {
x: px(0.0),
y: px(-2.0),
})
.into_any_element()
} else {
Button::new("tools-not-supported-button", "Tools Unsupported")
.disabled(true)
.label_size(LabelSize::Small)
.color(Color::Muted)
.tooltip(Tooltip::text("This model does not support tools."))
.into_any_element()
}
}
fn render_footer(
&self,
_: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Option<gpui::AnyElement> {
Some(
h_flex()
.w_full()
.border_t_1()
.border_color(cx.theme().colors().border_variant)
.p_1()
.gap_4()
.justify_between()
.child(
Button::new("configure", "Configure")
.icon(IconName::Settings)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.icon_position(IconPosition::Start)
.on_click(|_, window, cx| {
window.dispatch_action(ManageProfiles::default().boxed_clone(), cx);
}),
)
.into_any(),
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use fs::FakeFs;
use gpui::TestAppContext;
#[gpui::test]
fn entries_include_custom_profiles(_cx: &mut TestAppContext) {
let candidates = vec![
ProfileCandidate {
id: AgentProfileId("write".into()),
name: SharedString::from("Write"),
is_builtin: true,
},
ProfileCandidate {
id: AgentProfileId("my-custom".into()),
name: SharedString::from("My Custom"),
is_builtin: false,
},
];
let entries = ProfilePickerDelegate::entries_from_candidates(&candidates);
assert!(entries.iter().any(|entry| matches!(
entry,
ProfilePickerEntry::Profile(profile)
if candidates[profile.candidate_index].id.as_str() == "my-custom"
)));
assert!(entries.iter().any(|entry| matches!(
entry,
ProfilePickerEntry::Header(label) if label.as_ref() == "Custom Profiles"
)));
}
#[gpui::test]
fn fuzzy_filter_returns_no_results_and_keeps_configure(cx: &mut TestAppContext) {
let candidates = vec![ProfileCandidate {
id: AgentProfileId("write".into()),
name: SharedString::from("Write"),
is_builtin: true,
}];
let delegate = ProfilePickerDelegate {
fs: FakeFs::new(cx.executor()),
provider: Arc::new(TestProfileProvider::new(AgentProfileId("write".into()))),
background: cx.executor(),
candidates,
string_candidates: Arc::new(Vec::new()),
filtered_entries: Vec::new(),
selected_index: 0,
query: String::new(),
cancel: None,
};
let matches = Vec::new(); // No matches
let _entries = delegate.entries_from_matches(matches);
}
#[gpui::test]
fn active_profile_selection_logic_works(cx: &mut TestAppContext) {
let candidates = vec![
ProfileCandidate {
id: AgentProfileId("write".into()),
name: SharedString::from("Write"),
is_builtin: true,
},
ProfileCandidate {
id: AgentProfileId("ask".into()),
name: SharedString::from("Ask"),
is_builtin: true,
},
];
let delegate = ProfilePickerDelegate {
fs: FakeFs::new(cx.executor()),
provider: Arc::new(TestProfileProvider::new(AgentProfileId("write".into()))),
background: cx.executor(),
candidates,
string_candidates: Arc::new(Vec::new()),
filtered_entries: vec![
ProfilePickerEntry::Profile(ProfileMatchEntry {
candidate_index: 0,
positions: Vec::new(),
}),
ProfilePickerEntry::Profile(ProfileMatchEntry {
candidate_index: 1,
positions: Vec::new(),
}),
],
selected_index: 0,
query: String::new(),
cancel: None,
};
// Active profile should be found at index 0
let active_index = delegate.index_of_profile(&AgentProfileId("write".into()));
assert_eq!(active_index, Some(0));
}
struct TestProfileProvider {
profile_id: AgentProfileId,
}
impl TestProfileProvider {
fn new(profile_id: AgentProfileId) -> Self {
Self { profile_id }
}
}
impl ProfileProvider for TestProfileProvider {
fn profile_id(&self, _cx: &App) -> AgentProfileId {
self.profile_id.clone()
}
fn set_profile(&self, _profile_id: AgentProfileId, _cx: &mut App) {}
fn profiles_supported(&self, _cx: &App) -> bool {
true
}
fn documentation_side(position: DockPosition) -> DocumentationSide {
match position {
DockPosition::Left => DocumentationSide::Right,
DockPosition::Bottom => DocumentationSide::Left,
DockPosition::Right => DocumentationSide::Left,
}
}

View File

@@ -48,7 +48,7 @@ impl Render for BurnModeTooltip {
let keybinding = KeyBinding::for_action(&ToggleBurnMode, window, cx)
.map(|kb| kb.size(rems_from_px(12.)));
tooltip_container(cx, |this, _| {
tooltip_container(window, cx, |this, _, _| {
this
.child(
h_flex()

View File

@@ -704,7 +704,7 @@ impl ContextPillHover {
impl Render for ContextPillHover {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
tooltip_container(cx, move |this, cx| {
tooltip_container(window, cx, move |this, window, cx| {
this.occlude()
.on_mouse_move(|_, _, cx| cx.stop_propagation())
.on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())

View File

@@ -40,7 +40,7 @@ impl AgentOnboardingModal {
}
fn view_blog(&mut self, _: &ClickEvent, _: &mut Window, cx: &mut Context<Self>) {
cx.open_url("https://zed.dev/blog/fastest-ai-code-editor");
cx.open_url("http://zed.dev/blog/fastest-ai-code-editor");
cx.notify();
agent_onboarding_event!("Blog Link Clicked");

View File

@@ -12,8 +12,8 @@ impl UnavailableEditingTooltip {
}
impl Render for UnavailableEditingTooltip {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
tooltip_container(cx, |this, _| {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
tooltip_container(window, cx, |this, _, _| {
this.child(Label::new("Unavailable Editing")).child(
div().max_w_64().child(
Label::new(format!(

View File

@@ -67,6 +67,7 @@ pub enum Model {
alias = "claude-opus-4-1-thinking-latest"
)]
ClaudeOpus4_1Thinking,
#[default]
#[serde(rename = "claude-sonnet-4", alias = "claude-sonnet-4-latest")]
ClaudeSonnet4,
#[serde(
@@ -74,14 +75,6 @@ pub enum Model {
alias = "claude-sonnet-4-thinking-latest"
)]
ClaudeSonnet4Thinking,
#[default]
#[serde(rename = "claude-sonnet-4-5", alias = "claude-sonnet-4-5-latest")]
ClaudeSonnet4_5,
#[serde(
rename = "claude-sonnet-4-5-thinking",
alias = "claude-sonnet-4-5-thinking-latest"
)]
ClaudeSonnet4_5Thinking,
#[serde(rename = "claude-3-7-sonnet", alias = "claude-3-7-sonnet-latest")]
Claude3_7Sonnet,
#[serde(
@@ -140,14 +133,6 @@ impl Model {
return Ok(Self::ClaudeOpus4);
}
if id.starts_with("claude-sonnet-4-5-thinking") {
return Ok(Self::ClaudeSonnet4_5Thinking);
}
if id.starts_with("claude-sonnet-4-5") {
return Ok(Self::ClaudeSonnet4_5);
}
if id.starts_with("claude-sonnet-4-thinking") {
return Ok(Self::ClaudeSonnet4Thinking);
}
@@ -195,8 +180,6 @@ impl Model {
Self::ClaudeOpus4_1Thinking => "claude-opus-4-1-thinking-latest",
Self::ClaudeSonnet4 => "claude-sonnet-4-latest",
Self::ClaudeSonnet4Thinking => "claude-sonnet-4-thinking-latest",
Self::ClaudeSonnet4_5 => "claude-sonnet-4-5-latest",
Self::ClaudeSonnet4_5Thinking => "claude-sonnet-4-5-thinking-latest",
Self::Claude3_5Sonnet => "claude-3-5-sonnet-latest",
Self::Claude3_7Sonnet => "claude-3-7-sonnet-latest",
Self::Claude3_7SonnetThinking => "claude-3-7-sonnet-thinking-latest",
@@ -214,7 +197,6 @@ impl Model {
Self::ClaudeOpus4 | Self::ClaudeOpus4Thinking => "claude-opus-4-20250514",
Self::ClaudeOpus4_1 | Self::ClaudeOpus4_1Thinking => "claude-opus-4-1-20250805",
Self::ClaudeSonnet4 | Self::ClaudeSonnet4Thinking => "claude-sonnet-4-20250514",
Self::ClaudeSonnet4_5 | Self::ClaudeSonnet4_5Thinking => "claude-sonnet-4-5-20250929",
Self::Claude3_5Sonnet => "claude-3-5-sonnet-latest",
Self::Claude3_7Sonnet | Self::Claude3_7SonnetThinking => "claude-3-7-sonnet-latest",
Self::Claude3_5Haiku => "claude-3-5-haiku-latest",
@@ -233,8 +215,6 @@ impl Model {
Self::ClaudeOpus4_1Thinking => "Claude Opus 4.1 Thinking",
Self::ClaudeSonnet4 => "Claude Sonnet 4",
Self::ClaudeSonnet4Thinking => "Claude Sonnet 4 Thinking",
Self::ClaudeSonnet4_5 => "Claude Sonnet 4.5",
Self::ClaudeSonnet4_5Thinking => "Claude Sonnet 4.5 Thinking",
Self::Claude3_7Sonnet => "Claude 3.7 Sonnet",
Self::Claude3_5Sonnet => "Claude 3.5 Sonnet",
Self::Claude3_7SonnetThinking => "Claude 3.7 Sonnet Thinking",
@@ -256,8 +236,6 @@ impl Model {
| Self::ClaudeOpus4_1Thinking
| Self::ClaudeSonnet4
| Self::ClaudeSonnet4Thinking
| Self::ClaudeSonnet4_5
| Self::ClaudeSonnet4_5Thinking
| Self::Claude3_5Sonnet
| Self::Claude3_5Haiku
| Self::Claude3_7Sonnet
@@ -283,8 +261,6 @@ impl Model {
| Self::ClaudeOpus4_1Thinking
| Self::ClaudeSonnet4
| Self::ClaudeSonnet4Thinking
| Self::ClaudeSonnet4_5
| Self::ClaudeSonnet4_5Thinking
| Self::Claude3_5Sonnet
| Self::Claude3_5Haiku
| Self::Claude3_7Sonnet
@@ -304,8 +280,6 @@ impl Model {
| Self::ClaudeOpus4_1Thinking
| Self::ClaudeSonnet4
| Self::ClaudeSonnet4Thinking
| Self::ClaudeSonnet4_5
| Self::ClaudeSonnet4_5Thinking
| Self::Claude3_5Sonnet
| Self::Claude3_7Sonnet
| Self::Claude3_7SonnetThinking
@@ -325,8 +299,6 @@ impl Model {
| Self::ClaudeOpus4_1Thinking
| Self::ClaudeSonnet4
| Self::ClaudeSonnet4Thinking
| Self::ClaudeSonnet4_5
| Self::ClaudeSonnet4_5Thinking
| Self::Claude3_5Sonnet
| Self::Claude3_7Sonnet
| Self::Claude3_7SonnetThinking
@@ -346,7 +318,6 @@ impl Model {
Self::ClaudeOpus4
| Self::ClaudeOpus4_1
| Self::ClaudeSonnet4
| Self::ClaudeSonnet4_5
| Self::Claude3_5Sonnet
| Self::Claude3_7Sonnet
| Self::Claude3_5Haiku
@@ -356,7 +327,6 @@ impl Model {
Self::ClaudeOpus4Thinking
| Self::ClaudeOpus4_1Thinking
| Self::ClaudeSonnet4Thinking
| Self::ClaudeSonnet4_5Thinking
| Self::Claude3_7SonnetThinking => AnthropicModelMode::Thinking {
budget_tokens: Some(4_096),
},

View File

@@ -85,8 +85,11 @@ impl AskPassSession {
let askpass_script_path = temp_dir.path().join(ASKPASS_SCRIPT_NAME);
let (askpass_opened_tx, askpass_opened_rx) = oneshot::channel::<()>();
let listener = UnixListener::bind(&askpass_socket).context("creating askpass socket")?;
let zed_cli_path =
util::get_shell_safe_zed_cli_path().context("getting zed-cli path for askpass")?;
#[cfg(not(target_os = "windows"))]
let zed_path = util::get_shell_safe_zed_path()?;
#[cfg(target_os = "windows")]
let zed_path = std::env::current_exe()
.context("finding current executable path for use in askpass")?;
let (askpass_kill_master_tx, askpass_kill_master_rx) = oneshot::channel::<()>();
let mut kill_tx = Some(askpass_kill_master_tx);
@@ -134,7 +137,7 @@ impl AskPassSession {
});
// Create an askpass script that communicates back to this process.
let askpass_script = generate_askpass_script(&zed_cli_path, &askpass_socket);
let askpass_script = generate_askpass_script(&zed_path, &askpass_socket);
fs::write(&askpass_script_path, askpass_script)
.await
.with_context(|| format!("creating askpass script at {askpass_script_path:?}"))?;
@@ -251,10 +254,10 @@ pub fn main(socket: &str) {
#[inline]
#[cfg(not(target_os = "windows"))]
fn generate_askpass_script(zed_cli_path: &str, askpass_socket: &std::path::Path) -> String {
fn generate_askpass_script(zed_path: &str, askpass_socket: &std::path::Path) -> String {
format!(
"{shebang}\n{print_args} | {zed_cli} --askpass={askpass_socket} 2> /dev/null \n",
zed_cli = zed_cli_path,
"{shebang}\n{print_args} | {zed_exe} --askpass={askpass_socket} 2> /dev/null \n",
zed_exe = zed_path,
askpass_socket = askpass_socket.display(),
print_args = "printf '%s\\0' \"$@\"",
shebang = "#!/bin/sh",
@@ -263,13 +266,13 @@ fn generate_askpass_script(zed_cli_path: &str, askpass_socket: &std::path::Path)
#[inline]
#[cfg(target_os = "windows")]
fn generate_askpass_script(zed_cli_path: &str, askpass_socket: &std::path::Path) -> String {
fn generate_askpass_script(zed_path: &std::path::Path, askpass_socket: &std::path::Path) -> String {
format!(
r#"
$ErrorActionPreference = 'Stop';
($args -join [char]0) | & "{zed_cli}" --askpass={askpass_socket} 2> $null
($args -join [char]0) | & "{zed_exe}" --askpass={askpass_socket} 2> $null
"#,
zed_cli = zed_cli_path,
zed_exe = zed_path.display(),
askpass_socket = askpass_socket.display(),
)
}

View File

@@ -67,7 +67,7 @@ impl TryFrom<&str> for EncryptedPassword {
unsafe {
CryptProtectMemory(
value.as_mut_ptr() as _,
padded_length,
len,
CRYPTPROTECTMEMORY_SAME_PROCESS,
)?;
}
@@ -97,7 +97,7 @@ pub(crate) fn decrypt(mut password: EncryptedPassword) -> Result<String> {
unsafe {
CryptUnprotectMemory(
password.0.as_mut_ptr() as _,
password.0.len().try_into()?,
password.1,
CRYPTPROTECTMEMORY_SAME_PROCESS,
)
.context("while decrypting a SSH password")?

View File

@@ -6,7 +6,7 @@ use assistant_slash_command::{
use fuzzy::{PathMatch, StringMatchCandidate};
use gpui::{App, Entity, Task, WeakEntity};
use language::{
Anchor, BufferSnapshot, DiagnosticEntryRef, DiagnosticSeverity, LspAdapterDelegate,
Anchor, BufferSnapshot, DiagnosticEntry, DiagnosticSeverity, LspAdapterDelegate,
OffsetRangeExt, ToOffset,
};
use project::{DiagnosticSummary, PathMatchCandidateSet, Project};
@@ -367,7 +367,7 @@ pub fn collect_buffer_diagnostics(
fn collect_diagnostic(
output: &mut SlashCommandOutput,
entry: &DiagnosticEntryRef<'_, Anchor>,
entry: &DiagnosticEntry<Anchor>,
snapshot: &BufferSnapshot,
include_warnings: bool,
) {

View File

@@ -1161,7 +1161,7 @@ async fn build_buffer(
LineEnding::normalize(&mut text);
let text = Rope::from(text);
let language = cx
.update(|_cx| language_registry.load_language_for_file_path(&path))?
.update(|_cx| language_registry.language_for_file_path(&path))?
.await
.ok();
let buffer = cx.new(|cx| {

View File

@@ -22,6 +22,7 @@ denoise = { path = "../denoise" }
log.workspace = true
parking_lot.workspace = true
rodio = { workspace = true, features = [ "wav", "playback", "wav_output" ] }
rubato = "0.16.2"
serde.workspace = true
settings.workspace = true
smol.workspace = true

View File

@@ -1,26 +1,17 @@
use std::{
num::NonZero,
sync::{
Arc, Mutex,
atomic::{AtomicBool, Ordering},
},
time::Duration,
};
use std::{num::NonZero, time::Duration};
use crossbeam::queue::ArrayQueue;
use denoise::{Denoiser, DenoiserError};
use log::warn;
use rodio::{
ChannelCount, Sample, SampleRate, Source, conversions::SampleRateConverter, nz,
source::UniformSourceIterator,
};
use rodio::{ChannelCount, Sample, SampleRate, Source, conversions::ChannelCountConverter, nz};
use crate::rodio_ext::resample::FixedResampler;
pub use replayable::{Replay, ReplayDurationTooShort, Replayable};
mod replayable;
mod resample;
const MAX_CHANNELS: usize = 8;
#[derive(Debug, thiserror::Error)]
#[error("Replay duration is too short must be >= 100ms")]
pub struct ReplayDurationTooShort;
// These all require constant sources (so the span is infinitely long)
// this is not guaranteed by rodio however we know it to be true in all our
// applications. Rodio desperately needs a constant source concept.
@@ -41,8 +32,8 @@ pub trait RodioExt: Source + Sized {
self,
channel_count: ChannelCount,
sample_rate: SampleRate,
) -> UniformSourceIterator<Self>;
fn constant_samplerate(self, sample_rate: SampleRate) -> ConstantSampleRate<Self>;
) -> ConstantChannelCount<FixedResampler<Self>>;
fn constant_samplerate(self, sample_rate: SampleRate) -> FixedResampler<Self>;
fn possibly_disconnected_channels_to_mono(self) -> ToMono<Self>;
}
@@ -81,38 +72,7 @@ impl<S: Source> RodioExt for S {
self,
duration: Duration,
) -> Result<(Replay, Replayable<Self>), ReplayDurationTooShort> {
if duration < Duration::from_millis(100) {
return Err(ReplayDurationTooShort);
}
let samples_per_second = self.sample_rate().get() as usize * self.channels().get() as usize;
let samples_to_queue = duration.as_secs_f64() * samples_per_second as f64;
let samples_to_queue =
(samples_to_queue as usize).next_multiple_of(self.channels().get().into());
let chunk_size =
(samples_per_second.div_ceil(10)).next_multiple_of(self.channels().get() as usize);
let chunks_to_queue = samples_to_queue.div_ceil(chunk_size);
let is_active = Arc::new(AtomicBool::new(true));
let queue = Arc::new(ReplayQueue::new(chunks_to_queue, chunk_size));
Ok((
Replay {
rx: Arc::clone(&queue),
buffer: Vec::new().into_iter(),
sleep_duration: duration / 2,
sample_rate: self.sample_rate(),
channel_count: self.channels(),
source_is_active: is_active.clone(),
},
Replayable {
tx: queue,
inner: self,
buffer: Vec::with_capacity(chunk_size),
chunk_size,
is_active,
},
))
replayable::replayable(self, duration)
}
fn take_samples(self, n: usize) -> TakeSamples<S> {
TakeSamples {
@@ -128,37 +88,37 @@ impl<S: Source> RodioExt for S {
self,
channel_count: ChannelCount,
sample_rate: SampleRate,
) -> UniformSourceIterator<Self> {
UniformSourceIterator::new(self, channel_count, sample_rate)
) -> ConstantChannelCount<FixedResampler<Self>> {
ConstantChannelCount::new(self.constant_samplerate(sample_rate), channel_count)
}
fn constant_samplerate(self, sample_rate: SampleRate) -> ConstantSampleRate<Self> {
ConstantSampleRate::new(self, sample_rate)
fn constant_samplerate(self, sample_rate: SampleRate) -> FixedResampler<Self> {
FixedResampler::new(self, sample_rate)
}
fn possibly_disconnected_channels_to_mono(self) -> ToMono<Self> {
ToMono::new(self)
}
}
pub struct ConstantSampleRate<S: Source> {
inner: SampleRateConverter<S>,
pub struct ConstantChannelCount<S: Source> {
inner: ChannelCountConverter<S>,
channels: ChannelCount,
sample_rate: SampleRate,
}
impl<S: Source> ConstantSampleRate<S> {
fn new(source: S, target_rate: SampleRate) -> Self {
let input_sample_rate = source.sample_rate();
let channels = source.channels();
let inner = SampleRateConverter::new(source, input_sample_rate, target_rate, channels);
impl<S: Source> ConstantChannelCount<S> {
fn new(source: S, target_channels: ChannelCount) -> Self {
let input_channels = source.channels();
let sample_rate = source.sample_rate();
let inner = ChannelCountConverter::new(source, input_channels, target_channels);
Self {
sample_rate,
inner,
channels,
sample_rate: target_rate,
channels: target_channels,
}
}
}
impl<S: Source> Iterator for ConstantSampleRate<S> {
impl<S: Source> Iterator for ConstantChannelCount<S> {
type Item = rodio::Sample;
fn next(&mut self) -> Option<Self::Item> {
@@ -170,7 +130,7 @@ impl<S: Source> Iterator for ConstantSampleRate<S> {
}
}
impl<S: Source> Source for ConstantSampleRate<S> {
impl<S: Source> Source for ConstantChannelCount<S> {
fn current_span_len(&self) -> Option<usize> {
None
}
@@ -307,53 +267,6 @@ impl<S: Source> Source for TakeSamples<S> {
}
}
/// constant source, only works on a single span
#[derive(Debug)]
struct ReplayQueue {
inner: ArrayQueue<Vec<Sample>>,
normal_chunk_len: usize,
/// The last chunk in the queue may be smaller than
/// the normal chunk size. This is always equal to the
/// size of the last element in the queue.
/// (so normally chunk_size)
last_chunk: Mutex<Vec<Sample>>,
}
impl ReplayQueue {
fn new(queue_len: usize, chunk_size: usize) -> Self {
Self {
inner: ArrayQueue::new(queue_len),
normal_chunk_len: chunk_size,
last_chunk: Mutex::new(Vec::new()),
}
}
/// Returns the length in samples
fn len(&self) -> usize {
self.inner.len().saturating_sub(1) * self.normal_chunk_len
+ self
.last_chunk
.lock()
.expect("Self::push_last can not poison this lock")
.len()
}
fn pop(&self) -> Option<Vec<Sample>> {
self.inner.pop() // removes element that was inserted first
}
fn push_last(&self, mut samples: Vec<Sample>) {
let mut last_chunk = self
.last_chunk
.lock()
.expect("Self::len can not poison this lock");
std::mem::swap(&mut *last_chunk, &mut samples);
}
fn push_normal(&self, samples: Vec<Sample>) {
let _pushed_out_of_ringbuf = self.inner.force_push(samples);
}
}
/// constant source, only works on a single span
pub struct ProcessBuffer<const N: usize, S, F>
where
@@ -487,147 +400,15 @@ where
}
}
/// constant source, only works on a single span
#[derive(Debug)]
pub struct Replayable<S: Source> {
inner: S,
buffer: Vec<Sample>,
chunk_size: usize,
tx: Arc<ReplayQueue>,
is_active: Arc<AtomicBool>,
}
impl<S: Source> Iterator for Replayable<S> {
type Item = Sample;
fn next(&mut self) -> Option<Self::Item> {
if let Some(sample) = self.inner.next() {
self.buffer.push(sample);
// If the buffer is full send it
if self.buffer.len() == self.chunk_size {
self.tx.push_normal(std::mem::take(&mut self.buffer));
}
Some(sample)
} else {
let last_chunk = std::mem::take(&mut self.buffer);
self.tx.push_last(last_chunk);
self.is_active.store(false, Ordering::Relaxed);
None
}
}
fn size_hint(&self) -> (usize, Option<usize>) {
self.inner.size_hint()
}
}
impl<S: Source> Source for Replayable<S> {
fn current_span_len(&self) -> Option<usize> {
self.inner.current_span_len()
}
fn channels(&self) -> ChannelCount {
self.inner.channels()
}
fn sample_rate(&self) -> SampleRate {
self.inner.sample_rate()
}
fn total_duration(&self) -> Option<Duration> {
self.inner.total_duration()
}
}
/// constant source, only works on a single span
#[derive(Debug)]
pub struct Replay {
rx: Arc<ReplayQueue>,
buffer: std::vec::IntoIter<Sample>,
sleep_duration: Duration,
sample_rate: SampleRate,
channel_count: ChannelCount,
source_is_active: Arc<AtomicBool>,
}
impl Replay {
pub fn source_is_active(&self) -> bool {
// - source could return None and not drop
// - source could be dropped before returning None
self.source_is_active.load(Ordering::Relaxed) && Arc::strong_count(&self.rx) < 2
}
/// Duration of what is in the buffer and can be returned without blocking.
pub fn duration_ready(&self) -> Duration {
let samples_per_second = self.channels().get() as u32 * self.sample_rate().get();
let seconds_queued = self.samples_ready() as f64 / samples_per_second as f64;
Duration::from_secs_f64(seconds_queued)
}
/// Number of samples in the buffer and can be returned without blocking.
pub fn samples_ready(&self) -> usize {
self.rx.len() + self.buffer.len()
}
}
impl Iterator for Replay {
type Item = Sample;
fn next(&mut self) -> Option<Self::Item> {
if let Some(sample) = self.buffer.next() {
return Some(sample);
}
loop {
if let Some(new_buffer) = self.rx.pop() {
self.buffer = new_buffer.into_iter();
return self.buffer.next();
}
if !self.source_is_active() {
return None;
}
// The queue does not support blocking on a next item. We want this queue as it
// is quite fast and provides a fixed size. We know how many samples are in a
// buffer so if we do not get one now we must be getting one after `sleep_duration`.
std::thread::sleep(self.sleep_duration);
}
}
fn size_hint(&self) -> (usize, Option<usize>) {
((self.rx.len() + self.buffer.len()), None)
}
}
impl Source for Replay {
fn current_span_len(&self) -> Option<usize> {
None // source is not compatible with spans
}
fn channels(&self) -> ChannelCount {
self.channel_count
}
fn sample_rate(&self) -> SampleRate {
self.sample_rate
}
fn total_duration(&self) -> Option<Duration> {
None
}
}
#[cfg(test)]
mod tests {
use rodio::{nz, static_buffer::StaticSamplesBuffer};
use super::*;
const SAMPLES: [Sample; 5] = [0.0, 1.0, 2.0, 3.0, 4.0];
pub const SAMPLES: [Sample; 5] = [0.0, 1.0, 2.0, 3.0, 4.0];
fn test_source() -> StaticSamplesBuffer {
pub fn test_source() -> StaticSamplesBuffer {
StaticSamplesBuffer::new(nz!(1), nz!(1), &SAMPLES)
}
@@ -690,74 +471,4 @@ mod tests {
assert_eq!(yielded, SAMPLES.len())
}
}
mod instant_replay {
use super::*;
#[test]
fn continues_after_history() {
let input = test_source();
let (mut replay, mut source) = input
.replayable(Duration::from_secs(3))
.expect("longer than 100ms");
source.by_ref().take(3).count();
let yielded: Vec<Sample> = replay.by_ref().take(3).collect();
assert_eq!(&yielded, &SAMPLES[0..3],);
source.count();
let yielded: Vec<Sample> = replay.collect();
assert_eq!(&yielded, &SAMPLES[3..5],);
}
#[test]
fn keeps_only_latest() {
let input = test_source();
let (mut replay, mut source) = input
.replayable(Duration::from_secs(2))
.expect("longer than 100ms");
source.by_ref().take(5).count(); // get all items but do not end the source
let yielded: Vec<Sample> = replay.by_ref().take(2).collect();
assert_eq!(&yielded, &SAMPLES[3..5]);
source.count(); // exhaust source
assert_eq!(replay.next(), None);
}
#[test]
fn keeps_correct_amount_of_seconds() {
let input = StaticSamplesBuffer::new(nz!(1), nz!(16_000), &[0.0; 40_000]);
let (replay, mut source) = input
.replayable(Duration::from_secs(2))
.expect("longer than 100ms");
// exhaust but do not yet end source
source.by_ref().take(40_000).count();
// take all samples we can without blocking
let ready = replay.samples_ready();
let n_yielded = replay.take_samples(ready).count();
let max = source.sample_rate().get() * source.channels().get() as u32 * 2;
let margin = 16_000 / 10; // 100ms
assert!(n_yielded as u32 >= max - margin);
}
#[test]
fn samples_ready() {
let input = StaticSamplesBuffer::new(nz!(1), nz!(16_000), &[0.0; 40_000]);
let (mut replay, source) = input
.replayable(Duration::from_secs(2))
.expect("longer than 100ms");
assert_eq!(replay.by_ref().samples_ready(), 0);
source.take(8000).count(); // half a second
let margin = 16_000 / 10; // 100ms
let ready = replay.samples_ready();
assert!(ready >= 8000 - margin);
}
}
}

View File

@@ -0,0 +1,308 @@
use std::{
sync::{
Arc, Mutex,
atomic::{AtomicBool, Ordering},
},
time::Duration,
};
use crossbeam::queue::ArrayQueue;
use rodio::{ChannelCount, Sample, SampleRate, Source};
#[derive(Debug, thiserror::Error)]
#[error("Replay duration is too short must be >= 100ms")]
pub struct ReplayDurationTooShort;
pub fn replayable<S: Source>(
source: S,
duration: Duration,
) -> Result<(Replay, Replayable<S>), ReplayDurationTooShort> {
if duration < Duration::from_millis(100) {
return Err(ReplayDurationTooShort);
}
let samples_per_second = source.sample_rate().get() as usize * source.channels().get() as usize;
let samples_to_queue = duration.as_secs_f64() * samples_per_second as f64;
let samples_to_queue =
(samples_to_queue as usize).next_multiple_of(source.channels().get().into());
let chunk_size =
(samples_per_second.div_ceil(10)).next_multiple_of(source.channels().get() as usize);
let chunks_to_queue = samples_to_queue.div_ceil(chunk_size);
let is_active = Arc::new(AtomicBool::new(true));
let queue = Arc::new(ReplayQueue::new(chunks_to_queue, chunk_size));
Ok((
Replay {
rx: Arc::clone(&queue),
buffer: Vec::new().into_iter(),
sleep_duration: duration / 2,
sample_rate: source.sample_rate(),
channel_count: source.channels(),
source_is_active: is_active.clone(),
},
Replayable {
tx: queue,
inner: source,
buffer: Vec::with_capacity(chunk_size),
chunk_size,
is_active,
},
))
}
/// constant source, only works on a single span
#[derive(Debug)]
struct ReplayQueue {
inner: ArrayQueue<Vec<Sample>>,
normal_chunk_len: usize,
/// The last chunk in the queue may be smaller than
/// the normal chunk size. This is always equal to the
/// size of the last element in the queue.
/// (so normally chunk_size)
last_chunk: Mutex<Vec<Sample>>,
}
impl ReplayQueue {
fn new(queue_len: usize, chunk_size: usize) -> Self {
Self {
inner: ArrayQueue::new(queue_len),
normal_chunk_len: chunk_size,
last_chunk: Mutex::new(Vec::new()),
}
}
/// Returns the length in samples
fn len(&self) -> usize {
self.inner.len().saturating_sub(1) * self.normal_chunk_len
+ self
.last_chunk
.lock()
.expect("Self::push_last can not poison this lock")
.len()
}
fn pop(&self) -> Option<Vec<Sample>> {
self.inner.pop() // removes element that was inserted first
}
fn push_last(&self, mut samples: Vec<Sample>) {
let mut last_chunk = self
.last_chunk
.lock()
.expect("Self::len can not poison this lock");
std::mem::swap(&mut *last_chunk, &mut samples);
}
fn push_normal(&self, samples: Vec<Sample>) {
let _pushed_out_of_ringbuf = self.inner.force_push(samples);
}
}
/// constant source, only works on a single span
#[derive(Debug)]
pub struct Replayable<S: Source> {
inner: S,
buffer: Vec<Sample>,
chunk_size: usize,
tx: Arc<ReplayQueue>,
is_active: Arc<AtomicBool>,
}
impl<S: Source> Iterator for Replayable<S> {
type Item = Sample;
fn next(&mut self) -> Option<Self::Item> {
if let Some(sample) = self.inner.next() {
self.buffer.push(sample);
// If the buffer is full send it
if self.buffer.len() == self.chunk_size {
self.tx.push_normal(std::mem::take(&mut self.buffer));
}
Some(sample)
} else {
let last_chunk = std::mem::take(&mut self.buffer);
self.tx.push_last(last_chunk);
self.is_active.store(false, Ordering::Relaxed);
None
}
}
fn size_hint(&self) -> (usize, Option<usize>) {
self.inner.size_hint()
}
}
impl<S: Source> Source for Replayable<S> {
fn current_span_len(&self) -> Option<usize> {
self.inner.current_span_len()
}
fn channels(&self) -> ChannelCount {
self.inner.channels()
}
fn sample_rate(&self) -> SampleRate {
self.inner.sample_rate()
}
fn total_duration(&self) -> Option<Duration> {
self.inner.total_duration()
}
}
/// constant source, only works on a single span
#[derive(Debug)]
pub struct Replay {
rx: Arc<ReplayQueue>,
buffer: std::vec::IntoIter<Sample>,
sleep_duration: Duration,
sample_rate: SampleRate,
channel_count: ChannelCount,
source_is_active: Arc<AtomicBool>,
}
impl Replay {
pub fn source_is_active(&self) -> bool {
// - source could return None and not drop
// - source could be dropped before returning None
self.source_is_active.load(Ordering::Relaxed) && Arc::strong_count(&self.rx) < 2
}
/// Duration of what is in the buffer and can be returned without blocking.
pub fn duration_ready(&self) -> Duration {
let samples_per_second = self.channels().get() as u32 * self.sample_rate().get();
let seconds_queued = self.samples_ready() as f64 / samples_per_second as f64;
Duration::from_secs_f64(seconds_queued)
}
/// Number of samples in the buffer and can be returned without blocking.
pub fn samples_ready(&self) -> usize {
self.rx.len() + self.buffer.len()
}
}
impl Iterator for Replay {
type Item = Sample;
fn next(&mut self) -> Option<Self::Item> {
if let Some(sample) = self.buffer.next() {
return Some(sample);
}
loop {
if let Some(new_buffer) = self.rx.pop() {
self.buffer = new_buffer.into_iter();
return self.buffer.next();
}
if !self.source_is_active() {
return None;
}
// The queue does not support blocking on a next item. We want this queue as it
// is quite fast and provides a fixed size. We know how many samples are in a
// buffer so if we do not get one now we must be getting one after `sleep_duration`.
std::thread::sleep(self.sleep_duration);
}
}
fn size_hint(&self) -> (usize, Option<usize>) {
((self.rx.len() + self.buffer.len()), None)
}
}
impl Source for Replay {
fn current_span_len(&self) -> Option<usize> {
None // source is not compatible with spans
}
fn channels(&self) -> ChannelCount {
self.channel_count
}
fn sample_rate(&self) -> SampleRate {
self.sample_rate
}
fn total_duration(&self) -> Option<Duration> {
None
}
}
#[cfg(test)]
mod tests {
use rodio::{nz, static_buffer::StaticSamplesBuffer};
use super::*;
use crate::{
RodioExt,
rodio_ext::tests::{SAMPLES, test_source},
};
#[test]
fn continues_after_history() {
let input = test_source();
let (mut replay, mut source) = input
.replayable(Duration::from_secs(3))
.expect("longer than 100ms");
source.by_ref().take(3).count();
let yielded: Vec<Sample> = replay.by_ref().take(3).collect();
assert_eq!(&yielded, &SAMPLES[0..3],);
source.count();
let yielded: Vec<Sample> = replay.collect();
assert_eq!(&yielded, &SAMPLES[3..5],);
}
#[test]
fn keeps_only_latest() {
let input = test_source();
let (mut replay, mut source) = input
.replayable(Duration::from_secs(2))
.expect("longer than 100ms");
source.by_ref().take(5).count(); // get all items but do not end the source
let yielded: Vec<Sample> = replay.by_ref().take(2).collect();
assert_eq!(&yielded, &SAMPLES[3..5]);
source.count(); // exhaust source
assert_eq!(replay.next(), None);
}
#[test]
fn keeps_correct_amount_of_seconds() {
let input = StaticSamplesBuffer::new(nz!(1), nz!(16_000), &[0.0; 40_000]);
let (replay, mut source) = input
.replayable(Duration::from_secs(2))
.expect("longer than 100ms");
// exhaust but do not yet end source
source.by_ref().take(40_000).count();
// take all samples we can without blocking
let ready = replay.samples_ready();
let n_yielded = replay.take_samples(ready).count();
let max = source.sample_rate().get() * source.channels().get() as u32 * 2;
let margin = 16_000 / 10; // 100ms
assert!(n_yielded as u32 >= max - margin);
}
#[test]
fn samples_ready() {
let input = StaticSamplesBuffer::new(nz!(1), nz!(16_000), &[0.0; 40_000]);
let (mut replay, source) = input
.replayable(Duration::from_secs(2))
.expect("longer than 100ms");
assert_eq!(replay.by_ref().samples_ready(), 0);
source.take(8000).count(); // half a second
let margin = 16_000 / 10; // 100ms
let ready = replay.samples_ready();
assert!(ready >= 8000 - margin);
}
}

View File

@@ -0,0 +1,98 @@
use std::time::Duration;
use rodio::{Sample, SampleRate, Source};
use rubato::{FftFixedInOut, Resampler};
pub struct FixedResampler<S> {
input: S,
next_channel: usize,
next_frame: usize,
output_buffer: Vec<Vec<Sample>>,
input_buffer: Vec<Vec<Sample>>,
target_sample_rate: SampleRate,
resampler: FftFixedInOut<Sample>,
}
impl<S: Source> FixedResampler<S> {
pub fn new(input: S, target_sample_rate: SampleRate) -> Self {
let chunk_size_in =
Duration::from_millis(50).as_secs_f32() * input.sample_rate().get() as f32;
let chunk_size_in = chunk_size_in.ceil() as usize;
let resampler = FftFixedInOut::new(
input.sample_rate().get() as usize,
target_sample_rate.get() as usize,
chunk_size_in,
input.channels().get() as usize,
)
.expect(
"sample rates are non zero, and we are not changing it so there is no resample ratio",
);
Self {
next_channel: 0,
next_frame: 0,
output_buffer: resampler.output_buffer_allocate(true),
input_buffer: resampler.input_buffer_allocate(false),
target_sample_rate,
resampler,
input,
}
}
}
impl<S: Source> Source for FixedResampler<S> {
fn current_span_len(&self) -> Option<usize> {
None
}
fn channels(&self) -> rodio::ChannelCount {
self.input.channels()
}
fn sample_rate(&self) -> rodio::SampleRate {
self.target_sample_rate
}
fn total_duration(&self) -> Option<std::time::Duration> {
self.input.total_duration()
}
}
impl<S: Source> FixedResampler<S> {
fn next_sample(&mut self) -> Option<Sample> {
let sample = self.output_buffer[self.next_channel]
.get(self.next_frame)
.copied();
self.next_channel = (self.next_channel + 1) % self.input.channels().get() as usize;
self.next_frame += 1;
sample
}
}
impl<S: Source> Iterator for FixedResampler<S> {
type Item = Sample;
fn next(&mut self) -> Option<Self::Item> {
if let Some(sample) = self.next_sample() {
return Some(sample);
}
for input_channel in &mut self.input_buffer {
input_channel.clear();
}
for _ in 0..self.resampler.input_frames_next() {
for input_channel in &mut self.input_buffer {
input_channel.push(self.input.next()?);
}
}
self.resampler
.process_into_buffer(&mut self.input_buffer, &mut self.output_buffer, None).expect("Input and output buffer channels are correct as they have been set by the resampler. The buffer for each channel is the same length. The buffer length is what is requested the resampler.");
self.next_frame = 0;
self.next_sample()
}
}

View File

@@ -38,20 +38,6 @@ pub(crate) const JOBS: &[Job] = &[
std::fs::remove_file(&zed_wsl)
.context(format!("Failed to remove old file {}", zed_wsl.display()))
},
|app_dir| {
let open_console = app_dir.join("OpenConsole.exe");
log::info!("Removing old file: {}", open_console.display());
std::fs::remove_file(&open_console).context(format!(
"Failed to remove old file {}",
open_console.display()
))
},
|app_dir| {
let conpty = app_dir.join("conpty.dll");
log::info!("Removing old file: {}", conpty.display());
std::fs::remove_file(&conpty)
.context(format!("Failed to remove old file {}", conpty.display()))
},
// Copy new files
|app_dir| {
let zed_executable_source = app_dir.join("install\\Zed.exe");
@@ -101,38 +87,6 @@ pub(crate) const JOBS: &[Job] = &[
zed_wsl_dest.display()
))
},
|app_dir| {
let open_console_source = app_dir.join("install\\OpenConsole.exe");
let open_console_dest = app_dir.join("OpenConsole.exe");
log::info!(
"Copying new file {} to {}",
open_console_source.display(),
open_console_dest.display()
);
std::fs::copy(&open_console_source, &open_console_dest)
.map(|_| ())
.context(format!(
"Failed to copy new file {} to {}",
open_console_source.display(),
open_console_dest.display()
))
},
|app_dir| {
let conpty_source = app_dir.join("install\\conpty.dll");
let conpty_dest = app_dir.join("conpty.dll");
log::info!(
"Copying new file {} to {}",
conpty_source.display(),
conpty_dest.display()
);
std::fs::copy(&conpty_source, &conpty_dest)
.map(|_| ())
.context(format!(
"Failed to copy new file {} to {}",
conpty_source.display(),
conpty_dest.display()
))
},
// Clean up installer folder and updates folder
|app_dir| {
let updates_folder = app_dir.join("updates");

View File

@@ -22,6 +22,7 @@ pub struct BedrockModelCacheConfiguration {
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, EnumIter)]
pub enum Model {
// Anthropic models (already included)
#[default]
#[serde(rename = "claude-sonnet-4", alias = "claude-sonnet-4-latest")]
ClaudeSonnet4,
#[serde(
@@ -29,14 +30,6 @@ pub enum Model {
alias = "claude-sonnet-4-thinking-latest"
)]
ClaudeSonnet4Thinking,
#[default]
#[serde(rename = "claude-sonnet-4-5", alias = "claude-sonnet-4-5-latest")]
ClaudeSonnet4_5,
#[serde(
rename = "claude-sonnet-4-5-thinking",
alias = "claude-sonnet-4-5-thinking-latest"
)]
ClaudeSonnet4_5Thinking,
#[serde(rename = "claude-opus-4", alias = "claude-opus-4-latest")]
ClaudeOpus4,
#[serde(rename = "claude-opus-4-1", alias = "claude-opus-4-1-latest")]
@@ -151,14 +144,6 @@ impl Model {
Ok(Self::Claude3_7Sonnet)
} else if id.starts_with("claude-3-7-sonnet-thinking") {
Ok(Self::Claude3_7SonnetThinking)
} else if id.starts_with("claude-sonnet-4-5-thinking") {
Ok(Self::ClaudeSonnet4_5Thinking)
} else if id.starts_with("claude-sonnet-4-5") {
Ok(Self::ClaudeSonnet4_5)
} else if id.starts_with("claude-sonnet-4-thinking") {
Ok(Self::ClaudeSonnet4Thinking)
} else if id.starts_with("claude-sonnet-4") {
Ok(Self::ClaudeSonnet4)
} else {
anyhow::bail!("invalid model id {id}");
}
@@ -168,8 +153,6 @@ impl Model {
match self {
Model::ClaudeSonnet4 => "claude-sonnet-4",
Model::ClaudeSonnet4Thinking => "claude-sonnet-4-thinking",
Model::ClaudeSonnet4_5 => "claude-sonnet-4-5",
Model::ClaudeSonnet4_5Thinking => "claude-sonnet-4-5-thinking",
Model::ClaudeOpus4 => "claude-opus-4",
Model::ClaudeOpus4_1 => "claude-opus-4-1",
Model::ClaudeOpus4Thinking => "claude-opus-4-thinking",
@@ -231,9 +214,6 @@ impl Model {
Model::ClaudeSonnet4 | Model::ClaudeSonnet4Thinking => {
"anthropic.claude-sonnet-4-20250514-v1:0"
}
Model::ClaudeSonnet4_5 | Model::ClaudeSonnet4_5Thinking => {
"anthropic.claude-sonnet-4-5-20250929-v1:0"
}
Model::ClaudeOpus4 | Model::ClaudeOpus4Thinking => {
"anthropic.claude-opus-4-20250514-v1:0"
}
@@ -297,8 +277,6 @@ impl Model {
match self {
Self::ClaudeSonnet4 => "Claude Sonnet 4",
Self::ClaudeSonnet4Thinking => "Claude Sonnet 4 Thinking",
Self::ClaudeSonnet4_5 => "Claude Sonnet 4.5",
Self::ClaudeSonnet4_5Thinking => "Claude Sonnet 4.5 Thinking",
Self::ClaudeOpus4 => "Claude Opus 4",
Self::ClaudeOpus4_1 => "Claude Opus 4.1",
Self::ClaudeOpus4Thinking => "Claude Opus 4 Thinking",
@@ -368,8 +346,6 @@ impl Model {
| Self::ClaudeOpus4
| Self::ClaudeOpus4_1
| Self::ClaudeSonnet4Thinking
| Self::ClaudeSonnet4_5
| Self::ClaudeSonnet4_5Thinking
| Self::ClaudeOpus4Thinking
| Self::ClaudeOpus4_1Thinking => 200_000,
Self::AmazonNovaPremier => 1_000_000,
@@ -385,7 +361,6 @@ impl Model {
Self::Claude3Opus | Self::Claude3Sonnet | Self::Claude3_5Haiku => 4_096,
Self::Claude3_7Sonnet | Self::Claude3_7SonnetThinking => 128_000,
Self::ClaudeSonnet4 | Self::ClaudeSonnet4Thinking => 64_000,
Self::ClaudeSonnet4_5 | Self::ClaudeSonnet4_5Thinking => 64_000,
Self::ClaudeOpus4
| Self::ClaudeOpus4Thinking
| Self::ClaudeOpus4_1
@@ -410,9 +385,7 @@ impl Model {
| Self::ClaudeOpus4_1
| Self::ClaudeOpus4_1Thinking
| Self::ClaudeSonnet4
| Self::ClaudeSonnet4Thinking
| Self::ClaudeSonnet4_5
| Self::ClaudeSonnet4_5Thinking => 1.0,
| Self::ClaudeSonnet4Thinking => 1.0,
Self::Custom {
default_temperature,
..
@@ -436,8 +409,6 @@ impl Model {
| Self::ClaudeOpus4_1Thinking
| Self::ClaudeSonnet4
| Self::ClaudeSonnet4Thinking
| Self::ClaudeSonnet4_5
| Self::ClaudeSonnet4_5Thinking
| Self::Claude3_5Haiku => true,
// Amazon Nova models (all support tool use)
@@ -468,8 +439,6 @@ impl Model {
| Self::Claude3_7SonnetThinking
| Self::ClaudeSonnet4
| Self::ClaudeSonnet4Thinking
| Self::ClaudeSonnet4_5
| Self::ClaudeSonnet4_5Thinking
| Self::ClaudeOpus4
| Self::ClaudeOpus4Thinking
| Self::ClaudeOpus4_1
@@ -519,11 +488,9 @@ impl Model {
Model::Claude3_7SonnetThinking => BedrockModelMode::Thinking {
budget_tokens: Some(4096),
},
Model::ClaudeSonnet4Thinking | Model::ClaudeSonnet4_5Thinking => {
BedrockModelMode::Thinking {
budget_tokens: Some(4096),
}
}
Model::ClaudeSonnet4Thinking => BedrockModelMode::Thinking {
budget_tokens: Some(4096),
},
Model::ClaudeOpus4Thinking | Model::ClaudeOpus4_1Thinking => {
BedrockModelMode::Thinking {
budget_tokens: Some(4096),
@@ -575,8 +542,6 @@ impl Model {
| Model::Claude3_7SonnetThinking
| Model::ClaudeSonnet4
| Model::ClaudeSonnet4Thinking
| Model::ClaudeSonnet4_5
| Model::ClaudeSonnet4_5Thinking
| Model::ClaudeOpus4
| Model::ClaudeOpus4Thinking
| Model::ClaudeOpus4_1
@@ -610,8 +575,6 @@ impl Model {
| Model::Claude3_7SonnetThinking
| Model::ClaudeSonnet4
| Model::ClaudeSonnet4Thinking
| Model::ClaudeSonnet4_5
| Model::ClaudeSonnet4_5Thinking
| Model::Claude3Haiku
| Model::Claude3Sonnet
| Model::MetaLlama321BInstructV1
@@ -629,9 +592,7 @@ impl Model {
| Model::Claude3_7Sonnet
| Model::Claude3_7SonnetThinking
| Model::ClaudeSonnet4
| Model::ClaudeSonnet4Thinking
| Model::ClaudeSonnet4_5
| Model::ClaudeSonnet4_5Thinking,
| Model::ClaudeSonnet4Thinking,
"apac",
) => Ok(format!("{}.{}", region_group, model_id)),
@@ -670,10 +631,6 @@ mod tests {
Model::ClaudeSonnet4.cross_region_inference_id("eu-west-1")?,
"eu.anthropic.claude-sonnet-4-20250514-v1:0"
);
assert_eq!(
Model::ClaudeSonnet4_5.cross_region_inference_id("eu-west-1")?,
"eu.anthropic.claude-sonnet-4-5-20250929-v1:0"
);
assert_eq!(
Model::Claude3Sonnet.cross_region_inference_id("eu-west-1")?,
"eu.anthropic.claude-3-sonnet-20240229-v1:0"

View File

@@ -22,7 +22,6 @@ default = []
[dependencies]
anyhow.workspace = true
askpass.workspace = true
clap.workspace = true
collections.workspace = true
ipc-channel = "0.19"

View File

@@ -116,11 +116,6 @@ struct Args {
))]
#[arg(long)]
uninstall: bool,
/// Used for SSH/Git password authentication, to remove the need for netcat as a dependency,
/// by having Zed act like netcat communicating over a Unix socket.
#[arg(long, hide = true)]
askpass: Option<String>,
}
fn parse_path_with_position(argument_str: &str) -> anyhow::Result<String> {
@@ -208,12 +203,6 @@ fn main() -> Result<()> {
}
let args = Args::parse();
// `zed --askpass` Makes zed operate in nc/netcat mode for use with askpass
if let Some(socket) = &args.askpass {
askpass::main(socket);
return Ok(());
}
// Set custom data directory before any path operations
let user_data_dir = args.user_data_dir.clone();
if let Some(dir) = &user_data_dir {

View File

@@ -17,6 +17,5 @@ cloud_llm_client.workspace = true
indoc.workspace = true
ordered-float.workspace = true
rustc-hash.workspace = true
serde.workspace = true
strum.workspace = true
workspace-hack.workspace = true

View File

@@ -5,7 +5,6 @@ use cloud_llm_client::predict_edits_v3::{self, Event, PromptFormat, ReferencedDe
use indoc::indoc;
use ordered_float::OrderedFloat;
use rustc_hash::{FxHashMap, FxHashSet};
use serde::Serialize;
use std::fmt::Write;
use std::sync::Arc;
use std::{cmp::Reverse, collections::BinaryHeap, ops::Range, path::Path};
@@ -76,7 +75,7 @@ pub enum DeclarationStyle {
Declaration,
}
#[derive(Clone, Debug, Serialize)]
#[derive(Clone, Debug)]
pub struct SectionLabels {
pub excerpt_index: usize,
pub section_ranges: Vec<(Arc<Path>, Range<usize>)>,

View File

@@ -20,5 +20,7 @@ LLM_DATABASE_MAX_CONNECTIONS = 5
LLM_API_SECRET = "llm-secret"
OPENAI_API_KEY = "llm-secret"
# SLACK_PANICS_WEBHOOK = ""
# RUST_LOG=info
# LOG_JSON=true

View File

@@ -46,6 +46,7 @@ rand.workspace = true
reqwest = { version = "0.11", features = ["json"] }
reqwest_client.workspace = true
rpc.workspace = true
rustc-demangle.workspace = true
scrypt = "0.11"
sea-orm = { version = "1.1.0-rc.1", features = ["sqlx-postgres", "postgres-array", "runtime-tokio-rustls", "with-uuid"] }
semantic_version.workspace = true

View File

@@ -214,6 +214,11 @@ spec:
secretKeyRef:
name: blob-store
key: bucket
- name: SLACK_PANICS_WEBHOOK
valueFrom:
secretKeyRef:
name: slack
key: panics_webhook
- name: COMPLETE_WITH_LANGUAGE_MODEL_RATE_LIMIT_PER_HOUR
value: "1000"
- name: SUPERMAVEN_ADMIN_API_KEY

View File

@@ -1,6 +1,8 @@
pub mod contributors;
pub mod events;
pub mod extensions;
pub mod ips_file;
pub mod slack;
use crate::{AppState, Error, Result, auth, db::UserId, rpc};
use anyhow::Context as _;

View File

@@ -1,28 +1,33 @@
use super::ips_file::IpsFile;
use crate::api::CloudflareIpCountryHeader;
use crate::{AppState, Error, Result};
use crate::{AppState, Error, Result, api::slack};
use anyhow::anyhow;
use aws_sdk_s3::primitives::ByteStream;
use axum::{
Extension, Router, TypedHeader,
body::Bytes,
headers::Header,
http::{HeaderName, StatusCode},
http::{HeaderMap, HeaderName, StatusCode},
routing::post,
};
use chrono::Duration;
use semantic_version::SemanticVersion;
use serde::{Deserialize, Serialize};
use serde_json::json;
use sha2::{Digest, Sha256};
use std::sync::{Arc, OnceLock};
use telemetry_events::{Event, EventRequestBody};
use telemetry_events::{Event, EventRequestBody, Panic};
use util::ResultExt;
use uuid::Uuid;
const CRASH_REPORTS_BUCKET: &str = "zed-crash-reports";
pub fn router() -> Router {
Router::new()
.route("/telemetry/events", post(post_events))
.route("/telemetry/crashes", post(post_panic))
.route("/telemetry/crashes", post(post_crash))
.route("/telemetry/panics", post(post_panic))
.route("/telemetry/hangs", post(post_panic))
.route("/telemetry/hangs", post(post_hang))
}
pub struct ZedChecksumHeader(Vec<u8>);
@@ -53,12 +58,437 @@ impl Header for ZedChecksumHeader {
}
}
pub async fn post_panic() -> Result<()> {
// as of v0.201.x crash/panic reporting is now done via Sentry.
// The endpoint returns OK to avoid spurious errors for old clients.
pub async fn post_crash(
Extension(app): Extension<Arc<AppState>>,
headers: HeaderMap,
body: Bytes,
) -> Result<()> {
let report = IpsFile::parse(&body)?;
let version_threshold = SemanticVersion::new(0, 123, 0);
let bundle_id = &report.header.bundle_id;
let app_version = &report.app_version();
if bundle_id == "dev.zed.Zed-Dev" {
log::error!("Crash uploads from {} are ignored.", bundle_id);
return Ok(());
}
if app_version.is_none() || app_version.unwrap() < version_threshold {
log::error!(
"Crash uploads from {} are ignored.",
report.header.app_version
);
return Ok(());
}
let app_version = app_version.unwrap();
if let Some(blob_store_client) = app.blob_store_client.as_ref() {
let response = blob_store_client
.head_object()
.bucket(CRASH_REPORTS_BUCKET)
.key(report.header.incident_id.clone() + ".ips")
.send()
.await;
if response.is_ok() {
log::info!("We've already uploaded this crash");
return Ok(());
}
blob_store_client
.put_object()
.bucket(CRASH_REPORTS_BUCKET)
.key(report.header.incident_id.clone() + ".ips")
.acl(aws_sdk_s3::types::ObjectCannedAcl::PublicRead)
.body(ByteStream::from(body.to_vec()))
.send()
.await
.map_err(|e| log::error!("Failed to upload crash: {}", e))
.ok();
}
let recent_panic_on: Option<i64> = headers
.get("x-zed-panicked-on")
.and_then(|h| h.to_str().ok())
.and_then(|s| s.parse().ok());
let installation_id = headers
.get("x-zed-installation-id")
.and_then(|h| h.to_str().ok())
.map(|s| s.to_string())
.unwrap_or_default();
let mut recent_panic = None;
if let Some(recent_panic_on) = recent_panic_on {
let crashed_at = match report.timestamp() {
Ok(t) => Some(t),
Err(e) => {
log::error!("Can't parse {}: {}", report.header.timestamp, e);
None
}
};
if crashed_at.is_some_and(|t| (t.timestamp_millis() - recent_panic_on).abs() <= 30000) {
recent_panic = headers.get("x-zed-panic").and_then(|h| h.to_str().ok());
}
}
let description = report.description(recent_panic);
let summary = report.backtrace_summary();
tracing::error!(
service = "client",
version = %report.header.app_version,
os_version = %report.header.os_version,
bundle_id = %report.header.bundle_id,
incident_id = %report.header.incident_id,
installation_id = %installation_id,
description = %description,
backtrace = %summary,
"crash report"
);
if let Some(kinesis_client) = app.kinesis_client.clone()
&& let Some(stream) = app.config.kinesis_stream.clone()
{
let properties = json!({
"app_version": report.header.app_version,
"os_version": report.header.os_version,
"os_name": "macOS",
"bundle_id": report.header.bundle_id,
"incident_id": report.header.incident_id,
"installation_id": installation_id,
"description": description,
"backtrace": summary,
});
let row = SnowflakeRow::new(
"Crash Reported",
None,
false,
Some(installation_id),
properties,
);
let data = serde_json::to_vec(&row)?;
kinesis_client
.put_record()
.stream_name(stream)
.partition_key(row.insert_id.unwrap_or_default())
.data(data.into())
.send()
.await
.log_err();
}
if let Some(slack_panics_webhook) = app.config.slack_panics_webhook.clone() {
let payload = slack::WebhookBody::new(|w| {
w.add_section(|s| s.text(slack::Text::markdown(description)))
.add_section(|s| {
s.add_field(slack::Text::markdown(format!(
"*Version:*\n{} ({})",
bundle_id, app_version
)))
.add_field({
let hostname = app.config.blob_store_url.clone().unwrap_or_default();
let hostname = hostname.strip_prefix("https://").unwrap_or_else(|| {
hostname.strip_prefix("http://").unwrap_or_default()
});
slack::Text::markdown(format!(
"*Incident:*\n<https://{}.{}/{}.ips|{}…>",
CRASH_REPORTS_BUCKET,
hostname,
report.header.incident_id,
report
.header
.incident_id
.chars()
.take(8)
.collect::<String>(),
))
})
})
.add_rich_text(|r| r.add_preformatted(|p| p.add_text(summary)))
});
let payload_json = serde_json::to_string(&payload).map_err(|err| {
log::error!("Failed to serialize payload to JSON: {err}");
Error::Internal(anyhow!(err))
})?;
reqwest::Client::new()
.post(slack_panics_webhook)
.header("Content-Type", "application/json")
.body(payload_json)
.send()
.await
.map_err(|err| {
log::error!("Failed to send payload to Slack: {err}");
Error::Internal(anyhow!(err))
})?;
}
Ok(())
}
pub async fn post_hang(
Extension(app): Extension<Arc<AppState>>,
TypedHeader(ZedChecksumHeader(checksum)): TypedHeader<ZedChecksumHeader>,
body: Bytes,
) -> Result<()> {
let Some(expected) = calculate_json_checksum(app.clone(), &body) else {
return Err(Error::http(
StatusCode::INTERNAL_SERVER_ERROR,
"events not enabled".into(),
))?;
};
if checksum != expected {
return Err(Error::http(
StatusCode::BAD_REQUEST,
"invalid checksum".into(),
))?;
}
let incident_id = Uuid::new_v4().to_string();
// dump JSON into S3 so we can get frame offsets if we need to.
if let Some(blob_store_client) = app.blob_store_client.as_ref() {
blob_store_client
.put_object()
.bucket(CRASH_REPORTS_BUCKET)
.key(incident_id.clone() + ".hang.json")
.acl(aws_sdk_s3::types::ObjectCannedAcl::PublicRead)
.body(ByteStream::from(body.to_vec()))
.send()
.await
.map_err(|e| log::error!("Failed to upload crash: {}", e))
.ok();
}
let report: telemetry_events::HangReport = serde_json::from_slice(&body).map_err(|err| {
log::error!("can't parse report json: {err}");
Error::Internal(anyhow!(err))
})?;
let mut backtrace = "Possible hang detected on main thread:".to_string();
let unknown = "<unknown>".to_string();
for frame in report.backtrace.iter() {
backtrace.push_str(&format!("\n{}", frame.symbols.first().unwrap_or(&unknown)));
}
tracing::error!(
service = "client",
version = %report.app_version.unwrap_or_default().to_string(),
os_name = %report.os_name,
os_version = report.os_version.unwrap_or_default(),
incident_id = %incident_id,
installation_id = %report.installation_id.unwrap_or_default(),
backtrace = %backtrace,
"hang report");
Ok(())
}
pub async fn post_panic(
Extension(app): Extension<Arc<AppState>>,
TypedHeader(ZedChecksumHeader(checksum)): TypedHeader<ZedChecksumHeader>,
body: Bytes,
) -> Result<()> {
let Some(expected) = calculate_json_checksum(app.clone(), &body) else {
return Err(Error::http(
StatusCode::INTERNAL_SERVER_ERROR,
"events not enabled".into(),
))?;
};
if checksum != expected {
return Err(Error::http(
StatusCode::BAD_REQUEST,
"invalid checksum".into(),
))?;
}
let report: telemetry_events::PanicRequest = serde_json::from_slice(&body)
.map_err(|_| Error::http(StatusCode::BAD_REQUEST, "invalid json".into()))?;
let incident_id = uuid::Uuid::new_v4().to_string();
let panic = report.panic;
if panic.os_name == "Linux" && panic.os_version == Some("1.0.0".to_string()) {
return Err(Error::http(
StatusCode::BAD_REQUEST,
"invalid os version".into(),
))?;
}
if let Some(blob_store_client) = app.blob_store_client.as_ref() {
let response = blob_store_client
.head_object()
.bucket(CRASH_REPORTS_BUCKET)
.key(incident_id.clone() + ".json")
.send()
.await;
if response.is_ok() {
log::info!("We've already uploaded this crash");
return Ok(());
}
blob_store_client
.put_object()
.bucket(CRASH_REPORTS_BUCKET)
.key(incident_id.clone() + ".json")
.acl(aws_sdk_s3::types::ObjectCannedAcl::PublicRead)
.body(ByteStream::from(body.to_vec()))
.send()
.await
.map_err(|e| log::error!("Failed to upload crash: {}", e))
.ok();
}
let backtrace = panic.backtrace.join("\n");
tracing::error!(
service = "client",
version = %panic.app_version,
os_name = %panic.os_name,
os_version = %panic.os_version.clone().unwrap_or_default(),
incident_id = %incident_id,
installation_id = %panic.installation_id.clone().unwrap_or_default(),
description = %panic.payload,
backtrace = %backtrace,
"panic report"
);
if let Some(kinesis_client) = app.kinesis_client.clone()
&& let Some(stream) = app.config.kinesis_stream.clone()
{
let properties = json!({
"app_version": panic.app_version,
"os_name": panic.os_name,
"os_version": panic.os_version,
"incident_id": incident_id,
"installation_id": panic.installation_id,
"description": panic.payload,
"backtrace": backtrace,
});
let row = SnowflakeRow::new(
"Panic Reported",
None,
false,
panic.installation_id.clone(),
properties,
);
let data = serde_json::to_vec(&row)?;
kinesis_client
.put_record()
.stream_name(stream)
.partition_key(row.insert_id.unwrap_or_default())
.data(data.into())
.send()
.await
.log_err();
}
if !report_to_slack(&panic) {
return Ok(());
}
if let Some(slack_panics_webhook) = app.config.slack_panics_webhook.clone() {
let backtrace = if panic.backtrace.len() > 25 {
let total = panic.backtrace.len();
format!(
"{}\n and {} more",
panic
.backtrace
.iter()
.take(20)
.cloned()
.collect::<Vec<_>>()
.join("\n"),
total - 20
)
} else {
panic.backtrace.join("\n")
};
let backtrace_with_summary = panic.payload + "\n" + &backtrace;
let version = if panic.release_channel == "nightly"
&& !panic.app_version.contains("remote-server")
&& let Some(sha) = panic.app_commit_sha
{
format!("Zed Nightly {}", sha.chars().take(7).collect::<String>())
} else {
panic.app_version
};
let payload = slack::WebhookBody::new(|w| {
w.add_section(|s| s.text(slack::Text::markdown("Panic request".to_string())))
.add_section(|s| {
s.add_field(slack::Text::markdown(format!("*Version:*\n {version} ",)))
.add_field({
let hostname = app.config.blob_store_url.clone().unwrap_or_default();
let hostname = hostname.strip_prefix("https://").unwrap_or_else(|| {
hostname.strip_prefix("http://").unwrap_or_default()
});
slack::Text::markdown(format!(
"*{} {}:*\n<https://{}.{}/{}.json|{}…>",
panic.os_name,
panic.os_version.unwrap_or_default(),
CRASH_REPORTS_BUCKET,
hostname,
incident_id,
incident_id.chars().take(8).collect::<String>(),
))
})
})
.add_rich_text(|r| r.add_preformatted(|p| p.add_text(backtrace_with_summary)))
});
let payload_json = serde_json::to_string(&payload).map_err(|err| {
log::error!("Failed to serialize payload to JSON: {err}");
Error::Internal(anyhow!(err))
})?;
reqwest::Client::new()
.post(slack_panics_webhook)
.header("Content-Type", "application/json")
.body(payload_json)
.send()
.await
.map_err(|err| {
log::error!("Failed to send payload to Slack: {err}");
Error::Internal(anyhow!(err))
})?;
}
Ok(())
}
fn report_to_slack(panic: &Panic) -> bool {
// Panics on macOS should make their way to Slack as a crash report,
// so we don't need to send them a second time via this channel.
if panic.os_name == "macOS" {
return false;
}
if panic.payload.contains("ERROR_SURFACE_LOST_KHR") {
return false;
}
if panic.payload.contains("ERROR_INITIALIZATION_FAILED") {
return false;
}
if panic
.payload
.contains("GPU has crashed, and no debug information is available")
{
return false;
}
true
}
pub async fn post_events(
Extension(app): Extension<Arc<AppState>>,
TypedHeader(ZedChecksumHeader(checksum)): TypedHeader<ZedChecksumHeader>,

View File

@@ -0,0 +1,346 @@
use anyhow::Context as _;
use collections::HashMap;
use semantic_version::SemanticVersion;
use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Debug)]
pub struct IpsFile {
pub header: Header,
pub body: Body,
}
impl IpsFile {
pub fn parse(bytes: &[u8]) -> anyhow::Result<IpsFile> {
let mut split = bytes.splitn(2, |&b| b == b'\n');
let header_bytes = split.next().context("No header found")?;
let header: Header = serde_json::from_slice(header_bytes).context("parsing header")?;
let body_bytes = split.next().context("No body found")?;
let body: Body = serde_json::from_slice(body_bytes).context("parsing body")?;
Ok(IpsFile { header, body })
}
pub fn faulting_thread(&self) -> Option<&Thread> {
self.body.threads.get(self.body.faulting_thread? as usize)
}
pub fn app_version(&self) -> Option<SemanticVersion> {
self.header.app_version.parse().ok()
}
pub fn timestamp(&self) -> anyhow::Result<chrono::DateTime<chrono::FixedOffset>> {
chrono::DateTime::parse_from_str(&self.header.timestamp, "%Y-%m-%d %H:%M:%S%.f %#z")
.map_err(|e| anyhow::anyhow!(e))
}
pub fn description(&self, panic: Option<&str>) -> String {
let mut desc = if self.body.termination.indicator == "Abort trap: 6" {
match panic {
Some(panic_message) => format!("Panic `{}`", panic_message),
None => "Crash `Abort trap: 6` (possible panic)".into(),
}
} else if let Some(msg) = &self.body.exception.message {
format!("Exception `{}`", msg)
} else {
format!("Crash `{}`", self.body.termination.indicator)
};
if let Some(thread) = self.faulting_thread() {
if let Some(queue) = thread.queue.as_ref() {
desc += &format!(
" on thread {} ({})",
self.body.faulting_thread.unwrap_or_default(),
queue
);
} else {
desc += &format!(
" on thread {} ({})",
self.body.faulting_thread.unwrap_or_default(),
thread.name.clone().unwrap_or_default()
);
}
}
desc
}
pub fn backtrace_summary(&self) -> String {
if let Some(thread) = self.faulting_thread() {
let mut frames = thread
.frames
.iter()
.filter_map(|frame| {
if let Some(name) = &frame.symbol {
if self.is_ignorable_frame(name) {
return None;
}
Some(format!("{:#}", rustc_demangle::demangle(name)))
} else if let Some(image) = self.body.used_images.get(frame.image_index) {
Some(image.name.clone().unwrap_or("<unknown-image>".into()))
} else {
Some("<unknown>".into())
}
})
.collect::<Vec<_>>();
let total = frames.len();
if total > 21 {
frames = frames.into_iter().take(20).collect();
frames.push(format!(" and {} more...", total - 20))
}
frames.join("\n")
} else {
"<no backtrace available>".into()
}
}
fn is_ignorable_frame(&self, symbol: &String) -> bool {
[
"pthread_kill",
"panic",
"backtrace",
"rust_begin_unwind",
"abort",
]
.iter()
.any(|s| symbol.contains(s))
}
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(default)]
pub struct Header {
pub app_name: String,
pub timestamp: String,
pub app_version: String,
pub slice_uuid: String,
pub build_version: String,
pub platform: i64,
#[serde(rename = "bundleID", default)]
pub bundle_id: String,
pub share_with_app_devs: i64,
pub is_first_party: i64,
pub bug_type: String,
pub os_version: String,
pub roots_installed: i64,
pub name: String,
pub incident_id: String,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", default)]
pub struct Body {
pub uptime: i64,
pub proc_role: String,
pub version: i64,
#[serde(rename = "userID")]
pub user_id: i64,
pub deploy_version: i64,
pub model_code: String,
#[serde(rename = "coalitionID")]
pub coalition_id: i64,
pub os_version: OsVersion,
pub capture_time: String,
pub code_signing_monitor: i64,
pub incident: String,
pub pid: i64,
pub translated: bool,
pub cpu_type: String,
#[serde(rename = "roots_installed")]
pub roots_installed: i64,
#[serde(rename = "bug_type")]
pub bug_type: String,
pub proc_launch: String,
pub proc_start_abs_time: i64,
pub proc_exit_abs_time: i64,
pub proc_name: String,
pub proc_path: String,
pub bundle_info: BundleInfo,
pub store_info: StoreInfo,
pub parent_proc: String,
pub parent_pid: i64,
pub coalition_name: String,
pub crash_reporter_key: String,
#[serde(rename = "codeSigningID")]
pub code_signing_id: String,
#[serde(rename = "codeSigningTeamID")]
pub code_signing_team_id: String,
pub code_signing_flags: i64,
pub code_signing_validation_category: i64,
pub code_signing_trust_level: i64,
pub instruction_byte_stream: InstructionByteStream,
pub sip: String,
pub exception: Exception,
pub termination: Termination,
pub asi: Asi,
pub ext_mods: ExtMods,
pub faulting_thread: Option<i64>,
pub threads: Vec<Thread>,
pub used_images: Vec<UsedImage>,
pub shared_cache: SharedCache,
pub vm_summary: String,
pub legacy_info: LegacyInfo,
pub log_writing_signature: String,
pub trial_info: TrialInfo,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", default)]
pub struct OsVersion {
pub train: String,
pub build: String,
pub release_type: String,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", default)]
pub struct BundleInfo {
#[serde(rename = "CFBundleShortVersionString")]
pub cfbundle_short_version_string: String,
#[serde(rename = "CFBundleVersion")]
pub cfbundle_version: String,
#[serde(rename = "CFBundleIdentifier")]
pub cfbundle_identifier: String,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", default)]
pub struct StoreInfo {
pub device_identifier_for_vendor: String,
pub third_party: bool,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", default)]
pub struct InstructionByteStream {
#[serde(rename = "beforePC")]
pub before_pc: String,
#[serde(rename = "atPC")]
pub at_pc: String,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", default)]
pub struct Exception {
pub codes: String,
pub raw_codes: Vec<i64>,
#[serde(rename = "type")]
pub type_field: String,
pub subtype: Option<String>,
pub signal: String,
pub port: Option<i64>,
pub guard_id: Option<i64>,
pub message: Option<String>,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", default)]
pub struct Termination {
pub flags: i64,
pub code: i64,
pub namespace: String,
pub indicator: String,
pub by_proc: String,
pub by_pid: i64,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", default)]
pub struct Asi {
#[serde(rename = "libsystem_c.dylib")]
pub libsystem_c_dylib: Vec<String>,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", default)]
pub struct ExtMods {
pub caller: ExtMod,
pub system: ExtMod,
pub targeted: ExtMod,
pub warnings: i64,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", default)]
pub struct ExtMod {
#[serde(rename = "thread_create")]
pub thread_create: i64,
#[serde(rename = "thread_set_state")]
pub thread_set_state: i64,
#[serde(rename = "task_for_pid")]
pub task_for_pid: i64,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", default)]
pub struct Thread {
pub thread_state: HashMap<String, Value>,
pub id: i64,
pub triggered: Option<bool>,
pub name: Option<String>,
pub queue: Option<String>,
pub frames: Vec<Frame>,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", default)]
pub struct Frame {
pub image_offset: i64,
pub symbol: Option<String>,
pub symbol_location: Option<i64>,
pub image_index: usize,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", default)]
pub struct UsedImage {
pub source: String,
pub arch: Option<String>,
pub base: i64,
#[serde(rename = "CFBundleShortVersionString")]
pub cfbundle_short_version_string: Option<String>,
#[serde(rename = "CFBundleIdentifier")]
pub cfbundle_identifier: Option<String>,
pub size: i64,
pub uuid: String,
pub path: Option<String>,
pub name: Option<String>,
#[serde(rename = "CFBundleVersion")]
pub cfbundle_version: Option<String>,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", default)]
pub struct SharedCache {
pub base: i64,
pub size: i64,
pub uuid: String,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", default)]
pub struct LegacyInfo {
pub thread_triggered: ThreadTriggered,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", default)]
pub struct ThreadTriggered {
pub name: String,
pub queue: String,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", default)]
pub struct TrialInfo {
pub rollouts: Vec<Rollout>,
pub experiments: Vec<Value>,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", default)]
pub struct Rollout {
pub rollout_id: String,
pub factor_pack_ids: HashMap<String, Value>,
pub deployment_id: i64,
}

View File

@@ -0,0 +1,144 @@
use serde::{Deserialize, Serialize};
/// https://api.slack.com/reference/messaging/payload
#[derive(Default, Clone, Serialize, Deserialize)]
pub struct WebhookBody {
text: String,
#[serde(skip_serializing_if = "Vec::is_empty")]
blocks: Vec<Block>,
#[serde(skip_serializing_if = "Option::is_none")]
thread_ts: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
mrkdwn: Option<bool>,
}
impl WebhookBody {
pub fn new(f: impl FnOnce(Self) -> Self) -> Self {
f(Self::default())
}
pub fn add_section(mut self, build: impl FnOnce(Section) -> Section) -> Self {
self.blocks.push(Block::Section(build(Section::default())));
self
}
pub fn add_rich_text(mut self, build: impl FnOnce(RichText) -> RichText) -> Self {
self.blocks
.push(Block::RichText(build(RichText::default())));
self
}
}
#[derive(Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
/// https://api.slack.com/reference/block-kit/blocks
pub enum Block {
#[serde(rename = "section")]
Section(Section),
#[serde(rename = "rich_text")]
RichText(RichText),
// .... etc.
}
/// https://api.slack.com/reference/block-kit/blocks#section
#[derive(Default, Clone, Serialize, Deserialize)]
pub struct Section {
#[serde(skip_serializing_if = "Option::is_none")]
text: Option<Text>,
#[serde(skip_serializing_if = "Vec::is_empty")]
fields: Vec<Text>,
// fields, accessories...
}
impl Section {
pub fn text(mut self, text: Text) -> Self {
self.text = Some(text);
self
}
pub fn add_field(mut self, field: Text) -> Self {
self.fields.push(field);
self
}
}
/// https://api.slack.com/reference/block-kit/composition-objects#text
#[derive(Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum Text {
#[serde(rename = "plain_text")]
PlainText { text: String, emoji: bool },
#[serde(rename = "mrkdwn")]
Markdown { text: String, verbatim: bool },
}
impl Text {
pub fn plain(s: String) -> Self {
Self::PlainText {
text: s,
emoji: true,
}
}
pub fn markdown(s: String) -> Self {
Self::Markdown {
text: s,
verbatim: false,
}
}
}
#[derive(Default, Clone, Serialize, Deserialize)]
pub struct RichText {
elements: Vec<RichTextObject>,
}
impl RichText {
pub fn new(f: impl FnOnce(Self) -> Self) -> Self {
f(Self::default())
}
pub fn add_preformatted(
mut self,
build: impl FnOnce(RichTextPreformatted) -> RichTextPreformatted,
) -> Self {
self.elements.push(RichTextObject::Preformatted(build(
RichTextPreformatted::default(),
)));
self
}
}
/// https://api.slack.com/reference/block-kit/blocks#rich_text
#[derive(Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum RichTextObject {
#[serde(rename = "rich_text_preformatted")]
Preformatted(RichTextPreformatted),
// etc.
}
/// https://api.slack.com/reference/block-kit/blocks#rich_text_preformatted
#[derive(Clone, Default, Serialize, Deserialize)]
pub struct RichTextPreformatted {
#[serde(skip_serializing_if = "Vec::is_empty")]
elements: Vec<RichTextElement>,
#[serde(skip_serializing_if = "Option::is_none")]
border: Option<u8>,
}
impl RichTextPreformatted {
pub fn add_text(mut self, text: String) -> Self {
self.elements.push(RichTextElement::Text { text });
self
}
}
/// https://api.slack.com/reference/block-kit/blocks#element-types
#[derive(Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum RichTextElement {
#[serde(rename = "text")]
Text { text: String },
// etc.
}

View File

@@ -153,6 +153,7 @@ pub struct Config {
pub prediction_api_key: Option<Arc<str>>,
pub prediction_model: Option<Arc<str>>,
pub zed_client_checksum_seed: Option<String>,
pub slack_panics_webhook: Option<String>,
pub auto_join_channel_id: Option<ChannelId>,
pub supermaven_admin_api_key: Option<Arc<str>>,
}
@@ -203,6 +204,7 @@ impl Config {
prediction_api_key: None,
prediction_model: None,
zed_client_checksum_seed: None,
slack_panics_webhook: None,
auto_join_channel_id: None,
migrations_path: None,
seed_path: None,

View File

@@ -6514,8 +6514,14 @@ async fn test_right_click_menu_behind_collab_panel(cx: &mut TestAppContext) {
cx.simulate_keystrokes("cmd-n cmd-n cmd-n");
cx.update(|window, _cx| window.refresh());
let tab_bounds = cx.debug_bounds("TAB-2").unwrap();
let new_tab_button_bounds = cx.debug_bounds("ICON-Plus").unwrap();
assert!(
tab_bounds.intersects(&new_tab_button_bounds),
"Tab should overlap with the new tab button, if this is failing check if there's been a redesign!"
);
cx.simulate_event(MouseDownEvent {
button: MouseButton::Right,
position: new_tab_button_bounds.center(),

View File

@@ -599,6 +599,7 @@ impl TestServer {
prediction_api_key: None,
prediction_model: None,
zed_client_checksum_seed: None,
slack_panics_webhook: None,
auto_join_channel_id: None,
migrations_path: None,
seed_path: None,

View File

@@ -3170,8 +3170,8 @@ struct JoinChannelTooltip {
}
impl Render for JoinChannelTooltip {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
tooltip_container(cx, |container, cx| {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
tooltip_container(window, cx, |container, _, cx| {
let participants = self
.channel_store
.read(cx)

View File

@@ -40,7 +40,7 @@ impl DebuggerOnboardingModal {
}
fn view_blog(&mut self, _: &ClickEvent, _: &mut Window, cx: &mut Context<Self>) {
cx.open_url("https://zed.dev/blog/debugger");
cx.open_url("http://zed.dev/blog/debugger");
cx.notify();
debugger_onboarding_event!("Blog Link Clicked");

View File

@@ -9,10 +9,7 @@ use gpui::{
Action, AnyElement, Entity, EventEmitter, FocusHandle, Focusable, FontWeight, ListState,
Subscription, Task, WeakEntity, list,
};
use util::{
debug_panic,
paths::{PathStyle, is_absolute},
};
use util::debug_panic;
use crate::{StackTraceView, ToggleUserFrames};
use language::PointUtf16;
@@ -473,12 +470,8 @@ impl StackFrameList {
stack_frame.source.as_ref().and_then(|s| {
s.path
.as_deref()
.filter(|path| {
// Since we do not know if we are debugging on the host or (a remote/WSL) target,
// we need to check if either the path is absolute as Posix or Windows.
is_absolute(path, PathStyle::Posix) || is_absolute(path, PathStyle::Windows)
})
.map(|path| Arc::<Path>::from(Path::new(path)))
.filter(|path| path.is_absolute())
})
}

View File

@@ -15,7 +15,7 @@ use gpui::{
InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled, Subscription,
Task, WeakEntity, Window, actions, div,
};
use language::{Buffer, DiagnosticEntry, DiagnosticEntryRef, Point};
use language::{Buffer, DiagnosticEntry, Point};
use project::{
DiagnosticSummary, Event, Project, ProjectItem, ProjectPath,
project_settings::{DiagnosticSeverity, ProjectSettings},
@@ -350,7 +350,7 @@ impl BufferDiagnosticsEditor {
grouped
.entry(entry.diagnostic.group_id)
.or_default()
.push(DiagnosticEntryRef {
.push(DiagnosticEntry {
range: entry.range.to_point(&buffer_snapshot),
diagnostic: entry.diagnostic,
})
@@ -560,16 +560,13 @@ impl BufferDiagnosticsEditor {
})
}
fn set_diagnostics(&mut self, diagnostics: &[DiagnosticEntryRef<'_, Anchor>]) {
self.diagnostics = diagnostics
.iter()
.map(DiagnosticEntryRef::to_owned)
.collect();
fn set_diagnostics(&mut self, diagnostics: &Vec<DiagnosticEntry<Anchor>>) {
self.diagnostics = diagnostics.clone();
}
fn diagnostics_are_unchanged(
&self,
diagnostics: &Vec<DiagnosticEntryRef<'_, Anchor>>,
diagnostics: &Vec<DiagnosticEntry<Anchor>>,
snapshot: &BufferSnapshot,
) -> bool {
if self.diagnostics.len() != diagnostics.len() {

View File

@@ -6,7 +6,7 @@ use editor::{
hover_popover::diagnostics_markdown_style,
};
use gpui::{AppContext, Entity, Focusable, WeakEntity};
use language::{BufferId, Diagnostic, DiagnosticEntryRef};
use language::{BufferId, Diagnostic, DiagnosticEntry};
use lsp::DiagnosticSeverity;
use markdown::{Markdown, MarkdownElement};
use settings::Settings;
@@ -24,7 +24,7 @@ pub struct DiagnosticRenderer;
impl DiagnosticRenderer {
pub fn diagnostic_blocks_for_group(
diagnostic_group: Vec<DiagnosticEntryRef<'_, Point>>,
diagnostic_group: Vec<DiagnosticEntry<Point>>,
buffer_id: BufferId,
diagnostics_editor: Option<Arc<dyn DiagnosticsToolbarEditor>>,
cx: &mut App,
@@ -35,7 +35,7 @@ impl DiagnosticRenderer {
else {
return Vec::new();
};
let primary = &diagnostic_group[primary_ix];
let primary = diagnostic_group[primary_ix].clone();
let group_id = primary.diagnostic.group_id;
let mut results = vec![];
for entry in diagnostic_group.iter() {
@@ -123,7 +123,7 @@ impl DiagnosticRenderer {
impl editor::DiagnosticRenderer for DiagnosticRenderer {
fn render_group(
&self,
diagnostic_group: Vec<DiagnosticEntryRef<'_, Point>>,
diagnostic_group: Vec<DiagnosticEntry<Point>>,
buffer_id: BufferId,
snapshot: EditorSnapshot,
editor: WeakEntity<Editor>,
@@ -152,15 +152,19 @@ impl editor::DiagnosticRenderer for DiagnosticRenderer {
fn render_hover(
&self,
diagnostic_group: Vec<DiagnosticEntryRef<'_, Point>>,
diagnostic_group: Vec<DiagnosticEntry<Point>>,
range: Range<Point>,
buffer_id: BufferId,
cx: &mut App,
) -> Option<Entity<Markdown>> {
let blocks = Self::diagnostic_blocks_for_group(diagnostic_group, buffer_id, None, cx);
blocks
.into_iter()
.find_map(|block| (block.initial_range == range).then(|| block.markdown))
blocks.into_iter().find_map(|block| {
if block.initial_range == range {
Some(block.markdown)
} else {
None
}
})
}
fn open_link(
@@ -185,7 +189,7 @@ pub(crate) struct DiagnosticBlock {
impl DiagnosticBlock {
pub fn render_block(&self, editor: WeakEntity<Editor>, bcx: &BlockContext) -> AnyElement {
let cx = &bcx.app;
let status_colors = cx.theme().status();
let status_colors = bcx.app.theme().status();
let max_width = bcx.em_width * 120.;

View File

@@ -22,8 +22,7 @@ use gpui::{
Subscription, Task, WeakEntity, Window, actions, div,
};
use language::{
Bias, Buffer, BufferRow, BufferSnapshot, DiagnosticEntry, DiagnosticEntryRef, Point,
ToTreeSitterPoint,
Bias, Buffer, BufferRow, BufferSnapshot, DiagnosticEntry, Point, ToTreeSitterPoint,
};
use project::{
DiagnosticSummary, Project, ProjectPath,
@@ -413,8 +412,8 @@ impl ProjectDiagnosticsEditor {
fn diagnostics_are_unchanged(
&self,
existing: &[DiagnosticEntry<text::Anchor>],
new: &[DiagnosticEntryRef<'_, text::Anchor>],
existing: &Vec<DiagnosticEntry<text::Anchor>>,
new: &Vec<DiagnosticEntry<text::Anchor>>,
snapshot: &BufferSnapshot,
) -> bool {
if existing.len() != new.len() {
@@ -458,13 +457,7 @@ impl ProjectDiagnosticsEditor {
}) {
return true;
}
this.diagnostics.insert(
buffer_id,
diagnostics
.iter()
.map(DiagnosticEntryRef::to_owned)
.collect(),
);
this.diagnostics.insert(buffer_id, diagnostics.clone());
false
})?;
if unchanged {
@@ -476,7 +469,7 @@ impl ProjectDiagnosticsEditor {
grouped
.entry(entry.diagnostic.group_id)
.or_default()
.push(DiagnosticEntryRef {
.push(DiagnosticEntry {
range: entry.range.to_point(&buffer_snapshot),
diagnostic: entry.diagnostic,
})

View File

@@ -14,14 +14,12 @@ use workspace::{StatusItemView, ToolbarItemEvent, Workspace, item::ItemHandle};
use crate::{Deploy, IncludeWarnings, ProjectDiagnosticsEditor};
/// The status bar item that displays diagnostic counts.
pub struct DiagnosticIndicator {
summary: project::DiagnosticSummary,
active_editor: Option<WeakEntity<Editor>>,
workspace: WeakEntity<Workspace>,
current_diagnostic: Option<Diagnostic>,
active_editor: Option<WeakEntity<Editor>>,
_observe_active_editor: Option<Subscription>,
diagnostics_update: Task<()>,
diagnostic_summary_update: Task<()>,
}
@@ -75,9 +73,10 @@ impl Render for DiagnosticIndicator {
cx,
)
})
.on_click(
cx.listener(|this, _, window, cx| this.go_to_next_diagnostic(window, cx)),
),
.on_click(cx.listener(|this, _, window, cx| {
this.go_to_next_diagnostic(window, cx);
}))
.into_any_element(),
)
} else {
None
@@ -178,8 +177,7 @@ impl DiagnosticIndicator {
.filter(|entry| !entry.range.is_empty())
.min_by_key(|entry| (entry.diagnostic.severity, entry.range.len()))
.map(|entry| entry.diagnostic);
if new_diagnostic != self.current_diagnostic.as_ref() {
let new_diagnostic = new_diagnostic.cloned();
if new_diagnostic != self.current_diagnostic {
self.diagnostics_update =
cx.spawn_in(window, async move |diagnostics_indicator, cx| {
cx.background_executor()

View File

@@ -75,9 +75,12 @@ impl Render for ToolbarControls {
&ToggleDiagnosticsRefresh,
))
.on_click(cx.listener(move |toolbar_controls, _, _, cx| {
if let Some(editor) = toolbar_controls.editor() {
editor.stop_updating(cx);
cx.notify();
match toolbar_controls.editor() {
Some(editor) => {
editor.stop_updating(cx);
cx.notify();
}
None => {}
}
})),
)
@@ -92,10 +95,11 @@ impl Render for ToolbarControls {
&ToggleDiagnosticsRefresh,
))
.on_click(cx.listener({
move |toolbar_controls, _, window, cx| {
if let Some(editor) = toolbar_controls.editor() {
editor.refresh_diagnostics(window, cx)
}
move |toolbar_controls, _, window, cx| match toolbar_controls
.editor()
{
Some(editor) => editor.refresh_diagnostics(window, cx),
None => {}
}
})),
)
@@ -106,10 +110,9 @@ impl Render for ToolbarControls {
.icon_color(warning_color)
.shape(IconButtonShape::Square)
.tooltip(Tooltip::text(warning_tooltip))
.on_click(cx.listener(|this, _, window, cx| {
if let Some(editor) = &this.editor {
editor.toggle_warnings(window, cx)
}
.on_click(cx.listener(|this, _, window, cx| match &this.editor {
Some(editor) => editor.toggle_warnings(window, cx),
None => {}
})),
)
}

View File

@@ -23,7 +23,6 @@ itertools.workspace = true
language.workspace = true
log.workspace = true
ordered-float.workspace = true
postage.workspace = true
project.workspace = true
regex.workspace = true
serde.workspace = true

View File

@@ -55,13 +55,6 @@ impl Declaration {
}
}
pub fn as_file(&self) -> Option<&FileDeclaration> {
match self {
Declaration::Buffer { .. } => None,
Declaration::File { declaration, .. } => Some(declaration),
}
}
pub fn project_entry_id(&self) -> ProjectEntryId {
match self {
Declaration::File {

View File

@@ -1,10 +1,9 @@
use cloud_llm_client::predict_edits_v3::DeclarationScoreComponents;
use collections::HashMap;
use itertools::Itertools as _;
use language::BufferSnapshot;
use ordered_float::OrderedFloat;
use serde::Serialize;
use std::{cmp::Reverse, ops::Range};
use std::{cmp::Reverse, collections::HashMap, ops::Range};
use strum::EnumIter;
use text::{Point, ToPoint};
@@ -252,7 +251,6 @@ fn score_declaration(
pub struct DeclarationScores {
pub signature: f32,
pub declaration: f32,
pub retrieval: f32,
}
impl DeclarationScores {
@@ -260,7 +258,7 @@ impl DeclarationScores {
// TODO: handle truncation
// Score related to how likely this is the correct declaration, range 0 to 1
let retrieval = if components.is_same_file {
let accuracy_score = if components.is_same_file {
// TODO: use declaration_line_distance_rank
1.0 / components.same_file_declaration_count as f32
} else {
@@ -276,14 +274,13 @@ impl DeclarationScores {
};
// For now instead of linear combination, the scores are just multiplied together.
let combined_score = 10.0 * retrieval * distance_score;
let combined_score = 10.0 * accuracy_score * distance_score;
DeclarationScores {
signature: combined_score * components.excerpt_vs_signature_weighted_overlap,
// declaration score gets boosted both by being multiplied by 2 and by there being more
// weighted overlap.
declaration: 2.0 * combined_score * components.excerpt_vs_item_weighted_overlap,
retrieval,
}
}
}

View File

@@ -4,11 +4,8 @@ mod excerpt;
mod outline;
mod reference;
mod syntax_index;
pub mod text_similarity;
mod text_similarity;
use std::sync::Arc;
use collections::HashMap;
use gpui::{App, AppContext as _, Entity, Task};
use language::BufferSnapshot;
use text::{Point, ToOffset as _};
@@ -36,10 +33,8 @@ impl EditPredictionContext {
cx: &mut App,
) -> Task<Option<Self>> {
if let Some(syntax_index) = syntax_index {
let index_state =
syntax_index.read_with(cx, |index, _cx| Arc::downgrade(index.state()));
let index_state = syntax_index.read_with(cx, |index, _cx| index.state().clone());
cx.background_spawn(async move {
let index_state = index_state.upgrade()?;
let index_state = index_state.lock().await;
Self::gather_context(cursor_point, &buffer, &excerpt_options, Some(&index_state))
})
@@ -55,26 +50,6 @@ impl EditPredictionContext {
buffer: &BufferSnapshot,
excerpt_options: &EditPredictionExcerptOptions,
index_state: Option<&SyntaxIndexState>,
) -> Option<Self> {
Self::gather_context_with_references_fn(
cursor_point,
buffer,
excerpt_options,
index_state,
references_in_excerpt,
)
}
pub fn gather_context_with_references_fn(
cursor_point: Point,
buffer: &BufferSnapshot,
excerpt_options: &EditPredictionExcerptOptions,
index_state: Option<&SyntaxIndexState>,
get_references: impl FnOnce(
&EditPredictionExcerpt,
&EditPredictionExcerptText,
&BufferSnapshot,
) -> HashMap<Identifier, Vec<Reference>>,
) -> Option<Self> {
let excerpt = EditPredictionExcerpt::select_from_buffer(
cursor_point,
@@ -98,7 +73,7 @@ impl EditPredictionContext {
let cursor_offset_in_excerpt = cursor_offset_in_file.saturating_sub(excerpt.range.start);
let declarations = if let Some(index_state) = index_state {
let references = get_references(&excerpt, &excerpt_text, buffer);
let references = references_in_excerpt(&excerpt, &excerpt_text, buffer);
scored_declarations(
&index_state,
@@ -262,8 +237,7 @@ mod tests {
let lang_id = lang.id();
language_registry.add(Arc::new(lang));
let file_indexing_parallelism = 2;
let index = cx.new(|cx| SyntaxIndex::new(&project, file_indexing_parallelism, cx));
let index = cx.new(|cx| SyntaxIndex::new(&project, cx));
cx.run_until_parked();
(project, index, lang_id)

View File

@@ -1,5 +1,5 @@
use collections::HashMap;
use language::BufferSnapshot;
use std::collections::HashMap;
use std::ops::Range;
use util::RangeExt;
@@ -8,7 +8,7 @@ use crate::{
excerpt::{EditPredictionExcerpt, EditPredictionExcerptText},
};
#[derive(Debug, Clone)]
#[derive(Debug)]
pub struct Reference {
pub identifier: Identifier,
pub range: Range<usize>,
@@ -26,7 +26,7 @@ pub fn references_in_excerpt(
excerpt_text: &EditPredictionExcerptText,
snapshot: &BufferSnapshot,
) -> HashMap<Identifier, Vec<Reference>> {
let mut references = references_in_range(
let mut references = identifiers_in_range(
excerpt.range.clone(),
excerpt_text.body.as_str(),
ReferenceRegion::Nearby,
@@ -38,7 +38,7 @@ pub fn references_in_excerpt(
.iter()
.zip(excerpt_text.parent_signatures.iter())
{
references.extend(references_in_range(
references.extend(identifiers_in_range(
range.clone(),
text.as_str(),
ReferenceRegion::Breadcrumb,
@@ -46,7 +46,7 @@ pub fn references_in_excerpt(
));
}
let mut identifier_to_references: HashMap<Identifier, Vec<Reference>> = HashMap::default();
let mut identifier_to_references: HashMap<Identifier, Vec<Reference>> = HashMap::new();
for reference in references {
identifier_to_references
.entry(reference.identifier.clone())
@@ -57,7 +57,7 @@ pub fn references_in_excerpt(
}
/// Finds all nodes which have a "variable" match from the highlights query within the offset range.
pub fn references_in_range(
pub fn identifiers_in_range(
range: Range<usize>,
range_text: &str,
reference_region: ReferenceRegion,
@@ -120,7 +120,7 @@ mod test {
use indoc::indoc;
use language::{BufferSnapshot, Language, LanguageConfig, LanguageMatcher, tree_sitter_rust};
use crate::reference::{ReferenceRegion, references_in_range};
use crate::reference::{ReferenceRegion, identifiers_in_range};
#[gpui::test]
fn test_identifier_node_truncated(cx: &mut TestAppContext) {
@@ -136,7 +136,7 @@ mod test {
let buffer = create_buffer(code, cx);
let range = 0..35;
let references = references_in_range(
let references = identifiers_in_range(
range.clone(),
&code[range],
ReferenceRegion::Breadcrumb,

View File

@@ -1,18 +1,13 @@
use anyhow::{Result, anyhow};
use collections::{HashMap, HashSet};
use futures::channel::mpsc;
use futures::lock::Mutex;
use futures::{FutureExt as _, StreamExt, future};
use gpui::{App, AppContext as _, AsyncApp, Context, Entity, Task, WeakEntity};
use itertools::Itertools;
use gpui::{App, AppContext as _, Context, Entity, Task, WeakEntity};
use language::{Buffer, BufferEvent};
use postage::stream::Stream as _;
use project::buffer_store::{BufferStore, BufferStoreEvent};
use project::worktree_store::{WorktreeStore, WorktreeStoreEvent};
use project::{PathChange, Project, ProjectEntryId, ProjectPath};
use slotmap::SlotMap;
use std::iter;
use std::ops::{DerefMut, Range};
use std::ops::Range;
use std::sync::Arc;
use text::BufferId;
use util::{RangeExt as _, debug_panic, some_or_debug_panic};
@@ -22,60 +17,42 @@ use crate::declaration::{
};
use crate::outline::declarations_in_buffer;
// TODO
//
// * Also queue / debounce buffer changes. A challenge for this is that use of
// `buffer_declarations_containing_range` assumes that the index is always immediately up to date.
//
// * Add a per language configuration for skipping indexing.
// Potential future improvements:
//
// * Prevent indexing of a large file from blocking the queue.
//
// * Send multiple selected excerpt ranges. Challenge is that excerpt ranges influence which
// references are present and their scores.
//
// * Include single-file worktrees / non visible worktrees? E.g. go to definition that resolves to a
// file in a build dependency. Should not be editable in that case - but how to distinguish the case
// where it should be editable?
// Potential future optimizations:
//
// * Index files on multiple threads in Zed (currently only parallel for the CLI). Adding some kind
// of priority system to the background executor could help - it's single threaded for now to avoid
// interfering with other work.
// * Cache of buffers for files
//
// * Parse files directly instead of loading into a Rope.
//
// - This would allow the task handling dirty_files to be done entirely on the background executor.
//
// - Make SyntaxMap generic to handle embedded languages? Will also need to find line boundaries,
// but that can be done by scanning characters in the flat representation.
// * Parse files directly instead of loading into a Rope. Make SyntaxMap generic to handle embedded
// languages? Will also need to find line boundaries, but that can be done by scanning characters in
// the flat representation.
//
// * Use something similar to slotmap without key versions.
//
// * Concurrent slotmap
//
// * Use queue for parsing
pub struct SyntaxIndex {
state: Arc<Mutex<SyntaxIndexState>>,
project: WeakEntity<Project>,
initial_file_indexing_done_rx: postage::watch::Receiver<bool>,
}
#[derive(Default)]
pub struct SyntaxIndexState {
declarations: SlotMap<DeclarationId, Declaration>,
identifiers: HashMap<Identifier, HashSet<DeclarationId>>,
files: HashMap<ProjectEntryId, FileState>,
buffers: HashMap<BufferId, BufferState>,
dirty_files: HashMap<ProjectEntryId, ProjectPath>,
dirty_files_tx: mpsc::Sender<()>,
_file_indexing_task: Option<Task<()>>,
}
#[derive(Debug, Default)]
struct FileState {
declarations: Vec<DeclarationId>,
task: Option<Task<()>>,
}
#[derive(Default)]
@@ -85,107 +62,33 @@ struct BufferState {
}
impl SyntaxIndex {
pub fn new(
project: &Entity<Project>,
file_indexing_parallelism: usize,
cx: &mut Context<Self>,
) -> Self {
assert!(file_indexing_parallelism > 0);
let (dirty_files_tx, mut dirty_files_rx) = mpsc::channel::<()>(1);
let (mut initial_file_indexing_done_tx, initial_file_indexing_done_rx) =
postage::watch::channel();
let initial_state = SyntaxIndexState {
declarations: SlotMap::default(),
identifiers: HashMap::default(),
files: HashMap::default(),
buffers: HashMap::default(),
dirty_files: HashMap::default(),
dirty_files_tx,
_file_indexing_task: None,
};
let this = Self {
pub fn new(project: &Entity<Project>, cx: &mut Context<Self>) -> Self {
let mut this = Self {
project: project.downgrade(),
state: Arc::new(Mutex::new(initial_state)),
initial_file_indexing_done_rx,
state: Arc::new(Mutex::new(SyntaxIndexState::default())),
};
let worktree_store = project.read(cx).worktree_store();
let initial_worktree_snapshots = worktree_store
cx.subscribe(&worktree_store, Self::handle_worktree_store_event)
.detach();
for worktree in worktree_store
.read(cx)
.worktrees()
.map(|w| w.read(cx).snapshot())
.collect::<Vec<_>>();
this.state.try_lock().unwrap()._file_indexing_task =
Some(cx.spawn(async move |this, cx| {
let snapshots_file_count = initial_worktree_snapshots
.iter()
.map(|worktree| worktree.file_count())
.sum::<usize>();
let chunk_size = snapshots_file_count.div_ceil(file_indexing_parallelism);
let chunk_count = snapshots_file_count.div_ceil(chunk_size);
let file_chunks = initial_worktree_snapshots
.iter()
.flat_map(|worktree| {
let worktree_id = worktree.id();
worktree.files(false, 0).map(move |entry| {
(
entry.id,
ProjectPath {
worktree_id,
path: entry.path.clone(),
},
)
})
})
.chunks(chunk_size);
let mut tasks = Vec::with_capacity(chunk_count);
for chunk in file_chunks.into_iter() {
tasks.push(Self::update_dirty_files(
&this,
chunk.into_iter().collect(),
cx.clone(),
));
}
futures::future::join_all(tasks).await;
log::info!("Finished initial file indexing");
*initial_file_indexing_done_tx.borrow_mut() = true;
let Ok(state) = this.read_with(cx, |this, _cx| this.state.clone()) else {
return;
};
while dirty_files_rx.next().await.is_some() {
let mut state = state.lock().await;
let was_underused = state.dirty_files.capacity() > 255
&& state.dirty_files.len() * 8 < state.dirty_files.capacity();
let dirty_files = state.dirty_files.drain().collect::<Vec<_>>();
if was_underused {
state.dirty_files.shrink_to_fit();
}
drop(state);
if dirty_files.is_empty() {
continue;
}
let chunk_size = dirty_files.len().div_ceil(file_indexing_parallelism);
let chunk_count = dirty_files.len().div_ceil(chunk_size);
let mut tasks = Vec::with_capacity(chunk_count);
let chunks = dirty_files.into_iter().chunks(chunk_size);
for chunk in chunks.into_iter() {
tasks.push(Self::update_dirty_files(
&this,
chunk.into_iter().collect(),
cx.clone(),
));
}
futures::future::join_all(tasks).await;
}
}));
cx.subscribe(&worktree_store, Self::handle_worktree_store_event)
.detach();
.collect::<Vec<_>>()
{
for entry in worktree.files(false, 0) {
this.update_file(
entry.id,
ProjectPath {
worktree_id: worktree.id(),
path: entry.path.clone(),
},
cx,
);
}
}
let buffer_store = project.read(cx).buffer_store().clone();
for buffer in buffer_store.read(cx).buffers().collect::<Vec<_>>() {
@@ -197,63 +100,6 @@ impl SyntaxIndex {
this
}
async fn update_dirty_files(
this: &WeakEntity<Self>,
dirty_files: Vec<(ProjectEntryId, ProjectPath)>,
mut cx: AsyncApp,
) {
for (entry_id, project_path) in dirty_files {
let Ok(task) = this.update(&mut cx, |this, cx| {
this.update_file(entry_id, project_path, cx)
}) else {
return;
};
task.await;
}
}
pub fn wait_for_initial_file_indexing(&self, cx: &App) -> Task<Result<()>> {
if *self.initial_file_indexing_done_rx.borrow() {
Task::ready(Ok(()))
} else {
let mut rx = self.initial_file_indexing_done_rx.clone();
cx.background_spawn(async move {
loop {
match rx.recv().await {
Some(true) => return Ok(()),
Some(false) => {}
None => {
return Err(anyhow!(
"SyntaxIndex dropped while waiting for initial file indexing"
));
}
}
}
})
}
}
pub fn indexed_file_paths(&self, cx: &App) -> Task<Vec<ProjectPath>> {
let state = self.state.clone();
let project = self.project.clone();
cx.spawn(async move |cx| {
let state = state.lock().await;
let Some(project) = project.upgrade() else {
return vec![];
};
project
.read_with(cx, |project, cx| {
state
.files
.keys()
.filter_map(|entry_id| project.path_for_entry(*entry_id, cx))
.collect()
})
.unwrap_or_default()
})
}
fn handle_worktree_store_event(
&mut self,
_worktree_store: Entity<WorktreeStore>,
@@ -266,27 +112,22 @@ impl SyntaxIndex {
let state = Arc::downgrade(&self.state);
let worktree_id = *worktree_id;
let updated_entries_set = updated_entries_set.clone();
cx.background_spawn(async move {
cx.spawn(async move |this, cx| {
let Some(state) = state.upgrade() else { return };
let mut state = state.lock().await;
for (path, entry_id, path_change) in updated_entries_set.iter() {
if let PathChange::Removed = path_change {
state.files.remove(entry_id);
state.dirty_files.remove(entry_id);
state.lock().await.files.remove(entry_id);
} else {
let project_path = ProjectPath {
worktree_id,
path: path.clone(),
};
state.dirty_files.insert(*entry_id, project_path);
this.update(cx, |this, cx| {
this.update_file(*entry_id, project_path, cx);
})
.ok();
}
}
match state.dirty_files_tx.try_send(()) {
Err(err) if err.is_disconnected() => {
log::error!("bug: syntax indexing queue is disconnected");
}
_ => {}
}
})
.detach();
}
@@ -336,7 +177,7 @@ impl SyntaxIndex {
.detach();
}
fn register_buffer(&self, buffer: &Entity<Buffer>, cx: &mut Context<Self>) {
fn register_buffer(&mut self, buffer: &Entity<Buffer>, cx: &mut Context<Self>) {
let buffer_id = buffer.read(cx).remote_id();
cx.observe_release(buffer, move |this, _buffer, cx| {
this.with_state(cx, move |state| {
@@ -367,11 +208,8 @@ impl SyntaxIndex {
}
}
fn update_buffer(&self, buffer_entity: Entity<Buffer>, cx: &mut Context<Self>) {
fn update_buffer(&mut self, buffer_entity: Entity<Buffer>, cx: &mut Context<Self>) {
let buffer = buffer_entity.read(cx);
if buffer.language().is_none() {
return;
}
let Some(project_entry_id) =
project::File::from_dyn(buffer.file()).and_then(|f| f.project_entry_id(cx))
@@ -391,64 +229,70 @@ impl SyntaxIndex {
}
});
let state = Arc::downgrade(&self.state);
let task = cx.background_spawn(async move {
// TODO: How to handle errors?
let Ok(snapshot) = snapshot_task.await else {
return;
};
let rope = snapshot.text.as_rope();
let parse_task = cx.background_spawn(async move {
let snapshot = snapshot_task.await?;
let rope = snapshot.text.as_rope().clone();
let declarations = declarations_in_buffer(&snapshot)
.into_iter()
.map(|item| {
(
item.parent_index,
BufferDeclaration::from_outline(item, &rope),
)
anyhow::Ok((
declarations_in_buffer(&snapshot)
.into_iter()
.map(|item| {
(
item.parent_index,
BufferDeclaration::from_outline(item, &rope),
)
})
.collect::<Vec<_>>(),
rope,
))
});
let task = cx.spawn({
async move |this, cx| {
let Ok((declarations, rope)) = parse_task.await else {
return;
};
this.update(cx, move |this, cx| {
this.with_state(cx, move |state| {
let buffer_state = state
.buffers
.entry(buffer_id)
.or_insert_with(Default::default);
SyntaxIndexState::remove_buffer_declarations(
&buffer_state.declarations,
&mut state.declarations,
&mut state.identifiers,
);
let mut new_ids = Vec::with_capacity(declarations.len());
state.declarations.reserve(declarations.len());
for (parent_index, mut declaration) in declarations {
declaration.parent = parent_index
.and_then(|ix| some_or_debug_panic(new_ids.get(ix).copied()));
let identifier = declaration.identifier.clone();
let declaration_id = state.declarations.insert(Declaration::Buffer {
rope: rope.clone(),
buffer_id,
declaration,
project_entry_id,
});
new_ids.push(declaration_id);
state
.identifiers
.entry(identifier)
.or_default()
.insert(declaration_id);
}
buffer_state.declarations = new_ids;
});
})
.collect::<Vec<_>>();
let Some(state) = state.upgrade() else {
return;
};
let mut state = state.lock().await;
let state = state.deref_mut();
let buffer_state = state
.buffers
.entry(buffer_id)
.or_insert_with(Default::default);
SyntaxIndexState::remove_buffer_declarations(
&buffer_state.declarations,
&mut state.declarations,
&mut state.identifiers,
);
let mut new_ids = Vec::with_capacity(declarations.len());
state.declarations.reserve(declarations.len());
for (parent_index, mut declaration) in declarations {
declaration.parent =
parent_index.and_then(|ix| some_or_debug_panic(new_ids.get(ix).copied()));
let identifier = declaration.identifier.clone();
let declaration_id = state.declarations.insert(Declaration::Buffer {
rope: rope.clone(),
buffer_id,
declaration,
project_entry_id,
});
new_ids.push(declaration_id);
state
.identifiers
.entry(identifier)
.or_default()
.insert(declaration_id);
.ok();
}
buffer_state.declarations = new_ids;
});
self.with_state(cx, move |state| {
@@ -465,53 +309,28 @@ impl SyntaxIndex {
entry_id: ProjectEntryId,
project_path: ProjectPath,
cx: &mut Context<Self>,
) -> Task<()> {
) {
let Some(project) = self.project.upgrade() else {
return Task::ready(());
return;
};
let project = project.read(cx);
let language_registry = project.languages();
let Some(available_language) =
language_registry.language_for_file_path(project_path.path.as_std_path())
else {
return Task::ready(());
};
let language = if let Some(Ok(Ok(language))) = language_registry
.load_language(&available_language)
.now_or_never()
{
if language
.grammar()
.is_none_or(|grammar| grammar.outline_config.is_none())
{
return Task::ready(());
}
future::Either::Left(async { Ok(language) })
} else {
let language_registry = language_registry.clone();
future::Either::Right(async move {
anyhow::Ok(
language_registry
.load_language(&available_language)
.await??,
)
})
};
let Some(worktree) = project.worktree_for_id(project_path.worktree_id, cx) else {
return Task::ready(());
return;
};
let language_registry = project.languages().clone();
let snapshot_task = worktree.update(cx, |worktree, cx| {
let load_task = worktree.load_file(&project_path.path, cx);
cx.spawn(async move |_this, cx| {
let loaded_file = load_task.await?;
let language = language.await?;
let language = language_registry
.language_for_file_path(&project_path.path.as_std_path())
.await
.ok();
let buffer = cx.new(|cx| {
let mut buffer = Buffer::local(loaded_file.text, cx);
buffer.set_language(Some(language), cx);
buffer.set_language(language, cx);
buffer
})?;
@@ -524,58 +343,75 @@ impl SyntaxIndex {
})
});
let state = Arc::downgrade(&self.state);
cx.background_spawn(async move {
// TODO: How to handle errors?
let Ok(snapshot) = snapshot_task.await else {
return;
};
let parse_task = cx.background_spawn(async move {
let snapshot = snapshot_task.await?;
let rope = snapshot.as_rope();
let declarations = declarations_in_buffer(&snapshot)
.into_iter()
.map(|item| (item.parent_index, FileDeclaration::from_outline(item, rope)))
.collect::<Vec<_>>();
anyhow::Ok(declarations)
});
let Some(state) = state.upgrade() else {
return;
};
let mut state = state.lock().await;
let state = state.deref_mut();
let file_state = state.files.entry(entry_id).or_insert_with(Default::default);
for old_declaration_id in &file_state.declarations {
let Some(declaration) = state.declarations.remove(*old_declaration_id) else {
debug_panic!("declaration not found");
continue;
let task = cx.spawn({
async move |this, cx| {
// TODO: how to handle errors?
let Ok(declarations) = parse_task.await else {
return;
};
if let Some(identifier_declarations) =
state.identifiers.get_mut(declaration.identifier())
{
identifier_declarations.remove(old_declaration_id);
}
this.update(cx, |this, cx| {
this.with_state(cx, move |state| {
let file_state =
state.files.entry(entry_id).or_insert_with(Default::default);
for old_declaration_id in &file_state.declarations {
let Some(declaration) = state.declarations.remove(*old_declaration_id)
else {
debug_panic!("declaration not found");
continue;
};
if let Some(identifier_declarations) =
state.identifiers.get_mut(declaration.identifier())
{
identifier_declarations.remove(old_declaration_id);
}
}
let mut new_ids = Vec::with_capacity(declarations.len());
state.declarations.reserve(declarations.len());
for (parent_index, mut declaration) in declarations {
declaration.parent = parent_index
.and_then(|ix| some_or_debug_panic(new_ids.get(ix).copied()));
let identifier = declaration.identifier.clone();
let declaration_id = state.declarations.insert(Declaration::File {
project_entry_id: entry_id,
declaration,
});
new_ids.push(declaration_id);
state
.identifiers
.entry(identifier)
.or_default()
.insert(declaration_id);
}
file_state.declarations = new_ids;
});
})
.ok();
}
});
let mut new_ids = Vec::with_capacity(declarations.len());
state.declarations.reserve(declarations.len());
for (parent_index, mut declaration) in declarations {
declaration.parent =
parent_index.and_then(|ix| some_or_debug_panic(new_ids.get(ix).copied()));
let identifier = declaration.identifier.clone();
let declaration_id = state.declarations.insert(Declaration::File {
project_entry_id: entry_id,
declaration,
});
new_ids.push(declaration_id);
state
.identifiers
.entry(identifier)
.or_default()
.insert(declaration_id);
}
file_state.declarations = new_ids;
})
self.with_state(cx, move |state| {
state
.files
.entry(entry_id)
.or_insert_with(Default::default)
.task = Some(task);
});
}
}
@@ -740,13 +576,13 @@ mod tests {
let decls = index_state.declarations_for_identifier::<8>(&main);
assert_eq!(decls.len(), 2);
let decl = expect_file_decl("a.rs", &decls[0].1, &project, cx);
assert_eq!(decl.identifier, main);
assert_eq!(decl.item_range, 0..98);
let decl = expect_file_decl("c.rs", &decls[1].1, &project, cx);
let decl = expect_file_decl("c.rs", &decls[0].1, &project, cx);
assert_eq!(decl.identifier, main.clone());
assert_eq!(decl.item_range, 32..280);
let decl = expect_file_decl("a.rs", &decls[1].1, &project, cx);
assert_eq!(decl.identifier, main);
assert_eq!(decl.item_range, 0..98);
});
}
@@ -882,8 +718,8 @@ mod tests {
cx.update(|cx| {
let decls = index_state.declarations_for_identifier::<8>(&main);
assert_eq!(decls.len(), 2);
expect_file_decl("a.rs", &decls[0].1, &project, cx);
expect_file_decl("c.rs", &decls[1].1, &project, cx);
expect_file_decl("c.rs", &decls[0].1, &project, cx);
expect_file_decl("a.rs", &decls[1].1, &project, cx);
});
}
@@ -1016,8 +852,7 @@ mod tests {
let lang_id = lang.id();
language_registry.add(Arc::new(lang));
let file_indexing_parallelism = 2;
let index = cx.new(|cx| SyntaxIndex::new(&project, file_indexing_parallelism, cx));
let index = cx.new(|cx| SyntaxIndex::new(&project, cx));
cx.run_until_parked();
(project, index, lang_id)

View File

@@ -776,8 +776,6 @@ actions!(
UniqueLinesCaseInsensitive,
/// Removes duplicate lines (case-sensitive).
UniqueLinesCaseSensitive,
/// Removes the surrounding syntax node (for example brackets, or closures)
/// from the current selections.
UnwrapSyntaxNode,
/// Wraps selections in tag specified by language.
WrapSelectionsInTag

View File

@@ -122,7 +122,7 @@ use itertools::{Either, Itertools};
use language::{
AutoindentMode, BlockCommentConfig, BracketMatch, BracketPair, Buffer, BufferRow,
BufferSnapshot, Capability, CharClassifier, CharKind, CharScopeContext, CodeLabel, CursorShape,
DiagnosticEntryRef, DiffOptions, EditPredictionsMode, EditPreview, HighlightedText, IndentKind,
DiagnosticEntry, DiffOptions, EditPredictionsMode, EditPreview, HighlightedText, IndentKind,
IndentSize, Language, OffsetRangeExt, Point, Runnable, RunnableRange, Selection, SelectionGoal,
TextObject, TransactionId, TreeSitterOptions, WordsQuery,
language_settings::{
@@ -142,7 +142,7 @@ use mouse_context_menu::MouseContextMenu;
use movement::TextLayoutDetails;
use multi_buffer::{
ExcerptInfo, ExpandExcerptDirection, MultiBufferDiffHunk, MultiBufferPoint, MultiBufferRow,
ToOffsetUtf16,
MultiOrSingleBufferOffsetRange, ToOffsetUtf16,
};
use parking_lot::Mutex;
use persistence::DB;
@@ -404,7 +404,7 @@ pub fn set_blame_renderer(renderer: impl BlameRenderer + 'static, cx: &mut App)
pub trait DiagnosticRenderer {
fn render_group(
&self,
diagnostic_group: Vec<DiagnosticEntryRef<'_, Point>>,
diagnostic_group: Vec<DiagnosticEntry<Point>>,
buffer_id: BufferId,
snapshot: EditorSnapshot,
editor: WeakEntity<Editor>,
@@ -413,7 +413,7 @@ pub trait DiagnosticRenderer {
fn render_hover(
&self,
diagnostic_group: Vec<DiagnosticEntryRef<'_, Point>>,
diagnostic_group: Vec<DiagnosticEntry<Point>>,
range: Range<Point>,
buffer_id: BufferId,
cx: &mut App,
@@ -3211,27 +3211,22 @@ impl Editor {
let background_executor = cx.background_executor().clone();
let editor_id = cx.entity().entity_id().as_u64() as ItemId;
self.serialize_selections = cx.background_spawn(async move {
background_executor.timer(SERIALIZATION_THROTTLE_TIME).await;
let db_selections = selections
.iter()
.map(|selection| {
(
selection.start.to_offset(&snapshot),
selection.end.to_offset(&snapshot),
)
})
.collect();
background_executor.timer(SERIALIZATION_THROTTLE_TIME).await;
let db_selections = selections
.iter()
.map(|selection| {
(
selection.start.to_offset(&snapshot),
selection.end.to_offset(&snapshot),
)
})
.collect();
DB.save_editor_selections(editor_id, workspace_id, db_selections)
.await
.with_context(|| {
format!(
"persisting editor selections for editor {editor_id}, \
workspace {workspace_id:?}"
)
})
.log_err();
});
DB.save_editor_selections(editor_id, workspace_id, db_selections)
.await
.with_context(|| format!("persisting editor selections for editor {editor_id}, workspace {workspace_id:?}"))
.log_err();
});
}
}
@@ -6876,7 +6871,17 @@ impl Editor {
continue;
}
let range = Anchor::range_in_buffer(excerpt_id, buffer_id, start..end);
let range = Anchor {
buffer_id: Some(buffer_id),
excerpt_id,
text_anchor: start,
diff_base_anchor: None,
}..Anchor {
buffer_id: Some(buffer_id),
excerpt_id,
text_anchor: end,
diff_base_anchor: None,
};
if highlight.kind == lsp::DocumentHighlightKind::WRITE {
write_ranges.push(range);
} else {
@@ -12881,7 +12886,7 @@ impl Editor {
self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx);
self.change_selections(Default::default(), window, cx, |s| {
s.move_heads_with(|map, head, _| (movement::right(map, head), SelectionGoal::None));
});
})
}
pub fn move_up(&mut self, _: &MoveUp, window: &mut Window, cx: &mut Context<Self>) {
@@ -15170,8 +15175,12 @@ impl Editor {
}
let mut new_range = old_range.clone();
while let Some((node, range)) = buffer.syntax_ancestor(new_range.clone()) {
new_range = range;
while let Some((node, containing_range)) = buffer.syntax_ancestor(new_range.clone())
{
new_range = match containing_range {
MultiOrSingleBufferOffsetRange::Single(_) => break,
MultiOrSingleBufferOffsetRange::Multi(range) => range,
};
if !node.is_named() {
continue;
}
@@ -15301,14 +15310,20 @@ impl Editor {
&& let Some((_, ancestor_range)) =
buffer.syntax_ancestor(selection.start..selection.end)
{
ancestor_range
match ancestor_range {
MultiOrSingleBufferOffsetRange::Single(range) => range,
MultiOrSingleBufferOffsetRange::Multi(range) => range,
}
} else {
selection.range()
};
let mut parent = child.clone();
while let Some((_, ancestor_range)) = buffer.syntax_ancestor(parent.clone()) {
parent = ancestor_range;
parent = match ancestor_range {
MultiOrSingleBufferOffsetRange::Single(range) => range,
MultiOrSingleBufferOffsetRange::Multi(range) => range,
};
if parent.start < child.start || parent.end > child.end {
break;
}
@@ -15868,7 +15883,7 @@ impl Editor {
let snapshot = multi_buffer.snapshot(cx);
if let Some(buffer_id) = snapshot.buffer_id_for_excerpt(excerpt)
&& let Some(buffer) = multi_buffer.buffer(buffer_id)
&& let Some(excerpt_range) = snapshot.context_range_for_excerpt(excerpt)
&& let Some(excerpt_range) = snapshot.buffer_range_for_excerpt(excerpt)
{
let buffer_snapshot = buffer.read(cx).snapshot();
let excerpt_end_row = Point::from_anchor(&excerpt_range.end, &buffer_snapshot).row;
@@ -15965,11 +15980,11 @@ impl Editor {
active_group_id = Some(active_group.group_id);
}
fn filtered<'a>(
fn filtered(
snapshot: EditorSnapshot,
severity: GoToDiagnosticSeverityFilter,
diagnostics: impl Iterator<Item = DiagnosticEntryRef<'a, usize>>,
) -> impl Iterator<Item = DiagnosticEntryRef<'a, usize>> {
diagnostics: impl Iterator<Item = DiagnosticEntry<usize>>,
) -> impl Iterator<Item = DiagnosticEntry<usize>> {
diagnostics
.filter(move |entry| severity.matches(entry.diagnostic.severity))
.filter(|entry| entry.range.start != entry.range.end)
@@ -15993,7 +16008,7 @@ impl Editor {
.filter(|entry| entry.range.start >= selection.start),
);
let mut found: Option<DiagnosticEntryRef<usize>> = None;
let mut found: Option<DiagnosticEntry<usize>> = None;
if direction == Direction::Prev {
'outer: for prev_diagnostics in [before.collect::<Vec<_>>(), after.collect::<Vec<_>>()]
{
@@ -16903,8 +16918,7 @@ impl Editor {
let item_id = item.item_id();
if split {
let pane = workspace.adjacent_pane(window, cx);
workspace.add_item(pane, item, None, true, true, window, cx);
workspace.split_item(SplitDirection::Right, item, window, cx);
} else if PreviewTabsSettings::get_global(cx).enable_preview_from_code_navigation {
let (preview_item_id, preview_item_idx) =
workspace.active_pane().read_with(cx, |pane, _| {
@@ -17516,7 +17530,7 @@ impl Editor {
fn activate_diagnostics(
&mut self,
buffer_id: BufferId,
diagnostic: DiagnosticEntryRef<'_, usize>,
diagnostic: DiagnosticEntry<usize>,
window: &mut Window,
cx: &mut Context<Self>,
) {
@@ -17705,7 +17719,7 @@ impl Editor {
.map(|(line, _)| line)
.map(SharedString::new)
.unwrap_or_else(|| {
SharedString::new(&*diagnostic_entry.diagnostic.message)
SharedString::from(diagnostic_entry.diagnostic.message)
});
let start_anchor = snapshot.anchor_before(diagnostic_entry.range.start);
let (Ok(i) | Err(i)) = inline_diagnostics
@@ -22395,14 +22409,7 @@ fn wrap_with_prefix(
continue;
}
if !preserve_existing_whitespace {
// Keep a single whitespace grapheme as-is
if let Some(first) =
unicode_segmentation::UnicodeSegmentation::graphemes(token, true).next()
{
token = first;
} else {
token = " ";
}
token = " ";
grapheme_len = 1;
}
let current_prefix_len = if is_first_line {
@@ -22504,17 +22511,6 @@ fn test_wrap_with_prefix() {
),
"这是什\n么 钢\n"
);
assert_eq!(
wrap_with_prefix(
String::new(),
String::new(),
format!("foo{}bar", '\u{2009}'), // thin space
80,
NonZeroU32::new(4).unwrap(),
false,
),
format!("foo{}bar", '\u{2009}')
);
}
pub trait CollaborationHub {
@@ -24461,8 +24457,8 @@ fn all_edits_insertions_or_deletions(
struct MissingEditPredictionKeybindingTooltip;
impl Render for MissingEditPredictionKeybindingTooltip {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
ui::tooltip_container(cx, |container, cx| {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
ui::tooltip_container(window, cx, |container, _, cx| {
container
.flex_shrink_0()
.max_w_80()

View File

@@ -9161,64 +9161,6 @@ async fn test_unwrap_syntax_nodes(cx: &mut gpui::TestAppContext) {
});
cx.assert_editor_state(indoc! { r#"use mod1::{mod2::«mod3ˇ», mod5::«mod7ˇ»};"# });
cx.set_state(indoc! { r#"fn a() {
// what
// a
// ˇlong
// method
// I
// sure
// hope
// it
// works
}"# });
let buffer = cx.update_multibuffer(|multibuffer, _| multibuffer.as_singleton().unwrap());
let multi_buffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
cx.update(|_, cx| {
multi_buffer.update(cx, |multi_buffer, cx| {
multi_buffer.set_excerpts_for_path(
PathKey::for_buffer(&buffer, cx),
buffer,
[Point::new(1, 0)..Point::new(1, 0)],
3,
cx,
);
});
});
let editor2 = cx.new_window_entity(|window, cx| {
Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
});
let mut cx = EditorTestContext::for_editor_in(editor2, &mut cx).await;
cx.update_editor(|editor, window, cx| {
editor.change_selections(SelectionEffects::default(), window, cx, |s| {
s.select_ranges([Point::new(3, 0)..Point::new(3, 0)]);
})
});
cx.assert_editor_state(indoc! { "
fn a() {
// what
// a
ˇ // long
// method"});
cx.update_editor(|editor, window, cx| {
editor.unwrap_syntax_node(&UnwrapSyntaxNode, window, cx);
});
// Although we could potentially make the action work when the syntax node
// is half-hidden, it seems a bit dangerous as you can't easily tell what it
// did. Maybe we could also expand the excerpt to contain the range?
cx.assert_editor_state(indoc! { "
fn a() {
// what
// a
ˇ // long
// method"});
}
#[gpui::test]
@@ -11934,16 +11876,17 @@ async fn test_multiple_formatters(cx: &mut TestAppContext) {
);
fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
move |params, _| async move {
let requested_code_actions = params.context.only.expect("Expected code action request");
assert_eq!(requested_code_actions.len(), 1);
assert_eq!(
params.context.only,
Some(vec!["code-action-1".into(), "code-action-2".into()])
);
let uri = lsp::Uri::from_file_path(path!("/file.rs")).unwrap();
let code_action = match requested_code_actions[0].as_str() {
"code-action-1" => lsp::CodeAction {
Ok(Some(vec![
lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
kind: Some("code-action-1".into()),
edit: Some(lsp::WorkspaceEdit::new(
[(
uri,
uri.clone(),
vec![lsp::TextEdit::new(
lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
"applied-code-action-1-edit\n".to_string(),
@@ -11957,8 +11900,8 @@ async fn test_multiple_formatters(cx: &mut TestAppContext) {
..Default::default()
}),
..Default::default()
},
"code-action-2" => lsp::CodeAction {
}),
lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
kind: Some("code-action-2".into()),
edit: Some(lsp::WorkspaceEdit::new(
[(
@@ -11972,12 +11915,8 @@ async fn test_multiple_formatters(cx: &mut TestAppContext) {
.collect(),
)),
..Default::default()
},
req => panic!("Unexpected code action request: {:?}", req),
};
Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
code_action,
)]))
}),
]))
},
);

View File

@@ -1311,10 +1311,10 @@ impl EditorElement {
let range = snapshot
.buffer_snapshot
.anchor_before(start.to_point(&snapshot.display_snapshot))
.anchor_at(start.to_point(&snapshot.display_snapshot), Bias::Left)
..snapshot
.buffer_snapshot
.anchor_after(end.to_point(&snapshot.display_snapshot));
.anchor_at(end.to_point(&snapshot.display_snapshot), Bias::Right);
let Some(selection) = snapshot.remote_selections_in_range(&range, hub, cx).next() else {
return;
@@ -2618,7 +2618,7 @@ impl EditorElement {
let scroll_top = scroll_position.y * line_height;
let start_x = em_width;
let mut last_used_color: Option<(Hsla, Oid)> = None;
let mut last_used_color: Option<(PlayerColor, Oid)> = None;
let blame_renderer = cx.global::<GlobalBlameRenderer>().0.clone();
let shaped_lines = blamed_rows
@@ -2635,8 +2635,7 @@ impl EditorElement {
self.editor.clone(),
workspace.clone(),
buffer_id,
&*blame_renderer,
window,
blame_renderer.clone(),
cx,
)?;
@@ -7514,25 +7513,27 @@ fn render_blame_entry(
blame: &Entity<GitBlame>,
blame_entry: BlameEntry,
style: &EditorStyle,
last_used_color: &mut Option<(Hsla, Oid)>,
last_used_color: &mut Option<(PlayerColor, Oid)>,
editor: Entity<Editor>,
workspace: Entity<Workspace>,
buffer: BufferId,
renderer: &dyn BlameRenderer,
window: &mut Window,
renderer: Arc<dyn BlameRenderer>,
cx: &mut App,
) -> Option<AnyElement> {
let index: u32 = blame_entry.sha.into();
let mut sha_color = cx.theme().players().color_for_participant(index).cursor;
let mut sha_color = cx
.theme()
.players()
.color_for_participant(blame_entry.sha.into());
// If the last color we used is the same as the one we get for this line, but
// the commit SHAs are different, then we try again to get a different color.
if let Some((color, sha)) = *last_used_color
&& sha != blame_entry.sha
&& color == sha_color
{
sha_color = cx.theme().players().color_for_participant(index + 1).cursor;
}
match *last_used_color {
Some((color, sha)) if sha != blame_entry.sha && color.cursor == sha_color.cursor => {
let index: u32 = blame_entry.sha.into();
sha_color = cx.theme().players().color_for_participant(index + 1);
}
_ => {}
};
last_used_color.replace((sha_color, blame_entry.sha));
let blame = blame.read(cx);
@@ -7546,8 +7547,7 @@ fn render_blame_entry(
workspace.downgrade(),
editor,
ix,
sha_color,
window,
sha_color.cursor,
cx,
)
}

View File

@@ -95,7 +95,6 @@ pub trait BlameRenderer {
_: Entity<Editor>,
_: usize,
_: Hsla,
window: &mut Window,
_: &mut App,
) -> Option<AnyElement>;
@@ -143,7 +142,6 @@ impl BlameRenderer for () {
_: Entity<Editor>,
_: usize,
_: Hsla,
_: &mut Window,
_: &mut App,
) -> Option<AnyElement> {
None
@@ -675,8 +673,8 @@ async fn parse_commit_messages(
.as_ref()
.map(|(provider, remote)| GitRemote {
host: provider.clone(),
owner: remote.owner.clone().into(),
repo: remote.repo.clone().into(),
owner: remote.owner.to_string(),
repo: remote.repo.to_string(),
});
let pull_request = parsed_remote_url

View File

@@ -1,7 +1,6 @@
use crate::{
Anchor, Editor, EditorSettings, EditorSnapshot, FindAllReferences, GoToDefinition,
GoToDefinitionSplit, GoToTypeDefinition, GoToTypeDefinitionSplit, GotoDefinitionKind, InlayId,
Navigated, PointForPosition, SelectPhase,
GoToTypeDefinition, GotoDefinitionKind, InlayId, Navigated, PointForPosition, SelectPhase,
editor_settings::GoToDefinitionFallback,
hover_popover::{self, InlayHover},
scroll::ScrollAmount,
@@ -267,13 +266,10 @@ impl Editor {
);
let navigate_task = if point.as_valid().is_some() {
match (modifiers.shift, modifiers.alt) {
(true, true) => {
self.go_to_type_definition_split(&GoToTypeDefinitionSplit, window, cx)
}
(true, false) => self.go_to_type_definition(&GoToTypeDefinition, window, cx),
(false, true) => self.go_to_definition_split(&GoToDefinitionSplit, window, cx),
(false, false) => self.go_to_definition(&GoToDefinition, window, cx),
if modifiers.shift {
self.go_to_type_definition(&GoToTypeDefinition, window, cx)
} else {
self.go_to_definition(&GoToDefinition, window, cx)
}
} else {
Task::ready(Ok(Navigated::No))
@@ -301,10 +297,14 @@ pub fn update_inlay_link_and_hover_points(
let mut hover_updated = false;
if let Some(hovered_offset) = hovered_offset {
let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
let previous_valid_anchor =
buffer_snapshot.anchor_before(point_for_position.previous_valid.to_point(snapshot));
let next_valid_anchor =
buffer_snapshot.anchor_after(point_for_position.next_valid.to_point(snapshot));
let previous_valid_anchor = buffer_snapshot.anchor_at(
point_for_position.previous_valid.to_point(snapshot),
Bias::Left,
);
let next_valid_anchor = buffer_snapshot.anchor_at(
point_for_position.next_valid.to_point(snapshot),
Bias::Right,
);
if let Some(hovered_hint) = editor
.visible_inlay_hints(cx)
.into_iter()
@@ -1396,7 +1396,7 @@ mod tests {
let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx));
let expected_highlight = InlayHighlight {
inlay: InlayId::Hint(0),
inlay_position: buffer_snapshot.anchor_after(inlay_range.start),
inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right),
range: 0..hint_label.len(),
};
assert_set_eq!(actual_highlights, vec![&expected_highlight]);

View File

@@ -16,7 +16,7 @@ use itertools::Itertools;
use language::{DiagnosticEntry, Language, LanguageRegistry};
use lsp::DiagnosticSeverity;
use markdown::{Markdown, MarkdownElement, MarkdownStyle};
use multi_buffer::{ToOffset, ToPoint};
use multi_buffer::{MultiOrSingleBufferOffsetRange, ToOffset, ToPoint};
use project::{HoverBlock, HoverBlockKind, InlayHintLabelPart};
use settings::Settings;
use std::{borrow::Cow, cell::RefCell};
@@ -371,7 +371,7 @@ fn show_hover(
this.update(cx, |_, cx| cx.observe(&markdown, |_, _, cx| cx.notify()))?;
let local_diagnostic = DiagnosticEntry {
diagnostic: local_diagnostic.diagnostic.to_owned(),
diagnostic: local_diagnostic.diagnostic,
range: snapshot
.buffer_snapshot
.anchor_before(local_diagnostic.range.start)
@@ -477,8 +477,13 @@ fn show_hover(
})
.or_else(|| {
let snapshot = &snapshot.buffer_snapshot;
let range = snapshot.syntax_ancestor(anchor..anchor)?.1;
Some(snapshot.anchor_before(range.start)..snapshot.anchor_after(range.end))
match snapshot.syntax_ancestor(anchor..anchor)?.1 {
MultiOrSingleBufferOffsetRange::Multi(range) => Some(
snapshot.anchor_before(range.start)
..snapshot.anchor_after(range.end),
),
MultiOrSingleBufferOffsetRange::Single(_) => None,
}
})
.unwrap_or_else(|| anchor..anchor);
@@ -1785,7 +1790,7 @@ mod tests {
popover.symbol_range,
RangeInEditor::Inlay(InlayHighlight {
inlay: InlayId::Hint(0),
inlay_position: buffer_snapshot.anchor_after(inlay_range.start),
inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right),
range: ": ".len()..": ".len() + new_type_label.len(),
}),
"Popover range should match the new type label part"
@@ -1840,7 +1845,7 @@ mod tests {
popover.symbol_range,
RangeInEditor::Inlay(InlayHighlight {
inlay: InlayId::Hint(0),
inlay_position: buffer_snapshot.anchor_after(inlay_range.start),
inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right),
range: ": ".len() + new_type_label.len() + "<".len()
..": ".len() + new_type_label.len() + "<".len() + struct_label.len(),
}),

View File

@@ -2251,7 +2251,7 @@ pub mod tests {
.unwrap();
}
#[gpui::test(iterations = 4)]
#[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(InlayHintSettingsContent {

View File

@@ -578,11 +578,12 @@ fn deserialize_selection(
fn deserialize_anchor(buffer: &MultiBufferSnapshot, anchor: proto::EditorAnchor) -> Option<Anchor> {
let excerpt_id = ExcerptId::from_proto(anchor.excerpt_id);
Some(Anchor::in_buffer(
Some(Anchor {
excerpt_id,
buffer.buffer_id_for_excerpt(excerpt_id)?,
language::proto::deserialize_anchor(anchor.anchor?)?,
))
text_anchor: language::proto::deserialize_anchor(anchor.anchor?)?,
buffer_id: buffer.buffer_id_for_excerpt(excerpt_id),
diff_base_anchor: None,
})
}
impl Item for Editor {
@@ -1751,8 +1752,13 @@ impl SearchableItem for Editor {
.anchor_after(search_range.start + match_range.start);
let end = search_buffer
.anchor_before(search_range.start + match_range.end);
deleted_hunk_anchor.with_diff_base_anchor(start)
..deleted_hunk_anchor.with_diff_base_anchor(end)
Anchor {
diff_base_anchor: Some(start),
..deleted_hunk_anchor
}..Anchor {
diff_base_anchor: Some(end),
..deleted_hunk_anchor
}
} else {
let start = search_buffer
.anchor_after(search_range.start + match_range.start);

View File

@@ -1018,22 +1018,22 @@ mod tests {
[
Inlay::edit_prediction(
post_inc(&mut id),
buffer_snapshot.anchor_before(offset),
buffer_snapshot.anchor_at(offset, Bias::Left),
"test",
),
Inlay::edit_prediction(
post_inc(&mut id),
buffer_snapshot.anchor_after(offset),
buffer_snapshot.anchor_at(offset, Bias::Right),
"test",
),
Inlay::mock_hint(
post_inc(&mut id),
buffer_snapshot.anchor_before(offset),
buffer_snapshot.anchor_at(offset, Bias::Left),
"test",
),
Inlay::mock_hint(
post_inc(&mut id),
buffer_snapshot.anchor_after(offset),
buffer_snapshot.anchor_at(offset, Bias::Right),
"test",
),
]

View File

@@ -244,7 +244,9 @@ impl ScrollManager {
Bias::Left,
)
.to_point(map);
let top_anchor = map.buffer_snapshot.anchor_after(scroll_top_buffer_point);
let top_anchor = map
.buffer_snapshot
.anchor_at(scroll_top_buffer_point, Bias::Right);
self.set_anchor(
ScrollAnchor {
@@ -765,7 +767,7 @@ impl Editor {
.buffer()
.read(cx)
.snapshot(cx)
.anchor_before(Point::new(top_row, 0));
.anchor_at(Point::new(top_row, 0), Bias::Left);
let scroll_anchor = ScrollAnchor {
offset: gpui::Point::new(x, y),
anchor: top_anchor,

View File

@@ -440,15 +440,6 @@ pub struct MutableSelectionsCollection<'a> {
cx: &'a mut App,
}
impl<'a> fmt::Debug for MutableSelectionsCollection<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("MutableSelectionsCollection")
.field("collection", &self.collection)
.field("selections_changed", &self.selections_changed)
.finish()
}
}
impl<'a> MutableSelectionsCollection<'a> {
pub fn display_map(&mut self) -> DisplaySnapshot {
self.collection.display_map(self.cx)

View File

@@ -17,8 +17,8 @@ pub struct PullRequest {
#[derive(Clone)]
pub struct GitRemote {
pub host: Arc<dyn GitHostingProvider + Send + Sync + 'static>,
pub owner: SharedString,
pub repo: SharedString,
pub owner: String,
pub repo: String,
}
impl std::fmt::Debug for GitRemote {

View File

@@ -1,5 +1,5 @@
use crate::{
commit_tooltip::{CommitAvatar, CommitTooltip},
commit_tooltip::{CommitAvatar, CommitDetails, CommitTooltip},
commit_view::CommitView,
};
use editor::{BlameRenderer, Editor, hover_markdown_style};
@@ -17,7 +17,7 @@ use settings::Settings as _;
use theme::ThemeSettings;
use time::OffsetDateTime;
use time_format::format_local_timestamp;
use ui::{ContextMenu, Divider, IconButtonShape, prelude::*, tooltip_container};
use ui::{ContextMenu, Divider, IconButtonShape, prelude::*};
use workspace::Workspace;
const GIT_BLAME_MAX_AUTHOR_CHARS_DISPLAYED: usize = 20;
@@ -39,7 +39,6 @@ impl BlameRenderer for GitBlameRenderer {
editor: Entity<Editor>,
ix: usize,
sha_color: Hsla,
window: &mut Window,
cx: &mut App,
) -> Option<AnyElement> {
let relative_timestamp = blame_entry_relative_timestamp(&blame_entry);
@@ -47,15 +46,6 @@ impl BlameRenderer for GitBlameRenderer {
let author_name = blame_entry.author.as_deref().unwrap_or("<no name>");
let name = util::truncate_and_trailoff(author_name, GIT_BLAME_MAX_AUTHOR_CHARS_DISPLAYED);
let avatar = if ProjectSettings::get_global(cx).git.blame.show_avatar {
CommitAvatar::new(
&blame_entry.sha.to_string().into(),
details.as_ref().and_then(|it| it.remote.as_ref()),
)
.render(window, cx)
} else {
None
};
Some(
div()
.mr_2()
@@ -73,7 +63,6 @@ impl BlameRenderer for GitBlameRenderer {
.items_center()
.gap_2()
.child(div().text_color(sha_color).child(short_commit_id))
.children(avatar)
.child(name),
)
.child(relative_timestamp)
@@ -190,22 +179,31 @@ impl BlameRenderer for GitBlameRenderer {
.and_then(|t| OffsetDateTime::from_unix_timestamp(t).ok())
.unwrap_or(OffsetDateTime::now_utc());
let sha = blame.sha.to_string().into();
let author: SharedString = blame
.author
.clone()
.unwrap_or("<no name>".to_string())
.into();
let author_email = blame.author_mail.as_deref().unwrap_or_default();
let avatar = CommitAvatar::new(&sha, details.as_ref().and_then(|it| it.remote.as_ref()))
.render(window, cx);
let short_commit_id = sha
.get(..8)
.map(|sha| sha.to_string().into())
.unwrap_or_else(|| sha.clone());
let absolute_timestamp = format_local_timestamp(
let commit_details = CommitDetails {
sha: blame.sha.to_string().into(),
commit_time,
author_name: blame
.author
.clone()
.unwrap_or("<no name>".to_string())
.into(),
author_email: blame.author_mail.unwrap_or("".to_string()).into(),
message: details,
};
let avatar = CommitAvatar::new(&commit_details).render(window, cx);
let author = commit_details.author_name.clone();
let author_email = commit_details.author_email.clone();
let short_commit_id = commit_details
.sha
.get(0..8)
.map(|sha| sha.to_string().into())
.unwrap_or_else(|| commit_details.sha.clone());
let full_sha = commit_details.sha.to_string();
let absolute_timestamp = format_local_timestamp(
commit_details.commit_time,
OffsetDateTime::now_utc(),
time_format::TimestampFormat::MediumAbsolute,
);
@@ -217,143 +215,165 @@ impl BlameRenderer for GitBlameRenderer {
style
};
let message = details
let message = commit_details
.message
.as_ref()
.map(|_| MarkdownElement::new(markdown.clone(), markdown_style).into_any())
.unwrap_or("<no commit message>".into_any());
let pull_request = details
let pull_request = commit_details
.message
.as_ref()
.and_then(|details| details.pull_request.clone());
let ui_font_size = ThemeSettings::get_global(cx).ui_font_size(cx);
let message_max_height = window.line_height() * 12 + (ui_font_size / 0.4);
let commit_summary = CommitSummary {
sha: sha.clone(),
subject: details
sha: commit_details.sha.clone(),
subject: commit_details
.message
.as_ref()
.and_then(|details| {
Some(
details
.message
.split('\n')
.next()?
.trim_end()
.to_string()
.into(),
)
})
.unwrap_or_default(),
commit_timestamp: commit_time.unix_timestamp(),
author_name: author.clone(),
.map_or(Default::default(), |message| {
message
.message
.split('\n')
.next()
.unwrap()
.trim_end()
.to_string()
.into()
}),
commit_timestamp: commit_details.commit_time.unix_timestamp(),
author_name: commit_details.author_name.clone(),
has_parent: false,
};
let ui_font = ThemeSettings::get_global(cx).ui_font.clone();
// padding to avoid tooltip appearing right below the mouse cursor
// TODO: use tooltip_container here
Some(
tooltip_container(cx, |d, cx| {
d.occlude()
.on_mouse_move(|_, _, cx| cx.stop_propagation())
.on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
.child(
v_flex()
.w(gpui::rems(30.))
.gap_4()
.child(
h_flex()
.pb_1p5()
.gap_x_2()
.overflow_x_hidden()
.flex_wrap()
.children(avatar)
.child(author)
.when(!author_email.is_empty(), |this| {
this.child(
div()
.text_color(cx.theme().colors().text_muted)
.child(author_email.to_owned()),
)
})
.border_b_1()
.border_color(cx.theme().colors().border_variant),
)
.child(
div()
.id("inline-blame-commit-message")
.child(message)
.max_h(message_max_height)
.overflow_y_scroll()
.track_scroll(&scroll_handle),
)
.child(
h_flex()
.text_color(cx.theme().colors().text_muted)
.w_full()
.justify_between()
.pt_1p5()
.border_t_1()
.border_color(cx.theme().colors().border_variant)
.child(absolute_timestamp)
.child(
h_flex()
.gap_1p5()
.when_some(pull_request, |this, pr| {
this.child(
Button::new(
"pull-request-button",
format!("#{}", pr.number),
div()
.pl_2()
.pt_2p5()
.child(
v_flex()
.elevation_2(cx)
.font(ui_font)
.text_ui(cx)
.text_color(cx.theme().colors().text)
.py_1()
.px_2()
.map(|el| {
el.occlude()
.on_mouse_move(|_, _, cx| cx.stop_propagation())
.on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
.child(
v_flex()
.w(gpui::rems(30.))
.gap_4()
.child(
h_flex()
.pb_1p5()
.gap_x_2()
.overflow_x_hidden()
.flex_wrap()
.children(avatar)
.child(author)
.when(!author_email.is_empty(), |this| {
this.child(
div()
.text_color(
cx.theme().colors().text_muted,
)
.child(author_email),
)
.color(Color::Muted)
.icon(IconName::PullRequest)
.icon_color(Color::Muted)
.icon_position(IconPosition::Start)
.style(ButtonStyle::Subtle)
.on_click(move |_, _, cx| {
cx.stop_propagation();
cx.open_url(pr.url.as_str())
}),
)
})
.child(Divider::vertical())
.child(
Button::new(
"commit-sha-button",
short_commit_id.clone(),
)
.style(ButtonStyle::Subtle)
.color(Color::Muted)
.icon(IconName::FileGit)
.icon_color(Color::Muted)
.icon_position(IconPosition::Start)
.on_click(move |_, window, cx| {
CommitView::open(
commit_summary.clone(),
repository.downgrade(),
workspace.clone(),
window,
cx,
);
cx.stop_propagation();
}),
)
.child(
IconButton::new("copy-sha-button", IconName::Copy)
.shape(IconButtonShape::Square)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.on_click(move |_, _, cx| {
cx.stop_propagation();
cx.write_to_clipboard(
ClipboardItem::new_string(
sha.to_string(),
),
})
.border_b_1()
.border_color(cx.theme().colors().border_variant),
)
.child(
div()
.id("inline-blame-commit-message")
.child(message)
.max_h(message_max_height)
.overflow_y_scroll()
.track_scroll(&scroll_handle),
)
.child(
h_flex()
.text_color(cx.theme().colors().text_muted)
.w_full()
.justify_between()
.pt_1p5()
.border_t_1()
.border_color(cx.theme().colors().border_variant)
.child(absolute_timestamp)
.child(
h_flex()
.gap_1p5()
.when_some(pull_request, |this, pr| {
this.child(
Button::new(
"pull-request-button",
format!("#{}", pr.number),
)
.color(Color::Muted)
.icon(IconName::PullRequest)
.icon_color(Color::Muted)
.icon_position(IconPosition::Start)
.style(ButtonStyle::Subtle)
.on_click(move |_, _, cx| {
cx.stop_propagation();
cx.open_url(pr.url.as_str())
}),
)
})
.child(Divider::vertical())
.child(
Button::new(
"commit-sha-button",
short_commit_id.clone(),
)
.style(ButtonStyle::Subtle)
.color(Color::Muted)
.icon(IconName::FileGit)
.icon_color(Color::Muted)
.icon_position(IconPosition::Start)
.on_click(move |_, window, cx| {
CommitView::open(
commit_summary.clone(),
repository.downgrade(),
workspace.clone(),
window,
cx,
);
cx.stop_propagation();
}),
)
}),
),
),
),
)
})
.into_any_element(),
.child(
IconButton::new(
"copy-sha-button",
IconName::Copy,
)
.shape(IconButtonShape::Square)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.on_click(move |_, _, cx| {
cx.stop_propagation();
cx.write_to_clipboard(
ClipboardItem::new_string(
full_sha.clone(),
),
)
}),
),
),
),
)
}),
)
.into_any_element(),
)
}

View File

@@ -28,33 +28,25 @@ pub struct CommitDetails {
}
pub struct CommitAvatar<'a> {
sha: &'a SharedString,
remote: Option<&'a GitRemote>,
commit: &'a CommitDetails,
}
impl<'a> CommitAvatar<'a> {
pub fn new(sha: &'a SharedString, remote: Option<&'a GitRemote>) -> Self {
Self { sha, remote }
}
pub fn from_commit_details(details: &'a CommitDetails) -> Self {
Self {
sha: &details.sha,
remote: details
.message
.as_ref()
.and_then(|details| details.remote.as_ref()),
}
pub fn new(details: &'a CommitDetails) -> Self {
Self { commit: details }
}
}
impl<'a> CommitAvatar<'a> {
pub fn render(&'a self, window: &mut Window, cx: &mut App) -> Option<impl IntoElement + use<>> {
let remote = self
.remote
.commit
.message
.as_ref()
.and_then(|details| details.remote.clone())
.filter(|remote| remote.host_supports_avatars())?;
let avatar_url = CommitAvatarAsset::new(remote.clone(), self.sha.clone());
let avatar_url = CommitAvatarAsset::new(remote, self.commit.sha.clone());
let element = match window.use_asset::<CommitAvatarAsset>(&avatar_url, cx) {
// Loading or no avatar found
@@ -177,7 +169,7 @@ impl CommitTooltip {
impl Render for CommitTooltip {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let avatar = CommitAvatar::from_commit_details(&self.commit).render(window, cx);
let avatar = CommitAvatar::new(&self.commit).render(window, cx);
let author = self.commit.author_name.clone();
@@ -241,7 +233,7 @@ impl Render for CommitTooltip {
has_parent: false,
};
tooltip_container(cx, move |this, cx| {
tooltip_container(window, cx, move |this, _, cx| {
this.occlude()
.on_mouse_move(|_, _, cx| cx.stop_propagation())
.on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())

View File

@@ -23,7 +23,7 @@ On macOS, GPUI uses Metal for rendering. In order to use Metal, you need to do t
- Install [Xcode](https://apps.apple.com/us/app/xcode/id497799835?mt=12) from the macOS App Store, or from the [Apple Developer](https://developer.apple.com/download/all/) website. Note this requires a developer account.
> Ensure you launch Xcode after installing, and install the macOS components, which is the default option.
> Ensure you launch Xcode after installing, and install the macOS components, which is the default option. If you are on macOS 26 (Tahoe) you will need to use `--features gpui/runtime_shaders` or add the feature in the root `Cargo.toml`
- Install [Xcode command line tools](https://developer.apple.com/xcode/resources/)

View File

@@ -1,140 +0,0 @@
//! In GPUI, every model or view in the application is actually owned by a single top-level object called the `App`. When a new entity or view is created (referred to collectively as _entities_), the application is given ownership of their state to enable their participation in a variety of app services and interaction with other entities.
//!
//! To illustrate, consider the trivial app below. We start the app by calling `run` with a callback, which is passed a reference to the `App` that owns all the state for the application. This `App` is our gateway to all application-level services, such as opening windows, presenting dialogs, etc. It also has an `insert_entity` method, which is called below to create an entity and give ownership of it to the application.
//!
//! ```no_run
//! # use gpui::{App, AppContext, Application, Entity};
//! # struct Counter {
//! # count: usize,
//! # }
//! Application::new().run(|cx: &mut App| {
//! let _counter: Entity<Counter> = cx.new(|_cx| Counter { count: 0 });
//! // ...
//! });
//! ```
//!
//! The call to `new_entity` returns an _entity handle_, which carries a type parameter based on the type of object it references. By itself, this `Entity<Counter>` handle doesn't provide access to the entity's state. It's merely an inert identifier plus a compile-time type tag, and it maintains a reference counted pointer to the underlying `Counter` object that is owned by the app.
//!
//! Much like an `Rc` from the Rust standard library, this reference count is incremented when the handle is cloned and decremented when it is dropped to enable shared ownership over the underlying model, but unlike an `Rc` it only provides access to the model's state when a reference to an `App` is available. The handle doesn't truly _own_ the state, but it can be used to access the state from its true owner, the `App`. Stripping away some of the setup code for brevity:
//!
//! ```no_run
//! # use gpui::{App, AppContext, Application, Context, Entity};
//! # struct Counter {
//! # count: usize,
//! # }
//! Application::new().run(|cx: &mut App| {
//! let counter: Entity<Counter> = cx.new(|_cx| Counter { count: 0 });
//! // Call `update` to access the model's state.
//! counter.update(cx, |counter: &mut Counter, _cx: &mut Context<Counter>| {
//! counter.count += 1;
//! });
//! });
//! ```
//!
//! To update the counter, we call `update` on the handle, passing the context reference and a callback. The callback is yielded a mutable reference to the counter, which can be used to manipulate state.
//!
//! The callback is also provided a second `Context<Counter>` reference. This reference is similar to the `App` reference provided to the `run` callback. A `Context` is actually a wrapper around the `App`, including some additional data to indicate which particular entity it is tied to; in this case the counter.
//!
//! In addition to the application-level services provided by `App`, a `Context` provides access to entity-level services. For example, it can be used it to inform observers of this entity that its state has changed. Let's add that to our example, by calling `cx.notify()`.
//!
//! ```no_run
//! # use gpui::{App, AppContext, Application, Entity};
//! # struct Counter {
//! # count: usize,
//! # }
//! Application::new().run(|cx: &mut App| {
//! let counter: Entity<Counter> = cx.new(|_cx| Counter { count: 0 });
//! counter.update(cx, |counter, cx| {
//! counter.count += 1;
//! cx.notify(); // Notify observers
//! });
//! });
//! ```
//!
//! Next, these notifications need to be observed and reacted to. Before updating the counter, we'll construct a second counter that observes it. Whenever the first counter changes, twice its count is assigned to the second counter. Note how `observe` is called on the `Context` belonging to our second counter to arrange for it to be notified whenever the first counter notifies. The call to `observe` returns a `Subscription`, which is `detach`ed to preserve this behavior for as long as both counters exist. We could also store this subscription and drop it at a time of our choosing to cancel this behavior.
//!
//! The `observe` callback is passed a mutable reference to the observer and a _handle_ to the observed counter, whose state we access with the `read` method.
//!
//! ```no_run
//! # use gpui::{App, AppContext, Application, Entity, prelude::*};
//! # struct Counter {
//! # count: usize,
//! # }
//! Application::new().run(|cx: &mut App| {
//! let first_counter: Entity<Counter> = cx.new(|_cx| Counter { count: 0 });
//!
//! let second_counter = cx.new(|cx: &mut Context<Counter>| {
//! // Note we can set up the callback before the Counter is even created!
//! cx.observe(
//! &first_counter,
//! |second: &mut Counter, first: Entity<Counter>, cx| {
//! second.count = first.read(cx).count * 2;
//! },
//! )
//! .detach();
//!
//! Counter { count: 0 }
//! });
//!
//! first_counter.update(cx, |counter, cx| {
//! counter.count += 1;
//! cx.notify();
//! });
//!
//! assert_eq!(second_counter.read(cx).count, 2);
//! });
//! ```
//!
//! After updating the first counter, it can be noted that the observing counter's state is maintained according to our subscription.
//!
//! In addition to `observe` and `notify`, which indicate that an entity's state has changed, GPUI also offers `subscribe` and `emit`, which enables entities to emit typed events. To opt into this system, the emitting object must implement the `EventEmitter` trait.
//!
//! Let's introduce a new event type called `CounterChangeEvent`, then indicate that `Counter` can emit this type of event:
//!
//! ```no_run
//! use gpui::EventEmitter;
//! # struct Counter {
//! # count: usize,
//! # }
//! struct CounterChangeEvent {
//! increment: usize,
//! }
//!
//! impl EventEmitter<CounterChangeEvent> for Counter {}
//! ```
//!
//! Next, the example should be updated, replacing the observation with a subscription. Whenever the counter is incremented, a `Change` event is emitted to indicate the magnitude of the increase.
//!
//! ```no_run
//! # use gpui::{App, AppContext, Application, Context, Entity, EventEmitter};
//! # struct Counter {
//! # count: usize,
//! # }
//! # struct CounterChangeEvent {
//! # increment: usize,
//! # }
//! # impl EventEmitter<CounterChangeEvent> for Counter {}
//! Application::new().run(|cx: &mut App| {
//! let first_counter: Entity<Counter> = cx.new(|_cx| Counter { count: 0 });
//!
//! let second_counter = cx.new(|cx: &mut Context<Counter>| {
//! // Note we can set up the callback before the Counter is even created!
//! cx.subscribe(&first_counter, |second: &mut Counter, _first: Entity<Counter>, event, _cx| {
//! second.count += event.increment * 2;
//! })
//! .detach();
//!
//! Counter {
//! count: first_counter.read(cx).count * 2,
//! }
//! });
//!
//! first_counter.update(cx, |first, cx| {
//! first.count += 2;
//! cx.emit(CounterChangeEvent { increment: 2 });
//! cx.notify();
//! });
//!
//! assert_eq!(second_counter.read(cx).count, 4);
//! });
//! ```

View File

@@ -4,12 +4,12 @@ use crate::{
Subscription, Task, WeakEntity, WeakFocusHandle, Window, WindowHandle,
};
use anyhow::Result;
use derive_more::{Deref, DerefMut};
use futures::FutureExt;
use std::{
any::{Any, TypeId},
borrow::{Borrow, BorrowMut},
future::Future,
ops,
sync::Arc,
};
use util::Deferred;
@@ -17,25 +17,14 @@ use util::Deferred;
use super::{App, AsyncWindowContext, Entity, KeystrokeEvent};
/// The app context, with specialized behavior for the given entity.
#[derive(Deref, DerefMut)]
pub struct Context<'a, T> {
#[deref]
#[deref_mut]
app: &'a mut App,
entity_state: WeakEntity<T>,
}
impl<'a, T> ops::Deref for Context<'a, T> {
type Target = App;
fn deref(&self) -> &Self::Target {
self.app
}
}
impl<'a, T> ops::DerefMut for Context<'a, T> {
fn deref_mut(&mut self) -> &mut Self::Target {
self.app
}
}
impl<'a, T: 'static> Context<'a, T> {
pub(crate) fn new_context(app: &'a mut App, entity_state: WeakEntity<T>) -> Self {
Self { app, entity_state }

View File

@@ -1384,10 +1384,6 @@ impl Element for Div {
(child_max - child_min).into()
};
if let Some(scroll_handle) = self.interactivity.tracked_scroll_handle.as_ref() {
scroll_handle.scroll_to_active_item();
}
self.interactivity.prepaint(
global_id,
inspector_id,
@@ -2990,7 +2986,8 @@ where
}
/// Represents an element that can be scrolled *to* in its parent element.
/// Contrary to [ScrollHandle::scroll_to_active_item], an anchored element does not have to be an immediate child of the parent.
///
/// Contrary to [ScrollHandle::scroll_to_item], an anchored element does not have to be an immediate child of the parent.
#[derive(Clone)]
pub struct ScrollAnchor {
handle: ScrollHandle,
@@ -3025,7 +3022,6 @@ struct ScrollHandleState {
child_bounds: Vec<Bounds<Pixels>>,
scroll_to_bottom: bool,
overflow: Point<Overflow>,
active_item: Option<usize>,
}
/// A handle to the scrollable aspects of an element.
@@ -3085,44 +3081,32 @@ impl ScrollHandle {
self.0.borrow().child_bounds.get(ix).cloned()
}
/// Update [ScrollHandleState]'s active item for scrolling to in prepaint
pub fn scroll_to_item(&self, ix: usize) {
let mut state = self.0.borrow_mut();
state.active_item = Some(ix);
}
/// Scrolls the minimal amount to ensure that the child is
/// scroll_to_item scrolls the minimal amount to ensure that the child is
/// fully visible
fn scroll_to_active_item(&self) {
let mut state = self.0.borrow_mut();
pub fn scroll_to_item(&self, ix: usize) {
let state = self.0.borrow();
let Some(active_item_index) = state.active_item else {
let Some(bounds) = state.child_bounds.get(ix) else {
return;
};
let active_item = match state.child_bounds.get(active_item_index) {
Some(bounds) => {
let mut scroll_offset = state.offset.borrow_mut();
if state.overflow.y == Overflow::Scroll {
if bounds.top() + scroll_offset.y < state.bounds.top() {
scroll_offset.y = state.bounds.top() - bounds.top();
} else if bounds.bottom() + scroll_offset.y > state.bounds.bottom() {
scroll_offset.y = state.bounds.bottom() - bounds.bottom();
}
}
let mut scroll_offset = state.offset.borrow_mut();
if state.overflow.x == Overflow::Scroll {
if bounds.left() + scroll_offset.x < state.bounds.left() {
scroll_offset.x = state.bounds.left() - bounds.left();
} else if bounds.right() + scroll_offset.x > state.bounds.right() {
scroll_offset.x = state.bounds.right() - bounds.right();
}
}
None
if state.overflow.y == Overflow::Scroll {
if bounds.top() + scroll_offset.y < state.bounds.top() {
scroll_offset.y = state.bounds.top() - bounds.top();
} else if bounds.bottom() + scroll_offset.y > state.bounds.bottom() {
scroll_offset.y = state.bounds.bottom() - bounds.bottom();
}
None => Some(active_item_index),
};
state.active_item = active_item;
}
if state.overflow.x == Overflow::Scroll {
if bounds.left() + scroll_offset.x < state.bounds.left() {
scroll_offset.x = state.bounds.left() - bounds.left();
} else if bounds.right() + scroll_offset.x > state.bounds.right() {
scroll_offset.x = state.bounds.right() - bounds.right();
}
}
}
/// Scrolls to the bottom.

View File

@@ -88,10 +88,6 @@ pub enum ScrollStrategy {
/// May not be possible if there's not enough list items above the item scrolled to:
/// in this case, the element will be placed at the closest possible position.
Center,
/// Attempt to place the element at the bottom of the list's viewport.
/// May not be possible if there's not enough list items above the item scrolled to:
/// in this case, the element will be placed at the closest possible position.
Bottom,
}
#[derive(Clone, Copy, Debug)]
@@ -103,7 +99,6 @@ pub struct DeferredScrollToItem {
pub strategy: ScrollStrategy,
/// The offset in number of items
pub offset: usize,
pub scroll_strict: bool,
}
#[derive(Clone, Debug, Default)]
@@ -138,23 +133,12 @@ impl UniformListScrollHandle {
})))
}
/// Scroll the list so that the given item index is onscreen.
/// Scroll the list to the given item index.
pub fn scroll_to_item(&self, ix: usize, strategy: ScrollStrategy) {
self.0.borrow_mut().deferred_scroll_to_item = Some(DeferredScrollToItem {
item_index: ix,
strategy,
offset: 0,
scroll_strict: false,
});
}
/// Scroll the list so that the given item index is at scroll strategy position.
pub fn scroll_to_item_strict(&self, ix: usize, strategy: ScrollStrategy) {
self.0.borrow_mut().deferred_scroll_to_item = Some(DeferredScrollToItem {
item_index: ix,
strategy,
offset: 0,
scroll_strict: true,
});
}
@@ -168,7 +152,6 @@ impl UniformListScrollHandle {
item_index: ix,
strategy,
offset,
scroll_strict: false,
});
}
@@ -385,35 +368,24 @@ impl Element for UniformList {
updated_scroll_offset.y = -(item_bottom - list_height) - padding.bottom;
}
if deferred_scroll.scroll_strict
|| (scrolled_to_top
&& (item_top < scroll_top + offset_pixels
|| item_bottom > scroll_top + list_height))
{
match deferred_scroll.strategy {
ScrollStrategy::Top => {
updated_scroll_offset.y = -item_top
.max(Pixels::ZERO)
.min(content_height - list_height)
.max(Pixels::ZERO);
}
ScrollStrategy::Center => {
match deferred_scroll.strategy {
ScrollStrategy::Top => {}
ScrollStrategy::Center => {
if scrolled_to_top {
let item_center = item_top + item_height / 2.0;
let viewport_height = list_height - offset_pixels;
let viewport_center = offset_pixels + viewport_height / 2.0;
let target_scroll_top = item_center - viewport_center;
updated_scroll_offset.y = -target_scroll_top
.max(Pixels::ZERO)
.min(content_height - list_height)
.max(Pixels::ZERO);
}
ScrollStrategy::Bottom => {
updated_scroll_offset.y = -(item_bottom - list_height)
.max(Pixels::ZERO)
.min(content_height - list_height)
.max(Pixels::ZERO);
if item_top < scroll_top + offset_pixels
|| item_bottom > scroll_top + list_height
{
updated_scroll_offset.y = -target_scroll_top
.max(Pixels::ZERO)
.min(content_height - list_height)
.max(Pixels::ZERO);
}
}
}
}

View File

@@ -13,8 +13,6 @@
//! gpui = { git = "https://github.com/zed-industries/zed" }
//! ```
//!
//! - [Ownership and data flow](_ownership_and_data_flow)
//!
//! Everything in GPUI starts with an [`Application`]. You can create one with [`Application::new`], and
//! kick off your application by passing a callback to [`Application::run`]. Inside this callback,
//! you can create a new window with [`App::open_window`], and register your first root
@@ -67,8 +65,6 @@
#![allow(clippy::collapsible_else_if)] // False positives in platform specific code
#![allow(unused_mut)] // False positives in platform specific code
extern crate self as gpui;
#[macro_use]
mod action;
mod app;
@@ -109,9 +105,6 @@ mod util;
mod view;
mod window;
#[cfg(doc)]
pub mod _ownership_and_data_flow;
/// Do not touch, here be dragons for use by gpui_macros and such.
#[doc(hidden)]
pub mod private {
@@ -164,8 +157,6 @@ pub use taffy::{AvailableSpace, LayoutId};
#[cfg(any(test, feature = "test-support"))]
pub use test::*;
pub use text_system::*;
#[cfg(any(test, feature = "test-support"))]
pub use util::smol_timeout;
pub use util::{FutureExt, Timeout, arc_cow::ArcCow};
pub use view::*;
pub use window::*;

View File

@@ -396,6 +396,7 @@ impl MacTextSystemState {
let subpixel_shift = params
.subpixel_variant
.map(|v| v as f32 / SUBPIXEL_VARIANTS_X as f32);
cx.set_allows_font_smoothing(true);
cx.set_text_drawing_mode(CGTextDrawingMode::CGTextFill);
cx.set_gray_fill_color(0.0, 1.0);
cx.set_allows_antialiasing(true);

View File

@@ -114,8 +114,6 @@ impl<T: Future> Future for WithTimeout<T> {
}
#[cfg(any(test, feature = "test-support"))]
/// Uses smol executor to run a given future no longer than the timeout specified.
/// Note that this won't "rewind" on `cx.executor().advance_clock` call, truly waiting for the timeout to elapse.
pub async fn smol_timeout<F, T>(timeout: Duration, f: F) -> Result<T, ()>
where
F: Future<Output = T>,
@@ -142,35 +140,3 @@ pub(crate) fn atomic_incr_if_not_zero(counter: &AtomicUsize) -> usize {
}
}
}
#[cfg(test)]
mod tests {
use crate::TestAppContext;
use super::*;
#[gpui::test]
async fn test_with_timeout(cx: &mut TestAppContext) {
Task::ready(())
.with_timeout(Duration::from_secs(1), &cx.executor())
.await
.expect("Timeout should be noop");
let long_duration = Duration::from_secs(6000);
let short_duration = Duration::from_secs(1);
cx.executor()
.timer(long_duration)
.with_timeout(short_duration, &cx.executor())
.await
.expect_err("timeout should have triggered");
let fut = cx
.executor()
.timer(long_duration)
.with_timeout(short_duration, &cx.executor());
cx.executor().advance_clock(short_duration * 2);
futures::FutureExt::now_or_never(fut)
.unwrap_or_else(|| panic!("timeout should have triggered"))
.expect_err("timeout");
}
}

View File

@@ -165,7 +165,6 @@ pub enum IconName {
Option,
PageDown,
PageUp,
Paperclip,
Pencil,
PencilUnavailable,
Person,

View File

@@ -1,7 +1,7 @@
use crate::{
DebuggerTextObject, LanguageScope, Outline, OutlineConfig, RunnableCapture, RunnableTag,
TextObject, TreeSitterOptions,
diagnostic_set::{DiagnosticEntry, DiagnosticEntryRef, DiagnosticGroup},
diagnostic_set::{DiagnosticEntry, DiagnosticGroup},
language_settings::{LanguageSettings, language_settings},
outline::OutlineItem,
syntax_map::{
@@ -4519,7 +4519,7 @@ impl BufferSnapshot {
&'a self,
search_range: Range<T>,
reversed: bool,
) -> impl 'a + Iterator<Item = DiagnosticEntryRef<'a, O>>
) -> impl 'a + Iterator<Item = DiagnosticEntry<O>>
where
T: 'a + Clone + ToOffset,
O: 'a + FromAnchor,
@@ -4552,13 +4552,11 @@ impl BufferSnapshot {
})?;
iterators[next_ix]
.next()
.map(
|DiagnosticEntryRef { range, diagnostic }| DiagnosticEntryRef {
diagnostic,
range: FromAnchor::from_anchor(&range.start, self)
..FromAnchor::from_anchor(&range.end, self),
},
)
.map(|DiagnosticEntry { range, diagnostic }| DiagnosticEntry {
diagnostic,
range: FromAnchor::from_anchor(&range.start, self)
..FromAnchor::from_anchor(&range.end, self),
})
})
}
@@ -4574,7 +4572,7 @@ impl BufferSnapshot {
pub fn diagnostic_groups(
&self,
language_server_id: Option<LanguageServerId>,
) -> Vec<(LanguageServerId, DiagnosticGroup<'_, Anchor>)> {
) -> Vec<(LanguageServerId, DiagnosticGroup<Anchor>)> {
let mut groups = Vec::new();
if let Some(language_server_id) = language_server_id {
@@ -4605,7 +4603,7 @@ impl BufferSnapshot {
pub fn diagnostic_group<O>(
&self,
group_id: usize,
) -> impl Iterator<Item = DiagnosticEntryRef<'_, O>> + use<'_, O>
) -> impl Iterator<Item = DiagnosticEntry<O>> + '_
where
O: FromAnchor + 'static,
{

View File

@@ -34,66 +34,19 @@ pub struct DiagnosticEntry<T> {
pub diagnostic: Diagnostic,
}
/// A single diagnostic in a set. Generic over its range type, because
/// the diagnostics are stored internally as [`Anchor`]s, but can be
/// resolved to different coordinates types like [`usize`] byte offsets or
/// [`Point`](gpui::Point)s.
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct DiagnosticEntryRef<'a, T> {
/// The range of the buffer where the diagnostic applies.
pub range: Range<T>,
/// The information about the diagnostic.
pub diagnostic: &'a Diagnostic,
}
impl<T: PartialEq> PartialEq<DiagnosticEntry<T>> for DiagnosticEntryRef<'_, T> {
fn eq(&self, other: &DiagnosticEntry<T>) -> bool {
self.range == other.range && *self.diagnostic == other.diagnostic
}
}
impl<T: PartialEq> PartialEq<DiagnosticEntryRef<'_, T>> for DiagnosticEntry<T> {
fn eq(&self, other: &DiagnosticEntryRef<'_, T>) -> bool {
self.range == other.range && self.diagnostic == *other.diagnostic
}
}
impl<T: Clone> DiagnosticEntryRef<'_, T> {
pub fn to_owned(&self) -> DiagnosticEntry<T> {
DiagnosticEntry {
range: self.range.clone(),
diagnostic: self.diagnostic.clone(),
}
}
}
impl<'a> DiagnosticEntryRef<'a, Anchor> {
/// Converts the [DiagnosticEntry] to a different buffer coordinate type.
pub fn resolve<O: FromAnchor>(
&self,
buffer: &text::BufferSnapshot,
) -> DiagnosticEntryRef<'a, O> {
DiagnosticEntryRef {
range: O::from_anchor(&self.range.start, buffer)
..O::from_anchor(&self.range.end, buffer),
diagnostic: &self.diagnostic,
}
}
}
/// A group of related diagnostics, ordered by their start position
/// in the buffer.
#[derive(Debug, Serialize)]
pub struct DiagnosticGroup<'a, T> {
pub struct DiagnosticGroup<T> {
/// The diagnostics.
pub entries: Vec<DiagnosticEntryRef<'a, T>>,
pub entries: Vec<DiagnosticEntry<T>>,
/// The index into `entries` where the primary diagnostic is stored.
pub primary_ix: usize,
}
impl<'a> DiagnosticGroup<'a, Anchor> {
impl DiagnosticGroup<Anchor> {
/// Converts the entries in this [`DiagnosticGroup`] to a different buffer coordinate type.
pub fn resolve<O: FromAnchor>(&self, buffer: &text::BufferSnapshot) -> DiagnosticGroup<'a, O> {
pub fn resolve<O: FromAnchor>(&self, buffer: &text::BufferSnapshot) -> DiagnosticGroup<O> {
DiagnosticGroup {
entries: self
.entries
@@ -131,23 +84,6 @@ impl DiagnosticEntry<PointUtf16> {
})
}
}
impl DiagnosticEntryRef<'_, PointUtf16> {
/// Returns a raw LSP diagnostic used to provide diagnostic context to LSP
/// codeAction request
pub fn to_lsp_diagnostic_stub(&self) -> Result<lsp::Diagnostic> {
let range = range_to_lsp(self.range.clone())?;
Ok(lsp::Diagnostic {
range,
code: self.diagnostic.code.clone(),
severity: Some(self.diagnostic.severity),
source: self.diagnostic.source.clone(),
message: self.diagnostic.message.clone(),
data: self.diagnostic.data.clone(),
..Default::default()
})
}
}
impl DiagnosticSet {
/// Constructs a [DiagnosticSet] from a sequence of entries, ordered by
@@ -202,7 +138,7 @@ impl DiagnosticSet {
buffer: &'a text::BufferSnapshot,
inclusive: bool,
reversed: bool,
) -> impl 'a + Iterator<Item = DiagnosticEntryRef<'a, O>>
) -> impl 'a + Iterator<Item = DiagnosticEntry<O>>
where
T: 'a + ToOffset,
O: FromAnchor,
@@ -243,10 +179,10 @@ impl DiagnosticSet {
}
/// Adds all of this set's diagnostic groups to the given output vector.
pub fn groups<'a>(
&'a self,
pub fn groups(
&self,
language_server_id: LanguageServerId,
output: &mut Vec<(LanguageServerId, DiagnosticGroup<'a, Anchor>)>,
output: &mut Vec<(LanguageServerId, DiagnosticGroup<Anchor>)>,
buffer: &text::BufferSnapshot,
) {
let mut groups = HashMap::default();
@@ -254,10 +190,7 @@ impl DiagnosticSet {
groups
.entry(entry.diagnostic.group_id)
.or_insert(Vec::new())
.push(DiagnosticEntryRef {
range: entry.range.clone(),
diagnostic: &entry.diagnostic,
});
.push(entry.clone());
}
let start_ix = output.len();
@@ -291,7 +224,7 @@ impl DiagnosticSet {
&'a self,
group_id: usize,
buffer: &'a text::BufferSnapshot,
) -> impl 'a + Iterator<Item = DiagnosticEntryRef<'a, O>> {
) -> impl 'a + Iterator<Item = DiagnosticEntry<O>> {
self.iter()
.filter(move |entry| entry.diagnostic.group_id == group_id)
.map(|entry| entry.resolve(buffer))
@@ -314,14 +247,11 @@ impl sum_tree::Item for DiagnosticEntry<Anchor> {
impl DiagnosticEntry<Anchor> {
/// Converts the [DiagnosticEntry] to a different buffer coordinate type.
pub fn resolve<'a, O: FromAnchor>(
&'a self,
buffer: &text::BufferSnapshot,
) -> DiagnosticEntryRef<'a, O> {
DiagnosticEntryRef {
pub fn resolve<O: FromAnchor>(&self, buffer: &text::BufferSnapshot) -> DiagnosticEntry<O> {
DiagnosticEntry {
range: O::from_anchor(&self.range.start, buffer)
..O::from_anchor(&self.range.end, buffer),
diagnostic: &self.diagnostic,
diagnostic: self.diagnostic.clone(),
}
}
}

View File

@@ -75,7 +75,7 @@ use util::serde::default_true;
pub use buffer::Operation;
pub use buffer::*;
pub use diagnostic_set::{DiagnosticEntry, DiagnosticEntryRef, DiagnosticGroup};
pub use diagnostic_set::{DiagnosticEntry, DiagnosticGroup};
pub use language_registry::{
AvailableLanguage, BinaryStatus, LanguageNotFound, LanguageQueries, LanguageRegistry,
QUERY_FILENAME_PREFIXES,

View File

@@ -731,19 +731,15 @@ impl LanguageRegistry {
)
}
pub fn language_for_file_path(self: &Arc<Self>, path: &Path) -> Option<AvailableLanguage> {
self.language_for_file_internal(path, None, None)
}
pub fn load_language_for_file_path<'a>(
pub fn language_for_file_path<'a>(
self: &Arc<Self>,
path: &'a Path,
) -> impl Future<Output = Result<Arc<Language>>> + 'a {
let language = self.language_for_file_path(path);
let available_language = self.language_for_file_internal(path, None, None);
let this = self.clone();
async move {
if let Some(language) = language {
if let Some(language) = available_language {
this.load_language(&language).await?
} else {
Err(anyhow!(LanguageNotFound))

View File

@@ -151,8 +151,8 @@ impl LanguageModelProvider for AnthropicLanguageModelProvider {
fn recommended_models(&self, _cx: &App) -> Vec<Arc<dyn LanguageModel>> {
[
anthropic::Model::ClaudeSonnet4_5,
anthropic::Model::ClaudeSonnet4_5Thinking,
anthropic::Model::ClaudeSonnet4,
anthropic::Model::ClaudeSonnet4Thinking,
]
.into_iter()
.map(|model| self.create_language_model(model))

View File

@@ -1016,10 +1016,6 @@ mod tests {
name: "test case 2",
anotherStr: "bar",
},
{
name: "test case 3",
anotherStr: "baz",
},
}
notATableTest := []struct{
@@ -1068,22 +1064,21 @@ mod tests {
);
let go_test_count = tag_strings.iter().filter(|&tag| tag == "go-test").count();
// This is currently broken; see #39148
// let go_table_test_count = tag_strings
// .iter()
// .filter(|&tag| tag == "go-table-test-case")
// .count();
let go_table_test_count = tag_strings
.iter()
.filter(|&tag| tag == "go-table-test-case")
.count();
assert!(
go_test_count == 1,
"Should find exactly 1 go-test, found: {}",
go_test_count
);
// assert!(
// go_table_test_count == 3,
// "Should find exactly 3 go-table-test-case, found: {}",
// go_table_test_count
// );
assert!(
go_table_test_count == 2,
"Should find exactly 2 go-table-test-case, found: {}",
go_table_test_count
);
}
#[gpui::test]

View File

@@ -147,9 +147,9 @@
[
(
(identifier)
(identifier) @_loop_var_inner
(identifier) @_loop_var
)
(identifier) @_loop_var_outer
(identifier) @_loop_var
]
)
right: (identifier) @_range_var
@@ -159,7 +159,7 @@
(expression_statement
(call_expression
function: (selector_expression
operand: (identifier)
operand: (identifier) @_t_var
field: (field_identifier) @_run_method
(#eq? @_run_method "Run")
)
@@ -168,12 +168,12 @@
[
(selector_expression
operand: (identifier) @_tc_var
(#eq? @_tc_var @_loop_var_inner)
(#eq? @_tc_var @_loop_var)
field: (field_identifier) @_field_check
(#eq? @_field_check @_field_name)
)
(identifier) @_arg_var
(#eq? @_arg_var @_loop_var_outer)
(#eq? @_arg_var @_loop_var)
]
.
(func_literal

View File

@@ -154,16 +154,11 @@ impl LspAdapter for TyLspAdapter {
async fn workspace_configuration(
self: Arc<Self>,
delegate: &Arc<dyn LspAdapterDelegate>,
_: &Arc<dyn LspAdapterDelegate>,
toolchain: Option<Toolchain>,
cx: &mut AsyncApp,
_cx: &mut AsyncApp,
) -> Result<Value> {
let mut ret = cx
.update(|cx| {
language_server_settings(delegate.as_ref(), &self.name(), cx)
.and_then(|s| s.settings.clone())
})?
.unwrap_or_else(|| json!({}));
let mut ret = json!({});
if let Some(toolchain) = toolchain.and_then(|toolchain| {
serde_json::from_value::<PythonEnvironment>(toolchain.as_json).ok()
}) {
@@ -176,9 +171,10 @@ impl LspAdapter for TyLspAdapter {
"sysPrefix": sys_prefix
}
});
ret.as_object_mut()?
.entry("pythonExtension")
.or_insert_with(|| json!({ "activeEnvironment": environment }));
ret.as_object_mut()?.insert(
"pythonExtension".into(),
json!({ "activeEnvironment": environment }),
);
Some(())
});
}
@@ -467,6 +463,7 @@ impl LspAdapter for PyrightLspAdapter {
async fn workspace_configuration(
self: Arc<Self>,
adapter: &Arc<dyn LspAdapterDelegate>,
toolchain: Option<Toolchain>,
cx: &mut AsyncApp,

View File

@@ -335,7 +335,7 @@ impl Markdown {
for path in paths {
if let Ok(language) = registry
.load_language_for_file_path(Path::new(path.as_ref()))
.language_for_file_path(Path::new(path.as_ref()))
.await
{
languages_by_path.insert(path, language);

View File

@@ -826,33 +826,6 @@ impl<'a> MarkdownParser<'a> {
if let Some(image) = self.extract_image(source_range, attrs) {
elements.push(ParsedMarkdownElement::Image(image));
}
} else if matches!(
name.local,
local_name!("h1")
| local_name!("h2")
| local_name!("h3")
| local_name!("h4")
| local_name!("h5")
| local_name!("h6")
) {
let mut paragraph = MarkdownParagraph::new();
self.consume_paragraph(source_range.clone(), node, &mut paragraph);
if !paragraph.is_empty() {
elements.push(ParsedMarkdownElement::Heading(ParsedMarkdownHeading {
source_range,
level: match name.local {
local_name!("h1") => HeadingLevel::H1,
local_name!("h2") => HeadingLevel::H2,
local_name!("h3") => HeadingLevel::H3,
local_name!("h4") => HeadingLevel::H4,
local_name!("h5") => HeadingLevel::H5,
local_name!("h6") => HeadingLevel::H6,
_ => unreachable!(),
},
contents: paragraph,
}));
}
} else {
self.consume_children(source_range, node, elements);
}
@@ -861,40 +834,6 @@ impl<'a> MarkdownParser<'a> {
}
}
fn parse_paragraph(
&self,
source_range: Range<usize>,
node: &Rc<markup5ever_rcdom::Node>,
paragraph: &mut MarkdownParagraph,
) {
match &node.data {
markup5ever_rcdom::NodeData::Text { contents } => {
paragraph.push(MarkdownParagraphChunk::Text(ParsedMarkdownText {
source_range,
regions: Vec::default(),
contents: contents.borrow().to_string(),
region_ranges: Vec::default(),
highlights: Vec::default(),
}));
}
markup5ever_rcdom::NodeData::Element { .. } => {
self.consume_paragraph(source_range, node, paragraph);
}
_ => {}
}
}
fn consume_paragraph(
&self,
source_range: Range<usize>,
node: &Rc<markup5ever_rcdom::Node>,
paragraph: &mut MarkdownParagraph,
) {
for node in node.children.borrow().iter() {
self.parse_paragraph(source_range.clone(), node, paragraph);
}
}
fn consume_children(
&self,
source_range: Range<usize>,
@@ -1330,85 +1269,6 @@ mod tests {
);
}
#[gpui::test]
async fn test_html_heading_tags() {
let parsed = parse("<h1>Heading</h1><h2>Heading</h2><h3>Heading</h3><h4>Heading</h4><h5>Heading</h5><h6>Heading</h6>").await;
assert_eq!(
ParsedMarkdown {
children: vec![
ParsedMarkdownElement::Heading(ParsedMarkdownHeading {
level: HeadingLevel::H1,
source_range: 0..96,
contents: vec![MarkdownParagraphChunk::Text(ParsedMarkdownText {
source_range: 0..96,
contents: "Heading".into(),
highlights: Vec::default(),
region_ranges: Vec::default(),
regions: Vec::default()
})],
}),
ParsedMarkdownElement::Heading(ParsedMarkdownHeading {
level: HeadingLevel::H2,
source_range: 0..96,
contents: vec![MarkdownParagraphChunk::Text(ParsedMarkdownText {
source_range: 0..96,
contents: "Heading".into(),
highlights: Vec::default(),
region_ranges: Vec::default(),
regions: Vec::default()
})],
}),
ParsedMarkdownElement::Heading(ParsedMarkdownHeading {
level: HeadingLevel::H3,
source_range: 0..96,
contents: vec![MarkdownParagraphChunk::Text(ParsedMarkdownText {
source_range: 0..96,
contents: "Heading".into(),
highlights: Vec::default(),
region_ranges: Vec::default(),
regions: Vec::default()
})],
}),
ParsedMarkdownElement::Heading(ParsedMarkdownHeading {
level: HeadingLevel::H4,
source_range: 0..96,
contents: vec![MarkdownParagraphChunk::Text(ParsedMarkdownText {
source_range: 0..96,
contents: "Heading".into(),
highlights: Vec::default(),
region_ranges: Vec::default(),
regions: Vec::default()
})],
}),
ParsedMarkdownElement::Heading(ParsedMarkdownHeading {
level: HeadingLevel::H5,
source_range: 0..96,
contents: vec![MarkdownParagraphChunk::Text(ParsedMarkdownText {
source_range: 0..96,
contents: "Heading".into(),
highlights: Vec::default(),
region_ranges: Vec::default(),
regions: Vec::default()
})],
}),
ParsedMarkdownElement::Heading(ParsedMarkdownHeading {
level: HeadingLevel::H6,
source_range: 0..96,
contents: vec![MarkdownParagraphChunk::Text(ParsedMarkdownText {
source_range: 0..96,
contents: "Heading".into(),
highlights: Vec::default(),
region_ranges: Vec::default(),
regions: Vec::default()
})],
}),
],
},
parsed
);
}
#[gpui::test]
async fn test_html_image_tag() {
let parsed = parse("<img src=\"http://example.com/foo.png\" />").await;

View File

@@ -839,8 +839,8 @@ impl InteractiveMarkdownElementTooltip {
}
impl Render for InteractiveMarkdownElementTooltip {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
tooltip_container(cx, |el, _| {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
tooltip_container(window, cx, |el, _, _| {
let secondary_modifier = Keystroke {
modifiers: Modifiers::secondary_key(),
..Default::default()

View File

@@ -16,13 +16,6 @@ pub struct Anchor {
}
impl Anchor {
pub fn with_diff_base_anchor(self, diff_base_anchor: text::Anchor) -> Self {
Self {
diff_base_anchor: Some(diff_base_anchor),
..self
}
}
pub fn in_buffer(
excerpt_id: ExcerptId,
buffer_id: BufferId,

View File

@@ -17,7 +17,7 @@ use gpui::{App, AppContext as _, Context, Entity, EntityId, EventEmitter, Task};
use itertools::Itertools;
use language::{
AutoindentMode, Buffer, BufferChunks, BufferRow, BufferSnapshot, Capability, CharClassifier,
CharKind, CharScopeContext, Chunk, CursorShape, DiagnosticEntryRef, DiskState, File,
CharKind, CharScopeContext, Chunk, CursorShape, DiagnosticEntry, DiskState, File,
IndentGuideSettings, IndentSize, Language, LanguageScope, OffsetRangeExt, OffsetUtf16, Outline,
OutlineItem, Point, PointUtf16, Selection, TextDimension, TextObject, ToOffset as _,
ToPoint as _, TransactionId, TreeSitterOptions, Unclipped,
@@ -79,6 +79,12 @@ pub struct MultiBuffer {
buffer_changed_since_sync: Rc<Cell<bool>>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum MultiOrSingleBufferOffsetRange {
Single(Range<usize>),
Multi(Range<usize>),
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Event {
ExcerptsAdded {
@@ -1310,9 +1316,11 @@ impl MultiBuffer {
let end_locator = snapshot.excerpt_locator_for_id(selection.end.excerpt_id);
cursor.seek(&Some(start_locator), Bias::Left);
while let Some(excerpt) = cursor.item()
&& excerpt.locator <= *end_locator
{
while let Some(excerpt) = cursor.item() {
if excerpt.locator > *end_locator {
break;
}
let mut start = excerpt.range.context.start;
let mut end = excerpt.range.context.end;
if excerpt.id == selection.start.excerpt_id {
@@ -4791,6 +4799,7 @@ impl MultiBufferSnapshot {
where
D: TextDimension,
{
// let mut range = range.start..range.end;
let mut summary = D::zero(());
let mut cursor = self.excerpts.cursor::<ExcerptOffset>(());
cursor.seek(&range.start, Bias::Right);
@@ -4914,13 +4923,13 @@ impl MultiBufferSnapshot {
let locator = self.excerpt_locator_for_id(anchor.excerpt_id);
cursor.seek(&Some(locator), Bias::Left);
if cursor.item().is_none() && anchor.excerpt_id == ExcerptId::max() {
cursor.prev();
if cursor.item().is_none() {
cursor.next();
}
let mut position = cursor.start().1;
if let Some(excerpt) = cursor.item()
&& (excerpt.id == anchor.excerpt_id || anchor.excerpt_id == ExcerptId::max())
&& excerpt.id == anchor.excerpt_id
{
let excerpt_buffer_start = excerpt
.buffer
@@ -4959,20 +4968,24 @@ impl MultiBufferSnapshot {
let mut summaries = Vec::new();
while let Some(anchor) = anchors.peek() {
let excerpt_id = self.latest_excerpt_id(anchor.excerpt_id);
let excerpt_anchors = anchors.peeking_take_while(|anchor| {
self.latest_excerpt_id(anchor.excerpt_id) == excerpt_id
let excerpt_anchors = iter::from_fn(|| {
let anchor = anchors.peek()?;
if self.latest_excerpt_id(anchor.excerpt_id) == excerpt_id {
Some(anchors.next().unwrap())
} else {
None
}
});
let locator = self.excerpt_locator_for_id(excerpt_id);
cursor.seek_forward(locator, Bias::Left);
if cursor.item().is_none() && excerpt_id == ExcerptId::max() {
cursor.prev();
if cursor.item().is_none() {
cursor.next();
}
let excerpt_start_position = D::from_text_summary(&cursor.start().text);
if let Some(excerpt) = cursor.item() {
if excerpt.id != excerpt_id && excerpt_id != ExcerptId::max() {
if excerpt.id != excerpt_id {
let position = self.resolve_summary_for_anchor(
&Anchor::min(),
excerpt_start_position,
@@ -5079,14 +5092,20 @@ impl MultiBufferSnapshot {
let old_locator = self.excerpt_locator_for_id(old_excerpt_id);
cursor.seek_forward(&Some(old_locator), Bias::Left);
if cursor.item().is_none() {
cursor.next();
}
let next_excerpt = cursor.item();
let prev_excerpt = cursor.prev_item();
// Process all of the anchors for this excerpt.
while let Some((anchor_ix, &anchor)) =
anchors.next_if(|(_, anchor)| anchor.excerpt_id == old_excerpt_id)
{
let mut anchor = anchor;
while let Some((_, anchor)) = anchors.peek() {
if anchor.excerpt_id != old_excerpt_id {
break;
}
let (anchor_ix, anchor) = anchors.next().unwrap();
let mut anchor = *anchor;
// Leave min and max anchors unchanged if invalid or
// if the old excerpt still exists at this location
@@ -5122,7 +5141,12 @@ impl MultiBufferSnapshot {
{
text_anchor = excerpt.range.context.end;
}
Anchor::in_buffer(excerpt.id, excerpt.buffer_id, text_anchor)
Anchor {
buffer_id: Some(excerpt.buffer_id),
excerpt_id: excerpt.id,
text_anchor,
diff_base_anchor: None,
}
} else if let Some(excerpt) = prev_excerpt {
let mut text_anchor = excerpt
.range
@@ -5135,7 +5159,12 @@ impl MultiBufferSnapshot {
{
text_anchor = excerpt.range.context.start;
}
Anchor::in_buffer(excerpt.id, excerpt.buffer_id, text_anchor)
Anchor {
buffer_id: Some(excerpt.buffer_id),
excerpt_id: excerpt.id,
text_anchor,
diff_base_anchor: None,
}
} else if anchor.text_anchor.bias == Bias::Left {
Anchor::min()
} else {
@@ -5217,15 +5246,24 @@ impl MultiBufferSnapshot {
let buffer_start = excerpt.range.context.start.to_offset(&excerpt.buffer);
let text_anchor =
excerpt.clip_anchor(excerpt.buffer.anchor_at(buffer_start + overshoot, bias));
let anchor = Anchor::in_buffer(excerpt.id, excerpt.buffer_id, text_anchor);
match diff_base_anchor {
Some(diff_base_anchor) => anchor.with_diff_base_anchor(diff_base_anchor),
None => anchor,
Anchor {
buffer_id: Some(excerpt.buffer_id),
excerpt_id: excerpt.id,
text_anchor,
diff_base_anchor,
}
} else if excerpt_offset.is_zero() && bias == Bias::Left {
Anchor::min()
} else {
Anchor::max()
let mut anchor = if excerpt_offset.is_zero() && bias == Bias::Left {
Anchor::min()
} else {
Anchor::max()
};
// TODO this is a hack, because all APIs should be able to handle ExcerptId::min and max.
if let Some((excerpt_id, _, _)) = self.as_singleton() {
anchor.excerpt_id = *excerpt_id;
}
anchor
}
}
@@ -5237,12 +5275,22 @@ impl MultiBufferSnapshot {
text_anchor: text::Anchor,
) -> Option<Anchor> {
let excerpt_id = self.latest_excerpt_id(excerpt_id);
let excerpt = self.excerpt(excerpt_id)?;
Some(Anchor::in_buffer(
excerpt_id,
excerpt.buffer_id,
text_anchor,
))
let locator = self.excerpt_locator_for_id(excerpt_id);
let mut cursor = self.excerpts.cursor::<Option<&Locator>>(());
cursor.seek(locator, Bias::Left);
if let Some(excerpt) = cursor.item()
&& excerpt.id == excerpt_id
{
let text_anchor = excerpt.clip_anchor(text_anchor);
drop(cursor);
return Some(Anchor {
buffer_id: Some(excerpt.buffer_id),
excerpt_id,
text_anchor,
diff_base_anchor: None,
});
}
None
}
pub fn context_range_for_excerpt(&self, excerpt_id: ExcerptId) -> Option<Range<text::Anchor>> {
@@ -5278,8 +5326,8 @@ impl MultiBufferSnapshot {
}
}
pub fn excerpt_before(&self, excerpt_id: ExcerptId) -> Option<MultiBufferExcerpt<'_>> {
let start_locator = self.excerpt_locator_for_id(excerpt_id);
pub fn excerpt_before(&self, id: ExcerptId) -> Option<MultiBufferExcerpt<'_>> {
let start_locator = self.excerpt_locator_for_id(id);
let mut excerpts = self
.excerpts
.cursor::<Dimensions<Option<&Locator>, ExcerptDimension<usize>>>(());
@@ -5965,7 +6013,7 @@ impl MultiBufferSnapshot {
&self,
buffer_id: BufferId,
group_id: usize,
) -> impl Iterator<Item = DiagnosticEntryRef<'_, Point>> + '_ {
) -> impl Iterator<Item = DiagnosticEntry<Point>> + '_ {
self.lift_buffer_metadata(Point::zero()..self.max_point(), move |buffer, range| {
if buffer.remote_id() != buffer_id {
return None;
@@ -5974,16 +6022,16 @@ impl MultiBufferSnapshot {
buffer
.diagnostics_in_range(range, false)
.filter(move |diagnostic| diagnostic.diagnostic.group_id == group_id)
.map(move |DiagnosticEntryRef { diagnostic, range }| (range, diagnostic)),
.map(move |DiagnosticEntry { diagnostic, range }| (range, diagnostic)),
)
})
.map(|(range, diagnostic, _)| DiagnosticEntryRef { diagnostic, range })
.map(|(range, diagnostic, _)| DiagnosticEntry { diagnostic, range })
}
pub fn diagnostics_in_range<'a, T>(
&'a self,
range: Range<T>,
) -> impl Iterator<Item = DiagnosticEntryRef<'a, T>> + 'a
) -> impl Iterator<Item = DiagnosticEntry<T>> + 'a
where
T: 'a
+ text::ToOffset
@@ -6000,13 +6048,13 @@ impl MultiBufferSnapshot {
.map(|entry| (entry.range, entry.diagnostic)),
)
})
.map(|(range, diagnostic, _)| DiagnosticEntryRef { diagnostic, range })
.map(|(range, diagnostic, _)| DiagnosticEntry { diagnostic, range })
}
pub fn diagnostics_with_buffer_ids_in_range<'a, T>(
&'a self,
range: Range<T>,
) -> impl Iterator<Item = (BufferId, DiagnosticEntryRef<'a, T>)> + 'a
) -> impl Iterator<Item = (BufferId, DiagnosticEntry<T>)> + 'a
where
T: 'a
+ text::ToOffset
@@ -6023,23 +6071,25 @@ impl MultiBufferSnapshot {
.map(|entry| (entry.range, entry.diagnostic)),
)
})
.map(|(range, diagnostic, b)| (b.buffer_id, DiagnosticEntryRef { diagnostic, range }))
.map(|(range, diagnostic, b)| (b.buffer_id, DiagnosticEntry { diagnostic, range }))
}
pub fn syntax_ancestor<T: ToOffset>(
&self,
range: Range<T>,
) -> Option<(tree_sitter::Node<'_>, Range<usize>)> {
) -> Option<(tree_sitter::Node<'_>, MultiOrSingleBufferOffsetRange)> {
let range = range.start.to_offset(self)..range.end.to_offset(self);
let mut excerpt = self.excerpt_containing(range.clone())?;
let node = excerpt
.buffer()
.syntax_ancestor(excerpt.map_range_to_buffer(range))?;
let node_range = node.byte_range();
if !excerpt.contains_buffer_range(node_range.clone()) {
return None;
let range = if excerpt.contains_buffer_range(node_range.clone()) {
MultiOrSingleBufferOffsetRange::Multi(excerpt.map_range_from_buffer(node_range))
} else {
MultiOrSingleBufferOffsetRange::Single(node_range)
};
Some((node, excerpt.map_range_from_buffer(node_range)))
Some((node, range))
}
pub fn syntax_next_sibling<T: ToOffset>(
@@ -6198,14 +6248,7 @@ impl MultiBufferSnapshot {
.excerpts
.cursor::<Dimensions<Option<&Locator>, ExcerptDimension<Point>>>(());
let locator = self.excerpt_locator_for_id(excerpt_id);
let mut sought_exact = cursor.seek(&Some(locator), Bias::Left);
if cursor.item().is_none() && excerpt_id == ExcerptId::max() {
sought_exact = true;
cursor.prev();
} else if excerpt_id == ExcerptId::min() {
sought_exact = true;
}
if sought_exact {
if cursor.seek(&Some(locator), Bias::Left) {
let start = cursor.start().1.clone();
let end = cursor.end().1;
let mut diff_transforms = self
@@ -6223,6 +6266,17 @@ impl MultiBufferSnapshot {
}
}
pub fn buffer_range_for_excerpt(&self, excerpt_id: ExcerptId) -> Option<Range<text::Anchor>> {
let mut cursor = self.excerpts.cursor::<Option<&Locator>>(());
let locator = self.excerpt_locator_for_id(excerpt_id);
if cursor.seek(&Some(locator), Bias::Left)
&& let Some(excerpt) = cursor.item()
{
return Some(excerpt.range.context.clone());
}
None
}
fn excerpt(&self, excerpt_id: ExcerptId) -> Option<&Excerpt> {
let mut cursor = self.excerpts.cursor::<Option<&Locator>>(());
let locator = self.excerpt_locator_for_id(excerpt_id);
@@ -6231,9 +6285,6 @@ impl MultiBufferSnapshot {
&& excerpt.id == excerpt_id
{
return Some(excerpt);
} else if cursor.item().is_none() && excerpt_id == ExcerptId::max() {
cursor.prev();
return cursor.item();
}
None
}
@@ -6302,10 +6353,18 @@ impl MultiBufferSnapshot {
.selections_in_range(query_range, include_local)
.flat_map(move |(replica_id, line_mode, cursor_shape, selections)| {
selections.map(move |selection| {
let mut start =
Anchor::in_buffer(excerpt.id, excerpt.buffer_id, selection.start);
let mut end =
Anchor::in_buffer(excerpt.id, excerpt.buffer_id, selection.end);
let mut start = Anchor {
buffer_id: Some(excerpt.buffer_id),
excerpt_id: excerpt.id,
text_anchor: selection.start,
diff_base_anchor: None,
};
let mut end = Anchor {
buffer_id: Some(excerpt.buffer_id),
excerpt_id: excerpt.id,
text_anchor: selection.end,
diff_base_anchor: None,
};
if range.start.cmp(&start, self).is_gt() {
start = range.start;
}
@@ -7022,19 +7081,21 @@ impl<'a> MultiBufferExcerpt<'a> {
}
pub fn start_anchor(&self) -> Anchor {
Anchor::in_buffer(
self.excerpt.id,
self.excerpt.buffer_id,
self.excerpt.range.context.start,
)
Anchor {
buffer_id: Some(self.excerpt.buffer_id),
excerpt_id: self.excerpt.id,
text_anchor: self.excerpt.range.context.start,
diff_base_anchor: None,
}
}
pub fn end_anchor(&self) -> Anchor {
Anchor::in_buffer(
self.excerpt.id,
self.excerpt.buffer_id,
self.excerpt.range.context.end,
)
Anchor {
buffer_id: Some(self.excerpt.buffer_id),
excerpt_id: self.excerpt.id,
text_anchor: self.excerpt.range.context.end,
diff_base_anchor: None,
}
}
pub fn buffer(&self) -> &'a BufferSnapshot {

View File

@@ -401,10 +401,10 @@ impl AiPrivacyTooltip {
}
impl Render for AiPrivacyTooltip {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
const DESCRIPTION: &str = "We believe in opt-in data sharing as the default for building AI products, rather than opt-out. We'll only use or store your data if you affirmatively send it to us. ";
tooltip_container(cx, move |this, _| {
tooltip_container(window, cx, move |this, _, _| {
this.child(
h_flex()
.gap_1()

View File

@@ -333,7 +333,7 @@ fn render_base_keymap_section(tab_index: &mut isize, cx: &mut App) -> impl IntoE
ToggleButtonWithIcon::new("VS Code", IconName::EditorVsCode, |_, _, cx| {
write_keymap_base(BaseKeymap::VSCode, cx);
}),
ToggleButtonWithIcon::new("JetBrains", IconName::EditorJetBrains, |_, _, cx| {
ToggleButtonWithIcon::new("Jetbrains", IconName::EditorJetBrains, |_, _, cx| {
write_keymap_base(BaseKeymap::JetBrains, cx);
}),
ToggleButtonWithIcon::new("Sublime Text", IconName::EditorSublime, |_, _, cx| {

Some files were not shown because too many files have changed in this diff Show More