mirror of
https://github.com/zed-industries/zed.git
synced 2026-06-01 03:14:56 +07:00
Merge branch 'main' into info_level_diagnostics_counter
This commit is contained in:
commit
98fa733543
121 changed files with 1868 additions and 6164 deletions
4
.github/workflows/bump_zed_version.yml
vendored
4
.github/workflows/bump_zed_version.yml
vendored
|
|
@ -160,7 +160,7 @@ jobs:
|
||||||
name: steps::bot_commit
|
name: steps::bot_commit
|
||||||
uses: IAreKyleW00t/verified-bot-commit@126a6a11889ab05bcff72ec2403c326cd249b84c
|
uses: IAreKyleW00t/verified-bot-commit@126a6a11889ab05bcff72ec2403c326cd249b84c
|
||||||
with:
|
with:
|
||||||
message: ${{ needs.resolve_versions.outputs.preview_branch }} preview for @${{ github.actor }}
|
message: ${{ needs.resolve_versions.outputs.preview_branch }} preview
|
||||||
ref: refs/heads/${{ needs.resolve_versions.outputs.preview_branch }}
|
ref: refs/heads/${{ needs.resolve_versions.outputs.preview_branch }}
|
||||||
files: crates/zed/RELEASE_CHANNEL
|
files: crates/zed/RELEASE_CHANNEL
|
||||||
token: ${{ steps.generate-token.outputs.token }}
|
token: ${{ steps.generate-token.outputs.token }}
|
||||||
|
|
@ -206,7 +206,7 @@ jobs:
|
||||||
name: steps::bot_commit
|
name: steps::bot_commit
|
||||||
uses: IAreKyleW00t/verified-bot-commit@126a6a11889ab05bcff72ec2403c326cd249b84c
|
uses: IAreKyleW00t/verified-bot-commit@126a6a11889ab05bcff72ec2403c326cd249b84c
|
||||||
with:
|
with:
|
||||||
message: ${{ needs.resolve_versions.outputs.stable_branch }} stable for @${{ github.actor }}
|
message: ${{ needs.resolve_versions.outputs.stable_branch }} stable
|
||||||
ref: refs/heads/${{ needs.resolve_versions.outputs.stable_branch }}
|
ref: refs/heads/${{ needs.resolve_versions.outputs.stable_branch }}
|
||||||
files: crates/zed/RELEASE_CHANNEL
|
files: crates/zed/RELEASE_CHANNEL
|
||||||
token: ${{ steps.generate-token.outputs.token }}
|
token: ${{ steps.generate-token.outputs.token }}
|
||||||
|
|
|
||||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
|
|
@ -241,7 +241,7 @@ jobs:
|
||||||
timeout-minutes: 60
|
timeout-minutes: 60
|
||||||
check_scripts:
|
check_scripts:
|
||||||
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions')
|
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions')
|
||||||
runs-on: namespace-profile-8x16-ubuntu-2204
|
runs-on: namespace-profile-2x4-ubuntu-2404
|
||||||
steps:
|
steps:
|
||||||
- name: steps::checkout_repo
|
- name: steps::checkout_repo
|
||||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
|
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
|
||||||
|
|
|
||||||
2
.github/workflows/run_tests.yml
vendored
2
.github/workflows/run_tests.yml
vendored
|
|
@ -705,7 +705,7 @@ jobs:
|
||||||
needs:
|
needs:
|
||||||
- orchestrate
|
- orchestrate
|
||||||
if: needs.orchestrate.outputs.run_action_checks == 'true'
|
if: needs.orchestrate.outputs.run_action_checks == 'true'
|
||||||
runs-on: namespace-profile-8x16-ubuntu-2204
|
runs-on: namespace-profile-2x4-ubuntu-2404
|
||||||
steps:
|
steps:
|
||||||
- name: steps::checkout_repo
|
- name: steps::checkout_repo
|
||||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
|
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
|
||||||
|
|
|
||||||
8
Cargo.lock
generated
8
Cargo.lock
generated
|
|
@ -1293,12 +1293,10 @@ dependencies = [
|
||||||
"gpui",
|
"gpui",
|
||||||
"markdown_preview",
|
"markdown_preview",
|
||||||
"notifications",
|
"notifications",
|
||||||
"project",
|
|
||||||
"release_channel",
|
"release_channel",
|
||||||
"semver",
|
"semver",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"settings",
|
|
||||||
"smol",
|
"smol",
|
||||||
"telemetry",
|
"telemetry",
|
||||||
"ui",
|
"ui",
|
||||||
|
|
@ -6308,7 +6306,6 @@ dependencies = [
|
||||||
"theme",
|
"theme",
|
||||||
"theme_settings",
|
"theme_settings",
|
||||||
"ui",
|
"ui",
|
||||||
"ui_input",
|
|
||||||
"util",
|
"util",
|
||||||
"workspace",
|
"workspace",
|
||||||
"zed_actions",
|
"zed_actions",
|
||||||
|
|
@ -6609,7 +6606,6 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"smol",
|
"smol",
|
||||||
"telemetry",
|
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"text",
|
"text",
|
||||||
"thiserror 2.0.17",
|
"thiserror 2.0.17",
|
||||||
|
|
@ -7409,7 +7405,6 @@ dependencies = [
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"smol",
|
"smol",
|
||||||
"strum 0.27.2",
|
"strum 0.27.2",
|
||||||
"task",
|
|
||||||
"telemetry",
|
"telemetry",
|
||||||
"theme",
|
"theme",
|
||||||
"theme_settings",
|
"theme_settings",
|
||||||
|
|
@ -10795,9 +10790,11 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
||||||
name = "miniprofiler_ui"
|
name = "miniprofiler_ui"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"command_palette_hooks",
|
||||||
"gpui",
|
"gpui",
|
||||||
"rpc",
|
"rpc",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"settings",
|
||||||
"smol",
|
"smol",
|
||||||
"theme_settings",
|
"theme_settings",
|
||||||
"util",
|
"util",
|
||||||
|
|
@ -17771,7 +17768,6 @@ dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"log",
|
"log",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
"percent-encoding",
|
|
||||||
"rand 0.9.3",
|
"rand 0.9.3",
|
||||||
"regex",
|
"regex",
|
||||||
"release_channel",
|
"release_channel",
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path d="M11.8644 1.04416C12.2744 1.04416 12.6675 1.20702 12.9574 1.4969C13.2473 1.78679 13.4101 2.17996 13.4101 2.5899V14.183C13.4101 14.3184 13.3744 14.4513 13.3069 14.5685C13.2393 14.6859 13.1422 14.7833 13.0251 14.8513C12.9081 14.9192 12.7752 14.9551 12.6399 14.9556C12.5045 14.956 12.3715 14.921 12.2539 14.8538L8.76673 12.8614C8.53321 12.728 8.26896 12.6578 8.00004 12.6578C7.73111 12.6578 7.46686 12.728 7.23334 12.8614L3.74615 14.8538C3.62863 14.921 3.49553 14.956 3.36019 14.9556C3.22485 14.9551 3.092 14.9192 2.97493 14.8513C2.85787 14.7833 2.76069 14.6859 2.69312 14.5685C2.62556 14.4513 2.58997 14.3184 2.58994 14.183V2.5899C2.58994 2.17996 2.75278 1.78679 3.04267 1.4969C3.33255 1.20702 3.72572 1.04416 4.13568 1.04416H11.8644Z" fill="#C6CAD0"/>
|
<path d="M11.0885 2.44067C11.4162 2.44067 11.7304 2.57083 11.9621 2.80251C12.1938 3.0342 12.3239 3.34843 12.3239 3.67607V12.9416C12.3239 13.0498 12.2954 13.156 12.2414 13.2497C12.1874 13.3435 12.1098 13.4214 12.0162 13.4757C11.9227 13.53 11.8165 13.5587 11.7083 13.5591C11.6001 13.5594 11.4938 13.5314 11.3998 13.4777L8.61278 11.8853C8.42615 11.7787 8.21495 11.7226 8.00002 11.7226C7.78509 11.7226 7.57389 11.7787 7.38726 11.8853L4.6002 13.4777C4.50627 13.5314 4.3999 13.5594 4.29173 13.5591C4.18356 13.5587 4.07738 13.53 3.98382 13.4757C3.89026 13.4214 3.81259 13.3435 3.75859 13.2497C3.70459 13.156 3.67615 13.0498 3.67612 12.9416V3.67607C3.67612 3.34843 3.80627 3.0342 4.03796 2.80251C4.26964 2.57083 4.58387 2.44067 4.91152 2.44067H11.0885Z" stroke="#C6CAD0" stroke-width="1.11186" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path opacity="0.12" d="M11.0885 2.44067C11.4162 2.44067 11.7304 2.57083 11.9621 2.80251C12.1938 3.0342 12.3239 3.34843 12.3239 3.67607V12.9416C12.3239 13.0498 12.2954 13.156 12.2414 13.2497C12.1874 13.3435 12.1098 13.4214 12.0162 13.4757C11.9227 13.53 11.8165 13.5587 11.7083 13.5591C11.6001 13.5594 11.4938 13.5314 11.3998 13.4777L8.61278 11.8853C8.42615 11.7787 8.21495 11.7226 8.00002 11.7226C7.78509 11.7226 7.57389 11.7787 7.38726 11.8853L4.6002 13.4777C4.50627 13.5314 4.3999 13.5594 4.29173 13.5591C4.18356 13.5587 4.07738 13.53 3.98382 13.4757C3.89026 13.4214 3.81259 13.3435 3.75859 13.2497C3.70459 13.156 3.67615 13.0498 3.67612 12.9416V3.67607C3.67612 3.34843 3.80627 3.0342 4.03796 2.80251C4.26964 2.57083 4.58387 2.44067 4.91152 2.44067H11.0885Z" fill="#C6CAD0" stroke="#C6CAD0" stroke-width="1.11186" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 862 B After Width: | Height: | Size: 1.8 KiB |
|
|
@ -507,7 +507,6 @@
|
||||||
"shift-l": "pane::ActivateNextItem", // not a helix default
|
"shift-l": "pane::ActivateNextItem", // not a helix default
|
||||||
"g p": "pane::ActivatePreviousItem",
|
"g p": "pane::ActivatePreviousItem",
|
||||||
"shift-h": "pane::ActivatePreviousItem", // not a helix default
|
"shift-h": "pane::ActivatePreviousItem", // not a helix default
|
||||||
"g w": "vim::HelixJumpToWord",
|
|
||||||
"g .": "vim::HelixGotoLastModification",
|
"g .": "vim::HelixGotoLastModification",
|
||||||
"g o": "editor::ToggleSelectedDiffHunks", // Zed specific
|
"g o": "editor::ToggleSelectedDiffHunks", // Zed specific
|
||||||
"g shift-o": "git::ToggleStaged", // Zed specific
|
"g shift-o": "git::ToggleStaged", // Zed specific
|
||||||
|
|
@ -546,6 +545,7 @@
|
||||||
"]": ["vim::PushHelixNext", { "around": true }],
|
"]": ["vim::PushHelixNext", { "around": true }],
|
||||||
"[": ["vim::PushHelixPrevious", { "around": true }],
|
"[": ["vim::PushHelixPrevious", { "around": true }],
|
||||||
"g q": "vim::PushRewrap",
|
"g q": "vim::PushRewrap",
|
||||||
|
"g w": "vim::PushRewrap", // not a helix default & clashes with helix `goto_word`
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -975,11 +975,6 @@
|
||||||
//
|
//
|
||||||
// Default: true
|
// Default: true
|
||||||
"diff_stats": true,
|
"diff_stats": true,
|
||||||
// Maximum length of the commit message title before a warning is shown.
|
|
||||||
// Set to 0 to disable.
|
|
||||||
//
|
|
||||||
// Default: 72
|
|
||||||
"commit_title_max_length": 72,
|
|
||||||
},
|
},
|
||||||
"message_editor": {
|
"message_editor": {
|
||||||
// Whether to automatically replace emoji shortcodes with emoji characters.
|
// Whether to automatically replace emoji shortcodes with emoji characters.
|
||||||
|
|
@ -1691,14 +1686,6 @@
|
||||||
"prompt_format": "infer",
|
"prompt_format": "infer",
|
||||||
"max_output_tokens": 64,
|
"max_output_tokens": 64,
|
||||||
},
|
},
|
||||||
// Controls whether Zed may collect training data when using Zed's Edit Predictions.
|
|
||||||
// Data is only captured when the project is detected as open source.
|
|
||||||
// Possible values:
|
|
||||||
// - "default": use the preference previously set via the status-bar toggle,
|
|
||||||
// or false if no preference has been stored.
|
|
||||||
// - "yes": allow data collection for files in open-source projects.
|
|
||||||
// - "no": never allow data collection.
|
|
||||||
"allow_data_collection": "default",
|
|
||||||
},
|
},
|
||||||
// Settings specific to journaling
|
// Settings specific to journaling
|
||||||
"journal": {
|
"journal": {
|
||||||
|
|
@ -2526,6 +2513,11 @@
|
||||||
// Mostly useful for developers who are managing multiple instances of Zed.
|
// Mostly useful for developers who are managing multiple instances of Zed.
|
||||||
"nightly": {
|
"nightly": {
|
||||||
// "theme": "Andromeda"
|
// "theme": "Andromeda"
|
||||||
|
"instrumentation": {
|
||||||
|
"performance_profiler": {
|
||||||
|
"enabled": true,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
// Settings overrides to use when using Zed Stable.
|
// Settings overrides to use when using Zed Stable.
|
||||||
// Mostly useful for developers who are managing multiple instances of Zed.
|
// Mostly useful for developers who are managing multiple instances of Zed.
|
||||||
|
|
@ -2536,6 +2528,11 @@
|
||||||
// Mostly useful for developers who are managing multiple instances of Zed.
|
// Mostly useful for developers who are managing multiple instances of Zed.
|
||||||
"dev": {
|
"dev": {
|
||||||
// "theme": "Andromeda"
|
// "theme": "Andromeda"
|
||||||
|
"instrumentation": {
|
||||||
|
"performance_profiler": {
|
||||||
|
"enabled": true,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
// Settings overrides to use when using Linux.
|
// Settings overrides to use when using Linux.
|
||||||
"linux": {},
|
"linux": {},
|
||||||
|
|
@ -2655,4 +2652,16 @@
|
||||||
//
|
//
|
||||||
// Example: {"log": {"client": "warn"}}
|
// Example: {"log": {"client": "warn"}}
|
||||||
"log": {},
|
"log": {},
|
||||||
|
|
||||||
|
// Configuration for developer-oriented instrumentation tools that can be
|
||||||
|
// toggled at runtime.
|
||||||
|
"instrumentation": {
|
||||||
|
// Performance profiler, accessed via the `zed: open performance profiler`
|
||||||
|
// action. Collects timing data for foreground and background executor
|
||||||
|
// tasks. Enabling this may lead to increased memory usage, hence it's
|
||||||
|
// disabled by default for regular builds.
|
||||||
|
"performance_profiler": {
|
||||||
|
"enabled": false,
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2125,7 +2125,7 @@ impl AcpThread {
|
||||||
let curr_status = mem::replace(&mut call.status, new_status);
|
let curr_status = mem::replace(&mut call.status, new_status);
|
||||||
|
|
||||||
if let ToolCallStatus::WaitingForConfirmation { respond_tx, .. } = curr_status {
|
if let ToolCallStatus::WaitingForConfirmation { respond_tx, .. } = curr_status {
|
||||||
respond_tx.send(outcome).ok();
|
respond_tx.send(outcome).log_err();
|
||||||
} else if cfg!(debug_assertions) {
|
} else if cfg!(debug_assertions) {
|
||||||
panic!("tried to authorize an already authorized tool call");
|
panic!("tried to authorize an already authorized tool call");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -96,18 +96,13 @@ impl MentionUri {
|
||||||
let path = url.path();
|
let path = url.path();
|
||||||
match url.scheme() {
|
match url.scheme() {
|
||||||
"file" => {
|
"file" => {
|
||||||
let trimmed = if path_style.is_windows() {
|
let normalized = if path_style.is_windows() {
|
||||||
path.trim_start_matches("/")
|
path.trim_start_matches("/")
|
||||||
} else {
|
} else {
|
||||||
path
|
path
|
||||||
};
|
};
|
||||||
let decoded = decode(trimmed).unwrap_or(Cow::Borrowed(trimmed));
|
let decoded = decode(normalized).unwrap_or(Cow::Borrowed(normalized));
|
||||||
let normalized: Cow<str> = if path_style.is_windows() {
|
let path = decoded.as_ref();
|
||||||
Cow::Owned(decoded.replace('/', "\\"))
|
|
||||||
} else {
|
|
||||||
decoded
|
|
||||||
};
|
|
||||||
let path = normalized.as_ref();
|
|
||||||
|
|
||||||
if let Some(fragment) = url.fragment() {
|
if let Some(fragment) = url.fragment() {
|
||||||
let line_range = parse_line_range(fragment).log_err().unwrap_or(1..=1);
|
let line_range = parse_line_range(fragment).log_err().unwrap_or(1..=1);
|
||||||
|
|
@ -498,49 +493,6 @@ mod tests {
|
||||||
assert_eq!(parsed.to_uri().to_string(), file_uri);
|
assert_eq!(parsed.to_uri().to_string(), file_uri);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_file_uris_use_native_separators_on_windows() {
|
|
||||||
let parsed = MentionUri::parse("file:///C:/path/to/file.rs", PathStyle::Windows).unwrap();
|
|
||||||
match parsed {
|
|
||||||
MentionUri::File { abs_path } => {
|
|
||||||
assert_eq!(abs_path, PathBuf::from("C:\\path\\to\\file.rs"));
|
|
||||||
}
|
|
||||||
other => panic!("Expected File variant, got {other:?}"),
|
|
||||||
}
|
|
||||||
|
|
||||||
let parsed = MentionUri::parse("file:///C:/path/to/dir/", PathStyle::Windows).unwrap();
|
|
||||||
match parsed {
|
|
||||||
MentionUri::Directory { abs_path } => {
|
|
||||||
assert_eq!(abs_path, PathBuf::from("C:\\path\\to\\dir\\"));
|
|
||||||
}
|
|
||||||
other => panic!("Expected Directory variant, got {other:?}"),
|
|
||||||
}
|
|
||||||
|
|
||||||
let parsed = MentionUri::parse(
|
|
||||||
"file:///C:/path/to/file.rs?symbol=MySymbol#L10:20",
|
|
||||||
PathStyle::Windows,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
match parsed {
|
|
||||||
MentionUri::Symbol { abs_path, .. } => {
|
|
||||||
assert_eq!(abs_path, PathBuf::from("C:\\path\\to\\file.rs"));
|
|
||||||
}
|
|
||||||
other => panic!("Expected Symbol variant, got {other:?}"),
|
|
||||||
}
|
|
||||||
|
|
||||||
let parsed =
|
|
||||||
MentionUri::parse("file:///C:/path/to/file.rs#L5:15", PathStyle::Windows).unwrap();
|
|
||||||
match parsed {
|
|
||||||
MentionUri::Selection {
|
|
||||||
abs_path: Some(abs_path),
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
assert_eq!(abs_path, PathBuf::from("C:\\path\\to\\file.rs"));
|
|
||||||
}
|
|
||||||
other => panic!("Expected Selection variant, got {other:?}"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_to_directory_uri_without_slash() {
|
fn test_to_directory_uri_without_slash() {
|
||||||
let uri = MentionUri::Directory {
|
let uri = MentionUri::Directory {
|
||||||
|
|
|
||||||
|
|
@ -1309,9 +1309,7 @@ impl NativeAgentConnection {
|
||||||
{
|
{
|
||||||
response
|
response
|
||||||
.send(outcome)
|
.send(outcome)
|
||||||
.map_err(|_| {
|
.map(|_| anyhow!("authorization receiver was dropped"))
|
||||||
anyhow!("authorization receiver was dropped")
|
|
||||||
})
|
|
||||||
.log_err();
|
.log_err();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -189,15 +189,8 @@ impl AgentTool for GrepTool {
|
||||||
return Err("Search cancelled by user".to_string());
|
return Err("Search cancelled by user".to_string());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
let Some(SearchResult::Buffer { buffer, ranges }) = search_result else {
|
||||||
let (buffer, ranges) = match search_result {
|
break;
|
||||||
Some(SearchResult::Buffer { buffer, ranges }) => (buffer, ranges),
|
|
||||||
Some(SearchResult::LimitReached) => {
|
|
||||||
has_more_matches = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
Some(SearchResult::WaitingForScan) => continue,
|
|
||||||
None => break,
|
|
||||||
};
|
};
|
||||||
if ranges.is_empty() {
|
if ranges.is_empty() {
|
||||||
continue;
|
continue;
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ use buffer_diff::DiffHunkStatus;
|
||||||
use collections::{HashMap, HashSet};
|
use collections::{HashMap, HashSet};
|
||||||
use editor::{
|
use editor::{
|
||||||
Direction, Editor, EditorEvent, EditorSettings, MultiBuffer, MultiBufferSnapshot,
|
Direction, Editor, EditorEvent, EditorSettings, MultiBuffer, MultiBufferSnapshot,
|
||||||
SelectionEffects, SplittableEditor, ToPoint,
|
SelectionEffects, ToPoint,
|
||||||
actions::{GoToHunk, GoToPreviousHunk},
|
actions::{GoToHunk, GoToPreviousHunk},
|
||||||
multibuffer_context_lines,
|
multibuffer_context_lines,
|
||||||
scroll::Autoscroll,
|
scroll::Autoscroll,
|
||||||
|
|
@ -40,7 +40,7 @@ use zed_actions::assistant::ToggleFocus;
|
||||||
|
|
||||||
pub struct AgentDiffPane {
|
pub struct AgentDiffPane {
|
||||||
multibuffer: Entity<MultiBuffer>,
|
multibuffer: Entity<MultiBuffer>,
|
||||||
editor: Entity<SplittableEditor>,
|
editor: Entity<Editor>,
|
||||||
thread: Entity<AcpThread>,
|
thread: Entity<AcpThread>,
|
||||||
focus_handle: FocusHandle,
|
focus_handle: FocusHandle,
|
||||||
workspace: WeakEntity<Workspace>,
|
workspace: WeakEntity<Workspace>,
|
||||||
|
|
@ -91,21 +91,15 @@ impl AgentDiffPane {
|
||||||
|
|
||||||
let project = thread.read(cx).project().clone();
|
let project = thread.read(cx).project().clone();
|
||||||
let editor = cx.new(|cx| {
|
let editor = cx.new(|cx| {
|
||||||
let workspace_entity = workspace.upgrade().expect("workspace must exist");
|
let mut editor =
|
||||||
let diff_display_editor = SplittableEditor::new(
|
Editor::for_multibuffer(multibuffer.clone(), Some(project.clone()), window, cx);
|
||||||
EditorSettings::get_global(cx).diff_view_style,
|
editor.disable_inline_diagnostics();
|
||||||
multibuffer.clone(),
|
editor.set_expand_all_diff_hunks(cx);
|
||||||
project.clone(),
|
editor
|
||||||
workspace_entity,
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
diff_display_editor
|
|
||||||
.set_render_diff_hunk_controls(diff_hunk_controls(&thread, workspace.clone()), cx);
|
.set_render_diff_hunk_controls(diff_hunk_controls(&thread, workspace.clone()), cx);
|
||||||
diff_display_editor.update_editors(cx, |editor, _cx| {
|
editor.register_addon(AgentDiffAddon);
|
||||||
editor.register_addon(AgentDiffAddon);
|
editor.disable_mouse_wheel_zoom();
|
||||||
});
|
editor
|
||||||
diff_display_editor
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let action_log = thread.read(cx).action_log().clone();
|
let action_log = thread.read(cx).action_log().clone();
|
||||||
|
|
@ -187,8 +181,7 @@ impl AgentDiffPane {
|
||||||
(was_empty, is_excerpt_newly_added)
|
(was_empty, is_excerpt_newly_added)
|
||||||
});
|
});
|
||||||
|
|
||||||
let rhs_editor = self.editor.read(cx).rhs_editor().clone();
|
self.editor.update(cx, |editor, cx| {
|
||||||
rhs_editor.update(cx, |editor, cx| {
|
|
||||||
if was_empty {
|
if was_empty {
|
||||||
let first_hunk = editor
|
let first_hunk = editor
|
||||||
.diff_hunks_in_ranges(
|
.diff_hunks_in_ranges(
|
||||||
|
|
@ -245,8 +238,7 @@ impl AgentDiffPane {
|
||||||
|
|
||||||
pub fn move_to_path(&self, path_key: PathKey, window: &mut Window, cx: &mut App) {
|
pub fn move_to_path(&self, path_key: PathKey, window: &mut Window, cx: &mut App) {
|
||||||
if let Some(position) = self.multibuffer.read(cx).location_for_path(&path_key, cx) {
|
if let Some(position) = self.multibuffer.read(cx).location_for_path(&path_key, cx) {
|
||||||
let rhs_editor = self.editor.read(cx).rhs_editor().clone();
|
self.editor.update(cx, |editor, cx| {
|
||||||
rhs_editor.update(cx, |editor, cx| {
|
|
||||||
let first_hunk = editor
|
let first_hunk = editor
|
||||||
.diff_hunks_in_ranges(
|
.diff_hunks_in_ranges(
|
||||||
&[position..editor::Anchor::Max],
|
&[position..editor::Anchor::Max],
|
||||||
|
|
@ -265,16 +257,14 @@ impl AgentDiffPane {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn keep(&mut self, _: &Keep, window: &mut Window, cx: &mut Context<Self>) {
|
fn keep(&mut self, _: &Keep, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
let rhs_editor = self.editor.read(cx).rhs_editor().clone();
|
self.editor.update(cx, |editor, cx| {
|
||||||
rhs_editor.update(cx, |editor, cx| {
|
|
||||||
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
||||||
keep_edits_in_selection(editor, &snapshot, &self.thread, window, cx);
|
keep_edits_in_selection(editor, &snapshot, &self.thread, window, cx);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn reject(&mut self, _: &Reject, window: &mut Window, cx: &mut Context<Self>) {
|
fn reject(&mut self, _: &Reject, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
let rhs_editor = self.editor.read(cx).rhs_editor().clone();
|
self.editor.update(cx, |editor, cx| {
|
||||||
rhs_editor.update(cx, |editor, cx| {
|
|
||||||
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
||||||
reject_edits_in_selection(
|
reject_edits_in_selection(
|
||||||
editor,
|
editor,
|
||||||
|
|
@ -288,8 +278,7 @@ impl AgentDiffPane {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn reject_all(&mut self, _: &RejectAll, window: &mut Window, cx: &mut Context<Self>) {
|
fn reject_all(&mut self, _: &RejectAll, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
let rhs_editor = self.editor.read(cx).rhs_editor().clone();
|
self.editor.update(cx, |editor, cx| {
|
||||||
rhs_editor.update(cx, |editor, cx| {
|
|
||||||
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
||||||
reject_edits_in_ranges(
|
reject_edits_in_ranges(
|
||||||
editor,
|
editor,
|
||||||
|
|
@ -562,10 +551,7 @@ impl Item for AgentDiffPane {
|
||||||
cx: &App,
|
cx: &App,
|
||||||
f: &mut dyn FnMut(gpui::EntityId, &dyn project::ProjectItem),
|
f: &mut dyn FnMut(gpui::EntityId, &dyn project::ProjectItem),
|
||||||
) {
|
) {
|
||||||
self.editor
|
self.editor.for_each_project_item(cx, f)
|
||||||
.read(cx)
|
|
||||||
.rhs_editor()
|
|
||||||
.for_each_project_item(cx, f)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_nav_history(
|
fn set_nav_history(
|
||||||
|
|
@ -574,10 +560,8 @@ impl Item for AgentDiffPane {
|
||||||
_: &mut Window,
|
_: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
self.editor.update(cx, |editor, cx| {
|
self.editor.update(cx, |editor, _| {
|
||||||
editor.rhs_editor().update(cx, |editor, _| {
|
editor.set_nav_history(Some(nav_history));
|
||||||
editor.set_nav_history(Some(nav_history));
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -644,12 +628,14 @@ impl Item for AgentDiffPane {
|
||||||
&'a self,
|
&'a self,
|
||||||
type_id: TypeId,
|
type_id: TypeId,
|
||||||
self_handle: &'a Entity<Self>,
|
self_handle: &'a Entity<Self>,
|
||||||
cx: &'a App,
|
_: &'a App,
|
||||||
) -> Option<gpui::AnyEntity> {
|
) -> Option<gpui::AnyEntity> {
|
||||||
if type_id == TypeId::of::<Self>() {
|
if type_id == TypeId::of::<Self>() {
|
||||||
Some(self_handle.clone().into())
|
Some(self_handle.clone().into())
|
||||||
|
} else if type_id == TypeId::of::<Editor>() {
|
||||||
|
Some(self.editor.clone().into())
|
||||||
} else {
|
} else {
|
||||||
self.editor.act_as_type(type_id, cx)
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1813,7 +1799,7 @@ mod tests {
|
||||||
use gpui::{TestAppContext, UpdateGlobal, VisualTestContext};
|
use gpui::{TestAppContext, UpdateGlobal, VisualTestContext};
|
||||||
use project::{FakeFs, Project};
|
use project::{FakeFs, Project};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use settings::{DiffViewStyle, SettingsStore};
|
use settings::SettingsStore;
|
||||||
use std::{path::Path, rc::Rc};
|
use std::{path::Path, rc::Rc};
|
||||||
use util::path;
|
use util::path;
|
||||||
use workspace::{MultiWorkspace, PathList};
|
use workspace::{MultiWorkspace, PathList};
|
||||||
|
|
@ -1823,11 +1809,6 @@ mod tests {
|
||||||
cx.update(|cx| {
|
cx.update(|cx| {
|
||||||
let settings_store = SettingsStore::test(cx);
|
let settings_store = SettingsStore::test(cx);
|
||||||
cx.set_global(settings_store);
|
cx.set_global(settings_store);
|
||||||
SettingsStore::update_global(cx, |store, cx| {
|
|
||||||
store.update_user_settings(cx, |settings| {
|
|
||||||
settings.editor.diff_view_style = Some(DiffViewStyle::Unified);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
prompt_store::init(cx);
|
prompt_store::init(cx);
|
||||||
theme_settings::init(theme::LoadThemes::JustBase, cx);
|
theme_settings::init(theme::LoadThemes::JustBase, cx);
|
||||||
language_model::init(cx);
|
language_model::init(cx);
|
||||||
|
|
@ -1866,7 +1847,7 @@ mod tests {
|
||||||
let agent_diff = cx.new_window_entity(|window, cx| {
|
let agent_diff = cx.new_window_entity(|window, cx| {
|
||||||
AgentDiffPane::new(thread.clone(), workspace.downgrade(), window, cx)
|
AgentDiffPane::new(thread.clone(), workspace.downgrade(), window, cx)
|
||||||
});
|
});
|
||||||
let editor = agent_diff.read_with(cx, |diff, cx| diff.editor.read(cx).rhs_editor().clone());
|
let editor = agent_diff.read_with(cx, |diff, _cx| diff.editor.clone());
|
||||||
|
|
||||||
let buffer = project
|
let buffer = project
|
||||||
.update(cx, |project, cx| project.open_buffer(buffer_path, cx))
|
.update(cx, |project, cx| project.open_buffer(buffer_path, cx))
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ use crate::DEFAULT_THREAD_TITLE;
|
||||||
use crate::ExpandMessageEditor;
|
use crate::ExpandMessageEditor;
|
||||||
use crate::ManageProfiles;
|
use crate::ManageProfiles;
|
||||||
use crate::agent_connection_store::AgentConnectionStore;
|
use crate::agent_connection_store::AgentConnectionStore;
|
||||||
use crate::thread_metadata_store::{ThreadId, ThreadMetadataStore, ThreadMetadataStoreEvent};
|
use crate::thread_metadata_store::{ThreadId, ThreadMetadataStore};
|
||||||
use crate::{
|
use crate::{
|
||||||
AddContextServer, AgentDiffPane, ConversationView, CopyThreadToClipboard, Follow,
|
AddContextServer, AgentDiffPane, ConversationView, CopyThreadToClipboard, Follow,
|
||||||
InlineAssistant, LoadThreadFromClipboard, NewThread, OpenActiveThreadAsMarkdown, OpenAgentDiff,
|
InlineAssistant, LoadThreadFromClipboard, NewThread, OpenActiveThreadAsMarkdown, OpenAgentDiff,
|
||||||
|
|
@ -708,7 +708,6 @@ pub struct AgentPanel {
|
||||||
show_trust_workspace_message: bool,
|
show_trust_workspace_message: bool,
|
||||||
_base_view_observation: Option<Subscription>,
|
_base_view_observation: Option<Subscription>,
|
||||||
_draft_editor_observation: Option<Subscription>,
|
_draft_editor_observation: Option<Subscription>,
|
||||||
_thread_metadata_store_subscription: Subscription,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AgentPanel {
|
impl AgentPanel {
|
||||||
|
|
@ -1023,17 +1022,6 @@ impl AgentPanel {
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
});
|
});
|
||||||
|
|
||||||
let _thread_metadata_store_subscription = cx.subscribe(
|
|
||||||
&ThreadMetadataStore::global(cx),
|
|
||||||
|this, _store, event, cx| {
|
|
||||||
let ThreadMetadataStoreEvent::ThreadArchived(thread_id) = event;
|
|
||||||
if this.retained_threads.remove(thread_id).is_some() {
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut panel = Self {
|
let mut panel = Self {
|
||||||
workspace_id,
|
workspace_id,
|
||||||
base_view,
|
base_view,
|
||||||
|
|
@ -1067,7 +1055,6 @@ impl AgentPanel {
|
||||||
new_user_onboarding_upsell_dismissed: AtomicBool::new(OnboardingUpsell::dismissed(cx)),
|
new_user_onboarding_upsell_dismissed: AtomicBool::new(OnboardingUpsell::dismissed(cx)),
|
||||||
_base_view_observation: None,
|
_base_view_observation: None,
|
||||||
_draft_editor_observation: None,
|
_draft_editor_observation: None,
|
||||||
_thread_metadata_store_subscription,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Initial sync of agent servers from extensions
|
// Initial sync of agent servers from extensions
|
||||||
|
|
@ -1404,11 +1391,20 @@ impl AgentPanel {
|
||||||
) {
|
) {
|
||||||
let session_id = action.from_session_id.clone();
|
let session_id = action.from_session_id.clone();
|
||||||
|
|
||||||
let Some(content) = Self::initial_content_for_thread_summary(session_id.clone(), cx) else {
|
let Some(thread) = ThreadStore::global(cx)
|
||||||
|
.read(cx)
|
||||||
|
.entries()
|
||||||
|
.find(|t| t.id == session_id)
|
||||||
|
else {
|
||||||
log::error!("No session found for summarization with id {}", session_id);
|
log::error!("No session found for summarization with id {}", session_id);
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let Some(parent_session_id) = thread.parent_session_id else {
|
||||||
|
log::error!("Session {} has no parent session", session_id);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
cx.spawn_in(window, async move |this, cx| {
|
cx.spawn_in(window, async move |this, cx| {
|
||||||
this.update_in(cx, |this, window, cx| {
|
this.update_in(cx, |this, window, cx| {
|
||||||
this.external_thread(
|
this.external_thread(
|
||||||
|
|
@ -1416,7 +1412,10 @@ impl AgentPanel {
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
Some(content),
|
Some(AgentInitialContent::ThreadSummary {
|
||||||
|
session_id: parent_session_id,
|
||||||
|
title: Some(thread.title),
|
||||||
|
}),
|
||||||
true,
|
true,
|
||||||
"agent_panel",
|
"agent_panel",
|
||||||
window,
|
window,
|
||||||
|
|
@ -1428,21 +1427,6 @@ impl AgentPanel {
|
||||||
.detach_and_log_err(cx);
|
.detach_and_log_err(cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn initial_content_for_thread_summary(
|
|
||||||
session_id: acp::SessionId,
|
|
||||||
cx: &App,
|
|
||||||
) -> Option<AgentInitialContent> {
|
|
||||||
let thread = ThreadStore::global(cx)
|
|
||||||
.read(cx)
|
|
||||||
.entries()
|
|
||||||
.find(|t| t.id == session_id)?;
|
|
||||||
|
|
||||||
Some(AgentInitialContent::ThreadSummary {
|
|
||||||
session_id: thread.id,
|
|
||||||
title: Some(thread.title),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn external_thread(
|
fn external_thread(
|
||||||
&mut self,
|
&mut self,
|
||||||
agent_choice: Option<crate::Agent>,
|
agent_choice: Option<crate::Agent>,
|
||||||
|
|
@ -5064,89 +5048,6 @@ mod tests {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
|
||||||
async fn test_initial_content_for_thread_summary_uses_own_session_id(cx: &mut TestAppContext) {
|
|
||||||
init_test(cx);
|
|
||||||
cx.update(|cx| {
|
|
||||||
agent::ThreadStore::init_global(cx);
|
|
||||||
language_model::LanguageModelRegistry::test(cx);
|
|
||||||
});
|
|
||||||
|
|
||||||
let source_session_id = acp::SessionId::new("source-thread-session");
|
|
||||||
let source_title: SharedString = "Source Thread Title".into();
|
|
||||||
let db_thread = agent::DbThread {
|
|
||||||
title: source_title.clone(),
|
|
||||||
messages: Vec::new(),
|
|
||||||
updated_at: Utc::now(),
|
|
||||||
detailed_summary: None,
|
|
||||||
initial_project_snapshot: None,
|
|
||||||
cumulative_token_usage: Default::default(),
|
|
||||||
request_token_usage: HashMap::default(),
|
|
||||||
model: None,
|
|
||||||
profile: None,
|
|
||||||
imported: false,
|
|
||||||
subagent_context: None,
|
|
||||||
speed: None,
|
|
||||||
thinking_enabled: false,
|
|
||||||
thinking_effort: None,
|
|
||||||
draft_prompt: None,
|
|
||||||
ui_scroll_position: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
let thread_store = cx.update(|cx| ThreadStore::global(cx));
|
|
||||||
thread_store
|
|
||||||
.update(cx, |store, cx| {
|
|
||||||
store.save_thread(
|
|
||||||
source_session_id.clone(),
|
|
||||||
db_thread,
|
|
||||||
PathList::default(),
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.expect("saving source thread should succeed");
|
|
||||||
cx.run_until_parked();
|
|
||||||
|
|
||||||
thread_store.read_with(cx, |store, _cx| {
|
|
||||||
let entry = store
|
|
||||||
.thread_from_session_id(&source_session_id)
|
|
||||||
.expect("saved thread should be listed in the store");
|
|
||||||
assert!(
|
|
||||||
entry.parent_session_id.is_none(),
|
|
||||||
"saved thread is a root thread with no parent session"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
let content = cx
|
|
||||||
.update(|cx| {
|
|
||||||
AgentPanel::initial_content_for_thread_summary(source_session_id.clone(), cx)
|
|
||||||
})
|
|
||||||
.expect("initial content should be produced for a root thread");
|
|
||||||
|
|
||||||
match content {
|
|
||||||
AgentInitialContent::ThreadSummary { session_id, title } => {
|
|
||||||
assert_eq!(
|
|
||||||
session_id, source_session_id,
|
|
||||||
"thread-summary mention should use the source thread's own session id"
|
|
||||||
);
|
|
||||||
assert_eq!(title, Some(source_title.clone()));
|
|
||||||
}
|
|
||||||
_ => panic!("expected AgentInitialContent::ThreadSummary"),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unknown session ids should still produce no content.
|
|
||||||
let missing = cx.update(|cx| {
|
|
||||||
AgentPanel::initial_content_for_thread_summary(
|
|
||||||
acp::SessionId::new("does-not-exist"),
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
});
|
|
||||||
assert!(
|
|
||||||
missing.is_none(),
|
|
||||||
"unknown session ids should not produce initial content"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_cleanup_retained_threads_keeps_five_most_recent_idle_loadable_threads(
|
async fn test_cleanup_retained_threads_keeps_five_most_recent_idle_loadable_threads(
|
||||||
cx: &mut TestAppContext,
|
cx: &mut TestAppContext,
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,7 @@ use std::time::Instant;
|
||||||
use std::{collections::BTreeMap, rc::Rc, time::Duration};
|
use std::{collections::BTreeMap, rc::Rc, time::Duration};
|
||||||
use terminal_view::terminal_panel::TerminalPanel;
|
use terminal_view::terminal_panel::TerminalPanel;
|
||||||
use text::Anchor;
|
use text::Anchor;
|
||||||
use theme_settings::{AgentBufferFontSize, AgentUiFontSize};
|
use theme_settings::AgentFontSize;
|
||||||
use ui::{
|
use ui::{
|
||||||
Callout, CircularProgress, CommonAnimationExt, ContextMenu, ContextMenuEntry, CopyButton,
|
Callout, CircularProgress, CommonAnimationExt, ContextMenu, ContextMenuEntry, CopyButton,
|
||||||
DecoratedIcon, DiffStat, Disclosure, Divider, DividerColor, IconDecoration, IconDecorationKind,
|
DecoratedIcon, DiffStat, Disclosure, Divider, DividerColor, IconDecoration, IconDecorationKind,
|
||||||
|
|
@ -632,8 +632,7 @@ impl ConversationView {
|
||||||
let agent_server_store = project.read(cx).agent_server_store().clone();
|
let agent_server_store = project.read(cx).agent_server_store().clone();
|
||||||
let subscriptions = vec![
|
let subscriptions = vec![
|
||||||
cx.observe_global_in::<SettingsStore>(window, Self::agent_ui_font_size_changed),
|
cx.observe_global_in::<SettingsStore>(window, Self::agent_ui_font_size_changed),
|
||||||
cx.observe_global_in::<AgentUiFontSize>(window, Self::agent_ui_font_size_changed),
|
cx.observe_global_in::<AgentFontSize>(window, Self::agent_ui_font_size_changed),
|
||||||
cx.observe_global_in::<AgentBufferFontSize>(window, Self::agent_ui_font_size_changed),
|
|
||||||
cx.subscribe_in(
|
cx.subscribe_in(
|
||||||
&agent_server_store,
|
&agent_server_store,
|
||||||
window,
|
window,
|
||||||
|
|
|
||||||
|
|
@ -174,10 +174,6 @@ impl MentionSet {
|
||||||
self.mentions.values().map(|(uri, _)| uri.clone()).collect()
|
self.mentions.values().map(|(uri, _)| uri.clone()).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn mention_uri_for_crease(&self, crease_id: &CreaseId) -> Option<MentionUri> {
|
|
||||||
self.mentions.get(crease_id).map(|(uri, _)| uri.clone())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_mentions(&mut self, mentions: HashMap<CreaseId, (MentionUri, MentionTask)>) {
|
pub fn set_mentions(&mut self, mentions: HashMap<CreaseId, (MentionUri, MentionTask)>) {
|
||||||
self.mentions = mentions;
|
self.mentions = mentions;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,14 +15,12 @@ use anyhow::{Result, anyhow};
|
||||||
use editor::{
|
use editor::{
|
||||||
Addon, AnchorRangeExt, ContextMenuOptions, Editor, EditorElement, EditorEvent, EditorMode,
|
Addon, AnchorRangeExt, ContextMenuOptions, Editor, EditorElement, EditorEvent, EditorMode,
|
||||||
EditorStyle, Inlay, MultiBuffer, MultiBufferOffset, MultiBufferSnapshot, ToOffset,
|
EditorStyle, Inlay, MultiBuffer, MultiBufferOffset, MultiBufferSnapshot, ToOffset,
|
||||||
actions::{Copy, Paste},
|
actions::Paste, code_context_menus::CodeContextMenu, scroll::Autoscroll,
|
||||||
code_context_menus::CodeContextMenu,
|
|
||||||
scroll::Autoscroll,
|
|
||||||
};
|
};
|
||||||
use futures::{FutureExt as _, future::join_all};
|
use futures::{FutureExt as _, future::join_all};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AppContext, ClipboardEntry, ClipboardItem, Context, Entity, EventEmitter, FocusHandle,
|
AppContext, ClipboardEntry, Context, Entity, EventEmitter, FocusHandle, Focusable, ImageFormat,
|
||||||
Focusable, ImageFormat, KeyContext, SharedString, Subscription, Task, TextStyle, WeakEntity,
|
KeyContext, SharedString, Subscription, Task, TextStyle, WeakEntity,
|
||||||
};
|
};
|
||||||
use language::{Buffer, language_settings::InlayHintKind};
|
use language::{Buffer, language_settings::InlayHintKind};
|
||||||
use parking_lot::RwLock;
|
use parking_lot::RwLock;
|
||||||
|
|
@ -1188,16 +1186,6 @@ impl MessageEditor {
|
||||||
cx.propagate();
|
cx.propagate();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn copy(&mut self, _: &Copy, _: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
let Some(text) = self.serialized_copy_text(cx) else {
|
|
||||||
cx.propagate();
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
cx.stop_propagation();
|
|
||||||
cx.write_to_clipboard(ClipboardItem::new_string(text));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn paste_raw(&mut self, _: &PasteRaw, window: &mut Window, cx: &mut Context<Self>) {
|
fn paste_raw(&mut self, _: &PasteRaw, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
let editor = self.editor.clone();
|
let editor = self.editor.clone();
|
||||||
window.defer(cx, move |window, cx| {
|
window.defer(cx, move |window, cx| {
|
||||||
|
|
@ -1777,76 +1765,6 @@ impl MessageEditor {
|
||||||
editor.set_text(text, window, cx);
|
editor.set_text(text, window, cx);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn serialized_copy_text(&self, cx: &mut App) -> Option<String> {
|
|
||||||
let display_snapshot = self
|
|
||||||
.editor
|
|
||||||
.update(cx, |editor, cx| editor.display_snapshot(cx));
|
|
||||||
let editor = self.editor.read(cx);
|
|
||||||
if !editor.has_non_empty_selection(&display_snapshot) {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
|
||||||
let mention_set = self.mention_set.read(cx);
|
|
||||||
let mention_ranges = display_snapshot
|
|
||||||
.crease_snapshot
|
|
||||||
.crease_items_with_offsets(&snapshot)
|
|
||||||
.into_iter()
|
|
||||||
.filter_map(|(crease_id, range)| {
|
|
||||||
mention_set.mention_uri_for_crease(&crease_id).map(|uri| {
|
|
||||||
(
|
|
||||||
range.start.to_offset(&snapshot),
|
|
||||||
range.end.to_offset(&snapshot),
|
|
||||||
uri,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
let mut text = String::new();
|
|
||||||
let mut has_mentions = false;
|
|
||||||
let mut is_first = true;
|
|
||||||
|
|
||||||
for selection in editor
|
|
||||||
.selections
|
|
||||||
.all::<MultiBufferOffset>(&display_snapshot)
|
|
||||||
{
|
|
||||||
if is_first {
|
|
||||||
is_first = false;
|
|
||||||
} else {
|
|
||||||
text.push('\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut overlapping_mentions = mention_ranges
|
|
||||||
.iter()
|
|
||||||
.filter(|(start, end, _)| *start < selection.end && selection.start < *end)
|
|
||||||
.peekable();
|
|
||||||
|
|
||||||
if overlapping_mentions.peek().is_none() {
|
|
||||||
text.extend(snapshot.text_for_range(selection.start..selection.end));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
has_mentions = true;
|
|
||||||
|
|
||||||
let mut cursor = selection.start;
|
|
||||||
for (start, end, uri) in overlapping_mentions {
|
|
||||||
if cursor < *start {
|
|
||||||
text.extend(snapshot.text_for_range(cursor..*start));
|
|
||||||
}
|
|
||||||
|
|
||||||
write!(text, "{}", uri.as_link()).unwrap();
|
|
||||||
cursor = *end;
|
|
||||||
}
|
|
||||||
|
|
||||||
if cursor < selection.end {
|
|
||||||
text.extend(snapshot.text_for_range(cursor..selection.end));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
has_mentions.then_some(text)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Focusable for MessageEditor {
|
impl Focusable for MessageEditor {
|
||||||
|
|
@ -1863,7 +1781,6 @@ impl Render for MessageEditor {
|
||||||
.on_action(cx.listener(Self::send_immediately))
|
.on_action(cx.listener(Self::send_immediately))
|
||||||
.on_action(cx.listener(Self::chat_with_follow))
|
.on_action(cx.listener(Self::chat_with_follow))
|
||||||
.on_action(cx.listener(Self::cancel))
|
.on_action(cx.listener(Self::cancel))
|
||||||
.capture_action(cx.listener(Self::copy))
|
|
||||||
.on_action(cx.listener(Self::paste_raw))
|
.on_action(cx.listener(Self::paste_raw))
|
||||||
.capture_action(cx.listener(Self::paste))
|
.capture_action(cx.listener(Self::paste))
|
||||||
.flex_1()
|
.flex_1()
|
||||||
|
|
@ -1998,10 +1915,10 @@ mod tests {
|
||||||
};
|
};
|
||||||
|
|
||||||
use fs::FakeFs;
|
use fs::FakeFs;
|
||||||
use futures::{FutureExt as _, StreamExt as _};
|
use futures::StreamExt as _;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AppContext, ClipboardEntry, ClipboardItem, Entity, EventEmitter, ExternalPaths,
|
AppContext, ClipboardEntry, ClipboardItem, Entity, EventEmitter, ExternalPaths,
|
||||||
FocusHandle, Focusable, Task, TestAppContext, VisualTestContext,
|
FocusHandle, Focusable, TestAppContext, VisualTestContext,
|
||||||
};
|
};
|
||||||
use language_model::LanguageModelRegistry;
|
use language_model::LanguageModelRegistry;
|
||||||
use lsp::{CompletionContext, CompletionTriggerKind};
|
use lsp::{CompletionContext, CompletionTriggerKind};
|
||||||
|
|
@ -2017,7 +1934,6 @@ mod tests {
|
||||||
use crate::completion_provider::PromptContextType;
|
use crate::completion_provider::PromptContextType;
|
||||||
use crate::{
|
use crate::{
|
||||||
conversation_view::tests::init_test,
|
conversation_view::tests::init_test,
|
||||||
mention_set::insert_crease_for_mention,
|
|
||||||
message_editor::{Mention, MessageEditor, SessionCapabilities, parse_mention_links},
|
message_editor::{Mention, MessageEditor, SessionCapabilities, parse_mention_links},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -3932,318 +3848,6 @@ mod tests {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
|
||||||
async fn test_copy_with_selection_mentions_serializes_links(cx: &mut TestAppContext) {
|
|
||||||
init_test(cx);
|
|
||||||
|
|
||||||
let (source_message_editor, _source_editor, mut cx) = setup_paste_test_message_editor(
|
|
||||||
json!({"file.rs": "line 1\nline 2\nline 3\nline 4\n"}),
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
let workspace = source_message_editor.read_with(&cx, |message_editor, _| {
|
|
||||||
message_editor.workspace.upgrade().expect("workspace")
|
|
||||||
});
|
|
||||||
let project = workspace.read_with(&cx, |workspace, _| workspace.project().clone());
|
|
||||||
|
|
||||||
let source_text = "selection needs work\nselection looks fine";
|
|
||||||
let first_range = 0..9;
|
|
||||||
let second_start = "selection needs work\n".len();
|
|
||||||
let second_range = second_start..(second_start + "selection".len());
|
|
||||||
let first_uri = MentionUri::Selection {
|
|
||||||
abs_path: Some(path!("/project/file.rs").into()),
|
|
||||||
line_range: 0..=1,
|
|
||||||
};
|
|
||||||
let second_uri = MentionUri::Selection {
|
|
||||||
abs_path: Some(path!("/project/file.rs").into()),
|
|
||||||
line_range: 2..=3,
|
|
||||||
};
|
|
||||||
|
|
||||||
source_message_editor.update_in(&mut cx, |message_editor, window, cx| {
|
|
||||||
message_editor.set_text(source_text, window, cx);
|
|
||||||
|
|
||||||
let snapshot = message_editor
|
|
||||||
.editor
|
|
||||||
.read(cx)
|
|
||||||
.buffer()
|
|
||||||
.read(cx)
|
|
||||||
.snapshot(cx);
|
|
||||||
for (range, uri, content) in [
|
|
||||||
(
|
|
||||||
first_range.clone(),
|
|
||||||
first_uri.clone(),
|
|
||||||
"line 1\nline 2\n".to_string(),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
second_range.clone(),
|
|
||||||
second_uri.clone(),
|
|
||||||
"line 3\nline 4\n".to_string(),
|
|
||||||
),
|
|
||||||
] {
|
|
||||||
let Some((crease_id, tx)) = insert_crease_for_mention(
|
|
||||||
snapshot
|
|
||||||
.anchor_to_buffer_anchor(
|
|
||||||
snapshot.anchor_before(MultiBufferOffset(range.start)),
|
|
||||||
)
|
|
||||||
.expect("selection mention anchor should map to a buffer")
|
|
||||||
.0,
|
|
||||||
range.len(),
|
|
||||||
uri.name().into(),
|
|
||||||
uri.icon_path(cx),
|
|
||||||
uri.tooltip_text(),
|
|
||||||
Some(uri.clone()),
|
|
||||||
Some(message_editor.workspace.clone()),
|
|
||||||
None,
|
|
||||||
message_editor.editor.clone(),
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
) else {
|
|
||||||
panic!("expected mention crease insertion");
|
|
||||||
};
|
|
||||||
drop(tx);
|
|
||||||
|
|
||||||
message_editor.mention_set.update(cx, |mention_set, _cx| {
|
|
||||||
mention_set.insert_mention(
|
|
||||||
crease_id,
|
|
||||||
uri,
|
|
||||||
Task::ready(Ok(Mention::Text {
|
|
||||||
content,
|
|
||||||
tracked_buffers: Vec::new(),
|
|
||||||
}))
|
|
||||||
.shared(),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let buffer_len = snapshot.len();
|
|
||||||
message_editor.editor.update(cx, |editor, cx| {
|
|
||||||
editor.change_selections(Default::default(), window, cx, |selections| {
|
|
||||||
selections.select_ranges([MultiBufferOffset(0)..buffer_len]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
let copied_text = source_message_editor.update(&mut cx, |message_editor, cx| {
|
|
||||||
message_editor
|
|
||||||
.serialized_copy_text(cx)
|
|
||||||
.expect("selection mentions should serialize")
|
|
||||||
});
|
|
||||||
let expected_text = format!(
|
|
||||||
"{} needs work\n{} looks fine",
|
|
||||||
first_uri.as_link(),
|
|
||||||
second_uri.as_link()
|
|
||||||
);
|
|
||||||
assert_eq!(copied_text, expected_text);
|
|
||||||
|
|
||||||
let target_message_editor = workspace.update_in(&mut cx, |workspace, window, cx| {
|
|
||||||
let workspace_handle = cx.weak_entity();
|
|
||||||
let thread_store = cx.new(|cx| ThreadStore::new(cx));
|
|
||||||
let message_editor = cx.new(|cx| {
|
|
||||||
MessageEditor::new(
|
|
||||||
workspace_handle,
|
|
||||||
project.downgrade(),
|
|
||||||
Some(thread_store),
|
|
||||||
None,
|
|
||||||
Default::default(),
|
|
||||||
"Test Agent".into(),
|
|
||||||
"Test",
|
|
||||||
EditorMode::AutoHeight {
|
|
||||||
max_lines: None,
|
|
||||||
min_lines: 1,
|
|
||||||
},
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
});
|
|
||||||
workspace.active_pane().update(cx, |pane, cx| {
|
|
||||||
pane.add_item(
|
|
||||||
Box::new(cx.new(|_| MessageEditorItem(message_editor.clone()))),
|
|
||||||
true,
|
|
||||||
true,
|
|
||||||
None,
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
message_editor.read(cx).focus_handle(cx).focus(window, cx);
|
|
||||||
message_editor
|
|
||||||
});
|
|
||||||
|
|
||||||
cx.write_to_clipboard(ClipboardItem::new_string(copied_text));
|
|
||||||
target_message_editor.update_in(&mut cx, |message_editor, window, cx| {
|
|
||||||
message_editor.paste(&Paste, window, cx);
|
|
||||||
});
|
|
||||||
cx.run_until_parked();
|
|
||||||
|
|
||||||
let target_text = target_message_editor.read_with(&cx, |message_editor, cx| {
|
|
||||||
message_editor.editor.read(cx).text(cx)
|
|
||||||
});
|
|
||||||
assert_eq!(target_text, expected_text);
|
|
||||||
|
|
||||||
let contents = mention_contents(&target_message_editor, &mut cx).await;
|
|
||||||
assert_eq!(contents.len(), 2);
|
|
||||||
assert!(contents.iter().any(|(uri, _)| uri == &first_uri));
|
|
||||||
assert!(contents.iter().any(|(uri, _)| uri == &second_uri));
|
|
||||||
}
|
|
||||||
|
|
||||||
struct SelectionMentionFixture {
|
|
||||||
message_editor: Entity<MessageEditor>,
|
|
||||||
first_uri: MentionUri,
|
|
||||||
first_range: Range<usize>,
|
|
||||||
second_range: Range<usize>,
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn setup_selection_mention_fixture(
|
|
||||||
cx: &mut TestAppContext,
|
|
||||||
) -> (SelectionMentionFixture, VisualTestContext) {
|
|
||||||
let (message_editor, _source_editor, mut cx) = setup_paste_test_message_editor(
|
|
||||||
json!({"file.rs": "line 1\nline 2\nline 3\nline 4\n"}),
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
let source_text = "selection needs work\nselection looks fine";
|
|
||||||
let first_range = 0..9;
|
|
||||||
let second_start = "selection needs work\n".len();
|
|
||||||
let second_range = second_start..(second_start + "selection".len());
|
|
||||||
let first_uri = MentionUri::Selection {
|
|
||||||
abs_path: Some(path!("/project/file.rs").into()),
|
|
||||||
line_range: 0..=1,
|
|
||||||
};
|
|
||||||
let second_uri = MentionUri::Selection {
|
|
||||||
abs_path: Some(path!("/project/file.rs").into()),
|
|
||||||
line_range: 2..=3,
|
|
||||||
};
|
|
||||||
|
|
||||||
message_editor.update_in(&mut cx, |message_editor, window, cx| {
|
|
||||||
message_editor.set_text(source_text, window, cx);
|
|
||||||
|
|
||||||
let snapshot = message_editor
|
|
||||||
.editor
|
|
||||||
.read(cx)
|
|
||||||
.buffer()
|
|
||||||
.read(cx)
|
|
||||||
.snapshot(cx);
|
|
||||||
for (range, uri, content) in [
|
|
||||||
(
|
|
||||||
first_range.clone(),
|
|
||||||
first_uri.clone(),
|
|
||||||
"line 1\nline 2\n".to_string(),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
second_range.clone(),
|
|
||||||
second_uri.clone(),
|
|
||||||
"line 3\nline 4\n".to_string(),
|
|
||||||
),
|
|
||||||
] {
|
|
||||||
let Some((crease_id, tx)) = insert_crease_for_mention(
|
|
||||||
snapshot
|
|
||||||
.anchor_to_buffer_anchor(
|
|
||||||
snapshot.anchor_before(MultiBufferOffset(range.start)),
|
|
||||||
)
|
|
||||||
.expect("selection mention anchor should map to a buffer")
|
|
||||||
.0,
|
|
||||||
range.len(),
|
|
||||||
uri.name().into(),
|
|
||||||
uri.icon_path(cx),
|
|
||||||
uri.tooltip_text(),
|
|
||||||
Some(uri.clone()),
|
|
||||||
Some(message_editor.workspace.clone()),
|
|
||||||
None,
|
|
||||||
message_editor.editor.clone(),
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
) else {
|
|
||||||
panic!("expected mention crease insertion");
|
|
||||||
};
|
|
||||||
drop(tx);
|
|
||||||
|
|
||||||
message_editor.mention_set.update(cx, |mention_set, _cx| {
|
|
||||||
mention_set.insert_mention(
|
|
||||||
crease_id,
|
|
||||||
uri,
|
|
||||||
Task::ready(Ok(Mention::Text {
|
|
||||||
content,
|
|
||||||
tracked_buffers: Vec::new(),
|
|
||||||
}))
|
|
||||||
.shared(),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
(
|
|
||||||
SelectionMentionFixture {
|
|
||||||
message_editor,
|
|
||||||
first_uri,
|
|
||||||
first_range,
|
|
||||||
second_range,
|
|
||||||
},
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[gpui::test]
|
|
||||||
async fn test_serialized_copy_text_selection_covers_only_mention(cx: &mut TestAppContext) {
|
|
||||||
init_test(cx);
|
|
||||||
|
|
||||||
let (fixture, mut cx) = setup_selection_mention_fixture(cx).await;
|
|
||||||
|
|
||||||
fixture
|
|
||||||
.message_editor
|
|
||||||
.update_in(&mut cx, |message_editor, window, cx| {
|
|
||||||
let range = fixture.first_range.clone();
|
|
||||||
message_editor.editor.update(cx, |editor, cx| {
|
|
||||||
editor.change_selections(Default::default(), window, cx, |selections| {
|
|
||||||
selections.select_ranges([
|
|
||||||
MultiBufferOffset(range.start)..MultiBufferOffset(range.end)
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
let copied = fixture
|
|
||||||
.message_editor
|
|
||||||
.update(&mut cx, |message_editor, cx| {
|
|
||||||
message_editor.serialized_copy_text(cx)
|
|
||||||
});
|
|
||||||
|
|
||||||
assert_eq!(copied, Some(fixture.first_uri.as_link().to_string()));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[gpui::test]
|
|
||||||
async fn test_serialized_copy_text_returns_none_when_mentions_outside_selection(
|
|
||||||
cx: &mut TestAppContext,
|
|
||||||
) {
|
|
||||||
init_test(cx);
|
|
||||||
|
|
||||||
let (fixture, mut cx) = setup_selection_mention_fixture(cx).await;
|
|
||||||
|
|
||||||
let between_start = fixture.first_range.end;
|
|
||||||
let between_end = fixture.second_range.start - 1;
|
|
||||||
|
|
||||||
fixture
|
|
||||||
.message_editor
|
|
||||||
.update_in(&mut cx, |message_editor, window, cx| {
|
|
||||||
message_editor.editor.update(cx, |editor, cx| {
|
|
||||||
editor.change_selections(Default::default(), window, cx, |selections| {
|
|
||||||
selections.select_ranges([
|
|
||||||
MultiBufferOffset(between_start)..MultiBufferOffset(between_end)
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
let copied = fixture
|
|
||||||
.message_editor
|
|
||||||
.update(&mut cx, |message_editor, cx| {
|
|
||||||
message_editor.serialized_copy_text(cx)
|
|
||||||
});
|
|
||||||
|
|
||||||
assert_eq!(copied, None);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_paste_mention_link_with_completion_trigger_does_not_panic(
|
async fn test_paste_mention_link_with_completion_trigger_does_not_panic(
|
||||||
cx: &mut TestAppContext,
|
cx: &mut TestAppContext,
|
||||||
|
|
|
||||||
|
|
@ -785,8 +785,6 @@ impl ThreadMetadataStore {
|
||||||
if let Some(job) = archive_job {
|
if let Some(job) = archive_job {
|
||||||
self.in_flight_archives.insert(thread_id, job);
|
self.in_flight_archives.insert(thread_id, job);
|
||||||
}
|
}
|
||||||
|
|
||||||
cx.emit(ThreadMetadataStoreEvent::ThreadArchived(thread_id));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn unarchive(&mut self, thread_id: ThreadId, cx: &mut Context<Self>) {
|
pub fn unarchive(&mut self, thread_id: ThreadId, cx: &mut Context<Self>) {
|
||||||
|
|
@ -1230,13 +1228,6 @@ impl ThreadMetadataStore {
|
||||||
|
|
||||||
impl Global for ThreadMetadataStore {}
|
impl Global for ThreadMetadataStore {}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub enum ThreadMetadataStoreEvent {
|
|
||||||
ThreadArchived(ThreadId),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl gpui::EventEmitter<ThreadMetadataStoreEvent> for ThreadMetadataStore {}
|
|
||||||
|
|
||||||
struct ThreadMetadataDb(ThreadSafeConnection);
|
struct ThreadMetadataDb(ThreadSafeConnection);
|
||||||
|
|
||||||
impl Domain for ThreadMetadataDb {
|
impl Domain for ThreadMetadataDb {
|
||||||
|
|
|
||||||
|
|
@ -764,20 +764,8 @@ pub enum ToolChoice {
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
#[serde(tag = "type", rename_all = "lowercase")]
|
#[serde(tag = "type", rename_all = "lowercase")]
|
||||||
pub enum Thinking {
|
pub enum Thinking {
|
||||||
Enabled {
|
Enabled { budget_tokens: Option<u32> },
|
||||||
budget_tokens: Option<u32>,
|
Adaptive,
|
||||||
},
|
|
||||||
Adaptive {
|
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
||||||
display: Option<AdaptiveThinkingDisplay>,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
|
||||||
#[serde(rename_all = "lowercase")]
|
|
||||||
pub enum AdaptiveThinkingDisplay {
|
|
||||||
Omitted,
|
|
||||||
Summarized,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, EnumString)]
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize, EnumString)]
|
||||||
|
|
|
||||||
|
|
@ -11,9 +11,9 @@ use std::pin::Pin;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
AdaptiveThinkingDisplay, AnthropicError, AnthropicModelMode, CacheControl, CacheControlType,
|
AnthropicError, AnthropicModelMode, CacheControl, CacheControlType, ContentDelta, Event,
|
||||||
ContentDelta, Event, ImageSource, Message, RequestContent, ResponseContent, StringOrContents,
|
ImageSource, Message, RequestContent, ResponseContent, StringOrContents, Thinking, Tool,
|
||||||
Thinking, Tool, ToolChoice, ToolResultContent, ToolResultPart, Usage,
|
ToolChoice, ToolResultContent, ToolResultPart, Usage,
|
||||||
};
|
};
|
||||||
|
|
||||||
fn to_anthropic_content(content: MessageContent) -> Option<RequestContent> {
|
fn to_anthropic_content(content: MessageContent) -> Option<RequestContent> {
|
||||||
|
|
@ -180,9 +180,7 @@ pub fn into_anthropic(
|
||||||
AnthropicModelMode::Thinking { budget_tokens } => {
|
AnthropicModelMode::Thinking { budget_tokens } => {
|
||||||
Some(Thinking::Enabled { budget_tokens })
|
Some(Thinking::Enabled { budget_tokens })
|
||||||
}
|
}
|
||||||
AnthropicModelMode::AdaptiveThinking => Some(Thinking::Adaptive {
|
AnthropicModelMode::AdaptiveThinking => Some(Thinking::Adaptive),
|
||||||
display: Some(AdaptiveThinkingDisplay::Summarized),
|
|
||||||
}),
|
|
||||||
AnthropicModelMode::Default => None,
|
AnthropicModelMode::Default => None,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -124,7 +124,7 @@ impl AskPassSession {
|
||||||
ControlFlow::Continue(Ok(password))
|
ControlFlow::Continue(Ok(password))
|
||||||
} else {
|
} else {
|
||||||
if let Some(kill_tx) = kill_tx.lock().await.take() {
|
if let Some(kill_tx) = kill_tx.lock().await.take() {
|
||||||
kill_tx.send(()).ok();
|
kill_tx.send(()).log_err();
|
||||||
}
|
}
|
||||||
ControlFlow::Break(())
|
ControlFlow::Break(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,17 +17,15 @@ anyhow.workspace = true
|
||||||
auto_update.workspace = true
|
auto_update.workspace = true
|
||||||
client.workspace = true
|
client.workspace = true
|
||||||
db.workspace = true
|
db.workspace = true
|
||||||
editor.workspace = true
|
|
||||||
fs.workspace = true
|
fs.workspace = true
|
||||||
|
editor.workspace = true
|
||||||
|
notifications.workspace = true
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
markdown_preview.workspace = true
|
markdown_preview.workspace = true
|
||||||
notifications.workspace = true
|
|
||||||
project.workspace = true
|
|
||||||
release_channel.workspace = true
|
release_channel.workspace = true
|
||||||
semver.workspace = true
|
semver.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
settings.workspace = true
|
|
||||||
smol.workspace = true
|
smol.workspace = true
|
||||||
telemetry.workspace = true
|
telemetry.workspace = true
|
||||||
ui.workspace = true
|
ui.workspace = true
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,6 @@ use notifications::status_toast::StatusToast;
|
||||||
use release_channel::{AppVersion, ReleaseChannel};
|
use release_channel::{AppVersion, ReleaseChannel};
|
||||||
use semver::Version;
|
use semver::Version;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use settings::Settings as _;
|
|
||||||
use smol::io::AsyncReadExt;
|
use smol::io::AsyncReadExt;
|
||||||
use ui::{AnnouncementToast, ListBulletItem, ParallelAgentsIllustration, prelude::*};
|
use ui::{AnnouncementToast, ListBulletItem, ParallelAgentsIllustration, prelude::*};
|
||||||
use util::{ResultExt as _, maybe};
|
use util::{ResultExt as _, maybe};
|
||||||
|
|
@ -205,10 +204,7 @@ fn announcement_for_version(version: &Version, cx: &App) -> Option<AnnouncementC
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if *version >= version_with_parallel_agents
|
if *version >= version_with_parallel_agents && !ParallelAgentAnnouncement::dismissed(cx) {
|
||||||
&& !ParallelAgentAnnouncement::dismissed(cx)
|
|
||||||
&& !project::DisableAiSettings::get_global(cx).disable_ai
|
|
||||||
{
|
|
||||||
let fs = <dyn Fs>::global(cx);
|
let fs = <dyn Fs>::global(cx);
|
||||||
Some(AnnouncementContent {
|
Some(AnnouncementContent {
|
||||||
heading: "Introducing Parallel Agents".into(),
|
heading: "Introducing Parallel Agents".into(),
|
||||||
|
|
|
||||||
|
|
@ -58,13 +58,8 @@ pub async fn stream_completion(
|
||||||
additional_fields.insert("thinking".to_string(), Document::from(thinking_config));
|
additional_fields.insert("thinking".to_string(), Document::from(thinking_config));
|
||||||
}
|
}
|
||||||
Some(Thinking::Adaptive { effort: _ }) => {
|
Some(Thinking::Adaptive { effort: _ }) => {
|
||||||
let thinking_config = HashMap::from([
|
let thinking_config =
|
||||||
("type".to_string(), Document::String("adaptive".to_string())),
|
HashMap::from([("type".to_string(), Document::String("adaptive".to_string()))]);
|
||||||
(
|
|
||||||
"display".to_string(),
|
|
||||||
Document::String("summarized".to_string()),
|
|
||||||
),
|
|
||||||
]);
|
|
||||||
additional_fields.insert("thinking".to_string(), Document::from(thinking_config));
|
additional_fields.insert("thinking".to_string(), Document::from(thinking_config));
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
|
|
|
||||||
|
|
@ -740,21 +740,6 @@ impl UserStore {
|
||||||
.get(¤t_organization.id)
|
.get(¤t_organization.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
|
||||||
pub fn set_current_organization_configuration_for_test(
|
|
||||||
&mut self,
|
|
||||||
organization: Arc<Organization>,
|
|
||||||
configuration: OrganizationConfiguration,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) {
|
|
||||||
self.current_organization = Some(organization.clone());
|
|
||||||
self.organizations = vec![organization.clone()];
|
|
||||||
self.configuration_by_organization
|
|
||||||
.insert(organization.id.clone(), configuration);
|
|
||||||
cx.emit(Event::OrganizationChanged);
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn plan(&self) -> Option<Plan> {
|
pub fn plan(&self) -> Option<Plan> {
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
if let Ok(plan) = std::env::var("ZED_SIMULATE_PLAN").as_ref() {
|
if let Ok(plan) = std::env::var("ZED_SIMULATE_PLAN").as_ref() {
|
||||||
|
|
|
||||||
|
|
@ -5299,7 +5299,6 @@ async fn test_project_search(
|
||||||
"Unexpectedly reached search limit in tests. If you do want to assert limit-reached, change this panic call."
|
"Unexpectedly reached search limit in tests. If you do want to assert limit-reached, change this panic call."
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
SearchResult::WaitingForScan => {}
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -336,28 +336,17 @@ fn is_missing_action(name: &str) -> bool {
|
||||||
actions_available() && find_action_by_name(name).is_none()
|
actions_available() && find_action_by_name(name).is_none()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find the last binding (in keymap order) for the given action.
|
// Find the binding in reverse order, as the last binding takes precedence.
|
||||||
// Exact action matches are preferred over parameterized variants.
|
|
||||||
fn find_binding_in_keymap(keymap: &KeymapFile, action: &str) -> Option<String> {
|
fn find_binding_in_keymap(keymap: &KeymapFile, action: &str) -> Option<String> {
|
||||||
let find = |predicate: &dyn Fn(&str) -> bool| {
|
keymap.sections().rev().find_map(|section| {
|
||||||
keymap.sections().rev().find_map(|section| {
|
section.bindings().rev().find_map(|(keystroke, a)| {
|
||||||
section.bindings().rev().find_map(|(keystroke, a)| {
|
if name_for_action(a.to_string()) == action {
|
||||||
if predicate(&a.to_string()) {
|
Some(keystroke.to_string())
|
||||||
Some(keystroke.to_string())
|
} else {
|
||||||
} else {
|
None
|
||||||
None
|
}
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
};
|
})
|
||||||
|
|
||||||
// Look for exact match
|
|
||||||
if let Some(binding) = find(&|a| a == action) {
|
|
||||||
return Some(binding);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Look for parameterized match
|
|
||||||
find(&|a| name_for_action(a.to_string()) == action)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn find_binding(os: Os, action: &str) -> Option<String> {
|
fn find_binding(os: Os, action: &str) -> Option<String> {
|
||||||
|
|
@ -883,68 +872,3 @@ fn keymap_schema_for_actions(
|
||||||
&deprecation_messages,
|
&deprecation_messages,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use serde_json::json;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_find_binding_prefers_exact_match_over_parameterized() {
|
|
||||||
let keymap: KeymapFile = serde_json::from_value(json!([
|
|
||||||
{
|
|
||||||
"bindings": {
|
|
||||||
"ctrl-tab": "agents_sidebar::ToggleThreadSwitcher",
|
|
||||||
"ctrl-shift-tab": ["agents_sidebar::ToggleThreadSwitcher", { "select_last": true }]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]))
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let binding = find_binding_in_keymap(&keymap, "agents_sidebar::ToggleThreadSwitcher");
|
|
||||||
assert_eq!(binding.as_deref(), Some("ctrl-tab"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_find_binding_falls_back_to_parameterized_match() {
|
|
||||||
let keymap: KeymapFile = serde_json::from_value(json!([
|
|
||||||
{
|
|
||||||
"bindings": {
|
|
||||||
"ctrl-shift-tab": ["agents_sidebar::ToggleThreadSwitcher", { "select_last": true }]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]))
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let binding = find_binding_in_keymap(&keymap, "agents_sidebar::ToggleThreadSwitcher");
|
|
||||||
assert_eq!(binding.as_deref(), Some("ctrl-shift-tab"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_find_binding_prefers_exact_match_regardless_of_order() {
|
|
||||||
let keymap: KeymapFile = serde_json::from_value(json!([
|
|
||||||
{
|
|
||||||
"bindings": {
|
|
||||||
"ctrl-shift-tab": ["agents_sidebar::ToggleThreadSwitcher", { "select_last": true }],
|
|
||||||
"ctrl-tab": "agents_sidebar::ToggleThreadSwitcher"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]))
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let binding = find_binding_in_keymap(&keymap, "agents_sidebar::ToggleThreadSwitcher");
|
|
||||||
assert_eq!(binding.as_deref(), Some("ctrl-tab"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_find_binding_later_section_overrides_earlier() {
|
|
||||||
let keymap: KeymapFile = serde_json::from_value(json!([
|
|
||||||
{ "bindings": { "ctrl-a": "some::Action" } },
|
|
||||||
{ "bindings": { "ctrl-b": "some::Action" } }
|
|
||||||
]))
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let binding = find_binding_in_keymap(&keymap, "some::Action");
|
|
||||||
assert_eq!(binding.as_deref(), Some("ctrl-b"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -40,8 +40,7 @@ use release_channel::AppVersion;
|
||||||
use semver::Version;
|
use semver::Version;
|
||||||
use serde::de::DeserializeOwned;
|
use serde::de::DeserializeOwned;
|
||||||
use settings::{
|
use settings::{
|
||||||
EditPredictionDataCollectionChoice, EditPredictionPromptFormat, EditPredictionProvider,
|
EditPredictionPromptFormat, EditPredictionProvider, Settings as _, update_settings_file,
|
||||||
Settings as _, update_settings_file,
|
|
||||||
};
|
};
|
||||||
use std::collections::{VecDeque, hash_map};
|
use std::collections::{VecDeque, hash_map};
|
||||||
use std::env;
|
use std::env;
|
||||||
|
|
@ -152,7 +151,7 @@ pub struct EditPredictionStore {
|
||||||
preferred_experiment: Option<String>,
|
preferred_experiment: Option<String>,
|
||||||
available_experiments: Vec<String>,
|
available_experiments: Vec<String>,
|
||||||
pub mercury: Mercury,
|
pub mercury: Mercury,
|
||||||
legacy_data_collection_enabled: bool,
|
data_collection_choice: DataCollectionChoice,
|
||||||
reject_predictions_tx: mpsc::UnboundedSender<EditPredictionRejectionPayload>,
|
reject_predictions_tx: mpsc::UnboundedSender<EditPredictionRejectionPayload>,
|
||||||
settled_predictions_tx: mpsc::UnboundedSender<Instant>,
|
settled_predictions_tx: mpsc::UnboundedSender<Instant>,
|
||||||
shown_predictions: VecDeque<EditPrediction>,
|
shown_predictions: VecDeque<EditPrediction>,
|
||||||
|
|
@ -758,8 +757,9 @@ impl EditPredictionStore {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new(client: Arc<Client>, user_store: Entity<UserStore>, cx: &mut Context<Self>) -> Self {
|
pub fn new(client: Arc<Client>, user_store: Entity<UserStore>, cx: &mut Context<Self>) -> Self {
|
||||||
|
let data_collection_choice = Self::load_data_collection_choice(cx);
|
||||||
|
|
||||||
let llm_token = global_llm_token(cx);
|
let llm_token = global_llm_token(cx);
|
||||||
let legacy_data_collection_enabled = Self::load_legacy_data_collection_enabled(cx);
|
|
||||||
|
|
||||||
let (reject_tx, reject_rx) = mpsc::unbounded();
|
let (reject_tx, reject_rx) = mpsc::unbounded();
|
||||||
cx.background_spawn({
|
cx.background_spawn({
|
||||||
|
|
@ -814,8 +814,8 @@ impl EditPredictionStore {
|
||||||
preferred_experiment: None,
|
preferred_experiment: None,
|
||||||
available_experiments: Vec::new(),
|
available_experiments: Vec::new(),
|
||||||
mercury: Mercury::new(cx),
|
mercury: Mercury::new(cx),
|
||||||
legacy_data_collection_enabled,
|
|
||||||
|
|
||||||
|
data_collection_choice,
|
||||||
reject_predictions_tx: reject_tx,
|
reject_predictions_tx: reject_tx,
|
||||||
settled_predictions_tx,
|
settled_predictions_tx,
|
||||||
rated_predictions: Default::default(),
|
rated_predictions: Default::default(),
|
||||||
|
|
@ -2770,45 +2770,38 @@ impl EditPredictionStore {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn is_data_collection_enabled(&self, cx: &App) -> bool {
|
pub(crate) fn is_data_collection_enabled(&self, cx: &App) -> bool {
|
||||||
if !self.is_data_collection_allowed_by_organization(cx) {
|
self.data_collection_choice.is_enabled(cx)
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if cx.is_staff() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
match all_language_settings(None, cx)
|
|
||||||
.edit_predictions
|
|
||||||
.allow_data_collection
|
|
||||||
{
|
|
||||||
EditPredictionDataCollectionChoice::Yes => true,
|
|
||||||
EditPredictionDataCollectionChoice::No => false,
|
|
||||||
// Fall back to the legacy KV entry captured when the store was
|
|
||||||
// created, preserving existing users' choices without per-request
|
|
||||||
// database reads.
|
|
||||||
EditPredictionDataCollectionChoice::Default => self.legacy_data_collection_enabled,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_legacy_data_collection_enabled(cx: &App) -> bool {
|
fn load_data_collection_choice(cx: &App) -> DataCollectionChoice {
|
||||||
KeyValueStore::global(cx)
|
let choice = KeyValueStore::global(cx)
|
||||||
.read_kvp(ZED_PREDICT_DATA_COLLECTION_CHOICE)
|
.read_kvp(ZED_PREDICT_DATA_COLLECTION_CHOICE)
|
||||||
.log_err()
|
.log_err()
|
||||||
.flatten()
|
.flatten();
|
||||||
.as_deref()
|
|
||||||
== Some("true")
|
match choice.as_deref() {
|
||||||
|
Some("true") => DataCollectionChoice::Enabled,
|
||||||
|
Some("false") => DataCollectionChoice::Disabled,
|
||||||
|
Some(_) => {
|
||||||
|
log::error!("unknown value in '{ZED_PREDICT_DATA_COLLECTION_CHOICE}'");
|
||||||
|
DataCollectionChoice::NotAnswered
|
||||||
|
}
|
||||||
|
None => DataCollectionChoice::NotAnswered,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn is_data_collection_allowed_by_organization(&self, cx: &App) -> bool {
|
fn toggle_data_collection_choice(&mut self, cx: &mut Context<Self>) {
|
||||||
self.user_store
|
self.data_collection_choice = self.data_collection_choice.toggle();
|
||||||
.read(cx)
|
let new_choice = self.data_collection_choice;
|
||||||
.current_organization_configuration()
|
let is_enabled = new_choice.is_enabled(cx);
|
||||||
.is_none_or(|organization_configuration| {
|
let kvp = KeyValueStore::global(cx);
|
||||||
organization_configuration
|
db::write_and_log(cx, move || async move {
|
||||||
.edit_prediction
|
kvp.write_kvp(
|
||||||
.is_feedback_enabled
|
ZED_PREDICT_DATA_COLLECTION_CHOICE.into(),
|
||||||
})
|
is_enabled.to_string(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn shown_predictions(&self) -> impl DoubleEndedIterator<Item = &EditPrediction> {
|
pub fn shown_predictions(&self) -> impl DoubleEndedIterator<Item = &EditPrediction> {
|
||||||
|
|
@ -3001,37 +2994,70 @@ pub struct ZedUpdateRequiredError {
|
||||||
minimum_version: Version,
|
minimum_version: Version,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ZedPredictUpsell;
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub enum DataCollectionChoice {
|
||||||
|
NotAnswered,
|
||||||
|
Enabled,
|
||||||
|
Disabled,
|
||||||
|
}
|
||||||
|
|
||||||
fn is_upsell_dismissed(cx: &App) -> bool {
|
impl DataCollectionChoice {
|
||||||
// To make this backwards compatible with older versions of Zed, we
|
pub fn is_enabled(self, cx: &App) -> bool {
|
||||||
// check if the user has seen the previous Edit Prediction Onboarding
|
if cx.is_staff() {
|
||||||
// before, by checking the data collection choice which was written to
|
return true;
|
||||||
// the database once the user clicked on "Accept and Enable"
|
}
|
||||||
let kvp = KeyValueStore::global(cx);
|
match self {
|
||||||
if kvp
|
Self::Enabled => true,
|
||||||
.read_kvp(ZED_PREDICT_DATA_COLLECTION_CHOICE)
|
Self::NotAnswered | Self::Disabled => false,
|
||||||
.log_err()
|
}
|
||||||
.is_some_and(|s| s.is_some())
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
kvp.read_kvp(ZedPredictUpsell::KEY)
|
#[must_use]
|
||||||
.log_err()
|
pub fn toggle(&self) -> DataCollectionChoice {
|
||||||
.is_some_and(|s| s.is_some())
|
match self {
|
||||||
|
Self::Enabled => Self::Disabled,
|
||||||
|
Self::Disabled => Self::Enabled,
|
||||||
|
Self::NotAnswered => Self::Enabled,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<bool> for DataCollectionChoice {
|
||||||
|
fn from(value: bool) -> Self {
|
||||||
|
match value {
|
||||||
|
true => DataCollectionChoice::Enabled,
|
||||||
|
false => DataCollectionChoice::Disabled,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ZedPredictUpsell;
|
||||||
|
|
||||||
impl Dismissable for ZedPredictUpsell {
|
impl Dismissable for ZedPredictUpsell {
|
||||||
const KEY: &'static str = "dismissed-edit-predict-upsell";
|
const KEY: &'static str = "dismissed-edit-predict-upsell";
|
||||||
|
|
||||||
fn dismissed(cx: &App) -> bool {
|
fn dismissed(cx: &App) -> bool {
|
||||||
is_upsell_dismissed(cx)
|
// To make this backwards compatible with older versions of Zed, we
|
||||||
|
// check if the user has seen the previous Edit Prediction Onboarding
|
||||||
|
// before, by checking the data collection choice which was written to
|
||||||
|
// the database once the user clicked on "Accept and Enable"
|
||||||
|
let kvp = KeyValueStore::global(cx);
|
||||||
|
if kvp
|
||||||
|
.read_kvp(ZED_PREDICT_DATA_COLLECTION_CHOICE)
|
||||||
|
.log_err()
|
||||||
|
.is_some_and(|s| s.is_some())
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
kvp.read_kvp(Self::KEY)
|
||||||
|
.log_err()
|
||||||
|
.is_some_and(|s| s.is_some())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn should_show_upsell_modal(cx: &App) -> bool {
|
pub fn should_show_upsell_modal(cx: &App) -> bool {
|
||||||
!is_upsell_dismissed(cx)
|
!ZedPredictUpsell::dismissed(cx)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn init(cx: &mut App) {
|
pub fn init(cx: &mut App) {
|
||||||
|
|
|
||||||
|
|
@ -3,16 +3,11 @@ use crate::udiff::apply_diff_to_string;
|
||||||
use client::{RefreshLlmTokenListener, UserStore, test::FakeServer};
|
use client::{RefreshLlmTokenListener, UserStore, test::FakeServer};
|
||||||
use clock::FakeSystemClock;
|
use clock::FakeSystemClock;
|
||||||
use clock::ReplicaId;
|
use clock::ReplicaId;
|
||||||
use cloud_api_types::{
|
use cloud_api_types::{CreateLlmTokenResponse, LlmToken};
|
||||||
CreateLlmTokenResponse, LlmToken, Organization, OrganizationConfiguration,
|
|
||||||
OrganizationEditPredictionConfiguration, OrganizationId,
|
|
||||||
};
|
|
||||||
use cloud_llm_client::{
|
use cloud_llm_client::{
|
||||||
EditPredictionRejectReason, EditPredictionRejection, RejectEditPredictionsBody,
|
EditPredictionRejectReason, EditPredictionRejection, RejectEditPredictionsBody,
|
||||||
predict_edits_v3::{PredictEditsV3Request, PredictEditsV3Response},
|
predict_edits_v3::{PredictEditsV3Request, PredictEditsV3Response},
|
||||||
};
|
};
|
||||||
use db::AppDatabase;
|
|
||||||
use settings::EditPredictionDataCollectionChoice;
|
|
||||||
|
|
||||||
use futures::{
|
use futures::{
|
||||||
AsyncReadExt, FutureExt, StreamExt,
|
AsyncReadExt, FutureExt, StreamExt,
|
||||||
|
|
@ -2395,31 +2390,12 @@ struct RequestChannels {
|
||||||
|
|
||||||
fn init_test_with_fake_client(
|
fn init_test_with_fake_client(
|
||||||
cx: &mut TestAppContext,
|
cx: &mut TestAppContext,
|
||||||
) -> (Entity<EditPredictionStore>, RequestChannels) {
|
|
||||||
init_test_with_fake_client_and_legacy_data_collection(cx, None)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn init_test_with_fake_client_and_legacy_data_collection(
|
|
||||||
cx: &mut TestAppContext,
|
|
||||||
legacy_data_collection_choice: Option<&str>,
|
|
||||||
) -> (Entity<EditPredictionStore>, RequestChannels) {
|
) -> (Entity<EditPredictionStore>, RequestChannels) {
|
||||||
cx.update(move |cx| {
|
cx.update(move |cx| {
|
||||||
cx.set_global(AppDatabase::test_new());
|
|
||||||
let settings_store = SettingsStore::test(cx);
|
let settings_store = SettingsStore::test(cx);
|
||||||
cx.set_global(settings_store);
|
cx.set_global(settings_store);
|
||||||
zlog::init_test();
|
zlog::init_test();
|
||||||
|
|
||||||
if let Some(legacy_data_collection_choice) = legacy_data_collection_choice {
|
|
||||||
KeyValueStore::global(cx)
|
|
||||||
.write_kvp(
|
|
||||||
ZED_PREDICT_DATA_COLLECTION_CHOICE.into(),
|
|
||||||
legacy_data_collection_choice.to_string(),
|
|
||||||
)
|
|
||||||
.now_or_never()
|
|
||||||
.expect("legacy data collection write should complete immediately")
|
|
||||||
.expect("legacy data collection write should succeed");
|
|
||||||
}
|
|
||||||
|
|
||||||
let (predict_req_tx, predict_req_rx) = mpsc::unbounded();
|
let (predict_req_tx, predict_req_rx) = mpsc::unbounded();
|
||||||
let (reject_req_tx, reject_req_rx) = mpsc::unbounded();
|
let (reject_req_tx, reject_req_rx) = mpsc::unbounded();
|
||||||
|
|
||||||
|
|
@ -2796,7 +2772,6 @@ async fn test_v3_prediction_strips_cursor_marker_from_edit_text(cx: &mut TestApp
|
||||||
|
|
||||||
fn init_test(cx: &mut TestAppContext) {
|
fn init_test(cx: &mut TestAppContext) {
|
||||||
cx.update(|cx| {
|
cx.update(|cx| {
|
||||||
cx.set_global(AppDatabase::test_new());
|
|
||||||
let settings_store = SettingsStore::test(cx);
|
let settings_store = SettingsStore::test(cx);
|
||||||
cx.set_global(settings_store);
|
cx.set_global(settings_store);
|
||||||
});
|
});
|
||||||
|
|
@ -3417,252 +3392,6 @@ async fn test_edit_prediction_settled(cx: &mut TestAppContext) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
|
||||||
async fn test_data_collection_disabled_by_default(cx: &mut TestAppContext) {
|
|
||||||
let (ep_store, _channels) = init_test_with_fake_client(cx);
|
|
||||||
|
|
||||||
cx.update(|cx| {
|
|
||||||
assert!(!ep_store.read(cx).is_data_collection_enabled(cx));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#[gpui::test]
|
|
||||||
async fn test_data_collection_enabled_via_legacy_kv_store(cx: &mut TestAppContext) {
|
|
||||||
let (ep_store, _channels) =
|
|
||||||
init_test_with_fake_client_and_legacy_data_collection(cx, Some("true"));
|
|
||||||
|
|
||||||
cx.update(|cx| {
|
|
||||||
assert!(ep_store.read(cx).is_data_collection_enabled(cx));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#[gpui::test]
|
|
||||||
async fn test_data_collection_default_uses_cached_legacy_value(cx: &mut TestAppContext) {
|
|
||||||
let (ep_store, _channels) =
|
|
||||||
init_test_with_fake_client_and_legacy_data_collection(cx, Some("true"));
|
|
||||||
|
|
||||||
cx.update(|cx| {
|
|
||||||
assert!(ep_store.read(cx).is_data_collection_enabled(cx));
|
|
||||||
});
|
|
||||||
|
|
||||||
cx.update(|cx| KeyValueStore::global(cx))
|
|
||||||
.delete_kvp(ZED_PREDICT_DATA_COLLECTION_CHOICE.into())
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
cx.update(|cx| {
|
|
||||||
assert!(ep_store.read(cx).is_data_collection_enabled(cx));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#[gpui::test]
|
|
||||||
async fn test_data_collection_setting_overrides_kv_store(cx: &mut TestAppContext) {
|
|
||||||
let (ep_store, _channels) =
|
|
||||||
init_test_with_fake_client_and_legacy_data_collection(cx, Some("true"));
|
|
||||||
|
|
||||||
// An explicit false in settings.json wins over the KV store.
|
|
||||||
cx.update_global::<SettingsStore, _>(|settings, cx| {
|
|
||||||
settings.update_user_settings(cx, |content| {
|
|
||||||
content
|
|
||||||
.project
|
|
||||||
.all_languages
|
|
||||||
.edit_predictions
|
|
||||||
.get_or_insert_default()
|
|
||||||
.allow_data_collection = Some(EditPredictionDataCollectionChoice::No);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
cx.update(|cx| {
|
|
||||||
assert!(!ep_store.read(cx).is_data_collection_enabled(cx));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#[gpui::test]
|
|
||||||
async fn test_data_collection_enabled_via_setting(cx: &mut TestAppContext) {
|
|
||||||
let (ep_store, _channels) = init_test_with_fake_client(cx);
|
|
||||||
|
|
||||||
cx.update_global::<SettingsStore, _>(|settings, cx| {
|
|
||||||
settings.update_user_settings(cx, |content| {
|
|
||||||
content
|
|
||||||
.project
|
|
||||||
.all_languages
|
|
||||||
.edit_predictions
|
|
||||||
.get_or_insert_default()
|
|
||||||
.allow_data_collection = Some(EditPredictionDataCollectionChoice::Yes);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
cx.update(|cx| {
|
|
||||||
assert!(ep_store.read(cx).is_data_collection_enabled(cx));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#[gpui::test]
|
|
||||||
async fn test_data_collection_always_enabled_for_staff(cx: &mut TestAppContext) {
|
|
||||||
let (ep_store, _channels) = init_test_with_fake_client(cx);
|
|
||||||
|
|
||||||
cx.update(|cx| {
|
|
||||||
cx.set_staff(true);
|
|
||||||
assert!(ep_store.read(cx).is_data_collection_enabled(cx));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#[gpui::test]
|
|
||||||
async fn test_data_collection_disabled_by_organization_configuration(cx: &mut TestAppContext) {
|
|
||||||
let (ep_store, _channels) = init_test_with_fake_client(cx);
|
|
||||||
|
|
||||||
cx.update_global::<SettingsStore, _>(|settings, cx| {
|
|
||||||
settings.update_user_settings(cx, |content| {
|
|
||||||
content
|
|
||||||
.project
|
|
||||||
.all_languages
|
|
||||||
.edit_predictions
|
|
||||||
.get_or_insert_default()
|
|
||||||
.allow_data_collection = Some(EditPredictionDataCollectionChoice::Yes);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
let user_store = cx.update(|cx| ep_store.read(cx).user_store.clone());
|
|
||||||
cx.update(|cx| {
|
|
||||||
user_store.update(cx, |user_store, cx| {
|
|
||||||
user_store.set_current_organization_configuration_for_test(
|
|
||||||
Arc::new(Organization {
|
|
||||||
id: OrganizationId("org-1".into()),
|
|
||||||
name: "Org 1".into(),
|
|
||||||
is_personal: false,
|
|
||||||
}),
|
|
||||||
OrganizationConfiguration {
|
|
||||||
is_zed_model_provider_enabled: true,
|
|
||||||
is_agent_thread_feedback_enabled: true,
|
|
||||||
is_collaboration_enabled: true,
|
|
||||||
edit_prediction: OrganizationEditPredictionConfiguration {
|
|
||||||
is_enabled: true,
|
|
||||||
is_feedback_enabled: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
assert!(!ep_store.read(cx).is_data_collection_enabled(cx));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// When a user had data collection enabled via the legacy KV store (with no explicit
|
|
||||||
// setting in settings.json), toggle_data_collection must read the *resolved* state
|
|
||||||
// (true) and write Some(false).
|
|
||||||
#[gpui::test]
|
|
||||||
async fn test_toggle_data_collection_from_kv_enabled_state(cx: &mut TestAppContext) {
|
|
||||||
let (ep_store, _channels) =
|
|
||||||
init_test_with_fake_client_and_legacy_data_collection(cx, Some("true"));
|
|
||||||
|
|
||||||
cx.update(|cx| {
|
|
||||||
assert!(
|
|
||||||
ep_store.read(cx).is_data_collection_enabled(cx),
|
|
||||||
"data collection should be enabled via KV store before toggle"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Simulate what toggle_data_collection does: capture the resolved current
|
|
||||||
// state, then write its inverse.
|
|
||||||
let is_currently_enabled = cx.update(|cx| ep_store.read(cx).is_data_collection_enabled(cx));
|
|
||||||
cx.update_global::<SettingsStore, _>(|settings, cx| {
|
|
||||||
settings.update_user_settings(cx, |content| {
|
|
||||||
content
|
|
||||||
.project
|
|
||||||
.all_languages
|
|
||||||
.edit_predictions
|
|
||||||
.get_or_insert_default()
|
|
||||||
.allow_data_collection = Some(if is_currently_enabled {
|
|
||||||
EditPredictionDataCollectionChoice::No
|
|
||||||
} else {
|
|
||||||
EditPredictionDataCollectionChoice::Yes
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
cx.update(|cx| {
|
|
||||||
assert!(
|
|
||||||
!ep_store.read(cx).is_data_collection_enabled(cx),
|
|
||||||
"data collection should be disabled after toggling off from KV-enabled state"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#[gpui::test]
|
|
||||||
async fn test_upsell_shown_by_default(cx: &mut TestAppContext) {
|
|
||||||
init_test(cx);
|
|
||||||
let kvp = cx.update(|cx| KeyValueStore::global(cx));
|
|
||||||
kvp.delete_kvp(ZED_PREDICT_DATA_COLLECTION_CHOICE.into())
|
|
||||||
.await
|
|
||||||
.ok();
|
|
||||||
kvp.delete_kvp(ZedPredictUpsell::KEY.into()).await.ok();
|
|
||||||
|
|
||||||
cx.update(|cx| assert!(should_show_upsell_modal(cx)));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[gpui::test]
|
|
||||||
async fn test_upsell_dismissed_when_data_collection_choice_in_kv_store(cx: &mut TestAppContext) {
|
|
||||||
init_test(cx);
|
|
||||||
|
|
||||||
// Any value for the data collection key means the old upsell was already
|
|
||||||
// shown, regardless of whether data collection was accepted or declined.
|
|
||||||
for value in &["true", "false"] {
|
|
||||||
cx.update(|cx| KeyValueStore::global(cx))
|
|
||||||
.write_kvp(ZED_PREDICT_DATA_COLLECTION_CHOICE.into(), value.to_string())
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
cx.update(|cx| {
|
|
||||||
assert!(
|
|
||||||
!should_show_upsell_modal(cx),
|
|
||||||
"upsell should be suppressed when data collection choice is '{value}'"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
cx.update(|cx| KeyValueStore::global(cx))
|
|
||||||
.delete_kvp(ZED_PREDICT_DATA_COLLECTION_CHOICE.into())
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[gpui::test]
|
|
||||||
async fn test_upsell_dismissed_when_dismissed_key_set(cx: &mut TestAppContext) {
|
|
||||||
init_test(cx);
|
|
||||||
let kvp = cx.update(|cx| KeyValueStore::global(cx));
|
|
||||||
kvp.delete_kvp(ZED_PREDICT_DATA_COLLECTION_CHOICE.into())
|
|
||||||
.await
|
|
||||||
.ok();
|
|
||||||
kvp.write_kvp(ZedPredictUpsell::KEY.into(), "1".into())
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
cx.update(|cx| assert!(!should_show_upsell_modal(cx)));
|
|
||||||
|
|
||||||
kvp.delete_kvp(ZedPredictUpsell::KEY.into()).await.unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[gpui::test]
|
|
||||||
async fn test_upsell_dismissed_via_dismissable_api(cx: &mut TestAppContext) {
|
|
||||||
init_test(cx);
|
|
||||||
let kvp = cx.update(|cx| KeyValueStore::global(cx));
|
|
||||||
kvp.delete_kvp(ZED_PREDICT_DATA_COLLECTION_CHOICE.into())
|
|
||||||
.await
|
|
||||||
.ok();
|
|
||||||
kvp.delete_kvp(ZedPredictUpsell::KEY.into()).await.ok();
|
|
||||||
|
|
||||||
cx.update(|cx| {
|
|
||||||
assert!(should_show_upsell_modal(cx));
|
|
||||||
ZedPredictUpsell::set_dismissed(true, cx);
|
|
||||||
});
|
|
||||||
cx.run_until_parked();
|
|
||||||
|
|
||||||
cx.update(|cx| assert!(!should_show_upsell_modal(cx)));
|
|
||||||
|
|
||||||
kvp.delete_kvp(ZedPredictUpsell::KEY.into()).await.unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[ctor::ctor]
|
#[ctor::ctor]
|
||||||
fn init_logger() {
|
fn init_logger() {
|
||||||
zlog::init_test();
|
zlog::init_test();
|
||||||
|
|
|
||||||
|
|
@ -7,11 +7,9 @@ use edit_prediction_types::{
|
||||||
EditPredictionIconSet, SuggestionDisplayType,
|
EditPredictionIconSet, SuggestionDisplayType,
|
||||||
};
|
};
|
||||||
use feature_flags::FeatureFlagAppExt;
|
use feature_flags::FeatureFlagAppExt;
|
||||||
use fs::Fs;
|
|
||||||
use gpui::{App, Entity, prelude::*};
|
use gpui::{App, Entity, prelude::*};
|
||||||
use language::{Buffer, ToPoint as _};
|
use language::{Buffer, ToPoint as _};
|
||||||
use project::Project;
|
use project::Project;
|
||||||
use settings::{EditPredictionDataCollectionChoice, update_settings_file};
|
|
||||||
|
|
||||||
use crate::{BufferEditPrediction, EditPredictionStore};
|
use crate::{BufferEditPrediction, EditPredictionStore};
|
||||||
|
|
||||||
|
|
@ -77,7 +75,24 @@ impl EditPredictionDelegate for ZedEditPredictionDelegate {
|
||||||
.read(cx)
|
.read(cx)
|
||||||
.is_file_open_source(&self.project, file, cx);
|
.is_file_open_source(&self.project, file, cx);
|
||||||
|
|
||||||
if self.store.read(cx).is_data_collection_enabled(cx) {
|
if let Some(organization_configuration) = self
|
||||||
|
.store
|
||||||
|
.read(cx)
|
||||||
|
.user_store
|
||||||
|
.read(cx)
|
||||||
|
.current_organization_configuration()
|
||||||
|
{
|
||||||
|
if !organization_configuration
|
||||||
|
.edit_prediction
|
||||||
|
.is_feedback_enabled
|
||||||
|
{
|
||||||
|
return DataCollectionState::Disabled {
|
||||||
|
is_project_open_source,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.store.read(cx).data_collection_choice.is_enabled(cx) {
|
||||||
DataCollectionState::Enabled {
|
DataCollectionState::Enabled {
|
||||||
is_project_open_source,
|
is_project_open_source,
|
||||||
}
|
}
|
||||||
|
|
@ -87,9 +102,9 @@ impl EditPredictionDelegate for ZedEditPredictionDelegate {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
DataCollectionState::Disabled {
|
return DataCollectionState::Disabled {
|
||||||
is_project_open_source: false,
|
is_project_open_source: false,
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -98,26 +113,27 @@ impl EditPredictionDelegate for ZedEditPredictionDelegate {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.store
|
if let Some(organization_configuration) = self
|
||||||
|
.store
|
||||||
.read(cx)
|
.read(cx)
|
||||||
.is_data_collection_allowed_by_organization(cx)
|
.user_store
|
||||||
|
.read(cx)
|
||||||
|
.current_organization_configuration()
|
||||||
|
{
|
||||||
|
if !organization_configuration
|
||||||
|
.edit_prediction
|
||||||
|
.is_feedback_enabled
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
fn toggle_data_collection(&mut self, cx: &mut App) {
|
fn toggle_data_collection(&mut self, cx: &mut App) {
|
||||||
let fs = <dyn Fs>::global(cx);
|
self.store.update(cx, |store, cx| {
|
||||||
let is_currently_enabled = self.store.read(cx).is_data_collection_enabled(cx);
|
store.toggle_data_collection_choice(cx);
|
||||||
update_settings_file(fs, cx, move |settings, _| {
|
|
||||||
let edit_predictions = settings
|
|
||||||
.project
|
|
||||||
.all_languages
|
|
||||||
.edit_predictions
|
|
||||||
.get_or_insert_default();
|
|
||||||
|
|
||||||
edit_predictions.allow_data_collection = Some(if is_currently_enabled {
|
|
||||||
EditPredictionDataCollectionChoice::No
|
|
||||||
} else {
|
|
||||||
EditPredictionDataCollectionChoice::Yes
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -289,11 +289,6 @@ impl RelatedExcerptStore {
|
||||||
.ok()?;
|
.ok()?;
|
||||||
let type_definitions = project
|
let type_definitions = project
|
||||||
.update(cx, |project, cx| {
|
.update(cx, |project, cx| {
|
||||||
// tombi LSP for toml will open a scratch buffer with the JSON schema of
|
|
||||||
// the toml file when a goto type definition is requested
|
|
||||||
if is_tombi_lsp_in_toml(project, &buffer, cx) {
|
|
||||||
return Task::ready(Ok(None));
|
|
||||||
}
|
|
||||||
project.type_definitions(&buffer, identifier.range.start, cx)
|
project.type_definitions(&buffer, identifier.range.start, cx)
|
||||||
})
|
})
|
||||||
.ok()?;
|
.ok()?;
|
||||||
|
|
@ -568,7 +563,7 @@ impl RelatedBuffer {
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
self.cached_file = Some(CachedRelatedFile {
|
self.cached_file = Some(CachedRelatedFile {
|
||||||
excerpts,
|
excerpts: excerpts,
|
||||||
buffer_version: buffer.version().clone(),
|
buffer_version: buffer.version().clone(),
|
||||||
});
|
});
|
||||||
self.cached_file.as_ref().unwrap()
|
self.cached_file.as_ref().unwrap()
|
||||||
|
|
@ -674,7 +669,6 @@ fn identifiers_for_position(
|
||||||
if let Some(config) = config
|
if let Some(config) = config
|
||||||
&& config.identifier_capture_indices.contains(&capture.index)
|
&& config.identifier_capture_indices.contains(&capture.index)
|
||||||
&& range.contains_inclusive(&node_range)
|
&& range.contains_inclusive(&node_range)
|
||||||
&& !is_tsx_tag(&buffer, &capture.node)
|
|
||||||
&& Some(&node_range) != last_range.as_ref()
|
&& Some(&node_range) != last_range.as_ref()
|
||||||
{
|
{
|
||||||
let name = buffer.text_for_range(node_range.clone()).collect();
|
let name = buffer.text_for_range(node_range.clone()).collect();
|
||||||
|
|
@ -692,59 +686,3 @@ fn identifiers_for_position(
|
||||||
|
|
||||||
identifiers
|
identifiers
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_tsx_tag(buffer: &BufferSnapshot, node: &tree_sitter::Node) -> bool {
|
|
||||||
let Some(language_config) = buffer
|
|
||||||
.language()
|
|
||||||
.and_then(|l| l.config().jsx_tag_auto_close.as_ref())
|
|
||||||
else {
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
let Some(parent_kind) = node.parent().map(|n| n.kind()) else {
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
if parent_kind != &language_config.open_tag_node_name
|
|
||||||
&& parent_kind != &language_config.close_tag_node_name
|
|
||||||
&& parent_kind != &language_config.tag_name_node_name
|
|
||||||
&& language_config
|
|
||||||
.erroneous_close_tag_name_node_name
|
|
||||||
.as_ref()
|
|
||||||
.is_some_and(|kind| parent_kind != kind)
|
|
||||||
&& language_config
|
|
||||||
.erroneous_close_tag_node_name
|
|
||||||
.as_ref()
|
|
||||||
.is_some_and(|kind| parent_kind == kind)
|
|
||||||
&& parent_kind != &language_config.jsx_element_node_name
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
// do fetch `<Component />`, model probably understands `<div>`, but needs info for user defined components
|
|
||||||
if !buffer
|
|
||||||
.text_for_range(node.byte_range())
|
|
||||||
.all(|str| str.chars().all(|c| c.is_lowercase()))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_tombi_lsp_in_toml(
|
|
||||||
project: &Project,
|
|
||||||
buffer: &Entity<Buffer>,
|
|
||||||
cx: &mut Context<Project>,
|
|
||||||
) -> bool {
|
|
||||||
buffer.update(cx, |buffer, cx| {
|
|
||||||
if !buffer.language().is_some_and(|lang| lang.name() == "TOML") {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
project.lsp_store().update(cx, |lsp_store, cx| {
|
|
||||||
for (_, lsp) in lsp_store.running_language_servers_for_local_buffer(buffer, cx) {
|
|
||||||
if "tombi".eq_ignore_ascii_case(lsp.name().as_ref()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
false
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use std::sync::Arc;
|
use std::{iter, ops::Range, sync::Arc};
|
||||||
|
|
||||||
use collections::{HashMap, HashSet};
|
use collections::{HashMap, HashSet};
|
||||||
use futures::future::join_all;
|
use futures::future::join_all;
|
||||||
|
|
@ -7,15 +7,17 @@ use itertools::Itertools;
|
||||||
use language::{BufferId, ClientCommand};
|
use language::{BufferId, ClientCommand};
|
||||||
use multi_buffer::{Anchor, MultiBufferRow, MultiBufferSnapshot, ToPoint as _};
|
use multi_buffer::{Anchor, MultiBufferRow, MultiBufferSnapshot, ToPoint as _};
|
||||||
use project::{CodeAction, TaskSourceKind};
|
use project::{CodeAction, TaskSourceKind};
|
||||||
|
use settings::Settings as _;
|
||||||
use task::TaskContext;
|
use task::TaskContext;
|
||||||
|
use text::Point;
|
||||||
|
|
||||||
use ui::{Context, Window, div, prelude::*};
|
use ui::{Context, Window, div, prelude::*};
|
||||||
|
use workspace::PreviewTabsSettings;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
Editor, LSP_REQUEST_DEBOUNCE_TIMEOUT, SelectionEffects,
|
Editor, LSP_REQUEST_DEBOUNCE_TIMEOUT, MultibufferSelectionMode, SelectionEffects,
|
||||||
actions::ToggleCodeLens,
|
actions::ToggleCodeLens,
|
||||||
display_map::{BlockPlacement, BlockProperties, BlockStyle, CustomBlockId},
|
display_map::{BlockPlacement, BlockProperties, BlockStyle, CustomBlockId},
|
||||||
hover_links::HoverLink,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
|
|
@ -76,7 +78,6 @@ fn group_lenses_by_row(
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_code_lens_line(
|
fn render_code_lens_line(
|
||||||
buffer_id: BufferId,
|
|
||||||
line_number: usize,
|
line_number: usize,
|
||||||
lens: CodeLensLine,
|
lens: CodeLensLine,
|
||||||
editor: WeakEntity<Editor>,
|
editor: WeakEntity<Editor>,
|
||||||
|
|
@ -103,11 +104,11 @@ fn render_code_lens_line(
|
||||||
let action = item.action.clone();
|
let action = item.action.clone();
|
||||||
let editor_handle = editor.clone();
|
let editor_handle = editor.clone();
|
||||||
let position = lens.position;
|
let position = lens.position;
|
||||||
let id = SharedString::from(format!("{buffer_id}:{line_number}:{i}"));
|
let id = (line_number as u64) << 32 | (i as u64);
|
||||||
|
|
||||||
children.push(
|
children.push(
|
||||||
div()
|
div()
|
||||||
.id(ElementId::Name(id))
|
.id(ElementId::Integer(id))
|
||||||
.font(font.clone())
|
.font(font.clone())
|
||||||
.text_size(font_size)
|
.text_size(font_size)
|
||||||
.text_color(cx.app.theme().colors().text_muted)
|
.text_color(cx.app.theme().colors().text_muted)
|
||||||
|
|
@ -205,7 +206,7 @@ pub(super) fn try_handle_client_command(
|
||||||
schedule_task(task_template, action, editor, workspace, window, cx)
|
schedule_task(task_template, action, editor, workspace, window, cx)
|
||||||
}
|
}
|
||||||
Some(ClientCommand::ShowLocations) => {
|
Some(ClientCommand::ShowLocations) => {
|
||||||
try_show_references(arguments, action, editor, window, cx)
|
try_show_references(arguments, action, workspace, window, cx)
|
||||||
}
|
}
|
||||||
None => false,
|
None => false,
|
||||||
}
|
}
|
||||||
|
|
@ -260,7 +261,7 @@ fn schedule_task(
|
||||||
fn try_show_references(
|
fn try_show_references(
|
||||||
arguments: &[serde_json::Value],
|
arguments: &[serde_json::Value],
|
||||||
action: &CodeAction,
|
action: &CodeAction,
|
||||||
editor: &mut Editor,
|
workspace: &gpui::Entity<workspace::Workspace>,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Editor>,
|
cx: &mut Context<Editor>,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
|
|
@ -275,18 +276,73 @@ fn try_show_references(
|
||||||
}
|
}
|
||||||
|
|
||||||
let server_id = action.server_id;
|
let server_id = action.server_id;
|
||||||
let nav_entry = editor.navigation_entry(editor.selections.newest_anchor().head(), cx);
|
let project = workspace.read(cx).project().clone();
|
||||||
let links = locations
|
let workspace = workspace.clone();
|
||||||
.into_iter()
|
|
||||||
.map(|location| HoverLink::InlayHint(location, server_id))
|
cx.spawn_in(window, async move |_editor, cx| {
|
||||||
.collect();
|
let mut buffer_locations = std::collections::HashMap::default();
|
||||||
editor
|
|
||||||
.navigate_to_hover_links(None, links, nav_entry, false, window, cx)
|
for location in &locations {
|
||||||
.detach_and_log_err(cx);
|
let open_task = cx.update(|_, cx| {
|
||||||
|
project.update(cx, |project, cx| {
|
||||||
|
let uri: lsp::Uri = location.uri.clone();
|
||||||
|
project.open_local_buffer_via_lsp(uri, server_id, cx)
|
||||||
|
})
|
||||||
|
})?;
|
||||||
|
let buffer = open_task.await?;
|
||||||
|
|
||||||
|
let range = range_from_lsp(location.range);
|
||||||
|
buffer_locations
|
||||||
|
.entry(buffer)
|
||||||
|
.or_insert_with(Vec::new)
|
||||||
|
.push(range);
|
||||||
|
}
|
||||||
|
|
||||||
|
workspace.update_in(cx, |workspace, window, cx| {
|
||||||
|
let target = buffer_locations
|
||||||
|
.iter()
|
||||||
|
.flat_map(|(k, v)| iter::repeat(k.clone()).zip(v))
|
||||||
|
.map(|(buffer, location)| {
|
||||||
|
buffer
|
||||||
|
.read(cx)
|
||||||
|
.text_for_range(location.clone())
|
||||||
|
.collect::<String>()
|
||||||
|
})
|
||||||
|
.filter(|text| !text.contains('\n'))
|
||||||
|
.unique()
|
||||||
|
.take(3)
|
||||||
|
.join(", ");
|
||||||
|
let title = if target.is_empty() {
|
||||||
|
"References".to_owned()
|
||||||
|
} else {
|
||||||
|
format!("References to {target}")
|
||||||
|
};
|
||||||
|
let allow_preview =
|
||||||
|
PreviewTabsSettings::get_global(cx).enable_preview_multibuffer_from_code_navigation;
|
||||||
|
Editor::open_locations_in_multibuffer(
|
||||||
|
workspace,
|
||||||
|
buffer_locations,
|
||||||
|
title,
|
||||||
|
false,
|
||||||
|
allow_preview,
|
||||||
|
MultibufferSelectionMode::First,
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
})?;
|
||||||
|
anyhow::Ok(())
|
||||||
|
})
|
||||||
|
.detach_and_log_err(cx);
|
||||||
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn range_from_lsp(range: lsp::Range) -> Range<Point> {
|
||||||
|
let start = Point::new(range.start.line, range.start.character);
|
||||||
|
let end = Point::new(range.end.line, range.end.character);
|
||||||
|
start..end
|
||||||
|
}
|
||||||
|
|
||||||
impl Editor {
|
impl Editor {
|
||||||
pub(super) fn refresh_code_lenses(
|
pub(super) fn refresh_code_lenses(
|
||||||
&mut self,
|
&mut self,
|
||||||
|
|
@ -413,7 +469,6 @@ impl Editor {
|
||||||
height: Some(1),
|
height: Some(1),
|
||||||
style: BlockStyle::Flex,
|
style: BlockStyle::Flex,
|
||||||
render: Arc::new(render_code_lens_line(
|
render: Arc::new(render_code_lens_line(
|
||||||
buffer_id,
|
|
||||||
line_number,
|
line_number,
|
||||||
lens_line,
|
lens_line,
|
||||||
editor_handle.clone(),
|
editor_handle.clone(),
|
||||||
|
|
@ -554,7 +609,6 @@ impl Editor {
|
||||||
height: Some(1),
|
height: Some(1),
|
||||||
style: BlockStyle::Flex,
|
style: BlockStyle::Flex,
|
||||||
render: Arc::new(render_code_lens_line(
|
render: Arc::new(render_code_lens_line(
|
||||||
buffer_id,
|
|
||||||
line_number,
|
line_number,
|
||||||
lens_line,
|
lens_line,
|
||||||
editor_handle.clone(),
|
editor_handle.clone(),
|
||||||
|
|
|
||||||
|
|
@ -3860,6 +3860,9 @@ impl Editor {
|
||||||
cx.emit(EditorEvent::SelectionsChanged { local });
|
cx.emit(EditorEvent::SelectionsChanged { local });
|
||||||
|
|
||||||
let selections = &self.selections.disjoint_anchors_arc();
|
let selections = &self.selections.disjoint_anchors_arc();
|
||||||
|
if selections.len() == 1 {
|
||||||
|
cx.emit(SearchEvent::ActiveMatchChanged)
|
||||||
|
}
|
||||||
if local && let Some(buffer_snapshot) = buffer.as_singleton() {
|
if local && let Some(buffer_snapshot) = buffer.as_singleton() {
|
||||||
let inmemory_selections = selections
|
let inmemory_selections = selections
|
||||||
.iter()
|
.iter()
|
||||||
|
|
@ -9255,9 +9258,9 @@ impl Editor {
|
||||||
}))
|
}))
|
||||||
.tooltip(move |_window, cx| {
|
.tooltip(move |_window, cx| {
|
||||||
Tooltip::with_meta_in(
|
Tooltip::with_meta_in(
|
||||||
"Remove Bookmark",
|
"Remove bookmark",
|
||||||
Some(&ToggleBookmark),
|
Some(&ToggleBookmark),
|
||||||
SharedString::from("Right-click for more options"),
|
SharedString::from("Right-click for more options."),
|
||||||
&focus_handle,
|
&focus_handle,
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
|
|
@ -9543,16 +9546,15 @@ impl Editor {
|
||||||
};
|
};
|
||||||
let primary_action_text = "Unset breakpoint";
|
let primary_action_text = "Unset breakpoint";
|
||||||
let focus_handle = self.focus_handle.clone();
|
let focus_handle = self.focus_handle.clone();
|
||||||
let has_context_menu = self.has_mouse_context_menu();
|
|
||||||
|
|
||||||
let meta = if is_rejected {
|
let meta = if is_rejected {
|
||||||
SharedString::from("No executable code is associated with this line.")
|
SharedString::from("No executable code is associated with this line.")
|
||||||
} else if !breakpoint.is_disabled() {
|
} else if !breakpoint.is_disabled() {
|
||||||
SharedString::from(format!(
|
SharedString::from(format!(
|
||||||
"{alt_as_text}-click to disable\nright-click for more options"
|
"{alt_as_text}click to disable,\nright-click for more options."
|
||||||
))
|
))
|
||||||
} else {
|
} else {
|
||||||
SharedString::from("Right-click for more options")
|
SharedString::from("Right-click for more options.")
|
||||||
};
|
};
|
||||||
IconButton::new(("breakpoint_indicator", row.0 as usize), icon)
|
IconButton::new(("breakpoint_indicator", row.0 as usize), icon)
|
||||||
.icon_size(IconSize::XSmall)
|
.icon_size(IconSize::XSmall)
|
||||||
|
|
@ -9582,16 +9584,14 @@ impl Editor {
|
||||||
.on_right_click(cx.listener(move |editor, event: &ClickEvent, window, cx| {
|
.on_right_click(cx.listener(move |editor, event: &ClickEvent, window, cx| {
|
||||||
editor.set_gutter_context_menu(row, Some(position), event.position(), window, cx);
|
editor.set_gutter_context_menu(row, Some(position), event.position(), window, cx);
|
||||||
}))
|
}))
|
||||||
.when(!has_context_menu, |button| {
|
.tooltip(move |_window, cx| {
|
||||||
button.tooltip(move |_window, cx| {
|
Tooltip::with_meta_in(
|
||||||
Tooltip::with_meta_in(
|
primary_action_text,
|
||||||
primary_action_text,
|
Some(&ToggleBreakpoint),
|
||||||
Some(&ToggleBreakpoint),
|
meta.clone(),
|
||||||
meta.clone(),
|
&focus_handle,
|
||||||
&focus_handle,
|
cx,
|
||||||
cx,
|
)
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -9637,10 +9637,10 @@ impl Editor {
|
||||||
};
|
};
|
||||||
match self {
|
match self {
|
||||||
Intent::SetBookmark => format!(
|
Intent::SetBookmark => format!(
|
||||||
"{alt_as_text}-click to add a breakpoint\nright-click for more options"
|
"{alt_as_text}click to add a breakpoint,\nright-click for more options."
|
||||||
),
|
),
|
||||||
Intent::SetBreakpoint => format!(
|
Intent::SetBreakpoint => format!(
|
||||||
"{alt_as_text}-click to add a bookmark\nright-click for more options"
|
"{alt_as_text}click to add a bookmark,\nright-click for more options."
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -9667,7 +9667,6 @@ impl Editor {
|
||||||
};
|
};
|
||||||
|
|
||||||
let focus_handle = self.focus_handle.clone();
|
let focus_handle = self.focus_handle.clone();
|
||||||
let has_context_menu = self.has_mouse_context_menu();
|
|
||||||
IconButton::new(("add_breakpoint_button", row.0 as usize), intent.icon())
|
IconButton::new(("add_breakpoint_button", row.0 as usize), intent.icon())
|
||||||
.icon_size(IconSize::XSmall)
|
.icon_size(IconSize::XSmall)
|
||||||
.size(ui::ButtonSize::None)
|
.size(ui::ButtonSize::None)
|
||||||
|
|
@ -9696,16 +9695,14 @@ impl Editor {
|
||||||
.on_right_click(cx.listener(move |editor, event: &ClickEvent, window, cx| {
|
.on_right_click(cx.listener(move |editor, event: &ClickEvent, window, cx| {
|
||||||
editor.set_gutter_context_menu(row, Some(position), event.position(), window, cx);
|
editor.set_gutter_context_menu(row, Some(position), event.position(), window, cx);
|
||||||
}))
|
}))
|
||||||
.when(!has_context_menu, |button| {
|
.tooltip(move |_window, cx| {
|
||||||
button.tooltip(move |_window, cx| {
|
Tooltip::with_meta_in(
|
||||||
Tooltip::with_meta_in(
|
intent.as_str(),
|
||||||
intent.as_str(),
|
Some(&ToggleBreakpoint),
|
||||||
Some(&ToggleBreakpoint),
|
intent.secondary_and_options(),
|
||||||
intent.secondary_and_options(),
|
&focus_handle,
|
||||||
&focus_handle,
|
cx,
|
||||||
cx,
|
)
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -16013,7 +16010,7 @@ impl Editor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn navigation_entry(
|
fn navigation_entry(
|
||||||
&self,
|
&self,
|
||||||
cursor_anchor: Anchor,
|
cursor_anchor: Anchor,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ use crate::{
|
||||||
JoinLines,
|
JoinLines,
|
||||||
code_context_menus::CodeContextMenu,
|
code_context_menus::CodeContextMenu,
|
||||||
edit_prediction_tests::FakeEditPredictionDelegate,
|
edit_prediction_tests::FakeEditPredictionDelegate,
|
||||||
element::{StickyHeader, header_jump_data},
|
element::StickyHeader,
|
||||||
linked_editing_ranges::LinkedEditingRanges,
|
linked_editing_ranges::LinkedEditingRanges,
|
||||||
runnables::RunnableTasks,
|
runnables::RunnableTasks,
|
||||||
scroll::scroll_amount::ScrollAmount,
|
scroll::scroll_amount::ScrollAmount,
|
||||||
|
|
@ -19335,112 +19335,6 @@ fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
|
||||||
fn test_header_jump_data_uses_selection_excerpt(cx: &mut TestAppContext) {
|
|
||||||
init_test(cx, |_| {});
|
|
||||||
|
|
||||||
// 25-line buffer so excerpts at rows 1, 10, and 20 (each a 1-line range,
|
|
||||||
// expanded by 2 context lines) can't merge into a single excerpt.
|
|
||||||
let buffer_text = (0..25)
|
|
||||||
.map(|row| format!("line {row}"))
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join("\n");
|
|
||||||
let buffer = cx.new(|cx| Buffer::local(buffer_text, cx));
|
|
||||||
let buffer_id = buffer.read_with(cx, |buffer, _| buffer.remote_id());
|
|
||||||
|
|
||||||
let multibuffer = cx.new(|cx| {
|
|
||||||
let mut multibuffer = MultiBuffer::new(ReadWrite);
|
|
||||||
multibuffer.set_excerpts_for_path(
|
|
||||||
PathKey::sorted(0),
|
|
||||||
buffer.clone(),
|
|
||||||
[
|
|
||||||
Point::new(1, 0)..Point::new(1, 0),
|
|
||||||
Point::new(10, 0)..Point::new(10, 0),
|
|
||||||
Point::new(20, 0)..Point::new(20, 0),
|
|
||||||
],
|
|
||||||
2,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
multibuffer
|
|
||||||
});
|
|
||||||
|
|
||||||
let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
|
|
||||||
|
|
||||||
editor.update_in(cx, |editor, window, cx| {
|
|
||||||
let snapshot = editor.snapshot(window, cx);
|
|
||||||
let display_snapshot = editor.display_snapshot(cx);
|
|
||||||
|
|
||||||
// Ensure the three ranges landed in three separate excerpts.
|
|
||||||
let excerpts: Vec<_> = snapshot
|
|
||||||
.buffer_snapshot()
|
|
||||||
.excerpts_for_buffer(buffer_id)
|
|
||||||
.collect();
|
|
||||||
assert_eq!(excerpts.len(), 3);
|
|
||||||
|
|
||||||
// Place the cursor at the start of the third excerpt, expressed in
|
|
||||||
// terms of the underlying buffer.
|
|
||||||
let selection_buffer_row = 20;
|
|
||||||
let buffer_entity = editor.buffer().read(cx).buffer(buffer_id).unwrap();
|
|
||||||
let selection_anchor = editor.buffer().update(cx, |multibuffer, cx| {
|
|
||||||
multibuffer
|
|
||||||
.buffer_point_to_anchor(&buffer_entity, Point::new(selection_buffer_row, 0), cx)
|
|
||||||
.expect("buffer row 20 maps to a multibuffer anchor")
|
|
||||||
});
|
|
||||||
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
|
||||||
s.select_anchor_ranges([selection_anchor..selection_anchor])
|
|
||||||
});
|
|
||||||
|
|
||||||
let mut latest_selection_anchors: HashMap<BufferId, Anchor> = HashMap::default();
|
|
||||||
for selection in editor.selections.all_anchors(&display_snapshot).iter() {
|
|
||||||
let head = selection.head();
|
|
||||||
if let Some((text_anchor, _)) = snapshot.buffer_snapshot().anchor_to_buffer_anchor(head)
|
|
||||||
{
|
|
||||||
latest_selection_anchors.insert(text_anchor.buffer_id, head);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// The sticky buffer header represents the FIRST excerpt of its buffer,
|
|
||||||
// even when the cursor is in a later excerpt. That mismatch is the
|
|
||||||
// precondition for the regression.
|
|
||||||
let first_excerpt = snapshot
|
|
||||||
.buffer_snapshot()
|
|
||||||
.excerpt_boundaries_in_range(MultiBufferOffset(0)..snapshot.buffer_snapshot().len())
|
|
||||||
.next()
|
|
||||||
.expect("multibuffer has at least one excerpt")
|
|
||||||
.next;
|
|
||||||
|
|
||||||
let jump_data = header_jump_data(
|
|
||||||
&snapshot,
|
|
||||||
DisplayRow(0),
|
|
||||||
FILE_HEADER_HEIGHT + MULTI_BUFFER_EXCERPT_HEADER_HEIGHT,
|
|
||||||
&first_excerpt,
|
|
||||||
&latest_selection_anchors,
|
|
||||||
);
|
|
||||||
|
|
||||||
match jump_data {
|
|
||||||
JumpData::MultiBufferPoint {
|
|
||||||
position,
|
|
||||||
line_offset_from_top,
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
assert_eq!(
|
|
||||||
position.row, selection_buffer_row,
|
|
||||||
"jump should target the cursor's buffer row, not the first excerpt's row"
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
line_offset_from_top < selection_buffer_row,
|
|
||||||
"line_offset_from_top ({line_offset_from_top}) should be measured from the \
|
|
||||||
selection's excerpt, not the first excerpt; expected less than \
|
|
||||||
selection_buffer_row ({selection_buffer_row})"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
JumpData::MultiBufferRow { .. } => {
|
|
||||||
panic!("expected MultiBufferPoint jump data when a selection is present")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
|
async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
|
||||||
init_test(cx, |_| {});
|
init_test(cx, |_| {});
|
||||||
|
|
@ -32821,78 +32715,6 @@ async fn test_sticky_scroll(cx: &mut TestAppContext) {
|
||||||
assert_eq!(sticky_headers(10.0), vec![]);
|
assert_eq!(sticky_headers(10.0), vec![]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
|
||||||
async fn test_sticky_scroll_with_decoration_prefix_in_item(cx: &mut TestAppContext) {
|
|
||||||
init_test(cx, |_| {});
|
|
||||||
let mut cx = EditorTestContext::new(cx).await;
|
|
||||||
|
|
||||||
let language = Arc::new(
|
|
||||||
Language::new(
|
|
||||||
LanguageConfig {
|
|
||||||
name: "TypeScript".into(),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
|
|
||||||
)
|
|
||||||
.with_outline_query(
|
|
||||||
r#"
|
|
||||||
(class_declaration
|
|
||||||
"class" @context
|
|
||||||
name: (_) @name) @item
|
|
||||||
"#,
|
|
||||||
)
|
|
||||||
.expect("TypeScript outline query"),
|
|
||||||
);
|
|
||||||
|
|
||||||
let buffer = indoc! {"
|
|
||||||
ˇ@Decorator
|
|
||||||
class Foo {
|
|
||||||
x = 1;
|
|
||||||
y = 2;
|
|
||||||
z = 3;
|
|
||||||
w = 4;
|
|
||||||
}
|
|
||||||
"};
|
|
||||||
cx.set_state(buffer);
|
|
||||||
cx.update_editor(|e, _, cx| {
|
|
||||||
e.buffer()
|
|
||||||
.read(cx)
|
|
||||||
.as_singleton()
|
|
||||||
.unwrap()
|
|
||||||
.update(cx, |buffer, cx| {
|
|
||||||
buffer.set_language(Some(language), cx);
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
let mut sticky_headers = |offset: ScrollOffset| {
|
|
||||||
cx.update_editor(|e, window, cx| {
|
|
||||||
e.scroll(gpui::Point { x: 0., y: offset }, None, window, cx);
|
|
||||||
});
|
|
||||||
cx.run_until_parked();
|
|
||||||
cx.update_editor(|e, window, cx| {
|
|
||||||
EditorElement::sticky_headers(&e, &e.snapshot(window, cx))
|
|
||||||
.into_iter()
|
|
||||||
.map(
|
|
||||||
|StickyHeader {
|
|
||||||
start_point,
|
|
||||||
offset,
|
|
||||||
..
|
|
||||||
}| { (start_point, offset) },
|
|
||||||
)
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
let class_foo = Point { row: 1, column: 0 };
|
|
||||||
|
|
||||||
assert_eq!(sticky_headers(0.0), vec![]);
|
|
||||||
assert_eq!(sticky_headers(1.5), vec![(class_foo, 0.0)]);
|
|
||||||
assert_eq!(sticky_headers(2.5), vec![(class_foo, 0.0)]);
|
|
||||||
assert_eq!(sticky_headers(5.5), vec![(class_foo, -0.5)]);
|
|
||||||
assert_eq!(sticky_headers(6.0), vec![]);
|
|
||||||
assert_eq!(sticky_headers(7.0), vec![]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_sticky_scroll_with_expanded_deleted_diff_hunks(
|
async fn test_sticky_scroll_with_expanded_deleted_diff_hunks(
|
||||||
executor: BackgroundExecutor,
|
executor: BackgroundExecutor,
|
||||||
|
|
|
||||||
|
|
@ -895,8 +895,7 @@ impl EditorElement {
|
||||||
let hitbox = &position_map.gutter_hitbox;
|
let hitbox = &position_map.gutter_hitbox;
|
||||||
|
|
||||||
if event.position.x <= hitbox.bounds.right() - gutter_right_padding
|
if event.position.x <= hitbox.bounds.right() - gutter_right_padding
|
||||||
// Don't show the gutter_context_menu in collab notes
|
&& editor.collaboration_hub.is_none()
|
||||||
&& editor.project.is_some()
|
|
||||||
{
|
{
|
||||||
let point_for_position = position_map.point_for_position(event.position);
|
let point_for_position = position_map.point_for_position(event.position);
|
||||||
editor.set_gutter_context_menu(
|
editor.set_gutter_context_menu(
|
||||||
|
|
@ -1395,7 +1394,7 @@ impl EditorElement {
|
||||||
indicator.is_active && start_row == valid_point.row()
|
indicator.is_active && start_row == valid_point.row()
|
||||||
});
|
});
|
||||||
|
|
||||||
let gutter_hover_button = if gutter_hovered
|
let breakpoint_indicator = if gutter_hovered
|
||||||
&& !is_on_diff_review_button_row
|
&& !is_on_diff_review_button_row
|
||||||
&& split_side != Some(SplitSide::Left)
|
&& split_side != Some(SplitSide::Left)
|
||||||
{
|
{
|
||||||
|
|
@ -1440,16 +1439,13 @@ impl EditorElement {
|
||||||
editor.gutter_hover_button.1 = None;
|
editor.gutter_hover_button.1 = None;
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
} else if editor.has_mouse_context_menu() {
|
|
||||||
editor.gutter_hover_button.1 = None;
|
|
||||||
editor.gutter_hover_button.0
|
|
||||||
} else {
|
} else {
|
||||||
editor.gutter_hover_button.1 = None;
|
editor.gutter_hover_button.1 = None;
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
if &gutter_hover_button != &editor.gutter_hover_button.0 {
|
if &breakpoint_indicator != &editor.gutter_hover_button.0 {
|
||||||
editor.gutter_hover_button.0 = gutter_hover_button;
|
editor.gutter_hover_button.0 = breakpoint_indicator;
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -4732,10 +4728,7 @@ impl EditorElement {
|
||||||
let mut rows = Vec::<StickyHeader>::new();
|
let mut rows = Vec::<StickyHeader>::new();
|
||||||
|
|
||||||
for item in editor.sticky_headers.iter().flatten() {
|
for item in editor.sticky_headers.iter().flatten() {
|
||||||
let start_point = item
|
let start_point = item.range.start.to_point(snapshot.buffer_snapshot());
|
||||||
.source_range_for_text
|
|
||||||
.start
|
|
||||||
.to_point(snapshot.buffer_snapshot());
|
|
||||||
let end_point = item.range.end.to_point(snapshot.buffer_snapshot());
|
let end_point = item.range.end.to_point(snapshot.buffer_snapshot());
|
||||||
|
|
||||||
let sticky_row = snapshot
|
let sticky_row = snapshot
|
||||||
|
|
@ -8355,34 +8348,21 @@ pub(crate) fn header_jump_data(
|
||||||
) -> JumpData {
|
) -> JumpData {
|
||||||
let multibuffer_snapshot = editor_snapshot.buffer_snapshot();
|
let multibuffer_snapshot = editor_snapshot.buffer_snapshot();
|
||||||
let buffer = first_excerpt.buffer(multibuffer_snapshot);
|
let buffer = first_excerpt.buffer(multibuffer_snapshot);
|
||||||
let (jump_anchor, jump_buffer, excerpt_start) = if let Some(anchor) =
|
let (jump_anchor, jump_buffer) = if let Some(anchor) =
|
||||||
latest_selection_anchors.get(&first_excerpt.buffer_id())
|
latest_selection_anchors.get(&first_excerpt.buffer_id())
|
||||||
&& let Some((jump_anchor, selection_buffer)) =
|
&& let Some((jump_anchor, selection_buffer)) =
|
||||||
multibuffer_snapshot.anchor_to_buffer_anchor(*anchor)
|
multibuffer_snapshot.anchor_to_buffer_anchor(*anchor)
|
||||||
{
|
{
|
||||||
let jump_offset = text::ToOffset::to_offset(&jump_anchor, selection_buffer);
|
(jump_anchor, selection_buffer)
|
||||||
let selection_excerpt_start = multibuffer_snapshot
|
|
||||||
.excerpts_for_buffer(jump_anchor.buffer_id)
|
|
||||||
.find(|excerpt| {
|
|
||||||
let start = text::ToOffset::to_offset(&excerpt.context.start, selection_buffer);
|
|
||||||
let end = text::ToOffset::to_offset(&excerpt.context.end, selection_buffer);
|
|
||||||
start <= jump_offset && jump_offset <= end
|
|
||||||
})
|
|
||||||
.map(|excerpt| excerpt.context.start)
|
|
||||||
.unwrap_or(first_excerpt.range.context.start);
|
|
||||||
(jump_anchor, selection_buffer, selection_excerpt_start)
|
|
||||||
} else {
|
} else {
|
||||||
(
|
(first_excerpt.range.primary.start, buffer)
|
||||||
first_excerpt.range.primary.start,
|
|
||||||
buffer,
|
|
||||||
first_excerpt.range.context.start,
|
|
||||||
)
|
|
||||||
};
|
};
|
||||||
|
let excerpt_start = first_excerpt.range.context.start;
|
||||||
let jump_position = language::ToPoint::to_point(&jump_anchor, jump_buffer);
|
let jump_position = language::ToPoint::to_point(&jump_anchor, jump_buffer);
|
||||||
let rows_from_excerpt_start = if jump_anchor == excerpt_start {
|
let rows_from_excerpt_start = if jump_anchor == excerpt_start {
|
||||||
0
|
0
|
||||||
} else {
|
} else {
|
||||||
let excerpt_start_point = language::ToPoint::to_point(&excerpt_start, jump_buffer);
|
let excerpt_start_point = language::ToPoint::to_point(&excerpt_start, buffer);
|
||||||
jump_position.row.saturating_sub(excerpt_start_point.row)
|
jump_position.row.saturating_sub(excerpt_start_point.row)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1799,10 +1799,6 @@ impl SearchableItem for Editor {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Takes the current cursor position and finds the next match in the
|
|
||||||
/// provided `direction`, the provide `count` number of times, wrapping
|
|
||||||
/// around if necessary.
|
|
||||||
fn match_index_for_direction(
|
fn match_index_for_direction(
|
||||||
&mut self,
|
&mut self,
|
||||||
matches: &[Range<Anchor>],
|
matches: &[Range<Anchor>],
|
||||||
|
|
@ -1813,48 +1809,45 @@ impl SearchableItem for Editor {
|
||||||
_: &mut Window,
|
_: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> usize {
|
) -> usize {
|
||||||
if count == 0 {
|
let buffer = self.buffer().read(cx).snapshot(cx);
|
||||||
return current_index;
|
let current_index_position = if self.selections.disjoint_anchors_arc().len() == 1 {
|
||||||
}
|
|
||||||
|
|
||||||
let cursor = if self.selections.disjoint_anchors_arc().len() == 1 {
|
|
||||||
self.selections.newest_anchor().head()
|
self.selections.newest_anchor().head()
|
||||||
} else {
|
} else {
|
||||||
matches[current_index].start
|
matches[current_index].start
|
||||||
};
|
};
|
||||||
|
|
||||||
let buffer = self.buffer().read(cx).snapshot(cx);
|
let mut count = count % matches.len();
|
||||||
let new_idx = match direction {
|
if count == 0 {
|
||||||
Direction::Next => matches
|
return current_index;
|
||||||
.iter()
|
}
|
||||||
.position(|m| m.start.cmp(&cursor, &buffer).is_gt())
|
match direction {
|
||||||
.unwrap_or(0),
|
Direction::Next => {
|
||||||
Direction::Prev => matches
|
if matches[current_index]
|
||||||
.iter()
|
.start
|
||||||
.rposition(|m| m.end.cmp(&cursor, &buffer).is_lt())
|
.cmp(¤t_index_position, &buffer)
|
||||||
.unwrap_or(matches.len() - 1),
|
.is_gt()
|
||||||
} as isize;
|
{
|
||||||
|
count -= 1
|
||||||
|
}
|
||||||
|
|
||||||
// We'll use `count - 1` because the first jump to the next or previous
|
(current_index + count) % matches.len()
|
||||||
// match already happens in the scenario above, when we find the next or
|
}
|
||||||
// previous match starting from the cursor position.
|
Direction::Prev => {
|
||||||
let count = count.saturating_sub(1);
|
if matches[current_index]
|
||||||
let count = match direction {
|
.end
|
||||||
Direction::Prev => -(count as isize),
|
.cmp(¤t_index_position, &buffer)
|
||||||
Direction::Next => count as isize,
|
.is_lt()
|
||||||
};
|
{
|
||||||
|
count -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
let new_idx = (new_idx + count) % matches.len() as isize;
|
if current_index >= count {
|
||||||
let new_idx = if new_idx.is_negative() {
|
current_index - count
|
||||||
// We need a `matches.len() - 1` here in case `next_idx` has now been
|
} else {
|
||||||
// set to `0`, otherwise we'd end up returning `matches.len()`, which
|
matches.len() - (count - current_index)
|
||||||
// would be out of bounds.
|
}
|
||||||
new_idx + (matches.len() - 1) as isize
|
}
|
||||||
} else {
|
}
|
||||||
new_idx
|
|
||||||
};
|
|
||||||
assert!(new_idx < matches.len() as isize);
|
|
||||||
new_idx as usize
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn find_matches(
|
fn find_matches(
|
||||||
|
|
|
||||||
|
|
@ -433,17 +433,6 @@ impl SplittableEditor {
|
||||||
self.lhs.as_ref().map(|s| &s.editor)
|
self.lhs.as_ref().map(|s| &s.editor)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_editors(
|
|
||||||
&self,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
f: impl Fn(&mut Editor, &mut Context<Editor>),
|
|
||||||
) {
|
|
||||||
if let Some(lhs) = &self.lhs {
|
|
||||||
lhs.editor.update(cx, &f);
|
|
||||||
}
|
|
||||||
self.rhs_editor.update(cx, &f);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn diff_view_style(&self) -> DiffViewStyle {
|
pub fn diff_view_style(&self) -> DiffViewStyle {
|
||||||
self.diff_view_style
|
self.diff_view_style
|
||||||
}
|
}
|
||||||
|
|
@ -457,18 +446,15 @@ impl SplittableEditor {
|
||||||
render_diff_hunk_controls: RenderDiffHunkControlsFn,
|
render_diff_hunk_controls: RenderDiffHunkControlsFn,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
self.update_editors(cx, |editor, cx| {
|
self.rhs_editor.update(cx, |editor, cx| {
|
||||||
editor.set_render_diff_hunk_controls(render_diff_hunk_controls.clone(), cx);
|
editor.set_render_diff_hunk_controls(render_diff_hunk_controls.clone(), cx);
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
pub fn disable_diff_hunk_controls(&self, cx: &mut Context<Self>) {
|
if let Some(lhs) = &self.lhs {
|
||||||
let empty_controls = Arc::new(|_, _: &_, _, _, _, _: &_, _: &mut _, _: &mut _| {
|
lhs.editor.update(cx, |editor, cx| {
|
||||||
gpui::Empty.into_any_element()
|
editor.set_render_diff_hunk_controls(render_diff_hunk_controls.clone(), cx);
|
||||||
});
|
});
|
||||||
self.update_editors(cx, |editor, cx| {
|
}
|
||||||
editor.set_render_diff_hunk_controls(empty_controls.clone(), cx);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn focused_side(&self) -> SplitSide {
|
fn focused_side(&self) -> SplitSide {
|
||||||
|
|
@ -505,7 +491,6 @@ impl SplittableEditor {
|
||||||
editor.set_expand_all_diff_hunks(cx);
|
editor.set_expand_all_diff_hunks(cx);
|
||||||
editor.disable_runnables();
|
editor.disable_runnables();
|
||||||
editor.disable_inline_diagnostics();
|
editor.disable_inline_diagnostics();
|
||||||
editor.disable_mouse_wheel_zoom();
|
|
||||||
editor.set_minimap_visibility(crate::MinimapVisibility::Disabled, window, cx);
|
editor.set_minimap_visibility(crate::MinimapVisibility::Disabled, window, cx);
|
||||||
editor.start_temporary_diff_override();
|
editor.start_temporary_diff_override();
|
||||||
editor
|
editor
|
||||||
|
|
@ -601,7 +586,6 @@ impl SplittableEditor {
|
||||||
editor.disable_lsp_data();
|
editor.disable_lsp_data();
|
||||||
editor.disable_runnables();
|
editor.disable_runnables();
|
||||||
editor.disable_diagnostics(cx);
|
editor.disable_diagnostics(cx);
|
||||||
editor.disable_mouse_wheel_zoom();
|
|
||||||
editor.set_minimap_visibility(crate::MinimapVisibility::Disabled, window, cx);
|
editor.set_minimap_visibility(crate::MinimapVisibility::Disabled, window, cx);
|
||||||
editor
|
editor
|
||||||
});
|
});
|
||||||
|
|
@ -2141,9 +2125,14 @@ mod tests {
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
editor.update_editors(cx, |editor, cx| {
|
editor.rhs_editor.update(cx, |editor, cx| {
|
||||||
editor.set_soft_wrap_mode(soft_wrap, cx);
|
editor.set_soft_wrap_mode(soft_wrap, cx);
|
||||||
});
|
});
|
||||||
|
if let Some(lhs) = &editor.lhs {
|
||||||
|
lhs.editor.update(cx, |editor, cx| {
|
||||||
|
editor.set_soft_wrap_mode(soft_wrap, cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
editor
|
editor
|
||||||
});
|
});
|
||||||
(editor, cx)
|
(editor, cx)
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,6 @@ settings.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
theme.workspace = true
|
theme.workspace = true
|
||||||
ui.workspace = true
|
ui.workspace = true
|
||||||
ui_input.workspace = true
|
|
||||||
util.workspace = true
|
util.workspace = true
|
||||||
workspace.workspace = true
|
workspace.workspace = true
|
||||||
zed_actions.workspace = true
|
zed_actions.workspace = true
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,8 @@ use fuzzy::{StringMatch, StringMatchCandidate};
|
||||||
use fuzzy_nucleo::{PathMatch, PathMatchCandidate};
|
use fuzzy_nucleo::{PathMatch, PathMatchCandidate};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
Action, AnyElement, App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
|
Action, AnyElement, App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
|
||||||
KeyContext, Modifiers, ModifiersChangedEvent, ParentElement, Render,
|
KeyContext, Modifiers, ModifiersChangedEvent, ParentElement, Render, Styled, Task, WeakEntity,
|
||||||
StatefulInteractiveElement, Styled, Task, WeakEntity, Window, actions, rems,
|
Window, actions, rems,
|
||||||
};
|
};
|
||||||
use open_path_prompt::{
|
use open_path_prompt::{
|
||||||
OpenPathPrompt,
|
OpenPathPrompt,
|
||||||
|
|
@ -37,10 +37,9 @@ use std::{
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use ui::{
|
use ui::{
|
||||||
ButtonLike, CommonAnimationExt, ContextMenu, HighlightedLabel, Indicator, KeyBinding, ListItem,
|
ButtonLike, ContextMenu, HighlightedLabel, Indicator, KeyBinding, ListItem, ListItemSpacing,
|
||||||
ListItemSpacing, PopoverMenu, PopoverMenuHandle, TintColor, Tooltip, prelude::*,
|
PopoverMenu, PopoverMenuHandle, TintColor, Tooltip, prelude::*,
|
||||||
};
|
};
|
||||||
use ui_input::ErasedEditor;
|
|
||||||
use util::{
|
use util::{
|
||||||
ResultExt, maybe,
|
ResultExt, maybe,
|
||||||
paths::{PathStyle, PathWithPosition},
|
paths::{PathStyle, PathWithPosition},
|
||||||
|
|
@ -1758,41 +1757,6 @@ impl PickerDelegate for FileFinderDelegate {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_editor(
|
|
||||||
&self,
|
|
||||||
editor: &Arc<dyn ErasedEditor>,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<Picker<Self>>,
|
|
||||||
) -> Div {
|
|
||||||
let has_search_query = self.latest_search_query.is_some();
|
|
||||||
let is_project_scan_running = {
|
|
||||||
let worktree_store = self.project.read(cx).worktree_store();
|
|
||||||
!worktree_store.read(cx).initial_scan_completed()
|
|
||||||
};
|
|
||||||
|
|
||||||
h_flex()
|
|
||||||
.flex_none()
|
|
||||||
.h_9()
|
|
||||||
.px_2p5()
|
|
||||||
.justify_between()
|
|
||||||
.border_b_1()
|
|
||||||
.border_color(cx.theme().colors().border_variant)
|
|
||||||
.child(editor.render(window, cx))
|
|
||||||
.when(is_project_scan_running && has_search_query, |this| {
|
|
||||||
this.child(
|
|
||||||
h_flex()
|
|
||||||
.id("project-scan-indicator")
|
|
||||||
.tooltip(Tooltip::text("Project Scan in Progress…"))
|
|
||||||
.child(
|
|
||||||
Icon::new(IconName::LoadCircle)
|
|
||||||
.color(Color::Accent)
|
|
||||||
.size(IconSize::Small)
|
|
||||||
.with_rotate_animation(2),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_footer(&self, _: &mut Window, cx: &mut Context<Picker<Self>>) -> Option<AnyElement> {
|
fn render_footer(&self, _: &mut Window, cx: &mut Context<Picker<Self>>) -> Option<AnyElement> {
|
||||||
let focus_handle = self.focus_handle.clone();
|
let focus_handle = self.focus_handle.clone();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,6 @@ thiserror.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
smol.workspace = true
|
smol.workspace = true
|
||||||
telemetry.workspace = true
|
|
||||||
tempfile.workspace = true
|
tempfile.workspace = true
|
||||||
text.workspace = true
|
text.workspace = true
|
||||||
time.workspace = true
|
time.workspace = true
|
||||||
|
|
|
||||||
|
|
@ -72,133 +72,6 @@ pub trait Watcher: Send + Sync {
|
||||||
fn remove(&self, path: &Path) -> Result<()>;
|
fn remove(&self, path: &Path) -> Result<()>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Detect whether a path requires polling instead of native file watching.
|
|
||||||
///
|
|
||||||
/// Returns `true` for filesystem types where inotify/FSEvents/ReadDirectoryChanges
|
|
||||||
/// silently fail to deliver events: 9P (WSL drvfs), NFS, CIFS/SMB, FUSE (sshfs), etc.
|
|
||||||
///
|
|
||||||
/// Can be overridden with the `ZED_FILE_WATCHER_MODE` environment variable:
|
|
||||||
/// - `native` — always use native OS watcher
|
|
||||||
/// - `poll` — always use polling
|
|
||||||
/// - `auto` (default) — auto-detect based on filesystem type
|
|
||||||
pub fn requires_poll_watcher(path: &Path) -> bool {
|
|
||||||
match std::env::var("ZED_FILE_WATCHER_MODE")
|
|
||||||
.as_deref()
|
|
||||||
.unwrap_or("auto")
|
|
||||||
{
|
|
||||||
"native" => return false,
|
|
||||||
"poll" => return true,
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
{
|
|
||||||
let path = effective_watch_path(path);
|
|
||||||
return detect_requires_poll_watcher_linux(&path);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(target_os = "linux"))]
|
|
||||||
{
|
|
||||||
let _ = path;
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn effective_watch_path(path: &Path) -> PathBuf {
|
|
||||||
if path.exists() {
|
|
||||||
return path.to_path_buf();
|
|
||||||
}
|
|
||||||
|
|
||||||
for ancestor in path.ancestors() {
|
|
||||||
if ancestor.exists() {
|
|
||||||
return ancestor.to_path_buf();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
path.to_path_buf()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
fn detect_requires_poll_watcher_linux(path: &Path) -> bool {
|
|
||||||
use std::ffi::CString;
|
|
||||||
use std::os::unix::ffi::OsStrExt;
|
|
||||||
|
|
||||||
// Check filesystem type via statfs
|
|
||||||
let c_path = match CString::new(path.as_os_str().as_bytes()) {
|
|
||||||
Ok(p) => p,
|
|
||||||
Err(_) => return false,
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut stat: libc::statfs = unsafe { std::mem::zeroed() };
|
|
||||||
if unsafe { libc::statfs(c_path.as_ptr(), &mut stat) } != 0 {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filesystem magic numbers where inotify does not deliver events.
|
|
||||||
// These are defined in linux/magic.h and statfs(2).
|
|
||||||
const V9FS_MAGIC: i64 = 0x01021997; // Plan 9 / WSL2 interop (drvfs)
|
|
||||||
const NFS_SUPER_MAGIC: i64 = 0x6969;
|
|
||||||
const CIFS_MAGIC: i64 = 0xFF534D42u32 as i64; // CIFS/SMB
|
|
||||||
const SMB_SUPER_MAGIC: i64 = 0x517B;
|
|
||||||
const SMB2_MAGIC: i64 = 0xFE534D42u32 as i64;
|
|
||||||
const FUSE_SUPER_MAGIC: i64 = 0x65735546; // FUSE (includes sshfs)
|
|
||||||
|
|
||||||
let fs_type = stat.f_type;
|
|
||||||
if fs_type == V9FS_MAGIC
|
|
||||||
|| fs_type == NFS_SUPER_MAGIC
|
|
||||||
|| fs_type == CIFS_MAGIC
|
|
||||||
|| fs_type == SMB_SUPER_MAGIC
|
|
||||||
|| fs_type == SMB2_MAGIC
|
|
||||||
|| fs_type == FUSE_SUPER_MAGIC
|
|
||||||
{
|
|
||||||
log::info!(
|
|
||||||
"Detected network/virtual filesystem (type 0x{:x}) at {}, using poll watcher",
|
|
||||||
fs_type,
|
|
||||||
path.display()
|
|
||||||
);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Also check for WSL + /mnt/<drive>/ pattern as a fallback
|
|
||||||
// in case statfs returns an unexpected type for drvfs
|
|
||||||
if is_wsl_drvfs_path(path) {
|
|
||||||
log::info!(
|
|
||||||
"Detected WSL drvfs mount at {}, using poll watcher",
|
|
||||||
path.display()
|
|
||||||
);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
fn is_wsl_drvfs_path(path: &Path) -> bool {
|
|
||||||
// Only relevant inside WSL
|
|
||||||
if std::env::var_os("WSL_DISTRO_NAME").is_none() {
|
|
||||||
if let Ok(version) = std::fs::read_to_string("/proc/version") {
|
|
||||||
let v = version.to_lowercase();
|
|
||||||
if !v.contains("microsoft") && !v.contains("wsl") {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Windows drives are mounted at /mnt/c, /mnt/d, etc.
|
|
||||||
let path_str = match path.to_str() {
|
|
||||||
Some(s) => s,
|
|
||||||
None => return false,
|
|
||||||
};
|
|
||||||
if !path_str.starts_with("/mnt/") || path_str.len() < 6 {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
let after_mnt = &path_str[5..];
|
|
||||||
after_mnt.starts_with(|c: char| c.is_ascii_alphabetic())
|
|
||||||
&& (after_mnt.len() == 1 || after_mnt.as_bytes()[1] == b'/')
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
|
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
|
||||||
pub enum PathEventKind {
|
pub enum PathEventKind {
|
||||||
Removed,
|
Removed,
|
||||||
|
|
@ -1197,34 +1070,19 @@ impl Fs for RealFs {
|
||||||
use util::{ResultExt as _, paths::SanitizedPath};
|
use util::{ResultExt as _, paths::SanitizedPath};
|
||||||
let executor = self.executor.clone();
|
let executor = self.executor.clone();
|
||||||
|
|
||||||
let use_poll = requires_poll_watcher(path);
|
|
||||||
let watch_path = effective_watch_path(path);
|
|
||||||
|
|
||||||
let (tx, rx) = smol::channel::unbounded();
|
let (tx, rx) = smol::channel::unbounded();
|
||||||
let pending_paths: Arc<Mutex<Vec<PathEvent>>> = Default::default();
|
let pending_paths: Arc<Mutex<Vec<PathEvent>>> = Default::default();
|
||||||
|
let watcher = Arc::new(fs_watcher::FsWatcher::new(tx, pending_paths.clone()));
|
||||||
|
|
||||||
let mode = if use_poll {
|
// If the path doesn't exist yet (e.g. settings.json), watch the parent dir to learn when it's created.
|
||||||
log::info!(
|
if let Err(e) = watcher.add(path)
|
||||||
"Using poll watcher ({}ms interval) for {}",
|
&& let Some(parent) = path.parent()
|
||||||
fs_watcher::poll_interval().as_millis(),
|
&& let Err(parent_e) = watcher.add(parent)
|
||||||
path.display()
|
{
|
||||||
);
|
|
||||||
telemetry::event!("fs_watcher_poll", path = path.display().to_string());
|
|
||||||
fs_watcher::WatcherMode::Poll
|
|
||||||
} else {
|
|
||||||
fs_watcher::WatcherMode::Native
|
|
||||||
};
|
|
||||||
let watcher: Arc<dyn Watcher> = Arc::new(fs_watcher::FsWatcher::new(
|
|
||||||
tx.clone(),
|
|
||||||
pending_paths.clone(),
|
|
||||||
mode,
|
|
||||||
));
|
|
||||||
|
|
||||||
if let Err(e) = watcher.add(&watch_path) {
|
|
||||||
log::warn!(
|
log::warn!(
|
||||||
"Failed to watch {} using {}:\n{e}",
|
"Failed to watch {} and its parent directory {}:\n{e}\n{parent_e}",
|
||||||
path.display(),
|
path.display(),
|
||||||
watch_path.display()
|
parent.display()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,38 +4,27 @@ use std::{
|
||||||
collections::{BTreeMap, HashMap},
|
collections::{BTreeMap, HashMap},
|
||||||
ops::DerefMut,
|
ops::DerefMut,
|
||||||
path::Path,
|
path::Path,
|
||||||
sync::{Arc, LazyLock, OnceLock},
|
sync::{Arc, OnceLock},
|
||||||
time::Duration,
|
|
||||||
};
|
};
|
||||||
use util::{ResultExt, paths::SanitizedPath};
|
use util::{ResultExt, paths::SanitizedPath};
|
||||||
|
|
||||||
use crate::{PathEvent, PathEventKind, Watcher};
|
use crate::{PathEvent, PathEventKind, Watcher};
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
|
|
||||||
pub enum WatcherMode {
|
|
||||||
#[default]
|
|
||||||
Native,
|
|
||||||
Poll,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct FsWatcher {
|
pub struct FsWatcher {
|
||||||
tx: smol::channel::Sender<()>,
|
tx: smol::channel::Sender<()>,
|
||||||
pending_path_events: Arc<Mutex<Vec<PathEvent>>>,
|
pending_path_events: Arc<Mutex<Vec<PathEvent>>>,
|
||||||
registrations: Mutex<BTreeMap<Arc<std::path::Path>, WatcherRegistrationId>>,
|
registrations: Mutex<BTreeMap<Arc<std::path::Path>, WatcherRegistrationId>>,
|
||||||
mode: WatcherMode,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FsWatcher {
|
impl FsWatcher {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
tx: smol::channel::Sender<()>,
|
tx: smol::channel::Sender<()>,
|
||||||
pending_path_events: Arc<Mutex<Vec<PathEvent>>>,
|
pending_path_events: Arc<Mutex<Vec<PathEvent>>>,
|
||||||
mode: WatcherMode,
|
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
tx,
|
tx,
|
||||||
pending_path_events,
|
pending_path_events,
|
||||||
registrations: Default::default(),
|
registrations: Default::default(),
|
||||||
mode,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -48,10 +37,11 @@ impl Drop for FsWatcher {
|
||||||
std::mem::swap(old.deref_mut(), &mut registrations);
|
std::mem::swap(old.deref_mut(), &mut registrations);
|
||||||
}
|
}
|
||||||
|
|
||||||
let global_watcher = global_watcher();
|
let _ = global(|g| {
|
||||||
for (_, registration) in registrations {
|
for (_, registration) in registrations {
|
||||||
global_watcher.remove(registration);
|
g.remove(registration);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -59,10 +49,13 @@ impl Watcher for FsWatcher {
|
||||||
fn add(&self, path: &std::path::Path) -> anyhow::Result<()> {
|
fn add(&self, path: &std::path::Path) -> anyhow::Result<()> {
|
||||||
log::trace!("watcher add: {path:?}");
|
log::trace!("watcher add: {path:?}");
|
||||||
let tx = self.tx.clone();
|
let tx = self.tx.clone();
|
||||||
let pending_path_events = self.pending_path_events.clone();
|
let pending_paths = self.pending_path_events.clone();
|
||||||
|
|
||||||
if (self.mode == WatcherMode::Poll || cfg!(any(target_os = "windows", target_os = "macos")))
|
#[cfg(any(target_os = "windows", target_os = "macos"))]
|
||||||
&& let Some((watched_path, _)) = self
|
{
|
||||||
|
// Return early if an ancestor of this path was already being watched.
|
||||||
|
// saves a huge amount of memory
|
||||||
|
if let Some((watched_path, _)) = self
|
||||||
.registrations
|
.registrations
|
||||||
.lock()
|
.lock()
|
||||||
.range::<std::path::Path, _>((
|
.range::<std::path::Path, _>((
|
||||||
|
|
@ -70,36 +63,86 @@ impl Watcher for FsWatcher {
|
||||||
std::ops::Bound::Included(path),
|
std::ops::Bound::Included(path),
|
||||||
))
|
))
|
||||||
.next_back()
|
.next_back()
|
||||||
&& path.starts_with(watched_path.as_ref())
|
&& path.starts_with(watched_path.as_ref())
|
||||||
{
|
{
|
||||||
log::trace!(
|
log::trace!(
|
||||||
"path to watch is covered by existing registration: {path:?}, {watched_path:?}"
|
"path to watch is covered by existing registration: {path:?}, {watched_path:?}"
|
||||||
);
|
);
|
||||||
return Ok(());
|
return Ok(());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
if self.registrations.lock().contains_key(path) {
|
{
|
||||||
log::trace!("path to watch is already watched: {path:?}");
|
if self.registrations.lock().contains_key(path) {
|
||||||
return Ok(());
|
log::trace!("path to watch is already watched: {path:?}");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let root_path = SanitizedPath::new_arc(path);
|
let root_path = SanitizedPath::new_arc(path);
|
||||||
let path: Arc<std::path::Path> = path.into();
|
let path: Arc<std::path::Path> = path.into();
|
||||||
|
|
||||||
|
#[cfg(any(target_os = "windows", target_os = "macos"))]
|
||||||
|
let mode = notify::RecursiveMode::Recursive;
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
let mode = notify::RecursiveMode::NonRecursive;
|
||||||
|
|
||||||
let registration_path = path.clone();
|
let registration_path = path.clone();
|
||||||
let registration_id = global_watcher().add(
|
let registration_id = global({
|
||||||
path.clone(),
|
let watch_path = path.clone();
|
||||||
self.mode,
|
let callback_path = path;
|
||||||
move |result: Result<¬ify::Event, ¬ify::Error>| match result {
|
|g| {
|
||||||
Ok(event) => {
|
g.add(watch_path, mode, move |event: ¬ify::Event| {
|
||||||
log::trace!("watcher received event: {event:?}");
|
log::trace!("watcher received event: {event:?}");
|
||||||
push_notify_event(&tx, &pending_path_events, &root_path, path.as_ref(), event);
|
let kind = match event.kind {
|
||||||
}
|
EventKind::Create(_) => Some(PathEventKind::Created),
|
||||||
Err(error) => {
|
EventKind::Modify(_) => Some(PathEventKind::Changed),
|
||||||
push_notify_error(&tx, &pending_path_events, path.as_ref(), error);
|
EventKind::Remove(_) => Some(PathEventKind::Removed),
|
||||||
}
|
_ => None,
|
||||||
},
|
};
|
||||||
)?;
|
let mut path_events = event
|
||||||
|
.paths
|
||||||
|
.iter()
|
||||||
|
.filter_map(|event_path| {
|
||||||
|
let event_path = SanitizedPath::new(event_path);
|
||||||
|
event_path.starts_with(&root_path).then(|| PathEvent {
|
||||||
|
path: event_path.as_path().to_path_buf(),
|
||||||
|
kind,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let is_rescan_event = event.need_rescan();
|
||||||
|
if is_rescan_event {
|
||||||
|
log::warn!(
|
||||||
|
"filesystem watcher lost sync for {callback_path:?}; scheduling rescan"
|
||||||
|
);
|
||||||
|
// we only keep the first event per path below, this ensures it will be the rescan event
|
||||||
|
// we'll remove any existing pending events for the same reason once we have the lock below
|
||||||
|
path_events.retain(|p| &p.path != callback_path.as_ref());
|
||||||
|
path_events.push(PathEvent {
|
||||||
|
path: callback_path.to_path_buf(),
|
||||||
|
kind: Some(PathEventKind::Rescan),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if !path_events.is_empty() {
|
||||||
|
path_events.sort();
|
||||||
|
let mut pending_paths = pending_paths.lock();
|
||||||
|
if pending_paths.is_empty() {
|
||||||
|
tx.try_send(()).ok();
|
||||||
|
}
|
||||||
|
coalesce_pending_rescans(&mut pending_paths, &mut path_events);
|
||||||
|
util::extend_sorted(
|
||||||
|
&mut *pending_paths,
|
||||||
|
path_events,
|
||||||
|
usize::MAX,
|
||||||
|
|a, b| a.path.cmp(&b.path),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})??;
|
||||||
|
|
||||||
self.registrations
|
self.registrations
|
||||||
.lock()
|
.lock()
|
||||||
|
|
@ -114,85 +157,10 @@ impl Watcher for FsWatcher {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
};
|
};
|
||||||
|
|
||||||
global_watcher().remove(registration);
|
global(|w| w.remove(registration))
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn enqueue_path_events(
|
|
||||||
tx: &smol::channel::Sender<()>,
|
|
||||||
pending_path_events: &Arc<Mutex<Vec<PathEvent>>>,
|
|
||||||
mut path_events: Vec<PathEvent>,
|
|
||||||
) {
|
|
||||||
if path_events.is_empty() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
path_events.sort();
|
|
||||||
let mut pending_paths = pending_path_events.lock();
|
|
||||||
if pending_paths.is_empty() {
|
|
||||||
tx.try_send(()).ok();
|
|
||||||
}
|
|
||||||
coalesce_pending_rescans(&mut pending_paths, &mut path_events);
|
|
||||||
util::extend_sorted(&mut *pending_paths, path_events, usize::MAX, |a, b| {
|
|
||||||
a.path.cmp(&b.path)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn push_notify_event(
|
|
||||||
tx: &smol::channel::Sender<()>,
|
|
||||||
pending_path_events: &Arc<Mutex<Vec<PathEvent>>>,
|
|
||||||
root_path: &SanitizedPath,
|
|
||||||
watched_root: &Path,
|
|
||||||
event: ¬ify::Event,
|
|
||||||
) {
|
|
||||||
let kind = match event.kind {
|
|
||||||
EventKind::Create(_) => Some(PathEventKind::Created),
|
|
||||||
EventKind::Modify(_) => Some(PathEventKind::Changed),
|
|
||||||
EventKind::Remove(_) => Some(PathEventKind::Removed),
|
|
||||||
_ => None,
|
|
||||||
};
|
|
||||||
let mut path_events = event
|
|
||||||
.paths
|
|
||||||
.iter()
|
|
||||||
.filter_map(|event_path| {
|
|
||||||
let event_path = SanitizedPath::new(event_path);
|
|
||||||
event_path.starts_with(root_path).then(|| PathEvent {
|
|
||||||
path: event_path.as_path().to_path_buf(),
|
|
||||||
kind,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
if event.need_rescan() {
|
|
||||||
log::warn!("filesystem watcher lost sync for {watched_root:?}; scheduling rescan");
|
|
||||||
path_events.retain(|path_event| path_event.path != watched_root);
|
|
||||||
path_events.push(PathEvent {
|
|
||||||
path: watched_root.to_path_buf(),
|
|
||||||
kind: Some(PathEventKind::Rescan),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
enqueue_path_events(tx, pending_path_events, path_events);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn push_notify_error(
|
|
||||||
tx: &smol::channel::Sender<()>,
|
|
||||||
pending_path_events: &Arc<Mutex<Vec<PathEvent>>>,
|
|
||||||
watched_root: &Path,
|
|
||||||
error: ¬ify::Error,
|
|
||||||
) {
|
|
||||||
log::warn!("watcher error for {watched_root:?}: {error}");
|
|
||||||
enqueue_path_events(
|
|
||||||
tx,
|
|
||||||
pending_path_events,
|
|
||||||
vec![PathEvent {
|
|
||||||
path: watched_root.to_path_buf(),
|
|
||||||
kind: Some(PathEventKind::Rescan),
|
|
||||||
}],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn coalesce_pending_rescans(pending_paths: &mut Vec<PathEvent>, path_events: &mut Vec<PathEvent>) {
|
fn coalesce_pending_rescans(pending_paths: &mut Vec<PathEvent>, path_events: &mut Vec<PathEvent>) {
|
||||||
if !path_events
|
if !path_events
|
||||||
.iter()
|
.iter()
|
||||||
|
|
@ -247,34 +215,29 @@ fn is_covered_rescan(kind: Option<PathEventKind>, path: &Path, ancestor: &Path)
|
||||||
pub struct WatcherRegistrationId(u32);
|
pub struct WatcherRegistrationId(u32);
|
||||||
|
|
||||||
struct WatcherRegistrationState {
|
struct WatcherRegistrationState {
|
||||||
callback: Arc<dyn for<'a> Fn(Result<&'a notify::Event, &'a notify::Error>) + Send + Sync>,
|
callback: Arc<dyn Fn(¬ify::Event) + Send + Sync>,
|
||||||
path: Arc<std::path::Path>,
|
path: Arc<std::path::Path>,
|
||||||
mode: WatcherMode,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct WatcherState {
|
struct WatcherState {
|
||||||
watchers: HashMap<WatcherRegistrationId, WatcherRegistrationState>,
|
watchers: HashMap<WatcherRegistrationId, WatcherRegistrationState>,
|
||||||
native_path_registrations: HashMap<Arc<std::path::Path>, u32>,
|
path_registrations: HashMap<Arc<std::path::Path>, u32>,
|
||||||
poll_path_registrations: HashMap<Arc<std::path::Path>, u32>,
|
|
||||||
last_registration: WatcherRegistrationId,
|
last_registration: WatcherRegistrationId,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WatcherState {
|
|
||||||
fn path_registrations(&mut self, mode: WatcherMode) -> &mut HashMap<Arc<std::path::Path>, u32> {
|
|
||||||
match mode {
|
|
||||||
WatcherMode::Native => &mut self.native_path_registrations,
|
|
||||||
WatcherMode::Poll => &mut self.poll_path_registrations,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct GlobalWatcher {
|
pub struct GlobalWatcher {
|
||||||
state: Mutex<WatcherState>,
|
state: Mutex<WatcherState>,
|
||||||
|
|
||||||
// DANGER: never keep state lock while holding watcher lock
|
// DANGER: never keep the state lock while holding the watcher lock
|
||||||
// two mutexes because calling watcher.add triggers watcher.event, which needs watchers.
|
// two mutexes because calling watcher.add triggers an watcher.event, which needs watchers.
|
||||||
native_watcher: Mutex<Option<notify::RecommendedWatcher>>,
|
#[cfg(target_os = "linux")]
|
||||||
poll_watcher: Mutex<Option<notify::PollWatcher>>,
|
watcher: Mutex<notify::INotifyWatcher>,
|
||||||
|
#[cfg(target_os = "freebsd")]
|
||||||
|
watcher: Mutex<notify::KqueueWatcher>,
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
watcher: Mutex<notify::ReadDirectoryChangesWatcher>,
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
watcher: Mutex<notify::FsEventWatcher>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GlobalWatcher {
|
impl GlobalWatcher {
|
||||||
|
|
@ -282,17 +245,27 @@ impl GlobalWatcher {
|
||||||
fn add(
|
fn add(
|
||||||
&self,
|
&self,
|
||||||
path: Arc<std::path::Path>,
|
path: Arc<std::path::Path>,
|
||||||
mode: WatcherMode,
|
mode: notify::RecursiveMode,
|
||||||
cb: impl for<'a> Fn(Result<&'a notify::Event, &'a notify::Error>) + Send + Sync + 'static,
|
cb: impl Fn(¬ify::Event) + Send + Sync + 'static,
|
||||||
) -> anyhow::Result<WatcherRegistrationId> {
|
) -> anyhow::Result<WatcherRegistrationId> {
|
||||||
let mut state = self.state.lock();
|
use notify::Watcher;
|
||||||
let registrations_for_mode = state.path_registrations(mode);
|
|
||||||
let path_already_covered =
|
|
||||||
path_already_covered(path.as_ref(), registrations_for_mode, mode);
|
|
||||||
|
|
||||||
if !path_already_covered && !registrations_for_mode.contains_key(&path) {
|
let mut state = self.state.lock();
|
||||||
|
|
||||||
|
// Check if this path is already covered by an existing watched ancestor path.
|
||||||
|
// On macOS and Windows, watching is recursive, so we don't need to watch
|
||||||
|
// child paths if an ancestor is already being watched.
|
||||||
|
#[cfg(any(target_os = "windows", target_os = "macos"))]
|
||||||
|
let path_already_covered = state.path_registrations.keys().any(|existing| {
|
||||||
|
path.starts_with(existing.as_ref()) && path.as_ref() != existing.as_ref()
|
||||||
|
});
|
||||||
|
|
||||||
|
#[cfg(not(any(target_os = "windows", target_os = "macos")))]
|
||||||
|
let path_already_covered = false;
|
||||||
|
|
||||||
|
if !path_already_covered && !state.path_registrations.contains_key(&path) {
|
||||||
drop(state);
|
drop(state);
|
||||||
self.watch(&path, mode)?;
|
self.watcher.lock().watch(&path, mode)?;
|
||||||
state = self.state.lock();
|
state = self.state.lock();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -302,191 +275,39 @@ impl GlobalWatcher {
|
||||||
let registration_state = WatcherRegistrationState {
|
let registration_state = WatcherRegistrationState {
|
||||||
callback: Arc::new(cb),
|
callback: Arc::new(cb),
|
||||||
path: path.clone(),
|
path: path.clone(),
|
||||||
mode,
|
|
||||||
};
|
};
|
||||||
state.watchers.insert(id, registration_state);
|
state.watchers.insert(id, registration_state);
|
||||||
*state.path_registrations(mode).entry(path).or_insert(0) += 1;
|
*state.path_registrations.entry(path).or_insert(0) += 1;
|
||||||
|
|
||||||
Ok(id)
|
Ok(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn remove(&self, id: WatcherRegistrationId) {
|
pub fn remove(&self, id: WatcherRegistrationId) {
|
||||||
|
use notify::Watcher;
|
||||||
let mut state = self.state.lock();
|
let mut state = self.state.lock();
|
||||||
let Some(registration_state) = state.watchers.remove(&id) else {
|
let Some(registration_state) = state.watchers.remove(&id) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let path_registrations = state.path_registrations(registration_state.mode);
|
let Some(count) = state.path_registrations.get_mut(®istration_state.path) else {
|
||||||
let Some(count) = path_registrations.get_mut(®istration_state.path) else {
|
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
*count -= 1;
|
*count -= 1;
|
||||||
if *count == 0 {
|
if *count == 0 {
|
||||||
path_registrations.remove(®istration_state.path);
|
state.path_registrations.remove(®istration_state.path);
|
||||||
let path_still_covered = path_already_covered(
|
|
||||||
registration_state.path.as_ref(),
|
|
||||||
path_registrations,
|
|
||||||
registration_state.mode,
|
|
||||||
);
|
|
||||||
|
|
||||||
if !path_still_covered {
|
drop(state);
|
||||||
drop(state);
|
self.watcher
|
||||||
self.unwatch(®istration_state.path, registration_state.mode)
|
.lock()
|
||||||
.log_err();
|
.unwatch(®istration_state.path)
|
||||||
}
|
.log_err();
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn watch(&self, path: &Path, mode: WatcherMode) -> anyhow::Result<()> {
|
|
||||||
use notify::Watcher;
|
|
||||||
|
|
||||||
match mode {
|
|
||||||
WatcherMode::Native => {
|
|
||||||
self.ensure_native_watcher()?;
|
|
||||||
self.native_watcher
|
|
||||||
.lock()
|
|
||||||
.as_mut()
|
|
||||||
.expect("native watcher initialized")
|
|
||||||
.watch(
|
|
||||||
path,
|
|
||||||
if cfg!(any(target_os = "windows", target_os = "macos")) {
|
|
||||||
notify::RecursiveMode::Recursive
|
|
||||||
} else {
|
|
||||||
notify::RecursiveMode::NonRecursive
|
|
||||||
},
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
WatcherMode::Poll => {
|
|
||||||
self.ensure_poll_watcher()?;
|
|
||||||
self.poll_watcher
|
|
||||||
.lock()
|
|
||||||
.as_mut()
|
|
||||||
.expect("poll watcher initialized")
|
|
||||||
.watch(path, notify::RecursiveMode::Recursive)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn unwatch(&self, path: &Path, mode: WatcherMode) -> anyhow::Result<()> {
|
|
||||||
use notify::Watcher;
|
|
||||||
|
|
||||||
match mode {
|
|
||||||
WatcherMode::Native => {
|
|
||||||
if let Some(watcher) = self.native_watcher.lock().as_mut() {
|
|
||||||
watcher.unwatch(path)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
WatcherMode::Poll => {
|
|
||||||
if let Some(watcher) = self.poll_watcher.lock().as_mut() {
|
|
||||||
watcher.unwatch(path)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn ensure_native_watcher(&self) -> anyhow::Result<()> {
|
|
||||||
if self.native_watcher.lock().is_some() {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
let watcher = notify::recommended_watcher(handle_native_event)?;
|
|
||||||
*self.native_watcher.lock() = Some(watcher);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn ensure_poll_watcher(&self) -> anyhow::Result<()> {
|
|
||||||
if self.poll_watcher.lock().is_some() {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
let config = notify::Config::default().with_poll_interval(*POLL_INTERVAL);
|
|
||||||
let watcher = notify::PollWatcher::new(handle_poll_event, config)?;
|
|
||||||
*self.poll_watcher.lock() = Some(watcher);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn path_already_covered(
|
|
||||||
path: &Path,
|
|
||||||
path_registrations: &HashMap<Arc<std::path::Path>, u32>,
|
|
||||||
mode: WatcherMode,
|
|
||||||
) -> bool {
|
|
||||||
(mode == WatcherMode::Poll || cfg!(any(target_os = "windows", target_os = "macos")))
|
|
||||||
&& path_registrations
|
|
||||||
.keys()
|
|
||||||
.any(|existing| path.starts_with(existing.as_ref()) && path != existing.as_ref())
|
|
||||||
}
|
|
||||||
|
|
||||||
static POLL_INTERVAL: LazyLock<Duration> = LazyLock::new(|| {
|
|
||||||
let poll_ms: u64 = std::env::var("ZED_FILE_WATCHER_POLL_MS")
|
|
||||||
.ok()
|
|
||||||
.and_then(|value| value.parse().ok())
|
|
||||||
.unwrap_or(2000)
|
|
||||||
.clamp(500, 30000);
|
|
||||||
Duration::from_millis(poll_ms)
|
|
||||||
});
|
|
||||||
|
|
||||||
pub fn poll_interval() -> Duration {
|
|
||||||
*POLL_INTERVAL
|
|
||||||
}
|
|
||||||
|
|
||||||
static FS_WATCHER_INSTANCE: OnceLock<GlobalWatcher> = OnceLock::new();
|
|
||||||
|
|
||||||
fn global_watcher() -> &'static GlobalWatcher {
|
|
||||||
FS_WATCHER_INSTANCE.get_or_init(|| GlobalWatcher {
|
|
||||||
state: Mutex::new(WatcherState {
|
|
||||||
watchers: Default::default(),
|
|
||||||
native_path_registrations: Default::default(),
|
|
||||||
poll_path_registrations: Default::default(),
|
|
||||||
last_registration: Default::default(),
|
|
||||||
}),
|
|
||||||
native_watcher: Mutex::new(None),
|
|
||||||
poll_watcher: Mutex::new(None),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_native_event(event: Result<notify::Event, notify::Error>) {
|
|
||||||
handle_event(WatcherMode::Native, event);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_poll_event(event: Result<notify::Event, notify::Error>) {
|
|
||||||
handle_event(WatcherMode::Poll, event);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_event(mode: WatcherMode, event: Result<notify::Event, notify::Error>) {
|
|
||||||
log::trace!("global handle event for {mode:?}: {event:?}");
|
|
||||||
|
|
||||||
let callbacks = {
|
|
||||||
let state = global_watcher().state.lock();
|
|
||||||
state
|
|
||||||
.watchers
|
|
||||||
.values()
|
|
||||||
.filter(|registration| registration.mode == mode)
|
|
||||||
.map(|registration| registration.callback.clone())
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
};
|
|
||||||
|
|
||||||
match event {
|
|
||||||
Ok(event) => {
|
|
||||||
if matches!(event.kind, EventKind::Access(_)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for callback in callbacks {
|
|
||||||
callback(Ok(&event));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(error) => {
|
|
||||||
for callback in callbacks {
|
|
||||||
callback(Err(&error));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static FS_WATCHER_INSTANCE: OnceLock<anyhow::Result<GlobalWatcher, notify::Error>> =
|
||||||
|
OnceLock::new();
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
@ -578,8 +399,45 @@ mod tests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn global<T>(f: impl FnOnce(&GlobalWatcher) -> T) -> anyhow::Result<T> {
|
fn handle_event(event: Result<notify::Event, notify::Error>) {
|
||||||
let global_watcher = global_watcher();
|
log::trace!("global handle event: {event:?}");
|
||||||
global_watcher.ensure_native_watcher()?;
|
// Filter out access events, which could lead to a weird bug on Linux after upgrading notify
|
||||||
Ok(f(global_watcher))
|
// https://github.com/zed-industries/zed/actions/runs/14085230504/job/39449448832
|
||||||
|
let Some(event) = event
|
||||||
|
.log_err()
|
||||||
|
.filter(|event| !matches!(event.kind, EventKind::Access(_)))
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
global::<()>(move |watcher| {
|
||||||
|
let callbacks = {
|
||||||
|
let state = watcher.state.lock();
|
||||||
|
state
|
||||||
|
.watchers
|
||||||
|
.values()
|
||||||
|
.map(|r| r.callback.clone())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
};
|
||||||
|
for callback in callbacks {
|
||||||
|
callback(&event);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.log_err();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn global<T>(f: impl FnOnce(&GlobalWatcher) -> T) -> anyhow::Result<T> {
|
||||||
|
let result = FS_WATCHER_INSTANCE.get_or_init(|| {
|
||||||
|
notify::recommended_watcher(handle_event).map(|file_watcher| GlobalWatcher {
|
||||||
|
state: Mutex::new(WatcherState {
|
||||||
|
watchers: Default::default(),
|
||||||
|
path_registrations: Default::default(),
|
||||||
|
last_registration: Default::default(),
|
||||||
|
}),
|
||||||
|
watcher: Mutex::new(file_watcher),
|
||||||
|
})
|
||||||
|
});
|
||||||
|
match result {
|
||||||
|
Ok(g) => Ok(f(g)),
|
||||||
|
Err(e) => Err(anyhow::anyhow!("{e}")),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -83,7 +83,6 @@ pretty_assertions.workspace = true
|
||||||
project = { workspace = true, features = ["test-support"] }
|
project = { workspace = true, features = ["test-support"] }
|
||||||
rand.workspace = true
|
rand.workspace = true
|
||||||
settings = { workspace = true, features = ["test-support"] }
|
settings = { workspace = true, features = ["test-support"] }
|
||||||
task.workspace = true
|
|
||||||
unindent.workspace = true
|
unindent.workspace = true
|
||||||
workspace = { workspace = true, features = ["test-support"] }
|
workspace = { workspace = true, features = ["test-support"] }
|
||||||
zlog.workspace = true
|
zlog.workspace = true
|
||||||
|
|
|
||||||
|
|
@ -627,6 +627,9 @@ impl PickerDelegate for BranchListDelegate {
|
||||||
let focus_handle = self.focus_handle.clone();
|
let focus_handle = self.focus_handle.clone();
|
||||||
let editor = editor.as_any().downcast_ref::<Entity<Editor>>().unwrap();
|
let editor = editor.as_any().downcast_ref::<Entity<Editor>>().unwrap();
|
||||||
|
|
||||||
|
let show_inline_filter =
|
||||||
|
self.editor_position() == PickerEditorPosition::End || !self.show_footer;
|
||||||
|
|
||||||
v_flex()
|
v_flex()
|
||||||
.when(
|
.when(
|
||||||
self.editor_position() == PickerEditorPosition::End,
|
self.editor_position() == PickerEditorPosition::End,
|
||||||
|
|
@ -639,34 +642,32 @@ impl PickerDelegate for BranchListDelegate {
|
||||||
.h_9()
|
.h_9()
|
||||||
.px_2p5()
|
.px_2p5()
|
||||||
.child(editor.clone())
|
.child(editor.clone())
|
||||||
.when(
|
.when(show_inline_filter, |this| {
|
||||||
self.editor_position() == PickerEditorPosition::End,
|
let tooltip_label = match self.branch_filter {
|
||||||
|this| {
|
BranchFilter::All => "Filter Remote Branches",
|
||||||
let tooltip_label = match self.branch_filter {
|
BranchFilter::Remote => "Show All Branches",
|
||||||
BranchFilter::All => "Filter Remote Branches",
|
};
|
||||||
BranchFilter::Remote => "Show All Branches",
|
|
||||||
};
|
|
||||||
|
|
||||||
this.gap_1().justify_between().child({
|
this.gap_1().justify_between().child({
|
||||||
IconButton::new("filter-remotes", IconName::Filter)
|
IconButton::new("filter-remotes", IconName::Filter)
|
||||||
.toggle_state(self.branch_filter == BranchFilter::Remote)
|
.toggle_state(self.branch_filter == BranchFilter::Remote)
|
||||||
.tooltip(move |_, cx| {
|
.icon_size(IconSize::Small)
|
||||||
Tooltip::for_action_in(
|
.tooltip(move |_, cx| {
|
||||||
tooltip_label,
|
Tooltip::for_action_in(
|
||||||
&branch_picker::FilterRemotes,
|
tooltip_label,
|
||||||
&focus_handle,
|
&branch_picker::FilterRemotes,
|
||||||
cx,
|
&focus_handle,
|
||||||
)
|
cx,
|
||||||
})
|
)
|
||||||
.on_click(|_click, window, cx| {
|
})
|
||||||
window.dispatch_action(
|
.on_click(|_click, window, cx| {
|
||||||
branch_picker::FilterRemotes.boxed_clone(),
|
window.dispatch_action(
|
||||||
cx,
|
branch_picker::FilterRemotes.boxed_clone(),
|
||||||
);
|
cx,
|
||||||
})
|
);
|
||||||
})
|
})
|
||||||
},
|
})
|
||||||
),
|
}),
|
||||||
)
|
)
|
||||||
.when(
|
.when(
|
||||||
self.editor_position() == PickerEditorPosition::Start,
|
self.editor_position() == PickerEditorPosition::Start,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
use crate::branch_picker::{self, BranchList};
|
use crate::branch_picker::{self, BranchList};
|
||||||
use crate::git_panel::{GitPanel, commit_message_editor, panel_editor_style};
|
use crate::git_panel::{GitPanel, commit_message_editor, panel_editor_style};
|
||||||
use crate::git_panel_settings::GitPanelSettings;
|
|
||||||
use git::repository::CommitOptions;
|
use git::repository::CommitOptions;
|
||||||
use git::{Amend, Commit, GenerateCommitMessage, Signoff};
|
use git::{Amend, Commit, GenerateCommitMessage, Signoff};
|
||||||
use panel::panel_button;
|
use panel::panel_button;
|
||||||
|
|
@ -540,18 +539,6 @@ impl Render for CommitModal {
|
||||||
let border_radius = properties.modal_border_radius;
|
let border_radius = properties.modal_border_radius;
|
||||||
let editor_focus_handle = self.commit_editor.focus_handle(cx);
|
let editor_focus_handle = self.commit_editor.focus_handle(cx);
|
||||||
|
|
||||||
let max_title_length = GitPanelSettings::get_global(cx).commit_title_max_length;
|
|
||||||
let title_exceeds_limit = if max_title_length > 0 {
|
|
||||||
self.commit_editor
|
|
||||||
.read(cx)
|
|
||||||
.text(cx)
|
|
||||||
.lines()
|
|
||||||
.next()
|
|
||||||
.is_some_and(|title| title.len() > max_title_length)
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
};
|
|
||||||
|
|
||||||
v_flex()
|
v_flex()
|
||||||
.id("commit-modal")
|
.id("commit-modal")
|
||||||
.key_context("GitCommit")
|
.key_context("GitCommit")
|
||||||
|
|
@ -580,9 +567,6 @@ impl Render for CommitModal {
|
||||||
this.toggle_branch_selector(window, cx);
|
this.toggle_branch_selector(window, cx);
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.w(width)
|
|
||||||
.h_112()
|
|
||||||
.p(container_padding)
|
|
||||||
.elevation_3(cx)
|
.elevation_3(cx)
|
||||||
.overflow_hidden()
|
.overflow_hidden()
|
||||||
.flex_none()
|
.flex_none()
|
||||||
|
|
@ -591,50 +575,30 @@ impl Render for CommitModal {
|
||||||
.rounded(px(border_radius))
|
.rounded(px(border_radius))
|
||||||
.border_1()
|
.border_1()
|
||||||
.border_color(cx.theme().colors().border)
|
.border_color(cx.theme().colors().border)
|
||||||
|
.w(width)
|
||||||
|
.p(container_padding)
|
||||||
.child(
|
.child(
|
||||||
v_flex()
|
v_flex()
|
||||||
.id("editor-container")
|
.id("editor-container")
|
||||||
.cursor_text()
|
.justify_between()
|
||||||
.p_2()
|
.p_2()
|
||||||
.size_full()
|
.size_full()
|
||||||
.gap_2()
|
.gap_2()
|
||||||
.justify_between()
|
|
||||||
.rounded(properties.editor_border_radius())
|
.rounded(properties.editor_border_radius())
|
||||||
.overflow_hidden()
|
.overflow_hidden()
|
||||||
|
.cursor_text()
|
||||||
.bg(cx.theme().colors().editor_background)
|
.bg(cx.theme().colors().editor_background)
|
||||||
.border_1()
|
.border_1()
|
||||||
.border_color(if title_exceeds_limit {
|
.border_color(cx.theme().colors().border_variant)
|
||||||
cx.theme().status().warning_border
|
|
||||||
} else {
|
|
||||||
cx.theme().colors().border_variant
|
|
||||||
})
|
|
||||||
.on_click(cx.listener(move |_, _: &ClickEvent, window, cx| {
|
.on_click(cx.listener(move |_, _: &ClickEvent, window, cx| {
|
||||||
window.focus(&editor_focus_handle, cx);
|
window.focus(&editor_focus_handle, cx);
|
||||||
}))
|
}))
|
||||||
.child(self.render_commit_editor(window, cx))
|
.child(
|
||||||
.when(title_exceeds_limit, |this| {
|
div()
|
||||||
this.child(
|
.flex_1()
|
||||||
h_flex()
|
.size_full()
|
||||||
.absolute()
|
.child(self.render_commit_editor(window, cx)),
|
||||||
.bottom_12()
|
)
|
||||||
.w_full()
|
|
||||||
.py_1()
|
|
||||||
.px_2()
|
|
||||||
.gap_1()
|
|
||||||
.justify_center()
|
|
||||||
.child(
|
|
||||||
Icon::new(IconName::Warning)
|
|
||||||
.size(IconSize::XSmall)
|
|
||||||
.color(Color::Warning),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
Label::new(format!(
|
|
||||||
"Commit message title exceeds {max_title_length}-character limit."
|
|
||||||
))
|
|
||||||
.size(LabelSize::Small),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.child(self.render_footer(window, cx)),
|
.child(self.render_footer(window, cx)),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use buffer_diff::BufferDiff;
|
use buffer_diff::BufferDiff;
|
||||||
use editor::{Editor, EditorEvent, EditorSettings, MultiBuffer, SplittableEditor};
|
use editor::{Editor, EditorEvent, MultiBuffer};
|
||||||
use futures::{FutureExt, select_biased};
|
use futures::{FutureExt, select_biased};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AnyElement, App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, FocusHandle,
|
AnyElement, App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, FocusHandle,
|
||||||
|
|
@ -10,7 +10,6 @@ use gpui::{
|
||||||
};
|
};
|
||||||
use language::{Buffer, HighlightedText, LanguageRegistry};
|
use language::{Buffer, HighlightedText, LanguageRegistry};
|
||||||
use project::Project;
|
use project::Project;
|
||||||
use settings::Settings;
|
|
||||||
use std::{
|
use std::{
|
||||||
any::{Any, TypeId},
|
any::{Any, TypeId},
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
|
|
@ -27,7 +26,7 @@ use workspace::{
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct FileDiffView {
|
pub struct FileDiffView {
|
||||||
editor: Entity<SplittableEditor>,
|
editor: Entity<Editor>,
|
||||||
old_buffer: Entity<Buffer>,
|
old_buffer: Entity<Buffer>,
|
||||||
new_buffer: Entity<Buffer>,
|
new_buffer: Entity<Buffer>,
|
||||||
buffer_changes_tx: watch::Sender<()>,
|
buffer_changes_tx: watch::Sender<()>,
|
||||||
|
|
@ -58,14 +57,12 @@ impl FileDiffView {
|
||||||
let buffer_diff = build_buffer_diff(&old_buffer, &new_buffer, languages, cx).await?;
|
let buffer_diff = build_buffer_diff(&old_buffer, &new_buffer, languages, cx).await?;
|
||||||
|
|
||||||
workspace.update_in(cx, |workspace, window, cx| {
|
workspace.update_in(cx, |workspace, window, cx| {
|
||||||
let workspace_entity = cx.entity();
|
|
||||||
let diff_view = cx.new(|cx| {
|
let diff_view = cx.new(|cx| {
|
||||||
FileDiffView::new(
|
FileDiffView::new(
|
||||||
old_buffer,
|
old_buffer,
|
||||||
new_buffer,
|
new_buffer,
|
||||||
buffer_diff,
|
buffer_diff,
|
||||||
project.clone(),
|
project.clone(),
|
||||||
workspace_entity,
|
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
|
|
@ -86,7 +83,6 @@ impl FileDiffView {
|
||||||
new_buffer: Entity<Buffer>,
|
new_buffer: Entity<Buffer>,
|
||||||
diff: Entity<BufferDiff>,
|
diff: Entity<BufferDiff>,
|
||||||
project: Entity<Project>,
|
project: Entity<Project>,
|
||||||
workspace: Entity<Workspace>,
|
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
|
|
@ -96,19 +92,16 @@ impl FileDiffView {
|
||||||
multibuffer
|
multibuffer
|
||||||
});
|
});
|
||||||
let editor = cx.new(|cx| {
|
let editor = cx.new(|cx| {
|
||||||
let splittable = SplittableEditor::new(
|
let mut editor =
|
||||||
EditorSettings::get_global(cx).diff_view_style,
|
Editor::for_multibuffer(multibuffer.clone(), Some(project.clone()), window, cx);
|
||||||
multibuffer.clone(),
|
editor.start_temporary_diff_override();
|
||||||
project.clone(),
|
editor.disable_diagnostics(cx);
|
||||||
workspace,
|
editor.set_expand_all_diff_hunks(cx);
|
||||||
window,
|
editor.set_render_diff_hunk_controls(
|
||||||
|
Arc::new(|_, _, _, _, _, _, _, _| gpui::Empty.into_any_element()),
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
splittable.rhs_editor().update(cx, |editor, _| {
|
editor
|
||||||
editor.start_temporary_diff_override();
|
|
||||||
});
|
|
||||||
splittable.disable_diff_hunk_controls(cx);
|
|
||||||
splittable
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let (buffer_changes_tx, mut buffer_changes_rx) = watch::channel(());
|
let (buffer_changes_tx, mut buffer_changes_rx) = watch::channel(());
|
||||||
|
|
@ -275,19 +268,22 @@ impl Item for FileDiffView {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn deactivated(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
fn deactivated(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
self.editor.deactivated(window, cx);
|
self.editor
|
||||||
|
.update(cx, |editor, cx| editor.deactivated(window, cx));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn act_as_type<'a>(
|
fn act_as_type<'a>(
|
||||||
&'a self,
|
&'a self,
|
||||||
type_id: TypeId,
|
type_id: TypeId,
|
||||||
self_handle: &'a Entity<Self>,
|
self_handle: &'a Entity<Self>,
|
||||||
cx: &'a App,
|
_: &'a App,
|
||||||
) -> Option<gpui::AnyEntity> {
|
) -> Option<gpui::AnyEntity> {
|
||||||
if type_id == TypeId::of::<Self>() {
|
if type_id == TypeId::of::<Self>() {
|
||||||
Some(self_handle.clone().into())
|
Some(self_handle.clone().into())
|
||||||
|
} else if type_id == TypeId::of::<Editor>() {
|
||||||
|
Some(self.editor.clone().into())
|
||||||
} else {
|
} else {
|
||||||
self.editor.act_as_type(type_id, cx)
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -309,10 +305,8 @@ impl Item for FileDiffView {
|
||||||
_: &mut Window,
|
_: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
self.editor.update(cx, |editor, cx| {
|
self.editor.update(cx, |editor, _| {
|
||||||
editor.rhs_editor().update(cx, |editor, _| {
|
editor.set_nav_history(Some(nav_history));
|
||||||
editor.set_nav_history(Some(nav_history));
|
|
||||||
})
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -322,11 +316,8 @@ impl Item for FileDiffView {
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
self.editor.update(cx, |editor, cx| {
|
self.editor
|
||||||
editor
|
.update(cx, |editor, cx| editor.navigate(data, window, cx))
|
||||||
.rhs_editor()
|
|
||||||
.update(cx, |editor, cx| editor.navigate(data, window, cx))
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn breadcrumb_location(&self, _: &App) -> ToolbarItemLocation {
|
fn breadcrumb_location(&self, _: &App) -> ToolbarItemLocation {
|
||||||
|
|
@ -344,14 +335,13 @@ impl Item for FileDiffView {
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
self.editor.update(cx, |editor, cx| {
|
self.editor.update(cx, |editor, cx| {
|
||||||
editor.rhs_editor().update(cx, |editor, cx| {
|
editor.added_to_workspace(workspace, window, cx)
|
||||||
editor.added_to_workspace(workspace, window, cx)
|
|
||||||
})
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn can_save(&self, cx: &App) -> bool {
|
fn can_save(&self, cx: &App) -> bool {
|
||||||
self.editor.read(cx).rhs_editor().read(cx).can_save(cx)
|
// The editor handles the new buffer, so delegate to it
|
||||||
|
self.editor.read(cx).can_save(cx)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn save(
|
fn save(
|
||||||
|
|
@ -361,7 +351,9 @@ impl Item for FileDiffView {
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> Task<Result<()>> {
|
) -> Task<Result<()>> {
|
||||||
self.editor.save(options, project, window, cx)
|
// Delegate saving to the editor, which manages the new buffer
|
||||||
|
self.editor
|
||||||
|
.update(cx, |editor, cx| editor.save(options, project, window, cx))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -375,10 +367,9 @@ impl Render for FileDiffView {
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use editor::test::editor_test_context::assert_state_with_diff;
|
use editor::test::editor_test_context::assert_state_with_diff;
|
||||||
use gpui::BorrowAppContext;
|
|
||||||
use gpui::TestAppContext;
|
use gpui::TestAppContext;
|
||||||
use project::{FakeFs, Fs, Project};
|
use project::{FakeFs, Fs, Project};
|
||||||
use settings::{DiffViewStyle, SettingsStore};
|
use settings::SettingsStore;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use unindent::unindent;
|
use unindent::unindent;
|
||||||
use util::path;
|
use util::path;
|
||||||
|
|
@ -388,11 +379,6 @@ mod tests {
|
||||||
cx.update(|cx| {
|
cx.update(|cx| {
|
||||||
let settings_store = SettingsStore::test(cx);
|
let settings_store = SettingsStore::test(cx);
|
||||||
cx.set_global(settings_store);
|
cx.set_global(settings_store);
|
||||||
cx.update_global::<SettingsStore, _>(|store, cx| {
|
|
||||||
store.update_user_settings(cx, |settings| {
|
|
||||||
settings.editor.diff_view_style = Some(DiffViewStyle::Unified);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
theme_settings::init(theme::LoadThemes::JustBase, cx);
|
theme_settings::init(theme::LoadThemes::JustBase, cx);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -432,9 +418,7 @@ mod tests {
|
||||||
|
|
||||||
// Verify initial diff
|
// Verify initial diff
|
||||||
assert_state_with_diff(
|
assert_state_with_diff(
|
||||||
&diff_view.read_with(cx, |diff_view, cx| {
|
&diff_view.read_with(cx, |diff_view, _| diff_view.editor.clone()),
|
||||||
diff_view.editor.read(cx).rhs_editor().clone()
|
|
||||||
}),
|
|
||||||
cx,
|
cx,
|
||||||
&unindent(
|
&unindent(
|
||||||
"
|
"
|
||||||
|
|
@ -469,9 +453,7 @@ mod tests {
|
||||||
// The diff now reflects the changes to the new file
|
// The diff now reflects the changes to the new file
|
||||||
cx.executor().advance_clock(RECALCULATE_DIFF_DEBOUNCE);
|
cx.executor().advance_clock(RECALCULATE_DIFF_DEBOUNCE);
|
||||||
assert_state_with_diff(
|
assert_state_with_diff(
|
||||||
&diff_view.read_with(cx, |diff_view, cx| {
|
&diff_view.read_with(cx, |diff_view, _| diff_view.editor.clone()),
|
||||||
diff_view.editor.read(cx).rhs_editor().clone()
|
|
||||||
}),
|
|
||||||
cx,
|
cx,
|
||||||
&unindent(
|
&unindent(
|
||||||
"
|
"
|
||||||
|
|
@ -506,9 +488,7 @@ mod tests {
|
||||||
// The diff now reflects the changes to the new file
|
// The diff now reflects the changes to the new file
|
||||||
cx.executor().advance_clock(RECALCULATE_DIFF_DEBOUNCE);
|
cx.executor().advance_clock(RECALCULATE_DIFF_DEBOUNCE);
|
||||||
assert_state_with_diff(
|
assert_state_with_diff(
|
||||||
&diff_view.read_with(cx, |diff_view, cx| {
|
&diff_view.read_with(cx, |diff_view, _| diff_view.editor.clone()),
|
||||||
diff_view.editor.read(cx).rhs_editor().clone()
|
|
||||||
}),
|
|
||||||
cx,
|
cx,
|
||||||
&unindent(
|
&unindent(
|
||||||
"
|
"
|
||||||
|
|
@ -572,10 +552,8 @@ mod tests {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
diff_view.update_in(cx, |diff_view, window, cx| {
|
diff_view.update_in(cx, |diff_view, window, cx| {
|
||||||
diff_view.editor.update(cx, |splittable, cx| {
|
diff_view.editor.update(cx, |editor, cx| {
|
||||||
splittable.rhs_editor().update(cx, |editor, cx| {
|
editor.insert("modified ", window, cx);
|
||||||
editor.insert("modified ", window, cx);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4343,18 +4343,6 @@ impl GitPanel {
|
||||||
editor.max_point(cx).row().0 >= MAX_PANEL_EDITOR_LINES as u32
|
editor.max_point(cx).row().0 >= MAX_PANEL_EDITOR_LINES as u32
|
||||||
});
|
});
|
||||||
|
|
||||||
let max_title_length = GitPanelSettings::get_global(cx).commit_title_max_length;
|
|
||||||
let title_exceeds_limit = if max_title_length > 0 {
|
|
||||||
self.commit_editor
|
|
||||||
.read(cx)
|
|
||||||
.text(cx)
|
|
||||||
.lines()
|
|
||||||
.next()
|
|
||||||
.is_some_and(|title| title.len() > max_title_length)
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
};
|
|
||||||
|
|
||||||
let footer = v_flex()
|
let footer = v_flex()
|
||||||
.child(PanelRepoFooter::new(
|
.child(PanelRepoFooter::new(
|
||||||
display_name,
|
display_name,
|
||||||
|
|
@ -4362,41 +4350,15 @@ impl GitPanel {
|
||||||
head_commit,
|
head_commit,
|
||||||
Some(git_panel),
|
Some(git_panel),
|
||||||
))
|
))
|
||||||
.when(title_exceeds_limit, |this| {
|
|
||||||
this.child(
|
|
||||||
h_flex()
|
|
||||||
.px_2()
|
|
||||||
.py_1()
|
|
||||||
.gap_1()
|
|
||||||
.border_t_1()
|
|
||||||
.border_color(cx.theme().status().warning_border)
|
|
||||||
.bg(cx.theme().status().warning_background.opacity(0.5))
|
|
||||||
.child(
|
|
||||||
Icon::new(IconName::Warning)
|
|
||||||
.size(IconSize::XSmall)
|
|
||||||
.color(Color::Warning),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
Label::new(format!(
|
|
||||||
"Commit message title exceeds {max_title_length}-character limit."
|
|
||||||
))
|
|
||||||
.size(LabelSize::Small),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.child(
|
.child(
|
||||||
panel_editor_container(window, cx)
|
panel_editor_container(window, cx)
|
||||||
.id("commit-editor-container")
|
.id("commit-editor-container")
|
||||||
.cursor_text()
|
|
||||||
.relative()
|
.relative()
|
||||||
.w_full()
|
.w_full()
|
||||||
.h(max_height + footer_size)
|
.h(max_height + footer_size)
|
||||||
.border_t_1()
|
.border_t_1()
|
||||||
.border_color(if title_exceeds_limit {
|
.border_color(cx.theme().colors().border)
|
||||||
cx.theme().status().warning_border
|
.cursor_text()
|
||||||
} else {
|
|
||||||
cx.theme().colors().border
|
|
||||||
})
|
|
||||||
.on_click(cx.listener(move |this, _: &ClickEvent, window, cx| {
|
.on_click(cx.listener(move |this, _: &ClickEvent, window, cx| {
|
||||||
window.focus(&this.commit_editor.focus_handle(cx), cx);
|
window.focus(&this.commit_editor.focus_handle(cx), cx);
|
||||||
}))
|
}))
|
||||||
|
|
@ -4977,46 +4939,57 @@ impl GitPanel {
|
||||||
let toggle_state = self.header_state(header.header);
|
let toggle_state = self.header_state(header.header);
|
||||||
let section = header.header;
|
let section = header.header;
|
||||||
let weak = cx.weak_entity();
|
let weak = cx.weak_entity();
|
||||||
|
let show_checkbox_persistently = !matches!(&toggle_state, ToggleState::Unselected);
|
||||||
|
|
||||||
h_flex()
|
h_flex()
|
||||||
.id(id)
|
.id(id)
|
||||||
.cursor_pointer()
|
.group(group_name.clone())
|
||||||
.group(group_name)
|
|
||||||
.h(self.list_item_height())
|
.h(self.list_item_height())
|
||||||
.w_full()
|
.w_full()
|
||||||
|
.items_center()
|
||||||
.pl_3()
|
.pl_3()
|
||||||
.pr_1()
|
.pr_1()
|
||||||
.gap_2()
|
.gap_1p5()
|
||||||
.justify_between()
|
|
||||||
.hover(|s| s.bg(cx.theme().colors().ghost_element_hover))
|
|
||||||
.border_1()
|
.border_1()
|
||||||
.border_r_2()
|
.border_r_2()
|
||||||
.child(
|
.child(
|
||||||
Label::new(header.title())
|
h_flex().flex_1().child(
|
||||||
.color(Color::Muted)
|
Label::new(header.title())
|
||||||
.size(LabelSize::Small),
|
.color(Color::Muted)
|
||||||
|
.size(LabelSize::Small)
|
||||||
|
.line_height_style(LineHeightStyle::UiLabel)
|
||||||
|
.single_line(),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
Checkbox::new(checkbox_id, toggle_state)
|
div()
|
||||||
.disabled(!has_write_access)
|
.flex_none()
|
||||||
.fill()
|
.cursor_pointer()
|
||||||
.elevation(ElevationIndex::Surface),
|
.child(
|
||||||
)
|
Checkbox::new(checkbox_id, toggle_state)
|
||||||
.on_click(move |_, window, cx| {
|
.disabled(!has_write_access)
|
||||||
if !has_write_access {
|
.fill()
|
||||||
return;
|
.elevation(ElevationIndex::Surface)
|
||||||
}
|
.on_click_ext(move |_, _, window, cx| {
|
||||||
|
if !has_write_access {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
weak.update(cx, |this, cx| {
|
weak.update(cx, |this, cx| {
|
||||||
this.toggle_staged_for_entry(
|
this.toggle_staged_for_entry(
|
||||||
&GitListEntry::Header(GitHeaderEntry { header: section }),
|
&GitListEntry::Header(GitHeaderEntry { header: section }),
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
cx.stop_propagation();
|
cx.stop_propagation();
|
||||||
})
|
})
|
||||||
.ok();
|
.ok();
|
||||||
})
|
}),
|
||||||
|
)
|
||||||
|
.when(!show_checkbox_persistently, |this| {
|
||||||
|
this.visible_on_hover(group_name)
|
||||||
|
}),
|
||||||
|
)
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -6150,6 +6123,10 @@ impl RenderOnce for PanelRepoFooter {
|
||||||
util::truncate_and_trailoff(branch_name.trim_ascii(), branch_display_len)
|
util::truncate_and_trailoff(branch_name.trim_ascii(), branch_display_len)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let repo_selector_trigger = Button::new("repo-selector", truncated_repo_name)
|
||||||
|
.size(ButtonSize::None)
|
||||||
|
.label_size(LabelSize::Small);
|
||||||
|
|
||||||
let repo_selector = PopoverMenu::new("repository-switcher")
|
let repo_selector = PopoverMenu::new("repository-switcher")
|
||||||
.menu({
|
.menu({
|
||||||
let project = project;
|
let project = project;
|
||||||
|
|
@ -6159,9 +6136,8 @@ impl RenderOnce for PanelRepoFooter {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.trigger_with_tooltip(
|
.trigger_with_tooltip(
|
||||||
Button::new("repo-selector", truncated_repo_name)
|
repo_selector_trigger
|
||||||
.size(ButtonSize::None)
|
.when(single_repo, |this| this.disabled(true).color(Color::Muted))
|
||||||
.label_size(LabelSize::Small)
|
|
||||||
.truncate(true),
|
.truncate(true),
|
||||||
move |_, cx| {
|
move |_, cx| {
|
||||||
if single_repo {
|
if single_repo {
|
||||||
|
|
@ -6203,7 +6179,7 @@ impl RenderOnce for PanelRepoFooter {
|
||||||
});
|
});
|
||||||
|
|
||||||
h_flex()
|
h_flex()
|
||||||
.h_9()
|
.h(px(36.))
|
||||||
.w_full()
|
.w_full()
|
||||||
.px_2()
|
.px_2()
|
||||||
.justify_between()
|
.justify_between()
|
||||||
|
|
@ -6220,14 +6196,14 @@ impl RenderOnce for PanelRepoFooter {
|
||||||
Color::Muted
|
Color::Muted
|
||||||
},
|
},
|
||||||
))
|
))
|
||||||
.when(!single_repo, |this| {
|
.child(repo_selector)
|
||||||
this.child(repo_selector).when(show_separator, |this| {
|
.when(show_separator, |this| {
|
||||||
this.child(
|
this.child(
|
||||||
Label::new("/").size(LabelSize::Small).color(Color::Custom(
|
div()
|
||||||
cx.theme().colors().text_muted.opacity(0.4),
|
.text_sm()
|
||||||
)),
|
.text_color(cx.theme().colors().icon_muted.opacity(0.5))
|
||||||
)
|
.child("/"),
|
||||||
})
|
)
|
||||||
})
|
})
|
||||||
.child(branch_selector),
|
.child(branch_selector),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,6 @@ pub struct GitPanelSettings {
|
||||||
pub diff_stats: bool,
|
pub diff_stats: bool,
|
||||||
pub show_count_badge: bool,
|
pub show_count_badge: bool,
|
||||||
pub starts_open: bool,
|
pub starts_open: bool,
|
||||||
pub commit_title_max_length: usize,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
|
|
@ -77,7 +76,6 @@ impl Settings for GitPanelSettings {
|
||||||
diff_stats: git_panel.diff_stats.unwrap(),
|
diff_stats: git_panel.diff_stats.unwrap(),
|
||||||
show_count_badge: git_panel.show_count_badge.unwrap(),
|
show_count_badge: git_panel.show_count_badge.unwrap(),
|
||||||
starts_open: git_panel.starts_open.unwrap(),
|
starts_open: git_panel.starts_open.unwrap(),
|
||||||
commit_title_max_length: git_panel.commit_title_max_length.unwrap(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -359,9 +359,13 @@ impl ProjectDiff {
|
||||||
);
|
);
|
||||||
match branch_diff.read(cx).diff_base() {
|
match branch_diff.read(cx).diff_base() {
|
||||||
DiffBase::Head => {}
|
DiffBase::Head => {}
|
||||||
DiffBase::Merge { .. } => diff_display_editor.disable_diff_hunk_controls(cx),
|
DiffBase::Merge { .. } => diff_display_editor.set_render_diff_hunk_controls(
|
||||||
|
Arc::new(|_, _, _, _, _, _, _, _| gpui::Empty.into_any_element()),
|
||||||
|
cx,
|
||||||
|
),
|
||||||
}
|
}
|
||||||
diff_display_editor.rhs_editor().update(cx, |editor, cx| {
|
diff_display_editor.rhs_editor().update(cx, |editor, cx| {
|
||||||
|
editor.disable_diagnostics(cx);
|
||||||
editor.set_show_diff_review_button(true, cx);
|
editor.set_show_diff_review_button(true, cx);
|
||||||
|
|
||||||
match branch_diff.read(cx).diff_base() {
|
match branch_diff.read(cx).diff_base() {
|
||||||
|
|
|
||||||
|
|
@ -178,9 +178,14 @@ impl TextDiffView {
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
splittable.disable_diff_hunk_controls(cx);
|
splittable.set_render_diff_hunk_controls(
|
||||||
splittable.rhs_editor().update(cx, |editor, _cx| {
|
Arc::new(|_, _, _, _, _, _, _, _| gpui::Empty.into_any_element()),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
splittable.rhs_editor().update(cx, |editor, cx| {
|
||||||
editor.start_temporary_diff_override();
|
editor.start_temporary_diff_override();
|
||||||
|
editor.disable_diagnostics(cx);
|
||||||
|
editor.set_expand_all_diff_hunks(cx);
|
||||||
});
|
});
|
||||||
splittable
|
splittable
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -787,232 +787,23 @@ async fn open_worktree_workspace(
|
||||||
window_handle.update(cx, |multi_workspace, window, cx| {
|
window_handle.update(cx, |multi_workspace, window, cx| {
|
||||||
multi_workspace.activate(new_workspace.clone(), source_for_transfer, window, cx);
|
multi_workspace.activate(new_workspace.clone(), source_for_transfer, window, cx);
|
||||||
|
|
||||||
if is_creating_new_worktree {
|
new_workspace.update(cx, |workspace, cx| {
|
||||||
new_workspace.update(cx, |workspace, cx| {
|
workspace.run_create_worktree_tasks(window, cx);
|
||||||
workspace.run_create_worktree_tasks(window, cx);
|
});
|
||||||
|
})?;
|
||||||
|
|
||||||
if let Some(dock_position) = focused_dock {
|
if is_creating_new_worktree {
|
||||||
|
if let Some(dock_position) = focused_dock {
|
||||||
|
window_handle.update(cx, |_multi_workspace, window, cx| {
|
||||||
|
new_workspace.update(cx, |workspace, cx| {
|
||||||
let dock = workspace.dock_at_position(dock_position);
|
let dock = workspace.dock_at_position(dock_position);
|
||||||
if let Some(panel) = dock.read(cx).active_panel() {
|
if let Some(panel) = dock.read(cx).active_panel() {
|
||||||
panel.panel_focus_handle(cx).focus(window, cx);
|
panel.panel_focus_handle(cx).focus(window, cx);
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
});
|
})?;
|
||||||
}
|
}
|
||||||
})?;
|
}
|
||||||
|
|
||||||
anyhow::Ok(())
|
anyhow::Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use fs::Fs;
|
|
||||||
use gpui::{App, Task, TestAppContext};
|
|
||||||
use language::language_settings::AllLanguageSettings;
|
|
||||||
use project::project_settings::ProjectSettings;
|
|
||||||
use project::task_store::{TaskSettingsLocation, TaskStore};
|
|
||||||
use project::{FakeFs, WorktreeSettings};
|
|
||||||
use serde_json::json;
|
|
||||||
use settings::{SettingsLocation, SettingsStore};
|
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
use std::process::ExitStatus;
|
|
||||||
use std::sync::Mutex;
|
|
||||||
use task::SpawnInTerminal;
|
|
||||||
use theme::LoadThemes;
|
|
||||||
use util::path;
|
|
||||||
use util::rel_path::rel_path;
|
|
||||||
use workspace::{TerminalProvider, WorkspaceSettings};
|
|
||||||
|
|
||||||
struct CountingTerminalProvider {
|
|
||||||
spawned_task_labels: Arc<Mutex<Vec<String>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TerminalProvider for CountingTerminalProvider {
|
|
||||||
fn spawn(
|
|
||||||
&self,
|
|
||||||
task: SpawnInTerminal,
|
|
||||||
_window: &mut ui::Window,
|
|
||||||
_cx: &mut App,
|
|
||||||
) -> Task<Option<anyhow::Result<ExitStatus>>> {
|
|
||||||
self.spawned_task_labels
|
|
||||||
.lock()
|
|
||||||
.expect("terminal spawn mutex should not be poisoned")
|
|
||||||
.push(task.label);
|
|
||||||
Task::ready(Some(Ok(ExitStatus::default())))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn init_test(cx: &mut TestAppContext) {
|
|
||||||
zlog::init_test();
|
|
||||||
cx.update(|cx| {
|
|
||||||
let settings_store = SettingsStore::test(cx);
|
|
||||||
cx.set_global(settings_store);
|
|
||||||
theme_settings::init(LoadThemes::JustBase, cx);
|
|
||||||
AllLanguageSettings::register(cx);
|
|
||||||
editor::init(cx);
|
|
||||||
ProjectSettings::register(cx);
|
|
||||||
WorktreeSettings::register(cx);
|
|
||||||
WorkspaceSettings::register(cx);
|
|
||||||
TaskStore::init(None);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn install_counting_provider_and_worktree_hook(
|
|
||||||
workspace: &Entity<Workspace>,
|
|
||||||
spawned_task_labels: &Arc<Mutex<Vec<String>>>,
|
|
||||||
main_project_root: &Path,
|
|
||||||
hook_tasks_json: &str,
|
|
||||||
cx: &mut App,
|
|
||||||
) {
|
|
||||||
workspace.update(cx, |workspace, cx| {
|
|
||||||
workspace.set_terminal_provider(CountingTerminalProvider {
|
|
||||||
spawned_task_labels: spawned_task_labels.clone(),
|
|
||||||
});
|
|
||||||
|
|
||||||
let project = workspace.project().clone();
|
|
||||||
let Some(worktree) = project.read(cx).worktrees(cx).next() else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let worktree = worktree.read(cx);
|
|
||||||
let worktree_id = worktree.id();
|
|
||||||
let worktree_root = worktree.abs_path().to_path_buf();
|
|
||||||
if worktree_root == main_project_root {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let Some(task_inventory) = project
|
|
||||||
.read(cx)
|
|
||||||
.task_store()
|
|
||||||
.read(cx)
|
|
||||||
.task_inventory()
|
|
||||||
.cloned()
|
|
||||||
else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
task_inventory.update(cx, |inventory, _| {
|
|
||||||
inventory
|
|
||||||
.update_file_based_tasks(
|
|
||||||
TaskSettingsLocation::Worktree(SettingsLocation {
|
|
||||||
worktree_id,
|
|
||||||
path: rel_path(".zed"),
|
|
||||||
}),
|
|
||||||
Some(hook_tasks_json),
|
|
||||||
)
|
|
||||||
.expect("should inject create_worktree hook tasks for linked worktree");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#[gpui::test]
|
|
||||||
async fn test_create_worktree_hook_does_not_run_when_switching_back_to_main_worktree(
|
|
||||||
cx: &mut TestAppContext,
|
|
||||||
) {
|
|
||||||
init_test(cx);
|
|
||||||
|
|
||||||
let hook_tasks_json = r#"[{"label":"setup worktree","command":"echo","hide":"never","hooks":["create_worktree"]}]"#;
|
|
||||||
let fs = FakeFs::new(cx.background_executor.clone());
|
|
||||||
cx.update(|cx| <dyn Fs>::set_global(fs.clone(), cx));
|
|
||||||
fs.insert_tree(
|
|
||||||
"/root",
|
|
||||||
json!({
|
|
||||||
"project": {
|
|
||||||
".git": {},
|
|
||||||
".zed": {
|
|
||||||
"tasks.json": hook_tasks_json,
|
|
||||||
},
|
|
||||||
"src": {
|
|
||||||
"main.rs": "fn main() {}",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
let main_project_root = PathBuf::from(path!("/root/project"));
|
|
||||||
let project = Project::test(fs.clone(), [main_project_root.as_path()], cx).await;
|
|
||||||
project
|
|
||||||
.update(cx, |project, cx| project.git_scans_complete(cx))
|
|
||||||
.await;
|
|
||||||
|
|
||||||
let (multi_workspace, cx) =
|
|
||||||
cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
|
|
||||||
|
|
||||||
let spawned_task_labels = Arc::new(Mutex::new(Vec::new()));
|
|
||||||
multi_workspace.update(cx, |multi_workspace, cx| {
|
|
||||||
multi_workspace.retain_active_workspace(cx);
|
|
||||||
let active_workspace = multi_workspace.workspace().clone();
|
|
||||||
install_counting_provider_and_worktree_hook(
|
|
||||||
&active_workspace,
|
|
||||||
&spawned_task_labels,
|
|
||||||
&main_project_root,
|
|
||||||
hook_tasks_json,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
let main_workspace =
|
|
||||||
multi_workspace.read_with(cx, |multi_workspace, _| multi_workspace.workspace().clone());
|
|
||||||
main_workspace.update_in(cx, |workspace, window, cx| {
|
|
||||||
handle_create_worktree(
|
|
||||||
workspace,
|
|
||||||
&zed_actions::CreateWorktree {
|
|
||||||
worktree_name: Some("feature".to_string()),
|
|
||||||
branch_target: NewWorktreeBranchTarget::CurrentBranch,
|
|
||||||
},
|
|
||||||
window,
|
|
||||||
None,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
cx.run_until_parked();
|
|
||||||
|
|
||||||
let active_workspace =
|
|
||||||
multi_workspace.read_with(cx, |multi_workspace, _| multi_workspace.workspace().clone());
|
|
||||||
cx.update(|_, cx| {
|
|
||||||
install_counting_provider_and_worktree_hook(
|
|
||||||
&active_workspace,
|
|
||||||
&spawned_task_labels,
|
|
||||||
&main_project_root,
|
|
||||||
hook_tasks_json,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
active_workspace.update_in(cx, |workspace, window, cx| {
|
|
||||||
workspace.run_create_worktree_tasks(window, cx);
|
|
||||||
});
|
|
||||||
cx.run_until_parked();
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
spawned_task_labels
|
|
||||||
.lock()
|
|
||||||
.expect("terminal spawn mutex should not be poisoned")
|
|
||||||
.as_slice(),
|
|
||||||
["setup worktree"],
|
|
||||||
"create_worktree hook should run once for the created linked worktree"
|
|
||||||
);
|
|
||||||
|
|
||||||
active_workspace.update_in(cx, |workspace, window, cx| {
|
|
||||||
handle_switch_worktree(
|
|
||||||
workspace,
|
|
||||||
&zed_actions::SwitchWorktree {
|
|
||||||
path: main_project_root.clone(),
|
|
||||||
display_name: "project".to_string(),
|
|
||||||
},
|
|
||||||
window,
|
|
||||||
None,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
cx.run_until_parked();
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
spawned_task_labels
|
|
||||||
.lock()
|
|
||||||
.expect("terminal spawn mutex should not be poisoned")
|
|
||||||
.as_slice(),
|
|
||||||
["setup worktree"],
|
|
||||||
"switching back to the main worktree should not rerun create_worktree hooks"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1680,7 +1680,8 @@ impl App {
|
||||||
self.globals_by_type
|
self.globals_by_type
|
||||||
.get(&TypeId::of::<G>())
|
.get(&TypeId::of::<G>())
|
||||||
.map(|any_state| any_state.downcast_ref::<G>().unwrap())
|
.map(|any_state| any_state.downcast_ref::<G>().unwrap())
|
||||||
.unwrap_or_else(|| panic!("no state of type {} exists", type_name::<G>()))
|
.with_context(|| format!("no state of type {} exists", type_name::<G>()))
|
||||||
|
.unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Access the global of the given type if a value has been assigned.
|
/// Access the global of the given type if a value has been assigned.
|
||||||
|
|
@ -1698,7 +1699,8 @@ impl App {
|
||||||
self.globals_by_type
|
self.globals_by_type
|
||||||
.get_mut(&global_type)
|
.get_mut(&global_type)
|
||||||
.and_then(|any_state| any_state.downcast_mut::<G>())
|
.and_then(|any_state| any_state.downcast_mut::<G>())
|
||||||
.unwrap_or_else(|| panic!("no state of type {} exists", type_name::<G>()))
|
.with_context(|| format!("no state of type {} exists", type_name::<G>()))
|
||||||
|
.unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Access the global of the given type mutably. A default value is assigned if a global of this type has not
|
/// Access the global of the given type mutably. A default value is assigned if a global of this type has not
|
||||||
|
|
@ -1733,7 +1735,7 @@ impl App {
|
||||||
*self
|
*self
|
||||||
.globals_by_type
|
.globals_by_type
|
||||||
.remove(&global_type)
|
.remove(&global_type)
|
||||||
.unwrap_or_else(|| panic!("no global added for {}", type_name::<G>()))
|
.unwrap_or_else(|| panic!("no global added for {}", std::any::type_name::<G>()))
|
||||||
.downcast()
|
.downcast()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -738,7 +738,7 @@ pub trait InteractiveElement: Sized {
|
||||||
fn key_context<C, E>(mut self, key_context: C) -> Self
|
fn key_context<C, E>(mut self, key_context: C) -> Self
|
||||||
where
|
where
|
||||||
C: TryInto<KeyContext, Error = E>,
|
C: TryInto<KeyContext, Error = E>,
|
||||||
E: std::fmt::Display,
|
E: Debug,
|
||||||
{
|
{
|
||||||
if let Some(key_context) = key_context.try_into().log_err() {
|
if let Some(key_context) = key_context.try_into().log_err() {
|
||||||
self.interactivity().key_context = Some(key_context);
|
self.interactivity().key_context = Some(key_context);
|
||||||
|
|
|
||||||
|
|
@ -125,6 +125,14 @@ impl FollowState {
|
||||||
*self = FollowState::Tail { is_following: true };
|
*self = FollowState::Tail { is_following: true };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn stop_following(&mut self) {
|
||||||
|
if let FollowState::Tail { is_following: true } = self {
|
||||||
|
*self = FollowState::Tail {
|
||||||
|
is_following: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether the list is scrolling from top to bottom or bottom to top.
|
/// Whether the list is scrolling from top to bottom or bottom to top.
|
||||||
|
|
@ -464,9 +472,7 @@ impl ListState {
|
||||||
let state = &mut *self.0.borrow_mut();
|
let state = &mut *self.0.borrow_mut();
|
||||||
|
|
||||||
if distance < px(0.) {
|
if distance < px(0.) {
|
||||||
if let FollowState::Tail { is_following } = &mut state.follow_state {
|
state.follow_state.stop_following();
|
||||||
*is_following = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut cursor = state.items.cursor::<ListItemSummary>(());
|
let mut cursor = state.items.cursor::<ListItemSummary>(());
|
||||||
|
|
@ -544,9 +550,7 @@ impl ListState {
|
||||||
}
|
}
|
||||||
|
|
||||||
if scroll_top.item_ix < item_count {
|
if scroll_top.item_ix < item_count {
|
||||||
if let FollowState::Tail { is_following } = &mut state.follow_state {
|
state.follow_state.stop_following();
|
||||||
*is_following = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
state.logical_scroll_top = Some(scroll_top);
|
state.logical_scroll_top = Some(scroll_top);
|
||||||
|
|
@ -727,10 +731,8 @@ impl StateInner {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if let FollowState::Tail { is_following } = &mut self.follow_state {
|
if delta.y > px(0.) {
|
||||||
if delta.y > px(0.) {
|
self.follow_state.stop_following();
|
||||||
*is_following = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(handler) = self.scroll_handler.as_mut() {
|
if let Some(handler) = self.scroll_handler.as_mut() {
|
||||||
|
|
@ -1135,7 +1137,7 @@ impl StateInner {
|
||||||
content_height - self.scrollbar_drag_start_height.unwrap_or(content_height);
|
content_height - self.scrollbar_drag_start_height.unwrap_or(content_height);
|
||||||
let new_scroll_top = (point.y - drag_offset).abs().max(px(0.)).min(scroll_max);
|
let new_scroll_top = (point.y - drag_offset).abs().max(px(0.)).min(scroll_max);
|
||||||
|
|
||||||
self.follow_state = FollowState::Normal;
|
self.follow_state.stop_following();
|
||||||
|
|
||||||
if self.alignment == ListAlignment::Bottom && new_scroll_top == scroll_max {
|
if self.alignment == ListAlignment::Bottom && new_scroll_top == scroll_max {
|
||||||
self.logical_scroll_top = None;
|
self.logical_scroll_top = None;
|
||||||
|
|
@ -2014,14 +2016,11 @@ mod test {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Calling `set_follow_mode(FollowState::Normal)` or dragging the scrollbar should
|
|
||||||
/// fully disengage follow_tail — clearing any suspended state so
|
|
||||||
/// follow_tail won’t auto-re-engage.
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
fn test_follow_tail_suspended_state_cleared_by_explicit_actions(cx: &mut TestAppContext) {
|
fn test_follow_tail_reengages_after_scrollbar_disengagement(cx: &mut TestAppContext) {
|
||||||
let cx = cx.add_empty_window();
|
let cx = cx.add_empty_window();
|
||||||
|
|
||||||
// 10 items × 50px = 500px total, 200px viewport.
|
// 10 items × 50px = 500px total, 200px viewport, scroll_max = 300px.
|
||||||
let state = ListState::new(10, crate::ListAlignment::Top, px(0.)).measure_all();
|
let state = ListState::new(10, crate::ListAlignment::Top, px(0.)).measure_all();
|
||||||
|
|
||||||
struct TestView(ListState);
|
struct TestView(ListState);
|
||||||
|
|
@ -2038,59 +2037,24 @@ mod test {
|
||||||
let view = cx.update(|_, cx| cx.new(|_| TestView(state.clone())));
|
let view = cx.update(|_, cx| cx.new(|_| TestView(state.clone())));
|
||||||
|
|
||||||
state.set_follow_mode(FollowMode::Tail);
|
state.set_follow_mode(FollowMode::Tail);
|
||||||
// --- Part 1: set_follow_mode(FollowState::Normal) clears suspended state ---
|
|
||||||
|
|
||||||
cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
|
cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
|
||||||
view.clone().into_any_element()
|
view.clone().into_any_element()
|
||||||
});
|
});
|
||||||
|
assert!(state.is_following_tail());
|
||||||
|
|
||||||
// Scroll up — suspends follow_tail.
|
// Drag the scrollbar up to the middle — follow_tail should suspend.
|
||||||
cx.simulate_event(ScrollWheelEvent {
|
state.set_offset_from_scrollbar(point(px(0.), px(150.)));
|
||||||
position: point(px(50.), px(100.)),
|
|
||||||
delta: ScrollDelta::Pixels(point(px(0.), px(50.))),
|
|
||||||
..Default::default()
|
|
||||||
});
|
|
||||||
assert!(!state.is_following_tail());
|
assert!(!state.is_following_tail());
|
||||||
|
|
||||||
// Scroll back to the bottom — should re-engage follow_tail.
|
// Drag the scrollbar back to the bottom — follow_tail should re-engage
|
||||||
cx.simulate_event(ScrollWheelEvent {
|
// on the next paint.
|
||||||
position: point(px(50.), px(100.)),
|
state.set_offset_from_scrollbar(point(px(0.), px(300.)));
|
||||||
delta: ScrollDelta::Pixels(point(px(0.), px(-10000.))),
|
|
||||||
..Default::default()
|
|
||||||
});
|
|
||||||
|
|
||||||
cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
|
cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
|
||||||
view.clone().into_any_element()
|
view.into_any_element()
|
||||||
});
|
});
|
||||||
assert!(
|
assert!(
|
||||||
state.is_following_tail(),
|
state.is_following_tail(),
|
||||||
"follow_tail should re-engage after scrolling back to the bottom"
|
"follow_tail should re-engage after scrolling back to the bottom via the scrollbar"
|
||||||
);
|
|
||||||
|
|
||||||
// --- Part 2: scrollbar drag clears suspended state ---
|
|
||||||
|
|
||||||
cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
|
|
||||||
view.clone().into_any_element()
|
|
||||||
});
|
|
||||||
|
|
||||||
// Drag the scrollbar to the middle — should clear suspended state.
|
|
||||||
state.set_offset_from_scrollbar(point(px(0.), px(150.)));
|
|
||||||
|
|
||||||
// Scroll to the bottom.
|
|
||||||
cx.simulate_event(ScrollWheelEvent {
|
|
||||||
position: point(px(50.), px(100.)),
|
|
||||||
delta: ScrollDelta::Pixels(point(px(0.), px(-10000.))),
|
|
||||||
..Default::default()
|
|
||||||
});
|
|
||||||
|
|
||||||
// Paint — should NOT re-engage because the scrollbar drag
|
|
||||||
// cleared the suspended state.
|
|
||||||
cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
|
|
||||||
view.clone().into_any_element()
|
|
||||||
});
|
|
||||||
assert!(
|
|
||||||
!state.is_following_tail(),
|
|
||||||
"follow_tail should not re-engage after scrollbar drag cleared the suspended state"
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
use crate::{App, PlatformDispatcher, PlatformScheduler};
|
use crate::{App, PlatformDispatcher, PlatformScheduler};
|
||||||
use futures::channel::mpsc;
|
use futures::channel::mpsc;
|
||||||
use futures::prelude::*;
|
use futures::prelude::*;
|
||||||
use gpui_util::{TryFutureExt, TryFutureExtBacktrace};
|
use gpui_util::TryFutureExt;
|
||||||
use scheduler::Instant;
|
use scheduler::Instant;
|
||||||
use scheduler::Scheduler;
|
use scheduler::Scheduler;
|
||||||
use std::{
|
use std::{
|
||||||
|
|
@ -84,7 +84,7 @@ impl<T> Task<T> {
|
||||||
impl<T, E> Task<Result<T, E>>
|
impl<T, E> Task<Result<T, E>>
|
||||||
where
|
where
|
||||||
T: 'static,
|
T: 'static,
|
||||||
E: 'static + std::fmt::Display,
|
E: 'static + Debug,
|
||||||
{
|
{
|
||||||
/// Run the task to completion in the background and log any errors that occur.
|
/// Run the task to completion in the background and log any errors that occur.
|
||||||
#[track_caller]
|
#[track_caller]
|
||||||
|
|
@ -96,22 +96,6 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T, E> Task<Result<T, E>>
|
|
||||||
where
|
|
||||||
T: 'static,
|
|
||||||
E: 'static + std::fmt::Debug,
|
|
||||||
{
|
|
||||||
/// Like [`Self::detach_and_log_err`], but uses `{:?}` formatting on failure so `anyhow::Error`
|
|
||||||
/// values emit their full backtrace. Prefer `detach_and_log_err` unless a backtrace is wanted.
|
|
||||||
#[track_caller]
|
|
||||||
pub fn detach_and_log_err_with_backtrace(self, cx: &App) {
|
|
||||||
let location = *core::panic::Location::caller();
|
|
||||||
cx.foreground_executor()
|
|
||||||
.spawn(self.log_tracked_err_with_backtrace(location))
|
|
||||||
.detach();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> std::future::Future for Task<T> {
|
impl<T> std::future::Future for Task<T> {
|
||||||
type Output = T;
|
type Output = T;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,10 @@ use std::{
|
||||||
cell::LazyCell,
|
cell::LazyCell,
|
||||||
collections::{HashMap, VecDeque},
|
collections::{HashMap, VecDeque},
|
||||||
hash::{DefaultHasher, Hash, Hasher},
|
hash::{DefaultHasher, Hash, Hasher},
|
||||||
sync::Arc,
|
sync::{
|
||||||
|
Arc,
|
||||||
|
atomic::{AtomicBool, Ordering},
|
||||||
|
},
|
||||||
thread::ThreadId,
|
thread::ThreadId,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -348,6 +351,9 @@ impl Drop for ThreadTimings {
|
||||||
|
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
pub fn add_task_timing(timing: TaskTiming) {
|
pub fn add_task_timing(timing: TaskTiming) {
|
||||||
|
if !PROFILER_ENABLED.load(Ordering::Acquire) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
THREAD_TIMINGS.with(|timings| {
|
THREAD_TIMINGS.with(|timings| {
|
||||||
timings.lock().add_task_timing(timing);
|
timings.lock().add_task_timing(timing);
|
||||||
});
|
});
|
||||||
|
|
@ -357,3 +363,28 @@ pub fn add_task_timing(timing: TaskTiming) {
|
||||||
pub fn get_current_thread_task_timings() -> ThreadTaskTimings {
|
pub fn get_current_thread_task_timings() -> ThreadTaskTimings {
|
||||||
THREAD_TIMINGS.with(|timings| timings.lock().get_thread_task_timings())
|
THREAD_TIMINGS.with(|timings| timings.lock().get_thread_task_timings())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static PROFILER_ENABLED: AtomicBool = AtomicBool::new(false);
|
||||||
|
|
||||||
|
/// Enables or disables task timing collection at runtime.
|
||||||
|
///
|
||||||
|
/// When transitioning from enabled to disabled, `add_task_timing` becomes a
|
||||||
|
/// no-op and the existing per-thread buffers are cleared so stale data isn't
|
||||||
|
/// reported after a later re-enable. Calls with the current value are a no-op.
|
||||||
|
pub fn set_enabled(enabled: bool) -> bool {
|
||||||
|
if PROFILER_ENABLED.swap(enabled, Ordering::AcqRel) == enabled {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !enabled {
|
||||||
|
for global in GLOBAL_THREAD_TIMINGS.lock().iter() {
|
||||||
|
if let Some(timings) = global.timings.upgrade() {
|
||||||
|
let mut timings = timings.lock();
|
||||||
|
timings.timings.clear();
|
||||||
|
timings.timings.shrink_to_fit();
|
||||||
|
timings.total_pushed = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -208,16 +208,8 @@ impl ShapedLine {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Split text
|
// Split text
|
||||||
let left_text = if byte_index == self.text.len() {
|
let left_text = SharedString::new(self.text[..byte_index].to_string());
|
||||||
self.text.clone()
|
let right_text = SharedString::new(self.text[byte_index..].to_string());
|
||||||
} else {
|
|
||||||
SharedString::new(&self.text[..byte_index])
|
|
||||||
};
|
|
||||||
let right_text = if byte_index == 0 {
|
|
||||||
self.text.clone()
|
|
||||||
} else {
|
|
||||||
SharedString::new(&self.text[byte_index..])
|
|
||||||
};
|
|
||||||
|
|
||||||
let left_width = x_offset;
|
let left_width = x_offset;
|
||||||
let right_width = self.layout.width - left_width;
|
let right_width = self.layout.width - left_width;
|
||||||
|
|
|
||||||
|
|
@ -2237,13 +2237,7 @@ impl Dispatch<wl_data_device::WlDataDevice, ()> for WaylandClientStatePtr {
|
||||||
let paths: SmallVec<[_; 2]> = file_list
|
let paths: SmallVec<[_; 2]> = file_list
|
||||||
.lines()
|
.lines()
|
||||||
.filter_map(|path| Url::parse(path).log_err())
|
.filter_map(|path| Url::parse(path).log_err())
|
||||||
.filter_map(|url| match url.to_file_path() {
|
.filter_map(|url| url.to_file_path().log_err())
|
||||||
Ok(url) => Some(url),
|
|
||||||
Err(()) => {
|
|
||||||
log::error!("Failed turn {url:?} into a file path");
|
|
||||||
None
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect();
|
.collect();
|
||||||
let position = Point::new(x.into(), y.into());
|
let position = Point::new(x.into(), y.into());
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -908,13 +908,7 @@ impl X11Client {
|
||||||
let paths: SmallVec<[_; 2]> = file_list
|
let paths: SmallVec<[_; 2]> = file_list
|
||||||
.lines()
|
.lines()
|
||||||
.filter_map(|path| Url::parse(path).log_err())
|
.filter_map(|path| Url::parse(path).log_err())
|
||||||
.filter_map(|url| match url.to_file_path() {
|
.filter_map(|url| url.to_file_path().log_err())
|
||||||
Ok(url) => Some(url),
|
|
||||||
Err(()) => {
|
|
||||||
log::error!("Failed turn {url:?} into a file path");
|
|
||||||
None
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect();
|
.collect();
|
||||||
let input = PlatformInput::FileDrop(FileDropEvent::Entered {
|
let input = PlatformInput::FileDrop(FileDropEvent::Entered {
|
||||||
position: state.xdnd_state.position,
|
position: state.xdnd_state.position,
|
||||||
|
|
|
||||||
|
|
@ -79,12 +79,6 @@ pub trait ResultExt<E> {
|
||||||
type Ok;
|
type Ok;
|
||||||
|
|
||||||
fn log_err(self) -> Option<Self::Ok>;
|
fn log_err(self) -> Option<Self::Ok>;
|
||||||
/// Like [`ResultExt::log_err`], but uses `{:?}` formatting so `anyhow::Error` values emit their
|
|
||||||
/// full backtrace. Reach for this only when a backtrace is genuinely wanted — most call sites
|
|
||||||
/// should stick with `log_err` / `warn_on_err`, whose output is a single chained error message.
|
|
||||||
fn log_err_with_backtrace(self) -> Option<Self::Ok>
|
|
||||||
where
|
|
||||||
E: std::fmt::Debug;
|
|
||||||
/// Assert that this result should never be an error in development or tests.
|
/// Assert that this result should never be an error in development or tests.
|
||||||
fn debug_assert_ok(self, reason: &str) -> Self;
|
fn debug_assert_ok(self, reason: &str) -> Self;
|
||||||
fn warn_on_err(self) -> Option<Self::Ok>;
|
fn warn_on_err(self) -> Option<Self::Ok>;
|
||||||
|
|
@ -96,7 +90,7 @@ pub trait ResultExt<E> {
|
||||||
|
|
||||||
impl<T, E> ResultExt<E> for Result<T, E>
|
impl<T, E> ResultExt<E> for Result<T, E>
|
||||||
where
|
where
|
||||||
E: std::fmt::Display,
|
E: std::fmt::Debug,
|
||||||
{
|
{
|
||||||
type Ok = T;
|
type Ok = T;
|
||||||
|
|
||||||
|
|
@ -105,28 +99,10 @@ where
|
||||||
self.log_with_level(log::Level::Error)
|
self.log_with_level(log::Level::Error)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[track_caller]
|
|
||||||
fn log_err_with_backtrace(self) -> Option<T>
|
|
||||||
where
|
|
||||||
E: std::fmt::Debug,
|
|
||||||
{
|
|
||||||
match self {
|
|
||||||
Ok(value) => Some(value),
|
|
||||||
Err(error) => {
|
|
||||||
log_error_with_caller(
|
|
||||||
*Location::caller(),
|
|
||||||
DebugAsDisplay(&error),
|
|
||||||
log::Level::Error,
|
|
||||||
);
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[track_caller]
|
#[track_caller]
|
||||||
fn debug_assert_ok(self, reason: &str) -> Self {
|
fn debug_assert_ok(self, reason: &str) -> Self {
|
||||||
if let Err(error) = &self {
|
if let Err(error) = &self {
|
||||||
debug_panic!("{reason} - {error:#}");
|
debug_panic!("{reason} - {error:?}");
|
||||||
}
|
}
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
@ -157,7 +133,7 @@ where
|
||||||
|
|
||||||
fn log_error_with_caller<E>(caller: core::panic::Location<'_>, error: E, level: log::Level)
|
fn log_error_with_caller<E>(caller: core::panic::Location<'_>, error: E, level: log::Level)
|
||||||
where
|
where
|
||||||
E: std::fmt::Display,
|
E: std::fmt::Debug,
|
||||||
{
|
{
|
||||||
#[cfg(not(windows))]
|
#[cfg(not(windows))]
|
||||||
let file = caller.file();
|
let file = caller.file();
|
||||||
|
|
@ -180,7 +156,7 @@ where
|
||||||
&log::Record::builder()
|
&log::Record::builder()
|
||||||
.target(module_path.as_deref().unwrap_or(""))
|
.target(module_path.as_deref().unwrap_or(""))
|
||||||
.module_path(file.as_deref())
|
.module_path(file.as_deref())
|
||||||
.args(format_args!("{:#}", error))
|
.args(format_args!("{:?}", error))
|
||||||
.file(Some(caller.file()))
|
.file(Some(caller.file()))
|
||||||
.line(Some(caller.line()))
|
.line(Some(caller.line()))
|
||||||
.level(level)
|
.level(level)
|
||||||
|
|
@ -188,20 +164,10 @@ where
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn log_err<E: std::fmt::Display>(error: &E) {
|
pub fn log_err<E: std::fmt::Debug>(error: &E) {
|
||||||
log_error_with_caller(*Location::caller(), error, log::Level::Error);
|
log_error_with_caller(*Location::caller(), error, log::Level::Error);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Forces `{:?}` formatting through a `Display`-bounded logging helper so `anyhow::Error` emits a
|
|
||||||
// backtrace instead of the single-line chained message produced by its `Display`/`{:#}` forms.
|
|
||||||
struct DebugAsDisplay<'a, E>(&'a E);
|
|
||||||
|
|
||||||
impl<E: std::fmt::Debug> std::fmt::Display for DebugAsDisplay<'_, E> {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
write!(f, "{:?}", self.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait TryFutureExt {
|
pub trait TryFutureExt {
|
||||||
fn log_err(self) -> LogErrorFuture<Self>
|
fn log_err(self) -> LogErrorFuture<Self>
|
||||||
where
|
where
|
||||||
|
|
@ -219,25 +185,10 @@ pub trait TryFutureExt {
|
||||||
Self: Sized;
|
Self: Sized;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `{:?}`-formatting companion to [`TryFutureExt`]; emits a backtrace for `anyhow::Error`. Prefer
|
|
||||||
/// [`TryFutureExt`] unless a backtrace is genuinely wanted.
|
|
||||||
pub trait TryFutureExtBacktrace {
|
|
||||||
fn log_err_with_backtrace(self) -> LogErrorWithBacktraceFuture<Self>
|
|
||||||
where
|
|
||||||
Self: Sized;
|
|
||||||
|
|
||||||
fn log_tracked_err_with_backtrace(
|
|
||||||
self,
|
|
||||||
location: core::panic::Location<'static>,
|
|
||||||
) -> LogErrorWithBacktraceFuture<Self>
|
|
||||||
where
|
|
||||||
Self: Sized;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<F, T, E> TryFutureExt for F
|
impl<F, T, E> TryFutureExt for F
|
||||||
where
|
where
|
||||||
F: Future<Output = Result<T, E>>,
|
F: Future<Output = Result<T, E>>,
|
||||||
E: std::fmt::Display,
|
E: std::fmt::Debug,
|
||||||
{
|
{
|
||||||
#[track_caller]
|
#[track_caller]
|
||||||
fn log_err(self) -> LogErrorFuture<Self>
|
fn log_err(self) -> LogErrorFuture<Self>
|
||||||
|
|
@ -272,38 +223,13 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<F, T, E> TryFutureExtBacktrace for F
|
|
||||||
where
|
|
||||||
F: Future<Output = Result<T, E>>,
|
|
||||||
E: std::fmt::Debug,
|
|
||||||
{
|
|
||||||
#[track_caller]
|
|
||||||
fn log_err_with_backtrace(self) -> LogErrorWithBacktraceFuture<Self>
|
|
||||||
where
|
|
||||||
Self: Sized,
|
|
||||||
{
|
|
||||||
let location = Location::caller();
|
|
||||||
LogErrorWithBacktraceFuture(self, log::Level::Error, *location)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn log_tracked_err_with_backtrace(
|
|
||||||
self,
|
|
||||||
location: core::panic::Location<'static>,
|
|
||||||
) -> LogErrorWithBacktraceFuture<Self>
|
|
||||||
where
|
|
||||||
Self: Sized,
|
|
||||||
{
|
|
||||||
LogErrorWithBacktraceFuture(self, log::Level::Error, location)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub struct LogErrorFuture<F>(F, log::Level, core::panic::Location<'static>);
|
pub struct LogErrorFuture<F>(F, log::Level, core::panic::Location<'static>);
|
||||||
|
|
||||||
impl<F, T, E> Future for LogErrorFuture<F>
|
impl<F, T, E> Future for LogErrorFuture<F>
|
||||||
where
|
where
|
||||||
F: Future<Output = Result<T, E>>,
|
F: Future<Output = Result<T, E>>,
|
||||||
E: std::fmt::Display,
|
E: std::fmt::Debug,
|
||||||
{
|
{
|
||||||
type Output = Option<T>;
|
type Output = Option<T>;
|
||||||
|
|
||||||
|
|
@ -324,33 +250,6 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
pub struct LogErrorWithBacktraceFuture<F>(F, log::Level, core::panic::Location<'static>);
|
|
||||||
|
|
||||||
impl<F, T, E> Future for LogErrorWithBacktraceFuture<F>
|
|
||||||
where
|
|
||||||
F: Future<Output = Result<T, E>>,
|
|
||||||
E: std::fmt::Debug,
|
|
||||||
{
|
|
||||||
type Output = Option<T>;
|
|
||||||
|
|
||||||
fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> {
|
|
||||||
let level = self.1;
|
|
||||||
let location = self.2;
|
|
||||||
let inner = unsafe { Pin::new_unchecked(&mut self.get_unchecked_mut().0) };
|
|
||||||
match inner.poll(cx) {
|
|
||||||
Poll::Ready(output) => Poll::Ready(match output {
|
|
||||||
Ok(output) => Some(output),
|
|
||||||
Err(error) => {
|
|
||||||
log_error_with_caller(location, DebugAsDisplay(&error), level);
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
Poll::Pending => Poll::Pending,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct UnwrapFuture<F>(F);
|
pub struct UnwrapFuture<F>(F);
|
||||||
|
|
||||||
impl<F, T, E> Future for UnwrapFuture<F>
|
impl<F, T, E> Future for UnwrapFuture<F>
|
||||||
|
|
|
||||||
|
|
@ -13,14 +13,6 @@
|
||||||
"{"
|
"{"
|
||||||
"}" @end) @indent
|
"}" @end) @indent
|
||||||
|
|
||||||
(field_declaration_list
|
|
||||||
(access_specifier) @start
|
|
||||||
"}" @end) @indent
|
|
||||||
|
|
||||||
(field_declaration_list
|
|
||||||
(access_specifier)
|
|
||||||
(access_specifier) @outdent)
|
|
||||||
|
|
||||||
(_
|
(_
|
||||||
"("
|
"("
|
||||||
")" @end) @indent
|
")" @end) @indent
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
pub mod row_chunk;
|
pub mod row_chunk;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
ByteContent, DebuggerTextObject, LanguageScope, ModelineSettings, Outline, OutlineConfig,
|
DebuggerTextObject, LanguageScope, ModelineSettings, Outline, OutlineConfig, PLAIN_TEXT,
|
||||||
PLAIN_TEXT, RunnableCapture, RunnableTag, TextObject, TreeSitterOptions, analyze_byte_content,
|
RunnableCapture, RunnableTag, TextObject, TreeSitterOptions,
|
||||||
diagnostic_set::{DiagnosticEntry, DiagnosticEntryRef, DiagnosticGroup},
|
diagnostic_set::{DiagnosticEntry, DiagnosticEntryRef, DiagnosticGroup},
|
||||||
language_settings::{AutoIndentMode, LanguageSettings},
|
language_settings::{AutoIndentMode, LanguageSettings},
|
||||||
outline::OutlineItem,
|
outline::OutlineItem,
|
||||||
|
|
@ -1579,21 +1579,16 @@ impl Buffer {
|
||||||
|
|
||||||
let target_encoding = force_encoding.unwrap_or(current_encoding);
|
let target_encoding = force_encoding.unwrap_or(current_encoding);
|
||||||
|
|
||||||
let bytes = load_bytes_task.await?;
|
|
||||||
|
|
||||||
anyhow::ensure!(
|
|
||||||
analyze_byte_content(&bytes) != ByteContent::Binary,
|
|
||||||
"Binary files are not supported"
|
|
||||||
);
|
|
||||||
|
|
||||||
let is_unicode = target_encoding == encoding_rs::UTF_8
|
let is_unicode = target_encoding == encoding_rs::UTF_8
|
||||||
|| target_encoding == encoding_rs::UTF_16LE
|
|| target_encoding == encoding_rs::UTF_16LE
|
||||||
|| target_encoding == encoding_rs::UTF_16BE;
|
|| target_encoding == encoding_rs::UTF_16BE;
|
||||||
|
|
||||||
let (new_text, has_bom, encoding_used) = if force_encoding.is_some() && !is_unicode {
|
let (new_text, has_bom, encoding_used) = if force_encoding.is_some() && !is_unicode {
|
||||||
|
let bytes = load_bytes_task.await?;
|
||||||
let (cow, _had_errors) = target_encoding.decode_without_bom_handling(&bytes);
|
let (cow, _had_errors) = target_encoding.decode_without_bom_handling(&bytes);
|
||||||
(cow.into_owned(), false, target_encoding)
|
(cow.into_owned(), false, target_encoding)
|
||||||
} else {
|
} else {
|
||||||
|
let bytes = load_bytes_task.await?;
|
||||||
let (cow, used_enc, _had_errors) = target_encoding.decode(&bytes);
|
let (cow, used_enc, _had_errors) = target_encoding.decode(&bytes);
|
||||||
|
|
||||||
let actual_has_bom = if used_enc == encoding_rs::UTF_8 {
|
let actual_has_bom = if used_enc == encoding_rs::UTF_8 {
|
||||||
|
|
|
||||||
|
|
@ -1,158 +0,0 @@
|
||||||
pub const FILE_ANALYSIS_BYTES: usize = 1024;
|
|
||||||
|
|
||||||
#[derive(Debug, PartialEq)]
|
|
||||||
pub enum ByteContent {
|
|
||||||
Utf16Le,
|
|
||||||
Utf16Be,
|
|
||||||
Binary,
|
|
||||||
Unknown,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Heuristic check using null byte distribution plus a generic text-likeness
|
|
||||||
// heuristic. This prefers UTF-16 when many bytes are NUL and otherwise
|
|
||||||
// distinguishes between text-like and binary-like content.
|
|
||||||
pub fn analyze_byte_content(bytes: &[u8]) -> ByteContent {
|
|
||||||
if bytes.len() < 2 {
|
|
||||||
return ByteContent::Unknown;
|
|
||||||
}
|
|
||||||
|
|
||||||
if is_known_binary_header(bytes) {
|
|
||||||
return ByteContent::Binary;
|
|
||||||
}
|
|
||||||
|
|
||||||
let limit = bytes.len().min(FILE_ANALYSIS_BYTES);
|
|
||||||
let mut even_null_count = 0usize;
|
|
||||||
let mut odd_null_count = 0usize;
|
|
||||||
let mut non_text_like_count = 0usize;
|
|
||||||
|
|
||||||
for (i, &byte) in bytes[..limit].iter().enumerate() {
|
|
||||||
if byte == 0 {
|
|
||||||
if i % 2 == 0 {
|
|
||||||
even_null_count += 1;
|
|
||||||
} else {
|
|
||||||
odd_null_count += 1;
|
|
||||||
}
|
|
||||||
non_text_like_count += 1;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let is_text_like = match byte {
|
|
||||||
b'\t' | b'\n' | b'\r' | 0x0C => true,
|
|
||||||
0x20..=0x7E => true,
|
|
||||||
// Treat bytes that are likely part of UTF-8 or single-byte encodings as text-like.
|
|
||||||
0x80..=0xBF | 0xC2..=0xF4 => true,
|
|
||||||
_ => false,
|
|
||||||
};
|
|
||||||
|
|
||||||
if !is_text_like {
|
|
||||||
non_text_like_count += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let total_null_count = even_null_count + odd_null_count;
|
|
||||||
|
|
||||||
// If there are no NUL bytes at all, this is overwhelmingly likely to be text.
|
|
||||||
if total_null_count == 0 {
|
|
||||||
return ByteContent::Unknown;
|
|
||||||
}
|
|
||||||
|
|
||||||
let has_significant_nulls = total_null_count >= limit / 16;
|
|
||||||
let nulls_skew_to_even = even_null_count > odd_null_count * 4;
|
|
||||||
let nulls_skew_to_odd = odd_null_count > even_null_count * 4;
|
|
||||||
|
|
||||||
if has_significant_nulls {
|
|
||||||
let sample = &bytes[..limit];
|
|
||||||
|
|
||||||
// UTF-16BE ASCII: [0x00, char] — nulls at even positions (high byte first)
|
|
||||||
// UTF-16LE ASCII: [char, 0x00] — nulls at odd positions (low byte first)
|
|
||||||
|
|
||||||
if nulls_skew_to_even && is_plausible_utf16_text(sample, false) {
|
|
||||||
return ByteContent::Utf16Be;
|
|
||||||
}
|
|
||||||
|
|
||||||
if nulls_skew_to_odd && is_plausible_utf16_text(sample, true) {
|
|
||||||
return ByteContent::Utf16Le;
|
|
||||||
}
|
|
||||||
|
|
||||||
return ByteContent::Binary;
|
|
||||||
}
|
|
||||||
|
|
||||||
if non_text_like_count * 100 < limit * 8 {
|
|
||||||
ByteContent::Unknown
|
|
||||||
} else {
|
|
||||||
ByteContent::Binary
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_known_binary_header(bytes: &[u8]) -> bool {
|
|
||||||
bytes.starts_with(b"%PDF-") // PDF
|
|
||||||
|| bytes.starts_with(b"PK\x03\x04") // ZIP local header
|
|
||||||
|| bytes.starts_with(b"PK\x05\x06") // ZIP end of central directory
|
|
||||||
|| bytes.starts_with(b"PK\x07\x08") // ZIP spanning/splitting
|
|
||||||
|| bytes.starts_with(b"\x89PNG\r\n\x1a\n") // PNG
|
|
||||||
|| bytes.starts_with(b"\xFF\xD8\xFF") // JPEG
|
|
||||||
|| bytes.starts_with(b"GIF87a") // GIF87a
|
|
||||||
|| bytes.starts_with(b"GIF89a") // GIF89a
|
|
||||||
|| bytes.starts_with(b"IWAD") // Doom IWAD archive
|
|
||||||
|| bytes.starts_with(b"PWAD") // Doom PWAD archive
|
|
||||||
|| bytes.starts_with(b"RIFF") // WAV, AVI, WebP
|
|
||||||
|| bytes.starts_with(b"OggS") // OGG (Vorbis, Opus, FLAC)
|
|
||||||
|| bytes.starts_with(b"fLaC") // FLAC
|
|
||||||
|| bytes.starts_with(b"ID3") // MP3 with ID3v2 tag
|
|
||||||
|| bytes.starts_with(b"\xFF\xFB") // MP3 frame sync (MPEG1 Layer3)
|
|
||||||
|| bytes.starts_with(b"\xFF\xFA") // MP3 frame sync (MPEG1 Layer3)
|
|
||||||
|| bytes.starts_with(b"\xFF\xF3") // MP3 frame sync (MPEG2 Layer3)
|
|
||||||
|| bytes.starts_with(b"\xFF\xF2") // MP3 frame sync (MPEG2 Layer3)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Null byte skew alone is not enough to identify UTF-16 -- binary formats with
|
|
||||||
// small 16-bit values (like PCM audio) produce the same pattern. Decode the
|
|
||||||
// bytes as UTF-16 and reject if too many code units land in control character
|
|
||||||
// ranges or form unpaired surrogates, which real text almost never contains.
|
|
||||||
fn is_plausible_utf16_text(bytes: &[u8], little_endian: bool) -> bool {
|
|
||||||
let mut suspicious_count = 0usize;
|
|
||||||
let mut total = 0usize;
|
|
||||||
|
|
||||||
let mut i = 0;
|
|
||||||
while let Some(code_unit) = read_u16(bytes, i, little_endian) {
|
|
||||||
total += 1;
|
|
||||||
|
|
||||||
match code_unit {
|
|
||||||
0x0009 | 0x000A | 0x000C | 0x000D => {}
|
|
||||||
// C0/C1 control characters and non-characters
|
|
||||||
0x0000..=0x001F | 0x007F..=0x009F | 0xFFFE | 0xFFFF => suspicious_count += 1,
|
|
||||||
0xD800..=0xDBFF => {
|
|
||||||
let next_offset = i + 2;
|
|
||||||
let has_low_surrogate = read_u16(bytes, next_offset, little_endian)
|
|
||||||
.is_some_and(|next| (0xDC00..=0xDFFF).contains(&next));
|
|
||||||
if has_low_surrogate {
|
|
||||||
total += 1;
|
|
||||||
i += 2;
|
|
||||||
} else {
|
|
||||||
suspicious_count += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Lone low surrogate without a preceding high surrogate
|
|
||||||
0xDC00..=0xDFFF => suspicious_count += 1,
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
|
|
||||||
i += 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
if total == 0 {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Real UTF-16 text has near-zero control characters; binary data with
|
|
||||||
// small 16-bit values typically exceeds 5%. 2% provides a safe margin.
|
|
||||||
suspicious_count * 100 < total * 2
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read_u16(bytes: &[u8], offset: usize, little_endian: bool) -> Option<u16> {
|
|
||||||
let pair = [*bytes.get(offset)?, *bytes.get(offset + 1)?];
|
|
||||||
if little_endian {
|
|
||||||
return Some(u16::from_le_bytes(pair));
|
|
||||||
}
|
|
||||||
Some(u16::from_be_bytes(pair))
|
|
||||||
}
|
|
||||||
|
|
@ -9,7 +9,6 @@
|
||||||
mod buffer;
|
mod buffer;
|
||||||
mod diagnostic;
|
mod diagnostic;
|
||||||
mod diagnostic_set;
|
mod diagnostic_set;
|
||||||
mod file_content;
|
|
||||||
mod language_registry;
|
mod language_registry;
|
||||||
|
|
||||||
pub mod language_settings;
|
pub mod language_settings;
|
||||||
|
|
@ -92,7 +91,6 @@ pub use buffer::Operation;
|
||||||
pub use buffer::*;
|
pub use buffer::*;
|
||||||
pub use diagnostic::{Diagnostic, DiagnosticSourceKind};
|
pub use diagnostic::{Diagnostic, DiagnosticSourceKind};
|
||||||
pub use diagnostic_set::{DiagnosticEntry, DiagnosticEntryRef, DiagnosticGroup};
|
pub use diagnostic_set::{DiagnosticEntry, DiagnosticEntryRef, DiagnosticGroup};
|
||||||
pub use file_content::{ByteContent, FILE_ANALYSIS_BYTES, analyze_byte_content};
|
|
||||||
pub use language_registry::{
|
pub use language_registry::{
|
||||||
AvailableLanguage, BinaryStatus, LanguageNotFound, LanguageQueries, LanguageRegistry,
|
AvailableLanguage, BinaryStatus, LanguageNotFound, LanguageQueries, LanguageRegistry,
|
||||||
QUERY_FILENAME_PREFIXES,
|
QUERY_FILENAME_PREFIXES,
|
||||||
|
|
|
||||||
|
|
@ -16,10 +16,10 @@ use itertools::{Either, Itertools};
|
||||||
use settings::{DocumentFoldingRanges, DocumentSymbols, IntoGpui, SemanticTokens};
|
use settings::{DocumentFoldingRanges, DocumentSymbols, IntoGpui, SemanticTokens};
|
||||||
|
|
||||||
pub use settings::{
|
pub use settings::{
|
||||||
AutoIndentMode, CompletionSettingsContent, EditPredictionDataCollectionChoice,
|
AutoIndentMode, CompletionSettingsContent, EditPredictionPromptFormat, EditPredictionProvider,
|
||||||
EditPredictionPromptFormat, EditPredictionProvider, EditPredictionsMode, FormatOnSave,
|
EditPredictionsMode, FormatOnSave, Formatter, FormatterList, InlayHintKind,
|
||||||
Formatter, FormatterList, InlayHintKind, LanguageSettingsContent, LineEndingSetting,
|
LanguageSettingsContent, LineEndingSetting, LspInsertMode, RewrapBehavior,
|
||||||
LspInsertMode, RewrapBehavior, ShowWhitespaceSetting, SoftWrap, WordsCompletionMode,
|
ShowWhitespaceSetting, SoftWrap, WordsCompletionMode,
|
||||||
};
|
};
|
||||||
use settings::{RegisterSetting, Settings, SettingsLocation, SettingsStore, merge_from::MergeFrom};
|
use settings::{RegisterSetting, Settings, SettingsLocation, SettingsStore, merge_from::MergeFrom};
|
||||||
use shellexpand;
|
use shellexpand;
|
||||||
|
|
@ -478,11 +478,6 @@ pub struct EditPredictionSettings {
|
||||||
pub ollama: Option<OpenAiCompatibleEditPredictionSettings>,
|
pub ollama: Option<OpenAiCompatibleEditPredictionSettings>,
|
||||||
pub open_ai_compatible_api: Option<OpenAiCompatibleEditPredictionSettings>,
|
pub open_ai_compatible_api: Option<OpenAiCompatibleEditPredictionSettings>,
|
||||||
pub examples_dir: Option<Arc<Path>>,
|
pub examples_dir: Option<Arc<Path>>,
|
||||||
/// Controls whether training data collection is enabled.
|
|
||||||
///
|
|
||||||
/// `Default` means the value stored in the legacy KV store is used as a fallback,
|
|
||||||
/// preserving existing users' choices without a migration.
|
|
||||||
pub allow_data_collection: EditPredictionDataCollectionChoice,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EditPredictionSettings {
|
impl EditPredictionSettings {
|
||||||
|
|
@ -872,7 +867,6 @@ impl settings::Settings for AllLanguageSettings {
|
||||||
ollama: ollama_settings,
|
ollama: ollama_settings,
|
||||||
open_ai_compatible_api: openai_compatible_settings,
|
open_ai_compatible_api: openai_compatible_settings,
|
||||||
examples_dir: edit_predictions.examples_dir,
|
examples_dir: edit_predictions.examples_dir,
|
||||||
allow_data_collection: edit_predictions.allow_data_collection.unwrap_or_default(),
|
|
||||||
},
|
},
|
||||||
defaults: default_language_settings,
|
defaults: default_language_settings,
|
||||||
languages,
|
languages,
|
||||||
|
|
|
||||||
|
|
@ -365,9 +365,7 @@ impl LanguageModel for CopilotChatLanguageModel {
|
||||||
|
|
||||||
if model.supports_adaptive_thinking() {
|
if model.supports_adaptive_thinking() {
|
||||||
if anthropic_request.thinking.is_some() {
|
if anthropic_request.thinking.is_some() {
|
||||||
anthropic_request.thinking = Some(anthropic::Thinking::Adaptive {
|
anthropic_request.thinking = Some(anthropic::Thinking::Adaptive);
|
||||||
display: Some(anthropic::AdaptiveThinkingDisplay::Summarized),
|
|
||||||
});
|
|
||||||
anthropic_request.output_config =
|
anthropic_request.output_config =
|
||||||
effort.map(|effort| anthropic::OutputConfig {
|
effort.map(|effort| anthropic::OutputConfig {
|
||||||
effort: Some(effort),
|
effort: Some(effort),
|
||||||
|
|
|
||||||
|
|
@ -408,9 +408,7 @@ impl<TP: CloudLlmTokenProvider + 'static> LanguageModel for CloudLanguageModel<T
|
||||||
);
|
);
|
||||||
|
|
||||||
if enable_thinking && effort.is_some() {
|
if enable_thinking && effort.is_some() {
|
||||||
request.thinking = Some(anthropic::Thinking::Adaptive {
|
request.thinking = Some(anthropic::Thinking::Adaptive);
|
||||||
display: Some(anthropic::AdaptiveThinkingDisplay::Summarized),
|
|
||||||
});
|
|
||||||
request.output_config = Some(anthropic::OutputConfig { effort });
|
request.output_config = Some(anthropic::OutputConfig { effort });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -67,7 +67,6 @@ util.workspace = true
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
fs = { workspace = true, features = ["test-support"] }
|
fs = { workspace = true, features = ["test-support"] }
|
||||||
pretty_assertions.workspace = true
|
pretty_assertions.workspace = true
|
||||||
settings = { workspace = true, features = ["test-support"] }
|
|
||||||
theme = { workspace = true, features = ["test-support"] }
|
theme = { workspace = true, features = ["test-support"] }
|
||||||
tree-sitter-bash.workspace = true
|
tree-sitter-bash.workspace = true
|
||||||
tree-sitter-c.workspace = true
|
tree-sitter-c.workspace = true
|
||||||
|
|
|
||||||
|
|
@ -16,332 +16,6 @@ mod tests {
|
||||||
use std::num::NonZeroU32;
|
use std::num::NonZeroU32;
|
||||||
use unindent::Unindent;
|
use unindent::Unindent;
|
||||||
|
|
||||||
#[gpui::test]
|
|
||||||
async fn test_cpp_autoindent_access_specifier(cx: &mut TestAppContext) {
|
|
||||||
cx.update(|cx| {
|
|
||||||
let test_settings = SettingsStore::test(cx);
|
|
||||||
cx.set_global(test_settings);
|
|
||||||
cx.update_global::<SettingsStore, _>(|store, cx| {
|
|
||||||
store.update_user_settings(cx, |s| {
|
|
||||||
s.project.all_languages.defaults.tab_size = NonZeroU32::new(2);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
let language = crate::language("cpp", tree_sitter_cpp::LANGUAGE.into());
|
|
||||||
|
|
||||||
cx.new(|cx| {
|
|
||||||
let mut buffer = Buffer::local("", cx).with_language(language, cx);
|
|
||||||
|
|
||||||
buffer.edit(
|
|
||||||
[(
|
|
||||||
0..0,
|
|
||||||
r#"
|
|
||||||
class Foo {
|
|
||||||
public:
|
|
||||||
void bar();
|
|
||||||
private:
|
|
||||||
int x;
|
|
||||||
};
|
|
||||||
"#
|
|
||||||
.unindent(),
|
|
||||||
)],
|
|
||||||
Some(AutoindentMode::EachLine),
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
buffer.text(),
|
|
||||||
r#"
|
|
||||||
class Foo {
|
|
||||||
public:
|
|
||||||
void bar();
|
|
||||||
private:
|
|
||||||
int x;
|
|
||||||
};
|
|
||||||
"#
|
|
||||||
.unindent(),
|
|
||||||
"members after access specifiers should be indented one level deeper than the specifier"
|
|
||||||
);
|
|
||||||
|
|
||||||
buffer
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#[gpui::test]
|
|
||||||
async fn test_cpp_autoindent_access_specifier_next_line(cx: &mut TestAppContext) {
|
|
||||||
cx.update(|cx| {
|
|
||||||
let test_settings = SettingsStore::test(cx);
|
|
||||||
cx.set_global(test_settings);
|
|
||||||
cx.update_global::<SettingsStore, _>(|store, cx| {
|
|
||||||
store.update_user_settings(cx, |s| {
|
|
||||||
s.project.all_languages.defaults.tab_size = NonZeroU32::new(2);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
let language = crate::language("cpp", tree_sitter_cpp::LANGUAGE.into());
|
|
||||||
|
|
||||||
cx.new(|cx| {
|
|
||||||
let mut buffer = Buffer::local("", cx).with_language(language, cx);
|
|
||||||
|
|
||||||
buffer.edit(
|
|
||||||
[(
|
|
||||||
0..0,
|
|
||||||
r#"
|
|
||||||
class Foo {
|
|
||||||
public:
|
|
||||||
void bar();
|
|
||||||
void baz();
|
|
||||||
private:
|
|
||||||
int x;
|
|
||||||
};
|
|
||||||
"#
|
|
||||||
.unindent(),
|
|
||||||
)],
|
|
||||||
Some(AutoindentMode::EachLine),
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
buffer.text(),
|
|
||||||
r#"
|
|
||||||
class Foo {
|
|
||||||
public:
|
|
||||||
void bar();
|
|
||||||
void baz();
|
|
||||||
private:
|
|
||||||
int x;
|
|
||||||
};
|
|
||||||
"#
|
|
||||||
.unindent(),
|
|
||||||
"members after access specifiers should be indented one level deeper than the specifier"
|
|
||||||
);
|
|
||||||
|
|
||||||
buffer
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#[gpui::test]
|
|
||||||
async fn test_cpp_autoindent_nested_class_access_specifiers(cx: &mut TestAppContext) {
|
|
||||||
cx.update(|cx| {
|
|
||||||
let test_settings = SettingsStore::test(cx);
|
|
||||||
cx.set_global(test_settings);
|
|
||||||
cx.update_global::<SettingsStore, _>(|store, cx| {
|
|
||||||
store.update_user_settings(cx, |s| {
|
|
||||||
s.project.all_languages.defaults.tab_size = NonZeroU32::new(2);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
let language = crate::language("cpp", tree_sitter_cpp::LANGUAGE.into());
|
|
||||||
|
|
||||||
cx.new(|cx| {
|
|
||||||
let mut buffer = Buffer::local("", cx).with_language(language, cx);
|
|
||||||
|
|
||||||
buffer.edit(
|
|
||||||
[(
|
|
||||||
0..0,
|
|
||||||
r#"
|
|
||||||
class Outer {
|
|
||||||
public:
|
|
||||||
class Inner {
|
|
||||||
public:
|
|
||||||
void inner_pub();
|
|
||||||
private:
|
|
||||||
int inner_priv;
|
|
||||||
};
|
|
||||||
private:
|
|
||||||
int outer_priv;
|
|
||||||
};
|
|
||||||
"#
|
|
||||||
.unindent(),
|
|
||||||
)],
|
|
||||||
Some(AutoindentMode::EachLine),
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
buffer.text(),
|
|
||||||
r#"
|
|
||||||
class Outer {
|
|
||||||
public:
|
|
||||||
class Inner {
|
|
||||||
public:
|
|
||||||
void inner_pub();
|
|
||||||
private:
|
|
||||||
int inner_priv;
|
|
||||||
};
|
|
||||||
private:
|
|
||||||
int outer_priv;
|
|
||||||
};
|
|
||||||
"#
|
|
||||||
.unindent(),
|
|
||||||
"nested class access specifiers should indent independently at each nesting level"
|
|
||||||
);
|
|
||||||
|
|
||||||
buffer
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#[gpui::test]
|
|
||||||
async fn test_cpp_autoindent_consecutive_access_specifiers(cx: &mut TestAppContext) {
|
|
||||||
cx.update(|cx| {
|
|
||||||
let test_settings = SettingsStore::test(cx);
|
|
||||||
cx.set_global(test_settings);
|
|
||||||
cx.update_global::<SettingsStore, _>(|store, cx| {
|
|
||||||
store.update_user_settings(cx, |s| {
|
|
||||||
s.project.all_languages.defaults.tab_size = NonZeroU32::new(2);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
let language = crate::language("cpp", tree_sitter_cpp::LANGUAGE.into());
|
|
||||||
|
|
||||||
cx.new(|cx| {
|
|
||||||
let mut buffer = Buffer::local("", cx).with_language(language, cx);
|
|
||||||
|
|
||||||
buffer.edit(
|
|
||||||
[(
|
|
||||||
0..0,
|
|
||||||
r#"
|
|
||||||
class Foo {
|
|
||||||
public:
|
|
||||||
protected:
|
|
||||||
private:
|
|
||||||
int x;
|
|
||||||
};
|
|
||||||
"#
|
|
||||||
.unindent(),
|
|
||||||
)],
|
|
||||||
Some(AutoindentMode::EachLine),
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
buffer.text(),
|
|
||||||
r#"
|
|
||||||
class Foo {
|
|
||||||
public:
|
|
||||||
protected:
|
|
||||||
private:
|
|
||||||
int x;
|
|
||||||
};
|
|
||||||
"#
|
|
||||||
.unindent(),
|
|
||||||
"consecutive access specifiers with no members between them should all align at class level"
|
|
||||||
);
|
|
||||||
|
|
||||||
buffer
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#[gpui::test]
|
|
||||||
async fn test_cpp_autoindent_indented_access_specifiers(cx: &mut TestAppContext) {
|
|
||||||
cx.update(|cx| {
|
|
||||||
let test_settings = SettingsStore::test(cx);
|
|
||||||
cx.set_global(test_settings);
|
|
||||||
cx.update_global::<SettingsStore, _>(|store, cx| {
|
|
||||||
store.update_user_settings(cx, |s| {
|
|
||||||
s.project.all_languages.defaults.tab_size = NonZeroU32::new(2);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
let language = crate::language("cpp", tree_sitter_cpp::LANGUAGE.into());
|
|
||||||
|
|
||||||
cx.new(|cx| {
|
|
||||||
let mut buffer = Buffer::local("", cx).with_language(language, cx);
|
|
||||||
|
|
||||||
buffer.edit(
|
|
||||||
[(
|
|
||||||
0..0,
|
|
||||||
r#"
|
|
||||||
class Foo {
|
|
||||||
int default_member;
|
|
||||||
public:
|
|
||||||
void pub_method();
|
|
||||||
private:
|
|
||||||
int priv_member;
|
|
||||||
};
|
|
||||||
"#
|
|
||||||
.unindent(),
|
|
||||||
)],
|
|
||||||
Some(AutoindentMode::EachLine),
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
buffer.text(),
|
|
||||||
r#"
|
|
||||||
class Foo {
|
|
||||||
int default_member;
|
|
||||||
public:
|
|
||||||
void pub_method();
|
|
||||||
private:
|
|
||||||
int priv_member;
|
|
||||||
};
|
|
||||||
"#
|
|
||||||
.unindent(),
|
|
||||||
"access specifiers should be indented one level inside class braces"
|
|
||||||
);
|
|
||||||
|
|
||||||
buffer
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#[gpui::test]
|
|
||||||
async fn test_cpp_autoindent_access_specifier_with_method_bodies(cx: &mut TestAppContext) {
|
|
||||||
cx.update(|cx| {
|
|
||||||
let test_settings = SettingsStore::test(cx);
|
|
||||||
cx.set_global(test_settings);
|
|
||||||
cx.update_global::<SettingsStore, _>(|store, cx| {
|
|
||||||
store.update_user_settings(cx, |s| {
|
|
||||||
s.project.all_languages.defaults.tab_size = NonZeroU32::new(2);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
let language = crate::language("cpp", tree_sitter_cpp::LANGUAGE.into());
|
|
||||||
|
|
||||||
cx.new(|cx| {
|
|
||||||
let mut buffer = Buffer::local("", cx).with_language(language, cx);
|
|
||||||
|
|
||||||
buffer.edit(
|
|
||||||
[(
|
|
||||||
0..0,
|
|
||||||
r#"
|
|
||||||
class Foo {
|
|
||||||
public:
|
|
||||||
void bar() {
|
|
||||||
if (x)
|
|
||||||
y++;
|
|
||||||
}
|
|
||||||
private:
|
|
||||||
int get_x() {
|
|
||||||
return x;
|
|
||||||
}
|
|
||||||
int x;
|
|
||||||
};
|
|
||||||
"#
|
|
||||||
.unindent(),
|
|
||||||
)],
|
|
||||||
Some(AutoindentMode::EachLine),
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
buffer.text(),
|
|
||||||
r#"
|
|
||||||
class Foo {
|
|
||||||
public:
|
|
||||||
void bar() {
|
|
||||||
if (x)
|
|
||||||
y++;
|
|
||||||
}
|
|
||||||
private:
|
|
||||||
int get_x() {
|
|
||||||
return x;
|
|
||||||
}
|
|
||||||
int x;
|
|
||||||
};
|
|
||||||
"#
|
|
||||||
.unindent(),
|
|
||||||
"method bodies inside access specifier sections should compose brace and specifier indent"
|
|
||||||
);
|
|
||||||
|
|
||||||
buffer
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_cpp_autoindent_basic(cx: &mut TestAppContext) {
|
async fn test_cpp_autoindent_basic(cx: &mut TestAppContext) {
|
||||||
cx.update(|cx| {
|
cx.update(|cx| {
|
||||||
|
|
|
||||||
|
|
@ -999,6 +999,7 @@ impl SearchableItem for MarkdownPreviewView {
|
||||||
markdown.set_active_search_highlight(Some(index), cx);
|
markdown.set_active_search_highlight(Some(index), cx);
|
||||||
markdown.request_autoscroll_to_source_index(start, cx);
|
markdown.request_autoscroll_to_source_index(start, cx);
|
||||||
});
|
});
|
||||||
|
cx.emit(SearchEvent::ActiveMatchChanged);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,10 @@ workspace = true
|
||||||
path = "src/miniprofiler_ui.rs"
|
path = "src/miniprofiler_ui.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
command_palette_hooks.workspace = true
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
rpc.workspace = true
|
rpc.workspace = true
|
||||||
|
settings.workspace = true
|
||||||
theme_settings.workspace = true
|
theme_settings.workspace = true
|
||||||
zed_actions.workspace = true
|
zed_actions.workspace = true
|
||||||
workspace.workspace = true
|
workspace.workspace = true
|
||||||
|
|
|
||||||
|
|
@ -5,14 +5,17 @@ use std::{
|
||||||
time::{Duration, Instant},
|
time::{Duration, Instant},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use command_palette_hooks::CommandPaletteFilter;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
App, AppContext, ClipboardItem, Context, Div, Entity, Hsla, InteractiveElement,
|
App, AppContext, ClipboardItem, Context, Div, Entity, Hsla, InteractiveElement,
|
||||||
ParentElement as _, ProfilingCollector, Render, SerializedLocation, SerializedTaskTiming,
|
ParentElement as _, ProfilingCollector, Render, SerializedLocation, SerializedTaskTiming,
|
||||||
SerializedThreadTaskTimings, SharedString, StatefulInteractiveElement, Styled, Task,
|
SerializedThreadTaskTimings, SharedString, StatefulInteractiveElement, Styled, Task,
|
||||||
ThreadTimingsDelta, TitlebarOptions, UniformListScrollHandle, WeakEntity, WindowBounds,
|
ThreadTimingsDelta, TitlebarOptions, UniformListScrollHandle, WeakEntity, WindowBounds,
|
||||||
WindowOptions, div, prelude::FluentBuilder, px, relative, size, uniform_list,
|
WindowOptions, div, prelude::FluentBuilder, profiler, px, relative, size, uniform_list,
|
||||||
};
|
};
|
||||||
use rpc::{AnyProtoClient, proto};
|
use rpc::{AnyProtoClient, proto};
|
||||||
|
use settings::{RegisterSetting, Settings, SettingsContent, SettingsStore};
|
||||||
|
use std::any::TypeId;
|
||||||
use util::ResultExt;
|
use util::ResultExt;
|
||||||
use workspace::{
|
use workspace::{
|
||||||
Workspace,
|
Workspace,
|
||||||
|
|
@ -61,7 +64,49 @@ impl ProfileSource {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Default, RegisterSetting)]
|
||||||
|
struct PerformanceProfilerSettings {
|
||||||
|
enabled: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Settings for PerformanceProfilerSettings {
|
||||||
|
fn from_settings(content: &SettingsContent) -> Self {
|
||||||
|
let instrumentation = content.instrumentation.as_ref().unwrap();
|
||||||
|
let profiler = instrumentation.performance_profiler.as_ref().unwrap();
|
||||||
|
Self {
|
||||||
|
enabled: profiler.enabled.unwrap(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn init(startup_time: Instant, cx: &mut App) {
|
pub fn init(startup_time: Instant, cx: &mut App) {
|
||||||
|
let initial_enabled = PerformanceProfilerSettings::get_global(cx).enabled;
|
||||||
|
profiler::set_enabled(initial_enabled);
|
||||||
|
update_command_palette_filter(initial_enabled, cx);
|
||||||
|
|
||||||
|
cx.observe_global::<SettingsStore>(|cx| {
|
||||||
|
let enabled = PerformanceProfilerSettings::get_global(cx).enabled;
|
||||||
|
// `set_enabled` reports whether the value actually changed, so skip the
|
||||||
|
// filter update and window cleanup on the common no-op path — the
|
||||||
|
// settings observer fires for every settings change.
|
||||||
|
if !profiler::set_enabled(enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
update_command_palette_filter(enabled, cx);
|
||||||
|
if !enabled {
|
||||||
|
for window in cx
|
||||||
|
.windows()
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|window| window.downcast::<ProfilerWindow>())
|
||||||
|
{
|
||||||
|
window
|
||||||
|
.update(cx, |_, window, _| window.remove_window())
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
|
||||||
cx.observe_new(move |workspace: &mut workspace::Workspace, _, cx| {
|
cx.observe_new(move |workspace: &mut workspace::Workspace, _, cx| {
|
||||||
let workspace_handle = cx.entity().downgrade();
|
let workspace_handle = cx.entity().downgrade();
|
||||||
workspace.register_action(move |_workspace, _: &OpenPerformanceProfiler, window, cx| {
|
workspace.register_action(move |_workspace, _: &OpenPerformanceProfiler, window, cx| {
|
||||||
|
|
@ -71,6 +116,17 @@ pub fn init(startup_time: Instant, cx: &mut App) {
|
||||||
.detach();
|
.detach();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn update_command_palette_filter(enabled: bool, cx: &mut App) {
|
||||||
|
CommandPaletteFilter::update_global(cx, |filter, _| {
|
||||||
|
let action = [TypeId::of::<OpenPerformanceProfiler>()];
|
||||||
|
if enabled {
|
||||||
|
filter.show_action_types(&action);
|
||||||
|
} else {
|
||||||
|
filter.hide_action_types(&action);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
fn open_performance_profiler(
|
fn open_performance_profiler(
|
||||||
startup_time: Instant,
|
startup_time: Instant,
|
||||||
workspace_handle: WeakEntity<Workspace>,
|
workspace_handle: WeakEntity<Workspace>,
|
||||||
|
|
|
||||||
|
|
@ -838,7 +838,7 @@ impl<D: PickerDelegate> Picker<D> {
|
||||||
el.with_width_from_item(Some(widest_item))
|
el.with_width_from_item(Some(widest_item))
|
||||||
})
|
})
|
||||||
.flex_grow()
|
.flex_grow()
|
||||||
.py(DynamicSpacing::Base04.rems(cx))
|
.py_1()
|
||||||
.track_scroll(&scroll_handle)
|
.track_scroll(&scroll_handle)
|
||||||
.into_any_element(),
|
.into_any_element(),
|
||||||
ElementContainer::List(state) => list(
|
ElementContainer::List(state) => list(
|
||||||
|
|
@ -849,7 +849,7 @@ impl<D: PickerDelegate> Picker<D> {
|
||||||
)
|
)
|
||||||
.with_sizing_behavior(sizing_behavior)
|
.with_sizing_behavior(sizing_behavior)
|
||||||
.flex_grow()
|
.flex_grow()
|
||||||
.py(DynamicSpacing::Base04.rems(cx))
|
.py_2()
|
||||||
.into_any_element(),
|
.into_any_element(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1123,16 +1123,13 @@ impl<D: PickerDelegate> Render for Picker<D> {
|
||||||
.when(self.delegate.match_count() == 0, |el| {
|
.when(self.delegate.match_count() == 0, |el| {
|
||||||
el.when_some(self.delegate.no_matches_text(window, cx), |el, text| {
|
el.when_some(self.delegate.no_matches_text(window, cx), |el, text| {
|
||||||
el.child(
|
el.child(
|
||||||
v_flex()
|
v_flex().flex_grow().py_2().child(
|
||||||
.flex_grow()
|
ListItem::new("empty_state")
|
||||||
.py(DynamicSpacing::Base04.rems(cx))
|
.inset(true)
|
||||||
.child(
|
.spacing(ListItemSpacing::Sparse)
|
||||||
ListItem::new("empty_state")
|
.disabled(true)
|
||||||
.inset(true)
|
.child(Label::new(text).color(Color::Muted)),
|
||||||
.spacing(ListItemSpacing::Sparse)
|
),
|
||||||
.disabled(true)
|
|
||||||
.child(Label::new(text).color(Color::Muted)),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -6688,6 +6688,9 @@ impl LspStore {
|
||||||
.into_response()
|
.into_response()
|
||||||
.context("resolve completion")?;
|
.context("resolve completion")?;
|
||||||
|
|
||||||
|
// We must not use any data such as sortText, filterText, insertText and textEdit to edit `Completion` since they are not suppose change during resolve.
|
||||||
|
// Refer: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_completion
|
||||||
|
|
||||||
let mut completions = completions.borrow_mut();
|
let mut completions = completions.borrow_mut();
|
||||||
let completion = &mut completions[completion_index];
|
let completion = &mut completions[completion_index];
|
||||||
if let CompletionSource::Lsp {
|
if let CompletionSource::Lsp {
|
||||||
|
|
@ -6706,40 +6709,6 @@ impl LspStore {
|
||||||
);
|
);
|
||||||
**lsp_completion = resolved_completion;
|
**lsp_completion = resolved_completion;
|
||||||
*resolved = true;
|
*resolved = true;
|
||||||
|
|
||||||
// We must not use any data such as sortText, filterText, insertText and textEdit to edit `Completion` since they are not supposed to change during resolve.
|
|
||||||
// Refer: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_completion
|
|
||||||
//
|
|
||||||
// We still re-derive new_text here as a workaround for the specific
|
|
||||||
// VS Code TypeScript completion resolve flow that vtsls wraps:
|
|
||||||
// https://github.com/microsoft/vscode/blob/838b48504cd9a2338e2ca9e854da9cec990c4d57/extensions/typescript-language-features/src/languageFeatures/completions.ts#L218
|
|
||||||
//
|
|
||||||
// Some servers (e.g. vtsls with completeFunctionCalls) update
|
|
||||||
// insertText/textEdit during resolve to add snippet content like
|
|
||||||
// function call parentheses.
|
|
||||||
//
|
|
||||||
// vtsls resolve flow:
|
|
||||||
// https://github.com/yioneko/vtsls/blob/fecf52324a30e72dfab1537047556076720c1a5f/packages/service/src/service/completion.ts#L228-L244
|
|
||||||
// vtsls converter (isSnippet / insertTextFormat):
|
|
||||||
// https://github.com/yioneko/vtsls/blob/28e075105d7711d635ebf8aefc971bb8e1d2fe65/packages/service/src/utils/converter.ts#L149-L200
|
|
||||||
//
|
|
||||||
// NB: We only update the text content here, NOT the replace/insert
|
|
||||||
// ranges on `Completion`. Those ranges were converted to anchors from
|
|
||||||
// the original response and stay valid across buffer edits. The LSP
|
|
||||||
// ranges in the resolved text_edit are stale when completions are
|
|
||||||
// cached across keystrokes (see #34094).
|
|
||||||
let resolved_new_text = lsp_completion
|
|
||||||
.text_edit
|
|
||||||
.as_ref()
|
|
||||||
.map(|edit| match edit {
|
|
||||||
lsp::CompletionTextEdit::Edit(e) => e.new_text.clone(),
|
|
||||||
lsp::CompletionTextEdit::InsertAndReplace(e) => e.new_text.clone(),
|
|
||||||
})
|
|
||||||
.or_else(|| lsp_completion.insert_text.clone());
|
|
||||||
if let Some(mut resolved_new_text) = resolved_new_text {
|
|
||||||
LineEnding::normalize(&mut resolved_new_text);
|
|
||||||
completion.new_text = resolved_new_text;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -221,7 +221,6 @@ impl Search {
|
||||||
query.clone(),
|
query.clone(),
|
||||||
input_paths_tx,
|
input_paths_tx,
|
||||||
sorted_search_results_tx,
|
sorted_search_results_tx,
|
||||||
tx.clone(),
|
|
||||||
))
|
))
|
||||||
.boxed_local(),
|
.boxed_local(),
|
||||||
Self::open_buffers(
|
Self::open_buffers(
|
||||||
|
|
@ -413,21 +412,12 @@ impl Search {
|
||||||
query: Arc<SearchQuery>,
|
query: Arc<SearchQuery>,
|
||||||
tx: Sender<InputPath>,
|
tx: Sender<InputPath>,
|
||||||
results: Sender<oneshot::Receiver<ProjectPath>>,
|
results: Sender<oneshot::Receiver<ProjectPath>>,
|
||||||
results_tx: Sender<SearchResult>,
|
|
||||||
) -> impl AsyncFnOnce(&mut AsyncApp) {
|
) -> impl AsyncFnOnce(&mut AsyncApp) {
|
||||||
async move |cx| {
|
async move |cx| {
|
||||||
_ = maybe!(async move {
|
_ = maybe!(async move {
|
||||||
let gitignored_tracker = PathInclusionMatcher::new(query.clone());
|
let gitignored_tracker = PathInclusionMatcher::new(query.clone());
|
||||||
let include_ignored = query.include_ignored();
|
let include_ignored = query.include_ignored();
|
||||||
for worktree in worktrees {
|
for worktree in worktrees {
|
||||||
let scan_complete = worktree.read_with(cx, |worktree, _| {
|
|
||||||
worktree.as_local().map(|local| local.scan_complete())
|
|
||||||
});
|
|
||||||
if let Some(scan_complete) = scan_complete {
|
|
||||||
_ = results_tx.send(SearchResult::WaitingForScan).await;
|
|
||||||
scan_complete.await;
|
|
||||||
}
|
|
||||||
|
|
||||||
let (mut snapshot, worktree_settings) = worktree
|
let (mut snapshot, worktree_settings) = worktree
|
||||||
.read_with(cx, |this, _| {
|
.read_with(cx, |this, _| {
|
||||||
Some((this.snapshot(), this.as_local()?.settings()))
|
Some((this.snapshot(), this.as_local()?.settings()))
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,6 @@ pub enum SearchResult {
|
||||||
ranges: Vec<Range<Anchor>>,
|
ranges: Vec<Range<Anchor>>,
|
||||||
},
|
},
|
||||||
LimitReached,
|
LimitReached,
|
||||||
WaitingForScan,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq)]
|
#[derive(Clone, Copy, PartialEq)]
|
||||||
|
|
|
||||||
|
|
@ -130,7 +130,7 @@ impl TaskStore {
|
||||||
.payload
|
.payload
|
||||||
.task_variables
|
.task_variables
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|(k, v)| Some((k.parse().ok()?, v))),
|
.filter_map(|(k, v)| Some((k.parse().log_err()?, v))),
|
||||||
);
|
);
|
||||||
|
|
||||||
let snapshot = location.buffer.read(cx).snapshot();
|
let snapshot = location.buffer.read(cx).snapshot();
|
||||||
|
|
|
||||||
|
|
@ -6287,39 +6287,6 @@ async fn test_dirty_buffer_reloads_after_undo(cx: &mut gpui::TestAppContext) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
|
||||||
async fn test_buffer_file_change_to_binary_fails(cx: &mut gpui::TestAppContext) {
|
|
||||||
init_test(cx);
|
|
||||||
|
|
||||||
let fs = FakeFs::new(cx.executor());
|
|
||||||
fs.insert_tree(
|
|
||||||
path!("/dir"),
|
|
||||||
json!({
|
|
||||||
"file.txt": "",
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
|
|
||||||
let buffer = project
|
|
||||||
.update(cx, |p, cx| p.open_local_buffer(path!("/dir/file.txt"), cx))
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
fs.write(
|
|
||||||
path!("/dir/file.txt").as_ref(),
|
|
||||||
b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01",
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
cx.executor().run_until_parked();
|
|
||||||
|
|
||||||
// Test that existing buffer is left untouched
|
|
||||||
buffer.read_with(cx, |buffer, _| {
|
|
||||||
assert_eq!(buffer.text(), "");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_buffer_file_changes_on_disk(cx: &mut gpui::TestAppContext) {
|
async fn test_buffer_file_changes_on_disk(cx: &mut gpui::TestAppContext) {
|
||||||
init_test(cx);
|
init_test(cx);
|
||||||
|
|
@ -12322,7 +12289,7 @@ async fn search(
|
||||||
SearchResult::Buffer { buffer, ranges } => {
|
SearchResult::Buffer { buffer, ranges } => {
|
||||||
results.entry(buffer).or_insert(ranges);
|
results.entry(buffer).or_insert(ranges);
|
||||||
}
|
}
|
||||||
SearchResult::LimitReached | SearchResult::WaitingForScan => {}
|
SearchResult::LimitReached => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(results
|
Ok(results
|
||||||
|
|
|
||||||
|
|
@ -1020,11 +1020,8 @@ impl PickerDelegate for RecentProjectsDelegate {
|
||||||
.collect()
|
.collect()
|
||||||
};
|
};
|
||||||
|
|
||||||
if !matched_folders.is_empty() {
|
for (index, positions) in matched_folders {
|
||||||
entries.push(ProjectPickerEntry::Header("Current Folders".into()));
|
entries.push(ProjectPickerEntry::OpenFolder { index, positions });
|
||||||
for (index, positions) in matched_folders {
|
|
||||||
entries.push(ProjectPickerEntry::OpenFolder { index, positions });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1285,7 +1282,7 @@ impl PickerDelegate for RecentProjectsDelegate {
|
||||||
.child(
|
.child(
|
||||||
IconButton::new(("remove-folder", worktree_id.to_usize()), IconName::Close)
|
IconButton::new(("remove-folder", worktree_id.to_usize()), IconName::Close)
|
||||||
.icon_size(IconSize::Small)
|
.icon_size(IconSize::Small)
|
||||||
.tooltip(Tooltip::text("Remove Folder from Project"))
|
.tooltip(Tooltip::text("Remove Folder from Workspace"))
|
||||||
.on_click(cx.listener(move |picker, _, window, cx| {
|
.on_click(cx.listener(move |picker, _, window, cx| {
|
||||||
let Some(workspace) = picker.delegate.workspace.upgrade() else {
|
let Some(workspace) = picker.delegate.workspace.upgrade() else {
|
||||||
return;
|
return;
|
||||||
|
|
@ -1317,16 +1314,18 @@ impl PickerDelegate for RecentProjectsDelegate {
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.id("open_folder_item")
|
.id("open_folder_item")
|
||||||
|
.gap_3()
|
||||||
.w_full()
|
.w_full()
|
||||||
.gap_2p5()
|
.overflow_hidden()
|
||||||
.when(self.has_any_non_local_projects, |this| {
|
.when(self.has_any_non_local_projects, |this| {
|
||||||
this.child(Icon::new(icon).color(Color::Muted))
|
this.child(Icon::new(icon).color(Color::Muted))
|
||||||
})
|
})
|
||||||
.child(
|
.child(
|
||||||
v_flex()
|
v_flex()
|
||||||
.min_w_0()
|
.flex_1()
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
|
.min_w_0()
|
||||||
.gap_1()
|
.gap_1()
|
||||||
.child(HighlightedLabel::new(
|
.child(HighlightedLabel::new(
|
||||||
name.to_string(),
|
name.to_string(),
|
||||||
|
|
@ -1336,7 +1335,8 @@ impl PickerDelegate for RecentProjectsDelegate {
|
||||||
this.child(
|
this.child(
|
||||||
Label::new(branch)
|
Label::new(branch)
|
||||||
.color(Color::Muted)
|
.color(Color::Muted)
|
||||||
.truncate(),
|
.truncate()
|
||||||
|
.flex_1(),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.when(is_active, |this| {
|
.when(is_active, |this| {
|
||||||
|
|
@ -1359,7 +1359,7 @@ impl PickerDelegate for RecentProjectsDelegate {
|
||||||
this.tooltip(move |_, cx| {
|
this.tooltip(move |_, cx| {
|
||||||
if let Some(branch) = tooltip_branch.clone() {
|
if let Some(branch) = tooltip_branch.clone() {
|
||||||
Tooltip::with_meta(
|
Tooltip::with_meta(
|
||||||
format!("{}/{}", name, branch),
|
branch,
|
||||||
None,
|
None,
|
||||||
tooltip_path.clone(),
|
tooltip_path.clone(),
|
||||||
cx,
|
cx,
|
||||||
|
|
@ -1384,7 +1384,6 @@ impl PickerDelegate for RecentProjectsDelegate {
|
||||||
.map(|p| p.compact().to_string_lossy().to_string())
|
.map(|p| p.compact().to_string_lossy().to_string())
|
||||||
.collect();
|
.collect();
|
||||||
let tooltip_path: SharedString = ordered_paths.join("\n").into();
|
let tooltip_path: SharedString = ordered_paths.join("\n").into();
|
||||||
let icon = icon_for_remote_connection(self.project_connection_options.as_ref());
|
|
||||||
|
|
||||||
let mut path_start_offset = 0;
|
let mut path_start_offset = 0;
|
||||||
let (match_labels, path_highlights): (Vec<_>, Vec<_>) = paths
|
let (match_labels, path_highlights): (Vec<_>, Vec<_>) = paths
|
||||||
|
|
@ -1439,10 +1438,7 @@ impl PickerDelegate for RecentProjectsDelegate {
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.id("open_project_info_container")
|
.id("open_project_info_container")
|
||||||
.gap_2p5()
|
.gap_3()
|
||||||
.when(self.has_any_non_local_projects, |this| {
|
|
||||||
this.child(Icon::new(icon).color(Color::Muted))
|
|
||||||
})
|
|
||||||
.child({
|
.child({
|
||||||
let mut highlighted = highlighted_match;
|
let mut highlighted = highlighted_match;
|
||||||
if !self.render_paths {
|
if !self.render_paths {
|
||||||
|
|
@ -1489,12 +1485,6 @@ impl PickerDelegate for RecentProjectsDelegate {
|
||||||
})
|
})
|
||||||
.unzip();
|
.unzip();
|
||||||
|
|
||||||
let tooltip_title = if paths.len() > 1 {
|
|
||||||
"Add Folders to this Project"
|
|
||||||
} else {
|
|
||||||
"Add Folder to this Project"
|
|
||||||
};
|
|
||||||
|
|
||||||
let prefix = match &location {
|
let prefix = match &location {
|
||||||
SerializedWorkspaceLocation::Remote(options) => {
|
SerializedWorkspaceLocation::Remote(options) => {
|
||||||
Some(SharedString::from(options.display_name()))
|
Some(SharedString::from(options.display_name()))
|
||||||
|
|
@ -1519,9 +1509,9 @@ impl PickerDelegate for RecentProjectsDelegate {
|
||||||
.icon_size(IconSize::Small)
|
.icon_size(IconSize::Small)
|
||||||
.tooltip(move |_, cx| {
|
.tooltip(move |_, cx| {
|
||||||
Tooltip::with_meta(
|
Tooltip::with_meta(
|
||||||
tooltip_title,
|
"Add Folders to this Project",
|
||||||
None,
|
None,
|
||||||
"As a multi-root folder",
|
"As a multi-root folder project",
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
@ -1584,7 +1574,7 @@ impl PickerDelegate for RecentProjectsDelegate {
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.id("project_info_container")
|
.id("project_info_container")
|
||||||
.gap_2p5()
|
.gap_3()
|
||||||
.flex_grow()
|
.flex_grow()
|
||||||
.when(self.has_any_non_local_projects, |this| {
|
.when(self.has_any_non_local_projects, |this| {
|
||||||
this.child(Icon::new(icon).color(Color::Muted))
|
this.child(Icon::new(icon).color(Color::Muted))
|
||||||
|
|
@ -1832,7 +1822,7 @@ impl PickerDelegate for RecentProjectsDelegate {
|
||||||
menu.context(focus_handle)
|
menu.context(focus_handle)
|
||||||
.when(show_add_to_workspace, |menu| {
|
.when(show_add_to_workspace, |menu| {
|
||||||
menu.action(
|
menu.action(
|
||||||
"Add Folder to this Project",
|
"Add to this Workspace",
|
||||||
AddToWorkspace.boxed_clone(),
|
AddToWorkspace.boxed_clone(),
|
||||||
)
|
)
|
||||||
.separator()
|
.separator()
|
||||||
|
|
|
||||||
|
|
@ -1399,7 +1399,6 @@ impl BufferSearchBar {
|
||||||
}
|
}
|
||||||
let new_match_index = searchable_item
|
let new_match_index = searchable_item
|
||||||
.match_index_for_direction(matches, index, direction, count, *token, window, cx);
|
.match_index_for_direction(matches, index, direction, count, *token, window, cx);
|
||||||
self.active_match_index = Some(new_match_index);
|
|
||||||
|
|
||||||
searchable_item.update_matches(matches, Some(new_match_index), *token, window, cx);
|
searchable_item.update_matches(matches, Some(new_match_index), *token, window, cx);
|
||||||
searchable_item.activate_match(new_match_index, matches, *token, window, cx);
|
searchable_item.activate_match(new_match_index, matches, *token, window, cx);
|
||||||
|
|
@ -2258,8 +2257,8 @@ mod tests {
|
||||||
assert_eq!(search_bar.active_match_index, Some(0));
|
assert_eq!(search_bar.active_match_index, Some(0));
|
||||||
});
|
});
|
||||||
|
|
||||||
// Park the cursor in between matches and ensure that going to the previous match
|
// Park the cursor in between matches and ensure that going to the previous match selects
|
||||||
// selects the closest match to the left of the cursor.
|
// the closest match to the left.
|
||||||
editor.update_in(cx, |editor, window, cx| {
|
editor.update_in(cx, |editor, window, cx| {
|
||||||
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
||||||
s.select_display_ranges([
|
s.select_display_ranges([
|
||||||
|
|
@ -2268,6 +2267,7 @@ mod tests {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
search_bar.update_in(cx, |search_bar, window, cx| {
|
search_bar.update_in(cx, |search_bar, window, cx| {
|
||||||
|
assert_eq!(search_bar.active_match_index, Some(1));
|
||||||
search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
|
search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
editor.update(cx, |editor, cx| editor
|
editor.update(cx, |editor, cx| editor
|
||||||
|
|
@ -2280,8 +2280,8 @@ mod tests {
|
||||||
assert_eq!(search_bar.active_match_index, Some(0));
|
assert_eq!(search_bar.active_match_index, Some(0));
|
||||||
});
|
});
|
||||||
|
|
||||||
// Park the cursor in between matches and ensure that going to the next match
|
// Park the cursor in between matches and ensure that going to the next match selects the
|
||||||
// selects the closest match to the right of the cursor.
|
// closest match to the right.
|
||||||
editor.update_in(cx, |editor, window, cx| {
|
editor.update_in(cx, |editor, window, cx| {
|
||||||
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
||||||
s.select_display_ranges([
|
s.select_display_ranges([
|
||||||
|
|
@ -2290,6 +2290,7 @@ mod tests {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
search_bar.update_in(cx, |search_bar, window, cx| {
|
search_bar.update_in(cx, |search_bar, window, cx| {
|
||||||
|
assert_eq!(search_bar.active_match_index, Some(1));
|
||||||
search_bar.select_next_match(&SelectNextMatch, window, cx);
|
search_bar.select_next_match(&SelectNextMatch, window, cx);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
editor.update(cx, |editor, cx| editor
|
editor.update(cx, |editor, cx| editor
|
||||||
|
|
@ -2302,8 +2303,8 @@ mod tests {
|
||||||
assert_eq!(search_bar.active_match_index, Some(1));
|
assert_eq!(search_bar.active_match_index, Some(1));
|
||||||
});
|
});
|
||||||
|
|
||||||
// Park the cursor after the last match and ensure that going to the previous match
|
// Park the cursor after the last match and ensure that going to the previous match selects
|
||||||
// selects the last match.
|
// the last match.
|
||||||
editor.update_in(cx, |editor, window, cx| {
|
editor.update_in(cx, |editor, window, cx| {
|
||||||
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
||||||
s.select_display_ranges([
|
s.select_display_ranges([
|
||||||
|
|
@ -2312,6 +2313,7 @@ mod tests {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
search_bar.update_in(cx, |search_bar, window, cx| {
|
search_bar.update_in(cx, |search_bar, window, cx| {
|
||||||
|
assert_eq!(search_bar.active_match_index, Some(2));
|
||||||
search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
|
search_bar.select_prev_match(&SelectPreviousMatch, window, cx);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
editor.update(cx, |editor, cx| editor
|
editor.update(cx, |editor, cx| editor
|
||||||
|
|
@ -2324,8 +2326,8 @@ mod tests {
|
||||||
assert_eq!(search_bar.active_match_index, Some(2));
|
assert_eq!(search_bar.active_match_index, Some(2));
|
||||||
});
|
});
|
||||||
|
|
||||||
// Park the cursor after the last match and ensure that going to the next match
|
// Park the cursor after the last match and ensure that going to the next match selects the
|
||||||
// wraps around and selects the first match.
|
// first match.
|
||||||
editor.update_in(cx, |editor, window, cx| {
|
editor.update_in(cx, |editor, window, cx| {
|
||||||
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
||||||
s.select_display_ranges([
|
s.select_display_ranges([
|
||||||
|
|
@ -2334,6 +2336,7 @@ mod tests {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
search_bar.update_in(cx, |search_bar, window, cx| {
|
search_bar.update_in(cx, |search_bar, window, cx| {
|
||||||
|
assert_eq!(search_bar.active_match_index, Some(2));
|
||||||
search_bar.select_next_match(&SelectNextMatch, window, cx);
|
search_bar.select_next_match(&SelectNextMatch, window, cx);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
editor.update(cx, |editor, cx| editor
|
editor.update(cx, |editor, cx| editor
|
||||||
|
|
@ -2347,7 +2350,7 @@ mod tests {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Park the cursor before the first match and ensure that going to the previous match
|
// Park the cursor before the first match and ensure that going to the previous match
|
||||||
// wraps around and selects the last match.
|
// selects the last match.
|
||||||
editor.update_in(cx, |editor, window, cx| {
|
editor.update_in(cx, |editor, window, cx| {
|
||||||
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
||||||
s.select_display_ranges([
|
s.select_display_ranges([
|
||||||
|
|
|
||||||
|
|
@ -237,7 +237,6 @@ pub struct ProjectSearch {
|
||||||
search_id: usize,
|
search_id: usize,
|
||||||
no_results: Option<bool>,
|
no_results: Option<bool>,
|
||||||
limit_reached: bool,
|
limit_reached: bool,
|
||||||
waiting_for_scan: bool,
|
|
||||||
search_history_cursor: SearchHistoryCursor,
|
search_history_cursor: SearchHistoryCursor,
|
||||||
search_included_history_cursor: SearchHistoryCursor,
|
search_included_history_cursor: SearchHistoryCursor,
|
||||||
search_excluded_history_cursor: SearchHistoryCursor,
|
search_excluded_history_cursor: SearchHistoryCursor,
|
||||||
|
|
@ -300,7 +299,6 @@ impl ProjectSearch {
|
||||||
search_id: 0,
|
search_id: 0,
|
||||||
no_results: None,
|
no_results: None,
|
||||||
limit_reached: false,
|
limit_reached: false,
|
||||||
waiting_for_scan: false,
|
|
||||||
search_history_cursor: Default::default(),
|
search_history_cursor: Default::default(),
|
||||||
search_included_history_cursor: Default::default(),
|
search_included_history_cursor: Default::default(),
|
||||||
search_excluded_history_cursor: Default::default(),
|
search_excluded_history_cursor: Default::default(),
|
||||||
|
|
@ -325,7 +323,6 @@ impl ProjectSearch {
|
||||||
search_id: self.search_id,
|
search_id: self.search_id,
|
||||||
no_results: self.no_results,
|
no_results: self.no_results,
|
||||||
limit_reached: self.limit_reached,
|
limit_reached: self.limit_reached,
|
||||||
waiting_for_scan: false,
|
|
||||||
search_history_cursor: self.search_history_cursor.clone(),
|
search_history_cursor: self.search_history_cursor.clone(),
|
||||||
search_included_history_cursor: self.search_included_history_cursor.clone(),
|
search_included_history_cursor: self.search_included_history_cursor.clone(),
|
||||||
search_excluded_history_cursor: self.search_excluded_history_cursor.clone(),
|
search_excluded_history_cursor: self.search_excluded_history_cursor.clone(),
|
||||||
|
|
@ -425,17 +422,15 @@ impl ProjectSearch {
|
||||||
.update(cx, |excerpts, cx| excerpts.clear(cx));
|
.update(cx, |excerpts, cx| excerpts.clear(cx));
|
||||||
project_search.no_results = Some(true);
|
project_search.no_results = Some(true);
|
||||||
project_search.limit_reached = false;
|
project_search.limit_reached = false;
|
||||||
project_search.waiting_for_scan = false;
|
|
||||||
})
|
})
|
||||||
.ok()?;
|
.ok()?;
|
||||||
|
|
||||||
let mut limit_reached = false;
|
let mut limit_reached = false;
|
||||||
while let Some(results) = matches.next().await {
|
while let Some(results) = matches.next().await {
|
||||||
let (buffers_with_ranges, has_reached_limit, is_waiting_for_scan) = cx
|
let (buffers_with_ranges, has_reached_limit) = cx
|
||||||
.background_executor()
|
.background_executor()
|
||||||
.spawn(async move {
|
.spawn(async move {
|
||||||
let mut limit_reached = false;
|
let mut limit_reached = false;
|
||||||
let mut waiting_for_scan = false;
|
|
||||||
let mut buffers_with_ranges = Vec::with_capacity(results.len());
|
let mut buffers_with_ranges = Vec::with_capacity(results.len());
|
||||||
for result in results {
|
for result in results {
|
||||||
match result {
|
match result {
|
||||||
|
|
@ -445,23 +440,12 @@ impl ProjectSearch {
|
||||||
project::search::SearchResult::LimitReached => {
|
project::search::SearchResult::LimitReached => {
|
||||||
limit_reached = true;
|
limit_reached = true;
|
||||||
}
|
}
|
||||||
project::search::SearchResult::WaitingForScan => {
|
|
||||||
waiting_for_scan = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
(buffers_with_ranges, limit_reached, waiting_for_scan)
|
(buffers_with_ranges, limit_reached)
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
limit_reached |= has_reached_limit;
|
limit_reached |= has_reached_limit;
|
||||||
if is_waiting_for_scan {
|
|
||||||
project_search
|
|
||||||
.update(cx, |project_search, cx| {
|
|
||||||
project_search.waiting_for_scan = true;
|
|
||||||
cx.notify();
|
|
||||||
})
|
|
||||||
.ok()?;
|
|
||||||
}
|
|
||||||
let mut new_ranges = project_search
|
let mut new_ranges = project_search
|
||||||
.update(cx, |project_search, cx| {
|
.update(cx, |project_search, cx| {
|
||||||
project_search.excerpts.update(cx, |excerpts, cx| {
|
project_search.excerpts.update(cx, |excerpts, cx| {
|
||||||
|
|
@ -499,7 +483,6 @@ impl ProjectSearch {
|
||||||
project_search.no_results = Some(false);
|
project_search.no_results = Some(false);
|
||||||
}
|
}
|
||||||
project_search.limit_reached = limit_reached;
|
project_search.limit_reached = limit_reached;
|
||||||
project_search.waiting_for_scan = false;
|
|
||||||
project_search.pending_search.take();
|
project_search.pending_search.take();
|
||||||
cx.notify();
|
cx.notify();
|
||||||
})
|
})
|
||||||
|
|
@ -533,11 +516,8 @@ impl Render for ProjectSearchView {
|
||||||
let model = self.entity.read(cx);
|
let model = self.entity.read(cx);
|
||||||
let has_no_results = model.no_results.unwrap_or(false);
|
let has_no_results = model.no_results.unwrap_or(false);
|
||||||
let is_search_underway = model.pending_search.is_some();
|
let is_search_underway = model.pending_search.is_some();
|
||||||
let is_waiting_for_scan = model.waiting_for_scan;
|
|
||||||
|
|
||||||
let heading_text = if is_waiting_for_scan {
|
let heading_text = if is_search_underway {
|
||||||
"Loading project…"
|
|
||||||
} else if is_search_underway {
|
|
||||||
"Searching…"
|
"Searching…"
|
||||||
} else if has_no_results {
|
} else if has_no_results {
|
||||||
"No Results"
|
"No Results"
|
||||||
|
|
|
||||||
|
|
@ -222,6 +222,7 @@ impl VsCodeSettings {
|
||||||
which_key: None,
|
which_key: None,
|
||||||
modeline_lines: None,
|
modeline_lines: None,
|
||||||
feature_flags: None,
|
feature_flags: None,
|
||||||
|
instrumentation: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -182,14 +182,6 @@ pub struct EditPredictionSettingsContent {
|
||||||
pub open_ai_compatible_api: Option<CustomEditPredictionProviderSettingsContent>,
|
pub open_ai_compatible_api: Option<CustomEditPredictionProviderSettingsContent>,
|
||||||
/// The directory where manually captured edit prediction examples are stored.
|
/// The directory where manually captured edit prediction examples are stored.
|
||||||
pub examples_dir: Option<Arc<Path>>,
|
pub examples_dir: Option<Arc<Path>>,
|
||||||
/// Controls whether Zed may collect training data when using Zed's Edit Predictions.
|
|
||||||
/// Data is only ever captured for files in projects that are detected as open source.
|
|
||||||
///
|
|
||||||
/// - `"default"`: use the preference previously set via the status-bar toggle,
|
|
||||||
/// or false if no preference has been stored.
|
|
||||||
/// - `"yes"`: allow data collection for files in open-source projects.
|
|
||||||
/// - `"no"`: never allow data collection.
|
|
||||||
pub allow_data_collection: Option<EditPredictionDataCollectionChoice>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[with_fallible_options]
|
#[with_fallible_options]
|
||||||
|
|
@ -326,33 +318,6 @@ pub struct OllamaEditPredictionSettingsContent {
|
||||||
pub prompt_format: Option<EditPredictionPromptFormat>,
|
pub prompt_format: Option<EditPredictionPromptFormat>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Controls whether Zed collects training data when using Zed's Edit Predictions.
|
|
||||||
#[derive(
|
|
||||||
Copy,
|
|
||||||
Clone,
|
|
||||||
Debug,
|
|
||||||
Default,
|
|
||||||
Eq,
|
|
||||||
PartialEq,
|
|
||||||
Serialize,
|
|
||||||
Deserialize,
|
|
||||||
JsonSchema,
|
|
||||||
MergeFrom,
|
|
||||||
strum::VariantArray,
|
|
||||||
strum::VariantNames,
|
|
||||||
)]
|
|
||||||
#[serde(rename_all = "snake_case")]
|
|
||||||
pub enum EditPredictionDataCollectionChoice {
|
|
||||||
/// Use the preference previously set via the status-bar toggle, or false
|
|
||||||
/// if no preference has been stored.
|
|
||||||
#[default]
|
|
||||||
Default,
|
|
||||||
/// Allow Zed to collect training data from open-source projects.
|
|
||||||
Yes,
|
|
||||||
/// Never allow training data collection.
|
|
||||||
No,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The mode in which edit predictions should be displayed.
|
/// The mode in which edit predictions should be displayed.
|
||||||
#[derive(
|
#[derive(
|
||||||
Copy,
|
Copy,
|
||||||
|
|
|
||||||
|
|
@ -223,6 +223,33 @@ pub struct SettingsContent {
|
||||||
|
|
||||||
/// Local overrides for feature flags, keyed by flag name.
|
/// Local overrides for feature flags, keyed by flag name.
|
||||||
pub feature_flags: Option<FeatureFlagsMap>,
|
pub feature_flags: Option<FeatureFlagsMap>,
|
||||||
|
|
||||||
|
/// Settings for developer-oriented instrumentation tools (profilers,
|
||||||
|
/// tracers, etc.) that can be toggled at runtime.
|
||||||
|
pub instrumentation: Option<InstrumentationSettingsContent>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Configuration for developer-oriented instrumentation tools that collect
|
||||||
|
/// diagnostic data about a running Zed instance.
|
||||||
|
#[with_fallible_options]
|
||||||
|
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
|
||||||
|
pub struct InstrumentationSettingsContent {
|
||||||
|
/// Configuration for the performance profiler, accessed via the
|
||||||
|
/// `zed: open performance profiler` action.
|
||||||
|
pub performance_profiler: Option<PerformanceProfilerSettingsContent>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Configuration for the performance profiler which collects timing data
|
||||||
|
/// for foreground and background executor tasks.
|
||||||
|
#[with_fallible_options]
|
||||||
|
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
|
||||||
|
pub struct PerformanceProfilerSettingsContent {
|
||||||
|
/// Whether to collect timing data for foreground and background executor
|
||||||
|
/// tasks. Enabling this may lead to increased memory usage, hence it's
|
||||||
|
/// disabled by default for regular builds.
|
||||||
|
///
|
||||||
|
/// Default: false
|
||||||
|
pub enabled: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize, MergeFrom)]
|
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize, MergeFrom)]
|
||||||
|
|
@ -647,12 +674,6 @@ pub struct GitPanelSettingsContent {
|
||||||
///
|
///
|
||||||
/// Default: false
|
/// Default: false
|
||||||
pub starts_open: Option<bool>,
|
pub starts_open: Option<bool>,
|
||||||
|
|
||||||
/// Maximum length of the commit message title before a warning is shown.
|
|
||||||
/// Set to 0 to disable.
|
|
||||||
///
|
|
||||||
/// Default: 72
|
|
||||||
pub commit_title_max_length: Option<usize>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(
|
#[derive(
|
||||||
|
|
|
||||||
|
|
@ -1036,9 +1036,6 @@ pub struct ThemeColorsContent {
|
||||||
/// Background color for Vim yank highlight.
|
/// Background color for Vim yank highlight.
|
||||||
#[serde(rename = "vim.yank.background")]
|
#[serde(rename = "vim.yank.background")]
|
||||||
pub vim_yank_background: Option<String>,
|
pub vim_yank_background: Option<String>,
|
||||||
/// Foreground color for Helix jump labels.
|
|
||||||
#[serde(rename = "vim.helix_jump_label.foreground")]
|
|
||||||
pub vim_helix_jump_label_foreground: Option<String>,
|
|
||||||
/// Background color for Vim Helix Normal mode indicator.
|
/// Background color for Vim Helix Normal mode indicator.
|
||||||
#[serde(rename = "vim.helix_normal.background")]
|
#[serde(rename = "vim.helix_normal.background")]
|
||||||
pub vim_helix_normal_background: Option<String>,
|
pub vim_helix_normal_background: Option<String>,
|
||||||
|
|
|
||||||
|
|
@ -185,11 +185,10 @@ pub fn render_ollama_model_picker(
|
||||||
field.json_path,
|
field.json_path,
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
move |settings, app| {
|
move |settings, _cx| {
|
||||||
(field.write)(
|
(field.write)(
|
||||||
settings,
|
settings,
|
||||||
Some(settings::OllamaModelName(model_name.to_string())),
|
Some(settings::OllamaModelName(model_name.to_string())),
|
||||||
app,
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -121,8 +121,8 @@ fn render_settings_audio_device_dropdown<T: AsRef<Option<String>> + From<Option<
|
||||||
field.json_path,
|
field.json_path,
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
move |settings, app| {
|
move |settings, _cx| {
|
||||||
(field.write)(settings, value, app);
|
(field.write)(settings, value);
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.log_err();
|
.log_err();
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ use edit_prediction::{
|
||||||
open_ai_compatible::{open_ai_compatible_api_token, open_ai_compatible_api_url},
|
open_ai_compatible::{open_ai_compatible_api_token, open_ai_compatible_api_url},
|
||||||
};
|
};
|
||||||
use edit_prediction_ui::{get_available_providers, set_completion_provider};
|
use edit_prediction_ui::{get_available_providers, set_completion_provider};
|
||||||
use gpui::{App, Entity, ScrollHandle, prelude::*};
|
use gpui::{Entity, ScrollHandle, prelude::*};
|
||||||
use language::language_settings::AllLanguageSettings;
|
use language::language_settings::AllLanguageSettings;
|
||||||
|
|
||||||
use settings::Settings as _;
|
use settings::Settings as _;
|
||||||
|
|
@ -373,7 +373,7 @@ fn ollama_settings() -> Box<[SettingsPageItem]> {
|
||||||
.api_url
|
.api_url
|
||||||
.as_ref()
|
.as_ref()
|
||||||
},
|
},
|
||||||
write: |settings, value, _app: &App| {
|
write: |settings, value| {
|
||||||
settings
|
settings
|
||||||
.project
|
.project
|
||||||
.all_languages
|
.all_languages
|
||||||
|
|
@ -406,7 +406,7 @@ fn ollama_settings() -> Box<[SettingsPageItem]> {
|
||||||
.model
|
.model
|
||||||
.as_ref()
|
.as_ref()
|
||||||
},
|
},
|
||||||
write: |settings, value, _app: &App| {
|
write: |settings, value| {
|
||||||
settings
|
settings
|
||||||
.project
|
.project
|
||||||
.all_languages
|
.all_languages
|
||||||
|
|
@ -439,7 +439,7 @@ fn ollama_settings() -> Box<[SettingsPageItem]> {
|
||||||
.prompt_format
|
.prompt_format
|
||||||
.as_ref()
|
.as_ref()
|
||||||
},
|
},
|
||||||
write: |settings, value, _app: &App| {
|
write: |settings, value| {
|
||||||
settings
|
settings
|
||||||
.project
|
.project
|
||||||
.all_languages
|
.all_languages
|
||||||
|
|
@ -469,7 +469,7 @@ fn ollama_settings() -> Box<[SettingsPageItem]> {
|
||||||
.max_output_tokens
|
.max_output_tokens
|
||||||
.as_ref()
|
.as_ref()
|
||||||
},
|
},
|
||||||
write: |settings, value, _app: &App| {
|
write: |settings, value| {
|
||||||
settings
|
settings
|
||||||
.project
|
.project
|
||||||
.all_languages
|
.all_languages
|
||||||
|
|
@ -504,7 +504,7 @@ fn open_ai_compatible_settings() -> Box<[SettingsPageItem]> {
|
||||||
.api_url
|
.api_url
|
||||||
.as_ref()
|
.as_ref()
|
||||||
},
|
},
|
||||||
write: |settings, value, _app: &App| {
|
write: |settings, value| {
|
||||||
settings
|
settings
|
||||||
.project
|
.project
|
||||||
.all_languages
|
.all_languages
|
||||||
|
|
@ -537,7 +537,7 @@ fn open_ai_compatible_settings() -> Box<[SettingsPageItem]> {
|
||||||
.model
|
.model
|
||||||
.as_ref()
|
.as_ref()
|
||||||
},
|
},
|
||||||
write: |settings, value, _app: &App| {
|
write: |settings, value| {
|
||||||
settings
|
settings
|
||||||
.project
|
.project
|
||||||
.all_languages
|
.all_languages
|
||||||
|
|
@ -570,7 +570,7 @@ fn open_ai_compatible_settings() -> Box<[SettingsPageItem]> {
|
||||||
.prompt_format
|
.prompt_format
|
||||||
.as_ref()
|
.as_ref()
|
||||||
},
|
},
|
||||||
write: |settings, value, _app: &App| {
|
write: |settings, value| {
|
||||||
settings
|
settings
|
||||||
.project
|
.project
|
||||||
.all_languages
|
.all_languages
|
||||||
|
|
@ -600,7 +600,7 @@ fn open_ai_compatible_settings() -> Box<[SettingsPageItem]> {
|
||||||
.max_output_tokens
|
.max_output_tokens
|
||||||
.as_ref()
|
.as_ref()
|
||||||
},
|
},
|
||||||
write: |settings, value, _app: &App| {
|
write: |settings, value| {
|
||||||
settings
|
settings
|
||||||
.project
|
.project
|
||||||
.all_languages
|
.all_languages
|
||||||
|
|
@ -635,7 +635,7 @@ fn codestral_settings() -> Box<[SettingsPageItem]> {
|
||||||
.api_url
|
.api_url
|
||||||
.as_ref()
|
.as_ref()
|
||||||
},
|
},
|
||||||
write: |settings, value, _app: &App| {
|
write: |settings, value| {
|
||||||
settings
|
settings
|
||||||
.project
|
.project
|
||||||
.all_languages
|
.all_languages
|
||||||
|
|
@ -668,7 +668,7 @@ fn codestral_settings() -> Box<[SettingsPageItem]> {
|
||||||
.max_tokens
|
.max_tokens
|
||||||
.as_ref()
|
.as_ref()
|
||||||
},
|
},
|
||||||
write: |settings, value, _app: &App| {
|
write: |settings, value| {
|
||||||
settings
|
settings
|
||||||
.project
|
.project
|
||||||
.all_languages
|
.all_languages
|
||||||
|
|
@ -698,7 +698,7 @@ fn codestral_settings() -> Box<[SettingsPageItem]> {
|
||||||
.model
|
.model
|
||||||
.as_ref()
|
.as_ref()
|
||||||
},
|
},
|
||||||
write: |settings, value, _app: &App| {
|
write: |settings, value| {
|
||||||
settings
|
settings
|
||||||
.project
|
.project
|
||||||
.all_languages
|
.all_languages
|
||||||
|
|
|
||||||
|
|
@ -100,7 +100,7 @@ struct FocusFile(pub u32);
|
||||||
|
|
||||||
struct SettingField<T: 'static> {
|
struct SettingField<T: 'static> {
|
||||||
pick: fn(&SettingsContent) -> Option<&T>,
|
pick: fn(&SettingsContent) -> Option<&T>,
|
||||||
write: fn(&mut SettingsContent, Option<T>, &App),
|
write: fn(&mut SettingsContent, Option<T>),
|
||||||
|
|
||||||
/// A json-path-like string that gives a unique-ish string that identifies
|
/// A json-path-like string that gives a unique-ish string that identifies
|
||||||
/// where in the JSON the setting is defined.
|
/// where in the JSON the setting is defined.
|
||||||
|
|
@ -149,7 +149,7 @@ impl<T: 'static> SettingField<T> {
|
||||||
fn unimplemented(self) -> SettingField<UnimplementedSettingField> {
|
fn unimplemented(self) -> SettingField<UnimplementedSettingField> {
|
||||||
SettingField {
|
SettingField {
|
||||||
pick: |_| Some(&UnimplementedSettingField),
|
pick: |_| Some(&UnimplementedSettingField),
|
||||||
write: |_, _, _| unreachable!(),
|
write: |_, _| unreachable!(),
|
||||||
json_path: self.json_path,
|
json_path: self.json_path,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -232,8 +232,8 @@ impl<T: PartialEq + Clone + Send + Sync + 'static> AnySettingField for SettingFi
|
||||||
None,
|
None,
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
move |settings, app| {
|
move |settings, _| {
|
||||||
(this.write)(settings, value_to_set, app);
|
(this.write)(settings, value_to_set);
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
// todo(settings_ui): Don't log err
|
// todo(settings_ui): Don't log err
|
||||||
|
|
@ -504,7 +504,6 @@ fn init_renderers(cx: &mut App) {
|
||||||
.add_basic_renderer::<settings::TerminalBlink>(render_dropdown)
|
.add_basic_renderer::<settings::TerminalBlink>(render_dropdown)
|
||||||
.add_basic_renderer::<settings::CursorShapeContent>(render_dropdown)
|
.add_basic_renderer::<settings::CursorShapeContent>(render_dropdown)
|
||||||
.add_basic_renderer::<settings::EditPredictionPromptFormat>(render_dropdown)
|
.add_basic_renderer::<settings::EditPredictionPromptFormat>(render_dropdown)
|
||||||
.add_basic_renderer::<settings::EditPredictionDataCollectionChoice>(render_dropdown)
|
|
||||||
.add_basic_renderer::<f32>(render_editable_number_field)
|
.add_basic_renderer::<f32>(render_editable_number_field)
|
||||||
.add_basic_renderer::<u32>(render_editable_number_field)
|
.add_basic_renderer::<u32>(render_editable_number_field)
|
||||||
.add_basic_renderer::<u64>(render_editable_number_field)
|
.add_basic_renderer::<u64>(render_editable_number_field)
|
||||||
|
|
@ -4091,8 +4090,8 @@ fn render_text_field<T: From<String> + Into<String> + AsRef<str> + Clone>(
|
||||||
field.json_path,
|
field.json_path,
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
move |settings, app| {
|
move |settings, _cx| {
|
||||||
(field.write)(settings, new_text.map(Into::into), app);
|
(field.write)(settings, new_text.map(Into::into));
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.log_err(); // todo(settings_ui) don't log err
|
.log_err(); // todo(settings_ui) don't log err
|
||||||
|
|
@ -4123,8 +4122,8 @@ fn render_toggle_button<B: Into<bool> + From<bool> + Copy>(
|
||||||
telemetry::event!("Settings Change", setting = field.json_path, type = file.setting_type());
|
telemetry::event!("Settings Change", setting = field.json_path, type = file.setting_type());
|
||||||
|
|
||||||
let state = *state == ui::ToggleState::Selected;
|
let state = *state == ui::ToggleState::Selected;
|
||||||
update_settings_file(file.clone(), field.json_path, window, cx, move |settings, app| {
|
update_settings_file(file.clone(), field.json_path, window, cx, move |settings, _cx| {
|
||||||
(field.write)(settings, Some(state.into()), app);
|
(field.write)(settings, Some(state.into()));
|
||||||
})
|
})
|
||||||
.log_err(); // todo(settings_ui) don't log err
|
.log_err(); // todo(settings_ui) don't log err
|
||||||
}
|
}
|
||||||
|
|
@ -4158,8 +4157,8 @@ fn render_editable_number_field<T: NumberFieldType + Send + Sync>(
|
||||||
field.json_path,
|
field.json_path,
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
move |settings, app| {
|
move |settings, _cx| {
|
||||||
(field.write)(settings, Some(value), app);
|
(field.write)(settings, Some(value));
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.log_err(); // todo(settings_ui) don't log err
|
.log_err(); // todo(settings_ui) don't log err
|
||||||
|
|
@ -4198,8 +4197,8 @@ where
|
||||||
field.json_path,
|
field.json_path,
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
move |settings, app| {
|
move |settings, _cx| {
|
||||||
(field.write)(settings, Some(value), app);
|
(field.write)(settings, Some(value));
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.log_err(); // todo(settings_ui) don't log err
|
.log_err(); // todo(settings_ui) don't log err
|
||||||
|
|
@ -4253,8 +4252,8 @@ fn render_font_picker(
|
||||||
field.json_path,
|
field.json_path,
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
move |settings, app| {
|
move |settings, _cx| {
|
||||||
(field.write)(settings, Some(font_name.to_string().into()), app);
|
(field.write)(settings, Some(font_name.to_string().into()));
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.log_err(); // todo(settings_ui) don't log err
|
.log_err(); // todo(settings_ui) don't log err
|
||||||
|
|
@ -4303,11 +4302,10 @@ fn render_theme_picker(
|
||||||
field.json_path,
|
field.json_path,
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
move |settings, app| {
|
move |settings, _cx| {
|
||||||
(field.write)(
|
(field.write)(
|
||||||
settings,
|
settings,
|
||||||
Some(settings::ThemeName(theme_name.into())),
|
Some(settings::ThemeName(theme_name.into())),
|
||||||
app,
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
@ -4357,11 +4355,10 @@ fn render_icon_theme_picker(
|
||||||
field.json_path,
|
field.json_path,
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
move |settings, app| {
|
move |settings, _cx| {
|
||||||
(field.write)(
|
(field.write)(
|
||||||
settings,
|
settings,
|
||||||
Some(settings::IconThemeName(theme_name.into())),
|
Some(settings::IconThemeName(theme_name.into())),
|
||||||
app,
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -6141,45 +6141,6 @@ async fn test_archive_thread_keeps_metadata_but_hides_from_sidebar(cx: &mut Test
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
|
||||||
async fn test_archive_thread_drops_retained_conversation_view(cx: &mut TestAppContext) {
|
|
||||||
let project = init_test_project_with_agent_panel("/project-a", cx).await;
|
|
||||||
let (multi_workspace, cx) =
|
|
||||||
cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
|
|
||||||
let (sidebar, panel) = setup_sidebar_with_agent_panel(&multi_workspace, cx);
|
|
||||||
cx.run_until_parked();
|
|
||||||
|
|
||||||
let connection = acp_thread::StubAgentConnection::new();
|
|
||||||
connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk(
|
|
||||||
acp::ContentChunk::new("Done".into()),
|
|
||||||
)]);
|
|
||||||
open_thread_with_connection(&panel, connection, cx);
|
|
||||||
send_message(&panel, cx);
|
|
||||||
let session_id = active_session_id(&panel, cx);
|
|
||||||
let thread_id = active_thread_id(&panel, cx);
|
|
||||||
cx.run_until_parked();
|
|
||||||
|
|
||||||
sidebar.read_with(cx, |sidebar, _| {
|
|
||||||
assert!(
|
|
||||||
is_active_session(sidebar, &session_id),
|
|
||||||
"expected the newly created thread to be active before archiving",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
sidebar.update_in(cx, |sidebar, window, cx| {
|
|
||||||
sidebar.archive_thread(&session_id, window, cx);
|
|
||||||
});
|
|
||||||
cx.run_until_parked();
|
|
||||||
|
|
||||||
panel.read_with(cx, |panel, _| {
|
|
||||||
assert!(
|
|
||||||
!panel.is_retained_thread(&thread_id),
|
|
||||||
"archiving a thread must drop its ConversationView from retained_threads, \
|
|
||||||
but the archived thread id {thread_id:?} is still retained",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_archive_thread_active_entry_management(cx: &mut TestAppContext) {
|
async fn test_archive_thread_active_entry_management(cx: &mut TestAppContext) {
|
||||||
// Tests two archive scenarios:
|
// Tests two archive scenarios:
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,6 @@ url.workspace = true
|
||||||
util.workspace = true
|
util.workspace = true
|
||||||
urlencoding.workspace = true
|
urlencoding.workspace = true
|
||||||
parking_lot.workspace = true
|
parking_lot.workspace = true
|
||||||
percent-encoding.workspace = true
|
|
||||||
|
|
||||||
[target.'cfg(windows)'.dependencies]
|
[target.'cfg(windows)'.dependencies]
|
||||||
windows.workspace = true
|
windows.workspace = true
|
||||||
|
|
|
||||||
|
|
@ -138,10 +138,6 @@ pub(super) fn find_from_grid_point<T: EventListener>(
|
||||||
if let Ok(url) = Url::parse(&maybe_url_or_path) {
|
if let Ok(url) = Url::parse(&maybe_url_or_path) {
|
||||||
if let Ok(path) = url.to_file_path_ext(path_style) {
|
if let Ok(path) = url.to_file_path_ext(path_style) {
|
||||||
return (path.to_string_lossy().into_owned(), false, word_match);
|
return (path.to_string_lossy().into_owned(), false, word_match);
|
||||||
} else if let Some(path) = try_osc8_url_to_path(url)
|
|
||||||
&& path_style.is_posix()
|
|
||||||
{
|
|
||||||
return (path, false, word_match);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Fallback: strip file:// prefix if URL parsing fails
|
// Fallback: strip file:// prefix if URL parsing fails
|
||||||
|
|
@ -158,22 +154,6 @@ pub(super) fn find_from_grid_point<T: EventListener>(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// OSC 8 mandates that file:// URIs must be encoded as file://{host}{path}
|
|
||||||
// We need to skip the {host} part if it's set
|
|
||||||
fn try_osc8_url_to_path(url: url::Url) -> Option<String> {
|
|
||||||
use percent_encoding::percent_decode;
|
|
||||||
if url.scheme() != "file" {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let bytes = url
|
|
||||||
.path_segments()?
|
|
||||||
.skip(1)
|
|
||||||
.flat_map(|segment| percent_decode(segment.as_bytes()))
|
|
||||||
.collect::<Vec<u8>>();
|
|
||||||
bytes.try_into().ok()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn sanitize_url_punctuation<T: EventListener>(
|
fn sanitize_url_punctuation<T: EventListener>(
|
||||||
url: String,
|
url: String,
|
||||||
url_match: Match,
|
url_match: Match,
|
||||||
|
|
|
||||||
|
|
@ -489,7 +489,6 @@ impl TerminalView {
|
||||||
pub fn deploy_context_menu(
|
pub fn deploy_context_menu(
|
||||||
&mut self,
|
&mut self,
|
||||||
position: Point<Pixels>,
|
position: Point<Pixels>,
|
||||||
has_selection: bool,
|
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
|
|
@ -498,6 +497,13 @@ impl TerminalView {
|
||||||
.upgrade()
|
.upgrade()
|
||||||
.and_then(|workspace| workspace.read(cx).panel::<TerminalPanel>(cx))
|
.and_then(|workspace| workspace.read(cx).panel::<TerminalPanel>(cx))
|
||||||
.is_some_and(|terminal_panel| terminal_panel.read(cx).assistant_enabled());
|
.is_some_and(|terminal_panel| terminal_panel.read(cx).assistant_enabled());
|
||||||
|
let has_selection = self
|
||||||
|
.terminal
|
||||||
|
.read(cx)
|
||||||
|
.last_content
|
||||||
|
.selection_text
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|text| !text.is_empty());
|
||||||
let context_menu = ContextMenu::build(window, cx, |menu, _, _| {
|
let context_menu = ContextMenu::build(window, cx, |menu, _, _| {
|
||||||
menu.context(self.focus_handle.clone())
|
menu.context(self.focus_handle.clone())
|
||||||
.action("New Terminal", Box::new(NewTerminal::default()))
|
.action("New Terminal", Box::new(NewTerminal::default()))
|
||||||
|
|
@ -1243,21 +1249,12 @@ impl Render for TerminalView {
|
||||||
MouseButton::Right,
|
MouseButton::Right,
|
||||||
cx.listener(|this, event: &MouseDownEvent, window, cx| {
|
cx.listener(|this, event: &MouseDownEvent, window, cx| {
|
||||||
if !this.terminal.read(cx).mouse_mode(event.modifiers.shift) {
|
if !this.terminal.read(cx).mouse_mode(event.modifiers.shift) {
|
||||||
let had_selection = this.terminal.read(cx).last_content.selection.is_some();
|
if this.terminal.read(cx).last_content.selection.is_none() {
|
||||||
if !had_selection {
|
|
||||||
this.terminal.update(cx, |terminal, _| {
|
this.terminal.update(cx, |terminal, _| {
|
||||||
terminal.select_word_at_event_position(event);
|
terminal.select_word_at_event_position(event);
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
let has_selection = !had_selection
|
this.deploy_context_menu(event.position, window, cx);
|
||||||
|| this
|
|
||||||
.terminal
|
|
||||||
.read(cx)
|
|
||||||
.last_content
|
|
||||||
.selection_text
|
|
||||||
.as_ref()
|
|
||||||
.is_some_and(|text| !text.is_empty());
|
|
||||||
this.deploy_context_menu(event.position, has_selection, window, cx);
|
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -176,7 +176,6 @@ impl ThemeColors {
|
||||||
vim_visual_line_background: system.transparent,
|
vim_visual_line_background: system.transparent,
|
||||||
vim_visual_block_background: system.transparent,
|
vim_visual_block_background: system.transparent,
|
||||||
vim_yank_background: neutral().light_alpha().step_3(),
|
vim_yank_background: neutral().light_alpha().step_3(),
|
||||||
vim_helix_jump_label_foreground: red().light().step_9(),
|
|
||||||
vim_helix_normal_background: system.transparent,
|
vim_helix_normal_background: system.transparent,
|
||||||
vim_helix_select_background: system.transparent,
|
vim_helix_select_background: system.transparent,
|
||||||
vim_normal_foreground: system.transparent,
|
vim_normal_foreground: system.transparent,
|
||||||
|
|
@ -323,7 +322,6 @@ impl ThemeColors {
|
||||||
vim_visual_line_background: system.transparent,
|
vim_visual_line_background: system.transparent,
|
||||||
vim_visual_block_background: system.transparent,
|
vim_visual_block_background: system.transparent,
|
||||||
vim_yank_background: neutral().dark_alpha().step_4(),
|
vim_yank_background: neutral().dark_alpha().step_4(),
|
||||||
vim_helix_jump_label_foreground: red().dark().step_9(),
|
|
||||||
vim_helix_normal_background: system.transparent,
|
vim_helix_normal_background: system.transparent,
|
||||||
vim_helix_select_background: system.transparent,
|
vim_helix_select_background: system.transparent,
|
||||||
vim_normal_foreground: system.transparent,
|
vim_normal_foreground: system.transparent,
|
||||||
|
|
|
||||||
|
|
@ -260,7 +260,6 @@ pub(crate) fn zed_default_dark() -> Theme {
|
||||||
vim_visual_line_background: SystemColors::default().transparent,
|
vim_visual_line_background: SystemColors::default().transparent,
|
||||||
vim_visual_block_background: SystemColors::default().transparent,
|
vim_visual_block_background: SystemColors::default().transparent,
|
||||||
vim_yank_background: hsla(207.8 / 360., 81. / 100., 66. / 100., 0.2),
|
vim_yank_background: hsla(207.8 / 360., 81. / 100., 66. / 100., 0.2),
|
||||||
vim_helix_jump_label_foreground: red,
|
|
||||||
vim_helix_normal_background: SystemColors::default().transparent,
|
vim_helix_normal_background: SystemColors::default().transparent,
|
||||||
vim_helix_select_background: SystemColors::default().transparent,
|
vim_helix_select_background: SystemColors::default().transparent,
|
||||||
vim_normal_foreground: SystemColors::default().transparent,
|
vim_normal_foreground: SystemColors::default().transparent,
|
||||||
|
|
|
||||||
|
|
@ -177,8 +177,6 @@ pub struct ThemeColors {
|
||||||
pub vim_visual_block_background: Hsla,
|
pub vim_visual_block_background: Hsla,
|
||||||
/// Background color for Vim yank highlight.
|
/// Background color for Vim yank highlight.
|
||||||
pub vim_yank_background: Hsla,
|
pub vim_yank_background: Hsla,
|
||||||
/// Foreground color for Helix jump labels.
|
|
||||||
pub vim_helix_jump_label_foreground: Hsla,
|
|
||||||
/// Background color for Vim Helix Normal mode indicator.
|
/// Background color for Vim Helix Normal mode indicator.
|
||||||
pub vim_helix_normal_background: Hsla,
|
pub vim_helix_normal_background: Hsla,
|
||||||
/// Background color for Vim Helix Select mode indicator.
|
/// Background color for Vim Helix Select mode indicator.
|
||||||
|
|
|
||||||
|
|
@ -26,18 +26,7 @@ impl Focusable for IconThemeSelector {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ModalView for IconThemeSelector {
|
impl ModalView for IconThemeSelector {}
|
||||||
fn on_before_dismiss(
|
|
||||||
&mut self,
|
|
||||||
_window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) -> workspace::DismissDecision {
|
|
||||||
self.picker.update(cx, |picker, cx| {
|
|
||||||
picker.delegate.revert_theme(cx);
|
|
||||||
});
|
|
||||||
workspace::DismissDecision::Dismiss(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl IconThemeSelector {
|
impl IconThemeSelector {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
|
|
@ -143,13 +132,6 @@ impl IconThemeSelectorDelegate {
|
||||||
.unwrap_or(self.selected_index);
|
.unwrap_or(self.selected_index);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn revert_theme(&mut self, cx: &mut App) {
|
|
||||||
if !self.selection_completed {
|
|
||||||
Self::set_icon_theme(self.original_theme.clone(), cx);
|
|
||||||
self.selection_completed = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_icon_theme(name: IconThemeName, cx: &mut App) {
|
fn set_icon_theme(name: IconThemeName, cx: &mut App) {
|
||||||
SettingsStore::update_global(cx, |store, _| {
|
SettingsStore::update_global(cx, |store, _| {
|
||||||
let mut theme_settings = store.get::<ThemeSettings>(None).clone();
|
let mut theme_settings = store.get::<ThemeSettings>(None).clone();
|
||||||
|
|
@ -203,7 +185,10 @@ impl PickerDelegate for IconThemeSelectorDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<IconThemeSelectorDelegate>>) {
|
fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<IconThemeSelectorDelegate>>) {
|
||||||
self.revert_theme(cx);
|
if !self.selection_completed {
|
||||||
|
Self::set_icon_theme(self.original_theme.clone(), cx);
|
||||||
|
self.selection_completed = true;
|
||||||
|
}
|
||||||
|
|
||||||
self.selector
|
self.selector
|
||||||
.update(cx, |_, cx| cx.emit(DismissEvent))
|
.update(cx, |_, cx| cx.emit(DismissEvent))
|
||||||
|
|
|
||||||
|
|
@ -79,18 +79,7 @@ fn toggle_icon_theme_selector(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ModalView for ThemeSelector {
|
impl ModalView for ThemeSelector {}
|
||||||
fn on_before_dismiss(
|
|
||||||
&mut self,
|
|
||||||
_window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) -> workspace::DismissDecision {
|
|
||||||
self.picker.update(cx, |picker, cx| {
|
|
||||||
picker.delegate.revert_theme(cx);
|
|
||||||
});
|
|
||||||
workspace::DismissDecision::Dismiss(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ThemeSelector {
|
struct ThemeSelector {
|
||||||
picker: Entity<Picker<ThemeSelectorDelegate>>,
|
picker: Entity<Picker<ThemeSelectorDelegate>>,
|
||||||
|
|
@ -226,15 +215,6 @@ impl ThemeSelectorDelegate {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn revert_theme(&mut self, cx: &mut App) {
|
|
||||||
if !self.selection_completed {
|
|
||||||
SettingsStore::update_global(cx, |store, _| {
|
|
||||||
store.override_global(self.original_theme_settings.clone());
|
|
||||||
});
|
|
||||||
self.selection_completed = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_theme(&mut self, new_theme: Arc<Theme>, cx: &mut App) {
|
fn set_theme(&mut self, new_theme: Arc<Theme>, cx: &mut App) {
|
||||||
// Update the global (in-memory) theme settings.
|
// Update the global (in-memory) theme settings.
|
||||||
SettingsStore::update_global(cx, |store, _| {
|
SettingsStore::update_global(cx, |store, _| {
|
||||||
|
|
@ -391,7 +371,12 @@ impl PickerDelegate for ThemeSelectorDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<ThemeSelectorDelegate>>) {
|
fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<ThemeSelectorDelegate>>) {
|
||||||
self.revert_theme(cx);
|
if !self.selection_completed {
|
||||||
|
SettingsStore::update_global(cx, |store, _| {
|
||||||
|
store.override_global(self.original_theme_settings.clone());
|
||||||
|
});
|
||||||
|
self.selection_completed = true;
|
||||||
|
}
|
||||||
|
|
||||||
self.selector
|
self.selector
|
||||||
.update(cx, |_, cx| cx.emit(DismissEvent))
|
.update(cx, |_, cx| cx.emit(DismissEvent))
|
||||||
|
|
|
||||||
|
|
@ -775,11 +775,6 @@ pub fn theme_colors_refinement(
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|color| try_parse_color(color).ok())
|
.and_then(|color| try_parse_color(color).ok())
|
||||||
.or(editor_document_highlight_read_background),
|
.or(editor_document_highlight_read_background),
|
||||||
vim_helix_jump_label_foreground: this
|
|
||||||
.vim_helix_jump_label_foreground
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|color| try_parse_color(color).ok())
|
|
||||||
.or(status_colors.error),
|
|
||||||
vim_helix_normal_background: this
|
vim_helix_normal_background: this
|
||||||
.vim_helix_normal_background
|
.vim_helix_normal_background
|
||||||
.as_ref()
|
.as_ref()
|
||||||
|
|
@ -853,39 +848,3 @@ fn try_parse_color(color: &str) -> anyhow::Result<Hsla> {
|
||||||
|
|
||||||
Ok(hsla)
|
Ok(hsla)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn helix_jump_label_color_uses_theme_color_or_status_error() {
|
|
||||||
let status_error = try_parse_color("#e63333").expect("valid color");
|
|
||||||
let override_color = try_parse_color("#00ff00").expect("valid color");
|
|
||||||
let status_colors = StatusColorsRefinement {
|
|
||||||
error: Some(status_error),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
let fallback_refinement =
|
|
||||||
theme_colors_refinement(&ThemeColorsContent::default(), &status_colors);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
fallback_refinement.vim_helix_jump_label_foreground,
|
|
||||||
Some(status_error)
|
|
||||||
);
|
|
||||||
|
|
||||||
let override_refinement = theme_colors_refinement(
|
|
||||||
&ThemeColorsContent {
|
|
||||||
vim_helix_jump_label_foreground: Some("#00ff00".to_string()),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
&status_colors,
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
override_refinement.vim_helix_jump_label_foreground,
|
|
||||||
Some(override_color)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -96,17 +96,11 @@ pub(crate) struct UiFontSize(Pixels);
|
||||||
|
|
||||||
impl Global for UiFontSize {}
|
impl Global for UiFontSize {}
|
||||||
|
|
||||||
/// In-memory override for the UI font size in the agent panel.
|
/// In-memory override for the font size in the agent panel.
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct AgentUiFontSize(Pixels);
|
pub struct AgentFontSize(Pixels);
|
||||||
|
|
||||||
impl Global for AgentUiFontSize {}
|
impl Global for AgentFontSize {}
|
||||||
|
|
||||||
/// In-memory override for the buffer font size in the agent panel.
|
|
||||||
#[derive(Default)]
|
|
||||||
pub struct AgentBufferFontSize(Pixels);
|
|
||||||
|
|
||||||
impl Global for AgentBufferFontSize {}
|
|
||||||
|
|
||||||
/// Represents the selection of a theme, which can be either static or dynamic.
|
/// Represents the selection of a theme, which can be either static or dynamic.
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
|
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
|
||||||
|
|
@ -384,7 +378,7 @@ impl ThemeSettings {
|
||||||
|
|
||||||
/// Returns the agent panel font size. Falls back to the UI font size if unset.
|
/// Returns the agent panel font size. Falls back to the UI font size if unset.
|
||||||
pub fn agent_ui_font_size(&self, cx: &App) -> Pixels {
|
pub fn agent_ui_font_size(&self, cx: &App) -> Pixels {
|
||||||
cx.try_global::<AgentUiFontSize>()
|
cx.try_global::<AgentFontSize>()
|
||||||
.map(|size| size.0)
|
.map(|size| size.0)
|
||||||
.or(self.agent_ui_font_size)
|
.or(self.agent_ui_font_size)
|
||||||
.map(clamp_font_size)
|
.map(clamp_font_size)
|
||||||
|
|
@ -393,7 +387,7 @@ impl ThemeSettings {
|
||||||
|
|
||||||
/// Returns the agent panel buffer font size.
|
/// Returns the agent panel buffer font size.
|
||||||
pub fn agent_buffer_font_size(&self, cx: &App) -> Pixels {
|
pub fn agent_buffer_font_size(&self, cx: &App) -> Pixels {
|
||||||
cx.try_global::<AgentBufferFontSize>()
|
cx.try_global::<AgentFontSize>()
|
||||||
.map(|size| size.0)
|
.map(|size| size.0)
|
||||||
.or(self.agent_buffer_font_size)
|
.or(self.agent_buffer_font_size)
|
||||||
.map(clamp_font_size)
|
.map(clamp_font_size)
|
||||||
|
|
@ -549,16 +543,16 @@ pub fn reset_ui_font_size(cx: &mut App) {
|
||||||
pub fn adjust_agent_ui_font_size(cx: &mut App, f: impl FnOnce(Pixels) -> Pixels) {
|
pub fn adjust_agent_ui_font_size(cx: &mut App, f: impl FnOnce(Pixels) -> Pixels) {
|
||||||
let agent_ui_font_size = ThemeSettings::get_global(cx).agent_ui_font_size(cx);
|
let agent_ui_font_size = ThemeSettings::get_global(cx).agent_ui_font_size(cx);
|
||||||
let adjusted_size = cx
|
let adjusted_size = cx
|
||||||
.try_global::<AgentUiFontSize>()
|
.try_global::<AgentFontSize>()
|
||||||
.map_or(agent_ui_font_size, |adjusted_size| adjusted_size.0);
|
.map_or(agent_ui_font_size, |adjusted_size| adjusted_size.0);
|
||||||
cx.set_global(AgentUiFontSize(clamp_font_size(f(adjusted_size))));
|
cx.set_global(AgentFontSize(clamp_font_size(f(adjusted_size))));
|
||||||
cx.refresh_windows();
|
cx.refresh_windows();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resets the agent response font size in the agent panel to the default value.
|
/// Resets the agent response font size in the agent panel to the default value.
|
||||||
pub fn reset_agent_ui_font_size(cx: &mut App) {
|
pub fn reset_agent_ui_font_size(cx: &mut App) {
|
||||||
if cx.has_global::<AgentUiFontSize>() {
|
if cx.has_global::<AgentFontSize>() {
|
||||||
cx.remove_global::<AgentUiFontSize>();
|
cx.remove_global::<AgentFontSize>();
|
||||||
cx.refresh_windows();
|
cx.refresh_windows();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -567,16 +561,16 @@ pub fn reset_agent_ui_font_size(cx: &mut App) {
|
||||||
pub fn adjust_agent_buffer_font_size(cx: &mut App, f: impl FnOnce(Pixels) -> Pixels) {
|
pub fn adjust_agent_buffer_font_size(cx: &mut App, f: impl FnOnce(Pixels) -> Pixels) {
|
||||||
let agent_buffer_font_size = ThemeSettings::get_global(cx).agent_buffer_font_size(cx);
|
let agent_buffer_font_size = ThemeSettings::get_global(cx).agent_buffer_font_size(cx);
|
||||||
let adjusted_size = cx
|
let adjusted_size = cx
|
||||||
.try_global::<AgentBufferFontSize>()
|
.try_global::<AgentFontSize>()
|
||||||
.map_or(agent_buffer_font_size, |adjusted_size| adjusted_size.0);
|
.map_or(agent_buffer_font_size, |adjusted_size| adjusted_size.0);
|
||||||
cx.set_global(AgentBufferFontSize(clamp_font_size(f(adjusted_size))));
|
cx.set_global(AgentFontSize(clamp_font_size(f(adjusted_size))));
|
||||||
cx.refresh_windows();
|
cx.refresh_windows();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resets the user message font size in the agent panel to the default value.
|
/// Resets the user message font size in the agent panel to the default value.
|
||||||
pub fn reset_agent_buffer_font_size(cx: &mut App) {
|
pub fn reset_agent_buffer_font_size(cx: &mut App) {
|
||||||
if cx.has_global::<AgentBufferFontSize>() {
|
if cx.has_global::<AgentFontSize>() {
|
||||||
cx.remove_global::<AgentBufferFontSize>();
|
cx.remove_global::<AgentFontSize>();
|
||||||
cx.refresh_windows();
|
cx.refresh_windows();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue