Compare commits
41 Commits
v0.178.4-p
...
fix-compon
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
02d218ca6a | ||
|
|
8e1af73ae3 | ||
|
|
c70becb594 | ||
|
|
8cf5af1a84 | ||
|
|
247ee880d2 | ||
|
|
2e217759c0 | ||
|
|
0a0c163692 | ||
|
|
e80df25386 | ||
|
|
d9590f3f0e | ||
|
|
4ecd1b5174 | ||
|
|
70c973f6c3 | ||
|
|
e842b4eade | ||
|
|
606aa7a78c | ||
|
|
0081b816fe | ||
|
|
21949bcf1a | ||
|
|
ee7ed6d5b8 | ||
|
|
07b67c1bd3 | ||
|
|
f116b44ae8 | ||
|
|
43ab7fe0e2 | ||
|
|
6044773043 | ||
|
|
81af2c0bed | ||
|
|
ab199fda47 | ||
|
|
e60e8f3a0a | ||
|
|
edeed7b619 | ||
|
|
9be7934f12 | ||
|
|
009b90291e | ||
|
|
8b17dc66f6 | ||
|
|
de07b712fd | ||
|
|
be8f3b3791 | ||
|
|
3131b0459f | ||
|
|
3ec323ce0d | ||
|
|
c8b782d870 | ||
|
|
7bca15704b | ||
|
|
5268e74315 | ||
|
|
91c209900b | ||
|
|
74c29f1818 | ||
|
|
5858e61327 | ||
|
|
21cf2e38c5 | ||
|
|
a3ca5554fd | ||
|
|
acf9b22466 | ||
|
|
ffcd023f83 |
39
.github/workflows/ci.yml
vendored
39
.github/workflows/ci.yml
vendored
@@ -366,8 +366,7 @@ jobs:
|
||||
if: |
|
||||
github.repository_owner == 'zed-industries' &&
|
||||
needs.job_spec.outputs.run_tests == 'true'
|
||||
# Use bigger runners for PRs (speed); smaller for async (cost)
|
||||
runs-on: ${{ github.event_name == 'pull_request' && 'windows-2025-32' || 'windows-2025-16' }}
|
||||
runs-on: windows-2025-64
|
||||
steps:
|
||||
# more info here:- https://github.com/rust-lang/cargo/issues/13020
|
||||
- name: Enable longer pathnames for git
|
||||
@@ -432,28 +431,22 @@ jobs:
|
||||
- macos_tests
|
||||
- windows_clippy
|
||||
- windows_tests
|
||||
if: always()
|
||||
if: |
|
||||
always() && (
|
||||
needs.style.result == 'success'
|
||||
&& (
|
||||
needs.job_spec.outputs.run_tests == 'false'
|
||||
|| (needs.macos_tests.result == 'success'
|
||||
&& needs.linux_tests.result == 'success'
|
||||
&& needs.windows_tests.result == 'success'
|
||||
&& needs.windows_clippy.result == 'success'
|
||||
&& needs.build_remote_server.result == 'success'
|
||||
&& needs.migration_checks.result == 'success')
|
||||
)
|
||||
)
|
||||
steps:
|
||||
- name: Check all tests passed
|
||||
run: |
|
||||
# Check dependent jobs...
|
||||
RET_CODE=0
|
||||
# Always check style
|
||||
[[ "${{ needs.style.result }}" != 'success' ]] && { RET_CODE=1; echo "style tests failed"; }
|
||||
|
||||
# Only check test jobs if they were supposed to run
|
||||
if [[ "${{ needs.job_spec.outputs.run_tests }}" == "true" ]]; then
|
||||
[[ "${{ 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.migration_checks.result }}" != 'success' ]] && { RET_CODE=1; echo "Migration checks failed"; }
|
||||
[[ "${{ needs.build_remote_server.result }}" != 'success' ]] && { RET_CODE=1; echo "Remote server build failed"; }
|
||||
fi
|
||||
if [[ "$RET_CODE" -eq 0 ]]; then
|
||||
echo "All tests passed successfully!"
|
||||
fi
|
||||
exit $RET_CODE
|
||||
- name: All tests passed
|
||||
run: echo "All tests passed successfully!"
|
||||
|
||||
bundle-mac:
|
||||
timeout-minutes: 120
|
||||
|
||||
32
.github/workflows/community_release_actions.yml
vendored
32
.github/workflows/community_release_actions.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
else
|
||||
URL="https://zed.dev/releases/stable/latest"
|
||||
fi
|
||||
echo "URL=$URL" >> $GITHUB_OUTPUT
|
||||
echo "::set-output name=URL::$URL"
|
||||
- name: Get content
|
||||
uses: 2428392/gh-truncate-string-action@b3ff790d21cf42af3ca7579146eedb93c8fb0757 # v1.4.1
|
||||
id: get-content
|
||||
@@ -33,33 +33,3 @@ jobs:
|
||||
with:
|
||||
webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }}
|
||||
content: ${{ steps.get-content.outputs.string }}
|
||||
|
||||
send_release_notes_email:
|
||||
if: github.repository_owner == 'zed-industries' && !github.event.release.prerelease
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Check if release was promoted from preview
|
||||
id: check-promotion-from-preview
|
||||
run: |
|
||||
VERSION="${{ github.event.release.tag_name }}"
|
||||
PREVIEW_TAG="${VERSION}-pre"
|
||||
if git rev-parse "$PREVIEW_TAG" >/dev/null 2>&1; then
|
||||
echo "was_preview=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "was_preview=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Send release notes email
|
||||
if: steps.check-promotion-from-preview.outputs.was_preview == 'true'
|
||||
run: |
|
||||
curl -X POST "https://zed.dev/api/send_release_notes_email" \
|
||||
-H "Authorization: Bearer ${{ secrets.RELEASE_NOTES_API_TOKEN }}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"version": "${{ github.event.release.tag_name }}",
|
||||
"markdown_body": ${{ toJSON(github.event.release.body) }}
|
||||
}'
|
||||
|
||||
34
Cargo.lock
generated
34
Cargo.lock
generated
@@ -257,9 +257,9 @@ checksum = "34cd60c5e3152cef0a592f1b296f1cc93715d89d2551d85315828c3a09575ff4"
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.96"
|
||||
version = "1.0.97"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6b964d184e89d9b6b67dd2715bc8e74cf3107fb2b529990c90cf517326150bf4"
|
||||
checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f"
|
||||
|
||||
[[package]]
|
||||
name = "approx"
|
||||
@@ -673,16 +673,22 @@ dependencies = [
|
||||
"assistant_tool",
|
||||
"chrono",
|
||||
"collections",
|
||||
"feature_flags",
|
||||
"futures 0.3.31",
|
||||
"gpui",
|
||||
"language",
|
||||
"language_model",
|
||||
"project",
|
||||
"rand 0.8.5",
|
||||
"release_channel",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"settings",
|
||||
"theme",
|
||||
"ui",
|
||||
"util",
|
||||
"workspace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4639,6 +4645,7 @@ dependencies = [
|
||||
"collections",
|
||||
"db",
|
||||
"editor",
|
||||
"extension",
|
||||
"extension_host",
|
||||
"feature_flags",
|
||||
"fs",
|
||||
@@ -17008,7 +17015,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed"
|
||||
version = "0.178.4"
|
||||
version = "0.179.0"
|
||||
dependencies = [
|
||||
"activity_indicator",
|
||||
"anyhow",
|
||||
@@ -17203,13 +17210,6 @@ dependencies = [
|
||||
"zed_extension_api 0.1.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zed_purescript"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"zed_extension_api 0.1.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zed_ruff"
|
||||
version = "0.1.0"
|
||||
@@ -17239,20 +17239,6 @@ dependencies = [
|
||||
"zed_extension_api 0.1.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zed_uiua"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"zed_extension_api 0.1.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zed_zig"
|
||||
version = "0.3.3"
|
||||
dependencies = [
|
||||
"zed_extension_api 0.1.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zeno"
|
||||
version = "0.2.3"
|
||||
|
||||
@@ -174,14 +174,11 @@ members = [
|
||||
"extensions/html",
|
||||
"extensions/perplexity",
|
||||
"extensions/proto",
|
||||
"extensions/purescript",
|
||||
"extensions/ruff",
|
||||
"extensions/slash-commands-example",
|
||||
"extensions/snippets",
|
||||
"extensions/test-extension",
|
||||
"extensions/toml",
|
||||
"extensions/uiua",
|
||||
"extensions/zig",
|
||||
|
||||
#
|
||||
# Tooling
|
||||
|
||||
@@ -11,8 +11,8 @@ You should only perform actions that modify the user’s system if explicitly re
|
||||
|
||||
Be concise and direct in your responses.
|
||||
|
||||
The user has opened a project that contains the following top-level directories/files:
|
||||
The user has opened a project that contains the following root directories/files:
|
||||
|
||||
{{#each worktree_root_names}}
|
||||
- {{this}}
|
||||
{{#each worktrees}}
|
||||
- {{root_name}} (absolute path: {{abs_path}})
|
||||
{{/each}}
|
||||
|
||||
@@ -336,14 +336,14 @@
|
||||
"active_line_width": 1,
|
||||
// Determines how indent guides are colored.
|
||||
// This setting can take the following three values:
|
||||
///
|
||||
//
|
||||
// 1. "disabled"
|
||||
// 2. "fixed"
|
||||
// 3. "indent_aware"
|
||||
"coloring": "fixed",
|
||||
// Determines how indent guide backgrounds are colored.
|
||||
// This setting can take the following two values:
|
||||
///
|
||||
//
|
||||
// 1. "disabled"
|
||||
// 2. "indent_aware"
|
||||
"background_coloring": "disabled"
|
||||
@@ -402,8 +402,8 @@
|
||||
// Time to wait after scrolling the buffer, before requesting the hints,
|
||||
// set to 0 to disable debouncing.
|
||||
"scroll_debounce_ms": 50,
|
||||
/// A set of modifiers which, when pressed, will toggle the visibility of inlay hints.
|
||||
/// If the set if empty or not all the modifiers specified are pressed, inlay hints will not be toggled.
|
||||
// A set of modifiers which, when pressed, will toggle the visibility of inlay hints.
|
||||
// If the set if empty or not all the modifiers specified are pressed, inlay hints will not be toggled.
|
||||
"toggle_on_modifiers_press": {
|
||||
"control": false,
|
||||
"shift": false,
|
||||
@@ -440,7 +440,7 @@
|
||||
"scrollbar": {
|
||||
// When to show the scrollbar in the project panel.
|
||||
// This setting can take five values:
|
||||
///
|
||||
//
|
||||
// 1. null (default): Inherit editor settings
|
||||
// 2. Show the scrollbar if there's important information or
|
||||
// follow the system's configured behavior (default):
|
||||
@@ -455,7 +455,7 @@
|
||||
},
|
||||
// Which files containing diagnostic errors/warnings to mark in the project panel.
|
||||
// This setting can take the following three values:
|
||||
///
|
||||
//
|
||||
// 1. Do not mark any files:
|
||||
// "off"
|
||||
// 2. Only mark files with errors:
|
||||
@@ -512,7 +512,7 @@
|
||||
"scrollbar": {
|
||||
// When to show the scrollbar in the project panel.
|
||||
// This setting can take five values:
|
||||
///
|
||||
//
|
||||
// 1. null (default): Inherit editor settings
|
||||
// 2. Show the scrollbar if there's important information or
|
||||
// follow the system's configured behavior (default):
|
||||
@@ -547,7 +547,7 @@
|
||||
"git_panel": {
|
||||
// Whether to show the git panel button in the status bar.
|
||||
"button": true,
|
||||
// Where to the git panel. Can be 'left' or 'right'.
|
||||
// Where to show the git panel. Can be 'left' or 'right'.
|
||||
"dock": "left",
|
||||
// Default width of the git panel.
|
||||
"default_width": 360,
|
||||
@@ -686,7 +686,7 @@
|
||||
// Which files containing diagnostic errors/warnings to mark in the tabs.
|
||||
// Diagnostics are only shown when file icons are also active.
|
||||
// This setting only works when can take the following three values:
|
||||
///
|
||||
//
|
||||
// 1. Do not mark any files:
|
||||
// "off"
|
||||
// 2. Only mark files with errors:
|
||||
@@ -850,15 +850,7 @@
|
||||
//
|
||||
// The minimum column number to show the inline blame information at
|
||||
// "min_column": 0
|
||||
},
|
||||
// How git hunks are displayed visually in the editor.
|
||||
// This setting can take two values:
|
||||
//
|
||||
// 1. Show unstaged hunks filled and staged hunks hollow:
|
||||
// "hunk_style": "staged_hollow"
|
||||
// 2. Show unstaged hunks hollow and staged hunks filled:
|
||||
// "hunk_style": "unstaged_hollow"
|
||||
"hunk_style": "staged_hollow"
|
||||
}
|
||||
},
|
||||
// Configuration for how direnv configuration should be loaded. May take 2 values:
|
||||
// 1. Load direnv configuration using `direnv export json` directly.
|
||||
@@ -1022,7 +1014,7 @@
|
||||
"scrollbar": {
|
||||
// When to show the scrollbar in the terminal.
|
||||
// This setting can take five values:
|
||||
///
|
||||
//
|
||||
// 1. null (default): Inherit editor settings
|
||||
// 2. Show the scrollbar if there's important information or
|
||||
// follow the system's configured behavior (default):
|
||||
@@ -1093,6 +1085,31 @@
|
||||
"auto_install_extensions": {
|
||||
"html": true
|
||||
},
|
||||
// Controls how completions are processed for this language.
|
||||
"completions": {
|
||||
// Controls how words are completed.
|
||||
// For large documents, not all words may be fetched for completion.
|
||||
//
|
||||
// May take 3 values:
|
||||
// 1. "enabled"
|
||||
// Always fetch document's words for completions.
|
||||
// 2. "fallback"
|
||||
// Only if LSP response errors/times out/is empty, use document's words to show completions.
|
||||
// 3. "disabled"
|
||||
// Never fetch or complete document's words for completions.
|
||||
//
|
||||
// Default: fallback
|
||||
"words": "fallback",
|
||||
// Whether to fetch LSP completions or not.
|
||||
//
|
||||
// Default: true
|
||||
"lsp": true,
|
||||
// When fetching LSP completions, determines how long to wait for a response of a particular server.
|
||||
// When set to 0, waits indefinitely.
|
||||
//
|
||||
// Default: 500
|
||||
"lsp_fetch_timeout_ms": 500
|
||||
},
|
||||
// Different settings for specific languages.
|
||||
"languages": {
|
||||
"Astro": {
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use collections::HashMap;
|
||||
use editor::{Editor, MultiBuffer};
|
||||
use gpui::{
|
||||
list, AbsoluteLength, AnyElement, App, ClickEvent, DefiniteLength, EdgesRefinement, Empty,
|
||||
Entity, Focusable, Length, ListAlignment, ListOffset, ListState, StyleRefinement, Subscription,
|
||||
Task, TextStyleRefinement, UnderlineStyle,
|
||||
list, percentage, AbsoluteLength, Animation, AnimationExt, AnyElement, App, ClickEvent,
|
||||
DefiniteLength, EdgesRefinement, Empty, Entity, Focusable, Length, ListAlignment, ListOffset,
|
||||
ListState, StyleRefinement, Subscription, Task, TextStyleRefinement, Transformation,
|
||||
UnderlineStyle,
|
||||
};
|
||||
use language::{Buffer, LanguageRegistry};
|
||||
use language_model::{LanguageModelRegistry, LanguageModelToolUseId, Role};
|
||||
@@ -701,13 +703,13 @@ impl ActiveThread {
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_between()
|
||||
.py_0p5()
|
||||
.py_1()
|
||||
.pl_1()
|
||||
.pr_2()
|
||||
.bg(cx.theme().colors().editor_foreground.opacity(0.02))
|
||||
.bg(cx.theme().colors().editor_foreground.opacity(0.025))
|
||||
.map(|element| {
|
||||
if is_open {
|
||||
element.border_b_1().rounded_t(px(6.))
|
||||
element.border_b_1().rounded_t_md()
|
||||
} else {
|
||||
element.rounded_md()
|
||||
}
|
||||
@@ -731,16 +733,35 @@ impl ActiveThread {
|
||||
))
|
||||
.child(Label::new(tool_use.name)),
|
||||
)
|
||||
.child(
|
||||
Label::new(match tool_use.status {
|
||||
ToolUseStatus::Pending => "Pending",
|
||||
ToolUseStatus::Running => "Running",
|
||||
ToolUseStatus::Finished(_) => "Finished",
|
||||
ToolUseStatus::Error(_) => "Error",
|
||||
})
|
||||
.size(LabelSize::XSmall)
|
||||
.buffer_font(cx),
|
||||
),
|
||||
.child({
|
||||
let (icon_name, color, animated) = match &tool_use.status {
|
||||
ToolUseStatus::Pending => {
|
||||
(IconName::Warning, Color::Warning, false)
|
||||
}
|
||||
ToolUseStatus::Running => {
|
||||
(IconName::ArrowCircle, Color::Accent, true)
|
||||
}
|
||||
ToolUseStatus::Finished(_) => {
|
||||
(IconName::Check, Color::Success, false)
|
||||
}
|
||||
ToolUseStatus::Error(_) => (IconName::Close, Color::Error, false),
|
||||
};
|
||||
|
||||
let icon = Icon::new(icon_name).color(color).size(IconSize::Small);
|
||||
|
||||
if animated {
|
||||
icon.with_animation(
|
||||
"arrow-circle",
|
||||
Animation::new(Duration::from_secs(2)).repeat(),
|
||||
|icon, delta| {
|
||||
icon.transform(Transformation::rotate(percentage(delta)))
|
||||
},
|
||||
)
|
||||
.into_any_element()
|
||||
} else {
|
||||
icon.into_any_element()
|
||||
}
|
||||
}),
|
||||
)
|
||||
.map(|parent| {
|
||||
if !is_open {
|
||||
@@ -812,7 +833,7 @@ impl ActiveThread {
|
||||
.bg(cx.theme().colors().editor_foreground.opacity(0.02))
|
||||
.map(|element| {
|
||||
if is_open {
|
||||
element.border_b_1().rounded_t(px(6.))
|
||||
element.border_b_1().rounded_t_md()
|
||||
} else {
|
||||
element.rounded_md()
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ use language_model::{
|
||||
Role, StopReason,
|
||||
};
|
||||
use project::Project;
|
||||
use prompt_store::PromptBuilder;
|
||||
use prompt_store::{AssistantSystemPromptWorktree, PromptBuilder};
|
||||
use scripting_tool::{ScriptingSession, ScriptingTool};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use util::{post_inc, ResultExt, TryFutureExt as _};
|
||||
@@ -384,8 +384,14 @@ impl Thread {
|
||||
let worktree_root_names = self
|
||||
.project
|
||||
.read(cx)
|
||||
.worktree_root_names(cx)
|
||||
.map(ToString::to_string)
|
||||
.visible_worktrees(cx)
|
||||
.map(|worktree| {
|
||||
let worktree = worktree.read(cx);
|
||||
AssistantSystemPromptWorktree {
|
||||
root_name: worktree.root_name().into(),
|
||||
abs_path: worktree.abs_path(),
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let system_prompt = self
|
||||
.prompt_builder
|
||||
|
||||
@@ -20,13 +20,14 @@ impl ToolSelector {
|
||||
cx: &mut Context<Self>,
|
||||
) -> Entity<ContextMenu> {
|
||||
ContextMenu::build(window, cx, |mut menu, _window, cx| {
|
||||
let icon_position = IconPosition::End;
|
||||
let tools_by_source = self.tools.tools_by_source(cx);
|
||||
|
||||
let all_tools_enabled = self.tools.are_all_tools_enabled();
|
||||
menu = menu.header("Tools").toggleable_entry(
|
||||
"All Tools",
|
||||
all_tools_enabled,
|
||||
IconPosition::End,
|
||||
icon_position,
|
||||
None,
|
||||
{
|
||||
let tools = self.tools.clone();
|
||||
@@ -61,31 +62,51 @@ impl ToolSelector {
|
||||
tools.sort_by(|(_, name_a, _), (_, name_b, _)| name_a.cmp(name_b));
|
||||
}
|
||||
|
||||
menu = match source {
|
||||
menu = match &source {
|
||||
ToolSource::Native => menu.header("Zed"),
|
||||
ToolSource::ContextServer { id } => menu.separator().header(id),
|
||||
ToolSource::ContextServer { id } => {
|
||||
let all_tools_from_source_enabled =
|
||||
self.tools.are_all_tools_from_source_enabled(&source);
|
||||
|
||||
menu.separator().header(id).toggleable_entry(
|
||||
"All Tools",
|
||||
all_tools_from_source_enabled,
|
||||
icon_position,
|
||||
None,
|
||||
{
|
||||
let tools = self.tools.clone();
|
||||
let source = source.clone();
|
||||
move |_window, cx| {
|
||||
if all_tools_from_source_enabled {
|
||||
tools.disable_source(source.clone(), cx);
|
||||
} else {
|
||||
tools.enable_source(&source);
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
for (source, name, is_enabled) in tools {
|
||||
menu =
|
||||
menu.toggleable_entry(name.clone(), is_enabled, IconPosition::End, None, {
|
||||
let tools = self.tools.clone();
|
||||
move |_window, _cx| {
|
||||
if name.as_ref() == ScriptingTool::NAME {
|
||||
if is_enabled {
|
||||
tools.disable_scripting_tool();
|
||||
} else {
|
||||
tools.enable_scripting_tool();
|
||||
}
|
||||
menu = menu.toggleable_entry(name.clone(), is_enabled, icon_position, None, {
|
||||
let tools = self.tools.clone();
|
||||
move |_window, _cx| {
|
||||
if name.as_ref() == ScriptingTool::NAME {
|
||||
if is_enabled {
|
||||
tools.disable_scripting_tool();
|
||||
} else {
|
||||
if is_enabled {
|
||||
tools.disable(source.clone(), &[name.clone()]);
|
||||
} else {
|
||||
tools.enable(source.clone(), &[name.clone()]);
|
||||
}
|
||||
tools.enable_scripting_tool();
|
||||
}
|
||||
} else {
|
||||
if is_enabled {
|
||||
tools.disable(source.clone(), &[name.clone()]);
|
||||
} else {
|
||||
tools.enable(source.clone(), &[name.clone()]);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -46,56 +46,119 @@ impl ToolWorkingSet {
|
||||
}
|
||||
|
||||
pub fn tools(&self, cx: &App) -> Vec<Arc<dyn Tool>> {
|
||||
let mut tools = ToolRegistry::global(cx).tools();
|
||||
tools.extend(
|
||||
self.state
|
||||
.lock()
|
||||
.context_server_tools_by_id
|
||||
.values()
|
||||
.cloned(),
|
||||
);
|
||||
self.state.lock().tools(cx)
|
||||
}
|
||||
|
||||
tools
|
||||
pub fn tools_by_source(&self, cx: &App) -> IndexMap<ToolSource, Vec<Arc<dyn Tool>>> {
|
||||
self.state.lock().tools_by_source(cx)
|
||||
}
|
||||
|
||||
pub fn are_all_tools_enabled(&self) -> bool {
|
||||
let state = self.state.lock();
|
||||
|
||||
state.disabled_tools_by_source.is_empty() && !state.is_scripting_tool_disabled
|
||||
}
|
||||
|
||||
pub fn are_all_tools_from_source_enabled(&self, source: &ToolSource) -> bool {
|
||||
let state = self.state.lock();
|
||||
!state.disabled_tools_by_source.contains_key(source)
|
||||
}
|
||||
|
||||
pub fn enabled_tools(&self, cx: &App) -> Vec<Arc<dyn Tool>> {
|
||||
self.state.lock().enabled_tools(cx)
|
||||
}
|
||||
|
||||
pub fn enable_all_tools(&self) {
|
||||
let mut state = self.state.lock();
|
||||
|
||||
state.disabled_tools_by_source.clear();
|
||||
state.is_scripting_tool_disabled = false;
|
||||
state.enable_scripting_tool();
|
||||
}
|
||||
|
||||
pub fn disable_all_tools(&self, cx: &App) {
|
||||
let tools = self.tools_by_source(cx);
|
||||
|
||||
for (source, tools) in tools {
|
||||
let tool_names = tools
|
||||
.into_iter()
|
||||
.map(|tool| tool.name().into())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
self.disable(source, &tool_names);
|
||||
}
|
||||
|
||||
self.disable_scripting_tool();
|
||||
let mut state = self.state.lock();
|
||||
state.disable_all_tools(cx);
|
||||
}
|
||||
|
||||
pub fn enabled_tools(&self, cx: &App) -> Vec<Arc<dyn Tool>> {
|
||||
let all_tools = self.tools(cx);
|
||||
|
||||
all_tools
|
||||
.into_iter()
|
||||
.filter(|tool| self.is_enabled(&tool.source(), &tool.name().into()))
|
||||
.collect()
|
||||
pub fn enable_source(&self, source: &ToolSource) {
|
||||
let mut state = self.state.lock();
|
||||
state.enable_source(source);
|
||||
}
|
||||
|
||||
pub fn tools_by_source(&self, cx: &App) -> IndexMap<ToolSource, Vec<Arc<dyn Tool>>> {
|
||||
pub fn disable_source(&self, source: ToolSource, cx: &App) {
|
||||
let mut state = self.state.lock();
|
||||
state.disable_source(source, cx);
|
||||
}
|
||||
|
||||
pub fn insert(&self, tool: Arc<dyn Tool>) -> ToolId {
|
||||
let mut state = self.state.lock();
|
||||
let tool_id = state.next_tool_id;
|
||||
state.next_tool_id.0 += 1;
|
||||
state
|
||||
.context_server_tools_by_id
|
||||
.insert(tool_id, tool.clone());
|
||||
state.tools_changed();
|
||||
tool_id
|
||||
}
|
||||
|
||||
pub fn is_enabled(&self, source: &ToolSource, name: &Arc<str>) -> bool {
|
||||
self.state.lock().is_enabled(source, name)
|
||||
}
|
||||
|
||||
pub fn is_disabled(&self, source: &ToolSource, name: &Arc<str>) -> bool {
|
||||
self.state.lock().is_disabled(source, name)
|
||||
}
|
||||
|
||||
pub fn enable(&self, source: ToolSource, tools_to_enable: &[Arc<str>]) {
|
||||
let mut state = self.state.lock();
|
||||
state.enable(source, tools_to_enable);
|
||||
}
|
||||
|
||||
pub fn disable(&self, source: ToolSource, tools_to_disable: &[Arc<str>]) {
|
||||
let mut state = self.state.lock();
|
||||
state.disable(source, tools_to_disable);
|
||||
}
|
||||
|
||||
pub fn remove(&self, tool_ids_to_remove: &[ToolId]) {
|
||||
let mut state = self.state.lock();
|
||||
state
|
||||
.context_server_tools_by_id
|
||||
.retain(|id, _| !tool_ids_to_remove.contains(id));
|
||||
state.tools_changed();
|
||||
}
|
||||
|
||||
pub fn is_scripting_tool_enabled(&self) -> bool {
|
||||
let state = self.state.lock();
|
||||
!state.is_scripting_tool_disabled
|
||||
}
|
||||
|
||||
pub fn enable_scripting_tool(&self) {
|
||||
let mut state = self.state.lock();
|
||||
state.enable_scripting_tool();
|
||||
}
|
||||
|
||||
pub fn disable_scripting_tool(&self) {
|
||||
let mut state = self.state.lock();
|
||||
state.disable_scripting_tool();
|
||||
}
|
||||
}
|
||||
|
||||
impl WorkingSetState {
|
||||
fn tools_changed(&mut self) {
|
||||
self.context_server_tools_by_name.clear();
|
||||
self.context_server_tools_by_name.extend(
|
||||
self.context_server_tools_by_id
|
||||
.values()
|
||||
.map(|tool| (tool.name(), tool.clone())),
|
||||
);
|
||||
}
|
||||
|
||||
fn tools(&self, cx: &App) -> Vec<Arc<dyn Tool>> {
|
||||
let mut tools = ToolRegistry::global(cx).tools();
|
||||
tools.extend(self.context_server_tools_by_id.values().cloned());
|
||||
|
||||
tools
|
||||
}
|
||||
|
||||
fn tools_by_source(&self, cx: &App) -> IndexMap<ToolSource, Vec<Arc<dyn Tool>>> {
|
||||
let mut tools_by_source = IndexMap::default();
|
||||
|
||||
for tool in self.tools(cx) {
|
||||
@@ -114,78 +177,78 @@ impl ToolWorkingSet {
|
||||
tools_by_source
|
||||
}
|
||||
|
||||
pub fn insert(&self, tool: Arc<dyn Tool>) -> ToolId {
|
||||
let mut state = self.state.lock();
|
||||
let tool_id = state.next_tool_id;
|
||||
state.next_tool_id.0 += 1;
|
||||
state
|
||||
.context_server_tools_by_id
|
||||
.insert(tool_id, tool.clone());
|
||||
state.tools_changed();
|
||||
tool_id
|
||||
fn enabled_tools(&self, cx: &App) -> Vec<Arc<dyn Tool>> {
|
||||
let all_tools = self.tools(cx);
|
||||
|
||||
all_tools
|
||||
.into_iter()
|
||||
.filter(|tool| self.is_enabled(&tool.source(), &tool.name().into()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn is_enabled(&self, source: &ToolSource, name: &Arc<str>) -> bool {
|
||||
fn is_enabled(&self, source: &ToolSource, name: &Arc<str>) -> bool {
|
||||
!self.is_disabled(source, name)
|
||||
}
|
||||
|
||||
pub fn is_disabled(&self, source: &ToolSource, name: &Arc<str>) -> bool {
|
||||
let state = self.state.lock();
|
||||
state
|
||||
.disabled_tools_by_source
|
||||
fn is_disabled(&self, source: &ToolSource, name: &Arc<str>) -> bool {
|
||||
self.disabled_tools_by_source
|
||||
.get(source)
|
||||
.map_or(false, |disabled_tools| disabled_tools.contains(name))
|
||||
}
|
||||
|
||||
pub fn enable(&self, source: ToolSource, tools_to_enable: &[Arc<str>]) {
|
||||
let mut state = self.state.lock();
|
||||
state
|
||||
.disabled_tools_by_source
|
||||
fn enable(&mut self, source: ToolSource, tools_to_enable: &[Arc<str>]) {
|
||||
self.disabled_tools_by_source
|
||||
.entry(source)
|
||||
.or_default()
|
||||
.retain(|name| !tools_to_enable.contains(name));
|
||||
}
|
||||
|
||||
pub fn disable(&self, source: ToolSource, tools_to_disable: &[Arc<str>]) {
|
||||
let mut state = self.state.lock();
|
||||
state
|
||||
.disabled_tools_by_source
|
||||
fn disable(&mut self, source: ToolSource, tools_to_disable: &[Arc<str>]) {
|
||||
self.disabled_tools_by_source
|
||||
.entry(source)
|
||||
.or_default()
|
||||
.extend(tools_to_disable.into_iter().cloned());
|
||||
}
|
||||
|
||||
pub fn remove(&self, tool_ids_to_remove: &[ToolId]) {
|
||||
let mut state = self.state.lock();
|
||||
state
|
||||
.context_server_tools_by_id
|
||||
.retain(|id, _| !tool_ids_to_remove.contains(id));
|
||||
state.tools_changed();
|
||||
fn enable_source(&mut self, source: &ToolSource) {
|
||||
self.disabled_tools_by_source.remove(source);
|
||||
}
|
||||
|
||||
pub fn is_scripting_tool_enabled(&self) -> bool {
|
||||
let state = self.state.lock();
|
||||
!state.is_scripting_tool_disabled
|
||||
}
|
||||
fn disable_source(&mut self, source: ToolSource, cx: &App) {
|
||||
let tools_by_source = self.tools_by_source(cx);
|
||||
let Some(tools) = tools_by_source.get(&source) else {
|
||||
return;
|
||||
};
|
||||
|
||||
pub fn enable_scripting_tool(&self) {
|
||||
let mut state = self.state.lock();
|
||||
state.is_scripting_tool_disabled = false;
|
||||
}
|
||||
|
||||
pub fn disable_scripting_tool(&self) {
|
||||
let mut state = self.state.lock();
|
||||
state.is_scripting_tool_disabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
impl WorkingSetState {
|
||||
fn tools_changed(&mut self) {
|
||||
self.context_server_tools_by_name.clear();
|
||||
self.context_server_tools_by_name.extend(
|
||||
self.context_server_tools_by_id
|
||||
.values()
|
||||
.map(|tool| (tool.name(), tool.clone())),
|
||||
self.disabled_tools_by_source.insert(
|
||||
source,
|
||||
tools
|
||||
.into_iter()
|
||||
.map(|tool| tool.name().into())
|
||||
.collect::<HashSet<_>>(),
|
||||
);
|
||||
}
|
||||
|
||||
fn disable_all_tools(&mut self, cx: &App) {
|
||||
let tools = self.tools_by_source(cx);
|
||||
|
||||
for (source, tools) in tools {
|
||||
let tool_names = tools
|
||||
.into_iter()
|
||||
.map(|tool| tool.name().into())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
self.disable(source, &tool_names);
|
||||
}
|
||||
|
||||
self.disable_scripting_tool();
|
||||
}
|
||||
|
||||
fn enable_scripting_tool(&mut self) {
|
||||
self.is_scripting_tool_disabled = false;
|
||||
}
|
||||
|
||||
fn disable_scripting_tool(&mut self) {
|
||||
self.is_scripting_tool_disabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,15 +16,21 @@ anyhow.workspace = true
|
||||
assistant_tool.workspace = true
|
||||
chrono.workspace = true
|
||||
collections.workspace = true
|
||||
feature_flags.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
language.workspace = true
|
||||
language_model.workspace = true
|
||||
project.workspace = true
|
||||
release_channel.workspace = true
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
theme.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
workspace.workspace = true
|
||||
settings.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
rand.workspace = true
|
||||
|
||||
@@ -1,25 +1,38 @@
|
||||
mod bash_tool;
|
||||
mod delete_path_tool;
|
||||
mod diagnostics_tool;
|
||||
mod edit_files_tool;
|
||||
mod list_directory_tool;
|
||||
mod now_tool;
|
||||
mod path_search_tool;
|
||||
mod read_file_tool;
|
||||
mod regex_search;
|
||||
|
||||
use assistant_tool::ToolRegistry;
|
||||
use gpui::App;
|
||||
|
||||
use crate::bash_tool::BashTool;
|
||||
use crate::delete_path_tool::DeletePathTool;
|
||||
use crate::diagnostics_tool::DiagnosticsTool;
|
||||
use crate::edit_files_tool::EditFilesTool;
|
||||
use crate::list_directory_tool::ListDirectoryTool;
|
||||
use crate::now_tool::NowTool;
|
||||
use crate::path_search_tool::PathSearchTool;
|
||||
use crate::read_file_tool::ReadFileTool;
|
||||
use crate::regex_search::RegexSearchTool;
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
assistant_tool::init(cx);
|
||||
crate::edit_files_tool::log::init(cx);
|
||||
|
||||
let registry = ToolRegistry::global(cx);
|
||||
registry.register_tool(NowTool);
|
||||
registry.register_tool(ReadFileTool);
|
||||
registry.register_tool(ListDirectoryTool);
|
||||
registry.register_tool(BashTool);
|
||||
registry.register_tool(DeletePathTool);
|
||||
registry.register_tool(DiagnosticsTool);
|
||||
registry.register_tool(EditFilesTool);
|
||||
registry.register_tool(ListDirectoryTool);
|
||||
registry.register_tool(NowTool);
|
||||
registry.register_tool(PathSearchTool);
|
||||
registry.register_tool(ReadFileTool);
|
||||
registry.register_tool(RegexSearchTool);
|
||||
}
|
||||
|
||||
87
crates/assistant_tools/src/bash_tool.rs
Normal file
87
crates/assistant_tools/src/bash_tool.rs
Normal file
@@ -0,0 +1,87 @@
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use assistant_tool::Tool;
|
||||
use gpui::{App, Entity, Task};
|
||||
use language_model::LanguageModelRequestMessage;
|
||||
use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
use util::command::new_smol_command;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct BashToolInput {
|
||||
/// The bash command to execute as a one-liner.
|
||||
///
|
||||
/// WARNING: you must not `cd` into the working directory, as that's already
|
||||
/// taken care of automatically. Doing so will cause the command to fail!
|
||||
command: String,
|
||||
/// Working directory for the command. This must be one of the root directories of the project.
|
||||
working_directory: String,
|
||||
}
|
||||
|
||||
pub struct BashTool;
|
||||
|
||||
impl Tool for BashTool {
|
||||
fn name(&self) -> String {
|
||||
"bash".to_string()
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
include_str!("./bash_tool/description.md").to_string()
|
||||
}
|
||||
|
||||
fn input_schema(&self) -> serde_json::Value {
|
||||
let schema = schemars::schema_for!(BashToolInput);
|
||||
serde_json::to_value(&schema).unwrap()
|
||||
}
|
||||
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
input: serde_json::Value,
|
||||
_messages: &[LanguageModelRequestMessage],
|
||||
project: Entity<Project>,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<String>> {
|
||||
let input: BashToolInput = match serde_json::from_value(input) {
|
||||
Ok(input) => input,
|
||||
Err(err) => return Task::ready(Err(anyhow!(err))),
|
||||
};
|
||||
|
||||
let Some(worktree) = project
|
||||
.read(cx)
|
||||
.worktree_for_root_name(&input.working_directory, cx)
|
||||
else {
|
||||
return Task::ready(Err(anyhow!("Working directory not found in the project")));
|
||||
};
|
||||
let working_directory = 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 output = new_smol_command("bash")
|
||||
.arg("-c")
|
||||
.arg(&command)
|
||||
.current_dir(working_directory)
|
||||
.output()
|
||||
.await
|
||||
.context("Failed to execute bash command")?;
|
||||
|
||||
let output_string = String::from_utf8_lossy(&output.stdout).to_string();
|
||||
|
||||
if output.status.success() {
|
||||
if output_string.is_empty() {
|
||||
Ok("Command executed successfully.".to_string())
|
||||
} else {
|
||||
Ok(output_string)
|
||||
}
|
||||
} else {
|
||||
Ok(format!(
|
||||
"Command failed with exit code {}\n{}",
|
||||
output.status.code().unwrap_or(-1),
|
||||
&output_string
|
||||
))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
7
crates/assistant_tools/src/bash_tool/description.md
Normal file
7
crates/assistant_tools/src/bash_tool/description.md
Normal file
@@ -0,0 +1,7 @@
|
||||
Executes a bash one-liner and returns the combined output.
|
||||
|
||||
This tool spawns a bash process IN THE SPECIFIED WORKING DIRECTORY, combines stdout and stderr into one interleaved stream as they are produced (preserving the order of writes), and captures that stream into a string which is returned.
|
||||
|
||||
WARNING: **NEVER** use 'cd' commands to navigate to the working directory - this is automatically handled by the 'working_directory' parameter. Only use 'cd' to navigate to subdirectories within the specified working directory.
|
||||
|
||||
Remember that each invocation of this tool will spawn a new bash process, so you can't rely on any state from previous invocations.
|
||||
165
crates/assistant_tools/src/delete_path_tool.rs
Normal file
165
crates/assistant_tools/src/delete_path_tool.rs
Normal file
@@ -0,0 +1,165 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use assistant_tool::Tool;
|
||||
use gpui::{App, Entity, Task};
|
||||
use language_model::LanguageModelRequestMessage;
|
||||
use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{fs, path::PathBuf, sync::Arc};
|
||||
use util::paths::PathMatcher;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct DeletePathToolInput {
|
||||
/// The glob to match files in the project to delete.
|
||||
///
|
||||
/// <example>
|
||||
/// If the project has the following files:
|
||||
///
|
||||
/// - directory1/a/something.txt
|
||||
/// - directory2/a/things.txt
|
||||
/// - directory3/a/other.txt
|
||||
///
|
||||
/// You can delete the first two files by providing a glob of "*thing*.txt"
|
||||
/// </example>
|
||||
pub glob: String,
|
||||
}
|
||||
|
||||
pub struct DeletePathTool;
|
||||
|
||||
impl Tool for DeletePathTool {
|
||||
fn name(&self) -> String {
|
||||
"delete-path".into()
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
include_str!("./delete_path_tool/description.md").into()
|
||||
}
|
||||
|
||||
fn input_schema(&self) -> serde_json::Value {
|
||||
let schema = schemars::schema_for!(DeletePathToolInput);
|
||||
serde_json::to_value(&schema).unwrap()
|
||||
}
|
||||
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
input: serde_json::Value,
|
||||
_messages: &[LanguageModelRequestMessage],
|
||||
project: Entity<Project>,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<String>> {
|
||||
let glob = match serde_json::from_value::<DeletePathToolInput>(input) {
|
||||
Ok(input) => input.glob,
|
||||
Err(err) => return Task::ready(Err(anyhow!(err))),
|
||||
};
|
||||
let path_matcher = match PathMatcher::new(&[glob.clone()]) {
|
||||
Ok(matcher) => matcher,
|
||||
Err(err) => return Task::ready(Err(anyhow!("Invalid glob: {}", err))),
|
||||
};
|
||||
|
||||
struct Match {
|
||||
display_path: String,
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
let mut matches = Vec::new();
|
||||
let mut deleted_paths = Vec::new();
|
||||
let mut errors = Vec::new();
|
||||
|
||||
for worktree_handle in project.read(cx).worktrees(cx) {
|
||||
let worktree = worktree_handle.read(cx);
|
||||
let worktree_root = worktree.abs_path().to_path_buf();
|
||||
|
||||
// Don't consider ignored entries.
|
||||
for entry in worktree.entries(false, 0) {
|
||||
if path_matcher.is_match(&entry.path) {
|
||||
matches.push(Match {
|
||||
path: worktree_root.join(&entry.path),
|
||||
display_path: entry.path.display().to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if matches.is_empty() {
|
||||
return Task::ready(Ok(format!("No paths in the project matched {glob:?}")));
|
||||
}
|
||||
|
||||
let paths_matched = matches.len();
|
||||
|
||||
// Delete the files
|
||||
for Match { path, display_path } in matches {
|
||||
match fs::remove_file(&path) {
|
||||
Ok(()) => {
|
||||
deleted_paths.push(display_path);
|
||||
}
|
||||
Err(file_err) => {
|
||||
// Try to remove directory if it's not a file. Retrying as a directory
|
||||
// on error saves a syscall compared to checking whether it's
|
||||
// a directory up front for every single file.
|
||||
if let Err(dir_err) = fs::remove_dir_all(&path) {
|
||||
let error = if path.is_dir() {
|
||||
format!("Failed to delete directory {}: {dir_err}", display_path)
|
||||
} else {
|
||||
format!("Failed to delete file {}: {file_err}", display_path)
|
||||
};
|
||||
|
||||
errors.push(error);
|
||||
} else {
|
||||
deleted_paths.push(display_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if errors.is_empty() {
|
||||
// 0 deleted paths should never happen if there were no errors;
|
||||
// we already returned if matches was empty.
|
||||
let answer = if deleted_paths.len() == 1 {
|
||||
format!(
|
||||
"Deleted {}",
|
||||
deleted_paths.first().unwrap_or(&String::new())
|
||||
)
|
||||
} else {
|
||||
// Sort to group entries in the same directory together
|
||||
deleted_paths.sort();
|
||||
|
||||
let mut buf = format!("Deleted these {} paths:\n", deleted_paths.len());
|
||||
|
||||
for path in deleted_paths.iter() {
|
||||
buf.push('\n');
|
||||
buf.push_str(path);
|
||||
}
|
||||
|
||||
buf
|
||||
};
|
||||
|
||||
Task::ready(Ok(answer))
|
||||
} else {
|
||||
if deleted_paths.is_empty() {
|
||||
Task::ready(Err(anyhow!(
|
||||
"{glob:?} matched {} deleted because of {}:\n{}",
|
||||
if paths_matched == 1 {
|
||||
"1 path, but it was not".to_string()
|
||||
} else {
|
||||
format!("{} paths, but none were", paths_matched)
|
||||
},
|
||||
if errors.len() == 1 {
|
||||
"this error".to_string()
|
||||
} else {
|
||||
format!("{} errors", errors.len())
|
||||
},
|
||||
errors.join("\n")
|
||||
)))
|
||||
} else {
|
||||
// Sort to group entries in the same directory together
|
||||
deleted_paths.sort();
|
||||
Task::ready(Ok(format!(
|
||||
"Deleted {} paths matching glob {glob:?}:\n{}\n\nErrors:\n{}",
|
||||
deleted_paths.len(),
|
||||
deleted_paths.join("\n"),
|
||||
errors.join("\n")
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
Deletes all files and directories in the project which match the given glob, and returns a list of the paths that were deleted.
|
||||
127
crates/assistant_tools/src/diagnostics_tool.rs
Normal file
127
crates/assistant_tools/src/diagnostics_tool.rs
Normal file
@@ -0,0 +1,127 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use assistant_tool::Tool;
|
||||
use gpui::{App, Entity, Task};
|
||||
use language::{DiagnosticSeverity, OffsetRangeExt};
|
||||
use language_model::LanguageModelRequestMessage;
|
||||
use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
fmt::Write,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct DiagnosticsToolInput {
|
||||
/// The path to get diagnostics for. If not provided, returns a project-wide summary.
|
||||
///
|
||||
/// This path should never be absolute, and the first component
|
||||
/// of the path should always be a root directory in a project.
|
||||
///
|
||||
/// <example>
|
||||
/// If the project has the following root directories:
|
||||
///
|
||||
/// - lorem
|
||||
/// - ipsum
|
||||
///
|
||||
/// If you wanna access diagnostics for `dolor.txt` in `ipsum`, you should use the path `ipsum/dolor.txt`.
|
||||
/// </example>
|
||||
pub path: Option<PathBuf>,
|
||||
}
|
||||
|
||||
pub struct DiagnosticsTool;
|
||||
|
||||
impl Tool for DiagnosticsTool {
|
||||
fn name(&self) -> String {
|
||||
"diagnostics".into()
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
include_str!("./diagnostics_tool/description.md").into()
|
||||
}
|
||||
|
||||
fn input_schema(&self) -> serde_json::Value {
|
||||
let schema = schemars::schema_for!(DiagnosticsToolInput);
|
||||
serde_json::to_value(&schema).unwrap()
|
||||
}
|
||||
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
input: serde_json::Value,
|
||||
_messages: &[LanguageModelRequestMessage],
|
||||
project: Entity<Project>,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<String>> {
|
||||
let input = match serde_json::from_value::<DiagnosticsToolInput>(input) {
|
||||
Ok(input) => input,
|
||||
Err(err) => return Task::ready(Err(anyhow!(err))),
|
||||
};
|
||||
|
||||
if let Some(path) = input.path {
|
||||
let Some(project_path) = project.read(cx).find_project_path(&path, cx) else {
|
||||
return Task::ready(Err(anyhow!("Could not find path in project")));
|
||||
};
|
||||
let buffer = project.update(cx, |project, cx| project.open_buffer(project_path, cx));
|
||||
|
||||
cx.spawn(|cx| async move {
|
||||
let mut output = String::new();
|
||||
let buffer = buffer.await?;
|
||||
let snapshot = buffer.read_with(&cx, |buffer, _cx| buffer.snapshot())?;
|
||||
|
||||
for (_, group) in snapshot.diagnostic_groups(None) {
|
||||
let entry = &group.entries[group.primary_ix];
|
||||
let range = entry.range.to_point(&snapshot);
|
||||
let severity = match entry.diagnostic.severity {
|
||||
DiagnosticSeverity::ERROR => "error",
|
||||
DiagnosticSeverity::WARNING => "warning",
|
||||
_ => continue,
|
||||
};
|
||||
|
||||
writeln!(
|
||||
output,
|
||||
"{} at line {}: {}",
|
||||
severity,
|
||||
range.start.row + 1,
|
||||
entry.diagnostic.message
|
||||
)?;
|
||||
}
|
||||
|
||||
if output.is_empty() {
|
||||
Ok("File doesn't have errors or warnings!".to_string())
|
||||
} else {
|
||||
Ok(output)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
let project = project.read(cx);
|
||||
let mut output = String::new();
|
||||
let mut has_diagnostics = false;
|
||||
|
||||
for (project_path, _, summary) in project.diagnostic_summaries(true, cx) {
|
||||
if summary.error_count > 0 || summary.warning_count > 0 {
|
||||
let Some(worktree) = project.worktree_for_id(project_path.worktree_id, cx)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
has_diagnostics = true;
|
||||
output.push_str(&format!(
|
||||
"{}: {} error(s), {} warning(s)\n",
|
||||
Path::new(worktree.read(cx).root_name())
|
||||
.join(project_path.path)
|
||||
.display(),
|
||||
summary.error_count,
|
||||
summary.warning_count
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if has_diagnostics {
|
||||
Task::ready(Ok(output))
|
||||
} else {
|
||||
Task::ready(Ok("No errors or warnings found in the project.".to_string()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
16
crates/assistant_tools/src/diagnostics_tool/description.md
Normal file
16
crates/assistant_tools/src/diagnostics_tool/description.md
Normal file
@@ -0,0 +1,16 @@
|
||||
Get errors and warnings for the project or a specific file.
|
||||
|
||||
This tool can be invoked after a series of edits to determine if further edits are necessary, or if the user asks to fix errors or warnings in their codebase.
|
||||
|
||||
When a path is provided, shows all diagnostics for that specific file.
|
||||
When no path is provided, shows a summary of error and warning counts for all files in the project.
|
||||
|
||||
<example>
|
||||
To get diagnostics for a specific file:
|
||||
{
|
||||
"path": "src/main.rs"
|
||||
}
|
||||
|
||||
To get a project-wide diagnostic summary:
|
||||
{}
|
||||
</example>
|
||||
@@ -1,4 +1,5 @@
|
||||
mod edit_action;
|
||||
pub mod log;
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use assistant_tool::Tool;
|
||||
@@ -9,24 +10,50 @@ use gpui::{App, Entity, Task};
|
||||
use language_model::{
|
||||
LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, Role,
|
||||
};
|
||||
use project::{Project, ProjectPath};
|
||||
use log::{EditToolLog, EditToolRequestId};
|
||||
use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::Write;
|
||||
use std::sync::Arc;
|
||||
use util::ResultExt;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct EditFilesToolInput {
|
||||
/// High-level edit instructions. These will be interpreted by a smaller model,
|
||||
/// so explain the edits you want that model to make and to which files need changing.
|
||||
/// The description should be concise and clear. We will show this description to the user
|
||||
/// as well.
|
||||
/// High-level edit instructions. These will be interpreted by a smaller
|
||||
/// model, so explain the changes you want that model to make and which
|
||||
/// file paths need changing.
|
||||
///
|
||||
/// The description should be concise and clear. We will show this
|
||||
/// description to the user as well.
|
||||
///
|
||||
/// WARNING: When specifying which file paths need changing, you MUST
|
||||
/// start each path with one of the project's root directories.
|
||||
///
|
||||
/// WARNING: NEVER include code blocks or snippets in edit instructions.
|
||||
/// Only provide natural language descriptions of the changes needed! The tool will
|
||||
/// reject any instructions that contain code blocks or snippets.
|
||||
///
|
||||
/// The following examples assume we have two root directories in the project:
|
||||
/// - root-1
|
||||
/// - root-2
|
||||
///
|
||||
/// <example>
|
||||
/// If you want to rename a function you can say "Rename the function 'foo' to 'bar'".
|
||||
/// If you want to introduce a new quit function to kill the process, your
|
||||
/// instructions should be: "Add a new `quit` function to
|
||||
/// `root-1/src/main.rs` to kill the process".
|
||||
///
|
||||
/// Notice how the file path starts with root-1. Without that, the path
|
||||
/// would be ambiguous and the call would fail!
|
||||
/// </example>
|
||||
///
|
||||
/// <example>
|
||||
/// If you want to add a new function you can say "Add a new method to the `User` struct that prints the age".
|
||||
/// If you want to change documentation to always start with a capital
|
||||
/// letter, your instructions should be: "In `root-2/db.js`,
|
||||
/// `root-2/inMemory.js` and `root-2/sql.js`, change all the documentation
|
||||
/// to start with a capital letter".
|
||||
///
|
||||
/// Notice how we never specify code snippets in the instructions!
|
||||
/// </example>
|
||||
pub edit_instructions: String,
|
||||
}
|
||||
@@ -59,6 +86,45 @@ impl Tool for EditFilesTool {
|
||||
Err(err) => return Task::ready(Err(anyhow!(err))),
|
||||
};
|
||||
|
||||
match EditToolLog::try_global(cx) {
|
||||
Some(log) => {
|
||||
let req_id = log.update(cx, |log, cx| {
|
||||
log.new_request(input.edit_instructions.clone(), cx)
|
||||
});
|
||||
|
||||
let task =
|
||||
EditFilesTool::run(input, messages, project, Some((log.clone(), req_id)), cx);
|
||||
|
||||
cx.spawn(|mut cx| async move {
|
||||
let result = task.await;
|
||||
|
||||
let str_result = match &result {
|
||||
Ok(out) => Ok(out.clone()),
|
||||
Err(err) => Err(err.to_string()),
|
||||
};
|
||||
|
||||
log.update(&mut cx, |log, cx| {
|
||||
log.set_tool_output(req_id, str_result, cx)
|
||||
})
|
||||
.log_err();
|
||||
|
||||
result
|
||||
})
|
||||
}
|
||||
|
||||
None => EditFilesTool::run(input, messages, project, None, cx),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EditFilesTool {
|
||||
fn run(
|
||||
input: EditFilesToolInput,
|
||||
messages: &[LanguageModelRequestMessage],
|
||||
project: Entity<Project>,
|
||||
log: Option<(Entity<EditToolLog>, EditToolRequestId)>,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<String>> {
|
||||
let model_registry = LanguageModelRegistry::read_global(cx);
|
||||
let Some(model) = model_registry.editor_model() else {
|
||||
return Task::ready(Err(anyhow!("No editor model configured")));
|
||||
@@ -85,7 +151,7 @@ impl Tool for EditFilesTool {
|
||||
messages,
|
||||
tools: vec![],
|
||||
stop: vec![],
|
||||
temperature: None,
|
||||
temperature: Some(0.0),
|
||||
};
|
||||
|
||||
let mut parser = EditActionParser::new();
|
||||
@@ -96,26 +162,25 @@ impl Tool for EditFilesTool {
|
||||
let mut changed_buffers = HashSet::default();
|
||||
let mut applied_edits = 0;
|
||||
|
||||
let log = log.clone();
|
||||
|
||||
while let Some(chunk) = chunks.stream.next().await {
|
||||
for action in parser.parse_chunk(&chunk?) {
|
||||
let chunk = chunk?;
|
||||
|
||||
let new_actions = parser.parse_chunk(&chunk);
|
||||
|
||||
if let Some((ref log, req_id)) = log {
|
||||
log.update(&mut cx, |log, cx| {
|
||||
log.push_editor_response_chunk(req_id, &chunk, &new_actions, cx)
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
|
||||
for action in new_actions {
|
||||
let project_path = project.read_with(&cx, |project, cx| {
|
||||
let worktree_root_name = action
|
||||
.file_path()
|
||||
.components()
|
||||
.next()
|
||||
.context("Invalid path")?;
|
||||
let worktree = project
|
||||
.worktree_for_root_name(
|
||||
&worktree_root_name.as_os_str().to_string_lossy(),
|
||||
cx,
|
||||
)
|
||||
.context("Directory not found in project")?;
|
||||
anyhow::Ok(ProjectPath {
|
||||
worktree_id: worktree.read(cx).id(),
|
||||
path: Arc::from(
|
||||
action.file_path().strip_prefix(worktree_root_name).unwrap(),
|
||||
),
|
||||
})
|
||||
project
|
||||
.find_project_path(action.file_path(), cx)
|
||||
.context("Path not found in project")
|
||||
})??;
|
||||
|
||||
let buffer = project
|
||||
@@ -125,15 +190,27 @@ impl Tool for EditFilesTool {
|
||||
let diff = buffer
|
||||
.read_with(&cx, |buffer, cx| {
|
||||
let new_text = match action {
|
||||
EditAction::Replace { old, new, .. } => {
|
||||
EditAction::Replace {
|
||||
file_path,
|
||||
old,
|
||||
new,
|
||||
} => {
|
||||
// TODO: Replace in background?
|
||||
buffer.text().replace(&old, &new)
|
||||
let text = buffer.text();
|
||||
if text.contains(&old) {
|
||||
text.replace(&old, &new)
|
||||
} else {
|
||||
return Err(anyhow!(
|
||||
"Could not find search text in {}",
|
||||
file_path.display()
|
||||
));
|
||||
}
|
||||
}
|
||||
EditAction::Write { content, .. } => content,
|
||||
};
|
||||
|
||||
buffer.diff(new_text, cx)
|
||||
})?
|
||||
anyhow::Ok(buffer.diff(new_text, cx))
|
||||
})??
|
||||
.await;
|
||||
|
||||
let _clock =
|
||||
@@ -145,17 +222,29 @@ impl Tool for EditFilesTool {
|
||||
}
|
||||
}
|
||||
|
||||
let mut answer = match changed_buffers.len() {
|
||||
0 => "No files were edited.".to_string(),
|
||||
1 => "Successfully edited ".to_string(),
|
||||
_ => "Successfully edited these files:\n\n".to_string(),
|
||||
};
|
||||
|
||||
// Save each buffer once at the end
|
||||
for buffer in changed_buffers {
|
||||
project
|
||||
.update(&mut cx, |project, cx| project.save_buffer(buffer, cx))?
|
||||
.update(&mut cx, |project, cx| {
|
||||
if let Some(file) = buffer.read(&cx).file() {
|
||||
let _ = writeln!(&mut answer, "{}", &file.full_path(cx).display());
|
||||
}
|
||||
|
||||
project.save_buffer(buffer, cx)
|
||||
})?
|
||||
.await?;
|
||||
}
|
||||
|
||||
let errors = parser.errors();
|
||||
|
||||
if errors.is_empty() {
|
||||
Ok("Successfully applied all edits".into())
|
||||
Ok(answer.trim_end().to_string())
|
||||
} else {
|
||||
let error_message = errors
|
||||
.iter()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Edit files in the current project.
|
||||
Edit files in the current project by specifying instructions in natural language.
|
||||
|
||||
When using this tool, you should suggest one coherent edit that can be made to the codebase.
|
||||
|
||||
|
||||
@@ -106,7 +106,7 @@ Every *SEARCH/REPLACE block* must use this format:
|
||||
7. The end of the replace block: >>>>>>> REPLACE
|
||||
8. The closing fence: ```
|
||||
|
||||
Use the *FULL* file path, as shown to you by the user.
|
||||
Use the *FULL* file path, as shown to you by the user. Make sure to include the project's root directory name at the start of the path. *NEVER* specify the absolute path of the file!
|
||||
|
||||
Every *SEARCH* section must *EXACTLY MATCH* the existing file content, character for character, including all comments, docstrings, etc.
|
||||
If the file contains code or other data wrapped/escaped in json/xml/quotes or other containers, you need to propose edits to the literal contents of the file, including the container markup.
|
||||
|
||||
415
crates/assistant_tools/src/edit_files_tool/log.rs
Normal file
415
crates/assistant_tools/src/edit_files_tool/log.rs
Normal file
@@ -0,0 +1,415 @@
|
||||
use std::path::Path;
|
||||
|
||||
use collections::HashSet;
|
||||
use feature_flags::FeatureFlagAppExt;
|
||||
use gpui::{
|
||||
actions, list, prelude::*, App, Empty, Entity, EventEmitter, FocusHandle, Focusable, Global,
|
||||
ListAlignment, ListState, SharedString, Subscription, Window,
|
||||
};
|
||||
use release_channel::ReleaseChannel;
|
||||
use settings::Settings;
|
||||
use ui::prelude::*;
|
||||
use workspace::{item::ItemEvent, Item, Workspace, WorkspaceId};
|
||||
|
||||
use super::edit_action::EditAction;
|
||||
|
||||
actions!(debug, [EditTool]);
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
if cx.is_staff() || ReleaseChannel::global(cx) == ReleaseChannel::Dev {
|
||||
// Track events even before opening the log
|
||||
EditToolLog::global(cx);
|
||||
}
|
||||
|
||||
cx.observe_new(|workspace: &mut Workspace, _, _| {
|
||||
workspace.register_action(|workspace, _: &EditTool, window, cx| {
|
||||
let viewer = cx.new(EditToolLogViewer::new);
|
||||
workspace.add_item_to_active_pane(Box::new(viewer), None, true, window, cx)
|
||||
});
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
pub struct GlobalEditToolLog(Entity<EditToolLog>);
|
||||
|
||||
impl Global for GlobalEditToolLog {}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct EditToolLog {
|
||||
requests: Vec<EditToolRequest>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Hash, Eq, PartialEq)]
|
||||
pub struct EditToolRequestId(u32);
|
||||
|
||||
impl EditToolLog {
|
||||
pub fn global(cx: &mut App) -> Entity<Self> {
|
||||
match Self::try_global(cx) {
|
||||
Some(entity) => entity,
|
||||
None => {
|
||||
let entity = cx.new(|_cx| Self::default());
|
||||
cx.set_global(GlobalEditToolLog(entity.clone()));
|
||||
entity
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_global(cx: &App) -> Option<Entity<Self>> {
|
||||
cx.try_global::<GlobalEditToolLog>()
|
||||
.map(|log| log.0.clone())
|
||||
}
|
||||
|
||||
pub fn new_request(
|
||||
&mut self,
|
||||
instructions: String,
|
||||
cx: &mut Context<Self>,
|
||||
) -> EditToolRequestId {
|
||||
let id = EditToolRequestId(self.requests.len() as u32);
|
||||
self.requests.push(EditToolRequest {
|
||||
id,
|
||||
instructions,
|
||||
editor_response: None,
|
||||
tool_output: None,
|
||||
parsed_edits: Vec::new(),
|
||||
});
|
||||
cx.emit(EditToolLogEvent::Inserted);
|
||||
id
|
||||
}
|
||||
|
||||
pub fn push_editor_response_chunk(
|
||||
&mut self,
|
||||
id: EditToolRequestId,
|
||||
chunk: &str,
|
||||
new_actions: &[EditAction],
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if let Some(request) = self.requests.get_mut(id.0 as usize) {
|
||||
match &mut request.editor_response {
|
||||
None => {
|
||||
request.editor_response = Some(chunk.to_string());
|
||||
}
|
||||
Some(response) => {
|
||||
response.push_str(chunk);
|
||||
}
|
||||
}
|
||||
request.parsed_edits.extend(new_actions.iter().cloned());
|
||||
|
||||
cx.emit(EditToolLogEvent::Updated);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_tool_output(
|
||||
&mut self,
|
||||
id: EditToolRequestId,
|
||||
tool_output: Result<String, String>,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if let Some(request) = self.requests.get_mut(id.0 as usize) {
|
||||
request.tool_output = Some(tool_output);
|
||||
cx.emit(EditToolLogEvent::Updated);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum EditToolLogEvent {
|
||||
Inserted,
|
||||
Updated,
|
||||
}
|
||||
|
||||
impl EventEmitter<EditToolLogEvent> for EditToolLog {}
|
||||
|
||||
pub struct EditToolRequest {
|
||||
id: EditToolRequestId,
|
||||
instructions: String,
|
||||
// we don't use a result here because the error might have occurred after we got a response
|
||||
editor_response: Option<String>,
|
||||
parsed_edits: Vec<EditAction>,
|
||||
tool_output: Option<Result<String, String>>,
|
||||
}
|
||||
|
||||
pub struct EditToolLogViewer {
|
||||
focus_handle: FocusHandle,
|
||||
log: Entity<EditToolLog>,
|
||||
list_state: ListState,
|
||||
expanded_edits: HashSet<(EditToolRequestId, usize)>,
|
||||
_subscription: Subscription,
|
||||
}
|
||||
|
||||
impl EditToolLogViewer {
|
||||
pub fn new(cx: &mut Context<Self>) -> Self {
|
||||
let log = EditToolLog::global(cx);
|
||||
|
||||
let subscription = cx.subscribe(&log, Self::handle_log_event);
|
||||
|
||||
Self {
|
||||
focus_handle: cx.focus_handle(),
|
||||
log: log.clone(),
|
||||
list_state: ListState::new(
|
||||
log.read(cx).requests.len(),
|
||||
ListAlignment::Bottom,
|
||||
px(1024.),
|
||||
{
|
||||
let this = cx.entity().downgrade();
|
||||
move |ix, window: &mut Window, cx: &mut App| {
|
||||
this.update(cx, |this, cx| this.render_request(ix, window, cx))
|
||||
.unwrap()
|
||||
}
|
||||
},
|
||||
),
|
||||
expanded_edits: HashSet::default(),
|
||||
_subscription: subscription,
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_log_event(
|
||||
&mut self,
|
||||
_: Entity<EditToolLog>,
|
||||
event: &EditToolLogEvent,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
match event {
|
||||
EditToolLogEvent::Inserted => {
|
||||
let count = self.list_state.item_count();
|
||||
self.list_state.splice(count..count, 1);
|
||||
}
|
||||
EditToolLogEvent::Updated => {}
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn render_request(
|
||||
&self,
|
||||
index: usize,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> AnyElement {
|
||||
let requests = &self.log.read(cx).requests;
|
||||
let request = &requests[index];
|
||||
|
||||
v_flex()
|
||||
.gap_3()
|
||||
.child(Self::render_section(IconName::ArrowRight, "Tool Input"))
|
||||
.child(request.instructions.clone())
|
||||
.py_5()
|
||||
.when(index + 1 < requests.len(), |element| {
|
||||
element
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
})
|
||||
.map(|parent| match &request.editor_response {
|
||||
None => {
|
||||
if request.tool_output.is_none() {
|
||||
parent.child("...")
|
||||
} else {
|
||||
parent
|
||||
}
|
||||
}
|
||||
Some(response) => parent
|
||||
.child(Self::render_section(
|
||||
IconName::ZedAssistant,
|
||||
"Editor Response",
|
||||
))
|
||||
.child(Label::new(response.clone()).buffer_font(cx)),
|
||||
})
|
||||
.when(!request.parsed_edits.is_empty(), |parent| {
|
||||
parent
|
||||
.child(Self::render_section(IconName::Microscope, "Parsed Edits"))
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_2()
|
||||
.children(request.parsed_edits.iter().enumerate().map(
|
||||
|(index, edit)| {
|
||||
self.render_edit_action(edit, request.id, index, cx)
|
||||
},
|
||||
)),
|
||||
)
|
||||
})
|
||||
.when_some(request.tool_output.as_ref(), |parent, output| {
|
||||
parent
|
||||
.child(Self::render_section(IconName::ArrowLeft, "Tool Output"))
|
||||
.child(match output {
|
||||
Ok(output) => Label::new(output.clone()).color(Color::Success),
|
||||
Err(error) => Label::new(error.clone()).color(Color::Error),
|
||||
})
|
||||
})
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn render_section(icon: IconName, title: &'static str) -> AnyElement {
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(Icon::new(icon).color(Color::Muted))
|
||||
.child(Label::new(title).size(LabelSize::Small).color(Color::Muted))
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn render_edit_action(
|
||||
&self,
|
||||
edit_action: &EditAction,
|
||||
request_id: EditToolRequestId,
|
||||
index: usize,
|
||||
cx: &Context<Self>,
|
||||
) -> AnyElement {
|
||||
let expanded_id = (request_id, index);
|
||||
|
||||
match edit_action {
|
||||
EditAction::Replace {
|
||||
file_path,
|
||||
old,
|
||||
new,
|
||||
} => self
|
||||
.render_edit_action_container(
|
||||
expanded_id,
|
||||
&file_path,
|
||||
[
|
||||
Self::render_block(IconName::MagnifyingGlass, "Search", old.clone(), cx)
|
||||
.border_r_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.into_any(),
|
||||
Self::render_block(IconName::Replace, "Replace", new.clone(), cx)
|
||||
.into_any(),
|
||||
],
|
||||
cx,
|
||||
)
|
||||
.into_any(),
|
||||
EditAction::Write { file_path, content } => self
|
||||
.render_edit_action_container(
|
||||
expanded_id,
|
||||
&file_path,
|
||||
[
|
||||
Self::render_block(IconName::Pencil, "Write", content.clone(), cx)
|
||||
.into_any(),
|
||||
],
|
||||
cx,
|
||||
)
|
||||
.into_any(),
|
||||
}
|
||||
}
|
||||
|
||||
fn render_edit_action_container(
|
||||
&self,
|
||||
expanded_id: (EditToolRequestId, usize),
|
||||
file_path: &Path,
|
||||
content: impl IntoIterator<Item = AnyElement>,
|
||||
cx: &Context<Self>,
|
||||
) -> AnyElement {
|
||||
let is_expanded = self.expanded_edits.contains(&expanded_id);
|
||||
|
||||
v_flex()
|
||||
.child(
|
||||
h_flex()
|
||||
.bg(cx.theme().colors().element_background)
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.rounded_t_md()
|
||||
.when(!is_expanded, |el| el.rounded_b_md())
|
||||
.py_1()
|
||||
.px_2()
|
||||
.gap_1()
|
||||
.child(
|
||||
ui::Disclosure::new(ElementId::Integer(expanded_id.1), is_expanded)
|
||||
.on_click(cx.listener(move |this, _ev, _window, cx| {
|
||||
if is_expanded {
|
||||
this.expanded_edits.remove(&expanded_id);
|
||||
} else {
|
||||
this.expanded_edits.insert(expanded_id);
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
})),
|
||||
)
|
||||
.child(Label::new(file_path.display().to_string()).size(LabelSize::Small)),
|
||||
)
|
||||
.child(if is_expanded {
|
||||
h_flex()
|
||||
.border_1()
|
||||
.border_t_0()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.rounded_b_md()
|
||||
.children(content)
|
||||
.into_any()
|
||||
} else {
|
||||
Empty.into_any()
|
||||
})
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn render_block(icon: IconName, title: &'static str, content: String, cx: &App) -> Div {
|
||||
v_flex()
|
||||
.p_1()
|
||||
.gap_1()
|
||||
.flex_1()
|
||||
.h_full()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(Icon::new(icon).color(Color::Muted))
|
||||
.child(Label::new(title).size(LabelSize::Small).color(Color::Muted)),
|
||||
)
|
||||
.font(theme::ThemeSettings::get_global(cx).buffer_font.clone())
|
||||
.text_sm()
|
||||
.child(content)
|
||||
.child(div().flex_1())
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<()> for EditToolLogViewer {}
|
||||
|
||||
impl Focusable for EditToolLogViewer {
|
||||
fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Item for EditToolLogViewer {
|
||||
type Event = ();
|
||||
|
||||
fn to_item_events(_: &Self::Event, _: impl FnMut(ItemEvent)) {}
|
||||
|
||||
fn tab_content_text(&self, _window: &Window, _cx: &App) -> Option<SharedString> {
|
||||
Some("Edit Tool Log".into())
|
||||
}
|
||||
|
||||
fn telemetry_event_text(&self) -> Option<&'static str> {
|
||||
None
|
||||
}
|
||||
|
||||
fn clone_on_split(
|
||||
&self,
|
||||
_workspace_id: Option<WorkspaceId>,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<Entity<Self>>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
Some(cx.new(Self::new))
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for EditToolLogViewer {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
if self.list_state.item_count() == 0 {
|
||||
return v_flex()
|
||||
.justify_center()
|
||||
.size_full()
|
||||
.gap_1()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.text_center()
|
||||
.text_lg()
|
||||
.child("No requests yet")
|
||||
.child(
|
||||
div()
|
||||
.text_ui(cx)
|
||||
.child("Go ask the assistant to perform some edits"),
|
||||
);
|
||||
}
|
||||
|
||||
v_flex()
|
||||
.p_4()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.size_full()
|
||||
.child(list(self.list_state.clone()).flex_grow())
|
||||
}
|
||||
}
|
||||
@@ -12,10 +12,10 @@ pub struct ListDirectoryToolInput {
|
||||
/// The relative path of the directory to list.
|
||||
///
|
||||
/// This path should never be absolute, and the first component
|
||||
/// of the path should always be a top-level directory in a project.
|
||||
/// of the path should always be a root directory in a project.
|
||||
///
|
||||
/// <example>
|
||||
/// If the project has the following top-level directories:
|
||||
/// If the project has the following root directories:
|
||||
///
|
||||
/// - directory1
|
||||
/// - directory2
|
||||
@@ -24,7 +24,7 @@ pub struct ListDirectoryToolInput {
|
||||
/// </example>
|
||||
///
|
||||
/// <example>
|
||||
/// If the project has the following top-level directories:
|
||||
/// If the project has the following root directories:
|
||||
///
|
||||
/// - foo
|
||||
/// - bar
|
||||
@@ -62,27 +62,37 @@ impl Tool for ListDirectoryTool {
|
||||
Err(err) => return Task::ready(Err(anyhow!(err))),
|
||||
};
|
||||
|
||||
let Some(worktree_root_name) = input.path.components().next() else {
|
||||
return Task::ready(Err(anyhow!("Invalid path")));
|
||||
let Some(project_path) = project.read(cx).find_project_path(&input.path, cx) else {
|
||||
return Task::ready(Err(anyhow!("Path not found in project")));
|
||||
};
|
||||
let Some(worktree) = project
|
||||
.read(cx)
|
||||
.worktree_for_root_name(&worktree_root_name.as_os_str().to_string_lossy(), cx)
|
||||
.worktree_for_id(project_path.worktree_id, cx)
|
||||
else {
|
||||
return Task::ready(Err(anyhow!("Directory not found in the project")));
|
||||
return Task::ready(Err(anyhow!("Worktree not found")));
|
||||
};
|
||||
let path = input.path.strip_prefix(worktree_root_name).unwrap();
|
||||
let worktree = worktree.read(cx);
|
||||
|
||||
let Some(entry) = worktree.entry_for_path(&project_path.path) else {
|
||||
return Task::ready(Err(anyhow!("Path not found: {}", input.path.display())));
|
||||
};
|
||||
|
||||
if !entry.is_dir() {
|
||||
return Task::ready(Err(anyhow!("{} is a file.", input.path.display())));
|
||||
}
|
||||
|
||||
let mut output = String::new();
|
||||
for entry in worktree.read(cx).child_entries(path) {
|
||||
for entry in worktree.child_entries(&project_path.path) {
|
||||
writeln!(
|
||||
output,
|
||||
"{}",
|
||||
Path::new(worktree_root_name.as_os_str())
|
||||
.join(&entry.path)
|
||||
.display(),
|
||||
Path::new(worktree.root_name()).join(&entry.path).display(),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
if output.is_empty() {
|
||||
return Task::ready(Ok(format!("{} is empty.", input.path.display())));
|
||||
}
|
||||
Task::ready(Ok(output))
|
||||
}
|
||||
}
|
||||
|
||||
88
crates/assistant_tools/src/path_search_tool.rs
Normal file
88
crates/assistant_tools/src/path_search_tool.rs
Normal file
@@ -0,0 +1,88 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use assistant_tool::Tool;
|
||||
use gpui::{App, Entity, Task};
|
||||
use language_model::LanguageModelRequestMessage;
|
||||
use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{path::PathBuf, sync::Arc};
|
||||
use util::paths::PathMatcher;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct PathSearchToolInput {
|
||||
/// The glob to search all project paths for.
|
||||
///
|
||||
/// <example>
|
||||
/// If the project has the following root directories:
|
||||
///
|
||||
/// - directory1/a/something.txt
|
||||
/// - directory2/a/things.txt
|
||||
/// - directory3/a/other.txt
|
||||
///
|
||||
/// You can get back the first two paths by providing a glob of "*thing*.txt"
|
||||
/// </example>
|
||||
pub glob: String,
|
||||
}
|
||||
|
||||
pub struct PathSearchTool;
|
||||
|
||||
impl Tool for PathSearchTool {
|
||||
fn name(&self) -> String {
|
||||
"path-search".into()
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
include_str!("./path_search_tool/description.md").into()
|
||||
}
|
||||
|
||||
fn input_schema(&self) -> serde_json::Value {
|
||||
let schema = schemars::schema_for!(PathSearchToolInput);
|
||||
serde_json::to_value(&schema).unwrap()
|
||||
}
|
||||
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
input: serde_json::Value,
|
||||
_messages: &[LanguageModelRequestMessage],
|
||||
project: Entity<Project>,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<String>> {
|
||||
let glob = match serde_json::from_value::<PathSearchToolInput>(input) {
|
||||
Ok(input) => input.glob,
|
||||
Err(err) => return Task::ready(Err(anyhow!(err))),
|
||||
};
|
||||
let path_matcher = match PathMatcher::new(&[glob.clone()]) {
|
||||
Ok(matcher) => matcher,
|
||||
Err(err) => return Task::ready(Err(anyhow!("Invalid glob: {}", err))),
|
||||
};
|
||||
|
||||
let mut matches = Vec::new();
|
||||
|
||||
for worktree_handle in project.read(cx).worktrees(cx) {
|
||||
let worktree = worktree_handle.read(cx);
|
||||
let root_name = worktree.root_name();
|
||||
|
||||
// Don't consider ignored entries.
|
||||
for entry in worktree.entries(false, 0) {
|
||||
if path_matcher.is_match(&entry.path) {
|
||||
matches.push(
|
||||
PathBuf::from(root_name)
|
||||
.join(&entry.path)
|
||||
.to_string_lossy()
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if matches.is_empty() {
|
||||
Task::ready(Ok(format!(
|
||||
"No paths in the project matched the glob {glob:?}"
|
||||
)))
|
||||
} else {
|
||||
// Sort to group entries in the same directory together.
|
||||
matches.sort();
|
||||
Task::ready(Ok(matches.join("\n")))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
Returns all the paths in the project which match the given glob.
|
||||
@@ -5,7 +5,7 @@ use anyhow::{anyhow, Result};
|
||||
use assistant_tool::Tool;
|
||||
use gpui::{App, Entity, Task};
|
||||
use language_model::LanguageModelRequestMessage;
|
||||
use project::{Project, ProjectPath};
|
||||
use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -14,10 +14,10 @@ pub struct ReadFileToolInput {
|
||||
/// The relative path of the file to read.
|
||||
///
|
||||
/// This path should never be absolute, and the first component
|
||||
/// of the path should always be a top-level directory in a project.
|
||||
/// of the path should always be a root directory in a project.
|
||||
///
|
||||
/// <example>
|
||||
/// If the project has the following top-level directories:
|
||||
/// If the project has the following root directories:
|
||||
///
|
||||
/// - directory1
|
||||
/// - directory2
|
||||
@@ -56,18 +56,8 @@ impl Tool for ReadFileTool {
|
||||
Err(err) => return Task::ready(Err(anyhow!(err))),
|
||||
};
|
||||
|
||||
let Some(worktree_root_name) = input.path.components().next() else {
|
||||
return Task::ready(Err(anyhow!("Invalid path")));
|
||||
};
|
||||
let Some(worktree) = project
|
||||
.read(cx)
|
||||
.worktree_for_root_name(&worktree_root_name.as_os_str().to_string_lossy(), cx)
|
||||
else {
|
||||
return Task::ready(Err(anyhow!("Directory not found in the project")));
|
||||
};
|
||||
let project_path = ProjectPath {
|
||||
worktree_id: worktree.read(cx).id(),
|
||||
path: Arc::from(input.path.strip_prefix(worktree_root_name).unwrap()),
|
||||
let Some(project_path) = project.read(cx).find_project_path(&input.path, cx) else {
|
||||
return Task::ready(Err(anyhow!("Path not found in project")));
|
||||
};
|
||||
cx.spawn(|cx| async move {
|
||||
let buffer = cx
|
||||
|
||||
@@ -22,7 +22,6 @@ git2.workspace = true
|
||||
gpui.workspace = true
|
||||
language.workspace = true
|
||||
log.workspace = true
|
||||
pretty_assertions.workspace = true
|
||||
rope.workspace = true
|
||||
sum_tree.workspace = true
|
||||
text.workspace = true
|
||||
@@ -32,6 +31,7 @@ util.workspace = true
|
||||
ctor.workspace = true
|
||||
env_logger.workspace = true
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
pretty_assertions.workspace = true
|
||||
rand.workspace = true
|
||||
serde_json.workspace = true
|
||||
text = { workspace = true, features = ["test-support"] }
|
||||
|
||||
@@ -6,9 +6,9 @@ use rope::Rope;
|
||||
use std::cmp::Ordering;
|
||||
use std::mem;
|
||||
use std::{future::Future, iter, ops::Range, sync::Arc};
|
||||
use sum_tree::SumTree;
|
||||
use sum_tree::{SumTree, TreeMap};
|
||||
use text::ToOffset as _;
|
||||
use text::{Anchor, Bias, BufferId, OffsetRangeExt, Point};
|
||||
use text::{AnchorRangeExt, ToOffset as _};
|
||||
use util::ResultExt;
|
||||
|
||||
pub struct BufferDiff {
|
||||
@@ -26,7 +26,7 @@ pub struct BufferDiffSnapshot {
|
||||
#[derive(Clone)]
|
||||
struct BufferDiffInner {
|
||||
hunks: SumTree<InternalDiffHunk>,
|
||||
pending_hunks: SumTree<PendingHunk>,
|
||||
pending_hunks: TreeMap<usize, PendingHunk>,
|
||||
base_text: language::BufferSnapshot,
|
||||
base_text_exists: bool,
|
||||
}
|
||||
@@ -48,7 +48,7 @@ pub enum DiffHunkStatusKind {
|
||||
pub enum DiffHunkSecondaryStatus {
|
||||
HasSecondaryHunk,
|
||||
OverlapsWithSecondaryHunk,
|
||||
NoSecondaryHunk,
|
||||
None,
|
||||
SecondaryHunkAdditionPending,
|
||||
SecondaryHunkRemovalPending,
|
||||
}
|
||||
@@ -74,8 +74,6 @@ struct InternalDiffHunk {
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
struct PendingHunk {
|
||||
buffer_range: Range<Anchor>,
|
||||
diff_base_byte_range: Range<usize>,
|
||||
buffer_version: clock::Global,
|
||||
new_status: DiffHunkSecondaryStatus,
|
||||
}
|
||||
@@ -95,16 +93,6 @@ impl sum_tree::Item for InternalDiffHunk {
|
||||
}
|
||||
}
|
||||
|
||||
impl sum_tree::Item for PendingHunk {
|
||||
type Summary = DiffHunkSummary;
|
||||
|
||||
fn summary(&self, _cx: &text::BufferSnapshot) -> Self::Summary {
|
||||
DiffHunkSummary {
|
||||
buffer_range: self.buffer_range.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl sum_tree::Summary for DiffHunkSummary {
|
||||
type Context = text::BufferSnapshot;
|
||||
|
||||
@@ -188,7 +176,6 @@ impl BufferDiffSnapshot {
|
||||
}
|
||||
|
||||
impl BufferDiffInner {
|
||||
/// Returns the new index text and new pending hunks.
|
||||
fn stage_or_unstage_hunks(
|
||||
&mut self,
|
||||
unstaged_diff: &Self,
|
||||
@@ -196,7 +183,7 @@ impl BufferDiffInner {
|
||||
hunks: &[DiffHunk],
|
||||
buffer: &text::BufferSnapshot,
|
||||
file_exists: bool,
|
||||
) -> (Option<Rope>, SumTree<PendingHunk>) {
|
||||
) -> (Option<Rope>, Vec<(usize, PendingHunk)>) {
|
||||
let head_text = self
|
||||
.base_text_exists
|
||||
.then(|| self.base_text.as_rope().clone());
|
||||
@@ -208,41 +195,41 @@ impl BufferDiffInner {
|
||||
// entire file must be either created or deleted in the index.
|
||||
let (index_text, head_text) = match (index_text, head_text) {
|
||||
(Some(index_text), Some(head_text)) if file_exists || !stage => (index_text, head_text),
|
||||
(index_text, head_text) => {
|
||||
let (rope, new_status) = if stage {
|
||||
(_, head_text @ _) => {
|
||||
if stage {
|
||||
log::debug!("stage all");
|
||||
(
|
||||
return (
|
||||
file_exists.then(|| buffer.as_rope().clone()),
|
||||
DiffHunkSecondaryStatus::SecondaryHunkRemovalPending,
|
||||
)
|
||||
vec![(
|
||||
0,
|
||||
PendingHunk {
|
||||
buffer_version: buffer.version().clone(),
|
||||
new_status: DiffHunkSecondaryStatus::SecondaryHunkRemovalPending,
|
||||
},
|
||||
)],
|
||||
);
|
||||
} else {
|
||||
log::debug!("unstage all");
|
||||
(
|
||||
return (
|
||||
head_text,
|
||||
DiffHunkSecondaryStatus::SecondaryHunkAdditionPending,
|
||||
)
|
||||
};
|
||||
|
||||
let hunk = PendingHunk {
|
||||
buffer_range: Anchor::MIN..Anchor::MAX,
|
||||
diff_base_byte_range: 0..index_text.map_or(0, |rope| rope.len()),
|
||||
buffer_version: buffer.version().clone(),
|
||||
new_status,
|
||||
};
|
||||
let tree = SumTree::from_item(hunk, buffer);
|
||||
return (rope, tree);
|
||||
vec![(
|
||||
0,
|
||||
PendingHunk {
|
||||
buffer_version: buffer.version().clone(),
|
||||
new_status: DiffHunkSecondaryStatus::SecondaryHunkAdditionPending,
|
||||
},
|
||||
)],
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let mut unstaged_hunk_cursor = unstaged_diff.hunks.cursor::<DiffHunkSummary>(buffer);
|
||||
unstaged_hunk_cursor.next(buffer);
|
||||
|
||||
let mut pending_hunks = SumTree::new(buffer);
|
||||
let mut old_pending_hunks = unstaged_diff
|
||||
.pending_hunks
|
||||
.cursor::<DiffHunkSummary>(buffer);
|
||||
|
||||
// first, merge new hunks into pending_hunks
|
||||
let mut edits = Vec::new();
|
||||
let mut pending_hunks = Vec::new();
|
||||
let mut prev_unstaged_hunk_buffer_offset = 0;
|
||||
let mut prev_unstaged_hunk_base_text_offset = 0;
|
||||
for DiffHunk {
|
||||
buffer_range,
|
||||
diff_base_byte_range,
|
||||
@@ -250,58 +237,12 @@ impl BufferDiffInner {
|
||||
..
|
||||
} in hunks.iter().cloned()
|
||||
{
|
||||
let preceding_pending_hunks =
|
||||
old_pending_hunks.slice(&buffer_range.start, Bias::Left, buffer);
|
||||
|
||||
pending_hunks.append(preceding_pending_hunks, buffer);
|
||||
|
||||
// skip all overlapping old pending hunks
|
||||
while old_pending_hunks
|
||||
.item()
|
||||
.is_some_and(|preceding_pending_hunk_item| {
|
||||
preceding_pending_hunk_item
|
||||
.buffer_range
|
||||
.overlaps(&buffer_range, buffer)
|
||||
})
|
||||
{
|
||||
old_pending_hunks.next(buffer);
|
||||
}
|
||||
|
||||
// merge into pending hunks
|
||||
if (stage && secondary_status == DiffHunkSecondaryStatus::NoSecondaryHunk)
|
||||
if (stage && secondary_status == DiffHunkSecondaryStatus::None)
|
||||
|| (!stage && secondary_status == DiffHunkSecondaryStatus::HasSecondaryHunk)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
pending_hunks.push(
|
||||
PendingHunk {
|
||||
buffer_range,
|
||||
diff_base_byte_range,
|
||||
buffer_version: buffer.version().clone(),
|
||||
new_status: if stage {
|
||||
DiffHunkSecondaryStatus::SecondaryHunkRemovalPending
|
||||
} else {
|
||||
DiffHunkSecondaryStatus::SecondaryHunkAdditionPending
|
||||
},
|
||||
},
|
||||
buffer,
|
||||
);
|
||||
}
|
||||
// append the remainder
|
||||
pending_hunks.append(old_pending_hunks.suffix(buffer), buffer);
|
||||
|
||||
let mut prev_unstaged_hunk_buffer_offset = 0;
|
||||
let mut prev_unstaged_hunk_base_text_offset = 0;
|
||||
let mut edits = Vec::<(Range<usize>, String)>::new();
|
||||
|
||||
// then, iterate over all pending hunks (both new ones and the existing ones) and compute the edits
|
||||
for PendingHunk {
|
||||
buffer_range,
|
||||
diff_base_byte_range,
|
||||
..
|
||||
} in pending_hunks.iter().cloned()
|
||||
{
|
||||
let skipped_hunks = unstaged_hunk_cursor.slice(&buffer_range.start, Bias::Left, buffer);
|
||||
|
||||
if let Some(secondary_hunk) = skipped_hunks.last() {
|
||||
@@ -353,15 +294,22 @@ impl BufferDiffInner {
|
||||
.chunks_in_range(diff_base_byte_range.clone())
|
||||
.collect::<String>()
|
||||
};
|
||||
|
||||
pending_hunks.push((
|
||||
diff_base_byte_range.start,
|
||||
PendingHunk {
|
||||
buffer_version: buffer.version().clone(),
|
||||
new_status: if stage {
|
||||
DiffHunkSecondaryStatus::SecondaryHunkRemovalPending
|
||||
} else {
|
||||
DiffHunkSecondaryStatus::SecondaryHunkAdditionPending
|
||||
},
|
||||
},
|
||||
));
|
||||
edits.push((index_range, replacement_text));
|
||||
}
|
||||
|
||||
debug_assert!(edits.iter().is_sorted_by_key(|(range, _)| range.start));
|
||||
|
||||
let mut new_index_text = Rope::new();
|
||||
let mut index_cursor = index_text.cursor(0);
|
||||
|
||||
for (old_range, replacement_text) in edits {
|
||||
new_index_text.append(index_cursor.slice(old_range.start));
|
||||
index_cursor.seek_forward(old_range.end);
|
||||
@@ -406,14 +354,12 @@ impl BufferDiffInner {
|
||||
});
|
||||
|
||||
let mut secondary_cursor = None;
|
||||
let mut pending_hunks_cursor = None;
|
||||
let mut pending_hunks = TreeMap::default();
|
||||
if let Some(secondary) = secondary.as_ref() {
|
||||
let mut cursor = secondary.hunks.cursor::<DiffHunkSummary>(buffer);
|
||||
cursor.next(buffer);
|
||||
secondary_cursor = Some(cursor);
|
||||
let mut cursor = secondary.pending_hunks.cursor::<DiffHunkSummary>(buffer);
|
||||
cursor.next(buffer);
|
||||
pending_hunks_cursor = Some(cursor);
|
||||
pending_hunks = secondary.pending_hunks.clone();
|
||||
}
|
||||
|
||||
let max_point = buffer.max_point();
|
||||
@@ -432,33 +378,16 @@ impl BufferDiffInner {
|
||||
end_anchor = buffer.anchor_before(end_point);
|
||||
}
|
||||
|
||||
let mut secondary_status = DiffHunkSecondaryStatus::NoSecondaryHunk;
|
||||
let mut secondary_status = DiffHunkSecondaryStatus::None;
|
||||
|
||||
let mut has_pending = false;
|
||||
if let Some(pending_cursor) = pending_hunks_cursor.as_mut() {
|
||||
if start_anchor
|
||||
.cmp(&pending_cursor.start().buffer_range.start, buffer)
|
||||
.is_gt()
|
||||
{
|
||||
pending_cursor.seek_forward(&start_anchor, Bias::Left, buffer);
|
||||
}
|
||||
|
||||
if let Some(pending_hunk) = pending_cursor.item() {
|
||||
let mut pending_range = pending_hunk.buffer_range.to_point(buffer);
|
||||
if pending_range.end.column > 0 {
|
||||
pending_range.end.row += 1;
|
||||
pending_range.end.column = 0;
|
||||
}
|
||||
|
||||
if pending_range == (start_point..end_point) {
|
||||
if !buffer.has_edits_since_in_range(
|
||||
&pending_hunk.buffer_version,
|
||||
start_anchor..end_anchor,
|
||||
) {
|
||||
has_pending = true;
|
||||
secondary_status = pending_hunk.new_status;
|
||||
}
|
||||
}
|
||||
if let Some(pending_hunk) = pending_hunks.get(&start_base) {
|
||||
if !buffer.has_edits_since_in_range(
|
||||
&pending_hunk.buffer_version,
|
||||
start_anchor..end_anchor,
|
||||
) {
|
||||
has_pending = true;
|
||||
secondary_status = pending_hunk.new_status;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -520,7 +449,7 @@ impl BufferDiffInner {
|
||||
diff_base_byte_range: hunk.diff_base_byte_range.clone(),
|
||||
buffer_range: hunk.buffer_range.clone(),
|
||||
// The secondary status is not used by callers of this method.
|
||||
secondary_status: DiffHunkSecondaryStatus::NoSecondaryHunk,
|
||||
secondary_status: DiffHunkSecondaryStatus::None,
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -795,7 +724,7 @@ impl BufferDiff {
|
||||
base_text,
|
||||
hunks,
|
||||
base_text_exists,
|
||||
pending_hunks: SumTree::new(&buffer),
|
||||
pending_hunks: TreeMap::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -811,8 +740,8 @@ impl BufferDiff {
|
||||
cx.background_spawn(async move {
|
||||
BufferDiffInner {
|
||||
base_text: base_text_snapshot,
|
||||
pending_hunks: SumTree::new(&buffer),
|
||||
hunks: compute_hunks(base_text_pair, buffer),
|
||||
pending_hunks: TreeMap::default(),
|
||||
base_text_exists,
|
||||
}
|
||||
})
|
||||
@@ -822,7 +751,7 @@ impl BufferDiff {
|
||||
BufferDiffInner {
|
||||
base_text: language::Buffer::build_empty_snapshot(cx),
|
||||
hunks: SumTree::new(buffer),
|
||||
pending_hunks: SumTree::new(buffer),
|
||||
pending_hunks: TreeMap::default(),
|
||||
base_text_exists: false,
|
||||
}
|
||||
}
|
||||
@@ -838,7 +767,7 @@ impl BufferDiff {
|
||||
pub fn clear_pending_hunks(&mut self, cx: &mut Context<Self>) {
|
||||
if let Some(secondary_diff) = &self.secondary_diff {
|
||||
secondary_diff.update(cx, |diff, _| {
|
||||
diff.inner.pending_hunks = SumTree::from_summary(DiffHunkSummary::default());
|
||||
diff.inner.pending_hunks.clear();
|
||||
});
|
||||
cx.emit(BufferDiffEvent::DiffChanged {
|
||||
changed_range: Some(Anchor::MIN..Anchor::MAX),
|
||||
@@ -854,17 +783,18 @@ impl BufferDiff {
|
||||
file_exists: bool,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<Rope> {
|
||||
let (new_index_text, new_pending_hunks) = self.inner.stage_or_unstage_hunks(
|
||||
let (new_index_text, pending_hunks) = self.inner.stage_or_unstage_hunks(
|
||||
&self.secondary_diff.as_ref()?.read(cx).inner,
|
||||
stage,
|
||||
&hunks,
|
||||
buffer,
|
||||
file_exists,
|
||||
);
|
||||
|
||||
if let Some(unstaged_diff) = &self.secondary_diff {
|
||||
unstaged_diff.update(cx, |diff, _| {
|
||||
diff.inner.pending_hunks = new_pending_hunks;
|
||||
for (offset, pending_hunk) in pending_hunks {
|
||||
diff.inner.pending_hunks.insert(offset, pending_hunk);
|
||||
}
|
||||
});
|
||||
}
|
||||
cx.emit(BufferDiffEvent::HunksStagedOrUnstaged(
|
||||
@@ -986,9 +916,7 @@ impl BufferDiff {
|
||||
}
|
||||
_ => (true, Some(text::Anchor::MIN..text::Anchor::MAX)),
|
||||
};
|
||||
|
||||
let pending_hunks = mem::replace(&mut self.inner.pending_hunks, SumTree::new(buffer));
|
||||
|
||||
let pending_hunks = mem::take(&mut self.inner.pending_hunks);
|
||||
self.inner = new_state;
|
||||
if !base_text_changed {
|
||||
self.inner.pending_hunks = pending_hunks;
|
||||
@@ -1221,21 +1149,21 @@ impl DiffHunkStatus {
|
||||
pub fn deleted_none() -> Self {
|
||||
Self {
|
||||
kind: DiffHunkStatusKind::Deleted,
|
||||
secondary: DiffHunkSecondaryStatus::NoSecondaryHunk,
|
||||
secondary: DiffHunkSecondaryStatus::None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn added_none() -> Self {
|
||||
Self {
|
||||
kind: DiffHunkStatusKind::Added,
|
||||
secondary: DiffHunkSecondaryStatus::NoSecondaryHunk,
|
||||
secondary: DiffHunkSecondaryStatus::None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn modified_none() -> Self {
|
||||
Self {
|
||||
kind: DiffHunkStatusKind::Modified,
|
||||
secondary: DiffHunkSecondaryStatus::NoSecondaryHunk,
|
||||
secondary: DiffHunkSecondaryStatus::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1243,14 +1171,13 @@ impl DiffHunkStatus {
|
||||
/// Range (crossing new lines), old, new
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
#[track_caller]
|
||||
pub fn assert_hunks<ExpectedText, HunkIter>(
|
||||
diff_hunks: HunkIter,
|
||||
pub fn assert_hunks<Iter>(
|
||||
diff_hunks: Iter,
|
||||
buffer: &text::BufferSnapshot,
|
||||
diff_base: &str,
|
||||
expected_hunks: &[(Range<u32>, ExpectedText, ExpectedText, DiffHunkStatus)],
|
||||
expected_hunks: &[(Range<u32>, &str, &str, DiffHunkStatus)],
|
||||
) where
|
||||
HunkIter: Iterator<Item = DiffHunk>,
|
||||
ExpectedText: AsRef<str>,
|
||||
Iter: Iterator<Item = DiffHunk>,
|
||||
{
|
||||
let actual_hunks = diff_hunks
|
||||
.map(|hunk| {
|
||||
@@ -1270,14 +1197,14 @@ pub fn assert_hunks<ExpectedText, HunkIter>(
|
||||
.map(|(r, old_text, new_text, status)| {
|
||||
(
|
||||
Point::new(r.start, 0)..Point::new(r.end, 0),
|
||||
old_text.as_ref(),
|
||||
new_text.as_ref().to_string(),
|
||||
*old_text,
|
||||
new_text.to_string(),
|
||||
*status,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
pretty_assertions::assert_eq!(actual_hunks, expected_hunks);
|
||||
assert_eq!(actual_hunks, expected_hunks);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -1336,7 +1263,7 @@ mod tests {
|
||||
);
|
||||
|
||||
diff = cx.update(|cx| BufferDiff::build_empty(&buffer, cx));
|
||||
assert_hunks::<&str, _>(
|
||||
assert_hunks(
|
||||
diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &buffer, None),
|
||||
&buffer,
|
||||
&diff_base,
|
||||
@@ -1674,10 +1601,7 @@ mod tests {
|
||||
.hunks_intersecting_range(hunk_range.clone(), &buffer, &cx)
|
||||
.collect::<Vec<_>>();
|
||||
for hunk in &hunks {
|
||||
assert_ne!(
|
||||
hunk.secondary_status,
|
||||
DiffHunkSecondaryStatus::NoSecondaryHunk
|
||||
)
|
||||
assert_ne!(hunk.secondary_status, DiffHunkSecondaryStatus::None)
|
||||
}
|
||||
|
||||
let new_index_text = diff
|
||||
@@ -1956,10 +1880,10 @@ mod tests {
|
||||
let hunk_to_change = hunk.clone();
|
||||
let stage = match hunk.secondary_status {
|
||||
DiffHunkSecondaryStatus::HasSecondaryHunk => {
|
||||
hunk.secondary_status = DiffHunkSecondaryStatus::NoSecondaryHunk;
|
||||
hunk.secondary_status = DiffHunkSecondaryStatus::None;
|
||||
true
|
||||
}
|
||||
DiffHunkSecondaryStatus::NoSecondaryHunk => {
|
||||
DiffHunkSecondaryStatus::None => {
|
||||
hunk.secondary_status = DiffHunkSecondaryStatus::HasSecondaryHunk;
|
||||
false
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
//!
|
||||
//! A view for exploring Zed components.
|
||||
|
||||
#![allow(unused, dead_code)]
|
||||
|
||||
use std::iter::Iterator;
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -14,7 +16,7 @@ use gpui::{
|
||||
|
||||
use collections::HashMap;
|
||||
|
||||
use gpui::{ListState, ScrollHandle, UniformListScrollHandle};
|
||||
use gpui::{ListState, UniformListScrollHandle};
|
||||
use languages::LanguageRegistry;
|
||||
use notifications::status_toast::{StatusToast, ToastIcon};
|
||||
use project::Project;
|
||||
@@ -88,7 +90,6 @@ enum PreviewPage {
|
||||
|
||||
struct ComponentPreview {
|
||||
focus_handle: FocusHandle,
|
||||
_view_scroll_handle: ScrollHandle,
|
||||
nav_scroll_handle: UniformListScrollHandle,
|
||||
component_map: HashMap<ComponentId, ComponentMetadata>,
|
||||
active_page: PreviewPage,
|
||||
@@ -130,7 +131,6 @@ impl ComponentPreview {
|
||||
|
||||
let mut component_preview = Self {
|
||||
focus_handle: cx.focus_handle(),
|
||||
_view_scroll_handle: ScrollHandle::new(),
|
||||
nav_scroll_handle: UniformListScrollHandle::new(),
|
||||
language_registry,
|
||||
user_store,
|
||||
@@ -401,10 +401,12 @@ impl ComponentPreview {
|
||||
let component = self.component_map.get(&component_id);
|
||||
|
||||
if let Some(component) = component {
|
||||
v_flex()
|
||||
.w_full()
|
||||
.flex_initial()
|
||||
.min_h_full()
|
||||
div()
|
||||
.id("component-page-container")
|
||||
.flex_1()
|
||||
.size_full()
|
||||
.overflow_x_hidden()
|
||||
.overflow_y_scroll()
|
||||
.child(self.render_preview(component, window, cx))
|
||||
.into_any_element()
|
||||
} else {
|
||||
@@ -412,6 +414,7 @@ impl ComponentPreview {
|
||||
.size_full()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.text_center()
|
||||
.child("Component not found")
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
@@ -271,7 +271,10 @@ mod tests {
|
||||
use gpui::{AppContext as _, BackgroundExecutor, TestAppContext, UpdateGlobal};
|
||||
use indoc::indoc;
|
||||
use language::{
|
||||
language_settings::{AllLanguageSettings, AllLanguageSettingsContent},
|
||||
language_settings::{
|
||||
AllLanguageSettings, AllLanguageSettingsContent, CompletionSettings,
|
||||
WordsCompletionMode,
|
||||
},
|
||||
Point,
|
||||
};
|
||||
use project::Project;
|
||||
@@ -286,7 +289,13 @@ mod tests {
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_copilot(executor: BackgroundExecutor, cx: &mut TestAppContext) {
|
||||
// flaky
|
||||
init_test(cx, |_| {});
|
||||
init_test(cx, |settings| {
|
||||
settings.defaults.completions = Some(CompletionSettings {
|
||||
words: WordsCompletionMode::Disabled,
|
||||
lsp: true,
|
||||
lsp_fetch_timeout_ms: 0,
|
||||
});
|
||||
});
|
||||
|
||||
let (copilot, copilot_lsp) = Copilot::fake(cx);
|
||||
let mut cx = EditorLspTestContext::new_rust(
|
||||
@@ -511,7 +520,13 @@ mod tests {
|
||||
cx: &mut TestAppContext,
|
||||
) {
|
||||
// flaky
|
||||
init_test(cx, |_| {});
|
||||
init_test(cx, |settings| {
|
||||
settings.defaults.completions = Some(CompletionSettings {
|
||||
words: WordsCompletionMode::Disabled,
|
||||
lsp: true,
|
||||
lsp_fetch_timeout_ms: 0,
|
||||
});
|
||||
});
|
||||
|
||||
let (copilot, copilot_lsp) = Copilot::fake(cx);
|
||||
let mut cx = EditorLspTestContext::new_rust(
|
||||
|
||||
@@ -101,6 +101,7 @@ use itertools::Itertools;
|
||||
use language::{
|
||||
language_settings::{
|
||||
self, all_language_settings, language_settings, InlayHintSettings, RewrapBehavior,
|
||||
WordsCompletionMode,
|
||||
},
|
||||
point_from_lsp, text_diff_with_options, AutoindentMode, BracketMatch, BracketPair, Buffer,
|
||||
Capability, CharKind, CodeLabel, CursorShape, Diagnostic, DiffOptions, EditPredictionsMode,
|
||||
@@ -4021,9 +4022,8 @@ impl Editor {
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
let show_completion_documentation = buffer
|
||||
.read(cx)
|
||||
.snapshot()
|
||||
let buffer_snapshot = buffer.read(cx).snapshot();
|
||||
let show_completion_documentation = buffer_snapshot
|
||||
.settings_at(buffer_position, cx)
|
||||
.show_completion_documentation;
|
||||
|
||||
@@ -4047,6 +4047,51 @@ impl Editor {
|
||||
};
|
||||
let completions =
|
||||
provider.completions(&buffer, buffer_position, completion_context, window, cx);
|
||||
let (old_range, word_kind) = buffer_snapshot.surrounding_word(buffer_position);
|
||||
let (old_range, word_to_exclude) = if word_kind == Some(CharKind::Word) {
|
||||
let word_to_exclude = buffer_snapshot
|
||||
.text_for_range(old_range.clone())
|
||||
.collect::<String>();
|
||||
(
|
||||
buffer_snapshot.anchor_before(old_range.start)
|
||||
..buffer_snapshot.anchor_after(old_range.end),
|
||||
Some(word_to_exclude),
|
||||
)
|
||||
} else {
|
||||
(buffer_position..buffer_position, None)
|
||||
};
|
||||
|
||||
let completion_settings = language_settings(
|
||||
buffer_snapshot
|
||||
.language_at(buffer_position)
|
||||
.map(|language| language.name()),
|
||||
buffer_snapshot.file(),
|
||||
cx,
|
||||
)
|
||||
.completions;
|
||||
|
||||
// The document can be large, so stay in reasonable bounds when searching for words,
|
||||
// otherwise completion pop-up might be slow to appear.
|
||||
const WORD_LOOKUP_ROWS: u32 = 5_000;
|
||||
let buffer_row = text::ToPoint::to_point(&buffer_position, &buffer_snapshot).row;
|
||||
let min_word_search = buffer_snapshot.clip_point(
|
||||
Point::new(buffer_row.saturating_sub(WORD_LOOKUP_ROWS), 0),
|
||||
Bias::Left,
|
||||
);
|
||||
let max_word_search = buffer_snapshot.clip_point(
|
||||
Point::new(buffer_row + WORD_LOOKUP_ROWS, 0).min(buffer_snapshot.max_point()),
|
||||
Bias::Right,
|
||||
);
|
||||
let word_search_range = buffer_snapshot.point_to_offset(min_word_search)
|
||||
..buffer_snapshot.point_to_offset(max_word_search);
|
||||
let words = match completion_settings.words {
|
||||
WordsCompletionMode::Disabled => Task::ready(HashMap::default()),
|
||||
WordsCompletionMode::Enabled | WordsCompletionMode::Fallback => {
|
||||
cx.background_spawn(async move {
|
||||
buffer_snapshot.words_in_range(None, word_search_range)
|
||||
})
|
||||
}
|
||||
};
|
||||
let sort_completions = provider.sort_completions();
|
||||
|
||||
let id = post_inc(&mut self.next_completion_id);
|
||||
@@ -4055,8 +4100,55 @@ impl Editor {
|
||||
editor.update(&mut cx, |this, _| {
|
||||
this.completion_tasks.retain(|(task_id, _)| *task_id >= id);
|
||||
})?;
|
||||
let completions = completions.await.log_err();
|
||||
let menu = if let Some(completions) = completions {
|
||||
let mut completions = completions.await.log_err().unwrap_or_default();
|
||||
|
||||
match completion_settings.words {
|
||||
WordsCompletionMode::Enabled => {
|
||||
completions.extend(
|
||||
words
|
||||
.await
|
||||
.into_iter()
|
||||
.filter(|(word, _)| word_to_exclude.as_ref() != Some(word))
|
||||
.map(|(word, word_range)| Completion {
|
||||
old_range: old_range.clone(),
|
||||
new_text: word.clone(),
|
||||
label: CodeLabel::plain(word, None),
|
||||
documentation: None,
|
||||
source: CompletionSource::BufferWord {
|
||||
word_range,
|
||||
resolved: false,
|
||||
},
|
||||
confirm: None,
|
||||
}),
|
||||
);
|
||||
}
|
||||
WordsCompletionMode::Fallback => {
|
||||
if completions.is_empty() {
|
||||
completions.extend(
|
||||
words
|
||||
.await
|
||||
.into_iter()
|
||||
.filter(|(word, _)| word_to_exclude.as_ref() != Some(word))
|
||||
.map(|(word, word_range)| Completion {
|
||||
old_range: old_range.clone(),
|
||||
new_text: word.clone(),
|
||||
label: CodeLabel::plain(word, None),
|
||||
documentation: None,
|
||||
source: CompletionSource::BufferWord {
|
||||
word_range,
|
||||
resolved: false,
|
||||
},
|
||||
confirm: None,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
WordsCompletionMode::Disabled => {}
|
||||
}
|
||||
|
||||
let menu = if completions.is_empty() {
|
||||
None
|
||||
} else {
|
||||
let mut menu = CompletionsMenu::new(
|
||||
id,
|
||||
sort_completions,
|
||||
@@ -4070,8 +4162,6 @@ impl Editor {
|
||||
.await;
|
||||
|
||||
menu.visible().then_some(menu)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
editor.update_in(&mut cx, |editor, window, cx| {
|
||||
|
||||
@@ -16,7 +16,8 @@ use gpui::{
|
||||
use indoc::indoc;
|
||||
use language::{
|
||||
language_settings::{
|
||||
AllLanguageSettings, AllLanguageSettingsContent, LanguageSettingsContent, PrettierSettings,
|
||||
AllLanguageSettings, AllLanguageSettingsContent, CompletionSettings,
|
||||
LanguageSettingsContent, PrettierSettings,
|
||||
},
|
||||
BracketPairConfig,
|
||||
Capability::ReadWrite,
|
||||
@@ -30,7 +31,7 @@ use pretty_assertions::{assert_eq, assert_ne};
|
||||
use project::project_settings::{LspSettings, ProjectSettings};
|
||||
use project::FakeFs;
|
||||
use serde_json::{self, json};
|
||||
use std::{cell::RefCell, future::Future, rc::Rc, time::Instant};
|
||||
use std::{cell::RefCell, future::Future, rc::Rc, sync::atomic::AtomicBool, time::Instant};
|
||||
use std::{
|
||||
iter,
|
||||
sync::atomic::{self, AtomicUsize},
|
||||
@@ -9194,6 +9195,101 @@ async fn test_completion(cx: &mut TestAppContext) {
|
||||
apply_additional_edits.await.unwrap();
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_words_completion(cx: &mut TestAppContext) {
|
||||
let lsp_fetch_timeout_ms = 10;
|
||||
init_test(cx, |language_settings| {
|
||||
language_settings.defaults.completions = Some(CompletionSettings {
|
||||
words: WordsCompletionMode::Fallback,
|
||||
lsp: true,
|
||||
lsp_fetch_timeout_ms: 10,
|
||||
});
|
||||
});
|
||||
|
||||
let mut cx = EditorLspTestContext::new_rust(
|
||||
lsp::ServerCapabilities {
|
||||
completion_provider: Some(lsp::CompletionOptions {
|
||||
trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
|
||||
..lsp::CompletionOptions::default()
|
||||
}),
|
||||
signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
|
||||
..lsp::ServerCapabilities::default()
|
||||
},
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
|
||||
let throttle_completions = Arc::new(AtomicBool::new(false));
|
||||
|
||||
let lsp_throttle_completions = throttle_completions.clone();
|
||||
let _completion_requests_handler =
|
||||
cx.lsp
|
||||
.server
|
||||
.on_request::<lsp::request::Completion, _, _>(move |_, cx| {
|
||||
let lsp_throttle_completions = lsp_throttle_completions.clone();
|
||||
async move {
|
||||
if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
|
||||
cx.background_executor()
|
||||
.timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
|
||||
.await;
|
||||
}
|
||||
Ok(Some(lsp::CompletionResponse::Array(vec![
|
||||
lsp::CompletionItem {
|
||||
label: "first".into(),
|
||||
..Default::default()
|
||||
},
|
||||
lsp::CompletionItem {
|
||||
label: "last".into(),
|
||||
..Default::default()
|
||||
},
|
||||
])))
|
||||
}
|
||||
});
|
||||
|
||||
cx.set_state(indoc! {"
|
||||
oneˇ
|
||||
two
|
||||
three
|
||||
"});
|
||||
cx.simulate_keystroke(".");
|
||||
cx.executor().run_until_parked();
|
||||
cx.condition(|editor, _| editor.context_menu_visible())
|
||||
.await;
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
|
||||
{
|
||||
assert_eq!(
|
||||
completion_menu_entries(&menu),
|
||||
&["first", "last"],
|
||||
"When LSP server is fast to reply, no fallback word completions are used"
|
||||
);
|
||||
} else {
|
||||
panic!("expected completion menu to be open");
|
||||
}
|
||||
editor.cancel(&Cancel, window, cx);
|
||||
});
|
||||
cx.executor().run_until_parked();
|
||||
cx.condition(|editor, _| !editor.context_menu_visible())
|
||||
.await;
|
||||
|
||||
throttle_completions.store(true, atomic::Ordering::Release);
|
||||
cx.simulate_keystroke(".");
|
||||
cx.executor()
|
||||
.advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
|
||||
cx.executor().run_until_parked();
|
||||
cx.condition(|editor, _| editor.context_menu_visible())
|
||||
.await;
|
||||
cx.update_editor(|editor, _, _| {
|
||||
if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
|
||||
{
|
||||
assert_eq!(completion_menu_entries(&menu), &["one", "three", "two"],
|
||||
"When LSP server is slow, document words can be shown instead, if configured accordingly");
|
||||
} else {
|
||||
panic!("expected completion menu to be open");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_multiline_completion(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
@@ -55,7 +55,7 @@ use multi_buffer::{
|
||||
Anchor, ExcerptId, ExcerptInfo, ExpandExcerptDirection, MultiBufferPoint, MultiBufferRow,
|
||||
RowInfo,
|
||||
};
|
||||
use project::project_settings::{self, GitGutterSetting, GitHunkStyleSetting, ProjectSettings};
|
||||
use project::project_settings::{self, GitGutterSetting, ProjectSettings};
|
||||
use settings::Settings;
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
use std::{
|
||||
@@ -4415,7 +4415,7 @@ impl EditorElement {
|
||||
}),
|
||||
};
|
||||
|
||||
if let Some((hunk_bounds, background_color, corner_radii, status)) = hunk_to_paint {
|
||||
if let Some((hunk_bounds, background_color, corner_radii, _)) = hunk_to_paint {
|
||||
// Flatten the background color with the editor color to prevent
|
||||
// elements below transparent hunks from showing through
|
||||
let flattened_background_color = cx
|
||||
@@ -4424,29 +4424,13 @@ impl EditorElement {
|
||||
.editor_background
|
||||
.blend(background_color);
|
||||
|
||||
if !Self::diff_hunk_hollow(status, cx) {
|
||||
window.paint_quad(quad(
|
||||
hunk_bounds,
|
||||
corner_radii,
|
||||
flattened_background_color,
|
||||
Edges::default(),
|
||||
transparent_black(),
|
||||
));
|
||||
} else {
|
||||
let flattened_unstaged_background_color = cx
|
||||
.theme()
|
||||
.colors()
|
||||
.editor_background
|
||||
.blend(background_color.opacity(0.3));
|
||||
|
||||
window.paint_quad(quad(
|
||||
hunk_bounds,
|
||||
corner_radii,
|
||||
flattened_unstaged_background_color,
|
||||
Edges::all(Pixels(1.0)),
|
||||
flattened_background_color,
|
||||
));
|
||||
}
|
||||
window.paint_quad(quad(
|
||||
hunk_bounds,
|
||||
corner_radii,
|
||||
flattened_background_color,
|
||||
Edges::default(),
|
||||
transparent_black(),
|
||||
));
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -5651,18 +5635,6 @@ impl EditorElement {
|
||||
&[run],
|
||||
)
|
||||
}
|
||||
|
||||
fn diff_hunk_hollow(status: DiffHunkStatus, cx: &mut App) -> bool {
|
||||
let unstaged = status.has_secondary_hunk();
|
||||
let unstaged_hollow = ProjectSettings::get_global(cx)
|
||||
.git
|
||||
.hunk_style
|
||||
.map_or(false, |style| {
|
||||
matches!(style, GitHunkStyleSetting::UnstagedHollow)
|
||||
});
|
||||
|
||||
unstaged == unstaged_hollow
|
||||
}
|
||||
}
|
||||
|
||||
fn header_jump_data(
|
||||
@@ -6814,9 +6786,10 @@ impl Element for EditorElement {
|
||||
}
|
||||
};
|
||||
|
||||
let unstaged = diff_status.has_secondary_hunk();
|
||||
let hunk_opacity = if is_light { 0.16 } else { 0.12 };
|
||||
|
||||
let hollow_highlight = LineHighlight {
|
||||
let staged_highlight = LineHighlight {
|
||||
background: (background_color.opacity(if is_light {
|
||||
0.08
|
||||
} else {
|
||||
@@ -6830,13 +6803,13 @@ impl Element for EditorElement {
|
||||
}),
|
||||
};
|
||||
|
||||
let filled_highlight =
|
||||
let unstaged_highlight =
|
||||
solid_background(background_color.opacity(hunk_opacity)).into();
|
||||
|
||||
let background = if Self::diff_hunk_hollow(diff_status, cx) {
|
||||
hollow_highlight
|
||||
let background = if unstaged {
|
||||
unstaged_highlight
|
||||
} else {
|
||||
filled_highlight
|
||||
staged_highlight
|
||||
};
|
||||
|
||||
highlighted_rows
|
||||
|
||||
@@ -488,7 +488,7 @@ async fn parse_commit_messages(
|
||||
},
|
||||
))
|
||||
} else {
|
||||
None
|
||||
continue;
|
||||
};
|
||||
|
||||
let remote = parsed_remote_url
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
pub mod extension_builder;
|
||||
mod extension_events;
|
||||
mod extension_host_proxy;
|
||||
mod extension_manifest;
|
||||
mod types;
|
||||
@@ -14,12 +15,14 @@ use gpui::{App, Task};
|
||||
use language::LanguageName;
|
||||
use semantic_version::SemanticVersion;
|
||||
|
||||
pub use crate::extension_events::*;
|
||||
pub use crate::extension_host_proxy::*;
|
||||
pub use crate::extension_manifest::*;
|
||||
pub use crate::types::*;
|
||||
|
||||
/// Initializes the `extension` crate.
|
||||
pub fn init(cx: &mut App) {
|
||||
extension_events::init(cx);
|
||||
ExtensionHostProxy::default_global(cx);
|
||||
}
|
||||
|
||||
|
||||
35
crates/extension/src/extension_events.rs
Normal file
35
crates/extension/src/extension_events.rs
Normal file
@@ -0,0 +1,35 @@
|
||||
use gpui::{App, AppContext as _, Context, Entity, EventEmitter, Global, ReadGlobal as _};
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
let extension_events = cx.new(ExtensionEvents::new);
|
||||
cx.set_global(GlobalExtensionEvents(extension_events));
|
||||
}
|
||||
|
||||
struct GlobalExtensionEvents(Entity<ExtensionEvents>);
|
||||
|
||||
impl Global for GlobalExtensionEvents {}
|
||||
|
||||
/// An event bus for broadcasting extension-related events throughout the app.
|
||||
pub struct ExtensionEvents;
|
||||
|
||||
impl ExtensionEvents {
|
||||
/// Returns the global [`ExtensionEvents`].
|
||||
pub fn global(cx: &App) -> Entity<Self> {
|
||||
GlobalExtensionEvents::global(cx).0.clone()
|
||||
}
|
||||
|
||||
fn new(_cx: &mut Context<Self>) -> Self {
|
||||
Self
|
||||
}
|
||||
|
||||
pub fn emit(&mut self, event: Event, cx: &mut Context<Self>) {
|
||||
cx.emit(event)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum Event {
|
||||
ExtensionsUpdated,
|
||||
}
|
||||
|
||||
impl EventEmitter<Event> for ExtensionEvents {}
|
||||
@@ -14,7 +14,7 @@ use collections::{btree_map, BTreeMap, BTreeSet, HashMap, HashSet};
|
||||
use extension::extension_builder::{CompileExtensionOptions, ExtensionBuilder};
|
||||
pub use extension::ExtensionManifest;
|
||||
use extension::{
|
||||
ExtensionContextServerProxy, ExtensionGrammarProxy, ExtensionHostProxy,
|
||||
ExtensionContextServerProxy, ExtensionEvents, ExtensionGrammarProxy, ExtensionHostProxy,
|
||||
ExtensionIndexedDocsProviderProxy, ExtensionLanguageProxy, ExtensionLanguageServerProxy,
|
||||
ExtensionSlashCommandProxy, ExtensionSnippetProxy, ExtensionThemeProxy,
|
||||
};
|
||||
@@ -127,7 +127,6 @@ pub enum ExtensionOperation {
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum Event {
|
||||
ExtensionsUpdated,
|
||||
StartedReloading,
|
||||
ExtensionInstalled(Arc<str>),
|
||||
ExtensionFailedToLoad(Arc<str>),
|
||||
@@ -1214,7 +1213,9 @@ impl ExtensionStore {
|
||||
|
||||
self.extension_index = new_index;
|
||||
cx.notify();
|
||||
cx.emit(Event::ExtensionsUpdated);
|
||||
ExtensionEvents::global(cx).update(cx, |this, cx| {
|
||||
this.emit(extension::Event::ExtensionsUpdated, cx)
|
||||
});
|
||||
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
cx.background_spawn({
|
||||
|
||||
@@ -780,6 +780,7 @@ fn init_test(cx: &mut TestAppContext) {
|
||||
let store = SettingsStore::test(cx);
|
||||
cx.set_global(store);
|
||||
release_channel::init(SemanticVersion::default(), cx);
|
||||
extension::init(cx);
|
||||
theme::init(theme::LoadThemes::JustBase, cx);
|
||||
Project::init_settings(cx);
|
||||
ExtensionSettings::register(cx);
|
||||
|
||||
@@ -17,6 +17,7 @@ client.workspace = true
|
||||
collections.workspace = true
|
||||
db.workspace = true
|
||||
editor.workspace = true
|
||||
extension.workspace = true
|
||||
extension_host.workspace = true
|
||||
feature_flags.workspace = true
|
||||
fs.workspace = true
|
||||
|
||||
@@ -9,6 +9,7 @@ use std::{ops::Range, sync::Arc};
|
||||
use client::{ExtensionMetadata, ExtensionProvides};
|
||||
use collections::{BTreeMap, BTreeSet};
|
||||
use editor::{Editor, EditorElement, EditorStyle};
|
||||
use extension::ExtensionEvents;
|
||||
use extension_host::{ExtensionManifest, ExtensionOperation, ExtensionStore};
|
||||
use feature_flags::FeatureFlagAppExt as _;
|
||||
use fuzzy::{match_strings, StringMatchCandidate};
|
||||
@@ -212,7 +213,7 @@ pub struct ExtensionsPage {
|
||||
query_editor: Entity<Editor>,
|
||||
query_contains_error: bool,
|
||||
provides_filter: Option<ExtensionProvides>,
|
||||
_subscriptions: [gpui::Subscription; 2],
|
||||
_subscriptions: Vec<gpui::Subscription>,
|
||||
extension_fetch_task: Option<Task<()>>,
|
||||
upsells: BTreeSet<Feature>,
|
||||
}
|
||||
@@ -226,15 +227,12 @@ impl ExtensionsPage {
|
||||
cx.new(|cx| {
|
||||
let store = ExtensionStore::global(cx);
|
||||
let workspace_handle = workspace.weak_handle();
|
||||
let subscriptions = [
|
||||
let subscriptions = vec![
|
||||
cx.observe(&store, |_: &mut Self, _, cx| cx.notify()),
|
||||
cx.subscribe_in(
|
||||
&store,
|
||||
window,
|
||||
move |this, _, event, window, cx| match event {
|
||||
extension_host::Event::ExtensionsUpdated => {
|
||||
this.fetch_extensions_debounced(cx)
|
||||
}
|
||||
extension_host::Event::ExtensionInstalled(extension_id) => this
|
||||
.on_extension_installed(
|
||||
workspace_handle.clone(),
|
||||
@@ -245,6 +243,15 @@ impl ExtensionsPage {
|
||||
_ => {}
|
||||
},
|
||||
),
|
||||
cx.subscribe_in(
|
||||
&ExtensionEvents::global(cx),
|
||||
window,
|
||||
move |this, _, event, _window, cx| match event {
|
||||
extension::Event::ExtensionsUpdated => {
|
||||
this.fetch_extensions_debounced(cx);
|
||||
}
|
||||
},
|
||||
),
|
||||
];
|
||||
|
||||
let query_editor = cx.new(|cx| {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use futures::channel::oneshot;
|
||||
use fuzzy::StringMatchCandidate;
|
||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use project::DirectoryLister;
|
||||
use std::{
|
||||
@@ -9,7 +9,7 @@ use std::{
|
||||
Arc,
|
||||
},
|
||||
};
|
||||
use ui::{prelude::*, LabelLike, ListItemSpacing};
|
||||
use ui::{prelude::*, HighlightedLabel, ListItemSpacing};
|
||||
use ui::{Context, ListItem, Window};
|
||||
use util::{maybe, paths::compare_paths};
|
||||
use workspace::Workspace;
|
||||
@@ -22,6 +22,7 @@ pub struct OpenPathDelegate {
|
||||
selected_index: usize,
|
||||
directory_state: Option<DirectoryState>,
|
||||
matches: Vec<usize>,
|
||||
string_matches: Vec<StringMatch>,
|
||||
cancel_flag: Arc<AtomicBool>,
|
||||
should_dismiss: bool,
|
||||
}
|
||||
@@ -34,6 +35,7 @@ impl OpenPathDelegate {
|
||||
selected_index: 0,
|
||||
directory_state: None,
|
||||
matches: Vec::new(),
|
||||
string_matches: Vec::new(),
|
||||
cancel_flag: Arc::new(AtomicBool::new(false)),
|
||||
should_dismiss: true,
|
||||
}
|
||||
@@ -223,6 +225,7 @@ impl PickerDelegate for OpenPathDelegate {
|
||||
if suffix == "" {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.delegate.matches.clear();
|
||||
this.delegate.string_matches.clear();
|
||||
this.delegate
|
||||
.matches
|
||||
.extend(match_candidates.iter().map(|m| m.path.id));
|
||||
@@ -249,6 +252,7 @@ impl PickerDelegate for OpenPathDelegate {
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.delegate.matches.clear();
|
||||
this.delegate.string_matches = matches.clone();
|
||||
this.delegate
|
||||
.matches
|
||||
.extend(matches.into_iter().map(|m| m.candidate_id));
|
||||
@@ -337,13 +341,22 @@ impl PickerDelegate for OpenPathDelegate {
|
||||
let m = self.matches.get(ix)?;
|
||||
let directory_state = self.directory_state.as_ref()?;
|
||||
let candidate = directory_state.match_candidates.get(*m)?;
|
||||
let highlight_positions = self
|
||||
.string_matches
|
||||
.iter()
|
||||
.find(|string_match| string_match.candidate_id == *m)
|
||||
.map(|string_match| string_match.positions.clone())
|
||||
.unwrap_or_default();
|
||||
|
||||
Some(
|
||||
ListItem::new(ix)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.inset(true)
|
||||
.toggle_state(selected)
|
||||
.child(LabelLike::new().child(candidate.path.string.clone())),
|
||||
.child(HighlightedLabel::new(
|
||||
candidate.path.string.clone(),
|
||||
highlight_positions,
|
||||
)),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -795,8 +795,9 @@ impl GitRepository for RealGitRepository {
|
||||
cx: AsyncApp,
|
||||
) -> BoxFuture<Result<()>> {
|
||||
let working_directory = self.working_directory();
|
||||
let git_binary_path = self.git_binary_path.clone();
|
||||
cx.background_spawn(async move {
|
||||
let mut cmd = new_smol_command("git");
|
||||
let mut cmd = new_smol_command(&git_binary_path);
|
||||
cmd.current_dir(&working_directory?)
|
||||
.envs(env)
|
||||
.args(["commit", "--quiet", "-m"])
|
||||
|
||||
@@ -146,7 +146,7 @@ impl CommitModal {
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let panel = git_panel.read(cx);
|
||||
let suggested_commit_message = panel.suggest_commit_message(cx);
|
||||
let suggested_commit_message = panel.suggest_commit_message();
|
||||
|
||||
let commit_editor = git_panel.update(cx, |git_panel, cx| {
|
||||
git_panel.set_modal_open(true, cx);
|
||||
|
||||
@@ -1413,7 +1413,7 @@ impl GitPanel {
|
||||
return Some(message.to_string());
|
||||
}
|
||||
|
||||
self.suggest_commit_message(cx)
|
||||
self.suggest_commit_message()
|
||||
.filter(|message| !message.trim().is_empty())
|
||||
}
|
||||
|
||||
@@ -1582,15 +1582,7 @@ impl GitPanel {
|
||||
}
|
||||
|
||||
/// Suggests a commit message based on the changed files and their statuses
|
||||
pub fn suggest_commit_message(&self, cx: &App) -> Option<String> {
|
||||
if let Some(merge_message) = self
|
||||
.active_repository
|
||||
.as_ref()
|
||||
.and_then(|repo| repo.read(cx).merge_message.as_ref())
|
||||
{
|
||||
return Some(merge_message.clone());
|
||||
}
|
||||
|
||||
pub fn suggest_commit_message(&self) -> Option<String> {
|
||||
let git_status_entry = if let Some(staged_entry) = &self.single_staged_entry {
|
||||
Some(staged_entry)
|
||||
} else if let Some(single_tracked_entry) = &self.single_tracked_entry {
|
||||
@@ -1726,6 +1718,19 @@ impl GitPanel {
|
||||
}));
|
||||
}
|
||||
|
||||
fn update_editor_placeholder(&mut self, cx: &mut Context<Self>) {
|
||||
let suggested_commit_message = self.suggest_commit_message();
|
||||
let placeholder_text = suggested_commit_message
|
||||
.as_deref()
|
||||
.unwrap_or("Enter commit message");
|
||||
|
||||
self.commit_editor.update(cx, |editor, cx| {
|
||||
editor.set_placeholder_text(Arc::from(placeholder_text), cx)
|
||||
});
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub(crate) fn fetch(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if !self.can_push_and_pull(cx) {
|
||||
return;
|
||||
@@ -2182,6 +2187,7 @@ impl GitPanel {
|
||||
git_panel.clear_pending();
|
||||
}
|
||||
git_panel.update_visible_entries(cx);
|
||||
git_panel.update_editor_placeholder(cx);
|
||||
git_panel.update_scrollbar_properties(window, cx);
|
||||
})
|
||||
.ok();
|
||||
@@ -2217,7 +2223,7 @@ impl GitPanel {
|
||||
git_panel.commit_editor = cx.new(|cx| {
|
||||
commit_message_editor(
|
||||
buffer,
|
||||
git_panel.suggest_commit_message(cx).as_deref(),
|
||||
git_panel.suggest_commit_message().as_deref(),
|
||||
git_panel.project.clone(),
|
||||
true,
|
||||
window,
|
||||
@@ -2237,15 +2243,7 @@ impl GitPanel {
|
||||
fn update_visible_entries(&mut self, cx: &mut Context<Self>) {
|
||||
self.entries.clear();
|
||||
self.single_staged_entry.take();
|
||||
self.single_tracked_entry.take();
|
||||
self.conflicted_count = 0;
|
||||
self.conflicted_staged_count = 0;
|
||||
self.new_count = 0;
|
||||
self.tracked_count = 0;
|
||||
self.new_staged_count = 0;
|
||||
self.tracked_staged_count = 0;
|
||||
self.entry_count = 0;
|
||||
|
||||
self.single_staged_entry.take();
|
||||
let mut changed_entries = Vec::new();
|
||||
let mut new_entries = Vec::new();
|
||||
let mut conflict_entries = Vec::new();
|
||||
@@ -2397,15 +2395,6 @@ impl GitPanel {
|
||||
|
||||
self.select_first_entry_if_none(cx);
|
||||
|
||||
let suggested_commit_message = self.suggest_commit_message(cx);
|
||||
let placeholder_text = suggested_commit_message
|
||||
.as_deref()
|
||||
.unwrap_or("Enter commit message");
|
||||
|
||||
self.commit_editor.update(cx, |editor, cx| {
|
||||
editor.set_placeholder_text(Arc::from(placeholder_text), cx)
|
||||
});
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
@@ -2929,10 +2918,6 @@ impl GitPanel {
|
||||
.disabled(!can_commit || self.modal_open)
|
||||
.on_click({
|
||||
cx.listener(move |this, _: &ClickEvent, window, cx| {
|
||||
telemetry::event!(
|
||||
"Git Committed",
|
||||
source = "Git Panel"
|
||||
);
|
||||
this.commit_changes(window, cx)
|
||||
})
|
||||
}),
|
||||
@@ -3063,24 +3048,21 @@ impl GitPanel {
|
||||
"No Git repositories"
|
||||
},
|
||||
))
|
||||
.children({
|
||||
let worktree_count = self.project.read(cx).visible_worktrees(cx).count();
|
||||
(worktree_count > 0 && self.active_repository.is_none()).then(|| {
|
||||
h_flex().w_full().justify_around().child(
|
||||
panel_filled_button("Initialize Repository")
|
||||
.tooltip(Tooltip::for_action_title_in(
|
||||
"git init",
|
||||
&git::Init,
|
||||
&self.focus_handle,
|
||||
))
|
||||
.on_click(move |_, _, cx| {
|
||||
cx.defer(move |cx| {
|
||||
cx.dispatch_action(&git::Init);
|
||||
})
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
.children(self.active_repository.is_none().then(|| {
|
||||
h_flex().w_full().justify_around().child(
|
||||
panel_filled_button("Initialize Repository")
|
||||
.tooltip(Tooltip::for_action_title_in(
|
||||
"git init",
|
||||
&git::Init,
|
||||
&self.focus_handle,
|
||||
))
|
||||
.on_click(move |_, _, cx| {
|
||||
cx.defer(move |cx| {
|
||||
cx.dispatch_action(&git::Init);
|
||||
})
|
||||
}),
|
||||
)
|
||||
}))
|
||||
.text_ui_sm(cx)
|
||||
.mx_auto()
|
||||
.text_color(Color::Placeholder.color(cx)),
|
||||
|
||||
@@ -278,7 +278,7 @@ impl ProjectDiff {
|
||||
has_staged_hunks = true;
|
||||
has_unstaged_hunks = true;
|
||||
}
|
||||
DiffHunkSecondaryStatus::NoSecondaryHunk
|
||||
DiffHunkSecondaryStatus::None
|
||||
| DiffHunkSecondaryStatus::SecondaryHunkRemovalPending => {
|
||||
has_staged_hunks = true;
|
||||
}
|
||||
@@ -308,7 +308,7 @@ impl ProjectDiff {
|
||||
|
||||
fn handle_editor_event(
|
||||
&mut self,
|
||||
editor: &Entity<Editor>,
|
||||
_: &Entity<Editor>,
|
||||
event: &EditorEvent,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
@@ -330,11 +330,6 @@ impl ProjectDiff {
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
if editor.focus_handle(cx).contains_focused(window, cx) {
|
||||
if self.multibuffer.read(cx).is_empty() {
|
||||
self.focus_handle.focus(window)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn load_buffers(&mut self, cx: &mut Context<Self>) -> Vec<Task<Result<DiffBuffer>>> {
|
||||
@@ -924,6 +919,12 @@ impl Render for ProjectDiffToolbar {
|
||||
&StageAndNext,
|
||||
&focus_handle,
|
||||
))
|
||||
// don't actually disable the button so it's mashable
|
||||
.color(if button_states.stage {
|
||||
Color::Default
|
||||
} else {
|
||||
Color::Disabled
|
||||
})
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.dispatch_action(&StageAndNext, window, cx)
|
||||
})),
|
||||
@@ -935,6 +936,11 @@ impl Render for ProjectDiffToolbar {
|
||||
&UnstageAndNext,
|
||||
&focus_handle,
|
||||
))
|
||||
.color(if button_states.unstage {
|
||||
Color::Default
|
||||
} else {
|
||||
Color::Disabled
|
||||
})
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.dispatch_action(&UnstageAndNext, window, cx)
|
||||
})),
|
||||
|
||||
@@ -1948,6 +1948,14 @@ impl Interactivity {
|
||||
if pending_mouse_down.is_some() && hitbox.is_hovered(window) {
|
||||
captured_mouse_down = pending_mouse_down.take();
|
||||
window.refresh();
|
||||
} else if pending_mouse_down.is_some() {
|
||||
// Clear the pending mouse down event (without firing click handlers)
|
||||
// if the hitbox is not being hovered.
|
||||
// This avoids dragging elements that changed their position
|
||||
// immediately after being clicked.
|
||||
// See https://github.com/zed-industries/zed/issues/24600 for more details
|
||||
pending_mouse_down.take();
|
||||
window.refresh();
|
||||
}
|
||||
}
|
||||
// Fire click handlers during the bubble phase.
|
||||
|
||||
@@ -92,6 +92,8 @@ extern "C" {
|
||||
|
||||
#[ctor]
|
||||
unsafe fn build_classes() {
|
||||
let _: () = msg_send![class!(NSWindow), setAllowsAutomaticWindowTabbing: NO];
|
||||
|
||||
WINDOW_CLASS = build_window_class("GPUIWindow", class!(NSWindow));
|
||||
PANEL_CLASS = build_window_class("GPUIPanel", class!(NSPanel));
|
||||
VIEW_CLASS = {
|
||||
|
||||
@@ -4145,6 +4145,63 @@ impl BufferSnapshot {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn words_in_range(
|
||||
&self,
|
||||
query: Option<&str>,
|
||||
range: Range<usize>,
|
||||
) -> HashMap<String, Range<Anchor>> {
|
||||
if query.map_or(false, |query| query.is_empty()) {
|
||||
return HashMap::default();
|
||||
}
|
||||
|
||||
let classifier = CharClassifier::new(self.language.clone().map(|language| LanguageScope {
|
||||
language,
|
||||
override_id: None,
|
||||
}));
|
||||
|
||||
let mut query_ix = 0;
|
||||
let query = query.map(|query| query.chars().collect::<Vec<_>>());
|
||||
let query_len = query.as_ref().map_or(0, |query| query.len());
|
||||
|
||||
let mut words = HashMap::default();
|
||||
let mut current_word_start_ix = None;
|
||||
let mut chunk_ix = range.start;
|
||||
for chunk in self.chunks(range, false) {
|
||||
for (i, c) in chunk.text.char_indices() {
|
||||
let ix = chunk_ix + i;
|
||||
if classifier.is_word(c) {
|
||||
if current_word_start_ix.is_none() {
|
||||
current_word_start_ix = Some(ix);
|
||||
}
|
||||
|
||||
if let Some(query) = &query {
|
||||
if query_ix < query_len {
|
||||
let query_c = query.get(query_ix).expect(
|
||||
"query_ix is a vec of chars, which we access only if before the end",
|
||||
);
|
||||
if c.to_lowercase().eq(query_c.to_lowercase()) {
|
||||
query_ix += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
continue;
|
||||
} else if let Some(word_start) = current_word_start_ix.take() {
|
||||
if query_ix == query_len {
|
||||
let word_range = self.anchor_before(word_start)..self.anchor_after(ix);
|
||||
words.insert(
|
||||
self.text_for_range(word_start..ix).collect::<String>(),
|
||||
word_range,
|
||||
);
|
||||
}
|
||||
}
|
||||
query_ix = 0;
|
||||
}
|
||||
chunk_ix += chunk.text.len();
|
||||
}
|
||||
|
||||
words
|
||||
}
|
||||
}
|
||||
|
||||
fn indent_size_for_line(text: &text::BufferSnapshot, row: u32) -> IndentSize {
|
||||
|
||||
@@ -13,6 +13,7 @@ use proto::deserialize_operation;
|
||||
use rand::prelude::*;
|
||||
use regex::RegexBuilder;
|
||||
use settings::SettingsStore;
|
||||
use std::collections::BTreeSet;
|
||||
use std::{
|
||||
env,
|
||||
ops::Range,
|
||||
@@ -3140,6 +3141,93 @@ fn test_trailing_whitespace_ranges(mut rng: StdRng) {
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_words_in_range(cx: &mut gpui::App) {
|
||||
init_settings(cx, |_| {});
|
||||
|
||||
let contents = r#"let word=öäpple.bar你 Öäpple word2-öÄpPlE-Pizza-word ÖÄPPLE word"#;
|
||||
|
||||
let buffer = cx.new(|cx| {
|
||||
let buffer = Buffer::local(contents, cx).with_language(Arc::new(rust_lang()), cx);
|
||||
assert_eq!(buffer.text(), contents);
|
||||
buffer.check_invariants();
|
||||
buffer
|
||||
});
|
||||
|
||||
buffer.update(cx, |buffer, _| {
|
||||
let snapshot = buffer.snapshot();
|
||||
assert_eq!(
|
||||
BTreeSet::from_iter(["Pizza".to_string()]),
|
||||
snapshot
|
||||
.words_in_range(Some("piz"), 0..snapshot.len())
|
||||
.into_keys()
|
||||
.collect::<BTreeSet<_>>()
|
||||
);
|
||||
assert_eq!(
|
||||
BTreeSet::from_iter([
|
||||
"öäpple".to_string(),
|
||||
"Öäpple".to_string(),
|
||||
"öÄpPlE".to_string(),
|
||||
"ÖÄPPLE".to_string(),
|
||||
]),
|
||||
snapshot
|
||||
.words_in_range(Some("öp"), 0..snapshot.len())
|
||||
.into_keys()
|
||||
.collect::<BTreeSet<_>>()
|
||||
);
|
||||
assert_eq!(
|
||||
BTreeSet::from_iter([
|
||||
"öÄpPlE".to_string(),
|
||||
"Öäpple".to_string(),
|
||||
"ÖÄPPLE".to_string(),
|
||||
"öäpple".to_string(),
|
||||
]),
|
||||
snapshot
|
||||
.words_in_range(Some("öÄ"), 0..snapshot.len())
|
||||
.into_keys()
|
||||
.collect::<BTreeSet<_>>()
|
||||
);
|
||||
assert_eq!(
|
||||
BTreeSet::default(),
|
||||
snapshot
|
||||
.words_in_range(Some("öÄ好"), 0..snapshot.len())
|
||||
.into_keys()
|
||||
.collect::<BTreeSet<_>>()
|
||||
);
|
||||
assert_eq!(
|
||||
BTreeSet::from_iter(["bar你".to_string(),]),
|
||||
snapshot
|
||||
.words_in_range(Some("你"), 0..snapshot.len())
|
||||
.into_keys()
|
||||
.collect::<BTreeSet<_>>()
|
||||
);
|
||||
assert_eq!(
|
||||
BTreeSet::default(),
|
||||
snapshot
|
||||
.words_in_range(Some(""), 0..snapshot.len())
|
||||
.into_keys()
|
||||
.collect::<BTreeSet<_>>()
|
||||
);
|
||||
assert_eq!(
|
||||
BTreeSet::from_iter([
|
||||
"bar你".to_string(),
|
||||
"öÄpPlE".to_string(),
|
||||
"Öäpple".to_string(),
|
||||
"ÖÄPPLE".to_string(),
|
||||
"öäpple".to_string(),
|
||||
"let".to_string(),
|
||||
"Pizza".to_string(),
|
||||
"word".to_string(),
|
||||
"word2".to_string(),
|
||||
]),
|
||||
snapshot
|
||||
.words_in_range(None, 0..snapshot.len())
|
||||
.into_keys()
|
||||
.collect::<BTreeSet<_>>()
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
fn ruby_lang() -> Language {
|
||||
Language::new(
|
||||
LanguageConfig {
|
||||
|
||||
@@ -79,10 +79,10 @@ pub struct LanguageSettings {
|
||||
/// The column at which to soft-wrap lines, for buffers where soft-wrap
|
||||
/// is enabled.
|
||||
pub preferred_line_length: u32,
|
||||
// Whether to show wrap guides (vertical rulers) in the editor.
|
||||
// Setting this to true will show a guide at the 'preferred_line_length' value
|
||||
// if softwrap is set to 'preferred_line_length', and will show any
|
||||
// additional guides as specified by the 'wrap_guides' setting.
|
||||
/// Whether to show wrap guides (vertical rulers) in the editor.
|
||||
/// Setting this to true will show a guide at the 'preferred_line_length' value
|
||||
/// if softwrap is set to 'preferred_line_length', and will show any
|
||||
/// additional guides as specified by the 'wrap_guides' setting.
|
||||
pub show_wrap_guides: bool,
|
||||
/// Character counts at which to show wrap guides (vertical rulers) in the editor.
|
||||
pub wrap_guides: Vec<usize>,
|
||||
@@ -137,7 +137,7 @@ pub struct LanguageSettings {
|
||||
pub use_on_type_format: bool,
|
||||
/// Whether indentation of pasted content should be adjusted based on the context.
|
||||
pub auto_indent_on_paste: bool,
|
||||
// Controls how the editor handles the autoclosed characters.
|
||||
/// Controls how the editor handles the autoclosed characters.
|
||||
pub always_treat_brackets_as_autoclosed: bool,
|
||||
/// Which code actions to run on save
|
||||
pub code_actions_on_format: HashMap<String, bool>,
|
||||
@@ -151,6 +151,8 @@ pub struct LanguageSettings {
|
||||
/// Whether to display inline and alongside documentation for items in the
|
||||
/// completions menu.
|
||||
pub show_completion_documentation: bool,
|
||||
/// Completion settings for this language.
|
||||
pub completions: CompletionSettings,
|
||||
}
|
||||
|
||||
impl LanguageSettings {
|
||||
@@ -306,6 +308,50 @@ pub struct AllLanguageSettingsContent {
|
||||
pub file_types: HashMap<Arc<str>, Vec<String>>,
|
||||
}
|
||||
|
||||
/// Controls how completions are processed for this language.
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub struct CompletionSettings {
|
||||
/// Controls how words are completed.
|
||||
/// For large documents, not all words may be fetched for completion.
|
||||
///
|
||||
/// Default: `fallback`
|
||||
#[serde(default = "default_words_completion_mode")]
|
||||
pub words: WordsCompletionMode,
|
||||
/// Whether to fetch LSP completions or not.
|
||||
///
|
||||
/// Default: true
|
||||
#[serde(default = "default_true")]
|
||||
pub lsp: bool,
|
||||
/// When fetching LSP completions, determines how long to wait for a response of a particular server.
|
||||
/// When set to 0, waits indefinitely.
|
||||
///
|
||||
/// Default: 500
|
||||
#[serde(default = "lsp_fetch_timeout_ms")]
|
||||
pub lsp_fetch_timeout_ms: u64,
|
||||
}
|
||||
|
||||
/// Controls how document's words are completed.
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum WordsCompletionMode {
|
||||
/// Always fetch document's words for completions.
|
||||
Enabled,
|
||||
/// Only if LSP response errors/times out/is empty,
|
||||
/// use document's words to show completions.
|
||||
Fallback,
|
||||
/// Never fetch or complete document's words for completions.
|
||||
Disabled,
|
||||
}
|
||||
|
||||
fn default_words_completion_mode() -> WordsCompletionMode {
|
||||
WordsCompletionMode::Fallback
|
||||
}
|
||||
|
||||
fn lsp_fetch_timeout_ms() -> u64 {
|
||||
500
|
||||
}
|
||||
|
||||
/// The settings for a particular language.
|
||||
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct LanguageSettingsContent {
|
||||
@@ -478,6 +524,8 @@ pub struct LanguageSettingsContent {
|
||||
///
|
||||
/// Default: true
|
||||
pub show_completion_documentation: Option<bool>,
|
||||
/// Controls how completions are processed for this language.
|
||||
pub completions: Option<CompletionSettings>,
|
||||
}
|
||||
|
||||
/// The behavior of `editor::Rewrap`.
|
||||
@@ -1381,6 +1429,7 @@ fn merge_settings(settings: &mut LanguageSettings, src: &LanguageSettingsContent
|
||||
&mut settings.show_completion_documentation,
|
||||
src.show_completion_documentation,
|
||||
);
|
||||
merge(&mut settings.completions, src.completions);
|
||||
}
|
||||
|
||||
/// Allows to enable/disable formatting with Prettier
|
||||
|
||||
@@ -2007,17 +2007,22 @@ impl MultiBuffer {
|
||||
cx: &App,
|
||||
) -> Option<(Entity<Buffer>, Point, ExcerptId)> {
|
||||
let snapshot = self.read(cx);
|
||||
let (buffer, point, is_main_buffer) =
|
||||
snapshot.point_to_buffer_point(point.to_point(&snapshot))?;
|
||||
Some((
|
||||
self.buffers
|
||||
.borrow()
|
||||
.get(&buffer.remote_id())?
|
||||
let point = point.to_point(&snapshot);
|
||||
let mut cursor = snapshot.cursor::<Point>();
|
||||
cursor.seek(&point);
|
||||
|
||||
cursor.region().and_then(|region| {
|
||||
if !region.is_main_buffer {
|
||||
return None;
|
||||
}
|
||||
|
||||
let overshoot = point - region.range.start;
|
||||
let buffer_point = region.buffer_range.start + overshoot;
|
||||
let buffer = self.buffers.borrow()[®ion.buffer.remote_id()]
|
||||
.buffer
|
||||
.clone(),
|
||||
point,
|
||||
is_main_buffer,
|
||||
))
|
||||
.clone();
|
||||
Some((buffer, buffer_point, region.excerpt.id))
|
||||
})
|
||||
}
|
||||
|
||||
pub fn buffer_point_to_anchor(
|
||||
@@ -4171,36 +4176,22 @@ impl MultiBufferSnapshot {
|
||||
let region = cursor.region()?;
|
||||
let overshoot = offset - region.range.start;
|
||||
let buffer_offset = region.buffer_range.start + overshoot;
|
||||
if buffer_offset == region.buffer.len() + 1
|
||||
&& region.has_trailing_newline
|
||||
&& !region.is_main_buffer
|
||||
{
|
||||
return Some((&cursor.excerpt()?.buffer, cursor.main_buffer_position()?));
|
||||
} else if buffer_offset > region.buffer.len() {
|
||||
if buffer_offset > region.buffer.len() {
|
||||
return None;
|
||||
}
|
||||
Some((region.buffer, buffer_offset))
|
||||
}
|
||||
|
||||
pub fn point_to_buffer_point(
|
||||
&self,
|
||||
point: Point,
|
||||
) -> Option<(&BufferSnapshot, Point, ExcerptId)> {
|
||||
pub fn point_to_buffer_point(&self, point: Point) -> Option<(&BufferSnapshot, Point, bool)> {
|
||||
let mut cursor = self.cursor::<Point>();
|
||||
cursor.seek(&point);
|
||||
let region = cursor.region()?;
|
||||
let overshoot = point - region.range.start;
|
||||
let buffer_point = region.buffer_range.start + overshoot;
|
||||
let excerpt = cursor.excerpt()?;
|
||||
if buffer_point == region.buffer.max_point() + Point::new(1, 0)
|
||||
&& region.has_trailing_newline
|
||||
&& !region.is_main_buffer
|
||||
{
|
||||
return Some((&excerpt.buffer, cursor.main_buffer_position()?, excerpt.id));
|
||||
} else if buffer_point > region.buffer.max_point() {
|
||||
if buffer_point > region.buffer.max_point() {
|
||||
return None;
|
||||
}
|
||||
Some((region.buffer, buffer_point, excerpt.id))
|
||||
Some((region.buffer, buffer_point, region.is_main_buffer))
|
||||
}
|
||||
|
||||
pub fn suggested_indents(
|
||||
@@ -4742,9 +4733,6 @@ impl MultiBufferSnapshot {
|
||||
.buffer
|
||||
.text_summary_for_range(region.buffer_range.start.key..buffer_point),
|
||||
);
|
||||
if point == region.range.end.key && region.has_trailing_newline {
|
||||
position.add_assign(&D::from_text_summary(&TextSummary::newline()));
|
||||
}
|
||||
return Some(position);
|
||||
} else {
|
||||
return Some(D::from_text_summary(&self.text_summary()));
|
||||
|
||||
@@ -3121,100 +3121,6 @@ fn test_summaries_for_anchors(cx: &mut TestAppContext) {
|
||||
assert_eq!(point_2, Point::new(3, 0));
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_trailing_deletion_without_newline(cx: &mut TestAppContext) {
|
||||
let base_text_1 = "one\ntwo".to_owned();
|
||||
let text_1 = "one\n".to_owned();
|
||||
|
||||
let buffer_1 = cx.new(|cx| Buffer::local(text_1, cx));
|
||||
let diff_1 = cx.new(|cx| BufferDiff::new_with_base_text(&base_text_1, &buffer_1, cx));
|
||||
cx.run_until_parked();
|
||||
|
||||
let multibuffer = cx.new(|cx| {
|
||||
let mut multibuffer = MultiBuffer::singleton(buffer_1.clone(), cx);
|
||||
multibuffer.add_diff(diff_1.clone(), cx);
|
||||
multibuffer.expand_diff_hunks(vec![Anchor::min()..Anchor::max()], cx);
|
||||
multibuffer
|
||||
});
|
||||
|
||||
let (mut snapshot, mut subscription) = multibuffer.update(cx, |multibuffer, cx| {
|
||||
(multibuffer.snapshot(cx), multibuffer.subscribe())
|
||||
});
|
||||
|
||||
assert_new_snapshot(
|
||||
&multibuffer,
|
||||
&mut snapshot,
|
||||
&mut subscription,
|
||||
cx,
|
||||
indoc!(
|
||||
"
|
||||
one
|
||||
- two
|
||||
"
|
||||
),
|
||||
);
|
||||
|
||||
assert_eq!(snapshot.max_point(), Point::new(2, 0));
|
||||
assert_eq!(snapshot.len(), 8);
|
||||
|
||||
assert_eq!(
|
||||
snapshot
|
||||
.dimensions_from_points::<Point>([Point::new(2, 0)])
|
||||
.collect::<Vec<_>>(),
|
||||
vec![Point::new(2, 0)]
|
||||
);
|
||||
|
||||
let (_, translated_offset) = snapshot.point_to_buffer_offset(Point::new(2, 0)).unwrap();
|
||||
assert_eq!(translated_offset, "one\n".len());
|
||||
let (_, translated_point, _) = snapshot.point_to_buffer_point(Point::new(2, 0)).unwrap();
|
||||
assert_eq!(translated_point, Point::new(1, 0));
|
||||
|
||||
// The same, for an excerpt that's not at the end of the multibuffer.
|
||||
|
||||
let text_2 = "foo\n".to_owned();
|
||||
let buffer_2 = cx.new(|cx| Buffer::local(&text_2, cx));
|
||||
multibuffer.update(cx, |multibuffer, cx| {
|
||||
multibuffer.push_excerpts(
|
||||
buffer_2.clone(),
|
||||
[ExcerptRange {
|
||||
context: Point::new(0, 0)..Point::new(1, 0),
|
||||
primary: None,
|
||||
}],
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
assert_new_snapshot(
|
||||
&multibuffer,
|
||||
&mut snapshot,
|
||||
&mut subscription,
|
||||
cx,
|
||||
indoc!(
|
||||
"
|
||||
one
|
||||
- two
|
||||
|
||||
foo
|
||||
"
|
||||
),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
snapshot
|
||||
.dimensions_from_points::<Point>([Point::new(2, 0)])
|
||||
.collect::<Vec<_>>(),
|
||||
vec![Point::new(2, 0)]
|
||||
);
|
||||
|
||||
let buffer_1_id = buffer_1.read_with(cx, |buffer_1, _| buffer_1.remote_id());
|
||||
let (buffer, translated_offset) = snapshot.point_to_buffer_offset(Point::new(2, 0)).unwrap();
|
||||
assert_eq!(buffer.remote_id(), buffer_1_id);
|
||||
assert_eq!(translated_offset, "one\n".len());
|
||||
let (buffer, translated_point, _) = snapshot.point_to_buffer_point(Point::new(2, 0)).unwrap();
|
||||
assert_eq!(buffer.remote_id(), buffer_1_id);
|
||||
assert_eq!(translated_point, Point::new(1, 0));
|
||||
}
|
||||
|
||||
fn format_diff(
|
||||
text: &str,
|
||||
row_infos: &Vec<RowInfo>,
|
||||
@@ -3473,12 +3379,16 @@ fn assert_position_translation(snapshot: &MultiBufferSnapshot) {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some((buffer, offset)) = snapshot.point_to_buffer_offset(snapshot.max_point()) {
|
||||
assert!(offset <= buffer.len());
|
||||
}
|
||||
if let Some((buffer, point, _)) = snapshot.point_to_buffer_point(snapshot.max_point()) {
|
||||
assert!(point <= buffer.max_point());
|
||||
}
|
||||
let point = snapshot.max_point();
|
||||
let Some((buffer, offset)) = snapshot.point_to_buffer_offset(point) else {
|
||||
return;
|
||||
};
|
||||
assert!(offset <= buffer.len(),);
|
||||
|
||||
let Some((buffer, point, _)) = snapshot.point_to_buffer_point(point) else {
|
||||
return;
|
||||
};
|
||||
assert!(point <= buffer.max_point(),);
|
||||
}
|
||||
|
||||
fn assert_line_indents(snapshot: &MultiBufferSnapshot) {
|
||||
|
||||
@@ -318,9 +318,20 @@ impl GitStore {
|
||||
}
|
||||
// Update the statuses and merge message but keep everything else.
|
||||
let existing_handle = handle.clone();
|
||||
existing_handle.update(cx, |existing_handle, _| {
|
||||
existing_handle.update(cx, |existing_handle, cx| {
|
||||
existing_handle.repository_entry = repo.clone();
|
||||
if matches!(git_repo, GitRepo::Local { .. }) {
|
||||
if matches!(git_repo, GitRepo::Local { .. })
|
||||
&& existing_handle.merge_message != merge_message
|
||||
{
|
||||
if let (Some(merge_message), Some(buffer)) =
|
||||
(&merge_message, &existing_handle.commit_message_buffer)
|
||||
{
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
if buffer.is_empty() {
|
||||
buffer.set_text(merge_message.as_str(), cx);
|
||||
}
|
||||
})
|
||||
}
|
||||
existing_handle.merge_message = merge_message;
|
||||
}
|
||||
});
|
||||
@@ -1246,6 +1257,7 @@ impl Repository {
|
||||
buffer_store: Entity<BufferStore>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<Entity<Buffer>>> {
|
||||
let merge_message = self.merge_message.clone();
|
||||
cx.spawn(|repository, mut cx| async move {
|
||||
let buffer = buffer_store
|
||||
.update(&mut cx, |buffer_store, cx| buffer_store.create_buffer(cx))?
|
||||
@@ -1258,6 +1270,12 @@ impl Repository {
|
||||
})?;
|
||||
}
|
||||
|
||||
if let Some(merge_message) = merge_message {
|
||||
buffer.update(&mut cx, |buffer, cx| {
|
||||
buffer.set_text(merge_message.as_str(), cx)
|
||||
})?;
|
||||
}
|
||||
|
||||
repository.update(&mut cx, |repository, _| {
|
||||
repository.commit_message_buffer = Some(buffer.clone());
|
||||
})?;
|
||||
|
||||
@@ -23,13 +23,13 @@ use client::{proto, TypedEnvelope};
|
||||
use collections::{btree_map, BTreeMap, BTreeSet, HashMap, HashSet};
|
||||
use futures::{
|
||||
future::{join_all, Shared},
|
||||
select,
|
||||
select, select_biased,
|
||||
stream::FuturesUnordered,
|
||||
AsyncWriteExt, Future, FutureExt, StreamExt,
|
||||
};
|
||||
use globset::{Glob, GlobBuilder, GlobMatcher, GlobSet, GlobSetBuilder};
|
||||
use gpui::{
|
||||
App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, PromptLevel, SharedString, Task,
|
||||
App, AppContext, AsyncApp, Context, Entity, EventEmitter, PromptLevel, SharedString, Task,
|
||||
WeakEntity,
|
||||
};
|
||||
use http_client::HttpClient;
|
||||
@@ -4325,6 +4325,15 @@ impl LspStore {
|
||||
let offset = position.to_offset(&snapshot);
|
||||
let scope = snapshot.language_scope_at(offset);
|
||||
let language = snapshot.language().cloned();
|
||||
let completion_settings = language_settings(
|
||||
language.as_ref().map(|language| language.name()),
|
||||
buffer.read(cx).file(),
|
||||
cx,
|
||||
)
|
||||
.completions;
|
||||
if !completion_settings.lsp {
|
||||
return Task::ready(Ok(Vec::new()));
|
||||
}
|
||||
|
||||
let server_ids: Vec<_> = buffer.update(cx, |buffer, cx| {
|
||||
local
|
||||
@@ -4341,23 +4350,51 @@ impl LspStore {
|
||||
});
|
||||
|
||||
let buffer = buffer.clone();
|
||||
let lsp_timeout = completion_settings.lsp_fetch_timeout_ms;
|
||||
let lsp_timeout = if lsp_timeout > 0 {
|
||||
Some(Duration::from_millis(lsp_timeout))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
cx.spawn(move |this, mut cx| async move {
|
||||
let mut tasks = Vec::with_capacity(server_ids.len());
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.update(&mut cx, |lsp_store, cx| {
|
||||
for server_id in server_ids {
|
||||
let lsp_adapter = this.language_server_adapter_for_id(server_id);
|
||||
tasks.push((
|
||||
lsp_adapter,
|
||||
this.request_lsp(
|
||||
buffer.clone(),
|
||||
LanguageServerToQuery::Other(server_id),
|
||||
GetCompletions {
|
||||
position,
|
||||
context: context.clone(),
|
||||
let lsp_adapter = lsp_store.language_server_adapter_for_id(server_id);
|
||||
let lsp_timeout = lsp_timeout
|
||||
.map(|lsp_timeout| cx.background_executor().timer(lsp_timeout));
|
||||
let mut timeout = cx.background_spawn(async move {
|
||||
match lsp_timeout {
|
||||
Some(lsp_timeout) => {
|
||||
lsp_timeout.await;
|
||||
true
|
||||
},
|
||||
cx,
|
||||
),
|
||||
));
|
||||
None => false,
|
||||
}
|
||||
}).fuse();
|
||||
let mut lsp_request = lsp_store.request_lsp(
|
||||
buffer.clone(),
|
||||
LanguageServerToQuery::Other(server_id),
|
||||
GetCompletions {
|
||||
position,
|
||||
context: context.clone(),
|
||||
},
|
||||
cx,
|
||||
).fuse();
|
||||
let new_task = cx.background_spawn(async move {
|
||||
select_biased! {
|
||||
response = lsp_request => response,
|
||||
timeout_happened = timeout => {
|
||||
if timeout_happened {
|
||||
log::warn!("Fetching completions from server {server_id} timed out, timeout ms: {}", completion_settings.lsp_fetch_timeout_ms);
|
||||
return anyhow::Ok(Vec::new())
|
||||
} else {
|
||||
lsp_request.await
|
||||
}
|
||||
},
|
||||
}
|
||||
});
|
||||
tasks.push((lsp_adapter, new_task));
|
||||
}
|
||||
})?;
|
||||
|
||||
@@ -4416,47 +4453,58 @@ impl LspStore {
|
||||
{
|
||||
did_resolve = true;
|
||||
}
|
||||
} else {
|
||||
resolve_word_completion(
|
||||
&buffer_snapshot,
|
||||
&mut completions.borrow_mut()[completion_index],
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for completion_index in completion_indices {
|
||||
let Some(server_id) = completions.borrow()[completion_index].source.server_id()
|
||||
else {
|
||||
continue;
|
||||
let server_id = {
|
||||
let completion = &completions.borrow()[completion_index];
|
||||
completion.source.server_id()
|
||||
};
|
||||
if let Some(server_id) = server_id {
|
||||
let server_and_adapter = this
|
||||
.read_with(&cx, |lsp_store, _| {
|
||||
let server = lsp_store.language_server_for_id(server_id)?;
|
||||
let adapter =
|
||||
lsp_store.language_server_adapter_for_id(server.server_id())?;
|
||||
Some((server, adapter))
|
||||
})
|
||||
.ok()
|
||||
.flatten();
|
||||
let Some((server, adapter)) = server_and_adapter else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let server_and_adapter = this
|
||||
.read_with(&cx, |lsp_store, _| {
|
||||
let server = lsp_store.language_server_for_id(server_id)?;
|
||||
let adapter =
|
||||
lsp_store.language_server_adapter_for_id(server.server_id())?;
|
||||
Some((server, adapter))
|
||||
})
|
||||
.ok()
|
||||
.flatten();
|
||||
let Some((server, adapter)) = server_and_adapter else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let resolved = Self::resolve_completion_local(
|
||||
server,
|
||||
&buffer_snapshot,
|
||||
completions.clone(),
|
||||
completion_index,
|
||||
)
|
||||
.await
|
||||
.log_err()
|
||||
.is_some();
|
||||
if resolved {
|
||||
Self::regenerate_completion_labels(
|
||||
adapter,
|
||||
let resolved = Self::resolve_completion_local(
|
||||
server,
|
||||
&buffer_snapshot,
|
||||
completions.clone(),
|
||||
completion_index,
|
||||
)
|
||||
.await
|
||||
.log_err();
|
||||
did_resolve = true;
|
||||
.log_err()
|
||||
.is_some();
|
||||
if resolved {
|
||||
Self::regenerate_completion_labels(
|
||||
adapter,
|
||||
&buffer_snapshot,
|
||||
completions.clone(),
|
||||
completion_index,
|
||||
)
|
||||
.await
|
||||
.log_err();
|
||||
did_resolve = true;
|
||||
}
|
||||
} else {
|
||||
resolve_word_completion(
|
||||
&buffer_snapshot,
|
||||
&mut completions.borrow_mut()[completion_index],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4500,7 +4548,9 @@ impl LspStore {
|
||||
);
|
||||
server.request::<lsp::request::ResolveCompletionItem>(*lsp_completion.clone())
|
||||
}
|
||||
CompletionSource::Custom => return Ok(()),
|
||||
CompletionSource::BufferWord { .. } | CompletionSource::Custom => {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
};
|
||||
let resolved_completion = request.await?;
|
||||
@@ -4641,7 +4691,9 @@ impl LspStore {
|
||||
}
|
||||
serde_json::to_string(lsp_completion).unwrap().into_bytes()
|
||||
}
|
||||
CompletionSource::Custom => return Ok(()),
|
||||
CompletionSource::Custom | CompletionSource::BufferWord { .. } => {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
};
|
||||
let request = proto::ResolveCompletionDocumentation {
|
||||
@@ -8172,51 +8224,54 @@ impl LspStore {
|
||||
}
|
||||
|
||||
pub(crate) fn serialize_completion(completion: &CoreCompletion) -> proto::Completion {
|
||||
let (source, server_id, lsp_completion, lsp_defaults, resolved) = match &completion.source {
|
||||
let mut serialized_completion = proto::Completion {
|
||||
old_start: Some(serialize_anchor(&completion.old_range.start)),
|
||||
old_end: Some(serialize_anchor(&completion.old_range.end)),
|
||||
new_text: completion.new_text.clone(),
|
||||
..proto::Completion::default()
|
||||
};
|
||||
match &completion.source {
|
||||
CompletionSource::Lsp {
|
||||
server_id,
|
||||
lsp_completion,
|
||||
lsp_defaults,
|
||||
resolved,
|
||||
} => (
|
||||
proto::completion::Source::Lsp as i32,
|
||||
server_id.0 as u64,
|
||||
serde_json::to_vec(lsp_completion).unwrap(),
|
||||
lsp_defaults
|
||||
} => {
|
||||
serialized_completion.source = proto::completion::Source::Lsp as i32;
|
||||
serialized_completion.server_id = server_id.0 as u64;
|
||||
serialized_completion.lsp_completion = serde_json::to_vec(lsp_completion).unwrap();
|
||||
serialized_completion.lsp_defaults = lsp_defaults
|
||||
.as_deref()
|
||||
.map(|lsp_defaults| serde_json::to_vec(lsp_defaults).unwrap()),
|
||||
*resolved,
|
||||
),
|
||||
CompletionSource::Custom => (
|
||||
proto::completion::Source::Custom as i32,
|
||||
0,
|
||||
Vec::new(),
|
||||
None,
|
||||
true,
|
||||
),
|
||||
};
|
||||
|
||||
proto::Completion {
|
||||
old_start: Some(serialize_anchor(&completion.old_range.start)),
|
||||
old_end: Some(serialize_anchor(&completion.old_range.end)),
|
||||
new_text: completion.new_text.clone(),
|
||||
server_id,
|
||||
lsp_completion,
|
||||
lsp_defaults,
|
||||
resolved,
|
||||
source,
|
||||
.map(|lsp_defaults| serde_json::to_vec(lsp_defaults).unwrap());
|
||||
serialized_completion.resolved = *resolved;
|
||||
}
|
||||
CompletionSource::BufferWord {
|
||||
word_range,
|
||||
resolved,
|
||||
} => {
|
||||
serialized_completion.source = proto::completion::Source::BufferWord as i32;
|
||||
serialized_completion.buffer_word_start = Some(serialize_anchor(&word_range.start));
|
||||
serialized_completion.buffer_word_end = Some(serialize_anchor(&word_range.end));
|
||||
serialized_completion.resolved = *resolved;
|
||||
}
|
||||
CompletionSource::Custom => {
|
||||
serialized_completion.source = proto::completion::Source::Custom as i32;
|
||||
serialized_completion.resolved = true;
|
||||
}
|
||||
}
|
||||
|
||||
serialized_completion
|
||||
}
|
||||
|
||||
pub(crate) fn deserialize_completion(completion: proto::Completion) -> Result<CoreCompletion> {
|
||||
let old_start = completion
|
||||
.old_start
|
||||
.and_then(deserialize_anchor)
|
||||
.ok_or_else(|| anyhow!("invalid old start"))?;
|
||||
.context("invalid old start")?;
|
||||
let old_end = completion
|
||||
.old_end
|
||||
.and_then(deserialize_anchor)
|
||||
.ok_or_else(|| anyhow!("invalid old end"))?;
|
||||
.context("invalid old end")?;
|
||||
Ok(CoreCompletion {
|
||||
old_range: old_start..old_end,
|
||||
new_text: completion.new_text,
|
||||
@@ -8232,6 +8287,20 @@ impl LspStore {
|
||||
.transpose()?,
|
||||
resolved: completion.resolved,
|
||||
},
|
||||
Some(proto::completion::Source::BufferWord) => {
|
||||
let word_range = completion
|
||||
.buffer_word_start
|
||||
.and_then(deserialize_anchor)
|
||||
.context("invalid buffer word start")?
|
||||
..completion
|
||||
.buffer_word_end
|
||||
.and_then(deserialize_anchor)
|
||||
.context("invalid buffer word end")?;
|
||||
CompletionSource::BufferWord {
|
||||
word_range,
|
||||
resolved: completion.resolved,
|
||||
}
|
||||
}
|
||||
_ => anyhow::bail!("Unexpected completion source {}", completion.source),
|
||||
},
|
||||
})
|
||||
@@ -8296,6 +8365,40 @@ impl LspStore {
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_word_completion(snapshot: &BufferSnapshot, completion: &mut Completion) {
|
||||
let CompletionSource::BufferWord {
|
||||
word_range,
|
||||
resolved,
|
||||
} = &mut completion.source
|
||||
else {
|
||||
return;
|
||||
};
|
||||
if *resolved {
|
||||
return;
|
||||
}
|
||||
|
||||
if completion.new_text
|
||||
!= snapshot
|
||||
.text_for_range(word_range.clone())
|
||||
.collect::<String>()
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
let mut offset = 0;
|
||||
for chunk in snapshot.chunks(word_range.clone(), true) {
|
||||
let end_offset = offset + chunk.text.len();
|
||||
if let Some(highlight_id) = chunk.syntax_highlight_id {
|
||||
completion
|
||||
.label
|
||||
.runs
|
||||
.push((offset..end_offset, highlight_id));
|
||||
}
|
||||
offset = end_offset;
|
||||
}
|
||||
*resolved = true;
|
||||
}
|
||||
|
||||
impl EventEmitter<LspStoreEvent> for LspStore {}
|
||||
|
||||
fn remove_empty_hover_blocks(mut hover: Hover) -> Option<Hover> {
|
||||
|
||||
@@ -388,6 +388,10 @@ pub enum CompletionSource {
|
||||
resolved: bool,
|
||||
},
|
||||
Custom,
|
||||
BufferWord {
|
||||
word_range: Range<Anchor>,
|
||||
resolved: bool,
|
||||
},
|
||||
}
|
||||
|
||||
impl CompletionSource {
|
||||
|
||||
@@ -168,10 +168,6 @@ pub struct GitSettings {
|
||||
///
|
||||
/// Default: on
|
||||
pub inline_blame: Option<InlineBlameSettings>,
|
||||
/// How hunks are displayed visually in the editor.
|
||||
///
|
||||
/// Default: staged_hollow
|
||||
pub hunk_style: Option<GitHunkStyleSetting>,
|
||||
}
|
||||
|
||||
impl GitSettings {
|
||||
@@ -207,11 +203,20 @@ impl GitSettings {
|
||||
#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum GitHunkStyleSetting {
|
||||
/// Show unstaged hunks with a filled background and staged hunks hollow.
|
||||
/// Show unstaged hunks with a transparent background
|
||||
#[default]
|
||||
StagedHollow,
|
||||
/// Show unstaged hunks hollow and staged hunks with a filled background.
|
||||
UnstagedHollow,
|
||||
Transparent,
|
||||
/// Show unstaged hunks with a pattern background
|
||||
Pattern,
|
||||
/// Show unstaged hunks with a border background
|
||||
Border,
|
||||
|
||||
/// Show staged hunks with a pattern background
|
||||
StagedPattern,
|
||||
/// Show staged hunks with a pattern background
|
||||
StagedTransparent,
|
||||
/// Show staged hunks with a pattern background
|
||||
StagedBorder,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)]
|
||||
|
||||
@@ -941,7 +941,7 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
// Start the language server by opening a buffer with a compatible file extension.
|
||||
project
|
||||
let _ = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_local_buffer_with_lsp(path!("/the-root/src/a.rs"), cx)
|
||||
})
|
||||
@@ -6008,7 +6008,7 @@ async fn test_uncommitted_diff_for_buffer(cx: &mut gpui::TestAppContext) {
|
||||
0..0,
|
||||
"// the-deleted-contents\n",
|
||||
"",
|
||||
DiffHunkStatus::deleted(DiffHunkSecondaryStatus::NoSecondaryHunk),
|
||||
DiffHunkStatus::deleted(DiffHunkSecondaryStatus::None),
|
||||
)],
|
||||
);
|
||||
});
|
||||
@@ -6168,12 +6168,7 @@ async fn test_staging_hunks(cx: &mut gpui::TestAppContext) {
|
||||
"",
|
||||
DiffHunkStatus::deleted(HasSecondaryHunk),
|
||||
),
|
||||
(
|
||||
1..2,
|
||||
"two\n",
|
||||
"TWO\n",
|
||||
DiffHunkStatus::modified(NoSecondaryHunk),
|
||||
),
|
||||
(1..2, "two\n", "TWO\n", DiffHunkStatus::modified(None)),
|
||||
(
|
||||
3..4,
|
||||
"four\n",
|
||||
@@ -6222,12 +6217,7 @@ async fn test_staging_hunks(cx: &mut gpui::TestAppContext) {
|
||||
"",
|
||||
DiffHunkStatus::deleted(HasSecondaryHunk),
|
||||
),
|
||||
(
|
||||
1..2,
|
||||
"two\n",
|
||||
"TWO\n",
|
||||
DiffHunkStatus::modified(NoSecondaryHunk),
|
||||
),
|
||||
(1..2, "two\n", "TWO\n", DiffHunkStatus::modified(None)),
|
||||
(
|
||||
3..4,
|
||||
"four\n",
|
||||
@@ -6266,12 +6256,7 @@ async fn test_staging_hunks(cx: &mut gpui::TestAppContext) {
|
||||
"",
|
||||
DiffHunkStatus::deleted(HasSecondaryHunk),
|
||||
),
|
||||
(
|
||||
1..2,
|
||||
"two\n",
|
||||
"TWO\n",
|
||||
DiffHunkStatus::modified(NoSecondaryHunk),
|
||||
),
|
||||
(1..2, "two\n", "TWO\n", DiffHunkStatus::modified(None)),
|
||||
(
|
||||
3..4,
|
||||
"four\n",
|
||||
@@ -6292,223 +6277,6 @@ async fn test_staging_hunks(cx: &mut gpui::TestAppContext) {
|
||||
} else {
|
||||
panic!("Unexpected event {event:?}");
|
||||
}
|
||||
|
||||
// Allow writing to the git index to succeed again.
|
||||
fs.set_error_message_for_index_write("/dir/.git".as_ref(), None);
|
||||
|
||||
// Stage two hunks with separate operations.
|
||||
uncommitted_diff.update(cx, |diff, cx| {
|
||||
let hunks = diff.hunks(&snapshot, cx).collect::<Vec<_>>();
|
||||
diff.stage_or_unstage_hunks(true, &hunks[0..1], &snapshot, true, cx);
|
||||
diff.stage_or_unstage_hunks(true, &hunks[2..3], &snapshot, true, cx);
|
||||
});
|
||||
|
||||
// Both staged hunks appear as pending.
|
||||
uncommitted_diff.update(cx, |diff, cx| {
|
||||
assert_hunks(
|
||||
diff.hunks(&snapshot, cx),
|
||||
&snapshot,
|
||||
&diff.base_text_string().unwrap(),
|
||||
&[
|
||||
(
|
||||
0..0,
|
||||
"zero\n",
|
||||
"",
|
||||
DiffHunkStatus::deleted(SecondaryHunkRemovalPending),
|
||||
),
|
||||
(
|
||||
1..2,
|
||||
"two\n",
|
||||
"TWO\n",
|
||||
DiffHunkStatus::modified(NoSecondaryHunk),
|
||||
),
|
||||
(
|
||||
3..4,
|
||||
"four\n",
|
||||
"FOUR\n",
|
||||
DiffHunkStatus::modified(SecondaryHunkRemovalPending),
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
// Both staging operations take effect.
|
||||
cx.run_until_parked();
|
||||
uncommitted_diff.update(cx, |diff, cx| {
|
||||
assert_hunks(
|
||||
diff.hunks(&snapshot, cx),
|
||||
&snapshot,
|
||||
&diff.base_text_string().unwrap(),
|
||||
&[
|
||||
(0..0, "zero\n", "", DiffHunkStatus::deleted(NoSecondaryHunk)),
|
||||
(
|
||||
1..2,
|
||||
"two\n",
|
||||
"TWO\n",
|
||||
DiffHunkStatus::modified(NoSecondaryHunk),
|
||||
),
|
||||
(
|
||||
3..4,
|
||||
"four\n",
|
||||
"FOUR\n",
|
||||
DiffHunkStatus::modified(NoSecondaryHunk),
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[allow(clippy::format_collect)]
|
||||
#[gpui::test]
|
||||
async fn test_staging_lots_of_hunks_fast(cx: &mut gpui::TestAppContext) {
|
||||
use DiffHunkSecondaryStatus::*;
|
||||
init_test(cx);
|
||||
|
||||
let different_lines = (0..500)
|
||||
.step_by(5)
|
||||
.map(|i| format!("diff {}\n", i))
|
||||
.collect::<Vec<String>>();
|
||||
let committed_contents = (0..500).map(|i| format!("{}\n", i)).collect::<String>();
|
||||
let file_contents = (0..500)
|
||||
.map(|i| {
|
||||
if i % 5 == 0 {
|
||||
different_lines[i / 5].clone()
|
||||
} else {
|
||||
format!("{}\n", i)
|
||||
}
|
||||
})
|
||||
.collect::<String>();
|
||||
|
||||
let fs = FakeFs::new(cx.background_executor.clone());
|
||||
fs.insert_tree(
|
||||
"/dir",
|
||||
json!({
|
||||
".git": {},
|
||||
"file.txt": file_contents.clone()
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
fs.set_head_for_repo(
|
||||
"/dir/.git".as_ref(),
|
||||
&[("file.txt".into(), committed_contents.clone())],
|
||||
);
|
||||
fs.set_index_for_repo(
|
||||
"/dir/.git".as_ref(),
|
||||
&[("file.txt".into(), committed_contents.clone())],
|
||||
);
|
||||
|
||||
let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
|
||||
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_local_buffer("/dir/file.txt", cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot());
|
||||
let uncommitted_diff = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_uncommitted_diff(buffer.clone(), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let range = Anchor::MIN..snapshot.anchor_after(snapshot.max_point());
|
||||
|
||||
let mut expected_hunks: Vec<(Range<u32>, String, String, DiffHunkStatus)> = (0..500)
|
||||
.step_by(5)
|
||||
.map(|i| {
|
||||
(
|
||||
i as u32..i as u32 + 1,
|
||||
format!("{}\n", i),
|
||||
different_lines[i / 5].clone(),
|
||||
DiffHunkStatus::modified(HasSecondaryHunk),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
// The hunks are initially unstaged
|
||||
uncommitted_diff.read_with(cx, |diff, cx| {
|
||||
assert_hunks(
|
||||
diff.hunks(&snapshot, cx),
|
||||
&snapshot,
|
||||
&diff.base_text_string().unwrap(),
|
||||
&expected_hunks,
|
||||
);
|
||||
});
|
||||
|
||||
for (_, _, _, status) in expected_hunks.iter_mut() {
|
||||
*status = DiffHunkStatus::modified(SecondaryHunkRemovalPending);
|
||||
}
|
||||
|
||||
// Stage every hunk with a different call
|
||||
uncommitted_diff.update(cx, |diff, cx| {
|
||||
let hunks = diff
|
||||
.hunks_intersecting_range(range.clone(), &snapshot, cx)
|
||||
.collect::<Vec<_>>();
|
||||
for hunk in hunks {
|
||||
diff.stage_or_unstage_hunks(true, &[hunk], &snapshot, true, cx);
|
||||
}
|
||||
|
||||
assert_hunks(
|
||||
diff.hunks(&snapshot, cx),
|
||||
&snapshot,
|
||||
&diff.base_text_string().unwrap(),
|
||||
&expected_hunks,
|
||||
);
|
||||
});
|
||||
|
||||
// If we wait, we'll have no pending hunks
|
||||
cx.run_until_parked();
|
||||
for (_, _, _, status) in expected_hunks.iter_mut() {
|
||||
*status = DiffHunkStatus::modified(NoSecondaryHunk);
|
||||
}
|
||||
|
||||
uncommitted_diff.update(cx, |diff, cx| {
|
||||
assert_hunks(
|
||||
diff.hunks(&snapshot, cx),
|
||||
&snapshot,
|
||||
&diff.base_text_string().unwrap(),
|
||||
&expected_hunks,
|
||||
);
|
||||
});
|
||||
|
||||
for (_, _, _, status) in expected_hunks.iter_mut() {
|
||||
*status = DiffHunkStatus::modified(SecondaryHunkAdditionPending);
|
||||
}
|
||||
|
||||
// Unstage every hunk with a different call
|
||||
uncommitted_diff.update(cx, |diff, cx| {
|
||||
let hunks = diff
|
||||
.hunks_intersecting_range(range, &snapshot, cx)
|
||||
.collect::<Vec<_>>();
|
||||
for hunk in hunks {
|
||||
diff.stage_or_unstage_hunks(false, &[hunk], &snapshot, true, cx);
|
||||
}
|
||||
|
||||
assert_hunks(
|
||||
diff.hunks(&snapshot, cx),
|
||||
&snapshot,
|
||||
&diff.base_text_string().unwrap(),
|
||||
&expected_hunks,
|
||||
);
|
||||
});
|
||||
|
||||
// If we wait, we'll have no pending hunks, again
|
||||
cx.run_until_parked();
|
||||
for (_, _, _, status) in expected_hunks.iter_mut() {
|
||||
*status = DiffHunkStatus::modified(HasSecondaryHunk);
|
||||
}
|
||||
|
||||
uncommitted_diff.update(cx, |diff, cx| {
|
||||
assert_hunks(
|
||||
diff.hunks(&snapshot, cx),
|
||||
&snapshot,
|
||||
&diff.base_text_string().unwrap(),
|
||||
&expected_hunks,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
|
||||
@@ -7,13 +7,24 @@ use handlebars::{Handlebars, RenderError};
|
||||
use language::{BufferSnapshot, LanguageName, Point};
|
||||
use parking_lot::Mutex;
|
||||
use serde::Serialize;
|
||||
use std::{ops::Range, path::PathBuf, sync::Arc, time::Duration};
|
||||
use std::{
|
||||
ops::Range,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
use text::LineEnding;
|
||||
use util::ResultExt;
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct AssistantSystemPromptContext {
|
||||
pub worktree_root_names: Vec<String>,
|
||||
pub worktrees: Vec<AssistantSystemPromptWorktree>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct AssistantSystemPromptWorktree {
|
||||
pub root_name: String,
|
||||
pub abs_path: Arc<Path>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
@@ -223,11 +234,9 @@ impl PromptBuilder {
|
||||
|
||||
pub fn generate_assistant_system_prompt(
|
||||
&self,
|
||||
worktree_root_names: Vec<String>,
|
||||
worktrees: Vec<AssistantSystemPromptWorktree>,
|
||||
) -> Result<String, RenderError> {
|
||||
let prompt = AssistantSystemPromptContext {
|
||||
worktree_root_names,
|
||||
};
|
||||
let prompt = AssistantSystemPromptContext { worktrees };
|
||||
self.handlebars
|
||||
.lock()
|
||||
.render("assistant_system_prompt", &prompt)
|
||||
|
||||
@@ -1002,10 +1002,13 @@ message Completion {
|
||||
bool resolved = 6;
|
||||
Source source = 7;
|
||||
optional bytes lsp_defaults = 8;
|
||||
optional Anchor buffer_word_start = 9;
|
||||
optional Anchor buffer_word_end = 10;
|
||||
|
||||
enum Source {
|
||||
Lsp = 0;
|
||||
Custom = 1;
|
||||
BufferWord = 2;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -406,7 +406,6 @@ where
|
||||
self.seek_internal(pos, bias, &mut (), cx)
|
||||
}
|
||||
|
||||
/// Advances the cursor and returns traversed items as a tree.
|
||||
#[track_caller]
|
||||
pub fn slice<Target>(
|
||||
&mut self,
|
||||
|
||||
@@ -225,15 +225,6 @@ impl<T: Item> SumTree<T> {
|
||||
}))
|
||||
}
|
||||
|
||||
/// Useful in cases where the item type has a non-trivial context type, but the zero value of the summary type doesn't depend on that context.
|
||||
pub fn from_summary(summary: T::Summary) -> Self {
|
||||
SumTree(Arc::new(Node::Leaf {
|
||||
summary,
|
||||
items: ArrayVec::new(),
|
||||
item_summaries: ArrayVec::new(),
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn from_item(item: T, cx: &<T::Summary as Summary>::Context) -> Self {
|
||||
let mut tree = Self::new(cx);
|
||||
tree.push(item, cx);
|
||||
|
||||
@@ -935,19 +935,12 @@ impl Terminal {
|
||||
|
||||
if is_path_surrounded_by_common_symbols(&file_path) {
|
||||
word_match = Match::new(
|
||||
word_match.start().add(term, Boundary::Grid, 1),
|
||||
word_match.end().sub(term, Boundary::Grid, 1),
|
||||
word_match.start().add(term, Boundary::Cursor, 1),
|
||||
word_match.end().sub(term, Boundary::Cursor, 1),
|
||||
);
|
||||
file_path = file_path[1..file_path.len() - 1].to_owned();
|
||||
}
|
||||
|
||||
while file_path.ends_with(':') {
|
||||
file_path.pop();
|
||||
word_match = Match::new(
|
||||
*word_match.start(),
|
||||
word_match.end().sub(term, Boundary::Grid, 1),
|
||||
);
|
||||
}
|
||||
let mut colon_count = 0;
|
||||
for c in file_path.chars() {
|
||||
if c == ':' {
|
||||
@@ -973,7 +966,7 @@ impl Terminal {
|
||||
let stripped_len = file_path.len() - last_index;
|
||||
word_match = Match::new(
|
||||
*word_match.start(),
|
||||
word_match.end().sub(term, Boundary::Grid, stripped_len),
|
||||
word_match.end().sub(term, Boundary::Cursor, stripped_len),
|
||||
);
|
||||
file_path = file_path[0..last_index].to_owned();
|
||||
}
|
||||
|
||||
@@ -136,7 +136,6 @@ where
|
||||
|
||||
pub trait AnchorRangeExt {
|
||||
fn cmp(&self, b: &Range<Anchor>, buffer: &BufferSnapshot) -> Ordering;
|
||||
fn overlaps(&self, b: &Range<Anchor>, buffer: &BufferSnapshot) -> bool;
|
||||
}
|
||||
|
||||
impl AnchorRangeExt for Range<Anchor> {
|
||||
@@ -146,8 +145,4 @@ impl AnchorRangeExt for Range<Anchor> {
|
||||
ord => ord,
|
||||
}
|
||||
}
|
||||
|
||||
fn overlaps(&self, other: &Range<Anchor>, buffer: &BufferSnapshot) -> bool {
|
||||
self.start.cmp(&other.end, buffer).is_lt() && other.start.cmp(&self.end, buffer).is_lt()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -193,7 +193,7 @@ impl Render for ModalLayer {
|
||||
.child(
|
||||
h_flex()
|
||||
.occlude()
|
||||
.child(active_modal.modal.view())
|
||||
.child(div().child(active_modal.modal.view()))
|
||||
.on_mouse_down(MouseButton::Left, |_, _, cx| {
|
||||
cx.stop_propagation();
|
||||
}),
|
||||
|
||||
@@ -8,7 +8,7 @@ use ui::{animation::DefaultAnimations, prelude::*};
|
||||
|
||||
use crate::Workspace;
|
||||
|
||||
const DEFAULT_TOAST_DURATION: Duration = Duration::from_secs(10);
|
||||
const DEFAULT_TOAST_DURATION: Duration = Duration::from_millis(2400);
|
||||
const MINIMUM_RESUME_DURATION: Duration = Duration::from_millis(800);
|
||||
|
||||
actions!(toast, [RunAction]);
|
||||
|
||||
@@ -61,7 +61,7 @@ use std::{
|
||||
path::{Component, Path, PathBuf},
|
||||
pin::Pin,
|
||||
sync::{
|
||||
atomic::{self, AtomicI32, AtomicUsize, Ordering::SeqCst},
|
||||
atomic::{self, AtomicU32, AtomicUsize, Ordering::SeqCst},
|
||||
Arc,
|
||||
},
|
||||
time::{Duration, Instant},
|
||||
@@ -1525,7 +1525,6 @@ impl LocalWorktree {
|
||||
fs,
|
||||
fs_case_sensitive,
|
||||
status_updates_tx: scan_states_tx,
|
||||
scans_running: Arc::new(AtomicI32::new(0)),
|
||||
executor: background,
|
||||
scan_requests_rx,
|
||||
path_prefixes_to_scan_rx,
|
||||
@@ -4250,6 +4249,11 @@ struct PathEntry {
|
||||
scan_id: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct FsScanned {
|
||||
status_scans: Arc<AtomicU32>,
|
||||
}
|
||||
|
||||
impl sum_tree::Item for PathEntry {
|
||||
type Summary = PathEntrySummary;
|
||||
|
||||
@@ -4317,7 +4321,6 @@ struct BackgroundScanner {
|
||||
fs: Arc<dyn Fs>,
|
||||
fs_case_sensitive: bool,
|
||||
status_updates_tx: UnboundedSender<ScanState>,
|
||||
scans_running: Arc<AtomicI32>,
|
||||
executor: BackgroundExecutor,
|
||||
scan_requests_rx: channel::Receiver<ScanRequest>,
|
||||
path_prefixes_to_scan_rx: channel::Receiver<PathPrefixScanRequest>,
|
||||
@@ -4425,13 +4428,13 @@ impl BackgroundScanner {
|
||||
|
||||
// Perform an initial scan of the directory.
|
||||
drop(scan_job_tx);
|
||||
self.scan_dirs(true, scan_job_rx).await;
|
||||
let scans_running = self.scan_dirs(true, scan_job_rx).await;
|
||||
{
|
||||
let mut state = self.state.lock();
|
||||
state.snapshot.completed_scan_id = state.snapshot.scan_id;
|
||||
}
|
||||
|
||||
let scanning = self.scans_running.load(atomic::Ordering::Acquire) > 0;
|
||||
let scanning = scans_running.status_scans.load(atomic::Ordering::Acquire) > 0;
|
||||
self.send_status_update(scanning, SmallVec::new());
|
||||
|
||||
// Process any any FS events that occurred while performing the initial scan.
|
||||
@@ -4458,7 +4461,7 @@ impl BackgroundScanner {
|
||||
// these before handling changes reported by the filesystem.
|
||||
request = self.next_scan_request().fuse() => {
|
||||
let Ok(request) = request else { break };
|
||||
let scanning = self.scans_running.load(atomic::Ordering::Acquire) > 0;
|
||||
let scanning = scans_running.status_scans.load(atomic::Ordering::Acquire) > 0;
|
||||
if !self.process_scan_request(request, scanning).await {
|
||||
return;
|
||||
}
|
||||
@@ -4481,7 +4484,7 @@ impl BackgroundScanner {
|
||||
self.process_events(vec![abs_path]).await;
|
||||
}
|
||||
}
|
||||
let scanning = self.scans_running.load(atomic::Ordering::Acquire) > 0;
|
||||
let scanning = scans_running.status_scans.load(atomic::Ordering::Acquire) > 0;
|
||||
self.send_status_update(scanning, request.done);
|
||||
}
|
||||
|
||||
@@ -4675,7 +4678,7 @@ impl BackgroundScanner {
|
||||
.await;
|
||||
|
||||
self.update_ignore_statuses(scan_job_tx).await;
|
||||
self.scan_dirs(false, scan_job_rx).await;
|
||||
let scans_running = self.scan_dirs(false, scan_job_rx).await;
|
||||
|
||||
let status_update = if !dot_git_abs_paths.is_empty() {
|
||||
Some(self.update_git_repositories(dot_git_abs_paths))
|
||||
@@ -4686,7 +4689,6 @@ impl BackgroundScanner {
|
||||
let phase = self.phase;
|
||||
let status_update_tx = self.status_updates_tx.clone();
|
||||
let state = self.state.clone();
|
||||
let scans_running = self.scans_running.clone();
|
||||
self.executor
|
||||
.spawn(async move {
|
||||
if let Some(status_update) = status_update {
|
||||
@@ -4702,7 +4704,7 @@ impl BackgroundScanner {
|
||||
#[cfg(test)]
|
||||
state.snapshot.check_git_invariants();
|
||||
}
|
||||
let scanning = scans_running.load(atomic::Ordering::Acquire) > 0;
|
||||
let scanning = scans_running.status_scans.load(atomic::Ordering::Acquire) > 0;
|
||||
send_status_update_inner(phase, state, status_update_tx, scanning, SmallVec::new());
|
||||
})
|
||||
.detach();
|
||||
@@ -4727,8 +4729,9 @@ impl BackgroundScanner {
|
||||
}
|
||||
drop(scan_job_tx);
|
||||
}
|
||||
let scans_running = Arc::new(AtomicU32::new(0));
|
||||
while let Ok(job) = scan_job_rx.recv().await {
|
||||
self.scan_dir(&job).await.log_err();
|
||||
self.scan_dir(&scans_running, &job).await.log_err();
|
||||
}
|
||||
|
||||
!mem::take(&mut self.state.lock().paths_to_scan).is_empty()
|
||||
@@ -4738,16 +4741,16 @@ impl BackgroundScanner {
|
||||
&self,
|
||||
enable_progress_updates: bool,
|
||||
scan_jobs_rx: channel::Receiver<ScanJob>,
|
||||
) {
|
||||
) -> FsScanned {
|
||||
if self
|
||||
.status_updates_tx
|
||||
.unbounded_send(ScanState::Started)
|
||||
.is_err()
|
||||
{
|
||||
return;
|
||||
return FsScanned::default();
|
||||
}
|
||||
|
||||
inc_scans_running(&self.scans_running);
|
||||
let scans_running = Arc::new(AtomicU32::new(1));
|
||||
let progress_update_count = AtomicUsize::new(0);
|
||||
self.executor
|
||||
.scoped(|scope| {
|
||||
@@ -4792,7 +4795,7 @@ impl BackgroundScanner {
|
||||
// Recursively load directories from the file system.
|
||||
job = scan_jobs_rx.recv().fuse() => {
|
||||
let Ok(job) = job else { break };
|
||||
if let Err(err) = self.scan_dir(&job).await {
|
||||
if let Err(err) = self.scan_dir(&scans_running, &job).await {
|
||||
if job.path.as_ref() != Path::new("") {
|
||||
log::error!("error scanning directory {:?}: {}", job.abs_path, err);
|
||||
}
|
||||
@@ -4805,7 +4808,10 @@ impl BackgroundScanner {
|
||||
})
|
||||
.await;
|
||||
|
||||
dec_scans_running(&self.scans_running, 1);
|
||||
scans_running.fetch_sub(1, atomic::Ordering::Release);
|
||||
FsScanned {
|
||||
status_scans: scans_running,
|
||||
}
|
||||
}
|
||||
|
||||
fn send_status_update(&self, scanning: bool, barrier: SmallVec<[barrier::Sender; 1]>) -> bool {
|
||||
@@ -4818,7 +4824,7 @@ impl BackgroundScanner {
|
||||
)
|
||||
}
|
||||
|
||||
async fn scan_dir(&self, job: &ScanJob) -> Result<()> {
|
||||
async fn scan_dir(&self, scans_running: &Arc<AtomicU32>, job: &ScanJob) -> Result<()> {
|
||||
let root_abs_path;
|
||||
let root_char_bag;
|
||||
{
|
||||
@@ -4873,7 +4879,7 @@ impl BackgroundScanner {
|
||||
self.watcher.as_ref(),
|
||||
);
|
||||
if let Some(local_repo) = repo {
|
||||
inc_scans_running(&self.scans_running);
|
||||
scans_running.fetch_add(1, atomic::Ordering::Release);
|
||||
git_status_update_jobs
|
||||
.push(self.schedule_git_statuses_update(&mut state, local_repo));
|
||||
}
|
||||
@@ -4996,7 +5002,7 @@ impl BackgroundScanner {
|
||||
let task_state = self.state.clone();
|
||||
let phase = self.phase;
|
||||
let status_updates_tx = self.status_updates_tx.clone();
|
||||
let scans_running = self.scans_running.clone();
|
||||
let scans_running = scans_running.clone();
|
||||
self.executor
|
||||
.spawn(async move {
|
||||
if !git_status_update_jobs.is_empty() {
|
||||
@@ -5004,7 +5010,7 @@ impl BackgroundScanner {
|
||||
let status_updated = status_updates
|
||||
.iter()
|
||||
.any(|update_result| update_result.is_ok());
|
||||
dec_scans_running(&scans_running, status_updates.len() as i32);
|
||||
scans_running.fetch_sub(status_updates.len() as u32, atomic::Ordering::Release);
|
||||
if status_updated {
|
||||
let scanning = scans_running.load(atomic::Ordering::Acquire) > 0;
|
||||
send_status_update_inner(
|
||||
@@ -5464,7 +5470,6 @@ impl BackgroundScanner {
|
||||
}
|
||||
};
|
||||
|
||||
inc_scans_running(&self.scans_running);
|
||||
status_updates
|
||||
.push(self.schedule_git_statuses_update(&mut state, local_repository));
|
||||
}
|
||||
@@ -5497,12 +5502,9 @@ impl BackgroundScanner {
|
||||
});
|
||||
}
|
||||
|
||||
let scans_running = self.scans_running.clone();
|
||||
self.executor.spawn(async move {
|
||||
let updates_finished: Vec<Result<(), oneshot::Canceled>> =
|
||||
let _updates_finished: Vec<Result<(), oneshot::Canceled>> =
|
||||
join_all(status_updates).await;
|
||||
let n = updates_finished.len();
|
||||
dec_scans_running(&scans_running, n as i32);
|
||||
})
|
||||
}
|
||||
|
||||
@@ -5510,15 +5512,106 @@ impl BackgroundScanner {
|
||||
fn schedule_git_statuses_update(
|
||||
&self,
|
||||
state: &mut BackgroundScannerState,
|
||||
local_repository: LocalRepositoryEntry,
|
||||
mut local_repository: LocalRepositoryEntry,
|
||||
) -> oneshot::Receiver<()> {
|
||||
let repository_name = local_repository.work_directory.display_name();
|
||||
let path_key = local_repository.work_directory.path_key();
|
||||
|
||||
let job_state = self.state.clone();
|
||||
let (tx, rx) = oneshot::channel();
|
||||
|
||||
state.repository_scans.insert(
|
||||
local_repository.work_directory.path_key(),
|
||||
self.executor
|
||||
.spawn(do_git_status_update(job_state, local_repository, tx)),
|
||||
path_key.clone(),
|
||||
self.executor.spawn(async move {
|
||||
update_branches(&job_state, &mut local_repository)
|
||||
.await
|
||||
.log_err();
|
||||
log::trace!("updating git statuses for repo {repository_name}",);
|
||||
let t0 = Instant::now();
|
||||
|
||||
let Some(statuses) = local_repository
|
||||
.repo()
|
||||
.status(&[git::WORK_DIRECTORY_REPO_PATH.clone()])
|
||||
.log_err()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
log::trace!(
|
||||
"computed git statuses for repo {repository_name} in {:?}",
|
||||
t0.elapsed()
|
||||
);
|
||||
|
||||
let t0 = Instant::now();
|
||||
let mut changed_paths = Vec::new();
|
||||
let snapshot = job_state.lock().snapshot.snapshot.clone();
|
||||
|
||||
let Some(mut repository) = snapshot
|
||||
.repository(path_key)
|
||||
.context(
|
||||
"Tried to update git statuses for a repository that isn't in the snapshot",
|
||||
)
|
||||
.log_err()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
let merge_head_shas = local_repository.repo().merge_head_shas();
|
||||
if merge_head_shas != local_repository.current_merge_head_shas {
|
||||
mem::take(&mut repository.current_merge_conflicts);
|
||||
}
|
||||
|
||||
let mut new_entries_by_path = SumTree::new(&());
|
||||
for (repo_path, status) in statuses.entries.iter() {
|
||||
let project_path = repository.work_directory.try_unrelativize(repo_path);
|
||||
|
||||
new_entries_by_path.insert_or_replace(
|
||||
StatusEntry {
|
||||
repo_path: repo_path.clone(),
|
||||
status: *status,
|
||||
},
|
||||
&(),
|
||||
);
|
||||
if status.is_conflicted() {
|
||||
repository.current_merge_conflicts.insert(repo_path.clone());
|
||||
}
|
||||
|
||||
if let Some(path) = project_path {
|
||||
changed_paths.push(path);
|
||||
}
|
||||
}
|
||||
|
||||
repository.statuses_by_path = new_entries_by_path;
|
||||
let mut state = job_state.lock();
|
||||
state
|
||||
.snapshot
|
||||
.repositories
|
||||
.insert_or_replace(repository, &());
|
||||
state.snapshot.git_repositories.update(
|
||||
&local_repository.work_directory_id,
|
||||
|entry| {
|
||||
entry.current_merge_head_shas = merge_head_shas;
|
||||
entry.merge_message = std::fs::read_to_string(
|
||||
local_repository.dot_git_dir_abs_path.join("MERGE_MSG"),
|
||||
)
|
||||
.ok()
|
||||
.and_then(|merge_msg| Some(merge_msg.lines().next()?.to_owned()));
|
||||
entry.status_scan_id += 1;
|
||||
},
|
||||
);
|
||||
|
||||
util::extend_sorted(
|
||||
&mut state.changed_paths,
|
||||
changed_paths,
|
||||
usize::MAX,
|
||||
Ord::cmp,
|
||||
);
|
||||
|
||||
log::trace!(
|
||||
"applied git status updates for repo {repository_name} in {:?}",
|
||||
t0.elapsed(),
|
||||
);
|
||||
tx.send(()).ok();
|
||||
}),
|
||||
);
|
||||
rx
|
||||
}
|
||||
@@ -5550,15 +5643,6 @@ impl BackgroundScanner {
|
||||
}
|
||||
}
|
||||
|
||||
fn inc_scans_running(scans_running: &AtomicI32) {
|
||||
scans_running.fetch_add(1, atomic::Ordering::Release);
|
||||
}
|
||||
|
||||
fn dec_scans_running(scans_running: &AtomicI32, by: i32) {
|
||||
let old = scans_running.fetch_sub(by, atomic::Ordering::Release);
|
||||
debug_assert!(old >= by);
|
||||
}
|
||||
|
||||
fn send_status_update_inner(
|
||||
phase: BackgroundScannerPhase,
|
||||
state: Arc<Mutex<BackgroundScannerState>>,
|
||||
@@ -5606,100 +5690,6 @@ async fn update_branches(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn do_git_status_update(
|
||||
job_state: Arc<Mutex<BackgroundScannerState>>,
|
||||
mut local_repository: LocalRepositoryEntry,
|
||||
tx: oneshot::Sender<()>,
|
||||
) {
|
||||
let repository_name = local_repository.work_directory.display_name();
|
||||
log::trace!("updating git branches for repo {repository_name}");
|
||||
update_branches(&job_state, &mut local_repository)
|
||||
.await
|
||||
.log_err();
|
||||
let t0 = Instant::now();
|
||||
|
||||
log::trace!("updating git statuses for repo {repository_name}");
|
||||
let Some(statuses) = local_repository
|
||||
.repo()
|
||||
.status(&[git::WORK_DIRECTORY_REPO_PATH.clone()])
|
||||
.log_err()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
log::trace!(
|
||||
"computed git statuses for repo {repository_name} in {:?}",
|
||||
t0.elapsed()
|
||||
);
|
||||
|
||||
let t0 = Instant::now();
|
||||
let mut changed_paths = Vec::new();
|
||||
let snapshot = job_state.lock().snapshot.snapshot.clone();
|
||||
|
||||
let Some(mut repository) = snapshot
|
||||
.repository(local_repository.work_directory.path_key())
|
||||
.context("Tried to update git statuses for a repository that isn't in the snapshot")
|
||||
.log_err()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
let merge_head_shas = local_repository.repo().merge_head_shas();
|
||||
if merge_head_shas != local_repository.current_merge_head_shas {
|
||||
mem::take(&mut repository.current_merge_conflicts);
|
||||
}
|
||||
|
||||
let mut new_entries_by_path = SumTree::new(&());
|
||||
for (repo_path, status) in statuses.entries.iter() {
|
||||
let project_path = repository.work_directory.try_unrelativize(repo_path);
|
||||
|
||||
new_entries_by_path.insert_or_replace(
|
||||
StatusEntry {
|
||||
repo_path: repo_path.clone(),
|
||||
status: *status,
|
||||
},
|
||||
&(),
|
||||
);
|
||||
if status.is_conflicted() {
|
||||
repository.current_merge_conflicts.insert(repo_path.clone());
|
||||
}
|
||||
|
||||
if let Some(path) = project_path {
|
||||
changed_paths.push(path);
|
||||
}
|
||||
}
|
||||
|
||||
repository.statuses_by_path = new_entries_by_path;
|
||||
let mut state = job_state.lock();
|
||||
state
|
||||
.snapshot
|
||||
.repositories
|
||||
.insert_or_replace(repository, &());
|
||||
state
|
||||
.snapshot
|
||||
.git_repositories
|
||||
.update(&local_repository.work_directory_id, |entry| {
|
||||
entry.current_merge_head_shas = merge_head_shas;
|
||||
entry.merge_message =
|
||||
std::fs::read_to_string(local_repository.dot_git_dir_abs_path.join("MERGE_MSG"))
|
||||
.ok()
|
||||
.and_then(|merge_msg| Some(merge_msg.lines().next()?.to_owned()));
|
||||
entry.status_scan_id += 1;
|
||||
});
|
||||
|
||||
util::extend_sorted(
|
||||
&mut state.changed_paths,
|
||||
changed_paths,
|
||||
usize::MAX,
|
||||
Ord::cmp,
|
||||
);
|
||||
|
||||
log::trace!(
|
||||
"applied git status updates for repo {repository_name} in {:?}",
|
||||
t0.elapsed(),
|
||||
);
|
||||
tx.send(()).ok();
|
||||
}
|
||||
|
||||
fn build_diff(
|
||||
phase: BackgroundScannerPhase,
|
||||
old_snapshot: &Snapshot,
|
||||
|
||||
@@ -2426,10 +2426,11 @@ async fn test_git_repository_for_path(cx: &mut TestAppContext) {
|
||||
});
|
||||
}
|
||||
|
||||
// NOTE: This test always fails on Windows, because on Windows, unlike on Unix,
|
||||
// you can't rename a directory which some program has already open. This is a
|
||||
// limitation of the Windows. See:
|
||||
// https://stackoverflow.com/questions/41365318/access-is-denied-when-renaming-folder
|
||||
// NOTE:
|
||||
// This test always fails on Windows, because on Windows, unlike on Unix, you can't rename
|
||||
// a directory which some program has already open.
|
||||
// This is a limitation of the Windows.
|
||||
// See: https://stackoverflow.com/questions/41365318/access-is-denied-when-renaming-folder
|
||||
#[gpui::test]
|
||||
#[cfg_attr(target_os = "windows", ignore)]
|
||||
async fn test_file_status(cx: &mut TestAppContext) {
|
||||
@@ -3574,6 +3575,7 @@ fn git_checkout(name: &str, repo: &git2::Repository) {
|
||||
repo.checkout_head(None).expect("Failed to check out head");
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[track_caller]
|
||||
fn git_status(repo: &git2::Repository) -> collections::HashMap<String, git2::Status> {
|
||||
repo.statuses(None)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
description = "The fast, collaborative code editor."
|
||||
edition.workspace = true
|
||||
name = "zed"
|
||||
version = "0.178.4"
|
||||
version = "0.179.0"
|
||||
publish.workspace = true
|
||||
license = "GPL-3.0-or-later"
|
||||
authors = ["Zed Team <hi@zed.dev>"]
|
||||
|
||||
@@ -1 +1 @@
|
||||
preview
|
||||
dev
|
||||
|
||||
@@ -22,10 +22,24 @@ For more information, see:
|
||||
|
||||
## Edit Predictions {#edit-predictions}
|
||||
|
||||
Zed has built-in support for predicting multiple edits at a time via its [Zeta model](https://huggingface.co/zed-industries/zeta). Clicking "Introducing: Edit Prediction" on the top right will open a brief prompt setting up this feature.
|
||||
|
||||
Zed has built-in support for predicting multiple edits at a time [via Zeta](https://huggingface.co/zed-industries/zeta), Zed's open-source and open-data model.
|
||||
Edit predictions appear as you type, and most of the time, you can accept them by pressing `tab`.
|
||||
|
||||
### Configuring Zeta
|
||||
|
||||
Zed's Edit Prediction was initially introduced via a banner on the title bar.
|
||||
Clicking on it would take you to a modal with a button ("Enable Edit Prediction") that sets `zed` as your `edit_prediction_provider`.
|
||||
|
||||

|
||||
|
||||
But, if you haven't come across the banner, start using Zed's Edit Prediction by adding this to your settings:
|
||||
|
||||
```json
|
||||
"features": {
|
||||
"edit_prediction_provider": "zed"
|
||||
},
|
||||
```
|
||||
|
||||
### Conflict With Other `tab` Actions {#edit-predictions-conflict}
|
||||
|
||||
By default, when `tab` would normally perform a different action, Zed requires a modifier key to accept predictions:
|
||||
|
||||
@@ -63,6 +63,31 @@ You can install a local build on your machine with:
|
||||
|
||||
This will build zed and the cli in release mode and make them available at `~/.local/bin/zed`, installing .desktop files to `~/.local/share`.
|
||||
|
||||
> **_Note_**: If you encounter linker errors similar to the following:
|
||||
>
|
||||
> ```bash
|
||||
> error: linking with `cc` failed: exit status: 1 ...
|
||||
> = note: /usr/bin/ld: /tmp/rustcISMaod/libaws_lc_sys-79f08eb6d32e546e.rlib(f8e4fd781484bd36-bcm.o): in function `aws_lc_0_25_0_handle_cpu_env':
|
||||
> /aws-lc/crypto/fipsmodule/cpucap/cpu_intel.c:(.text.aws_lc_0_25_0_handle_cpu_env+0x63): undefined reference to `__isoc23_sscanf'
|
||||
> /usr/bin/ld: /tmp/rustcISMaod/libaws_lc_sys-79f08eb6d32e546e.rlib(f8e4fd781484bd36-bcm.o): in function `pkey_rsa_ctrl_str':
|
||||
> /aws-lc/crypto/fipsmodule/evp/p_rsa.c:741:(.text.pkey_rsa_ctrl_str+0x20d): undefined reference to `__isoc23_strtol'
|
||||
> /usr/bin/ld: /aws-lc/crypto/fipsmodule/evp/p_rsa.c:752:(.text.pkey_rsa_ctrl_str+0x258): undefined reference to `__isoc23_strtol'
|
||||
> collect2: error: ld returned 1 exit status
|
||||
> = note: some `extern` functions couldn't be found; some native libraries may need to be installed or have their path specified
|
||||
> = note: use the `-l` flag to specify native libraries to link
|
||||
> = note: use the `cargo:rustc-link-lib` directive to specify the native libraries to link with Cargo (see https://doc.rust-lang.org/cargo/reference/build-scripts.html#rustc-link-lib)
|
||||
> error: could not compile `remote_server` (bin "remote_server") due to 1 previous error
|
||||
> ```
|
||||
>
|
||||
> **Cause**:
|
||||
> this is caused by known bugs in aws-lc-rs(doesn't support GCC >= 14): [FIPS fails to build with GCC >= 14](https://github.com/aws/aws-lc-rs/issues/569)
|
||||
> & [GCC-14 - build failure for FIPS module](https://github.com/aws/aws-lc/issues/2010)
|
||||
>
|
||||
> You can refer to [linux: Linker error for remote_server when using script/install-linux](https://github.com/zed-industries/zed/issues/24880) for more information.
|
||||
>
|
||||
> **Workarounds**:
|
||||
> Set the remote server target to `x86_64-unknown-linux-gnu` like so `export REMOTE_SERVER_TARGET=x86_64-unknown-linux-gnu; script/install-linux`
|
||||
|
||||
## Wayland & X11
|
||||
|
||||
Zed supports both X11 and Wayland. By default, we pick whichever we can find at runtime. If you're on Wayland and want to run in X11 mode, use the environment variable `WAYLAND_DISPLAY=''`.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# PureScript
|
||||
|
||||
PureScript support is available through the [PureScript extension](https://github.com/zed-industries/zed/tree/main/extensions/purescript).
|
||||
PureScript support is available through the [PureScript extension](https://github.com/zed-extensions/purescript).
|
||||
|
||||
- Tree-sitter: [postsolar/tree-sitter-purescript](https://github.com/postsolar/tree-sitter-purescript)
|
||||
- Language-Server: [nwolverson/purescript-language-server](https://github.com/nwolverson/purescript-language-server)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
[Uiua](https://www.uiua.org/) is a general purpose, stack-based, array-oriented programming language with a focus on simplicity, beauty, and tacit code.
|
||||
|
||||
Uiua support is available through the [Uiua extension](https://github.com/zed-industries/zed/tree/main/extensions/uiua).
|
||||
Uiua support is available through the [Uiua extension](https://github.com/zed-extensions/uiua).
|
||||
|
||||
- Tree-sitter: [shnarazk/tree-sitter-uiua](https://github.com/shnarazk/tree-sitter-uiua)
|
||||
- Language Server: [uiua-lang/uiua](https://github.com/uiua-lang/uiua/)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Zig
|
||||
|
||||
Zig support is available through the [Zig extension](https://github.com/zed-industries/zed/tree/main/extensions/zig).
|
||||
Zig support is available through the [Zig extension](https://github.com/zed-extensions/zig).
|
||||
|
||||
- Tree-sitter: [tree-sitter-zig](https://github.com/tree-sitter-grammars/tree-sitter-zig)
|
||||
- Language Server: [zls](https://github.com/zigtools/zls)
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
[package]
|
||||
name = "zed_purescript"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
publish.workspace = true
|
||||
license = "Apache-2.0"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/purescript.rs"
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
zed_extension_api = "0.1.0"
|
||||
@@ -1 +0,0 @@
|
||||
../../LICENSE-APACHE
|
||||
@@ -1,15 +0,0 @@
|
||||
id = "purescript"
|
||||
name = "PureScript"
|
||||
description = "PureScript support."
|
||||
version = "0.1.0"
|
||||
schema_version = 1
|
||||
authors = ["Iván Molina Rebolledo <ivanmolinarebolledo@gmail.com>"]
|
||||
repository = "https://github.com/zed-industries/zed"
|
||||
|
||||
[language_servers.purescript-language-server]
|
||||
name = "PureScript Language Server"
|
||||
language = "PureScript"
|
||||
|
||||
[grammars.purescript]
|
||||
repository = "https://github.com/postsolar/tree-sitter-purescript"
|
||||
commit = "0554811a512b9cec08b5a83ce9096eb22da18213"
|
||||
@@ -1,3 +0,0 @@
|
||||
("(" @open ")" @close)
|
||||
("[" @open "]" @close)
|
||||
("{" @open "}" @close)
|
||||
@@ -1,14 +0,0 @@
|
||||
name = "PureScript"
|
||||
grammar = "purescript"
|
||||
path_suffixes = ["purs"]
|
||||
autoclose_before = ",=)}]"
|
||||
line_comments = ["-- "]
|
||||
block_comment = ["{- ", " -}"]
|
||||
brackets = [
|
||||
{ start = "{", end = "}", close = true, newline = true },
|
||||
{ start = "[", end = "]", close = true, newline = true },
|
||||
{ start = "(", end = ")", close = true, newline = true },
|
||||
{ start = "\"", end = "\"", close = true, newline = false },
|
||||
{ start = "'", end = "'", close = true, newline = false },
|
||||
{ start = "`", end = "`", close = true, newline = false },
|
||||
]
|
||||
@@ -1,144 +0,0 @@
|
||||
;; Copyright 2022 nvim-treesitter
|
||||
;;
|
||||
;; Licensed under the Apache License, Version 2.0 (the "License");
|
||||
;; you may not use this file except in compliance with the License.
|
||||
;; You may obtain a copy of the License at
|
||||
;;
|
||||
;; http://www.apache.org/licenses/LICENSE-2.0
|
||||
;;
|
||||
;; Unless required by applicable law or agreed to in writing, software
|
||||
;; distributed under the License is distributed on an "AS IS" BASIS,
|
||||
;; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
;; See the License for the specific language governing permissions and
|
||||
;; limitations under the License.
|
||||
|
||||
;; ----------------------------------------------------------------------------
|
||||
;; Literals and comments
|
||||
|
||||
(integer) @number
|
||||
(exp_negation) @number
|
||||
(exp_literal (number)) @float
|
||||
(char) @string
|
||||
[
|
||||
(string)
|
||||
(triple_quote_string)
|
||||
] @string
|
||||
|
||||
(comment) @comment
|
||||
|
||||
|
||||
;; ----------------------------------------------------------------------------
|
||||
;; Punctuation
|
||||
|
||||
[
|
||||
"("
|
||||
")"
|
||||
"{"
|
||||
"}"
|
||||
"["
|
||||
"]"
|
||||
] @punctuation.bracket
|
||||
|
||||
[
|
||||
(comma)
|
||||
";"
|
||||
] @punctuation.delimiter
|
||||
|
||||
|
||||
;; ----------------------------------------------------------------------------
|
||||
;; Keywords, operators, includes
|
||||
|
||||
[
|
||||
"forall"
|
||||
"∀"
|
||||
] @keyword
|
||||
|
||||
;; (pragma) @constant
|
||||
|
||||
[
|
||||
"if"
|
||||
"then"
|
||||
"else"
|
||||
"case"
|
||||
"of"
|
||||
] @keyword
|
||||
|
||||
[
|
||||
"import"
|
||||
"module"
|
||||
] @keyword
|
||||
|
||||
[
|
||||
(operator)
|
||||
(constructor_operator)
|
||||
(type_operator)
|
||||
(qualified_module) ; grabs the `.` (dot), ex: import System.IO
|
||||
(all_names)
|
||||
(wildcard)
|
||||
"="
|
||||
"|"
|
||||
"::"
|
||||
"=>"
|
||||
"->"
|
||||
"<-"
|
||||
"\\"
|
||||
"`"
|
||||
"@"
|
||||
"∷"
|
||||
"⇒"
|
||||
"<="
|
||||
"⇐"
|
||||
"→"
|
||||
"←"
|
||||
] @operator
|
||||
|
||||
(module) @title
|
||||
|
||||
[
|
||||
(where)
|
||||
"let"
|
||||
"in"
|
||||
"class"
|
||||
"instance"
|
||||
"derive"
|
||||
"foreign"
|
||||
"data"
|
||||
"newtype"
|
||||
"type"
|
||||
"as"
|
||||
"hiding"
|
||||
"do"
|
||||
"ado"
|
||||
"infix"
|
||||
"infixl"
|
||||
"infixr"
|
||||
] @keyword
|
||||
|
||||
|
||||
;; ----------------------------------------------------------------------------
|
||||
;; Functions and variables
|
||||
|
||||
(variable) @variable
|
||||
(pat_wildcard) @variable
|
||||
|
||||
(signature name: (variable) @type)
|
||||
(function
|
||||
name: (variable) @function
|
||||
patterns: (patterns))
|
||||
|
||||
|
||||
(exp_infix (exp_name) @function (#set! "priority" 101))
|
||||
(exp_apply . (exp_name (variable) @function))
|
||||
(exp_apply . (exp_name (qualified_variable (variable) @function)))
|
||||
|
||||
|
||||
;; ----------------------------------------------------------------------------
|
||||
;; Types
|
||||
|
||||
(type) @type
|
||||
(type_variable) @type
|
||||
|
||||
(constructor) @constructor
|
||||
|
||||
; True or False
|
||||
((constructor) @_bool (#match? @_bool "(True|False)")) @boolean
|
||||
@@ -1,3 +0,0 @@
|
||||
(_ "[" "]" @end) @indent
|
||||
(_ "{" "}" @end) @indent
|
||||
(_ "(" ")" @end) @indent
|
||||
@@ -1,97 +0,0 @@
|
||||
use std::{env, fs};
|
||||
use zed_extension_api::{self as zed, serde_json, Result};
|
||||
|
||||
const SERVER_PATH: &str = "node_modules/.bin/purescript-language-server";
|
||||
const PACKAGE_NAME: &str = "purescript-language-server";
|
||||
|
||||
struct PurescriptExtension {
|
||||
did_find_server: bool,
|
||||
}
|
||||
|
||||
impl PurescriptExtension {
|
||||
fn server_exists(&self) -> bool {
|
||||
fs::metadata(SERVER_PATH).map_or(false, |stat| stat.is_file())
|
||||
}
|
||||
|
||||
fn server_script_path(&mut self, language_server_id: &zed::LanguageServerId) -> Result<String> {
|
||||
let server_exists = self.server_exists();
|
||||
if self.did_find_server && server_exists {
|
||||
return Ok(SERVER_PATH.to_string());
|
||||
}
|
||||
|
||||
zed::set_language_server_installation_status(
|
||||
language_server_id,
|
||||
&zed::LanguageServerInstallationStatus::CheckingForUpdate,
|
||||
);
|
||||
let version = zed::npm_package_latest_version(PACKAGE_NAME)?;
|
||||
|
||||
if !server_exists
|
||||
|| zed::npm_package_installed_version(PACKAGE_NAME)?.as_ref() != Some(&version)
|
||||
{
|
||||
zed::set_language_server_installation_status(
|
||||
language_server_id,
|
||||
&zed::LanguageServerInstallationStatus::Downloading,
|
||||
);
|
||||
let result = zed::npm_install_package(PACKAGE_NAME, &version);
|
||||
match result {
|
||||
Ok(()) => {
|
||||
if !self.server_exists() {
|
||||
Err(format!(
|
||||
"installed package '{PACKAGE_NAME}' did not contain expected path '{SERVER_PATH}'",
|
||||
))?;
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
if !self.server_exists() {
|
||||
Err(error)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.did_find_server = true;
|
||||
Ok(SERVER_PATH.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl zed::Extension for PurescriptExtension {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
did_find_server: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn language_server_command(
|
||||
&mut self,
|
||||
language_server_id: &zed::LanguageServerId,
|
||||
_worktree: &zed::Worktree,
|
||||
) -> Result<zed::Command> {
|
||||
let server_path = self.server_script_path(language_server_id)?;
|
||||
Ok(zed::Command {
|
||||
command: zed::node_binary_path()?,
|
||||
args: vec![
|
||||
env::current_dir()
|
||||
.unwrap()
|
||||
.join(&server_path)
|
||||
.to_string_lossy()
|
||||
.to_string(),
|
||||
"--stdio".to_string(),
|
||||
],
|
||||
env: Default::default(),
|
||||
})
|
||||
}
|
||||
|
||||
fn language_server_initialization_options(
|
||||
&mut self,
|
||||
_language_server_id: &zed::LanguageServerId,
|
||||
_worktree: &zed::Worktree,
|
||||
) -> Result<Option<serde_json::Value>> {
|
||||
Ok(Some(serde_json::json!({
|
||||
"purescript": {
|
||||
"addSpagoSources": true
|
||||
}
|
||||
})))
|
||||
}
|
||||
}
|
||||
|
||||
zed::register_extension!(PurescriptExtension);
|
||||
@@ -1,16 +0,0 @@
|
||||
[package]
|
||||
name = "zed_uiua"
|
||||
version = "0.0.1"
|
||||
edition.workspace = true
|
||||
publish.workspace = true
|
||||
license = "Apache-2.0"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/uiua.rs"
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
zed_extension_api = "0.1.0"
|
||||
@@ -1 +0,0 @@
|
||||
../../LICENSE-APACHE
|
||||
@@ -1,15 +0,0 @@
|
||||
id = "uiua"
|
||||
name = "Uiua"
|
||||
description = "Uiua support."
|
||||
version = "0.0.1"
|
||||
schema_version = 1
|
||||
authors = ["Max Brunsfeld <max@zed.dev>"]
|
||||
repository = "https://github.com/zed-industries/zed"
|
||||
|
||||
[language_servers.uiua]
|
||||
name = "Uiua LSP"
|
||||
language = "Uiua"
|
||||
|
||||
[grammars.uiua]
|
||||
repository = "https://github.com/shnarazk/tree-sitter-uiua"
|
||||
commit = "21dc2db39494585bf29a3f86d5add6e9d11a22ba"
|
||||
@@ -1,11 +0,0 @@
|
||||
name = "Uiua"
|
||||
grammar = "uiua"
|
||||
path_suffixes = ["ua"]
|
||||
line_comments = ["# "]
|
||||
autoclose_before = ")]}\""
|
||||
brackets = [
|
||||
{ start = "{", end = "}", close = true, newline = false},
|
||||
{ start = "[", end = "]", close = true, newline = false },
|
||||
{ start = "(", end = ")", close = true, newline = false },
|
||||
{ start = "\"", end = "\"", close = true, newline = false, not_in = ["string"] },
|
||||
]
|
||||
@@ -1,50 +0,0 @@
|
||||
[
|
||||
(openParen)
|
||||
(closeParen)
|
||||
(openCurly)
|
||||
(closeCurly)
|
||||
(openBracket)
|
||||
(closeBracket)
|
||||
] @punctuation.bracket
|
||||
|
||||
[
|
||||
(branchSeparator)
|
||||
(underscore)
|
||||
] @constructor
|
||||
; ] @punctuation.delimiter
|
||||
|
||||
[ (character) ] @constant.character
|
||||
[ (comment) ] @comment
|
||||
[ (constant) ] @constant.numeric
|
||||
[ (identifier) ] @variable
|
||||
[ (leftArrow) ] @keyword
|
||||
[ (function) ] @function
|
||||
[ (modifier1) ] @operator
|
||||
[ (modifier2) ] @operator
|
||||
[ (number) ] @constant.numeric
|
||||
[ (placeHolder) ] @special
|
||||
[ (otherConstant) ] @string.special
|
||||
[ (signature) ] @type
|
||||
[ (system) ] @function.builtin
|
||||
[ (tripleMinus) ] @module
|
||||
|
||||
; planet
|
||||
[
|
||||
"id"
|
||||
"identity"
|
||||
"∘"
|
||||
"dip"
|
||||
"⊙"
|
||||
"gap"
|
||||
"⋅"
|
||||
] @tag
|
||||
|
||||
[
|
||||
(string)
|
||||
(multiLineString)
|
||||
] @string
|
||||
|
||||
; [
|
||||
; (deprecated)
|
||||
; (identifierDeprecated)
|
||||
; ] @warning
|
||||
@@ -1,3 +0,0 @@
|
||||
[
|
||||
(array)
|
||||
] @indent
|
||||
@@ -1,27 +0,0 @@
|
||||
use zed_extension_api::{self as zed, Result};
|
||||
|
||||
struct UiuaExtension;
|
||||
|
||||
impl zed::Extension for UiuaExtension {
|
||||
fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
|
||||
fn language_server_command(
|
||||
&mut self,
|
||||
_language_server_id: &zed::LanguageServerId,
|
||||
worktree: &zed::Worktree,
|
||||
) -> Result<zed::Command> {
|
||||
let path = worktree
|
||||
.which("uiua")
|
||||
.ok_or_else(|| "uiua is not installed".to_string())?;
|
||||
|
||||
Ok(zed::Command {
|
||||
command: path,
|
||||
args: vec!["lsp".to_string()],
|
||||
env: Default::default(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
zed::register_extension!(UiuaExtension);
|
||||
@@ -1,16 +0,0 @@
|
||||
[package]
|
||||
name = "zed_zig"
|
||||
version = "0.3.3"
|
||||
edition.workspace = true
|
||||
publish.workspace = true
|
||||
license = "Apache-2.0"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/zig.rs"
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
zed_extension_api = "0.1.0"
|
||||
@@ -1 +0,0 @@
|
||||
../../LICENSE-APACHE
|
||||
@@ -1,15 +0,0 @@
|
||||
id = "zig"
|
||||
name = "Zig"
|
||||
description = "Zig support."
|
||||
version = "0.3.3"
|
||||
schema_version = 1
|
||||
authors = ["Allan Calix <contact@acx.dev>"]
|
||||
repository = "https://github.com/zed-industries/zed"
|
||||
|
||||
[language_servers.zls]
|
||||
name = "zls"
|
||||
language = "Zig"
|
||||
|
||||
[grammars.zig]
|
||||
repository = "https://github.com/tree-sitter-grammars/tree-sitter-zig"
|
||||
commit = "eb7d58c2dc4fbeea4745019dee8df013034ae66b"
|
||||
@@ -1,3 +0,0 @@
|
||||
("(" @open ")" @close)
|
||||
("[" @open "]" @close)
|
||||
("{" @open "}" @close)
|
||||
@@ -1,12 +0,0 @@
|
||||
name = "Zig"
|
||||
grammar = "zig"
|
||||
path_suffixes = ["zig", "zon"]
|
||||
line_comments = ["// ", "/// ", "//! "]
|
||||
autoclose_before = ";:.,=}])"
|
||||
brackets = [
|
||||
{ start = "{", end = "}", close = true, newline = true },
|
||||
{ start = "[", end = "]", close = true, newline = true },
|
||||
{ start = "(", end = ")", close = true, newline = true },
|
||||
{ start = "\"", end = "\"", close = true, newline = false, not_in = ["string"] },
|
||||
{ start = "'", end = "'", close = true, newline = false, not_in = ["string", "comment"] },
|
||||
]
|
||||
@@ -1,295 +0,0 @@
|
||||
; Variables
|
||||
|
||||
(identifier) @variable
|
||||
|
||||
; Parameters
|
||||
|
||||
(parameter
|
||||
name: (identifier) @variable.parameter)
|
||||
|
||||
; Types
|
||||
|
||||
(parameter
|
||||
type: (identifier) @type)
|
||||
|
||||
((identifier) @type
|
||||
(#match? @type "^[A-Z_][a-zA-Z0-9_]*"))
|
||||
|
||||
(variable_declaration
|
||||
(identifier) @type
|
||||
"="
|
||||
[
|
||||
(struct_declaration)
|
||||
(enum_declaration)
|
||||
(union_declaration)
|
||||
(opaque_declaration)
|
||||
])
|
||||
|
||||
[
|
||||
(builtin_type)
|
||||
"anyframe"
|
||||
] @type.builtin
|
||||
|
||||
; Constants
|
||||
|
||||
((identifier) @constant
|
||||
(#match? @constant "^[A-Z][A-Z_0-9]+$"))
|
||||
|
||||
[
|
||||
"null"
|
||||
"unreachable"
|
||||
"undefined"
|
||||
] @constant.builtin
|
||||
|
||||
(field_expression
|
||||
.
|
||||
member: (identifier) @constant)
|
||||
|
||||
(enum_declaration
|
||||
(container_field
|
||||
type: (identifier) @constant))
|
||||
|
||||
; Labels
|
||||
|
||||
(block_label (identifier) @label)
|
||||
|
||||
(break_label (identifier) @label)
|
||||
|
||||
; Fields
|
||||
|
||||
(field_initializer
|
||||
.
|
||||
(identifier) @variable.member)
|
||||
|
||||
(field_expression
|
||||
(_)
|
||||
member: (identifier) @property)
|
||||
|
||||
(field_expression
|
||||
(_)
|
||||
member: (identifier) @type (#match? @type "^[A-Z_][a-zA-Z0-9_]*"))
|
||||
|
||||
(container_field
|
||||
name: (identifier) @property)
|
||||
|
||||
(initializer_list
|
||||
(assignment_expression
|
||||
left: (field_expression
|
||||
.
|
||||
member: (identifier) @property)))
|
||||
|
||||
; Functions
|
||||
|
||||
(builtin_identifier) @function.builtin
|
||||
|
||||
(call_expression
|
||||
function: (identifier) @function.call)
|
||||
|
||||
(call_expression
|
||||
function: (field_expression
|
||||
member: (identifier) @function.call))
|
||||
|
||||
(function_declaration
|
||||
name: (identifier) @function)
|
||||
|
||||
; Modules
|
||||
|
||||
(variable_declaration
|
||||
(identifier) @module
|
||||
(builtin_function
|
||||
(builtin_identifier) @keyword.import
|
||||
(#any-of? @keyword.import "@import" "@cImport")))
|
||||
|
||||
; Builtins
|
||||
|
||||
[
|
||||
"c"
|
||||
"..."
|
||||
] @variable.builtin
|
||||
|
||||
((identifier) @variable.builtin
|
||||
(#eq? @variable.builtin "_"))
|
||||
|
||||
(calling_convention
|
||||
(identifier) @variable.builtin)
|
||||
|
||||
; Keywords
|
||||
|
||||
[
|
||||
"asm"
|
||||
"defer"
|
||||
"errdefer"
|
||||
"test"
|
||||
"error"
|
||||
"const"
|
||||
"var"
|
||||
] @keyword
|
||||
|
||||
[
|
||||
"struct"
|
||||
"union"
|
||||
"enum"
|
||||
"opaque"
|
||||
] @keyword.type
|
||||
|
||||
[
|
||||
"async"
|
||||
"await"
|
||||
"suspend"
|
||||
"nosuspend"
|
||||
"resume"
|
||||
] @keyword.coroutine
|
||||
|
||||
"fn" @keyword.function
|
||||
|
||||
[
|
||||
"and"
|
||||
"or"
|
||||
"orelse"
|
||||
] @keyword.operator
|
||||
|
||||
"return" @keyword.return
|
||||
|
||||
[
|
||||
"if"
|
||||
"else"
|
||||
"switch"
|
||||
] @keyword.conditional
|
||||
|
||||
[
|
||||
"for"
|
||||
"while"
|
||||
"break"
|
||||
"continue"
|
||||
] @keyword.repeat
|
||||
|
||||
[
|
||||
"usingnamespace"
|
||||
"export"
|
||||
] @keyword.import
|
||||
|
||||
[
|
||||
"try"
|
||||
"catch"
|
||||
] @keyword.exception
|
||||
|
||||
[
|
||||
"volatile"
|
||||
"allowzero"
|
||||
"noalias"
|
||||
"addrspace"
|
||||
"align"
|
||||
"callconv"
|
||||
"linksection"
|
||||
"pub"
|
||||
"inline"
|
||||
"noinline"
|
||||
"extern"
|
||||
"comptime"
|
||||
"packed"
|
||||
"threadlocal"
|
||||
] @keyword.modifier
|
||||
|
||||
; Operator
|
||||
|
||||
[
|
||||
"="
|
||||
"*="
|
||||
"*%="
|
||||
"*|="
|
||||
"/="
|
||||
"%="
|
||||
"+="
|
||||
"+%="
|
||||
"+|="
|
||||
"-="
|
||||
"-%="
|
||||
"-|="
|
||||
"<<="
|
||||
"<<|="
|
||||
">>="
|
||||
"&="
|
||||
"^="
|
||||
"|="
|
||||
"!"
|
||||
"~"
|
||||
"-"
|
||||
"-%"
|
||||
"&"
|
||||
"=="
|
||||
"!="
|
||||
">"
|
||||
">="
|
||||
"<="
|
||||
"<"
|
||||
"&"
|
||||
"^"
|
||||
"|"
|
||||
"<<"
|
||||
">>"
|
||||
"<<|"
|
||||
"+"
|
||||
"++"
|
||||
"+%"
|
||||
"-%"
|
||||
"+|"
|
||||
"-|"
|
||||
"*"
|
||||
"/"
|
||||
"%"
|
||||
"**"
|
||||
"*%"
|
||||
"*|"
|
||||
"||"
|
||||
".*"
|
||||
".?"
|
||||
"?"
|
||||
".."
|
||||
] @operator
|
||||
|
||||
; Literals
|
||||
|
||||
(character) @string
|
||||
|
||||
([
|
||||
(string)
|
||||
(multiline_string)
|
||||
] @string
|
||||
(#set! "priority" 95))
|
||||
|
||||
(integer) @number
|
||||
|
||||
(float) @number.float
|
||||
|
||||
(boolean) @boolean
|
||||
|
||||
(escape_sequence) @string.escape
|
||||
|
||||
; Punctuation
|
||||
|
||||
[
|
||||
"["
|
||||
"]"
|
||||
"("
|
||||
")"
|
||||
"{"
|
||||
"}"
|
||||
] @punctuation.bracket
|
||||
|
||||
[
|
||||
";"
|
||||
"."
|
||||
","
|
||||
":"
|
||||
"=>"
|
||||
"->"
|
||||
] @punctuation.delimiter
|
||||
|
||||
(payload "|" @punctuation.bracket)
|
||||
|
||||
; Comments
|
||||
|
||||
(comment) @comment
|
||||
|
||||
((comment) @comment.documentation
|
||||
(#match? @comment.documentation "^//(/|!)"))
|
||||
@@ -1,17 +0,0 @@
|
||||
[
|
||||
(block)
|
||||
(switch_expression)
|
||||
(initializer_list)
|
||||
] @indent.begin
|
||||
|
||||
(block
|
||||
"}" @indent.end)
|
||||
|
||||
(_ "[" "]" @end) @indent
|
||||
(_ "{" "}" @end) @indent
|
||||
(_ "(" ")" @end) @indent
|
||||
|
||||
[
|
||||
(comment)
|
||||
(multiline_string)
|
||||
] @indent.ignore
|
||||
@@ -1,10 +0,0 @@
|
||||
((comment) @injection.content
|
||||
(#set! injection.language "comment"))
|
||||
|
||||
; TODO: add when asm is added
|
||||
; (asm_output_item (string) @injection.content
|
||||
; (#set! injection.language "asm"))
|
||||
; (asm_input_item (string) @injection.content
|
||||
; (#set! injection.language "asm"))
|
||||
; (asm_clobbers (string) @injection.content
|
||||
; (#set! injection.language "asm"))
|
||||
@@ -1,50 +0,0 @@
|
||||
(test_declaration
|
||||
"test" @context
|
||||
[
|
||||
(string)
|
||||
(identifier)
|
||||
] @name) @item
|
||||
|
||||
(function_declaration
|
||||
"pub"? @context
|
||||
[
|
||||
"extern"
|
||||
"export"
|
||||
"inline"
|
||||
"noinline"
|
||||
]? @context
|
||||
"fn" @context
|
||||
name: (_) @name) @item
|
||||
|
||||
(source_file
|
||||
(variable_declaration
|
||||
"pub"? @context
|
||||
(identifier) @name
|
||||
"=" (_) @context) @item)
|
||||
|
||||
(struct_declaration
|
||||
(variable_declaration
|
||||
"pub"? @context
|
||||
(identifier) @name
|
||||
"=" (_) @context) @item)
|
||||
|
||||
(union_declaration
|
||||
(variable_declaration
|
||||
"pub"? @context
|
||||
(identifier) @name
|
||||
"=" (_) @context) @item)
|
||||
|
||||
(enum_declaration
|
||||
(variable_declaration
|
||||
"pub"? @context
|
||||
(identifier) @name
|
||||
"=" (_) @context) @item)
|
||||
|
||||
(opaque_declaration
|
||||
(variable_declaration
|
||||
"pub"? @context
|
||||
(identifier) @name
|
||||
"=" (_) @context) @item)
|
||||
|
||||
(container_field
|
||||
. (_) @name) @item
|
||||
@@ -1,27 +0,0 @@
|
||||
(function_declaration
|
||||
body: (_
|
||||
"{"
|
||||
(_)* @function.inside
|
||||
"}")) @function.around
|
||||
|
||||
(test_declaration
|
||||
(block
|
||||
"{"
|
||||
(_)* @function.inside
|
||||
"}")) @function.around
|
||||
|
||||
(variable_declaration
|
||||
(struct_declaration
|
||||
"struct"
|
||||
"{"
|
||||
[(_) ","]* @class.inside
|
||||
"}")) @class.around
|
||||
|
||||
(variable_declaration
|
||||
(enum_declaration
|
||||
"enum"
|
||||
"{"
|
||||
(_)* @class.inside
|
||||
"}")) @class.around
|
||||
|
||||
(comment)+ @comment.around
|
||||
@@ -1,171 +0,0 @@
|
||||
use std::fs;
|
||||
use zed_extension_api::{self as zed, serde_json, settings::LspSettings, LanguageServerId, Result};
|
||||
|
||||
struct ZigExtension {
|
||||
cached_binary_path: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct ZlsBinary {
|
||||
path: String,
|
||||
args: Option<Vec<String>>,
|
||||
environment: Option<Vec<(String, String)>>,
|
||||
}
|
||||
|
||||
impl ZigExtension {
|
||||
fn language_server_binary(
|
||||
&mut self,
|
||||
language_server_id: &LanguageServerId,
|
||||
worktree: &zed::Worktree,
|
||||
) -> Result<ZlsBinary> {
|
||||
let mut args: Option<Vec<String>> = None;
|
||||
|
||||
let (platform, arch) = zed::current_platform();
|
||||
let environment = match platform {
|
||||
zed::Os::Mac | zed::Os::Linux => Some(worktree.shell_env()),
|
||||
zed::Os::Windows => None,
|
||||
};
|
||||
|
||||
if let Ok(lsp_settings) = LspSettings::for_worktree("zls", worktree) {
|
||||
if let Some(binary) = lsp_settings.binary {
|
||||
args = binary.arguments;
|
||||
if let Some(path) = binary.path {
|
||||
return Ok(ZlsBinary {
|
||||
path: path.clone(),
|
||||
args,
|
||||
environment,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(path) = worktree.which("zls") {
|
||||
return Ok(ZlsBinary {
|
||||
path,
|
||||
args,
|
||||
environment,
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(path) = &self.cached_binary_path {
|
||||
if fs::metadata(path).map_or(false, |stat| stat.is_file()) {
|
||||
return Ok(ZlsBinary {
|
||||
path: path.clone(),
|
||||
args,
|
||||
environment,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
zed::set_language_server_installation_status(
|
||||
language_server_id,
|
||||
&zed::LanguageServerInstallationStatus::CheckingForUpdate,
|
||||
);
|
||||
|
||||
// Note that in github releases and on zlstools.org the tar.gz asset is not shown
|
||||
// but is available at https://builds.zigtools.org/zls-{os}-{arch}-{version}.tar.gz
|
||||
let release = zed::latest_github_release(
|
||||
"zigtools/zls",
|
||||
zed::GithubReleaseOptions {
|
||||
require_assets: true,
|
||||
pre_release: false,
|
||||
},
|
||||
)?;
|
||||
|
||||
let arch: &str = match arch {
|
||||
zed::Architecture::Aarch64 => "aarch64",
|
||||
zed::Architecture::X86 => "x86",
|
||||
zed::Architecture::X8664 => "x86_64",
|
||||
};
|
||||
|
||||
let os: &str = match platform {
|
||||
zed::Os::Mac => "macos",
|
||||
zed::Os::Linux => "linux",
|
||||
zed::Os::Windows => "windows",
|
||||
};
|
||||
|
||||
let extension: &str = match platform {
|
||||
zed::Os::Mac | zed::Os::Linux => "tar.gz",
|
||||
zed::Os::Windows => "zip",
|
||||
};
|
||||
|
||||
let asset_name: String = format!("zls-{}-{}-{}.{}", os, arch, release.version, extension);
|
||||
let download_url = format!("https://builds.zigtools.org/{}", asset_name);
|
||||
|
||||
let version_dir = format!("zls-{}", release.version);
|
||||
let binary_path = match platform {
|
||||
zed::Os::Mac | zed::Os::Linux => format!("{version_dir}/zls"),
|
||||
zed::Os::Windows => format!("{version_dir}/zls.exe"),
|
||||
};
|
||||
|
||||
if !fs::metadata(&binary_path).map_or(false, |stat| stat.is_file()) {
|
||||
zed::set_language_server_installation_status(
|
||||
language_server_id,
|
||||
&zed::LanguageServerInstallationStatus::Downloading,
|
||||
);
|
||||
|
||||
zed::download_file(
|
||||
&download_url,
|
||||
&version_dir,
|
||||
match platform {
|
||||
zed::Os::Mac | zed::Os::Linux => zed::DownloadedFileType::GzipTar,
|
||||
zed::Os::Windows => zed::DownloadedFileType::Zip,
|
||||
},
|
||||
)
|
||||
.map_err(|e| format!("failed to download file: {e}"))?;
|
||||
|
||||
zed::make_file_executable(&binary_path)?;
|
||||
|
||||
let entries =
|
||||
fs::read_dir(".").map_err(|e| format!("failed to list working directory {e}"))?;
|
||||
for entry in entries {
|
||||
let entry = entry.map_err(|e| format!("failed to load directory entry {e}"))?;
|
||||
if entry.file_name().to_str() != Some(&version_dir) {
|
||||
fs::remove_dir_all(entry.path()).ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.cached_binary_path = Some(binary_path.clone());
|
||||
Ok(ZlsBinary {
|
||||
path: binary_path,
|
||||
args,
|
||||
environment,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl zed::Extension for ZigExtension {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
cached_binary_path: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn language_server_command(
|
||||
&mut self,
|
||||
language_server_id: &LanguageServerId,
|
||||
worktree: &zed::Worktree,
|
||||
) -> Result<zed::Command> {
|
||||
let zls_binary = self.language_server_binary(language_server_id, worktree)?;
|
||||
Ok(zed::Command {
|
||||
command: zls_binary.path,
|
||||
args: zls_binary.args.unwrap_or_default(),
|
||||
env: zls_binary.environment.unwrap_or_default(),
|
||||
})
|
||||
}
|
||||
|
||||
fn language_server_workspace_configuration(
|
||||
&mut self,
|
||||
_language_server_id: &zed::LanguageServerId,
|
||||
worktree: &zed::Worktree,
|
||||
) -> Result<Option<serde_json::Value>> {
|
||||
let settings = LspSettings::for_worktree("zls", worktree)
|
||||
.ok()
|
||||
.and_then(|lsp_settings| lsp_settings.settings.clone())
|
||||
.unwrap_or_default();
|
||||
Ok(Some(settings))
|
||||
}
|
||||
}
|
||||
|
||||
zed::register_extension!(ZigExtension);
|
||||
Reference in New Issue
Block a user