Compare commits

..

1 Commits

Author SHA1 Message Date
Peter Tripp
f855eb7470 ci: Switch Linux arm from buildjet to github 2025-06-07 15:01:30 -04:00
89 changed files with 893 additions and 2616 deletions

View File

@@ -183,9 +183,6 @@ jobs:
- name: Check for todo! and FIXME comments
run: script/check-todos
- name: Check modifier use in keymaps
run: script/check-keymaps
- name: Run style checks
uses: ./.github/actions/check_style
@@ -686,7 +683,7 @@ jobs:
timeout-minutes: 60
name: Linux arm64 release bundle
runs-on:
- buildjet-16vcpu-ubuntu-2204-arm
- hosted-linux-arm-1
if: |
startsWith(github.ref, 'refs/tags/v')
|| contains(github.event.pull_request.labels.*.name, 'run-bundling')

1
Cargo.lock generated
View File

@@ -99,7 +99,6 @@ dependencies = [
"paths",
"picker",
"postage",
"pretty_assertions",
"project",
"prompt_store",
"proto",

View File

@@ -35,7 +35,7 @@
"ctrl-shift-f5": "debugger::Restart",
"f6": "debugger::Pause",
"f7": "debugger::StepOver",
"ctrl-f11": "debugger::StepInto",
"cmd-f11": "debugger::StepInto",
"shift-f11": "debugger::StepOut",
"f11": "zed::ToggleFullScreen",
"ctrl-alt-z": "edit_prediction::RateCompletions",
@@ -59,6 +59,7 @@
"tab": "editor::Tab",
"shift-tab": "editor::Backtab",
"ctrl-k": "editor::CutToEndOfLine",
// "ctrl-t": "editor::Transpose",
"ctrl-k ctrl-q": "editor::Rewrap",
"ctrl-k q": "editor::Rewrap",
"ctrl-backspace": "editor::DeleteToPreviousWordStart",
@@ -99,16 +100,21 @@
"shift-down": "editor::SelectDown",
"shift-left": "editor::SelectLeft",
"shift-right": "editor::SelectRight",
"ctrl-shift-left": "editor::SelectToPreviousWordStart",
"ctrl-shift-right": "editor::SelectToNextWordEnd",
"ctrl-shift-left": "editor::SelectToPreviousWordStart", // cursorWordLeftSelect
"ctrl-shift-right": "editor::SelectToNextWordEnd", // cursorWordRightSelect
"ctrl-shift-home": "editor::SelectToBeginning",
"ctrl-shift-end": "editor::SelectToEnd",
"ctrl-a": "editor::SelectAll",
"ctrl-l": "editor::SelectLine",
"ctrl-shift-i": "editor::Format",
"alt-shift-o": "editor::OrganizeImports",
// "cmd-shift-left": ["editor::SelectToBeginningOfLine", {"stop_at_soft_wraps": true, "stop_at_indent": true }],
// "ctrl-shift-a": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": true, "stop_at_indent": true }],
"shift-home": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": true, "stop_at_indent": true }],
// "cmd-shift-right": ["editor::SelectToEndOfLine", { "stop_at_soft_wraps": true }],
// "ctrl-shift-e": ["editor::SelectToEndOfLine", { "stop_at_soft_wraps": true }],
"shift-end": ["editor::SelectToEndOfLine", { "stop_at_soft_wraps": true }],
// "alt-v": ["editor::MovePageUp", { "center_cursor": true }],
"ctrl-alt-space": "editor::ShowCharacterPalette",
"ctrl-;": "editor::ToggleLineNumbers",
"ctrl-'": "editor::ToggleSelectedDiffHunks",
@@ -134,6 +140,7 @@
"find": "buffer_search::Deploy",
"ctrl-f": "buffer_search::Deploy",
"ctrl-h": "buffer_search::DeployReplace",
// "cmd-e": ["buffer_search::Deploy", { "focus": false }],
"ctrl->": "assistant::QuoteSelection",
"ctrl-<": "assistant::InsertIntoEditor",
"ctrl-alt-e": "editor::SelectEnclosingSymbol",
@@ -260,8 +267,8 @@
{
"context": "AgentPanel && prompt_editor",
"bindings": {
"ctrl-n": "agent::NewTextThread",
"ctrl-alt-t": "agent::NewThread"
"cmd-n": "agent::NewTextThread",
"cmd-alt-t": "agent::NewThread"
}
},
{

View File

@@ -38,7 +38,7 @@
"ctrl-shift-d": "editor::DuplicateSelection",
"alt-f3": "editor::SelectAllMatches", // find_all_under
// "ctrl-f3": "", // find_under (cancels any selections)
// "ctrl-alt-shift-g": "" // find_under_prev (cancels any selections)
// "cmd-alt-shift-g": "" // find_under_prev (cancels any selections)
"f9": "editor::SortLinesCaseSensitive",
"ctrl-f9": "editor::SortLinesCaseInsensitive",
"f12": "editor::GoToDefinition",

View File

@@ -28,8 +28,7 @@
"context": "InlineAssistEditor",
"use_key_equivalents": true,
"bindings": {
"cmd-shift-backspace": "editor::Cancel",
"cmd-enter": "menu::Confirm"
"cmd-shift-backspace": "editor::Cancel"
// "alt-enter": // Quick Question
// "cmd-shift-enter": // Full File Context
// "cmd-shift-k": // Toggle input focus (editor <> inline assist)

View File

@@ -101,12 +101,9 @@
// The second option is decimal.
"unit": "binary"
},
// Determines the modifier to be used to add multiple cursors with the mouse. The open hover link mouse gestures will adapt such that it do not conflict with the multicursor modifier.
//
// 1. Maps to `Alt` on Linux and Windows and to `Option` on MacOS:
// "alt"
// 2. Maps `Control` on Linux and Windows and to `Command` on MacOS:
// "cmd_or_ctrl" (alias: "cmd", "ctrl")
// The key to use for adding multiple cursors
// Currently "alt" or "cmd_or_ctrl" (also aliased as
// "cmd" and "ctrl") are supported.
"multi_cursor_modifier": "alt",
// Whether to enable vim modes and key bindings.
"vim_mode": false,
@@ -217,8 +214,6 @@
"show_signature_help_after_edits": false,
// Whether to show code action button at start of buffer line.
"inline_code_actions": true,
// Whether to allow drag and drop text selection in buffer.
"drag_and_drop_selection": true,
// What to do when go to definition yields no results.
//
// 1. Do nothing: `none`
@@ -604,9 +599,7 @@
// 2. Never show indent guides:
// "never"
"show": "always"
},
// Whether to hide the root entry when only one folder is open in the window.
"hide_root": false
}
},
"outline_panel": {
// Whether to show the outline panel button in the status bar

View File

@@ -109,6 +109,5 @@ gpui = { workspace = true, "features" = ["test-support"] }
indoc.workspace = true
language = { workspace = true, "features" = ["test-support"] }
language_model = { workspace = true, "features" = ["test-support"] }
pretty_assertions.workspace = true
project = { workspace = true, features = ["test-support"] }
rand.workspace = true

View File

@@ -57,7 +57,7 @@ use zed_llm_client::{CompletionIntent, UsageLimit};
use crate::active_thread::{self, ActiveThread, ActiveThreadEvent};
use crate::agent_configuration::{AgentConfiguration, AssistantConfigurationEvent};
use crate::agent_diff::AgentDiff;
use crate::history_store::{HistoryEntryId, HistoryStore};
use crate::history_store::{HistoryStore, RecentEntry};
use crate::message_editor::{MessageEditor, MessageEditorEvent};
use crate::thread::{Thread, ThreadError, ThreadId, ThreadSummary, TokenUsageRatio};
use crate::thread_history::{HistoryEntryElement, ThreadHistory};
@@ -257,7 +257,6 @@ impl ActiveView {
pub fn prompt_editor(
context_editor: Entity<ContextEditor>,
history_store: Entity<HistoryStore>,
language_registry: Arc<LanguageRegistry>,
window: &mut Window,
cx: &mut App,
@@ -323,19 +322,6 @@ impl ActiveView {
editor.set_text(summary, window, cx);
})
}
ContextEvent::PathChanged { old_path, new_path } => {
history_store.update(cx, |history_store, cx| {
if let Some(old_path) = old_path {
history_store
.replace_recently_opened_text_thread(old_path, new_path, cx);
} else {
history_store.push_recently_opened_entry(
HistoryEntryId::Context(new_path.clone()),
cx,
);
}
});
}
_ => {}
}
}),
@@ -530,7 +516,8 @@ impl AgentPanel {
HistoryStore::new(
thread_store.clone(),
context_store.clone(),
[HistoryEntryId::Thread(thread_id)],
[RecentEntry::Thread(thread_id, thread.clone())],
window,
cx,
)
});
@@ -557,13 +544,7 @@ impl AgentPanel {
editor.insert_default_prompt(window, cx);
editor
});
ActiveView::prompt_editor(
context_editor,
history_store.clone(),
language_registry.clone(),
window,
cx,
)
ActiveView::prompt_editor(context_editor, language_registry.clone(), window, cx)
}
};
@@ -600,9 +581,86 @@ impl AgentPanel {
let panel = weak_panel.clone();
let assistant_navigation_menu =
ContextMenu::build_persistent(window, cx, move |mut menu, _window, cx| {
if let Some(panel) = panel.upgrade() {
menu = Self::populate_recently_opened_menu_section(menu, panel, cx);
let recently_opened = panel
.update(cx, |this, cx| {
this.history_store.update(cx, |history_store, cx| {
history_store.recently_opened_entries(cx)
})
})
.unwrap_or_default();
if !recently_opened.is_empty() {
menu = menu.header("Recently Opened");
for entry in recently_opened.iter() {
if let RecentEntry::Context(context) = entry {
if context.read(cx).path().is_none() {
log::error!(
"bug: text thread in recent history list was never saved"
);
continue;
}
}
let summary = entry.summary(cx);
menu = menu.entry_with_end_slot_on_hover(
summary,
None,
{
let panel = panel.clone();
let entry = entry.clone();
move |window, cx| {
panel
.update(cx, {
let entry = entry.clone();
move |this, cx| match entry {
RecentEntry::Thread(_, thread) => {
this.open_thread(thread, window, cx)
}
RecentEntry::Context(context) => {
let Some(path) = context.read(cx).path()
else {
return;
};
this.open_saved_prompt_editor(
path.clone(),
window,
cx,
)
.detach_and_log_err(cx)
}
}
})
.ok();
}
},
IconName::Close,
"Close Entry".into(),
{
let panel = panel.clone();
let entry = entry.clone();
move |_window, cx| {
panel
.update(cx, |this, cx| {
this.history_store.update(
cx,
|history_store, cx| {
history_store.remove_recently_opened_entry(
&entry, cx,
);
},
);
})
.ok();
}
},
);
}
menu = menu.separator();
}
menu.action("View All", Box::new(OpenHistory))
.end_slot_action(DeleteRecentlyOpenThread.boxed_clone())
.fixed_width(px(320.).into())
@@ -840,7 +898,6 @@ impl AgentPanel {
self.set_active_view(
ActiveView::prompt_editor(
context_editor.clone(),
self.history_store.clone(),
self.language_registry.clone(),
window,
cx,
@@ -927,13 +984,7 @@ impl AgentPanel {
)
});
self.set_active_view(
ActiveView::prompt_editor(
editor.clone(),
self.history_store.clone(),
self.language_registry.clone(),
window,
cx,
),
ActiveView::prompt_editor(editor.clone(), self.language_registry.clone(), window, cx),
window,
cx,
);
@@ -1332,6 +1383,16 @@ impl AgentPanel {
}
}
}
ActiveView::TextThread { context_editor, .. } => {
let context = context_editor.read(cx).context();
// When switching away from an unsaved text thread, delete its entry.
if context.read(cx).path().is_none() {
let context = context.clone();
self.history_store.update(cx, |store, cx| {
store.remove_recently_opened_entry(&RecentEntry::Context(context), cx);
});
}
}
_ => {}
}
@@ -1339,14 +1400,13 @@ impl AgentPanel {
ActiveView::Thread { thread, .. } => self.history_store.update(cx, |store, cx| {
if let Some(thread) = thread.upgrade() {
let id = thread.read(cx).id().clone();
store.push_recently_opened_entry(HistoryEntryId::Thread(id), cx);
store.push_recently_opened_entry(RecentEntry::Thread(id, thread), cx);
}
}),
ActiveView::TextThread { context_editor, .. } => {
self.history_store.update(cx, |store, cx| {
if let Some(path) = context_editor.read(cx).context().read(cx).path() {
store.push_recently_opened_entry(HistoryEntryId::Context(path.clone()), cx)
}
let context = context_editor.read(cx).context().clone();
store.push_recently_opened_entry(RecentEntry::Context(context), cx)
})
}
_ => {}
@@ -1365,70 +1425,6 @@ impl AgentPanel {
self.focus_handle(cx).focus(window);
}
fn populate_recently_opened_menu_section(
mut menu: ContextMenu,
panel: Entity<Self>,
cx: &mut Context<ContextMenu>,
) -> ContextMenu {
let entries = panel
.read(cx)
.history_store
.read(cx)
.recently_opened_entries(cx);
if entries.is_empty() {
return menu;
}
menu = menu.header("Recently Opened");
for entry in entries {
let title = entry.title().clone();
let id = entry.id();
menu = menu.entry_with_end_slot_on_hover(
title,
None,
{
let panel = panel.downgrade();
let id = id.clone();
move |window, cx| {
let id = id.clone();
panel
.update(cx, move |this, cx| match id {
HistoryEntryId::Thread(id) => this
.open_thread_by_id(&id, window, cx)
.detach_and_log_err(cx),
HistoryEntryId::Context(path) => this
.open_saved_prompt_editor(path.clone(), window, cx)
.detach_and_log_err(cx),
})
.ok();
}
},
IconName::Close,
"Close Entry".into(),
{
let panel = panel.downgrade();
let id = id.clone();
move |_window, cx| {
panel
.update(cx, |this, cx| {
this.history_store.update(cx, |history_store, cx| {
history_store.remove_recently_opened_entry(&id, cx);
});
})
.ok();
}
},
);
}
menu = menu.separator();
menu
}
}
impl Focusable for AgentPanel {

View File

@@ -282,18 +282,15 @@ pub fn unordered_thread_entries(
text_thread_store: Entity<TextThreadStore>,
cx: &App,
) -> impl Iterator<Item = (DateTime<Utc>, ThreadContextEntry)> {
let threads = thread_store
.read(cx)
.reverse_chronological_threads()
.map(|thread| {
(
thread.updated_at,
ThreadContextEntry::Thread {
id: thread.id.clone(),
title: thread.summary.clone(),
},
)
});
let threads = thread_store.read(cx).unordered_threads().map(|thread| {
(
thread.updated_at,
ThreadContextEntry::Thread {
id: thread.id.clone(),
title: thread.summary.clone(),
},
)
});
let text_threads = text_thread_store
.read(cx)
@@ -303,7 +300,7 @@ pub fn unordered_thread_entries(
context.mtime.to_utc(),
ThreadContextEntry::Context {
path: context.path.clone(),
title: context.title.clone(),
title: context.title.clone().into(),
},
)
});

View File

@@ -105,7 +105,7 @@ impl Tool for ContextServerTool {
arguments
);
let response = protocol
.request::<context_server::types::requests::CallTool>(
.request::<context_server::types::request::CallTool>(
context_server::types::CallToolParams {
name: tool_name,
arguments,
@@ -123,9 +123,6 @@ impl Tool for ContextServerTool {
types::ToolResponseContent::Image { .. } => {
log::warn!("Ignoring image content from tool response");
}
types::ToolResponseContent::Audio { .. } => {
log::warn!("Ignoring audio content from tool response");
}
types::ToolResponseContent::Resource { .. } => {
log::warn!("Ignoring resource content from tool response");
}

View File

@@ -1,17 +1,18 @@
use std::{collections::VecDeque, path::Path, sync::Arc};
use anyhow::{Context as _, Result};
use assistant_context_editor::SavedContextMetadata;
use anyhow::Context as _;
use assistant_context_editor::{AssistantContext, SavedContextMetadata};
use chrono::{DateTime, Utc};
use gpui::{AsyncApp, Entity, SharedString, Task, prelude::*};
use itertools::Itertools;
use paths::contexts_dir;
use futures::future::{TryFutureExt as _, join_all};
use gpui::{Entity, Task, prelude::*};
use serde::{Deserialize, Serialize};
use smol::future::FutureExt;
use std::time::Duration;
use ui::App;
use ui::{App, SharedString, Window};
use util::ResultExt as _;
use crate::{
Thread,
thread::ThreadId,
thread_store::{SerializedThreadMetadata, ThreadStore},
};
@@ -40,34 +41,52 @@ impl HistoryEntry {
HistoryEntry::Context(context) => HistoryEntryId::Context(context.path.clone()),
}
}
pub fn title(&self) -> &SharedString {
match self {
HistoryEntry::Thread(thread) => &thread.summary,
HistoryEntry::Context(context) => &context.title,
}
}
}
/// Generic identifier for a history entry.
#[derive(Clone, PartialEq, Eq, Debug)]
#[derive(Clone, PartialEq, Eq)]
pub enum HistoryEntryId {
Thread(ThreadId),
Context(Arc<Path>),
}
#[derive(Clone, Debug)]
pub(crate) enum RecentEntry {
Thread(ThreadId, Entity<Thread>),
Context(Entity<AssistantContext>),
}
impl PartialEq for RecentEntry {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::Thread(l0, _), Self::Thread(r0, _)) => l0 == r0,
(Self::Context(l0), Self::Context(r0)) => l0 == r0,
_ => false,
}
}
}
impl Eq for RecentEntry {}
impl RecentEntry {
pub(crate) fn summary(&self, cx: &App) -> SharedString {
match self {
RecentEntry::Thread(_, thread) => thread.read(cx).summary().or_default(),
RecentEntry::Context(context) => context.read(cx).summary().or_default(),
}
}
}
#[derive(Serialize, Deserialize)]
enum SerializedRecentOpen {
enum SerializedRecentEntry {
Thread(String),
ContextName(String),
/// Old format which stores the full path
Context(String),
}
pub struct HistoryStore {
thread_store: Entity<ThreadStore>,
context_store: Entity<assistant_context_editor::ContextStore>,
recently_opened_entries: VecDeque<HistoryEntryId>,
recently_opened_entries: VecDeque<RecentEntry>,
_subscriptions: Vec<gpui::Subscription>,
_save_recently_opened_entries_task: Task<()>,
}
@@ -76,7 +95,8 @@ impl HistoryStore {
pub fn new(
thread_store: Entity<ThreadStore>,
context_store: Entity<assistant_context_editor::ContextStore>,
initial_recent_entries: impl IntoIterator<Item = HistoryEntryId>,
initial_recent_entries: impl IntoIterator<Item = RecentEntry>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let subscriptions = vec![
@@ -84,20 +104,68 @@ impl HistoryStore {
cx.observe(&context_store, |_, _, cx| cx.notify()),
];
cx.spawn(async move |this, cx| {
let entries = Self::load_recently_opened_entries(cx).await.log_err()?;
this.update(cx, |this, _| {
this.recently_opened_entries
.extend(
entries.into_iter().take(
MAX_RECENTLY_OPENED_ENTRIES
.saturating_sub(this.recently_opened_entries.len()),
),
);
window
.spawn(cx, {
let thread_store = thread_store.downgrade();
let context_store = context_store.downgrade();
let this = cx.weak_entity();
async move |cx| {
let path = paths::data_dir().join(NAVIGATION_HISTORY_PATH);
let contents = cx
.background_spawn(async move { std::fs::read_to_string(path) })
.await
.ok()?;
let entries = serde_json::from_str::<Vec<SerializedRecentEntry>>(&contents)
.context("deserializing persisted agent panel navigation history")
.log_err()?
.into_iter()
.take(MAX_RECENTLY_OPENED_ENTRIES)
.map(|serialized| match serialized {
SerializedRecentEntry::Thread(id) => thread_store
.update_in(cx, |thread_store, window, cx| {
let thread_id = ThreadId::from(id.as_str());
thread_store
.open_thread(&thread_id, window, cx)
.map_ok(|thread| RecentEntry::Thread(thread_id, thread))
.boxed()
})
.unwrap_or_else(|_| {
async {
anyhow::bail!("no thread store");
}
.boxed()
}),
SerializedRecentEntry::Context(id) => context_store
.update(cx, |context_store, cx| {
context_store
.open_local_context(Path::new(&id).into(), cx)
.map_ok(RecentEntry::Context)
.boxed()
})
.unwrap_or_else(|_| {
async {
anyhow::bail!("no context store");
}
.boxed()
}),
});
let entries = join_all(entries)
.await
.into_iter()
.filter_map(|result| result.log_with_level(log::Level::Debug))
.collect::<VecDeque<_>>();
this.update(cx, |this, _| {
this.recently_opened_entries.extend(entries);
this.recently_opened_entries
.truncate(MAX_RECENTLY_OPENED_ENTRIES);
})
.ok();
Some(())
}
})
.ok()
})
.detach();
.detach();
Self {
thread_store,
@@ -116,20 +184,19 @@ impl HistoryStore {
return history_entries;
}
history_entries.extend(
self.thread_store
.read(cx)
.reverse_chronological_threads()
.cloned()
.map(HistoryEntry::Thread),
);
history_entries.extend(
self.context_store
.read(cx)
.unordered_contexts()
.cloned()
.map(HistoryEntry::Context),
);
for thread in self
.thread_store
.update(cx, |this, _cx| this.reverse_chronological_threads())
{
history_entries.push(HistoryEntry::Thread(thread));
}
for context in self
.context_store
.update(cx, |this, _cx| this.reverse_chronological_contexts())
{
history_entries.push(HistoryEntry::Context(context));
}
history_entries.sort_unstable_by_key(|entry| std::cmp::Reverse(entry.updated_at()));
history_entries
@@ -139,62 +206,15 @@ impl HistoryStore {
self.entries(cx).into_iter().take(limit).collect()
}
pub fn recently_opened_entries(&self, cx: &App) -> Vec<HistoryEntry> {
#[cfg(debug_assertions)]
if std::env::var("ZED_SIMULATE_NO_THREAD_HISTORY").is_ok() {
return Vec::new();
}
let thread_entries = self
.thread_store
.read(cx)
.reverse_chronological_threads()
.flat_map(|thread| {
self.recently_opened_entries
.iter()
.enumerate()
.flat_map(|(index, entry)| match entry {
HistoryEntryId::Thread(id) if &thread.id == id => {
Some((index, HistoryEntry::Thread(thread.clone())))
}
_ => None,
})
});
let context_entries =
self.context_store
.read(cx)
.unordered_contexts()
.flat_map(|context| {
self.recently_opened_entries
.iter()
.enumerate()
.flat_map(|(index, entry)| match entry {
HistoryEntryId::Context(path) if &context.path == path => {
Some((index, HistoryEntry::Context(context.clone())))
}
_ => None,
})
});
thread_entries
.chain(context_entries)
// optimization to halt iteration early
.take(self.recently_opened_entries.len())
.sorted_unstable_by_key(|(index, _)| *index)
.map(|(_, entry)| entry)
.collect()
}
fn save_recently_opened_entries(&mut self, cx: &mut Context<Self>) {
let serialized_entries = self
.recently_opened_entries
.iter()
.filter_map(|entry| match entry {
HistoryEntryId::Context(path) => path.file_name().map(|file| {
SerializedRecentOpen::ContextName(file.to_string_lossy().to_string())
}),
HistoryEntryId::Thread(id) => Some(SerializedRecentOpen::Thread(id.to_string())),
RecentEntry::Context(context) => Some(SerializedRecentEntry::Context(
context.read(cx).path()?.to_str()?.to_owned(),
)),
RecentEntry::Thread(id, _) => Some(SerializedRecentEntry::Thread(id.to_string())),
})
.collect::<Vec<_>>();
@@ -213,33 +233,7 @@ impl HistoryStore {
});
}
fn load_recently_opened_entries(cx: &AsyncApp) -> Task<Result<Vec<HistoryEntryId>>> {
cx.background_spawn(async move {
let path = paths::data_dir().join(NAVIGATION_HISTORY_PATH);
let contents = smol::fs::read_to_string(path).await?;
let entries = serde_json::from_str::<Vec<SerializedRecentOpen>>(&contents)
.context("deserializing persisted agent panel navigation history")?
.into_iter()
.take(MAX_RECENTLY_OPENED_ENTRIES)
.flat_map(|entry| match entry {
SerializedRecentOpen::Thread(id) => {
Some(HistoryEntryId::Thread(id.as_str().into()))
}
SerializedRecentOpen::ContextName(file_name) => Some(HistoryEntryId::Context(
contexts_dir().join(file_name).into(),
)),
SerializedRecentOpen::Context(path) => {
Path::new(&path).file_name().map(|file_name| {
HistoryEntryId::Context(contexts_dir().join(file_name).into())
})
}
})
.collect::<Vec<_>>();
Ok(entries)
})
}
pub fn push_recently_opened_entry(&mut self, entry: HistoryEntryId, cx: &mut Context<Self>) {
pub fn push_recently_opened_entry(&mut self, entry: RecentEntry, cx: &mut Context<Self>) {
self.recently_opened_entries
.retain(|old_entry| old_entry != &entry);
self.recently_opened_entries.push_front(entry);
@@ -250,33 +244,24 @@ impl HistoryStore {
pub fn remove_recently_opened_thread(&mut self, id: ThreadId, cx: &mut Context<Self>) {
self.recently_opened_entries.retain(|entry| match entry {
HistoryEntryId::Thread(thread_id) if thread_id == &id => false,
RecentEntry::Thread(thread_id, _) if thread_id == &id => false,
_ => true,
});
self.save_recently_opened_entries(cx);
}
pub fn replace_recently_opened_text_thread(
&mut self,
old_path: &Path,
new_path: &Arc<Path>,
cx: &mut Context<Self>,
) {
for entry in &mut self.recently_opened_entries {
match entry {
HistoryEntryId::Context(path) if path.as_ref() == old_path => {
*entry = HistoryEntryId::Context(new_path.clone());
break;
}
_ => {}
}
}
self.save_recently_opened_entries(cx);
}
pub fn remove_recently_opened_entry(&mut self, entry: &HistoryEntryId, cx: &mut Context<Self>) {
pub fn remove_recently_opened_entry(&mut self, entry: &RecentEntry, cx: &mut Context<Self>) {
self.recently_opened_entries
.retain(|old_entry| old_entry != entry);
self.save_recently_opened_entries(cx);
}
pub fn recently_opened_entries(&self, _cx: &mut Context<Self>) -> VecDeque<RecentEntry> {
#[cfg(debug_assertions)]
if std::env::var("ZED_SIMULATE_NO_THREAD_HISTORY").is_ok() {
return VecDeque::new();
}
self.recently_opened_entries.clone()
}
}

View File

@@ -195,20 +195,20 @@ impl MessageSegment {
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProjectSnapshot {
pub worktree_snapshots: Vec<WorktreeSnapshot>,
pub unsaved_buffer_paths: Vec<String>,
pub timestamp: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WorktreeSnapshot {
pub worktree_path: String,
pub git_state: Option<GitState>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GitState {
pub remote_url: Option<String>,
pub head_sha: Option<String>,
@@ -247,7 +247,7 @@ impl LastRestoreCheckpoint {
}
}
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub enum DetailedSummaryState {
#[default]
NotGenerated,
@@ -391,7 +391,7 @@ impl ThreadSummary {
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExceededWindowError {
/// Model used when last message exceeded context window
model_id: LanguageModelId,

View File

@@ -671,7 +671,7 @@ impl RenderOnce for HistoryEntryElement {
),
HistoryEntry::Context(context) => (
context.path.to_string_lossy().to_string(),
context.title.clone(),
context.title.clone().into(),
context.mtime.timestamp(),
),
};

View File

@@ -89,7 +89,7 @@ pub fn init(cx: &mut App) {
pub struct SharedProjectContext(Rc<RefCell<Option<ProjectContext>>>);
impl SharedProjectContext {
pub fn borrow(&self) -> Ref<'_, Option<ProjectContext>> {
pub fn borrow(&self) -> Ref<Option<ProjectContext>> {
self.0.borrow()
}
}
@@ -393,11 +393,16 @@ impl ThreadStore {
self.threads.len()
}
pub fn reverse_chronological_threads(&self) -> impl Iterator<Item = &SerializedThreadMetadata> {
// ordering is from "ORDER BY" in `list_threads`
pub fn unordered_threads(&self) -> impl Iterator<Item = &SerializedThreadMetadata> {
self.threads.iter()
}
pub fn reverse_chronological_threads(&self) -> Vec<SerializedThreadMetadata> {
let mut threads = self.threads.iter().cloned().collect::<Vec<_>>();
threads.sort_unstable_by_key(|thread| std::cmp::Reverse(thread.updated_at));
threads
}
pub fn create_thread(&mut self, cx: &mut Context<Self>) -> Entity<Thread> {
cx.new(|cx| {
Thread::new(
@@ -562,7 +567,7 @@ impl ThreadStore {
if protocol.capable(context_server::protocol::ServerCapability::Tools) {
if let Some(response) = protocol
.request::<context_server::types::requests::ListTools>(())
.request::<context_server::types::request::ListTools>(())
.await
.log_err()
{
@@ -603,7 +608,7 @@ pub struct SerializedThreadMetadata {
pub updated_at: DateTime<Utc>,
}
#[derive(Serialize, Deserialize, Debug, PartialEq)]
#[derive(Serialize, Deserialize, Debug)]
pub struct SerializedThread {
pub version: String,
pub summary: SharedString,
@@ -629,7 +634,7 @@ pub struct SerializedThread {
pub profile: Option<AgentProfileId>,
}
#[derive(Serialize, Deserialize, Debug, PartialEq)]
#[derive(Serialize, Deserialize, Debug)]
pub struct SerializedLanguageModel {
pub provider: String,
pub model: String,
@@ -690,15 +695,11 @@ impl SerializedThreadV0_1_0 {
messages.push(message);
}
SerializedThread {
messages,
version: SerializedThread::VERSION.to_string(),
..self.0
}
SerializedThread { messages, ..self.0 }
}
}
#[derive(Debug, Serialize, Deserialize, PartialEq)]
#[derive(Debug, Serialize, Deserialize)]
pub struct SerializedMessage {
pub id: MessageId,
pub role: Role,
@@ -716,7 +717,7 @@ pub struct SerializedMessage {
pub is_hidden: bool,
}
#[derive(Debug, Serialize, Deserialize, PartialEq)]
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum SerializedMessageSegment {
#[serde(rename = "text")]
@@ -734,14 +735,14 @@ pub enum SerializedMessageSegment {
},
}
#[derive(Debug, Serialize, Deserialize, PartialEq)]
#[derive(Debug, Serialize, Deserialize)]
pub struct SerializedToolUse {
pub id: LanguageModelToolUseId,
pub name: SharedString,
pub input: serde_json::Value,
}
#[derive(Debug, Serialize, Deserialize, PartialEq)]
#[derive(Debug, Serialize, Deserialize)]
pub struct SerializedToolResult {
pub tool_use_id: LanguageModelToolUseId,
pub is_error: bool,
@@ -804,7 +805,7 @@ impl LegacySerializedMessage {
}
}
#[derive(Debug, Serialize, Deserialize, PartialEq)]
#[derive(Debug, Serialize, Deserialize)]
pub struct SerializedCrease {
pub start: usize,
pub end: usize,
@@ -923,7 +924,7 @@ impl ThreadsDatabase {
fn bytes_encode(
item: &Self::EItem,
) -> Result<std::borrow::Cow<'_, [u8]>, heed::BoxedError> {
) -> Result<std::borrow::Cow<[u8]>, heed::BoxedError> {
serde_json::to_vec(&item.0)
.map(std::borrow::Cow::Owned)
.map_err(Into::into)
@@ -1061,181 +1062,3 @@ impl ThreadsDatabase {
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::thread::{DetailedSummaryState, MessageId};
use chrono::Utc;
use language_model::{Role, TokenUsage};
use pretty_assertions::assert_eq;
#[test]
fn test_legacy_serialized_thread_upgrade() {
let updated_at = Utc::now();
let legacy_thread = LegacySerializedThread {
summary: "Test conversation".into(),
updated_at,
messages: vec![LegacySerializedMessage {
id: MessageId(1),
role: Role::User,
text: "Hello, world!".to_string(),
tool_uses: vec![],
tool_results: vec![],
}],
initial_project_snapshot: None,
};
let upgraded = legacy_thread.upgrade();
assert_eq!(
upgraded,
SerializedThread {
summary: "Test conversation".into(),
updated_at,
messages: vec![SerializedMessage {
id: MessageId(1),
role: Role::User,
segments: vec![SerializedMessageSegment::Text {
text: "Hello, world!".to_string()
}],
tool_uses: vec![],
tool_results: vec![],
context: "".to_string(),
creases: vec![],
is_hidden: false
}],
version: SerializedThread::VERSION.to_string(),
initial_project_snapshot: None,
cumulative_token_usage: TokenUsage::default(),
request_token_usage: vec![],
detailed_summary_state: DetailedSummaryState::default(),
exceeded_window_error: None,
model: None,
completion_mode: None,
tool_use_limit_reached: false,
profile: None
}
)
}
#[test]
fn test_serialized_threadv0_1_0_upgrade() {
let updated_at = Utc::now();
let thread_v0_1_0 = SerializedThreadV0_1_0(SerializedThread {
summary: "Test conversation".into(),
updated_at,
messages: vec![
SerializedMessage {
id: MessageId(1),
role: Role::User,
segments: vec![SerializedMessageSegment::Text {
text: "Use tool_1".to_string(),
}],
tool_uses: vec![],
tool_results: vec![],
context: "".to_string(),
creases: vec![],
is_hidden: false,
},
SerializedMessage {
id: MessageId(2),
role: Role::Assistant,
segments: vec![SerializedMessageSegment::Text {
text: "I want to use a tool".to_string(),
}],
tool_uses: vec![SerializedToolUse {
id: "abc".into(),
name: "tool_1".into(),
input: serde_json::Value::Null,
}],
tool_results: vec![],
context: "".to_string(),
creases: vec![],
is_hidden: false,
},
SerializedMessage {
id: MessageId(1),
role: Role::User,
segments: vec![SerializedMessageSegment::Text {
text: "Here is the tool result".to_string(),
}],
tool_uses: vec![],
tool_results: vec![SerializedToolResult {
tool_use_id: "abc".into(),
is_error: false,
content: LanguageModelToolResultContent::Text("abcdef".into()),
output: Some(serde_json::Value::Null),
}],
context: "".to_string(),
creases: vec![],
is_hidden: false,
},
],
version: SerializedThreadV0_1_0::VERSION.to_string(),
initial_project_snapshot: None,
cumulative_token_usage: TokenUsage::default(),
request_token_usage: vec![],
detailed_summary_state: DetailedSummaryState::default(),
exceeded_window_error: None,
model: None,
completion_mode: None,
tool_use_limit_reached: false,
profile: None,
});
let upgraded = thread_v0_1_0.upgrade();
assert_eq!(
upgraded,
SerializedThread {
summary: "Test conversation".into(),
updated_at,
messages: vec![
SerializedMessage {
id: MessageId(1),
role: Role::User,
segments: vec![SerializedMessageSegment::Text {
text: "Use tool_1".to_string()
}],
tool_uses: vec![],
tool_results: vec![],
context: "".to_string(),
creases: vec![],
is_hidden: false
},
SerializedMessage {
id: MessageId(2),
role: Role::Assistant,
segments: vec![SerializedMessageSegment::Text {
text: "I want to use a tool".to_string(),
}],
tool_uses: vec![SerializedToolUse {
id: "abc".into(),
name: "tool_1".into(),
input: serde_json::Value::Null,
}],
tool_results: vec![SerializedToolResult {
tool_use_id: "abc".into(),
is_error: false,
content: LanguageModelToolResultContent::Text("abcdef".into()),
output: Some(serde_json::Value::Null),
}],
context: "".to_string(),
creases: vec![],
is_hidden: false,
},
],
version: SerializedThread::VERSION.to_string(),
initial_project_snapshot: None,
cumulative_token_usage: TokenUsage::default(),
request_token_usage: vec![],
detailed_summary_state: DetailedSummaryState::default(),
exceeded_window_error: None,
model: None,
completion_mode: None,
tool_use_limit_reached: false,
profile: None
}
)
}
}

View File

@@ -11,7 +11,7 @@ use assistant_slash_commands::FileCommandMetadata;
use client::{self, proto, telemetry::Telemetry};
use clock::ReplicaId;
use collections::{HashMap, HashSet};
use fs::{Fs, RenameOptions};
use fs::{Fs, RemoveOptions};
use futures::{FutureExt, StreamExt, future::Shared};
use gpui::{
App, AppContext as _, Context, Entity, EventEmitter, RenderImage, SharedString, Subscription,
@@ -452,10 +452,6 @@ pub enum ContextEvent {
MessagesEdited,
SummaryChanged,
SummaryGenerated,
PathChanged {
old_path: Option<Arc<Path>>,
new_path: Arc<Path>,
},
StreamedCompletion,
StartedThoughtProcess(Range<language::Anchor>),
EndedThoughtProcess(language::Anchor),
@@ -2898,34 +2894,22 @@ impl AssistantContext {
}
fs.create_dir(contexts_dir().as_ref()).await?;
// rename before write ensures that only one file exists
if let Some(old_path) = old_path.as_ref() {
fs.atomic_write(new_path.clone(), serde_json::to_string(&context).unwrap())
.await?;
if let Some(old_path) = old_path {
if new_path.as_path() != old_path.as_ref() {
fs.rename(
fs.remove_file(
&old_path,
&new_path,
RenameOptions {
overwrite: true,
ignore_if_exists: true,
RemoveOptions {
recursive: false,
ignore_if_not_exists: true,
},
)
.await?;
}
}
// update path before write in case it fails
this.update(cx, {
let new_path: Arc<Path> = new_path.clone().into();
move |this, cx| {
this.path = Some(new_path.clone());
cx.emit(ContextEvent::PathChanged { old_path, new_path });
}
})
.ok();
fs.atomic_write(new_path, serde_json::to_string(&context).unwrap())
.await?;
this.update(cx, |this, _| this.path = Some(new_path.into()))?;
}
Ok(())
@@ -3293,7 +3277,7 @@ impl SavedContextV0_1_0 {
#[derive(Debug, Clone)]
pub struct SavedContextMetadata {
pub title: SharedString,
pub title: String,
pub path: Arc<Path>,
pub mtime: chrono::DateTime<chrono::Local>,
}

View File

@@ -580,7 +580,6 @@ impl ContextEditor {
});
}
ContextEvent::SummaryGenerated => {}
ContextEvent::PathChanged { .. } => {}
ContextEvent::StartedThoughtProcess(range) => {
let creases = self.insert_thought_process_output_sections(
[(

View File

@@ -347,6 +347,12 @@ impl ContextStore {
self.contexts_metadata.iter()
}
pub fn reverse_chronological_contexts(&self) -> Vec<SavedContextMetadata> {
let mut contexts = self.contexts_metadata.iter().cloned().collect::<Vec<_>>();
contexts.sort_unstable_by_key(|thread| std::cmp::Reverse(thread.mtime));
contexts
}
pub fn create(&mut self, cx: &mut Context<Self>) -> Entity<AssistantContext> {
let context = cx.new(|cx| {
AssistantContext::local(
@@ -612,16 +618,6 @@ impl ContextStore {
ContextEvent::SummaryChanged => {
self.advertise_contexts(cx);
}
ContextEvent::PathChanged { old_path, new_path } => {
if let Some(old_path) = old_path.as_ref() {
for metadata in &mut self.contexts_metadata {
if &metadata.path == old_path {
metadata.path = new_path.clone();
break;
}
}
}
}
ContextEvent::Operation(operation) => {
let context_id = context.read(cx).id().to_proto();
let operation = operation.to_proto();
@@ -796,7 +792,7 @@ impl ContextStore {
.next()
{
contexts.push(SavedContextMetadata {
title: title.to_string().into(),
title: title.to_string(),
path: path.into(),
mtime: metadata.mtime.timestamp_for_user().into(),
});
@@ -869,7 +865,7 @@ impl ContextStore {
if protocol.capable(context_server::protocol::ServerCapability::Prompts) {
if let Some(response) = protocol
.request::<context_server::types::requests::PromptsList>(())
.request::<context_server::types::request::PromptsList>(())
.await
.log_err()
{

View File

@@ -87,7 +87,7 @@ impl SlashCommand for ContextServerSlashCommand {
let protocol = server.client().context("Context server not initialized")?;
let response = protocol
.request::<context_server::types::requests::CompletionComplete>(
.request::<context_server::types::request::CompletionComplete>(
context_server::types::CompletionCompleteParams {
reference: context_server::types::CompletionReference::Prompt(
context_server::types::PromptReference {
@@ -145,7 +145,7 @@ impl SlashCommand for ContextServerSlashCommand {
cx.foreground_executor().spawn(async move {
let protocol = server.client().context("Context server not initialized")?;
let response = protocol
.request::<context_server::types::requests::PromptsGet>(
.request::<context_server::types::request::PromptsGet>(
context_server::types::PromptsGetParams {
name: prompt_name.clone(),
arguments: Some(prompt_args),

View File

@@ -46,19 +46,15 @@ fn adapt_to_json_schema_subset(json: &mut Value) -> Result<()> {
);
}
const KEYS_TO_REMOVE: [(&str, fn(&Value) -> bool); 5] = [
("format", |value| value.is_string()),
("additionalProperties", |value| value.is_boolean()),
("exclusiveMinimum", |value| value.is_number()),
("exclusiveMaximum", |value| value.is_number()),
("optional", |value| value.is_boolean()),
const KEYS_TO_REMOVE: [&str; 5] = [
"format",
"additionalProperties",
"exclusiveMinimum",
"exclusiveMaximum",
"optional",
];
for (key, predicate) in KEYS_TO_REMOVE {
if let Some(value) = obj.get(key) {
if predicate(value) {
obj.remove(key);
}
}
for key in KEYS_TO_REMOVE {
obj.remove(key);
}
// If a type is not specified for an input parameter, add a default type
@@ -157,24 +153,6 @@ mod tests {
"type": "integer"
})
);
// Ensure that we do not remove keys that are actually supported (e.g. "format" can just be used as another property)
let mut json = json!({
"description": "A test field",
"type": "integer",
"format": {},
});
adapt_to_json_schema_subset(&mut json).unwrap();
assert_eq!(
json,
json!({
"description": "A test field",
"type": "integer",
"format": {},
})
);
}
#[test]

View File

@@ -111,7 +111,7 @@ pub struct ChannelMembership {
pub role: proto::ChannelRole,
}
impl ChannelMembership {
pub fn sort_key(&self) -> MembershipSortKey<'_> {
pub fn sort_key(&self) -> MembershipSortKey {
MembershipSortKey {
role_order: match self.role {
proto::ChannelRole::Admin => 0,

View File

@@ -32,7 +32,7 @@ impl ChannelIndex {
.retain(|channel_id| !channels.contains(channel_id));
}
pub fn bulk_insert(&mut self) -> ChannelPathsInsertGuard<'_> {
pub fn bulk_insert(&mut self) -> ChannelPathsInsertGuard {
ChannelPathsInsertGuard {
channels_ordered: &mut self.channels_ordered,
channels_by_id: &mut self.channels_by_id,

View File

@@ -39,7 +39,7 @@ enum ProxyType<'t> {
HttpProxy(HttpProxyType<'t>),
}
fn parse_proxy_type(proxy: &Url) -> Option<((String, u16), ProxyType<'_>)> {
fn parse_proxy_type(proxy: &Url) -> Option<((String, u16), ProxyType)> {
let scheme = proxy.scheme();
let host = proxy.host()?.to_string();
let port = proxy.port_or_known_default()?;

View File

@@ -8,7 +8,7 @@
use anyhow::Result;
use crate::client::Client;
use crate::types::{self, Notification, Request};
use crate::types::{self, Request};
pub struct ModelContextProtocol {
inner: Client,
@@ -20,10 +20,9 @@ impl ModelContextProtocol {
}
fn supported_protocols() -> Vec<types::ProtocolVersion> {
vec![
types::ProtocolVersion(types::LATEST_PROTOCOL_VERSION.to_string()),
types::ProtocolVersion(types::VERSION_2024_11_05.to_string()),
]
vec![types::ProtocolVersion(
types::LATEST_PROTOCOL_VERSION.to_string(),
)]
}
pub async fn initialize(
@@ -43,7 +42,7 @@ impl ModelContextProtocol {
let response: types::InitializeResponse = self
.inner
.request(types::requests::Initialize::METHOD, params)
.request(types::request::Initialize::METHOD, params)
.await?;
anyhow::ensure!(
@@ -54,13 +53,16 @@ impl ModelContextProtocol {
log::trace!("mcp server info {:?}", response.server_info);
self.inner.notify(
types::NotificationType::Initialized.as_str(),
serde_json::json!({}),
)?;
let initialized_protocol = InitializedContextServerProtocol {
inner: self.inner,
initialize: response,
};
initialized_protocol.notify::<types::notifications::Initialized>(())?;
Ok(initialized_protocol)
}
}
@@ -94,8 +96,4 @@ impl InitializedContextServerProtocol {
pub async fn request<T: Request>(&self, params: T::Params) -> Result<T::Response> {
self.inner.request(T::METHOD, params).await
}
pub fn notify<T: Notification>(&self, params: T::Params) -> Result<()> {
self.inner.notify(T::METHOD, params)
}
}

View File

@@ -14,7 +14,7 @@ pub fn create_fake_transport(
executor: BackgroundExecutor,
) -> FakeTransport {
let name = name.into();
FakeTransport::new(executor).on_request::<crate::types::requests::Initialize>(move |_params| {
FakeTransport::new(executor).on_request::<crate::types::request::Initialize>(move |_params| {
create_initialize_response(name.clone())
})
}

View File

@@ -3,10 +3,9 @@ use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use url::Url;
pub const LATEST_PROTOCOL_VERSION: &str = "2025-03-26";
pub const VERSION_2024_11_05: &str = "2024-11-05";
pub const LATEST_PROTOCOL_VERSION: &str = "2024-11-05";
pub mod requests {
pub mod request {
use super::*;
macro_rules! request {
@@ -83,57 +82,6 @@ pub trait Request {
const METHOD: &'static str;
}
pub mod notifications {
use super::*;
macro_rules! notification {
($method:expr, $name:ident, $params:ty) => {
pub struct $name;
impl Notification for $name {
type Params = $params;
const METHOD: &'static str = $method;
}
};
}
notification!("notifications/initialized", Initialized, ());
notification!("notifications/progress", Progress, ProgressParams);
notification!("notifications/message", Message, MessageParams);
notification!(
"notifications/resources/updated",
ResourcesUpdated,
ResourcesUpdatedParams
);
notification!(
"notifications/resources/list_changed",
ResourcesListChanged,
()
);
notification!("notifications/tools/list_changed", ToolsListChanged, ());
notification!("notifications/prompts/list_changed", PromptsListChanged, ());
notification!("notifications/roots/list_changed", RootsListChanged, ());
}
pub trait Notification {
type Params: DeserializeOwned + Serialize + Send + Sync + 'static;
const METHOD: &'static str;
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MessageParams {
pub level: LoggingLevel,
pub logger: Option<String>,
pub data: serde_json::Value,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ResourcesUpdatedParams {
pub uri: String,
}
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(transparent)]
pub struct ProtocolVersion(pub String);
@@ -343,20 +291,13 @@ pub enum MessageContent {
#[serde(skip_serializing_if = "Option::is_none")]
annotations: Option<MessageAnnotations>,
},
#[serde(rename = "image", rename_all = "camelCase")]
#[serde(rename = "image")]
Image {
data: String,
mime_type: String,
#[serde(skip_serializing_if = "Option::is_none")]
annotations: Option<MessageAnnotations>,
},
#[serde(rename = "audio", rename_all = "camelCase")]
Audio {
data: String,
mime_type: String,
#[serde(skip_serializing_if = "Option::is_none")]
annotations: Option<MessageAnnotations>,
},
#[serde(rename = "resource")]
Resource {
resource: ResourceContents,
@@ -453,8 +394,6 @@ pub struct ServerCapabilities {
#[serde(skip_serializing_if = "Option::is_none")]
pub logging: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub completions: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub prompts: Option<PromptsCapabilities>,
#[serde(skip_serializing_if = "Option::is_none")]
pub resources: Option<ResourcesCapabilities>,
@@ -499,28 +438,6 @@ pub struct Tool {
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub input_schema: serde_json::Value,
#[serde(skip_serializing_if = "Option::is_none")]
pub annotations: Option<ToolAnnotations>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ToolAnnotations {
/// A human-readable title for the tool.
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
/// If true, the tool does not modify its environment.
#[serde(skip_serializing_if = "Option::is_none")]
pub read_only_hint: Option<bool>,
/// If true, the tool may perform destructive updates to its environment.
#[serde(skip_serializing_if = "Option::is_none")]
pub destructive_hint: Option<bool>,
/// If true, calling the tool repeatedly with the same arguments will have no additional effect on its environment.
#[serde(skip_serializing_if = "Option::is_none")]
pub idempotent_hint: Option<bool>,
/// If true, this tool may interact with an "open world" of external entities.
#[serde(skip_serializing_if = "Option::is_none")]
pub open_world_hint: Option<bool>,
}
#[derive(Debug, Serialize, Deserialize)]
@@ -611,6 +528,34 @@ pub struct ModelHint {
pub name: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum NotificationType {
Initialized,
Progress,
Message,
ResourcesUpdated,
ResourcesListChanged,
ToolsListChanged,
PromptsListChanged,
RootsListChanged,
}
impl NotificationType {
pub fn as_str(&self) -> &'static str {
match self {
NotificationType::Initialized => "notifications/initialized",
NotificationType::Progress => "notifications/progress",
NotificationType::Message => "notifications/message",
NotificationType::ResourcesUpdated => "notifications/resources/updated",
NotificationType::ResourcesListChanged => "notifications/resources/list_changed",
NotificationType::ToolsListChanged => "notifications/tools/list_changed",
NotificationType::PromptsListChanged => "notifications/prompts/list_changed",
NotificationType::RootsListChanged => "notifications/roots/list_changed",
}
}
}
#[derive(Debug, Serialize)]
#[serde(untagged)]
pub enum ClientNotification {
@@ -631,14 +576,12 @@ pub enum ProgressToken {
Number(f64),
}
#[derive(Debug, Serialize, Deserialize)]
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ProgressParams {
pub progress_token: ProgressToken,
pub progress: f64,
#[serde(skip_serializing_if = "Option::is_none")]
pub message: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub total: Option<f64>,
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
pub meta: Option<HashMap<String, serde_json::Value>>,
@@ -682,8 +625,6 @@ pub enum ToolResponseContent {
Text { text: String },
#[serde(rename = "image", rename_all = "camelCase")]
Image { data: String, mime_type: String },
#[serde(rename = "audio", rename_all = "camelCase")]
Audio { data: String, mime_type: String },
#[serde(rename = "resource")]
Resource { resource: ResourceContents },
}

View File

@@ -28,7 +28,7 @@ use settings::SettingsStore;
use sign_in::{reinstall_and_sign_in_within_workspace, sign_out_within_workspace};
use std::{
any::TypeId,
env::home_dir,
env,
ffi::OsString,
mem,
ops::Range,
@@ -408,30 +408,24 @@ impl Copilot {
let proxy_url = copilot_settings.proxy.clone()?;
let no_verify = copilot_settings.proxy_no_verify;
let http_or_https_proxy = if proxy_url.starts_with("http:") {
Some("HTTP_PROXY")
"HTTP_PROXY"
} else if proxy_url.starts_with("https:") {
Some("HTTPS_PROXY")
"HTTPS_PROXY"
} else {
log::error!(
"Unsupported protocol scheme for language server proxy (must be http or https)"
);
None
return None;
};
let mut env = HashMap::default();
env.insert(http_or_https_proxy.to_string(), proxy_url);
if let Some(proxy_type) = http_or_https_proxy {
env.insert(proxy_type.to_string(), proxy_url);
if let Some(true) = no_verify {
env.insert("NODE_TLS_REJECT_UNAUTHORIZED".to_string(), "0".to_string());
};
}
if let Some(true) = no_verify {
env.insert("NODE_TLS_REJECT_UNAUTHORIZED".to_string(), "0".to_string());
};
if let Ok(oauth_token) = env::var(copilot_chat::COPILOT_OAUTH_ENV_VAR) {
env.insert(copilot_chat::COPILOT_OAUTH_ENV_VAR.to_string(), oauth_token);
}
if env.is_empty() { None } else { Some(env) }
Some(env)
}
#[cfg(any(test, feature = "test-support"))]
@@ -486,14 +480,11 @@ impl Copilot {
env,
};
let root_path = home_dir();
let root_path = root_path.as_deref().unwrap_or_else(|| {
if cfg!(target_os = "windows") {
Path::new("C:/")
} else {
Path::new("/")
}
});
let root_path = if cfg!(target_os = "windows") {
Path::new("C:/")
} else {
Path::new("/")
};
let server_name = LanguageServerName("copilot".into());
let server = LanguageServer::new(

View File

@@ -16,8 +16,6 @@ use paths::home_dir;
use serde::{Deserialize, Serialize};
use settings::watch_config_dir;
pub const COPILOT_OAUTH_ENV_VAR: &str = "GH_COPILOT_TOKEN";
#[derive(Default, Clone, Debug, PartialEq)]
pub struct CopilotChatSettings {
pub api_url: Arc<str>,
@@ -407,19 +405,13 @@ impl CopilotChat {
})
.detach_and_log_err(cx);
let this = Self {
oauth_token: std::env::var(COPILOT_OAUTH_ENV_VAR).ok(),
Self {
oauth_token: None,
api_token: None,
models: None,
settings,
client,
};
if this.oauth_token.is_some() {
cx.spawn(async move |this, mut cx| Self::update_models(&this, &mut cx).await)
.detach_and_log_err(cx);
}
this
}
async fn update_models(this: &WeakEntity<Self>, cx: &mut AsyncApp) -> Result<()> {

View File

@@ -464,7 +464,7 @@ impl BlockMap {
map
}
pub fn read(&self, wrap_snapshot: WrapSnapshot, edits: Patch<u32>) -> BlockMapReader<'_> {
pub fn read(&self, wrap_snapshot: WrapSnapshot, edits: Patch<u32>) -> BlockMapReader {
self.sync(&wrap_snapshot, edits);
*self.wrap_snapshot.borrow_mut() = wrap_snapshot.clone();
BlockMapReader {
@@ -479,7 +479,7 @@ impl BlockMap {
}
}
pub fn write(&mut self, wrap_snapshot: WrapSnapshot, edits: Patch<u32>) -> BlockMapWriter<'_> {
pub fn write(&mut self, wrap_snapshot: WrapSnapshot, edits: Patch<u32>) -> BlockMapWriter {
self.sync(&wrap_snapshot, edits);
*self.wrap_snapshot.borrow_mut() = wrap_snapshot;
BlockMapWriter(self)
@@ -1327,7 +1327,7 @@ impl BlockSnapshot {
}
}
pub(super) fn row_infos(&self, start_row: BlockRow) -> BlockRows<'_> {
pub(super) fn row_infos(&self, start_row: BlockRow) -> BlockRows {
let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>(&());
cursor.seek(&start_row, Bias::Right, &());
let (output_start, input_start) = cursor.start();

View File

@@ -357,7 +357,7 @@ impl FoldMap {
&mut self,
inlay_snapshot: InlaySnapshot,
edits: Vec<InlayEdit>,
) -> (FoldMapWriter<'_>, FoldSnapshot, Vec<FoldEdit>) {
) -> (FoldMapWriter, FoldSnapshot, Vec<FoldEdit>) {
let (snapshot, edits) = self.read(inlay_snapshot, edits);
(FoldMapWriter(self), snapshot, edits)
}
@@ -730,7 +730,7 @@ impl FoldSnapshot {
(line_end - line_start) as u32
}
pub fn row_infos(&self, start_row: u32) -> FoldRows<'_> {
pub fn row_infos(&self, start_row: u32) -> FoldRows {
if start_row > self.transforms.summary().output.lines.row {
panic!("invalid display row {}", start_row);
}

View File

@@ -726,7 +726,7 @@ impl WrapSnapshot {
self.transforms.summary().output.longest_row
}
pub fn row_infos(&self, start_row: u32) -> WrapRows<'_> {
pub fn row_infos(&self, start_row: u32) -> WrapRows {
let mut transforms = self.transforms.cursor::<(WrapPoint, TabPoint)>(&());
transforms.seek(&WrapPoint::new(start_row, 0), Bias::Left, &());
let mut input_row = transforms.start().1.row();

View File

@@ -213,14 +213,11 @@ use workspace::{
searchable::SearchEvent,
};
use crate::signature_help::{SignatureHelpHiddenBy, SignatureHelpState};
use crate::{
code_context_menus::CompletionsMenuSource,
hover_links::{find_url, find_url_from_range},
};
use crate::{
editor_settings::MultiCursorModifier,
signature_help::{SignatureHelpHiddenBy, SignatureHelpState},
};
pub const FILE_HEADER_HEIGHT: u32 = 2;
pub const MULTI_BUFFER_EXCERPT_HEADER_HEIGHT: u32 = 1;
@@ -256,6 +253,14 @@ pub type RenderDiffHunkControlsFn = Arc<
) -> AnyElement,
>;
const COLUMNAR_SELECTION_MODIFIERS: Modifiers = Modifiers {
alt: true,
shift: true,
control: false,
platform: false,
function: false,
};
struct InlineValueCache {
enabled: bool,
inlays: Vec<InlayId>,
@@ -906,21 +911,6 @@ struct InlineBlamePopover {
popover_state: InlineBlamePopoverState,
}
enum SelectionDragState {
/// State when no drag related activity is detected.
None,
/// State when the mouse is down on a selection that is about to be dragged.
ReadyToDrag {
selection: Selection<Anchor>,
click_position: gpui::Point<Pixels>,
},
/// State when the mouse is dragging the selection in the editor.
Dragging {
selection: Selection<Anchor>,
drop_cursor: Selection<Anchor>,
},
}
/// Represents a breakpoint indicator that shows up when hovering over lines in the gutter that don't have
/// a breakpoint on them.
#[derive(Clone, Copy, Debug)]
@@ -1106,8 +1096,6 @@ pub struct Editor {
hide_mouse_mode: HideMouseMode,
pub change_list: ChangeList,
inline_value_cache: InlineValueCache,
selection_drag_state: SelectionDragState,
drag_and_drop_selection_enabled: bool,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
@@ -2002,8 +1990,6 @@ impl Editor {
.unwrap_or_default(),
change_list: ChangeList::new(),
mode,
selection_drag_state: SelectionDragState::None,
drag_and_drop_selection_enabled: EditorSettings::get_global(cx).drag_and_drop_selection,
};
if let Some(breakpoints) = editor.breakpoint_store.as_ref() {
editor
@@ -3549,7 +3535,6 @@ impl Editor {
pub fn cancel(&mut self, _: &Cancel, window: &mut Window, cx: &mut Context<Self>) {
self.selection_mark_mode = false;
self.selection_drag_state = SelectionDragState::None;
if self.clear_expanded_diff_hunks(cx) {
cx.notify();
@@ -7106,29 +7091,6 @@ impl Editor {
)
}
fn multi_cursor_modifier(
cursor_event: bool,
modifiers: &Modifiers,
cx: &mut Context<Self>,
) -> bool {
let multi_cursor_setting = EditorSettings::get_global(cx).multi_cursor_modifier;
if cursor_event {
match multi_cursor_setting {
MultiCursorModifier::Alt => modifiers.alt,
MultiCursorModifier::CmdOrCtrl => modifiers.secondary(),
}
} else {
match multi_cursor_setting {
MultiCursorModifier::Alt => modifiers.secondary(),
MultiCursorModifier::CmdOrCtrl => modifiers.alt,
}
}
}
fn columnar_selection_modifiers(multi_cursor_modifier: bool, modifiers: &Modifiers) -> bool {
modifiers.shift && multi_cursor_modifier && modifiers.number_of_modifiers() == 2
}
fn update_selection_mode(
&mut self,
modifiers: &Modifiers,
@@ -7136,10 +7098,7 @@ impl Editor {
window: &mut Window,
cx: &mut Context<Self>,
) {
let multi_cursor_modifier = Self::multi_cursor_modifier(true, modifiers, cx);
if !Self::columnar_selection_modifiers(multi_cursor_modifier, modifiers)
|| self.selections.pending.is_none()
{
if modifiers != &COLUMNAR_SELECTION_MODIFIERS || self.selections.pending.is_none() {
return;
}
@@ -10604,44 +10563,6 @@ impl Editor {
});
}
pub fn move_selection_on_drop(
&mut self,
selection: &Selection<Anchor>,
target: DisplayPoint,
is_cut: bool,
window: &mut Window,
cx: &mut Context<Self>,
) {
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
let buffer = &display_map.buffer_snapshot;
let mut edits = Vec::new();
let insert_point = display_map
.clip_point(target, Bias::Left)
.to_point(&display_map);
let text = buffer
.text_for_range(selection.start..selection.end)
.collect::<String>();
if is_cut {
edits.push(((selection.start..selection.end), String::new()));
}
let insert_anchor = buffer.anchor_before(insert_point);
edits.push(((insert_anchor..insert_anchor), text));
let last_edit_start = insert_anchor.bias_left(buffer);
let last_edit_end = insert_anchor.bias_right(buffer);
self.transact(window, cx, |this, window, cx| {
this.buffer.update(cx, |buffer, cx| {
buffer.edit(edits, None, cx);
});
this.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
s.select_anchor_ranges([last_edit_start..last_edit_end]);
});
});
}
pub fn clear_selection_drag_state(&mut self) {
self.selection_drag_state = SelectionDragState::None;
}
pub fn duplicate(
&mut self,
upwards: bool,
@@ -18849,11 +18770,6 @@ impl Editor {
cx.emit(EditorEvent::BufferEdited);
cx.emit(SearchEvent::MatchesInvalidated);
if *singleton_buffer_edited {
if let Some(buffer) = edited_buffer {
if buffer.read(cx).file().is_none() {
cx.emit(EditorEvent::TitleChanged);
}
}
if let Some(project) = &self.project {
#[allow(clippy::mutable_key_type)]
let languages_affected = multibuffer.update(cx, |multibuffer, cx| {
@@ -19045,7 +18961,6 @@ impl Editor {
self.show_breadcrumbs = editor_settings.toolbar.breadcrumbs;
self.cursor_shape = editor_settings.cursor_shape.unwrap_or_default();
self.hide_mouse_mode = editor_settings.hide_mouse.unwrap_or_default();
self.drag_and_drop_selection_enabled = editor_settings.drag_and_drop_selection;
}
if old_cursor_shape != self.cursor_shape {

View File

@@ -49,7 +49,6 @@ pub struct EditorSettings {
#[serde(default)]
pub diagnostics_max_severity: Option<DiagnosticSeverity>,
pub inline_code_actions: bool,
pub drag_and_drop_selection: bool,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
@@ -423,7 +422,7 @@ pub struct EditorSettingsContent {
/// Default: always
pub seed_search_query_from_cursor: Option<SeedQuerySetting>,
pub use_smartcase_search: Option<bool>,
/// Determines the modifier to be used to add multiple cursors with the mouse. The open hover link mouse gestures will adapt such that it do not conflict with the multicursor modifier.
/// The key to use for adding multiple cursors
///
/// Default: alt
pub multi_cursor_modifier: Option<MultiCursorModifier>,
@@ -496,11 +495,6 @@ pub struct EditorSettingsContent {
///
/// Default: true
pub inline_code_actions: Option<bool>,
/// Whether to allow drag and drop text selection in buffer.
///
/// Default: true
pub drag_and_drop_selection: Option<bool>,
}
// Toolbar related settings

View File

@@ -1,14 +1,14 @@
use crate::{
ActiveDiagnostic, BlockId, CURSORS_VISIBLE_FOR, ChunkRendererContext, ChunkReplacement,
CodeActionSource, ConflictsOurs, ConflictsOursMarker, ConflictsOuter, ConflictsTheirs,
ConflictsTheirsMarker, ContextMenuPlacement, CursorShape, CustomBlockId, DisplayDiffHunk,
DisplayPoint, DisplayRow, DocumentHighlightRead, DocumentHighlightWrite, EditDisplayMode,
Editor, EditorMode, EditorSettings, EditorSnapshot, EditorStyle, FILE_HEADER_HEIGHT,
FocusedBlock, GutterDimensions, HalfPageDown, HalfPageUp, HandleInput, HoveredCursor,
InlayHintRefreshReason, InlineCompletion, JumpData, LineDown, LineHighlight, LineUp,
MAX_LINE_LEN, MIN_LINE_NUMBER_DIGITS, MINIMAP_FONT_SIZE, MULTI_BUFFER_EXCERPT_HEADER_HEIGHT,
OpenExcerpts, PageDown, PageUp, PhantomBreakpointIndicator, Point, RowExt, RowRangeExt,
SelectPhase, SelectedTextHighlight, Selection, SelectionDragState, SoftWrap,
ActiveDiagnostic, BlockId, COLUMNAR_SELECTION_MODIFIERS, CURSORS_VISIBLE_FOR,
ChunkRendererContext, ChunkReplacement, CodeActionSource, ConflictsOurs, ConflictsOursMarker,
ConflictsOuter, ConflictsTheirs, ConflictsTheirsMarker, ContextMenuPlacement, CursorShape,
CustomBlockId, DisplayDiffHunk, DisplayPoint, DisplayRow, DocumentHighlightRead,
DocumentHighlightWrite, EditDisplayMode, Editor, EditorMode, EditorSettings, EditorSnapshot,
EditorStyle, FILE_HEADER_HEIGHT, FocusedBlock, GutterDimensions, HalfPageDown, HalfPageUp,
HandleInput, HoveredCursor, InlayHintRefreshReason, InlineCompletion, JumpData, LineDown,
LineHighlight, LineUp, MAX_LINE_LEN, MIN_LINE_NUMBER_DIGITS, MINIMAP_FONT_SIZE,
MULTI_BUFFER_EXCERPT_HEADER_HEIGHT, OpenExcerpts, PageDown, PageUp, PhantomBreakpointIndicator,
Point, RowExt, RowRangeExt, SelectPhase, SelectedTextHighlight, Selection, SoftWrap,
StickyHeaderExcerpt, ToPoint, ToggleFold,
code_context_menus::{CodeActionsMenu, MENU_ASIDE_MAX_WIDTH, MENU_ASIDE_MIN_WIDTH, MENU_GAP},
display_map::{
@@ -17,7 +17,8 @@ use crate::{
},
editor_settings::{
CurrentLineHighlight, DoubleClickInMultibuffer, MinimapThumb, MinimapThumbBorder,
ScrollBeyondLastLine, ScrollbarAxes, ScrollbarDiagnostics, ShowMinimap, ShowScrollbar,
MultiCursorModifier, ScrollBeyondLastLine, ScrollbarAxes, ScrollbarDiagnostics,
ShowMinimap, ShowScrollbar,
},
git::blame::{BlameRenderer, GitBlame, GlobalBlameRenderer},
hover_popover::{
@@ -78,11 +79,10 @@ use std::{
time::Duration,
};
use sum_tree::Bias;
use text::{BufferId, SelectionGoal};
use text::BufferId;
use theme::{ActiveTheme, Appearance, BufferLineHeight, PlayerColor};
use ui::{ButtonLike, KeyBinding, POPOVER_Y_PADDING, Tooltip, h_flex, prelude::*};
use unicode_segmentation::UnicodeSegmentation;
use util::post_inc;
use util::{RangeExt, ResultExt, debug_panic};
use workspace::{CollaboratorId, Workspace, item::Item, notifications::NotifyTaskExt};
@@ -620,7 +620,6 @@ impl EditorElement {
let text_hitbox = &position_map.text_hitbox;
let gutter_hitbox = &position_map.gutter_hitbox;
let point_for_position = position_map.point_for_position(event.position);
let mut click_count = event.click_count;
let mut modifiers = event.modifiers;
@@ -634,20 +633,6 @@ impl EditorElement {
return;
}
if editor.drag_and_drop_selection_enabled && click_count == 1 {
let newest_anchor = editor.selections.newest_anchor();
let snapshot = editor.snapshot(window, cx);
let selection = newest_anchor.map(|anchor| anchor.to_display_point(&snapshot));
if point_for_position.intersects_selection(&selection) {
editor.selection_drag_state = SelectionDragState::ReadyToDrag {
selection: newest_anchor.clone(),
click_position: event.position,
};
cx.stop_propagation();
return;
}
}
let is_singleton = editor.buffer().read(cx).is_singleton();
if click_count == 2 && !is_singleton {
@@ -691,9 +676,9 @@ impl EditorElement {
}
}
let point_for_position = position_map.point_for_position(event.position);
let position = point_for_position.previous_valid;
let multi_cursor_modifier = Editor::multi_cursor_modifier(true, &modifiers, cx);
if Editor::columnar_selection_modifiers(multi_cursor_modifier, &modifiers) {
if modifiers == COLUMNAR_SELECTION_MODIFIERS {
editor.select(
SelectPhase::BeginColumnar {
position,
@@ -714,6 +699,11 @@ impl EditorElement {
cx,
);
} else {
let multi_cursor_setting = EditorSettings::get_global(cx).multi_cursor_modifier;
let multi_cursor_modifier = match multi_cursor_setting {
MultiCursorModifier::Alt => modifiers.alt,
MultiCursorModifier::CmdOrCtrl => modifiers.secondary(),
};
editor.select(
SelectPhase::Begin {
position,
@@ -831,47 +821,6 @@ impl EditorElement {
let text_hitbox = &position_map.text_hitbox;
let end_selection = editor.has_pending_selection();
let pending_nonempty_selections = editor.has_pending_nonempty_selection();
let point_for_position = position_map.point_for_position(event.position);
match editor.selection_drag_state {
SelectionDragState::ReadyToDrag {
selection: _,
ref click_position,
} => {
if event.position == *click_position {
editor.select(
SelectPhase::Begin {
position: point_for_position.previous_valid,
add: false,
click_count: 1, // ready to drag state only occurs on click count 1
},
window,
cx,
);
editor.selection_drag_state = SelectionDragState::None;
cx.stop_propagation();
return;
}
}
SelectionDragState::Dragging { ref selection, .. } => {
let snapshot = editor.snapshot(window, cx);
let selection_display = selection.map(|anchor| anchor.to_display_point(&snapshot));
if !point_for_position.intersects_selection(&selection_display) {
let is_cut = !event.modifiers.control;
editor.move_selection_on_drop(
&selection.clone(),
point_for_position.previous_valid,
is_cut,
window,
cx,
);
editor.selection_drag_state = SelectionDragState::None;
cx.stop_propagation();
return;
}
}
_ => {}
}
if end_selection {
editor.select(SelectPhase::End, window, cx);
@@ -918,9 +867,13 @@ impl EditorElement {
let text_hitbox = &position_map.text_hitbox;
let pending_nonempty_selections = editor.has_pending_nonempty_selection();
let hovered_link_modifier = Editor::multi_cursor_modifier(false, &event.modifiers(), cx);
let multi_cursor_setting = EditorSettings::get_global(cx).multi_cursor_modifier;
let multi_cursor_modifier = match multi_cursor_setting {
MultiCursorModifier::Alt => event.modifiers().secondary(),
MultiCursorModifier::CmdOrCtrl => event.modifiers().alt,
};
if !pending_nonempty_selections && hovered_link_modifier && text_hitbox.is_hovered(window) {
if !pending_nonempty_selections && multi_cursor_modifier && text_hitbox.is_hovered(window) {
let point = position_map.point_for_position(event.up.position);
editor.handle_click_hovered_link(point, event.modifiers(), window, cx);
@@ -935,15 +888,12 @@ impl EditorElement {
window: &mut Window,
cx: &mut Context<Editor>,
) {
if !editor.has_pending_selection()
&& matches!(editor.selection_drag_state, SelectionDragState::None)
{
if !editor.has_pending_selection() {
return;
}
let text_bounds = position_map.text_hitbox.bounds;
let point_for_position = position_map.point_for_position(event.position);
let mut scroll_delta = gpui::Point::<f32>::default();
let vertical_margin = position_map.line_height.min(text_bounds.size.height / 3.0);
let top = text_bounds.origin.y + vertical_margin;
@@ -975,46 +925,15 @@ impl EditorElement {
scroll_delta.x = scale_horizontal_mouse_autoscroll_delta(event.position.x - right);
}
if !editor.has_pending_selection() {
let drop_anchor = position_map
.snapshot
.display_point_to_anchor(point_for_position.previous_valid, Bias::Left);
match editor.selection_drag_state {
SelectionDragState::Dragging {
ref mut drop_cursor,
..
} => {
drop_cursor.start = drop_anchor;
drop_cursor.end = drop_anchor;
}
SelectionDragState::ReadyToDrag { ref selection, .. } => {
let drop_cursor = Selection {
id: post_inc(&mut editor.selections.next_selection_id),
start: drop_anchor,
end: drop_anchor,
reversed: false,
goal: SelectionGoal::None,
};
editor.selection_drag_state = SelectionDragState::Dragging {
selection: selection.clone(),
drop_cursor,
};
}
_ => {}
}
editor.apply_scroll_delta(scroll_delta, window, cx);
cx.notify();
} else {
editor.select(
SelectPhase::Update {
position: point_for_position.previous_valid,
goal_column: point_for_position.exact_unclipped.column(),
scroll_delta,
},
window,
cx,
);
}
editor.select(
SelectPhase::Update {
position: point_for_position.previous_valid,
goal_column: point_for_position.exact_unclipped.column(),
scroll_delta,
},
window,
cx,
);
}
fn mouse_moved(
@@ -1243,34 +1162,6 @@ impl EditorElement {
let player = editor.current_user_player_color(cx);
selections.push((player, layouts));
if let SelectionDragState::Dragging {
ref selection,
ref drop_cursor,
} = editor.selection_drag_state
{
if drop_cursor
.start
.cmp(&selection.start, &snapshot.buffer_snapshot)
.eq(&Ordering::Less)
|| drop_cursor
.end
.cmp(&selection.end, &snapshot.buffer_snapshot)
.eq(&Ordering::Greater)
{
let drag_cursor_layout = SelectionLayout::new(
drop_cursor.clone(),
false,
CursorShape::Bar,
&snapshot.display_snapshot,
false,
false,
None,
);
let absent_color = cx.theme().players().absent();
selections.push((absent_color, vec![drag_cursor_layout]));
}
}
}
if let Some(collaboration_hub) = &editor.collaboration_hub {
@@ -9351,35 +9242,6 @@ impl PointForPosition {
None
}
}
pub fn intersects_selection(&self, selection: &Selection<DisplayPoint>) -> bool {
let Some(valid_point) = self.as_valid() else {
return false;
};
let range = selection.range();
let candidate_row = valid_point.row();
let candidate_col = valid_point.column();
let start_row = range.start.row();
let start_col = range.start.column();
let end_row = range.end.row();
let end_col = range.end.column();
if candidate_row < start_row || candidate_row > end_row {
false
} else if start_row == end_row {
candidate_col >= start_col && candidate_col < end_col
} else {
if candidate_row == start_row {
candidate_col >= start_col
} else if candidate_row == end_row {
candidate_col < end_col
} else {
true
}
}
}
}
impl PositionMap {

View File

@@ -1,7 +1,7 @@
use crate::{
Anchor, Editor, EditorSettings, EditorSnapshot, FindAllReferences, GoToDefinition,
GoToTypeDefinition, GotoDefinitionKind, InlayId, Navigated, PointForPosition, SelectPhase,
editor_settings::GoToDefinitionFallback,
editor_settings::{GoToDefinitionFallback, MultiCursorModifier},
hover_popover::{self, InlayHover},
scroll::ScrollAmount,
};
@@ -120,7 +120,11 @@ impl Editor {
window: &mut Window,
cx: &mut Context<Self>,
) {
let hovered_link_modifier = Editor::multi_cursor_modifier(false, &modifiers, cx);
let multi_cursor_setting = EditorSettings::get_global(cx).multi_cursor_modifier;
let hovered_link_modifier = match multi_cursor_setting {
MultiCursorModifier::Alt => modifiers.secondary(),
MultiCursorModifier::CmdOrCtrl => modifiers.alt,
};
if !hovered_link_modifier || self.has_pending_selection() {
self.hide_hovered_link(cx);
return;

View File

@@ -411,7 +411,7 @@ impl<'a> MutableSelectionsCollection<'a> {
self.collection.display_map(self.cx)
}
pub fn buffer(&self) -> Ref<'_, MultiBufferSnapshot> {
pub fn buffer(&self) -> Ref<MultiBufferSnapshot> {
self.collection.buffer(self.cx)
}

View File

@@ -724,7 +724,7 @@ impl IncrementalCompilationCache {
}
impl CacheStore for IncrementalCompilationCache {
fn get(&self, key: &[u8]) -> Option<Cow<'_, [u8]>> {
fn get(&self, key: &[u8]) -> Option<Cow<[u8]>> {
self.cache.get(key).map(|v| v.into())
}

View File

@@ -323,7 +323,7 @@ pub trait GitRepository: Send + Sync {
/// Resolve a list of refs to SHAs.
fn revparse_batch(&self, revs: Vec<String>) -> BoxFuture<Result<Vec<Option<String>>>>;
fn head_sha(&self) -> BoxFuture<'_, Option<String>> {
fn head_sha(&self) -> BoxFuture<Option<String>> {
async move {
self.revparse_batch(vec!["HEAD".into()])
.await
@@ -525,7 +525,7 @@ impl GitRepository for RealGitRepository {
repo.commondir().into()
}
fn show(&self, commit: String) -> BoxFuture<'_, Result<CommitDetails>> {
fn show(&self, commit: String) -> BoxFuture<Result<CommitDetails>> {
let working_directory = self.working_directory();
self.executor
.spawn(async move {
@@ -561,7 +561,7 @@ impl GitRepository for RealGitRepository {
.boxed()
}
fn load_commit(&self, commit: String, cx: AsyncApp) -> BoxFuture<'_, Result<CommitDiff>> {
fn load_commit(&self, commit: String, cx: AsyncApp) -> BoxFuture<Result<CommitDiff>> {
let Some(working_directory) = self.repository.lock().workdir().map(ToOwned::to_owned)
else {
return future::ready(Err(anyhow!("no working directory"))).boxed();
@@ -668,7 +668,7 @@ impl GitRepository for RealGitRepository {
commit: String,
mode: ResetMode,
env: Arc<HashMap<String, String>>,
) -> BoxFuture<'_, Result<()>> {
) -> BoxFuture<Result<()>> {
async move {
let working_directory = self.working_directory();
@@ -698,7 +698,7 @@ impl GitRepository for RealGitRepository {
commit: String,
paths: Vec<RepoPath>,
env: Arc<HashMap<String, String>>,
) -> BoxFuture<'_, Result<()>> {
) -> BoxFuture<Result<()>> {
let working_directory = self.working_directory();
let git_binary_path = self.git_binary_path.clone();
async move {
@@ -723,7 +723,7 @@ impl GitRepository for RealGitRepository {
.boxed()
}
fn load_index_text(&self, path: RepoPath) -> BoxFuture<'_, Option<String>> {
fn load_index_text(&self, path: RepoPath) -> BoxFuture<Option<String>> {
// https://git-scm.com/book/en/v2/Git-Internals-Git-Objects
const GIT_MODE_SYMLINK: u32 = 0o120000;
@@ -756,7 +756,7 @@ impl GitRepository for RealGitRepository {
.boxed()
}
fn load_committed_text(&self, path: RepoPath) -> BoxFuture<'_, Option<String>> {
fn load_committed_text(&self, path: RepoPath) -> BoxFuture<Option<String>> {
let repo = self.repository.clone();
self.executor
.spawn(async move {
@@ -777,7 +777,7 @@ impl GitRepository for RealGitRepository {
path: RepoPath,
content: Option<String>,
env: Arc<HashMap<String, String>>,
) -> BoxFuture<'_, anyhow::Result<()>> {
) -> BoxFuture<anyhow::Result<()>> {
let working_directory = self.working_directory();
let git_binary_path = self.git_binary_path.clone();
self.executor
@@ -841,7 +841,7 @@ impl GitRepository for RealGitRepository {
remote.url().map(|url| url.to_string())
}
fn revparse_batch(&self, revs: Vec<String>) -> BoxFuture<'_, Result<Vec<Option<String>>>> {
fn revparse_batch(&self, revs: Vec<String>) -> BoxFuture<Result<Vec<Option<String>>>> {
let working_directory = self.working_directory();
self.executor
.spawn(async move {
@@ -891,14 +891,14 @@ impl GitRepository for RealGitRepository {
.boxed()
}
fn merge_message(&self) -> BoxFuture<'_, Option<String>> {
fn merge_message(&self) -> BoxFuture<Option<String>> {
let path = self.path().join("MERGE_MSG");
self.executor
.spawn(async move { std::fs::read_to_string(&path).ok() })
.boxed()
}
fn status(&self, path_prefixes: &[RepoPath]) -> BoxFuture<'_, Result<GitStatus>> {
fn status(&self, path_prefixes: &[RepoPath]) -> BoxFuture<Result<GitStatus>> {
let git_binary_path = self.git_binary_path.clone();
let working_directory = self.working_directory();
let path_prefixes = path_prefixes.to_owned();
@@ -919,7 +919,7 @@ impl GitRepository for RealGitRepository {
.boxed()
}
fn branches(&self) -> BoxFuture<'_, Result<Vec<Branch>>> {
fn branches(&self) -> BoxFuture<Result<Vec<Branch>>> {
let working_directory = self.working_directory();
let git_binary_path = self.git_binary_path.clone();
self.executor
@@ -986,7 +986,7 @@ impl GitRepository for RealGitRepository {
.boxed()
}
fn change_branch(&self, name: String) -> BoxFuture<'_, Result<()>> {
fn change_branch(&self, name: String) -> BoxFuture<Result<()>> {
let repo = self.repository.clone();
self.executor
.spawn(async move {
@@ -1018,7 +1018,7 @@ impl GitRepository for RealGitRepository {
.boxed()
}
fn create_branch(&self, name: String) -> BoxFuture<'_, Result<()>> {
fn create_branch(&self, name: String) -> BoxFuture<Result<()>> {
let repo = self.repository.clone();
self.executor
.spawn(async move {
@@ -1030,7 +1030,7 @@ impl GitRepository for RealGitRepository {
.boxed()
}
fn blame(&self, path: RepoPath, content: Rope) -> BoxFuture<'_, Result<crate::blame::Blame>> {
fn blame(&self, path: RepoPath, content: Rope) -> BoxFuture<Result<crate::blame::Blame>> {
let working_directory = self.working_directory();
let git_binary_path = self.git_binary_path.clone();
@@ -1052,7 +1052,7 @@ impl GitRepository for RealGitRepository {
.boxed()
}
fn diff(&self, diff: DiffType) -> BoxFuture<'_, Result<String>> {
fn diff(&self, diff: DiffType) -> BoxFuture<Result<String>> {
let working_directory = self.working_directory();
let git_binary_path = self.git_binary_path.clone();
self.executor
@@ -1083,7 +1083,7 @@ impl GitRepository for RealGitRepository {
&self,
paths: Vec<RepoPath>,
env: Arc<HashMap<String, String>>,
) -> BoxFuture<'_, Result<()>> {
) -> BoxFuture<Result<()>> {
let working_directory = self.working_directory();
let git_binary_path = self.git_binary_path.clone();
self.executor
@@ -1111,7 +1111,7 @@ impl GitRepository for RealGitRepository {
&self,
paths: Vec<RepoPath>,
env: Arc<HashMap<String, String>>,
) -> BoxFuture<'_, Result<()>> {
) -> BoxFuture<Result<()>> {
let working_directory = self.working_directory();
let git_binary_path = self.git_binary_path.clone();
@@ -1143,7 +1143,7 @@ impl GitRepository for RealGitRepository {
name_and_email: Option<(SharedString, SharedString)>,
options: CommitOptions,
env: Arc<HashMap<String, String>>,
) -> BoxFuture<'_, Result<()>> {
) -> BoxFuture<Result<()>> {
let working_directory = self.working_directory();
self.executor
.spawn(async move {
@@ -1182,7 +1182,7 @@ impl GitRepository for RealGitRepository {
ask_pass: AskPassDelegate,
env: Arc<HashMap<String, String>>,
cx: AsyncApp,
) -> BoxFuture<'_, Result<RemoteCommandOutput>> {
) -> BoxFuture<Result<RemoteCommandOutput>> {
let working_directory = self.working_directory();
let executor = cx.background_executor().clone();
async move {
@@ -1214,7 +1214,7 @@ impl GitRepository for RealGitRepository {
ask_pass: AskPassDelegate,
env: Arc<HashMap<String, String>>,
cx: AsyncApp,
) -> BoxFuture<'_, Result<RemoteCommandOutput>> {
) -> BoxFuture<Result<RemoteCommandOutput>> {
let working_directory = self.working_directory();
let executor = cx.background_executor().clone();
async move {
@@ -1239,7 +1239,7 @@ impl GitRepository for RealGitRepository {
ask_pass: AskPassDelegate,
env: Arc<HashMap<String, String>>,
cx: AsyncApp,
) -> BoxFuture<'_, Result<RemoteCommandOutput>> {
) -> BoxFuture<Result<RemoteCommandOutput>> {
let working_directory = self.working_directory();
let remote_name = format!("{}", fetch_options);
let executor = cx.background_executor().clone();
@@ -1257,7 +1257,7 @@ impl GitRepository for RealGitRepository {
.boxed()
}
fn get_remotes(&self, branch_name: Option<String>) -> BoxFuture<'_, Result<Vec<Remote>>> {
fn get_remotes(&self, branch_name: Option<String>) -> BoxFuture<Result<Vec<Remote>>> {
let working_directory = self.working_directory();
let git_binary_path = self.git_binary_path.clone();
self.executor
@@ -1303,7 +1303,7 @@ impl GitRepository for RealGitRepository {
.boxed()
}
fn check_for_pushed_commit(&self) -> BoxFuture<'_, Result<Vec<SharedString>>> {
fn check_for_pushed_commit(&self) -> BoxFuture<Result<Vec<SharedString>>> {
let working_directory = self.working_directory();
let git_binary_path = self.git_binary_path.clone();
self.executor
@@ -1396,7 +1396,7 @@ impl GitRepository for RealGitRepository {
.boxed()
}
fn restore_checkpoint(&self, checkpoint: GitRepositoryCheckpoint) -> BoxFuture<'_, Result<()>> {
fn restore_checkpoint(&self, checkpoint: GitRepositoryCheckpoint) -> BoxFuture<Result<()>> {
let working_directory = self.working_directory();
let git_binary_path = self.git_binary_path.clone();
@@ -1435,7 +1435,7 @@ impl GitRepository for RealGitRepository {
&self,
left: GitRepositoryCheckpoint,
right: GitRepositoryCheckpoint,
) -> BoxFuture<'_, Result<bool>> {
) -> BoxFuture<Result<bool>> {
let working_directory = self.working_directory();
let git_binary_path = self.git_binary_path.clone();
@@ -1474,7 +1474,7 @@ impl GitRepository for RealGitRepository {
&self,
base_checkpoint: GitRepositoryCheckpoint,
target_checkpoint: GitRepositoryCheckpoint,
) -> BoxFuture<'_, Result<String>> {
) -> BoxFuture<Result<String>> {
let working_directory = self.working_directory();
let git_binary_path = self.git_binary_path.clone();

View File

@@ -27,12 +27,11 @@ use git::status::StageStatus;
use git::{Amend, ToggleStaged, repository::RepoPath, status::FileStatus};
use git::{ExpandCommitEditor, RestoreTrackedFiles, StageAll, TrashUntrackedFiles, UnstageAll};
use gpui::{
Action, Animation, AnimationExt as _, AsyncApp, AsyncWindowContext, Axis, ClickEvent, Corner,
DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, KeyContext,
ListHorizontalSizingBehavior, ListSizingBehavior, Modifiers, ModifiersChangedEvent,
MouseButton, MouseDownEvent, Point, PromptLevel, ScrollStrategy, Subscription, Task,
Transformation, UniformListScrollHandle, WeakEntity, actions, anchored, deferred, percentage,
uniform_list,
Action, Animation, AnimationExt as _, Axis, ClickEvent, Corner, DismissEvent, Entity,
EventEmitter, FocusHandle, Focusable, KeyContext, ListHorizontalSizingBehavior,
ListSizingBehavior, Modifiers, ModifiersChangedEvent, MouseButton, MouseDownEvent, Point,
PromptLevel, ScrollStrategy, Subscription, Task, Transformation, UniformListScrollHandle,
WeakEntity, actions, anchored, deferred, percentage, uniform_list,
};
use itertools::Itertools;
use language::{Buffer, File};
@@ -64,11 +63,11 @@ use ui::{
Tooltip, prelude::*,
};
use util::{ResultExt, TryFutureExt, maybe};
use workspace::AppState;
use workspace::{
Workspace,
dock::{DockPosition, Panel, PanelEvent},
notifications::{DetachAndPromptErr, ErrorMessagePrompt, NotificationId},
notifications::DetachAndPromptErr,
};
use zed_llm_client::CompletionIntent;
@@ -390,148 +389,144 @@ pub(crate) fn commit_message_editor(
}
impl GitPanel {
fn new(
workspace: &mut Workspace,
pub fn new(
workspace: Entity<Workspace>,
project: Entity<Project>,
app_state: Arc<AppState>,
window: &mut Window,
cx: &mut Context<Workspace>,
) -> Entity<Self> {
let project = workspace.project().clone();
let app_state = workspace.app_state().clone();
cx: &mut Context<Self>,
) -> Self {
let fs = app_state.fs.clone();
let git_store = project.read(cx).git_store().clone();
let active_repository = project.read(cx).active_repository(cx);
let workspace = workspace.downgrade();
let git_panel = cx.new(|cx| {
let focus_handle = cx.focus_handle();
cx.on_focus(&focus_handle, window, Self::focus_in).detach();
cx.on_focus_out(&focus_handle, window, |this, _, window, cx| {
this.hide_scrollbars(window, cx);
})
.detach();
let focus_handle = cx.focus_handle();
cx.on_focus(&focus_handle, window, Self::focus_in).detach();
cx.on_focus_out(&focus_handle, window, |this, _, window, cx| {
this.hide_scrollbars(window, cx);
})
.detach();
let mut was_sort_by_path = GitPanelSettings::get_global(cx).sort_by_path;
cx.observe_global::<SettingsStore>(move |this, cx| {
let is_sort_by_path = GitPanelSettings::get_global(cx).sort_by_path;
if is_sort_by_path != was_sort_by_path {
this.update_visible_entries(cx);
}
was_sort_by_path = is_sort_by_path
})
.detach();
let mut was_sort_by_path = GitPanelSettings::get_global(cx).sort_by_path;
cx.observe_global::<SettingsStore>(move |this, cx| {
let is_sort_by_path = GitPanelSettings::get_global(cx).sort_by_path;
if is_sort_by_path != was_sort_by_path {
this.update_visible_entries(cx);
}
was_sort_by_path = is_sort_by_path
})
.detach();
// just to let us render a placeholder editor.
// Once the active git repo is set, this buffer will be replaced.
let temporary_buffer = cx.new(|cx| Buffer::local("", cx));
let commit_editor = cx.new(|cx| {
commit_message_editor(temporary_buffer, None, project.clone(), true, window, cx)
});
commit_editor.update(cx, |editor, cx| {
editor.clear(window, cx);
});
let scroll_handle = UniformListScrollHandle::new();
let vertical_scrollbar = ScrollbarProperties {
axis: Axis::Vertical,
state: ScrollbarState::new(scroll_handle.clone()).parent_entity(&cx.entity()),
show_scrollbar: false,
show_track: false,
auto_hide: false,
hide_task: None,
};
let horizontal_scrollbar = ScrollbarProperties {
axis: Axis::Horizontal,
state: ScrollbarState::new(scroll_handle.clone()).parent_entity(&cx.entity()),
show_scrollbar: false,
show_track: false,
auto_hide: false,
hide_task: None,
};
let mut assistant_enabled = AgentSettings::get_global(cx).enabled;
let _settings_subscription = cx.observe_global::<SettingsStore>(move |_, cx| {
if assistant_enabled != AgentSettings::get_global(cx).enabled {
assistant_enabled = AgentSettings::get_global(cx).enabled;
cx.notify();
}
});
cx.subscribe_in(
&git_store,
window,
move |this, _git_store, event, window, cx| match event {
GitStoreEvent::ActiveRepositoryChanged(_) => {
this.active_repository = this.project.read(cx).active_repository(cx);
this.schedule_update(true, window, cx);
}
GitStoreEvent::RepositoryUpdated(
_,
RepositoryEvent::Updated { full_scan },
true,
) => {
this.schedule_update(*full_scan, window, cx);
}
GitStoreEvent::RepositoryAdded(_) | GitStoreEvent::RepositoryRemoved(_) => {
this.schedule_update(false, window, cx);
}
GitStoreEvent::IndexWriteError(error) => {
this.workspace
.update(cx, |workspace, cx| {
workspace.show_error(error, cx);
})
.ok();
}
GitStoreEvent::RepositoryUpdated(_, _, _) => {}
GitStoreEvent::JobsUpdated | GitStoreEvent::ConflictsUpdated => {}
},
)
.detach();
let mut this = Self {
active_repository,
commit_editor,
conflicted_count: 0,
conflicted_staged_count: 0,
current_modifiers: window.modifiers(),
add_coauthors: true,
generate_commit_message_task: None,
entries: Vec::new(),
focus_handle: cx.focus_handle(),
fs,
new_count: 0,
new_staged_count: 0,
pending: Vec::new(),
pending_commit: None,
amend_pending: false,
pending_serialization: Task::ready(None),
single_staged_entry: None,
single_tracked_entry: None,
project,
scroll_handle,
max_width_item_index: None,
selected_entry: None,
marked_entries: Vec::new(),
tracked_count: 0,
tracked_staged_count: 0,
update_visible_entries_task: Task::ready(()),
width: None,
show_placeholders: false,
context_menu: None,
workspace: workspace.weak_handle(),
modal_open: false,
entry_count: 0,
horizontal_scrollbar,
vertical_scrollbar,
_settings_subscription,
};
this.schedule_update(false, window, cx);
this
// just to let us render a placeholder editor.
// Once the active git repo is set, this buffer will be replaced.
let temporary_buffer = cx.new(|cx| Buffer::local("", cx));
let commit_editor = cx.new(|cx| {
commit_message_editor(temporary_buffer, None, project.clone(), true, window, cx)
});
commit_editor.update(cx, |editor, cx| {
editor.clear(window, cx);
});
let scroll_handle = UniformListScrollHandle::new();
cx.subscribe_in(
&git_store,
window,
move |this, git_store, event, window, cx| match event {
GitStoreEvent::ActiveRepositoryChanged(_) => {
this.active_repository = git_store.read(cx).active_repository();
this.schedule_update(true, window, cx);
}
GitStoreEvent::RepositoryUpdated(
_,
RepositoryEvent::Updated { full_scan },
true,
) => {
this.schedule_update(*full_scan, window, cx);
}
GitStoreEvent::RepositoryAdded(_) | GitStoreEvent::RepositoryRemoved(_) => {
this.schedule_update(false, window, cx);
}
GitStoreEvent::IndexWriteError(error) => {
this.workspace
.update(cx, |workspace, cx| {
workspace.show_error(error, cx);
})
.ok();
}
GitStoreEvent::RepositoryUpdated(_, _, _) => {}
GitStoreEvent::JobsUpdated | GitStoreEvent::ConflictsUpdated => {}
},
)
.detach();
let vertical_scrollbar = ScrollbarProperties {
axis: Axis::Vertical,
state: ScrollbarState::new(scroll_handle.clone()).parent_entity(&cx.entity()),
show_scrollbar: false,
show_track: false,
auto_hide: false,
hide_task: None,
};
let horizontal_scrollbar = ScrollbarProperties {
axis: Axis::Horizontal,
state: ScrollbarState::new(scroll_handle.clone()).parent_entity(&cx.entity()),
show_scrollbar: false,
show_track: false,
auto_hide: false,
hide_task: None,
};
let mut assistant_enabled = AgentSettings::get_global(cx).enabled;
let _settings_subscription = cx.observe_global::<SettingsStore>(move |_, cx| {
if assistant_enabled != AgentSettings::get_global(cx).enabled {
assistant_enabled = AgentSettings::get_global(cx).enabled;
cx.notify();
}
});
let mut git_panel = Self {
active_repository,
commit_editor,
conflicted_count: 0,
conflicted_staged_count: 0,
current_modifiers: window.modifiers(),
add_coauthors: true,
generate_commit_message_task: None,
entries: Vec::new(),
focus_handle: cx.focus_handle(),
fs,
new_count: 0,
new_staged_count: 0,
pending: Vec::new(),
pending_commit: None,
amend_pending: false,
pending_serialization: Task::ready(None),
single_staged_entry: None,
single_tracked_entry: None,
project,
scroll_handle,
max_width_item_index: None,
selected_entry: None,
marked_entries: Vec::new(),
tracked_count: 0,
tracked_staged_count: 0,
update_visible_entries_task: Task::ready(()),
width: None,
show_placeholders: false,
context_menu: None,
workspace,
modal_open: false,
entry_count: 0,
horizontal_scrollbar,
vertical_scrollbar,
_settings_subscription,
};
git_panel.schedule_update(false, window, cx);
git_panel
}
@@ -1779,19 +1774,7 @@ impl GitPanel {
this.generate_commit_message_task.take();
});
let mut diff_text = match diff.await {
Ok(result) => match result {
Ok(text) => text,
Err(e) => {
Self::show_commit_message_error(&this, &e, cx);
return anyhow::Ok(());
}
},
Err(e) => {
Self::show_commit_message_error(&this, &e, cx);
return anyhow::Ok(());
}
};
let mut diff_text = diff.await??;
const ONE_MB: usize = 1_000_000;
if diff_text.len() > ONE_MB {
@@ -1829,37 +1812,26 @@ impl GitPanel {
};
let stream = model.stream_completion_text(request, &cx);
match stream.await {
Ok(mut messages) => {
if !text_empty {
this.update(cx, |this, cx| {
this.commit_message_buffer(cx).update(cx, |buffer, cx| {
let insert_position = buffer.anchor_before(buffer.len());
buffer.edit([(insert_position..insert_position, "\n")], None, cx)
});
})?;
}
let mut messages = stream.await?;
while let Some(message) = messages.stream.next().await {
match message {
Ok(text) => {
this.update(cx, |this, cx| {
this.commit_message_buffer(cx).update(cx, |buffer, cx| {
let insert_position = buffer.anchor_before(buffer.len());
buffer.edit([(insert_position..insert_position, text)], None, cx);
});
})?;
}
Err(e) => {
Self::show_commit_message_error(&this, &e, cx);
break;
}
}
}
}
Err(e) => {
Self::show_commit_message_error(&this, &e, cx);
}
if !text_empty {
this.update(cx, |this, cx| {
this.commit_message_buffer(cx).update(cx, |buffer, cx| {
let insert_position = buffer.anchor_before(buffer.len());
buffer.edit([(insert_position..insert_position, "\n")], None, cx)
});
})?;
}
while let Some(message) = messages.stream.next().await {
let text = message?;
this.update(cx, |this, cx| {
this.commit_message_buffer(cx).update(cx, |buffer, cx| {
let insert_position = buffer.anchor_before(buffer.len());
buffer.edit([(insert_position..insert_position, text)], None, cx);
});
})?;
}
anyhow::Ok(())
@@ -2717,26 +2689,6 @@ impl GitPanel {
}
}
fn show_commit_message_error<E>(weak_this: &WeakEntity<Self>, err: &E, cx: &mut AsyncApp)
where
E: std::fmt::Debug + std::fmt::Display,
{
if let Ok(Some(workspace)) = weak_this.update(cx, |this, _cx| this.workspace.upgrade()) {
let _ = workspace.update(cx, |workspace, cx| {
struct CommitMessageError;
let notification_id = NotificationId::unique::<CommitMessageError>();
workspace.show_notification(notification_id, cx, |cx| {
cx.new(|cx| {
ErrorMessagePrompt::new(
format!("Failed to generate commit message: {err}"),
cx,
)
})
});
});
}
}
fn show_remote_output(&self, action: RemoteAction, info: RemoteCommandOutput, cx: &mut App) {
let Some(workspace) = self.workspace.upgrade() else {
return;
@@ -4189,32 +4141,6 @@ impl GitPanel {
self.amend_pending = value;
cx.notify();
}
pub async fn load(
workspace: WeakEntity<Workspace>,
mut cx: AsyncWindowContext,
) -> anyhow::Result<Entity<Self>> {
let serialized_panel = cx
.background_spawn(async move { KEY_VALUE_STORE.read_kvp(&GIT_PANEL_KEY) })
.await
.context("loading git panel")
.log_err()
.flatten()
.and_then(|panel| serde_json::from_str::<SerializedGitPanel>(&panel).log_err());
workspace.update_in(&mut cx, |workspace, window, cx| {
let panel = GitPanel::new(workspace, window, cx);
if let Some(serialized_panel) = serialized_panel {
panel.update(cx, |panel, cx| {
panel.width = serialized_panel.width;
cx.notify();
})
}
panel
})
}
}
fn current_language_model(cx: &Context<'_, GitPanel>) -> Option<Arc<dyn LanguageModel>> {
@@ -4926,7 +4852,7 @@ impl Component for PanelRepoFooter {
#[cfg(test)]
mod tests {
use git::status::StatusCode;
use gpui::{TestAppContext, VisualTestContext};
use gpui::TestAppContext;
use project::{FakeFs, WorktreeSettings};
use serde_json::json;
use settings::SettingsStore;
@@ -4990,9 +4916,8 @@ mod tests {
let project =
Project::test(fs.clone(), [path!("/root/zed/crates/gpui").as_ref()], cx).await;
let workspace =
cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
let cx = &mut VisualTestContext::from_window(*workspace, cx);
let (workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
cx.read(|cx| {
project
@@ -5009,7 +4934,10 @@ mod tests {
cx.executor().run_until_parked();
let panel = workspace.update(cx, GitPanel::new).unwrap();
let app_state = workspace.read_with(cx, |workspace, _| workspace.app_state().clone());
let panel = cx.new_window_entity(|window, cx| {
GitPanel::new(workspace.clone(), project.clone(), app_state, window, cx)
});
let handle = cx.update_window_entity(&panel, |panel, _, _| {
std::mem::replace(&mut panel.update_visible_entries_task, Task::ready(()))

View File

@@ -64,7 +64,7 @@ pub struct AppCell {
impl AppCell {
#[doc(hidden)]
#[track_caller]
pub fn borrow(&self) -> AppRef<'_> {
pub fn borrow(&self) -> AppRef {
if option_env!("TRACK_THREAD_BORROWS").is_some() {
let thread_id = std::thread::current().id();
eprintln!("borrowed {thread_id:?}");
@@ -74,7 +74,7 @@ impl AppCell {
#[doc(hidden)]
#[track_caller]
pub fn borrow_mut(&self) -> AppRefMut<'_> {
pub fn borrow_mut(&self) -> AppRefMut {
if option_env!("TRACK_THREAD_BORROWS").is_some() {
let thread_id = std::thread::current().id();
eprintln!("borrowed {thread_id:?}");
@@ -84,7 +84,7 @@ impl AppCell {
#[doc(hidden)]
#[track_caller]
pub fn try_borrow_mut(&self) -> Result<AppRefMut<'_>, BorrowMutError> {
pub fn try_borrow_mut(&self) -> Result<AppRefMut, BorrowMutError> {
if option_env!("TRACK_THREAD_BORROWS").is_some() {
let thread_id = std::thread::current().id();
eprintln!("borrowed {thread_id:?}");

View File

@@ -718,7 +718,7 @@ impl<T> ops::Index<usize> for AtlasTextureList<T> {
impl<T> AtlasTextureList<T> {
#[allow(unused)]
fn drain(&mut self) -> std::vec::Drain<'_, Option<T>> {
fn drain(&mut self) -> std::vec::Drain<Option<T>> {
self.free_list.clear();
self.textures.drain(..)
}

View File

@@ -25,7 +25,7 @@ pub(crate) const ESCAPE_KEY: u16 = 0x1b;
const TAB_KEY: u16 = 0x09;
const SHIFT_TAB_KEY: u16 = 0x19;
pub fn key_to_native(key: &str) -> Cow<'_, str> {
pub fn key_to_native(key: &str) -> Cow<str> {
use cocoa::appkit::*;
let code = match key {
"space" => SPACE_KEY,

View File

@@ -149,7 +149,7 @@ impl Scene {
),
allow(dead_code)
)]
pub(crate) fn batches(&self) -> impl Iterator<Item = PrimitiveBatch<'_>> {
pub(crate) fn batches(&self) -> impl Iterator<Item = PrimitiveBatch> {
BatchIterator {
shadows: &self.shadows,
shadows_start: 0,

View File

@@ -616,7 +616,7 @@ impl Hash for (dyn AsCacheKeyRef + '_) {
}
impl AsCacheKeyRef for CacheKey {
fn as_cache_key_ref(&self) -> CacheKeyRef<'_> {
fn as_cache_key_ref(&self) -> CacheKeyRef {
CacheKeyRef {
text: &self.text,
font_size: self.font_size,
@@ -645,7 +645,7 @@ impl<'a> Borrow<dyn AsCacheKeyRef + 'a> for Arc<CacheKey> {
}
impl AsCacheKeyRef for CacheKeyRef<'_> {
fn as_cache_key_ref(&self) -> CacheKeyRef<'_> {
fn as_cache_key_ref(&self) -> CacheKeyRef {
*self
}
}

View File

@@ -1874,12 +1874,9 @@ impl Buffer {
}
/// Ensures that the buffer ends with a single newline character, and
/// no other whitespace. Skips if the buffer is empty.
/// no other whitespace.
pub fn ensure_final_newline(&mut self, cx: &mut Context<Self>) {
let len = self.len();
if len == 0 {
return;
}
let mut offset = len;
for chunk in self.as_rope().reversed_chunks_in_range(0..len) {
let non_whitespace_len = chunk
@@ -3130,7 +3127,7 @@ impl BufferSnapshot {
None
}
fn get_highlights(&self, range: Range<usize>) -> (SyntaxMapCaptures<'_>, Vec<HighlightMap>) {
fn get_highlights(&self, range: Range<usize>) -> (SyntaxMapCaptures, Vec<HighlightMap>) {
let captures = self.syntax.captures(range, &self.text, |grammar| {
grammar.highlights_query.as_ref()
});
@@ -3146,7 +3143,7 @@ impl BufferSnapshot {
/// in an arbitrary way due to being stored in a [`Rope`](text::Rope). The text is also
/// returned in chunks where each chunk has a single syntax highlighting style and
/// diagnostic status.
pub fn chunks<T: ToOffset>(&self, range: Range<T>, language_aware: bool) -> BufferChunks<'_> {
pub fn chunks<T: ToOffset>(&self, range: Range<T>, language_aware: bool) -> BufferChunks {
let range = range.start.to_offset(self)..range.end.to_offset(self);
let mut syntax = None;
@@ -3195,12 +3192,12 @@ impl BufferSnapshot {
}
/// Iterates over every [`SyntaxLayer`] in the buffer.
pub fn syntax_layers(&self) -> impl Iterator<Item = SyntaxLayer<'_>> + '_ {
pub fn syntax_layers(&self) -> impl Iterator<Item = SyntaxLayer> + '_ {
self.syntax
.layers_for_range(0..self.len(), &self.text, true)
}
pub fn syntax_layer_at<D: ToOffset>(&self, position: D) -> Option<SyntaxLayer<'_>> {
pub fn syntax_layer_at<D: ToOffset>(&self, position: D) -> Option<SyntaxLayer> {
let offset = position.to_offset(self);
self.syntax
.layers_for_range(offset..offset, &self.text, false)
@@ -3211,7 +3208,7 @@ impl BufferSnapshot {
pub fn smallest_syntax_layer_containing<D: ToOffset>(
&self,
range: Range<D>,
) -> Option<SyntaxLayer<'_>> {
) -> Option<SyntaxLayer> {
let range = range.to_offset(self);
return self
.syntax
@@ -3429,7 +3426,7 @@ impl BufferSnapshot {
}
/// Returns the root syntax node within the given row
pub fn syntax_root_ancestor(&self, position: Anchor) -> Option<tree_sitter::Node<'_>> {
pub fn syntax_root_ancestor(&self, position: Anchor) -> Option<tree_sitter::Node> {
let start_offset = position.to_offset(self);
let row = self.summary_for_anchor::<text::PointUtf16>(&position).row as usize;
@@ -3766,7 +3763,7 @@ impl BufferSnapshot {
&self,
range: Range<usize>,
query: fn(&Grammar) -> Option<&tree_sitter::Query>,
) -> SyntaxMapMatches<'_> {
) -> SyntaxMapMatches {
self.syntax.matches(range, self, query)
}

View File

@@ -1126,7 +1126,7 @@ impl<'a> SyntaxMapMatches<'a> {
&self.grammars
}
pub fn peek(&self) -> Option<SyntaxMapMatch<'_>> {
pub fn peek(&self) -> Option<SyntaxMapMatch> {
let layer = self.layers.first()?;
if !layer.has_next {
@@ -1550,7 +1550,7 @@ fn insert_newlines_between_ranges(
impl OwnedSyntaxLayer {
/// Returns the root syntax node for this layer.
pub fn node(&self) -> Node<'_> {
pub fn node(&self) -> Node {
self.tree
.root_node_with_offset(self.offset.0, self.offset.1)
}

View File

@@ -863,13 +863,6 @@ impl Render for ConfigurationView {
copilot::initiate_sign_in(window, cx)
})),
)
.child(
Label::new(
format!("You can also assign the {} environment variable and restart Zed.", copilot::copilot_chat::COPILOT_OAUTH_ENV_VAR),
)
.size(LabelSize::Small)
.color(Color::Muted),
)
}
},
None => v_flex().gap_6().child(Label::new(ERROR_LABEL)),

View File

@@ -372,15 +372,15 @@ pub fn into_deepseek(
for message in request.messages {
for content in message.content {
match content {
MessageContent::Text(text) => messages.push(match message.role {
Role::User => deepseek::RequestMessage::User { content: text },
Role::Assistant => deepseek::RequestMessage::Assistant {
content: Some(text),
tool_calls: Vec::new(),
},
Role::System => deepseek::RequestMessage::System { content: text },
}),
MessageContent::Thinking { .. } => {}
MessageContent::Text(text) | MessageContent::Thinking { text, .. } => messages
.push(match message.role {
Role::User => deepseek::RequestMessage::User { content: text },
Role::Assistant => deepseek::RequestMessage::Assistant {
content: Some(text),
tool_calls: Vec::new(),
},
Role::System => deepseek::RequestMessage::System { content: text },
}),
MessageContent::RedactedThinking(_) => {}
MessageContent::Image(_) => {}
MessageContent::ToolUse(tool_use) => {
@@ -485,13 +485,6 @@ impl DeepSeekEventMapper {
events.push(Ok(LanguageModelCompletionEvent::Text(content)));
}
if let Some(reasoning_content) = choice.delta.reasoning_content.clone() {
events.push(Ok(LanguageModelCompletionEvent::Thinking {
text: reasoning_content,
signature: None,
}));
}
if let Some(tool_calls) = choice.delta.tool_calls.as_ref() {
for tool_call in tool_calls {
let entry = self.tool_calls_by_index.entry(tool_call.index).or_default();

View File

@@ -250,15 +250,15 @@ impl LmStudioLanguageModel {
for message in request.messages {
for content in message.content {
match content {
MessageContent::Text(text) => messages.push(match message.role {
Role::User => ChatMessage::User { content: text },
Role::Assistant => ChatMessage::Assistant {
content: Some(text),
tool_calls: Vec::new(),
},
Role::System => ChatMessage::System { content: text },
}),
MessageContent::Thinking { .. } => {}
MessageContent::Text(text) | MessageContent::Thinking { text, .. } => messages
.push(match message.role {
Role::User => ChatMessage::User { content: text },
Role::Assistant => ChatMessage::Assistant {
content: Some(text),
tool_calls: Vec::new(),
},
Role::System => ChatMessage::System { content: text },
}),
MessageContent::RedactedThinking(_) => {}
MessageContent::Image(_) => {}
MessageContent::ToolUse(tool_use) => {
@@ -471,13 +471,6 @@ impl LmStudioEventMapper {
events.push(Ok(LanguageModelCompletionEvent::Text(content)));
}
if let Some(reasoning_content) = choice.delta.reasoning_content {
events.push(Ok(LanguageModelCompletionEvent::Thinking {
text: reasoning_content,
signature: None,
}));
}
if let Some(tool_calls) = choice.delta.tool_calls {
for tool_call in tool_calls {
let entry = self.tool_calls_by_index.entry(tool_call.index).or_default();

View File

@@ -18,8 +18,6 @@ use language_model::{
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsStore};
use std::collections::HashMap;
use std::pin::Pin;
use std::str::FromStr;
use std::sync::Arc;
use strum::IntoEnumIterator;
@@ -29,6 +27,9 @@ use util::ResultExt;
use crate::{AllLanguageModelSettings, ui::InstructionListItem};
use std::collections::HashMap;
use std::pin::Pin;
const PROVIDER_ID: &str = "mistral";
const PROVIDER_NAME: &str = "Mistral";
@@ -47,7 +48,6 @@ pub struct AvailableModel {
pub max_output_tokens: Option<u32>,
pub max_completion_tokens: Option<u32>,
pub supports_tools: Option<bool>,
pub supports_images: Option<bool>,
}
pub struct MistralLanguageModelProvider {
@@ -215,7 +215,6 @@ impl LanguageModelProvider for MistralLanguageModelProvider {
max_output_tokens: model.max_output_tokens,
max_completion_tokens: model.max_completion_tokens,
supports_tools: model.supports_tools,
supports_images: model.supports_images,
},
);
}
@@ -315,7 +314,7 @@ impl LanguageModel for MistralLanguageModel {
}
fn supports_images(&self) -> bool {
self.model.supports_images()
false
}
fn telemetry_id(&self) -> String {
@@ -390,113 +389,58 @@ pub fn into_mistral(
let stream = true;
let mut messages = Vec::new();
for message in &request.messages {
match message.role {
Role::User => {
let mut message_content = mistral::MessageContent::empty();
for content in &message.content {
match content {
MessageContent::Text(text) => {
message_content
.push_part(mistral::MessagePart::Text { text: text.clone() });
}
MessageContent::Image(image_content) => {
message_content.push_part(mistral::MessagePart::ImageUrl {
image_url: image_content.to_base64_url(),
});
}
MessageContent::Thinking { text, .. } => {
message_content
.push_part(mistral::MessagePart::Text { text: text.clone() });
}
MessageContent::RedactedThinking(_) => {}
MessageContent::ToolUse(_) | MessageContent::ToolResult(_) => {
// Tool content is not supported in User messages for Mistral
}
for message in request.messages {
for content in message.content {
match content {
MessageContent::Text(text) | MessageContent::Thinking { text, .. } => messages
.push(match message.role {
Role::User => mistral::RequestMessage::User { content: text },
Role::Assistant => mistral::RequestMessage::Assistant {
content: Some(text),
tool_calls: Vec::new(),
},
Role::System => mistral::RequestMessage::System { content: text },
}),
MessageContent::RedactedThinking(_) => {}
MessageContent::Image(_) => {}
MessageContent::ToolUse(tool_use) => {
let tool_call = mistral::ToolCall {
id: tool_use.id.to_string(),
content: mistral::ToolCallContent::Function {
function: mistral::FunctionContent {
name: tool_use.name.to_string(),
arguments: serde_json::to_string(&tool_use.input)
.unwrap_or_default(),
},
},
};
if let Some(mistral::RequestMessage::Assistant { tool_calls, .. }) =
messages.last_mut()
{
tool_calls.push(tool_call);
} else {
messages.push(mistral::RequestMessage::Assistant {
content: None,
tool_calls: vec![tool_call],
});
}
}
if !matches!(message_content, mistral::MessageContent::Plain { ref content } if content.is_empty())
{
messages.push(mistral::RequestMessage::User {
content: message_content,
MessageContent::ToolResult(tool_result) => {
let content = match &tool_result.content {
LanguageModelToolResultContent::Text(text) => text.to_string(),
LanguageModelToolResultContent::Image(_) => {
// TODO: Mistral image support
"[Tool responded with an image, but Zed doesn't support these in Mistral models yet]".to_string()
}
};
messages.push(mistral::RequestMessage::Tool {
content,
tool_call_id: tool_result.tool_use_id.to_string(),
});
}
}
Role::Assistant => {
for content in &message.content {
match content {
MessageContent::Text(text) | MessageContent::Thinking { text, .. } => {
messages.push(mistral::RequestMessage::Assistant {
content: Some(text.clone()),
tool_calls: Vec::new(),
});
}
MessageContent::RedactedThinking(_) => {}
MessageContent::Image(_) => {}
MessageContent::ToolUse(tool_use) => {
let tool_call = mistral::ToolCall {
id: tool_use.id.to_string(),
content: mistral::ToolCallContent::Function {
function: mistral::FunctionContent {
name: tool_use.name.to_string(),
arguments: serde_json::to_string(&tool_use.input)
.unwrap_or_default(),
},
},
};
if let Some(mistral::RequestMessage::Assistant { tool_calls, .. }) =
messages.last_mut()
{
tool_calls.push(tool_call);
} else {
messages.push(mistral::RequestMessage::Assistant {
content: None,
tool_calls: vec![tool_call],
});
}
}
MessageContent::ToolResult(_) => {
// Tool results are not supported in Assistant messages
}
}
}
}
Role::System => {
for content in &message.content {
match content {
MessageContent::Text(text) | MessageContent::Thinking { text, .. } => {
messages.push(mistral::RequestMessage::System {
content: text.clone(),
});
}
MessageContent::RedactedThinking(_) => {}
MessageContent::Image(_)
| MessageContent::ToolUse(_)
| MessageContent::ToolResult(_) => {
// Images and tools are not supported in System messages
}
}
}
}
}
}
for message in &request.messages {
for content in &message.content {
if let MessageContent::ToolResult(tool_result) = content {
let content = match &tool_result.content {
LanguageModelToolResultContent::Text(text) => text.to_string(),
LanguageModelToolResultContent::Image(_) => {
"[Tool responded with an image, but Zed doesn't support these in Mistral models yet]".to_string()
}
};
messages.push(mistral::RequestMessage::Tool {
content,
tool_call_id: tool_result.tool_use_id.to_string(),
});
}
}
}
@@ -875,88 +819,62 @@ impl Render for ConfigurationView {
#[cfg(test)]
mod tests {
use super::*;
use language_model::{LanguageModelImage, LanguageModelRequestMessage, MessageContent};
use language_model;
#[test]
fn test_into_mistral_basic_conversion() {
let request = LanguageModelRequest {
fn test_into_mistral_conversion() {
let request = language_model::LanguageModelRequest {
messages: vec![
LanguageModelRequestMessage {
role: Role::System,
content: vec![MessageContent::Text("System prompt".into())],
language_model::LanguageModelRequestMessage {
role: language_model::Role::System,
content: vec![language_model::MessageContent::Text(
"You are a helpful assistant.".to_string(),
)],
cache: false,
},
LanguageModelRequestMessage {
role: Role::User,
content: vec![MessageContent::Text("Hello".into())],
language_model::LanguageModelRequestMessage {
role: language_model::Role::User,
content: vec![language_model::MessageContent::Text(
"Hello, how are you?".to_string(),
)],
cache: false,
},
],
temperature: Some(0.5),
tools: vec![],
temperature: Some(0.7),
tools: Vec::new(),
tool_choice: None,
thread_id: None,
prompt_id: None,
intent: None,
mode: None,
stop: vec![],
stop: Vec::new(),
};
let mistral_request = into_mistral(request, "mistral-small-latest".into(), None);
let model_name = "mistral-medium-latest".to_string();
let max_output_tokens = Some(1000);
let mistral_request = into_mistral(request, model_name, max_output_tokens);
assert_eq!(mistral_request.model, "mistral-small-latest");
assert_eq!(mistral_request.temperature, Some(0.5));
assert_eq!(mistral_request.messages.len(), 2);
assert_eq!(mistral_request.model, "mistral-medium-latest");
assert_eq!(mistral_request.temperature, Some(0.7));
assert_eq!(mistral_request.max_tokens, Some(1000));
assert!(mistral_request.stream);
}
assert!(mistral_request.tools.is_empty());
assert!(mistral_request.tool_choice.is_none());
#[test]
fn test_into_mistral_with_image() {
let request = LanguageModelRequest {
messages: vec![LanguageModelRequestMessage {
role: Role::User,
content: vec![
MessageContent::Text("What's in this image?".into()),
MessageContent::Image(LanguageModelImage {
source: "base64data".into(),
size: Default::default(),
}),
],
cache: false,
}],
tools: vec![],
tool_choice: None,
temperature: None,
thread_id: None,
prompt_id: None,
intent: None,
mode: None,
stop: vec![],
};
assert_eq!(mistral_request.messages.len(), 2);
let mistral_request = into_mistral(request, "pixtral-12b-latest".into(), None);
assert_eq!(mistral_request.messages.len(), 1);
assert!(matches!(
&mistral_request.messages[0],
mistral::RequestMessage::User {
content: mistral::MessageContent::Multipart { .. }
match &mistral_request.messages[0] {
mistral::RequestMessage::System { content } => {
assert_eq!(content, "You are a helpful assistant.");
}
));
_ => panic!("Expected System message"),
}
if let mistral::RequestMessage::User {
content: mistral::MessageContent::Multipart { content },
} = &mistral_request.messages[0]
{
assert_eq!(content.len(), 2);
assert!(matches!(
&content[0],
mistral::MessagePart::Text { text } if text == "What's in this image?"
));
assert!(matches!(
&content[1],
mistral::MessagePart::ImageUrl { image_url } if image_url.starts_with("data:image/png;base64,")
));
match &mistral_request.messages[1] {
mistral::RequestMessage::User { content } => {
assert_eq!(content, "Hello, how are you?");
}
_ => panic!("Expected User message"),
}
}
}

View File

@@ -80,21 +80,4 @@
)
) @item
; Add support for parameterized tests
(
(call_expression
function: (call_expression
function: (member_expression
object: [(identifier) @_name (member_expression object: (identifier) @_name)]
property: (property_identifier) @_property
)
(#any-of? @_name "it" "test" "describe" "context" "suite")
(#eq? @_property "each")
)
arguments: (
arguments . (string (string_fragment) @name)
)
)
) @item
(comment) @annotation

View File

@@ -19,22 +19,3 @@
(#set! tag js-test)
)
; Add support for parameterized tests
(
(call_expression
function: (call_expression
function: (member_expression
object: [(identifier) @_name (member_expression object: (identifier) @_name)]
property: (property_identifier) @_property
)
(#any-of? @_name "it" "test" "describe" "context" "suite")
(#eq? @_property "each")
)
arguments: (
arguments . (string (string_fragment) @run)
)
) @_js-test
(#set! tag js-test)
)

View File

@@ -34,15 +34,11 @@ const TYPESCRIPT_RUNNER_VARIABLE: VariableName =
VariableName::Custom(Cow::Borrowed("TYPESCRIPT_RUNNER"));
const TYPESCRIPT_JEST_TASK_VARIABLE: VariableName =
VariableName::Custom(Cow::Borrowed("TYPESCRIPT_JEST"));
const TYPESCRIPT_JEST_TEST_NAME_VARIABLE: VariableName =
VariableName::Custom(Cow::Borrowed("TYPESCRIPT_JEST_TEST_NAME"));
const TYPESCRIPT_MOCHA_TASK_VARIABLE: VariableName =
VariableName::Custom(Cow::Borrowed("TYPESCRIPT_MOCHA"));
const TYPESCRIPT_VITEST_TASK_VARIABLE: VariableName =
VariableName::Custom(Cow::Borrowed("TYPESCRIPT_VITEST"));
const TYPESCRIPT_VITEST_TEST_NAME_VARIABLE: VariableName =
VariableName::Custom(Cow::Borrowed("TYPESCRIPT_VITEST_TEST_NAME"));
const TYPESCRIPT_JASMINE_TASK_VARIABLE: VariableName =
VariableName::Custom(Cow::Borrowed("TYPESCRIPT_JASMINE"));
const TYPESCRIPT_BUILD_SCRIPT_TASK_VARIABLE: VariableName =
@@ -187,10 +183,7 @@ impl ContextProvider for TypeScriptContextProvider {
args: vec![
TYPESCRIPT_JEST_TASK_VARIABLE.template_value(),
"--testNamePattern".to_owned(),
format!(
"\"{}\"",
TYPESCRIPT_JEST_TEST_NAME_VARIABLE.template_value()
),
format!("\"{}\"", VariableName::Symbol.template_value()),
VariableName::RelativeFile.template_value(),
],
tags: vec![
@@ -228,7 +221,7 @@ impl ContextProvider for TypeScriptContextProvider {
TYPESCRIPT_VITEST_TASK_VARIABLE.template_value(),
"run".to_owned(),
"--testNamePattern".to_owned(),
format!("\"{}\"", TYPESCRIPT_VITEST_TASK_VARIABLE.template_value()),
format!("\"{}\"", VariableName::Symbol.template_value()),
VariableName::RelativeFile.template_value(),
],
tags: vec![
@@ -351,27 +344,14 @@ impl ContextProvider for TypeScriptContextProvider {
fn build_context(
&self,
current_vars: &task::TaskVariables,
_variables: &task::TaskVariables,
location: ContextLocation<'_>,
_project_env: Option<HashMap<String, String>>,
_toolchains: Arc<dyn LanguageToolchainStore>,
cx: &mut App,
) -> Task<Result<task::TaskVariables>> {
let mut vars = task::TaskVariables::default();
if let Some(symbol) = current_vars.get(&VariableName::Symbol) {
vars.insert(
TYPESCRIPT_JEST_TEST_NAME_VARIABLE,
replace_test_name_parameters(symbol),
);
vars.insert(
TYPESCRIPT_VITEST_TEST_NAME_VARIABLE,
replace_test_name_parameters(symbol),
);
}
let Some((fs, worktree_root)) = location.fs.zip(location.worktree_root) else {
return Task::ready(Ok(vars));
return Task::ready(Ok(task::TaskVariables::default()));
};
let package_json_contents = self.last_package_json.clone();
@@ -381,10 +361,7 @@ impl ContextProvider for TypeScriptContextProvider {
.context("package.json context retrieval")
.log_err()
.unwrap_or_else(task::TaskVariables::default);
vars.extend(variables);
Ok(vars)
Ok(variables)
})
}
}
@@ -449,12 +426,6 @@ fn eslint_server_binary_arguments(server_path: &Path) -> Vec<OsString> {
]
}
fn replace_test_name_parameters(test_name: &str) -> String {
let pattern = regex::Regex::new(r"(%|\$)[0-9a-zA-Z]+").unwrap();
pattern.replace_all(test_name, "(.+?)").to_string()
}
pub struct TypeScriptLspAdapter {
node: NodeRuntime,
}

View File

@@ -88,21 +88,4 @@
)
) @item
; Add support for parameterized tests
(
(call_expression
function: (call_expression
function: (member_expression
object: [(identifier) @_name (member_expression object: (identifier) @_name)]
property: (property_identifier) @_property
)
(#any-of? @_name "it" "test" "describe" "context" "suite")
(#any-of? @_property "each")
)
arguments: (
arguments . (string (string_fragment) @name)
)
)
) @item
(comment) @annotation

View File

@@ -19,22 +19,3 @@
(#set! tag js-test)
)
; Add support for parameterized tests
(
(call_expression
function: (call_expression
function: (member_expression
object: [(identifier) @_name (member_expression object: (identifier) @_name)]
property: (property_identifier) @_property
)
(#any-of? @_name "it" "test" "describe" "context" "suite")
(#any-of? @_property "each")
)
arguments: (
arguments . (string (string_fragment) @run)
)
) @_js-test
(#set! tag js-test)
)

View File

@@ -412,7 +412,7 @@ impl libwebrtc::native::audio_mixer::AudioMixerSource for AudioMixerSource {
self.sample_rate
}
fn get_audio_frame_with_info<'a>(&self, target_sample_rate: u32) -> Option<AudioFrame<'_>> {
fn get_audio_frame_with_info<'a>(&self, target_sample_rate: u32) -> Option<AudioFrame> {
assert_eq!(self.sample_rate, target_sample_rate);
let buf = self.buffer.lock().pop_front()?;
Some(AudioFrame {

View File

@@ -277,8 +277,6 @@ pub struct ResponseMessageDelta {
pub role: Option<Role>,
pub content: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reasoning_content: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tool_calls: Option<Vec<ToolCallChunk>>,
}

View File

@@ -231,7 +231,7 @@ impl Markdown {
&self.parsed_markdown
}
pub fn escape(s: &str) -> Cow<'_, str> {
pub fn escape(s: &str) -> Cow<str> {
// Valid to use bytes since multi-byte UTF-8 doesn't use ASCII chars.
let count = s
.bytes()

View File

@@ -72,25 +72,25 @@ impl<'a> MarkdownParser<'a> {
self.cursor >= self.tokens.len() - 1
}
fn peek(&self, steps: usize) -> Option<&(Event<'_>, Range<usize>)> {
fn peek(&self, steps: usize) -> Option<&(Event, Range<usize>)> {
if self.eof() || (steps + self.cursor) >= self.tokens.len() {
return self.tokens.last();
}
return self.tokens.get(self.cursor + steps);
}
fn previous(&self) -> Option<&(Event<'_>, Range<usize>)> {
fn previous(&self) -> Option<&(Event, Range<usize>)> {
if self.cursor == 0 || self.cursor > self.tokens.len() {
return None;
}
return self.tokens.get(self.cursor - 1);
}
fn current(&self) -> Option<&(Event<'_>, Range<usize>)> {
fn current(&self) -> Option<&(Event, Range<usize>)> {
return self.peek(0);
}
fn current_event(&self) -> Option<&Event<'_>> {
fn current_event(&self) -> Option<&Event> {
return self.current().map(|(event, _)| event);
}

View File

@@ -60,10 +60,6 @@ pub enum Model {
OpenCodestralMamba,
#[serde(rename = "devstral-small-latest", alias = "devstral-small-latest")]
DevstralSmallLatest,
#[serde(rename = "pixtral-12b-latest", alias = "pixtral-12b-latest")]
Pixtral12BLatest,
#[serde(rename = "pixtral-large-latest", alias = "pixtral-large-latest")]
PixtralLargeLatest,
#[serde(rename = "custom")]
Custom {
@@ -74,7 +70,6 @@ pub enum Model {
max_output_tokens: Option<u32>,
max_completion_tokens: Option<u32>,
supports_tools: Option<bool>,
supports_images: Option<bool>,
},
}
@@ -91,9 +86,6 @@ impl Model {
"mistral-small-latest" => Ok(Self::MistralSmallLatest),
"open-mistral-nemo" => Ok(Self::OpenMistralNemo),
"open-codestral-mamba" => Ok(Self::OpenCodestralMamba),
"devstral-small-latest" => Ok(Self::DevstralSmallLatest),
"pixtral-12b-latest" => Ok(Self::Pixtral12BLatest),
"pixtral-large-latest" => Ok(Self::PixtralLargeLatest),
invalid_id => anyhow::bail!("invalid model id '{invalid_id}'"),
}
}
@@ -107,8 +99,6 @@ impl Model {
Self::OpenMistralNemo => "open-mistral-nemo",
Self::OpenCodestralMamba => "open-codestral-mamba",
Self::DevstralSmallLatest => "devstral-small-latest",
Self::Pixtral12BLatest => "pixtral-12b-latest",
Self::PixtralLargeLatest => "pixtral-large-latest",
Self::Custom { name, .. } => name,
}
}
@@ -122,8 +112,6 @@ impl Model {
Self::OpenMistralNemo => "open-mistral-nemo",
Self::OpenCodestralMamba => "open-codestral-mamba",
Self::DevstralSmallLatest => "devstral-small-latest",
Self::Pixtral12BLatest => "pixtral-12b-latest",
Self::PixtralLargeLatest => "pixtral-large-latest",
Self::Custom {
name, display_name, ..
} => display_name.as_ref().unwrap_or(name),
@@ -139,8 +127,6 @@ impl Model {
Self::OpenMistralNemo => 131000,
Self::OpenCodestralMamba => 256000,
Self::DevstralSmallLatest => 262144,
Self::Pixtral12BLatest => 128000,
Self::PixtralLargeLatest => 128000,
Self::Custom { max_tokens, .. } => *max_tokens,
}
}
@@ -162,29 +148,10 @@ impl Model {
| Self::MistralSmallLatest
| Self::OpenMistralNemo
| Self::OpenCodestralMamba
| Self::DevstralSmallLatest
| Self::Pixtral12BLatest
| Self::PixtralLargeLatest => true,
| Self::DevstralSmallLatest => true,
Self::Custom { supports_tools, .. } => supports_tools.unwrap_or(false),
}
}
pub fn supports_images(&self) -> bool {
match self {
Self::Pixtral12BLatest
| Self::PixtralLargeLatest
| Self::MistralMediumLatest
| Self::MistralSmallLatest => true,
Self::CodestralLatest
| Self::MistralLargeLatest
| Self::OpenMistralNemo
| Self::OpenCodestralMamba
| Self::DevstralSmallLatest => false,
Self::Custom {
supports_images, ..
} => supports_images.unwrap_or(false),
}
}
}
#[derive(Debug, Serialize, Deserialize)]
@@ -264,8 +231,7 @@ pub enum RequestMessage {
tool_calls: Vec<ToolCall>,
},
User {
#[serde(flatten)]
content: MessageContent,
content: String,
},
System {
content: String,
@@ -276,54 +242,6 @@ pub enum RequestMessage {
},
}
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
#[serde(untagged)]
pub enum MessageContent {
#[serde(rename = "content")]
Plain { content: String },
#[serde(rename = "content")]
Multipart { content: Vec<MessagePart> },
}
impl MessageContent {
pub fn empty() -> Self {
Self::Plain {
content: String::new(),
}
}
pub fn push_part(&mut self, part: MessagePart) {
match self {
Self::Plain { content } => match part {
MessagePart::Text { text } => {
content.push_str(&text);
}
part => {
let mut parts = if content.is_empty() {
Vec::new()
} else {
vec![MessagePart::Text {
text: content.clone(),
}]
};
parts.push(part);
*self = Self::Multipart { content: parts };
}
},
Self::Multipart { content } => {
content.push(part);
}
}
}
}
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum MessagePart {
Text { text: String },
ImageUrl { image_url: String },
}
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
pub struct ToolCall {
pub id: String,

View File

@@ -728,7 +728,7 @@ impl MultiBuffer {
self.snapshot.borrow().clone()
}
pub fn read(&self, cx: &App) -> Ref<'_, MultiBufferSnapshot> {
pub fn read(&self, cx: &App) -> Ref<MultiBufferSnapshot> {
self.sync(cx);
self.snapshot.borrow()
}
@@ -2601,59 +2601,14 @@ impl MultiBuffer {
}
if let Some(buffer) = self.as_singleton() {
let buffer = buffer.read(cx);
if let Some(file) = buffer.file() {
if let Some(file) = buffer.read(cx).file() {
return file.file_name(cx).to_string_lossy();
}
if let Some(title) = self.buffer_based_title(buffer) {
return title;
}
};
}
"untitled".into()
}
fn buffer_based_title(&self, buffer: &Buffer) -> Option<Cow<'_, str>> {
let mut is_leading_whitespace = true;
let mut count = 0;
let mut prev_was_space = false;
let mut title = String::new();
for ch in buffer.snapshot().chars() {
if is_leading_whitespace && ch.is_whitespace() {
continue;
}
is_leading_whitespace = false;
if ch == '\n' || count >= 40 {
break;
}
if ch.is_whitespace() {
if !prev_was_space {
title.push(' ');
count += 1;
prev_was_space = true;
}
} else {
title.push(ch);
count += 1;
prev_was_space = false;
}
}
let title = title.trim_end().to_string();
if !title.is_empty() {
return Some(title.into());
}
None
}
pub fn set_title(&mut self, title: String, cx: &mut Context<Self>) {
self.title = Some(title);
cx.notify();
@@ -3779,7 +3734,7 @@ impl MultiBufferSnapshot {
.flat_map(|c| c.chars().rev())
}
fn reversed_chunks_in_range(&self, range: Range<usize>) -> ReversedMultiBufferChunks<'_> {
fn reversed_chunks_in_range(&self, range: Range<usize>) -> ReversedMultiBufferChunks {
let mut cursor = self.cursor::<usize>();
cursor.seek(&range.end);
let current_chunks = cursor.region().as_ref().map(|region| {
@@ -4294,7 +4249,7 @@ impl MultiBufferSnapshot {
self.excerpts.summary().widest_line_number + 1
}
pub fn bytes_in_range<T: ToOffset>(&self, range: Range<T>) -> MultiBufferBytes<'_> {
pub fn bytes_in_range<T: ToOffset>(&self, range: Range<T>) -> MultiBufferBytes {
let range = range.start.to_offset(self)..range.end.to_offset(self);
let mut excerpts = self.cursor::<usize>();
excerpts.seek(&range.start);
@@ -4333,7 +4288,7 @@ impl MultiBufferSnapshot {
pub fn reversed_bytes_in_range<T: ToOffset>(
&self,
range: Range<T>,
) -> ReversedMultiBufferBytes<'_> {
) -> ReversedMultiBufferBytes {
let range = range.start.to_offset(self)..range.end.to_offset(self);
let mut chunks = self.reversed_chunks_in_range(range.clone());
let chunk = chunks.next().map_or(&[][..], |c| c.as_bytes());
@@ -4344,7 +4299,7 @@ impl MultiBufferSnapshot {
}
}
pub fn row_infos(&self, start_row: MultiBufferRow) -> MultiBufferRows<'_> {
pub fn row_infos(&self, start_row: MultiBufferRow) -> MultiBufferRows {
let mut cursor = self.cursor::<Point>();
cursor.seek(&Point::new(start_row.0, 0));
let mut result = MultiBufferRows {
@@ -4357,11 +4312,7 @@ impl MultiBufferSnapshot {
result
}
pub fn chunks<T: ToOffset>(
&self,
range: Range<T>,
language_aware: bool,
) -> MultiBufferChunks<'_> {
pub fn chunks<T: ToOffset>(&self, range: Range<T>, language_aware: bool) -> MultiBufferChunks {
let mut chunks = MultiBufferChunks {
excerpt_offset_range: ExcerptOffset::new(0)..ExcerptOffset::new(0),
range: 0..0,
@@ -5322,7 +5273,7 @@ impl MultiBufferSnapshot {
.map(|excerpt| (excerpt.id, &excerpt.buffer, excerpt.range.clone()))
}
fn cursor<D: TextDimension + Default>(&self) -> MultiBufferCursor<'_, D> {
fn cursor<D: TextDimension + Default>(&self) -> MultiBufferCursor<D> {
let excerpts = self.excerpts.cursor(&());
let diff_transforms = self.diff_transforms.cursor(&());
MultiBufferCursor {
@@ -6085,7 +6036,7 @@ impl MultiBufferSnapshot {
pub fn syntax_ancestor<T: ToOffset>(
&self,
range: Range<T>,
) -> Option<(tree_sitter::Node<'_>, MultiOrSingleBufferOffsetRange)> {
) -> Option<(tree_sitter::Node, MultiOrSingleBufferOffsetRange)> {
let range = range.start.to_offset(self)..range.end.to_offset(self);
let mut excerpt = self.excerpt_containing(range.clone())?;
let node = excerpt
@@ -6283,10 +6234,7 @@ impl MultiBufferSnapshot {
}
/// Returns the excerpt containing range and its offset start within the multibuffer or none if `range` spans multiple excerpts
pub fn excerpt_containing<T: ToOffset>(
&self,
range: Range<T>,
) -> Option<MultiBufferExcerpt<'_>> {
pub fn excerpt_containing<T: ToOffset>(&self, range: Range<T>) -> Option<MultiBufferExcerpt> {
let range = range.start.to_offset(self)..range.end.to_offset(self);
let mut cursor = self.cursor::<usize>();
cursor.seek(&range.start);
@@ -6940,7 +6888,7 @@ impl Excerpt {
}
}
fn chunks_in_range(&self, range: Range<usize>, language_aware: bool) -> ExcerptChunks<'_> {
fn chunks_in_range(&self, range: Range<usize>, language_aware: bool) -> ExcerptChunks {
let content_start = self.range.context.start.to_offset(&self.buffer);
let chunks_start = content_start + range.start;
let chunks_end = content_start + cmp::min(range.end, self.text_summary.len);

View File

@@ -3651,69 +3651,3 @@ fn assert_line_indents(snapshot: &MultiBufferSnapshot) {
"reversed_line_indents({max_row})"
);
}
#[gpui::test]
fn test_new_empty_buffer_uses_untitled_title(cx: &mut App) {
let buffer = cx.new(|cx| Buffer::local("", cx));
let multibuffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
assert_eq!(multibuffer.read(cx).title(cx), "untitled");
}
#[gpui::test]
fn test_new_empty_buffer_uses_untitled_title_when_only_contains_whitespace(cx: &mut App) {
let buffer = cx.new(|cx| Buffer::local("\n ", cx));
let multibuffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
assert_eq!(multibuffer.read(cx).title(cx), "untitled");
}
#[gpui::test]
fn test_new_empty_buffer_takes_first_line_for_title(cx: &mut App) {
let buffer = cx.new(|cx| Buffer::local("Hello World\nSecond line", cx));
let multibuffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
assert_eq!(multibuffer.read(cx).title(cx), "Hello World");
}
#[gpui::test]
fn test_new_empty_buffer_takes_trimmed_first_line_for_title(cx: &mut App) {
let buffer = cx.new(|cx| Buffer::local("\nHello, World ", cx));
let multibuffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
assert_eq!(multibuffer.read(cx).title(cx), "Hello, World");
}
#[gpui::test]
fn test_new_empty_buffer_uses_truncated_first_line_for_title(cx: &mut App) {
let title = "aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeee";
let title_after = "aaaaaaaaaabbbbbbbbbbccccccccccdddddddddd";
let buffer = cx.new(|cx| Buffer::local(title, cx));
let multibuffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
assert_eq!(multibuffer.read(cx).title(cx), title_after);
}
#[gpui::test]
fn test_new_empty_buffer_uses_truncated_first_line_for_title_after_merging_adjacent_spaces(
cx: &mut App,
) {
let title = "aaaaaaaaaabbbbbbbbbb ccccccccccddddddddddeeeeeeeeee";
let title_after = "aaaaaaaaaabbbbbbbbbb ccccccccccddddddddd";
let buffer = cx.new(|cx| Buffer::local(title, cx));
let multibuffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
assert_eq!(multibuffer.read(cx).title(cx), title_after);
}
#[gpui::test]
fn test_new_empty_buffers_title_can_be_set(cx: &mut App) {
let buffer = cx.new(|cx| Buffer::local("Hello World", cx));
let multibuffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
assert_eq!(multibuffer.read(cx).title(cx), "Hello World");
multibuffer.update(cx, |multibuffer, cx| {
multibuffer.set_title("Hey".into(), cx)
});
assert_eq!(multibuffer.read(cx).title(cx), "Hey");
}

View File

@@ -1107,14 +1107,38 @@ impl OutlinePanel {
});
} else {
let mut offset = Point::default();
let expand_excerpt_control_height = 1.0;
if let Some(buffer_id) = scroll_to_buffer {
if multi_buffer_snapshot.as_singleton().is_none()
&& !active_editor.read(cx).is_buffer_folded(buffer_id, cx)
{
offset.y = -(active_editor.read(cx).file_header_size() as f32);
let current_folded = active_editor.read(cx).is_buffer_folded(buffer_id, cx);
if current_folded {
let previous_buffer_id = self
.fs_entries
.iter()
.rev()
.filter_map(|entry| match entry {
FsEntry::File(file) => Some(file.buffer_id),
FsEntry::ExternalFile(external_file) => {
Some(external_file.buffer_id)
}
FsEntry::Directory(..) => None,
})
.skip_while(|id| *id != buffer_id)
.nth(1);
if let Some(previous_buffer_id) = previous_buffer_id {
if !active_editor
.read(cx)
.is_buffer_folded(previous_buffer_id, cx)
{
offset.y += expand_excerpt_control_height;
}
}
} else {
if multi_buffer_snapshot.as_singleton().is_none() {
offset.y = -(active_editor.read(cx).file_header_size() as f32);
}
offset.y -= expand_excerpt_control_height;
}
}
active_editor.update(cx, |editor, cx| {
editor.set_scroll_anchor(ScrollAnchor { offset, anchor }, window, cx);
});

View File

@@ -211,7 +211,7 @@ pub struct GitEntry {
}
impl GitEntry {
pub fn to_ref(&self) -> GitEntryRef<'_> {
pub fn to_ref(&self) -> GitEntryRef {
GitEntryRef {
entry: &self.entry,
git_summary: self.git_summary,

View File

@@ -467,7 +467,7 @@ impl CompletionSource {
}
}
pub fn lsp_completion(&self, apply_defaults: bool) -> Option<Cow<'_, lsp::CompletionItem>> {
pub fn lsp_completion(&self, apply_defaults: bool) -> Option<Cow<lsp::CompletionItem>> {
if let Self::Lsp {
lsp_completion,
lsp_defaults,

View File

@@ -464,9 +464,6 @@ impl ProjectPanel {
if project_panel_settings.hide_gitignore != new_settings.hide_gitignore {
this.update_visible_entries(None, cx);
}
if project_panel_settings.hide_root != new_settings.hide_root {
this.update_visible_entries(None, cx);
}
project_panel_settings = new_settings;
this.update_diagnostics(cx);
cx.notify();
@@ -771,12 +768,6 @@ impl ProjectPanel {
let is_remote = project.is_via_collab();
let is_local = project.is_local();
let settings = ProjectPanelSettings::get_global(cx);
let visible_worktrees_count = project.visible_worktrees(cx).count();
let should_hide_rename = is_root
&& (cfg!(target_os = "windows")
|| (settings.hide_root && visible_worktrees_count == 1));
let context_menu = ContextMenu::build(window, cx, |menu, _, _| {
menu.context(self.focus_handle.clone()).map(|menu| {
if is_read_only {
@@ -826,7 +817,7 @@ impl ProjectPanel {
Box::new(zed_actions::workspace::CopyRelativePath),
)
.separator()
.when(!should_hide_rename, |menu| {
.when(!is_root || !cfg!(target_os = "windows"), |menu| {
menu.action("Rename", Box::new(Rename))
})
.when(!is_root & !is_remote, |menu| {
@@ -1547,16 +1538,6 @@ impl ProjectPanel {
if Some(entry) == worktree.read(cx).root_entry() {
return;
}
if Some(entry) == worktree.read(cx).root_entry() {
let settings = ProjectPanelSettings::get_global(cx);
let visible_worktrees_count =
self.project.read(cx).visible_worktrees(cx).count();
if settings.hide_root && visible_worktrees_count == 1 {
return;
}
}
self.edit_state = Some(EditState {
worktree_id,
entry_id: sub_entry_id,
@@ -2125,11 +2106,19 @@ impl ProjectPanel {
}
fn select_first(&mut self, _: &SelectFirst, window: &mut Window, cx: &mut Context<Self>) {
if let Some((worktree_id, visible_worktree_entries, _)) = self.visible_entries.first() {
if let Some(entry) = visible_worktree_entries.first() {
let worktree = self
.visible_entries
.first()
.and_then(|(worktree_id, _, _)| {
self.project.read(cx).worktree_for_id(*worktree_id, cx)
});
if let Some(worktree) = worktree {
let worktree = worktree.read(cx);
let worktree_id = worktree.id();
if let Some(root_entry) = worktree.root_entry() {
let selection = SelectedEntry {
worktree_id: *worktree_id,
entry_id: entry.id,
worktree_id,
entry_id: root_entry.id,
};
self.selection = Some(selection);
if window.modifiers().shift {
@@ -2782,31 +2771,6 @@ impl ProjectPanel {
Some(())
}
fn create_new_git_entry(
parent_entry: &Entry,
git_summary: GitSummary,
new_entry_kind: EntryKind,
) -> GitEntry {
GitEntry {
entry: Entry {
id: NEW_ENTRY_ID,
kind: new_entry_kind,
path: parent_entry.path.join("\0").into(),
inode: 0,
mtime: parent_entry.mtime,
size: parent_entry.size,
is_ignored: parent_entry.is_ignored,
is_external: false,
is_private: false,
is_always_included: parent_entry.is_always_included,
canonical_path: parent_entry.canonical_path.clone(),
char_bag: parent_entry.char_bag,
is_fifo: parent_entry.is_fifo,
},
git_summary,
}
}
fn update_visible_entries(
&mut self,
new_selected_entry: Option<(WorktreeId, ProjectEntryId)>,
@@ -2826,10 +2790,7 @@ impl ProjectPanel {
let old_ancestors = std::mem::take(&mut self.ancestors);
self.visible_entries.clear();
let mut max_width_item = None;
let visible_worktrees: Vec<_> = project.visible_worktrees(cx).collect();
let hide_root = settings.hide_root && visible_worktrees.len() == 1;
for worktree in visible_worktrees {
for worktree in project.visible_worktrees(cx) {
let worktree_snapshot = worktree.read(cx).snapshot();
let worktree_id = worktree_snapshot.id();
@@ -2864,18 +2825,6 @@ impl ProjectPanel {
GitTraversal::new(&repo_snapshots, worktree_snapshot.entries(true, 0));
let mut auto_folded_ancestors = vec![];
while let Some(entry) = entry_iter.entry() {
if hide_root && Some(entry.entry) == worktree.read(cx).root_entry() {
if new_entry_parent_id == Some(entry.id) {
visible_worktree_entries.push(Self::create_new_git_entry(
&entry.entry,
entry.git_summary,
new_entry_kind,
));
new_entry_parent_id = None;
}
entry_iter.advance();
continue;
}
if auto_collapse_dirs && entry.kind.is_dir() {
auto_folded_ancestors.push(entry.id);
if !self.unfolded_dir_ids.contains(&entry.id) {
@@ -2929,11 +2878,24 @@ impl ProjectPanel {
false
};
if precedes_new_entry && (!hide_gitignore || !entry.is_ignored) {
visible_worktree_entries.push(Self::create_new_git_entry(
&entry.entry,
entry.git_summary,
new_entry_kind,
));
visible_worktree_entries.push(GitEntry {
entry: Entry {
id: NEW_ENTRY_ID,
kind: new_entry_kind,
path: entry.path.join("\0").into(),
inode: 0,
mtime: entry.mtime,
size: entry.size,
is_ignored: entry.is_ignored,
is_external: false,
is_private: false,
is_always_included: entry.is_always_included,
canonical_path: entry.canonical_path.clone(),
char_bag: entry.char_bag,
is_fifo: entry.is_fifo,
},
git_summary: entry.git_summary,
});
}
let worktree_abs_path = worktree.read(cx).abs_path();
let (depth, path) = if Some(entry.entry) == worktree.read(cx).root_entry() {
@@ -3265,7 +3227,7 @@ impl ProjectPanel {
None
}
fn entry_at_index(&self, index: usize) -> Option<(WorktreeId, GitEntryRef<'_>)> {
fn entry_at_index(&self, index: usize) -> Option<(WorktreeId, GitEntryRef)> {
let mut offset = 0;
for (worktree_id, visible_worktree_entries, _) in &self.visible_entries {
if visible_worktree_entries.len() > offset + index {
@@ -3767,7 +3729,7 @@ impl ProjectPanel {
None
}
})
.unwrap_or_else(|| (0, entry.path.components().count()));
.unwrap_or((0, 0));
(depth, difference)
}

View File

@@ -44,7 +44,6 @@ pub struct ProjectPanelSettings {
pub auto_fold_dirs: bool,
pub scrollbar: ScrollbarSettings,
pub show_diagnostics: ShowDiagnostics,
pub hide_root: bool,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
@@ -146,10 +145,6 @@ pub struct ProjectPanelSettingsContent {
pub show_diagnostics: Option<ShowDiagnostics>,
/// Settings related to indent guides in the project panel.
pub indent_guides: Option<IndentGuidesSettingsContent>,
/// Whether to hide the root entry when only one folder is open in the window.
///
/// Default: false
pub hide_root: Option<bool>,
}
impl Settings for ProjectPanelSettings {

View File

@@ -309,7 +309,6 @@ async fn test_auto_collapse_dir_paths(cx: &mut gpui::TestAppContext) {
)
.await;
// Test 1: Multiple worktrees with auto_fold_dirs = true
let project = Project::test(
fs.clone(),
[path!("/root1").as_ref(), path!("/root2").as_ref()],
@@ -393,66 +392,6 @@ async fn test_auto_collapse_dir_paths(cx: &mut gpui::TestAppContext) {
separator!(" file_1.java"),
]
);
// Test 2: Single worktree with auto_fold_dirs = true and hide_root = true
{
let project = Project::test(fs.clone(), [path!("/root1").as_ref()], cx).await;
let workspace =
cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
let cx = &mut VisualTestContext::from_window(*workspace, cx);
cx.update(|_, cx| {
let settings = *ProjectPanelSettings::get_global(cx);
ProjectPanelSettings::override_global(
ProjectPanelSettings {
auto_fold_dirs: true,
hide_root: true,
..settings
},
cx,
);
});
let panel = workspace.update(cx, ProjectPanel::new).unwrap();
assert_eq!(
visible_entries_as_strings(&panel, 0..10, cx),
&[separator!("> dir_1/nested_dir_1/nested_dir_2/nested_dir_3")],
"Single worktree with hide_root=true should hide root and show auto-folded paths"
);
toggle_expand_dir(
&panel,
"root1/dir_1/nested_dir_1/nested_dir_2/nested_dir_3",
cx,
);
assert_eq!(
visible_entries_as_strings(&panel, 0..10, cx),
&[
separator!("v dir_1/nested_dir_1/nested_dir_2/nested_dir_3 <== selected"),
separator!(" > nested_dir_4/nested_dir_5"),
separator!(" file_a.java"),
separator!(" file_b.java"),
separator!(" file_c.java"),
],
"Expanded auto-folded path with hidden root should show contents without root prefix"
);
toggle_expand_dir(
&panel,
"root1/dir_1/nested_dir_1/nested_dir_2/nested_dir_3/nested_dir_4/nested_dir_5",
cx,
);
assert_eq!(
visible_entries_as_strings(&panel, 0..10, cx),
&[
separator!("v dir_1/nested_dir_1/nested_dir_2/nested_dir_3"),
separator!(" v nested_dir_4/nested_dir_5 <== selected"),
separator!(" file_d.java"),
separator!(" file_a.java"),
separator!(" file_b.java"),
separator!(" file_c.java"),
],
"Nested expansion with hidden root should maintain proper indentation"
);
}
}
#[gpui::test(iterations = 30)]
@@ -2536,7 +2475,6 @@ async fn test_select_directory(cx: &mut gpui::TestAppContext) {
]
);
}
#[gpui::test]
async fn test_select_first_last(cx: &mut gpui::TestAppContext) {
init_test_with_editor(cx);
@@ -2605,46 +2543,6 @@ async fn test_select_first_last(cx: &mut gpui::TestAppContext) {
" file_2.py <== selected",
]
);
cx.update(|_, cx| {
let settings = *ProjectPanelSettings::get_global(cx);
ProjectPanelSettings::override_global(
ProjectPanelSettings {
hide_root: true,
..settings
},
cx,
);
});
let panel = workspace.update(cx, ProjectPanel::new).unwrap();
#[rustfmt::skip]
assert_eq!(
visible_entries_as_strings(&panel, 0..10, cx),
&[
"> dir_1",
"> zdir_2",
" file_1.py",
" file_2.py",
],
"With hide_root=true, root should be hidden"
);
panel.update_in(cx, |panel, window, cx| {
panel.select_first(&SelectFirst, window, cx)
});
assert_eq!(
visible_entries_as_strings(&panel, 0..10, cx),
&[
"> dir_1 <== selected",
"> zdir_2",
" file_1.py",
" file_2.py",
],
"With hide_root=true, first entry should be dir_1, not the hidden root"
);
}
#[gpui::test]
@@ -2891,101 +2789,6 @@ async fn test_rename_root_of_worktree(cx: &mut gpui::TestAppContext) {
);
}
#[gpui::test]
async fn test_rename_with_hide_root(cx: &mut gpui::TestAppContext) {
init_test_with_editor(cx);
let fs = FakeFs::new(cx.executor().clone());
fs.insert_tree(
"/root1",
json!({
"dir1": { "file1.txt": "content" },
"file2.txt": "content",
}),
)
.await;
fs.insert_tree("/root2", json!({ "file3.txt": "content" }))
.await;
// Test 1: Single worktree, hide_root=true - rename should be blocked
{
let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await;
let workspace =
cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
let cx = &mut VisualTestContext::from_window(*workspace, cx);
cx.update(|_, cx| {
let settings = *ProjectPanelSettings::get_global(cx);
ProjectPanelSettings::override_global(
ProjectPanelSettings {
hide_root: true,
..settings
},
cx,
);
});
let panel = workspace.update(cx, ProjectPanel::new).unwrap();
panel.update(cx, |panel, cx| {
let project = panel.project.read(cx);
let worktree = project.visible_worktrees(cx).next().unwrap();
let root_entry = worktree.read(cx).root_entry().unwrap();
panel.selection = Some(SelectedEntry {
worktree_id: worktree.read(cx).id(),
entry_id: root_entry.id,
});
});
panel.update_in(cx, |panel, window, cx| panel.rename(&Rename, window, cx));
assert!(
panel.read_with(cx, |panel, _| panel.edit_state.is_none()),
"Rename should be blocked when hide_root=true with single worktree"
);
}
// Test 2: Multiple worktrees, hide_root=true - rename should work
{
let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
let workspace =
cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
let cx = &mut VisualTestContext::from_window(*workspace, cx);
cx.update(|_, cx| {
let settings = *ProjectPanelSettings::get_global(cx);
ProjectPanelSettings::override_global(
ProjectPanelSettings {
hide_root: true,
..settings
},
cx,
);
});
let panel = workspace.update(cx, ProjectPanel::new).unwrap();
select_path(&panel, "root1", cx);
panel.update_in(cx, |panel, window, cx| panel.rename(&Rename, window, cx));
#[cfg(target_os = "windows")]
assert!(
panel.read_with(cx, |panel, _| panel.edit_state.is_none()),
"Rename should be blocked on Windows even with multiple worktrees"
);
#[cfg(not(target_os = "windows"))]
{
assert!(
panel.read_with(cx, |panel, _| panel.edit_state.is_some()),
"Rename should work with multiple worktrees on non-Windows when hide_root=true"
);
panel.update_in(cx, |panel, window, cx| {
panel.cancel(&menu::Cancel, window, cx)
});
}
}
}
#[gpui::test]
async fn test_multiple_marked_entries(cx: &mut gpui::TestAppContext) {
init_test_with_editor(cx);
@@ -5295,155 +5098,6 @@ async fn test_create_entries_without_selection(cx: &mut gpui::TestAppContext) {
);
}
#[gpui::test]
async fn test_create_entries_without_selection_hide_root(cx: &mut gpui::TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor().clone());
fs.insert_tree(
path!("/root"),
json!({
"existing_dir": {
"existing_file.txt": "",
},
"existing_file.txt": "",
}),
)
.await;
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
let cx = &mut VisualTestContext::from_window(*workspace, cx);
cx.update(|_, cx| {
let settings = *ProjectPanelSettings::get_global(cx);
ProjectPanelSettings::override_global(
ProjectPanelSettings {
hide_root: true,
..settings
},
cx,
);
});
let panel = workspace
.update(cx, |workspace, window, cx| {
let panel = ProjectPanel::new(workspace, window, cx);
workspace.add_panel(panel.clone(), window, cx);
panel
})
.unwrap();
#[rustfmt::skip]
assert_eq!(
visible_entries_as_strings(&panel, 0..20, cx),
&[
"> existing_dir",
" existing_file.txt",
],
"Initial state with hide_root=true, root should be hidden and nothing selected"
);
panel.update(cx, |panel, _| {
assert!(
panel.selection.is_none(),
"Should have no selection initially"
);
});
// Test 1: Create new file when no entry is selected
panel.update_in(cx, |panel, window, cx| {
panel.new_file(&NewFile, window, cx);
});
panel.update_in(cx, |panel, window, cx| {
assert!(panel.filename_editor.read(cx).is_focused(window));
});
#[rustfmt::skip]
assert_eq!(
visible_entries_as_strings(&panel, 0..20, cx),
&[
"> existing_dir",
" [EDITOR: ''] <== selected",
" existing_file.txt",
],
"Editor should appear at root level when hide_root=true and no selection"
);
let confirm = panel.update_in(cx, |panel, window, cx| {
panel.filename_editor.update(cx, |editor, cx| {
editor.set_text("new_file_at_root.txt", window, cx)
});
panel.confirm_edit(window, cx).unwrap()
});
confirm.await.unwrap();
#[rustfmt::skip]
assert_eq!(
visible_entries_as_strings(&panel, 0..20, cx),
&[
"> existing_dir",
" existing_file.txt",
" new_file_at_root.txt <== selected <== marked",
],
"New file should be created at root level and visible without root prefix"
);
assert!(
fs.is_file(Path::new("/root/new_file_at_root.txt")).await,
"File should be created in the actual root directory"
);
// Test 2: Create new directory when no entry is selected
panel.update(cx, |panel, _| {
panel.selection = None;
});
panel.update_in(cx, |panel, window, cx| {
panel.new_directory(&NewDirectory, window, cx);
});
panel.update_in(cx, |panel, window, cx| {
assert!(panel.filename_editor.read(cx).is_focused(window));
});
#[rustfmt::skip]
assert_eq!(
visible_entries_as_strings(&panel, 0..20, cx),
&[
"> [EDITOR: ''] <== selected",
"> existing_dir",
" existing_file.txt",
" new_file_at_root.txt",
],
"Directory editor should appear at root level when hide_root=true and no selection"
);
let confirm = panel.update_in(cx, |panel, window, cx| {
panel.filename_editor.update(cx, |editor, cx| {
editor.set_text("new_dir_at_root", window, cx)
});
panel.confirm_edit(window, cx).unwrap()
});
confirm.await.unwrap();
#[rustfmt::skip]
assert_eq!(
visible_entries_as_strings(&panel, 0..20, cx),
&[
"> existing_dir",
"v new_dir_at_root <== selected",
" existing_file.txt",
" new_file_at_root.txt",
],
"New directory should be created at root level and visible without root prefix"
);
assert!(
fs.is_dir(Path::new("/root/new_dir_at_root")).await,
"Directory should be created in the actual root directory"
);
}
#[gpui::test]
async fn test_highlight_entry_for_external_drag(cx: &mut gpui::TestAppContext) {
init_test(cx);
@@ -5643,184 +5297,6 @@ async fn test_highlight_entry_for_selection_drag(cx: &mut gpui::TestAppContext)
});
}
#[gpui::test]
async fn test_hide_root(cx: &mut gpui::TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor().clone());
fs.insert_tree(
"/root1",
json!({
"dir1": {
"file1.txt": "content",
"file2.txt": "content",
},
"dir2": {
"file3.txt": "content",
},
"file4.txt": "content",
}),
)
.await;
fs.insert_tree(
"/root2",
json!({
"dir3": {
"file5.txt": "content",
},
"file6.txt": "content",
}),
)
.await;
// Test 1: Single worktree with hide_root = false
{
let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await;
let workspace =
cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
let cx = &mut VisualTestContext::from_window(*workspace, cx);
cx.update(|_, cx| {
let settings = *ProjectPanelSettings::get_global(cx);
ProjectPanelSettings::override_global(
ProjectPanelSettings {
hide_root: false,
..settings
},
cx,
);
});
let panel = workspace.update(cx, ProjectPanel::new).unwrap();
#[rustfmt::skip]
assert_eq!(
visible_entries_as_strings(&panel, 0..10, cx),
&[
"v root1",
" > dir1",
" > dir2",
" file4.txt",
],
"With hide_root=false and single worktree, root should be visible"
);
}
// Test 2: Single worktree with hide_root = true
{
let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await;
let workspace =
cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
let cx = &mut VisualTestContext::from_window(*workspace, cx);
// Set hide_root to true
cx.update(|_, cx| {
let settings = *ProjectPanelSettings::get_global(cx);
ProjectPanelSettings::override_global(
ProjectPanelSettings {
hide_root: true,
..settings
},
cx,
);
});
let panel = workspace.update(cx, ProjectPanel::new).unwrap();
assert_eq!(
visible_entries_as_strings(&panel, 0..10, cx),
&["> dir1", "> dir2", " file4.txt",],
"With hide_root=true and single worktree, root should be hidden"
);
// Test expanding directories still works without root
toggle_expand_dir(&panel, "root1/dir1", cx);
assert_eq!(
visible_entries_as_strings(&panel, 0..10, cx),
&[
"v dir1 <== selected",
" file1.txt",
" file2.txt",
"> dir2",
" file4.txt",
],
"Should be able to expand directories even when root is hidden"
);
}
// Test 3: Multiple worktrees with hide_root = true
{
let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
let workspace =
cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
let cx = &mut VisualTestContext::from_window(*workspace, cx);
// Set hide_root to true
cx.update(|_, cx| {
let settings = *ProjectPanelSettings::get_global(cx);
ProjectPanelSettings::override_global(
ProjectPanelSettings {
hide_root: true,
..settings
},
cx,
);
});
let panel = workspace.update(cx, ProjectPanel::new).unwrap();
assert_eq!(
visible_entries_as_strings(&panel, 0..10, cx),
&[
"v root1",
" > dir1",
" > dir2",
" file4.txt",
"v root2",
" > dir3",
" file6.txt",
],
"With hide_root=true and multiple worktrees, roots should still be visible"
);
}
// Test 4: Multiple worktrees with hide_root = false
{
let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
let workspace =
cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
let cx = &mut VisualTestContext::from_window(*workspace, cx);
cx.update(|_, cx| {
let settings = *ProjectPanelSettings::get_global(cx);
ProjectPanelSettings::override_global(
ProjectPanelSettings {
hide_root: false,
..settings
},
cx,
);
});
let panel = workspace.update(cx, ProjectPanel::new).unwrap();
assert_eq!(
visible_entries_as_strings(&panel, 0..10, cx),
&[
"v root1",
" > dir1",
" > dir2",
" file4.txt",
"v root2",
" > dir3",
" file6.txt",
],
"With hide_root=false and multiple worktrees, roots should be visible"
);
}
}
fn select_path(panel: &Entity<ProjectPanel>, path: impl AsRef<Path>, cx: &mut VisualTestContext) {
let path = path.as_ref();
panel.update(cx, |panel, cx| {

View File

@@ -281,7 +281,7 @@ impl RemoteEntry {
matches!(self, Self::Project { .. })
}
fn connection(&self) -> Cow<'_, SshConnection> {
fn connection(&self) -> Cow<SshConnection> {
match self {
Self::Project { connection, .. } => Cow::Borrowed(connection),
Self::SshConfig { host, .. } => Cow::Owned(SshConnection {

View File

@@ -53,7 +53,7 @@ impl Chunk {
}
#[inline(always)]
pub fn as_slice(&self) -> ChunkSlice<'_> {
pub fn as_slice(&self) -> ChunkSlice {
ChunkSlice {
chars: self.chars,
chars_utf16: self.chars_utf16,
@@ -64,7 +64,7 @@ impl Chunk {
}
#[inline(always)]
pub fn slice(&self, range: Range<usize>) -> ChunkSlice<'_> {
pub fn slice(&self, range: Range<usize>) -> ChunkSlice {
self.as_slice().slice(range)
}
}

View File

@@ -241,7 +241,7 @@ impl Rope {
self.chunks.extent(&())
}
pub fn cursor(&self, offset: usize) -> Cursor<'_> {
pub fn cursor(&self, offset: usize) -> Cursor {
Cursor::new(self, offset)
}
@@ -258,23 +258,23 @@ impl Rope {
.flat_map(|chunk| chunk.chars().rev())
}
pub fn bytes_in_range(&self, range: Range<usize>) -> Bytes<'_> {
pub fn bytes_in_range(&self, range: Range<usize>) -> Bytes {
Bytes::new(self, range, false)
}
pub fn reversed_bytes_in_range(&self, range: Range<usize>) -> Bytes<'_> {
pub fn reversed_bytes_in_range(&self, range: Range<usize>) -> Bytes {
Bytes::new(self, range, true)
}
pub fn chunks(&self) -> Chunks<'_> {
pub fn chunks(&self) -> Chunks {
self.chunks_in_range(0..self.len())
}
pub fn chunks_in_range(&self, range: Range<usize>) -> Chunks<'_> {
pub fn chunks_in_range(&self, range: Range<usize>) -> Chunks {
Chunks::new(self, range, false)
}
pub fn reversed_chunks_in_range(&self, range: Range<usize>) -> Chunks<'_> {
pub fn reversed_chunks_in_range(&self, range: Range<usize>) -> Chunks {
Chunks::new(self, range, true)
}

View File

@@ -380,7 +380,7 @@ impl<T: Item> SumTree<T> {
items
}
pub fn iter(&self) -> Iter<'_, T> {
pub fn iter(&self) -> Iter<T> {
Iter::new(self)
}

View File

@@ -103,7 +103,7 @@ pub struct VenvSettingsContent<'a> {
}
impl VenvSettings {
pub fn as_option(&self) -> Option<VenvSettingsContent<'_>> {
pub fn as_option(&self) -> Option<VenvSettingsContent> {
match self {
VenvSettings::Off => None,
VenvSettings::On {

View File

@@ -2049,7 +2049,7 @@ impl BufferSnapshot {
self.visible_text.reversed_chars_at(offset)
}
pub fn reversed_chunks_in_range<T: ToOffset>(&self, range: Range<T>) -> rope::Chunks<'_> {
pub fn reversed_chunks_in_range<T: ToOffset>(&self, range: Range<T>) -> rope::Chunks {
let range = range.start.to_offset(self)..range.end.to_offset(self);
self.visible_text.reversed_chunks_in_range(range)
}

View File

@@ -595,7 +595,6 @@ impl TitleBar {
.icon(IconName::GitBranch)
.icon_position(IconPosition::Start)
.icon_color(Color::Muted)
.icon_size(IconSize::Indicator)
},
),
)

View File

@@ -1,10 +1,9 @@
use editor::{Editor, EditorSettings};
use editor::Editor;
use gpui::{Context, Window, actions, impl_actions, impl_internal_actions};
use language::Point;
use schemars::JsonSchema;
use search::{BufferSearchBar, SearchOptions, buffer_search};
use serde_derive::Deserialize;
use settings::Settings;
use std::{iter::Peekable, str::Chars};
use util::serde::default_true;
use workspace::{notifications::NotifyResultExt, searchable::Direction};
@@ -159,9 +158,6 @@ impl Vim {
if action.backwards {
options |= SearchOptions::BACKWARDS;
}
if EditorSettings::get_global(cx).search.case_sensitive {
options |= SearchOptions::CASE_SENSITIVE;
}
search_bar.set_search_options(options, cx);
let prior_mode = if self.temp_mode {
Mode::Insert

View File

@@ -915,9 +915,6 @@ impl Vim {
if mode == Mode::Normal || mode != last_mode {
self.current_tx.take();
self.current_anchor.take();
self.update_editor(window, cx, |_, editor, _, _| {
editor.clear_selection_drag_state();
});
}
Vim::take_forced_motion(cx);
if mode != Mode::Insert && mode != Mode::Replace {

View File

@@ -150,7 +150,7 @@ impl<T> Drop for Changed<'_, T> {
}
impl<T> Receiver<T> {
pub fn borrow(&mut self) -> parking_lot::MappedRwLockReadGuard<'_, T> {
pub fn borrow(&mut self) -> parking_lot::MappedRwLockReadGuard<T> {
let state = self.state.read();
self.version = state.version;
RwLockReadGuard::map(state, |state| &state.value)

View File

@@ -2925,9 +2925,15 @@ impl Pane {
let ix = if moved_right { ix - 1 } else { ix };
let is_pinned_in_to_pane = this.is_tab_pinned(ix);
if !was_pinned_in_from_pane && is_pinned_in_to_pane {
if (was_pinned_in_from_pane && is_pinned_in_to_pane)
|| (!was_pinned_in_from_pane && !is_pinned_in_to_pane)
{
return;
}
if is_pinned_in_to_pane {
this.pinned_tab_count += 1;
} else if was_pinned_in_from_pane && !is_pinned_in_to_pane {
} else {
this.pinned_tab_count -= 1;
}
} else if this.items.len() >= to_pane_old_length {

View File

@@ -6255,15 +6255,7 @@ fn resize_right_dock(
window: &mut Window,
cx: &mut App,
) {
let mut size = new_size.max(workspace.bounds.left() - RESIZE_HANDLE_SIZE);
workspace.left_dock.read_with(cx, |left_dock, cx| {
let left_dock_size = left_dock
.active_panel_size(window, cx)
.unwrap_or(Pixels(0.0));
if left_dock_size + size > workspace.bounds.right() {
size = workspace.bounds.right() - left_dock_size
}
});
let size = new_size.max(workspace.bounds.left() - RESIZE_HANDLE_SIZE);
workspace.right_dock.update(cx, |right_dock, cx| {
if WorkspaceSettings::get_global(cx)
.resize_all_panels_in_dock

View File

@@ -2566,7 +2566,7 @@ impl Snapshot {
include_dirs: bool,
include_ignored: bool,
start_offset: usize,
) -> Traversal<'_> {
) -> Traversal {
let mut cursor = self.entries_by_path.cursor(&());
cursor.seek(
&TraversalTarget::Count {
@@ -2593,19 +2593,19 @@ impl Snapshot {
include_dirs: bool,
include_ignored: bool,
path: &Path,
) -> Traversal<'_> {
) -> Traversal {
Traversal::new(self, include_files, include_dirs, include_ignored, path)
}
pub fn files(&self, include_ignored: bool, start: usize) -> Traversal<'_> {
pub fn files(&self, include_ignored: bool, start: usize) -> Traversal {
self.traverse_from_offset(true, false, include_ignored, start)
}
pub fn directories(&self, include_ignored: bool, start: usize) -> Traversal<'_> {
pub fn directories(&self, include_ignored: bool, start: usize) -> Traversal {
self.traverse_from_offset(false, true, include_ignored, start)
}
pub fn entries(&self, include_ignored: bool, start: usize) -> Traversal<'_> {
pub fn entries(&self, include_ignored: bool, start: usize) -> Traversal {
self.traverse_from_offset(true, true, include_ignored, start)
}

View File

@@ -472,7 +472,6 @@ fn initialize_panels(
let project_panel = ProjectPanel::load(workspace_handle.clone(), cx.clone());
let outline_panel = OutlinePanel::load(workspace_handle.clone(), cx.clone());
let terminal_panel = TerminalPanel::load(workspace_handle.clone(), cx.clone());
let git_panel = GitPanel::load(workspace_handle.clone(), cx.clone());
let channels_panel =
collab_ui::collab_panel::CollabPanel::load(workspace_handle.clone(), cx.clone());
let chat_panel =
@@ -486,14 +485,12 @@ fn initialize_panels(
project_panel,
outline_panel,
terminal_panel,
git_panel,
channels_panel,
chat_panel,
notification_panel,
) = futures::try_join!(
project_panel,
outline_panel,
git_panel,
terminal_panel,
channels_panel,
chat_panel,
@@ -504,7 +501,6 @@ fn initialize_panels(
workspace.add_panel(project_panel, window, cx);
workspace.add_panel(outline_panel, window, cx);
workspace.add_panel(terminal_panel, window, cx);
workspace.add_panel(git_panel, window, cx);
workspace.add_panel(channels_panel, window, cx);
workspace.add_panel(chat_panel, window, cx);
workspace.add_panel(notification_panel, window, cx);
@@ -522,6 +518,12 @@ fn initialize_panels(
)
.detach()
});
let entity = cx.entity();
let project = workspace.project().clone();
let app_state = workspace.app_state().clone();
let git_panel = cx.new(|cx| GitPanel::new(entity, project, app_state, window, cx));
workspace.add_panel(git_panel, window, cx);
})?;
let is_assistant2_enabled = !cfg!(test);
@@ -3025,7 +3027,7 @@ mod tests {
});
cx.read(|cx| {
assert!(editor.is_dirty(cx));
assert_eq!(editor.read(cx).title(cx), "hi");
assert_eq!(editor.read(cx).title(cx), "untitled");
});
// When the save completes, the buffer's title is updated and the language is assigned based

View File

@@ -209,18 +209,13 @@ Custom models will be listed in the model dropdown in the Agent Panel. You can a
> ✅ Supports tool use in some cases.
> Visit [the Copilot Chat code](https://github.com/zed-industries/zed/blob/9e0330ba7d848755c9734bf456c716bddf0973f3/crates/language_models/src/provider/copilot_chat.rs#L189-L198) for the supported subset.
You can use GitHub Copilot Chat with the Zed assistant by choosing it via the model dropdown in the Agent Panel.
1. Open the settings view (`agent: open configuration`) and go to the GitHub Copilot Chat section
2. Click on `Sign in to use GitHub Copilot`, follow the steps shown in the modal.
Alternatively, you can provide an OAuth token via the `GH_COPILOT_TOKEN` environment variable.
You can use GitHub Copilot chat with the Zed assistant by choosing it via the model dropdown in the Agent Panel.
### Google AI {#google-ai}
> ✅ Supports tool use
You can use Gemini models with the Zed assistant by choosing it via the model dropdown in the Agent Panel.
You can use Gemini 1.5 Pro/Flash with the Zed assistant by choosing it via the model dropdown in the Agent Panel.
1. Go to the Google AI Studio site and [create an API key](https://aistudio.google.com/app/apikey).
2. Open the settings view (`agent: open configuration`) and go to the Google AI section
@@ -307,8 +302,7 @@ The Zed Assistant comes pre-configured with several Mistral models (codestral-la
"max_tokens": 32000,
"max_output_tokens": 4096,
"max_completion_tokens": 1024,
"supports_tools": true,
"supports_images": false
"supports_tools": true
}
]
}
@@ -380,10 +374,10 @@ The `supports_tools` option controls whether or not the model will use additiona
If the model is tagged with `tools` in the Ollama catalog this option should be supplied, and built in profiles `Ask` and `Write` can be used.
If the model is not tagged with `tools` in the Ollama catalog, this option can still be supplied with value `true`; however be aware that only the `Minimal` built in profile will work.
The `supports_thinking` option controls whether or not the model will perform an explicit “thinking” (reasoning) pass before producing its final answer.
The `supports_thinking` option controls whether or not the model will perform an explicit “thinking” (reasoning) pass before producing its final answer.
If the model is tagged with `thinking` in the Ollama catalog, set this option and you can use it in zed.
The `supports_images` option enables the models vision capabilities, allowing it to process images included in the conversation context.
The `supports_images` option enables the models vision capabilities, allowing it to process images included in the conversation context.
If the model is tagged with `vision` in the Ollama catalog, set this option and you can use it in zed.
### OpenAI {#openai}

View File

@@ -1216,16 +1216,6 @@ or
`boolean` values
### Drag And Drop Selection
- Description: Whether to allow drag and drop text selection in buffer.
- Setting: `drag_and_drop_selection`
- Default: `true`
**Options**
`boolean` values
## Editor Toolbar
- Description: Whether or not to show various elements in the editor toolbar.
@@ -1934,30 +1924,6 @@ Example:
`boolean` values
## Multi Cursor Modifier
- Description: Determines the modifier to be used to add multiple cursors with the mouse. The open hover link mouse gestures will adapt such that it do not conflict with the multicursor modifier.
- Setting: `multi_cursor_modifier`
- Default: `alt`
**Options**
1. Maps to `Alt` on Linux and Windows and to `Option` on MacOS:
```jsonc
{
"multi_cursor_modifier": "alt",
}
```
2. Maps `Control` on Linux and Windows and to `Command` on MacOS:
```jsonc
{
"multi_cursor_modifier": "cmd_or_ctrl", // alias: "cmd", "ctrl"
}
```
## Hover Popover Enabled
- Description: Whether or not to show the informational hover box when moving the mouse over symbols in the editor.
@@ -3098,8 +3064,7 @@ Run the `theme selector: toggle` action in the command palette to see a current
"show_diagnostics": "all",
"indent_guides": {
"show": "always"
},
"hide_root": false
}
}
}
```

View File

@@ -48,6 +48,8 @@ You can trigger formatting via {#kb editor::Format} or the `editor: format` acti
}
```
See [Clang-Format Style Options](https://clang.llvm.org/docs/ClangFormatStyleOptions.html) for a complete list of options.
## Compile Commands
For some projects Clangd requires a `compile_commands.json` file to properly analyze your project. This file contains the compilation database that tells clangd how your project should be built.

View File

@@ -20,15 +20,6 @@ To use the Deno Language Server with TypeScript and TSX files, you will likely w
}
},
"languages": {
"JavaScript": {
"language_servers": [
"deno",
"!typescript-language-server",
"!vtsls",
"!eslint"
],
"formatter": "language_server"
},
"TypeScript": {
"language_servers": [
"deno",

View File

@@ -21,11 +21,7 @@ There are multiple language servers available for Ruby. Zed supports the two fol
They both have an overlapping feature set of autocomplete, diagnostics, code actions, etc. and it's up to you to decide which one you want to use. Note that you can't use both at the same time.
In addition to these two language servers, Zed also supports:
- [rubocop](https://github.com/rubocop/rubocop) which is a static code analyzer and linter for Ruby. Under the hood, it's also used by Zed as a language server, but its functionality is complimentary to that of solargraph and ruby-lsp.
- [sorbet](https://sorbet.org/) which is a static type checker for Ruby with a custom gradual type system.
- [steep](https://github.com/soutaro/steep) which is a static type checker for Ruby that leverages Ruby Signature (RBS).
In addition to these two language servers, Zed also supports [rubocop](https://github.com/rubocop/rubocop) which is a static code analyzer and linter for Ruby. Under the hood, it's also used by Zed as a language server, but its functionality is complimentary to that of solargraph and ruby-lsp.
When configuring a language server, it helps to open the LSP Logs window using the 'dev: Open Language Server Logs' command. You can then choose the corresponding language instance to see any logged information.
@@ -35,7 +31,7 @@ The [Ruby extension](https://github.com/zed-extensions/ruby) offers both `solarg
### Language Server Activation
For all supported Ruby language servers (`solargraph`, `ruby-lsp`, `rubocop`, `sorbet`, and `steep`), the Ruby extension follows this activation sequence:
For all Ruby language servers (`solargraph`, `ruby-lsp`, and `rubocop`), the Ruby extension follows this activation sequence:
1. If the language server is found in your project's `Gemfile`, it will be used through `bundle exec`.
2. If not found in the `Gemfile`, the Ruby extension will look for the executable in your system `PATH`.
@@ -192,52 +188,6 @@ Rubocop has unsafe autocorrection disabled by default. We can tell Zed to enable
}
```
## Setting up Sorbet
[Sorbet](https://sorbet.org/) is a popular static type checker for Ruby that includes a language server.
To enable Sorbet, add `\"sorbet\"` to the `language_servers` list for Ruby in your `settings.json`. You may want to disable other language servers if Sorbet is intended to be your primary LSP, or if you plan to use it alongside another LSP for specific features like type checking.
```json
{
"languages": {
"Ruby": {
"language_servers": [
"ruby-lsp",
"sorbet",
"!rubocop",
"!solargraph",
"..."
]
}
}
}
```
For all aspects of installing Sorbet, setting it up in your project, and configuring its behavior, please refer to the [official Sorbet documentation](https://sorbet.org/docs/overview).
## Setting up Steep
[Steep](https://github.com/soutaro/steep) is a static type checker for Ruby that uses RBS files to define types.
To enable Steep, add `\"steep\"` to the `language_servers` list for Ruby in your `settings.json`. You may need to adjust the order or disable other LSPs depending on your desired setup.
```json
{
"languages": {
"Ruby": {
"language_servers": [
"ruby-lsp",
"steep",
"!solargraph",
"!rubocop",
"..."
]
}
}
}
```
## Using the Tailwind CSS Language Server with Ruby
It's possible to use the [Tailwind CSS Language Server](https://github.com/tailwindlabs/tailwindcss-intellisense/tree/HEAD/packages/tailwindcss-language-server#readme) in Ruby and ERB files.
@@ -291,14 +241,8 @@ To run tests in your Ruby project, you can set up custom tasks in your local `.z
```json
[
{
"label": "test $ZED_RELATIVE_FILE -n /$ZED_CUSTOM_RUBY_TEST_NAME/",
"command": "bin/rails",
"args": [
"test",
"$ZED_RELATIVE_FILE",
"-n",
"\"$ZED_CUSTOM_RUBY_TEST_NAME\""
],
"label": "test $ZED_RELATIVE_FILE -n /$ZED_SYMBOL/",
"command": "bin/rails test $ZED_RELATIVE_FILE -n /$ZED_SYMBOL/",
"tags": ["ruby-test"]
}
]
@@ -308,21 +252,14 @@ Note: We can't use `args` here because of the way quotes are handled.
### Minitest
Plain minitest does not support running tests by line number, only by name, so we need to use `$ZED_CUSTOM_RUBY_TEST_NAME` instead:
Plain minitest does not support running tests by line number, only by name, so we need to use `$ZED_SYMBOL` instead:
```json
[
{
"label": "-Itest $ZED_RELATIVE_FILE -n /$ZED_CUSTOM_RUBY_TEST_NAME/",
"command": "bundle",
"args": [
"exec",
"ruby",
"-Itest",
"$ZED_RELATIVE_FILE",
"-n",
"\"$ZED_CUSTOM_RUBY_TEST_NAME\""
],
"label": "-Itest $ZED_RELATIVE_FILE -n /$ZED_SYMBOL/",
"command": "bundle exec ruby",
"args": ["-Itest", "$ZED_RELATIVE_FILE", "-n /$ZED_SYMBOL/"],
"tags": ["ruby-test"]
}
]
@@ -334,8 +271,8 @@ Plain minitest does not support running tests by line number, only by name, so w
[
{
"label": "test $ZED_RELATIVE_FILE:$ZED_ROW",
"command": "bundle",
"args": ["exec", "rspec", "\"$ZED_RELATIVE_FILE:$ZED_ROW\""],
"command": "bundle exec rspec",
"args": ["\"$ZED_RELATIVE_FILE:$ZED_ROW\""],
"tags": ["ruby-test"]
}
]
@@ -347,8 +284,8 @@ Plain minitest does not support running tests by line number, only by name, so w
[
{
"label": "test $ZED_RELATIVE_FILE:$ZED_ROW",
"command": "bundle",
"args": ["exec", "qt", "exec", "qt", "\"$ZED_RELATIVE_FILE:$ZED_ROW\""],
"command": "bundle exec qt",
"args": ["\"$ZED_RELATIVE_FILE:$ZED_ROW\""],
"tags": ["ruby-test"]
}
]
@@ -360,8 +297,8 @@ Plain minitest does not support running tests by line number, only by name, so w
[
{
"label": "test $ZED_RELATIVE_FILE:$ZED_ROW",
"command": "bundle",
"args": ["exec", "tldr", "\"$ZED_RELATIVE_FILE:$ZED_ROW\""],
"command": "bundle exec tldr",
"args": ["\"$ZED_RELATIVE_FILE:$ZED_ROW\""],
"tags": ["ruby-test"]
}
]

View File

@@ -42,7 +42,7 @@ You can add this to Zed project settings (`.zed/settings.json`) or via your Zed
### Advanced Formatting
Sql-formatter also allows more precise control by providing [sql-formatter configuration options](https://github.com/sql-formatter-org/sql-formatter#configuration-options). To provide these, create a `.sql-formatter.json` file in your project:
Sql-formatter also allows more precise control by providing [sql-formatter configuration options](https://github.com/sql-formatter-org/sql-formatter#configuration-options). To provide these, create a `sql-formatter.json` file in your project:
```json
{
@@ -53,7 +53,7 @@ Sql-formatter also allows more precise control by providing [sql-formatter confi
}
```
When using a `.sql-formatter.json` file you can use a more simplified set of Zed settings since the language need not be specified inline:
When using a `sql-formatter.json` file you can use a more simplified set of Zed settings since the language need not be specified inline:
```json
"languages": {

View File

@@ -1,26 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
pattern='cmd-'
result=$(git grep --no-color --line-number --fixed-strings -e "$pattern" -- \
'assets/keymaps/' \
':(exclude)assets/keymaps/storybook.json' \
':(exclude)assets/keymaps/default-macos.json' \
':(exclude)assets/keymaps/macos/*.json' || true)
if [[ -n "${result}" ]]; then
echo "${result}"
echo "Error: Found 'cmd-' in non-macOS keymap files."
exit 1
fi
pattern='super-|win-|fn-'
result=$(git grep --no-color --line-number --fixed-strings -e "$pattern" -- \
'assets/keymaps/' || true)
if [[ -n "${result}" ]]; then
echo "${result}"
echo "Error: Found 'super-', 'win-', or 'fn-' in keymap files. Currently these aren't used."
exit 1
fi

View File

@@ -8,7 +8,7 @@ result=$(git grep --no-color --ignore-case --line-number --extended-regexp -e $p
':(exclude).github/workflows/ci.yml' \
':(exclude)*criteria.md' \
':(exclude)*prompt.md' || true)
echo "${result}"
if [[ -n "${result}" ]]; then
echo "${result}"
exit 1
fi