Compare commits
15 Commits
v0.202.8
...
workflow-p
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a51fa34e21 | ||
|
|
7517971013 | ||
|
|
84b1a5c259 | ||
|
|
47d45ee25f | ||
|
|
a4f5d6e0b7 | ||
|
|
3f17c5b13b | ||
|
|
9592a829f5 | ||
|
|
a9c6ef160d | ||
|
|
ccc9c98bc5 | ||
|
|
43dc1e7ac7 | ||
|
|
85d884ef1d | ||
|
|
f65a2b5e18 | ||
|
|
2f39930e58 | ||
|
|
9f567f497d | ||
|
|
9f6da06170 |
10
.github/workflows/ci.yml
vendored
10
.github/workflows/ci.yml
vendored
@@ -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
7
Cargo.lock
generated
@@ -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",
|
||||
]
|
||||
|
||||
1
assets/icons/text-search.svg
Normal file
1
assets/icons/text-search.svg
Normal 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
1
assets/icons/undo.svg
Normal 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 |
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
)?;
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -172,3 +172,7 @@ path = "examples/window_shadow.rs"
|
||||
[[example]]
|
||||
name = "input"
|
||||
path = "examples/input.rs"
|
||||
|
||||
[[example]]
|
||||
name = "shadow"
|
||||
path = "examples/shadow.rs"
|
||||
|
||||
29
crates/gpui/examples/shadow.rs
Normal file
29
crates/gpui/examples/shadow.rs
Normal 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();
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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, |_| {});
|
||||
|
||||
@@ -68,3 +68,5 @@
|
||||
")" @context))))
|
||||
]
|
||||
) @item
|
||||
|
||||
(comment) @annotation
|
||||
|
||||
@@ -147,3 +147,5 @@
|
||||
")" @context)))
|
||||
]
|
||||
(type_qualifier)? @context) @item
|
||||
|
||||
(comment) @annotation
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
(comment) @annotation
|
||||
(type_declaration
|
||||
"type" @context
|
||||
(type_spec
|
||||
|
||||
@@ -71,3 +71,5 @@
|
||||
)
|
||||
)
|
||||
) @item
|
||||
|
||||
(comment) @annotation
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
(decorator) @annotation
|
||||
|
||||
(class_definition
|
||||
"class" @context
|
||||
name: (identifier) @name
|
||||
|
||||
@@ -74,3 +74,5 @@
|
||||
)
|
||||
)
|
||||
) @item
|
||||
|
||||
(comment) @annotation
|
||||
|
||||
@@ -74,3 +74,5 @@
|
||||
)
|
||||
)
|
||||
) @item
|
||||
|
||||
(comment) @annotation
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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)
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>())
|
||||
|
||||
@@ -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>"]
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
@@ -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"],
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
@@ -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(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -29,3 +29,5 @@
|
||||
(visibility_modifier)? @context
|
||||
"const" @context
|
||||
name: (_) @name) @item
|
||||
|
||||
(statement_comment) @annotation
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "zed_html"
|
||||
version = "0.1.1"
|
||||
version = "0.1.2"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
license = "Apache-2.0"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
(comment) @annotation
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "zed_php"
|
||||
version = "0.1.2"
|
||||
version = "0.1.3"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
license = "Apache-2.0"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -42,3 +42,5 @@
|
||||
)
|
||||
)
|
||||
) @item
|
||||
|
||||
(comment) @annotation
|
||||
|
||||
Reference in New Issue
Block a user