Compare commits

..

26 Commits

Author SHA1 Message Date
Richard Feldman
5324ef894e Use Pratt Parsing for shell commands 2025-03-08 00:42:59 -05:00
Richard Feldman
b1fca741a9 wip 2025-03-08 00:41:17 -05:00
Richard Feldman
fb51b99198 Make tests avoid the non-space options 2025-03-08 00:41:17 -05:00
Richard Feldman
cb79ee20c7 Make sandboxed_shell work except for operators without spaces 2025-03-08 00:41:17 -05:00
Richard Feldman
c5f0a5bb3e wip 2025-03-08 00:41:17 -05:00
Richard Feldman
38136cb0c0 Add sandboxed_shell 2025-03-08 00:41:17 -05:00
Danilo Leal
6ed6e8bc26 git: Refine diff hunk controls visuals (#26317)
You may need to zoom in hard to see this 😅 but the main addition of this
PR is just ensuring there's also horizontal border instead of just on
the bottom. Also added some box-shadow here to make it pop out of the
diff a bit more.

| Before | After |
|--------|--------|
| ![CleanShot 2025-03-08 at 12  37
40@2x](https://github.com/user-attachments/assets/98e0329e-646f-4455-89fa-3f6ec1211361)
| ![CleanShot 2025-03-08 at 12  36
40@2x](https://github.com/user-attachments/assets/7f667e65-5b72-4156-b0ec-2b162eb76f2f)
|

Release Notes:

- N/A
2025-03-08 01:00:47 -03:00
Max Brunsfeld
4846e6fb3a Fix performance bottlenecks when multi buffers have huge numbers of buffers (#26308)
This is motivated by trying to make the Project Diff view usable with
huge Git change sets.

Release Notes:

- Improved performance of rendering multibuffers with very large numbers
of buffers
2025-03-08 02:15:15 +00:00
Mikayla Maki
cb543f9546 Git UI papercuts (#26316)
Release Notes:

- Git Beta: added `git:Add` as an alias for the existing `git::Diff`
- Git Beta: Fixed a bug where the 'generate commit message' keybinding
wasn't working.
- Git Beta: Made the empty project diff state a little more helpful with
a button to push, and a button to close the item.
2025-03-08 01:49:06 +00:00
Michael Sloan
450d727a04 Fixes to excerpt movement actions and bindings + add multibuffer and singleton_buffer key contexts (#26264)
Closes #26002 

Release Notes:

- Added `multibuffer` key context.
- `cmd-down` and `cmd-shift-down` on Mac now moves to the end of the
last line of a singleton buffer instead of the beginning. In
multibuffers, these now move to the start of the next excerpt.
- Fixed `vim::PreviousSectionEnd` (bound to `[ ]`) to move to the
beginning of the line, matching the behavior of `vim::NextSectionEnd`.
- Added `editor::MoveToStartOfNextExcerpt` and
`editor::MoveToEndOfPreviousExcerpt`.
2025-03-08 00:58:47 +00:00
Mikayla Maki
60b3eb3f76 Add git branch switching aliases (#26315)
This gives us _very_ rudimentary support for `git switch` and `git
checkout` now, by making them aliases for our existing `git::branch`
call.

Release Notes:

- Git Beta: Added `git::Switch` and `git::CheckoutBranch` as aliases for
the existing `git::Branch`
2025-03-08 00:02:57 +00:00
Marshall Bowers
bbe7c9a738 assistant2: Factor out Thread::all_tools_finished method (#26314)
This PR factors out a new `Thread::all_tools_finished` method to
encapsulate some of the boilerplate in the `ThreadEvent::ToolFinished`
event handler.

This should make this event handler easier to replicate for the eval
use-case.

Release Notes:

- N/A
2025-03-07 23:35:32 +00:00
Mikayla Maki
f6345a6995 Improve when the commit suggestions would show (#26313)
Release Notes:

- Git Beta: Fixed a few bugs where the suggested commit text wouldn't
show in certain cases, or would update slowly.
2025-03-07 23:33:48 +00:00
Marshall Bowers
e70d0edfac assistant_tool: Pass an Entity<Project> to Tool::run (#26312)
This PR updates the `Tool::run` method to take an `Entity<Project>`
instead of a `WeakEntity<Project>`.

Release Notes:

- N/A
2025-03-07 23:30:56 +00:00
Marshall Bowers
921c24e274 assistant2: Add helper methods to Thread for dealing with tool use (#26310)
This PR adds two new helper methods to the `Thread` for dealing with
tool use:

- `use_pending_tools` - This uses all of the tools that are pending
- The reason we aren't calling this directly in `stream_completion` is
that we still might need to have a way for users to confirm that they
want tools to be run, which would need to happen at the UI layer in the
`ActiveThread`.
- `send_tool_results_to_model` - This encapsulates inserting a new user
message that contains the tool results and sending them up to the model.

Release Notes:

- N/A
2025-03-07 23:16:45 +00:00
Marshall Bowers
18f3f8097f assistant_tool: Decouple Tool from Workspace (#26309)
This PR decouples the `Tool` trait from the `Workspace` (and from the
UI, in general).

`Tool::run` now takes a `WeakEntity<Project>` instead of a
`WeakEntity<Workspace>` and a `Window`.

Release Notes:

- N/A
2025-03-07 22:41:56 +00:00
Marshall Bowers
4f6682c7fe haskell: Extract to zed-extensions/haskell repository (#26306)
This PR extracts the Haskell extension to the
[zed-extensions/haskell](https://github.com/zed-extensions/haskell)
repository.

Release Notes:

- N/A
2025-03-07 22:07:04 +00:00
Kiran_Peraka
f57dece2d5 git: Fix errors not showing in the toast notification (#26303)
Release Notes:

- Resolved an issue where error messages from Git were not being
displayed in toast notifications.
<img width="1702" alt="Screenshot 2025-03-08 at 1 11 30 AM"
src="https://github.com/user-attachments/assets/a46517db-4e64-4c5e-a64e-96e820ca9aec"
/>
2025-03-07 20:57:53 +00:00
Kirill Bulatov
103ad635d9 Refactor Completions to allow non-LSP ones better (#26300)
A preparation for https://github.com/zed-industries/zed/issues/4957 that
pushes all LSP-related data out from the basic completion item, so that
it's possible to create completion items without any trace of LSP
clearly.

Release Notes:

- N/A
2025-03-07 20:19:28 +00:00
Mikayla Maki
ec5e7a2653 Change the default staging and unstaging state display (#26299)
This adds a setting for the "border" hunk display mode, as discussed,
and makes it the default.

Here's how it looks in light mode:

<img width="1512" alt="Screenshot 2025-03-07 at 11 39 25 AM"
src="https://github.com/user-attachments/assets/a934faa3-ec69-47e1-ad46-535e48b98e9f"
/>

And dark mode: 

<img width="1511" alt="Screenshot 2025-03-07 at 11 39 56 AM"
src="https://github.com/user-attachments/assets/43c9afd1-22bb-4bd8-96ce-82702a6cbc80"
/>


Release Notes:

- Git Beta: Adjusted the default hunk styling for staged and unstaged
changes

Co-authored-by: Conrad <conrad@zed.dev>
Co-authored-by: Nate <nate@zed.dev>
2025-03-07 19:56:24 +00:00
Marshall Bowers
05d3ee8555 extension: Require that grammar names are written in snake_case (#26295)
This PR updates the `ExtensionBuilder` to require that grammar names are
written in snake_case.

The grammar names are used to construct identifiers, so we need them to
be valid C identifiers.

Release Notes:

- N/A
2025-03-07 19:02:35 +00:00
Nate Butler
1b34437839 component_preview: Add component pages (#26284)
This PR adds pages to component preview when clicking on a given
component in the sidebar.

This will let us create richer previews & better docs for using
components in the future.

Release Notes:

- N/A
2025-03-07 18:56:17 +00:00
Danilo Leal
3ff2c8fc38 Add file icon for Luau (#26293)
Closes https://github.com/zed-industries/zed/issues/14948

Release Notes:

- N/A
2025-03-07 15:27:00 -03:00
Cole Miller
b0b0b00fae worktree: Add some info-level logging about added and removed repository entries (#26291)
Trying to track down a user's reported issue with parent repositories
not getting picked up.

Release Notes:

- N/A
2025-03-07 18:02:05 +00:00
Conrad Irwin
80fb88520f Remove worktree and project notifies (#26244)
This reduces the number of multibuffer syncs from 100,000 to 20,000.
Before this change each editor individually observed the project, so
literally any project change was amplified by the number of editors you
had open.

Now editors listen to their buffers instead of the project, and other
users of `cx.observe` on the project have been updated to use specific
events to reduce churn.

Follow up to #26237


Release Notes:

- Improved performance of Zed in large repos with lots of file system
events.

---------

Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
2025-03-07 10:51:46 -07:00
Conrad Irwin
aef84d453a Update Linux Graphics troubleshooting (#26263)
Try to re-order the tips for clarity, and make it clear that /etc/prime
could be wrong either way around.

Also remove section on FIPS now we nolonger bundle openssl

Updates #15629

Release Notes:

- N/A
2025-03-07 09:57:11 -07:00
77 changed files with 2970 additions and 1461 deletions

37
Cargo.lock generated
View File

@@ -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"

View File

@@ -171,7 +171,6 @@ members = [
"extensions/emmet",
"extensions/glsl",
"extensions/haskell",
"extensions/html",
"extensions/perplexity",
"extensions/proto",

View 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

View File

@@ -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

View File

@@ -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,

View File

@@ -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",

View File

@@ -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.

View File

@@ -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);
})),

View File

@@ -38,7 +38,7 @@ use language_model::{
use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu};
use multi_buffer::MultiBufferRow;
use parking_lot::Mutex;
use project::{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()
})),

View File

@@ -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);
});
}
}

View File

@@ -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,
)

View File

@@ -27,7 +27,7 @@ use language::{Buffer, Point, Selection, TransactionId};
use language_model::{report_assistant_event, LanguageModelRegistry};
use multi_buffer::MultiBufferRow;
use parking_lot::Mutex;
use project::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()
})),

View File

@@ -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.

View File

@@ -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,
)
})
})
})
}

View File

@@ -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);
}

