Compare commits

..

3 Commits

Author SHA1 Message Date
Richard Feldman
44501581ee Start on streaming JSON 2025-03-07 10:14:16 -05:00
Richard Feldman
ae95142cc8 Got basic chunk streaming working 2025-03-07 00:23:37 -05:00
Richard Feldman
b1b8d596b9 Use full_moon for lexing 2025-03-06 21:53:18 -05:00
226 changed files with 4945 additions and 6996 deletions

View File

@@ -26,6 +26,3 @@ rustflags = [
"-C",
"target-feature=+crt-static", # This fixes the linking issue when compiling livekit on Windows
]
[env]
MACOSX_DEPLOYMENT_TARGET = "10.15.7"

1540
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -171,6 +171,7 @@ members = [
"extensions/emmet",
"extensions/glsl",
"extensions/haskell",
"extensions/html",
"extensions/perplexity",
"extensions/proto",
@@ -370,7 +371,7 @@ zeta = { path = "crates/zeta" }
#
aho-corasick = "1.1"
alacritty_terminal = { git = "https://github.com/zed-industries/alacritty.git", branch = "add-hush-login-flag" }
alacritty_terminal = { git = "https://github.com/zed-industries/alacritty.git", rev = "03c2907b44b4189aac5fdeaea331f5aab5c7072e" }
any_vec = "0.14"
anyhow = "1.0.86"
arrayvec = { version = "0.7.4", features = ["serde"] }
@@ -451,7 +452,7 @@ livekit = { git = "https://github.com/zed-industries/livekit-rust-sdks", rev = "
], default-features = false }
log = { version = "0.4.16", features = ["kv_unstable_serde", "serde"] }
markup5ever_rcdom = "0.3.0"
mlua = { version = "0.10", features = ["lua54", "vendored", "async", "send"] }
mlua = { version = "0.10", features = ["lua54", "vendored"] }
nanoid = "0.4"
nbformat = { version = "0.10.0" }
nix = "0.29"
@@ -597,12 +598,12 @@ features = [
]
[workspace.dependencies.windows]
version = "0.60"
version = "0.58"
features = [
"implement",
"Foundation_Collections",
"Foundation_Numerics",
"Storage_Search",
"Storage_Streams",
"Storage",
"System_Threading",
"UI_StartScreen",
"UI_ViewManagement",
@@ -623,11 +624,9 @@ features = [
"Win32_System_Com_StructuredStorage",
"Win32_System_Console",
"Win32_System_DataExchange",
"Win32_System_IO",
"Win32_System_LibraryLoader",
"Win32_System_Memory",
"Win32_System_Ole",
"Win32_System_Pipes",
"Win32_System_SystemInformation",
"Win32_System_SystemServices",
"Win32_System_Threading",
@@ -753,9 +752,5 @@ new_ret_no_self = { level = "allow" }
should_implement_trait = { level = "allow" }
let_underscore_future = "allow"
# in Rust it can be very tedious to reduce argument count without
# running afoul of the borrow checker.
too_many_arguments = "allow"
[workspace.metadata.cargo-machete]
ignored = ["bindgen", "cbindgen", "prost_build", "serde", "component", "linkme"]

View File

@@ -1,3 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.36197 1.67985C5.3748 1.41534 4.36011 2.00117 4.0956 2.98834L2.17985 10.138C1.91534 11.1252 2.50117 12.1399 3.48833 12.4044L10.638 14.3202C11.6252 14.5847 12.6399 13.9988 12.9044 13.0117L14.8202 5.86197C15.0847 4.8748 14.4988 3.86012 13.5117 3.59561L6.36197 1.67985ZM10.0457 4.58266C9.77896 4.51119 9.50479 4.66948 9.43332 4.93621L8.76235 7.44028C8.69088 7.70701 8.84917 7.98118 9.11591 8.05265L11.62 8.72362C11.8867 8.79509 12.1609 8.6368 12.2324 8.37006L12.9033 5.86599C12.9748 5.59926 12.8165 5.32509 12.5498 5.25362L10.0457 4.58266Z" fill="black"/>
</svg>

Before

Width:  |  Height:  |  Size: 707 B

View File

@@ -1,7 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.5 13L1.5 5H11.5L6.5 13Z" fill="black"/>
<path d="M14 9H9L11.5 5L14 9Z" fill="black" fill-opacity="0.75"/>
<path d="M9 9L14 9L11.5 13L9 9Z" fill="black" fill-opacity="0.65"/>
<path d="M14 5L15.25 7L12.75 7L14 5Z" fill="black" fill-opacity="0.5"/>
<path d="M14 9L12.75 7H15.25L14 9Z" fill="black" fill-opacity="0.55"/>
</svg>

Before

Width:  |  Height:  |  Size: 432 B

View File

@@ -475,7 +475,9 @@
"ctrl-alt-delete": "editor::DeleteToNextSubwordEnd",
"ctrl-alt-d": "editor::DeleteToNextSubwordEnd",
"ctrl-alt-left": "editor::MoveToPreviousSubwordStart",
// "ctrl-alt-b": "editor::MoveToPreviousSubwordStart",
"ctrl-alt-right": "editor::MoveToNextSubwordEnd",
"ctrl-alt-f": "editor::MoveToNextSubwordEnd",
"ctrl-alt-shift-left": "editor::SelectToPreviousSubwordStart",
"ctrl-alt-shift-b": "editor::SelectToPreviousSubwordStart",
"ctrl-alt-shift-right": "editor::SelectToNextSubwordEnd",
@@ -731,48 +733,28 @@
"up": "menu::SelectPrevious",
"down": "menu::SelectNext",
"enter": "menu::Confirm",
"alt-y": "git::StageFile",
"alt-shift-y": "git::UnstageFile",
"ctrl-alt-y": "git::ToggleStaged",
"space": "git::ToggleStaged",
"ctrl-space": "git::StageAll",
"ctrl-shift-space": "git::UnstageAll",
"tab": "git_panel::FocusEditor",
"shift-tab": "git_panel::FocusEditor",
"escape": "git_panel::ToggleFocus",
"ctrl-enter": "git::Commit",
"alt-enter": "menu::SecondaryConfirm",
"backspace": "git::RestoreFile"
"alt-enter": "menu::SecondaryConfirm"
}
},
{
"context": "GitCommit > Editor",
"bindings": {
"escape": "menu::Cancel",
"enter": "editor::Newline",
"ctrl-enter": "git::Commit",
"alt-l": "git::GenerateCommitMessage"
}
},
{
"context": "GitPanel",
"use_key_equivalents": true,
"bindings": {
"ctrl-g ctrl-g": "git::Fetch",
"ctrl-g up": "git::Push",
"ctrl-g down": "git::Pull",
"ctrl-g shift-up": "git::ForcePush",
"ctrl-g d": "git::Diff",
"ctrl-g backspace": "git::RestoreTrackedFiles",
"ctrl-g shift-backspace": "git::TrashUntrackedFiles",
"ctrl-space": "git::StageAll",
"ctrl-shift-space": "git::UnstageAll"
}
},
{
"context": "GitDiff > Editor",
"bindings": {
"ctrl-enter": "git::Commit",
"ctrl-space": "git::StageAll",
"ctrl-shift-space": "git::UnstageAll"
"ctrl-enter": "git::Commit"
}
},
{
@@ -787,7 +769,6 @@
"escape": "git_panel::FocusChanges",
"tab": "git_panel::FocusChanges",
"shift-tab": "git_panel::FocusChanges",
"enter": "editor::Newline",
"ctrl-enter": "git::Commit",
"alt-up": "git_panel::FocusChanges",
"alt-l": "git::GenerateCommitMessage"

View File

@@ -31,13 +31,13 @@
"enter": "menu::Confirm",
"ctrl-enter": "menu::SecondaryConfirm",
"cmd-enter": "menu::SecondaryConfirm",
"cmd-escape": "menu::Cancel",
"ctrl-escape": "menu::Cancel",
"ctrl-c": "menu::Cancel",
"escape": "menu::Cancel",
"alt-shift-enter": "menu::Restart",
"cmd-shift-w": "workspace::CloseWindow",
"shift-escape": "workspace::ToggleZoom",
"cmd-escape": "menu::Cancel",
"cmd-o": "workspace::Open",
"cmd-=": ["zed::IncreaseBufferFontSize", { "persist": false }],
"cmd-+": ["zed::IncreaseBufferFontSize", { "persist": false }],
@@ -108,8 +108,8 @@
"cmd-right": ["editor::MoveToEndOfLine", { "stop_at_soft_wraps": true }],
"ctrl-e": ["editor::MoveToEndOfLine", { "stop_at_soft_wraps": false }],
"end": ["editor::MoveToEndOfLine", { "stop_at_soft_wraps": true }],
"cmd-up": "editor::MoveToBeginning",
"cmd-down": "editor::MoveToEnd",
"cmd-up": "editor::MoveToStartOfExcerpt",
"cmd-down": "editor::MoveToEndOfExcerpt",
"cmd-home": "editor::MoveToBeginning", // Typed via `cmd-fn-left`
"cmd-end": "editor::MoveToEnd", // Typed via `cmd-fn-right`
"shift-up": "editor::SelectUp",
@@ -124,8 +124,8 @@
"alt-shift-right": "editor::SelectToNextWordEnd", // cursorWordRightSelect
"ctrl-shift-up": "editor::SelectToStartOfParagraph",
"ctrl-shift-down": "editor::SelectToEndOfParagraph",
"cmd-shift-up": "editor::SelectToBeginning",
"cmd-shift-down": "editor::SelectToEnd",
"cmd-shift-up": "editor::SelectToStartOfExcerpt",
"cmd-shift-down": "editor::SelectToEndOfExcerpt",
"cmd-a": "editor::SelectAll",
"cmd-l": "editor::SelectLine",
"cmd-shift-i": "editor::Format",
@@ -172,16 +172,6 @@
"alt-enter": "editor::OpenSelectionsInMultibuffer"
}
},
{
"context": "Editor && multibuffer",
"use_key_equivalents": true,
"bindings": {
"cmd-up": "editor::MoveToStartOfExcerpt",
"cmd-down": "editor::MoveToStartOfNextExcerpt",
"cmd-shift-up": "editor::SelectToStartOfExcerpt",
"cmd-shift-down": "editor::SelectToStartOfNextExcerpt"
}
},
{
"context": "Editor && mode == full && edit_prediction",
"use_key_equivalents": true,
@@ -763,25 +753,28 @@
"cmd-up": "menu::SelectFirst",
"cmd-down": "menu::SelectLast",
"enter": "menu::Confirm",
"cmd-alt-y": "git::ToggleStaged",
"space": "git::ToggleStaged",
"cmd-y": "git::StageFile",
"cmd-shift-y": "git::UnstageFile",
"cmd-shift-space": "git::StageAll",
"ctrl-shift-space": "git::UnstageAll",
"alt-down": "git_panel::FocusEditor",
"tab": "git_panel::FocusEditor",
"shift-tab": "git_panel::FocusEditor",
"escape": "git_panel::ToggleFocus",
"cmd-enter": "git::Commit",
"backspace": "git::RestoreFile"
"cmd-enter": "git::Commit"
}
},
{
"context": "GitDiff > Editor",
"use_key_equivalents": true,
"bindings": {
"cmd-enter": "git::Commit",
"cmd-ctrl-y": "git::StageAll",
"cmd-ctrl-shift-y": "git::UnstageAll"
"cmd-enter": "git::Commit"
}
},
{
"context": "AskPass > Editor",
"use_key_equivalents": true,
"bindings": {
"enter": "menu::Confirm"
}
},
{
@@ -797,27 +790,11 @@
"alt-tab": "git::GenerateCommitMessage"
}
},
{
"context": "GitPanel",
"use_key_equivalents": true,
"bindings": {
"ctrl-g ctrl-g": "git::Fetch",
"ctrl-g up": "git::Push",
"ctrl-g down": "git::Pull",
"ctrl-g shift-up": "git::ForcePush",
"ctrl-g d": "git::Diff",
"ctrl-g backspace": "git::RestoreTrackedFiles",
"ctrl-g shift-backspace": "git::TrashUntrackedFiles",
"cmd-ctrl-y": "git::StageAll",
"cmd-ctrl-shift-y": "git::UnstageAll"
}
},
{
"context": "GitCommit > Editor",
"use_key_equivalents": true,
"bindings": {
"enter": "editor::Newline",
"escape": "menu::Cancel",
"cmd-enter": "git::Commit",
"alt-tab": "git::GenerateCommitMessage"
}

View File

@@ -845,7 +845,7 @@
// "hunk_style": "transparent"
// 2. Show unstaged hunks with a pattern background:
// "hunk_style": "pattern"
"hunk_style": "staged_border"
"hunk_style": "transparent"
},
// Configuration for how direnv configuration should be loaded. May take 2 values:
// 1. Load direnv configuration using `direnv export json` directly.
@@ -1055,6 +1055,7 @@
// }
//
"file_types": {
"Plain Text": ["txt"],
"JSONC": ["**/.zed/**/*.json", "**/zed/**/*.json", "**/Zed/**/*.json", "**/.vscode/**/*.json"],
"Shell Script": [".env.*"]
},

View File

