Compare commits

..

8 Commits

Author SHA1 Message Date
Richard Feldman
ef60a60e11 After migration, rename the heed db to delete in a way we can get back
Co-authored-by: Agus Zubiaga <hi@aguz.me>
2025-05-28 15:09:06 -04:00
Richard Feldman
5d2ac968d8 Add a bunch of tests
Co-authored-by: Agus Zubiaga <hi@aguz.me>
2025-05-28 15:05:32 -04:00
Richard Feldman
e9d4b8766f Initial LMDB -> SQLite changes
Co-authored-by: Agus Zubiaga <hi@aguz.me>
2025-05-28 14:11:05 -04:00
Richard Feldman
6812872d1a wip 2025-05-28 13:43:15 -04:00
Richard Feldman
2aebeb067c Don't save buffer until *after* autoformatting completes.
Co-authored-by: Agus Zubiaga <hi@aguz.me>
2025-05-28 13:22:55 -04:00
Richard Feldman
7dfd5d1963 Reproduce format_on_save incorrectly marking buffers as stale for LLM
Co-authored-by: Agus Zubiaga <hi@aguz.me>
2025-05-28 13:10:34 -04:00
Richard Feldman
a677b891a1 Do a log::error on an unexpected role, not debug_panic! 2025-05-28 12:16:56 -04:00
Richard Feldman
a2cb480244 Apply format_on_save to edits made with the edit tool 2025-05-28 10:58:02 -04:00
65 changed files with 2365 additions and 2952 deletions

View File

@@ -1,8 +1,8 @@
name: Bug Report (AI Related)
name: Bug Report (Agent Panel)
description: Zed Agent Panel Bugs
type: "Bug"
labels: ["ai"]
title: "AI: <a short description of the AI Related bug>"
labels: ["agent", "ai"]
title: "Agent Panel: <a short description of the Agent Panel bug>"
body:
- type: textarea
attributes:

11
Cargo.lock generated
View File

