Compare commits

...

14 Commits

Author SHA1 Message Date
Joseph T. Lyons
d54f6f97a7 zed 0.173.2 2025-02-05 13:10:36 -05:00
gcp-cherry-pick-bot[bot]
0542019e25 Fix the worktree's repository_for_path (cherry-pick #24279) (#24292)
Cherry-picked Fix the worktree's repository_for_path (#24279)

Go back to a less optimized implementation for now since the custom
cursor target seems to have some bugs.

Release Notes:

- Fixed missing git blame and status output in some projects with
multiple git repositories

Co-authored-by: Cole Miller <cole@zed.dev>
2025-02-05 12:59:55 -05:00
Danilo Leal
9f825ca3cf edit prediction: Fix license detection error logging + check for different spellings (#24281)
Follow-up to https://github.com/zed-industries/zed/pull/24278

This PR ensures we're checking if there's a license-type file in both US
& UK English spelling, and fixes the error logging again, treating for
when the worktree contains just a single file or multiple.

Release Notes:

- N/A

Co-Authored-By: Bennet Bo Fenner <53836821+bennetbo@users.noreply.github.com>
2025-02-05 14:36:49 -03:00
gcp-cherry-pick-bot[bot]
af0f36de7b Revert recent anti-aliasing improvements (cherry-pick #24289) (#24291)
Cherry-picked Revert recent anti-aliasing improvements (#24289)

This reverts commit 31fa414422.
This reverts commit b9e0aae49f.

`lyon` commit revert:


![image](https://github.com/user-attachments/assets/0243f61c-0713-416d-b8db-47372e04abaa)

`MSAA` commit revert:


![image](https://github.com/user-attachments/assets/b1a4a9fe-0192-47ef-be6f-52e03c025724)


Release Notes:

- N/A

Co-authored-by: Kirill Bulatov <kirill@zed.dev>
2025-02-05 19:35:25 +02:00
Bennet Bo Fenner
68b4f9ee1d edit prediction: Improve UX around disabled_globs and show_inline_completions (#24207)
Release Notes:

- N/A

---------

Co-authored-by: Danilo <danilo@zed.dev>
Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
2025-02-05 12:22:52 -05:00
gcp-cherry-pick-bot[bot]
8ed71b2dd0 Implement character index for point (cherry-pick #23989) (#24284)
Cherry-picked Implement character index for point (#23989)

Fixes #22939
Fixes #23970
Supersedes https://github.com/zed-industries/zed/pull/23469

Release Notes:

- Fixed a bug where Zed could crash with certain input sources on macOS

---------

Co-authored-by: Louis Brunner <louis.brunner.fr@gmail.com>
Co-authored-by: ben <ben@zed.dev>

Co-authored-by: Mikayla Maki <mikayla@zed.dev>
Co-authored-by: Louis Brunner <louis.brunner.fr@gmail.com>
Co-authored-by: ben <ben@zed.dev>
2025-02-05 11:15:25 -05:00
gcp-cherry-pick-bot[bot]
79714fca98 Fix panic when deleting an empty line after a deleted hunk (cherry-pick #24255) (#24282)
Cherry-picked Fix panic when deleting an empty line after a deleted hunk
(#24255)

Release Notes:

- Fix a panic when deleting text after a deletion hunk

Co-authored-by: Cole Miller <cole@zed.dev>
2025-02-05 11:07:04 -05:00
Agus Zubiaga
eafad7d9db Accept edit predictions with alt-tab in addition to tab (#24272)
When you have an edit prediction available, you can now also accept it
with `alt-tab` (or `alt-enter` on Linux) even if you don't have an LSP
completions menu open. This is meant to lower the mental load when going
from one mode to another.

Release Notes:

- N/A
2025-02-05 12:41:24 -03:00
Agus Zubiaga
45f9192137 edit predictions: Onboarding funnel telemetry (#24237)
Release Notes:

- N/A
2025-02-05 12:37:48 -03:00
Agus Zubiaga
1748fbeec8 edit prediction: Allow enabling OSS data collection with no project open (#24265)
This was an leftover from when we were persisting a per-project setting.

Release Notes:

- N/A
2025-02-05 12:15:38 -03:00
Agus Zubiaga
57d09d2fd3 zed 0.173.1 2025-02-04 18:50:01 -03:00
Agus Zubiaga
bc67f990ae edit prediction: Fix jump cursor position when scrolled (#24230)
We were looking up line layouts without subtracting start row so we
would get the wrong one when scrolled

Release Notes:

- N/A
2025-02-04 18:48:30 -03:00
Agus Zubiaga
722fd59d9c edit prediction: Do not render jump cursor until line layout is ready (#24226)
This is pretty rare but I found a case where `line_layouts` didn't have
the requested line yet, so we now skip rendering the cursor for that
period and avoid panicking.

Release Notes:

- N/A
2025-02-04 18:11:59 -03:00
Peter Tripp
cf4e294007 v0.173.x preview 2025-02-04 14:36:32 -05:00
43 changed files with 1065 additions and 1246 deletions

80
Cargo.lock generated
View File

@@ -1821,7 +1821,7 @@ dependencies = [
[[package]]
name = "blade-graphics"
version = "0.6.0"
source = "git+https://github.com/kvark/blade?rev=b16f5c7bd873c7126f48c82c39e7ae64602ae74f#b16f5c7bd873c7126f48c82c39e7ae64602ae74f"
source = "git+https://github.com/kvark/blade?rev=091a8401033847bb9b6ace3fcf70448d069621c5#091a8401033847bb9b6ace3fcf70448d069621c5"
dependencies = [
"ash",
"ash-window",
@@ -1853,7 +1853,7 @@ dependencies = [
[[package]]
name = "blade-macros"
version = "0.3.0"
source = "git+https://github.com/kvark/blade?rev=b16f5c7bd873c7126f48c82c39e7ae64602ae74f#b16f5c7bd873c7126f48c82c39e7ae64602ae74f"
source = "git+https://github.com/kvark/blade?rev=091a8401033847bb9b6ace3fcf70448d069621c5#091a8401033847bb9b6ace3fcf70448d069621c5"
dependencies = [
"proc-macro2",
"quote",
@@ -1863,7 +1863,7 @@ dependencies = [
[[package]]
name = "blade-util"
version = "0.2.0"
source = "git+https://github.com/kvark/blade?rev=b16f5c7bd873c7126f48c82c39e7ae64602ae74f#b16f5c7bd873c7126f48c82c39e7ae64602ae74f"
source = "git+https://github.com/kvark/blade?rev=091a8401033847bb9b6ace3fcf70448d069621c5#091a8401033847bb9b6ace3fcf70448d069621c5"
dependencies = [
"blade-graphics",
"bytemuck",
@@ -4668,12 +4668,6 @@ version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ce81f49ae8a0482e4c55ea62ebbd7e5a686af544c00b9d090bba3ff9be97b3d"
[[package]]
name = "float_next_after"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8bf7cc16383c4b8d58b9905a8509f02926ce3058053c056376248d958c9df1e8"
[[package]]
name = "flume"
version = "0.11.1"
@@ -5419,7 +5413,6 @@ dependencies = [
"inventory",
"itertools 0.14.0",
"log",
"lyon",
"media",
"metal",
"naga",
@@ -6385,9 +6378,11 @@ dependencies = [
"lsp",
"paths",
"project",
"regex",
"serde_json",
"settings",
"supermaven",
"telemetry",
"theme",
"ui",
"workspace",
@@ -7433,69 +7428,6 @@ dependencies = [
"url",
]
[[package]]
name = "lyon"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91e7f9cda98b5430809e63ca5197b06c7d191bf7e26dfc467d5a3f0290e2a74f"
dependencies = [
"lyon_algorithms",
"lyon_extra",
"lyon_tessellation",
]
[[package]]
name = "lyon_algorithms"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f13c9be19d257c7d37e70608ed858e8eab4b2afcea2e3c9a622e892acbf43c08"
dependencies = [
"lyon_path",
"num-traits",
]
[[package]]
name = "lyon_extra"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ca94c7bf1e2557c2798989c43416822c12fc5dcc5e17cc3307ef0e71894a955"
dependencies = [
"lyon_path",
"thiserror 1.0.69",
]
[[package]]
name = "lyon_geom"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8af69edc087272df438b3ee436c4bb6d7c04aa8af665cfd398feae627dbd8570"
dependencies = [
"arrayvec",
"euclid",
"num-traits",
]
[[package]]
name = "lyon_path"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e0b8aec2f58586f6eef237985b9a9b7cb3a3aff4417c575075cf95bf925252e"
dependencies = [
"lyon_geom",
"num-traits",
]
[[package]]
name = "lyon_tessellation"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "579d42360a4b09846eff2feef28f538696c7d6c7439bfa65874ff3cbe0951b2c"
dependencies = [
"float_next_after",
"lyon_path",
"num-traits",
]
[[package]]
name = "mac"
version = "0.1.1"
@@ -16411,7 +16343,7 @@ dependencies = [
[[package]]
name = "zed"
version = "0.173.0"
version = "0.173.2"
dependencies = [
"activity_indicator",
"anyhow",

View File

@@ -373,9 +373,9 @@ async-watch = "0.3.1"
async_zip = { version = "0.0.17", features = ["deflate", "deflate64"] }
base64 = "0.22"
bitflags = "2.6.0"
blade-graphics = { git = "https://github.com/kvark/blade", rev = "b16f5c7bd873c7126f48c82c39e7ae64602ae74f" }
blade-macros = { git = "https://github.com/kvark/blade", rev = "b16f5c7bd873c7126f48c82c39e7ae64602ae74f" }
blade-util = { git = "https://github.com/kvark/blade", rev = "b16f5c7bd873c7126f48c82c39e7ae64602ae74f" }
blade-graphics = { git = "https://github.com/kvark/blade", rev = "091a8401033847bb9b6ace3fcf70448d069621c5" }
blade-macros = { git = "https://github.com/kvark/blade", rev = "091a8401033847bb9b6ace3fcf70448d069621c5" }
blade-util = { git = "https://github.com/kvark/blade", rev = "091a8401033847bb9b6ace3fcf70448d069621c5" }
naga = { version = "23.1.0", features = ["wgsl-in"] }
blake3 = "1.5.3"
bytes = "1.0"

View File

@@ -0,0 +1,6 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path opacity="0.6" fill-rule="evenodd" clip-rule="evenodd" d="M6.75 9.31247L8.25 10.5576V11.75H1.75V10.0803L4.49751 7.44273L5.65909 8.40693L3.73923 10.25H6.75V9.31247ZM8.25 5.85739V4.25H6.31358L8.25 5.85739ZM1.75 5.16209V7.1H3.25V6.4072L1.75 5.16209Z" fill="black"/>
<path opacity="0.6" fill-rule="evenodd" clip-rule="evenodd" d="M10.9624 9.40853L11.9014 8L10.6241 6.08397L9.37598 6.91603L10.0986 8L9.80184 8.44518L10.9624 9.40853Z" fill="black"/>
<path opacity="0.6" fill-rule="evenodd" clip-rule="evenodd" d="M12.8936 11.0116L14.9014 8L12.6241 4.58397L11.376 5.41603L13.0986 8L11.7331 10.0483L12.8936 11.0116Z" fill="black"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.1225 13.809C14.0341 13.9146 13.877 13.9289 13.7711 13.8409L1.19311 3.40021C1.08659 3.31178 1.07221 3.15362 1.16104 3.04743L1.87752 2.19101C1.96588 2.0854 2.123 2.07112 2.22895 2.15906L14.8069 12.5998C14.9134 12.6882 14.9278 12.8464 14.839 12.9526L14.1225 13.809Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -502,6 +502,13 @@
"tab": "editor::ComposeCompletion"
}
},
{
"context": "Editor && inline_completion",
"bindings": {
// Changing the modifier currently breaks accepting while you also an LSP completions menu open
"alt-enter": "editor::AcceptInlineCompletion"
}
},
{
"context": "Editor && inline_completion && !showing_completions",
"use_key_equivalents": true,
@@ -509,13 +516,6 @@
"tab": "editor::AcceptInlineCompletion"
}
},
{
"context": "Editor && inline_completion && showing_completions",
"bindings": {
// Currently, changing this binding breaks the preview behavior
"alt-enter": "editor::AcceptInlineCompletion"
}
},
{
"context": "Editor && showing_code_actions",
"bindings": {

View File

@@ -579,6 +579,13 @@
"tab": "editor::ComposeCompletion"
}
},
{
"context": "Editor && inline_completion",
"bindings": {
// Changing the modifier currently breaks accepting while you also an LSP completions menu open
"alt-tab": "editor::AcceptInlineCompletion"
}
},
{
"context": "Editor && inline_completion && !showing_completions",
"use_key_equivalents": true,
@@ -586,13 +593,6 @@
"tab": "editor::AcceptInlineCompletion"
}
},
{
"context": "Editor && inline_completion && showing_completions",
"bindings": {
// Currently, changing this binding breaks the preview behavior
"alt-tab": "editor::AcceptInlineCompletion"
}
},
{
"context": "Editor && showing_code_actions",
"use_key_equivalents": true,

View File

@@ -2,10 +2,7 @@ use crate::{Completion, Copilot};
use anyhow::Result;
use gpui::{App, Context, Entity, EntityId, Task};
use inline_completion::{Direction, InlineCompletion, InlineCompletionProvider};
use language::{
language_settings::{all_language_settings, AllLanguageSettings},
Buffer, OffsetRangeExt, ToOffset,
};
use language::{language_settings::AllLanguageSettings, Buffer, OffsetRangeExt, ToOffset};
use settings::Settings;
use std::{path::Path, time::Duration};
@@ -73,19 +70,11 @@ impl InlineCompletionProvider for CopilotCompletionProvider {
fn is_enabled(
&self,
buffer: &Entity<Buffer>,
cursor_position: language::Anchor,
_buffer: &Entity<Buffer>,
_cursor_position: language::Anchor,
cx: &App,
) -> bool {
if !self.copilot.read(cx).status().is_authorized() {
return false;
}
let buffer = buffer.read(cx);
let file = buffer.file();
let language = buffer.language_at(cursor_position);
let settings = all_language_settings(file, cx);
settings.inline_completions_enabled(language.as_ref(), file.map(|f| f.path().as_ref()), cx)
self.copilot.read(cx).status().is_authorized()
}
fn refresh(
@@ -205,7 +194,7 @@ impl InlineCompletionProvider for CopilotCompletionProvider {
fn discard(&mut self, cx: &mut Context<Self>) {
let settings = AllLanguageSettings::get_global(cx);
let copilot_enabled = settings.inline_completions_enabled(None, None, cx);
let copilot_enabled = settings.show_inline_completions(None, cx);
if !copilot_enabled {
return;

View File

@@ -1142,12 +1142,7 @@ impl DisplaySnapshot {
}
pub fn line_indent_for_buffer_row(&self, buffer_row: MultiBufferRow) -> LineIndent {
let (buffer, range) = self
.buffer_snapshot
.buffer_line_for_row(buffer_row)
.unwrap();
buffer.line_indent_for_row(range.start.row)
self.buffer_snapshot.line_indent_for_row(buffer_row)
}
pub fn line_len(&self, row: DisplayRow) -> u32 {

View File

@@ -63,10 +63,10 @@ pub use editor_settings::{
CurrentLineHighlight, EditorSettings, ScrollBeyondLastLine, SearchSettings, ShowScrollbar,
};
pub use editor_settings_controls::*;
use element::LineWithInvisibles;
pub use element::{
CursorLayout, EditorElement, HighlightedRange, HighlightedRangeLine, PointForPosition,
};
use element::{LineWithInvisibles, PositionMap};
use futures::{future, FutureExt};
use fuzzy::StringMatchCandidate;
@@ -679,7 +679,7 @@ pub struct Editor {
stale_inline_completion_in_menu: Option<InlineCompletionState>,
// enable_inline_completions is a switch that Vim can use to disable
// edit predictions based on its mode.
enable_inline_completions: bool,
show_inline_completions: bool,
show_inline_completions_override: Option<bool>,
menu_inline_completions_policy: MenuInlineCompletionsPolicy,
inlay_hint_cache: InlayHintCache,
@@ -715,6 +715,7 @@ pub struct Editor {
>,
>,
last_bounds: Option<Bounds<Pixels>>,
last_position_map: Option<Rc<PositionMap>>,
expect_bounds_change: Option<Bounds<Pixels>>,
tasks: BTreeMap<(BufferId, BufferRow), RunnableTasks>,
tasks_update_task: Option<Task<()>>,
@@ -1377,6 +1378,7 @@ impl Editor {
gutter_hovered: false,
pixel_position_of_newest_cursor: None,
last_bounds: None,
last_position_map: None,
expect_bounds_change: None,
gutter_dimensions: GutterDimensions::default(),
style: None,
@@ -1385,7 +1387,7 @@ impl Editor {
next_editor_action_id: EditorActionId::default(),
editor_actions: Rc::default(),
show_inline_completions_override: None,
enable_inline_completions: true,
show_inline_completions: true,
menu_inline_completions_policy: MenuInlineCompletionsPolicy::ByProvider,
custom_context_menu: None,
show_git_blame_gutter: false,
@@ -1815,9 +1817,9 @@ impl Editor {
self.input_enabled = input_enabled;
}
pub fn set_inline_completions_enabled(&mut self, enabled: bool, cx: &mut Context<Self>) {
self.enable_inline_completions = enabled;
if !self.enable_inline_completions {
pub fn set_show_inline_completions_enabled(&mut self, enabled: bool, cx: &mut Context<Self>) {
self.show_inline_completions = enabled;
if !self.show_inline_completions {
self.take_active_inline_completion(cx);
cx.notify();
}
@@ -1868,8 +1870,11 @@ impl Editor {
if let Some((buffer, cursor_buffer_position)) =
self.buffer.read(cx).text_anchor_for_position(cursor, cx)
{
let show_inline_completions =
!self.should_show_inline_completions(&buffer, cursor_buffer_position, cx);
let show_inline_completions = !self.should_show_inline_completions_in_buffer(
&buffer,
cursor_buffer_position,
cx,
);
self.set_show_inline_completions(Some(show_inline_completions), window, cx);
}
}
@@ -1885,42 +1890,6 @@ impl Editor {
self.refresh_inline_completion(false, true, window, cx);
}
pub fn inline_completions_enabled(&self, cx: &App) -> 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: &Entity<Buffer>,
buffer_position: language::Anchor,
cx: &App,
) -> bool {
if !self.snippet_stack.is_empty() {
return false;
}
if self.inline_completions_disabled_in_scope(buffer, buffer_position, cx) {
return false;
}
if let Some(provider) = self.inline_completion_provider() {
if let Some(show_inline_completions) = self.show_inline_completions_override {
show_inline_completions
} else {
self.mode == EditorMode::Full && provider.is_enabled(buffer, buffer_position, cx)
}
} else {
false
}
}
fn inline_completions_disabled_in_scope(
&self,
buffer: &Entity<Buffer>,
@@ -3943,10 +3912,6 @@ impl Editor {
self.do_completion(action.item_ix, CompletionIntent::Compose, window, cx)
}
fn toggle_zed_predict_onboarding(&mut self, window: &mut Window, cx: &mut Context<Self>) {
window.dispatch_action(zed_actions::OpenZedPredictOnboarding.boxed_clone(), cx);
}
fn do_completion(
&mut self,
item_ix: Option<usize>,
@@ -4651,9 +4616,18 @@ impl Editor {
let (buffer, cursor_buffer_position) =
self.buffer.read(cx).text_anchor_for_position(cursor, cx)?;
if !self.inline_completions_enabled_in_buffer(&buffer, cursor_buffer_position, cx) {
self.discard_inline_completion(false, cx);
return None;
}
if !user_requested
&& (!self.enable_inline_completions
|| !self.should_show_inline_completions(&buffer, cursor_buffer_position, cx)
&& (!self.show_inline_completions
|| !self.should_show_inline_completions_in_buffer(
&buffer,
cursor_buffer_position,
cx,
)
|| !self.is_focused(window)
|| buffer.read(cx).is_empty())
{
@@ -4666,6 +4640,77 @@ impl Editor {
Some(())
}
pub fn should_show_inline_completions(&self, cx: &App) -> bool {
let cursor = self.selections.newest_anchor().head();
if let Some((buffer, cursor_position)) =
self.buffer.read(cx).text_anchor_for_position(cursor, cx)
{
self.should_show_inline_completions_in_buffer(&buffer, cursor_position, cx)
} else {
false
}
}
fn should_show_inline_completions_in_buffer(
&self,
buffer: &Entity<Buffer>,
buffer_position: language::Anchor,
cx: &App,
) -> bool {
if !self.snippet_stack.is_empty() {
return false;
}
if self.inline_completions_disabled_in_scope(buffer, buffer_position, cx) {
return false;
}
if let Some(show_inline_completions) = self.show_inline_completions_override {
show_inline_completions
} else {
let buffer = buffer.read(cx);
self.mode == EditorMode::Full
&& language_settings(
buffer.language_at(buffer_position).map(|l| l.name()),
buffer.file(),
cx,
)
.show_inline_completions
}
}
pub fn inline_completions_enabled(&self, cx: &App) -> bool {
let cursor = self.selections.newest_anchor().head();
if let Some((buffer, cursor_position)) =
self.buffer.read(cx).text_anchor_for_position(cursor, cx)
{
self.inline_completions_enabled_in_buffer(&buffer, cursor_position, cx)
} else {
false
}
}
fn inline_completions_enabled_in_buffer(
&self,
buffer: &Entity<Buffer>,
buffer_position: language::Anchor,
cx: &App,
) -> bool {
maybe!({
let provider = self.inline_completion_provider()?;
if !provider.is_enabled(&buffer, buffer_position, cx) {
return Some(false);
}
let buffer = buffer.read(cx);
let Some(file) = buffer.file() else {
return Some(true);
};
let settings = all_language_settings(Some(file), cx);
Some(settings.inline_completions_enabled_for_path(file.path()))
})
.unwrap_or(false)
}
fn cycle_inline_completion(
&mut self,
direction: Direction,
@@ -4676,8 +4721,8 @@ impl Editor {
let cursor = self.selections.newest_anchor().head();
let (buffer, cursor_buffer_position) =
self.buffer.read(cx).text_anchor_for_position(cursor, cx)?;
if !self.enable_inline_completions
|| !self.should_show_inline_completions(&buffer, cursor_buffer_position, cx)
if !self.show_inline_completions
|| !self.should_show_inline_completions_in_buffer(&buffer, cursor_buffer_position, cx)
{
return None;
}
@@ -5015,7 +5060,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.show_inline_completions
|| self
.active_inline_completion
.as_ref()
@@ -5418,6 +5463,7 @@ impl Editor {
min_width: Pixels,
max_width: Pixels,
cursor_point: Point,
start_row: DisplayRow,
line_layouts: &[LineWithInvisibles],
style: &EditorStyle,
accept_keystroke: &gpui::Keystroke,
@@ -5441,7 +5487,11 @@ impl Editor {
.on_mouse_down(MouseButton::Left, |_, window, _| window.prevent_default())
.on_click(cx.listener(|this, _event, window, cx| {
cx.stop_propagation();
this.toggle_zed_predict_onboarding(window, cx)
this.report_editor_event("Edit Prediction Provider ToS Clicked", None, cx);
window.dispatch_action(
zed_actions::OpenZedPredictOnboarding.boxed_clone(),
cx,
);
}))
.child(
h_flex()
@@ -5470,6 +5520,7 @@ impl Editor {
Some(completion) => self.render_edit_prediction_cursor_popover_preview(
completion,
cursor_point,
start_row,
line_layouts,
style,
cx,
@@ -5479,6 +5530,7 @@ impl Editor {
Some(stale_completion) => self.render_edit_prediction_cursor_popover_preview(
stale_completion,
cursor_point,
start_row,
line_layouts,
style,
cx,
@@ -5565,6 +5617,7 @@ impl Editor {
&self,
completion: &InlineCompletionState,
cursor_point: Point,
start_row: DisplayRow,
line_layouts: &[LineWithInvisibles],
style: &EditorStyle,
cx: &mut Context<Editor>,
@@ -5672,11 +5725,13 @@ impl Editor {
let end_point = range_around_target.end.to_point(&snapshot);
let target_point = target.text_anchor.to_point(&snapshot);
let start_column_x =
line_layouts[start_point.row as usize].x_for_index(start_point.column as usize);
let target_column_x = line_layouts[target_point.row as usize]
.x_for_index(target_point.column as usize);
let cursor_relative_position = target_column_x - start_column_x;
let cursor_relative_position = line_layouts
.get(start_point.row.saturating_sub(start_row.0) as usize)
.map(|line| {
let start_column_x = line.x_for_index(start_point.column as usize);
let target_column_x = line.x_for_index(target_point.column as usize);
target_column_x - start_column_x
});
let fade_before = start_point.column > 0;
let fade_after = end_point.column < snapshot.line_len(end_point.row);
@@ -5719,15 +5774,17 @@ impl Editor {
),
)
})
.child(
div()
.w(px(2.))
.h_full()
.bg(cursor_color)
.absolute()
.top_0()
.left(cursor_relative_position),
),
.when_some(cursor_relative_position, |parent, position| {
parent.child(
div()
.w(px(2.))
.h_full()
.bg(cursor_color)
.absolute()
.top_0()
.left(position),
)
}),
)
}),
)
@@ -14059,7 +14116,8 @@ impl Editor {
.get("vim_mode")
== Some(&serde_json::Value::Bool(true));
let copilot_enabled = all_language_settings(file, cx).inline_completions.provider
let edit_predictions_provider = all_language_settings(file, cx).inline_completions.provider;
let copilot_enabled = edit_predictions_provider
== language::language_settings::InlineCompletionProvider::Copilot;
let copilot_enabled_for_language = self
.buffer
@@ -14074,6 +14132,7 @@ impl Editor {
vim_mode,
copilot_enabled,
copilot_enabled_for_language,
edit_predictions_provider,
is_via_ssh = project.is_via_ssh(),
);
}
@@ -14386,7 +14445,7 @@ impl Editor {
.and_then(|item| item.to_any().downcast_ref::<T>())
}
fn character_size(&self, window: &mut Window) -> gpui::Point<Pixels> {
fn character_size(&self, window: &mut Window) -> gpui::Size<Pixels> {
let text_layout_details = self.text_layout_details(window);
let style = &text_layout_details.editor_style;
let font_id = window.text_system().resolve_font(&style.text.font());
@@ -14394,7 +14453,7 @@ impl Editor {
let line_height = style.text.line_height_in_pixels(window.rem_size());
let em_width = window.text_system().em_width(font_id, font_size).unwrap();
gpui::Point::new(em_width, line_height)
gpui::Size::new(em_width, line_height)
}
}
@@ -15902,9 +15961,9 @@ impl EntityInputHandler for Editor {
cx: &mut Context<Self>,
) -> Option<gpui::Bounds<Pixels>> {
let text_layout_details = self.text_layout_details(window);
let gpui::Point {
x: em_width,
y: line_height,
let gpui::Size {
width: em_width,
height: line_height,
} = self.character_size(window);
let snapshot = self.snapshot(window, cx);
@@ -15922,6 +15981,24 @@ impl EntityInputHandler for Editor {
size: size(em_width, line_height),
})
}
fn character_index_for_point(
&mut self,
point: gpui::Point<Pixels>,
_window: &mut Window,
_cx: &mut Context<Self>,
) -> Option<usize> {
let position_map = self.last_position_map.as_ref()?;
if !position_map.text_hitbox.contains(&point) {
return None;
}
let display_point = position_map.point_for_position(point).previous_valid;
let anchor = position_map
.snapshot
.display_point_to_anchor(display_point, Bias::Left);
let utf16_offset = anchor.to_offset_utf16(&position_map.snapshot.buffer_snapshot);
Some(utf16_offset.0)
}
}
trait SelectionExt {

View File

@@ -503,7 +503,6 @@ impl EditorElement {
let position_map = layout.position_map.clone();
window.on_key_event({
let editor = self.editor.clone();
let text_hitbox = layout.text_hitbox.clone();
move |event: &ModifiersChangedEvent, phase, window, cx| {
if phase != DispatchPhase::Bubble {
return;
@@ -512,7 +511,7 @@ impl EditorElement {
if editor.hover_state.focused(window, cx) {
return;
}
Self::modifiers_changed(editor, event, &position_map, &text_hitbox, window, cx)
Self::modifiers_changed(editor, event, &position_map, window, cx)
})
}
});
@@ -522,19 +521,18 @@ impl EditorElement {
editor: &mut Editor,
event: &ModifiersChangedEvent,
position_map: &PositionMap,
text_hitbox: &Hitbox,
window: &mut Window,
cx: &mut Context<Editor>,
) {
editor.update_inline_completion_preview(&event.modifiers, window, cx);
let mouse_position = window.mouse_position();
if !text_hitbox.is_hovered(window) {
if !position_map.text_hitbox.is_hovered(window) {
return;
}
editor.update_hovered_link(
position_map.point_for_position(text_hitbox.bounds, mouse_position),
position_map.point_for_position(mouse_position),
&position_map.snapshot,
event.modifiers,
window,
@@ -542,14 +540,11 @@ impl EditorElement {
)
}
#[allow(clippy::too_many_arguments)]
fn mouse_left_down(
editor: &mut Editor,
event: &MouseDownEvent,
hovered_hunk: Option<Range<Anchor>>,
position_map: &PositionMap,
text_hitbox: &Hitbox,
gutter_hitbox: &Hitbox,
line_numbers: &HashMap<MultiBufferRow, LineNumberLayout>,
window: &mut Window,
cx: &mut Context<Editor>,
@@ -558,6 +553,8 @@ impl EditorElement {
return;
}
let text_hitbox = &position_map.text_hitbox;
let gutter_hitbox = &position_map.gutter_hitbox;
let mut click_count = event.click_count;
let mut modifiers = event.modifiers;
@@ -614,8 +611,7 @@ impl EditorElement {
}
}
let point_for_position =
position_map.point_for_position(text_hitbox.bounds, event.position);
let point_for_position = position_map.point_for_position(event.position);
let position = point_for_position.previous_valid;
if modifiers.shift && modifiers.alt {
editor.select(
@@ -690,15 +686,13 @@ impl EditorElement {
editor: &mut Editor,
event: &MouseDownEvent,
position_map: &PositionMap,
text_hitbox: &Hitbox,
window: &mut Window,
cx: &mut Context<Editor>,
) {
if !text_hitbox.is_hovered(window) {
if !position_map.text_hitbox.is_hovered(window) {
return;
}
let point_for_position =
position_map.point_for_position(text_hitbox.bounds, event.position);
let point_for_position = position_map.point_for_position(event.position);
mouse_context_menu::deploy_context_menu(
editor,
Some(event.position),
@@ -713,16 +707,14 @@ impl EditorElement {
editor: &mut Editor,
event: &MouseDownEvent,
position_map: &PositionMap,
text_hitbox: &Hitbox,
window: &mut Window,
cx: &mut Context<Editor>,
) {
if !text_hitbox.is_hovered(window) || window.default_prevented() {
if !position_map.text_hitbox.is_hovered(window) || window.default_prevented() {
return;
}
let point_for_position =
position_map.point_for_position(text_hitbox.bounds, event.position);
let point_for_position = position_map.point_for_position(event.position);
let position = point_for_position.previous_valid;
editor.select(
@@ -739,15 +731,11 @@ impl EditorElement {
fn mouse_up(
editor: &mut Editor,
event: &MouseUpEvent,
#[cfg_attr(
not(any(target_os = "linux", target_os = "freebsd")),
allow(unused_variables)
)]
position_map: &PositionMap,
text_hitbox: &Hitbox,
window: &mut Window,
cx: &mut Context<Editor>,
) {
let text_hitbox = &position_map.text_hitbox;
let end_selection = editor.has_pending_selection();
let pending_nonempty_selections = editor.has_pending_nonempty_selection();
@@ -767,8 +755,7 @@ impl EditorElement {
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
if EditorSettings::get_global(cx).middle_click_paste {
if let Some(text) = cx.read_from_primary().and_then(|item| item.text()) {
let point_for_position =
position_map.point_for_position(text_hitbox.bounds, event.position);
let point_for_position = position_map.point_for_position(event.position);
let position = point_for_position.previous_valid;
editor.select(
@@ -791,10 +778,10 @@ impl EditorElement {
editor: &mut Editor,
event: &ClickEvent,
position_map: &PositionMap,
text_hitbox: &Hitbox,
window: &mut Window,
cx: &mut Context<Editor>,
) {
let text_hitbox = &position_map.text_hitbox;
let pending_nonempty_selections = editor.has_pending_nonempty_selection();
let multi_cursor_setting = EditorSettings::get_global(cx).multi_cursor_modifier;
@@ -804,7 +791,7 @@ impl EditorElement {
};
if !pending_nonempty_selections && multi_cursor_modifier && text_hitbox.is_hovered(window) {
let point = position_map.point_for_position(text_hitbox.bounds, event.up.position);
let point = position_map.point_for_position(event.up.position);
editor.handle_click_hovered_link(point, event.modifiers(), window, cx);
cx.stop_propagation();
@@ -815,7 +802,6 @@ impl EditorElement {
editor: &mut Editor,
event: &MouseMoveEvent,
position_map: &PositionMap,
text_bounds: Bounds<Pixels>,
window: &mut Window,
cx: &mut Context<Editor>,
) {
@@ -823,7 +809,8 @@ impl EditorElement {
return;
}
let point_for_position = position_map.point_for_position(text_bounds, event.position);
let text_bounds = position_map.text_hitbox.bounds;
let point_for_position = position_map.point_for_position(event.position);
let mut scroll_delta = gpui::Point::<f32>::default();
let vertical_margin = position_map.line_height.min(text_bounds.size.height / 3.0);
let top = text_bounds.origin.y + vertical_margin;
@@ -870,19 +857,18 @@ impl EditorElement {
editor: &mut Editor,
event: &MouseMoveEvent,
position_map: &PositionMap,
text_hitbox: &Hitbox,
gutter_hitbox: &Hitbox,
window: &mut Window,
cx: &mut Context<Editor>,
) {
let text_hitbox = &position_map.text_hitbox;
let gutter_hitbox = &position_map.gutter_hitbox;
let modifiers = event.modifiers;
let gutter_hovered = gutter_hitbox.is_hovered(window);
editor.set_gutter_hovered(gutter_hovered, cx);
// Don't trigger hover popover if mouse is hovering over context menu
if text_hitbox.is_hovered(window) {
let point_for_position =
position_map.point_for_position(text_hitbox.bounds, event.position);
let point_for_position = position_map.point_for_position(event.position);
editor.update_hovered_link(
point_for_position,
@@ -3300,6 +3286,7 @@ impl EditorElement {
min_width,
max_width,
cursor_point,
start_row,
&line_layouts,
style,
accept_keystroke.as_ref()?,
@@ -4107,8 +4094,7 @@ impl EditorElement {
window: &mut Window,
cx: &mut App,
) -> Vec<AnyElement> {
let point_for_position =
position_map.point_for_position(text_hitbox.bounds, window.mouse_position());
let point_for_position = position_map.point_for_position(window.mouse_position());
let mut controls = vec![];
@@ -4245,7 +4231,10 @@ impl EditorElement {
let scroll_top = layout.position_map.snapshot.scroll_position().y;
let gutter_bg = cx.theme().colors().editor_gutter_background;
window.paint_quad(fill(layout.gutter_hitbox.bounds, gutter_bg));
window.paint_quad(fill(layout.text_hitbox.bounds, self.style.background));
window.paint_quad(fill(
layout.position_map.text_hitbox.bounds,
self.style.background,
));
if let EditorMode::Full = layout.mode {
let mut active_rows = layout.active_rows.iter().peekable();
@@ -4270,8 +4259,8 @@ impl EditorElement {
end: layout.gutter_hitbox.right(),
}),
CurrentLineHighlight::Line => Some(Range {
start: layout.text_hitbox.bounds.left(),
end: layout.text_hitbox.bounds.right(),
start: layout.position_map.text_hitbox.bounds.left(),
end: layout.position_map.text_hitbox.bounds.right(),
}),
CurrentLineHighlight::All => Some(Range {
start: layout.hitbox.left(),
@@ -4345,7 +4334,7 @@ impl EditorElement {
layout.position_map.snapshot.scroll_position().x * layout.position_map.em_width;
for (wrap_position, active) in layout.wrap_guides.iter() {
let x = (layout.text_hitbox.origin.x
let x = (layout.position_map.text_hitbox.origin.x
+ *wrap_position
+ layout.position_map.em_width / 2.)
- scroll_left;
@@ -4357,7 +4346,7 @@ impl EditorElement {
|| scrollbar_y.as_ref().map_or(false, |sy| sy.visible)
};
if x < layout.text_hitbox.origin.x
if x < layout.position_map.text_hitbox.origin.x
|| (show_scrollbars && x > self.scrollbar_left(&layout.hitbox.bounds))
{
continue;
@@ -4370,8 +4359,8 @@ impl EditorElement {
};
window.paint_quad(fill(
Bounds {
origin: point(x, layout.text_hitbox.origin.y),
size: size(px(1.), layout.text_hitbox.size.height),
origin: point(x, layout.position_map.text_hitbox.origin.y),
size: size(px(1.), layout.position_map.text_hitbox.size.height),
},
color,
));
@@ -4746,7 +4735,7 @@ impl EditorElement {
fn paint_text(&mut self, layout: &mut EditorLayout, window: &mut Window, cx: &mut App) {
window.with_content_mask(
Some(ContentMask {
bounds: layout.text_hitbox.bounds,
bounds: layout.position_map.text_hitbox.bounds,
}),
|window| {
let cursor_style = if self
@@ -4760,7 +4749,7 @@ impl EditorElement {
} else {
CursorStyle::IBeam
};
window.set_cursor_style(cursor_style, &layout.text_hitbox);
window.set_cursor_style(cursor_style, &layout.position_map.text_hitbox);
let invisible_display_ranges = self.paint_highlights(layout, window);
self.paint_lines(&invisible_display_ranges, layout, window, cx);
@@ -4782,7 +4771,7 @@ impl EditorElement {
layout: &mut EditorLayout,
window: &mut Window,
) -> SmallVec<[Range<DisplayPoint>; 32]> {
window.paint_layer(layout.text_hitbox.bounds, |window| {
window.paint_layer(layout.position_map.text_hitbox.bounds, |window| {
let mut invisible_display_ranges = SmallVec::<[Range<DisplayPoint>; 32]>::new();
let line_end_overshoot = 0.15 * layout.position_map.line_height;
for (range, color) in &layout.highlighted_ranges {
@@ -4861,7 +4850,7 @@ impl EditorElement {
// A softer than perfect black
let redaction_color = gpui::rgb(0x0e1111);
window.paint_layer(layout.text_hitbox.bounds, |window| {
window.paint_layer(layout.position_map.text_hitbox.bounds, |window| {
for range in layout.redacted_ranges.iter() {
self.paint_highlighted_range(
range.clone(),
@@ -5435,13 +5424,13 @@ impl EditorElement {
.collect(),
};
highlighted_range.paint(layout.text_hitbox.bounds, window);
highlighted_range.paint(layout.position_map.text_hitbox.bounds, window);
}
}
fn paint_inline_blame(&mut self, layout: &mut EditorLayout, window: &mut Window, cx: &mut App) {
if let Some(mut inline_blame) = layout.inline_blame.take() {
window.paint_layer(layout.text_hitbox.bounds, |window| {
window.paint_layer(layout.position_map.text_hitbox.bounds, |window| {
inline_blame.paint(window, cx);
})
}
@@ -5560,8 +5549,6 @@ impl EditorElement {
window.on_mouse_event({
let position_map = layout.position_map.clone();
let editor = self.editor.clone();
let text_hitbox = layout.text_hitbox.clone();
let gutter_hitbox = layout.gutter_hitbox.clone();
let multi_buffer_range =
layout
.display_hunks
@@ -5600,32 +5587,16 @@ impl EditorElement {
event,
multi_buffer_range.clone(),
&position_map,
&text_hitbox,
&gutter_hitbox,
line_numbers.as_ref(),
window,
cx,
);
}),
MouseButton::Right => editor.update(cx, |editor, cx| {
Self::mouse_right_down(
editor,
event,
&position_map,
&text_hitbox,
window,
cx,
);
Self::mouse_right_down(editor, event, &position_map, window, cx);
}),
MouseButton::Middle => editor.update(cx, |editor, cx| {
Self::mouse_middle_down(
editor,
event,
&position_map,
&text_hitbox,
window,
cx,
);
Self::mouse_middle_down(editor, event, &position_map, window, cx);
}),
_ => {}
};
@@ -5636,12 +5607,11 @@ impl EditorElement {
window.on_mouse_event({
let editor = self.editor.clone();
let position_map = layout.position_map.clone();
let text_hitbox = layout.text_hitbox.clone();
move |event: &MouseUpEvent, phase, window, cx| {
if phase == DispatchPhase::Bubble {
editor.update(cx, |editor, cx| {
Self::mouse_up(editor, event, &position_map, &text_hitbox, window, cx)
Self::mouse_up(editor, event, &position_map, window, cx)
});
}
}
@@ -5650,8 +5620,6 @@ impl EditorElement {
window.on_mouse_event({
let editor = self.editor.clone();
let position_map = layout.position_map.clone();
let text_hitbox = layout.text_hitbox.clone();
let mut captured_mouse_down = None;
move |event: &MouseUpEvent, phase, window, cx| match phase {
@@ -5665,7 +5633,7 @@ impl EditorElement {
.clone();
let mut pending_mouse_down = pending_mouse_down.borrow_mut();
if pending_mouse_down.is_some() && text_hitbox.is_hovered(window) {
if pending_mouse_down.is_some() && position_map.text_hitbox.is_hovered(window) {
captured_mouse_down = pending_mouse_down.take();
window.refresh();
}
@@ -5677,7 +5645,7 @@ impl EditorElement {
down: mouse_down,
up: event.clone(),
};
Self::click(editor, &event, &position_map, &text_hitbox, window, cx);
Self::click(editor, &event, &position_map, window, cx);
}
}),
}
@@ -5686,8 +5654,6 @@ impl EditorElement {
window.on_mouse_event({
let position_map = layout.position_map.clone();
let editor = self.editor.clone();
let text_hitbox = layout.text_hitbox.clone();
let gutter_hitbox = layout.gutter_hitbox.clone();
move |event: &MouseMoveEvent, phase, window, cx| {
if phase == DispatchPhase::Bubble {
@@ -5698,25 +5664,10 @@ impl EditorElement {
if event.pressed_button == Some(MouseButton::Left)
|| event.pressed_button == Some(MouseButton::Middle)
{
Self::mouse_dragged(
editor,
event,
&position_map,
text_hitbox.bounds,
window,
cx,
)
Self::mouse_dragged(editor, event, &position_map, window, cx)
}
Self::mouse_moved(
editor,
event,
&position_map,
&text_hitbox,
&gutter_hitbox,
window,
cx,
)
Self::mouse_moved(editor, event, &position_map, window, cx)
});
}
}
@@ -7566,6 +7517,12 @@ impl Element for EditorElement {
em_width,
em_advance,
snapshot,
gutter_hitbox: gutter_hitbox.clone(),
text_hitbox: text_hitbox.clone(),
});
self.editor.update(cx, |editor, _| {
editor.last_position_map = Some(position_map.clone())
});
let hunk_controls = self.layout_diff_hunk_controls(
@@ -7589,7 +7546,6 @@ impl Element for EditorElement {
wrap_guides,
indent_guides,
hitbox,
text_hitbox,
gutter_hitbox,
display_hunks,
content_origin,
@@ -7761,7 +7717,6 @@ impl IntoElement for EditorElement {
pub struct EditorLayout {
position_map: Rc<PositionMap>,
hitbox: Hitbox,
text_hitbox: Hitbox,
gutter_hitbox: Hitbox,
content_origin: gpui::Point<Pixels>,
scrollbars_layout: AxisPair<Option<ScrollbarLayout>>,
@@ -7941,15 +7896,17 @@ struct CreaseTrailerLayout {
bounds: Bounds<Pixels>,
}
struct PositionMap {
size: Size<Pixels>,
line_height: Pixels,
scroll_pixel_position: gpui::Point<Pixels>,
scroll_max: gpui::Point<f32>,
em_width: Pixels,
em_advance: Pixels,
line_layouts: Vec<LineWithInvisibles>,
snapshot: EditorSnapshot,
pub(crate) struct PositionMap {
pub size: Size<Pixels>,
pub line_height: Pixels,
pub scroll_pixel_position: gpui::Point<Pixels>,
pub scroll_max: gpui::Point<f32>,
pub em_width: Pixels,
pub em_advance: Pixels,
pub line_layouts: Vec<LineWithInvisibles>,
pub snapshot: EditorSnapshot,
pub text_hitbox: Hitbox,
pub gutter_hitbox: Hitbox,
}
#[derive(Debug, Copy, Clone)]
@@ -7971,11 +7928,8 @@ impl PointForPosition {
}
impl PositionMap {
fn point_for_position(
&self,
text_bounds: Bounds<Pixels>,
position: gpui::Point<Pixels>,
) -> PointForPosition {
pub(crate) fn point_for_position(&self, position: gpui::Point<Pixels>) -> PointForPosition {
let text_bounds = self.text_hitbox.bounds;
let scroll_position = self.snapshot.scroll_position();
let position = position - text_bounds.origin;
let y = position.y.max(px(0.)).min(self.size.height);
@@ -8244,8 +8198,8 @@ impl HighlightedRange {
};
let top_curve_width = curve_width(first_line.start_x, first_line.end_x);
let mut builder = gpui::PathBuilder::fill();
builder.curve_to(first_top_right + curve_height, first_top_right);
let mut path = gpui::Path::new(first_top_right - top_curve_width);
path.curve_to(first_top_right + curve_height, first_top_right);
let mut iter = lines.iter().enumerate().peekable();
while let Some((ix, line)) = iter.next() {
@@ -8256,42 +8210,42 @@ impl HighlightedRange {
match next_top_right.x.partial_cmp(&bottom_right.x).unwrap() {
Ordering::Equal => {
builder.line_to(bottom_right);
path.line_to(bottom_right);
}
Ordering::Less => {
let curve_width = curve_width(next_top_right.x, bottom_right.x);
builder.line_to(bottom_right - curve_height);
path.line_to(bottom_right - curve_height);
if self.corner_radius > Pixels::ZERO {
builder.curve_to(bottom_right - curve_width, bottom_right);
path.curve_to(bottom_right - curve_width, bottom_right);
}
builder.line_to(next_top_right + curve_width);
path.line_to(next_top_right + curve_width);
if self.corner_radius > Pixels::ZERO {
builder.curve_to(next_top_right + curve_height, next_top_right);
path.curve_to(next_top_right + curve_height, next_top_right);
}
}
Ordering::Greater => {
let curve_width = curve_width(bottom_right.x, next_top_right.x);
builder.line_to(bottom_right - curve_height);
path.line_to(bottom_right - curve_height);
if self.corner_radius > Pixels::ZERO {
builder.curve_to(bottom_right + curve_width, bottom_right);
path.curve_to(bottom_right + curve_width, bottom_right);
}
builder.line_to(next_top_right - curve_width);
path.line_to(next_top_right - curve_width);
if self.corner_radius > Pixels::ZERO {
builder.curve_to(next_top_right + curve_height, next_top_right);
path.curve_to(next_top_right + curve_height, next_top_right);
}
}
}
} else {
let curve_width = curve_width(line.start_x, line.end_x);
builder.line_to(bottom_right - curve_height);
path.line_to(bottom_right - curve_height);
if self.corner_radius > Pixels::ZERO {
builder.curve_to(bottom_right - curve_width, bottom_right);
path.curve_to(bottom_right - curve_width, bottom_right);
}
let bottom_left = point(line.start_x, bottom_right.y);
builder.line_to(bottom_left + curve_width);
path.line_to(bottom_left + curve_width);
if self.corner_radius > Pixels::ZERO {
builder.curve_to(bottom_left - curve_height, bottom_left);
path.curve_to(bottom_left - curve_height, bottom_left);
}
}
}
@@ -8299,26 +8253,24 @@ impl HighlightedRange {
if first_line.start_x > last_line.start_x {
let curve_width = curve_width(last_line.start_x, first_line.start_x);
let second_top_left = point(last_line.start_x, start_y + self.line_height);
builder.line_to(second_top_left + curve_height);
path.line_to(second_top_left + curve_height);
if self.corner_radius > Pixels::ZERO {
builder.curve_to(second_top_left + curve_width, second_top_left);
path.curve_to(second_top_left + curve_width, second_top_left);
}
let first_bottom_left = point(first_line.start_x, second_top_left.y);
builder.line_to(first_bottom_left - curve_width);
path.line_to(first_bottom_left - curve_width);
if self.corner_radius > Pixels::ZERO {
builder.curve_to(first_bottom_left - curve_height, first_bottom_left);
path.curve_to(first_bottom_left - curve_height, first_bottom_left);
}
}
builder.line_to(first_top_left + curve_height);
path.line_to(first_top_left + curve_height);
if self.corner_radius > Pixels::ZERO {
builder.curve_to(first_top_left + top_curve_width, first_top_left);
path.curve_to(first_top_left + top_curve_width, first_top_left);
}
builder.line_to(first_top_right - top_curve_width);
path.line_to(first_top_right - top_curve_width);
if let Ok(path) = builder.build() {
window.paint_path(path, self.color);
}
window.paint_path(path, self.color);
}
}

View File

@@ -229,9 +229,10 @@ pub fn deploy_context_menu(
cx,
),
None => {
let character_size = editor.character_size(window);
let menu_position = MenuPosition::PinnedToEditor {
source: source_anchor,
offset: editor.character_size(window),
offset: gpui::point(character_size.width, character_size.height),
};
Some(MouseContextMenu::new(
menu_position,

View File

@@ -108,7 +108,6 @@ thiserror.workspace = true
util.workspace = true
uuid.workspace = true
waker-fn = "1.2.0"
lyon = "1.0"
[target.'cfg(target_os = "macos")'.dependencies]
block = "0.1"
@@ -206,7 +205,6 @@ rand.workspace = true
util = { workspace = true, features = ["test-support"] }
http_client = { workspace = true, features = ["test-support"] }
unicode-segmentation.workspace = true
lyon = { version = "1.0", features = ["extra"] }
[target.'cfg(target_os = "windows")'.build-dependencies]
embed-resource = "3.0"

View File

@@ -218,17 +218,13 @@ impl Render for GradientViewer {
let height = square_bounds.size.height;
let horizontal_offset = height;
let vertical_offset = px(30.);
let mut builder = gpui::PathBuilder::fill();
builder.move_to(square_bounds.bottom_left());
builder
.line_to(square_bounds.origin + point(horizontal_offset, vertical_offset));
builder.line_to(
let mut path = gpui::Path::new(square_bounds.bottom_left());
path.line_to(square_bounds.origin + point(horizontal_offset, vertical_offset));
path.line_to(
square_bounds.top_right() + point(-horizontal_offset, vertical_offset),
);
builder.line_to(square_bounds.bottom_right());
builder.line_to(square_bounds.bottom_left());
let path = builder.build().unwrap();
path.line_to(square_bounds.bottom_right());
path.line_to(square_bounds.bottom_left());
window.paint_path(
path,
linear_gradient(

View File

@@ -364,6 +364,20 @@ impl EntityInputHandler for TextInput {
),
))
}
fn character_index_for_point(
&mut self,
point: gpui::Point<Pixels>,
_window: &mut Window,
_cx: &mut Context<Self>,
) -> Option<usize> {
let line_point = self.last_bounds?.localize(&point)?;
let last_layout = self.last_layout.as_ref()?;
assert_eq!(last_layout.text, self.content);
let utf8_index = last_layout.index_for_x(point.x - line_point.x)?;
Some(self.offset_to_utf16(utf8_index))
}
}
struct TextElement {

View File

@@ -1,62 +1,46 @@
use gpui::{
canvas, div, linear_color_stop, linear_gradient, point, prelude::*, px, rgb, size, Application,
Background, Bounds, ColorSpace, Context, MouseDownEvent, Path, PathBuilder, PathStyle, Pixels,
Point, Render, StrokeOptions, Window, WindowOptions,
canvas, div, point, prelude::*, px, size, App, Application, Bounds, Context, MouseDownEvent,
Path, Pixels, Point, Render, Window, WindowOptions,
};
struct PaintingViewer {
default_lines: Vec<(Path<Pixels>, Background)>,
default_lines: Vec<Path<Pixels>>,
lines: Vec<Vec<Point<Pixels>>>,
start: Point<Pixels>,
_painting: bool,
}
impl PaintingViewer {
fn new(_window: &mut Window, _cx: &mut Context<Self>) -> Self {
fn new() -> Self {
let mut lines = vec![];
// draw a Rust logo
let mut builder = lyon::path::Path::svg_builder();
lyon::extra::rust_logo::build_logo_path(&mut builder);
// move down the Path
let mut builder: PathBuilder = builder.into();
builder.translate(point(px(10.), px(100.)));
builder.scale(0.9);
let path = builder.build().unwrap();
lines.push((path, gpui::black().into()));
// draw a line
let mut path = Path::new(point(px(50.), px(180.)));
path.line_to(point(px(100.), px(120.)));
// go back to close the path
path.line_to(point(px(100.), px(121.)));
path.line_to(point(px(50.), px(181.)));
lines.push(path);
// draw a lightening bolt ⚡
let mut builder = PathBuilder::fill();
builder.move_to(point(px(150.), px(200.)));
builder.line_to(point(px(200.), px(125.)));
builder.line_to(point(px(200.), px(175.)));
builder.line_to(point(px(250.), px(100.)));
let path = builder.build().unwrap();
lines.push((path, rgb(0x1d4ed8).into()));
let mut path = Path::new(point(px(150.), px(200.)));
path.line_to(point(px(200.), px(125.)));
path.line_to(point(px(200.), px(175.)));
path.line_to(point(px(250.), px(100.)));
lines.push(path);
// draw a ⭐
let mut builder = PathBuilder::fill();
builder.move_to(point(px(350.), px(100.)));
builder.line_to(point(px(370.), px(160.)));
builder.line_to(point(px(430.), px(160.)));
builder.line_to(point(px(380.), px(200.)));
builder.line_to(point(px(400.), px(260.)));
builder.line_to(point(px(350.), px(220.)));
builder.line_to(point(px(300.), px(260.)));
builder.line_to(point(px(320.), px(200.)));
builder.line_to(point(px(270.), px(160.)));
builder.line_to(point(px(330.), px(160.)));
builder.line_to(point(px(350.), px(100.)));
let path = builder.build().unwrap();
lines.push((
path,
linear_gradient(
180.,
linear_color_stop(rgb(0xFACC15), 0.7),
linear_color_stop(rgb(0xD56D0C), 1.),
)
.color_space(ColorSpace::Oklab),
));
let mut path = Path::new(point(px(350.), px(100.)));
path.line_to(point(px(370.), px(160.)));
path.line_to(point(px(430.), px(160.)));
path.line_to(point(px(380.), px(200.)));
path.line_to(point(px(400.), px(260.)));
path.line_to(point(px(350.), px(220.)));
path.line_to(point(px(300.), px(260.)));
path.line_to(point(px(320.), px(200.)));
path.line_to(point(px(270.), px(160.)));
path.line_to(point(px(330.), px(160.)));
path.line_to(point(px(350.), px(100.)));
lines.push(path);
let square_bounds = Bounds {
origin: point(px(450.), px(100.)),
@@ -65,42 +49,18 @@ impl PaintingViewer {
let height = square_bounds.size.height;
let horizontal_offset = height;
let vertical_offset = px(30.);
let mut builder = PathBuilder::fill();
builder.move_to(square_bounds.bottom_left());
builder.curve_to(
let mut path = Path::new(square_bounds.bottom_left());
path.curve_to(
square_bounds.origin + point(horizontal_offset, vertical_offset),
square_bounds.origin + point(px(0.0), vertical_offset),
);
builder.line_to(square_bounds.top_right() + point(-horizontal_offset, vertical_offset));
builder.curve_to(
path.line_to(square_bounds.top_right() + point(-horizontal_offset, vertical_offset));
path.curve_to(
square_bounds.bottom_right(),
square_bounds.top_right() + point(px(0.0), vertical_offset),
);
builder.line_to(square_bounds.bottom_left());
let path = builder.build().unwrap();
lines.push((
path,
linear_gradient(
180.,
linear_color_stop(gpui::blue(), 0.4),
linear_color_stop(gpui::red(), 1.),
),
));
// draw a wave
let options = StrokeOptions::default()
.with_line_width(1.)
.with_line_join(lyon::path::LineJoin::Bevel);
let mut builder = PathBuilder::stroke(px(1.)).with_style(PathStyle::Stroke(options));
builder.move_to(point(px(40.), px(320.)));
for i in 0..50 {
builder.line_to(point(
px(40.0 + i as f32 * 10.0),
px(320.0 + (i as f32 * 10.0).sin() * 40.0),
));
}
let path = builder.build().unwrap();
lines.push((path, gpui::green().into()));
path.line_to(square_bounds.bottom_left());
lines.push(path);
Self {
default_lines: lines.clone(),
@@ -155,28 +115,27 @@ impl Render for PaintingViewer {
canvas(
move |_, _, _| {},
move |_, _, window, _| {
for (path, color) in default_lines {
window.paint_path(path, color);
const STROKE_WIDTH: Pixels = px(2.0);
for path in default_lines {
window.paint_path(path, gpui::black());
}
for points in lines {
if points.len() < 2 {
continue;
let mut path = Path::new(points[0]);
for p in points.iter().skip(1) {
path.line_to(*p);
}
let mut builder = PathBuilder::stroke(px(1.));
for (i, p) in points.into_iter().enumerate() {
if i == 0 {
builder.move_to(p);
} else {
builder.line_to(p);
let mut last = points.last().unwrap();
for p in points.iter().rev() {
let mut offset_x = px(0.);
if last.x == p.x {
offset_x = STROKE_WIDTH;
}
path.line_to(point(p.x + offset_x, p.y + STROKE_WIDTH));
last = p;
}
if let Ok(path) = builder.build() {
window.paint_path(path, gpui::black());
}
window.paint_path(path, gpui::black());
}
},
)
@@ -226,13 +185,13 @@ impl Render for PaintingViewer {
}
fn main() {
Application::new().run(|cx| {
Application::new().run(|cx: &mut App| {
cx.open_window(
WindowOptions {
focus: true,
..Default::default()
},
|window, cx| cx.new(|cx| PaintingViewer::new(window, cx)),
|_, cx| cx.new(|_| PaintingViewer::new()),
)
.unwrap();
cx.activate(true);

View File

@@ -217,6 +217,19 @@ impl Point<Pixels> {
}
}
impl<T> Point<T>
where
T: Sub<T, Output = T> + Debug + Clone + Default,
{
/// Get the position of this point, relative to the given origin
pub fn relative_to(&self, origin: &Point<T>) -> Point<T> {
point(
self.x.clone() - origin.x.clone(),
self.y.clone() - origin.y.clone(),
)
}
}
impl<T, Rhs> Mul<Rhs> for Point<T>
where
T: Mul<Rhs, Output = T> + Clone + Default + Debug,
@@ -376,6 +389,13 @@ pub struct Size<T: Clone + Default + Debug> {
pub height: T,
}
impl<T: Clone + Default + Debug> Size<T> {
/// Create a new Size, a synonym for [`size`]
pub fn new(width: T, height: T) -> Self {
size(width, height)
}
}
/// Constructs a new `Size<T>` with the provided width and height.
///
/// # Arguments
@@ -1456,6 +1476,17 @@ where
}
}
impl<T> Bounds<T>
where
T: Add<T, Output = T> + PartialOrd + Clone + Default + Debug + Sub<T, Output = T>,
{
/// Convert a point to the coordinate space defined by this Bounds
pub fn localize(&self, point: &Point<T>) -> Option<Point<T>> {
self.contains(point)
.then(|| point.relative_to(&self.origin))
}
}
/// Checks if the bounds represent an empty area.
///
/// # Returns

View File

@@ -82,7 +82,6 @@ mod input;
mod interactive;
mod key_dispatch;
mod keymap;
mod path_builder;
mod platform;
pub mod prelude;
mod scene;
@@ -136,7 +135,6 @@ pub use input::*;
pub use interactive::*;
use key_dispatch::*;
pub use keymap::*;
pub use path_builder::*;
pub use platform::*;
pub use refineable::*;
pub use scene::*;

View File

@@ -62,6 +62,14 @@ pub trait EntityInputHandler: 'static + Sized {
window: &mut Window,
cx: &mut Context<Self>,
) -> Option<Bounds<Pixels>>;
/// See [`InputHandler::character_index_for_point`] for details
fn character_index_for_point(
&mut self,
point: crate::Point<Pixels>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Option<usize>;
}
/// The canonical implementation of [`PlatformInputHandler`]. Call [`Window::handle_input`]
@@ -158,4 +166,15 @@ impl<V: EntityInputHandler> InputHandler for ElementInputHandler<V> {
view.bounds_for_range(range_utf16, self.element_bounds, window, cx)
})
}
fn character_index_for_point(
&mut self,
point: crate::Point<Pixels>,
window: &mut Window,
cx: &mut App,
) -> Option<usize> {
self.view.update(cx, |view, cx| {
view.character_index_for_point(point, window, cx)
})
}
}

View File

@@ -1,241 +0,0 @@
use anyhow::Error;
use etagere::euclid::Vector2D;
use lyon::geom::Angle;
use lyon::tessellation::{
BuffersBuilder, FillTessellator, FillVertex, StrokeTessellator, StrokeVertex, VertexBuffers,
};
pub use lyon::math::Transform;
pub use lyon::tessellation::{FillOptions, FillRule, StrokeOptions};
use crate::{point, px, Path, Pixels, Point};
/// Style of the PathBuilder
pub enum PathStyle {
/// Stroke style
Stroke(StrokeOptions),
/// Fill style
Fill(FillOptions),
}
/// A [`Path`] builder.
pub struct PathBuilder {
raw: lyon::path::builder::WithSvg<lyon::path::BuilderImpl>,
transform: Option<lyon::math::Transform>,
/// PathStyle of the PathBuilder
pub style: PathStyle,
}
impl From<lyon::path::Builder> for PathBuilder {
fn from(builder: lyon::path::Builder) -> Self {
Self {
raw: builder.with_svg(),
..Default::default()
}
}
}
impl From<lyon::path::builder::WithSvg<lyon::path::BuilderImpl>> for PathBuilder {
fn from(raw: lyon::path::builder::WithSvg<lyon::path::BuilderImpl>) -> Self {
Self {
raw,
..Default::default()
}
}
}
impl From<lyon::math::Point> for Point<Pixels> {
fn from(p: lyon::math::Point) -> Self {
point(px(p.x), px(p.y))
}
}
impl From<Point<Pixels>> for lyon::math::Point {
fn from(p: Point<Pixels>) -> Self {
lyon::math::point(p.x.0, p.y.0)
}
}
impl Default for PathBuilder {
fn default() -> Self {
Self {
raw: lyon::path::Path::builder().with_svg(),
style: PathStyle::Fill(FillOptions::default()),
transform: None,
}
}
}
impl PathBuilder {
/// Creates a new [`PathBuilder`] to build a Stroke path.
pub fn stroke(width: Pixels) -> Self {
Self {
style: PathStyle::Stroke(StrokeOptions::default().with_line_width(width.0)),
..Self::default()
}
}
/// Creates a new [`PathBuilder`] to build a Fill path.
pub fn fill() -> Self {
Self::default()
}
/// Sets the style of the [`PathBuilder`].
pub fn with_style(self, style: PathStyle) -> Self {
Self { style, ..self }
}
/// Move the current point to the given point.
#[inline]
pub fn move_to(&mut self, to: Point<Pixels>) {
self.raw.move_to(to.into());
}
/// Draw a straight line from the current point to the given point.
#[inline]
pub fn line_to(&mut self, to: Point<Pixels>) {
self.raw.line_to(to.into());
}
/// Draw a curve from the current point to the given point, using the given control point.
#[inline]
pub fn curve_to(&mut self, to: Point<Pixels>, ctrl: Point<Pixels>) {
self.raw.quadratic_bezier_to(ctrl.into(), to.into());
}
/// Adds a cubic Bézier to the [`Path`] given its two control points
/// and its end point.
#[inline]
pub fn cubic_bezier_to(
&mut self,
to: Point<Pixels>,
control_a: Point<Pixels>,
control_b: Point<Pixels>,
) {
self.raw
.cubic_bezier_to(control_a.into(), control_b.into(), to.into());
}
/// Close the current sub-path.
#[inline]
pub fn close(&mut self) {
self.raw.close();
}
/// Applies a transform to the path.
#[inline]
pub fn transform(&mut self, transform: Transform) {
self.transform = Some(transform);
}
/// Applies a translation to the path.
#[inline]
pub fn translate(&mut self, to: Point<Pixels>) {
if let Some(transform) = self.transform {
self.transform = Some(transform.then_translate(Vector2D::new(to.x.0, to.y.0)));
} else {
self.transform = Some(Transform::translation(to.x.0, to.y.0))
}
}
/// Applies a scale to the path.
#[inline]
pub fn scale(&mut self, scale: f32) {
if let Some(transform) = self.transform {
self.transform = Some(transform.then_scale(scale, scale));
} else {
self.transform = Some(Transform::scale(scale, scale));
}
}
/// Applies a rotation to the path.
///
/// The `angle` is in degrees value in the range 0.0 to 360.0.
#[inline]
pub fn rotate(&mut self, angle: f32) {
let radians = angle.to_radians();
if let Some(transform) = self.transform {
self.transform = Some(transform.then_rotate(Angle::radians(radians)));
} else {
self.transform = Some(Transform::rotation(Angle::radians(radians)));
}
}
/// Builds into a [`Path`].
#[inline]
pub fn build(self) -> Result<Path<Pixels>, Error> {
let path = if let Some(transform) = self.transform {
self.raw.build().transformed(&transform)
} else {
self.raw.build()
};
match self.style {
PathStyle::Stroke(options) => Self::tessellate_stroke(&path, &options),
PathStyle::Fill(options) => Self::tessellate_fill(&path, &options),
}
}
fn tessellate_fill(
path: &lyon::path::Path,
options: &FillOptions,
) -> Result<Path<Pixels>, Error> {
// Will contain the result of the tessellation.
let mut buf: VertexBuffers<lyon::math::Point, u16> = VertexBuffers::new();
let mut tessellator = FillTessellator::new();
// Compute the tessellation.
tessellator.tessellate_path(
path,
options,
&mut BuffersBuilder::new(&mut buf, |vertex: FillVertex| vertex.position()),
)?;
Ok(Self::build_path(buf))
}
fn tessellate_stroke(
path: &lyon::path::Path,
options: &StrokeOptions,
) -> Result<Path<Pixels>, Error> {
// Will contain the result of the tessellation.
let mut buf: VertexBuffers<lyon::math::Point, u16> = VertexBuffers::new();
let mut tessellator = StrokeTessellator::new();
// Compute the tessellation.
tessellator.tessellate_path(
path,
options,
&mut BuffersBuilder::new(&mut buf, |vertex: StrokeVertex| vertex.position()),
)?;
Ok(Self::build_path(buf))
}
/// Builds a [`Path`] from a [`lyon::VertexBuffers`].
pub fn build_path(buf: VertexBuffers<lyon::math::Point, u16>) -> Path<Pixels> {
if buf.vertices.is_empty() {
return Path::new(Point::default());
}
let first_point = buf.vertices[0];
let mut path = Path::new(first_point.into());
for i in 0..buf.indices.len() / 3 {
let i0 = buf.indices[i * 3] as usize;
let i1 = buf.indices[i * 3 + 1] as usize;
let i2 = buf.indices[i * 3 + 2] as usize;
let v0 = buf.vertices[i0];
let v1 = buf.vertices[i1];
let v2 = buf.vertices[i2];
path.push_triangle(
(v0.into(), v1.into(), v2.into()),
(point(0., 1.), point(0., 1.), point(0., 1.)),
);
}
path
}
}

View File

@@ -792,6 +792,14 @@ impl PlatformInputHandler {
cx,
)
}
#[allow(unused)]
pub fn character_index_for_point(&mut self, point: Point<Pixels>) -> Option<usize> {
self.cx
.update(|window, cx| self.handler.character_index_for_point(point, window, cx))
.ok()
.flatten()
}
}
/// A struct representing a selection in a text buffer, in UTF16 characters.
@@ -882,6 +890,16 @@ pub trait InputHandler: 'static {
cx: &mut App,
) -> Option<Bounds<Pixels>>;
/// Get the character offset for the given point in terms of UTF16 characters
///
/// Corresponds to [characterIndexForPoint:](https://developer.apple.com/documentation/appkit/nstextinputclient/characterindex(for:))
fn character_index_for_point(
&mut self,
point: Point<Pixels>,
window: &mut Window,
cx: &mut App,
) -> Option<usize>;
/// Allows a given input context to opt into getting raw key repeats instead of
/// sending these to the platform.
/// TODO: Ideally we should be able to set ApplePressAndHoldEnabled in NSUserDefaults

View File

@@ -27,7 +27,6 @@ struct BladeAtlasState {
tiles_by_key: FxHashMap<AtlasKey, AtlasTile>,
initializations: Vec<AtlasTextureId>,
uploads: Vec<PendingUpload>,
path_sample_count: u32,
}
#[cfg(gles)]
@@ -43,11 +42,10 @@ impl BladeAtlasState {
pub struct BladeTextureInfo {
pub size: gpu::Extent,
pub raw_view: gpu::TextureView,
pub msaa_view: Option<gpu::TextureView>,
}
impl BladeAtlas {
pub(crate) fn new(gpu: &Arc<gpu::Context>, path_sample_count: u32) -> Self {
pub(crate) fn new(gpu: &Arc<gpu::Context>) -> Self {
BladeAtlas(Mutex::new(BladeAtlasState {
gpu: Arc::clone(gpu),
upload_belt: BufferBelt::new(BufferBeltDescriptor {
@@ -59,7 +57,6 @@ impl BladeAtlas {
tiles_by_key: Default::default(),
initializations: Vec::new(),
uploads: Vec::new(),
path_sample_count,
}))
}
@@ -109,7 +106,6 @@ impl BladeAtlas {
depth: 1,
},
raw_view: texture.raw_view,
msaa_view: texture.msaa_view,
}
}
}
@@ -208,39 +204,6 @@ impl BladeAtlasState {
}
}
// We currently only enable MSAA for path textures.
let (msaa, msaa_view) = if self.path_sample_count > 1 && kind == AtlasTextureKind::Path {
let msaa = self.gpu.create_texture(gpu::TextureDesc {
name: "msaa path texture",
format,
size: gpu::Extent {
width: size.width.into(),
height: size.height.into(),
depth: 1,
},
array_layer_count: 1,
mip_level_count: 1,
sample_count: self.path_sample_count,
dimension: gpu::TextureDimension::D2,
usage: gpu::TextureUsage::TARGET,
});
(
Some(msaa),
Some(self.gpu.create_texture_view(
msaa,
gpu::TextureViewDesc {
name: "msaa texture view",
format,
dimension: gpu::ViewDimension::D2,
subresources: &Default::default(),
},
)),
)
} else {
(None, None)
};
let raw = self.gpu.create_texture(gpu::TextureDesc {
name: "atlas",
format,
@@ -277,8 +240,6 @@ impl BladeAtlasState {
format,
raw,
raw_view,
msaa,
msaa_view,
live_atlas_keys: 0,
};
@@ -393,8 +354,6 @@ struct BladeAtlasTexture {
allocator: BucketedAtlasAllocator,
raw: gpu::Texture,
raw_view: gpu::TextureView,
msaa: Option<gpu::Texture>,
msaa_view: Option<gpu::TextureView>,
format: gpu::TextureFormat,
live_atlas_keys: u32,
}
@@ -422,12 +381,6 @@ impl BladeAtlasTexture {
fn destroy(&mut self, gpu: &gpu::Context) {
gpu.destroy_texture(self.raw);
gpu.destroy_texture_view(self.raw_view);
if let Some(msaa) = self.msaa {
gpu.destroy_texture(msaa);
}
if let Some(msaa_view) = self.msaa_view {
gpu.destroy_texture_view(msaa_view);
}
}
fn bytes_per_pixel(&self) -> u8 {

View File

@@ -7,18 +7,16 @@ use crate::{
MonochromeSprite, Path, PathId, PathVertex, PolychromeSprite, PrimitiveBatch, Quad,
ScaledPixels, Scene, Shadow, Size, Underline,
};
use blade_graphics as gpu;
use blade_util::{BufferBelt, BufferBeltDescriptor};
use bytemuck::{Pod, Zeroable};
use collections::HashMap;
#[cfg(target_os = "macos")]
use media::core_video::CVMetalTextureCache;
use blade_graphics as gpu;
use blade_util::{BufferBelt, BufferBeltDescriptor};
use std::{mem, sync::Arc};
const MAX_FRAME_TIME_MS: u32 = 10000;
// Use 4x MSAA, all devices support it.
// https://developer.apple.com/documentation/metal/mtldevice/1433355-supportstexturesamplecount
const PATH_SAMPLE_COUNT: u32 = 4;
#[repr(C)]
#[derive(Clone, Copy, Pod, Zeroable)]
@@ -210,10 +208,7 @@ impl BladePipelines {
blend: Some(gpu::BlendState::ADDITIVE),
write_mask: gpu::ColorWrites::default(),
}],
multisample_state: gpu::MultisampleState {
sample_count: PATH_SAMPLE_COUNT,
..Default::default()
},
multisample_state: gpu::MultisampleState::default(),
}),
paths: gpu.create_render_pipeline(gpu::RenderPipelineDesc {
name: "paths",
@@ -353,7 +348,7 @@ impl BladeRenderer {
min_chunk_size: 0x1000,
alignment: 0x40, // Vulkan `minStorageBufferOffsetAlignment` on Intel Xe
});
let atlas = Arc::new(BladeAtlas::new(&context.gpu, PATH_SAMPLE_COUNT));
let atlas = Arc::new(BladeAtlas::new(&context.gpu));
let atlas_sampler = context.gpu.create_sampler(gpu::SamplerDesc {
name: "atlas",
mag_filter: gpu::FilterMode::Linear,
@@ -502,38 +497,27 @@ impl BladeRenderer {
};
let vertex_buf = unsafe { self.instance_belt.alloc_typed(&vertices, &self.gpu) };
let frame_view = tex_info.raw_view;
let color_target = if let Some(msaa_view) = tex_info.msaa_view {
gpu::RenderTarget {
view: msaa_view,
init_op: gpu::InitOp::Clear(gpu::TextureColor::OpaqueBlack),
finish_op: gpu::FinishOp::ResolveTo(frame_view),
}
} else {
gpu::RenderTarget {
view: frame_view,
init_op: gpu::InitOp::Clear(gpu::TextureColor::OpaqueBlack),
finish_op: gpu::FinishOp::Store,
}
};
if let mut pass = self.command_encoder.render(
let mut pass = self.command_encoder.render(
"paths",
gpu::RenderTargetSet {
colors: &[color_target],
colors: &[gpu::RenderTarget {
view: tex_info.raw_view,
init_op: gpu::InitOp::Clear(gpu::TextureColor::OpaqueBlack),
finish_op: gpu::FinishOp::Store,
}],
depth_stencil: None,
},
) {
let mut encoder = pass.with(&self.pipelines.path_rasterization);
encoder.bind(
0,
&ShaderPathRasterizationData {
globals,
b_path_vertices: vertex_buf,
},
);
encoder.draw(0, vertices.len() as u32, 0, 1);
}
);
let mut encoder = pass.with(&self.pipelines.path_rasterization);
encoder.bind(
0,
&ShaderPathRasterizationData {
globals,
b_path_vertices: vertex_buf,
},
);
encoder.draw(0, vertices.len() as u32, 0, 1);
}
}

View File

@@ -13,14 +13,13 @@ use std::borrow::Cow;
pub(crate) struct MetalAtlas(Mutex<MetalAtlasState>);
impl MetalAtlas {
pub(crate) fn new(device: Device, path_sample_count: u32) -> Self {
pub(crate) fn new(device: Device) -> Self {
MetalAtlas(Mutex::new(MetalAtlasState {
device: AssertSend(device),
monochrome_textures: Default::default(),
polychrome_textures: Default::default(),
path_textures: Default::default(),
tiles_by_key: Default::default(),
path_sample_count,
}))
}
@@ -28,10 +27,6 @@ impl MetalAtlas {
self.0.lock().texture(id).metal_texture.clone()
}
pub(crate) fn msaa_texture(&self, id: AtlasTextureId) -> Option<metal::Texture> {
self.0.lock().texture(id).msaa_texture.clone()
}
pub(crate) fn allocate(
&self,
size: Size<DevicePixels>,
@@ -59,7 +54,6 @@ struct MetalAtlasState {
polychrome_textures: AtlasTextureList<MetalAtlasTexture>,
path_textures: AtlasTextureList<MetalAtlasTexture>,
tiles_by_key: FxHashMap<AtlasKey, AtlasTile>,
path_sample_count: u32,
}
impl PlatformAtlas for MetalAtlas {
@@ -182,18 +176,6 @@ impl MetalAtlasState {
texture_descriptor.set_usage(usage);
let metal_texture = self.device.new_texture(&texture_descriptor);
// We currently only enable MSAA for path textures.
let msaa_texture = if self.path_sample_count > 1 && kind == AtlasTextureKind::Path {
let mut descriptor = texture_descriptor.clone();
descriptor.set_texture_type(metal::MTLTextureType::D2Multisample);
descriptor.set_storage_mode(metal::MTLStorageMode::Private);
descriptor.set_sample_count(self.path_sample_count as _);
let msaa_texture = self.device.new_texture(&descriptor);
Some(msaa_texture)
} else {
None
};
let texture_list = match kind {
AtlasTextureKind::Monochrome => &mut self.monochrome_textures,
AtlasTextureKind::Polychrome => &mut self.polychrome_textures,
@@ -209,7 +191,6 @@ impl MetalAtlasState {
},
allocator: etagere::BucketedAtlasAllocator::new(size.into()),
metal_texture: AssertSend(metal_texture),
msaa_texture: AssertSend(msaa_texture),
live_atlas_keys: 0,
};
@@ -236,7 +217,6 @@ struct MetalAtlasTexture {
id: AtlasTextureId,
allocator: BucketedAtlasAllocator,
metal_texture: AssertSend<metal::Texture>,
msaa_texture: AssertSend<Option<metal::Texture>>,
live_atlas_keys: u32,
}

View File

@@ -28,9 +28,6 @@ pub(crate) type PointF = crate::Point<f32>;
const SHADERS_METALLIB: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/shaders.metallib"));
#[cfg(feature = "runtime_shaders")]
const SHADERS_SOURCE_FILE: &str = include_str!(concat!(env!("OUT_DIR"), "/stitched_shaders.metal"));
// Use 4x MSAA, all devices support it.
// https://developer.apple.com/documentation/metal/mtldevice/1433355-supportstexturesamplecount
const PATH_SAMPLE_COUNT: u32 = 4;
pub type Context = Arc<Mutex<InstanceBufferPool>>;
pub type Renderer = MetalRenderer;
@@ -173,7 +170,6 @@ impl MetalRenderer {
"path_rasterization_vertex",
"path_rasterization_fragment",
MTLPixelFormat::R16Float,
PATH_SAMPLE_COUNT,
);
let path_sprites_pipeline_state = build_pipeline_state(
&device,
@@ -233,7 +229,7 @@ impl MetalRenderer {
);
let command_queue = device.new_command_queue();
let sprite_atlas = Arc::new(MetalAtlas::new(device.clone(), PATH_SAMPLE_COUNT));
let sprite_atlas = Arc::new(MetalAtlas::new(device.clone()));
let core_video_texture_cache =
unsafe { CVMetalTextureCache::new(device.as_ptr()).unwrap() };
@@ -535,20 +531,10 @@ impl MetalRenderer {
.unwrap();
let texture = self.sprite_atlas.metal_texture(texture_id);
let msaa_texture = self.sprite_atlas.msaa_texture(texture_id);
if let Some(msaa_texture) = msaa_texture {
color_attachment.set_texture(Some(&msaa_texture));
color_attachment.set_resolve_texture(Some(&texture));
color_attachment.set_load_action(metal::MTLLoadAction::Clear);
color_attachment.set_store_action(metal::MTLStoreAction::MultisampleResolve);
} else {
color_attachment.set_texture(Some(&texture));
color_attachment.set_load_action(metal::MTLLoadAction::Clear);
color_attachment.set_store_action(metal::MTLStoreAction::Store);
}
color_attachment.set_texture(Some(&texture));
color_attachment.set_load_action(metal::MTLLoadAction::Clear);
color_attachment.set_store_action(metal::MTLStoreAction::Store);
color_attachment.set_clear_color(metal::MTLClearColor::new(0., 0., 0., 1.));
let command_encoder = command_buffer.new_render_command_encoder(render_pass_descriptor);
command_encoder.set_render_pipeline_state(&self.paths_rasterization_pipeline_state);
command_encoder.set_vertex_buffer(
@@ -1174,7 +1160,6 @@ fn build_path_rasterization_pipeline_state(
vertex_fn_name: &str,
fragment_fn_name: &str,
pixel_format: metal::MTLPixelFormat,
path_sample_count: u32,
) -> metal::RenderPipelineState {
let vertex_fn = library
.get_function(vertex_fn_name, None)
@@ -1187,10 +1172,6 @@ fn build_path_rasterization_pipeline_state(
descriptor.set_label(label);
descriptor.set_vertex_function(Some(vertex_fn.as_ref()));
descriptor.set_fragment_function(Some(fragment_fn.as_ref()));
if path_sample_count > 1 {
descriptor.set_raster_sample_count(path_sample_count as _);
descriptor.set_alpha_to_coverage_enabled(true);
}
let color_attachment = descriptor.color_attachments().object_at(0).unwrap();
color_attachment.set_pixel_format(pixel_format);
color_attachment.set_blending_enabled(true);

View File

@@ -17,8 +17,8 @@ use cocoa::{
},
base::{id, nil},
foundation::{
NSArray, NSAutoreleasePool, NSDictionary, NSFastEnumeration, NSInteger, NSPoint, NSRect,
NSSize, NSString, NSUInteger,
NSArray, NSAutoreleasePool, NSDictionary, NSFastEnumeration, NSInteger, NSNotFound,
NSPoint, NSRect, NSSize, NSString, NSUInteger,
},
};
use core_graphics::display::{CGDirectDisplayID, CGPoint, CGRect};
@@ -227,6 +227,11 @@ unsafe fn build_classes() {
accepts_first_mouse as extern "C" fn(&Object, Sel, id) -> BOOL,
);
decl.add_method(
sel!(characterIndexForPoint:),
character_index_for_point as extern "C" fn(&Object, Sel, NSPoint) -> u64,
);
decl.register()
};
}
@@ -1687,17 +1692,7 @@ extern "C" fn first_rect_for_character_range(
range: NSRange,
_: id,
) -> NSRect {
let frame: NSRect = unsafe {
let state = get_window_state(this);
let lock = state.lock();
let mut frame = NSWindow::frame(lock.native_window);
let content_layout_rect: CGRect = msg_send![lock.native_window, contentLayoutRect];
let style_mask: NSWindowStyleMask = msg_send![lock.native_window, styleMask];
if !style_mask.contains(NSWindowStyleMask::NSFullSizeContentViewWindowMask) {
frame.origin.y -= frame.size.height - content_layout_rect.size.height;
}
frame
};
let frame = get_frame(this);
with_input_handler(this, |input_handler| {
input_handler.bounds_for_range(range.to_range()?)
})
@@ -1718,6 +1713,20 @@ extern "C" fn first_rect_for_character_range(
)
}
fn get_frame(this: &Object) -> NSRect {
unsafe {
let state = get_window_state(this);
let lock = state.lock();
let mut frame = NSWindow::frame(lock.native_window);
let content_layout_rect: CGRect = msg_send![lock.native_window, contentLayoutRect];
let style_mask: NSWindowStyleMask = msg_send![lock.native_window, styleMask];
if !style_mask.contains(NSWindowStyleMask::NSFullSizeContentViewWindowMask) {
frame.origin.y -= frame.size.height - content_layout_rect.size.height;
}
frame
}
}
extern "C" fn insert_text(this: &Object, _: Sel, text: id, replacement_range: NSRange) {
unsafe {
let is_attributed_string: BOOL =
@@ -1831,6 +1840,24 @@ extern "C" fn accepts_first_mouse(this: &Object, _: Sel, _: id) -> BOOL {
YES
}
extern "C" fn character_index_for_point(this: &Object, _: Sel, position: NSPoint) -> u64 {
let position = screen_point_to_gpui_point(this, position);
with_input_handler(this, |input_handler| {
input_handler.character_index_for_point(position)
})
.flatten()
.map(|index| index as u64)
.unwrap_or(NSNotFound as u64)
}
fn screen_point_to_gpui_point(this: &Object, position: NSPoint) -> Point<Pixels> {
let frame = get_frame(this);
let window_x = position.x - frame.origin.x;
let window_y = frame.size.height - (position.y - frame.origin.y);
let position = point(px(window_x as f32), px(window_y as f32));
position
}
extern "C" fn dragging_entered(this: &Object, _: Sel, dragging_info: id) -> NSDragOperation {
let window_state = unsafe { get_window_state(this) };
let position = drag_event_position(&window_state, dragging_info);

View File

@@ -715,13 +715,6 @@ impl Path<Pixels> {
}
}
/// Move the start, current point to the given point.
pub fn move_to(&mut self, to: Point<Pixels>) {
self.contour_count += 1;
self.start = to;
self.current = to;
}
/// Draw a straight line from the current point to the given point.
pub fn line_to(&mut self, to: Point<Pixels>) {
self.contour_count += 1;
@@ -751,8 +744,7 @@ impl Path<Pixels> {
self.current = to;
}
/// Push a triangle to the Path.
pub fn push_triangle(
fn push_triangle(
&mut self,
xy: (Point<Pixels>, Point<Pixels>, Point<Pixels>),
st: (Point<f32>, Point<f32>, Point<f32>),

View File

@@ -14,6 +14,7 @@ doctest = false
[dependencies]
anyhow.workspace = true
client.workspace = true
copilot.workspace = true
editor.workspace = true
feature_flags.workspace = true
@@ -22,13 +23,14 @@ gpui.workspace = true
inline_completion.workspace = true
language.workspace = true
paths.workspace = true
regex.workspace = true
settings.workspace = true
supermaven.workspace = true
telemetry.workspace = true
ui.workspace = true
workspace.workspace = true
zed_actions.workspace = true
zeta.workspace = true
client.workspace = true
[dev-dependencies]
copilot = { workspace = true, features = ["test-support"] }

View File

@@ -17,8 +17,12 @@ use language::{
},
File, Language,
};
use regex::Regex;
use settings::{update_settings_file, Settings, SettingsStore};
use std::{path::Path, sync::Arc, time::Duration};
use std::{
sync::{Arc, LazyLock},
time::Duration,
};
use supermaven::{AccountStatus, Supermaven};
use ui::{
prelude::*, Clickable, ContextMenu, ContextMenuEntry, IconButton, IconButtonShape, PopoverMenu,
@@ -71,9 +75,7 @@ impl Render for InlineCompletionButton {
};
let status = copilot.read(cx).status();
let enabled = self.editor_enabled.unwrap_or_else(|| {
all_language_settings.inline_completions_enabled(None, None, cx)
});
let enabled = self.editor_enabled.unwrap_or(false);
let icon = match status {
Status::Error(_) => IconName::CopilotError,
@@ -228,25 +230,35 @@ impl Render for InlineCompletionButton {
return div();
}
fn icon_button() -> IconButton {
IconButton::new("zed-predict-pending-button", IconName::ZedPredict)
.shape(IconButtonShape::Square)
}
let enabled = self.editor_enabled.unwrap_or(false);
let zeta_icon = if enabled {
IconName::ZedPredict
} else {
IconName::ZedPredictDisabled
};
let current_user_terms_accepted =
self.user_store.read(cx).current_user_has_accepted_terms();
if !current_user_terms_accepted.unwrap_or(false) {
let signed_in = current_user_terms_accepted.is_some();
let tooltip_meta = if signed_in {
"Read Terms of Service"
} else {
"Sign in to use"
};
let icon_button = || {
let base = IconButton::new("zed-predict-pending-button", zeta_icon)
.shape(IconButtonShape::Square);
return div().child(
icon_button()
.tooltip(move |window, cx| {
match (
current_user_terms_accepted,
self.popover_menu_handle.is_deployed(),
enabled,
) {
(Some(false) | None, _, _) => {
let signed_in = current_user_terms_accepted.is_some();
let tooltip_meta = if signed_in {
"Read Terms of Service"
} else {
"Sign in to use"
};
base.tooltip(move |window, cx| {
Tooltip::with_meta(
"Edit Predictions",
None,
@@ -255,23 +267,37 @@ impl Render for InlineCompletionButton {
cx,
)
})
.on_click(cx.listener(move |_, _, window, cx| {
window.dispatch_action(
zed_actions::OpenZedPredictOnboarding.boxed_clone(),
cx,
);
})),
);
}
.on_click(cx.listener(
move |_, _, window, cx| {
telemetry::event!(
"Pending ToS Clicked",
source = "Edit Prediction Status Button"
);
window.dispatch_action(
zed_actions::OpenZedPredictOnboarding.boxed_clone(),
cx,
);
},
))
}
(Some(true), true, _) => base,
(Some(true), false, true) => base.tooltip(|window, cx| {
Tooltip::for_action("Edit Prediction", &ToggleMenu, window, cx)
}),
(Some(true), false, false) => base.tooltip(|window, cx| {
Tooltip::with_meta(
"Edit Prediction",
Some(&ToggleMenu),
"Disabled For This File",
window,
cx,
)
}),
}
};
let this = cx.entity().clone();
if !self.popover_menu_handle.is_deployed() {
icon_button().tooltip(|window, cx| {
Tooltip::for_action("Edit Prediction", &ToggleMenu, window, cx)
});
}
let mut popover_menu = PopoverMenu::new("zeta")
.menu(move |window, cx| {
Some(this.update(cx, |this, cx| this.build_zeta_context_menu(window, cx)))
@@ -358,15 +384,10 @@ impl InlineCompletionButton {
})
}
// Predict Edits at Cursor alt-tab
// Automatically Predict:
// ✓ PATH
// ✓ Rust
// ✓ All Files
pub fn build_language_settings_menu(&self, mut menu: ContextMenu, cx: &mut App) -> ContextMenu {
let fs = self.fs.clone();
menu = menu.header("Predict Edits For:");
menu = menu.header("Show Predict Edits For");
if let Some(language) = self.language.clone() {
let fs = fs.clone();
@@ -377,69 +398,87 @@ impl InlineCompletionButton {
menu = menu.toggleable_entry(
language.name(),
language_enabled,
IconPosition::Start,
IconPosition::End,
None,
move |_, cx| {
toggle_inline_completions_for_language(language.clone(), fs.clone(), cx)
toggle_show_inline_completions_for_language(language.clone(), fs.clone(), cx)
},
);
}
let settings = AllLanguageSettings::get_global(cx);
if let Some(file) = &self.file {
let path = file.path().clone();
let path_enabled = settings.inline_completions_enabled_for_path(&path);
let globally_enabled = settings.show_inline_completions(None, cx);
menu = menu.toggleable_entry(
"All Files",
globally_enabled,
IconPosition::End,
None,
move |_, cx| toggle_inline_completions_globally(fs.clone(), cx),
);
menu = menu.separator().header("Privacy Settings");
menu = menu.toggleable_entry(
"This File",
path_enabled,
IconPosition::Start,
None,
move |window, cx| {
if let Some(provider) = &self.inline_completion_provider {
let data_collection = provider.data_collection_state(cx);
if data_collection.is_supported() {
let provider = provider.clone();
let enabled = data_collection.is_enabled();
menu = menu.item(
// TODO: We want to add something later that communicates whether
// the current project is open-source.
ContextMenuEntry::new("Share Training Data")
.toggleable(IconPosition::End, data_collection.is_enabled())
.documentation_aside(|_| {
Label::new("Zed automatically detects if your project is open-source. This setting is only applicable in such cases.").into_any_element()
})
.handler(move |_, cx| {
provider.toggle_data_collection(cx);
if !enabled {
telemetry::event!(
"Data Collection Enabled",
source = "Edit Prediction Status Menu"
);
} else {
telemetry::event!(
"Data Collection Disabled",
source = "Edit Prediction Status Menu"
);
}
})
)
}
}
menu = menu.item(
ContextMenuEntry::new("Exclude Files")
.documentation_aside(|_| {
Label::new("This item takes you to the settings where you can specify files that will never be captured by any edit prediction model. You can list both specific file extensions and individual file names.").into_any_element()
})
.handler(move |window, cx| {
if let Some(workspace) = window.root().flatten() {
let workspace = workspace.downgrade();
window
.spawn(cx, |cx| {
configure_disabled_globs(
open_disabled_globs_setting_in_editor(
workspace,
path_enabled.then_some(path.clone()),
cx,
)
})
.detach_and_log_err(cx);
}
},
);
}
let globally_enabled = settings.inline_completions_enabled(None, None, cx);
menu = menu.toggleable_entry(
"All Files",
globally_enabled,
IconPosition::Start,
None,
move |_, cx| toggle_inline_completions_globally(fs.clone(), cx),
}),
);
if let Some(provider) = &self.inline_completion_provider {
let data_collection = provider.data_collection_state(cx);
if data_collection.is_supported() {
let provider = provider.clone();
menu = menu
.separator()
.header("Help Improve The Model")
.header("Valid Only For OSS Projects");
menu = menu.item(
// TODO: We want to add something later that communicates whether
// the current project is open-source.
ContextMenuEntry::new("Share Training Data")
.toggleable(IconPosition::Start, data_collection.is_enabled())
.handler(move |_, cx| {
provider.toggle_data_collection(cx);
}),
);
}
if self.file.as_ref().map_or(false, |file| {
!all_language_settings(Some(file), cx).inline_completions_enabled_for_path(file.path())
}) {
menu = menu.item(
ContextMenuEntry::new("This file is excluded.")
.disabled(true)
.icon(IconName::ZedPredictDisabled)
.icon_size(IconSize::Small),
);
}
if let Some(editor_focus_handle) = self.editor_focus_handle.clone() {
@@ -528,12 +567,11 @@ impl InlineCompletionButton {
self.editor_enabled = {
let file = file.as_ref();
Some(
file.map(|file| !file.is_private()).unwrap_or(true)
&& all_language_settings(file, cx).inline_completions_enabled(
language,
file.map(|file| file.path().as_ref()),
cx,
),
file.map(|file| {
all_language_settings(Some(file), cx)
.inline_completions_enabled_for_path(file.path())
})
.unwrap_or(true),
)
};
self.inline_completion_provider = editor.inline_completion_provider();
@@ -598,9 +636,8 @@ impl SupermavenButtonStatus {
}
}
async fn configure_disabled_globs(
async fn open_disabled_globs_setting_in_editor(
workspace: WeakEntity<Workspace>,
path_to_disable: Option<Arc<Path>>,
mut cx: AsyncWindowContext,
) -> Result<()> {
let settings_editor = workspace
@@ -619,34 +656,34 @@ async fn configure_disabled_globs(
let text = item.buffer().read(cx).snapshot(cx).text();
let settings = cx.global::<SettingsStore>();
let edits = settings.edits_for_update::<AllLanguageSettings>(&text, |file| {
let copilot = file.inline_completions.get_or_insert_with(Default::default);
let globs = copilot.disabled_globs.get_or_insert_with(|| {
settings
.get::<AllLanguageSettings>(None)
.inline_completions
.disabled_globs
.iter()
.map(|glob| glob.glob().to_string())
.collect()
});
if let Some(path_to_disable) = &path_to_disable {
globs.push(path_to_disable.to_string_lossy().into_owned());
} else {
globs.clear();
}
// Ensure that we always have "inline_completions { "disabled_globs": [] }"
let edits = settings.edits_for_update::<AllLanguageSettings>(&text, |file| {
file.inline_completions
.get_or_insert_with(Default::default)
.disabled_globs
.get_or_insert_with(Vec::new);
});
if !edits.is_empty() {
item.change_selections(Some(Autoscroll::newest()), window, cx, |selections| {
selections.select_ranges(edits.iter().map(|e| e.0.clone()));
});
item.edit(edits.iter().cloned(), cx);
}
// When *enabling* a path, don't actually perform an edit, just select the range.
if path_to_disable.is_some() {
item.edit(edits.iter().cloned(), cx);
}
let text = item.buffer().read(cx).snapshot(cx).text();
static DISABLED_GLOBS_REGEX: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r#""disabled_globs":\s*\[\s*(?P<content>(?:.|\n)*?)\s*\]"#).unwrap()
});
// Only capture [...]
let range = DISABLED_GLOBS_REGEX.captures(&text).and_then(|captures| {
captures
.name("content")
.map(|inner_match| inner_match.start()..inner_match.end())
});
if let Some(range) = range {
item.change_selections(Some(Autoscroll::newest()), window, cx, |selections| {
selections.select_ranges(vec![range]);
});
}
})?;
@@ -654,8 +691,7 @@ async fn configure_disabled_globs(
}
fn toggle_inline_completions_globally(fs: Arc<dyn Fs>, cx: &mut App) {
let show_inline_completions =
all_language_settings(None, cx).inline_completions_enabled(None, None, cx);
let show_inline_completions = all_language_settings(None, cx).show_inline_completions(None, cx);
update_settings_file::<AllLanguageSettings>(fs, cx, move |file, _| {
file.defaults.show_inline_completions = Some(!show_inline_completions)
});
@@ -669,9 +705,13 @@ fn set_completion_provider(fs: Arc<dyn Fs>, cx: &mut App, provider: InlineComple
});
}
fn toggle_inline_completions_for_language(language: Arc<Language>, fs: Arc<dyn Fs>, cx: &mut App) {
fn toggle_show_inline_completions_for_language(
language: Arc<Language>,
fs: Arc<dyn Fs>,
cx: &mut App,
) {
let show_inline_completions =
all_language_settings(None, cx).inline_completions_enabled(Some(&language), None, cx);
all_language_settings(None, cx).show_inline_completions(Some(&language), cx);
update_settings_file::<AllLanguageSettings>(fs, cx, move |file, _| {
file.languages
.entry(language.name())

View File

@@ -886,18 +886,7 @@ impl AllLanguageSettings {
}
/// Returns whether edit predictions are enabled for the given language and path.
pub fn inline_completions_enabled(
&self,
language: Option<&Arc<Language>>,
path: Option<&Path>,
cx: &App,
) -> bool {
if let Some(path) = path {
if !self.inline_completions_enabled_for_path(path) {
return false;
}
}
pub fn show_inline_completions(&self, language: Option<&Arc<Language>>, cx: &App) -> bool {
self.language(None, language.map(|l| l.name()).as_ref(), cx)
.show_inline_completions
}

View File

@@ -3,7 +3,7 @@ use anyhow::Result;
use futures::StreamExt as _;
use gpui::{App, Context, Entity, EntityId, Task};
use inline_completion::{Direction, InlineCompletion, InlineCompletionProvider};
use language::{language_settings::all_language_settings, Anchor, Buffer, BufferSnapshot};
use language::{Anchor, Buffer, BufferSnapshot};
use std::{
ops::{AddAssign, Range},
path::Path,
@@ -113,16 +113,8 @@ impl InlineCompletionProvider for SupermavenCompletionProvider {
false
}
fn is_enabled(&self, buffer: &Entity<Buffer>, cursor_position: Anchor, cx: &App) -> bool {
if !self.supermaven.read(cx).is_enabled() {
return false;
}
let buffer = buffer.read(cx);
let file = buffer.file();
let language = buffer.language_at(cursor_position);
let settings = all_language_settings(file, cx);
settings.inline_completions_enabled(language.as_ref(), file.map(|f| f.path().as_ref()), cx)
fn is_enabled(&self, _buffer: &Entity<Buffer>, _cursor_position: Anchor, cx: &App) -> bool {
self.supermaven.read(cx).is_enabled()
}
fn is_refreshing(&self) -> bool {

View File

@@ -1073,6 +1073,15 @@ impl InputHandler for TerminalInputHandler {
fn apple_press_and_hold_enabled(&mut self) -> bool {
false
}
fn character_index_for_point(
&mut self,
_point: Point<Pixels>,
_window: &mut Window,
_cx: &mut App,
) -> Option<usize> {
None
}
}
pub fn is_blank(cell: &IndexedCell) -> bool {

View File

@@ -47,6 +47,7 @@ pub struct ContextMenuEntry {
handler: Rc<dyn Fn(Option<&FocusHandle>, &mut Window, &mut App)>,
action: Option<Box<dyn Action>>,
disabled: bool,
documentation_aside: Option<Rc<dyn Fn(&mut App) -> AnyElement>>,
}
impl ContextMenuEntry {
@@ -61,6 +62,7 @@ impl ContextMenuEntry {
handler: Rc::new(|_, _, _| {}),
action: None,
disabled: false,
documentation_aside: None,
}
}
@@ -108,6 +110,14 @@ impl ContextMenuEntry {
self.disabled = disabled;
self
}
pub fn documentation_aside(
mut self,
element: impl Fn(&mut App) -> AnyElement + 'static,
) -> Self {
self.documentation_aside = Some(Rc::new(element));
self
}
}
impl From<ContextMenuEntry> for ContextMenuItem {
@@ -125,6 +135,7 @@ pub struct ContextMenu {
clicked: bool,
_on_blur_subscription: Subscription,
keep_open_on_confirm: bool,
documentation_aside: Option<(usize, Rc<dyn Fn(&mut App) -> AnyElement>)>,
}
impl Focusable for ContextMenu {
@@ -161,6 +172,7 @@ impl ContextMenu {
clicked: false,
_on_blur_subscription,
keep_open_on_confirm: false,
documentation_aside: None,
},
window,
cx,
@@ -209,6 +221,7 @@ impl ContextMenu {
icon_color: None,
action,
disabled: false,
documentation_aside: None,
}));
self
}
@@ -231,6 +244,7 @@ impl ContextMenu {
icon_color: None,
action,
disabled: false,
documentation_aside: None,
}));
self
}
@@ -281,6 +295,7 @@ impl ContextMenu {
icon_size: IconSize::Small,
icon_color: None,
disabled: false,
documentation_aside: None,
}));
self
}
@@ -294,7 +309,6 @@ impl ContextMenu {
toggle: None,
label: label.into(),
action: Some(action.boxed_clone()),
handler: Rc::new(move |context, window, cx| {
if let Some(context) = &context {
window.focus(context);
@@ -306,6 +320,7 @@ impl ContextMenu {
icon_position: IconPosition::End,
icon_color: None,
disabled: true,
documentation_aside: None,
}));
self
}
@@ -314,7 +329,6 @@ impl ContextMenu {
self.items.push(ContextMenuItem::Entry(ContextMenuEntry {
toggle: None,
label: label.into(),
action: Some(action.boxed_clone()),
handler: Rc::new(move |_, window, cx| window.dispatch_action(action.boxed_clone(), cx)),
icon: Some(IconName::ArrowUpRight),
@@ -322,6 +336,7 @@ impl ContextMenu {
icon_position: IconPosition::End,
icon_color: None,
disabled: false,
documentation_aside: None,
}));
self
}
@@ -356,15 +371,16 @@ impl ContextMenu {
}
fn select_first(&mut self, _: &SelectFirst, _: &mut Window, cx: &mut Context<Self>) {
self.selected_index = self.items.iter().position(|item| item.is_selectable());
if let Some(ix) = self.items.iter().position(|item| item.is_selectable()) {
self.select_index(ix);
}
cx.notify();
}
pub fn select_last(&mut self) -> Option<usize> {
for (ix, item) in self.items.iter().enumerate().rev() {
if item.is_selectable() {
self.selected_index = Some(ix);
return Some(ix);
return self.select_index(ix);
}
}
None
@@ -384,7 +400,7 @@ impl ContextMenu {
} else {
for (ix, item) in self.items.iter().enumerate().skip(next_index) {
if item.is_selectable() {
self.selected_index = Some(ix);
self.select_index(ix);
cx.notify();
break;
}
@@ -402,7 +418,7 @@ impl ContextMenu {
} else {
for (ix, item) in self.items.iter().enumerate().take(ix).rev() {
if item.is_selectable() {
self.selected_index = Some(ix);
self.select_index(ix);
cx.notify();
break;
}
@@ -413,6 +429,20 @@ impl ContextMenu {
}
}
fn select_index(&mut self, ix: usize) -> Option<usize> {
self.documentation_aside = None;
let item = self.items.get(ix)?;
if item.is_selectable() {
self.selected_index = Some(ix);
if let ContextMenuItem::Entry(entry) = item {
if let Some(callback) = &entry.documentation_aside {
self.documentation_aside = Some((ix, callback.clone()));
}
}
}
Some(ix)
}
pub fn on_action_dispatch(
&mut self,
dispatched: &dyn Action,
@@ -436,7 +466,7 @@ impl ContextMenu {
false
}
}) {
self.selected_index = Some(ix);
self.select_index(ix);
self.delayed = true;
cx.notify();
let action = dispatched.boxed_clone();
@@ -479,198 +509,275 @@ impl Render for ContextMenu {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let ui_font_size = ThemeSettings::get_global(cx).ui_font_size;
WithRemSize::new(ui_font_size)
.occlude()
.elevation_2(cx)
.flex()
.flex_row()
.child(
v_flex()
.id("context-menu")
.min_w(px(200.))
.max_h(vh(0.75, window))
.flex_1()
.overflow_y_scroll()
.track_focus(&self.focus_handle(cx))
.on_mouse_down_out(
cx.listener(|this, _, window, cx| this.cancel(&menu::Cancel, window, cx)),
)
.key_context("menu")
.on_action(cx.listener(ContextMenu::select_first))
.on_action(cx.listener(ContextMenu::handle_select_last))
.on_action(cx.listener(ContextMenu::select_next))
.on_action(cx.listener(ContextMenu::select_prev))
.on_action(cx.listener(ContextMenu::confirm))
.on_action(cx.listener(ContextMenu::cancel))
.when(!self.delayed, |mut el| {
for item in self.items.iter() {
if let ContextMenuItem::Entry(ContextMenuEntry {
action: Some(action),
disabled: false,
..
}) = item
{
el = el.on_boxed_action(
&**action,
cx.listener(ContextMenu::on_action_dispatch),
);
}
}
el
})
.child(List::new().children(self.items.iter_mut().enumerate().map(
|(ix, item)| {
match item {
ContextMenuItem::Separator => ListSeparator.into_any_element(),
ContextMenuItem::Header(header) => {
ListSubHeader::new(header.clone())
.inset(true)
.into_any_element()
}
ContextMenuItem::Label(label) => ListItem::new(ix)
.inset(true)
.disabled(true)
.child(Label::new(label.clone()))
.into_any_element(),
ContextMenuItem::Entry(ContextMenuEntry {
toggle,
label,
handler,
icon,
icon_position,
icon_size,
icon_color,
action,
disabled,
}) => {
let handler = handler.clone();
let menu = cx.entity().downgrade();
let icon_color = if *disabled {
Color::Muted
} else {
icon_color.unwrap_or(Color::Default)
};
let label_color = if *disabled {
Color::Muted
} else {
Color::Default
};
let label_element = if let Some(icon_name) = icon {
h_flex()
.gap_1p5()
.when(*icon_position == IconPosition::Start, |flex| {
flex.child(
Icon::new(*icon_name)
.size(*icon_size)
.color(icon_color),
)
})
.child(Label::new(label.clone()).color(label_color))
.when(*icon_position == IconPosition::End, |flex| {
flex.child(
Icon::new(*icon_name)
.size(*icon_size)
.color(icon_color),
)
})
.into_any_element()
} else {
Label::new(label.clone())
.color(label_color)
.into_any_element()
};
let aside = self
.documentation_aside
.as_ref()
.map(|(_, callback)| callback.clone());
ListItem::new(ix)
.inset(true)
.disabled(*disabled)
.toggle_state(Some(ix) == self.selected_index)
.when_some(*toggle, |list_item, (position, toggled)| {
let contents = if toggled {
v_flex().flex_none().child(
Icon::new(IconName::Check).color(Color::Accent),
)
h_flex()
.w_full()
.items_start()
.gap_1()
.when_some(aside, |this, aside| {
this.child(
WithRemSize::new(ui_font_size)
.occlude()
.elevation_2(cx)
.p_2()
.max_w_80()
.child(aside(cx)),
)
})
.child(
WithRemSize::new(ui_font_size)
.occlude()
.elevation_2(cx)
.flex()
.flex_row()
.child(
v_flex()
.id("context-menu")
.min_w(px(200.))
.max_h(vh(0.75, window))
.flex_1()
.overflow_y_scroll()
.track_focus(&self.focus_handle(cx))
.on_mouse_down_out(cx.listener(|this, _, window, cx| {
this.cancel(&menu::Cancel, window, cx)
}))
.key_context("menu")
.on_action(cx.listener(ContextMenu::select_first))
.on_action(cx.listener(ContextMenu::handle_select_last))
.on_action(cx.listener(ContextMenu::select_next))
.on_action(cx.listener(ContextMenu::select_prev))
.on_action(cx.listener(ContextMenu::confirm))
.on_action(cx.listener(ContextMenu::cancel))
.when(!self.delayed, |mut el| {
for item in self.items.iter() {
if let ContextMenuItem::Entry(ContextMenuEntry {
action: Some(action),
disabled: false,
..
}) = item
{
el = el.on_boxed_action(
&**action,
cx.listener(ContextMenu::on_action_dispatch),
);
}
}
el
})
.child(List::new().children(self.items.iter_mut().enumerate().map(
|(ix, item)| {
match item {
ContextMenuItem::Separator => {
ListSeparator.into_any_element()
}
ContextMenuItem::Header(header) => {
ListSubHeader::new(header.clone())
.inset(true)
.into_any_element()
}
ContextMenuItem::Label(label) => ListItem::new(ix)
.inset(true)
.disabled(true)
.child(Label::new(label.clone()))
.into_any_element(),
ContextMenuItem::Entry(ContextMenuEntry {
toggle,
label,
handler,
icon,
icon_position,
icon_size,
icon_color,
action,
disabled,
documentation_aside,
}) => {
let handler = handler.clone();
let menu = cx.entity().downgrade();
let icon_color = if *disabled {
Color::Muted
} else {
v_flex()
.flex_none()
.size(IconSize::default().rems())
icon_color.unwrap_or(Color::Default)
};
match position {
IconPosition::Start => {
list_item.start_slot(contents)
}
IconPosition::End => list_item.end_slot(contents),
}
})
.child(
h_flex()
.w_full()
.justify_between()
.child(label_element)
.debug_selector(|| format!("MENU_ITEM-{}", label))
.children(action.as_ref().and_then(|action| {
self.action_context
.as_ref()
.map(|focus| {
KeyBinding::for_action_in(
&**action, focus, window,
let label_color = if *disabled {
Color::Muted
} else {
Color::Default
};
let label_element = if let Some(icon_name) = icon {
h_flex()
.gap_1p5()
.when(
*icon_position == IconPosition::Start,
|flex| {
flex.child(
Icon::new(*icon_name)
.size(*icon_size)
.color(icon_color),
)
})
.unwrap_or_else(|| {
KeyBinding::for_action(
&**action, window,
},
)
.child(
Label::new(label.clone())
.color(label_color),
)
.when(
*icon_position == IconPosition::End,
|flex| {
flex.child(
Icon::new(*icon_name)
.size(*icon_size)
.color(icon_color),
)
})
.map(|binding| div().ml_4().child(binding))
})),
)
.on_click({
let context = self.action_context.clone();
move |_, window, cx| {
handler(context.as_ref(), window, cx);
menu.update(cx, |menu, cx| {
menu.clicked = true;
cx.emit(DismissEvent);
},
)
.into_any_element()
} else {
Label::new(label.clone())
.color(label_color)
.into_any_element()
};
let documentation_aside_callback =
documentation_aside.clone();
div()
.id(("context-menu-child", ix))
.when_some(
documentation_aside_callback,
|this, documentation_aside_callback| {
this.occlude().on_hover(cx.listener(
move |menu, hovered, _, cx| {
if *hovered {
menu.documentation_aside = Some((ix, documentation_aside_callback.clone()));
cx.notify();
} else if matches!(menu.documentation_aside, Some((id, _)) if id == ix) {
menu.documentation_aside = None;
cx.notify();
}
},
))
},
)
.child(
ListItem::new(ix)
.inset(true)
.disabled(*disabled)
.toggle_state(
Some(ix) == self.selected_index,
)
.when_some(
*toggle,
|list_item, (position, toggled)| {
let contents = if toggled {
v_flex().flex_none().child(
Icon::new(IconName::Check)
.color(Color::Accent),
)
} else {
v_flex().flex_none().size(
IconSize::default().rems(),
)
};
match position {
IconPosition::Start => {
list_item
.start_slot(contents)
}
IconPosition::End => {
list_item.end_slot(contents)
}
}
},
)
.child(
h_flex()
.w_full()
.justify_between()
.child(label_element)
.debug_selector(|| {
format!("MENU_ITEM-{}", label)
})
.children(
action.as_ref().and_then(
|action| {
self.action_context
.as_ref()
.map(|focus| {
KeyBinding::for_action_in(
&**action, focus,
window,
)
})
.unwrap_or_else(|| {
KeyBinding::for_action(
&**action, window,
)
})
.map(|binding| {
div().ml_4().child(binding)
})
},
),
),
)
.on_click({
let context =
self.action_context.clone();
move |_, window, cx| {
handler(
context.as_ref(),
window,
cx,
);
menu.update(cx, |menu, cx| {
menu.clicked = true;
cx.emit(DismissEvent);
})
.ok();
}
}),
)
.into_any_element()
}
ContextMenuItem::CustomEntry {
entry_render,
handler,
selectable,
} => {
let handler = handler.clone();
let menu = cx.entity().downgrade();
let selectable = *selectable;
ListItem::new(ix)
.inset(true)
.toggle_state(if selectable {
Some(ix) == self.selected_index
} else {
false
})
.ok();
}
})
.into_any_element()
}
ContextMenuItem::CustomEntry {
entry_render,
handler,
selectable,
} => {
let handler = handler.clone();
let menu = cx.entity().downgrade();
let selectable = *selectable;
ListItem::new(ix)
.inset(true)
.toggle_state(if selectable {
Some(ix) == self.selected_index
} else {
false
})
.selectable(selectable)
.when(selectable, |item| {
item.on_click({
let context = self.action_context.clone();
move |_, window, cx| {
handler(context.as_ref(), window, cx);
menu.update(cx, |menu, cx| {
menu.clicked = true;
cx.emit(DismissEvent);
.selectable(selectable)
.when(selectable, |item| {
item.on_click({
let context = self.action_context.clone();
move |_, window, cx| {
handler(context.as_ref(), window, cx);
menu.update(cx, |menu, cx| {
menu.clicked = true;
cx.emit(DismissEvent);
})
.ok();
}
})
.ok();
}
})
})
.child(entry_render(window, cx))
.into_any_element()
}
}
},
))),
})
.child(entry_render(window, cx))
.into_any_element()
}
}
},
))),
),
)
}
}

View File

@@ -323,6 +323,7 @@ pub enum IconName {
ZedAssistant2,
ZedAssistantFilled,
ZedPredict,
ZedPredictDisabled,
ZedXCopilot,
}

View File

@@ -1289,7 +1289,7 @@ impl Vim {
.map_or(false, |provider| provider.show_completions_in_normal_mode()),
_ => false,
};
editor.set_inline_completions_enabled(enable_inline_completions, cx);
editor.set_show_inline_completions_enabled(enable_inline_completions, cx);
});
cx.notify()
}

View File

@@ -2665,21 +2665,10 @@ impl Snapshot {
/// Get the repository whose work directory contains the given path.
pub fn repository_for_path(&self, path: &Path) -> Option<&RepositoryEntry> {
let mut cursor = self.repositories.cursor::<PathProgress>(&());
let mut repository = None;
// Git repositories may contain other git repositories. As a side effect of
// lexicographic sorting by path, deeper repositories will be after higher repositories
// So, let's loop through every matching repository until we can't find any more to find
// the deepest repository that could contain this path.
while cursor.seek_forward(&PathTarget::Contains(path), Bias::Left, &())
&& cursor.item().is_some()
{
repository = cursor.item();
cursor.next(&());
}
repository
self.repositories
.iter()
.filter(|repo| repo.work_directory.directory_contains(path))
.last()
}
/// Given an ordered iterator of entries, returns an iterator of those entries,
@@ -5965,7 +5954,6 @@ impl<'a> Iterator for Traversal<'a> {
enum PathTarget<'a> {
Path(&'a Path),
Successor(&'a Path),
Contains(&'a Path),
}
impl<'a> PathTarget<'a> {
@@ -5979,13 +5967,6 @@ impl<'a> PathTarget<'a> {
Ordering::Equal
}
}
PathTarget::Contains(path) => {
if path.starts_with(other) {
Ordering::Equal
} else {
Ordering::Greater
}
}
}
}
}

View File

@@ -2,7 +2,7 @@
description = "The fast, collaborative code editor."
edition.workspace = true
name = "zed"
version = "0.173.0"
version = "0.173.2"
publish.workspace = true
license = "GPL-3.0-or-later"
authors = ["Zed Team <hi@zed.dev>"]

View File

@@ -1 +1 @@
dev
preview

View File

@@ -94,7 +94,20 @@ pub fn init(client: Arc<Client>, user_store: Entity<UserStore>, cx: &mut App) {
let user_store = user_store.clone();
move |cx| {
let new_provider = all_language_settings(None, cx).inline_completions.provider;
if new_provider != provider {
let tos_accepted = user_store
.read(cx)
.current_user_has_accepted_terms()
.unwrap_or(false);
telemetry::event!(
"Edit Prediction Provider Changed",
from = provider,
to = new_provider,
zed_ai_tos_accepted = tos_accepted,
);
provider = new_provider;
assign_inline_completion_providers(
&editors,
@@ -104,11 +117,7 @@ pub fn init(client: Arc<Client>, user_store: Entity<UserStore>, cx: &mut App) {
cx,
);
if !user_store
.read(cx)
.current_user_has_accepted_terms()
.unwrap_or(false)
{
if !tos_accepted {
match provider {
InlineCompletionProvider::Zed => {
let Some(window) = cx.active_window() else {

View File

@@ -16,8 +16,8 @@ use gpui::{
use search::{buffer_search, BufferSearchBar};
use settings::{Settings, SettingsStore};
use ui::{
prelude::*, ButtonStyle, ContextMenu, IconButton, IconButtonShape, IconName, IconSize,
PopoverMenu, PopoverMenuHandle, Tooltip,
prelude::*, ButtonStyle, ContextMenu, ContextMenuEntry, IconButton, IconButtonShape, IconName,
IconSize, PopoverMenu, PopoverMenuHandle, Tooltip,
};
use vim_mode_setting::VimModeSetting;
use workspace::{
@@ -94,7 +94,8 @@ impl Render for QuickActionBar {
git_blame_inline_enabled,
show_git_blame_gutter,
auto_signature_help_enabled,
inline_completions_enabled,
show_inline_completions,
inline_completion_enabled,
) = {
let editor = editor.read(cx);
let selection_menu_enabled = editor.selection_menu_enabled(cx);
@@ -103,7 +104,8 @@ 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);
let show_inline_completions = editor.should_show_inline_completions(cx);
let inline_completion_enabled = editor.inline_completions_enabled(cx);
(
selection_menu_enabled,
@@ -112,7 +114,8 @@ impl Render for QuickActionBar {
git_blame_inline_enabled,
show_git_blame_gutter,
auto_signature_help_enabled,
inline_completions_enabled,
show_inline_completions,
inline_completion_enabled,
)
};
@@ -294,12 +297,12 @@ impl Render for QuickActionBar {
},
);
menu = menu.toggleable_entry(
"Edit Predictions",
inline_completions_enabled,
IconPosition::Start,
Some(editor::actions::ToggleInlineCompletions.boxed_clone()),
{
let mut inline_completion_entry = ContextMenuEntry::new("Edit Predictions")
.toggleable(IconPosition::Start, inline_completion_enabled && show_inline_completions)
.disabled(!inline_completion_enabled)
.action(Some(
editor::actions::ToggleInlineCompletions.boxed_clone(),
)).handler({
let editor = editor.clone();
move |window, cx| {
editor
@@ -312,8 +315,14 @@ impl Render for QuickActionBar {
})
.ok();
}
},
);
});
if !inline_completion_enabled {
inline_completion_entry = inline_completion_entry.documentation_aside(|_| {
Label::new("You can't toggle edit predictions for this file as it is within the excluded files list.").into_any_element()
});
}
menu = menu.item(inline_completion_entry);
menu = menu.separator();

View File

@@ -6,6 +6,8 @@ use settings::SettingsStore;
use ui::{prelude::*, ButtonLike, Tooltip};
use util::ResultExt;
use crate::onboarding_event;
/// Prompts the user to try Zed's Edit Prediction feature
pub struct ZedPredictBanner {
dismissed: bool,
@@ -53,6 +55,7 @@ impl ZedPredictBanner {
}
fn dismiss(&mut self, cx: &mut Context<Self>) {
onboarding_event!("Banner Dismissed");
persist_dismissed(cx);
self.dismissed = true;
cx.notify();
@@ -107,6 +110,7 @@ impl Render for ZedPredictBanner {
),
)
.on_click(|_, window, cx| {
onboarding_event!("Banner Clicked");
window.dispatch_action(Box::new(zed_actions::OpenZedPredictOnboarding), cx)
}),
)

View File

@@ -1,6 +1,6 @@
use std::{sync::Arc, time::Duration};
use crate::{Zeta, ZED_PREDICT_DATA_COLLECTION_CHOICE};
use crate::{onboarding_event, ZED_PREDICT_DATA_COLLECTION_CHOICE};
use client::{Client, UserStore};
use db::kvp::KEY_VALUE_STORE;
use feature_flags::FeatureFlagAppExt as _;
@@ -11,10 +11,9 @@ use gpui::{
};
use language::language_settings::{AllLanguageSettings, InlineCompletionProvider};
use settings::{update_settings_file, Settings};
use ui::{prelude::*, Checkbox, TintColor, Tooltip};
use ui::{prelude::*, Checkbox, TintColor};
use util::ResultExt;
use workspace::{notifications::NotifyTaskExt, ModalView, Workspace};
use worktree::Worktree;
/// Introduces user to Zed's Edit Prediction feature and terms of service
pub struct ZedPredictModal {
@@ -26,7 +25,6 @@ pub struct ZedPredictModal {
terms_of_service: bool,
data_collection_expanded: bool,
data_collection_opted_in: bool,
worktrees: Vec<Entity<Worktree>>,
}
#[derive(PartialEq, Eq)]
@@ -48,8 +46,6 @@ impl ZedPredictModal {
window: &mut Window,
cx: &mut Context<Workspace>,
) {
let worktrees = workspace.visible_worktrees(cx).collect();
workspace.toggle_modal(window, cx, |_window, cx| Self {
user_store,
client,
@@ -59,23 +55,28 @@ impl ZedPredictModal {
terms_of_service: false,
data_collection_expanded: false,
data_collection_opted_in: false,
worktrees,
});
}
fn view_terms(&mut self, _: &ClickEvent, _: &mut Window, cx: &mut Context<Self>) {
cx.open_url("https://zed.dev/terms-of-service");
cx.notify();
onboarding_event!("ToS Link Clicked");
}
fn view_blog(&mut self, _: &ClickEvent, _: &mut Window, cx: &mut Context<Self>) {
cx.open_url("https://zed.dev/blog/"); // TODO Add the link when live
cx.notify();
onboarding_event!("Blog Link clicked");
}
fn inline_completions_doc(&mut self, _: &ClickEvent, _: &mut Window, cx: &mut Context<Self>) {
cx.open_url("https://zed.dev/docs/configuring-zed#inline-completions");
cx.notify();
onboarding_event!("Docs Link Clicked");
}
fn accept_and_enable(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
@@ -107,17 +108,15 @@ impl ZedPredictModal {
.inline_completion_provider = Some(InlineCompletionProvider::Zed);
});
if this.worktrees.is_empty() {
cx.emit(DismissEvent);
return;
}
Zeta::register(None, this.client.clone(), this.user_store.clone(), cx);
cx.emit(DismissEvent);
})
})
.detach_and_notify_err(window, cx);
onboarding_event!(
"Enable Clicked",
data_collection_opted_in = self.data_collection_opted_in,
);
}
fn sign_in(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
@@ -134,12 +133,15 @@ impl ZedPredictModal {
this.update(&mut cx, |this, cx| {
this.sign_in_status = status;
onboarding_event!("Signed In");
cx.notify()
})?;
result
})
.detach_and_notify_err(window, cx);
onboarding_event!("Sign In Clicked");
}
fn cancel(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
@@ -171,6 +173,7 @@ impl Render for ZedPredictModal {
.track_focus(&self.focus_handle(cx))
.on_action(cx.listener(Self::cancel))
.on_action(cx.listener(|_, _: &menu::Cancel, _window, cx| {
onboarding_event!("Cancelled", trigger = "Action");
cx.emit(DismissEvent);
}))
.on_any_mouse_down(cx.listener(|this, _: &MouseDownEvent, window, _cx| {
@@ -253,6 +256,7 @@ impl Render for ZedPredictModal {
.child(h_flex().absolute().top_2().right_2().child(
IconButton::new("cancel", IconName::X).on_click(cx.listener(
|_, _: &ClickEvent, _window, cx| {
onboarding_event!("Cancelled", trigger = "X click");
cx.emit(DismissEvent);
},
)),
@@ -314,7 +318,7 @@ impl Render for ZedPredictModal {
.label("Read and accept the")
.on_click(cx.listener(move |this, state, _window, cx| {
this.terms_of_service = *state == ToggleState::Selected;
cx.notify()
cx.notify();
})),
)
.child(
@@ -336,17 +340,6 @@ impl Render for ZedPredictModal {
)
.label("Optionally share training data (OSS-only).")
.fill()
.when(self.worktrees.is_empty(), |element| {
element.disabled(true).tooltip(move |window, cx| {
Tooltip::with_meta(
"No Project Open",
None,
"Open a project to enable this option.",
window,
cx,
)
})
})
.on_click(cx.listener(
move |this, state, _window, cx| {
this.data_collection_opted_in =
@@ -355,7 +348,6 @@ impl Render for ZedPredictModal {
},
)),
)
// TODO: show each worktree if more than 1
.child(
Button::new("learn-more", "Learn More")
.icon(accordion_icons.0)
@@ -364,7 +356,11 @@ impl Render for ZedPredictModal {
.on_click(cx.listener(|this, _, _, cx| {
this.data_collection_expanded =
!this.data_collection_expanded;
cx.notify()
cx.notify();
if this.data_collection_expanded {
onboarding_event!("Data Collection Learn More Clicked");
}
})),
),
)

View File

@@ -0,0 +1,9 @@
#[macro_export]
macro_rules! onboarding_event {
($name:expr) => {
telemetry::event!($name, source = "Edit Prediction Onboarding");
};
($name:expr, $($key:ident $(= $value:expr)?),+ $(,)?) => {
telemetry::event!($name, source = "Edit Prediction Onboarding", $($key $(= $value)?),+);
};
}

View File

@@ -52,6 +52,8 @@ impl RateCompletionModal {
pub fn toggle(workspace: &mut Workspace, window: &mut Window, cx: &mut Context<Workspace>) {
if let Some(zeta) = Zeta::global(cx) {
workspace.toggle_modal(window, cx, |_window, cx| RateCompletionModal::new(zeta, cx));
telemetry::event!("Rate Completion Modal Open", source = "Edit Prediction");
}
}

View File

@@ -3,6 +3,7 @@ mod init;
mod license_detection;
mod onboarding_banner;
mod onboarding_modal;
mod onboarding_telemetry;
mod rate_completion_modal;
pub(crate) use completion_diff_element::*;
@@ -24,8 +25,7 @@ use gpui::{
};
use http_client::{HttpClient, Method};
use language::{
language_settings::all_language_settings, Anchor, Buffer, BufferSnapshot, EditPreview,
OffsetRangeExt, Point, ToOffset, ToPoint,
Anchor, Buffer, BufferSnapshot, EditPreview, OffsetRangeExt, Point, ToOffset, ToPoint,
};
use language_models::LlmApiToken;
use postage::watch;
@@ -952,21 +952,33 @@ impl LicenseDetectionWatcher {
pub fn new(worktree: &Worktree, cx: &mut Context<Worktree>) -> Self {
let (mut is_open_source_tx, is_open_source_rx) = watch::channel_with::<bool>(false);
let loaded_file_fut = worktree.load_file(Path::new("LICENSE"), cx);
const LICENSE_FILES_TO_CHECK: [&'static str; 2] = ["LICENSE", "LICENCE"]; // US and UK English spelling
// Check if worktree is a single file, if so we do not need to check for a LICENSE file
let task = if worktree.abs_path().is_file() {
Task::ready(())
} else {
let loaded_files_task = futures::future::join_all(
LICENSE_FILES_TO_CHECK
.iter()
.map(|file| worktree.load_file(Path::new(file), cx)),
);
cx.background_executor().spawn(async move {
for loaded_file in loaded_files_task.await {
if let Some(content) = loaded_file.log_err() {
if is_license_eligible_for_data_collection(&content.text) {
*is_open_source_tx.borrow_mut() = true;
break;
}
}
}
})
};
Self {
is_open_source_rx,
_is_open_source_task: cx.spawn(|_, _| async move {
// TODO: Don't display error if file not found
let Some(loaded_file) = loaded_file_fut.await.log_err() else {
return;
};
let is_loaded_file_open_source_thing: bool =
is_license_eligible_for_data_collection(&loaded_file.text);
*is_open_source_tx.borrow_mut() = is_loaded_file_open_source_thing;
}),
_is_open_source_task: task,
}
}
@@ -1456,15 +1468,11 @@ impl inline_completion::InlineCompletionProvider for ZetaInlineCompletionProvide
fn is_enabled(
&self,
buffer: &Entity<Buffer>,
cursor_position: language::Anchor,
cx: &App,
_buffer: &Entity<Buffer>,
_cursor_position: language::Anchor,
_cx: &App,
) -> bool {
let buffer = buffer.read(cx);
let file = buffer.file();
let language = buffer.language_at(cursor_position);
let settings = all_language_settings(file, cx);
settings.inline_completions_enabled(language.as_ref(), file.map(|f| f.path().as_ref()), cx)
true
}
fn needs_terms_acceptance(&self, cx: &App) -> bool {