View File

@@ -5,9 +5,9 @@ use assistant_slash_command::{AfterCompletion, SlashCommandLine, SlashCommandWor
use editor::{CompletionProvider, Editor};
use fuzzy::{match_strings, StringMatchCandidate};
use gpui::{App, AppContext as _, Context, Entity, Task, WeakEntity, Window};
use language::{Anchor, Buffer, 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())

View File

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

View File

@@ -4,8 +4,8 @@ mod tool_working_set;
use std::sync::Arc;
use anyhow::Result;
use gpui::{App, 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>>;
}

View File

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

View File

@@ -1,11 +1,11 @@
use std::sync::Arc;
use anyhow::{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)]

View File

@@ -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) {

View File

@@ -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,

View File

@@ -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()

View File

@@ -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),

View File

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

View File

@@ -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(

View File

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

View File

@@ -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();

View File

@@ -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,

View File

@@ -6,11 +6,11 @@ use gpui::{
};
use language::Buffer;
use language::CodeLabel;
use lsp::LanguageServerId;
use markdown::Markdown;
use multi_buffer::{Anchor, ExcerptId};
use ordered_float::OrderedFloat;
use project::lsp_store::CompletionDocumentation;
use project::CompletionSource;
use project::{CodeAction, Completion, TaskSourceKind};
use std::{
@@ -233,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 {

View File

@@ -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,
}
}
}

