Compare commits
41 Commits
settings-u
...
v0.169.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
61b01a0ca9 | ||
|
|
41a267d49b | ||
|
|
349fb5254d | ||
|
|
05563d25b7 | ||
|
|
efb6342e9d | ||
|
|
68737cae56 | ||
|
|
9677a7d8ac | ||
|
|
aa2d6378a4 | ||
|
|
747c7e9b58 | ||
|
|
458637f2e1 | ||
|
|
2a6f6ce5b6 | ||
|
|
395d2e7d46 | ||
|
|
6e1e392853 | ||
|
|
8a33b2b450 | ||
|
|
96c5be5fa5 | ||
|
|
7fafc88706 | ||
|
|
db503cf48b | ||
|
|
745f6ceb3a | ||
|
|
8d807cedda | ||
|
|
680e5f149b | ||
|
|
e4bcbc0eea | ||
|
|
7fe8a4449e | ||
|
|
a917a894bf | ||
|
|
0d03674def | ||
|
|
8fb1d135ad | ||
|
|
62e098b365 | ||
|
|
fd6ac07fc2 | ||
|
|
18b08e2eae | ||
|
|
dc90aaa4cd | ||
|
|
142f949e73 | ||
|
|
9c0c853a2c | ||
|
|
e74e2863f9 | ||
|
|
784c1280f3 | ||
|
|
75f682b80f | ||
|
|
8052cf8415 | ||
|
|
d1bb1f14e0 | ||
|
|
8d027b819d | ||
|
|
88f7071fc7 | ||
|
|
a297e19866 | ||
|
|
92ba505b92 | ||
|
|
9cb86456a2 |
59
.github/workflows/ci.yml
vendored
59
.github/workflows/ci.yml
vendored
@@ -10,7 +10,6 @@ on:
|
||||
pull_request:
|
||||
branches:
|
||||
- "**"
|
||||
merge_group:
|
||||
|
||||
concurrency:
|
||||
# Allow only one workflow per any non-`main` branch.
|
||||
@@ -24,31 +23,6 @@ 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'
|
||||
@@ -121,7 +95,6 @@ jobs:
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- test
|
||||
needs: check_docs_only
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
@@ -129,35 +102,29 @@ 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' && needs.check_docs_only.outputs.docs_only == 'false'
|
||||
if: github.event_name == 'pull_request'
|
||||
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"
|
||||
@@ -171,7 +138,6 @@ 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
|
||||
@@ -182,26 +148,21 @@ 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
|
||||
@@ -212,7 +173,6 @@ 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
|
||||
@@ -223,18 +183,15 @@ 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
|
||||
@@ -243,7 +200,6 @@ 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
|
||||
@@ -254,23 +210,20 @@ 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: 60
|
||||
timeout-minutes: 120
|
||||
name: Create a macOS bundle
|
||||
runs-on:
|
||||
- self-hosted
|
||||
@@ -359,9 +312,9 @@ jobs:
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
bundle-linux:
|
||||
bundle-linux-x86_x64:
|
||||
timeout-minutes: 60
|
||||
name: Create a Linux bundle
|
||||
name: Linux x86_x64 release bundle
|
||||
runs-on:
|
||||
- buildjet-16vcpu-ubuntu-2004
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/v') || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
|
||||
@@ -409,7 +362,7 @@ jobs:
|
||||
|
||||
bundle-linux-aarch64: # this runs on ubuntu22.04
|
||||
timeout-minutes: 60
|
||||
name: Create arm64 Linux bundle
|
||||
name: Linux arm64 release bundle
|
||||
runs-on:
|
||||
- buildjet-16vcpu-ubuntu-2204-arm
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/v') || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
|
||||
@@ -458,7 +411,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, bundle-linux-aarch64]
|
||||
needs: [bundle-mac, bundle-linux-x86_x64, bundle-linux-aarch64]
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- bundle
|
||||
|
||||
1
.github/workflows/docs.yml
vendored
1
.github/workflows/docs.yml
vendored
@@ -7,7 +7,6 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
merge_group:
|
||||
|
||||
jobs:
|
||||
check_formatting:
|
||||
|
||||
18
Cargo.lock
generated
18
Cargo.lock
generated
@@ -2666,6 +2666,7 @@ dependencies = [
|
||||
"envy",
|
||||
"extension",
|
||||
"file_finder",
|
||||
"fireworks",
|
||||
"fs",
|
||||
"futures 0.3.31",
|
||||
"git",
|
||||
@@ -4590,6 +4591,17 @@ dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fireworks"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"futures 0.3.31",
|
||||
"http_client",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fixedbitset"
|
||||
version = "0.4.2"
|
||||
@@ -4794,13 +4806,11 @@ dependencies = [
|
||||
"rope",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"shlex",
|
||||
"smol",
|
||||
"tempfile",
|
||||
"text",
|
||||
"time",
|
||||
"util",
|
||||
"which 6.0.3",
|
||||
"windows 0.58.0",
|
||||
]
|
||||
|
||||
@@ -6297,6 +6307,7 @@ dependencies = [
|
||||
"futures 0.3.31",
|
||||
"gpui",
|
||||
"indoc",
|
||||
"inline_completion",
|
||||
"language",
|
||||
"lsp",
|
||||
"paths",
|
||||
@@ -16199,7 +16210,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed"
|
||||
version = "0.169.0"
|
||||
version = "0.169.3"
|
||||
dependencies = [
|
||||
"activity_indicator",
|
||||
"anyhow",
|
||||
@@ -16653,6 +16664,7 @@ dependencies = [
|
||||
"tree-sitter-go",
|
||||
"tree-sitter-rust",
|
||||
"ui",
|
||||
"util",
|
||||
"uuid",
|
||||
"workspace",
|
||||
"worktree",
|
||||
|
||||
@@ -40,6 +40,7 @@ members = [
|
||||
"crates/feedback",
|
||||
"crates/file_finder",
|
||||
"crates/file_icons",
|
||||
"crates/fireworks",
|
||||
"crates/fs",
|
||||
"crates/fsevent",
|
||||
"crates/fuzzy",
|
||||
@@ -222,6 +223,7 @@ 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" }
|
||||
|
||||
@@ -805,7 +805,8 @@
|
||||
"context": "RateCompletionModal",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-enter": "zeta::ThumbsUp",
|
||||
"cmd-shift-enter": "zeta::ThumbsUpActiveCompletion",
|
||||
"cmd-shift-backspace": "zeta::ThumbsDownActiveCompletion",
|
||||
"shift-down": "zeta::NextEdit",
|
||||
"shift-up": "zeta::PreviousEdit",
|
||||
"right": "zeta::PreviewCompletion"
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Workspace && !Terminal",
|
||||
"context": "Workspace",
|
||||
"bindings": {
|
||||
"ctrl-x ctrl-c": "zed::Quit", // save-buffers-kill-terminal
|
||||
"ctrl-x 5 0": "workspace::CloseWindow", // delete-frame
|
||||
@@ -72,6 +72,18 @@
|
||||
"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": {
|
||||
|
||||
@@ -110,7 +110,7 @@
|
||||
"g y": "editor::GoToTypeDefinition",
|
||||
"g shift-i": "editor::GoToImplementation",
|
||||
"g x": "editor::OpenUrl",
|
||||
"g f": "editor::OpenFile",
|
||||
"g f": "editor::OpenSelectedFilename",
|
||||
"g n": "vim::SelectNextMatch",
|
||||
"g shift-n": "vim::SelectPreviousMatch",
|
||||
"g l": "vim::SelectNext",
|
||||
|
||||
@@ -16,7 +16,9 @@ use editor::{
|
||||
EditorStyle, ExcerptId, ExcerptRange, GutterDimensions, MultiBuffer, MultiBufferSnapshot,
|
||||
ToOffset as _, ToPoint,
|
||||
};
|
||||
use feature_flags::{FeatureFlagAppExt as _, ZedPro};
|
||||
use feature_flags::{
|
||||
Assistant2FeatureFlag, FeatureFlagAppExt as _, FeatureFlagViewExt as _, ZedPro,
|
||||
};
|
||||
use fs::Fs;
|
||||
use futures::{
|
||||
channel::mpsc,
|
||||
@@ -73,7 +75,16 @@ 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();
|
||||
}
|
||||
@@ -91,6 +102,7 @@ pub struct InlineAssistant {
|
||||
prompt_builder: Arc<PromptBuilder>,
|
||||
telemetry: Arc<Telemetry>,
|
||||
fs: Arc<dyn Fs>,
|
||||
is_assistant2_enabled: bool,
|
||||
}
|
||||
|
||||
impl Global for InlineAssistant {}
|
||||
@@ -112,6 +124,7 @@ impl InlineAssistant {
|
||||
prompt_builder,
|
||||
telemetry,
|
||||
fs,
|
||||
is_assistant2_enabled: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -172,15 +185,22 @@ 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| {
|
||||
editor.push_code_action_provider(
|
||||
Rc::new(AssistantCodeActionProvider {
|
||||
editor: cx.view().downgrade(),
|
||||
workspace: workspace.downgrade(),
|
||||
}),
|
||||
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,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1184,6 +1204,7 @@ impl InlineAssistant {
|
||||
editor.set_show_wrap_guides(false, cx);
|
||||
editor.set_show_gutter(false, cx);
|
||||
editor.scroll_manager.set_forbid_vertical_scroll(true);
|
||||
editor.set_show_scrollbars(false, cx);
|
||||
editor.set_read_only(true);
|
||||
editor.set_show_inline_completions(Some(false), cx);
|
||||
editor.highlight_rows::<DeletedLines>(
|
||||
@@ -3426,7 +3447,13 @@ 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 @@ 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;
|
||||
|
||||
@@ -53,7 +54,16 @@ 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();
|
||||
}
|
||||
@@ -76,6 +86,7 @@ pub struct InlineAssistant {
|
||||
prompt_builder: Arc<PromptBuilder>,
|
||||
telemetry: Arc<Telemetry>,
|
||||
fs: Arc<dyn Fs>,
|
||||
is_assistant2_enabled: bool,
|
||||
}
|
||||
|
||||
impl Global for InlineAssistant {}
|
||||
@@ -97,6 +108,7 @@ impl InlineAssistant {
|
||||
prompt_builder,
|
||||
telemetry,
|
||||
fs,
|
||||
is_assistant2_enabled: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,21 +169,31 @@ 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| {
|
||||
let thread_store = workspace
|
||||
.read(cx)
|
||||
.panel::<AssistantPanel>(cx)
|
||||
.map(|assistant_panel| assistant_panel.read(cx).thread_store().downgrade());
|
||||
if is_assistant2_enabled {
|
||||
let thread_store = workspace
|
||||
.read(cx)
|
||||
.panel::<AssistantPanel>(cx)
|
||||
.map(|assistant_panel| assistant_panel.read(cx).thread_store().downgrade());
|
||||
|
||||
editor.push_code_action_provider(
|
||||
Rc::new(AssistantCodeActionProvider {
|
||||
editor: cx.view().downgrade(),
|
||||
workspace: workspace.downgrade(),
|
||||
thread_store,
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1254,6 +1276,7 @@ impl InlineAssistant {
|
||||
editor.set_show_wrap_guides(false, cx);
|
||||
editor.set_show_gutter(false, cx);
|
||||
editor.scroll_manager.set_forbid_vertical_scroll(true);
|
||||
editor.set_show_scrollbars(false, cx);
|
||||
editor.set_read_only(true);
|
||||
editor.set_show_inline_completions(Some(false), cx);
|
||||
editor.highlight_rows::<DeletedLines>(
|
||||
@@ -1573,7 +1596,13 @@ 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>,
|
||||
|
||||
@@ -34,6 +34,7 @@ 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
|
||||
|
||||
@@ -440,8 +440,11 @@ async fn predict_edits(
|
||||
_country_code_header: Option<TypedHeader<CloudflareIpCountryHeader>>,
|
||||
Json(params): Json<PredictEditsParams>,
|
||||
) -> Result<impl IntoResponse> {
|
||||
if !claims.is_staff {
|
||||
return Err(anyhow!("not found"))?;
|
||||
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(),
|
||||
));
|
||||
}
|
||||
|
||||
let api_url = state
|
||||
@@ -459,29 +462,66 @@ async fn predict_edits(
|
||||
.prediction_model
|
||||
.as_ref()
|
||||
.context("no PREDICTION_MODEL configured on the server")?;
|
||||
|
||||
let outline_prefix = params
|
||||
.outline
|
||||
.as_ref()
|
||||
.map(|outline| format!("### Outline for current file:\n{}\n", outline))
|
||||
.unwrap_or_default();
|
||||
|
||||
let prompt = include_str!("./llm/prediction_prompt.md")
|
||||
.replace("<outline>", &outline_prefix)
|
||||
.replace("<events>", ¶ms.input_events)
|
||||
.replace("<excerpt>", ¶ms.input_excerpt);
|
||||
let mut response = open_ai::complete_text(
|
||||
|
||||
let request_start = std::time::Instant::now();
|
||||
let mut response = fireworks::complete(
|
||||
&state.http_client,
|
||||
api_url,
|
||||
api_key,
|
||||
open_ai::CompletionRequest {
|
||||
fireworks::CompletionRequest {
|
||||
model: model.to_string(),
|
||||
prompt: prompt.clone(),
|
||||
max_tokens: 1024,
|
||||
max_tokens: 2048,
|
||||
temperature: 0.,
|
||||
prediction: Some(open_ai::Prediction::Content {
|
||||
prediction: Some(fireworks::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,
|
||||
}))
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
<outline>## Task
|
||||
Below is an instruction that describes a task, paired with an input that provides further context. Write a response that appropriately completes the request.
|
||||
|
||||
### Instruction:
|
||||
|
||||
@@ -22,6 +22,8 @@ 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>,
|
||||
@@ -37,6 +39,7 @@ 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>,
|
||||
@@ -58,6 +61,7 @@ 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,6 +4025,7 @@ 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"))?
|
||||
@@ -4061,6 +4062,7 @@ 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(),
|
||||
|
||||
@@ -17,8 +17,8 @@ pub struct CopilotCompletionProvider {
|
||||
completions: Vec<Completion>,
|
||||
active_completion_index: usize,
|
||||
file_extension: Option<String>,
|
||||
pending_refresh: Task<Result<()>>,
|
||||
pending_cycling_refresh: Task<Result<()>>,
|
||||
pending_refresh: Option<Task<Result<()>>>,
|
||||
pending_cycling_refresh: Option<Task<Result<()>>>,
|
||||
copilot: Model<Copilot>,
|
||||
}
|
||||
|
||||
@@ -30,8 +30,8 @@ impl CopilotCompletionProvider {
|
||||
completions: Vec::new(),
|
||||
active_completion_index: 0,
|
||||
file_extension: None,
|
||||
pending_refresh: Task::ready(Ok(())),
|
||||
pending_cycling_refresh: Task::ready(Ok(())),
|
||||
pending_refresh: None,
|
||||
pending_cycling_refresh: None,
|
||||
copilot,
|
||||
}
|
||||
}
|
||||
@@ -67,6 +67,10 @@ impl InlineCompletionProvider for CopilotCompletionProvider {
|
||||
false
|
||||
}
|
||||
|
||||
fn is_refreshing(&self) -> bool {
|
||||
self.pending_refresh.is_some()
|
||||
}
|
||||
|
||||
fn is_enabled(
|
||||
&self,
|
||||
buffer: &Model<Buffer>,
|
||||
@@ -92,7 +96,7 @@ impl InlineCompletionProvider for CopilotCompletionProvider {
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
let copilot = self.copilot.clone();
|
||||
self.pending_refresh = cx.spawn(|this, mut cx| async move {
|
||||
self.pending_refresh = Some(cx.spawn(|this, mut cx| async move {
|
||||
if debounce {
|
||||
cx.background_executor()
|
||||
.timer(COPILOT_DEBOUNCE_TIMEOUT)
|
||||
@@ -108,7 +112,8 @@ impl InlineCompletionProvider for CopilotCompletionProvider {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
if !completions.is_empty() {
|
||||
this.cycled = false;
|
||||
this.pending_cycling_refresh = Task::ready(Ok(()));
|
||||
this.pending_refresh = None;
|
||||
this.pending_cycling_refresh = None;
|
||||
this.completions.clear();
|
||||
this.active_completion_index = 0;
|
||||
this.buffer_id = Some(buffer.entity_id());
|
||||
@@ -129,7 +134,7 @@ impl InlineCompletionProvider for CopilotCompletionProvider {
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
fn cycle(
|
||||
@@ -161,7 +166,7 @@ impl InlineCompletionProvider for CopilotCompletionProvider {
|
||||
cx.notify();
|
||||
} else {
|
||||
let copilot = self.copilot.clone();
|
||||
self.pending_cycling_refresh = cx.spawn(|this, mut cx| async move {
|
||||
self.pending_cycling_refresh = Some(cx.spawn(|this, mut cx| async move {
|
||||
let completions = copilot
|
||||
.update(&mut cx, |copilot, cx| {
|
||||
copilot.completions_cycling(&buffer, cursor_position, cx)
|
||||
@@ -185,7 +190,7 @@ impl InlineCompletionProvider for CopilotCompletionProvider {
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
});
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||
use gpui::{
|
||||
div, px, uniform_list, AnyElement, BackgroundExecutor, Div, FontWeight, ListSizingBehavior,
|
||||
Model, ScrollStrategy, SharedString, Size, StrikethroughStyle, StyledText,
|
||||
UniformListScrollHandle, ViewContext, WeakView,
|
||||
div, pulsating_between, px, uniform_list, Animation, AnimationExt, AnyElement,
|
||||
BackgroundExecutor, Div, FontWeight, ListSizingBehavior, Model, ScrollStrategy, SharedString,
|
||||
Size, StrikethroughStyle, StyledText, UniformListScrollHandle, ViewContext, WeakView,
|
||||
};
|
||||
use language::Buffer;
|
||||
use language::{CodeLabel, Documentation};
|
||||
@@ -10,6 +10,8 @@ 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},
|
||||
@@ -158,7 +160,7 @@ pub struct CompletionsMenu {
|
||||
pub buffer: Model<Buffer>,
|
||||
pub completions: Rc<RefCell<Box<[Completion]>>>,
|
||||
match_candidates: Rc<[StringMatchCandidate]>,
|
||||
pub entries: Rc<[CompletionEntry]>,
|
||||
pub entries: Rc<RefCell<Vec<CompletionEntry>>>,
|
||||
pub selected_item: usize,
|
||||
scroll_handle: UniformListScrollHandle,
|
||||
resolve_completions: bool,
|
||||
@@ -195,7 +197,7 @@ impl CompletionsMenu {
|
||||
show_completion_documentation,
|
||||
completions: RefCell::new(completions).into(),
|
||||
match_candidates,
|
||||
entries: Vec::new().into(),
|
||||
entries: RefCell::new(Vec::new()).into(),
|
||||
selected_item: 0,
|
||||
scroll_handle: UniformListScrollHandle::new(),
|
||||
resolve_completions: true,
|
||||
@@ -244,7 +246,7 @@ impl CompletionsMenu {
|
||||
string: completion.clone(),
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
.collect::<Vec<_>>();
|
||||
Self {
|
||||
id,
|
||||
sort_completions,
|
||||
@@ -252,7 +254,7 @@ impl CompletionsMenu {
|
||||
buffer,
|
||||
completions: RefCell::new(completions).into(),
|
||||
match_candidates,
|
||||
entries,
|
||||
entries: RefCell::new(entries).into(),
|
||||
selected_item: 0,
|
||||
scroll_handle: UniformListScrollHandle::new(),
|
||||
resolve_completions: false,
|
||||
@@ -290,7 +292,8 @@ impl CompletionsMenu {
|
||||
provider: Option<&dyn CompletionProvider>,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) {
|
||||
self.update_selection_index(self.entries.len() - 1, provider, cx);
|
||||
let index = self.entries.borrow().len() - 1;
|
||||
self.update_selection_index(index, provider, cx);
|
||||
}
|
||||
|
||||
fn update_selection_index(
|
||||
@@ -312,12 +315,12 @@ impl CompletionsMenu {
|
||||
if self.selected_item > 0 {
|
||||
self.selected_item - 1
|
||||
} else {
|
||||
self.entries.len() - 1
|
||||
self.entries.borrow().len() - 1
|
||||
}
|
||||
}
|
||||
|
||||
fn next_match_index(&self) -> usize {
|
||||
if self.selected_item + 1 < self.entries.len() {
|
||||
if self.selected_item + 1 < self.entries.borrow().len() {
|
||||
self.selected_item + 1
|
||||
} else {
|
||||
0
|
||||
@@ -326,24 +329,15 @@ impl CompletionsMenu {
|
||||
|
||||
pub fn show_inline_completion_hint(&mut self, hint: InlineCompletionMenuHint) {
|
||||
let hint = CompletionEntry::InlineCompletionHint(hint);
|
||||
|
||||
self.entries = match self.entries.first() {
|
||||
let mut entries = self.entries.borrow_mut();
|
||||
match entries.first() {
|
||||
Some(CompletionEntry::InlineCompletionHint { .. }) => {
|
||||
let mut entries = Vec::from(&*self.entries);
|
||||
entries[0] = hint;
|
||||
entries
|
||||
}
|
||||
_ => {
|
||||
if self.selected_item != 0 {
|
||||
self.selected_item += 1;
|
||||
}
|
||||
let mut entries = Vec::with_capacity(self.entries.len() + 1);
|
||||
entries.push(hint);
|
||||
entries.extend_from_slice(&self.entries);
|
||||
entries
|
||||
entries.insert(0, hint);
|
||||
}
|
||||
}
|
||||
.into();
|
||||
}
|
||||
|
||||
pub fn resolve_visible_completions(
|
||||
@@ -369,13 +363,14 @@ impl CompletionsMenu {
|
||||
let visible_count = last_rendered_range
|
||||
.clone()
|
||||
.map_or(APPROXIMATE_VISIBLE_COUNT, |range| range.count());
|
||||
let entries = self.entries.borrow();
|
||||
let entry_range = if self.selected_item == 0 {
|
||||
0..min(visible_count, self.entries.len())
|
||||
} else if self.selected_item == self.entries.len() - 1 {
|
||||
self.entries.len().saturating_sub(visible_count)..self.entries.len()
|
||||
0..min(visible_count, entries.len())
|
||||
} else if self.selected_item == entries.len() - 1 {
|
||||
entries.len().saturating_sub(visible_count)..entries.len()
|
||||
} else {
|
||||
last_rendered_range.map_or(0..0, |range| {
|
||||
min(range.start, self.entries.len())..min(range.end, self.entries.len())
|
||||
min(range.start, entries.len())..min(range.end, entries.len())
|
||||
})
|
||||
};
|
||||
|
||||
@@ -386,24 +381,25 @@ impl CompletionsMenu {
|
||||
entry_range.clone(),
|
||||
EXTRA_TO_RESOLVE,
|
||||
EXTRA_TO_RESOLVE,
|
||||
self.entries.len(),
|
||||
entries.len(),
|
||||
);
|
||||
|
||||
// Avoid work by sometimes filtering out completions that already have documentation.
|
||||
// This filtering doesn't happen if the completions are currently being updated.
|
||||
let completions = self.completions.borrow();
|
||||
let candidate_ids = entry_indices
|
||||
.flat_map(|i| Self::entry_candidate_id(&self.entries[i]))
|
||||
.flat_map(|i| Self::entry_candidate_id(&entries[i]))
|
||||
.filter(|i| completions[*i].documentation.is_none());
|
||||
|
||||
// Current selection is always resolved even if it already has documentation, to handle
|
||||
// out-of-spec language servers that return more results later.
|
||||
let candidate_ids = match Self::entry_candidate_id(&self.entries[self.selected_item]) {
|
||||
let candidate_ids = match Self::entry_candidate_id(&entries[self.selected_item]) {
|
||||
None => candidate_ids.collect::<Vec<usize>>(),
|
||||
Some(selected_candidate_id) => iter::once(selected_candidate_id)
|
||||
.chain(candidate_ids.filter(|id| *id != selected_candidate_id))
|
||||
.collect::<Vec<usize>>(),
|
||||
};
|
||||
drop(entries);
|
||||
|
||||
if candidate_ids.is_empty() {
|
||||
return;
|
||||
@@ -432,7 +428,7 @@ impl CompletionsMenu {
|
||||
}
|
||||
|
||||
pub fn visible(&self) -> bool {
|
||||
!self.entries.is_empty()
|
||||
!self.entries.borrow().is_empty()
|
||||
}
|
||||
|
||||
fn origin(&self, cursor_position: DisplayPoint) -> ContextMenuOrigin {
|
||||
@@ -449,6 +445,7 @@ impl CompletionsMenu {
|
||||
let show_completion_documentation = self.show_completion_documentation;
|
||||
let widest_completion_ix = self
|
||||
.entries
|
||||
.borrow()
|
||||
.iter()
|
||||
.enumerate()
|
||||
.max_by_key(|(_, mat)| match mat {
|
||||
@@ -465,33 +462,38 @@ impl CompletionsMenu {
|
||||
|
||||
len
|
||||
}
|
||||
CompletionEntry::InlineCompletionHint(InlineCompletionMenuHint {
|
||||
provider_name,
|
||||
..
|
||||
}) => provider_name.len(),
|
||||
CompletionEntry::InlineCompletionHint(hint) => {
|
||||
"Zed AI / ".chars().count() + hint.label().chars().count()
|
||||
}
|
||||
})
|
||||
.map(|(ix, _)| ix);
|
||||
drop(completions);
|
||||
|
||||
let selected_item = self.selected_item;
|
||||
let completions = self.completions.clone();
|
||||
let matches = self.entries.clone();
|
||||
let entries = self.entries.clone();
|
||||
let last_rendered_range = self.last_rendered_range.clone();
|
||||
let style = style.clone();
|
||||
let list = uniform_list(
|
||||
cx.view().clone(),
|
||||
"completions",
|
||||
matches.len(),
|
||||
self.entries.borrow().len(),
|
||||
move |_editor, range, cx| {
|
||||
last_rendered_range.borrow_mut().replace(range.clone());
|
||||
let start_ix = range.start;
|
||||
let completions_guard = completions.borrow_mut();
|
||||
|
||||
matches[range]
|
||||
entries.borrow()[range]
|
||||
.iter()
|
||||
.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;
|
||||
@@ -575,20 +577,57 @@ impl CompletionsMenu {
|
||||
.end_slot::<Label>(documentation_label),
|
||||
)
|
||||
}
|
||||
CompletionEntry::InlineCompletionHint(InlineCompletionMenuHint {
|
||||
provider_name,
|
||||
..
|
||||
}) => div().min_w(px(250.)).max_w(px(500.)).child(
|
||||
CompletionEntry::InlineCompletionHint(
|
||||
hint @ InlineCompletionMenuHint::None,
|
||||
) => 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(
|
||||
StyledText::new(format!(
|
||||
"{} Completion",
|
||||
SharedString::new_static(provider_name)
|
||||
))
|
||||
.with_highlights(&style.text, None),
|
||||
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),
|
||||
),
|
||||
)
|
||||
.on_click(cx.listener(move |editor, _event, cx| {
|
||||
cx.stop_propagation();
|
||||
@@ -623,7 +662,7 @@ impl CompletionsMenu {
|
||||
return None;
|
||||
}
|
||||
|
||||
let multiline_docs = match &self.entries[self.selected_item] {
|
||||
let multiline_docs = match &self.entries.borrow()[self.selected_item] {
|
||||
CompletionEntry::Match(mat) => {
|
||||
match self.completions.borrow_mut()[mat.candidate_id]
|
||||
.documentation
|
||||
@@ -645,19 +684,20 @@ impl CompletionsMenu {
|
||||
Documentation::Undocumented => 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()),
|
||||
},
|
||||
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,
|
||||
};
|
||||
|
||||
Some(
|
||||
@@ -769,12 +809,14 @@ impl CompletionsMenu {
|
||||
}
|
||||
drop(completions);
|
||||
|
||||
let mut new_entries: Vec<_> = matches.into_iter().map(CompletionEntry::Match).collect();
|
||||
if let Some(CompletionEntry::InlineCompletionHint(hint)) = self.entries.first() {
|
||||
new_entries.insert(0, CompletionEntry::InlineCompletionHint(hint.clone()));
|
||||
let mut entries = self.entries.borrow_mut();
|
||||
if let Some(CompletionEntry::InlineCompletionHint(_)) = entries.first() {
|
||||
entries.truncate(1);
|
||||
} else {
|
||||
entries.truncate(0);
|
||||
}
|
||||
entries.extend(matches.into_iter().map(CompletionEntry::Match));
|
||||
|
||||
self.entries = new_entries.into();
|
||||
self.selected_item = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -459,9 +459,21 @@ pub fn make_suggestion_styles(cx: &WindowContext) -> InlineCompletionStyles {
|
||||
type CompletionId = usize;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct InlineCompletionMenuHint {
|
||||
provider_name: &'static str,
|
||||
text: InlineCompletionText,
|
||||
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",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -1727,8 +1739,12 @@ impl Editor {
|
||||
self.input_enabled = input_enabled;
|
||||
}
|
||||
|
||||
pub fn set_inline_completions_enabled(&mut self, enabled: bool) {
|
||||
pub fn set_inline_completions_enabled(&mut self, enabled: bool, cx: &mut ViewContext<Self>) {
|
||||
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) {
|
||||
@@ -1787,6 +1803,17 @@ impl Editor {
|
||||
self.refresh_inline_completion(false, true, cx);
|
||||
}
|
||||
|
||||
pub fn inline_completions_enabled(&self, cx: &AppContext) -> bool {
|
||||
let cursor = self.selections.newest_anchor().head();
|
||||
if let Some((buffer, buffer_position)) =
|
||||
self.buffer.read(cx).text_anchor_for_position(cursor, cx)
|
||||
{
|
||||
self.should_show_inline_completions(&buffer, buffer_position, cx)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn should_show_inline_completions(
|
||||
&self,
|
||||
buffer: &Model<Buffer>,
|
||||
@@ -3808,6 +3835,26 @@ 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
|
||||
@@ -3815,12 +3862,10 @@ impl Editor {
|
||||
return None;
|
||||
};
|
||||
|
||||
let mat = completions_menu
|
||||
.entries
|
||||
.get(item_ix.unwrap_or(completions_menu.selected_item))?;
|
||||
|
||||
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(())));
|
||||
@@ -3832,12 +3877,14 @@ impl Editor {
|
||||
mat
|
||||
}
|
||||
};
|
||||
let candidate_id = mat.candidate_id;
|
||||
drop(entries);
|
||||
|
||||
let buffer_handle = completions_menu.buffer;
|
||||
let completion = completions_menu
|
||||
.completions
|
||||
.borrow()
|
||||
.get(mat.candidate_id)?
|
||||
.get(candidate_id)?
|
||||
.clone();
|
||||
cx.stop_propagation();
|
||||
|
||||
@@ -3986,7 +4033,7 @@ impl Editor {
|
||||
let apply_edits = provider.apply_additional_edits_for_completion(
|
||||
buffer_handle,
|
||||
completions_menu.completions.clone(),
|
||||
mat.candidate_id,
|
||||
candidate_id,
|
||||
true,
|
||||
cx,
|
||||
);
|
||||
@@ -4283,15 +4330,29 @@ impl Editor {
|
||||
self.available_code_actions.take();
|
||||
}
|
||||
|
||||
pub fn push_code_action_provider(
|
||||
pub fn add_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();
|
||||
@@ -4481,7 +4542,8 @@ impl Editor {
|
||||
if !user_requested
|
||||
&& (!self.enable_inline_completions
|
||||
|| !self.should_show_inline_completions(&buffer, cursor_buffer_position, cx)
|
||||
|| !self.is_focused(cx))
|
||||
|| !self.is_focused(cx)
|
||||
|| buffer.read(cx).is_empty())
|
||||
{
|
||||
self.discard_inline_completion(false, cx);
|
||||
return None;
|
||||
@@ -4571,6 +4633,23 @@ impl Editor {
|
||||
_: &AcceptInlineCompletion,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let buffer = self.buffer.read(cx);
|
||||
let snapshot = buffer.snapshot(cx);
|
||||
let selection = self.selections.newest_adjusted(cx);
|
||||
let cursor = selection.head();
|
||||
let current_indent = snapshot.indent_size_for_line(MultiBufferRow(cursor.row));
|
||||
let suggested_indents = snapshot.suggested_indents([cursor.row], cx);
|
||||
if let Some(suggested_indent) = suggested_indents.get(&MultiBufferRow(cursor.row)).copied()
|
||||
{
|
||||
if cursor.column < suggested_indent.len
|
||||
&& cursor.column <= current_indent.len
|
||||
&& current_indent.len <= suggested_indent.len
|
||||
{
|
||||
self.tab(&Default::default(), cx);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if self.show_inline_completions_in_menu(cx) {
|
||||
self.hide_context_menu(cx);
|
||||
}
|
||||
@@ -4636,8 +4715,19 @@ impl Editor {
|
||||
});
|
||||
}
|
||||
InlineCompletion::Edit(edits) => {
|
||||
if edits.len() == 1 && edits[0].0.start == edits[0].0.end {
|
||||
let text = edits[0].1.as_str();
|
||||
// Find an insertion that starts at the cursor position.
|
||||
let snapshot = self.buffer.read(cx).snapshot(cx);
|
||||
let cursor_offset = self.selections.newest::<usize>(cx).head();
|
||||
let insertion = edits.iter().find_map(|(range, text)| {
|
||||
let range = range.to_offset(&snapshot);
|
||||
if range.is_empty() && range.start == cursor_offset {
|
||||
Some(text)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
if let Some(text) = insertion {
|
||||
let mut partial_completion = text
|
||||
.chars()
|
||||
.by_ref()
|
||||
@@ -4660,6 +4750,8 @@ impl Editor {
|
||||
|
||||
self.refresh_inline_completion(true, true, cx);
|
||||
cx.notify();
|
||||
} else {
|
||||
self.accept_inline_completion(&Default::default(), cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4734,6 +4826,7 @@ 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()
|
||||
@@ -4856,8 +4949,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 {
|
||||
@@ -4874,12 +4967,11 @@ impl Editor {
|
||||
}
|
||||
};
|
||||
|
||||
Some(InlineCompletionMenuHint {
|
||||
provider_name,
|
||||
text,
|
||||
})
|
||||
Some(InlineCompletionMenuHint::Loaded { text })
|
||||
} else if provider.is_refreshing(cx) {
|
||||
Some(InlineCompletionMenuHint::Loading)
|
||||
} else {
|
||||
None
|
||||
Some(InlineCompletionMenuHint::None)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5110,9 +5202,11 @@ impl Editor {
|
||||
.borrow()
|
||||
.as_ref()
|
||||
.map_or(false, |menu| match menu {
|
||||
CodeContextMenu::Completions(menu) => menu.entries.first().map_or(false, |entry| {
|
||||
matches!(entry, CompletionEntry::InlineCompletionHint(_))
|
||||
}),
|
||||
CodeContextMenu::Completions(menu) => {
|
||||
menu.entries.borrow().first().map_or(false, |entry| {
|
||||
matches!(entry, CompletionEntry::InlineCompletionHint(_))
|
||||
})
|
||||
}
|
||||
CodeContextMenu::CodeActions(_) => false,
|
||||
})
|
||||
}
|
||||
@@ -13529,6 +13623,8 @@ pub trait CompletionProvider {
|
||||
}
|
||||
|
||||
pub trait CodeActionProvider {
|
||||
fn id(&self) -> Arc<str>;
|
||||
|
||||
fn code_actions(
|
||||
&self,
|
||||
buffer: &Model<Buffer>,
|
||||
@@ -13547,6 +13643,10 @@ pub trait CodeActionProvider {
|
||||
}
|
||||
|
||||
impl CodeActionProvider for Model<Project> {
|
||||
fn id(&self) -> Arc<str> {
|
||||
"project".into()
|
||||
}
|
||||
|
||||
fn code_actions(
|
||||
&self,
|
||||
buffer: &Model<Buffer>,
|
||||
|
||||
@@ -8473,7 +8473,7 @@ async fn test_completion_page_up_down_keys(cx: &mut gpui::TestAppContext) {
|
||||
cx.update_editor(|editor, _| {
|
||||
if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
|
||||
{
|
||||
assert_eq!(completion_menu_entries(&menu.entries), &["first", "last"]);
|
||||
assert_eq!(completion_menu_entries(&menu), &["first", "last"]);
|
||||
} else {
|
||||
panic!("expected completion menu to be open");
|
||||
}
|
||||
@@ -8566,7 +8566,7 @@ async fn test_completion_sort(cx: &mut gpui::TestAppContext) {
|
||||
if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
|
||||
{
|
||||
assert_eq!(
|
||||
completion_menu_entries(&menu.entries),
|
||||
completion_menu_entries(&menu),
|
||||
&["r", "ret", "Range", "return"]
|
||||
);
|
||||
} else {
|
||||
@@ -11080,6 +11080,7 @@ async fn test_completions_default_resolve_data_handling(cx: &mut gpui::TestAppCo
|
||||
assert_eq!(
|
||||
completions_menu
|
||||
.entries
|
||||
.borrow()
|
||||
.iter()
|
||||
.flat_map(|c| match c {
|
||||
CompletionEntry::Match(mat) => Some(mat.string.clone()),
|
||||
@@ -11190,7 +11191,7 @@ async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui:
|
||||
if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
|
||||
{
|
||||
assert_eq!(
|
||||
completion_menu_entries(&menu.entries),
|
||||
completion_menu_entries(&menu),
|
||||
&["bg-red", "bg-blue", "bg-yellow"]
|
||||
);
|
||||
} else {
|
||||
@@ -11203,10 +11204,7 @@ async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui:
|
||||
cx.update_editor(|editor, _| {
|
||||
if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
|
||||
{
|
||||
assert_eq!(
|
||||
completion_menu_entries(&menu.entries),
|
||||
&["bg-blue", "bg-yellow"]
|
||||
);
|
||||
assert_eq!(completion_menu_entries(&menu), &["bg-blue", "bg-yellow"]);
|
||||
} else {
|
||||
panic!("expected completion menu to be open");
|
||||
}
|
||||
@@ -11220,18 +11218,19 @@ async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui:
|
||||
cx.update_editor(|editor, _| {
|
||||
if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
|
||||
{
|
||||
assert_eq!(completion_menu_entries(&menu.entries), &["bg-yellow"]);
|
||||
assert_eq!(completion_menu_entries(&menu), &["bg-yellow"]);
|
||||
} else {
|
||||
panic!("expected completion menu to be open");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn completion_menu_entries(entries: &[CompletionEntry]) -> Vec<&str> {
|
||||
fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
|
||||
let entries = menu.entries.borrow();
|
||||
entries
|
||||
.iter()
|
||||
.flat_map(|e| match e {
|
||||
CompletionEntry::Match(mat) => Some(mat.string.as_str()),
|
||||
CompletionEntry::Match(mat) => Some(mat.string.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.collect()
|
||||
|
||||
@@ -543,8 +543,29 @@ 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(&OpenExcerpts, cx);
|
||||
editor.open_excerpts_common(
|
||||
Some(JumpData::MultiBufferRow {
|
||||
row: MultiBufferRow(multi_buffer_row),
|
||||
line_offset_from_top,
|
||||
}),
|
||||
false,
|
||||
cx,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -1312,11 +1333,15 @@ impl EditorElement {
|
||||
total_text_units
|
||||
.horizontal
|
||||
.zip(track_bounds.horizontal)
|
||||
.map(|(total_text_units_x, track_bounds_x)| {
|
||||
.and_then(|(total_text_units_x, track_bounds_x)| {
|
||||
if text_units_per_page.horizontal >= total_text_units_x {
|
||||
return None;
|
||||
}
|
||||
|
||||
let thumb_percent =
|
||||
(text_units_per_page.horizontal / total_text_units_x).min(1.);
|
||||
|
||||
track_bounds_x.size.width * thumb_percent
|
||||
Some(track_bounds_x.size.width * thumb_percent)
|
||||
}),
|
||||
total_text_units.vertical.zip(track_bounds.vertical).map(
|
||||
|(total_text_units_y, track_bounds_y)| {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
use gpui::{prelude::*, Model};
|
||||
use indoc::indoc;
|
||||
use inline_completion::InlineCompletionProvider;
|
||||
use language::{Language, LanguageConfig};
|
||||
use multi_buffer::{Anchor, MultiBufferSnapshot, ToPoint};
|
||||
use std::ops::Range;
|
||||
use std::{num::NonZeroU32, ops::Range, sync::Arc};
|
||||
use text::{Point, ToOffset};
|
||||
|
||||
use crate::{
|
||||
@@ -122,6 +123,54 @@ async fn test_inline_completion_jump_button(cx: &mut gpui::TestAppContext) {
|
||||
"});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_indentation(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |settings| {
|
||||
settings.defaults.tab_size = NonZeroU32::new(4)
|
||||
});
|
||||
|
||||
let language = Arc::new(
|
||||
Language::new(
|
||||
LanguageConfig::default(),
|
||||
Some(tree_sitter_rust::LANGUAGE.into()),
|
||||
)
|
||||
.with_indents_query(r#"(_ "(" ")" @end) @indent"#)
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
|
||||
let provider = cx.new_model(|_| FakeInlineCompletionProvider::default());
|
||||
assign_editor_completion_provider(provider.clone(), &mut cx);
|
||||
|
||||
cx.set_state(indoc! {"
|
||||
const a: A = (
|
||||
ˇ
|
||||
);
|
||||
"});
|
||||
|
||||
propose_edits(
|
||||
&provider,
|
||||
vec![(Point::new(1, 0)..Point::new(1, 0), " const function()")],
|
||||
&mut cx,
|
||||
);
|
||||
cx.update_editor(|editor, cx| editor.update_visible_inline_completion(cx));
|
||||
|
||||
assert_editor_active_edit_completion(&mut cx, |_, edits| {
|
||||
assert_eq!(edits.len(), 1);
|
||||
assert_eq!(edits[0].1.as_str(), " const function()");
|
||||
});
|
||||
|
||||
// When the cursor is before the suggested indentation level, accepting a
|
||||
// completion should just indent.
|
||||
accept_completion(&mut cx);
|
||||
cx.assert_editor_state(indoc! {"
|
||||
const a: A = (
|
||||
ˇ
|
||||
);
|
||||
"});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_inline_completion_invalidation_range(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
@@ -338,6 +387,10 @@ impl InlineCompletionProvider for FakeInlineCompletionProvider {
|
||||
true
|
||||
}
|
||||
|
||||
fn is_refreshing(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn refresh(
|
||||
&mut self,
|
||||
_buffer: gpui::Model<language::Buffer>,
|
||||
|
||||
@@ -59,9 +59,9 @@ impl FeatureFlag for ToolUseFeatureFlag {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ZetaFeatureFlag;
|
||||
impl FeatureFlag for ZetaFeatureFlag {
|
||||
const NAME: &'static str = "zeta";
|
||||
pub struct PredictEditsFeatureFlag;
|
||||
impl FeatureFlag for PredictEditsFeatureFlag {
|
||||
const NAME: &'static str = "predict-edits";
|
||||
}
|
||||
|
||||
pub struct GitUiFeatureFlag;
|
||||
|
||||
19
crates/fireworks/Cargo.toml
Normal file
19
crates/fireworks/Cargo.toml
Normal file
@@ -0,0 +1,19 @@
|
||||
[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
crates/fireworks/LICENSE-GPL
Symbolic link
1
crates/fireworks/LICENSE-GPL
Symbolic link
@@ -0,0 +1 @@
|
||||
../../LICENSE-GPL
|
||||
173
crates/fireworks/src/fireworks.rs
Normal file
173
crates/fireworks/src/fireworks.rs
Normal file
@@ -0,0 +1,173 @@
|
||||
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,12 +47,9 @@ 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,9 +9,6 @@ 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)]
|
||||
@@ -521,7 +518,24 @@ impl Fs for RealFs {
|
||||
|
||||
async fn atomic_write(&self, path: PathBuf, data: String) -> Result<()> {
|
||||
smol::unblock(move || {
|
||||
let mut tmp_file = create_temp_file(&path)?;
|
||||
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()
|
||||
}?;
|
||||
tmp_file.write_all(data.as_bytes())?;
|
||||
tmp_file.persist(path)?;
|
||||
Ok::<(), anyhow::Error>(())
|
||||
@@ -536,43 +550,13 @@ impl Fs for RealFs {
|
||||
if let Some(path) = path.parent() {
|
||||
self.create_dir(path).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()),
|
||||
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?;
|
||||
}
|
||||
writer.flush().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn canonicalize(&self, path: &Path) -> Result<PathBuf> {
|
||||
@@ -1979,84 +1963,6 @@ 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, TOOLTIP_DELAY,
|
||||
WrappedLine, WrappedLineLayout, TOOLTIP_DELAY,
|
||||
};
|
||||
use anyhow::anyhow;
|
||||
use parking_lot::{Mutex, MutexGuard};
|
||||
@@ -443,6 +443,36 @@ 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()
|
||||
|
||||
@@ -28,6 +28,7 @@ pub trait InlineCompletionProvider: 'static + Sized {
|
||||
cursor_position: language::Anchor,
|
||||
cx: &AppContext,
|
||||
) -> bool;
|
||||
fn is_refreshing(&self) -> bool;
|
||||
fn refresh(
|
||||
&mut self,
|
||||
buffer: Model<Buffer>,
|
||||
@@ -63,6 +64,7 @@ pub trait InlineCompletionProviderHandle {
|
||||
) -> bool;
|
||||
fn show_completions_in_menu(&self) -> bool;
|
||||
fn show_completions_in_normal_mode(&self) -> bool;
|
||||
fn is_refreshing(&self, cx: &AppContext) -> bool;
|
||||
fn refresh(
|
||||
&self,
|
||||
buffer: Model<Buffer>,
|
||||
@@ -116,6 +118,10 @@ where
|
||||
self.read(cx).is_enabled(buffer, cursor_position, cx)
|
||||
}
|
||||
|
||||
fn is_refreshing(&self, cx: &AppContext) -> bool {
|
||||
self.read(cx).is_refreshing()
|
||||
}
|
||||
|
||||
fn refresh(
|
||||
&self,
|
||||
buffer: Model<Buffer>,
|
||||
|
||||
@@ -19,6 +19,7 @@ editor.workspace = true
|
||||
feature_flags.workspace = true
|
||||
fs.workspace = true
|
||||
gpui.workspace = true
|
||||
inline_completion.workspace = true
|
||||
language.workspace = true
|
||||
paths.workspace = true
|
||||
settings.workspace = true
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
use anyhow::Result;
|
||||
use copilot::{Copilot, Status};
|
||||
use editor::{scroll::Autoscroll, Editor};
|
||||
use feature_flags::{FeatureFlagAppExt, ZetaFeatureFlag};
|
||||
use feature_flags::{FeatureFlagAppExt, PredictEditsFeatureFlag};
|
||||
use fs::Fs;
|
||||
use gpui::{
|
||||
actions, div, Action, AppContext, AsyncWindowContext, Corner, Entity, IntoElement,
|
||||
ParentElement, Render, Subscription, View, ViewContext, WeakView, WindowContext,
|
||||
actions, div, pulsating_between, Action, Animation, AnimationExt, AppContext,
|
||||
AsyncWindowContext, Corner, Entity, IntoElement, ParentElement, Render, Subscription, View,
|
||||
ViewContext, WeakView, WindowContext,
|
||||
};
|
||||
use language::{
|
||||
language_settings::{
|
||||
@@ -14,7 +15,7 @@ use language::{
|
||||
File, Language,
|
||||
};
|
||||
use settings::{update_settings_file, Settings, SettingsStore};
|
||||
use std::{path::Path, sync::Arc};
|
||||
use std::{path::Path, sync::Arc, time::Duration};
|
||||
use supermaven::{AccountStatus, Supermaven};
|
||||
use workspace::{
|
||||
create_and_open_local_file,
|
||||
@@ -39,6 +40,7 @@ pub struct InlineCompletionButton {
|
||||
editor_enabled: Option<bool>,
|
||||
language: Option<Arc<Language>>,
|
||||
file: Option<Arc<dyn File>>,
|
||||
inline_completion_provider: Option<Arc<dyn inline_completion::InlineCompletionProviderHandle>>,
|
||||
fs: Arc<dyn Fs>,
|
||||
workspace: WeakView<Workspace>,
|
||||
}
|
||||
@@ -199,29 +201,40 @@ impl Render for InlineCompletionButton {
|
||||
);
|
||||
}
|
||||
|
||||
InlineCompletionProvider::Zeta => {
|
||||
if !cx.has_flag::<ZetaFeatureFlag>() {
|
||||
InlineCompletionProvider::Zed => {
|
||||
if !cx.has_flag::<PredictEditsFeatureFlag>() {
|
||||
return div();
|
||||
}
|
||||
|
||||
div().child(
|
||||
IconButton::new("zeta", IconName::ZedPredict)
|
||||
.tooltip(|cx| {
|
||||
Tooltip::with_meta(
|
||||
"Zed Predict",
|
||||
Some(&RateCompletions),
|
||||
"Click to rate completions",
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.on_click(cx.listener(|this, _, cx| {
|
||||
if let Some(workspace) = this.workspace.upgrade() {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
RateCompletionModal::toggle(workspace, cx)
|
||||
});
|
||||
}
|
||||
})),
|
||||
)
|
||||
let this = cx.view().clone();
|
||||
let button = IconButton::new("zeta", IconName::ZedPredict)
|
||||
.tooltip(|cx| Tooltip::text("Edit Prediction", cx));
|
||||
|
||||
let is_refreshing = self
|
||||
.inline_completion_provider
|
||||
.as_ref()
|
||||
.map_or(false, |provider| provider.is_refreshing(cx));
|
||||
|
||||
let mut popover_menu = PopoverMenu::new("zeta")
|
||||
.menu(move |cx| {
|
||||
Some(this.update(cx, |this, cx| this.build_zeta_context_menu(cx)))
|
||||
})
|
||||
.anchor(Corner::BottomRight);
|
||||
if is_refreshing {
|
||||
popover_menu = popover_menu.trigger(
|
||||
button.with_animation(
|
||||
"pulsating-label",
|
||||
Animation::new(Duration::from_secs(2))
|
||||
.repeat()
|
||||
.with_easing(pulsating_between(0.2, 1.0)),
|
||||
|icon_button, delta| icon_button.alpha(delta),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
popover_menu = popover_menu.trigger(button);
|
||||
}
|
||||
|
||||
div().child(popover_menu.into_any_element())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -245,6 +258,7 @@ impl InlineCompletionButton {
|
||||
editor_enabled: None,
|
||||
language: None,
|
||||
file: None,
|
||||
inline_completion_provider: None,
|
||||
workspace,
|
||||
fs,
|
||||
}
|
||||
@@ -360,6 +374,25 @@ impl InlineCompletionButton {
|
||||
})
|
||||
}
|
||||
|
||||
fn build_zeta_context_menu(&self, cx: &mut ViewContext<Self>) -> View<ContextMenu> {
|
||||
let workspace = self.workspace.clone();
|
||||
ContextMenu::build(cx, |menu, cx| {
|
||||
self.build_language_settings_menu(menu, cx)
|
||||
.separator()
|
||||
.entry(
|
||||
"Rate Completions",
|
||||
Some(RateCompletions.boxed_clone()),
|
||||
move |cx| {
|
||||
workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
RateCompletionModal::toggle(workspace, cx)
|
||||
})
|
||||
.ok();
|
||||
},
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn update_enabled(&mut self, editor: View<Editor>, cx: &mut ViewContext<Self>) {
|
||||
let editor = editor.read(cx);
|
||||
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
||||
@@ -377,6 +410,7 @@ impl InlineCompletionButton {
|
||||
),
|
||||
)
|
||||
};
|
||||
self.inline_completion_provider = editor.inline_completion_provider();
|
||||
self.language = language.cloned();
|
||||
self.file = file;
|
||||
|
||||
|
||||
@@ -203,7 +203,7 @@ pub enum InlineCompletionProvider {
|
||||
#[default]
|
||||
Copilot,
|
||||
Supermaven,
|
||||
Zeta,
|
||||
Zed,
|
||||
}
|
||||
|
||||
/// The settings for inline completions, such as [GitHub Copilot](https://github.com/features/copilot)
|
||||
|
||||
@@ -212,9 +212,18 @@ impl LspAdapter for TypeScriptLspAdapter {
|
||||
_ => None,
|
||||
}?;
|
||||
|
||||
let text = match &item.detail {
|
||||
Some(detail) => format!("{} {}", item.label, detail),
|
||||
None => item.label.clone(),
|
||||
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()
|
||||
};
|
||||
|
||||
Some(language::CodeLabel {
|
||||
|
||||
@@ -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" | "command-r" | "deepseek-coder-v2" | "yi-coder"
|
||||
| "llama3.2" => 128000,
|
||||
"llama3.1" | "phi3" | "phi3.5" | "phi4" | "command-r" | "deepseek-coder-v2"
|
||||
| "yi-coder" | "llama3.2" => 128000,
|
||||
_ => DEFAULT_TOKENS,
|
||||
}
|
||||
.clamp(1, MAXIMUM_TOKENS)
|
||||
|
||||
@@ -36,6 +36,7 @@ pub struct PerformCompletionParams {
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct PredictEditsParams {
|
||||
pub outline: Option<String>,
|
||||
pub input_events: String,
|
||||
pub input_excerpt: String,
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ pub struct SupermavenCompletionProvider {
|
||||
buffer_id: Option<EntityId>,
|
||||
completion_id: Option<SupermavenCompletionStateId>,
|
||||
file_extension: Option<String>,
|
||||
pending_refresh: Task<Result<()>>,
|
||||
pending_refresh: Option<Task<Result<()>>>,
|
||||
}
|
||||
|
||||
impl SupermavenCompletionProvider {
|
||||
@@ -29,7 +29,7 @@ impl SupermavenCompletionProvider {
|
||||
buffer_id: None,
|
||||
completion_id: None,
|
||||
file_extension: None,
|
||||
pending_refresh: Task::ready(Ok(())),
|
||||
pending_refresh: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -122,6 +122,10 @@ impl InlineCompletionProvider for SupermavenCompletionProvider {
|
||||
settings.inline_completions_enabled(language.as_ref(), file.map(|f| f.path().as_ref()), cx)
|
||||
}
|
||||
|
||||
fn is_refreshing(&self) -> bool {
|
||||
self.pending_refresh.is_some()
|
||||
}
|
||||
|
||||
fn refresh(
|
||||
&mut self,
|
||||
buffer_handle: Model<Buffer>,
|
||||
@@ -135,7 +139,7 @@ impl InlineCompletionProvider for SupermavenCompletionProvider {
|
||||
return;
|
||||
};
|
||||
|
||||
self.pending_refresh = cx.spawn(|this, mut cx| async move {
|
||||
self.pending_refresh = Some(cx.spawn(|this, mut cx| async move {
|
||||
if debounce {
|
||||
cx.background_executor().timer(DEBOUNCE_TIMEOUT).await;
|
||||
}
|
||||
@@ -152,11 +156,12 @@ impl InlineCompletionProvider for SupermavenCompletionProvider {
|
||||
.to_string(),
|
||||
)
|
||||
});
|
||||
this.pending_refresh = None;
|
||||
cx.notify();
|
||||
})?;
|
||||
}
|
||||
Ok(())
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
fn cycle(
|
||||
@@ -169,12 +174,12 @@ impl InlineCompletionProvider for SupermavenCompletionProvider {
|
||||
}
|
||||
|
||||
fn accept(&mut self, _cx: &mut ModelContext<Self>) {
|
||||
self.pending_refresh = Task::ready(Ok(()));
|
||||
self.pending_refresh = None;
|
||||
self.completion_id = None;
|
||||
}
|
||||
|
||||
fn discard(&mut self, _cx: &mut ModelContext<Self>) {
|
||||
self.pending_refresh = Task::ready(Ok(()));
|
||||
self.pending_refresh = None;
|
||||
self.completion_id = None;
|
||||
}
|
||||
|
||||
|
||||
@@ -146,13 +146,16 @@ 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 {
|
||||
let activate_item = Some(item.item_id().as_u64()) == active_item;
|
||||
if Some(item.item_id().as_u64()) == active_item {
|
||||
active_item_index = Some(item_index);
|
||||
}
|
||||
pane.add_item(Box::new(item), false, false, None, cx);
|
||||
item_index += 1;
|
||||
if activate_item {
|
||||
pane.activate_item(item_index, false, false, cx);
|
||||
}
|
||||
}
|
||||
if let Some(index) = active_item_index {
|
||||
pane.activate_item(index, false, false, cx);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ use ui::{
|
||||
};
|
||||
use util::{ResultExt, TryFutureExt};
|
||||
use workspace::{
|
||||
dock::{DockPosition, Panel, PanelEvent},
|
||||
dock::{DockPosition, Panel, PanelEvent, PanelHandle},
|
||||
item::SerializableItem,
|
||||
move_active_item, move_item, pane,
|
||||
ui::IconName,
|
||||
@@ -75,6 +75,7 @@ pub struct TerminalPanel {
|
||||
deferred_tasks: HashMap<TaskId, Task<()>>,
|
||||
assistant_enabled: bool,
|
||||
assistant_tab_bar_button: Option<AnyView>,
|
||||
active: bool,
|
||||
}
|
||||
|
||||
impl TerminalPanel {
|
||||
@@ -82,7 +83,6 @@ 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,6 +95,7 @@ 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
|
||||
@@ -281,6 +282,25 @@ 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)
|
||||
}
|
||||
|
||||
@@ -1339,7 +1359,9 @@ impl Panel for TerminalPanel {
|
||||
}
|
||||
|
||||
fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {
|
||||
if !active || !self.has_no_terminals(cx) {
|
||||
let old_active = self.active;
|
||||
self.active = active;
|
||||
if !active || old_active == active || !self.has_no_terminals(cx) {
|
||||
return;
|
||||
}
|
||||
cx.defer(|this, cx| {
|
||||
|
||||
@@ -22,6 +22,7 @@ pub struct IconButton {
|
||||
icon_size: IconSize,
|
||||
icon_color: Color,
|
||||
selected_icon: Option<IconName>,
|
||||
alpha: Option<f32>,
|
||||
}
|
||||
|
||||
impl IconButton {
|
||||
@@ -33,6 +34,7 @@ impl IconButton {
|
||||
icon_size: IconSize::default(),
|
||||
icon_color: Color::Default,
|
||||
selected_icon: None,
|
||||
alpha: None,
|
||||
};
|
||||
this.base.base = this.base.base.debug_selector(|| format!("ICON-{:?}", icon));
|
||||
this
|
||||
@@ -53,6 +55,11 @@ impl IconButton {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn alpha(mut self, alpha: f32) -> Self {
|
||||
self.alpha = Some(alpha);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn selected_icon(mut self, icon: impl Into<Option<IconName>>) -> Self {
|
||||
self.selected_icon = icon.into();
|
||||
self
|
||||
@@ -146,6 +153,7 @@ impl RenderOnce for IconButton {
|
||||
let is_selected = self.base.selected;
|
||||
let selected_style = self.base.selected_style;
|
||||
|
||||
let color = self.icon_color.color(cx).opacity(self.alpha.unwrap_or(1.0));
|
||||
self.base
|
||||
.map(|this| match self.shape {
|
||||
IconButtonShape::Square => {
|
||||
@@ -161,7 +169,7 @@ impl RenderOnce for IconButton {
|
||||
.selected_icon(self.selected_icon)
|
||||
.when_some(selected_style, |this, style| this.selected_style(style))
|
||||
.size(self.icon_size)
|
||||
.color(self.icon_color),
|
||||
.color(Color::Custom(color)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,28 @@ pub trait PopoverTrigger: IntoElement + Clickable + Toggleable + 'static {}
|
||||
|
||||
impl<T: IntoElement + Clickable + Toggleable + 'static> PopoverTrigger for T {}
|
||||
|
||||
impl<T: Clickable> Clickable for gpui::AnimationElement<T>
|
||||
where
|
||||
T: Clickable + 'static,
|
||||
{
|
||||
fn on_click(self, handler: impl Fn(&gpui::ClickEvent, &mut WindowContext) + 'static) -> Self {
|
||||
self.map_element(|e| e.on_click(handler))
|
||||
}
|
||||
|
||||
fn cursor_style(self, cursor_style: gpui::CursorStyle) -> Self {
|
||||
self.map_element(|e| e.cursor_style(cursor_style))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Toggleable> Toggleable for gpui::AnimationElement<T>
|
||||
where
|
||||
T: Toggleable + 'static,
|
||||
{
|
||||
fn toggle_state(self, selected: bool) -> Self {
|
||||
self.map_element(|e| e.toggle_state(selected))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PopoverMenuHandle<M>(Rc<RefCell<Option<PopoverMenuHandleState<M>>>>);
|
||||
|
||||
impl<M> Clone for PopoverMenuHandle<M> {
|
||||
|
||||
@@ -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);
|
||||
editor.set_inline_completions_enabled(enable_inline_completions, cx);
|
||||
});
|
||||
cx.notify()
|
||||
}
|
||||
|
||||
@@ -2295,6 +2295,19 @@ 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,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
description = "The fast, collaborative code editor."
|
||||
edition = "2021"
|
||||
name = "zed"
|
||||
version = "0.169.0"
|
||||
version = "0.169.3"
|
||||
publish = false
|
||||
license = "GPL-3.0-or-later"
|
||||
authors = ["Zed Team <hi@zed.dev>"]
|
||||
|
||||
@@ -1 +1 @@
|
||||
dev
|
||||
stable
|
||||
@@ -4,7 +4,7 @@ use client::Client;
|
||||
use collections::HashMap;
|
||||
use copilot::{Copilot, CopilotCompletionProvider};
|
||||
use editor::{Editor, EditorMode};
|
||||
use feature_flags::{FeatureFlagAppExt, ZetaFeatureFlag};
|
||||
use feature_flags::{FeatureFlagAppExt, PredictEditsFeatureFlag};
|
||||
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::<ZetaFeatureFlag>() {
|
||||
if cx.has_flag::<PredictEditsFeatureFlag>() {
|
||||
cx.on_action(clear_zeta_edit_history);
|
||||
}
|
||||
|
||||
cx.observe_flag::<ZetaFeatureFlag, _>({
|
||||
cx.observe_flag::<PredictEditsFeatureFlag, _>({
|
||||
let editors = editors.clone();
|
||||
let client = client.clone();
|
||||
move |active, cx| {
|
||||
@@ -164,8 +164,11 @@ fn assign_inline_completion_provider(
|
||||
editor.set_inline_completion_provider(Some(provider), cx);
|
||||
}
|
||||
}
|
||||
language::language_settings::InlineCompletionProvider::Zeta => {
|
||||
if cx.has_flag::<ZetaFeatureFlag>() || cfg!(debug_assertions) {
|
||||
|
||||
language::language_settings::InlineCompletionProvider::Zed => {
|
||||
if cx.has_flag::<PredictEditsFeatureFlag>()
|
||||
|| (cfg!(debug_assertions) && client.status().borrow().is_connected())
|
||||
{
|
||||
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() {
|
||||
|
||||
@@ -94,6 +94,7 @@ impl Render for QuickActionBar {
|
||||
git_blame_inline_enabled,
|
||||
show_git_blame_gutter,
|
||||
auto_signature_help_enabled,
|
||||
inline_completions_enabled,
|
||||
) = {
|
||||
let editor = editor.read(cx);
|
||||
let selection_menu_enabled = editor.selection_menu_enabled(cx);
|
||||
@@ -102,6 +103,7 @@ impl Render for QuickActionBar {
|
||||
let git_blame_inline_enabled = editor.git_blame_inline_enabled();
|
||||
let show_git_blame_gutter = editor.show_git_blame_gutter();
|
||||
let auto_signature_help_enabled = editor.auto_signature_help_enabled(cx);
|
||||
let inline_completions_enabled = editor.inline_completions_enabled(cx);
|
||||
|
||||
(
|
||||
selection_menu_enabled,
|
||||
@@ -110,6 +112,7 @@ impl Render for QuickActionBar {
|
||||
git_blame_inline_enabled,
|
||||
show_git_blame_gutter,
|
||||
auto_signature_help_enabled,
|
||||
inline_completions_enabled,
|
||||
)
|
||||
};
|
||||
|
||||
@@ -283,6 +286,26 @@ impl Render for QuickActionBar {
|
||||
},
|
||||
);
|
||||
|
||||
menu = menu.toggleable_entry(
|
||||
"Inline Completions",
|
||||
inline_completions_enabled,
|
||||
IconPosition::Start,
|
||||
Some(editor::actions::ToggleInlineCompletions.boxed_clone()),
|
||||
{
|
||||
let editor = editor.clone();
|
||||
move |cx| {
|
||||
editor
|
||||
.update(cx, |editor, cx| {
|
||||
editor.toggle_inline_completions(
|
||||
&editor::actions::ToggleInlineCompletions,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
menu = menu.separator();
|
||||
|
||||
menu = menu.toggleable_entry(
|
||||
|
||||
@@ -39,6 +39,7 @@ telemetry.workspace = true
|
||||
telemetry_events.workspace = true
|
||||
theme.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
uuid.workspace = true
|
||||
workspace.workspace = true
|
||||
|
||||
|
||||
161
crates/zeta/src/completion_diff_element.rs
Normal file
161
crates/zeta/src/completion_diff_element.rs
Normal file
@@ -0,0 +1,161 @@
|
||||
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,13 +1,11 @@
|
||||
use crate::{InlineCompletion, InlineCompletionRating, Zeta};
|
||||
use crate::{CompletionDiffElement, InlineCompletion, InlineCompletionRating, Zeta};
|
||||
use editor::Editor;
|
||||
use gpui::{
|
||||
actions, prelude::*, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView,
|
||||
HighlightStyle, Model, StyledText, TextStyle, View, ViewContext,
|
||||
actions, prelude::*, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Model,
|
||||
View, ViewContext,
|
||||
};
|
||||
use language::{language_settings, OffsetRangeExt};
|
||||
use settings::Settings;
|
||||
use language::language_settings;
|
||||
use std::time::Duration;
|
||||
use theme::ThemeSettings;
|
||||
use ui::{prelude::*, KeyBinding, List, ListItem, ListItemSpacing, Tooltip};
|
||||
use workspace::{ModalView, Workspace};
|
||||
|
||||
@@ -15,8 +13,6 @@ actions!(
|
||||
zeta,
|
||||
[
|
||||
RateCompletions,
|
||||
ThumbsUp,
|
||||
ThumbsDown,
|
||||
ThumbsUpActiveCompletion,
|
||||
ThumbsDownActiveCompletion,
|
||||
NextEdit,
|
||||
@@ -41,6 +37,7 @@ pub struct RateCompletionModal {
|
||||
selected_index: usize,
|
||||
focus_handle: FocusHandle,
|
||||
_subscription: gpui::Subscription,
|
||||
current_view: RateCompletionView,
|
||||
}
|
||||
|
||||
struct ActiveCompletion {
|
||||
@@ -48,6 +45,21 @@ 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) {
|
||||
@@ -57,12 +69,14 @@ 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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,7 +88,7 @@ impl RateCompletionModal {
|
||||
self.selected_index += 1;
|
||||
self.selected_index = usize::min(
|
||||
self.selected_index,
|
||||
self.zeta.read(cx).recent_completions().count(),
|
||||
self.zeta.read(cx).shown_completions().count(),
|
||||
);
|
||||
cx.notify();
|
||||
}
|
||||
@@ -88,7 +102,7 @@ impl RateCompletionModal {
|
||||
let next_index = self
|
||||
.zeta
|
||||
.read(cx)
|
||||
.recent_completions()
|
||||
.shown_completions()
|
||||
.skip(self.selected_index)
|
||||
.enumerate()
|
||||
.skip(1) // Skip straight to the next item
|
||||
@@ -103,12 +117,12 @@ impl RateCompletionModal {
|
||||
|
||||
fn select_prev_edit(&mut self, _: &PreviousEdit, cx: &mut ViewContext<Self>) {
|
||||
let zeta = self.zeta.read(cx);
|
||||
let completions_len = zeta.recent_completions_len();
|
||||
let completions_len = zeta.shown_completions_len();
|
||||
|
||||
let prev_index = self
|
||||
.zeta
|
||||
.read(cx)
|
||||
.recent_completions()
|
||||
.shown_completions()
|
||||
.rev()
|
||||
.skip((completions_len - 1) - self.selected_index)
|
||||
.enumerate()
|
||||
@@ -129,28 +143,7 @@ impl RateCompletionModal {
|
||||
}
|
||||
|
||||
fn select_last(&mut self, _: &menu::SelectLast, cx: &mut ViewContext<Self>) {
|
||||
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);
|
||||
self.selected_index = self.zeta.read(cx).shown_completions_len() - 1;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
@@ -177,7 +170,11 @@ impl RateCompletionModal {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn thumbs_down_active(&mut self, _: &ThumbsDownActiveCompletion, cx: &mut ViewContext<Self>) {
|
||||
pub 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;
|
||||
@@ -213,7 +210,7 @@ impl RateCompletionModal {
|
||||
let completion = self
|
||||
.zeta
|
||||
.read(cx)
|
||||
.recent_completions()
|
||||
.shown_completions()
|
||||
.skip(self.selected_index)
|
||||
.take(1)
|
||||
.next()
|
||||
@@ -226,7 +223,7 @@ impl RateCompletionModal {
|
||||
let completion = self
|
||||
.zeta
|
||||
.read(cx)
|
||||
.recent_completions()
|
||||
.shown_completions()
|
||||
.skip(self.selected_index)
|
||||
.take(1)
|
||||
.next()
|
||||
@@ -246,7 +243,7 @@ impl RateCompletionModal {
|
||||
self.selected_index = self
|
||||
.zeta
|
||||
.read(cx)
|
||||
.recent_completions()
|
||||
.shown_completions()
|
||||
.enumerate()
|
||||
.find(|(_, completion_b)| completion.id == completion_b.id)
|
||||
.map(|(ix, _)| ix)
|
||||
@@ -286,99 +283,127 @@ 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 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 border_color = cx.theme().colors().border;
|
||||
let bg_color = cx.theme().colors().editor_background;
|
||||
|
||||
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 border_color = cx.theme().colors().border;
|
||||
let bg_color = cx.theme().colors().editor_background;
|
||||
|
||||
let label_container = || h_flex().pl_1().gap_1p5();
|
||||
let label_container = h_flex().pl_1().gap_1p5();
|
||||
|
||||
Some(
|
||||
v_flex()
|
||||
.size_full()
|
||||
.overflow_hidden()
|
||||
.relative()
|
||||
.child(
|
||||
div()
|
||||
.id("diff")
|
||||
.py_4()
|
||||
.px_6()
|
||||
v_flex()
|
||||
.size_full()
|
||||
.bg(bg_color)
|
||||
.overflow_scroll()
|
||||
.child(StyledText::new(diff).with_highlights(&text_style, diff_highlights)),
|
||||
.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))
|
||||
)
|
||||
.when_some((!rated).then(|| ()), |this, _| {
|
||||
.when(!rated, |this| {
|
||||
this.child(
|
||||
h_flex()
|
||||
.p_2()
|
||||
.gap_2()
|
||||
.border_y_1()
|
||||
.border_color(border_color)
|
||||
|
||||
.child(
|
||||
Icon::new(IconName::Info)
|
||||
.size(IconSize::XSmall)
|
||||
@@ -390,14 +415,14 @@ impl RateCompletionModal {
|
||||
.pr_2()
|
||||
.flex_wrap()
|
||||
.child(
|
||||
Label::new("Ensure you explain why this completion is negative or positive. In case it's negative, report what you expected instead.")
|
||||
Label::new("Explain why this completion is good or bad. If it's negative, describe what you expected instead.")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
)
|
||||
)
|
||||
)
|
||||
})
|
||||
.when_some((!rated).then(|| ()), |this, _| {
|
||||
.when(!rated, |this| {
|
||||
this.child(
|
||||
div()
|
||||
.h_40()
|
||||
@@ -417,7 +442,7 @@ impl RateCompletionModal {
|
||||
.justify_between()
|
||||
.children(if rated {
|
||||
Some(
|
||||
label_container()
|
||||
label_container
|
||||
.child(
|
||||
Icon::new(IconName::Check)
|
||||
.size(IconSize::Small)
|
||||
@@ -427,7 +452,7 @@ impl RateCompletionModal {
|
||||
)
|
||||
} else if active_completion.completion.edits.is_empty() {
|
||||
Some(
|
||||
label_container()
|
||||
label_container
|
||||
.child(
|
||||
Icon::new(IconName::Warning)
|
||||
.size(IconSize::Small)
|
||||
@@ -435,30 +460,14 @@ 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)
|
||||
@@ -468,6 +477,11 @@ 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,
|
||||
@@ -477,16 +491,15 @@ 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);
|
||||
})),
|
||||
@@ -512,7 +525,6 @@ 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))
|
||||
@@ -526,16 +538,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)
|
||||
@@ -561,12 +573,12 @@ impl Render for RateCompletionModal {
|
||||
div()
|
||||
.p_2()
|
||||
.child(
|
||||
Label::new("No completions yet. Use the editor to generate some and rate them!")
|
||||
Label::new("No completions yet. Use the editor to generate some, and make sure to rate them!")
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.into_any_element(),
|
||||
)
|
||||
.children(self.zeta.read(cx).recent_completions().cloned().enumerate().map(
|
||||
.children(self.zeta.read(cx).shown_completions().cloned().enumerate().map(
|
||||
|(index, completion)| {
|
||||
let selected =
|
||||
self.active_completion.as_ref().map_or(false, |selected| {
|
||||
@@ -575,27 +587,45 @@ 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(
|
||||
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)
|
||||
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)
|
||||
)
|
||||
)
|
||||
)
|
||||
.tooltip(move |cx| {
|
||||
Tooltip::text(tooltip_text, cx)
|
||||
})
|
||||
.on_click(cx.listener(move |this, _, cx| {
|
||||
this.select_completion(Some(completion.clone()), true, cx);
|
||||
}))
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
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};
|
||||
@@ -30,6 +32,7 @@ use std::{
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use telemetry_events::InlineCompletionRating;
|
||||
use util::ResultExt;
|
||||
use uuid::Uuid;
|
||||
|
||||
const CURSOR_MARKER: &'static str = "<|user_cursor_is_here|>";
|
||||
@@ -71,6 +74,7 @@ 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>,
|
||||
@@ -154,9 +158,8 @@ pub struct Zeta {
|
||||
client: Arc<Client>,
|
||||
events: VecDeque<Event>,
|
||||
registered_buffers: HashMap<gpui::EntityId, RegisteredBuffer>,
|
||||
recent_completions: VecDeque<InlineCompletion>,
|
||||
shown_completions: VecDeque<InlineCompletion>,
|
||||
rated_completions: HashSet<InlineCompletionId>,
|
||||
shown_completions: HashSet<InlineCompletionId>,
|
||||
llm_token: LlmApiToken,
|
||||
_llm_token_subscription: Subscription,
|
||||
}
|
||||
@@ -184,9 +187,8 @@ impl Zeta {
|
||||
Self {
|
||||
client,
|
||||
events: VecDeque::new(),
|
||||
recent_completions: VecDeque::new(),
|
||||
shown_completions: VecDeque::new(),
|
||||
rated_completions: HashSet::default(),
|
||||
shown_completions: HashSet::default(),
|
||||
registered_buffers: HashMap::default(),
|
||||
llm_token: LlmApiToken::default(),
|
||||
_llm_token_subscription: cx.subscribe(
|
||||
@@ -205,7 +207,7 @@ impl Zeta {
|
||||
}
|
||||
|
||||
fn push_event(&mut self, event: Event) {
|
||||
const MAX_EVENT_COUNT: usize = 20;
|
||||
const MAX_EVENT_COUNT: usize = 16;
|
||||
|
||||
if let Some(Event::BufferChange {
|
||||
new_snapshot: last_new_snapshot,
|
||||
@@ -231,8 +233,8 @@ impl Zeta {
|
||||
}
|
||||
|
||||
self.events.push_back(event);
|
||||
if self.events.len() > MAX_EVENT_COUNT {
|
||||
self.events.pop_front();
|
||||
if self.events.len() >= MAX_EVENT_COUNT {
|
||||
self.events.drain(..MAX_EVENT_COUNT / 2);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -291,38 +293,44 @@ impl Zeta {
|
||||
let events = self.events.clone();
|
||||
let path = snapshot
|
||||
.file()
|
||||
.map(|f| f.path().clone())
|
||||
.map(|f| Arc::from(f.full_path(cx).as_path()))
|
||||
.unwrap_or_else(|| Arc::from(Path::new("untitled")));
|
||||
|
||||
let client = self.client.clone();
|
||||
let llm_token = self.llm_token.clone();
|
||||
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
cx.spawn(|_, cx| async move {
|
||||
let request_sent_at = Instant::now();
|
||||
|
||||
let input_events = cx
|
||||
let (input_events, input_excerpt, input_outline) = cx
|
||||
.background_executor()
|
||||
.spawn(async move {
|
||||
let mut input_events = String::new();
|
||||
for event in events {
|
||||
if !input_events.is_empty() {
|
||||
input_events.push('\n');
|
||||
input_events.push('\n');
|
||||
.spawn({
|
||||
let snapshot = snapshot.clone();
|
||||
let excerpt_range = excerpt_range.clone();
|
||||
async move {
|
||||
let mut input_events = String::new();
|
||||
for event in events {
|
||||
if !input_events.is_empty() {
|
||||
input_events.push('\n');
|
||||
input_events.push('\n');
|
||||
}
|
||||
input_events.push_str(&event.to_prompt());
|
||||
}
|
||||
input_events.push_str(&event.to_prompt());
|
||||
|
||||
let input_excerpt = prompt_for_excerpt(&snapshot, &excerpt_range, offset);
|
||||
let input_outline = prompt_for_outline(&snapshot);
|
||||
|
||||
(input_events, input_excerpt, input_outline)
|
||||
}
|
||||
input_events
|
||||
})
|
||||
.await;
|
||||
|
||||
let input_excerpt = prompt_for_excerpt(&snapshot, &excerpt_range, offset);
|
||||
let input_outline = prompt_for_outline(&snapshot);
|
||||
|
||||
log::debug!("Events:\n{}\nExcerpt:\n{}", input_events, input_excerpt);
|
||||
|
||||
let body = PredictEditsParams {
|
||||
input_events: input_events.clone(),
|
||||
input_excerpt: input_excerpt.clone(),
|
||||
outline: Some(input_outline.clone()),
|
||||
};
|
||||
|
||||
let response = perform_predict_edits(client, llm_token, body).await?;
|
||||
@@ -330,10 +338,11 @@ impl Zeta {
|
||||
let output_excerpt = response.output_excerpt;
|
||||
log::debug!("completion response: {}", output_excerpt);
|
||||
|
||||
let inline_completion = Self::process_completion_response(
|
||||
Self::process_completion_response(
|
||||
output_excerpt,
|
||||
&snapshot,
|
||||
excerpt_range,
|
||||
offset,
|
||||
path,
|
||||
input_outline,
|
||||
input_events,
|
||||
@@ -341,20 +350,7 @@ impl Zeta {
|
||||
request_sent_at,
|
||||
&cx,
|
||||
)
|
||||
.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)
|
||||
.await
|
||||
})
|
||||
}
|
||||
|
||||
@@ -487,8 +483,8 @@ and then another
|
||||
}
|
||||
|
||||
zeta.update(&mut cx, |zeta, _cx| {
|
||||
zeta.recent_completions.get_mut(2).unwrap().edits = Arc::new([]);
|
||||
zeta.recent_completions.get_mut(3).unwrap().edits = Arc::new([]);
|
||||
zeta.shown_completions.get_mut(2).unwrap().edits = Arc::new([]);
|
||||
zeta.shown_completions.get_mut(3).unwrap().edits = Arc::new([]);
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
@@ -571,6 +567,7 @@ and then another
|
||||
output_excerpt: String,
|
||||
snapshot: &BufferSnapshot,
|
||||
excerpt_range: Range<usize>,
|
||||
cursor_offset: usize,
|
||||
path: Arc<Path>,
|
||||
input_outline: String,
|
||||
input_events: String,
|
||||
@@ -582,9 +579,34 @@ and then another
|
||||
cx.background_executor().spawn(async move {
|
||||
let content = output_excerpt.replace(CURSOR_MARKER, "");
|
||||
|
||||
let codefence_start = content
|
||||
.find(EDITABLE_REGION_START_MARKER)
|
||||
.context("could not find start marker")?;
|
||||
let start_markers = content
|
||||
.match_indices(EDITABLE_REGION_START_MARKER)
|
||||
.collect::<Vec<_>>();
|
||||
anyhow::ensure!(
|
||||
start_markers.len() == 1,
|
||||
"expected exactly one start marker, found {}",
|
||||
start_markers.len()
|
||||
);
|
||||
|
||||
let end_markers = content
|
||||
.match_indices(EDITABLE_REGION_END_MARKER)
|
||||
.collect::<Vec<_>>();
|
||||
anyhow::ensure!(
|
||||
end_markers.len() == 1,
|
||||
"expected exactly one end marker, found {}",
|
||||
end_markers.len()
|
||||
);
|
||||
|
||||
let sof_markers = content
|
||||
.match_indices(START_OF_FILE_MARKER)
|
||||
.collect::<Vec<_>>();
|
||||
anyhow::ensure!(
|
||||
sof_markers.len() <= 1,
|
||||
"expected at most one start-of-file marker, found {}",
|
||||
sof_markers.len()
|
||||
);
|
||||
|
||||
let codefence_start = start_markers[0].0;
|
||||
let content = &content[codefence_start..];
|
||||
|
||||
let newline_ix = content.find('\n').context("could not find newline")?;
|
||||
@@ -605,6 +627,7 @@ and then another
|
||||
id: InlineCompletionId::new(),
|
||||
path,
|
||||
excerpt_range,
|
||||
cursor_offset,
|
||||
edits: edits.into(),
|
||||
snapshot: snapshot.clone(),
|
||||
input_outline: input_outline.into(),
|
||||
@@ -687,12 +710,13 @@ and then another
|
||||
self.rated_completions.contains(&completion_id)
|
||||
}
|
||||
|
||||
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 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 rate_completion(
|
||||
@@ -702,6 +726,7 @@ and then another
|
||||
feedback: String,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
self.rated_completions.insert(completion.id);
|
||||
telemetry::event!(
|
||||
"Inline Completion Rated",
|
||||
rating,
|
||||
@@ -715,12 +740,12 @@ and then another
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn recent_completions(&self) -> impl DoubleEndedIterator<Item = &InlineCompletion> {
|
||||
self.recent_completions.iter()
|
||||
pub fn shown_completions(&self) -> impl DoubleEndedIterator<Item = &InlineCompletion> {
|
||||
self.shown_completions.iter()
|
||||
}
|
||||
|
||||
pub fn recent_completions_len(&self) -> usize {
|
||||
self.recent_completions.len()
|
||||
pub fn shown_completions_len(&self) -> usize {
|
||||
self.shown_completions.len()
|
||||
}
|
||||
|
||||
fn report_changes_for_buffer(
|
||||
@@ -943,7 +968,7 @@ impl CurrentInlineCompletion {
|
||||
|
||||
struct PendingCompletion {
|
||||
id: usize,
|
||||
_task: Task<Result<()>>,
|
||||
_task: Task<()>,
|
||||
}
|
||||
|
||||
pub struct ZetaInlineCompletionProvider {
|
||||
@@ -996,6 +1021,10 @@ impl inline_completion::InlineCompletionProvider for ZetaInlineCompletionProvide
|
||||
settings.inline_completions_enabled(language.as_ref(), file.map(|f| f.path().as_ref()), cx)
|
||||
}
|
||||
|
||||
fn is_refreshing(&self) -> bool {
|
||||
!self.pending_completions.is_empty()
|
||||
}
|
||||
|
||||
fn refresh(
|
||||
&mut self,
|
||||
buffer: Model<Buffer>,
|
||||
@@ -1017,13 +1046,16 @@ impl inline_completion::InlineCompletionProvider for ZetaInlineCompletionProvide
|
||||
})
|
||||
});
|
||||
|
||||
let mut completion = None;
|
||||
if let Ok(completion_request) = completion_request {
|
||||
completion = Some(CurrentInlineCompletion {
|
||||
buffer_id: buffer.entity_id(),
|
||||
completion: completion_request.await?,
|
||||
});
|
||||
}
|
||||
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),
|
||||
};
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
if this.pending_completions[0].id == pending_completion_id {
|
||||
@@ -1032,27 +1064,27 @@ impl inline_completion::InlineCompletionProvider for ZetaInlineCompletionProvide
|
||||
this.pending_completions.clear();
|
||||
}
|
||||
|
||||
if let Some(new_completion) = completion {
|
||||
if let Some(new_completion) = completion.context("zeta prediction failed").log_err()
|
||||
{
|
||||
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.id)
|
||||
this.zeta.update(cx, |zeta, cx| {
|
||||
zeta.completion_shown(&new_completion.completion, cx);
|
||||
});
|
||||
this.current_completion = Some(new_completion);
|
||||
}
|
||||
} else {
|
||||
this.zeta.update(cx, |zeta, _cx| {
|
||||
zeta.completion_shown(new_completion.completion.id)
|
||||
this.zeta.update(cx, |zeta, cx| {
|
||||
zeta.completion_shown(&new_completion.completion, cx);
|
||||
});
|
||||
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
|
||||
@@ -1177,6 +1209,7 @@ 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(),
|
||||
|
||||
@@ -170,13 +170,3 @@ 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`
|
||||
|
||||
Reference in New Issue
Block a user