Compare commits

...

12 Commits

Author SHA1 Message Date
morgankrey
1e809aa190 change references to Inline Assist 2025-04-23 20:34:05 -05:00
morgankrey
6866528943 clean up organization of models section 2025-04-23 15:52:12 -05:00
morgankrey
d156fd4c09 start of agent docs 2025-04-23 13:36:38 -05:00
Richard Feldman
f6774ae60d More graceful invalid JSON handling (#29295)
Now we're more tolerant of invalid JSON coming back from the model
(possibly because it was incomplete and we're streaming), plus if we do
end up with invalid JSON once it has all streamed back, we report what
the malformed JSON actually was:

<img width="444" alt="Screenshot 2025-04-23 at 1 49 14 PM"
src="https://github.com/user-attachments/assets/480f5da7-869b-49f3-9ffd-8f08ccddb33d"
/>

Release Notes:

- N/A
2025-04-23 14:08:26 -04:00
Marshall Bowers
92e810bfec language_models: Pass up mode for completion requests through Zed (#29294)
This PR makes it so we pass up the `mode` for completion requests
through the Zed provider.

Release Notes:

- N/A
2025-04-23 18:02:03 +00:00
Cole Miller
724c935196 Highlight merge conflicts and provide for resolving them (#28065)
TODO:

- [x] Make it work in the project diff:
  - [x] Support non-singleton buffers
  - [x] Adjust excerpt boundaries to show full conflicts
- [x] Write tests for conflict-related events and state management
- [x] Prevent hunk buttons from appearing inside conflicts
- [x] Make sure it works over SSH, collab
- [x] Allow separate theming of markers

Bonus:

- [ ] Count of conflicts in toolbar
- [ ] Keyboard-driven navigation and resolution
- [ ] ~~Inlay hints to contextualize "ours"/"theirs"~~

Release Notes:

- Implemented initial support for resolving merge conflicts.

---------

Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
2025-04-23 12:38:46 -04:00
Kirill Bulatov
ef54b58346 Fix relative paths not properly resolved in the terminal during cmd-click (#29289)
Closes https://github.com/zed-industries/zed/pull/28342
Closes https://github.com/zed-industries/zed/issues/28339
Fixes
https://github.com/zed-industries/zed/pull/29274#issuecomment-2824794396

Release Notes:

- Fixed relative paths not properly resolved in the terminal during
cmd-click
2025-04-23 19:36:58 +03:00
Joseph T. Lyons
01bdd170ec Bump Zed to v0.185 (#29287)
Release Notes:

-N/A
2025-04-23 16:20:08 +00:00
Cole Miller
4b9f4feff1 debugger: Fix stack frame list flickering (#29282)
Closes #ISSUE

Release Notes:

- N/A *or* Added/Fixed/Improved ...

---------

Co-authored-by: Anthony Eid <hello@anthonyeid.me>
2025-04-23 16:12:53 +00:00
Bibiana André
19fb1e1b0d Fix workspace bottom obscured when bottom dock is full height (#27689)
When dragging the pane separator of the bottom dock to full window
height, the contents at the bottom of the dock and workspace window
overflowed the screen, becoming obscured. This happened because setting
a new size in resize_bottom_dock(...) was not taking in consideration
the top bounds of the workspace window, which caused the bottom bounds
of both dock and workspace to overflow. The issue was fixed by
subtracting the workspace.bounds.top() value to the dock's new size.

Closes #12966

Release Notes:

- N/A
2025-04-23 15:43:20 +00:00
Marshall Bowers
f2cb6d69d5 collab: Add head_commit_details column to project_repositories (#29284)
This PR adds the `head_commit_details` column to the
`project_repositories` table, since it was missed in
https://github.com/zed-industries/zed/pull/29007.

Release Notes:

- N/A
2025-04-23 15:35:49 +00:00
Bennet Bo Fenner
822b6f837d agent: Expose web search tool to beta users (#29273)
This gives all beta users access to the web search tool

Release Notes:

- agent: Added `web_search` tool
2025-04-23 15:30:20 +00:00
69 changed files with 2182 additions and 541 deletions

10
Cargo.lock generated
View File

@@ -703,9 +703,10 @@ dependencies = [
"anyhow",
"assistant_tool",
"chrono",
"client",
"clock",
"collections",
"component",
"feature_flags",
"futures 0.3.31",
"gpui",
"html_to_markdown",
@@ -16631,7 +16632,6 @@ version = "0.1.0"
dependencies = [
"anyhow",
"client",
"feature_flags",
"futures 0.3.31",
"gpui",
"http_client",
@@ -18209,7 +18209,7 @@ dependencies = [
[[package]]
name = "zed"
version = "0.184.0"
version = "0.185.0"
dependencies = [
"activity_indicator",
"agent",
@@ -18400,9 +18400,9 @@ dependencies = [
[[package]]
name = "zed_llm_client"
version = "0.6.1"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad17428120f5ca776dc5195e2411a282f5150a26d5536671f8943c622c31274f"
checksum = "3c1666cd923c5eb4635f3743e69c6920d0ed71f29b26920616a5d220607df7c4"
dependencies = [
"anyhow",
"serde",

View File

@@ -605,7 +605,7 @@ wasmtime-wasi = "29"
which = "6.0.0"
wit-component = "0.221"
workspace-hack = "0.1.0"
zed_llm_client = "0.6.1"
zed_llm_client = "0.7.0"
zstd = "0.11"
metal = "0.29"

View File

@@ -1328,7 +1328,7 @@ impl InlineAssistant {
editor.highlight_rows::<InlineAssist>(
row_range,
cx.theme().status().info_background,
false,
Default::default(),
cx,
);
}
@@ -1393,7 +1393,7 @@ impl InlineAssistant {
editor.highlight_rows::<DeletedLines>(
Anchor::min()..Anchor::max(),
cx.theme().status().deleted_background,
false,
Default::default(),
cx,
);
editor

View File

@@ -23,7 +23,6 @@ use gpui::{
use language::LanguageRegistry;
use language_model::{
AuthenticateError, ConfiguredModel, LanguageModelProviderId, LanguageModelRegistry,
ZED_CLOUD_PROVIDER_ID,
};
use project::Project;
use prompt_library::{PromptLibrary, open_prompt_library};
@@ -489,8 +488,8 @@ impl AssistantPanel {
// If we're signed out and don't have a provider configured, or we're signed-out AND Zed.dev is
// the provider, we want to show a nudge to sign in.
let show_zed_ai_notice = client_status.is_signed_out()
&& model.map_or(true, |model| model.provider.id().0 == ZED_CLOUD_PROVIDER_ID);
let show_zed_ai_notice =
client_status.is_signed_out() && model.map_or(true, |model| model.is_provided_by_zed());
self.show_zed_ai_notice = show_zed_ai_notice;
cx.notify();

View File

@@ -1226,7 +1226,7 @@ impl InlineAssistant {
editor.highlight_rows::<InlineAssist>(
row_range,
cx.theme().status().info_background,
false,
Default::default(),
cx,
);
}
@@ -1291,7 +1291,7 @@ impl InlineAssistant {
editor.highlight_rows::<DeletedLines>(
Anchor::min()..Anchor::max(),
cx.theme().status().deleted_background,
false,
Default::default(),
cx,
);
editor

View File

@@ -17,7 +17,6 @@ assistant_tool.workspace = true
chrono.workspace = true
collections.workspace = true
component.workspace = true
feature_flags.workspace = true
futures.workspace = true
gpui.workspace = true
html_to_markdown.workspace = true
@@ -41,6 +40,8 @@ worktree.workspace = true
zed_llm_client.workspace = true
[dev-dependencies]
client = { workspace = true, features = ["test-support"] }
clock = { workspace = true, features = ["test-support"] }
collections = { workspace = true, features = ["test-support"] }
gpui = { workspace = true, features = ["test-support"] }
language = { workspace = true, features = ["test-support"] }

View File

@@ -29,9 +29,9 @@ use std::sync::Arc;
use assistant_tool::ToolRegistry;
use copy_path_tool::CopyPathTool;
use feature_flags::FeatureFlagAppExt;
use gpui::App;
use http_client::HttpClientWithUrl;
use language_model::LanguageModelRegistry;
use move_path_tool::MovePathTool;
use web_search_tool::WebSearchTool;
@@ -85,34 +85,45 @@ pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut App) {
registry.register_tool(ThinkingTool);
registry.register_tool(FetchTool::new(http_client));
cx.observe_flag::<feature_flags::ZedProWebSearchTool, _>({
move |is_enabled, cx| {
if is_enabled {
ToolRegistry::global(cx).register_tool(WebSearchTool);
} else {
ToolRegistry::global(cx).unregister_tool(WebSearchTool);
cx.subscribe(
&LanguageModelRegistry::global(cx),
move |registry, event, cx| match event {
language_model::Event::DefaultModelChanged => {
let using_zed_provider = registry
.read(cx)
.default_model()
.map_or(false, |default| default.is_provided_by_zed());
if using_zed_provider {
ToolRegistry::global(cx).register_tool(WebSearchTool);
} else {
ToolRegistry::global(cx).unregister_tool(WebSearchTool);
}
}
}
})
_ => {}
},
)
.detach();
}
#[cfg(test)]
mod tests {
use client::Client;
use clock::FakeSystemClock;
use http_client::FakeHttpClient;
use super::*;
#[gpui::test]
fn test_builtin_tool_schema_compatibility(cx: &mut App) {
crate::init(
Arc::new(http_client::HttpClientWithUrl::new(
FakeHttpClient::with_200_response(),
"https://zed.dev",
None,
)),
settings::init(cx);
let client = Client::new(
Arc::new(FakeSystemClock::new()),
FakeHttpClient::with_200_response(),
cx,
);
language_model::init(client.clone(), cx);
crate::init(client.http_client(), cx);
for tool in ToolRegistry::global(cx).tools() {
let actual_schema = tool

View File

@@ -0,0 +1,2 @@
alter table project_repositories
add column head_commit_details varchar;

View File

@@ -411,12 +411,12 @@ impl RunningState {
.log_err();
if let Some(thread_id) = thread_id {
this.select_thread(*thread_id, cx);
this.select_thread(*thread_id, window, cx);
}
}
SessionEvent::Threads => {
let threads = this.session.update(cx, |this, cx| this.threads(cx));
this.select_current_thread(&threads, cx);
this.select_current_thread(&threads, window, cx);
}
SessionEvent::CapabilitiesLoaded => {
let capabilities = this.capabilities(cx);
@@ -731,6 +731,7 @@ impl RunningState {
pub fn select_current_thread(
&mut self,
threads: &Vec<(Thread, ThreadStatus)>,
window: &mut Window,
cx: &mut Context<Self>,
) {
let selected_thread = self
@@ -743,7 +744,7 @@ impl RunningState {
};
if Some(ThreadId(selected_thread.id)) != self.thread_id {
self.select_thread(ThreadId(selected_thread.id), cx);
self.select_thread(ThreadId(selected_thread.id), window, cx);
}
}
@@ -756,7 +757,7 @@ impl RunningState {
.map(|id| self.session().read(cx).thread_status(id))
}
fn select_thread(&mut self, thread_id: ThreadId, cx: &mut Context<Self>) {
fn select_thread(&mut self, thread_id: ThreadId, window: &mut Window, cx: &mut Context<Self>) {
if self.thread_id.is_some_and(|id| id == thread_id) {
return;
}
@@ -764,8 +765,7 @@ impl RunningState {
self.thread_id = Some(thread_id);
self.stack_frame_list
.update(cx, |list, cx| list.refresh(cx));
cx.notify();
.update(cx, |list, cx| list.schedule_refresh(true, window, cx));
}
pub fn continue_thread(&mut self, cx: &mut Context<Self>) {
@@ -917,9 +917,9 @@ impl RunningState {
for (thread, _) in threads {
let state = state.clone();
let thread_id = thread.id;
this = this.entry(thread.name, None, move |_, cx| {
this = this.entry(thread.name, None, move |window, cx| {
state.update(cx, |state, cx| {
state.select_thread(ThreadId(thread_id), cx);
state.select_thread(ThreadId(thread_id), window, cx);
});
});
}

View File

@@ -1,5 +1,6 @@
use std::path::Path;
use std::sync::Arc;
use std::time::Duration;
use anyhow::{Result, anyhow};
use dap::StackFrameId;
@@ -28,11 +29,11 @@ pub struct StackFrameList {
_subscription: Subscription,
session: Entity<Session>,
state: WeakEntity<RunningState>,
invalidate: bool,
entries: Vec<StackFrameEntry>,
workspace: WeakEntity<Workspace>,
selected_stack_frame_id: Option<StackFrameId>,
scrollbar_state: ScrollbarState,
_refresh_task: Task<()>,
}
#[allow(clippy::large_enum_variant)]
@@ -68,14 +69,17 @@ impl StackFrameList {
);
let _subscription =
cx.subscribe_in(&session, window, |this, _, event, _, cx| match event {
SessionEvent::Stopped(_) | SessionEvent::StackTrace | SessionEvent::Threads => {
this.refresh(cx);
cx.subscribe_in(&session, window, |this, _, event, window, cx| match event {
SessionEvent::Threads => {
this.schedule_refresh(false, window, cx);
}
SessionEvent::Stopped(..) | SessionEvent::StackTrace => {
this.schedule_refresh(true, window, cx);
}
_ => {}
});
Self {
let mut this = Self {
scrollbar_state: ScrollbarState::new(list.clone()),
list,
session,
@@ -83,10 +87,12 @@ impl StackFrameList {
focus_handle,
state,
_subscription,
invalidate: true,
entries: Default::default(),
selected_stack_frame_id: None,
}
_refresh_task: Task::ready(()),
};
this.schedule_refresh(true, window, cx);
this
}
#[cfg(test)]
@@ -136,10 +142,32 @@ impl StackFrameList {
self.selected_stack_frame_id
}
pub(super) fn refresh(&mut self, cx: &mut Context<Self>) {
self.invalidate = true;
self.entries.clear();
cx.notify();
pub(super) fn schedule_refresh(
&mut self,
select_first: bool,
window: &mut Window,
cx: &mut Context<Self>,
) {
const REFRESH_DEBOUNCE: Duration = Duration::from_millis(20);
self._refresh_task = cx.spawn_in(window, async move |this, cx| {
let debounce = this
.update(cx, |this, cx| {
let new_stack_frames = this.stack_frames(cx);
new_stack_frames.is_empty() && !this.entries.is_empty()
})
.ok()
.unwrap_or_default();
if debounce {
cx.background_executor().timer(REFRESH_DEBOUNCE).await;
}
this.update_in(cx, |this, window, cx| {
this.build_entries(select_first, window, cx);
cx.notify();
})
.ok();
})
}
pub fn build_entries(
@@ -515,13 +543,7 @@ impl StackFrameList {
}
impl Render for StackFrameList {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
if self.invalidate {
self.build_entries(self.entries.is_empty(), window, cx);
self.invalidate = false;
cx.notify();
}
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
div()
.size_full()
.p_1()

View File

@@ -152,7 +152,7 @@ async fn test_fetch_initial_stack_frames_and_go_to_stack_frame(
cx.run_until_parked();
// select first thread
active_debug_session_panel(workspace, cx).update_in(cx, |session, _, cx| {
active_debug_session_panel(workspace, cx).update_in(cx, |session, window, cx| {
session
.mode()
.as_running()
@@ -162,6 +162,7 @@ async fn test_fetch_initial_stack_frames_and_go_to_stack_frame(
&running_state
.session()
.update(cx, |session, cx| session.threads(cx)),
window,
cx,
);
});
@@ -330,7 +331,7 @@ async fn test_select_stack_frame(executor: BackgroundExecutor, cx: &mut TestAppC
cx.run_until_parked();
// select first thread
active_debug_session_panel(workspace, cx).update_in(cx, |session, _, cx| {
active_debug_session_panel(workspace, cx).update_in(cx, |session, window, cx| {
session
.mode()
.as_running()
@@ -340,6 +341,7 @@ async fn test_select_stack_frame(executor: BackgroundExecutor, cx: &mut TestAppC
&running_state
.session()
.update(cx, |session, cx| session.threads(cx)),
window,
cx,
);
});
@@ -704,7 +706,7 @@ async fn test_collapsed_entries(executor: BackgroundExecutor, cx: &mut TestAppCo
cx.run_until_parked();
// select first thread
active_debug_session_panel(workspace, cx).update_in(cx, |session, _, cx| {
active_debug_session_panel(workspace, cx).update_in(cx, |session, window, cx| {
session
.mode()
.as_running()
@@ -714,6 +716,7 @@ async fn test_collapsed_entries(executor: BackgroundExecutor, cx: &mut TestAppCo
&running_state
.session()
.update(cx, |session, cx| session.threads(cx)),
window,
cx,
);
});

View File

@@ -269,6 +269,12 @@ enum DocumentHighlightWrite {}
enum InputComposition {}
enum SelectedTextHighlight {}
pub enum ConflictsOuter {}
pub enum ConflictsOurs {}
pub enum ConflictsTheirs {}
pub enum ConflictsOursMarker {}
pub enum ConflictsTheirsMarker {}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum Navigated {
Yes,
@@ -694,6 +700,10 @@ pub trait Addon: 'static {
}
fn to_any(&self) -> &dyn std::any::Any;
fn to_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
None
}
}
/// A set of caret positions, registered when the editor was edited.
@@ -1083,11 +1093,27 @@ impl SelectionHistory {
}
}
#[derive(Clone, Copy)]
pub struct RowHighlightOptions {
pub autoscroll: bool,
pub include_gutter: bool,
}
impl Default for RowHighlightOptions {
fn default() -> Self {
Self {
autoscroll: Default::default(),
include_gutter: true,
}
}
}
struct RowHighlight {
index: usize,
range: Range<Anchor>,
color: Hsla,
should_autoscroll: bool,
options: RowHighlightOptions,
type_id: TypeId,
}
#[derive(Clone, Debug)]
@@ -5942,7 +5968,10 @@ impl Editor {
self.highlight_rows::<EditPredictionPreview>(
target..target,
cx.theme().colors().editor_highlighted_line_background,
true,
RowHighlightOptions {
autoscroll: true,
..Default::default()
},
cx,
);
self.request_autoscroll(Autoscroll::fit(), cx);
@@ -13449,7 +13478,7 @@ impl Editor {
start..end,
highlight_color
.unwrap_or_else(|| cx.theme().colors().editor_highlighted_line_background),
false,
Default::default(),
cx,
);
self.request_autoscroll(Autoscroll::center().for_anchor(start), cx);
@@ -16765,7 +16794,7 @@ impl Editor {
&mut self,
range: Range<Anchor>,
color: Hsla,
should_autoscroll: bool,
options: RowHighlightOptions,
cx: &mut Context<Self>,
) {
let snapshot = self.buffer().read(cx).snapshot(cx);
@@ -16797,7 +16826,7 @@ impl Editor {
merged = true;
prev_highlight.index = index;
prev_highlight.color = color;
prev_highlight.should_autoscroll = should_autoscroll;
prev_highlight.options = options;
}
}
@@ -16808,7 +16837,8 @@ impl Editor {
range: range.clone(),
index,
color,
should_autoscroll,
options,
type_id: TypeId::of::<T>(),
},
);
}
@@ -16914,7 +16944,15 @@ impl Editor {
used_highlight_orders.entry(row).or_insert(highlight.index);
if highlight.index >= *used_index {
*used_index = highlight.index;
unique_rows.insert(DisplayRow(row), highlight.color.into());
unique_rows.insert(
DisplayRow(row),
LineHighlight {
include_gutter: highlight.options.include_gutter,
border: None,
background: highlight.color.into(),
type_id: Some(highlight.type_id),
},
);
}
}
unique_rows
@@ -16930,7 +16968,7 @@ impl Editor {
.values()
.flat_map(|highlighted_rows| highlighted_rows.iter())
.filter_map(|highlight| {
if highlight.should_autoscroll {
if highlight.options.autoscroll {
Some(highlight.range.start.to_display_point(snapshot).row())
} else {
None
@@ -17405,13 +17443,19 @@ impl Editor {
});
self.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx);
}
multi_buffer::Event::ExcerptsRemoved { ids } => {
multi_buffer::Event::ExcerptsRemoved {
ids,
removed_buffer_ids,
} => {
self.refresh_inlay_hints(InlayHintRefreshReason::ExcerptsRemoved(ids.clone()), cx);
let buffer = self.buffer.read(cx);
self.registered_buffers
.retain(|buffer_id, _| buffer.buffer(*buffer_id).is_some());
jsx_tag_auto_close::refresh_enabled_in_any_buffer(self, multibuffer, cx);
cx.emit(EditorEvent::ExcerptsRemoved { ids: ids.clone() })
cx.emit(EditorEvent::ExcerptsRemoved {
ids: ids.clone(),
removed_buffer_ids: removed_buffer_ids.clone(),
})
}
multi_buffer::Event::ExcerptsEdited {
excerpt_ids,
@@ -18219,6 +18263,13 @@ impl Editor {
.and_then(|item| item.to_any().downcast_ref::<T>())
}
pub fn addon_mut<T: Addon>(&mut self) -> Option<&mut T> {
let type_id = std::any::TypeId::of::<T>();
self.addons
.get_mut(&type_id)
.and_then(|item| item.to_any_mut()?.downcast_mut::<T>())
}
fn character_size(&self, window: &mut Window) -> gpui::Size<Pixels> {
let text_layout_details = self.text_layout_details(window);
let style = &text_layout_details.editor_style;
@@ -19732,6 +19783,7 @@ pub enum EditorEvent {
},
ExcerptsRemoved {
ids: Vec<ExcerptId>,
removed_buffer_ids: Vec<BufferId>,
},
BufferFoldToggled {
ids: Vec<ExcerptId>,
@@ -20672,24 +20724,8 @@ impl Render for MissingEditPredictionKeybindingTooltip {
pub struct LineHighlight {
pub background: Background,
pub border: Option<gpui::Hsla>,
}
impl From<Hsla> for LineHighlight {
fn from(hsla: Hsla) -> Self {
Self {
background: hsla.into(),
border: None,
}
}
}
impl From<Background> for LineHighlight {
fn from(background: Background) -> Self {
Self {
background,
border: None,
}
}
pub include_gutter: bool,
pub type_id: Option<TypeId>,
}
fn render_diff_hunk_controls(

View File

@@ -1,6 +1,7 @@
use crate::{
ActiveDiagnostic, BlockId, COLUMNAR_SELECTION_MODIFIERS, CURSORS_VISIBLE_FOR,
ChunkRendererContext, ChunkReplacement, ContextMenuPlacement, CursorShape, CustomBlockId,
ChunkRendererContext, ChunkReplacement, 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,
@@ -4036,6 +4037,7 @@ impl EditorElement {
line_height: Pixels,
scroll_pixel_position: gpui::Point<Pixels>,
display_hunks: &[(DisplayDiffHunk, Option<Hitbox>)],
highlighted_rows: &BTreeMap<DisplayRow, LineHighlight>,
editor: Entity<Editor>,
window: &mut Window,
cx: &mut App,
@@ -4064,6 +4066,22 @@ impl EditorElement {
{
continue;
}
if highlighted_rows
.get(&display_row_range.start)
.and_then(|highlight| highlight.type_id)
.is_some_and(|type_id| {
[
TypeId::of::<ConflictsOuter>(),
TypeId::of::<ConflictsOursMarker>(),
TypeId::of::<ConflictsOurs>(),
TypeId::of::<ConflictsTheirs>(),
TypeId::of::<ConflictsTheirsMarker>(),
]
.contains(&type_id)
})
{
continue;
}
let row_ix = (display_row_range.start - row_range.start).0 as usize;
if row_infos[row_ix].diff_status.is_none() {
continue;
@@ -4258,14 +4276,21 @@ impl EditorElement {
highlight_row_end: DisplayRow,
highlight: crate::LineHighlight,
edges| {
let mut origin_x = layout.hitbox.left();
let mut width = layout.hitbox.size.width;
if !highlight.include_gutter {
origin_x += layout.gutter_hitbox.size.width;
width -= layout.gutter_hitbox.size.width;
}
let origin = point(
layout.hitbox.origin.x,
origin_x,
layout.hitbox.origin.y
+ (highlight_row_start.as_f32() - scroll_top)
* layout.position_map.line_height,
);
let size = size(
layout.hitbox.size.width,
width,
layout.position_map.line_height
* highlight_row_end.next_row().minus(highlight_row_start) as f32,
);
@@ -6789,10 +6814,16 @@ impl Element for EditorElement {
} else {
background_color.opacity(0.36)
}),
include_gutter: true,
type_id: None,
};
let filled_highlight =
solid_background(background_color.opacity(hunk_opacity)).into();
let filled_highlight = LineHighlight {
background: solid_background(background_color.opacity(hunk_opacity)),
border: None,
include_gutter: true,
type_id: None,
};
let background = if Self::diff_hunk_hollow(diff_status, cx) {
hollow_highlight
@@ -7551,6 +7582,7 @@ impl Element for EditorElement {
line_height,
scroll_pixel_position,
&display_hunks,
&highlighted_rows,
self.editor.clone(),
window,
cx,

View File

@@ -288,7 +288,7 @@ impl FollowableItem for Editor {
}
true
}
EditorEvent::ExcerptsRemoved { ids } => {
EditorEvent::ExcerptsRemoved { ids, .. } => {
update
.deleted_excerpts
.extend(ids.iter().map(ExcerptId::to_proto));

View File

@@ -84,11 +84,6 @@ impl FeatureFlag for ZedPro {
const NAME: &'static str = "zed-pro";
}
pub struct ZedProWebSearchTool {}
impl FeatureFlag for ZedProWebSearchTool {
const NAME: &'static str = "zed-pro-web-search-tool";
}
pub struct NotebookFeatureFlag;
impl FeatureFlag for NotebookFeatureFlag {

View File

@@ -34,6 +34,7 @@ pub struct FakeGitRepositoryState {
pub blames: HashMap<RepoPath, Blame>,
pub current_branch_name: Option<String>,
pub branches: HashSet<String>,
pub merge_head_shas: Vec<String>,
pub simulated_index_write_error_message: Option<String>,
}
@@ -47,12 +48,20 @@ impl FakeGitRepositoryState {
blames: Default::default(),
current_branch_name: Default::default(),
branches: Default::default(),
merge_head_shas: Default::default(),
simulated_index_write_error_message: Default::default(),
}
}
}
impl FakeGitRepository {
fn with_state<F, T>(&self, write: bool, f: F) -> Result<T>
where
F: FnOnce(&mut FakeGitRepositoryState) -> T,
{
self.fs.with_git_state(&self.dot_git_path, write, f)
}
fn with_state_async<F, T>(&self, write: bool, f: F) -> BoxFuture<'static, Result<T>>
where
F: 'static + Send + FnOnce(&mut FakeGitRepositoryState) -> Result<T>,
@@ -137,11 +146,18 @@ impl GitRepository for FakeGitRepository {
}
fn merge_head_shas(&self) -> Vec<String> {
vec![]
self.with_state(false, |state| state.merge_head_shas.clone())
.unwrap()
}
fn show(&self, _commit: String) -> BoxFuture<Result<CommitDetails>> {
unimplemented!()
fn show(&self, commit: String) -> BoxFuture<Result<CommitDetails>> {
async {
Ok(CommitDetails {
sha: commit.into(),
..Default::default()
})
}
.boxed()
}
fn reset(

View File

@@ -133,7 +133,7 @@ pub struct CommitSummary {
pub has_parent: bool,
}
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
#[derive(Clone, Debug, Default, Hash, PartialEq, Eq)]
pub struct CommitDetails {
pub sha: SharedString,
pub message: SharedString,

View File

@@ -0,0 +1,473 @@
use collections::{HashMap, HashSet};
use editor::{
ConflictsOurs, ConflictsOursMarker, ConflictsOuter, ConflictsTheirs, ConflictsTheirsMarker,
Editor, EditorEvent, ExcerptId, MultiBuffer, RowHighlightOptions,
display_map::{BlockContext, BlockPlacement, BlockProperties, BlockStyle, CustomBlockId},
};
use gpui::{
App, Context, Entity, InteractiveElement as _, ParentElement as _, Subscription, WeakEntity,
};
use language::{Anchor, Buffer, BufferId};
use project::{ConflictRegion, ConflictSet, ConflictSetUpdate};
use std::{ops::Range, sync::Arc};
use ui::{
ActiveTheme, AnyElement, Element as _, StatefulInteractiveElement, Styled,
StyledTypography as _, div, h_flex, rems,
};
pub(crate) struct ConflictAddon {
buffers: HashMap<BufferId, BufferConflicts>,
}
impl ConflictAddon {
pub(crate) fn conflict_set(&self, buffer_id: BufferId) -> Option<Entity<ConflictSet>> {
self.buffers
.get(&buffer_id)
.map(|entry| entry.conflict_set.clone())
}
}
struct BufferConflicts {
block_ids: Vec<(Range<Anchor>, CustomBlockId)>,
conflict_set: Entity<ConflictSet>,
_subscription: Subscription,
}
impl editor::Addon for ConflictAddon {
fn to_any(&self) -> &dyn std::any::Any {
self
}
fn to_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
Some(self)
}
}
pub fn register_editor(editor: &mut Editor, buffer: Entity<MultiBuffer>, cx: &mut Context<Editor>) {
// Only show conflict UI for singletons and in the project diff.
if !editor.buffer().read(cx).is_singleton()
&& !editor.buffer().read(cx).all_diff_hunks_expanded()
{
return;
}
editor.register_addon(ConflictAddon {
buffers: Default::default(),
});
let buffers = buffer.read(cx).all_buffers().clone();
for buffer in buffers {
buffer_added(editor, buffer, cx);
}
cx.subscribe(&cx.entity(), |editor, _, event, cx| match event {
EditorEvent::ExcerptsAdded { buffer, .. } => buffer_added(editor, buffer.clone(), cx),
EditorEvent::ExcerptsExpanded { ids } => {
let multibuffer = editor.buffer().read(cx).snapshot(cx);
for excerpt_id in ids {
let Some(buffer) = multibuffer.buffer_for_excerpt(*excerpt_id) else {
continue;
};
let addon = editor.addon::<ConflictAddon>().unwrap();
let Some(conflict_set) = addon.conflict_set(buffer.remote_id()).clone() else {
return;
};
excerpt_for_buffer_updated(editor, conflict_set, cx);
}
}
EditorEvent::ExcerptsRemoved {
removed_buffer_ids, ..
} => buffers_removed(editor, removed_buffer_ids, cx),
_ => {}
})
.detach();
}
fn excerpt_for_buffer_updated(
editor: &mut Editor,
conflict_set: Entity<ConflictSet>,
cx: &mut Context<Editor>,
) {
let conflicts_len = conflict_set.read(cx).snapshot().conflicts.len();
conflicts_updated(
editor,
conflict_set,
&ConflictSetUpdate {
buffer_range: None,
old_range: 0..conflicts_len,
new_range: 0..conflicts_len,
},
cx,
);
}
fn buffer_added(editor: &mut Editor, buffer: Entity<Buffer>, cx: &mut Context<Editor>) {
let Some(project) = &editor.project else {
return;
};
let git_store = project.read(cx).git_store().clone();
let buffer_conflicts = editor
.addon_mut::<ConflictAddon>()
.unwrap()
.buffers
.entry(buffer.read(cx).remote_id())
.or_insert_with(|| {
let conflict_set = git_store.update(cx, |git_store, cx| {
git_store.open_conflict_set(buffer.clone(), cx)
});
let subscription = cx.subscribe(&conflict_set, conflicts_updated);
BufferConflicts {
block_ids: Vec::new(),
conflict_set: conflict_set.clone(),
_subscription: subscription,
}
});
let conflict_set = buffer_conflicts.conflict_set.clone();
let conflicts_len = conflict_set.read(cx).snapshot().conflicts.len();
let addon_conflicts_len = buffer_conflicts.block_ids.len();
conflicts_updated(
editor,
conflict_set,
&ConflictSetUpdate {
buffer_range: None,
old_range: 0..addon_conflicts_len,
new_range: 0..conflicts_len,
},
cx,
);
}
fn buffers_removed(editor: &mut Editor, removed_buffer_ids: &[BufferId], cx: &mut Context<Editor>) {
let mut removed_block_ids = HashSet::default();
editor
.addon_mut::<ConflictAddon>()
.unwrap()
.buffers
.retain(|buffer_id, buffer| {
if removed_buffer_ids.contains(&buffer_id) {
removed_block_ids.extend(buffer.block_ids.iter().map(|(_, block_id)| *block_id));
false
} else {
true
}
});
editor.remove_blocks(removed_block_ids, None, cx);
}
fn conflicts_updated(
editor: &mut Editor,
conflict_set: Entity<ConflictSet>,
event: &ConflictSetUpdate,
cx: &mut Context<Editor>,
) {
let buffer_id = conflict_set.read(cx).snapshot.buffer_id;
let conflict_set = conflict_set.read(cx).snapshot();
let multibuffer = editor.buffer().read(cx);
let snapshot = multibuffer.snapshot(cx);
let excerpts = multibuffer.excerpts_for_buffer(buffer_id, cx);
let Some(buffer_snapshot) = excerpts
.first()
.and_then(|(excerpt_id, _)| snapshot.buffer_for_excerpt(*excerpt_id))
else {
return;
};
// Remove obsolete highlights and blocks
let conflict_addon = editor.addon_mut::<ConflictAddon>().unwrap();
if let Some(buffer_conflicts) = conflict_addon.buffers.get_mut(&buffer_id) {
let old_conflicts = buffer_conflicts.block_ids[event.old_range.clone()].to_owned();
let mut removed_highlighted_ranges = Vec::new();
let mut removed_block_ids = HashSet::default();
for (conflict_range, block_id) in old_conflicts {
let Some((excerpt_id, _)) = excerpts.iter().find(|(_, range)| {
let precedes_start = range
.context
.start
.cmp(&conflict_range.start, &buffer_snapshot)
.is_le();
let follows_end = range
.context
.end
.cmp(&conflict_range.start, &buffer_snapshot)
.is_ge();
precedes_start && follows_end
}) else {
continue;
};
let excerpt_id = *excerpt_id;
let Some(range) = snapshot
.anchor_in_excerpt(excerpt_id, conflict_range.start)
.zip(snapshot.anchor_in_excerpt(excerpt_id, conflict_range.end))
.map(|(start, end)| start..end)
else {
continue;
};
removed_highlighted_ranges.push(range.clone());
removed_block_ids.insert(block_id);
}
editor.remove_highlighted_rows::<ConflictsOuter>(removed_highlighted_ranges.clone(), cx);
editor.remove_highlighted_rows::<ConflictsOurs>(removed_highlighted_ranges.clone(), cx);
editor
.remove_highlighted_rows::<ConflictsOursMarker>(removed_highlighted_ranges.clone(), cx);
editor.remove_highlighted_rows::<ConflictsTheirs>(removed_highlighted_ranges.clone(), cx);
editor.remove_highlighted_rows::<ConflictsTheirsMarker>(
removed_highlighted_ranges.clone(),
cx,
);
editor.remove_blocks(removed_block_ids, None, cx);
}
// Add new highlights and blocks
let editor_handle = cx.weak_entity();
let new_conflicts = &conflict_set.conflicts[event.new_range.clone()];
let mut blocks = Vec::new();
for conflict in new_conflicts {
let Some((excerpt_id, _)) = excerpts.iter().find(|(_, range)| {
let precedes_start = range
.context
.start
.cmp(&conflict.range.start, &buffer_snapshot)
.is_le();
let follows_end = range
.context
.end
.cmp(&conflict.range.start, &buffer_snapshot)
.is_ge();
precedes_start && follows_end
}) else {
continue;
};
let excerpt_id = *excerpt_id;
update_conflict_highlighting(editor, conflict, &snapshot, excerpt_id, cx);
let Some(anchor) = snapshot.anchor_in_excerpt(excerpt_id, conflict.range.start) else {
continue;
};
let editor_handle = editor_handle.clone();
blocks.push(BlockProperties {
placement: BlockPlacement::Above(anchor),
height: Some(1),
style: BlockStyle::Fixed,
render: Arc::new({
let conflict = conflict.clone();
move |cx| render_conflict_buttons(&conflict, excerpt_id, editor_handle.clone(), cx)
}),
priority: 0,
})
}
let new_block_ids = editor.insert_blocks(blocks, None, cx);
let conflict_addon = editor.addon_mut::<ConflictAddon>().unwrap();
if let Some(buffer_conflicts) = conflict_addon.buffers.get_mut(&buffer_id) {
buffer_conflicts.block_ids.splice(
event.old_range.clone(),
new_conflicts
.iter()
.map(|conflict| conflict.range.clone())
.zip(new_block_ids),
);
}
}
fn update_conflict_highlighting(
editor: &mut Editor,
conflict: &ConflictRegion,
buffer: &editor::MultiBufferSnapshot,
excerpt_id: editor::ExcerptId,
cx: &mut Context<Editor>,
) {
log::debug!("update conflict highlighting for {conflict:?}");
let theme = cx.theme().clone();
let colors = theme.colors();
let outer_start = buffer
.anchor_in_excerpt(excerpt_id, conflict.range.start)
.unwrap();
let outer_end = buffer
.anchor_in_excerpt(excerpt_id, conflict.range.end)
.unwrap();
let our_start = buffer
.anchor_in_excerpt(excerpt_id, conflict.ours.start)
.unwrap();
let our_end = buffer
.anchor_in_excerpt(excerpt_id, conflict.ours.end)
.unwrap();
let their_start = buffer
.anchor_in_excerpt(excerpt_id, conflict.theirs.start)
.unwrap();
let their_end = buffer
.anchor_in_excerpt(excerpt_id, conflict.theirs.end)
.unwrap();
let ours_background = colors.version_control_conflict_ours_background;
let ours_marker = colors.version_control_conflict_ours_marker_background;
let theirs_background = colors.version_control_conflict_theirs_background;
let theirs_marker = colors.version_control_conflict_theirs_marker_background;
let divider_background = colors.version_control_conflict_divider_background;
let options = RowHighlightOptions {
include_gutter: false,
..Default::default()
};
// Prevent diff hunk highlighting within the entire conflict region.
editor.highlight_rows::<ConflictsOuter>(
outer_start..outer_end,
divider_background,
options,
cx,
);
editor.highlight_rows::<ConflictsOurs>(our_start..our_end, ours_background, options, cx);
editor.highlight_rows::<ConflictsOursMarker>(outer_start..our_start, ours_marker, options, cx);
editor.highlight_rows::<ConflictsTheirs>(
their_start..their_end,
theirs_background,
options,
cx,
);
editor.highlight_rows::<ConflictsTheirsMarker>(
their_end..outer_end,
theirs_marker,
options,
cx,
);
}
fn render_conflict_buttons(
conflict: &ConflictRegion,
excerpt_id: ExcerptId,
editor: WeakEntity<Editor>,
cx: &mut BlockContext,
) -> AnyElement {
h_flex()
.h(cx.line_height)
.items_end()
.ml(cx.gutter_dimensions.width)
.id(cx.block_id)
.gap_0p5()
.child(
div()
.id("ours")
.px_1()
.child("Take Ours")
.rounded_t(rems(0.2))
.text_ui_sm(cx)
.hover(|this| this.bg(cx.theme().colors().element_background))
.cursor_pointer()
.on_click({
let editor = editor.clone();
let conflict = conflict.clone();
let ours = conflict.ours.clone();
move |_, _, cx| {
resolve_conflict(editor.clone(), excerpt_id, &conflict, &[ours.clone()], cx)
}
}),
)
.child(
div()
.id("theirs")
.px_1()
.child("Take Theirs")
.rounded_t(rems(0.2))
.text_ui_sm(cx)
.hover(|this| this.bg(cx.theme().colors().element_background))
.cursor_pointer()
.on_click({
let editor = editor.clone();
let conflict = conflict.clone();
let theirs = conflict.theirs.clone();
move |_, _, cx| {
resolve_conflict(
editor.clone(),
excerpt_id,
&conflict,
&[theirs.clone()],
cx,
)
}
}),
)
.child(
div()
.id("both")
.px_1()
.child("Take Both")
.rounded_t(rems(0.2))
.text_ui_sm(cx)
.hover(|this| this.bg(cx.theme().colors().element_background))
.cursor_pointer()
.on_click({
let editor = editor.clone();
let conflict = conflict.clone();
let ours = conflict.ours.clone();
let theirs = conflict.theirs.clone();
move |_, _, cx| {
resolve_conflict(
editor.clone(),
excerpt_id,
&conflict,
&[ours.clone(), theirs.clone()],
cx,
)
}
}),
)
.into_any()
}
fn resolve_conflict(
editor: WeakEntity<Editor>,
excerpt_id: ExcerptId,
resolved_conflict: &ConflictRegion,
ranges: &[Range<Anchor>],
cx: &mut App,
) {
let Some(editor) = editor.upgrade() else {
return;
};
let multibuffer = editor.read(cx).buffer().read(cx);
let snapshot = multibuffer.snapshot(cx);
let Some(buffer) = resolved_conflict
.ours
.end
.buffer_id
.and_then(|buffer_id| multibuffer.buffer(buffer_id))
else {
return;
};
let buffer_snapshot = buffer.read(cx).snapshot();
resolved_conflict.resolve(buffer, ranges, cx);
editor.update(cx, |editor, cx| {
let conflict_addon = editor.addon_mut::<ConflictAddon>().unwrap();
let Some(state) = conflict_addon.buffers.get_mut(&buffer_snapshot.remote_id()) else {
return;
};
let Ok(ix) = state.block_ids.binary_search_by(|(range, _)| {
range
.start
.cmp(&resolved_conflict.range.start, &buffer_snapshot)
}) else {
return;
};
let &(_, block_id) = &state.block_ids[ix];
let start = snapshot
.anchor_in_excerpt(excerpt_id, resolved_conflict.range.start)
.unwrap();
let end = snapshot
.anchor_in_excerpt(excerpt_id, resolved_conflict.range.end)
.unwrap();
editor.remove_highlighted_rows::<ConflictsOuter>(vec![start..end], cx);
editor.remove_highlighted_rows::<ConflictsOurs>(vec![start..end], cx);
editor.remove_highlighted_rows::<ConflictsTheirs>(vec![start..end], cx);
editor.remove_highlighted_rows::<ConflictsOursMarker>(vec![start..end], cx);
editor.remove_highlighted_rows::<ConflictsTheirsMarker>(vec![start..end], cx);
editor.remove_blocks(HashSet::from_iter([block_id]), None, cx);
})
}

View File

@@ -447,7 +447,7 @@ impl GitPanel {
.ok();
}
GitStoreEvent::RepositoryUpdated(_, _, _) => {}
GitStoreEvent::JobsUpdated => {}
GitStoreEvent::JobsUpdated | GitStoreEvent::ConflictsUpdated => {}
},
)
.detach();
@@ -1650,7 +1650,7 @@ impl GitPanel {
if let Some(merge_message) = self
.active_repository
.as_ref()
.and_then(|repo| repo.read(cx).merge_message.as_ref())
.and_then(|repo| repo.read(cx).merge.message.as_ref())
{
return Some(merge_message.to_string());
}

View File

@@ -3,6 +3,7 @@ use std::any::Any;
use ::settings::Settings;
use command_palette_hooks::CommandPaletteFilter;
use commit_modal::CommitModal;
use editor::Editor;
mod blame_ui;
use git::{
repository::{Branch, Upstream, UpstreamTracking, UpstreamTrackingStatus},
@@ -20,6 +21,7 @@ pub mod branch_picker;
mod commit_modal;
pub mod commit_tooltip;
mod commit_view;
mod conflict_view;
pub mod git_panel;
mod git_panel_settings;
pub mod onboarding;
@@ -35,6 +37,11 @@ pub fn init(cx: &mut App) {
editor::set_blame_renderer(blame_ui::GitBlameRenderer, cx);
cx.observe_new(|editor: &mut Editor, _, cx| {
conflict_view::register_editor(editor, editor.buffer().clone(), cx);
})
.detach();
cx.observe_new(|workspace: &mut Workspace, _, cx| {
ProjectDiff::register(workspace, cx);
CommitModal::register(workspace);

View File

@@ -1,4 +1,5 @@
use crate::{
conflict_view::ConflictAddon,
git_panel::{GitPanel, GitPanelAddon, GitStatusEntry},
remote_button::{render_publish_button, render_push_button},
};
@@ -26,7 +27,10 @@ use project::{
Project, ProjectPath,
git_store::{GitStore, GitStoreEvent, RepositoryEvent},
};
use std::any::{Any, TypeId};
use std::{
any::{Any, TypeId},
ops::Range,
};
use theme::ActiveTheme;
use ui::{KeyBinding, Tooltip, prelude::*, vertical_divider};
use util::ResultExt as _;
@@ -48,7 +52,6 @@ pub struct ProjectDiff {
focus_handle: FocusHandle,
update_needed: postage::watch::Sender<()>,
pending_scroll: Option<PathKey>,
current_branch: Option<Branch>,
_task: Task<Result<()>>,
_subscription: Subscription,
}
@@ -61,9 +64,9 @@ struct DiffBuffer {
file_status: FileStatus,
}
const CONFLICT_NAMESPACE: u32 = 0;
const TRACKED_NAMESPACE: u32 = 1;
const NEW_NAMESPACE: u32 = 2;
const CONFLICT_NAMESPACE: u32 = 1;
const TRACKED_NAMESPACE: u32 = 2;
const NEW_NAMESPACE: u32 = 3;
impl ProjectDiff {
pub(crate) fn register(workspace: &mut Workspace, cx: &mut Context<Workspace>) {
@@ -154,7 +157,8 @@ impl ProjectDiff {
window,
move |this, _git_store, event, _window, _cx| match event {
GitStoreEvent::ActiveRepositoryChanged(_)
| GitStoreEvent::RepositoryUpdated(_, RepositoryEvent::Updated { .. }, true) => {
| GitStoreEvent::RepositoryUpdated(_, RepositoryEvent::Updated { .. }, true)
| GitStoreEvent::ConflictsUpdated => {
*this.update_needed.borrow_mut() = ();
}
_ => {}
@@ -178,7 +182,6 @@ impl ProjectDiff {
multibuffer,
pending_scroll: None,
update_needed: send,
current_branch: None,
_task: worker,
_subscription: git_store_subscription,
}
@@ -395,11 +398,25 @@ impl ProjectDiff {
let buffer = diff_buffer.buffer;
let diff = diff_buffer.diff;
let conflict_addon = self
.editor
.read(cx)
.addon::<ConflictAddon>()
.expect("project diff editor should have a conflict addon");
let snapshot = buffer.read(cx).snapshot();
let diff = diff.read(cx);
let diff_hunk_ranges = diff
.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot, cx)
.map(|diff_hunk| diff_hunk.buffer_range.to_point(&snapshot))
.map(|diff_hunk| diff_hunk.buffer_range.clone());
let conflicts = conflict_addon
.conflict_set(snapshot.remote_id())
.map(|conflict_set| conflict_set.read(cx).snapshot().conflicts.clone())
.unwrap_or_default();
let conflicts = conflicts.iter().map(|conflict| conflict.range.clone());
let excerpt_ranges = merge_anchor_ranges(diff_hunk_ranges, conflicts, &snapshot)
.map(|range| range.to_point(&snapshot))
.collect::<Vec<_>>();
let (was_empty, is_excerpt_newly_added) = self.multibuffer.update(cx, |multibuffer, cx| {
@@ -407,7 +424,7 @@ impl ProjectDiff {
let (_, is_newly_added) = multibuffer.set_excerpts_for_path(
path_key.clone(),
buffer,
diff_hunk_ranges,
excerpt_ranges,
editor::DEFAULT_MULTIBUFFER_CONTEXT,
cx,
);
@@ -450,18 +467,6 @@ impl ProjectDiff {
cx: &mut AsyncWindowContext,
) -> Result<()> {
while let Some(_) = recv.next().await {
this.update(cx, |this, cx| {
let new_branch = this
.git_store
.read(cx)
.active_repository()
.and_then(|active_repository| active_repository.read(cx).branch.clone());
if new_branch != this.current_branch {
this.current_branch = new_branch;
cx.notify();
}
})?;
let buffers_to_load = this.update(cx, |this, cx| this.load_buffers(cx))?;
for buffer_to_load in buffers_to_load {
if let Some(buffer) = buffer_to_load.await.log_err() {
@@ -1127,47 +1132,6 @@ impl RenderOnce for ProjectDiffEmptyState {
}
}
// .when(self.can_push_and_pull, |this| {
// let remote_button = crate::render_remote_button(
// "project-diff-remote-button",
// &branch,
// self.focus_handle.clone(),
// false,
// );
// match remote_button {
// Some(button) => {
// this.child(h_flex().justify_around().child(button))
// }
// None => this.child(
// h_flex()
// .justify_around()
// .child(Label::new("Remote up to date")),
// ),
// }
// }),
//
// // .map(|this| {
// this.child(h_flex().justify_around().mt_1().child(
// Button::new("project-diff-close-button", "Close").when_some(
// self.focus_handle.clone(),
// |this, focus_handle| {
// this.key_binding(KeyBinding::for_action_in(
// &CloseActiveItem::default(),
// &focus_handle,
// window,
// cx,
// ))
// .on_click(move |_, window, cx| {
// window.focus(&focus_handle);
// window
// .dispatch_action(Box::new(CloseActiveItem::default()), cx);
// })
// },
// ),
// ))
// }),
mod preview {
use git::repository::{
Branch, CommitSummary, Upstream, UpstreamTracking, UpstreamTrackingStatus,
@@ -1293,6 +1257,53 @@ mod preview {
}
}
fn merge_anchor_ranges<'a>(
left: impl 'a + Iterator<Item = Range<Anchor>>,
right: impl 'a + Iterator<Item = Range<Anchor>>,
snapshot: &'a language::BufferSnapshot,
) -> impl 'a + Iterator<Item = Range<Anchor>> {
let mut left = left.fuse().peekable();
let mut right = right.fuse().peekable();
std::iter::from_fn(move || {
let Some(left_range) = left.peek() else {
return right.next();
};
let Some(right_range) = right.peek() else {
return left.next();
};
let mut next_range = if left_range.start.cmp(&right_range.start, snapshot).is_lt() {
left.next().unwrap()
} else {
right.next().unwrap()
};
// Extend the basic range while there's overlap with a range from either stream.
loop {
if let Some(left_range) = left
.peek()
.filter(|range| range.start.cmp(&next_range.end, &snapshot).is_le())
.cloned()
{
left.next();
next_range.end = left_range.end;
} else if let Some(right_range) = right
.peek()
.filter(|range| range.start.cmp(&next_range.end, &snapshot).is_le())
.cloned()
{
right.next();
next_range.end = right_range.end;
} else {
break;
}
}
Some(next_range)
})
}
#[cfg(not(target_os = "windows"))]
#[cfg(test)]
mod tests {

View File

@@ -2,7 +2,8 @@ pub mod cursor_position;
use cursor_position::{LineIndicatorFormat, UserCaretPosition};
use editor::{
Anchor, Editor, MultiBufferSnapshot, ToOffset, ToPoint, actions::Tab, scroll::Autoscroll,
Anchor, Editor, MultiBufferSnapshot, RowHighlightOptions, ToOffset, ToPoint, actions::Tab,
scroll::Autoscroll,
};
use gpui::{
App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Render, SharedString, Styled,
@@ -180,7 +181,10 @@ impl GoToLine {
editor.highlight_rows::<GoToLineRowHighlights>(
start..end,
cx.theme().colors().editor_highlighted_line_background,
true,
RowHighlightOptions {
autoscroll: true,
..Default::default()
},
cx,
);
editor.request_autoscroll(Autoscroll::center(), cx);

View File

@@ -42,6 +42,10 @@ impl ConfiguredModel {
pub fn is_same_as(&self, other: &ConfiguredModel) -> bool {
self.model.id() == other.model.id() && self.provider.id() == other.provider.id()
}
pub fn is_provided_by_zed(&self) -> bool {
self.provider.id().0 == crate::ZED_CLOUD_PROVIDER_ID
}
}
pub enum Event {

View File

@@ -714,39 +714,32 @@ pub fn map_to_language_model_completion_events(
if let Some(tool_use) = state.tool_uses_by_index.get_mut(&index) {
tool_use.input_json.push_str(&partial_json);
return Some((
vec![maybe!({
Ok(LanguageModelCompletionEvent::ToolUse(
// Try to convert invalid (incomplete) JSON into
// valid JSON that serde can accept, e.g. by closing
// unclosed delimiters. This way, we can update the
// UI with whatever has been streamed back so far.
if let Ok(input) = serde_json::Value::from_str(
&partial_json_fixer::fix_json(&tool_use.input_json),
) {
return Some((
vec![Ok(LanguageModelCompletionEvent::ToolUse(
LanguageModelToolUse {
id: tool_use.id.clone().into(),
name: tool_use.name.clone().into(),
is_input_complete: false,
input: if tool_use.input_json.is_empty() {
serde_json::Value::Object(
serde_json::Map::default(),
)
} else {
serde_json::Value::from_str(
// Convert invalid (incomplete) JSON into
// JSON that serde will accept, e.g. by closing
// unclosed delimiters. This way, we can update
// the UI with whatever has been streamed back so far.
&partial_json_fixer::fix_json(
&tool_use.input_json,
),
)
.map_err(|err| anyhow!(err))?
},
input,
},
))
})],
state,
));
))],
state,
));
}
}
}
},
Event::ContentBlockStop { index } => {
if let Some(tool_use) = state.tool_uses_by_index.remove(&index) {
let input_json = tool_use.input_json.trim();
return Some((
vec![maybe!({
Ok(LanguageModelCompletionEvent::ToolUse(
@@ -754,15 +747,15 @@ pub fn map_to_language_model_completion_events(
id: tool_use.id.into(),
name: tool_use.name.into(),
is_input_complete: true,
input: if tool_use.input_json.is_empty() {
input: if input_json.is_empty() {
serde_json::Value::Object(
serde_json::Map::default(),
)
} else {
serde_json::Value::from_str(
&tool_use.input_json,
input_json
)
.map_err(|err| anyhow!(err))?
.map_err(|err| anyhow!("Error parsing tool call input JSON: {err:?} - JSON string was: {input_json:?}"))?
},
},
))

View File

@@ -35,7 +35,7 @@ use strum::IntoEnumIterator;
use thiserror::Error;
use ui::{TintColor, prelude::*};
use zed_llm_client::{
CURRENT_PLAN_HEADER_NAME, CompletionBody, EXPIRED_LLM_TOKEN_HEADER_NAME,
CURRENT_PLAN_HEADER_NAME, CompletionBody, CompletionMode, EXPIRED_LLM_TOKEN_HEADER_NAME,
MAX_LLM_MONTHLY_SPEND_REACHED_HEADER_NAME, MODEL_REQUESTS_RESOURCE_HEADER_VALUE,
SUBSCRIPTION_LIMIT_RESOURCE_HEADER_NAME,
};
@@ -748,6 +748,7 @@ impl LanguageModel for CloudLanguageModel {
CompletionBody {
thread_id,
prompt_id,
mode: Some(CompletionMode::Max),
provider: zed_llm_client::LanguageModelProvider::Anthropic,
model: request.model.clone(),
provider_request: serde_json::to_value(&request)?,
@@ -794,6 +795,7 @@ impl LanguageModel for CloudLanguageModel {
CompletionBody {
thread_id,
prompt_id,
mode: Some(CompletionMode::Max),
provider: zed_llm_client::LanguageModelProvider::OpenAi,
model: request.model.clone(),
provider_request: serde_json::to_value(&request)?,
@@ -824,6 +826,7 @@ impl LanguageModel for CloudLanguageModel {
CompletionBody {
thread_id,
prompt_id,
mode: Some(CompletionMode::Max),
provider: zed_llm_client::LanguageModelProvider::Google,
model: request.model.clone(),
provider_request: serde_json::to_value(&request)?,

View File

@@ -95,6 +95,7 @@ pub enum Event {
},
ExcerptsRemoved {
ids: Vec<ExcerptId>,
removed_buffer_ids: Vec<BufferId>,
},
ExcerptsExpanded {
ids: Vec<ExcerptId>,
@@ -2021,7 +2022,12 @@ impl MultiBuffer {
pub fn clear(&mut self, cx: &mut Context<Self>) {
self.sync(cx);
let ids = self.excerpt_ids();
self.buffers.borrow_mut().clear();
let removed_buffer_ids = self
.buffers
.borrow_mut()
.drain()
.map(|(id, _)| id)
.collect();
self.excerpts_by_path.clear();
self.paths_by_excerpt.clear();
let mut snapshot = self.snapshot.borrow_mut();
@@ -2046,7 +2052,10 @@ impl MultiBuffer {
singleton_buffer_edited: false,
edited_buffer: None,
});
cx.emit(Event::ExcerptsRemoved { ids });
cx.emit(Event::ExcerptsRemoved {
ids,
removed_buffer_ids,
});
cx.notify();
}
@@ -2310,9 +2319,9 @@ impl MultiBuffer {
new_excerpts.append(suffix, &());
drop(cursor);
snapshot.excerpts = new_excerpts;
for buffer_id in removed_buffer_ids {
self.diffs.remove(&buffer_id);
snapshot.diffs.remove(&buffer_id);
for buffer_id in &removed_buffer_ids {
self.diffs.remove(buffer_id);
snapshot.diffs.remove(buffer_id);
}
if changed_trailing_excerpt {
@@ -2325,7 +2334,10 @@ impl MultiBuffer {
singleton_buffer_edited: false,
edited_buffer: None,
});
cx.emit(Event::ExcerptsRemoved { ids });
cx.emit(Event::ExcerptsRemoved {
ids,
removed_buffer_ids,
});
cx.notify();
}

View File

@@ -635,7 +635,7 @@ fn test_excerpt_events(cx: &mut App) {
predecessor,
excerpts,
} => follower.insert_excerpts_with_ids_after(predecessor, buffer, excerpts, cx),
Event::ExcerptsRemoved { ids } => follower.remove_excerpts(ids, cx),
Event::ExcerptsRemoved { ids, .. } => follower.remove_excerpts(ids, cx),
Event::Edited { .. } => {
*follower_edit_event_count.write() += 1;
}

View File

@@ -4,6 +4,7 @@ use std::{
sync::Arc,
};
use editor::RowHighlightOptions;
use editor::{Anchor, AnchorRangeExt, Editor, scroll::Autoscroll};
use fuzzy::StringMatch;
use gpui::{
@@ -171,7 +172,10 @@ impl OutlineViewDelegate {
active_editor.highlight_rows::<OutlineRowHighlights>(
outline_item.range.start..outline_item.range.end,
cx.theme().colors().editor_highlighted_line_background,
true,
RowHighlightOptions {
autoscroll: true,
..Default::default()
},
cx,
);
active_editor.request_autoscroll(Autoscroll::center(), cx);

View File

@@ -5028,7 +5028,7 @@ fn subscribe_for_editor_events(
.extend(excerpts.iter().map(|&(excerpt_id, _)| excerpt_id));
outline_panel.update_fs_entries(editor.clone(), debounce, window, cx);
}
EditorEvent::ExcerptsRemoved { ids } => {
EditorEvent::ExcerptsRemoved { ids, .. } => {
let mut ids = ids.iter().collect::<HashSet<_>>();
for excerpts in outline_panel.excerpts.values_mut() {
excerpts.retain(|excerpt_id, _| !ids.remove(excerpt_id));

View File

@@ -641,6 +641,7 @@ impl CompletionsQuery {
}
}
#[derive(Debug)]
pub enum SessionEvent {
Modules,
LoadedSources,

View File

@@ -1,3 +1,4 @@
mod conflict_set;
pub mod git_traversal;
use crate::{
@@ -10,11 +11,12 @@ use askpass::AskPassDelegate;
use buffer_diff::{BufferDiff, BufferDiffEvent};
use client::ProjectId;
use collections::HashMap;
pub use conflict_set::{ConflictRegion, ConflictSet, ConflictSetSnapshot, ConflictSetUpdate};
use fs::Fs;
use futures::{
FutureExt as _, StreamExt as _,
FutureExt, StreamExt as _,
channel::{mpsc, oneshot},
future::{self, Shared},
future::{self, Shared, try_join_all},
};
use git::{
BuildPermalinkParams, GitHostingProviderRegistry, WORK_DIRECTORY_REPO_PATH,
@@ -74,7 +76,7 @@ pub struct GitStore {
#[allow(clippy::type_complexity)]
loading_diffs:
HashMap<(BufferId, DiffKind), Shared<Task<Result<Entity<BufferDiff>, Arc<anyhow::Error>>>>>,
diffs: HashMap<BufferId, Entity<BufferDiffState>>,
diffs: HashMap<BufferId, Entity<BufferGitState>>,
shared_diffs: HashMap<proto::PeerId, HashMap<BufferId, SharedDiffs>>,
_subscriptions: Vec<Subscription>,
}
@@ -85,12 +87,15 @@ struct SharedDiffs {
uncommitted: Option<Entity<BufferDiff>>,
}
struct BufferDiffState {
struct BufferGitState {
unstaged_diff: Option<WeakEntity<BufferDiff>>,
uncommitted_diff: Option<WeakEntity<BufferDiff>>,
conflict_set: Option<WeakEntity<ConflictSet>>,
recalculate_diff_task: Option<Task<Result<()>>>,
reparse_conflict_markers_task: Option<Task<Result<()>>>,
language: Option<Arc<Language>>,
language_registry: Option<Arc<LanguageRegistry>>,
conflict_updated_futures: Vec<oneshot::Sender<()>>,
recalculating_tx: postage::watch::Sender<bool>,
/// These operation counts are used to ensure that head and index text
@@ -224,17 +229,26 @@ impl sum_tree::KeyedItem for StatusEntry {
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct RepositoryId(pub u64);
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct MergeDetails {
pub conflicted_paths: TreeSet<RepoPath>,
pub message: Option<SharedString>,
pub apply_head: Option<CommitDetails>,
pub cherry_pick_head: Option<CommitDetails>,
pub merge_heads: Vec<CommitDetails>,
pub rebase_head: Option<CommitDetails>,
pub revert_head: Option<CommitDetails>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RepositorySnapshot {
pub id: RepositoryId,
pub merge_message: Option<SharedString>,
pub statuses_by_path: SumTree<StatusEntry>,
pub work_directory_abs_path: Arc<Path>,
pub branch: Option<Branch>,
pub head_commit: Option<CommitDetails>,
pub merge_conflicts: TreeSet<RepoPath>,
pub merge_head_shas: Vec<SharedString>,
pub scan_id: u64,
pub merge: MergeDetails,
}
type JobId = u64;
@@ -297,6 +311,7 @@ pub enum GitStoreEvent {
RepositoryRemoved(RepositoryId),
IndexWriteError(anyhow::Error),
JobsUpdated,
ConflictsUpdated,
}
impl EventEmitter<RepositoryEvent> for Repository {}
@@ -681,10 +696,11 @@ impl GitStore {
let text_snapshot = buffer.text_snapshot();
this.loading_diffs.remove(&(buffer_id, kind));
let git_store = cx.weak_entity();
let diff_state = this
.diffs
.entry(buffer_id)
.or_insert_with(|| cx.new(|_| BufferDiffState::default()));
.or_insert_with(|| cx.new(|_| BufferGitState::new(git_store)));
let diff = cx.new(|cx| BufferDiff::new(&text_snapshot, cx));
@@ -737,6 +753,62 @@ impl GitStore {
diff_state.read(cx).uncommitted_diff.as_ref()?.upgrade()
}
pub fn open_conflict_set(
&mut self,
buffer: Entity<Buffer>,
cx: &mut Context<Self>,
) -> Entity<ConflictSet> {
log::debug!("open conflict set");
let buffer_id = buffer.read(cx).remote_id();
if let Some(git_state) = self.diffs.get(&buffer_id) {
if let Some(conflict_set) = git_state
.read(cx)
.conflict_set
.as_ref()
.and_then(|weak| weak.upgrade())
{
let conflict_set = conflict_set.clone();
let buffer_snapshot = buffer.read(cx).text_snapshot();
git_state.update(cx, |state, cx| {
let _ = state.reparse_conflict_markers(buffer_snapshot, cx);
});
return conflict_set;
}
}
let is_unmerged = self
.repository_and_path_for_buffer_id(buffer_id, cx)
.map_or(false, |(repo, path)| {
repo.read(cx)
.snapshot
.merge
.conflicted_paths
.contains(&path)
});
let git_store = cx.weak_entity();
let buffer_git_state = self
.diffs
.entry(buffer_id)
.or_insert_with(|| cx.new(|_| BufferGitState::new(git_store)));
let conflict_set = cx.new(|cx| ConflictSet::new(buffer_id, is_unmerged, cx));
self._subscriptions
.push(cx.subscribe(&conflict_set, |_, _, _, cx| {
cx.emit(GitStoreEvent::ConflictsUpdated);
}));
buffer_git_state.update(cx, |state, cx| {
state.conflict_set = Some(conflict_set.downgrade());
let buffer_snapshot = buffer.read(cx).text_snapshot();
let _ = state.reparse_conflict_markers(buffer_snapshot, cx);
});
conflict_set
}
pub fn project_path_git_status(
&self,
project_path: &ProjectPath,
@@ -1079,6 +1151,35 @@ impl GitStore {
cx: &mut Context<Self>,
) {
let id = repo.read(cx).id;
let merge_conflicts = repo.read(cx).snapshot.merge.conflicted_paths.clone();
for (buffer_id, diff) in self.diffs.iter() {
if let Some((buffer_repo, repo_path)) =
self.repository_and_path_for_buffer_id(*buffer_id, cx)
{
if buffer_repo == repo {
diff.update(cx, |diff, cx| {
if let Some(conflict_set) = &diff.conflict_set {
let conflict_status_changed =
conflict_set.update(cx, |conflict_set, cx| {
let has_conflict = merge_conflicts.contains(&repo_path);
conflict_set.set_has_conflict(has_conflict, cx)
})?;
if conflict_status_changed {
let buffer_store = self.buffer_store.read(cx);
if let Some(buffer) = buffer_store.get(*buffer_id) {
let _ = diff.reparse_conflict_markers(
buffer.read(cx).text_snapshot(),
cx,
);
}
}
}
anyhow::Ok(())
})
.ok();
}
}
}
cx.emit(GitStoreEvent::RepositoryUpdated(
id,
event.clone(),
@@ -1218,9 +1319,15 @@ impl GitStore {
if let Some(diff_state) = self.diffs.get_mut(&buffer.read(cx).remote_id()) {
let buffer = buffer.read(cx).text_snapshot();
diff_state.update(cx, |diff_state, cx| {
diff_state.recalculate_diffs(buffer, cx);
futures.extend(diff_state.wait_for_recalculation());
diff_state.recalculate_diffs(buffer.clone(), cx);
futures.extend(diff_state.wait_for_recalculation().map(FutureExt::boxed));
});
futures.push(diff_state.update(cx, |diff_state, cx| {
diff_state
.reparse_conflict_markers(buffer, cx)
.map(|_| {})
.boxed()
}));
}
}
async move {
@@ -2094,13 +2201,86 @@ impl GitStore {
}
}
impl BufferDiffState {
impl BufferGitState {
fn new(_git_store: WeakEntity<GitStore>) -> Self {
Self {
unstaged_diff: Default::default(),
uncommitted_diff: Default::default(),
recalculate_diff_task: Default::default(),
language: Default::default(),
language_registry: Default::default(),
recalculating_tx: postage::watch::channel_with(false).0,
hunk_staging_operation_count: 0,
hunk_staging_operation_count_as_of_write: 0,
head_text: Default::default(),
index_text: Default::default(),
head_changed: Default::default(),
index_changed: Default::default(),
language_changed: Default::default(),
conflict_updated_futures: Default::default(),
conflict_set: Default::default(),
reparse_conflict_markers_task: Default::default(),
}
}
fn buffer_language_changed(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
self.language = buffer.read(cx).language().cloned();
self.language_changed = true;
let _ = self.recalculate_diffs(buffer.read(cx).text_snapshot(), cx);
}
fn reparse_conflict_markers(
&mut self,
buffer: text::BufferSnapshot,
cx: &mut Context<Self>,
) -> oneshot::Receiver<()> {
let (tx, rx) = oneshot::channel();
let Some(conflict_set) = self
.conflict_set
.as_ref()
.and_then(|conflict_set| conflict_set.upgrade())
else {
return rx;
};
let old_snapshot = conflict_set.read_with(cx, |conflict_set, _| {
if conflict_set.has_conflict {
Some(conflict_set.snapshot())
} else {
None
}
});
if let Some(old_snapshot) = old_snapshot {
self.conflict_updated_futures.push(tx);
self.reparse_conflict_markers_task = Some(cx.spawn(async move |this, cx| {
let (snapshot, changed_range) = cx
.background_spawn(async move {
let new_snapshot = ConflictSet::parse(&buffer);
let changed_range = old_snapshot.compare(&new_snapshot, &buffer);
(new_snapshot, changed_range)
})
.await;
this.update(cx, |this, cx| {
if let Some(conflict_set) = &this.conflict_set {
conflict_set
.update(cx, |conflict_set, cx| {
conflict_set.set_snapshot(snapshot, changed_range, cx);
})
.ok();
}
let futures = std::mem::take(&mut this.conflict_updated_futures);
for tx in futures {
tx.send(()).ok();
}
})
}))
}
rx
}
fn unstaged_diff(&self) -> Option<Entity<BufferDiff>> {
self.unstaged_diff.as_ref().and_then(|set| set.upgrade())
}
@@ -2335,26 +2515,6 @@ impl BufferDiffState {
}
}
impl Default for BufferDiffState {
fn default() -> Self {
Self {
unstaged_diff: Default::default(),
uncommitted_diff: Default::default(),
recalculate_diff_task: Default::default(),
language: Default::default(),
language_registry: Default::default(),
recalculating_tx: postage::watch::channel_with(false).0,
hunk_staging_operation_count: 0,
hunk_staging_operation_count_as_of_write: 0,
head_text: Default::default(),
index_text: Default::default(),
head_changed: Default::default(),
index_changed: Default::default(),
language_changed: Default::default(),
}
}
}
fn make_remote_delegate(
this: Entity<GitStore>,
project_id: u64,
@@ -2397,14 +2557,12 @@ impl RepositorySnapshot {
fn empty(id: RepositoryId, work_directory_abs_path: Arc<Path>) -> Self {
Self {
id,
merge_message: None,
statuses_by_path: Default::default(),
work_directory_abs_path,
branch: None,
head_commit: None,
merge_conflicts: Default::default(),
merge_head_shas: Default::default(),
scan_id: 0,
merge: Default::default(),
}
}
@@ -2419,7 +2577,8 @@ impl RepositorySnapshot {
.collect(),
removed_statuses: Default::default(),
current_merge_conflicts: self
.merge_conflicts
.merge
.conflicted_paths
.iter()
.map(|repo_path| repo_path.to_proto())
.collect(),
@@ -2480,7 +2639,8 @@ impl RepositorySnapshot {
updated_statuses,
removed_statuses,
current_merge_conflicts: self
.merge_conflicts
.merge
.conflicted_paths
.iter()
.map(|path| path.as_ref().to_proto())
.collect(),
@@ -2515,7 +2675,7 @@ impl RepositorySnapshot {
}
pub fn has_conflict(&self, repo_path: &RepoPath) -> bool {
self.merge_conflicts.contains(repo_path)
self.merge.conflicted_paths.contains(repo_path)
}
/// This is the name that will be displayed in the repository selector for this repository.
@@ -2529,7 +2689,77 @@ impl RepositorySnapshot {
}
}
impl MergeDetails {
async fn load(
backend: &Arc<dyn GitRepository>,
status: &SumTree<StatusEntry>,
prev_snapshot: &RepositorySnapshot,
) -> Result<(MergeDetails, bool)> {
fn sha_eq<'a>(
l: impl IntoIterator<Item = &'a CommitDetails>,
r: impl IntoIterator<Item = &'a CommitDetails>,
) -> bool {
l.into_iter()
.map(|commit| &commit.sha)
.eq(r.into_iter().map(|commit| &commit.sha))
}
let merge_heads = try_join_all(
backend
.merge_head_shas()
.into_iter()
.map(|sha| backend.show(sha)),
)
.await?;
let cherry_pick_head = backend.show("CHERRY_PICK_HEAD".into()).await.ok();
let rebase_head = backend.show("REBASE_HEAD".into()).await.ok();
let revert_head = backend.show("REVERT_HEAD".into()).await.ok();
let apply_head = backend.show("APPLY_HEAD".into()).await.ok();
let message = backend.merge_message().await.map(SharedString::from);
let merge_heads_changed = !sha_eq(
merge_heads.as_slice(),
prev_snapshot.merge.merge_heads.as_slice(),
) || !sha_eq(
cherry_pick_head.as_ref(),
prev_snapshot.merge.cherry_pick_head.as_ref(),
) || !sha_eq(
apply_head.as_ref(),
prev_snapshot.merge.apply_head.as_ref(),
) || !sha_eq(
rebase_head.as_ref(),
prev_snapshot.merge.rebase_head.as_ref(),
) || !sha_eq(
revert_head.as_ref(),
prev_snapshot.merge.revert_head.as_ref(),
);
let conflicted_paths = if merge_heads_changed {
TreeSet::from_ordered_entries(
status
.iter()
.filter(|entry| entry.status.is_conflicted())
.map(|entry| entry.repo_path.clone()),
)
} else {
prev_snapshot.merge.conflicted_paths.clone()
};
let details = MergeDetails {
conflicted_paths,
message,
apply_head,
cherry_pick_head,
merge_heads,
rebase_head,
revert_head,
};
Ok((details, merge_heads_changed))
}
}
impl Repository {
pub fn snapshot(&self) -> RepositorySnapshot {
self.snapshot.clone()
}
fn local(
id: RepositoryId,
work_directory_abs_path: Arc<Path>,
@@ -3731,7 +3961,7 @@ impl Repository {
.as_ref()
.map(proto_to_commit_details);
self.snapshot.merge_conflicts = conflicted_paths;
self.snapshot.merge.conflicted_paths = conflicted_paths;
let edits = update
.removed_statuses
@@ -4321,16 +4551,6 @@ async fn compute_snapshot(
let branches = backend.branches().await?;
let branch = branches.into_iter().find(|branch| branch.is_head);
let statuses = backend.status(&[WORK_DIRECTORY_REPO_PATH.clone()]).await?;
let merge_message = backend
.merge_message()
.await
.and_then(|msg| Some(msg.lines().nth(0)?.to_owned().into()));
let merge_head_shas = backend
.merge_head_shas()
.into_iter()
.map(SharedString::from)
.collect();
let statuses_by_path = SumTree::from_iter(
statuses
.entries
@@ -4341,47 +4561,36 @@ async fn compute_snapshot(
}),
&(),
);
let (merge_details, merge_heads_changed) =
MergeDetails::load(&backend, &statuses_by_path, &prev_snapshot).await?;
let merge_head_shas_changed = merge_head_shas != prev_snapshot.merge_head_shas;
if merge_head_shas_changed
if merge_heads_changed
|| branch != prev_snapshot.branch
|| statuses_by_path != prev_snapshot.statuses_by_path
{
events.push(RepositoryEvent::Updated { full_scan: true });
}
let mut current_merge_conflicts = TreeSet::default();
for (repo_path, status) in statuses.entries.iter() {
if status.is_conflicted() {
current_merge_conflicts.insert(repo_path.clone());
}
}
// Cache merge conflict paths so they don't change from staging/unstaging,
// until the merge heads change (at commit time, etc.).
let mut merge_conflicts = prev_snapshot.merge_conflicts.clone();
if merge_head_shas_changed {
merge_conflicts = current_merge_conflicts;
if merge_heads_changed {
events.push(RepositoryEvent::MergeHeadsChanged);
}
// Useful when branch is None in detached head state
let head_commit = match backend.head_sha() {
Some(head_sha) => backend.show(head_sha).await.ok(),
Some(head_sha) => backend.show(head_sha).await.log_err(),
None => None,
};
let snapshot = RepositorySnapshot {
id,
merge_message,
statuses_by_path,
work_directory_abs_path,
scan_id: prev_snapshot.scan_id + 1,
branch,
head_commit,
merge_conflicts,
merge_head_shas,
merge: merge_details,
};
Ok((snapshot, events))

View File

@@ -0,0 +1,560 @@
use gpui::{App, Context, Entity, EventEmitter};
use std::{cmp::Ordering, ops::Range, sync::Arc};
use text::{Anchor, BufferId, OffsetRangeExt as _};
pub struct ConflictSet {
pub has_conflict: bool,
pub snapshot: ConflictSetSnapshot,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ConflictSetUpdate {
pub buffer_range: Option<Range<Anchor>>,
pub old_range: Range<usize>,
pub new_range: Range<usize>,
}
#[derive(Debug, Clone)]
pub struct ConflictSetSnapshot {
pub buffer_id: BufferId,
pub conflicts: Arc<[ConflictRegion]>,
}
impl ConflictSetSnapshot {
pub fn conflicts_in_range(
&self,
range: Range<Anchor>,
buffer: &text::BufferSnapshot,
) -> &[ConflictRegion] {
let start_ix = self
.conflicts
.binary_search_by(|conflict| {
conflict
.range
.end
.cmp(&range.start, buffer)
.then(Ordering::Greater)
})
.unwrap_err();
let end_ix = start_ix
+ self.conflicts[start_ix..]
.binary_search_by(|conflict| {
conflict
.range
.start
.cmp(&range.end, buffer)
.then(Ordering::Less)
})
.unwrap_err();
&self.conflicts[start_ix..end_ix]
}
pub fn compare(&self, other: &Self, buffer: &text::BufferSnapshot) -> ConflictSetUpdate {
let common_prefix_len = self
.conflicts
.iter()
.zip(other.conflicts.iter())
.take_while(|(old, new)| old == new)
.count();
let common_suffix_len = self.conflicts[common_prefix_len..]
.iter()
.rev()
.zip(other.conflicts[common_prefix_len..].iter().rev())
.take_while(|(old, new)| old == new)
.count();
let old_conflicts =
&self.conflicts[common_prefix_len..(self.conflicts.len() - common_suffix_len)];
let new_conflicts =
&other.conflicts[common_prefix_len..(other.conflicts.len() - common_suffix_len)];
let old_range = common_prefix_len..(common_prefix_len + old_conflicts.len());
let new_range = common_prefix_len..(common_prefix_len + new_conflicts.len());
let start = match (old_conflicts.first(), new_conflicts.first()) {
(None, None) => None,
(None, Some(conflict)) => Some(conflict.range.start),
(Some(conflict), None) => Some(conflict.range.start),
(Some(first), Some(second)) => Some(first.range.start.min(&second.range.start, buffer)),
};
let end = match (old_conflicts.last(), new_conflicts.last()) {
(None, None) => None,
(None, Some(conflict)) => Some(conflict.range.end),
(Some(first), None) => Some(first.range.end),
(Some(first), Some(second)) => Some(first.range.end.max(&second.range.end, buffer)),
};
ConflictSetUpdate {
buffer_range: start.zip(end).map(|(start, end)| start..end),
old_range,
new_range,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ConflictRegion {
pub range: Range<Anchor>,
pub ours: Range<Anchor>,
pub theirs: Range<Anchor>,
pub base: Option<Range<Anchor>>,
}
impl ConflictRegion {
pub fn resolve(
&self,
buffer: Entity<language::Buffer>,
ranges: &[Range<Anchor>],
cx: &mut App,
) {
let buffer_snapshot = buffer.read(cx).snapshot();
let mut deletions = Vec::new();
let empty = "";
let outer_range = self.range.to_offset(&buffer_snapshot);
let mut offset = outer_range.start;
for kept_range in ranges {
let kept_range = kept_range.to_offset(&buffer_snapshot);
if kept_range.start > offset {
deletions.push((offset..kept_range.start, empty));
}
offset = kept_range.end;
}
if outer_range.end > offset {
deletions.push((offset..outer_range.end, empty));
}
buffer.update(cx, |buffer, cx| {
buffer.edit(deletions, None, cx);
});
}
}
impl ConflictSet {
pub fn new(buffer_id: BufferId, has_conflict: bool, _: &mut Context<Self>) -> Self {
Self {
has_conflict,
snapshot: ConflictSetSnapshot {
buffer_id,
conflicts: Default::default(),
},
}
}
pub fn set_has_conflict(&mut self, has_conflict: bool, cx: &mut Context<Self>) -> bool {
if has_conflict != self.has_conflict {
self.has_conflict = has_conflict;
if !self.has_conflict {
cx.emit(ConflictSetUpdate {
buffer_range: None,
old_range: 0..self.snapshot.conflicts.len(),
new_range: 0..0,
});
self.snapshot.conflicts = Default::default();
}
true
} else {
false
}
}
pub fn snapshot(&self) -> ConflictSetSnapshot {
self.snapshot.clone()
}
pub fn set_snapshot(
&mut self,
snapshot: ConflictSetSnapshot,
update: ConflictSetUpdate,
cx: &mut Context<Self>,
) {
self.snapshot = snapshot;
cx.emit(update);
}
pub fn parse(buffer: &text::BufferSnapshot) -> ConflictSetSnapshot {
let mut conflicts = Vec::new();
let mut line_pos = 0;
let mut lines = buffer.text_for_range(0..buffer.len()).lines();
let mut conflict_start: Option<usize> = None;
let mut ours_start: Option<usize> = None;
let mut ours_end: Option<usize> = None;
let mut base_start: Option<usize> = None;
let mut base_end: Option<usize> = None;
let mut theirs_start: Option<usize> = None;
while let Some(line) = lines.next() {
let line_end = line_pos + line.len();
if line.starts_with("<<<<<<< ") {
// If we see a new conflict marker while already parsing one,
// abandon the previous one and start a new one
conflict_start = Some(line_pos);
ours_start = Some(line_end + 1);
} else if line.starts_with("||||||| ")
&& conflict_start.is_some()
&& ours_start.is_some()
{
ours_end = Some(line_pos);
base_start = Some(line_end + 1);
} else if line.starts_with("=======")
&& conflict_start.is_some()
&& ours_start.is_some()
{
// Set ours_end if not already set (would be set if we have base markers)
if ours_end.is_none() {
ours_end = Some(line_pos);
} else if base_start.is_some() {
base_end = Some(line_pos);
}
theirs_start = Some(line_end + 1);
} else if line.starts_with(">>>>>>> ")
&& conflict_start.is_some()
&& ours_start.is_some()
&& ours_end.is_some()
&& theirs_start.is_some()
{
let theirs_end = line_pos;
let conflict_end = line_end + 1;
let range = buffer.anchor_after(conflict_start.unwrap())
..buffer.anchor_before(conflict_end);
let ours = buffer.anchor_after(ours_start.unwrap())
..buffer.anchor_before(ours_end.unwrap());
let theirs =
buffer.anchor_after(theirs_start.unwrap())..buffer.anchor_before(theirs_end);
let base = base_start
.zip(base_end)
.map(|(start, end)| buffer.anchor_after(start)..buffer.anchor_before(end));
conflicts.push(ConflictRegion {
range,
ours,
theirs,
base,
});
conflict_start = None;
ours_start = None;
ours_end = None;
base_start = None;
base_end = None;
theirs_start = None;
}
line_pos = line_end + 1;
}
ConflictSetSnapshot {
conflicts: conflicts.into(),
buffer_id: buffer.remote_id(),
}
}
}
impl EventEmitter<ConflictSetUpdate> for ConflictSet {}
#[cfg(test)]
mod tests {
use std::sync::mpsc;
use crate::{Project, project_settings::ProjectSettings};
use super::*;
use fs::FakeFs;
use git::status::{UnmergedStatus, UnmergedStatusCode};
use gpui::{BackgroundExecutor, TestAppContext};
use language::language_settings::AllLanguageSettings;
use serde_json::json;
use settings::Settings as _;
use text::{Buffer, BufferId, ToOffset as _};
use unindent::Unindent as _;
use util::path;
use worktree::WorktreeSettings;
#[test]
fn test_parse_conflicts_in_buffer() {
// Create a buffer with conflict markers
let test_content = r#"
This is some text before the conflict.
<<<<<<< HEAD
This is our version
=======
This is their version
>>>>>>> branch-name
Another conflict:
<<<<<<< HEAD
Our second change
||||||| merged common ancestors
Original content
=======
Their second change
>>>>>>> branch-name
"#
.unindent();
let buffer_id = BufferId::new(1).unwrap();
let buffer = Buffer::new(0, buffer_id, test_content);
let snapshot = buffer.snapshot();
let conflict_snapshot = ConflictSet::parse(&snapshot);
assert_eq!(conflict_snapshot.conflicts.len(), 2);
let first = &conflict_snapshot.conflicts[0];
assert!(first.base.is_none());
let our_text = snapshot
.text_for_range(first.ours.clone())
.collect::<String>();
let their_text = snapshot
.text_for_range(first.theirs.clone())
.collect::<String>();
assert_eq!(our_text, "This is our version\n");
assert_eq!(their_text, "This is their version\n");
let second = &conflict_snapshot.conflicts[1];
assert!(second.base.is_some());
let our_text = snapshot
.text_for_range(second.ours.clone())
.collect::<String>();
let their_text = snapshot
.text_for_range(second.theirs.clone())
.collect::<String>();
let base_text = snapshot
.text_for_range(second.base.as_ref().unwrap().clone())
.collect::<String>();
assert_eq!(our_text, "Our second change\n");
assert_eq!(their_text, "Their second change\n");
assert_eq!(base_text, "Original content\n");
// Test conflicts_in_range
let range = snapshot.anchor_before(0)..snapshot.anchor_before(snapshot.len());
let conflicts_in_range = conflict_snapshot.conflicts_in_range(range, &snapshot);
assert_eq!(conflicts_in_range.len(), 2);
// Test with a range that includes only the first conflict
let first_conflict_end = conflict_snapshot.conflicts[0].range.end;
let range = snapshot.anchor_before(0)..first_conflict_end;
let conflicts_in_range = conflict_snapshot.conflicts_in_range(range, &snapshot);
assert_eq!(conflicts_in_range.len(), 1);
// Test with a range that includes only the second conflict
let second_conflict_start = conflict_snapshot.conflicts[1].range.start;
let range = second_conflict_start..snapshot.anchor_before(snapshot.len());
let conflicts_in_range = conflict_snapshot.conflicts_in_range(range, &snapshot);
assert_eq!(conflicts_in_range.len(), 1);
// Test with a range that doesn't include any conflicts
let range = buffer.anchor_after(first_conflict_end.to_offset(&buffer) + 1)
..buffer.anchor_before(second_conflict_start.to_offset(&buffer) - 1);
let conflicts_in_range = conflict_snapshot.conflicts_in_range(range, &snapshot);
assert_eq!(conflicts_in_range.len(), 0);
}
#[test]
fn test_nested_conflict_markers() {
// Create a buffer with nested conflict markers
let test_content = r#"
This is some text before the conflict.
<<<<<<< HEAD
This is our version
<<<<<<< HEAD
This is a nested conflict marker
=======
This is their version in a nested conflict
>>>>>>> branch-nested
=======
This is their version
>>>>>>> branch-name
"#
.unindent();
let buffer_id = BufferId::new(1).unwrap();
let buffer = Buffer::new(0, buffer_id, test_content.to_string());
let snapshot = buffer.snapshot();
let conflict_snapshot = ConflictSet::parse(&snapshot);
assert_eq!(conflict_snapshot.conflicts.len(), 1);
// The conflict should have our version, their version, but no base
let conflict = &conflict_snapshot.conflicts[0];
assert!(conflict.base.is_none());
// Check that the nested conflict was detected correctly
let our_text = snapshot
.text_for_range(conflict.ours.clone())
.collect::<String>();
assert_eq!(our_text, "This is a nested conflict marker\n");
let their_text = snapshot
.text_for_range(conflict.theirs.clone())
.collect::<String>();
assert_eq!(their_text, "This is their version in a nested conflict\n");
}
#[test]
fn test_conflicts_in_range() {
// Create a buffer with conflict markers
let test_content = r#"
one
<<<<<<< HEAD1
two
=======
three
>>>>>>> branch1
four
five
<<<<<<< HEAD2
six
=======
seven
>>>>>>> branch2
eight
nine
<<<<<<< HEAD3
ten
=======
eleven
>>>>>>> branch3
twelve
<<<<<<< HEAD4
thirteen
=======
fourteen
>>>>>>> branch4
fifteen
"#
.unindent();
let buffer_id = BufferId::new(1).unwrap();
let buffer = Buffer::new(0, buffer_id, test_content.clone());
let snapshot = buffer.snapshot();
let conflict_snapshot = ConflictSet::parse(&snapshot);
assert_eq!(conflict_snapshot.conflicts.len(), 4);
let range = test_content.find("seven").unwrap()..test_content.find("eleven").unwrap();
let range = buffer.anchor_before(range.start)..buffer.anchor_after(range.end);
assert_eq!(
conflict_snapshot.conflicts_in_range(range, &snapshot),
&conflict_snapshot.conflicts[1..=2]
);
let range = test_content.find("one").unwrap()..test_content.find("<<<<<<< HEAD2").unwrap();
let range = buffer.anchor_before(range.start)..buffer.anchor_after(range.end);
assert_eq!(
conflict_snapshot.conflicts_in_range(range, &snapshot),
&conflict_snapshot.conflicts[0..=1]
);
let range =
test_content.find("eight").unwrap() - 1..test_content.find(">>>>>>> branch3").unwrap();
let range = buffer.anchor_before(range.start)..buffer.anchor_after(range.end);
assert_eq!(
conflict_snapshot.conflicts_in_range(range, &snapshot),
&conflict_snapshot.conflicts[1..=2]
);
let range = test_content.find("thirteen").unwrap() - 1..test_content.len();
let range = buffer.anchor_before(range.start)..buffer.anchor_after(range.end);
assert_eq!(
conflict_snapshot.conflicts_in_range(range, &snapshot),
&conflict_snapshot.conflicts[3..=3]
);
}
#[gpui::test]
async fn test_conflict_updates(executor: BackgroundExecutor, cx: &mut TestAppContext) {
env_logger::try_init().ok();
cx.update(|cx| {
settings::init(cx);
WorktreeSettings::register(cx);
ProjectSettings::register(cx);
AllLanguageSettings::register(cx);
});
let initial_text = "
one
two
three
four
five
"
.unindent();
let fs = FakeFs::new(executor);
fs.insert_tree(
path!("/project"),
json!({
".git": {},
"a.txt": initial_text,
}),
)
.await;
let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
let (git_store, buffer) = project.update(cx, |project, cx| {
(
project.git_store().clone(),
project.open_local_buffer(path!("/project/a.txt"), cx),
)
});
let buffer = buffer.await.unwrap();
let conflict_set = git_store.update(cx, |git_store, cx| {
git_store.open_conflict_set(buffer.clone(), cx)
});
let (events_tx, events_rx) = mpsc::channel::<ConflictSetUpdate>();
let _conflict_set_subscription = cx.update(|cx| {
cx.subscribe(&conflict_set, move |_, event, _| {
events_tx.send(event.clone()).ok();
})
});
let conflicts_snapshot = conflict_set.update(cx, |conflict_set, _| conflict_set.snapshot());
assert!(conflicts_snapshot.conflicts.is_empty());
buffer.update(cx, |buffer, cx| {
buffer.edit(
[
(4..4, "<<<<<<< HEAD\n"),
(14..14, "=======\nTWO\n>>>>>>> branch\n"),
],
None,
cx,
);
});
cx.run_until_parked();
events_rx.try_recv().expect_err(
"no conflicts should be registered as long as the file's status is unchanged",
);
fs.with_git_state(path!("/project/.git").as_ref(), true, |state| {
state.unmerged_paths.insert(
"a.txt".into(),
UnmergedStatus {
first_head: UnmergedStatusCode::Updated,
second_head: UnmergedStatusCode::Updated,
},
);
// Cause the repository to emit MergeHeadsChanged.
state.merge_head_shas = vec!["abc".into(), "def".into()]
})
.unwrap();
cx.run_until_parked();
let update = events_rx
.try_recv()
.expect("status change should trigger conflict parsing");
assert_eq!(update.old_range, 0..0);
assert_eq!(update.new_range, 0..1);
let conflict = conflict_set.update(cx, |conflict_set, _| {
conflict_set.snapshot().conflicts[0].clone()
});
cx.update(|cx| {
conflict.resolve(buffer.clone(), &[conflict.theirs.clone()], cx);
});
cx.run_until_parked();
let update = events_rx
.try_recv()
.expect("conflicts should be removed after resolution");
assert_eq!(update.old_range, 0..1);
assert_eq!(update.new_range, 0..0);
}
}

View File

@@ -29,7 +29,10 @@ pub mod search_history;
mod yarn;
use crate::git_store::GitStore;
pub use git_store::git_traversal::{ChildEntriesGitIter, GitEntry, GitEntryRef, GitTraversal};
pub use git_store::{
ConflictRegion, ConflictSet, ConflictSetSnapshot, ConflictSetUpdate,
git_traversal::{ChildEntriesGitIter, GitEntry, GitEntryRef, GitTraversal},
};
use anyhow::{Context as _, Result, anyhow};
use buffer_store::{BufferStore, BufferStoreEvent};

View File

@@ -115,6 +115,7 @@ pub struct TerminalView {
blinking_paused: bool,
blink_epoch: usize,
hover_target_tooltip: Option<String>,
hover_tooltip_update: Task<()>,
workspace_id: Option<WorkspaceId>,
show_breadcrumbs: bool,
block_below_cursor: Option<Rc<BlockProperties>>,
@@ -197,6 +198,7 @@ impl TerminalView {
blinking_paused: false,
blink_epoch: 0,
hover_target_tooltip: None,
hover_tooltip_update: Task::ready(()),
workspace_id,
show_breadcrumbs: TerminalSettings::get_global(cx).toolbar.breadcrumbs,
block_below_cursor: None,
@@ -844,7 +846,7 @@ fn subscribe_for_terminal_events(
let terminal_events_subscription = cx.subscribe_in(
terminal,
window,
move |this, _, event, window, cx| match event {
move |terminal_view, _, event, window, cx| match event {
Event::Wakeup => {
cx.notify();
cx.emit(Event::Wakeup);
@@ -853,7 +855,7 @@ fn subscribe_for_terminal_events(
}
Event::Bell => {
this.has_bell = true;
terminal_view.has_bell = true;
cx.emit(Event::Wakeup);
}
@@ -862,7 +864,7 @@ fn subscribe_for_terminal_events(
TerminalSettings::get_global(cx).blinking,
TerminalBlink::TerminalControlled
) {
this.blinking_terminal_enabled = *blinking;
terminal_view.blinking_terminal_enabled = *blinking;
}
}
@@ -871,25 +873,46 @@ fn subscribe_for_terminal_events(
}
Event::NewNavigationTarget(maybe_navigation_target) => {
this.hover_target_tooltip =
maybe_navigation_target
.as_ref()
.and_then(|navigation_target| match navigation_target {
MaybeNavigationTarget::Url(url) => Some(url.clone()),
MaybeNavigationTarget::PathLike(path_like_target) => {
let valid_files_to_open_task = possible_open_target(
&workspace,
&path_like_target.terminal_dir,
&path_like_target.maybe_path,
cx,
);
Some(match smol::block_on(valid_files_to_open_task)? {
OpenTarget::File(path, _) | OpenTarget::Worktree(path, _) => {
path.to_string(|path| path.to_string_lossy().to_string())
}
})
}
});
match maybe_navigation_target.as_ref() {
None => {
terminal_view.hover_target_tooltip = None;
terminal_view.hover_tooltip_update = Task::ready(());
}
Some(MaybeNavigationTarget::Url(url)) => {
terminal_view.hover_target_tooltip = Some(url.clone());
terminal_view.hover_tooltip_update = Task::ready(());
}
Some(MaybeNavigationTarget::PathLike(path_like_target)) => {
let valid_files_to_open_task = possible_open_target(
&workspace,
&path_like_target.terminal_dir,
&path_like_target.maybe_path,
cx,
);
terminal_view.hover_tooltip_update =
cx.spawn(async move |terminal_view, cx| {
let file_to_open = valid_files_to_open_task.await;
terminal_view
.update(cx, |terminal_view, _| match file_to_open {
Some(
OpenTarget::File(path, _)
| OpenTarget::Worktree(path, _),
) => {
terminal_view.hover_target_tooltip =
Some(path.to_string(|path| {
path.to_string_lossy().to_string()
}));
}
None => {
terminal_view.hover_target_tooltip = None;
}
})
.ok();
});
}
}
cx.notify()
}
@@ -897,7 +920,7 @@ fn subscribe_for_terminal_events(
MaybeNavigationTarget::Url(url) => cx.open_url(url),
MaybeNavigationTarget::PathLike(path_like_target) => {
if this.hover_target_tooltip.is_none() {
if terminal_view.hover_target_tooltip.is_none() {
return;
}
let task_workspace = workspace.clone();
@@ -1207,9 +1230,12 @@ fn possible_open_target(
let fs = workspace.read(cx).project().read(cx).fs().clone();
cx.background_spawn(async move {
for path_to_check in fs_paths_to_check {
if let Some(metadata) = fs.metadata(&path_to_check.path).await.ok().flatten() {
return Some(OpenTarget::File(path_to_check, metadata));
for mut path_to_check in fs_paths_to_check {
if let Some(fs_path_to_check) = fs.canonicalize(&path_to_check.path).await.ok() {
if let Some(metadata) = fs.metadata(&fs_path_to_check).await.ok().flatten() {
path_to_check.path = fs_path_to_check;
return Some(OpenTarget::File(path_to_check, metadata));
}
}
}

View File

@@ -143,6 +143,11 @@ impl ThemeColors {
version_control_renamed: MODIFIED_COLOR,
version_control_conflict: orange().light().step_12(),
version_control_ignored: gray().light().step_12(),
version_control_conflict_ours_background: green().light().step_10().alpha(0.5),
version_control_conflict_theirs_background: blue().light().step_10().alpha(0.5),
version_control_conflict_ours_marker_background: green().light().step_10().alpha(0.7),
version_control_conflict_theirs_marker_background: blue().light().step_10().alpha(0.7),
version_control_conflict_divider_background: Hsla::default(),
}
}
@@ -258,6 +263,11 @@ impl ThemeColors {
version_control_renamed: MODIFIED_COLOR,
version_control_conflict: orange().dark().step_12(),
version_control_ignored: gray().dark().step_12(),
version_control_conflict_ours_background: green().dark().step_10().alpha(0.5),
version_control_conflict_theirs_background: blue().dark().step_10().alpha(0.5),
version_control_conflict_ours_marker_background: green().dark().step_10().alpha(0.7),
version_control_conflict_theirs_marker_background: blue().dark().step_10().alpha(0.7),
version_control_conflict_divider_background: Hsla::default(),
}
}
}

View File

@@ -201,6 +201,23 @@ pub(crate) fn zed_default_dark() -> Theme {
version_control_renamed: MODIFIED_COLOR,
version_control_conflict: crate::orange().light().step_12(),
version_control_ignored: crate::gray().light().step_12(),
version_control_conflict_ours_background: crate::green()
.light()
.step_12()
.alpha(0.5),
version_control_conflict_theirs_background: crate::blue()
.light()
.step_12()
.alpha(0.5),
version_control_conflict_ours_marker_background: crate::green()
.light()
.step_12()
.alpha(0.7),
version_control_conflict_theirs_marker_background: crate::blue()
.light()
.step_12()
.alpha(0.7),
version_control_conflict_divider_background: Hsla::default(),
},
status: StatusColors {
conflict: yellow,

View File

@@ -586,6 +586,26 @@ pub struct ThemeColorsContent {
/// Ignored version control color.
#[serde(rename = "version_control.ignored")]
pub version_control_ignored: Option<String>,
/// Background color for row highlights of "ours" regions in merge conflicts.
#[serde(rename = "version_control.conflict.ours_background")]
pub version_control_conflict_ours_background: Option<String>,
/// Background color for row highlights of "theirs" regions in merge conflicts.
#[serde(rename = "version_control.conflict.theirs_background")]
pub version_control_conflict_theirs_background: Option<String>,
/// Background color for row highlights of "ours" conflict markers in merge conflicts.
#[serde(rename = "version_control.conflict.ours_marker_background")]
pub version_control_conflict_ours_marker_background: Option<String>,
/// Background color for row highlights of "theirs" conflict markers in merge conflicts.
#[serde(rename = "version_control.conflict.theirs_marker_background")]
pub version_control_conflict_theirs_marker_background: Option<String>,
/// Background color for row highlights of the "ours"/"theirs" divider in merge conflicts.
#[serde(rename = "version_control.conflict.divider_background")]
pub version_control_conflict_divider_background: Option<String>,
}
impl ThemeColorsContent {
@@ -1037,6 +1057,26 @@ impl ThemeColorsContent {
.and_then(|color| try_parse_color(color).ok())
// Fall back to `conflict`, for backwards compatibility.
.or(status_colors.ignored),
version_control_conflict_ours_background: self
.version_control_conflict_ours_background
.as_ref()
.and_then(|color| try_parse_color(color).ok()),
version_control_conflict_theirs_background: self
.version_control_conflict_theirs_background
.as_ref()
.and_then(|color| try_parse_color(color).ok()),
version_control_conflict_ours_marker_background: self
.version_control_conflict_ours_marker_background
.as_ref()
.and_then(|color| try_parse_color(color).ok()),
version_control_conflict_theirs_marker_background: self
.version_control_conflict_theirs_marker_background
.as_ref()
.and_then(|color| try_parse_color(color).ok()),
version_control_conflict_divider_background: self
.version_control_conflict_divider_background
.as_ref()
.and_then(|color| try_parse_color(color).ok()),
}
}
}

View File

@@ -261,6 +261,14 @@ pub struct ThemeColors {
pub version_control_conflict: Hsla,
/// Represents an ignored entry in version control systems.
pub version_control_ignored: Hsla,
/// Represents the "ours" region of a merge conflict.
pub version_control_conflict_ours_background: Hsla,
/// Represents the "theirs" region of a merge conflict.
pub version_control_conflict_theirs_background: Hsla,
pub version_control_conflict_ours_marker_background: Hsla,
pub version_control_conflict_theirs_marker_background: Hsla,
pub version_control_conflict_divider_background: Hsla,
}
#[derive(EnumIter, Debug, Clone, Copy, AsRefStr)]

View File

@@ -1500,7 +1500,7 @@ impl ShellExec {
editor.highlight_rows::<ShellExec>(
input_range.clone().unwrap(),
cx.theme().status().unreachable_background,
false,
Default::default(),
cx,
);

View File

@@ -61,4 +61,11 @@ impl WebSearchRegistry {
self.active_provider = Some(provider);
}
}
pub fn unregister_provider(&mut self, id: WebSearchProviderId) {
self.providers.remove(&id);
if self.active_provider.as_ref().map(|provider| provider.id()) == Some(id) {
self.active_provider = None;
}
}
}

View File

@@ -14,7 +14,6 @@ path = "src/web_search_providers.rs"
[dependencies]
anyhow.workspace = true
client.workspace = true
feature_flags.workspace = true
futures.workspace = true
gpui.workspace = true
http_client.workspace = true

View File

@@ -50,9 +50,11 @@ impl State {
}
}
pub const ZED_WEB_SEARCH_PROVIDER_ID: &'static str = "zed.dev";
impl WebSearchProvider for CloudWebSearchProvider {
fn id(&self) -> WebSearchProviderId {
WebSearchProviderId("zed.dev".into())
WebSearchProviderId(ZED_WEB_SEARCH_PROVIDER_ID.into())
}
fn search(&self, query: String, cx: &mut App) -> Task<Result<WebSearchResponse>> {

View File

@@ -1,10 +1,10 @@
mod cloud;
use client::Client;
use feature_flags::{FeatureFlagAppExt, ZedProWebSearchTool};
use gpui::{App, Context};
use language_model::LanguageModelRegistry;
use std::sync::Arc;
use web_search::WebSearchRegistry;
use web_search::{WebSearchProviderId, WebSearchRegistry};
pub fn init(client: Arc<Client>, cx: &mut App) {
let registry = WebSearchRegistry::global(cx);
@@ -18,18 +18,27 @@ fn register_web_search_providers(
client: Arc<Client>,
cx: &mut Context<WebSearchRegistry>,
) {
cx.observe_flag::<ZedProWebSearchTool, _>({
let client = client.clone();
move |is_enabled, cx| {
if is_enabled {
WebSearchRegistry::global(cx).update(cx, |registry, cx| {
registry.register_provider(
cx.subscribe(
&LanguageModelRegistry::global(cx),
move |this, registry, event, cx| match event {
language_model::Event::DefaultModelChanged => {
let using_zed_provider = registry
.read(cx)
.default_model()
.map_or(false, |default| default.is_provided_by_zed());
if using_zed_provider {
this.register_provider(
cloud::CloudWebSearchProvider::new(client.clone(), cx),
cx,
);
});
)
} else {
this.unregister_provider(WebSearchProviderId(
cloud::ZED_WEB_SEARCH_PROVIDER_ID.into(),
));
}
}
}
})
_ => {}
},
)
.detach();
}

View File

@@ -5879,7 +5879,8 @@ fn resize_bottom_dock(
window: &mut Window,
cx: &mut App,
) {
let size = new_size.min(workspace.bounds.bottom() - RESIZE_HANDLE_SIZE);
let size =
new_size.min(workspace.bounds.bottom() - RESIZE_HANDLE_SIZE - workspace.bounds.top());
workspace.bottom_dock.update(cx, |bottom_dock, cx| {
bottom_dock.resize_active_panel(Some(size), window, cx);
});

View File

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

View File

@@ -4,12 +4,14 @@ Welcome to Zed's documentation.
This is built on push to `main` and published automatically to [https://zed.dev/docs](https://zed.dev/docs).
To preview the docs locally you will need to install [mdBook](https://rust-lang.github.io/mdBook/) (`cargo install mdbook`) and then run:
To preview the docs locally you will need to install [mdBook](https://rust-lang.github.io/mdBook/) (`cargo install mdbook@0.4.40`) and then run:
```sh
mdbook serve docs
```
It's important to note the version number above. For an unknown reason, as of 2025-04-23, running 0.4.48 will cause odd URL behavior that breaks docs.
Before committing, verify that the docs are formatted in the way prettier expects with:
```

View File

@@ -21,8 +21,8 @@ enable = false
"/ruby.html" = "/docs/languages/ruby.html"
"/python.html" = "/docs/languages/python.html"
"/adding-new-languages.html" = "/docs/extensions/languages.html"
"/language-model-integration.html" = "/docs/assistant/assistant.html"
"/assistant.html" = "/docs/assistant/assistant.html"
# "/language-model-integration.html" = "/docs/agent/assistant.html"
# "/assistant.html" = "/docs/agent/assistant.html"
"/developing-zed.html" = "/docs/development.html"
"/conversations.html" = "/community-links"

View File

@@ -37,18 +37,26 @@
- [Environment Variables](./environment.md)
- [REPL](./repl.md)
# Assistant
# Agent
- [Overview](./assistant/assistant.md)
- [Configuration](./assistant/configuration.md)
- [Assistant Panel](./assistant/assistant-panel.md)
- [Contexts](./assistant/contexts.md)
- [Inline Assistant](./assistant/inline-assistant.md)
- [Commands](./assistant/commands.md)
- [Prompts](./assistant/prompting.md)
- [Context Servers](./assistant/context-servers.md)
- [Model Context Protocol](./assistant/model-context-protocol.md)
- [Model Improvement](./model-improvement.md)
- [Overview](./agent/assistant.md)
- [Subscription](./agent/subscription.md)
- [Plans and Usage](./agent/plans-and-usage.md)
- [Billing](./agent/billing.md)
- [Models](./agent/models.md)
- [Configuration](./agent/configuration.md)
- [Custom API Keys](./agent/custom-api-keys.md)
- [Product](./agent/product.md)
- [Assistant Panel](./agent/assistant-panel.md)
- [Inline Assistant](./agent/inline-assist.md)
- [Contexts](./agent/contexts.md)
- [Commands](./agent/commands.md)
- [Prompts](./agent/prompting.md)
- [Enhancing the Agent](./agent/enhancing.md)
- [Context Servers](./agent/context-servers.md)
- [Model Context Protocol](./agent/model-context-protocol.md)
- [Privacy and Security](./agent/privacy-and-security.md)
- [Model Improvement](./agent/model-improvement.md)
# Extensions

View File

@@ -6,7 +6,7 @@ This section covers various aspects of the Assistant:
- [Assistant Panel](./assistant-panel.md): Create and collaboratively edit new chats, and manage interactions with language models.
- [Inline Assistant](./inline-assistant.md): Discover how to use the Assistant to power inline transformations directly within your code editor and terminal.
- [Inline Assist](./inline-assist.md): Discover how to use the Inline Assist feature to power inline transformations directly within your code editor and terminal.
- [Providers & Configuration](./configuration.md): Configure the Assistant, and set up different language model providers like Anthropic, OpenAI, Ollama, LM Studio, Google Gemini, and GitHub Copilot Chat.

35
docs/src/agent/billing.md Normal file
View File

@@ -0,0 +1,35 @@
# Billing
We use Stripe as our billing and payments provider. All Pro plans require payment via credit card. For invoice based billing, a Business plan is required. Contact sales@zed.dev for more details.
## Settings {#settings}
You can access billing settings at /account. Clicking [button] will navigate you to Stripes secure portal, where you can update all billing-related settings and configuration.
## Billing Cycles {#billing-cycles}
Zed is billed on a monthly basis based on the date you initially subscribe. Well also bill for additional prompts used beyond your plans prompt limit, if usage exceeds $20 before month end. See [usage-based pricing](./plans-and-usage.md#ubp) for more.
## Invoice History {#invoice-history}
You can access your invoice history by navigating to /account and clicking [button]. From Stripes secure portal, you can download all current and historical invoices.
## Updating Billing Information {#updating-billing-info}
You can update your payment method, company name, address, and tax information through the billing portal. We use Stripe as our payment processor to ensure secure transactions. Please note that changes to billing information will **only** affect future invoices - **we cannot modify historical invoices**.
## Cancellation and Refunds {#cancel-refund}
You can cancel your subscription directly through the billing portal using the “Cancel subscription” button. Your access will continue until the end of your current billing period.
You can self-serve a refund by going to the billing portal and clicking on the Cancel subscription button. Our self-serve refund policy is as follows:
**EU, UK or Turkey customers**
Eligible for a refund if you cancel your subscription within 14 days of purchase.
**All other customers (US + rest of world)**
Refundable within 24 hours after purchase.
If youre not in the window of self-serve refunds, reach out at billing-support@zed.dev and well be happy to assist you.

View File

@@ -0,0 +1,165 @@
# Configuring the Assistant
Here's a bird's-eye view of all the configuration options available in Zed's Assistant:
- Configure Custom API Keys for LLM Providers
- [Custom API Keys](./custom-api-keys.md)
- Advanced configuration options
- [Configuring Endpoints](#custom-endpoint)
- [Configuring Timeouts](#provider-timeout)
- [Configuring Models](#default-model)
- [Configuring Feature-specific Models](#feature-specific-models)
- [Configuring Alternative Models for Inline Assists](#alternative-assists)
- [Common Panel Settings](#common-panel-settings)
- [General Configuration Example](#general-example)
## Configure Custom API Keys for LLM Providers {#configure-custom-api-keys}
See [Configuring Custom API Keys](./custom-api-keys.md)
## Advanced Configuration {#advanced-configuration}
### Custom Endpoints {#custom-endpoint}
You can use a custom API endpoint for different providers, as long as it's compatible with the providers API structure.
To do so, add the following to your Zed `settings.json`:
```json
{
"language_models": {
"some-provider": {
"api_url": "http://localhost:11434"
}
}
}
```
Where `some-provider` can be any of the following values: `anthropic`, `google`, `ollama`, `openai`.
### Configuring Models {#default-model}
Zed's hosted LLM service sets `claude-3-7-sonnet-latest` as the default model.
However, you can change it either via the model dropdown in the Assistant Panel's bottom-left corner or by manually editing the `default_model` object in your settings:
```json
{
"assistant": {
"version": "2",
"default_model": {
"provider": "zed.dev",
"model": "gpt-4o"
}
}
}
```
#### Feature-specific Models {#feature-specific-models}
> Currently only available in [Preview](https://zed.dev/releases/preview).
Zed allows you to configure different models for specific features.
This provides flexibility to use more powerful models for certain tasks while using faster or more efficient models for others.
If a feature-specific model is not set, it will fall back to using the default model, which is the one you set on the Agent Panel.
You can configure the following feature-specific models:
- Thread summary model: Used for generating thread summaries
- Inline Assist model: Used for the Inline Assist feature
- Commit message model: Used for generating Git commit messages
Example configuration:
```json
{
"assistant": {
"version": "2",
"default_model": {
"provider": "zed.dev",
"model": "claude-3-7-sonnet"
},
"inline_assistant_model": {
"provider": "anthropic",
"model": "claude-3-5-sonnet"
},
"commit_message_model": {
"provider": "openai",
"model": "gpt-4o-mini"
},
"thread_summary_model": {
"provider": "google",
"model": "gemini-2.0-flash"
}
}
}
```
### Configuring Alternative Models for Inline Assists {#alternative-assists}
You can configure additional models that will be used to perform inline assists in parallel. When you do this,
the Inline Assist UI will surface controls to cycle between the alternatives generated by each model. The models
you specify here are always used in _addition_ to your default model. For example, the following configuration
will generate two outputs for every assist. One with Claude 3.5 Sonnet, and one with GPT-4o.
```json
{
"assistant": {
"default_model": {
"provider": "zed.dev",
"model": "claude-3-5-sonnet"
},
"inline_alternatives": [
{
"provider": "zed.dev",
"model": "gpt-4o"
}
],
"version": "2"
}
}
```
## Common Panel Settings {#common-panel-settings}
| key | type | default | description |
| -------------- | ------- | ------- | ------------------------------------------------------------------------------------- |
| enabled | boolean | true | Setting this to `false` will completely disable the assistant |
| button | boolean | true | Show the assistant icon in the status bar |
| dock | string | "right" | The default dock position for the assistant panel. Can be ["left", "right", "bottom"] |
| default_height | string | null | The pixel height of the assistant panel when docked to the bottom |
| default_width | string | null | The pixel width of the assistant panel when docked to the left or right |
## General Configuration Example {#general-example}
```json
{
"assistant": {
"enabled": true,
"default_model": {
"provider": "zed.dev",
"model": "claude-3-7-sonnet"
},
"editor_model": {
"provider": "openai",
"model": "gpt-4o"
},
"inline_assistant_model": {
"provider": "anthropic",
"model": "claude-3-5-sonnet"
},
"commit_message_model": {
"provider": "openai",
"model": "gpt-4o-mini"
},
"thread_summary_model": {
"provider": "google",
"model": "gemini-1.5-flash"
},
"version": "2",
"button": true,
"default_width": 480,
"dock": "right"
}
}
```

View File

@@ -2,7 +2,7 @@
Contexts are like conversations in most assistant-like tools. A context is a collaborative tool for sharing information between you, your project, and the assistant/model.
The model can reference content from your active context in the assistant panel, but also elsewhere like the inline assistant.
The model can reference content from your active context in the assistant panel, but also elsewhere like Inline Assist.
### Saving and Loading Contexts

View File

@@ -1,24 +1,16 @@
# Configuring the Assistant
# Configuring Custom API Keys
Here's a bird's-eye view of all the configuration options available in Zed's Assistant:
While Zed offers hosted versions of models through our various plans, we're always happy to support users wanting to supply their own API keys for LLM providers.
- Configure LLM Providers
- [Zed AI (Configured by default when signed in)](#zed-ai)
- Supported LLM Providers
- [Anthropic](#anthropic)
- [GitHub Copilot Chat](#github-copilot-chat)
- [Google AI](#google-ai)
- [Ollama](#ollama)
- [OpenAI](#openai)
- [DeepSeek](#deepseek)
- [OpenAI API Compatible](#openai-api-compatible)
- [LM Studio](#lmstudio)
- Advanced configuration options
- [Configuring Endpoints](#custom-endpoint)
- [Configuring Timeouts](#provider-timeout)
- [Configuring Models](#default-model)
- [Configuring Feature-specific Models](#feature-specific-models)
- [Configuring Alternative Models for Inline Assists](#alternative-assists)
- [Common Panel Settings](#common-panel-settings)
- [General Configuration Example](#general-example)
## Providers {#providers}
@@ -26,13 +18,9 @@ To access the Assistant configuration view, run `assistant: show configuration`
Below you can find all the supported providers available so far.
### Zed AI {#zed-ai}
A hosted service providing convenient and performant support for AI-enabled coding in Zed, powered by Anthropic's Claude 3.5 Sonnet and accessible just by signing in.
### Anthropic {#anthropic}
You can use Claude 3.5 Sonnet via [Zed AI](#zed-ai) for free. To use other Anthropic models you will need to configure it by providing your own API key.
You can use Anthropic models with the Zed assistant by choosing it via the model dropdown in the assistant panel.
1. Sign up for Anthropic and [create an API key](https://console.anthropic.com/settings/keys)
2. Make sure that your Anthropic account has credits
@@ -251,7 +239,7 @@ The Zed Assistant comes pre-configured to use the latest version for common mode
Custom models will be listed in the model dropdown in the assistant panel. You can also modify the `api_url` to use a custom endpoint if needed.
### OpenAI API Compatible
### OpenAI API Compatible{#openai-api-compatible}
Zed supports using OpenAI compatible APIs by specifying a custom `endpoint` and `available_models` for the OpenAI provider.
@@ -293,150 +281,3 @@ Example configuration for using X.ai Grok with Zed:
```
Tip: Set [LM Studio as a login item](https://lmstudio.ai/docs/advanced/headless#run-the-llm-service-on-machine-login) to automate running the LM Studio server.
## Advanced Configuration {#advanced-configuration}
### Custom Endpoints {#custom-endpoint}
You can use a custom API endpoint for different providers, as long as it's compatible with the providers API structure.
To do so, add the following to your Zed `settings.json`:
```json
{
"language_models": {
"some-provider": {
"api_url": "http://localhost:11434"
}
}
}
```
Where `some-provider` can be any of the following values: `anthropic`, `google`, `ollama`, `openai`.
### Configuring Models {#default-model}
Zed's hosted LLM service sets `claude-3-7-sonnet-latest` as the default model.
However, you can change it either via the model dropdown in the Assistant Panel's bottom-left corner or by manually editing the `default_model` object in your settings:
```json
{
"assistant": {
"version": "2",
"default_model": {
"provider": "zed.dev",
"model": "gpt-4o"
}
}
}
```
#### Feature-specific Models {#feature-specific-models}
> Currently only available in [Preview](https://zed.dev/releases/preview).
Zed allows you to configure different models for specific features.
This provides flexibility to use more powerful models for certain tasks while using faster or more efficient models for others.
If a feature-specific model is not set, it will fall back to using the default model, which is the one you set on the Agent Panel.
You can configure the following feature-specific models:
- Thread summary model: Used for generating thread summaries
- Inline assistant model: Used for the inline assistant feature
- Commit message model: Used for generating Git commit messages
Example configuration:
```json
{
"assistant": {
"version": "2",
"default_model": {
"provider": "zed.dev",
"model": "claude-3-7-sonnet"
},
"inline_assistant_model": {
"provider": "anthropic",
"model": "claude-3-5-sonnet"
},
"commit_message_model": {
"provider": "openai",
"model": "gpt-4o-mini"
},
"thread_summary_model": {
"provider": "google",
"model": "gemini-2.0-flash"
}
}
}
```
### Configuring Alternative Models for Inline Assists {#alternative-assists}
You can configure additional models that will be used to perform inline assists in parallel. When you do this,
the inline assist UI will surface controls to cycle between the alternatives generated by each model. The models
you specify here are always used in _addition_ to your default model. For example, the following configuration
will generate two outputs for every assist. One with Claude 3.5 Sonnet, and one with GPT-4o.
```json
{
"assistant": {
"default_model": {
"provider": "zed.dev",
"model": "claude-3-5-sonnet"
},
"inline_alternatives": [
{
"provider": "zed.dev",
"model": "gpt-4o"
}
],
"version": "2"
}
}
```
## Common Panel Settings {#common-panel-settings}
| key | type | default | description |
| -------------- | ------- | ------- | ------------------------------------------------------------------------------------- |
| enabled | boolean | true | Setting this to `false` will completely disable the assistant |
| button | boolean | true | Show the assistant icon in the status bar |
| dock | string | "right" | The default dock position for the assistant panel. Can be ["left", "right", "bottom"] |
| default_height | string | null | The pixel height of the assistant panel when docked to the bottom |
| default_width | string | null | The pixel width of the assistant panel when docked to the left or right |
## General Configuration Example {#general-example}
```json
{
"assistant": {
"enabled": true,
"default_model": {
"provider": "zed.dev",
"model": "claude-3-7-sonnet"
},
"editor_model": {
"provider": "openai",
"model": "gpt-4o"
},
"inline_assistant_model": {
"provider": "anthropic",
"model": "claude-3-5-sonnet"
},
"commit_message_model": {
"provider": "openai",
"model": "gpt-4o-mini"
},
"thread_summary_model": {
"provider": "google",
"model": "gemini-1.5-flash"
},
"version": "2",
"button": true,
"default_width": 480,
"dock": "right"
}
}
```

View File

@@ -0,0 +1 @@
# Enhancing the Agent

View File

@@ -0,0 +1,44 @@
# Inline Assist
## Using Inline Assist
You can use `ctrl-enter` to open Inline Assist nearly anywhere you can enter text: Editors, the assistant panel, the prompt library, channel notes, and even within the terminal panel.
Inline Assist allows you to send the current selection (or the current line) to a model and modify the selection with the model's response.
You can also perform multiple generation requests in parallel by pressing `ctrl-enter` with multiple cursors, or by pressing `ctrl-enter` with a selection that spans multiple excerpts in a multibuffer.
Inline Assist pulls its context from the assistant panel, allowing you to provide additional instructions or rules for code transformations.
> **Note**: Inline Assist sees the entire active context from the assistant panel. This means the assistant panel's context editor becomes one of the most powerful tools for shaping the results of Inline Assist.
## Using Prompts & Commands
While you can't directly use slash commands (and by extension, the `/prompt` command to include prompts) with Inline Assist, you can use them in the active context in the assistant panel.
A common workflow when using Inline Assist is to create a context in the assistant panel, add the desired context through text, prompts and commands, and then use Inline Assist to generate and apply transformations.
### Example Recipe - Fixing Errors with Inline Assist
1. Create a new chat in the assistant panel.
2. Use the `/diagnostic` command to add current diagnostics to the context.
3. OR use the `/terminal` command to add the current terminal output to the context (maybe a panic, error, or log?)
4. Use Inline Assist to generate a fix for the error.
## Prefilling Prompts
To create a custom keybinding that prefills a prompt, you can add the following format in your keymap:
```json
[
{
"context": "Editor && mode == full",
"bindings": {
"ctrl-shift-enter": [
"assistant::InlineAssist",
{ "prompt": "Build a snake game" }
]
}
}
]
```

32
docs/src/agent/models.md Normal file
View File

@@ -0,0 +1,32 @@
# Models
Zeds plans offer hosted versions of major LLMs, generally with higher rate limits than individual API keys. Were working hard to expand the models supported by Zeds subscription offerings, so please check back often.
| Model | Provider | Max Mode | Context Window | Price per Prompt | Price per Request |
| --- | --- | --- | --- | --- | --- |
| Claude 3.5 Sonnet | Anthropic | ❌ | 120k | $0.04 | N/A |
| Claude 3.7 Sonnet | Anthropic | ❌ | 120k | $0.04 | N/A |
| Claude 3.7 Sonnet | Anthropic | ✅ | 200k | N/A | $0.05 |
## Usage {#usage}
The models above can be used with the prompts included in your plan. For models not marked with [“Max Mode”](#max-mode), each prompt is counted against the monthly limit of your plan. If youve exceeded your limit for the month, and are on a paid plan, you can enable usage-based pricing to continue using models for the rest of the month. See [Plans and Usage](./plans-and-usage.md) for more information.
## Max Mode {#max-mode}
In Max Mode, we enable models to use [large context windows](#context-windows), unlimited tool calls, and other capabilities for expanded reasoning, to allow an unfettered agentic experience. Because of the increased cost to Zed, each subsequent request beyond the initial user prompt in [Max Mode](#max-mode) models is counted as a prompt for metering. In addition, usage-based pricing per request is slightly more expensive for [Max Mode](#max-mode) models than usage-based pricing per prompt for regular models.
## Context Windows {#context-windows}
A context window is the maximum span of text and code an LLM can consider at once, including both the input prompt and output generated by the model.
In [Max Mode](#max-mode), we increase context window size to allow models enhanced reasoning capabilities.
Each Agent thread in Zed maintains its own context window. The more prompts, attached files, and responses included in a session, the larger the context window grows.
For best results, its recommended you take a purpose-based approach to Agent thread management, starting a new thread for each unique task.
## Tool Calls {#tool-calls}
Models can use tools to interface with your code. In [Max Mode](#max-mode), models can use an unlimited number of tools per prompt, with each tool call counting as a prompt for metering purposes. For non-Max Mode models, youll need to interact with the model every 25 tool calls to continue, at which point a new prompt will be counted against your plan limit.
*We need a list of tools here for when we launch, with a summary of what they do? Maybe in a new page*

View File

@@ -0,0 +1,32 @@
# Plans and Usage
To view your current usage, you can visit your account at zed.dev/account. Youll also see some usage meters in-product when youre nearing the limit for your plan or trial.
## Available Plans {#plans}
Personal (details below)
Trial (details below)
Pro (details below)
Business (details below)
For costs and more information on pricing, visit Zeds pricing page. Please note that if youre interested in just using Zed as the worlds fastest editor, with no AI or subscription features, you can always do so for free, without [authentication](link to Josephs auth page).
## Usage {#usage}
A `prompt` in Zed is an input from the user, initiated on pressing enter, composed of one or many `requests`. A `prompt` can be initiated from the Agent panel, or via Inline Assist.
A `request` in Zed is a response to a `prompt`, plus any tool calls that are initiated as part of that response. There may be one `request` per `prompt`, or many.
Most models offered by Zed are metered per-prompt. Some models that use large context windows and unlimited tool calls ([“Max Mode”](./models.md#max-mode)) count each individual request within a prompt against your prompt limit, since the agentic work spawned by the prompt is expensive to support. See [Models](./models.md) for a list of which subset of models are metered by request.
Plans come with a set amount of prompts included, with the number varying depending on the plan youve selected.
## Usage-Based Pricing {#ubp}
You may opt in to usage-based pricing for prompts that exceed what is included in your paid plan from [your account page](/account).
Usage-based pricing is only available with a paid plan, and is exclusively opt-in. From the dashboard, you can toggle usage-based pricing for usage exceeding your paid plan. You can also configure a spend limit in USD. Once the spend limit is hit, well stop any further usage until your prompt limit resets.
We will bill for additional prompts when youve made prompts totaling $20, or when your billing date occurs, whichever comes first.
Cost per request for each model can be found on the [models](./models.md) page.
## Business Usage {#business-usage}
Email sales@zed.dev with any questions on business plans, metering, and usage-based pricing.

View File

@@ -0,0 +1,3 @@
# Privacy and Security
*To be completed*

View File

@@ -0,0 +1 @@
# Product

View File

@@ -19,7 +19,7 @@ Here are some tips for using prompts effectively:
The Prompt Library is an interface for writing and managing prompts. Like other text-driven UIs in Zed, it is a full editor with syntax highlighting, keyboard shortcuts, etc.
You can use the inline assistant right in the prompt editor, allowing you to automate and rewrite prompts.
You can use Inline Assist right in the prompt editor, allowing you to automate and rewrite prompts.
### Opening the Prompt Library
@@ -131,7 +131,7 @@ By using nested prompts, you can create modular and reusable prompt components t
### Prompt Templates
Zed uses prompt templates to power internal assistant features, like the terminal assistant, or the content prompt used in the inline assistant.
Zed uses prompt templates to power internal assistant features, like the terminal assistant, or the content prompt used in Inline Assist.
Zed has the following internal prompt templates:

View File

@@ -0,0 +1 @@
# Subscription

View File

@@ -1,44 +0,0 @@
# Inline Assistant
## Using the Inline Assistant
You can use `ctrl-enter` to open the inline assistant nearly anywhere you can enter text: Editors, the assistant panel, the prompt library, channel notes, and even within the terminal panel.
The inline assistant allows you to send the current selection (or the current line) to a language model and modify the selection with the language model's response.
You can also perform multiple generation requests in parallel by pressing `ctrl-enter` with multiple cursors, or by pressing `ctrl-enter` with a selection that spans multiple excerpts in a multibuffer.
The inline assistant pulls its context from the assistant panel, allowing you to provide additional instructions or rules for code transformations.
> **Note**: The inline assistant sees the entire active context from the assistant panel. This means the assistant panel's context editor becomes one of the most powerful tools for shaping the results of the inline assistant.
## Using Prompts & Commands
While you can't directly use slash commands (and by extension, the `/prompt` command to include prompts) in the inline assistant, you can use them in the active context in the assistant panel.
A common workflow when using the inline assistant is to create a context in the assistant panel, add the desired context through text, prompts and commands, and then use the inline assistant to generate and apply transformations.
### Example Recipe - Fixing Errors with the Inline Assistant
1. Create a new chat in the assistant panel.
2. Use the `/diagnostic` command to add current diagnostics to the context.
3. OR use the `/terminal` command to add the current terminal output to the context (maybe a panic, error, or log?)
4. Use the inline assistant to generate a fix for the error.
## Prefilling Prompts
To create a custom keybinding that prefills a prompt, you can add the following format in your keymap:
```json
[
{
"context": "Editor && mode == full",
"bindings": {
"ctrl-shift-enter": [
"assistant::InlineAssist",
{ "prompt": "Build a snake game" }
]
}
}
]
```

View File

@@ -298,4 +298,4 @@ You should be able to sign-in to Supermaven by clicking on the Supermaven icon i
## See also
You may also use the Assistant Panel or the Inline Assistant to interact with language models, see [the assistant documentation](assistant/assistant.md) for more information.
You may also use the Assistant Panel or Inline Assist to interact with language models, see [the assistant documentation](assistant/assistant.md) for more information.

View File

@@ -146,7 +146,7 @@ These commands open new panes or jump to specific panes.
### In insert mode
The following commands help you bring up Zed's completion menu, request a suggestion from GitHub Copilot, or open the inline AI assistant without leaving insert mode.
The following commands help you bring up Zed's completion menu, request a suggestion from GitHub Copilot, or open [Inline Assist](./agent/inline-assist.md) without leaving insert mode.
| Command | Default Shortcut |
| ---------------------------------------------------------------------------- | ---------------- |