Compare commits
26 Commits
sublime_st
...
lua-run-cl
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5324ef894e | ||
|
|
b1fca741a9 | ||
|
|
fb51b99198 | ||
|
|
cb79ee20c7 | ||
|
|
c5f0a5bb3e | ||
|
|
38136cb0c0 | ||
|
|
6ed6e8bc26 | ||
|
|
4846e6fb3a | ||
|
|
cb543f9546 | ||
|
|
450d727a04 | ||
|
|
60b3eb3f76 | ||
|
|
bbe7c9a738 | ||
|
|
f6345a6995 | ||
|
|
e70d0edfac | ||
|
|
921c24e274 | ||
|
|
18f3f8097f | ||
|
|
4f6682c7fe | ||
|
|
f57dece2d5 | ||
|
|
103ad635d9 | ||
|
|
ec5e7a2653 | ||
|
|
05d3ee8555 | ||
|
|
1b34437839 | ||
|
|
3ff2c8fc38 | ||
|
|
b0b0b00fae | ||
|
|
80fb88520f | ||
|
|
aef84d453a |
37
Cargo.lock
generated
37
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]]
|
||||
@@ -2301,7 +2300,7 @@ dependencies = [
|
||||
"cap-primitives",
|
||||
"cap-std",
|
||||
"io-lifetimes",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2329,7 +2328,7 @@ dependencies = [
|
||||
"ipnet",
|
||||
"maybe-owned",
|
||||
"rustix",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.59.0",
|
||||
"winx",
|
||||
]
|
||||
|
||||
@@ -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]]
|
||||
@@ -4403,7 +4402,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4536,6 +4535,7 @@ dependencies = [
|
||||
"async-tar",
|
||||
"async-trait",
|
||||
"collections",
|
||||
"convert_case 0.8.0",
|
||||
"fs",
|
||||
"futures 0.3.31",
|
||||
"gpui",
|
||||
@@ -5064,7 +5064,7 @@ checksum = "5e2e6123af26f0f2c51cc66869137080199406754903cc926a7690401ce09cb4"
|
||||
dependencies = [
|
||||
"io-lifetimes",
|
||||
"rustix",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6709,7 +6709,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2285ddfe3054097ef4b2fe909ef8c3bcd1ea52a8f0d274416caebeef39f04a65"
|
||||
dependencies = [
|
||||
"io-lifetimes",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -10734,7 +10734,7 @@ dependencies = [
|
||||
"once_cell",
|
||||
"socket2",
|
||||
"tracing",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -11656,7 +11656,7 @@ dependencies = [
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"once_cell",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -11927,8 +11927,8 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"settings",
|
||||
"shlex",
|
||||
"util",
|
||||
"workspace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -13427,7 +13427,7 @@ dependencies = [
|
||||
"fd-lock",
|
||||
"io-lifetimes",
|
||||
"rustix",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.59.0",
|
||||
"winx",
|
||||
]
|
||||
|
||||
@@ -13567,7 +13567,7 @@ dependencies = [
|
||||
"getrandom 0.3.1",
|
||||
"once_cell",
|
||||
"rustix",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -15913,7 +15913,7 @@ version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
|
||||
dependencies = [
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -16378,7 +16378,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f3fd376f71958b862e7afb20cfe5a22830e1963462f3a17f49d82a6c1d1f42d"
|
||||
dependencies = [
|
||||
"bitflags 2.8.0",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -17070,13 +17070,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 |
@@ -105,6 +105,7 @@
|
||||
"ctrl-shift-home": "editor::SelectToBeginning",
|
||||
"ctrl-shift-end": "editor::SelectToEnd",
|
||||
"ctrl-a": "editor::SelectAll",
|
||||
"ctrl-l": "editor::SelectLine",
|
||||
"ctrl-shift-i": "editor::Format",
|
||||
// "cmd-shift-left": ["editor::SelectToBeginningOfLine", {"stop_at_soft_wraps": true, "stop_at_indent": true }],
|
||||
// "ctrl-shift-a": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": true, "stop_at_indent": true }],
|
||||
@@ -470,15 +471,15 @@
|
||||
"ctrl-shift-d": "editor::DuplicateLineDown",
|
||||
"ctrl-shift-j": "editor::JoinLines",
|
||||
"ctrl-alt-backspace": "editor::DeleteToPreviousSubwordStart",
|
||||
"ctrl-alt-h": "editor::DeleteToPreviousSubwordStart",
|
||||
"ctrl-alt-delete": "editor::DeleteToNextSubwordEnd",
|
||||
"ctrl-alt-left": "editor::MoveToPreviousSubwordStart", // macos sublime
|
||||
"ctrl-alt-right": "editor::MoveToNextSubwordStart", // macos sublime
|
||||
"alt-left": "editor::MoveToPreviousWordStart",
|
||||
"alt-right": "editor::MoveToNextWordEnd",
|
||||
"ctrl-l": "editor::SelectLine", // goes downwards
|
||||
//"alt-l": "editor::SelectLineUp",
|
||||
"alt-shift-left": "editor::SelectToPreviousSubwordStart",
|
||||
"alt-shift-right": "editor::SelectToNextSubwordEnd"
|
||||
"ctrl-alt-d": "editor::DeleteToNextSubwordEnd",
|
||||
"ctrl-alt-left": "editor::MoveToPreviousSubwordStart",
|
||||
"ctrl-alt-right": "editor::MoveToNextSubwordEnd",
|
||||
"ctrl-alt-shift-left": "editor::SelectToPreviousSubwordStart",
|
||||
"ctrl-alt-shift-b": "editor::SelectToPreviousSubwordStart",
|
||||
"ctrl-alt-shift-right": "editor::SelectToNextSubwordEnd",
|
||||
"ctrl-alt-shift-f": "editor::SelectToNextSubwordEnd"
|
||||
}
|
||||
},
|
||||
// Bindings from Atom
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -28,10 +28,6 @@
|
||||
{
|
||||
"context": "Editor",
|
||||
"bindings": {
|
||||
"alt-left": "editor::MoveToPreviousSubwordStart",
|
||||
"alt-right": "editor::MoveToNextSubwordStart",
|
||||
"alt-shift-left": "editor::SelectToPreviousSubwordStart",
|
||||
"alt-shift-right": "editor::SelectToNextSubwordEnd",
|
||||
"ctrl-alt-up": "editor::AddSelectionAbove",
|
||||
"ctrl-alt-down": "editor::AddSelectionBelow",
|
||||
"ctrl-shift-up": "editor::MoveLineUp",
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -16,6 +16,7 @@ doctest = false
|
||||
anyhow.workspace = true
|
||||
assistant_tool.workspace = true
|
||||
collections.workspace = true
|
||||
shlex.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
mlua.workspace = true
|
||||
@@ -27,11 +28,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,9 +1,7 @@
|
||||
---@diagnostic disable: undefined-global
|
||||
|
||||
-- Create a sandbox environment
|
||||
local sandbox = {}
|
||||
|
||||
-- Allow access to standard libraries (safe subset)
|
||||
sandbox.string = string
|
||||
sandbox.table = table
|
||||
sandbox.math = math
|
||||
@@ -15,24 +13,19 @@ sandbox.pairs = pairs
|
||||
sandbox.ipairs = ipairs
|
||||
sandbox.search = search
|
||||
|
||||
-- Create a sandboxed version of LuaFileIO
|
||||
local io = {}
|
||||
|
||||
-- File functions
|
||||
io.open = sb_io_open
|
||||
io.popen = sb_io_popen
|
||||
|
||||
-- Add the sandboxed io library to the sandbox environment
|
||||
sandbox.io = io
|
||||
|
||||
|
||||
-- Load the script with the sandbox environment
|
||||
local user_script_fn, err = load(user_script, nil, "t", sandbox)
|
||||
|
||||
if not user_script_fn then
|
||||
error("Failed to load user script: " .. tostring(err))
|
||||
end
|
||||
|
||||
-- Execute the user script within the sandbox
|
||||
local success, result = pcall(user_script_fn)
|
||||
|
||||
if not success then
|
||||
|
||||
713
crates/scripting_tool/src/sandboxed_shell.rs
Normal file
713
crates/scripting_tool/src/sandboxed_shell.rs
Normal file
@@ -0,0 +1,713 @@
|
||||
/// Models will commonly generate POSIX shell one-liner commands which
|
||||
/// they run via io.popen() in Lua. Instead of giving those shell command
|
||||
/// strings to the operating system - which is a security risk, and
|
||||
/// which can eaisly fail on Windows, since Windows doesn't do POSIX - we
|
||||
/// parse the shell command ourselves and translate it into a sequence of
|
||||
/// commands in our normal sandbox. Essentially, this is an extremely
|
||||
/// minimalstic shell which Lua popen() commands can execute in.
|
||||
///
|
||||
/// Our shell supports:
|
||||
/// - Basic commands and args
|
||||
/// - The operators `|`, `&&`, `;`, `>`, `1>`, `2>`, `&>`, `>&`
|
||||
///
|
||||
/// The operators currently have to have whitespace around them because the
|
||||
/// `shlex` crate we use to tokenize the strings does not treat operators
|
||||
/// as word boundaries, even though shells do. Fortunately, LLMs consistently
|
||||
/// generate spaces around these operators anyway.
|
||||
use mlua::{Error, Result};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Default)]
|
||||
pub struct ShellCmd {
|
||||
pub command: String,
|
||||
pub args: Vec<String>,
|
||||
pub stdout_redirect: Option<String>,
|
||||
pub stderr_redirect: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum Operator {
|
||||
/// The `|` shell operator (highest precedence)
|
||||
Pipe,
|
||||
/// The `&&` shell operator (medium precedence)
|
||||
And,
|
||||
/// The `;` shell operator (lowest precedence)
|
||||
Semicolon,
|
||||
}
|
||||
|
||||
impl Operator {
|
||||
fn precedence(&self) -> u8 {
|
||||
match self {
|
||||
Operator::Pipe => 3,
|
||||
Operator::And => 2,
|
||||
Operator::Semicolon => 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum ShellAst {
|
||||
Command(ShellCmd),
|
||||
Operation {
|
||||
operator: Operator,
|
||||
left: Box<ShellAst>,
|
||||
right: Box<ShellAst>,
|
||||
},
|
||||
}
|
||||
|
||||
impl ShellAst {
|
||||
/// Parse a shell string and build an abstract syntax tree.
|
||||
pub fn parse(string: impl AsRef<str>) -> Result<Self> {
|
||||
let string = string.as_ref();
|
||||
|
||||
// Check for unsupported shell features
|
||||
if string.contains('$')
|
||||
|| string.contains('`')
|
||||
|| string.contains('(')
|
||||
|| string.contains(')')
|
||||
|| string.contains('{')
|
||||
|| string.contains('}')
|
||||
{
|
||||
return Err(Error::RuntimeError(
|
||||
"Complex shell features (subshells, variables, backgrounding, etc.) are not available in this shell."
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let mut parser = ShellParser::new(string);
|
||||
parser.parse_expression(0)
|
||||
}
|
||||
}
|
||||
|
||||
enum Redirect {
|
||||
Stdout,
|
||||
Stderr,
|
||||
Both,
|
||||
}
|
||||
|
||||
struct ShellParser<'a> {
|
||||
lexer: shlex::Shlex<'a>,
|
||||
current_token: Option<String>,
|
||||
}
|
||||
|
||||
impl<'a> ShellParser<'a> {
|
||||
fn new(input: &'a str) -> Self {
|
||||
let mut lexer = shlex::Shlex::new(input);
|
||||
let current_token = lexer.next();
|
||||
|
||||
Self {
|
||||
lexer,
|
||||
current_token,
|
||||
}
|
||||
}
|
||||
|
||||
fn advance(&mut self) {
|
||||
self.current_token = self.lexer.next();
|
||||
}
|
||||
|
||||
fn peek(&self) -> Option<&str> {
|
||||
self.current_token.as_deref()
|
||||
}
|
||||
|
||||
fn parse_expression(&mut self, min_precedence: u8) -> Result<ShellAst> {
|
||||
// Parse the first command or atom
|
||||
let mut left = ShellAst::Command(self.parse_command()?);
|
||||
|
||||
// While we have operators with sufficient precedence, keep building the tree
|
||||
loop {
|
||||
let op = match self.parse_operator() {
|
||||
Some(op) if op.precedence() >= min_precedence => op,
|
||||
_ => break,
|
||||
};
|
||||
|
||||
// Consume the operator token
|
||||
self.advance();
|
||||
|
||||
// Special case for trailing semicolons - if we have no more tokens,
|
||||
// we don't need to parse another command
|
||||
if op == Operator::Semicolon && self.peek().is_none() {
|
||||
break;
|
||||
}
|
||||
|
||||
// Parse the right side with higher precedence
|
||||
// For left-associative operators, we use op.precedence() + 1
|
||||
let right = self.parse_expression(op.precedence() + 1)?;
|
||||
|
||||
// Build the operation node
|
||||
left = ShellAst::Operation {
|
||||
operator: op,
|
||||
left: Box::new(left),
|
||||
right: Box::new(right),
|
||||
};
|
||||
}
|
||||
|
||||
Ok(left)
|
||||
}
|
||||
|
||||
fn parse_operator(&self) -> Option<Operator> {
|
||||
match self.peek()? {
|
||||
"|" => Some(Operator::Pipe),
|
||||
"&&" => Some(Operator::And),
|
||||
";" => Some(Operator::Semicolon),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_redirection(&mut self, cmd: &mut ShellCmd, redirect: Redirect) -> Result<()> {
|
||||
self.advance(); // consume the redirection operator
|
||||
|
||||
let target = self.peek().ok_or_else(|| {
|
||||
Error::RuntimeError("Missing redirection target in shell".to_string())
|
||||
})?;
|
||||
|
||||
match redirect {
|
||||
Redirect::Stdout => {
|
||||
cmd.stdout_redirect = Some(target.to_string());
|
||||
}
|
||||
Redirect::Stderr => {
|
||||
cmd.stderr_redirect = Some(target.to_string());
|
||||
}
|
||||
Redirect::Both => {
|
||||
cmd.stdout_redirect = Some(target.to_string());
|
||||
cmd.stderr_redirect = Some(target.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
self.advance(); // consume the target
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_command(&mut self) -> Result<ShellCmd> {
|
||||
let mut cmd = ShellCmd::default();
|
||||
|
||||
// Process tokens until we hit an operator or end of input
|
||||
loop {
|
||||
let redirect;
|
||||
|
||||
match self.peek() {
|
||||
Some(token) => {
|
||||
match token {
|
||||
"|" | "&&" | ";" => break, // These are operators, not part of the command
|
||||
">" | "1>" => {
|
||||
redirect = Some(Redirect::Stdout);
|
||||
}
|
||||
"2>" => {
|
||||
redirect = Some(Redirect::Stderr);
|
||||
}
|
||||
"&>" | ">&" => {
|
||||
redirect = Some(Redirect::Both);
|
||||
}
|
||||
"&" => {
|
||||
// Reject ampersand as it's used for backgrounding processes
|
||||
return Err(Error::RuntimeError(
|
||||
"Background processes (using &) are not available in this shell."
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
_ => {
|
||||
redirect = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
break; // We ran out of tokens; exit the loop.
|
||||
}
|
||||
}
|
||||
|
||||
// We do this separate conditional after the borrow from the peek()
|
||||
// has expired, to avoid a borrow checker error.
|
||||
match redirect {
|
||||
Some(redirect) => {
|
||||
self.handle_redirection(&mut cmd, redirect)?;
|
||||
}
|
||||
None => {
|
||||
// It's either the command name or an argument
|
||||
let mut token = self.current_token.take().unwrap();
|
||||
self.advance();
|
||||
|
||||
// Handle trailing semicolons
|
||||
let original_token_len = token.len();
|
||||
while token.ends_with(';') {
|
||||
token.pop();
|
||||
}
|
||||
|
||||
let had_semicolon = token.len() != original_token_len;
|
||||
|
||||
if cmd.command.is_empty() {
|
||||
cmd.command = token;
|
||||
} else {
|
||||
cmd.args.push(token);
|
||||
}
|
||||
|
||||
if had_semicolon {
|
||||
// Put the semicolon back as the next token, so after we break we parse it.
|
||||
self.current_token = Some(";".to_string());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if cmd.command.is_empty() {
|
||||
return Err(Error::RuntimeError(
|
||||
"Missing command to run in shell".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(cmd)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_simple_command() {
|
||||
// Basic command with no args or operators
|
||||
let cmd = "ls";
|
||||
let ast = ShellAst::parse(cmd).expect("parsing failed for {cmd:?}");
|
||||
|
||||
if let ShellAst::Command(shell_cmd) = ast {
|
||||
assert_eq!(shell_cmd.command, "ls");
|
||||
assert!(shell_cmd.args.is_empty());
|
||||
assert_eq!(shell_cmd.stdout_redirect, None);
|
||||
assert_eq!(shell_cmd.stderr_redirect, None);
|
||||
} else {
|
||||
panic!("Expected Command node");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_command_with_args() {
|
||||
// Command with arguments
|
||||
let cmd = "ls -la /home";
|
||||
let ast = ShellAst::parse(cmd).expect("parsing failed for {cmd:?}");
|
||||
|
||||
if let ShellAst::Command(shell_cmd) = ast {
|
||||
assert_eq!(shell_cmd.command, "ls");
|
||||
assert_eq!(shell_cmd.args, vec!["-la".to_string(), "/home".to_string()]);
|
||||
assert_eq!(shell_cmd.stdout_redirect, None);
|
||||
assert_eq!(shell_cmd.stderr_redirect, None);
|
||||
} else {
|
||||
panic!("Expected Command node");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_simple_pipe() {
|
||||
// Test pipe operator
|
||||
let cmd = "ls -l | grep txt";
|
||||
let ast = ShellAst::parse(cmd).expect("parsing failed for {cmd:?}");
|
||||
|
||||
if let ShellAst::Operation {
|
||||
operator,
|
||||
left,
|
||||
right,
|
||||
} = ast
|
||||
{
|
||||
assert_eq!(operator, Operator::Pipe);
|
||||
|
||||
if let ShellAst::Command(left_cmd) = *left {
|
||||
assert_eq!(left_cmd.command, "ls");
|
||||
assert_eq!(left_cmd.args, vec!["-l".to_string()]);
|
||||
} else {
|
||||
panic!("Expected Command node for left side");
|
||||
}
|
||||
|
||||
if let ShellAst::Command(right_cmd) = *right {
|
||||
assert_eq!(right_cmd.command, "grep");
|
||||
assert_eq!(right_cmd.args, vec!["txt".to_string()]);
|
||||
} else {
|
||||
panic!("Expected Command node for right side");
|
||||
}
|
||||
} else {
|
||||
panic!("Expected Operation node");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_simple_and() {
|
||||
// Test && operator
|
||||
let cmd = "mkdir test && cd test";
|
||||
let ast = ShellAst::parse(cmd).expect("parsing failed for {cmd:?}");
|
||||
|
||||
if let ShellAst::Operation {
|
||||
operator,
|
||||
left,
|
||||
right,
|
||||
} = ast
|
||||
{
|
||||
assert_eq!(operator, Operator::And);
|
||||
|
||||
if let ShellAst::Command(left_cmd) = *left {
|
||||
assert_eq!(left_cmd.command, "mkdir");
|
||||
assert_eq!(left_cmd.args, vec!["test".to_string()]);
|
||||
} else {
|
||||
panic!("Expected Command node for left side");
|
||||
}
|
||||
|
||||
if let ShellAst::Command(right_cmd) = *right {
|
||||
assert_eq!(right_cmd.command, "cd");
|
||||
assert_eq!(right_cmd.args, vec!["test".to_string()]);
|
||||
} else {
|
||||
panic!("Expected Command node for right side");
|
||||
}
|
||||
} else {
|
||||
panic!("Expected Operation node");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_complex_chain_with_precedence() {
|
||||
// Test a more complex chain with different precedence levels
|
||||
let cmd = "echo hello | grep e && ls -l ; echo done";
|
||||
let ast = ShellAst::parse(cmd).expect("parsing failed for {cmd:?}");
|
||||
|
||||
// The tree should be structured with precedence:
|
||||
// - Pipe has highest precedence
|
||||
// - Then And
|
||||
// - Then Semicolon (lowest)
|
||||
|
||||
if let ShellAst::Operation {
|
||||
operator,
|
||||
left,
|
||||
right,
|
||||
} = &ast
|
||||
{
|
||||
assert_eq!(*operator, Operator::Semicolon);
|
||||
|
||||
if let ShellAst::Operation {
|
||||
operator,
|
||||
left: inner_left,
|
||||
right: inner_right,
|
||||
} = &**left
|
||||
{
|
||||
assert_eq!(*operator, Operator::And);
|
||||
|
||||
if let ShellAst::Operation {
|
||||
operator,
|
||||
left: pipe_left,
|
||||
right: pipe_right,
|
||||
} = &**inner_left
|
||||
{
|
||||
assert_eq!(*operator, Operator::Pipe);
|
||||
|
||||
if let ShellAst::Command(cmd) = &**pipe_left {
|
||||
assert_eq!(cmd.command, "echo");
|
||||
assert_eq!(cmd.args, vec!["hello".to_string()]);
|
||||
} else {
|
||||
panic!("Expected Command node for pipe left branch");
|
||||
}
|
||||
|
||||
if let ShellAst::Command(cmd) = &**pipe_right {
|
||||
assert_eq!(cmd.command, "grep");
|
||||
assert_eq!(cmd.args, vec!["e".to_string()]);
|
||||
} else {
|
||||
panic!("Expected Command node for pipe right branch");
|
||||
}
|
||||
} else {
|
||||
panic!("Expected Pipe operation node");
|
||||
}
|
||||
|
||||
if let ShellAst::Command(cmd) = &**inner_right {
|
||||
assert_eq!(cmd.command, "ls");
|
||||
assert_eq!(cmd.args, vec!["-l".to_string()]);
|
||||
} else {
|
||||
panic!("Expected Command node for and right branch");
|
||||
}
|
||||
} else {
|
||||
panic!("Expected And operation node");
|
||||
}
|
||||
|
||||
if let ShellAst::Command(cmd) = &**right {
|
||||
assert_eq!(cmd.command, "echo");
|
||||
assert_eq!(cmd.args, vec!["done".to_string()]);
|
||||
} else {
|
||||
panic!("Expected Command node for semicolon right branch");
|
||||
}
|
||||
} else {
|
||||
panic!("Expected Semicolon operation node");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_stdout_redirection() {
|
||||
// Test stdout redirection
|
||||
let cmd = "echo hello > output.txt";
|
||||
let ast = ShellAst::parse(cmd).expect("parsing failed for {cmd:?}");
|
||||
|
||||
if let ShellAst::Command(shell_cmd) = ast {
|
||||
assert_eq!(shell_cmd.command, "echo");
|
||||
assert_eq!(shell_cmd.args, vec!["hello".to_string()]);
|
||||
assert_eq!(shell_cmd.stdout_redirect, Some("output.txt".to_string()));
|
||||
assert_eq!(shell_cmd.stderr_redirect, None);
|
||||
} else {
|
||||
panic!("Expected Command node");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_stderr_redirection() {
|
||||
// Test stderr redirection
|
||||
let cmd = "find / -name test 2> errors.log";
|
||||
let ast = ShellAst::parse(cmd).expect("parsing failed for {cmd:?}");
|
||||
|
||||
if let ShellAst::Command(shell_cmd) = ast {
|
||||
assert_eq!(shell_cmd.command, "find");
|
||||
assert_eq!(
|
||||
shell_cmd.args,
|
||||
vec!["/".to_string(), "-name".to_string(), "test".to_string()]
|
||||
);
|
||||
assert_eq!(shell_cmd.stdout_redirect, None);
|
||||
assert_eq!(shell_cmd.stderr_redirect, Some("errors.log".to_string()));
|
||||
} else {
|
||||
panic!("Expected Command node");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_both_redirections() {
|
||||
// Test both stdout and stderr redirection
|
||||
let cmd = "make &> build.log";
|
||||
let ast = ShellAst::parse(cmd).expect("parsing failed for {cmd:?}");
|
||||
|
||||
if let ShellAst::Command(shell_cmd) = ast {
|
||||
assert_eq!(shell_cmd.command, "make");
|
||||
assert!(shell_cmd.args.is_empty());
|
||||
assert_eq!(shell_cmd.stdout_redirect, Some("build.log".to_string()));
|
||||
assert_eq!(shell_cmd.stderr_redirect, Some("build.log".to_string()));
|
||||
} else {
|
||||
panic!("Expected Command node");
|
||||
}
|
||||
|
||||
// Test alternative syntax
|
||||
let cmd = "make >& build.log";
|
||||
let ast = ShellAst::parse(cmd).expect("parsing failed for {cmd:?}");
|
||||
|
||||
if let ShellAst::Command(shell_cmd) = ast {
|
||||
assert_eq!(shell_cmd.command, "make");
|
||||
assert!(shell_cmd.args.is_empty());
|
||||
assert_eq!(shell_cmd.stdout_redirect, Some("build.log".to_string()));
|
||||
assert_eq!(shell_cmd.stderr_redirect, Some("build.log".to_string()));
|
||||
} else {
|
||||
panic!("Expected Command node");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multiple_operators() {
|
||||
// Test multiple operators in a single command
|
||||
let cmd =
|
||||
"find . -name \"*.rs\" | grep impl && echo \"Found implementations\" ; echo \"Done\"";
|
||||
|
||||
// Verify the AST structure
|
||||
let ast = ShellAst::parse(cmd).expect("parsing failed for {cmd:?}");
|
||||
|
||||
if let ShellAst::Operation {
|
||||
operator: semicolon_op,
|
||||
left: semicolon_left,
|
||||
right: semicolon_right,
|
||||
} = ast
|
||||
{
|
||||
assert_eq!(semicolon_op, Operator::Semicolon);
|
||||
|
||||
if let ShellAst::Operation {
|
||||
operator: and_op,
|
||||
left: and_left,
|
||||
right: and_right,
|
||||
} = *semicolon_left
|
||||
{
|
||||
assert_eq!(and_op, Operator::And);
|
||||
|
||||
if let ShellAst::Operation {
|
||||
operator: pipe_op,
|
||||
left: pipe_left,
|
||||
right: pipe_right,
|
||||
} = *and_left
|
||||
{
|
||||
assert_eq!(pipe_op, Operator::Pipe);
|
||||
|
||||
if let ShellAst::Command(cmd) = *pipe_left {
|
||||
assert_eq!(cmd.command, "find");
|
||||
assert_eq!(
|
||||
cmd.args,
|
||||
vec![".".to_string(), "-name".to_string(), "*.rs".to_string()]
|
||||
);
|
||||
} else {
|
||||
panic!("Expected Command node for pipe left");
|
||||
}
|
||||
|
||||
if let ShellAst::Command(cmd) = *pipe_right {
|
||||
assert_eq!(cmd.command, "grep");
|
||||
assert_eq!(cmd.args, vec!["impl".to_string()]);
|
||||
} else {
|
||||
panic!("Expected Command node for pipe right");
|
||||
}
|
||||
} else {
|
||||
panic!("Expected Pipe operation");
|
||||
}
|
||||
|
||||
if let ShellAst::Command(cmd) = *and_right {
|
||||
assert_eq!(cmd.command, "echo");
|
||||
assert_eq!(cmd.args, vec!["Found implementations".to_string()]);
|
||||
} else {
|
||||
panic!("Expected Command node for and right");
|
||||
}
|
||||
} else {
|
||||
panic!("Expected And operation");
|
||||
}
|
||||
|
||||
if let ShellAst::Command(cmd) = *semicolon_right {
|
||||
assert_eq!(cmd.command, "echo");
|
||||
assert_eq!(cmd.args, vec!["Done".to_string()]);
|
||||
} else {
|
||||
panic!("Expected Command node for semicolon right");
|
||||
}
|
||||
} else {
|
||||
panic!("Expected Semicolon operation at root");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pipe_with_redirections() {
|
||||
// Test pipe with redirections
|
||||
let cmd = "cat file.txt | grep error > results.txt 2> errors.log";
|
||||
let ast = ShellAst::parse(cmd).expect("parsing failed for {cmd:?}");
|
||||
|
||||
if let ShellAst::Operation {
|
||||
operator,
|
||||
left,
|
||||
right,
|
||||
} = ast
|
||||
{
|
||||
assert_eq!(operator, Operator::Pipe);
|
||||
|
||||
if let ShellAst::Command(left_cmd) = *left {
|
||||
assert_eq!(left_cmd.command, "cat");
|
||||
assert_eq!(left_cmd.args, vec!["file.txt".to_string()]);
|
||||
assert_eq!(left_cmd.stdout_redirect, None);
|
||||
assert_eq!(left_cmd.stderr_redirect, None);
|
||||
} else {
|
||||
panic!("Expected Command node for left side");
|
||||
}
|
||||
|
||||
if let ShellAst::Command(right_cmd) = *right {
|
||||
assert_eq!(right_cmd.command, "grep");
|
||||
assert_eq!(right_cmd.args, vec!["error".to_string()]);
|
||||
assert_eq!(right_cmd.stdout_redirect, Some("results.txt".to_string()));
|
||||
assert_eq!(right_cmd.stderr_redirect, Some("errors.log".to_string()));
|
||||
} else {
|
||||
panic!("Expected Command node for right side");
|
||||
}
|
||||
} else {
|
||||
panic!("Expected Operation node");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_quoted_arguments() {
|
||||
// Test quoted arguments
|
||||
let cmd = "echo \"hello world\" | grep \"o w\"";
|
||||
let ast = ShellAst::parse(cmd).expect("parsing failed for {cmd:?}");
|
||||
|
||||
if let ShellAst::Operation {
|
||||
operator,
|
||||
left,
|
||||
right,
|
||||
} = ast
|
||||
{
|
||||
assert_eq!(operator, Operator::Pipe);
|
||||
|
||||
if let ShellAst::Command(left_cmd) = *left {
|
||||
assert_eq!(left_cmd.command, "echo");
|
||||
assert_eq!(left_cmd.args, vec!["hello world".to_string()]);
|
||||
} else {
|
||||
panic!("Expected Command node for left side");
|
||||
}
|
||||
|
||||
if let ShellAst::Command(right_cmd) = *right {
|
||||
assert_eq!(right_cmd.command, "grep");
|
||||
assert_eq!(right_cmd.args, vec!["o w".to_string()]);
|
||||
} else {
|
||||
panic!("Expected Command node for right side");
|
||||
}
|
||||
} else {
|
||||
panic!("Expected Operation node");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unsupported_features() {
|
||||
// Test unsupported shell features
|
||||
let result = ShellAst::parse("echo $HOME");
|
||||
assert!(result.is_err());
|
||||
|
||||
let result = ShellAst::parse("echo `date`");
|
||||
assert!(result.is_err());
|
||||
|
||||
let result = ShellAst::parse("echo $(date)");
|
||||
assert!(result.is_err());
|
||||
|
||||
let result = ShellAst::parse("for i in {1..5}; do echo $i; done");
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_complex_command() {
|
||||
let cmd = "find /path/to/dir -type f -name \"*.txt\" -exec grep \"pattern with spaces\";";
|
||||
let ast = ShellAst::parse(cmd).expect("parsing failed for {cmd:?}");
|
||||
|
||||
if let ShellAst::Command(shell_cmd) = ast {
|
||||
assert_eq!(shell_cmd.command, "find");
|
||||
assert_eq!(
|
||||
shell_cmd.args,
|
||||
vec![
|
||||
"/path/to/dir".to_string(),
|
||||
"-type".to_string(),
|
||||
"f".to_string(),
|
||||
"-name".to_string(),
|
||||
"*.txt".to_string(),
|
||||
"-exec".to_string(),
|
||||
"grep".to_string(),
|
||||
"pattern with spaces".to_string(),
|
||||
]
|
||||
);
|
||||
assert_eq!(shell_cmd.stdout_redirect, None);
|
||||
assert_eq!(shell_cmd.stderr_redirect, None);
|
||||
} else {
|
||||
panic!("Expected Command node");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_command() {
|
||||
// Test empty command
|
||||
let result = ShellAst::parse("");
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_missing_redirection_target() {
|
||||
// Test missing redirection target
|
||||
let result = ShellAst::parse("echo hello >");
|
||||
assert!(result.is_err());
|
||||
|
||||
let result = ShellAst::parse("ls 2>");
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ampersand_as_argument() {
|
||||
// Test & as a background operator is not allowed
|
||||
let result = ShellAst::parse("grep & file.txt");
|
||||
assert!(result.is_err());
|
||||
|
||||
// Verify the error message mentions background processes
|
||||
if let Err(Error::RuntimeError(msg)) = ShellAst::parse("grep & file.txt") {
|
||||
assert!(msg.contains("Background processes"));
|
||||
} else {
|
||||
panic!("Expected RuntimeError about background processes");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,14 @@
|
||||
mod sandboxed_shell;
|
||||
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);
|
||||
@@ -38,18 +39,13 @@ impl Tool for ScriptingTool {
|
||||
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;
|
||||
|
||||
@@ -11,11 +11,15 @@ use project::{search::SearchQuery, Fs, Project};
|
||||
use regex::Regex;
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
fs::File,
|
||||
path::{Path, PathBuf},
|
||||
process::{Command, Stdio},
|
||||
sync::Arc,
|
||||
};
|
||||
use util::{paths::PathMatcher, ResultExt};
|
||||
|
||||
use crate::sandboxed_shell::{Operator, ShellAst, ShellCmd};
|
||||
|
||||
pub struct ScriptOutput {
|
||||
pub stdout: String,
|
||||
}
|
||||
@@ -96,6 +100,16 @@ impl Session {
|
||||
}
|
||||
})?,
|
||||
)?;
|
||||
globals.set(
|
||||
"sb_io_popen",
|
||||
lua.create_function({
|
||||
move |lua, shell_str| {
|
||||
let mut allowed_commands = HashMap::default(); // TODO persist this
|
||||
|
||||
Self::io_popen(&lua, root_dir.as_ref(), shell_str, &mut allowed_commands)
|
||||
}
|
||||
})?,
|
||||
)?;
|
||||
globals.set("user_script", script)?;
|
||||
|
||||
lua.load(SANDBOX_PREAMBLE).exec_async().await?;
|
||||
@@ -126,6 +140,399 @@ impl Session {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sandboxed io.popen() function in Lua.
|
||||
fn io_popen(
|
||||
lua: &Lua,
|
||||
root_dir: Option<&Arc<Path>>,
|
||||
shell_str: mlua::String,
|
||||
allowed_commands: &mut HashMap<String, bool>,
|
||||
) -> mlua::Result<(Option<Table>, String)> {
|
||||
let root_dir = root_dir.ok_or_else(|| {
|
||||
mlua::Error::runtime("cannot execute command without a root directory")
|
||||
})?;
|
||||
|
||||
// Parse the shell command into our AST
|
||||
let ast = ShellAst::parse(shell_str.to_str()?)?;
|
||||
|
||||
// Create a lua file handle for the command output
|
||||
let file = lua.create_table()?;
|
||||
|
||||
// Create a buffer to store the command output
|
||||
let output_buffer = Arc::new(Mutex::new(String::new()));
|
||||
|
||||
// Execute the shell command based on the parsed AST
|
||||
match ast {
|
||||
ShellAst::Command(shell_cmd) => {
|
||||
let result = Self::execute_command(&shell_cmd, root_dir, allowed_commands)?;
|
||||
output_buffer.lock().push_str(&result);
|
||||
}
|
||||
ShellAst::Operation {
|
||||
operator,
|
||||
left,
|
||||
right,
|
||||
} => {
|
||||
// Handle compound operations by recursively executing them
|
||||
let left_output = Self::execute_ast_node(*left, root_dir, allowed_commands)?;
|
||||
|
||||
match operator {
|
||||
Operator::Pipe => {
|
||||
// For pipe, use left output as input to right command
|
||||
let right_output = Self::execute_ast_node_with_input(
|
||||
*right,
|
||||
&left_output,
|
||||
root_dir,
|
||||
allowed_commands,
|
||||
)?;
|
||||
output_buffer.lock().push_str(&right_output);
|
||||
}
|
||||
Operator::And => {
|
||||
// For AND, only execute right if left was successful (non-empty output as success indicator)
|
||||
if !left_output.trim().is_empty() {
|
||||
let right_output =
|
||||
Self::execute_ast_node(*right, root_dir, allowed_commands)?;
|
||||
output_buffer.lock().push_str(&right_output);
|
||||
} else {
|
||||
output_buffer.lock().push_str(&left_output);
|
||||
}
|
||||
}
|
||||
Operator::Semicolon => {
|
||||
// For semicolon, execute both regardless of result
|
||||
output_buffer.lock().push_str(&left_output);
|
||||
let right_output =
|
||||
Self::execute_ast_node(*right, root_dir, allowed_commands)?;
|
||||
output_buffer.lock().push_str(&right_output);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set up the file's content
|
||||
file.set(
|
||||
"__content",
|
||||
lua.create_userdata(FileContent(RefCell::new(
|
||||
output_buffer.lock().as_bytes().to_vec(),
|
||||
)))?,
|
||||
)?;
|
||||
file.set("__position", 0usize)?;
|
||||
file.set("__read_perm", true)?;
|
||||
file.set("__write_perm", false)?;
|
||||
|
||||
// Implement the read method for the file
|
||||
let read_fn = {
|
||||
lua.create_function(
|
||||
move |_lua, (file_userdata, format): (mlua::Table, Option<mlua::Value>)| {
|
||||
let content = file_userdata.get::<mlua::AnyUserData>("__content")?;
|
||||
let mut position = file_userdata.get::<usize>("__position")?;
|
||||
let content_ref = content.borrow::<FileContent>()?;
|
||||
let content_vec = content_ref.0.borrow();
|
||||
|
||||
if position >= content_vec.len() {
|
||||
return Ok(None); // EOF
|
||||
}
|
||||
|
||||
match format {
|
||||
Some(mlua::Value::String(s)) => {
|
||||
let format_str = s.to_string_lossy();
|
||||
|
||||
// Handle different read formats
|
||||
if format_str.starts_with("*a") {
|
||||
// Read all
|
||||
let result =
|
||||
String::from_utf8_lossy(&content_vec[position..]).to_string();
|
||||
position = content_vec.len();
|
||||
file_userdata.set("__position", position)?;
|
||||
Ok(Some(result))
|
||||
} else if format_str.starts_with("*l") {
|
||||
// Read line
|
||||
let mut line = Vec::new();
|
||||
let mut found_newline = false;
|
||||
|
||||
while position < content_vec.len() {
|
||||
let byte = content_vec[position];
|
||||
position += 1;
|
||||
|
||||
if byte == b'\n' {
|
||||
found_newline = true;
|
||||
break;
|
||||
}
|
||||
|
||||
// Handle \r\n sequence
|
||||
if byte == b'\r'
|
||||
&& position < content_vec.len()
|
||||
&& content_vec[position] == b'\n'
|
||||
{
|
||||
position += 1;
|
||||
found_newline = true;
|
||||
break;
|
||||
}
|
||||
|
||||
line.push(byte);
|
||||
}
|
||||
|
||||
file_userdata.set("__position", position)?;
|
||||
|
||||
if !found_newline
|
||||
&& line.is_empty()
|
||||
&& position >= content_vec.len()
|
||||
{
|
||||
return Ok(None); // EOF
|
||||
}
|
||||
|
||||
let result = String::from_utf8_lossy(&line).to_string();
|
||||
Ok(Some(result))
|
||||
} else {
|
||||
Err(mlua::Error::runtime(format!(
|
||||
"Unsupported read format: {}",
|
||||
format_str
|
||||
)))
|
||||
}
|
||||
}
|
||||
Some(_) => Err(mlua::Error::runtime("Invalid format")),
|
||||
None => {
|
||||
// Default is to read a line
|
||||
let mut line = Vec::new();
|
||||
let mut found_newline = false;
|
||||
|
||||
while position < content_vec.len() {
|
||||
let byte = content_vec[position];
|
||||
position += 1;
|
||||
|
||||
if byte == b'\n' {
|
||||
found_newline = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if byte == b'\r'
|
||||
&& position < content_vec.len()
|
||||
&& content_vec[position] == b'\n'
|
||||
{
|
||||
position += 1;
|
||||
found_newline = true;
|
||||
break;
|
||||
}
|
||||
|
||||
line.push(byte);
|
||||
}
|
||||
|
||||
file_userdata.set("__position", position)?;
|
||||
|
||||
if !found_newline && line.is_empty() && position >= content_vec.len() {
|
||||
return Ok(None); // EOF
|
||||
}
|
||||
|
||||
let result = String::from_utf8_lossy(&line).to_string();
|
||||
Ok(Some(result))
|
||||
}
|
||||
}
|
||||
},
|
||||
)?
|
||||
};
|
||||
file.set("read", read_fn)?;
|
||||
|
||||
// Implement close method
|
||||
let close_fn = lua.create_function(|_lua, _: mlua::Table| Ok(true))?;
|
||||
file.set("close", close_fn)?;
|
||||
|
||||
Ok((Some(file), String::new()))
|
||||
}
|
||||
|
||||
// Helper function to execute a single command
|
||||
fn execute_command(
|
||||
cmd: &ShellCmd,
|
||||
root_dir: &Arc<Path>,
|
||||
allowed_commands: &mut HashMap<String, bool>,
|
||||
) -> mlua::Result<String> {
|
||||
// Check if command is allowed
|
||||
if !allowed_commands.contains_key(&cmd.command) {
|
||||
// If it's the first time we see this command, ask for permission
|
||||
// In a real application, this would prompt the user, but for simplicity
|
||||
// we'll just allow all commands in this sample implementation
|
||||
allowed_commands.insert(cmd.command.clone(), true);
|
||||
}
|
||||
|
||||
if !allowed_commands[&cmd.command] {
|
||||
return Err(mlua::Error::runtime(format!(
|
||||
"Command '{}' is not allowed in this sandbox",
|
||||
cmd.command
|
||||
)));
|
||||
}
|
||||
|
||||
// Execute the command
|
||||
let mut command = Command::new(&cmd.command);
|
||||
|
||||
// Set the current directory
|
||||
command.current_dir(root_dir);
|
||||
|
||||
// Add arguments
|
||||
command.args(&cmd.args);
|
||||
|
||||
// Configure stdio
|
||||
command.stdin(Stdio::piped());
|
||||
command.stdout(Stdio::piped());
|
||||
command.stderr(Stdio::piped());
|
||||
|
||||
// Execute the command
|
||||
let output = command
|
||||
.output()
|
||||
.map_err(|e| mlua::Error::runtime(format!("Failed to execute command: {}", e)))?;
|
||||
|
||||
let mut result = String::new();
|
||||
|
||||
// Handle stdout
|
||||
if cmd.stdout_redirect.is_none() {
|
||||
result.push_str(&String::from_utf8_lossy(&output.stdout));
|
||||
} else {
|
||||
// Handle file redirection
|
||||
let redirect_path = root_dir.join(cmd.stdout_redirect.as_ref().unwrap());
|
||||
Self::write_to_file(&redirect_path, &output.stdout)
|
||||
.map_err(|e| mlua::Error::runtime(format!("Failed to redirect stdout: {}", e)))?;
|
||||
}
|
||||
|
||||
// Handle stderr
|
||||
if cmd.stderr_redirect.is_none() {
|
||||
// If stderr is not redirected, append it to the result
|
||||
result.push_str(&String::from_utf8_lossy(&output.stderr));
|
||||
} else {
|
||||
// Handle file redirection
|
||||
let redirect_path = root_dir.join(cmd.stderr_redirect.as_ref().unwrap());
|
||||
Self::write_to_file(&redirect_path, &output.stderr)
|
||||
.map_err(|e| mlua::Error::runtime(format!("Failed to redirect stderr: {}", e)))?;
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
// Helper function to write data to a file
|
||||
fn write_to_file(path: &Path, data: &[u8]) -> std::io::Result<()> {
|
||||
let mut file = File::create(path)?;
|
||||
std::io::Write::write_all(&mut file, data)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Helper function to execute an AST node
|
||||
fn execute_ast_node(
|
||||
node: ShellAst,
|
||||
root_dir: &Arc<Path>,
|
||||
allowed_commands: &mut HashMap<String, bool>,
|
||||
) -> mlua::Result<String> {
|
||||
match node {
|
||||
ShellAst::Command(cmd) => Self::execute_command(&cmd, root_dir, allowed_commands),
|
||||
ShellAst::Operation {
|
||||
operator,
|
||||
left,
|
||||
right,
|
||||
} => {
|
||||
let left_output = Self::execute_ast_node(*left, root_dir, allowed_commands)?;
|
||||
|
||||
match operator {
|
||||
Operator::Pipe => Self::execute_ast_node_with_input(
|
||||
*right,
|
||||
&left_output,
|
||||
root_dir,
|
||||
allowed_commands,
|
||||
),
|
||||
Operator::And => {
|
||||
if !left_output.trim().is_empty() {
|
||||
Self::execute_ast_node(*right, root_dir, allowed_commands)
|
||||
} else {
|
||||
Ok(left_output)
|
||||
}
|
||||
}
|
||||
Operator::Semicolon => {
|
||||
let mut result = left_output;
|
||||
let right_output =
|
||||
Self::execute_ast_node(*right, root_dir, allowed_commands)?;
|
||||
result.push_str(&right_output);
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to execute an AST node with input from a previous command
|
||||
fn execute_ast_node_with_input(
|
||||
node: ShellAst,
|
||||
input: &str,
|
||||
root_dir: &Arc<Path>,
|
||||
allowed_commands: &mut HashMap<String, bool>,
|
||||
) -> mlua::Result<String> {
|
||||
match node {
|
||||
ShellAst::Command(cmd) => {
|
||||
// Check if command is allowed
|
||||
if !allowed_commands.contains_key(&cmd.command) {
|
||||
allowed_commands.insert(cmd.command.clone(), true);
|
||||
}
|
||||
|
||||
if !allowed_commands[&cmd.command] {
|
||||
return Err(mlua::Error::runtime(format!(
|
||||
"Command '{}' is not allowed in this sandbox",
|
||||
cmd.command
|
||||
)));
|
||||
}
|
||||
|
||||
// Execute the command with input
|
||||
let mut command = Command::new(&cmd.command);
|
||||
command.current_dir(root_dir);
|
||||
command.args(&cmd.args);
|
||||
|
||||
// Configure stdio
|
||||
command.stdin(Stdio::piped());
|
||||
command.stdout(Stdio::piped());
|
||||
command.stderr(Stdio::piped());
|
||||
|
||||
let mut child = command.spawn().map_err(|e| {
|
||||
mlua::Error::runtime(format!("Failed to execute command: {}", e))
|
||||
})?;
|
||||
|
||||
// Write input to stdin
|
||||
if let Some(mut stdin) = child.stdin.take() {
|
||||
std::io::Write::write_all(&mut stdin, input.as_bytes()).map_err(|e| {
|
||||
mlua::Error::runtime(format!("Failed to write to stdin: {}", e))
|
||||
})?;
|
||||
// Stdin is closed when it goes out of scope
|
||||
}
|
||||
|
||||
let output = child.wait_with_output().map_err(|e| {
|
||||
mlua::Error::runtime(format!("Failed to wait for command: {}", e))
|
||||
})?;
|
||||
|
||||
let mut result = String::new();
|
||||
|
||||
// Handle stdout
|
||||
if cmd.stdout_redirect.is_none() {
|
||||
result.push_str(&String::from_utf8_lossy(&output.stdout));
|
||||
} else {
|
||||
// Handle file redirection
|
||||
let redirect_path = root_dir.join(cmd.stdout_redirect.as_ref().unwrap());
|
||||
Self::write_to_file(&redirect_path, &output.stdout).map_err(|e| {
|
||||
mlua::Error::runtime(format!("Failed to redirect stdout: {}", e))
|
||||
})?;
|
||||
}
|
||||
|
||||
// Handle stderr
|
||||
if cmd.stderr_redirect.is_none() {
|
||||
result.push_str(&String::from_utf8_lossy(&output.stderr));
|
||||
} else {
|
||||
// Handle file redirection
|
||||
let redirect_path = root_dir.join(cmd.stderr_redirect.as_ref().unwrap());
|
||||
Self::write_to_file(&redirect_path, &output.stderr).map_err(|e| {
|
||||
mlua::Error::runtime(format!("Failed to redirect stderr: {}", e))
|
||||
})?;
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
ShellAst::Operation { .. } => {
|
||||
// For complex operations, we'd need to create temporary files for intermediate results
|
||||
// For simplicity, we'll return an error for now
|
||||
Err(mlua::Error::runtime(
|
||||
"Nested operations in pipes are not supported",
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Sandboxed io.open() function in Lua.
|
||||
fn io_open(
|
||||
lua: &Lua,
|
||||
|
||||
@@ -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()));
|
||||
|
||||
@@ -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"]);
|
||||
}
|
||||
|
||||
|
||||
@@ -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