Compare commits

..

1 Commits

Author SHA1 Message Date
Conrad Irwin
f8ae9d6e1f Fix vim mouse selections
Closes #27720
2025-12-17 17:29:09 -07:00
64 changed files with 851 additions and 2501 deletions

View File

@@ -61,8 +61,7 @@ jobs:
uses: namespacelabs/nscloud-cache-action@v1
with:
cache: rust
- id: cargo_fmt
name: steps::cargo_fmt
- name: steps::cargo_fmt
run: cargo fmt --all -- --check
shell: bash -euxo pipefail {0}
- name: extension_tests::run_clippy

View File

@@ -26,8 +26,7 @@ jobs:
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
node-version: '20'
- id: clippy
name: steps::clippy
- name: steps::clippy
run: ./script/clippy
shell: bash -euxo pipefail {0}
- name: steps::clear_target_dir_if_large
@@ -72,15 +71,15 @@ jobs:
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
node-version: '20'
- id: clippy
name: steps::clippy
- name: steps::clippy
run: ./script/clippy
shell: bash -euxo pipefail {0}
- id: record_clippy_failure
name: steps::record_clippy_failure
if: always()
run: echo "failed=${{ steps.clippy.outcome == 'failure' }}" >> "$GITHUB_OUTPUT"
- name: steps::trigger_autofix
if: failure() && github.event_name == 'pull_request' && github.actor != 'zed-zippy[bot]'
run: gh workflow run autofix_pr.yml -f pr_number=${{ github.event.pull_request.number }} -f run_clippy=true
shell: bash -euxo pipefail {0}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: steps::cargo_install_nextest
uses: taiki-e/install-action@nextest
- name: steps::clear_target_dir_if_large
@@ -94,8 +93,6 @@ jobs:
run: |
rm -rf ./../.cargo
shell: bash -euxo pipefail {0}
outputs:
clippy_failed: ${{ steps.record_clippy_failure.outputs.failed == 'true' }}
timeout-minutes: 60
run_tests_windows:
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions')
@@ -114,8 +111,7 @@ jobs:
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
node-version: '20'
- id: clippy
name: steps::clippy
- name: steps::clippy
run: ./script/clippy.ps1
shell: pwsh
- name: steps::clear_target_dir_if_large

View File

@@ -20,8 +20,7 @@ jobs:
with:
clean: false
fetch-depth: 0
- id: cargo_fmt
name: steps::cargo_fmt
- name: steps::cargo_fmt
run: cargo fmt --all -- --check
shell: bash -euxo pipefail {0}
- name: ./script/clippy
@@ -45,8 +44,7 @@ jobs:
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
node-version: '20'
- id: clippy
name: steps::clippy
- name: steps::clippy
run: ./script/clippy.ps1
shell: pwsh
- name: steps::clear_target_dir_if_large

View File

@@ -74,19 +74,18 @@ jobs:
uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2
with:
version: '9'
- id: prettier
name: steps::prettier
- name: ./script/prettier
run: ./script/prettier
shell: bash -euxo pipefail {0}
- id: cargo_fmt
name: steps::cargo_fmt
- name: steps::cargo_fmt
run: cargo fmt --all -- --check
shell: bash -euxo pipefail {0}
- id: record_style_failure
name: steps::record_style_failure
if: always()
run: echo "failed=${{ steps.prettier.outcome == 'failure' || steps.cargo_fmt.outcome == 'failure' }}" >> "$GITHUB_OUTPUT"
- name: steps::trigger_autofix
if: failure() && github.event_name == 'pull_request' && github.actor != 'zed-zippy[bot]'
run: gh workflow run autofix_pr.yml -f pr_number=${{ github.event.pull_request.number }} -f run_clippy=false
shell: bash -euxo pipefail {0}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: ./script/check-todos
run: ./script/check-todos
shell: bash -euxo pipefail {0}
@@ -97,8 +96,6 @@ jobs:
uses: crate-ci/typos@2d0ce569feab1f8752f1dde43cc2f2aa53236e06
with:
config: ./typos.toml
outputs:
style_failed: ${{ steps.record_style_failure.outputs.failed == 'true' }}
timeout-minutes: 60
run_tests_windows:
needs:
@@ -119,8 +116,7 @@ jobs:
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
node-version: '20'
- id: clippy
name: steps::clippy
- name: steps::clippy
run: ./script/clippy.ps1
shell: pwsh
- name: steps::clear_target_dir_if_large
@@ -167,15 +163,15 @@ jobs:
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
node-version: '20'
- id: clippy
name: steps::clippy
- name: steps::clippy
run: ./script/clippy
shell: bash -euxo pipefail {0}
- id: record_clippy_failure
name: steps::record_clippy_failure
if: always()
run: echo "failed=${{ steps.clippy.outcome == 'failure' }}" >> "$GITHUB_OUTPUT"
- name: steps::trigger_autofix
if: failure() && github.event_name == 'pull_request' && github.actor != 'zed-zippy[bot]'
run: gh workflow run autofix_pr.yml -f pr_number=${{ github.event.pull_request.number }} -f run_clippy=true
shell: bash -euxo pipefail {0}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: steps::cargo_install_nextest
uses: taiki-e/install-action@nextest
- name: steps::clear_target_dir_if_large
@@ -189,8 +185,6 @@ jobs:
run: |
rm -rf ./../.cargo
shell: bash -euxo pipefail {0}
outputs:
clippy_failed: ${{ steps.record_clippy_failure.outputs.failed == 'true' }}
timeout-minutes: 60
run_tests_mac:
needs:
@@ -211,8 +205,7 @@ jobs:
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
node-version: '20'
- id: clippy
name: steps::clippy
- name: steps::clippy
run: ./script/clippy
shell: bash -euxo pipefail {0}
- name: steps::clear_target_dir_if_large
@@ -592,24 +585,6 @@ jobs:
exit $EXIT_CODE
shell: bash -euxo pipefail {0}
call_autofix:
needs:
- check_style
- run_tests_linux
if: always() && (needs.check_style.outputs.style_failed == 'true' || needs.run_tests_linux.outputs.clippy_failed == 'true') && github.event_name == 'pull_request' && github.actor != 'zed-zippy[bot]'
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: run_tests::call_autofix::dispatch_autofix
run: gh workflow run autofix_pr.yml -f pr_number=${{ github.event.pull_request.number }} -f run_clippy=${{ needs.run_tests_linux.outputs.clippy_failed == 'true' }}
shell: bash -euxo pipefail {0}
env:
GITHUB_TOKEN: ${{ steps.get-app-token.outputs.token }}
concurrency:
group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.ref_name == 'main' && github.sha || 'anysha' }}
cancel-in-progress: true

32
Cargo.lock generated
View File

