Merge branch 'main' into mention-more

This commit is contained in:
Agus Zubiaga 2025-08-11 20:48:31 -03:00
commit 26befa1ec6
54 changed files with 1269 additions and 291 deletions

View file

@ -0,0 +1,35 @@
name: Bug Report (Windows)
description: Zed Windows-Related Bugs
type: "Bug"
labels: ["windows"]
title: "Windows: <a short description of the Windows bug>"
body:
- type: textarea
attributes:
label: Summary
description: Describe the bug with a one line summary, and provide detailed reproduction steps
value: |
<!-- Please insert a one line summary of the issue below -->
SUMMARY_SENTENCE_HERE
### Description
<!-- Describe with sufficient detail to reproduce from a clean Zed install. -->
Steps to trigger the problem:
1.
2.
3.
**Expected Behavior**:
**Actual Behavior**:
validations:
required: true
- type: textarea
id: environment
attributes:
label: Zed Version and System Specs
description: 'Open Zed, and in the command palette select "zed: copy system specs into clipboard"'
placeholder: |
Output of "zed: copy system specs into clipboard"
validations:
required: true

View file

@ -20,7 +20,168 @@ runs:
with:
node-version: "18"
- name: Configure crash dumps
shell: powershell
run: |
# Record the start time for this CI run
$runStartTime = Get-Date
$runStartTimeStr = $runStartTime.ToString("yyyy-MM-dd HH:mm:ss")
Write-Host "CI run started at: $runStartTimeStr"
# Save the timestamp for later use
echo "CI_RUN_START_TIME=$($runStartTime.Ticks)" >> $env:GITHUB_ENV
# Create crash dump directory in workspace (non-persistent)
$dumpPath = "$env:GITHUB_WORKSPACE\crash_dumps"
New-Item -ItemType Directory -Force -Path $dumpPath | Out-Null
Write-Host "Setting up crash dump detection..."
Write-Host "Workspace dump path: $dumpPath"
# Note: We're NOT modifying registry on stateful runners
# Instead, we'll check default Windows crash locations after tests
- name: Run tests
shell: powershell
working-directory: ${{ inputs.working-directory }}
run: cargo nextest run --workspace --no-fail-fast
run: |
$env:RUST_BACKTRACE = "full"
# Enable Windows debugging features
$env:_NT_SYMBOL_PATH = "srv*https://msdl.microsoft.com/download/symbols"
# .NET crash dump environment variables (ephemeral)
$env:COMPlus_DbgEnableMiniDump = "1"
$env:COMPlus_DbgMiniDumpType = "4"
$env:COMPlus_CreateDumpDiagnostics = "1"
cargo nextest run --workspace --no-fail-fast
continue-on-error: true
- name: Analyze crash dumps
if: always()
shell: powershell
run: |
Write-Host "Checking for crash dumps..."
# Get the CI run start time from the environment
$runStartTime = [DateTime]::new([long]$env:CI_RUN_START_TIME)
Write-Host "Only analyzing dumps created after: $($runStartTime.ToString('yyyy-MM-dd HH:mm:ss'))"
# Check all possible crash dump locations
$searchPaths = @(
"$env:GITHUB_WORKSPACE\crash_dumps",
"$env:LOCALAPPDATA\CrashDumps",
"$env:TEMP",
"$env:GITHUB_WORKSPACE",
"$env:USERPROFILE\AppData\Local\CrashDumps",
"C:\Windows\System32\config\systemprofile\AppData\Local\CrashDumps"
)
$dumps = @()
foreach ($path in $searchPaths) {
if (Test-Path $path) {
Write-Host "Searching in: $path"
$found = Get-ChildItem "$path\*.dmp" -ErrorAction SilentlyContinue | Where-Object {
$_.CreationTime -gt $runStartTime
}
if ($found) {
$dumps += $found
Write-Host " Found $($found.Count) dump(s) from this CI run"
}
}
}
if ($dumps) {
Write-Host "Found $($dumps.Count) crash dump(s)"
# Install debugging tools if not present
$cdbPath = "C:\Program Files (x86)\Windows Kits\10\Debuggers\x64\cdb.exe"
if (-not (Test-Path $cdbPath)) {
Write-Host "Installing Windows Debugging Tools..."
$url = "https://go.microsoft.com/fwlink/?linkid=2237387"
Invoke-WebRequest -Uri $url -OutFile winsdksetup.exe
Start-Process -Wait winsdksetup.exe -ArgumentList "/features OptionId.WindowsDesktopDebuggers /quiet"
}
foreach ($dump in $dumps) {
Write-Host "`n=================================="
Write-Host "Analyzing crash dump: $($dump.Name)"
Write-Host "Size: $([math]::Round($dump.Length / 1MB, 2)) MB"
Write-Host "Time: $($dump.CreationTime)"
Write-Host "=================================="
# Set symbol path
$env:_NT_SYMBOL_PATH = "srv*C:\symbols*https://msdl.microsoft.com/download/symbols"
# Run analysis
$analysisOutput = & $cdbPath -z $dump.FullName -c "!analyze -v; ~*k; lm; q" 2>&1 | Out-String
# Extract key information
if ($analysisOutput -match "ExceptionCode:\s*([\w]+)") {
Write-Host "Exception Code: $($Matches[1])"
if ($Matches[1] -eq "c0000005") {
Write-Host "Exception Type: ACCESS VIOLATION"
}
}
if ($analysisOutput -match "EXCEPTION_RECORD:\s*(.+)") {
Write-Host "Exception Record: $($Matches[1])"
}
if ($analysisOutput -match "FAULTING_IP:\s*\n(.+)") {
Write-Host "Faulting Instruction: $($Matches[1])"
}
# Save full analysis
$analysisFile = "$($dump.FullName).analysis.txt"
$analysisOutput | Out-File -FilePath $analysisFile
Write-Host "`nFull analysis saved to: $analysisFile"
# Print stack trace section
Write-Host "`n--- Stack Trace Preview ---"
$stackSection = $analysisOutput -split "STACK_TEXT:" | Select-Object -Last 1
$stackLines = $stackSection -split "`n" | Select-Object -First 20
$stackLines | ForEach-Object { Write-Host $_ }
Write-Host "--- End Stack Trace Preview ---"
}
Write-Host "`n⚠ Crash dumps detected! Download the 'crash-dumps' artifact for detailed analysis."
# Copy dumps to workspace for artifact upload
$artifactPath = "$env:GITHUB_WORKSPACE\crash_dumps_collected"
New-Item -ItemType Directory -Force -Path $artifactPath | Out-Null
foreach ($dump in $dumps) {
$destName = "$($dump.Directory.Name)_$($dump.Name)"
Copy-Item $dump.FullName -Destination "$artifactPath\$destName"
if (Test-Path "$($dump.FullName).analysis.txt") {
Copy-Item "$($dump.FullName).analysis.txt" -Destination "$artifactPath\$destName.analysis.txt"
}
}
Write-Host "Copied $($dumps.Count) dump(s) to artifact directory"
} else {
Write-Host "No crash dumps from this CI run found"
}
- name: Upload crash dumps
if: always()
uses: actions/upload-artifact@v4
with:
name: crash-dumps-${{ github.run_id }}-${{ github.run_attempt }}
path: |
crash_dumps_collected/*.dmp
crash_dumps_collected/*.txt
if-no-files-found: ignore
retention-days: 7
- name: Check test results
shell: powershell
working-directory: ${{ inputs.working-directory }}
run: |
# Re-check test results to fail the job if tests failed
if ($LASTEXITCODE -ne 0) {
Write-Host "Tests failed with exit code: $LASTEXITCODE"
exit $LASTEXITCODE
}

2
Cargo.lock generated
View file

@ -205,6 +205,8 @@ dependencies = [
"gpui",
"gpui_tokio",
"handlebars 4.5.0",
"html_to_markdown",
"http_client",
"indoc",
"itertools 0.14.0",
"language",

View file

@ -174,6 +174,10 @@ impl Diff {
buffer_text
)
}
pub fn has_revealed_range(&self, cx: &App) -> bool {
self.multibuffer().read(cx).excerpt_paths().next().is_some()
}
}
pub struct PendingDiff {

View file

@ -29,8 +29,14 @@ impl Terminal {
cx: &mut Context<Self>,
) -> Self {
Self {
command: cx
.new(|cx| Markdown::new(command.into(), Some(language_registry.clone()), None, cx)),
command: cx.new(|cx| {
Markdown::new(
format!("```\n{}\n```", command).into(),
Some(language_registry.clone()),
None,
cx,
)
}),
working_dir,
terminal,
started_at: Instant::now(),

View file

@ -17,8 +17,6 @@ use util::{
pub struct ActionLog {
/// Buffers that we want to notify the model about when they change.
tracked_buffers: BTreeMap<Entity<Buffer>, TrackedBuffer>,
/// Has the model edited a file since it last checked diagnostics?
edited_since_project_diagnostics_check: bool,
/// The project this action log is associated with
project: Entity<Project>,
}
@ -28,7 +26,6 @@ impl ActionLog {
pub fn new(project: Entity<Project>) -> Self {
Self {
tracked_buffers: BTreeMap::default(),
edited_since_project_diagnostics_check: false,
project,
}
}
@ -37,16 +34,6 @@ impl ActionLog {
&self.project
}
/// Notifies a diagnostics check
pub fn checked_project_diagnostics(&mut self) {
self.edited_since_project_diagnostics_check = false;
}
/// Returns true if any files have been edited since the last project diagnostics check
pub fn has_edited_files_since_project_diagnostics_check(&self) -> bool {
self.edited_since_project_diagnostics_check
}
pub fn latest_snapshot(&self, buffer: &Entity<Buffer>) -> Option<text::BufferSnapshot> {
Some(self.tracked_buffers.get(buffer)?.snapshot.clone())
}
@ -543,14 +530,11 @@ impl ActionLog {
/// Mark a buffer as created by agent, so we can refresh it in the context
pub fn buffer_created(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
self.edited_since_project_diagnostics_check = true;
self.track_buffer_internal(buffer.clone(), true, cx);
}
/// Mark a buffer as edited by agent, so we can refresh it in the context
pub fn buffer_edited(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
self.edited_since_project_diagnostics_check = true;
let tracked_buffer = self.track_buffer_internal(buffer.clone(), false, cx);
if let TrackedBufferStatus::Deleted = tracked_buffer.status {
tracked_buffer.status = TrackedBufferStatus::Modified;

View file

@ -27,6 +27,8 @@ fs.workspace = true
futures.workspace = true
gpui.workspace = true
handlebars = { workspace = true, features = ["rust-embed"] }
html_to_markdown.workspace = true
http_client.workspace = true
indoc.workspace = true
itertools.workspace = true
language.workspace = true

View file

@ -1,8 +1,8 @@
use crate::{AgentResponseEvent, Thread, templates::Templates};
use crate::{
CopyPathTool, CreateDirectoryTool, EditFileTool, FindPathTool, GrepTool, ListDirectoryTool,
MessageContent, MovePathTool, NowTool, OpenTool, ReadFileTool, TerminalTool, ThinkingTool,
ToolCallAuthorization, WebSearchTool,
CopyPathTool, CreateDirectoryTool, DiagnosticsTool, EditFileTool, FetchTool, FindPathTool,
GrepTool, ListDirectoryTool, MessageContent, MovePathTool, NowTool, OpenTool, ReadFileTool,
TerminalTool, ThinkingTool, ToolCallAuthorization, WebSearchTool,
};
use acp_thread::ModelSelector;
use agent_client_protocol as acp;
@ -420,11 +420,13 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
let mut thread = Thread::new(project.clone(), agent.project_context.clone(), action_log.clone(), agent.templates.clone(), default_model);
thread.add_tool(CreateDirectoryTool::new(project.clone()));
thread.add_tool(CopyPathTool::new(project.clone()));
thread.add_tool(DiagnosticsTool::new(project.clone()));
thread.add_tool(MovePathTool::new(project.clone()));
thread.add_tool(ListDirectoryTool::new(project.clone()));
thread.add_tool(OpenTool::new(project.clone()));
thread.add_tool(ThinkingTool);
thread.add_tool(FindPathTool::new(project.clone()));
thread.add_tool(FetchTool::new(project.read(cx).client().http_client()));
thread.add_tool(GrepTool::new(project.clone()));
thread.add_tool(ReadFileTool::new(project.clone(), action_log));
thread.add_tool(EditFileTool::new(cx.entity()));

View file

@ -5,9 +5,11 @@ use action_log::ActionLog;
use agent_client_protocol::{self as acp};
use anyhow::Result;
use client::{Client, UserStore};
use fs::FakeFs;
use fs::{FakeFs, Fs};
use futures::channel::mpsc::UnboundedReceiver;
use gpui::{AppContext, Entity, Task, TestAppContext, http_client::FakeHttpClient};
use gpui::{
App, AppContext, Entity, Task, TestAppContext, UpdateGlobal, http_client::FakeHttpClient,
};
use indoc::indoc;
use language_model::{
LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelId,
@ -20,6 +22,7 @@ use reqwest_client::ReqwestClient;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_json::json;
use settings::SettingsStore;
use smol::stream::StreamExt;
use std::{cell::RefCell, path::Path, rc::Rc, sync::Arc, time::Duration};
use util::path;
@ -283,6 +286,63 @@ async fn test_tool_authorization(cx: &mut TestAppContext) {
})
]
);
// Simulate yet another tool call.
fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
LanguageModelToolUse {
id: "tool_id_3".into(),
name: ToolRequiringPermission.name().into(),
raw_input: "{}".into(),
input: json!({}),
is_input_complete: true,
},
));
fake_model.end_last_completion_stream();
// Respond by always allowing tools.
let tool_call_auth_3 = next_tool_call_authorization(&mut events).await;
tool_call_auth_3
.response
.send(tool_call_auth_3.options[0].id.clone())
.unwrap();
cx.run_until_parked();
let completion = fake_model.pending_completions().pop().unwrap();
let message = completion.messages.last().unwrap();
assert_eq!(
message.content,
vec![MessageContent::ToolResult(LanguageModelToolResult {
tool_use_id: tool_call_auth_3.tool_call.id.0.to_string().into(),
tool_name: ToolRequiringPermission.name().into(),
is_error: false,
content: "Allowed".into(),
output: Some("Allowed".into())
})]
);
// Simulate a final tool call, ensuring we don't trigger authorization.
fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
LanguageModelToolUse {
id: "tool_id_4".into(),
name: ToolRequiringPermission.name().into(),
raw_input: "{}".into(),
input: json!({}),
is_input_complete: true,
},
));
fake_model.end_last_completion_stream();
cx.run_until_parked();
let completion = fake_model.pending_completions().pop().unwrap();
let message = completion.messages.last().unwrap();
assert_eq!(
message.content,
vec![MessageContent::ToolResult(LanguageModelToolResult {
tool_use_id: "tool_id_4".into(),
tool_name: ToolRequiringPermission.name().into(),
is_error: false,
content: "Allowed".into(),
output: Some("Allowed".into())
})]
);
}
#[gpui::test]
@ -774,13 +834,17 @@ impl TestModel {
async fn setup(cx: &mut TestAppContext, model: TestModel) -> ThreadTest {
cx.executor().allow_parking();
let fs = FakeFs::new(cx.background_executor.clone());
cx.update(|cx| {
settings::init(cx);
watch_settings(fs.clone(), cx);
Project::init_settings(cx);
agent_settings::init(cx);
});
let templates = Templates::new();
let fs = FakeFs::new(cx.background_executor.clone());
fs.insert_tree(path!("/test"), json!({})).await;
let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
@ -842,3 +906,26 @@ fn init_logger() {
env_logger::init();
}
}
fn watch_settings(fs: Arc<dyn Fs>, cx: &mut App) {
let fs = fs.clone();
cx.spawn({
async move |cx| {
let mut new_settings_content_rx = settings::watch_config_file(
cx.background_executor(),
fs,
paths::settings_file().clone(),
);
while let Some(new_settings_content) = new_settings_content_rx.next().await {
cx.update(|cx| {
SettingsStore::update_global(cx, |settings, cx| {
settings.set_user_settings(&new_settings_content, cx)
})
})
.ok();
}
}
})
.detach();
}

View file

@ -110,9 +110,9 @@ impl AgentTool for ToolRequiringPermission {
event_stream: ToolCallEventStream,
cx: &mut App,
) -> Task<Result<String>> {
let auth_check = event_stream.authorize("Authorize?".into());
let authorize = event_stream.authorize("Authorize?", cx);
cx.foreground_executor().spawn(async move {
auth_check.await?;
authorize.await?;
Ok("Allowed".to_string())
})
}

View file

@ -2,10 +2,12 @@ use crate::{SystemPromptTemplate, Template, Templates};
use acp_thread::MentionUri;
use action_log::ActionLog;
use agent_client_protocol as acp;
use agent_settings::AgentSettings;
use anyhow::{Context as _, Result, anyhow};
use assistant_tool::adapt_schema_to_format;
use cloud_llm_client::{CompletionIntent, CompletionMode};
use collections::HashMap;
use fs::Fs;
use futures::{
channel::{mpsc, oneshot},
stream::FuturesUnordered,
@ -22,9 +24,10 @@ use project::Project;
use prompt_store::ProjectContext;
use schemars::{JsonSchema, Schema};
use serde::{Deserialize, Serialize};
use settings::{Settings, update_settings_file};
use smol::stream::StreamExt;
use std::fmt::Write;
use std::{cell::RefCell, collections::BTreeMap, future::Future, path::Path, rc::Rc, sync::Arc};
use std::{cell::RefCell, collections::BTreeMap, path::Path, rc::Rc, sync::Arc};
use util::{ResultExt, markdown::MarkdownCodeBlock};
#[derive(Debug, Clone)]
@ -529,8 +532,9 @@ impl Thread {
}));
};
let fs = self.project.read(cx).fs().clone();
let tool_event_stream =
ToolCallEventStream::new(&tool_use, tool.kind(), event_stream.clone());
ToolCallEventStream::new(&tool_use, tool.kind(), event_stream.clone(), Some(fs));
tool_event_stream.update_fields(acp::ToolCallUpdateFields {
status: Some(acp::ToolCallStatus::InProgress),
..Default::default()
@ -917,6 +921,7 @@ pub struct ToolCallEventStream {
kind: acp::ToolKind,
input: serde_json::Value,
stream: AgentResponseEventStream,
fs: Option<Arc<dyn Fs>>,
}
impl ToolCallEventStream {
@ -935,6 +940,7 @@ impl ToolCallEventStream {
},
acp::ToolKind::Other,
AgentResponseEventStream(events_tx),
None,
);
(stream, ToolCallEventStreamReceiver(events_rx))
@ -944,12 +950,14 @@ impl ToolCallEventStream {
tool_use: &LanguageModelToolUse,
kind: acp::ToolKind,
stream: AgentResponseEventStream,
fs: Option<Arc<dyn Fs>>,
) -> Self {
Self {
tool_use_id: tool_use.id.clone(),
kind,
input: tool_use.input.clone(),
stream,
fs,
}
}
@ -984,7 +992,11 @@ impl ToolCallEventStream {
.ok();
}
pub fn authorize(&self, title: String) -> impl use<> + Future<Output = Result<()>> {
pub fn authorize(&self, title: impl Into<String>, cx: &mut App) -> Task<Result<()>> {
if agent_settings::AgentSettings::get_global(cx).always_allow_tool_actions {
return Task::ready(Ok(()));
}
let (response_tx, response_rx) = oneshot::channel();
self.stream
.0
@ -992,7 +1004,7 @@ impl ToolCallEventStream {
ToolCallAuthorization {
tool_call: AgentResponseEventStream::initial_tool_call(
&self.tool_use_id,
title,
title.into(),
self.kind.clone(),
self.input.clone(),
),
@ -1017,12 +1029,22 @@ impl ToolCallEventStream {
},
)))
.ok();
async move {
match response_rx.await?.0.as_ref() {
"allow" | "always_allow" => Ok(()),
_ => Err(anyhow!("Permission to run tool denied by user")),
let fs = self.fs.clone();
cx.spawn(async move |cx| match response_rx.await?.0.as_ref() {
"always_allow" => {
if let Some(fs) = fs.clone() {
cx.update(|cx| {
update_settings_file::<AgentSettings>(fs, cx, |settings, _| {
settings.set_always_allow_tool_actions(true);
});
})?;
}
Ok(())
}
}
"allow" => Ok(()),
_ => Err(anyhow!("Permission to run tool denied by user")),
})
}
}

View file

@ -1,7 +1,9 @@
mod copy_path_tool;
mod create_directory_tool;
mod delete_path_tool;
mod diagnostics_tool;
mod edit_file_tool;
mod fetch_tool;
mod find_path_tool;
mod grep_tool;
mod list_directory_tool;
@ -16,7 +18,9 @@ mod web_search_tool;
pub use copy_path_tool::*;
pub use create_directory_tool::*;
pub use delete_path_tool::*;
pub use diagnostics_tool::*;
pub use edit_file_tool::*;
pub use fetch_tool::*;
pub use find_path_tool::*;
pub use grep_tool::*;
pub use list_directory_tool::*;

View file

@ -0,0 +1,177 @@
use crate::{AgentTool, ToolCallEventStream};
use agent_client_protocol as acp;
use anyhow::{Result, anyhow};
use gpui::{App, Entity, Task};
use language::{DiagnosticSeverity, OffsetRangeExt};
use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::{fmt::Write, path::Path, sync::Arc};
use ui::SharedString;
use util::markdown::MarkdownInlineCode;
/// Get errors and warnings for the project or a specific file.
///
/// This tool can be invoked after a series of edits to determine if further edits are necessary, or if the user asks to fix errors or warnings in their codebase.
///
/// When a path is provided, shows all diagnostics for that specific file.
/// When no path is provided, shows a summary of error and warning counts for all files in the project.
///
/// <example>
/// To get diagnostics for a specific file:
/// {
/// "path": "src/main.rs"
/// }
///
/// To get a project-wide diagnostic summary:
/// {}
/// </example>
///
/// <guidelines>
/// - If you think you can fix a diagnostic, make 1-2 attempts and then give up.
/// - Don't remove code you've generated just because you can't fix an error. The user can help you fix it.
/// </guidelines>
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct DiagnosticsToolInput {
/// The path to get diagnostics for. If not provided, returns a project-wide summary.
///
/// This path should never be absolute, and the first component
/// of the path should always be a root directory in a project.
///
/// <example>
/// If the project has the following root directories:
///
/// - lorem
/// - ipsum
///
/// If you wanna access diagnostics for `dolor.txt` in `ipsum`, you should use the path `ipsum/dolor.txt`.
/// </example>
pub path: Option<String>,
}
pub struct DiagnosticsTool {
project: Entity<Project>,
}
impl DiagnosticsTool {
pub fn new(project: Entity<Project>) -> Self {
Self { project }
}
}
impl AgentTool for DiagnosticsTool {
type Input = DiagnosticsToolInput;
type Output = String;
fn name(&self) -> SharedString {
"diagnostics".into()
}
fn kind(&self) -> acp::ToolKind {
acp::ToolKind::Read
}
fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
if let Some(path) = input.ok().and_then(|input| match input.path {
Some(path) if !path.is_empty() => Some(path),
_ => None,
}) {
format!("Check diagnostics for {}", MarkdownInlineCode(&path)).into()
} else {
"Check project diagnostics".into()
}
}
fn run(
self: Arc<Self>,
input: Self::Input,
event_stream: ToolCallEventStream,
cx: &mut App,
) -> Task<Result<Self::Output>> {
match input.path {
Some(path) if !path.is_empty() => {
let Some(project_path) = self.project.read(cx).find_project_path(&path, cx) else {
return Task::ready(Err(anyhow!("Could not find path {path} in project",)));
};
let buffer = self
.project
.update(cx, |project, cx| project.open_buffer(project_path, cx));
cx.spawn(async move |cx| {
let mut output = String::new();
let buffer = buffer.await?;
let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
for (_, group) in snapshot.diagnostic_groups(None) {
let entry = &group.entries[group.primary_ix];
let range = entry.range.to_point(&snapshot);
let severity = match entry.diagnostic.severity {
DiagnosticSeverity::ERROR => "error",
DiagnosticSeverity::WARNING => "warning",
_ => continue,
};
writeln!(
output,
"{} at line {}: {}",
severity,
range.start.row + 1,
entry.diagnostic.message
)?;
event_stream.update_fields(acp::ToolCallUpdateFields {
content: Some(vec![output.clone().into()]),
..Default::default()
});
}
if output.is_empty() {
Ok("File doesn't have errors or warnings!".to_string())
} else {
Ok(output)
}
})
}
_ => {
let project = self.project.read(cx);
let mut output = String::new();
let mut has_diagnostics = false;
for (project_path, _, summary) in project.diagnostic_summaries(true, cx) {
if summary.error_count > 0 || summary.warning_count > 0 {
let Some(worktree) = project.worktree_for_id(project_path.worktree_id, cx)
else {
continue;
};
has_diagnostics = true;
output.push_str(&format!(
"{}: {} error(s), {} warning(s)\n",
Path::new(worktree.read(cx).root_name())
.join(project_path.path)
.display(),
summary.error_count,
summary.warning_count
));
}
}
if has_diagnostics {
event_stream.update_fields(acp::ToolCallUpdateFields {
content: Some(vec![output.clone().into()]),
..Default::default()
});
Task::ready(Ok(output))
} else {
let text = "No errors or warnings found in the project.";
event_stream.update_fields(acp::ToolCallUpdateFields {
content: Some(vec![text.into()]),
..Default::default()
});
Task::ready(Ok(text.into()))
}
}
}
}
}

View file

@ -133,7 +133,7 @@ impl EditFileTool {
&self,
input: &EditFileToolInput,
event_stream: &ToolCallEventStream,
cx: &App,
cx: &mut App,
) -> Task<Result<()>> {
if agent_settings::AgentSettings::get_global(cx).always_allow_tool_actions {
return Task::ready(Ok(()));
@ -147,8 +147,9 @@ impl EditFileTool {
.components()
.any(|component| component.as_os_str() == local_settings_folder.as_os_str())
{
return cx.foreground_executor().spawn(
event_stream.authorize(format!("{} (local settings)", input.display_description)),
return event_stream.authorize(
format!("{} (local settings)", input.display_description),
cx,
);
}
@ -156,9 +157,9 @@ impl EditFileTool {
// so check for that edge case too.
if let Ok(canonical_path) = std::fs::canonicalize(&input.path) {
if canonical_path.starts_with(paths::config_dir()) {
return cx.foreground_executor().spawn(
event_stream
.authorize(format!("{} (global settings)", input.display_description)),
return event_stream.authorize(
format!("{} (global settings)", input.display_description),
cx,
);
}
}
@ -173,8 +174,7 @@ impl EditFileTool {
if project_path.is_some() {
Task::ready(Ok(()))
} else {
cx.foreground_executor()
.spawn(event_stream.authorize(input.display_description.clone()))
event_stream.authorize(&input.display_description, cx)
}
}
}

View file

@ -0,0 +1,161 @@
use std::rc::Rc;
use std::sync::Arc;
use std::{borrow::Cow, cell::RefCell};
use agent_client_protocol as acp;
use anyhow::{Context as _, Result, bail};
use futures::AsyncReadExt as _;
use gpui::{App, AppContext as _, Task};
use html_to_markdown::{TagHandler, convert_html_to_markdown, markdown};
use http_client::{AsyncBody, HttpClientWithUrl};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use ui::SharedString;
use util::markdown::MarkdownEscaped;
use crate::{AgentTool, ToolCallEventStream};
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
enum ContentType {
Html,
Plaintext,
Json,
}
/// Fetches a URL and returns the content as Markdown.
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct FetchToolInput {
/// The URL to fetch.
url: String,
}
pub struct FetchTool {
http_client: Arc<HttpClientWithUrl>,
}
impl FetchTool {
pub fn new(http_client: Arc<HttpClientWithUrl>) -> Self {
Self { http_client }
}
async fn build_message(http_client: Arc<HttpClientWithUrl>, url: &str) -> Result<String> {
let url = if !url.starts_with("https://") && !url.starts_with("http://") {
Cow::Owned(format!("https://{url}"))
} else {
Cow::Borrowed(url)
};
let mut response = http_client.get(&url, AsyncBody::default(), true).await?;
let mut body = Vec::new();
response
.body_mut()
.read_to_end(&mut body)
.await
.context("error reading response body")?;
if response.status().is_client_error() {
let text = String::from_utf8_lossy(body.as_slice());
bail!(
"status error {}, response: {text:?}",
response.status().as_u16()
);
}
let Some(content_type) = response.headers().get("content-type") else {
bail!("missing Content-Type header");
};
let content_type = content_type
.to_str()
.context("invalid Content-Type header")?;
let content_type = if content_type.starts_with("text/plain") {
ContentType::Plaintext
} else if content_type.starts_with("application/json") {
ContentType::Json
} else {
ContentType::Html
};
match content_type {
ContentType::Html => {
let mut handlers: Vec<TagHandler> = vec![
Rc::new(RefCell::new(markdown::WebpageChromeRemover)),
Rc::new(RefCell::new(markdown::ParagraphHandler)),
Rc::new(RefCell::new(markdown::HeadingHandler)),
Rc::new(RefCell::new(markdown::ListHandler)),
Rc::new(RefCell::new(markdown::TableHandler::new())),
Rc::new(RefCell::new(markdown::StyledTextHandler)),
];
if url.contains("wikipedia.org") {
use html_to_markdown::structure::wikipedia;
handlers.push(Rc::new(RefCell::new(wikipedia::WikipediaChromeRemover)));
handlers.push(Rc::new(RefCell::new(wikipedia::WikipediaInfoboxHandler)));
handlers.push(Rc::new(
RefCell::new(wikipedia::WikipediaCodeHandler::new()),
));
} else {
handlers.push(Rc::new(RefCell::new(markdown::CodeHandler)));
}
convert_html_to_markdown(&body[..], &mut handlers)
}
ContentType::Plaintext => Ok(std::str::from_utf8(&body)?.to_owned()),
ContentType::Json => {
let json: serde_json::Value = serde_json::from_slice(&body)?;
Ok(format!(
"```json\n{}\n```",
serde_json::to_string_pretty(&json)?
))
}
}
}
}
impl AgentTool for FetchTool {
type Input = FetchToolInput;
type Output = String;
fn name(&self) -> SharedString {
"fetch".into()
}
fn kind(&self) -> acp::ToolKind {
acp::ToolKind::Fetch
}
fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
match input {
Ok(input) => format!("Fetch {}", MarkdownEscaped(&input.url)).into(),
Err(_) => "Fetch URL".into(),
}
}
fn run(
self: Arc<Self>,
input: Self::Input,
event_stream: ToolCallEventStream,
cx: &mut App,
) -> Task<Result<Self::Output>> {
let text = cx.background_spawn({
let http_client = self.http_client.clone();
async move { Self::build_message(http_client, &input.url).await }
});
cx.foreground_executor().spawn(async move {
let text = text.await?;
if text.trim().is_empty() {
bail!("no textual content found");
}
event_stream.update_fields(acp::ToolCallUpdateFields {
content: Some(vec![text.clone().into()]),
..Default::default()
});
Ok(text)
})
}
}

View file

@ -65,7 +65,7 @@ impl AgentTool for OpenTool {
) -> Task<Result<Self::Output>> {
// If path_or_url turns out to be a path in the project, make it absolute.
let abs_path = to_absolute_path(&input.path_or_url, self.project.clone(), cx);
let authorize = event_stream.authorize(self.initial_title(Ok(input.clone())).to_string());
let authorize = event_stream.authorize(self.initial_title(Ok(input.clone())), cx);
cx.background_spawn(async move {
authorize.await?;

View file

@ -5,7 +5,6 @@ use gpui::{App, AppContext, Entity, SharedString, Task};
use project::{Project, terminals::TerminalKind};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::Settings;
use std::{
path::{Path, PathBuf},
sync::Arc,
@ -61,21 +60,6 @@ impl TerminalTool {
determine_shell: determine_shell.shared(),
}
}
fn authorize(
&self,
input: &TerminalToolInput,
event_stream: &ToolCallEventStream,
cx: &App,
) -> Task<Result<()>> {
if agent_settings::AgentSettings::get_global(cx).always_allow_tool_actions {
return Task::ready(Ok(()));
}
// TODO: do we want to have a special title here?
cx.foreground_executor()
.spawn(event_stream.authorize(self.initial_title(Ok(input.clone())).to_string()))
}
}
impl AgentTool for TerminalTool {
@ -152,7 +136,7 @@ impl AgentTool for TerminalTool {
env
});
let authorize = self.authorize(&input, &event_stream, cx);
let authorize = event_stream.authorize(self.initial_title(Ok(input.clone())), cx);
cx.spawn({
async move |cx| {

View file

@ -39,7 +39,7 @@ use theme::ThemeSettings;
use ui::{
Disclosure, Divider, DividerColor, KeyBinding, Scrollbar, ScrollbarState, Tooltip, prelude::*,
};
use util::ResultExt;
use util::{ResultExt, size::format_file_size, time::duration_alt_display};
use workspace::{CollaboratorId, Workspace};
use zed_actions::agent::{Chat, NextHistoryMessage, PreviousHistoryMessage};
@ -76,6 +76,7 @@ pub struct AcpThreadView {
edits_expanded: bool,
plan_expanded: bool,
editor_expanded: bool,
terminal_expanded: bool,
message_history: Rc<RefCell<MessageHistory<Vec<acp::ContentBlock>>>>,
_cancel_task: Option<Task<()>>,
_subscriptions: [Subscription; 1],
@ -201,6 +202,7 @@ impl AcpThreadView {
edits_expanded: false,
plan_expanded: false,
editor_expanded: false,
terminal_expanded: true,
message_history,
_subscriptions: [subscription],
_cancel_task: None,
@ -796,7 +798,7 @@ impl AcpThreadView {
window,
cx,
);
view.set_embedded_mode(None, cx);
view.set_embedded_mode(Some(1000), cx);
view
});
@ -942,17 +944,26 @@ impl AcpThreadView {
.child(message_body)
.into_any()
}
AgentThreadEntry::ToolCall(tool_call) => div()
.w_full()
.py_1p5()
.px_5()
.child(self.render_tool_call(index, tool_call, window, cx))
.into_any(),
AgentThreadEntry::ToolCall(tool_call) => {
let has_terminals = tool_call.terminals().next().is_some();
div().w_full().py_1p5().px_5().map(|this| {
if has_terminals {
this.children(tool_call.terminals().map(|terminal| {
self.render_terminal_tool_call(terminal, tool_call, window, cx)
}))
} else {
this.child(self.render_tool_call(index, tool_call, window, cx))
}
})
}
.into_any(),
};
let Some(thread) = self.thread() else {
return primary;
};
let is_generating = matches!(thread.read(cx).status(), ThreadStatus::Generating);
if index == total_entries - 1 && !is_generating {
v_flex()
@ -1181,19 +1192,27 @@ impl AcpThreadView {
),
};
let needs_confirmation = match &tool_call.status {
ToolCallStatus::WaitingForConfirmation { .. } => true,
_ => tool_call
.content
.iter()
.any(|content| matches!(content, ToolCallContent::Diff(_))),
};
let needs_confirmation = matches!(
tool_call.status,
ToolCallStatus::WaitingForConfirmation { .. }
);
let is_edit = matches!(tool_call.kind, acp::ToolKind::Edit);
let has_diff = tool_call
.content
.iter()
.any(|content| matches!(content, ToolCallContent::Diff { .. }));
let has_nonempty_diff = tool_call.content.iter().any(|content| match content {
ToolCallContent::Diff(diff) => diff.read(cx).has_revealed_range(cx),
_ => false,
});
let is_collapsible =
!tool_call.content.is_empty() && !needs_confirmation && !is_edit && !has_diff;
let is_open = tool_call.content.is_empty()
|| needs_confirmation
|| has_nonempty_diff
|| self.expanded_tool_calls.contains(&tool_call.id);
let is_collapsible = !tool_call.content.is_empty() && !needs_confirmation;
let is_open = !is_collapsible || self.expanded_tool_calls.contains(&tool_call.id);
let gradient_color = cx.theme().colors().panel_background;
let gradient_overlay = {
let gradient_overlay = |color: Hsla| {
div()
.absolute()
.top_0()
@ -1202,13 +1221,13 @@ impl AcpThreadView {
.h_full()
.bg(linear_gradient(
90.,
linear_color_stop(gradient_color, 1.),
linear_color_stop(gradient_color.opacity(0.2), 0.),
linear_color_stop(color, 1.),
linear_color_stop(color.opacity(0.2), 0.),
))
};
v_flex()
.when(needs_confirmation, |this| {
.when(needs_confirmation || is_edit || has_diff, |this| {
this.rounded_lg()
.border_1()
.border_color(self.tool_card_border_color(cx))
@ -1222,7 +1241,7 @@ impl AcpThreadView {
.gap_1()
.justify_between()
.map(|this| {
if needs_confirmation {
if needs_confirmation || is_edit || has_diff {
this.pl_2()
.pr_1()
.py_1()
@ -1299,13 +1318,23 @@ impl AcpThreadView {
.child(self.render_markdown(
tool_call.label.clone(),
default_markdown_style(
needs_confirmation,
needs_confirmation || is_edit || has_diff,
window,
cx,
),
)),
)
.child(gradient_overlay)
.map(|this| {
if needs_confirmation {
this.child(gradient_overlay(
self.tool_card_header_bg(cx),
))
} else {
this.child(gradient_overlay(
cx.theme().colors().panel_background,
))
}
})
.on_click(cx.listener({
let id = tool_call.id.clone();
move |this: &mut Self, _, _, cx: &mut Context<Self>| {
@ -1340,11 +1369,9 @@ impl AcpThreadView {
.children(tool_call.content.iter().map(|content| {
div()
.py_1p5()
.child(
self.render_tool_call_content(
content, window, cx,
),
)
.child(self.render_tool_call_content(
content, tool_call, window, cx,
))
.into_any_element()
}))
.child(self.render_permission_buttons(
@ -1358,11 +1385,9 @@ impl AcpThreadView {
this.children(tool_call.content.iter().map(|content| {
div()
.py_1p5()
.child(
self.render_tool_call_content(
content, window, cx,
),
)
.child(self.render_tool_call_content(
content, tool_call, window, cx,
))
.into_any_element()
}))
}
@ -1379,6 +1404,7 @@ impl AcpThreadView {
fn render_tool_call_content(
&self,
content: &ToolCallContent,
tool_call: &ToolCall,
window: &Window,
cx: &Context<Self>,
) -> AnyElement {
@ -1399,7 +1425,9 @@ impl AcpThreadView {
}
}
ToolCallContent::Diff(diff) => self.render_diff_editor(&diff.read(cx).multibuffer()),
ToolCallContent::Terminal(terminal) => self.render_terminal(terminal),
ToolCallContent::Terminal(terminal) => {
self.render_terminal_tool_call(terminal, tool_call, window, cx)
}
}
}
@ -1412,14 +1440,22 @@ impl AcpThreadView {
cx: &Context<Self>,
) -> Div {
h_flex()
.p_1p5()
.py_1()
.pl_2()
.pr_1()
.gap_1()
.justify_end()
.justify_between()
.flex_wrap()
.when(!empty_content, |this| {
this.border_t_1()
.border_color(self.tool_card_border_color(cx))
})
.children(options.iter().map(|option| {
.child(
div()
.min_w(rems_from_px(145.))
.child(LoadingLabel::new("Waiting for Confirmation").size(LabelSize::Small)),
)
.child(h_flex().gap_0p5().children(options.iter().map(|option| {
let option_id = SharedString::from(option.id.0.clone());
Button::new((option_id, entry_ix), option.name.clone())
.map(|this| match option.kind {
@ -1452,7 +1488,7 @@ impl AcpThreadView {
);
}
}))
}))
})))
}
fn render_diff_editor(&self, multibuffer: &Entity<MultiBuffer>) -> AnyElement {
@ -1468,18 +1504,242 @@ impl AcpThreadView {
.into_any()
}
fn render_terminal(&self, terminal: &Entity<acp_thread::Terminal>) -> AnyElement {
v_flex()
.h_72()
fn render_terminal_tool_call(
&self,
terminal: &Entity<acp_thread::Terminal>,
tool_call: &ToolCall,
window: &Window,
cx: &Context<Self>,
) -> AnyElement {
let terminal_data = terminal.read(cx);
let working_dir = terminal_data.working_dir();
let command = terminal_data.command();
let started_at = terminal_data.started_at();
let tool_failed = matches!(
&tool_call.status,
ToolCallStatus::Rejected
| ToolCallStatus::Canceled
| ToolCallStatus::Allowed {
status: acp::ToolCallStatus::Failed,
..
}
);
let output = terminal_data.output();
let command_finished = output.is_some();
let truncated_output = output.is_some_and(|output| output.was_content_truncated);
let output_line_count = output.map(|output| output.content_line_count).unwrap_or(0);
let command_failed = command_finished
&& output.is_some_and(|o| o.exit_status.is_none_or(|status| !status.success()));
let time_elapsed = if let Some(output) = output {
output.ended_at.duration_since(started_at)
} else {
started_at.elapsed()
};
let header_bg = cx
.theme()
.colors()
.element_background
.blend(cx.theme().colors().editor_foreground.opacity(0.025));
let border_color = cx.theme().colors().border.opacity(0.6);
let working_dir = working_dir
.as_ref()
.map(|path| format!("{}", path.display()))
.unwrap_or_else(|| "current directory".to_string());
let header = h_flex()
.id(SharedString::from(format!(
"terminal-tool-header-{}",
terminal.entity_id()
)))
.flex_none()
.gap_1()
.justify_between()
.rounded_t_md()
.child(
if let Some(terminal_view) = self.terminal_views.get(&terminal.entity_id()) {
// TODO: terminal has all the state we need to reproduce
// what we had in the terminal card.
terminal_view.clone().into_any_element()
} else {
Empty.into_any()
},
div()
.id(("command-target-path", terminal.entity_id()))
.w_full()
.max_w_full()
.overflow_x_scroll()
.child(
Label::new(working_dir)
.buffer_font(cx)
.size(LabelSize::XSmall)
.color(Color::Muted),
),
)
.when(!command_finished, |header| {
header
.gap_1p5()
.child(
Button::new(
SharedString::from(format!("stop-terminal-{}", terminal.entity_id())),
"Stop",
)
.icon(IconName::Stop)
.icon_position(IconPosition::Start)
.icon_size(IconSize::Small)
.icon_color(Color::Error)
.label_size(LabelSize::Small)
.tooltip(move |window, cx| {
Tooltip::with_meta(
"Stop This Command",
None,
"Also possible by placing your cursor inside the terminal and using regular terminal bindings.",
window,
cx,
)
})
.on_click({
let terminal = terminal.clone();
cx.listener(move |_this, _event, _window, cx| {
let inner_terminal = terminal.read(cx).inner().clone();
inner_terminal.update(cx, |inner_terminal, _cx| {
inner_terminal.kill_active_task();
});
})
}),
)
.child(Divider::vertical())
.child(
Icon::new(IconName::ArrowCircle)
.size(IconSize::XSmall)
.color(Color::Info)
.with_animation(
"arrow-circle",
Animation::new(Duration::from_secs(2)).repeat(),
|icon, delta| {
icon.transform(Transformation::rotate(percentage(delta)))
},
),
)
})
.when(tool_failed || command_failed, |header| {
header.child(
div()
.id(("terminal-tool-error-code-indicator", terminal.entity_id()))
.child(
Icon::new(IconName::Close)
.size(IconSize::Small)
.color(Color::Error),
)
.when_some(output.and_then(|o| o.exit_status), |this, status| {
this.tooltip(Tooltip::text(format!(
"Exited with code {}",
status.code().unwrap_or(-1),
)))
}),
)
})
.when(truncated_output, |header| {
let tooltip = if let Some(output) = output {
if output_line_count + 10 > terminal::MAX_SCROLL_HISTORY_LINES {
"Output exceeded terminal max lines and was \
truncated, the model received the first 16 KB."
.to_string()
} else {
format!(
"Output is {} long—to avoid unexpected token usage, \
only 16 KB was sent back to the model.",
format_file_size(output.original_content_len as u64, true),
)
}
} else {
"Output was truncated".to_string()
};
header.child(
h_flex()
.id(("terminal-tool-truncated-label", terminal.entity_id()))
.gap_1()
.child(
Icon::new(IconName::Info)
.size(IconSize::XSmall)
.color(Color::Ignored),
)
.child(
Label::new("Truncated")
.color(Color::Muted)
.size(LabelSize::XSmall),
)
.tooltip(Tooltip::text(tooltip)),
)
})
.when(time_elapsed > Duration::from_secs(10), |header| {
header.child(
Label::new(format!("({})", duration_alt_display(time_elapsed)))
.buffer_font(cx)
.color(Color::Muted)
.size(LabelSize::XSmall),
)
})
.child(
Disclosure::new(
SharedString::from(format!(
"terminal-tool-disclosure-{}",
terminal.entity_id()
)),
self.terminal_expanded,
)
.opened_icon(IconName::ChevronUp)
.closed_icon(IconName::ChevronDown)
.on_click(cx.listener(move |this, _event, _window, _cx| {
this.terminal_expanded = !this.terminal_expanded;
})),
);
let show_output =
self.terminal_expanded && self.terminal_views.contains_key(&terminal.entity_id());
v_flex()
.mb_2()
.border_1()
.when(tool_failed || command_failed, |card| card.border_dashed())
.border_color(border_color)
.rounded_lg()
.overflow_hidden()
.child(
v_flex()
.p_2()
.gap_0p5()
.bg(header_bg)
.text_xs()
.child(header)
.child(
MarkdownElement::new(
command.clone(),
terminal_command_markdown_style(window, cx),
)
.code_block_renderer(
markdown::CodeBlockRenderer::Default {
copy_button: false,
copy_button_on_hover: true,
border: false,
},
),
),
)
.when(show_output, |this| {
let terminal_view = self.terminal_views.get(&terminal.entity_id()).unwrap();
this.child(
div()
.pt_2()
.border_t_1()
.when(tool_failed || command_failed, |card| card.border_dashed())
.border_color(border_color)
.bg(cx.theme().colors().editor_background)
.rounded_b_md()
.text_ui_sm(cx)
.child(terminal_view.clone()),
)
})
.into_any()
}
@ -3056,6 +3316,18 @@ fn diff_editor_text_style_refinement(cx: &mut App) -> TextStyleRefinement {
}
}
fn terminal_command_markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
let default_md_style = default_markdown_style(true, window, cx);
MarkdownStyle {
base_text_style: TextStyle {
..default_md_style.base_text_style
},
selection_background_color: cx.theme().colors().element_selection_background,
..Default::default()
}
}
#[cfg(test)]
mod tests {
use agent_client_protocol::SessionId;

View file

@ -86,7 +86,7 @@ impl Tool for DiagnosticsTool {
input: serde_json::Value,
_request: Arc<LanguageModelRequest>,
project: Entity<Project>,
action_log: Entity<ActionLog>,
_action_log: Entity<ActionLog>,
_model: Arc<dyn LanguageModel>,
_window: Option<AnyWindowHandle>,
cx: &mut App,
@ -159,10 +159,6 @@ impl Tool for DiagnosticsTool {
}
}
action_log.update(cx, |action_log, _cx| {
action_log.checked_project_diagnostics();
});
if has_diagnostics {
Task::ready(Ok(output.into())).into()
} else {

View file

@ -152,6 +152,9 @@ impl PythonDebugAdapter {
maybe!(async move {
let response = latest_release.filter(|response| response.status().is_success())?;
let download_dir = debug_adapters_dir().join(Self::ADAPTER_NAME);
std::fs::create_dir_all(&download_dir).ok()?;
let mut output = String::new();
response
.into_body()

View file

@ -2172,6 +2172,9 @@ impl Fs for FakeFs {
async fn atomic_write(&self, path: PathBuf, data: String) -> Result<()> {
self.simulate_random_delay().await;
let path = normalize_path(path.as_path());
if let Some(path) = path.parent() {
self.create_dir(path).await?;
}
self.write_file_internal(path, data.into_bytes(), true)?;
Ok(())
}

View file

@ -1,5 +1,6 @@
use gpui::{
App, Application, Context, Menu, MenuItem, Window, WindowOptions, actions, div, prelude::*, rgb,
App, Application, Context, Menu, MenuItem, SystemMenuType, Window, WindowOptions, actions, div,
prelude::*, rgb,
};
struct SetMenus;
@ -27,7 +28,11 @@ fn main() {
// Add menu items
cx.set_menus(vec![Menu {
name: "set_menus".into(),
items: vec![MenuItem::action("Quit", Quit)],
items: vec![
MenuItem::os_submenu("Services", SystemMenuType::Services),
MenuItem::separator(),
MenuItem::action("Quit", Quit),
],
}]);
cx.open_window(WindowOptions::default(), |_, cx| cx.new(|_| SetMenus {}))
.unwrap();

View file

@ -20,6 +20,34 @@ impl Menu {
}
}
/// OS menus are menus that are recognized by the operating system
/// This allows the operating system to provide specialized items for
/// these menus
pub struct OsMenu {
/// The name of the menu
pub name: SharedString,
/// The type of menu
pub menu_type: SystemMenuType,
}
impl OsMenu {
/// Create an OwnedOsMenu from this OsMenu
pub fn owned(self) -> OwnedOsMenu {
OwnedOsMenu {
name: self.name.to_string().into(),
menu_type: self.menu_type,
}
}
}
/// The type of system menu
#[derive(Copy, Clone, Eq, PartialEq)]
pub enum SystemMenuType {
/// The 'Services' menu in the Application menu on macOS
Services,
}
/// The different kinds of items that can be in a menu
pub enum MenuItem {
/// A separator between items
@ -28,6 +56,9 @@ pub enum MenuItem {
/// A submenu
Submenu(Menu),
/// A menu, managed by the system (for example, the Services menu on macOS)
SystemMenu(OsMenu),
/// An action that can be performed
Action {
/// The name of this menu item
@ -53,6 +84,14 @@ impl MenuItem {
Self::Submenu(menu)
}
/// Creates a new submenu that is populated by the OS
pub fn os_submenu(name: impl Into<SharedString>, menu_type: SystemMenuType) -> Self {
Self::SystemMenu(OsMenu {
name: name.into(),
menu_type,
})
}
/// Creates a new menu item that invokes an action
pub fn action(name: impl Into<SharedString>, action: impl Action) -> Self {
Self::Action {
@ -89,10 +128,23 @@ impl MenuItem {
action,
os_action,
},
MenuItem::SystemMenu(os_menu) => OwnedMenuItem::SystemMenu(os_menu.owned()),
}
}
}
/// OS menus are menus that are recognized by the operating system
/// This allows the operating system to provide specialized items for
/// these menus
#[derive(Clone)]
pub struct OwnedOsMenu {
/// The name of the menu
pub name: SharedString,
/// The type of menu
pub menu_type: SystemMenuType,
}
/// A menu of the application, either a main menu or a submenu
#[derive(Clone)]
pub struct OwnedMenu {
@ -111,6 +163,9 @@ pub enum OwnedMenuItem {
/// A submenu
Submenu(OwnedMenu),
/// A menu, managed by the system (for example, the Services menu on macOS)
SystemMenu(OwnedOsMenu),
/// An action that can be performed
Action {
/// The name of this menu item
@ -139,6 +194,7 @@ impl Clone for OwnedMenuItem {
action: action.boxed_clone(),
os_action: *os_action,
},
OwnedMenuItem::SystemMenu(os_menu) => OwnedMenuItem::SystemMenu(os_menu.clone()),
}
}
}

View file

@ -7,9 +7,9 @@ use super::{
use crate::{
Action, AnyWindowHandle, BackgroundExecutor, ClipboardEntry, ClipboardItem, ClipboardString,
CursorStyle, ForegroundExecutor, Image, ImageFormat, KeyContext, Keymap, MacDispatcher,
MacDisplay, MacWindow, Menu, MenuItem, OwnedMenu, PathPromptOptions, Platform, PlatformDisplay,
PlatformKeyboardLayout, PlatformTextSystem, PlatformWindow, Result, SemanticVersion, Task,
WindowAppearance, WindowParams, hash,
MacDisplay, MacWindow, Menu, MenuItem, OsMenu, OwnedMenu, PathPromptOptions, Platform,
PlatformDisplay, PlatformKeyboardLayout, PlatformTextSystem, PlatformWindow, Result,
SemanticVersion, SystemMenuType, Task, WindowAppearance, WindowParams, hash,
};
use anyhow::{Context as _, anyhow};
use block::ConcreteBlock;
@ -413,9 +413,20 @@ impl MacPlatform {
}
item.setSubmenu_(submenu);
item.setTitle_(ns_string(&name));
if name == "Services" {
let app: id = msg_send![APP_CLASS, sharedApplication];
app.setServicesMenu_(item);
item
}
MenuItem::SystemMenu(OsMenu { name, menu_type }) => {
let item = NSMenuItem::new(nil).autorelease();
let submenu = NSMenu::new(nil).autorelease();
submenu.setDelegate_(delegate);
item.setSubmenu_(submenu);
item.setTitle_(ns_string(&name));
match menu_type {
SystemMenuType::Services => {
let app: id = msg_send![APP_CLASS, sharedApplication];
app.setServicesMenu_(item);
}
}
item

View file

@ -476,7 +476,7 @@ pub(crate) struct Underline {
pub content_mask: ContentMask<ScaledPixels>,
pub color: Hsla,
pub thickness: ScaledPixels,
pub wavy: bool,
pub wavy: u32,
}
impl From<Underline> for Primitive {

View file

@ -2814,7 +2814,7 @@ impl Window {
content_mask: content_mask.scale(scale_factor),
color: style.color.unwrap_or_default().opacity(element_opacity),
thickness: style.thickness.scale(scale_factor),
wavy: style.wavy,
wavy: if style.wavy { 1 } else { 0 },
});
}
@ -2845,7 +2845,7 @@ impl Window {
content_mask: content_mask.scale(scale_factor),
thickness: style.thickness.scale(scale_factor),
color: style.color.unwrap_or_default().opacity(opacity),
wavy: false,
wavy: 0,
});
}

View file

@ -121,8 +121,16 @@ impl ApplicationMenu {
menu.action(name, action)
}
OwnedMenuItem::Submenu(_) => menu,
OwnedMenuItem::SystemMenu(_) => {
// A system menu doesn't make sense in this context, so ignore it
menu
}
})
}
OwnedMenuItem::SystemMenu(_) => {
// A system menu doesn't make sense in this context, so ignore it
menu
}
})
})
}

View file

@ -420,7 +420,7 @@ pub struct Switch {
id: ElementId,
toggle_state: ToggleState,
disabled: bool,
on_click: Option<Box<dyn Fn(&ToggleState, &mut Window, &mut App) + 'static>>,
on_click: Option<Rc<dyn Fn(&ToggleState, &mut Window, &mut App) + 'static>>,
label: Option<SharedString>,
key_binding: Option<KeyBinding>,
color: SwitchColor,
@ -459,7 +459,7 @@ impl Switch {
mut self,
handler: impl Fn(&ToggleState, &mut Window, &mut App) + 'static,
) -> Self {
self.on_click = Some(Box::new(handler));
self.on_click = Some(Rc::new(handler));
self
}
@ -513,10 +513,16 @@ impl RenderOnce for Switch {
.when_some(
self.tab_index.filter(|_| !self.disabled),
|this, tab_index| {
this.tab_index(tab_index).focus(|mut style| {
style.border_color = Some(cx.theme().colors().border_focused);
style
})
this.tab_index(tab_index)
.focus(|mut style| {
style.border_color = Some(cx.theme().colors().border_focused);
style
})
.when_some(self.on_click.clone(), |this, on_click| {
this.on_click(move |_, window, cx| {
on_click(&self.toggle_state.inverse(), window, cx)
})
})
},
)
.child(

View file

@ -31,7 +31,7 @@ impl Vim {
) {
let count = Vim::take_count(cx).unwrap_or(1);
Vim::take_forced_motion(cx);
self.update_editor(window, cx, |_, editor, window, cx| {
self.update_editor(cx, |_, editor, cx| {
if let Some(selections) = editor
.change_list
.next_change(count, direction)
@ -49,7 +49,7 @@ impl Vim {
}
pub(crate) fn push_to_change_list(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let Some((new_positions, buffer)) = self.update_editor(window, cx, |vim, editor, _, cx| {
let Some((new_positions, buffer)) = self.update_editor(cx, |vim, editor, cx| {
let (map, selections) = editor.selections.all_adjusted_display(cx);
let buffer = editor.buffer().clone();

View file

@ -241,9 +241,9 @@ impl Deref for WrappedAction {
pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
// Vim::action(editor, cx, |vim, action: &StartOfLine, window, cx| {
Vim::action(editor, cx, |vim, action: &VimSet, window, cx| {
Vim::action(editor, cx, |vim, action: &VimSet, _, cx| {
for option in action.options.iter() {
vim.update_editor(window, cx, |_, editor, _, cx| match option {
vim.update_editor(cx, |_, editor, cx| match option {
VimOption::Wrap(true) => {
editor
.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx);
@ -298,7 +298,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
});
Vim::action(editor, cx, |vim, action: &VimSave, window, cx| {
vim.update_editor(window, cx, |_, editor, window, cx| {
vim.update_editor(cx, |_, editor, cx| {
let Some(project) = editor.project.clone() else {
return;
};
@ -375,7 +375,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
cx,
);
}
vim.update_editor(window, cx, |vim, editor, window, cx| match action {
vim.update_editor(cx, |vim, editor, cx| match action {
DeleteMarks::Marks(s) => {
if s.starts_with('-') || s.ends_with('-') || s.contains(['\'', '`']) {
err(s.clone(), window, cx);
@ -432,7 +432,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
});
Vim::action(editor, cx, |vim, action: &VimEdit, window, cx| {
vim.update_editor(window, cx, |vim, editor, window, cx| {
vim.update_editor(cx, |vim, editor, cx| {
let Some(workspace) = vim.workspace(window) else {
return;
};
@ -462,11 +462,10 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
.map(|c| Keystroke::parse(&c.to_string()).unwrap())
.collect();
vim.switch_mode(Mode::Normal, true, window, cx);
let initial_selections = vim.update_editor(window, cx, |_, editor, _, _| {
editor.selections.disjoint_anchors()
});
let initial_selections =
vim.update_editor(cx, |_, editor, _| editor.selections.disjoint_anchors());
if let Some(range) = &action.range {
let result = vim.update_editor(window, cx, |vim, editor, window, cx| {
let result = vim.update_editor(cx, |vim, editor, cx| {
let range = range.buffer_range(vim, editor, window, cx)?;
editor.change_selections(
SelectionEffects::no_scroll().nav_history(false),
@ -498,7 +497,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
cx.spawn_in(window, async move |vim, cx| {
task.await;
vim.update_in(cx, |vim, window, cx| {
vim.update_editor(window, cx, |_, editor, window, cx| {
vim.update_editor(cx, |_, editor, cx| {
if had_range {
editor.change_selections(SelectionEffects::default(), window, cx, |s| {
s.select_anchor_ranges([s.newest_anchor().range()]);
@ -510,7 +509,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
} else {
vim.switch_mode(Mode::Normal, true, window, cx);
}
vim.update_editor(window, cx, |_, editor, _, cx| {
vim.update_editor(cx, |_, editor, cx| {
if let Some(first_sel) = initial_selections {
if let Some(tx_id) = editor
.buffer()
@ -548,7 +547,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
Vim::action(editor, cx, |vim, action: &GoToLine, window, cx| {
vim.switch_mode(Mode::Normal, false, window, cx);
let result = vim.update_editor(window, cx, |vim, editor, window, cx| {
let result = vim.update_editor(cx, |vim, editor, cx| {
let snapshot = editor.snapshot(window, cx);
let buffer_row = action.range.head().buffer_row(vim, editor, window, cx)?;
let current = editor.selections.newest::<Point>(cx);
@ -573,7 +572,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
});
Vim::action(editor, cx, |vim, action: &YankCommand, window, cx| {
vim.update_editor(window, cx, |vim, editor, window, cx| {
vim.update_editor(cx, |vim, editor, cx| {
let snapshot = editor.snapshot(window, cx);
if let Ok(range) = action.range.buffer_range(vim, editor, window, cx) {
let end = if range.end < snapshot.buffer_snapshot.max_row() {
@ -600,7 +599,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
});
Vim::action(editor, cx, |vim, action: &WithRange, window, cx| {
let result = vim.update_editor(window, cx, |vim, editor, window, cx| {
let result = vim.update_editor(cx, |vim, editor, cx| {
action.range.buffer_range(vim, editor, window, cx)
});
@ -619,7 +618,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
};
let previous_selections = vim
.update_editor(window, cx, |_, editor, window, cx| {
.update_editor(cx, |_, editor, cx| {
let selections = action.restore_selection.then(|| {
editor
.selections
@ -635,7 +634,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
.flatten();
window.dispatch_action(action.action.boxed_clone(), cx);
cx.defer_in(window, move |vim, window, cx| {
vim.update_editor(window, cx, |_, editor, window, cx| {
vim.update_editor(cx, |_, editor, cx| {
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
if let Some(previous_selections) = previous_selections {
s.select_ranges(previous_selections);
@ -1536,7 +1535,7 @@ impl OnMatchingLines {
}
pub fn run(&self, vim: &mut Vim, window: &mut Window, cx: &mut Context<Vim>) {
let result = vim.update_editor(window, cx, |vim, editor, window, cx| {
let result = vim.update_editor(cx, |vim, editor, cx| {
self.range.buffer_range(vim, editor, window, cx)
});
@ -1600,7 +1599,7 @@ impl OnMatchingLines {
});
};
vim.update_editor(window, cx, |_, editor, window, cx| {
vim.update_editor(cx, |_, editor, cx| {
let snapshot = editor.snapshot(window, cx);
let mut row = range.start.0;
@ -1680,7 +1679,7 @@ pub struct ShellExec {
impl Vim {
pub fn cancel_running_command(&mut self, window: &mut Window, cx: &mut Context<Self>) {
if self.running_command.take().is_some() {
self.update_editor(window, cx, |_, editor, window, cx| {
self.update_editor(cx, |_, editor, cx| {
editor.transact(window, cx, |editor, _window, _cx| {
editor.clear_row_highlights::<ShellExec>();
})
@ -1691,7 +1690,7 @@ impl Vim {
fn prepare_shell_command(
&mut self,
command: &str,
window: &mut Window,
_: &mut Window,
cx: &mut Context<Self>,
) -> String {
let mut ret = String::new();
@ -1711,7 +1710,7 @@ impl Vim {
}
match c {
'%' => {
self.update_editor(window, cx, |_, editor, _window, cx| {
self.update_editor(cx, |_, editor, cx| {
if let Some((_, buffer, _)) = editor.active_excerpt(cx) {
if let Some(file) = buffer.read(cx).file() {
if let Some(local) = file.as_local() {
@ -1747,7 +1746,7 @@ impl Vim {
let Some(workspace) = self.workspace(window) else {
return;
};
let command = self.update_editor(window, cx, |_, editor, window, cx| {
let command = self.update_editor(cx, |_, editor, cx| {
let snapshot = editor.snapshot(window, cx);
let start = editor.selections.newest_display(cx);
let text_layout_details = editor.text_layout_details(window);
@ -1794,7 +1793,7 @@ impl Vim {
let Some(workspace) = self.workspace(window) else {
return;
};
let command = self.update_editor(window, cx, |_, editor, window, cx| {
let command = self.update_editor(cx, |_, editor, cx| {
let snapshot = editor.snapshot(window, cx);
let start = editor.selections.newest_display(cx);
let range = object
@ -1896,7 +1895,7 @@ impl ShellExec {
let mut input_snapshot = None;
let mut input_range = None;
let mut needs_newline_prefix = false;
vim.update_editor(window, cx, |vim, editor, window, cx| {
vim.update_editor(cx, |vim, editor, cx| {
let snapshot = editor.buffer().read(cx).snapshot(cx);
let range = if let Some(range) = self.range.clone() {
let Some(range) = range.buffer_range(vim, editor, window, cx).log_err() else {
@ -1990,7 +1989,7 @@ impl ShellExec {
}
vim.update_in(cx, |vim, window, cx| {
vim.update_editor(window, cx, |_, editor, window, cx| {
vim.update_editor(cx, |_, editor, cx| {
editor.transact(window, cx, |editor, window, cx| {
editor.edit([(range.clone(), text)], cx);
let snapshot = editor.buffer().read(cx).snapshot(cx);

View file

@ -56,9 +56,7 @@ impl Vim {
self.pop_operator(window, cx);
if self.editor_input_enabled() {
self.update_editor(window, cx, |_, editor, window, cx| {
editor.insert(&text, window, cx)
});
self.update_editor(cx, |_, editor, cx| editor.insert(&text, window, cx));
} else {
self.input_ignored(text, window, cx);
}
@ -214,9 +212,7 @@ impl Vim {
text.push_str(suffix);
if self.editor_input_enabled() {
self.update_editor(window, cx, |_, editor, window, cx| {
editor.insert(&text, window, cx)
});
self.update_editor(cx, |_, editor, cx| editor.insert(&text, window, cx));
} else {
self.input_ignored(text.into(), window, cx);
}

View file

@ -62,7 +62,7 @@ impl Vim {
cx: &mut Context<Self>,
mut is_boundary: impl FnMut(char, char, &CharClassifier) -> bool,
) {
self.update_editor(window, cx, |_, editor, window, cx| {
self.update_editor(cx, |_, editor, cx| {
editor.change_selections(Default::default(), window, cx, |s| {
s.move_with(|map, selection| {
let times = times.unwrap_or(1);
@ -115,7 +115,7 @@ impl Vim {
cx: &mut Context<Self>,
mut is_boundary: impl FnMut(char, char, &CharClassifier) -> bool,
) {
self.update_editor(window, cx, |_, editor, window, cx| {
self.update_editor(cx, |_, editor, cx| {
editor.change_selections(Default::default(), window, cx, |s| {
s.move_with(|map, selection| {
let times = times.unwrap_or(1);
@ -175,7 +175,7 @@ impl Vim {
window: &mut Window,
cx: &mut Context<Self>,
) {
self.update_editor(window, cx, |_, editor, window, cx| {
self.update_editor(cx, |_, editor, cx| {
let text_layout_details = editor.text_layout_details(window);
editor.change_selections(Default::default(), window, cx, |s| {
s.move_with(|map, selection| {
@ -253,7 +253,7 @@ impl Vim {
})
}
Motion::FindForward { .. } => {
self.update_editor(window, cx, |_, editor, window, cx| {
self.update_editor(cx, |_, editor, cx| {
let text_layout_details = editor.text_layout_details(window);
editor.change_selections(Default::default(), window, cx, |s| {
s.move_with(|map, selection| {
@ -280,7 +280,7 @@ impl Vim {
});
}
Motion::FindBackward { .. } => {
self.update_editor(window, cx, |_, editor, window, cx| {
self.update_editor(cx, |_, editor, cx| {
let text_layout_details = editor.text_layout_details(window);
editor.change_selections(Default::default(), window, cx, |s| {
s.move_with(|map, selection| {
@ -312,7 +312,7 @@ impl Vim {
fn helix_insert(&mut self, _: &HelixInsert, window: &mut Window, cx: &mut Context<Self>) {
self.start_recording(cx);
self.update_editor(window, cx, |_, editor, window, cx| {
self.update_editor(cx, |_, editor, cx| {
editor.change_selections(Default::default(), window, cx, |s| {
s.move_with(|_map, selection| {
// In helix normal mode, move cursor to start of selection and collapse
@ -328,7 +328,7 @@ impl Vim {
fn helix_append(&mut self, _: &HelixAppend, window: &mut Window, cx: &mut Context<Self>) {
self.start_recording(cx);
self.switch_mode(Mode::Insert, false, window, cx);
self.update_editor(window, cx, |_, editor, window, cx| {
self.update_editor(cx, |_, editor, cx| {
editor.change_selections(Default::default(), window, cx, |s| {
s.move_with(|map, selection| {
let point = if selection.is_empty() {
@ -343,7 +343,7 @@ impl Vim {
}
pub fn helix_replace(&mut self, text: &str, window: &mut Window, cx: &mut Context<Self>) {
self.update_editor(window, cx, |_, editor, window, cx| {
self.update_editor(cx, |_, editor, cx| {
editor.transact(window, cx, |editor, window, cx| {
let (map, selections) = editor.selections.all_display(cx);

View file

@ -31,7 +31,7 @@ pub(crate) fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
let count = Vim::take_count(cx).unwrap_or(1);
Vim::take_forced_motion(cx);
vim.store_visual_marks(window, cx);
vim.update_editor(window, cx, |vim, editor, window, cx| {
vim.update_editor(cx, |vim, editor, cx| {
editor.transact(window, cx, |editor, window, cx| {
let original_positions = vim.save_selection_starts(editor, cx);
for _ in 0..count {
@ -50,7 +50,7 @@ pub(crate) fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
let count = Vim::take_count(cx).unwrap_or(1);
Vim::take_forced_motion(cx);
vim.store_visual_marks(window, cx);
vim.update_editor(window, cx, |vim, editor, window, cx| {
vim.update_editor(cx, |vim, editor, cx| {
editor.transact(window, cx, |editor, window, cx| {
let original_positions = vim.save_selection_starts(editor, cx);
for _ in 0..count {
@ -69,7 +69,7 @@ pub(crate) fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
let count = Vim::take_count(cx).unwrap_or(1);
Vim::take_forced_motion(cx);
vim.store_visual_marks(window, cx);
vim.update_editor(window, cx, |vim, editor, window, cx| {
vim.update_editor(cx, |vim, editor, cx| {
editor.transact(window, cx, |editor, window, cx| {
let original_positions = vim.save_selection_starts(editor, cx);
for _ in 0..count {
@ -95,7 +95,7 @@ impl Vim {
cx: &mut Context<Self>,
) {
self.stop_recording(cx);
self.update_editor(window, cx, |_, editor, window, cx| {
self.update_editor(cx, |_, editor, cx| {
let text_layout_details = editor.text_layout_details(window);
editor.transact(window, cx, |editor, window, cx| {
let mut selection_starts: HashMap<_, _> = Default::default();
@ -137,7 +137,7 @@ impl Vim {
cx: &mut Context<Self>,
) {
self.stop_recording(cx);
self.update_editor(window, cx, |_, editor, window, cx| {
self.update_editor(cx, |_, editor, cx| {
editor.transact(window, cx, |editor, window, cx| {
let mut original_positions: HashMap<_, _> = Default::default();
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {

View file

@ -38,7 +38,7 @@ impl Vim {
if count <= 1 || Vim::globals(cx).dot_replaying {
self.create_mark("^".into(), window, cx);
self.update_editor(window, cx, |_, editor, window, cx| {
self.update_editor(cx, |_, editor, cx| {
editor.dismiss_menus_and_popups(false, window, cx);
if !HelixModeSetting::get_global(cx).0 {

View file

@ -679,7 +679,7 @@ impl Vim {
match self.mode {
Mode::Visual | Mode::VisualLine | Mode::VisualBlock => {
if !prior_selections.is_empty() {
self.update_editor(window, cx, |_, editor, window, cx| {
self.update_editor(cx, |_, editor, cx| {
editor.change_selections(Default::default(), window, cx, |s| {
s.select_ranges(prior_selections.iter().cloned())
})

View file

@ -132,7 +132,7 @@ pub(crate) fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
Vim::action(editor, cx, |vim, _: &HelixDelete, window, cx| {
vim.record_current_action(cx);
vim.update_editor(window, cx, |_, editor, window, cx| {
vim.update_editor(cx, |_, editor, cx| {
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.move_with(|map, selection| {
if selection.is_empty() {
@ -146,7 +146,7 @@ pub(crate) fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
});
Vim::action(editor, cx, |vim, _: &HelixCollapseSelection, window, cx| {
vim.update_editor(window, cx, |_, editor, window, cx| {
vim.update_editor(cx, |_, editor, cx| {
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.move_with(|map, selection| {
let mut point = selection.head();
@ -198,7 +198,7 @@ pub(crate) fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
Vim::action(editor, cx, |vim, _: &Undo, window, cx| {
let times = Vim::take_count(cx);
Vim::take_forced_motion(cx);
vim.update_editor(window, cx, |_, editor, window, cx| {
vim.update_editor(cx, |_, editor, cx| {
for _ in 0..times.unwrap_or(1) {
editor.undo(&editor::actions::Undo, window, cx);
}
@ -207,7 +207,7 @@ pub(crate) fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
Vim::action(editor, cx, |vim, _: &Redo, window, cx| {
let times = Vim::take_count(cx);
Vim::take_forced_motion(cx);
vim.update_editor(window, cx, |_, editor, window, cx| {
vim.update_editor(cx, |_, editor, cx| {
for _ in 0..times.unwrap_or(1) {
editor.redo(&editor::actions::Redo, window, cx);
}
@ -215,7 +215,7 @@ pub(crate) fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
});
Vim::action(editor, cx, |vim, _: &UndoLastLine, window, cx| {
Vim::take_forced_motion(cx);
vim.update_editor(window, cx, |vim, editor, window, cx| {
vim.update_editor(cx, |vim, editor, cx| {
let snapshot = editor.buffer().read(cx).snapshot(cx);
let Some(last_change) = editor.change_list.last_before_grouping() else {
return;
@ -526,7 +526,7 @@ impl Vim {
window: &mut Window,
cx: &mut Context<Self>,
) {
self.update_editor(window, cx, |_, editor, window, cx| {
self.update_editor(cx, |_, editor, cx| {
let text_layout_details = editor.text_layout_details(window);
editor.change_selections(
SelectionEffects::default().nav_history(motion.push_to_jump_list()),
@ -546,7 +546,7 @@ impl Vim {
fn insert_after(&mut self, _: &InsertAfter, window: &mut Window, cx: &mut Context<Self>) {
self.start_recording(cx);
self.switch_mode(Mode::Insert, false, window, cx);
self.update_editor(window, cx, |_, editor, window, cx| {
self.update_editor(cx, |_, editor, cx| {
editor.change_selections(Default::default(), window, cx, |s| {
s.move_cursors_with(|map, cursor, _| (right(map, cursor, 1), SelectionGoal::None));
});
@ -557,7 +557,7 @@ impl Vim {
self.start_recording(cx);
if self.mode.is_visual() {
let current_mode = self.mode;
self.update_editor(window, cx, |_, editor, window, cx| {
self.update_editor(cx, |_, editor, cx| {
editor.change_selections(Default::default(), window, cx, |s| {
s.move_with(|map, selection| {
if current_mode == Mode::VisualLine {
@ -581,7 +581,7 @@ impl Vim {
) {
self.start_recording(cx);
self.switch_mode(Mode::Insert, false, window, cx);
self.update_editor(window, cx, |_, editor, window, cx| {
self.update_editor(cx, |_, editor, cx| {
editor.change_selections(Default::default(), window, cx, |s| {
s.move_cursors_with(|map, cursor, _| {
(
@ -601,7 +601,7 @@ impl Vim {
) {
self.start_recording(cx);
self.switch_mode(Mode::Insert, false, window, cx);
self.update_editor(window, cx, |_, editor, window, cx| {
self.update_editor(cx, |_, editor, cx| {
editor.change_selections(Default::default(), window, cx, |s| {
s.move_cursors_with(|map, cursor, _| {
(next_line_end(map, cursor, 1), SelectionGoal::None)
@ -618,7 +618,7 @@ impl Vim {
) {
self.start_recording(cx);
self.switch_mode(Mode::Insert, false, window, cx);
self.update_editor(window, cx, |vim, editor, window, cx| {
self.update_editor(cx, |vim, editor, cx| {
let Some(Mark::Local(marks)) = vim.get_mark("^", editor, window, cx) else {
return;
};
@ -637,7 +637,7 @@ impl Vim {
) {
self.start_recording(cx);
self.switch_mode(Mode::Insert, false, window, cx);
self.update_editor(window, cx, |_, editor, window, cx| {
self.update_editor(cx, |_, editor, cx| {
editor.transact(window, cx, |editor, window, cx| {
let selections = editor.selections.all::<Point>(cx);
let snapshot = editor.buffer().read(cx).snapshot(cx);
@ -678,7 +678,7 @@ impl Vim {
) {
self.start_recording(cx);
self.switch_mode(Mode::Insert, false, window, cx);
self.update_editor(window, cx, |_, editor, window, cx| {
self.update_editor(cx, |_, editor, cx| {
let text_layout_details = editor.text_layout_details(window);
editor.transact(window, cx, |editor, window, cx| {
let selections = editor.selections.all::<Point>(cx);
@ -725,7 +725,7 @@ impl Vim {
self.record_current_action(cx);
let count = Vim::take_count(cx).unwrap_or(1);
Vim::take_forced_motion(cx);
self.update_editor(window, cx, |_, editor, window, cx| {
self.update_editor(cx, |_, editor, cx| {
editor.transact(window, cx, |editor, _, cx| {
let selections = editor.selections.all::<Point>(cx);
@ -754,7 +754,7 @@ impl Vim {
self.record_current_action(cx);
let count = Vim::take_count(cx).unwrap_or(1);
Vim::take_forced_motion(cx);
self.update_editor(window, cx, |_, editor, window, cx| {
self.update_editor(cx, |_, editor, cx| {
editor.transact(window, cx, |editor, window, cx| {
let selections = editor.selections.all::<Point>(cx);
let snapshot = editor.buffer().read(cx).snapshot(cx);
@ -804,7 +804,7 @@ impl Vim {
times -= 1;
}
self.update_editor(window, cx, |_, editor, window, cx| {
self.update_editor(cx, |_, editor, cx| {
editor.transact(window, cx, |editor, window, cx| {
for _ in 0..times {
editor.join_lines_impl(insert_whitespace, window, cx)
@ -828,10 +828,10 @@ impl Vim {
)
}
fn show_location(&mut self, _: &ShowLocation, window: &mut Window, cx: &mut Context<Self>) {
fn show_location(&mut self, _: &ShowLocation, _: &mut Window, cx: &mut Context<Self>) {
let count = Vim::take_count(cx);
Vim::take_forced_motion(cx);
self.update_editor(window, cx, |vim, editor, _window, cx| {
self.update_editor(cx, |vim, editor, cx| {
let selection = editor.selections.newest_anchor();
let Some((buffer, point, _)) = editor
.buffer()
@ -875,7 +875,7 @@ impl Vim {
fn toggle_comments(&mut self, _: &ToggleComments, window: &mut Window, cx: &mut Context<Self>) {
self.record_current_action(cx);
self.store_visual_marks(window, cx);
self.update_editor(window, cx, |vim, editor, window, cx| {
self.update_editor(cx, |vim, editor, cx| {
editor.transact(window, cx, |editor, window, cx| {
let original_positions = vim.save_selection_starts(editor, cx);
editor.toggle_comments(&Default::default(), window, cx);
@ -897,7 +897,7 @@ impl Vim {
let count = Vim::take_count(cx).unwrap_or(1);
Vim::take_forced_motion(cx);
self.stop_recording(cx);
self.update_editor(window, cx, |_, editor, window, cx| {
self.update_editor(cx, |_, editor, cx| {
editor.transact(window, cx, |editor, window, cx| {
editor.set_clip_at_line_ends(false, cx);
let (map, display_selections) = editor.selections.all_display(cx);

View file

@ -34,7 +34,7 @@ impl Vim {
} else {
None
};
self.update_editor(window, cx, |vim, editor, window, cx| {
self.update_editor(cx, |vim, editor, cx| {
let text_layout_details = editor.text_layout_details(window);
editor.transact(window, cx, |editor, window, cx| {
// We are swapping to insert mode anyway. Just set the line end clipping behavior now
@ -111,7 +111,7 @@ impl Vim {
cx: &mut Context<Self>,
) {
let mut objects_found = false;
self.update_editor(window, cx, |vim, editor, window, cx| {
self.update_editor(cx, |vim, editor, cx| {
// We are swapping to insert mode anyway. Just set the line end clipping behavior now
editor.set_clip_at_line_ends(false, cx);
editor.transact(window, cx, |editor, window, cx| {

View file

@ -31,7 +31,7 @@ impl Vim {
cx: &mut Context<Self>,
) {
self.stop_recording(cx);
self.update_editor(window, cx, |_, editor, window, cx| {
self.update_editor(cx, |_, editor, cx| {
editor.set_clip_at_line_ends(false, cx);
let text_layout_details = editor.text_layout_details(window);
editor.transact(window, cx, |editor, window, cx| {
@ -87,7 +87,7 @@ impl Vim {
cx: &mut Context<Self>,
) {
self.stop_recording(cx);
self.update_editor(window, cx, |_, editor, window, cx| {
self.update_editor(cx, |_, editor, cx| {
editor.transact(window, cx, |editor, window, cx| {
editor.set_clip_at_line_ends(false, cx);
let mut original_positions: HashMap<_, _> = Default::default();
@ -195,7 +195,7 @@ impl Vim {
let count = Vim::take_count(cx).unwrap_or(1) as u32;
Vim::take_forced_motion(cx);
self.update_editor(window, cx, |vim, editor, window, cx| {
self.update_editor(cx, |vim, editor, cx| {
let mut ranges = Vec::new();
let mut cursor_positions = Vec::new();
let snapshot = editor.buffer().read(cx).snapshot(cx);

View file

@ -22,7 +22,7 @@ impl Vim {
cx: &mut Context<Self>,
) {
self.stop_recording(cx);
self.update_editor(window, cx, |vim, editor, window, cx| {
self.update_editor(cx, |vim, editor, cx| {
let text_layout_details = editor.text_layout_details(window);
editor.transact(window, cx, |editor, window, cx| {
editor.set_clip_at_line_ends(false, cx);
@ -96,7 +96,7 @@ impl Vim {
cx: &mut Context<Self>,
) {
self.stop_recording(cx);
self.update_editor(window, cx, |vim, editor, window, cx| {
self.update_editor(cx, |vim, editor, cx| {
editor.transact(window, cx, |editor, window, cx| {
editor.set_clip_at_line_ends(false, cx);
// Emulates behavior in vim where if we expanded backwards to include a newline

View file

@ -53,7 +53,7 @@ impl Vim {
cx: &mut Context<Self>,
) {
self.store_visual_marks(window, cx);
self.update_editor(window, cx, |vim, editor, window, cx| {
self.update_editor(cx, |vim, editor, cx| {
let mut edits = Vec::new();
let mut new_anchors = Vec::new();

View file

@ -19,7 +19,7 @@ use crate::{
impl Vim {
pub fn create_mark(&mut self, text: Arc<str>, window: &mut Window, cx: &mut Context<Self>) {
self.update_editor(window, cx, |vim, editor, window, cx| {
self.update_editor(cx, |vim, editor, cx| {
let anchors = editor
.selections
.disjoint_anchors()
@ -49,7 +49,7 @@ impl Vim {
let mut ends = vec![];
let mut reversed = vec![];
self.update_editor(window, cx, |vim, editor, window, cx| {
self.update_editor(cx, |vim, editor, cx| {
let (map, selections) = editor.selections.all_display(cx);
for selection in selections {
let end = movement::saturating_left(&map, selection.end);
@ -190,7 +190,7 @@ impl Vim {
self.pop_operator(window, cx);
}
let mark = self
.update_editor(window, cx, |vim, editor, window, cx| {
.update_editor(cx, |vim, editor, cx| {
vim.get_mark(&text, editor, window, cx)
})
.flatten();
@ -209,7 +209,7 @@ impl Vim {
let Some(mut anchors) = anchors else { return };
self.update_editor(window, cx, |_, editor, _, cx| {
self.update_editor(cx, |_, editor, cx| {
editor.create_nav_history_entry(cx);
});
let is_active_operator = self.active_operator().is_some();
@ -231,7 +231,7 @@ impl Vim {
|| self.mode == Mode::VisualLine
|| self.mode == Mode::VisualBlock;
self.update_editor(window, cx, |_, editor, window, cx| {
self.update_editor(cx, |_, editor, cx| {
let map = editor.snapshot(window, cx);
let mut ranges: Vec<Range<Anchor>> = Vec::new();
for mut anchor in anchors {

View file

@ -32,7 +32,7 @@ impl Vim {
let count = Vim::take_count(cx).unwrap_or(1);
Vim::take_forced_motion(cx);
self.update_editor(window, cx, |vim, editor, window, cx| {
self.update_editor(cx, |vim, editor, cx| {
let text_layout_details = editor.text_layout_details(window);
editor.transact(window, cx, |editor, window, cx| {
editor.set_clip_at_line_ends(false, cx);
@ -236,7 +236,7 @@ impl Vim {
) {
self.stop_recording(cx);
let selected_register = self.selected_register.take();
self.update_editor(window, cx, |_, editor, window, cx| {
self.update_editor(cx, |_, editor, cx| {
editor.transact(window, cx, |editor, window, cx| {
editor.set_clip_at_line_ends(false, cx);
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
@ -273,7 +273,7 @@ impl Vim {
) {
self.stop_recording(cx);
let selected_register = self.selected_register.take();
self.update_editor(window, cx, |_, editor, window, cx| {
self.update_editor(cx, |_, editor, cx| {
let text_layout_details = editor.text_layout_details(window);
editor.transact(window, cx, |editor, window, cx| {
editor.set_clip_at_line_ends(false, cx);

View file

@ -97,7 +97,7 @@ impl Vim {
let amount = by(Vim::take_count(cx).map(|c| c as f32));
Vim::take_forced_motion(cx);
self.exit_temporary_normal(window, cx);
self.update_editor(window, cx, |_, editor, window, cx| {
self.update_editor(cx, |_, editor, cx| {
scroll_editor(editor, move_cursor, &amount, window, cx)
});
}

View file

@ -251,7 +251,7 @@ impl Vim {
// If the active editor has changed during a search, don't panic.
if prior_selections.iter().any(|s| {
self.update_editor(window, cx, |_, editor, window, cx| {
self.update_editor(cx, |_, editor, cx| {
!s.start
.is_valid(&editor.snapshot(window, cx).buffer_snapshot)
})
@ -457,7 +457,7 @@ impl Vim {
else {
return;
};
if let Some(result) = self.update_editor(window, cx, |vim, editor, window, cx| {
if let Some(result) = self.update_editor(cx, |vim, editor, cx| {
let range = action.range.buffer_range(vim, editor, window, cx)?;
let snapshot = &editor.snapshot(window, cx).buffer_snapshot;
let end_point = Point::new(range.end.0, snapshot.line_len(range.end));

View file

@ -45,7 +45,7 @@ impl Vim {
cx: &mut Context<Self>,
) {
self.store_visual_marks(window, cx);
self.update_editor(window, cx, |vim, editor, window, cx| {
self.update_editor(cx, |vim, editor, cx| {
editor.set_clip_at_line_ends(false, cx);
editor.transact(window, cx, |editor, window, cx| {
let text_layout_details = editor.text_layout_details(window);

View file

@ -14,7 +14,7 @@ impl Vim {
cx: &mut Context<Self>,
) {
self.stop_recording(cx);
self.update_editor(window, cx, |_, editor, window, cx| {
self.update_editor(cx, |_, editor, cx| {
let text_layout_details = editor.text_layout_details(window);
editor.transact(window, cx, |editor, window, cx| {
let mut selection_starts: HashMap<_, _> = Default::default();
@ -51,7 +51,7 @@ impl Vim {
cx: &mut Context<Self>,
) {
self.stop_recording(cx);
self.update_editor(window, cx, |_, editor, window, cx| {
self.update_editor(cx, |_, editor, cx| {
editor.transact(window, cx, |editor, window, cx| {
let mut original_positions: HashMap<_, _> = Default::default();
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {

View file

@ -25,7 +25,7 @@ impl Vim {
window: &mut Window,
cx: &mut Context<Self>,
) {
self.update_editor(window, cx, |vim, editor, window, cx| {
self.update_editor(cx, |vim, editor, cx| {
let text_layout_details = editor.text_layout_details(window);
editor.transact(window, cx, |editor, window, cx| {
editor.set_clip_at_line_ends(false, cx);
@ -70,7 +70,7 @@ impl Vim {
window: &mut Window,
cx: &mut Context<Self>,
) {
self.update_editor(window, cx, |vim, editor, window, cx| {
self.update_editor(cx, |vim, editor, cx| {
editor.transact(window, cx, |editor, window, cx| {
editor.set_clip_at_line_ends(false, cx);
let mut start_positions: HashMap<_, _> = Default::default();

View file

@ -49,7 +49,7 @@ impl Vim {
window: &mut Window,
cx: &mut Context<Self>,
) {
self.update_editor(window, cx, |vim, editor, window, cx| {
self.update_editor(cx, |vim, editor, cx| {
editor.transact(window, cx, |editor, window, cx| {
editor.set_clip_at_line_ends(false, cx);
let map = editor.snapshot(window, cx);
@ -94,7 +94,7 @@ impl Vim {
window: &mut Window,
cx: &mut Context<Self>,
) {
self.update_editor(window, cx, |vim, editor, window, cx| {
self.update_editor(cx, |vim, editor, cx| {
editor.transact(window, cx, |editor, window, cx| {
editor.set_clip_at_line_ends(false, cx);
let map = editor.snapshot(window, cx);
@ -148,7 +148,7 @@ impl Vim {
cx: &mut Context<Self>,
) {
self.stop_recording(cx);
self.update_editor(window, cx, |vim, editor, window, cx| {
self.update_editor(cx, |vim, editor, cx| {
editor.set_clip_at_line_ends(false, cx);
let mut selection = editor.selections.newest_display(cx);
let snapshot = editor.snapshot(window, cx);
@ -167,7 +167,7 @@ impl Vim {
pub fn exchange_visual(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.stop_recording(cx);
self.update_editor(window, cx, |vim, editor, window, cx| {
self.update_editor(cx, |vim, editor, cx| {
let selection = editor.selections.newest_anchor();
let new_range = selection.start..selection.end;
let snapshot = editor.snapshot(window, cx);
@ -178,7 +178,7 @@ impl Vim {
pub fn clear_exchange(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.stop_recording(cx);
self.update_editor(window, cx, |_, editor, _, cx| {
self.update_editor(cx, |_, editor, cx| {
editor.clear_background_highlights::<VimExchange>(cx);
});
self.clear_operator(window, cx);
@ -193,7 +193,7 @@ impl Vim {
cx: &mut Context<Self>,
) {
self.stop_recording(cx);
self.update_editor(window, cx, |vim, editor, window, cx| {
self.update_editor(cx, |vim, editor, cx| {
editor.set_clip_at_line_ends(false, cx);
let text_layout_details = editor.text_layout_details(window);
let mut selection = editor.selections.newest_display(cx);

View file

@ -18,7 +18,7 @@ pub(crate) fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
Vim::take_count(cx);
Vim::take_forced_motion(cx);
vim.store_visual_marks(window, cx);
vim.update_editor(window, cx, |vim, editor, window, cx| {
vim.update_editor(cx, |vim, editor, cx| {
editor.transact(window, cx, |editor, window, cx| {
let mut positions = vim.save_selection_starts(editor, cx);
editor.rewrap_impl(
@ -55,7 +55,7 @@ impl Vim {
cx: &mut Context<Self>,
) {
self.stop_recording(cx);
self.update_editor(window, cx, |_, editor, window, cx| {
self.update_editor(cx, |_, editor, cx| {
let text_layout_details = editor.text_layout_details(window);
editor.transact(window, cx, |editor, window, cx| {
let mut selection_starts: HashMap<_, _> = Default::default();
@ -100,7 +100,7 @@ impl Vim {
cx: &mut Context<Self>,
) {
self.stop_recording(cx);
self.update_editor(window, cx, |_, editor, window, cx| {
self.update_editor(cx, |_, editor, cx| {
editor.transact(window, cx, |editor, window, cx| {
let mut original_positions: HashMap<_, _> = Default::default();
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {

View file

@ -29,7 +29,7 @@ impl Vim {
let count = Vim::take_count(cx);
let forced_motion = Vim::take_forced_motion(cx);
let mode = self.mode;
self.update_editor(window, cx, |_, editor, window, cx| {
self.update_editor(cx, |_, editor, cx| {
let text_layout_details = editor.text_layout_details(window);
editor.transact(window, cx, |editor, window, cx| {
editor.set_clip_at_line_ends(false, cx);
@ -140,7 +140,7 @@ impl Vim {
};
let surround = pair.end != *text;
self.update_editor(window, cx, |_, editor, window, cx| {
self.update_editor(cx, |_, editor, cx| {
editor.transact(window, cx, |editor, window, cx| {
editor.set_clip_at_line_ends(false, cx);
@ -228,7 +228,7 @@ impl Vim {
) {
if let Some(will_replace_pair) = object_to_bracket_pair(target) {
self.stop_recording(cx);
self.update_editor(window, cx, |_, editor, window, cx| {
self.update_editor(cx, |_, editor, cx| {
editor.transact(window, cx, |editor, window, cx| {
editor.set_clip_at_line_ends(false, cx);
@ -344,7 +344,7 @@ impl Vim {
) -> bool {
let mut valid = false;
if let Some(pair) = object_to_bracket_pair(object) {
self.update_editor(window, cx, |_, editor, window, cx| {
self.update_editor(cx, |_, editor, cx| {
editor.transact(window, cx, |editor, window, cx| {
editor.set_clip_at_line_ends(false, cx);
let (display_map, selections) = editor.selections.all_adjusted_display(cx);

View file

@ -748,7 +748,7 @@ impl Vim {
editor,
cx,
|vim, action: &editor::actions::AcceptEditPrediction, window, cx| {
vim.update_editor(window, cx, |_, editor, window, cx| {
vim.update_editor(cx, |_, editor, cx| {
editor.accept_edit_prediction(action, window, cx);
});
// In non-insertion modes, predictions will be hidden and instead a jump will be
@ -847,7 +847,7 @@ impl Vim {
if let Some(action) = keystroke_event.action.as_ref() {
// Keystroke is handled by the vim system, so continue forward
if action.name().starts_with("vim::") {
self.update_editor(window, cx, |_, editor, _, cx| {
self.update_editor(cx, |_, editor, cx| {
editor.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx)
});
return;
@ -909,7 +909,7 @@ impl Vim {
anchor,
is_deactivate,
} => {
self.update_editor(window, cx, |vim, editor, window, cx| {
self.update_editor(cx, |vim, editor, cx| {
let mark = if *is_deactivate {
"\"".to_string()
} else {
@ -972,7 +972,7 @@ impl Vim {
if mode == Mode::Normal || mode != last_mode {
self.current_tx.take();
self.current_anchor.take();
self.update_editor(window, cx, |_, editor, _, _| {
self.update_editor(cx, |_, editor, _| {
editor.clear_selection_drag_state();
});
}
@ -988,7 +988,7 @@ impl Vim {
&& self.mode != self.last_mode
&& (self.mode == Mode::Insert || self.last_mode == Mode::Insert)
{
self.update_editor(window, cx, |vim, editor, _, cx| {
self.update_editor(cx, |vim, editor, cx| {
let is_relative = vim.mode != Mode::Insert;
editor.set_relative_line_number(Some(is_relative), cx)
});
@ -1003,7 +1003,7 @@ impl Vim {
}
// Adjust selections
self.update_editor(window, cx, |vim, editor, window, cx| {
self.update_editor(cx, |vim, editor, cx| {
if last_mode != Mode::VisualBlock && last_mode.is_visual() && mode == Mode::VisualBlock
{
vim.visual_block_motion(true, editor, window, cx, |_, point, goal| {
@ -1214,7 +1214,7 @@ impl Vim {
if preserve_selection {
self.switch_mode(Mode::Visual, true, window, cx);
} else {
self.update_editor(window, cx, |_, editor, window, cx| {
self.update_editor(cx, |_, editor, cx| {
editor.set_clip_at_line_ends(false, cx);
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.move_with(|_, selection| {
@ -1232,18 +1232,18 @@ impl Vim {
if let Some(old_vim) = Vim::globals(cx).focused_vim() {
if old_vim.entity_id() != cx.entity().entity_id() {
old_vim.update(cx, |vim, cx| {
vim.update_editor(window, cx, |_, editor, _, cx| {
vim.update_editor(cx, |_, editor, cx| {
editor.set_relative_line_number(None, cx)
});
});
self.update_editor(window, cx, |vim, editor, _, cx| {
self.update_editor(cx, |vim, editor, cx| {
let is_relative = vim.mode != Mode::Insert;
editor.set_relative_line_number(Some(is_relative), cx)
});
}
} else {
self.update_editor(window, cx, |vim, editor, _, cx| {
self.update_editor(cx, |vim, editor, cx| {
let is_relative = vim.mode != Mode::Insert;
editor.set_relative_line_number(Some(is_relative), cx)
});
@ -1256,35 +1256,30 @@ impl Vim {
self.stop_recording_immediately(NormalBefore.boxed_clone(), cx);
self.store_visual_marks(window, cx);
self.clear_operator(window, cx);
self.update_editor(window, cx, |vim, editor, _, cx| {
self.update_editor(cx, |vim, editor, cx| {
if vim.cursor_shape(cx) == CursorShape::Block {
editor.set_cursor_shape(CursorShape::Hollow, cx);
}
});
}
fn cursor_shape_changed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.update_editor(window, cx, |vim, editor, _, cx| {
fn cursor_shape_changed(&mut self, _: &mut Window, cx: &mut Context<Self>) {
self.update_editor(cx, |vim, editor, cx| {
editor.set_cursor_shape(vim.cursor_shape(cx), cx);
});
}
fn update_editor<S>(
&mut self,
window: &mut Window,
cx: &mut Context<Self>,
update: impl FnOnce(&mut Self, &mut Editor, &mut Window, &mut Context<Editor>) -> S,
update: impl FnOnce(&mut Self, &mut Editor, &mut Context<Editor>) -> S,
) -> Option<S> {
let editor = self.editor.upgrade()?;
Some(editor.update(cx, |editor, cx| update(self, editor, window, cx)))
Some(editor.update(cx, |editor, cx| update(self, editor, cx)))
}
fn editor_selections(
&mut self,
window: &mut Window,
cx: &mut Context<Self>,
) -> Vec<Range<Anchor>> {
self.update_editor(window, cx, |_, editor, _, _| {
fn editor_selections(&mut self, _: &mut Window, cx: &mut Context<Self>) -> Vec<Range<Anchor>> {
self.update_editor(cx, |_, editor, _| {
editor
.selections
.disjoint_anchors()
@ -1300,7 +1295,7 @@ impl Vim {
window: &mut Window,
cx: &mut Context<Self>,
) -> Option<String> {
self.update_editor(window, cx, |_, editor, window, cx| {
self.update_editor(cx, |_, editor, cx| {
let selection = editor.selections.newest::<usize>(cx);
let snapshot = &editor.snapshot(window, cx).buffer_snapshot;
@ -1489,7 +1484,7 @@ impl Vim {
) {
match self.mode {
Mode::VisualLine | Mode::VisualBlock | Mode::Visual => {
self.update_editor(window, cx, |vim, editor, window, cx| {
self.update_editor(cx, |vim, editor, cx| {
let original_mode = vim.undo_modes.get(transaction_id);
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
match original_mode {
@ -1520,7 +1515,7 @@ impl Vim {
self.switch_mode(Mode::Normal, true, window, cx)
}
Mode::Normal => {
self.update_editor(window, cx, |_, editor, window, cx| {
self.update_editor(cx, |_, editor, cx| {
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.move_with(|map, selection| {
selection
@ -1547,7 +1542,7 @@ impl Vim {
self.current_anchor = Some(newest);
} else if self.current_anchor.as_ref().unwrap() != &newest {
if let Some(tx_id) = self.current_tx.take() {
self.update_editor(window, cx, |_, editor, _, cx| {
self.update_editor(cx, |_, editor, cx| {
editor.group_until_transaction(tx_id, cx)
});
}
@ -1694,7 +1689,7 @@ impl Vim {
}
Some(Operator::Register) => match self.mode {
Mode::Insert => {
self.update_editor(window, cx, |_, editor, window, cx| {
self.update_editor(cx, |_, editor, cx| {
if let Some(register) = Vim::update_globals(cx, |globals, cx| {
globals.read_register(text.chars().next(), Some(editor), cx)
}) {
@ -1720,7 +1715,7 @@ impl Vim {
}
if self.mode == Mode::Normal {
self.update_editor(window, cx, |_, editor, window, cx| {
self.update_editor(cx, |_, editor, cx| {
editor.accept_edit_prediction(
&editor::actions::AcceptEditPrediction {},
window,
@ -1733,7 +1728,7 @@ impl Vim {
}
fn sync_vim_settings(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.update_editor(window, cx, |vim, editor, window, cx| {
self.update_editor(cx, |vim, editor, cx| {
editor.set_cursor_shape(vim.cursor_shape(cx), cx);
editor.set_clip_at_line_ends(vim.clip_at_line_ends(), cx);
editor.set_collapse_matches(true);

View file

@ -104,7 +104,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
let count = Vim::take_count(cx).unwrap_or(1);
Vim::take_forced_motion(cx);
for _ in 0..count {
vim.update_editor(window, cx, |_, editor, window, cx| {
vim.update_editor(cx, |_, editor, cx| {
editor.select_larger_syntax_node(&Default::default(), window, cx);
});
}
@ -117,7 +117,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
let count = Vim::take_count(cx).unwrap_or(1);
Vim::take_forced_motion(cx);
for _ in 0..count {
vim.update_editor(window, cx, |_, editor, window, cx| {
vim.update_editor(cx, |_, editor, cx| {
editor.select_smaller_syntax_node(&Default::default(), window, cx);
});
}
@ -129,7 +129,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
return;
};
let marks = vim
.update_editor(window, cx, |vim, editor, window, cx| {
.update_editor(cx, |vim, editor, cx| {
vim.get_mark("<", editor, window, cx)
.zip(vim.get_mark(">", editor, window, cx))
})
@ -148,7 +148,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
vim.create_visual_marks(vim.mode, window, cx);
}
vim.update_editor(window, cx, |_, editor, window, cx| {
vim.update_editor(cx, |_, editor, cx| {
editor.set_clip_at_line_ends(false, cx);
editor.change_selections(Default::default(), window, cx, |s| {
let map = s.display_map();
@ -189,7 +189,7 @@ impl Vim {
window: &mut Window,
cx: &mut Context<Self>,
) {
self.update_editor(window, cx, |vim, editor, window, cx| {
self.update_editor(cx, |vim, editor, cx| {
let text_layout_details = editor.text_layout_details(window);
if vim.mode == Mode::VisualBlock
&& !matches!(
@ -397,7 +397,7 @@ impl Vim {
self.switch_mode(target_mode, true, window, cx);
}
self.update_editor(window, cx, |_, editor, window, cx| {
self.update_editor(cx, |_, editor, cx| {
editor.change_selections(Default::default(), window, cx, |s| {
s.move_with(|map, selection| {
let mut mut_selection = selection.clone();
@ -475,7 +475,7 @@ impl Vim {
window: &mut Window,
cx: &mut Context<Self>,
) {
self.update_editor(window, cx, |_, editor, window, cx| {
self.update_editor(cx, |_, editor, cx| {
editor.split_selection_into_lines(&Default::default(), window, cx);
editor.change_selections(Default::default(), window, cx, |s| {
s.move_cursors_with(|map, cursor, _| {
@ -493,7 +493,7 @@ impl Vim {
window: &mut Window,
cx: &mut Context<Self>,
) {
self.update_editor(window, cx, |_, editor, window, cx| {
self.update_editor(cx, |_, editor, cx| {
editor.split_selection_into_lines(&Default::default(), window, cx);
editor.change_selections(Default::default(), window, cx, |s| {
s.move_cursors_with(|map, cursor, _| {
@ -517,7 +517,7 @@ impl Vim {
}
pub fn other_end(&mut self, _: &OtherEnd, window: &mut Window, cx: &mut Context<Self>) {
self.update_editor(window, cx, |_, editor, window, cx| {
self.update_editor(cx, |_, editor, cx| {
editor.change_selections(Default::default(), window, cx, |s| {
s.move_with(|_, selection| {
selection.reversed = !selection.reversed;
@ -533,7 +533,7 @@ impl Vim {
cx: &mut Context<Self>,
) {
let mode = self.mode;
self.update_editor(window, cx, |_, editor, window, cx| {
self.update_editor(cx, |_, editor, cx| {
editor.change_selections(Default::default(), window, cx, |s| {
s.move_with(|_, selection| {
selection.reversed = !selection.reversed;
@ -547,7 +547,7 @@ impl Vim {
pub fn visual_delete(&mut self, line_mode: bool, window: &mut Window, cx: &mut Context<Self>) {
self.store_visual_marks(window, cx);
self.update_editor(window, cx, |vim, editor, window, cx| {
self.update_editor(cx, |vim, editor, cx| {
let mut original_columns: HashMap<_, _> = Default::default();
let line_mode = line_mode || editor.selections.line_mode;
editor.selections.line_mode = false;
@ -631,7 +631,7 @@ impl Vim {
pub fn visual_yank(&mut self, line_mode: bool, window: &mut Window, cx: &mut Context<Self>) {
self.store_visual_marks(window, cx);
self.update_editor(window, cx, |vim, editor, window, cx| {
self.update_editor(cx, |vim, editor, cx| {
let line_mode = line_mode || editor.selections.line_mode;
// For visual line mode, adjust selections to avoid yanking the next line when on \n
@ -679,7 +679,7 @@ impl Vim {
cx: &mut Context<Self>,
) {
self.stop_recording(cx);
self.update_editor(window, cx, |_, editor, window, cx| {
self.update_editor(cx, |_, editor, cx| {
editor.transact(window, cx, |editor, window, cx| {
let (display_map, selections) = editor.selections.all_adjusted_display(cx);
@ -722,7 +722,7 @@ impl Vim {
Vim::take_forced_motion(cx);
let count =
Vim::take_count(cx).unwrap_or_else(|| if self.mode.is_visual() { 1 } else { 2 });
self.update_editor(window, cx, |_, editor, window, cx| {
self.update_editor(cx, |_, editor, cx| {
editor.set_clip_at_line_ends(false, cx);
for _ in 0..count {
if editor
@ -745,7 +745,7 @@ impl Vim {
Vim::take_forced_motion(cx);
let count =
Vim::take_count(cx).unwrap_or_else(|| if self.mode.is_visual() { 1 } else { 2 });
self.update_editor(window, cx, |_, editor, window, cx| {
self.update_editor(cx, |_, editor, cx| {
for _ in 0..count {
if editor
.select_previous(&Default::default(), window, cx)
@ -773,7 +773,7 @@ impl Vim {
let mut start_selection = 0usize;
let mut end_selection = 0usize;
self.update_editor(window, cx, |_, editor, _, _| {
self.update_editor(cx, |_, editor, _| {
editor.set_collapse_matches(false);
});
if vim_is_normal {
@ -791,7 +791,7 @@ impl Vim {
}
});
}
self.update_editor(window, cx, |_, editor, _, cx| {
self.update_editor(cx, |_, editor, cx| {
let latest = editor.selections.newest::<usize>(cx);
start_selection = latest.start;
end_selection = latest.end;
@ -812,7 +812,7 @@ impl Vim {
self.stop_replaying(cx);
return;
}
self.update_editor(window, cx, |_, editor, window, cx| {
self.update_editor(cx, |_, editor, cx| {
let latest = editor.selections.newest::<usize>(cx);
if vim_is_normal {
start_selection = latest.start;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 146 KiB

After

Width:  |  Height:  |  Size: 0 B

Before After
Before After

View file

@ -35,10 +35,7 @@ pub fn app_menus() -> Vec<Menu> {
],
}),
MenuItem::separator(),
MenuItem::submenu(Menu {
name: "Services".into(),
items: vec![],
}),
MenuItem::os_submenu("Services", gpui::SystemMenuType::Services),
MenuItem::separator(),
MenuItem::action("Extensions", zed_actions::Extensions::default()),
MenuItem::action("Install CLI", install_cli::Install),