@@ -383,11 +383,6 @@
"font_style": null,
"font_weight": null
},
"variable.special": {
"color": "#83a598ff",
"font_style": null,
"font_weight": null
},
"variant": {
"color": "#83a598ff",
"font_style": null,
@@ -776,11 +771,6 @@
"font_style": null,
"font_weight": null
},
"variable.special": {
"color": "#83a598ff",
"font_style": null,
"font_weight": null
},
"variant": {
"color": "#83a598ff",
"font_style": null,
@@ -1169,11 +1159,6 @@
"font_style": null,
"font_weight": null
},
"variable.special": {
"color": "#83a598ff",
"font_style": null,
"font_weight": null
},
"variant": {
"color": "#83a598ff",
"font_style": null,
@@ -1562,11 +1547,6 @@
"font_style": null,
"font_weight": null
},
"variable.special": {
"color": "#066578ff",
"font_style": null,
"font_weight": null
},
"variant": {
"color": "#0b6678ff",
"font_style": null,
@@ -1955,11 +1935,6 @@
"font_style": null,
"font_weight": null
},
"variable.special": {
"color": "#066578ff",
"font_style": null,
"font_weight": null
},
"variant": {
"color": "#0b6678ff",
"font_style": null,
@@ -2348,11 +2323,6 @@
"font_style": null,
"font_weight": null
},
"variable.special": {
"color": "#066578ff",
"font_style": null,
"font_weight": null
},
"variant": {
"color": "#0b6678ff",
"font_style": null,

View File

@@ -9,10 +9,7 @@ use gpui::{
};
use language::{LanguageRegistry, LanguageServerBinaryStatus, LanguageServerId};
use lsp::LanguageServerName;
use project::{
EnvironmentErrorMessage, LanguageServerProgress, LspStoreEvent, Project,
ProjectEnvironmentEvent, WorktreeId,
};
use project::{EnvironmentErrorMessage, LanguageServerProgress, Project, WorktreeId};
use smallvec::SmallVec;
use std::{cmp::Reverse, fmt::Write, sync::Arc, time::Duration};
use ui::{prelude::*, ButtonLike, ContextMenu, PopoverMenu, PopoverMenuHandle, Tooltip};
@@ -76,22 +73,7 @@ impl ActivityIndicator {
})
.detach();
cx.subscribe(
&project.read(cx).lsp_store(),
|_, _, event, cx| match event {
LspStoreEvent::LanguageServerUpdate { .. } => cx.notify(),
_ => {}
},
)
.detach();
cx.subscribe(
&project.read(cx).environment().clone(),
|_, _, event, cx| match event {
ProjectEnvironmentEvent::ErrorsUpdated => cx.notify(),
},
)
.detach();
cx.observe(&project, |_, _, cx| cx.notify()).detach();
if let Some(auto_updater) = auto_updater.as_ref() {
cx.observe(auto_updater, |_, _, cx| cx.notify()).detach();
@@ -222,7 +204,7 @@ impl ActivityIndicator {
message: error.0.clone(),
on_click: Some(Arc::new(move |this, window, cx| {
this.project.update(cx, |project, cx| {
project.remove_environment_error(worktree_id, cx);
project.remove_environment_error(cx, worktree_id);
});
window.dispatch_action(Box::new(workspace::OpenLog), cx);
})),

View File

@@ -38,7 +38,7 @@ use language_model::{
use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu};
use multi_buffer::MultiBufferRow;
use parking_lot::Mutex;
use project::{CodeAction, LspAction, ProjectTransaction};
use project::{ActionVariant, CodeAction, ProjectTransaction};
use prompt_store::PromptBuilder;
use rope::Rope;
use settings::{update_settings_file, Settings, SettingsStore};
@@ -386,6 +386,7 @@ impl InlineAssistant {
}
}
#[allow(clippy::too_many_arguments)]
pub fn suggest_assist(
&mut self,
editor: &Entity<Editor>,
@@ -1673,6 +1674,7 @@ impl Focusable for PromptEditor {
impl PromptEditor {
const MAX_LINES: u8 = 8;
#[allow(clippy::too_many_arguments)]
fn new(
id: InlineAssistId,
gutter_dimensions: Arc<Mutex<GutterDimensions>>,
@@ -2331,6 +2333,7 @@ struct InlineAssist {
}
impl InlineAssist {
#[allow(clippy::too_many_arguments)]
fn new(
assist_id: InlineAssistId,
group_id: InlineAssistGroupId,
@@ -3566,7 +3569,7 @@ impl CodeActionProvider for AssistantCodeActionProvider {
Task::ready(Ok(vec![CodeAction {
server_id: language::LanguageServerId(0),
range: snapshot.anchor_before(range.start)..snapshot.anchor_after(range.end),
lsp_action: LspAction::Action(Box::new(lsp::CodeAction {
lsp_action: ActionVariant::Action(Box::new(lsp::CodeAction {
title: "Fix with Assistant".into(),
..Default::default()
})),

View File

@@ -702,6 +702,7 @@ impl Focusable for PromptEditor {
impl PromptEditor {
const MAX_LINES: u8 = 8;
#[allow(clippy::too_many_arguments)]
fn new(
id: TerminalInlineAssistId,
prompt_history: VecDeque<String>,

View File

@@ -59,7 +59,6 @@ prompt_library.workspace = true
prompt_store.workspace = true
proto.workspace = true
rope.workspace = true
scripting_tool.workspace = true
serde.workspace = true
serde_json.workspace = true
settings.workspace = true
@@ -82,8 +81,8 @@ zed_actions.workspace = true
[dev-dependencies]
editor = { workspace = true, features = ["test-support"] }
gpui = { workspace = true, "features" = ["test-support"] }
indoc.workspace = true
language = { workspace = true, "features" = ["test-support"] }
language_model = { workspace = true, "features" = ["test-support"] }
project = { workspace = true, features = ["test-support"] }
rand.workspace = true
indoc.workspace = true

View File

@@ -1,20 +1,21 @@
use std::sync::Arc;
use assistant_tool::ToolWorkingSet;
use collections::HashMap;
use editor::{Editor, MultiBuffer};
use gpui::{
list, AbsoluteLength, AnyElement, App, ClickEvent, DefiniteLength, EdgesRefinement, Empty,
Entity, Focusable, FontWeight, Length, ListAlignment, ListOffset, ListState, StyleRefinement,
Subscription, Task, TextStyleRefinement, UnderlineStyle,
Entity, Focusable, Length, ListAlignment, ListOffset, ListState, StyleRefinement, Subscription,
Task, TextStyleRefinement, UnderlineStyle, WeakEntity,
};
use language::{Buffer, LanguageRegistry};
use language_model::{LanguageModelRegistry, LanguageModelToolUseId, Role};
use markdown::{Markdown, MarkdownStyle};
use scripting_tool::{ScriptingTool, ScriptingToolInput};
use settings::Settings as _;
use theme::ThemeSettings;
use ui::{prelude::*, Disclosure, KeyBinding};
use util::ResultExt as _;
use workspace::Workspace;
use crate::thread::{MessageId, RequestKind, Thread, ThreadError, ThreadEvent};
use crate::thread_store::ThreadStore;
@@ -22,15 +23,15 @@ use crate::tool_use::{ToolUse, ToolUseStatus};
use crate::ui::ContextPill;
pub struct ActiveThread {
workspace: WeakEntity<Workspace>,
language_registry: Arc<LanguageRegistry>,
tools: Arc<ToolWorkingSet>,
thread_store: Entity<ThreadStore>,
thread: Entity<Thread>,
save_thread_task: Option<Task<()>>,
messages: Vec<MessageId>,
list_state: ListState,
rendered_messages_by_id: HashMap<MessageId, Entity<Markdown>>,
rendered_scripting_tool_uses:
HashMap<LanguageModelToolUseId, (Entity<Markdown>, Option<String>)>,
editing_message: Option<(MessageId, EditMessageState)>,
expanded_tool_uses: HashMap<LanguageModelToolUseId, bool>,
last_error: Option<ThreadError>,
@@ -45,7 +46,9 @@ impl ActiveThread {
pub fn new(
thread: Entity<Thread>,
thread_store: Entity<ThreadStore>,
workspace: WeakEntity<Workspace>,
language_registry: Arc<LanguageRegistry>,
tools: Arc<ToolWorkingSet>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
@@ -55,13 +58,14 @@ impl ActiveThread {
];
let mut this = Self {
workspace,
language_registry,
tools,
thread_store,
thread: thread.clone(),
save_thread_task: None,
messages: Vec::new(),
rendered_messages_by_id: HashMap::default(),
rendered_scripting_tool_uses: HashMap::default(),
expanded_tool_uses: HashMap::default(),
list_state: ListState::new(0, ListAlignment::Bottom, px(1024.), {
let this = cx.entity().downgrade();
@@ -77,16 +81,6 @@ impl ActiveThread {
for message in thread.read(cx).messages().cloned().collect::<Vec<_>>() {
this.push_message(&message.id, message.text.clone(), window, cx);
for tool_use in thread.read(cx).scripting_tool_uses_for_message(message.id) {
this.render_scripting_tool_use_markdown(
tool_use.id.clone(),
tool_use.name.as_ref(),
tool_use.input.clone(),
window,
cx,
);
}
}
this
@@ -179,8 +173,6 @@ impl ActiveThread {
text_style.refine(&TextStyleRefinement {
font_family: Some(theme_settings.ui_font.family.clone()),
font_fallbacks: theme_settings.ui_font.fallbacks.clone(),
font_features: Some(theme_settings.ui_font.features.clone()),
font_size: Some(ui_font_size.into()),
color: Some(cx.theme().colors().text),
..Default::default()
@@ -205,7 +197,7 @@ impl ActiveThread {
right: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
bottom: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
},
background: Some(colors.terminal_background.into()),
background: Some(colors.editor_background.into()),
border_color: Some(colors.border_variant),
border_widths: EdgesRefinement {
top: Some(AbsoluteLength::Pixels(Pixels(1.))),
@@ -215,8 +207,6 @@ impl ActiveThread {
},
text: Some(TextStyleRefinement {
font_family: Some(theme_settings.buffer_font.family.clone()),
font_fallbacks: theme_settings.buffer_font.fallbacks.clone(),
font_features: Some(theme_settings.buffer_font.features.clone()),
font_size: Some(buffer_font_size.into()),
..Default::default()
}),
@@ -224,8 +214,6 @@ impl ActiveThread {
},
inline_code: TextStyleRefinement {
font_family: Some(theme_settings.buffer_font.family.clone()),
font_fallbacks: theme_settings.buffer_font.fallbacks.clone(),
font_features: Some(theme_settings.buffer_font.features.clone()),
font_size: Some(buffer_font_size.into()),
background_color: Some(colors.editor_foreground.opacity(0.1)),
..Default::default()
@@ -253,39 +241,9 @@ impl ActiveThread {
})
}
/// Renders the input of a scripting tool use to Markdown.
///
/// Does nothing if the tool use does not correspond to the scripting tool.
fn render_scripting_tool_use_markdown(
&mut self,
tool_use_id: LanguageModelToolUseId,
tool_name: &str,
tool_input: serde_json::Value,
window: &mut Window,
cx: &mut Context<Self>,
) {
if tool_name != ScriptingTool::NAME {
return;
}
let tool_input_result = serde_json::from_value::<ScriptingToolInput>(tool_input);
let (lua_script, summary) = if let Ok(input) = tool_input_result {
(input.lua_script, input.summary)
} else {
(String::new(), None)
};
let lua_script =
self.render_markdown(format!("```lua\n{lua_script}\n```").into(), window, cx);
self.rendered_scripting_tool_uses
.insert(tool_use_id, (lua_script, summary));
}
fn handle_thread_event(
&mut self,
_thread: &Entity<Thread>,
_: &Entity<Thread>,
event: &ThreadEvent,
window: &mut Window,
cx: &mut Context<Self>,
@@ -336,28 +294,46 @@ impl ActiveThread {
cx.notify();
}
ThreadEvent::UsePendingTools => {
self.thread.update(cx, |thread, cx| {
thread.use_pending_tools(cx);
});
}
ThreadEvent::ToolFinished {
pending_tool_use, ..
} => {
if let Some(tool_use) = pending_tool_use {
self.render_scripting_tool_use_markdown(
tool_use.id.clone(),
tool_use.name.as_ref(),
tool_use.input.clone(),
window,
cx,
);
}
let pending_tool_uses = self
.thread
.read(cx)
.pending_tool_uses()
.into_iter()
.filter(|tool_use| tool_use.status.is_idle())
.cloned()
.collect::<Vec<_>>();
if self.thread.read(cx).all_tools_finished() {
for tool_use in pending_tool_uses {
if let Some(tool) = self.tools.tool(&tool_use.name, cx) {
let task = tool.run(tool_use.input, self.workspace.clone(), window, cx);
self.thread.update(cx, |thread, cx| {
thread.insert_tool_output(tool_use.id.clone(), task, cx);
});
}
}
}
ThreadEvent::ToolFinished { .. } => {
let all_tools_finished = self
.thread
.read(cx)
.pending_tool_uses()
.into_iter()
.all(|tool_use| tool_use.status.is_error());
if all_tools_finished {
let model_registry = LanguageModelRegistry::read_global(cx);
if let Some(model) = model_registry.active_model() {
self.thread.update(cx, |thread, cx| {
thread.send_tool_results_to_model(model, cx);
// Insert a user message to contain the tool results.
thread.insert_user_message(
// TODO: Sending up a user message without any content results in the model sending back
// responses that also don't have any content. We currently don't handle this case well,
// so for now we provide some text to keep the model on track.
"Here are the tool results.",
Vec::new(),
cx,
);
thread.send_to_model(model, RequestKind::Chat, true, cx);
});
}
}
@@ -501,17 +477,12 @@ impl ActiveThread {
return Empty.into_any();
};
let thread = self.thread.read(cx);
let context = thread.context_for_message(message_id);
let tool_uses = thread.tool_uses_for_message(message_id);
let scripting_tool_uses = thread.scripting_tool_uses_for_message(message_id);
let context = self.thread.read(cx).context_for_message(message_id);
let tool_uses = self.thread.read(cx).tool_uses_for_message(message_id);
let colors = cx.theme().colors();
// Don't render user messages that are just there for returning tool results.
if message.role == Role::User
&& (thread.message_has_tool_results(message_id)
|| thread.message_has_scripting_tool_results(message_id))
{
if message.role == Role::User && self.thread.read(cx).message_has_tool_results(message_id) {
return Empty.into_any();
}
@@ -524,8 +495,6 @@ impl ActiveThread {
.filter(|(id, _)| *id == message_id)
.map(|(_, state)| state.editor.clone());
let colors = cx.theme().colors();
let message_content = v_flex()
.child(
if let Some(edit_message_editor) = edit_message_editor.clone() {
@@ -661,22 +630,16 @@ impl ActiveThread {
.id(("message-container", ix))
.child(message_content)
.map(|parent| {
if tool_uses.is_empty() && scripting_tool_uses.is_empty() {
if tool_uses.is_empty() {
return parent;
}
parent.child(
v_flex()
.children(
tool_uses
.into_iter()
.map(|tool_use| self.render_tool_use(tool_use, cx)),
)
.children(
scripting_tool_uses
.into_iter()
.map(|tool_use| self.render_scripting_tool_use(tool_use, cx)),
),
v_flex().children(
tool_uses
.into_iter()
.map(|tool_use| self.render_tool_use(tool_use, cx)),
),
)
}),
Role::System => div().id(("message-container", ix)).py_1().px_2().child(
@@ -710,13 +673,8 @@ impl ActiveThread {
.pl_1()
.pr_2()
.bg(cx.theme().colors().editor_foreground.opacity(0.02))
.map(|element| {
if is_open {
element.border_b_1().rounded_t(px(6.))
} else {
element.rounded_md()
}
})
.when(is_open, |element| element.border_b_1().rounded_t(px(6.)))
.when(!is_open, |element| element.rounded_md())
.border_color(cx.theme().colors().border)
.child(
h_flex()
@@ -790,139 +748,6 @@ impl ActiveThread {
}),
)
}
fn render_scripting_tool_use(
&self,
tool_use: ToolUse,
cx: &mut Context<Self>,
) -> impl IntoElement {
let is_open = self
.expanded_tool_uses
.get(&tool_use.id)
.copied()
.unwrap_or_default();
div().px_2p5().child(
v_flex()
.gap_1()
.rounded_lg()
.border_1()
.border_color(cx.theme().colors().border)
.child(
h_flex()
.justify_between()
.py_0p5()
.pl_1()
.pr_2()
.bg(cx.theme().colors().editor_foreground.opacity(0.02))
.map(|element| {
if is_open {
element.border_b_1().rounded_t(px(6.))
} else {
element.rounded_md()
}
})
.border_color(cx.theme().colors().border)
.child(
h_flex()
.gap_1()
.child(Disclosure::new("tool-use-disclosure", is_open).on_click(
cx.listener({
let tool_use_id = tool_use.id.clone();
move |this, _event, _window, _cx| {
let is_open = this
.expanded_tool_uses
.entry(tool_use_id.clone())
.or_insert(false);
*is_open = !*is_open;
}
}),
))
.child(Label::new(tool_use.name)),
)
.child(
Label::new(match tool_use.status {
ToolUseStatus::Pending => "Pending",
ToolUseStatus::Running => "Running",
ToolUseStatus::Finished(_) => "Finished",
ToolUseStatus::Error(_) => "Error",
})
.size(LabelSize::XSmall)
.buffer_font(cx),
),
)
.map(|parent| {
if !is_open {
return parent;
}
let scripting_tool_result =
self.rendered_scripting_tool_uses.get(&tool_use.id).cloned();
let (lua_script_markdown, summary) = match scripting_tool_result {
Some((markdown, summary)) => (Some(markdown), summary),
None => (None, None),
};
parent.child(
v_flex()
.child(
v_flex()
.gap_0p5()
.py_1()
.px_2p5()
.border_b_1()
.border_color(cx.theme().colors().border)
.child(v_flex().when_some(
summary.clone(),
|parent, summary| {
parent.child(
v_flex()
.gap_0p5()
.child(
Label::new("Summary:")
.weight(FontWeight::SEMIBOLD),
)
.child(Label::new(summary))
.pb_1(),
)
},
))
.child(Label::new("Input:").weight(FontWeight::SEMIBOLD))
.map(|parent| {
if let Some(markdown) = lua_script_markdown {
parent.child(markdown)
} else {
parent.child(Label::new(
"Failed to render script input to Markdown",
))
}
}),
)
.map(|parent| match tool_use.status {
ToolUseStatus::Finished(output) => parent.child(
v_flex()
.gap_0p5()
.py_1()
.px_2p5()
.child(Label::new("Result:"))
.child(Label::new(output)),
),
ToolUseStatus::Error(err) => parent.child(
v_flex()
.gap_0p5()
.py_1()
.px_2p5()
.child(Label::new("Error:"))
.child(Label::new(err)),
),
ToolUseStatus::Pending | ToolUseStatus::Running => parent,
}),
)
}),
)
}
}
impl Render for ActiveThread {

View File

@@ -92,6 +92,7 @@ pub struct AssistantPanel {
context_editor: Option<Entity<ContextEditor>>,
configuration: Option<Entity<AssistantConfiguration>>,
configuration_subscription: Option<Subscription>,
tools: Arc<ToolWorkingSet>,
local_timezone: UtcOffset,
active_view: ActiveView,
history_store: Entity<HistoryStore>,
@@ -132,7 +133,7 @@ impl AssistantPanel {
log::info!("[assistant2-debug] finished initializing ContextStore");
workspace.update_in(&mut cx, |workspace, window, cx| {
cx.new(|cx| Self::new(workspace, thread_store, context_store, window, cx))
cx.new(|cx| Self::new(workspace, thread_store, context_store, tools, window, cx))
})
})
}
@@ -141,6 +142,7 @@ impl AssistantPanel {
workspace: &Workspace,
thread_store: Entity<ThreadStore>,
context_store: Entity<assistant_context_editor::ContextStore>,
tools: Arc<ToolWorkingSet>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
@@ -166,29 +168,30 @@ impl AssistantPanel {
let history_store =
cx.new(|cx| HistoryStore::new(thread_store.clone(), context_store.clone(), cx));
let thread = cx.new(|cx| {
ActiveThread::new(
thread.clone(),
thread_store.clone(),
language_registry.clone(),
window,
cx,
)
});
Self {
active_view: ActiveView::Thread,
workspace,
project: project.clone(),
workspace: workspace.clone(),
project,
fs: fs.clone(),
language_registry,
language_registry: language_registry.clone(),
thread_store: thread_store.clone(),
thread,
thread: cx.new(|cx| {
ActiveThread::new(
thread.clone(),
thread_store.clone(),
workspace,
language_registry,
tools.clone(),
window,
cx,
)
}),
message_editor,
context_store,
context_editor: None,
configuration: None,
configuration_subscription: None,
tools,
local_timezone: UtcOffset::from_whole_seconds(
chrono::Local::now().offset().local_minus_utc(),
)
@@ -243,7 +246,9 @@ impl AssistantPanel {
ActiveThread::new(
thread.clone(),
self.thread_store.clone(),
self.workspace.clone(),
self.language_registry.clone(),
self.tools.clone(),
window,
cx,
)
@@ -376,7 +381,9 @@ impl AssistantPanel {
ActiveThread::new(
thread.clone(),
this.thread_store.clone(),
this.workspace.clone(),
this.language_registry.clone(),
this.tools.clone(),
window,
cx,
)

View File

@@ -167,8 +167,8 @@ impl PickerDelegate for FetchContextPickerDelegate {
}
}
fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> Option<SharedString> {
Some("Enter the URL that you would like to fetch".into())
fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> SharedString {
"Enter the URL that you would like to fetch".into()
}
fn selected_index(&self) -> usize {

View File

@@ -25,7 +25,7 @@ use crate::{
pub struct ContextStrip {
context_store: Entity<ContextStore>,
context_picker: Entity<ContextPicker>,
pub context_picker: Entity<ContextPicker>,
context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
focus_handle: FocusHandle,
suggest_context_kind: SuggestContextKind,
@@ -36,6 +36,7 @@ pub struct ContextStrip {
}
impl ContextStrip {
#[allow(clippy::too_many_arguments)]
pub fn new(
context_store: Entity<ContextStore>,
workspace: WeakEntity<Workspace>,

View File

@@ -27,7 +27,7 @@ use language::{Buffer, Point, Selection, TransactionId};
use language_model::{report_assistant_event, LanguageModelRegistry};
use multi_buffer::MultiBufferRow;
use parking_lot::Mutex;
use project::LspAction;
use project::ActionVariant;
use project::{CodeAction, ProjectTransaction};
use prompt_store::PromptBuilder;
use settings::{Settings, SettingsStore};
@@ -480,6 +480,7 @@ impl InlineAssistant {
}
}
#[allow(clippy::too_many_arguments)]
pub fn suggest_assist(
&mut self,
editor: &Entity<Editor>,
@@ -1450,6 +1451,7 @@ struct InlineAssistScrollLock {
}
impl EditorInlineAssists {
#[allow(clippy::too_many_arguments)]
fn new(editor: &Entity<Editor>, window: &mut Window, cx: &mut App) -> Self {
let (highlight_updates_tx, mut highlight_updates_rx) = async_watch::channel(());
Self {
@@ -1561,6 +1563,7 @@ pub struct InlineAssist {
}
impl InlineAssist {
#[allow(clippy::too_many_arguments)]
fn new(
assist_id: InlineAssistId,
group_id: InlineAssistGroupId,
@@ -1725,7 +1728,7 @@ impl CodeActionProvider for AssistantCodeActionProvider {
Task::ready(Ok(vec![CodeAction {
server_id: language::LanguageServerId(0),
range: snapshot.anchor_before(range.start)..snapshot.anchor_after(range.end),
lsp_action: LspAction::Action(Box::new(lsp::CodeAction {
lsp_action: ActionVariant::Action(Box::new(lsp::CodeAction {
title: "Fix with Assistant".into(),
..Default::default()
})),

View File

@@ -816,6 +816,7 @@ impl InlineAssistId {
}
impl PromptEditor<BufferCodegen> {
#[allow(clippy::too_many_arguments)]
pub fn new_buffer(
id: InlineAssistId,
gutter_dimensions: Arc<Mutex<GutterDimensions>>,
@@ -975,6 +976,7 @@ impl TerminalInlineAssistId {
}
impl PromptEditor<TerminalCodegen> {
#[allow(clippy::too_many_arguments)]
pub fn new_terminal(
id: TerminalInlineAssistId,
prompt_history: VecDeque<String>,

View File

@@ -2,7 +2,6 @@ use std::sync::Arc;
use editor::actions::MoveUp;
use editor::{Editor, EditorElement, EditorEvent, EditorStyle};
use file_icons::FileIcons;
use fs::Fs;
use gpui::{
Animation, AnimationExt, App, DismissEvent, Entity, Focusable, Subscription, TextStyle,
@@ -16,8 +15,8 @@ use std::time::Duration;
use text::Bias;
use theme::ThemeSettings;
use ui::{
prelude::*, ButtonLike, Disclosure, KeyBinding, PlatformStyle, PopoverMenu, PopoverMenuHandle,
Switch, Tooltip,
prelude::*, ButtonLike, KeyBinding, PlatformStyle, PopoverMenu, PopoverMenuHandle, Switch,
Tooltip,
};
use vim_mode_setting::VimModeSetting;
use workspace::Workspace;
@@ -40,7 +39,6 @@ pub struct MessageEditor {
inline_context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
model_selector: Entity<AssistantModelSelector>,
use_tools: bool,
edits_expanded: bool,
_subscriptions: Vec<Subscription>,
}
@@ -119,7 +117,6 @@ impl MessageEditor {
)
}),
use_tools: false,
edits_expanded: false,
_subscriptions: subscriptions,
}
}
@@ -306,9 +303,6 @@ impl Render for MessageEditor {
px(64.)
};
let changed_buffers = self.thread.read(cx).scripting_changed_buffers(cx);
let changed_buffers_count = changed_buffers.len();
v_flex()
.size_full()
.when(is_streaming_completion, |parent| {
@@ -369,109 +363,6 @@ impl Render for MessageEditor {
),
)
})
.when(changed_buffers_count > 0, |parent| {
parent.child(
v_flex()
.mx_2()
.bg(cx.theme().colors().element_background)
.border_1()
.border_b_0()
.border_color(cx.theme().colors().border)
.rounded_t_md()
.child(
h_flex()
.gap_2()
.p_2()
.child(
Disclosure::new("edits-disclosure", self.edits_expanded)
.on_click(cx.listener(|this, _ev, _window, cx| {
this.edits_expanded = !this.edits_expanded;
cx.notify();
})),
)
.child(
Label::new("Edits")
.size(LabelSize::XSmall)
.color(Color::Muted),
)
.child(Label::new("").size(LabelSize::XSmall).color(Color::Muted))
.child(
Label::new(format!(
"{} {}",
changed_buffers_count,
if changed_buffers_count == 1 {
"file"
} else {
"files"
}
))
.size(LabelSize::XSmall)
.color(Color::Muted),
),
)
.when(self.edits_expanded, |parent| {
parent.child(
v_flex().bg(cx.theme().colors().editor_background).children(
changed_buffers.enumerate().flat_map(|(index, buffer)| {
let file = buffer.read(cx).file()?;
let path = file.path();
let parent_label = path.parent().and_then(|parent| {
let parent_str = parent.to_string_lossy();
if parent_str.is_empty() {
None
} else {
Some(
Label::new(format!(
"{}{}",
parent_str,
std::path::MAIN_SEPARATOR_STR
))
.color(Color::Muted)
.size(LabelSize::Small),
)
}
});
let name_label = path.file_name().map(|name| {
Label::new(name.to_string_lossy().to_string())
.size(LabelSize::Small)
});
let file_icon = FileIcons::get_icon(&path, cx)
.map(Icon::from_path)
.unwrap_or_else(|| Icon::new(IconName::File));
let element = div()
.p_2()
.when(index + 1 < changed_buffers_count, |parent| {
parent
.border_color(cx.theme().colors().border)
.border_b_1()
})
.child(
h_flex()
.gap_2()
.child(file_icon)
.child(
// TODO: handle overflow
h_flex()
.children(parent_label)
.children(name_label),
)
// TODO: show lines changed
.child(Label::new("+").color(Color::Created))
.child(Label::new("-").color(Color::Deleted)),
);
Some(element)
}),
),
)
}),
)
})
.child(
v_flex()
.key_context("MessageEditor")
@@ -498,7 +389,6 @@ impl Render for MessageEditor {
let text_style = TextStyle {
color: cx.theme().colors().text,
font_family: settings.ui_font.family.clone(),
font_fallbacks: settings.ui_font.fallbacks.clone(),
font_features: settings.ui_font.features.clone(),
font_size: font_size.into(),
font_weight: settings.ui_font.weight,

View File

@@ -5,15 +5,13 @@ use assistant_tool::ToolWorkingSet;
use chrono::{DateTime, Utc};
use collections::{BTreeMap, HashMap, HashSet};
use futures::StreamExt as _;
use gpui::{App, AppContext, Context, Entity, EventEmitter, SharedString, Task};
use gpui::{App, Context, EventEmitter, SharedString, Task};
use language_model::{
LanguageModel, LanguageModelCompletionEvent, LanguageModelRegistry, LanguageModelRequest,
LanguageModelRequestMessage, LanguageModelRequestTool, LanguageModelToolResult,
LanguageModelToolUseId, MaxMonthlySpendReachedError, MessageContent, PaymentRequiredError,
Role, StopReason,
};
use project::Project;
use scripting_tool::{ScriptingSession, ScriptingTool};
use serde::{Deserialize, Serialize};
use util::{post_inc, TryFutureExt as _};
use uuid::Uuid;
@@ -73,21 +71,12 @@ pub struct Thread {
context_by_message: HashMap<MessageId, Vec<ContextId>>,
completion_count: usize,
pending_completions: Vec<PendingCompletion>,
project: Entity<Project>,
tools: Arc<ToolWorkingSet>,
tool_use: ToolUseState,
scripting_session: Entity<ScriptingSession>,
scripting_tool_use: ToolUseState,
}
impl Thread {
pub fn new(
project: Entity<Project>,
tools: Arc<ToolWorkingSet>,
cx: &mut Context<Self>,
) -> Self {
let scripting_session = cx.new(|cx| ScriptingSession::new(project.clone(), cx));
pub fn new(tools: Arc<ToolWorkingSet>, _cx: &mut Context<Self>) -> Self {
Self {
id: ThreadId::new(),
updated_at: Utc::now(),
@@ -99,20 +88,16 @@ impl Thread {
context_by_message: HashMap::default(),
completion_count: 0,
pending_completions: Vec::new(),
project,
tools,
tool_use: ToolUseState::new(),
scripting_session,
scripting_tool_use: ToolUseState::new(),
}
}
pub fn from_saved(
id: ThreadId,
saved: SavedThread,
project: Entity<Project>,
tools: Arc<ToolWorkingSet>,
cx: &mut Context<Self>,
_cx: &mut Context<Self>,
) -> Self {
let next_message_id = MessageId(
saved
@@ -121,11 +106,7 @@ impl Thread {
.map(|message| message.id.0 + 1)
.unwrap_or(0),
);
let tool_use =
ToolUseState::from_saved_messages(&saved.messages, |name| name != ScriptingTool::NAME);
let scripting_tool_use =
ToolUseState::from_saved_messages(&saved.messages, |name| name == ScriptingTool::NAME);
let scripting_session = cx.new(|cx| ScriptingSession::new(project.clone(), cx));
let tool_use = ToolUseState::from_saved_messages(&saved.messages);
Self {
id,
@@ -146,11 +127,8 @@ impl Thread {
context_by_message: HashMap::default(),
completion_count: 0,
pending_completions: Vec::new(),
project,
tools,
tool_use,
scripting_session,
scripting_tool_use,
}
}
@@ -211,65 +189,33 @@ impl Thread {
)
}
/// Returns whether all of the tool uses have finished running.
pub fn all_tools_finished(&self) -> bool {
let mut all_pending_tool_uses = self
.tool_use
.pending_tool_uses()
.into_iter()
.chain(self.scripting_tool_use.pending_tool_uses());
// If the only pending tool uses left are the ones with errors, then that means that we've finished running all
// of the pending tools.
all_pending_tool_uses.all(|tool_use| tool_use.status.is_error())
pub fn pending_tool_uses(&self) -> Vec<&PendingToolUse> {
self.tool_use.pending_tool_uses()
}
pub fn tool_uses_for_message(&self, id: MessageId) -> Vec<ToolUse> {
self.tool_use.tool_uses_for_message(id)
}
pub fn scripting_tool_uses_for_message(&self, id: MessageId) -> Vec<ToolUse> {
self.scripting_tool_use.tool_uses_for_message(id)
}
pub fn tool_results_for_message(&self, id: MessageId) -> Vec<&LanguageModelToolResult> {
self.tool_use.tool_results_for_message(id)
}
pub fn scripting_tool_results_for_message(
&self,
id: MessageId,
) -> Vec<&LanguageModelToolResult> {
self.scripting_tool_use.tool_results_for_message(id)
}
pub fn scripting_changed_buffers<'a>(
&self,
cx: &'a App,
) -> impl ExactSizeIterator<Item = &'a Entity<language::Buffer>> {
self.scripting_session.read(cx).changed_buffers()
}
pub fn message_has_tool_results(&self, message_id: MessageId) -> bool {
self.tool_use.message_has_tool_results(message_id)
}
pub fn message_has_scripting_tool_results(&self, message_id: MessageId) -> bool {
self.scripting_tool_use.message_has_tool_results(message_id)
}
pub fn insert_user_message(
&mut self,
text: impl Into<String>,
context: Vec<ContextSnapshot>,
cx: &mut Context<Self>,
) -> MessageId {
) {
let message_id = self.insert_message(Role::User, text, cx);
let context_ids = context.iter().map(|context| context.id).collect::<Vec<_>>();
self.context
.extend(context.into_iter().map(|context| (context.id, context)));
self.context_by_message.insert(message_id, context_ids);
message_id
}
pub fn insert_message(
@@ -348,25 +294,16 @@ impl Thread {
let mut request = self.to_completion_request(request_kind, cx);
if use_tools {
let mut tools = Vec::new();
tools.push(LanguageModelRequestTool {
name: ScriptingTool::NAME.into(),
description: ScriptingTool::DESCRIPTION.into(),
input_schema: ScriptingTool::input_schema(),
});
tools.extend(
self.tools()
.tools(cx)
.into_iter()
.map(|tool| LanguageModelRequestTool {
name: tool.name(),
description: tool.description(),
input_schema: tool.input_schema(),
}),
);
request.tools = tools;
request.tools = self
.tools()
.tools(cx)
.into_iter()
.map(|tool| LanguageModelRequestTool {
name: tool.name(),
description: tool.description(),
input_schema: tool.input_schema(),
})
.collect();
}
self.stream_completion(request, model, cx);
@@ -396,13 +333,10 @@ impl Thread {
content: Vec::new(),
cache: false,
};
match request_kind {
RequestKind::Chat => {
self.tool_use
.attach_tool_results(message.id, &mut request_message);
self.scripting_tool_use
.attach_tool_results(message.id, &mut request_message);
}
RequestKind::Summarize => {
// We don't care about tool use during summarization.
@@ -419,13 +353,11 @@ impl Thread {
RequestKind::Chat => {
self.tool_use
.attach_tool_uses(message.id, &mut request_message);
self.scripting_tool_use
.attach_tool_uses(message.id, &mut request_message);
}
RequestKind::Summarize => {
// We don't care about tool use during summarization.
}
};
}
request.messages.push(request_message);
}
@@ -489,7 +421,7 @@ impl Thread {
// Importantly: We do *not* want to emit a `StreamedAssistantText` event here, as it
// will result in duplicating the text of the chunk in the rendered Markdown.
thread.insert_message(Role::Assistant, chunk, cx);
};
}
}
}
LanguageModelCompletionEvent::ToolUse(tool_use) => {
@@ -498,15 +430,9 @@ impl Thread {
.iter()
.rfind(|message| message.role == Role::Assistant)
{
if tool_use.name.as_ref() == ScriptingTool::NAME {
thread
.scripting_tool_use
.request_tool_use(last_assistant_message.id, tool_use);
} else {
thread
.tool_use
.request_tool_use(last_assistant_message.id, tool_use);
}
thread
.tool_use
.request_tool_use(last_assistant_message.id, tool_use);
}
}
}
@@ -624,63 +550,6 @@ impl Thread {
});
}
pub fn use_pending_tools(&mut self, cx: &mut Context<Self>) {
let pending_tool_uses = self
.tool_use
.pending_tool_uses()
.into_iter()
.filter(|tool_use| tool_use.status.is_idle())
.cloned()
.collect::<Vec<_>>();
for tool_use in pending_tool_uses {
if let Some(tool) = self.tools.tool(&tool_use.name, cx) {
let task = tool.run(tool_use.input, self.project.clone(), cx);
self.insert_tool_output(tool_use.id.clone(), task, cx);
}
}
let pending_scripting_tool_uses = self
.scripting_tool_use
.pending_tool_uses()
.into_iter()
.filter(|tool_use| tool_use.status.is_idle())
.cloned()
.collect::<Vec<_>>();
for scripting_tool_use in pending_scripting_tool_uses {
let task = match ScriptingTool::deserialize_input(scripting_tool_use.input) {
Err(err) => Task::ready(Err(err.into())),
Ok(input) => {
let (script_id, script_task) =
self.scripting_session.update(cx, move |session, cx| {
session.run_script(input.lua_script, cx)
});
let session = self.scripting_session.clone();
cx.spawn(|_, cx| async move {
script_task.await;
let message = session.read_with(&cx, |session, _cx| {
// Using a id to get the script output seems impractical.
// Why not just include it in the Task result?
// This is because we'll later report the script state as it runs,
session
.get(script_id)
.output_message_for_llm()
.expect("Script shouldn't still be running")
})?;
Ok(message)
})
}
};
self.insert_scripting_tool_output(scripting_tool_use.id.clone(), task, cx);
}
}
pub fn insert_tool_output(
&mut self,
tool_use_id: LanguageModelToolUseId,
@@ -693,14 +562,11 @@ impl Thread {
let output = output.await;
thread
.update(&mut cx, |thread, cx| {
let pending_tool_use = thread
thread
.tool_use
.insert_tool_output(tool_use_id.clone(), output);
cx.emit(ThreadEvent::ToolFinished {
tool_use_id,
pending_tool_use,
});
cx.emit(ThreadEvent::ToolFinished { tool_use_id });
})
.ok();
}
@@ -710,52 +576,6 @@ impl Thread {
.run_pending_tool(tool_use_id, insert_output_task);
}
pub fn insert_scripting_tool_output(
&mut self,
tool_use_id: LanguageModelToolUseId,
output: Task<Result<String>>,
cx: &mut Context<Self>,
) {
let insert_output_task = cx.spawn(|thread, mut cx| {
let tool_use_id = tool_use_id.clone();
async move {
let output = output.await;
thread
.update(&mut cx, |thread, cx| {
let pending_tool_use = thread
.scripting_tool_use
.insert_tool_output(tool_use_id.clone(), output);
cx.emit(ThreadEvent::ToolFinished {
tool_use_id,
pending_tool_use,
});
})
.ok();
}
});
self.scripting_tool_use
.run_pending_tool(tool_use_id, insert_output_task);
}
pub fn send_tool_results_to_model(
&mut self,
model: Arc<dyn LanguageModel>,
cx: &mut Context<Self>,
) {
// Insert a user message to contain the tool results.
self.insert_user_message(
// TODO: Sending up a user message without any content results in the model sending back
// responses that also don't have any content. We currently don't handle this case well,
// so for now we provide some text to keep the model on track.
"Here are the tool results.",
Vec::new(),
cx,
);
self.send_to_model(model, RequestKind::Chat, true, cx);
}
/// Cancels the last pending completion, if there are any pending.
///
/// Returns whether a completion was canceled.
@@ -788,8 +608,6 @@ pub enum ThreadEvent {
ToolFinished {
#[allow(unused)]
tool_use_id: LanguageModelToolUseId,
/// The pending tool use that corresponds to this tool.
pending_tool_use: Option<PendingToolUse>,
},
}

View File

@@ -26,6 +26,7 @@ pub fn init(cx: &mut App) {
}
pub struct ThreadStore {
#[allow(unused)]
project: Entity<Project>,
tools: Arc<ToolWorkingSet>,
context_server_manager: Entity<ContextServerManager>,
@@ -77,7 +78,7 @@ impl ThreadStore {
}
pub fn create_thread(&mut self, cx: &mut Context<Self>) -> Entity<Thread> {
cx.new(|cx| Thread::new(self.project.clone(), self.tools.clone(), cx))
cx.new(|cx| Thread::new(self.tools.clone(), cx))
}
pub fn open_thread(
@@ -95,15 +96,7 @@ impl ThreadStore {
.ok_or_else(|| anyhow!("no thread found with ID: {id:?}"))?;
this.update(&mut cx, |this, cx| {
cx.new(|cx| {
Thread::from_saved(
id.clone(),
thread,
this.project.clone(),
this.tools.clone(),
cx,
)
})
cx.new(|cx| Thread::from_saved(id.clone(), thread, this.tools.clone(), cx))
})
})
}
@@ -116,35 +109,28 @@ impl ThreadStore {
updated_at: thread.updated_at(),
messages: thread
.messages()
.map(|message| {
let all_tool_uses = thread
.map(|message| SavedMessage {
id: message.id,
role: message.role,
text: message.text.clone(),
tool_uses: thread
.tool_uses_for_message(message.id)
.into_iter()
.chain(thread.scripting_tool_uses_for_message(message.id))
.map(|tool_use| SavedToolUse {
id: tool_use.id,
name: tool_use.name,
input: tool_use.input,
})
.collect();
let all_tool_results = thread
.collect(),
tool_results: thread
.tool_results_for_message(message.id)
.into_iter()
.chain(thread.scripting_tool_results_for_message(message.id))
.map(|tool_result| SavedToolResult {
tool_use_id: tool_result.tool_use_id.clone(),
is_error: tool_result.is_error,
content: tool_result.content.clone(),
})
.collect();
SavedMessage {
id: message.id,
role: message.role,
text: message.text.clone(),
tool_uses: all_tool_uses,
tool_results: all_tool_results,
}
.collect(),
})
.collect(),
};

View File

@@ -46,39 +46,25 @@ impl ToolUseState {
}
}
/// Constructs a [`ToolUseState`] from the given list of [`SavedMessage`]s.
///
/// Accepts a function to filter the tools that should be used to populate the state.
pub fn from_saved_messages(
messages: &[SavedMessage],
mut filter_by_tool_name: impl FnMut(&str) -> bool,
) -> Self {
pub fn from_saved_messages(messages: &[SavedMessage]) -> Self {
let mut this = Self::new();
let mut tool_names_by_id = HashMap::default();
for message in messages {
match message.role {
Role::Assistant => {
if !message.tool_uses.is_empty() {
let tool_uses = message
.tool_uses
.iter()
.filter(|tool_use| (filter_by_tool_name)(tool_use.name.as_ref()))
.map(|tool_use| LanguageModelToolUse {
id: tool_use.id.clone(),
name: tool_use.name.clone().into(),
input: tool_use.input.clone(),
})
.collect::<Vec<_>>();
tool_names_by_id.extend(
tool_uses
this.tool_uses_by_assistant_message.insert(
message.id,
message
.tool_uses
.iter()
.map(|tool_use| (tool_use.id.clone(), tool_use.name.clone())),
.map(|tool_use| LanguageModelToolUse {
id: tool_use.id.clone(),
name: tool_use.name.clone().into(),
input: tool_use.input.clone(),
})
.collect(),
);
this.tool_uses_by_assistant_message
.insert(message.id, tool_uses);
}
}
Role::User => {
@@ -90,14 +76,6 @@ impl ToolUseState {
for tool_result in &message.tool_results {
let tool_use_id = tool_result.tool_use_id.clone();
let Some(tool_use) = tool_names_by_id.get(&tool_use_id) else {
log::warn!("no tool name found for tool use: {tool_use_id:?}");
continue;
};
if !(filter_by_tool_name)(tool_use.as_ref()) {
continue;
}
tool_uses_by_user_message.push(tool_use_id.clone());
this.tool_results.insert(
@@ -224,7 +202,7 @@ impl ToolUseState {
&mut self,
tool_use_id: LanguageModelToolUseId,
output: Result<String>,
) -> Option<PendingToolUse> {
) {
match output {
Ok(output) => {
self.tool_results.insert(
@@ -235,7 +213,7 @@ impl ToolUseState {
is_error: false,
},
);
self.pending_tool_uses_by_id.remove(&tool_use_id)
self.pending_tool_uses_by_id.remove(&tool_use_id);
}
Err(err) => {
self.tool_results.insert(
@@ -250,8 +228,6 @@ impl ToolUseState {
if let Some(tool_use) = self.pending_tool_uses_by_id.get_mut(&tool_use_id) {
tool_use.status = PendingToolUseStatus::Error(err.to_string().into());
}
self.pending_tool_uses_by_id.get(&tool_use_id).cloned()
}
}
}
@@ -291,7 +267,6 @@ impl ToolUseState {
pub struct PendingToolUse {
pub id: LanguageModelToolUseId,
/// The ID of the Assistant message in which the tool use was requested.
#[allow(unused)]
pub assistant_message_id: MessageId,
pub name: Arc<str>,
pub input: serde_json::Value,

View File

@@ -647,6 +647,7 @@ impl AssistantContext {
)
}
#[allow(clippy::too_many_arguments)]
pub fn new(
id: ContextId,
replica_id: ReplicaId,
@@ -767,6 +768,7 @@ impl AssistantContext {
}
}
#[allow(clippy::too_many_arguments)]
pub fn deserialize(
saved_context: SavedContext,
path: PathBuf,

View File

@@ -535,6 +535,7 @@ impl ContextEditor {
}
}
#[allow(clippy::too_many_arguments)]
pub fn run_command(
&mut self,
command_range: Range<language::Anchor>,
@@ -2056,6 +2057,7 @@ impl ContextEditor {
.unwrap_or_else(|| Cow::Borrowed(DEFAULT_TAB_TITLE))
}
#[allow(clippy::too_many_arguments)]
fn render_patch_block(
&mut self,
range: Range<text::Anchor>,

View File

@@ -104,53 +104,49 @@ impl ContextStore {
const CONTEXT_WATCH_DURATION: Duration = Duration::from_millis(100);
let (mut events, _) = fs.watch(contexts_dir(), CONTEXT_WATCH_DURATION).await;
let this =
cx.new(|cx: &mut Context<Self>| {
let context_server_factory_registry =
ContextServerFactoryRegistry::default_global(cx);
let context_server_manager = cx.new(|cx| {
ContextServerManager::new(
context_server_factory_registry,
project.clone(),
cx,
)
});
let mut this = Self {
contexts: Vec::new(),
contexts_metadata: Vec::new(),
context_server_manager,
context_server_slash_command_ids: HashMap::default(),
host_contexts: Vec::new(),
fs,
languages,
slash_commands,
telemetry,
_watch_updates: cx.spawn(|this, mut cx| {
async move {
while events.next().await.is_some() {
this.update(&mut cx, |this, cx| this.reload(cx))?
.await
.log_err();
}
anyhow::Ok(())
let this = cx.new(|cx: &mut Context<Self>| {
let context_server_factory_registry =
ContextServerFactoryRegistry::default_global(cx);
let context_server_manager = cx.new(|cx| {
ContextServerManager::new(context_server_factory_registry, project.clone(), cx)
});
let mut this = Self {
contexts: Vec::new(),
contexts_metadata: Vec::new(),
context_server_manager,
context_server_slash_command_ids: HashMap::default(),
host_contexts: Vec::new(),
fs,
languages,
slash_commands,
telemetry,
_watch_updates: cx.spawn(|this, mut cx| {
async move {
while events.next().await.is_some() {
this.update(&mut cx, |this, cx| this.reload(cx))?
.await
.log_err();
}
.log_err()
}),
client_subscription: None,
_project_subscriptions: vec![
cx.subscribe(&project, Self::handle_project_event)
],
project_is_shared: false,
client: project.read(cx).client(),
project: project.clone(),
prompt_builder,
};
this.handle_project_shared(project.clone(), cx);
this.synchronize_contexts(cx);
this.register_context_server_handlers(cx);
this.reload(cx).detach_and_log_err(cx);
this
})?;
anyhow::Ok(())
}
.log_err()
}),
client_subscription: None,
_project_subscriptions: vec![
cx.observe(&project, Self::handle_project_changed),
cx.subscribe(&project, Self::handle_project_event),
],
project_is_shared: false,
client: project.read(cx).client(),
project: project.clone(),
prompt_builder,
};
this.handle_project_changed(project.clone(), cx);
this.synchronize_contexts(cx);
this.register_context_server_handlers(cx);
this.reload(cx).detach_and_log_err(cx);
this
})?;
Ok(this)
})
@@ -292,7 +288,7 @@ impl ContextStore {
})?
}
fn handle_project_shared(&mut self, _: Entity<Project>, cx: &mut Context<Self>) {
fn handle_project_changed(&mut self, _: Entity<Project>, cx: &mut Context<Self>) {
let is_shared = self.project.read(cx).is_shared();
let was_shared = mem::replace(&mut self.project_is_shared, is_shared);
if is_shared == was_shared {
@@ -322,14 +318,11 @@ impl ContextStore {
fn handle_project_event(
&mut self,
project: Entity<Project>,
_: Entity<Project>,
event: &project::Event,
cx: &mut Context<Self>,
) {
match event {
project::Event::RemoteIdChanged(_) => {
self.handle_project_shared(project, cx);
}
project::Event::Reshared => {
self.advertise_contexts(cx);
}

View File

@@ -5,9 +5,9 @@ use assistant_slash_command::{AfterCompletion, SlashCommandLine, SlashCommandWor
use editor::{CompletionProvider, Editor};
use fuzzy::{match_strings, StringMatchCandidate};
use gpui::{App, AppContext as _, Context, Entity, Task, WeakEntity, Window};
use language::{Anchor, Buffer, ToPoint};
use language::{Anchor, Buffer, LanguageServerId, ToPoint};
use parking_lot::Mutex;
use project::{lsp_store::CompletionDocumentation, CompletionIntent, CompletionSource};
use project::{lsp_store::CompletionDocumentation, CompletionIntent};
use rope::Point;
use std::{
cell::RefCell,
@@ -125,8 +125,10 @@ impl SlashCommandCompletionProvider {
)),
new_text,
label: command.label(cx),
server_id: LanguageServerId(0),
lsp_completion: Default::default(),
confirm,
source: CompletionSource::Custom,
resolved: true,
})
})
.collect()
@@ -134,6 +136,7 @@ impl SlashCommandCompletionProvider {
})
}
#[allow(clippy::too_many_arguments)]
fn complete_command_argument(
&self,
command_name: &str,
@@ -222,8 +225,10 @@ impl SlashCommandCompletionProvider {
label: new_argument.label,
new_text,
documentation: None,
server_id: LanguageServerId(0),
lsp_completion: Default::default(),
confirm,
source: CompletionSource::Custom,
resolved: true,
}
})
.collect())

View File

@@ -88,6 +88,7 @@ pub trait SlashCommand: 'static + Send + Sync {
fn accepts_arguments(&self) -> bool {
self.requires_argument()
}
#[allow(clippy::too_many_arguments)]
fn run(
self: Arc<Self>,
arguments: &[String],

View File

@@ -17,6 +17,6 @@ collections.workspace = true
derive_more.workspace = true
gpui.workspace = true
parking_lot.workspace = true
project.workspace = true
serde.workspace = true
serde_json.workspace = true
workspace.workspace = true

View File

@@ -4,8 +4,8 @@ mod tool_working_set;
use std::sync::Arc;
use anyhow::Result;
use gpui::{App, Entity, Task};
use project::Project;
use gpui::{App, Task, WeakEntity, Window};
use workspace::Workspace;
pub use crate::tool_registry::*;
pub use crate::tool_working_set::*;
@@ -31,7 +31,8 @@ pub trait Tool: 'static + Send + Sync {
fn run(
self: Arc<Self>,
input: serde_json::Value,
project: Entity<Project>,
workspace: WeakEntity<Workspace>,
window: &mut Window,
cx: &mut App,
) -> Task<Result<String>>;
}

View File

@@ -20,3 +20,4 @@ project.workspace = true
schemars.workspace = true
serde.workspace = true
serde_json.workspace = true
workspace.workspace = true

View File

@@ -1,11 +1,11 @@
use std::sync::Arc;
use anyhow::Result;
use anyhow::{anyhow, Result};
use assistant_tool::Tool;
use gpui::{App, Entity, Task};
use project::Project;
use gpui::{App, Task, WeakEntity, Window};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use workspace::Workspace;
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct ListWorktreesToolInput {}
@@ -34,9 +34,16 @@ impl Tool for ListWorktreesTool {
fn run(
self: Arc<Self>,
_input: serde_json::Value,
project: Entity<Project>,
workspace: WeakEntity<Workspace>,
_window: &mut Window,
cx: &mut App,
) -> Task<Result<String>> {
let Some(workspace) = workspace.upgrade() else {
return Task::ready(Err(anyhow!("workspace dropped")));
};
let project = workspace.read(cx).project().clone();
cx.spawn(|cx| async move {
cx.update(|cx| {
#[derive(Debug, Serialize)]

View File

@@ -3,8 +3,7 @@ use std::sync::Arc;
use anyhow::{anyhow, Result};
use assistant_tool::Tool;
use chrono::{Local, Utc};
use gpui::{App, Entity, Task};
use project::Project;
use gpui::{App, Task, WeakEntity, Window};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
@@ -42,7 +41,8 @@ impl Tool for NowTool {
fn run(
self: Arc<Self>,
input: serde_json::Value,
_project: Entity<Project>,
_workspace: WeakEntity<workspace::Workspace>,
_window: &mut Window,
_cx: &mut App,
) -> Task<Result<String>> {
let input: NowToolInput = match serde_json::from_value(input) {

View File

@@ -3,10 +3,11 @@ use std::sync::Arc;
use anyhow::{anyhow, Result};
use assistant_tool::Tool;
use gpui::{App, Entity, Task};
use project::{Project, ProjectPath, WorktreeId};
use gpui::{App, Task, WeakEntity, Window};
use project::{ProjectPath, WorktreeId};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use workspace::Workspace;
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct ReadFileToolInput {
@@ -37,14 +38,20 @@ impl Tool for ReadFileTool {
fn run(
self: Arc<Self>,
input: serde_json::Value,
project: Entity<Project>,
workspace: WeakEntity<Workspace>,
_window: &mut Window,
cx: &mut App,
) -> Task<Result<String>> {
let Some(workspace) = workspace.upgrade() else {
return Task::ready(Err(anyhow!("workspace dropped")));
};
let input = match serde_json::from_value::<ReadFileToolInput>(input) {
Ok(input) => input,
Err(err) => return Task::ready(Err(anyhow!(err))),
};
let project = workspace.read(cx).project().clone();
let project_path = ProjectPath {
worktree_id: WorktreeId::from_usize(input.worktree_id),
path: input.path,

View File

@@ -828,6 +828,7 @@ impl BufferDiff {
Some(start..end)
}
#[allow(clippy::too_many_arguments)]
pub async fn update_diff(
this: Entity<BufferDiff>,
buffer: text::BufferSnapshot,
@@ -837,8 +838,8 @@ impl BufferDiff {
language: Option<Arc<Language>>,
language_registry: Option<Arc<LanguageRegistry>>,
cx: &mut AsyncApp,
) -> anyhow::Result<BufferDiffSnapshot> {
let inner = if base_text_changed || language_changed {
) -> anyhow::Result<Option<Range<Anchor>>> {
let snapshot = if base_text_changed || language_changed {
cx.update(|cx| {
Self::build(
buffer.clone(),
@@ -860,45 +861,18 @@ impl BufferDiff {
})?
.await
};
Ok(BufferDiffSnapshot {
inner,
secondary_diff: None,
})
this.update(cx, |this, _| this.set_state(snapshot, &buffer))
}
pub fn set_snapshot(
pub fn update_diff_from(
&mut self,
buffer: &text::BufferSnapshot,
new_snapshot: BufferDiffSnapshot,
language_changed: bool,
secondary_changed_range: Option<Range<Anchor>>,
other: &Entity<Self>,
cx: &mut Context<Self>,
) -> Option<Range<Anchor>> {
let changed_range = self.set_state(new_snapshot.inner, buffer);
if language_changed {
cx.emit(BufferDiffEvent::LanguageChanged);
}
let changed_range = match (secondary_changed_range, changed_range) {
(None, None) => None,
(Some(unstaged_range), None) => self.range_to_hunk_range(unstaged_range, &buffer, cx),
(None, Some(uncommitted_range)) => Some(uncommitted_range),
(Some(unstaged_range), Some(uncommitted_range)) => {
let mut start = uncommitted_range.start;
let mut end = uncommitted_range.end;
if let Some(unstaged_range) = self.range_to_hunk_range(unstaged_range, &buffer, cx)
{
start = unstaged_range.start.min(&uncommitted_range.start, &buffer);
end = unstaged_range.end.max(&uncommitted_range.end, &buffer);
}
Some(start..end)
}
};
cx.emit(BufferDiffEvent::DiffChanged {
changed_range: changed_range.clone(),
});
changed_range
let other = other.read(cx).inner.clone();
self.set_state(other, buffer)
}
fn set_state(

View File

@@ -229,6 +229,7 @@ impl Database {
}
/// Creates a new channel message.
#[allow(clippy::too_many_arguments)]
pub async fn create_channel_message(
&self,
channel_id: ChannelId,

View File

@@ -122,6 +122,7 @@ impl Database {
.await
}
#[allow(clippy::too_many_arguments)]
pub async fn get_or_create_user_by_github_account_tx(
&self,
github_login: &str,

View File

@@ -289,6 +289,7 @@ impl LlmDatabase {
.await
}
#[allow(clippy::too_many_arguments)]
pub async fn record_usage(
&self,
user_id: UserId,
@@ -553,6 +554,7 @@ impl LlmDatabase {
.await
}
#[allow(clippy::too_many_arguments)]
async fn update_usage_for_measure(
&self,
user_id: UserId,

View File

@@ -33,6 +33,7 @@ pub struct LlmTokenClaims {
const LLM_TOKEN_LIFETIME: Duration = Duration::from_secs(60 * 60);
impl LlmTokenClaims {
#[allow(clippy::too_many_arguments)]
pub fn create(
user: &user::Model,
is_staff: bool,

View File

@@ -697,6 +697,7 @@ impl Server {
})
}
#[allow(clippy::too_many_arguments)]
pub fn handle_connection(
self: &Arc<Self>,
connection: Connection,
@@ -1080,6 +1081,7 @@ pub fn routes(server: Arc<Server>) -> Router<(), Body> {
.layer(Extension(server))
}
#[allow(clippy::too_many_arguments)]
pub async fn handle_websocket_request(
TypedHeader(ProtocolVersion(protocol_version)): TypedHeader<ProtocolVersion>,
app_version_header: Option<TypedHeader<AppVersionHeader>>,

View File

@@ -3,6 +3,7 @@ use crate::{
tests::{rust_lang, TestServer},
};
use call::ActiveCall;
use collections::HashMap;
use editor::{
actions::{
ConfirmCodeAction, ConfirmCompletion, ConfirmRename, ContextMenuFirst, Redo, Rename,
@@ -1982,6 +1983,7 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA
blame_entry("3a3a3a", 2..3),
blame_entry("4c4c4c", 3..4),
],
permalinks: HashMap::default(), // This field is deprecrated
messages: [
("1b1b1b", "message for idx-0"),
("0d0d0d", "message for idx-1"),

View File

@@ -6770,7 +6770,7 @@ async fn test_remote_git_branches(
assert_eq!(branches_b, branches_set);
cx_b.update(|cx| repo_b.read(cx).change_branch(new_branch))
cx_b.update(|cx| repo_b.read(cx).change_branch(new_branch.to_string()))
.await
.unwrap()
.unwrap();
@@ -6790,15 +6790,23 @@ async fn test_remote_git_branches(
assert_eq!(host_branch.name, branches[2]);
// Also try creating a new branch
cx_b.update(|cx| repo_b.read(cx).create_branch("totally-new-branch"))
.await
.unwrap()
.unwrap();
cx_b.update(|cx| {
repo_b
.read(cx)
.create_branch("totally-new-branch".to_string())
})
.await
.unwrap()
.unwrap();
cx_b.update(|cx| repo_b.read(cx).change_branch("totally-new-branch"))
.await
.unwrap()
.unwrap();
cx_b.update(|cx| {
repo_b
.read(cx)
.change_branch("totally-new-branch".to_string())
})
.await
.unwrap()
.unwrap();
executor.run_until_parked();

View File

@@ -463,6 +463,7 @@ impl<T: RandomizedTest> TestPlan<T> {
})
}
#[allow(clippy::too_many_arguments)]
async fn apply_server_operation(
plan: Arc<Mutex<Self>>,
deterministic: BackgroundExecutor,

View File

@@ -294,7 +294,7 @@ async fn test_ssh_collaboration_git_branches(
assert_eq!(&branches_b, &branches_set);
cx_b.update(|cx| repo_b.read(cx).change_branch(new_branch))
cx_b.update(|cx| repo_b.read(cx).change_branch(new_branch.to_string()))
.await
.unwrap()
.unwrap();
@@ -316,15 +316,23 @@ async fn test_ssh_collaboration_git_branches(
assert_eq!(server_branch.name, branches[2]);
// Also try creating a new branch
cx_b.update(|cx| repo_b.read(cx).create_branch("totally-new-branch"))
.await
.unwrap()
.unwrap();
cx_b.update(|cx| {
repo_b
.read(cx)
.create_branch("totally-new-branch".to_string())
})
.await
.unwrap()
.unwrap();
cx_b.update(|cx| repo_b.read(cx).change_branch("totally-new-branch"))
.await
.unwrap()
.unwrap();
cx_b.update(|cx| {
repo_b
.read(cx)
.change_branch("totally-new-branch".to_string())
})
.await
.unwrap()
.unwrap();
executor.run_until_parked();

View File

@@ -10,9 +10,9 @@ use gpui::{
};
use language::{
language_settings::SoftWrap, Anchor, Buffer, BufferSnapshot, CodeLabel, LanguageRegistry,
ToOffset,
LanguageServerId, ToOffset,
};
use project::{search::SearchQuery, Completion, CompletionSource};
use project::{search::SearchQuery, Completion};
use settings::Settings;
use std::{
cell::RefCell,
@@ -309,9 +309,11 @@ impl MessageEditor {
old_range: range.clone(),
new_text,
label,
confirm: None,
documentation: None,
source: CompletionSource::Custom,
server_id: LanguageServerId(0), // TODO: Make this optional or something?
lsp_completion: Default::default(), // TODO: Make this optional or something?
confirm: None,
resolved: true,
}
})
.collect()

View File

@@ -869,6 +869,7 @@ impl CollabPanel {
})
}
#[allow(clippy::too_many_arguments)]
fn render_participant_project(
&self,
project_id: u64,

View File

@@ -78,7 +78,6 @@ pub struct ComponentId(pub &'static str);
#[derive(Clone)]
pub struct ComponentMetadata {
id: ComponentId,
name: SharedString,
scope: Option<ComponentScope>,
description: Option<SharedString>,
@@ -86,10 +85,6 @@ pub struct ComponentMetadata {
}
impl ComponentMetadata {
pub fn id(&self) -> ComponentId {
self.id.clone()
}
pub fn name(&self) -> SharedString {
self.name.clone()
}
@@ -161,11 +156,9 @@ pub fn components() -> AllComponents {
for (ref scope, name, description) in &data.components {
let preview = data.previews.get(name).cloned();
let component_name = SharedString::new_static(name);
let id = ComponentId(name);
all_components.insert(
id.clone(),
ComponentId(name),
ComponentMetadata {
id,
name: component_name,
scope: scope.clone(),
description: description.map(Into::into),

View File

@@ -23,4 +23,3 @@ project.workspace = true
ui.workspace = true
workspace.workspace = true
notifications.workspace = true
collections.workspace = true

View File

@@ -6,14 +6,12 @@ use std::iter::Iterator;
use std::sync::Arc;
use client::UserStore;
use component::{components, ComponentId, ComponentMetadata};
use component::{components, ComponentMetadata};
use gpui::{
list, prelude::*, uniform_list, App, Entity, EventEmitter, FocusHandle, Focusable, Task,
WeakEntity, Window,
};
use collections::HashMap;
use gpui::{ListState, ScrollHandle, UniformListScrollHandle};
use languages::LanguageRegistry;
use notifications::status_toast::{StatusToast, ToastIcon};
@@ -61,8 +59,6 @@ pub fn init(app_state: Arc<AppState>, cx: &mut App) {
}
enum PreviewEntry {
AllComponents,
Separator,
Component(ComponentMetadata),
SectionHeader(SharedString),
}
@@ -79,22 +75,13 @@ impl From<SharedString> for PreviewEntry {
}
}
#[derive(Default, Debug, Clone, PartialEq, Eq)]
enum PreviewPage {
#[default]
AllComponents,
Component(ComponentId),
}
struct ComponentPreview {
focus_handle: FocusHandle,
_view_scroll_handle: ScrollHandle,
nav_scroll_handle: UniformListScrollHandle,
component_map: HashMap<ComponentId, ComponentMetadata>,
active_page: PreviewPage,
components: Vec<ComponentMetadata>,
component_list: ListState,
cursor_index: usize,
selected_index: usize,
language_registry: Arc<LanguageRegistry>,
workspace: WeakEntity<Workspace>,
user_store: Entity<UserStore>,
@@ -108,25 +95,22 @@ impl ComponentPreview {
selected_index: impl Into<Option<usize>>,
cx: &mut Context<Self>,
) -> Self {
let sorted_components = components().all_sorted();
let components = components().all_sorted();
let initial_length = components.len();
let selected_index = selected_index.into().unwrap_or(0);
let component_list = ListState::new(
sorted_components.len(),
gpui::ListAlignment::Top,
px(1500.0),
{
let component_list =
ListState::new(initial_length, gpui::ListAlignment::Top, px(1500.0), {
let this = cx.entity().downgrade();
move |ix, window: &mut Window, cx: &mut App| {
this.update(cx, |this, cx| {
let component = this.get_component(ix);
this.render_preview(&component, window, cx)
this.render_preview(ix, &component, window, cx)
.into_any_element()
})
.unwrap()
}
},
);
});
let mut component_preview = Self {
focus_handle: cx.focus_handle(),
@@ -135,15 +119,13 @@ impl ComponentPreview {
language_registry,
user_store,
workspace,
active_page: PreviewPage::AllComponents,
component_map: components().0,
components: sorted_components,
components,
component_list,
cursor_index: selected_index,
selected_index,
};
if component_preview.cursor_index > 0 {
component_preview.scroll_to_preview(component_preview.cursor_index, cx);
if component_preview.selected_index > 0 {
component_preview.scroll_to_preview(component_preview.selected_index, cx);
}
component_preview.update_component_list(cx);
@@ -153,12 +135,7 @@ impl ComponentPreview {
fn scroll_to_preview(&mut self, ix: usize, cx: &mut Context<Self>) {
self.component_list.scroll_to_reveal_item(ix);
self.cursor_index = ix;
cx.notify();
}
fn set_active_page(&mut self, page: PreviewPage, cx: &mut Context<Self>) {
self.active_page = page;
self.selected_index = ix;
cx.notify();
}
@@ -169,6 +146,7 @@ impl ComponentPreview {
fn scope_ordered_entries(&self) -> Vec<PreviewEntry> {
use std::collections::HashMap;
// Group components by scope
let mut scope_groups: HashMap<Option<ComponentScope>, Vec<ComponentMetadata>> =
HashMap::default();
@@ -179,12 +157,15 @@ impl ComponentPreview {
.push(component.clone());
}
// Sort components within each scope by name
for components in scope_groups.values_mut() {
components.sort_by_key(|c| c.name().to_lowercase());
}
// Build entries with scopes in a defined order
let mut entries = Vec::new();
// Define scope order (we want Unknown at the end)
let known_scopes = [
ComponentScope::Layout,
ComponentScope::Input,
@@ -194,16 +175,15 @@ impl ComponentPreview {
ComponentScope::VersionControl,
];
// Always show all components first
entries.push(PreviewEntry::AllComponents);
entries.push(PreviewEntry::Separator);
// First add components with known scopes
for scope in known_scopes.iter() {
let scope_key = Some(scope.clone());
if let Some(components) = scope_groups.remove(&scope_key) {
if !components.is_empty() {
// Add section header
entries.push(PreviewEntry::SectionHeader(scope.to_string().into()));
// Add all components under this scope
for component in components {
entries.push(PreviewEntry::Component(component));
}
@@ -211,13 +191,16 @@ impl ComponentPreview {
}
}
// Handle components with Unknown scope
for (scope, components) in &scope_groups {
if let Some(ComponentScope::Unknown(_)) = scope {
if !components.is_empty() {
// Add the unknown scope header
if let Some(scope_value) = scope {
entries.push(PreviewEntry::SectionHeader(scope_value.to_string().into()));
}
// Add all components under this unknown scope
for component in components {
entries.push(PreviewEntry::Component(component.clone()));
}
@@ -225,9 +208,9 @@ impl ComponentPreview {
}
}
// Handle components with no scope
if let Some(components) = scope_groups.get(&None) {
if !components.is_empty() {
entries.push(PreviewEntry::Separator);
entries.push(PreviewEntry::SectionHeader("Uncategorized".into()));
for component in components {
@@ -243,42 +226,22 @@ impl ComponentPreview {
&self,
ix: usize,
entry: &PreviewEntry,
selected: bool,
cx: &Context<Self>,
) -> impl IntoElement {
match entry {
PreviewEntry::Component(component_metadata) => {
let id = component_metadata.id();
let selected = self.active_page == PreviewPage::Component(id.clone());
ListItem::new(ix)
.child(Label::new(component_metadata.name().clone()).color(Color::Default))
.selectable(true)
.toggle_state(selected)
.inset(true)
.on_click(cx.listener(move |this, _, _, cx| {
let id = id.clone();
this.set_active_page(PreviewPage::Component(id), cx);
}))
.into_any_element()
}
PreviewEntry::Component(component_metadata) => ListItem::new(ix)
.child(Label::new(component_metadata.name().clone()).color(Color::Default))
.selectable(true)
.toggle_state(selected)
.inset(true)
.on_click(cx.listener(move |this, _, _, cx| {
this.scroll_to_preview(ix, cx);
}))
.into_any_element(),
PreviewEntry::SectionHeader(shared_string) => ListSubHeader::new(shared_string)
.inset(true)
.into_any_element(),
PreviewEntry::AllComponents => {
let selected = self.active_page == PreviewPage::AllComponents;
ListItem::new(ix)
.child(Label::new("All Components").color(Color::Default))
.selectable(true)
.toggle_state(selected)
.inset(true)
.on_click(cx.listener(move |this, _, _, cx| {
this.set_active_page(PreviewPage::AllComponents, cx);
}))
.into_any_element()
}
PreviewEntry::Separator => ListItem::new(ix)
.child(h_flex().pt_3().child(Divider::horizontal_dashed()))
.into_any_element(),
}
}
@@ -297,13 +260,11 @@ impl ComponentPreview {
weak_entity
.update(cx, |this, cx| match entry {
PreviewEntry::Component(component) => this
.render_preview(component, window, cx)
.render_preview(ix, component, window, cx)
.into_any_element(),
PreviewEntry::SectionHeader(shared_string) => this
.render_scope_header(ix, shared_string.clone(), window, cx)
.into_any_element(),
PreviewEntry::AllComponents => div().w_full().h_0().into_any_element(),
PreviewEntry::Separator => div().w_full().h_0().into_any_element(),
})
.unwrap()
},
@@ -329,6 +290,7 @@ impl ComponentPreview {
fn render_preview(
&self,
_ix: usize,
component: &ComponentMetadata,
window: &mut Window,
cx: &mut App,
@@ -379,44 +341,6 @@ impl ComponentPreview {
.into_any_element()
}
fn render_all_components(&self) -> impl IntoElement {
v_flex()
.id("component-list")
.px_8()
.pt_4()
.size_full()
.child(
list(self.component_list.clone())
.flex_grow()
.with_sizing_behavior(gpui::ListSizingBehavior::Auto),
)
}
fn render_component_page(
&mut self,
component_id: &ComponentId,
window: &mut Window,
cx: &mut Context<Self>,
) -> impl IntoElement {
let component = self.component_map.get(&component_id);
if let Some(component) = component {
v_flex()
.w_full()
.flex_initial()
.min_h_full()
.child(self.render_preview(component, window, cx))
.into_any_element()
} else {
v_flex()
.size_full()
.items_center()
.justify_center()
.child("Component not found")
.into_any_element()
}
}
fn test_status_toast(&self, window: &mut Window, cx: &mut Context<Self>) {
if let Some(workspace) = self.workspace.upgrade() {
workspace.update(cx, |workspace, cx| {
@@ -439,9 +363,8 @@ impl ComponentPreview {
}
impl Render for ComponentPreview {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
fn render(&mut self, _window: &mut Window, cx: &mut Context<'_, Self>) -> impl IntoElement {
let sidebar_entries = self.scope_ordered_entries();
let active_page = self.active_page.clone();
h_flex()
.id("component-preview")
@@ -463,7 +386,12 @@ impl Render for ComponentPreview {
move |this, range, _window, cx| {
range
.map(|ix| {
this.render_sidebar_entry(ix, &sidebar_entries[ix], cx)
this.render_sidebar_entry(
ix,
&sidebar_entries[ix],
ix == this.selected_index,
cx,
)
})
.collect()
},
@@ -487,12 +415,18 @@ impl Render for ComponentPreview {
),
),
)
.child(match active_page {
PreviewPage::AllComponents => self.render_all_components().into_any_element(),
PreviewPage::Component(id) => self
.render_component_page(&id, window, cx)
.into_any_element(),
})
.child(
v_flex()
.id("component-list")
.px_8()
.pt_4()
.size_full()
.child(
list(self.component_list.clone())
.flex_grow()
.with_sizing_behavior(gpui::ListSizingBehavior::Auto),
),
)
}
}
@@ -531,7 +465,7 @@ impl Item for ComponentPreview {
let language_registry = self.language_registry.clone();
let user_store = self.user_store.clone();
let weak_workspace = self.workspace.clone();
let selected_index = self.cursor_index;
let selected_index = self.selected_index;
Some(cx.new(|cx| {
Self::new(

View File

@@ -31,3 +31,4 @@ settings.workspace = true
smol.workspace = true
url = { workspace = true, features = ["serde"] }
util.workspace = true
workspace.workspace = true

View File

@@ -1,9 +1,8 @@
use std::sync::Arc;
use anyhow::{anyhow, bail, Result};
use anyhow::{anyhow, bail};
use assistant_tool::Tool;
use gpui::{App, Entity, Task};
use project::Project;
use gpui::{App, Entity, Task, Window};
use crate::manager::ContextServerManager;
use crate::types;
@@ -50,11 +49,12 @@ impl Tool for ContextServerTool {
}
fn run(
self: Arc<Self>,
self: std::sync::Arc<Self>,
input: serde_json::Value,
_project: Entity<Project>,
_workspace: gpui::WeakEntity<workspace::Workspace>,
_: &mut Window,
cx: &mut App,
) -> Task<Result<String>> {
) -> gpui::Task<gpui::Result<String>> {
if let Some(server) = self.server_manager.read(cx).get_server(&self.server_id) {
cx.foreground_executor().spawn({
let tool_name = self.tool.name.clone();

View File

@@ -623,21 +623,16 @@ impl Copilot {
pub fn sign_out(&mut self, cx: &mut Context<Self>) -> Task<Result<()>> {
self.update_sign_in_status(request::SignInStatus::NotSignedIn, cx);
match &self.server {
CopilotServer::Running(RunningCopilotServer { lsp: server, .. }) => {
let server = server.clone();
cx.background_spawn(async move {
server
.request::<request::SignOut>(request::SignOutParams {})
.await?;
anyhow::Ok(())
})
}
CopilotServer::Disabled => cx.background_spawn(async move {
clear_copilot_config_dir().await;
if let CopilotServer::Running(RunningCopilotServer { lsp: server, .. }) = &self.server {
let server = server.clone();
cx.background_spawn(async move {
server
.request::<request::SignOut>(request::SignOutParams {})
.await?;
anyhow::Ok(())
}),
_ => Task::ready(Err(anyhow!("copilot hasn't started yet"))),
})
} else {
Task::ready(Err(anyhow!("copilot hasn't started yet")))
}
}
@@ -1021,10 +1016,6 @@ async fn clear_copilot_dir() {
remove_matching(paths::copilot_dir(), |_| true).await
}
async fn clear_copilot_config_dir() {
remove_matching(copilot_chat::copilot_chat_config_dir(), |_| true).await
}
async fn get_copilot_lsp(http: Arc<dyn HttpClient>) -> anyhow::Result<PathBuf> {
const SERVER_PATH: &str = "dist/language-server.js";

View File

@@ -4,14 +4,13 @@ use std::sync::OnceLock;
use anyhow::{anyhow, Result};
use chrono::DateTime;
use collections::HashSet;
use fs::Fs;
use futures::{io::BufReader, stream::BoxStream, AsyncBufReadExt, AsyncReadExt, StreamExt};
use gpui::{prelude::*, App, AsyncApp, Global};
use http_client::{AsyncBody, HttpClient, Method, Request as HttpRequest};
use paths::home_dir;
use serde::{Deserialize, Serialize};
use settings::watch_config_dir;
use settings::watch_config_file;
use strum::EnumIter;
pub const COPILOT_CHAT_COMPLETION_URL: &str = "https://api.githubcopilot.com/chat/completions";
@@ -213,7 +212,7 @@ pub fn init(fs: Arc<dyn Fs>, client: Arc<dyn HttpClient>, cx: &mut App) {
cx.set_global(GlobalCopilotChat(copilot_chat));
}
pub fn copilot_chat_config_dir() -> &'static PathBuf {
fn copilot_chat_config_dir() -> &'static PathBuf {
static COPILOT_CHAT_CONFIG_DIR: OnceLock<PathBuf> = OnceLock::new();
COPILOT_CHAT_CONFIG_DIR.get_or_init(|| {
@@ -238,18 +237,27 @@ impl CopilotChat {
}
pub fn new(fs: Arc<dyn Fs>, client: Arc<dyn HttpClient>, cx: &App) -> Self {
let config_paths: HashSet<PathBuf> = copilot_chat_config_paths().into_iter().collect();
let dir_path = copilot_chat_config_dir();
let config_paths = copilot_chat_config_paths();
let resolve_config_path = {
let fs = fs.clone();
async move {
for config_path in config_paths.iter() {
if fs.metadata(config_path).await.is_ok_and(|v| v.is_some()) {
return config_path.clone();
}
}
config_paths[0].clone()
}
};
cx.spawn(|cx| async move {
let mut parent_watch_rx = watch_config_dir(
cx.background_executor(),
fs.clone(),
dir_path.clone(),
config_paths,
);
while let Some(contents) = parent_watch_rx.next().await {
let config_file = resolve_config_path.await;
let mut config_file_rx = watch_config_file(cx.background_executor(), fs, config_file);
while let Some(contents) = config_file_rx.next().await {
let oauth_token = extract_oauth_token(contents);
cx.update(|cx| {
if let Some(this) = Self::global(cx).as_ref() {
this.update(cx, |this, cx| {

View File

@@ -311,10 +311,7 @@ impl ProjectDiagnosticsEditor {
cx: &mut Context<Workspace>,
) {
if let Some(existing) = workspace.item_of_type::<ProjectDiagnosticsEditor>(cx) {
let is_active = workspace
.active_item(cx)
.is_some_and(|item| item.item_id() == existing.item_id());
workspace.activate_item(&existing, true, !is_active, window, cx);
workspace.activate_item(&existing, true, true, window, cx);
} else {
let workspace_handle = cx.entity().downgrade();

View File

@@ -340,9 +340,7 @@ gpui::actions!(
MoveToPreviousWordStart,
MoveToStartOfParagraph,
MoveToStartOfExcerpt,
MoveToStartOfNextExcerpt,
MoveToEndOfExcerpt,
MoveToEndOfPreviousExcerpt,
MoveUp,
Newline,
NewlineAbove,
@@ -380,9 +378,7 @@ gpui::actions!(
SelectAll,
SelectAllMatches,
SelectToStartOfExcerpt,
SelectToStartOfNextExcerpt,
SelectToEndOfExcerpt,
SelectToEndOfPreviousExcerpt,
SelectDown,
SelectEnclosingSymbol,
SelectLargerSyntaxNode,

View File

@@ -6,11 +6,11 @@ use gpui::{
};
use language::Buffer;
use language::CodeLabel;
use lsp::LanguageServerId;
use markdown::Markdown;
use multi_buffer::{Anchor, ExcerptId};
use ordered_float::OrderedFloat;
use project::lsp_store::CompletionDocumentation;
use project::CompletionSource;
use project::{CodeAction, Completion, TaskSourceKind};
use std::{
@@ -233,9 +233,11 @@ impl CompletionsMenu {
runs: Default::default(),
filter_range: Default::default(),
},
server_id: LanguageServerId(usize::MAX),
documentation: None,
lsp_completion: Default::default(),
confirm: None,
source: CompletionSource::Custom,
resolved: true,
})
.collect();
@@ -498,12 +500,7 @@ impl CompletionsMenu {
// Ignore font weight for syntax highlighting, as we'll use it
// for fuzzy matches.
highlight.font_weight = None;
if completion
.source
.lsp_completion(false)
.and_then(|lsp_completion| lsp_completion.deprecated)
.unwrap_or(false)
{
if completion.lsp_completion.deprecated.unwrap_or(false) {
highlight.strikethrough = Some(StrikethroughStyle {
thickness: 1.0.into(),
..Default::default()
@@ -711,12 +708,7 @@ impl CompletionsMenu {
let completion = &completions[mat.candidate_id];
let sort_key = completion.sort_key();
let sort_text =
if let CompletionSource::Lsp { lsp_completion, .. } = &completion.source {
lsp_completion.sort_text.as_deref()
} else {
None
};
let sort_text = completion.lsp_completion.sort_text.as_deref();
let score = Reverse(OrderedFloat(mat.score));
if mat.score >= 0.2 {

View File

@@ -113,6 +113,7 @@ pub struct DisplayMap {
}
impl DisplayMap {
#[allow(clippy::too_many_arguments)]
pub fn new(
buffer: Entity<MultiBuffer>,
font: Font,

View File

@@ -726,6 +726,7 @@ impl BlockMap {
self.show_excerpt_controls
}
#[allow(clippy::too_many_arguments)]
fn header_and_footer_blocks<'a, R, T>(
show_excerpt_controls: bool,
excerpt_footer_height: u32,

View File

@@ -138,9 +138,8 @@ use multi_buffer::{
use project::{
lsp_store::{CompletionDocumentation, FormatTrigger, LspFormatTarget, OpenLspBufferHandle},
project_settings::{GitGutterSetting, ProjectSettings},
CodeAction, Completion, CompletionIntent, CompletionSource, DocumentHighlight, InlayHint,
Location, LocationLink, PrepareRenameResponse, Project, ProjectItem, ProjectTransaction,
TaskSourceKind,
CodeAction, Completion, CompletionIntent, DocumentHighlight, InlayHint, Location, LocationLink,
PrepareRenameResponse, Project, ProjectItem, ProjectTransaction, TaskSourceKind,
};
use rand::prelude::*;
use rpc::{proto::*, ErrorExt};
@@ -1251,6 +1250,11 @@ impl Editor {
let mut project_subscriptions = Vec::new();
if mode == EditorMode::Full {
if let Some(project) = project.as_ref() {
if buffer.read(cx).is_singleton() {
project_subscriptions.push(cx.observe_in(project, window, |_, _, _, cx| {
cx.emit(EditorEvent::TitleChanged);
}));
}
project_subscriptions.push(cx.subscribe_in(
project,
window,
@@ -1573,16 +1577,13 @@ impl Editor {
}
}
if let Some(singleton_buffer) = self.buffer.read(cx).as_singleton() {
if let Some(extension) = singleton_buffer
.read(cx)
.file()
.and_then(|file| file.path().extension()?.to_str())
{
key_context.set("extension", extension.to_string());
}
} else {
key_context.add("multibuffer");
if let Some(extension) = self
.buffer
.read(cx)
.as_singleton()
.and_then(|buffer| buffer.read(cx).file()?.path().extension()?.to_str())
{
key_context.set("extension", extension.to_string());
}
if has_active_edit_prediction {
@@ -5931,6 +5932,7 @@ impl Editor {
const EDIT_PREDICTION_POPOVER_PADDING_X: Pixels = Pixels(24.);
const EDIT_PREDICTION_POPOVER_PADDING_Y: Pixels = Pixels(2.);
#[allow(clippy::too_many_arguments)]
fn render_edit_prediction_popover(
&mut self,
text_bounds: &Bounds<Pixels>,
@@ -6042,6 +6044,7 @@ impl Editor {
}
}
#[allow(clippy::too_many_arguments)]
fn render_edit_prediction_modifier_jump_popover(
&mut self,
text_bounds: &Bounds<Pixels>,
@@ -6137,6 +6140,7 @@ impl Editor {
Some((element, origin))
}
#[allow(clippy::too_many_arguments)]
fn render_edit_prediction_scroll_popover(
&mut self,
to_y: impl Fn(Size<Pixels>) -> Pixels,
@@ -6167,6 +6171,7 @@ impl Editor {
Some((element, origin))
}
#[allow(clippy::too_many_arguments)]
fn render_edit_prediction_eager_jump_popover(
&mut self,
text_bounds: &Bounds<Pixels>,
@@ -6236,6 +6241,7 @@ impl Editor {
}
}
#[allow(clippy::too_many_arguments)]
fn render_edit_prediction_end_of_line_popover(
self: &mut Editor,
label: &'static str,
@@ -6294,6 +6300,7 @@ impl Editor {
Some((element, origin))
}
#[allow(clippy::too_many_arguments)]
fn render_edit_prediction_diff_popover(
self: &Editor,
text_bounds: &Bounds<Pixels>,
@@ -6601,6 +6608,7 @@ impl Editor {
editor_bg_color.blend(accent_color.opacity(0.6))
}
#[allow(clippy::too_many_arguments)]
fn render_edit_prediction_cursor_popover(
&self,
min_width: Pixels,
@@ -9841,31 +9849,6 @@ impl Editor {
})
}
pub fn move_to_start_of_next_excerpt(
&mut self,
_: &MoveToStartOfNextExcerpt,
window: &mut Window,
cx: &mut Context<Self>,
) {
if matches!(self.mode, EditorMode::SingleLine { .. }) {
cx.propagate();
return;
}
self.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
s.move_with(|map, selection| {
selection.collapse_to(
movement::start_of_excerpt(
map,
selection.head(),
workspace::searchable::Direction::Next,
),
SelectionGoal::None,
)
});
})
}
pub fn move_to_end_of_excerpt(
&mut self,
_: &MoveToEndOfExcerpt,
@@ -9891,31 +9874,6 @@ impl Editor {
})
}
pub fn move_to_end_of_previous_excerpt(
&mut self,
_: &MoveToEndOfPreviousExcerpt,
window: &mut Window,
cx: &mut Context<Self>,
) {
if matches!(self.mode, EditorMode::SingleLine { .. }) {
cx.propagate();
return;
}
self.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
s.move_with(|map, selection| {
selection.collapse_to(
movement::end_of_excerpt(
map,
selection.head(),
workspace::searchable::Direction::Prev,
),
SelectionGoal::None,
)
});
})
}
pub fn select_to_start_of_excerpt(
&mut self,
_: &SelectToStartOfExcerpt,
@@ -9937,27 +9895,6 @@ impl Editor {
})
}
pub fn select_to_start_of_next_excerpt(
&mut self,
_: &SelectToStartOfNextExcerpt,
window: &mut Window,
cx: &mut Context<Self>,
) {
if matches!(self.mode, EditorMode::SingleLine { .. }) {
cx.propagate();
return;
}
self.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
s.move_heads_with(|map, head, _| {
(
movement::start_of_excerpt(map, head, workspace::searchable::Direction::Next),
SelectionGoal::None,
)
});
})
}
pub fn select_to_end_of_excerpt(
&mut self,
_: &SelectToEndOfExcerpt,
@@ -9979,27 +9916,6 @@ impl Editor {
})
}
pub fn select_to_end_of_previous_excerpt(
&mut self,
_: &SelectToEndOfPreviousExcerpt,
window: &mut Window,
cx: &mut Context<Self>,
) {
if matches!(self.mode, EditorMode::SingleLine { .. }) {
cx.propagate();
return;
}
self.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
s.move_heads_with(|map, head, _| {
(
movement::end_of_excerpt(map, head, workspace::searchable::Direction::Prev),
SelectionGoal::None,
)
});
})
}
pub fn move_to_beginning(
&mut self,
_: &MoveToBeginning,
@@ -11632,7 +11548,7 @@ impl Editor {
fn go_to_next_hunk(&mut self, _: &GoToHunk, window: &mut Window, cx: &mut Context<Self>) {
let snapshot = self.snapshot(window, cx);
let selection = self.selections.newest::<Point>(cx);
self.go_to_hunk_before_or_after_position(
self.go_to_hunk_after_or_before_position(
&snapshot,
selection.head(),
Direction::Next,
@@ -11641,7 +11557,7 @@ impl Editor {
);
}
fn go_to_hunk_before_or_after_position(
fn go_to_hunk_after_or_before_position(
&mut self,
snapshot: &EditorSnapshot,
position: Point,
@@ -11692,7 +11608,7 @@ impl Editor {
) {
let snapshot = self.snapshot(window, cx);
let selection = self.selections.newest::<Point>(cx);
self.go_to_hunk_before_or_after_position(
self.go_to_hunk_after_or_before_position(
&snapshot,
selection.head(),
Direction::Prev,
@@ -13854,6 +13770,21 @@ impl Editor {
return;
}
let snapshot = self.snapshot(window, cx);
let newest_range = self.selections.newest::<Point>(cx).range();
let run_twice = snapshot
.hunks_for_ranges([newest_range])
.first()
.is_some_and(|hunk| {
let next_line = Point::new(hunk.row_range.end.0 + 1, 0);
self.hunk_after_position(&snapshot, next_line)
.is_some_and(|other| other.row_range == hunk.row_range)
});
if run_twice {
self.go_to_next_hunk(&GoToHunk, window, cx);
}
self.stage_or_unstage_diff_hunks(stage, ranges, cx);
self.go_to_next_hunk(&GoToHunk, window, cx);
}
@@ -14281,13 +14212,6 @@ impl Editor {
EditorSettings::override_global(editor_settings, cx);
}
pub fn line_numbers_enabled(&self, cx: &App) -> bool {
if let Some(show_line_numbers) = self.show_line_numbers {
return show_line_numbers;
}
EditorSettings::get_global(cx).gutter.line_numbers
}
pub fn should_use_relative_line_numbers(&self, cx: &mut App) -> bool {
self.use_relative_line_numbers
.unwrap_or(EditorSettings::get_global(cx).relative_line_numbers)
@@ -14963,14 +14887,14 @@ impl Editor {
&self,
window: &mut Window,
cx: &mut App,
) -> BTreeMap<DisplayRow, LineHighlight> {
) -> BTreeMap<DisplayRow, Background> {
let snapshot = self.snapshot(window, cx);
let mut used_highlight_orders = HashMap::default();
self.highlighted_rows
.iter()
.flat_map(|(_, highlighted_rows)| highlighted_rows.iter())
.fold(
BTreeMap::<DisplayRow, LineHighlight>::new(),
BTreeMap::<DisplayRow, Background>::new(),
|mut unique_rows, highlight| {
let start = highlight.range.start.to_display_point(&snapshot);
let end = highlight.range.end.to_display_point(&snapshot);
@@ -15518,9 +15442,14 @@ impl Editor {
}
multi_buffer::Event::DirtyChanged => cx.emit(EditorEvent::DirtyChanged),
multi_buffer::Event::Saved => cx.emit(EditorEvent::Saved),
multi_buffer::Event::FileHandleChanged
| multi_buffer::Event::Reloaded
| multi_buffer::Event::BufferDiffChanged => cx.emit(EditorEvent::TitleChanged),
multi_buffer::Event::FileHandleChanged | multi_buffer::Event::Reloaded => {
cx.emit(EditorEvent::TitleChanged)
}
// multi_buffer::Event::DiffBaseChanged => {
// self.scrollbar_marker_state.dirty = true;
// cx.emit(EditorEvent::DiffBaseChanged);
// cx.notify();
// }
multi_buffer::Event::Closed => cx.emit(EditorEvent::Closed),
multi_buffer::Event::DiagnosticsUpdated => {
self.refresh_active_diagnostics(cx);
@@ -16978,41 +16907,38 @@ fn snippet_completions(
Some(Completion {
old_range: range,
new_text: snippet.body.clone(),
source: CompletionSource::Lsp {
server_id: LanguageServerId(usize::MAX),
resolved: true,
lsp_completion: Box::new(lsp::CompletionItem {
label: snippet.prefix.first().unwrap().clone(),
kind: Some(CompletionItemKind::SNIPPET),
label_details: snippet.description.as_ref().map(|description| {
lsp::CompletionItemLabelDetails {
detail: Some(description.clone()),
description: None,
}
}),
insert_text_format: Some(InsertTextFormat::SNIPPET),
text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
lsp::InsertReplaceEdit {
new_text: snippet.body.clone(),
insert: lsp_range,
replace: lsp_range,
},
)),
filter_text: Some(snippet.body.clone()),
sort_text: Some(char::MAX.to_string()),
..lsp::CompletionItem::default()
}),
lsp_defaults: None,
},
resolved: false,
label: CodeLabel {
text: matching_prefix.clone(),
runs: Vec::new(),
runs: vec![],
filter_range: 0..matching_prefix.len(),
},
server_id: LanguageServerId(usize::MAX),
documentation: snippet
.description
.clone()
.map(|description| CompletionDocumentation::SingleLine(description.into())),
lsp_completion: lsp::CompletionItem {
label: snippet.prefix.first().unwrap().clone(),
kind: Some(CompletionItemKind::SNIPPET),
label_details: snippet.description.as_ref().map(|description| {
lsp::CompletionItemLabelDetails {
detail: Some(description.clone()),
description: None,
}
}),
insert_text_format: Some(InsertTextFormat::SNIPPET),
text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
lsp::InsertReplaceEdit {
new_text: snippet.body.clone(),
insert: lsp_range,
replace: lsp_range,
},
)),
filter_text: Some(snippet.body.clone()),
sort_text: Some(char::MAX.to_string()),
..Default::default()
},
confirm: None,
})
})
@@ -18510,27 +18436,3 @@ impl Render for MissingEditPredictionKeybindingTooltip {
})
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
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,
}
}
}

View File

@@ -12334,6 +12334,24 @@ async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext)
},
};
let item_0_out = lsp::CompletionItem {
commit_characters: Some(default_commit_characters.clone()),
insert_text_format: Some(default_insert_text_format),
..item_0
};
let items_out = iter::once(item_0_out)
.chain(items[1..].iter().map(|item| lsp::CompletionItem {
commit_characters: Some(default_commit_characters.clone()),
data: Some(default_data.clone()),
insert_text_mode: Some(default_insert_text_mode),
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
range: default_edit_range,
new_text: item.label.clone(),
})),
..item.clone()
}))
.collect::<Vec<lsp::CompletionItem>>();
let mut cx = EditorLspTestContext::new_rust(
lsp::ServerCapabilities {
completion_provider: Some(lsp::CompletionOptions {
@@ -12352,11 +12370,10 @@ async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext)
let completion_data = default_data.clone();
let completion_characters = default_commit_characters.clone();
let completion_items = items.clone();
cx.handle_request::<lsp::request::Completion, _, _>(move |_, _, _| {
let default_data = completion_data.clone();
let default_commit_characters = completion_characters.clone();
let items = completion_items.clone();
let items = items.clone();
async move {
Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
items,
@@ -12405,7 +12422,7 @@ async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext)
.iter()
.map(|mat| mat.string.clone())
.collect::<Vec<String>>(),
items
items_out
.iter()
.map(|completion| completion.label.clone())
.collect::<Vec<String>>()
@@ -12418,18 +12435,14 @@ async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext)
// with 4 from the end.
assert_eq!(
*resolved_items.lock(),
[&items[0..16], &items[items.len() - 4..items.len()]]
.concat()
.iter()
.cloned()
.map(|mut item| {
if item.data.is_none() {
item.data = Some(default_data.clone());
}
item
})
.collect::<Vec<lsp::CompletionItem>>(),
"Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
[
&items_out[0..16],
&items_out[items_out.len() - 4..items_out.len()]
]
.concat()
.iter()
.cloned()
.collect::<Vec<lsp::CompletionItem>>()
);
resolved_items.lock().clear();
@@ -12440,15 +12453,9 @@ async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext)
// Completions that have already been resolved are skipped.
assert_eq!(
*resolved_items.lock(),
items[items.len() - 16..items.len() - 4]
items_out[items_out.len() - 16..items_out.len() - 4]
.iter()
.cloned()
.map(|mut item| {
if item.data.is_none() {
item.data = Some(default_data.clone());
}
item
})
.collect::<Vec<lsp::CompletionItem>>()
);
resolved_items.lock().clear();
@@ -16406,199 +16413,6 @@ async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut Test
);
}
#[gpui::test]
async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
init_test(cx, |_| {});
cx.update(|cx| {
let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
"keymaps/default-linux.json",
cx,
)
.unwrap();
cx.bind_keys(default_key_bindings);
});
let (editor, cx) = cx.add_window_view(|window, cx| {
let multi_buffer = MultiBuffer::build_multi(
[
("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
],
cx,
);
let mut editor = Editor::new(
EditorMode::Full,
multi_buffer.clone(),
None,
true,
window,
cx,
);
let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
// fold all but the second buffer, so that we test navigating between two
// adjacent folded buffers, as well as folded buffers at the start and
// end the multibuffer
editor.fold_buffer(buffer_ids[0], cx);
editor.fold_buffer(buffer_ids[2], cx);
editor.fold_buffer(buffer_ids[3], cx);
editor
});
cx.simulate_resize(size(px(1000.), px(1000.)));
let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
cx.assert_excerpts_with_selections(indoc! {"
[EXCERPT]
ˇ[FOLDED]
[EXCERPT]
a1
b1
[EXCERPT]
[FOLDED]
[EXCERPT]
[FOLDED]
"
});
cx.simulate_keystroke("down");
cx.assert_excerpts_with_selections(indoc! {"
[EXCERPT]
[FOLDED]
[EXCERPT]
ˇa1
b1
[EXCERPT]
[FOLDED]
[EXCERPT]
[FOLDED]
"
});
cx.simulate_keystroke("down");
cx.assert_excerpts_with_selections(indoc! {"
[EXCERPT]
[FOLDED]
[EXCERPT]
a1
ˇb1
[EXCERPT]
[FOLDED]
[EXCERPT]
[FOLDED]
"
});
cx.simulate_keystroke("down");
cx.assert_excerpts_with_selections(indoc! {"
[EXCERPT]
[FOLDED]
[EXCERPT]
a1
b1
ˇ[EXCERPT]
[FOLDED]
[EXCERPT]
[FOLDED]
"
});
cx.simulate_keystroke("down");
cx.assert_excerpts_with_selections(indoc! {"
[EXCERPT]
[FOLDED]
[EXCERPT]
a1
b1
[EXCERPT]
ˇ[FOLDED]
[EXCERPT]
[FOLDED]
"
});
for _ in 0..5 {
cx.simulate_keystroke("down");
cx.assert_excerpts_with_selections(indoc! {"
[EXCERPT]
[FOLDED]
[EXCERPT]
a1
b1
[EXCERPT]
[FOLDED]
[EXCERPT]
ˇ[FOLDED]
"
});
}
cx.simulate_keystroke("up");
cx.assert_excerpts_with_selections(indoc! {"
[EXCERPT]
[FOLDED]
[EXCERPT]
a1
b1
[EXCERPT]
ˇ[FOLDED]
[EXCERPT]
[FOLDED]
"
});
cx.simulate_keystroke("up");
cx.assert_excerpts_with_selections(indoc! {"
[EXCERPT]
[FOLDED]
[EXCERPT]
a1
b1
ˇ[EXCERPT]
[FOLDED]
[EXCERPT]
[FOLDED]
"
});
cx.simulate_keystroke("up");
cx.assert_excerpts_with_selections(indoc! {"
[EXCERPT]
[FOLDED]
[EXCERPT]
a1
ˇb1
[EXCERPT]
[FOLDED]
[EXCERPT]
[FOLDED]
"
});
cx.simulate_keystroke("up");
cx.assert_excerpts_with_selections(indoc! {"
[EXCERPT]
[FOLDED]
[EXCERPT]
ˇa1
b1
[EXCERPT]
[FOLDED]
[EXCERPT]
[FOLDED]
"
});
for _ in 0..5 {
cx.simulate_keystroke("up");
cx.assert_excerpts_with_selections(indoc! {"
[EXCERPT]
ˇ[FOLDED]
[EXCERPT]
a1
b1
[EXCERPT]
[FOLDED]
[EXCERPT]
[FOLDED]
"
});
}
}
#[gpui::test]
async fn test_inline_completion_text(cx: &mut TestAppContext) {
init_test(cx, |_| {});

View File

@@ -20,10 +20,10 @@ use crate::{
DisplayRow, DocumentHighlightRead, DocumentHighlightWrite, EditDisplayMode, Editor, EditorMode,
EditorSettings, EditorSnapshot, EditorStyle, ExpandExcerpts, FocusedBlock, GoToHunk,
GoToPreviousHunk, GutterDimensions, HalfPageDown, HalfPageUp, HandleInput, HoveredCursor,
InlayHintRefreshReason, InlineCompletion, JumpData, LineDown, LineHighlight, LineUp,
OpenExcerpts, PageDown, PageUp, Point, RowExt, RowRangeExt, SelectPhase, SelectedTextHighlight,
Selection, SoftWrap, StickyHeaderExcerpt, ToPoint, ToggleFold, COLUMNAR_SELECTION_MODIFIERS,
CURSORS_VISIBLE_FOR, FILE_HEADER_HEIGHT, GIT_BLAME_MAX_AUTHOR_CHARS_DISPLAYED, MAX_LINE_LEN,
InlayHintRefreshReason, InlineCompletion, JumpData, LineDown, LineUp, OpenExcerpts, PageDown,
PageUp, Point, RowExt, RowRangeExt, SelectPhase, SelectedTextHighlight, Selection, SoftWrap,
StickyHeaderExcerpt, ToPoint, ToggleFold, COLUMNAR_SELECTION_MODIFIERS, CURSORS_VISIBLE_FOR,
FILE_HEADER_HEIGHT, GIT_BLAME_MAX_AUTHOR_CHARS_DISPLAYED, MAX_LINE_LEN,
MULTI_BUFFER_EXCERPT_HEADER_HEIGHT,
};
use buffer_diff::{DiffHunkStatus, DiffHunkStatusKind};
@@ -282,9 +282,7 @@ impl EditorElement {
register_action(editor, window, Editor::move_to_beginning);
register_action(editor, window, Editor::move_to_end);
register_action(editor, window, Editor::move_to_start_of_excerpt);
register_action(editor, window, Editor::move_to_start_of_next_excerpt);
register_action(editor, window, Editor::move_to_end_of_excerpt);
register_action(editor, window, Editor::move_to_end_of_previous_excerpt);
register_action(editor, window, Editor::select_up);
register_action(editor, window, Editor::select_down);
register_action(editor, window, Editor::select_left);
@@ -298,9 +296,7 @@ impl EditorElement {
register_action(editor, window, Editor::select_to_start_of_paragraph);
register_action(editor, window, Editor::select_to_end_of_paragraph);
register_action(editor, window, Editor::select_to_start_of_excerpt);
register_action(editor, window, Editor::select_to_start_of_next_excerpt);
register_action(editor, window, Editor::select_to_end_of_excerpt);
register_action(editor, window, Editor::select_to_end_of_previous_excerpt);
register_action(editor, window, Editor::select_to_beginning);
register_action(editor, window, Editor::select_to_end);
register_action(editor, window, Editor::select_all);
@@ -958,6 +954,7 @@ impl EditorElement {
cx.notify()
}
#[allow(clippy::too_many_arguments)]
fn layout_selections(
&self,
start_anchor: Anchor,
@@ -1129,6 +1126,7 @@ impl EditorElement {
cursors
}
#[allow(clippy::too_many_arguments)]
fn layout_visible_cursors(
&self,
snapshot: &EditorSnapshot,
@@ -1482,6 +1480,7 @@ impl EditorElement {
axis_pair(horizontal_scrollbar, vertical_scrollbar)
}
#[allow(clippy::too_many_arguments)]
fn prepaint_crease_toggles(
&self,
crease_toggles: &mut [Option<AnyElement>],
@@ -1516,6 +1515,7 @@ impl EditorElement {
}
}
#[allow(clippy::too_many_arguments)]
fn prepaint_crease_trailers(
&self,
trailers: Vec<Option<AnyElement>>,
@@ -1592,6 +1592,7 @@ impl EditorElement {
display_hunks
}
#[allow(clippy::too_many_arguments)]
fn layout_inline_diagnostics(
&self,
line_layouts: &[LineWithInvisibles],
@@ -1690,7 +1691,7 @@ impl EditorElement {
let pos_y = content_origin.y
+ line_height * (row.0 as f32 - scroll_pixel_position.y / line_height);
let window_ix = row.0.saturating_sub(start_row.0) as usize;
let window_ix = row.minus(start_row) as usize;
let pos_x = {
let crease_trailer_layout = &crease_trailers[window_ix];
let line_layout = &line_layouts[window_ix];
@@ -1742,6 +1743,7 @@ impl EditorElement {
elements
}
#[allow(clippy::too_many_arguments)]
fn layout_inline_blame(
&self,
display_row: DisplayRow,
@@ -1821,6 +1823,7 @@ impl EditorElement {
Some(element)
}
#[allow(clippy::too_many_arguments)]
fn layout_blame_entries(
&self,
buffer_rows: &[RowInfo],
@@ -1889,6 +1892,7 @@ impl EditorElement {
Some(shaped_lines)
}
#[allow(clippy::too_many_arguments)]
fn layout_indent_guides(
&self,
content_origin: gpui::Point<Pixels>,
@@ -2006,6 +2010,7 @@ impl EditorElement {
(offset_y, length)
}
#[allow(clippy::too_many_arguments)]
fn layout_run_indicators(
&self,
line_height: Pixels,
@@ -2099,6 +2104,7 @@ impl EditorElement {
})
}
#[allow(clippy::too_many_arguments)]
fn layout_code_actions_indicator(
&self,
line_height: Pixels,
@@ -2197,6 +2203,7 @@ impl EditorElement {
relative_rows
}
#[allow(clippy::too_many_arguments)]
fn layout_line_numbers(
&self,
gutter_hitbox: Option<&Hitbox>,
@@ -2412,6 +2419,7 @@ impl EditorElement {
}
}
#[allow(clippy::too_many_arguments)]
fn prepaint_lines(
&self,
start_row: DisplayRow,
@@ -2438,6 +2446,7 @@ impl EditorElement {
line_elements
}
#[allow(clippy::too_many_arguments)]
fn render_block(
&self,
block: &Block,
@@ -2937,6 +2946,7 @@ impl EditorElement {
}))
}
#[allow(clippy::too_many_arguments)]
fn render_blocks(
&self,
rows: Range<DisplayRow>,
@@ -3121,6 +3131,7 @@ impl EditorElement {
/// Returns true if any of the blocks changed size since the previous frame. This will trigger
/// a restart of rendering for the editor based on the new sizes.
#[allow(clippy::too_many_arguments)]
fn layout_blocks(
&self,
blocks: &mut Vec<BlockLayout>,
@@ -3164,6 +3175,7 @@ impl EditorElement {
}
}
#[allow(clippy::too_many_arguments)]
fn layout_sticky_buffer_header(
&self,
StickyHeaderExcerpt {
@@ -3238,6 +3250,7 @@ impl EditorElement {
header
}
#[allow(clippy::too_many_arguments)]
fn layout_cursor_popovers(
&self,
line_height: Pixels,
@@ -3426,6 +3439,7 @@ impl EditorElement {
);
}
#[allow(clippy::too_many_arguments)]
fn layout_gutter_menu(
&self,
line_height: Pixels,
@@ -3478,6 +3492,7 @@ impl EditorElement {
);
}
#[allow(clippy::too_many_arguments)]
fn layout_popovers_above_or_below_line(
&self,
target_position: gpui::Point<Pixels>,
@@ -3591,6 +3606,7 @@ impl EditorElement {
})
}
#[allow(clippy::too_many_arguments)]
fn layout_context_menu_aside(
&self,
y_flipped: bool,
@@ -3786,6 +3802,7 @@ impl EditorElement {
})
}
#[allow(clippy::too_many_arguments)]
fn layout_hover_popovers(
&self,
snapshot: &EditorSnapshot,
@@ -3902,6 +3919,7 @@ impl EditorElement {
}
}
#[allow(clippy::too_many_arguments)]
fn layout_diff_hunk_controls(
&self,
row_range: Range<DisplayRow>,
@@ -3986,6 +4004,7 @@ impl EditorElement {
controls
}
#[allow(clippy::too_many_arguments)]
fn layout_signature_help(
&self,
hitbox: &Hitbox,
@@ -4113,74 +4132,46 @@ impl EditorElement {
}
}
let mut paint_highlight = |highlight_row_start: DisplayRow,
highlight_row_end: DisplayRow,
highlight: crate::LineHighlight,
edges| {
let origin = point(
layout.hitbox.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,
layout.position_map.line_height
* highlight_row_end.next_row().minus(highlight_row_start) as f32,
);
let mut quad = fill(Bounds { origin, size }, highlight.background);
if let Some(border_color) = highlight.border {
quad.border_color = border_color;
quad.border_widths = edges
}
window.paint_quad(quad);
};
let mut paint_highlight =
|highlight_row_start: DisplayRow, highlight_row_end: DisplayRow, color| {
let origin = point(
layout.hitbox.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,
layout.position_map.line_height
* highlight_row_end.next_row().minus(highlight_row_start) as f32,
);
window.paint_quad(fill(Bounds { origin, size }, color));
};
let mut current_paint: Option<(LineHighlight, Range<DisplayRow>, Edges<Pixels>)> =
None;
let mut current_paint: Option<(gpui::Background, Range<DisplayRow>)> = None;
for (&new_row, &new_background) in &layout.highlighted_rows {
match &mut current_paint {
Some((current_background, current_range, mut edges)) => {
Some((current_background, current_range)) => {
let current_background = *current_background;
let new_range_started = current_background != new_background
|| current_range.end.next_row() != new_row;
if new_range_started {
if current_range.end.next_row() == new_row {
edges.bottom = px(0.);
};
paint_highlight(
current_range.start,
current_range.end,
current_background,
edges,
);
let edges = Edges {
top: if current_range.end.next_row() != new_row {
px(1.)
} else {
px(0.)
},
bottom: px(1.),
..Default::default()
};
current_paint = Some((new_background, new_row..new_row, edges));
current_paint = Some((new_background, new_row..new_row));
continue;
} else {
current_range.end = current_range.end.next_row();
}
}
None => {
let edges = Edges {
top: px(1.),
bottom: px(1.),
..Default::default()
};
current_paint = Some((new_background, new_row..new_row, edges))
}
None => current_paint = Some((new_background, new_row..new_row)),
};
}
if let Some((color, range, edges)) = current_paint {
paint_highlight(range.start, range.end, color, edges);
if let Some((color, range)) = current_paint {
paint_highlight(range.start, range.end, color);
}
let scroll_left =
@@ -4358,11 +4349,6 @@ impl EditorElement {
fn paint_gutter_diff_hunks(layout: &mut EditorLayout, window: &mut Window, cx: &mut App) {
let is_light = cx.theme().appearance().is_light();
let hunk_style = ProjectSettings::get_global(cx)
.git
.hunk_style
.unwrap_or_default();
if layout.display_hunks.is_empty() {
return;
}
@@ -4426,23 +4412,9 @@ impl EditorElement {
if let Some((hunk_bounds, mut background_color, corner_radii, secondary_status)) =
hunk_to_paint
{
match hunk_style {
GitHunkStyleSetting::Transparent | GitHunkStyleSetting::Pattern => {
if secondary_status.has_secondary_hunk() {
background_color =
background_color.opacity(if is_light { 0.2 } else { 0.32 });
}
}
GitHunkStyleSetting::StagedPattern
| GitHunkStyleSetting::StagedTransparent => {
if !secondary_status.has_secondary_hunk() {
background_color =
background_color.opacity(if is_light { 0.2 } else { 0.32 });
}
}
GitHunkStyleSetting::StagedBorder | GitHunkStyleSetting::Border => {
// Don't change the background color
}
if secondary_status.has_secondary_hunk() {
background_color =
background_color.opacity(if is_light { 0.2 } else { 0.32 });
}
// Flatten the background color with the editor color to prevent
@@ -4653,7 +4625,6 @@ impl EditorElement {
};
window.set_cursor_style(cursor_style, &layout.position_map.text_hitbox);
self.paint_lines_background(layout, window, cx);
let invisible_display_ranges = self.paint_highlights(layout, window);
self.paint_lines(&invisible_display_ranges, layout, window, cx);
self.paint_redactions(layout, window);
@@ -4744,18 +4715,6 @@ impl EditorElement {
}
}
fn paint_lines_background(
&mut self,
layout: &mut EditorLayout,
window: &mut Window,
cx: &mut App,
) {
for (ix, line_with_invisibles) in layout.position_map.line_layouts.iter().enumerate() {
let row = DisplayRow(layout.visible_display_row_range.start.0 + ix as u32);
line_with_invisibles.draw_background(layout, row, layout.content_origin, window, cx);
}
}
fn paint_redactions(&mut self, layout: &EditorLayout, window: &mut Window) {
if layout.redacted_ranges.is_empty() {
return;
@@ -5294,6 +5253,7 @@ impl EditorElement {
});
}
#[allow(clippy::too_many_arguments)]
fn paint_highlighted_range(
&self,
range: Range<DisplayPoint>,
@@ -5719,6 +5679,7 @@ impl AcceptEditPredictionBinding {
}
}
#[allow(clippy::too_many_arguments)]
fn prepaint_gutter_button(
button: IconButton,
row: DisplayRow,
@@ -5969,6 +5930,7 @@ impl fmt::Debug for LineFragment {
}
impl LineWithInvisibles {
#[allow(clippy::too_many_arguments)]
fn from_chunks<'a>(
chunks: impl Iterator<Item = HighlightedChunk<'a>>,
editor_style: &EditorStyle,
@@ -6173,6 +6135,7 @@ impl LineWithInvisibles {
layouts
}
#[allow(clippy::too_many_arguments)]
fn prepaint(
&mut self,
line_height: Pixels,
@@ -6207,6 +6170,7 @@ impl LineWithInvisibles {
}
}
#[allow(clippy::too_many_arguments)]
fn draw(
&self,
layout: &EditorLayout,
@@ -6250,35 +6214,7 @@ impl LineWithInvisibles {
);
}
fn draw_background(
&self,
layout: &EditorLayout,
row: DisplayRow,
content_origin: gpui::Point<Pixels>,
window: &mut Window,
cx: &mut App,
) {
let line_height = layout.position_map.line_height;
let line_y = line_height
* (row.as_f32() - layout.position_map.scroll_pixel_position.y / line_height);
let mut fragment_origin =
content_origin + gpui::point(-layout.position_map.scroll_pixel_position.x, line_y);
for fragment in &self.fragments {
match fragment {
LineFragment::Text(line) => {
line.paint_background(fragment_origin, line_height, window, cx)
.log_err();
fragment_origin.x += line.width;
}
LineFragment::Element { size, .. } => {
fragment_origin.x += size.width;
}
}
}
}
#[allow(clippy::too_many_arguments)]
fn draw_invisibles(
&self,
selection_ranges: &[Range<DisplayPoint>],
@@ -6798,10 +6734,10 @@ impl Element for EditorElement {
.update(cx, |editor, cx| editor.highlighted_display_rows(window, cx));
let is_light = cx.theme().appearance().is_light();
let hunk_style = ProjectSettings::get_global(cx)
let use_pattern = ProjectSettings::get_global(cx)
.git
.hunk_style
.unwrap_or_default();
.map_or(false, |style| matches!(style, GitHunkStyleSetting::Pattern));
for (ix, row_info) in row_infos.iter().enumerate() {
let Some(diff_status) = row_info.diff_status else {
@@ -6821,74 +6757,26 @@ impl Element for EditorElement {
let unstaged = diff_status.has_secondary_hunk();
let hunk_opacity = if is_light { 0.16 } else { 0.12 };
let slash_width = line_height.0 / 1.5; // ~16 by default
let staged_highlight: LineHighlight = match hunk_style {
GitHunkStyleSetting::Transparent
| GitHunkStyleSetting::Pattern
| GitHunkStyleSetting::Border => {
solid_background(background_color.opacity(hunk_opacity)).into()
}
GitHunkStyleSetting::StagedPattern => {
pattern_slash(background_color.opacity(hunk_opacity), slash_width)
.into()
}
GitHunkStyleSetting::StagedTransparent => {
solid_background(background_color.opacity(if is_light {
0.08
} else {
0.04
}))
.into()
}
GitHunkStyleSetting::StagedBorder => LineHighlight {
background: (background_color.opacity(if is_light {
0.08
} else {
0.06
}))
.into(),
border: Some(if is_light {
background_color.opacity(0.48)
} else {
background_color.opacity(0.36)
}),
},
};
let unstaged_highlight = match hunk_style {
GitHunkStyleSetting::Transparent => {
solid_background(background_color.opacity(if is_light {
0.08
} else {
0.04
}))
.into()
}
GitHunkStyleSetting::Pattern => {
pattern_slash(background_color.opacity(hunk_opacity), slash_width)
.into()
}
GitHunkStyleSetting::Border => LineHighlight {
background: (background_color.opacity(if is_light {
0.08
} else {
0.02
}))
.into(),
border: Some(background_color.opacity(0.5)),
},
GitHunkStyleSetting::StagedPattern
| GitHunkStyleSetting::StagedTransparent
| GitHunkStyleSetting::StagedBorder => {
solid_background(background_color.opacity(hunk_opacity)).into()
}
let staged_background =
solid_background(background_color.opacity(hunk_opacity));
let unstaged_background = if use_pattern {
pattern_slash(
background_color.opacity(hunk_opacity),
window.rem_size().0 * 1.125, // ~18 by default
)
} else {
solid_background(background_color.opacity(if is_light {
0.08
} else {
0.04
}))
};
let background = if unstaged {
unstaged_highlight
unstaged_background
} else {
staged_highlight
staged_background
};
highlighted_rows
@@ -7672,6 +7560,7 @@ struct ScrollbarRangeData {
}
impl ScrollbarRangeData {
#[allow(clippy::too_many_arguments)]
pub fn new(
scrollbar_bounds: Bounds<Pixels>,
letter_size: Size<Pixels>,
@@ -7736,7 +7625,7 @@ pub struct EditorLayout {
indent_guides: Option<Vec<IndentGuideLayout>>,
visible_display_row_range: Range<DisplayRow>,
active_rows: BTreeMap<DisplayRow, bool>,
highlighted_rows: BTreeMap<DisplayRow, LineHighlight>,
highlighted_rows: BTreeMap<DisplayRow, gpui::Background>,
line_elements: SmallVec<[AnyElement; 1]>,
line_numbers: Arc<HashMap<MultiBufferRow, LineNumberLayout>>,
display_hunks: Vec<(DisplayDiffHunk, Option<Hitbox>)>,
@@ -8908,16 +8797,14 @@ fn diff_hunk_controls(
.h(line_height)
.mr_1()
.gap_1()
.px_0p5()
.px_1()
.pb_1()
.border_x_1()
.border_b_1()
.border_color(cx.theme().colors().border_variant)
.rounded_b_lg()
.bg(cx.theme().colors().editor_background)
.gap_1()
.occlude()
.shadow_md()
.child(if status.has_secondary_hunk() {
Button::new(("stage", row as u64), "Stage")
.alpha(if status.is_pending() { 0.66 } else { 1.0 })
@@ -8974,7 +8861,7 @@ fn diff_hunk_controls(
})
})
.child(
Button::new("restore", "Restore")
Button::new("discard", "Restore")
.tooltip({
let focus_handle = editor.focus_handle(cx);
move |window, cx| {
@@ -9026,7 +8913,7 @@ fn diff_hunk_controls(
let snapshot = editor.snapshot(window, cx);
let position =
hunk_range.end.to_point(&snapshot.buffer_snapshot);
editor.go_to_hunk_before_or_after_position(
editor.go_to_hunk_after_or_before_position(
&snapshot,
position,
Direction::Next,
@@ -9062,7 +8949,7 @@ fn diff_hunk_controls(
let snapshot = editor.snapshot(window, cx);
let point =
hunk_range.start.to_point(&snapshot.buffer_snapshot);
editor.go_to_hunk_before_or_after_position(
editor.go_to_hunk_after_or_before_position(
&snapshot,
point,
Direction::Prev,

View File

@@ -370,6 +370,7 @@ impl GitBlame {
async move {
let Some(Blame {
entries,
permalinks,
messages,
remote_url,
}) = blame.await?
@@ -378,8 +379,13 @@ impl GitBlame {
};
let entries = build_blame_entry_sum_tree(entries, snapshot.max_point().row);
let commit_details =
parse_commit_messages(messages, remote_url, provider_registry).await;
let commit_details = parse_commit_messages(
messages,
remote_url,
&permalinks,
provider_registry,
)
.await;
anyhow::Ok(Some((entries, commit_details)))
}
@@ -471,6 +477,7 @@ fn build_blame_entry_sum_tree(entries: Vec<BlameEntry>, max_row: u32) -> SumTree
async fn parse_commit_messages(
messages: impl IntoIterator<Item = (Oid, String)>,
remote_url: Option<String>,
deprecated_permalinks: &HashMap<Oid, Url>,
provider_registry: Arc<GitHostingProviderRegistry>,
) -> HashMap<Oid, ParsedCommitMessage> {
let mut commit_details = HashMap::default();
@@ -488,7 +495,11 @@ async fn parse_commit_messages(
},
))
} else {
continue;
// DEPRECATED (18 Apr 24): Sending permalinks over the wire is deprecated. Clients
// now do the parsing. This is here for backwards compatibility, so that
// when an old peer sends a client no `parsed_remote_url` but `deprecated_permalinks`,
// we fall back to that.
deprecated_permalinks.get(&oid).cloned()
};
let remote = parsed_remote_url

View File

@@ -448,9 +448,7 @@ pub fn end_of_excerpt(
if start.row() > DisplayRow(0) {
*start.row_mut() -= 1;
}
start = map.clip_point(start, Bias::Left);
*start.column_mut() = 0;
start
map.clip_point(start, Bias::Left)
}
Direction::Next => {
let mut end = excerpt.end_anchor().to_display_point(&map);

View File

@@ -223,6 +223,7 @@ impl ScrollManager {
self.anchor.scroll_position(snapshot)
}
#[allow(clippy::too_many_arguments)]
fn set_scroll_position(
&mut self,
scroll_position: gpui::Point<f32>,
@@ -297,6 +298,7 @@ impl ScrollManager {
);
}
#[allow(clippy::too_many_arguments)]
fn set_anchor(
&mut self,
anchor: ScrollAnchor,

View File

@@ -429,14 +429,12 @@ impl EditorTestContext {
if expected_selections.len() > 0 {
assert!(
is_selected,
"excerpt {ix} should be selected. got {:?}",
self.editor_state(),
"excerpt {} should be selected. Got {:?}",
ix,
self.editor_state()
);
} else {
assert!(
!is_selected,
"excerpt {ix} should not be selected, got: {selections:?}",
);
assert!(!is_selected, "excerpt {} should not be selected", ix);
}
continue;
}

View File

@@ -22,6 +22,7 @@ collections.workspace = true
env_logger.workspace = true
feature_flags.workspace = true
fs.workspace = true
git.workspace = true
gpui.workspace = true
http_client.workspace = true
language.workspace = true

View File

@@ -5,6 +5,7 @@ use client::{Client, UserStore};
use clock::RealSystemClock;
use collections::BTreeMap;
use feature_flags::FeatureFlagAppExt as _;
use git::GitHostingProviderRegistry;
use gpui::{AppContext as _, AsyncApp, BackgroundExecutor, Entity};
use http_client::{HttpClient, Method};
use language::LanguageRegistry;
@@ -273,7 +274,8 @@ async fn run_evaluation(
let repos_dir = Path::new(EVAL_REPOS_DIR);
let db_path = Path::new(EVAL_DB_PATH);
let api_key = std::env::var("OPENAI_API_KEY").unwrap();
let fs = Arc::new(RealFs::new(None)) as Arc<dyn Fs>;
let git_hosting_provider_registry = Arc::new(GitHostingProviderRegistry::new());
let fs = Arc::new(RealFs::new(git_hosting_provider_registry, None)) as Arc<dyn Fs>;
let clock = Arc::new(RealSystemClock);
let client = cx
.update(|cx| {
@@ -397,6 +399,7 @@ async fn run_evaluation(
}
}
#[allow(clippy::too_many_arguments)]
async fn run_eval_project(
evaluation_project: EvaluationProject,
user_store: &Entity<UserStore>,

View File

@@ -17,7 +17,6 @@ async-compression.workspace = true
async-tar.workspace = true
async-trait.workspace = true
collections.workspace = true
convert_case.workspace = true
fs.workspace = true
futures.workspace = true
gpui.workspace = true

View File

@@ -4,7 +4,6 @@ use crate::{
use anyhow::{anyhow, bail, Context as _, Result};
use async_compression::futures::bufread::GzipDecoder;
use async_tar::Archive;
use convert_case::{Case, Casing as _};
use futures::io::BufReader;
use futures::AsyncReadExt;
use http_client::{self, AsyncBody, HttpClient};
@@ -98,11 +97,6 @@ impl ExtensionBuilder {
}
for (grammar_name, grammar_metadata) in &extension_manifest.grammars {
let snake_cased_grammar_name = grammar_name.to_case(Case::Snake);
if grammar_name.as_ref() != snake_cased_grammar_name.as_str() {
bail!("grammar name '{grammar_name}' must be written in snake_case: {snake_cased_grammar_name}");
}
log::info!(
"compiling grammar {grammar_name} for extension {}",
extension_dir.display()

View File

@@ -195,6 +195,7 @@ static mut EXTENSION: Option<Box<dyn Extension>> = None;
pub static ZED_API_VERSION: [u8; 6] = *include_bytes!(concat!(env!("OUT_DIR"), "/version_bytes"));
mod wit {
#![allow(clippy::too_many_arguments, clippy::missing_safety_doc)]
wit_bindgen::generate!({
skip: ["init-extension"],

View File

@@ -218,6 +218,7 @@ impl ExtensionStore {
cx.global::<GlobalExtensionStore>().0.clone()
}
#[allow(clippy::too_many_arguments)]
pub fn new(
extensions_dir: PathBuf,
build_dir: Option<PathBuf>,

View File

@@ -80,6 +80,11 @@ impl FeatureFlag for PredictEditsNonEagerModeFeatureFlag {
}
}
pub struct GitUiFeatureFlag;
impl FeatureFlag for GitUiFeatureFlag {
const NAME: &'static str = "git-ui";
}
pub struct Remoting {}
impl FeatureFlag for Remoting {
const NAME: &'static str = "remoting";

View File

@@ -653,6 +653,7 @@ impl FileSearchQuery {
}
impl FileFinderDelegate {
#[allow(clippy::too_many_arguments)]
fn new(
file_finder: WeakEntity<FileFinder>,
workspace: WeakEntity<Workspace>,

View File

@@ -436,8 +436,8 @@ impl PickerDelegate for NewPathDelegate {
)
}
fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> Option<SharedString> {
Some("Type a path...".into())
fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> SharedString {
"Type a path...".into()
}
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {

View File

@@ -347,14 +347,12 @@ impl PickerDelegate for OpenPathDelegate {
)
}
fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> Option<SharedString> {
let text = if let Some(error) = self.directory_state.as_ref().and_then(|s| s.error.clone())
{
fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> SharedString {
if let Some(error) = self.directory_state.as_ref().and_then(|s| s.error.clone()) {
error
} else {
"No such file or directory".into()
};
Some(text)
}
}
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {

View File

@@ -11,6 +11,7 @@ use collections::HashMap;
use git::status::StatusCode;
#[cfg(any(test, feature = "test-support"))]
use git::status::TrackedStatus;
use git::GitHostingProviderRegistry;
#[cfg(any(test, feature = "test-support"))]
use git::{repository::RepoPath, status::FileStatus};
@@ -246,6 +247,7 @@ impl From<MTime> for proto::Timestamp {
#[derive(Default)]
pub struct RealFs {
git_hosting_provider_registry: Arc<GitHostingProviderRegistry>,
git_binary_path: Option<PathBuf>,
}
@@ -298,8 +300,14 @@ impl FileHandle for std::fs::File {
pub struct RealWatcher {}
impl RealFs {
pub fn new(git_binary_path: Option<PathBuf>) -> Self {
Self { git_binary_path }
pub fn new(
git_hosting_provider_registry: Arc<GitHostingProviderRegistry>,
git_binary_path: Option<PathBuf>,
) -> Self {
Self {
git_hosting_provider_registry,
git_binary_path,
}
}
}
@@ -762,6 +770,7 @@ impl Fs for RealFs {
Some(Arc::new(RealGitRepository::new(
repo,
self.git_binary_path.clone(),
self.git_hosting_provider_registry.clone(),
)))
}

View File

@@ -170,6 +170,7 @@ impl<'a> Matcher<'a> {
score
}
#[allow(clippy::too_many_arguments)]
fn recursive_score_match(
&mut self,
path: &[char],

View File

@@ -1,15 +1,17 @@
use crate::commit::get_messages;
use crate::Oid;
use crate::{parse_git_remote_url, BuildCommitPermalinkParams, GitHostingProviderRegistry, Oid};
use anyhow::{anyhow, Context as _, Result};
use collections::{HashMap, HashSet};
use serde::{Deserialize, Serialize};
use std::io::Write;
use std::process::Stdio;
use std::sync::Arc;
use std::{ops::Range, path::Path};
use text::Rope;
use time::macros::format_description;
use time::OffsetDateTime;
use time::UtcOffset;
use url::Url;
pub use git2 as libgit;
@@ -17,6 +19,7 @@ pub use git2 as libgit;
pub struct Blame {
pub entries: Vec<BlameEntry>,
pub messages: HashMap<Oid, String>,
pub permalinks: HashMap<Oid, Url>,
pub remote_url: Option<String>,
}
@@ -27,15 +30,32 @@ impl Blame {
path: &Path,
content: &Rope,
remote_url: Option<String>,
provider_registry: Arc<GitHostingProviderRegistry>,
) -> Result<Self> {
let output = run_git_blame(git_binary, working_directory, path, content)?;
let mut entries = parse_git_blame(&output)?;
entries.sort_unstable_by(|a, b| a.range.start.cmp(&b.range.start));
let mut permalinks = HashMap::default();
let mut unique_shas = HashSet::default();
let parsed_remote_url = remote_url
.as_deref()
.and_then(|remote_url| parse_git_remote_url(provider_registry, remote_url));
for entry in entries.iter_mut() {
unique_shas.insert(entry.sha);
// DEPRECATED (18 Apr 24): Sending permalinks over the wire is deprecated. Clients
// now do the parsing.
if let Some((provider, remote)) = parsed_remote_url.as_ref() {
permalinks.entry(entry.sha).or_insert_with(|| {
provider.build_commit_permalink(
remote,
BuildCommitPermalinkParams {
sha: entry.sha.to_string().as_str(),
},
)
});
}
}
let shas = unique_shas.into_iter().collect::<Vec<_>>();
@@ -44,6 +64,7 @@ impl Blame {
Ok(Self {
entries,
permalinks,
messages,
remote_url,
})

View File

@@ -1,5 +1,5 @@
use crate::status::FileStatus;
use crate::SHORT_SHA_LENGTH;
use crate::GitHostingProviderRegistry;
use crate::{blame::Blame, status::GitStatus};
use anyhow::{anyhow, Context, Result};
use askpass::{AskPassResult, AskPassSession};
@@ -57,14 +57,6 @@ pub struct Upstream {
pub tracking: UpstreamTracking,
}
impl Upstream {
pub fn remote_name(&self) -> Option<&str> {
self.ref_name
.strip_prefix("refs/remotes/")
.and_then(|stripped| stripped.split("/").next())
}
}
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
pub enum UpstreamTracking {
/// Remote ref not present in local repository.
@@ -128,12 +120,6 @@ pub struct CommitDetails {
pub committer_name: SharedString,
}
impl CommitDetails {
pub fn short_sha(&self) -> SharedString {
self.sha[..SHORT_SHA_LENGTH].to_string().into()
}
}
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub struct Remote {
pub name: SharedString,
@@ -163,12 +149,7 @@ pub trait GitRepository: Send + Sync {
/// Also returns `None` for symlinks.
fn load_committed_text(&self, path: &RepoPath) -> Option<String>;
fn set_index_text(
&self,
path: &RepoPath,
content: Option<String>,
env: &HashMap<String, String>,
) -> anyhow::Result<()>;
fn set_index_text(&self, path: &RepoPath, content: Option<String>) -> anyhow::Result<()>;
/// Returns the URL of the remote with the given name.
fn remote_url(&self, name: &str) -> Option<String>;
@@ -186,13 +167,8 @@ pub trait GitRepository: Send + Sync {
fn create_branch(&self, _: &str) -> Result<()>;
fn branch_exits(&self, _: &str) -> Result<bool>;
fn reset(&self, commit: &str, mode: ResetMode, env: &HashMap<String, String>) -> Result<()>;
fn checkout_files(
&self,
commit: &str,
paths: &[RepoPath],
env: &HashMap<String, String>,
) -> Result<()>;
fn reset(&self, commit: &str, mode: ResetMode) -> Result<()>;
fn checkout_files(&self, commit: &str, paths: &[RepoPath]) -> Result<()>;
fn show(&self, commit: &str) -> Result<CommitDetails>;
@@ -213,18 +189,13 @@ pub trait GitRepository: Send + Sync {
/// Updates the index to match the worktree at the given paths.
///
/// If any of the paths have been deleted from the worktree, they will be removed from the index if found there.
fn stage_paths(&self, paths: &[RepoPath], env: &HashMap<String, String>) -> Result<()>;
fn stage_paths(&self, paths: &[RepoPath]) -> Result<()>;
/// Updates the index to match HEAD at the given paths.
///
/// If any of the paths were previously staged but do not exist in HEAD, they will be removed from the index.
fn unstage_paths(&self, paths: &[RepoPath], env: &HashMap<String, String>) -> Result<()>;
fn unstage_paths(&self, paths: &[RepoPath]) -> Result<()>;
fn commit(
&self,
message: &str,
name_and_email: Option<(&str, &str)>,
env: &HashMap<String, String>,
) -> Result<()>;
fn commit(&self, message: &str, name_and_email: Option<(&str, &str)>) -> Result<()>;
fn push(
&self,
@@ -232,7 +203,6 @@ pub trait GitRepository: Send + Sync {
upstream_name: &str,
options: Option<PushOptions>,
askpass: AskPassSession,
env: &HashMap<String, String>,
) -> Result<RemoteCommandOutput>;
fn pull(
@@ -240,13 +210,8 @@ pub trait GitRepository: Send + Sync {
branch_name: &str,
upstream_name: &str,
askpass: AskPassSession,
env: &HashMap<String, String>,
) -> Result<RemoteCommandOutput>;
fn fetch(
&self,
askpass: AskPassSession,
env: &HashMap<String, String>,
) -> Result<RemoteCommandOutput>;
fn fetch(&self, askpass: AskPassSession) -> Result<RemoteCommandOutput>;
fn get_remotes(&self, branch_name: Option<&str>) -> Result<Vec<Remote>>;
@@ -277,13 +242,19 @@ impl std::fmt::Debug for dyn GitRepository {
pub struct RealGitRepository {
pub repository: Mutex<git2::Repository>,
pub git_binary_path: PathBuf,
hosting_provider_registry: Arc<GitHostingProviderRegistry>,
}
impl RealGitRepository {
pub fn new(repository: git2::Repository, git_binary_path: Option<PathBuf>) -> Self {
pub fn new(
repository: git2::Repository,
git_binary_path: Option<PathBuf>,
hosting_provider_registry: Arc<GitHostingProviderRegistry>,
) -> Self {
Self {
repository: Mutex::new(repository),
git_binary_path: git_binary_path.unwrap_or_else(|| PathBuf::from("git")),
hosting_provider_registry,
}
}
@@ -337,7 +308,7 @@ impl GitRepository for RealGitRepository {
Ok(details)
}
fn reset(&self, commit: &str, mode: ResetMode, env: &HashMap<String, String>) -> Result<()> {
fn reset(&self, commit: &str, mode: ResetMode) -> Result<()> {
let working_directory = self.working_directory()?;
let mode_flag = match mode {
@@ -346,7 +317,6 @@ impl GitRepository for RealGitRepository {
};
let output = new_std_command(&self.git_binary_path)
.envs(env)
.current_dir(&working_directory)
.args(["reset", mode_flag, commit])
.output()?;
@@ -359,12 +329,7 @@ impl GitRepository for RealGitRepository {
Ok(())
}
fn checkout_files(
&self,
commit: &str,
paths: &[RepoPath],
env: &HashMap<String, String>,
) -> Result<()> {
fn checkout_files(&self, commit: &str, paths: &[RepoPath]) -> Result<()> {
if paths.is_empty() {
return Ok(());
}
@@ -372,7 +337,6 @@ impl GitRepository for RealGitRepository {
let output = new_std_command(&self.git_binary_path)
.current_dir(&working_directory)
.envs(env)
.args(["checkout", commit, "--"])
.args(paths.iter().map(|path| path.as_ref()))
.output()?;
@@ -421,17 +385,11 @@ impl GitRepository for RealGitRepository {
Some(content)
}
fn set_index_text(
&self,
path: &RepoPath,
content: Option<String>,
env: &HashMap<String, String>,
) -> anyhow::Result<()> {
fn set_index_text(&self, path: &RepoPath, content: Option<String>) -> anyhow::Result<()> {
let working_directory = self.working_directory()?;
if let Some(content) = content {
let mut child = new_std_command(&self.git_binary_path)
.current_dir(&working_directory)
.envs(env)
.args(["hash-object", "-w", "--stdin"])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
@@ -444,7 +402,6 @@ impl GitRepository for RealGitRepository {
let output = new_std_command(&self.git_binary_path)
.current_dir(&working_directory)
.envs(env)
.args(["update-index", "--add", "--cacheinfo", "100644", &sha])
.arg(path.as_ref())
.output()?;
@@ -458,7 +415,6 @@ impl GitRepository for RealGitRepository {
} else {
let output = new_std_command(&self.git_binary_path)
.current_dir(&working_directory)
.envs(env)
.args(["update-index", "--force-remove"])
.arg(path.as_ref())
.output()?;
@@ -625,6 +581,7 @@ impl GitRepository for RealGitRepository {
path,
&content,
remote_url,
self.hosting_provider_registry.clone(),
)
}
@@ -650,13 +607,12 @@ impl GitRepository for RealGitRepository {
Ok(String::from_utf8_lossy(&output.stdout).to_string())
}
fn stage_paths(&self, paths: &[RepoPath], env: &HashMap<String, String>) -> Result<()> {
fn stage_paths(&self, paths: &[RepoPath]) -> Result<()> {
let working_directory = self.working_directory()?;
if !paths.is_empty() {
let output = new_std_command(&self.git_binary_path)
.current_dir(&working_directory)
.envs(env)
.args(["update-index", "--add", "--remove", "--"])
.args(paths.iter().map(|p| p.as_ref()))
.output()?;
@@ -671,13 +627,12 @@ impl GitRepository for RealGitRepository {
Ok(())
}
fn unstage_paths(&self, paths: &[RepoPath], env: &HashMap<String, String>) -> Result<()> {
fn unstage_paths(&self, paths: &[RepoPath]) -> Result<()> {
let working_directory = self.working_directory()?;
if !paths.is_empty() {
let output = new_std_command(&self.git_binary_path)
.current_dir(&working_directory)
.envs(env)
.args(["reset", "--quiet", "--"])
.args(paths.iter().map(|p| p.as_ref()))
.output()?;
@@ -692,17 +647,11 @@ impl GitRepository for RealGitRepository {
Ok(())
}
fn commit(
&self,
message: &str,
name_and_email: Option<(&str, &str)>,
env: &HashMap<String, String>,
) -> Result<()> {
fn commit(&self, message: &str, name_and_email: Option<(&str, &str)>) -> Result<()> {
let working_directory = self.working_directory()?;
let mut cmd = new_std_command(&self.git_binary_path);
cmd.current_dir(&working_directory)
.envs(env)
.args(["commit", "--quiet", "-m"])
.arg(message)
.arg("--cleanup=strip");
@@ -728,13 +677,11 @@ impl GitRepository for RealGitRepository {
remote_name: &str,
options: Option<PushOptions>,
ask_pass: AskPassSession,
env: &HashMap<String, String>,
) -> Result<RemoteCommandOutput> {
let working_directory = self.working_directory()?;
let mut command = new_smol_command("git");
command
.envs(env)
.env("GIT_ASKPASS", ask_pass.script_path())
.env("SSH_ASKPASS", ask_pass.script_path())
.env("SSH_ASKPASS_REQUIRE", "force")
@@ -745,9 +692,7 @@ impl GitRepository for RealGitRepository {
PushOptions::Force => "--force-with-lease",
}))
.arg(remote_name)
.arg(format!("{}:{}", branch_name, branch_name))
.stdout(smol::process::Stdio::piped())
.stderr(smol::process::Stdio::piped());
.arg(format!("{}:{}", branch_name, branch_name));
let git_process = command.spawn()?;
run_remote_command(ask_pass, git_process)
@@ -758,44 +703,33 @@ impl GitRepository for RealGitRepository {
branch_name: &str,
remote_name: &str,
ask_pass: AskPassSession,
env: &HashMap<String, String>,
) -> Result<RemoteCommandOutput> {
let working_directory = self.working_directory()?;
let mut command = new_smol_command("git");
command
.envs(env)
.env("GIT_ASKPASS", ask_pass.script_path())
.env("SSH_ASKPASS", ask_pass.script_path())
.env("SSH_ASKPASS_REQUIRE", "force")
.current_dir(&working_directory)
.args(["pull"])
.arg(remote_name)
.arg(branch_name)
.stdout(smol::process::Stdio::piped())
.stderr(smol::process::Stdio::piped());
.arg(branch_name);
let git_process = command.spawn()?;
run_remote_command(ask_pass, git_process)
}
fn fetch(
&self,
ask_pass: AskPassSession,
env: &HashMap<String, String>,
) -> Result<RemoteCommandOutput> {
fn fetch(&self, ask_pass: AskPassSession) -> Result<RemoteCommandOutput> {
let working_directory = self.working_directory()?;
let mut command = new_smol_command("git");
command
.envs(env)
.env("GIT_ASKPASS", ask_pass.script_path())
.env("SSH_ASKPASS", ask_pass.script_path())
.env("SSH_ASKPASS_REQUIRE", "force")
.current_dir(&working_directory)
.args(["fetch", "--all"])
.stdout(smol::process::Stdio::piped())
.stderr(smol::process::Stdio::piped());
.args(["fetch", "--all"]);
let git_process = command.spawn()?;
run_remote_command(ask_pass, git_process)
@@ -979,12 +913,7 @@ impl GitRepository for FakeGitRepository {
state.head_contents.get(path.as_ref()).cloned()
}
fn set_index_text(
&self,
path: &RepoPath,
content: Option<String>,
_env: &HashMap<String, String>,
) -> anyhow::Result<()> {
fn set_index_text(&self, path: &RepoPath, content: Option<String>) -> anyhow::Result<()> {
let mut state = self.state.lock();
if let Some(message) = state.simulated_index_write_error_message.clone() {
return Err(anyhow::anyhow!(message));
@@ -1017,11 +946,11 @@ impl GitRepository for FakeGitRepository {
unimplemented!()
}
fn reset(&self, _: &str, _: ResetMode, _: &HashMap<String, String>) -> Result<()> {
fn reset(&self, _: &str, _: ResetMode) -> Result<()> {
unimplemented!()
}
fn checkout_files(&self, _: &str, _: &[RepoPath], _: &HashMap<String, String>) -> Result<()> {
fn checkout_files(&self, _: &str, _: &[RepoPath]) -> Result<()> {
unimplemented!()
}
@@ -1107,20 +1036,15 @@ impl GitRepository for FakeGitRepository {
.cloned()
}
fn stage_paths(&self, _paths: &[RepoPath], _env: &HashMap<String, String>) -> Result<()> {
fn stage_paths(&self, _paths: &[RepoPath]) -> Result<()> {
unimplemented!()
}
fn unstage_paths(&self, _paths: &[RepoPath], _env: &HashMap<String, String>) -> Result<()> {
fn unstage_paths(&self, _paths: &[RepoPath]) -> Result<()> {
unimplemented!()
}
fn commit(
&self,
_message: &str,
_name_and_email: Option<(&str, &str)>,
_env: &HashMap<String, String>,
) -> Result<()> {
fn commit(&self, _message: &str, _name_and_email: Option<(&str, &str)>) -> Result<()> {
unimplemented!()
}
@@ -1130,7 +1054,6 @@ impl GitRepository for FakeGitRepository {
_remote: &str,
_options: Option<PushOptions>,
_ask_pass: AskPassSession,
_env: &HashMap<String, String>,
) -> Result<RemoteCommandOutput> {
unimplemented!()
}
@@ -1140,16 +1063,11 @@ impl GitRepository for FakeGitRepository {
_branch: &str,
_remote: &str,
_ask_pass: AskPassSession,
_env: &HashMap<String, String>,
) -> Result<RemoteCommandOutput> {
unimplemented!()
}
fn fetch(
&self,
_ask_pass: AskPassSession,
_env: &HashMap<String, String>,
) -> Result<RemoteCommandOutput> {
fn fetch(&self, _ask_pass: AskPassSession) -> Result<RemoteCommandOutput> {
unimplemented!()
}

View File

@@ -54,39 +54,6 @@ impl From<TrackedStatus> for FileStatus {
}
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum StageStatus {
Staged,
Unstaged,
PartiallyStaged,
}
impl StageStatus {
pub fn is_fully_staged(&self) -> bool {
matches!(self, StageStatus::Staged)
}
pub fn is_fully_unstaged(&self) -> bool {
matches!(self, StageStatus::Unstaged)
}
pub fn has_staged(&self) -> bool {
matches!(self, StageStatus::Staged | StageStatus::PartiallyStaged)
}
pub fn has_unstaged(&self) -> bool {
matches!(self, StageStatus::Unstaged | StageStatus::PartiallyStaged)
}
pub fn as_bool(self) -> Option<bool> {
match self {
StageStatus::Staged => Some(true),
StageStatus::Unstaged => Some(false),
StageStatus::PartiallyStaged => None,
}
}
}
impl FileStatus {
pub const fn worktree(worktree_status: StatusCode) -> Self {
FileStatus::Tracked(TrackedStatus {
@@ -139,15 +106,15 @@ impl FileStatus {
Ok(status)
}
pub fn staging(self) -> StageStatus {
pub fn is_staged(self) -> Option<bool> {
match self {
FileStatus::Untracked | FileStatus::Ignored | FileStatus::Unmerged { .. } => {
StageStatus::Unstaged
Some(false)
}
FileStatus::Tracked(tracked) => match (tracked.index_status, tracked.worktree_status) {
(StatusCode::Unmodified, _) => StageStatus::Unstaged,
(_, StatusCode::Unmodified) => StageStatus::Staged,
_ => StageStatus::PartiallyStaged,
(StatusCode::Unmodified, _) => Some(false),
(_, StatusCode::Unmodified) => Some(true),
_ => None,
},
}
}

View File

@@ -18,12 +18,13 @@ test-support = ["multi_buffer/test-support"]
[dependencies]
anyhow.workspace = true
askpass.workspace = true
askpass.workspace= true
buffer_diff.workspace = true
collections.workspace = true
component.workspace = true
db.workspace = true
editor.workspace = true
feature_flags.workspace = true
futures.workspace = true
fuzzy.workspace = true
git.workspace = true
@@ -50,7 +51,6 @@ strum.workspace = true
telemetry.workspace = true
theme.workspace = true
time.workspace = true
time_format.workspace = true
ui.workspace = true
util.workspace = true
workspace.workspace = true

View File

@@ -1,18 +1,16 @@
use anyhow::{anyhow, Context as _};
use fuzzy::StringMatchCandidate;
use fuzzy::{StringMatch, StringMatchCandidate};
use git::repository::Branch;
use gpui::{
rems, App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
InteractiveElement, IntoElement, Modifiers, ModifiersChangedEvent, ParentElement, Render,
SharedString, Styled, Subscription, Task, Window,
InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled, Subscription,
Task, Window,
};
use picker::{Picker, PickerDelegate};
use project::git::Repository;
use std::sync::Arc;
use time::OffsetDateTime;
use time_format::format_local_timestamp;
use ui::{prelude::*, HighlightedLabel, ListItem, ListItemSpacing};
use ui::{prelude::*, HighlightedLabel, ListItem, ListItemSpacing, PopoverMenuHandle};
use util::ResultExt;
use workspace::notifications::DetachAndPromptErr;
use workspace::{ModalView, Workspace};
@@ -20,30 +18,10 @@ use workspace::{ModalView, Workspace};
pub fn init(cx: &mut App) {
cx.observe_new(|workspace: &mut Workspace, _, _| {
workspace.register_action(open);
workspace.register_action(switch);
workspace.register_action(checkout_branch);
})
.detach();
}
pub fn checkout_branch(
workspace: &mut Workspace,
_: &zed_actions::git::CheckoutBranch,
window: &mut Window,
cx: &mut Context<Workspace>,
) {
open(workspace, &zed_actions::git::Branch, window, cx);
}
pub fn switch(
workspace: &mut Workspace,
_: &zed_actions::git::Switch,
window: &mut Window,
cx: &mut Context<Workspace>,
) {
open(workspace, &zed_actions::git::Branch, window, cx);
}
pub fn open(
workspace: &mut Workspace,
_: &zed_actions::git::Branch,
@@ -53,7 +31,7 @@ pub fn open(
let repository = workspace.project().read(cx).active_repository(cx).clone();
let style = BranchListStyle::Modal;
workspace.toggle_modal(window, cx, |window, cx| {
BranchList::new(repository, style, rems(34.), window, cx)
BranchList::new(repository, style, 34., window, cx)
})
}
@@ -63,7 +41,7 @@ pub fn popover(
cx: &mut App,
) -> Entity<BranchList> {
cx.new(|cx| {
let list = BranchList::new(repository, BranchListStyle::Popover, rems(20.), window, cx);
let list = BranchList::new(repository, BranchListStyle::Popover, 15., window, cx);
list.focus_handle(cx).focus(window);
list
})
@@ -76,7 +54,8 @@ enum BranchListStyle {
}
pub struct BranchList {
width: Rems,
rem_width: f32,
pub popover_handle: PopoverMenuHandle<Self>,
pub picker: Entity<Picker<BranchListDelegate>>,
_subscription: Subscription,
}
@@ -85,26 +64,20 @@ impl BranchList {
fn new(
repository: Option<Entity<Repository>>,
style: BranchListStyle,
width: Rems,
rem_width: f32,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let popover_handle = PopoverMenuHandle::default();
let all_branches_request = repository
.clone()
.map(|repository| repository.read(cx).branches());
cx.spawn_in(window, |this, mut cx| async move {
let mut all_branches = all_branches_request
let all_branches = all_branches_request
.context("No active repository")?
.await??;
all_branches.sort_by_key(|branch| {
branch
.most_recent_commit
.as_ref()
.map(|commit| 0 - commit.commit_timestamp)
});
this.update_in(&mut cx, |this, window, cx| {
this.picker.update(cx, |picker, cx| {
picker.delegate.all_branches = Some(all_branches);
@@ -116,7 +89,7 @@ impl BranchList {
})
.detach_and_log_err(cx);
let delegate = BranchListDelegate::new(repository.clone(), style);
let delegate = BranchListDelegate::new(repository.clone(), style, 20);
let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
let _subscription = cx.subscribe(&picker, |_, _, _, cx| {
@@ -125,20 +98,11 @@ impl BranchList {
Self {
picker,
width,
rem_width,
popover_handle,
_subscription,
}
}
fn handle_modifiers_changed(
&mut self,
ev: &ModifiersChangedEvent,
_: &mut Window,
cx: &mut Context<Self>,
) {
self.picker
.update(cx, |picker, _| picker.delegate.modifiers = ev.modifiers)
}
}
impl ModalView for BranchList {}
impl EventEmitter<DismissEvent> for BranchList {}
@@ -152,8 +116,7 @@ impl Focusable for BranchList {
impl Render for BranchList {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
v_flex()
.w(self.width)
.on_modifiers_changed(cx.listener(Self::handle_modifiers_changed))
.w(rems(self.rem_width))
.child(self.picker.clone())
.on_mouse_down_out({
cx.listener(move |this, _, window, cx| {
@@ -166,10 +129,20 @@ impl Render for BranchList {
}
#[derive(Debug, Clone)]
struct BranchEntry {
branch: Branch,
positions: Vec<usize>,
is_new: bool,
enum BranchEntry {
Branch(StringMatch),
History(String),
NewBranch { name: String },
}
impl BranchEntry {
fn name(&self) -> &str {
match self {
Self::Branch(branch) => &branch.string,
Self::History(branch) => &branch,
Self::NewBranch { name } => &name,
}
}
}
pub struct BranchListDelegate {
@@ -179,11 +152,16 @@ pub struct BranchListDelegate {
style: BranchListStyle,
selected_index: usize,
last_query: String,
modifiers: Modifiers,
/// Max length of branch name before we truncate it and add a trailing `...`.
branch_name_trailoff_after: usize,
}
impl BranchListDelegate {
fn new(repo: Option<Entity<Repository>>, style: BranchListStyle) -> Self {
fn new(
repo: Option<Entity<Repository>>,
style: BranchListStyle,
branch_name_trailoff_after: usize,
) -> Self {
Self {
matches: vec![],
repo,
@@ -191,30 +169,15 @@ impl BranchListDelegate {
all_branches: None,
selected_index: 0,
last_query: Default::default(),
modifiers: Default::default(),
branch_name_trailoff_after,
}
}
fn create_branch(
&self,
new_branch_name: SharedString,
window: &mut Window,
cx: &mut Context<Picker<Self>>,
) {
let Some(repo) = self.repo.clone() else {
return;
};
cx.spawn(|_, cx| async move {
cx.update(|cx| repo.read(cx).create_branch(&new_branch_name))?
.await??;
cx.update(|cx| repo.read(cx).change_branch(&new_branch_name))?
.await??;
Ok(())
})
.detach_and_prompt_err("Failed to create branch", window, cx, |e, _, _| {
Some(e.to_string())
});
cx.emit(DismissEvent);
pub fn branch_count(&self) -> usize {
self.matches
.iter()
.filter(|item| matches!(item, BranchEntry::Branch(_)))
.count()
}
}
@@ -248,28 +211,37 @@ impl PickerDelegate for BranchListDelegate {
window: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Task<()> {
let Some(all_branches) = self.all_branches.clone() else {
let Some(mut all_branches) = self.all_branches.clone() else {
return Task::ready(());
};
const RECENT_BRANCHES_COUNT: usize = 10;
cx.spawn_in(window, move |picker, mut cx| async move {
let mut matches: Vec<BranchEntry> = if query.is_empty() {
all_branches
const RECENT_BRANCHES_COUNT: usize = 10;
if query.is_empty() {
if all_branches.len() > RECENT_BRANCHES_COUNT {
// Truncate list of recent branches
// Do a partial sort to show recent-ish branches first.
all_branches.select_nth_unstable_by(RECENT_BRANCHES_COUNT - 1, |lhs, rhs| {
rhs.priority_key().cmp(&lhs.priority_key())
});
all_branches.truncate(RECENT_BRANCHES_COUNT);
}
all_branches.sort_unstable_by(|lhs, rhs| {
rhs.is_head.cmp(&lhs.is_head).then(lhs.name.cmp(&rhs.name))
});
}
let candidates = all_branches
.into_iter()
.enumerate()
.map(|(ix, command)| StringMatchCandidate::new(ix, &command.name))
.collect::<Vec<StringMatchCandidate>>();
let matches: Vec<BranchEntry> = if query.is_empty() {
candidates
.into_iter()
.take(RECENT_BRANCHES_COUNT)
.map(|branch| BranchEntry {
branch,
positions: Vec::new(),
is_new: false,
})
.map(|candidate| BranchEntry::History(candidate.string))
.collect()
} else {
let candidates = all_branches
.iter()
.enumerate()
.map(|(ix, command)| StringMatchCandidate::new(ix, &command.name.clone()))
.collect::<Vec<StringMatchCandidate>>();
fuzzy::match_strings(
&candidates,
&query,
@@ -281,35 +253,20 @@ impl PickerDelegate for BranchListDelegate {
.await
.iter()
.cloned()
.map(|candidate| BranchEntry {
branch: all_branches[candidate.candidate_id].clone(),
positions: candidate.positions,
is_new: false,
})
.map(BranchEntry::Branch)
.collect()
};
picker
.update(&mut cx, |picker, _| {
#[allow(clippy::nonminimal_bool)]
if !query.is_empty()
&& !matches
.first()
.is_some_and(|entry| entry.branch.name == query)
{
matches.push(BranchEntry {
branch: Branch {
name: query.clone().into(),
is_head: false,
upstream: None,
most_recent_commit: None,
},
positions: Vec::new(),
is_new: true,
})
}
let delegate = &mut picker.delegate;
delegate.matches = matches;
if delegate.matches.is_empty() {
if !query.is_empty() {
delegate.matches.push(BranchEntry::NewBranch {
name: query.trim().replace(' ', "-"),
});
}
delegate.selected_index = 0;
} else {
delegate.selected_index =
@@ -321,14 +278,10 @@ impl PickerDelegate for BranchListDelegate {
})
}
fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
let Some(entry) = self.matches.get(self.selected_index()) else {
fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
let Some(branch) = self.matches.get(self.selected_index()) else {
return;
};
if entry.is_new {
self.create_branch(entry.branch.name.clone(), window, cx);
return;
}
let current_branch = self.repo.as_ref().map(|repo| {
repo.update(cx, |repo, _| {
@@ -338,14 +291,14 @@ impl PickerDelegate for BranchListDelegate {
if current_branch
.flatten()
.is_some_and(|current_branch| current_branch == entry.branch.name)
.is_some_and(|current_branch| current_branch == branch.name())
{
cx.emit(DismissEvent);
return;
}
cx.spawn_in(window, {
let branch = entry.branch.clone();
let branch = branch.clone();
|picker, mut cx| async move {
let branch_change_task = picker.update(&mut cx, |this, cx| {
let repo = this
@@ -358,8 +311,22 @@ impl PickerDelegate for BranchListDelegate {
let cx = cx.to_async();
anyhow::Ok(async move {
cx.update(|cx| repo.read(cx).change_branch(&branch.name))?
.await?
match branch {
BranchEntry::Branch(StringMatch {
string: branch_name,
..
})
| BranchEntry::History(branch_name) => {
cx.update(|cx| repo.read(cx).change_branch(branch_name))?
.await?
}
BranchEntry::NewBranch { name: branch_name } => {
cx.update(|cx| repo.read(cx).create_branch(branch_name.clone()))?
.await??;
cx.update(|cx| repo.read(cx).change_branch(branch_name))?
.await?
}
}
})
})??;
@@ -379,35 +346,16 @@ impl PickerDelegate for BranchListDelegate {
cx.emit(DismissEvent);
}
fn render_header(&self, _: &mut Window, _cx: &mut Context<Picker<Self>>) -> Option<AnyElement> {
None
}
fn render_match(
&self,
ix: usize,
selected: bool,
_window: &mut Window,
cx: &mut Context<Picker<Self>>,
_cx: &mut Context<Picker<Self>>,
) -> Option<Self::ListItem> {
let entry = &self.matches[ix];
let (commit_time, subject) = entry
.branch
.most_recent_commit
.as_ref()
.map(|commit| {
let subject = commit.subject.clone();
let commit_time = OffsetDateTime::from_unix_timestamp(commit.commit_timestamp)
.unwrap_or_else(|_| OffsetDateTime::now_utc());
let formatted_time = format_local_timestamp(
commit_time,
OffsetDateTime::now_utc(),
time_format::TimestampFormat::Relative,
);
(Some(formatted_time), Some(subject))
})
.unwrap_or_else(|| (None, None));
let hit = &self.matches[ix];
let shortened_branch_name =
util::truncate_and_trailoff(&hit.name(), self.branch_name_trailoff_after);
Some(
ListItem::new(SharedString::from(format!("vcs-menu-{ix}")))
@@ -418,67 +366,29 @@ impl PickerDelegate for BranchListDelegate {
})
.spacing(ListItemSpacing::Sparse)
.toggle_state(selected)
.child(
v_flex()
.w_full()
.child(
h_flex()
.w_full()
.flex_shrink()
.overflow_x_hidden()
.gap_2()
.justify_between()
.child(div().flex_shrink().overflow_x_hidden().child(
if entry.is_new {
Label::new(format!(
"Create branch \"{}\"",
entry.branch.name
))
.into_any_element()
} else {
HighlightedLabel::new(
entry.branch.name.clone(),
entry.positions.clone(),
)
.truncate()
.into_any_element()
},
))
.when_some(commit_time, |el, commit_time| {
el.child(
Label::new(commit_time)
.size(LabelSize::Small)
.color(Color::Muted)
.into_element(),
)
}),
)
.when(self.style == BranchListStyle::Modal, |el| {
el.child(div().max_w_96().child({
let message = if entry.is_new {
if let Some(current_branch) =
self.repo.as_ref().and_then(|repo| {
repo.read(cx).current_branch().map(|b| b.name.clone())
})
{
format!("based off {}", current_branch)
} else {
"based off the current branch".to_string()
}
} else {
subject.unwrap_or("no commits found".into()).to_string()
};
Label::new(message)
.size(LabelSize::Small)
.truncate()
.color(Color::Muted)
}))
}),
),
.when(matches!(hit, BranchEntry::History(_)), |el| {
el.end_slot(
Icon::new(IconName::HistoryRerun)
.color(Color::Muted)
.size(IconSize::Small),
)
})
.map(|el| match hit {
BranchEntry::Branch(branch) => {
let highlights: Vec<_> = branch
.positions
.iter()
.filter(|index| index < &&self.branch_name_trailoff_after)
.copied()
.collect();
el.child(HighlightedLabel::new(shortened_branch_name, highlights))
}
BranchEntry::History(_) => el.child(Label::new(shortened_branch_name)),
BranchEntry::NewBranch { name } => {
el.child(Label::new(format!("Create branch '{name}'")))
}
}),
)
}
fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> Option<SharedString> {
None
}
}

View File

@@ -4,7 +4,7 @@ If you can accurately express the change in just the subject line, don't include
Don't repeat information from the subject line in the message body.
Only return the commit message in your response. Do not include any additional meta-commentary about the task. Do not include the raw diff output in the commit message.
Only return the commit message in your response. Do not include any additional meta-commentary about the task.
Follow good Git style:

View File

@@ -2,9 +2,9 @@
use crate::branch_picker::{self, BranchList};
use crate::git_panel::{commit_message_editor, GitPanel};
use git::{Commit, GenerateCommitMessage};
use git::Commit;
use panel::{panel_button, panel_editor_style, panel_filled_button};
use ui::{prelude::*, KeybindingHint, PopoverMenu, PopoverMenuHandle, Tooltip};
use ui::{prelude::*, KeybindingHint, PopoverMenu, Tooltip};
use editor::{Editor, EditorElement};
use gpui::*;
@@ -65,11 +65,11 @@ pub fn init(cx: &mut App) {
}
pub struct CommitModal {
branch_list: Entity<BranchList>,
git_panel: Entity<GitPanel>,
commit_editor: Entity<Editor>,
restore_dock: RestoreDock,
properties: ModalContainerProperties,
branch_list_handle: PopoverMenuHandle<BranchList>,
}
impl Focusable for CommitModal {
@@ -146,6 +146,7 @@ impl CommitModal {
cx: &mut Context<Self>,
) -> Self {
let panel = git_panel.read(cx);
let active_repository = panel.active_repository.clone();
let suggested_commit_message = panel.suggest_commit_message();
let commit_editor = git_panel.update(cx, |git_panel, cx| {
@@ -176,7 +177,11 @@ impl CommitModal {
let focus_handle = commit_editor.focus_handle(cx);
cx.on_focus_out(&focus_handle, window, |this, _, window, cx| {
if !this.branch_list_handle.is_focused(window, cx) {
if !this
.branch_list
.focus_handle(cx)
.contains_focused(window, cx)
{
cx.emit(DismissEvent);
}
})
@@ -185,11 +190,11 @@ impl CommitModal {
let properties = ModalContainerProperties::new(window, 50);
Self {
branch_list: branch_picker::popover(active_repository.clone(), window, cx),
git_panel,
commit_editor,
restore_dock,
properties,
branch_list_handle: PopoverMenuHandle::default(),
}
}
@@ -227,29 +232,34 @@ impl CommitModal {
}
pub fn render_footer(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let (can_commit, tooltip, commit_label, co_authors, generate_commit_message, active_repo) =
let git_panel = self.git_panel.clone();
let (branch, can_commit, tooltip, commit_label, co_authors, generate_commit_message) =
self.git_panel.update(cx, |git_panel, cx| {
let branch = git_panel
.active_repository
.as_ref()
.and_then(|repo| {
repo.read(cx)
.repository_entry
.branch()
.map(|b| b.name.clone())
})
.unwrap_or_else(|| "<no branch>".into());
let (can_commit, tooltip) = git_panel.configure_commit_button(cx);
let title = git_panel.commit_button_title();
let co_authors = git_panel.render_co_authors(cx);
let generate_commit_message = git_panel.render_generate_commit_message_button(cx);
let active_repo = git_panel.active_repository.clone();
(
branch,
can_commit,
tooltip,
title,
co_authors,
generate_commit_message,
active_repo,
)
});
let branch = active_repo
.as_ref()
.and_then(|repo| repo.read(cx).repository_entry.branch())
.map(|b| b.name.clone())
.unwrap_or_else(|| "<no branch>".into());
let branch_picker_button = panel_button(branch)
.icon(IconName::GitBranch)
.icon_size(IconSize::Small)
@@ -266,8 +276,10 @@ impl CommitModal {
.style(ButtonStyle::Transparent);
let branch_picker = PopoverMenu::new("popover-button")
.menu(move |window, cx| Some(branch_picker::popover(active_repo.clone(), window, cx)))
.with_handle(self.branch_list_handle.clone())
.menu({
let branch_list = self.branch_list.clone();
move |_window, _cx| Some(branch_list.clone())
})
.trigger_with_tooltip(
branch_picker_button,
Tooltip::for_action_title("Switch Branch", &zed_actions::git::Branch),
@@ -277,7 +289,6 @@ impl CommitModal {
x: px(0.0),
y: px(-2.0),
});
let focus_handle = self.focus_handle(cx);
let close_kb_hint =
if let Some(close_kb) = ui::KeyBinding::for_action(&menu::Cancel, window, cx) {
@@ -289,9 +300,12 @@ impl CommitModal {
None
};
let panel_editor_focus_handle =
git_panel.update(cx, |git_panel, cx| git_panel.editor_focus_handle(cx));
let commit_button = panel_filled_button(commit_label)
.tooltip({
let panel_editor_focus_handle = focus_handle.clone();
let panel_editor_focus_handle = panel_editor_focus_handle.clone();
move |window, cx| {
Tooltip::for_action_in(tooltip, &Commit, &panel_editor_focus_handle, window, cx)
}
@@ -316,14 +330,7 @@ impl CommitModal {
.child(
h_flex()
.gap_1()
.flex_shrink()
.overflow_x_hidden()
.child(
h_flex()
.flex_shrink()
.overflow_x_hidden()
.child(branch_picker),
)
.child(branch_picker)
.children(generate_commit_message)
.children(co_authors),
)
@@ -350,14 +357,6 @@ impl CommitModal {
.update(cx, |git_panel, cx| git_panel.commit_changes(window, cx));
cx.emit(DismissEvent);
}
fn toggle_branch_selector(&mut self, window: &mut Window, cx: &mut Context<Self>) {
if self.branch_list_handle.is_focused(window, cx) {
self.focus_handle(cx).focus(window)
} else {
self.branch_list_handle.toggle(window, cx);
}
}
}
impl Render for CommitModal {
@@ -373,24 +372,11 @@ impl Render for CommitModal {
.key_context("GitCommit")
.on_action(cx.listener(Self::dismiss))
.on_action(cx.listener(Self::commit))
.on_action(cx.listener(|this, _: &GenerateCommitMessage, _, cx| {
this.git_panel.update(cx, |panel, cx| {
panel.generate_commit_message(cx);
})
}))
.on_action(
cx.listener(|this, _: &zed_actions::git::Branch, window, cx| {
this.toggle_branch_selector(window, cx);
}),
)
.on_action(
cx.listener(|this, _: &zed_actions::git::CheckoutBranch, window, cx| {
this.toggle_branch_selector(window, cx);
}),
)
.on_action(
cx.listener(|this, _: &zed_actions::git::Switch, window, cx| {
this.toggle_branch_selector(window, cx);
this.branch_list.update(cx, |branch_list, cx| {
branch_list.popover_handle.toggle(window, cx);
})
}),
)
.elevation_3(cx)

File diff suppressed because it is too large Load Diff

View File

@@ -1,13 +1,9 @@
use ::settings::Settings;
use git::{
repository::{Branch, Upstream, UpstreamTracking, UpstreamTrackingStatus},
status::{FileStatus, StatusCode, UnmergedStatus, UnmergedStatusCode},
};
use git::status::FileStatus;
use git_panel_settings::GitPanelSettings;
use gpui::{App, Entity, FocusHandle};
use project::Project;
use gpui::App;
use project_diff::ProjectDiff;
use ui::prelude::*;
use ui::{ActiveTheme, Color, Icon, IconName, IntoElement};
use workspace::Workspace;
mod askpass_modal;
@@ -29,493 +25,67 @@ pub fn init(cx: &mut App) {
cx.observe_new(|workspace: &mut Workspace, _, cx| {
let project = workspace.project().read(cx);
if project.is_read_only(cx) {
if project.is_via_collab() {
return;
}
if !project.is_via_collab() {
workspace.register_action(|workspace, _: &git::Fetch, window, cx| {
let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
return;
};
panel.update(cx, |panel, cx| {
panel.fetch(window, cx);
});
});
workspace.register_action(|workspace, _: &git::Push, window, cx| {
let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
return;
};
panel.update(cx, |panel, cx| {
panel.push(false, window, cx);
});
});
workspace.register_action(|workspace, _: &git::ForcePush, window, cx| {
let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
return;
};
panel.update(cx, |panel, cx| {
panel.push(true, window, cx);
});
});
workspace.register_action(|workspace, _: &git::Pull, window, cx| {
let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
return;
};
panel.update(cx, |panel, cx| {
panel.pull(window, cx);
});
});
}
workspace.register_action(|workspace, action: &git::StageAll, window, cx| {
workspace.register_action(|workspace, _: &git::Fetch, window, cx| {
let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
return;
};
panel.update(cx, |panel, cx| {
panel.stage_all(action, window, cx);
panel.fetch(window, cx);
});
});
workspace.register_action(|workspace, action: &git::UnstageAll, window, cx| {
workspace.register_action(|workspace, _: &git::Push, window, cx| {
let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
return;
};
panel.update(cx, |panel, cx| {
panel.unstage_all(action, window, cx);
panel.push(false, window, cx);
});
});
workspace.register_action(|workspace, _: &git::ForcePush, window, cx| {
let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
return;
};
panel.update(cx, |panel, cx| {
panel.push(true, window, cx);
});
});
workspace.register_action(|workspace, _: &git::Pull, window, cx| {
let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
return;
};
panel.update(cx, |panel, cx| {
panel.pull(window, cx);
});
});
})
.detach();
}
pub fn git_status_icon(status: FileStatus) -> impl IntoElement {
GitStatusIcon::new(status)
}
fn can_push_and_pull(project: &Entity<Project>, cx: &App) -> bool {
!project.read(cx).is_via_collab()
}
fn render_remote_button(
id: impl Into<SharedString>,
branch: &Branch,
keybinding_target: Option<FocusHandle>,
show_fetch_button: bool,
) -> Option<impl IntoElement> {
let id = id.into();
let upstream = branch.upstream.as_ref();
match upstream {
Some(Upstream {
tracking: UpstreamTracking::Tracked(UpstreamTrackingStatus { ahead, behind }),
..
}) => match (*ahead, *behind) {
(0, 0) if show_fetch_button => {
Some(remote_button::render_fetch_button(keybinding_target, id))
}
(0, 0) => None,
(ahead, 0) => Some(remote_button::render_push_button(
keybinding_target.clone(),
id,
ahead,
)),
(ahead, behind) => Some(remote_button::render_pull_button(
keybinding_target.clone(),
id,
ahead,
behind,
)),
},
Some(Upstream {
tracking: UpstreamTracking::Gone,
..
}) => Some(remote_button::render_republish_button(
keybinding_target,
id,
)),
None => Some(remote_button::render_publish_button(keybinding_target, id)),
}
}
mod remote_button {
use gpui::{hsla, point, Action, AnyView, BoxShadow, ClickEvent, Corner, FocusHandle};
use ui::{
div, h_flex, px, rems, ActiveTheme, AnyElement, App, ButtonCommon, ButtonLike, Clickable,
ContextMenu, ElementId, ElevationIndex, FluentBuilder, Icon, IconName, IconSize,
IntoElement, Label, LabelCommon, LabelSize, LineHeightStyle, ParentElement, PopoverMenu,
RenderOnce, SharedString, Styled, Tooltip, Window,
// TODO: Add updated status colors to theme
pub fn git_status_icon(status: FileStatus, cx: &App) -> impl IntoElement {
let (icon_name, color) = if status.is_conflicted() {
(
IconName::Warning,
cx.theme().colors().version_control_conflict,
)
} else if status.is_deleted() {
(
IconName::SquareMinus,
cx.theme().colors().version_control_deleted,
)
} else if status.is_modified() {
(
IconName::SquareDot,
cx.theme().colors().version_control_modified,
)
} else {
(
IconName::SquarePlus,
cx.theme().colors().version_control_added,
)
};
pub fn render_fetch_button(
keybinding_target: Option<FocusHandle>,
id: SharedString,
) -> SplitButton {
SplitButton::new(
id,
"Fetch",
0,
0,
Some(IconName::ArrowCircle),
keybinding_target.clone(),
move |_, window, cx| {
window.dispatch_action(Box::new(git::Fetch), cx);
},
move |window, cx| {
git_action_tooltip(
"Fetch updates from remote",
&git::Fetch,
"git fetch",
keybinding_target.clone(),
window,
cx,
)
},
)
}
pub fn render_push_button(
keybinding_target: Option<FocusHandle>,
id: SharedString,
ahead: u32,
) -> SplitButton {
SplitButton::new(
id,
"Push",
ahead as usize,
0,
None,
keybinding_target.clone(),
move |_, window, cx| {
window.dispatch_action(Box::new(git::Push), cx);
},
move |window, cx| {
git_action_tooltip(
"Push committed changes to remote",
&git::Push,
"git push",
keybinding_target.clone(),
window,
cx,
)
},
)
}
pub fn render_pull_button(
keybinding_target: Option<FocusHandle>,
id: SharedString,
ahead: u32,
behind: u32,
) -> SplitButton {
SplitButton::new(
id,
"Pull",
ahead as usize,
behind as usize,
None,
keybinding_target.clone(),
move |_, window, cx| {
window.dispatch_action(Box::new(git::Pull), cx);
},
move |window, cx| {
git_action_tooltip(
"Pull",
&git::Pull,
"git pull",
keybinding_target.clone(),
window,
cx,
)
},
)
}
pub fn render_publish_button(
keybinding_target: Option<FocusHandle>,
id: SharedString,
) -> SplitButton {
SplitButton::new(
id,
"Publish",
0,
0,
Some(IconName::ArrowUpFromLine),
keybinding_target.clone(),
move |_, window, cx| {
window.dispatch_action(Box::new(git::Push), cx);
},
move |window, cx| {
git_action_tooltip(
"Publish branch to remote",
&git::Push,
"git push --set-upstream",
keybinding_target.clone(),
window,
cx,
)
},
)
}
pub fn render_republish_button(
keybinding_target: Option<FocusHandle>,
id: SharedString,
) -> SplitButton {
SplitButton::new(
id,
"Republish",
0,
0,
Some(IconName::ArrowUpFromLine),
keybinding_target.clone(),
move |_, window, cx| {
window.dispatch_action(Box::new(git::Push), cx);
},
move |window, cx| {
git_action_tooltip(
"Re-publish branch to remote",
&git::Push,
"git push --set-upstream",
keybinding_target.clone(),
window,
cx,
)
},
)
}
fn git_action_tooltip(
label: impl Into<SharedString>,
action: &dyn Action,
command: impl Into<SharedString>,
focus_handle: Option<FocusHandle>,
window: &mut Window,
cx: &mut App,
) -> AnyView {
let label = label.into();
let command = command.into();
if let Some(handle) = focus_handle {
Tooltip::with_meta_in(
label.clone(),
Some(action),
command.clone(),
&handle,
window,
cx,
)
} else {
Tooltip::with_meta(label.clone(), Some(action), command.clone(), window, cx)
}
}
fn render_git_action_menu(
id: impl Into<ElementId>,
keybinding_target: Option<FocusHandle>,
) -> impl IntoElement {
PopoverMenu::new(id.into())
.trigger(
ui::ButtonLike::new_rounded_right("split-button-right")
.layer(ui::ElevationIndex::ModalSurface)
.size(ui::ButtonSize::None)
.child(
div()
.px_1()
.child(Icon::new(IconName::ChevronDownSmall).size(IconSize::XSmall)),
),
)
.menu(move |window, cx| {
Some(ContextMenu::build(window, cx, |context_menu, _, _| {
context_menu
.when_some(keybinding_target.clone(), |el, keybinding_target| {
el.context(keybinding_target.clone())
})
.action("Fetch", git::Fetch.boxed_clone())
.action("Pull", git::Pull.boxed_clone())
.separator()
.action("Push", git::Push.boxed_clone())
.action("Force Push", git::ForcePush.boxed_clone())
}))
})
.anchor(Corner::TopRight)
}
#[derive(IntoElement)]
pub struct SplitButton {
pub left: ButtonLike,
pub right: AnyElement,
}
impl SplitButton {
#[allow(clippy::too_many_arguments)]
fn new(
id: impl Into<SharedString>,
left_label: impl Into<SharedString>,
ahead_count: usize,
behind_count: usize,
left_icon: Option<IconName>,
keybinding_target: Option<FocusHandle>,
left_on_click: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static,
) -> Self {
let id = id.into();
fn count(count: usize) -> impl IntoElement {
h_flex()
.ml_neg_px()
.h(rems(0.875))
.items_center()
.overflow_hidden()
.px_0p5()
.child(
Label::new(count.to_string())
.size(LabelSize::XSmall)
.line_height_style(LineHeightStyle::UiLabel),
)
}
let should_render_counts = left_icon.is_none() && (ahead_count > 0 || behind_count > 0);
let left = ui::ButtonLike::new_rounded_left(ElementId::Name(
format!("split-button-left-{}", id).into(),
))
.layer(ui::ElevationIndex::ModalSurface)
.size(ui::ButtonSize::Compact)
.when(should_render_counts, |this| {
this.child(
h_flex()
.ml_neg_0p5()
.mr_1()
.when(behind_count > 0, |this| {
this.child(Icon::new(IconName::ArrowDown).size(IconSize::XSmall))
.child(count(behind_count))
})
.when(ahead_count > 0, |this| {
this.child(Icon::new(IconName::ArrowUp).size(IconSize::XSmall))
.child(count(ahead_count))
}),
)
})
.when_some(left_icon, |this, left_icon| {
this.child(
h_flex()
.ml_neg_0p5()
.mr_1()
.child(Icon::new(left_icon).size(IconSize::XSmall)),
)
})
.child(
div()
.child(Label::new(left_label).size(LabelSize::Small))
.mr_0p5(),
)
.on_click(left_on_click)
.tooltip(tooltip);
let right = render_git_action_menu(
ElementId::Name(format!("split-button-right-{}", id).into()),
keybinding_target,
)
.into_any_element();
Self { left, right }
}
}
impl RenderOnce for SplitButton {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
h_flex()
.rounded_sm()
.border_1()
.border_color(cx.theme().colors().text_muted.alpha(0.12))
.child(div().flex_grow().child(self.left))
.child(
div()
.h_full()
.w_px()
.bg(cx.theme().colors().text_muted.alpha(0.16)),
)
.child(self.right)
.bg(ElevationIndex::Surface.on_elevation_bg(cx))
.shadow(smallvec::smallvec![BoxShadow {
color: hsla(0.0, 0.0, 0.0, 0.16),
offset: point(px(0.), px(1.)),
blur_radius: px(0.),
spread_radius: px(0.),
}])
}
}
}
#[derive(IntoElement, IntoComponent)]
#[component(scope = "Version Control")]
pub struct GitStatusIcon {
status: FileStatus,
}
impl GitStatusIcon {
pub fn new(status: FileStatus) -> Self {
Self { status }
}
}
impl RenderOnce for GitStatusIcon {
fn render(self, _window: &mut ui::Window, cx: &mut App) -> impl IntoElement {
let status = self.status;
let (icon_name, color) = if status.is_conflicted() {
(
IconName::Warning,
cx.theme().colors().version_control_conflict,
)
} else if status.is_deleted() {
(
IconName::SquareMinus,
cx.theme().colors().version_control_deleted,
)
} else if status.is_modified() {
(
IconName::SquareDot,
cx.theme().colors().version_control_modified,
)
} else {
(
IconName::SquarePlus,
cx.theme().colors().version_control_added,
)
};
Icon::new(icon_name).color(Color::Custom(color))
}
}
// View this component preview using `workspace: open component-preview`
impl ComponentPreview for GitStatusIcon {
fn preview(_window: &mut Window, _cx: &mut App) -> AnyElement {
fn tracked_file_status(code: StatusCode) -> FileStatus {
FileStatus::Tracked(git::status::TrackedStatus {
index_status: code,
worktree_status: code,
})
}
let modified = tracked_file_status(StatusCode::Modified);
let added = tracked_file_status(StatusCode::Added);
let deleted = tracked_file_status(StatusCode::Deleted);
let conflict = UnmergedStatus {
first_head: UnmergedStatusCode::Updated,
second_head: UnmergedStatusCode::Updated,
}
.into();
v_flex()
.gap_6()
.children(vec![example_group(vec![
single_example("Modified", GitStatusIcon::new(modified).into_any_element()),
single_example("Added", GitStatusIcon::new(added).into_any_element()),
single_example("Deleted", GitStatusIcon::new(deleted).into_any_element()),
single_example(
"Conflicted",
GitStatusIcon::new(conflict).into_any_element(),
),
])])
.into_any_element()
}
Icon::new(icon_name).color(Color::Custom(color))
}

View File

@@ -7,10 +7,10 @@ use editor::{
scroll::Autoscroll,
Editor, EditorEvent,
};
use feature_flags::FeatureFlagViewExt;
use futures::StreamExt;
use git::{
repository::Branch, status::FileStatus, Commit, StageAll, StageAndNext, ToggleStaged,
UnstageAll, UnstageAndNext,
status::FileStatus, Commit, StageAll, StageAndNext, ToggleStaged, UnstageAll, UnstageAndNext,
};
use gpui::{
actions, Action, AnyElement, AnyView, App, AppContext as _, AsyncWindowContext, Entity,
@@ -24,27 +24,27 @@ use project::{
};
use std::any::{Any, TypeId};
use theme::ActiveTheme;
use ui::{prelude::*, vertical_divider, KeyBinding, Tooltip};
use ui::{prelude::*, vertical_divider, Tooltip};
use util::ResultExt as _;
use workspace::{
item::{BreadcrumbText, Item, ItemEvent, ItemHandle, TabContentParams},
searchable::SearchableItemHandle,
CloseActiveItem, ItemNavHistory, SerializableItem, ToolbarItemEvent, ToolbarItemLocation,
ToolbarItemView, Workspace,
ItemNavHistory, SerializableItem, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView,
Workspace,
};
actions!(git, [Diff, Add]);
actions!(git, [Diff]);
pub struct ProjectDiff {
project: Entity<Project>,
multibuffer: Entity<MultiBuffer>,
editor: Entity<Editor>,
project: Entity<Project>,
git_store: Entity<GitStore>,
workspace: WeakEntity<Workspace>,
focus_handle: FocusHandle,
update_needed: postage::watch::Sender<()>,
pending_scroll: Option<PathKey>,
current_branch: Option<Branch>,
_task: Task<Result<()>>,
_subscription: Subscription,
}
@@ -63,13 +63,13 @@ const NEW_NAMESPACE: &'static str = "2";
impl ProjectDiff {
pub(crate) fn register(
workspace: &mut Workspace,
_window: Option<&mut Window>,
_: &mut Workspace,
window: Option<&mut Window>,
cx: &mut Context<Workspace>,
) {
workspace.register_action(Self::deploy);
workspace.register_action(|workspace, _: &Add, window, cx| {
Self::deploy(workspace, &Diff, window, cx);
let Some(window) = window else { return };
cx.when_flag_enabled::<feature_flags::GitUiFeatureFlag>(window, |workspace, _, _cx| {
workspace.register_action(Self::deploy);
});
workspace::register_serializable_item::<ProjectDiff>(cx);
@@ -121,12 +121,6 @@ impl ProjectDiff {
}
}
pub fn autoscroll(&self, cx: &mut Context<Self>) {
self.editor.update(cx, |editor, cx| {
editor.request_autoscroll(Autoscroll::fit(), cx);
})
}
fn new(
project: Entity<Project>,
workspace: Entity<Workspace>,
@@ -144,7 +138,6 @@ impl ProjectDiff {
window,
cx,
);
diff_display_editor.disable_inline_diagnostics();
diff_display_editor.set_expand_all_diff_hunks(cx);
diff_display_editor.register_addon(GitPanelAddon {
workspace: workspace.downgrade(),
@@ -185,7 +178,6 @@ impl ProjectDiff {
multibuffer,
pending_scroll: None,
update_needed: send,
current_branch: None,
_task: worker,
_subscription: git_store_subscription,
}
@@ -451,20 +443,6 @@ impl ProjectDiff {
mut cx: AsyncWindowContext,
) -> Result<()> {
while let Some(_) = recv.next().await {
this.update(&mut cx, |this, cx| {
let new_branch =
this.git_store
.read(cx)
.active_repository()
.and_then(|active_repository| {
active_repository.read(cx).current_branch().cloned()
});
if new_branch != this.current_branch {
this.current_branch = new_branch;
cx.notify();
}
})?;
let buffers_to_load = this.update(&mut 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() {
@@ -474,10 +452,7 @@ impl ProjectDiff {
})?;
}
}
this.update(&mut cx, |this, cx| {
this.pending_scroll.take();
cx.notify();
})?;
this.update(&mut cx, |this, _| this.pending_scroll.take())?;
}
Ok(())
@@ -666,11 +641,9 @@ impl Item for ProjectDiff {
}
impl Render for ProjectDiff {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let is_empty = self.multibuffer.read(cx).is_empty();
let can_push_and_pull = crate::can_push_and_pull(&self.project, cx);
div()
.track_focus(&self.focus_handle)
.key_context(if is_empty { "EmptyPane" } else { "GitDiff" })
@@ -680,61 +653,7 @@ impl Render for ProjectDiff {
.justify_center()
.size_full()
.when(is_empty, |el| {
el.child(
v_flex()
.gap_1()
.child(
h_flex()
.justify_around()
.child(Label::new("No uncommitted changes")),
)
.when(can_push_and_pull, |this_div| {
let keybinding_focus_handle = self.focus_handle(cx);
this_div.when_some(self.current_branch.as_ref(), |this_div, branch| {
let remote_button = crate::render_remote_button(
"project-diff-remote-button",
branch,
Some(keybinding_focus_handle.clone()),
false,
);
match remote_button {
Some(button) => {
this_div.child(h_flex().justify_around().child(button))
}
None => this_div.child(
h_flex()
.justify_around()
.child(Label::new("Remote up to date")),
),
}
})
})
.map(|this| {
let keybinding_focus_handle = self.focus_handle(cx).clone();
this.child(
h_flex().justify_around().mt_1().child(
Button::new("project-diff-close-button", "Close")
// .style(ButtonStyle::Transparent)
.key_binding(KeyBinding::for_action_in(
&CloseActiveItem::default(),
&keybinding_focus_handle,
window,
cx,
))
.on_click(move |_, window, cx| {
window.focus(&keybinding_focus_handle);
window.dispatch_action(
Box::new(CloseActiveItem::default()),
cx,
);
}),
),
)
}),
)
el.child(Label::new("No uncommitted changes"))
})
.when(!is_empty, |el| el.child(self.editor.clone()))
}

View File

@@ -152,10 +152,7 @@ impl GoToLine {
cx: &mut Context<Self>,
) {
match event {
editor::EditorEvent::Blurred => {
self.prev_scroll_position.take();
cx.emit(DismissEvent)
}
editor::EditorEvent::Blurred => cx.emit(DismissEvent),
editor::EditorEvent::BufferEdited { .. } => self.highlight_current_line(cx),
_ => {}
}

View File

@@ -207,7 +207,7 @@ blade-macros.workspace = true
flume = "0.11"
rand.workspace = true
windows.workspace = true
windows-core = "0.60"
windows-core = "0.58"
[dev-dependencies]
backtrace = "0.3"

View File

@@ -178,7 +178,6 @@ impl EntityMap {
}
}
#[track_caller]
fn double_lease_panic<T>(operation: &str) -> ! {
panic!(
"cannot {operation} {} while it is already being updated",

View File

@@ -634,7 +634,7 @@ impl Display for ColorSpace {
}
/// A background color, which can be either a solid color or a linear gradient.
#[derive(Clone, Copy, PartialEq)]
#[derive(Debug, Clone, Copy, PartialEq)]
#[repr(C)]
pub struct Background {
pub(crate) tag: BackgroundTag,
@@ -646,28 +646,6 @@ pub struct Background {
pad: u32,
}
impl std::fmt::Debug for Background {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self.tag {
BackgroundTag::Solid => write!(f, "Solid({:?})", self.solid),
BackgroundTag::LinearGradient => {
write!(
f,
"LinearGradient({}, {:?}, {:?})",
self.gradient_angle_or_pattern_height, self.colors[0], self.colors[1]
)
}
BackgroundTag::PatternSlash => {
write!(
f,
"PatternSlash({:?}, {})",
self.solid, self.gradient_angle_or_pattern_height
)
}
}
}
}
impl Eq for Background {}
impl Default for Background {
fn default() -> Self {
@@ -683,11 +661,11 @@ impl Default for Background {
}
/// Creates a hash pattern background
pub fn pattern_slash(color: Hsla, height: f32) -> Background {
pub fn pattern_slash(color: Hsla, thickness: f32) -> Background {
Background {
tag: BackgroundTag::PatternSlash,
solid: color,
gradient_angle_or_pattern_height: height,
gradient_angle_or_pattern_height: thickness,
..Default::default()
}
}

View File

@@ -76,9 +76,8 @@ impl Keystroke {
}
/// key syntax is:
/// [secondary-][ctrl-][alt-][shift-][cmd-][fn-]key[->key_char]
/// [ctrl-][alt-][shift-][cmd-][fn-]key[->key_char]
/// key_char syntax is only used for generating test events,
/// secondary means "cmd" on macOS and "ctrl" on other platforms
/// when matching a key with an key_char set will be matched without it.
pub fn parse(source: &str) -> std::result::Result<Self, InvalidKeystrokeError> {
let mut control = false;
@@ -96,13 +95,6 @@ impl Keystroke {
"alt" => alt = true,
"shift" => shift = true,
"fn" => function = true,
"secondary" => {
if cfg!(target_os = "macos") {
platform = true
} else {
control = true
};
}
"cmd" | "super" | "win" => platform = true,
_ => {
if let Some(next) = components.peek() {

View File

@@ -115,6 +115,7 @@ pub struct WaylandWindowStatePtr {
}
impl WaylandWindowState {
#[allow(clippy::too_many_arguments)]
pub(crate) fn new(
handle: AnyWindowHandle,
surface: wl_surface::WlSurface,

View File

@@ -353,6 +353,7 @@ where
}
impl X11WindowState {
#[allow(clippy::too_many_arguments)]
pub fn new(
handle: AnyWindowHandle,
client: X11ClientStatePtr,
@@ -711,6 +712,7 @@ enum WmHintPropertyState {
}
impl X11Window {
#[allow(clippy::too_many_arguments)]
pub fn new(
handle: AnyWindowHandle,
client: X11ClientStatePtr,

View File

@@ -347,7 +347,20 @@ impl MacPlatform {
msg_send![item, setAllowsAutomaticKeyEquivalentLocalization: NO];
}
item.setKeyEquivalentModifierMask_(mask);
} else {
}
// For multi-keystroke bindings, render the keystroke as part of the title.
else {
use std::fmt::Write;
let mut name = format!("{name} [");
for (i, keystroke) in keystrokes.iter().enumerate() {
if i > 0 {
name.push(' ');
}
write!(&mut name, "{}", keystroke).unwrap();
}
name.push(']');
item = NSMenuItem::alloc(nil)
.initWithTitle_action_keyEquivalent_(
ns_string(&name),

View File

@@ -268,10 +268,6 @@ unsafe fn build_window_class(name: &'static str, superclass: &Class) -> *const C
sel!(windowWillEnterFullScreen:),
window_will_enter_fullscreen as extern "C" fn(&Object, Sel, id),
);
decl.add_method(
sel!(windowWillExitFullScreen:),
window_will_exit_fullscreen as extern "C" fn(&Object, Sel, id),
);
decl.add_method(
sel!(windowDidMove:),
window_did_move as extern "C" fn(&Object, Sel, id),
@@ -338,7 +334,6 @@ struct MacWindowState {
last_key_equivalent: Option<KeyDownEvent>,
synthetic_drag_counter: usize,
traffic_light_position: Option<Point<Pixels>>,
transparent_titlebar: bool,
previous_modifiers_changed_event: Option<PlatformInput>,
keystroke_for_do_command: Option<Keystroke>,
do_command_handled: Option<bool>,
@@ -618,9 +613,6 @@ impl MacWindow {
traffic_light_position: titlebar
.as_ref()
.and_then(|titlebar| titlebar.traffic_light_position),
transparent_titlebar: titlebar
.as_ref()
.map_or(true, |titlebar| titlebar.appears_transparent),
previous_modifiers_changed_event: None,
keystroke_for_do_command: None,
do_command_handled: None,
@@ -1498,19 +1490,6 @@ extern "C" fn window_will_enter_fullscreen(this: &Object, _: Sel, _: id) {
let window_state = unsafe { get_window_state(this) };
let mut lock = window_state.as_ref().lock();
lock.fullscreen_restore_bounds = lock.bounds();
unsafe {
lock.native_window.setTitlebarAppearsTransparent_(NO);
}
}
extern "C" fn window_will_exit_fullscreen(this: &Object, _: Sel, _: id) {
let window_state = unsafe { get_window_state(this) };
let mut lock = window_state.as_ref().lock();
if lock.transparent_titlebar {
unsafe {
lock.native_window.setTitlebarAppearsTransparent_(YES);
}
}
}
extern "C" fn window_did_move(this: &Object, _: Sel, _: id) {

View File

@@ -170,7 +170,7 @@ fn set_data_to_clipboard<T>(data: &[T], format: u32) -> Result<()> {
let handle = GlobalLock(global);
std::ptr::copy_nonoverlapping(data.as_ptr(), handle as _, data.len());
let _ = GlobalUnlock(global);
SetClipboardData(format, Some(HANDLE(global.0)))?;
SetClipboardData(format, HANDLE(global.0))?;
}
Ok(())
}

View File

@@ -1049,7 +1049,7 @@ impl IDWriteTextRenderer_Impl for TextRenderer_Impl {
_measuringmode: DWRITE_MEASURING_MODE,
glyphrun: *const DWRITE_GLYPH_RUN,
glyphrundescription: *const DWRITE_GLYPH_RUN_DESCRIPTION,
_clientdrawingeffect: windows::core::Ref<windows::core::IUnknown>,
_clientdrawingeffect: Option<&windows::core::IUnknown>,
) -> windows::core::Result<()> {
unsafe {
let glyphrun = &*glyphrun;
@@ -1113,7 +1113,7 @@ impl IDWriteTextRenderer_Impl for TextRenderer_Impl {
_baselineoriginx: f32,
_baselineoriginy: f32,
_underline: *const DWRITE_UNDERLINE,
_clientdrawingeffect: windows::core::Ref<windows::core::IUnknown>,
_clientdrawingeffect: Option<&windows::core::IUnknown>,
) -> windows::core::Result<()> {
Err(windows::core::Error::new(
E_NOTIMPL,
@@ -1127,7 +1127,7 @@ impl IDWriteTextRenderer_Impl for TextRenderer_Impl {
_baselineoriginx: f32,
_baselineoriginy: f32,
_strikethrough: *const DWRITE_STRIKETHROUGH,
_clientdrawingeffect: windows::core::Ref<windows::core::IUnknown>,
_clientdrawingeffect: Option<&windows::core::IUnknown>,
) -> windows::core::Result<()> {
Err(windows::core::Error::new(
E_NOTIMPL,
@@ -1140,10 +1140,10 @@ impl IDWriteTextRenderer_Impl for TextRenderer_Impl {
_clientdrawingcontext: *const ::core::ffi::c_void,
_originx: f32,
_originy: f32,
_inlineobject: windows::core::Ref<IDWriteInlineObject>,
_inlineobject: Option<&IDWriteInlineObject>,
_issideways: BOOL,
_isrighttoleft: BOOL,
_clientdrawingeffect: windows::core::Ref<windows::core::IUnknown>,
_clientdrawingeffect: Option<&windows::core::IUnknown>,
) -> windows::core::Result<()> {
Err(windows::core::Error::new(
E_NOTIMPL,

View File

@@ -215,7 +215,7 @@ fn available_monitors() -> SmallVec<[HMONITOR; 4]> {
let mut monitors: SmallVec<[HMONITOR; 4]> = SmallVec::new();
unsafe {
EnumDisplayMonitors(
None,
HDC::default(),
None,
Some(monitor_enum_proc),
LPARAM(&mut monitors as *mut _ as _),

View File

@@ -177,12 +177,7 @@ fn handle_size_msg(
fn handle_size_move_loop(handle: HWND) -> Option<isize> {
unsafe {
let ret = SetTimer(
Some(handle),
SIZE_MOVE_LOOP_TIMER_ID,
USER_TIMER_MINIMUM,
None,
);
let ret = SetTimer(handle, SIZE_MOVE_LOOP_TIMER_ID, USER_TIMER_MINIMUM, None);
if ret == 0 {
log::error!(
"unable to create timer: {}",
@@ -195,7 +190,7 @@ fn handle_size_move_loop(handle: HWND) -> Option<isize> {
fn handle_size_move_loop_exit(handle: HWND) -> Option<isize> {
unsafe {
KillTimer(Some(handle), SIZE_MOVE_LOOP_TIMER_ID).log_err();
KillTimer(handle, SIZE_MOVE_LOOP_TIMER_ID).log_err();
}
None
}
@@ -222,7 +217,7 @@ fn handle_paint_msg(handle: HWND, state_ptr: Rc<WindowsWindowStatePtr>) -> Optio
request_frame(Default::default());
state_ptr.state.borrow_mut().callbacks.request_frame = Some(request_frame);
}
unsafe { ValidateRect(Some(handle), None).ok().log_err() };
unsafe { ValidateRect(handle, None).ok().log_err() };
Some(0)
}
@@ -781,7 +776,7 @@ fn handle_activate_msg(
if state_ptr.hide_title_bar {
if let Some(titlebar_rect) = state_ptr.state.borrow().get_titlebar_rect().log_err() {
unsafe {
InvalidateRect(Some(handle), Some(&titlebar_rect), false)
InvalidateRect(handle, Some(&titlebar_rect), FALSE)
.ok()
.log_err()
};
@@ -1110,7 +1105,7 @@ fn handle_nc_mouse_up_msg(
HTCLOSE => {
if last_button == HTCLOSE {
unsafe {
PostMessageW(Some(handle), WM_CLOSE, WPARAM::default(), LPARAM::default())
PostMessageW(handle, WM_CLOSE, WPARAM::default(), LPARAM::default())
.log_err()
};
handled = true;
@@ -1138,7 +1133,7 @@ fn handle_set_cursor(lparam: LPARAM, state_ptr: Rc<WindowsWindowStatePtr>) -> Op
) {
return None;
}
unsafe { SetCursor(Some(state_ptr.state.borrow().current_cursor)) };
unsafe { SetCursor(state_ptr.state.borrow().current_cursor) };
Some(1)
}

Some files were not shown because too many files have changed in this diff Show More