Merge branch 'main' into 13194-autocompletion-documentation-scroll

This commit is contained in:
Conrad Irwin 2025-08-12 11:29:18 -06:00
commit c54fb933e1
80 changed files with 3400 additions and 878 deletions

View file

@ -0,0 +1,35 @@
name: Bug Report (Windows Alpha)
description: Zed Windows Alpha Related Bugs
type: "Bug"
labels: ["windows"]
title: "Windows Alpha: <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: with:
node-version: "18" 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 - name: Run tests
shell: powershell shell: powershell
working-directory: ${{ inputs.working-directory }} 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
}

6
Cargo.lock generated
View file

@ -29,6 +29,7 @@ dependencies = [
"tempfile", "tempfile",
"terminal", "terminal",
"ui", "ui",
"url",
"util", "util",
"workspace-hack", "workspace-hack",
] ]
@ -196,6 +197,7 @@ dependencies = [
"clock", "clock",
"cloud_llm_client", "cloud_llm_client",
"collections", "collections",
"context_server",
"ctor", "ctor",
"editor", "editor",
"env_logger 0.11.8", "env_logger 0.11.8",
@ -204,6 +206,8 @@ dependencies = [
"gpui", "gpui",
"gpui_tokio", "gpui_tokio",
"handlebars 4.5.0", "handlebars 4.5.0",
"html_to_markdown",
"http_client",
"indoc", "indoc",
"itertools 0.14.0", "itertools 0.14.0",
"language", "language",
@ -18023,6 +18027,7 @@ dependencies = [
"command_palette_hooks", "command_palette_hooks",
"db", "db",
"editor", "editor",
"env_logger 0.11.8",
"futures 0.3.31", "futures 0.3.31",
"git_ui", "git_ui",
"gpui", "gpui",
@ -20920,6 +20925,7 @@ dependencies = [
"menu", "menu",
"postage", "postage",
"project", "project",
"rand 0.8.5",
"regex", "regex",
"release_channel", "release_channel",
"reqwest_client", "reqwest_client",

View file

@ -333,10 +333,14 @@
"ctrl-x ctrl-c": "editor::ShowEditPrediction", // zed specific "ctrl-x ctrl-c": "editor::ShowEditPrediction", // zed specific
"ctrl-x ctrl-l": "editor::ToggleCodeActions", // zed specific "ctrl-x ctrl-l": "editor::ToggleCodeActions", // zed specific
"ctrl-x ctrl-z": "editor::Cancel", "ctrl-x ctrl-z": "editor::Cancel",
"ctrl-x ctrl-e": "vim::LineDown",
"ctrl-x ctrl-y": "vim::LineUp",
"ctrl-w": "editor::DeleteToPreviousWordStart", "ctrl-w": "editor::DeleteToPreviousWordStart",
"ctrl-u": "editor::DeleteToBeginningOfLine", "ctrl-u": "editor::DeleteToBeginningOfLine",
"ctrl-t": "vim::Indent", "ctrl-t": "vim::Indent",
"ctrl-d": "vim::Outdent", "ctrl-d": "vim::Outdent",
"ctrl-y": "vim::InsertFromAbove",
"ctrl-e": "vim::InsertFromBelow",
"ctrl-k": ["vim::PushDigraph", {}], "ctrl-k": ["vim::PushDigraph", {}],
"ctrl-v": ["vim::PushLiteral", {}], "ctrl-v": ["vim::PushLiteral", {}],
"ctrl-shift-v": "editor::Paste", // note: this is *very* similar to ctrl-v in vim, but ctrl-shift-v on linux is the typical shortcut for paste when ctrl-v is already in use. "ctrl-shift-v": "editor::Paste", // note: this is *very* similar to ctrl-v in vim, but ctrl-shift-v on linux is the typical shortcut for paste when ctrl-v is already in use.

View file

@ -34,6 +34,7 @@ settings.workspace = true
smol.workspace = true smol.workspace = true
terminal.workspace = true terminal.workspace = true
ui.workspace = true ui.workspace = true
url.workspace = true
util.workspace = true util.workspace = true
workspace-hack.workspace = true workspace-hack.workspace = true

View file

@ -1,13 +1,15 @@
mod connection; mod connection;
mod diff; mod diff;
mod mention;
mod terminal; mod terminal;
pub use connection::*; pub use connection::*;
pub use diff::*; pub use diff::*;
pub use mention::*;
pub use terminal::*; pub use terminal::*;
use action_log::ActionLog; use action_log::ActionLog;
use agent_client_protocol as acp; use agent_client_protocol::{self as acp};
use anyhow::{Context as _, Result}; use anyhow::{Context as _, Result};
use editor::Bias; use editor::Bias;
use futures::{FutureExt, channel::oneshot, future::BoxFuture}; use futures::{FutureExt, channel::oneshot, future::BoxFuture};
@ -21,12 +23,7 @@ use std::error::Error;
use std::fmt::Formatter; use std::fmt::Formatter;
use std::process::ExitStatus; use std::process::ExitStatus;
use std::rc::Rc; use std::rc::Rc;
use std::{ use std::{fmt::Display, mem, path::PathBuf, sync::Arc};
fmt::Display,
mem,
path::{Path, PathBuf},
sync::Arc,
};
use ui::App; use ui::App;
use util::ResultExt; use util::ResultExt;
@ -53,38 +50,6 @@ impl UserMessage {
} }
} }
#[derive(Debug)]
pub struct MentionPath<'a>(&'a Path);
impl<'a> MentionPath<'a> {
const PREFIX: &'static str = "@file:";
pub fn new(path: &'a Path) -> Self {
MentionPath(path)
}
pub fn try_parse(url: &'a str) -> Option<Self> {
let path = url.strip_prefix(Self::PREFIX)?;
Some(MentionPath(Path::new(path)))
}
pub fn path(&self) -> &Path {
self.0
}
}
impl Display for MentionPath<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"[@{}]({}{})",
self.0.file_name().unwrap_or_default().display(),
Self::PREFIX,
self.0.display()
)
}
}
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq)]
pub struct AssistantMessage { pub struct AssistantMessage {
pub chunks: Vec<AssistantMessageChunk>, pub chunks: Vec<AssistantMessageChunk>,
@ -254,6 +219,15 @@ impl ToolCall {
} }
if let Some(raw_output) = raw_output { if let Some(raw_output) = raw_output {
if self.content.is_empty() {
if let Some(markdown) = markdown_for_raw_output(&raw_output, &language_registry, cx)
{
self.content
.push(ToolCallContent::ContentBlock(ContentBlock::Markdown {
markdown,
}));
}
}
self.raw_output = Some(raw_output); self.raw_output = Some(raw_output);
} }
} }
@ -325,6 +299,7 @@ impl Display for ToolCallStatus {
pub enum ContentBlock { pub enum ContentBlock {
Empty, Empty,
Markdown { markdown: Entity<Markdown> }, Markdown { markdown: Entity<Markdown> },
ResourceLink { resource_link: acp::ResourceLink },
} }
impl ContentBlock { impl ContentBlock {
@ -356,36 +331,67 @@ impl ContentBlock {
language_registry: &Arc<LanguageRegistry>, language_registry: &Arc<LanguageRegistry>,
cx: &mut App, cx: &mut App,
) { ) {
let new_content = match block { if matches!(self, ContentBlock::Empty) {
acp::ContentBlock::Text(text_content) => text_content.text.clone(), if let acp::ContentBlock::ResourceLink(resource_link) = block {
acp::ContentBlock::ResourceLink(resource_link) => { *self = ContentBlock::ResourceLink { resource_link };
if let Some(path) = resource_link.uri.strip_prefix("file://") { return;
format!("{}", MentionPath(path.as_ref()))
} else {
resource_link.uri.clone()
}
} }
acp::ContentBlock::Image(_) }
| acp::ContentBlock::Audio(_)
| acp::ContentBlock::Resource(_) => String::new(), let new_content = self.extract_content_from_block(block);
};
match self { match self {
ContentBlock::Empty => { ContentBlock::Empty => {
*self = ContentBlock::Markdown { *self = Self::create_markdown_block(new_content, language_registry, cx);
markdown: cx.new(|cx| {
Markdown::new(
new_content.into(),
Some(language_registry.clone()),
None,
cx,
)
}),
};
} }
ContentBlock::Markdown { markdown } => { ContentBlock::Markdown { markdown } => {
markdown.update(cx, |markdown, cx| markdown.append(&new_content, cx)); markdown.update(cx, |markdown, cx| markdown.append(&new_content, cx));
} }
ContentBlock::ResourceLink { resource_link } => {
let existing_content = Self::resource_link_to_content(&resource_link.uri);
let combined = format!("{}\n{}", existing_content, new_content);
*self = Self::create_markdown_block(combined, language_registry, cx);
}
}
}
fn resource_link_to_content(uri: &str) -> String {
if let Some(uri) = MentionUri::parse(&uri).log_err() {
uri.to_link()
} else {
uri.to_string().clone()
}
}
fn create_markdown_block(
content: String,
language_registry: &Arc<LanguageRegistry>,
cx: &mut App,
) -> ContentBlock {
ContentBlock::Markdown {
markdown: cx
.new(|cx| Markdown::new(content.into(), Some(language_registry.clone()), None, cx)),
}
}
fn extract_content_from_block(&self, block: acp::ContentBlock) -> String {
match block {
acp::ContentBlock::Text(text_content) => text_content.text.clone(),
acp::ContentBlock::ResourceLink(resource_link) => {
Self::resource_link_to_content(&resource_link.uri)
}
acp::ContentBlock::Resource(acp::EmbeddedResource {
resource:
acp::EmbeddedResourceResource::TextResourceContents(acp::TextResourceContents {
uri,
..
}),
..
}) => Self::resource_link_to_content(&uri),
acp::ContentBlock::Image(_)
| acp::ContentBlock::Audio(_)
| acp::ContentBlock::Resource(_) => String::new(),
} }
} }
@ -393,6 +399,7 @@ impl ContentBlock {
match self { match self {
ContentBlock::Empty => "", ContentBlock::Empty => "",
ContentBlock::Markdown { markdown } => markdown.read(cx).source(), ContentBlock::Markdown { markdown } => markdown.read(cx).source(),
ContentBlock::ResourceLink { resource_link } => &resource_link.uri,
} }
} }
@ -400,6 +407,14 @@ impl ContentBlock {
match self { match self {
ContentBlock::Empty => None, ContentBlock::Empty => None,
ContentBlock::Markdown { markdown } => Some(markdown), ContentBlock::Markdown { markdown } => Some(markdown),
ContentBlock::ResourceLink { .. } => None,
}
}
pub fn resource_link(&self) -> Option<&acp::ResourceLink> {
match self {
ContentBlock::ResourceLink { resource_link } => Some(resource_link),
_ => None,
} }
} }
} }
@ -1266,6 +1281,48 @@ impl AcpThread {
} }
} }
fn markdown_for_raw_output(
raw_output: &serde_json::Value,
language_registry: &Arc<LanguageRegistry>,
cx: &mut App,
) -> Option<Entity<Markdown>> {
match raw_output {
serde_json::Value::Null => None,
serde_json::Value::Bool(value) => Some(cx.new(|cx| {
Markdown::new(
value.to_string().into(),
Some(language_registry.clone()),
None,
cx,
)
})),
serde_json::Value::Number(value) => Some(cx.new(|cx| {
Markdown::new(
value.to_string().into(),
Some(language_registry.clone()),
None,
cx,
)
})),
serde_json::Value::String(value) => Some(cx.new(|cx| {
Markdown::new(
value.clone().into(),
Some(language_registry.clone()),
None,
cx,
)
})),
value => Some(cx.new(|cx| {
Markdown::new(
format!("```json\n{}\n```", value).into(),
Some(language_registry.clone()),
None,
cx,
)
})),
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@ -1278,7 +1335,7 @@ mod tests {
use serde_json::json; use serde_json::json;
use settings::SettingsStore; use settings::SettingsStore;
use smol::stream::StreamExt as _; use smol::stream::StreamExt as _;
use std::{cell::RefCell, rc::Rc, time::Duration}; use std::{cell::RefCell, path::Path, rc::Rc, time::Duration};
use util::path; use util::path;

View file

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

View file

@ -0,0 +1,125 @@
use agent_client_protocol as acp;
use anyhow::{Result, bail};
use std::path::PathBuf;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum MentionUri {
File(PathBuf),
Symbol(PathBuf, String),
Thread(acp::SessionId),
Rule(String),
}
impl MentionUri {
pub fn parse(input: &str) -> Result<Self> {
let url = url::Url::parse(input)?;
let path = url.path();
match url.scheme() {
"file" => {
if let Some(fragment) = url.fragment() {
Ok(Self::Symbol(path.into(), fragment.into()))
} else {
let file_path =
PathBuf::from(format!("{}{}", url.host_str().unwrap_or(""), path));
Ok(Self::File(file_path))
}
}
"zed" => {
if let Some(thread) = path.strip_prefix("/agent/thread/") {
Ok(Self::Thread(acp::SessionId(thread.into())))
} else if let Some(rule) = path.strip_prefix("/agent/rule/") {
Ok(Self::Rule(rule.into()))
} else {
bail!("invalid zed url: {:?}", input);
}
}
other => bail!("unrecognized scheme {:?}", other),
}
}
pub fn name(&self) -> String {
match self {
MentionUri::File(path) => path.file_name().unwrap().to_string_lossy().into_owned(),
MentionUri::Symbol(_path, name) => name.clone(),
MentionUri::Thread(thread) => thread.to_string(),
MentionUri::Rule(rule) => rule.clone(),
}
}
pub fn to_link(&self) -> String {
let name = self.name();
let uri = self.to_uri();
format!("[{name}]({uri})")
}
pub fn to_uri(&self) -> String {
match self {
MentionUri::File(path) => {
format!("file://{}", path.display())
}
MentionUri::Symbol(path, name) => {
format!("file://{}#{}", path.display(), name)
}
MentionUri::Thread(thread) => {
format!("zed:///agent/thread/{}", thread.0)
}
MentionUri::Rule(rule) => {
format!("zed:///agent/rule/{}", rule)
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_mention_uri_parse_and_display() {
// Test file URI
let file_uri = "file:///path/to/file.rs";
let parsed = MentionUri::parse(file_uri).unwrap();
match &parsed {
MentionUri::File(path) => assert_eq!(path.to_str().unwrap(), "/path/to/file.rs"),
_ => panic!("Expected File variant"),
}
assert_eq!(parsed.to_uri(), file_uri);
// Test symbol URI
let symbol_uri = "file:///path/to/file.rs#MySymbol";
let parsed = MentionUri::parse(symbol_uri).unwrap();
match &parsed {
MentionUri::Symbol(path, symbol) => {
assert_eq!(path.to_str().unwrap(), "/path/to/file.rs");
assert_eq!(symbol, "MySymbol");
}
_ => panic!("Expected Symbol variant"),
}
assert_eq!(parsed.to_uri(), symbol_uri);
// Test thread URI
let thread_uri = "zed:///agent/thread/session123";
let parsed = MentionUri::parse(thread_uri).unwrap();
match &parsed {
MentionUri::Thread(session_id) => assert_eq!(session_id.0.as_ref(), "session123"),
_ => panic!("Expected Thread variant"),
}
assert_eq!(parsed.to_uri(), thread_uri);
// Test rule URI
let rule_uri = "zed:///agent/rule/my_rule";
let parsed = MentionUri::parse(rule_uri).unwrap();
match &parsed {
MentionUri::Rule(rule) => assert_eq!(rule, "my_rule"),
_ => panic!("Expected Rule variant"),
}
assert_eq!(parsed.to_uri(), rule_uri);
// Test invalid scheme
assert!(MentionUri::parse("http://example.com").is_err());
// Test invalid zed path
assert!(MentionUri::parse("zed:///invalid/path").is_err());
}
}

View file

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

View file

@ -17,8 +17,6 @@ use util::{
pub struct ActionLog { pub struct ActionLog {
/// Buffers that we want to notify the model about when they change. /// Buffers that we want to notify the model about when they change.
tracked_buffers: BTreeMap<Entity<Buffer>, TrackedBuffer>, 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 /// The project this action log is associated with
project: Entity<Project>, project: Entity<Project>,
} }
@ -28,7 +26,6 @@ impl ActionLog {
pub fn new(project: Entity<Project>) -> Self { pub fn new(project: Entity<Project>) -> Self {
Self { Self {
tracked_buffers: BTreeMap::default(), tracked_buffers: BTreeMap::default(),
edited_since_project_diagnostics_check: false,
project, project,
} }
} }
@ -37,16 +34,6 @@ impl ActionLog {
&self.project &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> { pub fn latest_snapshot(&self, buffer: &Entity<Buffer>) -> Option<text::BufferSnapshot> {
Some(self.tracked_buffers.get(buffer)?.snapshot.clone()) 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 /// 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>) { 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); self.track_buffer_internal(buffer.clone(), true, cx);
} }
/// Mark a buffer as edited by agent, so we can refresh it in the context /// 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>) { 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); let tracked_buffer = self.track_buffer_internal(buffer.clone(), false, cx);
if let TrackedBufferStatus::Deleted = tracked_buffer.status { if let TrackedBufferStatus::Deleted = tracked_buffer.status {
tracked_buffer.status = TrackedBufferStatus::Modified; tracked_buffer.status = TrackedBufferStatus::Modified;

View file

@ -23,10 +23,13 @@ assistant_tools.workspace = true
chrono.workspace = true chrono.workspace = true
cloud_llm_client.workspace = true cloud_llm_client.workspace = true
collections.workspace = true collections.workspace = true
context_server.workspace = true
fs.workspace = true fs.workspace = true
futures.workspace = true futures.workspace = true
gpui.workspace = true gpui.workspace = true
handlebars = { workspace = true, features = ["rust-embed"] } handlebars = { workspace = true, features = ["rust-embed"] }
html_to_markdown.workspace = true
http_client.workspace = true
indoc.workspace = true indoc.workspace = true
itertools.workspace = true itertools.workspace = true
language.workspace = true language.workspace = true
@ -58,6 +61,7 @@ workspace-hack.workspace = true
ctor.workspace = true ctor.workspace = true
client = { workspace = true, "features" = ["test-support"] } client = { workspace = true, "features" = ["test-support"] }
clock = { workspace = true, "features" = ["test-support"] } clock = { workspace = true, "features" = ["test-support"] }
context_server = { workspace = true, "features" = ["test-support"] }
editor = { workspace = true, "features" = ["test-support"] } editor = { workspace = true, "features" = ["test-support"] }
env_logger.workspace = true env_logger.workspace = true
fs = { workspace = true, "features" = ["test-support"] } fs = { workspace = true, "features" = ["test-support"] }

View file

@ -1,8 +1,8 @@
use crate::{AgentResponseEvent, Thread, templates::Templates}; use crate::{AgentResponseEvent, Thread, templates::Templates};
use crate::{ use crate::{
CopyPathTool, CreateDirectoryTool, EditFileTool, FindPathTool, GrepTool, ListDirectoryTool, ContextServerRegistry, CopyPathTool, CreateDirectoryTool, DiagnosticsTool, EditFileTool,
MovePathTool, NowTool, OpenTool, ReadFileTool, TerminalTool, ThinkingTool, FetchTool, FindPathTool, GrepTool, ListDirectoryTool, MessageContent, MovePathTool, NowTool,
ToolCallAuthorization, WebSearchTool, OpenTool, ReadFileTool, TerminalTool, ThinkingTool, ToolCallAuthorization, WebSearchTool,
}; };
use acp_thread::ModelSelector; use acp_thread::ModelSelector;
use agent_client_protocol as acp; use agent_client_protocol as acp;
@ -55,6 +55,7 @@ pub struct NativeAgent {
project_context: Rc<RefCell<ProjectContext>>, project_context: Rc<RefCell<ProjectContext>>,
project_context_needs_refresh: watch::Sender<()>, project_context_needs_refresh: watch::Sender<()>,
_maintain_project_context: Task<Result<()>>, _maintain_project_context: Task<Result<()>>,
context_server_registry: Entity<ContextServerRegistry>,
/// Shared templates for all threads /// Shared templates for all threads
templates: Arc<Templates>, templates: Arc<Templates>,
project: Entity<Project>, project: Entity<Project>,
@ -90,6 +91,9 @@ impl NativeAgent {
_maintain_project_context: cx.spawn(async move |this, cx| { _maintain_project_context: cx.spawn(async move |this, cx| {
Self::maintain_project_context(this, project_context_needs_refresh_rx, cx).await Self::maintain_project_context(this, project_context_needs_refresh_rx, cx).await
}), }),
context_server_registry: cx.new(|cx| {
ContextServerRegistry::new(project.read(cx).context_server_store(), cx)
}),
templates, templates,
project, project,
prompt_store, prompt_store,
@ -385,7 +389,13 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
// Create AcpThread // Create AcpThread
let acp_thread = cx.update(|cx| { let acp_thread = cx.update(|cx| {
cx.new(|cx| { cx.new(|cx| {
acp_thread::AcpThread::new("agent2", self.clone(), project.clone(), session_id.clone(), cx) acp_thread::AcpThread::new(
"agent2",
self.clone(),
project.clone(),
session_id.clone(),
cx,
)
}) })
})?; })?;
let action_log = cx.update(|cx| acp_thread.read(cx).action_log().clone())?; let action_log = cx.update(|cx| acp_thread.read(cx).action_log().clone())?;
@ -413,18 +423,30 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
}) })
.ok_or_else(|| { .ok_or_else(|| {
log::warn!("No default model configured in settings"); log::warn!("No default model configured in settings");
anyhow!("No default model configured. Please configure a default model in settings.") anyhow!(
"No default model. Please configure a default model in settings."
)
})?; })?;
let thread = cx.new(|cx| { let thread = cx.new(|cx| {
let mut thread = Thread::new(project.clone(), agent.project_context.clone(), action_log.clone(), agent.templates.clone(), default_model); let mut thread = Thread::new(
project.clone(),
agent.project_context.clone(),
agent.context_server_registry.clone(),
action_log.clone(),
agent.templates.clone(),
default_model,
cx,
);
thread.add_tool(CreateDirectoryTool::new(project.clone())); thread.add_tool(CreateDirectoryTool::new(project.clone()));
thread.add_tool(CopyPathTool::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(MovePathTool::new(project.clone()));
thread.add_tool(ListDirectoryTool::new(project.clone())); thread.add_tool(ListDirectoryTool::new(project.clone()));
thread.add_tool(OpenTool::new(project.clone())); thread.add_tool(OpenTool::new(project.clone()));
thread.add_tool(ThinkingTool); thread.add_tool(ThinkingTool);
thread.add_tool(FindPathTool::new(project.clone())); 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(GrepTool::new(project.clone()));
thread.add_tool(ReadFileTool::new(project.clone(), action_log)); thread.add_tool(ReadFileTool::new(project.clone(), action_log));
thread.add_tool(EditFileTool::new(cx.entity())); thread.add_tool(EditFileTool::new(cx.entity()));
@ -448,7 +470,7 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
acp_thread: acp_thread.downgrade(), acp_thread: acp_thread.downgrade(),
_subscription: cx.observe_release(&acp_thread, |this, acp_thread, _cx| { _subscription: cx.observe_release(&acp_thread, |this, acp_thread, _cx| {
this.sessions.remove(acp_thread.session_id()); this.sessions.remove(acp_thread.session_id());
}) }),
}, },
); );
})?; })?;
@ -494,10 +516,13 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
})?; })?;
log::debug!("Found session for: {}", session_id); log::debug!("Found session for: {}", session_id);
// Convert prompt to message let message: Vec<MessageContent> = params
let message = convert_prompt_to_message(params.prompt); .prompt
.into_iter()
.map(Into::into)
.collect::<Vec<_>>();
log::info!("Converted prompt to message: {} chars", message.len()); log::info!("Converted prompt to message: {} chars", message.len());
log::debug!("Message content: {}", message); log::debug!("Message content: {:?}", message);
// Get model using the ModelSelector capability (always available for agent2) // Get model using the ModelSelector capability (always available for agent2)
// Get the selected model from the thread directly // Get the selected model from the thread directly
@ -601,39 +626,6 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
} }
} }
/// Convert ACP content blocks to a message string
fn convert_prompt_to_message(blocks: Vec<acp::ContentBlock>) -> String {
log::debug!("Converting {} content blocks to message", blocks.len());
let mut message = String::new();
for block in blocks {
match block {
acp::ContentBlock::Text(text) => {
log::trace!("Processing text block: {} chars", text.text.len());
message.push_str(&text.text);
}
acp::ContentBlock::ResourceLink(link) => {
log::trace!("Processing resource link: {}", link.uri);
message.push_str(&format!(" @{} ", link.uri));
}
acp::ContentBlock::Image(_) => {
log::trace!("Processing image block");
message.push_str(" [image] ");
}
acp::ContentBlock::Audio(_) => {
log::trace!("Processing audio block");
message.push_str(" [audio] ");
}
acp::ContentBlock::Resource(resource) => {
log::trace!("Processing resource block: {:?}", resource.resource);
message.push_str(&format!(" [resource: {:?}] ", resource.resource));
}
}
}
message
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;

View file

@ -1,17 +1,21 @@
use super::*; use super::*;
use crate::MessageContent;
use acp_thread::AgentConnection; use acp_thread::AgentConnection;
use action_log::ActionLog; use action_log::ActionLog;
use agent_client_protocol::{self as acp}; use agent_client_protocol::{self as acp};
use agent_settings::AgentProfileId;
use anyhow::Result; use anyhow::Result;
use client::{Client, UserStore}; use client::{Client, UserStore};
use fs::FakeFs; use fs::{FakeFs, Fs};
use futures::channel::mpsc::UnboundedReceiver; 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 indoc::indoc;
use language_model::{ use language_model::{
LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelId, LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelId,
LanguageModelRegistry, LanguageModelToolResult, LanguageModelToolUse, MessageContent, Role, LanguageModelRegistry, LanguageModelToolResult, LanguageModelToolUse, Role, StopReason,
StopReason, fake_provider::FakeLanguageModel, fake_provider::FakeLanguageModel,
}; };
use project::Project; use project::Project;
use prompt_store::ProjectContext; use prompt_store::ProjectContext;
@ -19,6 +23,7 @@ use reqwest_client::ReqwestClient;
use schemars::JsonSchema; use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::json; use serde_json::json;
use settings::SettingsStore;
use smol::stream::StreamExt; use smol::stream::StreamExt;
use std::{cell::RefCell, path::Path, rc::Rc, sync::Arc, time::Duration}; use std::{cell::RefCell, path::Path, rc::Rc, sync::Arc, time::Duration};
use util::path; use util::path;
@ -162,7 +167,9 @@ async fn test_basic_tool_calls(cx: &mut TestAppContext) {
} else { } else {
false false
} }
}) }),
"{}",
thread.to_markdown()
); );
}); });
} }
@ -266,14 +273,14 @@ async fn test_tool_authorization(cx: &mut TestAppContext) {
assert_eq!( assert_eq!(
message.content, message.content,
vec![ vec![
MessageContent::ToolResult(LanguageModelToolResult { language_model::MessageContent::ToolResult(LanguageModelToolResult {
tool_use_id: tool_call_auth_1.tool_call.id.0.to_string().into(), tool_use_id: tool_call_auth_1.tool_call.id.0.to_string().into(),
tool_name: ToolRequiringPermission.name().into(), tool_name: ToolRequiringPermission.name().into(),
is_error: false, is_error: false,
content: "Allowed".into(), content: "Allowed".into(),
output: Some("Allowed".into()) output: Some("Allowed".into())
}), }),
MessageContent::ToolResult(LanguageModelToolResult { language_model::MessageContent::ToolResult(LanguageModelToolResult {
tool_use_id: tool_call_auth_2.tool_call.id.0.to_string().into(), tool_use_id: tool_call_auth_2.tool_call.id.0.to_string().into(),
tool_name: ToolRequiringPermission.name().into(), tool_name: ToolRequiringPermission.name().into(),
is_error: true, is_error: true,
@ -282,6 +289,67 @@ 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![language_model::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![language_model::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] #[gpui::test]
@ -409,6 +477,82 @@ async fn test_concurrent_tool_calls(cx: &mut TestAppContext) {
}); });
} }
#[gpui::test]
async fn test_profiles(cx: &mut TestAppContext) {
let ThreadTest {
model, thread, fs, ..
} = setup(cx, TestModel::Fake).await;
let fake_model = model.as_fake();
thread.update(cx, |thread, _cx| {
thread.add_tool(DelayTool);
thread.add_tool(EchoTool);
thread.add_tool(InfiniteTool);
});
// Override profiles and wait for settings to be loaded.
fs.insert_file(
paths::settings_file(),
json!({
"agent": {
"profiles": {
"test-1": {
"name": "Test Profile 1",
"tools": {
EchoTool.name(): true,
DelayTool.name(): true,
}
},
"test-2": {
"name": "Test Profile 2",
"tools": {
InfiniteTool.name(): true,
}
}
}
}
})
.to_string()
.into_bytes(),
)
.await;
cx.run_until_parked();
// Test that test-1 profile (default) has echo and delay tools
thread.update(cx, |thread, cx| {
thread.set_profile(AgentProfileId("test-1".into()));
thread.send("test", cx);
});
cx.run_until_parked();
let mut pending_completions = fake_model.pending_completions();
assert_eq!(pending_completions.len(), 1);
let completion = pending_completions.pop().unwrap();
let tool_names: Vec<String> = completion
.tools
.iter()
.map(|tool| tool.name.clone())
.collect();
assert_eq!(tool_names, vec![DelayTool.name(), EchoTool.name()]);
fake_model.end_last_completion_stream();
// Switch to test-2 profile, and verify that it has only the infinite tool.
thread.update(cx, |thread, cx| {
thread.set_profile(AgentProfileId("test-2".into()));
thread.send("test2", cx)
});
cx.run_until_parked();
let mut pending_completions = fake_model.pending_completions();
assert_eq!(pending_completions.len(), 1);
let completion = pending_completions.pop().unwrap();
let tool_names: Vec<String> = completion
.tools
.iter()
.map(|tool| tool.name.clone())
.collect();
assert_eq!(tool_names, vec![InfiniteTool.name()]);
}
#[gpui::test] #[gpui::test]
#[ignore = "can't run on CI yet"] #[ignore = "can't run on CI yet"]
async fn test_cancellation(cx: &mut TestAppContext) { async fn test_cancellation(cx: &mut TestAppContext) {
@ -535,6 +679,7 @@ async fn test_agent_connection(cx: &mut TestAppContext) {
language_models::init(user_store.clone(), client.clone(), cx); language_models::init(user_store.clone(), client.clone(), cx);
Project::init_settings(cx); Project::init_settings(cx);
LanguageModelRegistry::test(cx); LanguageModelRegistry::test(cx);
agent_settings::init(cx);
}); });
cx.executor().forbid_parking(); cx.executor().forbid_parking();
@ -730,6 +875,7 @@ async fn test_tool_updates_to_completion(cx: &mut TestAppContext) {
id: acp::ToolCallId("1".into()), id: acp::ToolCallId("1".into()),
fields: acp::ToolCallUpdateFields { fields: acp::ToolCallUpdateFields {
status: Some(acp::ToolCallStatus::Completed), status: Some(acp::ToolCallStatus::Completed),
raw_output: Some("Finished thinking.".into()),
..Default::default() ..Default::default()
}, },
} }
@ -753,6 +899,7 @@ struct ThreadTest {
model: Arc<dyn LanguageModel>, model: Arc<dyn LanguageModel>,
thread: Entity<Thread>, thread: Entity<Thread>,
project_context: Rc<RefCell<ProjectContext>>, project_context: Rc<RefCell<ProjectContext>>,
fs: Arc<FakeFs>,
} }
enum TestModel { enum TestModel {
@ -773,28 +920,59 @@ impl TestModel {
async fn setup(cx: &mut TestAppContext, model: TestModel) -> ThreadTest { async fn setup(cx: &mut TestAppContext, model: TestModel) -> ThreadTest {
cx.executor().allow_parking(); cx.executor().allow_parking();
let fs = FakeFs::new(cx.background_executor.clone());
fs.create_dir(paths::settings_file().parent().unwrap())
.await
.unwrap();
fs.insert_file(
paths::settings_file(),
json!({
"agent": {
"default_profile": "test-profile",
"profiles": {
"test-profile": {
"name": "Test Profile",
"tools": {
EchoTool.name(): true,
DelayTool.name(): true,
WordListTool.name(): true,
ToolRequiringPermission.name(): true,
InfiniteTool.name(): true,
}
}
}
}
})
.to_string()
.into_bytes(),
)
.await;
cx.update(|cx| { cx.update(|cx| {
settings::init(cx); settings::init(cx);
Project::init_settings(cx); Project::init_settings(cx);
agent_settings::init(cx);
gpui_tokio::init(cx);
let http_client = ReqwestClient::user_agent("agent tests").unwrap();
cx.set_http_client(Arc::new(http_client));
client::init_settings(cx);
let client = Client::production(cx);
let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
language_model::init(client.clone(), cx);
language_models::init(user_store.clone(), client.clone(), cx);
watch_settings(fs.clone(), cx);
}); });
let templates = Templates::new(); let templates = Templates::new();
let fs = FakeFs::new(cx.background_executor.clone());
fs.insert_tree(path!("/test"), json!({})).await; fs.insert_tree(path!("/test"), json!({})).await;
let project = Project::test(fs, [path!("/test").as_ref()], cx).await; let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await;
let model = cx let model = cx
.update(|cx| { .update(|cx| {
gpui_tokio::init(cx);
let http_client = ReqwestClient::user_agent("agent tests").unwrap();
cx.set_http_client(Arc::new(http_client));
client::init_settings(cx);
let client = Client::production(cx);
let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
language_model::init(client.clone(), cx);
language_models::init(user_store.clone(), client.clone(), cx);
if let TestModel::Fake = model { if let TestModel::Fake = model {
Task::ready(Arc::new(FakeLanguageModel::default()) as Arc<_>) Task::ready(Arc::new(FakeLanguageModel::default()) as Arc<_>)
} else { } else {
@ -817,20 +995,25 @@ async fn setup(cx: &mut TestAppContext, model: TestModel) -> ThreadTest {
.await; .await;
let project_context = Rc::new(RefCell::new(ProjectContext::default())); let project_context = Rc::new(RefCell::new(ProjectContext::default()));
let context_server_registry =
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
let action_log = cx.new(|_| ActionLog::new(project.clone())); let action_log = cx.new(|_| ActionLog::new(project.clone()));
let thread = cx.new(|_| { let thread = cx.new(|cx| {
Thread::new( Thread::new(
project, project,
project_context.clone(), project_context.clone(),
context_server_registry,
action_log, action_log,
templates, templates,
model.clone(), model.clone(),
cx,
) )
}); });
ThreadTest { ThreadTest {
model, model,
thread, thread,
project_context, project_context,
fs,
} }
} }
@ -841,3 +1024,26 @@ fn init_logger() {
env_logger::init(); 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, event_stream: ToolCallEventStream,
cx: &mut App, cx: &mut App,
) -> Task<Result<String>> { ) -> Task<Result<String>> {
let auth_check = event_stream.authorize("Authorize?".into()); let authorize = event_stream.authorize("Authorize?", cx);
cx.foreground_executor().spawn(async move { cx.foreground_executor().spawn(async move {
auth_check.await?; authorize.await?;
Ok("Allowed".to_string()) Ok("Allowed".to_string())
}) })
} }

View file

@ -1,28 +1,33 @@
use crate::{SystemPromptTemplate, Template, Templates}; use crate::{ContextServerRegistry, SystemPromptTemplate, Template, Templates};
use acp_thread::MentionUri;
use action_log::ActionLog; use action_log::ActionLog;
use agent_client_protocol as acp; use agent_client_protocol as acp;
use agent_settings::{AgentProfileId, AgentSettings};
use anyhow::{Context as _, Result, anyhow}; use anyhow::{Context as _, Result, anyhow};
use assistant_tool::adapt_schema_to_format; use assistant_tool::adapt_schema_to_format;
use cloud_llm_client::{CompletionIntent, CompletionMode}; use cloud_llm_client::{CompletionIntent, CompletionMode};
use collections::HashMap; use collections::HashMap;
use fs::Fs;
use futures::{ use futures::{
channel::{mpsc, oneshot}, channel::{mpsc, oneshot},
stream::FuturesUnordered, stream::FuturesUnordered,
}; };
use gpui::{App, Context, Entity, SharedString, Task}; use gpui::{App, Context, Entity, SharedString, Task};
use language_model::{ use language_model::{
LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelImage,
LanguageModelRequest, LanguageModelRequestMessage, LanguageModelRequestTool, LanguageModelRequest, LanguageModelRequestMessage, LanguageModelRequestTool,
LanguageModelToolResult, LanguageModelToolResultContent, LanguageModelToolSchemaFormat, LanguageModelToolResult, LanguageModelToolResultContent, LanguageModelToolSchemaFormat,
LanguageModelToolUse, LanguageModelToolUseId, MessageContent, Role, StopReason, LanguageModelToolUse, LanguageModelToolUseId, Role, StopReason,
}; };
use log; use log;
use project::Project; use project::Project;
use prompt_store::ProjectContext; use prompt_store::ProjectContext;
use schemars::{JsonSchema, Schema}; use schemars::{JsonSchema, Schema};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use settings::{Settings, update_settings_file};
use smol::stream::StreamExt; use smol::stream::StreamExt;
use std::{cell::RefCell, collections::BTreeMap, fmt::Write, future::Future, rc::Rc, sync::Arc}; use std::fmt::Write;
use std::{cell::RefCell, collections::BTreeMap, path::Path, rc::Rc, sync::Arc};
use util::{ResultExt, markdown::MarkdownCodeBlock}; use util::{ResultExt, markdown::MarkdownCodeBlock};
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -31,6 +36,23 @@ pub struct AgentMessage {
pub content: Vec<MessageContent>, pub content: Vec<MessageContent>,
} }
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MessageContent {
Text(String),
Thinking {
text: String,
signature: Option<String>,
},
Mention {
uri: MentionUri,
content: String,
},
RedactedThinking(String),
Image(LanguageModelImage),
ToolUse(LanguageModelToolUse),
ToolResult(LanguageModelToolResult),
}
impl AgentMessage { impl AgentMessage {
pub fn to_markdown(&self) -> String { pub fn to_markdown(&self) -> String {
let mut markdown = format!("## {}\n", self.role); let mut markdown = format!("## {}\n", self.role);
@ -90,6 +112,9 @@ impl AgentMessage {
.unwrap(); .unwrap();
} }
} }
MessageContent::Mention { uri, .. } => {
write!(markdown, "{}", uri.to_link()).ok();
}
} }
} }
@ -123,6 +148,8 @@ pub struct Thread {
running_turn: Option<Task<()>>, running_turn: Option<Task<()>>,
pending_tool_uses: HashMap<LanguageModelToolUseId, LanguageModelToolUse>, pending_tool_uses: HashMap<LanguageModelToolUseId, LanguageModelToolUse>,
tools: BTreeMap<SharedString, Arc<dyn AnyAgentTool>>, tools: BTreeMap<SharedString, Arc<dyn AnyAgentTool>>,
context_server_registry: Entity<ContextServerRegistry>,
profile_id: AgentProfileId,
project_context: Rc<RefCell<ProjectContext>>, project_context: Rc<RefCell<ProjectContext>>,
templates: Arc<Templates>, templates: Arc<Templates>,
pub selected_model: Arc<dyn LanguageModel>, pub selected_model: Arc<dyn LanguageModel>,
@ -134,16 +161,21 @@ impl Thread {
pub fn new( pub fn new(
project: Entity<Project>, project: Entity<Project>,
project_context: Rc<RefCell<ProjectContext>>, project_context: Rc<RefCell<ProjectContext>>,
context_server_registry: Entity<ContextServerRegistry>,
action_log: Entity<ActionLog>, action_log: Entity<ActionLog>,
templates: Arc<Templates>, templates: Arc<Templates>,
default_model: Arc<dyn LanguageModel>, default_model: Arc<dyn LanguageModel>,
cx: &mut Context<Self>,
) -> Self { ) -> Self {
let profile_id = AgentSettings::get_global(cx).default_profile.clone();
Self { Self {
messages: Vec::new(), messages: Vec::new(),
completion_mode: CompletionMode::Normal, completion_mode: CompletionMode::Normal,
running_turn: None, running_turn: None,
pending_tool_uses: HashMap::default(), pending_tool_uses: HashMap::default(),
tools: BTreeMap::default(), tools: BTreeMap::default(),
context_server_registry,
profile_id,
project_context, project_context,
templates, templates,
selected_model: default_model, selected_model: default_model,
@ -176,6 +208,10 @@ impl Thread {
self.tools.remove(name).is_some() self.tools.remove(name).is_some()
} }
pub fn set_profile(&mut self, profile_id: AgentProfileId) {
self.profile_id = profile_id;
}
pub fn cancel(&mut self) { pub fn cancel(&mut self) {
self.running_turn.take(); self.running_turn.take();
@ -200,10 +236,11 @@ impl Thread {
/// The returned channel will report all the occurrences in which the model stops before erroring or ending its turn. /// The returned channel will report all the occurrences in which the model stops before erroring or ending its turn.
pub fn send( pub fn send(
&mut self, &mut self,
content: impl Into<MessageContent>, content: impl Into<UserMessage>,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> mpsc::UnboundedReceiver<Result<AgentResponseEvent, LanguageModelCompletionError>> { ) -> mpsc::UnboundedReceiver<Result<AgentResponseEvent, LanguageModelCompletionError>> {
let content = content.into(); let content = content.into().0;
let model = self.selected_model.clone(); let model = self.selected_model.clone();
log::info!("Thread::send called with model: {:?}", model.name()); log::info!("Thread::send called with model: {:?}", model.name());
log::debug!("Thread::send content: {:?}", content); log::debug!("Thread::send content: {:?}", content);
@ -216,7 +253,7 @@ impl Thread {
let user_message_ix = self.messages.len(); let user_message_ix = self.messages.len();
self.messages.push(AgentMessage { self.messages.push(AgentMessage {
role: Role::User, role: Role::User,
content: vec![content], content,
}); });
log::info!("Total messages in thread: {}", self.messages.len()); log::info!("Total messages in thread: {}", self.messages.len());
self.running_turn = Some(cx.spawn(async move |thread, cx| { self.running_turn = Some(cx.spawn(async move |thread, cx| {
@ -295,6 +332,7 @@ impl Thread {
} else { } else {
acp::ToolCallStatus::Completed acp::ToolCallStatus::Completed
}), }),
raw_output: tool_result.output.clone(),
..Default::default() ..Default::default()
}, },
); );
@ -338,7 +376,7 @@ impl Thread {
log::debug!("System message built"); log::debug!("System message built");
AgentMessage { AgentMessage {
role: Role::System, role: Role::System,
content: vec![prompt.into()], content: vec![prompt.as_str().into()],
} }
} }
@ -506,8 +544,9 @@ impl Thread {
})); }));
}; };
let fs = self.project.read(cx).fs().clone();
let tool_event_stream = 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 { tool_event_stream.update_fields(acp::ToolCallUpdateFields {
status: Some(acp::ToolCallStatus::InProgress), status: Some(acp::ToolCallStatus::InProgress),
..Default::default() ..Default::default()
@ -600,21 +639,23 @@ impl Thread {
let messages = self.build_request_messages(); let messages = self.build_request_messages();
log::info!("Request will include {} messages", messages.len()); log::info!("Request will include {} messages", messages.len());
let tools: Vec<LanguageModelRequestTool> = self let tools = if let Some(tools) = self.tools(cx).log_err() {
.tools tools
.values() .filter_map(|tool| {
.filter_map(|tool| { let tool_name = tool.name().to_string();
let tool_name = tool.name().to_string(); log::trace!("Including tool: {}", tool_name);
log::trace!("Including tool: {}", tool_name); Some(LanguageModelRequestTool {
Some(LanguageModelRequestTool { name: tool_name,
name: tool_name, description: tool.description().to_string(),
description: tool.description(cx).to_string(), input_schema: tool
input_schema: tool .input_schema(self.selected_model.tool_input_format())
.input_schema(self.selected_model.tool_input_format()) .log_err()?,
.log_err()?, })
}) })
}) .collect()
.collect(); } else {
Vec::new()
};
log::info!("Request includes {} tools", tools.len()); log::info!("Request includes {} tools", tools.len());
@ -635,6 +676,35 @@ impl Thread {
request request
} }
fn tools<'a>(&'a self, cx: &'a App) -> Result<impl Iterator<Item = &'a Arc<dyn AnyAgentTool>>> {
let profile = AgentSettings::get_global(cx)
.profiles
.get(&self.profile_id)
.context("profile not found")?;
Ok(self
.tools
.iter()
.filter_map(|(tool_name, tool)| {
if profile.is_tool_enabled(tool_name) {
Some(tool)
} else {
None
}
})
.chain(self.context_server_registry.read(cx).servers().flat_map(
|(server_id, tools)| {
tools.iter().filter_map(|(tool_name, tool)| {
if profile.is_context_server_tool_enabled(&server_id.0, tool_name) {
Some(tool)
} else {
None
}
})
},
)))
}
fn build_request_messages(&self) -> Vec<LanguageModelRequestMessage> { fn build_request_messages(&self) -> Vec<LanguageModelRequestMessage> {
log::trace!( log::trace!(
"Building request messages from {} thread messages", "Building request messages from {} thread messages",
@ -654,11 +724,7 @@ impl Thread {
}, },
message.content.len() message.content.len()
); );
LanguageModelRequestMessage { message.to_request()
role: message.role,
content: message.content.clone(),
cache: false,
}
}) })
.collect(); .collect();
messages messages
@ -673,6 +739,20 @@ impl Thread {
} }
} }
pub struct UserMessage(Vec<MessageContent>);
impl From<Vec<MessageContent>> for UserMessage {
fn from(content: Vec<MessageContent>) -> Self {
UserMessage(content)
}
}
impl<T: Into<MessageContent>> From<T> for UserMessage {
fn from(content: T) -> Self {
UserMessage(vec![content.into()])
}
}
pub trait AgentTool pub trait AgentTool
where where
Self: 'static + Sized, Self: 'static + Sized,
@ -682,7 +762,7 @@ where
fn name(&self) -> SharedString; fn name(&self) -> SharedString;
fn description(&self, _cx: &mut App) -> SharedString { fn description(&self) -> SharedString {
let schema = schemars::schema_for!(Self::Input); let schema = schemars::schema_for!(Self::Input);
SharedString::new( SharedString::new(
schema schema
@ -718,13 +798,13 @@ where
pub struct Erased<T>(T); pub struct Erased<T>(T);
pub struct AgentToolOutput { pub struct AgentToolOutput {
llm_output: LanguageModelToolResultContent, pub llm_output: LanguageModelToolResultContent,
raw_output: serde_json::Value, pub raw_output: serde_json::Value,
} }
pub trait AnyAgentTool { pub trait AnyAgentTool {
fn name(&self) -> SharedString; fn name(&self) -> SharedString;
fn description(&self, cx: &mut App) -> SharedString; fn description(&self) -> SharedString;
fn kind(&self) -> acp::ToolKind; fn kind(&self) -> acp::ToolKind;
fn initial_title(&self, input: serde_json::Value) -> SharedString; fn initial_title(&self, input: serde_json::Value) -> SharedString;
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value>; fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value>;
@ -744,8 +824,8 @@ where
self.0.name() self.0.name()
} }
fn description(&self, cx: &mut App) -> SharedString { fn description(&self) -> SharedString {
self.0.description(cx) self.0.description()
} }
fn kind(&self) -> agent_client_protocol::ToolKind { fn kind(&self) -> agent_client_protocol::ToolKind {
@ -884,6 +964,7 @@ pub struct ToolCallEventStream {
kind: acp::ToolKind, kind: acp::ToolKind,
input: serde_json::Value, input: serde_json::Value,
stream: AgentResponseEventStream, stream: AgentResponseEventStream,
fs: Option<Arc<dyn Fs>>,
} }
impl ToolCallEventStream { impl ToolCallEventStream {
@ -902,6 +983,7 @@ impl ToolCallEventStream {
}, },
acp::ToolKind::Other, acp::ToolKind::Other,
AgentResponseEventStream(events_tx), AgentResponseEventStream(events_tx),
None,
); );
(stream, ToolCallEventStreamReceiver(events_rx)) (stream, ToolCallEventStreamReceiver(events_rx))
@ -911,12 +993,14 @@ impl ToolCallEventStream {
tool_use: &LanguageModelToolUse, tool_use: &LanguageModelToolUse,
kind: acp::ToolKind, kind: acp::ToolKind,
stream: AgentResponseEventStream, stream: AgentResponseEventStream,
fs: Option<Arc<dyn Fs>>,
) -> Self { ) -> Self {
Self { Self {
tool_use_id: tool_use.id.clone(), tool_use_id: tool_use.id.clone(),
kind, kind,
input: tool_use.input.clone(), input: tool_use.input.clone(),
stream, stream,
fs,
} }
} }
@ -951,7 +1035,11 @@ impl ToolCallEventStream {
.ok(); .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(); let (response_tx, response_rx) = oneshot::channel();
self.stream self.stream
.0 .0
@ -959,7 +1047,7 @@ impl ToolCallEventStream {
ToolCallAuthorization { ToolCallAuthorization {
tool_call: AgentResponseEventStream::initial_tool_call( tool_call: AgentResponseEventStream::initial_tool_call(
&self.tool_use_id, &self.tool_use_id,
title, title.into(),
self.kind.clone(), self.kind.clone(),
self.input.clone(), self.input.clone(),
), ),
@ -984,12 +1072,22 @@ impl ToolCallEventStream {
}, },
))) )))
.ok(); .ok();
async move { let fs = self.fs.clone();
match response_rx.await?.0.as_ref() { cx.spawn(async move |cx| match response_rx.await?.0.as_ref() {
"allow" | "always_allow" => Ok(()), "always_allow" => {
_ => Err(anyhow!("Permission to run tool denied by user")), 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")),
})
} }
} }
@ -1037,3 +1135,207 @@ impl std::ops::DerefMut for ToolCallEventStreamReceiver {
&mut self.0 &mut self.0
} }
} }
impl AgentMessage {
fn to_request(&self) -> language_model::LanguageModelRequestMessage {
let mut message = LanguageModelRequestMessage {
role: self.role,
content: Vec::with_capacity(self.content.len()),
cache: false,
};
const OPEN_CONTEXT: &str = "<context>\n\
The following items were attached by the user. \
They are up-to-date and don't need to be re-read.\n\n";
const OPEN_FILES_TAG: &str = "<files>";
const OPEN_SYMBOLS_TAG: &str = "<symbols>";
const OPEN_THREADS_TAG: &str = "<threads>";
const OPEN_RULES_TAG: &str =
"<rules>\nThe user has specified the following rules that should be applied:\n";
let mut file_context = OPEN_FILES_TAG.to_string();
let mut symbol_context = OPEN_SYMBOLS_TAG.to_string();
let mut thread_context = OPEN_THREADS_TAG.to_string();
let mut rules_context = OPEN_RULES_TAG.to_string();
for chunk in &self.content {
let chunk = match chunk {
MessageContent::Text(text) => language_model::MessageContent::Text(text.clone()),
MessageContent::Thinking { text, signature } => {
language_model::MessageContent::Thinking {
text: text.clone(),
signature: signature.clone(),
}
}
MessageContent::RedactedThinking(value) => {
language_model::MessageContent::RedactedThinking(value.clone())
}
MessageContent::ToolUse(value) => {
language_model::MessageContent::ToolUse(value.clone())
}
MessageContent::ToolResult(value) => {
language_model::MessageContent::ToolResult(value.clone())
}
MessageContent::Image(value) => {
language_model::MessageContent::Image(value.clone())
}
MessageContent::Mention { uri, content } => {
match uri {
MentionUri::File(path) | MentionUri::Symbol(path, _) => {
write!(
&mut symbol_context,
"\n{}",
MarkdownCodeBlock {
tag: &codeblock_tag(&path),
text: &content.to_string(),
}
)
.ok();
}
MentionUri::Thread(_session_id) => {
write!(&mut thread_context, "\n{}\n", content).ok();
}
MentionUri::Rule(_user_prompt_id) => {
write!(
&mut rules_context,
"\n{}",
MarkdownCodeBlock {
tag: "",
text: &content
}
)
.ok();
}
}
language_model::MessageContent::Text(uri.to_link())
}
};
message.content.push(chunk);
}
let len_before_context = message.content.len();
if file_context.len() > OPEN_FILES_TAG.len() {
file_context.push_str("</files>\n");
message
.content
.push(language_model::MessageContent::Text(file_context));
}
if symbol_context.len() > OPEN_SYMBOLS_TAG.len() {
symbol_context.push_str("</symbols>\n");
message
.content
.push(language_model::MessageContent::Text(symbol_context));
}
if thread_context.len() > OPEN_THREADS_TAG.len() {
thread_context.push_str("</threads>\n");
message
.content
.push(language_model::MessageContent::Text(thread_context));
}
if rules_context.len() > OPEN_RULES_TAG.len() {
rules_context.push_str("</user_rules>\n");
message
.content
.push(language_model::MessageContent::Text(rules_context));
}
if message.content.len() > len_before_context {
message.content.insert(
len_before_context,
language_model::MessageContent::Text(OPEN_CONTEXT.into()),
);
message
.content
.push(language_model::MessageContent::Text("</context>".into()));
}
message
}
}
fn codeblock_tag(full_path: &Path) -> String {
let mut result = String::new();
if let Some(extension) = full_path.extension().and_then(|ext| ext.to_str()) {
let _ = write!(result, "{} ", extension);
}
let _ = write!(result, "{}", full_path.display());
result
}
impl From<acp::ContentBlock> for MessageContent {
fn from(value: acp::ContentBlock) -> Self {
match value {
acp::ContentBlock::Text(text_content) => MessageContent::Text(text_content.text),
acp::ContentBlock::Image(image_content) => {
MessageContent::Image(convert_image(image_content))
}
acp::ContentBlock::Audio(_) => {
// TODO
MessageContent::Text("[audio]".to_string())
}
acp::ContentBlock::ResourceLink(resource_link) => {
match MentionUri::parse(&resource_link.uri) {
Ok(uri) => Self::Mention {
uri,
content: String::new(),
},
Err(err) => {
log::error!("Failed to parse mention link: {}", err);
MessageContent::Text(format!(
"[{}]({})",
resource_link.name, resource_link.uri
))
}
}
}
acp::ContentBlock::Resource(resource) => match resource.resource {
acp::EmbeddedResourceResource::TextResourceContents(resource) => {
match MentionUri::parse(&resource.uri) {
Ok(uri) => Self::Mention {
uri,
content: resource.text,
},
Err(err) => {
log::error!("Failed to parse mention link: {}", err);
MessageContent::Text(
MarkdownCodeBlock {
tag: &resource.uri,
text: &resource.text,
}
.to_string(),
)
}
}
}
acp::EmbeddedResourceResource::BlobResourceContents(_) => {
// TODO
MessageContent::Text("[blob]".to_string())
}
},
}
}
}
fn convert_image(image_content: acp::ImageContent) -> LanguageModelImage {
LanguageModelImage {
source: image_content.data.into(),
// TODO: make this optional?
size: gpui::Size::new(0.into(), 0.into()),
}
}
impl From<&str> for MessageContent {
fn from(text: &str) -> Self {
MessageContent::Text(text.into())
}
}

View file

@ -1,7 +1,10 @@
mod context_server_registry;
mod copy_path_tool; mod copy_path_tool;
mod create_directory_tool; mod create_directory_tool;
mod delete_path_tool; mod delete_path_tool;
mod diagnostics_tool;
mod edit_file_tool; mod edit_file_tool;
mod fetch_tool;
mod find_path_tool; mod find_path_tool;
mod grep_tool; mod grep_tool;
mod list_directory_tool; mod list_directory_tool;
@ -13,10 +16,13 @@ mod terminal_tool;
mod thinking_tool; mod thinking_tool;
mod web_search_tool; mod web_search_tool;
pub use context_server_registry::*;
pub use copy_path_tool::*; pub use copy_path_tool::*;
pub use create_directory_tool::*; pub use create_directory_tool::*;
pub use delete_path_tool::*; pub use delete_path_tool::*;
pub use diagnostics_tool::*;
pub use edit_file_tool::*; pub use edit_file_tool::*;
pub use fetch_tool::*;
pub use find_path_tool::*; pub use find_path_tool::*;
pub use grep_tool::*; pub use grep_tool::*;
pub use list_directory_tool::*; pub use list_directory_tool::*;

View file

@ -0,0 +1,231 @@
use crate::{AgentToolOutput, AnyAgentTool, ToolCallEventStream};
use agent_client_protocol::ToolKind;
use anyhow::{Result, anyhow, bail};
use collections::{BTreeMap, HashMap};
use context_server::ContextServerId;
use gpui::{App, Context, Entity, SharedString, Task};
use project::context_server_store::{ContextServerStatus, ContextServerStore};
use std::sync::Arc;
use util::ResultExt;
pub struct ContextServerRegistry {
server_store: Entity<ContextServerStore>,
registered_servers: HashMap<ContextServerId, RegisteredContextServer>,
_subscription: gpui::Subscription,
}
struct RegisteredContextServer {
tools: BTreeMap<SharedString, Arc<dyn AnyAgentTool>>,
load_tools: Task<Result<()>>,
}
impl ContextServerRegistry {
pub fn new(server_store: Entity<ContextServerStore>, cx: &mut Context<Self>) -> Self {
let mut this = Self {
server_store: server_store.clone(),
registered_servers: HashMap::default(),
_subscription: cx.subscribe(&server_store, Self::handle_context_server_store_event),
};
for server in server_store.read(cx).running_servers() {
this.reload_tools_for_server(server.id(), cx);
}
this
}
pub fn servers(
&self,
) -> impl Iterator<
Item = (
&ContextServerId,
&BTreeMap<SharedString, Arc<dyn AnyAgentTool>>,
),
> {
self.registered_servers
.iter()
.map(|(id, server)| (id, &server.tools))
}
fn reload_tools_for_server(&mut self, server_id: ContextServerId, cx: &mut Context<Self>) {
let Some(server) = self.server_store.read(cx).get_running_server(&server_id) else {
return;
};
let Some(client) = server.client() else {
return;
};
if !client.capable(context_server::protocol::ServerCapability::Tools) {
return;
}
let registered_server =
self.registered_servers
.entry(server_id.clone())
.or_insert(RegisteredContextServer {
tools: BTreeMap::default(),
load_tools: Task::ready(Ok(())),
});
registered_server.load_tools = cx.spawn(async move |this, cx| {
let response = client
.request::<context_server::types::requests::ListTools>(())
.await;
this.update(cx, |this, cx| {
let Some(registered_server) = this.registered_servers.get_mut(&server_id) else {
return;
};
registered_server.tools.clear();
if let Some(response) = response.log_err() {
for tool in response.tools {
let tool = Arc::new(ContextServerTool::new(
this.server_store.clone(),
server.id(),
tool,
));
registered_server.tools.insert(tool.name(), tool);
}
cx.notify();
}
})
});
}
fn handle_context_server_store_event(
&mut self,
_: Entity<ContextServerStore>,
event: &project::context_server_store::Event,
cx: &mut Context<Self>,
) {
match event {
project::context_server_store::Event::ServerStatusChanged { server_id, status } => {
match status {
ContextServerStatus::Starting => {}
ContextServerStatus::Running => {
self.reload_tools_for_server(server_id.clone(), cx);
}
ContextServerStatus::Stopped | ContextServerStatus::Error(_) => {
self.registered_servers.remove(&server_id);
cx.notify();
}
}
}
}
}
}
struct ContextServerTool {
store: Entity<ContextServerStore>,
server_id: ContextServerId,
tool: context_server::types::Tool,
}
impl ContextServerTool {
fn new(
store: Entity<ContextServerStore>,
server_id: ContextServerId,
tool: context_server::types::Tool,
) -> Self {
Self {
store,
server_id,
tool,
}
}
}
impl AnyAgentTool for ContextServerTool {
fn name(&self) -> SharedString {
self.tool.name.clone().into()
}
fn description(&self) -> SharedString {
self.tool.description.clone().unwrap_or_default().into()
}
fn kind(&self) -> ToolKind {
ToolKind::Other
}
fn initial_title(&self, _input: serde_json::Value) -> SharedString {
format!("Run MCP tool `{}`", self.tool.name).into()
}
fn input_schema(
&self,
format: language_model::LanguageModelToolSchemaFormat,
) -> Result<serde_json::Value> {
let mut schema = self.tool.input_schema.clone();
assistant_tool::adapt_schema_to_format(&mut schema, format)?;
Ok(match schema {
serde_json::Value::Null => {
serde_json::json!({ "type": "object", "properties": [] })
}
serde_json::Value::Object(map) if map.is_empty() => {
serde_json::json!({ "type": "object", "properties": [] })
}
_ => schema,
})
}
fn run(
self: Arc<Self>,
input: serde_json::Value,
_event_stream: ToolCallEventStream,
cx: &mut App,
) -> Task<Result<AgentToolOutput>> {
let Some(server) = self.store.read(cx).get_running_server(&self.server_id) else {
return Task::ready(Err(anyhow!("Context server not found")));
};
let tool_name = self.tool.name.clone();
let server_clone = server.clone();
let input_clone = input.clone();
cx.spawn(async move |_cx| {
let Some(protocol) = server_clone.client() else {
bail!("Context server not initialized");
};
let arguments = if let serde_json::Value::Object(map) = input_clone {
Some(map.into_iter().collect())
} else {
None
};
log::trace!(
"Running tool: {} with arguments: {:?}",
tool_name,
arguments
);
let response = protocol
.request::<context_server::types::requests::CallTool>(
context_server::types::CallToolParams {
name: tool_name,
arguments,
meta: None,
},
)
.await?;
let mut result = String::new();
for content in response.content {
match content {
context_server::types::ToolResponseContent::Text { text } => {
result.push_str(&text);
}
context_server::types::ToolResponseContent::Image { .. } => {
log::warn!("Ignoring image content from tool response");
}
context_server::types::ToolResponseContent::Audio { .. } => {
log::warn!("Ignoring audio content from tool response");
}
context_server::types::ToolResponseContent::Resource { .. } => {
log::warn!("Ignoring resource content from tool response");
}
}
}
Ok(AgentToolOutput {
raw_output: result.clone().into(),
llm_output: result.into(),
})
})
}
}

View file

@ -0,0 +1,163 @@
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
)?;
}
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 {
Task::ready(Ok(output))
} else {
Task::ready(Ok("No errors or warnings found in the project.".into()))
}
}
}
}
}

