Compare commits

...

24 Commits

Author SHA1 Message Date
Max Brunsfeld
bfbb18476f Fix management of rust-analyzer binaries on windows (#36056)
Closes https://github.com/zed-industries/zed/issues/34472


* Avoid removing the just-downloaded exe
* Invoke exe within nested version directory

Release Notes:

- Fix issue where Rust-analyzer was not installed correctly on windows

Co-authored-by: Lukas Wirth <lukas@zed.dev>
2025-08-12 17:26:56 +00:00
Dino
978b75bba9 vim: Support filename in :tabedit and :tabnew commands (#35775)
Update both `:tabedit` and `:tabnew` commands in order to support a
single argument, a filename, that, when provided, ensures that the new
tab either opens an existing file or associates the new tab with the
filename, so that when saving the buffer's content, the file is created.

Relates to #21112 

Release Notes:

- vim: Added support for filenames in both `:tabnew` and `:tabedit` commands
2025-08-12 11:13:36 -06:00
localcc
1f20d5bf54 Fix nightly icon (#36051)
Release Notes:

- N/A
2025-08-12 16:18:42 +00:00
Rishabh Bothra
9de04ce215 language_models: Add vision support for OpenAI gpt-5, gpt-5-mini, and gpt-5-nano models (#36047)
## Summary
Enable image processing capabilities for GPT-5 series models by updating
the `supports_images()` method.

## Changes
- Add vision support for `gpt-5`, `gpt-5-mini`, and `gpt-5-nano` models
- Update `supports_images()` method in
`crates/language_models/src/provider/open_ai.rs`

## Models with Vision Support (after this PR)
- gpt-4o
- gpt-4o-mini
- gpt-4.1
- gpt-4.1-mini
- gpt-4.1-nano
- gpt-5 (new)
- gpt-5-mini (new)
- gpt-5-nano (new)
- o1
- o3
- o4-mini

This brings GPT-5 vision capabilities in line with other OpenAI models
that support image processing.

Release Notes:

- Added vision support for OpenAI models
2025-08-12 16:04:51 +00:00
Oleksiy Syvokon
d8fc53608e docs: Update OpenAI models list (#36050)
Closes #ISSUE

Release Notes:

- N/A
2025-08-12 16:03:13 +00:00
Joseph T. Lyons
39c19abdfd Update windows alpha GitHub Issue template (#36049)
Release Notes:

- N/A
2025-08-12 15:55:10 +00:00
Danilo Leal
b105028c05 agent2: Add custom UI for resource link content blocks (#36005)
Release Notes:

- N/A

---------

Co-authored-by: Agus Zubiaga <agus@zed.dev>
2025-08-12 12:39:27 -03:00
Piotr Osiewicz
d2162446d0 python: Fix venv activation in remote projects (#36043)
Crux of the issue was that we were checking whether a venv activation
script exists on local filesystem, which is obviously wrong for remote
projects. This PR also does away with `source` for venv activation in
favor of `.`, which is compliant with `sh`

Co-authored-by: Lukas Wirth <lukas@zed.dev>

Closes #34648

Release Notes:

- Python: fixed activation of virtual environments in terminals for
remote projects

Co-authored-by: Lukas Wirth <lukas@zed.dev>
2025-08-12 14:33:46 +00:00
Piotr Osiewicz
360d4db87c python: Fix flickering in the status bar (#36039)
- **util: Have maybe! use async closures instead of async blocks**
- **python: Fix flickering of virtual environment indicator in status
bar**

Closes #30723

Release Notes:

- Python: Fixed flickering of the status bar virtual environment
indicator

---------

Co-authored-by: Lukas Wirth <lukas@zed.dev>
2025-08-12 13:36:28 +00:00
Agus Zubiaga
44953375cc Include mention context in acp-based native agent (#36006)
Also adds data-layer support for symbols, thread, and rules.

Release Notes:

- N/A

---------

Co-authored-by: Cole Miller <cole@zed.dev>
2025-08-12 13:12:58 +00:00
Antonio Scandurra
2444321756 Support profiles in agent2 (#36034)
We still need a profile selector.

Release Notes:

- N/A

---------

Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
2025-08-12 12:17:48 +00:00
Piotr Osiewicz
13bf45dd4a python: Fix toolchain serialization not working with multiple venvs in a single worktree (#36035)
Our database did not allow more than entry for a given toolchain for a
single worktree (due to incorrect primary key)

Co-authored-by: Lukas Wirth <lukas@zed.dev>

Release Notes:

- Python: Fixed toolchain selector not working with multiple venvs in a
single worktree.

Co-authored-by: Lukas Wirth <lukas@zed.dev>
2025-08-12 12:10:53 +00:00
Lukas Spiss
b61b71405d go: Add support for running sub-tests in table tests (#35657)
One killer feature for the Go runner is to execute individual subtests
within a table-test easily. Goland has had this feature forever, while
in VSCode this has been notably missing.


https://github.com/user-attachments/assets/363417a2-d1b1-43ca-8377-08ce062d6104


Release Notes:

- Added support to run Go table-test subtests.
2025-08-12 11:56:33 +03:00
Michael Sloan
cc5eb24066 zeta: Add latency telemetry for 1% of edit predictions (#36020)
Release Notes:

- N/A

Co-authored-by: Oleksiy <oleksiy@zed.dev>
2025-08-12 06:47:54 +00:00
Conrad Irwin
52a9101970 vim: Add ctrl-y/e in insert mode (#36017)
Closes #17292

Release Notes:

- vim: Added ctrl-y/ctrl-e in insert mode to copy the next character
from the line above or below
2025-08-11 23:20:09 -06:00
Conrad Irwin
1a798830cb Fix running vim tests with --features neovim (#36014)
This was broken incidentally in
https://github.com/zed-industries/zed/pull/33417

A better fix would be to fix app shutdown to take control of the
executor so that we *can* run
foreground tasks; but that is a bit fiddly (draft #36015) 

Release Notes:

- N/A
2025-08-12 05:08:58 +00:00
Kirill Bulatov
481e3e5092 Ignore capability registrations with empty capabilities (#36000) 2025-08-12 07:53:20 +03:00
Matt
b35e69692d docs: Add a missing comma in Rust debugging JSON (#36007)
Update the Rust debugging doc to include a missing comma in one of the
example JSON's.
2025-08-12 03:06:02 +00:00
Conrad Irwin
add67bde43 Remove unnecessary argument from Vim#update_editor (#36001)
Release Notes:

- N/A
2025-08-11 16:10:06 -06:00
Victor Tran
fa3d0aaed4 gpui: Allow selection of "Services" menu independent of menu title (#34115)
Release Notes:

- N/A

---

In the same vein as #29538, the "Services" menu on macOS depended on the
text being exactly "Services", not allowing for i18n of the menu name.

This PR introduces a new menu type called `OsMenu` that defines a
special menu that can be populated by the system. Currently, it takes
one enum value, `ServicesMenu` that tells the system to populate its
contents with the items it would usually populate the "Services" menu
with.

An example of this being used has been implemented in the `set_menus`
example:
`cargo run -p gpui --example set_menus`

---

Point to consider:

In `mac/platform.rs:414` the existing code for setting the "Services"
menu remains for backwards compatibility. Should this remain now that
this new method exists to set the menu, or should it be removed?

---------

Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>
2025-08-11 21:10:14 +00:00
Danilo Leal
094e878ccf agent2: Refine terminal tool call display (#35984)
Release Notes:

- N/A
2025-08-11 17:50:47 -03:00
Joseph T. Lyons
54d4665100 Add windows issue template (#35998)
Release Notes:

- N/A
2025-08-11 19:25:18 +00:00
localcc
2c84e33b7b Fix icon padding (#35990)
Release Notes:

- N/A
2025-08-11 19:57:39 +02:00
Bennet Bo Fenner
bb6ea22944 agent2: Port more tools (#35987)
Release Notes:

- N/A

---------

Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
Co-authored-by: Antonio Scandurra <me@as-cii.com>
2025-08-11 17:24:48 +00:00
71 changed files with 3198 additions and 841 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

6
Cargo.lock generated
View File

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

View File

@@ -333,10 +333,14 @@
"ctrl-x ctrl-c": "editor::ShowEditPrediction", // zed specific
"ctrl-x ctrl-l": "editor::ToggleCodeActions", // zed specific
"ctrl-x ctrl-z": "editor::Cancel",
"ctrl-x ctrl-e": "vim::LineDown",
"ctrl-x ctrl-y": "vim::LineUp",
"ctrl-w": "editor::DeleteToPreviousWordStart",
"ctrl-u": "editor::DeleteToBeginningOfLine",
"ctrl-t": "vim::Indent",
"ctrl-d": "vim::Outdent",
"ctrl-y": "vim::InsertFromAbove",
"ctrl-e": "vim::InsertFromBelow",
"ctrl-k": ["vim::PushDigraph", {}],
"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.

View File

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

View File

@@ -1,13 +1,15 @@
mod connection;
mod diff;
mod mention;
mod terminal;
pub use connection::*;
pub use diff::*;
pub use mention::*;
pub use terminal::*;
use action_log::ActionLog;
use agent_client_protocol as acp;
use agent_client_protocol::{self as acp};
use anyhow::{Context as _, Result};
use editor::Bias;
use futures::{FutureExt, channel::oneshot, future::BoxFuture};
@@ -21,12 +23,7 @@ use std::error::Error;
use std::fmt::Formatter;
use std::process::ExitStatus;
use std::rc::Rc;
use std::{
fmt::Display,
mem,
path::{Path, PathBuf},
sync::Arc,
};
use std::{fmt::Display, mem, path::PathBuf, sync::Arc};
use ui::App;
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)]
pub struct AssistantMessage {
pub chunks: Vec<AssistantMessageChunk>,
@@ -254,6 +219,15 @@ impl ToolCall {
}
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);
}
}
@@ -325,6 +299,7 @@ impl Display for ToolCallStatus {
pub enum ContentBlock {
Empty,
Markdown { markdown: Entity<Markdown> },
ResourceLink { resource_link: acp::ResourceLink },
}
impl ContentBlock {
@@ -356,36 +331,67 @@ impl ContentBlock {
language_registry: &Arc<LanguageRegistry>,
cx: &mut App,
) {
let new_content = match block {
acp::ContentBlock::Text(text_content) => text_content.text.clone(),
acp::ContentBlock::ResourceLink(resource_link) => {
if let Some(path) = resource_link.uri.strip_prefix("file://") {
format!("{}", MentionPath(path.as_ref()))
} else {
resource_link.uri.clone()
}
if matches!(self, ContentBlock::Empty) {
if let acp::ContentBlock::ResourceLink(resource_link) = block {
*self = ContentBlock::ResourceLink { resource_link };
return;
}
acp::ContentBlock::Image(_)
| acp::ContentBlock::Audio(_)
| acp::ContentBlock::Resource(_) => String::new(),
};
}
let new_content = self.extract_content_from_block(block);
match self {
ContentBlock::Empty => {
*self = ContentBlock::Markdown {
markdown: cx.new(|cx| {
Markdown::new(
new_content.into(),
Some(language_registry.clone()),
None,
cx,
)
}),
};
*self = Self::create_markdown_block(new_content, language_registry, cx);
}
ContentBlock::Markdown { markdown } => {
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 {
ContentBlock::Empty => "",
ContentBlock::Markdown { markdown } => markdown.read(cx).source(),
ContentBlock::ResourceLink { resource_link } => &resource_link.uri,
}
}
@@ -400,6 +407,14 @@ impl ContentBlock {
match self {
ContentBlock::Empty => None,
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)]
mod tests {
use super::*;
@@ -1278,7 +1335,7 @@ mod tests {
use serde_json::json;
use settings::SettingsStore;
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;

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>,
) -> Self {
Self {
command: cx
.new(|cx| Markdown::new(command.into(), Some(language_registry.clone()), None, cx)),
command: cx.new(|cx| {
Markdown::new(
format!("```\n{}\n```", command).into(),
Some(language_registry.clone()),
None,
cx,
)
}),
working_dir,
terminal,
started_at: Instant::now(),

View File

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

View File

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

View File

@@ -1,8 +1,8 @@
use crate::{AgentResponseEvent, Thread, templates::Templates};
use crate::{
CopyPathTool, CreateDirectoryTool, EditFileTool, FindPathTool, GrepTool, ListDirectoryTool,
MovePathTool, NowTool, OpenTool, ReadFileTool, TerminalTool, ThinkingTool,
ToolCallAuthorization, WebSearchTool,
ContextServerRegistry, CopyPathTool, CreateDirectoryTool, DiagnosticsTool, EditFileTool,
FetchTool, FindPathTool, GrepTool, ListDirectoryTool, MessageContent, MovePathTool, NowTool,
OpenTool, ReadFileTool, TerminalTool, ThinkingTool, ToolCallAuthorization, WebSearchTool,
};
use acp_thread::ModelSelector;
use agent_client_protocol as acp;
@@ -55,6 +55,7 @@ pub struct NativeAgent {
project_context: Rc<RefCell<ProjectContext>>,
project_context_needs_refresh: watch::Sender<()>,
_maintain_project_context: Task<Result<()>>,
context_server_registry: Entity<ContextServerRegistry>,
/// Shared templates for all threads
templates: Arc<Templates>,
project: Entity<Project>,
@@ -90,6 +91,9 @@ impl NativeAgent {
_maintain_project_context: cx.spawn(async move |this, cx| {
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,
project,
prompt_store,
@@ -385,7 +389,13 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
// Create AcpThread
let acp_thread = cx.update(|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())?;
@@ -413,18 +423,30 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
})
.ok_or_else(|| {
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 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(CopyPathTool::new(project.clone()));
thread.add_tool(DiagnosticsTool::new(project.clone()));
thread.add_tool(MovePathTool::new(project.clone()));
thread.add_tool(ListDirectoryTool::new(project.clone()));
thread.add_tool(OpenTool::new(project.clone()));
thread.add_tool(ThinkingTool);
thread.add_tool(FindPathTool::new(project.clone()));
thread.add_tool(FetchTool::new(project.read(cx).client().http_client()));
thread.add_tool(GrepTool::new(project.clone()));
thread.add_tool(ReadFileTool::new(project.clone(), action_log));
thread.add_tool(EditFileTool::new(cx.entity()));
@@ -448,7 +470,7 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
acp_thread: acp_thread.downgrade(),
_subscription: cx.observe_release(&acp_thread, |this, acp_thread, _cx| {
this.sessions.remove(acp_thread.session_id());
})
}),
},
);
})?;
@@ -494,10 +516,13 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
})?;
log::debug!("Found session for: {}", session_id);
// Convert prompt to message
let message = convert_prompt_to_message(params.prompt);
let message: Vec<MessageContent> = params
.prompt
.into_iter()
.map(Into::into)
.collect::<Vec<_>>();
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 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)]
mod tests {
use super::*;

View File

@@ -1,7 +1,9 @@
use super::*;
use crate::MessageContent;
use acp_thread::AgentConnection;
use action_log::ActionLog;
use agent_client_protocol::{self as acp};
use agent_settings::AgentProfileId;
use anyhow::Result;
use client::{Client, UserStore};
use fs::{FakeFs, Fs};
@@ -12,8 +14,8 @@ use gpui::{
use indoc::indoc;
use language_model::{
LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelId,
LanguageModelRegistry, LanguageModelToolResult, LanguageModelToolUse, MessageContent, Role,
StopReason, fake_provider::FakeLanguageModel,
LanguageModelRegistry, LanguageModelToolResult, LanguageModelToolUse, Role, StopReason,
fake_provider::FakeLanguageModel,
};
use project::Project;
use prompt_store::ProjectContext;
@@ -165,7 +167,9 @@ async fn test_basic_tool_calls(cx: &mut TestAppContext) {
} else {
false
}
})
}),
"{}",
thread.to_markdown()
);
});
}
@@ -269,14 +273,14 @@ async fn test_tool_authorization(cx: &mut TestAppContext) {
assert_eq!(
message.content,
vec![
MessageContent::ToolResult(LanguageModelToolResult {
language_model::MessageContent::ToolResult(LanguageModelToolResult {
tool_use_id: tool_call_auth_1.tool_call.id.0.to_string().into(),
tool_name: ToolRequiringPermission.name().into(),
is_error: false,
content: "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_name: ToolRequiringPermission.name().into(),
is_error: true,
@@ -309,13 +313,15 @@ async fn test_tool_authorization(cx: &mut TestAppContext) {
let message = completion.messages.last().unwrap();
assert_eq!(
message.content,
vec![MessageContent::ToolResult(LanguageModelToolResult {
tool_use_id: tool_call_auth_3.tool_call.id.0.to_string().into(),
tool_name: ToolRequiringPermission.name().into(),
is_error: false,
content: "Allowed".into(),
output: Some("Allowed".into())
})]
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.
@@ -334,13 +340,15 @@ async fn test_tool_authorization(cx: &mut TestAppContext) {
let message = completion.messages.last().unwrap();
assert_eq!(
message.content,
vec![MessageContent::ToolResult(LanguageModelToolResult {
tool_use_id: "tool_id_4".into(),
tool_name: ToolRequiringPermission.name().into(),
is_error: false,
content: "Allowed".into(),
output: Some("Allowed".into())
})]
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())
}
)]
);
}
@@ -469,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]
#[ignore = "can't run on CI yet"]
async fn test_cancellation(cx: &mut TestAppContext) {
@@ -595,6 +679,7 @@ async fn test_agent_connection(cx: &mut TestAppContext) {
language_models::init(user_store.clone(), client.clone(), cx);
Project::init_settings(cx);
LanguageModelRegistry::test(cx);
agent_settings::init(cx);
});
cx.executor().forbid_parking();
@@ -790,6 +875,7 @@ async fn test_tool_updates_to_completion(cx: &mut TestAppContext) {
id: acp::ToolCallId("1".into()),
fields: acp::ToolCallUpdateFields {
status: Some(acp::ToolCallStatus::Completed),
raw_output: Some("Finished thinking.".into()),
..Default::default()
},
}
@@ -813,6 +899,7 @@ struct ThreadTest {
model: Arc<dyn LanguageModel>,
thread: Entity<Thread>,
project_context: Rc<RefCell<ProjectContext>>,
fs: Arc<FakeFs>,
}
enum TestModel {
@@ -835,30 +922,57 @@ async fn setup(cx: &mut TestAppContext, model: TestModel) -> ThreadTest {
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| {
settings::init(cx);
watch_settings(fs.clone(), 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();
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
.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 {
Task::ready(Arc::new(FakeLanguageModel::default()) as Arc<_>)
} else {
@@ -881,20 +995,25 @@ async fn setup(cx: &mut TestAppContext, model: TestModel) -> ThreadTest {
.await;
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 thread = cx.new(|_| {
let thread = cx.new(|cx| {
Thread::new(
project,
project_context.clone(),
context_server_registry,
action_log,
templates,
model.clone(),
cx,
)
});
ThreadTest {
model,
thread,
project_context,
fs,
}
}

View File

@@ -1,7 +1,8 @@
use crate::{SystemPromptTemplate, Template, Templates};
use crate::{ContextServerRegistry, SystemPromptTemplate, Template, Templates};
use acp_thread::MentionUri;
use action_log::ActionLog;
use agent_client_protocol as acp;
use agent_settings::AgentSettings;
use agent_settings::{AgentProfileId, AgentSettings};
use anyhow::{Context as _, Result, anyhow};
use assistant_tool::adapt_schema_to_format;
use cloud_llm_client::{CompletionIntent, CompletionMode};
@@ -13,10 +14,10 @@ use futures::{
};
use gpui::{App, Context, Entity, SharedString, Task};
use language_model::{
LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent,
LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelImage,
LanguageModelRequest, LanguageModelRequestMessage, LanguageModelRequestTool,
LanguageModelToolResult, LanguageModelToolResultContent, LanguageModelToolSchemaFormat,
LanguageModelToolUse, LanguageModelToolUseId, MessageContent, Role, StopReason,
LanguageModelToolUse, LanguageModelToolUseId, Role, StopReason,
};
use log;
use project::Project;
@@ -25,7 +26,8 @@ use schemars::{JsonSchema, Schema};
use serde::{Deserialize, Serialize};
use settings::{Settings, update_settings_file};
use smol::stream::StreamExt;
use std::{cell::RefCell, collections::BTreeMap, fmt::Write, 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};
#[derive(Debug, Clone)]
@@ -34,6 +36,23 @@ pub struct AgentMessage {
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 {
pub fn to_markdown(&self) -> String {
let mut markdown = format!("## {}\n", self.role);
@@ -93,6 +112,9 @@ impl AgentMessage {
.unwrap();
}
}
MessageContent::Mention { uri, .. } => {
write!(markdown, "{}", uri.to_link()).ok();
}
}
}
@@ -126,6 +148,8 @@ pub struct Thread {
running_turn: Option<Task<()>>,
pending_tool_uses: HashMap<LanguageModelToolUseId, LanguageModelToolUse>,
tools: BTreeMap<SharedString, Arc<dyn AnyAgentTool>>,
context_server_registry: Entity<ContextServerRegistry>,
profile_id: AgentProfileId,
project_context: Rc<RefCell<ProjectContext>>,
templates: Arc<Templates>,
pub selected_model: Arc<dyn LanguageModel>,
@@ -137,16 +161,21 @@ impl Thread {
pub fn new(
project: Entity<Project>,
project_context: Rc<RefCell<ProjectContext>>,
context_server_registry: Entity<ContextServerRegistry>,
action_log: Entity<ActionLog>,
templates: Arc<Templates>,
default_model: Arc<dyn LanguageModel>,
cx: &mut Context<Self>,
) -> Self {
let profile_id = AgentSettings::get_global(cx).default_profile.clone();
Self {
messages: Vec::new(),
completion_mode: CompletionMode::Normal,
running_turn: None,
pending_tool_uses: HashMap::default(),
tools: BTreeMap::default(),
context_server_registry,
profile_id,
project_context,
templates,
selected_model: default_model,
@@ -179,6 +208,10 @@ impl Thread {
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) {
self.running_turn.take();
@@ -203,10 +236,11 @@ impl Thread {
/// The returned channel will report all the occurrences in which the model stops before erroring or ending its turn.
pub fn send(
&mut self,
content: impl Into<MessageContent>,
content: impl Into<UserMessage>,
cx: &mut Context<Self>,
) -> mpsc::UnboundedReceiver<Result<AgentResponseEvent, LanguageModelCompletionError>> {
let content = content.into();
let content = content.into().0;
let model = self.selected_model.clone();
log::info!("Thread::send called with model: {:?}", model.name());
log::debug!("Thread::send content: {:?}", content);
@@ -219,7 +253,7 @@ impl Thread {
let user_message_ix = self.messages.len();
self.messages.push(AgentMessage {
role: Role::User,
content: vec![content],
content,
});
log::info!("Total messages in thread: {}", self.messages.len());
self.running_turn = Some(cx.spawn(async move |thread, cx| {
@@ -298,6 +332,7 @@ impl Thread {
} else {
acp::ToolCallStatus::Completed
}),
raw_output: tool_result.output.clone(),
..Default::default()
},
);
@@ -341,7 +376,7 @@ impl Thread {
log::debug!("System message built");
AgentMessage {
role: Role::System,
content: vec![prompt.into()],
content: vec![prompt.as_str().into()],
}
}
@@ -604,21 +639,23 @@ impl Thread {
let messages = self.build_request_messages();
log::info!("Request will include {} messages", messages.len());
let tools: Vec<LanguageModelRequestTool> = self
.tools
.values()
.filter_map(|tool| {
let tool_name = tool.name().to_string();
log::trace!("Including tool: {}", tool_name);
Some(LanguageModelRequestTool {
name: tool_name,
description: tool.description(cx).to_string(),
input_schema: tool
.input_schema(self.selected_model.tool_input_format())
.log_err()?,
let tools = if let Some(tools) = self.tools(cx).log_err() {
tools
.filter_map(|tool| {
let tool_name = tool.name().to_string();
log::trace!("Including tool: {}", tool_name);
Some(LanguageModelRequestTool {
name: tool_name,
description: tool.description().to_string(),
input_schema: tool
.input_schema(self.selected_model.tool_input_format())
.log_err()?,
})
})
})
.collect();
.collect()
} else {
Vec::new()
};
log::info!("Request includes {} tools", tools.len());
@@ -639,6 +676,35 @@ impl Thread {
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> {
log::trace!(
"Building request messages from {} thread messages",
@@ -658,11 +724,7 @@ impl Thread {
},
message.content.len()
);
LanguageModelRequestMessage {
role: message.role,
content: message.content.clone(),
cache: false,
}
message.to_request()
})
.collect();
messages
@@ -677,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
where
Self: 'static + Sized,
@@ -686,7 +762,7 @@ where
fn name(&self) -> SharedString;
fn description(&self, _cx: &mut App) -> SharedString {
fn description(&self) -> SharedString {
let schema = schemars::schema_for!(Self::Input);
SharedString::new(
schema
@@ -722,13 +798,13 @@ where
pub struct Erased<T>(T);
pub struct AgentToolOutput {
llm_output: LanguageModelToolResultContent,
raw_output: serde_json::Value,
pub llm_output: LanguageModelToolResultContent,
pub raw_output: serde_json::Value,
}
pub trait AnyAgentTool {
fn name(&self) -> SharedString;
fn description(&self, cx: &mut App) -> SharedString;
fn description(&self) -> SharedString;
fn kind(&self) -> acp::ToolKind;
fn initial_title(&self, input: serde_json::Value) -> SharedString;
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value>;
@@ -748,8 +824,8 @@ where
self.0.name()
}
fn description(&self, cx: &mut App) -> SharedString {
self.0.description(cx)
fn description(&self) -> SharedString {
self.0.description()
}
fn kind(&self) -> agent_client_protocol::ToolKind {
@@ -1059,3 +1135,207 @@ impl std::ops::DerefMut for ToolCallEventStreamReceiver {
&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 create_directory_tool;
mod delete_path_tool;
mod diagnostics_tool;
mod edit_file_tool;
mod fetch_tool;
mod find_path_tool;
mod grep_tool;
mod list_directory_tool;
@@ -13,10 +16,13 @@ mod terminal_tool;
mod thinking_tool;
mod web_search_tool;
pub use context_server_registry::*;
pub use copy_path_tool::*;
pub use create_directory_tool::*;
pub use delete_path_tool::*;
pub use diagnostics_tool::*;
pub use edit_file_tool::*;
pub use fetch_tool::*;
pub use find_path_tool::*;
pub use grep_tool::*;
pub use list_directory_tool::*;

View File

@@ -0,0 +1,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

@@ -454,9 +454,8 @@ fn resolve_path(
#[cfg(test)]
mod tests {
use crate::Templates;
use super::*;
use crate::{ContextServerRegistry, Templates};
use action_log::ActionLog;
use client::TelemetrySettings;
use fs::Fs;
@@ -475,9 +474,20 @@ mod tests {
fs.insert_tree("/root", json!({})).await;
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
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 thread =
cx.new(|_| Thread::new(project, Rc::default(), action_log, Templates::new(), model));
let thread = cx.new(|cx| {
Thread::new(
project,
Rc::default(),
context_server_registry,
action_log,
Templates::new(),
model,
cx,
)
});
let result = cx
.update(|cx| {
let input = EditFileToolInput {
@@ -661,14 +671,18 @@ mod tests {
});
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 thread = cx.new(|_| {
let thread = cx.new(|cx| {
Thread::new(
project,
Rc::default(),
context_server_registry,
action_log.clone(),
Templates::new(),
model.clone(),
cx,
)
});
@@ -792,15 +806,19 @@ mod tests {
.unwrap();
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 model = Arc::new(FakeLanguageModel::default());
let thread = cx.new(|_| {
let thread = cx.new(|cx| {
Thread::new(
project,
Rc::default(),
context_server_registry,
action_log.clone(),
Templates::new(),
model.clone(),
cx,
)
});
@@ -914,15 +932,19 @@ mod tests {
init_test(cx);
let fs = project::FakeFs::new(cx.executor());
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 model = Arc::new(FakeLanguageModel::default());
let thread = cx.new(|_| {
let thread = cx.new(|cx| {
Thread::new(
project,
Rc::default(),
context_server_registry,
action_log.clone(),
Templates::new(),
model.clone(),
cx,
)
});
let tool = Arc::new(EditFileTool { thread });
@@ -1041,15 +1063,19 @@ mod tests {
let fs = project::FakeFs::new(cx.executor());
fs.insert_tree("/project", json!({})).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 model = Arc::new(FakeLanguageModel::default());
let thread = cx.new(|_| {
let thread = cx.new(|cx| {
Thread::new(
project,
Rc::default(),
context_server_registry,
action_log.clone(),
Templates::new(),
model.clone(),
cx,
)
});
let tool = Arc::new(EditFileTool { thread });
@@ -1148,14 +1174,18 @@ mod tests {
.await;
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 thread = cx.new(|_| {
let thread = cx.new(|cx| {
Thread::new(
project.clone(),
Rc::default(),
context_server_registry.clone(),
action_log.clone(),
Templates::new(),
model.clone(),
cx,
)
});
let tool = Arc::new(EditFileTool { thread });
@@ -1225,14 +1255,18 @@ mod tests {
.await;
let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
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 thread = cx.new(|_| {
let thread = cx.new(|cx| {
Thread::new(
project.clone(),
Rc::default(),
context_server_registry.clone(),
action_log.clone(),
Templates::new(),
model.clone(),
cx,
)
});
let tool = Arc::new(EditFileTool { thread });
@@ -1305,14 +1339,18 @@ mod tests {
.await;
let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
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 thread = cx.new(|_| {
let thread = cx.new(|cx| {
Thread::new(
project.clone(),
Rc::default(),
context_server_registry.clone(),
action_log.clone(),
Templates::new(),
model.clone(),
cx,
)
});
let tool = Arc::new(EditFileTool { thread });
@@ -1382,14 +1420,18 @@ mod tests {
let fs = project::FakeFs::new(cx.executor());
let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
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 thread = cx.new(|_| {
let thread = cx.new(|cx| {
Thread::new(
project.clone(),
Rc::default(),
context_server_registry,
action_log.clone(),
Templates::new(),
model.clone(),
cx,
)
});
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(),
),
raw_output: Some(serde_json::json!({
"paths": &matches,
})),
..Default::default()
});

View File

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

View File

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

View File

@@ -48,6 +48,20 @@ pub struct AgentProfileSettings {
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)]
pub struct ContextServerPreset {
pub tools: IndexMap<Arc<str>, bool>,

View File

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

View File

@@ -1,6 +1,6 @@
use acp_thread::{
AcpThread, AcpThreadEvent, AgentThreadEntry, AssistantMessage, AssistantMessageChunk,
LoadError, MentionPath, ThreadStatus, ToolCall, ToolCallContent, ToolCallStatus,
LoadError, MentionUri, ThreadStatus, ToolCall, ToolCallContent, ToolCallStatus,
};
use acp_thread::{AgentConnection, Plan};
use action_log::ActionLog;
@@ -28,6 +28,7 @@ use markdown::{HeadingLevelStyles, Markdown, MarkdownElement, MarkdownStyle};
use parking_lot::Mutex;
use project::{CompletionIntent, Project};
use settings::{Settings as _, SettingsStore};
use std::path::PathBuf;
use std::{
cell::RefCell, collections::BTreeMap, path::Path, process::ExitStatus, rc::Rc, sync::Arc,
time::Duration,
@@ -38,7 +39,7 @@ use theme::ThemeSettings;
use ui::{
Disclosure, Divider, DividerColor, KeyBinding, Scrollbar, ScrollbarState, Tooltip, prelude::*,
};
use util::ResultExt;
use util::{ResultExt, size::format_file_size, time::duration_alt_display};
use workspace::{CollaboratorId, Workspace};
use zed_actions::agent::{Chat, NextHistoryMessage, PreviousHistoryMessage};
@@ -75,6 +76,7 @@ pub struct AcpThreadView {
edits_expanded: bool,
plan_expanded: bool,
editor_expanded: bool,
terminal_expanded: bool,
message_history: Rc<RefCell<MessageHistory<Vec<acp::ContentBlock>>>>,
_cancel_task: Option<Task<()>>,
_subscriptions: [Subscription; 1],
@@ -200,6 +202,7 @@ impl AcpThreadView {
edits_expanded: false,
plan_expanded: false,
editor_expanded: false,
terminal_expanded: true,
message_history,
_subscriptions: [subscription],
_cancel_task: None,
@@ -374,81 +377,101 @@ impl AcpThreadView {
let mut ix = 0;
let mut chunks: Vec<acp::ContentBlock> = Vec::new();
let project = self.project.clone();
self.message_editor.update(cx, |editor, cx| {
let text = editor.text(cx);
editor.display_map.update(cx, |map, cx| {
let snapshot = map.snapshot(cx);
for (crease_id, crease) in snapshot.crease_snapshot.creases() {
// Skip creases that have been edited out of the message buffer.
if !crease.range().start.is_valid(&snapshot.buffer_snapshot) {
continue;
}
if let Some(project_path) =
self.mention_set.lock().path_for_crease_id(crease_id)
{
let crease_range = crease.range().to_offset(&snapshot.buffer_snapshot);
if crease_range.start > ix {
chunks.push(text[ix..crease_range.start].into());
let contents = self.mention_set.lock().contents(project, cx);
cx.spawn_in(window, async move |this, cx| {
let contents = match contents.await {
Ok(contents) => contents,
Err(e) => {
this.update(cx, |this, cx| {
this.last_error =
Some(cx.new(|cx| Markdown::new(e.to_string().into(), None, None, cx)));
})
.ok();
return;
}
};
this.update_in(cx, |this, window, cx| {
this.message_editor.update(cx, |editor, cx| {
let text = editor.text(cx);
editor.display_map.update(cx, |map, cx| {
let snapshot = map.snapshot(cx);
for (crease_id, crease) in snapshot.crease_snapshot.creases() {
// Skip creases that have been edited out of the message buffer.
if !crease.range().start.is_valid(&snapshot.buffer_snapshot) {
continue;
}
if let Some(mention) = contents.get(&crease_id) {
let crease_range =
crease.range().to_offset(&snapshot.buffer_snapshot);
if crease_range.start > ix {
chunks.push(text[ix..crease_range.start].into());
}
chunks.push(acp::ContentBlock::Resource(acp::EmbeddedResource {
annotations: None,
resource: acp::EmbeddedResourceResource::TextResourceContents(
acp::TextResourceContents {
mime_type: None,
text: mention.content.clone(),
uri: mention.uri.to_uri(),
},
),
}));
ix = crease_range.end;
}
}
if let Some(abs_path) = project.read(cx).absolute_path(&project_path, cx) {
let path_str = abs_path.display().to_string();
chunks.push(acp::ContentBlock::ResourceLink(acp::ResourceLink {
uri: path_str.clone(),
name: path_str,
annotations: None,
description: None,
mime_type: None,
size: None,
title: None,
}));
if ix < text.len() {
let last_chunk = text[ix..].trim_end();
if !last_chunk.is_empty() {
chunks.push(last_chunk.into());
}
}
ix = crease_range.end;
}
})
});
if chunks.is_empty() {
return;
}
if ix < text.len() {
let last_chunk = text[ix..].trim_end();
if !last_chunk.is_empty() {
chunks.push(last_chunk.into());
}
}
})
});
if chunks.is_empty() {
return;
}
let Some(thread) = self.thread() else {
return;
};
let task = thread.update(cx, |thread, cx| thread.send(chunks.clone(), cx));
cx.spawn(async move |this, cx| {
let result = task.await;
this.update(cx, |this, cx| {
if let Err(err) = result {
this.last_error =
Some(cx.new(|cx| Markdown::new(err.to_string().into(), None, None, cx)))
}
let Some(thread) = this.thread() else {
return;
};
let task = thread.update(cx, |thread, cx| thread.send(chunks.clone(), cx));
cx.spawn(async move |this, cx| {
let result = task.await;
this.update(cx, |this, cx| {
if let Err(err) = result {
this.last_error =
Some(cx.new(|cx| {
Markdown::new(err.to_string().into(), None, None, cx)
}))
}
})
})
.detach();
let mention_set = this.mention_set.clone();
this.set_editor_is_expanded(false, cx);
this.message_editor.update(cx, |editor, cx| {
editor.clear(window, cx);
editor.remove_creases(mention_set.lock().drain(), cx)
});
this.scroll_to_bottom(cx);
this.message_history.borrow_mut().push(chunks);
})
.ok();
})
.detach();
let mention_set = self.mention_set.clone();
self.set_editor_is_expanded(false, cx);
self.message_editor.update(cx, |editor, cx| {
editor.clear(window, cx);
editor.remove_creases(mention_set.lock().drain(), cx)
});
self.scroll_to_bottom(cx);
self.message_history.borrow_mut().push(chunks);
}
fn previous_history_message(
@@ -561,16 +584,19 @@ impl AcpThreadView {
acp::ContentBlock::Text(text_content) => {
text.push_str(&text_content.text);
}
acp::ContentBlock::ResourceLink(resource_link) => {
let path = Path::new(&resource_link.uri);
acp::ContentBlock::Resource(acp::EmbeddedResource {
resource: acp::EmbeddedResourceResource::TextResourceContents(resource),
..
}) => {
let path = PathBuf::from(&resource.uri);
let project_path = project.read(cx).project_path_for_absolute_path(&path, cx);
let start = text.len();
let content = MentionPath::new(&path).to_string();
let content = MentionUri::File(path).to_uri();
text.push_str(&content);
let end = text.len();
if let Some(project_path) =
project.read(cx).project_path_for_absolute_path(&path, cx)
{
let filename: SharedString = path
if let Some(project_path) = project_path {
let filename: SharedString = project_path
.path
.file_name()
.unwrap_or_default()
.to_string_lossy()
@@ -581,7 +607,8 @@ impl AcpThreadView {
}
acp::ContentBlock::Image(_)
| acp::ContentBlock::Audio(_)
| acp::ContentBlock::Resource(_) => {}
| acp::ContentBlock::Resource(_)
| acp::ContentBlock::ResourceLink(_) => {}
}
}
@@ -600,18 +627,21 @@ impl AcpThreadView {
};
let anchor = snapshot.anchor_before(range.start);
let crease_id = crate::context_picker::insert_crease_for_mention(
anchor.excerpt_id,
anchor.text_anchor,
range.end - range.start,
filename,
crease_icon_path,
message_editor.clone(),
window,
cx,
);
if let Some(crease_id) = crease_id {
mention_set.lock().insert(crease_id, project_path);
if let Some(project_path) = project.read(cx).absolute_path(&project_path, cx) {
let crease_id = crate::context_picker::insert_crease_for_mention(
anchor.excerpt_id,
anchor.text_anchor,
range.end - range.start,
filename,
crease_icon_path,
message_editor.clone(),
window,
cx,
);
if let Some(crease_id) = crease_id {
mention_set.lock().insert(crease_id, project_path);
}
}
}
@@ -768,7 +798,7 @@ impl AcpThreadView {
window,
cx,
);
view.set_embedded_mode(None, cx);
view.set_embedded_mode(Some(1000), cx);
view
});
@@ -914,17 +944,26 @@ impl AcpThreadView {
.child(message_body)
.into_any()
}
AgentThreadEntry::ToolCall(tool_call) => div()
.w_full()
.py_1p5()
.px_5()
.child(self.render_tool_call(index, tool_call, window, cx))
.into_any(),
AgentThreadEntry::ToolCall(tool_call) => {
let has_terminals = tool_call.terminals().next().is_some();
div().w_full().py_1p5().px_5().map(|this| {
if has_terminals {
this.children(tool_call.terminals().map(|terminal| {
self.render_terminal_tool_call(terminal, tool_call, window, cx)
}))
} else {
this.child(self.render_tool_call(index, tool_call, window, cx))
}
})
}
.into_any(),
};
let Some(thread) = self.thread() else {
return primary;
};
let is_generating = matches!(thread.read(cx).status(), ThreadStatus::Generating);
if index == total_entries - 1 && !is_generating {
v_flex()
@@ -1069,10 +1108,10 @@ impl AcpThreadView {
.size(IconSize::Small)
.color(Color::Muted);
let base_container = h_flex().size_4().justify_center();
if is_collapsible {
h_flex()
.size_4()
.justify_center()
base_container
.child(
div()
.group_hover(&group_name, |s| s.invisible().w_0())
@@ -1103,7 +1142,7 @@ impl AcpThreadView {
),
)
} else {
div().child(tool_icon)
base_container.child(tool_icon)
}
}
@@ -1166,15 +1205,16 @@ impl AcpThreadView {
ToolCallContent::Diff(diff) => diff.read(cx).has_revealed_range(cx),
_ => false,
});
let is_collapsible =
!tool_call.content.is_empty() && !needs_confirmation && !is_edit && !has_diff;
let use_card_layout = needs_confirmation || is_edit || has_diff;
let is_collapsible = !tool_call.content.is_empty() && !use_card_layout;
let is_open = tool_call.content.is_empty()
|| needs_confirmation
|| has_nonempty_diff
|| self.expanded_tool_calls.contains(&tool_call.id);
let gradient_color = cx.theme().colors().panel_background;
let gradient_overlay = {
let gradient_overlay = |color: Hsla| {
div()
.absolute()
.top_0()
@@ -1183,13 +1223,43 @@ impl AcpThreadView {
.h_full()
.bg(linear_gradient(
90.,
linear_color_stop(gradient_color, 1.),
linear_color_stop(gradient_color.opacity(0.2), 0.),
linear_color_stop(color, 1.),
linear_color_stop(color.opacity(0.2), 0.),
))
};
let gradient_color = if use_card_layout {
self.tool_card_header_bg(cx)
} else {
cx.theme().colors().panel_background
};
let tool_output_display = match &tool_call.status {
ToolCallStatus::WaitingForConfirmation { options, .. } => v_flex()
.w_full()
.children(tool_call.content.iter().map(|content| {
div()
.child(self.render_tool_call_content(content, tool_call, window, cx))
.into_any_element()
}))
.child(self.render_permission_buttons(
options,
entry_ix,
tool_call.id.clone(),
tool_call.content.is_empty(),
cx,
)),
ToolCallStatus::Allowed { .. } | ToolCallStatus::Canceled => v_flex()
.w_full()
.children(tool_call.content.iter().map(|content| {
div()
.child(self.render_tool_call_content(content, tool_call, window, cx))
.into_any_element()
})),
ToolCallStatus::Rejected => v_flex().size_0(),
};
v_flex()
.when(needs_confirmation || is_edit || has_diff, |this| {
.when(use_card_layout, |this| {
this.rounded_lg()
.border_1()
.border_color(self.tool_card_border_color(cx))
@@ -1203,7 +1273,7 @@ impl AcpThreadView {
.gap_1()
.justify_between()
.map(|this| {
if needs_confirmation || is_edit || has_diff {
if use_card_layout {
this.pl_2()
.pr_1()
.py_1()
@@ -1220,13 +1290,6 @@ impl AcpThreadView {
.group(&card_header_id)
.relative()
.w_full()
.map(|this| {
if tool_call.locations.len() == 1 {
this.gap_0()
} else {
this.gap_1p5()
}
})
.text_size(self.tool_name_font_size())
.child(self.render_tool_call_icon(
card_header_id,
@@ -1270,6 +1333,7 @@ impl AcpThreadView {
.id("non-card-label-container")
.w_full()
.relative()
.ml_1p5()
.overflow_hidden()
.child(
h_flex()
@@ -1286,7 +1350,7 @@ impl AcpThreadView {
),
)),
)
.child(gradient_overlay)
.child(gradient_overlay(gradient_color))
.on_click(cx.listener({
let id = tool_call.id.clone();
move |this: &mut Self, _, _, cx: &mut Context<Self>| {
@@ -1303,87 +1367,110 @@ impl AcpThreadView {
)
.children(status_icon),
)
.when(is_open, |this| {
this.child(
v_flex()
.text_xs()
.when(is_collapsible, |this| {
this.mt_1()
.border_1()
.border_color(self.tool_card_border_color(cx))
.bg(cx.theme().colors().editor_background)
.rounded_lg()
})
.map(|this| {
if is_open {
match &tool_call.status {
ToolCallStatus::WaitingForConfirmation { options, .. } => this
.children(tool_call.content.iter().map(|content| {
div()
.py_1p5()
.child(
self.render_tool_call_content(
content, window, cx,
),
)
.into_any_element()
}))
.child(self.render_permission_buttons(
options,
entry_ix,
tool_call.id.clone(),
tool_call.content.is_empty(),
cx,
)),
ToolCallStatus::Allowed { .. } | ToolCallStatus::Canceled => {
this.children(tool_call.content.iter().map(|content| {
div()
.py_1p5()
.child(
self.render_tool_call_content(
content, window, cx,
),
)
.into_any_element()
}))
}
ToolCallStatus::Rejected => this,
}
} else {
this
}
}),
)
})
.when(is_open, |this| this.child(tool_output_display))
}
fn render_tool_call_content(
&self,
content: &ToolCallContent,
tool_call: &ToolCall,
window: &Window,
cx: &Context<Self>,
) -> AnyElement {
match content {
ToolCallContent::ContentBlock(content) => {
if let Some(md) = content.markdown() {
div()
.p_2()
.child(
self.render_markdown(
md.clone(),
default_markdown_style(false, window, cx),
),
)
.into_any_element()
if let Some(resource_link) = content.resource_link() {
self.render_resource_link(resource_link, cx)
} else if let Some(markdown) = content.markdown() {
self.render_markdown_output(markdown.clone(), tool_call.id.clone(), window, cx)
} else {
Empty.into_any_element()
}
}
ToolCallContent::Diff(diff) => self.render_diff_editor(&diff.read(cx).multibuffer()),
ToolCallContent::Terminal(terminal) => self.render_terminal(terminal),
ToolCallContent::Terminal(terminal) => {
self.render_terminal_tool_call(terminal, tool_call, window, cx)
}
}
}
fn render_markdown_output(
&self,
markdown: Entity<Markdown>,
tool_call_id: acp::ToolCallId,
window: &Window,
cx: &Context<Self>,
) -> AnyElement {
let button_id = SharedString::from(format!("tool_output-{:?}", tool_call_id.clone()));
v_flex()
.mt_1p5()
.ml(px(7.))
.px_3p5()
.gap_2()
.border_l_1()
.border_color(self.tool_card_border_color(cx))
.text_sm()
.text_color(cx.theme().colors().text_muted)
.child(self.render_markdown(markdown, default_markdown_style(false, window, cx)))
.child(
Button::new(button_id, "Collapse Output")
.full_width()
.style(ButtonStyle::Outlined)
.label_size(LabelSize::Small)
.icon(IconName::ChevronUp)
.icon_color(Color::Muted)
.icon_position(IconPosition::Start)
.on_click(cx.listener({
let id = tool_call_id.clone();
move |this: &mut Self, _, _, cx: &mut Context<Self>| {
this.expanded_tool_calls.remove(&id);
cx.notify();
}
})),
)
.into_any_element()
}
fn render_resource_link(
&self,
resource_link: &acp::ResourceLink,
cx: &Context<Self>,
) -> AnyElement {
let uri: SharedString = resource_link.uri.clone().into();
let label: SharedString = if let Some(path) = resource_link.uri.strip_prefix("file://") {
path.to_string().into()
} else {
uri.clone()
};
let button_id = SharedString::from(format!("item-{}", uri.clone()));
div()
.ml(px(7.))
.pl_2p5()
.border_l_1()
.border_color(self.tool_card_border_color(cx))
.overflow_hidden()
.child(
Button::new(button_id, label)
.label_size(LabelSize::Small)
.color(Color::Muted)
.icon(IconName::ArrowUpRight)
.icon_size(IconSize::XSmall)
.icon_color(Color::Muted)
.truncate(true)
.on_click(cx.listener({
let workspace = self.workspace.clone();
move |_, _, window, cx: &mut Context<Self>| {
Self::open_link(uri.clone(), &workspace, window, cx);
}
})),
)
.into_any_element()
}
fn render_permission_buttons(
&self,
options: &[acp::PermissionOption],
@@ -1393,14 +1480,22 @@ impl AcpThreadView {
cx: &Context<Self>,
) -> Div {
h_flex()
.p_1p5()
.py_1()
.pl_2()
.pr_1()
.gap_1()
.justify_end()
.justify_between()
.flex_wrap()
.when(!empty_content, |this| {
this.border_t_1()
.border_color(self.tool_card_border_color(cx))
})
.children(options.iter().map(|option| {
.child(
div()
.min_w(rems_from_px(145.))
.child(LoadingLabel::new("Waiting for Confirmation").size(LabelSize::Small)),
)
.child(h_flex().gap_0p5().children(options.iter().map(|option| {
let option_id = SharedString::from(option.id.0.clone());
Button::new((option_id, entry_ix), option.name.clone())
.map(|this| match option.kind {
@@ -1433,7 +1528,7 @@ impl AcpThreadView {
);
}
}))
}))
})))
}
fn render_diff_editor(&self, multibuffer: &Entity<MultiBuffer>) -> AnyElement {
@@ -1449,18 +1544,244 @@ impl AcpThreadView {
.into_any()
}
fn render_terminal(&self, terminal: &Entity<acp_thread::Terminal>) -> AnyElement {
v_flex()
.h_72()
fn render_terminal_tool_call(
&self,
terminal: &Entity<acp_thread::Terminal>,
tool_call: &ToolCall,
window: &Window,
cx: &Context<Self>,
) -> AnyElement {
let terminal_data = terminal.read(cx);
let working_dir = terminal_data.working_dir();
let command = terminal_data.command();
let started_at = terminal_data.started_at();
let tool_failed = matches!(
&tool_call.status,
ToolCallStatus::Rejected
| ToolCallStatus::Canceled
| ToolCallStatus::Allowed {
status: acp::ToolCallStatus::Failed,
..
}
);
let output = terminal_data.output();
let command_finished = output.is_some();
let truncated_output = output.is_some_and(|output| output.was_content_truncated);
let output_line_count = output.map(|output| output.content_line_count).unwrap_or(0);
let command_failed = command_finished
&& output.is_some_and(|o| o.exit_status.is_none_or(|status| !status.success()));
let time_elapsed = if let Some(output) = output {
output.ended_at.duration_since(started_at)
} else {
started_at.elapsed()
};
let header_bg = cx
.theme()
.colors()
.element_background
.blend(cx.theme().colors().editor_foreground.opacity(0.025));
let border_color = cx.theme().colors().border.opacity(0.6);
let working_dir = working_dir
.as_ref()
.map(|path| format!("{}", path.display()))
.unwrap_or_else(|| "current directory".to_string());
let header = h_flex()
.id(SharedString::from(format!(
"terminal-tool-header-{}",
terminal.entity_id()
)))
.flex_none()
.gap_1()
.justify_between()
.rounded_t_md()
.child(
if let Some(terminal_view) = self.terminal_views.get(&terminal.entity_id()) {
// TODO: terminal has all the state we need to reproduce
// what we had in the terminal card.
terminal_view.clone().into_any_element()
} else {
Empty.into_any()
},
div()
.id(("command-target-path", terminal.entity_id()))
.w_full()
.max_w_full()
.overflow_x_scroll()
.child(
Label::new(working_dir)
.buffer_font(cx)
.size(LabelSize::XSmall)
.color(Color::Muted),
),
)
.when(!command_finished, |header| {
header
.gap_1p5()
.child(
Button::new(
SharedString::from(format!("stop-terminal-{}", terminal.entity_id())),
"Stop",
)
.icon(IconName::Stop)
.icon_position(IconPosition::Start)
.icon_size(IconSize::Small)
.icon_color(Color::Error)
.label_size(LabelSize::Small)
.tooltip(move |window, cx| {
Tooltip::with_meta(
"Stop This Command",
None,
"Also possible by placing your cursor inside the terminal and using regular terminal bindings.",
window,
cx,
)
})
.on_click({
let terminal = terminal.clone();
cx.listener(move |_this, _event, _window, cx| {
let inner_terminal = terminal.read(cx).inner().clone();
inner_terminal.update(cx, |inner_terminal, _cx| {
inner_terminal.kill_active_task();
});
})
}),
)
.child(Divider::vertical())
.child(
Icon::new(IconName::ArrowCircle)
.size(IconSize::XSmall)
.color(Color::Info)
.with_animation(
"arrow-circle",
Animation::new(Duration::from_secs(2)).repeat(),
|icon, delta| {
icon.transform(Transformation::rotate(percentage(delta)))
},
),
)
})
.when(tool_failed || command_failed, |header| {
header.child(
div()
.id(("terminal-tool-error-code-indicator", terminal.entity_id()))
.child(
Icon::new(IconName::Close)
.size(IconSize::Small)
.color(Color::Error),
)
.when_some(output.and_then(|o| o.exit_status), |this, status| {
this.tooltip(Tooltip::text(format!(
"Exited with code {}",
status.code().unwrap_or(-1),
)))
}),
)
})
.when(truncated_output, |header| {
let tooltip = if let Some(output) = output {
if output_line_count + 10 > terminal::MAX_SCROLL_HISTORY_LINES {
"Output exceeded terminal max lines and was \
truncated, the model received the first 16 KB."
.to_string()
} else {
format!(
"Output is {} long—to avoid unexpected token usage, \
only 16 KB was sent back to the model.",
format_file_size(output.original_content_len as u64, true),
)
}
} else {
"Output was truncated".to_string()
};
header.child(
h_flex()
.id(("terminal-tool-truncated-label", terminal.entity_id()))
.gap_1()
.child(
Icon::new(IconName::Info)
.size(IconSize::XSmall)
.color(Color::Ignored),
)
.child(
Label::new("Truncated")
.color(Color::Muted)
.size(LabelSize::XSmall),
)
.tooltip(Tooltip::text(tooltip)),
)
})
.when(time_elapsed > Duration::from_secs(10), |header| {
header.child(
Label::new(format!("({})", duration_alt_display(time_elapsed)))
.buffer_font(cx)
.color(Color::Muted)
.size(LabelSize::XSmall),
)
})
.child(
Disclosure::new(
SharedString::from(format!(
"terminal-tool-disclosure-{}",
terminal.entity_id()
)),
self.terminal_expanded,
)
.opened_icon(IconName::ChevronUp)
.closed_icon(IconName::ChevronDown)
.on_click(cx.listener(move |this, _event, _window, _cx| {
this.terminal_expanded = !this.terminal_expanded;
})),
);
let show_output =
self.terminal_expanded && self.terminal_views.contains_key(&terminal.entity_id());
v_flex()
.mb_2()
.border_1()
.when(tool_failed || command_failed, |card| card.border_dashed())
.border_color(border_color)
.rounded_lg()
.overflow_hidden()
.child(
v_flex()
.pt_1()
.pb_2()
.px_2()
.gap_0p5()
.bg(header_bg)
.text_xs()
.child(header)
.child(
MarkdownElement::new(
command.clone(),
terminal_command_markdown_style(window, cx),
)
.code_block_renderer(
markdown::CodeBlockRenderer::Default {
copy_button: false,
copy_button_on_hover: true,
border: false,
},
),
),
)
.when(show_output, |this| {
let terminal_view = self.terminal_views.get(&terminal.entity_id()).unwrap();
this.child(
div()
.pt_2()
.border_t_1()
.when(tool_failed || command_failed, |card| card.border_dashed())
.border_color(border_color)
.bg(cx.theme().colors().editor_background)
.rounded_b_md()
.text_ui_sm(cx)
.child(terminal_view.clone()),
)
})
.into_any()
}
@@ -2311,25 +2632,31 @@ impl AcpThreadView {
return;
};
if let Some(mention_path) = MentionPath::try_parse(&url) {
workspace.update(cx, |workspace, cx| {
let project = workspace.project();
let Some((path, entry)) = project.update(cx, |project, cx| {
let path = project.find_project_path(mention_path.path(), cx)?;
let entry = project.entry_for_path(&path, cx)?;
Some((path, entry))
}) else {
return;
};
if let Some(mention) = MentionUri::parse(&url).log_err() {
workspace.update(cx, |workspace, cx| match mention {
MentionUri::File(path) => {
let project = workspace.project();
let Some((path, entry)) = project.update(cx, |project, cx| {
let path = project.find_project_path(path, cx)?;
let entry = project.entry_for_path(&path, cx)?;
Some((path, entry))
}) else {
return;
};
if entry.is_dir() {
project.update(cx, |_, cx| {
cx.emit(project::Event::RevealInProjectPanel(entry.id));
});
} else {
workspace
.open_path(path, None, true, window, cx)
.detach_and_log_err(cx);
if entry.is_dir() {
project.update(cx, |_, cx| {
cx.emit(project::Event::RevealInProjectPanel(entry.id));
});
} else {
workspace
.open_path(path, None, true, window, cx)
.detach_and_log_err(cx);
}
}
_ => {
// TODO
unimplemented!()
}
})
} else {
@@ -2724,6 +3051,7 @@ impl AcpThreadView {
anchor..anchor,
self.message_editor.clone(),
self.mention_set.clone(),
self.project.clone(),
cx,
);
@@ -2866,7 +3194,7 @@ fn user_message_markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
style.base_text_style = text_style;
style.link_callback = Some(Rc::new(move |url, cx| {
if MentionPath::try_parse(url).is_some() {
if MentionUri::parse(url).is_ok() {
let colors = cx.theme().colors();
Some(TextStyleRefinement {
background_color: Some(colors.element_background),
@@ -3030,6 +3358,18 @@ fn diff_editor_text_style_refinement(cx: &mut App) -> TextStyleRefinement {
}
}
fn terminal_command_markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
let default_md_style = default_markdown_style(true, window, cx);
MarkdownStyle {
base_text_style: TextStyle {
..default_md_style.base_text_style
},
selection_background_color: cx.theme().colors().element_selection_background,
..Default::default()
}
}
#[cfg(test)]
mod tests {
use agent_client_protocol::SessionId;

View File

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

View File

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

View File

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

View File

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

View File

@@ -167,6 +167,7 @@ fn generate_test_function(
));
cx_teardowns.extend(quote!(
dispatcher.run_until_parked();
#cx_varname.executor().forbid_parking();
#cx_varname.quit();
dispatcher.run_until_parked();
));
@@ -232,7 +233,7 @@ fn generate_test_function(
cx_teardowns.extend(quote!(
drop(#cx_varname_lock);
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();
));
continue;
@@ -247,6 +248,7 @@ fn generate_test_function(
));
cx_teardowns.extend(quote!(
dispatcher.run_until_parked();
#cx_varname.executor().forbid_parking();
#cx_varname.quit();
dispatcher.run_until_parked();
));

View File

@@ -301,7 +301,25 @@ impl LanguageModel for OpenAiLanguageModel {
}
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 {

View File

@@ -487,6 +487,8 @@ const GO_MODULE_ROOT_TASK_VARIABLE: VariableName =
VariableName::Custom(Cow::Borrowed("GO_MODULE_ROOT"));
const GO_SUBTEST_NAME_TASK_VARIABLE: VariableName =
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 {
fn build_context(
@@ -545,10 +547,19 @@ impl ContextProvider for GoContextProvider {
let go_subtest_variable = extract_subtest_name(_subtest_name.unwrap_or(""))
.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(
[
go_package_variable,
go_subtest_variable,
go_table_test_case_variable,
go_module_root_variable,
]
.into_iter()
@@ -570,6 +581,28 @@ impl ContextProvider for GoContextProvider {
let module_cwd = Some(GO_MODULE_ROOT_TASK_VARIABLE.template_value());
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 {
label: format!(
"go test {} -run {}",
@@ -842,10 +875,21 @@ mod tests {
.collect()
});
let tag_strings: Vec<String> = runnables
.iter()
.flat_map(|r| &r.runnable.tags)
.map(|tag| tag.0.to_string())
.collect();
assert!(
runnables.len() == 2,
"Should find test function and subtest with double quotes, found: {}",
runnables.len()
tag_strings.contains(&"go-test".to_string()),
"Should find go-test tag, found: {:?}",
tag_strings
);
assert!(
tag_strings.contains(&"go-subtest".to_string()),
"Should find go-subtest tag, found: {:?}",
tag_strings
);
let buffer = cx.new(|cx| {
@@ -860,10 +904,299 @@ mod tests {
.collect()
});
let tag_strings: Vec<String> = runnables
.iter()
.flat_map(|r| &r.runnable.tags)
.map(|tag| tag.0.to_string())
.collect();
assert!(
runnables.len() == 2,
"Should find test function and subtest with backticks, found: {}",
runnables.len()
tag_strings.contains(&"go-test".to_string()),
"Should find go-test tag, found: {:?}",
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)
)
; 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

@@ -238,7 +238,7 @@ impl LspAdapter for RustLspAdapter {
)
.await?;
make_file_executable(&server_path).await?;
remove_matching(&container_dir, |path| server_path != path).await;
remove_matching(&container_dir, |path| path != destination_path).await;
GithubBinaryMetadata::write_to_file(
&GithubBinaryMetadata {
metadata_version: 1,
@@ -1023,8 +1023,14 @@ async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServ
last = Some(path);
}
let path = last.context("no cached binary")?;
let path = match RustLspAdapter::GITHUB_ASSET_KIND {
AssetKind::TarGz | AssetKind::Gz => path.clone(), // Tar and gzip extract in place.
AssetKind::Zip => path.clone().join("rust-analyzer.exe"), // zip contains a .exe
};
anyhow::Ok(LanguageServerBinary {
path: last.context("no cached binary")?,
path,
env: None,
arguments: Default::default(),
})

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>) {
if let Some(capabilities) = serde_json::to_string(&server.capabilities()).ok() {
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.
}
"workspace/symbol" => {
let options = parse_register_capabilities(reg)?;
server.update_capabilities(|capabilities| {
capabilities.workspace_symbol_provider = Some(options);
});
notify_server_capabilities_updated(&server, cx);
if let Some(options) = parse_register_capabilities(reg)? {
server.update_capabilities(|capabilities| {
capabilities.workspace_symbol_provider = Some(options);
});
notify_server_capabilities_updated(&server, cx);
}
}
"workspace/fileOperations" => {
let caps = reg
.register_options
.map(serde_json::from_value)
.transpose()?
.unwrap_or_default();
server.update_capabilities(|capabilities| {
capabilities
.workspace
.get_or_insert_default()
.file_operations = Some(caps);
});
notify_server_capabilities_updated(&server, cx);
if let Some(options) = reg.register_options {
let caps = serde_json::from_value(options)?;
server.update_capabilities(|capabilities| {
capabilities
.workspace
.get_or_insert_default()
.file_operations = Some(caps);
});
notify_server_capabilities_updated(&server, cx);
}
}
"workspace/executeCommand" => {
let options = reg
.register_options
.map(serde_json::from_value)
.transpose()?
.unwrap_or_default();
server.update_capabilities(|capabilities| {
capabilities.execute_command_provider = Some(options);
});
notify_server_capabilities_updated(&server, cx);
if let Some(options) = reg.register_options {
let options = serde_json::from_value(options)?;
server.update_capabilities(|capabilities| {
capabilities.execute_command_provider = Some(options);
});
notify_server_capabilities_updated(&server, cx);
}
}
"textDocument/rangeFormatting" => {
let options = parse_register_capabilities(reg)?;
server.update_capabilities(|capabilities| {
capabilities.document_range_formatting_provider = Some(options);
});
notify_server_capabilities_updated(&server, cx);
if let Some(options) = parse_register_capabilities(reg)? {
server.update_capabilities(|capabilities| {
capabilities.document_range_formatting_provider = Some(options);
});
notify_server_capabilities_updated(&server, cx);
}
}
"textDocument/onTypeFormatting" => {
let options = reg
if let Some(options) = reg
.register_options
.map(serde_json::from_value)
.transpose()?
.unwrap_or_default();
server.update_capabilities(|capabilities| {
capabilities.document_on_type_formatting_provider = Some(options);
});
notify_server_capabilities_updated(&server, cx);
{
server.update_capabilities(|capabilities| {
capabilities.document_on_type_formatting_provider = Some(options);
});
notify_server_capabilities_updated(&server, cx);
}
}
"textDocument/formatting" => {
let options = parse_register_capabilities(reg)?;
server.update_capabilities(|capabilities| {
capabilities.document_formatting_provider = Some(options);
});
notify_server_capabilities_updated(&server, cx);
if let Some(options) = parse_register_capabilities(reg)? {
server.update_capabilities(|capabilities| {
capabilities.document_formatting_provider = Some(options);
});
notify_server_capabilities_updated(&server, cx);
}
}
"textDocument/rename" => {
let options = parse_register_capabilities(reg)?;
server.update_capabilities(|capabilities| {
capabilities.rename_provider = Some(options);
});
notify_server_capabilities_updated(&server, cx);
if let Some(options) = parse_register_capabilities(reg)? {
server.update_capabilities(|capabilities| {
capabilities.rename_provider = Some(options);
});
notify_server_capabilities_updated(&server, cx);
}
}
"textDocument/inlayHint" => {
let options = parse_register_capabilities(reg)?;
server.update_capabilities(|capabilities| {
capabilities.inlay_hint_provider = Some(options);
});
notify_server_capabilities_updated(&server, cx);
if let Some(options) = parse_register_capabilities(reg)? {
server.update_capabilities(|capabilities| {
capabilities.inlay_hint_provider = Some(options);
});
notify_server_capabilities_updated(&server, cx);
}
}
"textDocument/documentSymbol" => {
let options = parse_register_capabilities(reg)?;
server.update_capabilities(|capabilities| {
capabilities.document_symbol_provider = Some(options);
});
notify_server_capabilities_updated(&server, cx);
if let Some(options) = parse_register_capabilities(reg)? {
server.update_capabilities(|capabilities| {
capabilities.document_symbol_provider = Some(options);
});
notify_server_capabilities_updated(&server, cx);
}
}
"textDocument/codeAction" => {
let options = reg
if let Some(options) = reg
.register_options
.map(serde_json::from_value)
.transpose()?;
let provider_capability = match options {
None => lsp::CodeActionProviderCapability::Simple(true),
Some(options) => lsp::CodeActionProviderCapability::Options(options),
};
server.update_capabilities(|capabilities| {
capabilities.code_action_provider = Some(provider_capability);
});
notify_server_capabilities_updated(&server, cx);
.transpose()?
{
server.update_capabilities(|capabilities| {
capabilities.code_action_provider =
Some(lsp::CodeActionProviderCapability::Options(options));
});
notify_server_capabilities_updated(&server, cx);
}
}
"textDocument/definition" => {
let caps = parse_register_capabilities(reg)?;
server.update_capabilities(|capabilities| {
capabilities.definition_provider = Some(caps);
});
notify_server_capabilities_updated(&server, cx);
if let Some(options) = parse_register_capabilities(reg)? {
server.update_capabilities(|capabilities| {
capabilities.definition_provider = Some(options);
});
notify_server_capabilities_updated(&server, cx);
}
}
"textDocument/completion" => {
let caps = reg
if let Some(caps) = reg
.register_options
.map(serde_json::from_value)
.transpose()?
.unwrap_or_default();
server.update_capabilities(|capabilities| {
capabilities.completion_provider = Some(caps);
});
notify_server_capabilities_updated(&server, cx);
{
server.update_capabilities(|capabilities| {
capabilities.completion_provider = Some(caps);
});
notify_server_capabilities_updated(&server, cx);
}
}
"textDocument/hover" => {
let caps = reg
if let Some(caps) = reg
.register_options
.map(serde_json::from_value)
.transpose()?
.unwrap_or_else(|| lsp::HoverProviderCapability::Simple(true));
server.update_capabilities(|capabilities| {
capabilities.hover_provider = Some(caps);
});
notify_server_capabilities_updated(&server, cx);
{
server.update_capabilities(|capabilities| {
capabilities.hover_provider = Some(caps);
});
notify_server_capabilities_updated(&server, cx);
}
}
"textDocument/signatureHelp" => {
let caps = reg
if let Some(caps) = reg
.register_options
.map(serde_json::from_value)
.transpose()?
.unwrap_or_default();
server.update_capabilities(|capabilities| {
capabilities.signature_help_provider = Some(caps);
});
notify_server_capabilities_updated(&server, cx);
{
server.update_capabilities(|capabilities| {
capabilities.signature_help_provider = Some(caps);
});
notify_server_capabilities_updated(&server, cx);
}
}
"textDocument/synchronization" => {
let caps = reg
if let Some(caps) = reg
.register_options
.map(serde_json::from_value)
.transpose()?
.unwrap_or_else(|| {
lsp::TextDocumentSyncCapability::Options(
lsp::TextDocumentSyncOptions::default(),
)
{
server.update_capabilities(|capabilities| {
capabilities.text_document_sync = Some(caps);
});
server.update_capabilities(|capabilities| {
capabilities.text_document_sync = Some(caps);
});
notify_server_capabilities_updated(&server, cx);
notify_server_capabilities_updated(&server, cx);
}
}
"textDocument/codeLens" => {
let caps = reg
if let Some(caps) = reg
.register_options
.map(serde_json::from_value)
.transpose()?
.unwrap_or_else(|| lsp::CodeLensOptions {
resolve_provider: None,
{
server.update_capabilities(|capabilities| {
capabilities.code_lens_provider = Some(caps);
});
server.update_capabilities(|capabilities| {
capabilities.code_lens_provider = Some(caps);
});
notify_server_capabilities_updated(&server, cx);
notify_server_capabilities_updated(&server, cx);
}
}
"textDocument/diagnostic" => {
let caps = reg
if let Some(caps) = reg
.register_options
.map(serde_json::from_value)
.transpose()?
.unwrap_or_else(|| {
lsp::DiagnosticServerCapabilities::RegistrationOptions(
lsp::DiagnosticRegistrationOptions::default(),
)
{
server.update_capabilities(|capabilities| {
capabilities.diagnostic_provider = Some(caps);
});
server.update_capabilities(|capabilities| {
capabilities.diagnostic_provider = Some(caps);
});
notify_server_capabilities_updated(&server, cx);
notify_server_capabilities_updated(&server, cx);
}
}
"textDocument/colorProvider" => {
let caps = reg
if let Some(caps) = reg
.register_options
.map(serde_json::from_value)
.transpose()?
.unwrap_or_else(|| lsp::ColorProviderCapability::Simple(true));
server.update_capabilities(|capabilities| {
capabilities.color_provider = Some(caps);
});
notify_server_capabilities_updated(&server, cx);
{
server.update_capabilities(|capabilities| {
capabilities.color_provider = Some(caps);
});
notify_server_capabilities_updated(&server, cx);
}
}
_ => 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(
languages: &Arc<LanguageRegistry>,
cx: &mut Context<'_, LspStore>,

View File

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

View File

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

View File

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

View File

@@ -887,10 +887,10 @@ macro_rules! maybe {
(|| $block)()
};
(async $block:block) => {
(|| async $block)()
(async || $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
db.workspace = true
editor.workspace = true
env_logger.workspace = true
futures.workspace = true
gpui.workspace = true
itertools.workspace = true

View File

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

View File

@@ -241,9 +241,9 @@ impl Deref for WrappedAction {
pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
// Vim::action(editor, cx, |vim, action: &StartOfLine, window, cx| {
Vim::action(editor, cx, |vim, action: &VimSet, window, cx| {
Vim::action(editor, cx, |vim, action: &VimSet, _, cx| {
for option in action.options.iter() {
vim.update_editor(window, cx, |_, editor, _, cx| match option {
vim.update_editor(cx, |_, editor, cx| match option {
VimOption::Wrap(true) => {
editor
.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx);
@@ -298,7 +298,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
});
Vim::action(editor, cx, |vim, action: &VimSave, window, cx| {
vim.update_editor(window, cx, |_, editor, window, cx| {
vim.update_editor(cx, |_, editor, cx| {
let Some(project) = editor.project.clone() else {
return;
};
@@ -375,7 +375,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
cx,
);
}
vim.update_editor(window, cx, |vim, editor, window, cx| match action {
vim.update_editor(cx, |vim, editor, cx| match action {
DeleteMarks::Marks(s) => {
if s.starts_with('-') || s.ends_with('-') || s.contains(['\'', '`']) {
err(s.clone(), window, cx);
@@ -432,7 +432,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
});
Vim::action(editor, cx, |vim, action: &VimEdit, window, cx| {
vim.update_editor(window, cx, |vim, editor, window, cx| {
vim.update_editor(cx, |vim, editor, cx| {
let Some(workspace) = vim.workspace(window) else {
return;
};
@@ -462,11 +462,10 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
.map(|c| Keystroke::parse(&c.to_string()).unwrap())
.collect();
vim.switch_mode(Mode::Normal, true, window, cx);
let initial_selections = vim.update_editor(window, cx, |_, editor, _, _| {
editor.selections.disjoint_anchors()
});
let initial_selections =
vim.update_editor(cx, |_, editor, _| editor.selections.disjoint_anchors());
if let Some(range) = &action.range {
let result = vim.update_editor(window, cx, |vim, editor, window, cx| {
let result = vim.update_editor(cx, |vim, editor, cx| {
let range = range.buffer_range(vim, editor, window, cx)?;
editor.change_selections(
SelectionEffects::no_scroll().nav_history(false),
@@ -498,7 +497,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
cx.spawn_in(window, async move |vim, cx| {
task.await;
vim.update_in(cx, |vim, window, cx| {
vim.update_editor(window, cx, |_, editor, window, cx| {
vim.update_editor(cx, |_, editor, cx| {
if had_range {
editor.change_selections(SelectionEffects::default(), window, cx, |s| {
s.select_anchor_ranges([s.newest_anchor().range()]);
@@ -510,7 +509,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
} else {
vim.switch_mode(Mode::Normal, true, window, cx);
}
vim.update_editor(window, cx, |_, editor, _, cx| {
vim.update_editor(cx, |_, editor, cx| {
if let Some(first_sel) = initial_selections {
if let Some(tx_id) = editor
.buffer()
@@ -548,7 +547,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
Vim::action(editor, cx, |vim, action: &GoToLine, window, cx| {
vim.switch_mode(Mode::Normal, false, window, cx);
let result = vim.update_editor(window, cx, |vim, editor, window, cx| {
let result = vim.update_editor(cx, |vim, editor, cx| {
let snapshot = editor.snapshot(window, cx);
let buffer_row = action.range.head().buffer_row(vim, editor, window, cx)?;
let current = editor.selections.newest::<Point>(cx);
@@ -573,7 +572,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
});
Vim::action(editor, cx, |vim, action: &YankCommand, window, cx| {
vim.update_editor(window, cx, |vim, editor, window, cx| {
vim.update_editor(cx, |vim, editor, cx| {
let snapshot = editor.snapshot(window, cx);
if let Ok(range) = action.range.buffer_range(vim, editor, window, cx) {
let end = if range.end < snapshot.buffer_snapshot.max_row() {
@@ -600,7 +599,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
});
Vim::action(editor, cx, |vim, action: &WithRange, window, cx| {
let result = vim.update_editor(window, cx, |vim, editor, window, cx| {
let result = vim.update_editor(cx, |vim, editor, cx| {
action.range.buffer_range(vim, editor, window, cx)
});
@@ -619,7 +618,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
};
let previous_selections = vim
.update_editor(window, cx, |_, editor, window, cx| {
.update_editor(cx, |_, editor, cx| {
let selections = action.restore_selection.then(|| {
editor
.selections
@@ -635,7 +634,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
.flatten();
window.dispatch_action(action.action.boxed_clone(), cx);
cx.defer_in(window, move |vim, window, cx| {
vim.update_editor(window, cx, |_, editor, window, cx| {
vim.update_editor(cx, |_, editor, cx| {
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
if let Some(previous_selections) = previous_selections {
s.select_ranges(previous_selections);
@@ -1176,8 +1175,10 @@ fn generate_commands(_: &App) -> Vec<VimCommand> {
VimCommand::str(("ls", ""), "tab_switcher::ToggleAll"),
VimCommand::new(("new", ""), workspace::NewFileSplitHorizontal),
VimCommand::new(("vne", "w"), workspace::NewFileSplitVertical),
VimCommand::new(("tabe", "dit"), workspace::NewFile),
VimCommand::new(("tabnew", ""), workspace::NewFile),
VimCommand::new(("tabe", "dit"), workspace::NewFile)
.args(|_action, args| Some(VimEdit { filename: args }.boxed_clone())),
VimCommand::new(("tabnew", ""), workspace::NewFile)
.args(|_action, args| Some(VimEdit { filename: args }.boxed_clone())),
VimCommand::new(("tabn", "ext"), workspace::ActivateNextItem).count(),
VimCommand::new(("tabp", "revious"), workspace::ActivatePreviousItem).count(),
VimCommand::new(("tabN", "ext"), workspace::ActivatePreviousItem).count(),
@@ -1536,7 +1537,7 @@ impl OnMatchingLines {
}
pub fn run(&self, vim: &mut Vim, window: &mut Window, cx: &mut Context<Vim>) {
let result = vim.update_editor(window, cx, |vim, editor, window, cx| {
let result = vim.update_editor(cx, |vim, editor, cx| {
self.range.buffer_range(vim, editor, window, cx)
});
@@ -1600,7 +1601,7 @@ impl OnMatchingLines {
});
};
vim.update_editor(window, cx, |_, editor, window, cx| {
vim.update_editor(cx, |_, editor, cx| {
let snapshot = editor.snapshot(window, cx);
let mut row = range.start.0;
@@ -1680,7 +1681,7 @@ pub struct ShellExec {
impl Vim {
pub fn cancel_running_command(&mut self, window: &mut Window, cx: &mut Context<Self>) {
if self.running_command.take().is_some() {
self.update_editor(window, cx, |_, editor, window, cx| {
self.update_editor(cx, |_, editor, cx| {
editor.transact(window, cx, |editor, _window, _cx| {
editor.clear_row_highlights::<ShellExec>();
})
@@ -1691,7 +1692,7 @@ impl Vim {
fn prepare_shell_command(
&mut self,
command: &str,
window: &mut Window,
_: &mut Window,
cx: &mut Context<Self>,
) -> String {
let mut ret = String::new();
@@ -1711,7 +1712,7 @@ impl Vim {
}
match c {
'%' => {
self.update_editor(window, cx, |_, editor, _window, cx| {
self.update_editor(cx, |_, editor, cx| {
if let Some((_, buffer, _)) = editor.active_excerpt(cx) {
if let Some(file) = buffer.read(cx).file() {
if let Some(local) = file.as_local() {
@@ -1747,7 +1748,7 @@ impl Vim {
let Some(workspace) = self.workspace(window) else {
return;
};
let command = self.update_editor(window, cx, |_, editor, window, cx| {
let command = self.update_editor(cx, |_, editor, cx| {
let snapshot = editor.snapshot(window, cx);
let start = editor.selections.newest_display(cx);
let text_layout_details = editor.text_layout_details(window);
@@ -1794,7 +1795,7 @@ impl Vim {
let Some(workspace) = self.workspace(window) else {
return;
};
let command = self.update_editor(window, cx, |_, editor, window, cx| {
let command = self.update_editor(cx, |_, editor, cx| {
let snapshot = editor.snapshot(window, cx);
let start = editor.selections.newest_display(cx);
let range = object
@@ -1896,7 +1897,7 @@ impl ShellExec {
let mut input_snapshot = None;
let mut input_range = None;
let mut needs_newline_prefix = false;
vim.update_editor(window, cx, |vim, editor, window, cx| {
vim.update_editor(cx, |vim, editor, cx| {
let snapshot = editor.buffer().read(cx).snapshot(cx);
let range = if let Some(range) = self.range.clone() {
let Some(range) = range.buffer_range(vim, editor, window, cx).log_err() else {
@@ -1990,7 +1991,7 @@ impl ShellExec {
}
vim.update_in(cx, |vim, window, cx| {
vim.update_editor(window, cx, |_, editor, window, cx| {
vim.update_editor(cx, |_, editor, cx| {
editor.transact(window, cx, |editor, window, cx| {
editor.edit([(range.clone(), text)], cx);
let snapshot = editor.buffer().read(cx).snapshot(cx);
@@ -2477,4 +2478,110 @@ mod test {
"});
// Once ctrl-v to input character literals is added there should be a test for redo
}
#[gpui::test]
async fn test_command_tabnew(cx: &mut TestAppContext) {
let mut cx = VimTestContext::new(cx, true).await;
// Create a new file to ensure that, when the filename is used with
// `:tabnew`, it opens the existing file in a new tab.
let fs = cx.workspace(|workspace, _, cx| workspace.project().read(cx).fs().clone());
fs.as_fake()
.insert_file(path!("/root/dir/file_2.rs"), "file_2".as_bytes().to_vec())
.await;
cx.simulate_keystrokes(": tabnew");
cx.simulate_keystrokes("enter");
cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 2));
// Assert that the new tab is empty and not associated with any file, as
// no file path was provided to the `:tabnew` command.
cx.workspace(|workspace, _window, cx| {
let active_editor = workspace.active_item_as::<Editor>(cx).unwrap();
let buffer = active_editor
.read(cx)
.buffer()
.read(cx)
.as_singleton()
.unwrap();
assert!(&buffer.read(cx).file().is_none());
});
// Leverage the filename as an argument to the `:tabnew` command,
// ensuring that the file, instead of an empty buffer, is opened in a
// new tab.
cx.simulate_keystrokes(": tabnew space dir/file_2.rs");
cx.simulate_keystrokes("enter");
cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 3));
cx.workspace(|workspace, _, cx| {
assert_active_item(workspace, path!("/root/dir/file_2.rs"), "file_2", cx);
});
// If the `filename` argument provided to the `:tabnew` command is for a
// file that doesn't yet exist, it should still associate the buffer
// with that file path, so that when the buffer contents are saved, the
// file is created.
cx.simulate_keystrokes(": tabnew space dir/file_3.rs");
cx.simulate_keystrokes("enter");
cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 4));
cx.workspace(|workspace, _, cx| {
assert_active_item(workspace, path!("/root/dir/file_3.rs"), "", cx);
});
}
#[gpui::test]
async fn test_command_tabedit(cx: &mut TestAppContext) {
let mut cx = VimTestContext::new(cx, true).await;
// Create a new file to ensure that, when the filename is used with
// `:tabedit`, it opens the existing file in a new tab.
let fs = cx.workspace(|workspace, _, cx| workspace.project().read(cx).fs().clone());
fs.as_fake()
.insert_file(path!("/root/dir/file_2.rs"), "file_2".as_bytes().to_vec())
.await;
cx.simulate_keystrokes(": tabedit");
cx.simulate_keystrokes("enter");
cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 2));
// Assert that the new tab is empty and not associated with any file, as
// no file path was provided to the `:tabedit` command.
cx.workspace(|workspace, _window, cx| {
let active_editor = workspace.active_item_as::<Editor>(cx).unwrap();
let buffer = active_editor
.read(cx)
.buffer()
.read(cx)
.as_singleton()
.unwrap();
assert!(&buffer.read(cx).file().is_none());
});
// Leverage the filename as an argument to the `:tabedit` command,
// ensuring that the file, instead of an empty buffer, is opened in a
// new tab.
cx.simulate_keystrokes(": tabedit space dir/file_2.rs");
cx.simulate_keystrokes("enter");
cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 3));
cx.workspace(|workspace, _, cx| {
assert_active_item(workspace, path!("/root/dir/file_2.rs"), "file_2", cx);
});
// If the `filename` argument provided to the `:tabedit` command is for a
// file that doesn't yet exist, it should still associate the buffer
// with that file path, so that when the buffer contents are saved, the
// file is created.
cx.simulate_keystrokes(": tabedit space dir/file_3.rs");
cx.simulate_keystrokes("enter");
cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 4));
cx.workspace(|workspace, _, cx| {
assert_active_item(workspace, path!("/root/dir/file_3.rs"), "", cx);
});
}
}

View File

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

View File

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

View File

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

View File

@@ -3,7 +3,9 @@ use editor::{Bias, Editor};
use gpui::{Action, Context, Window, actions};
use language::SelectionGoal;
use settings::Settings;
use text::Point;
use vim_mode_setting::HelixModeSetting;
use workspace::searchable::Direction;
actions!(
vim,
@@ -11,13 +13,23 @@ actions!(
/// Switches to normal mode with cursor positioned before the current character.
NormalBefore,
/// 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>) {
Vim::action(editor, cx, Vim::normal_before);
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 {
@@ -38,7 +50,7 @@ impl Vim {
if count <= 1 || Vim::globals(cx).dot_replaying {
self.create_mark("^".into(), window, cx);
self.update_editor(window, cx, |_, editor, window, cx| {
self.update_editor(cx, |_, editor, cx| {
editor.dismiss_menus_and_popups(false, window, cx);
if !HelixModeSetting::get_global(cx).0 {
@@ -71,6 +83,29 @@ impl Vim {
self.switch_mode(Mode::Normal, true, window, cx);
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)]
@@ -156,4 +191,13 @@ mod test {
.await;
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 {
Mode::Visual | Mode::VisualLine | Mode::VisualBlock => {
if !prior_selections.is_empty() {
self.update_editor(window, cx, |_, editor, window, cx| {
self.update_editor(cx, |_, editor, cx| {
editor.change_selections(Default::default(), window, cx, |s| {
s.select_ranges(prior_selections.iter().cloned())
})

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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 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| {
let mut insert = conn
.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
UPDATE SET
name = ?5,
path = ?6
path = ?6,
raw_json = ?7
))
.context("Preparing insertion")?;
@@ -1444,6 +1458,7 @@ impl WorkspaceDb {
toolchain.language_name.as_ref(),
toolchain.name.as_ref(),
toolchain.path.as_ref(),
toolchain.as_json.to_string(),
))?;
Ok(())

Binary file not shown.

Before

Width:  |  Height:  |  Size: 146 KiB

After

Width:  |  Height:  |  Size: 189 KiB

View File

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

View File

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

View File

@@ -429,6 +429,7 @@ impl Zeta {
body,
editable_range,
} = gather_task.await?;
let done_gathering_context_at = Instant::now();
log::debug!(
"Events:\n{}\nExcerpt:\n{:?}",
@@ -481,6 +482,7 @@ impl Zeta {
}
};
let received_response_at = Instant::now();
log::debug!("completion response: {}", &response.output_excerpt);
if let Some(usage) = usage {
@@ -492,7 +494,7 @@ impl Zeta {
.ok();
}
Self::process_completion_response(
let edit_prediction = Self::process_completion_response(
response,
buffer,
&snapshot,
@@ -505,7 +507,25 @@ impl Zeta {
buffer_snapshotted_at,
&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}
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`:
```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",
"adapter": "CodeLLDB"
"adapter": "CodeLLDB",
"build": {
"command": "cargo",
"args": ["build"]