Compare commits
32 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5cc3b3a04f | ||
|
|
422dc4f307 | ||
|
|
b1aa0e2efd | ||
|
|
3dbfee1c47 | ||
|
|
0c9992c5e9 | ||
|
|
cec46079fe | ||
|
|
f9b69aeff0 | ||
|
|
f00cb371f4 | ||
|
|
25e1e2ecdd | ||
|
|
f2d29f4790 | ||
|
|
623e13761b | ||
|
|
302a4bbdd0 | ||
|
|
c4f8f2fbf4 | ||
|
|
52c7447106 | ||
|
|
65f7412a02 | ||
|
|
8aab646aec | ||
|
|
9ad059d3be | ||
|
|
0d0a08203f | ||
|
|
81463223d5 | ||
|
|
e8807e5764 | ||
|
|
73f129a685 | ||
|
|
fa529b2ad2 | ||
|
|
27c5d39d28 | ||
|
|
83ca2f9e88 | ||
|
|
847457df1b | ||
|
|
8c7a04c6bf | ||
|
|
b22ccfaff5 | ||
|
|
4930d3aa80 | ||
|
|
5d633a3968 | ||
|
|
6967ea41e5 | ||
|
|
5068581b39 | ||
|
|
f3bd6b88db |
2
.github/workflows/autofix_pr.yml
vendored
2
.github/workflows/autofix_pr.yml
vendored
@@ -90,7 +90,7 @@ jobs:
|
||||
runs-on: namespace-profile-2x4-ubuntu-2404
|
||||
steps:
|
||||
- id: get-app-token
|
||||
name: autofix_pr::commit_changes::authenticate_as_zippy
|
||||
name: steps::authenticate_as_zippy
|
||||
uses: actions/create-github-app-token@bef1eaf1c0ac2b148ee2a0a74c65fbe6db0631f1
|
||||
with:
|
||||
app-id: ${{ secrets.ZED_ZIPPY_APP_ID }}
|
||||
|
||||
2
.github/workflows/cherry_pick.yml
vendored
2
.github/workflows/cherry_pick.yml
vendored
@@ -30,7 +30,7 @@ jobs:
|
||||
with:
|
||||
clean: false
|
||||
- id: get-app-token
|
||||
name: cherry_pick::run_cherry_pick::authenticate_as_zippy
|
||||
name: steps::authenticate_as_zippy
|
||||
uses: actions/create-github-app-token@bef1eaf1c0ac2b148ee2a0a74c65fbe6db0631f1
|
||||
with:
|
||||
app-id: ${{ secrets.ZED_ZIPPY_APP_ID }}
|
||||
|
||||
8
.github/workflows/release.yml
vendored
8
.github/workflows/release.yml
vendored
@@ -478,11 +478,17 @@ jobs:
|
||||
if: startsWith(github.ref, 'refs/tags/v') && endsWith(github.ref, '-pre') && !endsWith(github.ref, '.0-pre')
|
||||
runs-on: namespace-profile-2x4-ubuntu-2404
|
||||
steps:
|
||||
- id: get-app-token
|
||||
name: steps::authenticate_as_zippy
|
||||
uses: actions/create-github-app-token@bef1eaf1c0ac2b148ee2a0a74c65fbe6db0631f1
|
||||
with:
|
||||
app-id: ${{ secrets.ZED_ZIPPY_APP_ID }}
|
||||
private-key: ${{ secrets.ZED_ZIPPY_APP_PRIVATE_KEY }}
|
||||
- name: gh release edit "$GITHUB_REF_NAME" --repo=zed-industries/zed --draft=false
|
||||
run: gh release edit "$GITHUB_REF_NAME" --repo=zed-industries/zed --draft=false
|
||||
shell: bash -euxo pipefail {0}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ steps.get-app-token.outputs.token }}
|
||||
notify_on_failure:
|
||||
needs:
|
||||
- upload_release_assets
|
||||
|
||||
54
Cargo.lock
generated
54
Cargo.lock
generated
@@ -2667,9 +2667,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cap-fs-ext"
|
||||
version = "3.4.5"
|
||||
version = "3.4.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d5528f85b1e134ae811704e41ef80930f56e795923f866813255bc342cc20654"
|
||||
checksum = "e41cc18551193fe8fa6f15c1e3c799bc5ec9e2cfbfaa8ed46f37013e3e6c173c"
|
||||
dependencies = [
|
||||
"cap-primitives",
|
||||
"cap-std",
|
||||
@@ -2679,9 +2679,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cap-net-ext"
|
||||
version = "3.4.5"
|
||||
version = "3.4.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "20a158160765c6a7d0d8c072a53d772e4cb243f38b04bfcf6b4939cfbe7482e7"
|
||||
checksum = "9f83833816c66c986e913b22ac887cec216ea09301802054316fc5301809702c"
|
||||
dependencies = [
|
||||
"cap-primitives",
|
||||
"cap-std",
|
||||
@@ -2691,9 +2691,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cap-primitives"
|
||||
version = "3.4.5"
|
||||
version = "3.4.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6cf3aea8a5081171859ef57bc1606b1df6999df4f1110f8eef68b30098d1d3a"
|
||||
checksum = "0a1e394ed14f39f8bc26f59d4c0c010dbe7f0a1b9bafff451b1f98b67c8af62a"
|
||||
dependencies = [
|
||||
"ambient-authority",
|
||||
"fs-set-times",
|
||||
@@ -2709,9 +2709,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cap-rand"
|
||||
version = "3.4.5"
|
||||
version = "3.4.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d8144c22e24bbcf26ade86cb6501a0916c46b7e4787abdb0045a467eb1645a1d"
|
||||
checksum = "0acb89ccf798a28683f00089d0630dfaceec087234eae0d308c05ddeaa941b40"
|
||||
dependencies = [
|
||||
"ambient-authority",
|
||||
"rand 0.8.5",
|
||||
@@ -2719,9 +2719,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cap-std"
|
||||
version = "3.4.5"
|
||||
version = "3.4.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6dc3090992a735d23219de5c204927163d922f42f575a0189b005c62d37549a"
|
||||
checksum = "07c0355ca583dd58f176c3c12489d684163861ede3c9efa6fd8bba314c984189"
|
||||
dependencies = [
|
||||
"cap-primitives",
|
||||
"io-extras",
|
||||
@@ -2731,9 +2731,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cap-time-ext"
|
||||
version = "3.4.5"
|
||||
version = "3.4.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "def102506ce40c11710a9b16e614af0cde8e76ae51b1f48c04b8d79f4b671a80"
|
||||
checksum = "491af520b8770085daa0466978c75db90368c71896523f2464214e38359b1a5b"
|
||||
dependencies = [
|
||||
"ambient-authority",
|
||||
"cap-primitives",
|
||||
@@ -2896,6 +2896,17 @@ dependencies = [
|
||||
"util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "chardetng"
|
||||
version = "0.1.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "14b8f0b65b7b08ae3c8187e8d77174de20cb6777864c6b832d8ad365999cf1ea"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"encoding_rs",
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.42"
|
||||
@@ -8797,6 +8808,7 @@ dependencies = [
|
||||
"ctor",
|
||||
"diffy",
|
||||
"ec4rs",
|
||||
"encoding_rs",
|
||||
"fs",
|
||||
"futures 0.3.31",
|
||||
"fuzzy",
|
||||
@@ -12465,6 +12477,7 @@ dependencies = [
|
||||
"dap",
|
||||
"dap_adapters",
|
||||
"db",
|
||||
"encoding_rs",
|
||||
"extension",
|
||||
"fancy-regex",
|
||||
"fs",
|
||||
@@ -19120,6 +19133,20 @@ dependencies = [
|
||||
"winsafe",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "which_key"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"command_palette",
|
||||
"gpui",
|
||||
"serde",
|
||||
"settings",
|
||||
"theme",
|
||||
"ui",
|
||||
"util",
|
||||
"workspace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "whoami"
|
||||
version = "1.6.1"
|
||||
@@ -20217,8 +20244,10 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-lock 2.8.0",
|
||||
"chardetng",
|
||||
"clock",
|
||||
"collections",
|
||||
"encoding_rs",
|
||||
"fs",
|
||||
"futures 0.3.31",
|
||||
"fuzzy",
|
||||
@@ -20730,6 +20759,7 @@ dependencies = [
|
||||
"watch",
|
||||
"web_search",
|
||||
"web_search_providers",
|
||||
"which_key",
|
||||
"windows 0.61.3",
|
||||
"winresource",
|
||||
"workspace",
|
||||
|
||||
@@ -192,6 +192,7 @@ members = [
|
||||
"crates/vercel",
|
||||
"crates/vim",
|
||||
"crates/vim_mode_setting",
|
||||
"crates/which_key",
|
||||
"crates/watch",
|
||||
"crates/web_search",
|
||||
"crates/web_search_providers",
|
||||
@@ -415,6 +416,7 @@ util_macros = { path = "crates/util_macros" }
|
||||
vercel = { path = "crates/vercel" }
|
||||
vim = { path = "crates/vim" }
|
||||
vim_mode_setting = { path = "crates/vim_mode_setting" }
|
||||
which_key = { path = "crates/which_key" }
|
||||
|
||||
watch = { path = "crates/watch" }
|
||||
web_search = { path = "crates/web_search" }
|
||||
@@ -476,6 +478,7 @@ bytes = "1.0"
|
||||
cargo_metadata = "0.19"
|
||||
cargo_toml = "0.21"
|
||||
cfg-if = "1.0.3"
|
||||
chardetng = "0.1"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
ciborium = "0.2"
|
||||
circular-buffer = "1.0"
|
||||
@@ -499,6 +502,7 @@ dotenvy = "0.15.0"
|
||||
ec4rs = "1.1"
|
||||
emojis = "0.6.1"
|
||||
env_logger = "0.11"
|
||||
encoding_rs = "0.8"
|
||||
exec = "0.3.1"
|
||||
fancy-regex = "0.16.0"
|
||||
fork = "0.4.0"
|
||||
|
||||
@@ -905,8 +905,8 @@
|
||||
"bindings": {
|
||||
"left": "git_panel::CollapseSelectedEntry",
|
||||
"right": "git_panel::ExpandSelectedEntry",
|
||||
"up": "menu::SelectPrevious",
|
||||
"down": "menu::SelectNext",
|
||||
"up": "git_panel::PreviousEntry",
|
||||
"down": "git_panel::NextEntry",
|
||||
"enter": "menu::Confirm",
|
||||
"alt-y": "git::StageFile",
|
||||
"alt-shift-y": "git::UnstageFile",
|
||||
|
||||
@@ -981,12 +981,12 @@
|
||||
"context": "GitPanel && ChangesList",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"up": "git_panel::PreviousEntry",
|
||||
"down": "git_panel::NextEntry",
|
||||
"cmd-up": "git_panel::FirstEntry",
|
||||
"cmd-down": "git_panel::LastEntry",
|
||||
"left": "git_panel::CollapseSelectedEntry",
|
||||
"right": "git_panel::ExpandSelectedEntry",
|
||||
"up": "menu::SelectPrevious",
|
||||
"down": "menu::SelectNext",
|
||||
"cmd-up": "menu::SelectFirst",
|
||||
"cmd-down": "menu::SelectLast",
|
||||
"enter": "menu::Confirm",
|
||||
"cmd-alt-y": "git::ToggleStaged",
|
||||
"space": "git::ToggleStaged",
|
||||
|
||||
@@ -908,10 +908,10 @@
|
||||
"context": "GitPanel && ChangesList",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"up": "git_panel::PreviousEntry",
|
||||
"down": "git_panel::NextEntry",
|
||||
"left": "git_panel::CollapseSelectedEntry",
|
||||
"right": "git_panel::ExpandSelectedEntry",
|
||||
"up": "menu::SelectPrevious",
|
||||
"down": "menu::SelectNext",
|
||||
"enter": "menu::Confirm",
|
||||
"alt-y": "git::StageFile",
|
||||
"shift-alt-y": "git::UnstageFile",
|
||||
|
||||
@@ -14,7 +14,6 @@ The section you'll need to rewrite is marked with <rewrite_this></rewrite_this>
|
||||
The context around the relevant section has been truncated (possibly in the middle of a line) for brevity.
|
||||
{{/if}}
|
||||
|
||||
{{#if rewrite_section}}
|
||||
And here's the section to rewrite based on that prompt again for reference:
|
||||
|
||||
<rewrite_this>
|
||||
@@ -33,8 +32,6 @@ Below are the diagnostic errors visible to the user. If the user requests probl
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
|
||||
{{/if}}
|
||||
|
||||
Only make changes that are necessary to fulfill the prompt, leave everything else as-is. All surrounding {{content_type}} will be preserved.
|
||||
|
||||
Start at the indentation level in the original file in the rewritten {{content_type}}.
|
||||
|
||||
@@ -1705,7 +1705,12 @@
|
||||
// }
|
||||
//
|
||||
"file_types": {
|
||||
"JSONC": ["**/.zed/**/*.json", "**/zed/**/*.json", "**/Zed/**/*.json", "**/.vscode/**/*.json", "tsconfig*.json"],
|
||||
"JSONC": [
|
||||
"**/.zed/*.json",
|
||||
"**/.vscode/**/*.json",
|
||||
"**/{zed,Zed}/{settings,keymap,tasks,debug}.json",
|
||||
"tsconfig*.json",
|
||||
],
|
||||
"Markdown": [".rules", ".cursorrules", ".windsurfrules", ".clinerules"],
|
||||
"Shell Script": [".env.*"],
|
||||
},
|
||||
@@ -2152,6 +2157,13 @@
|
||||
// The shape can be one of the following: "block", "bar", "underline", "hollow".
|
||||
"cursor_shape": {},
|
||||
},
|
||||
// Which-key popup settings
|
||||
"which_key": {
|
||||
// Whether to show the which-key popup when holding down key combinations.
|
||||
"enabled": false,
|
||||
// Delay in milliseconds before showing the which-key popup.
|
||||
"delay_ms": 1000,
|
||||
},
|
||||
// The server to connect to. If the environment variable
|
||||
// ZED_SERVER_URL is set, it will override this setting.
|
||||
"server_url": "https://zed.dev",
|
||||
|
||||
@@ -216,14 +216,10 @@ impl HistoryStore {
|
||||
}
|
||||
|
||||
pub fn reload(&self, cx: &mut Context<Self>) {
|
||||
let database_future = ThreadsDatabase::connect(cx);
|
||||
let database_connection = ThreadsDatabase::connect(cx);
|
||||
cx.spawn(async move |this, cx| {
|
||||
let threads = database_future
|
||||
.await
|
||||
.map_err(|err| anyhow!(err))?
|
||||
.list_threads()
|
||||
.await?;
|
||||
|
||||
let database = database_connection.await;
|
||||
let threads = database.map_err(|err| anyhow!(err))?.list_threads().await?;
|
||||
this.update(cx, |this, cx| {
|
||||
if this.recently_opened_entries.len() < MAX_RECENTLY_OPENED_ENTRIES {
|
||||
for thread in threads
|
||||
@@ -344,7 +340,8 @@ impl HistoryStore {
|
||||
fn load_recently_opened_entries(cx: &AsyncApp) -> Task<Result<VecDeque<HistoryEntryId>>> {
|
||||
cx.background_spawn(async move {
|
||||
if cfg!(any(feature = "test-support", test)) {
|
||||
anyhow::bail!("history store does not persist in tests");
|
||||
log::warn!("history store does not persist in tests");
|
||||
return Ok(VecDeque::new());
|
||||
}
|
||||
let json = KEY_VALUE_STORE
|
||||
.read_kvp(RECENTLY_OPENED_THREADS_KEY)?
|
||||
|
||||
@@ -13,7 +13,7 @@ path = "src/agent_ui.rs"
|
||||
doctest = false
|
||||
|
||||
[features]
|
||||
test-support = ["assistant_text_thread/test-support", "eval_utils", "gpui/test-support", "language/test-support", "reqwest_client", "workspace/test-support"]
|
||||
test-support = ["assistant_text_thread/test-support", "eval_utils", "gpui/test-support", "language/test-support", "reqwest_client", "workspace/test-support", "agent/test-support"]
|
||||
unit-eval = []
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -75,6 +75,9 @@ pub struct BufferCodegen {
|
||||
session_id: Uuid,
|
||||
}
|
||||
|
||||
pub const REWRITE_SECTION_TOOL_NAME: &str = "rewrite_section";
|
||||
pub const FAILURE_MESSAGE_TOOL_NAME: &str = "failure_message";
|
||||
|
||||
impl BufferCodegen {
|
||||
pub fn new(
|
||||
buffer: Entity<MultiBuffer>,
|
||||
@@ -522,12 +525,12 @@ impl CodegenAlternative {
|
||||
|
||||
let tools = vec![
|
||||
LanguageModelRequestTool {
|
||||
name: "rewrite_section".to_string(),
|
||||
name: REWRITE_SECTION_TOOL_NAME.to_string(),
|
||||
description: "Replaces text in <rewrite_this></rewrite_this> tags with your replacement_text.".to_string(),
|
||||
input_schema: language_model::tool_schema::root_schema_for::<RewriteSectionInput>(tool_input_format).to_value(),
|
||||
},
|
||||
LanguageModelRequestTool {
|
||||
name: "failure_message".to_string(),
|
||||
name: FAILURE_MESSAGE_TOOL_NAME.to_string(),
|
||||
description: "Use this tool to provide a message to the user when you're unable to complete a task.".to_string(),
|
||||
input_schema: language_model::tool_schema::root_schema_for::<FailureMessageInput>(tool_input_format).to_value(),
|
||||
},
|
||||
@@ -1167,7 +1170,7 @@ impl CodegenAlternative {
|
||||
let process_tool_use = move |tool_use: LanguageModelToolUse| -> Option<ToolUseOutput> {
|
||||
let mut chars_read_so_far = chars_read_so_far.lock();
|
||||
match tool_use.name.as_ref() {
|
||||
"rewrite_section" => {
|
||||
REWRITE_SECTION_TOOL_NAME => {
|
||||
let Ok(input) =
|
||||
serde_json::from_value::<RewriteSectionInput>(tool_use.input)
|
||||
else {
|
||||
@@ -1180,7 +1183,7 @@ impl CodegenAlternative {
|
||||
description: None,
|
||||
})
|
||||
}
|
||||
"failure_message" => {
|
||||
FAILURE_MESSAGE_TOOL_NAME => {
|
||||
let Ok(mut input) =
|
||||
serde_json::from_value::<FailureMessageInput>(tool_use.input)
|
||||
else {
|
||||
@@ -1493,7 +1496,10 @@ mod tests {
|
||||
use indoc::indoc;
|
||||
use language::{Buffer, Point};
|
||||
use language_model::fake_provider::FakeLanguageModel;
|
||||
use language_model::{LanguageModelRegistry, TokenUsage};
|
||||
use language_model::{
|
||||
LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelRegistry,
|
||||
LanguageModelToolUse, StopReason, TokenUsage,
|
||||
};
|
||||
use languages::rust_lang;
|
||||
use rand::prelude::*;
|
||||
use settings::SettingsStore;
|
||||
@@ -1805,6 +1811,51 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
// When not streaming tool calls, we strip backticks as part of parsing the model's
|
||||
// plain text response. This is a regression test for a bug where we stripped
|
||||
// backticks incorrectly.
|
||||
#[gpui::test]
|
||||
async fn test_allows_model_to_output_backticks(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let text = "- Improved; `cmd+click` behavior. Now requires `cmd` to be pressed before the click starts or it doesn't run. ([#44579](https://github.com/zed-industries/zed/pull/44579); thanks [Zachiah](https://github.com/Zachiah))";
|
||||
let buffer = cx.new(|cx| Buffer::local("", cx));
|
||||
let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
let range = buffer.read_with(cx, |buffer, cx| {
|
||||
let snapshot = buffer.snapshot(cx);
|
||||
snapshot.anchor_before(Point::new(0, 0))..snapshot.anchor_after(Point::new(0, 0))
|
||||
});
|
||||
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
|
||||
let codegen = cx.new(|cx| {
|
||||
CodegenAlternative::new(
|
||||
buffer.clone(),
|
||||
range.clone(),
|
||||
true,
|
||||
prompt_builder,
|
||||
Uuid::new_v4(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let events_tx = simulate_tool_based_completion(&codegen, cx);
|
||||
let chunk_len = text.find('`').unwrap();
|
||||
events_tx
|
||||
.unbounded_send(rewrite_tool_use("tool_1", &text[..chunk_len], false))
|
||||
.unwrap();
|
||||
events_tx
|
||||
.unbounded_send(rewrite_tool_use("tool_2", &text, true))
|
||||
.unwrap();
|
||||
events_tx
|
||||
.unbounded_send(LanguageModelCompletionEvent::Stop(StopReason::EndTurn))
|
||||
.unwrap();
|
||||
drop(events_tx);
|
||||
cx.run_until_parked();
|
||||
|
||||
assert_eq!(
|
||||
buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx).text()),
|
||||
text
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_strip_invalid_spans_from_codeblock() {
|
||||
assert_chunks("Lorem ipsum dolor", "Lorem ipsum dolor").await;
|
||||
@@ -1870,4 +1921,39 @@ mod tests {
|
||||
});
|
||||
chunks_tx
|
||||
}
|
||||
|
||||
fn simulate_tool_based_completion(
|
||||
codegen: &Entity<CodegenAlternative>,
|
||||
cx: &mut TestAppContext,
|
||||
) -> mpsc::UnboundedSender<LanguageModelCompletionEvent> {
|
||||
let (events_tx, events_rx) = mpsc::unbounded();
|
||||
let model = Arc::new(FakeLanguageModel::default());
|
||||
codegen.update(cx, |codegen, cx| {
|
||||
let completion_stream = Task::ready(Ok(events_rx.map(Ok).boxed()
|
||||
as BoxStream<
|
||||
'static,
|
||||
Result<LanguageModelCompletionEvent, LanguageModelCompletionError>,
|
||||
>));
|
||||
codegen.generation = codegen.handle_completion(model, completion_stream, cx);
|
||||
});
|
||||
events_tx
|
||||
}
|
||||
|
||||
fn rewrite_tool_use(
|
||||
id: &str,
|
||||
replacement_text: &str,
|
||||
is_complete: bool,
|
||||
) -> LanguageModelCompletionEvent {
|
||||
let input = RewriteSectionInput {
|
||||
replacement_text: replacement_text.into(),
|
||||
};
|
||||
LanguageModelCompletionEvent::ToolUse(LanguageModelToolUse {
|
||||
id: id.into(),
|
||||
name: REWRITE_SECTION_TOOL_NAME.into(),
|
||||
raw_input: serde_json::to_string(&input).unwrap(),
|
||||
input: serde_json::to_value(&input).unwrap(),
|
||||
is_input_complete: is_complete,
|
||||
thought_signature: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2271,6 +2271,36 @@ pub mod evals {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg_attr(not(feature = "unit-eval"), ignore)]
|
||||
fn eval_empty_buffer() {
|
||||
run_eval(
|
||||
20,
|
||||
1.0,
|
||||
"Write a Python hello, world program".to_string(),
|
||||
"ˇ".to_string(),
|
||||
|output| match output {
|
||||
InlineAssistantOutput::Success {
|
||||
full_buffer_text, ..
|
||||
} => {
|
||||
if full_buffer_text.is_empty() {
|
||||
EvalOutput::failed("expected some output".to_string())
|
||||
} else {
|
||||
EvalOutput::passed(format!("Produced {full_buffer_text}"))
|
||||
}
|
||||
}
|
||||
o @ InlineAssistantOutput::Failure { .. } => EvalOutput::failed(format!(
|
||||
"Assistant output does not match expected output: {:?}",
|
||||
o
|
||||
)),
|
||||
o @ InlineAssistantOutput::Malformed { .. } => EvalOutput::failed(format!(
|
||||
"Assistant output does not match expected output: {:?}",
|
||||
o
|
||||
)),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
fn run_eval(
|
||||
iterations: usize,
|
||||
expected_pass_ratio: f32,
|
||||
|
||||
@@ -12,6 +12,10 @@ workspace = true
|
||||
path = "src/agent_ui_v2.rs"
|
||||
doctest = false
|
||||
|
||||
[features]
|
||||
test-support = ["agent/test-support"]
|
||||
|
||||
|
||||
[dependencies]
|
||||
agent.workspace = true
|
||||
agent_servers.workspace = true
|
||||
@@ -38,3 +42,6 @@ time_format.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
workspace.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
agent = { workspace = true, features = ["test-support"] }
|
||||
|
||||
@@ -1 +1 @@
|
||||
LICENSE-GPL
|
||||
../../LICENSE-GPL
|
||||
@@ -1,6 +1,6 @@
|
||||
use anyhow::{Context as _, Result};
|
||||
use edit_prediction_context::{EditPredictionExcerpt, EditPredictionExcerptOptions};
|
||||
use edit_prediction_types::{Direction, EditPrediction, EditPredictionDelegate};
|
||||
use edit_prediction_types::{EditPrediction, EditPredictionDelegate};
|
||||
use futures::AsyncReadExt;
|
||||
use gpui::{App, Context, Entity, Task};
|
||||
use http_client::HttpClient;
|
||||
@@ -300,16 +300,6 @@ impl EditPredictionDelegate for CodestralEditPredictionDelegate {
|
||||
}));
|
||||
}
|
||||
|
||||
fn cycle(
|
||||
&mut self,
|
||||
_buffer: Entity<Buffer>,
|
||||
_cursor_position: Anchor,
|
||||
_direction: Direction,
|
||||
_cx: &mut Context<Self>,
|
||||
) {
|
||||
// Codestral doesn't support multiple completions, so cycling does nothing
|
||||
}
|
||||
|
||||
fn accept(&mut self, _cx: &mut Context<Self>) {
|
||||
log::debug!("Codestral: Completion accepted");
|
||||
self.pending_request = None;
|
||||
|
||||
@@ -4,6 +4,7 @@ pub mod copilot_responses;
|
||||
pub mod request;
|
||||
mod sign_in;
|
||||
|
||||
use crate::request::NextEditSuggestions;
|
||||
use crate::sign_in::initiate_sign_out;
|
||||
use ::fs::Fs;
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
@@ -18,7 +19,7 @@ use http_client::HttpClient;
|
||||
use language::language_settings::CopilotSettings;
|
||||
use language::{
|
||||
Anchor, Bias, Buffer, BufferSnapshot, Language, PointUtf16, ToPointUtf16,
|
||||
language_settings::{EditPredictionProvider, all_language_settings, language_settings},
|
||||
language_settings::{EditPredictionProvider, all_language_settings},
|
||||
point_from_lsp, point_to_lsp,
|
||||
};
|
||||
use lsp::{LanguageServer, LanguageServerBinary, LanguageServerId, LanguageServerName};
|
||||
@@ -40,7 +41,7 @@ use std::{
|
||||
sync::Arc,
|
||||
};
|
||||
use sum_tree::Dimensions;
|
||||
use util::{ResultExt, fs::remove_matching, rel_path::RelPath};
|
||||
use util::{ResultExt, fs::remove_matching};
|
||||
use workspace::Workspace;
|
||||
|
||||
pub use crate::copilot_edit_prediction_delegate::CopilotEditPredictionDelegate;
|
||||
@@ -315,6 +316,15 @@ struct GlobalCopilot(Entity<Copilot>);
|
||||
|
||||
impl Global for GlobalCopilot {}
|
||||
|
||||
/// Copilot's NextEditSuggestion response, with coordinates converted to Anchors.
|
||||
struct CopilotEditPrediction {
|
||||
buffer: Entity<Buffer>,
|
||||
range: Range<Anchor>,
|
||||
text: String,
|
||||
command: Option<lsp::Command>,
|
||||
snapshot: BufferSnapshot,
|
||||
}
|
||||
|
||||
impl Copilot {
|
||||
pub fn global(cx: &App) -> Option<Entity<Self>> {
|
||||
cx.try_global::<GlobalCopilot>()
|
||||
@@ -873,101 +883,19 @@ impl Copilot {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn completions<T>(
|
||||
pub(crate) fn completions(
|
||||
&mut self,
|
||||
buffer: &Entity<Buffer>,
|
||||
position: T,
|
||||
position: Anchor,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<Vec<Completion>>>
|
||||
where
|
||||
T: ToPointUtf16,
|
||||
{
|
||||
self.request_completions::<request::GetCompletions, _>(buffer, position, cx)
|
||||
}
|
||||
|
||||
pub fn completions_cycling<T>(
|
||||
&mut self,
|
||||
buffer: &Entity<Buffer>,
|
||||
position: T,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<Vec<Completion>>>
|
||||
where
|
||||
T: ToPointUtf16,
|
||||
{
|
||||
self.request_completions::<request::GetCompletionsCycling, _>(buffer, position, cx)
|
||||
}
|
||||
|
||||
pub fn accept_completion(
|
||||
&mut self,
|
||||
completion: &Completion,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
let server = match self.server.as_authenticated() {
|
||||
Ok(server) => server,
|
||||
Err(error) => return Task::ready(Err(error)),
|
||||
};
|
||||
let request =
|
||||
server
|
||||
.lsp
|
||||
.request::<request::NotifyAccepted>(request::NotifyAcceptedParams {
|
||||
uuid: completion.uuid.clone(),
|
||||
});
|
||||
cx.background_spawn(async move {
|
||||
request
|
||||
.await
|
||||
.into_response()
|
||||
.context("copilot: notify accepted")?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn discard_completions(
|
||||
&mut self,
|
||||
completions: &[Completion],
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
let server = match self.server.as_authenticated() {
|
||||
Ok(server) => server,
|
||||
Err(_) => return Task::ready(Ok(())),
|
||||
};
|
||||
let request =
|
||||
server
|
||||
.lsp
|
||||
.request::<request::NotifyRejected>(request::NotifyRejectedParams {
|
||||
uuids: completions
|
||||
.iter()
|
||||
.map(|completion| completion.uuid.clone())
|
||||
.collect(),
|
||||
});
|
||||
cx.background_spawn(async move {
|
||||
request
|
||||
.await
|
||||
.into_response()
|
||||
.context("copilot: notify rejected")?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn request_completions<R, T>(
|
||||
&mut self,
|
||||
buffer: &Entity<Buffer>,
|
||||
position: T,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<Vec<Completion>>>
|
||||
where
|
||||
R: 'static
|
||||
+ lsp::request::Request<
|
||||
Params = request::GetCompletionsParams,
|
||||
Result = request::GetCompletionsResult,
|
||||
>,
|
||||
T: ToPointUtf16,
|
||||
{
|
||||
) -> Task<Result<Vec<CopilotEditPrediction>>> {
|
||||
self.register_buffer(buffer, cx);
|
||||
|
||||
let server = match self.server.as_authenticated() {
|
||||
Ok(server) => server,
|
||||
Err(error) => return Task::ready(Err(error)),
|
||||
};
|
||||
let buffer_entity = buffer.clone();
|
||||
let lsp = server.lsp.clone();
|
||||
let registered_buffer = server
|
||||
.registered_buffers
|
||||
@@ -977,46 +905,31 @@ impl Copilot {
|
||||
let buffer = buffer.read(cx);
|
||||
let uri = registered_buffer.uri.clone();
|
||||
let position = position.to_point_utf16(buffer);
|
||||
let settings = language_settings(
|
||||
buffer.language_at(position).map(|l| l.name()),
|
||||
buffer.file(),
|
||||
cx,
|
||||
);
|
||||
let tab_size = settings.tab_size;
|
||||
let hard_tabs = settings.hard_tabs;
|
||||
let relative_path = buffer
|
||||
.file()
|
||||
.map_or(RelPath::empty().into(), |file| file.path().clone());
|
||||
|
||||
cx.background_spawn(async move {
|
||||
let (version, snapshot) = snapshot.await?;
|
||||
let result = lsp
|
||||
.request::<R>(request::GetCompletionsParams {
|
||||
doc: request::GetCompletionsDocument {
|
||||
uri,
|
||||
tab_size: tab_size.into(),
|
||||
indent_size: 1,
|
||||
insert_spaces: !hard_tabs,
|
||||
relative_path: relative_path.to_proto(),
|
||||
position: point_to_lsp(position),
|
||||
version: version.try_into().unwrap(),
|
||||
},
|
||||
.request::<NextEditSuggestions>(request::NextEditSuggestionsParams {
|
||||
text_document: lsp::VersionedTextDocumentIdentifier { uri, version },
|
||||
position: point_to_lsp(position),
|
||||
})
|
||||
.await
|
||||
.into_response()
|
||||
.context("copilot: get completions")?;
|
||||
let completions = result
|
||||
.completions
|
||||
.edits
|
||||
.into_iter()
|
||||
.map(|completion| {
|
||||
let start = snapshot
|
||||
.clip_point_utf16(point_from_lsp(completion.range.start), Bias::Left);
|
||||
let end =
|
||||
snapshot.clip_point_utf16(point_from_lsp(completion.range.end), Bias::Left);
|
||||
Completion {
|
||||
uuid: completion.uuid,
|
||||
CopilotEditPrediction {
|
||||
buffer: buffer_entity.clone(),
|
||||
range: snapshot.anchor_before(start)..snapshot.anchor_after(end),
|
||||
text: completion.text,
|
||||
command: completion.command,
|
||||
snapshot: snapshot.clone(),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
@@ -1024,6 +937,35 @@ impl Copilot {
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn accept_completion(
|
||||
&mut self,
|
||||
completion: &CopilotEditPrediction,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
let server = match self.server.as_authenticated() {
|
||||
Ok(server) => server,
|
||||
Err(error) => return Task::ready(Err(error)),
|
||||
};
|
||||
if let Some(command) = &completion.command {
|
||||
let request = server
|
||||
.lsp
|
||||
.request::<lsp::ExecuteCommand>(lsp::ExecuteCommandParams {
|
||||
command: command.command.clone(),
|
||||
arguments: command.arguments.clone().unwrap_or_default(),
|
||||
..Default::default()
|
||||
});
|
||||
cx.background_spawn(async move {
|
||||
request
|
||||
.await
|
||||
.into_response()
|
||||
.context("copilot: notify accepted")?;
|
||||
Ok(())
|
||||
})
|
||||
} else {
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn status(&self) -> Status {
|
||||
match &self.server {
|
||||
CopilotServer::Starting { task } => Status::Starting { task: task.clone() },
|
||||
@@ -1260,7 +1202,11 @@ async fn get_copilot_lsp(fs: Arc<dyn Fs>, node_runtime: NodeRuntime) -> anyhow::
|
||||
mod tests {
|
||||
use super::*;
|
||||
use gpui::TestAppContext;
|
||||
use util::{path, paths::PathStyle, rel_path::rel_path};
|
||||
use util::{
|
||||
path,
|
||||
paths::PathStyle,
|
||||
rel_path::{RelPath, rel_path},
|
||||
};
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_buffer_management(cx: &mut TestAppContext) {
|
||||
|
||||
@@ -1,49 +1,29 @@
|
||||
use crate::{Completion, Copilot};
|
||||
use crate::{Copilot, CopilotEditPrediction};
|
||||
use anyhow::Result;
|
||||
use edit_prediction_types::{Direction, EditPrediction, EditPredictionDelegate};
|
||||
use gpui::{App, Context, Entity, EntityId, Task};
|
||||
use language::{Buffer, OffsetRangeExt, ToOffset, language_settings::AllLanguageSettings};
|
||||
use settings::Settings;
|
||||
use std::{path::Path, time::Duration};
|
||||
use edit_prediction_types::{EditPrediction, EditPredictionDelegate, interpolate_edits};
|
||||
use gpui::{App, Context, Entity, Task};
|
||||
use language::{Anchor, Buffer, EditPreview, OffsetRangeExt};
|
||||
use std::{ops::Range, sync::Arc, time::Duration};
|
||||
|
||||
pub const COPILOT_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(75);
|
||||
|
||||
pub struct CopilotEditPredictionDelegate {
|
||||
cycled: bool,
|
||||
buffer_id: Option<EntityId>,
|
||||
completions: Vec<Completion>,
|
||||
active_completion_index: usize,
|
||||
file_extension: Option<String>,
|
||||
completion: Option<(CopilotEditPrediction, EditPreview)>,
|
||||
pending_refresh: Option<Task<Result<()>>>,
|
||||
pending_cycling_refresh: Option<Task<Result<()>>>,
|
||||
copilot: Entity<Copilot>,
|
||||
}
|
||||
|
||||
impl CopilotEditPredictionDelegate {
|
||||
pub fn new(copilot: Entity<Copilot>) -> Self {
|
||||
Self {
|
||||
cycled: false,
|
||||
buffer_id: None,
|
||||
completions: Vec::new(),
|
||||
active_completion_index: 0,
|
||||
file_extension: None,
|
||||
completion: None,
|
||||
pending_refresh: None,
|
||||
pending_cycling_refresh: None,
|
||||
copilot,
|
||||
}
|
||||
}
|
||||
|
||||
fn active_completion(&self) -> Option<&Completion> {
|
||||
self.completions.get(self.active_completion_index)
|
||||
}
|
||||
|
||||
fn push_completion(&mut self, new_completion: Completion) {
|
||||
for completion in &self.completions {
|
||||
if completion.text == new_completion.text && completion.range == new_completion.range {
|
||||
return;
|
||||
}
|
||||
}
|
||||
self.completions.push(new_completion);
|
||||
fn active_completion(&self) -> Option<&(CopilotEditPrediction, EditPreview)> {
|
||||
self.completion.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,12 +44,8 @@ impl EditPredictionDelegate for CopilotEditPredictionDelegate {
|
||||
true
|
||||
}
|
||||
|
||||
fn supports_jump_to_edit() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn is_refreshing(&self, _cx: &App) -> bool {
|
||||
self.pending_refresh.is_some() && self.completions.is_empty()
|
||||
self.pending_refresh.is_some() && self.completion.is_none()
|
||||
}
|
||||
|
||||
fn is_enabled(
|
||||
@@ -102,160 +78,96 @@ impl EditPredictionDelegate for CopilotEditPredictionDelegate {
|
||||
})?
|
||||
.await?;
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
if !completions.is_empty() {
|
||||
this.cycled = false;
|
||||
if let Some(mut completion) = completions.into_iter().next()
|
||||
&& let Some(trimmed_completion) = cx
|
||||
.update(|cx| trim_completion(&completion, cx))
|
||||
.ok()
|
||||
.flatten()
|
||||
{
|
||||
let preview = buffer
|
||||
.update(cx, |this, cx| {
|
||||
this.preview_edits(Arc::from(std::slice::from_ref(&trimmed_completion)), cx)
|
||||
})?
|
||||
.await;
|
||||
this.update(cx, |this, cx| {
|
||||
this.pending_refresh = None;
|
||||
this.pending_cycling_refresh = None;
|
||||
this.completions.clear();
|
||||
this.active_completion_index = 0;
|
||||
this.buffer_id = Some(buffer.entity_id());
|
||||
this.file_extension = buffer.read(cx).file().and_then(|file| {
|
||||
Some(
|
||||
Path::new(file.file_name(cx))
|
||||
.extension()?
|
||||
.to_str()?
|
||||
.to_string(),
|
||||
)
|
||||
});
|
||||
completion.range = trimmed_completion.0;
|
||||
completion.text = trimmed_completion.1.to_string();
|
||||
this.completion = Some((completion, preview));
|
||||
|
||||
for completion in completions {
|
||||
this.push_completion(completion);
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
})?;
|
||||
})?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}));
|
||||
}
|
||||
|
||||
fn cycle(
|
||||
&mut self,
|
||||
buffer: Entity<Buffer>,
|
||||
cursor_position: language::Anchor,
|
||||
direction: Direction,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if self.cycled {
|
||||
match direction {
|
||||
Direction::Prev => {
|
||||
self.active_completion_index = if self.active_completion_index == 0 {
|
||||
self.completions.len().saturating_sub(1)
|
||||
} else {
|
||||
self.active_completion_index - 1
|
||||
};
|
||||
}
|
||||
Direction::Next => {
|
||||
if self.completions.is_empty() {
|
||||
self.active_completion_index = 0
|
||||
} else {
|
||||
self.active_completion_index =
|
||||
(self.active_completion_index + 1) % self.completions.len();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
} else {
|
||||
let copilot = self.copilot.clone();
|
||||
self.pending_cycling_refresh = Some(cx.spawn(async move |this, cx| {
|
||||
let completions = copilot
|
||||
.update(cx, |copilot, cx| {
|
||||
copilot.completions_cycling(&buffer, cursor_position, cx)
|
||||
})?
|
||||
.await?;
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
this.cycled = true;
|
||||
this.file_extension = buffer.read(cx).file().and_then(|file| {
|
||||
Some(
|
||||
Path::new(file.file_name(cx))
|
||||
.extension()?
|
||||
.to_str()?
|
||||
.to_string(),
|
||||
)
|
||||
});
|
||||
for completion in completions {
|
||||
this.push_completion(completion);
|
||||
}
|
||||
this.cycle(buffer, cursor_position, direction, cx);
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
fn accept(&mut self, cx: &mut Context<Self>) {
|
||||
if let Some(completion) = self.active_completion() {
|
||||
if let Some((completion, _)) = self.active_completion() {
|
||||
self.copilot
|
||||
.update(cx, |copilot, cx| copilot.accept_completion(completion, cx))
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn discard(&mut self, cx: &mut Context<Self>) {
|
||||
let settings = AllLanguageSettings::get_global(cx);
|
||||
|
||||
let copilot_enabled = settings.show_edit_predictions(None, cx);
|
||||
|
||||
if !copilot_enabled {
|
||||
return;
|
||||
}
|
||||
|
||||
self.copilot
|
||||
.update(cx, |copilot, cx| {
|
||||
copilot.discard_completions(&self.completions, cx)
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
fn discard(&mut self, _: &mut Context<Self>) {}
|
||||
|
||||
fn suggest(
|
||||
&mut self,
|
||||
buffer: &Entity<Buffer>,
|
||||
cursor_position: language::Anchor,
|
||||
_: language::Anchor,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<EditPrediction> {
|
||||
let buffer_id = buffer.entity_id();
|
||||
let buffer = buffer.read(cx);
|
||||
let completion = self.active_completion()?;
|
||||
if Some(buffer_id) != self.buffer_id
|
||||
let (completion, edit_preview) = self.active_completion()?;
|
||||
|
||||
if Some(buffer_id) != Some(completion.buffer.entity_id())
|
||||
|| !completion.range.start.is_valid(buffer)
|
||||
|| !completion.range.end.is_valid(buffer)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let edits = vec![(
|
||||
completion.range.clone(),
|
||||
Arc::from(completion.text.as_ref()),
|
||||
)];
|
||||
let edits = interpolate_edits(&completion.snapshot, &buffer.snapshot(), &edits)
|
||||
.filter(|edits| !edits.is_empty())?;
|
||||
|
||||
let mut completion_range = completion.range.to_offset(buffer);
|
||||
let prefix_len = common_prefix(
|
||||
buffer.chars_for_range(completion_range.clone()),
|
||||
completion.text.chars(),
|
||||
);
|
||||
completion_range.start += prefix_len;
|
||||
let suffix_len = common_prefix(
|
||||
buffer.reversed_chars_for_range(completion_range.clone()),
|
||||
completion.text[prefix_len..].chars().rev(),
|
||||
);
|
||||
completion_range.end = completion_range.end.saturating_sub(suffix_len);
|
||||
Some(EditPrediction::Local {
|
||||
id: None,
|
||||
edits,
|
||||
edit_preview: Some(edit_preview.clone()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if completion_range.is_empty()
|
||||
&& completion_range.start == cursor_position.to_offset(buffer)
|
||||
{
|
||||
let completion_text = &completion.text[prefix_len..completion.text.len() - suffix_len];
|
||||
if completion_text.trim().is_empty() {
|
||||
None
|
||||
} else {
|
||||
let position = cursor_position.bias_right(buffer);
|
||||
Some(EditPrediction::Local {
|
||||
id: None,
|
||||
edits: vec![(position..position, completion_text.into())],
|
||||
edit_preview: None,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
fn trim_completion(
|
||||
completion: &CopilotEditPrediction,
|
||||
cx: &mut App,
|
||||
) -> Option<(Range<Anchor>, Arc<str>)> {
|
||||
let buffer = completion.buffer.read(cx);
|
||||
let mut completion_range = completion.range.to_offset(buffer);
|
||||
let prefix_len = common_prefix(
|
||||
buffer.chars_for_range(completion_range.clone()),
|
||||
completion.text.chars(),
|
||||
);
|
||||
completion_range.start += prefix_len;
|
||||
let suffix_len = common_prefix(
|
||||
buffer.reversed_chars_for_range(completion_range.clone()),
|
||||
completion.text[prefix_len..].chars().rev(),
|
||||
);
|
||||
completion_range.end = completion_range.end.saturating_sub(suffix_len);
|
||||
let completion_text = &completion.text[prefix_len..completion.text.len() - suffix_len];
|
||||
if completion_text.trim().is_empty() {
|
||||
None
|
||||
} else {
|
||||
let completion_range =
|
||||
buffer.anchor_after(completion_range.start)..buffer.anchor_after(completion_range.end);
|
||||
|
||||
Some((completion_range, Arc::from(completion_text)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -282,6 +194,7 @@ mod tests {
|
||||
Point,
|
||||
language_settings::{CompletionSettingsContent, LspInsertMode, WordsCompletionMode},
|
||||
};
|
||||
use lsp::Uri;
|
||||
use project::Project;
|
||||
use serde_json::json;
|
||||
use settings::{AllLanguageSettingsContent, SettingsStore};
|
||||
@@ -337,12 +250,15 @@ mod tests {
|
||||
));
|
||||
handle_copilot_completion_request(
|
||||
&copilot_lsp,
|
||||
vec![crate::request::Completion {
|
||||
vec![crate::request::NextEditSuggestion {
|
||||
text: "one.copilot1".into(),
|
||||
range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
|
||||
..Default::default()
|
||||
command: None,
|
||||
text_document: lsp::VersionedTextDocumentIdentifier {
|
||||
uri: Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(),
|
||||
version: 0,
|
||||
},
|
||||
}],
|
||||
vec![],
|
||||
);
|
||||
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
@@ -383,12 +299,15 @@ mod tests {
|
||||
));
|
||||
handle_copilot_completion_request(
|
||||
&copilot_lsp,
|
||||
vec![crate::request::Completion {
|
||||
vec![crate::request::NextEditSuggestion {
|
||||
text: "one.copilot1".into(),
|
||||
range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
|
||||
..Default::default()
|
||||
command: None,
|
||||
text_document: lsp::VersionedTextDocumentIdentifier {
|
||||
uri: Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(),
|
||||
version: 0,
|
||||
},
|
||||
}],
|
||||
vec![],
|
||||
);
|
||||
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
||||
cx.update_editor(|editor, _, cx| {
|
||||
@@ -412,12 +331,15 @@ mod tests {
|
||||
// After debouncing, new Copilot completions should be requested.
|
||||
handle_copilot_completion_request(
|
||||
&copilot_lsp,
|
||||
vec![crate::request::Completion {
|
||||
vec![crate::request::NextEditSuggestion {
|
||||
text: "one.copilot2".into(),
|
||||
range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 5)),
|
||||
..Default::default()
|
||||
command: None,
|
||||
text_document: lsp::VersionedTextDocumentIdentifier {
|
||||
uri: Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(),
|
||||
version: 0,
|
||||
},
|
||||
}],
|
||||
vec![],
|
||||
);
|
||||
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
@@ -479,45 +401,6 @@ mod tests {
|
||||
assert_eq!(editor.display_text(cx), "one.cop\ntwo\nthree\n");
|
||||
assert_eq!(editor.text(cx), "one.cop\ntwo\nthree\n");
|
||||
});
|
||||
|
||||
// Reset the editor to verify how suggestions behave when tabbing on leading indentation.
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.set_text("fn foo() {\n \n}", window, cx);
|
||||
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
||||
s.select_ranges([Point::new(1, 2)..Point::new(1, 2)])
|
||||
});
|
||||
});
|
||||
handle_copilot_completion_request(
|
||||
&copilot_lsp,
|
||||
vec![crate::request::Completion {
|
||||
text: " let x = 4;".into(),
|
||||
range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 2)),
|
||||
..Default::default()
|
||||
}],
|
||||
vec![],
|
||||
);
|
||||
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.next_edit_prediction(&Default::default(), window, cx)
|
||||
});
|
||||
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
assert!(editor.has_active_edit_prediction());
|
||||
assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}");
|
||||
assert_eq!(editor.text(cx), "fn foo() {\n \n}");
|
||||
|
||||
// Tabbing inside of leading whitespace inserts indentation without accepting the suggestion.
|
||||
editor.tab(&Default::default(), window, cx);
|
||||
assert!(editor.has_active_edit_prediction());
|
||||
assert_eq!(editor.text(cx), "fn foo() {\n \n}");
|
||||
assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}");
|
||||
|
||||
// Using AcceptEditPrediction again accepts the suggestion.
|
||||
editor.accept_edit_prediction(&Default::default(), window, cx);
|
||||
assert!(!editor.has_active_edit_prediction());
|
||||
assert_eq!(editor.text(cx), "fn foo() {\n let x = 4;\n}");
|
||||
assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}");
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
@@ -570,12 +453,15 @@ mod tests {
|
||||
));
|
||||
handle_copilot_completion_request(
|
||||
&copilot_lsp,
|
||||
vec![crate::request::Completion {
|
||||
vec![crate::request::NextEditSuggestion {
|
||||
text: "one.copilot1".into(),
|
||||
range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
|
||||
..Default::default()
|
||||
command: None,
|
||||
text_document: lsp::VersionedTextDocumentIdentifier {
|
||||
uri: Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(),
|
||||
version: 0,
|
||||
},
|
||||
}],
|
||||
vec![],
|
||||
);
|
||||
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
@@ -614,12 +500,15 @@ mod tests {
|
||||
));
|
||||
handle_copilot_completion_request(
|
||||
&copilot_lsp,
|
||||
vec![crate::request::Completion {
|
||||
vec![crate::request::NextEditSuggestion {
|
||||
text: "one.123. copilot\n 456".into(),
|
||||
range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
|
||||
..Default::default()
|
||||
command: None,
|
||||
text_document: lsp::VersionedTextDocumentIdentifier {
|
||||
uri: Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(),
|
||||
version: 0,
|
||||
},
|
||||
}],
|
||||
vec![],
|
||||
);
|
||||
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
@@ -686,15 +575,18 @@ mod tests {
|
||||
|
||||
handle_copilot_completion_request(
|
||||
&copilot_lsp,
|
||||
vec![crate::request::Completion {
|
||||
vec![crate::request::NextEditSuggestion {
|
||||
text: "two.foo()".into(),
|
||||
range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 2)),
|
||||
..Default::default()
|
||||
command: None,
|
||||
text_document: lsp::VersionedTextDocumentIdentifier {
|
||||
uri: Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(),
|
||||
version: 0,
|
||||
},
|
||||
}],
|
||||
vec![],
|
||||
);
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.next_edit_prediction(&Default::default(), window, cx)
|
||||
editor.show_edit_prediction(&Default::default(), window, cx)
|
||||
});
|
||||
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
@@ -703,15 +595,22 @@ mod tests {
|
||||
assert_eq!(editor.text(cx), "one\ntw\nthree\n");
|
||||
|
||||
editor.backspace(&Default::default(), window, cx);
|
||||
});
|
||||
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
||||
cx.run_until_parked();
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
assert!(editor.has_active_edit_prediction());
|
||||
assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
|
||||
assert_eq!(editor.text(cx), "one\nt\nthree\n");
|
||||
|
||||
editor.backspace(&Default::default(), window, cx);
|
||||
});
|
||||
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
||||
cx.run_until_parked();
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
assert!(editor.has_active_edit_prediction());
|
||||
assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
|
||||
assert_eq!(editor.text(cx), "one\n\nthree\n");
|
||||
|
||||
// Deleting across the original suggestion range invalidates it.
|
||||
editor.backspace(&Default::default(), window, cx);
|
||||
assert!(!editor.has_active_edit_prediction());
|
||||
@@ -765,19 +664,22 @@ mod tests {
|
||||
|
||||
handle_copilot_completion_request(
|
||||
&copilot_lsp,
|
||||
vec![crate::request::Completion {
|
||||
vec![crate::request::NextEditSuggestion {
|
||||
text: "b = 2 + a".into(),
|
||||
range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 5)),
|
||||
..Default::default()
|
||||
command: None,
|
||||
text_document: lsp::VersionedTextDocumentIdentifier {
|
||||
uri: Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(),
|
||||
version: 0,
|
||||
},
|
||||
}],
|
||||
vec![],
|
||||
);
|
||||
_ = editor.update(cx, |editor, window, cx| {
|
||||
// Ensure copilot suggestions are shown for the first excerpt.
|
||||
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
||||
s.select_ranges([Point::new(1, 5)..Point::new(1, 5)])
|
||||
});
|
||||
editor.next_edit_prediction(&Default::default(), window, cx);
|
||||
editor.show_edit_prediction(&Default::default(), window, cx);
|
||||
});
|
||||
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
||||
_ = editor.update(cx, |editor, _, cx| {
|
||||
@@ -791,12 +693,15 @@ mod tests {
|
||||
|
||||
handle_copilot_completion_request(
|
||||
&copilot_lsp,
|
||||
vec![crate::request::Completion {
|
||||
vec![crate::request::NextEditSuggestion {
|
||||
text: "d = 4 + c".into(),
|
||||
range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 6)),
|
||||
..Default::default()
|
||||
command: None,
|
||||
text_document: lsp::VersionedTextDocumentIdentifier {
|
||||
uri: Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(),
|
||||
version: 0,
|
||||
},
|
||||
}],
|
||||
vec![],
|
||||
);
|
||||
_ = editor.update(cx, |editor, window, cx| {
|
||||
// Move to another excerpt, ensuring the suggestion gets cleared.
|
||||
@@ -873,15 +778,18 @@ mod tests {
|
||||
));
|
||||
handle_copilot_completion_request(
|
||||
&copilot_lsp,
|
||||
vec![crate::request::Completion {
|
||||
vec![crate::request::NextEditSuggestion {
|
||||
text: "two.foo()".into(),
|
||||
range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 2)),
|
||||
..Default::default()
|
||||
command: None,
|
||||
text_document: lsp::VersionedTextDocumentIdentifier {
|
||||
uri: Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(),
|
||||
version: 0,
|
||||
},
|
||||
}],
|
||||
vec![],
|
||||
);
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.next_edit_prediction(&Default::default(), window, cx)
|
||||
editor.show_edit_prediction(&Default::default(), window, cx)
|
||||
});
|
||||
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
||||
cx.update_editor(|editor, _, cx| {
|
||||
@@ -903,12 +811,15 @@ mod tests {
|
||||
));
|
||||
handle_copilot_completion_request(
|
||||
&copilot_lsp,
|
||||
vec![crate::request::Completion {
|
||||
vec![crate::request::NextEditSuggestion {
|
||||
text: "two.foo()".into(),
|
||||
range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 3)),
|
||||
..Default::default()
|
||||
command: None,
|
||||
text_document: lsp::VersionedTextDocumentIdentifier {
|
||||
uri: Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(),
|
||||
version: 0,
|
||||
},
|
||||
}],
|
||||
vec![],
|
||||
);
|
||||
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
||||
cx.update_editor(|editor, _, cx| {
|
||||
@@ -930,12 +841,15 @@ mod tests {
|
||||
));
|
||||
handle_copilot_completion_request(
|
||||
&copilot_lsp,
|
||||
vec![crate::request::Completion {
|
||||
vec![crate::request::NextEditSuggestion {
|
||||
text: "two.foo()".into(),
|
||||
range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 4)),
|
||||
..Default::default()
|
||||
command: None,
|
||||
text_document: lsp::VersionedTextDocumentIdentifier {
|
||||
uri: Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(),
|
||||
version: 0,
|
||||
},
|
||||
}],
|
||||
vec![],
|
||||
);
|
||||
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
||||
cx.update_editor(|editor, _, cx| {
|
||||
@@ -1011,16 +925,20 @@ mod tests {
|
||||
.unwrap();
|
||||
|
||||
let mut copilot_requests = copilot_lsp
|
||||
.set_request_handler::<crate::request::GetCompletions, _, _>(
|
||||
.set_request_handler::<crate::request::NextEditSuggestions, _, _>(
|
||||
move |_params, _cx| async move {
|
||||
Ok(crate::request::GetCompletionsResult {
|
||||
completions: vec![crate::request::Completion {
|
||||
Ok(crate::request::NextEditSuggestionsResult {
|
||||
edits: vec![crate::request::NextEditSuggestion {
|
||||
text: "next line".into(),
|
||||
range: lsp::Range::new(
|
||||
lsp::Position::new(1, 0),
|
||||
lsp::Position::new(1, 0),
|
||||
),
|
||||
..Default::default()
|
||||
command: None,
|
||||
text_document: lsp::VersionedTextDocumentIdentifier {
|
||||
uri: Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(),
|
||||
version: 0,
|
||||
},
|
||||
}],
|
||||
})
|
||||
},
|
||||
@@ -1049,23 +967,14 @@ mod tests {
|
||||
|
||||
fn handle_copilot_completion_request(
|
||||
lsp: &lsp::FakeLanguageServer,
|
||||
completions: Vec<crate::request::Completion>,
|
||||
completions_cycling: Vec<crate::request::Completion>,
|
||||
completions: Vec<crate::request::NextEditSuggestion>,
|
||||
) {
|
||||
lsp.set_request_handler::<crate::request::GetCompletions, _, _>(move |_params, _cx| {
|
||||
let completions = completions.clone();
|
||||
async move {
|
||||
Ok(crate::request::GetCompletionsResult {
|
||||
completions: completions.clone(),
|
||||
})
|
||||
}
|
||||
});
|
||||
lsp.set_request_handler::<crate::request::GetCompletionsCycling, _, _>(
|
||||
lsp.set_request_handler::<crate::request::NextEditSuggestions, _, _>(
|
||||
move |_params, _cx| {
|
||||
let completions_cycling = completions_cycling.clone();
|
||||
let completions = completions.clone();
|
||||
async move {
|
||||
Ok(crate::request::GetCompletionsResult {
|
||||
completions: completions_cycling.clone(),
|
||||
Ok(crate::request::NextEditSuggestionsResult {
|
||||
edits: completions.clone(),
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use lsp::VersionedTextDocumentIdentifier;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub enum CheckStatus {}
|
||||
@@ -88,72 +89,6 @@ impl lsp::request::Request for SignOut {
|
||||
const METHOD: &'static str = "signOut";
|
||||
}
|
||||
|
||||
pub enum GetCompletions {}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct GetCompletionsParams {
|
||||
pub doc: GetCompletionsDocument,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct GetCompletionsDocument {
|
||||
pub tab_size: u32,
|
||||
pub indent_size: u32,
|
||||
pub insert_spaces: bool,
|
||||
pub uri: lsp::Uri,
|
||||
pub relative_path: String,
|
||||
pub position: lsp::Position,
|
||||
pub version: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct GetCompletionsResult {
|
||||
pub completions: Vec<Completion>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Completion {
|
||||
pub text: String,
|
||||
pub position: lsp::Position,
|
||||
pub uuid: String,
|
||||
pub range: lsp::Range,
|
||||
pub display_text: String,
|
||||
}
|
||||
|
||||
impl lsp::request::Request for GetCompletions {
|
||||
type Params = GetCompletionsParams;
|
||||
type Result = GetCompletionsResult;
|
||||
const METHOD: &'static str = "getCompletions";
|
||||
}
|
||||
|
||||
pub enum GetCompletionsCycling {}
|
||||
|
||||
impl lsp::request::Request for GetCompletionsCycling {
|
||||
type Params = GetCompletionsParams;
|
||||
type Result = GetCompletionsResult;
|
||||
const METHOD: &'static str = "getCompletionsCycling";
|
||||
}
|
||||
|
||||
pub enum LogMessage {}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct LogMessageParams {
|
||||
pub level: u8,
|
||||
pub message: String,
|
||||
pub metadata_str: String,
|
||||
pub extra: Vec<String>,
|
||||
}
|
||||
|
||||
impl lsp::notification::Notification for LogMessage {
|
||||
type Params = LogMessageParams;
|
||||
const METHOD: &'static str = "LogMessage";
|
||||
}
|
||||
|
||||
pub enum StatusNotification {}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
@@ -223,3 +158,36 @@ impl lsp::request::Request for NotifyRejected {
|
||||
type Result = String;
|
||||
const METHOD: &'static str = "notifyRejected";
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct NextEditSuggestions;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct NextEditSuggestionsParams {
|
||||
pub(crate) text_document: VersionedTextDocumentIdentifier,
|
||||
pub(crate) position: lsp::Position,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct NextEditSuggestion {
|
||||
pub text: String,
|
||||
pub text_document: VersionedTextDocumentIdentifier,
|
||||
pub range: lsp::Range,
|
||||
pub command: Option<lsp::Command>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct NextEditSuggestionsResult {
|
||||
pub edits: Vec<NextEditSuggestion>,
|
||||
}
|
||||
|
||||
impl lsp::request::Request for NextEditSuggestions {
|
||||
type Params = NextEditSuggestionsParams;
|
||||
type Result = NextEditSuggestionsResult;
|
||||
|
||||
const METHOD: &'static str = "textDocument/copilotInlineEdit";
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ use std::{cmp, sync::Arc};
|
||||
|
||||
use client::{Client, UserStore};
|
||||
use cloud_llm_client::EditPredictionRejectReason;
|
||||
use edit_prediction_types::{DataCollectionState, Direction, EditPredictionDelegate};
|
||||
use edit_prediction_types::{DataCollectionState, EditPredictionDelegate};
|
||||
use gpui::{App, Entity, prelude::*};
|
||||
use language::{Buffer, ToPoint as _};
|
||||
use project::Project;
|
||||
@@ -139,15 +139,6 @@ impl EditPredictionDelegate for ZedEditPredictionDelegate {
|
||||
});
|
||||
}
|
||||
|
||||
fn cycle(
|
||||
&mut self,
|
||||
_buffer: Entity<language::Buffer>,
|
||||
_cursor_position: language::Anchor,
|
||||
_direction: Direction,
|
||||
_cx: &mut Context<Self>,
|
||||
) {
|
||||
}
|
||||
|
||||
fn accept(&mut self, cx: &mut Context<Self>) {
|
||||
self.store.update(cx, |store, cx| {
|
||||
store.accept_current_prediction(&self.project, cx);
|
||||
|
||||
@@ -95,13 +95,6 @@ pub trait EditPredictionDelegate: 'static + Sized {
|
||||
debounce: bool,
|
||||
cx: &mut Context<Self>,
|
||||
);
|
||||
fn cycle(
|
||||
&mut self,
|
||||
buffer: Entity<Buffer>,
|
||||
cursor_position: language::Anchor,
|
||||
direction: Direction,
|
||||
cx: &mut Context<Self>,
|
||||
);
|
||||
fn accept(&mut self, cx: &mut Context<Self>);
|
||||
fn discard(&mut self, cx: &mut Context<Self>);
|
||||
fn did_show(&mut self, _cx: &mut Context<Self>) {}
|
||||
@@ -136,13 +129,6 @@ pub trait EditPredictionDelegateHandle {
|
||||
debounce: bool,
|
||||
cx: &mut App,
|
||||
);
|
||||
fn cycle(
|
||||
&self,
|
||||
buffer: Entity<Buffer>,
|
||||
cursor_position: language::Anchor,
|
||||
direction: Direction,
|
||||
cx: &mut App,
|
||||
);
|
||||
fn did_show(&self, cx: &mut App);
|
||||
fn accept(&self, cx: &mut App);
|
||||
fn discard(&self, cx: &mut App);
|
||||
@@ -215,18 +201,6 @@ where
|
||||
})
|
||||
}
|
||||
|
||||
fn cycle(
|
||||
&self,
|
||||
buffer: Entity<Buffer>,
|
||||
cursor_position: language::Anchor,
|
||||
direction: Direction,
|
||||
cx: &mut App,
|
||||
) {
|
||||
self.update(cx, |this, cx| {
|
||||
this.cycle(buffer, cursor_position, direction, cx)
|
||||
})
|
||||
}
|
||||
|
||||
fn accept(&self, cx: &mut App) {
|
||||
self.update(cx, |this, cx| this.accept(cx))
|
||||
}
|
||||
|
||||
@@ -485,15 +485,6 @@ impl EditPredictionDelegate for FakeEditPredictionDelegate {
|
||||
) {
|
||||
}
|
||||
|
||||
fn cycle(
|
||||
&mut self,
|
||||
_buffer: gpui::Entity<language::Buffer>,
|
||||
_cursor_position: language::Anchor,
|
||||
_direction: edit_prediction_types::Direction,
|
||||
_cx: &mut gpui::Context<Self>,
|
||||
) {
|
||||
}
|
||||
|
||||
fn accept(&mut self, _cx: &mut gpui::Context<Self>) {}
|
||||
|
||||
fn discard(&mut self, _cx: &mut gpui::Context<Self>) {}
|
||||
@@ -561,15 +552,6 @@ impl EditPredictionDelegate for FakeNonZedEditPredictionDelegate {
|
||||
) {
|
||||
}
|
||||
|
||||
fn cycle(
|
||||
&mut self,
|
||||
_buffer: gpui::Entity<language::Buffer>,
|
||||
_cursor_position: language::Anchor,
|
||||
_direction: edit_prediction_types::Direction,
|
||||
_cx: &mut gpui::Context<Self>,
|
||||
) {
|
||||
}
|
||||
|
||||
fn accept(&mut self, _cx: &mut gpui::Context<Self>) {}
|
||||
|
||||
fn discard(&mut self, _cx: &mut gpui::Context<Self>) {}
|
||||
|
||||
@@ -73,11 +73,7 @@ pub use multi_buffer::{
|
||||
pub use split::SplittableEditor;
|
||||
pub use text::Bias;
|
||||
|
||||
use ::git::{
|
||||
Restore,
|
||||
blame::{BlameEntry, ParsedCommitMessage},
|
||||
status::FileStatus,
|
||||
};
|
||||
use ::git::{Restore, blame::BlameEntry, commit::ParsedCommitMessage, status::FileStatus};
|
||||
use aho_corasick::{AhoCorasick, AhoCorasickBuilder, BuildError};
|
||||
use anyhow::{Context as _, Result, anyhow, bail};
|
||||
use blink_manager::BlinkManager;
|
||||
@@ -7468,26 +7464,6 @@ impl Editor {
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn cycle_edit_prediction(
|
||||
&mut self,
|
||||
direction: Direction,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<()> {
|
||||
let provider = self.edit_prediction_provider()?;
|
||||
let cursor = self.selections.newest_anchor().head();
|
||||
let (buffer, cursor_buffer_position) =
|
||||
self.buffer.read(cx).text_anchor_for_position(cursor, cx)?;
|
||||
if self.edit_predictions_hidden_for_vim_mode || !self.should_show_edit_predictions() {
|
||||
return None;
|
||||
}
|
||||
|
||||
provider.cycle(buffer, cursor_buffer_position, direction, cx);
|
||||
self.update_visible_edit_prediction(window, cx);
|
||||
|
||||
Some(())
|
||||
}
|
||||
|
||||
pub fn show_edit_prediction(
|
||||
&mut self,
|
||||
_: &ShowEditPrediction,
|
||||
@@ -7525,42 +7501,6 @@ impl Editor {
|
||||
.detach();
|
||||
}
|
||||
|
||||
pub fn next_edit_prediction(
|
||||
&mut self,
|
||||
_: &NextEditPrediction,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if self.has_active_edit_prediction() {
|
||||
self.cycle_edit_prediction(Direction::Next, window, cx);
|
||||
} else {
|
||||
let is_copilot_disabled = self
|
||||
.refresh_edit_prediction(false, true, window, cx)
|
||||
.is_none();
|
||||
if is_copilot_disabled {
|
||||
cx.propagate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn previous_edit_prediction(
|
||||
&mut self,
|
||||
_: &PreviousEditPrediction,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if self.has_active_edit_prediction() {
|
||||
self.cycle_edit_prediction(Direction::Prev, window, cx);
|
||||
} else {
|
||||
let is_copilot_disabled = self
|
||||
.refresh_edit_prediction(false, true, window, cx)
|
||||
.is_none();
|
||||
if is_copilot_disabled {
|
||||
cx.propagate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn accept_partial_edit_prediction(
|
||||
&mut self,
|
||||
granularity: EditPredictionGranularity,
|
||||
|
||||
@@ -69,7 +69,6 @@ use util::{
|
||||
use workspace::{
|
||||
CloseActiveItem, CloseAllItems, CloseOtherItems, MoveItemToPaneInDirection, NavigationEntry,
|
||||
OpenOptions, ViewId,
|
||||
invalid_item_view::InvalidItemView,
|
||||
item::{FollowEvent, FollowableItem, Item, ItemHandle, SaveOptions},
|
||||
register_project_item,
|
||||
};
|
||||
@@ -27667,11 +27666,10 @@ async fn test_non_utf_8_opens(cx: &mut TestAppContext) {
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
handle.to_any_view().entity_type(),
|
||||
TypeId::of::<InvalidItemView>()
|
||||
);
|
||||
// The test file content `vec![0xff, 0xfe, ...]` starts with a UTF-16 LE BOM.
|
||||
// Previously, this fell back to `InvalidItemView` because it wasn't valid UTF-8.
|
||||
// With auto-detection enabled, this is now recognized as UTF-16 and opens in the Editor.
|
||||
assert_eq!(handle.to_any_view().entity_type(), TypeId::of::<Editor>());
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
|
||||
@@ -37,11 +37,7 @@ use crate::{
|
||||
use buffer_diff::{DiffHunkStatus, DiffHunkStatusKind};
|
||||
use collections::{BTreeMap, HashMap};
|
||||
use file_icons::FileIcons;
|
||||
use git::{
|
||||
Oid,
|
||||
blame::{BlameEntry, ParsedCommitMessage},
|
||||
status::FileStatus,
|
||||
};
|
||||
use git::{Oid, blame::BlameEntry, commit::ParsedCommitMessage, status::FileStatus};
|
||||
use gpui::{
|
||||
Action, Along, AnyElement, App, AppContext, AvailableSpace, Axis as ScrollbarAxis, BorderStyle,
|
||||
Bounds, ClickEvent, ClipboardItem, ContentMask, Context, Corner, Corners, CursorStyle,
|
||||
@@ -594,8 +590,6 @@ impl EditorElement {
|
||||
register_action(editor, window, Editor::show_signature_help);
|
||||
register_action(editor, window, Editor::signature_help_prev);
|
||||
register_action(editor, window, Editor::signature_help_next);
|
||||
register_action(editor, window, Editor::next_edit_prediction);
|
||||
register_action(editor, window, Editor::previous_edit_prediction);
|
||||
register_action(editor, window, Editor::show_edit_prediction);
|
||||
register_action(editor, window, Editor::context_menu_first);
|
||||
register_action(editor, window, Editor::context_menu_prev);
|
||||
|
||||
@@ -3,9 +3,9 @@ use anyhow::{Context as _, Result};
|
||||
use collections::HashMap;
|
||||
|
||||
use git::{
|
||||
GitHostingProviderRegistry, GitRemote, Oid,
|
||||
blame::{Blame, BlameEntry, ParsedCommitMessage},
|
||||
parse_git_remote_url,
|
||||
GitHostingProviderRegistry, Oid,
|
||||
blame::{Blame, BlameEntry},
|
||||
commit::ParsedCommitMessage,
|
||||
};
|
||||
use gpui::{
|
||||
AnyElement, App, AppContext as _, Context, Entity, Hsla, ScrollHandle, Subscription, Task,
|
||||
@@ -525,12 +525,7 @@ impl GitBlame {
|
||||
.git_store()
|
||||
.read(cx)
|
||||
.repository_and_path_for_buffer_id(buffer.read(cx).remote_id(), cx)
|
||||
.and_then(|(repo, _)| {
|
||||
repo.read(cx)
|
||||
.remote_upstream_url
|
||||
.clone()
|
||||
.or(repo.read(cx).remote_origin_url.clone())
|
||||
});
|
||||
.and_then(|(repo, _)| repo.read(cx).default_remote_url());
|
||||
let blame_buffer = project
|
||||
.update(cx, |project, cx| project.blame_buffer(&buffer, None, cx));
|
||||
Ok(async move {
|
||||
@@ -554,13 +549,19 @@ impl GitBlame {
|
||||
entries,
|
||||
snapshot.max_point().row,
|
||||
);
|
||||
let commit_details = parse_commit_messages(
|
||||
messages,
|
||||
remote_url,
|
||||
provider_registry.clone(),
|
||||
)
|
||||
.await;
|
||||
|
||||
let commit_details = messages
|
||||
.into_iter()
|
||||
.map(|(oid, message)| {
|
||||
let parsed_commit_message =
|
||||
ParsedCommitMessage::parse(
|
||||
oid.to_string(),
|
||||
message,
|
||||
remote_url.as_deref(),
|
||||
Some(provider_registry.clone()),
|
||||
);
|
||||
(oid, parsed_commit_message)
|
||||
})
|
||||
.collect();
|
||||
res.push((
|
||||
id,
|
||||
snapshot,
|
||||
@@ -680,55 +681,6 @@ fn build_blame_entry_sum_tree(entries: Vec<BlameEntry>, max_row: u32) -> SumTree
|
||||
entries
|
||||
}
|
||||
|
||||
async fn parse_commit_messages(
|
||||
messages: impl IntoIterator<Item = (Oid, String)>,
|
||||
remote_url: Option<String>,
|
||||
provider_registry: Arc<GitHostingProviderRegistry>,
|
||||
) -> HashMap<Oid, ParsedCommitMessage> {
|
||||
let mut commit_details = HashMap::default();
|
||||
|
||||
let parsed_remote_url = remote_url
|
||||
.as_deref()
|
||||
.and_then(|remote_url| parse_git_remote_url(provider_registry, remote_url));
|
||||
|
||||
for (oid, message) in messages {
|
||||
let permalink = if let Some((provider, git_remote)) = parsed_remote_url.as_ref() {
|
||||
Some(provider.build_commit_permalink(
|
||||
git_remote,
|
||||
git::BuildCommitPermalinkParams {
|
||||
sha: oid.to_string().as_str(),
|
||||
},
|
||||
))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let remote = parsed_remote_url
|
||||
.as_ref()
|
||||
.map(|(provider, remote)| GitRemote {
|
||||
host: provider.clone(),
|
||||
owner: remote.owner.clone().into(),
|
||||
repo: remote.repo.clone().into(),
|
||||
});
|
||||
|
||||
let pull_request = parsed_remote_url
|
||||
.as_ref()
|
||||
.and_then(|(provider, remote)| provider.extract_pull_request(remote, &message));
|
||||
|
||||
commit_details.insert(
|
||||
oid,
|
||||
ParsedCommitMessage {
|
||||
message: message.into(),
|
||||
permalink,
|
||||
remote,
|
||||
pull_request,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
commit_details
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -434,7 +434,18 @@ impl RealFs {
|
||||
for component in path.components() {
|
||||
match component {
|
||||
std::path::Component::Prefix(_) => {
|
||||
let canonicalized = std::fs::canonicalize(component)?;
|
||||
let component = component.as_os_str();
|
||||
let canonicalized = if component
|
||||
.to_str()
|
||||
.map(|e| e.ends_with("\\"))
|
||||
.unwrap_or(false)
|
||||
{
|
||||
std::fs::canonicalize(component)
|
||||
} else {
|
||||
let mut component = component.to_os_string();
|
||||
component.push("\\");
|
||||
std::fs::canonicalize(component)
|
||||
}?;
|
||||
|
||||
let mut strip = PathBuf::new();
|
||||
for component in canonicalized.components() {
|
||||
@@ -3394,6 +3405,26 @@ mod tests {
|
||||
assert_eq!(content, "Hello");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
#[cfg(target_os = "windows")]
|
||||
async fn test_realfs_canonicalize(executor: BackgroundExecutor) {
|
||||
use util::paths::SanitizedPath;
|
||||
|
||||
let fs = RealFs {
|
||||
bundled_git_binary_path: None,
|
||||
executor,
|
||||
next_job_id: Arc::new(AtomicUsize::new(0)),
|
||||
job_event_subscribers: Arc::new(Mutex::new(Vec::new())),
|
||||
};
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let file = temp_dir.path().join("test (1).txt");
|
||||
let file = SanitizedPath::new(&file);
|
||||
std::fs::write(&file, "test").unwrap();
|
||||
|
||||
let canonicalized = fs.canonicalize(file.as_path()).await;
|
||||
assert!(canonicalized.is_ok());
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_rename(executor: BackgroundExecutor) {
|
||||
let fs = FakeFs::new(executor.clone());
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
use crate::Oid;
|
||||
use crate::commit::get_messages;
|
||||
use crate::repository::RepoPath;
|
||||
use crate::{GitRemote, Oid};
|
||||
use anyhow::{Context as _, Result};
|
||||
use collections::{HashMap, HashSet};
|
||||
use futures::AsyncWriteExt;
|
||||
use gpui::SharedString;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::process::Stdio;
|
||||
use std::{ops::Range, path::Path};
|
||||
@@ -21,14 +20,6 @@ pub struct Blame {
|
||||
pub messages: HashMap<Oid, String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct ParsedCommitMessage {
|
||||
pub message: SharedString,
|
||||
pub permalink: Option<url::Url>,
|
||||
pub pull_request: Option<crate::hosting_provider::PullRequest>,
|
||||
pub remote: Option<GitRemote>,
|
||||
}
|
||||
|
||||
impl Blame {
|
||||
pub async fn for_path(
|
||||
git_binary: &Path,
|
||||
|
||||
@@ -1,7 +1,52 @@
|
||||
use crate::{Oid, status::StatusCode};
|
||||
use crate::{
|
||||
BuildCommitPermalinkParams, GitHostingProviderRegistry, GitRemote, Oid, parse_git_remote_url,
|
||||
status::StatusCode,
|
||||
};
|
||||
use anyhow::{Context as _, Result};
|
||||
use collections::HashMap;
|
||||
use std::path::Path;
|
||||
use gpui::SharedString;
|
||||
use std::{path::Path, sync::Arc};
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct ParsedCommitMessage {
|
||||
pub message: SharedString,
|
||||
pub permalink: Option<url::Url>,
|
||||
pub pull_request: Option<crate::hosting_provider::PullRequest>,
|
||||
pub remote: Option<GitRemote>,
|
||||
}
|
||||
|
||||
impl ParsedCommitMessage {
|
||||
pub fn parse(
|
||||
sha: String,
|
||||
message: String,
|
||||
remote_url: Option<&str>,
|
||||
provider_registry: Option<Arc<GitHostingProviderRegistry>>,
|
||||
) -> Self {
|
||||
if let Some((hosting_provider, remote)) = provider_registry
|
||||
.and_then(|reg| remote_url.and_then(|url| parse_git_remote_url(reg, url)))
|
||||
{
|
||||
let pull_request = hosting_provider.extract_pull_request(&remote, &message);
|
||||
Self {
|
||||
message: message.into(),
|
||||
permalink: Some(
|
||||
hosting_provider
|
||||
.build_commit_permalink(&remote, BuildCommitPermalinkParams { sha: &sha }),
|
||||
),
|
||||
pull_request,
|
||||
remote: Some(GitRemote {
|
||||
host: hosting_provider,
|
||||
owner: remote.owner.into(),
|
||||
repo: remote.repo.into(),
|
||||
}),
|
||||
}
|
||||
} else {
|
||||
Self {
|
||||
message: message.into(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_messages(working_directory: &Path, shas: &[Oid]) -> Result<HashMap<Oid, String>> {
|
||||
if shas.is_empty() {
|
||||
|
||||
@@ -3,10 +3,7 @@ use crate::{
|
||||
commit_view::CommitView,
|
||||
};
|
||||
use editor::{BlameRenderer, Editor, hover_markdown_style};
|
||||
use git::{
|
||||
blame::{BlameEntry, ParsedCommitMessage},
|
||||
repository::CommitSummary,
|
||||
};
|
||||
use git::{blame::BlameEntry, commit::ParsedCommitMessage, repository::CommitSummary};
|
||||
use gpui::{
|
||||
ClipboardItem, Entity, Hsla, MouseButton, ScrollHandle, Subscription, TextStyle,
|
||||
TextStyleRefinement, UnderlineStyle, WeakEntity, prelude::*,
|
||||
|
||||
155
crates/git_ui/src/clone.rs
Normal file
155
crates/git_ui/src/clone.rs
Normal file
@@ -0,0 +1,155 @@
|
||||
use gpui::{App, Context, WeakEntity, Window};
|
||||
use notifications::status_toast::{StatusToast, ToastIcon};
|
||||
use std::sync::Arc;
|
||||
use ui::{Color, IconName, SharedString};
|
||||
use util::ResultExt;
|
||||
use workspace::{self, Workspace};
|
||||
|
||||
pub fn clone_and_open(
|
||||
repo_url: SharedString,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
on_success: Arc<
|
||||
dyn Fn(&mut Workspace, &mut Window, &mut Context<Workspace>) + Send + Sync + 'static,
|
||||
>,
|
||||
) {
|
||||
let destination_prompt = cx.prompt_for_paths(gpui::PathPromptOptions {
|
||||
files: false,
|
||||
directories: true,
|
||||
multiple: false,
|
||||
prompt: Some("Select as Repository Destination".into()),
|
||||
});
|
||||
|
||||
window
|
||||
.spawn(cx, async move |cx| {
|
||||
let mut paths = destination_prompt.await.ok()?.ok()??;
|
||||
let mut destination_dir = paths.pop()?;
|
||||
|
||||
let repo_name = repo_url
|
||||
.split('/')
|
||||
.next_back()
|
||||
.map(|name| name.strip_suffix(".git").unwrap_or(name))
|
||||
.unwrap_or("repository")
|
||||
.to_owned();
|
||||
|
||||
let clone_task = workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
let fs = workspace.app_state().fs.clone();
|
||||
let destination_dir = destination_dir.clone();
|
||||
let repo_url = repo_url.clone();
|
||||
cx.spawn(async move |_workspace, _cx| {
|
||||
fs.git_clone(&repo_url, destination_dir.as_path()).await
|
||||
})
|
||||
})
|
||||
.ok()?;
|
||||
|
||||
if let Err(error) = clone_task.await {
|
||||
workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
let toast = StatusToast::new(error.to_string(), cx, |this, _| {
|
||||
this.icon(ToastIcon::new(IconName::XCircle).color(Color::Error))
|
||||
.dismiss_button(true)
|
||||
});
|
||||
workspace.toggle_status_toast(toast, cx);
|
||||
})
|
||||
.log_err();
|
||||
return None;
|
||||
}
|
||||
|
||||
let has_worktrees = workspace
|
||||
.read_with(cx, |workspace, cx| {
|
||||
workspace.project().read(cx).worktrees(cx).next().is_some()
|
||||
})
|
||||
.ok()?;
|
||||
|
||||
let prompt_answer = if has_worktrees {
|
||||
cx.update(|window, cx| {
|
||||
window.prompt(
|
||||
gpui::PromptLevel::Info,
|
||||
&format!("Git Clone: {}", repo_name),
|
||||
None,
|
||||
&["Add repo to project", "Open repo in new project"],
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.ok()?
|
||||
.await
|
||||
.ok()?
|
||||
} else {
|
||||
// Don't ask if project is empty
|
||||
0
|
||||
};
|
||||
|
||||
destination_dir.push(&repo_name);
|
||||
|
||||
match prompt_answer {
|
||||
0 => {
|
||||
workspace
|
||||
.update_in(cx, |workspace, window, cx| {
|
||||
let create_task = workspace.project().update(cx, |project, cx| {
|
||||
project.create_worktree(destination_dir.as_path(), true, cx)
|
||||
});
|
||||
|
||||
let workspace_weak = cx.weak_entity();
|
||||
let on_success = on_success.clone();
|
||||
cx.spawn_in(window, async move |_window, cx| {
|
||||
if create_task.await.log_err().is_some() {
|
||||
workspace_weak
|
||||
.update_in(cx, |workspace, window, cx| {
|
||||
(on_success)(workspace, window, cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
})
|
||||
.ok()?;
|
||||
}
|
||||
1 => {
|
||||
workspace
|
||||
.update(cx, move |workspace, cx| {
|
||||
let app_state = workspace.app_state().clone();
|
||||
let destination_path = destination_dir.clone();
|
||||
let on_success = on_success.clone();
|
||||
|
||||
workspace::open_new(
|
||||
Default::default(),
|
||||
app_state,
|
||||
cx,
|
||||
move |workspace, window, cx| {
|
||||
cx.activate(true);
|
||||
|
||||
let create_task =
|
||||
workspace.project().update(cx, |project, cx| {
|
||||
project.create_worktree(
|
||||
destination_path.as_path(),
|
||||
true,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let workspace_weak = cx.weak_entity();
|
||||
cx.spawn_in(window, async move |_window, cx| {
|
||||
if create_task.await.log_err().is_some() {
|
||||
workspace_weak
|
||||
.update_in(cx, |workspace, window, cx| {
|
||||
(on_success)(workspace, window, cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
},
|
||||
)
|
||||
.detach();
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
Some(())
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
@@ -3,7 +3,7 @@ use editor::hover_markdown_style;
|
||||
use futures::Future;
|
||||
use git::blame::BlameEntry;
|
||||
use git::repository::CommitSummary;
|
||||
use git::{GitRemote, blame::ParsedCommitMessage};
|
||||
use git::{GitRemote, commit::ParsedCommitMessage};
|
||||
use gpui::{
|
||||
App, Asset, ClipboardItem, Element, Entity, MouseButton, ParentElement, Render, ScrollHandle,
|
||||
StatefulInteractiveElement, WeakEntity, prelude::*,
|
||||
|
||||
@@ -15,12 +15,13 @@ use askpass::AskPassDelegate;
|
||||
use cloud_llm_client::CompletionIntent;
|
||||
use collections::{BTreeMap, HashMap, HashSet};
|
||||
use db::kvp::KEY_VALUE_STORE;
|
||||
use editor::RewrapOptions;
|
||||
use editor::{
|
||||
Direction, Editor, EditorElement, EditorMode, MultiBuffer, MultiBufferOffset,
|
||||
actions::ExpandAllDiffHunks,
|
||||
};
|
||||
use futures::StreamExt as _;
|
||||
use git::blame::ParsedCommitMessage;
|
||||
use git::commit::ParsedCommitMessage;
|
||||
use git::repository::{
|
||||
Branch, CommitDetails, CommitOptions, CommitSummary, DiffType, FetchOptions, GitCommitter,
|
||||
PushOptions, Remote, RemoteCommandOutput, ResetMode, Upstream, UpstreamTracking,
|
||||
@@ -30,15 +31,14 @@ use git::stash::GitStash;
|
||||
use git::status::StageStatus;
|
||||
use git::{Amend, Signoff, ToggleStaged, repository::RepoPath, status::FileStatus};
|
||||
use git::{
|
||||
ExpandCommitEditor, RestoreTrackedFiles, StageAll, StashAll, StashApply, StashPop,
|
||||
TrashUntrackedFiles, UnstageAll,
|
||||
ExpandCommitEditor, GitHostingProviderRegistry, RestoreTrackedFiles, StageAll, StashAll,
|
||||
StashApply, StashPop, TrashUntrackedFiles, UnstageAll,
|
||||
};
|
||||
use gpui::{
|
||||
Action, AsyncApp, AsyncWindowContext, Bounds, ClickEvent, Corner, DismissEvent, Entity,
|
||||
EventEmitter, FocusHandle, Focusable, KeyContext, ListHorizontalSizingBehavior,
|
||||
ListSizingBehavior, MouseButton, MouseDownEvent, Point, PromptLevel, ScrollStrategy,
|
||||
Subscription, Task, UniformListScrollHandle, WeakEntity, actions, anchored, deferred, point,
|
||||
size, uniform_list,
|
||||
EventEmitter, FocusHandle, Focusable, KeyContext, MouseButton, MouseDownEvent, Point,
|
||||
PromptLevel, ScrollStrategy, Subscription, Task, UniformListScrollHandle, WeakEntity, actions,
|
||||
anchored, deferred, point, size, uniform_list,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use language::{Buffer, File};
|
||||
@@ -46,7 +46,7 @@ use language_model::{
|
||||
ConfiguredModel, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage,
|
||||
Role, ZED_CLOUD_PROVIDER_ID,
|
||||
};
|
||||
use menu::{Confirm, SecondaryConfirm, SelectFirst, SelectLast, SelectNext, SelectPrevious};
|
||||
use menu;
|
||||
use multi_buffer::ExcerptInfo;
|
||||
use notifications::status_toast::{StatusToast, ToastIcon};
|
||||
use panel::{
|
||||
@@ -93,6 +93,14 @@ actions!(
|
||||
FocusEditor,
|
||||
/// Focuses on the changes list.
|
||||
FocusChanges,
|
||||
/// Select next git panel menu item, and show it in the diff view
|
||||
NextEntry,
|
||||
/// Select previous git panel menu item, and show it in the diff view
|
||||
PreviousEntry,
|
||||
/// Select first git panel menu item, and show it in the diff view
|
||||
FirstEntry,
|
||||
/// Select last git panel menu item, and show it in the diff view
|
||||
LastEntry,
|
||||
/// Toggles automatic co-author suggestions.
|
||||
ToggleFillCoAuthors,
|
||||
/// Toggles sorting entries by path vs status.
|
||||
@@ -204,8 +212,7 @@ const GIT_PANEL_KEY: &str = "GitPanel";
|
||||
|
||||
const UPDATE_DEBOUNCE: Duration = Duration::from_millis(50);
|
||||
// TODO: We should revise this part. It seems the indentation width is not aligned with the one in project panel
|
||||
const TREE_INDENT: f32 = 12.0;
|
||||
const TREE_INDENT_GUIDE_OFFSET: f32 = 16.0;
|
||||
const TREE_INDENT: f32 = 16.0;
|
||||
|
||||
pub fn register(workspace: &mut Workspace) {
|
||||
workspace.register_action(|workspace, _: &ToggleFocus, window, cx| {
|
||||
@@ -793,20 +800,63 @@ impl GitPanel {
|
||||
pub fn select_entry_by_path(
|
||||
&mut self,
|
||||
path: ProjectPath,
|
||||
_: &mut Window,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let Some(git_repo) = self.active_repository.as_ref() else {
|
||||
return;
|
||||
};
|
||||
let Some(repo_path) = git_repo.read(cx).project_path_to_repo_path(&path, cx) else {
|
||||
return;
|
||||
|
||||
let (repo_path, section) = {
|
||||
let repo = git_repo.read(cx);
|
||||
let Some(repo_path) = repo.project_path_to_repo_path(&path, cx) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let section = repo
|
||||
.status_for_path(&repo_path)
|
||||
.map(|status| status.status)
|
||||
.map(|status| {
|
||||
if repo.had_conflict_on_last_merge_head_change(&repo_path) {
|
||||
Section::Conflict
|
||||
} else if status.is_created() {
|
||||
Section::New
|
||||
} else {
|
||||
Section::Tracked
|
||||
}
|
||||
});
|
||||
|
||||
(repo_path, section)
|
||||
};
|
||||
|
||||
let mut needs_rebuild = false;
|
||||
if let (Some(section), Some(tree_state)) = (section, self.view_mode.tree_state_mut()) {
|
||||
let mut current_dir = repo_path.parent();
|
||||
while let Some(dir) = current_dir {
|
||||
let key = TreeKey {
|
||||
section,
|
||||
path: RepoPath::from_rel_path(dir),
|
||||
};
|
||||
|
||||
if tree_state.expanded_dirs.get(&key) == Some(&false) {
|
||||
tree_state.expanded_dirs.insert(key, true);
|
||||
needs_rebuild = true;
|
||||
}
|
||||
|
||||
current_dir = dir.parent();
|
||||
}
|
||||
}
|
||||
|
||||
if needs_rebuild {
|
||||
self.update_visible_entries(window, cx);
|
||||
}
|
||||
|
||||
let Some(ix) = self.entry_by_path(&repo_path) else {
|
||||
return;
|
||||
};
|
||||
|
||||
self.selected_entry = Some(ix);
|
||||
cx.notify();
|
||||
self.scroll_to_selected_entry(cx);
|
||||
}
|
||||
|
||||
fn serialization_key(workspace: &Workspace) -> Option<String> {
|
||||
@@ -894,9 +944,22 @@ impl GitPanel {
|
||||
}
|
||||
|
||||
fn scroll_to_selected_entry(&mut self, cx: &mut Context<Self>) {
|
||||
if let Some(selected_entry) = self.selected_entry {
|
||||
let Some(selected_entry) = self.selected_entry else {
|
||||
cx.notify();
|
||||
return;
|
||||
};
|
||||
|
||||
let visible_index = match &self.view_mode {
|
||||
GitPanelViewMode::Flat => Some(selected_entry),
|
||||
GitPanelViewMode::Tree(state) => state
|
||||
.logical_indices
|
||||
.iter()
|
||||
.position(|&ix| ix == selected_entry),
|
||||
};
|
||||
|
||||
if let Some(visible_index) = visible_index {
|
||||
self.scroll_handle
|
||||
.scroll_to_item(selected_entry, ScrollStrategy::Center);
|
||||
.scroll_to_item(visible_index, ScrollStrategy::Center);
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
@@ -914,12 +977,12 @@ impl GitPanel {
|
||||
|
||||
if let GitListEntry::Directory(dir_entry) = entry {
|
||||
if dir_entry.expanded {
|
||||
self.select_next(&SelectNext, window, cx);
|
||||
self.select_next(&menu::SelectNext, window, cx);
|
||||
} else {
|
||||
self.toggle_directory(&dir_entry.key, window, cx);
|
||||
}
|
||||
} else {
|
||||
self.select_next(&SelectNext, window, cx);
|
||||
self.select_next(&menu::SelectNext, window, cx);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -937,14 +1000,19 @@ impl GitPanel {
|
||||
if dir_entry.expanded {
|
||||
self.toggle_directory(&dir_entry.key, window, cx);
|
||||
} else {
|
||||
self.select_previous(&SelectPrevious, window, cx);
|
||||
self.select_previous(&menu::SelectPrevious, window, cx);
|
||||
}
|
||||
} else {
|
||||
self.select_previous(&SelectPrevious, window, cx);
|
||||
self.select_previous(&menu::SelectPrevious, window, cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn select_first(&mut self, _: &SelectFirst, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
fn select_first(
|
||||
&mut self,
|
||||
_: &menu::SelectFirst,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let first_entry = match &self.view_mode {
|
||||
GitPanelViewMode::Flat => self
|
||||
.entries
|
||||
@@ -967,7 +1035,7 @@ impl GitPanel {
|
||||
|
||||
fn select_previous(
|
||||
&mut self,
|
||||
_: &SelectPrevious,
|
||||
_: &menu::SelectPrevious,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
@@ -1016,7 +1084,7 @@ impl GitPanel {
|
||||
self.scroll_to_selected_entry(cx);
|
||||
}
|
||||
|
||||
fn select_next(&mut self, _: &SelectNext, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
fn select_next(&mut self, _: &menu::SelectNext, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
let item_count = self.entries.len();
|
||||
if item_count == 0 {
|
||||
return;
|
||||
@@ -1054,13 +1122,50 @@ impl GitPanel {
|
||||
self.scroll_to_selected_entry(cx);
|
||||
}
|
||||
|
||||
fn select_last(&mut self, _: &SelectLast, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
if self.entries.last().is_some() {
|
||||
self.selected_entry = Some(self.entries.len() - 1);
|
||||
self.scroll_to_selected_entry(cx);
|
||||
}
|
||||
}
|
||||
|
||||
/// Show diff view at selected entry, only if the diff view is open
|
||||
fn move_diff_to_entry(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
maybe!({
|
||||
let workspace = self.workspace.upgrade()?;
|
||||
|
||||
if let Some(project_diff) = workspace.read(cx).item_of_type::<ProjectDiff>(cx) {
|
||||
let entry = self.entries.get(self.selected_entry?)?.status_entry()?;
|
||||
|
||||
project_diff.update(cx, |project_diff, cx| {
|
||||
project_diff.move_to_entry(entry.clone(), window, cx);
|
||||
});
|
||||
}
|
||||
|
||||
Some(())
|
||||
});
|
||||
}
|
||||
|
||||
fn first_entry(&mut self, _: &FirstEntry, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.select_first(&menu::SelectFirst, window, cx);
|
||||
self.move_diff_to_entry(window, cx);
|
||||
}
|
||||
|
||||
fn last_entry(&mut self, _: &LastEntry, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.select_last(&menu::SelectLast, window, cx);
|
||||
self.move_diff_to_entry(window, cx);
|
||||
}
|
||||
|
||||
fn next_entry(&mut self, _: &NextEntry, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.select_next(&menu::SelectNext, window, cx);
|
||||
self.move_diff_to_entry(window, cx);
|
||||
}
|
||||
|
||||
fn previous_entry(&mut self, _: &PreviousEntry, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.select_previous(&menu::SelectPrevious, window, cx);
|
||||
self.move_diff_to_entry(window, cx);
|
||||
}
|
||||
|
||||
fn focus_editor(&mut self, _: &FocusEditor, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.commit_editor.update(cx, |editor, cx| {
|
||||
window.focus(&editor.focus_handle(cx), cx);
|
||||
@@ -1074,7 +1179,7 @@ impl GitPanel {
|
||||
.as_ref()
|
||||
.is_some_and(|active_repository| active_repository.read(cx).status_summary().count > 0);
|
||||
if have_entries && self.selected_entry.is_none() {
|
||||
self.select_first(&SelectFirst, window, cx);
|
||||
self.select_first(&menu::SelectFirst, window, cx);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2076,7 +2181,13 @@ impl GitPanel {
|
||||
let editor = cx.new(|cx| Editor::for_buffer(buffer, None, window, cx));
|
||||
let wrapped_message = editor.update(cx, |editor, cx| {
|
||||
editor.select_all(&Default::default(), window, cx);
|
||||
editor.rewrap(&Default::default(), window, cx);
|
||||
editor.rewrap_impl(
|
||||
RewrapOptions {
|
||||
override_language_settings: false,
|
||||
preserve_existing_whitespace: true,
|
||||
},
|
||||
cx,
|
||||
);
|
||||
editor.text(cx)
|
||||
});
|
||||
if wrapped_message.trim().is_empty() {
|
||||
@@ -2737,93 +2848,15 @@ impl GitPanel {
|
||||
}
|
||||
|
||||
pub(crate) fn git_clone(&mut self, repo: String, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let path = cx.prompt_for_paths(gpui::PathPromptOptions {
|
||||
files: false,
|
||||
directories: true,
|
||||
multiple: false,
|
||||
prompt: Some("Select as Repository Destination".into()),
|
||||
});
|
||||
|
||||
let workspace = self.workspace.clone();
|
||||
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let mut paths = path.await.ok()?.ok()??;
|
||||
let mut path = paths.pop()?;
|
||||
let repo_name = repo.split("/").last()?.strip_suffix(".git")?.to_owned();
|
||||
|
||||
let fs = this.read_with(cx, |this, _| this.fs.clone()).ok()?;
|
||||
|
||||
let prompt_answer = match fs.git_clone(&repo, path.as_path()).await {
|
||||
Ok(_) => cx.update(|window, cx| {
|
||||
window.prompt(
|
||||
PromptLevel::Info,
|
||||
&format!("Git Clone: {}", repo_name),
|
||||
None,
|
||||
&["Add repo to project", "Open repo in new project"],
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
Err(e) => {
|
||||
this.update(cx, |this: &mut GitPanel, cx| {
|
||||
let toast = StatusToast::new(e.to_string(), cx, |this, _| {
|
||||
this.icon(ToastIcon::new(IconName::XCircle).color(Color::Error))
|
||||
.dismiss_button(true)
|
||||
});
|
||||
|
||||
this.workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace.toggle_status_toast(toast, cx);
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.ok()?;
|
||||
|
||||
return None;
|
||||
}
|
||||
}
|
||||
.ok()?;
|
||||
|
||||
path.push(repo_name);
|
||||
match prompt_answer.await.ok()? {
|
||||
0 => {
|
||||
workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace
|
||||
.project()
|
||||
.update(cx, |project, cx| {
|
||||
project.create_worktree(path.as_path(), true, cx)
|
||||
})
|
||||
.detach();
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
1 => {
|
||||
workspace
|
||||
.update(cx, move |workspace, cx| {
|
||||
workspace::open_new(
|
||||
Default::default(),
|
||||
workspace.app_state().clone(),
|
||||
cx,
|
||||
move |workspace, _, cx| {
|
||||
cx.activate(true);
|
||||
workspace
|
||||
.project()
|
||||
.update(cx, |project, cx| {
|
||||
project.create_worktree(&path, true, cx)
|
||||
})
|
||||
.detach();
|
||||
},
|
||||
)
|
||||
.detach();
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
Some(())
|
||||
})
|
||||
.detach();
|
||||
crate::clone::clone_and_open(
|
||||
repo.into(),
|
||||
workspace,
|
||||
window,
|
||||
cx,
|
||||
Arc::new(|_workspace: &mut workspace::Workspace, _window, _cx| {}),
|
||||
);
|
||||
}
|
||||
|
||||
pub(crate) fn git_init(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
@@ -4591,7 +4624,10 @@ impl GitPanel {
|
||||
},
|
||||
)
|
||||
.with_render_fn(cx.entity(), |_, params, _, _| {
|
||||
let left_offset = px(TREE_INDENT_GUIDE_OFFSET);
|
||||
// Magic number to align the tree item is 3 here
|
||||
// because we're using 12px as the left-side padding
|
||||
// and 3 makes the alignment work with the bounding box of the icon
|
||||
let left_offset = px(TREE_INDENT + 3_f32);
|
||||
let indent_size = params.indent_size;
|
||||
let item_height = params.item_height;
|
||||
|
||||
@@ -4619,10 +4655,6 @@ impl GitPanel {
|
||||
})
|
||||
.size_full()
|
||||
.flex_grow()
|
||||
.with_sizing_behavior(ListSizingBehavior::Auto)
|
||||
.with_horizontal_sizing_behavior(
|
||||
ListHorizontalSizingBehavior::Unconstrained,
|
||||
)
|
||||
.with_width_from_item(self.max_width_item_index)
|
||||
.track_scroll(&self.scroll_handle),
|
||||
)
|
||||
@@ -4646,7 +4678,7 @@ impl GitPanel {
|
||||
}
|
||||
|
||||
fn entry_label(&self, label: impl Into<SharedString>, color: Color) -> Label {
|
||||
Label::new(label.into()).color(color).single_line()
|
||||
Label::new(label.into()).color(color)
|
||||
}
|
||||
|
||||
fn list_item_height(&self) -> Rems {
|
||||
@@ -4668,8 +4700,8 @@ impl GitPanel {
|
||||
.h(self.list_item_height())
|
||||
.w_full()
|
||||
.items_end()
|
||||
.px(rems(0.75)) // ~12px
|
||||
.pb(rems(0.3125)) // ~ 5px
|
||||
.px_3()
|
||||
.pb_1()
|
||||
.child(
|
||||
Label::new(header.title())
|
||||
.color(Color::Muted)
|
||||
@@ -4726,8 +4758,8 @@ impl GitPanel {
|
||||
git::AddToGitignore.boxed_clone(),
|
||||
)
|
||||
.separator()
|
||||
.action("Open Diff", Confirm.boxed_clone())
|
||||
.action("Open File", SecondaryConfirm.boxed_clone())
|
||||
.action("Open Diff", menu::Confirm.boxed_clone())
|
||||
.action("Open File", menu::SecondaryConfirm.boxed_clone())
|
||||
.separator()
|
||||
.action_disabled_when(is_created, "View File History", Box::new(git::FileHistory))
|
||||
});
|
||||
@@ -4857,113 +4889,68 @@ impl GitPanel {
|
||||
let marked_bg_alpha = 0.12;
|
||||
let state_opacity_step = 0.04;
|
||||
|
||||
let info_color = cx.theme().status().info;
|
||||
|
||||
let base_bg = match (selected, marked) {
|
||||
(true, true) => cx
|
||||
.theme()
|
||||
.status()
|
||||
.info
|
||||
.alpha(selected_bg_alpha + marked_bg_alpha),
|
||||
(true, false) => cx.theme().status().info.alpha(selected_bg_alpha),
|
||||
(false, true) => cx.theme().status().info.alpha(marked_bg_alpha),
|
||||
(true, true) => info_color.alpha(selected_bg_alpha + marked_bg_alpha),
|
||||
(true, false) => info_color.alpha(selected_bg_alpha),
|
||||
(false, true) => info_color.alpha(marked_bg_alpha),
|
||||
_ => cx.theme().colors().ghost_element_background,
|
||||
};
|
||||
|
||||
let hover_bg = if selected {
|
||||
cx.theme()
|
||||
.status()
|
||||
.info
|
||||
.alpha(selected_bg_alpha + state_opacity_step)
|
||||
} else {
|
||||
cx.theme().colors().ghost_element_hover
|
||||
};
|
||||
|
||||
let active_bg = if selected {
|
||||
cx.theme()
|
||||
.status()
|
||||
.info
|
||||
.alpha(selected_bg_alpha + state_opacity_step * 2.0)
|
||||
} else {
|
||||
cx.theme().colors().ghost_element_active
|
||||
};
|
||||
|
||||
let mut name_row = h_flex()
|
||||
.items_center()
|
||||
.gap_1()
|
||||
.flex_1()
|
||||
.pl(if tree_view {
|
||||
px(depth as f32 * TREE_INDENT)
|
||||
} else {
|
||||
px(0.)
|
||||
})
|
||||
.child(git_status_icon(status));
|
||||
|
||||
name_row = if tree_view {
|
||||
name_row.child(
|
||||
self.entry_label(display_name, label_color)
|
||||
.when(status.is_deleted(), Label::strikethrough)
|
||||
.truncate(),
|
||||
let (hover_bg, active_bg) = if selected {
|
||||
(
|
||||
info_color.alpha(selected_bg_alpha + state_opacity_step),
|
||||
info_color.alpha(selected_bg_alpha + state_opacity_step * 2.0),
|
||||
)
|
||||
} else {
|
||||
name_row.child(h_flex().items_center().flex_1().map(|this| {
|
||||
self.path_formatted(
|
||||
this,
|
||||
entry.parent_dir(path_style),
|
||||
path_color,
|
||||
display_name,
|
||||
label_color,
|
||||
path_style,
|
||||
git_path_style,
|
||||
status.is_deleted(),
|
||||
)
|
||||
}))
|
||||
(
|
||||
cx.theme().colors().ghost_element_hover,
|
||||
cx.theme().colors().ghost_element_active,
|
||||
)
|
||||
};
|
||||
|
||||
let name_row = h_flex()
|
||||
.min_w_0()
|
||||
.flex_1()
|
||||
.gap_1()
|
||||
.child(git_status_icon(status))
|
||||
.map(|this| {
|
||||
if tree_view {
|
||||
this.pl(px(depth as f32 * TREE_INDENT)).child(
|
||||
self.entry_label(display_name, label_color)
|
||||
.when(status.is_deleted(), Label::strikethrough)
|
||||
.truncate(),
|
||||
)
|
||||
} else {
|
||||
this.child(self.path_formatted(
|
||||
entry.parent_dir(path_style),
|
||||
path_color,
|
||||
display_name,
|
||||
label_color,
|
||||
path_style,
|
||||
git_path_style,
|
||||
status.is_deleted(),
|
||||
))
|
||||
}
|
||||
});
|
||||
|
||||
h_flex()
|
||||
.id(id)
|
||||
.h(self.list_item_height())
|
||||
.w_full()
|
||||
.pl_3()
|
||||
.pr_1()
|
||||
.gap_1p5()
|
||||
.border_1()
|
||||
.border_r_2()
|
||||
.when(selected && self.focus_handle.is_focused(window), |el| {
|
||||
el.border_color(cx.theme().colors().panel_focused_border)
|
||||
})
|
||||
.px(rems(0.75)) // ~12px
|
||||
.overflow_hidden()
|
||||
.flex_none()
|
||||
.gap_1p5()
|
||||
.bg(base_bg)
|
||||
.hover(|this| this.bg(hover_bg))
|
||||
.active(|this| this.bg(active_bg))
|
||||
.on_click({
|
||||
cx.listener(move |this, event: &ClickEvent, window, cx| {
|
||||
this.selected_entry = Some(ix);
|
||||
cx.notify();
|
||||
if event.modifiers().secondary() {
|
||||
this.open_file(&Default::default(), window, cx)
|
||||
} else {
|
||||
this.open_diff(&Default::default(), window, cx);
|
||||
this.focus_handle.focus(window, cx);
|
||||
}
|
||||
})
|
||||
})
|
||||
.on_mouse_down(
|
||||
MouseButton::Right,
|
||||
move |event: &MouseDownEvent, window, cx| {
|
||||
// why isn't this happening automatically? we are passing MouseButton::Right to `on_mouse_down`?
|
||||
if event.button != MouseButton::Right {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(this) = handle.upgrade() else {
|
||||
return;
|
||||
};
|
||||
this.update(cx, |this, cx| {
|
||||
this.deploy_entry_context_menu(event.position, ix, window, cx);
|
||||
});
|
||||
cx.stop_propagation();
|
||||
},
|
||||
)
|
||||
.child(name_row.overflow_x_hidden())
|
||||
.hover(|s| s.bg(hover_bg))
|
||||
.active(|s| s.bg(active_bg))
|
||||
.child(name_row)
|
||||
.child(
|
||||
div()
|
||||
.id(checkbox_wrapper_id)
|
||||
@@ -5013,6 +5000,35 @@ impl GitPanel {
|
||||
}),
|
||||
),
|
||||
)
|
||||
.on_click({
|
||||
cx.listener(move |this, event: &ClickEvent, window, cx| {
|
||||
this.selected_entry = Some(ix);
|
||||
cx.notify();
|
||||
if event.modifiers().secondary() {
|
||||
this.open_file(&Default::default(), window, cx)
|
||||
} else {
|
||||
this.open_diff(&Default::default(), window, cx);
|
||||
this.focus_handle.focus(window, cx);
|
||||
}
|
||||
})
|
||||
})
|
||||
.on_mouse_down(
|
||||
MouseButton::Right,
|
||||
move |event: &MouseDownEvent, window, cx| {
|
||||
// why isn't this happening automatically? we are passing MouseButton::Right to `on_mouse_down`?
|
||||
if event.button != MouseButton::Right {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(this) = handle.upgrade() else {
|
||||
return;
|
||||
};
|
||||
this.update(cx, |this, cx| {
|
||||
this.deploy_entry_context_menu(event.position, ix, window, cx);
|
||||
});
|
||||
cx.stop_propagation();
|
||||
},
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
@@ -5037,29 +5053,23 @@ impl GitPanel {
|
||||
let selected_bg_alpha = 0.08;
|
||||
let state_opacity_step = 0.04;
|
||||
|
||||
let base_bg = if selected {
|
||||
cx.theme().status().info.alpha(selected_bg_alpha)
|
||||
let info_color = cx.theme().status().info;
|
||||
let colors = cx.theme().colors();
|
||||
|
||||
let (base_bg, hover_bg, active_bg) = if selected {
|
||||
(
|
||||
info_color.alpha(selected_bg_alpha),
|
||||
info_color.alpha(selected_bg_alpha + state_opacity_step),
|
||||
info_color.alpha(selected_bg_alpha + state_opacity_step * 2.0),
|
||||
)
|
||||
} else {
|
||||
cx.theme().colors().ghost_element_background
|
||||
(
|
||||
colors.ghost_element_background,
|
||||
colors.ghost_element_hover,
|
||||
colors.ghost_element_active,
|
||||
)
|
||||
};
|
||||
|
||||
let hover_bg = if selected {
|
||||
cx.theme()
|
||||
.status()
|
||||
.info
|
||||
.alpha(selected_bg_alpha + state_opacity_step)
|
||||
} else {
|
||||
cx.theme().colors().ghost_element_hover
|
||||
};
|
||||
|
||||
let active_bg = if selected {
|
||||
cx.theme()
|
||||
.status()
|
||||
.info
|
||||
.alpha(selected_bg_alpha + state_opacity_step * 2.0)
|
||||
} else {
|
||||
cx.theme().colors().ghost_element_active
|
||||
};
|
||||
let folder_icon = if entry.expanded {
|
||||
IconName::FolderOpen
|
||||
} else {
|
||||
@@ -5082,9 +5092,8 @@ impl GitPanel {
|
||||
};
|
||||
|
||||
let name_row = h_flex()
|
||||
.items_center()
|
||||
.min_w_0()
|
||||
.gap_1()
|
||||
.flex_1()
|
||||
.pl(px(entry.depth as f32 * TREE_INDENT))
|
||||
.child(
|
||||
Icon::new(folder_icon)
|
||||
@@ -5096,28 +5105,21 @@ impl GitPanel {
|
||||
h_flex()
|
||||
.id(id)
|
||||
.h(self.list_item_height())
|
||||
.min_w_0()
|
||||
.w_full()
|
||||
.items_center()
|
||||
.pl_3()
|
||||
.pr_1()
|
||||
.gap_1p5()
|
||||
.justify_between()
|
||||
.border_1()
|
||||
.border_r_2()
|
||||
.when(selected && self.focus_handle.is_focused(window), |el| {
|
||||
el.border_color(cx.theme().colors().panel_focused_border)
|
||||
})
|
||||
.px(rems(0.75))
|
||||
.overflow_hidden()
|
||||
.flex_none()
|
||||
.gap_1p5()
|
||||
.bg(base_bg)
|
||||
.hover(|this| this.bg(hover_bg))
|
||||
.active(|this| this.bg(active_bg))
|
||||
.on_click({
|
||||
let key = entry.key.clone();
|
||||
cx.listener(move |this, _event: &ClickEvent, window, cx| {
|
||||
this.selected_entry = Some(ix);
|
||||
this.toggle_directory(&key, window, cx);
|
||||
})
|
||||
})
|
||||
.child(name_row.overflow_x_hidden())
|
||||
.hover(|s| s.bg(hover_bg))
|
||||
.active(|s| s.bg(active_bg))
|
||||
.child(name_row)
|
||||
.child(
|
||||
div()
|
||||
.id(checkbox_wrapper_id)
|
||||
@@ -5156,12 +5158,18 @@ impl GitPanel {
|
||||
}),
|
||||
),
|
||||
)
|
||||
.on_click({
|
||||
let key = entry.key.clone();
|
||||
cx.listener(move |this, _event: &ClickEvent, window, cx| {
|
||||
this.selected_entry = Some(ix);
|
||||
this.toggle_directory(&key, window, cx);
|
||||
})
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn path_formatted(
|
||||
&self,
|
||||
parent: Div,
|
||||
directory: Option<String>,
|
||||
path_color: Color,
|
||||
file_name: String,
|
||||
@@ -5170,42 +5178,32 @@ impl GitPanel {
|
||||
git_path_style: GitPathStyle,
|
||||
strikethrough: bool,
|
||||
) -> Div {
|
||||
parent
|
||||
.when(git_path_style == GitPathStyle::FileNameFirst, |this| {
|
||||
this.child(
|
||||
self.entry_label(
|
||||
match directory.as_ref().is_none_or(|d| d.is_empty()) {
|
||||
true => file_name.clone(),
|
||||
false => format!("{file_name} "),
|
||||
},
|
||||
label_color,
|
||||
)
|
||||
.when(strikethrough, Label::strikethrough),
|
||||
)
|
||||
})
|
||||
.when_some(directory, |this, dir| {
|
||||
match (
|
||||
!dir.is_empty(),
|
||||
git_path_style == GitPathStyle::FileNameFirst,
|
||||
) {
|
||||
(true, true) => this.child(
|
||||
self.entry_label(dir, path_color)
|
||||
.when(strikethrough, Label::strikethrough),
|
||||
),
|
||||
(true, false) => this.child(
|
||||
self.entry_label(
|
||||
format!("{dir}{}", path_style.primary_separator()),
|
||||
path_color,
|
||||
)
|
||||
.when(strikethrough, Label::strikethrough),
|
||||
),
|
||||
_ => this,
|
||||
}
|
||||
})
|
||||
.when(git_path_style == GitPathStyle::FilePathFirst, |this| {
|
||||
this.child(
|
||||
let file_name_first = git_path_style == GitPathStyle::FileNameFirst;
|
||||
let file_path_first = git_path_style == GitPathStyle::FilePathFirst;
|
||||
|
||||
let file_name = format!("{} ", file_name);
|
||||
|
||||
h_flex()
|
||||
.min_w_0()
|
||||
.overflow_hidden()
|
||||
.when(file_path_first, |this| this.flex_row_reverse())
|
||||
.child(
|
||||
div().flex_none().child(
|
||||
self.entry_label(file_name, label_color)
|
||||
.when(strikethrough, Label::strikethrough),
|
||||
),
|
||||
)
|
||||
.when_some(directory, |this, dir| {
|
||||
let path_name = if file_name_first {
|
||||
dir
|
||||
} else {
|
||||
format!("{dir}{}", path_style.primary_separator())
|
||||
};
|
||||
|
||||
this.child(
|
||||
self.entry_label(path_name, path_color)
|
||||
.truncate()
|
||||
.when(strikethrough, Label::strikethrough),
|
||||
)
|
||||
})
|
||||
}
|
||||
@@ -5390,6 +5388,10 @@ impl Render for GitPanel {
|
||||
.on_action(cx.listener(Self::select_next))
|
||||
.on_action(cx.listener(Self::select_previous))
|
||||
.on_action(cx.listener(Self::select_last))
|
||||
.on_action(cx.listener(Self::first_entry))
|
||||
.on_action(cx.listener(Self::next_entry))
|
||||
.on_action(cx.listener(Self::previous_entry))
|
||||
.on_action(cx.listener(Self::last_entry))
|
||||
.on_action(cx.listener(Self::close_panel))
|
||||
.on_action(cx.listener(Self::open_diff))
|
||||
.on_action(cx.listener(Self::open_file))
|
||||
@@ -5540,6 +5542,7 @@ impl GitPanelMessageTooltip {
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Entity<Self> {
|
||||
let remote_url = repository.read(cx).default_remote_url();
|
||||
cx.new(|cx| {
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let (details, workspace) = git_panel.update(cx, |git_panel, cx| {
|
||||
@@ -5549,16 +5552,21 @@ impl GitPanelMessageTooltip {
|
||||
)
|
||||
})?;
|
||||
let details = details.await?;
|
||||
let provider_registry = cx
|
||||
.update(|_, app| GitHostingProviderRegistry::default_global(app))
|
||||
.ok();
|
||||
|
||||
let commit_details = crate::commit_tooltip::CommitDetails {
|
||||
sha: details.sha.clone(),
|
||||
author_name: details.author_name.clone(),
|
||||
author_email: details.author_email.clone(),
|
||||
commit_time: OffsetDateTime::from_unix_timestamp(details.commit_timestamp)?,
|
||||
message: Some(ParsedCommitMessage {
|
||||
message: details.message,
|
||||
..Default::default()
|
||||
}),
|
||||
message: Some(ParsedCommitMessage::parse(
|
||||
details.sha.to_string(),
|
||||
details.message.to_string(),
|
||||
remote_url.as_deref(),
|
||||
provider_registry,
|
||||
)),
|
||||
};
|
||||
|
||||
this.update(cx, |this: &mut GitPanelMessageTooltip, cx| {
|
||||
@@ -6855,7 +6863,7 @@ mod tests {
|
||||
// the Project Diff's active path.
|
||||
panel.update_in(cx, |panel, window, cx| {
|
||||
panel.selected_entry = Some(1);
|
||||
panel.open_diff(&Confirm, window, cx);
|
||||
panel.open_diff(&menu::Confirm, window, cx);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
|
||||
@@ -6871,6 +6879,128 @@ mod tests {
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_tree_view_reveals_collapsed_parent_on_select_entry_by_path(
|
||||
cx: &mut TestAppContext,
|
||||
) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.background_executor.clone());
|
||||
fs.insert_tree(
|
||||
path!("/project"),
|
||||
json!({
|
||||
".git": {},
|
||||
"src": {
|
||||
"a": {
|
||||
"foo.rs": "fn foo() {}",
|
||||
},
|
||||
"b": {
|
||||
"bar.rs": "fn bar() {}",
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
fs.set_status_for_repo(
|
||||
path!("/project/.git").as_ref(),
|
||||
&[
|
||||
("src/a/foo.rs", StatusCode::Modified.worktree()),
|
||||
("src/b/bar.rs", StatusCode::Modified.worktree()),
|
||||
],
|
||||
);
|
||||
|
||||
let project = Project::test(fs.clone(), [Path::new(path!("/project"))], cx).await;
|
||||
let workspace =
|
||||
cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
|
||||
let cx = &mut VisualTestContext::from_window(*workspace, cx);
|
||||
|
||||
cx.read(|cx| {
|
||||
project
|
||||
.read(cx)
|
||||
.worktrees(cx)
|
||||
.next()
|
||||
.unwrap()
|
||||
.read(cx)
|
||||
.as_local()
|
||||
.unwrap()
|
||||
.scan_complete()
|
||||
})
|
||||
.await;
|
||||
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
cx.update(|_window, cx| {
|
||||
SettingsStore::update_global(cx, |store, cx| {
|
||||
store.update_user_settings(cx, |settings| {
|
||||
settings.git_panel.get_or_insert_default().tree_view = Some(true);
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
let panel = workspace.update(cx, GitPanel::new).unwrap();
|
||||
|
||||
let handle = cx.update_window_entity(&panel, |panel, _, _| {
|
||||
std::mem::replace(&mut panel.update_visible_entries_task, Task::ready(()))
|
||||
});
|
||||
cx.executor().advance_clock(2 * UPDATE_DEBOUNCE);
|
||||
handle.await;
|
||||
|
||||
let src_key = panel.read_with(cx, |panel, _| {
|
||||
panel
|
||||
.entries
|
||||
.iter()
|
||||
.find_map(|entry| match entry {
|
||||
GitListEntry::Directory(dir) if dir.key.path == repo_path("src") => {
|
||||
Some(dir.key.clone())
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.expect("src directory should exist in tree view")
|
||||
});
|
||||
|
||||
panel.update_in(cx, |panel, window, cx| {
|
||||
panel.toggle_directory(&src_key, window, cx);
|
||||
});
|
||||
|
||||
panel.read_with(cx, |panel, _| {
|
||||
let state = panel
|
||||
.view_mode
|
||||
.tree_state()
|
||||
.expect("tree view state should exist");
|
||||
assert_eq!(state.expanded_dirs.get(&src_key).copied(), Some(false));
|
||||
});
|
||||
|
||||
let worktree_id =
|
||||
cx.read(|cx| project.read(cx).worktrees(cx).next().unwrap().read(cx).id());
|
||||
let project_path = ProjectPath {
|
||||
worktree_id,
|
||||
path: RelPath::unix("src/a/foo.rs").unwrap().into_arc(),
|
||||
};
|
||||
|
||||
panel.update_in(cx, |panel, window, cx| {
|
||||
panel.select_entry_by_path(project_path, window, cx);
|
||||
});
|
||||
|
||||
panel.read_with(cx, |panel, _| {
|
||||
let state = panel
|
||||
.view_mode
|
||||
.tree_state()
|
||||
.expect("tree view state should exist");
|
||||
assert_eq!(state.expanded_dirs.get(&src_key).copied(), Some(true));
|
||||
|
||||
let selected_ix = panel.selected_entry.expect("selection should be set");
|
||||
assert!(state.logical_indices.contains(&selected_ix));
|
||||
|
||||
let selected_entry = panel
|
||||
.entries
|
||||
.get(selected_ix)
|
||||
.and_then(|entry| entry.status_entry())
|
||||
.expect("selected entry should be a status entry");
|
||||
assert_eq!(selected_entry.repo_path, repo_path("src/a/foo.rs"));
|
||||
});
|
||||
}
|
||||
|
||||
fn assert_entry_paths(entries: &[GitListEntry], expected_paths: &[Option<&str>]) {
|
||||
assert_eq!(entries.len(), expected_paths.len());
|
||||
for (entry, expected_path) in entries.iter().zip(expected_paths) {
|
||||
|
||||
@@ -10,6 +10,7 @@ use ui::{
|
||||
};
|
||||
|
||||
mod blame_ui;
|
||||
pub mod clone;
|
||||
|
||||
use git::{
|
||||
repository::{Branch, Upstream, UpstreamTracking, UpstreamTrackingStatus},
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use anyhow::Context as _;
|
||||
use collections::HashSet;
|
||||
use fuzzy::StringMatchCandidate;
|
||||
|
||||
use git::repository::Worktree as GitWorktree;
|
||||
@@ -9,7 +10,11 @@ use gpui::{
|
||||
actions, rems,
|
||||
};
|
||||
use picker::{Picker, PickerDelegate, PickerEditorPosition};
|
||||
use project::{DirectoryLister, git_store::Repository};
|
||||
use project::{
|
||||
DirectoryLister,
|
||||
git_store::Repository,
|
||||
trusted_worktrees::{PathTrust, RemoteHostLocation, TrustedWorktrees},
|
||||
};
|
||||
use recent_projects::{RemoteConnectionModal, connect};
|
||||
use remote::{RemoteConnectionOptions, remote_client::ConnectionIdentifier};
|
||||
use std::{path::PathBuf, sync::Arc};
|
||||
@@ -219,7 +224,6 @@ impl WorktreeListDelegate {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Picker<Self>>,
|
||||
) {
|
||||
let workspace = self.workspace.clone();
|
||||
let Some(repo) = self.repo.clone() else {
|
||||
return;
|
||||
};
|
||||
@@ -247,6 +251,7 @@ impl WorktreeListDelegate {
|
||||
|
||||
let branch = worktree_branch.to_string();
|
||||
let window_handle = window.window_handle();
|
||||
let workspace = self.workspace.clone();
|
||||
cx.spawn_in(window, async move |_, cx| {
|
||||
let Some(paths) = worktree_path.await? else {
|
||||
return anyhow::Ok(());
|
||||
@@ -257,8 +262,32 @@ impl WorktreeListDelegate {
|
||||
repo.create_worktree(branch.clone(), path.clone(), commit)
|
||||
})?
|
||||
.await??;
|
||||
let new_worktree_path = path.join(branch);
|
||||
|
||||
let final_path = path.join(branch);
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
if let Some(trusted_worktrees) = TrustedWorktrees::try_get_global(cx) {
|
||||
let repo_path = &repo.read(cx).snapshot().work_directory_abs_path;
|
||||
let project = workspace.project();
|
||||
if let Some((parent_worktree, _)) =
|
||||
project.read(cx).find_worktree(repo_path, cx)
|
||||
{
|
||||
trusted_worktrees.update(cx, |trusted_worktrees, cx| {
|
||||
if trusted_worktrees.can_trust(parent_worktree.read(cx).id(), cx) {
|
||||
trusted_worktrees.trust(
|
||||
HashSet::from_iter([PathTrust::AbsPath(
|
||||
new_worktree_path.clone(),
|
||||
)]),
|
||||
project
|
||||
.read(cx)
|
||||
.remote_connection_options(cx)
|
||||
.map(RemoteHostLocation::from),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
})?;
|
||||
|
||||
let (connection_options, app_state, is_local) =
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
@@ -274,7 +303,7 @@ impl WorktreeListDelegate {
|
||||
.update_in(cx, |workspace, window, cx| {
|
||||
workspace.open_workspace_for_paths(
|
||||
replace_current_window,
|
||||
vec![final_path],
|
||||
vec![new_worktree_path],
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
@@ -283,7 +312,7 @@ impl WorktreeListDelegate {
|
||||
} else if let Some(connection_options) = connection_options {
|
||||
open_remote_worktree(
|
||||
connection_options,
|
||||
vec![final_path],
|
||||
vec![new_worktree_path],
|
||||
app_state,
|
||||
window_handle,
|
||||
replace_current_window,
|
||||
|
||||
@@ -512,6 +512,8 @@ pub enum Model {
|
||||
Gemini25Pro,
|
||||
#[serde(rename = "gemini-3-pro-preview")]
|
||||
Gemini3Pro,
|
||||
#[serde(rename = "gemini-3-flash-preview")]
|
||||
Gemini3Flash,
|
||||
#[serde(rename = "custom")]
|
||||
Custom {
|
||||
name: String,
|
||||
@@ -534,6 +536,7 @@ impl Model {
|
||||
Self::Gemini25Flash => "gemini-2.5-flash",
|
||||
Self::Gemini25Pro => "gemini-2.5-pro",
|
||||
Self::Gemini3Pro => "gemini-3-pro-preview",
|
||||
Self::Gemini3Flash => "gemini-3-flash-preview",
|
||||
Self::Custom { name, .. } => name,
|
||||
}
|
||||
}
|
||||
@@ -543,6 +546,7 @@ impl Model {
|
||||
Self::Gemini25Flash => "gemini-2.5-flash",
|
||||
Self::Gemini25Pro => "gemini-2.5-pro",
|
||||
Self::Gemini3Pro => "gemini-3-pro-preview",
|
||||
Self::Gemini3Flash => "gemini-3-flash-preview",
|
||||
Self::Custom { name, .. } => name,
|
||||
}
|
||||
}
|
||||
@@ -553,6 +557,7 @@ impl Model {
|
||||
Self::Gemini25Flash => "Gemini 2.5 Flash",
|
||||
Self::Gemini25Pro => "Gemini 2.5 Pro",
|
||||
Self::Gemini3Pro => "Gemini 3 Pro",
|
||||
Self::Gemini3Flash => "Gemini 3 Flash",
|
||||
Self::Custom {
|
||||
name, display_name, ..
|
||||
} => display_name.as_ref().unwrap_or(name),
|
||||
@@ -565,6 +570,7 @@ impl Model {
|
||||
Self::Gemini25Flash => 1_048_576,
|
||||
Self::Gemini25Pro => 1_048_576,
|
||||
Self::Gemini3Pro => 1_048_576,
|
||||
Self::Gemini3Flash => 1_048_576,
|
||||
Self::Custom { max_tokens, .. } => *max_tokens,
|
||||
}
|
||||
}
|
||||
@@ -575,6 +581,7 @@ impl Model {
|
||||
Model::Gemini25Flash => Some(65_536),
|
||||
Model::Gemini25Pro => Some(65_536),
|
||||
Model::Gemini3Pro => Some(65_536),
|
||||
Model::Gemini3Flash => Some(65_536),
|
||||
Model::Custom { .. } => None,
|
||||
}
|
||||
}
|
||||
@@ -599,6 +606,7 @@ impl Model {
|
||||
budget_tokens: None,
|
||||
}
|
||||
}
|
||||
Self::Gemini3Flash => GoogleModelMode::Default,
|
||||
Self::Custom { mode, .. } => *mode,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -462,6 +462,17 @@ impl DispatchTree {
|
||||
(bindings, partial, context_stack)
|
||||
}
|
||||
|
||||
/// Find the bindings that can follow the current input sequence.
|
||||
pub fn possible_next_bindings_for_input(
|
||||
&self,
|
||||
input: &[Keystroke],
|
||||
context_stack: &[KeyContext],
|
||||
) -> Vec<KeyBinding> {
|
||||
self.keymap
|
||||
.borrow()
|
||||
.possible_next_bindings_for_input(input, context_stack)
|
||||
}
|
||||
|
||||
/// dispatch_key processes the keystroke
|
||||
/// input should be set to the value of `pending` from the previous call to dispatch_key.
|
||||
/// This returns three instructions to the input handler:
|
||||
|
||||
@@ -215,6 +215,41 @@ impl Keymap {
|
||||
Some(contexts.len())
|
||||
}
|
||||
}
|
||||
|
||||
/// Find the bindings that can follow the current input sequence.
|
||||
pub fn possible_next_bindings_for_input(
|
||||
&self,
|
||||
input: &[Keystroke],
|
||||
context_stack: &[KeyContext],
|
||||
) -> Vec<KeyBinding> {
|
||||
let mut bindings = self
|
||||
.bindings()
|
||||
.enumerate()
|
||||
.rev()
|
||||
.filter_map(|(ix, binding)| {
|
||||
let depth = self.binding_enabled(binding, context_stack)?;
|
||||
let pending = binding.match_keystrokes(input);
|
||||
match pending {
|
||||
None => None,
|
||||
Some(is_pending) => {
|
||||
if !is_pending || is_no_action(&*binding.action) {
|
||||
return None;
|
||||
}
|
||||
Some((depth, BindingIndex(ix), binding))
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
bindings.sort_by(|(depth_a, ix_a, _), (depth_b, ix_b, _)| {
|
||||
depth_b.cmp(depth_a).then(ix_b.cmp(ix_a))
|
||||
});
|
||||
|
||||
bindings
|
||||
.into_iter()
|
||||
.map(|(_, _, binding)| binding.clone())
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -1025,13 +1025,26 @@ impl PlatformWindow for WaylandWindow {
|
||||
fn resize(&mut self, size: Size<Pixels>) {
|
||||
let state = self.borrow();
|
||||
let state_ptr = self.0.clone();
|
||||
let dp_size = size.to_device_pixels(self.scale_factor());
|
||||
|
||||
// Keep window geometry consistent with configure handling. On Wayland, window geometry is
|
||||
// surface-local: resizing should not attempt to translate the window; the compositor
|
||||
// controls placement. We also account for client-side decoration insets and tiling.
|
||||
let window_geometry = inset_by_tiling(
|
||||
Bounds {
|
||||
origin: Point::default(),
|
||||
size,
|
||||
},
|
||||
state.inset(),
|
||||
state.tiling,
|
||||
)
|
||||
.map(|v| v.0 as i32)
|
||||
.map_size(|v| if v <= 0 { 1 } else { v });
|
||||
|
||||
state.surface_state.set_geometry(
|
||||
state.bounds.origin.x.0 as i32,
|
||||
state.bounds.origin.y.0 as i32,
|
||||
dp_size.width.0,
|
||||
dp_size.height.0,
|
||||
window_geometry.origin.x,
|
||||
window_geometry.origin.y,
|
||||
window_geometry.size.width,
|
||||
window_geometry.size.height,
|
||||
);
|
||||
|
||||
state
|
||||
|
||||
@@ -52,6 +52,11 @@ pub fn apply_features_and_fallbacks(
|
||||
&kCFTypeDictionaryKeyCallBacks,
|
||||
&kCFTypeDictionaryValueCallBacks,
|
||||
);
|
||||
|
||||
for value in &values {
|
||||
CFRelease(*value as _);
|
||||
}
|
||||
|
||||
let new_descriptor = CTFontDescriptorCreateWithAttributes(attrs);
|
||||
CFRelease(attrs as _);
|
||||
let new_descriptor = CTFontDescriptor::wrap_under_create_rule(new_descriptor);
|
||||
|
||||
@@ -1398,7 +1398,6 @@ extern "C" fn will_finish_launching(_this: &mut Object, _: Sel, _: id) {
|
||||
}
|
||||
|
||||
extern "C" fn did_finish_launching(this: &mut Object, _: Sel, _: id) {
|
||||
log::info!("did_finish_launching: entered");
|
||||
unsafe {
|
||||
let app: id = msg_send![APP_CLASS, sharedApplication];
|
||||
app.setActivationPolicy_(NSApplicationActivationPolicyRegular);
|
||||
@@ -1413,13 +1412,10 @@ extern "C" fn did_finish_launching(this: &mut Object, _: Sel, _: id) {
|
||||
];
|
||||
|
||||
let platform = get_mac_platform(this);
|
||||
|
||||
log::info!("did_finish_launching: about to call finish_launching callback");
|
||||
let callback = platform.0.lock().finish_launching.take();
|
||||
if let Some(callback) = callback {
|
||||
callback();
|
||||
}
|
||||
log::info!("did_finish_launching: finish_launching callback completed");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1462,7 +1458,6 @@ extern "C" fn on_keyboard_layout_change(this: &mut Object, _: Sel, _: id) {
|
||||
}
|
||||
|
||||
extern "C" fn open_urls(this: &mut Object, _: Sel, _: id, urls: id) {
|
||||
log::info!("open_urls: entered");
|
||||
let urls = unsafe {
|
||||
(0..urls.count())
|
||||
.filter_map(|i| {
|
||||
@@ -1477,7 +1472,6 @@ extern "C" fn open_urls(this: &mut Object, _: Sel, _: id, urls: id) {
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
log::info!("open_urls: received {} URLs: {:?}", urls.len(), urls);
|
||||
let platform = unsafe { get_mac_platform(this) };
|
||||
let mut lock = platform.0.lock();
|
||||
if let Some(mut callback) = lock.open_urls.take() {
|
||||
|
||||
@@ -110,13 +110,21 @@ impl ScreenCaptureSource for MacScreenCaptureSource {
|
||||
let _: id = msg_send![configuration, setHeight: meta.resolution.height.0 as i64];
|
||||
let stream: id = msg_send![stream, initWithFilter:filter configuration:configuration delegate:delegate];
|
||||
|
||||
// Stream contains filter, configuration, and delegate internally so we release them here
|
||||
// to prevent a memory leak when steam is dropped
|
||||
let _: () = msg_send![filter, release];
|
||||
let _: () = msg_send![configuration, release];
|
||||
let _: () = msg_send![delegate, release];
|
||||
|
||||
let (mut tx, rx) = oneshot::channel();
|
||||
|
||||
let mut error: id = nil;
|
||||
let _: () = msg_send![stream, addStreamOutput:output type:SCStreamOutputTypeScreen sampleHandlerQueue:0 error:&mut error as *mut id];
|
||||
if error != nil {
|
||||
let message: id = msg_send![error, localizedDescription];
|
||||
tx.send(Err(anyhow!("failed to add stream output {message:?}")))
|
||||
let _: () = msg_send![stream, release];
|
||||
let _: () = msg_send![output, release];
|
||||
tx.send(Err(anyhow!("failed to add stream output {message:?}")))
|
||||
.ok();
|
||||
return rx;
|
||||
}
|
||||
@@ -132,8 +140,10 @@ impl ScreenCaptureSource for MacScreenCaptureSource {
|
||||
};
|
||||
Ok(Box::new(stream) as Box<dyn ScreenCaptureStream>)
|
||||
} else {
|
||||
let _: () = msg_send![stream, release];
|
||||
let _: () = msg_send![output, release];
|
||||
let message: id = msg_send![error, localizedDescription];
|
||||
Err(anyhow!("failed to stop screen capture stream {message:?}"))
|
||||
Err(anyhow!("failed to start screen capture stream {message:?}"))
|
||||
};
|
||||
if let Some(tx) = tx.borrow_mut().take() {
|
||||
tx.send(result).ok();
|
||||
|
||||
@@ -8,6 +8,7 @@ use anyhow::anyhow;
|
||||
use cocoa::appkit::CGFloat;
|
||||
use collections::HashMap;
|
||||
use core_foundation::{
|
||||
array::{CFArray, CFArrayRef},
|
||||
attributed_string::CFMutableAttributedString,
|
||||
base::{CFRange, TCFType},
|
||||
number::CFNumber,
|
||||
@@ -21,8 +22,10 @@ use core_graphics::{
|
||||
};
|
||||
use core_text::{
|
||||
font::CTFont,
|
||||
font_collection::CTFontCollectionRef,
|
||||
font_descriptor::{
|
||||
kCTFontSlantTrait, kCTFontSymbolicTrait, kCTFontWeightTrait, kCTFontWidthTrait,
|
||||
CTFontDescriptor, kCTFontSlantTrait, kCTFontSymbolicTrait, kCTFontWeightTrait,
|
||||
kCTFontWidthTrait,
|
||||
},
|
||||
line::CTLine,
|
||||
string_attributes::kCTFontAttributeName,
|
||||
@@ -97,7 +100,26 @@ impl PlatformTextSystem for MacTextSystem {
|
||||
fn all_font_names(&self) -> Vec<String> {
|
||||
let mut names = Vec::new();
|
||||
let collection = core_text::font_collection::create_for_all_families();
|
||||
let Some(descriptors) = collection.get_descriptors() else {
|
||||
// NOTE: We intentionally avoid using `collection.get_descriptors()` here because
|
||||
// it has a memory leak bug in core-text v21.0.0. The upstream code uses
|
||||
// `wrap_under_get_rule` but `CTFontCollectionCreateMatchingFontDescriptors`
|
||||
// follows the Create Rule (caller owns the result), so it should use
|
||||
// `wrap_under_create_rule`. We call the function directly with correct memory management.
|
||||
unsafe extern "C" {
|
||||
fn CTFontCollectionCreateMatchingFontDescriptors(
|
||||
collection: CTFontCollectionRef,
|
||||
) -> CFArrayRef;
|
||||
}
|
||||
let descriptors: Option<CFArray<CTFontDescriptor>> = unsafe {
|
||||
let array_ref =
|
||||
CTFontCollectionCreateMatchingFontDescriptors(collection.as_concrete_TypeRef());
|
||||
if array_ref.is_null() {
|
||||
None
|
||||
} else {
|
||||
Some(CFArray::wrap_under_create_rule(array_ref))
|
||||
}
|
||||
};
|
||||
let Some(descriptors) = descriptors else {
|
||||
return names;
|
||||
};
|
||||
for descriptor in descriptors.into_iter() {
|
||||
|
||||
@@ -1190,6 +1190,7 @@ impl PlatformWindow for MacWindow {
|
||||
let (done_tx, done_rx) = oneshot::channel();
|
||||
let done_tx = Cell::new(Some(done_tx));
|
||||
let block = ConcreteBlock::new(move |answer: NSInteger| {
|
||||
let _: () = msg_send![alert, release];
|
||||
if let Some(done_tx) = done_tx.take() {
|
||||
let _ = done_tx.send(answer.try_into().unwrap());
|
||||
}
|
||||
|
||||
@@ -182,6 +182,11 @@ impl LineWrapper {
|
||||
// Cyrillic for Russian, Ukrainian, etc.
|
||||
// https://en.wikipedia.org/wiki/Cyrillic_script_in_Unicode
|
||||
matches!(c, '\u{0400}'..='\u{04FF}') ||
|
||||
|
||||
// Vietnamese (https://vietunicode.sourceforge.net/charset/)
|
||||
matches!(c, '\u{1E00}'..='\u{1EFF}') || // Latin Extended Additional
|
||||
matches!(c, '\u{0300}'..='\u{036F}') || // Combining Diacritical Marks
|
||||
|
||||
// Some other known special characters that should be treated as word characters,
|
||||
// e.g. `a-b`, `var_name`, `I'm`, '@mention`, `#hashtag`, `100%`, `3.1415`,
|
||||
// `2^3`, `a~b`, `a=1`, `Self::new`, etc.
|
||||
@@ -618,7 +623,12 @@ mod tests {
|
||||
#[track_caller]
|
||||
fn assert_word(word: &str) {
|
||||
for c in word.chars() {
|
||||
assert!(LineWrapper::is_word_char(c), "assertion failed for '{}'", c);
|
||||
assert!(
|
||||
LineWrapper::is_word_char(c),
|
||||
"assertion failed for '{}' (unicode 0x{:x})",
|
||||
c,
|
||||
c as u32
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -661,6 +671,8 @@ mod tests {
|
||||
assert_word("ƀƁƂƃƄƅƆƇƈƉƊƋƌƍƎƏ");
|
||||
// Cyrillic
|
||||
assert_word("АБВГДЕЖЗИЙКЛМНОП");
|
||||
// Vietnamese (https://github.com/zed-industries/zed/issues/23245)
|
||||
assert_word("ThậmchíđếnkhithuachạychúngcònnhẫntâmgiếtnốtsốđôngtùchínhtrịởYênBáivàCaoBằng");
|
||||
|
||||
// non-word characters
|
||||
assert_not_word("你好");
|
||||
|
||||
@@ -4450,6 +4450,13 @@ impl Window {
|
||||
dispatch_tree.highest_precedence_binding_for_action(action, &context_stack)
|
||||
}
|
||||
|
||||
/// Find the bindings that can follow the current input sequence for the current context stack.
|
||||
pub fn possible_bindings_for_input(&self, input: &[Keystroke]) -> Vec<KeyBinding> {
|
||||
self.rendered_frame
|
||||
.dispatch_tree
|
||||
.possible_next_bindings_for_input(input, &self.context_stack())
|
||||
}
|
||||
|
||||
fn context_stack_for_focus_handle(
|
||||
&self,
|
||||
focus_handle: &FocusHandle,
|
||||
|
||||
@@ -32,6 +32,7 @@ async-trait.workspace = true
|
||||
clock.workspace = true
|
||||
collections.workspace = true
|
||||
ec4rs.workspace = true
|
||||
encoding_rs.workspace = true
|
||||
fs.workspace = true
|
||||
futures.workspace = true
|
||||
fuzzy.workspace = true
|
||||
|
||||
@@ -25,6 +25,7 @@ use anyhow::{Context as _, Result};
|
||||
use clock::Lamport;
|
||||
pub use clock::ReplicaId;
|
||||
use collections::{HashMap, HashSet};
|
||||
use encoding_rs::Encoding;
|
||||
use fs::MTime;
|
||||
use futures::channel::oneshot;
|
||||
use gpui::{
|
||||
@@ -131,6 +132,8 @@ pub struct Buffer {
|
||||
change_bits: Vec<rc::Weak<Cell<bool>>>,
|
||||
_subscriptions: Vec<gpui::Subscription>,
|
||||
tree_sitter_data: Arc<TreeSitterData>,
|
||||
encoding: &'static Encoding,
|
||||
has_bom: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -1100,6 +1103,8 @@ impl Buffer {
|
||||
has_conflict: false,
|
||||
change_bits: Default::default(),
|
||||
_subscriptions: Vec::new(),
|
||||
encoding: encoding_rs::UTF_8,
|
||||
has_bom: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1383,6 +1388,26 @@ impl Buffer {
|
||||
self.saved_mtime
|
||||
}
|
||||
|
||||
/// Returns the character encoding of the buffer's file.
|
||||
pub fn encoding(&self) -> &'static Encoding {
|
||||
self.encoding
|
||||
}
|
||||
|
||||
/// Sets the character encoding of the buffer.
|
||||
pub fn set_encoding(&mut self, encoding: &'static Encoding) {
|
||||
self.encoding = encoding;
|
||||
}
|
||||
|
||||
/// Returns whether the buffer has a Byte Order Mark.
|
||||
pub fn has_bom(&self) -> bool {
|
||||
self.has_bom
|
||||
}
|
||||
|
||||
/// Sets whether the buffer has a Byte Order Mark.
|
||||
pub fn set_has_bom(&mut self, has_bom: bool) {
|
||||
self.has_bom = has_bom;
|
||||
}
|
||||
|
||||
/// Assign a language to the buffer.
|
||||
pub fn set_language_async(&mut self, language: Option<Arc<Language>>, cx: &mut Context<Self>) {
|
||||
self.set_language_(language, cfg!(any(test, feature = "test-support")), cx);
|
||||
|
||||
@@ -125,7 +125,7 @@ pub fn init(on_headless_host: bool, cx: &mut App) {
|
||||
let server_id = server.server_id();
|
||||
let weak_lsp_store = cx.weak_entity();
|
||||
log_store.copilot_log_subscription =
|
||||
Some(server.on_notification::<copilot::request::LogMessage, _>(
|
||||
Some(server.on_notification::<lsp::notification::LogMessage, _>(
|
||||
move |params, cx| {
|
||||
weak_lsp_store
|
||||
.update(cx, |lsp_store, cx| {
|
||||
|
||||
@@ -40,6 +40,7 @@ clock.workspace = true
|
||||
collections.workspace = true
|
||||
context_server.workspace = true
|
||||
dap.workspace = true
|
||||
encoding_rs.workspace = true
|
||||
extension.workspace = true
|
||||
fancy-regex.workspace = true
|
||||
fs.workspace = true
|
||||
|
||||
@@ -376,6 +376,8 @@ impl LocalBufferStore {
|
||||
|
||||
let text = buffer.as_rope().clone();
|
||||
let line_ending = buffer.line_ending();
|
||||
let encoding = buffer.encoding();
|
||||
let has_bom = buffer.has_bom();
|
||||
let version = buffer.version();
|
||||
let buffer_id = buffer.remote_id();
|
||||
let file = buffer.file().cloned();
|
||||
@@ -387,7 +389,7 @@ impl LocalBufferStore {
|
||||
}
|
||||
|
||||
let save = worktree.update(cx, |worktree, cx| {
|
||||
worktree.write_file(path, text, line_ending, cx)
|
||||
worktree.write_file(path, text, line_ending, encoding, has_bom, cx)
|
||||
});
|
||||
|
||||
cx.spawn(async move |this, cx| {
|
||||
@@ -630,7 +632,11 @@ impl LocalBufferStore {
|
||||
})
|
||||
.await;
|
||||
cx.insert_entity(reservation, |_| {
|
||||
Buffer::build(text_buffer, Some(loaded.file), Capability::ReadWrite)
|
||||
let mut buffer =
|
||||
Buffer::build(text_buffer, Some(loaded.file), Capability::ReadWrite);
|
||||
buffer.set_encoding(loaded.encoding);
|
||||
buffer.set_has_bom(loaded.has_bom);
|
||||
buffer
|
||||
})?
|
||||
}
|
||||
Err(error) if is_not_found_error(&error) => cx.new(|cx| {
|
||||
|
||||
@@ -5948,6 +5948,11 @@ impl Repository {
|
||||
self.pending_ops.edit(edits, ());
|
||||
ids
|
||||
}
|
||||
pub fn default_remote_url(&self) -> Option<String> {
|
||||
self.remote_upstream_url
|
||||
.clone()
|
||||
.or(self.remote_origin_url.clone())
|
||||
}
|
||||
}
|
||||
|
||||
fn get_permalink_in_rust_registry_src(
|
||||
|
||||
@@ -65,6 +65,7 @@ use debugger::{
|
||||
dap_store::{DapStore, DapStoreEvent},
|
||||
session::Session,
|
||||
};
|
||||
use encoding_rs;
|
||||
pub use environment::ProjectEnvironment;
|
||||
#[cfg(test)]
|
||||
use futures::future::join_all;
|
||||
@@ -5461,13 +5462,22 @@ impl Project {
|
||||
.await
|
||||
.context("Failed to load settings file")?;
|
||||
|
||||
let has_bom = file.has_bom;
|
||||
|
||||
let new_text = cx.read_global::<SettingsStore, _>(|store, cx| {
|
||||
store.new_text_for_update(file.text, move |settings| update(settings, cx))
|
||||
})?;
|
||||
worktree
|
||||
.update(cx, |worktree, cx| {
|
||||
let line_ending = text::LineEnding::detect(&new_text);
|
||||
worktree.write_file(rel_path.clone(), new_text.into(), line_ending, cx)
|
||||
worktree.write_file(
|
||||
rel_path.clone(),
|
||||
new_text.into(),
|
||||
line_ending,
|
||||
encoding_rs::UTF_8,
|
||||
has_bom,
|
||||
cx,
|
||||
)
|
||||
})?
|
||||
.await
|
||||
.context("Failed to write settings file")?;
|
||||
|
||||
@@ -112,7 +112,7 @@ pub struct ContentPromptContextV2 {
|
||||
pub language_name: Option<String>,
|
||||
pub is_truncated: bool,
|
||||
pub document_content: String,
|
||||
pub rewrite_section: Option<String>,
|
||||
pub rewrite_section: String,
|
||||
pub diagnostic_errors: Vec<ContentPromptDiagnosticContext>,
|
||||
}
|
||||
|
||||
@@ -310,7 +310,6 @@ impl PromptBuilder {
|
||||
};
|
||||
|
||||
const MAX_CTX: usize = 50000;
|
||||
let is_insert = range.is_empty();
|
||||
let mut is_truncated = false;
|
||||
|
||||
let before_range = 0..range.start;
|
||||
@@ -335,28 +334,19 @@ impl PromptBuilder {
|
||||
for chunk in buffer.text_for_range(truncated_before) {
|
||||
document_content.push_str(chunk);
|
||||
}
|
||||
if is_insert {
|
||||
document_content.push_str("<insert_here></insert_here>");
|
||||
} else {
|
||||
document_content.push_str("<rewrite_this>\n");
|
||||
for chunk in buffer.text_for_range(range.clone()) {
|
||||
document_content.push_str(chunk);
|
||||
}
|
||||
document_content.push_str("\n</rewrite_this>");
|
||||
|
||||
document_content.push_str("<rewrite_this>\n");
|
||||
for chunk in buffer.text_for_range(range.clone()) {
|
||||
document_content.push_str(chunk);
|
||||
}
|
||||
document_content.push_str("\n</rewrite_this>");
|
||||
|
||||
for chunk in buffer.text_for_range(truncated_after) {
|
||||
document_content.push_str(chunk);
|
||||
}
|
||||
|
||||
let rewrite_section = if !is_insert {
|
||||
let mut section = String::new();
|
||||
for chunk in buffer.text_for_range(range.clone()) {
|
||||
section.push_str(chunk);
|
||||
}
|
||||
Some(section)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let rewrite_section: String = buffer.text_for_range(range.clone()).collect();
|
||||
|
||||
let diagnostics = buffer.diagnostics_in_range::<_, Point>(range, false);
|
||||
let diagnostic_errors: Vec<ContentPromptDiagnosticContext> = diagnostics
|
||||
.map(|entry| {
|
||||
|
||||
@@ -7,7 +7,6 @@ use crate::{
|
||||
search_bar::{ActionButtonState, input_base_styles, render_action_button, render_text_input},
|
||||
};
|
||||
use any_vec::AnyVec;
|
||||
use anyhow::Context as _;
|
||||
use collections::HashMap;
|
||||
use editor::{
|
||||
DisplayPoint, Editor, EditorSettings, MultiBufferOffset,
|
||||
@@ -634,15 +633,19 @@ impl BufferSearchBar {
|
||||
.read(cx)
|
||||
.as_singleton()
|
||||
.expect("query editor should be backed by a singleton buffer");
|
||||
|
||||
query_buffer
|
||||
.read(cx)
|
||||
.set_language_registry(languages.clone());
|
||||
|
||||
cx.spawn(async move |buffer_search_bar, cx| {
|
||||
use anyhow::Context as _;
|
||||
|
||||
let regex_language = languages
|
||||
.language_for_name("regex")
|
||||
.await
|
||||
.context("loading regex language")?;
|
||||
|
||||
buffer_search_bar
|
||||
.update(cx, |buffer_search_bar, cx| {
|
||||
buffer_search_bar.regex_language = Some(regex_language);
|
||||
|
||||
@@ -158,6 +158,9 @@ pub struct SettingsContent {
|
||||
/// Default: false
|
||||
pub disable_ai: Option<SaturatingBool>,
|
||||
|
||||
/// Settings for the which-key popup.
|
||||
pub which_key: Option<WhichKeySettingsContent>,
|
||||
|
||||
/// Settings related to Vim mode in Zed.
|
||||
pub vim: Option<VimSettingsContent>,
|
||||
}
|
||||
@@ -976,6 +979,19 @@ pub struct ReplSettingsContent {
|
||||
pub max_columns: Option<usize>,
|
||||
}
|
||||
|
||||
/// Settings for configuring the which-key popup behaviour.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
|
||||
pub struct WhichKeySettingsContent {
|
||||
/// Whether to show the which-key popup when holding down key combinations
|
||||
///
|
||||
/// Default: false
|
||||
pub enabled: Option<bool>,
|
||||
/// Delay in milliseconds before showing the which-key popup.
|
||||
///
|
||||
/// Default: 700
|
||||
pub delay_ms: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||
/// An ExtendingVec in the settings can only accumulate new values.
|
||||
///
|
||||
|
||||
@@ -215,6 +215,7 @@ impl VsCodeSettings {
|
||||
vim: None,
|
||||
vim_mode: None,
|
||||
workspace: self.workspace_settings_content(),
|
||||
which_key: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1233,6 +1233,49 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
|
||||
}
|
||||
}).collect(),
|
||||
}),
|
||||
SettingsPageItem::SectionHeader("Which-key Menu"),
|
||||
SettingsPageItem::SettingItem(SettingItem {
|
||||
title: "Show Which-key Menu",
|
||||
description: "Display the which-key menu with matching bindings while a multi-stroke binding is pending.",
|
||||
field: Box::new(SettingField {
|
||||
json_path: Some("which_key.enabled"),
|
||||
pick: |settings_content| {
|
||||
settings_content
|
||||
.which_key
|
||||
.as_ref()
|
||||
.and_then(|settings| settings.enabled.as_ref())
|
||||
},
|
||||
write: |settings_content, value| {
|
||||
settings_content
|
||||
.which_key
|
||||
.get_or_insert_default()
|
||||
.enabled = value;
|
||||
},
|
||||
}),
|
||||
metadata: None,
|
||||
files: USER,
|
||||
}),
|
||||
SettingsPageItem::SettingItem(SettingItem {
|
||||
title: "Menu Delay",
|
||||
description: "Delay in milliseconds before the which-key menu appears.",
|
||||
field: Box::new(SettingField {
|
||||
json_path: Some("which_key.delay_ms"),
|
||||
pick: |settings_content| {
|
||||
settings_content
|
||||
.which_key
|
||||
.as_ref()
|
||||
.and_then(|settings| settings.delay_ms.as_ref())
|
||||
},
|
||||
write: |settings_content, value| {
|
||||
settings_content
|
||||
.which_key
|
||||
.get_or_insert_default()
|
||||
.delay_ms = value;
|
||||
},
|
||||
}),
|
||||
metadata: None,
|
||||
files: USER,
|
||||
}),
|
||||
SettingsPageItem::SectionHeader("Multibuffer"),
|
||||
SettingsPageItem::SettingItem(SettingItem {
|
||||
title: "Double Click In Multibuffer",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::{Supermaven, SupermavenCompletionStateId};
|
||||
use anyhow::Result;
|
||||
use edit_prediction_types::{Direction, EditPrediction, EditPredictionDelegate};
|
||||
use edit_prediction_types::{EditPrediction, EditPredictionDelegate};
|
||||
use futures::StreamExt as _;
|
||||
use gpui::{App, Context, Entity, EntityId, Task};
|
||||
use language::{Anchor, Buffer, BufferSnapshot};
|
||||
@@ -189,15 +189,6 @@ impl EditPredictionDelegate for SupermavenEditPredictionDelegate {
|
||||
}));
|
||||
}
|
||||
|
||||
fn cycle(
|
||||
&mut self,
|
||||
_buffer: Entity<Buffer>,
|
||||
_cursor_position: Anchor,
|
||||
_direction: Direction,
|
||||
_cx: &mut Context<Self>,
|
||||
) {
|
||||
}
|
||||
|
||||
fn accept(&mut self, _cx: &mut Context<Self>) {
|
||||
reset_completion_cache(self, _cx);
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ use alacritty_terminal::{
|
||||
use log::{info, warn};
|
||||
use regex::Regex;
|
||||
use std::{
|
||||
iter::{once, once_with},
|
||||
ops::{Index, Range},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
@@ -232,14 +233,17 @@ fn path_match<T>(
|
||||
(line_end.line.0 - line_start.line.0 + 1) as usize * term.grid().columns(),
|
||||
);
|
||||
let first_cell = &term.grid()[line_start];
|
||||
let mut prev_len = 0;
|
||||
line.push(first_cell.c);
|
||||
let mut start_offset = 0;
|
||||
let mut prev_char_is_space = first_cell.c == ' ';
|
||||
let mut hovered_point_byte_offset = None;
|
||||
let mut hovered_word_start_offset = None;
|
||||
let mut hovered_word_end_offset = None;
|
||||
|
||||
if !first_cell.flags.intersects(WIDE_CHAR_SPACERS) {
|
||||
start_offset += first_cell.c.len_utf8();
|
||||
if line_start == hovered {
|
||||
hovered_point_byte_offset = Some(0);
|
||||
if line_start == hovered {
|
||||
hovered_point_byte_offset = Some(0);
|
||||
if first_cell.c != ' ' {
|
||||
hovered_word_start_offset = Some(0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -247,27 +251,44 @@ fn path_match<T>(
|
||||
if cell.point > line_end {
|
||||
break;
|
||||
}
|
||||
let is_spacer = cell.flags.intersects(WIDE_CHAR_SPACERS);
|
||||
if cell.point == hovered {
|
||||
debug_assert!(hovered_point_byte_offset.is_none());
|
||||
if start_offset > 0 && cell.flags.contains(Flags::WIDE_CHAR_SPACER) {
|
||||
// If we hovered on a trailing spacer, back up to the end of the previous char's bytes.
|
||||
start_offset -= 1;
|
||||
|
||||
if !cell.flags.intersects(WIDE_CHAR_SPACERS) {
|
||||
prev_len = line.len();
|
||||
match cell.c {
|
||||
' ' | '\t' => {
|
||||
if hovered_point_byte_offset.is_some() && !prev_char_is_space {
|
||||
if hovered_word_end_offset.is_none() {
|
||||
hovered_word_end_offset = Some(line.len());
|
||||
}
|
||||
}
|
||||
line.push(' ');
|
||||
prev_char_is_space = true;
|
||||
}
|
||||
c @ _ => {
|
||||
if hovered_point_byte_offset.is_none() && prev_char_is_space {
|
||||
hovered_word_start_offset = Some(line.len());
|
||||
}
|
||||
line.push(c);
|
||||
prev_char_is_space = false;
|
||||
}
|
||||
}
|
||||
hovered_point_byte_offset = Some(start_offset);
|
||||
} else if cell.point < hovered && !is_spacer {
|
||||
start_offset += cell.c.len_utf8();
|
||||
}
|
||||
|
||||
if !is_spacer {
|
||||
line.push(match cell.c {
|
||||
'\t' => ' ',
|
||||
c @ _ => c,
|
||||
});
|
||||
if cell.point == hovered {
|
||||
debug_assert!(hovered_point_byte_offset.is_none());
|
||||
hovered_point_byte_offset = Some(prev_len);
|
||||
}
|
||||
}
|
||||
let line = line.trim_ascii_end();
|
||||
let hovered_point_byte_offset = hovered_point_byte_offset?;
|
||||
let hovered_word_range = {
|
||||
let word_start_offset = hovered_word_start_offset.unwrap_or(0);
|
||||
(word_start_offset != 0)
|
||||
.then_some(word_start_offset..hovered_word_end_offset.unwrap_or(line.len()))
|
||||
};
|
||||
if line.len() <= hovered_point_byte_offset {
|
||||
return None;
|
||||
}
|
||||
let found_from_range = |path_range: Range<usize>,
|
||||
link_range: Range<usize>,
|
||||
position: Option<(u32, Option<u32>)>| {
|
||||
@@ -313,10 +334,27 @@ fn path_match<T>(
|
||||
for regex in path_hyperlink_regexes {
|
||||
let mut path_found = false;
|
||||
|
||||
for captures in regex.captures_iter(&line) {
|
||||
for (line_start_offset, captures) in once(
|
||||
regex
|
||||
.captures_iter(&line)
|
||||
.next()
|
||||
.map(|captures| (0, captures)),
|
||||
)
|
||||
.chain(once_with(|| {
|
||||
if let Some(hovered_word_range) = &hovered_word_range {
|
||||
regex
|
||||
.captures_iter(&line[hovered_word_range.clone()])
|
||||
.next()
|
||||
.map(|captures| (hovered_word_range.start, captures))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}))
|
||||
.flatten()
|
||||
{
|
||||
path_found = true;
|
||||
let match_range = captures.get(0).unwrap().range();
|
||||
let (path_range, line_column) = if let Some(path) = captures.name("path") {
|
||||
let (mut path_range, line_column) = if let Some(path) = captures.name("path") {
|
||||
let parse = |name: &str| {
|
||||
captures
|
||||
.name(name)
|
||||
@@ -330,10 +368,15 @@ fn path_match<T>(
|
||||
} else {
|
||||
(match_range.clone(), None)
|
||||
};
|
||||
let link_range = captures
|
||||
let mut link_range = captures
|
||||
.name("link")
|
||||
.map_or_else(|| match_range.clone(), |link| link.range());
|
||||
|
||||
path_range.start += line_start_offset;
|
||||
path_range.end += line_start_offset;
|
||||
link_range.start += line_start_offset;
|
||||
link_range.end += line_start_offset;
|
||||
|
||||
if !link_range.contains(&hovered_point_byte_offset) {
|
||||
// No match, just skip.
|
||||
continue;
|
||||
@@ -638,9 +681,6 @@ mod tests {
|
||||
test_path!(
|
||||
"‹«🦀 multiple_👉same_line 🦀» 🚣«4» 🏛️«2»›: 🦀 multiple_same_line 🦀 🚣4 🏛️2:"
|
||||
);
|
||||
test_path!(
|
||||
"🦀 multiple_same_line 🦀 🚣4 🏛️2 ‹«🦀 multiple_👉same_line 🦀» 🚣«4» 🏛️«2»›:"
|
||||
);
|
||||
|
||||
// ls output (tab separated)
|
||||
test_path!(
|
||||
@@ -977,7 +1017,7 @@ mod tests {
|
||||
use crate::TerminalSettings;
|
||||
use alacritty_terminal::{
|
||||
event::VoidListener,
|
||||
grid::Dimensions,
|
||||
grid::Scroll,
|
||||
index::{Column, Point as AlacPoint},
|
||||
term::test::mock_term,
|
||||
term::{Term, search::Match},
|
||||
@@ -986,14 +1026,20 @@ mod tests {
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
use util_macros::perf;
|
||||
|
||||
fn build_test_term(line: &str) -> (Term<VoidListener>, AlacPoint) {
|
||||
let content = line.repeat(500);
|
||||
let term = mock_term(&content);
|
||||
let point = AlacPoint::new(
|
||||
term.grid().bottommost_line() - 1,
|
||||
Column(term.grid().last_column().0 / 2),
|
||||
);
|
||||
|
||||
fn build_test_term(
|
||||
line: &str,
|
||||
repeat: usize,
|
||||
hover_offset_column: usize,
|
||||
) -> (Term<VoidListener>, AlacPoint) {
|
||||
let content = line.repeat(repeat);
|
||||
let mut term = mock_term(&content);
|
||||
term.resize(TermSize {
|
||||
columns: 1024,
|
||||
screen_lines: 10,
|
||||
});
|
||||
term.scroll_display(Scroll::Top);
|
||||
let point =
|
||||
AlacPoint::new(Line(term.topmost_line().0 + 3), Column(hover_offset_column));
|
||||
(term, point)
|
||||
}
|
||||
|
||||
@@ -1002,11 +1048,14 @@ mod tests {
|
||||
const LINE: &str = " Compiling terminal v0.1.0 (/Hyperlinks/Bench/Source/zed-hyperlinks/crates/terminal)\r\n";
|
||||
thread_local! {
|
||||
static TEST_TERM_AND_POINT: (Term<VoidListener>, AlacPoint) =
|
||||
build_test_term(LINE);
|
||||
build_test_term(LINE, 500, 50);
|
||||
}
|
||||
TEST_TERM_AND_POINT.with(|(term, point)| {
|
||||
assert!(
|
||||
find_from_grid_point_bench(term, *point).is_some(),
|
||||
assert_eq!(
|
||||
find_from_grid_point_bench(term, *point)
|
||||
.map(|(path, ..)| path)
|
||||
.unwrap_or_default(),
|
||||
"/Hyperlinks/Bench/Source/zed-hyperlinks/crates/terminal",
|
||||
"Hyperlink should have been found"
|
||||
);
|
||||
});
|
||||
@@ -1017,11 +1066,14 @@ mod tests {
|
||||
const LINE: &str = " --> /Hyperlinks/Bench/Source/zed-hyperlinks/crates/terminal/terminal.rs:1000:42\r\n";
|
||||
thread_local! {
|
||||
static TEST_TERM_AND_POINT: (Term<VoidListener>, AlacPoint) =
|
||||
build_test_term(LINE);
|
||||
build_test_term(LINE, 500, 50);
|
||||
}
|
||||
TEST_TERM_AND_POINT.with(|(term, point)| {
|
||||
assert!(
|
||||
find_from_grid_point_bench(term, *point).is_some(),
|
||||
assert_eq!(
|
||||
find_from_grid_point_bench(term, *point)
|
||||
.map(|(path, ..)| path)
|
||||
.unwrap_or_default(),
|
||||
"/Hyperlinks/Bench/Source/zed-hyperlinks/crates/terminal/terminal.rs:1000:42",
|
||||
"Hyperlink should have been found"
|
||||
);
|
||||
});
|
||||
@@ -1032,11 +1084,111 @@ mod tests {
|
||||
const LINE: &str = "Cargo.toml experiments notebooks rust-toolchain.toml tooling\r\n";
|
||||
thread_local! {
|
||||
static TEST_TERM_AND_POINT: (Term<VoidListener>, AlacPoint) =
|
||||
build_test_term(LINE);
|
||||
build_test_term(LINE, 500, 60);
|
||||
}
|
||||
TEST_TERM_AND_POINT.with(|(term, point)| {
|
||||
assert!(
|
||||
find_from_grid_point_bench(term, *point).is_some(),
|
||||
assert_eq!(
|
||||
find_from_grid_point_bench(term, *point)
|
||||
.map(|(path, ..)| path)
|
||||
.unwrap_or_default(),
|
||||
"rust-toolchain.toml",
|
||||
"Hyperlink should have been found"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[perf]
|
||||
// https://github.com/zed-industries/zed/pull/44407
|
||||
pub fn pr_44407_hyperlink_benchmark() {
|
||||
const LINE: &str = "-748, 706, 163, 222, -980, 949, 381, -568, 199, 501, 760, -821, 90, -451, 183, 867, -351, -810, -762, -109, 423, 84, 14, -77, -820, -345, 74, -791, 930, -618, -900, 862, -959, 289, -19, 471, -757, 793, 155, -554, 249, 830, 402, 732, -731, -866, -720, -703, -257, -439, 731, 872, -489, 676, -167, 613, -698, 415, -80, -453, -896, 333, -511, 621, -450, 624, -309, -575, 177, 141, 891, -104, -97, -367, -599, -675, 607, -225, -760, 552, -465, 804, 55, 282, 104, -929, -252,\
|
||||
-311, 900, 550, 599, -80, 774, 553, 837, -395, 541, 953, 154, -396, -596, -111, -802, -221, -337, -633, -73, -527, -82, -658, -264, 222, 375, 434, 204, -756, -703, 303, 239, -257, -365, -351, 904, 364, -743, -484, 655, -542, 446, 888, 632, -167, -260, 716, 150, 806, 723, 513, -118, -323, -683, 983, -564, 358, -16, -287, 277, -607, 87, 365, -1, 164, 401, 257, 369, -893, 145, -969, 375, -53, 541, -408, -865, 753, 258, 337, -886, 593, -378, -528, 191, 204, 566, -61, -621, 769, 524, -628, 6,\
|
||||
249, 896, -785, -776, 321, -681, 604, -740, 886, 426, -480, -983, 23, -247, 125, -666, 913, 842, -460, -797, -483, -58, -565, -587, -206, 197, 715, 764, -97, 457, -149, -226, 261, 194, -390, 431, 180, -778, 829, -657, -668, 397, 859, 152, -178, 677, -18, 687, -247, 96, 466, -572, 478, 622, -143, -25, -471, 265, 335, 957, 152, -951, -647, 670, 57, 152, -115, 206, 87, 629, -798, -125, -725, -31, 844, 398, -876, 44, 963, -211, 518, -8, -103, -999, 948, 823, 149, -803, 769, -236, -683, 527,\
|
||||
-108, -36, 18, -437, 687, -305, -526, 972, -965, 276, 420, -259, -379, -142, -747, 600, -578, 197, 673, 890, 324, -931, 755, -765, -422, 785, -369, -110, -505, 532, -208, -438, 713, 110, 853, 996, -360, 823, 289, -699, 629, -661, 560, -329, -323, 439, 571, -537, 644, -84, 25, -536, -161, 112, 169, -922, -537, -734, -423, 37, 451, -149, 408, 18, -672, 206, -784, 444, 593, -241, 502, -259, -798, -352, -658, 712, -675, -734, 627, -620, 64, -554, 999, -537, -160, -641, 464, 894, 29, 322, 566,\
|
||||
-510, -749, 982, 204, 967, -261, -986, -136, 251, -598, 995, -831, 891, 22, 761, -783, -415, 125, 470, -919, -97, -668, 85, 205, -175, -550, 502, 652, -468, 798, 775, -216, 89, -433, -24, -621, 877, -126, 951, 809, 782, 156, -618, -841, -463, 19, -723, -904, 550, 263, 991, -758, -114, 446, -731, -623, -634, 462, 48, 851, 333, -846, 480, 892, -966, -910, -436, 317, -711, -341, -294, 124, 238, -214, -281, 467, -950, -342, 913, -90, -388, -573, 740, -883, -451, 493, -500, 863, 930, 127, 530,\
|
||||
-810, 540, 541, -664, -951, -227, -420, -476, -581, -534, 549, 253, 984, -985, -84, -521, 538, 484, -440, 371, 784, -306, -850, 530, -133, 251, -799, 446, -170, -243, -674, 769, 646, 778, -680, -714, -442, 804, 901, -774, 69, 307, -293, 755, 443, 224, -918, -771, 723, 40, 132, 568, -847, -47, 844, 69, 986, -293, -459, 313, 155, 331, 69, 280, -637, 569, 104, -119, -988, 252, 857, -590, 810, -891, 484, 566, -934, -587, -290, 566, 587, 489, 870, 280, 454, -252, 613, -701, -278, 195, -198,\
|
||||
683, 533, -372, 707, -152, 371, 866, 609, -5, -372, -30, -694, 552, 192, 452, -663, 350, -985, 10, 884, 813, -592, -331, -470, 711, -941, 928, 379, -339, 220, 999, 376, 507, 179, 916, 84, 104, 392, 192, 299, -860, 218, -698, -919, -452, 37, 850, 5, -874, 287, 123, -746, -575, 776, -909, 118, 903, -275, 450, -996, -591, -920, -850, 453, -896, 73, 83, -535, -20, 287, -765, 442, 808, 45, 445, 202, 917, -208, 783, 790, -534, 373, -129, 556, -757, -69, 459, -163, -59, 265, -563, -889, 635,\
|
||||
-583, -261, -790, 799, 826, 953, 85, 619, 334, 842, 672, -869, -4, -833, 315, 942, -524, 579, 926, 628, -404, 128, -629, 161, 568, -117, -526, 223, -876, 906, 176, -549, -317, 381, 375, -801, -416, 647, 335, 253, -386, -375, -254, 635, 352, 317, 398, -422, 111, 201, 220, 554, -972, 853, 378, 956, 942, -857, -289, -333, -180, 488, -814, -42, -595, 721, 39, 644, 721, -242, -44, 643, -457, -419, 560, -863, 974, 458, 222, -882, 526, -243, -318, -343, -707, -401, 117, 677, -489, 546, -903,\
|
||||
-960, -881, -684, 125, -928, -995, -692, -773, 647, -718, -862, -814, 671, 664, -130, -856, -674, 653, 711, 194, -685, -160, 138, -27, -128, -671, -242, 526, 494, -674, 424, -921, -778, 313, -237, 332, 913, 252, 808, -936, 289, 755, 52, -139, 57, -19, -827, -775, -561, -14, 107, -84, 622, -303, -747, 258, -942, 290, 211, -919, -207, 797, 95, 794, -830, -181, -788, 757, 75, -946, -949, -988, 152, 340, 732, 886, -891, -642, -666, 321, -910, 841, 632, 298, 55, -349, 498, 287, -711, 97, 305,\
|
||||
-974, -987, 790, -64, 605, -583, -821, 345, 887, -861, 548, 894, 288, 452, 556, -448, 813, 420, 545, 967, 127, -947, 19, -314, -607, -513, -851, 254, -290, -938, -783, -93, 474, 368, -485, -935, -539, 81, 404, -283, 779, 345, -164, 53, 563, -771, 911, -323, 522, -998, 315, 415, 460, 58, -541, -878, -152, -886, 201, -446, -810, 549, -142, -575, -632, 521, 549, 209, -681, 998, 798, -611, -919, -708, -4, 677, -172, 588, 750, -435, 508, 609, 498, -535, -691, -738, 85, 615, 705, 169, 425,\
|
||||
-669, -491, -783, 73, -847, 228, -981, -812, -229, 950, -904, 175, -438, 632, -556, 910, 173, 576, -751, -53, -169, 635, 607, -944, -13, -84, 105, -644, 984, 935, 259, -445, 620, -405, 832, 167, 114, 209, -181, -944, -496, 693, -473, 137, 38, -873, -334, -353, -57, 397, 944, 698, 811, -401, 712, -667, 905, 276, -653, 368, -543, -349, 414, 287, 894, 935, 461, 55, 741, -623, -660, -773, 617, 834, 278, -121, 52, 495, -855, -440, -210, -99, 279, -661, 540, 934, 540, 784, 895, 268, -503, 513,\
|
||||
-484, -352, 528, 341, -451, 885, -71, 799, -195, -885, -585, -233, 92, 453, 994, 464, 694, 190, -561, -116, 675, -775, -236, 556, -110, -465, 77, -781, 507, -960, -410, 229, -632, 717, 597, 429, 358, -430, -692, -825, 576, 571, 758, -891, 528, -267, 190, -869, 132, -811, 796, 750, -596, -681, 870, 360, 969, 860, -412, -567, 694, -86, -498, 38, -178, -583, -778, 412, 842, -586, 722, -192, 350, 363, 81, -677, -163, 564, 543, 671, 110, 314, 739, -552, -224, -644, 922, 685, 134, 613, 793,\
|
||||
-363, -244, -284, -257, -561, 418, 988, 333, 110, -966, 790, 927, 536, -620, -309, -358, 895, -867, -796, -357, 308, -740, 287, -732, -363, -969, 658, 711, 511, 256, 590, -574, 815, -845, -84, 546, -581, -71, -334, -890, 652, -959, 320, -236, 445, -851, 825, -756, -4, 877, 308, 573, -117, 293, 686, -483, 391, 342, -550, -982, 713, 886, 552, 474, -673, 283, -591, -383, 988, 435, -131, 708, -326, -884, 87, 680, -818, -408, -486, 813, -307, -799, 23, -497, 802, -146, -100, 541, 7, -493, 577,\
|
||||
50, -270, 672, 834, 111, -788, 247, 337, 628, -33, -964, -519, 683, 54, -703, 633, -127, -448, 759, -975, 696, 2, -870, -760, 67, 696, 306, 750, 615, 155, -933, -568, 399, 795, 164, -460, 205, 439, -526, -691, 35, -136, -481, -63, 73, -598, 748, 133, 874, -29, 4, -73, 472, 389, 962, 231, -328, 240, 149, 959, 46, -207, 72, -514, -608, 0, -14, 32, 374, -478, -806, 919, -729, -286, 652, 109, 509, -879, -979, -865, 584, -92, -346, -992, 781, 401, 575, 993, -746, -33, 684, -683, 750, -105,\
|
||||
-425, -508, -627, 27, 770, -45, 338, 921, -139, -392, -933, 634, 563, 224, -780, 921, 991, 737, 22, 64, 414, -249, -687, 869, 50, 759, -97, 515, 20, -775, -332, 957, 138, -542, -835, 591, -819, 363, -715, -146, -950, -641, -35, -435, -407, -548, -984, 383, -216, -559, 853, 4, -410, -319, -831, -459, -628, -819, -324, 755, 696, -192, 238, -234, -724, -445, 915, 302, -708, 484, 224, -641, 25, -771, 528, -106, -744, -588, 913, -554, -515, -239, -843, -812, -171, 721, 543, -269, 440, 151,\
|
||||
996, -723, -557, -522, -280, -514, -593, 208, 715, 404, 353, 270, -483, -785, 318, -313, 798, 638, 764, 748, -929, -827, -318, -56, 389, -546, -958, -398, 463, -700, 461, 311, -787, -488, 877, 456, 166, 535, -995, -189, -715, 244, 40, 484, 212, -329, -351, 638, -69, -446, -292, 801, -822, 490, -486, -185, 790, 370, -340, 401, -656, 584, 561, -749, 269, -19, -294, -111, 975, 874, -73, 851, 231, -331, -684, 460, 765, -654, -76, 10, 733, 520, 521, 416, -958, -202, -186, -167, 175, 343, -50,\
|
||||
673, -763, -854, -977, -17, -853, -122, -25, 180, 149, 268, 874, -816, -745, 747, -303, -959, 390, 509, 18, -66, 275, -277, 9, 837, -124, 989, -542, -649, -845, 894, 926, 997, -847, -809, -579, -96, -372, 766, 238, -251, 503, 559, 276, -281, -102, -735, 815, 109, 175, -10, 128, 543, -558, -707, 949, 996, -422, -506, 252, 702, -930, 552, -961, 584, -79, -177, 341, -275, 503, -21, 677, -545, 8, -956, -795, -870, -254, 170, -502, -880, 106, 174, 459, 603, -600, -963, 164, -136, -641, -309,\
|
||||
-380, -707, -727, -10, 727, 952, 997, -731, -133, 269, 287, 855, 716, -650, 479, 299, -839, -308, -782, 769, 545, 663, -536, -115, 904, -986, -258, -562, 582, 664, 408, -525, -889, 471, -370, -534, -220, 310, 766, 931, -193, -897, -192, -74, -365, -256, -359, -328, 658, -691, -431, 406, 699, 425, 713, -584, -45, -588, 289, 658, -290, -880, -987, -444, 371, 904, -155, 81, -278, -708, -189, -78, 655, 342, -998, -647, -734, -218, 726, 619, 663, 744, 518, 60, -409, 561, -727, -961, -306,\
|
||||
-147, -550, 240, -218, -393, 267, 724, 791, -548, 480, 180, -631, 825, -170, 107, 227, -691, 905, -909, 359, 227, 287, 909, 632, -89, -522, 80, -429, 37, 561, -732, -474, 565, -798, -460, 188, 507, -511, -654, 212, -314, -376, -997, -114, -708, 512, -848, 781, 126, -956, -298, 354, -400, -121, 510, 445, 926, 27, -708, 676, 248, 834, 542, 236, -105, -153, 102, 128, 96, -348, -626, 598, 8, 978, -589, -461, -38, 381, -232, -817, 467, 356, -151, -460, 429, -408, 425, 618, -611, -247, 819,\
|
||||
963, -160, 1000, 141, -647, -875, 108, 790, -127, 463, -37, -195, -542, 12, 845, -384, 770, -129, 315, 826, -942, 430, 146, -170, -583, -903, -489, 497, -559, -401, -29, -129, -411, 166, 942, -646, -862, -404, 785, 777, -111, -481, -738, 490, 741, -398, 846, -178, -509, -661, 748, 297, -658, -567, 531, 427, -201, -41, -808, -668, 782, -860, -324, 249, 835, -234, 116, 542, -201, 328, 675, 480, -906, 188, 445, 63, -525, 811, 277, 133, 779, -680, 950, -477, -306, -64, 552, -890, -956, 169,\
|
||||
442, 44, -169, -243, -242, 423, -884, -757, -403, 739, -350, 383, 429, 153, -702, -725, 51, 310, 857, -56, 538, 46, -311, 132, -620, -297, -124, 534, 884, -629, -117, 506, -837, -100, -27, -381, -735, 262, 843, 703, 260, -457, 834, 469, 9, 950, 59, 127, -820, 518, 64, -783, 659, -608, -676, 802, 30, 589, 246, -369, 361, 347, 534, -376, 68, 941, 709, 264, 384, 481, 628, 199, -568, -342, -337, 853, -804, -858, -169, -270, 641, -344, 112, 530, -773, -349, -135, -367, -350, -756, -911, 180,\
|
||||
-660, 116, -478, -265, -581, 510, 520, -986, 935, 219, 522, 744, 47, -145, 917, 638, 301, 296, 858, -721, 511, -816, 328, 473, 441, 697, -260, -673, -379, 893, 458, 154, 86, 905, 590, 231, -717, -179, 79, 272, -439, -192, 178, -200, 51, 717, -256, -358, -626, -518, -314, -825, -325, 588, 675, -892, -798, 448, -518, 603, -23, 668, -655, 845, -314, 783, -347, -496, 921, 893, -163, -748, -906, 11, -143, -64, 300, 336, 882, 646, 533, 676, -98, -148, -607, -952, -481, -959, -874, 764, 537,\
|
||||
736, -347, 646, -843, 966, -916, -718, -391, -648, 740, 755, 919, -608, 388, -655, 68, 201, 675, -855, 7, -503, 881, 760, 669, 831, 721, -564, -445, 217, 331, 970, 521, 486, -254, 25, -259, 336, -831, 252, -995, 908, -412, -240, 123, -478, 366, 264, -504, -843, 632, -288, 896, 301, 423, 185, 318, 380, 457, -450, -162, -313, 673, -963, 570, 433, -548, 107, -39, -142, -98, -884, -3, 599, -486, -926, 923, -82, 686, 290, 99, -382, -789, 16, 495, 570, 284, 474, -504, -201, -178, -1, 592, 52,\
|
||||
827, -540, -151, -991, 130, 353, -420, -467, -661, 417, -690, 942, 936, 814, -566, -251, -298, 341, -139, 786, 129, 525, -861, 680, 955, -245, -50, 331, 412, -38, -66, 611, -558, 392, -629, -471, -68, -535, 744, 495, 87, 558, 695, 260, -308, 215, -464, 239, -50, 193, -540, 184, -8, -194, 148, 898, -557, -21, 884, 644, -785, -689, -281, -737, 267, 50, 206, 292, 265, 380, -511, 310, 53, 375, -497, -40, 312, -606, -395, 142, 422, 662, -584, 72, 144, 40, -679, -593, 581, 689, -829, 442, 822,\
|
||||
977, -832, -134, -248, -207, 248, 29, 259, 189, 592, -834, -866, 102, 0, 340, 25, -354, -239, 420, -730, -992, -925, -314, 420, 914, 607, -296, -415, -30, 813, 866, 153, -90, 150, -81, 636, -392, -222, -835, 482, -631, -962, -413, -727, 280, 686, -382, 157, -404, -511, -432, 455, 58, 108, -408, 290, -829, -252, 113, 550, -935, 925, 422, 38, 789, 361, 487, -460, -769, -963, -285, 206, -799, -488, -233, 416, 143, -456, 753, 520, 599, 621, -168, 178, -841, 51, 952, 374, 166, -300, -576, 844,\
|
||||
-656, 90, 780, 371, 730, -896, -895, -386, -662, 467, -61, 130, -362, -675, -113, 135, -761, -55, 408, 822, 675, -347, 725, 114, 952, -510, -972, 390, -413, -277, -52, 315, -80, 401, -712, 147, -202, 84, 214, -178, 970, -571, -210, 525, -887, -863, 504, 192, 837, -594, 203, -876, -209, 305, -826, 377, 103, -928, -803, -956, 949, -868, -547, 824, -994, 516, 93, -524, -866, -890, -988, -501, 15, -6, 413, -825, 304, -818, -223, 525, 176, 610, 828, 391, 940, 540, -831, 650, 438, 589, 941, 57,\
|
||||
523, 126, 221, 860, -282, -262, -226, 764, 743, -640, 390, 384, -434, 608, -983, 566, -446, 618, 456, -176, -278, 215, 871, -180, 444, -931, -200, -781, 404, 881, 780, -782, 517, -739, -548, -811, 201, -95, -249, -228, 491, -299, 700, 964, -550, 108, 334, -653, 245, -293, -552, 350, -685, -415, -818, 216, -194, -255, 295, 249, 408, 351, 287, 379, 682, 231, -693, 902, -902, 574, 937, -708, -402, -460, 827, -268, 791, 343, -780, -150, -738, 920, -430, -88, -361, -588, -727, -47, -297, 662,\
|
||||
-840, -637, -635, 916, -857, 938, 132, -553, 391, -522, 640, 626, 690, 833, 867, -555, 577, 226, 686, -44, 0, -965, 651, -1, 909, 595, -646, 740, -821, -648, -962, 927, -193, 159, 490, 594, -189, 707, -884, 759, -278, -160, -566, -340, 19, 862, -440, 445, -598, 341, 664, -311, 309, -159, 19, -672, 705, -646, 976, 247, 686, -830, -27, -667, 81, 399, -423, -567, 945, 38, 51, 740, 621, 204, -199, -908, -593, 424, 250, -561, 695, 9, 520, 878, 120, -109, 42, -375, -635, -711, -687, 383, -278,\
|
||||
36, 970, 925, 864, 836, 309, 117, 89, 654, -387, 346, -53, 617, -164, -624, 184, -45, 852, 498, -513, 794, -682, -576, 13, -147, 285, -776, -886, -96, 483, 994, -188, 346, -629, -848, 738, 51, 128, -898, -753, -906, 270, -203, -577, 48, -243, -210, 666, 353, 636, -954, 862, 560, -944, -877, -137, 440, -945, -316, 274, -211, -435, 615, -635, -468, 744, 948, -589, 525, 757, -191, -431, 42, 451, -160, -827, -991, 324, 697, 342, -610, 894, -787, -384, 872, 734, 878, 70, -260, 57, 397, -518,\
|
||||
629, -510, -94, 207, 214, -625, 106, -882, -575, 908, -650, 723, -154, 45, 108, -69, -565, 927, -68, -351, 707, -282, 429, -889, -596, 848, 578, -492, 41, -822, -992, 168, -286, -780, 970, 597, -293, -12, 367, 708, -415, 194, -86, -390, 224, 69, -368, -674, 1000, -672, 356, -202, -169, 826, 476, -285, 29, -448, 545, 186, 319, 67, 705, 412, 225, -212, -351, -391, -783, -9, 875, -59, -159, -123, -151, -296, 871, -638, 359, 909, -945, 345, -16, -562, -363, -183, -625, -115, -571, -329, 514,\
|
||||
99, 263, 463, -39, 597, -652, -349, 246, 77, -127, -563, -879, -30, 756, 777, -865, 675, -813, -501, 871, -406, -627, 834, -609, -205, -812, 643, -204, 291, -251, -184, -584, -541, 410, -573, -600, 908, -871, -687, 296, -713, -139, -778, -790, 347, -52, -400, 407, -653, 670, 39, -856, 904, 433, 392, 590, -271, -144, -863, 443, 353, 468, -544, 486, -930, 458, -596, -890, 163, 822, 768, 980, -783, -792, 126, 386, 367, -264, 603, -61, 728, 160, -4, -837, 832, 591, 436, 518, 796, -622, -867,\
|
||||
-669, -947, 253, 100, -792, 841, 413, 833, -249, -550, 282, -825, 936, -348, 898, -451, -283, 818, -237, 630, 216, -499, -637, -511, 767, -396, 221, 958, -586, -920, 401, -313, -580, -145, -270, 118, 497, 426, -975, 480, -445, -150, -721, -929, 439, -893, 902, 960, -525, -793, 924, 563, 683, -727, -86, 309, 432, -762, -345, 371, -617, 149, -215, -228, 505, 593, -20, -292, 704, -999, 149, -104, 819, -414, -443, 517, -599, -5, 145, -24, -993, -283, 904, 174, -112, -276, -860, 44, -257,\
|
||||
-931, -821, -667, 540, 421, 485, 531, 407, 833, 431, -415, 878, 503, -901, 639, -608, 896, 860, 927, 424, 113, -808, -323, 729, 382, -922, 548, -791, -379, 207, 203, 559, 537, 137, 999, -913, -240, 942, 249, 616, 775, -4, 915, 855, -987, -234, -384, 948, -310, -542, 125, -289, -599, 967, -492, -349, -552, 562, -926, 632, -164, 217, -165, -496, 847, 684, -884, 457, -748, -745, -38, 93, 961, 934, 588, 366, -130, 851, -803, -811, -211, 428, 183, -469, 888, 596, -475, -899, -681, 508, 184,\
|
||||
921, 863, -610, -416, -119, -966, -686, 210, 733, 715, -889, -925, -434, -566, -455, 596, -514, 983, 755, -194, -802, -313, 91, -541, 808, -834, 243, -377, 256, 966, -402, -773, -308, -605, 266, 866, 118, -425, -531, 498, 666, 813, -267, 830, 69, -869, -496, 735, 28, 488, -645, -493, -689, 170, -940, 532, 844, -658, -617, 408, -200, 764, -665, 568, 342, 621, 908, 471, 280, 859, 709, 898, 81, -547, 406, 514, -595, 43, -824, -696, -746, -429, -59, -263, -813, 233, 279, -125, 687, -418,\
|
||||
-530, 409, 614, 803, -407, 78, -676, -39, -887, -141, -292, 270, -343, 400, 907, 588, 668, 899, 973, 103, -101, -11, 397, -16, 165, 705, -410, -585, 316, 391, -346, -336, 957, -118, -538, -441, -845, 121, 591, -359, -188, -362, -208, 27, -925, -157, -495, -177, -580, 9, 531, -752, 94, 107, 820, 769, -500, 852, 617, 145, 355, 34, -463, -265, -709, -111, -855, -405, 560, 470, 3, -177, -164, -249, 450, 662, 841, -689, -509, 987, -33, 769, 234, -2, 203, 780, 744, -895, 497, -432, -406, -264,\
|
||||
-71, 124, 778, -897, 495, 127, -76, 52, -768, 205, 464, -992, 801, -83, -806, 545, -316, 146, 772, 786, 289, -936, 145, -30, -722, -455, 270, 444, 427, -482, 383, -861, 36, 630, -404, 83, 864, 743, -351, -846, 315, -837, 357, -195, 450, -715, 227, -942, 740, -519, 476, 716, 713, 169, 492, -112, -49, -931, 866, 95, -725, 198, -50, -17, -660, 356, -142, -781, 53, 431, 720, 143, -416, 446, -497, 490, -96, 157, 239, 487, -337, -224, -445, 813, 92, -22, 603, 424, 952, -632, -367, 898, -927,\
|
||||
884, -277, -187, -777, 537, -575, -313, 347, -33, 800, 672, -919, -541, 5, -270, -94, -265, -793, -183, -761, -516, -608, -218, 57, -889, -912, 508, 93, -90, 34, 530, 201, 999, -37, -186, -62, -980, 239, 902, 983, -287, -634, 524, -772, 470, -961, 32, 162, 315, -411, 400, -235, -283, -787, -703, 869, 792, 543, -274, 239, 733, -439, 306, 349, 579, -200, -201, -824, 384, -246, 133, -508, 770, -102, 957, -825, 740, 748, -376, 183, -426, 46, 668, -886, -43, -174, 672, -419, 390, 927, 1000,\
|
||||
318, 886, 47, 908, -540, -825, -5, 314, -999, 354, -603, 966, -633, -689, 985, 534, -290, 167, -652, -797, -612, -79, 488, 622, -464, -950, 595, 897, 704, -238, -395, 125, 831, -180, 226, -379, 310, 564, 56, -978, 895, -61, 686, -251, 434, -417, 161, -512, 752, 528, -589, -425, 66, -925, -157, 1000, 96, 256, -239, -784, -882, -464, -909, 663, -177, -678, -441, 669, -564, -201, -121, -743, 187, -107, -768, -682, 355, 161, 411, 984, -954, 166, -842, -755, 267, -709, 372, -699, -272, -850,\
|
||||
403, -839, 949, 622, -62, 51, 917, 70, 528, -558, -632, 832, 276, 61, -445, -195, 960, 846, -474, 764, 879, -411, 948, -62, -592, -123, -96, -551, -555, -724, 849, 250, -808, -732, 797, -839, -554, 306, -919, 888, 484, -728, 152, -122, -287, 16, -345, -396, -268, -963, -500, 433, 343, 418, -480, 828, 594, 821, -9, 933, -230, 707, -847, -610, -748, -234, 688, 935, 713, 865, -743, 293, -143, -20, 928, -906, -762, 528, 722, 412, -70, 622, -245, 539, -686, 730, -866, -705, 28, -916, -623,\
|
||||
-768, -614, -915, -123, -183, 680, -223, 515, -37, -235, -5, 260, 347, -239, -322, -861, -848, -936, 945, 721, -580, -639, 780, -153, -26, 685, 177, 587, 307, -915, 435, 658, 539, -229, -719, -171, -858, 162, 734, -539, -437, 246, 639, 765, -477, -342, -209, -284, -779, -414, -452, 914, 338, -83, 759, 567, 266, -485, 14, 225, 347, -432, -242, 997, -365, -764, 119, -641, -416, -388, -436, -388, -54, -649, -571, -920, -477, 714, -363, 836, 369, 702, 869, 503, -287, -679, 46, -666, -202,\
|
||||
-602, 71, -259, 967, 601, -571, -830, -993, -271, 281, -494, 482, -180, 572, 587, -651, -566, -448, -228, 511, -924, 832, -52, -712, 402, -644, -533, -865, 269, 965, 56, 675, 179, -338, -272, 614, 602, -283, 303, -70, 909, -942, 117, 839, 468, 813, -765, 884, -697, -813, 352, 374, -705, -295, 633, 211, -754, 597, -941, -142, -393, -469, -653, 688, 996, 911, 214, 431, 453, -141, 874, -81, -258, -735, -3, -110, -338, -929, -182, -306, -104, -840, -588, -759, -157, -801, 848, -698, 627, 914,\
|
||||
-33, -353, 425, 150, -798, 553, 934, -778, -196, -132, 808, 745, -894, 144, 213, 662, 273, -79, 454, -60, -467, 48, -15, -807, 69, -930, 749, 559, -867, -103, 258, -677, 750, -303, 846, -227, -936, 744, -770, 770, -434, 594, -477, 589, -612, 535, 357, -623, 683, 369, 905, 980, -410, -663, 762, -888, -563, -845, 843, 353, -491, 996, -255, -336, -132, 695, -823, 289, -143, 365, 916, 877, 245, -530, -848, -804, -118, -108, 847, 620, -355, 499, 881, 92, -640, 542, 38, 626, -260, -34, -378,\
|
||||
598, 890, 305, -118, 711, -385, 600, -570, 27, -129, -893, 354, 459, 374, 816, 470, 356, 661, 877, 735, -286, -780, 620, 943, -169, -888, 978, 441, -667, -399, 662, 249, 137, 598, -863, -453, 722, -815, -251, -995, -294, -707, 901, 763, 977, 137, 431, -994, 905, 593, 694, 444, -626, -816, 252, 282, 616, 841, 360, -932, 817, -908, 50, 394, -120, -786, -338, 499, -982, -95, -454, 838, -312, 320, -127, -653, 53, 16, 988, -968, -151, -369, -836, 293, -271, 483, 18, 724, -204, -965, 245, 310,\
|
||||
987, 552, -835, -912, -861, 254, 560, 124, 145, 798, 178, 476, 138, -311, 151, -907, -886, -592, 728, -43, -489, 873, -422, -439, -489, 375, -703, -459, 338, 418, -25, 332, -454, 730, -604, -800, 37, -172, -197, -568, -563, -332, 228, -182, 994, -123, 444, -567, 98, 78, 0, -504, -150, 88, -936, 199, -651, -776, 192, 46, 526, -727, -991, 534, -659, -738, 256, -894, 965, -76, 816, 435, -418, 800, 838, 67, -733, 570, 112, -514, -416\r\
|
||||
";
|
||||
thread_local! {
|
||||
static TEST_TERM_AND_POINT: (Term<VoidListener>, AlacPoint) =
|
||||
build_test_term(&LINE, 5, 50);
|
||||
}
|
||||
TEST_TERM_AND_POINT.with(|(term, point)| {
|
||||
assert_eq!(
|
||||
find_from_grid_point_bench(term, *point)
|
||||
.map(|(path, ..)| path)
|
||||
.unwrap_or_default(),
|
||||
"392",
|
||||
"Hyperlink should have been found"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[perf]
|
||||
// https://github.com/zed-industries/zed/issues/44510
|
||||
pub fn issue_44510_hyperlink_benchmark() {
|
||||
const LINE: &str = "..............................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................\
|
||||
..............................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................\
|
||||
..............................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................\
|
||||
..............................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................\
|
||||
..............................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................\
|
||||
..............................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................\
|
||||
..............................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................\
|
||||
..............................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................\
|
||||
..............................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................\
|
||||
..............................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................\
|
||||
..............................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................\
|
||||
..............................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................\
|
||||
..............................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................\
|
||||
..............................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................\
|
||||
...............................................E.\r\
|
||||
";
|
||||
thread_local! {
|
||||
static TEST_TERM_AND_POINT: (Term<VoidListener>, AlacPoint) =
|
||||
build_test_term(&LINE, 5, 50);
|
||||
}
|
||||
TEST_TERM_AND_POINT.with(|(term, point)| {
|
||||
assert_eq!(
|
||||
find_from_grid_point_bench(term, *point)
|
||||
.map(|(path, ..)| path)
|
||||
.unwrap_or_default(),
|
||||
LINE.trim_end_matches(['.', '\r', '\n']),
|
||||
"Hyperlink should have been found"
|
||||
);
|
||||
});
|
||||
|
||||
@@ -8,8 +8,8 @@ mod terminal_slash_command;
|
||||
use assistant_slash_command::SlashCommandRegistry;
|
||||
use editor::{EditorSettings, actions::SelectAll, blink_manager::BlinkManager};
|
||||
use gpui::{
|
||||
Action, AnyElement, App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
|
||||
KeyContext, KeyDownEvent, Keystroke, MouseButton, MouseDownEvent, Pixels, Render,
|
||||
Action, AnyElement, App, ClipboardEntry, DismissEvent, Entity, EventEmitter, FocusHandle,
|
||||
Focusable, KeyContext, KeyDownEvent, Keystroke, MouseButton, MouseDownEvent, Pixels, Render,
|
||||
ScrollWheelEvent, Styled, Subscription, Task, WeakEntity, actions, anchored, deferred, div,
|
||||
};
|
||||
use persistence::TERMINAL_DB;
|
||||
@@ -687,10 +687,30 @@ impl TerminalView {
|
||||
|
||||
///Attempt to paste the clipboard into the terminal
|
||||
fn paste(&mut self, _: &Paste, _: &mut Window, cx: &mut Context<Self>) {
|
||||
if let Some(clipboard_string) = cx.read_from_clipboard().and_then(|item| item.text()) {
|
||||
self.terminal
|
||||
.update(cx, |terminal, _cx| terminal.paste(&clipboard_string));
|
||||
let Some(clipboard) = cx.read_from_clipboard() else {
|
||||
return;
|
||||
};
|
||||
|
||||
if clipboard.entries().iter().any(|entry| match entry {
|
||||
ClipboardEntry::Image(image) => !image.bytes.is_empty(),
|
||||
_ => false,
|
||||
}) {
|
||||
self.forward_ctrl_v(cx);
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(text) = clipboard.text() {
|
||||
self.terminal
|
||||
.update(cx, |terminal, _cx| terminal.paste(&text));
|
||||
}
|
||||
}
|
||||
|
||||
/// Emits a raw Ctrl+V so TUI agents can read the OS clipboard directly
|
||||
/// and attach images using their native workflows.
|
||||
fn forward_ctrl_v(&self, cx: &mut Context<Self>) {
|
||||
self.terminal.update(cx, |term, _| {
|
||||
term.input(vec![0x16]);
|
||||
});
|
||||
}
|
||||
|
||||
fn send_text(&mut self, text: &SendText, _: &mut Window, cx: &mut Context<Self>) {
|
||||
|
||||
@@ -330,10 +330,12 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
|
||||
let Some(range) = range.buffer_range(vim, editor, window, cx).ok() else {
|
||||
return;
|
||||
};
|
||||
let Some((line_ending, text, whole_buffer)) = editor.buffer().update(cx, |multi, cx| {
|
||||
let Some((line_ending, encoding, has_bom, text, whole_buffer)) = editor.buffer().update(cx, |multi, cx| {
|
||||
Some(multi.as_singleton()?.update(cx, |buffer, _| {
|
||||
(
|
||||
buffer.line_ending(),
|
||||
buffer.encoding(),
|
||||
buffer.has_bom(),
|
||||
buffer.as_rope().slice_rows(range.start.0..range.end.0 + 1),
|
||||
range.start.0 == 0 && range.end.0 + 1 >= buffer.row_count(),
|
||||
)
|
||||
@@ -429,7 +431,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
|
||||
return;
|
||||
};
|
||||
worktree
|
||||
.write_file(path.into_arc(), text.clone(), line_ending, cx)
|
||||
.write_file(path.into_arc(), text.clone(), line_ending, encoding, has_bom, cx)
|
||||
.detach_and_prompt_err("Failed to write lines", window, cx, |_, _, _| None);
|
||||
});
|
||||
})
|
||||
|
||||
23
crates/which_key/Cargo.toml
Normal file
23
crates/which_key/Cargo.toml
Normal file
@@ -0,0 +1,23 @@
|
||||
[package]
|
||||
name = "which_key"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
publish.workspace = true
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/which_key.rs"
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
command_palette.workspace = true
|
||||
gpui.workspace = true
|
||||
serde.workspace = true
|
||||
settings.workspace = true
|
||||
theme.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
workspace.workspace = true
|
||||
1
crates/which_key/LICENSE-GPL
Symbolic link
1
crates/which_key/LICENSE-GPL
Symbolic link
@@ -0,0 +1 @@
|
||||
../../LICENSE-GPL
|
||||
98
crates/which_key/src/which_key.rs
Normal file
98
crates/which_key/src/which_key.rs
Normal file
@@ -0,0 +1,98 @@
|
||||
//! Which-key support for Zed.
|
||||
|
||||
mod which_key_modal;
|
||||
mod which_key_settings;
|
||||
|
||||
use gpui::{App, Keystroke};
|
||||
use settings::Settings;
|
||||
use std::{sync::LazyLock, time::Duration};
|
||||
use util::ResultExt;
|
||||
use which_key_modal::WhichKeyModal;
|
||||
use which_key_settings::WhichKeySettings;
|
||||
use workspace::Workspace;
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
WhichKeySettings::register(cx);
|
||||
|
||||
cx.observe_new(|_: &mut Workspace, window, cx| {
|
||||
let Some(window) = window else {
|
||||
return;
|
||||
};
|
||||
let mut timer = None;
|
||||
cx.observe_pending_input(window, move |workspace, window, cx| {
|
||||
if window.pending_input_keystrokes().is_none() {
|
||||
if let Some(modal) = workspace.active_modal::<WhichKeyModal>(cx) {
|
||||
modal.update(cx, |modal, cx| modal.dismiss(cx));
|
||||
};
|
||||
timer.take();
|
||||
return;
|
||||
}
|
||||
|
||||
let which_key_settings = WhichKeySettings::get_global(cx);
|
||||
if !which_key_settings.enabled {
|
||||
return;
|
||||
}
|
||||
|
||||
let delay_ms = which_key_settings.delay_ms;
|
||||
|
||||
timer.replace(cx.spawn_in(window, async move |workspace_handle, cx| {
|
||||
cx.background_executor()
|
||||
.timer(Duration::from_millis(delay_ms))
|
||||
.await;
|
||||
workspace_handle
|
||||
.update_in(cx, |workspace, window, cx| {
|
||||
if workspace.active_modal::<WhichKeyModal>(cx).is_some() {
|
||||
return;
|
||||
};
|
||||
|
||||
workspace.toggle_modal(window, cx, |window, cx| {
|
||||
WhichKeyModal::new(workspace_handle.clone(), window, cx)
|
||||
});
|
||||
})
|
||||
.log_err();
|
||||
}));
|
||||
})
|
||||
.detach();
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
// Hard-coded list of keystrokes to filter out from which-key display
|
||||
pub static FILTERED_KEYSTROKES: LazyLock<Vec<Vec<Keystroke>>> = LazyLock::new(|| {
|
||||
[
|
||||
// Modifiers on normal vim commands
|
||||
"g h",
|
||||
"g j",
|
||||
"g k",
|
||||
"g l",
|
||||
"g $",
|
||||
"g ^",
|
||||
// Duplicate keys with "ctrl" held, e.g. "ctrl-w ctrl-a" is duplicate of "ctrl-w a"
|
||||
"ctrl-w ctrl-a",
|
||||
"ctrl-w ctrl-c",
|
||||
"ctrl-w ctrl-h",
|
||||
"ctrl-w ctrl-j",
|
||||
"ctrl-w ctrl-k",
|
||||
"ctrl-w ctrl-l",
|
||||
"ctrl-w ctrl-n",
|
||||
"ctrl-w ctrl-o",
|
||||
"ctrl-w ctrl-p",
|
||||
"ctrl-w ctrl-q",
|
||||
"ctrl-w ctrl-s",
|
||||
"ctrl-w ctrl-v",
|
||||
"ctrl-w ctrl-w",
|
||||
"ctrl-w ctrl-]",
|
||||
"ctrl-w ctrl-shift-w",
|
||||
"ctrl-w ctrl-g t",
|
||||
"ctrl-w ctrl-g shift-t",
|
||||
]
|
||||
.iter()
|
||||
.filter_map(|s| {
|
||||
let keystrokes: Result<Vec<_>, _> = s
|
||||
.split(' ')
|
||||
.map(|keystroke_str| Keystroke::parse(keystroke_str))
|
||||
.collect();
|
||||
keystrokes.ok()
|
||||
})
|
||||
.collect()
|
||||
});
|
||||
308
crates/which_key/src/which_key_modal.rs
Normal file
308
crates/which_key/src/which_key_modal.rs
Normal file
@@ -0,0 +1,308 @@
|
||||
//! Modal implementation for the which-key display.
|
||||
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
App, Context, DismissEvent, EventEmitter, FocusHandle, Focusable, FontWeight, Keystroke,
|
||||
ScrollHandle, Subscription, WeakEntity, Window,
|
||||
};
|
||||
use settings::Settings;
|
||||
use std::collections::HashMap;
|
||||
use theme::ThemeSettings;
|
||||
use ui::{
|
||||
Divider, DividerColor, DynamicSpacing, LabelSize, WithScrollbar, prelude::*,
|
||||
text_for_keystrokes,
|
||||
};
|
||||
use workspace::{ModalView, Workspace};
|
||||
|
||||
use crate::FILTERED_KEYSTROKES;
|
||||
|
||||
pub struct WhichKeyModal {
|
||||
_workspace: WeakEntity<Workspace>,
|
||||
focus_handle: FocusHandle,
|
||||
scroll_handle: ScrollHandle,
|
||||
bindings: Vec<(SharedString, SharedString)>,
|
||||
pending_keys: SharedString,
|
||||
_pending_input_subscription: Subscription,
|
||||
_focus_out_subscription: Subscription,
|
||||
}
|
||||
|
||||
impl WhichKeyModal {
|
||||
pub fn new(
|
||||
workspace: WeakEntity<Workspace>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
// Keep focus where it currently is
|
||||
let focus_handle = window.focused(cx).unwrap_or(cx.focus_handle());
|
||||
|
||||
let handle = cx.weak_entity();
|
||||
let mut this = Self {
|
||||
_workspace: workspace,
|
||||
focus_handle: focus_handle.clone(),
|
||||
scroll_handle: ScrollHandle::new(),
|
||||
bindings: Vec::new(),
|
||||
pending_keys: SharedString::new_static(""),
|
||||
_pending_input_subscription: cx.observe_pending_input(
|
||||
window,
|
||||
|this: &mut Self, window, cx| {
|
||||
this.update_pending_keys(window, cx);
|
||||
},
|
||||
),
|
||||
_focus_out_subscription: window.on_focus_out(&focus_handle, cx, move |_, _, cx| {
|
||||
handle.update(cx, |_, cx| cx.emit(DismissEvent)).ok();
|
||||
}),
|
||||
};
|
||||
this.update_pending_keys(window, cx);
|
||||
this
|
||||
}
|
||||
|
||||
pub fn dismiss(&self, cx: &mut Context<Self>) {
|
||||
cx.emit(DismissEvent)
|
||||
}
|
||||
|
||||
fn update_pending_keys(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let Some(pending_keys) = window.pending_input_keystrokes() else {
|
||||
cx.emit(DismissEvent);
|
||||
return;
|
||||
};
|
||||
let bindings = window.possible_bindings_for_input(pending_keys);
|
||||
|
||||
let mut binding_data = bindings
|
||||
.iter()
|
||||
.map(|binding| {
|
||||
// Map to keystrokes
|
||||
(
|
||||
binding
|
||||
.keystrokes()
|
||||
.iter()
|
||||
.map(|k| k.inner().to_owned())
|
||||
.collect::<Vec<_>>(),
|
||||
binding.action(),
|
||||
)
|
||||
})
|
||||
.filter(|(keystrokes, _action)| {
|
||||
// Check if this binding matches any filtered keystroke pattern
|
||||
!FILTERED_KEYSTROKES.iter().any(|filtered| {
|
||||
keystrokes.len() >= filtered.len()
|
||||
&& keystrokes[..filtered.len()] == filtered[..]
|
||||
})
|
||||
})
|
||||
.map(|(keystrokes, action)| {
|
||||
// Map to remaining keystrokes and action name
|
||||
let remaining_keystrokes = keystrokes[pending_keys.len()..].to_vec();
|
||||
let action_name: SharedString =
|
||||
command_palette::humanize_action_name(action.name()).into();
|
||||
(remaining_keystrokes, action_name)
|
||||
})
|
||||
.collect();
|
||||
|
||||
binding_data = group_bindings(binding_data);
|
||||
|
||||
// Sort bindings from shortest to longest, with groups last
|
||||
// Using stable sort to preserve relative order of equal elements
|
||||
binding_data.sort_by(|(keystrokes_a, action_a), (keystrokes_b, action_b)| {
|
||||
// Groups (actions starting with "+") should go last
|
||||
let is_group_a = action_a.starts_with('+');
|
||||
let is_group_b = action_b.starts_with('+');
|
||||
|
||||
// First, separate groups from non-groups
|
||||
let group_cmp = is_group_a.cmp(&is_group_b);
|
||||
if group_cmp != std::cmp::Ordering::Equal {
|
||||
return group_cmp;
|
||||
}
|
||||
|
||||
// Then sort by keystroke count
|
||||
let keystroke_cmp = keystrokes_a.len().cmp(&keystrokes_b.len());
|
||||
if keystroke_cmp != std::cmp::Ordering::Equal {
|
||||
return keystroke_cmp;
|
||||
}
|
||||
|
||||
// Finally sort by text length, then lexicographically for full stability
|
||||
let text_a = text_for_keystrokes(keystrokes_a, cx);
|
||||
let text_b = text_for_keystrokes(keystrokes_b, cx);
|
||||
let text_len_cmp = text_a.len().cmp(&text_b.len());
|
||||
if text_len_cmp != std::cmp::Ordering::Equal {
|
||||
return text_len_cmp;
|
||||
}
|
||||
text_a.cmp(&text_b)
|
||||
});
|
||||
binding_data.dedup();
|
||||
self.pending_keys = text_for_keystrokes(&pending_keys, cx).into();
|
||||
self.bindings = binding_data
|
||||
.into_iter()
|
||||
.map(|(keystrokes, action)| (text_for_keystrokes(&keystrokes, cx).into(), action))
|
||||
.collect();
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for WhichKeyModal {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let has_rows = !self.bindings.is_empty();
|
||||
let viewport_size = window.viewport_size();
|
||||
|
||||
let max_panel_width = px((f32::from(viewport_size.width) * 0.5).min(480.0));
|
||||
let max_content_height = px(f32::from(viewport_size.height) * 0.4);
|
||||
|
||||
// Push above status bar when visible
|
||||
let status_height = self
|
||||
._workspace
|
||||
.upgrade()
|
||||
.and_then(|workspace| {
|
||||
workspace.read_with(cx, |workspace, cx| {
|
||||
if workspace.status_bar_visible(cx) {
|
||||
Some(
|
||||
DynamicSpacing::Base04.px(cx) * 2.0
|
||||
+ ThemeSettings::get_global(cx).ui_font_size(cx),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
})
|
||||
.unwrap_or(px(0.));
|
||||
|
||||
let margin_bottom = px(16.);
|
||||
let bottom_offset = margin_bottom + status_height;
|
||||
|
||||
// Title section
|
||||
let title_section = {
|
||||
let mut column = v_flex().gap(px(0.)).child(
|
||||
div()
|
||||
.child(
|
||||
Label::new(self.pending_keys.clone())
|
||||
.size(LabelSize::Default)
|
||||
.weight(FontWeight::MEDIUM)
|
||||
.color(Color::Accent),
|
||||
)
|
||||
.mb(px(2.)),
|
||||
);
|
||||
|
||||
if has_rows {
|
||||
column = column.child(
|
||||
div()
|
||||
.child(Divider::horizontal().color(DividerColor::BorderFaded))
|
||||
.mb(px(2.)),
|
||||
);
|
||||
}
|
||||
|
||||
column
|
||||
};
|
||||
|
||||
let content = h_flex()
|
||||
.items_start()
|
||||
.id("which-key-content")
|
||||
.gap(px(8.))
|
||||
.overflow_y_scroll()
|
||||
.track_scroll(&self.scroll_handle)
|
||||
.h_full()
|
||||
.max_h(max_content_height)
|
||||
.child(
|
||||
// Keystrokes column
|
||||
v_flex()
|
||||
.gap(px(4.))
|
||||
.flex_shrink_0()
|
||||
.children(self.bindings.iter().map(|(keystrokes, _)| {
|
||||
div()
|
||||
.child(
|
||||
Label::new(keystrokes.clone())
|
||||
.size(LabelSize::Default)
|
||||
.color(Color::Accent),
|
||||
)
|
||||
.text_align(gpui::TextAlign::Right)
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
// Actions column
|
||||
v_flex()
|
||||
.gap(px(4.))
|
||||
.flex_1()
|
||||
.min_w_0()
|
||||
.children(self.bindings.iter().map(|(_, action_name)| {
|
||||
let is_group = action_name.starts_with('+');
|
||||
let label_color = if is_group {
|
||||
Color::Success
|
||||
} else {
|
||||
Color::Default
|
||||
};
|
||||
|
||||
div().child(
|
||||
Label::new(action_name.clone())
|
||||
.size(LabelSize::Default)
|
||||
.color(label_color)
|
||||
.single_line()
|
||||
.truncate(),
|
||||
)
|
||||
})),
|
||||
);
|
||||
|
||||
div()
|
||||
.id("which-key-buffer-panel-scroll")
|
||||
.occlude()
|
||||
.absolute()
|
||||
.bottom(bottom_offset)
|
||||
.right(px(16.))
|
||||
.min_w(px(220.))
|
||||
.max_w(max_panel_width)
|
||||
.elevation_3(cx)
|
||||
.px(px(12.))
|
||||
.child(v_flex().child(title_section).when(has_rows, |el| {
|
||||
el.child(
|
||||
div()
|
||||
.max_h(max_content_height)
|
||||
.child(content)
|
||||
.vertical_scrollbar_for(&self.scroll_handle, window, cx),
|
||||
)
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<DismissEvent> for WhichKeyModal {}
|
||||
|
||||
impl Focusable for WhichKeyModal {
|
||||
fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl ModalView for WhichKeyModal {
|
||||
fn render_bare(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
fn group_bindings(
|
||||
binding_data: Vec<(Vec<Keystroke>, SharedString)>,
|
||||
) -> Vec<(Vec<Keystroke>, SharedString)> {
|
||||
let mut groups: HashMap<Option<Keystroke>, Vec<(Vec<Keystroke>, SharedString)>> =
|
||||
HashMap::new();
|
||||
|
||||
// Group bindings by their first keystroke
|
||||
for (remaining_keystrokes, action_name) in binding_data {
|
||||
let first_key = remaining_keystrokes.first().cloned();
|
||||
groups
|
||||
.entry(first_key)
|
||||
.or_default()
|
||||
.push((remaining_keystrokes, action_name));
|
||||
}
|
||||
|
||||
let mut result = Vec::new();
|
||||
|
||||
for (first_key, mut group_bindings) in groups {
|
||||
// Remove duplicates within each group
|
||||
group_bindings.dedup_by_key(|(keystrokes, _)| keystrokes.clone());
|
||||
|
||||
if let Some(first_key) = first_key
|
||||
&& group_bindings.len() > 1
|
||||
{
|
||||
// This is a group - create a single entry with just the first keystroke
|
||||
let first_keystroke = vec![first_key];
|
||||
let count = group_bindings.len();
|
||||
result.push((first_keystroke, format!("+{} keybinds", count).into()));
|
||||
} else {
|
||||
// Not a group or empty keystrokes - add all bindings as-is
|
||||
result.append(&mut group_bindings);
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
18
crates/which_key/src/which_key_settings.rs
Normal file
18
crates/which_key/src/which_key_settings.rs
Normal file
@@ -0,0 +1,18 @@
|
||||
use settings::{RegisterSetting, Settings, SettingsContent, WhichKeySettingsContent};
|
||||
|
||||
#[derive(Debug, Clone, Copy, RegisterSetting)]
|
||||
pub struct WhichKeySettings {
|
||||
pub enabled: bool,
|
||||
pub delay_ms: u64,
|
||||
}
|
||||
|
||||
impl Settings for WhichKeySettings {
|
||||
fn from_settings(content: &SettingsContent) -> Self {
|
||||
let which_key: &WhichKeySettingsContent = content.which_key.as_ref().unwrap();
|
||||
|
||||
Self {
|
||||
enabled: which_key.enabled.unwrap(),
|
||||
delay_ms: which_key.delay_ms.unwrap(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
use crate::persistence::model::DockData;
|
||||
use crate::utility_pane::utility_slot_for_dock_position;
|
||||
use crate::{DraggedDock, Event, ModalLayer, Pane};
|
||||
use crate::{Workspace, status_bar::StatusItemView};
|
||||
use anyhow::Context as _;
|
||||
@@ -705,7 +704,7 @@ impl Dock {
|
||||
panel: &Entity<T>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
) -> bool {
|
||||
if let Some(panel_ix) = self
|
||||
.panel_entries
|
||||
.iter()
|
||||
@@ -724,15 +723,12 @@ impl Dock {
|
||||
}
|
||||
}
|
||||
|
||||
let slot = utility_slot_for_dock_position(self.position);
|
||||
if let Some(workspace) = self.workspace.upgrade() {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace.clear_utility_pane_if_provider(slot, Entity::entity_id(panel), cx);
|
||||
});
|
||||
}
|
||||
|
||||
self.panel_entries.remove(panel_ix);
|
||||
cx.notify();
|
||||
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -886,8 +886,12 @@ impl<T: Item> ItemHandle for Entity<T> {
|
||||
// Only trigger autosave if focus has truly left the item.
|
||||
// If focus is still within the item's hierarchy (e.g., moved to a context menu),
|
||||
// don't trigger autosave to avoid unwanted formatting and cursor jumps.
|
||||
// Also skip autosave if focus moved to a modal (e.g., command palette),
|
||||
// since the user is still interacting with the workspace.
|
||||
let focus_handle = item.item_focus_handle(cx);
|
||||
if !focus_handle.contains_focused(window, cx) {
|
||||
if !focus_handle.contains_focused(window, cx)
|
||||
&& !workspace.has_active_modal(window, cx)
|
||||
{
|
||||
Pane::autosave_item(&item, workspace.project.clone(), window, cx)
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
@@ -22,12 +22,17 @@ pub trait ModalView: ManagedView {
|
||||
fn fade_out_background(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn render_bare(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
trait ModalViewHandle {
|
||||
fn on_before_dismiss(&mut self, window: &mut Window, cx: &mut App) -> DismissDecision;
|
||||
fn view(&self) -> AnyView;
|
||||
fn fade_out_background(&self, cx: &mut App) -> bool;
|
||||
fn render_bare(&self, cx: &mut App) -> bool;
|
||||
}
|
||||
|
||||
impl<V: ModalView> ModalViewHandle for Entity<V> {
|
||||
@@ -42,6 +47,10 @@ impl<V: ModalView> ModalViewHandle for Entity<V> {
|
||||
fn fade_out_background(&self, cx: &mut App) -> bool {
|
||||
self.read(cx).fade_out_background()
|
||||
}
|
||||
|
||||
fn render_bare(&self, cx: &mut App) -> bool {
|
||||
self.read(cx).render_bare()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ActiveModal {
|
||||
@@ -167,9 +176,13 @@ impl ModalLayer {
|
||||
impl Render for ModalLayer {
|
||||
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let Some(active_modal) = &self.active_modal else {
|
||||
return div();
|
||||
return div().into_any_element();
|
||||
};
|
||||
|
||||
if active_modal.modal.render_bare(cx) {
|
||||
return active_modal.modal.view().into_any_element();
|
||||
}
|
||||
|
||||
div()
|
||||
.absolute()
|
||||
.size_full()
|
||||
@@ -195,5 +208,6 @@ impl Render for ModalLayer {
|
||||
}),
|
||||
),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,46 +102,31 @@ impl Render for SecurityModal {
|
||||
.child(Icon::new(IconName::Warning).color(Color::Warning))
|
||||
.child(Label::new(header_label)),
|
||||
)
|
||||
.children(self.restricted_paths.values().map(|restricted_path| {
|
||||
.children(self.restricted_paths.values().filter_map(|restricted_path| {
|
||||
let abs_path = if restricted_path.is_file {
|
||||
restricted_path.abs_path.parent()
|
||||
} else {
|
||||
Some(restricted_path.abs_path.as_ref())
|
||||
};
|
||||
|
||||
let label = match abs_path {
|
||||
Some(abs_path) => match &restricted_path.host {
|
||||
Some(remote_host) => match &remote_host.user_name {
|
||||
Some(user_name) => format!(
|
||||
"{} ({}@{})",
|
||||
self.shorten_path(abs_path).display(),
|
||||
user_name,
|
||||
remote_host.host_identifier
|
||||
),
|
||||
None => format!(
|
||||
"{} ({})",
|
||||
self.shorten_path(abs_path).display(),
|
||||
remote_host.host_identifier
|
||||
),
|
||||
},
|
||||
None => self.shorten_path(abs_path).display().to_string(),
|
||||
},
|
||||
None => match &restricted_path.host {
|
||||
Some(remote_host) => match &remote_host.user_name {
|
||||
Some(user_name) => format!(
|
||||
"Workspace trust ({}@{})",
|
||||
user_name, remote_host.host_identifier
|
||||
),
|
||||
None => {
|
||||
format!("Workspace trust ({})", remote_host.host_identifier)
|
||||
}
|
||||
},
|
||||
None => "Workspace trust".to_string(),
|
||||
}?;
|
||||
let label = match &restricted_path.host {
|
||||
Some(remote_host) => match &remote_host.user_name {
|
||||
Some(user_name) => format!(
|
||||
"{} ({}@{})",
|
||||
self.shorten_path(abs_path).display(),
|
||||
user_name,
|
||||
remote_host.host_identifier
|
||||
),
|
||||
None => format!(
|
||||
"{} ({})",
|
||||
self.shorten_path(abs_path).display(),
|
||||
remote_host.host_identifier
|
||||
),
|
||||
},
|
||||
None => self.shorten_path(abs_path).display().to_string(),
|
||||
};
|
||||
h_flex()
|
||||
Some(h_flex()
|
||||
.pl(IconSize::default().rems() + rems(0.5))
|
||||
.child(Label::new(label).color(Color::Muted))
|
||||
.child(Label::new(label).color(Color::Muted)))
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
|
||||
@@ -135,7 +135,9 @@ pub use workspace_settings::{
|
||||
use zed_actions::{Spawn, feedback::FileBugReport};
|
||||
|
||||
use crate::{
|
||||
item::ItemBufferKind, notifications::NotificationId, utility_pane::UTILITY_PANE_MIN_WIDTH,
|
||||
item::ItemBufferKind,
|
||||
notifications::NotificationId,
|
||||
utility_pane::{UTILITY_PANE_MIN_WIDTH, utility_slot_for_dock_position},
|
||||
};
|
||||
use crate::{
|
||||
persistence::{
|
||||
@@ -986,6 +988,7 @@ impl AppState {
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn test(cx: &mut App) -> Arc<Self> {
|
||||
use fs::Fs;
|
||||
use node_runtime::NodeRuntime;
|
||||
use session::Session;
|
||||
use settings::SettingsStore;
|
||||
@@ -996,6 +999,7 @@ impl AppState {
|
||||
}
|
||||
|
||||
let fs = fs::FakeFs::new(cx.background_executor().clone());
|
||||
<dyn Fs>::set_global(fs.clone(), cx);
|
||||
let languages = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
|
||||
let clock = Arc::new(clock::FakeSystemClock::new());
|
||||
let http_client = http_client::FakeHttpClient::with_404_response();
|
||||
@@ -1890,10 +1894,18 @@ impl Workspace {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let mut found_in_dock = None;
|
||||
for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] {
|
||||
dock.update(cx, |dock, cx| {
|
||||
dock.remove_panel(panel, window, cx);
|
||||
})
|
||||
let found = dock.update(cx, |dock, cx| dock.remove_panel(panel, window, cx));
|
||||
|
||||
if found {
|
||||
found_in_dock = Some(dock.clone());
|
||||
}
|
||||
}
|
||||
if let Some(found_in_dock) = found_in_dock {
|
||||
let position = found_in_dock.read(cx).position();
|
||||
let slot = utility_slot_for_dock_position(position);
|
||||
self.clear_utility_pane_if_provider(slot, Entity::entity_id(panel), cx);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,8 +25,10 @@ test-support = [
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
async-lock.workspace = true
|
||||
chardetng.workspace = true
|
||||
clock.workspace = true
|
||||
collections.workspace = true
|
||||
encoding_rs.workspace = true
|
||||
fs.workspace = true
|
||||
futures.workspace = true
|
||||
fuzzy.workspace = true
|
||||
|
||||
@@ -5,8 +5,10 @@ mod worktree_tests;
|
||||
|
||||
use ::ignore::gitignore::{Gitignore, GitignoreBuilder};
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use chardetng::EncodingDetector;
|
||||
use clock::ReplicaId;
|
||||
use collections::{HashMap, HashSet, VecDeque};
|
||||
use encoding_rs::Encoding;
|
||||
use fs::{Fs, MTime, PathEvent, RemoveOptions, Watcher, copy_recursive, read_dir_items};
|
||||
use futures::{
|
||||
FutureExt as _, Stream, StreamExt,
|
||||
@@ -105,6 +107,8 @@ pub enum CreatedEntry {
|
||||
pub struct LoadedFile {
|
||||
pub file: Arc<File>,
|
||||
pub text: String,
|
||||
pub encoding: &'static Encoding,
|
||||
pub has_bom: bool,
|
||||
}
|
||||
|
||||
pub struct LoadedBinaryFile {
|
||||
@@ -741,10 +745,14 @@ impl Worktree {
|
||||
path: Arc<RelPath>,
|
||||
text: Rope,
|
||||
line_ending: LineEnding,
|
||||
encoding: &'static Encoding,
|
||||
has_bom: bool,
|
||||
cx: &Context<Worktree>,
|
||||
) -> Task<Result<Arc<File>>> {
|
||||
match self {
|
||||
Worktree::Local(this) => this.write_file(path, text, line_ending, cx),
|
||||
Worktree::Local(this) => {
|
||||
this.write_file(path, text, line_ending, encoding, has_bom, cx)
|
||||
}
|
||||
Worktree::Remote(_) => {
|
||||
Task::ready(Err(anyhow!("remote worktree can't yet write files")))
|
||||
}
|
||||
@@ -1351,7 +1359,9 @@ impl LocalWorktree {
|
||||
anyhow::bail!("File is too large to load");
|
||||
}
|
||||
}
|
||||
let text = fs.load(&abs_path).await?;
|
||||
|
||||
let content = fs.load_bytes(&abs_path).await?;
|
||||
let (text, encoding, has_bom) = decode_byte(content);
|
||||
|
||||
let worktree = this.upgrade().context("worktree was dropped")?;
|
||||
let file = match entry.await? {
|
||||
@@ -1379,7 +1389,12 @@ impl LocalWorktree {
|
||||
}
|
||||
};
|
||||
|
||||
Ok(LoadedFile { file, text })
|
||||
Ok(LoadedFile {
|
||||
file,
|
||||
text,
|
||||
encoding,
|
||||
has_bom,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1462,6 +1477,8 @@ impl LocalWorktree {
|
||||
path: Arc<RelPath>,
|
||||
text: Rope,
|
||||
line_ending: LineEnding,
|
||||
encoding: &'static Encoding,
|
||||
has_bom: bool,
|
||||
cx: &Context<Worktree>,
|
||||
) -> Task<Result<Arc<File>>> {
|
||||
let fs = self.fs.clone();
|
||||
@@ -1471,7 +1488,49 @@ impl LocalWorktree {
|
||||
let write = cx.background_spawn({
|
||||
let fs = fs.clone();
|
||||
let abs_path = abs_path.clone();
|
||||
async move { fs.save(&abs_path, &text, line_ending).await }
|
||||
async move {
|
||||
let bom_bytes = if has_bom {
|
||||
if encoding == encoding_rs::UTF_16LE {
|
||||
vec![0xFF, 0xFE]
|
||||
} else if encoding == encoding_rs::UTF_16BE {
|
||||
vec![0xFE, 0xFF]
|
||||
} else if encoding == encoding_rs::UTF_8 {
|
||||
vec![0xEF, 0xBB, 0xBF]
|
||||
} else {
|
||||
vec![]
|
||||
}
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
|
||||
// For UTF-8, use the optimized `fs.save` which writes Rope chunks directly to disk
|
||||
// without allocating a contiguous string.
|
||||
if encoding == encoding_rs::UTF_8 && !has_bom {
|
||||
return fs.save(&abs_path, &text, line_ending).await;
|
||||
}
|
||||
// For legacy encodings (e.g. Shift-JIS), we fall back to converting the entire Rope
|
||||
// to a String/Bytes in memory before writing.
|
||||
//
|
||||
// Note: This is inefficient for very large files compared to the streaming approach above,
|
||||
// but supporting streaming writes for arbitrary encodings would require a significant
|
||||
// refactor of the `fs` crate to expose a Writer interface.
|
||||
let text_string = text.to_string();
|
||||
let normalized_text = match line_ending {
|
||||
LineEnding::Unix => text_string,
|
||||
LineEnding::Windows => text_string.replace('\n', "\r\n"),
|
||||
};
|
||||
|
||||
let (cow, _, _) = encoding.encode(&normalized_text);
|
||||
let bytes = if !bom_bytes.is_empty() {
|
||||
let mut bytes = bom_bytes;
|
||||
bytes.extend_from_slice(&cow);
|
||||
bytes.into()
|
||||
} else {
|
||||
cow
|
||||
};
|
||||
|
||||
fs.write(&abs_path, &bytes).await
|
||||
}
|
||||
});
|
||||
|
||||
cx.spawn(async move |this, cx| {
|
||||
@@ -5782,3 +5841,40 @@ impl fs::Watcher for NullWatcher {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn decode_byte(bytes: Vec<u8>) -> (String, &'static Encoding, bool) {
|
||||
// check BOM
|
||||
if let Some((encoding, _bom_len)) = Encoding::for_bom(&bytes) {
|
||||
let (cow, _) = encoding.decode_with_bom_removal(&bytes);
|
||||
return (cow.into_owned(), encoding, true);
|
||||
}
|
||||
|
||||
fn detect_encoding(bytes: Vec<u8>) -> (String, &'static Encoding) {
|
||||
let mut detector = EncodingDetector::new();
|
||||
detector.feed(&bytes, true);
|
||||
|
||||
let encoding = detector.guess(None, true); // Use None for TLD hint to ensure neutral detection logic.
|
||||
|
||||
let (cow, _, _) = encoding.decode(&bytes);
|
||||
(cow.into_owned(), encoding)
|
||||
}
|
||||
|
||||
match String::from_utf8(bytes) {
|
||||
Ok(text) => {
|
||||
// ISO-2022-JP (and other ISO-2022 variants) consists entirely of 7-bit ASCII bytes,
|
||||
// so it is valid UTF-8. However, it contains escape sequences starting with '\x1b'.
|
||||
// If we find an escape character, we double-check the encoding to prevent
|
||||
// displaying raw escape sequences instead of the correct characters.
|
||||
if text.contains('\x1b') {
|
||||
let (s, enc) = detect_encoding(text.into_bytes());
|
||||
(s, enc, false)
|
||||
} else {
|
||||
(text, encoding_rs::UTF_8, false)
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
let (s, enc) = detect_encoding(e.into_bytes());
|
||||
(s, enc, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::{Entry, EntryKind, Event, PathChange, Worktree, WorktreeModelHandle};
|
||||
use anyhow::Result;
|
||||
use anyhow::{Context as _, Result};
|
||||
use encoding_rs;
|
||||
use fs::{FakeFs, Fs, RealFs, RemoveOptions};
|
||||
use git::{DOT_GIT, GITIGNORE, REPO_EXCLUDE};
|
||||
use gpui::{AppContext as _, BackgroundExecutor, BorrowAppContext, Context, Task, TestAppContext};
|
||||
@@ -19,6 +20,7 @@ use std::{
|
||||
};
|
||||
use util::{
|
||||
ResultExt, path,
|
||||
paths::PathStyle,
|
||||
rel_path::{RelPath, rel_path},
|
||||
test::TempTree,
|
||||
};
|
||||
@@ -723,6 +725,8 @@ async fn test_write_file(cx: &mut TestAppContext) {
|
||||
rel_path("tracked-dir/file.txt").into(),
|
||||
"hello".into(),
|
||||
Default::default(),
|
||||
encoding_rs::UTF_8,
|
||||
false,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
@@ -734,6 +738,8 @@ async fn test_write_file(cx: &mut TestAppContext) {
|
||||
rel_path("ignored-dir/file.txt").into(),
|
||||
"world".into(),
|
||||
Default::default(),
|
||||
encoding_rs::UTF_8,
|
||||
false,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
@@ -2035,8 +2041,14 @@ fn randomly_mutate_worktree(
|
||||
})
|
||||
} else {
|
||||
log::info!("overwriting file {:?} ({})", &entry.path, entry.id.0);
|
||||
let task =
|
||||
worktree.write_file(entry.path.clone(), "".into(), Default::default(), cx);
|
||||
let task = worktree.write_file(
|
||||
entry.path.clone(),
|
||||
"".into(),
|
||||
Default::default(),
|
||||
encoding_rs::UTF_8,
|
||||
false,
|
||||
cx,
|
||||
);
|
||||
cx.background_spawn(async move {
|
||||
task.await?;
|
||||
Ok(())
|
||||
@@ -2552,3 +2564,176 @@ fn init_test(cx: &mut gpui::TestAppContext) {
|
||||
cx.set_global(settings_store);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_load_file_encoding(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let test_cases: Vec<(&str, &[u8], &str)> = vec![
|
||||
("utf8.txt", "こんにちは".as_bytes(), "こんにちは"), // "こんにちは" is Japanese "Hello"
|
||||
(
|
||||
"sjis.txt",
|
||||
&[0x82, 0xb1, 0x82, 0xf1, 0x82, 0xc9, 0x82, 0xbf, 0x82, 0xcd],
|
||||
"こんにちは",
|
||||
),
|
||||
(
|
||||
"eucjp.txt",
|
||||
&[0xa4, 0xb3, 0xa4, 0xf3, 0xa4, 0xcb, 0xa4, 0xc1, 0xa4, 0xcf],
|
||||
"こんにちは",
|
||||
),
|
||||
(
|
||||
"iso2022jp.txt",
|
||||
&[
|
||||
0x1b, 0x24, 0x42, 0x24, 0x33, 0x24, 0x73, 0x24, 0x4b, 0x24, 0x41, 0x24, 0x4f, 0x1b,
|
||||
0x28, 0x42,
|
||||
],
|
||||
"こんにちは",
|
||||
),
|
||||
// Western Europe (Windows-1252)
|
||||
// "Café" -> 0xE9 is 'é' in Windows-1252 (it is typically 0xC3 0xA9 in UTF-8)
|
||||
("win1252.txt", &[0x43, 0x61, 0x66, 0xe9], "Café"),
|
||||
// Chinese Simplified (GBK)
|
||||
// Note: We use a slightly longer string here because short byte sequences can be ambiguous
|
||||
// in multi-byte encodings. Providing more context helps the heuristic detector guess correctly.
|
||||
// Text: "今天天气不错" (Today's weather is not bad / nice)
|
||||
// Bytes:
|
||||
// 今: BD F1
|
||||
// 天: CC EC
|
||||
// 天: CC EC
|
||||
// 气: C6 F8
|
||||
// 不: B2 BB
|
||||
// 错: B4 ED
|
||||
(
|
||||
"gbk.txt",
|
||||
&[
|
||||
0xbd, 0xf1, 0xcc, 0xec, 0xcc, 0xec, 0xc6, 0xf8, 0xb2, 0xbb, 0xb4, 0xed,
|
||||
],
|
||||
"今天天气不错",
|
||||
),
|
||||
(
|
||||
"utf16le_bom.txt",
|
||||
&[
|
||||
0xFF, 0xFE, // BOM
|
||||
0x53, 0x30, // こ
|
||||
0x93, 0x30, // ん
|
||||
0x6B, 0x30, // に
|
||||
0x61, 0x30, // ち
|
||||
0x6F, 0x30, // は
|
||||
],
|
||||
"こんにちは",
|
||||
),
|
||||
(
|
||||
"utf8_bom.txt",
|
||||
&[
|
||||
0xEF, 0xBB, 0xBF, // UTF-8 BOM
|
||||
0xE3, 0x81, 0x93, // こ
|
||||
0xE3, 0x82, 0x93, // ん
|
||||
0xE3, 0x81, 0xAB, // に
|
||||
0xE3, 0x81, 0xA1, // ち
|
||||
0xE3, 0x81, 0xAF, // は
|
||||
],
|
||||
"こんにちは",
|
||||
),
|
||||
];
|
||||
|
||||
let root_path = if cfg!(windows) {
|
||||
Path::new("C:\\root")
|
||||
} else {
|
||||
Path::new("/root")
|
||||
};
|
||||
|
||||
let fs = FakeFs::new(cx.background_executor.clone());
|
||||
|
||||
let mut files_json = serde_json::Map::new();
|
||||
for (name, _, _) in &test_cases {
|
||||
files_json.insert(name.to_string(), serde_json::Value::String("".to_string()));
|
||||
}
|
||||
|
||||
for (name, bytes, _) in &test_cases {
|
||||
let path = root_path.join(name);
|
||||
fs.write(&path, bytes).await.unwrap();
|
||||
}
|
||||
|
||||
let tree = Worktree::local(
|
||||
root_path,
|
||||
true,
|
||||
fs,
|
||||
Default::default(),
|
||||
true,
|
||||
&mut cx.to_async(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
|
||||
.await;
|
||||
|
||||
for (name, _, expected) in test_cases {
|
||||
let loaded = tree
|
||||
.update(cx, |tree, cx| tree.load_file(rel_path(name), cx))
|
||||
.await
|
||||
.with_context(|| format!("Failed to load {}", name))
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
loaded.text, expected,
|
||||
"Encoding mismatch for file: {}",
|
||||
name
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_write_file_encoding(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx);
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
let root_path = if cfg!(windows) {
|
||||
Path::new("C:\\root")
|
||||
} else {
|
||||
Path::new("/root")
|
||||
};
|
||||
fs.create_dir(root_path).await.unwrap();
|
||||
let file_path = root_path.join("test.txt");
|
||||
|
||||
fs.insert_file(&file_path, "initial".into()).await;
|
||||
|
||||
let worktree = Worktree::local(
|
||||
root_path,
|
||||
true,
|
||||
fs.clone(),
|
||||
Default::default(),
|
||||
true,
|
||||
&mut cx.to_async(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let path: Arc<Path> = Path::new("test.txt").into();
|
||||
let rel_path = RelPath::new(&path, PathStyle::local()).unwrap().into_arc();
|
||||
|
||||
let text = text::Rope::from("こんにちは");
|
||||
|
||||
let task = worktree.update(cx, |wt, cx| {
|
||||
wt.write_file(
|
||||
rel_path,
|
||||
text,
|
||||
text::LineEnding::Unix,
|
||||
encoding_rs::SHIFT_JIS,
|
||||
false,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
task.await.unwrap();
|
||||
|
||||
let bytes = fs.load_bytes(&file_path).await.unwrap();
|
||||
|
||||
let expected_bytes = vec![
|
||||
0x82, 0xb1, // こ
|
||||
0x82, 0xf1, // ん
|
||||
0x82, 0xc9, // に
|
||||
0x82, 0xbf, // ち
|
||||
0x82, 0xcd, // は
|
||||
];
|
||||
|
||||
assert_eq!(bytes, expected_bytes, "Should be saved as Shift-JIS");
|
||||
}
|
||||
|
||||
@@ -163,6 +163,7 @@ vim_mode_setting.workspace = true
|
||||
watch.workspace = true
|
||||
web_search.workspace = true
|
||||
web_search_providers.workspace = true
|
||||
which_key.workspace = true
|
||||
workspace.workspace = true
|
||||
zed_actions.workspace = true
|
||||
zed_env_vars.workspace = true
|
||||
@@ -195,6 +196,10 @@ terminal_view = { workspace = true, features = ["test-support"] }
|
||||
tree-sitter-md.workspace = true
|
||||
tree-sitter-rust.workspace = true
|
||||
workspace = { workspace = true, features = ["test-support"] }
|
||||
agent_ui = { workspace = true, features = ["test-support"] }
|
||||
agent_ui_v2 = { workspace = true, features = ["test-support"] }
|
||||
search = { workspace = true, features = ["test-support"] }
|
||||
|
||||
|
||||
[package.metadata.bundle-dev]
|
||||
icon = ["resources/app-icon-dev@2x.png", "resources/app-icon-dev.png"]
|
||||
|
||||
BIN
crates/zed/resources/Document.icns
Normal file
BIN
crates/zed/resources/Document.icns
Normal file
Binary file not shown.
@@ -15,11 +15,13 @@ use extension::ExtensionHostProxy;
|
||||
use fs::{Fs, RealFs};
|
||||
use futures::{StreamExt, channel::oneshot, future};
|
||||
use git::GitHostingProviderRegistry;
|
||||
use git_ui::clone::clone_and_open;
|
||||
use gpui::{App, AppContext, Application, AsyncApp, Focusable as _, QuitMode, UpdateGlobal as _};
|
||||
|
||||
use gpui_tokio::Tokio;
|
||||
use language::LanguageRegistry;
|
||||
use onboarding::{FIRST_OPEN, show_onboarding_view};
|
||||
use project_panel::ProjectPanel;
|
||||
use prompt_store::PromptBuilder;
|
||||
use remote::RemoteConnectionOptions;
|
||||
use reqwest_client::ReqwestClient;
|
||||
@@ -33,10 +35,12 @@ use release_channel::{AppCommitSha, AppVersion, ReleaseChannel};
|
||||
use session::{AppSession, Session};
|
||||
use settings::{BaseKeymap, Settings, SettingsStore, watch_config_file};
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
env,
|
||||
io::{self, IsTerminal},
|
||||
path::{Path, PathBuf},
|
||||
process,
|
||||
rc::Rc,
|
||||
sync::{Arc, OnceLock},
|
||||
time::Instant,
|
||||
};
|
||||
@@ -656,6 +660,7 @@ pub fn main() {
|
||||
inspector_ui::init(app_state.clone(), cx);
|
||||
json_schema_store::init(cx);
|
||||
miniprofiler_ui::init(*STARTUP_TIME.get().unwrap(), cx);
|
||||
which_key::init(cx);
|
||||
|
||||
cx.observe_global::<SettingsStore>({
|
||||
let http = app_state.client.http_client();
|
||||
@@ -742,18 +747,16 @@ pub fn main() {
|
||||
})
|
||||
}
|
||||
|
||||
let request = open_rx.try_next().ok().flatten();
|
||||
log::info!(
|
||||
"finish_launching: try_next() returned {:?}",
|
||||
request.as_ref().map(|r| &r.urls)
|
||||
);
|
||||
match request.and_then(|request| OpenRequest::parse(request, cx).log_err()) {
|
||||
match open_rx
|
||||
.try_next()
|
||||
.ok()
|
||||
.flatten()
|
||||
.and_then(|request| OpenRequest::parse(request, cx).log_err())
|
||||
{
|
||||
Some(request) => {
|
||||
log::info!("finish_launching: got request, calling handle_open_request");
|
||||
handle_open_request(request, app_state.clone(), cx);
|
||||
}
|
||||
None => {
|
||||
log::info!("finish_launching: no request, spawning restore_or_create_workspace");
|
||||
cx.spawn({
|
||||
let app_state = app_state.clone();
|
||||
async move |cx| {
|
||||
@@ -894,6 +897,41 @@ fn handle_open_request(request: OpenRequest, app_state: Arc<AppState>, cx: &mut
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
OpenRequestKind::GitClone { repo_url } => {
|
||||
workspace::with_active_or_new_workspace(cx, |_workspace, window, cx| {
|
||||
if window.is_window_active() {
|
||||
clone_and_open(
|
||||
repo_url,
|
||||
cx.weak_entity(),
|
||||
window,
|
||||
cx,
|
||||
Arc::new(|workspace: &mut workspace::Workspace, window, cx| {
|
||||
workspace.focus_panel::<ProjectPanel>(window, cx);
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let subscription = Rc::new(RefCell::new(None));
|
||||
subscription.replace(Some(cx.observe_in(&cx.entity(), window, {
|
||||
let subscription = subscription.clone();
|
||||
let repo_url = repo_url.clone();
|
||||
move |_, workspace_entity, window, cx| {
|
||||
if window.is_window_active() && subscription.take().is_some() {
|
||||
clone_and_open(
|
||||
repo_url.clone(),
|
||||
workspace_entity.downgrade(),
|
||||
window,
|
||||
cx,
|
||||
Arc::new(|workspace: &mut workspace::Workspace, window, cx| {
|
||||
workspace.focus_panel::<ProjectPanel>(window, cx);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
})));
|
||||
});
|
||||
}
|
||||
OpenRequestKind::GitCommit { sha } => {
|
||||
cx.spawn(async move |cx| {
|
||||
let paths_with_position =
|
||||
|
||||
@@ -707,7 +707,6 @@ fn setup_or_teardown_ai_panel<P: Panel>(
|
||||
.disable_ai
|
||||
|| cfg!(test);
|
||||
let existing_panel = workspace.panel::<P>(cx);
|
||||
|
||||
match (disable_ai, existing_panel) {
|
||||
(false, None) => cx.spawn_in(window, async move |workspace, cx| {
|
||||
let panel = load_panel(workspace.clone(), cx.clone()).await?;
|
||||
@@ -2327,7 +2326,7 @@ mod tests {
|
||||
use project::{Project, ProjectPath};
|
||||
use semver::Version;
|
||||
use serde_json::json;
|
||||
use settings::{SettingsStore, watch_config_file};
|
||||
use settings::{SaturatingBool, SettingsStore, watch_config_file};
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
time::Duration,
|
||||
@@ -5171,6 +5170,28 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_disable_ai_crash(cx: &mut gpui::TestAppContext) {
|
||||
let app_state = init_test(cx);
|
||||
cx.update(init);
|
||||
let project = Project::test(app_state.fs.clone(), [], cx).await;
|
||||
let _window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
cx.update(|cx| {
|
||||
SettingsStore::update_global(cx, |settings_store, cx| {
|
||||
settings_store.update_user_settings(cx, |settings| {
|
||||
settings.disable_ai = Some(SaturatingBool(true));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
// If this panics, the test has failed
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_prefer_focused_window(cx: &mut gpui::TestAppContext) {
|
||||
let app_state = init_test(cx);
|
||||
|
||||
@@ -145,23 +145,6 @@ fn register_backward_compatible_actions(editor: &mut Editor, cx: &mut Context<Ed
|
||||
},
|
||||
))
|
||||
.detach();
|
||||
editor
|
||||
.register_action(cx.listener(
|
||||
|editor, _: &copilot::NextSuggestion, window: &mut Window, cx: &mut Context<Editor>| {
|
||||
editor.next_edit_prediction(&Default::default(), window, cx);
|
||||
},
|
||||
))
|
||||
.detach();
|
||||
editor
|
||||
.register_action(cx.listener(
|
||||
|editor,
|
||||
_: &copilot::PreviousSuggestion,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Editor>| {
|
||||
editor.previous_edit_prediction(&Default::default(), window, cx);
|
||||
},
|
||||
))
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn assign_edit_prediction_provider(
|
||||
|
||||
@@ -25,6 +25,7 @@ use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
use ui::SharedString;
|
||||
use util::ResultExt;
|
||||
use util::paths::PathWithPosition;
|
||||
use workspace::PathList;
|
||||
@@ -58,6 +59,9 @@ pub enum OpenRequestKind {
|
||||
/// `None` opens settings without navigating to a specific path.
|
||||
setting_path: Option<String>,
|
||||
},
|
||||
GitClone {
|
||||
repo_url: SharedString,
|
||||
},
|
||||
GitCommit {
|
||||
sha: String,
|
||||
},
|
||||
@@ -113,6 +117,8 @@ impl OpenRequest {
|
||||
this.kind = Some(OpenRequestKind::Setting {
|
||||
setting_path: Some(setting_path.to_string()),
|
||||
});
|
||||
} else if let Some(clone_path) = url.strip_prefix("zed://git/clone") {
|
||||
this.parse_git_clone_url(clone_path)?
|
||||
} else if let Some(commit_path) = url.strip_prefix("zed://git/commit/") {
|
||||
this.parse_git_commit_url(commit_path)?
|
||||
} else if url.starts_with("ssh://") {
|
||||
@@ -143,6 +149,26 @@ impl OpenRequest {
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_git_clone_url(&mut self, clone_path: &str) -> Result<()> {
|
||||
// Format: /?repo=<url> or ?repo=<url>
|
||||
let clone_path = clone_path.strip_prefix('/').unwrap_or(clone_path);
|
||||
|
||||
let query = clone_path
|
||||
.strip_prefix('?')
|
||||
.context("invalid git clone url: missing query string")?;
|
||||
|
||||
let repo_url = url::form_urlencoded::parse(query.as_bytes())
|
||||
.find_map(|(key, value)| (key == "repo").then_some(value))
|
||||
.filter(|s| !s.is_empty())
|
||||
.context("invalid git clone url: missing repo query parameter")?
|
||||
.to_string()
|
||||
.into();
|
||||
|
||||
self.kind = Some(OpenRequestKind::GitClone { repo_url });
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_git_commit_url(&mut self, commit_path: &str) -> Result<()> {
|
||||
// Format: <sha>?repo=<path>
|
||||
let (sha, query) = commit_path
|
||||
@@ -1087,4 +1113,80 @@ mod tests {
|
||||
|
||||
assert!(!errored_reuse);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_parse_git_clone_url(cx: &mut TestAppContext) {
|
||||
let _app_state = init_test(cx);
|
||||
|
||||
let request = cx.update(|cx| {
|
||||
OpenRequest::parse(
|
||||
RawOpenRequest {
|
||||
urls: vec![
|
||||
"zed://git/clone/?repo=https://github.com/zed-industries/zed.git".into(),
|
||||
],
|
||||
..Default::default()
|
||||
},
|
||||
cx,
|
||||
)
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
match request.kind {
|
||||
Some(OpenRequestKind::GitClone { repo_url }) => {
|
||||
assert_eq!(repo_url, "https://github.com/zed-industries/zed.git");
|
||||
}
|
||||
_ => panic!("Expected GitClone kind"),
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_parse_git_clone_url_without_slash(cx: &mut TestAppContext) {
|
||||
let _app_state = init_test(cx);
|
||||
|
||||
let request = cx.update(|cx| {
|
||||
OpenRequest::parse(
|
||||
RawOpenRequest {
|
||||
urls: vec![
|
||||
"zed://git/clone?repo=https://github.com/zed-industries/zed.git".into(),
|
||||
],
|
||||
..Default::default()
|
||||
},
|
||||
cx,
|
||||
)
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
match request.kind {
|
||||
Some(OpenRequestKind::GitClone { repo_url }) => {
|
||||
assert_eq!(repo_url, "https://github.com/zed-industries/zed.git");
|
||||
}
|
||||
_ => panic!("Expected GitClone kind"),
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_parse_git_clone_url_with_encoding(cx: &mut TestAppContext) {
|
||||
let _app_state = init_test(cx);
|
||||
|
||||
let request = cx.update(|cx| {
|
||||
OpenRequest::parse(
|
||||
RawOpenRequest {
|
||||
urls: vec![
|
||||
"zed://git/clone/?repo=https%3A%2F%2Fgithub.com%2Fzed-industries%2Fzed.git"
|
||||
.into(),
|
||||
],
|
||||
..Default::default()
|
||||
},
|
||||
cx,
|
||||
)
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
match request.kind {
|
||||
Some(OpenRequestKind::GitClone { repo_url }) => {
|
||||
assert_eq!(repo_url, "https://github.com/zed-industries/zed.git");
|
||||
}
|
||||
_ => panic!("Expected GitClone kind"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,6 +106,17 @@ mv Cargo.toml.backup Cargo.toml
|
||||
popd
|
||||
echo "Bundled ${app_path}"
|
||||
|
||||
# DocumentTypes.plist references CFBundleTypeIconFile "Document", so the bundle must contain Document.icns.
|
||||
# We use the app icon as a placeholder document icon for now.
|
||||
document_icon_source="crates/zed/resources/Document.icns"
|
||||
document_icon_target="${app_path}/Contents/Resources/Document.icns"
|
||||
if [[ -f "${document_icon_source}" ]]; then
|
||||
mkdir -p "$(dirname "${document_icon_target}")"
|
||||
cp "${document_icon_source}" "${document_icon_target}"
|
||||
else
|
||||
echo "cargo::warning=Missing ${document_icon_source}; macOS document icons may not appear in Finder."
|
||||
fi
|
||||
|
||||
if [[ -n "${MACOS_CERTIFICATE:-}" && -n "${MACOS_CERTIFICATE_PASSWORD:-}" && -n "${APPLE_NOTARIZATION_KEY:-}" && -n "${APPLE_NOTARIZATION_KEY_ID:-}" && -n "${APPLE_NOTARIZATION_ISSUER_ID:-}" ]]; then
|
||||
can_code_sign=true
|
||||
|
||||
|
||||
81
script/verify-macos-document-icon
Executable file
81
script/verify-macos-document-icon
Executable file
@@ -0,0 +1,81 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
cat <<'USAGE'
|
||||
Usage:
|
||||
script/verify-macos-document-icon /path/to/Zed.app
|
||||
|
||||
Verifies that the given macOS app bundle's Info.plist references a document icon
|
||||
named "Document" and that the corresponding icon file exists in the bundle.
|
||||
|
||||
Specifically checks:
|
||||
- CFBundleDocumentTypes[*].CFBundleTypeIconFile includes "Document"
|
||||
- Contents/Resources/Document.icns exists
|
||||
|
||||
Exit codes:
|
||||
0 - success
|
||||
1 - verification failed
|
||||
2 - invalid usage / missing prerequisites
|
||||
USAGE
|
||||
}
|
||||
|
||||
fail() {
|
||||
echo "error: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
if [[ $# -ne 1 ]]; then
|
||||
usage >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
app_path="$1"
|
||||
|
||||
if [[ ! -d "${app_path}" ]]; then
|
||||
fail "app bundle not found: ${app_path}"
|
||||
fi
|
||||
|
||||
info_plist="${app_path}/Contents/Info.plist"
|
||||
if [[ ! -f "${info_plist}" ]]; then
|
||||
fail "missing Info.plist: ${info_plist}"
|
||||
fi
|
||||
|
||||
if ! command -v plutil >/dev/null 2>&1; then
|
||||
fail "plutil not found (required on macOS to read Info.plist)"
|
||||
fi
|
||||
|
||||
# Convert to JSON for robust parsing. plutil outputs JSON to stdout in this mode.
|
||||
info_json="$(plutil -convert json -o - "${info_plist}")"
|
||||
|
||||
# Check that CFBundleDocumentTypes exists and that at least one entry references "Document".
|
||||
# We use Python for JSON parsing; macOS ships with Python 3 on many setups, but not all.
|
||||
# If python3 isn't available, fall back to a simpler grep-based check.
|
||||
has_document_icon_ref="false"
|
||||
if command -v python3 >/dev/null 2>&1; then
|
||||
has_document_icon_ref="$(python3 -c "import json,sys; d=json.load(sys.stdin); types=d.get('CFBundleDocumentTypes', []); vals=[t.get('CFBundleTypeIconFile') for t in types if isinstance(t, dict)]; print('true' if 'Document' in vals else 'false')" <<<"${info_json}")"
|
||||
else
|
||||
# This is a best-effort fallback. It may produce false negatives if the JSON formatting differs.
|
||||
if echo "${info_json}" | grep -q '"CFBundleTypeIconFile"[[:space:]]*:[[:space:]]*"Document"'; then
|
||||
has_document_icon_ref="true"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ "${has_document_icon_ref}" != "true" ]]; then
|
||||
echo "Verification failed for: ${app_path}" >&2
|
||||
echo "Expected Info.plist to reference CFBundleTypeIconFile \"Document\" in CFBundleDocumentTypes." >&2
|
||||
echo "Tip: This bundle may be missing DocumentTypes.plist extensions or may have different icon naming." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
document_icon_path="${app_path}/Contents/Resources/Document.icns"
|
||||
if [[ ! -f "${document_icon_path}" ]]; then
|
||||
echo "Verification failed for: ${app_path}" >&2
|
||||
echo "Expected document icon to exist: ${document_icon_path}" >&2
|
||||
echo "Tip: The bundle script should copy crates/zed/resources/Document.icns into Contents/Resources/Document.icns." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "OK: ${app_path}"
|
||||
echo " - Info.plist references CFBundleTypeIconFile \"Document\""
|
||||
echo " - Found ${document_icon_path}"
|
||||
@@ -109,19 +109,6 @@ fn run_autofix(pr_number: &WorkflowInput, run_clippy: &WorkflowInput) -> NamedJo
|
||||
}
|
||||
|
||||
fn commit_changes(pr_number: &WorkflowInput, autofix_job: &NamedJob) -> NamedJob {
|
||||
fn authenticate_as_zippy() -> (Step<Use>, StepOutput) {
|
||||
let step = named::uses(
|
||||
"actions",
|
||||
"create-github-app-token",
|
||||
"bef1eaf1c0ac2b148ee2a0a74c65fbe6db0631f1",
|
||||
)
|
||||
.add_with(("app-id", vars::ZED_ZIPPY_APP_ID))
|
||||
.add_with(("private-key", vars::ZED_ZIPPY_APP_PRIVATE_KEY))
|
||||
.id("get-app-token");
|
||||
let output = StepOutput::new(&step, "token");
|
||||
(step, output)
|
||||
}
|
||||
|
||||
fn checkout_pr(pr_number: &WorkflowInput, token: &StepOutput) -> Step<Run> {
|
||||
named::bash(&format!("gh pr checkout {pr_number}")).add_env(("GITHUB_TOKEN", token))
|
||||
}
|
||||
@@ -148,7 +135,7 @@ fn commit_changes(pr_number: &WorkflowInput, autofix_job: &NamedJob) -> NamedJob
|
||||
.add_env(("GITHUB_TOKEN", token))
|
||||
}
|
||||
|
||||
let (authenticate, token) = authenticate_as_zippy();
|
||||
let (authenticate, token) = steps::authenticate_as_zippy();
|
||||
|
||||
named::job(
|
||||
Job::default()
|
||||
|
||||
@@ -3,7 +3,7 @@ use gh_workflow::*;
|
||||
use crate::tasks::workflows::{
|
||||
runners,
|
||||
steps::{self, NamedJob, named},
|
||||
vars::{self, StepOutput, WorkflowInput},
|
||||
vars::{StepOutput, WorkflowInput},
|
||||
};
|
||||
|
||||
pub fn cherry_pick() -> Workflow {
|
||||
@@ -29,19 +29,6 @@ fn run_cherry_pick(
|
||||
commit: &WorkflowInput,
|
||||
channel: &WorkflowInput,
|
||||
) -> NamedJob {
|
||||
fn authenticate_as_zippy() -> (Step<Use>, StepOutput) {
|
||||
let step = named::uses(
|
||||
"actions",
|
||||
"create-github-app-token",
|
||||
"bef1eaf1c0ac2b148ee2a0a74c65fbe6db0631f1",
|
||||
) // v2
|
||||
.add_with(("app-id", vars::ZED_ZIPPY_APP_ID))
|
||||
.add_with(("private-key", vars::ZED_ZIPPY_APP_PRIVATE_KEY))
|
||||
.id("get-app-token");
|
||||
let output = StepOutput::new(&step, "token");
|
||||
(step, output)
|
||||
}
|
||||
|
||||
fn cherry_pick(
|
||||
branch: &WorkflowInput,
|
||||
commit: &WorkflowInput,
|
||||
@@ -54,7 +41,7 @@ fn run_cherry_pick(
|
||||
.add_env(("GITHUB_TOKEN", token))
|
||||
}
|
||||
|
||||
let (authenticate, token) = authenticate_as_zippy();
|
||||
let (authenticate, token) = steps::authenticate_as_zippy();
|
||||
|
||||
named::job(
|
||||
Job::default()
|
||||
|
||||
@@ -97,17 +97,20 @@ pub(crate) fn create_sentry_release() -> Step<Use> {
|
||||
}
|
||||
|
||||
fn auto_release_preview(deps: &[&NamedJob; 1]) -> NamedJob {
|
||||
let (authenticate, token) = steps::authenticate_as_zippy();
|
||||
|
||||
named::job(
|
||||
dependant_job(deps)
|
||||
.runs_on(runners::LINUX_SMALL)
|
||||
.cond(Expression::new(indoc::indoc!(
|
||||
r#"startsWith(github.ref, 'refs/tags/v') && endsWith(github.ref, '-pre') && !endsWith(github.ref, '.0-pre')"#
|
||||
)))
|
||||
.add_step(authenticate)
|
||||
.add_step(
|
||||
steps::script(
|
||||
r#"gh release edit "$GITHUB_REF_NAME" --repo=zed-industries/zed --draft=false"#,
|
||||
)
|
||||
.add_env(("GITHUB_TOKEN", vars::GITHUB_TOKEN)),
|
||||
.add_env(("GITHUB_TOKEN", &token)),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -354,3 +354,16 @@ pub fn trigger_autofix(run_clippy: bool) -> Step<Run> {
|
||||
))
|
||||
.add_env(("GITHUB_TOKEN", vars::GITHUB_TOKEN))
|
||||
}
|
||||
|
||||
pub fn authenticate_as_zippy() -> (Step<Use>, StepOutput) {
|
||||
let step = named::uses(
|
||||
"actions",
|
||||
"create-github-app-token",
|
||||
"bef1eaf1c0ac2b148ee2a0a74c65fbe6db0631f1",
|
||||
)
|
||||
.add_with(("app-id", vars::ZED_ZIPPY_APP_ID))
|
||||
.add_with(("private-key", vars::ZED_ZIPPY_APP_PRIVATE_KEY))
|
||||
.id("get-app-token");
|
||||
let output = StepOutput::new(&step, "token");
|
||||
(step, output)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user