View file

@ -133,7 +133,7 @@ impl EditFileTool {
&self, &self,
input: &EditFileToolInput, input: &EditFileToolInput,
event_stream: &ToolCallEventStream, event_stream: &ToolCallEventStream,
cx: &App, cx: &mut App,
) -> Task<Result<()>> { ) -> Task<Result<()>> {
if agent_settings::AgentSettings::get_global(cx).always_allow_tool_actions { if agent_settings::AgentSettings::get_global(cx).always_allow_tool_actions {
return Task::ready(Ok(())); return Task::ready(Ok(()));
@ -147,8 +147,9 @@ impl EditFileTool {
.components() .components()
.any(|component| component.as_os_str() == local_settings_folder.as_os_str()) .any(|component| component.as_os_str() == local_settings_folder.as_os_str())
{ {
return cx.foreground_executor().spawn( return event_stream.authorize(
event_stream.authorize(format!("{} (local settings)", input.display_description)), format!("{} (local settings)", input.display_description),
cx,
); );
} }
@ -156,9 +157,9 @@ impl EditFileTool {
// so check for that edge case too. // so check for that edge case too.
if let Ok(canonical_path) = std::fs::canonicalize(&input.path) { if let Ok(canonical_path) = std::fs::canonicalize(&input.path) {
if canonical_path.starts_with(paths::config_dir()) { if canonical_path.starts_with(paths::config_dir()) {
return cx.foreground_executor().spawn( return event_stream.authorize(
event_stream format!("{} (global settings)", input.display_description),
.authorize(format!("{} (global settings)", input.display_description)), cx,
); );
} }
} }
@ -173,8 +174,7 @@ impl EditFileTool {
if project_path.is_some() { if project_path.is_some() {
Task::ready(Ok(())) Task::ready(Ok(()))
} else { } else {
cx.foreground_executor() event_stream.authorize(&input.display_description, cx)
.spawn(event_stream.authorize(input.display_description.clone()))
} }
} }
} }
@ -454,9 +454,8 @@ fn resolve_path(
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::Templates;
use super::*; use super::*;
use crate::{ContextServerRegistry, Templates};
use action_log::ActionLog; use action_log::ActionLog;
use client::TelemetrySettings; use client::TelemetrySettings;
use fs::Fs; use fs::Fs;
@ -475,9 +474,20 @@ mod tests {
fs.insert_tree("/root", json!({})).await; fs.insert_tree("/root", json!({})).await;
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
let action_log = cx.new(|_| ActionLog::new(project.clone())); let action_log = cx.new(|_| ActionLog::new(project.clone()));
let context_server_registry =
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
let model = Arc::new(FakeLanguageModel::default()); let model = Arc::new(FakeLanguageModel::default());
let thread = let thread = cx.new(|cx| {
cx.new(|_| Thread::new(project, Rc::default(), action_log, Templates::new(), model)); Thread::new(
project,
Rc::default(),
context_server_registry,
action_log,
Templates::new(),
model,
cx,
)
});
let result = cx let result = cx
.update(|cx| { .update(|cx| {
let input = EditFileToolInput { let input = EditFileToolInput {
@ -661,14 +671,18 @@ mod tests {
}); });
let action_log = cx.new(|_| ActionLog::new(project.clone())); let action_log = cx.new(|_| ActionLog::new(project.clone()));
let context_server_registry =
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
let model = Arc::new(FakeLanguageModel::default()); let model = Arc::new(FakeLanguageModel::default());
let thread = cx.new(|_| { let thread = cx.new(|cx| {
Thread::new( Thread::new(
project, project,
Rc::default(), Rc::default(),
context_server_registry,
action_log.clone(), action_log.clone(),
Templates::new(), Templates::new(),
model.clone(), model.clone(),
cx,
) )
}); });
@ -792,15 +806,19 @@ mod tests {
.unwrap(); .unwrap();
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
let context_server_registry =
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
let action_log = cx.new(|_| ActionLog::new(project.clone())); let action_log = cx.new(|_| ActionLog::new(project.clone()));
let model = Arc::new(FakeLanguageModel::default()); let model = Arc::new(FakeLanguageModel::default());
let thread = cx.new(|_| { let thread = cx.new(|cx| {
Thread::new( Thread::new(
project, project,
Rc::default(), Rc::default(),
context_server_registry,
action_log.clone(), action_log.clone(),
Templates::new(), Templates::new(),
model.clone(), model.clone(),
cx,
) )
}); });
@ -914,15 +932,19 @@ mod tests {
init_test(cx); init_test(cx);
let fs = project::FakeFs::new(cx.executor()); let fs = project::FakeFs::new(cx.executor());
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
let context_server_registry =
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
let action_log = cx.new(|_| ActionLog::new(project.clone())); let action_log = cx.new(|_| ActionLog::new(project.clone()));
let model = Arc::new(FakeLanguageModel::default()); let model = Arc::new(FakeLanguageModel::default());
let thread = cx.new(|_| { let thread = cx.new(|cx| {
Thread::new( Thread::new(
project, project,
Rc::default(), Rc::default(),
context_server_registry,
action_log.clone(), action_log.clone(),
Templates::new(), Templates::new(),
model.clone(), model.clone(),
cx,
) )
}); });
let tool = Arc::new(EditFileTool { thread }); let tool = Arc::new(EditFileTool { thread });
@ -1041,15 +1063,19 @@ mod tests {
let fs = project::FakeFs::new(cx.executor()); let fs = project::FakeFs::new(cx.executor());
fs.insert_tree("/project", json!({})).await; fs.insert_tree("/project", json!({})).await;
let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
let context_server_registry =
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
let action_log = cx.new(|_| ActionLog::new(project.clone())); let action_log = cx.new(|_| ActionLog::new(project.clone()));
let model = Arc::new(FakeLanguageModel::default()); let model = Arc::new(FakeLanguageModel::default());
let thread = cx.new(|_| { let thread = cx.new(|cx| {
Thread::new( Thread::new(
project, project,
Rc::default(), Rc::default(),
context_server_registry,
action_log.clone(), action_log.clone(),
Templates::new(), Templates::new(),
model.clone(), model.clone(),
cx,
) )
}); });
let tool = Arc::new(EditFileTool { thread }); let tool = Arc::new(EditFileTool { thread });
@ -1148,14 +1174,18 @@ mod tests {
.await; .await;
let action_log = cx.new(|_| ActionLog::new(project.clone())); let action_log = cx.new(|_| ActionLog::new(project.clone()));
let context_server_registry =
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
let model = Arc::new(FakeLanguageModel::default()); let model = Arc::new(FakeLanguageModel::default());
let thread = cx.new(|_| { let thread = cx.new(|cx| {
Thread::new( Thread::new(
project.clone(), project.clone(),
Rc::default(), Rc::default(),
context_server_registry.clone(),
action_log.clone(), action_log.clone(),
Templates::new(), Templates::new(),
model.clone(), model.clone(),
cx,
) )
}); });
let tool = Arc::new(EditFileTool { thread }); let tool = Arc::new(EditFileTool { thread });
@ -1225,14 +1255,18 @@ mod tests {
.await; .await;
let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
let action_log = cx.new(|_| ActionLog::new(project.clone())); let action_log = cx.new(|_| ActionLog::new(project.clone()));
let context_server_registry =
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
let model = Arc::new(FakeLanguageModel::default()); let model = Arc::new(FakeLanguageModel::default());
let thread = cx.new(|_| { let thread = cx.new(|cx| {
Thread::new( Thread::new(
project.clone(), project.clone(),
Rc::default(), Rc::default(),
context_server_registry.clone(),
action_log.clone(), action_log.clone(),
Templates::new(), Templates::new(),
model.clone(), model.clone(),
cx,
) )
}); });
let tool = Arc::new(EditFileTool { thread }); let tool = Arc::new(EditFileTool { thread });
@ -1305,14 +1339,18 @@ mod tests {
.await; .await;
let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
let action_log = cx.new(|_| ActionLog::new(project.clone())); let action_log = cx.new(|_| ActionLog::new(project.clone()));
let context_server_registry =
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
let model = Arc::new(FakeLanguageModel::default()); let model = Arc::new(FakeLanguageModel::default());
let thread = cx.new(|_| { let thread = cx.new(|cx| {
Thread::new( Thread::new(
project.clone(), project.clone(),
Rc::default(), Rc::default(),
context_server_registry.clone(),
action_log.clone(), action_log.clone(),
Templates::new(), Templates::new(),
model.clone(), model.clone(),
cx,
) )
}); });
let tool = Arc::new(EditFileTool { thread }); let tool = Arc::new(EditFileTool { thread });
@ -1382,14 +1420,18 @@ mod tests {
let fs = project::FakeFs::new(cx.executor()); let fs = project::FakeFs::new(cx.executor());
let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
let action_log = cx.new(|_| ActionLog::new(project.clone())); let action_log = cx.new(|_| ActionLog::new(project.clone()));
let context_server_registry =
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
let model = Arc::new(FakeLanguageModel::default()); let model = Arc::new(FakeLanguageModel::default());
let thread = cx.new(|_| { let thread = cx.new(|cx| {
Thread::new( Thread::new(
project.clone(), project.clone(),
Rc::default(), Rc::default(),
context_server_registry,
action_log.clone(), action_log.clone(),
Templates::new(), Templates::new(),
model.clone(), model.clone(),
cx,
) )
}); });
let tool = Arc::new(EditFileTool { thread }); let tool = Arc::new(EditFileTool { thread });

View file

@ -0,0 +1,155 @@
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");
}
Ok(text)
})
}
}

View file

@ -139,9 +139,6 @@ impl AgentTool for FindPathTool {
}) })
.collect(), .collect(),
), ),
raw_output: Some(serde_json::json!({
"paths": &matches,
})),
..Default::default() ..Default::default()
}); });

