Compare commits
21 Commits
v0.182.7-p
...
breakpoint
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
551537b191 | ||
|
|
1d758d47cc | ||
|
|
2752c08810 | ||
|
|
780143298a | ||
|
|
088d7c1342 | ||
|
|
64de6bd2a8 | ||
|
|
6aa0248ab3 | ||
|
|
342134fbab | ||
|
|
b47aa33459 | ||
|
|
9f6c5e2877 | ||
|
|
7bf6cd4ccf | ||
|
|
c7963c8a93 | ||
|
|
dd4629433b | ||
|
|
2e56935997 | ||
|
|
e43a397f1d | ||
|
|
9d0fe164a7 | ||
|
|
6d7fef6fd3 | ||
|
|
b67d3fd21b | ||
|
|
1cb4f8288d | ||
|
|
3a8fe4d973 | ||
|
|
9d6d152918 |
6
.github/workflows/ci.yml
vendored
6
.github/workflows/ci.yml
vendored
@@ -465,6 +465,7 @@ jobs:
|
||||
- job_spec
|
||||
- style
|
||||
- migration_checks
|
||||
# run_tests: If adding required tests, add them here and to script below.
|
||||
- workspace_hack
|
||||
- linux_tests
|
||||
- build_remote_server
|
||||
@@ -482,11 +483,14 @@ jobs:
|
||||
|
||||
# Only check test jobs if they were supposed to run
|
||||
if [[ "${{ needs.job_spec.outputs.run_tests }}" == "true" ]]; then
|
||||
[[ "${{ needs.workspace_hack.result }}" != 'success' ]] && { RET_CODE=1; echo "Workspace Hack failed"; }
|
||||
[[ "${{ needs.macos_tests.result }}" != 'success' ]] && { RET_CODE=1; echo "macOS tests failed"; }
|
||||
[[ "${{ needs.linux_tests.result }}" != 'success' ]] && { RET_CODE=1; echo "Linux tests failed"; }
|
||||
[[ "${{ needs.windows_tests.result }}" != 'success' ]] && { RET_CODE=1; echo "Windows tests failed"; }
|
||||
[[ "${{ needs.windows_clippy.result }}" != 'success' ]] && { RET_CODE=1; echo "Windows clippy failed"; }
|
||||
[[ "${{ needs.build_remote_server.result }}" != 'success' ]] && { RET_CODE=1; echo "Remote server build failed"; }
|
||||
# This check is intentionally disabled. See: https://github.com/zed-industries/zed/pull/28431
|
||||
# [[ "${{ needs.migration_checks.result }}" != 'success' ]] && { RET_CODE=1; echo "Migration Checks failed"; }
|
||||
fi
|
||||
if [[ "$RET_CODE" -eq 0 ]]; then
|
||||
echo "All tests passed successfully!"
|
||||
@@ -739,7 +743,7 @@ jobs:
|
||||
echo "/nix/var/nix/profiles/default/bin" >> $GITHUB_PATH
|
||||
echo "/Users/administrator/.nix-profile/bin" >> $GITHUB_PATH
|
||||
|
||||
- uses: cachix/install-nix-action@02a151ada4993995686f9ed4f1be7cfbb229e56f # v31
|
||||
- uses: cachix/install-nix-action@d1ca217b388ee87b2507a9a93bf01368bde7cec2 # v31
|
||||
if: ${{ matrix.system.install_nix }}
|
||||
with:
|
||||
github_access_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
2
.github/workflows/release_nightly.yml
vendored
2
.github/workflows/release_nightly.yml
vendored
@@ -206,7 +206,7 @@ jobs:
|
||||
echo "/nix/var/nix/profiles/default/bin" >> $GITHUB_PATH
|
||||
echo "/Users/administrator/.nix-profile/bin" >> $GITHUB_PATH
|
||||
|
||||
- uses: cachix/install-nix-action@02a151ada4993995686f9ed4f1be7cfbb229e56f # v31
|
||||
- uses: cachix/install-nix-action@d1ca217b388ee87b2507a9a93bf01368bde7cec2 # v31
|
||||
if: ${{ matrix.system.install_nix }}
|
||||
with:
|
||||
github_access_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
[
|
||||
{
|
||||
"label": "Debug Zed with LLDB",
|
||||
"adapter": "LLDB",
|
||||
"label": "Debug Zed (CodeLLDB)",
|
||||
"adapter": "CodeLLDB",
|
||||
"program": "$ZED_WORKTREE_ROOT/target/debug/zed",
|
||||
"request": "launch",
|
||||
"cwd": "$ZED_WORKTREE_ROOT"
|
||||
},
|
||||
{
|
||||
"label": "Debug Zed with GDB",
|
||||
"label": "Debug Zed (GDB)",
|
||||
"adapter": "GDB",
|
||||
"program": "$ZED_WORKTREE_ROOT/target/debug/zed",
|
||||
"request": "launch",
|
||||
|
||||
3
Cargo.lock
generated
3
Cargo.lock
generated
@@ -4600,6 +4600,7 @@ dependencies = [
|
||||
"client",
|
||||
"clock",
|
||||
"collections",
|
||||
"command_palette_hooks",
|
||||
"convert_case 0.8.0",
|
||||
"ctor",
|
||||
"db",
|
||||
@@ -18105,7 +18106,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed"
|
||||
version = "0.182.0"
|
||||
version = "0.183.0"
|
||||
dependencies = [
|
||||
"activity_indicator",
|
||||
"agent",
|
||||
|
||||
1
assets/icons/file_icons/vyper.svg
Normal file
1
assets/icons/file_icons/vyper.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="16" height="16" fill="none" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><g style="fill:#000;fill-opacity:1" fill="#180c25"><path d="m-116.1-101.4-28.9-28.9a6.7 6.7 0 0 1-1.8-4.7v-41.2c0-2.4-2.4-4.8-4.8-4.8h-9.6a5.2 5.2 0 0 0-4.8 4.8v48c0 2.5 1 5 2.7 6.8l33.6 33.6a9.6 9.6 0 0 0 6.8 2.8h4.8c2.7 0 4.8-2.2 4.8-4.8v-4.8c0-2.5-1-5-2.8-6.8zM-79.6-176.2c0-2.4-2.4-4.8-4.8-4.8h-9.7a5.2 5.2 0 0 0-4.7 4.8v41.2c0 1.8-.8 3.5-2 4.7l-9.6 9.7a9.5 9.5 0 0 0-2.8 6.8v4.8c0 2.6 2.1 4.7 4.8 4.7h4.8c2.4 0 4.9-.9 6.7-2.8l14.4-14.3a9.6 9.6 0 0 0 2.8-6.8v-48z" style="fill:#000;fill-opacity:1;stroke-width:.255894" transform="translate(21.6 22.7) scale(.11067)"/></g></svg>
|
||||
|
After Width: | Height: | Size: 677 B |
@@ -58,6 +58,12 @@
|
||||
"ctrl-r": "outline::Toggle"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && !agent_diff",
|
||||
"bindings": {
|
||||
"ctrl-k ctrl-z": "git::Restore"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Pane",
|
||||
"bindings": {
|
||||
|
||||
@@ -60,6 +60,12 @@
|
||||
"cmd-r": "outline::Toggle"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && !agent_diff",
|
||||
"bindings": {
|
||||
"cmd-k cmd-z": "git::Restore"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Pane",
|
||||
"bindings": {
|
||||
|
||||
1
assets/prompts/assistant_system_prompt_reminder.hbs
Normal file
1
assets/prompts/assistant_system_prompt_reminder.hbs
Normal file
@@ -0,0 +1 @@
|
||||
In your response, make sure to remember and follow my instructions about how to format code blocks (and don't mention that you are remembering it, just follow the instructions).
|
||||
@@ -62,7 +62,7 @@ pub struct ActiveThread {
|
||||
copied_code_block_ids: HashSet<(MessageId, usize)>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
notification_subscriptions: HashMap<WindowHandle<AgentNotification>, Vec<Subscription>>,
|
||||
feedback_message_editor: Option<Entity<Editor>>,
|
||||
open_feedback_editors: HashMap<MessageId, Entity<Editor>>,
|
||||
}
|
||||
|
||||
struct RenderedMessage {
|
||||
@@ -636,7 +636,7 @@ impl ActiveThread {
|
||||
notifications: Vec::new(),
|
||||
_subscriptions: subscriptions,
|
||||
notification_subscriptions: HashMap::default(),
|
||||
feedback_message_editor: None,
|
||||
open_feedback_editors: HashMap::default(),
|
||||
};
|
||||
|
||||
for message in thread.read(cx).messages().cloned().collect::<Vec<_>>() {
|
||||
@@ -939,7 +939,7 @@ impl ActiveThread {
|
||||
|this, _, event, window, cx| match event {
|
||||
AgentNotificationEvent::Accepted => {
|
||||
let handle = window.window_handle();
|
||||
cx.activate(true); // Switch back to the Zed application
|
||||
cx.activate(true);
|
||||
|
||||
let workspace_handle = this.workspace.clone();
|
||||
|
||||
@@ -1111,34 +1111,37 @@ impl ActiveThread {
|
||||
|
||||
fn handle_feedback_click(
|
||||
&mut self,
|
||||
message_id: MessageId,
|
||||
feedback: ThreadFeedback,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let report = self.thread.update(cx, |thread, cx| {
|
||||
thread.report_message_feedback(message_id, feedback, cx)
|
||||
});
|
||||
|
||||
cx.spawn(async move |this, cx| {
|
||||
report.await?;
|
||||
this.update(cx, |_this, cx| cx.notify())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
|
||||
match feedback {
|
||||
ThreadFeedback::Positive => {
|
||||
let report = self
|
||||
.thread
|
||||
.update(cx, |thread, cx| thread.report_feedback(feedback, cx));
|
||||
|
||||
let this = cx.entity().downgrade();
|
||||
cx.spawn(async move |_, cx| {
|
||||
report.await?;
|
||||
this.update(cx, |_this, cx| cx.notify())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
self.open_feedback_editors.remove(&message_id);
|
||||
}
|
||||
ThreadFeedback::Negative => {
|
||||
self.handle_show_feedback_comments(window, cx);
|
||||
self.handle_show_feedback_comments(message_id, window, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_show_feedback_comments(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if self.feedback_message_editor.is_some() {
|
||||
return;
|
||||
}
|
||||
|
||||
fn handle_show_feedback_comments(
|
||||
&mut self,
|
||||
message_id: MessageId,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let buffer = cx.new(|cx| {
|
||||
let empty_string = String::new();
|
||||
MultiBuffer::singleton(cx.new(|cx| Buffer::local(empty_string, cx)), cx)
|
||||
@@ -1160,34 +1163,47 @@ impl ActiveThread {
|
||||
});
|
||||
|
||||
editor.read(cx).focus_handle(cx).focus(window);
|
||||
self.feedback_message_editor = Some(editor);
|
||||
self.open_feedback_editors.insert(message_id, editor);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn submit_feedback_message(&mut self, cx: &mut Context<Self>) {
|
||||
let Some(editor) = self.feedback_message_editor.clone() else {
|
||||
fn submit_feedback_message(&mut self, message_id: MessageId, cx: &mut Context<Self>) {
|
||||
let Some(editor) = self.open_feedback_editors.get(&message_id) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let report_task = self.thread.update(cx, |thread, cx| {
|
||||
thread.report_feedback(ThreadFeedback::Negative, cx)
|
||||
thread.report_message_feedback(message_id, ThreadFeedback::Negative, cx)
|
||||
});
|
||||
|
||||
let comments = editor.read(cx).text(cx);
|
||||
if !comments.is_empty() {
|
||||
let thread_id = self.thread.read(cx).id().clone();
|
||||
let comments_value = String::from(comments.as_str());
|
||||
|
||||
telemetry::event!("Assistant Thread Feedback Comments", thread_id, comments);
|
||||
let message_content = self
|
||||
.thread
|
||||
.read(cx)
|
||||
.message(message_id)
|
||||
.map(|msg| msg.to_string())
|
||||
.unwrap_or_default();
|
||||
|
||||
telemetry::event!(
|
||||
"Assistant Thread Feedback Comments",
|
||||
thread_id,
|
||||
message_id = message_id.0,
|
||||
message_content,
|
||||
comments = comments_value
|
||||
);
|
||||
|
||||
self.open_feedback_editors.remove(&message_id);
|
||||
|
||||
cx.spawn(async move |this, cx| {
|
||||
report_task.await?;
|
||||
this.update(cx, |_this, cx| cx.notify())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
self.feedback_message_editor = None;
|
||||
|
||||
let this = cx.entity().downgrade();
|
||||
cx.spawn(async move |_, cx| {
|
||||
report_task.await?;
|
||||
this.update(cx, |_this, cx| cx.notify())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
fn render_message(&self, ix: usize, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
|
||||
@@ -1214,7 +1230,18 @@ impl ActiveThread {
|
||||
|
||||
let is_first_message = ix == 0;
|
||||
let is_last_message = ix == self.messages.len() - 1;
|
||||
let show_feedback = is_last_message && message.role != Role::User;
|
||||
|
||||
let show_feedback = (!is_generating && is_last_message && message.role != Role::User)
|
||||
|| self.messages.get(ix + 1).map_or(false, |next_id| {
|
||||
self.thread
|
||||
.read(cx)
|
||||
.message(*next_id)
|
||||
.map_or(false, |next_message| {
|
||||
next_message.role == Role::User
|
||||
&& thread.tool_uses_for_message(*next_id, cx).is_empty()
|
||||
&& thread.tool_results_for_message(*next_id).is_empty()
|
||||
})
|
||||
});
|
||||
|
||||
let needs_confirmation = tool_uses.iter().any(|tool_use| tool_use.needs_confirmation);
|
||||
|
||||
@@ -1287,8 +1314,9 @@ impl ActiveThread {
|
||||
let editor_bg_color = colors.editor_background;
|
||||
let bg_user_message_header = editor_bg_color.blend(active_color.opacity(0.25));
|
||||
|
||||
let feedback_container = h_flex().pt_2().pb_4().px_4().gap_1().justify_between();
|
||||
let feedback_items = match self.thread.read(cx).feedback() {
|
||||
let feedback_container = h_flex().py_2().px_4().gap_1().justify_between();
|
||||
|
||||
let feedback_items = match self.thread.read(cx).message_feedback(message_id) {
|
||||
Some(feedback) => feedback_container
|
||||
.child(
|
||||
Label::new(match feedback {
|
||||
@@ -1302,18 +1330,20 @@ impl ActiveThread {
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.pr_1()
|
||||
.gap_1()
|
||||
.child(
|
||||
IconButton::new("feedback-thumbs-up", IconName::ThumbsUp)
|
||||
IconButton::new(("feedback-thumbs-up", ix), IconName::ThumbsUp)
|
||||
.shape(ui::IconButtonShape::Square)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_color(match feedback {
|
||||
ThreadFeedback::Positive => Color::Accent,
|
||||
ThreadFeedback::Negative => Color::Ignored,
|
||||
})
|
||||
.shape(ui::IconButtonShape::Square)
|
||||
.tooltip(Tooltip::text("Helpful Response"))
|
||||
.on_click(cx.listener(move |this, _, window, cx| {
|
||||
this.handle_feedback_click(
|
||||
message_id,
|
||||
ThreadFeedback::Positive,
|
||||
window,
|
||||
cx,
|
||||
@@ -1321,16 +1351,17 @@ impl ActiveThread {
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("feedback-thumbs-down", IconName::ThumbsDown)
|
||||
IconButton::new(("feedback-thumbs-down", ix), IconName::ThumbsDown)
|
||||
.shape(ui::IconButtonShape::Square)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_color(match feedback {
|
||||
ThreadFeedback::Positive => Color::Ignored,
|
||||
ThreadFeedback::Negative => Color::Accent,
|
||||
})
|
||||
.shape(ui::IconButtonShape::Square)
|
||||
.tooltip(Tooltip::text("Not Helpful"))
|
||||
.on_click(cx.listener(move |this, _, window, cx| {
|
||||
this.handle_feedback_click(
|
||||
message_id,
|
||||
ThreadFeedback::Negative,
|
||||
window,
|
||||
cx,
|
||||
@@ -1351,13 +1382,14 @@ impl ActiveThread {
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
IconButton::new("feedback-thumbs-up", IconName::ThumbsUp)
|
||||
IconButton::new(("feedback-thumbs-up", ix), IconName::ThumbsUp)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_color(Color::Ignored)
|
||||
.shape(ui::IconButtonShape::Square)
|
||||
.tooltip(Tooltip::text("Helpful Response"))
|
||||
.on_click(cx.listener(move |this, _, window, cx| {
|
||||
this.handle_feedback_click(
|
||||
message_id,
|
||||
ThreadFeedback::Positive,
|
||||
window,
|
||||
cx,
|
||||
@@ -1365,13 +1397,14 @@ impl ActiveThread {
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("feedback-thumbs-down", IconName::ThumbsDown)
|
||||
IconButton::new(("feedback-thumbs-down", ix), IconName::ThumbsDown)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_color(Color::Ignored)
|
||||
.shape(ui::IconButtonShape::Square)
|
||||
.tooltip(Tooltip::text("Not Helpful"))
|
||||
.on_click(cx.listener(move |this, _, window, cx| {
|
||||
this.handle_feedback_click(
|
||||
message_id,
|
||||
ThreadFeedback::Negative,
|
||||
window,
|
||||
cx,
|
||||
@@ -1669,31 +1702,31 @@ impl ActiveThread {
|
||||
.child(generating_label.unwrap()),
|
||||
)
|
||||
})
|
||||
.when(show_feedback && !is_generating, |parent| {
|
||||
.when(show_feedback, move |parent| {
|
||||
parent.child(feedback_items).when_some(
|
||||
self.feedback_message_editor.clone(),
|
||||
|parent, feedback_editor| {
|
||||
self.open_feedback_editors.get(&message_id),
|
||||
move |parent, feedback_editor| {
|
||||
let focus_handle = feedback_editor.focus_handle(cx);
|
||||
parent.child(
|
||||
v_flex()
|
||||
.key_context("AgentFeedbackMessageEditor")
|
||||
.on_action(cx.listener(|this, _: &menu::Cancel, _, cx| {
|
||||
this.feedback_message_editor = None;
|
||||
.on_action(cx.listener(move |this, _: &menu::Cancel, _, cx| {
|
||||
this.open_feedback_editors.remove(&message_id);
|
||||
cx.notify();
|
||||
}))
|
||||
.on_action(cx.listener(|this, _: &menu::Confirm, _, cx| {
|
||||
this.submit_feedback_message(cx);
|
||||
.on_action(cx.listener(move |this, _: &menu::Confirm, _, cx| {
|
||||
this.submit_feedback_message(message_id, cx);
|
||||
cx.notify();
|
||||
}))
|
||||
.on_action(cx.listener(Self::confirm_editing_message))
|
||||
.my_3()
|
||||
.mb_2()
|
||||
.mx_4()
|
||||
.p_2()
|
||||
.rounded_md()
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.child(feedback_editor)
|
||||
.child(feedback_editor.clone())
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
@@ -1710,10 +1743,13 @@ impl ActiveThread {
|
||||
)
|
||||
.map(|kb| kb.size(rems_from_px(10.))),
|
||||
)
|
||||
.on_click(cx.listener(|this, _, _, cx| {
|
||||
this.feedback_message_editor = None;
|
||||
cx.notify();
|
||||
})),
|
||||
.on_click(cx.listener(
|
||||
move |this, _, _window, cx| {
|
||||
this.open_feedback_editors
|
||||
.remove(&message_id);
|
||||
cx.notify();
|
||||
},
|
||||
)),
|
||||
)
|
||||
.child(
|
||||
Button::new(
|
||||
@@ -1732,9 +1768,9 @@ impl ActiveThread {
|
||||
.map(|kb| kb.size(rems_from_px(10.))),
|
||||
)
|
||||
.on_click(
|
||||
cx.listener(|this, _, _, cx| {
|
||||
this.submit_feedback_message(cx);
|
||||
cx.notify();
|
||||
cx.listener(move |this, _, _window, cx| {
|
||||
this.submit_feedback_message(message_id, cx);
|
||||
cx.notify()
|
||||
}),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1624,7 +1624,21 @@ impl prompt_library::InlineAssistDelegate for PromptLibraryInlineAssist {
|
||||
cx: &mut Context<PromptLibrary>,
|
||||
) {
|
||||
InlineAssistant::update_global(cx, |assistant, cx| {
|
||||
assistant.assist(&prompt_editor, self.workspace.clone(), None, window, cx)
|
||||
let Some(project) = self
|
||||
.workspace
|
||||
.upgrade()
|
||||
.map(|workspace| workspace.read(cx).project().downgrade())
|
||||
else {
|
||||
return;
|
||||
};
|
||||
assistant.assist(
|
||||
&prompt_editor,
|
||||
self.workspace.clone(),
|
||||
project,
|
||||
None,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -18,16 +18,133 @@ use text::{Anchor, ToPoint};
|
||||
use ui::prelude::*;
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::context::AssistantContext;
|
||||
use crate::context_picker::file_context_picker::search_files;
|
||||
use crate::context_picker::symbol_context_picker::search_symbols;
|
||||
use crate::context_store::ContextStore;
|
||||
use crate::thread_store::ThreadStore;
|
||||
|
||||
use super::fetch_context_picker::fetch_url_content;
|
||||
use super::thread_context_picker::ThreadContextEntry;
|
||||
use super::file_context_picker::FileMatch;
|
||||
use super::symbol_context_picker::SymbolMatch;
|
||||
use super::thread_context_picker::{ThreadContextEntry, ThreadMatch, search_threads};
|
||||
use super::{
|
||||
ContextPickerMode, MentionLink, recent_context_picker_entries, supported_context_picker_modes,
|
||||
ContextPickerMode, MentionLink, RecentEntry, recent_context_picker_entries,
|
||||
supported_context_picker_modes,
|
||||
};
|
||||
|
||||
pub(crate) enum Match {
|
||||
Symbol(SymbolMatch),
|
||||
File(FileMatch),
|
||||
Thread(ThreadMatch),
|
||||
Fetch(SharedString),
|
||||
Mode(ContextPickerMode),
|
||||
}
|
||||
|
||||
fn search(
|
||||
mode: Option<ContextPickerMode>,
|
||||
query: String,
|
||||
cancellation_flag: Arc<AtomicBool>,
|
||||
recent_entries: Vec<RecentEntry>,
|
||||
thread_store: Option<WeakEntity<ThreadStore>>,
|
||||
workspace: Entity<Workspace>,
|
||||
cx: &mut App,
|
||||
) -> Task<Vec<Match>> {
|
||||
match mode {
|
||||
Some(ContextPickerMode::File) => {
|
||||
let search_files_task =
|
||||
search_files(query.clone(), cancellation_flag.clone(), &workspace, cx);
|
||||
cx.background_spawn(async move {
|
||||
search_files_task
|
||||
.await
|
||||
.into_iter()
|
||||
.map(Match::File)
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
Some(ContextPickerMode::Symbol) => {
|
||||
let search_symbols_task =
|
||||
search_symbols(query.clone(), cancellation_flag.clone(), &workspace, cx);
|
||||
cx.background_spawn(async move {
|
||||
search_symbols_task
|
||||
.await
|
||||
.into_iter()
|
||||
.map(Match::Symbol)
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
Some(ContextPickerMode::Thread) => {
|
||||
if let Some(thread_store) = thread_store.as_ref().and_then(|t| t.upgrade()) {
|
||||
let search_threads_task =
|
||||
search_threads(query.clone(), cancellation_flag.clone(), thread_store, cx);
|
||||
cx.background_spawn(async move {
|
||||
search_threads_task
|
||||
.await
|
||||
.into_iter()
|
||||
.map(Match::Thread)
|
||||
.collect()
|
||||
})
|
||||
} else {
|
||||
Task::ready(Vec::new())
|
||||
}
|
||||
}
|
||||
Some(ContextPickerMode::Fetch) => {
|
||||
if !query.is_empty() {
|
||||
Task::ready(vec![Match::Fetch(query.into())])
|
||||
} else {
|
||||
Task::ready(Vec::new())
|
||||
}
|
||||
}
|
||||
None => {
|
||||
if query.is_empty() {
|
||||
let mut matches = recent_entries
|
||||
.into_iter()
|
||||
.map(|entry| match entry {
|
||||
super::RecentEntry::File {
|
||||
project_path,
|
||||
path_prefix,
|
||||
} => Match::File(FileMatch {
|
||||
mat: fuzzy::PathMatch {
|
||||
score: 1.,
|
||||
positions: Vec::new(),
|
||||
worktree_id: project_path.worktree_id.to_usize(),
|
||||
path: project_path.path,
|
||||
path_prefix,
|
||||
is_dir: false,
|
||||
distance_to_relative_ancestor: 0,
|
||||
},
|
||||
is_recent: true,
|
||||
}),
|
||||
super::RecentEntry::Thread(thread_context_entry) => {
|
||||
Match::Thread(ThreadMatch {
|
||||
thread: thread_context_entry,
|
||||
is_recent: true,
|
||||
})
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
matches.extend(
|
||||
supported_context_picker_modes(&thread_store)
|
||||
.into_iter()
|
||||
.map(Match::Mode),
|
||||
);
|
||||
|
||||
Task::ready(matches)
|
||||
} else {
|
||||
let search_files_task =
|
||||
search_files(query.clone(), cancellation_flag.clone(), &workspace, cx);
|
||||
cx.background_spawn(async move {
|
||||
search_files_task
|
||||
.await
|
||||
.into_iter()
|
||||
.map(Match::File)
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ContextPickerCompletionProvider {
|
||||
workspace: WeakEntity<Workspace>,
|
||||
context_store: WeakEntity<ContextStore>,
|
||||
@@ -50,97 +167,20 @@ impl ContextPickerCompletionProvider {
|
||||
}
|
||||
}
|
||||
|
||||
fn default_completions(
|
||||
excerpt_id: ExcerptId,
|
||||
source_range: Range<Anchor>,
|
||||
context_store: Entity<ContextStore>,
|
||||
thread_store: Option<WeakEntity<ThreadStore>>,
|
||||
editor: Entity<Editor>,
|
||||
workspace: Entity<Workspace>,
|
||||
cx: &App,
|
||||
) -> Vec<Completion> {
|
||||
let mut completions = Vec::new();
|
||||
|
||||
completions.extend(
|
||||
recent_context_picker_entries(
|
||||
context_store.clone(),
|
||||
thread_store.clone(),
|
||||
workspace.clone(),
|
||||
cx,
|
||||
)
|
||||
.iter()
|
||||
.filter_map(|entry| match entry {
|
||||
super::RecentEntry::File {
|
||||
project_path,
|
||||
path_prefix,
|
||||
} => Some(Self::completion_for_path(
|
||||
project_path.clone(),
|
||||
path_prefix,
|
||||
true,
|
||||
false,
|
||||
excerpt_id,
|
||||
source_range.clone(),
|
||||
editor.clone(),
|
||||
context_store.clone(),
|
||||
cx,
|
||||
)),
|
||||
super::RecentEntry::Thread(thread_context_entry) => {
|
||||
let thread_store = thread_store
|
||||
.as_ref()
|
||||
.and_then(|thread_store| thread_store.upgrade())?;
|
||||
Some(Self::completion_for_thread(
|
||||
thread_context_entry.clone(),
|
||||
excerpt_id,
|
||||
source_range.clone(),
|
||||
true,
|
||||
editor.clone(),
|
||||
context_store.clone(),
|
||||
thread_store,
|
||||
))
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
completions.extend(
|
||||
supported_context_picker_modes(&thread_store)
|
||||
.iter()
|
||||
.map(|mode| {
|
||||
Completion {
|
||||
replace_range: source_range.clone(),
|
||||
new_text: format!("@{} ", mode.mention_prefix()),
|
||||
label: CodeLabel::plain(mode.label().to_string(), None),
|
||||
icon_path: Some(mode.icon().path().into()),
|
||||
documentation: None,
|
||||
source: project::CompletionSource::Custom,
|
||||
insert_text_mode: None,
|
||||
// This ensures that when a user accepts this completion, the
|
||||
// completion menu will still be shown after "@category " is
|
||||
// inserted
|
||||
confirm: Some(Arc::new(|_, _, _| true)),
|
||||
}
|
||||
}),
|
||||
);
|
||||
completions
|
||||
}
|
||||
|
||||
fn build_code_label_for_full_path(
|
||||
file_name: &str,
|
||||
directory: Option<&str>,
|
||||
cx: &App,
|
||||
) -> CodeLabel {
|
||||
let comment_id = cx.theme().syntax().highlight_id("comment").map(HighlightId);
|
||||
let mut label = CodeLabel::default();
|
||||
|
||||
label.push_str(&file_name, None);
|
||||
label.push_str(" ", None);
|
||||
|
||||
if let Some(directory) = directory {
|
||||
label.push_str(&directory, comment_id);
|
||||
fn completion_for_mode(source_range: Range<Anchor>, mode: ContextPickerMode) -> Completion {
|
||||
Completion {
|
||||
replace_range: source_range.clone(),
|
||||
new_text: format!("@{} ", mode.mention_prefix()),
|
||||
label: CodeLabel::plain(mode.label().to_string(), None),
|
||||
icon_path: Some(mode.icon().path().into()),
|
||||
documentation: None,
|
||||
source: project::CompletionSource::Custom,
|
||||
insert_text_mode: None,
|
||||
// This ensures that when a user accepts this completion, the
|
||||
// completion menu will still be shown after "@category " is
|
||||
// inserted
|
||||
confirm: Some(Arc::new(|_, _, _| true)),
|
||||
}
|
||||
|
||||
label.filter_range = 0..label.text().len();
|
||||
|
||||
label
|
||||
}
|
||||
|
||||
fn completion_for_thread(
|
||||
@@ -261,11 +301,8 @@ impl ContextPickerCompletionProvider {
|
||||
path_prefix,
|
||||
);
|
||||
|
||||
let label = Self::build_code_label_for_full_path(
|
||||
&file_name,
|
||||
directory.as_ref().map(|s| s.as_ref()),
|
||||
cx,
|
||||
);
|
||||
let label =
|
||||
build_code_label_for_full_path(&file_name, directory.as_ref().map(|s| s.as_ref()), cx);
|
||||
let full_path = if let Some(directory) = directory {
|
||||
format!("{}{}", directory, file_name)
|
||||
} else {
|
||||
@@ -382,6 +419,22 @@ impl ContextPickerCompletionProvider {
|
||||
}
|
||||
}
|
||||
|
||||
fn build_code_label_for_full_path(file_name: &str, directory: Option<&str>, cx: &App) -> CodeLabel {
|
||||
let comment_id = cx.theme().syntax().highlight_id("comment").map(HighlightId);
|
||||
let mut label = CodeLabel::default();
|
||||
|
||||
label.push_str(&file_name, None);
|
||||
label.push_str(" ", None);
|
||||
|
||||
if let Some(directory) = directory {
|
||||
label.push_str(&directory, comment_id);
|
||||
}
|
||||
|
||||
label.filter_range = 0..label.text().len();
|
||||
|
||||
label
|
||||
}
|
||||
|
||||
impl CompletionProvider for ContextPickerCompletionProvider {
|
||||
fn completions(
|
||||
&self,
|
||||
@@ -404,10 +457,9 @@ impl CompletionProvider for ContextPickerCompletionProvider {
|
||||
return Task::ready(Ok(None));
|
||||
};
|
||||
|
||||
let Some(workspace) = self.workspace.upgrade() else {
|
||||
return Task::ready(Ok(None));
|
||||
};
|
||||
let Some(context_store) = self.context_store.upgrade() else {
|
||||
let Some((workspace, context_store)) =
|
||||
self.workspace.upgrade().zip(self.context_store.upgrade())
|
||||
else {
|
||||
return Task::ready(Ok(None));
|
||||
};
|
||||
|
||||
@@ -419,154 +471,89 @@ impl CompletionProvider for ContextPickerCompletionProvider {
|
||||
let editor = self.editor.clone();
|
||||
let http_client = workspace.read(cx).client().http_client().clone();
|
||||
|
||||
let MentionCompletion { mode, argument, .. } = state;
|
||||
let query = argument.unwrap_or_else(|| "".to_string());
|
||||
|
||||
let recent_entries = recent_context_picker_entries(
|
||||
context_store.clone(),
|
||||
thread_store.clone(),
|
||||
workspace.clone(),
|
||||
cx,
|
||||
);
|
||||
|
||||
let search_task = search(
|
||||
mode,
|
||||
query,
|
||||
Arc::<AtomicBool>::default(),
|
||||
recent_entries,
|
||||
thread_store.clone(),
|
||||
workspace.clone(),
|
||||
cx,
|
||||
);
|
||||
|
||||
cx.spawn(async move |_, cx| {
|
||||
let mut completions = Vec::new();
|
||||
let matches = search_task.await;
|
||||
let Some(editor) = editor.upgrade() else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let MentionCompletion { mode, argument, .. } = state;
|
||||
|
||||
let query = argument.unwrap_or_else(|| "".to_string());
|
||||
match mode {
|
||||
Some(ContextPickerMode::File) => {
|
||||
let path_matches = cx
|
||||
.update(|cx| {
|
||||
super::file_context_picker::search_paths(
|
||||
query,
|
||||
Arc::<AtomicBool>::default(),
|
||||
&workspace,
|
||||
cx,
|
||||
)
|
||||
})?
|
||||
.await;
|
||||
|
||||
if let Some(editor) = editor.upgrade() {
|
||||
completions.reserve(path_matches.len());
|
||||
cx.update(|cx| {
|
||||
completions.extend(path_matches.iter().map(|mat| {
|
||||
Self::completion_for_path(
|
||||
ProjectPath {
|
||||
worktree_id: WorktreeId::from_usize(mat.worktree_id),
|
||||
path: mat.path.clone(),
|
||||
},
|
||||
&mat.path_prefix,
|
||||
false,
|
||||
mat.is_dir,
|
||||
excerpt_id,
|
||||
source_range.clone(),
|
||||
editor.clone(),
|
||||
context_store.clone(),
|
||||
cx,
|
||||
)
|
||||
}));
|
||||
})?;
|
||||
}
|
||||
}
|
||||
Some(ContextPickerMode::Symbol) => {
|
||||
if let Some(editor) = editor.upgrade() {
|
||||
let symbol_matches = cx
|
||||
.update(|cx| {
|
||||
super::symbol_context_picker::search_symbols(
|
||||
query,
|
||||
Arc::new(AtomicBool::default()),
|
||||
&workspace,
|
||||
cx,
|
||||
)
|
||||
})?
|
||||
.await?;
|
||||
cx.update(|cx| {
|
||||
completions.extend(symbol_matches.into_iter().filter_map(
|
||||
|(_, symbol)| {
|
||||
Self::completion_for_symbol(
|
||||
symbol,
|
||||
excerpt_id,
|
||||
source_range.clone(),
|
||||
editor.clone(),
|
||||
context_store.clone(),
|
||||
workspace.clone(),
|
||||
cx,
|
||||
)
|
||||
Ok(Some(cx.update(|cx| {
|
||||
matches
|
||||
.into_iter()
|
||||
.filter_map(|mat| match mat {
|
||||
Match::File(FileMatch { mat, is_recent }) => {
|
||||
Some(Self::completion_for_path(
|
||||
ProjectPath {
|
||||
worktree_id: WorktreeId::from_usize(mat.worktree_id),
|
||||
path: mat.path.clone(),
|
||||
},
|
||||
));
|
||||
})?;
|
||||
}
|
||||
}
|
||||
Some(ContextPickerMode::Fetch) => {
|
||||
if let Some(editor) = editor.upgrade() {
|
||||
if !query.is_empty() {
|
||||
completions.push(Self::completion_for_fetch(
|
||||
source_range.clone(),
|
||||
query.into(),
|
||||
&mat.path_prefix,
|
||||
is_recent,
|
||||
mat.is_dir,
|
||||
excerpt_id,
|
||||
source_range.clone(),
|
||||
editor.clone(),
|
||||
context_store.clone(),
|
||||
http_client.clone(),
|
||||
));
|
||||
}
|
||||
|
||||
context_store.update(cx, |store, _| {
|
||||
let urls = store.context().iter().filter_map(|context| {
|
||||
if let AssistantContext::FetchedUrl(context) = context {
|
||||
Some(context.url.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
for url in urls {
|
||||
completions.push(Self::completion_for_fetch(
|
||||
source_range.clone(),
|
||||
url,
|
||||
excerpt_id,
|
||||
editor.clone(),
|
||||
context_store.clone(),
|
||||
http_client.clone(),
|
||||
));
|
||||
}
|
||||
})?;
|
||||
}
|
||||
}
|
||||
Some(ContextPickerMode::Thread) => {
|
||||
if let Some((thread_store, editor)) = thread_store
|
||||
.and_then(|thread_store| thread_store.upgrade())
|
||||
.zip(editor.upgrade())
|
||||
{
|
||||
let threads = cx
|
||||
.update(|cx| {
|
||||
super::thread_context_picker::search_threads(
|
||||
query,
|
||||
thread_store.clone(),
|
||||
cx,
|
||||
)
|
||||
})?
|
||||
.await;
|
||||
for thread in threads {
|
||||
completions.push(Self::completion_for_thread(
|
||||
thread.clone(),
|
||||
excerpt_id,
|
||||
source_range.clone(),
|
||||
false,
|
||||
editor.clone(),
|
||||
context_store.clone(),
|
||||
thread_store.clone(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
cx.update(|cx| {
|
||||
if let Some(editor) = editor.upgrade() {
|
||||
completions.extend(Self::default_completions(
|
||||
excerpt_id,
|
||||
source_range.clone(),
|
||||
context_store.clone(),
|
||||
thread_store.clone(),
|
||||
editor,
|
||||
workspace.clone(),
|
||||
cx,
|
||||
));
|
||||
))
|
||||
}
|
||||
})?;
|
||||
}
|
||||
}
|
||||
Ok(Some(completions))
|
||||
Match::Symbol(SymbolMatch { symbol, .. }) => Self::completion_for_symbol(
|
||||
symbol,
|
||||
excerpt_id,
|
||||
source_range.clone(),
|
||||
editor.clone(),
|
||||
context_store.clone(),
|
||||
workspace.clone(),
|
||||
cx,
|
||||
),
|
||||
Match::Thread(ThreadMatch {
|
||||
thread, is_recent, ..
|
||||
}) => {
|
||||
let thread_store = thread_store.as_ref().and_then(|t| t.upgrade())?;
|
||||
Some(Self::completion_for_thread(
|
||||
thread,
|
||||
excerpt_id,
|
||||
source_range.clone(),
|
||||
is_recent,
|
||||
editor.clone(),
|
||||
context_store.clone(),
|
||||
thread_store,
|
||||
))
|
||||
}
|
||||
Match::Fetch(url) => Some(Self::completion_for_fetch(
|
||||
source_range.clone(),
|
||||
url,
|
||||
excerpt_id,
|
||||
editor.clone(),
|
||||
context_store.clone(),
|
||||
http_client.clone(),
|
||||
)),
|
||||
Match::Mode(mode) => {
|
||||
Some(Self::completion_for_mode(source_range.clone(), mode))
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
})?))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -676,7 +663,12 @@ impl MentionCompletion {
|
||||
let mut end = last_mention_start + 1;
|
||||
if let Some(mode_text) = parts.next() {
|
||||
end += mode_text.len();
|
||||
mode = ContextPickerMode::try_from(mode_text).ok();
|
||||
|
||||
if let Some(parsed_mode) = ContextPickerMode::try_from(mode_text).ok() {
|
||||
mode = Some(parsed_mode);
|
||||
} else {
|
||||
argument = Some(mode_text.to_string());
|
||||
}
|
||||
match rest_of_line[mode_text.len()..].find(|c: char| !c.is_whitespace()) {
|
||||
Some(whitespace_count) => {
|
||||
if let Some(argument_text) = parts.next() {
|
||||
@@ -702,13 +694,13 @@ impl MentionCompletion {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use gpui::{Focusable, TestAppContext, VisualTestContext};
|
||||
use gpui::{EventEmitter, FocusHandle, Focusable, TestAppContext, VisualTestContext};
|
||||
use project::{Project, ProjectPath};
|
||||
use serde_json::json;
|
||||
use settings::SettingsStore;
|
||||
use std::{ops::Deref, path::PathBuf};
|
||||
use std::ops::Deref;
|
||||
use util::{path, separator};
|
||||
use workspace::AppState;
|
||||
use workspace::{AppState, Item};
|
||||
|
||||
#[test]
|
||||
fn test_mention_completion_parse() {
|
||||
@@ -768,9 +760,42 @@ mod tests {
|
||||
})
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
MentionCompletion::try_parse("Lorem @main", 0),
|
||||
Some(MentionCompletion {
|
||||
source_range: 6..11,
|
||||
mode: None,
|
||||
argument: Some("main".to_string()),
|
||||
})
|
||||
);
|
||||
|
||||
assert_eq!(MentionCompletion::try_parse("test@", 0), None);
|
||||
}
|
||||
|
||||
struct AtMentionEditor(Entity<Editor>);
|
||||
|
||||
impl Item for AtMentionEditor {
|
||||
type Event = ();
|
||||
|
||||
fn include_in_nav_history() -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<()> for AtMentionEditor {}
|
||||
|
||||
impl Focusable for AtMentionEditor {
|
||||
fn focus_handle(&self, cx: &App) -> FocusHandle {
|
||||
self.0.read(cx).focus_handle(cx).clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for AtMentionEditor {
|
||||
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||
self.0.clone().into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_context_completion_provider(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
@@ -846,25 +871,27 @@ mod tests {
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
let item = workspace
|
||||
.update_in(&mut cx, |workspace, window, cx| {
|
||||
workspace.open_path(
|
||||
ProjectPath {
|
||||
worktree_id,
|
||||
path: PathBuf::from("editor").into(),
|
||||
},
|
||||
let editor = workspace.update_in(&mut cx, |workspace, window, cx| {
|
||||
let editor = cx.new(|cx| {
|
||||
Editor::new(
|
||||
editor::EditorMode::Full,
|
||||
multi_buffer::MultiBuffer::build_simple("", cx),
|
||||
None,
|
||||
true,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.expect("Could not open test file");
|
||||
|
||||
let editor = cx.update(|_, cx| {
|
||||
item.act_as::<Editor>(cx)
|
||||
.expect("Opened test file wasn't an editor")
|
||||
});
|
||||
workspace.active_pane().update(cx, |pane, cx| {
|
||||
pane.add_item(
|
||||
Box::new(cx.new(|_| AtMentionEditor(editor.clone()))),
|
||||
true,
|
||||
true,
|
||||
None,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
editor
|
||||
});
|
||||
|
||||
let context_store = cx.new(|_| ContextStore::new(project.downgrade(), None));
|
||||
@@ -895,10 +922,10 @@ mod tests {
|
||||
assert_eq!(
|
||||
current_completion_labels(editor),
|
||||
&[
|
||||
"editor dir/",
|
||||
"seven.txt dir/b/",
|
||||
"six.txt dir/b/",
|
||||
"five.txt dir/b/",
|
||||
"four.txt dir/a/",
|
||||
"Files & Directories",
|
||||
"Symbols",
|
||||
"Fetch"
|
||||
@@ -993,14 +1020,14 @@ mod tests {
|
||||
editor.update(&mut cx, |editor, cx| {
|
||||
assert_eq!(
|
||||
editor.text(cx),
|
||||
"Lorem [@one.txt](@file:dir/a/one.txt) Ipsum [@editor](@file:dir/editor)"
|
||||
"Lorem [@one.txt](@file:dir/a/one.txt) Ipsum [@seven.txt](@file:dir/b/seven.txt)"
|
||||
);
|
||||
assert!(!editor.has_visible_completions_menu());
|
||||
assert_eq!(
|
||||
crease_ranges(editor, cx),
|
||||
vec![
|
||||
Point::new(0, 6)..Point::new(0, 37),
|
||||
Point::new(0, 44)..Point::new(0, 71)
|
||||
Point::new(0, 44)..Point::new(0, 79)
|
||||
]
|
||||
);
|
||||
});
|
||||
@@ -1010,14 +1037,14 @@ mod tests {
|
||||
editor.update(&mut cx, |editor, cx| {
|
||||
assert_eq!(
|
||||
editor.text(cx),
|
||||
"Lorem [@one.txt](@file:dir/a/one.txt) Ipsum [@editor](@file:dir/editor)\n@"
|
||||
"Lorem [@one.txt](@file:dir/a/one.txt) Ipsum [@seven.txt](@file:dir/b/seven.txt)\n@"
|
||||
);
|
||||
assert!(editor.has_visible_completions_menu());
|
||||
assert_eq!(
|
||||
crease_ranges(editor, cx),
|
||||
vec![
|
||||
Point::new(0, 6)..Point::new(0, 37),
|
||||
Point::new(0, 44)..Point::new(0, 71)
|
||||
Point::new(0, 44)..Point::new(0, 79)
|
||||
]
|
||||
);
|
||||
});
|
||||
@@ -1031,15 +1058,15 @@ mod tests {
|
||||
editor.update(&mut cx, |editor, cx| {
|
||||
assert_eq!(
|
||||
editor.text(cx),
|
||||
"Lorem [@one.txt](@file:dir/a/one.txt) Ipsum [@editor](@file:dir/editor)\n[@seven.txt](@file:dir/b/seven.txt)"
|
||||
"Lorem [@one.txt](@file:dir/a/one.txt) Ipsum [@seven.txt](@file:dir/b/seven.txt)\n[@six.txt](@file:dir/b/six.txt)"
|
||||
);
|
||||
assert!(!editor.has_visible_completions_menu());
|
||||
assert_eq!(
|
||||
crease_ranges(editor, cx),
|
||||
vec![
|
||||
Point::new(0, 6)..Point::new(0, 37),
|
||||
Point::new(0, 44)..Point::new(0, 71),
|
||||
Point::new(1, 0)..Point::new(1, 35)
|
||||
Point::new(0, 44)..Point::new(0, 79),
|
||||
Point::new(1, 0)..Point::new(1, 31)
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
@@ -58,7 +58,7 @@ pub struct FileContextPickerDelegate {
|
||||
workspace: WeakEntity<Workspace>,
|
||||
context_store: WeakEntity<ContextStore>,
|
||||
confirm_behavior: ConfirmBehavior,
|
||||
matches: Vec<PathMatch>,
|
||||
matches: Vec<FileMatch>,
|
||||
selected_index: usize,
|
||||
}
|
||||
|
||||
@@ -114,7 +114,7 @@ impl PickerDelegate for FileContextPickerDelegate {
|
||||
return Task::ready(());
|
||||
};
|
||||
|
||||
let search_task = search_paths(query, Arc::<AtomicBool>::default(), &workspace, cx);
|
||||
let search_task = search_files(query, Arc::<AtomicBool>::default(), &workspace, cx);
|
||||
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
// TODO: This should be probably be run in the background.
|
||||
@@ -128,7 +128,7 @@ impl PickerDelegate for FileContextPickerDelegate {
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
|
||||
let Some(mat) = self.matches.get(self.selected_index) else {
|
||||
let Some(FileMatch { mat, .. }) = self.matches.get(self.selected_index) else {
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -181,7 +181,7 @@ impl PickerDelegate for FileContextPickerDelegate {
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Picker<Self>>,
|
||||
) -> Option<Self::ListItem> {
|
||||
let path_match = &self.matches[ix];
|
||||
let FileMatch { mat, .. } = &self.matches[ix];
|
||||
|
||||
Some(
|
||||
ListItem::new(ix)
|
||||
@@ -189,9 +189,9 @@ impl PickerDelegate for FileContextPickerDelegate {
|
||||
.toggle_state(selected)
|
||||
.child(render_file_context_entry(
|
||||
ElementId::NamedInteger("file-ctx-picker".into(), ix),
|
||||
&path_match.path,
|
||||
&path_match.path_prefix,
|
||||
path_match.is_dir,
|
||||
&mat.path,
|
||||
&mat.path_prefix,
|
||||
mat.is_dir,
|
||||
self.context_store.clone(),
|
||||
cx,
|
||||
)),
|
||||
@@ -199,12 +199,17 @@ impl PickerDelegate for FileContextPickerDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn search_paths(
|
||||
pub struct FileMatch {
|
||||
pub mat: PathMatch,
|
||||
pub is_recent: bool,
|
||||
}
|
||||
|
||||
pub(crate) fn search_files(
|
||||
query: String,
|
||||
cancellation_flag: Arc<AtomicBool>,
|
||||
workspace: &Entity<Workspace>,
|
||||
cx: &App,
|
||||
) -> Task<Vec<PathMatch>> {
|
||||
) -> Task<Vec<FileMatch>> {
|
||||
if query.is_empty() {
|
||||
let workspace = workspace.read(cx);
|
||||
let project = workspace.project().read(cx);
|
||||
@@ -213,28 +218,34 @@ pub(crate) fn search_paths(
|
||||
.into_iter()
|
||||
.filter_map(|(project_path, _)| {
|
||||
let worktree = project.worktree_for_id(project_path.worktree_id, cx)?;
|
||||
Some(PathMatch {
|
||||
score: 0.,
|
||||
positions: Vec::new(),
|
||||
worktree_id: project_path.worktree_id.to_usize(),
|
||||
path: project_path.path,
|
||||
path_prefix: worktree.read(cx).root_name().into(),
|
||||
distance_to_relative_ancestor: 0,
|
||||
is_dir: false,
|
||||
Some(FileMatch {
|
||||
mat: PathMatch {
|
||||
score: 0.,
|
||||
positions: Vec::new(),
|
||||
worktree_id: project_path.worktree_id.to_usize(),
|
||||
path: project_path.path,
|
||||
path_prefix: worktree.read(cx).root_name().into(),
|
||||
distance_to_relative_ancestor: 0,
|
||||
is_dir: false,
|
||||
},
|
||||
is_recent: true,
|
||||
})
|
||||
});
|
||||
|
||||
let file_matches = project.worktrees(cx).flat_map(|worktree| {
|
||||
let worktree = worktree.read(cx);
|
||||
let path_prefix: Arc<str> = worktree.root_name().into();
|
||||
worktree.entries(false, 0).map(move |entry| PathMatch {
|
||||
score: 0.,
|
||||
positions: Vec::new(),
|
||||
worktree_id: worktree.id().to_usize(),
|
||||
path: entry.path.clone(),
|
||||
path_prefix: path_prefix.clone(),
|
||||
distance_to_relative_ancestor: 0,
|
||||
is_dir: entry.is_dir(),
|
||||
worktree.entries(false, 0).map(move |entry| FileMatch {
|
||||
mat: PathMatch {
|
||||
score: 0.,
|
||||
positions: Vec::new(),
|
||||
worktree_id: worktree.id().to_usize(),
|
||||
path: entry.path.clone(),
|
||||
path_prefix: path_prefix.clone(),
|
||||
distance_to_relative_ancestor: 0,
|
||||
is_dir: entry.is_dir(),
|
||||
},
|
||||
is_recent: false,
|
||||
})
|
||||
});
|
||||
|
||||
@@ -269,6 +280,12 @@ pub(crate) fn search_paths(
|
||||
executor,
|
||||
)
|
||||
.await
|
||||
.into_iter()
|
||||
.map(|mat| FileMatch {
|
||||
mat,
|
||||
is_recent: false,
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ use std::cmp::Reverse;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use anyhow::Result;
|
||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||
use gpui::{
|
||||
App, AppContext, DismissEvent, Entity, FocusHandle, Focusable, Stateful, Task, WeakEntity,
|
||||
@@ -119,11 +119,7 @@ impl PickerDelegate for SymbolContextPickerDelegate {
|
||||
let search_task = search_symbols(query, Arc::<AtomicBool>::default(), &workspace, cx);
|
||||
let context_store = self.context_store.clone();
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let symbols = search_task
|
||||
.await
|
||||
.context("Failed to load symbols")
|
||||
.log_err()
|
||||
.unwrap_or_default();
|
||||
let symbols = search_task.await;
|
||||
|
||||
let symbol_entries = context_store
|
||||
.read_with(cx, |context_store, cx| {
|
||||
@@ -285,12 +281,16 @@ fn find_matching_symbol(symbol: &Symbol, candidates: &[DocumentSymbol]) -> Optio
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SymbolMatch {
|
||||
pub symbol: Symbol,
|
||||
}
|
||||
|
||||
pub(crate) fn search_symbols(
|
||||
query: String,
|
||||
cancellation_flag: Arc<AtomicBool>,
|
||||
workspace: &Entity<Workspace>,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<Vec<(StringMatch, Symbol)>>> {
|
||||
) -> Task<Vec<SymbolMatch>> {
|
||||
let symbols_task = workspace.update(cx, |workspace, cx| {
|
||||
workspace
|
||||
.project()
|
||||
@@ -298,19 +298,28 @@ pub(crate) fn search_symbols(
|
||||
});
|
||||
let project = workspace.read(cx).project().clone();
|
||||
cx.spawn(async move |cx| {
|
||||
let symbols = symbols_task.await?;
|
||||
let (visible_match_candidates, external_match_candidates): (Vec<_>, Vec<_>) = project
|
||||
.update(cx, |project, cx| {
|
||||
symbols
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(id, symbol)| StringMatchCandidate::new(id, &symbol.label.filter_text()))
|
||||
.partition(|candidate| {
|
||||
project
|
||||
.entry_for_path(&symbols[candidate.id].path, cx)
|
||||
.map_or(false, |e| !e.is_ignored)
|
||||
})
|
||||
})?;
|
||||
let Some(symbols) = symbols_task.await.log_err() else {
|
||||
return Vec::new();
|
||||
};
|
||||
let Some((visible_match_candidates, external_match_candidates)): Option<(Vec<_>, Vec<_>)> =
|
||||
project
|
||||
.update(cx, |project, cx| {
|
||||
symbols
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(id, symbol)| {
|
||||
StringMatchCandidate::new(id, &symbol.label.filter_text())
|
||||
})
|
||||
.partition(|candidate| {
|
||||
project
|
||||
.entry_for_path(&symbols[candidate.id].path, cx)
|
||||
.map_or(false, |e| !e.is_ignored)
|
||||
})
|
||||
})
|
||||
.log_err()
|
||||
else {
|
||||
return Vec::new();
|
||||
};
|
||||
|
||||
const MAX_MATCHES: usize = 100;
|
||||
let mut visible_matches = cx.background_executor().block(fuzzy::match_strings(
|
||||
@@ -339,7 +348,7 @@ pub(crate) fn search_symbols(
|
||||
let mut matches = visible_matches;
|
||||
matches.append(&mut external_matches);
|
||||
|
||||
Ok(matches
|
||||
matches
|
||||
.into_iter()
|
||||
.map(|mut mat| {
|
||||
let symbol = symbols[mat.candidate_id].clone();
|
||||
@@ -347,19 +356,19 @@ pub(crate) fn search_symbols(
|
||||
for position in &mut mat.positions {
|
||||
*position += filter_start;
|
||||
}
|
||||
(mat, symbol)
|
||||
SymbolMatch { symbol }
|
||||
})
|
||||
.collect())
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
|
||||
fn compute_symbol_entries(
|
||||
symbols: Vec<(StringMatch, Symbol)>,
|
||||
symbols: Vec<SymbolMatch>,
|
||||
context_store: &ContextStore,
|
||||
cx: &App,
|
||||
) -> Vec<SymbolEntry> {
|
||||
let mut symbol_entries = Vec::with_capacity(symbols.len());
|
||||
for (_, symbol) in symbols {
|
||||
for SymbolMatch { symbol, .. } in symbols {
|
||||
let symbols_for_path = context_store.included_symbols_by_path().get(&symbol.path);
|
||||
let is_included = if let Some(symbols_for_path) = symbols_for_path {
|
||||
let mut is_included = false;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
|
||||
use fuzzy::StringMatchCandidate;
|
||||
use gpui::{App, DismissEvent, Entity, FocusHandle, Focusable, Task, WeakEntity};
|
||||
@@ -114,11 +115,11 @@ impl PickerDelegate for ThreadContextPickerDelegate {
|
||||
return Task::ready(());
|
||||
};
|
||||
|
||||
let search_task = search_threads(query, threads, cx);
|
||||
let search_task = search_threads(query, Arc::new(AtomicBool::default()), threads, cx);
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let matches = search_task.await;
|
||||
this.update(cx, |this, cx| {
|
||||
this.delegate.matches = matches;
|
||||
this.delegate.matches = matches.into_iter().map(|mat| mat.thread).collect();
|
||||
this.delegate.selected_index = 0;
|
||||
cx.notify();
|
||||
})
|
||||
@@ -217,11 +218,18 @@ pub fn render_thread_context_entry(
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ThreadMatch {
|
||||
pub thread: ThreadContextEntry,
|
||||
pub is_recent: bool,
|
||||
}
|
||||
|
||||
pub(crate) fn search_threads(
|
||||
query: String,
|
||||
cancellation_flag: Arc<AtomicBool>,
|
||||
thread_store: Entity<ThreadStore>,
|
||||
cx: &mut App,
|
||||
) -> Task<Vec<ThreadContextEntry>> {
|
||||
) -> Task<Vec<ThreadMatch>> {
|
||||
let threads = thread_store.update(cx, |this, _cx| {
|
||||
this.threads()
|
||||
.into_iter()
|
||||
@@ -236,6 +244,12 @@ pub(crate) fn search_threads(
|
||||
cx.background_spawn(async move {
|
||||
if query.is_empty() {
|
||||
threads
|
||||
.into_iter()
|
||||
.map(|thread| ThreadMatch {
|
||||
thread,
|
||||
is_recent: false,
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
let candidates = threads
|
||||
.iter()
|
||||
@@ -247,14 +261,17 @@ pub(crate) fn search_threads(
|
||||
&query,
|
||||
false,
|
||||
100,
|
||||
&Default::default(),
|
||||
&cancellation_flag,
|
||||
executor,
|
||||
)
|
||||
.await;
|
||||
|
||||
matches
|
||||
.into_iter()
|
||||
.map(|mat| threads[mat.candidate_id].clone())
|
||||
.map(|mat| ThreadMatch {
|
||||
thread: threads[mat.candidate_id].clone(),
|
||||
is_recent: false,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -28,6 +28,7 @@ use language_model::{LanguageModelRegistry, report_assistant_event};
|
||||
use multi_buffer::MultiBufferRow;
|
||||
use parking_lot::Mutex;
|
||||
use project::LspAction;
|
||||
use project::Project;
|
||||
use project::{CodeAction, ProjectTransaction};
|
||||
use prompt_store::PromptBuilder;
|
||||
use settings::{Settings, SettingsStore};
|
||||
@@ -254,6 +255,7 @@ impl InlineAssistant {
|
||||
assistant.assist(
|
||||
&active_editor,
|
||||
cx.entity().downgrade(),
|
||||
workspace.project().downgrade(),
|
||||
thread_store,
|
||||
window,
|
||||
cx,
|
||||
@@ -262,7 +264,14 @@ impl InlineAssistant {
|
||||
}
|
||||
InlineAssistTarget::Terminal(active_terminal) => {
|
||||
TerminalInlineAssistant::update_global(cx, |assistant, cx| {
|
||||
assistant.assist(&active_terminal, cx.entity(), thread_store, window, cx)
|
||||
assistant.assist(
|
||||
&active_terminal,
|
||||
cx.entity().downgrade(),
|
||||
workspace.project().downgrade(),
|
||||
thread_store,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
}
|
||||
};
|
||||
@@ -312,17 +321,11 @@ impl InlineAssistant {
|
||||
&mut self,
|
||||
editor: &Entity<Editor>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
project: WeakEntity<Project>,
|
||||
thread_store: Option<WeakEntity<ThreadStore>>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
let Some(project) = workspace
|
||||
.upgrade()
|
||||
.map(|workspace| workspace.read(cx).project().downgrade())
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
let (snapshot, initial_selections) = editor.update(cx, |editor, cx| {
|
||||
(
|
||||
editor.snapshot(window, cx),
|
||||
|
||||
@@ -3,14 +3,16 @@ use std::sync::Arc;
|
||||
use crate::assistant_model_selector::ModelType;
|
||||
use collections::HashSet;
|
||||
use editor::actions::MoveUp;
|
||||
use editor::{ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement, EditorStyle};
|
||||
use editor::{
|
||||
ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement, EditorStyle, MultiBuffer,
|
||||
};
|
||||
use file_icons::FileIcons;
|
||||
use fs::Fs;
|
||||
use gpui::{
|
||||
Animation, AnimationExt, App, DismissEvent, Entity, Focusable, Subscription, TextStyle,
|
||||
WeakEntity, linear_color_stop, linear_gradient, point, pulsating_between,
|
||||
};
|
||||
use language::Buffer;
|
||||
use language::{Buffer, Language};
|
||||
use language_model::{ConfiguredModel, LanguageModelRegistry};
|
||||
use language_model_selector::ToggleModelSelector;
|
||||
use multi_buffer;
|
||||
@@ -66,8 +68,24 @@ impl MessageEditor {
|
||||
let inline_context_picker_menu_handle = PopoverMenuHandle::default();
|
||||
let model_selector_menu_handle = PopoverMenuHandle::default();
|
||||
|
||||
let language = Language::new(
|
||||
language::LanguageConfig {
|
||||
completion_query_characters: HashSet::from_iter(['.', '-', '_', '@']),
|
||||
..Default::default()
|
||||
},
|
||||
None,
|
||||
);
|
||||
|
||||
let editor = cx.new(|cx| {
|
||||
let mut editor = Editor::auto_height(10, window, cx);
|
||||
let buffer = cx.new(|cx| Buffer::local("", cx).with_language(Arc::new(language), cx));
|
||||
let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
let mut editor = Editor::new(
|
||||
editor::EditorMode::AutoHeight { max_lines: 10 },
|
||||
buffer,
|
||||
None,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
editor.set_placeholder_text("Ask anything, @ to mention, ↑ to select", cx);
|
||||
editor.set_show_indent_guides(false, cx);
|
||||
editor.set_context_menu_options(ContextMenuOptions {
|
||||
@@ -75,7 +93,6 @@ impl MessageEditor {
|
||||
max_entries_visible: 12,
|
||||
placement: Some(ContextMenuPlacement::Above),
|
||||
});
|
||||
|
||||
editor
|
||||
});
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ use language_model::{
|
||||
ConfiguredModel, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage,
|
||||
Role, report_assistant_event,
|
||||
};
|
||||
use project::Project;
|
||||
use prompt_store::PromptBuilder;
|
||||
use std::sync::Arc;
|
||||
use telemetry_events::{AssistantEventData, AssistantKind, AssistantPhase};
|
||||
@@ -66,7 +67,8 @@ impl TerminalInlineAssistant {
|
||||
pub fn assist(
|
||||
&mut self,
|
||||
terminal_view: &Entity<TerminalView>,
|
||||
workspace: Entity<Workspace>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
project: WeakEntity<Project>,
|
||||
thread_store: Option<WeakEntity<ThreadStore>>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
@@ -75,7 +77,6 @@ impl TerminalInlineAssistant {
|
||||
let assist_id = self.next_assist_id.post_inc();
|
||||
let prompt_buffer =
|
||||
cx.new(|cx| MultiBuffer::singleton(cx.new(|cx| Buffer::local(String::new(), cx)), cx));
|
||||
let project = workspace.read(cx).project().downgrade();
|
||||
let context_store = cx.new(|_cx| ContextStore::new(project, thread_store.clone()));
|
||||
let codegen = cx.new(|_| TerminalCodegen::new(terminal, self.telemetry.clone()));
|
||||
|
||||
@@ -87,7 +88,7 @@ impl TerminalInlineAssistant {
|
||||
codegen,
|
||||
self.fs.clone(),
|
||||
context_store.clone(),
|
||||
workspace.downgrade(),
|
||||
workspace.clone(),
|
||||
thread_store.clone(),
|
||||
window,
|
||||
cx,
|
||||
@@ -106,7 +107,7 @@ impl TerminalInlineAssistant {
|
||||
assist_id,
|
||||
terminal_view,
|
||||
prompt_editor,
|
||||
workspace.downgrade(),
|
||||
workspace.clone(),
|
||||
context_store,
|
||||
window,
|
||||
cx,
|
||||
|
||||
@@ -182,7 +182,7 @@ pub struct ThreadCheckpoint {
|
||||
git_checkpoint: GitStoreCheckpoint,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
pub enum ThreadFeedback {
|
||||
Positive,
|
||||
Negative,
|
||||
@@ -260,6 +260,7 @@ pub struct Thread {
|
||||
initial_project_snapshot: Shared<Task<Option<Arc<ProjectSnapshot>>>>,
|
||||
cumulative_token_usage: TokenUsage,
|
||||
feedback: Option<ThreadFeedback>,
|
||||
message_feedback: HashMap<MessageId, ThreadFeedback>,
|
||||
}
|
||||
|
||||
impl Thread {
|
||||
@@ -298,6 +299,7 @@ impl Thread {
|
||||
},
|
||||
cumulative_token_usage: TokenUsage::default(),
|
||||
feedback: None,
|
||||
message_feedback: HashMap::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -361,6 +363,7 @@ impl Thread {
|
||||
initial_project_snapshot: Task::ready(serialized.initial_project_snapshot).shared(),
|
||||
cumulative_token_usage: serialized.cumulative_token_usage,
|
||||
feedback: None,
|
||||
message_feedback: HashMap::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -997,6 +1000,20 @@ impl Thread {
|
||||
|
||||
self.attached_tracked_files_state(&mut request.messages, cx);
|
||||
|
||||
// Add reminder to the last user message about code blocks
|
||||
if let Some(last_user_message) = request
|
||||
.messages
|
||||
.iter_mut()
|
||||
.rev()
|
||||
.find(|msg| msg.role == Role::User)
|
||||
{
|
||||
last_user_message
|
||||
.content
|
||||
.push(MessageContent::Text(system_prompt_reminder(
|
||||
&self.prompt_builder,
|
||||
)));
|
||||
}
|
||||
|
||||
request
|
||||
}
|
||||
|
||||
@@ -1504,24 +1521,38 @@ impl Thread {
|
||||
canceled
|
||||
}
|
||||
|
||||
/// Returns the feedback given to the thread, if any.
|
||||
pub fn feedback(&self) -> Option<ThreadFeedback> {
|
||||
self.feedback
|
||||
}
|
||||
|
||||
/// Reports feedback about the thread and stores it in our telemetry backend.
|
||||
pub fn report_feedback(
|
||||
pub fn message_feedback(&self, message_id: MessageId) -> Option<ThreadFeedback> {
|
||||
self.message_feedback.get(&message_id).copied()
|
||||
}
|
||||
|
||||
pub fn report_message_feedback(
|
||||
&mut self,
|
||||
message_id: MessageId,
|
||||
feedback: ThreadFeedback,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
if self.message_feedback.get(&message_id) == Some(&feedback) {
|
||||
return Task::ready(Ok(()));
|
||||
}
|
||||
|
||||
let final_project_snapshot = Self::project_snapshot(self.project.clone(), cx);
|
||||
let serialized_thread = self.serialize(cx);
|
||||
let thread_id = self.id().clone();
|
||||
let client = self.project.read(cx).client();
|
||||
self.feedback = Some(feedback);
|
||||
|
||||
self.message_feedback.insert(message_id, feedback);
|
||||
|
||||
cx.notify();
|
||||
|
||||
let message_content = self
|
||||
.message(message_id)
|
||||
.map(|msg| msg.to_string())
|
||||
.unwrap_or_default();
|
||||
|
||||
cx.background_spawn(async move {
|
||||
let final_project_snapshot = final_project_snapshot.await;
|
||||
let serialized_thread = serialized_thread.await?;
|
||||
@@ -1536,6 +1567,8 @@ impl Thread {
|
||||
"Assistant Thread Rated",
|
||||
rating,
|
||||
thread_id,
|
||||
message_id = message_id.0,
|
||||
message_content,
|
||||
thread_data,
|
||||
final_project_snapshot
|
||||
);
|
||||
@@ -1545,6 +1578,52 @@ impl Thread {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn report_feedback(
|
||||
&mut self,
|
||||
feedback: ThreadFeedback,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
let last_assistant_message_id = self
|
||||
.messages
|
||||
.iter()
|
||||
.rev()
|
||||
.find(|msg| msg.role == Role::Assistant)
|
||||
.map(|msg| msg.id);
|
||||
|
||||
if let Some(message_id) = last_assistant_message_id {
|
||||
self.report_message_feedback(message_id, feedback, cx)
|
||||
} else {
|
||||
let final_project_snapshot = Self::project_snapshot(self.project.clone(), cx);
|
||||
let serialized_thread = self.serialize(cx);
|
||||
let thread_id = self.id().clone();
|
||||
let client = self.project.read(cx).client();
|
||||
self.feedback = Some(feedback);
|
||||
cx.notify();
|
||||
|
||||
cx.background_spawn(async move {
|
||||
let final_project_snapshot = final_project_snapshot.await;
|
||||
let serialized_thread = serialized_thread.await?;
|
||||
let thread_data = serde_json::to_value(serialized_thread)
|
||||
.unwrap_or_else(|_| serde_json::Value::Null);
|
||||
|
||||
let rating = match feedback {
|
||||
ThreadFeedback::Positive => "positive",
|
||||
ThreadFeedback::Negative => "negative",
|
||||
};
|
||||
telemetry::event!(
|
||||
"Assistant Thread Rated",
|
||||
rating,
|
||||
thread_id,
|
||||
thread_data,
|
||||
final_project_snapshot
|
||||
);
|
||||
client.telemetry().flush_events();
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a snapshot of the current project state including git information and unsaved buffers.
|
||||
fn project_snapshot(
|
||||
project: Entity<Project>,
|
||||
@@ -1810,6 +1889,12 @@ impl Thread {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn system_prompt_reminder(prompt_builder: &prompt_store::PromptBuilder) -> String {
|
||||
prompt_builder
|
||||
.generate_assistant_system_prompt_reminder()
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ThreadError {
|
||||
PaymentRequired,
|
||||
@@ -1879,7 +1964,7 @@ mod tests {
|
||||
)
|
||||
.await;
|
||||
|
||||
let (_workspace, _thread_store, thread, context_store) =
|
||||
let (_workspace, _thread_store, thread, context_store, prompt_builder) =
|
||||
setup_test_environment(cx, project.clone()).await;
|
||||
|
||||
add_file_to_context(&project, &context_store, "test/code.rs", cx)
|
||||
@@ -1933,8 +2018,14 @@ fn main() {{
|
||||
});
|
||||
|
||||
assert_eq!(request.messages.len(), 1);
|
||||
let expected_full_message = format!("{}Please explain this code", expected_context);
|
||||
assert_eq!(request.messages[0].string_contents(), expected_full_message);
|
||||
let actual_message = request.messages[0].string_contents();
|
||||
let expected_content = format!(
|
||||
"{}Please explain this code{}",
|
||||
expected_context,
|
||||
system_prompt_reminder(&prompt_builder)
|
||||
);
|
||||
|
||||
assert_eq!(actual_message, expected_content);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
@@ -1951,7 +2042,7 @@ fn main() {{
|
||||
)
|
||||
.await;
|
||||
|
||||
let (_, _thread_store, thread, context_store) =
|
||||
let (_, _thread_store, thread, context_store, _prompt_builder) =
|
||||
setup_test_environment(cx, project.clone()).await;
|
||||
|
||||
// Open files individually
|
||||
@@ -2051,7 +2142,7 @@ fn main() {{
|
||||
)
|
||||
.await;
|
||||
|
||||
let (_, _thread_store, thread, _context_store) =
|
||||
let (_, _thread_store, thread, _context_store, prompt_builder) =
|
||||
setup_test_environment(cx, project.clone()).await;
|
||||
|
||||
// Insert user message without any context (empty context vector)
|
||||
@@ -2077,11 +2168,14 @@ fn main() {{
|
||||
});
|
||||
|
||||
assert_eq!(request.messages.len(), 1);
|
||||
assert_eq!(
|
||||
request.messages[0].string_contents(),
|
||||
"What is the best way to learn Rust?"
|
||||
let actual_message = request.messages[0].string_contents();
|
||||
let expected_content = format!(
|
||||
"What is the best way to learn Rust?{}",
|
||||
system_prompt_reminder(&prompt_builder)
|
||||
);
|
||||
|
||||
assert_eq!(actual_message, expected_content);
|
||||
|
||||
// Add second message, also without context
|
||||
let message2_id = thread.update(cx, |thread, cx| {
|
||||
thread.insert_user_message("Are there any good books?", vec![], None, cx)
|
||||
@@ -2097,14 +2191,17 @@ fn main() {{
|
||||
});
|
||||
|
||||
assert_eq!(request.messages.len(), 2);
|
||||
assert_eq!(
|
||||
request.messages[0].string_contents(),
|
||||
"What is the best way to learn Rust?"
|
||||
);
|
||||
assert_eq!(
|
||||
request.messages[1].string_contents(),
|
||||
"Are there any good books?"
|
||||
// First message should be the system prompt
|
||||
assert_eq!(request.messages[0].role, Role::User);
|
||||
|
||||
// Second message should be the user message with prompt reminder
|
||||
let actual_message = request.messages[1].string_contents();
|
||||
let expected_content = format!(
|
||||
"Are there any good books?{}",
|
||||
system_prompt_reminder(&prompt_builder)
|
||||
);
|
||||
|
||||
assert_eq!(actual_message, expected_content);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
@@ -2117,7 +2214,7 @@ fn main() {{
|
||||
)
|
||||
.await;
|
||||
|
||||
let (_workspace, _thread_store, thread, context_store) =
|
||||
let (_workspace, _thread_store, thread, context_store, prompt_builder) =
|
||||
setup_test_environment(cx, project.clone()).await;
|
||||
|
||||
// Open buffer and add it to context
|
||||
@@ -2177,11 +2274,14 @@ fn main() {{
|
||||
// The last message should be the stale buffer notification
|
||||
assert_eq!(last_message.role, Role::User);
|
||||
|
||||
// Check the exact content of the message
|
||||
let expected_content = "These files changed since last read:\n- code.rs\n";
|
||||
let actual_message = last_message.string_contents();
|
||||
let expected_content = format!(
|
||||
"These files changed since last read:\n- code.rs\n{}",
|
||||
system_prompt_reminder(&prompt_builder)
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
last_message.string_contents(),
|
||||
expected_content,
|
||||
actual_message, expected_content,
|
||||
"Last message should be exactly the stale buffer notification"
|
||||
);
|
||||
}
|
||||
@@ -2219,24 +2319,27 @@ fn main() {{
|
||||
Entity<ThreadStore>,
|
||||
Entity<Thread>,
|
||||
Entity<ContextStore>,
|
||||
Arc<PromptBuilder>,
|
||||
) {
|
||||
let (workspace, cx) =
|
||||
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
|
||||
|
||||
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
|
||||
|
||||
let thread_store = cx.update(|_, cx| {
|
||||
ThreadStore::new(
|
||||
project.clone(),
|
||||
Arc::default(),
|
||||
Arc::new(PromptBuilder::new(None).unwrap()),
|
||||
cx,
|
||||
)
|
||||
.unwrap()
|
||||
ThreadStore::new(project.clone(), Arc::default(), prompt_builder.clone(), cx).unwrap()
|
||||
});
|
||||
|
||||
let thread = thread_store.update(cx, |store, cx| store.create_thread(cx));
|
||||
let context_store = cx.new(|_cx| ContextStore::new(project.downgrade(), None));
|
||||
|
||||
(workspace, thread_store, thread, context_store)
|
||||
(
|
||||
workspace,
|
||||
thread_store,
|
||||
thread,
|
||||
context_store,
|
||||
prompt_builder,
|
||||
)
|
||||
}
|
||||
|
||||
async fn add_file_to_context(
|
||||
|
||||
@@ -10,7 +10,7 @@ use collections::{BTreeSet, HashMap, HashSet, hash_map};
|
||||
use editor::{
|
||||
Anchor, Editor, EditorEvent, MenuInlineCompletionsPolicy, ProposedChangeLocation,
|
||||
ProposedChangesEditor, RowExt, ToOffset as _, ToPoint,
|
||||
actions::{FoldAt, MoveToEndOfLine, Newline, ShowCompletions, UnfoldAt},
|
||||
actions::{MoveToEndOfLine, Newline, ShowCompletions},
|
||||
display_map::{
|
||||
BlockContext, BlockId, BlockPlacement, BlockProperties, BlockStyle, Crease, CreaseMetadata,
|
||||
CustomBlockId, FoldId, RenderBlock, ToDisplayPoint,
|
||||
@@ -1053,7 +1053,7 @@ impl ContextEditor {
|
||||
let creases = editor.insert_creases(creases, cx);
|
||||
|
||||
for buffer_row in buffer_rows_to_fold.into_iter().rev() {
|
||||
editor.fold_at(&FoldAt { buffer_row }, window, cx);
|
||||
editor.fold_at(buffer_row, window, cx);
|
||||
}
|
||||
|
||||
creases
|
||||
@@ -1109,7 +1109,7 @@ impl ContextEditor {
|
||||
buffer_rows_to_fold.clear();
|
||||
}
|
||||
for buffer_row in buffer_rows_to_fold.into_iter().rev() {
|
||||
editor.fold_at(&FoldAt { buffer_row }, window, cx);
|
||||
editor.fold_at(buffer_row, window, cx);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1844,13 +1844,7 @@ impl ContextEditor {
|
||||
|_, _, _, _| Empty.into_any(),
|
||||
);
|
||||
editor.insert_creases(vec![crease], cx);
|
||||
editor.fold_at(
|
||||
&FoldAt {
|
||||
buffer_row: start_row,
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
editor.fold_at(start_row, window, cx);
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -2042,7 +2036,7 @@ impl ContextEditor {
|
||||
cx,
|
||||
);
|
||||
for buffer_row in buffer_rows_to_fold.into_iter().rev() {
|
||||
editor.fold_at(&FoldAt { buffer_row }, window, cx);
|
||||
editor.fold_at(buffer_row, window, cx);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -2820,7 +2814,7 @@ fn render_thought_process_fold_icon_button(
|
||||
.start
|
||||
.to_point(&editor.buffer().read(cx).read(cx));
|
||||
let buffer_row = MultiBufferRow(buffer_start.row);
|
||||
editor.unfold_at(&UnfoldAt { buffer_row }, window, cx);
|
||||
editor.unfold_at(buffer_row, window, cx);
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
@@ -2847,7 +2841,7 @@ fn render_fold_icon_button(
|
||||
.start
|
||||
.to_point(&editor.buffer().read(cx).read(cx));
|
||||
let buffer_row = MultiBufferRow(buffer_start.row);
|
||||
editor.unfold_at(&UnfoldAt { buffer_row }, window, cx);
|
||||
editor.unfold_at(buffer_row, window, cx);
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
@@ -2907,7 +2901,7 @@ fn quote_selection_fold_placeholder(title: String, editor: WeakEntity<Editor>) -
|
||||
.start
|
||||
.to_point(&editor.buffer().read(cx).read(cx));
|
||||
let buffer_row = MultiBufferRow(buffer_start.row);
|
||||
editor.unfold_at(&UnfoldAt { buffer_row }, window, cx);
|
||||
editor.unfold_at(buffer_row, window, cx);
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
|
||||
@@ -3,7 +3,7 @@ use anyhow::{Context as _, Result, anyhow};
|
||||
use assistant_tool::{ActionLog, Tool};
|
||||
use futures::io::BufReader;
|
||||
use futures::{AsyncBufReadExt, AsyncReadExt};
|
||||
use gpui::{App, Entity, Task};
|
||||
use gpui::{App, AppContext, Entity, Task};
|
||||
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
||||
use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
@@ -123,108 +123,184 @@ impl Tool for BashTool {
|
||||
worktree.read(cx).abs_path()
|
||||
};
|
||||
|
||||
cx.spawn(async move |_| {
|
||||
// Add 2>&1 to merge stderr into stdout for proper interleaving.
|
||||
let command = format!("({}) 2>&1", input.command);
|
||||
|
||||
let mut cmd = new_smol_command("bash")
|
||||
.arg("-c")
|
||||
.arg(&command)
|
||||
.current_dir(working_dir)
|
||||
.stdout(std::process::Stdio::piped())
|
||||
.spawn()
|
||||
.context("Failed to execute bash command")?;
|
||||
|
||||
// Capture stdout with a limit
|
||||
let stdout = cmd.stdout.take().unwrap();
|
||||
let mut reader = BufReader::new(stdout);
|
||||
|
||||
const MESSAGE_1: &str = "Command output too long. The first ";
|
||||
const MESSAGE_2: &str = " bytes:\n\n";
|
||||
const ERR_MESSAGE_1: &str = "Command failed with exit code ";
|
||||
const ERR_MESSAGE_2: &str = "\n\n";
|
||||
|
||||
const STDOUT_LIMIT: usize = 8192;
|
||||
|
||||
const LIMIT: usize = STDOUT_LIMIT
|
||||
- (MESSAGE_1.len()
|
||||
+ (STDOUT_LIMIT.ilog10() as usize + 1) // byte count
|
||||
+ MESSAGE_2.len()
|
||||
+ ERR_MESSAGE_1.len()
|
||||
+ 3 // status code
|
||||
+ ERR_MESSAGE_2.len());
|
||||
|
||||
// Read one more byte to determine whether the output was truncated
|
||||
let mut buffer = vec![0; LIMIT + 1];
|
||||
let mut bytes_read = 0;
|
||||
|
||||
// Read until we reach the limit
|
||||
loop {
|
||||
let read = reader.read(&mut buffer).await?;
|
||||
if read == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
bytes_read += read;
|
||||
if bytes_read > LIMIT {
|
||||
bytes_read = LIMIT + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Repeatedly fill the output reader's buffer without copying it.
|
||||
loop {
|
||||
let skipped_bytes = reader.fill_buf().await?;
|
||||
if skipped_bytes.is_empty() {
|
||||
break;
|
||||
}
|
||||
let skipped_bytes_len = skipped_bytes.len();
|
||||
reader.consume_unpin(skipped_bytes_len);
|
||||
}
|
||||
|
||||
let output_bytes = &buffer[..bytes_read];
|
||||
|
||||
// Let the process continue running
|
||||
let status = cmd.status().await.context("Failed to get command status")?;
|
||||
|
||||
let output_string = if bytes_read > LIMIT {
|
||||
// Valid to find `\n` in UTF-8 since 0-127 ASCII characters are not used in
|
||||
// multi-byte characters.
|
||||
let last_line_ix = output_bytes.iter().rposition(|b| *b == b'\n');
|
||||
let output_string = String::from_utf8_lossy(
|
||||
&output_bytes[..last_line_ix.unwrap_or(output_bytes.len())],
|
||||
);
|
||||
|
||||
format!(
|
||||
"{}{}{}{}",
|
||||
MESSAGE_1,
|
||||
output_string.len(),
|
||||
MESSAGE_2,
|
||||
output_string
|
||||
)
|
||||
} else {
|
||||
String::from_utf8_lossy(&output_bytes).into()
|
||||
};
|
||||
|
||||
let output_with_status = if status.success() {
|
||||
if output_string.is_empty() {
|
||||
"Command executed successfully.".to_string()
|
||||
} else {
|
||||
output_string.to_string()
|
||||
}
|
||||
} else {
|
||||
format!(
|
||||
"{}{}{}{}",
|
||||
ERR_MESSAGE_1,
|
||||
status.code().unwrap_or(-1),
|
||||
ERR_MESSAGE_2,
|
||||
output_string,
|
||||
)
|
||||
};
|
||||
|
||||
debug_assert!(output_with_status.len() <= STDOUT_LIMIT);
|
||||
|
||||
Ok(output_with_status)
|
||||
})
|
||||
cx.background_spawn(run_command_limited(working_dir, input.command))
|
||||
}
|
||||
}
|
||||
|
||||
const LIMIT: usize = 16 * 1024;
|
||||
|
||||
async fn run_command_limited(working_dir: Arc<Path>, command: String) -> Result<String> {
|
||||
// Add 2>&1 to merge stderr into stdout for proper interleaving.
|
||||
let command = format!("({}) 2>&1", command);
|
||||
|
||||
let mut cmd = new_smol_command("bash")
|
||||
.arg("-c")
|
||||
.arg(&command)
|
||||
.current_dir(working_dir)
|
||||
.stdout(std::process::Stdio::piped())
|
||||
.spawn()
|
||||
.context("Failed to execute bash command")?;
|
||||
|
||||
// Capture stdout with a limit
|
||||
let stdout = cmd.stdout.take().unwrap();
|
||||
let mut reader = BufReader::new(stdout);
|
||||
|
||||
// Read one more byte to determine whether the output was truncated
|
||||
let mut buffer = vec![0; LIMIT + 1];
|
||||
let mut bytes_read = 0;
|
||||
|
||||
// Read until we reach the limit
|
||||
loop {
|
||||
let read = reader.read(&mut buffer[bytes_read..]).await?;
|
||||
if read == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
bytes_read += read;
|
||||
if bytes_read > LIMIT {
|
||||
bytes_read = LIMIT + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Repeatedly fill the output reader's buffer without copying it.
|
||||
loop {
|
||||
let skipped_bytes = reader.fill_buf().await?;
|
||||
if skipped_bytes.is_empty() {
|
||||
break;
|
||||
}
|
||||
let skipped_bytes_len = skipped_bytes.len();
|
||||
reader.consume_unpin(skipped_bytes_len);
|
||||
}
|
||||
|
||||
let output_bytes = &buffer[..bytes_read.min(LIMIT)];
|
||||
|
||||
let status = cmd.status().await.context("Failed to get command status")?;
|
||||
|
||||
let output_string = if bytes_read > LIMIT {
|
||||
// Valid to find `\n` in UTF-8 since 0-127 ASCII characters are not used in
|
||||
// multi-byte characters.
|
||||
let last_line_ix = output_bytes.iter().rposition(|b| *b == b'\n');
|
||||
let until_last_line = &output_bytes[..last_line_ix.unwrap_or(output_bytes.len())];
|
||||
let output_string = String::from_utf8_lossy(until_last_line);
|
||||
|
||||
format!(
|
||||
"Command output too long. The first {} bytes:\n\n{}",
|
||||
output_string.len(),
|
||||
output_block(&output_string),
|
||||
)
|
||||
} else {
|
||||
output_block(&String::from_utf8_lossy(&output_bytes))
|
||||
};
|
||||
|
||||
let output_with_status = if status.success() {
|
||||
if output_string.is_empty() {
|
||||
"Command executed successfully.".to_string()
|
||||
} else {
|
||||
output_string.to_string()
|
||||
}
|
||||
} else {
|
||||
format!(
|
||||
"Command failed with exit code {}\n\n{}",
|
||||
status.code().unwrap_or(-1),
|
||||
output_string,
|
||||
)
|
||||
};
|
||||
|
||||
Ok(output_with_status)
|
||||
}
|
||||
|
||||
fn output_block(output: &str) -> String {
|
||||
format!(
|
||||
"```\n{}{}```",
|
||||
output,
|
||||
if output.ends_with('\n') { "" } else { "\n" }
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(not(windows))]
|
||||
mod tests {
|
||||
use gpui::TestAppContext;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_run_command_simple(cx: &mut TestAppContext) {
|
||||
cx.executor().allow_parking();
|
||||
|
||||
let result =
|
||||
run_command_limited(Path::new(".").into(), "echo 'Hello, World!'".to_string()).await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(result.unwrap(), "```\nHello, World!\n```");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_interleaved_stdout_stderr(cx: &mut TestAppContext) {
|
||||
cx.executor().allow_parking();
|
||||
|
||||
let command =
|
||||
"echo 'stdout 1' && echo 'stderr 1' >&2 && echo 'stdout 2' && echo 'stderr 2' >&2";
|
||||
let result = run_command_limited(Path::new(".").into(), command.to_string()).await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(
|
||||
result.unwrap(),
|
||||
"```\nstdout 1\nstderr 1\nstdout 2\nstderr 2\n```"
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_multiple_output_reads(cx: &mut TestAppContext) {
|
||||
cx.executor().allow_parking();
|
||||
|
||||
// Command with multiple outputs that might require multiple reads
|
||||
let result = run_command_limited(
|
||||
Path::new(".").into(),
|
||||
"echo '1'; sleep 0.01; echo '2'; sleep 0.01; echo '3'".to_string(),
|
||||
)
|
||||
.await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(result.unwrap(), "```\n1\n2\n3\n```");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_output_truncation_single_line(cx: &mut TestAppContext) {
|
||||
cx.executor().allow_parking();
|
||||
|
||||
let cmd = format!("echo '{}';", "X".repeat(LIMIT * 2));
|
||||
|
||||
let result = run_command_limited(Path::new(".").into(), cmd).await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
let output = result.unwrap();
|
||||
|
||||
let content_start = output.find("```\n").map(|i| i + 4).unwrap_or(0);
|
||||
let content_end = output.rfind("\n```").unwrap_or(output.len());
|
||||
let content_length = content_end - content_start;
|
||||
|
||||
// Output should be exactly the limit
|
||||
assert_eq!(content_length, LIMIT);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_output_truncation_multiline(cx: &mut TestAppContext) {
|
||||
cx.executor().allow_parking();
|
||||
|
||||
let cmd = format!("echo '{}'; ", "X".repeat(120)).repeat(160);
|
||||
let result = run_command_limited(Path::new(".").into(), cmd).await;
|
||||
|
||||
assert!(result.is_ok());
|
||||
let output = result.unwrap();
|
||||
|
||||
assert!(output.starts_with("Command output too long. The first 16334 bytes:\n\n"));
|
||||
|
||||
let content_start = output.find("```\n").map(|i| i + 4).unwrap_or(0);
|
||||
let content_end = output.rfind("\n```").unwrap_or(output.len());
|
||||
let content_length = content_end - content_start;
|
||||
|
||||
assert!(content_length <= LIMIT);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,10 @@ pub struct RegexSearchToolInput {
|
||||
/// When not provided, starts from the beginning.
|
||||
#[serde(default)]
|
||||
pub offset: u32,
|
||||
|
||||
/// Whether the regex is case-sensitive. Defaults to false (case-insensitive).
|
||||
#[serde(default)]
|
||||
pub case_sensitive: bool,
|
||||
}
|
||||
|
||||
impl RegexSearchToolInput {
|
||||
@@ -64,12 +68,17 @@ impl Tool for RegexSearchTool {
|
||||
match serde_json::from_value::<RegexSearchToolInput>(input.clone()) {
|
||||
Ok(input) => {
|
||||
let page = input.page();
|
||||
let regex = MarkdownString::inline_code(&input.regex);
|
||||
let regex_str = MarkdownString::inline_code(&input.regex);
|
||||
let case_info = if input.case_sensitive {
|
||||
" (case-sensitive)"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
|
||||
if page > 1 {
|
||||
format!("Get page {page} of search results for regex “{regex}”")
|
||||
format!("Get page {page} of search results for regex {regex_str}{case_info}")
|
||||
} else {
|
||||
format!("Search files for regex “{regex}”")
|
||||
format!("Search files for regex {regex_str}{case_info}")
|
||||
}
|
||||
}
|
||||
Err(_) => "Search with regex".to_string(),
|
||||
@@ -86,15 +95,16 @@ impl Tool for RegexSearchTool {
|
||||
) -> Task<Result<String>> {
|
||||
const CONTEXT_LINES: u32 = 2;
|
||||
|
||||
let (offset, regex) = match serde_json::from_value::<RegexSearchToolInput>(input) {
|
||||
Ok(input) => (input.offset, input.regex),
|
||||
Err(err) => return Task::ready(Err(anyhow!(err))),
|
||||
};
|
||||
let (offset, regex, case_sensitive) =
|
||||
match serde_json::from_value::<RegexSearchToolInput>(input) {
|
||||
Ok(input) => (input.offset, input.regex, input.case_sensitive),
|
||||
Err(err) => return Task::ready(Err(anyhow!(err))),
|
||||
};
|
||||
|
||||
let query = match SearchQuery::regex(
|
||||
®ex,
|
||||
false,
|
||||
false,
|
||||
case_sensitive,
|
||||
false,
|
||||
PathMatcher::default(),
|
||||
PathMatcher::default(),
|
||||
|
||||
@@ -15,6 +15,7 @@ use gpui::{
|
||||
Action, App, AsyncWindowContext, Context, Entity, EntityId, EventEmitter, FocusHandle,
|
||||
Focusable, Subscription, Task, WeakEntity, actions,
|
||||
};
|
||||
|
||||
use project::{
|
||||
Project,
|
||||
debugger::{
|
||||
@@ -94,6 +95,87 @@ impl DebugPanel {
|
||||
})
|
||||
}
|
||||
|
||||
fn filter_action_types(&self, cx: &mut App) {
|
||||
let (has_active_session, supports_restart, support_step_back, status) = self
|
||||
.active_session()
|
||||
.map(|item| {
|
||||
let running = item.read(cx).mode().as_running().cloned();
|
||||
|
||||
match running {
|
||||
Some(running) => {
|
||||
let caps = running.read(cx).capabilities(cx);
|
||||
(
|
||||
!running.read(cx).session().read(cx).is_terminated(),
|
||||
caps.supports_restart_request.unwrap_or_default(),
|
||||
caps.supports_step_back.unwrap_or_default(),
|
||||
running.read(cx).thread_status(cx),
|
||||
)
|
||||
}
|
||||
None => (false, false, false, None),
|
||||
}
|
||||
})
|
||||
.unwrap_or((false, false, false, None));
|
||||
|
||||
let filter = CommandPaletteFilter::global_mut(cx);
|
||||
let debugger_action_types = [
|
||||
TypeId::of::<Disconnect>(),
|
||||
TypeId::of::<Stop>(),
|
||||
TypeId::of::<ToggleIgnoreBreakpoints>(),
|
||||
];
|
||||
|
||||
let running_action_types = [TypeId::of::<Pause>()];
|
||||
|
||||
let stopped_action_type = [
|
||||
TypeId::of::<Continue>(),
|
||||
TypeId::of::<StepOver>(),
|
||||
TypeId::of::<StepInto>(),
|
||||
TypeId::of::<StepOut>(),
|
||||
TypeId::of::<editor::actions::DebuggerRunToCursor>(),
|
||||
TypeId::of::<editor::actions::DebuggerEvaluateSelectedText>(),
|
||||
];
|
||||
|
||||
let step_back_action_type = [TypeId::of::<StepBack>()];
|
||||
let restart_action_type = [TypeId::of::<Restart>()];
|
||||
|
||||
if has_active_session {
|
||||
filter.show_action_types(debugger_action_types.iter());
|
||||
|
||||
if supports_restart {
|
||||
filter.show_action_types(restart_action_type.iter());
|
||||
} else {
|
||||
filter.hide_action_types(&restart_action_type);
|
||||
}
|
||||
|
||||
if support_step_back {
|
||||
filter.show_action_types(step_back_action_type.iter());
|
||||
} else {
|
||||
filter.hide_action_types(&step_back_action_type);
|
||||
}
|
||||
|
||||
match status {
|
||||
Some(ThreadStatus::Running) => {
|
||||
filter.show_action_types(running_action_types.iter());
|
||||
filter.hide_action_types(&stopped_action_type);
|
||||
}
|
||||
Some(ThreadStatus::Stopped) => {
|
||||
filter.show_action_types(stopped_action_type.iter());
|
||||
filter.hide_action_types(&running_action_types);
|
||||
}
|
||||
_ => {
|
||||
filter.hide_action_types(&running_action_types);
|
||||
filter.hide_action_types(&stopped_action_type);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// show only the `debug: start`
|
||||
filter.hide_action_types(&debugger_action_types);
|
||||
filter.hide_action_types(&step_back_action_type);
|
||||
filter.hide_action_types(&restart_action_type);
|
||||
filter.hide_action_types(&running_action_types);
|
||||
filter.hide_action_types(&stopped_action_type);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load(
|
||||
workspace: WeakEntity<Workspace>,
|
||||
cx: AsyncWindowContext,
|
||||
@@ -111,63 +193,15 @@ impl DebugPanel {
|
||||
)
|
||||
});
|
||||
|
||||
cx.observe_new::<DebugPanel>(|debug_panel, _, cx| {
|
||||
Self::filter_action_types(debug_panel, cx);
|
||||
})
|
||||
.detach();
|
||||
|
||||
cx.observe(&debug_panel, |_, debug_panel, cx| {
|
||||
let (has_active_session, supports_restart, support_step_back) = debug_panel
|
||||
.update(cx, |this, cx| {
|
||||
this.active_session()
|
||||
.map(|item| {
|
||||
let running = item.read(cx).mode().as_running().cloned();
|
||||
|
||||
match running {
|
||||
Some(running) => {
|
||||
let caps = running.read(cx).capabilities(cx);
|
||||
(
|
||||
true,
|
||||
caps.supports_restart_request.unwrap_or_default(),
|
||||
caps.supports_step_back.unwrap_or_default(),
|
||||
)
|
||||
}
|
||||
None => (false, false, false),
|
||||
}
|
||||
})
|
||||
.unwrap_or((false, false, false))
|
||||
});
|
||||
|
||||
let filter = CommandPaletteFilter::global_mut(cx);
|
||||
let debugger_action_types = [
|
||||
TypeId::of::<Continue>(),
|
||||
TypeId::of::<StepOver>(),
|
||||
TypeId::of::<StepInto>(),
|
||||
TypeId::of::<StepOut>(),
|
||||
TypeId::of::<Stop>(),
|
||||
TypeId::of::<Disconnect>(),
|
||||
TypeId::of::<Pause>(),
|
||||
TypeId::of::<ToggleIgnoreBreakpoints>(),
|
||||
];
|
||||
|
||||
let step_back_action_type = [TypeId::of::<StepBack>()];
|
||||
let restart_action_type = [TypeId::of::<Restart>()];
|
||||
|
||||
if has_active_session {
|
||||
filter.show_action_types(debugger_action_types.iter());
|
||||
|
||||
if supports_restart {
|
||||
filter.show_action_types(restart_action_type.iter());
|
||||
} else {
|
||||
filter.hide_action_types(&restart_action_type);
|
||||
}
|
||||
|
||||
if support_step_back {
|
||||
filter.show_action_types(step_back_action_type.iter());
|
||||
} else {
|
||||
filter.hide_action_types(&step_back_action_type);
|
||||
}
|
||||
} else {
|
||||
// show only the `debug: start`
|
||||
filter.hide_action_types(&debugger_action_types);
|
||||
filter.hide_action_types(&step_back_action_type);
|
||||
filter.hide_action_types(&restart_action_type);
|
||||
}
|
||||
debug_panel.update(cx, |debug_panel, cx| {
|
||||
Self::filter_action_types(debug_panel, cx);
|
||||
});
|
||||
})
|
||||
.detach();
|
||||
|
||||
@@ -243,6 +277,12 @@ impl DebugPanel {
|
||||
cx,
|
||||
);
|
||||
|
||||
if let Some(running) = session_item.read(cx).mode().as_running().cloned() {
|
||||
// We might want to make this an event subscription and only notify when a new thread is selected
|
||||
// This is used to filter the command menu correctly
|
||||
cx.observe(&running, |_, _, cx| cx.notify()).detach();
|
||||
}
|
||||
|
||||
self.sessions.push(session_item.clone());
|
||||
self.activate_session(session_item, window, cx);
|
||||
}
|
||||
@@ -360,6 +400,8 @@ impl DebugPanel {
|
||||
self.active_session = self.sessions.first().cloned();
|
||||
}
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn sessions_drop_down_menu(
|
||||
@@ -378,7 +420,7 @@ impl DebugPanel {
|
||||
ContextMenu::build(window, cx, move |mut this, _, _| {
|
||||
for session in sessions.into_iter() {
|
||||
let weak_session = session.downgrade();
|
||||
let weak_id = weak_session.entity_id();
|
||||
let weak_session_id = weak_session.entity_id();
|
||||
|
||||
this = this.custom_entry(
|
||||
{
|
||||
@@ -400,7 +442,8 @@ impl DebugPanel {
|
||||
let weak = weak.clone();
|
||||
move |_, _, cx| {
|
||||
weak.update(cx, |panel, cx| {
|
||||
panel.close_session(weak_id, cx);
|
||||
panel
|
||||
.close_session(weak_session_id, cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
use dap::debugger_settings::DebuggerSettings;
|
||||
use debugger_panel::{DebugPanel, ToggleFocus};
|
||||
use editor::Editor;
|
||||
use feature_flags::{Debugger, FeatureFlagViewExt};
|
||||
use gpui::{App, actions};
|
||||
use gpui::{App, EntityInputHandler, actions};
|
||||
use new_session_modal::NewSessionModal;
|
||||
use project::debugger::{self, breakpoint_store::SourceBreakpoint};
|
||||
use session::DebugSession;
|
||||
use settings::Settings;
|
||||
use util::maybe;
|
||||
use workspace::{ShutdownDebugAdapters, Workspace};
|
||||
|
||||
pub mod attach_modal;
|
||||
@@ -110,7 +113,9 @@ pub fn init(cx: &mut App) {
|
||||
.active_session()
|
||||
.and_then(|session| session.read(cx).mode().as_running().cloned())
|
||||
}) {
|
||||
active_item.update(cx, |item, cx| item.stop_thread(cx))
|
||||
cx.defer(move |cx| {
|
||||
active_item.update(cx, |item, cx| item.stop_thread(cx))
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -155,4 +160,91 @@ pub fn init(cx: &mut App) {
|
||||
})
|
||||
})
|
||||
.detach();
|
||||
|
||||
cx.observe_new({
|
||||
move |editor: &mut Editor, _, cx| {
|
||||
editor
|
||||
.register_action(cx.listener(
|
||||
move |editor, _: &editor::actions::DebuggerRunToCursor, _, cx| {
|
||||
maybe!({
|
||||
let debug_panel =
|
||||
editor.workspace()?.read(cx).panel::<DebugPanel>(cx)?;
|
||||
let cursor_point: language::Point = editor.selections.newest(cx).head();
|
||||
let active_session = debug_panel.read(cx).active_session()?;
|
||||
|
||||
let (buffer, position, _) = editor
|
||||
.buffer()
|
||||
.read(cx)
|
||||
.point_to_buffer_point(cursor_point, cx)?;
|
||||
|
||||
let path =
|
||||
debugger::breakpoint_store::BreakpointStore::abs_path_from_buffer(
|
||||
&buffer, cx,
|
||||
)?;
|
||||
|
||||
let source_breakpoint = SourceBreakpoint {
|
||||
row: position.row,
|
||||
path,
|
||||
message: None,
|
||||
condition: None,
|
||||
hit_condition: None,
|
||||
state: debugger::breakpoint_store::BreakpointState::Enabled,
|
||||
};
|
||||
|
||||
active_session
|
||||
.update(cx, |session_item, _| {
|
||||
session_item.mode().as_running().cloned()
|
||||
})?
|
||||
.update(cx, |state, cx| {
|
||||
if let Some(thread_id) = state.selected_thread_id() {
|
||||
state.session().update(cx, |session, cx| {
|
||||
session.run_to_position(
|
||||
source_breakpoint,
|
||||
thread_id,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
Some(())
|
||||
});
|
||||
},
|
||||
))
|
||||
.detach();
|
||||
|
||||
editor
|
||||
.register_action(cx.listener(
|
||||
move |editor, _: &editor::actions::DebuggerEvaluateSelectedText, window, cx| {
|
||||
maybe!({
|
||||
let debug_panel =
|
||||
editor.workspace()?.read(cx).panel::<DebugPanel>(cx)?;
|
||||
let active_session = debug_panel.read(cx).active_session()?;
|
||||
|
||||
let text = editor.text_for_range(
|
||||
editor.selections.newest(cx).range(),
|
||||
&mut None,
|
||||
window,
|
||||
cx,
|
||||
)?;
|
||||
|
||||
active_session
|
||||
.update(cx, |session_item, _| {
|
||||
session_item.mode().as_running().cloned()
|
||||
})?
|
||||
.update(cx, |state, cx| {
|
||||
let stack_id = state.selected_stack_frame_id(cx);
|
||||
|
||||
state.session().update(cx, |session, cx| {
|
||||
session.evaluate(text, None, stack_id, None, cx);
|
||||
})
|
||||
});
|
||||
Some(())
|
||||
});
|
||||
},
|
||||
))
|
||||
.detach();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
pub mod running;
|
||||
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use dap::client::SessionId;
|
||||
use gpui::{App, Entity, EventEmitter, FocusHandle, Focusable, Subscription, Task, WeakEntity};
|
||||
use project::Project;
|
||||
@@ -30,6 +32,7 @@ impl DebugSessionState {
|
||||
pub struct DebugSession {
|
||||
remote_id: Option<workspace::ViewId>,
|
||||
mode: DebugSessionState,
|
||||
label: OnceLock<String>,
|
||||
dap_store: WeakEntity<DapStore>,
|
||||
_debug_panel: WeakEntity<DebugPanel>,
|
||||
_worktree_store: WeakEntity<WorktreeStore>,
|
||||
@@ -68,6 +71,7 @@ impl DebugSession {
|
||||
})],
|
||||
remote_id: None,
|
||||
mode: DebugSessionState::Running(mode),
|
||||
label: OnceLock::new(),
|
||||
dap_store: project.read(cx).dap_store().downgrade(),
|
||||
_debug_panel,
|
||||
_worktree_store: project.read(cx).worktree_store().downgrade(),
|
||||
@@ -92,36 +96,45 @@ impl DebugSession {
|
||||
}
|
||||
|
||||
pub(crate) fn label(&self, cx: &App) -> String {
|
||||
if let Some(label) = self.label.get() {
|
||||
return label.to_owned();
|
||||
}
|
||||
|
||||
let session_id = match &self.mode {
|
||||
DebugSessionState::Running(running_state) => running_state.read(cx).session_id(),
|
||||
};
|
||||
|
||||
let Ok(Some(session)) = self
|
||||
.dap_store
|
||||
.read_with(cx, |store, _| store.session_by_id(session_id))
|
||||
else {
|
||||
return "".to_owned();
|
||||
};
|
||||
session
|
||||
.read(cx)
|
||||
.as_local()
|
||||
.expect("Remote Debug Sessions are not implemented yet")
|
||||
.label()
|
||||
|
||||
self.label
|
||||
.get_or_init(|| {
|
||||
session
|
||||
.read(cx)
|
||||
.as_local()
|
||||
.expect("Remote Debug Sessions are not implemented yet")
|
||||
.label()
|
||||
})
|
||||
.to_owned()
|
||||
}
|
||||
|
||||
pub(crate) fn label_element(&self, cx: &App) -> AnyElement {
|
||||
let label = self.label(cx);
|
||||
|
||||
let (icon, color) = match &self.mode {
|
||||
let icon = match &self.mode {
|
||||
DebugSessionState::Running(state) => {
|
||||
if state.read(cx).session().read(cx).is_terminated() {
|
||||
(Some(Indicator::dot().color(Color::Error)), Color::Error)
|
||||
Some(Indicator::dot().color(Color::Error))
|
||||
} else {
|
||||
match state.read(cx).thread_status(cx).unwrap_or_default() {
|
||||
project::debugger::session::ThreadStatus::Stopped => (
|
||||
Some(Indicator::dot().color(Color::Conflict)),
|
||||
Color::Conflict,
|
||||
),
|
||||
_ => (Some(Indicator::dot().color(Color::Success)), Color::Success),
|
||||
project::debugger::session::ThreadStatus::Stopped => {
|
||||
Some(Indicator::dot().color(Color::Conflict))
|
||||
}
|
||||
_ => Some(Indicator::dot().color(Color::Success)),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -131,7 +144,7 @@ impl DebugSession {
|
||||
.gap_2()
|
||||
.when_some(icon, |this, indicator| this.child(indicator))
|
||||
.justify_between()
|
||||
.child(Label::new(label).color(color))
|
||||
.child(Label::new(label))
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -432,6 +432,10 @@ impl RunningState {
|
||||
self.session_id
|
||||
}
|
||||
|
||||
pub(crate) fn selected_stack_frame_id(&self, cx: &App) -> Option<dap::StackFrameId> {
|
||||
self.stack_frame_list.read(cx).selected_stack_frame_id()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn stack_frame_list(&self) -> &Entity<StackFrameList> {
|
||||
&self.stack_frame_list
|
||||
@@ -492,7 +496,6 @@ impl RunningState {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn selected_thread_id(&self) -> Option<ThreadId> {
|
||||
self.thread_id
|
||||
}
|
||||
|
||||
@@ -141,7 +141,7 @@ impl Console {
|
||||
state.evaluate(
|
||||
expression,
|
||||
Some(dap::EvaluateArgumentsContext::Variables),
|
||||
self.stack_frame_list.read(cx).current_stack_frame_id(),
|
||||
self.stack_frame_list.read(cx).selected_stack_frame_id(),
|
||||
None,
|
||||
cx,
|
||||
);
|
||||
@@ -384,7 +384,7 @@ impl ConsoleQueryBarCompletionProvider {
|
||||
) -> Task<Result<Option<Vec<Completion>>>> {
|
||||
let completion_task = console.update(cx, |console, cx| {
|
||||
console.session.update(cx, |state, cx| {
|
||||
let frame_id = console.stack_frame_list.read(cx).current_stack_frame_id();
|
||||
let frame_id = console.stack_frame_list.read(cx).selected_stack_frame_id();
|
||||
|
||||
state.completions(
|
||||
CompletionsQuery::new(buffer.read(cx), buffer_position, frame_id),
|
||||
|
||||
@@ -31,7 +31,7 @@ pub struct StackFrameList {
|
||||
invalidate: bool,
|
||||
entries: Vec<StackFrameEntry>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
current_stack_frame_id: Option<StackFrameId>,
|
||||
selected_stack_frame_id: Option<StackFrameId>,
|
||||
scrollbar_state: ScrollbarState,
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ impl StackFrameList {
|
||||
_subscription,
|
||||
invalidate: true,
|
||||
entries: Default::default(),
|
||||
current_stack_frame_id: None,
|
||||
selected_stack_frame_id: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,8 +132,8 @@ impl StackFrameList {
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
pub fn current_stack_frame_id(&self) -> Option<StackFrameId> {
|
||||
self.current_stack_frame_id
|
||||
pub fn selected_stack_frame_id(&self) -> Option<StackFrameId> {
|
||||
self.selected_stack_frame_id
|
||||
}
|
||||
|
||||
pub(super) fn refresh(&mut self, cx: &mut Context<Self>) {
|
||||
@@ -188,20 +188,20 @@ impl StackFrameList {
|
||||
}
|
||||
|
||||
pub fn go_to_selected_stack_frame(&mut self, window: &Window, cx: &mut Context<Self>) {
|
||||
if let Some(current_stack_frame_id) = self.current_stack_frame_id {
|
||||
if let Some(selected_stack_frame_id) = self.selected_stack_frame_id {
|
||||
let frame = self
|
||||
.entries
|
||||
.iter()
|
||||
.find_map(|entry| match entry {
|
||||
StackFrameEntry::Normal(dap) => {
|
||||
if dap.id == current_stack_frame_id {
|
||||
if dap.id == selected_stack_frame_id {
|
||||
Some(dap)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
StackFrameEntry::Collapsed(daps) => {
|
||||
daps.iter().find(|dap| dap.id == current_stack_frame_id)
|
||||
daps.iter().find(|dap| dap.id == selected_stack_frame_id)
|
||||
}
|
||||
})
|
||||
.cloned();
|
||||
@@ -220,7 +220,7 @@ impl StackFrameList {
|
||||
window: &Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
self.current_stack_frame_id = Some(stack_frame.id);
|
||||
self.selected_stack_frame_id = Some(stack_frame.id);
|
||||
|
||||
cx.emit(StackFrameListEvent::SelectedStackFrameChanged(
|
||||
stack_frame.id,
|
||||
@@ -319,7 +319,7 @@ impl StackFrameList {
|
||||
cx: &mut Context<Self>,
|
||||
) -> AnyElement {
|
||||
let source = stack_frame.source.clone();
|
||||
let is_selected_frame = Some(stack_frame.id) == self.current_stack_frame_id;
|
||||
let is_selected_frame = Some(stack_frame.id) == self.selected_stack_frame_id;
|
||||
|
||||
let formatted_path = format!(
|
||||
"{}:{}",
|
||||
|
||||
@@ -191,7 +191,7 @@ async fn test_fetch_initial_stack_frames_and_go_to_stack_frame(
|
||||
.update(cx, |state, _| state.stack_frame_list().clone());
|
||||
|
||||
stack_frame_list.update(cx, |stack_frame_list, cx| {
|
||||
assert_eq!(Some(1), stack_frame_list.current_stack_frame_id());
|
||||
assert_eq!(Some(1), stack_frame_list.selected_stack_frame_id());
|
||||
assert_eq!(stack_frames, stack_frame_list.dap_stack_frames(cx));
|
||||
});
|
||||
});
|
||||
@@ -425,7 +425,7 @@ async fn test_select_stack_frame(executor: BackgroundExecutor, cx: &mut TestAppC
|
||||
.unwrap();
|
||||
|
||||
stack_frame_list.update(cx, |stack_frame_list, cx| {
|
||||
assert_eq!(Some(1), stack_frame_list.current_stack_frame_id());
|
||||
assert_eq!(Some(1), stack_frame_list.selected_stack_frame_id());
|
||||
assert_eq!(stack_frames, stack_frame_list.dap_stack_frames(cx));
|
||||
});
|
||||
|
||||
@@ -440,7 +440,7 @@ async fn test_select_stack_frame(executor: BackgroundExecutor, cx: &mut TestAppC
|
||||
cx.run_until_parked();
|
||||
|
||||
stack_frame_list.update(cx, |stack_frame_list, cx| {
|
||||
assert_eq!(Some(2), stack_frame_list.current_stack_frame_id());
|
||||
assert_eq!(Some(2), stack_frame_list.selected_stack_frame_id());
|
||||
assert_eq!(stack_frames, stack_frame_list.dap_stack_frames(cx));
|
||||
});
|
||||
|
||||
|
||||
@@ -212,7 +212,7 @@ async fn test_basic_fetch_initial_scope_and_variables(
|
||||
running_state.update(cx, |running_state, cx| {
|
||||
let (stack_frame_list, stack_frame_id) =
|
||||
running_state.stack_frame_list().update(cx, |list, _| {
|
||||
(list.flatten_entries(), list.current_stack_frame_id())
|
||||
(list.flatten_entries(), list.selected_stack_frame_id())
|
||||
});
|
||||
|
||||
assert_eq!(stack_frames, stack_frame_list);
|
||||
@@ -483,7 +483,7 @@ async fn test_fetch_variables_for_multiple_scopes(
|
||||
running_state.update(cx, |running_state, cx| {
|
||||
let (stack_frame_list, stack_frame_id) =
|
||||
running_state.stack_frame_list().update(cx, |list, _| {
|
||||
(list.flatten_entries(), list.current_stack_frame_id())
|
||||
(list.flatten_entries(), list.selected_stack_frame_id())
|
||||
});
|
||||
|
||||
assert_eq!(Some(1), stack_frame_id);
|
||||
@@ -1565,7 +1565,7 @@ async fn test_variable_list_only_sends_requests_when_rendering(
|
||||
running_state.update(cx, |running_state, cx| {
|
||||
let (stack_frame_list, stack_frame_id) =
|
||||
running_state.stack_frame_list().update(cx, |list, _| {
|
||||
(list.flatten_entries(), list.current_stack_frame_id())
|
||||
(list.flatten_entries(), list.selected_stack_frame_id())
|
||||
});
|
||||
|
||||
assert_eq!(Some(1), stack_frame_id);
|
||||
@@ -1877,7 +1877,7 @@ async fn test_it_fetches_scopes_variables_when_you_select_a_stack_frame(
|
||||
running_state.update(cx, |running_state, cx| {
|
||||
let (stack_frame_list, stack_frame_id) =
|
||||
running_state.stack_frame_list().update(cx, |list, _| {
|
||||
(list.flatten_entries(), list.current_stack_frame_id())
|
||||
(list.flatten_entries(), list.selected_stack_frame_id())
|
||||
});
|
||||
|
||||
let variable_list = running_state.variable_list().read(cx);
|
||||
@@ -1888,7 +1888,7 @@ async fn test_it_fetches_scopes_variables_when_you_select_a_stack_frame(
|
||||
running_state
|
||||
.stack_frame_list()
|
||||
.read(cx)
|
||||
.current_stack_frame_id(),
|
||||
.selected_stack_frame_id(),
|
||||
Some(1)
|
||||
);
|
||||
|
||||
@@ -1934,7 +1934,7 @@ async fn test_it_fetches_scopes_variables_when_you_select_a_stack_frame(
|
||||
running_state.update(cx, |running_state, cx| {
|
||||
let (stack_frame_list, stack_frame_id) =
|
||||
running_state.stack_frame_list().update(cx, |list, _| {
|
||||
(list.flatten_entries(), list.current_stack_frame_id())
|
||||
(list.flatten_entries(), list.selected_stack_frame_id())
|
||||
});
|
||||
|
||||
let variable_list = running_state.variable_list().read(cx);
|
||||
|
||||
@@ -35,6 +35,7 @@ assets.workspace = true
|
||||
client.workspace = true
|
||||
clock.workspace = true
|
||||
collections.workspace = true
|
||||
command_palette_hooks.workspace = true
|
||||
convert_case.workspace = true
|
||||
db.workspace = true
|
||||
buffer_diff.workspace = true
|
||||
|
||||
@@ -110,20 +110,6 @@ pub struct ToggleComments {
|
||||
pub ignore_indent: bool,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct FoldAt {
|
||||
#[serde(skip)]
|
||||
pub buffer_row: MultiBufferRow,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct UnfoldAt {
|
||||
#[serde(skip)]
|
||||
pub buffer_row: MultiBufferRow,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct MoveUpByLines {
|
||||
@@ -226,7 +212,6 @@ impl_actions!(
|
||||
ExpandExcerpts,
|
||||
ExpandExcerptsDown,
|
||||
ExpandExcerptsUp,
|
||||
FoldAt,
|
||||
HandleInput,
|
||||
MoveDownByLines,
|
||||
MovePageDown,
|
||||
@@ -244,7 +229,6 @@ impl_actions!(
|
||||
ShowCompletions,
|
||||
ToggleCodeActions,
|
||||
ToggleComments,
|
||||
UnfoldAt,
|
||||
FoldAtLevel,
|
||||
]
|
||||
);
|
||||
@@ -424,6 +408,8 @@ actions!(
|
||||
DisableBreakpoint,
|
||||
EnableBreakpoint,
|
||||
EditLogBreakpoint,
|
||||
DebuggerRunToCursor,
|
||||
DebuggerEvaluateSelectedText,
|
||||
ToggleAutoSignatureHelp,
|
||||
ToggleGitBlameInline,
|
||||
OpenGitBlameCommit,
|
||||
|
||||
@@ -6365,9 +6365,20 @@ impl Editor {
|
||||
breakpoint_display_points
|
||||
}
|
||||
|
||||
fn breakpoint_context_menu(
|
||||
fn edit_breakpoint_context_menu(
|
||||
&self,
|
||||
anchor: Anchor,
|
||||
window: &Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Entity<ui::ContextMenu> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn basic_breakpoint_context_menu(
|
||||
&self,
|
||||
anchor: Anchor,
|
||||
edit_menu_position: (Anchor, gpui::Point<Pixels>),
|
||||
is_edit_menu: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Entity<ui::ContextMenu> {
|
||||
@@ -6385,12 +6396,6 @@ impl Editor {
|
||||
.breakpoint_at_row(row, window, cx)
|
||||
.map(|(anchor, bp)| (anchor, Arc::from(bp)));
|
||||
|
||||
let log_breakpoint_msg = if breakpoint.as_ref().is_some_and(|bp| bp.1.message.is_some()) {
|
||||
"Edit Log Breakpoint"
|
||||
} else {
|
||||
"Set Log Breakpoint"
|
||||
};
|
||||
|
||||
let condition_breakpoint_msg = if breakpoint
|
||||
.as_ref()
|
||||
.is_some_and(|bp| bp.1.condition.is_some())
|
||||
@@ -6415,9 +6420,16 @@ impl Editor {
|
||||
"Set Breakpoint"
|
||||
};
|
||||
|
||||
let toggle_state_msg = breakpoint.as_ref().map_or(None, |bp| match bp.1.state {
|
||||
BreakpointState::Enabled => Some("Disable"),
|
||||
BreakpointState::Disabled => Some("Enable"),
|
||||
let run_to_cursor = command_palette_hooks::CommandPaletteFilter::try_global(cx)
|
||||
.map_or(false, |filter| !filter.is_hidden(&DebuggerRunToCursor));
|
||||
|
||||
let breakpoint_is_at_row = breakpoint.is_some();
|
||||
let log_editor = cx.new(|cx| {
|
||||
let mut log_editor = Editor::single_line(window, cx);
|
||||
if let Some(text) = &breakpoint.as_ref().and_then(|bp| bp.1.message.clone()) {
|
||||
log_editor.insert(text.as_ref(), window, cx);
|
||||
}
|
||||
log_editor
|
||||
});
|
||||
|
||||
let (anchor, breakpoint) =
|
||||
@@ -6426,8 +6438,88 @@ impl Editor {
|
||||
ui::ContextMenu::build(window, cx, |menu, _, _cx| {
|
||||
menu.on_blur_subscription(Subscription::new(|| {}))
|
||||
.context(focus_handle)
|
||||
.when_some(toggle_state_msg, |this, msg| {
|
||||
this.entry(msg, None, {
|
||||
.when(run_to_cursor, |this| {
|
||||
let weak_editor = weak_editor.clone();
|
||||
this.entry("Run to cursor", None, move |window, cx| {
|
||||
weak_editor
|
||||
.update(cx, |editor, cx| {
|
||||
editor.change_selections(None, window, cx, |s| {
|
||||
s.select_ranges([Point::new(row, 0)..Point::new(row, 0)])
|
||||
});
|
||||
})
|
||||
.ok();
|
||||
|
||||
window.dispatch_action(Box::new(DebuggerRunToCursor), cx);
|
||||
})
|
||||
.separator()
|
||||
})
|
||||
.entry("edit breakpoint", None, {
|
||||
let weak_editor = weak_editor.clone();
|
||||
move |window, cx| {
|
||||
weak_editor
|
||||
.update(cx, |editor, cx| {
|
||||
let context_menu =
|
||||
editor.edit_breakpoint_context_menu(anchor, window, cx);
|
||||
let (source, clicked_point) = edit_menu_position;
|
||||
|
||||
editor.mouse_context_menu = MouseContextMenu::pinned_to_editor(
|
||||
editor,
|
||||
source,
|
||||
clicked_point,
|
||||
context_menu,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
.when(breakpoint_is_at_row, |this| {
|
||||
let weak_editor = weak_editor.clone();
|
||||
let breakpoint = breakpoint.clone();
|
||||
|
||||
this.custom_row(move |window, cx| {
|
||||
let breakpoint = weak_editor
|
||||
.update(cx, |editor, cx| {
|
||||
editor.breakpoint_at_row(row, window, cx).map(|bp| bp.1)
|
||||
})
|
||||
.ok()
|
||||
.flatten()
|
||||
.unwrap_or_else(|| breakpoint.as_ref().clone());
|
||||
|
||||
let is_enabled = match breakpoint.is_enabled() {
|
||||
true => ToggleState::Selected,
|
||||
false => ToggleState::Unselected,
|
||||
};
|
||||
|
||||
ui::CheckboxWithLabel::new(
|
||||
"enable-breakpoint",
|
||||
Label::new("Enable"),
|
||||
is_enabled,
|
||||
{
|
||||
let weak_editor = weak_editor.clone();
|
||||
let breakpoint = breakpoint.clone();
|
||||
|
||||
move |_, _, cx| {
|
||||
weak_editor
|
||||
.update(cx, |this, cx| {
|
||||
this.edit_breakpoint_at_anchor(
|
||||
anchor,
|
||||
breakpoint.clone(),
|
||||
BreakpointEditAction::InvertState,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
},
|
||||
)
|
||||
.checkbox_position(IconPosition::End)
|
||||
.into_any_element()
|
||||
})
|
||||
})
|
||||
.when(!breakpoint_is_at_row, |this| {
|
||||
this.entry(set_breakpoint_msg, None, {
|
||||
let weak_editor = weak_editor.clone();
|
||||
let breakpoint = breakpoint.clone();
|
||||
move |_window, cx| {
|
||||
@@ -6436,7 +6528,7 @@ impl Editor {
|
||||
this.edit_breakpoint_at_anchor(
|
||||
anchor,
|
||||
breakpoint.as_ref().clone(),
|
||||
BreakpointEditAction::InvertState,
|
||||
BreakpointEditAction::Toggle,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
@@ -6444,69 +6536,72 @@ impl Editor {
|
||||
}
|
||||
})
|
||||
})
|
||||
.entry(set_breakpoint_msg, None, {
|
||||
let weak_editor = weak_editor.clone();
|
||||
let breakpoint = breakpoint.clone();
|
||||
move |_window, cx| {
|
||||
weak_editor
|
||||
.update(cx, |this, cx| {
|
||||
this.edit_breakpoint_at_anchor(
|
||||
anchor,
|
||||
breakpoint.as_ref().clone(),
|
||||
BreakpointEditAction::Toggle,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
})
|
||||
.entry(log_breakpoint_msg, None, {
|
||||
let breakpoint = breakpoint.clone();
|
||||
let weak_editor = weak_editor.clone();
|
||||
move |window, cx| {
|
||||
weak_editor
|
||||
.update(cx, |this, cx| {
|
||||
this.add_edit_breakpoint_block(
|
||||
anchor,
|
||||
breakpoint.as_ref(),
|
||||
BreakpointPromptEditAction::Log,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
})
|
||||
.entry(condition_breakpoint_msg, None, {
|
||||
let breakpoint = breakpoint.clone();
|
||||
let weak_editor = weak_editor.clone();
|
||||
move |window, cx| {
|
||||
weak_editor
|
||||
.update(cx, |this, cx| {
|
||||
this.add_edit_breakpoint_block(
|
||||
anchor,
|
||||
breakpoint.as_ref(),
|
||||
BreakpointPromptEditAction::Condition,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
})
|
||||
.entry(hit_condition_breakpoint_msg, None, move |window, cx| {
|
||||
weak_editor
|
||||
.update(cx, |this, cx| {
|
||||
this.add_edit_breakpoint_block(
|
||||
.when(is_edit_menu, |this| {
|
||||
this.custom_entry(
|
||||
{
|
||||
breakpoint_message_editor_element(
|
||||
anchor,
|
||||
breakpoint.as_ref(),
|
||||
BreakpointPromptEditAction::HitCondition,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.log_err();
|
||||
breakpoint.clone(),
|
||||
BreakpointMessageKind::Log,
|
||||
log_editor.clone(),
|
||||
weak_editor.clone(),
|
||||
)
|
||||
},
|
||||
{
|
||||
let breakpoint = breakpoint.clone();
|
||||
let weak_editor = weak_editor.clone();
|
||||
let log_editor = log_editor.clone();
|
||||
|
||||
move |_, cx| {
|
||||
let log_message = log_editor.read(cx).text(cx);
|
||||
|
||||
weak_editor
|
||||
.update(cx, |this, cx| {
|
||||
this.edit_breakpoint_at_anchor(
|
||||
anchor,
|
||||
breakpoint.as_ref().clone(),
|
||||
BreakpointEditAction::EditLogMessage(
|
||||
log_message.into(),
|
||||
),
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
// .entry(condition_breakpoint_msg, None, {
|
||||
// let breakpoint = breakpoint.clone();
|
||||
// let weak_editor = weak_editor.clone();
|
||||
// move |window, cx| {
|
||||
// weak_editor
|
||||
// .update(cx, |this, cx| {
|
||||
// this.add_edit_breakpoint_block(
|
||||
// anchor,
|
||||
// breakpoint.as_ref(),
|
||||
// BreakpointPromptEditAction::Condition,
|
||||
// window,
|
||||
// cx,
|
||||
// );
|
||||
// })
|
||||
// .log_err();
|
||||
// }
|
||||
// })
|
||||
// .entry(hit_condition_breakpoint_msg, None, move |window, cx| {
|
||||
// weak_editor
|
||||
// .update(cx, |this, cx| {
|
||||
// this.add_edit_breakpoint_block(
|
||||
// anchor,
|
||||
// breakpoint.as_ref(),
|
||||
// BreakpointPromptEditAction::HitCondition,
|
||||
// window,
|
||||
// cx,
|
||||
// );
|
||||
// })
|
||||
// .log_err();
|
||||
// })
|
||||
})
|
||||
}
|
||||
|
||||
@@ -8209,12 +8304,18 @@ impl Editor {
|
||||
IndentSize::tab()
|
||||
} else {
|
||||
let tab_size = settings.tab_size.get();
|
||||
let char_column = snapshot
|
||||
let indent_remainder = snapshot
|
||||
.text_for_range(Point::new(cursor.row, 0)..cursor)
|
||||
.flat_map(str::chars)
|
||||
.count()
|
||||
+ row_delta as usize;
|
||||
let chars_to_next_tab_stop = tab_size - (char_column as u32 % tab_size);
|
||||
.fold(row_delta % tab_size, |counter: u32, c| {
|
||||
if c == '\t' {
|
||||
0
|
||||
} else {
|
||||
(counter + 1) % tab_size
|
||||
}
|
||||
});
|
||||
|
||||
let chars_to_next_tab_stop = tab_size - indent_remainder;
|
||||
IndentSize::spaces(chars_to_next_tab_stop)
|
||||
};
|
||||
selection.start = Point::new(cursor.row, cursor.column + row_delta + tab_size.len);
|
||||
@@ -8721,6 +8822,48 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
fn render_breakpoint_menu_editor(
|
||||
editor: &Entity<Editor>,
|
||||
window: &mut Window,
|
||||
cx: &App,
|
||||
) -> impl IntoElement {
|
||||
let settings = ThemeSettings::get_global(cx);
|
||||
let theme = cx.theme();
|
||||
|
||||
let text_style = TextStyle {
|
||||
color: cx.theme().colors().text,
|
||||
font_family: settings.buffer_font.family.clone(),
|
||||
font_features: settings.buffer_font.features.clone(),
|
||||
font_size: AbsoluteLength::Rems(Rems(0.75)),
|
||||
font_weight: settings.buffer_font.weight,
|
||||
line_height: relative(settings.buffer_line_height.value()),
|
||||
background_color: Some(theme.colors().editor_background),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let element = EditorElement::new(
|
||||
editor,
|
||||
EditorStyle {
|
||||
background: theme.colors().editor_background,
|
||||
local_player: theme.players().local(),
|
||||
text: text_style,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
div()
|
||||
.rounded_sm()
|
||||
.when(
|
||||
editor.focus_handle(cx).contains_focused(window, cx),
|
||||
|this| this.border_color(theme.colors().border_focused),
|
||||
)
|
||||
.child(element)
|
||||
.min_w(Pixels(300.0))
|
||||
.size_full()
|
||||
.bg(theme.colors().editor_background)
|
||||
.h_4()
|
||||
}
|
||||
|
||||
fn set_breakpoint_context_menu(
|
||||
&mut self,
|
||||
display_row: DisplayRow,
|
||||
@@ -8738,7 +8881,13 @@ impl Editor {
|
||||
.snapshot(cx)
|
||||
.anchor_before(Point::new(display_row.0, 0u32));
|
||||
|
||||
let context_menu = self.breakpoint_context_menu(position.unwrap_or(source), window, cx);
|
||||
let context_menu = self.basic_breakpoint_context_menu(
|
||||
position.unwrap_or(source),
|
||||
(source, clicked_point),
|
||||
false,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
|
||||
self.mouse_context_menu = MouseContextMenu::pinned_to_editor(
|
||||
self,
|
||||
@@ -11567,7 +11716,7 @@ impl Editor {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Editor>,
|
||||
) {
|
||||
this.unfold_ranges(&[range.clone()], false, true, cx);
|
||||
this.unfold_ranges(&[range.clone()], false, auto_scroll.is_some(), cx);
|
||||
this.change_selections(auto_scroll, window, cx, |s| {
|
||||
if replace_newest {
|
||||
s.delete(s.newest_anchor().id);
|
||||
@@ -11742,16 +11891,21 @@ impl Editor {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut new_selections = self.selections.all::<usize>(cx);
|
||||
let mut new_selections = Vec::new();
|
||||
|
||||
let reversed = self.selections.oldest::<usize>(cx).reversed;
|
||||
let buffer = &display_map.buffer_snapshot;
|
||||
let query_matches = select_next_state
|
||||
.query
|
||||
.stream_find_iter(buffer.bytes_in_range(0..buffer.len()));
|
||||
|
||||
for query_match in query_matches {
|
||||
let query_match = query_match.unwrap(); // can only fail due to I/O
|
||||
let offset_range = query_match.start()..query_match.end();
|
||||
for query_match in query_matches.into_iter() {
|
||||
let query_match = query_match.context("query match for select all action")?; // can only fail due to I/O
|
||||
let offset_range = if reversed {
|
||||
query_match.end()..query_match.start()
|
||||
} else {
|
||||
query_match.start()..query_match.end()
|
||||
};
|
||||
let display_range = offset_range.start.to_display_point(&display_map)
|
||||
..offset_range.end.to_display_point(&display_map);
|
||||
|
||||
@@ -11759,52 +11913,14 @@ impl Editor {
|
||||
|| (!movement::is_inside_word(&display_map, display_range.start)
|
||||
&& !movement::is_inside_word(&display_map, display_range.end))
|
||||
{
|
||||
self.selections.change_with(cx, |selections| {
|
||||
new_selections.push(Selection {
|
||||
id: selections.new_selection_id(),
|
||||
start: offset_range.start,
|
||||
end: offset_range.end,
|
||||
reversed: false,
|
||||
goal: SelectionGoal::None,
|
||||
});
|
||||
});
|
||||
new_selections.push(offset_range.start..offset_range.end);
|
||||
}
|
||||
}
|
||||
|
||||
new_selections.sort_by_key(|selection| selection.start);
|
||||
let mut ix = 0;
|
||||
while ix + 1 < new_selections.len() {
|
||||
let current_selection = &new_selections[ix];
|
||||
let next_selection = &new_selections[ix + 1];
|
||||
if current_selection.range().overlaps(&next_selection.range()) {
|
||||
if current_selection.id < next_selection.id {
|
||||
new_selections.remove(ix + 1);
|
||||
} else {
|
||||
new_selections.remove(ix);
|
||||
}
|
||||
} else {
|
||||
ix += 1;
|
||||
}
|
||||
}
|
||||
|
||||
let reversed = self.selections.oldest::<usize>(cx).reversed;
|
||||
|
||||
for selection in new_selections.iter_mut() {
|
||||
selection.reversed = reversed;
|
||||
}
|
||||
|
||||
select_next_state.done = true;
|
||||
self.unfold_ranges(
|
||||
&new_selections
|
||||
.iter()
|
||||
.map(|selection| selection.range())
|
||||
.collect::<Vec<_>>(),
|
||||
false,
|
||||
false,
|
||||
cx,
|
||||
);
|
||||
self.change_selections(Some(Autoscroll::fit()), window, cx, |selections| {
|
||||
selections.select(new_selections)
|
||||
self.unfold_ranges(&new_selections.clone(), false, false, cx);
|
||||
self.change_selections(None, window, cx, |selections| {
|
||||
selections.select_ranges(new_selections)
|
||||
});
|
||||
|
||||
Ok(())
|
||||
@@ -14909,8 +15025,12 @@ impl Editor {
|
||||
self.fold_creases(to_fold, true, window, cx);
|
||||
}
|
||||
|
||||
pub fn fold_at(&mut self, fold_at: &FoldAt, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let buffer_row = fold_at.buffer_row;
|
||||
pub fn fold_at(
|
||||
&mut self,
|
||||
buffer_row: MultiBufferRow,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
|
||||
|
||||
if let Some(crease) = display_map.crease_for_buffer_row(buffer_row) {
|
||||
@@ -14980,16 +15100,16 @@ impl Editor {
|
||||
|
||||
pub fn unfold_at(
|
||||
&mut self,
|
||||
unfold_at: &UnfoldAt,
|
||||
buffer_row: MultiBufferRow,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
|
||||
|
||||
let intersection_range = Point::new(unfold_at.buffer_row.0, 0)
|
||||
let intersection_range = Point::new(buffer_row.0, 0)
|
||||
..Point::new(
|
||||
unfold_at.buffer_row.0,
|
||||
display_map.buffer_snapshot.line_len(unfold_at.buffer_row),
|
||||
buffer_row.0,
|
||||
display_map.buffer_snapshot.line_len(buffer_row),
|
||||
);
|
||||
|
||||
let autoscroll = self
|
||||
@@ -18014,6 +18134,58 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
enum BreakpointMessageKind {
|
||||
Log,
|
||||
Conditional,
|
||||
HitConditional,
|
||||
}
|
||||
|
||||
fn breakpoint_message_editor_element(
|
||||
anchor: Anchor,
|
||||
breakpoint: Arc<Breakpoint>,
|
||||
message_kind: BreakpointMessageKind,
|
||||
message_editor: Entity<Editor>,
|
||||
weak_editor: WeakEntity<Editor>,
|
||||
) -> impl Fn(&mut Window, &mut App) -> AnyElement {
|
||||
let log_editor = message_editor.clone();
|
||||
let breakpoint = breakpoint.clone();
|
||||
let weak_editor = weak_editor.clone();
|
||||
|
||||
move |window, cx| {
|
||||
let label = Label::new("Log message");
|
||||
let log_editor = log_editor.clone();
|
||||
let breakpoint = breakpoint.clone();
|
||||
let weak_editor = weak_editor.clone();
|
||||
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.size_full()
|
||||
.child(label)
|
||||
.child(Editor::render_breakpoint_menu_editor(
|
||||
&log_editor.clone(),
|
||||
window,
|
||||
cx,
|
||||
))
|
||||
.on_action(move |_: &menu::Confirm, _, cx| {
|
||||
let log_message = log_editor.read(cx).text(cx);
|
||||
|
||||
weak_editor
|
||||
.update(cx, |this, cx| {
|
||||
this.edit_breakpoint_at_anchor(
|
||||
anchor,
|
||||
breakpoint.as_ref().clone(),
|
||||
BreakpointEditAction::EditLogMessage(log_message.into()),
|
||||
cx,
|
||||
);
|
||||
|
||||
this.mouse_context_menu = None;
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
// Consider user intent and default settings
|
||||
fn choose_completion_range(
|
||||
completion: &Completion,
|
||||
@@ -19358,15 +19530,11 @@ impl EditorSnapshot {
|
||||
Arc::new(move |folded, window: &mut Window, cx: &mut App| {
|
||||
if folded {
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.fold_at(&crate::FoldAt { buffer_row }, window, cx)
|
||||
editor.fold_at(buffer_row, window, cx)
|
||||
});
|
||||
} else {
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.unfold_at(
|
||||
&crate::UnfoldAt { buffer_row },
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
editor.unfold_at(buffer_row, window, cx)
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -19390,9 +19558,9 @@ impl EditorSnapshot {
|
||||
.toggle_state(folded)
|
||||
.on_click(window.listener_for(&editor, move |this, _e, window, cx| {
|
||||
if folded {
|
||||
this.unfold_at(&UnfoldAt { buffer_row }, window, cx);
|
||||
this.unfold_at(buffer_row, window, cx);
|
||||
} else {
|
||||
this.fold_at(&FoldAt { buffer_row }, window, cx);
|
||||
this.fold_at(buffer_row, window, cx);
|
||||
}
|
||||
}))
|
||||
.into_any_element(),
|
||||
|
||||
@@ -2918,7 +2918,32 @@ async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut TestAppConte
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_tab_with_mixed_whitespace(cx: &mut TestAppContext) {
|
||||
async fn test_tab_with_mixed_whitespace_txt(cx: &mut TestAppContext) {
|
||||
init_test(cx, |settings| {
|
||||
settings.defaults.tab_size = NonZeroU32::new(3)
|
||||
});
|
||||
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
cx.set_state(indoc! {"
|
||||
ˇ
|
||||
\t ˇ
|
||||
\t ˇ
|
||||
\t ˇ
|
||||
\t \t\t \t \t\t \t\t \t \t ˇ
|
||||
"});
|
||||
|
||||
cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
|
||||
cx.assert_editor_state(indoc! {"
|
||||
ˇ
|
||||
\t ˇ
|
||||
\t ˇ
|
||||
\t ˇ
|
||||
\t \t\t \t \t\t \t\t \t \t ˇ
|
||||
"});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_tab_with_mixed_whitespace_rust(cx: &mut TestAppContext) {
|
||||
init_test(cx, |settings| {
|
||||
settings.defaults.tab_size = NonZeroU32::new(4)
|
||||
});
|
||||
@@ -5817,6 +5842,37 @@ async fn test_select_all_matches(cx: &mut TestAppContext) {
|
||||
cx.assert_editor_state("abc\n« ˇ»abc\nabc");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_select_all_matches_does_not_scroll(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
|
||||
let large_body_1 = "\nd".repeat(200);
|
||||
let large_body_2 = "\ne".repeat(200);
|
||||
|
||||
cx.set_state(&format!(
|
||||
"abc\nabc{large_body_1} «ˇa»bc{large_body_2}\nefabc\nabc"
|
||||
));
|
||||
let initial_scroll_position = cx.update_editor(|editor, _, cx| {
|
||||
let scroll_position = editor.scroll_position(cx);
|
||||
assert!(scroll_position.y > 0.0, "Initial selection is between two large bodies and should have the editor scrolled to it");
|
||||
scroll_position
|
||||
});
|
||||
|
||||
cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
|
||||
.unwrap();
|
||||
cx.assert_editor_state(&format!(
|
||||
"«ˇa»bc\n«ˇa»bc{large_body_1} «ˇa»bc{large_body_2}\nef«ˇa»bc\n«ˇa»bc"
|
||||
));
|
||||
let scroll_position_after_selection =
|
||||
cx.update_editor(|editor, _, cx| editor.scroll_position(cx));
|
||||
assert_eq!(
|
||||
initial_scroll_position, scroll_position_after_selection,
|
||||
"Scroll position should not change after selecting all matches"
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
@@ -387,14 +387,12 @@ impl EditorElement {
|
||||
register_action(editor, window, Editor::fold_at_level);
|
||||
register_action(editor, window, Editor::fold_all);
|
||||
register_action(editor, window, Editor::fold_function_bodies);
|
||||
register_action(editor, window, Editor::fold_at);
|
||||
register_action(editor, window, Editor::fold_recursive);
|
||||
register_action(editor, window, Editor::toggle_fold);
|
||||
register_action(editor, window, Editor::toggle_fold_recursive);
|
||||
register_action(editor, window, Editor::unfold_lines);
|
||||
register_action(editor, window, Editor::unfold_recursive);
|
||||
register_action(editor, window, Editor::unfold_all);
|
||||
register_action(editor, window, Editor::unfold_at);
|
||||
register_action(editor, window, Editor::fold_selected_ranges);
|
||||
register_action(editor, window, Editor::set_mark);
|
||||
register_action(editor, window, Editor::swap_selection_ends);
|
||||
@@ -3892,7 +3890,15 @@ impl EditorElement {
|
||||
)
|
||||
})?;
|
||||
|
||||
element.prepaint_as_root(position, AvailableSpace::min_size(), window, cx);
|
||||
element.prepaint_as_root(
|
||||
position,
|
||||
Size {
|
||||
width: AvailableSpace::Definite(Pixels(100.)),
|
||||
height: AvailableSpace::MinContent,
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
Some(element)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use crate::CopyAndTrim;
|
||||
use crate::actions::FormatSelections;
|
||||
use crate::{
|
||||
Copy, CopyPermalinkToLine, Cut, DisplayPoint, DisplaySnapshot, Editor, EditorMode,
|
||||
FindAllReferences, GoToDeclaration, GoToDefinition, GoToImplementation, GoToTypeDefinition,
|
||||
Paste, Rename, RevealInFileManager, SelectMode, ToDisplayPoint, ToggleCodeActions,
|
||||
actions::Format, selections_collection::SelectionsCollection,
|
||||
Copy, CopyAndTrim, CopyPermalinkToLine, Cut, DebuggerEvaluateSelectedText, DisplayPoint,
|
||||
DisplaySnapshot, Editor, EditorMode, FindAllReferences, GoToDeclaration, GoToDefinition,
|
||||
GoToImplementation, GoToTypeDefinition, Paste, Rename, RevealInFileManager, SelectMode,
|
||||
ToDisplayPoint, ToggleCodeActions,
|
||||
actions::{Format, FormatSelections},
|
||||
selections_collection::SelectionsCollection,
|
||||
};
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{Context, DismissEvent, Entity, Focusable as _, Pixels, Point, Subscription, Window};
|
||||
@@ -169,9 +169,19 @@ pub fn deploy_context_menu(
|
||||
.is_some()
|
||||
});
|
||||
|
||||
let evaluate_selection = command_palette_hooks::CommandPaletteFilter::try_global(cx)
|
||||
.map_or(false, |filter| {
|
||||
!filter.is_hidden(&DebuggerEvaluateSelectedText)
|
||||
});
|
||||
|
||||
ui::ContextMenu::build(window, cx, |menu, _window, _cx| {
|
||||
let builder = menu
|
||||
.on_blur_subscription(Subscription::new(|| {}))
|
||||
.when(evaluate_selection && has_selections, |builder| {
|
||||
builder
|
||||
.action("Evaluate Selection", Box::new(DebuggerEvaluateSelectedText))
|
||||
.separator()
|
||||
})
|
||||
.action("Go to Definition", Box::new(GoToDefinition))
|
||||
.action("Go to Declaration", Box::new(GoToDeclaration))
|
||||
.action("Go to Type Definition", Box::new(GoToTypeDefinition))
|
||||
|
||||
@@ -105,21 +105,56 @@ enum TrashCancel {
|
||||
Cancel,
|
||||
}
|
||||
|
||||
struct GitMenuState {
|
||||
has_tracked_changes: bool,
|
||||
has_staged_changes: bool,
|
||||
has_unstaged_changes: bool,
|
||||
has_new_changes: bool,
|
||||
}
|
||||
|
||||
fn git_panel_context_menu(
|
||||
focus_handle: FocusHandle,
|
||||
state: GitMenuState,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Entity<ContextMenu> {
|
||||
ContextMenu::build(window, cx, |context_menu, _, _| {
|
||||
ContextMenu::build(window, cx, move |context_menu, _, _| {
|
||||
context_menu
|
||||
.context(focus_handle)
|
||||
.action("Stage All", StageAll.boxed_clone())
|
||||
.action("Unstage All", UnstageAll.boxed_clone())
|
||||
.map(|menu| {
|
||||
if state.has_unstaged_changes {
|
||||
menu.action("Stage All", StageAll.boxed_clone())
|
||||
} else {
|
||||
menu.disabled_action("Stage All", StageAll.boxed_clone())
|
||||
}
|
||||
})
|
||||
.map(|menu| {
|
||||
if state.has_staged_changes {
|
||||
menu.action("Unstage All", UnstageAll.boxed_clone())
|
||||
} else {
|
||||
menu.disabled_action("Unstage All", UnstageAll.boxed_clone())
|
||||
}
|
||||
})
|
||||
.separator()
|
||||
.action("Open Diff", project_diff::Diff.boxed_clone())
|
||||
.separator()
|
||||
.action("Discard Tracked Changes", RestoreTrackedFiles.boxed_clone())
|
||||
.action("Trash Untracked Files", TrashUntrackedFiles.boxed_clone())
|
||||
.map(|menu| {
|
||||
if state.has_tracked_changes {
|
||||
menu.action("Discard Tracked Changes", RestoreTrackedFiles.boxed_clone())
|
||||
} else {
|
||||
menu.disabled_action(
|
||||
"Discard Tracked Changes",
|
||||
RestoreTrackedFiles.boxed_clone(),
|
||||
)
|
||||
}
|
||||
})
|
||||
.map(|menu| {
|
||||
if state.has_new_changes {
|
||||
menu.action("Trash Untracked Files", TrashUntrackedFiles.boxed_clone())
|
||||
} else {
|
||||
menu.disabled_action("Trash Untracked Files", TrashUntrackedFiles.boxed_clone())
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2571,13 +2606,30 @@ impl GitPanel {
|
||||
|
||||
fn render_overflow_menu(&self, id: impl Into<ElementId>) -> impl IntoElement {
|
||||
let focus_handle = self.focus_handle.clone();
|
||||
let has_tracked_changes = self.has_tracked_changes();
|
||||
let has_staged_changes = self.has_staged_changes();
|
||||
let has_unstaged_changes = self.has_unstaged_changes();
|
||||
let has_new_changes = self.new_count > 0;
|
||||
|
||||
PopoverMenu::new(id.into())
|
||||
.trigger(
|
||||
IconButton::new("overflow-menu-trigger", IconName::EllipsisVertical)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted),
|
||||
)
|
||||
.menu(move |window, cx| Some(git_panel_context_menu(focus_handle.clone(), window, cx)))
|
||||
.menu(move |window, cx| {
|
||||
Some(git_panel_context_menu(
|
||||
focus_handle.clone(),
|
||||
GitMenuState {
|
||||
has_tracked_changes,
|
||||
has_staged_changes,
|
||||
has_unstaged_changes,
|
||||
has_new_changes,
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
))
|
||||
})
|
||||
.anchor(Corner::TopRight)
|
||||
}
|
||||
|
||||
@@ -3449,7 +3501,17 @@ impl GitPanel {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let context_menu = git_panel_context_menu(self.focus_handle.clone(), window, cx);
|
||||
let context_menu = git_panel_context_menu(
|
||||
self.focus_handle.clone(),
|
||||
GitMenuState {
|
||||
has_tracked_changes: self.has_tracked_changes(),
|
||||
has_staged_changes: self.has_staged_changes(),
|
||||
has_unstaged_changes: self.has_unstaged_changes(),
|
||||
has_new_changes: self.new_count > 0,
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
self.set_context_menu(context_menu, position, window, cx);
|
||||
}
|
||||
|
||||
|
||||
@@ -218,7 +218,7 @@ impl BreakpointStore {
|
||||
}
|
||||
}
|
||||
|
||||
fn abs_path_from_buffer(buffer: &Entity<Buffer>, cx: &App) -> Option<Arc<Path>> {
|
||||
pub fn abs_path_from_buffer(buffer: &Entity<Buffer>, cx: &App) -> Option<Arc<Path>> {
|
||||
worktree::File::from_dyn(buffer.read(cx).file())
|
||||
.and_then(|file| file.worktree.read(cx).absolutize(&file.path).ok())
|
||||
.map(Arc::<Path>::from)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use crate::project_settings::ProjectSettings;
|
||||
|
||||
use super::breakpoint_store::{BreakpointStore, BreakpointStoreEvent, BreakpointUpdatedReason};
|
||||
use super::breakpoint_store::{
|
||||
BreakpointStore, BreakpointStoreEvent, BreakpointUpdatedReason, SourceBreakpoint,
|
||||
};
|
||||
use super::dap_command::{
|
||||
self, Attach, ConfigurationDone, ContinueCommand, DapCommand, DisconnectCommand,
|
||||
EvaluateCommand, Initialize, Launch, LoadedSourcesCommand, LocalDapCommand, LocationsCommand,
|
||||
@@ -163,6 +165,7 @@ pub struct LocalMode {
|
||||
config: DebugAdapterConfig,
|
||||
adapter: Arc<dyn DebugAdapter>,
|
||||
breakpoint_store: Entity<BreakpointStore>,
|
||||
tmp_breakpoint: Option<SourceBreakpoint>,
|
||||
}
|
||||
|
||||
fn client_source(abs_path: &Path) -> dap::Source {
|
||||
@@ -383,6 +386,7 @@ impl LocalMode {
|
||||
client,
|
||||
adapter,
|
||||
breakpoint_store,
|
||||
tmp_breakpoint: None,
|
||||
config: config.clone(),
|
||||
};
|
||||
|
||||
@@ -431,6 +435,7 @@ impl LocalMode {
|
||||
.read_with(cx, |store, cx| store.breakpoints_from_path(&abs_path, cx))
|
||||
.into_iter()
|
||||
.filter(|bp| bp.state.is_enabled())
|
||||
.chain(self.tmp_breakpoint.clone())
|
||||
.map(Into::into)
|
||||
.collect();
|
||||
|
||||
@@ -1040,6 +1045,40 @@ impl Session {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run_to_position(
|
||||
&mut self,
|
||||
breakpoint: SourceBreakpoint,
|
||||
active_thread_id: ThreadId,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
match &mut self.mode {
|
||||
Mode::Local(local_mode) => {
|
||||
if !matches!(
|
||||
self.thread_states.thread_state(active_thread_id),
|
||||
Some(ThreadStatus::Stopped)
|
||||
) {
|
||||
return;
|
||||
};
|
||||
let path = breakpoint.path.clone();
|
||||
local_mode.tmp_breakpoint = Some(breakpoint);
|
||||
let task = local_mode.send_breakpoints_from_path(
|
||||
path,
|
||||
BreakpointUpdatedReason::Toggled,
|
||||
cx,
|
||||
);
|
||||
|
||||
cx.spawn(async move |this, cx| {
|
||||
task.await;
|
||||
this.update(cx, |this, cx| {
|
||||
this.continue_thread(active_thread_id, cx);
|
||||
})
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
Mode::Remote(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn output(
|
||||
&self,
|
||||
since: OutputToken,
|
||||
@@ -1086,6 +1125,16 @@ impl Session {
|
||||
}
|
||||
|
||||
fn handle_stopped_event(&mut self, event: StoppedEvent, cx: &mut Context<Self>) {
|
||||
if let Some((local, path)) = self.as_local_mut().and_then(|local| {
|
||||
let breakpoint = local.tmp_breakpoint.take()?;
|
||||
let path = breakpoint.path.clone();
|
||||
Some((local, path))
|
||||
}) {
|
||||
local
|
||||
.send_breakpoints_from_path(path, BreakpointUpdatedReason::Toggled, cx)
|
||||
.detach();
|
||||
};
|
||||
|
||||
if event.all_threads_stopped.unwrap_or_default() || event.thread_id.is_none() {
|
||||
self.thread_states.stop_all_threads();
|
||||
|
||||
|
||||
@@ -261,6 +261,12 @@ impl PromptBuilder {
|
||||
.render("assistant_system_prompt", context)
|
||||
}
|
||||
|
||||
pub fn generate_assistant_system_prompt_reminder(&self) -> Result<String, RenderError> {
|
||||
self.handlebars
|
||||
.lock()
|
||||
.render("assistant_system_prompt_reminder", &())
|
||||
}
|
||||
|
||||
pub fn generate_inline_transformation_prompt(
|
||||
&self,
|
||||
user_prompt: String,
|
||||
|
||||
@@ -264,6 +264,7 @@ const FILE_SUFFIXES_BY_ICON_KEY: &[(&str, &[&str])] = &[
|
||||
("vs_sln", &["sln"]),
|
||||
("vs_suo", &["suo"]),
|
||||
("vue", &["vue"]),
|
||||
("vyper", &["vy", "vyi"]),
|
||||
("wgsl", &["wgsl"]),
|
||||
("zig", &["zig"]),
|
||||
];
|
||||
@@ -349,6 +350,7 @@ const FILE_ICONS: &[(&str, &str)] = &[
|
||||
("vs_sln", "icons/file_icons/file.svg"),
|
||||
("vs_suo", "icons/file_icons/file.svg"),
|
||||
("vue", "icons/file_icons/vue.svg"),
|
||||
("vyper", "icons/file_icons/vyper.svg"),
|
||||
("wgsl", "icons/file_icons/wgsl.svg"),
|
||||
("zig", "icons/file_icons/zig.svg"),
|
||||
];
|
||||
|
||||
@@ -3768,69 +3768,21 @@ impl BackgroundScanner {
|
||||
// the git repository in an ancestor directory. Find any gitignore files
|
||||
// in ancestor directories.
|
||||
let root_abs_path = self.state.lock().snapshot.abs_path.clone();
|
||||
let mut containing_git_repository = None;
|
||||
for (index, ancestor) in root_abs_path.as_path().ancestors().enumerate() {
|
||||
if index != 0 {
|
||||
if Some(ancestor) == self.fs.home_dir().as_deref() {
|
||||
// Unless $HOME is itself the worktree root, don't consider it as a
|
||||
// containing git repository---expensive and likely unwanted.
|
||||
break;
|
||||
} else if let Ok(ignore) =
|
||||
build_gitignore(&ancestor.join(*GITIGNORE), self.fs.as_ref()).await
|
||||
{
|
||||
self.state
|
||||
.lock()
|
||||
.snapshot
|
||||
.ignores_by_parent_abs_path
|
||||
.insert(ancestor.into(), (ignore.into(), false));
|
||||
}
|
||||
}
|
||||
|
||||
let ancestor_dot_git = ancestor.join(*DOT_GIT);
|
||||
log::trace!("considering ancestor: {ancestor_dot_git:?}");
|
||||
// Check whether the directory or file called `.git` exists (in the
|
||||
// case of worktrees it's a file.)
|
||||
if self
|
||||
.fs
|
||||
.metadata(&ancestor_dot_git)
|
||||
.await
|
||||
.is_ok_and(|metadata| metadata.is_some())
|
||||
{
|
||||
if index != 0 {
|
||||
// We canonicalize, since the FS events use the canonicalized path.
|
||||
if let Some(ancestor_dot_git) =
|
||||
self.fs.canonicalize(&ancestor_dot_git).await.log_err()
|
||||
{
|
||||
let location_in_repo = root_abs_path
|
||||
.as_path()
|
||||
.strip_prefix(ancestor)
|
||||
.unwrap()
|
||||
.into();
|
||||
log::info!(
|
||||
"inserting parent git repo for this worktree: {location_in_repo:?}"
|
||||
);
|
||||
// We associate the external git repo with our root folder and
|
||||
// also mark where in the git repo the root folder is located.
|
||||
let local_repository = self.state.lock().insert_git_repository_for_path(
|
||||
WorkDirectory::AboveProject {
|
||||
absolute_path: ancestor.into(),
|
||||
location_in_repo,
|
||||
},
|
||||
ancestor_dot_git.clone().into(),
|
||||
self.fs.as_ref(),
|
||||
self.watcher.as_ref(),
|
||||
);
|
||||
|
||||
if local_repository.is_some() {
|
||||
containing_git_repository = Some(ancestor_dot_git)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Reached root of git repository.
|
||||
break;
|
||||
}
|
||||
}
|
||||
let (ignores, repo) = discover_ancestor_git_repo(self.fs.clone(), &root_abs_path).await;
|
||||
self.state
|
||||
.lock()
|
||||
.snapshot
|
||||
.ignores_by_parent_abs_path
|
||||
.extend(ignores);
|
||||
let containing_git_repository = repo.and_then(|(ancestor_dot_git, work_directory)| {
|
||||
self.state.lock().insert_git_repository_for_path(
|
||||
work_directory,
|
||||
ancestor_dot_git.as_path().into(),
|
||||
self.fs.as_ref(),
|
||||
self.watcher.as_ref(),
|
||||
)?;
|
||||
Some(ancestor_dot_git)
|
||||
});
|
||||
|
||||
log::info!("containing git repository: {containing_git_repository:?}");
|
||||
|
||||
@@ -4482,6 +4434,15 @@ impl BackgroundScanner {
|
||||
)
|
||||
.await;
|
||||
|
||||
let mut new_ancestor_repo = if relative_paths
|
||||
.iter()
|
||||
.any(|path| path.as_ref() == Path::new(""))
|
||||
{
|
||||
Some(discover_ancestor_git_repo(self.fs.clone(), &root_abs_path).await)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let mut state = self.state.lock();
|
||||
let doing_recursive_update = scan_queue_tx.is_some();
|
||||
|
||||
@@ -4533,6 +4494,21 @@ impl BackgroundScanner {
|
||||
}
|
||||
|
||||
state.insert_entry(fs_entry.clone(), self.fs.as_ref(), self.watcher.as_ref());
|
||||
|
||||
if path.as_ref() == Path::new("") {
|
||||
if let Some((ignores, repo)) = new_ancestor_repo.take() {
|
||||
log::trace!("updating ancestor git repository");
|
||||
state.snapshot.ignores_by_parent_abs_path.extend(ignores);
|
||||
if let Some((ancestor_dot_git, work_directory)) = repo {
|
||||
state.insert_git_repository_for_path(
|
||||
work_directory,
|
||||
ancestor_dot_git.as_path().into(),
|
||||
self.fs.as_ref(),
|
||||
self.watcher.as_ref(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(None) => {
|
||||
self.remove_repo_path(path, &mut state.snapshot);
|
||||
@@ -4811,6 +4787,68 @@ impl BackgroundScanner {
|
||||
}
|
||||
}
|
||||
|
||||
async fn discover_ancestor_git_repo(
|
||||
fs: Arc<dyn Fs>,
|
||||
root_abs_path: &SanitizedPath,
|
||||
) -> (
|
||||
HashMap<Arc<Path>, (Arc<Gitignore>, bool)>,
|
||||
Option<(PathBuf, WorkDirectory)>,
|
||||
) {
|
||||
let mut ignores = HashMap::default();
|
||||
for (index, ancestor) in root_abs_path.as_path().ancestors().enumerate() {
|
||||
if index != 0 {
|
||||
if Some(ancestor) == fs.home_dir().as_deref() {
|
||||
// Unless $HOME is itself the worktree root, don't consider it as a
|
||||
// containing git repository---expensive and likely unwanted.
|
||||
break;
|
||||
} else if let Ok(ignore) =
|
||||
build_gitignore(&ancestor.join(*GITIGNORE), fs.as_ref()).await
|
||||
{
|
||||
ignores.insert(ancestor.into(), (ignore.into(), false));
|
||||
}
|
||||
}
|
||||
|
||||
let ancestor_dot_git = ancestor.join(*DOT_GIT);
|
||||
log::trace!("considering ancestor: {ancestor_dot_git:?}");
|
||||
// Check whether the directory or file called `.git` exists (in the
|
||||
// case of worktrees it's a file.)
|
||||
if fs
|
||||
.metadata(&ancestor_dot_git)
|
||||
.await
|
||||
.is_ok_and(|metadata| metadata.is_some())
|
||||
{
|
||||
if index != 0 {
|
||||
// We canonicalize, since the FS events use the canonicalized path.
|
||||
if let Some(ancestor_dot_git) = fs.canonicalize(&ancestor_dot_git).await.log_err() {
|
||||
let location_in_repo = root_abs_path
|
||||
.as_path()
|
||||
.strip_prefix(ancestor)
|
||||
.unwrap()
|
||||
.into();
|
||||
log::info!("inserting parent git repo for this worktree: {location_in_repo:?}");
|
||||
// We associate the external git repo with our root folder and
|
||||
// also mark where in the git repo the root folder is located.
|
||||
return (
|
||||
ignores,
|
||||
Some((
|
||||
ancestor_dot_git,
|
||||
WorkDirectory::AboveProject {
|
||||
absolute_path: ancestor.into(),
|
||||
location_in_repo,
|
||||
},
|
||||
)),
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
// Reached root of git repository.
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
(ignores, None)
|
||||
}
|
||||
|
||||
fn build_diff(
|
||||
phase: BackgroundScannerPhase,
|
||||
old_snapshot: &Snapshot,
|
||||
|
||||
@@ -5,7 +5,7 @@ use crate::{
|
||||
use anyhow::Result;
|
||||
use fs::{FakeFs, Fs, RealFs, RemoveOptions};
|
||||
use git::GITIGNORE;
|
||||
use gpui::{AppContext as _, BorrowAppContext, Context, Task, TestAppContext};
|
||||
use gpui::{AppContext as _, BackgroundExecutor, BorrowAppContext, Context, Task, TestAppContext};
|
||||
use parking_lot::Mutex;
|
||||
use postage::stream::Stream;
|
||||
use pretty_assertions::assert_eq;
|
||||
@@ -1984,6 +1984,68 @@ fn test_unrelativize() {
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_repository_above_root(executor: BackgroundExecutor, cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(executor);
|
||||
fs.insert_tree(
|
||||
path!("/root"),
|
||||
json!({
|
||||
".git": {},
|
||||
"subproject": {
|
||||
"a.txt": "A"
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
let worktree = Worktree::local(
|
||||
path!("/root/subproject").as_ref(),
|
||||
true,
|
||||
fs.clone(),
|
||||
Arc::default(),
|
||||
&mut cx.to_async(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
worktree
|
||||
.update(cx, |worktree, _| {
|
||||
worktree.as_local().unwrap().scan_complete()
|
||||
})
|
||||
.await;
|
||||
cx.run_until_parked();
|
||||
let repos = worktree.update(cx, |worktree, _| {
|
||||
worktree
|
||||
.as_local()
|
||||
.unwrap()
|
||||
.git_repositories
|
||||
.values()
|
||||
.map(|entry| entry.work_directory_abs_path.clone())
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
pretty_assertions::assert_eq!(repos, [Path::new(path!("/root")).into()]);
|
||||
|
||||
eprintln!(">>>>>>>>>> touch");
|
||||
fs.touch_path(path!("/root/subproject")).await;
|
||||
worktree
|
||||
.update(cx, |worktree, _| {
|
||||
worktree.as_local().unwrap().scan_complete()
|
||||
})
|
||||
.await;
|
||||
cx.run_until_parked();
|
||||
|
||||
let repos = worktree.update(cx, |worktree, _| {
|
||||
worktree
|
||||
.as_local()
|
||||
.unwrap()
|
||||
.git_repositories
|
||||
.values()
|
||||
.map(|entry| entry.work_directory_abs_path.clone())
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
pretty_assertions::assert_eq!(repos, [Path::new(path!("/root")).into()]);
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn check_worktree_entries(
|
||||
tree: &Worktree,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
description = "The fast, collaborative code editor."
|
||||
edition.workspace = true
|
||||
name = "zed"
|
||||
version = "0.182.0"
|
||||
version = "0.183.0"
|
||||
publish.workspace = true
|
||||
license = "GPL-3.0-or-later"
|
||||
authors = ["Zed Team <hi@zed.dev>"]
|
||||
|
||||
@@ -498,10 +498,12 @@ impl RateCompletionModal {
|
||||
cx
|
||||
))
|
||||
.on_click(cx.listener(move |this, _, window, cx| {
|
||||
this.thumbs_down_active(
|
||||
&ThumbsDownActiveCompletion,
|
||||
window, cx,
|
||||
);
|
||||
if this.active_completion.is_some() {
|
||||
this.thumbs_down_active(
|
||||
&ThumbsDownActiveCompletion,
|
||||
window, cx,
|
||||
);
|
||||
}
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
@@ -517,7 +519,9 @@ impl RateCompletionModal {
|
||||
cx
|
||||
))
|
||||
.on_click(cx.listener(move |this, _, window, cx| {
|
||||
this.thumbs_up_active(&ThumbsUpActiveCompletion, window, cx);
|
||||
if this.active_completion.is_some() {
|
||||
this.thumbs_up_active(&ThumbsUpActiveCompletion, window, cx);
|
||||
}
|
||||
})),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -320,7 +320,7 @@ This example makes comments italic and changes the color of strings:
|
||||
|
||||
Change your theme:
|
||||
|
||||
1. Use the theme selector (<kbd>cmd-k cmd-t|ctrl-k ctrl-t</kbd>)
|
||||
1. Use the theme selector ({#kb theme_selector::Toggle})
|
||||
2. Or set it in your `settings.json`:
|
||||
|
||||
```json
|
||||
@@ -335,7 +335,7 @@ Create custom themes by creating a JSON file in `~/.config/zed/themes/`. Zed wil
|
||||
|
||||
### Using Theme Extensions
|
||||
|
||||
Zed supports theme extensions. Browse and install theme extensions from the Extensions panel (<kbd>cmd-shift-e|ctrl-shift-e</kbd>).
|
||||
Zed supports theme extensions. Browse and install theme extensions from the Extensions panel ({#kb zed::Extensions}).
|
||||
|
||||
To create your own theme extension, refer to the [Developing Theme Extensions](./extensions/themes.md) guide.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user