Compare commits
38 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5a120af767 | ||
|
|
6dbed8deeb | ||
|
|
0f55ff6f5b | ||
|
|
9ea7ed8e0a | ||
|
|
35d3d29bcf | ||
|
|
9f9f3d215d | ||
|
|
4aa4a40e2f | ||
|
|
5c239be757 | ||
|
|
e64a56ffad | ||
|
|
7d905d0791 | ||
|
|
a8ef0f2426 | ||
|
|
341972c79c | ||
|
|
38fbc73ac4 | ||
|
|
6c50659c30 | ||
|
|
a0284a272b | ||
|
|
af1a3cbaac | ||
|
|
05bc6b2abd | ||
|
|
6f2b88239b | ||
|
|
a9d2628c05 | ||
|
|
a038d61940 | ||
|
|
1d8bd151b7 | ||
|
|
ef583e6b5a | ||
|
|
a4dd92fe06 | ||
|
|
a0fca24e3f | ||
|
|
5d8ef94c86 | ||
|
|
fe35695b13 | ||
|
|
9ef454d7eb | ||
|
|
7e39023ea5 | ||
|
|
b78396505f | ||
|
|
69dde8e31d | ||
|
|
86f5bb1cc0 | ||
|
|
d855eb3acb | ||
|
|
632372a4f1 | ||
|
|
a248981fca | ||
|
|
9850bf8022 | ||
|
|
83889bb235 | ||
|
|
76d8623b86 | ||
|
|
c0b751be1f |
59
.github/workflows/ci.yml
vendored
59
.github/workflows/ci.yml
vendored
@@ -10,6 +10,7 @@ on:
|
||||
pull_request:
|
||||
branches:
|
||||
- "**"
|
||||
merge_group:
|
||||
|
||||
concurrency:
|
||||
# Allow only one workflow per any non-`main` branch.
|
||||
@@ -23,6 +24,31 @@ env:
|
||||
RUSTFLAGS: "-D warnings"
|
||||
|
||||
jobs:
|
||||
check_docs_only:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
docs_only: ${{ steps.check_changes.outputs.docs_only }}
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Check for non-docs changes
|
||||
id: check_changes
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" == "merge_group" ]; then
|
||||
# When we're running in a merge queue, never assume that the changes
|
||||
# are docs-only, as there could be other PRs in the group that
|
||||
# contain non-docs changes.
|
||||
echo "Running in the merge queue"
|
||||
echo "docs_only=false" >> $GITHUB_OUTPUT
|
||||
elif git diff --name-only ${{ github.event.pull_request.base.sha }} ${{ github.sha }} | grep -qvE '^docs/'; then
|
||||
echo "Detected non-docs changes"
|
||||
echo "docs_only=false" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "Docs-only change"
|
||||
echo "docs_only=true" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
migration_checks:
|
||||
name: Check Postgres and Protobuf migrations, mergability
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
@@ -95,6 +121,7 @@ jobs:
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- test
|
||||
needs: check_docs_only
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
@@ -102,29 +129,35 @@ jobs:
|
||||
clean: false
|
||||
|
||||
- name: cargo clippy
|
||||
if: needs.check_docs_only.outputs.docs_only == 'false'
|
||||
run: ./script/clippy
|
||||
|
||||
- name: Check unused dependencies
|
||||
if: needs.check_docs_only.outputs.docs_only == 'false'
|
||||
uses: bnjbvr/cargo-machete@main
|
||||
|
||||
- name: Check licenses
|
||||
if: needs.check_docs_only.outputs.docs_only == 'false'
|
||||
run: |
|
||||
script/check-licenses
|
||||
script/generate-licenses /tmp/zed_licenses_output
|
||||
|
||||
- name: Check for new vulnerable dependencies
|
||||
if: github.event_name == 'pull_request'
|
||||
if: github.event_name == 'pull_request' && needs.check_docs_only.outputs.docs_only == 'false'
|
||||
uses: actions/dependency-review-action@3b139cfc5fae8b618d3eae3675e383bb1769c019 # v4
|
||||
with:
|
||||
license-check: false
|
||||
|
||||
- name: Run tests
|
||||
if: needs.check_docs_only.outputs.docs_only == 'false'
|
||||
uses: ./.github/actions/run_tests
|
||||
|
||||
- name: Build collab
|
||||
if: needs.check_docs_only.outputs.docs_only == 'false'
|
||||
run: cargo build -p collab
|
||||
|
||||
- name: Build other binaries and features
|
||||
if: needs.check_docs_only.outputs.docs_only == 'false'
|
||||
run: |
|
||||
cargo build --workspace --bins --all-features
|
||||
cargo check -p gpui --features "macos-blade"
|
||||
@@ -138,6 +171,7 @@ jobs:
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
runs-on:
|
||||
- buildjet-16vcpu-ubuntu-2204
|
||||
needs: check_docs_only
|
||||
steps:
|
||||
- name: Add Rust to the PATH
|
||||
run: echo "$HOME/.cargo/bin" >> $GITHUB_PATH
|
||||
@@ -148,21 +182,26 @@ jobs:
|
||||
clean: false
|
||||
|
||||
- name: Cache dependencies
|
||||
if: needs.check_docs_only.outputs.docs_only == 'false'
|
||||
uses: swatinem/rust-cache@f0deed1e0edfc6a9be95417288c0e1099b1eeec3 # v2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
cache-provider: "buildjet"
|
||||
|
||||
- name: Install Linux dependencies
|
||||
if: needs.check_docs_only.outputs.docs_only == 'false'
|
||||
run: ./script/linux
|
||||
|
||||
- name: cargo clippy
|
||||
if: needs.check_docs_only.outputs.docs_only == 'false'
|
||||
run: ./script/clippy
|
||||
|
||||
- name: Run tests
|
||||
if: needs.check_docs_only.outputs.docs_only == 'false'
|
||||
uses: ./.github/actions/run_tests
|
||||
|
||||
- name: Build other binaries and features
|
||||
if: needs.check_docs_only.outputs.docs_only == 'false'
|
||||
run: |
|
||||
cargo build -p zed
|
||||
cargo check -p workspace
|
||||
@@ -173,6 +212,7 @@ jobs:
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
runs-on:
|
||||
- buildjet-16vcpu-ubuntu-2204
|
||||
needs: check_docs_only
|
||||
steps:
|
||||
- name: Add Rust to the PATH
|
||||
run: echo "$HOME/.cargo/bin" >> $GITHUB_PATH
|
||||
@@ -183,15 +223,18 @@ jobs:
|
||||
clean: false
|
||||
|
||||
- name: Cache dependencies
|
||||
if: needs.check_docs_only.outputs.docs_only == 'false'
|
||||
uses: swatinem/rust-cache@f0deed1e0edfc6a9be95417288c0e1099b1eeec3 # v2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
cache-provider: "buildjet"
|
||||
|
||||
- name: Install Clang & Mold
|
||||
if: needs.check_docs_only.outputs.docs_only == 'false'
|
||||
run: ./script/remote-server && ./script/install-mold 2.34.0
|
||||
|
||||
- name: Build Remote Server
|
||||
if: needs.check_docs_only.outputs.docs_only == 'false'
|
||||
run: cargo build -p remote_server
|
||||
|
||||
# todo(windows): Actually run the tests
|
||||
@@ -200,6 +243,7 @@ jobs:
|
||||
name: (Windows) Run Clippy and tests
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
runs-on: hosted-windows-1
|
||||
needs: check_docs_only
|
||||
steps:
|
||||
# more info here:- https://github.com/rust-lang/cargo/issues/13020
|
||||
- name: Enable longer pathnames for git
|
||||
@@ -210,20 +254,23 @@ jobs:
|
||||
clean: false
|
||||
|
||||
- name: Cache dependencies
|
||||
if: needs.check_docs_only.outputs.docs_only == 'false'
|
||||
uses: swatinem/rust-cache@f0deed1e0edfc6a9be95417288c0e1099b1eeec3 # v2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
cache-provider: "github"
|
||||
|
||||
- name: cargo clippy
|
||||
if: needs.check_docs_only.outputs.docs_only == 'false'
|
||||
# Windows can't run shell scripts, so we need to use `cargo xtask`.
|
||||
run: cargo xtask clippy
|
||||
|
||||
- name: Build Zed
|
||||
if: needs.check_docs_only.outputs.docs_only == 'false'
|
||||
run: cargo build
|
||||
|
||||
bundle-mac:
|
||||
timeout-minutes: 120
|
||||
timeout-minutes: 60
|
||||
name: Create a macOS bundle
|
||||
runs-on:
|
||||
- self-hosted
|
||||
@@ -312,9 +359,9 @@ jobs:
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
bundle-linux-x86_x64:
|
||||
bundle-linux:
|
||||
timeout-minutes: 60
|
||||
name: Linux x86_x64 release bundle
|
||||
name: Create a Linux bundle
|
||||
runs-on:
|
||||
- buildjet-16vcpu-ubuntu-2004
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/v') || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
|
||||
@@ -362,7 +409,7 @@ jobs:
|
||||
|
||||
bundle-linux-aarch64: # this runs on ubuntu22.04
|
||||
timeout-minutes: 60
|
||||
name: Linux arm64 release bundle
|
||||
name: Create arm64 Linux bundle
|
||||
runs-on:
|
||||
- buildjet-16vcpu-ubuntu-2204-arm
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/v') || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
|
||||
@@ -411,7 +458,7 @@ jobs:
|
||||
auto-release-preview:
|
||||
name: Auto release preview
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/v') && endsWith(github.ref, '-pre') && !endsWith(github.ref, '.0-pre') }}
|
||||
needs: [bundle-mac, bundle-linux-x86_x64, bundle-linux-aarch64]
|
||||
needs: [bundle-mac, bundle-linux, bundle-linux-aarch64]
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- bundle
|
||||
|
||||
1
.github/workflows/docs.yml
vendored
1
.github/workflows/docs.yml
vendored
@@ -7,6 +7,7 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
merge_group:
|
||||
|
||||
jobs:
|
||||
check_formatting:
|
||||
|
||||
733
Cargo.lock
generated
733
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -40,7 +40,6 @@ members = [
|
||||
"crates/feedback",
|
||||
"crates/file_finder",
|
||||
"crates/file_icons",
|
||||
"crates/fireworks",
|
||||
"crates/fs",
|
||||
"crates/fsevent",
|
||||
"crates/fuzzy",
|
||||
@@ -223,7 +222,6 @@ feature_flags = { path = "crates/feature_flags" }
|
||||
feedback = { path = "crates/feedback" }
|
||||
file_finder = { path = "crates/file_finder" }
|
||||
file_icons = { path = "crates/file_icons" }
|
||||
fireworks = { path = "crates/fireworks" }
|
||||
fs = { path = "crates/fs" }
|
||||
fsevent = { path = "crates/fsevent" }
|
||||
fuzzy = { path = "crates/fuzzy" }
|
||||
@@ -393,7 +391,7 @@ ignore = "0.4.22"
|
||||
image = "0.25.1"
|
||||
indexmap = { version = "2.7.0", features = ["serde"] }
|
||||
indoc = "2"
|
||||
itertools = "0.13.0"
|
||||
itertools = "0.14.0"
|
||||
jsonwebtoken = "9.3"
|
||||
jupyter-protocol = { version = "0.5.0" }
|
||||
jupyter-websocket-client = { version = "0.8.0" }
|
||||
@@ -514,14 +512,14 @@ url = "2.2"
|
||||
uuid = { version = "1.1.2", features = ["v4", "v5", "v7", "serde"] }
|
||||
wasmparser = "0.215"
|
||||
wasm-encoder = "0.215"
|
||||
wasmtime = { version = "24", default-features = false, features = [
|
||||
wasmtime = { version = "26", default-features = false, features = [
|
||||
"async",
|
||||
"demangle",
|
||||
"runtime",
|
||||
"cranelift",
|
||||
"component-model",
|
||||
] }
|
||||
wasmtime-wasi = "24"
|
||||
wasmtime-wasi = "26"
|
||||
which = "6.0.0"
|
||||
wit-component = "0.201"
|
||||
zstd = "0.11"
|
||||
|
||||
@@ -805,8 +805,7 @@
|
||||
"context": "RateCompletionModal",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-shift-enter": "zeta::ThumbsUpActiveCompletion",
|
||||
"cmd-shift-backspace": "zeta::ThumbsDownActiveCompletion",
|
||||
"cmd-enter": "zeta::ThumbsUp",
|
||||
"shift-down": "zeta::NextEdit",
|
||||
"shift-up": "zeta::PreviousEdit",
|
||||
"right": "zeta::PreviewCompletion"
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Workspace",
|
||||
"context": "Workspace && !Terminal",
|
||||
"bindings": {
|
||||
"ctrl-x ctrl-c": "zed::Quit", // save-buffers-kill-terminal
|
||||
"ctrl-x 5 0": "workspace::CloseWindow", // delete-frame
|
||||
@@ -72,18 +72,6 @@
|
||||
"ctrl-x s": "workspace::SaveAll" // save-some-buffers
|
||||
}
|
||||
},
|
||||
{
|
||||
// Workaround to enable using emacs in the Zed terminal.
|
||||
// Unbind so Zed ignores these keys and lets emacs handle them.
|
||||
"context": "Terminal",
|
||||
"bindings": {
|
||||
"ctrl-x ctrl-c": null, // save-buffers-kill-terminal
|
||||
"ctrl-x ctrl-f": null, // find-file
|
||||
"ctrl-x ctrl-s": null, // save-buffer
|
||||
"ctrl-x ctrl-w": null, // write-file
|
||||
"ctrl-x s": null // save-some-buffers
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "BufferSearchBar > Editor",
|
||||
"bindings": {
|
||||
|
||||
@@ -24,8 +24,8 @@
|
||||
"ctrl-g": ["editor::SelectNext", { "replace_newest": false }],
|
||||
"ctrl-cmd-g": ["editor::SelectPrevious", { "replace_newest": false }],
|
||||
"cmd-/": ["editor::ToggleComments", { "advance_downwards": true }],
|
||||
"cmd-up": "editor::SelectLargerSyntaxNode",
|
||||
"cmd-down": "editor::SelectSmallerSyntaxNode",
|
||||
"alt-up": "editor::SelectLargerSyntaxNode",
|
||||
"alt-down": "editor::SelectSmallerSyntaxNode",
|
||||
"shift-alt-up": "editor::MoveLineUp",
|
||||
"shift-alt-down": "editor::MoveLineDown",
|
||||
"cmd-alt-l": "editor::Format",
|
||||
|
||||
@@ -13,15 +13,15 @@ You must describe the change using the following XML structure:
|
||||
- <description> (optional) - An arbitrarily-long comment that describes the purpose
|
||||
of this edit.
|
||||
- <old_text> (optional) - An excerpt from the file's current contents that uniquely
|
||||
identifies a range within the file where the edit should occur. If this tag is not
|
||||
specified, then the entire file will be used as the range.
|
||||
identifies a range within the file where the edit should occur. Required for all operations
|
||||
except `create`.
|
||||
- <new_text> (required) - The new text to insert into the file.
|
||||
- <operation> (required) - The type of change that should occur at the given range
|
||||
of the file. Must be one of the following values:
|
||||
- `update`: Replaces the entire range with the new text.
|
||||
- `insert_before`: Inserts the new text before the range.
|
||||
- `insert_after`: Inserts new text after the range.
|
||||
- `create`: Creates a new file with the given path and the new text.
|
||||
- `create`: Creates or overwrites a file with the given path and the new text.
|
||||
- `delete`: Deletes the specified range from the file.
|
||||
|
||||
<guidelines>
|
||||
|
||||
@@ -372,6 +372,8 @@
|
||||
"default_width": 240,
|
||||
// Where to dock the project panel. Can be 'left' or 'right'.
|
||||
"dock": "left",
|
||||
// Spacing between worktree entries in the project panel. Can be 'comfortable' or 'standard'.
|
||||
"entry_spacing": "comfortable",
|
||||
// Whether to show file icons in the project panel.
|
||||
"file_icons": true,
|
||||
// Whether to show folder icons or chevrons for directories in the project panel.
|
||||
|
||||
@@ -595,7 +595,7 @@ impl AssistantPanel {
|
||||
true
|
||||
}
|
||||
|
||||
pane::Event::ActivateItem { local } => {
|
||||
pane::Event::ActivateItem { local, .. } => {
|
||||
if *local {
|
||||
self.workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
@@ -4272,6 +4272,10 @@ impl Item for ContextEditor {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn include_in_nav_history() -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
impl SearchableItem for ContextEditor {
|
||||
|
||||
@@ -16,9 +16,7 @@ use editor::{
|
||||
EditorStyle, ExcerptId, ExcerptRange, GutterDimensions, MultiBuffer, MultiBufferSnapshot,
|
||||
ToOffset as _, ToPoint,
|
||||
};
|
||||
use feature_flags::{
|
||||
Assistant2FeatureFlag, FeatureFlagAppExt as _, FeatureFlagViewExt as _, ZedPro,
|
||||
};
|
||||
use feature_flags::{FeatureFlagAppExt as _, ZedPro};
|
||||
use fs::Fs;
|
||||
use futures::{
|
||||
channel::mpsc,
|
||||
@@ -75,16 +73,7 @@ pub fn init(
|
||||
let workspace = cx.view().clone();
|
||||
InlineAssistant::update_global(cx, |inline_assistant, cx| {
|
||||
inline_assistant.register_workspace(&workspace, cx)
|
||||
});
|
||||
|
||||
cx.observe_flag::<Assistant2FeatureFlag, _>({
|
||||
|is_assistant2_enabled, _view, cx| {
|
||||
InlineAssistant::update_global(cx, |inline_assistant, _cx| {
|
||||
inline_assistant.is_assistant2_enabled = is_assistant2_enabled;
|
||||
});
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
@@ -102,7 +91,6 @@ pub struct InlineAssistant {
|
||||
prompt_builder: Arc<PromptBuilder>,
|
||||
telemetry: Arc<Telemetry>,
|
||||
fs: Arc<dyn Fs>,
|
||||
is_assistant2_enabled: bool,
|
||||
}
|
||||
|
||||
impl Global for InlineAssistant {}
|
||||
@@ -124,7 +112,6 @@ impl InlineAssistant {
|
||||
prompt_builder,
|
||||
telemetry,
|
||||
fs,
|
||||
is_assistant2_enabled: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,22 +172,15 @@ impl InlineAssistant {
|
||||
item: &dyn ItemHandle,
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
let is_assistant2_enabled = self.is_assistant2_enabled;
|
||||
|
||||
if let Some(editor) = item.act_as::<Editor>(cx) {
|
||||
editor.update(cx, |editor, cx| {
|
||||
if is_assistant2_enabled {
|
||||
editor
|
||||
.remove_code_action_provider(ASSISTANT_CODE_ACTION_PROVIDER_ID.into(), cx);
|
||||
} else {
|
||||
editor.add_code_action_provider(
|
||||
Rc::new(AssistantCodeActionProvider {
|
||||
editor: cx.view().downgrade(),
|
||||
workspace: workspace.downgrade(),
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
editor.push_code_action_provider(
|
||||
Rc::new(AssistantCodeActionProvider {
|
||||
editor: cx.view().downgrade(),
|
||||
workspace: workspace.downgrade(),
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -3446,13 +3426,7 @@ struct AssistantCodeActionProvider {
|
||||
workspace: WeakView<Workspace>,
|
||||
}
|
||||
|
||||
const ASSISTANT_CODE_ACTION_PROVIDER_ID: &str = "assistant";
|
||||
|
||||
impl CodeActionProvider for AssistantCodeActionProvider {
|
||||
fn id(&self) -> Arc<str> {
|
||||
ASSISTANT_CODE_ACTION_PROVIDER_ID.into()
|
||||
}
|
||||
|
||||
fn code_actions(
|
||||
&self,
|
||||
buffer: &Model<Buffer>,
|
||||
|
||||
@@ -19,6 +19,7 @@ assets.workspace = true
|
||||
assistant_tool.workspace = true
|
||||
async-watch.workspace = true
|
||||
client.workspace = true
|
||||
clock.workspace = true
|
||||
chrono.workspace = true
|
||||
collections.workspace = true
|
||||
command_palette_hooks.workspace = true
|
||||
@@ -33,6 +34,7 @@ gpui.workspace = true
|
||||
handlebars.workspace = true
|
||||
html_to_markdown.workspace = true
|
||||
http_client.workspace = true
|
||||
itertools.workspace = true
|
||||
language.workspace = true
|
||||
language_model.workspace = true
|
||||
language_model_selector.workspace = true
|
||||
|
||||
@@ -282,11 +282,13 @@ impl ActiveThread {
|
||||
.child(div().p_2p5().text_ui(cx).child(markdown.clone()))
|
||||
.when_some(context, |parent, context| {
|
||||
if !context.is_empty() {
|
||||
parent.child(h_flex().flex_wrap().gap_1().px_1p5().pb_1p5().children(
|
||||
context.iter().map(|context| {
|
||||
ContextPill::new_added(context.clone(), false, None)
|
||||
}),
|
||||
))
|
||||
parent.child(
|
||||
h_flex().flex_wrap().gap_1().px_1p5().pb_1p5().children(
|
||||
context.into_iter().map(|context| {
|
||||
ContextPill::new_added(context, false, None)
|
||||
}),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
parent
|
||||
}
|
||||
|
||||
@@ -421,8 +421,7 @@ impl CodegenAlternative {
|
||||
};
|
||||
|
||||
if let Some(context_store) = &self.context_store {
|
||||
let context = context_store.update(cx, |this, _cx| this.context().clone());
|
||||
attach_context_to_message(&mut request_message, context);
|
||||
attach_context_to_message(&mut request_message, context_store.read(cx).snapshot(cx));
|
||||
}
|
||||
|
||||
request_message.content.push(prompt.into());
|
||||
@@ -1053,7 +1052,7 @@ mod tests {
|
||||
stream::{self},
|
||||
Stream,
|
||||
};
|
||||
use gpui::{Context, TestAppContext};
|
||||
use gpui::TestAppContext;
|
||||
use indoc::indoc;
|
||||
use language::{
|
||||
language_settings, tree_sitter_rust, Buffer, Language, LanguageConfig, LanguageMatcher,
|
||||
|
||||
@@ -1,8 +1,17 @@
|
||||
use gpui::SharedString;
|
||||
use std::path::Path;
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
|
||||
use collections::BTreeMap;
|
||||
use gpui::{AppContext, Model, SharedString};
|
||||
use language::Buffer;
|
||||
use language_model::{LanguageModelRequestMessage, MessageContent};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use text::BufferId;
|
||||
use util::post_inc;
|
||||
|
||||
use crate::thread::Thread;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Serialize, Deserialize)]
|
||||
pub struct ContextId(pub(crate) usize);
|
||||
|
||||
@@ -14,16 +23,17 @@ impl ContextId {
|
||||
|
||||
/// Some context attached to a message in a thread.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Context {
|
||||
pub struct ContextSnapshot {
|
||||
pub id: ContextId,
|
||||
pub name: SharedString,
|
||||
pub parent: Option<SharedString>,
|
||||
pub tooltip: Option<SharedString>,
|
||||
pub kind: ContextKind,
|
||||
/// Text to send to the model. This is not refreshed by `snapshot`.
|
||||
pub text: SharedString,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ContextKind {
|
||||
File,
|
||||
Directory,
|
||||
@@ -31,18 +41,156 @@ pub enum ContextKind {
|
||||
Thread,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Context {
|
||||
File(FileContext),
|
||||
Directory(DirectoryContext),
|
||||
FetchedUrl(FetchedUrlContext),
|
||||
Thread(ThreadContext),
|
||||
}
|
||||
|
||||
impl Context {
|
||||
pub fn id(&self) -> ContextId {
|
||||
match self {
|
||||
Self::File(file) => file.id,
|
||||
Self::Directory(directory) => directory.snapshot.id,
|
||||
Self::FetchedUrl(url) => url.id,
|
||||
Self::Thread(thread) => thread.id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Model<Buffer> holds onto the buffer even if the file is deleted and closed. Should remove
|
||||
// the context from the message editor in this case.
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct FileContext {
|
||||
pub id: ContextId,
|
||||
pub buffer: Model<Buffer>,
|
||||
#[allow(unused)]
|
||||
pub version: clock::Global,
|
||||
pub text: SharedString,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct DirectoryContext {
|
||||
#[allow(unused)]
|
||||
pub path: Rc<Path>,
|
||||
// TODO: The choice to make this a BTreeMap was a result of use in a version of
|
||||
// ContextStore::will_include_buffer before I realized that the path logic should be used there
|
||||
// too.
|
||||
#[allow(unused)]
|
||||
pub buffers: BTreeMap<BufferId, (Model<Buffer>, clock::Global)>,
|
||||
pub snapshot: ContextSnapshot,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct FetchedUrlContext {
|
||||
pub id: ContextId,
|
||||
pub url: SharedString,
|
||||
pub text: SharedString,
|
||||
}
|
||||
|
||||
// TODO: Model<Thread> holds onto the thread even if the thread is deleted. Can either handle this
|
||||
// explicitly or have a WeakModel<Thread> and remove during snapshot.
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ThreadContext {
|
||||
pub id: ContextId,
|
||||
pub thread: Model<Thread>,
|
||||
pub text: SharedString,
|
||||
}
|
||||
|
||||
impl Context {
|
||||
pub fn snapshot(&self, cx: &AppContext) -> Option<ContextSnapshot> {
|
||||
match &self {
|
||||
Self::File(file_context) => file_context.snapshot(cx),
|
||||
Self::Directory(directory_context) => Some(directory_context.snapshot()),
|
||||
Self::FetchedUrl(fetched_url_context) => Some(fetched_url_context.snapshot()),
|
||||
Self::Thread(thread_context) => Some(thread_context.snapshot(cx)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FileContext {
|
||||
pub fn path(&self, cx: &AppContext) -> Option<Arc<Path>> {
|
||||
let buffer = self.buffer.read(cx);
|
||||
if let Some(file) = buffer.file() {
|
||||
Some(file.path().clone())
|
||||
} else {
|
||||
log::error!("Buffer that had a path unexpectedly no longer has a path.");
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn snapshot(&self, cx: &AppContext) -> Option<ContextSnapshot> {
|
||||
let path = self.path(cx)?;
|
||||
let full_path: SharedString = path.to_string_lossy().into_owned().into();
|
||||
let name = match path.file_name() {
|
||||
Some(name) => name.to_string_lossy().into_owned().into(),
|
||||
None => full_path.clone(),
|
||||
};
|
||||
let parent = path
|
||||
.parent()
|
||||
.and_then(|p| p.file_name())
|
||||
.map(|p| p.to_string_lossy().into_owned().into());
|
||||
|
||||
Some(ContextSnapshot {
|
||||
id: self.id,
|
||||
name,
|
||||
parent,
|
||||
tooltip: Some(full_path),
|
||||
kind: ContextKind::File,
|
||||
text: self.text.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl DirectoryContext {
|
||||
pub fn snapshot(&self) -> ContextSnapshot {
|
||||
self.snapshot.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl FetchedUrlContext {
|
||||
pub fn snapshot(&self) -> ContextSnapshot {
|
||||
ContextSnapshot {
|
||||
id: self.id,
|
||||
name: self.url.clone(),
|
||||
parent: None,
|
||||
tooltip: None,
|
||||
kind: ContextKind::FetchedUrl,
|
||||
text: self.text.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ThreadContext {
|
||||
pub fn snapshot(&self, cx: &AppContext) -> ContextSnapshot {
|
||||
let thread = self.thread.read(cx);
|
||||
ContextSnapshot {
|
||||
id: self.id,
|
||||
name: thread.summary().unwrap_or("New thread".into()),
|
||||
parent: None,
|
||||
tooltip: None,
|
||||
kind: ContextKind::Thread,
|
||||
text: self.text.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn attach_context_to_message(
|
||||
message: &mut LanguageModelRequestMessage,
|
||||
context: impl IntoIterator<Item = Context>,
|
||||
contexts: impl Iterator<Item = ContextSnapshot>,
|
||||
) {
|
||||
let mut file_context = String::new();
|
||||
let mut directory_context = String::new();
|
||||
let mut fetch_context = String::new();
|
||||
let mut thread_context = String::new();
|
||||
|
||||
for context in context.into_iter() {
|
||||
for context in contexts {
|
||||
match context.kind {
|
||||
ContextKind::File { .. } => {
|
||||
ContextKind::File => {
|
||||
file_context.push_str(&context.text);
|
||||
file_context.push('\n');
|
||||
}
|
||||
@@ -56,7 +204,7 @@ pub fn attach_context_to_message(
|
||||
fetch_context.push_str(&context.text);
|
||||
fetch_context.push('\n');
|
||||
}
|
||||
ContextKind::Thread => {
|
||||
ContextKind::Thread { .. } => {
|
||||
thread_context.push_str(&context.name);
|
||||
thread_context.push('\n');
|
||||
thread_context.push_str(&context.text);
|
||||
|
||||
@@ -2,17 +2,16 @@ use std::path::Path;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::anyhow;
|
||||
use fuzzy::PathMatch;
|
||||
use gpui::{AppContext, DismissEvent, FocusHandle, FocusableView, Task, View, WeakModel, WeakView};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use project::{PathMatchCandidateSet, ProjectPath, Worktree, WorktreeId};
|
||||
use project::{PathMatchCandidateSet, ProjectPath, WorktreeId};
|
||||
use ui::{prelude::*, ListItem};
|
||||
use util::ResultExt as _;
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::context_picker::{ConfirmBehavior, ContextPicker};
|
||||
use crate::context_store::{push_fenced_codeblock, ContextStore};
|
||||
use crate::context_store::ContextStore;
|
||||
|
||||
pub struct DirectoryContextPicker {
|
||||
picker: View<Picker<DirectoryContextPickerDelegate>>,
|
||||
@@ -179,107 +178,45 @@ impl PickerDelegate for DirectoryContextPickerDelegate {
|
||||
return;
|
||||
};
|
||||
|
||||
let workspace = self.workspace.clone();
|
||||
let Some(project) = workspace
|
||||
.upgrade()
|
||||
.map(|workspace| workspace.read(cx).project().clone())
|
||||
let project_path = ProjectPath {
|
||||
worktree_id: WorktreeId::from_usize(mat.worktree_id),
|
||||
path: mat.path.clone(),
|
||||
};
|
||||
|
||||
let Some(task) = self
|
||||
.context_store
|
||||
.update(cx, |context_store, cx| {
|
||||
context_store.add_directory(project_path, cx)
|
||||
})
|
||||
.ok()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let path = mat.path.clone();
|
||||
|
||||
let already_included = self
|
||||
.context_store
|
||||
.update(cx, |context_store, _cx| {
|
||||
if let Some(context_id) = context_store.included_directory(&path) {
|
||||
context_store.remove_context(&context_id);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
})
|
||||
.unwrap_or(true);
|
||||
if already_included {
|
||||
return;
|
||||
}
|
||||
|
||||
let worktree_id = WorktreeId::from_usize(mat.worktree_id);
|
||||
let workspace = self.workspace.clone();
|
||||
let confirm_behavior = self.confirm_behavior;
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let worktree = project.update(&mut cx, |project, cx| {
|
||||
project
|
||||
.worktree_for_id(worktree_id, cx)
|
||||
.ok_or_else(|| anyhow!("no worktree found for {worktree_id:?}"))
|
||||
})??;
|
||||
|
||||
let files = worktree.update(&mut cx, |worktree, _cx| {
|
||||
collect_files_in_path(worktree, &path)
|
||||
})?;
|
||||
|
||||
let open_buffer_tasks = project.update(&mut cx, |project, cx| {
|
||||
files
|
||||
.into_iter()
|
||||
.map(|file_path| {
|
||||
project.open_buffer(
|
||||
ProjectPath {
|
||||
worktree_id,
|
||||
path: file_path.clone(),
|
||||
},
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})?;
|
||||
|
||||
let buffers = futures::future::join_all(open_buffer_tasks).await;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
let mut text = String::new();
|
||||
|
||||
let mut ok_count = 0;
|
||||
|
||||
for buffer in buffers.into_iter().flatten() {
|
||||
let buffer = buffer.read(cx);
|
||||
let path = buffer.file().map_or(&path, |file| file.path());
|
||||
push_fenced_codeblock(&path, buffer.text(), &mut text);
|
||||
ok_count += 1;
|
||||
match task.await {
|
||||
Ok(()) => {
|
||||
this.update(&mut cx, |this, cx| match confirm_behavior {
|
||||
ConfirmBehavior::KeepOpen => {}
|
||||
ConfirmBehavior::Close => this.delegate.dismissed(cx),
|
||||
})?;
|
||||
}
|
||||
|
||||
if ok_count == 0 {
|
||||
Err(err) => {
|
||||
let Some(workspace) = workspace.upgrade() else {
|
||||
return anyhow::Ok(());
|
||||
};
|
||||
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace.show_error(
|
||||
&anyhow::anyhow!(
|
||||
"Could not read any text files from {}",
|
||||
path.display()
|
||||
),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
return anyhow::Ok(());
|
||||
}
|
||||
|
||||
this.delegate
|
||||
.context_store
|
||||
.update(cx, |context_store, _cx| {
|
||||
context_store.insert_directory(&path, text);
|
||||
workspace.update(&mut cx, |workspace, cx| {
|
||||
workspace.show_error(&err, cx);
|
||||
})?;
|
||||
|
||||
match confirm_behavior {
|
||||
ConfirmBehavior::KeepOpen => {}
|
||||
ConfirmBehavior::Close => this.delegate.dismissed(cx),
|
||||
}
|
||||
|
||||
anyhow::Ok(())
|
||||
})??;
|
||||
}
|
||||
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx)
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
|
||||
@@ -303,7 +240,7 @@ impl PickerDelegate for DirectoryContextPickerDelegate {
|
||||
let added = self.context_store.upgrade().map_or(false, |context_store| {
|
||||
context_store
|
||||
.read(cx)
|
||||
.included_directory(&path_match.path)
|
||||
.includes_directory(&path_match.path)
|
||||
.is_some()
|
||||
});
|
||||
|
||||
@@ -327,17 +264,3 @@ impl PickerDelegate for DirectoryContextPickerDelegate {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_files_in_path(worktree: &Worktree, path: &Path) -> Vec<Arc<Path>> {
|
||||
let mut files = Vec::new();
|
||||
|
||||
for entry in worktree.child_entries(path) {
|
||||
if entry.is_dir() {
|
||||
files.extend(collect_files_in_path(worktree, &entry.path));
|
||||
} else if entry.is_file() {
|
||||
files.push(entry.path.clone());
|
||||
}
|
||||
}
|
||||
|
||||
files
|
||||
}
|
||||
|
||||
@@ -82,10 +82,12 @@ impl FetchContextPickerDelegate {
|
||||
}
|
||||
|
||||
async fn build_message(http_client: Arc<HttpClientWithUrl>, url: &str) -> Result<String> {
|
||||
let mut url = url.to_owned();
|
||||
if !url.starts_with("https://") && !url.starts_with("http://") {
|
||||
url = format!("https://{url}");
|
||||
}
|
||||
let prefixed_url = if !url.starts_with("https://") && !url.starts_with("http://") {
|
||||
Some(format!("https://{url}"))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let url = prefixed_url.as_deref().unwrap_or(url);
|
||||
|
||||
let mut response = http_client.get(&url, AsyncBody::default(), true).await?;
|
||||
|
||||
@@ -200,7 +202,7 @@ impl PickerDelegate for FetchContextPickerDelegate {
|
||||
this.delegate
|
||||
.context_store
|
||||
.update(cx, |context_store, _cx| {
|
||||
if context_store.included_url(&url).is_none() {
|
||||
if context_store.includes_url(&url).is_none() {
|
||||
context_store.insert_fetched_url(url, text);
|
||||
}
|
||||
})?;
|
||||
@@ -234,7 +236,7 @@ impl PickerDelegate for FetchContextPickerDelegate {
|
||||
cx: &mut ViewContext<Picker<Self>>,
|
||||
) -> Option<Self::ListItem> {
|
||||
let added = self.context_store.upgrade().map_or(false, |context_store| {
|
||||
context_store.read(cx).included_url(&self.url).is_some()
|
||||
context_store.read(cx).includes_url(&self.url).is_some()
|
||||
});
|
||||
|
||||
Some(
|
||||
|
||||
@@ -11,7 +11,7 @@ use util::ResultExt as _;
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::context_picker::{ConfirmBehavior, ContextPicker};
|
||||
use crate::context_store::{ContextStore, IncludedFile};
|
||||
use crate::context_store::{ContextStore, FileInclusion};
|
||||
|
||||
pub struct FileContextPicker {
|
||||
picker: View<Picker<FileContextPickerDelegate>>,
|
||||
@@ -193,81 +193,41 @@ impl PickerDelegate for FileContextPickerDelegate {
|
||||
return;
|
||||
};
|
||||
|
||||
let workspace = self.workspace.clone();
|
||||
let Some(project) = workspace
|
||||
.upgrade()
|
||||
.map(|workspace| workspace.read(cx).project().clone())
|
||||
let project_path = ProjectPath {
|
||||
worktree_id: WorktreeId::from_usize(mat.worktree_id),
|
||||
path: mat.path.clone(),
|
||||
};
|
||||
|
||||
let Some(task) = self
|
||||
.context_store
|
||||
.update(cx, |context_store, cx| {
|
||||
context_store.add_file(project_path, cx)
|
||||
})
|
||||
.ok()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let path = mat.path.clone();
|
||||
|
||||
let already_included = self
|
||||
.context_store
|
||||
.update(cx, |context_store, _cx| {
|
||||
match context_store.included_file(&path) {
|
||||
Some(IncludedFile::Direct(context_id)) => {
|
||||
context_store.remove_context(&context_id);
|
||||
true
|
||||
}
|
||||
Some(IncludedFile::InDirectory(_)) => true,
|
||||
None => false,
|
||||
}
|
||||
})
|
||||
.unwrap_or(true);
|
||||
if already_included {
|
||||
return;
|
||||
}
|
||||
|
||||
let worktree_id = WorktreeId::from_usize(mat.worktree_id);
|
||||
let workspace = self.workspace.clone();
|
||||
let confirm_behavior = self.confirm_behavior;
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let Some(open_buffer_task) = project
|
||||
.update(&mut cx, |project, cx| {
|
||||
let project_path = ProjectPath {
|
||||
worktree_id,
|
||||
path: path.clone(),
|
||||
};
|
||||
|
||||
let task = project.open_buffer(project_path, cx);
|
||||
|
||||
Some(task)
|
||||
})
|
||||
.ok()
|
||||
.flatten()
|
||||
else {
|
||||
return anyhow::Ok(());
|
||||
};
|
||||
|
||||
let result = open_buffer_task.await;
|
||||
|
||||
this.update(&mut cx, |this, cx| match result {
|
||||
Ok(buffer) => {
|
||||
this.delegate
|
||||
.context_store
|
||||
.update(cx, |context_store, cx| {
|
||||
context_store.insert_file(buffer.read(cx));
|
||||
})?;
|
||||
|
||||
match confirm_behavior {
|
||||
match task.await {
|
||||
Ok(()) => {
|
||||
this.update(&mut cx, |this, cx| match confirm_behavior {
|
||||
ConfirmBehavior::KeepOpen => {}
|
||||
ConfirmBehavior::Close => this.delegate.dismissed(cx),
|
||||
}
|
||||
|
||||
anyhow::Ok(())
|
||||
})?;
|
||||
}
|
||||
Err(err) => {
|
||||
let Some(workspace) = workspace.upgrade() else {
|
||||
return anyhow::Ok(());
|
||||
};
|
||||
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace.update(&mut cx, |workspace, cx| {
|
||||
workspace.show_error(&err, cx);
|
||||
});
|
||||
|
||||
anyhow::Ok(())
|
||||
})?;
|
||||
}
|
||||
})??;
|
||||
}
|
||||
|
||||
anyhow::Ok(())
|
||||
})
|
||||
@@ -315,10 +275,11 @@ impl PickerDelegate for FileContextPickerDelegate {
|
||||
(file_name, Some(directory))
|
||||
};
|
||||
|
||||
let added = self
|
||||
.context_store
|
||||
.upgrade()
|
||||
.and_then(|context_store| context_store.read(cx).included_file(&path_match.path));
|
||||
let added = self.context_store.upgrade().and_then(|context_store| {
|
||||
context_store
|
||||
.read(cx)
|
||||
.will_include_file_path(&path_match.path, cx)
|
||||
});
|
||||
|
||||
Some(
|
||||
ListItem::new(ix)
|
||||
@@ -335,7 +296,7 @@ impl PickerDelegate for FileContextPickerDelegate {
|
||||
})),
|
||||
)
|
||||
.when_some(added, |el, added| match added {
|
||||
IncludedFile::Direct(_) => el.end_slot(
|
||||
FileInclusion::Direct(_) => el.end_slot(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
@@ -345,7 +306,7 @@ impl PickerDelegate for FileContextPickerDelegate {
|
||||
)
|
||||
.child(Label::new("Added").size(LabelSize::Small)),
|
||||
),
|
||||
IncludedFile::InDirectory(dir_name) => {
|
||||
FileInclusion::InDirectory(dir_name) => {
|
||||
let dir_name = dir_name.to_string_lossy().into_owned();
|
||||
|
||||
el.end_slot(
|
||||
|
||||
@@ -167,13 +167,7 @@ impl PickerDelegate for ThreadContextPickerDelegate {
|
||||
};
|
||||
|
||||
self.context_store
|
||||
.update(cx, |context_store, cx| {
|
||||
if let Some(context_id) = context_store.included_thread(&entry.id) {
|
||||
context_store.remove_context(&context_id);
|
||||
} else {
|
||||
context_store.insert_thread(thread.read(cx));
|
||||
}
|
||||
})
|
||||
.update(cx, |context_store, cx| context_store.add_thread(thread, cx))
|
||||
.ok();
|
||||
|
||||
match self.confirm_behavior {
|
||||
@@ -199,8 +193,8 @@ impl PickerDelegate for ThreadContextPickerDelegate {
|
||||
) -> Option<Self::ListItem> {
|
||||
let thread = &self.matches[ix];
|
||||
|
||||
let added = self.context_store.upgrade().map_or(false, |ctx_store| {
|
||||
ctx_store.read(cx).included_thread(&thread.id).is_some()
|
||||
let added = self.context_store.upgrade().map_or(false, |context_store| {
|
||||
context_store.read(cx).includes_thread(&thread.id).is_some()
|
||||
});
|
||||
|
||||
Some(
|
||||
|
||||
@@ -1,37 +1,54 @@
|
||||
use std::fmt::Write as _;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
|
||||
use collections::{HashMap, HashSet};
|
||||
use gpui::SharedString;
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use collections::{BTreeMap, HashMap};
|
||||
use gpui::{AppContext, Model, ModelContext, SharedString, Task, WeakView};
|
||||
use language::Buffer;
|
||||
use project::{ProjectPath, Worktree};
|
||||
use text::BufferId;
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::thread::Thread;
|
||||
use crate::{
|
||||
context::{Context, ContextId, ContextKind},
|
||||
thread::ThreadId,
|
||||
use crate::context::{
|
||||
Context, ContextId, ContextKind, ContextSnapshot, DirectoryContext, FetchedUrlContext,
|
||||
FileContext, ThreadContext,
|
||||
};
|
||||
use crate::thread::{Thread, ThreadId};
|
||||
|
||||
pub struct ContextStore {
|
||||
workspace: WeakView<Workspace>,
|
||||
context: Vec<Context>,
|
||||
// TODO: If an EntityId is used for all context types (like BufferId), can remove ContextId.
|
||||
next_context_id: ContextId,
|
||||
files: HashMap<PathBuf, ContextId>,
|
||||
files: BTreeMap<BufferId, ContextId>,
|
||||
directories: HashMap<PathBuf, ContextId>,
|
||||
threads: HashMap<ThreadId, ContextId>,
|
||||
fetched_urls: HashMap<String, ContextId>,
|
||||
}
|
||||
|
||||
impl ContextStore {
|
||||
pub fn new() -> Self {
|
||||
pub fn new(workspace: WeakView<Workspace>) -> Self {
|
||||
Self {
|
||||
workspace,
|
||||
context: Vec::new(),
|
||||
next_context_id: ContextId(0),
|
||||
files: HashMap::default(),
|
||||
files: BTreeMap::default(),
|
||||
directories: HashMap::default(),
|
||||
threads: HashMap::default(),
|
||||
fetched_urls: HashMap::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn snapshot<'a>(
|
||||
&'a self,
|
||||
cx: &'a AppContext,
|
||||
) -> impl Iterator<Item = ContextSnapshot> + 'a {
|
||||
self.context()
|
||||
.iter()
|
||||
.flat_map(|context| context.snapshot(cx))
|
||||
}
|
||||
|
||||
pub fn context(&self) -> &Vec<Context> {
|
||||
&self.context
|
||||
}
|
||||
@@ -44,42 +61,156 @@ impl ContextStore {
|
||||
self.fetched_urls.clear();
|
||||
}
|
||||
|
||||
pub fn insert_file(&mut self, buffer: &Buffer) {
|
||||
pub fn add_file(
|
||||
&mut self,
|
||||
project_path: ProjectPath,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
let workspace = self.workspace.clone();
|
||||
let Some(project) = workspace
|
||||
.upgrade()
|
||||
.map(|workspace| workspace.read(cx).project().clone())
|
||||
else {
|
||||
return Task::ready(Err(anyhow!("failed to read project")));
|
||||
};
|
||||
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let open_buffer_task = project.update(&mut cx, |project, cx| {
|
||||
project.open_buffer(project_path.clone(), cx)
|
||||
})?;
|
||||
|
||||
let buffer = open_buffer_task.await?;
|
||||
let buffer_id = buffer.update(&mut cx, |buffer, _cx| buffer.remote_id())?;
|
||||
|
||||
let already_included = this.update(&mut cx, |this, _cx| {
|
||||
match this.will_include_buffer(buffer_id, &project_path.path) {
|
||||
Some(FileInclusion::Direct(context_id)) => {
|
||||
this.remove_context(context_id);
|
||||
true
|
||||
}
|
||||
Some(FileInclusion::InDirectory(_)) => true,
|
||||
None => false,
|
||||
}
|
||||
})?;
|
||||
|
||||
if already_included {
|
||||
return anyhow::Ok(());
|
||||
}
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.insert_file(buffer, cx);
|
||||
})?;
|
||||
|
||||
anyhow::Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn insert_file(&mut self, buffer_model: Model<Buffer>, cx: &AppContext) {
|
||||
let buffer = buffer_model.read(cx);
|
||||
let Some(file) = buffer.file() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let path = file.path();
|
||||
let mut text = String::new();
|
||||
push_fenced_codeblock(file.path(), buffer.text(), &mut text);
|
||||
|
||||
let id = self.next_context_id.post_inc();
|
||||
self.files.insert(path.to_path_buf(), id);
|
||||
|
||||
let full_path: SharedString = path.to_string_lossy().into_owned().into();
|
||||
|
||||
let name = match path.file_name() {
|
||||
Some(name) => name.to_string_lossy().into_owned().into(),
|
||||
None => full_path.clone(),
|
||||
};
|
||||
|
||||
let parent = path
|
||||
.parent()
|
||||
.and_then(|p| p.file_name())
|
||||
.map(|p| p.to_string_lossy().into_owned().into());
|
||||
|
||||
let mut text = String::new();
|
||||
push_fenced_codeblock(path, buffer.text(), &mut text);
|
||||
|
||||
self.context.push(Context {
|
||||
self.files.insert(buffer.remote_id(), id);
|
||||
self.context.push(Context::File(FileContext {
|
||||
id,
|
||||
name,
|
||||
parent,
|
||||
tooltip: Some(full_path),
|
||||
kind: ContextKind::File,
|
||||
buffer: buffer_model,
|
||||
version: buffer.version.clone(),
|
||||
text: text.into(),
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
pub fn insert_directory(&mut self, path: &Path, text: impl Into<SharedString>) {
|
||||
pub fn add_directory(
|
||||
&mut self,
|
||||
project_path: ProjectPath,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
let workspace = self.workspace.clone();
|
||||
let Some(project) = workspace
|
||||
.upgrade()
|
||||
.map(|workspace| workspace.read(cx).project().clone())
|
||||
else {
|
||||
return Task::ready(Err(anyhow!("failed to read project")));
|
||||
};
|
||||
|
||||
let already_included = if let Some(context_id) = self.includes_directory(&project_path.path)
|
||||
{
|
||||
self.remove_context(context_id);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
};
|
||||
if already_included {
|
||||
return Task::ready(Ok(()));
|
||||
}
|
||||
|
||||
let worktree_id = project_path.worktree_id;
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let worktree = project.update(&mut cx, |project, cx| {
|
||||
project
|
||||
.worktree_for_id(worktree_id, cx)
|
||||
.ok_or_else(|| anyhow!("no worktree found for {worktree_id:?}"))
|
||||
})??;
|
||||
|
||||
let files = worktree.update(&mut cx, |worktree, _cx| {
|
||||
collect_files_in_path(worktree, &project_path.path)
|
||||
})?;
|
||||
|
||||
let open_buffer_tasks = project.update(&mut cx, |project, cx| {
|
||||
files
|
||||
.into_iter()
|
||||
.map(|file_path| {
|
||||
project.open_buffer(
|
||||
ProjectPath {
|
||||
worktree_id,
|
||||
path: file_path.clone(),
|
||||
},
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})?;
|
||||
|
||||
let buffers = futures::future::join_all(open_buffer_tasks).await;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
let mut text = String::new();
|
||||
let mut directory_buffers = BTreeMap::new();
|
||||
for buffer_model in buffers {
|
||||
let buffer_model = buffer_model?;
|
||||
let buffer = buffer_model.read(cx);
|
||||
let path = buffer.file().map_or(&project_path.path, |file| file.path());
|
||||
push_fenced_codeblock(&path, buffer.text(), &mut text);
|
||||
directory_buffers
|
||||
.insert(buffer.remote_id(), (buffer_model, buffer.version.clone()));
|
||||
}
|
||||
|
||||
if directory_buffers.is_empty() {
|
||||
bail!(
|
||||
"could not read any text files from {}",
|
||||
&project_path.path.display()
|
||||
);
|
||||
}
|
||||
|
||||
this.insert_directory(&project_path.path, directory_buffers, text);
|
||||
|
||||
anyhow::Ok(())
|
||||
})??;
|
||||
|
||||
anyhow::Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn insert_directory(
|
||||
&mut self,
|
||||
path: &Path,
|
||||
buffers: BTreeMap<BufferId, (Model<Buffer>, clock::Global)>,
|
||||
text: impl Into<SharedString>,
|
||||
) {
|
||||
let id = self.next_context_id.post_inc();
|
||||
self.directories.insert(path.to_path_buf(), id);
|
||||
|
||||
@@ -95,70 +226,104 @@ impl ContextStore {
|
||||
.and_then(|p| p.file_name())
|
||||
.map(|p| p.to_string_lossy().into_owned().into());
|
||||
|
||||
self.context.push(Context {
|
||||
id,
|
||||
name,
|
||||
parent,
|
||||
tooltip: Some(full_path),
|
||||
kind: ContextKind::Directory,
|
||||
text: text.into(),
|
||||
});
|
||||
self.context.push(Context::Directory(DirectoryContext {
|
||||
path: path.into(),
|
||||
buffers,
|
||||
snapshot: ContextSnapshot {
|
||||
id,
|
||||
name,
|
||||
parent,
|
||||
tooltip: Some(full_path),
|
||||
kind: ContextKind::Directory,
|
||||
text: text.into(),
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
pub fn insert_thread(&mut self, thread: &Thread) {
|
||||
let context_id = self.next_context_id.post_inc();
|
||||
self.threads.insert(thread.id().clone(), context_id);
|
||||
pub fn add_thread(&mut self, thread: Model<Thread>, cx: &mut ModelContext<Self>) {
|
||||
if let Some(context_id) = self.includes_thread(&thread.read(cx).id()) {
|
||||
self.remove_context(context_id);
|
||||
} else {
|
||||
self.insert_thread(thread, cx);
|
||||
}
|
||||
}
|
||||
|
||||
self.context.push(Context {
|
||||
id: context_id,
|
||||
name: thread.summary().unwrap_or("New thread".into()),
|
||||
parent: None,
|
||||
tooltip: None,
|
||||
kind: ContextKind::Thread,
|
||||
text: thread.text().into(),
|
||||
});
|
||||
pub fn insert_thread(&mut self, thread: Model<Thread>, cx: &AppContext) {
|
||||
let id = self.next_context_id.post_inc();
|
||||
let thread_ref = thread.read(cx);
|
||||
let text = thread_ref.text().into();
|
||||
|
||||
self.threads.insert(thread_ref.id().clone(), id);
|
||||
self.context
|
||||
.push(Context::Thread(ThreadContext { id, thread, text }));
|
||||
}
|
||||
|
||||
pub fn insert_fetched_url(&mut self, url: String, text: impl Into<SharedString>) {
|
||||
let context_id = self.next_context_id.post_inc();
|
||||
self.fetched_urls.insert(url.clone(), context_id);
|
||||
let id = self.next_context_id.post_inc();
|
||||
|
||||
self.context.push(Context {
|
||||
id: context_id,
|
||||
name: url.into(),
|
||||
parent: None,
|
||||
tooltip: None,
|
||||
kind: ContextKind::FetchedUrl,
|
||||
self.fetched_urls.insert(url.clone(), id);
|
||||
self.context.push(Context::FetchedUrl(FetchedUrlContext {
|
||||
id,
|
||||
url: url.into(),
|
||||
text: text.into(),
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
pub fn remove_context(&mut self, id: &ContextId) {
|
||||
let Some(ix) = self.context.iter().position(|context| context.id == *id) else {
|
||||
pub fn remove_context(&mut self, id: ContextId) {
|
||||
let Some(ix) = self.context.iter().position(|context| context.id() == id) else {
|
||||
return;
|
||||
};
|
||||
|
||||
match self.context.remove(ix).kind {
|
||||
ContextKind::File => {
|
||||
self.files.retain(|_, context_id| context_id != id);
|
||||
match self.context.remove(ix) {
|
||||
Context::File(_) => {
|
||||
self.files.retain(|_, context_id| *context_id != id);
|
||||
}
|
||||
ContextKind::Directory => {
|
||||
self.directories.retain(|_, context_id| context_id != id);
|
||||
Context::Directory(_) => {
|
||||
self.directories.retain(|_, context_id| *context_id != id);
|
||||
}
|
||||
ContextKind::FetchedUrl => {
|
||||
self.fetched_urls.retain(|_, context_id| context_id != id);
|
||||
Context::FetchedUrl(_) => {
|
||||
self.fetched_urls.retain(|_, context_id| *context_id != id);
|
||||
}
|
||||
ContextKind::Thread => {
|
||||
self.threads.retain(|_, context_id| context_id != id);
|
||||
Context::Thread(_) => {
|
||||
self.threads.retain(|_, context_id| *context_id != id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn included_file(&self, path: &Path) -> Option<IncludedFile> {
|
||||
if let Some(id) = self.files.get(path) {
|
||||
return Some(IncludedFile::Direct(*id));
|
||||
/// Returns whether the buffer is already included directly in the context, or if it will be
|
||||
/// included in the context via a directory. Directory inclusion is based on paths rather than
|
||||
/// buffer IDs as the directory will be re-scanned.
|
||||
pub fn will_include_buffer(&self, buffer_id: BufferId, path: &Path) -> Option<FileInclusion> {
|
||||
if let Some(context_id) = self.files.get(&buffer_id) {
|
||||
return Some(FileInclusion::Direct(*context_id));
|
||||
}
|
||||
|
||||
self.will_include_file_path_via_directory(path)
|
||||
}
|
||||
|
||||
/// Returns whether this file path is already included directly in the context, or if it will be
|
||||
/// included in the context via a directory.
|
||||
pub fn will_include_file_path(&self, path: &Path, cx: &AppContext) -> Option<FileInclusion> {
|
||||
if !self.files.is_empty() {
|
||||
let found_file_context = self.context.iter().find(|context| match &context {
|
||||
Context::File(file_context) => {
|
||||
if let Some(file_path) = file_context.path(cx) {
|
||||
*file_path == *path
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
_ => false,
|
||||
});
|
||||
if let Some(context) = found_file_context {
|
||||
return Some(FileInclusion::Direct(context.id()));
|
||||
}
|
||||
}
|
||||
|
||||
self.will_include_file_path_via_directory(path)
|
||||
}
|
||||
|
||||
fn will_include_file_path_via_directory(&self, path: &Path) -> Option<FileInclusion> {
|
||||
if self.directories.is_empty() {
|
||||
return None;
|
||||
}
|
||||
@@ -167,40 +332,27 @@ impl ContextStore {
|
||||
|
||||
while buf.pop() {
|
||||
if let Some(_) = self.directories.get(&buf) {
|
||||
return Some(IncludedFile::InDirectory(buf));
|
||||
return Some(FileInclusion::InDirectory(buf));
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
pub fn included_directory(&self, path: &Path) -> Option<ContextId> {
|
||||
pub fn includes_directory(&self, path: &Path) -> Option<ContextId> {
|
||||
self.directories.get(path).copied()
|
||||
}
|
||||
|
||||
pub fn included_thread(&self, thread_id: &ThreadId) -> Option<ContextId> {
|
||||
pub fn includes_thread(&self, thread_id: &ThreadId) -> Option<ContextId> {
|
||||
self.threads.get(thread_id).copied()
|
||||
}
|
||||
|
||||
pub fn included_url(&self, url: &str) -> Option<ContextId> {
|
||||
pub fn includes_url(&self, url: &str) -> Option<ContextId> {
|
||||
self.fetched_urls.get(url).copied()
|
||||
}
|
||||
|
||||
pub fn duplicated_names(&self) -> HashSet<SharedString> {
|
||||
let mut seen = HashSet::default();
|
||||
let mut dupes = HashSet::default();
|
||||
|
||||
for context in self.context().iter() {
|
||||
if !seen.insert(&context.name) {
|
||||
dupes.insert(context.name.clone());
|
||||
}
|
||||
}
|
||||
|
||||
dupes
|
||||
}
|
||||
}
|
||||
|
||||
pub enum IncludedFile {
|
||||
pub enum FileInclusion {
|
||||
Direct(ContextId),
|
||||
InDirectory(PathBuf),
|
||||
}
|
||||
@@ -225,3 +377,17 @@ pub(crate) fn push_fenced_codeblock(path: &Path, content: String, buffer: &mut S
|
||||
|
||||
buffer.push_str("```\n");
|
||||
}
|
||||
|
||||
fn collect_files_in_path(worktree: &Worktree, path: &Path) -> Vec<Arc<Path>> {
|
||||
let mut files = Vec::new();
|
||||
|
||||
for entry in worktree.child_entries(path) {
|
||||
if entry.is_dir() {
|
||||
files.extend(collect_files_in_path(worktree, &entry.path));
|
||||
} else if entry.is_file() {
|
||||
files.push(entry.path.clone());
|
||||
}
|
||||
}
|
||||
|
||||
files
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use collections::HashSet;
|
||||
use editor::Editor;
|
||||
use gpui::{
|
||||
AppContext, DismissEvent, EventEmitter, FocusHandle, Model, Subscription, View, WeakModel,
|
||||
WeakView,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use language::Buffer;
|
||||
use ui::{prelude::*, KeyBinding, PopoverMenu, PopoverMenuHandle, Tooltip};
|
||||
use workspace::Workspace;
|
||||
@@ -73,11 +75,17 @@ impl ContextStrip {
|
||||
let active_item = workspace.read(cx).active_item(cx)?;
|
||||
|
||||
let editor = active_item.to_any().downcast::<Editor>().ok()?.read(cx);
|
||||
let active_buffer = editor.buffer().read(cx).as_singleton()?;
|
||||
let active_buffer_model = editor.buffer().read(cx).as_singleton()?;
|
||||
let active_buffer = active_buffer_model.read(cx);
|
||||
|
||||
let path = active_buffer.read(cx).file()?.path();
|
||||
let path = active_buffer.file()?.path();
|
||||
|
||||
if self.context_store.read(cx).included_file(path).is_some() {
|
||||
if self
|
||||
.context_store
|
||||
.read(cx)
|
||||
.will_include_buffer(active_buffer.remote_id(), path)
|
||||
.is_some()
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -88,7 +96,7 @@ impl ContextStrip {
|
||||
|
||||
Some(SuggestedContext::File {
|
||||
name,
|
||||
buffer: active_buffer.downgrade(),
|
||||
buffer: active_buffer_model.downgrade(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -106,7 +114,7 @@ impl ContextStrip {
|
||||
if self
|
||||
.context_store
|
||||
.read(cx)
|
||||
.included_thread(active_thread.id())
|
||||
.includes_thread(active_thread.id())
|
||||
.is_some()
|
||||
{
|
||||
return None;
|
||||
@@ -131,13 +139,24 @@ impl ContextStrip {
|
||||
impl Render for ContextStrip {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let context_store = self.context_store.read(cx);
|
||||
let context = context_store.context().clone();
|
||||
let context = context_store
|
||||
.context()
|
||||
.iter()
|
||||
.flat_map(|context| context.snapshot(cx))
|
||||
.collect::<Vec<_>>();
|
||||
let context_picker = self.context_picker.clone();
|
||||
let focus_handle = self.focus_handle.clone();
|
||||
|
||||
let suggested_context = self.suggested_context(cx);
|
||||
|
||||
let dupe_names = context_store.duplicated_names();
|
||||
let dupe_names = context
|
||||
.iter()
|
||||
.map(|context| context.name.clone())
|
||||
.sorted()
|
||||
.tuple_windows()
|
||||
.filter(|(a, b)| a == b)
|
||||
.map(|(a, _)| a)
|
||||
.collect::<HashSet<SharedString>>();
|
||||
|
||||
h_flex()
|
||||
.flex_wrap()
|
||||
@@ -194,11 +213,11 @@ impl Render for ContextStrip {
|
||||
context.clone(),
|
||||
dupe_names.contains(&context.name),
|
||||
Some({
|
||||
let context = context.clone();
|
||||
let id = context.id;
|
||||
let context_store = self.context_store.clone();
|
||||
Rc::new(cx.listener(move |_this, _event, cx| {
|
||||
context_store.update(cx, |this, _cx| {
|
||||
this.remove_context(&context.id);
|
||||
this.remove_context(id);
|
||||
});
|
||||
cx.notify();
|
||||
}))
|
||||
@@ -284,12 +303,12 @@ impl SuggestedContext {
|
||||
match self {
|
||||
Self::File { buffer, name: _ } => {
|
||||
if let Some(buffer) = buffer.upgrade() {
|
||||
context_store.insert_file(buffer.read(cx));
|
||||
context_store.insert_file(buffer, cx);
|
||||
};
|
||||
}
|
||||
Self::Thread { thread, name: _ } => {
|
||||
if let Some(thread) = thread.upgrade() {
|
||||
context_store.insert_thread(thread.read(cx));
|
||||
context_store.insert_thread(thread, cx);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ use editor::{
|
||||
Anchor, AnchorRangeExt, CodeActionProvider, Editor, EditorEvent, ExcerptId, ExcerptRange,
|
||||
GutterDimensions, MultiBuffer, MultiBufferSnapshot, ToOffset as _, ToPoint,
|
||||
};
|
||||
use feature_flags::{Assistant2FeatureFlag, FeatureFlagViewExt as _};
|
||||
use fs::Fs;
|
||||
use util::ResultExt;
|
||||
|
||||
@@ -54,16 +53,7 @@ pub fn init(
|
||||
let workspace = cx.view().clone();
|
||||
InlineAssistant::update_global(cx, |inline_assistant, cx| {
|
||||
inline_assistant.register_workspace(&workspace, cx)
|
||||
});
|
||||
|
||||
cx.observe_flag::<Assistant2FeatureFlag, _>({
|
||||
|is_assistant2_enabled, _view, cx| {
|
||||
InlineAssistant::update_global(cx, |inline_assistant, _cx| {
|
||||
inline_assistant.is_assistant2_enabled = is_assistant2_enabled;
|
||||
});
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
@@ -86,7 +76,6 @@ pub struct InlineAssistant {
|
||||
prompt_builder: Arc<PromptBuilder>,
|
||||
telemetry: Arc<Telemetry>,
|
||||
fs: Arc<dyn Fs>,
|
||||
is_assistant2_enabled: bool,
|
||||
}
|
||||
|
||||
impl Global for InlineAssistant {}
|
||||
@@ -108,7 +97,6 @@ impl InlineAssistant {
|
||||
prompt_builder,
|
||||
telemetry,
|
||||
fs,
|
||||
is_assistant2_enabled: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,31 +157,21 @@ impl InlineAssistant {
|
||||
item: &dyn ItemHandle,
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
let is_assistant2_enabled = self.is_assistant2_enabled;
|
||||
|
||||
if let Some(editor) = item.act_as::<Editor>(cx) {
|
||||
editor.update(cx, |editor, cx| {
|
||||
if is_assistant2_enabled {
|
||||
let thread_store = workspace
|
||||
.read(cx)
|
||||
.panel::<AssistantPanel>(cx)
|
||||
.map(|assistant_panel| assistant_panel.read(cx).thread_store().downgrade());
|
||||
let thread_store = workspace
|
||||
.read(cx)
|
||||
.panel::<AssistantPanel>(cx)
|
||||
.map(|assistant_panel| assistant_panel.read(cx).thread_store().downgrade());
|
||||
|
||||
editor.add_code_action_provider(
|
||||
Rc::new(AssistantCodeActionProvider {
|
||||
editor: cx.view().downgrade(),
|
||||
workspace: workspace.downgrade(),
|
||||
thread_store,
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
|
||||
// Remove the Assistant1 code action provider, as it still might be registered.
|
||||
editor.remove_code_action_provider("assistant".into(), cx);
|
||||
} else {
|
||||
editor
|
||||
.remove_code_action_provider(ASSISTANT_CODE_ACTION_PROVIDER_ID.into(), cx);
|
||||
}
|
||||
editor.push_code_action_provider(
|
||||
Rc::new(AssistantCodeActionProvider {
|
||||
editor: cx.view().downgrade(),
|
||||
workspace: workspace.downgrade(),
|
||||
thread_store,
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -357,7 +335,7 @@ impl InlineAssistant {
|
||||
let mut assist_to_focus = None;
|
||||
for range in codegen_ranges {
|
||||
let assist_id = self.next_assist_id.post_inc();
|
||||
let context_store = cx.new_model(|_cx| ContextStore::new());
|
||||
let context_store = cx.new_model(|_cx| ContextStore::new(workspace.clone()));
|
||||
let codegen = cx.new_model(|cx| {
|
||||
BufferCodegen::new(
|
||||
editor.read(cx).buffer().clone(),
|
||||
@@ -467,7 +445,7 @@ impl InlineAssistant {
|
||||
range.end = range.end.bias_right(&snapshot);
|
||||
}
|
||||
|
||||
let context_store = cx.new_model(|_cx| ContextStore::new());
|
||||
let context_store = cx.new_model(|_cx| ContextStore::new(workspace.clone()));
|
||||
|
||||
let codegen = cx.new_model(|cx| {
|
||||
BufferCodegen::new(
|
||||
@@ -1595,13 +1573,7 @@ struct AssistantCodeActionProvider {
|
||||
thread_store: Option<WeakModel<ThreadStore>>,
|
||||
}
|
||||
|
||||
const ASSISTANT_CODE_ACTION_PROVIDER_ID: &str = "assistant2";
|
||||
|
||||
impl CodeActionProvider for AssistantCodeActionProvider {
|
||||
fn id(&self) -> Arc<str> {
|
||||
ASSISTANT_CODE_ACTION_PROVIDER_ID.into()
|
||||
}
|
||||
|
||||
fn code_actions(
|
||||
&self,
|
||||
buffer: &Model<Buffer>,
|
||||
|
||||
@@ -47,7 +47,7 @@ impl MessageEditor {
|
||||
thread: Model<Thread>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let context_store = cx.new_model(|_cx| ContextStore::new());
|
||||
let context_store = cx.new_model(|_cx| ContextStore::new(workspace.clone()));
|
||||
let context_picker_menu_handle = PopoverMenuHandle::default();
|
||||
let inline_context_picker_menu_handle = PopoverMenuHandle::default();
|
||||
let model_selector_menu_handle = PopoverMenuHandle::default();
|
||||
@@ -147,11 +147,10 @@ impl MessageEditor {
|
||||
editor.clear(cx);
|
||||
text
|
||||
});
|
||||
let context = self
|
||||
.context_store
|
||||
.update(cx, |this, _cx| this.context().clone());
|
||||
|
||||
self.thread.update(cx, |thread, cx| {
|
||||
let thread = self.thread.clone();
|
||||
thread.update(cx, |thread, cx| {
|
||||
let context = self.context_store.read(cx).snapshot(cx).collect::<Vec<_>>();
|
||||
thread.insert_user_message(user_message, context, cx);
|
||||
let mut request = thread.to_completion_request(request_kind, cx);
|
||||
|
||||
|
||||
@@ -78,7 +78,7 @@ impl TerminalInlineAssistant {
|
||||
let prompt_buffer = cx.new_model(|cx| {
|
||||
MultiBuffer::singleton(cx.new_model(|cx| Buffer::local(String::new(), cx)), cx)
|
||||
});
|
||||
let context_store = cx.new_model(|_cx| ContextStore::new());
|
||||
let context_store = cx.new_model(|_cx| ContextStore::new(workspace.clone()));
|
||||
let codegen = cx.new_model(|_| TerminalCodegen::new(terminal, self.telemetry.clone()));
|
||||
|
||||
let prompt_editor = cx.new_view(|cx| {
|
||||
@@ -245,10 +245,10 @@ impl TerminalInlineAssistant {
|
||||
cache: false,
|
||||
};
|
||||
|
||||
let context = assist
|
||||
.context_store
|
||||
.update(cx, |this, _cx| this.context().clone());
|
||||
attach_context_to_message(&mut request_message, context);
|
||||
attach_context_to_message(
|
||||
&mut request_message,
|
||||
assist.context_store.read(cx).snapshot(cx),
|
||||
);
|
||||
|
||||
request_message.content.push(prompt.into());
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::sync::Arc;
|
||||
use anyhow::Result;
|
||||
use assistant_tool::ToolWorkingSet;
|
||||
use chrono::{DateTime, Utc};
|
||||
use collections::{HashMap, HashSet};
|
||||
use collections::{BTreeMap, HashMap, HashSet};
|
||||
use futures::future::Shared;
|
||||
use futures::{FutureExt as _, StreamExt as _};
|
||||
use gpui::{AppContext, EventEmitter, ModelContext, SharedString, Task};
|
||||
@@ -17,7 +17,7 @@ use serde::{Deserialize, Serialize};
|
||||
use util::{post_inc, TryFutureExt as _};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::context::{attach_context_to_message, Context, ContextId};
|
||||
use crate::context::{attach_context_to_message, ContextId, ContextSnapshot};
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum RequestKind {
|
||||
@@ -64,7 +64,7 @@ pub struct Thread {
|
||||
pending_summary: Task<Option<()>>,
|
||||
messages: Vec<Message>,
|
||||
next_message_id: MessageId,
|
||||
context: HashMap<ContextId, Context>,
|
||||
context: BTreeMap<ContextId, ContextSnapshot>,
|
||||
context_by_message: HashMap<MessageId, Vec<ContextId>>,
|
||||
completion_count: usize,
|
||||
pending_completions: Vec<PendingCompletion>,
|
||||
@@ -83,7 +83,7 @@ impl Thread {
|
||||
pending_summary: Task::ready(None),
|
||||
messages: Vec::new(),
|
||||
next_message_id: MessageId(0),
|
||||
context: HashMap::default(),
|
||||
context: BTreeMap::default(),
|
||||
context_by_message: HashMap::default(),
|
||||
completion_count: 0,
|
||||
pending_completions: Vec::new(),
|
||||
@@ -131,7 +131,7 @@ impl Thread {
|
||||
&self.tools
|
||||
}
|
||||
|
||||
pub fn context_for_message(&self, id: MessageId) -> Option<Vec<Context>> {
|
||||
pub fn context_for_message(&self, id: MessageId) -> Option<Vec<ContextSnapshot>> {
|
||||
let context = self.context_by_message.get(&id)?;
|
||||
Some(
|
||||
context
|
||||
@@ -149,7 +149,7 @@ impl Thread {
|
||||
pub fn insert_user_message(
|
||||
&mut self,
|
||||
text: impl Into<String>,
|
||||
context: Vec<Context>,
|
||||
context: Vec<ContextSnapshot>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
let message_id = self.insert_message(Role::User, text, cx);
|
||||
|
||||
@@ -3,12 +3,12 @@ use std::rc::Rc;
|
||||
use gpui::ClickEvent;
|
||||
use ui::{prelude::*, IconButtonShape, Tooltip};
|
||||
|
||||
use crate::context::{Context, ContextKind};
|
||||
use crate::context::{ContextKind, ContextSnapshot};
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub enum ContextPill {
|
||||
Added {
|
||||
context: Context,
|
||||
context: ContextSnapshot,
|
||||
dupe_name: bool,
|
||||
on_remove: Option<Rc<dyn Fn(&ClickEvent, &mut WindowContext)>>,
|
||||
},
|
||||
@@ -21,7 +21,7 @@ pub enum ContextPill {
|
||||
|
||||
impl ContextPill {
|
||||
pub fn new_added(
|
||||
context: Context,
|
||||
context: ContextSnapshot,
|
||||
dupe_name: bool,
|
||||
on_remove: Option<Rc<dyn Fn(&ClickEvent, &mut WindowContext)>>,
|
||||
) -> Self {
|
||||
@@ -49,10 +49,10 @@ impl ContextPill {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn kind(&self) -> &ContextKind {
|
||||
pub fn kind(&self) -> ContextKind {
|
||||
match self {
|
||||
Self::Added { context, .. } => &context.kind,
|
||||
Self::Suggested { kind, .. } => kind,
|
||||
Self::Added { context, .. } => context.kind,
|
||||
Self::Suggested { kind, .. } => *kind,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,10 +19,7 @@ use tempfile::NamedTempFile;
|
||||
use util::paths::PathWithPosition;
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||
use {
|
||||
std::io::IsTerminal,
|
||||
util::{load_login_shell_environment, load_shell_from_passwd, ResultExt},
|
||||
};
|
||||
use std::io::IsTerminal;
|
||||
|
||||
struct Detect;
|
||||
|
||||
@@ -167,15 +164,24 @@ fn main() -> Result<()> {
|
||||
None
|
||||
};
|
||||
|
||||
// On Linux, desktop entry uses `cli` to spawn `zed`, so we need to load env vars from the shell
|
||||
// since it doesn't inherit env vars from the terminal.
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||
if !std::io::stdout().is_terminal() {
|
||||
load_shell_from_passwd().log_err();
|
||||
load_login_shell_environment().log_err();
|
||||
}
|
||||
let env = {
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||
{
|
||||
// On Linux, the desktop entry uses `cli` to spawn `zed`.
|
||||
// We need to handle env vars correctly since std::env::vars() may not contain
|
||||
// project-specific vars (e.g. those set by direnv).
|
||||
// By setting env to None here, the LSP will use worktree env vars instead,
|
||||
// which is what we want.
|
||||
if !std::io::stdout().is_terminal() {
|
||||
None
|
||||
} else {
|
||||
Some(std::env::vars().collect::<HashMap<_, _>>())
|
||||
}
|
||||
}
|
||||
|
||||
let env = Some(std::env::vars().collect::<HashMap<_, _>>());
|
||||
#[cfg(not(any(target_os = "linux", target_os = "freebsd")))]
|
||||
Some(std::env::vars().collect::<HashMap<_, _>>())
|
||||
};
|
||||
|
||||
let exit_status = Arc::new(Mutex::new(None));
|
||||
let mut paths = vec![];
|
||||
|
||||
@@ -34,7 +34,6 @@ collections.workspace = true
|
||||
dashmap.workspace = true
|
||||
derive_more.workspace = true
|
||||
envy = "0.4.2"
|
||||
fireworks.workspace = true
|
||||
futures.workspace = true
|
||||
google_ai.workspace = true
|
||||
hex.workspace = true
|
||||
|
||||
@@ -438,7 +438,8 @@ CREATE TABLE IF NOT EXISTS billing_subscriptions (
|
||||
billing_customer_id INTEGER NOT NULL REFERENCES billing_customers(id),
|
||||
stripe_subscription_id TEXT NOT NULL,
|
||||
stripe_subscription_status TEXT NOT NULL,
|
||||
stripe_cancel_at TIMESTAMP
|
||||
stripe_cancel_at TIMESTAMP,
|
||||
stripe_cancellation_reason TEXT
|
||||
);
|
||||
|
||||
CREATE INDEX "ix_billing_subscriptions_on_billing_customer_id" ON billing_subscriptions (billing_customer_id);
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
alter table billing_subscriptions
|
||||
add column stripe_cancellation_reason text;
|
||||
@@ -12,8 +12,8 @@ use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use std::{str::FromStr, sync::Arc, time::Duration};
|
||||
use stripe::{
|
||||
BillingPortalSession, CreateBillingPortalSession, CreateBillingPortalSessionFlowData,
|
||||
CreateBillingPortalSessionFlowDataAfterCompletion,
|
||||
BillingPortalSession, CancellationDetailsReason, CreateBillingPortalSession,
|
||||
CreateBillingPortalSessionFlowData, CreateBillingPortalSessionFlowDataAfterCompletion,
|
||||
CreateBillingPortalSessionFlowDataAfterCompletionRedirect,
|
||||
CreateBillingPortalSessionFlowDataType, CreateCustomer, Customer, CustomerId, EventObject,
|
||||
EventType, Expandable, ListEvents, Subscription, SubscriptionId, SubscriptionStatus,
|
||||
@@ -21,8 +21,10 @@ use stripe::{
|
||||
use util::ResultExt;
|
||||
|
||||
use crate::api::events::SnowflakeRow;
|
||||
use crate::db::billing_subscription::{StripeCancellationReason, StripeSubscriptionStatus};
|
||||
use crate::llm::{DEFAULT_MAX_MONTHLY_SPEND, FREE_TIER_MONTHLY_SPENDING_LIMIT};
|
||||
use crate::rpc::{ResultExt as _, Server};
|
||||
use crate::{db::UserId, llm::db::LlmDatabase};
|
||||
use crate::{
|
||||
db::{
|
||||
billing_customer, BillingSubscriptionId, CreateBillingCustomerParams,
|
||||
@@ -32,10 +34,6 @@ use crate::{
|
||||
},
|
||||
stripe_billing::StripeBilling,
|
||||
};
|
||||
use crate::{
|
||||
db::{billing_subscription::StripeSubscriptionStatus, UserId},
|
||||
llm::db::LlmDatabase,
|
||||
};
|
||||
use crate::{AppState, Cents, Error, Result};
|
||||
|
||||
pub fn router() -> Router {
|
||||
@@ -251,6 +249,13 @@ async fn create_billing_subscription(
|
||||
));
|
||||
}
|
||||
|
||||
if app.db.has_overdue_billing_subscriptions(user.id).await? {
|
||||
return Err(Error::http(
|
||||
StatusCode::PAYMENT_REQUIRED,
|
||||
"user has overdue billing subscriptions".into(),
|
||||
));
|
||||
}
|
||||
|
||||
let customer_id =
|
||||
if let Some(existing_customer) = app.db.get_billing_customer_by_user_id(user.id).await? {
|
||||
CustomerId::from_str(&existing_customer.stripe_customer_id)
|
||||
@@ -679,6 +684,12 @@ async fn handle_customer_subscription_event(
|
||||
.and_then(|cancel_at| DateTime::from_timestamp(cancel_at, 0))
|
||||
.map(|time| time.naive_utc()),
|
||||
),
|
||||
stripe_cancellation_reason: ActiveValue::set(
|
||||
subscription
|
||||
.cancellation_details
|
||||
.and_then(|details| details.reason)
|
||||
.map(|reason| reason.into()),
|
||||
),
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
@@ -715,6 +726,10 @@ async fn handle_customer_subscription_event(
|
||||
billing_customer_id: billing_customer.id,
|
||||
stripe_subscription_id: subscription.id.to_string(),
|
||||
stripe_subscription_status: subscription.status.into(),
|
||||
stripe_cancellation_reason: subscription
|
||||
.cancellation_details
|
||||
.and_then(|details| details.reason)
|
||||
.map(|reason| reason.into()),
|
||||
})
|
||||
.await?;
|
||||
}
|
||||
@@ -791,6 +806,16 @@ impl From<SubscriptionStatus> for StripeSubscriptionStatus {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CancellationDetailsReason> for StripeCancellationReason {
|
||||
fn from(value: CancellationDetailsReason) -> Self {
|
||||
match value {
|
||||
CancellationDetailsReason::CancellationRequested => Self::CancellationRequested,
|
||||
CancellationDetailsReason::PaymentDisputed => Self::PaymentDisputed,
|
||||
CancellationDetailsReason::PaymentFailed => Self::PaymentFailed,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Finds or creates a billing customer using the provided customer.
|
||||
async fn find_or_create_billing_customer(
|
||||
app: &Arc<AppState>,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::db::billing_subscription::StripeSubscriptionStatus;
|
||||
use crate::db::billing_subscription::{StripeCancellationReason, StripeSubscriptionStatus};
|
||||
|
||||
use super::*;
|
||||
|
||||
@@ -7,6 +7,7 @@ pub struct CreateBillingSubscriptionParams {
|
||||
pub billing_customer_id: BillingCustomerId,
|
||||
pub stripe_subscription_id: String,
|
||||
pub stripe_subscription_status: StripeSubscriptionStatus,
|
||||
pub stripe_cancellation_reason: Option<StripeCancellationReason>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
@@ -15,6 +16,7 @@ pub struct UpdateBillingSubscriptionParams {
|
||||
pub stripe_subscription_id: ActiveValue<String>,
|
||||
pub stripe_subscription_status: ActiveValue<StripeSubscriptionStatus>,
|
||||
pub stripe_cancel_at: ActiveValue<Option<DateTime>>,
|
||||
pub stripe_cancellation_reason: ActiveValue<Option<StripeCancellationReason>>,
|
||||
}
|
||||
|
||||
impl Database {
|
||||
@@ -28,6 +30,7 @@ impl Database {
|
||||
billing_customer_id: ActiveValue::set(params.billing_customer_id),
|
||||
stripe_subscription_id: ActiveValue::set(params.stripe_subscription_id.clone()),
|
||||
stripe_subscription_status: ActiveValue::set(params.stripe_subscription_status),
|
||||
stripe_cancellation_reason: ActiveValue::set(params.stripe_cancellation_reason),
|
||||
..Default::default()
|
||||
})
|
||||
.exec_without_returning(&*tx)
|
||||
@@ -51,6 +54,7 @@ impl Database {
|
||||
stripe_subscription_id: params.stripe_subscription_id.clone(),
|
||||
stripe_subscription_status: params.stripe_subscription_status.clone(),
|
||||
stripe_cancel_at: params.stripe_cancel_at.clone(),
|
||||
stripe_cancellation_reason: params.stripe_cancellation_reason.clone(),
|
||||
..Default::default()
|
||||
})
|
||||
.exec(&*tx)
|
||||
@@ -166,4 +170,40 @@ impl Database {
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
/// Returns whether the user has any overdue billing subscriptions.
|
||||
pub async fn has_overdue_billing_subscriptions(&self, user_id: UserId) -> Result<bool> {
|
||||
Ok(self.count_overdue_billing_subscriptions(user_id).await? > 0)
|
||||
}
|
||||
|
||||
/// Returns the count of the overdue billing subscriptions for the user with the specified ID.
|
||||
///
|
||||
/// This includes subscriptions:
|
||||
/// - Whose status is `past_due`
|
||||
/// - Whose status is `canceled` and the cancellation reason is `payment_failed`
|
||||
pub async fn count_overdue_billing_subscriptions(&self, user_id: UserId) -> Result<usize> {
|
||||
self.transaction(|tx| async move {
|
||||
let past_due = billing_subscription::Column::StripeSubscriptionStatus
|
||||
.eq(StripeSubscriptionStatus::PastDue);
|
||||
let payment_failed = billing_subscription::Column::StripeSubscriptionStatus
|
||||
.eq(StripeSubscriptionStatus::Canceled)
|
||||
.and(
|
||||
billing_subscription::Column::StripeCancellationReason
|
||||
.eq(StripeCancellationReason::PaymentFailed),
|
||||
);
|
||||
|
||||
let count = billing_subscription::Entity::find()
|
||||
.inner_join(billing_customer::Entity)
|
||||
.filter(
|
||||
billing_customer::Column::UserId
|
||||
.eq(user_id)
|
||||
.and(past_due.or(payment_failed)),
|
||||
)
|
||||
.count(&*tx)
|
||||
.await?;
|
||||
|
||||
Ok(count as usize)
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ pub struct Model {
|
||||
pub stripe_subscription_id: String,
|
||||
pub stripe_subscription_status: StripeSubscriptionStatus,
|
||||
pub stripe_cancel_at: Option<DateTime>,
|
||||
pub stripe_cancellation_reason: Option<StripeCancellationReason>,
|
||||
pub created_at: DateTime,
|
||||
}
|
||||
|
||||
@@ -73,3 +74,18 @@ impl StripeSubscriptionStatus {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The cancellation reason for a Stripe subscription.
|
||||
///
|
||||
/// [Stripe docs](https://docs.stripe.com/api/subscriptions/object#subscription_object-cancellation_details-reason)
|
||||
#[derive(Eq, PartialEq, Copy, Clone, Debug, EnumIter, DeriveActiveEnum, Hash, Serialize)]
|
||||
#[sea_orm(rs_type = "String", db_type = "String(StringLen::None)")]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum StripeCancellationReason {
|
||||
#[sea_orm(string_value = "cancellation_requested")]
|
||||
CancellationRequested,
|
||||
#[sea_orm(string_value = "payment_disputed")]
|
||||
PaymentDisputed,
|
||||
#[sea_orm(string_value = "payment_failed")]
|
||||
PaymentFailed,
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::db::billing_subscription::StripeSubscriptionStatus;
|
||||
use crate::db::billing_subscription::{StripeCancellationReason, StripeSubscriptionStatus};
|
||||
use crate::db::tests::new_test_user;
|
||||
use crate::db::{CreateBillingCustomerParams, CreateBillingSubscriptionParams};
|
||||
use crate::test_both_dbs;
|
||||
@@ -41,6 +41,7 @@ async fn test_get_active_billing_subscriptions(db: &Arc<Database>) {
|
||||
billing_customer_id: customer.id,
|
||||
stripe_subscription_id: "sub_active_user".into(),
|
||||
stripe_subscription_status: StripeSubscriptionStatus::Active,
|
||||
stripe_cancellation_reason: None,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -75,6 +76,7 @@ async fn test_get_active_billing_subscriptions(db: &Arc<Database>) {
|
||||
billing_customer_id: customer.id,
|
||||
stripe_subscription_id: "sub_past_due_user".into(),
|
||||
stripe_subscription_status: StripeSubscriptionStatus::PastDue,
|
||||
stripe_cancellation_reason: None,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -86,3 +88,113 @@ async fn test_get_active_billing_subscriptions(db: &Arc<Database>) {
|
||||
assert_eq!(subscription_count, 0);
|
||||
}
|
||||
}
|
||||
|
||||
test_both_dbs!(
|
||||
test_count_overdue_billing_subscriptions,
|
||||
test_count_overdue_billing_subscriptions_postgres,
|
||||
test_count_overdue_billing_subscriptions_sqlite
|
||||
);
|
||||
|
||||
async fn test_count_overdue_billing_subscriptions(db: &Arc<Database>) {
|
||||
// A user with no subscription has no overdue billing subscriptions.
|
||||
{
|
||||
let user_id = new_test_user(db, "no-subscription-user@example.com").await;
|
||||
let subscription_count = db
|
||||
.count_overdue_billing_subscriptions(user_id)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(subscription_count, 0);
|
||||
}
|
||||
|
||||
// A user with a past-due subscription has an overdue billing subscription.
|
||||
{
|
||||
let user_id = new_test_user(db, "past-due-user@example.com").await;
|
||||
let customer = db
|
||||
.create_billing_customer(&CreateBillingCustomerParams {
|
||||
user_id,
|
||||
stripe_customer_id: "cus_past_due_user".into(),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(customer.stripe_customer_id, "cus_past_due_user".to_string());
|
||||
|
||||
db.create_billing_subscription(&CreateBillingSubscriptionParams {
|
||||
billing_customer_id: customer.id,
|
||||
stripe_subscription_id: "sub_past_due_user".into(),
|
||||
stripe_subscription_status: StripeSubscriptionStatus::PastDue,
|
||||
stripe_cancellation_reason: None,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let subscription_count = db
|
||||
.count_overdue_billing_subscriptions(user_id)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(subscription_count, 1);
|
||||
}
|
||||
|
||||
// A user with a canceled subscription with a reason of `payment_failed` has an overdue billing subscription.
|
||||
{
|
||||
let user_id =
|
||||
new_test_user(db, "canceled-subscription-payment-failed-user@example.com").await;
|
||||
let customer = db
|
||||
.create_billing_customer(&CreateBillingCustomerParams {
|
||||
user_id,
|
||||
stripe_customer_id: "cus_canceled_subscription_payment_failed_user".into(),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
customer.stripe_customer_id,
|
||||
"cus_canceled_subscription_payment_failed_user".to_string()
|
||||
);
|
||||
|
||||
db.create_billing_subscription(&CreateBillingSubscriptionParams {
|
||||
billing_customer_id: customer.id,
|
||||
stripe_subscription_id: "sub_canceled_subscription_payment_failed_user".into(),
|
||||
stripe_subscription_status: StripeSubscriptionStatus::Canceled,
|
||||
stripe_cancellation_reason: Some(StripeCancellationReason::PaymentFailed),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let subscription_count = db
|
||||
.count_overdue_billing_subscriptions(user_id)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(subscription_count, 1);
|
||||
}
|
||||
|
||||
// A user with a canceled subscription with a reason of `cancellation_requested` has no overdue billing subscriptions.
|
||||
{
|
||||
let user_id = new_test_user(db, "canceled-subscription-user@example.com").await;
|
||||
let customer = db
|
||||
.create_billing_customer(&CreateBillingCustomerParams {
|
||||
user_id,
|
||||
stripe_customer_id: "cus_canceled_subscription_user".into(),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
customer.stripe_customer_id,
|
||||
"cus_canceled_subscription_user".to_string()
|
||||
);
|
||||
|
||||
db.create_billing_subscription(&CreateBillingSubscriptionParams {
|
||||
billing_customer_id: customer.id,
|
||||
stripe_subscription_id: "sub_canceled_subscription_user".into(),
|
||||
stripe_subscription_status: StripeSubscriptionStatus::Canceled,
|
||||
stripe_cancellation_reason: Some(StripeCancellationReason::CancellationRequested),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let subscription_count = db
|
||||
.count_overdue_billing_subscriptions(user_id)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(subscription_count, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -440,11 +440,8 @@ async fn predict_edits(
|
||||
_country_code_header: Option<TypedHeader<CloudflareIpCountryHeader>>,
|
||||
Json(params): Json<PredictEditsParams>,
|
||||
) -> Result<impl IntoResponse> {
|
||||
if !claims.is_staff && !claims.has_predict_edits_feature_flag {
|
||||
return Err(Error::http(
|
||||
StatusCode::FORBIDDEN,
|
||||
"no access to Zed's edit prediction feature".to_string(),
|
||||
));
|
||||
if !claims.is_staff {
|
||||
return Err(anyhow!("not found"))?;
|
||||
}
|
||||
|
||||
let api_url = state
|
||||
@@ -473,55 +470,26 @@ async fn predict_edits(
|
||||
.replace("<outline>", &outline_prefix)
|
||||
.replace("<events>", ¶ms.input_events)
|
||||
.replace("<excerpt>", ¶ms.input_excerpt);
|
||||
|
||||
let request_start = std::time::Instant::now();
|
||||
let mut response = fireworks::complete(
|
||||
let mut response = open_ai::complete_text(
|
||||
&state.http_client,
|
||||
api_url,
|
||||
api_key,
|
||||
fireworks::CompletionRequest {
|
||||
open_ai::CompletionRequest {
|
||||
model: model.to_string(),
|
||||
prompt: prompt.clone(),
|
||||
max_tokens: 2048,
|
||||
max_tokens: 1024,
|
||||
temperature: 0.,
|
||||
prediction: Some(fireworks::Prediction::Content {
|
||||
prediction: Some(open_ai::Prediction::Content {
|
||||
content: params.input_excerpt,
|
||||
}),
|
||||
rewrite_speculation: Some(true),
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
let duration = request_start.elapsed();
|
||||
|
||||
let choice = response
|
||||
.completion
|
||||
.choices
|
||||
.pop()
|
||||
.context("no output from completion response")?;
|
||||
|
||||
state.executor.spawn_detached({
|
||||
let kinesis_client = state.kinesis_client.clone();
|
||||
let kinesis_stream = state.config.kinesis_stream.clone();
|
||||
let model = model.clone();
|
||||
async move {
|
||||
SnowflakeRow::new(
|
||||
"Fireworks Completion Requested",
|
||||
claims.metrics_id,
|
||||
claims.is_staff,
|
||||
claims.system_id.clone(),
|
||||
json!({
|
||||
"model": model.to_string(),
|
||||
"headers": response.headers,
|
||||
"usage": response.completion.usage,
|
||||
"duration": duration.as_secs_f64(),
|
||||
}),
|
||||
)
|
||||
.write(&kinesis_client, &kinesis_stream)
|
||||
.await
|
||||
.log_err();
|
||||
}
|
||||
});
|
||||
|
||||
Ok(Json(PredictEditsResponse {
|
||||
output_excerpt: choice.text,
|
||||
}))
|
||||
|
||||
@@ -22,8 +22,6 @@ pub struct LlmTokenClaims {
|
||||
pub github_user_login: String,
|
||||
pub is_staff: bool,
|
||||
pub has_llm_closed_beta_feature_flag: bool,
|
||||
#[serde(default)]
|
||||
pub has_predict_edits_feature_flag: bool,
|
||||
pub has_llm_subscription: bool,
|
||||
pub max_monthly_spend_in_cents: u32,
|
||||
pub custom_llm_monthly_allowance_in_cents: Option<u32>,
|
||||
@@ -39,7 +37,6 @@ impl LlmTokenClaims {
|
||||
is_staff: bool,
|
||||
billing_preferences: Option<billing_preference::Model>,
|
||||
has_llm_closed_beta_feature_flag: bool,
|
||||
has_predict_edits_feature_flag: bool,
|
||||
has_llm_subscription: bool,
|
||||
plan: rpc::proto::Plan,
|
||||
system_id: Option<String>,
|
||||
@@ -61,7 +58,6 @@ impl LlmTokenClaims {
|
||||
github_user_login: user.github_login.clone(),
|
||||
is_staff,
|
||||
has_llm_closed_beta_feature_flag,
|
||||
has_predict_edits_feature_flag,
|
||||
has_llm_subscription,
|
||||
max_monthly_spend_in_cents: billing_preferences
|
||||
.map_or(DEFAULT_MAX_MONTHLY_SPEND.0, |preferences| {
|
||||
|
||||
@@ -4025,7 +4025,6 @@ async fn get_llm_api_token(
|
||||
let flags = db.get_user_flags(session.user_id()).await?;
|
||||
let has_language_models_feature_flag = flags.iter().any(|flag| flag == "language-models");
|
||||
let has_llm_closed_beta_feature_flag = flags.iter().any(|flag| flag == "llm-closed-beta");
|
||||
let has_predict_edits_feature_flag = flags.iter().any(|flag| flag == "predict-edits");
|
||||
|
||||
if !session.is_staff() && !has_language_models_feature_flag {
|
||||
Err(anyhow!("permission denied"))?
|
||||
@@ -4062,7 +4061,6 @@ async fn get_llm_api_token(
|
||||
session.is_staff(),
|
||||
billing_preferences,
|
||||
has_llm_closed_beta_feature_flag,
|
||||
has_predict_edits_feature_flag,
|
||||
has_llm_subscription,
|
||||
session.current_plan(&db).await?,
|
||||
session.system_id.clone(),
|
||||
|
||||
@@ -18,7 +18,6 @@ collections.workspace = true
|
||||
ctor.workspace = true
|
||||
editor.workspace = true
|
||||
env_logger.workspace = true
|
||||
feature_flags.workspace = true
|
||||
gpui.workspace = true
|
||||
language.workspace = true
|
||||
log.workspace = true
|
||||
|
||||
@@ -14,7 +14,6 @@ use editor::{
|
||||
scroll::Autoscroll,
|
||||
Editor, EditorEvent, ExcerptId, ExcerptRange, MultiBuffer, ToOffset,
|
||||
};
|
||||
use feature_flags::FeatureFlagAppExt;
|
||||
use gpui::{
|
||||
actions, div, svg, AnyElement, AnyView, AppContext, Context, EventEmitter, FocusHandle,
|
||||
FocusableView, Global, HighlightStyle, InteractiveElement, IntoElement, Model, ParentElement,
|
||||
@@ -933,18 +932,16 @@ fn context_range_for_entry(
|
||||
snapshot: &BufferSnapshot,
|
||||
cx: &AppContext,
|
||||
) -> Range<Point> {
|
||||
if cx.is_staff() {
|
||||
if let Some(rows) = heuristic_syntactic_expand(
|
||||
entry.range.clone(),
|
||||
DIAGNOSTIC_EXPANSION_ROW_LIMIT,
|
||||
snapshot,
|
||||
cx,
|
||||
) {
|
||||
return Range {
|
||||
start: Point::new(*rows.start(), 0),
|
||||
end: snapshot.clip_point(Point::new(*rows.end(), u32::MAX), Bias::Left),
|
||||
};
|
||||
}
|
||||
if let Some(rows) = heuristic_syntactic_expand(
|
||||
entry.range.clone(),
|
||||
DIAGNOSTIC_EXPANSION_ROW_LIMIT,
|
||||
snapshot,
|
||||
cx,
|
||||
) {
|
||||
return Range {
|
||||
start: Point::new(*rows.start(), 0),
|
||||
end: snapshot.clip_point(Point::new(*rows.end(), u32::MAX), Bias::Left),
|
||||
};
|
||||
}
|
||||
Range {
|
||||
start: Point::new(entry.range.start.row.saturating_sub(context), 0),
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||
use gpui::{
|
||||
div, pulsating_between, px, uniform_list, Animation, AnimationExt, AnyElement,
|
||||
BackgroundExecutor, Div, FontWeight, ListSizingBehavior, Model, ScrollStrategy, SharedString,
|
||||
Size, StrikethroughStyle, StyledText, UniformListScrollHandle, ViewContext, WeakView,
|
||||
div, px, uniform_list, AnyElement, BackgroundExecutor, Div, FontWeight, ListSizingBehavior,
|
||||
Model, ScrollStrategy, SharedString, Size, StrikethroughStyle, StyledText,
|
||||
UniformListScrollHandle, ViewContext, WeakView,
|
||||
};
|
||||
use language::Buffer;
|
||||
use language::{CodeLabel, Documentation};
|
||||
@@ -10,8 +10,6 @@ use lsp::LanguageServerId;
|
||||
use multi_buffer::{Anchor, ExcerptId};
|
||||
use ordered_float::OrderedFloat;
|
||||
use project::{CodeAction, Completion, TaskSourceKind};
|
||||
use settings::Settings;
|
||||
use std::time::Duration;
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
cmp::{min, Reverse},
|
||||
@@ -335,6 +333,9 @@ impl CompletionsMenu {
|
||||
entries[0] = hint;
|
||||
}
|
||||
_ => {
|
||||
if self.selected_item != 0 {
|
||||
self.selected_item += 1;
|
||||
}
|
||||
entries.insert(0, hint);
|
||||
}
|
||||
}
|
||||
@@ -462,9 +463,10 @@ impl CompletionsMenu {
|
||||
|
||||
len
|
||||
}
|
||||
CompletionEntry::InlineCompletionHint(hint) => {
|
||||
"Zed AI / ".chars().count() + hint.label().chars().count()
|
||||
}
|
||||
CompletionEntry::InlineCompletionHint(InlineCompletionMenuHint {
|
||||
provider_name,
|
||||
..
|
||||
}) => provider_name.len(),
|
||||
})
|
||||
.map(|(ix, _)| ix);
|
||||
drop(completions);
|
||||
@@ -488,12 +490,6 @@ impl CompletionsMenu {
|
||||
.enumerate()
|
||||
.map(|(ix, mat)| {
|
||||
let item_ix = start_ix + ix;
|
||||
let buffer_font = theme::ThemeSettings::get_global(cx).buffer_font.clone();
|
||||
let base_label = h_flex()
|
||||
.gap_1()
|
||||
.child(div().font(buffer_font.clone()).child("Zed AI"))
|
||||
.child(div().px_0p5().child("/").opacity(0.2));
|
||||
|
||||
match mat {
|
||||
CompletionEntry::Match(mat) => {
|
||||
let candidate_id = mat.candidate_id;
|
||||
@@ -577,57 +573,20 @@ impl CompletionsMenu {
|
||||
.end_slot::<Label>(documentation_label),
|
||||
)
|
||||
}
|
||||
CompletionEntry::InlineCompletionHint(
|
||||
hint @ InlineCompletionMenuHint::None,
|
||||
) => div().min_w(px(250.)).max_w(px(500.)).child(
|
||||
CompletionEntry::InlineCompletionHint(InlineCompletionMenuHint {
|
||||
provider_name,
|
||||
..
|
||||
}) => div().min_w(px(250.)).max_w(px(500.)).child(
|
||||
ListItem::new("inline-completion")
|
||||
.inset(true)
|
||||
.toggle_state(item_ix == selected_item)
|
||||
.start_slot(Icon::new(IconName::ZedPredict))
|
||||
.child(
|
||||
base_label.child(
|
||||
StyledText::new(hint.label())
|
||||
.with_highlights(&style.text, None),
|
||||
),
|
||||
),
|
||||
),
|
||||
CompletionEntry::InlineCompletionHint(
|
||||
hint @ InlineCompletionMenuHint::Loading,
|
||||
) => div().min_w(px(250.)).max_w(px(500.)).child(
|
||||
ListItem::new("inline-completion")
|
||||
.inset(true)
|
||||
.toggle_state(item_ix == selected_item)
|
||||
.start_slot(Icon::new(IconName::ZedPredict))
|
||||
.child(base_label.child({
|
||||
let text_style = style.text.clone();
|
||||
StyledText::new(hint.label())
|
||||
.with_highlights(&text_style, None)
|
||||
.with_animation(
|
||||
"pulsating-label",
|
||||
Animation::new(Duration::from_secs(1))
|
||||
.repeat()
|
||||
.with_easing(pulsating_between(0.4, 0.8)),
|
||||
move |text, delta| {
|
||||
let mut text_style = text_style.clone();
|
||||
text_style.color =
|
||||
text_style.color.opacity(delta);
|
||||
text.with_highlights(&text_style, None)
|
||||
},
|
||||
)
|
||||
})),
|
||||
),
|
||||
CompletionEntry::InlineCompletionHint(
|
||||
hint @ InlineCompletionMenuHint::Loaded { .. },
|
||||
) => div().min_w(px(250.)).max_w(px(500.)).child(
|
||||
ListItem::new("inline-completion")
|
||||
.inset(true)
|
||||
.toggle_state(item_ix == selected_item)
|
||||
.start_slot(Icon::new(IconName::ZedPredict))
|
||||
.child(
|
||||
base_label.child(
|
||||
StyledText::new(hint.label())
|
||||
.with_highlights(&style.text, None),
|
||||
),
|
||||
StyledText::new(format!(
|
||||
"{} Completion",
|
||||
SharedString::new_static(provider_name)
|
||||
))
|
||||
.with_highlights(&style.text, None),
|
||||
)
|
||||
.on_click(cx.listener(move |editor, _event, cx| {
|
||||
cx.stop_propagation();
|
||||
@@ -684,20 +643,19 @@ impl CompletionsMenu {
|
||||
Documentation::Undocumented => return None,
|
||||
}
|
||||
}
|
||||
CompletionEntry::InlineCompletionHint(InlineCompletionMenuHint::Loaded { text }) => {
|
||||
match text {
|
||||
InlineCompletionText::Edit { text, highlights } => div()
|
||||
.mx_1()
|
||||
.rounded_md()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.child(
|
||||
gpui::StyledText::new(text.clone())
|
||||
.with_highlights(&style.text, highlights.clone()),
|
||||
),
|
||||
InlineCompletionText::Move(text) => div().child(text.clone()),
|
||||
}
|
||||
}
|
||||
CompletionEntry::InlineCompletionHint(_) => return None,
|
||||
CompletionEntry::InlineCompletionHint(hint) => match &hint.text {
|
||||
InlineCompletionText::Edit { text, highlights } => div()
|
||||
.mx_1()
|
||||
.rounded(px(6.))
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.child(
|
||||
gpui::StyledText::new(text.clone())
|
||||
.with_highlights(&style.text, highlights.clone()),
|
||||
),
|
||||
InlineCompletionText::Move(text) => div().child(text.clone()),
|
||||
},
|
||||
};
|
||||
|
||||
Some(
|
||||
|
||||
@@ -459,21 +459,9 @@ pub fn make_suggestion_styles(cx: &WindowContext) -> InlineCompletionStyles {
|
||||
type CompletionId = usize;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum InlineCompletionMenuHint {
|
||||
Loading,
|
||||
Loaded { text: InlineCompletionText },
|
||||
None,
|
||||
}
|
||||
|
||||
impl InlineCompletionMenuHint {
|
||||
pub fn label(&self) -> &'static str {
|
||||
match self {
|
||||
InlineCompletionMenuHint::Loading | InlineCompletionMenuHint::Loaded { .. } => {
|
||||
"Edit Prediction"
|
||||
}
|
||||
InlineCompletionMenuHint::None => "No Prediction",
|
||||
}
|
||||
}
|
||||
struct InlineCompletionMenuHint {
|
||||
provider_name: &'static str,
|
||||
text: InlineCompletionText,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -1739,12 +1727,8 @@ impl Editor {
|
||||
self.input_enabled = input_enabled;
|
||||
}
|
||||
|
||||
pub fn set_inline_completions_enabled(&mut self, enabled: bool, cx: &mut ViewContext<Self>) {
|
||||
pub fn set_inline_completions_enabled(&mut self, enabled: bool) {
|
||||
self.enable_inline_completions = enabled;
|
||||
if !self.enable_inline_completions {
|
||||
self.take_active_inline_completion(cx);
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_autoindent(&mut self, autoindent: bool) {
|
||||
@@ -3835,26 +3819,6 @@ impl Editor {
|
||||
) -> Option<Task<std::result::Result<(), anyhow::Error>>> {
|
||||
use language::ToOffset as _;
|
||||
|
||||
{
|
||||
let context_menu = self.context_menu.borrow();
|
||||
if let CodeContextMenu::Completions(menu) = context_menu.as_ref()? {
|
||||
let entries = menu.entries.borrow();
|
||||
let entry = entries.get(item_ix.unwrap_or(menu.selected_item));
|
||||
match entry {
|
||||
Some(CompletionEntry::InlineCompletionHint(
|
||||
InlineCompletionMenuHint::Loading,
|
||||
)) => return Some(Task::ready(Ok(()))),
|
||||
Some(CompletionEntry::InlineCompletionHint(InlineCompletionMenuHint::None)) => {
|
||||
drop(entries);
|
||||
drop(context_menu);
|
||||
self.context_menu_next(&Default::default(), cx);
|
||||
return Some(Task::ready(Ok(())));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let completions_menu =
|
||||
if let CodeContextMenu::Completions(menu) = self.hide_context_menu(cx)? {
|
||||
menu
|
||||
@@ -3865,7 +3829,7 @@ impl Editor {
|
||||
let entries = completions_menu.entries.borrow();
|
||||
let mat = entries.get(item_ix.unwrap_or(completions_menu.selected_item))?;
|
||||
let mat = match mat {
|
||||
CompletionEntry::InlineCompletionHint(_) => {
|
||||
CompletionEntry::InlineCompletionHint { .. } => {
|
||||
self.accept_inline_completion(&AcceptInlineCompletion, cx);
|
||||
cx.stop_propagation();
|
||||
return Some(Task::ready(Ok(())));
|
||||
@@ -4330,29 +4294,15 @@ impl Editor {
|
||||
self.available_code_actions.take();
|
||||
}
|
||||
|
||||
pub fn add_code_action_provider(
|
||||
pub fn push_code_action_provider(
|
||||
&mut self,
|
||||
provider: Rc<dyn CodeActionProvider>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
if self
|
||||
.code_action_providers
|
||||
.iter()
|
||||
.any(|existing_provider| existing_provider.id() == provider.id())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
self.code_action_providers.push(provider);
|
||||
self.refresh_code_actions(cx);
|
||||
}
|
||||
|
||||
pub fn remove_code_action_provider(&mut self, id: Arc<str>, cx: &mut ViewContext<Self>) {
|
||||
self.code_action_providers
|
||||
.retain(|provider| provider.id() != id);
|
||||
self.refresh_code_actions(cx);
|
||||
}
|
||||
|
||||
fn refresh_code_actions(&mut self, cx: &mut ViewContext<Self>) -> Option<()> {
|
||||
let buffer = self.buffer.read(cx);
|
||||
let newest_selection = self.selections.newest_anchor().clone();
|
||||
@@ -4542,8 +4492,7 @@ impl Editor {
|
||||
if !user_requested
|
||||
&& (!self.enable_inline_completions
|
||||
|| !self.should_show_inline_completions(&buffer, cursor_buffer_position, cx)
|
||||
|| !self.is_focused(cx)
|
||||
|| buffer.read(cx).is_empty())
|
||||
|| !self.is_focused(cx))
|
||||
{
|
||||
self.discard_inline_completion(false, cx);
|
||||
return None;
|
||||
@@ -4813,7 +4762,6 @@ impl Editor {
|
||||
|| (!self.completion_tasks.is_empty() && !self.has_active_inline_completion()));
|
||||
if completions_menu_has_precedence
|
||||
|| !offset_selection.is_empty()
|
||||
|| !self.enable_inline_completions
|
||||
|| self
|
||||
.active_inline_completion
|
||||
.as_ref()
|
||||
@@ -4936,8 +4884,8 @@ impl Editor {
|
||||
&mut self,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Option<InlineCompletionMenuHint> {
|
||||
let provider = self.inline_completion_provider()?;
|
||||
if self.has_active_inline_completion() {
|
||||
let provider_name = self.inline_completion_provider()?.display_name();
|
||||
let editor_snapshot = self.snapshot(cx);
|
||||
|
||||
let text = match &self.active_inline_completion.as_ref()?.completion {
|
||||
@@ -4954,11 +4902,12 @@ impl Editor {
|
||||
}
|
||||
};
|
||||
|
||||
Some(InlineCompletionMenuHint::Loaded { text })
|
||||
} else if provider.is_refreshing(cx) {
|
||||
Some(InlineCompletionMenuHint::Loading)
|
||||
Some(InlineCompletionMenuHint {
|
||||
provider_name,
|
||||
text,
|
||||
})
|
||||
} else {
|
||||
Some(InlineCompletionMenuHint::None)
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13610,8 +13559,6 @@ pub trait CompletionProvider {
|
||||
}
|
||||
|
||||
pub trait CodeActionProvider {
|
||||
fn id(&self) -> Arc<str>;
|
||||
|
||||
fn code_actions(
|
||||
&self,
|
||||
buffer: &Model<Buffer>,
|
||||
@@ -13630,10 +13577,6 @@ pub trait CodeActionProvider {
|
||||
}
|
||||
|
||||
impl CodeActionProvider for Model<Project> {
|
||||
fn id(&self) -> Arc<str> {
|
||||
"project".into()
|
||||
}
|
||||
|
||||
fn code_actions(
|
||||
&self,
|
||||
buffer: &Model<Buffer>,
|
||||
|
||||
@@ -543,29 +543,8 @@ impl EditorElement {
|
||||
// and run the selection logic.
|
||||
modifiers.alt = false;
|
||||
} else {
|
||||
let scroll_position_row =
|
||||
position_map.scroll_pixel_position.y / position_map.line_height;
|
||||
let display_row = (((event.position - gutter_hitbox.bounds.origin).y
|
||||
+ position_map.scroll_pixel_position.y)
|
||||
/ position_map.line_height)
|
||||
as u32;
|
||||
let multi_buffer_row = position_map
|
||||
.snapshot
|
||||
.display_point_to_point(
|
||||
DisplayPoint::new(DisplayRow(display_row), 0),
|
||||
Bias::Right,
|
||||
)
|
||||
.row;
|
||||
let line_offset_from_top = display_row - scroll_position_row as u32;
|
||||
// if double click is made without alt, open the corresponding excerp
|
||||
editor.open_excerpts_common(
|
||||
Some(JumpData::MultiBufferRow {
|
||||
row: MultiBufferRow(multi_buffer_row),
|
||||
line_offset_from_top,
|
||||
}),
|
||||
false,
|
||||
cx,
|
||||
);
|
||||
editor.open_excerpts(&OpenExcerpts, cx);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -3979,7 +3958,13 @@ impl EditorElement {
|
||||
let Some(()) = line.paint(hitbox.origin, line_height, cx).log_err() else {
|
||||
continue;
|
||||
};
|
||||
cx.set_cursor_style(CursorStyle::PointingHand, hitbox);
|
||||
// In singleton buffers, we select corresponding lines on the line number click, so use | -like cursor.
|
||||
// In multi buffers, we open file at the line number clicked, so use a pointing hand cursor.
|
||||
if is_singleton {
|
||||
cx.set_cursor_style(CursorStyle::IBeam, hitbox);
|
||||
} else {
|
||||
cx.set_cursor_style(CursorStyle::PointingHand, hitbox);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,28 +18,19 @@ use wasm_encoder::{ComponentSectionId, Encode as _, RawSection, Section as _};
|
||||
use wasmparser::Parser;
|
||||
use wit_component::ComponentEncoder;
|
||||
|
||||
/// Currently, we compile with Rust's `wasm32-wasip1` target, which works with WASI `preview1`.
|
||||
/// But the WASM component model is based on WASI `preview2`. So we need an 'adapter' WASM
|
||||
/// module, which implements the `preview1` interface in terms of `preview2`.
|
||||
///
|
||||
/// Once Rust 1.78 is released, there will be a `wasm32-wasip2` target available, so we will
|
||||
/// not need the adapter anymore.
|
||||
const RUST_TARGET: &str = "wasm32-wasip1";
|
||||
const WASI_ADAPTER_URL: &str =
|
||||
"https://github.com/bytecodealliance/wasmtime/releases/download/v18.0.2/wasi_snapshot_preview1.reactor.wasm";
|
||||
|
||||
const RUST_TARGET: &str = "wasm32-wasip2";
|
||||
/// Compiling Tree-sitter parsers from C to WASM requires Clang 17, and a WASM build of libc
|
||||
/// and clang's runtime library. The `wasi-sdk` provides these binaries.
|
||||
///
|
||||
/// Once Clang 17 and its wasm target are available via system package managers, we won't need
|
||||
/// to download this.
|
||||
const WASI_SDK_URL: &str = "https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-21/";
|
||||
const WASI_SDK_URL: &str = "https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-24/";
|
||||
const WASI_SDK_ASSET_NAME: Option<&str> = if cfg!(target_os = "macos") {
|
||||
Some("wasi-sdk-21.0-macos.tar.gz")
|
||||
Some("wasi-sdk-24.0-macos.tar.gz")
|
||||
} else if cfg!(any(target_os = "linux", target_os = "freebsd")) {
|
||||
Some("wasi-sdk-21.0-linux.tar.gz")
|
||||
Some("wasi-sdk-24.0-linux.tar.gz")
|
||||
} else if cfg!(target_os = "windows") {
|
||||
Some("wasi-sdk-21.0.m-mingw.tar.gz")
|
||||
Some("wasi-sdk-24.0.m-mingw.tar.gz")
|
||||
} else {
|
||||
None
|
||||
};
|
||||
@@ -121,8 +112,6 @@ impl ExtensionBuilder {
|
||||
options: CompileExtensionOptions,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
self.install_rust_wasm_target_if_needed()?;
|
||||
let adapter_bytes = self.install_wasi_preview1_adapter_if_needed().await?;
|
||||
|
||||
let cargo_toml_content = fs::read_to_string(extension_dir.join("Cargo.toml"))?;
|
||||
let cargo_toml: CargoToml = toml::from_str(&cargo_toml_content)?;
|
||||
|
||||
@@ -130,6 +119,7 @@ impl ExtensionBuilder {
|
||||
"compiling Rust crate for extension {}",
|
||||
extension_dir.display()
|
||||
);
|
||||
|
||||
let output = util::command::new_std_command("cargo")
|
||||
.args(["build", "--target", RUST_TARGET])
|
||||
.args(options.release.then_some("--release"))
|
||||
@@ -168,20 +158,12 @@ impl ExtensionBuilder {
|
||||
let wasm_bytes = fs::read(&wasm_path)
|
||||
.with_context(|| format!("failed to read output module `{}`", wasm_path.display()))?;
|
||||
|
||||
let encoder = ComponentEncoder::default()
|
||||
.module(&wasm_bytes)?
|
||||
.adapter("wasi_snapshot_preview1", &adapter_bytes)
|
||||
.context("failed to load adapter module")?
|
||||
.validate(true);
|
||||
|
||||
log::info!(
|
||||
"encoding wasm component for extension {}",
|
||||
extension_dir.display()
|
||||
);
|
||||
|
||||
let component_bytes = encoder
|
||||
.encode()
|
||||
.context("failed to encode wasm component")?;
|
||||
let component_bytes = wasm_bytes;
|
||||
|
||||
let component_bytes = self
|
||||
.strip_custom_sections(&component_bytes)
|
||||
@@ -379,38 +361,6 @@ impl ExtensionBuilder {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn install_wasi_preview1_adapter_if_needed(&self) -> Result<Vec<u8>> {
|
||||
let cache_path = self.cache_dir.join("wasi_snapshot_preview1.reactor.wasm");
|
||||
if let Ok(content) = fs::read(&cache_path) {
|
||||
if Parser::is_core_wasm(&content) {
|
||||
return Ok(content);
|
||||
}
|
||||
}
|
||||
|
||||
fs::remove_file(&cache_path).ok();
|
||||
|
||||
log::info!(
|
||||
"downloading wasi adapter module to {}",
|
||||
cache_path.display()
|
||||
);
|
||||
let mut response = self
|
||||
.http
|
||||
.get(WASI_ADAPTER_URL, AsyncBody::default(), true)
|
||||
.await?;
|
||||
|
||||
let mut content = Vec::new();
|
||||
let mut body = BufReader::new(response.body_mut());
|
||||
body.read_to_end(&mut content).await?;
|
||||
|
||||
fs::write(&cache_path, &content)
|
||||
.with_context(|| format!("failed to save file {}", cache_path.display()))?;
|
||||
|
||||
if !Parser::is_core_wasm(&content) {
|
||||
bail!("downloaded wasi adapter is invalid");
|
||||
}
|
||||
Ok(content)
|
||||
}
|
||||
|
||||
async fn install_wasi_sdk_if_needed(&self) -> Result<PathBuf> {
|
||||
let url = if let Some(asset_name) = WASI_SDK_ASSET_NAME {
|
||||
format!("{WASI_SDK_URL}/{asset_name}")
|
||||
|
||||
@@ -175,7 +175,7 @@ impl ExtensionManifest {
|
||||
.await
|
||||
.with_context(|| format!("failed to load {extension_name} extension.toml"))?;
|
||||
toml::from_str(&manifest_content)
|
||||
.with_context(|| format!("invalid extension.json for extension {extension_name}"))
|
||||
.with_context(|| format!("invalid extension.toml for extension {extension_name}"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ impl HostWorktree for WasmState {
|
||||
latest::HostWorktree::which(self, delegate, binary_name).await
|
||||
}
|
||||
|
||||
fn drop(&mut self, _worktree: Resource<Worktree>) -> Result<()> {
|
||||
async fn drop(&mut self, _worktree: Resource<Worktree>) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,7 +92,7 @@ impl HostWorktree for WasmState {
|
||||
latest::HostWorktree::which(self, delegate, binary_name).await
|
||||
}
|
||||
|
||||
fn drop(&mut self, _worktree: Resource<Worktree>) -> Result<()> {
|
||||
async fn drop(&mut self, _worktree: Resource<Worktree>) -> Result<()> {
|
||||
// We only ever hand out borrows of worktrees.
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -147,7 +147,7 @@ impl HostWorktree for WasmState {
|
||||
latest::HostWorktree::which(self, delegate, binary_name).await
|
||||
}
|
||||
|
||||
fn drop(&mut self, _worktree: Resource<Worktree>) -> Result<()> {
|
||||
async fn drop(&mut self, _worktree: Resource<Worktree>) -> Result<()> {
|
||||
// We only ever hand out borrows of worktrees.
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -240,7 +240,7 @@ impl HostKeyValueStore for WasmState {
|
||||
kv_store.insert(key, value).await.to_wasmtime_result()
|
||||
}
|
||||
|
||||
fn drop(&mut self, _worktree: Resource<ExtensionKeyValueStore>) -> Result<()> {
|
||||
async fn drop(&mut self, _worktree: Resource<ExtensionKeyValueStore>) -> Result<()> {
|
||||
// We only ever hand out borrows of key-value stores.
|
||||
Ok(())
|
||||
}
|
||||
@@ -282,7 +282,7 @@ impl HostWorktree for WasmState {
|
||||
latest::HostWorktree::which(self, delegate, binary_name).await
|
||||
}
|
||||
|
||||
fn drop(&mut self, _worktree: Resource<Worktree>) -> Result<()> {
|
||||
async fn drop(&mut self, _worktree: Resource<Worktree>) -> Result<()> {
|
||||
// We only ever hand out borrows of worktrees.
|
||||
Ok(())
|
||||
}
|
||||
@@ -350,7 +350,7 @@ impl http_client::HostHttpResponseStream for WasmState {
|
||||
.to_wasmtime_result()
|
||||
}
|
||||
|
||||
fn drop(&mut self, _resource: Resource<ExtensionHttpResponseStream>) -> Result<()> {
|
||||
async fn drop(&mut self, _resource: Resource<ExtensionHttpResponseStream>) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -259,7 +259,7 @@ impl HostKeyValueStore for WasmState {
|
||||
kv_store.insert(key, value).await.to_wasmtime_result()
|
||||
}
|
||||
|
||||
fn drop(&mut self, _worktree: Resource<ExtensionKeyValueStore>) -> Result<()> {
|
||||
async fn drop(&mut self, _worktree: Resource<ExtensionKeyValueStore>) -> Result<()> {
|
||||
// We only ever hand out borrows of key-value stores.
|
||||
Ok(())
|
||||
}
|
||||
@@ -275,7 +275,7 @@ impl HostProject for WasmState {
|
||||
Ok(project.worktree_ids())
|
||||
}
|
||||
|
||||
fn drop(&mut self, _project: Resource<Project>) -> Result<()> {
|
||||
async fn drop(&mut self, _project: Resource<Project>) -> Result<()> {
|
||||
// We only ever hand out borrows of projects.
|
||||
Ok(())
|
||||
}
|
||||
@@ -325,7 +325,7 @@ impl HostWorktree for WasmState {
|
||||
Ok(delegate.which(binary_name).await)
|
||||
}
|
||||
|
||||
fn drop(&mut self, _worktree: Resource<Worktree>) -> Result<()> {
|
||||
async fn drop(&mut self, _worktree: Resource<Worktree>) -> Result<()> {
|
||||
// We only ever hand out borrows of worktrees.
|
||||
Ok(())
|
||||
}
|
||||
@@ -393,7 +393,7 @@ impl http_client::HostHttpResponseStream for WasmState {
|
||||
.to_wasmtime_result()
|
||||
}
|
||||
|
||||
fn drop(&mut self, _resource: Resource<ExtensionHttpResponseStream>) -> Result<()> {
|
||||
async fn drop(&mut self, _resource: Resource<ExtensionHttpResponseStream>) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,9 +59,9 @@ impl FeatureFlag for ToolUseFeatureFlag {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PredictEditsFeatureFlag;
|
||||
impl FeatureFlag for PredictEditsFeatureFlag {
|
||||
const NAME: &'static str = "predict-edits";
|
||||
pub struct ZetaFeatureFlag;
|
||||
impl FeatureFlag for ZetaFeatureFlag {
|
||||
const NAME: &'static str = "zeta";
|
||||
}
|
||||
|
||||
pub struct GitUiFeatureFlag;
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
[package]
|
||||
name = "fireworks"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/fireworks.rs"
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
futures.workspace = true
|
||||
http_client.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
@@ -1 +0,0 @@
|
||||
../../LICENSE-GPL
|
||||
@@ -1,173 +0,0 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use futures::AsyncReadExt;
|
||||
use http_client::{http::HeaderMap, AsyncBody, HttpClient, Method, Request as HttpRequest};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub const FIREWORKS_API_URL: &str = "https://api.openai.com/v1";
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct CompletionRequest {
|
||||
pub model: String,
|
||||
pub prompt: String,
|
||||
pub max_tokens: u32,
|
||||
pub temperature: f32,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub prediction: Option<Prediction>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub rewrite_speculation: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Serialize, Debug)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum Prediction {
|
||||
Content { content: String },
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Response {
|
||||
pub completion: CompletionResponse,
|
||||
pub headers: Headers,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct CompletionResponse {
|
||||
pub id: String,
|
||||
pub object: String,
|
||||
pub created: u64,
|
||||
pub model: String,
|
||||
pub choices: Vec<CompletionChoice>,
|
||||
pub usage: Usage,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct CompletionChoice {
|
||||
pub text: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct Usage {
|
||||
pub prompt_tokens: u32,
|
||||
pub completion_tokens: u32,
|
||||
pub total_tokens: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize)]
|
||||
pub struct Headers {
|
||||
pub server_processing_time: Option<f64>,
|
||||
pub request_id: Option<String>,
|
||||
pub prompt_tokens: Option<u32>,
|
||||
pub speculation_generated_tokens: Option<u32>,
|
||||
pub cached_prompt_tokens: Option<u32>,
|
||||
pub backend_host: Option<String>,
|
||||
pub num_concurrent_requests: Option<u32>,
|
||||
pub deployment: Option<String>,
|
||||
pub tokenizer_queue_duration: Option<f64>,
|
||||
pub tokenizer_duration: Option<f64>,
|
||||
pub prefill_queue_duration: Option<f64>,
|
||||
pub prefill_duration: Option<f64>,
|
||||
pub generation_queue_duration: Option<f64>,
|
||||
}
|
||||
|
||||
impl Headers {
|
||||
pub fn parse(headers: &HeaderMap) -> Self {
|
||||
Headers {
|
||||
request_id: headers
|
||||
.get("x-request-id")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.map(String::from),
|
||||
server_processing_time: headers
|
||||
.get("fireworks-server-processing-time")
|
||||
.and_then(|v| v.to_str().ok()?.parse().ok()),
|
||||
prompt_tokens: headers
|
||||
.get("fireworks-prompt-tokens")
|
||||
.and_then(|v| v.to_str().ok()?.parse().ok()),
|
||||
speculation_generated_tokens: headers
|
||||
.get("fireworks-speculation-generated-tokens")
|
||||
.and_then(|v| v.to_str().ok()?.parse().ok()),
|
||||
cached_prompt_tokens: headers
|
||||
.get("fireworks-cached-prompt-tokens")
|
||||
.and_then(|v| v.to_str().ok()?.parse().ok()),
|
||||
backend_host: headers
|
||||
.get("fireworks-backend-host")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.map(String::from),
|
||||
num_concurrent_requests: headers
|
||||
.get("fireworks-num-concurrent-requests")
|
||||
.and_then(|v| v.to_str().ok()?.parse().ok()),
|
||||
deployment: headers
|
||||
.get("fireworks-deployment")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.map(String::from),
|
||||
tokenizer_queue_duration: headers
|
||||
.get("fireworks-tokenizer-queue-duration")
|
||||
.and_then(|v| v.to_str().ok()?.parse().ok()),
|
||||
tokenizer_duration: headers
|
||||
.get("fireworks-tokenizer-duration")
|
||||
.and_then(|v| v.to_str().ok()?.parse().ok()),
|
||||
prefill_queue_duration: headers
|
||||
.get("fireworks-prefill-queue-duration")
|
||||
.and_then(|v| v.to_str().ok()?.parse().ok()),
|
||||
prefill_duration: headers
|
||||
.get("fireworks-prefill-duration")
|
||||
.and_then(|v| v.to_str().ok()?.parse().ok()),
|
||||
generation_queue_duration: headers
|
||||
.get("fireworks-generation-queue-duration")
|
||||
.and_then(|v| v.to_str().ok()?.parse().ok()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn complete(
|
||||
client: &dyn HttpClient,
|
||||
api_url: &str,
|
||||
api_key: &str,
|
||||
request: CompletionRequest,
|
||||
) -> Result<Response> {
|
||||
let uri = format!("{api_url}/completions");
|
||||
let request_builder = HttpRequest::builder()
|
||||
.method(Method::POST)
|
||||
.uri(uri)
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Authorization", format!("Bearer {}", api_key));
|
||||
|
||||
let request = request_builder.body(AsyncBody::from(serde_json::to_string(&request)?))?;
|
||||
let mut response = client.send(request).await?;
|
||||
|
||||
if response.status().is_success() {
|
||||
let headers = Headers::parse(response.headers());
|
||||
|
||||
let mut body = String::new();
|
||||
response.body_mut().read_to_string(&mut body).await?;
|
||||
|
||||
Ok(Response {
|
||||
completion: serde_json::from_str(&body)?,
|
||||
headers,
|
||||
})
|
||||
} else {
|
||||
let mut body = String::new();
|
||||
response.body_mut().read_to_string(&mut body).await?;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct FireworksResponse {
|
||||
error: FireworksError,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct FireworksError {
|
||||
message: String,
|
||||
}
|
||||
|
||||
match serde_json::from_str::<FireworksResponse>(&body) {
|
||||
Ok(response) if !response.error.message.is_empty() => Err(anyhow!(
|
||||
"Failed to connect to Fireworks API: {}",
|
||||
response.error.message,
|
||||
)),
|
||||
|
||||
_ => Err(anyhow!(
|
||||
"Failed to connect to Fireworks API: {} {}",
|
||||
response.status(),
|
||||
body,
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -47,9 +47,12 @@ windows.workspace = true
|
||||
|
||||
[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies]
|
||||
ashpd.workspace = true
|
||||
which.workspace = true
|
||||
shlex.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
|
||||
[features]
|
||||
test-support = ["gpui/test-support", "git/test-support"]
|
||||
|
||||
|
||||
@@ -9,6 +9,9 @@ use git::GitHostingProviderRegistry;
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||
use ashpd::desktop::trash;
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||
use smol::process::Command;
|
||||
|
||||
#[cfg(unix)]
|
||||
use std::os::fd::AsFd;
|
||||
#[cfg(unix)]
|
||||
@@ -518,24 +521,7 @@ impl Fs for RealFs {
|
||||
|
||||
async fn atomic_write(&self, path: PathBuf, data: String) -> Result<()> {
|
||||
smol::unblock(move || {
|
||||
let mut tmp_file = if cfg!(any(target_os = "linux", target_os = "freebsd")) {
|
||||
// Use the directory of the destination as temp dir to avoid
|
||||
// invalid cross-device link error, and XDG_CACHE_DIR for fallback.
|
||||
// See https://github.com/zed-industries/zed/pull/8437 for more details.
|
||||
NamedTempFile::new_in(path.parent().unwrap_or(paths::temp_dir()))
|
||||
} else if cfg!(target_os = "windows") {
|
||||
// If temp dir is set to a different drive than the destination,
|
||||
// we receive error:
|
||||
//
|
||||
// failed to persist temporary file:
|
||||
// The system cannot move the file to a different disk drive. (os error 17)
|
||||
//
|
||||
// So we use the directory of the destination as a temp dir to avoid it.
|
||||
// https://github.com/zed-industries/zed/issues/16571
|
||||
NamedTempFile::new_in(path.parent().unwrap_or(paths::temp_dir()))
|
||||
} else {
|
||||
NamedTempFile::new()
|
||||
}?;
|
||||
let mut tmp_file = create_temp_file(&path)?;
|
||||
tmp_file.write_all(data.as_bytes())?;
|
||||
tmp_file.persist(path)?;
|
||||
Ok::<(), anyhow::Error>(())
|
||||
@@ -550,13 +536,43 @@ impl Fs for RealFs {
|
||||
if let Some(path) = path.parent() {
|
||||
self.create_dir(path).await?;
|
||||
}
|
||||
let file = smol::fs::File::create(path).await?;
|
||||
let mut writer = smol::io::BufWriter::with_capacity(buffer_size, file);
|
||||
for chunk in chunks(text, line_ending) {
|
||||
writer.write_all(chunk.as_bytes()).await?;
|
||||
match smol::fs::File::create(path).await {
|
||||
Ok(file) => {
|
||||
let mut writer = smol::io::BufWriter::with_capacity(buffer_size, file);
|
||||
for chunk in chunks(text, line_ending) {
|
||||
writer.write_all(chunk.as_bytes()).await?;
|
||||
}
|
||||
writer.flush().await?;
|
||||
Ok(())
|
||||
}
|
||||
Err(e) if e.kind() == std::io::ErrorKind::PermissionDenied => {
|
||||
if cfg!(any(target_os = "linux", target_os = "freebsd")) {
|
||||
let target_path = path.to_path_buf();
|
||||
let temp_file = smol::unblock(move || create_temp_file(&target_path)).await?;
|
||||
|
||||
let temp_path = temp_file.into_temp_path();
|
||||
let temp_path_for_write = temp_path.to_path_buf();
|
||||
|
||||
let async_file = smol::fs::OpenOptions::new()
|
||||
.write(true)
|
||||
.open(&temp_path)
|
||||
.await?;
|
||||
|
||||
let mut writer = smol::io::BufWriter::with_capacity(buffer_size, async_file);
|
||||
|
||||
for chunk in chunks(text, line_ending) {
|
||||
writer.write_all(chunk.as_bytes()).await?;
|
||||
}
|
||||
writer.flush().await?;
|
||||
|
||||
write_to_file_as_root(temp_path_for_write, path.to_path_buf()).await
|
||||
} else {
|
||||
// Todo: Implement for Mac and Windows
|
||||
Err(e.into())
|
||||
}
|
||||
}
|
||||
Err(e) => Err(e.into()),
|
||||
}
|
||||
writer.flush().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn canonicalize(&self, path: &Path) -> Result<PathBuf> {
|
||||
@@ -1963,6 +1979,84 @@ fn chunks(rope: &Rope, line_ending: LineEnding) -> impl Iterator<Item = &str> {
|
||||
})
|
||||
}
|
||||
|
||||
fn create_temp_file(path: &Path) -> Result<NamedTempFile> {
|
||||
let temp_file = if cfg!(any(target_os = "linux", target_os = "freebsd")) {
|
||||
// Use the directory of the destination as temp dir to avoid
|
||||
// invalid cross-device link error, and XDG_CACHE_DIR for fallback.
|
||||
// See https://github.com/zed-industries/zed/pull/8437 for more details.
|
||||
NamedTempFile::new_in(path.parent().unwrap_or(paths::temp_dir()))?
|
||||
} else if cfg!(target_os = "windows") {
|
||||
// If temp dir is set to a different drive than the destination,
|
||||
// we receive error:
|
||||
//
|
||||
// failed to persist temporary file:
|
||||
// The system cannot move the file to a different disk drive. (os error 17)
|
||||
//
|
||||
// So we use the directory of the destination as a temp dir to avoid it.
|
||||
// https://github.com/zed-industries/zed/issues/16571
|
||||
NamedTempFile::new_in(path.parent().unwrap_or(paths::temp_dir()))?
|
||||
} else {
|
||||
NamedTempFile::new()?
|
||||
};
|
||||
|
||||
Ok(temp_file)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
async fn write_to_file_as_root(_temp_file_path: PathBuf, _target_file_path: PathBuf) -> Result<()> {
|
||||
unimplemented!("write_to_file_as_root is not implemented")
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
async fn write_to_file_as_root(_temp_file_path: PathBuf, _target_file_path: PathBuf) -> Result<()> {
|
||||
unimplemented!("write_to_file_as_root is not implemented")
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||
async fn write_to_file_as_root(temp_file_path: PathBuf, target_file_path: PathBuf) -> Result<()> {
|
||||
use shlex::try_quote;
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
use which::which;
|
||||
|
||||
let pkexec_path = smol::unblock(|| which("pkexec"))
|
||||
.await
|
||||
.map_err(|_| anyhow::anyhow!("pkexec not found in PATH"))?;
|
||||
|
||||
let script_file = smol::unblock(move || {
|
||||
let script_file = tempfile::Builder::new()
|
||||
.prefix("write-to-file-as-root-")
|
||||
.tempfile_in(paths::temp_dir())?;
|
||||
|
||||
writeln!(
|
||||
script_file.as_file(),
|
||||
"#!/usr/bin/env sh\nset -eu\ncat \"{}\" > \"{}\"",
|
||||
try_quote(&temp_file_path.to_string_lossy())?,
|
||||
try_quote(&target_file_path.to_string_lossy())?
|
||||
)?;
|
||||
|
||||
let mut perms = script_file.as_file().metadata()?.permissions();
|
||||
perms.set_mode(0o700); // rwx------
|
||||
script_file.as_file().set_permissions(perms)?;
|
||||
|
||||
Result::<_>::Ok(script_file)
|
||||
})
|
||||
.await?;
|
||||
|
||||
let script_path = script_file.into_temp_path();
|
||||
|
||||
let output = Command::new(&pkexec_path)
|
||||
.arg("--disable-internal-agent")
|
||||
.arg(&script_path)
|
||||
.output()
|
||||
.await?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(anyhow::anyhow!("Failed to write to file as root"));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn normalize_path(path: &Path) -> PathBuf {
|
||||
let mut components = path.components().peekable();
|
||||
let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() {
|
||||
|
||||
@@ -2,7 +2,7 @@ use crate::{
|
||||
ActiveTooltip, AnyTooltip, AnyView, Bounds, DispatchPhase, Element, ElementId, GlobalElementId,
|
||||
HighlightStyle, Hitbox, IntoElement, LayoutId, MouseDownEvent, MouseMoveEvent, MouseUpEvent,
|
||||
Pixels, Point, SharedString, Size, TextRun, TextStyle, Truncate, WhiteSpace, WindowContext,
|
||||
WrappedLine, WrappedLineLayout, TOOLTIP_DELAY,
|
||||
WrappedLine, TOOLTIP_DELAY,
|
||||
};
|
||||
use anyhow::anyhow;
|
||||
use parking_lot::{Mutex, MutexGuard};
|
||||
@@ -443,36 +443,6 @@ impl TextLayout {
|
||||
None
|
||||
}
|
||||
|
||||
/// Retrieve the layout for the line containing the given byte index.
|
||||
pub fn line_layout_for_index(&self, index: usize) -> Option<Arc<WrappedLineLayout>> {
|
||||
let element_state = self.lock();
|
||||
let element_state = element_state
|
||||
.as_ref()
|
||||
.expect("measurement has not been performed");
|
||||
let bounds = element_state
|
||||
.bounds
|
||||
.expect("prepaint has not been performed");
|
||||
let line_height = element_state.line_height;
|
||||
|
||||
let mut line_origin = bounds.origin;
|
||||
let mut line_start_ix = 0;
|
||||
|
||||
for line in &element_state.lines {
|
||||
let line_end_ix = line_start_ix + line.len();
|
||||
if index < line_start_ix {
|
||||
break;
|
||||
} else if index > line_end_ix {
|
||||
line_origin.y += line.size(line_height).height;
|
||||
line_start_ix = line_end_ix + 1;
|
||||
continue;
|
||||
} else {
|
||||
return Some(line.layout.clone());
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// The bounds of this layout.
|
||||
pub fn bounds(&self) -> Bounds<Pixels> {
|
||||
self.0.lock().as_ref().unwrap().bounds.unwrap()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use anyhow::Result;
|
||||
use copilot::{Copilot, Status};
|
||||
use editor::{scroll::Autoscroll, Editor};
|
||||
use feature_flags::{FeatureFlagAppExt, PredictEditsFeatureFlag};
|
||||
use feature_flags::{FeatureFlagAppExt, ZetaFeatureFlag};
|
||||
use fs::Fs;
|
||||
use gpui::{
|
||||
actions, div, pulsating_between, Action, Animation, AnimationExt, AppContext,
|
||||
@@ -201,14 +201,14 @@ impl Render for InlineCompletionButton {
|
||||
);
|
||||
}
|
||||
|
||||
InlineCompletionProvider::Zed => {
|
||||
if !cx.has_flag::<PredictEditsFeatureFlag>() {
|
||||
InlineCompletionProvider::Zeta => {
|
||||
if !cx.has_flag::<ZetaFeatureFlag>() {
|
||||
return div();
|
||||
}
|
||||
|
||||
let this = cx.view().clone();
|
||||
let button = IconButton::new("zeta", IconName::ZedPredict)
|
||||
.tooltip(|cx| Tooltip::text("Edit Prediction", cx));
|
||||
.tooltip(|cx| Tooltip::text("Zed Predict", cx));
|
||||
|
||||
let is_refreshing = self
|
||||
.inline_completion_provider
|
||||
|
||||
@@ -203,7 +203,7 @@ pub enum InlineCompletionProvider {
|
||||
#[default]
|
||||
Copilot,
|
||||
Supermaven,
|
||||
Zed,
|
||||
Zeta,
|
||||
}
|
||||
|
||||
/// The settings for inline completions, such as [GitHub Copilot](https://github.com/features/copilot)
|
||||
|
||||
@@ -212,18 +212,9 @@ impl LspAdapter for TypeScriptLspAdapter {
|
||||
_ => None,
|
||||
}?;
|
||||
|
||||
let one_line = |s: &str| s.replace(" ", "").replace('\n', " ");
|
||||
|
||||
let text = if let Some(description) = item
|
||||
.label_details
|
||||
.as_ref()
|
||||
.and_then(|label_details| label_details.description.as_ref())
|
||||
{
|
||||
format!("{} {}", item.label, one_line(description))
|
||||
} else if let Some(detail) = &item.detail {
|
||||
format!("{} {}", item.label, one_line(detail))
|
||||
} else {
|
||||
item.label.clone()
|
||||
let text = match &item.detail {
|
||||
Some(detail) => format!("{} {}", item.label, detail),
|
||||
None => item.label.clone(),
|
||||
};
|
||||
|
||||
Some(language::CodeLabel {
|
||||
|
||||
@@ -14,7 +14,7 @@ use parser::{parse_links_only, parse_markdown, MarkdownEvent, MarkdownTag, Markd
|
||||
|
||||
use std::{iter, mem, ops::Range, rc::Rc, sync::Arc};
|
||||
use theme::SyntaxTheme;
|
||||
use ui::prelude::*;
|
||||
use ui::{prelude::*, Tooltip};
|
||||
use util::{ResultExt, TryFutureExt};
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -667,6 +667,31 @@ impl Element for MarkdownElement {
|
||||
}
|
||||
MarkdownTagEnd::CodeBlock => {
|
||||
builder.trim_trailing_newline();
|
||||
builder.flush_text();
|
||||
builder.modify_current_div(|el| {
|
||||
let id =
|
||||
ElementId::NamedInteger("copy-markdown-code".into(), range.end);
|
||||
let copy_button = div().absolute().top_1().right_1().w_5().child(
|
||||
IconButton::new(id, IconName::Copy)
|
||||
.icon_color(Color::Muted)
|
||||
.shape(ui::IconButtonShape::Square)
|
||||
.tooltip(|cx| Tooltip::text("Copy Code Block", cx))
|
||||
.on_click({
|
||||
let code = without_fences(
|
||||
parsed_markdown.source()[range.clone()].trim(),
|
||||
)
|
||||
.to_string();
|
||||
|
||||
move |_, cx| {
|
||||
cx.write_to_clipboard(ClipboardItem::new_string(
|
||||
code.clone(),
|
||||
))
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
el.child(copy_button)
|
||||
});
|
||||
builder.pop_div();
|
||||
builder.pop_code_block();
|
||||
if self.style.code_block.text.is_some() {
|
||||
@@ -917,6 +942,13 @@ impl MarkdownElementBuilder {
|
||||
self.div_stack.push(div);
|
||||
}
|
||||
|
||||
fn modify_current_div(&mut self, f: impl FnOnce(AnyDiv) -> AnyDiv) {
|
||||
self.flush_text();
|
||||
if let Some(div) = self.div_stack.pop() {
|
||||
self.div_stack.push(f(div));
|
||||
}
|
||||
}
|
||||
|
||||
fn pop_div(&mut self) {
|
||||
self.flush_text();
|
||||
let div = self.div_stack.pop().unwrap().into_any_element();
|
||||
@@ -1001,7 +1033,7 @@ impl MarkdownElementBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
fn flush_text(&mut self) {
|
||||
pub fn flush_text(&mut self) {
|
||||
let line = mem::take(&mut self.pending_line);
|
||||
if line.text.is_empty() {
|
||||
return;
|
||||
@@ -1220,3 +1252,43 @@ impl RenderedText {
|
||||
.find(|link| link.source_range.contains(&source_index))
|
||||
}
|
||||
}
|
||||
|
||||
/// Some markdown blocks are indented, and others have e.g. ```rust … ``` around them.
|
||||
/// If this block is fenced with backticks, strip them off (and the language name).
|
||||
/// We use this when copying code blocks to the clipboard.
|
||||
fn without_fences(mut markdown: &str) -> &str {
|
||||
if let Some(opening_backticks) = markdown.find("```") {
|
||||
markdown = &markdown[opening_backticks..];
|
||||
|
||||
// Trim off the next newline. This also trims off a language name if it's there.
|
||||
if let Some(newline) = markdown.find('\n') {
|
||||
markdown = &markdown[newline + 1..];
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(closing_backticks) = markdown.rfind("```") {
|
||||
markdown = &markdown[..closing_backticks];
|
||||
};
|
||||
|
||||
markdown
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_without_fences() {
|
||||
let input = "```rust\nlet x = 5;\n```";
|
||||
assert_eq!(without_fences(input), "let x = 5;\n");
|
||||
|
||||
let input = " ```\nno language\n``` ";
|
||||
assert_eq!(without_fences(input), "no language\n");
|
||||
|
||||
let input = "plain text";
|
||||
assert_eq!(without_fences(input), "plain text");
|
||||
|
||||
let input = "```python\nprint('hello')\nprint('world')\n```";
|
||||
assert_eq!(without_fences(input), "print('hello')\nprint('world')\n");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -269,7 +269,7 @@ struct ExcerptIdMapping {
|
||||
|
||||
/// A range of text from a single [`Buffer`], to be shown as an [`Excerpt`].
|
||||
/// These ranges are relative to the buffer itself
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
|
||||
pub struct ExcerptRange<T> {
|
||||
/// The full range of text to be shown in the excerpt.
|
||||
pub context: Range<T>,
|
||||
|
||||
@@ -83,8 +83,8 @@ fn get_max_tokens(name: &str) -> usize {
|
||||
"codellama" | "starcoder2" => 16384,
|
||||
"mistral" | "codestral" | "mixstral" | "llava" | "qwen2" | "qwen2.5-coder"
|
||||
| "dolphin-mixtral" => 32768,
|
||||
"llama3.1" | "phi3" | "phi3.5" | "phi4" | "command-r" | "deepseek-coder-v2"
|
||||
| "yi-coder" | "llama3.2" => 128000,
|
||||
"llama3.1" | "phi3" | "phi3.5" | "command-r" | "deepseek-coder-v2" | "yi-coder"
|
||||
| "llama3.2" => 128000,
|
||||
_ => DEFAULT_TOKENS,
|
||||
}
|
||||
.clamp(1, MAXIMUM_TOKENS)
|
||||
|
||||
@@ -394,12 +394,10 @@ impl PartialEq for PanelEntry {
|
||||
Self::FoldedDirs(FoldedDirsEntry {
|
||||
worktree_id: worktree_id_a,
|
||||
entries: entries_a,
|
||||
..
|
||||
}),
|
||||
Self::FoldedDirs(FoldedDirsEntry {
|
||||
worktree_id: worktree_id_b,
|
||||
entries: entries_b,
|
||||
..
|
||||
}),
|
||||
) => worktree_id_a == worktree_id_b && entries_a == entries_b,
|
||||
(Self::Outline(a), Self::Outline(b)) => a == b,
|
||||
@@ -523,25 +521,13 @@ impl SearchData {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq)]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||
struct OutlineEntryExcerpt {
|
||||
id: ExcerptId,
|
||||
buffer_id: BufferId,
|
||||
range: ExcerptRange<language::Anchor>,
|
||||
}
|
||||
|
||||
impl PartialEq for OutlineEntryExcerpt {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.buffer_id == other.buffer_id && self.id == other.id
|
||||
}
|
||||
}
|
||||
|
||||
impl Hash for OutlineEntryExcerpt {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
(self.buffer_id, self.id).hash(state)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq)]
|
||||
struct OutlineEntryOutline {
|
||||
buffer_id: BufferId,
|
||||
@@ -551,13 +537,24 @@ struct OutlineEntryOutline {
|
||||
|
||||
impl PartialEq for OutlineEntryOutline {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.buffer_id == other.buffer_id && self.excerpt_id == other.excerpt_id
|
||||
self.buffer_id == other.buffer_id
|
||||
&& self.excerpt_id == other.excerpt_id
|
||||
&& self.outline.depth == other.outline.depth
|
||||
&& self.outline.range == other.outline.range
|
||||
&& self.outline.text == other.outline.text
|
||||
}
|
||||
}
|
||||
|
||||
impl Hash for OutlineEntryOutline {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
(self.buffer_id, self.excerpt_id).hash(state);
|
||||
(
|
||||
self.buffer_id,
|
||||
self.excerpt_id,
|
||||
self.outline.depth,
|
||||
&self.outline.range,
|
||||
&self.outline.text,
|
||||
)
|
||||
.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1060,8 +1057,7 @@ impl OutlinePanel {
|
||||
FsEntry::Directory(..) => None,
|
||||
})
|
||||
.skip_while(|id| *id != buffer_id)
|
||||
.skip(1)
|
||||
.next();
|
||||
.nth(1);
|
||||
if let Some(previous_buffer_id) = previous_buffer_id {
|
||||
if !active_editor.read(cx).buffer_folded(previous_buffer_id, cx)
|
||||
{
|
||||
@@ -1813,7 +1809,10 @@ impl OutlinePanel {
|
||||
}
|
||||
|
||||
fn reveal_entry_for_selection(&mut self, editor: View<Editor>, cx: &mut ViewContext<Self>) {
|
||||
if !self.active || !OutlinePanelSettings::get_global(cx).auto_reveal_entries {
|
||||
if !self.active
|
||||
|| !OutlinePanelSettings::get_global(cx).auto_reveal_entries
|
||||
|| self.focus_handle.contains_focused(cx)
|
||||
{
|
||||
return;
|
||||
}
|
||||
let project = self.project.clone();
|
||||
@@ -4965,6 +4964,7 @@ impl GenerationState {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use db::indoc;
|
||||
use gpui::{TestAppContext, VisualTestContext, WindowHandle};
|
||||
use language::{tree_sitter_rust, Language, LanguageConfig, LanguageMatcher};
|
||||
use pretty_assertions::assert_eq;
|
||||
@@ -5498,6 +5498,312 @@ mod tests {
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_navigating_in_singleton(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let root = "/root";
|
||||
let fs = FakeFs::new(cx.background_executor.clone());
|
||||
fs.insert_tree(
|
||||
root,
|
||||
json!({
|
||||
"src": {
|
||||
"lib.rs": indoc!("
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||
struct OutlineEntryExcerpt {
|
||||
id: ExcerptId,
|
||||
buffer_id: BufferId,
|
||||
range: ExcerptRange<language::Anchor>,
|
||||
}"),
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
let project = Project::test(fs.clone(), [root.as_ref()], cx).await;
|
||||
project.read_with(cx, |project, _| {
|
||||
project.languages().add(Arc::new(
|
||||
rust_lang()
|
||||
.with_outline_query(
|
||||
r#"
|
||||
(struct_item
|
||||
(visibility_modifier)? @context
|
||||
"struct" @context
|
||||
name: (_) @name) @item
|
||||
|
||||
(field_declaration
|
||||
(visibility_modifier)? @context
|
||||
name: (_) @name) @item
|
||||
"#,
|
||||
)
|
||||
.unwrap(),
|
||||
))
|
||||
});
|
||||
let workspace = add_outline_panel(&project, cx).await;
|
||||
let cx = &mut VisualTestContext::from_window(*workspace, cx);
|
||||
let outline_panel = outline_panel(&workspace, cx);
|
||||
outline_panel.update(cx, |outline_panel, cx| outline_panel.set_active(true, cx));
|
||||
|
||||
let _editor = workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace.open_abs_path(PathBuf::from("/root/src/lib.rs"), true, cx)
|
||||
})
|
||||
.unwrap()
|
||||
.await
|
||||
.expect("Failed to open Rust source file")
|
||||
.downcast::<Editor>()
|
||||
.expect("Should open an editor for Rust source file");
|
||||
|
||||
cx.executor()
|
||||
.advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
|
||||
cx.run_until_parked();
|
||||
outline_panel.update(cx, |outline_panel, cx| {
|
||||
assert_eq!(
|
||||
display_entries(
|
||||
&snapshot(&outline_panel, cx),
|
||||
&outline_panel.cached_entries,
|
||||
outline_panel.selected_entry()
|
||||
),
|
||||
indoc!(
|
||||
"
|
||||
outline: struct OutlineEntryExcerpt
|
||||
outline: id
|
||||
outline: buffer_id
|
||||
outline: range"
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
outline_panel.update(cx, |outline_panel, cx| {
|
||||
outline_panel.select_next(&SelectNext, cx);
|
||||
});
|
||||
cx.executor()
|
||||
.advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
|
||||
cx.run_until_parked();
|
||||
outline_panel.update(cx, |outline_panel, cx| {
|
||||
assert_eq!(
|
||||
display_entries(
|
||||
&snapshot(&outline_panel, cx),
|
||||
&outline_panel.cached_entries,
|
||||
outline_panel.selected_entry()
|
||||
),
|
||||
indoc!(
|
||||
"
|
||||
outline: struct OutlineEntryExcerpt <==== selected
|
||||
outline: id
|
||||
outline: buffer_id
|
||||
outline: range"
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
outline_panel.update(cx, |outline_panel, cx| {
|
||||
outline_panel.select_next(&SelectNext, cx);
|
||||
});
|
||||
cx.executor()
|
||||
.advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
|
||||
cx.run_until_parked();
|
||||
outline_panel.update(cx, |outline_panel, cx| {
|
||||
assert_eq!(
|
||||
display_entries(
|
||||
&snapshot(&outline_panel, cx),
|
||||
&outline_panel.cached_entries,
|
||||
outline_panel.selected_entry()
|
||||
),
|
||||
indoc!(
|
||||
"
|
||||
outline: struct OutlineEntryExcerpt
|
||||
outline: id <==== selected
|
||||
outline: buffer_id
|
||||
outline: range"
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
outline_panel.update(cx, |outline_panel, cx| {
|
||||
outline_panel.select_next(&SelectNext, cx);
|
||||
});
|
||||
cx.executor()
|
||||
.advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
|
||||
cx.run_until_parked();
|
||||
outline_panel.update(cx, |outline_panel, cx| {
|
||||
assert_eq!(
|
||||
display_entries(
|
||||
&snapshot(&outline_panel, cx),
|
||||
&outline_panel.cached_entries,
|
||||
outline_panel.selected_entry()
|
||||
),
|
||||
indoc!(
|
||||
"
|
||||
outline: struct OutlineEntryExcerpt
|
||||
outline: id
|
||||
outline: buffer_id <==== selected
|
||||
outline: range"
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
outline_panel.update(cx, |outline_panel, cx| {
|
||||
outline_panel.select_next(&SelectNext, cx);
|
||||
});
|
||||
cx.executor()
|
||||
.advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
|
||||
cx.run_until_parked();
|
||||
outline_panel.update(cx, |outline_panel, cx| {
|
||||
assert_eq!(
|
||||
display_entries(
|
||||
&snapshot(&outline_panel, cx),
|
||||
&outline_panel.cached_entries,
|
||||
outline_panel.selected_entry()
|
||||
),
|
||||
indoc!(
|
||||
"
|
||||
outline: struct OutlineEntryExcerpt
|
||||
outline: id
|
||||
outline: buffer_id
|
||||
outline: range <==== selected"
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
outline_panel.update(cx, |outline_panel, cx| {
|
||||
outline_panel.select_next(&SelectNext, cx);
|
||||
});
|
||||
cx.executor()
|
||||
.advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
|
||||
cx.run_until_parked();
|
||||
outline_panel.update(cx, |outline_panel, cx| {
|
||||
assert_eq!(
|
||||
display_entries(
|
||||
&snapshot(&outline_panel, cx),
|
||||
&outline_panel.cached_entries,
|
||||
outline_panel.selected_entry()
|
||||
),
|
||||
indoc!(
|
||||
"
|
||||
outline: struct OutlineEntryExcerpt <==== selected
|
||||
outline: id
|
||||
outline: buffer_id
|
||||
outline: range"
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
outline_panel.update(cx, |outline_panel, cx| {
|
||||
outline_panel.select_prev(&SelectPrev, cx);
|
||||
});
|
||||
cx.executor()
|
||||
.advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
|
||||
cx.run_until_parked();
|
||||
outline_panel.update(cx, |outline_panel, cx| {
|
||||
assert_eq!(
|
||||
display_entries(
|
||||
&snapshot(&outline_panel, cx),
|
||||
&outline_panel.cached_entries,
|
||||
outline_panel.selected_entry()
|
||||
),
|
||||
indoc!(
|
||||
"
|
||||
outline: struct OutlineEntryExcerpt
|
||||
outline: id
|
||||
outline: buffer_id
|
||||
outline: range <==== selected"
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
outline_panel.update(cx, |outline_panel, cx| {
|
||||
outline_panel.select_prev(&SelectPrev, cx);
|
||||
});
|
||||
cx.executor()
|
||||
.advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
|
||||
cx.run_until_parked();
|
||||
outline_panel.update(cx, |outline_panel, cx| {
|
||||
assert_eq!(
|
||||
display_entries(
|
||||
&snapshot(&outline_panel, cx),
|
||||
&outline_panel.cached_entries,
|
||||
outline_panel.selected_entry()
|
||||
),
|
||||
indoc!(
|
||||
"
|
||||
outline: struct OutlineEntryExcerpt
|
||||
outline: id
|
||||
outline: buffer_id <==== selected
|
||||
outline: range"
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
outline_panel.update(cx, |outline_panel, cx| {
|
||||
outline_panel.select_prev(&SelectPrev, cx);
|
||||
});
|
||||
cx.executor()
|
||||
.advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
|
||||
cx.run_until_parked();
|
||||
outline_panel.update(cx, |outline_panel, cx| {
|
||||
assert_eq!(
|
||||
display_entries(
|
||||
&snapshot(&outline_panel, cx),
|
||||
&outline_panel.cached_entries,
|
||||
outline_panel.selected_entry()
|
||||
),
|
||||
indoc!(
|
||||
"
|
||||
outline: struct OutlineEntryExcerpt
|
||||
outline: id <==== selected
|
||||
outline: buffer_id
|
||||
outline: range"
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
outline_panel.update(cx, |outline_panel, cx| {
|
||||
outline_panel.select_prev(&SelectPrev, cx);
|
||||
});
|
||||
cx.executor()
|
||||
.advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
|
||||
cx.run_until_parked();
|
||||
outline_panel.update(cx, |outline_panel, cx| {
|
||||
assert_eq!(
|
||||
display_entries(
|
||||
&snapshot(&outline_panel, cx),
|
||||
&outline_panel.cached_entries,
|
||||
outline_panel.selected_entry()
|
||||
),
|
||||
indoc!(
|
||||
"
|
||||
outline: struct OutlineEntryExcerpt <==== selected
|
||||
outline: id
|
||||
outline: buffer_id
|
||||
outline: range"
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
outline_panel.update(cx, |outline_panel, cx| {
|
||||
outline_panel.select_prev(&SelectPrev, cx);
|
||||
});
|
||||
cx.executor()
|
||||
.advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
|
||||
cx.run_until_parked();
|
||||
outline_panel.update(cx, |outline_panel, cx| {
|
||||
assert_eq!(
|
||||
display_entries(
|
||||
&snapshot(&outline_panel, cx),
|
||||
&outline_panel.cached_entries,
|
||||
outline_panel.selected_entry()
|
||||
),
|
||||
indoc!(
|
||||
"
|
||||
outline: struct OutlineEntryExcerpt
|
||||
outline: id
|
||||
outline: buffer_id
|
||||
outline: range <==== selected"
|
||||
)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_frontend_repo_structure(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
@@ -54,8 +54,8 @@ use std::{
|
||||
use theme::ThemeSettings;
|
||||
use ui::{
|
||||
prelude::*, v_flex, ContextMenu, DecoratedIcon, Icon, IconDecoration, IconDecorationKind,
|
||||
IndentGuideColors, IndentGuideLayout, KeyBinding, Label, ListItem, Scrollbar, ScrollbarState,
|
||||
Tooltip,
|
||||
IndentGuideColors, IndentGuideLayout, KeyBinding, Label, ListItem, ListItemSpacing, Scrollbar,
|
||||
ScrollbarState, Tooltip,
|
||||
};
|
||||
use util::{maybe, paths::compare_paths, ResultExt, TakeUntilExt, TryFutureExt};
|
||||
use workspace::{
|
||||
@@ -3447,6 +3447,12 @@ impl ProjectPanel {
|
||||
ListItem::new(entry_id.to_proto() as usize)
|
||||
.indent_level(depth)
|
||||
.indent_step_size(px(settings.indent_size))
|
||||
.spacing(match settings.entry_spacing {
|
||||
project_panel_settings::EntrySpacing::Comfortable => ListItemSpacing::Dense,
|
||||
project_panel_settings::EntrySpacing::Standard => {
|
||||
ListItemSpacing::ExtraDense
|
||||
}
|
||||
})
|
||||
.selectable(false)
|
||||
.when_some(canonical_path, |this, path| {
|
||||
this.end_slot::<AnyElement>(
|
||||
|
||||
@@ -18,11 +18,22 @@ pub enum ShowIndentGuides {
|
||||
Never,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum EntrySpacing {
|
||||
/// Comfortable spacing of entries.
|
||||
#[default]
|
||||
Comfortable,
|
||||
/// The standard spacing of entries.
|
||||
Standard,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone, Copy, PartialEq)]
|
||||
pub struct ProjectPanelSettings {
|
||||
pub button: bool,
|
||||
pub default_width: Pixels,
|
||||
pub dock: ProjectPanelDockPosition,
|
||||
pub entry_spacing: EntrySpacing,
|
||||
pub file_icons: bool,
|
||||
pub folder_icons: bool,
|
||||
pub git_status: bool,
|
||||
@@ -90,6 +101,10 @@ pub struct ProjectPanelSettingsContent {
|
||||
///
|
||||
/// Default: left
|
||||
pub dock: Option<ProjectPanelDockPosition>,
|
||||
/// Spacing between worktree entries in the project panel.
|
||||
///
|
||||
/// Default: comfortable
|
||||
pub entry_spacing: Option<EntrySpacing>,
|
||||
/// Whether to show file icons in the project panel.
|
||||
///
|
||||
/// Default: true
|
||||
|
||||
@@ -13,5 +13,5 @@ workspace = true
|
||||
|
||||
[dependencies]
|
||||
gpui.workspace = true
|
||||
itertools = { package = "itertools", version = "0.13" }
|
||||
itertools = { package = "itertools", version = "0.14" }
|
||||
smallvec.workspace = true
|
||||
|
||||
@@ -10,7 +10,7 @@ use windows::Win32::{Foundation::HANDLE, System::Threading::GetProcessId};
|
||||
|
||||
use sysinfo::{Pid, Process, ProcessRefreshKind, RefreshKind, System, UpdateKind};
|
||||
|
||||
struct ProcessIdGetter {
|
||||
pub struct ProcessIdGetter {
|
||||
handle: i32,
|
||||
fallback_pid: u32,
|
||||
}
|
||||
@@ -31,6 +31,10 @@ impl ProcessIdGetter {
|
||||
}
|
||||
Some(Pid::from_u32(pid as u32))
|
||||
}
|
||||
|
||||
pub fn fallback_pid(&self) -> u32 {
|
||||
self.fallback_pid
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
@@ -62,6 +66,10 @@ impl ProcessIdGetter {
|
||||
}
|
||||
Some(Pid::from_u32(pid))
|
||||
}
|
||||
|
||||
pub fn fallback_pid(&self) -> u32 {
|
||||
self.fallback_pid
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -96,6 +104,10 @@ impl PtyProcessInfo {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pid_getter(&self) -> &ProcessIdGetter {
|
||||
&self.pid_getter
|
||||
}
|
||||
|
||||
fn refresh(&mut self) -> Option<&Process> {
|
||||
let pid = self.pid_getter.pid()?;
|
||||
if self.system.refresh_processes_specifics(
|
||||
|
||||
@@ -146,16 +146,13 @@ fn populate_pane_items(
|
||||
cx: &mut ViewContext<Pane>,
|
||||
) {
|
||||
let mut item_index = pane.items_len();
|
||||
let mut active_item_index = None;
|
||||
for item in items {
|
||||
if Some(item.item_id().as_u64()) == active_item {
|
||||
active_item_index = Some(item_index);
|
||||
}
|
||||
let activate_item = Some(item.item_id().as_u64()) == active_item;
|
||||
pane.add_item(Box::new(item), false, false, None, cx);
|
||||
item_index += 1;
|
||||
}
|
||||
if let Some(index) = active_item_index {
|
||||
pane.activate_item(index, false, false, cx);
|
||||
if activate_item {
|
||||
pane.activate_item(item_index, false, false, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ use ui::{
|
||||
};
|
||||
use util::{ResultExt, TryFutureExt};
|
||||
use workspace::{
|
||||
dock::{DockPosition, Panel, PanelEvent, PanelHandle},
|
||||
dock::{DockPosition, Panel, PanelEvent},
|
||||
item::SerializableItem,
|
||||
move_active_item, move_item, pane,
|
||||
ui::IconName,
|
||||
@@ -75,7 +75,6 @@ pub struct TerminalPanel {
|
||||
deferred_tasks: HashMap<TaskId, Task<()>>,
|
||||
assistant_enabled: bool,
|
||||
assistant_tab_bar_button: Option<AnyView>,
|
||||
active: bool,
|
||||
}
|
||||
|
||||
impl TerminalPanel {
|
||||
@@ -83,6 +82,7 @@ impl TerminalPanel {
|
||||
let project = workspace.project();
|
||||
let pane = new_terminal_pane(workspace.weak_handle(), project.clone(), false, cx);
|
||||
let center = PaneGroup::new(pane.clone());
|
||||
cx.focus_view(&pane);
|
||||
let terminal_panel = Self {
|
||||
center,
|
||||
active_pane: pane,
|
||||
@@ -95,7 +95,6 @@ impl TerminalPanel {
|
||||
deferred_tasks: HashMap::default(),
|
||||
assistant_enabled: false,
|
||||
assistant_tab_bar_button: None,
|
||||
active: false,
|
||||
};
|
||||
terminal_panel.apply_tab_bar_buttons(&terminal_panel.active_pane, cx);
|
||||
terminal_panel
|
||||
@@ -282,25 +281,6 @@ impl TerminalPanel {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(workspace) = workspace.upgrade() {
|
||||
let should_focus = workspace
|
||||
.update(&mut cx, |workspace, cx| {
|
||||
workspace.active_item(cx).is_none()
|
||||
&& workspace.is_dock_at_position_open(terminal_panel.position(cx), cx)
|
||||
})
|
||||
.unwrap_or(false);
|
||||
|
||||
if should_focus {
|
||||
terminal_panel
|
||||
.update(&mut cx, |panel, cx| {
|
||||
panel.active_pane.update(cx, |pane, cx| {
|
||||
pane.focus_active_item(cx);
|
||||
});
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
|
||||
Ok(terminal_panel)
|
||||
}
|
||||
|
||||
@@ -1359,9 +1339,7 @@ impl Panel for TerminalPanel {
|
||||
}
|
||||
|
||||
fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {
|
||||
let old_active = self.active;
|
||||
self.active = active;
|
||||
if !active || old_active == active || !self.has_no_terminals(cx) {
|
||||
if !active || !self.has_no_terminals(cx) {
|
||||
return;
|
||||
}
|
||||
cx.defer(|this, cx| {
|
||||
|
||||
36
crates/terminal_view/src/terminal_tab_tooltip.rs
Normal file
36
crates/terminal_view/src/terminal_tab_tooltip.rs
Normal file
@@ -0,0 +1,36 @@
|
||||
use gpui::{IntoElement, Render, ViewContext};
|
||||
use ui::{prelude::*, tooltip_container, Divider};
|
||||
|
||||
pub struct TerminalTooltip {
|
||||
title: SharedString,
|
||||
pid: u32,
|
||||
}
|
||||
|
||||
impl TerminalTooltip {
|
||||
pub fn new(title: impl Into<SharedString>, pid: u32) -> Self {
|
||||
Self {
|
||||
title: title.into(),
|
||||
pid,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for TerminalTooltip {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
tooltip_container(cx, move |this, _cx| {
|
||||
this.occlude()
|
||||
.on_mouse_move(|_, cx| cx.stop_propagation())
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_1()
|
||||
.child(Label::new(self.title.clone()))
|
||||
.child(Divider::horizontal())
|
||||
.child(
|
||||
Label::new(format!("Process ID (PID): {}", self.pid))
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::Small),
|
||||
),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
mod persistence;
|
||||
pub mod terminal_element;
|
||||
pub mod terminal_panel;
|
||||
pub mod terminal_tab_tooltip;
|
||||
|
||||
use collections::HashSet;
|
||||
use editor::{actions::SelectAll, scroll::Autoscroll, Editor};
|
||||
@@ -26,13 +27,16 @@ use terminal::{
|
||||
};
|
||||
use terminal_element::{is_blank, TerminalElement};
|
||||
use terminal_panel::TerminalPanel;
|
||||
use terminal_tab_tooltip::TerminalTooltip;
|
||||
use ui::{h_flex, prelude::*, ContextMenu, Icon, IconName, Label, Tooltip};
|
||||
use util::{
|
||||
paths::{PathWithPosition, SanitizedPath},
|
||||
ResultExt,
|
||||
};
|
||||
use workspace::{
|
||||
item::{BreadcrumbText, Item, ItemEvent, SerializableItem, TabContentParams},
|
||||
item::{
|
||||
BreadcrumbText, Item, ItemEvent, SerializableItem, TabContentParams, TabTooltipContent,
|
||||
},
|
||||
register_serializable_item,
|
||||
searchable::{SearchEvent, SearchOptions, SearchableItem, SearchableItemHandle},
|
||||
CloseActiveItem, NewCenterTerminal, NewTerminal, OpenVisible, ToolbarItemLocation, Workspace,
|
||||
@@ -996,8 +1000,17 @@ impl Render for TerminalView {
|
||||
impl Item for TerminalView {
|
||||
type Event = ItemEvent;
|
||||
|
||||
fn tab_tooltip_text(&self, cx: &AppContext) -> Option<SharedString> {
|
||||
Some(self.terminal().read(cx).title(false).into())
|
||||
fn tab_tooltip_content(&self, cx: &AppContext) -> Option<TabTooltipContent> {
|
||||
let terminal = self.terminal().read(cx);
|
||||
let title = terminal.title(false);
|
||||
let pid = terminal.pty_info.pid_getter().fallback_pid();
|
||||
|
||||
Some(TabTooltipContent::Custom(Box::new(
|
||||
move |cx: &mut WindowContext| {
|
||||
cx.new_view(|_| TerminalTooltip::new(title.clone(), pid))
|
||||
.into()
|
||||
},
|
||||
)))
|
||||
}
|
||||
|
||||
fn tab_content(&self, params: TabContentParams, cx: &WindowContext) -> AnyElement {
|
||||
|
||||
@@ -11,6 +11,7 @@ use crate::{prelude::*, Disclosure};
|
||||
pub enum ListItemSpacing {
|
||||
#[default]
|
||||
Dense,
|
||||
ExtraDense,
|
||||
Sparse,
|
||||
}
|
||||
|
||||
@@ -219,6 +220,7 @@ impl RenderOnce for ListItem {
|
||||
.px(DynamicSpacing::Base06.rems(cx))
|
||||
.map(|this| match self.spacing {
|
||||
ListItemSpacing::Dense => this,
|
||||
ListItemSpacing::ExtraDense => this.py_neg_px(),
|
||||
ListItemSpacing::Sparse => this.py_1(),
|
||||
})
|
||||
.when(self.inset && !self.disabled, |this| {
|
||||
|
||||
@@ -1204,7 +1204,7 @@ impl Vim {
|
||||
.map_or(false, |provider| provider.show_completions_in_normal_mode()),
|
||||
_ => false,
|
||||
};
|
||||
editor.set_inline_completions_enabled(enable_inline_completions, cx);
|
||||
editor.set_inline_completions_enabled(enable_inline_completions);
|
||||
});
|
||||
cx.notify()
|
||||
}
|
||||
|
||||
@@ -178,6 +178,11 @@ impl TabContentParams {
|
||||
}
|
||||
}
|
||||
|
||||
pub enum TabTooltipContent {
|
||||
Text(SharedString),
|
||||
Custom(Box<dyn Fn(&mut WindowContext) -> AnyView>),
|
||||
}
|
||||
|
||||
pub trait Item: FocusableView + EventEmitter<Self::Event> {
|
||||
type Event;
|
||||
|
||||
@@ -206,6 +211,25 @@ pub trait Item: FocusableView + EventEmitter<Self::Event> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Returns the tab tooltip text.
|
||||
///
|
||||
/// Use this if you don't need to customize the tab tooltip content.
|
||||
fn tab_tooltip_text(&self, _: &AppContext) -> Option<SharedString> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Returns the tab tooltip content.
|
||||
///
|
||||
/// By default this returns a Tooltip text from
|
||||
/// `tab_tooltip_text`.
|
||||
fn tab_tooltip_content(&self, cx: &AppContext) -> Option<TabTooltipContent> {
|
||||
self.tab_tooltip_text(cx).map(TabTooltipContent::Text)
|
||||
}
|
||||
|
||||
fn tab_description(&self, _: usize, _: &AppContext) -> Option<SharedString> {
|
||||
None
|
||||
}
|
||||
|
||||
fn to_item_events(_event: &Self::Event, _f: impl FnMut(ItemEvent)) {}
|
||||
|
||||
fn deactivated(&mut self, _: &mut ViewContext<Self>) {}
|
||||
@@ -214,12 +238,6 @@ pub trait Item: FocusableView + EventEmitter<Self::Event> {
|
||||
fn navigate(&mut self, _: Box<dyn Any>, _: &mut ViewContext<Self>) -> bool {
|
||||
false
|
||||
}
|
||||
fn tab_tooltip_text(&self, _: &AppContext) -> Option<SharedString> {
|
||||
None
|
||||
}
|
||||
fn tab_description(&self, _: usize, _: &AppContext) -> Option<SharedString> {
|
||||
None
|
||||
}
|
||||
|
||||
fn telemetry_event_text(&self) -> Option<&'static str> {
|
||||
None
|
||||
@@ -320,6 +338,10 @@ pub trait Item: FocusableView + EventEmitter<Self::Event> {
|
||||
fn preserve_preview(&self, _cx: &AppContext) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn include_in_nav_history() -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
pub trait SerializableItem: Item {
|
||||
@@ -394,10 +416,11 @@ pub trait ItemHandle: 'static + Send {
|
||||
handler: Box<dyn Fn(ItemEvent, &mut WindowContext)>,
|
||||
) -> gpui::Subscription;
|
||||
fn focus_handle(&self, cx: &WindowContext) -> FocusHandle;
|
||||
fn tab_tooltip_text(&self, cx: &AppContext) -> Option<SharedString>;
|
||||
fn tab_description(&self, detail: usize, cx: &AppContext) -> Option<SharedString>;
|
||||
fn tab_content(&self, params: TabContentParams, cx: &WindowContext) -> AnyElement;
|
||||
fn tab_icon(&self, cx: &WindowContext) -> Option<Icon>;
|
||||
fn tab_tooltip_text(&self, cx: &AppContext) -> Option<SharedString>;
|
||||
fn tab_tooltip_content(&self, cx: &AppContext) -> Option<TabTooltipContent>;
|
||||
fn telemetry_event_text(&self, cx: &WindowContext) -> Option<&'static str>;
|
||||
fn dragged_tab_content(&self, params: TabContentParams, cx: &WindowContext) -> AnyElement;
|
||||
fn project_path(&self, cx: &AppContext) -> Option<ProjectPath>;
|
||||
@@ -464,6 +487,7 @@ pub trait ItemHandle: 'static + Send {
|
||||
fn downgrade_item(&self) -> Box<dyn WeakItemHandle>;
|
||||
fn workspace_settings<'a>(&self, cx: &'a AppContext) -> &'a WorkspaceSettings;
|
||||
fn preserve_preview(&self, cx: &AppContext) -> bool;
|
||||
fn include_in_nav_history(&self) -> bool;
|
||||
}
|
||||
|
||||
pub trait WeakItemHandle: Send + Sync {
|
||||
@@ -498,10 +522,6 @@ impl<T: Item> ItemHandle for View<T> {
|
||||
self.focus_handle(cx)
|
||||
}
|
||||
|
||||
fn tab_tooltip_text(&self, cx: &AppContext) -> Option<SharedString> {
|
||||
self.read(cx).tab_tooltip_text(cx)
|
||||
}
|
||||
|
||||
fn telemetry_event_text(&self, cx: &WindowContext) -> Option<&'static str> {
|
||||
self.read(cx).telemetry_event_text()
|
||||
}
|
||||
@@ -518,6 +538,14 @@ impl<T: Item> ItemHandle for View<T> {
|
||||
self.read(cx).tab_icon(cx)
|
||||
}
|
||||
|
||||
fn tab_tooltip_content(&self, cx: &AppContext) -> Option<TabTooltipContent> {
|
||||
self.read(cx).tab_tooltip_content(cx)
|
||||
}
|
||||
|
||||
fn tab_tooltip_text(&self, cx: &AppContext) -> Option<SharedString> {
|
||||
self.read(cx).tab_tooltip_text(cx)
|
||||
}
|
||||
|
||||
fn dragged_tab_content(&self, params: TabContentParams, cx: &WindowContext) -> AnyElement {
|
||||
self.read(cx).tab_content(
|
||||
TabContentParams {
|
||||
@@ -877,6 +905,10 @@ impl<T: Item> ItemHandle for View<T> {
|
||||
fn preserve_preview(&self, cx: &AppContext) -> bool {
|
||||
self.read(cx).preserve_preview(cx)
|
||||
}
|
||||
|
||||
fn include_in_nav_history(&self) -> bool {
|
||||
T::include_in_nav_history()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Box<dyn ItemHandle>> for AnyView {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::{
|
||||
item::{
|
||||
ActivateOnClose, ClosePosition, Item, ItemHandle, ItemSettings, PreviewTabsSettings,
|
||||
ShowDiagnostics, TabContentParams, WeakItemHandle,
|
||||
ShowDiagnostics, TabContentParams, TabTooltipContent, WeakItemHandle,
|
||||
},
|
||||
move_item,
|
||||
notifications::NotifyResultExt,
|
||||
@@ -206,6 +206,7 @@ pub enum Event {
|
||||
},
|
||||
ActivateItem {
|
||||
local: bool,
|
||||
focus_changed: bool,
|
||||
},
|
||||
Remove {
|
||||
focus_on_pane: Option<View<Pane>>,
|
||||
@@ -236,7 +237,7 @@ impl fmt::Debug for Event {
|
||||
.debug_struct("AddItem")
|
||||
.field("item", &item.item_id())
|
||||
.finish(),
|
||||
Event::ActivateItem { local } => f
|
||||
Event::ActivateItem { local, .. } => f
|
||||
.debug_struct("ActivateItem")
|
||||
.field("local", local)
|
||||
.finish(),
|
||||
@@ -1092,9 +1093,6 @@ impl Pane {
|
||||
prev_item.deactivated(cx);
|
||||
}
|
||||
}
|
||||
cx.emit(Event::ActivateItem {
|
||||
local: activate_pane,
|
||||
});
|
||||
|
||||
if let Some(newly_active_item) = self.items.get(index) {
|
||||
self.activation_history
|
||||
@@ -1114,6 +1112,11 @@ impl Pane {
|
||||
self.focus_active_item(cx);
|
||||
}
|
||||
|
||||
cx.emit(Event::ActivateItem {
|
||||
local: activate_pane,
|
||||
focus_changed: focus_item,
|
||||
});
|
||||
|
||||
if !self.is_tab_pinned(index) {
|
||||
self.tab_bar_scroll_handle
|
||||
.scroll_to_item(index - self.pinned_tab_count);
|
||||
@@ -2146,8 +2149,11 @@ impl Pane {
|
||||
this.drag_split_direction = None;
|
||||
this.handle_external_paths_drop(paths, cx)
|
||||
}))
|
||||
.when_some(item.tab_tooltip_text(cx), |tab, text| {
|
||||
tab.tooltip(move |cx| Tooltip::text(text.clone(), cx))
|
||||
.when_some(item.tab_tooltip_content(cx), |tab, content| match content {
|
||||
TabTooltipContent::Text(text) => {
|
||||
tab.tooltip(move |cx| Tooltip::text(text.clone(), cx))
|
||||
}
|
||||
TabTooltipContent::Custom(element_fn) => tab.tooltip(move |cx| element_fn(cx)),
|
||||
})
|
||||
.start_slot::<Indicator>(indicator)
|
||||
.map(|this| {
|
||||
@@ -3095,8 +3101,14 @@ impl Render for Pane {
|
||||
|
||||
impl ItemNavHistory {
|
||||
pub fn push<D: 'static + Send + Any>(&mut self, data: Option<D>, cx: &mut WindowContext) {
|
||||
self.history
|
||||
.push(data, self.item.clone(), self.is_preview, cx);
|
||||
if self
|
||||
.item
|
||||
.upgrade()
|
||||
.is_some_and(|item| item.include_in_nav_history())
|
||||
{
|
||||
self.history
|
||||
.push(data, self.item.clone(), self.is_preview, cx);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pop_backward(&mut self, cx: &mut WindowContext) -> Option<NavigationEntry> {
|
||||
|
||||
@@ -2295,19 +2295,6 @@ impl Workspace {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_dock_at_position_open(
|
||||
&self,
|
||||
position: DockPosition,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> bool {
|
||||
let dock = match position {
|
||||
DockPosition::Left => &self.left_dock,
|
||||
DockPosition::Bottom => &self.bottom_dock,
|
||||
DockPosition::Right => &self.right_dock,
|
||||
};
|
||||
dock.read(cx).is_open()
|
||||
}
|
||||
|
||||
pub fn toggle_dock(&mut self, dock_side: DockPosition, cx: &mut ViewContext<Self>) {
|
||||
let dock = match dock_side {
|
||||
DockPosition::Left => &self.left_dock,
|
||||
@@ -2988,13 +2975,15 @@ impl Workspace {
|
||||
match target {
|
||||
Some(ActivateInDirectionTarget::Pane(pane)) => cx.focus_view(&pane),
|
||||
Some(ActivateInDirectionTarget::Dock(dock)) => {
|
||||
dock.update(cx, |dock, cx| {
|
||||
// Defer this to avoid a panic when the dock's active panel is already on the stack.
|
||||
cx.defer(move |cx| {
|
||||
let dock = dock.read(cx);
|
||||
if let Some(panel) = dock.active_panel() {
|
||||
panel.focus_handle(cx).focus(cx);
|
||||
} else {
|
||||
log::error!("Could not find a focus target when in switching focus in {direction} direction for a {:?} dock", dock.position());
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
@@ -3112,7 +3101,10 @@ impl Workspace {
|
||||
pane::Event::Remove { focus_on_pane } => {
|
||||
self.remove_pane(pane, focus_on_pane.clone(), cx);
|
||||
}
|
||||
pane::Event::ActivateItem { local } => {
|
||||
pane::Event::ActivateItem {
|
||||
local,
|
||||
focus_changed,
|
||||
} => {
|
||||
cx.on_next_frame(|_, cx| {
|
||||
cx.invalidate_character_coordinates();
|
||||
});
|
||||
@@ -3127,6 +3119,7 @@ impl Workspace {
|
||||
self.active_item_path_changed(cx);
|
||||
self.update_active_view_for_followers(cx);
|
||||
}
|
||||
serialize_workspace = *focus_changed || &pane != self.active_pane();
|
||||
}
|
||||
pane::Event::UserSavedItem { item, save_intent } => {
|
||||
cx.emit(Event::UserSavedItem {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
description = "The fast, collaborative code editor."
|
||||
edition = "2021"
|
||||
name = "zed"
|
||||
version = "0.169.2"
|
||||
version = "0.170.0"
|
||||
publish = false
|
||||
license = "GPL-3.0-or-later"
|
||||
authors = ["Zed Team <hi@zed.dev>"]
|
||||
|
||||
@@ -1 +1 @@
|
||||
stable
|
||||
dev
|
||||
@@ -4,7 +4,7 @@ use client::Client;
|
||||
use collections::HashMap;
|
||||
use copilot::{Copilot, CopilotCompletionProvider};
|
||||
use editor::{Editor, EditorMode};
|
||||
use feature_flags::{FeatureFlagAppExt, PredictEditsFeatureFlag};
|
||||
use feature_flags::{FeatureFlagAppExt, ZetaFeatureFlag};
|
||||
use gpui::{AnyWindowHandle, AppContext, Context, ViewContext, WeakView};
|
||||
use language::language_settings::{all_language_settings, InlineCompletionProvider};
|
||||
use settings::SettingsStore;
|
||||
@@ -49,11 +49,11 @@ pub fn init(client: Arc<Client>, cx: &mut AppContext) {
|
||||
});
|
||||
}
|
||||
|
||||
if cx.has_flag::<PredictEditsFeatureFlag>() {
|
||||
if cx.has_flag::<ZetaFeatureFlag>() {
|
||||
cx.on_action(clear_zeta_edit_history);
|
||||
}
|
||||
|
||||
cx.observe_flag::<PredictEditsFeatureFlag, _>({
|
||||
cx.observe_flag::<ZetaFeatureFlag, _>({
|
||||
let editors = editors.clone();
|
||||
let client = client.clone();
|
||||
move |active, cx| {
|
||||
@@ -164,11 +164,8 @@ fn assign_inline_completion_provider(
|
||||
editor.set_inline_completion_provider(Some(provider), cx);
|
||||
}
|
||||
}
|
||||
|
||||
language::language_settings::InlineCompletionProvider::Zed => {
|
||||
if cx.has_flag::<PredictEditsFeatureFlag>()
|
||||
|| (cfg!(debug_assertions) && client.status().borrow().is_connected())
|
||||
{
|
||||
language::language_settings::InlineCompletionProvider::Zeta => {
|
||||
if cx.has_flag::<ZetaFeatureFlag>() || cfg!(debug_assertions) {
|
||||
let zeta = zeta::Zeta::register(client.clone(), cx);
|
||||
if let Some(buffer) = editor.buffer().read(cx).as_singleton() {
|
||||
if buffer.read(cx).file().is_some() {
|
||||
|
||||
@@ -39,7 +39,6 @@ telemetry.workspace = true
|
||||
telemetry_events.workspace = true
|
||||
theme.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
uuid.workspace = true
|
||||
workspace.workspace = true
|
||||
|
||||
|
||||
@@ -1,161 +0,0 @@
|
||||
use std::cmp;
|
||||
|
||||
use crate::InlineCompletion;
|
||||
use gpui::{
|
||||
point, prelude::*, quad, size, AnyElement, AppContext, Bounds, Corners, Edges, HighlightStyle,
|
||||
Hsla, StyledText, TextLayout, TextStyle,
|
||||
};
|
||||
use language::OffsetRangeExt;
|
||||
use settings::Settings;
|
||||
use theme::ThemeSettings;
|
||||
use ui::prelude::*;
|
||||
|
||||
pub struct CompletionDiffElement {
|
||||
element: AnyElement,
|
||||
text_layout: TextLayout,
|
||||
cursor_offset: usize,
|
||||
}
|
||||
|
||||
impl CompletionDiffElement {
|
||||
pub fn new(completion: &InlineCompletion, cx: &AppContext) -> Self {
|
||||
let mut diff = completion
|
||||
.snapshot
|
||||
.text_for_range(completion.excerpt_range.clone())
|
||||
.collect::<String>();
|
||||
|
||||
let mut cursor_offset_in_diff = None;
|
||||
let mut delta = 0;
|
||||
let mut diff_highlights = Vec::new();
|
||||
for (old_range, new_text) in completion.edits.iter() {
|
||||
let old_range = old_range.to_offset(&completion.snapshot);
|
||||
|
||||
if cursor_offset_in_diff.is_none() && completion.cursor_offset <= old_range.end {
|
||||
cursor_offset_in_diff =
|
||||
Some(completion.cursor_offset - completion.excerpt_range.start + delta);
|
||||
}
|
||||
|
||||
let old_start_in_diff = old_range.start - completion.excerpt_range.start + delta;
|
||||
let old_end_in_diff = old_range.end - completion.excerpt_range.start + delta;
|
||||
if old_start_in_diff < old_end_in_diff {
|
||||
diff_highlights.push((
|
||||
old_start_in_diff..old_end_in_diff,
|
||||
HighlightStyle {
|
||||
background_color: Some(cx.theme().status().deleted_background),
|
||||
strikethrough: Some(gpui::StrikethroughStyle {
|
||||
thickness: px(1.),
|
||||
color: Some(cx.theme().colors().text_muted),
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
if !new_text.is_empty() {
|
||||
diff.insert_str(old_end_in_diff, new_text);
|
||||
diff_highlights.push((
|
||||
old_end_in_diff..old_end_in_diff + new_text.len(),
|
||||
HighlightStyle {
|
||||
background_color: Some(cx.theme().status().created_background),
|
||||
..Default::default()
|
||||
},
|
||||
));
|
||||
delta += new_text.len();
|
||||
}
|
||||
}
|
||||
|
||||
let cursor_offset_in_diff = cursor_offset_in_diff
|
||||
.unwrap_or_else(|| completion.cursor_offset - completion.excerpt_range.start + delta);
|
||||
|
||||
let settings = ThemeSettings::get_global(cx).clone();
|
||||
let text_style = TextStyle {
|
||||
color: cx.theme().colors().editor_foreground,
|
||||
font_size: settings.buffer_font_size(cx).into(),
|
||||
font_family: settings.buffer_font.family,
|
||||
font_features: settings.buffer_font.features,
|
||||
font_fallbacks: settings.buffer_font.fallbacks,
|
||||
line_height: relative(settings.buffer_line_height.value()),
|
||||
font_weight: settings.buffer_font.weight,
|
||||
font_style: settings.buffer_font.style,
|
||||
..Default::default()
|
||||
};
|
||||
let element = StyledText::new(diff).with_highlights(&text_style, diff_highlights);
|
||||
let text_layout = element.layout().clone();
|
||||
|
||||
CompletionDiffElement {
|
||||
element: element.into_any_element(),
|
||||
text_layout,
|
||||
cursor_offset: cursor_offset_in_diff,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoElement for CompletionDiffElement {
|
||||
type Element = Self;
|
||||
|
||||
fn into_element(self) -> Self {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Element for CompletionDiffElement {
|
||||
type RequestLayoutState = ();
|
||||
type PrepaintState = ();
|
||||
|
||||
fn id(&self) -> Option<ElementId> {
|
||||
None
|
||||
}
|
||||
|
||||
fn request_layout(
|
||||
&mut self,
|
||||
_id: Option<&gpui::GlobalElementId>,
|
||||
cx: &mut WindowContext,
|
||||
) -> (gpui::LayoutId, Self::RequestLayoutState) {
|
||||
(self.element.request_layout(cx), ())
|
||||
}
|
||||
|
||||
fn prepaint(
|
||||
&mut self,
|
||||
_id: Option<&gpui::GlobalElementId>,
|
||||
_bounds: gpui::Bounds<Pixels>,
|
||||
_request_layout: &mut Self::RequestLayoutState,
|
||||
cx: &mut WindowContext,
|
||||
) -> Self::PrepaintState {
|
||||
self.element.prepaint(cx);
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
_id: Option<&gpui::GlobalElementId>,
|
||||
_bounds: gpui::Bounds<Pixels>,
|
||||
_request_layout: &mut Self::RequestLayoutState,
|
||||
_prepaint: &mut Self::PrepaintState,
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
if let Some(position) = self.text_layout.position_for_index(self.cursor_offset) {
|
||||
let bounds = self.text_layout.bounds();
|
||||
let line_height = self.text_layout.line_height();
|
||||
let line_width = self
|
||||
.text_layout
|
||||
.line_layout_for_index(self.cursor_offset)
|
||||
.map_or(bounds.size.width, |layout| layout.width());
|
||||
cx.paint_quad(quad(
|
||||
Bounds::new(
|
||||
point(bounds.origin.x, position.y),
|
||||
size(cmp::max(bounds.size.width, line_width), line_height),
|
||||
),
|
||||
Corners::default(),
|
||||
cx.theme().colors().editor_active_line_background,
|
||||
Edges::default(),
|
||||
Hsla::transparent_black(),
|
||||
));
|
||||
self.element.paint(cx);
|
||||
cx.paint_quad(quad(
|
||||
Bounds::new(position, size(px(2.), line_height)),
|
||||
Corners::default(),
|
||||
cx.theme().players().local().cursor,
|
||||
Edges::default(),
|
||||
Hsla::transparent_black(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,13 @@
|
||||
use crate::{CompletionDiffElement, InlineCompletion, InlineCompletionRating, Zeta};
|
||||
use crate::{InlineCompletion, InlineCompletionRating, Zeta};
|
||||
use editor::Editor;
|
||||
use gpui::{
|
||||
actions, prelude::*, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Model,
|
||||
View, ViewContext,
|
||||
actions, prelude::*, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView,
|
||||
HighlightStyle, Model, StyledText, TextStyle, View, ViewContext,
|
||||
};
|
||||
use language::language_settings;
|
||||
use language::{language_settings, OffsetRangeExt};
|
||||
use settings::Settings;
|
||||
use std::time::Duration;
|
||||
use theme::ThemeSettings;
|
||||
use ui::{prelude::*, KeyBinding, List, ListItem, ListItemSpacing, Tooltip};
|
||||
use workspace::{ModalView, Workspace};
|
||||
|
||||
@@ -13,6 +15,8 @@ actions!(
|
||||
zeta,
|
||||
[
|
||||
RateCompletions,
|
||||
ThumbsUp,
|
||||
ThumbsDown,
|
||||
ThumbsUpActiveCompletion,
|
||||
ThumbsDownActiveCompletion,
|
||||
NextEdit,
|
||||
@@ -37,7 +41,6 @@ pub struct RateCompletionModal {
|
||||
selected_index: usize,
|
||||
focus_handle: FocusHandle,
|
||||
_subscription: gpui::Subscription,
|
||||
current_view: RateCompletionView,
|
||||
}
|
||||
|
||||
struct ActiveCompletion {
|
||||
@@ -45,21 +48,6 @@ struct ActiveCompletion {
|
||||
feedback_editor: View<Editor>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
|
||||
enum RateCompletionView {
|
||||
SuggestedEdits,
|
||||
RawInput,
|
||||
}
|
||||
|
||||
impl RateCompletionView {
|
||||
pub fn name(&self) -> &'static str {
|
||||
match self {
|
||||
Self::SuggestedEdits => "Suggested Edits",
|
||||
Self::RawInput => "Recorded Events & Input",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RateCompletionModal {
|
||||
pub fn toggle(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
|
||||
if let Some(zeta) = Zeta::global(cx) {
|
||||
@@ -69,14 +57,12 @@ impl RateCompletionModal {
|
||||
|
||||
pub fn new(zeta: Model<Zeta>, cx: &mut ViewContext<Self>) -> Self {
|
||||
let subscription = cx.observe(&zeta, |_, _, cx| cx.notify());
|
||||
|
||||
Self {
|
||||
zeta,
|
||||
selected_index: 0,
|
||||
focus_handle: cx.focus_handle(),
|
||||
active_completion: None,
|
||||
_subscription: subscription,
|
||||
current_view: RateCompletionView::SuggestedEdits,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,7 +74,7 @@ impl RateCompletionModal {
|
||||
self.selected_index += 1;
|
||||
self.selected_index = usize::min(
|
||||
self.selected_index,
|
||||
self.zeta.read(cx).shown_completions().count(),
|
||||
self.zeta.read(cx).recent_completions().count(),
|
||||
);
|
||||
cx.notify();
|
||||
}
|
||||
@@ -102,7 +88,7 @@ impl RateCompletionModal {
|
||||
let next_index = self
|
||||
.zeta
|
||||
.read(cx)
|
||||
.shown_completions()
|
||||
.recent_completions()
|
||||
.skip(self.selected_index)
|
||||
.enumerate()
|
||||
.skip(1) // Skip straight to the next item
|
||||
@@ -117,12 +103,12 @@ impl RateCompletionModal {
|
||||
|
||||
fn select_prev_edit(&mut self, _: &PreviousEdit, cx: &mut ViewContext<Self>) {
|
||||
let zeta = self.zeta.read(cx);
|
||||
let completions_len = zeta.shown_completions_len();
|
||||
let completions_len = zeta.recent_completions_len();
|
||||
|
||||
let prev_index = self
|
||||
.zeta
|
||||
.read(cx)
|
||||
.shown_completions()
|
||||
.recent_completions()
|
||||
.rev()
|
||||
.skip((completions_len - 1) - self.selected_index)
|
||||
.enumerate()
|
||||
@@ -143,7 +129,28 @@ impl RateCompletionModal {
|
||||
}
|
||||
|
||||
fn select_last(&mut self, _: &menu::SelectLast, cx: &mut ViewContext<Self>) {
|
||||
self.selected_index = self.zeta.read(cx).shown_completions_len() - 1;
|
||||
self.selected_index = self.zeta.read(cx).recent_completions_len() - 1;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn thumbs_up(&mut self, _: &ThumbsUp, cx: &mut ViewContext<Self>) {
|
||||
self.zeta.update(cx, |zeta, cx| {
|
||||
let completion = zeta
|
||||
.recent_completions()
|
||||
.skip(self.selected_index)
|
||||
.next()
|
||||
.cloned();
|
||||
|
||||
if let Some(completion) = completion {
|
||||
zeta.rate_completion(
|
||||
&completion,
|
||||
InlineCompletionRating::Positive,
|
||||
"".to_string(),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
});
|
||||
self.select_next_edit(&Default::default(), cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
@@ -170,11 +177,7 @@ impl RateCompletionModal {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn thumbs_down_active(
|
||||
&mut self,
|
||||
_: &ThumbsDownActiveCompletion,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
fn thumbs_down_active(&mut self, _: &ThumbsDownActiveCompletion, cx: &mut ViewContext<Self>) {
|
||||
if let Some(active) = &self.active_completion {
|
||||
if active.feedback_editor.read(cx).text(cx).is_empty() {
|
||||
return;
|
||||
@@ -210,7 +213,7 @@ impl RateCompletionModal {
|
||||
let completion = self
|
||||
.zeta
|
||||
.read(cx)
|
||||
.shown_completions()
|
||||
.recent_completions()
|
||||
.skip(self.selected_index)
|
||||
.take(1)
|
||||
.next()
|
||||
@@ -223,7 +226,7 @@ impl RateCompletionModal {
|
||||
let completion = self
|
||||
.zeta
|
||||
.read(cx)
|
||||
.shown_completions()
|
||||
.recent_completions()
|
||||
.skip(self.selected_index)
|
||||
.take(1)
|
||||
.next()
|
||||
@@ -243,7 +246,7 @@ impl RateCompletionModal {
|
||||
self.selected_index = self
|
||||
.zeta
|
||||
.read(cx)
|
||||
.shown_completions()
|
||||
.recent_completions()
|
||||
.enumerate()
|
||||
.find(|(_, completion_b)| completion.id == completion_b.id)
|
||||
.map(|(ix, _)| ix)
|
||||
@@ -283,127 +286,99 @@ impl RateCompletionModal {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn render_view_nav(&self, cx: &ViewContext<Self>) -> impl IntoElement {
|
||||
h_flex()
|
||||
.h_8()
|
||||
.px_1()
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.bg(cx.theme().colors().elevated_surface_background)
|
||||
.gap_1()
|
||||
.child(
|
||||
Button::new(
|
||||
ElementId::Name("suggested-edits".into()),
|
||||
RateCompletionView::SuggestedEdits.name(),
|
||||
)
|
||||
.label_size(LabelSize::Small)
|
||||
.on_click(cx.listener(move |this, _, cx| {
|
||||
this.current_view = RateCompletionView::SuggestedEdits;
|
||||
cx.notify();
|
||||
}))
|
||||
.toggle_state(self.current_view == RateCompletionView::SuggestedEdits),
|
||||
)
|
||||
.child(
|
||||
Button::new(
|
||||
ElementId::Name("raw-input".into()),
|
||||
RateCompletionView::RawInput.name(),
|
||||
)
|
||||
.label_size(LabelSize::Small)
|
||||
.on_click(cx.listener(move |this, _, cx| {
|
||||
this.current_view = RateCompletionView::RawInput;
|
||||
cx.notify();
|
||||
}))
|
||||
.toggle_state(self.current_view == RateCompletionView::RawInput),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_suggested_edits(&self, cx: &mut ViewContext<Self>) -> Option<gpui::Stateful<Div>> {
|
||||
let active_completion = self.active_completion.as_ref()?;
|
||||
let bg_color = cx.theme().colors().editor_background;
|
||||
|
||||
Some(
|
||||
div()
|
||||
.id("diff")
|
||||
.p_4()
|
||||
.size_full()
|
||||
.bg(bg_color)
|
||||
.overflow_scroll()
|
||||
.whitespace_nowrap()
|
||||
.child(CompletionDiffElement::new(
|
||||
&active_completion.completion,
|
||||
cx,
|
||||
)),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_raw_input(&self, cx: &mut ViewContext<Self>) -> Option<gpui::Stateful<Div>> {
|
||||
Some(
|
||||
v_flex()
|
||||
.size_full()
|
||||
.overflow_hidden()
|
||||
.relative()
|
||||
.child(
|
||||
div()
|
||||
.id("raw-input")
|
||||
.py_4()
|
||||
.px_6()
|
||||
.size_full()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.overflow_scroll()
|
||||
.child(if let Some(active_completion) = &self.active_completion {
|
||||
format!(
|
||||
"{}\n{}",
|
||||
active_completion.completion.input_events,
|
||||
active_completion.completion.input_excerpt
|
||||
)
|
||||
} else {
|
||||
"No active completion".to_string()
|
||||
}),
|
||||
)
|
||||
.id("raw-input-view"),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_active_completion(&mut self, cx: &mut ViewContext<Self>) -> Option<impl IntoElement> {
|
||||
let active_completion = self.active_completion.as_ref()?;
|
||||
let completion_id = active_completion.completion.id;
|
||||
let focus_handle = &self.focus_handle(cx);
|
||||
|
||||
let border_color = cx.theme().colors().border;
|
||||
let bg_color = cx.theme().colors().editor_background;
|
||||
let mut diff = active_completion
|
||||
.completion
|
||||
.snapshot
|
||||
.text_for_range(active_completion.completion.excerpt_range.clone())
|
||||
.collect::<String>();
|
||||
|
||||
let mut delta = 0;
|
||||
let mut diff_highlights = Vec::new();
|
||||
for (old_range, new_text) in active_completion.completion.edits.iter() {
|
||||
let old_range = old_range.to_offset(&active_completion.completion.snapshot);
|
||||
let old_start_in_text =
|
||||
old_range.start - active_completion.completion.excerpt_range.start + delta;
|
||||
let old_end_in_text =
|
||||
old_range.end - active_completion.completion.excerpt_range.start + delta;
|
||||
if old_start_in_text < old_end_in_text {
|
||||
diff_highlights.push((
|
||||
old_start_in_text..old_end_in_text,
|
||||
HighlightStyle {
|
||||
background_color: Some(cx.theme().status().deleted_background),
|
||||
strikethrough: Some(gpui::StrikethroughStyle {
|
||||
thickness: px(1.),
|
||||
color: Some(cx.theme().colors().text_muted),
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
if !new_text.is_empty() {
|
||||
diff.insert_str(old_end_in_text, new_text);
|
||||
diff_highlights.push((
|
||||
old_end_in_text..old_end_in_text + new_text.len(),
|
||||
HighlightStyle {
|
||||
background_color: Some(cx.theme().status().created_background),
|
||||
..Default::default()
|
||||
},
|
||||
));
|
||||
delta += new_text.len();
|
||||
}
|
||||
}
|
||||
|
||||
let settings = ThemeSettings::get_global(cx).clone();
|
||||
let text_style = TextStyle {
|
||||
color: cx.theme().colors().editor_foreground,
|
||||
font_size: settings.buffer_font_size(cx).into(),
|
||||
font_family: settings.buffer_font.family,
|
||||
font_features: settings.buffer_font.features,
|
||||
font_fallbacks: settings.buffer_font.fallbacks,
|
||||
line_height: relative(settings.buffer_line_height.value()),
|
||||
font_weight: settings.buffer_font.weight,
|
||||
font_style: settings.buffer_font.style,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let rated = self.zeta.read(cx).is_completion_rated(completion_id);
|
||||
let was_shown = self.zeta.read(cx).was_completion_shown(completion_id);
|
||||
let feedback_empty = active_completion
|
||||
.feedback_editor
|
||||
.read(cx)
|
||||
.text(cx)
|
||||
.is_empty();
|
||||
|
||||
let label_container = h_flex().pl_1().gap_1p5();
|
||||
let border_color = cx.theme().colors().border;
|
||||
let bg_color = cx.theme().colors().editor_background;
|
||||
|
||||
let label_container = || h_flex().pl_1().gap_1p5();
|
||||
|
||||
Some(
|
||||
v_flex()
|
||||
.size_full()
|
||||
.overflow_hidden()
|
||||
.relative()
|
||||
.child(
|
||||
v_flex()
|
||||
div()
|
||||
.id("diff")
|
||||
.py_4()
|
||||
.px_6()
|
||||
.size_full()
|
||||
.overflow_hidden()
|
||||
.relative()
|
||||
.child(self.render_view_nav(cx))
|
||||
.when_some(match self.current_view {
|
||||
RateCompletionView::SuggestedEdits => self.render_suggested_edits(cx),
|
||||
RateCompletionView::RawInput => self.render_raw_input(cx),
|
||||
}, |this, element| this.child(element))
|
||||
.bg(bg_color)
|
||||
.overflow_scroll()
|
||||
.child(StyledText::new(diff).with_highlights(&text_style, diff_highlights)),
|
||||
)
|
||||
.when(!rated, |this| {
|
||||
.when_some((!rated).then(|| ()), |this, _| {
|
||||
this.child(
|
||||
h_flex()
|
||||
.p_2()
|
||||
.gap_2()
|
||||
.border_y_1()
|
||||
.border_color(border_color)
|
||||
|
||||
.child(
|
||||
Icon::new(IconName::Info)
|
||||
.size(IconSize::XSmall)
|
||||
@@ -415,14 +390,14 @@ impl RateCompletionModal {
|
||||
.pr_2()
|
||||
.flex_wrap()
|
||||
.child(
|
||||
Label::new("Explain why this completion is good or bad. If it's negative, describe what you expected instead.")
|
||||
Label::new("Ensure you explain why this completion is negative or positive. In case it's negative, report what you expected instead.")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
)
|
||||
)
|
||||
)
|
||||
})
|
||||
.when(!rated, |this| {
|
||||
.when_some((!rated).then(|| ()), |this, _| {
|
||||
this.child(
|
||||
div()
|
||||
.h_40()
|
||||
@@ -442,7 +417,7 @@ impl RateCompletionModal {
|
||||
.justify_between()
|
||||
.children(if rated {
|
||||
Some(
|
||||
label_container
|
||||
label_container()
|
||||
.child(
|
||||
Icon::new(IconName::Check)
|
||||
.size(IconSize::Small)
|
||||
@@ -452,7 +427,7 @@ impl RateCompletionModal {
|
||||
)
|
||||
} else if active_completion.completion.edits.is_empty() {
|
||||
Some(
|
||||
label_container
|
||||
label_container()
|
||||
.child(
|
||||
Icon::new(IconName::Warning)
|
||||
.size(IconSize::Small)
|
||||
@@ -460,14 +435,30 @@ impl RateCompletionModal {
|
||||
)
|
||||
.child(Label::new("No edits produced.").color(Color::Muted)),
|
||||
)
|
||||
} else if !was_shown {
|
||||
Some(
|
||||
label_container()
|
||||
.child(
|
||||
Icon::new(IconName::Warning)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Warning),
|
||||
)
|
||||
.child(Label::new("Completion wasn't shown because another valid one was already on screen.")),
|
||||
)
|
||||
} else {
|
||||
Some(label_container)
|
||||
Some(label_container())
|
||||
})
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
Button::new("bad", "Bad Completion")
|
||||
.key_binding(KeyBinding::for_action_in(
|
||||
&ThumbsDown,
|
||||
&self.focus_handle(cx),
|
||||
cx,
|
||||
))
|
||||
.style(ButtonStyle::Filled)
|
||||
.icon(IconName::ThumbsDown)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_position(IconPosition::Start)
|
||||
@@ -477,11 +468,6 @@ impl RateCompletionModal {
|
||||
Tooltip::text("Explain what's bad about it before reporting it", cx)
|
||||
})
|
||||
})
|
||||
.key_binding(KeyBinding::for_action_in(
|
||||
&ThumbsDownActiveCompletion,
|
||||
focus_handle,
|
||||
cx,
|
||||
))
|
||||
.on_click(cx.listener(move |this, _, cx| {
|
||||
this.thumbs_down_active(
|
||||
&ThumbsDownActiveCompletion,
|
||||
@@ -491,15 +477,16 @@ impl RateCompletionModal {
|
||||
)
|
||||
.child(
|
||||
Button::new("good", "Good Completion")
|
||||
.key_binding(KeyBinding::for_action_in(
|
||||
&ThumbsUp,
|
||||
&self.focus_handle(cx),
|
||||
cx,
|
||||
))
|
||||
.style(ButtonStyle::Filled)
|
||||
.icon(IconName::ThumbsUp)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_position(IconPosition::Start)
|
||||
.disabled(rated)
|
||||
.key_binding(KeyBinding::for_action_in(
|
||||
&ThumbsUpActiveCompletion,
|
||||
focus_handle,
|
||||
cx,
|
||||
))
|
||||
.on_click(cx.listener(move |this, _, cx| {
|
||||
this.thumbs_up_active(&ThumbsUpActiveCompletion, cx);
|
||||
})),
|
||||
@@ -525,6 +512,7 @@ impl Render for RateCompletionModal {
|
||||
.on_action(cx.listener(Self::select_next_edit))
|
||||
.on_action(cx.listener(Self::select_first))
|
||||
.on_action(cx.listener(Self::select_last))
|
||||
.on_action(cx.listener(Self::thumbs_up))
|
||||
.on_action(cx.listener(Self::thumbs_up_active))
|
||||
.on_action(cx.listener(Self::thumbs_down_active))
|
||||
.on_action(cx.listener(Self::focus_completions))
|
||||
@@ -538,16 +526,16 @@ impl Render for RateCompletionModal {
|
||||
.shadow_lg()
|
||||
.child(
|
||||
v_flex()
|
||||
.w_72()
|
||||
.h_full()
|
||||
.border_r_1()
|
||||
.border_color(border_color)
|
||||
.w_96()
|
||||
.h_full()
|
||||
.flex_shrink_0()
|
||||
.overflow_hidden()
|
||||
.child(
|
||||
h_flex()
|
||||
.h_8()
|
||||
.px_2()
|
||||
.py_1()
|
||||
.justify_between()
|
||||
.border_b_1()
|
||||
.border_color(border_color)
|
||||
@@ -573,12 +561,12 @@ impl Render for RateCompletionModal {
|
||||
div()
|
||||
.p_2()
|
||||
.child(
|
||||
Label::new("No completions yet. Use the editor to generate some, and make sure to rate them!")
|
||||
Label::new("No completions yet. Use the editor to generate some and rate them!")
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.into_any_element(),
|
||||
)
|
||||
.children(self.zeta.read(cx).shown_completions().cloned().enumerate().map(
|
||||
.children(self.zeta.read(cx).recent_completions().cloned().enumerate().map(
|
||||
|(index, completion)| {
|
||||
let selected =
|
||||
self.active_completion.as_ref().map_or(false, |selected| {
|
||||
@@ -587,45 +575,27 @@ impl Render for RateCompletionModal {
|
||||
let rated =
|
||||
self.zeta.read(cx).is_completion_rated(completion.id);
|
||||
|
||||
let (icon_name, icon_color, tooltip_text) = match (rated, completion.edits.is_empty()) {
|
||||
(true, _) => (IconName::Check, Color::Success, "Rated Completion"),
|
||||
(false, true) => (IconName::File, Color::Muted, "No Edits Produced"),
|
||||
(false, false) => (IconName::FileDiff, Color::Accent, "Edits Available"),
|
||||
};
|
||||
|
||||
let file_name = completion.path.file_name().map(|f| f.to_string_lossy().to_string()).unwrap_or("untitled".to_string());
|
||||
let file_path = completion.path.parent().map(|p| p.to_string_lossy().to_string());
|
||||
|
||||
ListItem::new(completion.id)
|
||||
.inset(true)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.focused(index == self.selected_index)
|
||||
.toggle_state(selected)
|
||||
.start_slot(if rated {
|
||||
Icon::new(IconName::Check).color(Color::Success).size(IconSize::Small)
|
||||
} else if completion.edits.is_empty() {
|
||||
Icon::new(IconName::File).color(Color::Muted).size(IconSize::Small)
|
||||
} else {
|
||||
Icon::new(IconName::FileDiff).color(Color::Accent).size(IconSize::Small)
|
||||
})
|
||||
.child(
|
||||
h_flex()
|
||||
.id("completion-content")
|
||||
.gap_3()
|
||||
.child(
|
||||
Icon::new(icon_name)
|
||||
.color(icon_color)
|
||||
.size(IconSize::Small)
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
.child(
|
||||
h_flex().gap_1()
|
||||
.child(Label::new(file_name).size(LabelSize::Small))
|
||||
.when_some(file_path, |this, p| this.child(Label::new(p).size(LabelSize::Small).color(Color::Muted)))
|
||||
)
|
||||
.child(Label::new(format!("{} ago, {:.2?}", format_time_ago(completion.response_received_at.elapsed()), completion.latency()))
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::XSmall)
|
||||
)
|
||||
v_flex()
|
||||
.pl_1p5()
|
||||
.child(Label::new(completion.path.to_string_lossy().to_string()).size(LabelSize::Small))
|
||||
.child(Label::new(format!("{} ago, {:.2?}", format_time_ago(completion.response_received_at.elapsed()), completion.latency()))
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::XSmall)
|
||||
)
|
||||
)
|
||||
.tooltip(move |cx| {
|
||||
Tooltip::text(tooltip_text, cx)
|
||||
})
|
||||
.on_click(cx.listener(move |this, _, cx| {
|
||||
this.select_completion(Some(completion.clone()), true, cx);
|
||||
}))
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
mod completion_diff_element;
|
||||
mod rate_completion_modal;
|
||||
|
||||
pub(crate) use completion_diff_element::*;
|
||||
pub use rate_completion_modal::*;
|
||||
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
@@ -32,7 +30,6 @@ use std::{
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use telemetry_events::InlineCompletionRating;
|
||||
use util::ResultExt;
|
||||
use uuid::Uuid;
|
||||
|
||||
const CURSOR_MARKER: &'static str = "<|user_cursor_is_here|>";
|
||||
@@ -74,7 +71,6 @@ pub struct InlineCompletion {
|
||||
id: InlineCompletionId,
|
||||
path: Arc<Path>,
|
||||
excerpt_range: Range<usize>,
|
||||
cursor_offset: usize,
|
||||
edits: Arc<[(Range<Anchor>, String)]>,
|
||||
snapshot: BufferSnapshot,
|
||||
input_outline: Arc<str>,
|
||||
@@ -158,8 +154,9 @@ pub struct Zeta {
|
||||
client: Arc<Client>,
|
||||
events: VecDeque<Event>,
|
||||
registered_buffers: HashMap<gpui::EntityId, RegisteredBuffer>,
|
||||
shown_completions: VecDeque<InlineCompletion>,
|
||||
recent_completions: VecDeque<InlineCompletion>,
|
||||
rated_completions: HashSet<InlineCompletionId>,
|
||||
shown_completions: HashSet<InlineCompletionId>,
|
||||
llm_token: LlmApiToken,
|
||||
_llm_token_subscription: Subscription,
|
||||
}
|
||||
@@ -187,8 +184,9 @@ impl Zeta {
|
||||
Self {
|
||||
client,
|
||||
events: VecDeque::new(),
|
||||
shown_completions: VecDeque::new(),
|
||||
recent_completions: VecDeque::new(),
|
||||
rated_completions: HashSet::default(),
|
||||
shown_completions: HashSet::default(),
|
||||
registered_buffers: HashMap::default(),
|
||||
llm_token: LlmApiToken::default(),
|
||||
_llm_token_subscription: cx.subscribe(
|
||||
@@ -207,7 +205,7 @@ impl Zeta {
|
||||
}
|
||||
|
||||
fn push_event(&mut self, event: Event) {
|
||||
const MAX_EVENT_COUNT: usize = 16;
|
||||
const MAX_EVENT_COUNT: usize = 20;
|
||||
|
||||
if let Some(Event::BufferChange {
|
||||
new_snapshot: last_new_snapshot,
|
||||
@@ -233,8 +231,8 @@ impl Zeta {
|
||||
}
|
||||
|
||||
self.events.push_back(event);
|
||||
if self.events.len() >= MAX_EVENT_COUNT {
|
||||
self.events.drain(..MAX_EVENT_COUNT / 2);
|
||||
if self.events.len() > MAX_EVENT_COUNT {
|
||||
self.events.pop_front();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -293,13 +291,13 @@ impl Zeta {
|
||||
let events = self.events.clone();
|
||||
let path = snapshot
|
||||
.file()
|
||||
.map(|f| Arc::from(f.full_path(cx).as_path()))
|
||||
.map(|f| f.path().clone())
|
||||
.unwrap_or_else(|| Arc::from(Path::new("untitled")));
|
||||
|
||||
let client = self.client.clone();
|
||||
let llm_token = self.llm_token.clone();
|
||||
|
||||
cx.spawn(|_, cx| async move {
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let request_sent_at = Instant::now();
|
||||
|
||||
let (input_events, input_excerpt, input_outline) = cx
|
||||
@@ -338,11 +336,10 @@ impl Zeta {
|
||||
let output_excerpt = response.output_excerpt;
|
||||
log::debug!("completion response: {}", output_excerpt);
|
||||
|
||||
Self::process_completion_response(
|
||||
let inline_completion = Self::process_completion_response(
|
||||
output_excerpt,
|
||||
&snapshot,
|
||||
excerpt_range,
|
||||
offset,
|
||||
path,
|
||||
input_outline,
|
||||
input_events,
|
||||
@@ -350,7 +347,20 @@ impl Zeta {
|
||||
request_sent_at,
|
||||
&cx,
|
||||
)
|
||||
.await
|
||||
.await?;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.recent_completions
|
||||
.push_front(inline_completion.clone());
|
||||
if this.recent_completions.len() > 50 {
|
||||
let completion = this.recent_completions.pop_back().unwrap();
|
||||
this.shown_completions.remove(&completion.id);
|
||||
this.rated_completions.remove(&completion.id);
|
||||
}
|
||||
cx.notify();
|
||||
})?;
|
||||
|
||||
Ok(inline_completion)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -483,8 +493,8 @@ and then another
|
||||
}
|
||||
|
||||
zeta.update(&mut cx, |zeta, _cx| {
|
||||
zeta.shown_completions.get_mut(2).unwrap().edits = Arc::new([]);
|
||||
zeta.shown_completions.get_mut(3).unwrap().edits = Arc::new([]);
|
||||
zeta.recent_completions.get_mut(2).unwrap().edits = Arc::new([]);
|
||||
zeta.recent_completions.get_mut(3).unwrap().edits = Arc::new([]);
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
@@ -567,7 +577,6 @@ and then another
|
||||
output_excerpt: String,
|
||||
snapshot: &BufferSnapshot,
|
||||
excerpt_range: Range<usize>,
|
||||
cursor_offset: usize,
|
||||
path: Arc<Path>,
|
||||
input_outline: String,
|
||||
input_events: String,
|
||||
@@ -627,7 +636,6 @@ and then another
|
||||
id: InlineCompletionId::new(),
|
||||
path,
|
||||
excerpt_range,
|
||||
cursor_offset,
|
||||
edits: edits.into(),
|
||||
snapshot: snapshot.clone(),
|
||||
input_outline: input_outline.into(),
|
||||
@@ -710,13 +718,12 @@ and then another
|
||||
self.rated_completions.contains(&completion_id)
|
||||
}
|
||||
|
||||
pub fn completion_shown(&mut self, completion: &InlineCompletion, cx: &mut ModelContext<Self>) {
|
||||
self.shown_completions.push_front(completion.clone());
|
||||
if self.shown_completions.len() > 50 {
|
||||
let completion = self.shown_completions.pop_back().unwrap();
|
||||
self.rated_completions.remove(&completion.id);
|
||||
}
|
||||
cx.notify();
|
||||
pub fn was_completion_shown(&self, completion_id: InlineCompletionId) -> bool {
|
||||
self.shown_completions.contains(&completion_id)
|
||||
}
|
||||
|
||||
pub fn completion_shown(&mut self, completion_id: InlineCompletionId) {
|
||||
self.shown_completions.insert(completion_id);
|
||||
}
|
||||
|
||||
pub fn rate_completion(
|
||||
@@ -726,7 +733,6 @@ and then another
|
||||
feedback: String,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
self.rated_completions.insert(completion.id);
|
||||
telemetry::event!(
|
||||
"Inline Completion Rated",
|
||||
rating,
|
||||
@@ -740,12 +746,12 @@ and then another
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn shown_completions(&self) -> impl DoubleEndedIterator<Item = &InlineCompletion> {
|
||||
self.shown_completions.iter()
|
||||
pub fn recent_completions(&self) -> impl DoubleEndedIterator<Item = &InlineCompletion> {
|
||||
self.recent_completions.iter()
|
||||
}
|
||||
|
||||
pub fn shown_completions_len(&self) -> usize {
|
||||
self.shown_completions.len()
|
||||
pub fn recent_completions_len(&self) -> usize {
|
||||
self.recent_completions.len()
|
||||
}
|
||||
|
||||
fn report_changes_for_buffer(
|
||||
@@ -968,7 +974,7 @@ impl CurrentInlineCompletion {
|
||||
|
||||
struct PendingCompletion {
|
||||
id: usize,
|
||||
_task: Task<()>,
|
||||
_task: Task<Result<()>>,
|
||||
}
|
||||
|
||||
pub struct ZetaInlineCompletionProvider {
|
||||
@@ -1046,16 +1052,13 @@ impl inline_completion::InlineCompletionProvider for ZetaInlineCompletionProvide
|
||||
})
|
||||
});
|
||||
|
||||
let completion = match completion_request {
|
||||
Ok(completion_request) => {
|
||||
let completion_request = completion_request.await;
|
||||
completion_request.map(|completion| CurrentInlineCompletion {
|
||||
buffer_id: buffer.entity_id(),
|
||||
completion,
|
||||
})
|
||||
}
|
||||
Err(error) => Err(error),
|
||||
};
|
||||
let mut completion = None;
|
||||
if let Ok(completion_request) = completion_request {
|
||||
completion = Some(CurrentInlineCompletion {
|
||||
buffer_id: buffer.entity_id(),
|
||||
completion: completion_request.await?,
|
||||
});
|
||||
}
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
if this.pending_completions[0].id == pending_completion_id {
|
||||
@@ -1064,27 +1067,27 @@ impl inline_completion::InlineCompletionProvider for ZetaInlineCompletionProvide
|
||||
this.pending_completions.clear();
|
||||
}
|
||||
|
||||
if let Some(new_completion) = completion.context("zeta prediction failed").log_err()
|
||||
{
|
||||
if let Some(new_completion) = completion {
|
||||
if let Some(old_completion) = this.current_completion.as_ref() {
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
if new_completion.should_replace_completion(&old_completion, &snapshot) {
|
||||
this.zeta.update(cx, |zeta, cx| {
|
||||
zeta.completion_shown(&new_completion.completion, cx);
|
||||
this.zeta.update(cx, |zeta, _cx| {
|
||||
zeta.completion_shown(new_completion.completion.id)
|
||||
});
|
||||
this.current_completion = Some(new_completion);
|
||||
}
|
||||
} else {
|
||||
this.zeta.update(cx, |zeta, cx| {
|
||||
zeta.completion_shown(&new_completion.completion, cx);
|
||||
this.zeta.update(cx, |zeta, _cx| {
|
||||
zeta.completion_shown(new_completion.completion.id)
|
||||
});
|
||||
this.current_completion = Some(new_completion);
|
||||
}
|
||||
} else {
|
||||
this.current_completion = None;
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
});
|
||||
|
||||
// We always maintain at most two pending completions. When we already
|
||||
@@ -1209,7 +1212,6 @@ mod tests {
|
||||
snapshot: buffer.read(cx).snapshot(),
|
||||
id: InlineCompletionId::new(),
|
||||
excerpt_range: 0..0,
|
||||
cursor_offset: 0,
|
||||
input_outline: "".into(),
|
||||
input_events: "".into(),
|
||||
input_excerpt: "".into(),
|
||||
|
||||
@@ -2262,6 +2262,7 @@ Run the `theme selector: toggle` action in the command palette to see a current
|
||||
"button": true,
|
||||
"default_width": 240,
|
||||
"dock": "left",
|
||||
"entry_spacing": "comfortable",
|
||||
"file_icons": true,
|
||||
"folder_icons": true,
|
||||
"git_status": true,
|
||||
@@ -2303,6 +2304,30 @@ Run the `theme selector: toggle` action in the command palette to see a current
|
||||
}
|
||||
```
|
||||
|
||||
### Entry Spacing
|
||||
|
||||
- Description: Spacing between worktree entries
|
||||
- Setting: `entry_spacing`
|
||||
- Default: `comfortable`
|
||||
|
||||
**Options**
|
||||
|
||||
1. Comfortable entry spacing
|
||||
|
||||
```json
|
||||
{
|
||||
"entry_spacing": "comfortable"
|
||||
}
|
||||
```
|
||||
|
||||
2. Standard entry spacing
|
||||
|
||||
```json
|
||||
{
|
||||
"entry_spacing": "standard"
|
||||
}
|
||||
```
|
||||
|
||||
### Git Status
|
||||
|
||||
- Description: Indicates newly created and updated files
|
||||
|
||||
@@ -124,3 +124,16 @@ Then clean and rebuild the project:
|
||||
cargo clean
|
||||
cargo run
|
||||
```
|
||||
|
||||
## Tips & Tricks
|
||||
|
||||
If you are building Zed a lot, you may find that macOS continually verifies new
|
||||
builds which can add a few seconds to your iteration cycles.
|
||||
|
||||
To fix this, you can:
|
||||
|
||||
- Run `sudo spctl developer-mode enable-terminal` to enable the Developer Tools panel in System Settings.
|
||||
- In System Settings, search for "Developer Tools" and add your terminal (e.g. iTerm or Ghostty) to the list under "Allow applications to use developer tools"
|
||||
- Restart your terminal.
|
||||
|
||||
Thanks to the nextest developers for publishing [this](https://nexte.st/docs/installation/macos/#gatekeeper).
|
||||
|
||||
@@ -170,3 +170,13 @@ rm ~/.local/zed.app/lib/libcrypto.so.1.1
|
||||
```
|
||||
|
||||
This will force zed to fallback to the system `libssl` and `libcrypto` libraries.
|
||||
|
||||
### Editing files requiring root access
|
||||
|
||||
When you try to edit files that require root access, Zed requires `pkexec` (part of polkit) to handle authentication prompts.
|
||||
|
||||
Polkit comes pre-installed with most desktop environments like GNOME and KDE. If you're using a minimal system and polkit is not installed, you can install it with:
|
||||
|
||||
- Ubuntu/Debian: `sudo apt install policykit-1`
|
||||
- Fedora: `sudo dnf install polkit`
|
||||
- Arch Linux: `sudo pacman -S polkit`
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
channel = "1.81"
|
||||
profile = "minimal"
|
||||
components = [ "rustfmt", "clippy" ]
|
||||
targets = [ "x86_64-apple-darwin", "aarch64-apple-darwin", "x86_64-unknown-linux-gnu", "wasm32-wasip1", "x86_64-pc-windows-msvc" ]
|
||||
targets = [ "x86_64-apple-darwin", "aarch64-apple-darwin", "x86_64-unknown-linux-gnu", "wasm32-wasip2", "x86_64-pc-windows-msvc" ]
|
||||
|
||||
Reference in New Issue
Block a user