View file

@ -101,7 +101,7 @@ impl AgentTool for GrepTool {
fn run( fn run(
self: Arc<Self>, self: Arc<Self>,
input: Self::Input, input: Self::Input,
event_stream: ToolCallEventStream, _event_stream: ToolCallEventStream,
cx: &mut App, cx: &mut App,
) -> Task<Result<Self::Output>> { ) -> Task<Result<Self::Output>> {
const CONTEXT_LINES: u32 = 2; const CONTEXT_LINES: u32 = 2;
@ -282,33 +282,22 @@ impl AgentTool for GrepTool {
} }
} }
event_stream.update_fields(acp::ToolCallUpdateFields {
content: Some(vec![output.clone().into()]),
..Default::default()
});
matches_found += 1; matches_found += 1;
} }
} }
let output = if matches_found == 0 { if matches_found == 0 {
"No matches found".to_string() Ok("No matches found".into())
} else if has_more_matches { } else if has_more_matches {
format!( Ok(format!(
"Showing matches {}-{} (there were more matches found; use offset: {} to see next page):\n{output}", "Showing matches {}-{} (there were more matches found; use offset: {} to see next page):\n{output}",
input.offset + 1, input.offset + 1,
input.offset + matches_found, input.offset + matches_found,
input.offset + RESULTS_PER_PAGE, input.offset + RESULTS_PER_PAGE,
) ))
} else { } else {
format!("Found {matches_found} matches:\n{output}") Ok(format!("Found {matches_found} matches:\n{output}"))
}; }
event_stream.update_fields(acp::ToolCallUpdateFields {
content: Some(vec![output.clone().into()]),
..Default::default()
});
Ok(output)
}) })
} }
} }