View File

@@ -20,10 +20,10 @@ use crate::{
DisplayRow, DocumentHighlightRead, DocumentHighlightWrite, EditDisplayMode, Editor, EditorMode,
EditorSettings, EditorSnapshot, EditorStyle, ExpandExcerpts, FocusedBlock, GoToHunk,
GoToPreviousHunk, GutterDimensions, HalfPageDown, HalfPageUp, HandleInput, HoveredCursor,
InlayHintRefreshReason, InlineCompletion, JumpData, LineDown, 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| {

View File

@@ -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);

View File

@@ -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

View File

@@ -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()

View File

@@ -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)

View File

@@ -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,
},
}
}

View File

@@ -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,

View File

@@ -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);
})
}

View File

@@ -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,
},),
],
);

View File

@@ -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.),
}])
}
}
}

View File

@@ -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()))
}

View File

@@ -634,7 +634,7 @@ impl Display for ColorSpace {
}
/// A background color, which can be either a solid color or a linear gradient.
#[derive(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 {

View File

@@ -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,

View File

@@ -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;

View File

@@ -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();
}

View File

@@ -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)

View File

@@ -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;
}

View File

@@ -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,
})
}
}
}
}

View File

@@ -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
}
}

View File

@@ -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)]

View File

@@ -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 {

View File

@@ -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"] }

View File

@@ -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

View 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");
}
}
}

View File

@@ -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;

View File

@@ -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,

View File

@@ -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"),

View File

@@ -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()));

View File

@@ -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();

View File

@@ -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;

View File

@@ -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(_) => {

View File

@@ -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;

View File

@@ -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]

View File

@@ -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"]);
}

View File

@@ -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)

View File

@@ -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.

View File

@@ -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"

View File

@@ -1 +0,0 @@
../../LICENSE-APACHE

View File

@@ -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"

View File

@@ -1,3 +0,0 @@
("(" @open ")" @close)
("[" @open "]" @close)
("{" @open "}" @close)

View File

@@ -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 },
]

View File

@@ -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

View File

@@ -1,3 +0,0 @@
(_ "[" "]" @end) @indent
(_ "{" "}" @end) @indent
(_ "(" ")" @end) @indent

View File

@@ -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

View File

@@ -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)

View File

@@ -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);