Compare commits
26 Commits
new-ui-lis
...
actual-fs
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
106fca327b | ||
|
|
ed6bf7f161 | ||
|
|
f14d6670ba | ||
|
|
22d9b5d8ca | ||
|
|
6ed6e8bc26 | ||
|
|
4846e6fb3a | ||
|
|
cb543f9546 | ||
|
|
450d727a04 | ||
|
|
60b3eb3f76 | ||
|
|
bbe7c9a738 | ||
|
|
f6345a6995 | ||
|
|
e70d0edfac | ||
|
|
921c24e274 | ||
|
|
18f3f8097f | ||
|
|
4f6682c7fe | ||
|
|
f57dece2d5 | ||
|
|
103ad635d9 | ||
|
|
ec5e7a2653 | ||
|
|
05d3ee8555 | ||
|
|
1b34437839 | ||
|
|
3ff2c8fc38 | ||
|
|
b0b0b00fae | ||
|
|
80fb88520f | ||
|
|
aef84d453a | ||
|
|
e06d010aab | ||
|
|
14148f53d4 |
14
Cargo.lock
generated
14
Cargo.lock
generated
@@ -658,9 +658,9 @@ dependencies = [
|
||||
"derive_more",
|
||||
"gpui",
|
||||
"parking_lot",
|
||||
"project",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"workspace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -675,7 +675,6 @@ dependencies = [
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"workspace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3047,6 +3046,7 @@ name = "component_preview"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"client",
|
||||
"collections",
|
||||
"component",
|
||||
"gpui",
|
||||
"languages",
|
||||
@@ -3139,7 +3139,6 @@ dependencies = [
|
||||
"smol",
|
||||
"url",
|
||||
"util",
|
||||
"workspace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4536,6 +4535,7 @@ dependencies = [
|
||||
"async-tar",
|
||||
"async-trait",
|
||||
"collections",
|
||||
"convert_case 0.8.0",
|
||||
"fs",
|
||||
"futures 0.3.31",
|
||||
"gpui",
|
||||
@@ -11928,7 +11928,6 @@ dependencies = [
|
||||
"serde_json",
|
||||
"settings",
|
||||
"util",
|
||||
"workspace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -17070,13 +17069,6 @@ dependencies = [
|
||||
"zed_extension_api 0.1.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zed_haskell"
|
||||
version = "0.1.3"
|
||||
dependencies = [
|
||||
"zed_extension_api 0.1.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zed_html"
|
||||
version = "0.1.6"
|
||||
|
||||
@@ -171,7 +171,6 @@ members = [
|
||||
|
||||
"extensions/emmet",
|
||||
"extensions/glsl",
|
||||
"extensions/haskell",
|
||||
"extensions/html",
|
||||
"extensions/perplexity",
|
||||
"extensions/proto",
|
||||
|
||||
3
assets/icons/file_icons/luau.svg
Normal file
3
assets/icons/file_icons/luau.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<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>
|
||||
|
After Width: | Height: | Size: 707 B |
@@ -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::MoveToStartOfExcerpt",
|
||||
"cmd-down": "editor::MoveToEndOfExcerpt",
|
||||
"cmd-up": "editor::MoveToBeginning",
|
||||
"cmd-down": "editor::MoveToEnd",
|
||||
"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::SelectToStartOfExcerpt",
|
||||
"cmd-shift-down": "editor::SelectToEndOfExcerpt",
|
||||
"cmd-shift-up": "editor::SelectToBeginning",
|
||||
"cmd-shift-down": "editor::SelectToEnd",
|
||||
"cmd-a": "editor::SelectAll",
|
||||
"cmd-l": "editor::SelectLine",
|
||||
"cmd-shift-i": "editor::Format",
|
||||
@@ -172,6 +172,16 @@
|
||||
"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,
|
||||
|
||||
@@ -845,7 +845,7 @@
|
||||
// "hunk_style": "transparent"
|
||||
// 2. Show unstaged hunks with a pattern background:
|
||||
// "hunk_style": "pattern"
|
||||
"hunk_style": "transparent"
|
||||
"hunk_style": "staged_border"
|
||||
},
|
||||
// Configuration for how direnv configuration should be loaded. May take 2 values:
|
||||
// 1. Load direnv configuration using `direnv export json` directly.
|
||||
|
||||
@@ -9,7 +9,10 @@ use gpui::{
|
||||
};
|
||||
use language::{LanguageRegistry, LanguageServerBinaryStatus, LanguageServerId};
|
||||
use lsp::LanguageServerName;
|
||||
use project::{EnvironmentErrorMessage, LanguageServerProgress, Project, WorktreeId};
|
||||
use project::{
|
||||
EnvironmentErrorMessage, LanguageServerProgress, LspStoreEvent, Project,
|
||||
ProjectEnvironmentEvent, WorktreeId,
|
||||
};
|
||||
use smallvec::SmallVec;
|
||||
use std::{cmp::Reverse, fmt::Write, sync::Arc, time::Duration};
|
||||
use ui::{prelude::*, ButtonLike, ContextMenu, PopoverMenu, PopoverMenuHandle, Tooltip};
|
||||
@@ -73,7 +76,22 @@ impl ActivityIndicator {
|
||||
})
|
||||
.detach();
|
||||
|
||||
cx.observe(&project, |_, _, cx| cx.notify()).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();
|
||||
|
||||
if let Some(auto_updater) = auto_updater.as_ref() {
|
||||
cx.observe(auto_updater, |_, _, cx| cx.notify()).detach();
|
||||
@@ -204,7 +222,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(cx, worktree_id);
|
||||
project.remove_environment_error(worktree_id, cx);
|
||||
});
|
||||
window.dispatch_action(Box::new(workspace::OpenLog), cx);
|
||||
})),
|
||||
|
||||
@@ -38,7 +38,7 @@ use language_model::{
|
||||
use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu};
|
||||
use multi_buffer::MultiBufferRow;
|
||||
use parking_lot::Mutex;
|
||||
use project::{ActionVariant, CodeAction, ProjectTransaction};
|
||||
use project::{CodeAction, LspAction, ProjectTransaction};
|
||||
use prompt_store::PromptBuilder;
|
||||
use rope::Rope;
|
||||
use settings::{update_settings_file, Settings, SettingsStore};
|
||||
@@ -3569,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: ActionVariant::Action(Box::new(lsp::CodeAction {
|
||||
lsp_action: LspAction::Action(Box::new(lsp::CodeAction {
|
||||
title: "Fix with Assistant".into(),
|
||||
..Default::default()
|
||||
})),
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
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, Length, ListAlignment, ListOffset, ListState, StyleRefinement, Subscription,
|
||||
Task, TextStyleRefinement, UnderlineStyle, WeakEntity,
|
||||
Task, TextStyleRefinement, UnderlineStyle,
|
||||
};
|
||||
use language::{Buffer, LanguageRegistry};
|
||||
use language_model::{LanguageModelRegistry, LanguageModelToolUseId, Role};
|
||||
@@ -15,7 +14,6 @@ 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;
|
||||
@@ -23,9 +21,7 @@ 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<()>>,
|
||||
@@ -46,9 +42,7 @@ 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 {
|
||||
@@ -58,9 +52,7 @@ impl ActiveThread {
|
||||
];
|
||||
|
||||
let mut this = Self {
|
||||
workspace,
|
||||
language_registry,
|
||||
tools,
|
||||
thread_store,
|
||||
thread: thread.clone(),
|
||||
save_thread_task: None,
|
||||
@@ -300,46 +292,16 @@ impl ActiveThread {
|
||||
cx.notify();
|
||||
}
|
||||
ThreadEvent::UsePendingTools => {
|
||||
let pending_tool_uses = self
|
||||
.thread
|
||||
.read(cx)
|
||||
.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.workspace.clone(), window, cx);
|
||||
|
||||
self.thread.update(cx, |thread, cx| {
|
||||
thread.insert_tool_output(tool_use.id.clone(), task, cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
self.thread.update(cx, |thread, cx| {
|
||||
thread.use_pending_tools(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 {
|
||||
if self.thread.read(cx).all_tools_finished() {
|
||||
let model_registry = LanguageModelRegistry::read_global(cx);
|
||||
if let Some(model) = model_registry.active_model() {
|
||||
self.thread.update(cx, |thread, 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);
|
||||
thread.send_tool_results_to_model(model, cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,7 +92,6 @@ 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>,
|
||||
@@ -133,7 +132,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, tools, window, cx))
|
||||
cx.new(|cx| Self::new(workspace, thread_store, context_store, window, cx))
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -142,7 +141,6 @@ 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 {
|
||||
@@ -170,8 +168,8 @@ impl AssistantPanel {
|
||||
|
||||
Self {
|
||||
active_view: ActiveView::Thread,
|
||||
workspace: workspace.clone(),
|
||||
project,
|
||||
workspace,
|
||||
project: project.clone(),
|
||||
fs: fs.clone(),
|
||||
language_registry: language_registry.clone(),
|
||||
thread_store: thread_store.clone(),
|
||||
@@ -179,9 +177,7 @@ impl AssistantPanel {
|
||||
ActiveThread::new(
|
||||
thread.clone(),
|
||||
thread_store.clone(),
|
||||
workspace,
|
||||
language_registry,
|
||||
tools.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
@@ -191,7 +187,6 @@ impl AssistantPanel {
|
||||
context_editor: None,
|
||||
configuration: None,
|
||||
configuration_subscription: None,
|
||||
tools,
|
||||
local_timezone: UtcOffset::from_whole_seconds(
|
||||
chrono::Local::now().offset().local_minus_utc(),
|
||||
)
|
||||
@@ -246,9 +241,7 @@ impl AssistantPanel {
|
||||
ActiveThread::new(
|
||||
thread.clone(),
|
||||
self.thread_store.clone(),
|
||||
self.workspace.clone(),
|
||||
self.language_registry.clone(),
|
||||
self.tools.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
@@ -381,9 +374,7 @@ impl AssistantPanel {
|
||||
ActiveThread::new(
|
||||
thread.clone(),
|
||||
this.thread_store.clone(),
|
||||
this.workspace.clone(),
|
||||
this.language_registry.clone(),
|
||||
this.tools.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
|
||||
@@ -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::ActionVariant;
|
||||
use project::LspAction;
|
||||
use project::{CodeAction, ProjectTransaction};
|
||||
use prompt_store::PromptBuilder;
|
||||
use settings::{Settings, SettingsStore};
|
||||
@@ -1728,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: ActionVariant::Action(Box::new(lsp::CodeAction {
|
||||
lsp_action: LspAction::Action(Box::new(lsp::CodeAction {
|
||||
title: "Fix with Assistant".into(),
|
||||
..Default::default()
|
||||
})),
|
||||
|
||||
@@ -5,13 +5,14 @@ use assistant_tool::ToolWorkingSet;
|
||||
use chrono::{DateTime, Utc};
|
||||
use collections::{BTreeMap, HashMap, HashSet};
|
||||
use futures::StreamExt as _;
|
||||
use gpui::{App, Context, EventEmitter, SharedString, Task};
|
||||
use gpui::{App, Context, Entity, EventEmitter, SharedString, Task};
|
||||
use language_model::{
|
||||
LanguageModel, LanguageModelCompletionEvent, LanguageModelRegistry, LanguageModelRequest,
|
||||
LanguageModelRequestMessage, LanguageModelRequestTool, LanguageModelToolResult,
|
||||
LanguageModelToolUseId, MaxMonthlySpendReachedError, MessageContent, PaymentRequiredError,
|
||||
Role, StopReason,
|
||||
};
|
||||
use project::Project;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use util::{post_inc, TryFutureExt as _};
|
||||
use uuid::Uuid;
|
||||
@@ -71,12 +72,17 @@ 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,
|
||||
}
|
||||
|
||||
impl Thread {
|
||||
pub fn new(tools: Arc<ToolWorkingSet>, _cx: &mut Context<Self>) -> Self {
|
||||
pub fn new(
|
||||
project: Entity<Project>,
|
||||
tools: Arc<ToolWorkingSet>,
|
||||
_cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
Self {
|
||||
id: ThreadId::new(),
|
||||
updated_at: Utc::now(),
|
||||
@@ -88,6 +94,7 @@ impl Thread {
|
||||
context_by_message: HashMap::default(),
|
||||
completion_count: 0,
|
||||
pending_completions: Vec::new(),
|
||||
project,
|
||||
tools,
|
||||
tool_use: ToolUseState::new(),
|
||||
}
|
||||
@@ -96,6 +103,7 @@ impl Thread {
|
||||
pub fn from_saved(
|
||||
id: ThreadId,
|
||||
saved: SavedThread,
|
||||
project: Entity<Project>,
|
||||
tools: Arc<ToolWorkingSet>,
|
||||
_cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
@@ -127,6 +135,7 @@ impl Thread {
|
||||
context_by_message: HashMap::default(),
|
||||
completion_count: 0,
|
||||
pending_completions: Vec::new(),
|
||||
project,
|
||||
tools,
|
||||
tool_use,
|
||||
}
|
||||
@@ -193,6 +202,15 @@ impl Thread {
|
||||
self.tool_use.pending_tool_uses()
|
||||
}
|
||||
|
||||
/// Returns whether all of the tool uses have finished running.
|
||||
pub fn all_tools_finished(&self) -> bool {
|
||||
// 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.
|
||||
self.pending_tool_uses()
|
||||
.into_iter()
|
||||
.all(|tool_use| tool_use.status.is_error())
|
||||
}
|
||||
|
||||
pub fn tool_uses_for_message(&self, id: MessageId) -> Vec<ToolUse> {
|
||||
self.tool_use.tool_uses_for_message(id)
|
||||
}
|
||||
@@ -550,6 +568,23 @@ impl Thread {
|
||||
});
|
||||
}
|
||||
|
||||
pub fn use_pending_tools(&mut self, cx: &mut Context<Self>) {
|
||||
let pending_tool_uses = self
|
||||
.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert_tool_output(
|
||||
&mut self,
|
||||
tool_use_id: LanguageModelToolUseId,
|
||||
@@ -576,6 +611,23 @@ impl Thread {
|
||||
.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.
|
||||
|
||||
@@ -26,7 +26,6 @@ pub fn init(cx: &mut App) {
|
||||
}
|
||||
|
||||
pub struct ThreadStore {
|
||||
#[allow(unused)]
|
||||
project: Entity<Project>,
|
||||
tools: Arc<ToolWorkingSet>,
|
||||
context_server_manager: Entity<ContextServerManager>,
|
||||
@@ -78,7 +77,7 @@ impl ThreadStore {
|
||||
}
|
||||
|
||||
pub fn create_thread(&mut self, cx: &mut Context<Self>) -> Entity<Thread> {
|
||||
cx.new(|cx| Thread::new(self.tools.clone(), cx))
|
||||
cx.new(|cx| Thread::new(self.project.clone(), self.tools.clone(), cx))
|
||||
}
|
||||
|
||||
pub fn open_thread(
|
||||
@@ -96,7 +95,15 @@ 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.tools.clone(), cx))
|
||||
cx.new(|cx| {
|
||||
Thread::from_saved(
|
||||
id.clone(),
|
||||
thread,
|
||||
this.project.clone(),
|
||||
this.tools.clone(),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -104,49 +104,53 @@ 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();
|
||||
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(())
|
||||
}
|
||||
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
|
||||
})?;
|
||||
.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
|
||||
})?;
|
||||
|
||||
Ok(this)
|
||||
})
|
||||
@@ -288,7 +292,7 @@ impl ContextStore {
|
||||
})?
|
||||
}
|
||||
|
||||
fn handle_project_changed(&mut self, _: Entity<Project>, cx: &mut Context<Self>) {
|
||||
fn handle_project_shared(&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 {
|
||||
@@ -318,11 +322,14 @@ impl ContextStore {
|
||||
|
||||
fn handle_project_event(
|
||||
&mut self,
|
||||
_: Entity<Project>,
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -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, LanguageServerId, ToPoint};
|
||||
use language::{Anchor, Buffer, ToPoint};
|
||||
use parking_lot::Mutex;
|
||||
use project::{lsp_store::CompletionDocumentation, CompletionIntent};
|
||||
use project::{lsp_store::CompletionDocumentation, CompletionIntent, CompletionSource};
|
||||
use rope::Point;
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
@@ -125,10 +125,8 @@ impl SlashCommandCompletionProvider {
|
||||
)),
|
||||
new_text,
|
||||
label: command.label(cx),
|
||||
server_id: LanguageServerId(0),
|
||||
lsp_completion: Default::default(),
|
||||
confirm,
|
||||
resolved: true,
|
||||
source: CompletionSource::Custom,
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
@@ -225,10 +223,8 @@ impl SlashCommandCompletionProvider {
|
||||
label: new_argument.label,
|
||||
new_text,
|
||||
documentation: None,
|
||||
server_id: LanguageServerId(0),
|
||||
lsp_completion: Default::default(),
|
||||
confirm,
|
||||
resolved: true,
|
||||
source: CompletionSource::Custom,
|
||||
}
|
||||
})
|
||||
.collect())
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -4,8 +4,8 @@ mod tool_working_set;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use gpui::{App, Task, WeakEntity, Window};
|
||||
use workspace::Workspace;
|
||||
use gpui::{App, Entity, Task};
|
||||
use project::Project;
|
||||
|
||||
pub use crate::tool_registry::*;
|
||||
pub use crate::tool_working_set::*;
|
||||
@@ -31,8 +31,7 @@ pub trait Tool: 'static + Send + Sync {
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
input: serde_json::Value,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
window: &mut Window,
|
||||
project: Entity<Project>,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<String>>;
|
||||
}
|
||||
|
||||
@@ -20,4 +20,3 @@ project.workspace = true
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
workspace.workspace = true
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use anyhow::Result;
|
||||
use assistant_tool::Tool;
|
||||
use gpui::{App, Task, WeakEntity, Window};
|
||||
use gpui::{App, Entity, Task};
|
||||
use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use workspace::Workspace;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct ListWorktreesToolInput {}
|
||||
@@ -34,16 +34,9 @@ impl Tool for ListWorktreesTool {
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
_input: serde_json::Value,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
_window: &mut Window,
|
||||
project: Entity<Project>,
|
||||
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)]
|
||||
|
||||
@@ -3,7 +3,8 @@ use std::sync::Arc;
|
||||
use anyhow::{anyhow, Result};
|
||||
use assistant_tool::Tool;
|
||||
use chrono::{Local, Utc};
|
||||
use gpui::{App, Task, WeakEntity, Window};
|
||||
use gpui::{App, Entity, Task};
|
||||
use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -41,8 +42,7 @@ impl Tool for NowTool {
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
input: serde_json::Value,
|
||||
_workspace: WeakEntity<workspace::Workspace>,
|
||||
_window: &mut Window,
|
||||
_project: Entity<Project>,
|
||||
_cx: &mut App,
|
||||
) -> Task<Result<String>> {
|
||||
let input: NowToolInput = match serde_json::from_value(input) {
|
||||
|
||||
@@ -3,11 +3,10 @@ use std::sync::Arc;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use assistant_tool::Tool;
|
||||
use gpui::{App, Task, WeakEntity, Window};
|
||||
use project::{ProjectPath, WorktreeId};
|
||||
use gpui::{App, Entity, Task};
|
||||
use project::{Project, ProjectPath, WorktreeId};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use workspace::Workspace;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct ReadFileToolInput {
|
||||
@@ -38,20 +37,14 @@ impl Tool for ReadFileTool {
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
input: serde_json::Value,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
_window: &mut Window,
|
||||
project: Entity<Project>,
|
||||
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,
|
||||
|
||||
@@ -10,9 +10,9 @@ use gpui::{
|
||||
};
|
||||
use language::{
|
||||
language_settings::SoftWrap, Anchor, Buffer, BufferSnapshot, CodeLabel, LanguageRegistry,
|
||||
LanguageServerId, ToOffset,
|
||||
ToOffset,
|
||||
};
|
||||
use project::{search::SearchQuery, Completion};
|
||||
use project::{search::SearchQuery, Completion, CompletionSource};
|
||||
use settings::Settings;
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
@@ -309,11 +309,9 @@ impl MessageEditor {
|
||||
old_range: range.clone(),
|
||||
new_text,
|
||||
label,
|
||||
documentation: None,
|
||||
server_id: LanguageServerId(0), // TODO: Make this optional or something?
|
||||
lsp_completion: Default::default(), // TODO: Make this optional or something?
|
||||
confirm: None,
|
||||
resolved: true,
|
||||
documentation: None,
|
||||
source: CompletionSource::Custom,
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
|
||||
@@ -78,6 +78,7 @@ pub struct ComponentId(pub &'static str);
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ComponentMetadata {
|
||||
id: ComponentId,
|
||||
name: SharedString,
|
||||
scope: Option<ComponentScope>,
|
||||
description: Option<SharedString>,
|
||||
@@ -85,6 +86,10 @@ pub struct ComponentMetadata {
|
||||
}
|
||||
|
||||
impl ComponentMetadata {
|
||||
pub fn id(&self) -> ComponentId {
|
||||
self.id.clone()
|
||||
}
|
||||
|
||||
pub fn name(&self) -> SharedString {
|
||||
self.name.clone()
|
||||
}
|
||||
@@ -156,9 +161,11 @@ 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(
|
||||
ComponentId(name),
|
||||
id.clone(),
|
||||
ComponentMetadata {
|
||||
id,
|
||||
name: component_name,
|
||||
scope: scope.clone(),
|
||||
description: description.map(Into::into),
|
||||
|
||||
@@ -23,3 +23,4 @@ project.workspace = true
|
||||
ui.workspace = true
|
||||
workspace.workspace = true
|
||||
notifications.workspace = true
|
||||
collections.workspace = true
|
||||
|
||||
@@ -6,12 +6,14 @@ use std::iter::Iterator;
|
||||
use std::sync::Arc;
|
||||
|
||||
use client::UserStore;
|
||||
use component::{components, ComponentMetadata};
|
||||
use component::{components, ComponentId, 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};
|
||||
@@ -59,6 +61,8 @@ pub fn init(app_state: Arc<AppState>, cx: &mut App) {
|
||||
}
|
||||
|
||||
enum PreviewEntry {
|
||||
AllComponents,
|
||||
Separator,
|
||||
Component(ComponentMetadata),
|
||||
SectionHeader(SharedString),
|
||||
}
|
||||
@@ -75,13 +79,22 @@ 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,
|
||||
selected_index: usize,
|
||||
cursor_index: usize,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
user_store: Entity<UserStore>,
|
||||
@@ -95,22 +108,25 @@ impl ComponentPreview {
|
||||
selected_index: impl Into<Option<usize>>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let components = components().all_sorted();
|
||||
let initial_length = components.len();
|
||||
let sorted_components = components().all_sorted();
|
||||
let selected_index = selected_index.into().unwrap_or(0);
|
||||
|
||||
let component_list =
|
||||
ListState::new(initial_length, gpui::ListAlignment::Top, px(1500.0), {
|
||||
let component_list = ListState::new(
|
||||
sorted_components.len(),
|
||||
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(ix, &component, window, cx)
|
||||
this.render_preview(&component, window, cx)
|
||||
.into_any_element()
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
let mut component_preview = Self {
|
||||
focus_handle: cx.focus_handle(),
|
||||
@@ -119,13 +135,15 @@ impl ComponentPreview {
|
||||
language_registry,
|
||||
user_store,
|
||||
workspace,
|
||||
components,
|
||||
active_page: PreviewPage::AllComponents,
|
||||
component_map: components().0,
|
||||
components: sorted_components,
|
||||
component_list,
|
||||
selected_index,
|
||||
cursor_index: selected_index,
|
||||
};
|
||||
|
||||
if component_preview.selected_index > 0 {
|
||||
component_preview.scroll_to_preview(component_preview.selected_index, cx);
|
||||
if component_preview.cursor_index > 0 {
|
||||
component_preview.scroll_to_preview(component_preview.cursor_index, cx);
|
||||
}
|
||||
|
||||
component_preview.update_component_list(cx);
|
||||
@@ -135,7 +153,12 @@ impl ComponentPreview {
|
||||
|
||||
fn scroll_to_preview(&mut self, ix: usize, cx: &mut Context<Self>) {
|
||||
self.component_list.scroll_to_reveal_item(ix);
|
||||
self.selected_index = ix;
|
||||
self.cursor_index = ix;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn set_active_page(&mut self, page: PreviewPage, cx: &mut Context<Self>) {
|
||||
self.active_page = page;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
@@ -146,7 +169,6 @@ 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();
|
||||
|
||||
@@ -157,15 +179,12 @@ 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,
|
||||
@@ -175,15 +194,16 @@ impl ComponentPreview {
|
||||
ComponentScope::VersionControl,
|
||||
];
|
||||
|
||||
// First add components with known scopes
|
||||
// Always show all components first
|
||||
entries.push(PreviewEntry::AllComponents);
|
||||
entries.push(PreviewEntry::Separator);
|
||||
|
||||
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));
|
||||
}
|
||||
@@ -191,16 +211,13 @@ 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()));
|
||||
}
|
||||
@@ -208,9 +225,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 {
|
||||
@@ -226,22 +243,42 @@ impl ComponentPreview {
|
||||
&self,
|
||||
ix: usize,
|
||||
entry: &PreviewEntry,
|
||||
selected: bool,
|
||||
cx: &Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
match entry {
|
||||
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::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::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(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -260,11 +297,13 @@ impl ComponentPreview {
|
||||
weak_entity
|
||||
.update(cx, |this, cx| match entry {
|
||||
PreviewEntry::Component(component) => this
|
||||
.render_preview(ix, component, window, cx)
|
||||
.render_preview(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()
|
||||
},
|
||||
@@ -290,7 +329,6 @@ impl ComponentPreview {
|
||||
|
||||
fn render_preview(
|
||||
&self,
|
||||
_ix: usize,
|
||||
component: &ComponentMetadata,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
@@ -341,6 +379,44 @@ 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| {
|
||||
@@ -363,8 +439,9 @@ 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")
|
||||
@@ -386,12 +463,7 @@ impl Render for ComponentPreview {
|
||||
move |this, range, _window, cx| {
|
||||
range
|
||||
.map(|ix| {
|
||||
this.render_sidebar_entry(
|
||||
ix,
|
||||
&sidebar_entries[ix],
|
||||
ix == this.selected_index,
|
||||
cx,
|
||||
)
|
||||
this.render_sidebar_entry(ix, &sidebar_entries[ix], cx)
|
||||
})
|
||||
.collect()
|
||||
},
|
||||
@@ -415,18 +487,12 @@ impl Render for ComponentPreview {
|
||||
),
|
||||
),
|
||||
)
|
||||
.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),
|
||||
),
|
||||
)
|
||||
.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(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -465,7 +531,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.selected_index;
|
||||
let selected_index = self.cursor_index;
|
||||
|
||||
Some(cx.new(|cx| {
|
||||
Self::new(
|
||||
|
||||
@@ -31,4 +31,3 @@ settings.workspace = true
|
||||
smol.workspace = true
|
||||
url = { workspace = true, features = ["serde"] }
|
||||
util.workspace = true
|
||||
workspace.workspace = true
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{anyhow, bail};
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use assistant_tool::Tool;
|
||||
use gpui::{App, Entity, Task, Window};
|
||||
use gpui::{App, Entity, Task};
|
||||
use project::Project;
|
||||
|
||||
use crate::manager::ContextServerManager;
|
||||
use crate::types;
|
||||
@@ -49,12 +50,11 @@ impl Tool for ContextServerTool {
|
||||
}
|
||||
|
||||
fn run(
|
||||
self: std::sync::Arc<Self>,
|
||||
self: Arc<Self>,
|
||||
input: serde_json::Value,
|
||||
_workspace: gpui::WeakEntity<workspace::Workspace>,
|
||||
_: &mut Window,
|
||||
_project: Entity<Project>,
|
||||
cx: &mut App,
|
||||
) -> gpui::Task<gpui::Result<String>> {
|
||||
) -> Task<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();
|
||||
|
||||
@@ -4,13 +4,14 @@ 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_file;
|
||||
use settings::watch_config_dir;
|
||||
use strum::EnumIter;
|
||||
|
||||
pub const COPILOT_CHAT_COMPLETION_URL: &str = "https://api.githubcopilot.com/chat/completions";
|
||||
@@ -237,27 +238,18 @@ impl CopilotChat {
|
||||
}
|
||||
|
||||
pub fn new(fs: Arc<dyn Fs>, client: Arc<dyn HttpClient>, cx: &App) -> Self {
|
||||
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()
|
||||
}
|
||||
};
|
||||
let config_paths: HashSet<PathBuf> = copilot_chat_config_paths().into_iter().collect();
|
||||
let dir_path = copilot_chat_config_dir();
|
||||
|
||||
cx.spawn(|cx| async move {
|
||||
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 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 oauth_token = extract_oauth_token(contents);
|
||||
|
||||
cx.update(|cx| {
|
||||
if let Some(this) = Self::global(cx).as_ref() {
|
||||
this.update(cx, |this, cx| {
|
||||
|
||||
@@ -311,7 +311,10 @@ impl ProjectDiagnosticsEditor {
|
||||
cx: &mut Context<Workspace>,
|
||||
) {
|
||||
if let Some(existing) = workspace.item_of_type::<ProjectDiagnosticsEditor>(cx) {
|
||||
workspace.activate_item(&existing, true, true, window, 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);
|
||||
} else {
|
||||
let workspace_handle = cx.entity().downgrade();
|
||||
|
||||
|
||||
@@ -340,7 +340,9 @@ gpui::actions!(
|
||||
MoveToPreviousWordStart,
|
||||
MoveToStartOfParagraph,
|
||||
MoveToStartOfExcerpt,
|
||||
MoveToStartOfNextExcerpt,
|
||||
MoveToEndOfExcerpt,
|
||||
MoveToEndOfPreviousExcerpt,
|
||||
MoveUp,
|
||||
Newline,
|
||||
NewlineAbove,
|
||||
@@ -378,7 +380,9 @@ gpui::actions!(
|
||||
SelectAll,
|
||||
SelectAllMatches,
|
||||
SelectToStartOfExcerpt,
|
||||
SelectToStartOfNextExcerpt,
|
||||
SelectToEndOfExcerpt,
|
||||
SelectToEndOfPreviousExcerpt,
|
||||
SelectDown,
|
||||
SelectEnclosingSymbol,
|
||||
SelectLargerSyntaxNode,
|
||||
|
||||
@@ -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,11 +233,9 @@ impl CompletionsMenu {
|
||||
runs: Default::default(),
|
||||
filter_range: Default::default(),
|
||||
},
|
||||
server_id: LanguageServerId(usize::MAX),
|
||||
documentation: None,
|
||||
lsp_completion: Default::default(),
|
||||
confirm: None,
|
||||
resolved: true,
|
||||
source: CompletionSource::Custom,
|
||||
})
|
||||
.collect();
|
||||
|
||||
@@ -500,7 +498,12 @@ impl CompletionsMenu {
|
||||
// Ignore font weight for syntax highlighting, as we'll use it
|
||||
// for fuzzy matches.
|
||||
highlight.font_weight = None;
|
||||
if completion.lsp_completion.deprecated.unwrap_or(false) {
|
||||
if completion
|
||||
.source
|
||||
.lsp_completion()
|
||||
.and_then(|lsp_completion| lsp_completion.deprecated)
|
||||
.unwrap_or(false)
|
||||
{
|
||||
highlight.strikethrough = Some(StrikethroughStyle {
|
||||
thickness: 1.0.into(),
|
||||
..Default::default()
|
||||
@@ -708,7 +711,10 @@ impl CompletionsMenu {
|
||||
|
||||
let completion = &completions[mat.candidate_id];
|
||||
let sort_key = completion.sort_key();
|
||||
let sort_text = completion.lsp_completion.sort_text.as_deref();
|
||||
let sort_text = completion
|
||||
.source
|
||||
.lsp_completion()
|
||||
.and_then(|lsp_completion| lsp_completion.sort_text.as_deref());
|
||||
let score = Reverse(OrderedFloat(mat.score));
|
||||
|
||||
if mat.score >= 0.2 {
|
||||
|
||||
@@ -138,8 +138,9 @@ use multi_buffer::{
|
||||
use project::{
|
||||
lsp_store::{CompletionDocumentation, FormatTrigger, LspFormatTarget, OpenLspBufferHandle},
|
||||
project_settings::{GitGutterSetting, ProjectSettings},
|
||||
CodeAction, Completion, CompletionIntent, DocumentHighlight, InlayHint, Location, LocationLink,
|
||||
PrepareRenameResponse, Project, ProjectItem, ProjectTransaction, TaskSourceKind,
|
||||
CodeAction, Completion, CompletionIntent, CompletionSource, DocumentHighlight, InlayHint,
|
||||
Location, LocationLink, PrepareRenameResponse, Project, ProjectItem, ProjectTransaction,
|
||||
TaskSourceKind,
|
||||
};
|
||||
use rand::prelude::*;
|
||||
use rpc::{proto::*, ErrorExt};
|
||||
@@ -1250,11 +1251,6 @@ 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,
|
||||
@@ -1577,13 +1573,16 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
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 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 has_active_edit_prediction {
|
||||
@@ -9849,6 +9848,31 @@ 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,
|
||||
@@ -9874,6 +9898,31 @@ 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,
|
||||
@@ -9895,6 +9944,27 @@ 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,
|
||||
@@ -9916,6 +9986,27 @@ 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,
|
||||
@@ -14887,14 +14978,14 @@ impl Editor {
|
||||
&self,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> BTreeMap<DisplayRow, Background> {
|
||||
) -> BTreeMap<DisplayRow, LineHighlight> {
|
||||
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, Background>::new(),
|
||||
BTreeMap::<DisplayRow, LineHighlight>::new(),
|
||||
|mut unique_rows, highlight| {
|
||||
let start = highlight.range.start.to_display_point(&snapshot);
|
||||
let end = highlight.range.end.to_display_point(&snapshot);
|
||||
@@ -15442,14 +15533,9 @@ 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 => {
|
||||
cx.emit(EditorEvent::TitleChanged)
|
||||
}
|
||||
// multi_buffer::Event::DiffBaseChanged => {
|
||||
// self.scrollbar_marker_state.dirty = true;
|
||||
// cx.emit(EditorEvent::DiffBaseChanged);
|
||||
// cx.notify();
|
||||
// }
|
||||
multi_buffer::Event::FileHandleChanged
|
||||
| multi_buffer::Event::Reloaded
|
||||
| multi_buffer::Event::BufferDiffChanged => cx.emit(EditorEvent::TitleChanged),
|
||||
multi_buffer::Event::Closed => cx.emit(EditorEvent::Closed),
|
||||
multi_buffer::Event::DiagnosticsUpdated => {
|
||||
self.refresh_active_diagnostics(cx);
|
||||
@@ -16907,38 +16993,40 @@ fn snippet_completions(
|
||||
Some(Completion {
|
||||
old_range: range,
|
||||
new_text: snippet.body.clone(),
|
||||
resolved: false,
|
||||
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()
|
||||
}),
|
||||
},
|
||||
label: CodeLabel {
|
||||
text: matching_prefix.clone(),
|
||||
runs: vec![],
|
||||
runs: Vec::new(),
|
||||
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,
|
||||
})
|
||||
})
|
||||
@@ -18436,3 +18524,27 @@ 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16413,6 +16413,199 @@ 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, |_| {});
|
||||
|
||||
@@ -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, 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, 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,
|
||||
MULTI_BUFFER_EXCERPT_HEADER_HEIGHT,
|
||||
};
|
||||
use buffer_diff::{DiffHunkStatus, DiffHunkStatusKind};
|
||||
@@ -282,7 +282,9 @@ 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);
|
||||
@@ -296,7 +298,9 @@ 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);
|
||||
@@ -4132,46 +4136,74 @@ impl EditorElement {
|
||||
}
|
||||
}
|
||||
|
||||
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 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 current_paint: Option<(gpui::Background, Range<DisplayRow>)> = None;
|
||||
let mut current_paint: Option<(LineHighlight, Range<DisplayRow>, Edges<Pixels>)> =
|
||||
None;
|
||||
for (&new_row, &new_background) in &layout.highlighted_rows {
|
||||
match &mut current_paint {
|
||||
Some((current_background, current_range)) => {
|
||||
Some((current_background, current_range, mut edges)) => {
|
||||
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,
|
||||
);
|
||||
current_paint = Some((new_background, new_row..new_row));
|
||||
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));
|
||||
continue;
|
||||
} else {
|
||||
current_range.end = current_range.end.next_row();
|
||||
}
|
||||
}
|
||||
None => current_paint = Some((new_background, new_row..new_row)),
|
||||
None => {
|
||||
let edges = Edges {
|
||||
top: px(1.),
|
||||
bottom: px(1.),
|
||||
..Default::default()
|
||||
};
|
||||
current_paint = Some((new_background, new_row..new_row, edges))
|
||||
}
|
||||
};
|
||||
}
|
||||
if let Some((color, range)) = current_paint {
|
||||
paint_highlight(range.start, range.end, color);
|
||||
if let Some((color, range, edges)) = current_paint {
|
||||
paint_highlight(range.start, range.end, color, edges);
|
||||
}
|
||||
|
||||
let scroll_left =
|
||||
@@ -4431,6 +4463,9 @@ impl EditorElement {
|
||||
background_color.opacity(if is_light { 0.2 } else { 0.32 });
|
||||
}
|
||||
}
|
||||
GitHunkStyleSetting::StagedBorder | GitHunkStyleSetting::Border => {
|
||||
// Don't change the background color
|
||||
}
|
||||
}
|
||||
|
||||
// Flatten the background color with the editor color to prevent
|
||||
@@ -6775,12 +6810,15 @@ impl Element for EditorElement {
|
||||
let hunk_opacity = if is_light { 0.16 } else { 0.12 };
|
||||
let slash_width = line_height.0 / 1.5; // ~16 by default
|
||||
|
||||
let staged_background = match hunk_style {
|
||||
GitHunkStyleSetting::Transparent | GitHunkStyleSetting::Pattern => {
|
||||
solid_background(background_color.opacity(hunk_opacity))
|
||||
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 {
|
||||
@@ -6788,30 +6826,56 @@ impl Element for EditorElement {
|
||||
} 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_background = match hunk_style {
|
||||
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 => {
|
||||
solid_background(background_color.opacity(hunk_opacity))
|
||||
| GitHunkStyleSetting::StagedTransparent
|
||||
| GitHunkStyleSetting::StagedBorder => {
|
||||
solid_background(background_color.opacity(hunk_opacity)).into()
|
||||
}
|
||||
};
|
||||
|
||||
let background = if unstaged {
|
||||
unstaged_background
|
||||
unstaged_highlight
|
||||
} else {
|
||||
staged_background
|
||||
staged_highlight
|
||||
};
|
||||
|
||||
highlighted_rows
|
||||
@@ -7660,7 +7724,7 @@ pub struct EditorLayout {
|
||||
indent_guides: Option<Vec<IndentGuideLayout>>,
|
||||
visible_display_row_range: Range<DisplayRow>,
|
||||
active_rows: BTreeMap<DisplayRow, bool>,
|
||||
highlighted_rows: BTreeMap<DisplayRow, gpui::Background>,
|
||||
highlighted_rows: BTreeMap<DisplayRow, LineHighlight>,
|
||||
line_elements: SmallVec<[AnyElement; 1]>,
|
||||
line_numbers: Arc<HashMap<MultiBufferRow, LineNumberLayout>>,
|
||||
display_hunks: Vec<(DisplayDiffHunk, Option<Hitbox>)>,
|
||||
@@ -8832,14 +8896,16 @@ fn diff_hunk_controls(
|
||||
.h(line_height)
|
||||
.mr_1()
|
||||
.gap_1()
|
||||
.px_1()
|
||||
.px_0p5()
|
||||
.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 })
|
||||
@@ -8896,7 +8962,7 @@ fn diff_hunk_controls(
|
||||
})
|
||||
})
|
||||
.child(
|
||||
Button::new("discard", "Restore")
|
||||
Button::new("restore", "Restore")
|
||||
.tooltip({
|
||||
let focus_handle = editor.focus_handle(cx);
|
||||
move |window, cx| {
|
||||
|
||||
@@ -448,7 +448,9 @@ pub fn end_of_excerpt(
|
||||
if start.row() > DisplayRow(0) {
|
||||
*start.row_mut() -= 1;
|
||||
}
|
||||
map.clip_point(start, Bias::Left)
|
||||
start = map.clip_point(start, Bias::Left);
|
||||
*start.column_mut() = 0;
|
||||
start
|
||||
}
|
||||
Direction::Next => {
|
||||
let mut end = excerpt.end_anchor().to_display_point(&map);
|
||||
|
||||
@@ -429,12 +429,14 @@ impl EditorTestContext {
|
||||
if expected_selections.len() > 0 {
|
||||
assert!(
|
||||
is_selected,
|
||||
"excerpt {} should be selected. Got {:?}",
|
||||
ix,
|
||||
self.editor_state()
|
||||
"excerpt {ix} should be selected. got {:?}",
|
||||
self.editor_state(),
|
||||
);
|
||||
} else {
|
||||
assert!(!is_selected, "excerpt {} should not be selected", ix);
|
||||
assert!(
|
||||
!is_selected,
|
||||
"excerpt {ix} should not be selected, got: {selections:?}",
|
||||
);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ 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
|
||||
|
||||
@@ -4,6 +4,7 @@ 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};
|
||||
@@ -97,6 +98,11 @@ 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()
|
||||
|
||||
@@ -692,7 +692,9 @@ impl GitRepository for RealGitRepository {
|
||||
PushOptions::Force => "--force-with-lease",
|
||||
}))
|
||||
.arg(remote_name)
|
||||
.arg(format!("{}:{}", branch_name, branch_name));
|
||||
.arg(format!("{}:{}", branch_name, branch_name))
|
||||
.stdout(smol::process::Stdio::piped())
|
||||
.stderr(smol::process::Stdio::piped());
|
||||
let git_process = command.spawn()?;
|
||||
|
||||
run_remote_command(ask_pass, git_process)
|
||||
@@ -714,7 +716,9 @@ impl GitRepository for RealGitRepository {
|
||||
.current_dir(&working_directory)
|
||||
.args(["pull"])
|
||||
.arg(remote_name)
|
||||
.arg(branch_name);
|
||||
.arg(branch_name)
|
||||
.stdout(smol::process::Stdio::piped())
|
||||
.stderr(smol::process::Stdio::piped());
|
||||
let git_process = command.spawn()?;
|
||||
|
||||
run_remote_command(ask_pass, git_process)
|
||||
@@ -729,7 +733,9 @@ impl GitRepository for RealGitRepository {
|
||||
.env("SSH_ASKPASS", ask_pass.script_path())
|
||||
.env("SSH_ASKPASS_REQUIRE", "force")
|
||||
.current_dir(&working_directory)
|
||||
.args(["fetch", "--all"]);
|
||||
.args(["fetch", "--all"])
|
||||
.stdout(smol::process::Stdio::piped())
|
||||
.stderr(smol::process::Stdio::piped());
|
||||
let git_process = command.spawn()?;
|
||||
|
||||
run_remote_command(ask_pass, git_process)
|
||||
|
||||
@@ -54,6 +54,39 @@ 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 {
|
||||
@@ -106,15 +139,15 @@ impl FileStatus {
|
||||
Ok(status)
|
||||
}
|
||||
|
||||
pub fn is_staged(self) -> Option<bool> {
|
||||
pub fn staging(self) -> StageStatus {
|
||||
match self {
|
||||
FileStatus::Untracked | FileStatus::Ignored | FileStatus::Unmerged { .. } => {
|
||||
Some(false)
|
||||
StageStatus::Unstaged
|
||||
}
|
||||
FileStatus::Tracked(tracked) => match (tracked.index_status, tracked.worktree_status) {
|
||||
(StatusCode::Unmodified, _) => Some(false),
|
||||
(_, StatusCode::Unmodified) => Some(true),
|
||||
_ => None,
|
||||
(StatusCode::Unmodified, _) => StageStatus::Unstaged,
|
||||
(_, StatusCode::Unmodified) => StageStatus::Staged,
|
||||
_ => StageStatus::PartiallyStaged,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,10 +18,30 @@ 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,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
use crate::branch_picker::{self, BranchList};
|
||||
use crate::git_panel::{commit_message_editor, GitPanel};
|
||||
use git::Commit;
|
||||
use git::{Commit, GenerateCommitMessage};
|
||||
use panel::{panel_button, panel_editor_style, panel_filled_button};
|
||||
use ui::{prelude::*, KeybindingHint, PopoverMenu, Tooltip};
|
||||
|
||||
@@ -372,11 +372,24 @@ 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.branch_list.update(cx, |branch_list, cx| {
|
||||
branch_list.popover_handle.toggle(window, cx);
|
||||
})
|
||||
toggle_branch_picker(this, window, cx);
|
||||
}),
|
||||
)
|
||||
.on_action(
|
||||
cx.listener(|this, _: &zed_actions::git::CheckoutBranch, window, cx| {
|
||||
toggle_branch_picker(this, window, cx);
|
||||
}),
|
||||
)
|
||||
.on_action(
|
||||
cx.listener(|this, _: &zed_actions::git::Switch, window, cx| {
|
||||
toggle_branch_picker(this, window, cx);
|
||||
}),
|
||||
)
|
||||
.elevation_3(cx)
|
||||
@@ -415,3 +428,13 @@ impl Render for CommitModal {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn toggle_branch_picker(
|
||||
this: &mut CommitModal,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<'_, CommitModal>,
|
||||
) {
|
||||
this.branch_list.update(cx, |branch_list, cx| {
|
||||
branch_list.popover_handle.toggle(window, cx);
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use crate::askpass_modal::AskPassModal;
|
||||
use crate::branch_picker;
|
||||
use crate::commit_modal::CommitModal;
|
||||
use crate::git_panel_settings::StatusStyle;
|
||||
use crate::remote_output_toast::{RemoteAction, RemoteOutputToast};
|
||||
use crate::repository_selector::filtered_repository_entries;
|
||||
use crate::{branch_picker, render_remote_button};
|
||||
use crate::{
|
||||
git_panel_settings::GitPanelSettings, git_status_icon, repository_selector::RepositorySelector,
|
||||
};
|
||||
@@ -22,15 +22,15 @@ use git::repository::{
|
||||
Branch, CommitDetails, CommitSummary, DiffType, PushOptions, Remote, RemoteCommandOutput,
|
||||
ResetMode, Upstream, UpstreamTracking, UpstreamTrackingStatus,
|
||||
};
|
||||
use git::status::StageStatus;
|
||||
use git::{repository::RepoPath, status::FileStatus, Commit, ToggleStaged};
|
||||
use git::{ExpandCommitEditor, RestoreTrackedFiles, StageAll, TrashUntrackedFiles, UnstageAll};
|
||||
use gpui::{
|
||||
actions, anchored, deferred, hsla, percentage, point, uniform_list, Action, Animation,
|
||||
AnimationExt as _, AnyView, BoxShadow, ClickEvent, Corner, DismissEvent, Entity, EventEmitter,
|
||||
FocusHandle, Focusable, KeyContext, ListHorizontalSizingBehavior, ListSizingBehavior,
|
||||
Modifiers, ModifiersChangedEvent, MouseButton, MouseDownEvent, Point, PromptLevel,
|
||||
ScrollStrategy, Stateful, Subscription, Task, Transformation, UniformListScrollHandle,
|
||||
WeakEntity,
|
||||
actions, anchored, deferred, percentage, uniform_list, Action, Animation, AnimationExt as _,
|
||||
ClickEvent, Corner, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, KeyContext,
|
||||
ListHorizontalSizingBehavior, ListSizingBehavior, Modifiers, ModifiersChangedEvent,
|
||||
MouseButton, MouseDownEvent, Point, PromptLevel, ScrollStrategy, Stateful, Subscription, Task,
|
||||
Transformation, UniformListScrollHandle, WeakEntity,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use language::{Buffer, File};
|
||||
@@ -48,7 +48,6 @@ use project::{
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::Settings as _;
|
||||
use smallvec::smallvec;
|
||||
use std::cell::RefCell;
|
||||
use std::future::Future;
|
||||
use std::path::{Path, PathBuf};
|
||||
@@ -57,8 +56,8 @@ use std::{collections::HashSet, sync::Arc, time::Duration, usize};
|
||||
use strum::{IntoEnumIterator, VariantNames};
|
||||
use time::OffsetDateTime;
|
||||
use ui::{
|
||||
prelude::*, ButtonLike, Checkbox, ContextMenu, ElevationIndex, PopoverMenu, Scrollbar,
|
||||
ScrollbarState, Tooltip,
|
||||
prelude::*, Checkbox, ContextMenu, ElevationIndex, PopoverMenu, Scrollbar, ScrollbarState,
|
||||
Tooltip,
|
||||
};
|
||||
use util::{maybe, post_inc, ResultExt, TryFutureExt};
|
||||
use workspace::{AppState, OpenOptions, OpenVisible};
|
||||
@@ -195,7 +194,7 @@ pub struct GitStatusEntry {
|
||||
pub(crate) worktree_path: Arc<Path>,
|
||||
pub(crate) abs_path: PathBuf,
|
||||
pub(crate) status: FileStatus,
|
||||
pub(crate) is_staged: Option<bool>,
|
||||
pub(crate) staging: StageStatus,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
@@ -209,7 +208,7 @@ enum TargetStatus {
|
||||
struct PendingOperation {
|
||||
finished: bool,
|
||||
target_status: TargetStatus,
|
||||
repo_paths: HashSet<RepoPath>,
|
||||
entries: Vec<GitStatusEntry>,
|
||||
op_id: usize,
|
||||
}
|
||||
|
||||
@@ -226,6 +225,8 @@ pub struct GitPanel {
|
||||
add_coauthors: bool,
|
||||
generate_commit_message_task: Option<Task<Option<()>>>,
|
||||
entries: Vec<GitListEntry>,
|
||||
single_staged_entry: Option<GitStatusEntry>,
|
||||
single_tracked_entry: Option<GitStatusEntry>,
|
||||
focus_handle: FocusHandle,
|
||||
fs: Arc<dyn Fs>,
|
||||
hide_scrollbar_task: Option<Task<()>>,
|
||||
@@ -365,6 +366,8 @@ impl GitPanel {
|
||||
pending: Vec::new(),
|
||||
pending_commit: None,
|
||||
pending_serialization: Task::ready(None),
|
||||
single_staged_entry: None,
|
||||
single_tracked_entry: None,
|
||||
project,
|
||||
scroll_handle,
|
||||
scrollbar_state,
|
||||
@@ -828,13 +831,13 @@ impl GitPanel {
|
||||
.repo_path_to_project_path(&entry.repo_path)?;
|
||||
let workspace = self.workspace.clone();
|
||||
|
||||
if entry.status.is_staged() != Some(false) {
|
||||
self.perform_stage(false, vec![entry.repo_path.clone()], cx);
|
||||
if entry.status.staging().has_staged() {
|
||||
self.change_file_stage(false, vec![entry.clone()], cx);
|
||||
}
|
||||
let filename = path.path.file_name()?.to_string_lossy();
|
||||
|
||||
if !entry.status.is_created() {
|
||||
self.perform_checkout(vec![entry.repo_path.clone()], cx);
|
||||
self.perform_checkout(vec![entry.clone()], cx);
|
||||
} else {
|
||||
let prompt = prompt(&format!("Trash {}?", filename), None, window, cx);
|
||||
cx.spawn_in(window, |_, mut cx| async move {
|
||||
@@ -863,7 +866,7 @@ impl GitPanel {
|
||||
});
|
||||
}
|
||||
|
||||
fn perform_checkout(&mut self, repo_paths: Vec<RepoPath>, cx: &mut Context<Self>) {
|
||||
fn perform_checkout(&mut self, entries: Vec<GitStatusEntry>, cx: &mut Context<Self>) {
|
||||
let workspace = self.workspace.clone();
|
||||
let Some(active_repository) = self.active_repository.clone() else {
|
||||
return;
|
||||
@@ -873,19 +876,19 @@ impl GitPanel {
|
||||
self.pending.push(PendingOperation {
|
||||
op_id,
|
||||
target_status: TargetStatus::Reverted,
|
||||
repo_paths: repo_paths.iter().cloned().collect(),
|
||||
entries: entries.clone(),
|
||||
finished: false,
|
||||
});
|
||||
self.update_visible_entries(cx);
|
||||
let task = cx.spawn(|_, mut cx| async move {
|
||||
let tasks: Vec<_> = workspace.update(&mut cx, |workspace, cx| {
|
||||
workspace.project().update(cx, |project, cx| {
|
||||
repo_paths
|
||||
entries
|
||||
.iter()
|
||||
.filter_map(|repo_path| {
|
||||
.filter_map(|entry| {
|
||||
let path = active_repository
|
||||
.read(cx)
|
||||
.repo_path_to_project_path(&repo_path)?;
|
||||
.repo_path_to_project_path(&entry.repo_path)?;
|
||||
Some(project.open_buffer(path, cx))
|
||||
})
|
||||
.collect()
|
||||
@@ -895,7 +898,15 @@ impl GitPanel {
|
||||
let buffers = futures::future::join_all(tasks).await;
|
||||
|
||||
active_repository
|
||||
.update(&mut cx, |repo, _| repo.checkout_files("HEAD", repo_paths))?
|
||||
.update(&mut cx, |repo, _| {
|
||||
repo.checkout_files(
|
||||
"HEAD",
|
||||
entries
|
||||
.iter()
|
||||
.map(|entries| entries.repo_path.clone())
|
||||
.collect(),
|
||||
)
|
||||
})?
|
||||
.await??;
|
||||
|
||||
let tasks: Vec<_> = cx.update(|cx| {
|
||||
@@ -983,8 +994,7 @@ impl GitPanel {
|
||||
match prompt.await {
|
||||
Ok(RestoreCancel::RestoreTrackedFiles) => {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
let repo_paths = entries.into_iter().map(|entry| entry.repo_path).collect();
|
||||
this.perform_checkout(repo_paths, cx);
|
||||
this.perform_checkout(entries, cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
@@ -1053,16 +1063,10 @@ impl GitPanel {
|
||||
})?;
|
||||
let to_unstage = to_delete
|
||||
.into_iter()
|
||||
.filter_map(|entry| {
|
||||
if entry.status.is_staged() != Some(false) {
|
||||
Some(entry.repo_path.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.filter(|entry| !entry.status.staging().is_fully_unstaged())
|
||||
.collect();
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.perform_stage(false, to_unstage, cx)
|
||||
this.change_file_stage(false, to_unstage, cx)
|
||||
})?;
|
||||
for task in tasks {
|
||||
task.await?;
|
||||
@@ -1075,25 +1079,25 @@ impl GitPanel {
|
||||
}
|
||||
|
||||
fn stage_all(&mut self, _: &StageAll, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
let repo_paths = self
|
||||
let entries = self
|
||||
.entries
|
||||
.iter()
|
||||
.filter_map(|entry| entry.status_entry())
|
||||
.filter(|status_entry| status_entry.is_staged != Some(true))
|
||||
.map(|status_entry| status_entry.repo_path.clone())
|
||||
.filter(|status_entry| status_entry.staging.has_unstaged())
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
self.perform_stage(true, repo_paths, cx);
|
||||
self.change_file_stage(true, entries, cx);
|
||||
}
|
||||
|
||||
fn unstage_all(&mut self, _: &UnstageAll, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
let repo_paths = self
|
||||
let entries = self
|
||||
.entries
|
||||
.iter()
|
||||
.filter_map(|entry| entry.status_entry())
|
||||
.filter(|status_entry| status_entry.is_staged != Some(false))
|
||||
.map(|status_entry| status_entry.repo_path.clone())
|
||||
.filter(|status_entry| status_entry.staging.has_staged())
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
self.perform_stage(false, repo_paths, cx);
|
||||
self.change_file_stage(false, entries, cx);
|
||||
}
|
||||
|
||||
fn toggle_staged_for_entry(
|
||||
@@ -1107,10 +1111,10 @@ impl GitPanel {
|
||||
};
|
||||
let (stage, repo_paths) = match entry {
|
||||
GitListEntry::GitStatusEntry(status_entry) => {
|
||||
if status_entry.status.is_staged().unwrap_or(false) {
|
||||
(false, vec![status_entry.repo_path.clone()])
|
||||
if status_entry.status.staging().is_fully_staged() {
|
||||
(false, vec![status_entry.clone()])
|
||||
} else {
|
||||
(true, vec![status_entry.repo_path.clone()])
|
||||
(true, vec![status_entry.clone()])
|
||||
}
|
||||
}
|
||||
GitListEntry::Header(section) => {
|
||||
@@ -1122,18 +1126,23 @@ impl GitPanel {
|
||||
.filter_map(|entry| entry.status_entry())
|
||||
.filter(|status_entry| {
|
||||
section.contains(&status_entry, repository)
|
||||
&& status_entry.is_staged != Some(goal_staged_state)
|
||||
&& status_entry.staging.as_bool() != Some(goal_staged_state)
|
||||
})
|
||||
.map(|status_entry| status_entry.repo_path.clone())
|
||||
.map(|status_entry| status_entry.clone())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
(goal_staged_state, entries)
|
||||
}
|
||||
};
|
||||
self.perform_stage(stage, repo_paths, cx);
|
||||
self.change_file_stage(stage, repo_paths, cx);
|
||||
}
|
||||
|
||||
fn perform_stage(&mut self, stage: bool, repo_paths: Vec<RepoPath>, cx: &mut Context<Self>) {
|
||||
fn change_file_stage(
|
||||
&mut self,
|
||||
stage: bool,
|
||||
entries: Vec<GitStatusEntry>,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let Some(active_repository) = self.active_repository.clone() else {
|
||||
return;
|
||||
};
|
||||
@@ -1145,10 +1154,9 @@ impl GitPanel {
|
||||
} else {
|
||||
TargetStatus::Unstaged
|
||||
},
|
||||
repo_paths: repo_paths.iter().cloned().collect(),
|
||||
entries: entries.clone(),
|
||||
finished: false,
|
||||
});
|
||||
let repo_paths = repo_paths.clone();
|
||||
let repository = active_repository.read(cx);
|
||||
self.update_counts(repository);
|
||||
cx.notify();
|
||||
@@ -1158,11 +1166,21 @@ impl GitPanel {
|
||||
let result = cx
|
||||
.update(|cx| {
|
||||
if stage {
|
||||
active_repository
|
||||
.update(cx, |repo, cx| repo.stage_entries(repo_paths.clone(), cx))
|
||||
active_repository.update(cx, |repo, cx| {
|
||||
let repo_paths = entries
|
||||
.iter()
|
||||
.map(|entry| entry.repo_path.clone())
|
||||
.collect();
|
||||
repo.stage_entries(repo_paths, cx)
|
||||
})
|
||||
} else {
|
||||
active_repository
|
||||
.update(cx, |repo, cx| repo.unstage_entries(repo_paths.clone(), cx))
|
||||
active_repository.update(cx, |repo, cx| {
|
||||
let repo_paths = entries
|
||||
.iter()
|
||||
.map(|entry| entry.repo_path.clone())
|
||||
.collect();
|
||||
repo.unstage_entries(repo_paths, cx)
|
||||
})
|
||||
}
|
||||
})?
|
||||
.await;
|
||||
@@ -1397,21 +1415,13 @@ impl GitPanel {
|
||||
|
||||
/// Suggests a commit message based on the changed files and their statuses
|
||||
pub fn suggest_commit_message(&self) -> Option<String> {
|
||||
if self.total_staged_count() != 1 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let entry = self
|
||||
.entries
|
||||
.iter()
|
||||
.find(|entry| match entry.status_entry() {
|
||||
Some(entry) => entry.is_staged.unwrap_or(false),
|
||||
_ => false,
|
||||
})?;
|
||||
|
||||
let GitListEntry::GitStatusEntry(git_status_entry) = entry.clone() else {
|
||||
return None;
|
||||
};
|
||||
let git_status_entry = if let Some(staged_entry) = &self.single_staged_entry {
|
||||
Some(staged_entry)
|
||||
} else if let Some(single_tracked_entry) = &self.single_tracked_entry {
|
||||
Some(single_tracked_entry)
|
||||
} else {
|
||||
None
|
||||
}?;
|
||||
|
||||
let action_text = if git_status_entry.status.is_deleted() {
|
||||
Some("Delete")
|
||||
@@ -1571,6 +1581,7 @@ impl GitPanel {
|
||||
this.show_remote_output(RemoteAction::Fetch, remote_message, cx);
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Error while fetching {:?}", e);
|
||||
this.show_err_toast(e, cx);
|
||||
}
|
||||
}
|
||||
@@ -1629,7 +1640,10 @@ impl GitPanel {
|
||||
Ok(remote_message) => {
|
||||
this.show_remote_output(RemoteAction::Pull, remote_message, cx)
|
||||
}
|
||||
Err(err) => this.show_err_toast(err, cx),
|
||||
Err(err) => {
|
||||
log::error!("Error while pull {:?}", err);
|
||||
this.show_err_toast(err, cx)
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
|
||||
@@ -1697,6 +1711,7 @@ impl GitPanel {
|
||||
this.show_remote_output(RemoteAction::Push(remote), remote_message, cx);
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Error while pushing {:?}", e);
|
||||
this.show_err_toast(e, cx);
|
||||
}
|
||||
})?;
|
||||
@@ -1731,7 +1746,7 @@ impl GitPanel {
|
||||
}
|
||||
|
||||
fn can_push_and_pull(&self, cx: &App) -> bool {
|
||||
!self.project.read(cx).is_via_collab()
|
||||
crate::can_push_and_pull(&self.project, cx)
|
||||
}
|
||||
|
||||
fn get_current_remote(
|
||||
@@ -1965,9 +1980,13 @@ impl GitPanel {
|
||||
|
||||
fn update_visible_entries(&mut self, cx: &mut Context<Self>) {
|
||||
self.entries.clear();
|
||||
self.single_staged_entry.take();
|
||||
self.single_staged_entry.take();
|
||||
let mut changed_entries = Vec::new();
|
||||
let mut new_entries = Vec::new();
|
||||
let mut conflict_entries = Vec::new();
|
||||
let mut last_staged = None;
|
||||
let mut staged_count = 0;
|
||||
|
||||
let Some(repo) = self.active_repository.as_ref() else {
|
||||
// Just clear entries if no repository is active.
|
||||
@@ -1980,12 +1999,15 @@ impl GitPanel {
|
||||
for entry in repo.status() {
|
||||
let is_conflict = repo.has_conflict(&entry.repo_path);
|
||||
let is_new = entry.status.is_created();
|
||||
let is_staged = entry.status.is_staged();
|
||||
let staging = entry.status.staging();
|
||||
|
||||
if self.pending.iter().any(|pending| {
|
||||
pending.target_status == TargetStatus::Reverted
|
||||
&& !pending.finished
|
||||
&& pending.repo_paths.contains(&entry.repo_path)
|
||||
&& pending
|
||||
.entries
|
||||
.iter()
|
||||
.any(|pending| pending.repo_path == entry.repo_path)
|
||||
}) {
|
||||
continue;
|
||||
}
|
||||
@@ -2002,9 +2024,14 @@ impl GitPanel {
|
||||
worktree_path,
|
||||
abs_path,
|
||||
status: entry.status,
|
||||
is_staged,
|
||||
staging,
|
||||
};
|
||||
|
||||
if staging.has_staged() {
|
||||
staged_count += 1;
|
||||
last_staged = Some(entry.clone());
|
||||
}
|
||||
|
||||
if is_conflict {
|
||||
conflict_entries.push(entry);
|
||||
} else if is_new {
|
||||
@@ -2014,6 +2041,40 @@ impl GitPanel {
|
||||
}
|
||||
}
|
||||
|
||||
let mut pending_staged_count = 0;
|
||||
let mut last_pending_staged = None;
|
||||
let mut pending_status_for_last_staged = None;
|
||||
for pending in self.pending.iter() {
|
||||
if pending.target_status == TargetStatus::Staged {
|
||||
pending_staged_count += pending.entries.len();
|
||||
last_pending_staged = pending.entries.iter().next().cloned();
|
||||
}
|
||||
if let Some(last_staged) = &last_staged {
|
||||
if pending
|
||||
.entries
|
||||
.iter()
|
||||
.any(|entry| entry.repo_path == last_staged.repo_path)
|
||||
{
|
||||
pending_status_for_last_staged = Some(pending.target_status);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if conflict_entries.len() == 0 && staged_count == 1 && pending_staged_count == 0 {
|
||||
match pending_status_for_last_staged {
|
||||
Some(TargetStatus::Staged) | None => {
|
||||
self.single_staged_entry = last_staged;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
} else if conflict_entries.len() == 0 && pending_staged_count == 1 {
|
||||
self.single_staged_entry = last_pending_staged;
|
||||
}
|
||||
|
||||
if conflict_entries.len() == 0 && changed_entries.len() == 1 {
|
||||
self.single_tracked_entry = changed_entries.first().cloned();
|
||||
}
|
||||
|
||||
if conflict_entries.len() > 0 {
|
||||
self.entries.push(GitListEntry::Header(GitHeaderEntry {
|
||||
header: Section::Conflict,
|
||||
@@ -2078,35 +2139,39 @@ impl GitPanel {
|
||||
};
|
||||
if repo.has_conflict(&status_entry.repo_path) {
|
||||
self.conflicted_count += 1;
|
||||
if self.entry_is_staged(status_entry) != Some(false) {
|
||||
if self.entry_staging(status_entry).has_staged() {
|
||||
self.conflicted_staged_count += 1;
|
||||
}
|
||||
} else if status_entry.status.is_created() {
|
||||
self.new_count += 1;
|
||||
if self.entry_is_staged(status_entry) != Some(false) {
|
||||
if self.entry_staging(status_entry).has_staged() {
|
||||
self.new_staged_count += 1;
|
||||
}
|
||||
} else {
|
||||
self.tracked_count += 1;
|
||||
if self.entry_is_staged(status_entry) != Some(false) {
|
||||
if self.entry_staging(status_entry).has_staged() {
|
||||
self.tracked_staged_count += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn entry_is_staged(&self, entry: &GitStatusEntry) -> Option<bool> {
|
||||
fn entry_staging(&self, entry: &GitStatusEntry) -> StageStatus {
|
||||
for pending in self.pending.iter().rev() {
|
||||
if pending.repo_paths.contains(&entry.repo_path) {
|
||||
if pending
|
||||
.entries
|
||||
.iter()
|
||||
.any(|pending_entry| pending_entry.repo_path == entry.repo_path)
|
||||
{
|
||||
match pending.target_status {
|
||||
TargetStatus::Staged => return Some(true),
|
||||
TargetStatus::Unstaged => return Some(false),
|
||||
TargetStatus::Staged => return StageStatus::Staged,
|
||||
TargetStatus::Unstaged => return StageStatus::Unstaged,
|
||||
TargetStatus::Reverted => continue,
|
||||
TargetStatus::Unchanged => continue,
|
||||
}
|
||||
}
|
||||
}
|
||||
entry.is_staged
|
||||
entry.staging
|
||||
}
|
||||
|
||||
pub(crate) fn has_staged_changes(&self) -> bool {
|
||||
@@ -2599,9 +2664,9 @@ impl GitPanel {
|
||||
let ix = self.entry_by_path(&repo_path)?;
|
||||
let entry = self.entries.get(ix)?;
|
||||
|
||||
let is_staged = self.entry_is_staged(entry.status_entry()?);
|
||||
let entry_staging = self.entry_staging(entry.status_entry()?);
|
||||
|
||||
let checkbox = Checkbox::new("stage-file", is_staged.into())
|
||||
let checkbox = Checkbox::new("stage-file", entry_staging.as_bool().into())
|
||||
.disabled(!self.has_write_access(cx))
|
||||
.fill()
|
||||
.elevation(ElevationIndex::Surface)
|
||||
@@ -2747,7 +2812,7 @@ impl GitPanel {
|
||||
let Some(entry) = self.entries.get(ix).and_then(|e| e.status_entry()) else {
|
||||
return;
|
||||
};
|
||||
let stage_title = if entry.status.is_staged() == Some(true) {
|
||||
let stage_title = if entry.status.staging().is_fully_staged() {
|
||||
"Unstage File"
|
||||
} else {
|
||||
"Stage File"
|
||||
@@ -2853,8 +2918,8 @@ impl GitPanel {
|
||||
let checkbox_id: ElementId =
|
||||
ElementId::Name(format!("entry_{}_{}_checkbox", display_name, ix).into());
|
||||
|
||||
let is_entry_staged = self.entry_is_staged(entry);
|
||||
let mut is_staged: ToggleState = self.entry_is_staged(entry).into();
|
||||
let entry_staging = self.entry_staging(entry);
|
||||
let mut is_staged: ToggleState = self.entry_staging(entry).as_bool().into();
|
||||
|
||||
if !self.has_staged_changes() && !self.has_conflicts() && !entry.status.is_created() {
|
||||
is_staged = ToggleState::Selected;
|
||||
@@ -2973,7 +3038,7 @@ impl GitPanel {
|
||||
})
|
||||
})
|
||||
.tooltip(move |window, cx| {
|
||||
let tooltip_name = if is_entry_staged.unwrap_or(false) {
|
||||
let tooltip_name = if entry_staging.is_fully_staged() {
|
||||
"Unstage"
|
||||
} else {
|
||||
"Stage"
|
||||
@@ -3246,159 +3311,6 @@ impl Render for GitPanelMessageTooltip {
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
struct SplitButton {
|
||||
pub left: ButtonLike,
|
||||
pub right: AnyElement,
|
||||
}
|
||||
|
||||
impl SplitButton {
|
||||
fn new(
|
||||
id: impl Into<SharedString>,
|
||||
left_label: impl Into<SharedString>,
|
||||
ahead_count: usize,
|
||||
behind_count: usize,
|
||||
left_icon: Option<IconName>,
|
||||
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()))
|
||||
.into_any_element();
|
||||
// .on_click(right_on_click);
|
||||
|
||||
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(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![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.),
|
||||
}])
|
||||
}
|
||||
}
|
||||
|
||||
fn render_git_action_menu(id: impl Into<ElementId>) -> 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
|
||||
.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, IntoComponent)]
|
||||
#[component(scope = "Version Control")]
|
||||
pub struct PanelRepoFooter {
|
||||
@@ -3449,200 +3361,6 @@ impl PanelRepoFooter {
|
||||
.menu(move |window, cx| Some(git_panel_context_menu(window, cx)))
|
||||
.anchor(Corner::TopRight)
|
||||
}
|
||||
|
||||
fn panel_focus_handle(&self, cx: &App) -> Option<FocusHandle> {
|
||||
if let Some(git_panel) = self.git_panel.clone() {
|
||||
Some(git_panel.focus_handle(cx))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn render_push_button(&self, id: SharedString, ahead: u32, cx: &mut App) -> SplitButton {
|
||||
let panel = self.git_panel.clone();
|
||||
let panel_focus_handle = self.panel_focus_handle(cx);
|
||||
|
||||
SplitButton::new(
|
||||
id,
|
||||
"Push",
|
||||
ahead as usize,
|
||||
0,
|
||||
None,
|
||||
move |_, window, cx| {
|
||||
if let Some(panel) = panel.as_ref() {
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.push(false, window, cx);
|
||||
});
|
||||
}
|
||||
},
|
||||
move |window, cx| {
|
||||
git_action_tooltip(
|
||||
"Push committed changes to remote",
|
||||
&git::Push,
|
||||
"git push",
|
||||
panel_focus_handle.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn render_pull_button(
|
||||
&self,
|
||||
id: SharedString,
|
||||
ahead: u32,
|
||||
behind: u32,
|
||||
cx: &mut App,
|
||||
) -> SplitButton {
|
||||
let panel = self.git_panel.clone();
|
||||
let panel_focus_handle = self.panel_focus_handle(cx);
|
||||
|
||||
SplitButton::new(
|
||||
id,
|
||||
"Pull",
|
||||
ahead as usize,
|
||||
behind as usize,
|
||||
None,
|
||||
move |_, window, cx| {
|
||||
if let Some(panel) = panel.as_ref() {
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.pull(window, cx);
|
||||
});
|
||||
}
|
||||
},
|
||||
move |window, cx| {
|
||||
git_action_tooltip(
|
||||
"Pull",
|
||||
&git::Pull,
|
||||
"git pull",
|
||||
panel_focus_handle.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn render_fetch_button(&self, id: SharedString, cx: &mut App) -> SplitButton {
|
||||
let panel = self.git_panel.clone();
|
||||
let panel_focus_handle = self.panel_focus_handle(cx);
|
||||
|
||||
SplitButton::new(
|
||||
id,
|
||||
"Fetch",
|
||||
0,
|
||||
0,
|
||||
Some(IconName::ArrowCircle),
|
||||
move |_, window, cx| {
|
||||
if let Some(panel) = panel.as_ref() {
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.fetch(window, cx);
|
||||
});
|
||||
}
|
||||
},
|
||||
move |window, cx| {
|
||||
git_action_tooltip(
|
||||
"Fetch updates from remote",
|
||||
&git::Fetch,
|
||||
"git fetch",
|
||||
panel_focus_handle.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn render_publish_button(&self, id: SharedString, cx: &mut App) -> SplitButton {
|
||||
let panel = self.git_panel.clone();
|
||||
let panel_focus_handle = self.panel_focus_handle(cx);
|
||||
|
||||
SplitButton::new(
|
||||
id,
|
||||
"Publish",
|
||||
0,
|
||||
0,
|
||||
Some(IconName::ArrowUpFromLine),
|
||||
move |_, window, cx| {
|
||||
if let Some(panel) = panel.as_ref() {
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.push(false, window, cx);
|
||||
});
|
||||
}
|
||||
},
|
||||
move |window, cx| {
|
||||
git_action_tooltip(
|
||||
"Publish branch to remote",
|
||||
&git::Push,
|
||||
"git push --set-upstream",
|
||||
panel_focus_handle.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn render_republish_button(&self, id: SharedString, cx: &mut App) -> SplitButton {
|
||||
let panel = self.git_panel.clone();
|
||||
let panel_focus_handle = self.panel_focus_handle(cx);
|
||||
|
||||
SplitButton::new(
|
||||
id,
|
||||
"Republish",
|
||||
0,
|
||||
0,
|
||||
Some(IconName::ArrowUpFromLine),
|
||||
move |_, window, cx| {
|
||||
if let Some(panel) = panel.as_ref() {
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.push(false, window, cx);
|
||||
});
|
||||
}
|
||||
},
|
||||
move |window, cx| {
|
||||
git_action_tooltip(
|
||||
"Re-publish branch to remote",
|
||||
&git::Push,
|
||||
"git push --set-upstream",
|
||||
panel_focus_handle.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn render_relevant_button(
|
||||
&self,
|
||||
id: impl Into<SharedString>,
|
||||
branch: &Branch,
|
||||
cx: &mut App,
|
||||
) -> Option<impl IntoElement> {
|
||||
if let Some(git_panel) = self.git_panel.as_ref() {
|
||||
if !git_panel.read(cx).can_push_and_pull(cx) {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
let id = id.into();
|
||||
let upstream = branch.upstream.as_ref();
|
||||
Some(match upstream {
|
||||
Some(Upstream {
|
||||
tracking: UpstreamTracking::Tracked(UpstreamTrackingStatus { ahead, behind }),
|
||||
..
|
||||
}) => match (*ahead, *behind) {
|
||||
(0, 0) => self.render_fetch_button(id, cx),
|
||||
(ahead, 0) => self.render_push_button(id, ahead, cx),
|
||||
(ahead, behind) => self.render_pull_button(id, ahead, behind, cx),
|
||||
},
|
||||
Some(Upstream {
|
||||
tracking: UpstreamTracking::Gone,
|
||||
..
|
||||
}) => self.render_republish_button(id, cx),
|
||||
None => self.render_publish_button(id, cx),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for PanelRepoFooter {
|
||||
@@ -3758,8 +3476,20 @@ impl RenderOnce for PanelRepoFooter {
|
||||
.children(spinner)
|
||||
.child(self.render_overflow_menu(overflow_menu_id))
|
||||
.when_some(branch, |this, branch| {
|
||||
let button = self.render_relevant_button(self.id.clone(), &branch, cx);
|
||||
this.children(button)
|
||||
let mut focus_handle = None;
|
||||
if let Some(git_panel) = self.git_panel.as_ref() {
|
||||
if !git_panel.read(cx).can_push_and_pull(cx) {
|
||||
return this;
|
||||
}
|
||||
focus_handle = Some(git_panel.focus_handle(cx));
|
||||
}
|
||||
|
||||
this.children(render_remote_button(
|
||||
self.id.clone(),
|
||||
&branch,
|
||||
focus_handle,
|
||||
true,
|
||||
))
|
||||
}),
|
||||
)
|
||||
}
|
||||
@@ -4145,14 +3875,14 @@ mod tests {
|
||||
repo_path: "crates/gpui/gpui.rs".into(),
|
||||
worktree_path: Path::new("gpui.rs").into(),
|
||||
status: StatusCode::Modified.worktree(),
|
||||
is_staged: Some(false),
|
||||
staging: StageStatus::Unstaged,
|
||||
}),
|
||||
GitListEntry::GitStatusEntry(GitStatusEntry {
|
||||
abs_path: path!("/root/zed/crates/util/util.rs").into(),
|
||||
repo_path: "crates/util/util.rs".into(),
|
||||
worktree_path: Path::new("../util/util.rs").into(),
|
||||
status: StatusCode::Modified.worktree(),
|
||||
is_staged: Some(false),
|
||||
staging: StageStatus::Unstaged,
|
||||
},),
|
||||
],
|
||||
);
|
||||
@@ -4219,14 +3949,14 @@ mod tests {
|
||||
repo_path: "crates/gpui/gpui.rs".into(),
|
||||
worktree_path: Path::new("../../gpui/gpui.rs").into(),
|
||||
status: StatusCode::Modified.worktree(),
|
||||
is_staged: Some(false),
|
||||
staging: StageStatus::Unstaged,
|
||||
}),
|
||||
GitListEntry::GitStatusEntry(GitStatusEntry {
|
||||
abs_path: path!("/root/zed/crates/util/util.rs").into(),
|
||||
repo_path: "crates/util/util.rs".into(),
|
||||
worktree_path: Path::new("util.rs").into(),
|
||||
status: StatusCode::Modified.worktree(),
|
||||
is_staged: Some(false),
|
||||
staging: StageStatus::Unstaged,
|
||||
},),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
use ::settings::Settings;
|
||||
use git::status::FileStatus;
|
||||
use git::{
|
||||
repository::{Branch, Upstream, UpstreamTracking, UpstreamTrackingStatus},
|
||||
status::FileStatus,
|
||||
};
|
||||
use git_panel_settings::GitPanelSettings;
|
||||
use gpui::App;
|
||||
use gpui::{App, Entity, FocusHandle};
|
||||
use project::Project;
|
||||
use project_diff::ProjectDiff;
|
||||
use ui::{ActiveTheme, Color, Icon, IconName, IntoElement};
|
||||
use ui::{ActiveTheme, Color, Icon, IconName, IntoElement, SharedString};
|
||||
use workspace::Workspace;
|
||||
|
||||
mod askpass_modal;
|
||||
@@ -89,3 +93,343 @@ pub fn git_status_icon(status: FileStatus, cx: &App) -> impl IntoElement {
|
||||
};
|
||||
Icon::new(icon_name).color(Color::Custom(color))
|
||||
}
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
pub fn render_fetch_button(
|
||||
keybinding_target: Option<FocusHandle>,
|
||||
id: SharedString,
|
||||
) -> SplitButton {
|
||||
SplitButton::new(
|
||||
id,
|
||||
"Fetch",
|
||||
0,
|
||||
0,
|
||||
Some(IconName::ArrowCircle),
|
||||
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,
|
||||
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,
|
||||
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),
|
||||
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),
|
||||
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>) -> 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
|
||||
.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 {
|
||||
fn new(
|
||||
id: impl Into<SharedString>,
|
||||
left_label: impl Into<SharedString>,
|
||||
ahead_count: usize,
|
||||
behind_count: usize,
|
||||
left_icon: Option<IconName>,
|
||||
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(),
|
||||
))
|
||||
.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.),
|
||||
}])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,8 @@ use editor::{
|
||||
use feature_flags::FeatureFlagViewExt;
|
||||
use futures::StreamExt;
|
||||
use git::{
|
||||
status::FileStatus, Commit, StageAll, StageAndNext, ToggleStaged, UnstageAll, UnstageAndNext,
|
||||
repository::Branch, status::FileStatus, Commit, StageAll, StageAndNext, ToggleStaged,
|
||||
UnstageAll, UnstageAndNext,
|
||||
};
|
||||
use gpui::{
|
||||
actions, Action, AnyElement, AnyView, App, AppContext as _, AsyncWindowContext, Entity,
|
||||
@@ -24,27 +25,27 @@ use project::{
|
||||
};
|
||||
use std::any::{Any, TypeId};
|
||||
use theme::ActiveTheme;
|
||||
use ui::{prelude::*, vertical_divider, Tooltip};
|
||||
use ui::{prelude::*, vertical_divider, KeyBinding, Tooltip};
|
||||
use util::ResultExt as _;
|
||||
use workspace::{
|
||||
item::{BreadcrumbText, Item, ItemEvent, ItemHandle, TabContentParams},
|
||||
searchable::SearchableItemHandle,
|
||||
ItemNavHistory, SerializableItem, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView,
|
||||
Workspace,
|
||||
CloseActiveItem, ItemNavHistory, SerializableItem, ToolbarItemEvent, ToolbarItemLocation,
|
||||
ToolbarItemView, Workspace,
|
||||
};
|
||||
|
||||
actions!(git, [Diff]);
|
||||
actions!(git, [Diff, Add]);
|
||||
|
||||
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,
|
||||
}
|
||||
@@ -70,6 +71,9 @@ impl ProjectDiff {
|
||||
let Some(window) = window else { return };
|
||||
cx.when_flag_enabled::<feature_flags::GitUiFeatureFlag>(window, |workspace, _, _cx| {
|
||||
workspace.register_action(Self::deploy);
|
||||
workspace.register_action(|workspace, _: &Add, window, cx| {
|
||||
Self::deploy(workspace, &Diff, window, cx);
|
||||
});
|
||||
});
|
||||
|
||||
workspace::register_serializable_item::<ProjectDiff>(cx);
|
||||
@@ -179,6 +183,7 @@ impl ProjectDiff {
|
||||
multibuffer,
|
||||
pending_scroll: None,
|
||||
update_needed: send,
|
||||
current_branch: None,
|
||||
_task: worker,
|
||||
_subscription: git_store_subscription,
|
||||
}
|
||||
@@ -444,6 +449,20 @@ 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() {
|
||||
@@ -642,9 +661,11 @@ impl Item for ProjectDiff {
|
||||
}
|
||||
|
||||
impl Render for ProjectDiff {
|
||||
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
fn render(&mut self, window: &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" })
|
||||
@@ -654,7 +675,61 @@ impl Render for ProjectDiff {
|
||||
.justify_center()
|
||||
.size_full()
|
||||
.when(is_empty, |el| {
|
||||
el.child(Label::new("No uncommitted changes"))
|
||||
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,
|
||||
);
|
||||
}),
|
||||
),
|
||||
)
|
||||
}),
|
||||
)
|
||||
})
|
||||
.when(!is_empty, |el| el.child(self.editor.clone()))
|
||||
}
|
||||
|
||||
@@ -634,7 +634,7 @@ impl Display for ColorSpace {
|
||||
}
|
||||
|
||||
/// A background color, which can be either a solid color or a linear gradient.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
#[repr(C)]
|
||||
pub struct Background {
|
||||
pub(crate) tag: BackgroundTag,
|
||||
@@ -646,6 +646,28 @@ 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 {
|
||||
|
||||
@@ -49,7 +49,7 @@ use std::{
|
||||
num::NonZeroU32,
|
||||
ops::{Deref, DerefMut, Range},
|
||||
path::{Path, PathBuf},
|
||||
str,
|
||||
rc, str,
|
||||
sync::{Arc, LazyLock},
|
||||
time::{Duration, Instant},
|
||||
vec,
|
||||
@@ -125,6 +125,7 @@ pub struct Buffer {
|
||||
/// Memoize calls to has_changes_since(saved_version).
|
||||
/// The contents of a cell are (self.version, has_changes) at the time of a last call.
|
||||
has_unsaved_edits: Cell<(clock::Global, bool)>,
|
||||
change_bits: Vec<rc::Weak<Cell<bool>>>,
|
||||
_subscriptions: Vec<gpui::Subscription>,
|
||||
}
|
||||
|
||||
@@ -978,6 +979,7 @@ impl Buffer {
|
||||
completion_triggers_timestamp: Default::default(),
|
||||
deferred_ops: OperationQueue::new(),
|
||||
has_conflict: false,
|
||||
change_bits: Default::default(),
|
||||
_subscriptions: Vec::new(),
|
||||
}
|
||||
}
|
||||
@@ -1252,6 +1254,7 @@ impl Buffer {
|
||||
self.non_text_state_update_count += 1;
|
||||
self.syntax_map.lock().clear(&self.text);
|
||||
self.language = language;
|
||||
self.was_changed();
|
||||
self.reparse(cx);
|
||||
cx.emit(BufferEvent::LanguageChanged);
|
||||
}
|
||||
@@ -1286,6 +1289,7 @@ impl Buffer {
|
||||
.set((self.saved_version().clone(), false));
|
||||
self.has_conflict = false;
|
||||
self.saved_mtime = mtime;
|
||||
self.was_changed();
|
||||
cx.emit(BufferEvent::Saved);
|
||||
cx.notify();
|
||||
}
|
||||
@@ -1381,6 +1385,7 @@ impl Buffer {
|
||||
|
||||
self.file = Some(new_file);
|
||||
if file_changed {
|
||||
self.was_changed();
|
||||
self.non_text_state_update_count += 1;
|
||||
if was_dirty != self.is_dirty() {
|
||||
cx.emit(BufferEvent::DirtyChanged);
|
||||
@@ -1958,6 +1963,23 @@ impl Buffer {
|
||||
self.text.subscribe()
|
||||
}
|
||||
|
||||
/// Adds a bit to the list of bits that are set when the buffer's text changes.
|
||||
///
|
||||
/// This allows downstream code to check if the buffer's text has changed without
|
||||
/// waiting for an effect cycle, which would be required if using eents.
|
||||
pub fn record_changes(&mut self, bit: rc::Weak<Cell<bool>>) {
|
||||
self.change_bits.push(bit);
|
||||
}
|
||||
|
||||
fn was_changed(&mut self) {
|
||||
self.change_bits.retain(|change_bit| {
|
||||
change_bit.upgrade().map_or(false, |bit| {
|
||||
bit.replace(true);
|
||||
true
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
/// Starts a transaction, if one is not already in-progress. When undoing or
|
||||
/// redoing edits, all of the edits performed within a transaction are undone
|
||||
/// or redone together.
|
||||
@@ -2368,6 +2390,7 @@ impl Buffer {
|
||||
}
|
||||
self.text.apply_ops(buffer_ops);
|
||||
self.deferred_ops.insert(deferred_ops);
|
||||
self.was_changed();
|
||||
self.flush_deferred_ops(cx);
|
||||
self.did_edit(&old_version, was_dirty, cx);
|
||||
// Notify independently of whether the buffer was edited as the operations could include a
|
||||
@@ -2502,7 +2525,8 @@ impl Buffer {
|
||||
}
|
||||
}
|
||||
|
||||
fn send_operation(&self, operation: Operation, is_local: bool, cx: &mut Context<Self>) {
|
||||
fn send_operation(&mut self, operation: Operation, is_local: bool, cx: &mut Context<Self>) {
|
||||
self.was_changed();
|
||||
cx.emit(BufferEvent::Operation {
|
||||
operation,
|
||||
is_local,
|
||||
|
||||
@@ -31,7 +31,7 @@ use smol::future::yield_now;
|
||||
use std::{
|
||||
any::type_name,
|
||||
borrow::Cow,
|
||||
cell::{Ref, RefCell},
|
||||
cell::{Cell, Ref, RefCell},
|
||||
cmp, fmt,
|
||||
future::Future,
|
||||
io,
|
||||
@@ -39,6 +39,7 @@ use std::{
|
||||
mem,
|
||||
ops::{Range, RangeBounds, Sub},
|
||||
path::Path,
|
||||
rc::Rc,
|
||||
str,
|
||||
sync::Arc,
|
||||
time::{Duration, Instant},
|
||||
@@ -76,6 +77,7 @@ pub struct MultiBuffer {
|
||||
history: History,
|
||||
title: Option<String>,
|
||||
capability: Capability,
|
||||
buffer_changed_since_sync: Rc<Cell<bool>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
@@ -121,6 +123,7 @@ pub enum Event {
|
||||
Discarded,
|
||||
DirtyChanged,
|
||||
DiagnosticsUpdated,
|
||||
BufferDiffChanged,
|
||||
}
|
||||
|
||||
/// A diff hunk, representing a range of consequent lines in a multibuffer.
|
||||
@@ -253,6 +256,7 @@ impl DiffState {
|
||||
if let Some(changed_range) = changed_range.clone() {
|
||||
this.buffer_diff_changed(diff, changed_range, cx)
|
||||
}
|
||||
cx.emit(Event::BufferDiffChanged);
|
||||
}
|
||||
BufferDiffEvent::LanguageChanged => this.buffer_diff_language_changed(diff, cx),
|
||||
_ => {}
|
||||
@@ -566,6 +570,7 @@ impl MultiBuffer {
|
||||
capability,
|
||||
title: None,
|
||||
buffers_by_path: Default::default(),
|
||||
buffer_changed_since_sync: Default::default(),
|
||||
history: History {
|
||||
next_transaction_id: clock::Lamport::default(),
|
||||
undo_stack: Vec::new(),
|
||||
@@ -585,6 +590,7 @@ impl MultiBuffer {
|
||||
subscriptions: Default::default(),
|
||||
singleton: false,
|
||||
capability,
|
||||
buffer_changed_since_sync: Default::default(),
|
||||
history: History {
|
||||
next_transaction_id: Default::default(),
|
||||
undo_stack: Default::default(),
|
||||
@@ -598,7 +604,11 @@ impl MultiBuffer {
|
||||
|
||||
pub fn clone(&self, new_cx: &mut Context<Self>) -> Self {
|
||||
let mut buffers = HashMap::default();
|
||||
let buffer_changed_since_sync = Rc::new(Cell::new(false));
|
||||
for (buffer_id, buffer_state) in self.buffers.borrow().iter() {
|
||||
buffer_state.buffer.update(new_cx, |buffer, _| {
|
||||
buffer.record_changes(Rc::downgrade(&buffer_changed_since_sync));
|
||||
});
|
||||
buffers.insert(
|
||||
*buffer_id,
|
||||
BufferState {
|
||||
@@ -627,6 +637,7 @@ impl MultiBuffer {
|
||||
capability: self.capability,
|
||||
history: self.history.clone(),
|
||||
title: self.title.clone(),
|
||||
buffer_changed_since_sync,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1726,19 +1737,25 @@ impl MultiBuffer {
|
||||
|
||||
self.sync(cx);
|
||||
|
||||
let buffer_id = buffer.read(cx).remote_id();
|
||||
let buffer_snapshot = buffer.read(cx).snapshot();
|
||||
let buffer_id = buffer_snapshot.remote_id();
|
||||
|
||||
let mut buffers = self.buffers.borrow_mut();
|
||||
let buffer_state = buffers.entry(buffer_id).or_insert_with(|| BufferState {
|
||||
last_version: buffer_snapshot.version().clone(),
|
||||
last_non_text_state_update_count: buffer_snapshot.non_text_state_update_count(),
|
||||
excerpts: Default::default(),
|
||||
_subscriptions: [
|
||||
cx.observe(&buffer, |_, _, cx| cx.notify()),
|
||||
cx.subscribe(&buffer, Self::on_buffer_event),
|
||||
],
|
||||
buffer: buffer.clone(),
|
||||
let buffer_state = buffers.entry(buffer_id).or_insert_with(|| {
|
||||
self.buffer_changed_since_sync.replace(true);
|
||||
buffer.update(cx, |buffer, _| {
|
||||
buffer.record_changes(Rc::downgrade(&self.buffer_changed_since_sync));
|
||||
});
|
||||
BufferState {
|
||||
last_version: buffer_snapshot.version().clone(),
|
||||
last_non_text_state_update_count: buffer_snapshot.non_text_state_update_count(),
|
||||
excerpts: Default::default(),
|
||||
_subscriptions: [
|
||||
cx.observe(&buffer, |_, _, cx| cx.notify()),
|
||||
cx.subscribe(&buffer, Self::on_buffer_event),
|
||||
],
|
||||
buffer: buffer.clone(),
|
||||
}
|
||||
});
|
||||
|
||||
let mut snapshot = self.snapshot.borrow_mut();
|
||||
@@ -2234,6 +2251,7 @@ impl MultiBuffer {
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.sync(cx);
|
||||
self.buffer_changed_since_sync.replace(true);
|
||||
|
||||
let diff = diff.read(cx);
|
||||
let buffer_id = diff.buffer_id;
|
||||
@@ -2712,6 +2730,11 @@ impl MultiBuffer {
|
||||
}
|
||||
|
||||
fn sync(&self, cx: &App) {
|
||||
let changed = self.buffer_changed_since_sync.replace(false);
|
||||
if !changed {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut snapshot = self.snapshot.borrow_mut();
|
||||
let mut excerpts_to_edit = Vec::new();
|
||||
let mut non_text_state_updated = false;
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::{path::Path, sync::Arc};
|
||||
use util::ResultExt;
|
||||
|
||||
use collections::HashMap;
|
||||
use gpui::{App, AppContext as _, Context, Entity, Task};
|
||||
use gpui::{App, AppContext as _, Context, Entity, EventEmitter, Task};
|
||||
use settings::Settings as _;
|
||||
use worktree::WorktreeId;
|
||||
|
||||
@@ -19,6 +19,12 @@ pub struct ProjectEnvironment {
|
||||
environment_error_messages: HashMap<WorktreeId, EnvironmentErrorMessage>,
|
||||
}
|
||||
|
||||
pub enum ProjectEnvironmentEvent {
|
||||
ErrorsUpdated,
|
||||
}
|
||||
|
||||
impl EventEmitter<ProjectEnvironmentEvent> for ProjectEnvironment {}
|
||||
|
||||
impl ProjectEnvironment {
|
||||
pub fn new(
|
||||
worktree_store: &Entity<WorktreeStore>,
|
||||
@@ -65,8 +71,13 @@ impl ProjectEnvironment {
|
||||
self.environment_error_messages.iter()
|
||||
}
|
||||
|
||||
pub(crate) fn remove_environment_error(&mut self, worktree_id: WorktreeId) {
|
||||
pub(crate) fn remove_environment_error(
|
||||
&mut self,
|
||||
worktree_id: WorktreeId,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.environment_error_messages.remove(&worktree_id);
|
||||
cx.emit(ProjectEnvironmentEvent::ErrorsUpdated);
|
||||
}
|
||||
|
||||
/// Returns the project environment, if possible.
|
||||
@@ -158,8 +169,9 @@ impl ProjectEnvironment {
|
||||
}
|
||||
|
||||
if let Some(error) = error_message {
|
||||
this.update(&mut cx, |this, _| {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.environment_error_messages.insert(worktree_id, error);
|
||||
cx.emit(ProjectEnvironmentEvent::ErrorsUpdated)
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
|
||||
@@ -1353,7 +1353,7 @@ impl Repository {
|
||||
let to_stage = self
|
||||
.repository_entry
|
||||
.status()
|
||||
.filter(|entry| !entry.status.is_staged().unwrap_or(false))
|
||||
.filter(|entry| !entry.status.staging().is_fully_staged())
|
||||
.map(|entry| entry.repo_path.clone())
|
||||
.collect();
|
||||
self.stage_entries(to_stage, cx)
|
||||
@@ -1363,7 +1363,7 @@ impl Repository {
|
||||
let to_unstage = self
|
||||
.repository_entry
|
||||
.status()
|
||||
.filter(|entry| entry.status.is_staged().unwrap_or(true))
|
||||
.filter(|entry| entry.status.staging().has_staged())
|
||||
.map(|entry| entry.repo_path.clone())
|
||||
.collect();
|
||||
self.unstage_entries(to_unstage, cx)
|
||||
|
||||
@@ -2,9 +2,9 @@ mod signature_help;
|
||||
|
||||
use crate::{
|
||||
lsp_store::{LocalLspStore, LspStore},
|
||||
ActionVariant, CodeAction, CoreCompletion, DocumentHighlight, Hover, HoverBlock,
|
||||
CodeAction, CompletionSource, CoreCompletion, DocumentHighlight, Hover, HoverBlock,
|
||||
HoverBlockKind, InlayHint, InlayHintLabel, InlayHintLabelPart, InlayHintLabelPartTooltip,
|
||||
InlayHintTooltip, Location, LocationLink, MarkupContent, PrepareRenameResponse,
|
||||
InlayHintTooltip, Location, LocationLink, LspAction, MarkupContent, PrepareRenameResponse,
|
||||
ProjectTransaction, ResolveState,
|
||||
};
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
@@ -2011,9 +2011,11 @@ impl LspCommand for GetCompletions {
|
||||
CoreCompletion {
|
||||
old_range,
|
||||
new_text,
|
||||
server_id,
|
||||
lsp_completion,
|
||||
resolved: false,
|
||||
source: CompletionSource::Lsp {
|
||||
server_id,
|
||||
lsp_completion: Box::new(lsp_completion),
|
||||
resolved: false,
|
||||
},
|
||||
}
|
||||
})
|
||||
.collect())
|
||||
@@ -2256,11 +2258,11 @@ impl LspCommand for GetCodeActions {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
ActionVariant::Action(Box::new(lsp_action))
|
||||
LspAction::Action(Box::new(lsp_action))
|
||||
}
|
||||
lsp::CodeActionOrCommand::Command(command) => {
|
||||
if available_commands.contains(&command.command) {
|
||||
ActionVariant::Command(command)
|
||||
LspAction::Command(command)
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -14,8 +14,8 @@ use crate::{
|
||||
toolchain_store::{EmptyToolchainStore, ToolchainStoreEvent},
|
||||
worktree_store::{WorktreeStore, WorktreeStoreEvent},
|
||||
yarn::YarnPathStore,
|
||||
ActionVariant, CodeAction, Completion, CoreCompletion, Hover, InlayHint, ProjectItem as _,
|
||||
ProjectPath, ProjectTransaction, ResolveState, Symbol, ToolchainStore,
|
||||
CodeAction, Completion, CompletionSource, CoreCompletion, Hover, InlayHint, LspAction,
|
||||
ProjectItem as _, ProjectPath, ProjectTransaction, ResolveState, Symbol, ToolchainStore,
|
||||
};
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use async_trait::async_trait;
|
||||
@@ -1629,7 +1629,7 @@ impl LocalLspStore {
|
||||
action: &mut CodeAction,
|
||||
) -> anyhow::Result<()> {
|
||||
match &mut action.lsp_action {
|
||||
ActionVariant::Action(lsp_action) => {
|
||||
LspAction::Action(lsp_action) => {
|
||||
if GetCodeActions::can_resolve_actions(&lang_server.capabilities())
|
||||
&& lsp_action.data.is_some()
|
||||
&& (lsp_action.command.is_none() || lsp_action.edit.is_none())
|
||||
@@ -1641,7 +1641,7 @@ impl LocalLspStore {
|
||||
);
|
||||
}
|
||||
}
|
||||
ActionVariant::Command(_) => {}
|
||||
LspAction::Command(_) => {}
|
||||
}
|
||||
anyhow::Ok(())
|
||||
}
|
||||
@@ -4401,26 +4401,33 @@ impl LspStore {
|
||||
let mut did_resolve = false;
|
||||
if let Some((client, project_id)) = client {
|
||||
for completion_index in completion_indices {
|
||||
let server_id = completions.borrow()[completion_index].server_id;
|
||||
|
||||
if Self::resolve_completion_remote(
|
||||
project_id,
|
||||
server_id,
|
||||
buffer_id,
|
||||
completions.clone(),
|
||||
completion_index,
|
||||
client.clone(),
|
||||
)
|
||||
.await
|
||||
.log_err()
|
||||
.is_some()
|
||||
{
|
||||
did_resolve = true;
|
||||
let server_id = {
|
||||
let completion = &completions.borrow()[completion_index];
|
||||
completion.source.server_id()
|
||||
};
|
||||
if let Some(server_id) = server_id {
|
||||
if Self::resolve_completion_remote(
|
||||
project_id,
|
||||
server_id,
|
||||
buffer_id,
|
||||
completions.clone(),
|
||||
completion_index,
|
||||
client.clone(),
|
||||
)
|
||||
.await
|
||||
.log_err()
|
||||
.is_some()
|
||||
{
|
||||
did_resolve = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for completion_index in completion_indices {
|
||||
let server_id = completions.borrow()[completion_index].server_id;
|
||||
let Some(server_id) = completions.borrow()[completion_index].source.server_id()
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let server_and_adapter = this
|
||||
.read_with(&cx, |lsp_store, _| {
|
||||
@@ -4480,10 +4487,19 @@ impl LspStore {
|
||||
|
||||
let request = {
|
||||
let completion = &completions.borrow()[completion_index];
|
||||
if completion.resolved {
|
||||
return Ok(());
|
||||
match &completion.source {
|
||||
CompletionSource::Lsp {
|
||||
lsp_completion,
|
||||
resolved,
|
||||
..
|
||||
} => {
|
||||
if *resolved {
|
||||
return Ok(());
|
||||
}
|
||||
server.request::<lsp::request::ResolveCompletionItem>(*lsp_completion.clone())
|
||||
}
|
||||
CompletionSource::Custom => return Ok(()),
|
||||
}
|
||||
server.request::<lsp::request::ResolveCompletionItem>(completion.lsp_completion.clone())
|
||||
};
|
||||
let completion_item = request.await?;
|
||||
|
||||
@@ -4508,15 +4524,20 @@ impl LspStore {
|
||||
// vtsls might change the type of completion after resolution.
|
||||
let mut completions = completions.borrow_mut();
|
||||
let completion = &mut completions[completion_index];
|
||||
if completion_item.insert_text_format != completion.lsp_completion.insert_text_format {
|
||||
completion.lsp_completion.insert_text_format = completion_item.insert_text_format;
|
||||
if let Some(lsp_completion) = completion.source.lsp_completion_mut() {
|
||||
if completion_item.insert_text_format != lsp_completion.insert_text_format {
|
||||
lsp_completion.insert_text_format = completion_item.insert_text_format;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut completions = completions.borrow_mut();
|
||||
let completion = &mut completions[completion_index];
|
||||
completion.lsp_completion = completion_item;
|
||||
completion.resolved = true;
|
||||
completion.source = CompletionSource::Lsp {
|
||||
lsp_completion: Box::new(completion_item),
|
||||
resolved: true,
|
||||
server_id: server.server_id(),
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -4527,9 +4548,13 @@ impl LspStore {
|
||||
completion_index: usize,
|
||||
) -> Result<()> {
|
||||
let completion_item = completions.borrow()[completion_index]
|
||||
.lsp_completion
|
||||
.clone();
|
||||
if let Some(lsp_documentation) = completion_item.documentation.clone() {
|
||||
.source
|
||||
.lsp_completion()
|
||||
.cloned();
|
||||
if let Some(lsp_documentation) = completion_item
|
||||
.as_ref()
|
||||
.and_then(|completion_item| completion_item.documentation.clone())
|
||||
{
|
||||
let mut completions = completions.borrow_mut();
|
||||
let completion = &mut completions[completion_index];
|
||||
completion.documentation = Some(lsp_documentation.into());
|
||||
@@ -4539,25 +4564,33 @@ impl LspStore {
|
||||
completion.documentation = Some(CompletionDocumentation::Undocumented);
|
||||
}
|
||||
|
||||
// NB: Zed does not have `details` inside the completion resolve capabilities, but certain language servers violate the spec and do not return `details` immediately, e.g. https://github.com/yioneko/vtsls/issues/213
|
||||
// So we have to update the label here anyway...
|
||||
let language = snapshot.language();
|
||||
let mut new_label = match language {
|
||||
Some(language) => {
|
||||
adapter
|
||||
.labels_for_completions(&[completion_item.clone()], language)
|
||||
.await?
|
||||
let mut new_label = match completion_item {
|
||||
Some(completion_item) => {
|
||||
// NB: Zed does not have `details` inside the completion resolve capabilities, but certain language servers violate the spec and do not return `details` immediately, e.g. https://github.com/yioneko/vtsls/issues/213
|
||||
// So we have to update the label here anyway...
|
||||
let language = snapshot.language();
|
||||
match language {
|
||||
Some(language) => {
|
||||
adapter
|
||||
.labels_for_completions(&[completion_item.clone()], language)
|
||||
.await?
|
||||
}
|
||||
None => Vec::new(),
|
||||
}
|
||||
.pop()
|
||||
.flatten()
|
||||
.unwrap_or_else(|| {
|
||||
CodeLabel::fallback_for_completion(
|
||||
&completion_item,
|
||||
language.map(|language| language.as_ref()),
|
||||
)
|
||||
})
|
||||
}
|
||||
None => Vec::new(),
|
||||
}
|
||||
.pop()
|
||||
.flatten()
|
||||
.unwrap_or_else(|| {
|
||||
CodeLabel::fallback_for_completion(
|
||||
&completion_item,
|
||||
language.map(|language| language.as_ref()),
|
||||
)
|
||||
});
|
||||
None => CodeLabel::plain(
|
||||
completions.borrow()[completion_index].new_text.clone(),
|
||||
None,
|
||||
),
|
||||
};
|
||||
ensure_uniform_list_compatible_label(&mut new_label);
|
||||
|
||||
let mut completions = completions.borrow_mut();
|
||||
@@ -4589,12 +4622,19 @@ impl LspStore {
|
||||
) -> Result<()> {
|
||||
let lsp_completion = {
|
||||
let completion = &completions.borrow()[completion_index];
|
||||
if completion.resolved {
|
||||
return Ok(());
|
||||
match &completion.source {
|
||||
CompletionSource::Lsp {
|
||||
lsp_completion,
|
||||
resolved,
|
||||
..
|
||||
} => {
|
||||
if *resolved {
|
||||
return Ok(());
|
||||
}
|
||||
serde_json::to_string(lsp_completion).unwrap().into_bytes()
|
||||
}
|
||||
CompletionSource::Custom => return Ok(()),
|
||||
}
|
||||
serde_json::to_string(&completion.lsp_completion)
|
||||
.unwrap()
|
||||
.into_bytes()
|
||||
};
|
||||
let request = proto::ResolveCompletionDocumentation {
|
||||
project_id,
|
||||
@@ -4622,8 +4662,11 @@ impl LspStore {
|
||||
let mut completions = completions.borrow_mut();
|
||||
let completion = &mut completions[completion_index];
|
||||
completion.documentation = Some(documentation);
|
||||
completion.lsp_completion = lsp_completion;
|
||||
completion.resolved = true;
|
||||
completion.source = CompletionSource::Lsp {
|
||||
server_id,
|
||||
lsp_completion,
|
||||
resolved: true,
|
||||
};
|
||||
|
||||
let old_range = response
|
||||
.old_start
|
||||
@@ -4659,17 +4702,12 @@ impl LspStore {
|
||||
completion: Some(Self::serialize_completion(&CoreCompletion {
|
||||
old_range: completion.old_range,
|
||||
new_text: completion.new_text,
|
||||
server_id: completion.server_id,
|
||||
lsp_completion: completion.lsp_completion,
|
||||
resolved: completion.resolved,
|
||||
source: completion.source,
|
||||
})),
|
||||
}
|
||||
};
|
||||
|
||||
let response = client.request(request).await?;
|
||||
completions.borrow_mut()[completion_index].resolved = true;
|
||||
|
||||
if let Some(transaction) = response.transaction {
|
||||
if let Some(transaction) = client.request(request).await?.transaction {
|
||||
let transaction = language::proto::deserialize_transaction(transaction)?;
|
||||
buffer_handle
|
||||
.update(&mut cx, |buffer, _| {
|
||||
@@ -4687,8 +4725,9 @@ impl LspStore {
|
||||
}
|
||||
})
|
||||
} else {
|
||||
let server_id = completions.borrow()[completion_index].server_id;
|
||||
let Some(server) = buffer_handle.update(cx, |buffer, cx| {
|
||||
let completion = &completions.borrow()[completion_index];
|
||||
let server_id = completion.source.server_id()?;
|
||||
Some(
|
||||
self.language_server_for_local_buffer(buffer, server_id, cx)?
|
||||
.1
|
||||
@@ -4709,7 +4748,11 @@ impl LspStore {
|
||||
.await
|
||||
.context("resolving completion")?;
|
||||
let completion = completions.borrow()[completion_index].clone();
|
||||
let additional_text_edits = completion.lsp_completion.additional_text_edits;
|
||||
let additional_text_edits = completion
|
||||
.source
|
||||
.lsp_completion()
|
||||
.as_ref()
|
||||
.and_then(|lsp_completion| lsp_completion.additional_text_edits.clone());
|
||||
if let Some(edits) = additional_text_edits {
|
||||
let edits = this
|
||||
.update(&mut cx, |this, cx| {
|
||||
@@ -6667,33 +6710,19 @@ impl LspStore {
|
||||
cx,
|
||||
);
|
||||
}
|
||||
lsp::WorkDoneProgress::Report(report) => {
|
||||
if self.on_lsp_work_progress(
|
||||
language_server_id,
|
||||
token.clone(),
|
||||
LanguageServerProgress {
|
||||
title: None,
|
||||
is_disk_based_diagnostics_progress,
|
||||
is_cancellable: report.cancellable.unwrap_or(false),
|
||||
message: report.message.clone(),
|
||||
percentage: report.percentage.map(|p| p as usize),
|
||||
last_update_at: cx.background_executor().now(),
|
||||
},
|
||||
cx,
|
||||
) {
|
||||
cx.emit(LspStoreEvent::LanguageServerUpdate {
|
||||
language_server_id,
|
||||
message: proto::update_language_server::Variant::WorkProgress(
|
||||
proto::LspWorkProgress {
|
||||
token,
|
||||
message: report.message,
|
||||
percentage: report.percentage,
|
||||
is_cancellable: report.cancellable,
|
||||
},
|
||||
),
|
||||
})
|
||||
}
|
||||
}
|
||||
lsp::WorkDoneProgress::Report(report) => self.on_lsp_work_progress(
|
||||
language_server_id,
|
||||
token,
|
||||
LanguageServerProgress {
|
||||
title: None,
|
||||
is_disk_based_diagnostics_progress,
|
||||
is_cancellable: report.cancellable.unwrap_or(false),
|
||||
message: report.message,
|
||||
percentage: report.percentage.map(|p| p as usize),
|
||||
last_update_at: cx.background_executor().now(),
|
||||
},
|
||||
cx,
|
||||
),
|
||||
lsp::WorkDoneProgress::End(_) => {
|
||||
language_server_status.progress_tokens.remove(&token);
|
||||
self.on_lsp_work_end(language_server_id, token.clone(), cx);
|
||||
@@ -6733,13 +6762,13 @@ impl LspStore {
|
||||
token: String,
|
||||
progress: LanguageServerProgress,
|
||||
cx: &mut Context<Self>,
|
||||
) -> bool {
|
||||
) {
|
||||
let mut did_update = false;
|
||||
if let Some(status) = self.language_server_statuses.get_mut(&language_server_id) {
|
||||
match status.pending_work.entry(token) {
|
||||
match status.pending_work.entry(token.clone()) {
|
||||
btree_map::Entry::Vacant(entry) => {
|
||||
entry.insert(progress);
|
||||
cx.notify();
|
||||
return true;
|
||||
entry.insert(progress.clone());
|
||||
did_update = true;
|
||||
}
|
||||
btree_map::Entry::Occupied(mut entry) => {
|
||||
let entry = entry.get_mut();
|
||||
@@ -6748,7 +6777,7 @@ impl LspStore {
|
||||
{
|
||||
entry.last_update_at = progress.last_update_at;
|
||||
if progress.message.is_some() {
|
||||
entry.message = progress.message;
|
||||
entry.message = progress.message.clone();
|
||||
}
|
||||
if progress.percentage.is_some() {
|
||||
entry.percentage = progress.percentage;
|
||||
@@ -6756,14 +6785,25 @@ impl LspStore {
|
||||
if progress.is_cancellable != entry.is_cancellable {
|
||||
entry.is_cancellable = progress.is_cancellable;
|
||||
}
|
||||
cx.notify();
|
||||
return true;
|
||||
did_update = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
if did_update {
|
||||
cx.emit(LspStoreEvent::LanguageServerUpdate {
|
||||
language_server_id,
|
||||
message: proto::update_language_server::Variant::WorkProgress(
|
||||
proto::LspWorkProgress {
|
||||
token,
|
||||
message: progress.message,
|
||||
percentage: progress.percentage.map(|p| p as u32),
|
||||
is_cancellable: Some(progress.is_cancellable),
|
||||
},
|
||||
),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn on_lsp_work_end(
|
||||
@@ -7142,8 +7182,7 @@ impl LspStore {
|
||||
Rc::new(RefCell::new(Box::new([Completion {
|
||||
old_range: completion.old_range,
|
||||
new_text: completion.new_text,
|
||||
lsp_completion: completion.lsp_completion,
|
||||
server_id: completion.server_id,
|
||||
source: completion.source,
|
||||
documentation: None,
|
||||
label: CodeLabel {
|
||||
text: Default::default(),
|
||||
@@ -7151,7 +7190,6 @@ impl LspStore {
|
||||
filter_range: Default::default(),
|
||||
},
|
||||
confirm: None,
|
||||
resolved: completion.resolved,
|
||||
}]))),
|
||||
0,
|
||||
false,
|
||||
@@ -8115,13 +8153,33 @@ impl LspStore {
|
||||
}
|
||||
|
||||
pub(crate) fn serialize_completion(completion: &CoreCompletion) -> proto::Completion {
|
||||
let (source, server_id, lsp_completion, resolved) = match &completion.source {
|
||||
CompletionSource::Lsp {
|
||||
server_id,
|
||||
lsp_completion,
|
||||
resolved,
|
||||
} => (
|
||||
proto::completion::Source::Lsp as i32,
|
||||
server_id.0 as u64,
|
||||
serde_json::to_vec(lsp_completion).unwrap(),
|
||||
*resolved,
|
||||
),
|
||||
CompletionSource::Custom => (
|
||||
proto::completion::Source::Custom as i32,
|
||||
0,
|
||||
Vec::new(),
|
||||
true,
|
||||
),
|
||||
};
|
||||
|
||||
proto::Completion {
|
||||
old_start: Some(serialize_anchor(&completion.old_range.start)),
|
||||
old_end: Some(serialize_anchor(&completion.old_range.end)),
|
||||
new_text: completion.new_text.clone(),
|
||||
server_id: completion.server_id.0 as u64,
|
||||
lsp_completion: serde_json::to_vec(&completion.lsp_completion).unwrap(),
|
||||
resolved: completion.resolved,
|
||||
server_id,
|
||||
lsp_completion,
|
||||
resolved,
|
||||
source,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8134,24 +8192,28 @@ impl LspStore {
|
||||
.old_end
|
||||
.and_then(deserialize_anchor)
|
||||
.ok_or_else(|| anyhow!("invalid old end"))?;
|
||||
let lsp_completion = serde_json::from_slice(&completion.lsp_completion)?;
|
||||
|
||||
Ok(CoreCompletion {
|
||||
old_range: old_start..old_end,
|
||||
new_text: completion.new_text,
|
||||
server_id: LanguageServerId(completion.server_id as usize),
|
||||
lsp_completion,
|
||||
resolved: completion.resolved,
|
||||
source: match proto::completion::Source::from_i32(completion.source) {
|
||||
Some(proto::completion::Source::Custom) => CompletionSource::Custom,
|
||||
Some(proto::completion::Source::Lsp) => CompletionSource::Lsp {
|
||||
server_id: LanguageServerId::from_proto(completion.server_id),
|
||||
lsp_completion: serde_json::from_slice(&completion.lsp_completion)?,
|
||||
resolved: completion.resolved,
|
||||
},
|
||||
_ => anyhow::bail!("Unexpected completion source {}", completion.source),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn serialize_code_action(action: &CodeAction) -> proto::CodeAction {
|
||||
let (kind, lsp_action) = match &action.lsp_action {
|
||||
ActionVariant::Action(code_action) => (
|
||||
LspAction::Action(code_action) => (
|
||||
proto::code_action::Kind::Action as i32,
|
||||
serde_json::to_vec(code_action).unwrap(),
|
||||
),
|
||||
ActionVariant::Command(command) => (
|
||||
LspAction::Command(command) => (
|
||||
proto::code_action::Kind::Command as i32,
|
||||
serde_json::to_vec(command).unwrap(),
|
||||
),
|
||||
@@ -8177,10 +8239,10 @@ impl LspStore {
|
||||
.ok_or_else(|| anyhow!("invalid end"))?;
|
||||
let lsp_action = match proto::code_action::Kind::from_i32(action.kind) {
|
||||
Some(proto::code_action::Kind::Action) => {
|
||||
ActionVariant::Action(serde_json::from_slice(&action.lsp_action)?)
|
||||
LspAction::Action(serde_json::from_slice(&action.lsp_action)?)
|
||||
}
|
||||
Some(proto::code_action::Kind::Command) => {
|
||||
ActionVariant::Command(serde_json::from_slice(&action.lsp_action)?)
|
||||
LspAction::Command(serde_json::from_slice(&action.lsp_action)?)
|
||||
}
|
||||
None => anyhow::bail!("Unknown action kind {}", action.kind),
|
||||
};
|
||||
@@ -8218,17 +8280,23 @@ fn remove_empty_hover_blocks(mut hover: Hover) -> Option<Hover> {
|
||||
}
|
||||
|
||||
async fn populate_labels_for_completions(
|
||||
mut new_completions: Vec<CoreCompletion>,
|
||||
new_completions: Vec<CoreCompletion>,
|
||||
language: Option<Arc<Language>>,
|
||||
lsp_adapter: Option<Arc<CachedLspAdapter>>,
|
||||
completions: &mut Vec<Completion>,
|
||||
) {
|
||||
let lsp_completions = new_completions
|
||||
.iter_mut()
|
||||
.map(|completion| mem::take(&mut completion.lsp_completion))
|
||||
.iter()
|
||||
.filter_map(|new_completion| {
|
||||
if let CompletionSource::Lsp { lsp_completion, .. } = &new_completion.source {
|
||||
Some(*lsp_completion.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let labels = if let Some((language, lsp_adapter)) = language.as_ref().zip(lsp_adapter) {
|
||||
let mut labels = if let Some((language, lsp_adapter)) = language.as_ref().zip(lsp_adapter) {
|
||||
lsp_adapter
|
||||
.labels_for_completions(&lsp_completions, language)
|
||||
.await
|
||||
@@ -8236,34 +8304,45 @@ async fn populate_labels_for_completions(
|
||||
.unwrap_or_default()
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
}
|
||||
.into_iter()
|
||||
.fuse();
|
||||
|
||||
for ((completion, lsp_completion), label) in new_completions
|
||||
.into_iter()
|
||||
.zip(lsp_completions)
|
||||
.zip(labels.into_iter().chain(iter::repeat(None)))
|
||||
{
|
||||
let documentation = if let Some(docs) = lsp_completion.documentation.clone() {
|
||||
Some(docs.into())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
for completion in new_completions {
|
||||
match &completion.source {
|
||||
CompletionSource::Lsp { lsp_completion, .. } => {
|
||||
let documentation = if let Some(docs) = lsp_completion.documentation.clone() {
|
||||
Some(docs.into())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let mut label = label.unwrap_or_else(|| {
|
||||
CodeLabel::fallback_for_completion(&lsp_completion, language.as_deref())
|
||||
});
|
||||
ensure_uniform_list_compatible_label(&mut label);
|
||||
|
||||
completions.push(Completion {
|
||||
old_range: completion.old_range,
|
||||
new_text: completion.new_text,
|
||||
label,
|
||||
server_id: completion.server_id,
|
||||
documentation,
|
||||
lsp_completion,
|
||||
confirm: None,
|
||||
resolved: false,
|
||||
})
|
||||
let mut label = labels.next().flatten().unwrap_or_else(|| {
|
||||
CodeLabel::fallback_for_completion(&lsp_completion, language.as_deref())
|
||||
});
|
||||
ensure_uniform_list_compatible_label(&mut label);
|
||||
completions.push(Completion {
|
||||
label,
|
||||
documentation,
|
||||
old_range: completion.old_range,
|
||||
new_text: completion.new_text,
|
||||
source: completion.source,
|
||||
confirm: None,
|
||||
})
|
||||
}
|
||||
CompletionSource::Custom => {
|
||||
let mut label = CodeLabel::plain(completion.new_text.clone(), None);
|
||||
ensure_uniform_list_compatible_label(&mut label);
|
||||
completions.push(Completion {
|
||||
label,
|
||||
documentation: None,
|
||||
old_range: completion.old_range,
|
||||
new_text: completion.new_text,
|
||||
source: completion.source,
|
||||
confirm: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ mod project_tests;
|
||||
mod direnv;
|
||||
mod environment;
|
||||
use buffer_diff::BufferDiff;
|
||||
pub use environment::EnvironmentErrorMessage;
|
||||
pub use environment::{EnvironmentErrorMessage, ProjectEnvironmentEvent};
|
||||
use git::Repository;
|
||||
pub mod search_history;
|
||||
mod yarn;
|
||||
@@ -364,14 +364,10 @@ pub struct Completion {
|
||||
pub new_text: String,
|
||||
/// A label for this completion that is shown in the menu.
|
||||
pub label: CodeLabel,
|
||||
/// The id of the language server that produced this completion.
|
||||
pub server_id: LanguageServerId,
|
||||
/// The documentation for this completion.
|
||||
pub documentation: Option<CompletionDocumentation>,
|
||||
/// The raw completion provided by the language server.
|
||||
pub lsp_completion: lsp::CompletionItem,
|
||||
/// Whether this completion has been resolved, to ensure it happens once per completion.
|
||||
pub resolved: bool,
|
||||
/// Completion data source which it was constructed from.
|
||||
pub source: CompletionSource,
|
||||
/// An optional callback to invoke when this completion is confirmed.
|
||||
/// Returns, whether new completions should be retriggered after the current one.
|
||||
/// If `true` is returned, the editor will show a new completion menu after this completion is confirmed.
|
||||
@@ -379,15 +375,53 @@ pub struct Completion {
|
||||
pub confirm: Option<Arc<dyn Send + Sync + Fn(CompletionIntent, &mut Window, &mut App) -> bool>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum CompletionSource {
|
||||
Lsp {
|
||||
/// The id of the language server that produced this completion.
|
||||
server_id: LanguageServerId,
|
||||
/// The raw completion provided by the language server.
|
||||
lsp_completion: Box<lsp::CompletionItem>,
|
||||
/// Whether this completion has been resolved, to ensure it happens once per completion.
|
||||
resolved: bool,
|
||||
},
|
||||
Custom,
|
||||
}
|
||||
|
||||
impl CompletionSource {
|
||||
pub fn server_id(&self) -> Option<LanguageServerId> {
|
||||
if let CompletionSource::Lsp { server_id, .. } = self {
|
||||
Some(*server_id)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn lsp_completion(&self) -> Option<&lsp::CompletionItem> {
|
||||
if let Self::Lsp { lsp_completion, .. } = self {
|
||||
Some(lsp_completion)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn lsp_completion_mut(&mut self) -> Option<&mut lsp::CompletionItem> {
|
||||
if let Self::Lsp { lsp_completion, .. } = self {
|
||||
Some(lsp_completion)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Completion {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("Completion")
|
||||
.field("old_range", &self.old_range)
|
||||
.field("new_text", &self.new_text)
|
||||
.field("label", &self.label)
|
||||
.field("server_id", &self.server_id)
|
||||
.field("documentation", &self.documentation)
|
||||
.field("lsp_completion", &self.lsp_completion)
|
||||
.field("source", &self.source)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
@@ -397,9 +431,7 @@ impl std::fmt::Debug for Completion {
|
||||
pub(crate) struct CoreCompletion {
|
||||
old_range: Range<Anchor>,
|
||||
new_text: String,
|
||||
server_id: LanguageServerId,
|
||||
lsp_completion: lsp::CompletionItem,
|
||||
resolved: bool,
|
||||
source: CompletionSource,
|
||||
}
|
||||
|
||||
/// A code action provided by a language server.
|
||||
@@ -411,12 +443,12 @@ pub struct CodeAction {
|
||||
pub range: Range<Anchor>,
|
||||
/// The raw code action provided by the language server.
|
||||
/// Can be either an action or a command.
|
||||
pub lsp_action: ActionVariant,
|
||||
pub lsp_action: LspAction,
|
||||
}
|
||||
|
||||
/// An action sent back by a language server.
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum ActionVariant {
|
||||
pub enum LspAction {
|
||||
/// An action with the full data, may have a command or may not.
|
||||
/// May require resolving.
|
||||
Action(Box<lsp::CodeAction>),
|
||||
@@ -424,7 +456,7 @@ pub enum ActionVariant {
|
||||
Command(lsp::Command),
|
||||
}
|
||||
|
||||
impl ActionVariant {
|
||||
impl LspAction {
|
||||
pub fn title(&self) -> &str {
|
||||
match self {
|
||||
Self::Action(action) => &action.title,
|
||||
@@ -886,7 +918,6 @@ impl Project {
|
||||
});
|
||||
|
||||
cx.subscribe(&ssh, Self::on_ssh_event).detach();
|
||||
cx.observe(&ssh, |_, _, cx| cx.notify()).detach();
|
||||
|
||||
let this = Self {
|
||||
buffer_ordered_messages_tx: tx,
|
||||
@@ -1371,9 +1402,9 @@ impl Project {
|
||||
self.environment.read(cx).environment_errors()
|
||||
}
|
||||
|
||||
pub fn remove_environment_error(&mut self, cx: &mut Context<Self>, worktree_id: WorktreeId) {
|
||||
self.environment.update(cx, |environment, _| {
|
||||
environment.remove_environment_error(worktree_id);
|
||||
pub fn remove_environment_error(&mut self, worktree_id: WorktreeId, cx: &mut Context<Self>) {
|
||||
self.environment.update(cx, |environment, cx| {
|
||||
environment.remove_environment_error(worktree_id, cx);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1764,7 +1795,6 @@ impl Project {
|
||||
};
|
||||
|
||||
cx.emit(Event::RemoteIdChanged(Some(project_id)));
|
||||
cx.notify();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1780,7 +1810,6 @@ impl Project {
|
||||
self.worktree_store.update(cx, |worktree_store, cx| {
|
||||
worktree_store.send_project_updates(cx);
|
||||
});
|
||||
cx.notify();
|
||||
cx.emit(Event::Reshared);
|
||||
Ok(())
|
||||
}
|
||||
@@ -1810,13 +1839,12 @@ impl Project {
|
||||
self.enqueue_buffer_ordered_message(BufferOrderedMessage::Resync)
|
||||
.unwrap();
|
||||
cx.emit(Event::Rejoined);
|
||||
cx.notify();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn unshare(&mut self, cx: &mut Context<Self>) -> Result<()> {
|
||||
self.unshare_internal(cx)?;
|
||||
cx.notify();
|
||||
cx.emit(Event::RemoteIdChanged(None));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1860,7 +1888,6 @@ impl Project {
|
||||
}
|
||||
self.disconnected_from_host_internal(cx);
|
||||
cx.emit(Event::DisconnectedFromHost);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn set_role(&mut self, role: proto::ChannelRole, cx: &mut Context<Self>) {
|
||||
@@ -2509,15 +2536,11 @@ impl Project {
|
||||
}
|
||||
}
|
||||
|
||||
fn on_worktree_added(&mut self, worktree: &Entity<Worktree>, cx: &mut Context<Self>) {
|
||||
{
|
||||
let mut remotely_created_models = self.remotely_created_models.lock();
|
||||
if remotely_created_models.retain_count > 0 {
|
||||
remotely_created_models.worktrees.push(worktree.clone())
|
||||
}
|
||||
fn on_worktree_added(&mut self, worktree: &Entity<Worktree>, _: &mut Context<Self>) {
|
||||
let mut remotely_created_models = self.remotely_created_models.lock();
|
||||
if remotely_created_models.retain_count > 0 {
|
||||
remotely_created_models.worktrees.push(worktree.clone())
|
||||
}
|
||||
cx.observe(worktree, |_, _, cx| cx.notify()).detach();
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn on_worktree_released(&mut self, id_to_remove: WorktreeId, cx: &mut Context<Self>) {
|
||||
@@ -2529,8 +2552,6 @@ impl Project {
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn on_buffer_event(
|
||||
@@ -3804,7 +3825,6 @@ impl Project {
|
||||
cx.emit(Event::CollaboratorJoined(collaborator.peer_id));
|
||||
this.collaborators
|
||||
.insert(collaborator.peer_id, collaborator);
|
||||
cx.notify();
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
@@ -3848,7 +3868,6 @@ impl Project {
|
||||
old_peer_id,
|
||||
new_peer_id,
|
||||
});
|
||||
cx.notify();
|
||||
Ok(())
|
||||
})?
|
||||
}
|
||||
@@ -3876,7 +3895,6 @@ impl Project {
|
||||
});
|
||||
|
||||
cx.emit(Event::CollaboratorLeft(peer_id));
|
||||
cx.notify();
|
||||
Ok(())
|
||||
})?
|
||||
}
|
||||
@@ -4292,7 +4310,6 @@ impl Project {
|
||||
worktrees: Vec<proto::WorktreeMetadata>,
|
||||
cx: &mut Context<Project>,
|
||||
) -> Result<()> {
|
||||
cx.notify();
|
||||
self.worktree_store.update(cx, |worktree_store, cx| {
|
||||
worktree_store.set_worktrees_from_proto(worktrees, self.replica_id(), cx)
|
||||
})
|
||||
@@ -4620,27 +4637,38 @@ impl Completion {
|
||||
/// A key that can be used to sort completions when displaying
|
||||
/// them to the user.
|
||||
pub fn sort_key(&self) -> (usize, &str) {
|
||||
let kind_key = match self.lsp_completion.kind {
|
||||
Some(lsp::CompletionItemKind::KEYWORD) => 0,
|
||||
Some(lsp::CompletionItemKind::VARIABLE) => 1,
|
||||
_ => 2,
|
||||
};
|
||||
const DEFAULT_KIND_KEY: usize = 2;
|
||||
let kind_key = self
|
||||
.source
|
||||
.lsp_completion()
|
||||
.and_then(|lsp_completion| lsp_completion.kind)
|
||||
.and_then(|lsp_completion_kind| match lsp_completion_kind {
|
||||
lsp::CompletionItemKind::KEYWORD => Some(0),
|
||||
lsp::CompletionItemKind::VARIABLE => Some(1),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap_or(DEFAULT_KIND_KEY);
|
||||
(kind_key, &self.label.text[self.label.filter_range.clone()])
|
||||
}
|
||||
|
||||
/// Whether this completion is a snippet.
|
||||
pub fn is_snippet(&self) -> bool {
|
||||
self.lsp_completion.insert_text_format == Some(lsp::InsertTextFormat::SNIPPET)
|
||||
self.source
|
||||
.lsp_completion()
|
||||
.map_or(false, |lsp_completion| {
|
||||
lsp_completion.insert_text_format == Some(lsp::InsertTextFormat::SNIPPET)
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns the corresponding color for this completion.
|
||||
///
|
||||
/// Will return `None` if this completion's kind is not [`CompletionItemKind::COLOR`].
|
||||
pub fn color(&self) -> Option<Hsla> {
|
||||
match self.lsp_completion.kind {
|
||||
Some(CompletionItemKind::COLOR) => color_extractor::extract_color(&self.lsp_completion),
|
||||
_ => None,
|
||||
let lsp_completion = self.source.lsp_completion()?;
|
||||
if lsp_completion.kind? == CompletionItemKind::COLOR {
|
||||
return color_extractor::extract_color(lsp_completion);
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -212,10 +212,15 @@ pub enum GitHunkStyleSetting {
|
||||
Transparent,
|
||||
/// Show unstaged hunks with a pattern background
|
||||
Pattern,
|
||||
/// Show unstaged hunks with a border background
|
||||
Border,
|
||||
|
||||
/// Show staged hunks with a pattern background
|
||||
StagedPattern,
|
||||
/// Show staged hunks with a pattern background
|
||||
StagedTransparent,
|
||||
/// Show staged hunks with a pattern background
|
||||
StagedBorder,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
|
||||
syntax = "proto3";
|
||||
package zed.messages;
|
||||
import "google/protobuf/wrappers.proto";
|
||||
|
||||
// Looking for a number? Search "// current max"
|
||||
|
||||
@@ -999,6 +999,12 @@ message Completion {
|
||||
uint64 server_id = 4;
|
||||
bytes lsp_completion = 5;
|
||||
bool resolved = 6;
|
||||
Source source = 7;
|
||||
|
||||
enum Source {
|
||||
Custom = 0;
|
||||
Lsp = 1;
|
||||
}
|
||||
}
|
||||
|
||||
message GetCodeActions {
|
||||
|
||||
@@ -27,11 +27,9 @@ serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
settings.workspace = true
|
||||
util.workspace = true
|
||||
workspace.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
collections = { workspace = true, features = ["test-support"] }
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
project = { workspace = true, features = ["test-support"] }
|
||||
settings = { workspace = true, features = ["test-support"] }
|
||||
workspace = { workspace = true, features = ["test-support"] }
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
mod session;
|
||||
|
||||
use project::Project;
|
||||
pub(crate) use session::*;
|
||||
|
||||
use assistant_tool::{Tool, ToolRegistry};
|
||||
use gpui::{App, AppContext as _, Task, WeakEntity, Window};
|
||||
use gpui::{App, AppContext as _, Entity, Task};
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use std::sync::Arc;
|
||||
use workspace::Workspace;
|
||||
|
||||
pub fn init(cx: &App) {
|
||||
let registry = ToolRegistry::global(cx);
|
||||
@@ -27,20 +27,7 @@ impl Tool for ScriptingTool {
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
r#"You can write a Lua script and I'll run it on my code base and tell you what its output was,
|
||||
including both stdout as well as the git diff of changes it made to the filesystem. That way,
|
||||
you can get more information about the code base, or make changes to the code base directly.
|
||||
The lua script will have access to `io` and it will run with the current working directory being in
|
||||
the root of the code base, so you can use it to explore, search, make changes, etc. You can also have
|
||||
the script print things, and I'll tell you what the output was. Note that `io` only has `open`, and
|
||||
then the file it returns only has the methods read, write, and close - it doesn't have popen or
|
||||
anything else. Also, I'm going to be putting this Lua script into JSON, so please don't use Lua's
|
||||
double quote syntax for string literals - use one of Lua's other syntaxes for string literals, so I
|
||||
don't have to escape the double quotes. There will be a global called `search` which accepts a regex
|
||||
(it's implemented using Rust's regex crate, so use that regex syntax) and runs that regex on the contents
|
||||
of every file in the code base (aside from gitignored files), then returns an array of tables with two
|
||||
fields: "path" (the path to the file that had the matches) and "matches" (an array of strings, with each
|
||||
string being a match that was found within the file)."#.into()
|
||||
include_str!("scripting_tool_description.txt").into()
|
||||
}
|
||||
|
||||
fn input_schema(&self) -> serde_json::Value {
|
||||
@@ -51,18 +38,13 @@ string being a match that was found within the file)."#.into()
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
input: serde_json::Value,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
_window: &mut Window,
|
||||
project: Entity<Project>,
|
||||
cx: &mut App,
|
||||
) -> Task<anyhow::Result<String>> {
|
||||
let input = match serde_json::from_value::<ScriptingToolInput>(input) {
|
||||
Err(err) => return Task::ready(Err(err.into())),
|
||||
Ok(input) => input,
|
||||
};
|
||||
let Ok(project) = workspace.read_with(cx, |workspace, _cx| workspace.project().clone())
|
||||
else {
|
||||
return Task::ready(Err(anyhow::anyhow!("No project found")));
|
||||
};
|
||||
|
||||
let session = cx.new(|cx| Session::new(project, cx));
|
||||
let lua_script = input.lua_script;
|
||||
|
||||
22
crates/scripting_tool/src/scripting_tool_description.txt
Normal file
22
crates/scripting_tool/src/scripting_tool_description.txt
Normal file
@@ -0,0 +1,22 @@
|
||||
You can write a Lua script and I'll run it on my codebase and tell you what its
|
||||
output was, including both stdout as well as the git diff of changes it made to
|
||||
the filesystem. That way, you can get more information about the code base, or
|
||||
make changes to the code base directly.
|
||||
|
||||
The Lua script will have access to `io` and it will run with the current working
|
||||
directory being in the root of the code base, so you can use it to explore,
|
||||
search, make changes, etc. You can also have the script print things, and I'll
|
||||
tell you what the output was. Note that `io` only has `open`, and then the file
|
||||
it returns only has the methods read, write, and close - it doesn't have popen
|
||||
or anything else.
|
||||
|
||||
Also, I'm going to be putting this Lua script into JSON, so please don't use
|
||||
Lua's double quote syntax for string literals - use one of Lua's other syntaxes
|
||||
for string literals, so I don't have to escape the double quotes.
|
||||
|
||||
There will be a global called `search` which accepts a regex (it's implemented
|
||||
using Rust's regex crate, so use that regex syntax) and runs that regex on the
|
||||
contents of every file in the code base (aside from gitignored files), then
|
||||
returns an array of tables with two fields: "path" (the path to the file that
|
||||
had the matches) and "matches" (an array of strings, with each string being a
|
||||
match that was found within the file).
|
||||
@@ -11,6 +11,7 @@ use project::{search::SearchQuery, Fs, Project};
|
||||
use regex::Regex;
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
fs::File,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
@@ -495,6 +496,16 @@ impl Session {
|
||||
let new_position = position + bytes.len();
|
||||
file_userdata.set("__position", new_position)?;
|
||||
|
||||
match std::fs::write(path.clone(), &*content_vec) {
|
||||
Ok(_) => (),
|
||||
Err(e) => {
|
||||
return Err(mlua::Error::runtime(format!(
|
||||
"Failed to write to file on the actual filesytem: {}",
|
||||
e
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
// Update fs_changes
|
||||
let path = file_userdata.get::<String>("__path")?;
|
||||
let path_buf = PathBuf::from(path);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::{settings_store::SettingsStore, Settings};
|
||||
use fs::Fs;
|
||||
use collections::HashSet;
|
||||
use fs::{Fs, PathEventKind};
|
||||
use futures::{channel::mpsc, StreamExt};
|
||||
use gpui::{App, BackgroundExecutor, ReadGlobal};
|
||||
use std::{path::PathBuf, sync::Arc, time::Duration};
|
||||
@@ -78,6 +79,55 @@ pub fn watch_config_file(
|
||||
rx
|
||||
}
|
||||
|
||||
pub fn watch_config_dir(
|
||||
executor: &BackgroundExecutor,
|
||||
fs: Arc<dyn Fs>,
|
||||
dir_path: PathBuf,
|
||||
config_paths: HashSet<PathBuf>,
|
||||
) -> mpsc::UnboundedReceiver<String> {
|
||||
let (tx, rx) = mpsc::unbounded();
|
||||
executor
|
||||
.spawn(async move {
|
||||
for file_path in &config_paths {
|
||||
if fs.metadata(file_path).await.is_ok_and(|v| v.is_some()) {
|
||||
if let Ok(contents) = fs.load(file_path).await {
|
||||
if tx.unbounded_send(contents).is_err() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let (events, _) = fs.watch(&dir_path, Duration::from_millis(100)).await;
|
||||
futures::pin_mut!(events);
|
||||
|
||||
while let Some(event_batch) = events.next().await {
|
||||
for event in event_batch {
|
||||
if config_paths.contains(&event.path) {
|
||||
match event.kind {
|
||||
Some(PathEventKind::Removed) => {
|
||||
if tx.unbounded_send(String::new()).is_err() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
Some(PathEventKind::Created) | Some(PathEventKind::Changed) => {
|
||||
if let Ok(contents) = fs.load(&event.path).await {
|
||||
if tx.unbounded_send(contents).is_err() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
rx
|
||||
}
|
||||
|
||||
pub fn update_settings_file<T: Settings>(
|
||||
fs: Arc<dyn Fs>,
|
||||
cx: &App,
|
||||
|
||||
@@ -311,7 +311,7 @@ const FILE_ICONS: &[(&str, &str)] = &[
|
||||
("lock", "icons/file_icons/lock.svg"),
|
||||
("log", "icons/file_icons/info.svg"),
|
||||
("lua", "icons/file_icons/lua.svg"),
|
||||
("luau", "icons/file_icons/file.svg"),
|
||||
("luau", "icons/file_icons/luau.svg"),
|
||||
("markdown", "icons/file_icons/book.svg"),
|
||||
("metal", "icons/file_icons/metal.svg"),
|
||||
("nim", "icons/file_icons/nim.svg"),
|
||||
|
||||
@@ -307,7 +307,7 @@ impl TitleBar {
|
||||
cx.notify()
|
||||
}),
|
||||
);
|
||||
subscriptions.push(cx.observe(&project, |_, _, cx| cx.notify()));
|
||||
subscriptions.push(cx.subscribe(&project, |_, _, _, cx| cx.notify()));
|
||||
subscriptions.push(cx.observe(&active_call, |this, _, cx| this.active_call_changed(cx)));
|
||||
subscriptions.push(cx.observe_window_activation(window, Self::window_activation_changed));
|
||||
subscriptions.push(cx.observe(&user_store, |_, _, cx| cx.notify()));
|
||||
|
||||
@@ -15,7 +15,6 @@ mod keybinding;
|
||||
mod keybinding_hint;
|
||||
mod label;
|
||||
mod list;
|
||||
pub mod list_2;
|
||||
mod modal;
|
||||
mod navigable;
|
||||
mod notification;
|
||||
|
||||
@@ -1,284 +0,0 @@
|
||||
use crate::prelude::*;
|
||||
use gpui::{uniform_list, Entity, Hsla};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
enum List2ItemHeight {
|
||||
Default = 27,
|
||||
}
|
||||
|
||||
impl List2ItemHeight {
|
||||
pub fn f32(&self) -> f32 {
|
||||
match self {
|
||||
List2ItemHeight::Default => 27.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub enum List2Item {
|
||||
InsetItem(List2InsetItem),
|
||||
SectionTitle(List2SectionTitle),
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct List2SectionTitle {
|
||||
label: SharedString,
|
||||
}
|
||||
|
||||
impl List2SectionTitle {
|
||||
pub fn new(label: impl Into<SharedString>) -> Self {
|
||||
Self {
|
||||
label: label.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct List2InsetItem {
|
||||
id: ElementId,
|
||||
icon: Option<IconName>,
|
||||
label: SharedString,
|
||||
selected: bool,
|
||||
}
|
||||
|
||||
impl List2InsetItem {
|
||||
pub fn new(id: impl Into<ElementId>, label: impl Into<SharedString>) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
icon: None,
|
||||
label: label.into(),
|
||||
selected: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn icon(mut self, icon: IconName) -> Self {
|
||||
self.icon = Some(icon);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn selected(mut self, selected: bool) -> Self {
|
||||
self.selected = selected;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub struct List2Builder {
|
||||
id: SharedString,
|
||||
items: Vec<List2Item>,
|
||||
internal_last_item_ix: usize,
|
||||
}
|
||||
|
||||
impl List2Builder {
|
||||
pub fn new(
|
||||
id: impl Into<SharedString>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
f: impl FnOnce(Self, &mut Window, &mut Context<Self>) -> Self,
|
||||
) -> Entity<Self> {
|
||||
cx.new(|cx| {
|
||||
window.refresh();
|
||||
f(
|
||||
Self {
|
||||
id: id.into(),
|
||||
items: Vec::new(),
|
||||
internal_last_item_ix: 0,
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// Request a unique [`gpui::ElementId`] for a new item.
|
||||
pub fn next_id(&mut self) -> ElementId {
|
||||
self.internal_last_item_ix += 1;
|
||||
let id = format!("list2_{}_{}", self.id, self.internal_last_item_ix).into();
|
||||
ElementId::Name(id)
|
||||
}
|
||||
|
||||
pub fn section_title(mut self, title: impl Into<SharedString>) -> Self {
|
||||
self.items
|
||||
.push(List2Item::SectionTitle(List2SectionTitle::new(title)));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn inset_item(mut self, item: List2InsetItem) -> Self {
|
||||
self.items.push(List2Item::InsetItem(item));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn render_item(&self, ix: usize, cx: &Context<Self>) -> AnyElement {
|
||||
let item = &self.items[ix];
|
||||
match item {
|
||||
List2Item::InsetItem(item) => {
|
||||
self.render_inset_item(item.clone(), cx).into_any_element()
|
||||
}
|
||||
List2Item::SectionTitle(item) => self
|
||||
.render_section_title(item.clone(), cx)
|
||||
.into_any_element(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render_section_title(
|
||||
&self,
|
||||
el: List2SectionTitle,
|
||||
cx: &Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
let height = px(List2ItemHeight::Default.f32());
|
||||
|
||||
h_flex()
|
||||
.h(height)
|
||||
.w_full()
|
||||
.px_3()
|
||||
.pt(px(7.))
|
||||
.pb(px(5.))
|
||||
.overflow_hidden()
|
||||
.flex_none()
|
||||
.child(div().text_ui(cx).child(el.label))
|
||||
}
|
||||
|
||||
pub fn render_inset_item(&self, el: List2InsetItem, cx: &Context<Self>) -> impl IntoElement {
|
||||
let height = px(List2ItemHeight::Default.f32());
|
||||
let color = List2::state_color(cx);
|
||||
|
||||
h_flex()
|
||||
.id(el.id)
|
||||
.h(height)
|
||||
.w_full()
|
||||
.items_center()
|
||||
.px_1p5()
|
||||
.overflow_hidden()
|
||||
.flex_none()
|
||||
.when(el.selected, |el| el.bg(color.selected_bg))
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.p_1p5()
|
||||
.rounded(px(4.))
|
||||
.when_some(el.icon, |el, icon| el.child(Icon::new(icon)))
|
||||
.child(div().text_ui(cx).child(el.label)),
|
||||
)
|
||||
}
|
||||
|
||||
// pub fn icon(mut self, icon: ToastIcon) -> Self {
|
||||
// self.icon = Some(icon);
|
||||
// self
|
||||
// }
|
||||
|
||||
// pub fn action(
|
||||
// mut self,
|
||||
// label: impl Into<SharedString>,
|
||||
// f: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
|
||||
// ) -> Self {
|
||||
// self.action = Some(ToastAction::new(label.into(), Some(Arc::new(f))));
|
||||
// self
|
||||
// }
|
||||
}
|
||||
|
||||
pub struct List2StateColor {
|
||||
selected_bg: Hsla,
|
||||
marked_bg: Hsla,
|
||||
active_bg: Hsla,
|
||||
hover_bg: Hsla,
|
||||
}
|
||||
|
||||
#[derive(IntoComponent)]
|
||||
#[component(scope = "Layout", description = "A list component")]
|
||||
pub struct List2 {}
|
||||
|
||||
impl List2 {
|
||||
fn state_color(cx: &App) -> List2StateColor {
|
||||
let selected_bg_alpha = 0.08;
|
||||
let marked_bg_alpha = 0.12;
|
||||
let state_opacity_step = 0.04;
|
||||
|
||||
let bg_color = |selected: bool, marked: bool| -> Hsla {
|
||||
match (selected, marked) {
|
||||
(true, true) => cx
|
||||
.theme()
|
||||
.status()
|
||||
.info
|
||||
.alpha(selected_bg_alpha + marked_bg_alpha),
|
||||
(true, false) => cx.theme().status().info.alpha(selected_bg_alpha),
|
||||
(false, true) => cx.theme().status().info.alpha(marked_bg_alpha),
|
||||
_ => cx.theme().colors().ghost_element_background,
|
||||
}
|
||||
};
|
||||
|
||||
let hover_bg = if true {
|
||||
cx.theme()
|
||||
.status()
|
||||
.info
|
||||
.alpha(selected_bg_alpha + state_opacity_step)
|
||||
} else {
|
||||
cx.theme().colors().ghost_element_hover
|
||||
};
|
||||
|
||||
let active_bg = if true {
|
||||
cx.theme()
|
||||
.status()
|
||||
.info
|
||||
.alpha(selected_bg_alpha + state_opacity_step * 2.0)
|
||||
} else {
|
||||
cx.theme().colors().ghost_element_active
|
||||
};
|
||||
|
||||
List2StateColor {
|
||||
selected_bg: bg_color(true, false),
|
||||
marked_bg: bg_color(false, true),
|
||||
active_bg,
|
||||
hover_bg,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl List2 {}
|
||||
|
||||
impl Render for List2Builder {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let list_id = ElementId::Name(format!("list_{}", self.id).into());
|
||||
let len = self.items.len();
|
||||
|
||||
uniform_list(
|
||||
cx.entity().clone(),
|
||||
list_id,
|
||||
len,
|
||||
move |this, range, _window, cx| range.map(|ix| this.render_item(ix, cx)).collect(),
|
||||
)
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.bg(cx.theme().colors().panel_background)
|
||||
.size_full()
|
||||
.flex_none()
|
||||
}
|
||||
}
|
||||
|
||||
impl ComponentPreview for List2 {
|
||||
fn preview(window: &mut Window, cx: &mut App) -> AnyElement {
|
||||
let basic_list = List2Builder::new("basic-list", window, cx, |this, _, _| {
|
||||
this.section_title("Favorites")
|
||||
.inset_item(
|
||||
List2InsetItem::new("1", "Recents")
|
||||
.icon(IconName::HistoryRerun.into())
|
||||
.selected(true),
|
||||
)
|
||||
.inset_item(List2InsetItem::new("2", "Desktop").icon(IconName::Folder.into()))
|
||||
.inset_item(List2InsetItem::new("3", "Folders").icon(IconName::Folder.into()))
|
||||
});
|
||||
|
||||
v_flex()
|
||||
.gap_6()
|
||||
.p_4()
|
||||
.children(vec![example_group_with_title(
|
||||
"Basic List",
|
||||
vec![single_example(
|
||||
"Basic List",
|
||||
div()
|
||||
.w_80()
|
||||
.h(px(640.))
|
||||
.child(basic_list)
|
||||
.into_any_element(),
|
||||
)],
|
||||
)])
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
@@ -852,6 +852,7 @@ impl<T: Item> ItemHandle for Entity<T> {
|
||||
.detach();
|
||||
|
||||
let item_id = self.item_id();
|
||||
workspace.update_item_dirty_state(self, window, cx);
|
||||
cx.observe_release_in(self, window, move |workspace, _, _, _| {
|
||||
workspace.panes_by_item.remove(&item_id);
|
||||
event_subscription.take();
|
||||
|
||||
@@ -2424,14 +2424,10 @@ impl Pane {
|
||||
.child(label),
|
||||
);
|
||||
|
||||
let single_entry_to_resolve = {
|
||||
let item_entries = self.items[ix].project_entry_ids(cx);
|
||||
if item_entries.len() == 1 {
|
||||
Some(item_entries[0])
|
||||
} else {
|
||||
None
|
||||
}
|
||||
};
|
||||
let single_entry_to_resolve = self.items[ix]
|
||||
.is_singleton(cx)
|
||||
.then(|| self.items[ix].project_entry_ids(cx).get(0).copied())
|
||||
.flatten();
|
||||
|
||||
let total_items = self.items.len();
|
||||
let has_items_to_left = ix > 0;
|
||||
|
||||
@@ -879,8 +879,6 @@ impl Workspace {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
cx.observe_in(&project, window, |_, _, _, cx| cx.notify())
|
||||
.detach();
|
||||
cx.subscribe_in(&project, window, move |this, _, event, window, cx| {
|
||||
match event {
|
||||
project::Event::RemoteIdChanged(_) => {
|
||||
|
||||
@@ -1570,7 +1570,6 @@ impl LocalWorktree {
|
||||
this.update_abs_path_and_refresh(new_path, cx);
|
||||
}
|
||||
}
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
@@ -3425,6 +3424,7 @@ impl BackgroundScannerState {
|
||||
}
|
||||
|
||||
fn remove_path(&mut self, path: &Path) {
|
||||
log::info!("background scanner removing path {path:?}");
|
||||
let mut new_entries;
|
||||
let removed_entries;
|
||||
{
|
||||
@@ -3480,7 +3480,14 @@ impl BackgroundScannerState {
|
||||
.git_repositories
|
||||
.retain(|id, _| removed_ids.binary_search(id).is_err());
|
||||
self.snapshot.repositories.retain(&(), |repository| {
|
||||
!repository.work_directory.path_key().0.starts_with(path)
|
||||
let retain = !repository.work_directory.path_key().0.starts_with(path);
|
||||
if !retain {
|
||||
log::info!(
|
||||
"dropping repository entry for {:?}",
|
||||
repository.work_directory
|
||||
);
|
||||
}
|
||||
retain
|
||||
});
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -3535,12 +3542,14 @@ impl BackgroundScannerState {
|
||||
fs: &dyn Fs,
|
||||
watcher: &dyn Watcher,
|
||||
) -> Option<LocalRepositoryEntry> {
|
||||
log::info!("insert git reposiutory for {dot_git_path:?}");
|
||||
let work_dir_id = self
|
||||
.snapshot
|
||||
.entry_for_path(work_directory.path_key().0)
|
||||
.map(|entry| entry.id)?;
|
||||
|
||||
if self.snapshot.git_repositories.get(&work_dir_id).is_some() {
|
||||
log::info!("existing git repository for {work_directory:?}");
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -3548,6 +3557,7 @@ impl BackgroundScannerState {
|
||||
|
||||
let t0 = Instant::now();
|
||||
let repository = fs.open_repo(&dot_git_abs_path)?;
|
||||
log::info!("opened git repo for {dot_git_abs_path:?}");
|
||||
|
||||
let repository_path = repository.path();
|
||||
watcher.add(&repository_path).log_err()?;
|
||||
@@ -3606,6 +3616,7 @@ impl BackgroundScannerState {
|
||||
.git_repositories
|
||||
.insert(work_dir_id, local_repository.clone());
|
||||
|
||||
log::info!("inserting new local git repository");
|
||||
Some(local_repository)
|
||||
}
|
||||
}
|
||||
@@ -3949,10 +3960,6 @@ pub struct StatusEntry {
|
||||
}
|
||||
|
||||
impl StatusEntry {
|
||||
pub fn is_staged(&self) -> Option<bool> {
|
||||
self.status.is_staged()
|
||||
}
|
||||
|
||||
fn to_proto(&self) -> proto::StatusEntry {
|
||||
let simple_status = match self.status {
|
||||
FileStatus::Ignored | FileStatus::Untracked => proto::GitStatus::Added as i32,
|
||||
@@ -4353,7 +4360,7 @@ impl BackgroundScanner {
|
||||
}
|
||||
|
||||
let ancestor_dot_git = ancestor.join(*DOT_GIT);
|
||||
log::debug!("considering ancestor: {ancestor_dot_git:?}");
|
||||
log::info!("considering ancestor: {ancestor_dot_git:?}");
|
||||
// Check whether the directory or file called `.git` exists (in the
|
||||
// case of worktrees it's a file.)
|
||||
if self
|
||||
@@ -4362,7 +4369,6 @@ impl BackgroundScanner {
|
||||
.await
|
||||
.is_ok_and(|metadata| metadata.is_some())
|
||||
{
|
||||
log::debug!(".git path exists");
|
||||
if index != 0 {
|
||||
// We canonicalize, since the FS events use the canonicalized path.
|
||||
if let Some(ancestor_dot_git) =
|
||||
@@ -4373,7 +4379,7 @@ impl BackgroundScanner {
|
||||
.strip_prefix(ancestor)
|
||||
.unwrap()
|
||||
.into();
|
||||
log::debug!(
|
||||
log::info!(
|
||||
"inserting parent git repo for this worktree: {location_in_repo:?}"
|
||||
);
|
||||
// We associate the external git repo with our root folder and
|
||||
@@ -4396,12 +4402,10 @@ impl BackgroundScanner {
|
||||
|
||||
// Reached root of git repository.
|
||||
break;
|
||||
} else {
|
||||
log::debug!(".git path doesn't exist");
|
||||
}
|
||||
}
|
||||
|
||||
log::debug!("containing git repository: {containing_git_repository:?}");
|
||||
log::info!("containing git repository: {containing_git_repository:?}");
|
||||
|
||||
let (scan_job_tx, scan_job_rx) = channel::unbounded();
|
||||
{
|
||||
@@ -4826,7 +4830,7 @@ impl BackgroundScanner {
|
||||
log::error!("skipping excluded directory {:?}", job.path);
|
||||
return Ok(());
|
||||
}
|
||||
log::debug!("scanning directory {:?}", job.path);
|
||||
log::info!("scanning directory {:?}", job.path);
|
||||
root_abs_path = snapshot.abs_path().clone();
|
||||
root_char_bag = snapshot.root_char_bag;
|
||||
}
|
||||
@@ -5408,7 +5412,7 @@ impl BackgroundScanner {
|
||||
}
|
||||
|
||||
fn update_git_repositories(&self, dot_git_paths: Vec<PathBuf>) -> Task<()> {
|
||||
log::debug!("reloading repositories: {dot_git_paths:?}");
|
||||
log::info!("reloading repositories: {dot_git_paths:?}");
|
||||
|
||||
let mut status_updates = Vec::new();
|
||||
{
|
||||
@@ -5889,14 +5893,21 @@ impl WorktreeModelHandle for Entity<Worktree> {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
cx.condition(&tree, |tree, _| tree.entry_for_path(file_name).is_some())
|
||||
.await;
|
||||
let mut events = cx.events(&tree);
|
||||
while events.next().await.is_some() {
|
||||
if tree.update(cx, |tree, _| tree.entry_for_path(file_name).is_some()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
fs.remove_file(&root_path.join(file_name), Default::default())
|
||||
.await
|
||||
.unwrap();
|
||||
cx.condition(&tree, |tree, _| tree.entry_for_path(file_name).is_none())
|
||||
.await;
|
||||
while events.next().await.is_some() {
|
||||
if tree.update(cx, |tree, _| tree.entry_for_path(file_name).is_none()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
cx.update(|cx| tree.read(cx).as_local().unwrap().scan_complete())
|
||||
.await;
|
||||
@@ -5950,19 +5961,22 @@ impl WorktreeModelHandle for Entity<Worktree> {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
cx.condition(&tree, |tree, _| {
|
||||
scan_id_increased(tree, &mut git_dir_scan_id)
|
||||
})
|
||||
.await;
|
||||
let mut events = cx.events(&tree);
|
||||
while events.next().await.is_some() {
|
||||
if tree.update(cx, |tree, _| scan_id_increased(tree, &mut git_dir_scan_id)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
fs.remove_file(&root_path.join(file_name), Default::default())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
cx.condition(&tree, |tree, _| {
|
||||
scan_id_increased(tree, &mut git_dir_scan_id)
|
||||
})
|
||||
.await;
|
||||
while events.next().await.is_some() {
|
||||
if tree.update(cx, |tree, _| scan_id_increased(tree, &mut git_dir_scan_id)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
cx.update(|cx| tree.read(cx).as_local().unwrap().scan_complete())
|
||||
.await;
|
||||
|
||||
@@ -12,6 +12,7 @@ use git::{
|
||||
},
|
||||
GITIGNORE,
|
||||
};
|
||||
use git2::RepositoryInitOptions;
|
||||
use gpui::{AppContext as _, BorrowAppContext, Context, Task, TestAppContext};
|
||||
use parking_lot::Mutex;
|
||||
use postage::stream::Stream;
|
||||
@@ -855,7 +856,7 @@ async fn test_write_file(cx: &mut TestAppContext) {
|
||||
"ignored-dir": {}
|
||||
}));
|
||||
|
||||
let tree = Worktree::local(
|
||||
let worktree = Worktree::local(
|
||||
dir.path(),
|
||||
true,
|
||||
Arc::new(RealFs::default()),
|
||||
@@ -868,32 +869,34 @@ async fn test_write_file(cx: &mut TestAppContext) {
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
fs::fs_watcher::global(|_| {}).unwrap();
|
||||
|
||||
cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
|
||||
cx.read(|cx| worktree.read(cx).as_local().unwrap().scan_complete())
|
||||
.await;
|
||||
tree.flush_fs_events(cx).await;
|
||||
worktree.flush_fs_events(cx).await;
|
||||
|
||||
tree.update(cx, |tree, cx| {
|
||||
tree.write_file(
|
||||
Path::new("tracked-dir/file.txt"),
|
||||
"hello".into(),
|
||||
Default::default(),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
tree.update(cx, |tree, cx| {
|
||||
tree.write_file(
|
||||
Path::new("ignored-dir/file.txt"),
|
||||
"world".into(),
|
||||
Default::default(),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
worktree
|
||||
.update(cx, |tree, cx| {
|
||||
tree.write_file(
|
||||
Path::new("tracked-dir/file.txt"),
|
||||
"hello".into(),
|
||||
Default::default(),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
worktree
|
||||
.update(cx, |tree, cx| {
|
||||
tree.write_file(
|
||||
Path::new("ignored-dir/file.txt"),
|
||||
"world".into(),
|
||||
Default::default(),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
tree.read_with(cx, |tree, _| {
|
||||
worktree.read_with(cx, |tree, _| {
|
||||
let tracked = tree.entry_for_path("tracked-dir/file.txt").unwrap();
|
||||
let ignored = tree.entry_for_path("ignored-dir/file.txt").unwrap();
|
||||
assert!(!tracked.is_ignored);
|
||||
@@ -3349,7 +3352,7 @@ async fn test_conflicted_cherry_pick(cx: &mut TestAppContext) {
|
||||
.expect("Failed to get HEAD")
|
||||
.peel_to_commit()
|
||||
.expect("HEAD is not a commit");
|
||||
git_checkout("refs/heads/master", &repo);
|
||||
git_checkout("refs/heads/main", &repo);
|
||||
std::fs::write(root_path.join("project/a.txt"), "b").unwrap();
|
||||
git_add("a.txt", &repo);
|
||||
git_commit("improve letter", &repo);
|
||||
@@ -3479,7 +3482,9 @@ const MODIFIED: GitSummary = GitSummary {
|
||||
|
||||
#[track_caller]
|
||||
fn git_init(path: &Path) -> git2::Repository {
|
||||
git2::Repository::init(path).expect("Failed to initialize git repository")
|
||||
let mut init_opts = RepositoryInitOptions::new();
|
||||
init_opts.initial_head("main");
|
||||
git2::Repository::init_opts(path, &init_opts).expect("Failed to initialize git repository")
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
|
||||
@@ -114,8 +114,9 @@ pub mod workspace {
|
||||
}
|
||||
|
||||
pub mod git {
|
||||
use gpui::action_with_deprecated_aliases;
|
||||
use gpui::{action_with_deprecated_aliases, actions};
|
||||
|
||||
actions!(git, [CheckoutBranch, Switch]);
|
||||
action_with_deprecated_aliases!(git, Branch, ["branches::OpenRecent"]);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ If you're used to a specific editor's defaults you can set a `base_keymap` in yo
|
||||
|
||||
- VSCode (default)
|
||||
- Atom
|
||||
- Emacs (Beta)
|
||||
- JetBrains
|
||||
- SublimeText
|
||||
- TextMate
|
||||
@@ -52,7 +53,7 @@ If you want to debug problems with custom keymaps you can use `debug: Open Key C
|
||||
|
||||
Zed has the ability to match against not just a single keypress, but a sequence of keys typed in order. Each key in the `"bindings"` map is a sequence of keypresses separated with a space.
|
||||
|
||||
Each key press is a sequence of modifiers followed by a key. The modifiers are:
|
||||
Each keypress is a sequence of modifiers followed by a key. The modifiers are:
|
||||
|
||||
- `ctrl-` The control key
|
||||
- `cmd-`, `win-` or `super-` for the platform modifier (Command on macOS, Windows key on Windows, and the Super key on Linux).
|
||||
@@ -77,7 +78,7 @@ The `shift-` modifier can only be used in combination with a letter to indicate
|
||||
|
||||
The `alt-` modifier can be used on many layouts to generate a different key. For example on macOS US keyboard the combination `alt-c` types `ç`. You can match against either in your keymap file, though by convention Zed spells this combination as `alt-c`.
|
||||
|
||||
It is possible to match against typing a modifier key on its own. For example `shift shift` can be used to implement JetBrains search everywhere shortcut. In this case the binding happens on key release instead of key press.
|
||||
It is possible to match against typing a modifier key on its own. For example `shift shift` can be used to implement JetBrains search everywhere shortcut. In this case the binding happens on key release instead of keypress.
|
||||
|
||||
### Contexts
|
||||
|
||||
@@ -138,13 +139,13 @@ As of Zed 0.162.0, Zed has some support for non-QWERTY keyboards on macOS. Bette
|
||||
|
||||
There are roughly three categories of keyboard to consider:
|
||||
|
||||
Keyboards that support full ASCII (QWERTY, DVORAK, COLEMAK, etc.). On these keyboards bindings are resolved based on the character that would be generated by the key. So to type `cmd-[`, find the key labelled `[` and press it with command.
|
||||
Keyboards that support full ASCII (QWERTY, DVORAK, COLEMAK, etc.). On these keyboards bindings are resolved based on the character that would be generated by the key. So to type `cmd-[`, find the key labeled `[` and press it with command.
|
||||
|
||||
Keyboards that are mostly non-ASCII, but support full ASCII when the command key is pressed. For example Cyrillic keyboards, Armenian, Hebrew, etc. On these keyboards bindings are resolved based on the character that would be generated by typing the key with command pressed. So to type `ctrl-a`, find the key that generates `cmd-a`. For these keyboards, keyboard shortcuts are displayed in the app using their ASCII equivalents. If the ASCII-equivalents are not printed on your keyboard, you can use the macOS keyboard viewer and holding down the `cmd` key to find things (though often the ASCII equivalents are in a QWERTY layout).
|
||||
|
||||
Finally keyboards that support extended Latin alphabets (usually ISO keyboards) require the most support. For example French AZERTY, German QWERTZ, etc. On these keyboards it is often not possible to type the entire ASCII range without option. To ensure that shortcuts _can_ be typed without option, keyboard shortcuts are mapped to "key equivalents" in the same way as [macOS](). This mapping is defined per layout, and is a compromise between leaving keyboard shortcuts triggered by the same character they are defined with, keeping shortcuts in the same place as a QWERTY layout, and moving shortcuts out of the way of system shortcuts.
|
||||
|
||||
For example on a German QWERTZ keyboard, the `cmd->` shortcut is moved to `cmd-:` because `cmd->` is the system window switcher and this is where that shortcut is typed on a QWERTY keyboard. `cmd-+` stays the same because + is still typable without option, and as a result, `cmd-[` and `cmd-]` become `cmd-ö` and `cmd-ä`, moving out of the way of the `+` key.
|
||||
For example on a German QWERTZ keyboard, the `cmd->` shortcut is moved to `cmd-:` because `cmd->` is the system window switcher and this is where that shortcut is typed on a QWERTY keyboard. `cmd-+` stays the same because + is still typeable without option, and as a result, `cmd-[` and `cmd-]` become `cmd-ö` and `cmd-ä`, moving out of the way of the `+` key.
|
||||
|
||||
If you are defining shortcuts in your personal keymap, you can opt into the key equivalent mapping by setting `use_key_equivalents` to `true` in your keymap:
|
||||
|
||||
@@ -208,7 +209,7 @@ There are some limitations to this, notably:
|
||||
|
||||
The argument to `SendKeystrokes` is a space-separated list of keystrokes (using the same syntax as above). Due to the way that keystrokes are parsed, any segment that is not recognized as a keypress will be sent verbatim to the currently focused input field.
|
||||
|
||||
If the argument to `SendKeystrokes` contains the binding used to trigger it, it will use the next-highest-precedence definition of that binding. This allows you to extend the default behaviour of a key binding.
|
||||
If the argument to `SendKeystrokes` contains the binding used to trigger it, it will use the next-highest-precedence definition of that binding. This allows you to extend the default behavior of a key binding.
|
||||
|
||||
### Forward keys to terminal
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Haskell
|
||||
|
||||
Haskell support is available through the [Haskell extension](https://github.com/zed-industries/zed/tree/main/extensions/haskell).
|
||||
Haskell support is available through the [Haskell extension](https://github.com/zed-extensions/haskell).
|
||||
|
||||
- Tree-sitter: [tree-sitter-haskell](https://github.com/tree-sitter/tree-sitter-haskell)
|
||||
- Language Server: [haskell-language-server](https://github.com/haskell/haskell-language-server)
|
||||
|
||||
@@ -89,14 +89,12 @@ Linux works on a large variety of systems configured in many different ways. We
|
||||
|
||||
If you see an error like "/lib64/libc.so.6: version 'GLIBC_2.29' not found" it means that your distribution's version of glibc is too old. You can either upgrade your system, or [install Zed from source](./development/linux.md).
|
||||
|
||||
### Graphics issues
|
||||
|
||||
### Zed fails to open windows
|
||||
|
||||
### Zed is very slow
|
||||
|
||||
Zed requires a GPU to run effectively. Under the hood, we use [Vulkan](https://www.vulkan.org/) to communicate with your GPU. If you are seeing problems with performance, or Zed fails to load, it is possible that Vulkan is the culprit.
|
||||
|
||||
If you're using an AMD GPU, you might get a 'Broken Pipe' error. Try using the RADV or Mesa drivers. (See the following GitHub issue for more details: [#13880](https://github.com/zed-industries/zed/issues/13880)).
|
||||
|
||||
If you see a notification saying `Zed failed to open a window: NoSupportedDeviceFound` this means that Vulkan cannot find a compatible GPU. You can begin troubleshooting Vulkan by installing the `vulkan-tools` package and running:
|
||||
|
||||
```sh
|
||||
@@ -105,21 +103,23 @@ vkcube
|
||||
|
||||
This should output a line describing your current graphics setup and show a rotating cube. If this does not work, you should be able to fix it by installing Vulkan compatible GPU drivers, however in some cases (for example running Linux on an Arm-based MacBook) there is no Vulkan support yet.
|
||||
|
||||
If you see errors like `ERROR_INITIALIZATION_FAILED` or `GPU Crashed` or `ERROR_SURFACE_LOST_KHR` then you may be able to work around this by installing different drivers for your GPU, or by selecting a different GPU to run on. (See the following GitHub issue for more details: [#14225](https://github.com/zed-industries/zed/issues/14225))
|
||||
You can find out which graphics card Zed is using by looking in the Zed log (`~/.local/share/zed/logs/Zed.log`) for `Using GPU: ...`.
|
||||
|
||||
As of Zed v0.146.x we log the selected GPU driver and you should see `Using GPU: ...` in the Zed log (`~/.local/share/zed/logs/Zed.log`).
|
||||
If you see errors like `ERROR_INITIALIZATION_FAILED` or `GPU Crashed` or `ERROR_SURFACE_LOST_KHR` then you may be able to work around this by installing different drivers for your GPU, or by selecting a different GPU to run on. (See [#14225](https://github.com/zed-industries/zed/issues/14225))
|
||||
|
||||
If Zed is selecting your integrated GPU instead of your discrete GPU, you can fix this by exporting the environment variable `DRI_PRIME=1` before running Zed.
|
||||
On some systems the file `/etc/prime-discrete` can be used to enforce the use of a discrete GPU using [PRIME](https://wiki.archlinux.org/title/PRIME). Depending on the details of your setup, you may need to change the contents of this file to "on" (to force discrete graphics) or "off" (to force integrated graphics).
|
||||
|
||||
On others, you may be able to the environment variable `DRI_PRIME=1` when running Zed to force the use of the discrete GPU.
|
||||
|
||||
If you're using an AMD GPU, you might get a 'Broken Pipe' error. Try using the RADV or Mesa drivers. (See [#13880](https://github.com/zed-industries/zed/issues/13880))
|
||||
|
||||
If you are using Mesa, and want more control over which GPU is selected you can run `MESA_VK_DEVICE_SELECT=list zed --foreground` to get a list of available GPUs and then export `MESA_VK_DEVICE_SELECT=xxxx:yyyy` to choose a specific device.
|
||||
|
||||
If you are using `amdvlk` you may find that zed only opens when run with `sudo $(which zed)`. To fix this, remove the `amdvlk` and `lib32-amdvlk` packages and install mesa/vulkan instead. ([#14141](https://github.com/zed-industries/zed/issues/14141).
|
||||
|
||||
If you have a discrete GPU and you are using [PRIME](https://wiki.archlinux.org/title/PRIME) (e.g. Pop_OS 24.04, ArchLinux, etc) you may be able to configure Zed to work by switching `/etc/prime-discrete` from 'off' to 'on' (or the reverse).
|
||||
If you are using `amdvlk` you may find that zed only opens when run with `sudo $(which zed)`. To fix this, remove the `amdvlk` and `lib32-amdvlk` packages and install mesa/vulkan instead. ([#14141](https://github.com/zed-industries/zed/issues/14141)).
|
||||
|
||||
For more information, the [Arch guide to Vulkan](https://wiki.archlinux.org/title/Vulkan) has some good steps that translate well to most distributions.
|
||||
|
||||
If Vulkan is configured correctly, and Zed is still slow for you, please [file an issue](https://github.com/zed-industries/zed) with as much information as possible.
|
||||
If Vulkan is configured correctly, and Zed is still not working for you, please [file an issue](https://github.com/zed-industries/zed) with as much information as possible.
|
||||
|
||||
### I can't open any files
|
||||
|
||||
@@ -153,20 +153,3 @@ If you are seeing "too many open files" then first try `sysctl fs.inotify`.
|
||||
- You should see that `max_user_watches` is 8000 or higher (you can change the limit with `sudo sysctl fs.inotify.max_user_watches=64000`). Zed needs one watch per directory in all your open projects + one per git repository + a handful more for settings, themes, keymaps, extensions.
|
||||
|
||||
It is also possible that you are running out of file descriptors. You can check the limits with `ulimit` and update them by editing `/etc/security/limits.conf`.
|
||||
|
||||
### FIPS Mode OpenSSL internal error {#fips}
|
||||
|
||||
If your machine is running in FIPS mode (`cat /proc/sys/crypto/fips_enabled` is set to `1`) Zed may fail to start and output the following when launched with `zed --foreground`:
|
||||
|
||||
```
|
||||
crypto/fips/fips.c:154: OpenSSL internal error: FATAL FIPS SELFTEST FAILURE
|
||||
```
|
||||
|
||||
As a workaround, remove the bundled `libssl` and `libcrypto` libraries from the `zed.app/lib` directory:
|
||||
|
||||
```
|
||||
rm ~/.local/zed.app/lib/libssl.so.1.1
|
||||
rm ~/.local/zed.app/lib/libcrypto.so.1.1
|
||||
```
|
||||
|
||||
This will force zed to fallback to the system `libssl` and `libcrypto` libraries.
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
[package]
|
||||
name = "zed_haskell"
|
||||
version = "0.1.3"
|
||||
edition.workspace = true
|
||||
publish.workspace = true
|
||||
license = "Apache-2.0"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/haskell.rs"
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
zed_extension_api = "0.1.0"
|
||||
@@ -1 +0,0 @@
|
||||
../../LICENSE-APACHE
|
||||
@@ -1,18 +0,0 @@
|
||||
id = "haskell"
|
||||
name = "Haskell"
|
||||
description = "Haskell support."
|
||||
version = "0.1.3"
|
||||
schema_version = 1
|
||||
authors = [
|
||||
"Pocæus <github@pocaeus.com>",
|
||||
"Lei <45155667+leifu1128@users.noreply.github.com>"
|
||||
]
|
||||
repository = "https://github.com/zed-industries/zed"
|
||||
|
||||
[language_servers.hls]
|
||||
name = "Haskell Language Server"
|
||||
language = "Haskell"
|
||||
|
||||
[grammars.haskell]
|
||||
repository = "https://github.com/tree-sitter/tree-sitter-haskell"
|
||||
commit = "8a99848fc734f9c4ea523b3f2a07df133cbbcec2"
|
||||
@@ -1,3 +0,0 @@
|
||||
("(" @open ")" @close)
|
||||
("[" @open "]" @close)
|
||||
("{" @open "}" @close)
|
||||
@@ -1,14 +0,0 @@
|
||||
name = "Haskell"
|
||||
grammar = "haskell"
|
||||
path_suffixes = ["hs"]
|
||||
autoclose_before = ",=)}]"
|
||||
line_comments = ["-- "]
|
||||
block_comment = ["{- ", " -}"]
|
||||
brackets = [
|
||||
{ start = "{", end = "}", close = true, newline = true },
|
||||
{ start = "[", end = "]", close = true, newline = true },
|
||||
{ start = "(", end = ")", close = true, newline = true },
|
||||
{ start = "\"", end = "\"", close = true, newline = false },
|
||||
{ start = "'", end = "'", close = true, newline = false },
|
||||
{ start = "`", end = "`", close = true, newline = false },
|
||||
]
|
||||
@@ -1,156 +0,0 @@
|
||||
;; Copyright 2022 nvim-treesitter
|
||||
;;
|
||||
;; Licensed under the Apache License, Version 2.0 (the "License");
|
||||
;; you may not use this file except in compliance with the License.
|
||||
;; You may obtain a copy of the License at
|
||||
;;
|
||||
;; http://www.apache.org/licenses/LICENSE-2.0
|
||||
;;
|
||||
;; Unless required by applicable law or agreed to in writing, software
|
||||
;; distributed under the License is distributed on an "AS IS" BASIS,
|
||||
;; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
;; See the License for the specific language governing permissions and
|
||||
;; limitations under the License.
|
||||
|
||||
;; ----------------------------------------------------------------------------
|
||||
;; Literals and comments
|
||||
|
||||
(integer) @number
|
||||
(exp_negation) @number
|
||||
(exp_literal (float)) @float
|
||||
(char) @string
|
||||
(string) @string
|
||||
|
||||
(con_unit) @symbol ; unit, as in ()
|
||||
|
||||
(comment) @comment
|
||||
|
||||
|
||||
;; ----------------------------------------------------------------------------
|
||||
;; Punctuation
|
||||
|
||||
[
|
||||
"("
|
||||
")"
|
||||
"{"
|
||||
"}"
|
||||
"["
|
||||
"]"
|
||||
] @punctuation.bracket
|
||||
|
||||
[
|
||||
(comma)
|
||||
";"
|
||||
] @punctuation.delimiter
|
||||
|
||||
|
||||
;; ----------------------------------------------------------------------------
|
||||
;; Keywords, operators, includes
|
||||
|
||||
[
|
||||
"forall"
|
||||
"∀"
|
||||
] @keyword
|
||||
|
||||
(pragma) @constant
|
||||
|
||||
[
|
||||
"if"
|
||||
"then"
|
||||
"else"
|
||||
"case"
|
||||
"of"
|
||||
] @keyword
|
||||
|
||||
(exp_lambda_cases "\\" ("cases" @variant))
|
||||
|
||||
[
|
||||
"import"
|
||||
"qualified"
|
||||
"module"
|
||||
] @keyword
|
||||
|
||||
[
|
||||
(operator)
|
||||
(constructor_operator)
|
||||
(type_operator)
|
||||
(tycon_arrow)
|
||||
(qualified_module) ; grabs the `.` (dot), ex: import System.IO
|
||||
(all_names)
|
||||
(wildcard)
|
||||
"="
|
||||
"|"
|
||||
"::"
|
||||
"=>"
|
||||
"->"
|
||||
"<-"
|
||||
"\\"
|
||||
"`"
|
||||
"@"
|
||||
] @operator
|
||||
|
||||
(module) @title
|
||||
|
||||
[
|
||||
(where)
|
||||
"let"
|
||||
"in"
|
||||
"class"
|
||||
"instance"
|
||||
"data"
|
||||
"newtype"
|
||||
"family"
|
||||
"type"
|
||||
"as"
|
||||
"hiding"
|
||||
"deriving"
|
||||
"via"
|
||||
"stock"
|
||||
"anyclass"
|
||||
"do"
|
||||
"mdo"
|
||||
"rec"
|
||||
"infix"
|
||||
"infixl"
|
||||
"infixr"
|
||||
] @keyword
|
||||
|
||||
|
||||
;; ----------------------------------------------------------------------------
|
||||
;; Functions and variables
|
||||
|
||||
(variable) @variable
|
||||
(pat_wildcard) @variable
|
||||
|
||||
(signature name: (variable) @type)
|
||||
(function
|
||||
name: (variable) @function
|
||||
patterns: (patterns))
|
||||
((signature (fun)) . (function (variable) @function))
|
||||
((signature (context (fun))) . (function (variable) @function))
|
||||
((signature (forall (context (fun)))) . (function (variable) @function))
|
||||
|
||||
(exp_infix (variable) @operator) ; consider infix functions as operators
|
||||
|
||||
(exp_infix (exp_name) @function (#set! "priority" 101))
|
||||
(exp_apply . (exp_name (variable) @function))
|
||||
(exp_apply . (exp_name (qualified_variable (variable) @function)))
|
||||
|
||||
|
||||
;; ----------------------------------------------------------------------------
|
||||
;; Types
|
||||
|
||||
(type) @type
|
||||
(type_variable) @type
|
||||
|
||||
(constructor) @constructor
|
||||
|
||||
; True or False
|
||||
((constructor) @_bool (#match? @_bool "(True|False)")) @boolean
|
||||
|
||||
|
||||
;; ----------------------------------------------------------------------------
|
||||
;; Quasi-quotes
|
||||
|
||||
(quoter) @function
|
||||
; Highlighting of quasiquote_body is handled by injections.scm
|
||||
@@ -1,3 +0,0 @@
|
||||
(_ "[" "]" @end) @indent
|
||||
(_ "{" "}" @end) @indent
|
||||
(_ "(" ")" @end) @indent
|
||||
@@ -1,26 +0,0 @@
|
||||
(adt
|
||||
"data" @context
|
||||
name: (type) @name) @item
|
||||
|
||||
(type_alias
|
||||
"type" @context
|
||||
name: (type) @name) @item
|
||||
|
||||
(newtype
|
||||
"newtype" @context
|
||||
name: (type) @name) @item
|
||||
|
||||
(signature
|
||||
name: (variable) @name) @item
|
||||
|
||||
(class
|
||||
"class" @context
|
||||
(class_head) @name) @item
|
||||
|
||||
(instance
|
||||
"instance" @context
|
||||
(instance_head) @name) @item
|
||||
|
||||
(foreign_import
|
||||
"foreign" @context
|
||||
(impent) @name) @item
|
||||
@@ -1,12 +0,0 @@
|
||||
(comment)+ @comment.around
|
||||
|
||||
[
|
||||
(adt)
|
||||
(type_alias)
|
||||
(newtype)
|
||||
] @class.around
|
||||
|
||||
(record_fields "{" (_)* @class.inside "}")
|
||||
|
||||
((signature)? (function)+) @function.around
|
||||
(function rhs:(_) @function.inside)
|
||||
@@ -1,67 +0,0 @@
|
||||
use zed::lsp::{Symbol, SymbolKind};
|
||||
use zed::{CodeLabel, CodeLabelSpan};
|
||||
use zed_extension_api::{self as zed, Result};
|
||||
|
||||
struct HaskellExtension;
|
||||
|
||||
impl zed::Extension for HaskellExtension {
|
||||
fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
|
||||
fn language_server_command(
|
||||
&mut self,
|
||||
_language_server_id: &zed::LanguageServerId,
|
||||
worktree: &zed::Worktree,
|
||||
) -> Result<zed::Command> {
|
||||
let path = worktree
|
||||
.which("haskell-language-server-wrapper")
|
||||
.ok_or_else(|| "hls must be installed via ghcup".to_string())?;
|
||||
|
||||
Ok(zed::Command {
|
||||
command: path,
|
||||
args: vec!["lsp".to_string()],
|
||||
env: worktree.shell_env(),
|
||||
})
|
||||
}
|
||||
|
||||
fn label_for_symbol(
|
||||
&self,
|
||||
_language_server_id: &zed::LanguageServerId,
|
||||
symbol: Symbol,
|
||||
) -> Option<CodeLabel> {
|
||||
let name = &symbol.name;
|
||||
|
||||
let (code, display_range, filter_range) = match symbol.kind {
|
||||
SymbolKind::Struct => {
|
||||
let data_decl = "data ";
|
||||
let code = format!("{data_decl}{name} = A");
|
||||
let display_range = 0..data_decl.len() + name.len();
|
||||
let filter_range = data_decl.len()..display_range.end;
|
||||
(code, display_range, filter_range)
|
||||
}
|
||||
SymbolKind::Constructor => {
|
||||
let data_decl = "data A = ";
|
||||
let code = format!("{data_decl}{name}");
|
||||
let display_range = data_decl.len()..data_decl.len() + name.len();
|
||||
let filter_range = 0..name.len();
|
||||
(code, display_range, filter_range)
|
||||
}
|
||||
SymbolKind::Variable => {
|
||||
let code = format!("{name} :: T");
|
||||
let display_range = 0..name.len();
|
||||
let filter_range = 0..name.len();
|
||||
(code, display_range, filter_range)
|
||||
}
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
Some(CodeLabel {
|
||||
spans: vec![CodeLabelSpan::code_range(display_range)],
|
||||
filter_range: filter_range.into(),
|
||||
code,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
zed::register_extension!(HaskellExtension);
|
||||
Reference in New Issue
Block a user