View file

@ -47,20 +47,13 @@ impl AgentTool for NowTool {
fn run( fn run(
self: Arc<Self>, self: Arc<Self>,
input: Self::Input, input: Self::Input,
event_stream: ToolCallEventStream, _event_stream: ToolCallEventStream,
_cx: &mut App, _cx: &mut App,
) -> Task<Result<String>> { ) -> Task<Result<String>> {
let now = match input.timezone { let now = match input.timezone {
Timezone::Utc => Utc::now().to_rfc3339(), Timezone::Utc => Utc::now().to_rfc3339(),
Timezone::Local => Local::now().to_rfc3339(), Timezone::Local => Local::now().to_rfc3339(),
}; };
let content = format!("The current datetime is {now}."); Task::ready(Ok(format!("The current datetime is {now}.")))
event_stream.update_fields(acp::ToolCallUpdateFields {
content: Some(vec![content.clone().into()]),
..Default::default()
});
Task::ready(Ok(content))
} }
} }

View file

@ -65,7 +65,7 @@ impl AgentTool for OpenTool {
) -> Task<Result<Self::Output>> { ) -> Task<Result<Self::Output>> {
// If path_or_url turns out to be a path in the project, make it absolute. // 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 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 { cx.background_spawn(async move {
authorize.await?; authorize.await?;

View file

@ -5,7 +5,6 @@ use gpui::{App, AppContext, Entity, SharedString, Task};
use project::{Project, terminals::TerminalKind}; use project::{Project, terminals::TerminalKind};
use schemars::JsonSchema; use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use settings::Settings;
use std::{ use std::{
path::{Path, PathBuf}, path::{Path, PathBuf},
sync::Arc, sync::Arc,
@ -61,21 +60,6 @@ impl TerminalTool {
determine_shell: determine_shell.shared(), 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 { impl AgentTool for TerminalTool {
@ -152,7 +136,7 @@ impl AgentTool for TerminalTool {
env env
}); });
let authorize = self.authorize(&input, &event_stream, cx); let authorize = event_stream.authorize(self.initial_title(Ok(input.clone())), cx);
cx.spawn({ cx.spawn({
async move |cx| { async move |cx| {

View file

@ -48,6 +48,20 @@ pub struct AgentProfileSettings {
pub context_servers: IndexMap<Arc<str>, ContextServerPreset>, pub context_servers: IndexMap<Arc<str>, ContextServerPreset>,
} }
impl AgentProfileSettings {
pub fn is_tool_enabled(&self, tool_name: &str) -> bool {
self.tools.get(tool_name) == Some(&true)
}
pub fn is_context_server_tool_enabled(&self, server_id: &str, tool_name: &str) -> bool {
self.enable_all_context_servers
|| self
.context_servers
.get(server_id)
.map_or(false, |preset| preset.tools.get(tool_name) == Some(&true))
}
}
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default)]
pub struct ContextServerPreset { pub struct ContextServerPreset {
pub tools: IndexMap<Arc<str>, bool>, pub tools: IndexMap<Arc<str>, bool>,

View file

@ -1,18 +1,20 @@
use std::ops::Range; use std::ops::Range;
use std::path::Path; use std::path::{Path, PathBuf};
use std::sync::Arc; use std::sync::Arc;
use std::sync::atomic::AtomicBool; use std::sync::atomic::AtomicBool;
use anyhow::Result; use acp_thread::MentionUri;
use anyhow::{Context as _, Result};
use collections::HashMap; use collections::HashMap;
use editor::display_map::CreaseId; use editor::display_map::CreaseId;
use editor::{CompletionProvider, Editor, ExcerptId}; use editor::{CompletionProvider, Editor, ExcerptId};
use file_icons::FileIcons; use file_icons::FileIcons;
use futures::future::try_join_all;
use gpui::{App, Entity, Task, WeakEntity}; use gpui::{App, Entity, Task, WeakEntity};
use language::{Buffer, CodeLabel, HighlightId}; use language::{Buffer, CodeLabel, HighlightId};
use lsp::CompletionContext; use lsp::CompletionContext;
use parking_lot::Mutex; use parking_lot::Mutex;
use project::{Completion, CompletionIntent, CompletionResponse, ProjectPath, WorktreeId}; use project::{Completion, CompletionIntent, CompletionResponse, Project, ProjectPath, WorktreeId};
use rope::Point; use rope::Point;
use text::{Anchor, ToPoint}; use text::{Anchor, ToPoint};
use ui::prelude::*; use ui::prelude::*;
@ -23,21 +25,63 @@ use crate::context_picker::file_context_picker::{extract_file_name_and_directory
#[derive(Default)] #[derive(Default)]
pub struct MentionSet { pub struct MentionSet {
paths_by_crease_id: HashMap<CreaseId, ProjectPath>, paths_by_crease_id: HashMap<CreaseId, MentionUri>,
} }
impl MentionSet { impl MentionSet {
pub fn insert(&mut self, crease_id: CreaseId, path: ProjectPath) { pub fn insert(&mut self, crease_id: CreaseId, path: PathBuf) {
self.paths_by_crease_id.insert(crease_id, path); self.paths_by_crease_id
} .insert(crease_id, MentionUri::File(path));
pub fn path_for_crease_id(&self, crease_id: CreaseId) -> Option<ProjectPath> {
self.paths_by_crease_id.get(&crease_id).cloned()
} }
pub fn drain(&mut self) -> impl Iterator<Item = CreaseId> { pub fn drain(&mut self) -> impl Iterator<Item = CreaseId> {
self.paths_by_crease_id.drain().map(|(id, _)| id) self.paths_by_crease_id.drain().map(|(id, _)| id)
} }
pub fn contents(
&self,
project: Entity<Project>,
cx: &mut App,
) -> Task<Result<HashMap<CreaseId, Mention>>> {
let contents = self
.paths_by_crease_id
.iter()
.map(|(crease_id, uri)| match uri {
MentionUri::File(path) => {
let crease_id = *crease_id;
let uri = uri.clone();
let path = path.to_path_buf();
let buffer_task = project.update(cx, |project, cx| {
let path = project
.find_project_path(path, cx)
.context("Failed to find project path")?;
anyhow::Ok(project.open_buffer(path, cx))
});
cx.spawn(async move |cx| {
let buffer = buffer_task?.await?;
let content = buffer.read_with(cx, |buffer, _cx| buffer.text())?;
anyhow::Ok((crease_id, Mention { uri, content }))
})
}
_ => {
// TODO
unimplemented!()
}
})
.collect::<Vec<_>>();
cx.spawn(async move |_cx| {
let contents = try_join_all(contents).await?.into_iter().collect();
anyhow::Ok(contents)
})
}
}
pub struct Mention {
pub uri: MentionUri,
pub content: String,
} }
pub struct ContextPickerCompletionProvider { pub struct ContextPickerCompletionProvider {
@ -68,6 +112,7 @@ impl ContextPickerCompletionProvider {
source_range: Range<Anchor>, source_range: Range<Anchor>,
editor: Entity<Editor>, editor: Entity<Editor>,
mention_set: Arc<Mutex<MentionSet>>, mention_set: Arc<Mutex<MentionSet>>,
project: Entity<Project>,
cx: &App, cx: &App,
) -> Completion { ) -> Completion {
let (file_name, directory) = let (file_name, directory) =
@ -112,6 +157,7 @@ impl ContextPickerCompletionProvider {
new_text_len - 1, new_text_len - 1,
editor, editor,
mention_set, mention_set,
project,
)), )),
} }
} }
@ -159,6 +205,7 @@ impl CompletionProvider for ContextPickerCompletionProvider {
return Task::ready(Ok(Vec::new())); return Task::ready(Ok(Vec::new()));
}; };
let project = workspace.read(cx).project().clone();
let snapshot = buffer.read(cx).snapshot(); let snapshot = buffer.read(cx).snapshot();
let source_range = snapshot.anchor_before(state.source_range.start) let source_range = snapshot.anchor_before(state.source_range.start)
..snapshot.anchor_after(state.source_range.end); ..snapshot.anchor_after(state.source_range.end);
@ -195,6 +242,7 @@ impl CompletionProvider for ContextPickerCompletionProvider {
source_range.clone(), source_range.clone(),
editor.clone(), editor.clone(),
mention_set.clone(), mention_set.clone(),
project.clone(),
cx, cx,
) )
}) })
@ -254,6 +302,7 @@ fn confirm_completion_callback(
content_len: usize, content_len: usize,
editor: Entity<Editor>, editor: Entity<Editor>,
mention_set: Arc<Mutex<MentionSet>>, mention_set: Arc<Mutex<MentionSet>>,
project: Entity<Project>,
) -> Arc<dyn Fn(CompletionIntent, &mut Window, &mut App) -> bool + Send + Sync> { ) -> Arc<dyn Fn(CompletionIntent, &mut Window, &mut App) -> bool + Send + Sync> {
Arc::new(move |_, window, cx| { Arc::new(move |_, window, cx| {
let crease_text = crease_text.clone(); let crease_text = crease_text.clone();
@ -261,6 +310,7 @@ fn confirm_completion_callback(
let editor = editor.clone(); let editor = editor.clone();
let project_path = project_path.clone(); let project_path = project_path.clone();
let mention_set = mention_set.clone(); let mention_set = mention_set.clone();
let project = project.clone();
window.defer(cx, move |window, cx| { window.defer(cx, move |window, cx| {
let crease_id = crate::context_picker::insert_crease_for_mention( let crease_id = crate::context_picker::insert_crease_for_mention(
excerpt_id, excerpt_id,
@ -272,8 +322,13 @@ fn confirm_completion_callback(
window, window,
cx, cx,
); );
let Some(path) = project.read(cx).absolute_path(&project_path, cx) else {
return;
};
if let Some(crease_id) = crease_id { if let Some(crease_id) = crease_id {
mention_set.lock().insert(crease_id, project_path); mention_set.lock().insert(crease_id, path);
} }
}); });
false false

File diff suppressed because it is too large Load diff

View file

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

View file

@ -152,6 +152,9 @@ impl PythonDebugAdapter {
maybe!(async move { maybe!(async move {
let response = latest_release.filter(|response| response.status().is_success())?; 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(); let mut output = String::new();
response response
.into_body() .into_body()

View file

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

View file

@ -1,5 +1,6 @@
use gpui::{ 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; struct SetMenus;
@ -27,7 +28,11 @@ fn main() {
// Add menu items // Add menu items
cx.set_menus(vec![Menu { cx.set_menus(vec![Menu {
name: "set_menus".into(), 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 {})) cx.open_window(WindowOptions::default(), |_, cx| cx.new(|_| SetMenus {}))
.unwrap(); .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 /// The different kinds of items that can be in a menu
pub enum MenuItem { pub enum MenuItem {
/// A separator between items /// A separator between items
@ -28,6 +56,9 @@ pub enum MenuItem {
/// A submenu /// A submenu
Submenu(Menu), Submenu(Menu),
/// A menu, managed by the system (for example, the Services menu on macOS)
SystemMenu(OsMenu),
/// An action that can be performed /// An action that can be performed
Action { Action {
/// The name of this menu item /// The name of this menu item
@ -53,6 +84,14 @@ impl MenuItem {
Self::Submenu(menu) 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 /// Creates a new menu item that invokes an action
pub fn action(name: impl Into<SharedString>, action: impl Action) -> Self { pub fn action(name: impl Into<SharedString>, action: impl Action) -> Self {
Self::Action { Self::Action {
@ -89,10 +128,23 @@ impl MenuItem {
action, action,
os_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 /// A menu of the application, either a main menu or a submenu
#[derive(Clone)] #[derive(Clone)]
pub struct OwnedMenu { pub struct OwnedMenu {
@ -111,6 +163,9 @@ pub enum OwnedMenuItem {
/// A submenu /// A submenu
Submenu(OwnedMenu), Submenu(OwnedMenu),
/// A menu, managed by the system (for example, the Services menu on macOS)
SystemMenu(OwnedOsMenu),
/// An action that can be performed /// An action that can be performed
Action { Action {
/// The name of this menu item /// The name of this menu item
@ -139,6 +194,7 @@ impl Clone for OwnedMenuItem {
action: action.boxed_clone(), action: action.boxed_clone(),
os_action: *os_action, os_action: *os_action,
}, },
OwnedMenuItem::SystemMenu(os_menu) => OwnedMenuItem::SystemMenu(os_menu.clone()),
} }
} }
} }

View file

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

View file

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

View file

@ -2814,7 +2814,7 @@ impl Window {
content_mask: content_mask.scale(scale_factor), content_mask: content_mask.scale(scale_factor),
color: style.color.unwrap_or_default().opacity(element_opacity), color: style.color.unwrap_or_default().opacity(element_opacity),
thickness: style.thickness.scale(scale_factor), 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), content_mask: content_mask.scale(scale_factor),
thickness: style.thickness.scale(scale_factor), thickness: style.thickness.scale(scale_factor),
color: style.color.unwrap_or_default().opacity(opacity), color: style.color.unwrap_or_default().opacity(opacity),
wavy: false, wavy: 0,
}); });
} }

View file

@ -167,6 +167,7 @@ fn generate_test_function(
)); ));
cx_teardowns.extend(quote!( cx_teardowns.extend(quote!(
dispatcher.run_until_parked(); dispatcher.run_until_parked();
#cx_varname.executor().forbid_parking();
#cx_varname.quit(); #cx_varname.quit();
dispatcher.run_until_parked(); dispatcher.run_until_parked();
)); ));
@ -232,7 +233,7 @@ fn generate_test_function(
cx_teardowns.extend(quote!( cx_teardowns.extend(quote!(
drop(#cx_varname_lock); drop(#cx_varname_lock);
dispatcher.run_until_parked(); dispatcher.run_until_parked();
#cx_varname.update(|cx| { cx.quit() }); #cx_varname.update(|cx| { cx.background_executor().forbid_parking(); cx.quit(); });
dispatcher.run_until_parked(); dispatcher.run_until_parked();
)); ));
continue; continue;
@ -247,6 +248,7 @@ fn generate_test_function(
)); ));
cx_teardowns.extend(quote!( cx_teardowns.extend(quote!(
dispatcher.run_until_parked(); dispatcher.run_until_parked();
#cx_varname.executor().forbid_parking();
#cx_varname.quit(); #cx_varname.quit();
dispatcher.run_until_parked(); dispatcher.run_until_parked();
)); ));

View file

@ -301,7 +301,25 @@ impl LanguageModel for OpenAiLanguageModel {
} }
fn supports_images(&self) -> bool { fn supports_images(&self) -> bool {
false use open_ai::Model;
match &self.model {
Model::FourOmni
| Model::FourOmniMini
| Model::FourPointOne
| Model::FourPointOneMini
| Model::FourPointOneNano
| Model::Five
| Model::FiveMini
| Model::FiveNano
| Model::O1
| Model::O3
| Model::O4Mini => true,
Model::ThreePointFiveTurbo
| Model::Four
| Model::FourTurbo
| Model::O3Mini
| Model::Custom { .. } => false,
}
} }
fn supports_tool_choice(&self, choice: LanguageModelToolChoice) -> bool { fn supports_tool_choice(&self, choice: LanguageModelToolChoice) -> bool {

View file

@ -487,6 +487,8 @@ const GO_MODULE_ROOT_TASK_VARIABLE: VariableName =
VariableName::Custom(Cow::Borrowed("GO_MODULE_ROOT")); VariableName::Custom(Cow::Borrowed("GO_MODULE_ROOT"));
const GO_SUBTEST_NAME_TASK_VARIABLE: VariableName = const GO_SUBTEST_NAME_TASK_VARIABLE: VariableName =
VariableName::Custom(Cow::Borrowed("GO_SUBTEST_NAME")); VariableName::Custom(Cow::Borrowed("GO_SUBTEST_NAME"));
const GO_TABLE_TEST_CASE_NAME_TASK_VARIABLE: VariableName =
VariableName::Custom(Cow::Borrowed("GO_TABLE_TEST_CASE_NAME"));
impl ContextProvider for GoContextProvider { impl ContextProvider for GoContextProvider {
fn build_context( fn build_context(
@ -545,10 +547,19 @@ impl ContextProvider for GoContextProvider {
let go_subtest_variable = extract_subtest_name(_subtest_name.unwrap_or("")) let go_subtest_variable = extract_subtest_name(_subtest_name.unwrap_or(""))
.map(|subtest_name| (GO_SUBTEST_NAME_TASK_VARIABLE.clone(), subtest_name)); .map(|subtest_name| (GO_SUBTEST_NAME_TASK_VARIABLE.clone(), subtest_name));
let table_test_case_name = variables.get(&VariableName::Custom(Cow::Borrowed(
"_table_test_case_name",
)));
let go_table_test_case_variable = table_test_case_name
.and_then(extract_subtest_name)
.map(|case_name| (GO_TABLE_TEST_CASE_NAME_TASK_VARIABLE.clone(), case_name));
Task::ready(Ok(TaskVariables::from_iter( Task::ready(Ok(TaskVariables::from_iter(
[ [
go_package_variable, go_package_variable,
go_subtest_variable, go_subtest_variable,
go_table_test_case_variable,
go_module_root_variable, go_module_root_variable,
] ]
.into_iter() .into_iter()
@ -570,6 +581,28 @@ impl ContextProvider for GoContextProvider {
let module_cwd = Some(GO_MODULE_ROOT_TASK_VARIABLE.template_value()); let module_cwd = Some(GO_MODULE_ROOT_TASK_VARIABLE.template_value());
Task::ready(Some(TaskTemplates(vec![ Task::ready(Some(TaskTemplates(vec![
TaskTemplate {
label: format!(
"go test {} -v -run {}/{}",
GO_PACKAGE_TASK_VARIABLE.template_value(),
VariableName::Symbol.template_value(),
GO_TABLE_TEST_CASE_NAME_TASK_VARIABLE.template_value(),
),
command: "go".into(),
args: vec![
"test".into(),
"-v".into(),
"-run".into(),
format!(
"\\^{}\\$/\\^{}\\$",
VariableName::Symbol.template_value(),
GO_TABLE_TEST_CASE_NAME_TASK_VARIABLE.template_value(),
),
],
cwd: package_cwd.clone(),
tags: vec!["go-table-test-case".to_owned()],
..TaskTemplate::default()
},
TaskTemplate { TaskTemplate {
label: format!( label: format!(
"go test {} -run {}", "go test {} -run {}",
@ -842,10 +875,21 @@ mod tests {
.collect() .collect()
}); });
let tag_strings: Vec<String> = runnables
.iter()
.flat_map(|r| &r.runnable.tags)
.map(|tag| tag.0.to_string())
.collect();
assert!( assert!(
runnables.len() == 2, tag_strings.contains(&"go-test".to_string()),
"Should find test function and subtest with double quotes, found: {}", "Should find go-test tag, found: {:?}",
runnables.len() tag_strings
);
assert!(
tag_strings.contains(&"go-subtest".to_string()),
"Should find go-subtest tag, found: {:?}",
tag_strings
); );
let buffer = cx.new(|cx| { let buffer = cx.new(|cx| {
@ -860,10 +904,299 @@ mod tests {
.collect() .collect()
}); });
let tag_strings: Vec<String> = runnables
.iter()
.flat_map(|r| &r.runnable.tags)
.map(|tag| tag.0.to_string())
.collect();
assert!( assert!(
runnables.len() == 2, tag_strings.contains(&"go-test".to_string()),
"Should find test function and subtest with backticks, found: {}", "Should find go-test tag, found: {:?}",
runnables.len() tag_strings
);
assert!(
tag_strings.contains(&"go-subtest".to_string()),
"Should find go-subtest tag, found: {:?}",
tag_strings
);
}
#[gpui::test]
fn test_go_table_test_slice_detection(cx: &mut TestAppContext) {
let language = language("go", tree_sitter_go::LANGUAGE.into());
let table_test = r#"
package main
import "testing"
func TestExample(t *testing.T) {
_ = "some random string"
testCases := []struct{
name string
anotherStr string
}{
{
name: "test case 1",
anotherStr: "foo",
},
{
name: "test case 2",
anotherStr: "bar",
},
}
notATableTest := []struct{
name string
}{
{
name: "some string",
},
{
name: "some other string",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// test code here
})
}
}
"#;
let buffer =
cx.new(|cx| crate::Buffer::local(table_test, cx).with_language(language.clone(), cx));
cx.executor().run_until_parked();
let runnables: Vec<_> = buffer.update(cx, |buffer, _| {
let snapshot = buffer.snapshot();
snapshot.runnable_ranges(0..table_test.len()).collect()
});
let tag_strings: Vec<String> = runnables
.iter()
.flat_map(|r| &r.runnable.tags)
.map(|tag| tag.0.to_string())
.collect();
assert!(
tag_strings.contains(&"go-test".to_string()),
"Should find go-test tag, found: {:?}",
tag_strings
);
assert!(
tag_strings.contains(&"go-table-test-case".to_string()),
"Should find go-table-test-case tag, found: {:?}",
tag_strings
);
let go_test_count = tag_strings.iter().filter(|&tag| tag == "go-test").count();
let go_table_test_count = tag_strings
.iter()
.filter(|&tag| tag == "go-table-test-case")
.count();
assert!(
go_test_count == 1,
"Should find exactly 1 go-test, found: {}",
go_test_count
);
assert!(
go_table_test_count == 2,
"Should find exactly 2 go-table-test-case, found: {}",
go_table_test_count
);
}
#[gpui::test]
fn test_go_table_test_slice_ignored(cx: &mut TestAppContext) {
let language = language("go", tree_sitter_go::LANGUAGE.into());
let table_test = r#"
package main
func Example() {
_ = "some random string"
notATableTest := []struct{
name string
}{
{
name: "some string",
},
{
name: "some other string",
},
}
}
"#;
let buffer =
cx.new(|cx| crate::Buffer::local(table_test, cx).with_language(language.clone(), cx));
cx.executor().run_until_parked();
let runnables: Vec<_> = buffer.update(cx, |buffer, _| {
let snapshot = buffer.snapshot();
snapshot.runnable_ranges(0..table_test.len()).collect()
});
let tag_strings: Vec<String> = runnables
.iter()
.flat_map(|r| &r.runnable.tags)
.map(|tag| tag.0.to_string())
.collect();
assert!(
!tag_strings.contains(&"go-test".to_string()),
"Should find go-test tag, found: {:?}",
tag_strings
);
assert!(
!tag_strings.contains(&"go-table-test-case".to_string()),
"Should find go-table-test-case tag, found: {:?}",
tag_strings
);
}
#[gpui::test]
fn test_go_table_test_map_detection(cx: &mut TestAppContext) {
let language = language("go", tree_sitter_go::LANGUAGE.into());
let table_test = r#"
package main
import "testing"
func TestExample(t *testing.T) {
_ = "some random string"
testCases := map[string]struct {
someStr string
fail bool
}{
"test failure": {
someStr: "foo",
fail: true,
},
"test success": {
someStr: "bar",
fail: false,
},
}
notATableTest := map[string]struct {
someStr string
}{
"some string": {
someStr: "foo",
},
"some other string": {
someStr: "bar",
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
// test code here
})
}
}
"#;
let buffer =
cx.new(|cx| crate::Buffer::local(table_test, cx).with_language(language.clone(), cx));
cx.executor().run_until_parked();
let runnables: Vec<_> = buffer.update(cx, |buffer, _| {
let snapshot = buffer.snapshot();
snapshot.runnable_ranges(0..table_test.len()).collect()
});
let tag_strings: Vec<String> = runnables
.iter()
.flat_map(|r| &r.runnable.tags)
.map(|tag| tag.0.to_string())
.collect();
assert!(
tag_strings.contains(&"go-test".to_string()),
"Should find go-test tag, found: {:?}",
tag_strings
);
assert!(
tag_strings.contains(&"go-table-test-case".to_string()),
"Should find go-table-test-case tag, found: {:?}",
tag_strings
);
let go_test_count = tag_strings.iter().filter(|&tag| tag == "go-test").count();
let go_table_test_count = tag_strings
.iter()
.filter(|&tag| tag == "go-table-test-case")
.count();
assert!(
go_test_count == 1,
"Should find exactly 1 go-test, found: {}",
go_test_count
);
assert!(
go_table_test_count == 2,
"Should find exactly 2 go-table-test-case, found: {}",
go_table_test_count
);
}
#[gpui::test]
fn test_go_table_test_map_ignored(cx: &mut TestAppContext) {
let language = language("go", tree_sitter_go::LANGUAGE.into());
let table_test = r#"
package main
func Example() {
_ = "some random string"
notATableTest := map[string]struct {
someStr string
}{
"some string": {
someStr: "foo",
},
"some other string": {
someStr: "bar",
},
}
}
"#;
let buffer =
cx.new(|cx| crate::Buffer::local(table_test, cx).with_language(language.clone(), cx));
cx.executor().run_until_parked();
let runnables: Vec<_> = buffer.update(cx, |buffer, _| {
let snapshot = buffer.snapshot();
snapshot.runnable_ranges(0..table_test.len()).collect()
});
let tag_strings: Vec<String> = runnables
.iter()
.flat_map(|r| &r.runnable.tags)
.map(|tag| tag.0.to_string())
.collect();
assert!(
!tag_strings.contains(&"go-test".to_string()),
"Should find go-test tag, found: {:?}",
tag_strings
);
assert!(
!tag_strings.contains(&"go-table-test-case".to_string()),
"Should find go-table-test-case tag, found: {:?}",
tag_strings
); );
} }

View file

@ -91,3 +91,103 @@
) @_ ) @_
(#set! tag go-main) (#set! tag go-main)
) )
; Table test cases - slice and map
(
(short_var_declaration
left: (expression_list (identifier) @_collection_var)
right: (expression_list
(composite_literal
type: [
(slice_type)
(map_type
key: (type_identifier) @_key_type
(#eq? @_key_type "string")
)
]
body: (literal_value
[
(literal_element
(literal_value
(keyed_element
(literal_element
(identifier) @_field_name
)
(literal_element
[
(interpreted_string_literal) @run @_table_test_case_name
(raw_string_literal) @run @_table_test_case_name
]
)
)
)
)
(keyed_element
(literal_element
[
(interpreted_string_literal) @run @_table_test_case_name
(raw_string_literal) @run @_table_test_case_name
]
)
)
]
)
)
)
)
(for_statement
(range_clause
left: (expression_list
[
(
(identifier)
(identifier) @_loop_var
)
(identifier) @_loop_var
]
)
right: (identifier) @_range_var
(#eq? @_range_var @_collection_var)
)
body: (block
(expression_statement
(call_expression
function: (selector_expression
operand: (identifier) @_t_var
field: (field_identifier) @_run_method
(#eq? @_run_method "Run")
)
arguments: (argument_list
.
[
(selector_expression
operand: (identifier) @_tc_var
(#eq? @_tc_var @_loop_var)
field: (field_identifier) @_field_check
(#eq? @_field_check @_field_name)
)
(identifier) @_arg_var
(#eq? @_arg_var @_loop_var)
]
.
(func_literal
parameters: (parameter_list
(parameter_declaration
type: (pointer_type
(qualified_type
package: (package_identifier) @_pkg
name: (type_identifier) @_type
(#eq? @_pkg "testing")
(#eq? @_type "T")
)
)
)
)
)
)
)
)
)
) @_
(#set! tag go-table-test-case)
)

View file

@ -3367,20 +3367,6 @@ impl LocalLspStore {
} }
} }
fn parse_register_capabilities<T: serde::de::DeserializeOwned>(
reg: lsp::Registration,
) -> anyhow::Result<OneOf<bool, T>> {
let caps = match reg
.register_options
.map(|options| serde_json::from_value::<T>(options))
.transpose()?
{
None => OneOf::Left(true),
Some(options) => OneOf::Right(options),
};
Ok(caps)
}
fn notify_server_capabilities_updated(server: &LanguageServer, cx: &mut Context<LspStore>) { fn notify_server_capabilities_updated(server: &LanguageServer, cx: &mut Context<LspStore>) {
if let Some(capabilities) = serde_json::to_string(&server.capabilities()).ok() { if let Some(capabilities) = serde_json::to_string(&server.capabilities()).ok() {
cx.emit(LspStoreEvent::LanguageServerUpdate { cx.emit(LspStoreEvent::LanguageServerUpdate {
@ -11690,190 +11676,190 @@ impl LspStore {
// Ignore payload since we notify clients of setting changes unconditionally, relying on them pulling the latest settings. // Ignore payload since we notify clients of setting changes unconditionally, relying on them pulling the latest settings.
} }
"workspace/symbol" => { "workspace/symbol" => {
let options = parse_register_capabilities(reg)?; if let Some(options) = parse_register_capabilities(reg)? {
server.update_capabilities(|capabilities| { server.update_capabilities(|capabilities| {
capabilities.workspace_symbol_provider = Some(options); capabilities.workspace_symbol_provider = Some(options);
}); });
notify_server_capabilities_updated(&server, cx); notify_server_capabilities_updated(&server, cx);
}
} }
"workspace/fileOperations" => { "workspace/fileOperations" => {
let caps = reg if let Some(options) = reg.register_options {
.register_options let caps = serde_json::from_value(options)?;
.map(serde_json::from_value) server.update_capabilities(|capabilities| {
.transpose()? capabilities
.unwrap_or_default(); .workspace
server.update_capabilities(|capabilities| { .get_or_insert_default()
capabilities .file_operations = Some(caps);
.workspace });
.get_or_insert_default() notify_server_capabilities_updated(&server, cx);
.file_operations = Some(caps); }
});
notify_server_capabilities_updated(&server, cx);
} }
"workspace/executeCommand" => { "workspace/executeCommand" => {
let options = reg if let Some(options) = reg.register_options {
.register_options let options = serde_json::from_value(options)?;
.map(serde_json::from_value) server.update_capabilities(|capabilities| {
.transpose()? capabilities.execute_command_provider = Some(options);
.unwrap_or_default(); });
server.update_capabilities(|capabilities| { notify_server_capabilities_updated(&server, cx);
capabilities.execute_command_provider = Some(options); }
});
notify_server_capabilities_updated(&server, cx);
} }
"textDocument/rangeFormatting" => { "textDocument/rangeFormatting" => {
let options = parse_register_capabilities(reg)?; if let Some(options) = parse_register_capabilities(reg)? {
server.update_capabilities(|capabilities| { server.update_capabilities(|capabilities| {
capabilities.document_range_formatting_provider = Some(options); capabilities.document_range_formatting_provider = Some(options);
}); });
notify_server_capabilities_updated(&server, cx); notify_server_capabilities_updated(&server, cx);
}
} }
"textDocument/onTypeFormatting" => { "textDocument/onTypeFormatting" => {
let options = reg if let Some(options) = reg
.register_options .register_options
.map(serde_json::from_value) .map(serde_json::from_value)
.transpose()? .transpose()?
.unwrap_or_default(); {
server.update_capabilities(|capabilities| { server.update_capabilities(|capabilities| {
capabilities.document_on_type_formatting_provider = Some(options); capabilities.document_on_type_formatting_provider = Some(options);
}); });
notify_server_capabilities_updated(&server, cx); notify_server_capabilities_updated(&server, cx);
}
} }
"textDocument/formatting" => { "textDocument/formatting" => {
let options = parse_register_capabilities(reg)?; if let Some(options) = parse_register_capabilities(reg)? {
server.update_capabilities(|capabilities| { server.update_capabilities(|capabilities| {
capabilities.document_formatting_provider = Some(options); capabilities.document_formatting_provider = Some(options);
}); });
notify_server_capabilities_updated(&server, cx); notify_server_capabilities_updated(&server, cx);
}
} }
"textDocument/rename" => { "textDocument/rename" => {
let options = parse_register_capabilities(reg)?; if let Some(options) = parse_register_capabilities(reg)? {
server.update_capabilities(|capabilities| { server.update_capabilities(|capabilities| {
capabilities.rename_provider = Some(options); capabilities.rename_provider = Some(options);
}); });
notify_server_capabilities_updated(&server, cx); notify_server_capabilities_updated(&server, cx);
}
} }
"textDocument/inlayHint" => { "textDocument/inlayHint" => {
let options = parse_register_capabilities(reg)?; if let Some(options) = parse_register_capabilities(reg)? {
server.update_capabilities(|capabilities| { server.update_capabilities(|capabilities| {
capabilities.inlay_hint_provider = Some(options); capabilities.inlay_hint_provider = Some(options);
}); });
notify_server_capabilities_updated(&server, cx); notify_server_capabilities_updated(&server, cx);
}
} }
"textDocument/documentSymbol" => { "textDocument/documentSymbol" => {
let options = parse_register_capabilities(reg)?; if let Some(options) = parse_register_capabilities(reg)? {
server.update_capabilities(|capabilities| { server.update_capabilities(|capabilities| {
capabilities.document_symbol_provider = Some(options); capabilities.document_symbol_provider = Some(options);
}); });
notify_server_capabilities_updated(&server, cx); notify_server_capabilities_updated(&server, cx);
}
} }
"textDocument/codeAction" => { "textDocument/codeAction" => {
let options = reg if let Some(options) = reg
.register_options .register_options
.map(serde_json::from_value) .map(serde_json::from_value)
.transpose()?; .transpose()?
let provider_capability = match options { {
None => lsp::CodeActionProviderCapability::Simple(true), server.update_capabilities(|capabilities| {
Some(options) => lsp::CodeActionProviderCapability::Options(options), capabilities.code_action_provider =
}; Some(lsp::CodeActionProviderCapability::Options(options));
server.update_capabilities(|capabilities| { });
capabilities.code_action_provider = Some(provider_capability); notify_server_capabilities_updated(&server, cx);
}); }
notify_server_capabilities_updated(&server, cx);
} }
"textDocument/definition" => { "textDocument/definition" => {
let caps = parse_register_capabilities(reg)?; if let Some(options) = parse_register_capabilities(reg)? {
server.update_capabilities(|capabilities| { server.update_capabilities(|capabilities| {
capabilities.definition_provider = Some(caps); capabilities.definition_provider = Some(options);
}); });
notify_server_capabilities_updated(&server, cx); notify_server_capabilities_updated(&server, cx);
}
} }
"textDocument/completion" => { "textDocument/completion" => {
let caps = reg if let Some(caps) = reg
.register_options .register_options
.map(serde_json::from_value) .map(serde_json::from_value)
.transpose()? .transpose()?
.unwrap_or_default(); {
server.update_capabilities(|capabilities| { server.update_capabilities(|capabilities| {
capabilities.completion_provider = Some(caps); capabilities.completion_provider = Some(caps);
}); });
notify_server_capabilities_updated(&server, cx); notify_server_capabilities_updated(&server, cx);
}
} }
"textDocument/hover" => { "textDocument/hover" => {
let caps = reg if let Some(caps) = reg
.register_options .register_options
.map(serde_json::from_value) .map(serde_json::from_value)
.transpose()? .transpose()?
.unwrap_or_else(|| lsp::HoverProviderCapability::Simple(true)); {
server.update_capabilities(|capabilities| { server.update_capabilities(|capabilities| {
capabilities.hover_provider = Some(caps); capabilities.hover_provider = Some(caps);
}); });
notify_server_capabilities_updated(&server, cx); notify_server_capabilities_updated(&server, cx);
}
} }
"textDocument/signatureHelp" => { "textDocument/signatureHelp" => {
let caps = reg if let Some(caps) = reg
.register_options .register_options
.map(serde_json::from_value) .map(serde_json::from_value)
.transpose()? .transpose()?
.unwrap_or_default(); {
server.update_capabilities(|capabilities| { server.update_capabilities(|capabilities| {
capabilities.signature_help_provider = Some(caps); capabilities.signature_help_provider = Some(caps);
}); });
notify_server_capabilities_updated(&server, cx); notify_server_capabilities_updated(&server, cx);
}
} }
"textDocument/synchronization" => { "textDocument/synchronization" => {
let caps = reg if let Some(caps) = reg
.register_options .register_options
.map(serde_json::from_value) .map(serde_json::from_value)
.transpose()? .transpose()?
.unwrap_or_else(|| { {
lsp::TextDocumentSyncCapability::Options( server.update_capabilities(|capabilities| {
lsp::TextDocumentSyncOptions::default(), capabilities.text_document_sync = Some(caps);
)
}); });
server.update_capabilities(|capabilities| { notify_server_capabilities_updated(&server, cx);
capabilities.text_document_sync = Some(caps); }
});
notify_server_capabilities_updated(&server, cx);
} }
"textDocument/codeLens" => { "textDocument/codeLens" => {
let caps = reg if let Some(caps) = reg
.register_options .register_options
.map(serde_json::from_value) .map(serde_json::from_value)
.transpose()? .transpose()?
.unwrap_or_else(|| lsp::CodeLensOptions { {
resolve_provider: None, server.update_capabilities(|capabilities| {
capabilities.code_lens_provider = Some(caps);
}); });
server.update_capabilities(|capabilities| { notify_server_capabilities_updated(&server, cx);
capabilities.code_lens_provider = Some(caps); }
});
notify_server_capabilities_updated(&server, cx);
} }
"textDocument/diagnostic" => { "textDocument/diagnostic" => {
let caps = reg if let Some(caps) = reg
.register_options .register_options
.map(serde_json::from_value) .map(serde_json::from_value)
.transpose()? .transpose()?
.unwrap_or_else(|| { {
lsp::DiagnosticServerCapabilities::RegistrationOptions( server.update_capabilities(|capabilities| {
lsp::DiagnosticRegistrationOptions::default(), capabilities.diagnostic_provider = Some(caps);
)
}); });
server.update_capabilities(|capabilities| { notify_server_capabilities_updated(&server, cx);
capabilities.diagnostic_provider = Some(caps); }
});
notify_server_capabilities_updated(&server, cx);
} }
"textDocument/colorProvider" => { "textDocument/colorProvider" => {
let caps = reg if let Some(caps) = reg
.register_options .register_options
.map(serde_json::from_value) .map(serde_json::from_value)
.transpose()? .transpose()?
.unwrap_or_else(|| lsp::ColorProviderCapability::Simple(true)); {
server.update_capabilities(|capabilities| { server.update_capabilities(|capabilities| {
capabilities.color_provider = Some(caps); capabilities.color_provider = Some(caps);
}); });
notify_server_capabilities_updated(&server, cx); notify_server_capabilities_updated(&server, cx);
}
} }
_ => log::warn!("unhandled capability registration: {reg:?}"), _ => log::warn!("unhandled capability registration: {reg:?}"),
} }
@ -12016,6 +12002,18 @@ impl LspStore {
} }
} }
// Registration with empty capabilities should be ignored.
// https://github.com/microsoft/vscode-languageserver-node/blob/d90a87f9557a0df9142cfb33e251cfa6fe27d970/client/src/common/formatting.ts#L67-L70
fn parse_register_capabilities<T: serde::de::DeserializeOwned>(
reg: lsp::Registration,
) -> anyhow::Result<Option<OneOf<bool, T>>> {
Ok(reg
.register_options
.map(|options| serde_json::from_value::<T>(options))
.transpose()?
.map(OneOf::Right))
}
fn subscribe_to_binary_statuses( fn subscribe_to_binary_statuses(
languages: &Arc<LanguageRegistry>, languages: &Arc<LanguageRegistry>,
cx: &mut Context<'_, LspStore>, cx: &mut Context<'_, LspStore>,

View file

@ -256,7 +256,7 @@ impl Project {
let local_path = if is_ssh_terminal { None } else { path.clone() }; let local_path = if is_ssh_terminal { None } else { path.clone() };
let mut python_venv_activate_command = None; let mut python_venv_activate_command = Task::ready(None);
let (spawn_task, shell) = match kind { let (spawn_task, shell) = match kind {
TerminalKind::Shell(_) => { TerminalKind::Shell(_) => {
@ -265,6 +265,7 @@ impl Project {
python_venv_directory, python_venv_directory,
&settings.detect_venv, &settings.detect_venv,
&settings.shell, &settings.shell,
cx,
); );
} }
@ -419,9 +420,12 @@ impl Project {
}) })
.detach(); .detach();
if let Some(activate_command) = python_venv_activate_command { this.activate_python_virtual_environment(
this.activate_python_virtual_environment(activate_command, &terminal_handle, cx); python_venv_activate_command,
} &terminal_handle,
cx,
);
terminal_handle terminal_handle
}) })
} }
@ -539,12 +543,15 @@ impl Project {
venv_base_directory: &Path, venv_base_directory: &Path,
venv_settings: &VenvSettings, venv_settings: &VenvSettings,
shell: &Shell, shell: &Shell,
) -> Option<String> { cx: &mut App,
let venv_settings = venv_settings.as_option()?; ) -> Task<Option<String>> {
let Some(venv_settings) = venv_settings.as_option() else {
return Task::ready(None);
};
let activate_keyword = match venv_settings.activate_script { let activate_keyword = match venv_settings.activate_script {
terminal_settings::ActivateScript::Default => match std::env::consts::OS { terminal_settings::ActivateScript::Default => match std::env::consts::OS {
"windows" => ".", "windows" => ".",
_ => "source", _ => ".",
}, },
terminal_settings::ActivateScript::Nushell => "overlay use", terminal_settings::ActivateScript::Nushell => "overlay use",
terminal_settings::ActivateScript::PowerShell => ".", terminal_settings::ActivateScript::PowerShell => ".",
@ -589,30 +596,44 @@ impl Project {
.join(activate_script_name) .join(activate_script_name)
.to_string_lossy() .to_string_lossy()
.to_string(); .to_string();
let quoted = shlex::try_quote(&path).ok()?;
smol::block_on(self.fs.metadata(path.as_ref()))
.ok()
.flatten()?;
Some(format!( let is_valid_path = self.resolve_abs_path(path.as_ref(), cx);
"{} {} ; clear{}", cx.background_spawn(async move {
activate_keyword, quoted, line_ending let quoted = shlex::try_quote(&path).ok()?;
)) if is_valid_path.await.is_some_and(|meta| meta.is_file()) {
Some(format!(
"{} {} ; clear{}",
activate_keyword, quoted, line_ending
))
} else {
None
}
})
} else { } else {
Some(format!( Task::ready(Some(format!(
"{activate_keyword} {activate_script_name} {name}; clear{line_ending}", "{activate_keyword} {activate_script_name} {name}; clear{line_ending}",
name = venv_settings.venv_name name = venv_settings.venv_name
)) )))
} }
} }
fn activate_python_virtual_environment( fn activate_python_virtual_environment(
&self, &self,
command: String, command: Task<Option<String>>,
terminal_handle: &Entity<Terminal>, terminal_handle: &Entity<Terminal>,
cx: &mut App, cx: &mut App,
) { ) {
terminal_handle.update(cx, |terminal, _| terminal.input(command.into_bytes())); terminal_handle.update(cx, |_, cx| {
cx.spawn(async move |this, cx| {
if let Some(command) = command.await {
this.update(cx, |this, _| {
this.input(command.into_bytes());
})
.ok();
}
})
.detach()
});
} }
pub fn local_terminal_handles(&self) -> &Vec<WeakEntity<terminal::Terminal>> { pub fn local_terminal_handles(&self) -> &Vec<WeakEntity<terminal::Terminal>> {

View file

@ -121,8 +121,16 @@ impl ApplicationMenu {
menu.action(name, action) menu.action(name, action)
} }
OwnedMenuItem::Submenu(_) => menu, 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

@ -8,6 +8,7 @@ use gpui::{
use language::{Buffer, BufferEvent, LanguageName, Toolchain}; use language::{Buffer, BufferEvent, LanguageName, Toolchain};
use project::{Project, ProjectPath, WorktreeId, toolchain_store::ToolchainStoreEvent}; use project::{Project, ProjectPath, WorktreeId, toolchain_store::ToolchainStoreEvent};
use ui::{Button, ButtonCommon, Clickable, FluentBuilder, LabelSize, SharedString, Tooltip}; use ui::{Button, ButtonCommon, Clickable, FluentBuilder, LabelSize, SharedString, Tooltip};
use util::maybe;
use workspace::{StatusItemView, Workspace, item::ItemHandle}; use workspace::{StatusItemView, Workspace, item::ItemHandle};
use crate::ToolchainSelector; use crate::ToolchainSelector;
@ -55,49 +56,61 @@ impl ActiveToolchain {
} }
fn spawn_tracker_task(window: &mut Window, cx: &mut Context<Self>) -> Task<Option<()>> { fn spawn_tracker_task(window: &mut Window, cx: &mut Context<Self>) -> Task<Option<()>> {
cx.spawn_in(window, async move |this, cx| { cx.spawn_in(window, async move |this, cx| {
let active_file = this let did_set_toolchain = maybe!(async {
.read_with(cx, |this, _| { let active_file = this
this.active_buffer .read_with(cx, |this, _| {
.as_ref() this.active_buffer
.map(|(_, buffer, _)| buffer.clone()) .as_ref()
}) .map(|(_, buffer, _)| buffer.clone())
.ok()
.flatten()?;
let workspace = this.read_with(cx, |this, _| this.workspace.clone()).ok()?;
let language_name = active_file
.read_with(cx, |this, _| Some(this.language()?.name()))
.ok()
.flatten()?;
let term = workspace
.update(cx, |workspace, cx| {
let languages = workspace.project().read(cx).languages();
Project::toolchain_term(languages.clone(), language_name.clone())
})
.ok()?
.await?;
let _ = this.update(cx, |this, cx| {
this.term = term;
cx.notify();
});
let (worktree_id, path) = active_file
.update(cx, |this, cx| {
this.file().and_then(|file| {
Some((
file.worktree_id(cx),
Arc::<Path>::from(file.path().parent()?),
))
}) })
.ok()
.flatten()?;
let workspace = this.read_with(cx, |this, _| this.workspace.clone()).ok()?;
let language_name = active_file
.read_with(cx, |this, _| Some(this.language()?.name()))
.ok()
.flatten()?;
let term = workspace
.update(cx, |workspace, cx| {
let languages = workspace.project().read(cx).languages();
Project::toolchain_term(languages.clone(), language_name.clone())
})
.ok()?
.await?;
let _ = this.update(cx, |this, cx| {
this.term = term;
cx.notify();
});
let (worktree_id, path) = active_file
.update(cx, |this, cx| {
this.file().and_then(|file| {
Some((
file.worktree_id(cx),
Arc::<Path>::from(file.path().parent()?),
))
})
})
.ok()
.flatten()?;
let toolchain =
Self::active_toolchain(workspace, worktree_id, path, language_name, cx).await?;
this.update(cx, |this, cx| {
this.active_toolchain = Some(toolchain);
cx.notify();
}) })
.ok() .ok()
.flatten()?; })
let toolchain = .await
Self::active_toolchain(workspace, worktree_id, path, language_name, cx).await?; .is_some();
let _ = this.update(cx, |this, cx| { if !did_set_toolchain {
this.active_toolchain = Some(toolchain); this.update(cx, |this, cx| {
this.active_toolchain = None;
cx.notify(); cx.notify();
}); })
Some(()) .ok();
}
did_set_toolchain.then_some(())
}) })
} }
@ -110,6 +123,17 @@ impl ActiveToolchain {
let editor = editor.read(cx); let editor = editor.read(cx);
if let Some((_, buffer, _)) = editor.active_excerpt(cx) { if let Some((_, buffer, _)) = editor.active_excerpt(cx) {
if let Some(worktree_id) = buffer.read(cx).file().map(|file| file.worktree_id(cx)) { if let Some(worktree_id) = buffer.read(cx).file().map(|file| file.worktree_id(cx)) {
if self
.active_buffer
.as_ref()
.is_some_and(|(old_worktree_id, old_buffer, _)| {
(old_worktree_id, old_buffer.entity_id())
== (&worktree_id, buffer.entity_id())
})
{
return;
}
let subscription = cx.subscribe_in( let subscription = cx.subscribe_in(
&buffer, &buffer,
window, window,
@ -231,7 +255,6 @@ impl StatusItemView for ActiveToolchain {
cx: &mut Context<Self>, cx: &mut Context<Self>,
) { ) {
if let Some(editor) = active_pane_item.and_then(|item| item.downcast::<Editor>()) { if let Some(editor) = active_pane_item.and_then(|item| item.downcast::<Editor>()) {
self.active_toolchain.take();
self.update_lister(editor, window, cx); self.update_lister(editor, window, cx);
} }
cx.notify(); cx.notify();

View file

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

View file

@ -887,10 +887,10 @@ macro_rules! maybe {
(|| $block)() (|| $block)()
}; };
(async $block:block) => { (async $block:block) => {
(|| async $block)() (async || $block)()
}; };
(async move $block:block) => { (async move $block:block) => {
(|| async move $block)() (async move || $block)()
}; };
} }

View file

@ -24,6 +24,7 @@ command_palette.workspace = true
command_palette_hooks.workspace = true command_palette_hooks.workspace = true
db.workspace = true db.workspace = true
editor.workspace = true editor.workspace = true
env_logger.workspace = true
futures.workspace = true futures.workspace = true
gpui.workspace = true gpui.workspace = true
itertools.workspace = true itertools.workspace = true

View file

@ -31,7 +31,7 @@ impl Vim {
) { ) {
let count = Vim::take_count(cx).unwrap_or(1); let count = Vim::take_count(cx).unwrap_or(1);
Vim::take_forced_motion(cx); Vim::take_forced_motion(cx);
self.update_editor(window, cx, |_, editor, window, cx| { self.update_editor(cx, |_, editor, cx| {
if let Some(selections) = editor if let Some(selections) = editor
.change_list .change_list
.next_change(count, direction) .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>) { 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 (map, selections) = editor.selections.all_adjusted_display(cx);
let buffer = editor.buffer().clone(); 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>) { 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: &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() { 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) => { VimOption::Wrap(true) => {
editor editor
.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx); .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::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 { let Some(project) = editor.project.clone() else {
return; return;
}; };
@ -375,7 +375,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
cx, cx,
); );
} }
vim.update_editor(window, cx, |vim, editor, window, cx| match action { vim.update_editor(cx, |vim, editor, cx| match action {
DeleteMarks::Marks(s) => { DeleteMarks::Marks(s) => {
if s.starts_with('-') || s.ends_with('-') || s.contains(['\'', '`']) { if s.starts_with('-') || s.ends_with('-') || s.contains(['\'', '`']) {
err(s.clone(), window, cx); 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::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 { let Some(workspace) = vim.workspace(window) else {
return; return;
}; };
@ -462,11 +462,10 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
.map(|c| Keystroke::parse(&c.to_string()).unwrap()) .map(|c| Keystroke::parse(&c.to_string()).unwrap())
.collect(); .collect();
vim.switch_mode(Mode::Normal, true, window, cx); vim.switch_mode(Mode::Normal, true, window, cx);
let initial_selections = vim.update_editor(window, cx, |_, editor, _, _| { let initial_selections =
editor.selections.disjoint_anchors() vim.update_editor(cx, |_, editor, _| editor.selections.disjoint_anchors());
});
if let Some(range) = &action.range { 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)?; let range = range.buffer_range(vim, editor, window, cx)?;
editor.change_selections( editor.change_selections(
SelectionEffects::no_scroll().nav_history(false), 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| { cx.spawn_in(window, async move |vim, cx| {
task.await; task.await;
vim.update_in(cx, |vim, window, cx| { vim.update_in(cx, |vim, window, cx| {
vim.update_editor(window, cx, |_, editor, window, cx| { vim.update_editor(cx, |_, editor, cx| {
if had_range { if had_range {
editor.change_selections(SelectionEffects::default(), window, cx, |s| { editor.change_selections(SelectionEffects::default(), window, cx, |s| {
s.select_anchor_ranges([s.newest_anchor().range()]); s.select_anchor_ranges([s.newest_anchor().range()]);
@ -510,7 +509,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
} else { } else {
vim.switch_mode(Mode::Normal, true, window, cx); 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(first_sel) = initial_selections {
if let Some(tx_id) = editor if let Some(tx_id) = editor
.buffer() .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::action(editor, cx, |vim, action: &GoToLine, window, cx| {
vim.switch_mode(Mode::Normal, false, 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 snapshot = editor.snapshot(window, cx);
let buffer_row = action.range.head().buffer_row(vim, editor, window, cx)?; let buffer_row = action.range.head().buffer_row(vim, editor, window, cx)?;
let current = editor.selections.newest::<Point>(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::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); let snapshot = editor.snapshot(window, cx);
if let Ok(range) = action.range.buffer_range(vim, editor, window, cx) { if let Ok(range) = action.range.buffer_range(vim, editor, window, cx) {
let end = if range.end < snapshot.buffer_snapshot.max_row() { 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| { 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) 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 let previous_selections = vim
.update_editor(window, cx, |_, editor, window, cx| { .update_editor(cx, |_, editor, cx| {
let selections = action.restore_selection.then(|| { let selections = action.restore_selection.then(|| {
editor editor
.selections .selections
@ -635,7 +634,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
.flatten(); .flatten();
window.dispatch_action(action.action.boxed_clone(), cx); window.dispatch_action(action.action.boxed_clone(), cx);
cx.defer_in(window, move |vim, window, 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| { editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
if let Some(previous_selections) = previous_selections { if let Some(previous_selections) = previous_selections {
s.select_ranges(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>) { 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) 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 snapshot = editor.snapshot(window, cx);
let mut row = range.start.0; let mut row = range.start.0;
@ -1680,7 +1679,7 @@ pub struct ShellExec {
impl Vim { impl Vim {
pub fn cancel_running_command(&mut self, window: &mut Window, cx: &mut Context<Self>) { pub fn cancel_running_command(&mut self, window: &mut Window, cx: &mut Context<Self>) {
if self.running_command.take().is_some() { 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.transact(window, cx, |editor, _window, _cx| {
editor.clear_row_highlights::<ShellExec>(); editor.clear_row_highlights::<ShellExec>();
}) })
@ -1691,7 +1690,7 @@ impl Vim {
fn prepare_shell_command( fn prepare_shell_command(
&mut self, &mut self,
command: &str, command: &str,
window: &mut Window, _: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> String { ) -> String {
let mut ret = String::new(); let mut ret = String::new();
@ -1711,7 +1710,7 @@ impl Vim {
} }
match c { 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((_, buffer, _)) = editor.active_excerpt(cx) {
if let Some(file) = buffer.read(cx).file() { if let Some(file) = buffer.read(cx).file() {
if let Some(local) = file.as_local() { if let Some(local) = file.as_local() {
@ -1747,7 +1746,7 @@ impl Vim {
let Some(workspace) = self.workspace(window) else { let Some(workspace) = self.workspace(window) else {
return; 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 snapshot = editor.snapshot(window, cx);
let start = editor.selections.newest_display(cx); let start = editor.selections.newest_display(cx);
let text_layout_details = editor.text_layout_details(window); let text_layout_details = editor.text_layout_details(window);
@ -1794,7 +1793,7 @@ impl Vim {
let Some(workspace) = self.workspace(window) else { let Some(workspace) = self.workspace(window) else {
return; 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 snapshot = editor.snapshot(window, cx);
let start = editor.selections.newest_display(cx); let start = editor.selections.newest_display(cx);
let range = object let range = object
@ -1896,7 +1895,7 @@ impl ShellExec {
let mut input_snapshot = None; let mut input_snapshot = None;
let mut input_range = None; let mut input_range = None;
let mut needs_newline_prefix = false; 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 snapshot = editor.buffer().read(cx).snapshot(cx);
let range = if let Some(range) = self.range.clone() { let range = if let Some(range) = self.range.clone() {
let Some(range) = range.buffer_range(vim, editor, window, cx).log_err() else { 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_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.transact(window, cx, |editor, window, cx| {
editor.edit([(range.clone(), text)], cx); editor.edit([(range.clone(), text)], cx);
let snapshot = editor.buffer().read(cx).snapshot(cx); let snapshot = editor.buffer().read(cx).snapshot(cx);

View file

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

View file

@ -62,7 +62,7 @@ impl Vim {
cx: &mut Context<Self>, cx: &mut Context<Self>,
mut is_boundary: impl FnMut(char, char, &CharClassifier) -> bool, 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| { editor.change_selections(Default::default(), window, cx, |s| {
s.move_with(|map, selection| { s.move_with(|map, selection| {
let times = times.unwrap_or(1); let times = times.unwrap_or(1);
@ -115,7 +115,7 @@ impl Vim {
cx: &mut Context<Self>, cx: &mut Context<Self>,
mut is_boundary: impl FnMut(char, char, &CharClassifier) -> bool, 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| { editor.change_selections(Default::default(), window, cx, |s| {
s.move_with(|map, selection| { s.move_with(|map, selection| {
let times = times.unwrap_or(1); let times = times.unwrap_or(1);
@ -175,7 +175,7 @@ impl Vim {
window: &mut Window, window: &mut Window,
cx: &mut Context<Self>, 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); let text_layout_details = editor.text_layout_details(window);
editor.change_selections(Default::default(), window, cx, |s| { editor.change_selections(Default::default(), window, cx, |s| {
s.move_with(|map, selection| { s.move_with(|map, selection| {
@ -253,7 +253,7 @@ impl Vim {
}) })
} }
Motion::FindForward { .. } => { 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); let text_layout_details = editor.text_layout_details(window);
editor.change_selections(Default::default(), window, cx, |s| { editor.change_selections(Default::default(), window, cx, |s| {
s.move_with(|map, selection| { s.move_with(|map, selection| {
@ -280,7 +280,7 @@ impl Vim {
}); });
} }
Motion::FindBackward { .. } => { 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); let text_layout_details = editor.text_layout_details(window);
editor.change_selections(Default::default(), window, cx, |s| { editor.change_selections(Default::default(), window, cx, |s| {
s.move_with(|map, selection| { s.move_with(|map, selection| {
@ -312,7 +312,7 @@ impl Vim {
fn helix_insert(&mut self, _: &HelixInsert, window: &mut Window, cx: &mut Context<Self>) { fn helix_insert(&mut self, _: &HelixInsert, window: &mut Window, cx: &mut Context<Self>) {
self.start_recording(cx); 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| { editor.change_selections(Default::default(), window, cx, |s| {
s.move_with(|_map, selection| { s.move_with(|_map, selection| {
// In helix normal mode, move cursor to start of selection and collapse // 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>) { fn helix_append(&mut self, _: &HelixAppend, window: &mut Window, cx: &mut Context<Self>) {
self.start_recording(cx); self.start_recording(cx);
self.switch_mode(Mode::Insert, false, window, 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| { editor.change_selections(Default::default(), window, cx, |s| {
s.move_with(|map, selection| { s.move_with(|map, selection| {
let point = if selection.is_empty() { 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>) { 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| { editor.transact(window, cx, |editor, window, cx| {
let (map, selections) = editor.selections.all_display(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); let count = Vim::take_count(cx).unwrap_or(1);
Vim::take_forced_motion(cx); Vim::take_forced_motion(cx);
vim.store_visual_marks(window, 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| { editor.transact(window, cx, |editor, window, cx| {
let original_positions = vim.save_selection_starts(editor, cx); let original_positions = vim.save_selection_starts(editor, cx);
for _ in 0..count { 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); let count = Vim::take_count(cx).unwrap_or(1);
Vim::take_forced_motion(cx); Vim::take_forced_motion(cx);
vim.store_visual_marks(window, 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| { editor.transact(window, cx, |editor, window, cx| {
let original_positions = vim.save_selection_starts(editor, cx); let original_positions = vim.save_selection_starts(editor, cx);
for _ in 0..count { 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); let count = Vim::take_count(cx).unwrap_or(1);
Vim::take_forced_motion(cx); Vim::take_forced_motion(cx);
vim.store_visual_marks(window, 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| { editor.transact(window, cx, |editor, window, cx| {
let original_positions = vim.save_selection_starts(editor, cx); let original_positions = vim.save_selection_starts(editor, cx);
for _ in 0..count { for _ in 0..count {
@ -95,7 +95,7 @@ impl Vim {
cx: &mut Context<Self>, cx: &mut Context<Self>,
) { ) {
self.stop_recording(cx); 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); let text_layout_details = editor.text_layout_details(window);
editor.transact(window, cx, |editor, window, cx| { editor.transact(window, cx, |editor, window, cx| {
let mut selection_starts: HashMap<_, _> = Default::default(); let mut selection_starts: HashMap<_, _> = Default::default();
@ -137,7 +137,7 @@ impl Vim {
cx: &mut Context<Self>, cx: &mut Context<Self>,
) { ) {
self.stop_recording(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.transact(window, cx, |editor, window, cx| {
let mut original_positions: HashMap<_, _> = Default::default(); let mut original_positions: HashMap<_, _> = Default::default();
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {

View file

@ -3,7 +3,9 @@ use editor::{Bias, Editor};
use gpui::{Action, Context, Window, actions}; use gpui::{Action, Context, Window, actions};
use language::SelectionGoal; use language::SelectionGoal;
use settings::Settings; use settings::Settings;
use text::Point;
use vim_mode_setting::HelixModeSetting; use vim_mode_setting::HelixModeSetting;
use workspace::searchable::Direction;
actions!( actions!(
vim, vim,
@ -11,13 +13,23 @@ actions!(
/// Switches to normal mode with cursor positioned before the current character. /// Switches to normal mode with cursor positioned before the current character.
NormalBefore, NormalBefore,
/// Temporarily switches to normal mode for one command. /// Temporarily switches to normal mode for one command.
TemporaryNormal TemporaryNormal,
/// Inserts the next character from the line above into the current line.
InsertFromAbove,
/// Inserts the next character from the line below into the current line.
InsertFromBelow
] ]
); );
pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) { pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
Vim::action(editor, cx, Vim::normal_before); Vim::action(editor, cx, Vim::normal_before);
Vim::action(editor, cx, Vim::temporary_normal); Vim::action(editor, cx, Vim::temporary_normal);
Vim::action(editor, cx, |vim, _: &InsertFromAbove, window, cx| {
vim.insert_around(Direction::Prev, window, cx)
});
Vim::action(editor, cx, |vim, _: &InsertFromBelow, window, cx| {
vim.insert_around(Direction::Next, window, cx)
})
} }
impl Vim { impl Vim {
@ -38,7 +50,7 @@ impl Vim {
if count <= 1 || Vim::globals(cx).dot_replaying { if count <= 1 || Vim::globals(cx).dot_replaying {
self.create_mark("^".into(), window, cx); 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); editor.dismiss_menus_and_popups(false, window, cx);
if !HelixModeSetting::get_global(cx).0 { if !HelixModeSetting::get_global(cx).0 {
@ -71,6 +83,29 @@ impl Vim {
self.switch_mode(Mode::Normal, true, window, cx); self.switch_mode(Mode::Normal, true, window, cx);
self.temp_mode = true; self.temp_mode = true;
} }
fn insert_around(&mut self, direction: Direction, _: &mut Window, cx: &mut Context<Self>) {
self.update_editor(cx, |_, editor, cx| {
let snapshot = editor.buffer().read(cx).snapshot(cx);
let mut edits = Vec::new();
for selection in editor.selections.all::<Point>(cx) {
let point = selection.head();
let new_row = match direction {
Direction::Next => point.row + 1,
Direction::Prev if point.row > 0 => point.row - 1,
_ => continue,
};
let source = snapshot.clip_point(Point::new(new_row, point.column), Bias::Left);
if let Some(c) = snapshot.chars_at(source).next()
&& c != '\n'
{
edits.push((point..point, c.to_string()))
}
}
editor.edit(edits, cx);
});
}
} }
#[cfg(test)] #[cfg(test)]
@ -156,4 +191,13 @@ mod test {
.await; .await;
cx.shared_state().await.assert_eq("hehello\nˇllo\n"); cx.shared_state().await.assert_eq("hehello\nˇllo\n");
} }
#[gpui::test]
async fn test_insert_ctrl_y(cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await;
cx.set_shared_state("hello\nˇ\nworld").await;
cx.simulate_shared_keystrokes("i ctrl-y ctrl-e").await;
cx.shared_state().await.assert_eq("hello\nhoˇ\nworld");
}
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -32,7 +32,7 @@ impl Vim {
let count = Vim::take_count(cx).unwrap_or(1); let count = Vim::take_count(cx).unwrap_or(1);
Vim::take_forced_motion(cx); 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); let text_layout_details = editor.text_layout_details(window);
editor.transact(window, cx, |editor, window, cx| { editor.transact(window, cx, |editor, window, cx| {
editor.set_clip_at_line_ends(false, cx); editor.set_clip_at_line_ends(false, cx);
@ -236,7 +236,7 @@ impl Vim {
) { ) {
self.stop_recording(cx); self.stop_recording(cx);
let selected_register = self.selected_register.take(); 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.transact(window, cx, |editor, window, cx| {
editor.set_clip_at_line_ends(false, cx); editor.set_clip_at_line_ends(false, cx);
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
@ -273,7 +273,7 @@ impl Vim {
) { ) {
self.stop_recording(cx); self.stop_recording(cx);
let selected_register = self.selected_register.take(); 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); let text_layout_details = editor.text_layout_details(window);
editor.transact(window, cx, |editor, window, cx| { editor.transact(window, cx, |editor, window, cx| {
editor.set_clip_at_line_ends(false, 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)); let amount = by(Vim::take_count(cx).map(|c| c as f32));
Vim::take_forced_motion(cx); Vim::take_forced_motion(cx);
self.exit_temporary_normal(window, 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) 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 the active editor has changed during a search, don't panic.
if prior_selections.iter().any(|s| { if prior_selections.iter().any(|s| {
self.update_editor(window, cx, |_, editor, window, cx| { self.update_editor(cx, |_, editor, cx| {
!s.start !s.start
.is_valid(&editor.snapshot(window, cx).buffer_snapshot) .is_valid(&editor.snapshot(window, cx).buffer_snapshot)
}) })
@ -457,7 +457,7 @@ impl Vim {
else { else {
return; 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 range = action.range.buffer_range(vim, editor, window, cx)?;
let snapshot = &editor.snapshot(window, cx).buffer_snapshot; let snapshot = &editor.snapshot(window, cx).buffer_snapshot;
let end_point = Point::new(range.end.0, snapshot.line_len(range.end)); 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>, cx: &mut Context<Self>,
) { ) {
self.store_visual_marks(window, cx); 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.set_clip_at_line_ends(false, cx);
editor.transact(window, cx, |editor, window, cx| { editor.transact(window, cx, |editor, window, cx| {
let text_layout_details = editor.text_layout_details(window); let text_layout_details = editor.text_layout_details(window);

View file

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

View file

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

View file

@ -49,7 +49,7 @@ impl Vim {
window: &mut Window, window: &mut Window,
cx: &mut Context<Self>, 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.transact(window, cx, |editor, window, cx| {
editor.set_clip_at_line_ends(false, cx); editor.set_clip_at_line_ends(false, cx);
let map = editor.snapshot(window, cx); let map = editor.snapshot(window, cx);
@ -94,7 +94,7 @@ impl Vim {
window: &mut Window, window: &mut Window,
cx: &mut Context<Self>, 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.transact(window, cx, |editor, window, cx| {
editor.set_clip_at_line_ends(false, cx); editor.set_clip_at_line_ends(false, cx);
let map = editor.snapshot(window, cx); let map = editor.snapshot(window, cx);
@ -148,7 +148,7 @@ impl Vim {
cx: &mut Context<Self>, cx: &mut Context<Self>,
) { ) {
self.stop_recording(cx); 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); editor.set_clip_at_line_ends(false, cx);
let mut selection = editor.selections.newest_display(cx); let mut selection = editor.selections.newest_display(cx);
let snapshot = editor.snapshot(window, 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>) { pub fn exchange_visual(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.stop_recording(cx); 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 selection = editor.selections.newest_anchor();
let new_range = selection.start..selection.end; let new_range = selection.start..selection.end;
let snapshot = editor.snapshot(window, cx); 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>) { pub fn clear_exchange(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.stop_recording(cx); self.stop_recording(cx);
self.update_editor(window, cx, |_, editor, _, cx| { self.update_editor(cx, |_, editor, cx| {
editor.clear_background_highlights::<VimExchange>(cx); editor.clear_background_highlights::<VimExchange>(cx);
}); });
self.clear_operator(window, cx); self.clear_operator(window, cx);
@ -193,7 +193,7 @@ impl Vim {
cx: &mut Context<Self>, cx: &mut Context<Self>,
) { ) {
self.stop_recording(cx); 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); editor.set_clip_at_line_ends(false, cx);
let text_layout_details = editor.text_layout_details(window); let text_layout_details = editor.text_layout_details(window);
let mut selection = editor.selections.newest_display(cx); 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_count(cx);
Vim::take_forced_motion(cx); Vim::take_forced_motion(cx);
vim.store_visual_marks(window, 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| { editor.transact(window, cx, |editor, window, cx| {
let mut positions = vim.save_selection_starts(editor, cx); let mut positions = vim.save_selection_starts(editor, cx);
editor.rewrap_impl( editor.rewrap_impl(
@ -55,7 +55,7 @@ impl Vim {
cx: &mut Context<Self>, cx: &mut Context<Self>,
) { ) {
self.stop_recording(cx); 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); let text_layout_details = editor.text_layout_details(window);
editor.transact(window, cx, |editor, window, cx| { editor.transact(window, cx, |editor, window, cx| {
let mut selection_starts: HashMap<_, _> = Default::default(); let mut selection_starts: HashMap<_, _> = Default::default();
@ -100,7 +100,7 @@ impl Vim {
cx: &mut Context<Self>, cx: &mut Context<Self>,
) { ) {
self.stop_recording(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.transact(window, cx, |editor, window, cx| {
let mut original_positions: HashMap<_, _> = Default::default(); let mut original_positions: HashMap<_, _> = Default::default();
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {

View file

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

View file

@ -15,6 +15,7 @@ impl VimTestContext {
if cx.has_global::<VimGlobals>() { if cx.has_global::<VimGlobals>() {
return; return;
} }
env_logger::try_init().ok();
cx.update(|cx| { cx.update(|cx| {
let settings = SettingsStore::test(cx); let settings = SettingsStore::test(cx);
cx.set_global(settings); cx.set_global(settings);

View file

@ -748,7 +748,7 @@ impl Vim {
editor, editor,
cx, cx,
|vim, action: &editor::actions::AcceptEditPrediction, window, 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); editor.accept_edit_prediction(action, window, cx);
}); });
// In non-insertion modes, predictions will be hidden and instead a jump will be // 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() { if let Some(action) = keystroke_event.action.as_ref() {
// Keystroke is handled by the vim system, so continue forward // Keystroke is handled by the vim system, so continue forward
if action.name().starts_with("vim::") { 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) editor.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx)
}); });
return; return;
@ -909,7 +909,7 @@ impl Vim {
anchor, anchor,
is_deactivate, is_deactivate,
} => { } => {
self.update_editor(window, cx, |vim, editor, window, cx| { self.update_editor(cx, |vim, editor, cx| {
let mark = if *is_deactivate { let mark = if *is_deactivate {
"\"".to_string() "\"".to_string()
} else { } else {
@ -972,7 +972,7 @@ impl Vim {
if mode == Mode::Normal || mode != last_mode { if mode == Mode::Normal || mode != last_mode {
self.current_tx.take(); self.current_tx.take();
self.current_anchor.take(); self.current_anchor.take();
self.update_editor(window, cx, |_, editor, _, _| { self.update_editor(cx, |_, editor, _| {
editor.clear_selection_drag_state(); editor.clear_selection_drag_state();
}); });
} }
@ -988,7 +988,7 @@ impl Vim {
&& self.mode != self.last_mode && self.mode != self.last_mode
&& (self.mode == Mode::Insert || self.last_mode == Mode::Insert) && (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; let is_relative = vim.mode != Mode::Insert;
editor.set_relative_line_number(Some(is_relative), cx) editor.set_relative_line_number(Some(is_relative), cx)
}); });
@ -1003,7 +1003,7 @@ impl Vim {
} }
// Adjust selections // 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 if last_mode != Mode::VisualBlock && last_mode.is_visual() && mode == Mode::VisualBlock
{ {
vim.visual_block_motion(true, editor, window, cx, |_, point, goal| { vim.visual_block_motion(true, editor, window, cx, |_, point, goal| {
@ -1214,7 +1214,7 @@ impl Vim {
if preserve_selection { if preserve_selection {
self.switch_mode(Mode::Visual, true, window, cx); self.switch_mode(Mode::Visual, true, window, cx);
} else { } else {
self.update_editor(window, cx, |_, editor, window, cx| { self.update_editor(cx, |_, editor, cx| {
editor.set_clip_at_line_ends(false, cx); editor.set_clip_at_line_ends(false, cx);
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.move_with(|_, selection| { s.move_with(|_, selection| {
@ -1232,18 +1232,18 @@ impl Vim {
if let Some(old_vim) = Vim::globals(cx).focused_vim() { if let Some(old_vim) = Vim::globals(cx).focused_vim() {
if old_vim.entity_id() != cx.entity().entity_id() { if old_vim.entity_id() != cx.entity().entity_id() {
old_vim.update(cx, |vim, cx| { 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) 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; let is_relative = vim.mode != Mode::Insert;
editor.set_relative_line_number(Some(is_relative), cx) editor.set_relative_line_number(Some(is_relative), cx)
}); });
} }
} else { } else {
self.update_editor(window, cx, |vim, editor, _, cx| { self.update_editor(cx, |vim, editor, cx| {
let is_relative = vim.mode != Mode::Insert; let is_relative = vim.mode != Mode::Insert;
editor.set_relative_line_number(Some(is_relative), cx) editor.set_relative_line_number(Some(is_relative), cx)
}); });
@ -1256,35 +1256,30 @@ impl Vim {
self.stop_recording_immediately(NormalBefore.boxed_clone(), cx); self.stop_recording_immediately(NormalBefore.boxed_clone(), cx);
self.store_visual_marks(window, cx); self.store_visual_marks(window, cx);
self.clear_operator(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 { if vim.cursor_shape(cx) == CursorShape::Block {
editor.set_cursor_shape(CursorShape::Hollow, cx); editor.set_cursor_shape(CursorShape::Hollow, cx);
} }
}); });
} }
fn cursor_shape_changed(&mut self, window: &mut Window, cx: &mut Context<Self>) { fn cursor_shape_changed(&mut self, _: &mut Window, cx: &mut Context<Self>) {
self.update_editor(window, cx, |vim, editor, _, cx| { self.update_editor(cx, |vim, editor, cx| {
editor.set_cursor_shape(vim.cursor_shape(cx), cx); editor.set_cursor_shape(vim.cursor_shape(cx), cx);
}); });
} }
fn update_editor<S>( fn update_editor<S>(
&mut self, &mut self,
window: &mut Window,
cx: &mut Context<Self>, 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> { ) -> Option<S> {
let editor = self.editor.upgrade()?; 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( fn editor_selections(&mut self, _: &mut Window, cx: &mut Context<Self>) -> Vec<Range<Anchor>> {
&mut self, self.update_editor(cx, |_, editor, _| {
window: &mut Window,
cx: &mut Context<Self>,
) -> Vec<Range<Anchor>> {
self.update_editor(window, cx, |_, editor, _, _| {
editor editor
.selections .selections
.disjoint_anchors() .disjoint_anchors()
@ -1300,7 +1295,7 @@ impl Vim {
window: &mut Window, window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> Option<String> { ) -> Option<String> {
self.update_editor(window, cx, |_, editor, window, cx| { self.update_editor(cx, |_, editor, cx| {
let selection = editor.selections.newest::<usize>(cx); let selection = editor.selections.newest::<usize>(cx);
let snapshot = &editor.snapshot(window, cx).buffer_snapshot; let snapshot = &editor.snapshot(window, cx).buffer_snapshot;
@ -1489,7 +1484,7 @@ impl Vim {
) { ) {
match self.mode { match self.mode {
Mode::VisualLine | Mode::VisualBlock | Mode::Visual => { 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); let original_mode = vim.undo_modes.get(transaction_id);
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
match original_mode { match original_mode {
@ -1520,7 +1515,7 @@ impl Vim {
self.switch_mode(Mode::Normal, true, window, cx) self.switch_mode(Mode::Normal, true, window, cx)
} }
Mode::Normal => { 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| { editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.move_with(|map, selection| { s.move_with(|map, selection| {
selection selection
@ -1547,7 +1542,7 @@ impl Vim {
self.current_anchor = Some(newest); self.current_anchor = Some(newest);
} else if self.current_anchor.as_ref().unwrap() != &newest { } else if self.current_anchor.as_ref().unwrap() != &newest {
if let Some(tx_id) = self.current_tx.take() { 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) editor.group_until_transaction(tx_id, cx)
}); });
} }
@ -1694,7 +1689,7 @@ impl Vim {
} }
Some(Operator::Register) => match self.mode { Some(Operator::Register) => match self.mode {
Mode::Insert => { 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| { if let Some(register) = Vim::update_globals(cx, |globals, cx| {
globals.read_register(text.chars().next(), Some(editor), cx) globals.read_register(text.chars().next(), Some(editor), cx)
}) { }) {
@ -1720,7 +1715,7 @@ impl Vim {
} }
if self.mode == Mode::Normal { if self.mode == Mode::Normal {
self.update_editor(window, cx, |_, editor, window, cx| { self.update_editor(cx, |_, editor, cx| {
editor.accept_edit_prediction( editor.accept_edit_prediction(
&editor::actions::AcceptEditPrediction {}, &editor::actions::AcceptEditPrediction {},
window, window,
@ -1733,7 +1728,7 @@ impl Vim {
} }
fn sync_vim_settings(&mut self, window: &mut Window, cx: &mut Context<Self>) { 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_cursor_shape(vim.cursor_shape(cx), cx);
editor.set_clip_at_line_ends(vim.clip_at_line_ends(), cx); editor.set_clip_at_line_ends(vim.clip_at_line_ends(), cx);
editor.set_collapse_matches(true); 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); let count = Vim::take_count(cx).unwrap_or(1);
Vim::take_forced_motion(cx); Vim::take_forced_motion(cx);
for _ in 0..count { 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); 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); let count = Vim::take_count(cx).unwrap_or(1);
Vim::take_forced_motion(cx); Vim::take_forced_motion(cx);
for _ in 0..count { 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); editor.select_smaller_syntax_node(&Default::default(), window, cx);
}); });
} }
@ -129,7 +129,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
return; return;
}; };
let marks = vim let marks = vim
.update_editor(window, cx, |vim, editor, window, cx| { .update_editor(cx, |vim, editor, cx| {
vim.get_mark("<", editor, window, cx) vim.get_mark("<", editor, window, cx)
.zip(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.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.set_clip_at_line_ends(false, cx);
editor.change_selections(Default::default(), window, cx, |s| { editor.change_selections(Default::default(), window, cx, |s| {
let map = s.display_map(); let map = s.display_map();
@ -189,7 +189,7 @@ impl Vim {
window: &mut Window, window: &mut Window,
cx: &mut Context<Self>, 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); let text_layout_details = editor.text_layout_details(window);
if vim.mode == Mode::VisualBlock if vim.mode == Mode::VisualBlock
&& !matches!( && !matches!(
@ -397,7 +397,7 @@ impl Vim {
self.switch_mode(target_mode, true, window, cx); 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| { editor.change_selections(Default::default(), window, cx, |s| {
s.move_with(|map, selection| { s.move_with(|map, selection| {
let mut mut_selection = selection.clone(); let mut mut_selection = selection.clone();
@ -475,7 +475,7 @@ impl Vim {
window: &mut Window, window: &mut Window,
cx: &mut Context<Self>, 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.split_selection_into_lines(&Default::default(), window, cx);
editor.change_selections(Default::default(), window, cx, |s| { editor.change_selections(Default::default(), window, cx, |s| {
s.move_cursors_with(|map, cursor, _| { s.move_cursors_with(|map, cursor, _| {
@ -493,7 +493,7 @@ impl Vim {
window: &mut Window, window: &mut Window,
cx: &mut Context<Self>, 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.split_selection_into_lines(&Default::default(), window, cx);
editor.change_selections(Default::default(), window, cx, |s| { editor.change_selections(Default::default(), window, cx, |s| {
s.move_cursors_with(|map, cursor, _| { 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>) { 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| { editor.change_selections(Default::default(), window, cx, |s| {
s.move_with(|_, selection| { s.move_with(|_, selection| {
selection.reversed = !selection.reversed; selection.reversed = !selection.reversed;
@ -533,7 +533,7 @@ impl Vim {
cx: &mut Context<Self>, cx: &mut Context<Self>,
) { ) {
let mode = self.mode; 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| { editor.change_selections(Default::default(), window, cx, |s| {
s.move_with(|_, selection| { s.move_with(|_, selection| {
selection.reversed = !selection.reversed; 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>) { pub fn visual_delete(&mut self, line_mode: bool, window: &mut Window, cx: &mut Context<Self>) {
self.store_visual_marks(window, cx); 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 mut original_columns: HashMap<_, _> = Default::default();
let line_mode = line_mode || editor.selections.line_mode; let line_mode = line_mode || editor.selections.line_mode;
editor.selections.line_mode = false; 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>) { pub fn visual_yank(&mut self, line_mode: bool, window: &mut Window, cx: &mut Context<Self>) {
self.store_visual_marks(window, cx); 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; let line_mode = line_mode || editor.selections.line_mode;
// For visual line mode, adjust selections to avoid yanking the next line when on \n // 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>, cx: &mut Context<Self>,
) { ) {
self.stop_recording(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.transact(window, cx, |editor, window, cx| {
let (display_map, selections) = editor.selections.all_adjusted_display(cx); let (display_map, selections) = editor.selections.all_adjusted_display(cx);
@ -722,7 +722,7 @@ impl Vim {
Vim::take_forced_motion(cx); Vim::take_forced_motion(cx);
let count = let count =
Vim::take_count(cx).unwrap_or_else(|| if self.mode.is_visual() { 1 } else { 2 }); 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); editor.set_clip_at_line_ends(false, cx);
for _ in 0..count { for _ in 0..count {
if editor if editor
@ -745,7 +745,7 @@ impl Vim {
Vim::take_forced_motion(cx); Vim::take_forced_motion(cx);
let count = let count =
Vim::take_count(cx).unwrap_or_else(|| if self.mode.is_visual() { 1 } else { 2 }); 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 { for _ in 0..count {
if editor if editor
.select_previous(&Default::default(), window, cx) .select_previous(&Default::default(), window, cx)
@ -773,7 +773,7 @@ impl Vim {
let mut start_selection = 0usize; let mut start_selection = 0usize;
let mut end_selection = 0usize; let mut end_selection = 0usize;
self.update_editor(window, cx, |_, editor, _, _| { self.update_editor(cx, |_, editor, _| {
editor.set_collapse_matches(false); editor.set_collapse_matches(false);
}); });
if vim_is_normal { 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); let latest = editor.selections.newest::<usize>(cx);
start_selection = latest.start; start_selection = latest.start;
end_selection = latest.end; end_selection = latest.end;
@ -812,7 +812,7 @@ impl Vim {
self.stop_replaying(cx); self.stop_replaying(cx);
return; return;
} }
self.update_editor(window, cx, |_, editor, window, cx| { self.update_editor(cx, |_, editor, cx| {
let latest = editor.selections.newest::<usize>(cx); let latest = editor.selections.newest::<usize>(cx);
if vim_is_normal { if vim_is_normal {
start_selection = latest.start; start_selection = latest.start;

View file

@ -0,0 +1,5 @@
{"Put":{"state":"hello\nˇ\nworld"}}
{"Key":"i"}
{"Key":"ctrl-y"}
{"Key":"ctrl-e"}
{"Get":{"state":"hello\nhoˇ\nworld","mode":"Insert"}}

View file

@ -542,6 +542,20 @@ define_connection! {
ALTER TABLE breakpoints ADD COLUMN condition TEXT; ALTER TABLE breakpoints ADD COLUMN condition TEXT;
ALTER TABLE breakpoints ADD COLUMN hit_condition TEXT; ALTER TABLE breakpoints ADD COLUMN hit_condition TEXT;
), ),
sql!(CREATE TABLE toolchains2 (
workspace_id INTEGER,
worktree_id INTEGER,
language_name TEXT NOT NULL,
name TEXT NOT NULL,
path TEXT NOT NULL,
raw_json TEXT NOT NULL,
relative_worktree_path TEXT NOT NULL,
PRIMARY KEY (workspace_id, worktree_id, language_name, relative_worktree_path)) STRICT;
INSERT INTO toolchains2
SELECT * FROM toolchains;
DROP TABLE toolchains;
ALTER TABLE toolchains2 RENAME TO toolchains;
)
]; ];
} }
@ -1428,12 +1442,12 @@ impl WorkspaceDb {
self.write(move |conn| { self.write(move |conn| {
let mut insert = conn let mut insert = conn
.exec_bound(sql!( .exec_bound(sql!(
INSERT INTO toolchains(workspace_id, worktree_id, relative_worktree_path, language_name, name, path) VALUES (?, ?, ?, ?, ?, ?) INSERT INTO toolchains(workspace_id, worktree_id, relative_worktree_path, language_name, name, path, raw_json) VALUES (?, ?, ?, ?, ?, ?, ?)
ON CONFLICT DO ON CONFLICT DO
UPDATE SET UPDATE SET
name = ?5, name = ?5,
path = ?6 path = ?6,
raw_json = ?7
)) ))
.context("Preparing insertion")?; .context("Preparing insertion")?;
@ -1444,6 +1458,7 @@ impl WorkspaceDb {
toolchain.language_name.as_ref(), toolchain.language_name.as_ref(),
toolchain.name.as_ref(), toolchain.name.as_ref(),
toolchain.path.as_ref(), toolchain.path.as_ref(),
toolchain.as_json.to_string(),
))?; ))?;
Ok(()) Ok(())

Binary file not shown.

Before

Width:  |  Height:  |  Size: 146 KiB

After

Width:  |  Height:  |  Size: 189 KiB

Before After
Before After

View file

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

View file

@ -26,6 +26,7 @@ collections.workspace = true
command_palette_hooks.workspace = true command_palette_hooks.workspace = true
copilot.workspace = true copilot.workspace = true
db.workspace = true db.workspace = true
edit_prediction.workspace = true
editor.workspace = true editor.workspace = true
feature_flags.workspace = true feature_flags.workspace = true
fs.workspace = true fs.workspace = true
@ -33,13 +34,13 @@ futures.workspace = true
gpui.workspace = true gpui.workspace = true
http_client.workspace = true http_client.workspace = true
indoc.workspace = true indoc.workspace = true
edit_prediction.workspace = true
language.workspace = true language.workspace = true
language_model.workspace = true language_model.workspace = true
log.workspace = true log.workspace = true
menu.workspace = true menu.workspace = true
postage.workspace = true postage.workspace = true
project.workspace = true project.workspace = true
rand.workspace = true
regex.workspace = true regex.workspace = true
release_channel.workspace = true release_channel.workspace = true
serde.workspace = true serde.workspace = true

View file

@ -429,6 +429,7 @@ impl Zeta {
body, body,
editable_range, editable_range,
} = gather_task.await?; } = gather_task.await?;
let done_gathering_context_at = Instant::now();
log::debug!( log::debug!(
"Events:\n{}\nExcerpt:\n{:?}", "Events:\n{}\nExcerpt:\n{:?}",
@ -481,6 +482,7 @@ impl Zeta {
} }
}; };
let received_response_at = Instant::now();
log::debug!("completion response: {}", &response.output_excerpt); log::debug!("completion response: {}", &response.output_excerpt);
if let Some(usage) = usage { if let Some(usage) = usage {
@ -492,7 +494,7 @@ impl Zeta {
.ok(); .ok();
} }
Self::process_completion_response( let edit_prediction = Self::process_completion_response(
response, response,
buffer, buffer,
&snapshot, &snapshot,
@ -505,7 +507,25 @@ impl Zeta {
buffer_snapshotted_at, buffer_snapshotted_at,
&cx, &cx,
) )
.await .await;
let finished_at = Instant::now();
// record latency for ~1% of requests
if rand::random::<u8>() <= 2 {
telemetry::event!(
"Edit Prediction Request",
context_latency = done_gathering_context_at
.duration_since(buffer_snapshotted_at)
.as_millis(),
request_latency = received_response_at
.duration_since(done_gathering_context_at)
.as_millis(),
process_latency = finished_at.duration_since(received_response_at).as_millis()
);
}
edit_prediction
}) })
} }

View file

@ -391,7 +391,7 @@ Zed will also use the `OPENAI_API_KEY` environment variable if it's defined.
#### Custom Models {#openai-custom-models} #### Custom Models {#openai-custom-models}
The Zed agent comes pre-configured to use the latest version for common models (GPT-3.5 Turbo, GPT-4, GPT-4 Turbo, GPT-4o, GPT-4o mini). The Zed agent comes pre-configured to use the latest version for common models (GPT-5, GPT-5 mini, o4-mini, GPT-4.1, and others).
To use alternate models, perhaps a preview release or a dated model release, or if you wish to control the request parameters, you can do so by adding the following to your Zed `settings.json`: To use alternate models, perhaps a preview release or a dated model release, or if you wish to control the request parameters, you can do so by adding the following to your Zed `settings.json`:
```json ```json

View file

@ -326,7 +326,7 @@ When you use `cargo build` or `cargo test` as the build command, Zed can infer t
[ [
{ {
"label": "Build & Debug native binary", "label": "Build & Debug native binary",
"adapter": "CodeLLDB" "adapter": "CodeLLDB",
"build": { "build": {
"command": "cargo", "command": "cargo",
"args": ["build"] "args": ["build"]