@@ -226,9 +226,9 @@ dependencies = [
[[package]]
name = "agent-client-protocol"
version = "0.9.2"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3e527d7dfe0f334313d42d1d9318f0a79665f6f21c440d0798f230a77a7ed6c"
checksum = "c2ffe7d502c1e451aafc5aff655000f84d09c9af681354ac0012527009b1af13"
dependencies = [
"agent-client-protocol-schema",
"anyhow",
@@ -243,9 +243,9 @@ dependencies = [
[[package]]
name = "agent-client-protocol-schema"
version = "0.10.5"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6903a00e8ac822f9bacac59a1932754d7387c72ebb7c9c7439ad021505591da4"
checksum = "8af81cc2d5c3f9c04f73db452efd058333735ba9d51c2cf7ef33c9fee038e7e6"
dependencies = [
"anyhow",
"derive_more 2.0.1",
@@ -793,7 +793,7 @@ dependencies = [
"url",
"wayland-backend",
"wayland-client",
"wayland-protocols",
"wayland-protocols 0.32.9",
"zbus",
]
@@ -7370,7 +7370,7 @@ dependencies = [
"wayland-backend",
"wayland-client",
"wayland-cursor",
"wayland-protocols",
"wayland-protocols 0.31.2",
"wayland-protocols-plasma",
"wayland-protocols-wlr",
"windows 0.61.3",
@@ -18927,6 +18927,18 @@ dependencies = [
"xcursor",
]
[[package]]
name = "wayland-protocols"
version = "0.31.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f81f365b8b4a97f422ac0e8737c438024b5951734506b0e1d775c73030561f4"
dependencies = [
"bitflags 2.9.4",
"wayland-backend",
"wayland-client",
"wayland-scanner",
]
[[package]]
name = "wayland-protocols"
version = "0.32.9"
@@ -18941,14 +18953,14 @@ dependencies = [
[[package]]
name = "wayland-protocols-plasma"
version = "0.3.9"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a07a14257c077ab3279987c4f8bb987851bf57081b93710381daea94f2c2c032"
checksum = "23803551115ff9ea9bce586860c5c5a971e360825a0309264102a9495a5ff479"
dependencies = [
"bitflags 2.9.4",
"wayland-backend",
"wayland-client",
"wayland-protocols",
"wayland-protocols 0.31.2",
"wayland-scanner",
]
@@ -18961,7 +18973,7 @@ dependencies = [
"bitflags 2.9.4",
"wayland-backend",
"wayland-client",
"wayland-protocols",
"wayland-protocols 0.32.9",
"wayland-scanner",
]

View File

@@ -438,7 +438,7 @@ ztracing_macro = { path = "crates/ztracing_macro" }
# External crates
#
agent-client-protocol = { version = "=0.9.2", features = ["unstable"] }
agent-client-protocol = { version = "=0.9.0", features = ["unstable"] }
aho-corasick = "1.1"
alacritty_terminal = "0.25.1-rc1"
any_vec = "0.14"

View File

@@ -227,7 +227,6 @@
"ctrl-g": "search::SelectNextMatch",
"ctrl-shift-g": "search::SelectPreviousMatch",
"ctrl-k l": "agent::OpenRulesLibrary",
"ctrl-shift-v": "agent::PasteRaw",
},
},
{
@@ -294,7 +293,6 @@
"shift-ctrl-r": "agent::OpenAgentDiff",
"ctrl-shift-y": "agent::KeepAll",
"ctrl-shift-n": "agent::RejectAll",
"ctrl-shift-v": "agent::PasteRaw",
},
},
{
@@ -306,7 +304,6 @@
"shift-ctrl-r": "agent::OpenAgentDiff",
"ctrl-shift-y": "agent::KeepAll",
"ctrl-shift-n": "agent::RejectAll",
"ctrl-shift-v": "agent::PasteRaw",
},
},
{

View File

@@ -267,7 +267,6 @@
"cmd-shift-g": "search::SelectPreviousMatch",
"cmd-k l": "agent::OpenRulesLibrary",
"alt-tab": "agent::CycleFavoriteModels",
"cmd-shift-v": "agent::PasteRaw",
},
},
{
@@ -336,7 +335,6 @@
"shift-ctrl-r": "agent::OpenAgentDiff",
"cmd-shift-y": "agent::KeepAll",
"cmd-shift-n": "agent::RejectAll",
"cmd-shift-v": "agent::PasteRaw",
},
},
{
@@ -349,7 +347,6 @@
"shift-ctrl-r": "agent::OpenAgentDiff",
"cmd-shift-y": "agent::KeepAll",
"cmd-shift-n": "agent::RejectAll",
"cmd-shift-v": "agent::PasteRaw",
},
},
{

View File

@@ -227,7 +227,6 @@
"ctrl-g": "search::SelectNextMatch",
"ctrl-shift-g": "search::SelectPreviousMatch",
"ctrl-k l": "agent::OpenRulesLibrary",
"ctrl-shift-v": "agent::PasteRaw",
},
},
{
@@ -297,7 +296,6 @@
"ctrl-shift-r": "agent::OpenAgentDiff",
"ctrl-shift-y": "agent::KeepAll",
"ctrl-shift-n": "agent::RejectAll",
"ctrl-shift-v": "agent::PasteRaw",
},
},
{
@@ -310,7 +308,6 @@
"ctrl-shift-r": "agent::OpenAgentDiff",
"ctrl-shift-y": "agent::KeepAll",
"ctrl-shift-n": "agent::RejectAll",
"ctrl-shift-v": "agent::PasteRaw",
},
},
{

View File

@@ -192,7 +192,6 @@ pub struct ToolCall {
pub locations: Vec<acp::ToolCallLocation>,
pub resolved_locations: Vec<Option<AgentLocation>>,
pub raw_input: Option<serde_json::Value>,
pub raw_input_markdown: Option<Entity<Markdown>>,
pub raw_output: Option<serde_json::Value>,
}
@@ -223,11 +222,6 @@ impl ToolCall {
}
}
let raw_input_markdown = tool_call
.raw_input
.as_ref()
.and_then(|input| markdown_for_raw_output(input, &language_registry, cx));
let result = Self {
id: tool_call.tool_call_id,
label: cx
@@ -238,7 +232,6 @@ impl ToolCall {
resolved_locations: Vec::default(),
status,
raw_input: tool_call.raw_input,
raw_input_markdown,
raw_output: tool_call.raw_output,
};
Ok(result)
@@ -314,7 +307,6 @@ impl ToolCall {
}
if let Some(raw_input) = raw_input {
self.raw_input_markdown = markdown_for_raw_output(&raw_input, &language_registry, cx);
self.raw_input = Some(raw_input);
}
@@ -1363,7 +1355,6 @@ impl AcpThread {
locations: Vec::new(),
resolved_locations: Vec::new(),
raw_input: None,
raw_input_markdown: None,
raw_output: None,
};
self.push_entry(AgentThreadEntry::ToolCall(failed_tool_call), cx);

View File

@@ -34,7 +34,7 @@ use theme::ThemeSettings;
use ui::prelude::*;
use util::{ResultExt, debug_panic};
use workspace::{CollaboratorId, Workspace};
use zed_actions::agent::{Chat, PasteRaw};
use zed_actions::agent::Chat;
pub struct MessageEditor {
mention_set: Entity<MentionSet>,
@@ -543,9 +543,6 @@ impl MessageEditor {
}
fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context<Self>) {
let Some(workspace) = self.workspace.upgrade() else {
return;
};
let editor_clipboard_selections = cx
.read_from_clipboard()
.and_then(|item| item.entries().first().cloned())
@@ -556,127 +553,133 @@ impl MessageEditor {
_ => None,
});
// Insert creases for pasted clipboard selections that:
// 1. Contain exactly one selection
// 2. Have an associated file path
// 3. Span multiple lines (not single-line selections)
// 4. Belong to a file that exists in the current project
let should_insert_creases = util::maybe!({
let selections = editor_clipboard_selections.as_ref()?;
if selections.len() > 1 {
return Some(false);
}
let selection = selections.first()?;
let file_path = selection.file_path.as_ref()?;
let line_range = selection.line_range.as_ref()?;
let has_file_context = editor_clipboard_selections
.as_ref()
.is_some_and(|selections| {
selections
.iter()
.any(|sel| sel.file_path.is_some() && sel.line_range.is_some())
});
if line_range.start() == line_range.end() {
return Some(false);
}
Some(
workspace
.read(cx)
.project()
.read(cx)
.project_path_for_absolute_path(file_path, cx)
.is_some(),
)
})
.unwrap_or(false);
if should_insert_creases && let Some(selections) = editor_clipboard_selections {
cx.stop_propagation();
let insertion_target = self
.editor
.read(cx)
.selections
.newest_anchor()
.start
.text_anchor;
let project = workspace.read(cx).project().clone();
for selection in selections {
if let (Some(file_path), Some(line_range)) =
(selection.file_path, selection.line_range)
{
let crease_text =
acp_thread::selection_name(Some(file_path.as_ref()), &line_range);
let mention_uri = MentionUri::Selection {
abs_path: Some(file_path.clone()),
line_range: line_range.clone(),
};
let mention_text = mention_uri.as_link().to_string();
let (excerpt_id, text_anchor, content_len) =
self.editor.update(cx, |editor, cx| {
let buffer = editor.buffer().read(cx);
let snapshot = buffer.snapshot(cx);
let (excerpt_id, _, buffer_snapshot) = snapshot.as_singleton().unwrap();
let text_anchor = insertion_target.bias_left(&buffer_snapshot);
editor.insert(&mention_text, window, cx);
editor.insert(" ", window, cx);
(*excerpt_id, text_anchor, mention_text.len())
});
let Some((crease_id, tx)) = insert_crease_for_mention(
excerpt_id,
text_anchor,
content_len,
crease_text.into(),
mention_uri.icon_path(cx),
None,
self.editor.clone(),
window,
cx,
) else {
continue;
};
drop(tx);
let mention_task = cx
.spawn({
let project = project.clone();
async move |_, cx| {
let project_path = project
.update(cx, |project, cx| {
project.project_path_for_absolute_path(&file_path, cx)
})
.map_err(|e| e.to_string())?
.ok_or_else(|| "project path not found".to_string())?;
let buffer = project
.update(cx, |project, cx| project.open_buffer(project_path, cx))
.map_err(|e| e.to_string())?
.await
.map_err(|e| e.to_string())?;
buffer
.update(cx, |buffer, cx| {
let start = Point::new(*line_range.start(), 0)
.min(buffer.max_point());
let end = Point::new(*line_range.end() + 1, 0)
.min(buffer.max_point());
let content = buffer.text_for_range(start..end).collect();
Mention::Text {
content,
tracked_buffers: vec![cx.entity()],
}
})
.map_err(|e| e.to_string())
}
})
.shared();
self.mention_set.update(cx, |mention_set, _cx| {
mention_set.insert_mention(crease_id, mention_uri.clone(), mention_task)
});
if has_file_context {
if let Some((workspace, selections)) =
self.workspace.upgrade().zip(editor_clipboard_selections)
{
let Some(first_selection) = selections.first() else {
return;
};
if let Some(file_path) = &first_selection.file_path {
// In case someone pastes selections from another window
// with a different project, we don't want to insert the
// crease (containing the absolute path) since the agent
// cannot access files outside the project.
let is_in_project = workspace
.read(cx)
.project()
.read(cx)
.project_path_for_absolute_path(file_path, cx)
.is_some();
if !is_in_project {
return;
}
}
cx.stop_propagation();
let insertion_target = self
.editor
.read(cx)
.selections
.newest_anchor()
.start
.text_anchor;
let project = workspace.read(cx).project().clone();
for selection in selections {
if let (Some(file_path), Some(line_range)) =
(selection.file_path, selection.line_range)
{
let crease_text =
acp_thread::selection_name(Some(file_path.as_ref()), &line_range);
let mention_uri = MentionUri::Selection {
abs_path: Some(file_path.clone()),
line_range: line_range.clone(),
};
let mention_text = mention_uri.as_link().to_string();
let (excerpt_id, text_anchor, content_len) =
self.editor.update(cx, |editor, cx| {
let buffer = editor.buffer().read(cx);
let snapshot = buffer.snapshot(cx);
let (excerpt_id, _, buffer_snapshot) =
snapshot.as_singleton().unwrap();
let text_anchor = insertion_target.bias_left(&buffer_snapshot);
editor.insert(&mention_text, window, cx);
editor.insert(" ", window, cx);
(*excerpt_id, text_anchor, mention_text.len())
});
let Some((crease_id, tx)) = insert_crease_for_mention(
excerpt_id,
text_anchor,
content_len,
crease_text.into(),
mention_uri.icon_path(cx),
None,
self.editor.clone(),
window,
cx,
) else {
continue;
};
drop(tx);
let mention_task = cx
.spawn({
let project = project.clone();
async move |_, cx| {
let project_path = project
.update(cx, |project, cx| {
project.project_path_for_absolute_path(&file_path, cx)
})
.map_err(|e| e.to_string())?
.ok_or_else(|| "project path not found".to_string())?;
let buffer = project
.update(cx, |project, cx| {
project.open_buffer(project_path, cx)
})
.map_err(|e| e.to_string())?
.await
.map_err(|e| e.to_string())?;
buffer
.update(cx, |buffer, cx| {
let start = Point::new(*line_range.start(), 0)
.min(buffer.max_point());
let end = Point::new(*line_range.end() + 1, 0)
.min(buffer.max_point());
let content =
buffer.text_for_range(start..end).collect();
Mention::Text {
content,
tracked_buffers: vec![cx.entity()],
}
})
.map_err(|e| e.to_string())
}
})
.shared();
self.mention_set.update(cx, |mention_set, _cx| {
mention_set.insert_mention(crease_id, mention_uri.clone(), mention_task)
});
}
}
return;
}
return;
}
if self.prompt_capabilities.borrow().image
@@ -687,13 +690,6 @@ impl MessageEditor {
}
}
fn paste_raw(&mut self, _: &PasteRaw, window: &mut Window, cx: &mut Context<Self>) {
let editor = self.editor.clone();
window.defer(cx, move |window, cx| {
editor.update(cx, |editor, cx| editor.paste(&Paste, window, cx));
});
}
pub fn insert_dragged_files(
&mut self,
paths: Vec<project::ProjectPath>,
@@ -971,7 +967,6 @@ impl Render for MessageEditor {
.on_action(cx.listener(Self::chat))
.on_action(cx.listener(Self::chat_with_follow))
.on_action(cx.listener(Self::cancel))
.on_action(cx.listener(Self::paste_raw))
.capture_action(cx.listener(Self::paste))
.flex_1()
.child({

View File

@@ -221,7 +221,7 @@ impl PickerDelegate for AcpModelPickerDelegate {
cx: &mut Context<Picker<Self>>,
) -> Task<()> {
let favorites = if self.selector.supports_favorites() {
AgentSettings::get_global(cx).favorite_model_ids()
Arc::new(AgentSettings::get_global(cx).favorite_model_ids())
} else {
Default::default()
};
@@ -242,7 +242,7 @@ impl PickerDelegate for AcpModelPickerDelegate {
this.update_in(cx, |this, window, cx| {
this.delegate.filtered_entries =
info_list_to_picker_entries(filtered_models, &favorites);
info_list_to_picker_entries(filtered_models, favorites);
// Finds the currently selected model in the list
let new_index = this
.delegate
@@ -406,7 +406,7 @@ impl PickerDelegate for AcpModelPickerDelegate {
fn info_list_to_picker_entries(
model_list: AgentModelList,
favorites: &HashSet<ModelId>,
favorites: Arc<HashSet<ModelId>>,
) -> Vec<AcpModelPickerEntry> {
let mut entries = Vec::new();
@@ -572,11 +572,13 @@ mod tests {
}
}
fn create_favorites(models: Vec<&str>) -> HashSet<ModelId> {
models
.into_iter()
.map(|m| ModelId::new(m.to_string()))
.collect()
fn create_favorites(models: Vec<&str>) -> Arc<HashSet<ModelId>> {
Arc::new(
models
.into_iter()
.map(|m| ModelId::new(m.to_string()))
.collect(),
)
}
fn get_entry_model_ids(entries: &[AcpModelPickerEntry]) -> Vec<&str> {
@@ -607,7 +609,7 @@ mod tests {
]);
let favorites = create_favorites(vec!["zed/gemini"]);
let entries = info_list_to_picker_entries(models, &favorites);
let entries = info_list_to_picker_entries(models, favorites);
assert!(matches!(
entries.first(),
@@ -623,7 +625,7 @@ mod tests {
let models = create_model_list(vec![("zed", vec!["zed/claude", "zed/gemini"])]);
let favorites = create_favorites(vec![]);
let entries = info_list_to_picker_entries(models, &favorites);
let entries = info_list_to_picker_entries(models, favorites);
assert!(matches!(
entries.first(),
@@ -639,7 +641,7 @@ mod tests {
]);
let favorites = create_favorites(vec!["zed/claude"]);
let entries = info_list_to_picker_entries(models, &favorites);
let entries = info_list_to_picker_entries(models, favorites);
for entry in &entries {
if let AcpModelPickerEntry::Model(info, is_favorite) = entry {
@@ -660,7 +662,7 @@ mod tests {
]);
let favorites = create_favorites(vec!["zed/gemini", "openai/gpt-5"]);
let entries = info_list_to_picker_entries(models, &favorites);
let entries = info_list_to_picker_entries(models, favorites);
let model_ids = get_entry_model_ids(&entries);
assert_eq!(model_ids[0], "zed/gemini");
@@ -681,7 +683,7 @@ mod tests {
let favorites = create_favorites(vec!["zed/claude"]);
let entries = info_list_to_picker_entries(models, &favorites);
let entries = info_list_to_picker_entries(models, favorites);
let labels = get_entry_labels(&entries);
assert_eq!(
@@ -721,7 +723,7 @@ mod tests {
]);
let favorites = create_favorites(vec!["zed/gemini"]);
let entries = info_list_to_picker_entries(models, &favorites);
let entries = info_list_to_picker_entries(models, favorites);
assert!(matches!(
entries.first(),

View File

@@ -34,7 +34,7 @@ use language::Buffer;
use language_model::LanguageModelRegistry;
use markdown::{HeadingLevelStyles, Markdown, MarkdownElement, MarkdownStyle};
use project::{AgentServerStore, ExternalAgentServerName, Project, ProjectEntryId};
use project::{Project, ProjectEntryId};
use prompt_store::{PromptId, PromptStore};
use rope::Point;
use settings::{NotifyWhenAgentWaiting, Settings as _, SettingsStore};
@@ -260,7 +260,6 @@ impl ThreadFeedbackState {
pub struct AcpThreadView {
agent: Rc<dyn AgentServer>,
agent_server_store: Entity<AgentServerStore>,
workspace: WeakEntity<Workspace>,
project: Entity<Project>,
thread_state: ThreadState,
@@ -407,7 +406,6 @@ impl AcpThreadView {
Self {
agent: agent.clone(),
agent_server_store,
workspace: workspace.clone(),
project: project.clone(),
entry_view_state,
@@ -739,7 +737,7 @@ impl AcpThreadView {
cx: &mut App,
) {
let agent_name = agent.name();
let (configuration_view, subscription) = if let Some(provider_id) = &err.provider_id {
let (configuration_view, subscription) = if let Some(provider_id) = err.provider_id {
let registry = LanguageModelRegistry::global(cx);
let sub = window.subscribe(&registry, cx, {
@@ -781,6 +779,7 @@ impl AcpThreadView {
configuration_view,
description: err
.description
.clone()
.map(|desc| cx.new(|cx| Markdown::new(desc.into(), None, None, cx))),
_subscription: subscription,
};
@@ -1089,7 +1088,10 @@ impl AcpThreadView {
window.defer(cx, |window, cx| {
Self::handle_auth_required(
this,
AuthRequired::new(),
AuthRequired {
description: None,
provider_id: None,
},
agent,
connection,
window,
@@ -1661,6 +1663,44 @@ impl AcpThreadView {
});
return;
}
} else if method.0.as_ref() == "anthropic-api-key" {
let registry = LanguageModelRegistry::global(cx);
let provider = registry
.read(cx)
.provider(&language_model::ANTHROPIC_PROVIDER_ID)
.unwrap();
let this = cx.weak_entity();
let agent = self.agent.clone();
let connection = connection.clone();
window.defer(cx, move |window, cx| {
if !provider.is_authenticated(cx) {
Self::handle_auth_required(
this,
AuthRequired {
description: Some("ANTHROPIC_API_KEY must be set".to_owned()),
provider_id: Some(language_model::ANTHROPIC_PROVIDER_ID),
},
agent,
connection,
window,
cx,
);
} else {
this.update(cx, |this, cx| {
this.thread_state = Self::initial_state(
agent,
None,
this.workspace.clone(),
this.project.clone(),
true,
window,
cx,
)
})
.ok();
}
});
return;
} else if method.0.as_ref() == "vertex-ai"
&& std::env::var("GOOGLE_API_KEY").is_err()
&& (std::env::var("GOOGLE_CLOUD_PROJECT").is_err()
@@ -2113,7 +2153,6 @@ impl AcpThreadView {
chunks,
indented: _,
}) => {
let mut is_blank = true;
let is_last = entry_ix + 1 == total_entries;
let style = default_markdown_style(false, false, window, cx);
@@ -2123,55 +2162,36 @@ impl AcpThreadView {
.children(chunks.iter().enumerate().filter_map(
|(chunk_ix, chunk)| match chunk {
AssistantMessageChunk::Message { block } => {
block.markdown().and_then(|md| {
let this_is_blank = md.read(cx).source().trim().is_empty();
is_blank = is_blank && this_is_blank;
if this_is_blank {
return None;
}
Some(
self.render_markdown(md.clone(), style.clone())
.into_any_element(),
)
block.markdown().map(|md| {
self.render_markdown(md.clone(), style.clone())
.into_any_element()
})
}
AssistantMessageChunk::Thought { block } => {
block.markdown().and_then(|md| {
let this_is_blank = md.read(cx).source().trim().is_empty();
is_blank = is_blank && this_is_blank;
if this_is_blank {
return None;
}
Some(
self.render_thinking_block(
entry_ix,
chunk_ix,
md.clone(),
window,
cx,
)
.into_any_element(),
block.markdown().map(|md| {
self.render_thinking_block(
entry_ix,
chunk_ix,
md.clone(),
window,
cx,
)
.into_any_element()
})
}
},
))
.into_any();
if is_blank {
Empty.into_any()
} else {
v_flex()
.px_5()
.py_1p5()
.when(is_last, |this| this.pb_4())
.w_full()
.text_ui(cx)
.child(message_body)
.into_any()
}
v_flex()
.px_5()
.py_1p5()
.when(is_first_indented, |this| this.pt_0p5())
.when(is_last, |this| this.pb_4())
.w_full()
.text_ui(cx)
.child(message_body)
.into_any()
}
AgentThreadEntry::ToolCall(tool_call) => {
let has_terminals = tool_call.terminals().next().is_some();
@@ -2203,7 +2223,7 @@ impl AcpThreadView {
div()
.relative()
.w_full()
.pl_5()
.pl(rems_from_px(20.0))
.bg(cx.theme().colors().panel_background.opacity(0.2))
.child(
div()
@@ -2420,12 +2440,6 @@ impl AcpThreadView {
let is_collapsible = !tool_call.content.is_empty() && !needs_confirmation;
let is_open = needs_confirmation || self.expanded_tool_calls.contains(&tool_call.id);
let input_output_header = |label: SharedString| {
Label::new(label)
.size(LabelSize::XSmall)
.color(Color::Muted)
.buffer_font(cx)
};
let tool_output_display =
if is_open {
@@ -2467,25 +2481,7 @@ impl AcpThreadView {
| ToolCallStatus::Completed
| ToolCallStatus::Failed
| ToolCallStatus::Canceled => v_flex()
.when(!is_edit && !is_terminal_tool, |this| {
this.mt_1p5().w_full().child(
v_flex()
.ml(rems(0.4))
.px_3p5()
.pb_1()
.gap_1()
.border_l_1()
.border_color(self.tool_card_border_color(cx))
.child(input_output_header("Raw Input:".into()))
.children(tool_call.raw_input_markdown.clone().map(|input| {
self.render_markdown(
input,
default_markdown_style(false, false, window, cx),
)
}))
.child(input_output_header("Output:".into())),
)
})
.w_full()
.children(tool_call.content.iter().enumerate().map(
|(content_ix, content)| {
div().child(self.render_tool_call_content(
@@ -2584,7 +2580,7 @@ impl AcpThreadView {
.gap_px()
.when(is_collapsible, |this| {
this.child(
Disclosure::new(("expand-output", entry_ix), is_open)
Disclosure::new(("expand", entry_ix), is_open)
.opened_icon(IconName::ChevronUp)
.closed_icon(IconName::ChevronDown)
.visible_on_hover(&card_header_id)
@@ -2770,20 +2766,20 @@ impl AcpThreadView {
let button_id = SharedString::from(format!("tool_output-{:?}", tool_call_id));
v_flex()
.mt_1p5()
.gap_2()
.map(|this| {
if card_layout {
this.when(context_ix > 0, |this| {
this.pt_2()
.border_t_1()
.border_color(self.tool_card_border_color(cx))
})
} else {
this.ml(rems(0.4))
.px_3p5()
.border_l_1()
.when(!card_layout, |this| {
this.ml(rems(0.4))
.px_3p5()
.border_l_1()
.border_color(self.tool_card_border_color(cx))
})
.when(card_layout, |this| {
this.px_2().pb_2().when(context_ix > 0, |this| {
this.border_t_1()
.pt_2()
.border_color(self.tool_card_border_color(cx))
}
})
})
.text_xs()
.text_color(cx.theme().colors().text_muted)
@@ -3504,119 +3500,138 @@ impl AcpThreadView {
pending_auth_method: Option<&acp::AuthMethodId>,
window: &mut Window,
cx: &Context<Self>,
) -> impl IntoElement {
) -> Div {
let show_description =
configuration_view.is_none() && description.is_none() && pending_auth_method.is_none();
let auth_methods = connection.auth_methods();
let agent_display_name = self
.agent_server_store
.read(cx)
.agent_display_name(&ExternalAgentServerName(self.agent.name()))
.unwrap_or_else(|| self.agent.name());
let show_fallback_description = auth_methods.len() > 1
&& configuration_view.is_none()
&& description.is_none()
&& pending_auth_method.is_none();
let auth_buttons = || {
h_flex().justify_end().flex_wrap().gap_1().children(
connection
.auth_methods()
.iter()
.enumerate()
.rev()
.map(|(ix, method)| {
let (method_id, name) = if self.project.read(cx).is_via_remote_server()
&& method.id.0.as_ref() == "oauth-personal"
&& method.name == "Log in with Google"
{
("spawn-gemini-cli".into(), "Log in with Gemini CLI".into())
} else {
(method.id.0.clone(), method.name.clone())
};
let agent_telemetry_id = connection.telemetry_id();
Button::new(method_id.clone(), name)
.label_size(LabelSize::Small)
.map(|this| {
if ix == 0 {
this.style(ButtonStyle::Tinted(TintColor::Accent))
} else {
this.style(ButtonStyle::Outlined)
}
})
.when_some(method.description.clone(), |this, description| {
this.tooltip(Tooltip::text(description))
})
.on_click({
cx.listener(move |this, _, window, cx| {
telemetry::event!(
"Authenticate Agent Started",
agent = agent_telemetry_id,
method = method_id
);
this.authenticate(
acp::AuthMethodId::new(method_id.clone()),
window,
cx,
)
})
})
}),
)
};
if pending_auth_method.is_some() {
return Callout::new()
.icon(IconName::Info)
.title(format!("Authenticating to {}", agent_display_name))
.actions_slot(
Icon::new(IconName::ArrowCircle)
.size(IconSize::Small)
.color(Color::Muted)
.with_rotate_animation(2)
.into_any_element(),
v_flex().flex_1().size_full().justify_end().child(
v_flex()
.p_2()
.pr_3()
.w_full()
.gap_1()
.border_t_1()
.border_color(cx.theme().colors().border)
.bg(cx.theme().status().warning.opacity(0.04))
.child(
h_flex()
.gap_1p5()
.child(
Icon::new(IconName::Warning)
.color(Color::Warning)
.size(IconSize::Small),
)
.child(Label::new("Authentication Required").size(LabelSize::Small)),
)
.into_any_element();
}
.children(description.map(|desc| {
div().text_ui(cx).child(self.render_markdown(
desc.clone(),
default_markdown_style(false, false, window, cx),
))
}))
.children(
configuration_view
.cloned()
.map(|view| div().w_full().child(view)),
)
.when(show_description, |el| {
el.child(
Label::new(format!(
"You are not currently authenticated with {}.{}",
self.agent.name(),
if auth_methods.len() > 1 {
" Please choose one of the following options:"
} else {
""
}
))
.size(LabelSize::Small)
.color(Color::Muted)
.mb_1()
.ml_5(),
)
})
.when_some(pending_auth_method, |el, _| {
el.child(
h_flex()
.py_4()
.w_full()
.justify_center()
.gap_1()
.child(
Icon::new(IconName::ArrowCircle)
.size(IconSize::Small)
.color(Color::Muted)
.with_rotate_animation(2),
)
.child(Label::new("Authenticating…").size(LabelSize::Small)),
)
})
.when(!auth_methods.is_empty(), |this| {
this.child(
h_flex()
.justify_end()
.flex_wrap()
.gap_1()
.when(!show_description, |this| {
this.border_t_1()
.mt_1()
.pt_2()
.border_color(cx.theme().colors().border.opacity(0.8))
})
.children(connection.auth_methods().iter().enumerate().rev().map(
|(ix, method)| {
let (method_id, name) = if self
.project
.read(cx)
.is_via_remote_server()
&& method.id.0.as_ref() == "oauth-personal"
&& method.name == "Log in with Google"
{
("spawn-gemini-cli".into(), "Log in with Gemini CLI".into())
} else {
(method.id.0.clone(), method.name.clone())
};
Callout::new()
.icon(IconName::Info)
.title(format!("Authenticate to {}", agent_display_name))
.when(auth_methods.len() == 1, |this| {
this.actions_slot(auth_buttons())
})
.description_slot(
v_flex()
.text_ui(cx)
.map(|this| {
if show_fallback_description {
this.child(
Label::new("Choose one of the following authentication options:")
.size(LabelSize::Small)
.color(Color::Muted),
)
} else {
this.children(
configuration_view
.cloned()
.map(|view| div().w_full().child(view)),
)
.children(description.map(|desc| {
self.render_markdown(
desc.clone(),
default_markdown_style(false, false, window, cx),
)
}))
}
})
.when(auth_methods.len() > 1, |this| {
this.gap_1().child(auth_buttons())
}),
)
.into_any_element()
let agent_telemetry_id = connection.telemetry_id();
Button::new(method_id.clone(), name)
.label_size(LabelSize::Small)
.map(|this| {
if ix == 0 {
this.style(ButtonStyle::Tinted(TintColor::Warning))
} else {
this.style(ButtonStyle::Outlined)
}
})
.when_some(
method.description.clone(),
|this, description| {
this.tooltip(Tooltip::text(description))
},
)
.on_click({
cx.listener(move |this, _, window, cx| {
telemetry::event!(
"Authenticate Agent Started",
agent = agent_telemetry_id,
method = method_id
);
this.authenticate(
acp::AuthMethodId::new(method_id.clone()),
window,
cx,
)
})
})
},
)),
)
}),
)
}
fn render_load_error(
@@ -5865,6 +5880,10 @@ impl AcpThreadView {
};
let connection = thread.read(cx).connection().clone();
let err = AuthRequired {
description: None,
provider_id: None,
};
this.clear_thread_error(cx);
if let Some(message) = this.in_flight_prompt.take() {
this.message_editor.update(cx, |editor, cx| {
@@ -5873,14 +5892,7 @@ impl AcpThreadView {
}
let this = cx.weak_entity();
window.defer(cx, |window, cx| {
Self::handle_auth_required(
this,
AuthRequired::new(),
agent,
connection,
window,
cx,
);
Self::handle_auth_required(this, err, agent, connection, window, cx);
})
}
}))
@@ -5893,10 +5905,14 @@ impl AcpThreadView {
};
let connection = thread.read(cx).connection().clone();
let err = AuthRequired {
description: None,
provider_id: None,
};
self.clear_thread_error(cx);
let this = cx.weak_entity();
window.defer(cx, |window, cx| {
Self::handle_auth_required(this, AuthRequired::new(), agent, connection, window, cx);
Self::handle_auth_required(this, err, agent, connection, window, cx);
})
}
@@ -5999,19 +6015,16 @@ impl Render for AcpThreadView {
configuration_view,
pending_auth_method,
..
} => v_flex()
.flex_1()
.size_full()
.justify_end()
.child(self.render_auth_required_state(
} => self
.render_auth_required_state(
connection,
description.as_ref(),
configuration_view.as_ref(),
pending_auth_method.as_ref(),
window,
cx,
))
.into_any_element(),
)
.into_any(),
ThreadState::Loading { .. } => v_flex()
.flex_1()
.child(self.render_recent_history(cx))

View File

@@ -71,7 +71,7 @@ use workspace::{
pane,
searchable::{SearchEvent, SearchableItem},
};
use zed_actions::agent::{AddSelectionToThread, PasteRaw, ToggleModelSelector};
use zed_actions::agent::{AddSelectionToThread, ToggleModelSelector};
use crate::CycleFavoriteModels;
@@ -1698,9 +1698,6 @@ impl TextThreadEditor {
window: &mut Window,
cx: &mut Context<Self>,
) {
let Some(workspace) = self.workspace.upgrade() else {
return;
};
let editor_clipboard_selections = cx
.read_from_clipboard()
.and_then(|item| item.entries().first().cloned())
@@ -1711,101 +1708,84 @@ impl TextThreadEditor {
_ => None,
});
// Insert creases for pasted clipboard selections that:
// 1. Contain exactly one selection
// 2. Have an associated file path
// 3. Span multiple lines (not single-line selections)
// 4. Belong to a file that exists in the current project
let should_insert_creases = util::maybe!({
let selections = editor_clipboard_selections.as_ref()?;
if selections.len() > 1 {
return Some(false);
}
let selection = selections.first()?;
let file_path = selection.file_path.as_ref()?;
let line_range = selection.line_range.as_ref()?;
let has_file_context = editor_clipboard_selections
.as_ref()
.is_some_and(|selections| {
selections
.iter()
.any(|sel| sel.file_path.is_some() && sel.line_range.is_some())
});
if line_range.start() == line_range.end() {
return Some(false);
}
if has_file_context {
if let Some(clipboard_item) = cx.read_from_clipboard() {
if let Some(ClipboardEntry::String(clipboard_text)) =
clipboard_item.entries().first()
{
if let Some(selections) = editor_clipboard_selections {
cx.stop_propagation();
Some(
workspace
.read(cx)
.project()
.read(cx)
.project_path_for_absolute_path(file_path, cx)
.is_some(),
)
})
.unwrap_or(false);
let text = clipboard_text.text();
self.editor.update(cx, |editor, cx| {
let mut current_offset = 0;
let weak_editor = cx.entity().downgrade();
if should_insert_creases && let Some(clipboard_item) = cx.read_from_clipboard() {
if let Some(ClipboardEntry::String(clipboard_text)) = clipboard_item.entries().first() {
if let Some(selections) = editor_clipboard_selections {
cx.stop_propagation();
for selection in selections {
if let (Some(file_path), Some(line_range)) =
(selection.file_path, selection.line_range)
{
let selected_text =
&text[current_offset..current_offset + selection.len];
let fence = assistant_slash_commands::codeblock_fence_for_path(
file_path.to_str(),
Some(line_range.clone()),
);
let formatted_text = format!("{fence}{selected_text}\n```");
let text = clipboard_text.text();
self.editor.update(cx, |editor, cx| {
let mut current_offset = 0;
let weak_editor = cx.entity().downgrade();
let insert_point = editor
.selections
.newest::<Point>(&editor.display_snapshot(cx))
.head();
let start_row = MultiBufferRow(insert_point.row);
for selection in selections {
if let (Some(file_path), Some(line_range)) =
(selection.file_path, selection.line_range)
{
let selected_text =
&text[current_offset..current_offset + selection.len];
let fence = assistant_slash_commands::codeblock_fence_for_path(
file_path.to_str(),
Some(line_range.clone()),
);
let formatted_text = format!("{fence}{selected_text}\n```");
editor.insert(&formatted_text, window, cx);
let insert_point = editor
.selections
.newest::<Point>(&editor.display_snapshot(cx))
.head();
let start_row = MultiBufferRow(insert_point.row);
let snapshot = editor.buffer().read(cx).snapshot(cx);
let anchor_before = snapshot.anchor_after(insert_point);
let anchor_after = editor
.selections
.newest_anchor()
.head()
.bias_left(&snapshot);
editor.insert(&formatted_text, window, cx);
editor.insert("\n", window, cx);
let snapshot = editor.buffer().read(cx).snapshot(cx);
let anchor_before = snapshot.anchor_after(insert_point);
let anchor_after = editor
.selections
.newest_anchor()
.head()
.bias_left(&snapshot);
let crease_text = acp_thread::selection_name(
Some(file_path.as_ref()),
&line_range,
);
editor.insert("\n", window, cx);
let fold_placeholder = quote_selection_fold_placeholder(
crease_text,
weak_editor.clone(),
);
let crease = Crease::inline(
anchor_before..anchor_after,
fold_placeholder,
render_quote_selection_output_toggle,
|_, _, _, _| Empty.into_any(),
);
editor.insert_creases(vec![crease], cx);
editor.fold_at(start_row, window, cx);
let crease_text = acp_thread::selection_name(
Some(file_path.as_ref()),
&line_range,
);
let fold_placeholder = quote_selection_fold_placeholder(
crease_text,
weak_editor.clone(),
);
let crease = Crease::inline(
anchor_before..anchor_after,
fold_placeholder,
render_quote_selection_output_toggle,
|_, _, _, _| Empty.into_any(),
);
editor.insert_creases(vec![crease], cx);
editor.fold_at(start_row, window, cx);
current_offset += selection.len;
if !selection.is_entire_line && current_offset < text.len() {
current_offset += 1;
current_offset += selection.len;
if !selection.is_entire_line && current_offset < text.len() {
current_offset += 1;
}
}
}
}
});
return;
});
return;
}
}
}
}
@@ -1964,12 +1944,6 @@ impl TextThreadEditor {
}
}
fn paste_raw(&mut self, _: &PasteRaw, window: &mut Window, cx: &mut Context<Self>) {
self.editor.update(cx, |editor, cx| {
editor.paste(&editor::actions::Paste, window, cx);
});
}
fn update_image_blocks(&mut self, cx: &mut Context<Self>) {
self.editor.update(cx, |editor, cx| {
let buffer = editor.buffer().read(cx).snapshot(cx);
@@ -2653,7 +2627,6 @@ impl Render for TextThreadEditor {
.capture_action(cx.listener(TextThreadEditor::copy))
.capture_action(cx.listener(TextThreadEditor::cut))
.capture_action(cx.listener(TextThreadEditor::paste))
.on_action(cx.listener(TextThreadEditor::paste_raw))
.capture_action(cx.listener(TextThreadEditor::cycle_message_role))
.capture_action(cx.listener(TextThreadEditor::confirm_command))
.on_action(cx.listener(TextThreadEditor::assist))

View File

@@ -103,9 +103,8 @@ impl Model {
pub fn max_output_tokens(&self) -> Option<u64> {
match self {
// Their API treats this max against the context window, which means we hit the limit a lot
// Using the default value of None in the API instead
Self::Chat | Self::Reasoner => None,
Self::Chat => Some(8_192),
Self::Reasoner => Some(64_000),
Self::Custom {
max_output_tokens, ..
} => *max_output_tokens,

View File

@@ -6,7 +6,7 @@ use crate::{
use anyhow::{Context as _, Result};
use futures::AsyncReadExt as _;
use gpui::{
App, AppContext as _, Entity, Global, SharedString, Task,
App, AppContext as _, Entity, SharedString, Task,
http_client::{self, AsyncBody, Method},
};
use language::{OffsetRangeExt as _, ToOffset, ToPoint as _};
@@ -300,19 +300,14 @@ pub const MERCURY_CREDENTIALS_URL: SharedString =
SharedString::new_static("https://api.inceptionlabs.ai/v1/edit/completions");
pub const MERCURY_CREDENTIALS_USERNAME: &str = "mercury-api-token";
pub static MERCURY_TOKEN_ENV_VAR: std::sync::LazyLock<EnvVar> = env_var!("MERCURY_AI_TOKEN");
struct GlobalMercuryApiKey(Entity<ApiKeyState>);
impl Global for GlobalMercuryApiKey {}
pub static MERCURY_API_KEY: std::sync::OnceLock<Entity<ApiKeyState>> = std::sync::OnceLock::new();
pub fn mercury_api_token(cx: &mut App) -> Entity<ApiKeyState> {
if let Some(global) = cx.try_global::<GlobalMercuryApiKey>() {
return global.0.clone();
}
let entity =
cx.new(|_| ApiKeyState::new(MERCURY_CREDENTIALS_URL, MERCURY_TOKEN_ENV_VAR.clone()));
cx.set_global(GlobalMercuryApiKey(entity.clone()));
entity
MERCURY_API_KEY
.get_or_init(|| {
cx.new(|_| ApiKeyState::new(MERCURY_CREDENTIALS_URL, MERCURY_TOKEN_ENV_VAR.clone()))
})
.clone()
}
pub fn load_mercury_api_token(cx: &mut App) -> Task<Result<(), language_model::AuthenticateError>> {

View File

@@ -1,7 +1,7 @@
use anyhow::Result;
use futures::AsyncReadExt as _;
use gpui::{
App, AppContext as _, Entity, Global, SharedString, Task,
App, AppContext as _, Entity, SharedString, Task,
http_client::{self, AsyncBody, Method},
};
use language::{Point, ToOffset as _};
@@ -272,19 +272,14 @@ pub const SWEEP_CREDENTIALS_URL: SharedString =
SharedString::new_static("https://autocomplete.sweep.dev");
pub const SWEEP_CREDENTIALS_USERNAME: &str = "sweep-api-token";
pub static SWEEP_AI_TOKEN_ENV_VAR: std::sync::LazyLock<EnvVar> = env_var!("SWEEP_AI_TOKEN");
struct GlobalSweepApiKey(Entity<ApiKeyState>);
impl Global for GlobalSweepApiKey {}
pub static SWEEP_API_KEY: std::sync::OnceLock<Entity<ApiKeyState>> = std::sync::OnceLock::new();
pub fn sweep_api_token(cx: &mut App) -> Entity<ApiKeyState> {
if let Some(global) = cx.try_global::<GlobalSweepApiKey>() {
return global.0.clone();
}
let entity =
cx.new(|_| ApiKeyState::new(SWEEP_CREDENTIALS_URL, SWEEP_AI_TOKEN_ENV_VAR.clone()));
cx.set_global(GlobalSweepApiKey(entity.clone()));
entity
SWEEP_API_KEY
.get_or_init(|| {
cx.new(|_| ApiKeyState::new(SWEEP_CREDENTIALS_URL, SWEEP_AI_TOKEN_ENV_VAR.clone()))
})
.clone()
}
pub fn load_sweep_api_token(cx: &mut App) -> Task<Result<(), language_model::AuthenticateError>> {

View File

@@ -348,61 +348,6 @@ where
);
}
#[gpui::test]
async fn test_bracket_colorization_after_language_swap(cx: &mut gpui::TestAppContext) {
init_test(cx, |language_settings| {
language_settings.defaults.colorize_brackets = Some(true);
});
let language_registry = Arc::new(language::LanguageRegistry::test(cx.executor()));
language_registry.add(markdown_lang());
language_registry.add(rust_lang());
let mut cx = EditorTestContext::new(cx).await;
cx.update_buffer(|buffer, cx| {
buffer.set_language_registry(language_registry.clone());
buffer.set_language(Some(markdown_lang()), cx);
});
cx.set_state(indoc! {r#"
fn main() {
let v: Vec<Stringˇ> = vec![];
}
"#});
cx.executor().advance_clock(Duration::from_millis(100));
cx.executor().run_until_parked();
assert_eq!(
r#"fn main«1()1» «1{
let v: Vec<String> = vec!«2[]2»;
}1»
1 hsla(207.80, 16.20%, 69.19%, 1.00)
2 hsla(29.00, 54.00%, 65.88%, 1.00)
"#,
&bracket_colors_markup(&mut cx),
"Markdown does not colorize <> brackets"
);
cx.update_buffer(|buffer, cx| {
buffer.set_language(Some(rust_lang()), cx);
});
cx.executor().advance_clock(Duration::from_millis(100));
cx.executor().run_until_parked();
assert_eq!(
r#"fn main«1()1» «1{
let v: Vec«2<String>2» = vec!«2[]2»;
}1»
1 hsla(207.80, 16.20%, 69.19%, 1.00)
2 hsla(29.00, 54.00%, 65.88%, 1.00)
"#,
&bracket_colors_markup(&mut cx),
"After switching to Rust, <> brackets are now colorized"
);
}
#[gpui::test]
async fn test_bracket_colorization_when_editing(cx: &mut gpui::TestAppContext) {
init_test(cx, |language_settings| {

View File

@@ -20880,36 +20880,6 @@ async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
.to_string(),
);
cx.update_editor(|editor, window, cx| {
editor.move_up(&MoveUp, window, cx);
editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
});
cx.assert_state_with_diff(
indoc! { "
ˇone
- two
three
five
"}
.to_string(),
);
cx.update_editor(|editor, window, cx| {
editor.move_down(&MoveDown, window, cx);
editor.move_down(&MoveDown, window, cx);
editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
});
cx.assert_state_with_diff(
indoc! { "
one
- two
ˇthree
- four
five
"}
.to_string(),
);
cx.set_state(indoc! { "
one
ˇTWO
@@ -20949,66 +20919,6 @@ async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
);
}
#[gpui::test]
async fn test_toggling_adjacent_diff_hunks_2(
executor: BackgroundExecutor,
cx: &mut TestAppContext,
) {
init_test(cx, |_| {});
let mut cx = EditorTestContext::new(cx).await;
let diff_base = r#"
lineA
lineB
lineC
lineD
"#
.unindent();
cx.set_state(
&r#"
ˇlineA1
lineB
lineD
"#
.unindent(),
);
cx.set_head_text(&diff_base);
executor.run_until_parked();
cx.update_editor(|editor, window, cx| {
editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
});
executor.run_until_parked();
cx.assert_state_with_diff(
r#"
- lineA
+ ˇlineA1
lineB
lineD
"#
.unindent(),
);
cx.update_editor(|editor, window, cx| {
editor.move_down(&MoveDown, window, cx);
editor.move_right(&MoveRight, window, cx);
editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
});
executor.run_until_parked();
cx.assert_state_with_diff(
r#"
- lineA
+ lineA1
lˇineB
- lineC
lineD
"#
.unindent(),
);
}
#[gpui::test]
async fn test_edits_around_expanded_deletion_hunks(
executor: BackgroundExecutor,

View File

@@ -331,6 +331,7 @@ static mut EXTENSION: Option<Box<dyn Extension>> = None;
pub static ZED_API_VERSION: [u8; 6] = *include_bytes!(concat!(env!("OUT_DIR"), "/version_bytes"));
mod wit {
wit_bindgen::generate!({
skip: ["init-extension"],
path: "./wit/since_v0.8.0",
@@ -523,12 +524,6 @@ impl wit::Guest for Component {
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)]
pub struct LanguageServerId(String);
impl LanguageServerId {
pub fn new(value: String) -> Self {
Self(value)
}
}
impl AsRef<str> for LanguageServerId {
fn as_ref(&self) -> &str {
&self.0
@@ -545,12 +540,6 @@ impl fmt::Display for LanguageServerId {
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)]
pub struct ContextServerId(String);
impl ContextServerId {
pub fn new(value: String) -> Self {
Self(value)
}
}
impl AsRef<str> for ContextServerId {
fn as_ref(&self) -> &str {
&self.0

View File

@@ -15,7 +15,6 @@ 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,
@@ -2181,13 +2180,7 @@ 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_impl(
RewrapOptions {
override_language_settings: false,
preserve_existing_whitespace: true,
},
cx,
);
editor.rewrap(&Default::default(), window, cx);
editor.text(cx)
});
if wrapped_message.trim().is_empty() {

View File

@@ -566,22 +566,22 @@ impl Model {
pub fn max_token_count(&self) -> u64 {
match self {
Self::Gemini25FlashLite
| Self::Gemini25Flash
| Self::Gemini25Pro
| Self::Gemini3Pro
| Self::Gemini3Flash => 1_048_576,
Self::Gemini25FlashLite => 1_048_576,
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,
}
}
pub fn max_output_tokens(&self) -> Option<u64> {
match self {
Model::Gemini25FlashLite
| Model::Gemini25Flash
| Model::Gemini25Pro
| Model::Gemini3Pro
| Model::Gemini3Flash => Some(65_536),
Model::Gemini25FlashLite => Some(65_536),
Model::Gemini25Flash => Some(65_536),
Model::Gemini25Pro => Some(65_536),
Model::Gemini3Pro => Some(65_536),
Model::Gemini3Flash => Some(65_536),
Model::Custom { .. } => None,
}
}

View File

@@ -198,14 +198,14 @@ wayland-backend = { version = "0.3.3", features = [
"client_system",
"dlopen",
], optional = true }
wayland-client = { version = "0.31.11", optional = true }
wayland-cursor = { version = "0.31.11", optional = true }
wayland-protocols = { version = "0.32.9", features = [
wayland-client = { version = "0.31.2", optional = true }
wayland-cursor = { version = "0.31.1", optional = true }
wayland-protocols = { version = "0.31.2", features = [
"client",
"staging",
"unstable",
], optional = true }
wayland-protocols-plasma = { version = "0.3.9", features = [
wayland-protocols-plasma = { version = "0.2.0", features = [
"client",
], optional = true }
wayland-protocols-wlr = { version = "0.3.9", features = [

View File

@@ -5,7 +5,6 @@ use gpui::{
struct SubWindow {
custom_titlebar: bool,
is_dialog: bool,
}
fn button(text: &str, on_click: impl Fn(&mut Window, &mut App) + 'static) -> impl IntoElement {
@@ -24,10 +23,7 @@ fn button(text: &str, on_click: impl Fn(&mut Window, &mut App) + 'static) -> imp
}
impl Render for SubWindow {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let window_bounds =
WindowBounds::Windowed(Bounds::centered(None, size(px(250.0), px(200.0)), cx));
fn render(&mut self, _window: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
div()
.flex()
.flex_col()
@@ -56,28 +52,8 @@ impl Render for SubWindow {
.child(
div()
.p_8()
.flex()
.flex_col()
.gap_2()
.child("SubWindow")
.when(self.is_dialog, |div| {
div.child(button("Open Nested Dialog", move |_, cx| {
cx.open_window(
WindowOptions {
window_bounds: Some(window_bounds),
kind: WindowKind::Dialog,
..Default::default()
},
|_, cx| {
cx.new(|_| SubWindow {
custom_titlebar: false,
is_dialog: true,
})
},
)
.unwrap();
}))
})
.child(button("Close", |window, _| {
window.remove_window();
})),
@@ -110,7 +86,6 @@ impl Render for WindowDemo {
|_, cx| {
cx.new(|_| SubWindow {
custom_titlebar: false,
is_dialog: false,
})
},
)
@@ -126,39 +101,6 @@ impl Render for WindowDemo {
|_, cx| {
cx.new(|_| SubWindow {
custom_titlebar: false,
is_dialog: false,
})
},
)
.unwrap();
}))
.child(button("Floating", move |_, cx| {
cx.open_window(
WindowOptions {
window_bounds: Some(window_bounds),
kind: WindowKind::Floating,
..Default::default()
},
|_, cx| {
cx.new(|_| SubWindow {
custom_titlebar: false,
is_dialog: false,
})
},
)
.unwrap();
}))
.child(button("Dialog", move |_, cx| {
cx.open_window(
WindowOptions {
window_bounds: Some(window_bounds),
kind: WindowKind::Dialog,
..Default::default()
},
|_, cx| {
cx.new(|_| SubWindow {
custom_titlebar: false,
is_dialog: true,
})
},
)
@@ -174,7 +116,6 @@ impl Render for WindowDemo {
|_, cx| {
cx.new(|_| SubWindow {
custom_titlebar: true,
is_dialog: false,
})
},
)
@@ -190,7 +131,6 @@ impl Render for WindowDemo {
|_, cx| {
cx.new(|_| SubWindow {
custom_titlebar: false,
is_dialog: false,
})
},
)
@@ -207,7 +147,6 @@ impl Render for WindowDemo {
|_, cx| {
cx.new(|_| SubWindow {
custom_titlebar: false,
is_dialog: false,
})
},
)
@@ -223,7 +162,6 @@ impl Render for WindowDemo {
|_, cx| {
cx.new(|_| SubWindow {
custom_titlebar: false,
is_dialog: false,
})
},
)
@@ -239,7 +177,6 @@ impl Render for WindowDemo {
|_, cx| {
cx.new(|_| SubWindow {
custom_titlebar: false,
is_dialog: false,
})
},
)

View File

@@ -28,8 +28,6 @@ pub use entity_map::*;
use http_client::{HttpClient, Url};
use smallvec::SmallVec;
#[cfg(any(test, feature = "test-support"))]
pub use test_app::*;
#[cfg(any(test, feature = "test-support"))]
pub use test_context::*;
use util::{ResultExt, debug_panic};
@@ -53,8 +51,6 @@ mod async_context;
mod context;
mod entity_map;
#[cfg(any(test, feature = "test-support"))]
mod test_app;
#[cfg(any(test, feature = "test-support"))]
mod test_context;
/// The duration for which futures returned from [Context::on_app_quit] can run before the application fully quits.

View File

@@ -1,605 +0,0 @@
//! A clean testing API for GPUI applications.
//!
//! `TestApp` provides a simpler alternative to `TestAppContext` with:
//! - Automatic effect flushing after updates
//! - Clean window creation and inspection
//! - Input simulation helpers
//!
//! # Example
//! ```ignore
//! #[test]
//! fn test_my_view() {
//! let mut app = TestApp::new();
//!
//! let mut window = app.open_window(|window, cx| {
//! MyView::new(window, cx)
//! });
//!
//! window.update(|view, window, cx| {
//! view.do_something(cx);
//! });
//!
//! // Check rendered state
//! assert_eq!(window.title(), Some("Expected Title"));
//! }
//! ```
use crate::{
AnyWindowHandle, App, AppCell, AppContext, AsyncApp, BackgroundExecutor, BorrowAppContext,
Bounds, ClipboardItem, Context, Entity, ForegroundExecutor, Global, InputEvent, Keystroke,
MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, Platform, Point, Render,
SceneSnapshot, Size, Task, TestDispatcher, TestPlatform, TextSystem, Window, WindowBounds,
WindowHandle, WindowOptions, app::GpuiMode,
};
use rand::{SeedableRng, rngs::StdRng};
use std::{future::Future, rc::Rc, sync::Arc, time::Duration};
/// A test application context with a clean API.
///
/// Unlike `TestAppContext`, `TestApp` automatically flushes effects after
/// each update and provides simpler window management.
pub struct TestApp {
app: Rc<AppCell>,
platform: Rc<TestPlatform>,
background_executor: BackgroundExecutor,
foreground_executor: ForegroundExecutor,
#[allow(dead_code)]
dispatcher: TestDispatcher,
text_system: Arc<TextSystem>,
}
impl TestApp {
/// Create a new test application.
pub fn new() -> Self {
Self::with_seed(0)
}
/// Create a new test application with a specific random seed.
pub fn with_seed(seed: u64) -> Self {
let dispatcher = TestDispatcher::new(StdRng::seed_from_u64(seed));
let arc_dispatcher = Arc::new(dispatcher.clone());
let background_executor = BackgroundExecutor::new(arc_dispatcher.clone());
let foreground_executor = ForegroundExecutor::new(arc_dispatcher);
let platform = TestPlatform::new(background_executor.clone(), foreground_executor.clone());
let asset_source = Arc::new(());
let http_client = http_client::FakeHttpClient::with_404_response();
let text_system = Arc::new(TextSystem::new(platform.text_system()));
let mut app = App::new_app(platform.clone(), asset_source, http_client);
app.borrow_mut().mode = GpuiMode::test();
Self {
app,
platform,
background_executor,
foreground_executor,
dispatcher,
text_system,
}
}
/// Run a closure with mutable access to the App context.
/// Automatically runs until parked after the closure completes.
pub fn update<R>(&mut self, f: impl FnOnce(&mut App) -> R) -> R {
let result = {
let mut app = self.app.borrow_mut();
app.update(f)
};
self.run_until_parked();
result
}
/// Run a closure with read-only access to the App context.
pub fn read<R>(&self, f: impl FnOnce(&App) -> R) -> R {
let app = self.app.borrow();
f(&app)
}
/// Create a new entity in the app.
pub fn new_entity<T: 'static>(
&mut self,
build: impl FnOnce(&mut Context<T>) -> T,
) -> Entity<T> {
self.update(|cx| cx.new(build))
}
/// Update an entity.
pub fn update_entity<T: 'static, R>(
&mut self,
entity: &Entity<T>,
f: impl FnOnce(&mut T, &mut Context<T>) -> R,
) -> R {
self.update(|cx| entity.update(cx, f))
}
/// Read an entity.
pub fn read_entity<T: 'static, R>(
&self,
entity: &Entity<T>,
f: impl FnOnce(&T, &App) -> R,
) -> R {
self.read(|cx| f(entity.read(cx), cx))
}
/// Open a test window with the given root view.
pub fn open_window<V: Render + 'static>(
&mut self,
build_view: impl FnOnce(&mut Window, &mut Context<V>) -> V,
) -> TestWindow<V> {
let bounds = self.read(|cx| Bounds::maximized(None, cx));
let handle = self.update(|cx| {
cx.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
..Default::default()
},
|window, cx| cx.new(|cx| build_view(window, cx)),
)
.unwrap()
});
TestWindow {
handle,
app: self.app.clone(),
platform: self.platform.clone(),
background_executor: self.background_executor.clone(),
}
}
/// Open a test window with specific options.
pub fn open_window_with_options<V: Render + 'static>(
&mut self,
options: WindowOptions,
build_view: impl FnOnce(&mut Window, &mut Context<V>) -> V,
) -> TestWindow<V> {
let handle = self.update(|cx| {
cx.open_window(options, |window, cx| cx.new(|cx| build_view(window, cx)))
.unwrap()
});
TestWindow {
handle,
app: self.app.clone(),
platform: self.platform.clone(),
background_executor: self.background_executor.clone(),
}
}
/// Run pending tasks until there's nothing left to do.
pub fn run_until_parked(&self) {
self.background_executor.run_until_parked();
}
/// Advance the simulated clock by the given duration.
pub fn advance_clock(&self, duration: Duration) {
self.background_executor.advance_clock(duration);
}
/// Spawn a future on the foreground executor.
pub fn spawn<Fut, R>(&self, f: impl FnOnce(AsyncApp) -> Fut) -> Task<R>
where
Fut: Future<Output = R> + 'static,
R: 'static,
{
self.foreground_executor.spawn(f(self.to_async()))
}
/// Spawn a future on the background executor.
pub fn background_spawn<R>(&self, future: impl Future<Output = R> + Send + 'static) -> Task<R>
where
R: Send + 'static,
{
self.background_executor.spawn(future)
}
/// Get an async handle to the app.
pub fn to_async(&self) -> AsyncApp {
AsyncApp {
app: Rc::downgrade(&self.app),
background_executor: self.background_executor.clone(),
foreground_executor: self.foreground_executor.clone(),
}
}
/// Get the background executor.
pub fn background_executor(&self) -> &BackgroundExecutor {
&self.background_executor
}
/// Get the foreground executor.
pub fn foreground_executor(&self) -> &ForegroundExecutor {
&self.foreground_executor
}
/// Get the text system.
pub fn text_system(&self) -> &Arc<TextSystem> {
&self.text_system
}
/// Check if a global of the given type exists.
pub fn has_global<G: Global>(&self) -> bool {
self.read(|cx| cx.has_global::<G>())
}
/// Set a global value.
pub fn set_global<G: Global>(&mut self, global: G) {
self.update(|cx| cx.set_global(global));
}
/// Read a global value.
pub fn read_global<G: Global, R>(&self, f: impl FnOnce(&G, &App) -> R) -> R {
self.read(|cx| f(cx.global(), cx))
}
/// Update a global value.
pub fn update_global<G: Global, R>(&mut self, f: impl FnOnce(&mut G, &mut App) -> R) -> R {
self.update(|cx| cx.update_global(f))
}
// Platform simulation methods
/// Write text to the simulated clipboard.
pub fn write_to_clipboard(&self, item: ClipboardItem) {
self.platform.write_to_clipboard(item);
}
/// Read from the simulated clipboard.
pub fn read_from_clipboard(&self) -> Option<ClipboardItem> {
self.platform.read_from_clipboard()
}
/// Get URLs that have been opened via `cx.open_url()`.
pub fn opened_url(&self) -> Option<String> {
self.platform.opened_url.borrow().clone()
}
/// Check if a file path prompt is pending.
pub fn did_prompt_for_new_path(&self) -> bool {
self.platform.did_prompt_for_new_path()
}
/// Simulate answering a path selection dialog.
pub fn simulate_new_path_selection(
&self,
select: impl FnOnce(&std::path::Path) -> Option<std::path::PathBuf>,
) {
self.platform.simulate_new_path_selection(select);
}
/// Check if a prompt dialog is pending.
pub fn has_pending_prompt(&self) -> bool {
self.platform.has_pending_prompt()
}
/// Simulate answering a prompt dialog.
pub fn simulate_prompt_answer(&self, button: &str) {
self.platform.simulate_prompt_answer(button);
}
/// Get all open windows.
pub fn windows(&self) -> Vec<AnyWindowHandle> {
self.read(|cx| cx.windows())
}
}
impl Default for TestApp {
fn default() -> Self {
Self::new()
}
}
/// A test window with inspection and simulation capabilities.
pub struct TestWindow<V> {
handle: WindowHandle<V>,
app: Rc<AppCell>,
platform: Rc<TestPlatform>,
background_executor: BackgroundExecutor,
}
impl<V: 'static + Render> TestWindow<V> {
/// Get the window handle.
pub fn handle(&self) -> WindowHandle<V> {
self.handle
}
/// Get the root view entity.
pub fn root(&self) -> Entity<V> {
let mut app = self.app.borrow_mut();
let any_handle: AnyWindowHandle = self.handle.into();
app.update_window(any_handle, |root_view, _, _| {
root_view.downcast::<V>().expect("root view type mismatch")
})
.expect("window not found")
}
/// Update the root view.
/// Automatically draws the window after the update to ensure the scene is current.
pub fn update<R>(&mut self, f: impl FnOnce(&mut V, &mut Window, &mut Context<V>) -> R) -> R {
let result = {
let mut app = self.app.borrow_mut();
let any_handle: AnyWindowHandle = self.handle.into();
app.update_window(any_handle, |root_view, window, cx| {
let view = root_view.downcast::<V>().expect("root view type mismatch");
view.update(cx, |view, cx| f(view, window, cx))
})
.expect("window not found")
};
self.background_executor.run_until_parked();
self.draw();
result
}
/// Read the root view.
pub fn read<R>(&self, f: impl FnOnce(&V, &App) -> R) -> R {
let app = self.app.borrow();
let view = self
.app
.borrow()
.windows
.get(self.handle.window_id())
.and_then(|w| w.as_ref())
.and_then(|w| w.root.clone())
.and_then(|r| r.downcast::<V>().ok())
.expect("window or root view not found");
f(view.read(&app), &app)
}
/// Get the window title.
pub fn title(&self) -> Option<String> {
let app = self.app.borrow();
app.read_window(&self.handle, |_, _cx| {
// TODO: expose title through Window API
None
})
.unwrap()
}
/// Simulate a keystroke.
/// Automatically draws the window after the keystroke.
pub fn simulate_keystroke(&mut self, keystroke: &str) {
let keystroke = Keystroke::parse(keystroke).unwrap();
{
let mut app = self.app.borrow_mut();
let any_handle: AnyWindowHandle = self.handle.into();
app.update_window(any_handle, |_, window, cx| {
window.dispatch_keystroke(keystroke, cx);
})
.unwrap();
}
self.background_executor.run_until_parked();
self.draw();
}
/// Simulate multiple keystrokes (space-separated).
pub fn simulate_keystrokes(&mut self, keystrokes: &str) {
for keystroke in keystrokes.split(' ') {
self.simulate_keystroke(keystroke);
}
}
/// Simulate typing text.
pub fn simulate_input(&mut self, input: &str) {
for char in input.chars() {
self.simulate_keystroke(&char.to_string());
}
}
/// Simulate a mouse move.
pub fn simulate_mouse_move(&mut self, position: Point<Pixels>) {
self.simulate_event(MouseMoveEvent {
position,
modifiers: Default::default(),
pressed_button: None,
});
}
/// Simulate a mouse down event.
pub fn simulate_mouse_down(&mut self, position: Point<Pixels>, button: MouseButton) {
self.simulate_event(MouseDownEvent {
position,
button,
modifiers: Default::default(),
click_count: 1,
first_mouse: false,
});
}
/// Simulate a mouse up event.
pub fn simulate_mouse_up(&mut self, position: Point<Pixels>, button: MouseButton) {
self.simulate_event(MouseUpEvent {
position,
button,
modifiers: Default::default(),
click_count: 1,
});
}
/// Simulate a click at the given position.
pub fn simulate_click(&mut self, position: Point<Pixels>, button: MouseButton) {
self.simulate_mouse_down(position, button);
self.simulate_mouse_up(position, button);
}
/// Simulate a scroll event.
pub fn simulate_scroll(&mut self, position: Point<Pixels>, delta: Point<Pixels>) {
self.simulate_event(crate::ScrollWheelEvent {
position,
delta: crate::ScrollDelta::Pixels(delta),
modifiers: Default::default(),
touch_phase: crate::TouchPhase::Moved,
});
}
/// Simulate an input event.
/// Automatically draws the window after the event.
pub fn simulate_event<E: InputEvent>(&mut self, event: E) {
let platform_input = event.to_platform_input();
{
let mut app = self.app.borrow_mut();
let any_handle: AnyWindowHandle = self.handle.into();
app.update_window(any_handle, |_, window, cx| {
window.dispatch_event(platform_input, cx);
})
.unwrap();
}
self.background_executor.run_until_parked();
self.draw();
}
/// Simulate resizing the window.
/// Automatically draws the window after the resize.
pub fn simulate_resize(&mut self, size: Size<Pixels>) {
let window_id = self.handle.window_id();
let mut app = self.app.borrow_mut();
if let Some(Some(window)) = app.windows.get_mut(window_id) {
if let Some(test_window) = window.platform_window.as_test() {
test_window.simulate_resize(size);
}
}
drop(app);
self.background_executor.run_until_parked();
self.draw();
}
/// Force a redraw of the window.
pub fn draw(&mut self) {
let mut app = self.app.borrow_mut();
let any_handle: AnyWindowHandle = self.handle.into();
app.update_window(any_handle, |_, window, cx| {
window.draw(cx).clear();
})
.unwrap();
}
/// Get a snapshot of the rendered scene for inspection.
/// The scene is automatically kept up to date after `update()` and `simulate_*()` calls.
pub fn scene_snapshot(&self) -> SceneSnapshot {
let app = self.app.borrow();
let window = app
.windows
.get(self.handle.window_id())
.and_then(|w| w.as_ref())
.expect("window not found");
window.rendered_frame.scene.snapshot()
}
/// Get the named diagnostic quads recorded during imperative paint, without inspecting the
/// rest of the scene snapshot.
///
/// This is useful for tests that want a stable, semantic view of layout/paint geometry without
/// coupling to the low-level quad/glyph output.
pub fn diagnostic_quads(&self) -> Vec<crate::scene::test_scene::DiagnosticQuad> {
self.scene_snapshot().diagnostic_quads
}
}
impl<V> Clone for TestWindow<V> {
fn clone(&self) -> Self {
Self {
handle: self.handle,
app: self.app.clone(),
platform: self.platform.clone(),
background_executor: self.background_executor.clone(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{FocusHandle, Focusable, div, prelude::*};
struct Counter {
count: usize,
focus_handle: FocusHandle,
}
impl Counter {
fn new(_window: &mut Window, cx: &mut Context<Self>) -> Self {
let focus_handle = cx.focus_handle();
Self {
count: 0,
focus_handle,
}
}
fn increment(&mut self, _cx: &mut Context<Self>) {
self.count += 1;
}
}
impl Focusable for Counter {
fn focus_handle(&self, _cx: &App) -> FocusHandle {
self.focus_handle.clone()
}
}
impl Render for Counter {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
div().child(format!("Count: {}", self.count))
}
}
#[test]
fn test_basic_usage() {
let mut app = TestApp::new();
let mut window = app.open_window(Counter::new);
window.update(|counter, _window, cx| {
counter.increment(cx);
});
window.read(|counter, _| {
assert_eq!(counter.count, 1);
});
}
#[test]
fn test_entity_creation() {
let mut app = TestApp::new();
let entity = app.new_entity(|cx| Counter {
count: 42,
focus_handle: cx.focus_handle(),
});
app.read_entity(&entity, |counter, _| {
assert_eq!(counter.count, 42);
});
app.update_entity(&entity, |counter, _cx| {
counter.count += 1;
});
app.read_entity(&entity, |counter, _| {
assert_eq!(counter.count, 43);
});
}
#[test]
fn test_globals() {
let mut app = TestApp::new();
struct MyGlobal(String);
impl Global for MyGlobal {}
assert!(!app.has_global::<MyGlobal>());
app.set_global(MyGlobal("hello".into()));
assert!(app.has_global::<MyGlobal>());
app.read_global::<MyGlobal, _>(|global, _| {
assert_eq!(global.0, "hello");
});
app.update_global::<MyGlobal, _>(|global, _| {
global.0 = "world".into();
});
app.read_global::<MyGlobal, _>(|global, _| {
assert_eq!(global.0, "world");
});
}
}

View File

@@ -3,9 +3,9 @@ use crate::{
BackgroundExecutor, BorrowAppContext, Bounds, Capslock, ClipboardItem, DrawPhase, Drawable,
Element, Empty, EventEmitter, ForegroundExecutor, Global, InputEvent, Keystroke, Modifiers,
ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels,
Platform, Point, Render, Result, Size, Task, TestDispatcher, TestPlatform, TestPlatformWindow,
TestScreenCaptureSource, TextSystem, VisualContext, Window, WindowBounds, WindowHandle,
WindowOptions, app::GpuiMode,
Platform, Point, Render, Result, Size, Task, TestDispatcher, TestPlatform,
TestScreenCaptureSource, TestWindow, TextSystem, VisualContext, Window, WindowBounds,
WindowHandle, WindowOptions, app::GpuiMode,
};
use anyhow::{anyhow, bail};
use futures::{Stream, StreamExt, channel::oneshot};
@@ -220,7 +220,7 @@ impl TestAppContext {
f(&cx)
}
/// Adds a new window. The Window will always be backed by a `TestPlatformWindow` which
/// Adds a new window. The Window will always be backed by a `TestWindow` which
/// can be retrieved with `self.test_window(handle)`
pub fn add_window<F, V>(&mut self, build_window: F) -> WindowHandle<V>
where
@@ -465,8 +465,8 @@ impl TestAppContext {
.unwrap();
}
/// Returns the `TestPlatformWindow` backing the given handle.
pub(crate) fn test_window(&self, window: AnyWindowHandle) -> TestPlatformWindow {
/// Returns the `TestWindow` backing the given handle.
pub(crate) fn test_window(&self, window: AnyWindowHandle) -> TestWindow {
self.app
.borrow_mut()
.windows

View File

@@ -808,15 +808,6 @@ impl LinearColorStop {
}
impl Background {
/// Returns the solid color if this is a solid background, None otherwise.
pub fn as_solid(&self) -> Option<Hsla> {
if self.tag == BackgroundTag::Solid {
Some(self.solid)
} else {
None
}
}
/// Use specified color space for color interpolation.
///
/// <https://developer.mozilla.org/en-US/docs/Web/CSS/color-interpolation-method>

View File

@@ -561,7 +561,7 @@ pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
fn update_ime_position(&self, _bounds: Bounds<Pixels>);
#[cfg(any(test, feature = "test-support"))]
fn as_test(&mut self) -> Option<&mut TestPlatformWindow> {
fn as_test(&mut self) -> Option<&mut TestWindow> {
None
}
}
@@ -1348,10 +1348,6 @@ pub enum WindowKind {
/// docks, notifications or wallpapers.
#[cfg(all(target_os = "linux", feature = "wayland"))]
LayerShell(layer_shell::LayerShellOptions),
/// A window that appears on top of its parent window and blocks interaction with it
/// until the modal window is closed
Dialog,
}
/// The appearance of the window, as defined by the operating system.

View File

@@ -36,6 +36,12 @@ use wayland_client::{
wl_shm_pool, wl_surface,
},
};
use wayland_protocols::wp::cursor_shape::v1::client::{
wp_cursor_shape_device_v1, wp_cursor_shape_manager_v1,
};
use wayland_protocols::wp::fractional_scale::v1::client::{
wp_fractional_scale_manager_v1, wp_fractional_scale_v1,
};
use wayland_protocols::wp::primary_selection::zv1::client::zwp_primary_selection_offer_v1::{
self, ZwpPrimarySelectionOfferV1,
};
@@ -55,14 +61,6 @@ use wayland_protocols::xdg::decoration::zv1::client::{
zxdg_decoration_manager_v1, zxdg_toplevel_decoration_v1,
};
use wayland_protocols::xdg::shell::client::{xdg_surface, xdg_toplevel, xdg_wm_base};
use wayland_protocols::{
wp::cursor_shape::v1::client::{wp_cursor_shape_device_v1, wp_cursor_shape_manager_v1},
xdg::dialog::v1::client::xdg_wm_dialog_v1::{self, XdgWmDialogV1},
};
use wayland_protocols::{
wp::fractional_scale::v1::client::{wp_fractional_scale_manager_v1, wp_fractional_scale_v1},
xdg::dialog::v1::client::xdg_dialog_v1::XdgDialogV1,
};
use wayland_protocols_plasma::blur::client::{org_kde_kwin_blur, org_kde_kwin_blur_manager};
use wayland_protocols_wlr::layer_shell::v1::client::{zwlr_layer_shell_v1, zwlr_layer_surface_v1};
use xkbcommon::xkb::ffi::XKB_KEYMAP_FORMAT_TEXT_V1;
@@ -124,7 +122,6 @@ pub struct Globals {
pub layer_shell: Option<zwlr_layer_shell_v1::ZwlrLayerShellV1>,
pub blur_manager: Option<org_kde_kwin_blur_manager::OrgKdeKwinBlurManager>,
pub text_input_manager: Option<zwp_text_input_manager_v3::ZwpTextInputManagerV3>,
pub dialog: Option<xdg_wm_dialog_v1::XdgWmDialogV1>,
pub executor: ForegroundExecutor,
}
@@ -135,7 +132,6 @@ impl Globals {
qh: QueueHandle<WaylandClientStatePtr>,
seat: wl_seat::WlSeat,
) -> Self {
let dialog_v = XdgWmDialogV1::interface().version;
Globals {
activation: globals.bind(&qh, 1..=1, ()).ok(),
compositor: globals
@@ -164,7 +160,6 @@ impl Globals {
layer_shell: globals.bind(&qh, 1..=5, ()).ok(),
blur_manager: globals.bind(&qh, 1..=1, ()).ok(),
text_input_manager: globals.bind(&qh, 1..=1, ()).ok(),
dialog: globals.bind(&qh, dialog_v..=dialog_v, ()).ok(),
executor,
qh,
}
@@ -734,7 +729,10 @@ impl LinuxClient for WaylandClient {
) -> anyhow::Result<Box<dyn PlatformWindow>> {
let mut state = self.0.borrow_mut();
let parent = state.keyboard_focused_window.clone();
let parent = state
.keyboard_focused_window
.as_ref()
.and_then(|w| w.toplevel());
let (window, surface_id) = WaylandWindow::new(
handle,
@@ -753,12 +751,7 @@ impl LinuxClient for WaylandClient {
fn set_cursor_style(&self, style: CursorStyle) {
let mut state = self.0.borrow_mut();
let need_update = state.cursor_style != Some(style)
&& (state.mouse_focused_window.is_none()
|| state
.mouse_focused_window
.as_ref()
.is_some_and(|w| !w.is_blocked()));
let need_update = state.cursor_style != Some(style);
if need_update {
let serial = state.serial_tracker.get(SerialKind::MouseEnter);
@@ -1018,7 +1011,7 @@ impl Dispatch<WlCallback, ObjectId> for WaylandClientStatePtr {
}
}
pub(crate) fn get_window(
fn get_window(
mut state: &mut RefMut<WaylandClientState>,
surface_id: &ObjectId,
) -> Option<WaylandWindowStatePtr> {
@@ -1661,30 +1654,6 @@ impl Dispatch<wl_pointer::WlPointer, ()> for WaylandClientStatePtr {
state.mouse_location = Some(point(px(surface_x as f32), px(surface_y as f32)));
if let Some(window) = state.mouse_focused_window.clone() {
if window.is_blocked() {
let default_style = CursorStyle::Arrow;
if state.cursor_style != Some(default_style) {
let serial = state.serial_tracker.get(SerialKind::MouseEnter);
state.cursor_style = Some(default_style);
if let Some(cursor_shape_device) = &state.cursor_shape_device {
cursor_shape_device.set_shape(serial, default_style.to_shape());
} else {
// cursor-shape-v1 isn't supported, set the cursor using a surface.
let wl_pointer = state
.wl_pointer
.clone()
.expect("window is focused by pointer");
let scale = window.primary_output_scale();
state.cursor.set_icon(
&wl_pointer,
serial,
default_style.to_icon_names(),
scale,
);
}
}
}
if state
.keyboard_focused_window
.as_ref()
@@ -2256,27 +2225,3 @@ impl Dispatch<zwp_primary_selection_source_v1::ZwpPrimarySelectionSourceV1, ()>
}
}
}
impl Dispatch<XdgWmDialogV1, ()> for WaylandClientStatePtr {
fn event(
_: &mut Self,
_: &XdgWmDialogV1,
_: <XdgWmDialogV1 as Proxy>::Event,
_: &(),
_: &Connection,
_: &QueueHandle<Self>,
) {
}
}
impl Dispatch<XdgDialogV1, ()> for WaylandClientStatePtr {
fn event(
_state: &mut Self,
_proxy: &XdgDialogV1,
_event: <XdgDialogV1 as Proxy>::Event,
_data: &(),
_conn: &Connection,
_qhandle: &QueueHandle<Self>,
) {
}
}

View File

@@ -7,7 +7,7 @@ use std::{
};
use blade_graphics as gpu;
use collections::{FxHashSet, HashMap};
use collections::HashMap;
use futures::channel::oneshot::Receiver;
use raw_window_handle as rwh;
@@ -20,7 +20,7 @@ use wayland_protocols::xdg::shell::client::xdg_surface;
use wayland_protocols::xdg::shell::client::xdg_toplevel::{self};
use wayland_protocols::{
wp::fractional_scale::v1::client::wp_fractional_scale_v1,
xdg::dialog::v1::client::xdg_dialog_v1::XdgDialogV1,
xdg::shell::client::xdg_toplevel::XdgToplevel,
};
use wayland_protocols_plasma::blur::client::org_kde_kwin_blur;
use wayland_protocols_wlr::layer_shell::v1::client::zwlr_layer_surface_v1;
@@ -29,7 +29,7 @@ use crate::{
AnyWindowHandle, Bounds, Decorations, Globals, GpuSpecs, Modifiers, Output, Pixels,
PlatformDisplay, PlatformInput, Point, PromptButton, PromptLevel, RequestFrameOptions,
ResizeEdge, Size, Tiling, WaylandClientStatePtr, WindowAppearance, WindowBackgroundAppearance,
WindowBounds, WindowControlArea, WindowControls, WindowDecorations, WindowParams, get_window,
WindowBounds, WindowControlArea, WindowControls, WindowDecorations, WindowParams,
layer_shell::LayerShellNotSupportedError, px, size,
};
use crate::{
@@ -87,8 +87,6 @@ struct InProgressConfigure {
pub struct WaylandWindowState {
surface_state: WaylandSurfaceState,
acknowledged_first_configure: bool,
parent: Option<WaylandWindowStatePtr>,
children: FxHashSet<ObjectId>,
pub surface: wl_surface::WlSurface,
app_id: Option<String>,
appearance: WindowAppearance,
@@ -128,7 +126,7 @@ impl WaylandSurfaceState {
surface: &wl_surface::WlSurface,
globals: &Globals,
params: &WindowParams,
parent: Option<WaylandWindowStatePtr>,
parent: Option<XdgToplevel>,
) -> anyhow::Result<Self> {
// For layer_shell windows, create a layer surface instead of an xdg surface
if let WindowKind::LayerShell(options) = &params.kind {
@@ -180,28 +178,10 @@ impl WaylandSurfaceState {
.get_xdg_surface(&surface, &globals.qh, surface.id());
let toplevel = xdg_surface.get_toplevel(&globals.qh, surface.id());
let xdg_parent = parent.as_ref().and_then(|w| w.toplevel());
if params.kind == WindowKind::Floating || params.kind == WindowKind::Dialog {
toplevel.set_parent(xdg_parent.as_ref());
if params.kind == WindowKind::Floating {
toplevel.set_parent(parent.as_ref());
}
let dialog = if params.kind == WindowKind::Dialog {
let dialog = globals.dialog.as_ref().map(|dialog| {
let xdg_dialog = dialog.get_xdg_dialog(&toplevel, &globals.qh, ());
xdg_dialog.set_modal();
xdg_dialog
});
if let Some(parent) = parent.as_ref() {
parent.add_child(surface.id());
}
dialog
} else {
None
};
if let Some(size) = params.window_min_size {
toplevel.set_min_size(size.width.0 as i32, size.height.0 as i32);
}
@@ -218,7 +198,6 @@ impl WaylandSurfaceState {
xdg_surface,
toplevel,
decoration,
dialog,
}))
}
}
@@ -227,7 +206,6 @@ pub struct WaylandXdgSurfaceState {
xdg_surface: xdg_surface::XdgSurface,
toplevel: xdg_toplevel::XdgToplevel,
decoration: Option<zxdg_toplevel_decoration_v1::ZxdgToplevelDecorationV1>,
dialog: Option<XdgDialogV1>,
}
pub struct WaylandLayerSurfaceState {
@@ -280,13 +258,7 @@ impl WaylandSurfaceState {
xdg_surface,
toplevel,
decoration: _decoration,
dialog,
}) => {
// drop the dialog before toplevel so compositor can explicitly unapply it's effects
if let Some(dialog) = dialog {
dialog.destroy();
}
// The role object (toplevel) must always be destroyed before the xdg_surface.
// See https://wayland.app/protocols/xdg-shell#xdg_surface:request:destroy
toplevel.destroy();
@@ -316,7 +288,6 @@ impl WaylandWindowState {
globals: Globals,
gpu_context: &BladeContext,
options: WindowParams,
parent: Option<WaylandWindowStatePtr>,
) -> anyhow::Result<Self> {
let renderer = {
let raw_window = RawWindow {
@@ -348,8 +319,6 @@ impl WaylandWindowState {
Ok(Self {
surface_state,
acknowledged_first_configure: false,
parent,
children: FxHashSet::default(),
surface,
app_id: None,
blur: None,
@@ -422,10 +391,6 @@ impl Drop for WaylandWindow {
fn drop(&mut self) {
let mut state = self.0.state.borrow_mut();
let surface_id = state.surface.id();
if let Some(parent) = state.parent.as_ref() {
parent.state.borrow_mut().children.remove(&surface_id);
}
let client = state.client.clone();
state.renderer.destroy();
@@ -483,10 +448,10 @@ impl WaylandWindow {
client: WaylandClientStatePtr,
params: WindowParams,
appearance: WindowAppearance,
parent: Option<WaylandWindowStatePtr>,
parent: Option<XdgToplevel>,
) -> anyhow::Result<(Self, ObjectId)> {
let surface = globals.compositor.create_surface(&globals.qh, ());
let surface_state = WaylandSurfaceState::new(&surface, &globals, &params, parent.clone())?;
let surface_state = WaylandSurfaceState::new(&surface, &globals, &params, parent)?;
if let Some(fractional_scale_manager) = globals.fractional_scale_manager.as_ref() {
fractional_scale_manager.get_fractional_scale(&surface, &globals.qh, surface.id());
@@ -508,7 +473,6 @@ impl WaylandWindow {
globals,
gpu_context,
params,
parent,
)?)),
callbacks: Rc::new(RefCell::new(Callbacks::default())),
});
@@ -537,16 +501,6 @@ impl WaylandWindowStatePtr {
Rc::ptr_eq(&self.state, &other.state)
}
pub fn add_child(&self, child: ObjectId) {
let mut state = self.state.borrow_mut();
state.children.insert(child);
}
pub fn is_blocked(&self) -> bool {
let state = self.state.borrow();
!state.children.is_empty()
}
pub fn frame(&self) {
let mut state = self.state.borrow_mut();
state.surface.frame(&state.globals.qh, state.surface.id());
@@ -864,9 +818,6 @@ impl WaylandWindowStatePtr {
}
pub fn handle_ime(&self, ime: ImeInput) {
if self.is_blocked() {
return;
}
let mut state = self.state.borrow_mut();
if let Some(mut input_handler) = state.input_handler.take() {
drop(state);
@@ -943,21 +894,6 @@ impl WaylandWindowStatePtr {
}
pub fn close(&self) {
let state = self.state.borrow();
let client = state.client.get_client();
#[allow(clippy::mutable_key_type)]
let children = state.children.clone();
drop(state);
for child in children {
let mut client_state = client.borrow_mut();
let window = get_window(&mut client_state, &child);
drop(client_state);
if let Some(child) = window {
child.close();
}
}
let mut callbacks = self.callbacks.borrow_mut();
if let Some(fun) = callbacks.close.take() {
fun()
@@ -965,9 +901,6 @@ impl WaylandWindowStatePtr {
}
pub fn handle_input(&self, input: PlatformInput) {
if self.is_blocked() {
return;
}
if let Some(ref mut fun) = self.callbacks.borrow_mut().input
&& !fun(input.clone()).propagate
{
@@ -1092,26 +1025,13 @@ impl PlatformWindow for WaylandWindow {
fn resize(&mut self, size: Size<Pixels>) {
let state = self.borrow();
let state_ptr = self.0.clone();
// 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 });
let dp_size = size.to_device_pixels(self.scale_factor());
state.surface_state.set_geometry(
window_geometry.origin.x,
window_geometry.origin.y,
window_geometry.size.width,
window_geometry.size.height,
state.bounds.origin.x.0 as i32,
state.bounds.origin.y.0 as i32,
dp_size.width.0,
dp_size.height.0,
);
state

View File

@@ -222,7 +222,7 @@ pub struct X11ClientState {
pub struct X11ClientStatePtr(pub Weak<RefCell<X11ClientState>>);
impl X11ClientStatePtr {
pub fn get_client(&self) -> Option<X11Client> {
fn get_client(&self) -> Option<X11Client> {
self.0.upgrade().map(X11Client)
}
@@ -752,7 +752,7 @@ impl X11Client {
}
}
pub(crate) fn get_window(&self, win: xproto::Window) -> Option<X11WindowStatePtr> {
fn get_window(&self, win: xproto::Window) -> Option<X11WindowStatePtr> {
let state = self.0.borrow();
state
.windows
@@ -789,12 +789,12 @@ impl X11Client {
let [atom, arg1, arg2, arg3, arg4] = event.data.as_data32();
let mut state = self.0.borrow_mut();
if atom == state.atoms.WM_DELETE_WINDOW && window.should_close() {
if atom == state.atoms.WM_DELETE_WINDOW {
// window "x" button clicked by user
// Rest of the close logic is handled in drop_window()
drop(state);
window.close();
state = self.0.borrow_mut();
if window.should_close() {
// Rest of the close logic is handled in drop_window()
window.close();
}
} else if atom == state.atoms._NET_WM_SYNC_REQUEST {
window.state.borrow_mut().last_sync_counter =
Some(x11rb::protocol::sync::Int64 {
@@ -1216,33 +1216,6 @@ impl X11Client {
Event::XinputMotion(event) => {
let window = self.get_window(event.event)?;
let mut state = self.0.borrow_mut();
if window.is_blocked() {
// We want to set the cursor to the default arrow
// when the window is blocked
let style = CursorStyle::Arrow;
let current_style = state
.cursor_styles
.get(&window.x_window)
.unwrap_or(&CursorStyle::Arrow);
if *current_style != style
&& let Some(cursor) = state.get_cursor_icon(style)
{
state.cursor_styles.insert(window.x_window, style);
check_reply(
|| "Failed to set cursor style",
state.xcb_connection.change_window_attributes(
window.x_window,
&ChangeWindowAttributesAux {
cursor: Some(cursor),
..Default::default()
},
),
)
.log_err();
state.xcb_connection.flush().log_err();
};
}
let pressed_button = pressed_button_from_mask(event.button_mask[0]);
let position = point(
px(event.event_x as f32 / u16::MAX as f32 / state.scale_factor),
@@ -1516,7 +1489,7 @@ impl LinuxClient for X11Client {
let parent_window = state
.keyboard_focused_window
.and_then(|focused_window| state.windows.get(&focused_window))
.map(|w| w.window.clone());
.map(|window| window.window.x_window);
let x_window = state
.xcb_connection
.generate_id()
@@ -1571,15 +1544,7 @@ impl LinuxClient for X11Client {
.cursor_styles
.get(&focused_window)
.unwrap_or(&CursorStyle::Arrow);
let window = state
.mouse_focused_window
.and_then(|w| state.windows.get(&w));
let should_change = *current_style != style
&& (window.is_none() || window.is_some_and(|w| !w.is_blocked()));
if !should_change {
if *current_style == style {
return;
}

View File

@@ -11,7 +11,6 @@ use crate::{
};
use blade_graphics as gpu;
use collections::FxHashSet;
use raw_window_handle as rwh;
use util::{ResultExt, maybe};
use x11rb::{
@@ -75,7 +74,6 @@ x11rb::atom_manager! {
_NET_WM_WINDOW_TYPE,
_NET_WM_WINDOW_TYPE_NOTIFICATION,
_NET_WM_WINDOW_TYPE_DIALOG,
_NET_WM_STATE_MODAL,
_NET_WM_SYNC,
_NET_SUPPORTED,
_MOTIF_WM_HINTS,
@@ -251,8 +249,6 @@ pub struct Callbacks {
pub struct X11WindowState {
pub destroyed: bool,
parent: Option<X11WindowStatePtr>,
children: FxHashSet<xproto::Window>,
client: X11ClientStatePtr,
executor: ForegroundExecutor,
atoms: XcbAtoms,
@@ -398,7 +394,7 @@ impl X11WindowState {
atoms: &XcbAtoms,
scale_factor: f32,
appearance: WindowAppearance,
parent_window: Option<X11WindowStatePtr>,
parent_window: Option<xproto::Window>,
) -> anyhow::Result<Self> {
let x_screen_index = params
.display_id
@@ -550,8 +546,8 @@ impl X11WindowState {
)?;
}
if params.kind == WindowKind::Floating || params.kind == WindowKind::Dialog {
if let Some(parent_window) = parent_window.as_ref().map(|w| w.x_window) {
if params.kind == WindowKind::Floating {
if let Some(parent_window) = parent_window {
// WM_TRANSIENT_FOR hint indicating the main application window. For floating windows, we set
// a parent window (WM_TRANSIENT_FOR) such that the window manager knows where to
// place the floating window in relation to the main window.
@@ -567,23 +563,11 @@ impl X11WindowState {
),
)?;
}
}
let parent = if params.kind == WindowKind::Dialog
&& let Some(parent) = parent_window
{
parent.add_child(x_window);
Some(parent)
} else {
None
};
if params.kind == WindowKind::Dialog {
// _NET_WM_WINDOW_TYPE_DIALOG indicates that this is a dialog (floating) window
// https://specifications.freedesktop.org/wm-spec/1.4/ar01s05.html
check_reply(
|| "X11 ChangeProperty32 setting window type for dialog window failed.",
|| "X11 ChangeProperty32 setting window type for floating window failed.",
xcb.change_property32(
xproto::PropMode::REPLACE,
x_window,
@@ -592,20 +576,6 @@ impl X11WindowState {
&[atoms._NET_WM_WINDOW_TYPE_DIALOG],
),
)?;
// We set the modal state for dialog windows, so that the window manager
// can handle it appropriately (e.g., prevent interaction with the parent window
// while the dialog is open).
check_reply(
|| "X11 ChangeProperty32 setting modal state for dialog window failed.",
xcb.change_property32(
xproto::PropMode::REPLACE,
x_window,
atoms._NET_WM_STATE,
xproto::AtomEnum::ATOM,
&[atoms._NET_WM_STATE_MODAL],
),
)?;
}
check_reply(
@@ -697,8 +667,6 @@ impl X11WindowState {
let display = Rc::new(X11Display::new(xcb, scale_factor, x_screen_index)?);
Ok(Self {
parent,
children: FxHashSet::default(),
client,
executor,
display,
@@ -752,11 +720,6 @@ pub(crate) struct X11Window(pub X11WindowStatePtr);
impl Drop for X11Window {
fn drop(&mut self) {
let mut state = self.0.state.borrow_mut();
if let Some(parent) = state.parent.as_ref() {
parent.state.borrow_mut().children.remove(&self.0.x_window);
}
state.renderer.destroy();
let destroy_x_window = maybe!({
@@ -771,6 +734,8 @@ impl Drop for X11Window {
.log_err();
if destroy_x_window.is_some() {
// Mark window as destroyed so that we can filter out when X11 events
// for it still come in.
state.destroyed = true;
let this_ptr = self.0.clone();
@@ -808,7 +773,7 @@ impl X11Window {
atoms: &XcbAtoms,
scale_factor: f32,
appearance: WindowAppearance,
parent_window: Option<X11WindowStatePtr>,
parent_window: Option<xproto::Window>,
) -> anyhow::Result<Self> {
let ptr = X11WindowStatePtr {
state: Rc::new(RefCell::new(X11WindowState::new(
@@ -1014,31 +979,7 @@ impl X11WindowStatePtr {
Ok(())
}
pub fn add_child(&self, child: xproto::Window) {
let mut state = self.state.borrow_mut();
state.children.insert(child);
}
pub fn is_blocked(&self) -> bool {
let state = self.state.borrow();
!state.children.is_empty()
}
pub fn close(&self) {
let state = self.state.borrow();
let client = state.client.clone();
#[allow(clippy::mutable_key_type)]
let children = state.children.clone();
drop(state);
if let Some(client) = client.get_client() {
for child in children {
if let Some(child_window) = client.get_window(child) {
child_window.close();
}
}
}
let mut callbacks = self.callbacks.borrow_mut();
if let Some(fun) = callbacks.close.take() {
fun()
@@ -1053,9 +994,6 @@ impl X11WindowStatePtr {
}
pub fn handle_input(&self, input: PlatformInput) {
if self.is_blocked() {
return;
}
if let Some(ref mut fun) = self.callbacks.borrow_mut().input
&& !fun(input.clone()).propagate
{
@@ -1078,9 +1016,6 @@ impl X11WindowStatePtr {
}
pub fn handle_ime_commit(&self, text: String) {
if self.is_blocked() {
return;
}
let mut state = self.state.borrow_mut();
if let Some(mut input_handler) = state.input_handler.take() {
drop(state);
@@ -1091,9 +1026,6 @@ impl X11WindowStatePtr {
}
pub fn handle_ime_preedit(&self, text: String) {
if self.is_blocked() {
return;
}
let mut state = self.state.borrow_mut();
if let Some(mut input_handler) = state.input_handler.take() {
drop(state);
@@ -1104,9 +1036,6 @@ impl X11WindowStatePtr {
}
pub fn handle_ime_unmark(&self) {
if self.is_blocked() {
return;
}
let mut state = self.state.borrow_mut();
if let Some(mut input_handler) = state.input_handler.take() {
drop(state);
@@ -1117,9 +1046,6 @@ impl X11WindowStatePtr {
}
pub fn handle_ime_delete(&self) {
if self.is_blocked() {
return;
}
let mut state = self.state.borrow_mut();
if let Some(mut input_handler) = state.input_handler.take() {
drop(state);

View File

@@ -62,12 +62,9 @@ static mut BLURRED_VIEW_CLASS: *const Class = ptr::null();
#[allow(non_upper_case_globals)]
const NSWindowStyleMaskNonactivatingPanel: NSWindowStyleMask =
NSWindowStyleMask::from_bits_retain(1 << 7);
// WindowLevel const value ref: https://docs.rs/core-graphics2/0.4.1/src/core_graphics2/window_level.rs.html
#[allow(non_upper_case_globals)]
const NSNormalWindowLevel: NSInteger = 0;
#[allow(non_upper_case_globals)]
const NSFloatingWindowLevel: NSInteger = 3;
#[allow(non_upper_case_globals)]
const NSPopUpWindowLevel: NSInteger = 101;
#[allow(non_upper_case_globals)]
const NSTrackingMouseEnteredAndExited: NSUInteger = 0x01;
@@ -426,8 +423,6 @@ struct MacWindowState {
select_previous_tab_callback: Option<Box<dyn FnMut()>>,
toggle_tab_bar_callback: Option<Box<dyn FnMut()>>,
activated_least_once: bool,
// The parent window if this window is a sheet (Dialog kind)
sheet_parent: Option<id>,
}
impl MacWindowState {
@@ -627,16 +622,11 @@ impl MacWindow {
}
let native_window: id = match kind {
WindowKind::Normal => {
msg_send![WINDOW_CLASS, alloc]
}
WindowKind::Normal | WindowKind::Floating => msg_send![WINDOW_CLASS, alloc],
WindowKind::PopUp => {
style_mask |= NSWindowStyleMaskNonactivatingPanel;
msg_send![PANEL_CLASS, alloc]
}
WindowKind::Floating | WindowKind::Dialog => {
msg_send![PANEL_CLASS, alloc]
}
};
let display = display_id
@@ -739,7 +729,6 @@ impl MacWindow {
select_previous_tab_callback: None,
toggle_tab_bar_callback: None,
activated_least_once: false,
sheet_parent: None,
})));
(*native_window).set_ivar(
@@ -790,18 +779,9 @@ impl MacWindow {
content_view.addSubview_(native_view.autorelease());
native_window.makeFirstResponder_(native_view);
let app: id = NSApplication::sharedApplication(nil);
let main_window: id = msg_send![app, mainWindow];
let mut sheet_parent = None;
match kind {
WindowKind::Normal | WindowKind::Floating => {
if kind == WindowKind::Floating {
// Let the window float keep above normal windows.
native_window.setLevel_(NSFloatingWindowLevel);
} else {
native_window.setLevel_(NSNormalWindowLevel);
}
native_window.setLevel_(NSNormalWindowLevel);
native_window.setAcceptsMouseMovedEvents_(YES);
if let Some(tabbing_identifier) = tabbing_identifier {
@@ -836,23 +816,10 @@ impl MacWindow {
NSWindowCollectionBehavior::NSWindowCollectionBehaviorFullScreenAuxiliary
);
}
WindowKind::Dialog => {
if !main_window.is_null() {
let parent = {
let active_sheet: id = msg_send![main_window, attachedSheet];
if active_sheet.is_null() {
main_window
} else {
active_sheet
}
};
let _: () =
msg_send![parent, beginSheet: native_window completionHandler: nil];
sheet_parent = Some(parent);
}
}
}
let app = NSApplication::sharedApplication(nil);
let main_window: id = msg_send![app, mainWindow];
if allows_automatic_window_tabbing
&& !main_window.is_null()
&& main_window != native_window
@@ -894,11 +861,7 @@ impl MacWindow {
// the window position might be incorrect if the main screen (the screen that contains the window that has focus)
// is different from the primary screen.
NSWindow::setFrameTopLeftPoint_(native_window, window_rect.origin);
{
let mut window_state = window.0.lock();
window_state.move_traffic_light();
window_state.sheet_parent = sheet_parent;
}
window.0.lock().move_traffic_light();
pool.drain();
@@ -975,7 +938,6 @@ impl Drop for MacWindow {
let mut this = self.0.lock();
this.renderer.destroy();
let window = this.native_window;
let sheet_parent = this.sheet_parent.take();
this.display_link.take();
unsafe {
this.native_window.setDelegate_(nil);
@@ -984,9 +946,6 @@ impl Drop for MacWindow {
this.executor
.spawn(async move {
unsafe {
if let Some(parent) = sheet_parent {
let _: () = msg_send![parent, endSheet: window];
}
window.close();
window.autorelease();
}

View File

@@ -3,7 +3,7 @@ use crate::{
DummyKeyboardMapper, ForegroundExecutor, Keymap, NoopTextSystem, Platform, PlatformDisplay,
PlatformKeyboardLayout, PlatformKeyboardMapper, PlatformTextSystem, PromptButton,
ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream, SourceMetadata, Task,
TestDisplay, TestPlatformWindow, WindowAppearance, WindowParams, size,
TestDisplay, TestWindow, WindowAppearance, WindowParams, size,
};
use anyhow::Result;
use collections::VecDeque;
@@ -26,7 +26,7 @@ pub(crate) struct TestPlatform {
background_executor: BackgroundExecutor,
foreground_executor: ForegroundExecutor,
pub(crate) active_window: RefCell<Option<TestPlatformWindow>>,
pub(crate) active_window: RefCell<Option<TestWindow>>,
active_display: Rc<dyn PlatformDisplay>,
active_cursor: Mutex<CursorStyle>,
current_clipboard_item: Mutex<Option<ClipboardItem>>,
@@ -196,7 +196,7 @@ impl TestPlatform {
rx
}
pub(crate) fn set_active_window(&self, window: Option<TestPlatformWindow>) {
pub(crate) fn set_active_window(&self, window: Option<TestWindow>) {
let executor = self.foreground_executor();
let previous_window = self.active_window.borrow_mut().take();
self.active_window.borrow_mut().clone_from(&window);
@@ -314,7 +314,7 @@ impl Platform for TestPlatform {
handle: AnyWindowHandle,
params: WindowParams,
) -> anyhow::Result<Box<dyn crate::PlatformWindow>> {
let window = TestPlatformWindow::new(
let window = TestWindow::new(
handle,
params,
self.weak.clone(),

View File

@@ -12,7 +12,7 @@ use std::{
sync::{self, Arc},
};
pub(crate) struct TestPlatformWindowState {
pub(crate) struct TestWindowState {
pub(crate) bounds: Bounds<Pixels>,
pub(crate) handle: AnyWindowHandle,
display: Rc<dyn PlatformDisplay>,
@@ -32,9 +32,9 @@ pub(crate) struct TestPlatformWindowState {
}
#[derive(Clone)]
pub(crate) struct TestPlatformWindow(pub(crate) Rc<Mutex<TestPlatformWindowState>>);
pub(crate) struct TestWindow(pub(crate) Rc<Mutex<TestWindowState>>);
impl HasWindowHandle for TestPlatformWindow {
impl HasWindowHandle for TestWindow {
fn window_handle(
&self,
) -> Result<raw_window_handle::WindowHandle<'_>, raw_window_handle::HandleError> {
@@ -42,7 +42,7 @@ impl HasWindowHandle for TestPlatformWindow {
}
}
impl HasDisplayHandle for TestPlatformWindow {
impl HasDisplayHandle for TestWindow {
fn display_handle(
&self,
) -> Result<raw_window_handle::DisplayHandle<'_>, raw_window_handle::HandleError> {
@@ -50,14 +50,14 @@ impl HasDisplayHandle for TestPlatformWindow {
}
}
impl TestPlatformWindow {
impl TestWindow {
pub fn new(
handle: AnyWindowHandle,
params: WindowParams,
platform: Weak<TestPlatform>,
display: Rc<dyn PlatformDisplay>,
) -> Self {
Self(Rc::new(Mutex::new(TestPlatformWindowState {
Self(Rc::new(Mutex::new(TestWindowState {
bounds: params.bounds,
display,
platform,
@@ -111,7 +111,7 @@ impl TestPlatformWindow {
}
}
impl PlatformWindow for TestPlatformWindow {
impl PlatformWindow for TestWindow {
fn bounds(&self) -> Bounds<Pixels> {
self.0.lock().bounds
}
@@ -272,7 +272,7 @@ impl PlatformWindow for TestPlatformWindow {
self.0.lock().sprite_atlas.clone()
}
fn as_test(&mut self) -> Option<&mut TestPlatformWindow> {
fn as_test(&mut self) -> Option<&mut TestWindow> {
Some(self)
}

View File

@@ -40,11 +40,6 @@ impl WindowsWindowInner {
lparam: LPARAM,
) -> LRESULT {
let handled = match msg {
// eagerly activate the window, so calls to `active_window` will work correctly
WM_MOUSEACTIVATE => {
unsafe { SetActiveWindow(handle).log_err() };
None
}
WM_ACTIVATE => self.handle_activate_msg(wparam),
WM_CREATE => self.handle_create_msg(handle),
WM_MOVE => self.handle_move_msg(handle, lparam),
@@ -270,14 +265,6 @@ impl WindowsWindowInner {
fn handle_destroy_msg(&self, handle: HWND) -> Option<isize> {
let callback = { self.state.callbacks.close.take() };
// Re-enable parent window if this was a modal dialog
if let Some(parent_hwnd) = self.parent_hwnd {
unsafe {
let _ = EnableWindow(parent_hwnd, true);
let _ = SetForegroundWindow(parent_hwnd);
}
}
if let Some(callback) = callback {
callback();
}

View File

@@ -659,7 +659,7 @@ impl Platform for WindowsPlatform {
if let Err(err) = result {
// ERROR_NOT_FOUND means the credential doesn't exist.
// Return Ok(None) to match macOS and Linux behavior.
if err.code() == ERROR_NOT_FOUND.to_hresult() {
if err.code().0 == ERROR_NOT_FOUND.0 as i32 {
return Ok(None);
}
return Err(err.into());

View File

@@ -83,7 +83,6 @@ pub(crate) struct WindowsWindowInner {
pub(crate) validation_number: usize,
pub(crate) main_receiver: flume::Receiver<RunnableVariant>,
pub(crate) platform_window_handle: HWND,
pub(crate) parent_hwnd: Option<HWND>,
}
impl WindowsWindowState {
@@ -242,7 +241,6 @@ impl WindowsWindowInner {
main_receiver: context.main_receiver.clone(),
platform_window_handle: context.platform_window_handle,
system_settings: WindowsSystemSettings::new(context.display),
parent_hwnd: context.parent_hwnd,
}))
}
@@ -370,7 +368,6 @@ struct WindowCreateContext {
disable_direct_composition: bool,
directx_devices: DirectXDevices,
invalidate_devices: Arc<AtomicBool>,
parent_hwnd: Option<HWND>,
}
impl WindowsWindow {
@@ -393,20 +390,6 @@ impl WindowsWindow {
invalidate_devices,
} = creation_info;
register_window_class(icon);
let parent_hwnd = if params.kind == WindowKind::Dialog {
let parent_window = unsafe { GetActiveWindow() };
if parent_window.is_invalid() {
None
} else {
// Disable the parent window to make this dialog modal
unsafe {
EnableWindow(parent_window, false).as_bool();
};
Some(parent_window)
}
} else {
None
};
let hide_title_bar = params
.titlebar
.as_ref()
@@ -433,14 +416,8 @@ impl WindowsWindow {
if params.is_minimizable {
dwstyle |= WS_MINIMIZEBOX;
}
let dwexstyle = if params.kind == WindowKind::Dialog {
dwstyle |= WS_POPUP | WS_CAPTION;
WS_EX_DLGMODALFRAME
} else {
WS_EX_APPWINDOW
};
(dwexstyle, dwstyle)
(WS_EX_APPWINDOW, dwstyle)
};
if !disable_direct_composition {
dwexstyle |= WS_EX_NOREDIRECTIONBITMAP;
@@ -472,7 +449,6 @@ impl WindowsWindow {
disable_direct_composition,
directx_devices,
invalidate_devices,
parent_hwnd,
};
let creation_result = unsafe {
CreateWindowExW(
@@ -484,7 +460,7 @@ impl WindowsWindow {
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
parent_hwnd,
None,
None,
Some(hinstance.into()),
Some(&context as *const _ as *const _),

View File

@@ -20,126 +20,6 @@ pub(crate) type PathVertex_ScaledPixels = PathVertex<ScaledPixels>;
pub(crate) type DrawOrder = u32;
/// Test-only scene snapshot for inspecting rendered content.
#[cfg(any(test, feature = "test-support"))]
pub mod test_scene {
use crate::{Bounds, Hsla, Point, ScaledPixels, SharedString};
/// A rendered quad (background, border, cursor, selection, etc.)
#[derive(Debug, Clone)]
pub struct RenderedQuad {
/// Bounds in scaled pixels.
pub bounds: Bounds<ScaledPixels>,
/// Background color (if solid).
pub background_color: Option<Hsla>,
/// Border color.
pub border_color: Hsla,
}
/// A named diagnostic quad for tests and debugging of imperative paint logic.
///
/// This is not necessarily a "real" painted quad; it is metadata recorded alongside a scene.
#[derive(Debug, Clone)]
pub struct DiagnosticQuad {
/// A stable name that test code can filter by.
pub name: SharedString,
/// Bounds in scaled pixels.
pub bounds: Bounds<ScaledPixels>,
/// Optional color hint (useful when visualizing).
pub color: Option<Hsla>,
}
/// A rendered text glyph.
#[derive(Debug, Clone)]
pub struct RenderedGlyph {
/// Origin position in scaled pixels.
pub origin: Point<ScaledPixels>,
/// Size in scaled pixels.
pub size: crate::Size<ScaledPixels>,
/// Color of the glyph.
pub color: Hsla,
}
/// Snapshot of scene contents for testing.
#[derive(Debug, Default)]
pub struct SceneSnapshot {
/// All rendered quads.
pub quads: Vec<RenderedQuad>,
/// All rendered text glyphs.
pub glyphs: Vec<RenderedGlyph>,
/// Named diagnostic quads recorded by imperative drawing code for tests/debugging.
pub diagnostic_quads: Vec<DiagnosticQuad>,
/// Number of shadow primitives.
pub shadow_count: usize,
/// Number of path primitives.
pub path_count: usize,
/// Number of underline primitives.
pub underline_count: usize,
/// Number of polychrome sprites (images, emoji).
pub polychrome_sprite_count: usize,
/// Number of surface primitives.
pub surface_count: usize,
}
impl SceneSnapshot {
/// Get unique Y positions of quads, sorted.
pub fn quad_y_positions(&self) -> Vec<f32> {
let mut positions: Vec<f32> = self.quads.iter().map(|q| q.bounds.origin.y.0).collect();
positions.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
positions.dedup();
positions
}
/// Get unique Y positions of glyphs, sorted.
pub fn glyph_y_positions(&self) -> Vec<f32> {
let mut positions: Vec<f32> = self.glyphs.iter().map(|g| g.origin.y.0).collect();
positions.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
positions.dedup();
positions
}
/// Find quads within a Y range.
pub fn quads_in_y_range(&self, min_y: f32, max_y: f32) -> Vec<&RenderedQuad> {
self.quads
.iter()
.filter(|q| {
let y = q.bounds.origin.y.0;
y >= min_y && y < max_y
})
.collect()
}
/// Find glyphs within a Y range.
pub fn glyphs_in_y_range(&self, min_y: f32, max_y: f32) -> Vec<&RenderedGlyph> {
self.glyphs
.iter()
.filter(|g| {
let y = g.origin.y.0;
y >= min_y && y < max_y
})
.collect()
}
/// Debug summary string.
pub fn summary(&self) -> String {
format!(
"quads: {}, glyphs: {}, diagnostic_quads: {}, shadows: {}, paths: {}, underlines: {}, polychrome: {}, surfaces: {}",
self.quads.len(),
self.glyphs.len(),
self.diagnostic_quads.len(),
self.shadow_count,
self.path_count,
self.underline_count,
self.polychrome_sprite_count,
self.surface_count,
)
}
}
}
#[cfg(any(test, feature = "test-support"))]
pub use test_scene::*;
#[derive(Default)]
pub(crate) struct Scene {
pub(crate) paint_operations: Vec<PaintOperation>,
@@ -152,8 +32,6 @@ pub(crate) struct Scene {
pub(crate) monochrome_sprites: Vec<MonochromeSprite>,
pub(crate) polychrome_sprites: Vec<PolychromeSprite>,
pub(crate) surfaces: Vec<PaintSurface>,
#[cfg(any(test, feature = "test-support"))]
pub(crate) diagnostic_quads: Vec<test_scene::DiagnosticQuad>,
}
impl Scene {
@@ -168,8 +46,6 @@ impl Scene {
self.monochrome_sprites.clear();
self.polychrome_sprites.clear();
self.surfaces.clear();
#[cfg(any(test, feature = "test-support"))]
self.diagnostic_quads.clear();
}
pub fn len(&self) -> usize {
@@ -248,41 +124,6 @@ impl Scene {
}
}
/// Create a snapshot of the scene for testing.
#[cfg(any(test, feature = "test-support"))]
pub fn snapshot(&self) -> SceneSnapshot {
let quads = self
.quads
.iter()
.map(|q| RenderedQuad {
bounds: q.bounds,
background_color: q.background.as_solid(),
border_color: q.border_color,
})
.collect();
let glyphs = self
.monochrome_sprites
.iter()
.map(|s| RenderedGlyph {
origin: s.bounds.origin,
size: s.bounds.size,
color: s.color,
})
.collect();
SceneSnapshot {
quads,
glyphs,
diagnostic_quads: self.diagnostic_quads.clone(),
shadow_count: self.shadows.len(),
path_count: self.paths.len(),
underline_count: self.underlines.len(),
polychrome_sprite_count: self.polychrome_sprites.len(),
surface_count: self.surfaces.len(),
}
}
pub fn finish(&mut self) {
self.shadows.sort_by_key(|shadow| shadow.order);
self.quads.sort_by_key(|quad| quad.order);
@@ -293,10 +134,6 @@ impl Scene {
self.polychrome_sprites
.sort_by_key(|sprite| (sprite.order, sprite.tile.tile_id));
self.surfaces.sort_by_key(|surface| surface.order);
#[cfg(any(test, feature = "test-support"))]
self.diagnostic_quads
.sort_by(|a, b| a.name.as_ref().cmp(b.name.as_ref()));
}
#[cfg_attr(
@@ -783,7 +620,7 @@ impl Default for TransformationMatrix {
#[repr(C)]
pub(crate) struct MonochromeSprite {
pub order: DrawOrder,
pub pad: u32,
pub pad: u32, // align to 8 bytes
pub bounds: Bounds<ScaledPixels>,
pub content_mask: ContentMask<ScaledPixels>,
pub color: Hsla,
@@ -801,7 +638,7 @@ impl From<MonochromeSprite> for Primitive {
#[repr(C)]
pub(crate) struct PolychromeSprite {
pub order: DrawOrder,
pub pad: u32,
pub pad: u32, // align to 8 bytes
pub grayscale: bool,
pub opacity: f32,
pub bounds: Bounds<ScaledPixels>,

View File

@@ -506,10 +506,6 @@ impl HitboxId {
///
/// See [`Hitbox::is_hovered`] for details.
pub fn is_hovered(self, window: &Window) -> bool {
// If this hitbox has captured the pointer, it's always considered hovered
if window.captured_hitbox == Some(self) {
return true;
}
let hit_test = &window.mouse_hit_test;
for id in hit_test.ids.iter().take(hit_test.hover_hitbox_count) {
if self == *id {
@@ -764,11 +760,6 @@ impl Frame {
self.tab_stops.clear();
self.focus = None;
#[cfg(any(test, feature = "test-support"))]
{
self.debug_bounds.clear();
}
#[cfg(any(feature = "inspector", debug_assertions))]
{
self.next_inspector_instance_ids.clear();
@@ -896,9 +887,6 @@ pub struct Window {
pub(crate) pending_input_observers: SubscriberSet<(), AnyObserver>,
prompt: Option<RenderablePromptHandle>,
pub(crate) client_inset: Option<Pixels>,
/// The hitbox that has captured the pointer, if any.
/// While captured, mouse events route to this hitbox regardless of hit testing.
captured_hitbox: Option<HitboxId>,
#[cfg(any(feature = "inspector", debug_assertions))]
inspector: Option<Entity<Inspector>>,
}
@@ -1323,7 +1311,6 @@ impl Window {
prompt: None,
client_inset: None,
image_cache_stack: Vec::new(),
captured_hitbox: None,
#[cfg(any(feature = "inspector", debug_assertions))]
inspector: None,
})
@@ -2007,26 +1994,6 @@ impl Window {
self.mouse_position
}
/// Captures the pointer for the given hitbox. While captured, all mouse move and mouse up
/// events will be routed to listeners that check this hitbox's `is_hovered` status,
/// regardless of actual hit testing. This enables drag operations that continue
/// even when the pointer moves outside the element's bounds.
///
/// The capture is automatically released on mouse up.
pub fn capture_pointer(&mut self, hitbox_id: HitboxId) {
self.captured_hitbox = Some(hitbox_id);
}
/// Releases any active pointer capture.
pub fn release_pointer(&mut self) {
self.captured_hitbox = None;
}
/// Returns the hitbox that has captured the pointer, if any.
pub fn captured_hitbox(&self) -> Option<HitboxId> {
self.captured_hitbox
}
/// The current state of the keyboard's modifiers
pub fn modifiers(&self) -> Modifiers {
self.modifiers
@@ -2999,41 +2966,6 @@ impl Window {
});
}
#[cfg(any(test, feature = "test-support"))]
/// Record a named diagnostic quad for test/debug snapshots.
///
/// This is intended for debugging and asserting against imperative painting logic. The
/// recorded quad does not affect rendering; it is captured alongside the rendered scene and
/// exposed via `scene_snapshot()`.
pub fn record_diagnostic_quad(
&mut self,
name: impl Into<SharedString>,
bounds: Bounds<Pixels>,
color: Option<Hsla>,
) {
self.invalidator.debug_assert_paint();
let scale_factor = self.scale_factor();
self.next_frame.scene.diagnostic_quads.push(crate::test_scene::DiagnosticQuad {
name: name.into(),
bounds: bounds.scale(scale_factor),
color,
});
}
#[cfg(not(any(test, feature = "test-support")))]
#[inline]
/// Record a named diagnostic quad for test/debug snapshots.
///
/// This is a no-op unless tests or the `test-support` feature are enabled.
pub fn record_diagnostic_quad(
&mut self,
_name: impl Into<SharedString>,
_bounds: Bounds<Pixels>,
_color: Option<Hsla>,
) {
}
/// Paint the given `Path` into the scene for the next frame at the current z-index.
///
/// This method should only be called as part of the paint phase of element drawing.
@@ -3918,11 +3850,6 @@ impl Window {
self.refresh();
}
}
// Auto-release pointer capture on mouse up
if event.is::<MouseUpEvent>() && self.captured_hitbox.is_some() {
self.captured_hitbox = None;
}
}
fn dispatch_key_event(&mut self, event: &dyn Any, cx: &mut App) {
@@ -5039,7 +4966,7 @@ impl<V: 'static> From<WindowHandle<V>> for AnyWindowHandle {
}
/// A handle to a window with any root view type, which can be downcast to a window with a specific root view type.
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
#[derive(Copy, Clone, PartialEq, Eq, Hash)]
pub struct AnyWindowHandle {
pub(crate) id: WindowId,
state_type: TypeId,

View File

@@ -1801,7 +1801,9 @@ impl Buffer {
self.syntax_map.lock().did_parse(syntax_snapshot);
self.request_autoindent(cx);
self.parse_status.0.send(ParseStatus::Idle).unwrap();
self.invalidate_tree_sitter_data(self.text.snapshot());
if self.text.version() != *self.tree_sitter_data.version() {
self.invalidate_tree_sitter_data(self.text.snapshot());
}
cx.emit(BufferEvent::Reparsed);
cx.notify();
}

View File

@@ -295,23 +295,6 @@ impl LspInstaller for TyLspAdapter {
})
}
async fn check_if_user_installed(
&self,
delegate: &dyn LspAdapterDelegate,
_: Option<Toolchain>,
_: &AsyncApp,
) -> Option<LanguageServerBinary> {
let Some(ty_bin) = delegate.which(Self::SERVER_NAME.as_ref()).await else {
return None;
};
let env = delegate.shell_env().await;
Some(LanguageServerBinary {
path: ty_bin,
env: Some(env),
arguments: vec!["server".into()],
})
}
async fn fetch_server_binary(
&self,
latest_version: Self::BinaryVersion,

View File

@@ -355,7 +355,7 @@ impl LspAdapter for RustLspAdapter {
| lsp::CompletionTextEdit::Edit(lsp::TextEdit { new_text, .. }),
) = completion.text_edit.as_ref()
&& let Ok(mut snippet) = snippet::Snippet::parse(new_text)
&& snippet.tabstops.len() > 1
&& !snippet.tabstops.is_empty()
{
label = String::new();
@@ -421,9 +421,7 @@ impl LspAdapter for RustLspAdapter {
0..label.rfind('(').unwrap_or(completion.label.len()),
highlight_id,
));
} else if detail_left.is_none()
&& kind != Some(lsp::CompletionItemKind::SNIPPET)
{
} else if detail_left.is_none() {
return None;
}
}
@@ -1599,40 +1597,6 @@ mod tests {
))
);
// Postfix completion without actual tabstops (only implicit final $0)
// The label should use completion.label so it can be filtered by "ref"
let ref_completion = adapter
.label_for_completion(
&lsp::CompletionItem {
kind: Some(lsp::CompletionItemKind::SNIPPET),
label: "ref".to_string(),
filter_text: Some("ref".to_string()),
label_details: Some(CompletionItemLabelDetails {
detail: None,
description: Some("&expr".to_string()),
}),
detail: Some("&expr".to_string()),
insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
range: lsp::Range::default(),
new_text: "&String::new()".to_string(),
})),
..Default::default()
},
&language,
)
.await;
assert!(
ref_completion.is_some(),
"ref postfix completion should have a label"
);
let ref_label = ref_completion.unwrap();
let filter_text = &ref_label.text[ref_label.filter_range.clone()];
assert!(
filter_text.contains("ref"),
"filter range text '{filter_text}' should contain 'ref' for filtering to work",
);
// Test for correct range calculation with mixed empty and non-empty tabstops.(See https://github.com/zed-industries/zed/issues/44825)
let res = adapter
.label_for_completion(

View File

@@ -155,15 +155,15 @@ impl Model {
pub fn max_token_count(&self) -> u64 {
match self {
Self::CodestralLatest => 256000,
Self::MistralLargeLatest => 256000,
Self::MistralLargeLatest => 131000,
Self::MistralMediumLatest => 128000,
Self::MistralSmallLatest => 32000,
Self::MagistralMediumLatest => 128000,
Self::MagistralSmallLatest => 128000,
Self::MagistralMediumLatest => 40000,
Self::MagistralSmallLatest => 40000,
Self::OpenMistralNemo => 131000,
Self::OpenCodestralMamba => 256000,
Self::DevstralMediumLatest => 256000,
Self::DevstralSmallLatest => 256000,
Self::DevstralMediumLatest => 128000,
Self::DevstralSmallLatest => 262144,
Self::Pixtral12BLatest => 128000,
Self::PixtralLargeLatest => 128000,
Self::Custom { max_tokens, .. } => *max_tokens,

View File

@@ -2610,8 +2610,9 @@ impl MultiBuffer {
for range in ranges {
let range = range.to_point(&snapshot);
let start = snapshot.point_to_offset(Point::new(range.start.row, 0));
let end = (snapshot.point_to_offset(Point::new(range.end.row + 1, 0)) + 1usize)
.min(snapshot.len());
let end = snapshot.point_to_offset(Point::new(range.end.row + 1, 0));
let start = start.saturating_sub_usize(1);
let end = snapshot.len().min(end + 1usize);
cursor.seek(&start, Bias::Right);
while let Some(item) = cursor.item() {
if *cursor.start() >= end {

View File

@@ -460,7 +460,7 @@ impl AgentServerStore {
.gemini
.as_ref()
.and_then(|settings| settings.ignore_system_version)
.unwrap_or(true),
.unwrap_or(false),
}),
);
self.external_agents.insert(

View File

@@ -1672,6 +1672,59 @@ impl GitStore {
}
}
fn mark_entries_pending_by_project_paths(
&mut self,
project_paths: &[ProjectPath],
stage: bool,
cx: &mut Context<Self>,
) {
let buffer_store = &self.buffer_store;
for project_path in project_paths {
let Some(buffer) = buffer_store.read(cx).get_by_path(project_path) else {
continue;
};
let buffer_id = buffer.read(cx).remote_id();
let Some(diff_state) = self.diffs.get(&buffer_id) else {
continue;
};
diff_state.update(cx, |diff_state, cx| {
let Some(uncommitted_diff) = diff_state.uncommitted_diff() else {
return;
};
let buffer_snapshot = buffer.read(cx).text_snapshot();
let file_exists = buffer
.read(cx)
.file()
.is_some_and(|file| file.disk_state().exists());
let all_hunks: Vec<_> = uncommitted_diff
.read(cx)
.hunks_intersecting_range(
text::Anchor::MIN..text::Anchor::MAX,
&buffer_snapshot,
cx,
)
.collect();
if !all_hunks.is_empty() {
uncommitted_diff.update(cx, |diff, cx| {
diff.stage_or_unstage_hunks(
stage,
&all_hunks,
&buffer_snapshot,
file_exists,
cx,
);
});
}
});
}
}
pub fn git_clone(
&self,
repo: String,
@@ -4200,6 +4253,28 @@ impl Repository {
save_futures
}
fn mark_entries_pending_for_stage(
&self,
entries: &[RepoPath],
stage: bool,
cx: &mut Context<Self>,
) {
let Some(git_store) = self.git_store() else {
return;
};
let mut project_paths = Vec::new();
for repo_path in entries {
if let Some(project_path) = self.repo_path_to_project_path(repo_path, cx) {
project_paths.push(project_path);
}
}
git_store.update(cx, move |git_store, cx| {
git_store.mark_entries_pending_by_project_paths(&project_paths, stage, cx);
});
}
pub fn stage_entries(
&mut self,
entries: Vec<RepoPath>,
@@ -4208,6 +4283,9 @@ impl Repository {
if entries.is_empty() {
return Task::ready(Ok(()));
}
self.mark_entries_pending_for_stage(&entries, true, cx);
let id = self.id;
let save_tasks = self.save_buffers(&entries, cx);
let paths = entries
@@ -4273,6 +4351,9 @@ impl Repository {
if entries.is_empty() {
return Task::ready(Ok(()));
}
self.mark_entries_pending_for_stage(&entries, false, cx);
let id = self.id;
let save_tasks = self.save_buffers(&entries, cx);
let paths = entries

View File

@@ -790,7 +790,8 @@ impl TerminalPanel {
}
pane.update(cx, |pane, cx| {
let focus = matches!(reveal_strategy, RevealStrategy::Always);
let focus = pane.has_focus(window, cx)
|| matches!(reveal_strategy, RevealStrategy::Always);
pane.add_item(terminal_view, true, focus, None, window, cx);
});
@@ -852,7 +853,8 @@ impl TerminalPanel {
}
pane.update(cx, |pane, cx| {
let focus = matches!(reveal_strategy, RevealStrategy::Always);
let focus = pane.has_focus(window, cx)
|| matches!(reveal_strategy, RevealStrategy::Always);
pane.add_item(terminal_view, true, focus, None, window, cx);
});
@@ -1169,67 +1171,64 @@ pub fn new_terminal_pane(
let source = tab.pane.clone();
let item_id_to_move = item.item_id();
// If no split direction, let the regular pane drop handler take care of it
let Some(split_direction) = pane.drag_split_direction() else {
return ControlFlow::Continue(());
let Ok(new_split_pane) = pane
.drag_split_direction()
.map(|split_direction| {
drop_closure_terminal_panel.update(cx, |terminal_panel, cx| {
let is_zoomed = if terminal_panel.active_pane == this_pane {
pane.is_zoomed()
} else {
terminal_panel.active_pane.read(cx).is_zoomed()
};
let new_pane = new_terminal_pane(
workspace.clone(),
project.clone(),
is_zoomed,
window,
cx,
);
terminal_panel.apply_tab_bar_buttons(&new_pane, cx);
terminal_panel.center.split(
&this_pane,
&new_pane,
split_direction,
cx,
)?;
anyhow::Ok(new_pane)
})
})
.transpose()
else {
return ControlFlow::Break(());
};
// Gather data synchronously before deferring
let is_zoomed = drop_closure_terminal_panel
.upgrade()
.map(|terminal_panel| {
let terminal_panel = terminal_panel.read(cx);
if terminal_panel.active_pane == this_pane {
pane.is_zoomed()
} else {
terminal_panel.active_pane.read(cx).is_zoomed()
}
})
.unwrap_or(false);
let workspace = workspace.clone();
let terminal_panel = drop_closure_terminal_panel.clone();
// Defer the split operation to avoid re-entrancy panic.
// The pane may be the one currently being updated, so we cannot
// call mark_positions (via split) synchronously.
cx.spawn_in(window, async move |_, cx| {
cx.update(|window, cx| {
let Ok(new_pane) =
terminal_panel.update(cx, |terminal_panel, cx| {
let new_pane = new_terminal_pane(
workspace, project, is_zoomed, window, cx,
);
terminal_panel.apply_tab_bar_buttons(&new_pane, cx);
terminal_panel.center.split(
&this_pane,
match new_split_pane.transpose() {
// Source pane may be the one currently updated, so defer the move.
Ok(Some(new_pane)) => cx
.spawn_in(window, async move |_, cx| {
cx.update(|window, cx| {
move_item(
&source,
&new_pane,
split_direction,
item_id_to_move,
new_pane.read(cx).active_item_index(),
true,
window,
cx,
)?;
anyhow::Ok(new_pane)
);
})
else {
return;
};
let Some(new_pane) = new_pane.log_err() else {
return;
};
move_item(
&source,
&new_pane,
item_id_to_move,
new_pane.read(cx).active_item_index(),
true,
window,
cx,
);
})
.ok();
})
.detach();
.ok();
})
.detach(),
// If we drop into existing pane or current pane,
// regular pane drop handler will take care of it,
// using the right tab index for the operation.
Ok(None) => return ControlFlow::Continue(()),
err @ Err(_) => {
err.log_err();
return ControlFlow::Break(());
}
};
} else if let Some(project_path) = item.project_path(cx)
&& let Some(entry_path) = project.read(cx).absolute_path(&project_path, cx)
{

View File

@@ -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, ClipboardEntry, DismissEvent, Entity, EventEmitter, FocusHandle,
Focusable, KeyContext, KeyDownEvent, Keystroke, MouseButton, MouseDownEvent, Pixels, Render,
Action, AnyElement, App, 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,32 +687,12 @@ impl TerminalView {
///Attempt to paste the clipboard into the terminal
fn paste(&mut self, _: &Paste, _: &mut Window, cx: &mut Context<Self>) {
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() {
if let Some(clipboard_string) = cx.read_from_clipboard().and_then(|item| item.text()) {
self.terminal
.update(cx, |terminal, _cx| terminal.paste(&text));
.update(cx, |terminal, _cx| terminal.paste(&clipboard_string));
}
}
/// 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>) {
self.clear_bell(cx);
self.terminal.update(cx, |term, _| {

View File

@@ -121,7 +121,7 @@ impl RenderOnce for Callout {
Severity::Info => (
IconName::Info,
Color::Muted,
cx.theme().status().info_background.opacity(0.1),
cx.theme().colors().panel_background.opacity(0.),
),
Severity::Success => (
IconName::Check,

View File

@@ -1303,6 +1303,39 @@ async fn test_mouse_selection(cx: &mut TestAppContext) {
cx.assert_state("one «ˇtwo» three", Mode::Visual)
}
#[gpui::test]
async fn test_mouse_drag_across_anchor_does_not_drift(cx: &mut TestAppContext) {
let mut cx = VimTestContext::new(cx, true).await;
cx.set_state("ˇone two three four", Mode::Normal);
let click_pos = cx.pixel_position("one ˇtwo three four");
let drag_left = cx.pixel_position("ˇone two three four");
let anchor_pos = cx.pixel_position("one tˇwo three four");
cx.simulate_mouse_down(click_pos, MouseButton::Left, Modifiers::none());
cx.run_until_parked();
cx.simulate_mouse_move(drag_left, MouseButton::Left, Modifiers::none());
cx.run_until_parked();
cx.assert_state("«ˇone t»wo three four", Mode::Visual);
cx.simulate_mouse_move(anchor_pos, MouseButton::Left, Modifiers::none());
cx.run_until_parked();
cx.simulate_mouse_move(drag_left, MouseButton::Left, Modifiers::none());
cx.run_until_parked();
cx.assert_state("«ˇone t»wo three four", Mode::Visual);
cx.simulate_mouse_move(anchor_pos, MouseButton::Left, Modifiers::none());
cx.run_until_parked();
cx.simulate_mouse_move(drag_left, MouseButton::Left, Modifiers::none());
cx.run_until_parked();
cx.assert_state("«ˇone t»wo three four", Mode::Visual);
cx.simulate_mouse_up(drag_left, MouseButton::Left, Modifiers::none());
}
#[perf]
#[gpui::test]
async fn test_lowercase_marks(cx: &mut TestAppContext) {

View File

@@ -23,8 +23,9 @@ use crate::normal::paste::Paste as VimPaste;
use collections::HashMap;
use editor::{
Anchor, Bias, Editor, EditorEvent, EditorSettings, HideMouseCursorOrigin, MultiBufferOffset,
SelectionEffects, ToPoint,
SelectionEffects,
actions::Paste,
display_map::ToDisplayPoint,
movement::{self, FindRange},
};
use gpui::{
@@ -37,6 +38,8 @@ use language::{
};
pub use mode_indicator::ModeIndicator;
use motion::Motion;
use multi_buffer::ToPoint as _;
use multi_buffer::ToPoint as _;
use normal::search::SearchSubmit;
use object::Object;
use schemars::JsonSchema;
@@ -503,6 +506,7 @@ pub(crate) struct Vim {
pub(crate) current_anchor: Option<Selection<Anchor>>,
pub(crate) undo_modes: HashMap<TransactionId, Mode>,
pub(crate) undo_last_line_tx: Option<TransactionId>,
extended_pending_selection_id: Option<usize>,
selected_register: Option<char>,
pub search: SearchState,
@@ -561,6 +565,7 @@ impl Vim {
current_tx: None,
undo_last_line_tx: None,
current_anchor: None,
extended_pending_selection_id: None,
undo_modes: HashMap::default(),
status_label: None,
@@ -1218,17 +1223,32 @@ impl Vim {
s.select_anchor_ranges(vec![pos..pos])
}
let snapshot = s.display_snapshot();
if let Some(pending) = s.pending_anchor_mut()
&& pending.reversed
let mut should_extend_pending = false;
if !last_mode.is_visual()
&& mode.is_visual()
&& !last_mode.is_visual()
&& let Some(pending) = s.pending_anchor()
{
let mut end = pending.end.to_point(&snapshot.buffer_snapshot());
end = snapshot
.buffer_snapshot()
.clip_point(end + Point::new(0, 1), Bias::Right);
pending.end = snapshot.buffer_snapshot().anchor_before(end);
let snapshot = s.display_snapshot();
let is_empty = pending
.start
.cmp(&pending.end, &snapshot.buffer_snapshot())
.is_eq();
should_extend_pending = pending.reversed
&& !is_empty
&& vim.extended_pending_selection_id != Some(pending.id);
};
if should_extend_pending {
let snapshot = s.display_snapshot();
if let Some(pending) = s.pending_anchor_mut() {
let end = pending.end.to_point(&snapshot.buffer_snapshot());
let end = end.to_display_point(&snapshot);
let new_end = movement::right(&snapshot, end);
pending.end = snapshot
.buffer_snapshot()
.anchor_before(new_end.to_point(&snapshot));
}
vim.extended_pending_selection_id = s.pending_anchor().map(|p| p.id)
}
s.move_with(|map, selection| {
@@ -1240,8 +1260,10 @@ impl Vim {
point = map.clip_point(point, Bias::Left);
}
selection.collapse_to(point, selection.goal)
} else if !last_mode.is_visual() && mode.is_visual() && selection.is_empty() {
selection.end = movement::right(map, selection.start);
} else if !last_mode.is_visual() && mode.is_visual() {
if selection.is_empty() {
selection.end = movement::right(map, selection.start);
}
}
});
})

View File

@@ -886,12 +886,8 @@ 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)
&& !workspace.has_active_modal(window, cx)
{
if !focus_handle.contains_focused(window, cx) {
Pane::autosave_item(&item, workspace.project.clone(), window, cx)
.detach_and_log_err(cx);
}

View File

@@ -193,12 +193,6 @@ impl Render for ModalLayer {
background.fade_out(0.2);
this.bg(background)
})
.on_mouse_down(
MouseButton::Left,
cx.listener(|this, _, window, cx| {
this.hide_modal(window, cx);
}),
)
.child(
v_flex()
.h(px(0.0))

View File

@@ -3296,53 +3296,4 @@ mod tests {
assert_eq!(workspace.center_group, new_workspace.center_group);
}
#[gpui::test]
async fn test_empty_workspace_window_bounds() {
zlog::init_test();
let db = WorkspaceDb::open_test_db("test_empty_workspace_window_bounds").await;
let id = db.next_id().await.unwrap();
// Create a workspace with empty paths (empty workspace)
let empty_paths: &[&str] = &[];
let display_uuid = Uuid::new_v4();
let window_bounds = SerializedWindowBounds(WindowBounds::Windowed(Bounds {
origin: point(px(100.0), px(200.0)),
size: size(px(800.0), px(600.0)),
}));
let workspace = SerializedWorkspace {
id,
paths: PathList::new(empty_paths),
location: SerializedWorkspaceLocation::Local,
center_group: Default::default(),
window_bounds: None,
display: None,
docks: Default::default(),
breakpoints: Default::default(),
centered_layout: false,
session_id: None,
window_id: None,
user_toolchains: Default::default(),
};
// Save the workspace (this creates the record with empty paths)
db.save_workspace(workspace.clone()).await;
// Save window bounds separately (as the actual code does via set_window_open_status)
db.set_window_open_status(id, window_bounds, display_uuid)
.await
.unwrap();
// Retrieve it using empty paths
let retrieved = db.workspace_for_roots(empty_paths).unwrap();
// Verify window bounds were persisted
assert_eq!(retrieved.id, id);
assert!(retrieved.window_bounds.is_some());
assert_eq!(retrieved.window_bounds.unwrap().0, window_bounds.0);
assert!(retrieved.display.is_some());
assert_eq!(retrieved.display.unwrap(), display_uuid);
}
}

View File

@@ -1748,18 +1748,26 @@ impl Workspace {
window
} else {
let window_bounds_override = window_bounds_env_override();
let is_empty_workspace = project_paths.is_empty();
let (window_bounds, display) = if let Some(bounds) = window_bounds_override {
(Some(WindowBounds::Windowed(bounds)), None)
} else if let Some(workspace) = serialized_workspace.as_ref()
&& let Some(display) = workspace.display
&& let Some(bounds) = workspace.window_bounds.as_ref()
{
} else if let Some(workspace) = serialized_workspace.as_ref() {
// Reopening an existing workspace - restore its saved bounds
(Some(bounds.0), Some(display))
} else if let Some((display, bounds)) = persistence::read_default_window_bounds() {
// New or empty workspace - use the last known window bounds
(Some(bounds), Some(display))
if let (Some(display), Some(bounds)) =
(workspace.display, workspace.window_bounds.as_ref())
{
(Some(bounds.0), Some(display))
} else {
(None, None)
}
} else if is_empty_workspace {
// Empty workspace - try to restore the last known no-project window bounds
if let Some((display, bounds)) = persistence::read_default_window_bounds() {
(Some(bounds), Some(display))
} else {
(None, None)
}
} else {
// New window - let GPUI's default_bounds() handle cascading
(None, None)
@@ -5665,24 +5673,12 @@ impl Workspace {
persistence::DB.save_workspace(serialized_workspace).await;
})
}
WorkspaceLocation::DetachFromSession => {
let window_bounds = SerializedWindowBounds(window.window_bounds());
let display = window.display(cx).and_then(|d| d.uuid().ok());
window.spawn(cx, async move |_| {
persistence::DB
.set_window_open_status(
database_id,
window_bounds,
display.unwrap_or_default(),
)
.await
.log_err();
persistence::DB
.set_session_id(database_id, None)
.await
.log_err();
})
}
WorkspaceLocation::DetachFromSession => window.spawn(cx, async move |_| {
persistence::DB
.set_session_id(database_id, None)
.await
.log_err();
}),
WorkspaceLocation::None => Task::ready(()),
}
}

Binary file not shown.

View File

@@ -354,8 +354,6 @@ pub mod agent {
ResetAgentZoom,
/// Toggles the utility/agent pane open/closed state.
ToggleAgentPane,
/// Pastes clipboard content without any formatting.
PasteRaw,
]
);
}

View File

@@ -15,10 +15,6 @@ When there is an appropriate language server available, Zed will provide complet
You can manually trigger completions with `ctrl-space` or by triggering the `editor::ShowCompletions` action from the command palette.
> Note: Using `ctrl-space` in Zed requires disabling the macOS global shortcut.
> Open **System Settings** > **Keyboard** > **Keyboard Shortcut**s >
> **Input Sources** and uncheck **Select the previous input source**.
For more information, see:
- [Configuring Supported Languages](./configuring-languages.md)

View File

@@ -106,17 +106,6 @@ 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

View File

@@ -1,81 +0,0 @@
#!/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}"

View File

@@ -45,15 +45,11 @@ pub(crate) fn run_tests() -> Workflow {
&should_run_tests,
]);
let check_style = check_style();
let run_tests_linux = run_platform_tests(Platform::Linux);
let call_autofix = call_autofix(&check_style, &run_tests_linux);
let mut jobs = vec![
orchestrate,
check_style,
check_style(),
should_run_tests.guard(run_platform_tests(Platform::Windows)),
should_run_tests.guard(run_tests_linux),
should_run_tests.guard(run_platform_tests(Platform::Linux)),
should_run_tests.guard(run_platform_tests(Platform::Mac)),
should_run_tests.guard(doctests()),
should_run_tests.guard(check_workspace_binaries()),
@@ -110,7 +106,6 @@ pub(crate) fn run_tests() -> Workflow {
workflow
})
.add_job(tests_pass.name, tests_pass.job)
.add_job(call_autofix.name, call_autofix.job)
}
// Generates a bash script that checks changed files against regex patterns
@@ -226,8 +221,6 @@ pub fn tests_pass(jobs: &[NamedJob]) -> NamedJob {
named::job(job)
}
pub const STYLE_FAILED_OUTPUT: &str = "style_failed";
fn check_style() -> NamedJob {
fn check_for_typos() -> Step<Use> {
named::uses(
@@ -243,58 +236,15 @@ fn check_style() -> NamedJob {
.add_step(steps::checkout_repo())
.add_step(steps::cache_rust_dependencies_namespace())
.add_step(steps::setup_pnpm())
.add_step(steps::prettier())
.add_step(steps::script("./script/prettier"))
.add_step(steps::cargo_fmt())
.add_step(steps::record_style_failure())
.add_step(steps::trigger_autofix(false))
.add_step(steps::script("./script/check-todos"))
.add_step(steps::script("./script/check-keymaps"))
.add_step(check_for_typos())
.outputs([(
STYLE_FAILED_OUTPUT.to_owned(),
format!(
"${{{{ steps.{}.outputs.failed == 'true' }}}}",
steps::RECORD_STYLE_FAILURE_STEP_ID
),
)]),
.add_step(check_for_typos()),
)
}
fn call_autofix(check_style: &NamedJob, run_tests_linux: &NamedJob) -> NamedJob {
fn dispatch_autofix(run_tests_linux_name: &str) -> Step<Run> {
let clippy_failed_expr = format!(
"needs.{}.outputs.{} == 'true'",
run_tests_linux_name, CLIPPY_FAILED_OUTPUT
);
named::bash(format!(
"gh workflow run autofix_pr.yml -f pr_number=${{{{ github.event.pull_request.number }}}} -f run_clippy=${{{{ {} }}}}",
clippy_failed_expr
))
.add_env(("GITHUB_TOKEN", "${{ steps.get-app-token.outputs.token }}"))
}
let style_failed_expr = format!(
"needs.{}.outputs.{} == 'true'",
check_style.name, STYLE_FAILED_OUTPUT
);
let clippy_failed_expr = format!(
"needs.{}.outputs.{} == 'true'",
run_tests_linux.name, CLIPPY_FAILED_OUTPUT
);
let (authenticate, _token) = steps::authenticate_as_zippy();
let job = Job::default()
.runs_on(runners::LINUX_SMALL)
.cond(Expression::new(format!(
"always() && ({} || {}) && github.event_name == 'pull_request' && github.actor != 'zed-zippy[bot]'",
style_failed_expr, clippy_failed_expr
)))
.needs(vec![check_style.name.clone(), run_tests_linux.name.clone()])
.add_step(authenticate)
.add_step(dispatch_autofix(&run_tests_linux.name));
named::job(job)
}
fn check_dependencies() -> NamedJob {
fn install_cargo_machete() -> Step<Use> {
named::uses(
@@ -355,8 +305,6 @@ fn check_workspace_binaries() -> NamedJob {
)
}
pub const CLIPPY_FAILED_OUTPUT: &str = "clippy_failed";
pub(crate) fn run_platform_tests(platform: Platform) -> NamedJob {
let runner = match platform {
Platform::Windows => runners::WINDOWS_DEFAULT,
@@ -379,23 +327,12 @@ pub(crate) fn run_platform_tests(platform: Platform) -> NamedJob {
.add_step(steps::setup_node())
.add_step(steps::clippy(platform))
.when(platform == Platform::Linux, |job| {
job.add_step(steps::record_clippy_failure())
})
.when(platform == Platform::Linux, |job| {
job.add_step(steps::cargo_install_nextest())
job.add_step(steps::trigger_autofix(true))
.add_step(steps::cargo_install_nextest())
})
.add_step(steps::clear_target_dir_if_large(platform))
.add_step(steps::cargo_nextest(platform))
.add_step(steps::cleanup_cargo_config(platform))
.when(platform == Platform::Linux, |job| {
job.outputs([(
CLIPPY_FAILED_OUTPUT.to_owned(),
format!(
"${{{{ steps.{}.outputs.failed == 'true' }}}}",
steps::RECORD_CLIPPY_FAILURE_STEP_ID
),
)])
}),
.add_step(steps::cleanup_cargo_config(platform)),
}
}

View File

@@ -54,25 +54,8 @@ pub fn setup_sentry() -> Step<Use> {
.add_with(("token", vars::SENTRY_AUTH_TOKEN))
}
pub const PRETTIER_STEP_ID: &str = "prettier";
pub const CARGO_FMT_STEP_ID: &str = "cargo_fmt";
pub const RECORD_STYLE_FAILURE_STEP_ID: &str = "record_style_failure";
pub fn prettier() -> Step<Run> {
named::bash("./script/prettier").id(PRETTIER_STEP_ID)
}
pub fn cargo_fmt() -> Step<Run> {
named::bash("cargo fmt --all -- --check").id(CARGO_FMT_STEP_ID)
}
pub fn record_style_failure() -> Step<Run> {
named::bash(format!(
"echo \"failed=${{{{ steps.{}.outcome == 'failure' || steps.{}.outcome == 'failure' }}}}\" >> \"$GITHUB_OUTPUT\"",
PRETTIER_STEP_ID, CARGO_FMT_STEP_ID
))
.id(RECORD_STYLE_FAILURE_STEP_ID)
.if_condition(Expression::new("always()"))
named::bash("cargo fmt --all -- --check")
}
pub fn cargo_install_nextest() -> Step<Use> {
@@ -118,25 +101,13 @@ pub fn clear_target_dir_if_large(platform: Platform) -> Step<Run> {
}
}
pub const CLIPPY_STEP_ID: &str = "clippy";
pub const RECORD_CLIPPY_FAILURE_STEP_ID: &str = "record_clippy_failure";
pub fn clippy(platform: Platform) -> Step<Run> {
match platform {
Platform::Windows => named::pwsh("./script/clippy.ps1").id(CLIPPY_STEP_ID),
_ => named::bash("./script/clippy").id(CLIPPY_STEP_ID),
Platform::Windows => named::pwsh("./script/clippy.ps1"),
_ => named::bash("./script/clippy"),
}
}
pub fn record_clippy_failure() -> Step<Run> {
named::bash(format!(
"echo \"failed=${{{{ steps.{}.outcome == 'failure' }}}}\" >> \"$GITHUB_OUTPUT\"",
CLIPPY_STEP_ID
))
.id(RECORD_CLIPPY_FAILURE_STEP_ID)
.if_condition(Expression::new("always()"))
}
pub fn cache_rust_dependencies_namespace() -> Step<Use> {
named::uses("namespacelabs", "nscloud-cache-action", "v1").add_with(("cache", "rust"))
}
@@ -374,6 +345,16 @@ pub fn git_checkout(ref_name: &dyn std::fmt::Display) -> Step<Run> {
))
}
pub fn trigger_autofix(run_clippy: bool) -> Step<Run> {
named::bash(format!(
"gh workflow run autofix_pr.yml -f pr_number=${{{{ github.event.pull_request.number }}}} -f run_clippy={run_clippy}"
))
.if_condition(Expression::new(
"failure() && github.event_name == 'pull_request' && github.actor != 'zed-zippy[bot]'",
))
.add_env(("GITHUB_TOKEN", vars::GITHUB_TOKEN))
}
pub fn authenticate_as_zippy() -> (Step<Use>, StepOutput) {
let step = named::uses(
"actions",