Compare commits

...

15 Commits

Author SHA1 Message Date
Nathan Sobo
a51fa34e21 Try to merge main 2024-08-07 20:00:24 -06:00
Piotr Osiewicz
7517971013 Activate workflow step before running a preview for it 2024-08-08 01:02:47 +02:00
Piotr Osiewicz
84b1a5c259 Use animated icon for resolution step 2024-08-08 01:00:47 +02:00
Piotr Osiewicz
47d45ee25f Make workflow edit border not take up a full width of the element 2024-08-08 00:30:58 +02:00
Piotr Osiewicz
a4f5d6e0b7 Add TintColor::Positive style 2024-08-07 23:50:29 +02:00
Piotr Osiewicz
3f17c5b13b Assign unique ElementIds to the separate elements 2024-08-07 23:37:41 +02:00
Piotr Osiewicz
9592a829f5 💄 pending and confirmed states 2024-08-07 23:22:21 +02:00
Piotr Osiewicz
a9c6ef160d Add edit reverting post confirmation 2024-08-07 20:28:44 +02:00
Piotr Osiewicz
ccc9c98bc5 Allow accepting/rejecting edit steps 2024-08-07 19:39:30 +02:00
Piotr Osiewicz
43dc1e7ac7 Checkpoint. Start working on header buttons 2024-08-07 19:26:11 +02:00
Antonio Scandurra
85d884ef1d Checkpoint 2024-08-07 18:04:39 +02:00
Antonio Scandurra
f65a2b5e18 Refactor workflow step status handling for improved reactivity
This commit introduces a more reactive approach to handling workflow step statuses:

- Remove the static `status` field from `WorkflowStep` struct
- Add a `status` method to `WorkflowStep` that dynamically computes the status
- Implement `Display` for `WorkflowStepStatus` for easier rendering
- Update `ContextEditor` to use the new dynamic status system
- Add status observation to `WorkflowStepAssists` for real-time updates
- Refactor `InlineAssistant` to use `assist_observations` for consistency

Co-Authored-By: Piotr <piotr@zed.dev>
2024-08-07 17:46:16 +02:00
Antonio Scandurra
2f39930e58 WIP: start on subscribing to assist status
My plan is to have the `ContextEditor` subscribe to the status of
each assist id, so that we can maintain an overall `WorkflowStepStatus`,
which we can then display in the header block decoration.
2024-08-07 14:54:22 +02:00
Antonio Scandurra
9f567f497d Start on displaying headers/footers for steps 2024-08-07 14:14:27 +02:00
Antonio Scandurra
9f6da06170 Avoid re-openin step transformation if it had already been applied 2024-08-07 10:46:15 +02:00
51 changed files with 1318 additions and 381 deletions

View File

@@ -231,20 +231,20 @@ jobs:
mv target/x86_64-apple-darwin/release/Zed.dmg target/x86_64-apple-darwin/release/Zed-x86_64.dmg
- name: Upload app bundle (universal) to workflow run if main branch or specific label
uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4
if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
with:
name: Zed_${{ github.event.pull_request.head.sha || github.sha }}.dmg
path: target/release/Zed.dmg
- name: Upload app bundle (aarch64) to workflow run if main branch or specific label
uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4
if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
with:
name: Zed_${{ github.event.pull_request.head.sha || github.sha }}-aarch64.dmg
path: target/aarch64-apple-darwin/release/Zed-aarch64.dmg
- name: Upload app bundle (x86_64) to workflow run if main branch or specific label
uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4
if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
with:
name: Zed_${{ github.event.pull_request.head.sha || github.sha }}-x86_64.dmg
@@ -319,7 +319,7 @@ jobs:
run: script/bundle-linux
- name: Upload Linux bundle to workflow run if main branch or specific label
uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4
if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
with:
name: zed-${{ github.event.pull_request.head.sha || github.sha }}-x86_64-unknown-linux-gnu.tar.gz
@@ -403,7 +403,7 @@ jobs:
run: script/bundle-linux
- name: Upload Linux bundle to workflow run if main branch or specific label
uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4
if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
with:
name: zed-${{ github.event.pull_request.head.sha || github.sha }}-aarch64-unknown-linux-gnu.tar.gz

7
Cargo.lock generated
View File