@@ -117,6 +117,7 @@ dependencies = [
"streaming_diff",
"telemetry",
"telemetry_events",
"tempfile",
"terminal",
"terminal_view",
"text",
@@ -658,9 +659,9 @@ name = "assistant_tools"
version = "0.1.0"
dependencies = [
"agent_settings",
"aho-corasick",
"anyhow",
"assistant_tool",
"async-watch",
"buffer_diff",
"chrono",
"client",
@@ -683,6 +684,7 @@ dependencies = [
"language_model",
"language_models",
"log",
"lsp",
"markdown",
"open",
"paths",
@@ -4031,8 +4033,6 @@ dependencies = [
"smol",
"task",
"telemetry",
"tree-sitter",
"tree-sitter-go",
"util",
"workspace-hack",
"zlog",
@@ -4732,7 +4732,6 @@ dependencies = [
"tree-sitter-rust",
"tree-sitter-typescript",
"ui",
"unicode-script",
"unicode-segmentation",
"unindent",
"url",
@@ -17116,6 +17115,8 @@ dependencies = [
"tempfile",
"tendril",
"unicase",
"unicode-script",
"unicode-segmentation",
"util_macros",
"walkdir",
"workspace-hack",
@@ -19679,7 +19680,7 @@ dependencies = [
[[package]]
name = "zed"
version = "0.189.1"
version = "0.189.0"
dependencies = [
"activity_indicator",
"agent",

View File

@@ -1452,7 +1452,9 @@
"language_servers": ["erlang-ls", "!elp", "..."]
},
"Git Commit": {
"allow_rewrap": "anywhere"
"allow_rewrap": "anywhere",
"preferred_line_length": 72,
"soft_wrap": "bounded"
},
"Go": {
"code_actions_on_format": {

View File

@@ -311,31 +311,6 @@ impl ActivityIndicator {
});
}
if let Some(session) = self
.project
.read(cx)
.dap_store()
.read(cx)
.sessions()
.find(|s| !s.read(cx).is_started())
{
return Some(Content {
icon: Some(
Icon::new(IconName::ArrowCircle)
.size(IconSize::Small)
.with_animation(
"arrow-circle",
Animation::new(Duration::from_secs(2)).repeat(),
|icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
)
.into_any_element(),
),
message: format!("Debug: {}", session.read(cx).adapter()),
tooltip_message: Some(session.read(cx).label().to_string()),
on_click: None,
});
}
let current_job = self
.project
.read(cx)

View File

@@ -107,3 +107,4 @@ language = { workspace = true, "features" = ["test-support"] }
language_model = { workspace = true, "features" = ["test-support"] }
project = { workspace = true, features = ["test-support"] }
rand.workspace = true
tempfile.workspace = true

View File

@@ -637,7 +637,7 @@ impl AgentConfiguration {
.hover(|style| style.bg(cx.theme().colors().element_hover))
.rounded_sm()
.child(
Label::new(tool.name())
Label::new(tool.ui_name())
.buffer_font(cx)
.size(LabelSize::Small),
)

View File

@@ -117,7 +117,7 @@ impl ToolPickerDelegate {
ToolSource::Native => {
if mode == ToolPickerMode::BuiltinTools {
items.extend(tools.into_iter().map(|tool| PickerItem::Tool {
name: tool.name().into(),
name: tool.ui_name().into(),
server_id: None,
}));
}
@@ -129,7 +129,7 @@ impl ToolPickerDelegate {
server_id: server_id.clone(),
});
items.extend(tools.into_iter().map(|tool| PickerItem::Tool {
name: tool.name().into(),
name: tool.ui_name().into(),
server_id: Some(server_id.clone()),
}));
}

View File

@@ -30,6 +30,10 @@ impl ContextServerTool {
impl Tool for ContextServerTool {
fn name(&self) -> String {
format!("{}-{}", self.server_id, self.tool.name)
}
fn ui_name(&self) -> String {
self.tool.name.clone()
}

View File

@@ -3414,8 +3414,8 @@ fn main() {{
});
cx.run_until_parked();
fake_model.stream_last_completion_response("Brief");
fake_model.stream_last_completion_response(" Introduction");
fake_model.stream_last_completion_response("Brief".into());
fake_model.stream_last_completion_response(" Introduction".into());
fake_model.end_last_completion_stream();
cx.run_until_parked();
@@ -3508,7 +3508,7 @@ fn main() {{
});
cx.run_until_parked();
fake_model.stream_last_completion_response("A successful summary");
fake_model.stream_last_completion_response("A successful summary".into());
fake_model.end_last_completion_stream();
cx.run_until_parked();
@@ -3550,7 +3550,7 @@ fn main() {{
fn simulate_successful_response(fake_model: &FakeLanguageModel, cx: &mut TestAppContext) {
cx.run_until_parked();
fake_model.stream_last_completion_response("Assistant response");
fake_model.stream_last_completion_response("Assistant response".into());
fake_model.end_last_completion_stream();
cx.run_until_parked();
}

View File

@@ -1,6 +1,6 @@
use std::borrow::Cow;
use std::cell::{Ref, RefCell};
use std::path::{Path, PathBuf};
use std::path::Path;
use std::rc::Rc;
use std::sync::Arc;
@@ -10,6 +10,10 @@ use assistant_tool::{ToolId, ToolSource, ToolWorkingSet};
use chrono::{DateTime, Utc};
use collections::HashMap;
use context_server::ContextServerId;
use db::sqlez::bindable::Column;
use db::sqlez::statement::Statement;
use db::sqlez_macros::sql;
use db::{define_connection, query};
use futures::channel::{mpsc, oneshot};
use futures::future::{self, BoxFuture, Shared};
use futures::{FutureExt as _, StreamExt as _};
@@ -17,8 +21,7 @@ use gpui::{
App, BackgroundExecutor, Context, Entity, EventEmitter, Global, ReadGlobal, SharedString,
Subscription, Task, prelude::*,
};
use heed::Database;
use heed::types::SerdeBincode;
use heed;
use language_model::{LanguageModelToolResultContent, LanguageModelToolUseId, Role, TokenUsage};
use project::context_server_store::{ContextServerStatus, ContextServerStore};
use project::{Project, ProjectItem, ProjectPath, Worktree};
@@ -36,6 +39,31 @@ use crate::thread::{
DetailedSummaryState, ExceededWindowError, MessageId, ProjectSnapshot, Thread, ThreadId,
};
// Implement Bind trait for ThreadId to use in SQL queries
// impl db::sqlez::bindable::Bind for ThreadId {
// fn bind(&self, statement: &Statement, start_index: i32) -> anyhow::Result<i32> {
// self.to_string().bind(statement, start_index)
// }
// }
// Implement Column trait for SerializedThreadMetadata
impl Column for SerializedThreadMetadata {
fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
let (id_str, next_index): (String, i32) = Column::column(statement, start_index)?;
let (summary, next_index): (String, i32) = Column::column(statement, next_index)?;
let (updated_at_timestamp, next_index): (i64, i32) = Column::column(statement, next_index)?;
Ok((
Self {
id: ThreadId::from(id_str.as_str()),
summary: summary.into(),
updated_at: DateTime::from_timestamp(updated_at_timestamp, 0).unwrap_or_default(),
},
next_index,
))
}
}
const RULES_FILE_NAMES: [&'static str; 6] = [
".rules",
".cursorrules",
@@ -657,6 +685,7 @@ pub struct SerializedThreadMetadata {
}
#[derive(Serialize, Deserialize, Debug)]
#[cfg_attr(test, derive(Clone))]
pub struct SerializedThread {
pub version: String,
pub summary: SharedString,
@@ -681,6 +710,7 @@ pub struct SerializedThread {
}
#[derive(Serialize, Deserialize, Debug)]
#[cfg_attr(test, derive(Clone))]
pub struct SerializedLanguageModel {
pub provider: String,
pub model: String,
@@ -745,7 +775,7 @@ impl SerializedThreadV0_1_0 {
}
}
#[derive(Debug, Serialize, Deserialize)]
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct SerializedMessage {
pub id: MessageId,
pub role: Role,
@@ -763,7 +793,7 @@ pub struct SerializedMessage {
pub is_hidden: bool,
}
#[derive(Debug, Serialize, Deserialize)]
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
#[serde(tag = "type")]
pub enum SerializedMessageSegment {
#[serde(rename = "text")]
@@ -781,14 +811,14 @@ pub enum SerializedMessageSegment {
},
}
#[derive(Debug, Serialize, Deserialize)]
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct SerializedToolUse {
pub id: LanguageModelToolUseId,
pub name: SharedString,
pub input: serde_json::Value,
}
#[derive(Debug, Serialize, Deserialize)]
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct SerializedToolResult {
pub tool_use_id: LanguageModelToolUseId,
pub is_error: bool,
@@ -850,7 +880,7 @@ impl LegacySerializedMessage {
}
}
#[derive(Debug, Serialize, Deserialize)]
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct SerializedCrease {
pub start: usize,
pub end: usize,
@@ -866,26 +896,6 @@ impl Global for GlobalThreadsDatabase {}
pub(crate) struct ThreadsDatabase {
executor: BackgroundExecutor,
env: heed::Env,
threads: Database<SerdeBincode<ThreadId>, SerializedThread>,
}
impl heed::BytesEncode<'_> for SerializedThread {
type EItem = SerializedThread;
fn bytes_encode(item: &Self::EItem) -> Result<Cow<[u8]>, heed::BoxedError> {
serde_json::to_vec(item).map(Cow::Owned).map_err(Into::into)
}
}
impl<'a> heed::BytesDecode<'a> for SerializedThread {
type DItem = SerializedThread;
fn bytes_decode(bytes: &'a [u8]) -> Result<Self::DItem, heed::BoxedError> {
// We implement this type manually because we want to call `SerializedThread::from_json`,
// instead of the Deserialize trait implementation for `SerializedThread`.
SerializedThread::from_json(bytes).map_err(Into::into)
}
}
impl ThreadsDatabase {
@@ -900,8 +910,7 @@ impl ThreadsDatabase {
let database_future = executor
.spawn({
let executor = executor.clone();
let database_path = paths::data_dir().join("threads/threads-db.1.mdb");
async move { ThreadsDatabase::new(database_path, executor) }
async move { ThreadsDatabase::new(executor).await }
})
.then(|result| future::ready(result.map(Arc::new).map_err(Arc::new)))
.boxed()
@@ -910,80 +919,511 @@ impl ThreadsDatabase {
cx.set_global(GlobalThreadsDatabase(database_future));
}
pub fn new(path: PathBuf, executor: BackgroundExecutor) -> Result<Self> {
std::fs::create_dir_all(&path)?;
const ONE_GB_IN_BYTES: usize = 1024 * 1024 * 1024;
let env = unsafe {
heed::EnvOpenOptions::new()
.map_size(ONE_GB_IN_BYTES)
.max_dbs(1)
.open(path)?
};
let mut txn = env.write_txn()?;
let threads = env.create_database(&mut txn, Some("threads"))?;
txn.commit()?;
Ok(Self {
executor,
env,
threads,
})
pub async fn new(executor: BackgroundExecutor) -> Result<Self> {
Ok(Self { executor })
}
pub fn list_threads(&self) -> Task<Result<Vec<SerializedThreadMetadata>>> {
let env = self.env.clone();
let threads = self.threads;
self.executor.spawn(async move {
let txn = env.read_txn()?;
let mut iter = threads.iter(&txn)?;
let mut threads = Vec::new();
while let Some((key, value)) = iter.next().transpose()? {
threads.push(SerializedThreadMetadata {
id: key,
summary: value.summary,
updated_at: value.updated_at,
});
}
Ok(threads)
})
self.executor
.spawn(async move { AGENT_THREADS.all_threads().await })
}
pub fn try_find_thread(&self, id: ThreadId) -> Task<Result<Option<SerializedThread>>> {
let env = self.env.clone();
let threads = self.threads;
self.executor.spawn(async move {
let txn = env.read_txn()?;
let thread = threads.get(&txn, &id)?;
Ok(thread)
})
self.executor
.spawn(async move { AGENT_THREADS.get_thread(id).await })
}
pub fn save_thread(&self, id: ThreadId, thread: SerializedThread) -> Task<Result<()>> {
let env = self.env.clone();
let threads = self.threads;
self.executor.spawn(async move {
let mut txn = env.write_txn()?;
threads.put(&mut txn, &id, &thread)?;
txn.commit()?;
Ok(())
})
self.executor
.spawn(async move { AGENT_THREADS.save_thread(id, thread).await })
}
pub fn delete_thread(&self, id: ThreadId) -> Task<Result<()>> {
let env = self.env.clone();
let threads = self.threads;
self.executor
.spawn(async move { AGENT_THREADS.delete_thread_by_id(id).await })
}
self.executor.spawn(async move {
let mut txn = env.write_txn()?;
threads.delete(&mut txn, &id)?;
txn.commit()?;
Ok(())
})
/// Migrate a legacy `heed` LMDB database to SQLite
pub async fn migrate_from_heed(heed_path: &Path) -> Result<()> {
Self::migrate_from_heed_to_db(heed_path, &AGENT_THREADS).await
}
/// Migrate a legacy `heed` LMDB database to a specific SQLite database
pub async fn migrate_from_heed_to_db(heed_path: &Path, db: &ThreadStoreDB) -> Result<()> {
if !heed_path.exists() {
return Ok(()); // No migration needed
}
// Open the old heed database
let env = unsafe {
heed::EnvOpenOptions::new()
.map_size(1024 * 1024 * 1024) // 1GB
.max_dbs(1)
.open(&heed_path)?
};
let txn = env.read_txn()?;
let old_threads: heed::Database<heed::types::SerdeBincode<ThreadId>, SerializedThread> =
env.open_database(&txn, Some("threads"))?
.ok_or_else(|| anyhow!("threads database not found"))?;
// Migrate all threads
for result in old_threads.iter(&txn)? {
if let Some((id, thread)) = result.log_err() {
db.save_thread(id, thread).await.log_err();
}
}
drop(txn);
drop(env);
// Rename the old heed database with .bak suffix
let mut backup_path = heed_path.to_path_buf();
let file_name = heed_path
.file_name()
.ok_or_else(|| anyhow!("invalid heed path"))?;
let new_name = format!("{}.bak", file_name.to_string_lossy());
backup_path.set_file_name(new_name);
std::fs::rename(&heed_path, &backup_path)?;
Ok(())
}
}
// Heed serialization helpers for migration
impl heed::BytesEncode<'_> for SerializedThread {
type EItem = SerializedThread;
fn bytes_encode(item: &Self::EItem) -> Result<Cow<[u8]>, heed::BoxedError> {
serde_json::to_vec(item).map(Cow::Owned).map_err(Into::into)
}
}
impl<'a> heed::BytesDecode<'a> for SerializedThread {
type DItem = SerializedThread;
fn bytes_decode(bytes: &'a [u8]) -> Result<Self::DItem, heed::BoxedError> {
SerializedThread::from_json(bytes).map_err(Into::into)
}
}
define_connection!(pub static ref AGENT_THREADS: ThreadStoreDB<()> =
&[sql!(
CREATE TABLE IF NOT EXISTS agent_threads(
id TEXT PRIMARY KEY,
summary TEXT NOT NULL,
updated_at INTEGER NOT NULL,
data TEXT NOT NULL
) STRICT;
)];
);
impl ThreadStoreDB {
query! {
pub async fn all_threads() -> Result<Vec<SerializedThreadMetadata>> {
SELECT id, summary, updated_at
FROM agent_threads
ORDER BY updated_at DESC
}
}
query! {
async fn get_thread_data(id: String) -> Result<Option<String>> {
SELECT data FROM agent_threads WHERE id = (?)
}
}
query! {
async fn save_thread_data(id: String, summary: String, updated_at: i64, data: String) -> Result<()> {
INSERT OR REPLACE INTO agent_threads (id, summary, updated_at, data)
VALUES ((?), (?), (?), (?))
}
}
query! {
async fn delete_thread_data(id: String) -> Result<()> {
DELETE FROM agent_threads WHERE id = (?)
}
}
pub async fn get_thread(&self, id: ThreadId) -> Result<Option<SerializedThread>> {
let id_str = id.to_string();
let result = self.get_thread_data(id_str).await?;
match result {
Some(json_str) => {
let thread = SerializedThread::from_json(json_str.as_bytes())?;
Ok(Some(thread))
}
None => Ok(None),
}
}
pub async fn save_thread(&self, id: ThreadId, thread: SerializedThread) -> Result<()> {
let thread_json = serde_json::to_string(&thread)?;
let updated_at = thread.updated_at.timestamp();
let id_str = id.to_string();
let summary = thread.summary.clone();
self.save_thread_data(id_str, summary.to_string(), updated_at, thread_json)
.await
}
pub async fn delete_thread_by_id(&self, id: ThreadId) -> Result<()> {
let id_str = id.to_string();
self.delete_thread_data(id_str).await
}
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::Utc;
use gpui::TestAppContext;
use std::sync::Arc;
use tempfile::TempDir;
#[gpui::test]
async fn test_save_load_delete_threads(_cx: &mut TestAppContext) {
let db = ThreadStoreDB::open_test_db("test_save_load_delete_threads").await;
// Test that no threads exist initially
let threads = db.all_threads().await.unwrap();
assert_eq!(threads.len(), 0);
// Create test thread data
let thread_id = ThreadId::from("test-thread-1");
let thread = SerializedThread {
version: SerializedThread::VERSION.to_string(),
summary: SharedString::from("Test thread summary"),
updated_at: Utc::now(),
messages: vec![],
initial_project_snapshot: None,
cumulative_token_usage: TokenUsage::default(),
request_token_usage: vec![],
detailed_summary_state: DetailedSummaryState::NotGenerated,
exceeded_window_error: None,
model: None,
completion_mode: Some(CompletionMode::Normal),
tool_use_limit_reached: false,
};
let thread_summary = thread.summary.clone();
let thread_version = thread.version.clone();
// Save thread
db.save_thread(thread_id.clone(), thread.clone())
.await
.unwrap();
// Load all threads
let threads = db.all_threads().await.unwrap();
assert_eq!(threads.len(), 1);
assert_eq!(threads[0].id, thread_id);
assert_eq!(threads[0].summary, thread_summary);
// Load specific thread
let loaded_thread = db.get_thread(thread_id.clone()).await.unwrap();
assert!(loaded_thread.is_some());
let loaded_thread = loaded_thread.unwrap();
assert_eq!(loaded_thread.summary, thread_summary);
assert_eq!(loaded_thread.version, thread_version);
// Update thread
let updated_thread = SerializedThread {
summary: SharedString::from("Updated summary"),
updated_at: Utc::now(),
..thread
};
db.save_thread(thread_id.clone(), updated_thread.clone())
.await
.unwrap();
// Verify update
let loaded_thread = db.get_thread(thread_id.clone()).await.unwrap().unwrap();
assert_eq!(loaded_thread.summary, SharedString::from("Updated summary"));
// Delete thread
db.delete_thread_by_id(thread_id.clone()).await.unwrap();
// Verify deletion
let loaded_thread = db.get_thread(thread_id.clone()).await.unwrap();
assert!(loaded_thread.is_none());
let threads = db.all_threads().await.unwrap();
assert_eq!(threads.len(), 0);
}
#[gpui::test]
async fn test_multiple_threads(_cx: &mut TestAppContext) {
let db = ThreadStoreDB::open_test_db("test_multiple_threads").await;
// Create multiple threads
let thread_ids = [
ThreadId::from("thread-1"),
ThreadId::from("thread-2"),
ThreadId::from("thread-3"),
];
for (i, thread_id) in thread_ids.iter().enumerate() {
let thread = SerializedThread {
version: SerializedThread::VERSION.to_string(),
summary: SharedString::from(format!("Thread {}", i + 1)),
updated_at: Utc::now() - chrono::Duration::hours(i as i64),
messages: vec![],
initial_project_snapshot: None,
cumulative_token_usage: TokenUsage::default(),
request_token_usage: Vec::new(),
detailed_summary_state: DetailedSummaryState::NotGenerated,
exceeded_window_error: None,
model: None,
completion_mode: Some(CompletionMode::Normal),
tool_use_limit_reached: false,
};
db.save_thread(thread_id.clone(), thread).await.unwrap();
}
// Load all threads - should be ordered by updated_at DESC
let threads = db.all_threads().await.unwrap();
assert_eq!(threads.len(), 3);
assert_eq!(threads[0].summary.as_ref(), "Thread 1");
assert_eq!(threads[1].summary.as_ref(), "Thread 2");
assert_eq!(threads[2].summary.as_ref(), "Thread 3");
// Delete middle thread
db.delete_thread_by_id(thread_ids[1].clone()).await.unwrap();
let threads = db.all_threads().await.unwrap();
assert_eq!(threads.len(), 2);
assert_eq!(threads[0].summary.as_ref(), "Thread 1");
assert_eq!(threads[1].summary.as_ref(), "Thread 3");
}
#[gpui::test]
async fn test_heed_to_sqlite_migration(_cx: &mut TestAppContext) {
use heed::types::SerdeBincode;
// Create a temporary directory for the heed database
let temp_dir = TempDir::new().unwrap();
let heed_path = temp_dir.path().join("test-heed-db");
// Create and populate heed database
{
std::fs::create_dir_all(&heed_path).unwrap();
let env = unsafe {
heed::EnvOpenOptions::new()
.map_size(1024 * 1024 * 1024)
.max_dbs(1)
.open(&heed_path)
.unwrap()
};
let mut txn = env.write_txn().unwrap();
let threads: heed::Database<SerdeBincode<ThreadId>, SerializedThread> =
env.create_database(&mut txn, Some("threads")).unwrap();
// Insert test data
let thread_ids = [
ThreadId::from("legacy-thread-1"),
ThreadId::from("legacy-thread-2"),
ThreadId::from("legacy-thread-3"),
];
for (i, thread_id) in thread_ids.iter().enumerate() {
let thread = SerializedThread {
version: SerializedThread::VERSION.to_string(),
summary: SharedString::from(format!("Legacy Thread {}", i + 1)),
updated_at: DateTime::from_timestamp(1700000000 - (i as i64) * 86400, 0)
.unwrap(),
messages: vec![SerializedMessage {
id: MessageId(i),
role: Role::User,
segments: vec![SerializedMessageSegment::Text {
text: format!("Test message {}", i),
}],
tool_uses: vec![],
tool_results: vec![],
context: String::new(),
creases: vec![],
is_hidden: false,
}],
initial_project_snapshot: None,
cumulative_token_usage: TokenUsage {
input_tokens: ((i + 1) * 100) as u32,
output_tokens: ((i + 1) * 50) as u32,
cache_creation_input_tokens: 0,
cache_read_input_tokens: 0,
},
request_token_usage: vec![],
detailed_summary_state: DetailedSummaryState::NotGenerated,
exceeded_window_error: None,
model: None,
completion_mode: Some(CompletionMode::Normal),
tool_use_limit_reached: false,
};
threads.put(&mut txn, thread_id, &thread).unwrap();
}
txn.commit().unwrap();
}
// Clear any existing SQLite data
let db = ThreadStoreDB::open_test_db("test_heed_to_sqlite_migration").await;
// Verify SQLite is empty
let threads_before = db.all_threads().await.unwrap();
assert_eq!(threads_before.len(), 0);
// Run migration
ThreadsDatabase::migrate_from_heed_to_db(&heed_path, &db)
.await
.unwrap();
// Verify all threads were migrated
let threads_after = db.all_threads().await.unwrap();
assert_eq!(threads_after.len(), 3);
// Verify thread metadata
let thread_summaries: Vec<_> = threads_after.iter().map(|t| t.summary.as_ref()).collect();
assert!(thread_summaries.contains(&"Legacy Thread 1"));
assert!(thread_summaries.contains(&"Legacy Thread 2"));
assert!(thread_summaries.contains(&"Legacy Thread 3"));
// Verify full thread data
for i in 1..=3 {
let thread_id = ThreadId::from(&format!("legacy-thread-{}", i) as &str);
let thread = db.get_thread(thread_id).await.unwrap().unwrap();
assert_eq!(thread.summary.as_ref(), format!("Legacy Thread {}", i));
assert_eq!(thread.messages.len(), 1);
assert_eq!(
thread.messages[0].segments[0],
SerializedMessageSegment::Text {
text: format!("Test message {}", i - 1)
}
);
assert_eq!(thread.cumulative_token_usage.input_tokens, (i * 100) as u32);
assert_eq!(thread.cumulative_token_usage.output_tokens, (i * 50) as u32);
}
// Verify heed database was renamed with .bak suffix
assert!(!heed_path.exists());
let mut backup_path = heed_path.to_path_buf();
backup_path.set_file_name(format!("{}.bak", heed_path.file_name().unwrap().to_string_lossy()));
assert!(backup_path.exists());
}
#[gpui::test]
async fn test_thread_serialization_deserialization(_cx: &mut TestAppContext) {
let db = ThreadStoreDB::open_test_db("test_thread_serialization_deserialization").await;
let thread_id = ThreadId::from("serialization-test");
let original_thread = SerializedThread {
version: SerializedThread::VERSION.to_string(),
summary: SharedString::from("Serialization test thread"),
updated_at: Utc::now(),
messages: vec![
SerializedMessage {
id: MessageId(1),
role: Role::User,
segments: vec![
SerializedMessageSegment::Text {
text: "Hello".to_string(),
},
SerializedMessageSegment::Thinking {
text: "Thinking about the response".to_string(),
signature: Some("sig123".to_string()),
},
],
tool_uses: vec![SerializedToolUse {
id: LanguageModelToolUseId::from("tool-1"),
name: SharedString::from("test_tool"),
input: serde_json::json!({"key": "value"}),
}],
tool_results: vec![SerializedToolResult {
tool_use_id: LanguageModelToolUseId::from("tool-1"),
is_error: false,
content: LanguageModelToolResultContent::Text("Result".into()),
output: None,
}],
context: String::new(),
creases: vec![SerializedCrease {
start: 0,
end: 5,
icon_path: SharedString::from("icon.png"),
label: SharedString::from("test-crease"),
}],
is_hidden: false,
},
SerializedMessage {
id: MessageId(2),
role: Role::Assistant,
segments: vec![SerializedMessageSegment::RedactedThinking {
data: vec![1, 2, 3, 4, 5],
}],
tool_uses: vec![],
tool_results: vec![],
context: String::new(),
creases: vec![],
is_hidden: true,
},
],
initial_project_snapshot: Some(Arc::new(ProjectSnapshot {
worktree_snapshots: vec![],
unsaved_buffer_paths: vec![],
timestamp: Utc::now(),
})),
cumulative_token_usage: TokenUsage {
input_tokens: 1000,
output_tokens: 500,
cache_creation_input_tokens: 0,
cache_read_input_tokens: 0,
},
request_token_usage: vec![TokenUsage {
input_tokens: 100,
output_tokens: 50,
cache_creation_input_tokens: 0,
cache_read_input_tokens: 0,
}],
detailed_summary_state: DetailedSummaryState::Generated {
text: SharedString::from("Detailed summary"),
message_id: MessageId(1),
},
exceeded_window_error: None,
model: Some(SerializedLanguageModel {
provider: "test-provider".to_string(),
model: "test-model".to_string(),
}),
completion_mode: Some(CompletionMode::Normal),
tool_use_limit_reached: true,
};
// Save thread
db.save_thread(thread_id.clone(), original_thread.clone())
.await
.unwrap();
// Load thread
let loaded_thread = db.get_thread(thread_id).await.unwrap().unwrap();
// Verify all fields
assert_eq!(loaded_thread.version, original_thread.version);
assert_eq!(loaded_thread.summary, original_thread.summary);
assert_eq!(loaded_thread.messages.len(), original_thread.messages.len());
assert_eq!(loaded_thread.messages[0].segments.len(), 2);
assert_eq!(loaded_thread.messages[0].tool_uses.len(), 1);
assert_eq!(loaded_thread.messages[0].tool_results.len(), 1);
assert_eq!(loaded_thread.messages[0].creases.len(), 1);
assert_eq!(loaded_thread.messages[1].is_hidden, true);
assert!(loaded_thread.initial_project_snapshot.is_some());
assert_eq!(
loaded_thread.cumulative_token_usage.input_tokens,
original_thread.cumulative_token_usage.input_tokens
);
assert_eq!(
loaded_thread.exceeded_window_error.is_none(),
original_thread.exceeded_window_error.is_none()
);
assert!(loaded_thread.model.is_some());
assert_eq!(loaded_thread.tool_use_limit_reached, true);
}
}

View File

@@ -3,7 +3,6 @@ mod context_editor;
mod context_history;
mod context_store;
pub mod language_model_selector;
mod max_mode_tooltip;
mod slash_command;
mod slash_command_picker;

View File

@@ -29,7 +29,6 @@ use paths::contexts_dir;
use project::Project;
use prompt_store::PromptBuilder;
use serde::{Deserialize, Serialize};
use settings::Settings;
use smallvec::SmallVec;
use std::{
cmp::{Ordering, max},
@@ -683,7 +682,6 @@ pub struct AssistantContext {
language_registry: Arc<LanguageRegistry>,
project: Option<Entity<Project>>,
prompt_builder: Arc<PromptBuilder>,
completion_mode: agent_settings::CompletionMode,
}
trait ContextAnnotation {
@@ -720,14 +718,6 @@ impl AssistantContext {
)
}
pub fn completion_mode(&self) -> agent_settings::CompletionMode {
self.completion_mode
}
pub fn set_completion_mode(&mut self, completion_mode: agent_settings::CompletionMode) {
self.completion_mode = completion_mode;
}
pub fn new(
id: ContextId,
replica_id: ReplicaId,
@@ -774,7 +764,6 @@ impl AssistantContext {
pending_cache_warming_task: Task::ready(None),
_subscriptions: vec![cx.subscribe(&buffer, Self::handle_buffer_event)],
pending_save: Task::ready(Ok(())),
completion_mode: AgentSettings::get_global(cx).preferred_completion_mode,
path: None,
buffer,
telemetry,
@@ -2332,15 +2321,7 @@ impl AssistantContext {
completion_request.messages.push(request_message);
}
}
let supports_max_mode = if let Some(model) = model {
model.supports_max_mode()
} else {
false
};
if supports_max_mode {
completion_request.mode = Some(self.completion_mode.into());
}
completion_request
}

View File

@@ -1210,8 +1210,8 @@ async fn test_summarization(cx: &mut TestAppContext) {
});
cx.run_until_parked();
fake_model.stream_last_completion_response("Brief");
fake_model.stream_last_completion_response(" Introduction");
fake_model.stream_last_completion_response("Brief".into());
fake_model.stream_last_completion_response(" Introduction".into());
fake_model.end_last_completion_stream();
cx.run_until_parked();
@@ -1274,7 +1274,7 @@ async fn test_thread_summary_error_retry(cx: &mut TestAppContext) {
});
cx.run_until_parked();
fake_model.stream_last_completion_response("A successful summary");
fake_model.stream_last_completion_response("A successful summary".into());
fake_model.end_last_completion_stream();
cx.run_until_parked();
@@ -1356,7 +1356,7 @@ fn setup_context_editor_with_fake_model(
fn simulate_successful_response(fake_model: &FakeLanguageModel, cx: &mut TestAppContext) {
cx.run_until_parked();
fake_model.stream_last_completion_response("Assistant response");
fake_model.stream_last_completion_response("Assistant response".into());
fake_model.end_last_completion_stream();
cx.run_until_parked();
}

View File

@@ -1,10 +1,7 @@
use crate::{
language_model_selector::{
LanguageModelSelector, LanguageModelSelectorPopoverMenu, ToggleModelSelector,
},
max_mode_tooltip::MaxModeTooltip,
use crate::language_model_selector::{
LanguageModelSelector, LanguageModelSelectorPopoverMenu, ToggleModelSelector,
};
use agent_settings::{AgentSettings, CompletionMode};
use agent_settings::AgentSettings;
use anyhow::Result;
use assistant_slash_command::{SlashCommand, SlashCommandOutputSection, SlashCommandWorkingSet};
use assistant_slash_commands::{
@@ -2011,17 +2008,17 @@ impl ContextEditor {
None => (ButtonStyle::Filled, None),
};
Button::new("send_button", "Send")
.label_size(LabelSize::Small)
ButtonLike::new("send_button")
.disabled(self.sending_disabled(cx))
.style(style)
.when_some(tooltip, |button, tooltip| {
button.tooltip(move |_, _| tooltip.clone())
})
.layer(ElevationIndex::ModalSurface)
.key_binding(
.child(Label::new("Send"))
.children(
KeyBinding::for_action_in(&Assist, &focus_handle, window, cx)
.map(|kb| kb.size(rems_from_px(12.))),
.map(|binding| binding.into_any_element()),
)
.on_click(move |_event, window, cx| {
focus_handle.dispatch_action(&Assist, window, cx);
@@ -2061,45 +2058,6 @@ impl ContextEditor {
)
}
fn render_max_mode_toggle(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
let context = self.context().read(cx);
let active_model = LanguageModelRegistry::read_global(cx)
.default_model()
.map(|default| default.model)?;
if !active_model.supports_max_mode() {
return None;
}
let active_completion_mode = context.completion_mode();
let max_mode_enabled = active_completion_mode == CompletionMode::Max;
let icon = if max_mode_enabled {
IconName::ZedBurnModeOn
} else {
IconName::ZedBurnMode
};
Some(
IconButton::new("burn-mode", icon)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.toggle_state(max_mode_enabled)
.selected_icon_color(Color::Error)
.on_click(cx.listener(move |this, _event, _window, cx| {
this.context().update(cx, |context, _cx| {
context.set_completion_mode(match active_completion_mode {
CompletionMode::Max => CompletionMode::Normal,
CompletionMode::Normal => CompletionMode::Max,
});
});
}))
.tooltip(move |_window, cx| {
cx.new(|_| MaxModeTooltip::new().selected(max_mode_enabled))
.into()
})
.into_any_element(),
)
}
fn render_language_model_selector(&self, cx: &mut Context<Self>) -> impl IntoElement {
let active_model = LanguageModelRegistry::read_global(cx)
.default_model()
@@ -2545,7 +2503,6 @@ impl Render for ContextEditor {
let provider = LanguageModelRegistry::read_global(cx)
.default_model()
.map(|default| default.provider);
let accept_terms = if self.show_accept_terms {
provider.as_ref().and_then(|provider| {
provider.render_accept_terms(LanguageModelProviderTosView::PromptEditorPopup, cx)
@@ -2555,8 +2512,6 @@ impl Render for ContextEditor {
};
let language_model_selector = self.language_model_selector_menu_handle.clone();
let max_mode_toggle = self.render_max_mode_toggle(cx);
v_flex()
.key_context("ContextEditor")
.capture_action(cx.listener(ContextEditor::cancel))
@@ -2596,28 +2551,31 @@ impl Render for ContextEditor {
})
.children(self.render_last_error(cx))
.child(
h_flex()
.relative()
.py_2()
.pl_1p5()
.pr_2()
.w_full()
.justify_between()
.border_t_1()
.border_color(cx.theme().colors().border_variant)
.bg(cx.theme().colors().editor_background)
.child(
h_flex()
.gap_0p5()
.child(self.render_inject_context_menu(cx))
.when_some(max_mode_toggle, |this, element| this.child(element)),
)
.child(
h_flex()
.gap_1()
.child(self.render_language_model_selector(cx))
.child(self.render_send_button(window, cx)),
),
h_flex().w_full().relative().child(
h_flex()
.p_2()
.w_full()
.border_t_1()
.border_color(cx.theme().colors().border_variant)
.bg(cx.theme().colors().editor_background)
.child(
h_flex()
.gap_1()
.child(self.render_inject_context_menu(cx))
.child(ui::Divider::vertical())
.child(
div()
.pl_0p5()
.child(self.render_language_model_selector(cx)),
),
)
.child(
h_flex()
.w_full()
.justify_end()
.child(self.render_send_button(window, cx)),
),
),
)
}
}

View File

@@ -1,60 +0,0 @@
use gpui::{Context, IntoElement, Render, Window};
use ui::{prelude::*, tooltip_container};
pub struct MaxModeTooltip {
selected: bool,
}
impl MaxModeTooltip {
pub fn new() -> Self {
Self { selected: false }
}
pub fn selected(mut self, selected: bool) -> Self {
self.selected = selected;
self
}
}
impl Render for MaxModeTooltip {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let icon = if self.selected {
IconName::ZedBurnModeOn
} else {
IconName::ZedBurnMode
};
let title = h_flex()
.gap_1()
.child(Icon::new(icon).size(IconSize::Small))
.child(Label::new("Burn Mode"));
tooltip_container(window, cx, |this, _, _| {
this.gap_0p5()
.map(|header| if self.selected {
header.child(
h_flex()
.justify_between()
.child(title)
.child(
h_flex()
.gap_0p5()
.child(Icon::new(IconName::Check).size(IconSize::XSmall).color(Color::Accent))
.child(Label::new("Turned On").size(LabelSize::XSmall).color(Color::Accent))
)
)
} else {
header.child(title)
})
.child(
div()
.max_w_72()
.child(
Label::new("Enables models to use large context windows, unlimited tool calls, and other capabilities for expanded reasoning, offering an unfettered agentic experience.")
.size(LabelSize::Small)
.color(Color::Muted)
)
)
})
}
}

View File

@@ -203,6 +203,11 @@ pub trait Tool: 'static + Send + Sync {
/// Returns the name of the tool.
fn name(&self) -> String;
/// Returns the name to be displayed in the UI for this tool.
fn ui_name(&self) -> String {
self.name()
}
/// Returns the description of the tool.
fn description(&self) -> String;

View File

@@ -16,9 +16,9 @@ eval = []
[dependencies]
agent_settings.workspace = true
aho-corasick.workspace = true
anyhow.workspace = true
assistant_tool.workspace = true
async-watch.workspace = true
buffer_diff.workspace = true
chrono.workspace = true
collections.workspace = true
@@ -36,6 +36,7 @@ itertools.workspace = true
language.workspace = true
language_model.workspace = true
log.workspace = true
lsp.workspace = true
markdown.workspace = true
open.workspace = true
paths.workspace = true
@@ -64,6 +65,7 @@ workspace.workspace = true
zed_llm_client.workspace = true
[dev-dependencies]
lsp = { workspace = true, features = ["test-support"] }
client = { workspace = true, features = ["test-support"] }
clock = { workspace = true, features = ["test-support"] }
collections = { workspace = true, features = ["test-support"] }

View File

@@ -0,0 +1,9 @@
Invoke multiple other tool calls either sequentially or concurrently.
This tool is useful when you need to perform several operations at once, improving efficiency by reducing the number of back-and-forth interactions needed to complete complex tasks.
If the tool calls are set to be run sequentially, then each tool call within the batch is executed in the order provided. If it's set to run concurrently, then they may run in a different order. Regardless, all tool calls will have the same permissions and context as if they were called individually.
This tool should never be used to run a total of one tool. Instead, just run that one tool directly. You can run batches within batches if desired, which is a way you can mix concurrent and sequential tool call execution.
When it's possible to run tools in a batch, you should run as many as possible in the batch, up to a maximum of 32. For example, don't run multiple consecutive batches of 10 when you could instead run one batch of 30.

View File

@@ -0,0 +1,19 @@
A tool for applying code actions to specific sections of your code. It uses language servers to provide refactoring capabilities similar to what you'd find in an IDE.
This tool can:
- List all available code actions for a selected text range
- Execute a specific code action on that range
- Rename symbols across your codebase. This tool is the preferred way to rename things, and you should always prefer to rename code symbols using this tool rather than using textual find/replace when both are available.
Use this tool when you want to:
- Discover what code actions are available for a piece of code
- Apply automatic fixes and code transformations
- Rename variables, functions, or other symbols consistently throughout your project
- Clean up imports, implement interfaces, or perform other language-specific operations
- If unsure what actions are available, call the tool without specifying an action to get a list
- For common operations, you can directly specify actions like "quickfix.all" or "source.organizeImports"
- For renaming, use the special "textDocument/rename" action and provide the new name in the arguments field
- Be specific with your text range and context to ensure the tool identifies the correct code location
The tool will automatically save any changes it makes to your files.

View File

@@ -0,0 +1,39 @@
Returns either an outline of the public code symbols in the entire project (grouped by file) or else an outline of both the public and private code symbols within a particular file.
When a path is provided, this tool returns a hierarchical outline of code symbols for that specific file.
When no path is provided, it returns a list of all public code symbols in the project, organized by file.
You can also provide an optional regular expression which filters the output by only showing code symbols which match that regex.
Results are paginated with 2000 entries per page. Use the optional 'offset' parameter to request subsequent pages.
Markdown headings indicate the structure of the output; just like
with markdown headings, the more # symbols there are at the beginning of a line,
the deeper it is in the hierarchy.
Each code symbol entry ends with a line number or range, which tells you what portion of the
underlying source code file corresponds to that part of the outline. You can use
that line information with other tools, to strategically read portions of the source code.
For example, you can use this tool to find a relevant symbol in the project, then get the outline of the file which contains that symbol, then use the line number information from that file's outline to read different sections of that file, without having to read the entire file all at once (which can be slow, or use a lot of tokens).
<example>
# class Foo [L123-136]
## method do_something(arg1, arg2) [L124-126]
## method process_data(data) [L128-135]
# class Bar [L145-161]
## method initialize() [L146-149]
## method update_state(new_state) [L160]
## private method _validate_state(state) [L161-162]
</example>
This example shows how tree-sitter outlines the structure of source code:
1. `class Foo` is defined on lines 123-136
- It contains a method `do_something` spanning lines 124-126
- It also has a method `process_data` spanning lines 128-135
2. `class Bar` is defined on lines 145-161
- It has an `initialize` method spanning lines 146-149
- It has an `update_state` method on line 160
- It has a private method `_validate_state` spanning lines 161-162

View File

@@ -0,0 +1,9 @@
Reads the contents of a path on the filesystem.
If the path is a directory, this lists all files and directories within that path.
If the path is a file, this returns the file's contents.
When reading a file, if the file is too big and no line range is specified, an outline of the file's code symbols is listed instead, which can be used to request specific line ranges in a subsequent call.
Similarly, if a directory has too many entries to show at once, a subset of entries will be shown,
and subsequent requests can use starting and ending line numbers to get other subsets.

File diff suppressed because it is too large Load Diff

View File

@@ -11,7 +11,7 @@ const END_TAGS: [&str; 3] = [OLD_TEXT_END_TAG, NEW_TEXT_END_TAG, EDITS_END_TAG];
#[derive(Debug)]
pub enum EditParserEvent {
OldTextChunk { chunk: String, done: bool },
OldText(String),
NewTextChunk { chunk: String, done: bool },
}
@@ -33,7 +33,7 @@ pub struct EditParser {
#[derive(Debug, PartialEq)]
enum EditParserState {
Pending,
WithinOldText { start: bool },
WithinOldText,
AfterOldText,
WithinNewText { start: bool },
}
@@ -56,23 +56,20 @@ impl EditParser {
EditParserState::Pending => {
if let Some(start) = self.buffer.find("<old_text>") {
self.buffer.drain(..start + "<old_text>".len());
self.state = EditParserState::WithinOldText { start: true };
self.state = EditParserState::WithinOldText;
} else {
break;
}
}
EditParserState::WithinOldText { start } => {
if !self.buffer.is_empty() {
if *start && self.buffer.starts_with('\n') {
self.buffer.remove(0);
}
*start = false;
}
EditParserState::WithinOldText => {
if let Some(tag_range) = self.find_end_tag() {
let mut chunk = self.buffer[..tag_range.start].to_string();
if chunk.ends_with('\n') {
chunk.pop();
let mut start = 0;
if self.buffer.starts_with('\n') {
start = 1;
}
let mut old_text = self.buffer[start..tag_range.start].to_string();
if old_text.ends_with('\n') {
old_text.pop();
}
self.metrics.tags += 1;
@@ -82,14 +79,8 @@ impl EditParser {
self.buffer.drain(..tag_range.end);
self.state = EditParserState::AfterOldText;
edit_events.push(EditParserEvent::OldTextChunk { chunk, done: true });
edit_events.push(EditParserEvent::OldText(old_text));
} else {
if !self.ends_with_tag_prefix() {
edit_events.push(EditParserEvent::OldTextChunk {
chunk: mem::take(&mut self.buffer),
done: false,
});
}
break;
}
}
@@ -124,7 +115,11 @@ impl EditParser {
self.state = EditParserState::Pending;
edit_events.push(EditParserEvent::NewTextChunk { chunk, done: true });
} else {
if !self.ends_with_tag_prefix() {
let mut end_prefixes = END_TAGS
.iter()
.flat_map(|tag| (1..tag.len()).map(move |i| &tag[..i]))
.chain(["\n"]);
if end_prefixes.all(|prefix| !self.buffer.ends_with(&prefix)) {
edit_events.push(EditParserEvent::NewTextChunk {
chunk: mem::take(&mut self.buffer),
done: false,
@@ -146,14 +141,6 @@ impl EditParser {
Some(start_ix..start_ix + tag.len())
}
fn ends_with_tag_prefix(&self) -> bool {
let mut end_prefixes = END_TAGS
.iter()
.flat_map(|tag| (1..tag.len()).map(move |i| &tag[..i]))
.chain(["\n"]);
end_prefixes.any(|prefix| self.buffer.ends_with(&prefix))
}
pub fn finish(self) -> EditParserMetrics {
self.metrics
}
@@ -425,28 +412,20 @@ mod tests {
chunk_indices.sort();
chunk_indices.push(input.len());
let mut old_text = Some(String::new());
let mut new_text = None;
let mut pending_edit = Edit::default();
let mut edits = Vec::new();
let mut last_ix = 0;
for chunk_ix in chunk_indices {
for event in parser.push(&input[last_ix..chunk_ix]) {
match event {
EditParserEvent::OldTextChunk { chunk, done } => {
old_text.as_mut().unwrap().push_str(&chunk);
if done {
pending_edit.old_text = old_text.take().unwrap();
new_text = Some(String::new());
}
EditParserEvent::OldText(old_text) => {
pending_edit.old_text = old_text;
}
EditParserEvent::NewTextChunk { chunk, done } => {
new_text.as_mut().unwrap().push_str(&chunk);
pending_edit.new_text.push_str(&chunk);
if done {
pending_edit.new_text = new_text.take().unwrap();
edits.push(pending_edit);
pending_edit = Edit::default();
old_text = Some(String::new());
}
}
}
@@ -454,6 +433,8 @@ mod tests {
last_ix = chunk_ix;
}
assert_eq!(pending_edit, Edit::default(), "unfinished edit");
edits
}
}

View File

@@ -1,694 +0,0 @@
use language::{Point, TextBufferSnapshot};
use std::{cmp, ops::Range};
const REPLACEMENT_COST: u32 = 1;
const INSERTION_COST: u32 = 3;
const DELETION_COST: u32 = 10;
/// A streaming fuzzy matcher that can process text chunks incrementally
/// and return the best match found so far at each step.
pub struct StreamingFuzzyMatcher {
snapshot: TextBufferSnapshot,
query_lines: Vec<String>,
incomplete_line: String,
best_match: Option<Range<usize>>,
matrix: SearchMatrix,
}
impl StreamingFuzzyMatcher {
pub fn new(snapshot: TextBufferSnapshot) -> Self {
let buffer_line_count = snapshot.max_point().row as usize + 1;
Self {
snapshot,
query_lines: Vec::new(),
incomplete_line: String::new(),
best_match: None,
matrix: SearchMatrix::new(buffer_line_count + 1),
}
}
/// Returns the query lines.
pub fn query_lines(&self) -> &[String] {
&self.query_lines
}
/// Push a new chunk of text and get the best match found so far.
///
/// This method accumulates text chunks and processes complete lines.
/// Partial lines are buffered internally until a newline is received.
///
/// # Returns
///
/// Returns `Some(range)` if a match has been found with the accumulated
/// query so far, or `None` if no suitable match exists yet.
pub fn push(&mut self, chunk: &str) -> Option<Range<usize>> {
// Add the chunk to our incomplete line buffer
self.incomplete_line.push_str(chunk);
if let Some((last_pos, _)) = self.incomplete_line.match_indices('\n').next_back() {
let complete_part = &self.incomplete_line[..=last_pos];
// Split into lines and add to query_lines
for line in complete_part.lines() {
self.query_lines.push(line.to_string());
}
self.incomplete_line.replace_range(..last_pos + 1, "");
self.best_match = self.resolve_location_fuzzy();
}
self.best_match.clone()
}
/// Finish processing and return the final best match.
///
/// This processes any remaining incomplete line before returning the final
/// match result.
pub fn finish(&mut self) -> Option<Range<usize>> {
// Process any remaining incomplete line
if !self.incomplete_line.is_empty() {
self.query_lines.push(self.incomplete_line.clone());
self.best_match = self.resolve_location_fuzzy();
}
self.best_match.clone()
}
fn resolve_location_fuzzy(&mut self) -> Option<Range<usize>> {
let new_query_line_count = self.query_lines.len();
let old_query_line_count = self.matrix.rows.saturating_sub(1);
if new_query_line_count == old_query_line_count {
return None;
}
self.matrix.resize_rows(new_query_line_count + 1);
// Process only the new query lines
for row in old_query_line_count..new_query_line_count {
let query_line = self.query_lines[row].trim();
let leading_deletion_cost = (row + 1) as u32 * DELETION_COST;
self.matrix.set(
row + 1,
0,
SearchState::new(leading_deletion_cost, SearchDirection::Up),
);
let mut buffer_lines = self.snapshot.as_rope().chunks().lines();
let mut col = 0;
while let Some(buffer_line) = buffer_lines.next() {
let buffer_line = buffer_line.trim();
let up = SearchState::new(
self.matrix
.get(row, col + 1)
.cost
.saturating_add(DELETION_COST),
SearchDirection::Up,
);
let left = SearchState::new(
self.matrix
.get(row + 1, col)
.cost
.saturating_add(INSERTION_COST),
SearchDirection::Left,
);
let diagonal = SearchState::new(
if query_line == buffer_line {
self.matrix.get(row, col).cost
} else if fuzzy_eq(query_line, buffer_line) {
self.matrix.get(row, col).cost + REPLACEMENT_COST
} else {
self.matrix
.get(row, col)
.cost
.saturating_add(DELETION_COST + INSERTION_COST)
},
SearchDirection::Diagonal,
);
self.matrix
.set(row + 1, col + 1, up.min(left).min(diagonal));
col += 1;
}
}
// Traceback to find the best match
let buffer_line_count = self.snapshot.max_point().row as usize + 1;
let mut buffer_row_end = buffer_line_count as u32;
let mut best_cost = u32::MAX;
for col in 1..=buffer_line_count {
let cost = self.matrix.get(new_query_line_count, col).cost;
if cost < best_cost {
best_cost = cost;
buffer_row_end = col as u32;
}
}
let mut matched_lines = 0;
let mut query_row = new_query_line_count;
let mut buffer_row_start = buffer_row_end;
while query_row > 0 && buffer_row_start > 0 {
let current = self.matrix.get(query_row, buffer_row_start as usize);
match current.direction {
SearchDirection::Diagonal => {
query_row -= 1;
buffer_row_start -= 1;
matched_lines += 1;
}
SearchDirection::Up => {
query_row -= 1;
}
SearchDirection::Left => {
buffer_row_start -= 1;
}
}
}
let matched_buffer_row_count = buffer_row_end - buffer_row_start;
let matched_ratio = matched_lines as f32
/ (matched_buffer_row_count as f32).max(new_query_line_count as f32);
if matched_ratio >= 0.8 {
let buffer_start_ix = self
.snapshot
.point_to_offset(Point::new(buffer_row_start, 0));
let buffer_end_ix = self.snapshot.point_to_offset(Point::new(
buffer_row_end - 1,
self.snapshot.line_len(buffer_row_end - 1),
));
Some(buffer_start_ix..buffer_end_ix)
} else {
None
}
}
}
fn fuzzy_eq(left: &str, right: &str) -> bool {
const THRESHOLD: f64 = 0.8;
let min_levenshtein = left.len().abs_diff(right.len());
let min_normalized_levenshtein =
1. - (min_levenshtein as f64 / cmp::max(left.len(), right.len()) as f64);
if min_normalized_levenshtein < THRESHOLD {
return false;
}
strsim::normalized_levenshtein(left, right) >= THRESHOLD
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
enum SearchDirection {
Up,
Left,
Diagonal,
}
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
struct SearchState {
cost: u32,
direction: SearchDirection,
}
impl SearchState {
fn new(cost: u32, direction: SearchDirection) -> Self {
Self { cost, direction }
}
}
struct SearchMatrix {
cols: usize,
rows: usize,
data: Vec<SearchState>,
}
impl SearchMatrix {
fn new(cols: usize) -> Self {
SearchMatrix {
cols,
rows: 0,
data: Vec::new(),
}
}
fn resize_rows(&mut self, needed_rows: usize) {
debug_assert!(needed_rows > self.rows);
self.rows = needed_rows;
self.data.resize(
self.rows * self.cols,
SearchState::new(0, SearchDirection::Diagonal),
);
}
fn get(&self, row: usize, col: usize) -> SearchState {
debug_assert!(row < self.rows && col < self.cols);
self.data[row * self.cols + col]
}
fn set(&mut self, row: usize, col: usize, state: SearchState) {
debug_assert!(row < self.rows && col < self.cols);
self.data[row * self.cols + col] = state;
}
}
#[cfg(test)]
mod tests {
use super::*;
use indoc::indoc;
use language::{BufferId, TextBuffer};
use rand::prelude::*;
use util::test::{generate_marked_text, marked_text_ranges};
#[test]
fn test_empty_query() {
let buffer = TextBuffer::new(
0,
BufferId::new(1).unwrap(),
"Hello world\nThis is a test\nFoo bar baz",
);
let snapshot = buffer.snapshot();
let mut finder = StreamingFuzzyMatcher::new(snapshot.clone());
assert_eq!(push(&mut finder, ""), None);
assert_eq!(finish(finder), None);
}
#[test]
fn test_streaming_exact_match() {
let buffer = TextBuffer::new(
0,
BufferId::new(1).unwrap(),
"Hello world\nThis is a test\nFoo bar baz",
);
let snapshot = buffer.snapshot();
let mut finder = StreamingFuzzyMatcher::new(snapshot.clone());
// Push partial query
assert_eq!(push(&mut finder, "This"), None);
// Complete the line
assert_eq!(
push(&mut finder, " is a test\n"),
Some("This is a test".to_string())
);
// Finish should return the same result
assert_eq!(finish(finder), Some("This is a test".to_string()));
}
#[test]
fn test_streaming_fuzzy_match() {
let buffer = TextBuffer::new(
0,
BufferId::new(1).unwrap(),
indoc! {"
function foo(a, b) {
return a + b;
}
function bar(x, y) {
return x * y;
}
"},
);
let snapshot = buffer.snapshot();
let mut finder = StreamingFuzzyMatcher::new(snapshot.clone());
// Push a fuzzy query that should match the first function
assert_eq!(
push(&mut finder, "function foo(a, c) {\n").as_deref(),
Some("function foo(a, b) {")
);
assert_eq!(
push(&mut finder, " return a + c;\n}\n").as_deref(),
Some(concat!(
"function foo(a, b) {\n",
" return a + b;\n",
"}"
))
);
}
#[test]
fn test_incremental_improvement() {
let buffer = TextBuffer::new(
0,
BufferId::new(1).unwrap(),
"Line 1\nLine 2\nLine 3\nLine 4\nLine 5",
);
let snapshot = buffer.snapshot();
let mut finder = StreamingFuzzyMatcher::new(snapshot.clone());
// No match initially
assert_eq!(push(&mut finder, "Lin"), None);
// Get a match when we complete a line
assert_eq!(push(&mut finder, "e 3\n"), Some("Line 3".to_string()));
// The match might change if we add more specific content
assert_eq!(
push(&mut finder, "Line 4\n"),
Some("Line 3\nLine 4".to_string())
);
assert_eq!(finish(finder), Some("Line 3\nLine 4".to_string()));
}
#[test]
fn test_incomplete_lines_buffering() {
let buffer = TextBuffer::new(
0,
BufferId::new(1).unwrap(),
indoc! {"
The quick brown fox
jumps over the lazy dog
Pack my box with five dozen liquor jugs
"},
);
let snapshot = buffer.snapshot();
let mut finder = StreamingFuzzyMatcher::new(snapshot.clone());
// Push text in small chunks across line boundaries
assert_eq!(push(&mut finder, "jumps "), None); // No newline yet
assert_eq!(push(&mut finder, "over the"), None); // Still no newline
assert_eq!(push(&mut finder, " lazy"), None); // Still incomplete
// Complete the line
assert_eq!(
push(&mut finder, " dog\n"),
Some("jumps over the lazy dog".to_string())
);
}
#[test]
fn test_multiline_fuzzy_match() {
let buffer = TextBuffer::new(
0,
BufferId::new(1).unwrap(),
indoc! {r#"
impl Display for User {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "User: {} ({})", self.name, self.email)
}
}
impl Debug for User {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.debug_struct("User")
.field("name", &self.name)
.field("email", &self.email)
.finish()
}
}
"#},
);
let snapshot = buffer.snapshot();
let mut finder = StreamingFuzzyMatcher::new(snapshot.clone());
assert_eq!(
push(&mut finder, "impl Debug for User {\n"),
Some("impl Debug for User {".to_string())
);
assert_eq!(
push(
&mut finder,
" fn fmt(&self, f: &mut Formatter) -> Result {\n"
)
.as_deref(),
Some(concat!(
"impl Debug for User {\n",
" fn fmt(&self, f: &mut Formatter) -> fmt::Result {"
))
);
assert_eq!(
push(&mut finder, " f.debug_struct(\"User\")\n").as_deref(),
Some(concat!(
"impl Debug for User {\n",
" fn fmt(&self, f: &mut Formatter) -> fmt::Result {\n",
" f.debug_struct(\"User\")"
))
);
assert_eq!(
push(
&mut finder,
" .field(\"name\", &self.username)\n"
)
.as_deref(),
Some(concat!(
"impl Debug for User {\n",
" fn fmt(&self, f: &mut Formatter) -> fmt::Result {\n",
" f.debug_struct(\"User\")\n",
" .field(\"name\", &self.name)"
))
);
assert_eq!(
finish(finder).as_deref(),
Some(concat!(
"impl Debug for User {\n",
" fn fmt(&self, f: &mut Formatter) -> fmt::Result {\n",
" f.debug_struct(\"User\")\n",
" .field(\"name\", &self.name)"
))
);
}
#[gpui::test(iterations = 100)]
fn test_resolve_location_single_line(mut rng: StdRng) {
assert_location_resolution(
concat!(
" Lorem\n",
"« ipsum»\n",
" dolor sit amet\n",
" consecteur",
),
"ipsum",
&mut rng,
);
}
#[gpui::test(iterations = 100)]
fn test_resolve_location_multiline(mut rng: StdRng) {
assert_location_resolution(
concat!(
" Lorem\n",
"« ipsum\n",
" dolor sit amet»\n",
" consecteur",
),
"ipsum\ndolor sit amet",
&mut rng,
);
}
#[gpui::test(iterations = 100)]
fn test_resolve_location_function_with_typo(mut rng: StdRng) {
assert_location_resolution(
indoc! {"
«fn foo1(a: usize) -> usize {
40
fn foo2(b: usize) -> usize {
42
}
"},
"fn foo1(a: usize) -> u32 {\n40\n}",
&mut rng,
);
}
#[gpui::test(iterations = 100)]
fn test_resolve_location_class_methods(mut rng: StdRng) {
assert_location_resolution(
indoc! {"
class Something {
one() { return 1; }
« two() { return 2222; }
three() { return 333; }
four() { return 4444; }
five() { return 5555; }
six() { return 6666; }»
seven() { return 7; }
eight() { return 8; }
}
"},
indoc! {"
two() { return 2222; }
four() { return 4444; }
five() { return 5555; }
six() { return 6666; }
"},
&mut rng,
);
}
#[gpui::test(iterations = 100)]
fn test_resolve_location_imports_no_match(mut rng: StdRng) {
assert_location_resolution(
indoc! {"
use std::ops::Range;
use std::sync::Mutex;
use std::{
collections::HashMap,
env,
ffi::{OsStr, OsString},
fs,
io::{BufRead, BufReader},
mem,
path::{Path, PathBuf},
process::Command,
sync::LazyLock,
time::SystemTime,
};
"},
indoc! {"
use std::collections::{HashMap, HashSet};
use std::ffi::{OsStr, OsString};
use std::fmt::Write as _;
use std::fs;
use std::io::{BufReader, Read, Write};
use std::mem;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::Arc;
"},
&mut rng,
);
}
#[gpui::test(iterations = 100)]
fn test_resolve_location_nested_closure(mut rng: StdRng) {
assert_location_resolution(
indoc! {"
impl Foo {
fn new() -> Self {
Self {
subscriptions: vec![
cx.observe_window_activation(window, |editor, window, cx| {
let active = window.is_window_active();
editor.blink_manager.update(cx, |blink_manager, cx| {
if active {
blink_manager.enable(cx);
} else {
blink_manager.disable(cx);
}
});
}),
];
}
}
}
"},
concat!(
" editor.blink_manager.update(cx, |blink_manager, cx| {\n",
" blink_manager.enable(cx);\n",
" });",
),
&mut rng,
);
}
#[gpui::test(iterations = 100)]
fn test_resolve_location_tool_invocation(mut rng: StdRng) {
assert_location_resolution(
indoc! {r#"
let tool = cx
.update(|cx| working_set.tool(&tool_name, cx))
.map_err(|err| {
anyhow!("Failed to look up tool '{}': {}", tool_name, err)
})?;
let Some(tool) = tool else {
return Err(anyhow!("Tool '{}' not found", tool_name));
};
let project = project.clone();
let action_log = action_log.clone();
let messages = messages.clone();
let tool_result = cx
.update(|cx| tool.run(invocation.input, &messages, project, action_log, cx))
.map_err(|err| anyhow!("Failed to start tool '{}': {}", tool_name, err))?;
tasks.push(tool_result.output);
"#},
concat!(
"let tool_result = cx\n",
" .update(|cx| tool.run(invocation.input, &messages, project, action_log, cx))\n",
" .output;",
),
&mut rng,
);
}
#[track_caller]
fn assert_location_resolution(text_with_expected_range: &str, query: &str, rng: &mut StdRng) {
let (text, expected_ranges) = marked_text_ranges(text_with_expected_range, false);
let buffer = TextBuffer::new(0, BufferId::new(1).unwrap(), text.clone());
let snapshot = buffer.snapshot();
let mut matcher = StreamingFuzzyMatcher::new(snapshot.clone());
// Split query into random chunks
let chunks = to_random_chunks(rng, query);
// Push chunks incrementally
for chunk in &chunks {
matcher.push(chunk);
}
let result = matcher.finish();
// If no expected ranges, we expect no match
if expected_ranges.is_empty() {
assert_eq!(
result, None,
"Expected no match for query: {:?}, but found: {:?}",
query, result
);
} else {
let mut actual_ranges = Vec::new();
if let Some(range) = result {
actual_ranges.push(range);
}
let text_with_actual_range = generate_marked_text(&text, &actual_ranges, false);
pretty_assertions::assert_eq!(
text_with_actual_range,
text_with_expected_range,
"Query: {:?}, Chunks: {:?}",
query,
chunks
);
}
}
fn to_random_chunks(rng: &mut StdRng, input: &str) -> Vec<String> {
let chunk_count = rng.gen_range(1..=cmp::min(input.len(), 50));
let mut chunk_indices = (0..input.len()).choose_multiple(rng, chunk_count);
chunk_indices.sort();
chunk_indices.push(input.len());
let mut chunks = Vec::new();
let mut last_ix = 0;
for chunk_ix in chunk_indices {
chunks.push(input[last_ix..chunk_ix].to_string());
last_ix = chunk_ix;
}
chunks
}
fn push(finder: &mut StreamingFuzzyMatcher, chunk: &str) -> Option<String> {
finder
.push(chunk)
.map(|range| finder.snapshot.text_for_range(range).collect::<String>())
}
fn finish(mut finder: StreamingFuzzyMatcher) -> Option<String> {
let snapshot = finder.snapshot.clone();
finder
.finish()
.map(|range| snapshot.text_for_range(range).collect::<String>())
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,15 @@
Renames a symbol across your codebase using the language server's semantic knowledge.
This tool performs a rename refactoring operation on a specified symbol. It uses the project's language server to analyze the code and perform the rename correctly across all files where the symbol is referenced.
Unlike a simple find and replace, this tool understands the semantic meaning of the code, so it only renames the specific symbol you specify and not unrelated text that happens to have the same name.
Examples of symbols you can rename:
- Variables
- Functions
- Classes/structs
- Fields/properties
- Methods
- Interfaces/traits
The language server handles updating all references to the renamed symbol throughout the codebase.

View File

@@ -0,0 +1,11 @@
Gives detailed information about code symbols in your project such as variables, functions, classes, interface, traits, and other programming constructs, using the editor's integrated Language Server Protocol (LSP) servers.
This tool is the preferred way to do things like:
* Find out where a code symbol is first declared (or first defined - that is, assigned)
* Find all the places where a code symbol is referenced
* Find the type definition for a code symbol
* Find a code symbol's implementation
This tool gives more reliable answers than things like regex searches, because it can account for relevant semantics like aliases. It should be used over textual search tools (e.g. regex) when searching for information about code symbols that this tool supports directly.
This tool should not be used when you need to search for something that is not a code symbol.

View File

@@ -56,7 +56,5 @@ async-pipe.workspace = true
gpui = { workspace = true, features = ["test-support"] }
settings = { workspace = true, features = ["test-support"] }
task = { workspace = true, features = ["test-support"] }
tree-sitter.workspace = true
tree-sitter-go.workspace = true
util = { workspace = true, features = ["test-support"] }
zlog.workspace = true

View File

@@ -275,386 +275,3 @@ impl InlineValueProvider for PythonInlineValueProvider {
variables
}
}
pub struct GoInlineValueProvider;
impl InlineValueProvider for GoInlineValueProvider {
fn provide(
&self,
mut node: language::Node,
source: &str,
max_row: usize,
) -> Vec<InlineValueLocation> {
let mut variables = Vec::new();
let mut variable_names = HashSet::new();
let mut scope = VariableScope::Local;
loop {
let mut variable_names_in_scope = HashMap::new();
for child in node.named_children(&mut node.walk()) {
if child.start_position().row >= max_row {
break;
}
if scope == VariableScope::Local {
match child.kind() {
"var_declaration" => {
for var_spec in child.named_children(&mut child.walk()) {
if var_spec.kind() == "var_spec" {
if let Some(name_node) = var_spec.child_by_field_name("name") {
let variable_name =
source[name_node.byte_range()].to_string();
if variable_names.contains(&variable_name) {
continue;
}
if let Some(index) =
variable_names_in_scope.get(&variable_name)
{
variables.remove(*index);
}
variable_names_in_scope
.insert(variable_name.clone(), variables.len());
variables.push(InlineValueLocation {
variable_name,
scope: VariableScope::Local,
lookup: VariableLookupKind::Variable,
row: name_node.end_position().row,
column: name_node.end_position().column,
});
}
}
}
}
"short_var_declaration" => {
if let Some(left_side) = child.child_by_field_name("left") {
for identifier in left_side.named_children(&mut left_side.walk()) {
if identifier.kind() == "identifier" {
let variable_name =
source[identifier.byte_range()].to_string();
if variable_names.contains(&variable_name) {
continue;
}
if let Some(index) =
variable_names_in_scope.get(&variable_name)
{
variables.remove(*index);
}
variable_names_in_scope
.insert(variable_name.clone(), variables.len());
variables.push(InlineValueLocation {
variable_name,
scope: VariableScope::Local,
lookup: VariableLookupKind::Variable,
row: identifier.end_position().row,
column: identifier.end_position().column,
});
}
}
}
}
"assignment_statement" => {
if let Some(left_side) = child.child_by_field_name("left") {
for identifier in left_side.named_children(&mut left_side.walk()) {
if identifier.kind() == "identifier" {
let variable_name =
source[identifier.byte_range()].to_string();
if variable_names.contains(&variable_name) {
continue;
}
if let Some(index) =
variable_names_in_scope.get(&variable_name)
{
variables.remove(*index);
}
variable_names_in_scope
.insert(variable_name.clone(), variables.len());
variables.push(InlineValueLocation {
variable_name,
scope: VariableScope::Local,
lookup: VariableLookupKind::Variable,
row: identifier.end_position().row,
column: identifier.end_position().column,
});
}
}
}
}
"function_declaration" | "method_declaration" => {
if let Some(params) = child.child_by_field_name("parameters") {
for param in params.named_children(&mut params.walk()) {
if param.kind() == "parameter_declaration" {
if let Some(name_node) = param.child_by_field_name("name") {
let variable_name =
source[name_node.byte_range()].to_string();
if variable_names.contains(&variable_name) {
continue;
}
if let Some(index) =
variable_names_in_scope.get(&variable_name)
{
variables.remove(*index);
}
variable_names_in_scope
.insert(variable_name.clone(), variables.len());
variables.push(InlineValueLocation {
variable_name,
scope: VariableScope::Local,
lookup: VariableLookupKind::Variable,
row: name_node.end_position().row,
column: name_node.end_position().column,
});
}
}
}
}
}
"for_statement" => {
if let Some(clause) = child.named_child(0) {
if clause.kind() == "for_clause" {
if let Some(init) = clause.named_child(0) {
if init.kind() == "short_var_declaration" {
if let Some(left_side) =
init.child_by_field_name("left")
{
if left_side.kind() == "expression_list" {
for identifier in left_side
.named_children(&mut left_side.walk())
{
if identifier.kind() == "identifier" {
let variable_name = source
[identifier.byte_range()]
.to_string();
if variable_names
.contains(&variable_name)
{
continue;
}
if let Some(index) =
variable_names_in_scope
.get(&variable_name)
{
variables.remove(*index);
}
variable_names_in_scope.insert(
variable_name.clone(),
variables.len(),
);
variables.push(InlineValueLocation {
variable_name,
scope: VariableScope::Local,
lookup:
VariableLookupKind::Variable,
row: identifier.end_position().row,
column: identifier
.end_position()
.column,
});
}
}
}
}
}
}
} else if clause.kind() == "range_clause" {
if let Some(left) = clause.child_by_field_name("left") {
if left.kind() == "expression_list" {
for identifier in left.named_children(&mut left.walk())
{
if identifier.kind() == "identifier" {
let variable_name =
source[identifier.byte_range()].to_string();
if variable_name == "_" {
continue;
}
if variable_names.contains(&variable_name) {
continue;
}
if let Some(index) =
variable_names_in_scope.get(&variable_name)
{
variables.remove(*index);
}
variable_names_in_scope.insert(
variable_name.clone(),
variables.len(),
);
variables.push(InlineValueLocation {
variable_name,
scope: VariableScope::Local,
lookup: VariableLookupKind::Variable,
row: identifier.end_position().row,
column: identifier.end_position().column,
});
}
}
}
}
}
}
}
_ => {}
}
} else if child.kind() == "var_declaration" {
for var_spec in child.named_children(&mut child.walk()) {
if var_spec.kind() == "var_spec" {
if let Some(name_node) = var_spec.child_by_field_name("name") {
let variable_name = source[name_node.byte_range()].to_string();
variables.push(InlineValueLocation {
variable_name,
scope: VariableScope::Global,
lookup: VariableLookupKind::Expression,
row: name_node.end_position().row,
column: name_node.end_position().column,
});
}
}
}
}
}
variable_names.extend(variable_names_in_scope.keys().cloned());
if matches!(node.kind(), "function_declaration" | "method_declaration") {
scope = VariableScope::Global;
}
if let Some(parent) = node.parent() {
node = parent;
} else {
break;
}
}
variables
}
}
#[cfg(test)]
mod tests {
use super::*;
use tree_sitter::Parser;
#[test]
fn test_go_inline_value_provider() {
let provider = GoInlineValueProvider;
let source = r#"
package main
func main() {
items := []int{1, 2, 3, 4, 5}
for i, v := range items {
println(i, v)
}
for j := 0; j < 10; j++ {
println(j)
}
}
"#;
let mut parser = Parser::new();
if parser
.set_language(&tree_sitter_go::LANGUAGE.into())
.is_err()
{
return;
}
let Some(tree) = parser.parse(source, None) else {
return;
};
let root_node = tree.root_node();
let mut main_body = None;
for child in root_node.named_children(&mut root_node.walk()) {
if child.kind() == "function_declaration" {
if let Some(name) = child.child_by_field_name("name") {
if &source[name.byte_range()] == "main" {
if let Some(body) = child.child_by_field_name("body") {
main_body = Some(body);
break;
}
}
}
}
}
let Some(main_body) = main_body else {
return;
};
let variables = provider.provide(main_body, source, 100);
assert!(variables.len() >= 2);
let variable_names: Vec<&str> =
variables.iter().map(|v| v.variable_name.as_str()).collect();
assert!(variable_names.contains(&"items"));
assert!(variable_names.contains(&"j"));
}
#[test]
fn test_go_inline_value_provider_counter_pattern() {
let provider = GoInlineValueProvider;
let source = r#"
package main
func main() {
N := 10
for i := range N {
println(i)
}
}
"#;
let mut parser = Parser::new();
if parser
.set_language(&tree_sitter_go::LANGUAGE.into())
.is_err()
{
return;
}
let Some(tree) = parser.parse(source, None) else {
return;
};
let root_node = tree.root_node();
let mut main_body = None;
for child in root_node.named_children(&mut root_node.walk()) {
if child.kind() == "function_declaration" {
if let Some(name) = child.child_by_field_name("name") {
if &source[name.byte_range()] == "main" {
if let Some(body) = child.child_by_field_name("body") {
main_body = Some(body);
break;
}
}
}
}
}
let Some(main_body) = main_body else {
return;
};
let variables = provider.provide(main_body, source, 100);
let variable_names: Vec<&str> =
variables.iter().map(|v| v.variable_name.as_str()).collect();
assert!(variable_names.contains(&"N"));
assert!(variable_names.contains(&"i"));
}
}

View File

@@ -18,7 +18,7 @@ use dap::{
GithubRepo,
},
configure_tcp_connection,
inline_value::{GoInlineValueProvider, PythonInlineValueProvider, RustInlineValueProvider},
inline_value::{PythonInlineValueProvider, RustInlineValueProvider},
};
use gdb::GdbDebugAdapter;
use go::GoDebugAdapter;
@@ -48,6 +48,5 @@ pub fn init(cx: &mut App) {
registry.add_inline_value_provider("Rust".to_string(), Arc::from(RustInlineValueProvider));
registry
.add_inline_value_provider("Python".to_string(), Arc::from(PythonInlineValueProvider));
registry.add_inline_value_provider("Go".to_string(), Arc::from(GoInlineValueProvider));
})
}

View File

@@ -312,22 +312,14 @@ impl DebugAdapter for GoDebugAdapter {
"processId": attach_config.process_id,
})
}
dap::DebugRequest::Launch(launch_config) => {
let mode = if launch_config.program != "." {
"exec"
} else {
"debug"
};
json!({
"request": "launch",
"mode": mode,
"program": launch_config.program,
"cwd": launch_config.cwd,
"args": launch_config.args,
"env": launch_config.env_json()
})
}
dap::DebugRequest::Launch(launch_config) => json!({
"request": "launch",
"mode": "debug",
"program": launch_config.program,
"cwd": launch_config.cwd,
"args": launch_config.args,
"env": launch_config.env_json()
}),
};
let map = args.as_object_mut().unwrap();

View File

@@ -149,8 +149,22 @@ impl DebugAdapter for PhpDebugAdapter {
"default": false
},
"pathMappings": {
"type": "object",
"description": "A mapping of server paths to local paths.",
"type": "array",
"description": "A list of server paths mapping to the local source paths on your machine for remote host debugging",
"items": {
"type": "object",
"properties": {
"serverPath": {
"type": "string",
"description": "Path on the server"
},
"localPath": {
"type": "string",
"description": "Corresponding path on the local machine"
}
},
"required": ["serverPath", "localPath"]
}
},
"log": {
"type": "boolean",

View File

@@ -295,6 +295,7 @@ impl DebugPanel {
})
})?
.await?;
dap_store
.update(cx, |dap_store, cx| {
dap_store.boot_session(session.clone(), definition, cx)
@@ -432,10 +433,7 @@ impl DebugPanel {
};
let dap_store_handle = self.project.read(cx).dap_store().clone();
let mut label = parent_session.read(cx).label().clone();
if !label.ends_with("(child)") {
label = format!("{label} (child)").into();
}
let label = parent_session.read(cx).label().clone();
let adapter = parent_session.read(cx).adapter().clone();
let mut binary = parent_session.read(cx).binary().clone();
binary.request_args = request.clone();

View File

@@ -1,6 +1,4 @@
use std::time::Duration;
use gpui::{Animation, AnimationExt as _, Entity, Transformation, percentage};
use gpui::Entity;
use project::debugger::session::{ThreadId, ThreadStatus};
use ui::{ContextMenu, DropdownMenu, DropdownStyle, Indicator, prelude::*};
@@ -25,40 +23,31 @@ impl DebugPanel {
let sessions = self.sessions().clone();
let weak = cx.weak_entity();
let running_state = running_state.read(cx);
let label = if let Some(active_session) = active_session.clone() {
let label = if let Some(active_session) = active_session {
active_session.read(cx).session(cx).read(cx).label()
} else {
SharedString::new_static("Unknown Session")
};
let is_terminated = running_state.session().read(cx).is_terminated();
let is_started = active_session
.is_some_and(|session| session.read(cx).session(cx).read(cx).is_started());
let session_state_indicator = if is_terminated {
Indicator::dot().color(Color::Error).into_any_element()
} else if !is_started {
Icon::new(IconName::ArrowCircle)
.size(IconSize::Small)
.color(Color::Muted)
.with_animation(
"arrow-circle",
Animation::new(Duration::from_secs(2)).repeat(),
|icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
)
.into_any_element()
} else {
match running_state.thread_status(cx).unwrap_or_default() {
ThreadStatus::Stopped => {
Indicator::dot().color(Color::Conflict).into_any_element()
let session_state_indicator = {
if is_terminated {
Some(Indicator::dot().color(Color::Error))
} else {
match running_state.thread_status(cx).unwrap_or_default() {
project::debugger::session::ThreadStatus::Stopped => {
Some(Indicator::dot().color(Color::Conflict))
}
_ => Some(Indicator::dot().color(Color::Success)),
}
_ => Indicator::dot().color(Color::Success).into_any_element(),
}
};
let trigger = h_flex()
.gap_2()
.child(session_state_indicator)
.when_some(session_state_indicator, |this, indicator| {
this.child(indicator)
})
.justify_between()
.child(
DebugPanel::dropdown_label(label)

View File

@@ -547,10 +547,6 @@ impl RunningState {
.for_each(|value| Self::substitute_variables_in_config(value, context));
}
serde_json::Value::String(s) => {
// Some built-in zed tasks wrap their arguments in quotes as they might contain spaces.
if s.starts_with("\"$ZED_") && s.ends_with('"') {
*s = s[1..s.len() - 1].to_string();
}
if let Some(substituted) = substitute_variables_in_str(&s, context) {
*s = substituted;
}
@@ -575,10 +571,6 @@ impl RunningState {
.for_each(|value| Self::relativlize_paths(None, value, context));
}
serde_json::Value::String(s) if key == Some("program") || key == Some("cwd") => {
// Some built-in zed tasks wrap their arguments in quotes as they might contain spaces.
if s.starts_with("\"$ZED_") && s.ends_with('"') {
*s = s[1..s.len() - 1].to_string();
}
resolve_path(s);
if let Some(substituted) = substitute_variables_in_str(&s, context) {
@@ -874,7 +866,6 @@ impl RunningState {
args,
..task.resolved.clone()
};
let terminal = project
.update_in(cx, |project, window, cx| {
project.create_terminal(
@@ -919,6 +910,12 @@ impl RunningState {
};
if config_is_valid {
// Ok(DebugTaskDefinition {
// label,
// adapter: DebugAdapterName(adapter),
// config,
// tcp_connection,
// })
} else if let Some((task, locator_name)) = build_output {
let locator_name =
locator_name.context("Could not find a valid locator for a build task")?;
@@ -937,7 +934,7 @@ impl RunningState {
let scenario = dap_registry
.adapter(&adapter)
.context(format!("{}: is not a valid adapter name", &adapter))
.ok_or_else(|| anyhow!("{}: is not a valid adapter name", &adapter))
.map(|adapter| adapter.config_from_zed_format(zed_config))??;
config = scenario.config;
Self::substitute_variables_in_config(&mut config, &task_context);

View File

@@ -110,7 +110,7 @@ impl Console {
}
fn is_running(&self, cx: &Context<Self>) -> bool {
self.session.read(cx).is_running()
self.session.read(cx).is_local()
}
fn handle_stack_frame_list_events(

View File

@@ -250,6 +250,9 @@ impl StackFrameList {
let Some(abs_path) = Self::abs_path_from_stack_frame(&stack_frame) else {
return Task::ready(Err(anyhow!("Project path not found")));
};
if abs_path.starts_with("<node_internals>") {
return Task::ready(Ok(()));
}
let row = stack_frame.line.saturating_sub(1) as u32;
cx.emit(StackFrameListEvent::SelectedStackFrameChanged(
stack_frame_id,
@@ -342,7 +345,6 @@ impl StackFrameList {
s.path
.as_deref()
.map(|path| Arc::<Path>::from(Path::new(path)))
.filter(|path| path.is_absolute())
})
}

View File

@@ -82,7 +82,6 @@ tree-sitter-rust = { workspace = true, optional = true }
tree-sitter-typescript = { workspace = true, optional = true }
tree-sitter-python = { workspace = true, optional = true }
unicode-segmentation.workspace = true
unicode-script.workspace = true
unindent = { workspace = true, optional = true }
ui.workspace = true
url.workspace = true

View File

@@ -201,7 +201,7 @@ use ui::{
ButtonSize, ButtonStyle, ContextMenu, Disclosure, IconButton, IconButtonShape, IconName,
IconSize, Indicator, Key, Tooltip, h_flex, prelude::*,
};
use util::{RangeExt, ResultExt, TryFutureExt, maybe, post_inc};
use util::{RangeExt, ResultExt, TryFutureExt, maybe, post_inc, wrap_with_prefix};
use workspace::{
CollaboratorId, Item as WorkspaceItem, ItemId, ItemNavHistory, OpenInTerminal, OpenTerminal,
RestoreOnStartupBehavior, SERIALIZATION_THROTTLE_TIME, SplitDirection, TabBarSettings, Toast,
@@ -19587,347 +19587,6 @@ fn update_uncommitted_diff_for_buffer(
})
}
fn char_len_with_expanded_tabs(offset: usize, text: &str, tab_size: NonZeroU32) -> usize {
let tab_size = tab_size.get() as usize;
let mut width = offset;
for ch in text.chars() {
width += if ch == '\t' {
tab_size - (width % tab_size)
} else {
1
};
}
width - offset
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_string_size_with_expanded_tabs() {
let nz = |val| NonZeroU32::new(val).unwrap();
assert_eq!(char_len_with_expanded_tabs(0, "", nz(4)), 0);
assert_eq!(char_len_with_expanded_tabs(0, "hello", nz(4)), 5);
assert_eq!(char_len_with_expanded_tabs(0, "\thello", nz(4)), 9);
assert_eq!(char_len_with_expanded_tabs(0, "abc\tab", nz(4)), 6);
assert_eq!(char_len_with_expanded_tabs(0, "hello\t", nz(4)), 8);
assert_eq!(char_len_with_expanded_tabs(0, "\t\t", nz(8)), 16);
assert_eq!(char_len_with_expanded_tabs(0, "x\t", nz(8)), 8);
assert_eq!(char_len_with_expanded_tabs(7, "x\t", nz(8)), 9);
}
}
/// Tokenizes a string into runs of text that should stick together, or that is whitespace.
struct WordBreakingTokenizer<'a> {
input: &'a str,
}
impl<'a> WordBreakingTokenizer<'a> {
fn new(input: &'a str) -> Self {
Self { input }
}
}
fn is_char_ideographic(ch: char) -> bool {
use unicode_script::Script::*;
use unicode_script::UnicodeScript;
matches!(ch.script(), Han | Tangut | Yi)
}
fn is_grapheme_ideographic(text: &str) -> bool {
text.chars().any(is_char_ideographic)
}
fn is_grapheme_whitespace(text: &str) -> bool {
text.chars().any(|x| x.is_whitespace())
}
fn should_stay_with_preceding_ideograph(text: &str) -> bool {
text.chars().next().map_or(false, |ch| {
matches!(ch, '。' | '、' | '' | '' | '' | '' | '' | '…')
})
}
#[derive(PartialEq, Eq, Debug, Clone, Copy)]
enum WordBreakToken<'a> {
Word { token: &'a str, grapheme_len: usize },
InlineWhitespace { token: &'a str, grapheme_len: usize },
Newline,
}
impl<'a> Iterator for WordBreakingTokenizer<'a> {
/// Yields a span, the count of graphemes in the token, and whether it was
/// whitespace. Note that it also breaks at word boundaries.
type Item = WordBreakToken<'a>;
fn next(&mut self) -> Option<Self::Item> {
use unicode_segmentation::UnicodeSegmentation;
if self.input.is_empty() {
return None;
}
let mut iter = self.input.graphemes(true).peekable();
let mut offset = 0;
let mut grapheme_len = 0;
if let Some(first_grapheme) = iter.next() {
let is_newline = first_grapheme == "\n";
let is_whitespace = is_grapheme_whitespace(first_grapheme);
offset += first_grapheme.len();
grapheme_len += 1;
if is_grapheme_ideographic(first_grapheme) && !is_whitespace {
if let Some(grapheme) = iter.peek().copied() {
if should_stay_with_preceding_ideograph(grapheme) {
offset += grapheme.len();
grapheme_len += 1;
}
}
} else {
let mut words = self.input[offset..].split_word_bound_indices().peekable();
let mut next_word_bound = words.peek().copied();
if next_word_bound.map_or(false, |(i, _)| i == 0) {
next_word_bound = words.next();
}
while let Some(grapheme) = iter.peek().copied() {
if next_word_bound.map_or(false, |(i, _)| i == offset) {
break;
};
if is_grapheme_whitespace(grapheme) != is_whitespace
|| (grapheme == "\n") != is_newline
{
break;
};
offset += grapheme.len();
grapheme_len += 1;
iter.next();
}
}
let token = &self.input[..offset];
self.input = &self.input[offset..];
if token == "\n" {
Some(WordBreakToken::Newline)
} else if is_whitespace {
Some(WordBreakToken::InlineWhitespace {
token,
grapheme_len,
})
} else {
Some(WordBreakToken::Word {
token,
grapheme_len,
})
}
} else {
None
}
}
}
#[test]
fn test_word_breaking_tokenizer() {
let tests: &[(&str, &[WordBreakToken<'static>])] = &[
("", &[]),
(" ", &[whitespace(" ", 2)]),
("Ʒ", &[word("Ʒ", 1)]),
("Ǽ", &[word("Ǽ", 1)]),
("", &[word("", 1)]),
("⋑⋑", &[word("⋑⋑", 2)]),
(
"原理,进而",
&[word("", 1), word("理,", 2), word("", 1), word("", 1)],
),
(
"hello world",
&[word("hello", 5), whitespace(" ", 1), word("world", 5)],
),
(
"hello, world",
&[word("hello,", 6), whitespace(" ", 1), word("world", 5)],
),
(
" hello world",
&[
whitespace(" ", 2),
word("hello", 5),
whitespace(" ", 1),
word("world", 5),
],
),
(
"这是什么 \n 钢笔",
&[
word("", 1),
word("", 1),
word("", 1),
word("", 1),
whitespace(" ", 1),
newline(),
whitespace(" ", 1),
word("", 1),
word("", 1),
],
),
("mutton", &[whitespace("", 1), word("mutton", 6)]),
];
fn word(token: &'static str, grapheme_len: usize) -> WordBreakToken<'static> {
WordBreakToken::Word {
token,
grapheme_len,
}
}
fn whitespace(token: &'static str, grapheme_len: usize) -> WordBreakToken<'static> {
WordBreakToken::InlineWhitespace {
token,
grapheme_len,
}
}
fn newline() -> WordBreakToken<'static> {
WordBreakToken::Newline
}
for (input, result) in tests {
assert_eq!(
WordBreakingTokenizer::new(input)
.collect::<Vec<_>>()
.as_slice(),
*result,
);
}
}
fn wrap_with_prefix(
line_prefix: String,
unwrapped_text: String,
wrap_column: usize,
tab_size: NonZeroU32,
preserve_existing_whitespace: bool,
) -> String {
let line_prefix_len = char_len_with_expanded_tabs(0, &line_prefix, tab_size);
let mut wrapped_text = String::new();
let mut current_line = line_prefix.clone();
let tokenizer = WordBreakingTokenizer::new(&unwrapped_text);
let mut current_line_len = line_prefix_len;
let mut in_whitespace = false;
for token in tokenizer {
let have_preceding_whitespace = in_whitespace;
match token {
WordBreakToken::Word {
token,
grapheme_len,
} => {
in_whitespace = false;
if current_line_len + grapheme_len > wrap_column
&& current_line_len != line_prefix_len
{
wrapped_text.push_str(current_line.trim_end());
wrapped_text.push('\n');
current_line.truncate(line_prefix.len());
current_line_len = line_prefix_len;
}
current_line.push_str(token);
current_line_len += grapheme_len;
}
WordBreakToken::InlineWhitespace {
mut token,
mut grapheme_len,
} => {
in_whitespace = true;
if have_preceding_whitespace && !preserve_existing_whitespace {
continue;
}
if !preserve_existing_whitespace {
token = " ";
grapheme_len = 1;
}
if current_line_len + grapheme_len > wrap_column {
wrapped_text.push_str(current_line.trim_end());
wrapped_text.push('\n');
current_line.truncate(line_prefix.len());
current_line_len = line_prefix_len;
} else if current_line_len != line_prefix_len || preserve_existing_whitespace {
current_line.push_str(token);
current_line_len += grapheme_len;
}
}
WordBreakToken::Newline => {
in_whitespace = true;
if preserve_existing_whitespace {
wrapped_text.push_str(current_line.trim_end());
wrapped_text.push('\n');
current_line.truncate(line_prefix.len());
current_line_len = line_prefix_len;
} else if have_preceding_whitespace {
continue;
} else if current_line_len + 1 > wrap_column && current_line_len != line_prefix_len
{
wrapped_text.push_str(current_line.trim_end());
wrapped_text.push('\n');
current_line.truncate(line_prefix.len());
current_line_len = line_prefix_len;
} else if current_line_len != line_prefix_len {
current_line.push(' ');
current_line_len += 1;
}
}
}
}
if !current_line.is_empty() {
wrapped_text.push_str(&current_line);
}
wrapped_text
}
#[test]
fn test_wrap_with_prefix() {
assert_eq!(
wrap_with_prefix(
"# ".to_string(),
"abcdefg".to_string(),
4,
NonZeroU32::new(4).unwrap(),
false,
),
"# abcdefg"
);
assert_eq!(
wrap_with_prefix(
"".to_string(),
"\thello world".to_string(),
8,
NonZeroU32::new(4).unwrap(),
false,
),
"hello\nworld"
);
assert_eq!(
wrap_with_prefix(
"// ".to_string(),
"xx \nyy zz aa bb cc".to_string(),
12,
NonZeroU32::new(4).unwrap(),
false,
),
"// xx yy zz\n// aa bb cc"
);
assert_eq!(
wrap_with_prefix(
String::new(),
"这是什么 \n 钢笔".to_string(),
3,
NonZeroU32::new(4).unwrap(),
false,
),
"这是什\n么 钢\n"
);
}
pub trait CollaborationHub {
fn collaborators<'a>(&self, cx: &'a App) -> &'a HashMap<PeerId, Collaborator>;
fn user_participant_indices<'a>(&self, cx: &'a App) -> &'a HashMap<u64, ParticipantIndex>;

View File

@@ -7607,10 +7607,7 @@ impl Element for EditorElement {
editor.gutter_dimensions = gutter_dimensions;
editor.set_visible_line_count(bounds.size.height / line_height, window, cx);
if matches!(
editor.mode,
EditorMode::AutoHeight { .. } | EditorMode::Minimap { .. }
) {
if matches!(editor.mode, EditorMode::Minimap { .. }) {
snapshot
} else {
let wrap_width_for = |column: u32| (column as f32 * em_advance).ceil();
@@ -9629,6 +9626,7 @@ fn compute_auto_height_layout(
let font_size = style.text.font_size.to_pixels(window.rem_size());
let line_height = style.text.line_height_in_pixels(window.rem_size());
let em_width = window.text_system().em_width(font_id, font_size).unwrap();
let em_advance = window.text_system().em_advance(font_id, font_size).unwrap();
let mut snapshot = editor.snapshot(window, cx);
let gutter_dimensions = snapshot
@@ -9645,10 +9643,18 @@ fn compute_auto_height_layout(
let overscroll = size(em_width, px(0.));
let editor_width = text_width - gutter_dimensions.margin - overscroll.width - em_width;
if !matches!(editor.soft_wrap_mode(cx), SoftWrap::None) {
if editor.set_wrap_width(Some(editor_width), cx) {
snapshot = editor.snapshot(window, cx);
}
let content_offset = point(gutter_dimensions.margin, Pixels::ZERO);
let editor_content_width = editor_width - content_offset.x;
let wrap_width_for = |column: u32| (column as f32 * em_advance).ceil();
let wrap_width = match editor.soft_wrap_mode(cx) {
SoftWrap::GitDiff => None,
SoftWrap::None => Some(wrap_width_for(MAX_LINE_LEN as u32 / 2)),
SoftWrap::EditorWidth => Some(editor_content_width),
SoftWrap::Column(column) => Some(wrap_width_for(column)),
SoftWrap::Bounded(column) => Some(editor_content_width.min(wrap_width_for(column))),
};
if editor.set_wrap_width(wrap_width, cx) {
snapshot = editor.snapshot(window, cx);
}
let scroll_height = (snapshot.max_point().row().next_row().0 as f32) * line_height;

View File

@@ -1527,6 +1527,7 @@ impl PickerDelegate for FileFinderDelegate {
)
.child(
h_flex()
.p_2()
.gap_2()
.child(
Button::new("open-selection", "Open").on_click(|_, window, cx| {

View File

@@ -54,6 +54,7 @@ use project::{
use serde::{Deserialize, Serialize};
use settings::{Settings as _, SettingsStore};
use std::future::Future;
use std::num::NonZeroU32;
use std::path::{Path, PathBuf};
use std::{collections::HashSet, sync::Arc, time::Duration, usize};
use strum::{IntoEnumIterator, VariantNames};
@@ -62,7 +63,7 @@ use ui::{
Checkbox, ContextMenu, ElevationIndex, PopoverMenu, Scrollbar, ScrollbarState, SplitButton,
Tooltip, prelude::*,
};
use util::{ResultExt, TryFutureExt, maybe};
use util::{ResultExt, TryFutureExt, maybe, wrap_with_prefix};
use workspace::AppState;
use notifications::status_toast::{StatusToast, ToastIcon};
@@ -384,7 +385,6 @@ pub(crate) fn commit_message_editor(
commit_editor.set_show_gutter(false, cx);
commit_editor.set_show_wrap_guides(false, cx);
commit_editor.set_show_indent_guides(false, cx);
commit_editor.set_hard_wrap(Some(72), cx);
let placeholder = placeholder.unwrap_or("Enter commit message".into());
commit_editor.set_placeholder_text(placeholder, cx);
commit_editor
@@ -1486,8 +1486,22 @@ impl GitPanel {
fn custom_or_suggested_commit_message(&self, cx: &mut Context<Self>) -> Option<String> {
let message = self.commit_editor.read(cx).text(cx);
let width = self
.commit_editor
.read(cx)
.buffer()
.read(cx)
.language_settings(cx)
.preferred_line_length as usize;
if !message.trim().is_empty() {
let message = wrap_with_prefix(
String::new(),
message,
width,
NonZeroU32::new(8).unwrap(), // tab size doesn't matter when prefix is empty
false,
);
return Some(message);
}

View File

@@ -680,7 +680,7 @@ pub struct CodeLabel {
pub filter_range: Range<usize>,
}
#[derive(Clone, Deserialize, JsonSchema)]
#[derive(Clone, Debug, Deserialize, JsonSchema)]
pub struct LanguageConfig {
/// Human-readable name of the language.
pub name: LanguageName,
@@ -791,7 +791,7 @@ pub struct LanguageMatcher {
}
/// The configuration for JSX tag auto-closing.
#[derive(Clone, Deserialize, JsonSchema)]
#[derive(Clone, Debug, Deserialize, JsonSchema)]
pub struct JsxTagAutoCloseConfig {
/// The name of the node for a opening tag
pub open_tag_node_name: String,
@@ -824,7 +824,7 @@ pub struct JsxTagAutoCloseConfig {
}
/// The configuration for documentation block for this language.
#[derive(Clone, Deserialize, JsonSchema)]
#[derive(Clone, Debug, Deserialize, JsonSchema)]
pub struct DocumentationConfig {
/// A start tag of documentation block.
pub start: Arc<str>,

View File

@@ -1076,7 +1076,7 @@ fn test_edit_sequence(language_name: &str, steps: &[&str], cx: &mut App) -> (Buf
.now_or_never()
.unwrap()
.unwrap();
let mut buffer = Buffer::new(0, BufferId::new(1).unwrap(), "");
let mut buffer = Buffer::new(0, BufferId::new(1).unwrap(), Default::default());
let mut mutated_syntax_map = SyntaxMap::new(&buffer);
mutated_syntax_map.set_language_registry(registry.clone());

View File

@@ -107,18 +107,14 @@ impl FakeLanguageModel {
self.current_completion_txs.lock().len()
}
pub fn stream_completion_response(
&self,
request: &LanguageModelRequest,
chunk: impl Into<String>,
) {
pub fn stream_completion_response(&self, request: &LanguageModelRequest, chunk: String) {
let current_completion_txs = self.current_completion_txs.lock();
let tx = current_completion_txs
.iter()
.find(|(req, _)| req == request)
.map(|(_, tx)| tx)
.unwrap();
tx.unbounded_send(chunk.into()).unwrap();
tx.unbounded_send(chunk).unwrap();
}
pub fn end_completion_stream(&self, request: &LanguageModelRequest) {
@@ -127,7 +123,7 @@ impl FakeLanguageModel {
.retain(|(req, _)| req != request);
}
pub fn stream_last_completion_response(&self, chunk: impl Into<String>) {
pub fn stream_last_completion_response(&self, chunk: String) {
self.stream_completion_response(self.pending_completions().last().unwrap(), chunk);
}

View File

@@ -357,9 +357,6 @@ const PYTHON_TEST_TARGET_TASK_VARIABLE: VariableName =
const PYTHON_ACTIVE_TOOLCHAIN_PATH: VariableName =
VariableName::Custom(Cow::Borrowed("PYTHON_ACTIVE_ZED_TOOLCHAIN"));
const PYTHON_ACTIVE_TOOLCHAIN_PATH_RAW: VariableName =
VariableName::Custom(Cow::Borrowed("PYTHON_ACTIVE_ZED_TOOLCHAIN_RAW"));
const PYTHON_MODULE_NAME_TASK_VARIABLE: VariableName =
VariableName::Custom(Cow::Borrowed("PYTHON_MODULE_NAME"));
@@ -381,25 +378,21 @@ impl ContextProvider for PythonContextProvider {
let worktree_id = location.buffer.read(cx).file().map(|f| f.worktree_id(cx));
cx.spawn(async move |cx| {
let raw_toolchain = if let Some(worktree_id) = worktree_id {
let active_toolchain = if let Some(worktree_id) = worktree_id {
toolchains
.active_toolchain(worktree_id, Arc::from("".as_ref()), "Python".into(), cx)
.await
.map_or_else(
|| String::from("python3"),
|toolchain| toolchain.path.to_string(),
)
.map_or_else(|| "python3".to_owned(), |toolchain| toolchain.path.into())
} else {
String::from("python3")
};
let active_toolchain = format!("\"{raw_toolchain}\"");
let toolchain = (PYTHON_ACTIVE_TOOLCHAIN_PATH, active_toolchain);
let raw_toolchain = (PYTHON_ACTIVE_TOOLCHAIN_PATH_RAW, raw_toolchain);
Ok(task::TaskVariables::from_iter(
test_target
.into_iter()
.chain(module_target.into_iter())
.chain([toolchain, raw_toolchain]),
.chain([toolchain]),
))
})
}
@@ -420,7 +413,6 @@ impl ContextProvider for PythonContextProvider {
"-c".to_owned(),
VariableName::SelectedText.template_value_with_whitespace(),
],
cwd: Some("$ZED_WORKTREE_ROOT".into()),
..TaskTemplate::default()
},
// Execute an entire file
@@ -428,7 +420,6 @@ impl ContextProvider for PythonContextProvider {
label: format!("run '{}'", VariableName::File.template_value()),
command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value(),
args: vec![VariableName::File.template_value_with_whitespace()],
cwd: Some("$ZED_WORKTREE_ROOT".into()),
..TaskTemplate::default()
},
// Execute a file as module
@@ -439,7 +430,6 @@ impl ContextProvider for PythonContextProvider {
"-m".to_owned(),
PYTHON_MODULE_NAME_TASK_VARIABLE.template_value(),
],
cwd: Some("$ZED_WORKTREE_ROOT".into()),
tags: vec!["python-module-main-method".to_owned()],
..TaskTemplate::default()
},
@@ -457,7 +447,6 @@ impl ContextProvider for PythonContextProvider {
"unittest".to_owned(),
VariableName::File.template_value_with_whitespace(),
],
cwd: Some("$ZED_WORKTREE_ROOT".into()),
..TaskTemplate::default()
},
// Run test(s) for a specific target within a file
@@ -473,7 +462,6 @@ impl ContextProvider for PythonContextProvider {
"python-unittest-class".to_owned(),
"python-unittest-method".to_owned(),
],
cwd: Some("$ZED_WORKTREE_ROOT".into()),
..TaskTemplate::default()
},
]
@@ -489,7 +477,6 @@ impl ContextProvider for PythonContextProvider {
"pytest".to_owned(),
VariableName::File.template_value_with_whitespace(),
],
cwd: Some("$ZED_WORKTREE_ROOT".into()),
..TaskTemplate::default()
},
// Run test(s) for a specific target within a file
@@ -501,7 +488,6 @@ impl ContextProvider for PythonContextProvider {
"pytest".to_owned(),
PYTHON_TEST_TARGET_TASK_VARIABLE.template_value_with_whitespace(),
],
cwd: Some("$ZED_WORKTREE_ROOT".into()),
tags: vec![
"python-pytest-class".to_owned(),
"python-pytest-method".to_owned(),

View File

@@ -1209,7 +1209,6 @@ impl Element for MarkdownElement {
) -> Self::PrepaintState {
let focus_handle = self.markdown.read(cx).focus_handle.clone();
window.set_focus_handle(&focus_handle, cx);
window.set_view_id(self.markdown.entity_id());
let hitbox = window.insert_hitbox(bounds, false);
rendered_markdown.element.prepaint(window, cx);

View File

@@ -622,7 +622,7 @@ impl LocalBufferStore {
Ok(buffer) => Ok(buffer),
Err(error) if is_not_found_error(&error) => cx.new(|cx| {
let buffer_id = BufferId::from(cx.entity_id().as_non_zero_u64());
let text_buffer = text::Buffer::new(0, buffer_id, "");
let text_buffer = text::Buffer::new(0, buffer_id, "".into());
Buffer::build(
text_buffer,
Some(Arc::new(File {

View File

@@ -101,10 +101,7 @@ impl DapStore {
pub fn init(client: &AnyProtoClient, cx: &mut App) {
static ADD_LOCATORS: Once = Once::new();
ADD_LOCATORS.call_once(|| {
let registry = DapRegistry::global(cx);
registry.add_locator(Arc::new(locators::cargo::CargoLocator {}));
registry.add_locator(Arc::new(locators::python::PythonLocator));
registry.add_locator(Arc::new(locators::go::GoLocator {}));
DapRegistry::global(cx).add_locator(Arc::new(locators::cargo::CargoLocator {}))
});
client.add_entity_request_handler(Self::handle_run_debug_locator);
client.add_entity_request_handler(Self::handle_get_debug_adapter_binary);
@@ -415,6 +412,7 @@ impl DapStore {
this.get_debug_adapter_binary(definition.clone(), session_id, console, cx)
})?
.await?;
session
.update(cx, |session, cx| {
session.boot(binary, worktree, dap_store, cx)

View File

@@ -1,3 +1 @@
pub(crate) mod cargo;
pub(crate) mod go;
pub(crate) mod python;

View File

@@ -1,248 +0,0 @@
use anyhow::Result;
use async_trait::async_trait;
use collections::FxHashMap;
use dap::{DapLocator, DebugRequest, adapters::DebugAdapterName};
use gpui::SharedString;
use std::path::PathBuf;
use task::{
BuildTaskDefinition, DebugScenario, RevealStrategy, RevealTarget, Shell, SpawnInTerminal,
TaskTemplate,
};
pub(crate) struct GoLocator;
#[async_trait]
impl DapLocator for GoLocator {
fn name(&self) -> SharedString {
SharedString::new_static("go-debug-locator")
}
fn create_scenario(
&self,
build_config: &TaskTemplate,
resolved_label: &str,
adapter: DebugAdapterName,
) -> Option<DebugScenario> {
if build_config.command != "go" {
return None;
}
let go_action = build_config.args.first()?;
match go_action.as_str() {
"run" => {
let program = build_config
.args
.get(1)
.cloned()
.unwrap_or_else(|| ".".to_string());
let build_task = TaskTemplate {
label: "go build debug".into(),
command: "go".into(),
args: vec![
"build".into(),
"-gcflags \"all=-N -l\"".into(),
program.clone(),
],
env: build_config.env.clone(),
cwd: build_config.cwd.clone(),
use_new_terminal: false,
allow_concurrent_runs: false,
reveal: RevealStrategy::Always,
reveal_target: RevealTarget::Dock,
hide: task::HideStrategy::Never,
shell: Shell::System,
tags: vec![],
show_summary: true,
show_command: true,
};
Some(DebugScenario {
label: resolved_label.to_string().into(),
adapter: adapter.0,
build: Some(BuildTaskDefinition::Template {
task_template: build_task,
locator_name: Some(self.name()),
}),
config: serde_json::Value::Null,
tcp_connection: None,
})
}
_ => None,
}
}
async fn run(&self, build_config: SpawnInTerminal) -> Result<DebugRequest> {
if build_config.args.is_empty() {
return Err(anyhow::anyhow!("Invalid Go command"));
}
let go_action = &build_config.args[0];
let cwd = build_config
.cwd
.as_ref()
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_else(|| ".".to_string());
let mut env = FxHashMap::default();
for (key, value) in &build_config.env {
env.insert(key.clone(), value.clone());
}
match go_action.as_str() {
"build" => {
let package = build_config
.args
.get(2)
.cloned()
.unwrap_or_else(|| ".".to_string());
Ok(DebugRequest::Launch(task::LaunchRequest {
program: package,
cwd: Some(PathBuf::from(&cwd)),
args: vec![],
env,
}))
}
_ => Err(anyhow::anyhow!("Unsupported Go command: {}", go_action)),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use task::{HideStrategy, RevealStrategy, RevealTarget, Shell, TaskTemplate};
#[test]
fn test_create_scenario_for_go_run() {
let locator = GoLocator;
let task = TaskTemplate {
label: "go run main.go".into(),
command: "go".into(),
args: vec!["run".into(), "main.go".into()],
env: Default::default(),
cwd: Some("${ZED_WORKTREE_ROOT}".into()),
use_new_terminal: false,
allow_concurrent_runs: false,
reveal: RevealStrategy::Always,
reveal_target: RevealTarget::Dock,
hide: HideStrategy::Never,
shell: Shell::System,
tags: vec![],
show_summary: true,
show_command: true,
};
let scenario =
locator.create_scenario(&task, "test label", DebugAdapterName("Delve".into()));
assert!(scenario.is_some());
let scenario = scenario.unwrap();
assert_eq!(scenario.adapter, "Delve");
assert_eq!(scenario.label, "test label");
assert!(scenario.build.is_some());
if let Some(BuildTaskDefinition::Template { task_template, .. }) = &scenario.build {
assert_eq!(task_template.command, "go");
assert!(task_template.args.contains(&"build".into()));
assert!(
task_template
.args
.contains(&"-gcflags \"all=-N -l\"".into())
);
assert!(task_template.args.contains(&"main.go".into()));
} else {
panic!("Expected BuildTaskDefinition::Template");
}
assert!(
scenario.config.is_null(),
"Initial config should be null to ensure it's invalid"
);
}
#[test]
fn test_create_scenario_for_go_build() {
let locator = GoLocator;
let task = TaskTemplate {
label: "go build".into(),
command: "go".into(),
args: vec!["build".into(), ".".into()],
env: Default::default(),
cwd: Some("${ZED_WORKTREE_ROOT}".into()),
use_new_terminal: false,
allow_concurrent_runs: false,
reveal: RevealStrategy::Always,
reveal_target: RevealTarget::Dock,
hide: HideStrategy::Never,
shell: Shell::System,
tags: vec![],
show_summary: true,
show_command: true,
};
let scenario =
locator.create_scenario(&task, "test label", DebugAdapterName("Delve".into()));
assert!(scenario.is_none());
}
#[test]
fn test_skip_non_go_commands_with_non_delve_adapter() {
let locator = GoLocator;
let task = TaskTemplate {
label: "cargo build".into(),
command: "cargo".into(),
args: vec!["build".into()],
env: Default::default(),
cwd: Some("${ZED_WORKTREE_ROOT}".into()),
use_new_terminal: false,
allow_concurrent_runs: false,
reveal: RevealStrategy::Always,
reveal_target: RevealTarget::Dock,
hide: HideStrategy::Never,
shell: Shell::System,
tags: vec![],
show_summary: true,
show_command: true,
};
let scenario = locator.create_scenario(
&task,
"test label",
DebugAdapterName("SomeOtherAdapter".into()),
);
assert!(scenario.is_none());
let scenario =
locator.create_scenario(&task, "test label", DebugAdapterName("Delve".into()));
assert!(scenario.is_none());
}
#[test]
fn test_skip_unsupported_go_commands() {
let locator = GoLocator;
let task = TaskTemplate {
label: "go clean".into(),
command: "go".into(),
args: vec!["clean".into()],
env: Default::default(),
cwd: Some("${ZED_WORKTREE_ROOT}".into()),
use_new_terminal: false,
allow_concurrent_runs: false,
reveal: RevealStrategy::Always,
reveal_target: RevealTarget::Dock,
hide: HideStrategy::Never,
shell: Shell::System,
tags: vec![],
show_summary: true,
show_command: true,
};
let scenario =
locator.create_scenario(&task, "test label", DebugAdapterName("Delve".into()));
assert!(scenario.is_none());
}
}

View File

@@ -1,106 +0,0 @@
use std::path::Path;
use anyhow::{Result, bail};
use async_trait::async_trait;
use dap::{DapLocator, DebugRequest, adapters::DebugAdapterName};
use gpui::SharedString;
use task::{DebugScenario, SpawnInTerminal, TaskTemplate, VariableName};
pub(crate) struct PythonLocator;
#[async_trait]
impl DapLocator for PythonLocator {
fn name(&self) -> SharedString {
SharedString::new_static("Python")
}
/// Determines whether this locator can generate debug target for given task.
fn create_scenario(
&self,
build_config: &TaskTemplate,
resolved_label: &str,
adapter: DebugAdapterName,
) -> Option<DebugScenario> {
if adapter.as_ref() != "Debugpy" {
return None;
}
let valid_program = build_config.command.starts_with("$ZED_")
|| Path::new(&build_config.command)
.file_name()
.map_or(false, |name| {
name.to_str().is_some_and(|path| path.starts_with("python"))
});
if !valid_program || build_config.args.iter().any(|arg| arg == "-c") {
// We cannot debug selections.
return None;
}
let command = if build_config.command
== VariableName::Custom("PYTHON_ACTIVE_ZED_TOOLCHAIN".into()).template_value()
{
VariableName::Custom("PYTHON_ACTIVE_ZED_TOOLCHAIN_RAW".into()).template_value()
} else {
build_config.command.clone()
};
let module_specifier_position = build_config
.args
.iter()
.position(|arg| arg == "-m")
.map(|position| position + 1);
// Skip the -m and module name, get all that's after.
let mut rest_of_the_args = module_specifier_position
.and_then(|position| build_config.args.get(position..))
.into_iter()
.flatten()
.fuse();
let mod_name = rest_of_the_args.next();
let args = rest_of_the_args.collect::<Vec<_>>();
let program_position = mod_name
.is_none()
.then(|| {
build_config
.args
.iter()
.position(|arg| *arg == "\"$ZED_FILE\"")
})
.flatten();
let args = if let Some(position) = program_position {
args.into_iter().skip(position).collect::<Vec<_>>()
} else {
args
};
if program_position.is_none() && mod_name.is_none() {
return None;
}
let mut config = serde_json::json!({
"request": "launch",
"python": command,
"args": args,
"cwd": build_config.cwd.clone()
});
if let Some(config_obj) = config.as_object_mut() {
if let Some(module) = mod_name {
config_obj.insert("module".to_string(), module.clone().into());
}
if let Some(program) = program_position {
config_obj.insert(
"program".to_string(),
build_config.args[program].clone().into(),
);
}
}
Some(DebugScenario {
adapter: adapter.0,
label: resolved_label.to_string().into(),
build: None,
config,
tcp_connection: None,
})
}
async fn run(&self, _: SpawnInTerminal) -> Result<DebugRequest> {
bail!("Python locator should not require DapLocator::run to be ran");
}
}

View File

@@ -121,17 +121,16 @@ impl From<dap::Thread> for Thread {
pub enum Mode {
Building,
Running(RunningMode),
Running(LocalMode),
}
#[derive(Clone)]
pub struct RunningMode {
pub struct LocalMode {
client: Arc<DebugAdapterClient>,
binary: DebugAdapterBinary,
tmp_breakpoint: Option<SourceBreakpoint>,
worktree: WeakEntity<Worktree>,
executor: BackgroundExecutor,
is_started: bool,
}
fn client_source(abs_path: &Path) -> dap::Source {
@@ -149,7 +148,7 @@ fn client_source(abs_path: &Path) -> dap::Source {
}
}
impl RunningMode {
impl LocalMode {
async fn new(
session_id: SessionId,
parent_session: Option<Entity<Session>>,
@@ -182,7 +181,6 @@ impl RunningMode {
tmp_breakpoint: None,
binary,
executor: cx.background_executor().clone(),
is_started: false,
})
}
@@ -375,7 +373,7 @@ impl RunningMode {
capabilities: &Capabilities,
initialized_rx: oneshot::Receiver<()>,
dap_store: WeakEntity<DapStore>,
cx: &mut Context<Session>,
cx: &App,
) -> Task<Result<()>> {
let raw = self.binary.request_args.clone();
@@ -407,7 +405,7 @@ impl RunningMode {
let this = self.clone();
let worktree = self.worktree().clone();
let configuration_sequence = cx.spawn({
async move |_, cx| {
async move |cx| {
let breakpoint_store =
dap_store.read_with(cx, |dap_store, _| dap_store.breakpoint_store().clone())?;
initialized_rx.await?;
@@ -455,20 +453,9 @@ impl RunningMode {
}
});
let task = cx.background_spawn(futures::future::try_join(launch, configuration_sequence));
cx.spawn(async move |this, cx| {
task.await?;
this.update(cx, |this, cx| {
if let Some(this) = this.as_running_mut() {
this.is_started = true;
cx.notify();
}
})
.ok();
anyhow::Ok(())
cx.background_spawn(async move {
futures::future::try_join(launch, configuration_sequence).await?;
Ok(())
})
}
@@ -717,7 +704,7 @@ impl Session {
cx.subscribe(&breakpoint_store, |this, store, event, cx| match event {
BreakpointStoreEvent::BreakpointsUpdated(path, reason) => {
if let Some(local) = (!this.ignore_breakpoints)
.then(|| this.as_running_mut())
.then(|| this.as_local_mut())
.flatten()
{
local
@@ -727,7 +714,7 @@ impl Session {
}
BreakpointStoreEvent::BreakpointsCleared(paths) => {
if let Some(local) = (!this.ignore_breakpoints)
.then(|| this.as_running_mut())
.then(|| this.as_local_mut())
.flatten()
{
local.unset_breakpoints_from_paths(paths, cx).detach();
@@ -819,7 +806,7 @@ impl Session {
let parent_session = self.parent_session.clone();
cx.spawn(async move |this, cx| {
let mode = RunningMode::new(
let mode = LocalMode::new(
id,
parent_session,
worktree.downgrade(),
@@ -919,29 +906,18 @@ impl Session {
return tx;
}
pub fn is_started(&self) -> bool {
match &self.mode {
Mode::Building => false,
Mode::Running(running) => running.is_started,
}
}
pub fn is_building(&self) -> bool {
matches!(self.mode, Mode::Building)
}
pub fn is_running(&self) -> bool {
pub fn is_local(&self) -> bool {
matches!(self.mode, Mode::Running(_))
}
pub fn as_running_mut(&mut self) -> Option<&mut RunningMode> {
pub fn as_local_mut(&mut self) -> Option<&mut LocalMode> {
match &mut self.mode {
Mode::Running(local_mode) => Some(local_mode),
Mode::Building => None,
}
}
pub fn as_running(&self) -> Option<&RunningMode> {
pub fn as_local(&self) -> Option<&LocalMode> {
match &self.mode {
Mode::Running(local_mode) => Some(local_mode),
Mode::Building => None,
@@ -1164,7 +1140,7 @@ impl Session {
body: Option<serde_json::Value>,
cx: &mut Context<Self>,
) -> Task<Result<()>> {
let Some(local_session) = self.as_running() else {
let Some(local_session) = self.as_local() else {
unreachable!("Cannot respond to remote client");
};
let client = local_session.client.clone();
@@ -1186,7 +1162,7 @@ impl Session {
fn handle_stopped_event(&mut self, event: StoppedEvent, cx: &mut Context<Self>) {
// todo(debugger): Find a clean way to get around the clone
let breakpoint_store = self.breakpoint_store.clone();
if let Some((local, path)) = self.as_running_mut().and_then(|local| {
if let Some((local, path)) = self.as_local_mut().and_then(|local| {
let breakpoint = local.tmp_breakpoint.take()?;
let path = breakpoint.path.clone();
Some((local, path))
@@ -1552,7 +1528,7 @@ impl Session {
self.ignore_breakpoints = ignore;
if let Some(local) = self.as_running() {
if let Some(local) = self.as_local() {
local.send_source_breakpoints(ignore, &self.breakpoint_store, cx)
} else {
// todo(debugger): We need to propagate this change to downstream sessions and send a message to upstream sessions
@@ -1574,7 +1550,7 @@ impl Session {
}
fn send_exception_breakpoints(&mut self, cx: &App) {
if let Some(local) = self.as_running() {
if let Some(local) = self.as_local() {
let exception_filters = self
.exception_breakpoints
.values()

View File

@@ -384,6 +384,7 @@ impl ShellBuilder {
/// Returns the program and arguments to run this task in a shell.
pub fn build(mut self, task_command: String, task_args: &Vec<String>) -> (String, Vec<String>) {
let task_command = format!("\"{task_command}\"");
let combined_command = task_args
.into_iter()
.fold(task_command, |mut command, arg| {

View File

@@ -237,18 +237,6 @@ impl TaskTemplate {
env
};
// We filter out env variables here that aren't set so we don't have extra white space in args
let args = self
.args
.iter()
.filter(|arg| {
arg.starts_with('$')
.then(|| env.get(&arg[1..]).is_some_and(|arg| !arg.trim().is_empty()))
.unwrap_or(true)
})
.cloned()
.collect();
Some(ResolvedTask {
id: id.clone(),
substituted_variables,
@@ -268,7 +256,7 @@ impl TaskTemplate {
},
),
command,
args,
args: self.args.clone(),
env,
use_new_terminal: self.use_new_terminal,
allow_concurrent_runs: self.allow_concurrent_runs,
@@ -715,7 +703,6 @@ mod tests {
label: "My task".into(),
command: "echo".into(),
args: vec!["$PATH".into()],
env: HashMap::from_iter([("PATH".to_owned(), "non-empty".to_owned())]),
..TaskTemplate::default()
};
let resolved_task = task
@@ -728,32 +715,6 @@ mod tests {
assert_eq!(resolved.args, task.args);
}
#[test]
fn test_empty_env_variables_excluded_from_args() {
let task = TaskTemplate {
label: "My task".into(),
command: "echo".into(),
args: vec![
"$EMPTY_VAR".into(),
"hello".into(),
"$WHITESPACE_VAR".into(),
"$UNDEFINED_VAR".into(),
"$WORLD".into(),
],
env: HashMap::from_iter([
("EMPTY_VAR".to_owned(), "".to_owned()),
("WHITESPACE_VAR".to_owned(), " ".to_owned()),
("WORLD".to_owned(), "non-empty".to_owned()),
]),
..TaskTemplate::default()
};
let resolved_task = task
.resolve_task(TEST_ID_BASE, &TaskContext::default())
.unwrap();
let resolved = resolved_task.resolved;
assert_eq!(resolved.args, vec!["hello", "$WORLD"]);
}
#[test]
fn test_errors_on_missing_zed_variable() {
let task = TaskTemplate {

View File

@@ -16,7 +16,7 @@ fn init_logger() {
#[test]
fn test_edit() {
let mut buffer = Buffer::new(0, BufferId::new(1).unwrap(), "abc");
let mut buffer = Buffer::new(0, BufferId::new(1).unwrap(), "abc".into());
assert_eq!(buffer.text(), "abc");
buffer.edit([(3..3, "def")]);
assert_eq!(buffer.text(), "abcdef");
@@ -175,7 +175,7 @@ fn test_line_endings() {
LineEnding::Windows
);
let mut buffer = Buffer::new(0, BufferId::new(1).unwrap(), "one\r\ntwo\rthree");
let mut buffer = Buffer::new(0, BufferId::new(1).unwrap(), "one\r\ntwo\rthree".into());
assert_eq!(buffer.text(), "one\ntwo\nthree");
assert_eq!(buffer.line_ending(), LineEnding::Windows);
buffer.check_invariants();
@@ -189,7 +189,7 @@ fn test_line_endings() {
#[test]
fn test_line_len() {
let mut buffer = Buffer::new(0, BufferId::new(1).unwrap(), "");
let mut buffer = Buffer::new(0, BufferId::new(1).unwrap(), "".into());
buffer.edit([(0..0, "abcd\nefg\nhij")]);
buffer.edit([(12..12, "kl\nmno")]);
buffer.edit([(18..18, "\npqrs\n")]);
@@ -206,7 +206,7 @@ fn test_line_len() {
#[test]
fn test_common_prefix_at_position() {
let text = "a = str; b = δα";
let buffer = Buffer::new(0, BufferId::new(1).unwrap(), text);
let buffer = Buffer::new(0, BufferId::new(1).unwrap(), text.into());
let offset1 = offset_after(text, "str");
let offset2 = offset_after(text, "δα");
@@ -257,7 +257,7 @@ fn test_text_summary_for_range() {
let buffer = Buffer::new(
0,
BufferId::new(1).unwrap(),
"ab\nefg\nhklm\nnopqrs\ntuvwxyz",
"ab\nefg\nhklm\nnopqrs\ntuvwxyz".into(),
);
assert_eq!(
buffer.text_summary_for_range::<TextSummary, _>(0..2),
@@ -347,7 +347,7 @@ fn test_text_summary_for_range() {
#[test]
fn test_chars_at() {
let mut buffer = Buffer::new(0, BufferId::new(1).unwrap(), "");
let mut buffer = Buffer::new(0, BufferId::new(1).unwrap(), "".into());
buffer.edit([(0..0, "abcd\nefgh\nij")]);
buffer.edit([(12..12, "kl\nmno")]);
buffer.edit([(18..18, "\npqrs")]);
@@ -369,7 +369,7 @@ fn test_chars_at() {
assert_eq!(chars.collect::<String>(), "PQrs");
// Regression test:
let mut buffer = Buffer::new(0, BufferId::new(1).unwrap(), "");
let mut buffer = Buffer::new(0, BufferId::new(1).unwrap(), "".into());
buffer.edit([(0..0, "[workspace]\nmembers = [\n \"xray_core\",\n \"xray_server\",\n \"xray_cli\",\n \"xray_wasm\",\n]\n")]);
buffer.edit([(60..60, "\n")]);
@@ -379,7 +379,7 @@ fn test_chars_at() {
#[test]
fn test_anchors() {
let mut buffer = Buffer::new(0, BufferId::new(1).unwrap(), "");
let mut buffer = Buffer::new(0, BufferId::new(1).unwrap(), "".into());
buffer.edit([(0..0, "abc")]);
let left_anchor = buffer.anchor_before(2);
let right_anchor = buffer.anchor_after(2);
@@ -497,7 +497,7 @@ fn test_anchors() {
#[test]
fn test_anchors_at_start_and_end() {
let mut buffer = Buffer::new(0, BufferId::new(1).unwrap(), "");
let mut buffer = Buffer::new(0, BufferId::new(1).unwrap(), "".into());
let before_start_anchor = buffer.anchor_before(0);
let after_end_anchor = buffer.anchor_after(0);
@@ -520,7 +520,7 @@ fn test_anchors_at_start_and_end() {
#[test]
fn test_undo_redo() {
let mut buffer = Buffer::new(0, BufferId::new(1).unwrap(), "1234");
let mut buffer = Buffer::new(0, BufferId::new(1).unwrap(), "1234".into());
// Set group interval to zero so as to not group edits in the undo stack.
buffer.set_group_interval(Duration::from_secs(0));
@@ -557,7 +557,7 @@ fn test_undo_redo() {
#[test]
fn test_history() {
let mut now = Instant::now();
let mut buffer = Buffer::new(0, BufferId::new(1).unwrap(), "123456");
let mut buffer = Buffer::new(0, BufferId::new(1).unwrap(), "123456".into());
buffer.set_group_interval(Duration::from_millis(300));
let transaction_1 = buffer.start_transaction_at(now).unwrap();
@@ -624,7 +624,7 @@ fn test_history() {
#[test]
fn test_finalize_last_transaction() {
let now = Instant::now();
let mut buffer = Buffer::new(0, BufferId::new(1).unwrap(), "123456");
let mut buffer = Buffer::new(0, BufferId::new(1).unwrap(), "123456".into());
buffer.history.group_interval = Duration::from_millis(1);
buffer.start_transaction_at(now);
@@ -660,7 +660,7 @@ fn test_finalize_last_transaction() {
#[test]
fn test_edited_ranges_for_transaction() {
let now = Instant::now();
let mut buffer = Buffer::new(0, BufferId::new(1).unwrap(), "1234567");
let mut buffer = Buffer::new(0, BufferId::new(1).unwrap(), "1234567".into());
buffer.start_transaction_at(now);
buffer.edit([(2..4, "cd")]);
@@ -699,9 +699,9 @@ fn test_edited_ranges_for_transaction() {
fn test_concurrent_edits() {
let text = "abcdef";
let mut buffer1 = Buffer::new(1, BufferId::new(1).unwrap(), text);
let mut buffer2 = Buffer::new(2, BufferId::new(1).unwrap(), text);
let mut buffer3 = Buffer::new(3, BufferId::new(1).unwrap(), text);
let mut buffer1 = Buffer::new(1, BufferId::new(1).unwrap(), text.into());
let mut buffer2 = Buffer::new(2, BufferId::new(1).unwrap(), text.into());
let mut buffer3 = Buffer::new(3, BufferId::new(1).unwrap(), text.into());
let buf1_op = buffer1.edit([(1..2, "12")]);
assert_eq!(buffer1.text(), "a12cdef");

View File

@@ -677,8 +677,7 @@ impl FromIterator<char> for LineIndent {
}
impl Buffer {
pub fn new(replica_id: u16, remote_id: BufferId, base_text: impl Into<String>) -> Buffer {
let mut base_text = base_text.into();
pub fn new(replica_id: u16, remote_id: BufferId, mut base_text: String) -> Buffer {
let line_ending = LineEnding::detect(&base_text);
LineEnding::normalize(&mut base_text);
Self::new_normalized(replica_id, remote_id, line_ending, Rope::from(base_text))

View File

@@ -145,14 +145,10 @@ impl ScrollbarState {
const MINIMUM_THUMB_SIZE: Pixels = px(25.);
let content_size = self.scroll_handle.content_size().along(axis);
let viewport_size = self.scroll_handle.viewport().size.along(axis);
if content_size.is_zero() || viewport_size.is_zero() || content_size <= viewport_size {
return None;
}
let visible_percentage = viewport_size / content_size;
let thumb_size = MINIMUM_THUMB_SIZE.max(viewport_size * visible_percentage);
if thumb_size > viewport_size {
if content_size.is_zero() || viewport_size.is_zero() || content_size < viewport_size {
return None;
}
let max_offset = content_size - viewport_size;
let current_offset = self
.scroll_handle
@@ -160,6 +156,12 @@ impl ScrollbarState {
.along(axis)
.clamp(-max_offset, Pixels::ZERO)
.abs();
let visible_percentage = viewport_size / content_size;
let thumb_size = MINIMUM_THUMB_SIZE.max(viewport_size * visible_percentage);
if thumb_size > viewport_size {
return None;
}
let start_offset = (current_offset / max_offset) * (viewport_size - thumb_size);
let thumb_percentage_start = start_offset / viewport_size;
let thumb_percentage_end = (start_offset + thumb_size) / viewport_size;

View File

@@ -37,6 +37,8 @@ smol.workspace = true
take-until.workspace = true
tempfile.workspace = true
unicase.workspace = true
unicode-script.workspace = true
unicode-segmentation.workspace = true
util_macros = { workspace = true, optional = true }
walkdir.workspace = true
workspace-hack.workspace = true

View File

@@ -14,6 +14,7 @@ use anyhow::Result;
use futures::Future;
use itertools::Either;
use regex::Regex;
use std::num::NonZeroU32;
use std::sync::{LazyLock, OnceLock};
use std::{
borrow::Cow,
@@ -183,29 +184,208 @@ pub fn truncate_lines_to_byte_limit(s: &str, max_bytes: usize) -> &str {
truncate_to_byte_limit(s, max_bytes)
}
#[test]
fn test_truncate_lines_to_byte_limit() {
let text = "Line 1\nLine 2\nLine 3\nLine 4";
fn char_len_with_expanded_tabs(offset: usize, text: &str, tab_size: NonZeroU32) -> usize {
let tab_size = tab_size.get() as usize;
let mut width = offset;
// Limit that includes all lines
assert_eq!(truncate_lines_to_byte_limit(text, 100), text);
for ch in text.chars() {
width += if ch == '\t' {
tab_size - (width % tab_size)
} else {
1
};
}
// Exactly the first line
assert_eq!(truncate_lines_to_byte_limit(text, 7), "Line 1\n");
width - offset
}
// Limit between lines
assert_eq!(truncate_lines_to_byte_limit(text, 13), "Line 1\n");
assert_eq!(truncate_lines_to_byte_limit(text, 20), "Line 1\nLine 2\n");
/// Tokenizes a string into runs of text that should stick together, or that is whitespace.
struct WordBreakingTokenizer<'a> {
input: &'a str,
}
// Limit before first newline
assert_eq!(truncate_lines_to_byte_limit(text, 6), "Line ");
impl<'a> WordBreakingTokenizer<'a> {
fn new(input: &'a str) -> Self {
Self { input }
}
}
// Test with non-ASCII characters
let text_utf8 = "Line 1\nLíne 2\nLine 3";
assert_eq!(
truncate_lines_to_byte_limit(text_utf8, 15),
"Line 1\nLíne 2\n"
);
fn is_char_ideographic(ch: char) -> bool {
use unicode_script::Script::*;
use unicode_script::UnicodeScript;
matches!(ch.script(), Han | Tangut | Yi)
}
fn is_grapheme_ideographic(text: &str) -> bool {
text.chars().any(is_char_ideographic)
}
fn is_grapheme_whitespace(text: &str) -> bool {
text.chars().any(|x| x.is_whitespace())
}
fn should_stay_with_preceding_ideograph(text: &str) -> bool {
text.chars().next().map_or(false, |ch| {
matches!(ch, '。' | '、' | '' | '' | '' | '' | '' | '…')
})
}
#[derive(PartialEq, Eq, Debug, Clone, Copy)]
enum WordBreakToken<'a> {
Word { token: &'a str, grapheme_len: usize },
InlineWhitespace { token: &'a str, grapheme_len: usize },
Newline,
}
impl<'a> Iterator for WordBreakingTokenizer<'a> {
/// Yields a span, the count of graphemes in the token, and whether it was
/// whitespace. Note that it also breaks at word boundaries.
type Item = WordBreakToken<'a>;
fn next(&mut self) -> Option<Self::Item> {
use unicode_segmentation::UnicodeSegmentation;
if self.input.is_empty() {
return None;
}
let mut iter = self.input.graphemes(true).peekable();
let mut offset = 0;
let mut grapheme_len = 0;
if let Some(first_grapheme) = iter.next() {
let is_newline = first_grapheme == "\n";
let is_whitespace = is_grapheme_whitespace(first_grapheme);
offset += first_grapheme.len();
grapheme_len += 1;
if is_grapheme_ideographic(first_grapheme) && !is_whitespace {
if let Some(grapheme) = iter.peek().copied() {
if should_stay_with_preceding_ideograph(grapheme) {
offset += grapheme.len();
grapheme_len += 1;
}
}
} else {
let mut words = self.input[offset..].split_word_bound_indices().peekable();
let mut next_word_bound = words.peek().copied();
if next_word_bound.map_or(false, |(i, _)| i == 0) {
next_word_bound = words.next();
}
while let Some(grapheme) = iter.peek().copied() {
if next_word_bound.map_or(false, |(i, _)| i == offset) {
break;
};
if is_grapheme_whitespace(grapheme) != is_whitespace
|| (grapheme == "\n") != is_newline
{
break;
};
offset += grapheme.len();
grapheme_len += 1;
iter.next();
}
}
let token = &self.input[..offset];
self.input = &self.input[offset..];
if token == "\n" {
Some(WordBreakToken::Newline)
} else if is_whitespace {
Some(WordBreakToken::InlineWhitespace {
token,
grapheme_len,
})
} else {
Some(WordBreakToken::Word {
token,
grapheme_len,
})
}
} else {
None
}
}
}
pub fn wrap_with_prefix(
line_prefix: String,
unwrapped_text: String,
wrap_column: usize,
tab_size: NonZeroU32,
preserve_existing_whitespace: bool,
) -> String {
let line_prefix_len = char_len_with_expanded_tabs(0, &line_prefix, tab_size);
let mut wrapped_text = String::new();
let mut current_line = line_prefix.clone();
let tokenizer = WordBreakingTokenizer::new(&unwrapped_text);
let mut current_line_len = line_prefix_len;
let mut in_whitespace = false;
for token in tokenizer {
let have_preceding_whitespace = in_whitespace;
match token {
WordBreakToken::Word {
token,
grapheme_len,
} => {
in_whitespace = false;
if current_line_len + grapheme_len > wrap_column
&& current_line_len != line_prefix_len
{
wrapped_text.push_str(current_line.trim_end());
wrapped_text.push('\n');
current_line.truncate(line_prefix.len());
current_line_len = line_prefix_len;
}
current_line.push_str(token);
current_line_len += grapheme_len;
}
WordBreakToken::InlineWhitespace {
mut token,
mut grapheme_len,
} => {
in_whitespace = true;
if have_preceding_whitespace && !preserve_existing_whitespace {
continue;
}
if !preserve_existing_whitespace {
token = " ";
grapheme_len = 1;
}
if current_line_len + grapheme_len > wrap_column {
wrapped_text.push_str(current_line.trim_end());
wrapped_text.push('\n');
current_line.truncate(line_prefix.len());
current_line_len = line_prefix_len;
} else if current_line_len != line_prefix_len || preserve_existing_whitespace {
current_line.push_str(token);
current_line_len += grapheme_len;
}
}
WordBreakToken::Newline => {
in_whitespace = true;
if preserve_existing_whitespace {
wrapped_text.push_str(current_line.trim_end());
wrapped_text.push('\n');
current_line.truncate(line_prefix.len());
current_line_len = line_prefix_len;
} else if have_preceding_whitespace {
continue;
} else if current_line_len + 1 > wrap_column && current_line_len != line_prefix_len
{
wrapped_text.push_str(current_line.trim_end());
wrapped_text.push('\n');
current_line.truncate(line_prefix.len());
current_line_len = line_prefix_len;
} else if current_line_len != line_prefix_len {
current_line.push(' ');
current_line_len += 1;
}
}
}
}
if !current_line.is_empty() {
wrapped_text.push_str(&current_line);
}
wrapped_text
}
pub fn post_inc<T: From<u8> + AddAssign<T> + Copy>(value: &mut T) -> T {
@@ -1401,6 +1581,163 @@ Line 3"#
);
}
#[test]
fn test_truncate_lines_to_byte_limit() {
let text = "Line 1\nLine 2\nLine 3\nLine 4";
// Limit that includes all lines
assert_eq!(truncate_lines_to_byte_limit(text, 100), text);
// Exactly the first line
assert_eq!(truncate_lines_to_byte_limit(text, 7), "Line 1\n");
// Limit between lines
assert_eq!(truncate_lines_to_byte_limit(text, 13), "Line 1\n");
assert_eq!(truncate_lines_to_byte_limit(text, 20), "Line 1\nLine 2\n");
// Limit before first newline
assert_eq!(truncate_lines_to_byte_limit(text, 6), "Line ");
// Test with non-ASCII characters
let text_utf8 = "Line 1\nLíne 2\nLine 3";
assert_eq!(
truncate_lines_to_byte_limit(text_utf8, 15),
"Line 1\nLíne 2\n"
);
}
#[test]
fn test_string_size_with_expanded_tabs() {
let nz = |val| NonZeroU32::new(val).unwrap();
assert_eq!(char_len_with_expanded_tabs(0, "", nz(4)), 0);
assert_eq!(char_len_with_expanded_tabs(0, "hello", nz(4)), 5);
assert_eq!(char_len_with_expanded_tabs(0, "\thello", nz(4)), 9);
assert_eq!(char_len_with_expanded_tabs(0, "abc\tab", nz(4)), 6);
assert_eq!(char_len_with_expanded_tabs(0, "hello\t", nz(4)), 8);
assert_eq!(char_len_with_expanded_tabs(0, "\t\t", nz(8)), 16);
assert_eq!(char_len_with_expanded_tabs(0, "x\t", nz(8)), 8);
assert_eq!(char_len_with_expanded_tabs(7, "x\t", nz(8)), 9);
}
#[test]
fn test_word_breaking_tokenizer() {
let tests: &[(&str, &[WordBreakToken<'static>])] = &[
("", &[]),
(" ", &[whitespace(" ", 2)]),
("Ʒ", &[word("Ʒ", 1)]),
("Ǽ", &[word("Ǽ", 1)]),
("", &[word("", 1)]),
("⋑⋑", &[word("⋑⋑", 2)]),
(
"原理,进而",
&[word("", 1), word("理,", 2), word("", 1), word("", 1)],
),
(
"hello world",
&[word("hello", 5), whitespace(" ", 1), word("world", 5)],
),
(
"hello, world",
&[word("hello,", 6), whitespace(" ", 1), word("world", 5)],
),
(
" hello world",
&[
whitespace(" ", 2),
word("hello", 5),
whitespace(" ", 1),
word("world", 5),
],
),
(
"这是什么 \n 钢笔",
&[
word("", 1),
word("", 1),
word("", 1),
word("", 1),
whitespace(" ", 1),
newline(),
whitespace(" ", 1),
word("", 1),
word("", 1),
],
),
("mutton", &[whitespace("", 1), word("mutton", 6)]),
];
fn word(token: &'static str, grapheme_len: usize) -> WordBreakToken<'static> {
WordBreakToken::Word {
token,
grapheme_len,
}
}
fn whitespace(token: &'static str, grapheme_len: usize) -> WordBreakToken<'static> {
WordBreakToken::InlineWhitespace {
token,
grapheme_len,
}
}
fn newline() -> WordBreakToken<'static> {
WordBreakToken::Newline
}
for (input, result) in tests {
assert_eq!(
WordBreakingTokenizer::new(input)
.collect::<Vec<_>>()
.as_slice(),
*result,
);
}
}
#[test]
fn test_wrap_with_prefix() {
assert_eq!(
wrap_with_prefix(
"# ".to_string(),
"abcdefg".to_string(),
4,
NonZeroU32::new(4).unwrap(),
false,
),
"# abcdefg"
);
assert_eq!(
wrap_with_prefix(
"".to_string(),
"\thello world".to_string(),
8,
NonZeroU32::new(4).unwrap(),
false,
),
"hello\nworld"
);
assert_eq!(
wrap_with_prefix(
"// ".to_string(),
"xx \nyy zz aa bb cc".to_string(),
12,
NonZeroU32::new(4).unwrap(),
false,
),
"// xx yy zz\n// aa bb cc"
);
assert_eq!(
wrap_with_prefix(
String::new(),
"这是什么 \n 钢笔".to_string(),
3,
NonZeroU32::new(4).unwrap(),
false,
),
"这是什\n么 钢\n"
);
}
#[test]
fn test_split_with_ranges() {
let input = "hi";

View File

@@ -1701,9 +1701,7 @@ fn previous_word_end(
let mut point = point.to_point(map);
if point.column < map.buffer_snapshot.line_len(MultiBufferRow(point.row)) {
if let Some(ch) = map.buffer_snapshot.chars_at(point).next() {
point.column += ch.len_utf8() as u32;
}
point.column += 1;
}
for _ in 0..times {
let new_point = movement::find_preceding_boundary_point(
@@ -1876,9 +1874,7 @@ fn previous_subword_end(
let mut point = point.to_point(map);
if point.column < map.buffer_snapshot.line_len(MultiBufferRow(point.row)) {
if let Some(ch) = map.buffer_snapshot.chars_at(point).next() {
point.column += ch.len_utf8() as u32;
}
point.column += 1;
}
for _ in 0..times {
let new_point = movement::find_preceding_boundary_point(
@@ -3617,16 +3613,6 @@ mod test {
4;5.6 567 678
789 890 901
"});
// With multi byte char
cx.set_shared_state(indoc! {r"
bar ˇó
"})
.await;
cx.simulate_shared_keystrokes("g e").await;
cx.shared_state().await.assert_eq(indoc! {"
baˇr ó
"});
}
#[gpui::test]

View File

@@ -27,7 +27,3 @@
{"Key":"g"}
{"Key":"shift-e"}
{"Get":{"state":"123 234 34ˇ5\n4;5.6 567 678\n789 890 901\n","mode":"Normal"}}
{"Put":{"state":"bar ˇó\n"}}
{"Key":"g"}
{"Key":"e"}
{"Get":{"state":"baˇr ó\n","mode":"Normal"}}

View File

@@ -2,7 +2,7 @@
description = "The fast, collaborative code editor."
edition.workspace = true
name = "zed"
version = "0.189.1"
version = "0.189.0"
publish.workspace = true
license = "GPL-3.0-or-later"
authors = ["Zed Team <hi@zed.dev>"]

View File

@@ -1 +1 @@
preview
dev

View File

@@ -24,14 +24,11 @@ Non-Max Mode usage will use up to 25 tool calls per one prompt. If your prompt e
In Max Mode, we enable models to use [large context windows](#context-windows), unlimited tool calls, and other capabilities for expanded reasoning, to allow an unfettered agentic experience.
Because of the increased cost to Zed, each subsequent request beyond the initial user prompt in Max Mode models is counted as a prompt for metering.
In addition, usage-based pricing per request is slightly more expensive for Max Mode models than usage-based pricing per prompt for regular models.
> Note that the Agent Panel using a Max Mode model may consume a good bit of your monthly prompt capacity, if many tool calls are used.
> We encourage you to think through what model is best for your needs before leaving the Agent Panel to work.
> Note that the Agent Panel using a Max Mode model may consume a good bit of your monthly prompt capacity, if many tool calls are used. We encourage you to think through what model is best for your needs before leaving the Agent Panel to work.
By default, all threads and [text threads](./text-threads.md) start in normal mode.
However, you can use the `agent.preferred_completion_mode` setting to have Max Mode activated by default.
By default, all Agent threads start in normal mode, however you can use the agent setting `preferred_completion_mode` to start new Agent threads in Max Mode.
## Context Windows {#context-windows}
@@ -39,7 +36,7 @@ A context window is the maximum span of text and code an LLM can consider at onc
In [Max Mode](#max-mode), we increase context window size to allow models to have enhanced reasoning capabilities.
Each Agent thread and text thread in Zed maintains its own context window.
Each Agent thread in Zed maintains its own context window.
The more prompts, attached files, and responses included in a session, the larger the context window grows.
For best results, its recommended you take a purpose-based approach to Agent thread management, starting a new thread for each unique task.