@@ -2504,6 +2504,7 @@ dependencies = [
"settings",
"sha2",
"sqlx",
"strum",
"subtle",
"supermaven_api",
"telemetry_events",
@@ -13775,7 +13776,7 @@ dependencies = [
[[package]]
name = "zed"
version = "0.148.0"
version = "0.149.0"
dependencies = [
"activity_indicator",
"anyhow",
@@ -13993,7 +13994,7 @@ dependencies = [
[[package]]
name = "zed_html"
version = "0.1.1"
version = "0.1.2"
dependencies = [
"zed_extension_api 0.0.6",
]
@@ -14014,7 +14015,7 @@ dependencies = [
[[package]]
name = "zed_php"
version = "0.1.2"
version = "0.1.3"
dependencies = [
"zed_extension_api 0.0.6",
]

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-text-search"><path d="M21 6H3"/><path d="M10 12H3"/><path d="M10 18H3"/><circle cx="17" cy="15" r="3"/><path d="m21 19-1.9-1.9"/></svg>

After

Width:  |  Height:  |  Size: 338 B

1
assets/icons/undo.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-undo"><path d="M3 7v6h6"/><path d="M21 17a9 9 0 0 0-9-9 9 9 0 0 0-6 2.3L3 13"/></svg>

After

Width:  |  Height:  |  Size: 288 B

View File

@@ -1,4 +1,4 @@
Your task is to map a step from the conversation above to operations on symbols inside the provided source files.
Your task is to map a step from the conversation above to suggestions on symbols inside the provided source files.
Guidelines:
- There's no need to describe *what* to do, just *where* to do it.
@@ -6,13 +6,13 @@ Guidelines:
- Don't create and then update a file.
- We'll create it in one shot.
- Prefer updating symbols lower in the syntax tree if possible.
- Never include operations on a parent symbol and one of its children in the same operations block.
- Never nest an operation with another operation or include CDATA or other content. All operations are leaf nodes.
- Never include suggestions on a parent symbol and one of its children in the same suggestions block.
- Never nest an operation with another operation or include CDATA or other content. All suggestions are leaf nodes.
- Include a description attribute for each operation with a brief, one-line description of the change to perform.
- Descriptions are required for all operations except delete.
- When generating multiple operations, ensure the descriptions are specific to each individual operation.
- Descriptions are required for all suggestions except delete.
- When generating multiple suggestions, ensure the descriptions are specific to each individual operation.
- Avoid referring to the location in the description. Focus on the change to be made, not the location where it's made. That's implicit with the symbol you provide.
- Don't generate multiple operations at the same location. Instead, combine them together in a single operation with a succinct combined description.
- Don't generate multiple suggestions at the same location. Instead, combine them together in a single operation with a succinct combined description.
Example 1:
@@ -33,12 +33,12 @@ impl Rectangle {
<step>Add new methods 'calculate_area' and 'calculate_perimeter' to the Rectangle struct</step>
<step>Implement the 'Display' trait for the Rectangle struct</step>
What are the operations for the step: <step>Add a new method 'calculate_area' to the Rectangle struct</step>
What are the suggestions for the step: <step>Add a new method 'calculate_area' to the Rectangle struct</step>
A (wrong):
{
"title": "Add Rectangle methods",
"operations": [
"suggestions": [
{
"kind": "AppendChild",
"path": "src/shapes.rs",
@@ -59,7 +59,7 @@ This demonstrates what NOT to do. NEVER append multiple children at the same loc
A (corrected):
{
"title": "Add Rectangle methods",
"operations": [
"suggestions": [
{
"kind": "AppendChild",
"path": "src/shapes.rs",
@@ -70,12 +70,12 @@ A (corrected):
}
User:
What are the operations for the step: <step>Implement the 'Display' trait for the Rectangle struct</step>
What are the suggestions for the step: <step>Implement the 'Display' trait for the Rectangle struct</step>
A:
{
"title": "Implement Display for Rectangle",
"operations": [
"suggestions": [
{
"kind": "InsertSiblingAfter",
"path": "src/shapes.rs",
@@ -109,12 +109,12 @@ impl User {
<step>Update the 'print_info' method to use formatted output</step>
<step>Remove the 'email' field from the User struct</step>
What are the operations for the step: <step>Update the 'print_info' method to use formatted output</step>
What are the suggestions for the step: <step>Update the 'print_info' method to use formatted output</step>
A:
{
"title": "Use formatted output",
"operations": [
"suggestions": [
{
"kind": "Update",
"path": "src/user.rs",
@@ -125,12 +125,12 @@ A:
}
User:
What are the operations for the step: <step>Remove the 'email' field from the User struct</step>
What are the suggestions for the step: <step>Remove the 'email' field from the User struct</step>
A:
{
"title": "Remove email field",
"operations": [
"suggestions": [
{
"kind": "Delete",
"path": "src/user.rs",
@@ -163,12 +163,12 @@ impl Vehicle {
<step>Add a 'use std::fmt;' statement at the beginning of the file</step>
<step>Add a new method 'start_engine' in the Vehicle impl block</step>
What are the operations for the step: <step>Add a 'use std::fmt;' statement at the beginning of the file</step>
What are the suggestions for the step: <step>Add a 'use std::fmt;' statement at the beginning of the file</step>
A:
{
"title": "Add use std::fmt statement",
"operations": [
"suggestions": [
{
"kind": "PrependChild",
"path": "src/vehicle.rs",
@@ -178,12 +178,12 @@ A:
}
User:
What are the operations for the step: <step>Add a new method 'start_engine' in the Vehicle impl block</step>
What are the suggestions for the step: <step>Add a new method 'start_engine' in the Vehicle impl block</step>
A:
{
"title": "Add start_engine method",
"operations": [
"suggestions": [
{
"kind": "InsertSiblingAfter",
"path": "src/vehicle.rs",
@@ -222,12 +222,12 @@ impl Employee {
<step>Make salary an f32</step>
What are the operations for the step: <step>Make salary an f32</step>
What are the suggestions for the step: <step>Make salary an f32</step>
A (wrong):
{
"title": "Change salary to f32",
"operations": [
"suggestions": [
{
"kind": "Update",
"path": "src/employee.rs",
@@ -248,7 +248,7 @@ This example demonstrates what not to do. `struct Employee salary` is a child of
A (corrected):
{
"title": "Change salary to f32",
"operations": [
"suggestions": [
{
"kind": "Update",
"path": "src/employee.rs",
@@ -259,12 +259,12 @@ A (corrected):
}
User:
What are the correct operations for the step: <step>Remove the 'department' field and update the 'print_details' method</step>
What are the correct suggestions for the step: <step>Remove the 'department' field and update the 'print_details' method</step>
A:
{
"title": "Remove department",
"operations": [
"suggestions": [
{
"kind": "Delete",
"path": "src/employee.rs",
@@ -311,7 +311,7 @@ impl Game {
A:
{
"title": "Add level field to Player",
"operations": [
"suggestions": [
{
"kind": "InsertSiblingAfter",
"path": "src/game.rs",
@@ -349,7 +349,7 @@ impl Config {
A:
{
"title": "Add load_from_file method",
"operations": [
"suggestions": [
{
"kind": "PrependChild",
"path": "src/config.rs",
@@ -389,7 +389,7 @@ impl Database {
A:
{
"title": "Add error handling to query",
"operations": [
"suggestions": [
{
"kind": "PrependChild",
"path": "src/database.rs",
@@ -410,4 +410,4 @@ A:
]
}
Now generate the operations for the following step:
Now generate the suggestions for the following step:

View File

@@ -9,12 +9,12 @@ use crate::{
SlashCommandCompletionProvider, SlashCommandRegistry,
},
terminal_inline_assistant::TerminalInlineAssistant,
Assist, CodegenStatus, ConfirmCommand, Context, ContextEvent, ContextId, ContextStore,
CycleMessageRole, DebugEditSteps, DeployHistory, DeployPromptLibrary, EditSuggestionGroup,
InlineAssist, InlineAssistId, InlineAssistant, InsertIntoEditor, MessageStatus, ModelSelector,
PendingSlashCommand, PendingSlashCommandStatus, QuoteSelection, RemoteContextMetadata,
ResolvedWorkflowStepEditSuggestions, SavedContextMetadata, Split, ToggleFocus,
ToggleModelSelector, WorkflowStepEditSuggestions,
Assist, ConfirmCommand, Context, ContextEvent, ContextId, ContextStore, CycleMessageRole,
DebugEditSteps, DeployHistory, DeployPromptLibrary, InlineAssist, InlineAssistId,
InlineAssistant, InsertIntoEditor, MessageStatus, ModelSelector, PendingSlashCommand,
PendingSlashCommandStatus, QuoteSelection, RemoteContextMetadata, ResolvedWorkflowStep,
SavedContextMetadata, Split, ToggleFocus, ToggleModelSelector, WorkflowStepStatus,
WorkflowSuggestionGroup,
};
use crate::{ContextStoreEvent, ShowConfiguration};
use anyhow::{anyhow, Result};
@@ -24,8 +24,8 @@ use collections::{BTreeSet, HashMap, HashSet};
use editor::{
actions::{FoldAt, MoveToEndOfLine, Newline, ShowCompletions, UnfoldAt},
display_map::{
BlockDisposition, BlockProperties, BlockStyle, Crease, CustomBlockId, RenderBlock,
ToDisplayPoint,
BlockContext, BlockDisposition, BlockProperties, BlockStyle, Crease, CustomBlockId,
RenderBlock, ToDisplayPoint,
},
scroll::{Autoscroll, AutoscrollStrategy, ScrollAnchor},
Anchor, Editor, EditorEvent, ExcerptRange, MultiBuffer, RowExt, ToOffset as _, ToPoint,
@@ -34,10 +34,11 @@ use editor::{display_map::CreaseId, FoldPlaceholder};
use fs::Fs;
use gpui::{
div, percentage, point, Action, Animation, AnimationExt, AnyElement, AnyView, AppContext,
AsyncWindowContext, ClipboardItem, Context as _, DismissEvent, Empty, Entity, EventEmitter,
FocusHandle, FocusableView, FontWeight, InteractiveElement, IntoElement, Model, ParentElement,
Pixels, ReadGlobal, Render, SharedString, StatefulInteractiveElement, Styled, Subscription,
Task, Transformation, UpdateGlobal, View, ViewContext, VisualContext, WeakView, WindowContext,
AsyncWindowContext, ClipboardItem, Context as _, DismissEvent, Empty, Entity, EntityId,
EventEmitter, FocusHandle, FocusableView, FontWeight, InteractiveElement, IntoElement, Model,
ParentElement, Pixels, ReadGlobal, Render, SharedString, StatefulInteractiveElement, Styled,
Subscription, Task, Transformation, UpdateGlobal, View, ViewContext, VisualContext, WeakView,
WindowContext,
};
use indexed_docs::IndexedDocsStore;
use language::{
@@ -64,13 +65,14 @@ use std::{
time::Duration,
};
use terminal_view::{terminal_panel::TerminalPanel, TerminalView};
use ui::TintColor;
use text::OffsetRangeExt;
use ui::{
prelude::*,
utils::{format_distance_from_now, DateTimeType},
Avatar, AvatarShape, ButtonLike, ContextMenu, Disclosure, ElevationIndex, KeyBinding, ListItem,
ListItemSpacing, PopoverMenu, PopoverMenuHandle, Tooltip,
};
use ui::{IconButtonShape, TintColor};
use util::ResultExt;
use workspace::{
dock::{DockPosition, Panel, PanelEvent},
@@ -1304,15 +1306,186 @@ struct ScrollPosition {
cursor: Anchor,
}
struct StepAssists {
struct WorkflowStep {
header_block_id: CustomBlockId,
footer_block_id: CustomBlockId,
suggestions: Option<ResolvedWorkflowStep>,
assists: Option<WorkflowStepAssists>,
}
impl WorkflowStep {
fn status(&self, cx: &AppContext) -> StepStatus {
if self.suggestions.is_none() {
StepStatus::ResolvingSuggestions
} else if let Some(assists) = self.assists.as_ref() {
let assistant = InlineAssistant::global(cx);
if assists
.assist_ids
.iter()
.any(|assist_id| assistant.assist_status(*assist_id, cx).is_pending())
{
StepStatus::Pending
} else if assists
.assist_ids
.iter()
.all(|assist_id| assistant.assist_status(*assist_id, cx).is_confirmed())
{
StepStatus::Confirmed
} else if assists
.assist_ids
.iter()
.all(|assist_id| assistant.assist_status(*assist_id, cx).is_done())
{
StepStatus::Done
} else {
StepStatus::Idle
}
} else {
StepStatus::Idle
}
}
}
struct WorkflowStepAssists {
assist_ids: Vec<InlineAssistId>,
editor: WeakView<Editor>,
_observe_assist_status: Task<()>,
}
enum StepStatus {
ResolvingSuggestions, // => Show "Resolving Step..." (or some UI that makes this status clear)
Idle, // => Show a "Transform" icon button (1 sparkle) like the one we have for inline assistant
Pending, // => Show a "Stop" icon button like the one we have for inline assistant
Done, // => Show a "Cancel" button and a "Confirm" icon button. The former undoes the assists, the latter confirms them all
Confirmed, // => Change the color of the borders to be muted, and maybe have a label above the step that says "Applied".
}
impl StepStatus {
pub(crate) fn is_confirmed(&self) -> bool {
matches!(self, Self::Confirmed)
}
pub(crate) fn into_element(
&self,
step_range: Range<language::Anchor>,
editor: WeakView<ContextEditor>,
cx: &mut BlockContext<'_, '_>,
) -> AnyElement {
let id = EntityId::from(cx.block_id);
match self {
StepStatus::ResolvingSuggestions => div()
.id(("resolving-suggestion-container", id))
.child(Icon::new(IconName::ArrowCircle).with_animation(
("resolving-suggestion-label", id),
Animation::new(Duration::from_secs(2)).repeat(),
|icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
))
.tooltip(|cx| Tooltip::text("Resolving steps...", cx))
.into_any_element(),
StepStatus::Idle => Button::new(("transform-workflow-step", id), "Preview")
.icon(IconName::Sparkle)
.icon_position(IconPosition::Start)
.icon_size(IconSize::Small)
.style(ButtonStyle::Tinted(TintColor::Accent))
.tooltip(|cx| Tooltip::text("Preview changes", cx))
.on_click({
let editor = editor.clone();
let step_range = step_range.clone();
move |_, cx| {
editor
.update(cx, |this, cx| {
this.activate_workflow_step(step_range.clone(), cx);
this.apply_edit_step(&step_range, cx)
})
.ok();
}
})
.into_any_element(),
StepStatus::Pending => h_flex()
.gap_1()
.child(div().child(Label::new("Applying...")).cursor_not_allowed())
.child(
IconButton::new(("stop-workflow-step", id), IconName::Stop)
.style(ButtonStyle::Tinted(TintColor::Negative))
.tooltip(|cx| Tooltip::text("Stop step execution", cx))
.on_click({
let editor = editor.clone();
let step_range = step_range.clone();
move |_, cx| {
editor
.update(cx, |this, cx| this.stop_edit_step(&step_range, cx))
.ok();
}
}),
)
.into_any_element(),
StepStatus::Done => h_flex()
.gap_1()
.child(
IconButton::new(("reject-workflow-step", id), IconName::Close)
.shape(IconButtonShape::Square)
.style(ButtonStyle::Tinted(TintColor::Negative))
.tooltip(|cx| Tooltip::text("Reject Step", cx))
.on_click({
let editor = editor.clone();
let step_range = step_range.clone();
move |_, cx| {
editor
.update(cx, |this, cx| {
this.confirm_edit_step(&step_range, true, cx);
})
.ok();
}
}),
)
.child(
IconButton::new(("confirm-workflow-step", id), IconName::Check)
.shape(IconButtonShape::Square)
.style(ButtonStyle::Tinted(TintColor::Positive))
.tooltip(|cx| Tooltip::text("Confirm Step", cx))
.on_click({
let editor = editor.clone();
let step_range = step_range.clone();
move |_, cx| {
editor
.update(cx, |this, cx| {
this.confirm_edit_step(&step_range, false, cx);
})
.ok();
}
}),
)
.into_any_element(),
StepStatus::Confirmed => h_flex()
.child(
Button::new(("revert-workflow-step", id), "Undo")
.style(ButtonStyle::Filled)
.icon(Some(IconName::Undo))
.icon_position(IconPosition::Start)
.icon_size(IconSize::Small)
.tooltip(|cx| Tooltip::text("Undo Step", cx))
.on_click({
let editor = editor.clone();
let step_range = step_range.clone();
move |_, cx| {
editor
.update(cx, |this, cx| {
this.undo_edit_step(&step_range, cx);
})
.ok();
}
}),
)
.into_any_element(),
}
}
}
#[derive(Debug, Eq, PartialEq)]
struct ActiveWorkflowStep {
range: Range<language::Anchor>,
suggestions: Option<ResolvedWorkflowStepEditSuggestions>,
suggestions_resolved: bool,
}
pub struct ContextEditor {
@@ -1328,7 +1501,7 @@ pub struct ContextEditor {
pending_slash_command_creases: HashMap<Range<language::Anchor>, CreaseId>,
pending_slash_command_blocks: HashMap<Range<language::Anchor>, CustomBlockId>,
_subscriptions: Vec<Subscription>,
assists_by_step: HashMap<Range<language::Anchor>, StepAssists>,
workflow_steps: HashMap<Range<language::Anchor>, WorkflowStep>,
active_workflow_step: Option<ActiveWorkflowStep>,
assistant_panel: WeakView<AssistantPanel>,
error_message: Option<SharedString>,
@@ -1387,7 +1560,7 @@ impl ContextEditor {
pending_slash_command_creases: HashMap::default(),
pending_slash_command_blocks: HashMap::default(),
_subscriptions,
assists_by_step: HashMap::default(),
workflow_steps: HashMap::default(),
active_workflow_step: None,
assistant_panel,
error_message: None,
@@ -1423,16 +1596,20 @@ impl ContextEditor {
}
fn assist(&mut self, _: &Assist, cx: &mut ViewContext<Self>) {
if !self.apply_edit_step(cx) {
if !self.apply_active_edit_step(cx) {
self.error_message = None;
self.send_to_model(cx);
cx.notify();
}
}
fn apply_edit_step(&mut self, cx: &mut ViewContext<Self>) -> bool {
if let Some(step) = self.active_workflow_step.as_ref() {
if let Some(assists) = self.assists_by_step.get(&step.range) {
fn apply_edit_step(
&mut self,
range: &Range<language::Anchor>,
cx: &mut ViewContext<Self>,
) -> bool {
if let Some(workflow_step) = self.workflow_steps.get(range) {
if let Some(assists) = workflow_step.assists.as_ref() {
let assist_ids = assists.assist_ids.clone();
cx.window_context().defer(|cx| {
InlineAssistant::update_global(cx, |assistant, cx| {
@@ -1451,6 +1628,91 @@ impl ContextEditor {
}
}
fn apply_active_edit_step(&mut self, cx: &mut ViewContext<Self>) -> bool {
let Some(step) = self.active_workflow_step.as_ref() else {
return false;
};
let range = step.range.clone();
self.apply_edit_step(&range, cx)
}
fn stop_edit_step(
&mut self,
range: &Range<language::Anchor>,
cx: &mut ViewContext<Self>,
) -> bool {
if let Some(workflow_step) = self.workflow_steps.get(range) {
if let Some(assists) = workflow_step.assists.as_ref() {
let assist_ids = assists.assist_ids.clone();
cx.window_context().defer(|cx| {
InlineAssistant::update_global(cx, |assistant, cx| {
for assist_id in assist_ids {
assistant.stop_assist(assist_id, cx);
}
})
});
!assists.assist_ids.is_empty()
} else {
false
}
} else {
false
}
}
fn undo_edit_step(
&mut self,
range: &Range<language::Anchor>,
cx: &mut ViewContext<Self>,
) -> bool {
if let Some(workflow_step) = self.workflow_steps.get(range) {
if let Some(assists) = workflow_step.assists.as_ref() {
let assist_ids = assists.assist_ids.clone();
cx.window_context().defer(|cx| {
InlineAssistant::update_global(cx, |assistant, cx| {
for assist_id in assist_ids {
assistant.undo_assist(assist_id, cx);
}
})
});
!assists.assist_ids.is_empty()
} else {
false
}
} else {
false
}
}
fn confirm_edit_step(
&mut self,
range: &Range<language::Anchor>,
undo: bool,
cx: &mut ViewContext<Self>,
) -> bool {
if let Some(workflow_step) = self.workflow_steps.get(range) {
if let Some(assists) = workflow_step.assists.as_ref() {
let assist_ids = assists.assist_ids.clone();
cx.window_context().defer(move |cx| {
InlineAssistant::update_global(cx, |assistant, cx| {
for assist_id in assist_ids {
assistant.finish_assist(assist_id, undo, cx);
}
})
});
!assists.assist_ids.is_empty()
} else {
false
}
} else {
false
}
}
fn send_to_model(&mut self, cx: &mut ViewContext<Self>) {
if let Some(user_message) = self.context.update(cx, |context, cx| context.assist(cx)) {
let new_selection = {
@@ -1493,16 +1755,13 @@ impl ContextEditor {
.text_for_range(step.tagged_range.clone())
.collect::<String>()
));
match &step.edit_suggestions {
WorkflowStepEditSuggestions::Resolved(ResolvedWorkflowStepEditSuggestions {
title,
edit_suggestions,
}) => {
match &step.status {
WorkflowStepStatus::Resolved(ResolvedWorkflowStep { title, suggestions }) => {
output.push_str("Resolution:\n");
output.push_str(&format!(" {:?}\n", title));
output.push_str(&format!(" {:?}\n", edit_suggestions));
output.push_str(&format!(" {:?}\n", suggestions));
}
WorkflowStepEditSuggestions::Pending(_) => {
WorkflowStepStatus::Pending(_) => {
output.push_str("Resolution: Pending\n");
}
}
@@ -1649,8 +1908,11 @@ impl ContextEditor {
context.save(Some(Duration::from_millis(500)), self.fs.clone(), cx);
});
}
ContextEvent::WorkflowStepsChanged => {
self.update_active_workflow_step(cx);
ContextEvent::WorkflowStepsChanged {
updated: inserted,
removed,
} => {
self.update_workflow_steps(inserted, removed, cx);
cx.notify();
}
ContextEvent::SummaryChanged => {
@@ -1922,27 +2184,156 @@ impl ContextEditor {
cx.emit(event.clone());
}
fn update_workflow_steps(
&mut self,
updated_steps: &[Range<language::Anchor>],
removed_steps: &[Range<language::Anchor>],
cx: &mut ViewContext<Self>,
) {
let mut blocks_to_remove = HashSet::default();
for range in removed_steps {
self.cancel_workflow_step_if_idle(range.clone(), cx);
if let Some(step) = self.workflow_steps.remove(range) {
blocks_to_remove.insert(step.header_block_id);
blocks_to_remove.insert(step.footer_block_id);
}
}
self.editor.update(cx, |editor, cx| {
editor.remove_blocks(blocks_to_remove, None, cx)
});
let buffer_snapshot = self.editor.read(cx).buffer().read(cx).snapshot(cx);
let (&excerpt_id, _, _) = buffer_snapshot.as_singleton().unwrap();
for range in updated_steps {
let Some(step) = self.context.read(cx).workflow_step_for_range(range.clone()) else {
continue;
};
if let Some(existing_step) = self.workflow_steps.get_mut(range) {
if existing_step.suggestions.is_none() {
existing_step.suggestions = step.status.as_resolved().cloned();
}
} else {
let suggestions = step.status.as_resolved().cloned();
let start = buffer_snapshot
.anchor_in_excerpt(excerpt_id, range.start)
.unwrap();
let end = buffer_snapshot
.anchor_in_excerpt(excerpt_id, range.end)
.unwrap();
let weak_self = cx.view().downgrade();
let block_ids = self.editor.update(cx, |editor, cx| {
let step_range = range.clone();
editor.insert_blocks(
vec![
BlockProperties {
position: start,
height: 1,
style: BlockStyle::Sticky,
render: Box::new({
let weak_self = weak_self.clone();
let step_range = step_range.clone();
move |cx| {
let current_status = weak_self
.update(&mut **cx, |context_editor, cx| {
let step = context_editor
.workflow_steps
.get(&step_range)?;
Some(step.status(cx))
})
.ok()
.flatten();
let theme = cx.theme().status();
let border_color = if current_status
.as_ref()
.map_or(false, |status| status.is_confirmed())
{
theme.ignored_border
} else {
theme.info_border
};
h_flex()
.h(1. * cx.line_height())
.w(DefiniteLength::Fraction(0.95))
.mx_auto()
.border_b_1()
.border_color(border_color)
.justify_end()
.gap_2()
.children(current_status.as_ref().map(|status| {
status.into_element(
step_range.clone(),
weak_self.clone(),
cx,
)
}))
.into_any()
}
}),
disposition: BlockDisposition::Above,
},
BlockProperties {
position: end,
height: 0,
style: BlockStyle::Sticky,
render: Box::new(move |cx| {
let current_status = weak_self
.update(&mut **cx, |context_editor, cx| {
let step =
context_editor.workflow_steps.get(&step_range)?;
Some(step.status(cx))
})
.ok()
.flatten();
let theme = cx.theme().status();
let border_color = if current_status
.as_ref()
.map_or(false, |status| status.is_confirmed())
{
theme.ignored_border
} else {
theme.info_border
};
v_flex()
.h_full()
.w(DefiniteLength::Fraction(0.95))
.mx_auto()
.border_t_1()
.border_color(border_color)
.into_any_element()
}),
disposition: BlockDisposition::Below,
},
],
None,
cx,
)
});
self.workflow_steps.insert(
range.clone(),
WorkflowStep {
header_block_id: block_ids[0],
footer_block_id: block_ids[1],
suggestions,
assists: None,
},
);
}
}
self.update_active_workflow_step(cx);
}
fn update_active_workflow_step(&mut self, cx: &mut ViewContext<Self>) {
let new_step = self
.workflow_step_range_for_cursor(cx)
.as_ref()
.and_then(|step_range| {
let workflow_step = self
.context
.read(cx)
.workflow_step_for_range(step_range.clone())?;
Some(ActiveWorkflowStep {
range: workflow_step.tagged_range.clone(),
suggestions: workflow_step.edit_suggestions.as_resolved().cloned(),
})
});
let new_step = self.active_workflow_step_for_cursor(cx);
if new_step.as_ref() != self.active_workflow_step.as_ref() {
if let Some(old_step) = self.active_workflow_step.take() {
self.cancel_workflow_step_if_idle(old_step.range, cx);
}
if let Some(new_step) = new_step {
self.activate_workflow_step(new_step, cx);
self.activate_workflow_step(new_step.range, cx);
}
}
}
@@ -1952,28 +2343,24 @@ impl ContextEditor {
step_range: Range<language::Anchor>,
cx: &mut ViewContext<Self>,
) {
let Some(step_assists) = self.assists_by_step.get_mut(&step_range) else {
let Some(step) = self.workflow_steps.get_mut(&step_range) else {
return;
};
let Some(editor) = step_assists.editor.upgrade() else {
self.assists_by_step.remove(&step_range);
let Some(assists) = step.assists.as_ref() else {
return;
};
let Some(editor) = assists.editor.upgrade() else {
return;
};
InlineAssistant::update_global(cx, |assistant, cx| {
step_assists.assist_ids.retain(|assist_id| {
match assistant.status_for_assist(*assist_id, cx) {
Some(CodegenStatus::Idle) | None => {
assistant.finish_assist(*assist_id, true, cx);
false
}
_ => true,
if matches!(step.status(cx), StepStatus::Idle) {
let assist_ids = step.assists.take().unwrap().assist_ids;
InlineAssistant::update_global(cx, |assistant, cx| {
for assist_id in assist_ids {
assistant.finish_assist(assist_id, true, cx)
}
});
});
if step_assists.assist_ids.is_empty() {
self.assists_by_step.remove(&step_range);
self.workspace
.update(cx, |workspace, cx| {
if let Some(pane) = workspace.pane_for(&editor) {
@@ -1990,68 +2377,121 @@ impl ContextEditor {
}
}
fn activate_workflow_step(&mut self, step: ActiveWorkflowStep, cx: &mut ViewContext<Self>) {
if let Some(step_assists) = self.assists_by_step.get(&step.range) {
if let Some(editor) = step_assists.editor.upgrade() {
for assist_id in &step_assists.assist_ids {
match InlineAssistant::global(cx).status_for_assist(*assist_id, cx) {
Some(CodegenStatus::Idle) | None => {}
_ => {
self.workspace
.update(cx, |workspace, cx| {
workspace.activate_item(&editor, false, false, cx);
})
.ok();
InlineAssistant::update_global(cx, |assistant, cx| {
assistant.scroll_to_assist(*assist_id, cx)
});
return;
}
fn activate_workflow_step(
&mut self,
step_range: Range<language::Anchor>,
cx: &mut ViewContext<Self>,
) {
let Some(step) = self.workflow_steps.get_mut(&step_range) else {
return;
};
let mut scroll_to_assist_id = None;
match step.status(cx) {
StepStatus::Idle => {
if let Some(ResolvedWorkflowStep { title, suggestions }) = step.suggestions.as_ref()
{
if let Some((editor, assist_ids)) = Self::suggest_edits(
title.clone(),
suggestions.clone(),
&self.project,
&self.assistant_panel,
&self.workspace,
cx,
) {
let mut observations = Vec::new();
InlineAssistant::update_global(cx, |assistant, _cx| {
for assist_id in &assist_ids {
observations.push(assistant.observe_assist(*assist_id));
}
});
step.assists = Some(WorkflowStepAssists {
assist_ids,
editor: editor.downgrade(),
_observe_assist_status: cx.spawn(|this, mut cx| async move {
while !observations.is_empty() {
let (result, ix, _) = futures::future::select_all(
observations
.iter_mut()
.map(|observation| Box::pin(observation.changed())),
)
.await;
if result.is_err() {
observations.remove(ix);
}
if this.update(&mut cx, |_, cx| cx.notify()).is_err() {
break;
}
}
}),
});
}
}
}
StepStatus::Pending => {
if let Some(assists) = step.assists.as_ref() {
let assistant = InlineAssistant::global(cx);
scroll_to_assist_id = assists
.assist_ids
.iter()
.copied()
.find(|assist_id| assistant.assist_status(*assist_id, cx).is_pending());
}
}
StepStatus::Done => {
if let Some(assists) = step.assists.as_ref() {
scroll_to_assist_id = assists.assist_ids.first().copied();
}
}
_ => {}
}
if let Some(ResolvedWorkflowStepEditSuggestions {
title,
edit_suggestions,
}) = step.suggestions.as_ref()
{
if let Some((editor, assist_ids)) =
self.suggest_edits(title.clone(), edit_suggestions.clone(), cx)
if let Some(assist_id) = scroll_to_assist_id {
if let Some(editor) = step
.assists
.as_ref()
.and_then(|assists| assists.editor.upgrade())
{
self.assists_by_step.insert(
step.range.clone(),
StepAssists {
assist_ids,
editor: editor.downgrade(),
},
);
self.workspace
.update(cx, |workspace, cx| {
workspace.activate_item(&editor, false, false, cx);
})
.ok();
InlineAssistant::update_global(cx, |assistant, cx| {
assistant.scroll_to_assist(assist_id, cx)
});
}
}
self.active_workflow_step = Some(step);
self.active_workflow_step = Some(ActiveWorkflowStep {
range: step_range,
suggestions_resolved: step.suggestions.is_some(),
});
}
fn suggest_edits(
&mut self,
title: String,
edit_suggestions: HashMap<Model<Buffer>, Vec<EditSuggestionGroup>>,
cx: &mut ViewContext<Self>,
suggestions: HashMap<Model<Buffer>, Vec<WorkflowSuggestionGroup>>,
project: &Model<Project>,
assistant_panel: &WeakView<AssistantPanel>,
workspace: &WeakView<Workspace>,
cx: &mut WindowContext,
) -> Option<(View<Editor>, Vec<InlineAssistId>)> {
let assistant_panel = self.assistant_panel.upgrade()?;
if edit_suggestions.is_empty() {
let assistant_panel = assistant_panel.upgrade()?;
if suggestions.is_empty() {
return None;
}
let editor;
let mut suggestion_groups = Vec::new();
if edit_suggestions.len() == 1 && edit_suggestions.values().next().unwrap().len() == 1 {
if suggestions.len() == 1 && suggestions.values().next().unwrap().len() == 1 {
// If there's only one buffer and one suggestion group, open it directly
let (buffer, groups) = edit_suggestions.into_iter().next().unwrap();
let (buffer, groups) = suggestions.into_iter().next().unwrap();
let group = groups.into_iter().next().unwrap();
editor = self
.workspace
editor = workspace
.update(cx, |workspace, cx| {
let active_pane = workspace.active_pane().clone();
workspace.open_project_item::<Editor>(active_pane, buffer, false, false, cx)
@@ -2091,10 +2531,10 @@ impl ContextEditor {
} else {
// If there are multiple buffers or suggestion groups, create a multibuffer
let multibuffer = cx.new_model(|cx| {
let replica_id = self.project.read(cx).replica_id();
let replica_id = project.read(cx).replica_id();
let mut multibuffer =
MultiBuffer::new(replica_id, Capability::ReadWrite).with_title(title);
for (buffer, groups) in edit_suggestions {
for (buffer, groups) in suggestions {
let excerpt_ids = multibuffer.push_excerpts(
buffer,
groups.iter().map(|suggestion_group| ExcerptRange {
@@ -2109,9 +2549,9 @@ impl ContextEditor {
});
editor = cx.new_view(|cx| {
Editor::for_multibuffer(multibuffer, Some(self.project.clone()), true, cx)
Editor::for_multibuffer(multibuffer, Some(project.clone()), true, cx)
});
self.workspace
workspace
.update(cx, |workspace, cx| {
workspace.add_item_to_active_pane(Box::new(editor.clone()), None, false, cx)
})
@@ -2124,7 +2564,7 @@ impl ContextEditor {
assist_ids.extend(suggestion.show(
&editor,
excerpt_id,
&self.workspace,
workspace,
&assistant_panel,
cx,
));
@@ -2516,10 +2956,10 @@ impl ContextEditor {
let focus_handle = self.focus_handle(cx).clone();
let button_text = match self.active_workflow_step.as_ref() {
Some(step) => {
if step.suggestions.is_none() {
"Computing Changes..."
} else {
if step.suggestions_resolved {
"Apply Changes"
} else {
"Computing Changes..."
}
}
None => "Send",
@@ -2563,31 +3003,31 @@ impl ContextEditor {
})
}
fn workflow_step_range_for_cursor(&self, cx: &AppContext) -> Option<Range<language::Anchor>> {
let newest_cursor = self
.editor
.read(cx)
.selections
.newest_anchor()
.head()
.text_anchor;
fn active_workflow_step_for_cursor(&self, cx: &AppContext) -> Option<ActiveWorkflowStep> {
let newest_cursor = self.editor.read(cx).selections.newest::<usize>(cx).head();
let context = self.context.read(cx);
let buffer = context.buffer().read(cx);
let edit_steps = context.workflow_steps();
edit_steps
.binary_search_by(|step| {
let step_range = step.tagged_range.clone();
if newest_cursor.cmp(&step_range.start, buffer).is_lt() {
let step_range = step.tagged_range.to_offset(&buffer);
if newest_cursor < step_range.start {
Ordering::Greater
} else if newest_cursor.cmp(&step_range.end, buffer).is_gt() {
} else if newest_cursor > step_range.end {
Ordering::Less
} else {
Ordering::Equal
}
})
.ok()
.map(|index| edit_steps[index].tagged_range.clone())
.and_then(|index| {
let range = edit_steps[index].tagged_range.clone();
Some(ActiveWorkflowStep {
suggestions_resolved: self.workflow_steps.get(&range)?.suggestions.is_some(),
range,
})
})
}
}

View File

@@ -284,7 +284,10 @@ pub enum ContextEvent {
AssistError(String),
MessagesEdited,
SummaryChanged,
WorkflowStepsChanged,
WorkflowStepsChanged {
removed: Vec<Range<language::Anchor>>,
updated: Vec<Range<language::Anchor>>,
},
StreamedCompletion,
PendingSlashCommandsUpdated {
removed: Vec<Range<language::Anchor>>,
@@ -348,37 +351,44 @@ pub struct SlashCommandId(clock::Lamport);
#[derive(Debug)]
pub struct WorkflowStep {
pub tagged_range: Range<language::Anchor>,
pub edit_suggestions: WorkflowStepEditSuggestions,
pub status: WorkflowStepStatus,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ResolvedWorkflowStepEditSuggestions {
pub struct ResolvedWorkflowStep {
pub title: String,
pub edit_suggestions: HashMap<Model<Buffer>, Vec<EditSuggestionGroup>>,
pub suggestions: HashMap<Model<Buffer>, Vec<WorkflowSuggestionGroup>>,
}
pub enum WorkflowStepEditSuggestions {
pub enum WorkflowStepStatus {
Pending(Task<Option<()>>),
Resolved(ResolvedWorkflowStepEditSuggestions),
Resolved(ResolvedWorkflowStep),
}
impl WorkflowStepEditSuggestions {
pub fn as_resolved(&self) -> Option<&ResolvedWorkflowStepEditSuggestions> {
impl WorkflowStepStatus {
pub fn as_resolved(&self) -> Option<&ResolvedWorkflowStep> {
match self {
WorkflowStepEditSuggestions::Resolved(suggestions) => Some(suggestions),
WorkflowStepEditSuggestions::Pending(_) => None,
WorkflowStepStatus::Resolved(suggestions) => Some(suggestions),
WorkflowStepStatus::Pending(_) => None,
}
}
pub fn is_resolved(&self) -> bool {
match self {
WorkflowStepStatus::Resolved(_) => true,
WorkflowStepStatus::Pending(_) => false,
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct EditSuggestionGroup {
pub struct WorkflowSuggestionGroup {
pub context_range: Range<language::Anchor>,
pub suggestions: Vec<EditSuggestion>,
pub suggestions: Vec<WorkflowSuggestion>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum EditSuggestion {
pub enum WorkflowSuggestion {
Update {
range: Range<language::Anchor>,
description: String,
@@ -407,40 +417,40 @@ pub enum EditSuggestion {
},
}
impl EditSuggestion {
impl WorkflowSuggestion {
pub fn range(&self) -> Range<language::Anchor> {
match self {
EditSuggestion::Update { range, .. } => range.clone(),
EditSuggestion::CreateFile { .. } => language::Anchor::MIN..language::Anchor::MAX,
EditSuggestion::InsertSiblingBefore { position, .. }
| EditSuggestion::InsertSiblingAfter { position, .. }
| EditSuggestion::PrependChild { position, .. }
| EditSuggestion::AppendChild { position, .. } => *position..*position,
EditSuggestion::Delete { range } => range.clone(),
WorkflowSuggestion::Update { range, .. } => range.clone(),
WorkflowSuggestion::CreateFile { .. } => language::Anchor::MIN..language::Anchor::MAX,
WorkflowSuggestion::InsertSiblingBefore { position, .. }
| WorkflowSuggestion::InsertSiblingAfter { position, .. }
| WorkflowSuggestion::PrependChild { position, .. }
| WorkflowSuggestion::AppendChild { position, .. } => *position..*position,
WorkflowSuggestion::Delete { range } => range.clone(),
}
}
pub fn description(&self) -> Option<&str> {
match self {
EditSuggestion::Update { description, .. }
| EditSuggestion::CreateFile { description }
| EditSuggestion::InsertSiblingBefore { description, .. }
| EditSuggestion::InsertSiblingAfter { description, .. }
| EditSuggestion::PrependChild { description, .. }
| EditSuggestion::AppendChild { description, .. } => Some(description),
EditSuggestion::Delete { .. } => None,
WorkflowSuggestion::Update { description, .. }
| WorkflowSuggestion::CreateFile { description }
| WorkflowSuggestion::InsertSiblingBefore { description, .. }
| WorkflowSuggestion::InsertSiblingAfter { description, .. }
| WorkflowSuggestion::PrependChild { description, .. }
| WorkflowSuggestion::AppendChild { description, .. } => Some(description),
WorkflowSuggestion::Delete { .. } => None,
}
}
fn description_mut(&mut self) -> Option<&mut String> {
match self {
EditSuggestion::Update { description, .. }
| EditSuggestion::CreateFile { description }
| EditSuggestion::InsertSiblingBefore { description, .. }
| EditSuggestion::InsertSiblingAfter { description, .. }
| EditSuggestion::PrependChild { description, .. }
| EditSuggestion::AppendChild { description, .. } => Some(description),
EditSuggestion::Delete { .. } => None,
WorkflowSuggestion::Update { description, .. }
| WorkflowSuggestion::CreateFile { description }
| WorkflowSuggestion::InsertSiblingBefore { description, .. }
| WorkflowSuggestion::InsertSiblingAfter { description, .. }
| WorkflowSuggestion::PrependChild { description, .. }
| WorkflowSuggestion::AppendChild { description, .. } => Some(description),
WorkflowSuggestion::Delete { .. } => None,
}
}
@@ -479,16 +489,16 @@ impl EditSuggestion {
let snapshot = buffer.read(cx).snapshot(cx);
match self {
EditSuggestion::Update { range, description } => {
WorkflowSuggestion::Update { range, description } => {
initial_prompt = description.clone();
suggestion_range = snapshot.anchor_in_excerpt(excerpt_id, range.start)?
..snapshot.anchor_in_excerpt(excerpt_id, range.end)?;
}
EditSuggestion::CreateFile { description } => {
WorkflowSuggestion::CreateFile { description } => {
initial_prompt = description.clone();
suggestion_range = editor::Anchor::min()..editor::Anchor::min();
}
EditSuggestion::InsertSiblingBefore {
WorkflowSuggestion::InsertSiblingBefore {
position,
description,
} => {
@@ -498,12 +508,13 @@ impl EditSuggestion {
buffer.start_transaction(cx);
let line_start = buffer.insert_empty_line(position, true, true, cx);
initial_transaction_id = buffer.end_transaction(cx);
buffer.refresh_preview(cx);
let line_start = buffer.read(cx).anchor_before(line_start);
line_start..line_start
});
}
EditSuggestion::InsertSiblingAfter {
WorkflowSuggestion::InsertSiblingAfter {
position,
description,
} => {
@@ -513,12 +524,13 @@ impl EditSuggestion {
buffer.start_transaction(cx);
let line_start = buffer.insert_empty_line(position, true, true, cx);
initial_transaction_id = buffer.end_transaction(cx);
buffer.refresh_preview(cx);
let line_start = buffer.read(cx).anchor_before(line_start);
line_start..line_start
});
}
EditSuggestion::PrependChild {
WorkflowSuggestion::PrependChild {
position,
description,
} => {
@@ -528,12 +540,13 @@ impl EditSuggestion {
buffer.start_transaction(cx);
let line_start = buffer.insert_empty_line(position, false, true, cx);
initial_transaction_id = buffer.end_transaction(cx);
buffer.refresh_preview(cx);
let line_start = buffer.read(cx).anchor_before(line_start);
line_start..line_start
});
}
EditSuggestion::AppendChild {
WorkflowSuggestion::AppendChild {
position,
description,
} => {
@@ -543,12 +556,13 @@ impl EditSuggestion {
buffer.start_transaction(cx);
let line_start = buffer.insert_empty_line(position, true, false, cx);
initial_transaction_id = buffer.end_transaction(cx);
buffer.refresh_preview(cx);
let line_start = buffer.read(cx).anchor_before(line_start);
line_start..line_start
});
}
EditSuggestion::Delete { range } => {
WorkflowSuggestion::Delete { range } => {
initial_prompt = "Delete".to_string();
suggestion_range = snapshot.anchor_in_excerpt(excerpt_id, range.start)?
..snapshot.anchor_in_excerpt(excerpt_id, range.end)?;
@@ -569,17 +583,14 @@ impl EditSuggestion {
}
}
impl Debug for WorkflowStepEditSuggestions {
impl Debug for WorkflowStepStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
WorkflowStepEditSuggestions::Pending(_) => write!(f, "EditStepOperations::Pending"),
WorkflowStepEditSuggestions::Resolved(ResolvedWorkflowStepEditSuggestions {
title,
edit_suggestions,
}) => f
WorkflowStepStatus::Pending(_) => write!(f, "EditStepOperations::Pending"),
WorkflowStepStatus::Resolved(ResolvedWorkflowStep { title, suggestions }) => f
.debug_struct("EditStepOperations::Parsed")
.field("title", title)
.field("edit_suggestions", edit_suggestions)
.field("suggestions", suggestions)
.finish(),
}
}
@@ -1160,11 +1171,20 @@ impl Context {
fn prune_invalid_edit_steps(&mut self, cx: &mut ModelContext<Self>) {
let buffer = self.buffer.read(cx);
let prev_len = self.workflow_steps.len();
let mut removed = Vec::new();
self.workflow_steps.retain(|step| {
step.tagged_range.start.is_valid(buffer) && step.tagged_range.end.is_valid(buffer)
if step.tagged_range.start.is_valid(buffer) && step.tagged_range.end.is_valid(buffer) {
true
} else {
removed.push(step.tagged_range.clone());
false
}
});
if self.workflow_steps.len() != prev_len {
cx.emit(ContextEvent::WorkflowStepsChanged);
cx.emit(ContextEvent::WorkflowStepsChanged {
removed,
updated: Vec::new(),
});
cx.notify();
}
}
@@ -1176,27 +1196,34 @@ impl Context {
cx: &mut ModelContext<Self>,
) {
let mut new_edit_steps = Vec::new();
let mut edits = Vec::new();
let buffer = self.buffer.read(cx).snapshot();
let mut message_lines = buffer.as_rope().chunks_in_range(range).lines();
let mut in_step = false;
let mut step_start = 0;
let mut step_open_tag_start_ix = 0;
let mut line_start_offset = message_lines.offset();
while let Some(line) = message_lines.next() {
if let Some(step_start_index) = line.find("<step>") {
if !in_step {
in_step = true;
step_start = line_start_offset + step_start_index;
step_open_tag_start_ix = line_start_offset + step_start_index;
}
}
if let Some(step_end_index) = line.find("</step>") {
if in_step {
let start_anchor = buffer.anchor_after(step_start);
let end_anchor =
buffer.anchor_before(line_start_offset + step_end_index + "</step>".len());
let tagged_range = start_anchor..end_anchor;
let step_open_tag_end_ix = step_open_tag_start_ix + "<step>".len();
let mut step_end_tag_start_ix = line_start_offset + step_end_index;
let step_end_tag_end_ix = step_end_tag_start_ix + "</step>".len();
if buffer.reversed_chars_at(step_end_tag_start_ix).next() == Some('\n') {
step_end_tag_start_ix -= 1;
}
edits.push((step_open_tag_start_ix..step_open_tag_end_ix, ""));
edits.push((step_end_tag_start_ix..step_end_tag_end_ix, ""));
let tagged_range = buffer.anchor_after(step_open_tag_end_ix)
..buffer.anchor_before(step_end_tag_start_ix);
// Check if a step with the same range already exists
let existing_step_index = self
@@ -1205,16 +1232,13 @@ impl Context {
if let Err(ix) = existing_step_index {
// Step doesn't exist, so add it
let task = self.compute_workflow_step_edit_suggestions(
tagged_range.clone(),
project.clone(),
cx,
);
let task =
self.resolve_workflow_step(tagged_range.clone(), project.clone(), cx);
new_edit_steps.push((
ix,
WorkflowStep {
tagged_range,
edit_suggestions: WorkflowStepEditSuggestions::Pending(task),
status: WorkflowStepStatus::Pending(task),
},
));
}
@@ -1226,16 +1250,22 @@ impl Context {
line_start_offset = message_lines.offset();
}
// Insert new steps and generate their corresponding tasks
let mut updated = Vec::new();
for (index, step) in new_edit_steps.into_iter().rev() {
updated.push(step.tagged_range.clone());
self.workflow_steps.insert(index, step);
}
self.buffer
.update(cx, |buffer, cx| buffer.edit(edits, None, cx));
cx.emit(ContextEvent::WorkflowStepsChanged);
cx.emit(ContextEvent::WorkflowStepsChanged {
removed: Vec::new(),
updated,
});
cx.notify();
}
fn compute_workflow_step_edit_suggestions(
fn resolve_workflow_step(
&self,
tagged_range: Range<language::Anchor>,
project: Model<Project>,
@@ -1265,13 +1295,13 @@ impl Context {
});
// Invoke the model to get its edit suggestions for this workflow step.
let step_suggestions = model
.use_tool::<tool::WorkflowStepEditSuggestions>(request, &cx)
let resolution = model
.use_tool::<tool::WorkflowStepResolution>(request, &cx)
.await?;
// Translate the parsed suggestions to our internal types, which anchor the suggestions to locations in the code.
let suggestion_tasks: Vec<_> = step_suggestions
.edit_suggestions
let suggestion_tasks: Vec<_> = resolution
.suggestions
.iter()
.map(|suggestion| suggestion.resolve(project.clone(), cx.clone()))
.collect();
@@ -1293,7 +1323,7 @@ impl Context {
let mut suggestion_groups_by_buffer = HashMap::default();
for (buffer, mut suggestions) in suggestions_by_buffer {
let mut suggestion_groups = Vec::<EditSuggestionGroup>::new();
let mut suggestion_groups = Vec::<WorkflowSuggestionGroup>::new();
let snapshot = buffer.update(&mut cx, |buffer, _| buffer.snapshot())?;
// Sort suggestions by their range so that earlier, larger ranges come first
suggestions.sort_by(|a, b| a.range().cmp(&b.range(), &snapshot));
@@ -1328,14 +1358,14 @@ impl Context {
last_group.suggestions.push(suggestion);
} else {
// Create a new group
suggestion_groups.push(EditSuggestionGroup {
suggestion_groups.push(WorkflowSuggestionGroup {
context_range,
suggestions: vec![suggestion],
});
}
} else {
// Create the first group
suggestion_groups.push(EditSuggestionGroup {
suggestion_groups.push(WorkflowSuggestionGroup {
context_range,
suggestions: vec![suggestion],
});
@@ -1353,13 +1383,14 @@ impl Context {
})
.map_err(|_| anyhow!("edit step not found"))?;
if let Some(edit_step) = this.workflow_steps.get_mut(step_index) {
edit_step.edit_suggestions = WorkflowStepEditSuggestions::Resolved(
ResolvedWorkflowStepEditSuggestions {
title: step_suggestions.step_title,
edit_suggestions: suggestion_groups_by_buffer,
},
);
cx.emit(ContextEvent::WorkflowStepsChanged);
edit_step.status = WorkflowStepStatus::Resolved(ResolvedWorkflowStep {
title: resolution.step_title,
suggestions: suggestion_groups_by_buffer,
});
cx.emit(ContextEvent::WorkflowStepsChanged {
updated: vec![tagged_range],
removed: Vec::new(),
});
}
anyhow::Ok(())
})?
@@ -3022,19 +3053,17 @@ mod tests {
model
.as_fake()
.respond_to_last_tool_use(Ok(serde_json::to_value(
tool::WorkflowStepEditSuggestions {
step_title: "Title".into(),
edit_suggestions: vec![tool::EditSuggestion {
path: "/root/hello.rs".into(),
// Simulate a symbol name that's slightly different than our outline query
kind: tool::EditSuggestionKind::Update {
symbol: "fn main()".into(),
description: "Extract a greeting function".into(),
},
}],
},
)
.respond_to_last_tool_use(Ok(serde_json::to_value(tool::WorkflowStepResolution {
step_title: "Title".into(),
suggestions: vec![tool::WorkflowSuggestion {
path: "/root/hello.rs".into(),
// Simulate a symbol name that's slightly different than our outline query
kind: tool::WorkflowSuggestionKind::Update {
symbol: "fn main()".into(),
description: "Extract a greeting function".into(),
},
}],
})
.unwrap()));
// Wait for tool use to be processed.
@@ -3074,11 +3103,9 @@ mod tests {
.iter()
.map(|step| {
let buffer = context.buffer.read(cx);
let status = match &step.edit_suggestions {
WorkflowStepEditSuggestions::Pending(_) => {
WorkflowStepEditSuggestionStatus::Pending
}
WorkflowStepEditSuggestions::Resolved { .. } => {
let status = match &step.status {
WorkflowStepStatus::Pending(_) => WorkflowStepEditSuggestionStatus::Pending,
WorkflowStepStatus::Resolved { .. } => {
WorkflowStepEditSuggestionStatus::Resolved
}
};
@@ -3490,15 +3517,15 @@ mod tool {
use super::*;
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct WorkflowStepEditSuggestions {
pub struct WorkflowStepResolution {
/// An extremely short title for the edit step represented by these operations.
pub step_title: String,
/// A sequence of operations to apply to the codebase.
/// When multiple operations are required for a step, be sure to include multiple operations in this list.
pub edit_suggestions: Vec<EditSuggestion>,
pub suggestions: Vec<WorkflowSuggestion>,
}
impl LanguageModelTool for WorkflowStepEditSuggestions {
impl LanguageModelTool for WorkflowStepResolution {
fn name() -> String {
"edit".into()
}
@@ -3527,19 +3554,19 @@ mod tool {
/// programmatic changes to source code. It provides a structured way to describe
/// edits for features like refactoring tools or AI-assisted coding suggestions.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct EditSuggestion {
pub struct WorkflowSuggestion {
/// The path to the file containing the relevant operation
pub path: String,
#[serde(flatten)]
pub kind: EditSuggestionKind,
pub kind: WorkflowSuggestionKind,
}
impl EditSuggestion {
impl WorkflowSuggestion {
pub(super) async fn resolve(
&self,
project: Model<Project>,
mut cx: AsyncAppContext,
) -> Result<(Model<Buffer>, super::EditSuggestion)> {
) -> Result<(Model<Buffer>, super::WorkflowSuggestion)> {
let path = self.path.clone();
let kind = self.kind.clone();
let buffer = project
@@ -3561,7 +3588,7 @@ mod tool {
let suggestion;
match kind {
EditSuggestionKind::Update {
WorkflowSuggestionKind::Update {
symbol,
description,
} => {
@@ -3578,12 +3605,12 @@ mod tool {
snapshot.line_len(symbol.range.end.row),
);
let range = snapshot.anchor_before(start)..snapshot.anchor_after(end);
suggestion = super::EditSuggestion::Update { range, description };
suggestion = super::WorkflowSuggestion::Update { range, description };
}
EditSuggestionKind::Create { description } => {
suggestion = super::EditSuggestion::CreateFile { description };
WorkflowSuggestionKind::Create { description } => {
suggestion = super::WorkflowSuggestion::CreateFile { description };
}
EditSuggestionKind::InsertSiblingBefore {
WorkflowSuggestionKind::InsertSiblingBefore {
symbol,
description,
} => {
@@ -3598,12 +3625,12 @@ mod tool {
annotation_range.start
}),
);
suggestion = super::EditSuggestion::InsertSiblingBefore {
suggestion = super::WorkflowSuggestion::InsertSiblingBefore {
position,
description,
};
}
EditSuggestionKind::InsertSiblingAfter {
WorkflowSuggestionKind::InsertSiblingAfter {
symbol,
description,
} => {
@@ -3612,12 +3639,12 @@ mod tool {
.with_context(|| format!("symbol not found: {:?}", symbol))?
.to_point(&snapshot);
let position = snapshot.anchor_after(symbol.range.end);
suggestion = super::EditSuggestion::InsertSiblingAfter {
suggestion = super::WorkflowSuggestion::InsertSiblingAfter {
position,
description,
};
}
EditSuggestionKind::PrependChild {
WorkflowSuggestionKind::PrependChild {
symbol,
description,
} => {
@@ -3632,18 +3659,18 @@ mod tool {
.body_range
.map_or(symbol.range.start, |body_range| body_range.start),
);
suggestion = super::EditSuggestion::PrependChild {
suggestion = super::WorkflowSuggestion::PrependChild {
position,
description,
};
} else {
suggestion = super::EditSuggestion::PrependChild {
suggestion = super::WorkflowSuggestion::PrependChild {
position: language::Anchor::MIN,
description,
};
}
}
EditSuggestionKind::AppendChild {
WorkflowSuggestionKind::AppendChild {
symbol,
description,
} => {
@@ -3658,18 +3685,18 @@ mod tool {
.body_range
.map_or(symbol.range.end, |body_range| body_range.end),
);
suggestion = super::EditSuggestion::AppendChild {
suggestion = super::WorkflowSuggestion::AppendChild {
position,
description,
};
} else {
suggestion = super::EditSuggestion::PrependChild {
suggestion = super::WorkflowSuggestion::PrependChild {
position: language::Anchor::MAX,
description,
};
}
}
EditSuggestionKind::Delete { symbol } => {
WorkflowSuggestionKind::Delete { symbol } => {
let symbol = outline
.find_most_similar(&symbol)
.with_context(|| format!("symbol not found: {:?}", symbol))?
@@ -3683,7 +3710,7 @@ mod tool {
snapshot.line_len(symbol.range.end.row),
);
let range = snapshot.anchor_before(start)..snapshot.anchor_after(end);
suggestion = super::EditSuggestion::Delete { range };
suggestion = super::WorkflowSuggestion::Delete { range };
}
}
@@ -3693,7 +3720,7 @@ mod tool {
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "kind")]
pub enum EditSuggestionKind {
pub enum WorkflowSuggestionKind {
/// Rewrites the specified symbol entirely based on the given description.
/// This operation completely replaces the existing symbol with new content.
Update {
@@ -3754,7 +3781,7 @@ mod tool {
},
}
impl EditSuggestionKind {
impl WorkflowSuggestionKind {
pub fn symbol(&self) -> Option<&str> {
match self {
Self::Update { symbol, .. } => Some(symbol),
@@ -3781,14 +3808,14 @@ mod tool {
pub fn initial_insertion(&self) -> Option<InitialInsertion> {
match self {
EditSuggestionKind::InsertSiblingBefore { .. } => {
WorkflowSuggestionKind::InsertSiblingBefore { .. } => {
Some(InitialInsertion::NewlineAfter)
}
EditSuggestionKind::InsertSiblingAfter { .. } => {
WorkflowSuggestionKind::InsertSiblingAfter { .. } => {
Some(InitialInsertion::NewlineBefore)
}
EditSuggestionKind::PrependChild { .. } => Some(InitialInsertion::NewlineAfter),
EditSuggestionKind::AppendChild { .. } => Some(InitialInsertion::NewlineBefore),
WorkflowSuggestionKind::PrependChild { .. } => Some(InitialInsertion::NewlineAfter),
WorkflowSuggestionKind::AppendChild { .. } => Some(InitialInsertion::NewlineBefore),
_ => None,
}
}

View File

@@ -68,6 +68,9 @@ pub struct InlineAssistant {
assists: HashMap<InlineAssistId, InlineAssist>,
assists_by_editor: HashMap<WeakView<Editor>, EditorInlineAssists>,
assist_groups: HashMap<InlineAssistGroupId, InlineAssistGroup>,
assist_observations:
HashMap<InlineAssistId, (async_watch::Sender<()>, async_watch::Receiver<()>)>,
confirmed_assists: HashMap<InlineAssistId, Model<Codegen>>,
prompt_history: VecDeque<String>,
prompt_builder: Arc<PromptBuilder>,
telemetry: Option<Arc<Telemetry>>,
@@ -88,6 +91,8 @@ impl InlineAssistant {
assists: HashMap::default(),
assists_by_editor: HashMap::default(),
assist_groups: HashMap::default(),
assist_observations: HashMap::default(),
confirmed_assists: HashMap::default(),
prompt_history: VecDeque::default(),
prompt_builder,
telemetry: Some(telemetry),
@@ -654,8 +659,21 @@ impl InlineAssistant {
if undo {
assist.codegen.update(cx, |codegen, cx| codegen.undo(cx));
} else {
self.confirmed_assists.insert(assist_id, assist.codegen);
}
}
// Remove the assist from the status updates map
self.assist_observations.remove(&assist_id);
}
pub fn undo_assist(&mut self, assist_id: InlineAssistId, cx: &mut WindowContext) -> bool {
let Some(codegen) = self.confirmed_assists.remove(&assist_id) else {
return false;
};
codegen.update(cx, |this, cx| this.undo(cx));
true
}
fn dismiss_assist(&mut self, assist_id: InlineAssistId, cx: &mut WindowContext) -> bool {
@@ -854,6 +872,10 @@ impl InlineAssistant {
)
})
.log_err();
if let Some((tx, _)) = self.assist_observations.get(&assist_id) {
tx.send(()).ok();
}
}
pub fn stop_assist(&mut self, assist_id: InlineAssistId, cx: &mut WindowContext) {
@@ -864,19 +886,24 @@ impl InlineAssistant {
};
assist.codegen.update(cx, |codegen, cx| codegen.stop(cx));
if let Some((tx, _)) = self.assist_observations.get(&assist_id) {
tx.send(()).ok();
}
}
pub fn status_for_assist(
&self,
assist_id: InlineAssistId,
cx: &WindowContext,
) -> Option<CodegenStatus> {
let assist = self.assists.get(&assist_id)?;
match &assist.codegen.read(cx).status {
CodegenStatus::Idle => Some(CodegenStatus::Idle),
CodegenStatus::Pending => Some(CodegenStatus::Pending),
CodegenStatus::Done => Some(CodegenStatus::Done),
CodegenStatus::Error(error) => Some(CodegenStatus::Error(anyhow!("{:?}", error))),
pub fn assist_status(&self, assist_id: InlineAssistId, cx: &AppContext) -> InlineAssistStatus {
if let Some(assist) = self.assists.get(&assist_id) {
match &assist.codegen.read(cx).status {
CodegenStatus::Idle => InlineAssistStatus::Idle,
CodegenStatus::Pending => InlineAssistStatus::Pending,
CodegenStatus::Done => InlineAssistStatus::Done,
CodegenStatus::Error(error) => InlineAssistStatus::Error(anyhow!("{:?}", error)),
}
} else if self.confirmed_assists.contains_key(&assist_id) {
InlineAssistStatus::Confirmed
} else {
InlineAssistStatus::Canceled
}
}
@@ -1060,6 +1087,37 @@ impl InlineAssistant {
.collect();
})
}
pub fn observe_assist(&mut self, assist_id: InlineAssistId) -> async_watch::Receiver<()> {
if let Some((_, rx)) = self.assist_observations.get(&assist_id) {
rx.clone()
} else {
let (tx, rx) = async_watch::channel(());
self.assist_observations.insert(assist_id, (tx, rx.clone()));
rx
}
}
}
pub enum InlineAssistStatus {
Idle,
Pending,
Done,
Error(anyhow::Error),
Confirmed,
Canceled,
}
impl InlineAssistStatus {
pub(crate) fn is_pending(&self) -> bool {
matches!(self, Self::Pending)
}
pub(crate) fn is_confirmed(&self) -> bool {
matches!(self, Self::Confirmed)
}
pub(crate) fn is_done(&self) -> bool {
matches!(self, Self::Done)
}
}
struct EditorInlineAssists {
@@ -1964,6 +2022,8 @@ impl InlineAssist {
if assist.decorations.is_none() {
this.finish_assist(assist_id, false, cx);
} else if let Some(tx) = this.assist_observations.get(&assist_id) {
tx.0.send(()).ok();
}
}
})
@@ -2037,7 +2097,7 @@ pub struct Codegen {
builder: Arc<PromptBuilder>,
}
pub enum CodegenStatus {
enum CodegenStatus {
Idle,
Pending,
Done,
@@ -2156,7 +2216,7 @@ impl Codegen {
if let Some(transformation_transaction_id) = self.transformation_transaction_id.take() {
self.buffer.update(cx, |buffer, cx| {
buffer.undo_transaction(transformation_transaction_id, cx)
buffer.undo_transaction(transformation_transaction_id, cx);
});
}
@@ -2510,10 +2570,12 @@ impl Codegen {
self.buffer.update(cx, |buffer, cx| {
if let Some(transaction_id) = self.transformation_transaction_id.take() {
buffer.undo_transaction(transaction_id, cx);
buffer.refresh_preview(cx);
}
if let Some(transaction_id) = self.initial_transaction_id.take() {
buffer.undo_transaction(transaction_id, cx);
buffer.refresh_preview(cx);
}
});
}

View File

@@ -1,4 +1,4 @@
use feature_flags::LanguageModels;
use feature_flags::ZedPro;
use language_model::{LanguageModel, LanguageModelAvailability, LanguageModelRegistry};
use proto::Plan;
@@ -143,8 +143,9 @@ impl PickerDelegate for ModelPickerDelegate {
selected: bool,
cx: &mut ViewContext<Picker<Self>>,
) -> Option<Self::ListItem> {
use feature_flags::FeatureFlagAppExt;
let model_info = self.filtered_models.get(ix)?;
let show_badges = cx.has_flag::<ZedPro>();
Some(
ListItem::new(ix)
.inset(true)
@@ -170,11 +171,13 @@ impl PickerDelegate for ModelPickerDelegate {
.children(match model_info.availability {
LanguageModelAvailability::Public => None,
LanguageModelAvailability::RequiresPlan(Plan::Free) => None,
LanguageModelAvailability::RequiresPlan(Plan::ZedPro) => Some(
Label::new("Pro")
.size(LabelSize::XSmall)
.color(Color::Muted),
),
LanguageModelAvailability::RequiresPlan(Plan::ZedPro) => {
show_badges.then(|| {
Label::new("Pro")
.size(LabelSize::XSmall)
.color(Color::Muted)
})
}
}),
)
.child(div().when(model_info.is_selected, |this| {
@@ -190,9 +193,6 @@ impl PickerDelegate for ModelPickerDelegate {
fn render_footer(&self, cx: &mut ViewContext<Picker<Self>>) -> Option<gpui::AnyElement> {
use feature_flags::FeatureFlagAppExt;
if !cx.has_flag::<LanguageModels>() {
return None;
}
let plan = proto::Plan::ZedPro;
let is_trial = false;
@@ -205,26 +205,28 @@ impl PickerDelegate for ModelPickerDelegate {
.p_1()
.gap_4()
.justify_between()
.child(match plan {
// Already a zed pro subscriber
Plan::ZedPro => Button::new("zed-pro", "Zed Pro")
.icon(IconName::ZedAssistant)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.icon_position(IconPosition::Start)
.on_click(|_, cx| {
cx.dispatch_action(Box::new(zed_actions::OpenAccountSettings))
}),
// Free user
Plan::Free => Button::new(
"try-pro",
if is_trial {
"Upgrade to Pro"
} else {
"Try Pro"
},
)
.on_click(|_, cx| cx.open_url(TRY_ZED_PRO_URL)),
.when(cx.has_flag::<ZedPro>(), |this| {
this.child(match plan {
// Already a zed pro subscriber
Plan::ZedPro => Button::new("zed-pro", "Zed Pro")
.icon(IconName::ZedAssistant)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.icon_position(IconPosition::Start)
.on_click(|_, cx| {
cx.dispatch_action(Box::new(zed_actions::OpenAccountSettings))
}),
// Free user
Plan::Free => Button::new(
"try-pro",
if is_trial {
"Upgrade to Pro"
} else {
"Try Pro"
},
)
.on_click(|_, cx| cx.open_url(TRY_ZED_PRO_URL)),
})
})
.child(
Button::new("configure", "Configure")

View File

@@ -58,6 +58,7 @@ serde_derive.workspace = true
serde_json.workspace = true
sha2.workspace = true
sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "postgres", "json", "time", "uuid", "any"] }
strum.workspace = true
subtle.workspace = true
rustc-demangle.workspace = true
telemetry_events.workspace = true

View File

@@ -235,7 +235,8 @@ impl Config {
}
/// The service mode that collab should run in.
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
#[derive(Debug, PartialEq, Eq, Clone, Copy, strum::Display)]
#[strum(serialize_all = "snake_case")]
pub enum ServiceMode {
Api,
Collab,

View File

@@ -6,21 +6,40 @@ use crate::{Config, Error, Result};
pub fn authorize_access_to_language_model(
config: &Config,
_claims: &LlmTokenClaims,
claims: &LlmTokenClaims,
country_code: Option<String>,
provider: LanguageModelProvider,
model: &str,
) -> Result<()> {
authorize_access_for_country(config, country_code, provider, model)?;
authorize_access_for_country(config, country_code, provider)?;
authorize_access_to_model(claims, provider, model)?;
Ok(())
}
fn authorize_access_to_model(
claims: &LlmTokenClaims,
provider: LanguageModelProvider,
model: &str,
) -> Result<()> {
if claims.is_staff {
return Ok(());
}
match (provider, model) {
(LanguageModelProvider::Anthropic, model) if model.starts_with("claude-3.5-sonnet") => {
Ok(())
}
_ => Err(Error::http(
StatusCode::FORBIDDEN,
format!("access to model {model:?} is not included in your plan"),
))?,
}
}
fn authorize_access_for_country(
config: &Config,
country_code: Option<String>,
provider: LanguageModelProvider,
_model: &str,
) -> Result<()> {
// In development we won't have the `CF-IPCountry` header, so we can't check
// the country code.
@@ -79,6 +98,7 @@ mod tests {
let claims = LlmTokenClaims {
user_id: 99,
plan: Plan::ZedPro,
is_staff: true,
..Default::default()
};
@@ -210,4 +230,101 @@ mod tests {
);
}
}
#[gpui::test]
async fn test_authorize_access_to_language_model_based_on_plan() {
let config = Config::test();
let test_cases = vec![
// Pro plan should have access to claude-3.5-sonnet
(
Plan::ZedPro,
LanguageModelProvider::Anthropic,
"claude-3.5-sonnet",
true,
),
// Free plan should have access to claude-3.5-sonnet
(
Plan::Free,
LanguageModelProvider::Anthropic,
"claude-3.5-sonnet",
true,
),
// Pro plan should NOT have access to other Anthropic models
(
Plan::ZedPro,
LanguageModelProvider::Anthropic,
"claude-3-opus",
false,
),
];
for (plan, provider, model, expected_access) in test_cases {
let claims = LlmTokenClaims {
plan,
..Default::default()
};
let result = authorize_access_to_language_model(
&config,
&claims,
Some("US".into()),
provider,
model,
);
if expected_access {
assert!(
result.is_ok(),
"Expected access to be granted for plan {:?}, provider {:?}, model {}",
plan,
provider,
model
);
} else {
let error = result.expect_err(&format!(
"Expected access to be denied for plan {:?}, provider {:?}, model {}",
plan, provider, model
));
let response = error.into_response();
assert_eq!(response.status(), StatusCode::FORBIDDEN);
}
}
}
#[gpui::test]
async fn test_authorize_access_to_language_model_for_staff() {
let config = Config::test();
let claims = LlmTokenClaims {
is_staff: true,
..Default::default()
};
// Staff should have access to all models
let test_cases = vec![
(LanguageModelProvider::Anthropic, "claude-3.5-sonnet"),
(LanguageModelProvider::Anthropic, "claude-2"),
(LanguageModelProvider::Anthropic, "claude-123-agi"),
(LanguageModelProvider::OpenAi, "gpt-4"),
(LanguageModelProvider::Google, "gemini-pro"),
];
for (provider, model) in test_cases {
let result = authorize_access_to_language_model(
&config,
&claims,
Some("US".into()),
provider,
model,
);
assert!(
result.is_ok(),
"Expected staff to have access to provider {:?}, model {}",
provider,
model
);
}
}
}

View File

@@ -13,13 +13,19 @@ pub struct LlmTokenClaims {
pub exp: u64,
pub jti: String,
pub user_id: u64,
pub is_staff: bool,
pub plan: rpc::proto::Plan,
}
const LLM_TOKEN_LIFETIME: Duration = Duration::from_secs(60 * 60);
impl LlmTokenClaims {
pub fn create(user_id: UserId, plan: rpc::proto::Plan, config: &Config) -> Result<String> {
pub fn create(
user_id: UserId,
is_staff: bool,
plan: rpc::proto::Plan,
config: &Config,
) -> Result<String> {
let secret = config
.llm_api_secret
.as_ref()
@@ -31,6 +37,7 @@ impl LlmTokenClaims {
exp: (now + LLM_TOKEN_LIFETIME).timestamp() as u64,
jti: uuid::Uuid::new_v4().to_string(),
user_id: user_id.to_proto(),
is_staff,
plan,
};

View File

@@ -279,10 +279,7 @@ async fn setup_llm_database(config: &Config) -> Result<()> {
}
async fn handle_root(Extension(mode): Extension<ServiceMode>) -> String {
format!(
"collab {mode:?} v{VERSION} ({})",
REVISION.unwrap_or("unknown")
)
format!("zed:{mode} v{VERSION} ({})", REVISION.unwrap_or("unknown"))
}
async fn handle_liveness_probe(

View File

@@ -5164,6 +5164,7 @@ async fn get_llm_api_token(
let token = LlmTokenClaims::create(
session.user_id(),
session.is_staff(),
session.current_plan().await?,
&session.app_state.config,
)?;

View File

@@ -10182,6 +10182,7 @@ impl Editor {
if let Some(autoscroll) = autoscroll {
self.request_autoscroll(autoscroll, cx);
}
cx.notify();
blocks
}
@@ -10196,6 +10197,7 @@ impl Editor {
if let Some(autoscroll) = autoscroll {
self.request_autoscroll(autoscroll, cx);
}
cx.notify();
}
pub fn replace_blocks(
@@ -10208,9 +10210,8 @@ impl Editor {
.update(cx, |display_map, _cx| display_map.replace_blocks(renderers));
if let Some(autoscroll) = autoscroll {
self.request_autoscroll(autoscroll, cx);
} else {
cx.notify();
}
cx.notify();
}
pub fn remove_blocks(
@@ -10225,6 +10226,7 @@ impl Editor {
if let Some(autoscroll) = autoscroll {
self.request_autoscroll(autoscroll, cx);
}
cx.notify();
}
pub fn row_for_block(

View File

@@ -894,6 +894,10 @@ impl Item for Editor {
_ => {}
}
}
fn preserve_preview(&self, cx: &AppContext) -> bool {
self.buffer.read(cx).preserve_preview(cx)
}
}
impl SerializableItem for Editor {

View File

@@ -172,3 +172,7 @@ path = "examples/window_shadow.rs"
[[example]]
name = "input"
path = "examples/input.rs"
[[example]]
name = "shadow"
path = "examples/shadow.rs"

View File

@@ -0,0 +1,29 @@
use gpui::*;
struct Shadow {}
impl Render for Shadow {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
div()
.flex()
.bg(rgb(0xffffff))
.size_full()
.justify_center()
.items_center()
.child(div().size_8().shadow_sm())
}
}
fn main() {
App::new().run(|cx: &mut AppContext| {
let bounds = Bounds::centered(None, size(px(300.0), px(300.0)), cx);
cx.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
..Default::default()
},
|cx| cx.new_view(|_cx| Shadow {}),
)
.unwrap();
});
}

View File

@@ -150,7 +150,7 @@ fn gaussian(x: f32, sigma: f32) -> f32{
fn erf(v: vec2<f32>) -> vec2<f32> {
let s = sign(v);
let a = abs(v);
let r1 = 1.0 + (0.278393 + (0.230389 + 0.078108 * (a * a)) * a) * a;
let r1 = 1.0 + (0.278393 + (0.230389 + (0.000972 + 0.078108 * a) * a) * a) * a;
let r2 = r1 * r1;
return s - s / (r2 * r2);
}

View File

@@ -657,9 +657,9 @@ float gaussian(float x, float sigma) {
float2 erf(float2 x) {
float2 s = sign(x);
float2 a = abs(x);
x = 1. + (0.278393 + (0.230389 + 0.078108 * (a * a)) * a) * a;
x *= x;
return s - s / (x * x);
float2 r1 = 1. + (0.278393 + (0.230389 + (0.000972 + 0.078108 * a) * a) * a) * a;
float2 r2 = r1 * r1;
return s - s / (r2 * r2);
}
float blur_along_x(float x, float y, float sigma, float corner,

View File

@@ -97,6 +97,7 @@ pub struct Buffer {
/// The version vector when this buffer was last loaded from
/// or saved to disk.
saved_version: clock::Global,
preview_version: clock::Global,
transaction_depth: usize,
was_dirty_before_starting_transaction: Option<bool>,
reload_task: Option<Task<Result<()>>>,
@@ -703,6 +704,7 @@ impl Buffer {
Self {
saved_mtime,
saved_version: buffer.version(),
preview_version: buffer.version(),
reload_task: None,
transaction_depth: 0,
was_dirty_before_starting_transaction: None,
@@ -1351,7 +1353,11 @@ impl Buffer {
})
.collect();
let preserve_preview = self.preserve_preview();
self.edit(edits, None, cx);
if preserve_preview {
self.refresh_preview();
}
}
/// Create a minimal edit that will cause the given row to be indented
@@ -2195,6 +2201,18 @@ impl Buffer {
pub fn completion_triggers(&self) -> &[String] {
&self.completion_triggers
}
/// Call this directly after performing edits to prevent the preview tab
/// from being dismissed by those edits. It causes `should_dismiss_preview`
/// to return false until there are additional edits.
pub fn refresh_preview(&mut self) {
self.preview_version = self.version.clone();
}
/// Whether we should preserve the preview status of a tab containing this buffer.
pub fn preserve_preview(&self) -> bool {
!self.has_edits_since(&self.preview_version)
}
}
#[doc(hidden)]

View File

@@ -1822,6 +1822,63 @@ fn test_autoindent_query_with_outdent_captures(cx: &mut AppContext) {
});
}
#[gpui::test]
async fn test_async_autoindents_preserve_preview(cx: &mut TestAppContext) {
cx.update(|cx| init_settings(cx, |_| {}));
// First we insert some newlines to request an auto-indent (asynchronously).
// Then we request that a preview tab be preserved for the new version, even though it's edited.
let buffer = cx.new_model(|cx| {
let text = "fn a() {}";
let mut buffer = Buffer::local(text, cx).with_language(Arc::new(rust_lang()), cx);
// This causes autoindent to be async.
buffer.set_sync_parse_timeout(Duration::ZERO);
buffer.edit([(8..8, "\n\n")], Some(AutoindentMode::EachLine), cx);
buffer.refresh_preview();
// Synchronously, we haven't auto-indented and we're still preserving the preview.
assert_eq!(buffer.text(), "fn a() {\n\n}");
assert!(buffer.preserve_preview());
buffer
});
// Now let the autoindent finish
cx.executor().run_until_parked();
// The auto-indent applied, but didn't dismiss our preview
buffer.update(cx, |buffer, cx| {
assert_eq!(buffer.text(), "fn a() {\n \n}");
assert!(buffer.preserve_preview());
// Edit inserting another line. It will autoindent async.
// Then refresh the preview version.
buffer.edit(
[(Point::new(1, 4)..Point::new(1, 4), "\n")],
Some(AutoindentMode::EachLine),
cx,
);
buffer.refresh_preview();
assert_eq!(buffer.text(), "fn a() {\n \n\n}");
assert!(buffer.preserve_preview());
// Then perform another edit, this time without refreshing the preview version.
buffer.edit([(Point::new(1, 4)..Point::new(1, 4), "x")], None, cx);
// This causes the preview to not be preserved.
assert!(!buffer.preserve_preview());
});
// Let the async autoindent from the first edit finish.
cx.executor().run_until_parked();
// The autoindent applies, but it shouldn't restore the preview status because we had an edit in the meantime.
buffer.update(cx, |buffer, _| {
assert_eq!(buffer.text(), "fn a() {\n x\n \n}");
assert!(!buffer.preserve_preview());
});
}
#[gpui::test]
fn test_insert_empty_line(cx: &mut AppContext) {
init_settings(cx, |_| {});

View File

@@ -68,3 +68,5 @@
")" @context))))
]
) @item
(comment) @annotation

View File

@@ -147,3 +147,5 @@
")" @context)))
]
(type_qualifier)? @context) @item
(comment) @annotation

View File

@@ -1,3 +1,4 @@
(comment) @annotation
(type_declaration
"type" @context
(type_spec

View File

@@ -71,3 +71,5 @@
)
)
) @item
(comment) @annotation

View File

@@ -1,3 +1,5 @@
(decorator) @annotation
(class_definition
"class" @context
name: (identifier) @name

View File

@@ -74,3 +74,5 @@
)
)
) @item
(comment) @annotation

View File

@@ -74,3 +74,5 @@
)
)
) @item
(comment) @annotation

View File

@@ -1762,6 +1762,23 @@ impl MultiBuffer {
cx.notify();
}
/// Preserve preview tabs containing this multibuffer until additional edits occur.
pub fn refresh_preview(&self, cx: &mut ModelContext<Self>) {
for buffer_state in self.buffers.borrow().values() {
buffer_state
.buffer
.update(cx, |buffer, _cx| buffer.refresh_preview());
}
}
/// Whether we should preserve the preview status of a tab containing this multi-buffer.
pub fn preserve_preview(&self, cx: &AppContext) -> bool {
self.buffers
.borrow()
.values()
.all(|state| state.buffer.read(cx).preserve_preview())
}
#[cfg(any(test, feature = "test-support"))]
pub fn is_parsing(&self, cx: &AppContext) -> bool {
self.as_singleton().unwrap().read(cx).is_parsing()

View File

@@ -1,4 +1,5 @@
use crate::{settings_store::SettingsStore, Settings};
use anyhow::Result;
use fs::Fs;
use futures::{channel::mpsc, StreamExt};
use gpui::{AppContext, BackgroundExecutor, ReadGlobal, UpdateGlobal};
@@ -66,6 +67,7 @@ pub fn watch_config_file(
pub fn handle_settings_file_changes(
mut user_settings_file_rx: mpsc::UnboundedReceiver<String>,
cx: &mut AppContext,
settings_changed: impl Fn(Result<()>, &mut AppContext) + 'static,
) {
let user_settings_content = cx
.background_executor()
@@ -79,9 +81,11 @@ pub fn handle_settings_file_changes(
cx.spawn(move |mut cx| async move {
while let Some(user_settings_content) = user_settings_file_rx.next().await {
let result = cx.update_global(|store: &mut SettingsStore, cx| {
store
.set_user_settings(&user_settings_content, cx)
.log_err();
let result = store.set_user_settings(&user_settings_content, cx);
if let Err(err) = &result {
log::error!("Failed to load user settings: {err}");
}
settings_changed(result, cx);
cx.refresh();
});
if result.is_err() {

View File

@@ -174,9 +174,9 @@ impl TerminalPanel {
self.pane.update(cx, |pane, cx| {
pane.set_render_tab_bar_buttons(cx, move |pane, cx| {
if !pane.has_focus(cx) {
return None;
return (None, None);
}
h_flex()
let right_children = h_flex()
.gap_2()
.children(additional_buttons.clone())
.child(
@@ -232,7 +232,8 @@ impl TerminalPanel {
})
})
.into_any_element()
.into()
.into();
(None, right_children)
});
});
}

View File

@@ -50,6 +50,7 @@ pub enum TintColor {
Accent,
Negative,
Warning,
Positive,
}
impl TintColor {
@@ -73,6 +74,12 @@ impl TintColor {
label_color: cx.theme().colors().text,
icon_color: cx.theme().colors().text,
},
TintColor::Positive => ButtonLikeStyles {
background: cx.theme().status().success_background,
border_color: cx.theme().status().success_border,
label_color: cx.theme().colors().text,
icon_color: cx.theme().colors().text,
},
}
}
}
@@ -83,6 +90,7 @@ impl From<TintColor> for Color {
TintColor::Accent => Color::Accent,
TintColor::Negative => Color::Error,
TintColor::Warning => Color::Warning,
TintColor::Positive => Color::Success,
}
}
}

View File

@@ -253,8 +253,10 @@ pub enum IconName {
Tab,
Terminal,
TextCursor,
TextSearch,
Trash,
TriangleRight,
Undo,
Update,
WholeWord,
XCircle,
@@ -414,9 +416,11 @@ impl IconName {
IconName::Tab => "icons/tab.svg",
IconName::Terminal => "icons/terminal.svg",
IconName::TextCursor => "icons/text-cursor.svg",
IconName::TextSearch => "icons/text-search.svg",
IconName::Trash => "icons/trash.svg",
IconName::TriangleRight => "icons/triangle_right.svg",
IconName::Update => "icons/update.svg",
IconName::Undo => "icons/undo.svg",
IconName::WholeWord => "icons/word_search.svg",
IconName::XCircle => "icons/error.svg",
IconName::ZedAssistant => "icons/zed_assistant.svg",

View File

@@ -287,6 +287,10 @@ pub trait Item: FocusableView + EventEmitter<Self::Event> {
fn pixel_position_of_cursor(&self, _: &AppContext) -> Option<Point<Pixels>> {
None
}
fn preserve_preview(&self, _cx: &AppContext) -> bool {
false
}
}
pub trait SerializableItem: Item {
@@ -427,6 +431,7 @@ pub trait ItemHandle: 'static + Send {
fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option<Point<Pixels>>;
fn downgrade_item(&self) -> Box<dyn WeakItemHandle>;
fn workspace_settings<'a>(&self, cx: &'a AppContext) -> &'a WorkspaceSettings;
fn preserve_preview(&self, cx: &AppContext) -> bool;
}
pub trait WeakItemHandle: Send + Sync {
@@ -818,6 +823,10 @@ impl<T: Item> ItemHandle for View<T> {
) -> Option<Box<dyn SerializableItemHandle>> {
SerializableItemRegistry::view_to_serializable_item_handle(self.to_any(), cx)
}
fn preserve_preview(&self, cx: &AppContext) -> bool {
self.read(cx).preserve_preview(cx)
}
}
impl From<Box<dyn ItemHandle>> for AnyView {

View File

@@ -237,7 +237,8 @@ pub struct Pane {
Option<Arc<dyn Fn(&mut Pane, &dyn Any, &mut ViewContext<Pane>) -> ControlFlow<(), ()>>>,
can_split: bool,
should_display_tab_bar: Rc<dyn Fn(&ViewContext<Pane>) -> bool>,
render_tab_bar_buttons: Rc<dyn Fn(&mut Pane, &mut ViewContext<Pane>) -> Option<AnyElement>>,
render_tab_bar_buttons:
Rc<dyn Fn(&mut Pane, &mut ViewContext<Pane>) -> (Option<AnyElement>, Option<AnyElement>)>,
_subscriptions: Vec<Subscription>,
tab_bar_scroll_handle: ScrollHandle,
/// Is None if navigation buttons are permanently turned off (and should not react to setting changes).
@@ -357,11 +358,11 @@ impl Pane {
should_display_tab_bar: Rc::new(|cx| TabBarSettings::get_global(cx).show),
render_tab_bar_buttons: Rc::new(move |pane, cx| {
if !pane.has_focus(cx) {
return None;
return (None, None);
}
// Ideally we would return a vec of elements here to pass directly to the [TabBar]'s
// `end_slot`, but due to needing a view here that isn't possible.
h_flex()
let right_children = h_flex()
// Instead we need to replicate the spacing from the [TabBar]'s `end_slot` here.
.gap(Spacing::Small.rems(cx))
.child(
@@ -441,7 +442,8 @@ impl Pane {
el.child(Self::render_menu_overlay(split_item_menu))
})
.into_any_element()
.into()
.into();
(None, right_children)
}),
display_nav_history_buttons: Some(
TabBarSettings::get_global(cx).show_nav_history_buttons,
@@ -586,7 +588,8 @@ impl Pane {
pub fn set_render_tab_bar_buttons<F>(&mut self, cx: &mut ViewContext<Self>, render: F)
where
F: 'static + Fn(&mut Pane, &mut ViewContext<Pane>) -> Option<AnyElement>,
F: 'static
+ Fn(&mut Pane, &mut ViewContext<Pane>) -> (Option<AnyElement>, Option<AnyElement>),
{
self.render_tab_bar_buttons = Rc::new(render);
cx.notify();
@@ -662,6 +665,12 @@ impl Pane {
self.preview_item_id
}
pub fn preview_item(&self) -> Option<Box<dyn ItemHandle>> {
self.preview_item_id
.and_then(|id| self.items.iter().find(|item| item.item_id() == id))
.cloned()
}
fn preview_item_idx(&self) -> Option<usize> {
if let Some(preview_item_id) = self.preview_item_id {
self.items
@@ -685,9 +694,9 @@ impl Pane {
}
pub fn handle_item_edit(&mut self, item_id: EntityId, cx: &AppContext) {
if let Some(preview_item_id) = self.preview_item_id {
if preview_item_id == item_id {
self.set_preview_item_id(None, cx)
if let Some(preview_item) = self.preview_item() {
if preview_item.item_id() == item_id && !preview_item.preserve_preview(cx) {
self.set_preview_item_id(None, cx);
}
}
}
@@ -1892,11 +1901,11 @@ impl Pane {
)
.map(|tab_bar| {
let render_tab_buttons = self.render_tab_bar_buttons.clone();
if let Some(buttons) = render_tab_buttons(self, cx) {
tab_bar.end_child(buttons)
} else {
tab_bar
}
let (left_children, right_children) = render_tab_buttons(self, cx);
tab_bar
.start_children(left_children)
.end_children(right_children)
})
.children(
self.items

View File

@@ -2611,6 +2611,25 @@ impl Workspace {
open_project_item
}
pub fn is_project_item_open<T>(
&self,
pane: &View<Pane>,
project_item: &Model<T::Item>,
cx: &AppContext,
) -> bool
where
T: ProjectItem,
{
use project::Item as _;
project_item
.read(cx)
.entry_id(cx)
.and_then(|entry_id| pane.read(cx).item_for_entry(entry_id, cx))
.and_then(|item| item.downcast::<T>())
.is_some()
}
pub fn open_project_item<T>(
&mut self,
pane: View<Pane>,
@@ -5193,7 +5212,7 @@ fn activate_any_workspace_window(cx: &mut AsyncAppContext) -> Option<WindowHandl
.flatten()
}
fn local_workspace_windows(cx: &AppContext) -> Vec<WindowHandle<Workspace>> {
pub fn local_workspace_windows(cx: &AppContext) -> Vec<WindowHandle<Workspace>> {
cx.windows()
.into_iter()
.filter_map(|window| window.downcast::<Workspace>())

View File

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

View File

@@ -19,7 +19,8 @@ use fs::{Fs, RealFs};
use futures::{future, StreamExt};
use git::GitHostingProviderRegistry;
use gpui::{
App, AppContext, AsyncAppContext, Context, Global, Task, UpdateGlobal as _, VisualContext,
Action, App, AppContext, AsyncAppContext, Context, DismissEvent, Global, Task,
UpdateGlobal as _, VisualContext,
};
use image_viewer;
use language::LanguageRegistry;
@@ -46,7 +47,10 @@ use theme::{ActiveTheme, SystemAppearance, ThemeRegistry, ThemeSettings};
use util::{maybe, parse_env_output, ResultExt, TryFutureExt};
use uuid::Uuid;
use welcome::{show_welcome_view, BaseKeymap, FIRST_OPEN};
use workspace::{AppState, WorkspaceSettings, WorkspaceStore};
use workspace::{
notifications::{simple_message_notification::MessageNotification, NotificationId},
AppState, WorkspaceSettings, WorkspaceStore,
};
use zed::{
app_menus, build_window_options, handle_cli_connection, handle_keymap_file_changes,
initialize_workspace, open_paths_with_positions, OpenListener, OpenRequest,
@@ -425,7 +429,7 @@ fn main() {
OpenListener::set_global(cx, open_listener.clone());
settings::init(cx);
handle_settings_file_changes(user_settings_file_rx, cx);
handle_settings_file_changes(user_settings_file_rx, cx, handle_settings_changed);
handle_keymap_file_changes(user_keymap_file_rx, cx);
client::init_settings(cx);
@@ -539,6 +543,31 @@ fn main() {
});
}
fn handle_settings_changed(result: Result<()>, cx: &mut AppContext) {
struct SettingsParseErrorNotification;
let id = NotificationId::unique::<SettingsParseErrorNotification>();
for workspace in workspace::local_workspace_windows(cx) {
workspace
.update(cx, |workspace, cx| match &result {
Ok(()) => workspace.dismiss_notification(&id, cx),
Err(error) => {
workspace.show_notification(id.clone(), cx, |cx| {
cx.new_view(|_| {
MessageNotification::new(format!("Invalid settings file\n{error}"))
.with_click_message("Open settings file")
.on_click(|cx| {
cx.dispatch_action(zed_actions::OpenSettings.boxed_clone());
cx.emit(DismissEvent);
})
})
});
}
})
.log_err();
}
}
fn handle_open_request(
request: OpenRequest,
app_state: Arc<AppState>,

View File

@@ -3096,7 +3096,7 @@ mod tests {
app_state.fs.clone(),
PathBuf::from("/keymap.json"),
);
handle_settings_file_changes(settings_rx, cx);
handle_settings_file_changes(settings_rx, cx, |_, _| {});
handle_keymap_file_changes(keymap_rx, cx);
});
workspace
@@ -3236,7 +3236,7 @@ mod tests {
PathBuf::from("/keymap.json"),
);
handle_settings_file_changes(settings_rx, cx);
handle_settings_file_changes(settings_rx, cx, |_, _| {});
handle_keymap_file_changes(keymap_rx, cx);
});

View File

@@ -1,3 +1,20 @@
# C#
C# support is available through the [C# extension](https://github.com/zed-industries/zed/tree/main/extensions/csharp).
## Configuration
The `OmniSharp` binary can be configured in a Zed settings file with:
```jsonc
{
"lsp": {
"omnisharp": {
"binary": {
"path": "/path/to/OmniSharp",
"args": ["optional", "additional", "args", "-lsp"],
},
},
},
}
```

View File

@@ -1,23 +1,48 @@
use std::fs;
use zed_extension_api::{self as zed, Result};
use zed_extension_api::{self as zed, settings::LspSettings, LanguageServerId, Result};
struct OmnisharpBinary {
path: String,
args: Option<Vec<String>>,
}
struct CsharpExtension {
cached_binary_path: Option<String>,
}
impl CsharpExtension {
fn language_server_binary_path(
fn language_server_binary(
&mut self,
language_server_id: &zed::LanguageServerId,
language_server_id: &LanguageServerId,
worktree: &zed::Worktree,
) -> Result<String> {
) -> Result<OmnisharpBinary> {
let binary_settings = LspSettings::for_worktree("omnisharp", worktree)
.ok()
.and_then(|lsp_settings| lsp_settings.binary);
let binary_args = binary_settings
.as_ref()
.and_then(|binary_settings| binary_settings.arguments.clone());
if let Some(path) = binary_settings.and_then(|binary_settings| binary_settings.path) {
return Ok(OmnisharpBinary {
path,
args: binary_args,
});
}
if let Some(path) = worktree.which("OmniSharp") {
return Ok(path);
return Ok(OmnisharpBinary {
path,
args: binary_args,
});
}
if let Some(path) = &self.cached_binary_path {
if fs::metadata(path).map_or(false, |stat| stat.is_file()) {
return Ok(path.clone());
if fs::metadata(&path).map_or(false, |stat| stat.is_file()) {
return Ok(OmnisharpBinary {
path: path.clone(),
args: binary_args,
});
}
}
@@ -88,7 +113,10 @@ impl CsharpExtension {
}
self.cached_binary_path = Some(binary_path.clone());
Ok(binary_path)
Ok(OmnisharpBinary {
path: binary_path,
args: binary_args,
})
}
}
@@ -104,9 +132,10 @@ impl zed::Extension for CsharpExtension {
language_server_id: &zed::LanguageServerId,
worktree: &zed::Worktree,
) -> Result<zed::Command> {
let omnisharp_binary = self.language_server_binary(language_server_id, worktree)?;
Ok(zed::Command {
command: self.language_server_binary_path(language_server_id, worktree)?,
args: vec!["-lsp".to_string()],
command: omnisharp_binary.path,
args: omnisharp_binary.args.unwrap_or_else(|| vec!["-lsp".into()]),
env: Default::default(),
})
}

View File

@@ -29,3 +29,5 @@
(visibility_modifier)? @context
"const" @context
name: (_) @name) @item
(statement_comment) @annotation

View File

@@ -1,6 +1,6 @@
[package]
name = "zed_html"
version = "0.1.1"
version = "0.1.2"
edition = "2021"
publish = false
license = "Apache-2.0"

View File

@@ -1,7 +1,7 @@
id = "html"
name = "HTML"
description = "HTML support."
version = "0.1.1"
version = "0.1.2"
schema_version = 1
authors = ["Isaac Clayton <slightknack@gmail.com>"]
repository = "https://github.com/zed-industries/zed"

View File

@@ -0,0 +1 @@
(comment) @annotation

View File

@@ -1,6 +1,6 @@
[package]
name = "zed_php"
version = "0.1.2"
version = "0.1.3"
edition = "2021"
publish = false
license = "Apache-2.0"

View File

@@ -1,7 +1,7 @@
id = "php"
name = "PHP"
description = "PHP support."
version = "0.1.2"
version = "0.1.3"
schema_version = 1
authors = ["Piotr Osiewicz <piotr@zed.dev>"]
repository = "https://github.com/zed-industries/zed"

View File

@@ -42,3 +42,5 @@
)
)
) @item
(comment) @annotation

View File

@@ -52,5 +52,6 @@ else
"assets/fonts/zed-sans"
];
};
PROTOC = "${pkgs.protobuf}/bin/protoc";
};
}