Compare commits
42 Commits
github-tok
...
v0.172.11
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
76db69d7de | ||
|
|
7d5aea2d6b | ||
|
|
b487d48b65 | ||
|
|
d60d102772 | ||
|
|
37913f5337 | ||
|
|
6416231e37 | ||
|
|
af707c512b | ||
|
|
43ccb51f54 | ||
|
|
3dd6dfb829 | ||
|
|
3c405afff4 | ||
|
|
b41e3a6b17 | ||
|
|
b1f2d8155c | ||
|
|
39d77b250e | ||
|
|
2126501be8 | ||
|
|
d4531af8aa | ||
|
|
4a2b99f7fa | ||
|
|
623b6a2da6 | ||
|
|
092261ac3b | ||
|
|
d7d7d4c46b | ||
|
|
841a71184a | ||
|
|
6ba3b2d340 | ||
|
|
86fb7c8630 | ||
|
|
356f90cc1d | ||
|
|
480fd23b1c | ||
|
|
a3c9f94d40 | ||
|
|
efb55f46ab | ||
|
|
6a0417670c | ||
|
|
0a8882f86b | ||
|
|
29d8db76e1 | ||
|
|
9c5c5d4ec5 | ||
|
|
f10e44f05c | ||
|
|
1be2281ccc | ||
|
|
edf69b3923 | ||
|
|
2d1d5b823e | ||
|
|
08e363cc35 | ||
|
|
e87ff54699 | ||
|
|
8436dfc15a | ||
|
|
2609a70a05 | ||
|
|
b0b9e64452 | ||
|
|
0d2e6fdcd9 | ||
|
|
e716eb6d46 | ||
|
|
dc808ae2c1 |
@@ -29,8 +29,7 @@ jobs:
|
||||
maxLength: 2000
|
||||
truncationSymbol: "..."
|
||||
- name: Discord Webhook Action
|
||||
uses: tsickert/discord-webhook@86dc739f3f165f16dadc5666051c367efa1692f4 # v6.0.0
|
||||
uses: tsickert/discord-webhook@c840d45a03a323fbc3f7507ac7769dbd91bfb164 # v5.3.0
|
||||
with:
|
||||
webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }}
|
||||
content: ${{ steps.get-content.outputs.string }}
|
||||
flags: 4 # suppress embeds - https://discord.com/developers/docs/resources/message#message-object-message-flags
|
||||
|
||||
21
Cargo.lock
generated
21
Cargo.lock
generated
@@ -4023,7 +4023,7 @@ dependencies = [
|
||||
"util",
|
||||
"uuid",
|
||||
"workspace",
|
||||
"zed_predict_tos",
|
||||
"zed_predict_onboarding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6388,7 +6388,7 @@ dependencies = [
|
||||
"ui",
|
||||
"workspace",
|
||||
"zed_actions",
|
||||
"zed_predict_tos",
|
||||
"zed_predict_onboarding",
|
||||
"zeta",
|
||||
]
|
||||
|
||||
@@ -13412,6 +13412,7 @@ dependencies = [
|
||||
"windows 0.58.0",
|
||||
"workspace",
|
||||
"zed_actions",
|
||||
"zed_predict_onboarding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -16286,7 +16287,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed"
|
||||
version = "0.172.0"
|
||||
version = "0.172.11"
|
||||
dependencies = [
|
||||
"activity_indicator",
|
||||
"anyhow",
|
||||
@@ -16409,7 +16410,7 @@ dependencies = [
|
||||
"winresource",
|
||||
"workspace",
|
||||
"zed_actions",
|
||||
"zed_predict_tos",
|
||||
"zed_predict_onboarding",
|
||||
"zeta",
|
||||
]
|
||||
|
||||
@@ -16524,13 +16525,21 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zed_predict_tos"
|
||||
name = "zed_predict_onboarding"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"client",
|
||||
"db",
|
||||
"feature_flags",
|
||||
"fs",
|
||||
"gpui",
|
||||
"language",
|
||||
"menu",
|
||||
"settings",
|
||||
"theme",
|
||||
"ui",
|
||||
"util",
|
||||
"workspace",
|
||||
]
|
||||
|
||||
@@ -16731,6 +16740,7 @@ dependencies = [
|
||||
"collections",
|
||||
"command_palette_hooks",
|
||||
"ctor",
|
||||
"db",
|
||||
"editor",
|
||||
"env_logger 0.11.6",
|
||||
"feature_flags",
|
||||
@@ -16745,6 +16755,7 @@ dependencies = [
|
||||
"menu",
|
||||
"reqwest_client",
|
||||
"rpc",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"settings",
|
||||
"similar",
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
resolver = "2"
|
||||
members = [
|
||||
"crates/activity_indicator",
|
||||
"crates/zed_predict_tos",
|
||||
"crates/anthropic",
|
||||
"crates/assets",
|
||||
"crates/assistant",
|
||||
@@ -151,6 +150,7 @@ members = [
|
||||
"crates/worktree",
|
||||
"crates/zed",
|
||||
"crates/zed_actions",
|
||||
"crates/zed_predict_onboarding",
|
||||
"crates/zeta",
|
||||
"crates/git_ui",
|
||||
|
||||
@@ -201,7 +201,6 @@ edition = "2021"
|
||||
|
||||
activity_indicator = { path = "crates/activity_indicator" }
|
||||
ai = { path = "crates/ai" }
|
||||
zed_predict_tos = { path = "crates/zed_predict_tos" }
|
||||
anthropic = { path = "crates/anthropic" }
|
||||
assets = { path = "crates/assets" }
|
||||
assistant = { path = "crates/assistant" }
|
||||
@@ -349,6 +348,7 @@ workspace = { path = "crates/workspace" }
|
||||
worktree = { path = "crates/worktree" }
|
||||
zed = { path = "crates/zed" }
|
||||
zed_actions = { path = "crates/zed_actions" }
|
||||
zed_predict_onboarding = { path = "crates/zed_predict_onboarding" }
|
||||
zeta = { path = "crates/zeta" }
|
||||
|
||||
#
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7 8.9V11C5.34478 11 4.65522 11 3 11V10.4L7 5.6V5H3V7.1" stroke="black" stroke-width="1.5"/>
|
||||
<path d="M12 5L14 8L12 11" stroke="black" stroke-width="1.5"/>
|
||||
<path d="M10 6.5L11 8L10 9.5" stroke="black" stroke-width="1.5"/>
|
||||
<path d="M7.5 8.9V11C5.43097 11 4.56903 11 2.5 11V10.4L7.5 5.6V5H2.5V7.1" stroke="black" stroke-width="1.5"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 334 B After Width: | Height: | Size: 342 B |
19
assets/icons/zed_predict_bg.svg
Normal file
19
assets/icons/zed_predict_bg.svg
Normal file
@@ -0,0 +1,19 @@
|
||||
<svg width="420" height="128" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<pattern id="tilePattern" width="22" height="22" patternUnits="userSpaceOnUse">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 5L14 8L12 11" stroke="black" stroke-width="1.5"/>
|
||||
<path d="M10 6.5L11 8L10 9.5" stroke="black" stroke-width="1.5"/>
|
||||
<path d="M7.5 8.9V11C5.43097 11 4.56903 11 2.5 11V10.4L7.5 5.6V5H2.5V7.1" stroke="black" stroke-width="1.5"/>
|
||||
</svg>
|
||||
</pattern>
|
||||
<linearGradient id="fade" y2="1" x2="0">
|
||||
<stop offset="0" stop-color="white" stop-opacity=".24"/>
|
||||
<stop offset="1" stop-color="white" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<mask id="fadeMask" maskContentUnits="objectBoundingBox">
|
||||
<rect width="1" height="1" fill="url(#fade)"/>
|
||||
</mask>
|
||||
</defs>
|
||||
<rect width="100%" height="100%" fill="url(#tilePattern)" mask="url(#fadeMask)"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 971 B |
@@ -821,5 +821,12 @@
|
||||
"shift-end": "terminal::ScrollToBottom",
|
||||
"ctrl-shift-space": "terminal::ToggleViMode"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ZedPredictModal",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"escape": "menu::Cancel"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -881,7 +881,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ZedPredictTos",
|
||||
"context": "ZedPredictModal",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"escape": "menu::Cancel"
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
"light": "One Light",
|
||||
"dark": "One Dark"
|
||||
},
|
||||
"icon_theme": "Zed (Default)",
|
||||
// The name of a base set of key bindings to use.
|
||||
// This setting can take four values, each named after another
|
||||
// text editor:
|
||||
@@ -775,7 +776,14 @@
|
||||
"load_direnv": "direct",
|
||||
"inline_completions": {
|
||||
// A list of globs representing files that inline completions should be disabled for.
|
||||
"disabled_globs": [".env"]
|
||||
"disabled_globs": [
|
||||
"**/.env*",
|
||||
"**/*.pem",
|
||||
"**/*.key",
|
||||
"**/*.cert",
|
||||
"**/*.crt",
|
||||
"**/secrets.yml"
|
||||
]
|
||||
},
|
||||
// Settings specific to journaling
|
||||
"journal": {
|
||||
|
||||
@@ -979,12 +979,13 @@ impl ContextEditor {
|
||||
let render_block: RenderBlock = Arc::new({
|
||||
let this = this.clone();
|
||||
let patch_range = range.clone();
|
||||
move |cx: &mut BlockContext<'_, '_>| {
|
||||
move |cx: &mut BlockContext| {
|
||||
let max_width = cx.max_width;
|
||||
let gutter_width = cx.gutter_dimensions.full_width();
|
||||
let block_id = cx.block_id;
|
||||
let selected = cx.selected;
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
let window = &mut cx.window;
|
||||
this.update(cx.app, |this, cx| {
|
||||
this.render_patch_block(
|
||||
patch_range.clone(),
|
||||
max_width,
|
||||
|
||||
@@ -121,9 +121,7 @@ pub enum Event {
|
||||
},
|
||||
ShowContacts,
|
||||
ParticipantIndicesChanged,
|
||||
TermsStatusUpdated {
|
||||
accepted: bool,
|
||||
},
|
||||
PrivateUserInfoUpdated,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
@@ -227,9 +225,7 @@ impl UserStore {
|
||||
};
|
||||
|
||||
this.set_current_user_accepted_tos_at(accepted_tos_at);
|
||||
cx.emit(Event::TermsStatusUpdated {
|
||||
accepted: accepted_tos_at.is_some(),
|
||||
});
|
||||
cx.emit(Event::PrivateUserInfoUpdated);
|
||||
})
|
||||
} else {
|
||||
anyhow::Ok(())
|
||||
@@ -244,6 +240,8 @@ impl UserStore {
|
||||
Status::SignedOut => {
|
||||
current_user_tx.send(None).await.ok();
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.accepted_tos_at = None;
|
||||
cx.emit(Event::PrivateUserInfoUpdated);
|
||||
cx.notify();
|
||||
this.clear_contacts()
|
||||
})?
|
||||
@@ -714,7 +712,7 @@ impl UserStore {
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.set_current_user_accepted_tos_at(Some(response.accepted_tos_at));
|
||||
cx.emit(Event::TermsStatusUpdated { accepted: true });
|
||||
cx.emit(Event::PrivateUserInfoUpdated);
|
||||
})
|
||||
} else {
|
||||
Err(anyhow!("client not found"))
|
||||
|
||||
@@ -447,7 +447,7 @@ async fn predict_edits(
|
||||
));
|
||||
}
|
||||
|
||||
let sample_input_output = claims.is_staff && rand::random::<f32>() < 0.1;
|
||||
let should_sample = claims.is_staff || params.can_collect_data;
|
||||
|
||||
let api_url = state
|
||||
.config
|
||||
@@ -541,7 +541,7 @@ async fn predict_edits(
|
||||
let output = choice.text.clone();
|
||||
|
||||
async move {
|
||||
let properties = if sample_input_output {
|
||||
let properties = if should_sample {
|
||||
json!({
|
||||
"model": model.to_string(),
|
||||
"headers": response.headers,
|
||||
|
||||
@@ -36,8 +36,8 @@ pub enum Model {
|
||||
Gpt3_5Turbo,
|
||||
#[serde(alias = "o1", rename = "o1")]
|
||||
O1,
|
||||
#[serde(alias = "o1-mini", rename = "o1-mini")]
|
||||
O1Mini,
|
||||
#[serde(alias = "o1-mini", rename = "o3-mini")]
|
||||
O3Mini,
|
||||
#[serde(alias = "claude-3-5-sonnet", rename = "claude-3.5-sonnet")]
|
||||
Claude3_5Sonnet,
|
||||
}
|
||||
@@ -46,7 +46,7 @@ impl Model {
|
||||
pub fn uses_streaming(&self) -> bool {
|
||||
match self {
|
||||
Self::Gpt4o | Self::Gpt4 | Self::Gpt3_5Turbo | Self::Claude3_5Sonnet => true,
|
||||
Self::O1Mini | Self::O1 => false,
|
||||
Self::O3Mini | Self::O1 => false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ impl Model {
|
||||
"gpt-4" => Ok(Self::Gpt4),
|
||||
"gpt-3.5-turbo" => Ok(Self::Gpt3_5Turbo),
|
||||
"o1" => Ok(Self::O1),
|
||||
"o1-mini" => Ok(Self::O1Mini),
|
||||
"o3-mini" => Ok(Self::O3Mini),
|
||||
"claude-3-5-sonnet" => Ok(Self::Claude3_5Sonnet),
|
||||
_ => Err(anyhow!("Invalid model id: {}", id)),
|
||||
}
|
||||
@@ -67,7 +67,7 @@ impl Model {
|
||||
Self::Gpt3_5Turbo => "gpt-3.5-turbo",
|
||||
Self::Gpt4 => "gpt-4",
|
||||
Self::Gpt4o => "gpt-4o",
|
||||
Self::O1Mini => "o1-mini",
|
||||
Self::O3Mini => "o3-mini",
|
||||
Self::O1 => "o1",
|
||||
Self::Claude3_5Sonnet => "claude-3-5-sonnet",
|
||||
}
|
||||
@@ -78,7 +78,7 @@ impl Model {
|
||||
Self::Gpt3_5Turbo => "GPT-3.5",
|
||||
Self::Gpt4 => "GPT-4",
|
||||
Self::Gpt4o => "GPT-4o",
|
||||
Self::O1Mini => "o1-mini",
|
||||
Self::O3Mini => "o3-mini",
|
||||
Self::O1 => "o1",
|
||||
Self::Claude3_5Sonnet => "Claude 3.5 Sonnet",
|
||||
}
|
||||
@@ -89,7 +89,7 @@ impl Model {
|
||||
Self::Gpt4o => 64000,
|
||||
Self::Gpt4 => 32768,
|
||||
Self::Gpt3_5Turbo => 12288,
|
||||
Self::O1Mini => 20000,
|
||||
Self::O3Mini => 20000,
|
||||
Self::O1 => 20000,
|
||||
Self::Claude3_5Sonnet => 200_000,
|
||||
}
|
||||
|
||||
@@ -88,7 +88,7 @@ url.workspace = true
|
||||
util.workspace = true
|
||||
uuid.workspace = true
|
||||
workspace.workspace = true
|
||||
zed_predict_tos.workspace = true
|
||||
zed_predict_onboarding.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
ctor.workspace = true
|
||||
|
||||
@@ -652,7 +652,7 @@ impl CompletionsMenu {
|
||||
)
|
||||
.on_click(cx.listener(move |editor, _event, window, cx| {
|
||||
cx.stop_propagation();
|
||||
editor.toggle_zed_predict_tos(window, cx);
|
||||
editor.toggle_zed_predict_onboarding(window, cx);
|
||||
})),
|
||||
),
|
||||
|
||||
|
||||
@@ -1140,12 +1140,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 {
|
||||
|
||||
@@ -63,13 +63,13 @@ 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;
|
||||
use zed_predict_tos::ZedPredictTos;
|
||||
use zed_predict_onboarding::ZedPredictModal;
|
||||
|
||||
use code_context_menus::{
|
||||
AvailableCodeAction, CodeActionContents, CodeActionsItem, CodeActionsMenu, CodeContextMenu,
|
||||
@@ -723,6 +723,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<()>>,
|
||||
@@ -1382,6 +1383,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,
|
||||
@@ -3947,12 +3949,21 @@ impl Editor {
|
||||
self.do_completion(action.item_ix, CompletionIntent::Compose, window, cx)
|
||||
}
|
||||
|
||||
fn toggle_zed_predict_tos(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
fn toggle_zed_predict_onboarding(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let (Some(workspace), Some(project)) = (self.workspace(), self.project.as_ref()) else {
|
||||
return;
|
||||
};
|
||||
|
||||
ZedPredictTos::toggle(workspace, project.read(cx).user_store().clone(), window, cx);
|
||||
let project = project.read(cx);
|
||||
|
||||
ZedPredictModal::toggle(
|
||||
workspace,
|
||||
project.user_store().clone(),
|
||||
project.client().clone(),
|
||||
project.fs().clone(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
fn do_completion(
|
||||
@@ -3984,7 +3995,7 @@ impl Editor {
|
||||
)) => {
|
||||
drop(entries);
|
||||
drop(context_menu);
|
||||
self.toggle_zed_predict_tos(window, cx);
|
||||
self.toggle_zed_predict_onboarding(window, cx);
|
||||
return Some(Task::ready(Ok(())));
|
||||
}
|
||||
_ => {}
|
||||
@@ -14086,7 +14097,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());
|
||||
@@ -14100,7 +14111,7 @@ impl Editor {
|
||||
.size
|
||||
.width;
|
||||
|
||||
gpui::Point::new(em_width, line_height)
|
||||
gpui::Size::new(em_width, line_height)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15601,9 +15612,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);
|
||||
@@ -15621,6 +15632,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 {
|
||||
|
||||
@@ -502,7 +502,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;
|
||||
@@ -511,7 +510,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)
|
||||
})
|
||||
}
|
||||
});
|
||||
@@ -521,17 +520,16 @@ impl EditorElement {
|
||||
editor: &mut Editor,
|
||||
event: &ModifiersChangedEvent,
|
||||
position_map: &PositionMap,
|
||||
text_hitbox: &Hitbox,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Editor>,
|
||||
) {
|
||||
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,
|
||||
@@ -539,14 +537,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>,
|
||||
@@ -555,6 +550,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;
|
||||
|
||||
@@ -611,8 +608,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(
|
||||
@@ -687,15 +683,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),
|
||||
@@ -710,16 +704,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(
|
||||
@@ -737,10 +729,10 @@ impl EditorElement {
|
||||
editor: &mut Editor,
|
||||
event: &MouseUpEvent,
|
||||
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();
|
||||
|
||||
@@ -755,7 +747,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.position);
|
||||
let point = position_map.point_for_position(event.position);
|
||||
editor.handle_click_hovered_link(point, event.modifiers, window, cx);
|
||||
|
||||
cx.stop_propagation();
|
||||
@@ -771,8 +763,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(
|
||||
@@ -795,7 +786,6 @@ impl EditorElement {
|
||||
editor: &mut Editor,
|
||||
event: &MouseMoveEvent,
|
||||
position_map: &PositionMap,
|
||||
text_bounds: Bounds<Pixels>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Editor>,
|
||||
) {
|
||||
@@ -803,7 +793,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;
|
||||
@@ -855,19 +846,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,
|
||||
@@ -3789,8 +3779,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![];
|
||||
|
||||
@@ -3927,7 +3916,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();
|
||||
@@ -3952,8 +3944,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(),
|
||||
@@ -4027,7 +4019,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;
|
||||
@@ -4039,7 +4031,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;
|
||||
@@ -4052,8 +4044,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,
|
||||
));
|
||||
@@ -4428,7 +4420,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
|
||||
@@ -4442,7 +4434,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);
|
||||
@@ -4464,7 +4456,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 {
|
||||
@@ -4543,7 +4535,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(),
|
||||
@@ -5117,13 +5109,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);
|
||||
})
|
||||
}
|
||||
@@ -5242,8 +5234,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
|
||||
@@ -5275,32 +5265,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);
|
||||
}),
|
||||
_ => {}
|
||||
};
|
||||
@@ -5311,12 +5285,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)
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -5324,8 +5297,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 {
|
||||
@@ -5336,25 +5307,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)
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -7159,6 +7115,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(
|
||||
@@ -7182,7 +7144,6 @@ impl Element for EditorElement {
|
||||
wrap_guides,
|
||||
indent_guides,
|
||||
hitbox,
|
||||
text_hitbox,
|
||||
gutter_hitbox,
|
||||
display_hunks,
|
||||
content_origin,
|
||||
@@ -7354,7 +7315,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>>,
|
||||
@@ -7534,15 +7494,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)]
|
||||
@@ -7564,11 +7526,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);
|
||||
|
||||
@@ -1040,10 +1040,10 @@ impl SerializableItem for Editor {
|
||||
} => window.spawn(cx, |mut cx| {
|
||||
let project = project.clone();
|
||||
async move {
|
||||
let language = if let Some(language_name) = language {
|
||||
let language_registry =
|
||||
project.update(&mut cx, |project, _| project.languages().clone())?;
|
||||
let language_registry =
|
||||
project.update(&mut cx, |project, _| project.languages().clone())?;
|
||||
|
||||
let language = if let Some(language_name) = language {
|
||||
// We don't fail here, because we'd rather not set the language if the name changed
|
||||
// than fail to restore the buffer.
|
||||
language_registry
|
||||
@@ -1061,6 +1061,7 @@ impl SerializableItem for Editor {
|
||||
|
||||
// Then set the text so that the dirty bit is set correctly
|
||||
buffer.update(&mut cx, |buffer, cx| {
|
||||
buffer.set_language_registry(language_registry);
|
||||
if let Some(language) = language {
|
||||
buffer.set_language(Some(language), cx);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -87,8 +87,8 @@ define_connection!(
|
||||
// mtime_seconds: Option<i64>,
|
||||
// mtime_nanos: Option<i32>,
|
||||
// )
|
||||
pub static ref DB: EditorDb<WorkspaceDb> =
|
||||
&[sql! (
|
||||
pub static ref DB: EditorDb<WorkspaceDb> = &[
|
||||
sql! (
|
||||
CREATE TABLE editors(
|
||||
item_id INTEGER NOT NULL,
|
||||
workspace_id INTEGER NOT NULL,
|
||||
@@ -134,7 +134,7 @@ define_connection!(
|
||||
ALTER TABLE editors ADD COLUMN mtime_seconds INTEGER DEFAULT NULL;
|
||||
ALTER TABLE editors ADD COLUMN mtime_nanos INTEGER DEFAULT NULL;
|
||||
),
|
||||
];
|
||||
];
|
||||
);
|
||||
|
||||
impl EditorDb {
|
||||
|
||||
@@ -444,6 +444,23 @@ impl ExtensionStore {
|
||||
.filter_map(|(name, theme)| theme.extension.as_ref().eq(extension_id).then_some(name))
|
||||
}
|
||||
|
||||
/// Returns the names of icon themes provided by extensions.
|
||||
pub fn extension_icon_themes<'a>(
|
||||
&'a self,
|
||||
extension_id: &'a str,
|
||||
) -> impl Iterator<Item = &'a Arc<str>> {
|
||||
self.extension_index
|
||||
.icon_themes
|
||||
.iter()
|
||||
.filter_map(|(name, icon_theme)| {
|
||||
icon_theme
|
||||
.extension
|
||||
.as_ref()
|
||||
.eq(extension_id)
|
||||
.then_some(name)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn fetch_extensions(
|
||||
&self,
|
||||
search: Option<&str>,
|
||||
|
||||
@@ -295,6 +295,25 @@ impl ExtensionsPage {
|
||||
);
|
||||
})
|
||||
.ok();
|
||||
return;
|
||||
}
|
||||
|
||||
let icon_themes = extension_store
|
||||
.extension_icon_themes(extension_id)
|
||||
.map(|name| name.to_string())
|
||||
.collect::<Vec<_>>();
|
||||
if !icon_themes.is_empty() {
|
||||
workspace
|
||||
.update(cx, |_workspace, cx| {
|
||||
window.dispatch_action(
|
||||
zed_actions::icon_theme_selector::Toggle {
|
||||
themes_filter: Some(icon_themes),
|
||||
}
|
||||
.boxed_clone(),
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -305,8 +305,14 @@ pub enum Model {
|
||||
Gemini15Pro,
|
||||
#[serde(rename = "gemini-1.5-flash")]
|
||||
Gemini15Flash,
|
||||
#[serde(rename = "gemini-2.0-flash-exp")]
|
||||
#[serde(rename = "gemini-2.0-pro-exp")]
|
||||
Gemini20Pro,
|
||||
#[serde(rename = "gemini-2.0-flash")]
|
||||
Gemini20Flash,
|
||||
#[serde(rename = "gemini-2.0-flash-thinking-exp")]
|
||||
Gemini20FlashThinking,
|
||||
#[serde(rename = "gemini-2.0-flash-lite-preview")]
|
||||
Gemini20FlashLite,
|
||||
#[serde(rename = "custom")]
|
||||
Custom {
|
||||
name: String,
|
||||
@@ -321,7 +327,10 @@ impl Model {
|
||||
match self {
|
||||
Model::Gemini15Pro => "gemini-1.5-pro",
|
||||
Model::Gemini15Flash => "gemini-1.5-flash",
|
||||
Model::Gemini20Flash => "gemini-2.0-flash-exp",
|
||||
Model::Gemini20Pro => "gemini-2.0-pro-exp",
|
||||
Model::Gemini20Flash => "gemini-2.0-flash",
|
||||
Model::Gemini20FlashThinking => "gemini-2.0-flash-thinking-exp",
|
||||
Model::Gemini20FlashLite => "gemini-2.0-flash-lite-preview",
|
||||
Model::Custom { name, .. } => name,
|
||||
}
|
||||
}
|
||||
@@ -330,7 +339,10 @@ impl Model {
|
||||
match self {
|
||||
Model::Gemini15Pro => "Gemini 1.5 Pro",
|
||||
Model::Gemini15Flash => "Gemini 1.5 Flash",
|
||||
Model::Gemini20Pro => "Gemini 2.0 Pro",
|
||||
Model::Gemini20Flash => "Gemini 2.0 Flash",
|
||||
Model::Gemini20FlashThinking => "Gemini 2.0 Flash Thinking",
|
||||
Model::Gemini20FlashLite => "Gemini 2.0 Flash Lite",
|
||||
Self::Custom {
|
||||
name, display_name, ..
|
||||
} => display_name.as_ref().unwrap_or(name),
|
||||
@@ -341,7 +353,10 @@ impl Model {
|
||||
match self {
|
||||
Model::Gemini15Pro => 2_000_000,
|
||||
Model::Gemini15Flash => 1_000_000,
|
||||
Model::Gemini20Pro => 2_000_000,
|
||||
Model::Gemini20Flash => 1_000_000,
|
||||
Model::Gemini20FlashThinking => 1_000_000,
|
||||
Model::Gemini20FlashLite => 1_000_000,
|
||||
Model::Custom { max_tokens, .. } => *max_tokens,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -3,6 +3,7 @@ use crate::{
|
||||
DefiniteLength, Element, ElementId, GlobalElementId, Hitbox, Image, InteractiveElement,
|
||||
Interactivity, IntoElement, LayoutId, Length, ObjectFit, Pixels, RenderImage, Resource,
|
||||
SharedString, SharedUri, StyleRefinement, Styled, SvgSize, Task, Window,
|
||||
SMOOTH_SVG_SCALE_FACTOR,
|
||||
};
|
||||
use anyhow::{anyhow, Result};
|
||||
|
||||
@@ -610,7 +611,7 @@ impl Asset for ImageAssetLoader {
|
||||
} else {
|
||||
let pixmap =
|
||||
// TODO: Can we make svgs always rescale?
|
||||
svg_renderer.render_pixmap(&bytes, SvgSize::ScaleFactor(1.0))?;
|
||||
svg_renderer.render_pixmap(&bytes, SvgSize::ScaleFactor(SMOOTH_SVG_SCALE_FACTOR))?;
|
||||
|
||||
let mut buffer =
|
||||
ImageBuffer::from_raw(pixmap.width(), pixmap.height(), pixmap.take()).unwrap();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 [`WindowContext::handle_input`]
|
||||
@@ -159,4 +167,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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -789,6 +789,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.
|
||||
@@ -879,6 +887,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
|
||||
|
||||
@@ -186,6 +186,8 @@ pub struct X11ClientState {
|
||||
pub(crate) ximc: Option<X11rbClient<Rc<XCBConnection>>>,
|
||||
pub(crate) xim_handler: Option<XimHandler>,
|
||||
pub modifiers: Modifiers,
|
||||
// TODO: Can the other updates to `modifiers` be removed so that this is unnecessary?
|
||||
pub last_modifiers_changed_event: Modifiers,
|
||||
|
||||
pub(crate) compose_state: Option<xkbc::compose::State>,
|
||||
pub(crate) pre_edit_text: Option<String>,
|
||||
@@ -434,6 +436,7 @@ impl X11Client {
|
||||
|
||||
X11Client(Rc::new(RefCell::new(X11ClientState {
|
||||
modifiers: Modifiers::default(),
|
||||
last_modifiers_changed_event: Modifiers::default(),
|
||||
event_loop: Some(event_loop),
|
||||
loop_handle: handle,
|
||||
common,
|
||||
@@ -867,11 +870,12 @@ impl X11Client {
|
||||
}
|
||||
|
||||
let modifiers = Modifiers::from_xkb(&state.xkb);
|
||||
if state.modifiers == modifiers {
|
||||
if state.last_modifiers_changed_event == modifiers {
|
||||
drop(state);
|
||||
} else {
|
||||
let focused_window_id = state.keyboard_focused_window?;
|
||||
state.modifiers = modifiers;
|
||||
state.last_modifiers_changed_event = modifiers;
|
||||
drop(state);
|
||||
|
||||
let focused_window = self.get_window(focused_window_id)?;
|
||||
|
||||
@@ -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};
|
||||
@@ -223,6 +223,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()
|
||||
};
|
||||
}
|
||||
@@ -1683,17 +1688,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()?)
|
||||
})
|
||||
@@ -1714,6 +1709,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 =
|
||||
@@ -1827,6 +1836,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);
|
||||
|
||||
@@ -3,6 +3,9 @@ use anyhow::anyhow;
|
||||
use resvg::tiny_skia::Pixmap;
|
||||
use std::{hash::Hash, sync::Arc};
|
||||
|
||||
/// When rendering SVGs, we render them at twice the size to get a higher-quality result.
|
||||
pub const SMOOTH_SVG_SCALE_FACTOR: f32 = 2.;
|
||||
|
||||
#[derive(Clone, PartialEq, Hash, Eq)]
|
||||
pub(crate) struct RenderSvgParams {
|
||||
pub(crate) path: SharedString,
|
||||
|
||||
@@ -13,7 +13,7 @@ use crate::{
|
||||
Subscription, TaffyLayoutEngine, Task, TextStyle, TextStyleRefinement, TransformationMatrix,
|
||||
Underline, UnderlineStyle, WindowAppearance, WindowBackgroundAppearance, WindowBounds,
|
||||
WindowControls, WindowDecorations, WindowOptions, WindowParams, WindowTextSystem,
|
||||
SUBPIXEL_VARIANTS,
|
||||
SMOOTH_SVG_SCALE_FACTOR, SUBPIXEL_VARIANTS,
|
||||
};
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use collections::{FxHashMap, FxHashSet};
|
||||
@@ -107,9 +107,9 @@ impl WindowInvalidator {
|
||||
|
||||
pub fn invalidate_view(&self, entity: EntityId, cx: &mut App) -> bool {
|
||||
let mut inner = self.inner.borrow_mut();
|
||||
inner.dirty_views.insert(entity);
|
||||
if inner.draw_phase == DrawPhase::None {
|
||||
inner.dirty = true;
|
||||
inner.dirty_views.insert(entity);
|
||||
cx.push_effect(Effect::Notify { emitter: entity });
|
||||
true
|
||||
} else {
|
||||
@@ -2567,12 +2567,11 @@ impl Window {
|
||||
let element_opacity = self.element_opacity();
|
||||
let scale_factor = self.scale_factor();
|
||||
let bounds = bounds.scale(scale_factor);
|
||||
// Render the SVG at twice the size to get a higher quality result.
|
||||
let params = RenderSvgParams {
|
||||
path,
|
||||
size: bounds
|
||||
.size
|
||||
.map(|pixels| DevicePixels::from((pixels.0 * 2.).ceil() as i32)),
|
||||
size: bounds.size.map(|pixels| {
|
||||
DevicePixels::from((pixels.0 * SMOOTH_SVG_SCALE_FACTOR).ceil() as i32)
|
||||
}),
|
||||
};
|
||||
|
||||
let Some(tile) =
|
||||
|
||||
@@ -17,6 +17,31 @@ pub struct InlineCompletion {
|
||||
pub edits: Vec<(Range<language::Anchor>, String)>,
|
||||
}
|
||||
|
||||
pub enum DataCollectionState {
|
||||
/// The provider doesn't support data collection.
|
||||
Unsupported,
|
||||
/// When there's a file not saved yet. In this case, we can't tell to which project it belongs.
|
||||
Unknown,
|
||||
/// Data collection is enabled
|
||||
Enabled,
|
||||
/// Data collection is disabled or unanswered.
|
||||
Disabled,
|
||||
}
|
||||
|
||||
impl DataCollectionState {
|
||||
pub fn is_supported(&self) -> bool {
|
||||
!matches!(self, DataCollectionState::Unsupported)
|
||||
}
|
||||
|
||||
pub fn is_unknown(&self) -> bool {
|
||||
matches!(self, DataCollectionState::Unknown)
|
||||
}
|
||||
|
||||
pub fn is_enabled(&self) -> bool {
|
||||
matches!(self, DataCollectionState::Enabled)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait InlineCompletionProvider: 'static + Sized {
|
||||
fn name() -> &'static str;
|
||||
fn display_name() -> &'static str;
|
||||
@@ -25,6 +50,10 @@ pub trait InlineCompletionProvider: 'static + Sized {
|
||||
fn show_tab_accept_marker() -> bool {
|
||||
false
|
||||
}
|
||||
fn data_collection_state(&self, _cx: &App) -> DataCollectionState {
|
||||
DataCollectionState::Unsupported
|
||||
}
|
||||
fn toggle_data_collection(&mut self, _cx: &mut App) {}
|
||||
fn is_enabled(
|
||||
&self,
|
||||
buffer: &Entity<Buffer>,
|
||||
@@ -71,6 +100,8 @@ pub trait InlineCompletionProviderHandle {
|
||||
fn show_completions_in_menu(&self) -> bool;
|
||||
fn show_completions_in_normal_mode(&self) -> bool;
|
||||
fn show_tab_accept_marker(&self) -> bool;
|
||||
fn data_collection_state(&self, cx: &App) -> DataCollectionState;
|
||||
fn toggle_data_collection(&self, cx: &mut App);
|
||||
fn needs_terms_acceptance(&self, cx: &App) -> bool;
|
||||
fn is_refreshing(&self, cx: &App) -> bool;
|
||||
fn refresh(
|
||||
@@ -121,6 +152,14 @@ where
|
||||
T::show_tab_accept_marker()
|
||||
}
|
||||
|
||||
fn data_collection_state(&self, cx: &App) -> DataCollectionState {
|
||||
self.read(cx).data_collection_state(cx)
|
||||
}
|
||||
|
||||
fn toggle_data_collection(&self, cx: &mut App) {
|
||||
self.update(cx, |this, cx| this.toggle_data_collection(cx))
|
||||
}
|
||||
|
||||
fn is_enabled(
|
||||
&self,
|
||||
buffer: &Entity<Buffer>,
|
||||
|
||||
@@ -29,7 +29,7 @@ workspace.workspace = true
|
||||
zed_actions.workspace = true
|
||||
zeta.workspace = true
|
||||
client.workspace = true
|
||||
zed_predict_tos.workspace = true
|
||||
zed_predict_onboarding.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
copilot = { workspace = true, features = ["test-support"] }
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
use anyhow::Result;
|
||||
use client::UserStore;
|
||||
use client::{Client, UserStore};
|
||||
use copilot::{Copilot, Status};
|
||||
use editor::{scroll::Autoscroll, Editor};
|
||||
use editor::{actions::ShowInlineCompletion, scroll::Autoscroll, Editor};
|
||||
use feature_flags::{
|
||||
FeatureFlagAppExt, PredictEditsFeatureFlag, PredictEditsRateCompletionsFeatureFlag,
|
||||
};
|
||||
use fs::Fs;
|
||||
use gpui::{
|
||||
actions, div, pulsating_between, Action, Animation, AnimationExt, App, AsyncWindowContext,
|
||||
Corner, Entity, IntoElement, ParentElement, Render, Subscription, WeakEntity,
|
||||
Corner, Entity, FocusHandle, Focusable, IntoElement, ParentElement, Render, Subscription,
|
||||
WeakEntity,
|
||||
};
|
||||
use language::{
|
||||
language_settings::{
|
||||
@@ -19,18 +20,16 @@ use language::{
|
||||
use settings::{update_settings_file, Settings, SettingsStore};
|
||||
use std::{path::Path, sync::Arc, time::Duration};
|
||||
use supermaven::{AccountStatus, Supermaven};
|
||||
use ui::{prelude::*, ButtonLike, Color, Icon, IconWithIndicator, Indicator, PopoverMenuHandle};
|
||||
use ui::{
|
||||
prelude::*, ButtonLike, Clickable, ContextMenu, ContextMenuEntry, IconButton,
|
||||
IconWithIndicator, Indicator, PopoverMenu, PopoverMenuHandle, Tooltip,
|
||||
};
|
||||
use workspace::{
|
||||
create_and_open_local_file,
|
||||
item::ItemHandle,
|
||||
notifications::NotificationId,
|
||||
ui::{
|
||||
ButtonCommon, Clickable, ContextMenu, IconButton, IconName, IconSize, PopoverMenu, Tooltip,
|
||||
},
|
||||
StatusItemView, Toast, Workspace,
|
||||
create_and_open_local_file, item::ItemHandle, notifications::NotificationId, StatusItemView,
|
||||
Toast, Workspace,
|
||||
};
|
||||
use zed_actions::OpenBrowser;
|
||||
use zed_predict_tos::ZedPredictTos;
|
||||
use zed_predict_onboarding::ZedPredictModal;
|
||||
use zeta::RateCompletionModal;
|
||||
|
||||
actions!(zeta, [RateCompletions]);
|
||||
@@ -43,9 +42,11 @@ struct CopilotErrorToast;
|
||||
pub struct InlineCompletionButton {
|
||||
editor_subscription: Option<(Subscription, usize)>,
|
||||
editor_enabled: Option<bool>,
|
||||
editor_focus_handle: Option<FocusHandle>,
|
||||
language: Option<Arc<Language>>,
|
||||
file: Option<Arc<dyn File>>,
|
||||
inline_completion_provider: Option<Arc<dyn inline_completion::InlineCompletionProviderHandle>>,
|
||||
client: Arc<Client>,
|
||||
fs: Arc<dyn Fs>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
user_store: Entity<UserStore>,
|
||||
@@ -229,14 +230,16 @@ impl Render for InlineCompletionButton {
|
||||
return div();
|
||||
}
|
||||
|
||||
if !self
|
||||
.user_store
|
||||
.read(cx)
|
||||
.current_user_has_accepted_terms()
|
||||
.unwrap_or(false)
|
||||
{
|
||||
let current_user_terms_accepted =
|
||||
self.user_store.read(cx).current_user_has_accepted_terms();
|
||||
|
||||
if !current_user_terms_accepted.unwrap_or(false) {
|
||||
let workspace = self.workspace.clone();
|
||||
let user_store = self.user_store.clone();
|
||||
let client = self.client.clone();
|
||||
let fs = self.fs.clone();
|
||||
|
||||
let signed_in = current_user_terms_accepted.is_some();
|
||||
|
||||
return div().child(
|
||||
ButtonLike::new("zeta-pending-tos-icon")
|
||||
@@ -250,20 +253,29 @@ impl Render for InlineCompletionButton {
|
||||
))
|
||||
.into_any_element(),
|
||||
)
|
||||
.tooltip(|window, cx| {
|
||||
.tooltip(move |window, cx| {
|
||||
Tooltip::with_meta(
|
||||
"Edit Predictions",
|
||||
None,
|
||||
"Read Terms of Service",
|
||||
if signed_in {
|
||||
"Read Terms of Service"
|
||||
} else {
|
||||
"Sign in to use"
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.on_click(cx.listener(move |_, _, window, cx| {
|
||||
let user_store = user_store.clone();
|
||||
|
||||
if let Some(workspace) = workspace.upgrade() {
|
||||
ZedPredictTos::toggle(workspace, user_store, window, cx);
|
||||
ZedPredictModal::toggle(
|
||||
workspace,
|
||||
user_store.clone(),
|
||||
client.clone(),
|
||||
fs.clone(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
})),
|
||||
);
|
||||
@@ -316,6 +328,7 @@ impl InlineCompletionButton {
|
||||
workspace: WeakEntity<Workspace>,
|
||||
fs: Arc<dyn Fs>,
|
||||
user_store: Entity<UserStore>,
|
||||
client: Arc<Client>,
|
||||
popover_menu_handle: PopoverMenuHandle<ContextMenu>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
@@ -329,11 +342,13 @@ impl InlineCompletionButton {
|
||||
Self {
|
||||
editor_subscription: None,
|
||||
editor_enabled: None,
|
||||
editor_focus_handle: None,
|
||||
language: None,
|
||||
file: None,
|
||||
inline_completion_provider: None,
|
||||
popover_menu_handle,
|
||||
workspace,
|
||||
client,
|
||||
fs,
|
||||
user_store,
|
||||
}
|
||||
@@ -364,21 +379,26 @@ 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:");
|
||||
|
||||
if let Some(language) = self.language.clone() {
|
||||
let fs = fs.clone();
|
||||
let language_enabled =
|
||||
language_settings::language_settings(Some(language.name()), None, cx)
|
||||
.show_inline_completions;
|
||||
|
||||
menu = menu.entry(
|
||||
format!(
|
||||
"{} Inline Completions for {}",
|
||||
if language_enabled { "Hide" } else { "Show" },
|
||||
language.name()
|
||||
),
|
||||
menu = menu.toggleable_entry(
|
||||
language.name(),
|
||||
language_enabled,
|
||||
IconPosition::Start,
|
||||
None,
|
||||
move |_, cx| {
|
||||
toggle_inline_completions_for_language(language.clone(), fs.clone(), cx)
|
||||
@@ -387,16 +407,14 @@ impl InlineCompletionButton {
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
menu = menu.entry(
|
||||
format!(
|
||||
"{} Inline Completions for This Path",
|
||||
if path_enabled { "Hide" } else { "Show" }
|
||||
),
|
||||
menu = menu.toggleable_entry(
|
||||
"This File",
|
||||
path_enabled,
|
||||
IconPosition::Start,
|
||||
None,
|
||||
move |window, cx| {
|
||||
if let Some(workspace) = window.root().flatten() {
|
||||
@@ -416,15 +434,48 @@ impl InlineCompletionButton {
|
||||
}
|
||||
|
||||
let globally_enabled = settings.inline_completions_enabled(None, None, cx);
|
||||
menu.entry(
|
||||
if globally_enabled {
|
||||
"Hide Inline Completions for All Files"
|
||||
} else {
|
||||
"Show Inline Completions for All Files"
|
||||
},
|
||||
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().item(
|
||||
ContextMenuEntry::new("Data Collection")
|
||||
.toggleable(IconPosition::Start, data_collection.is_enabled())
|
||||
.disabled(data_collection.is_unknown())
|
||||
.handler(move |_, cx| {
|
||||
provider.toggle_data_collection(cx);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(editor_focus_handle) = self.editor_focus_handle.clone() {
|
||||
menu = menu
|
||||
.separator()
|
||||
.entry(
|
||||
"Predict Edit at Cursor",
|
||||
Some(Box::new(ShowInlineCompletion)),
|
||||
{
|
||||
let editor_focus_handle = editor_focus_handle.clone();
|
||||
|
||||
move |window, cx| {
|
||||
editor_focus_handle.dispatch_action(&ShowInlineCompletion, window, cx);
|
||||
}
|
||||
},
|
||||
)
|
||||
.context(editor_focus_handle);
|
||||
}
|
||||
|
||||
menu
|
||||
}
|
||||
|
||||
fn build_copilot_context_menu(
|
||||
@@ -468,7 +519,7 @@ impl InlineCompletionButton {
|
||||
self.build_language_settings_menu(menu, cx).when(
|
||||
cx.has_flag::<PredictEditsRateCompletionsFeatureFlag>(),
|
||||
|this| {
|
||||
this.separator().entry(
|
||||
this.entry(
|
||||
"Rate Completions",
|
||||
Some(RateCompletions.boxed_clone()),
|
||||
move |window, cx| {
|
||||
@@ -504,6 +555,7 @@ impl InlineCompletionButton {
|
||||
self.inline_completion_provider = editor.inline_completion_provider();
|
||||
self.language = language.cloned();
|
||||
self.file = file;
|
||||
self.editor_focus_handle = Some(editor.focus_handle(cx));
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
@@ -146,7 +146,9 @@ impl<T> Outline<T> {
|
||||
}
|
||||
} else {
|
||||
let mut name_ranges = outline_match.name_ranges.iter();
|
||||
let mut name_range = name_ranges.next().unwrap();
|
||||
let Some(mut name_range) = name_ranges.next() else {
|
||||
continue;
|
||||
};
|
||||
let mut preceding_ranges_len = 0;
|
||||
for position in &mut string_match.positions {
|
||||
while *position >= preceding_ranges_len + name_range.len() {
|
||||
@@ -194,6 +196,40 @@ impl<T> Outline<T> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use gpui::TestAppContext;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_entries_with_no_names(cx: &mut TestAppContext) {
|
||||
let outline = Outline::new(vec![
|
||||
OutlineItem {
|
||||
depth: 0,
|
||||
range: Point::new(0, 0)..Point::new(5, 0),
|
||||
text: "class Foo".to_string(),
|
||||
highlight_ranges: vec![],
|
||||
name_ranges: vec![6..9],
|
||||
body_range: None,
|
||||
annotation_range: None,
|
||||
},
|
||||
OutlineItem {
|
||||
depth: 0,
|
||||
range: Point::new(2, 0)..Point::new(2, 7),
|
||||
text: "private".to_string(),
|
||||
highlight_ranges: vec![],
|
||||
name_ranges: vec![],
|
||||
body_range: None,
|
||||
annotation_range: None,
|
||||
},
|
||||
]);
|
||||
assert_eq!(
|
||||
outline
|
||||
.search(" ", cx.executor())
|
||||
.await
|
||||
.into_iter()
|
||||
.map(|mat| mat.string)
|
||||
.collect::<Vec<String>>(),
|
||||
vec!["class Foo".to_string()]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find_most_similar_with_low_similarity() {
|
||||
|
||||
@@ -82,6 +82,7 @@ impl CloudModel {
|
||||
| open_ai::Model::O1Mini
|
||||
| open_ai::Model::O1Preview
|
||||
| open_ai::Model::O1
|
||||
| open_ai::Model::O3Mini
|
||||
| open_ai::Model::Custom { .. } => {
|
||||
LanguageModelAvailability::RequiresPlan(Plan::ZedPro)
|
||||
}
|
||||
@@ -89,7 +90,10 @@ impl CloudModel {
|
||||
Self::Google(model) => match model {
|
||||
google_ai::Model::Gemini15Pro
|
||||
| google_ai::Model::Gemini15Flash
|
||||
| google_ai::Model::Gemini20Pro
|
||||
| google_ai::Model::Gemini20Flash
|
||||
| google_ai::Model::Gemini20FlashThinking
|
||||
| google_ai::Model::Gemini20FlashLite
|
||||
| google_ai::Model::Custom { .. } => {
|
||||
LanguageModelAvailability::RequiresPlan(Plan::ZedPro)
|
||||
}
|
||||
|
||||
@@ -179,7 +179,7 @@ impl LanguageModel for CopilotChatLanguageModel {
|
||||
CopilotChatModel::Gpt4o => open_ai::Model::FourOmni,
|
||||
CopilotChatModel::Gpt4 => open_ai::Model::Four,
|
||||
CopilotChatModel::Gpt3_5Turbo => open_ai::Model::ThreePointFiveTurbo,
|
||||
CopilotChatModel::O1 | CopilotChatModel::O1Mini => open_ai::Model::Four,
|
||||
CopilotChatModel::O1 | CopilotChatModel::O3Mini => open_ai::Model::Four,
|
||||
CopilotChatModel::Claude3_5Sonnet => unreachable!(),
|
||||
};
|
||||
count_open_ai_tokens(request, model, cx)
|
||||
|
||||
@@ -361,7 +361,10 @@ pub fn count_open_ai_tokens(
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
match model {
|
||||
open_ai::Model::Custom { .. } | open_ai::Model::O1Mini | open_ai::Model::O1 => {
|
||||
open_ai::Model::Custom { .. }
|
||||
| open_ai::Model::O1Mini
|
||||
| open_ai::Model::O1
|
||||
| open_ai::Model::O3Mini => {
|
||||
tiktoken_rs::num_tokens_from_messages("gpt-4", &messages)
|
||||
}
|
||||
_ => tiktoken_rs::num_tokens_from_messages(model.id(), &messages),
|
||||
|
||||
@@ -192,7 +192,7 @@ pub struct ModelEntry {
|
||||
pub publisher: String,
|
||||
pub arch: Option<String>,
|
||||
pub compatibility_type: CompatibilityType,
|
||||
pub quantization: String,
|
||||
pub quantization: Option<String>,
|
||||
pub state: ModelState,
|
||||
pub max_context_length: Option<u32>,
|
||||
pub loaded_context_length: Option<u32>,
|
||||
|
||||
@@ -3484,7 +3484,8 @@ impl MultiBufferSnapshot {
|
||||
let region = cursor.region()?;
|
||||
let buffer_start = if region.is_main_buffer {
|
||||
let start_overshoot = range.start.saturating_sub(region.range.start.key);
|
||||
region.buffer_range.start.key + start_overshoot
|
||||
(region.buffer_range.start.key + start_overshoot)
|
||||
.min(region.buffer_range.end.key)
|
||||
} else {
|
||||
cursor.main_buffer_position()?.key
|
||||
};
|
||||
|
||||
@@ -78,6 +78,8 @@ pub enum Model {
|
||||
O1Preview,
|
||||
#[serde(rename = "o1-mini", alias = "o1-mini")]
|
||||
O1Mini,
|
||||
#[serde(rename = "o3-mini", alias = "o3-mini")]
|
||||
O3Mini,
|
||||
|
||||
#[serde(rename = "custom")]
|
||||
Custom {
|
||||
@@ -115,6 +117,7 @@ impl Model {
|
||||
Self::O1 => "o1",
|
||||
Self::O1Preview => "o1-preview",
|
||||
Self::O1Mini => "o1-mini",
|
||||
Self::O3Mini => "o3-mini",
|
||||
Self::Custom { name, .. } => name,
|
||||
}
|
||||
}
|
||||
@@ -129,6 +132,7 @@ impl Model {
|
||||
Self::O1 => "o1",
|
||||
Self::O1Preview => "o1-preview",
|
||||
Self::O1Mini => "o1-mini",
|
||||
Self::O3Mini => "o3-mini",
|
||||
Self::Custom {
|
||||
name, display_name, ..
|
||||
} => display_name.as_ref().unwrap_or(name),
|
||||
@@ -145,6 +149,7 @@ impl Model {
|
||||
Self::O1 => 200000,
|
||||
Self::O1Preview => 128000,
|
||||
Self::O1Mini => 128000,
|
||||
Self::O3Mini => 200000,
|
||||
Self::Custom { max_tokens, .. } => *max_tokens,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2223,7 +2223,9 @@ impl BufferChangeSet {
|
||||
let base_text = cx.background_executor().spawn(snapshot).await;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.base_text = Some(base_text);
|
||||
cx.notify();
|
||||
cx.emit(BufferChangeSetEvent::DiffChanged {
|
||||
changed_range: text::Anchor::MIN..text::Anchor::MAX,
|
||||
});
|
||||
})
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -39,6 +39,9 @@ pub struct PredictEditsParams {
|
||||
pub outline: Option<String>,
|
||||
pub input_events: String,
|
||||
pub input_excerpt: String,
|
||||
/// Whether the user provided consent for sampling this interaction.
|
||||
#[serde(default)]
|
||||
pub can_collect_data: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
|
||||
@@ -1075,6 +1075,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 {
|
||||
|
||||
@@ -3,8 +3,9 @@ use std::sync::Arc;
|
||||
use gpui::{hsla, FontStyle, FontWeight, HighlightStyle, Hsla, WindowBackgroundAppearance};
|
||||
|
||||
use crate::{
|
||||
default_color_scales, AccentColors, Appearance, PlayerColors, StatusColors, SyntaxTheme,
|
||||
SystemColors, Theme, ThemeColors, ThemeFamily, ThemeStyles,
|
||||
default_color_scales, AccentColors, Appearance, PlayerColors, StatusColors,
|
||||
StatusColorsRefinement, SyntaxTheme, SystemColors, Theme, ThemeColors, ThemeFamily,
|
||||
ThemeStyles,
|
||||
};
|
||||
|
||||
/// The default theme family for Zed.
|
||||
@@ -21,6 +22,26 @@ pub fn zed_default_themes() -> ThemeFamily {
|
||||
}
|
||||
}
|
||||
|
||||
// If a theme customizes a foreground version of a status color, but does not
|
||||
// customize the background color, then use a partly-transparent version of the
|
||||
// foreground color for the background color.
|
||||
pub(crate) fn apply_status_color_defaults(status: &mut StatusColorsRefinement) {
|
||||
for (fg_color, bg_color) in [
|
||||
(&status.deleted, &mut status.deleted_background),
|
||||
(&status.created, &mut status.created_background),
|
||||
(&status.modified, &mut status.modified_background),
|
||||
(&status.conflict, &mut status.conflict_background),
|
||||
(&status.error, &mut status.error_background),
|
||||
(&status.hidden, &mut status.hidden_background),
|
||||
] {
|
||||
if bg_color.is_none() {
|
||||
if let Some(fg_color) = fg_color {
|
||||
*bg_color = Some(fg_color.opacity(0.25));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn zed_default_dark() -> Theme {
|
||||
let bg = hsla(215. / 360., 12. / 100., 15. / 100., 1.);
|
||||
let editor = hsla(220. / 360., 12. / 100., 18. / 100., 1.);
|
||||
|
||||
@@ -212,6 +212,19 @@ impl ThemeRegistry {
|
||||
self.get_icon_theme(DEFAULT_ICON_THEME_NAME)
|
||||
}
|
||||
|
||||
/// Returns the metadata of all icon themes in the registry.
|
||||
pub fn list_icon_themes(&self) -> Vec<ThemeMeta> {
|
||||
self.state
|
||||
.read()
|
||||
.icon_themes
|
||||
.values()
|
||||
.map(|theme| ThemeMeta {
|
||||
name: theme.name.clone(),
|
||||
appearance: theme.appearance,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Returns the icon theme with the specified name.
|
||||
pub fn get_icon_theme(&self, name: &str) -> Result<Arc<IconTheme>> {
|
||||
self.state
|
||||
@@ -242,6 +255,14 @@ impl ThemeRegistry {
|
||||
) -> Result<()> {
|
||||
let icon_theme_family = read_icon_theme(icon_theme_path, fs).await?;
|
||||
|
||||
let resolve_icon_path = |path: SharedString| {
|
||||
icons_root_dir
|
||||
.join(path.as_ref())
|
||||
.to_string_lossy()
|
||||
.to_string()
|
||||
.into()
|
||||
};
|
||||
|
||||
let mut state = self.state.write();
|
||||
for icon_theme in icon_theme_family.themes {
|
||||
let icon_theme = IconTheme {
|
||||
@@ -252,23 +273,21 @@ impl ThemeRegistry {
|
||||
AppearanceContent::Dark => Appearance::Dark,
|
||||
},
|
||||
directory_icons: DirectoryIcons {
|
||||
collapsed: icon_theme.directory_icons.collapsed,
|
||||
expanded: icon_theme.directory_icons.expanded,
|
||||
collapsed: icon_theme.directory_icons.collapsed.map(resolve_icon_path),
|
||||
expanded: icon_theme.directory_icons.expanded.map(resolve_icon_path),
|
||||
},
|
||||
chevron_icons: ChevronIcons {
|
||||
collapsed: icon_theme.chevron_icons.collapsed,
|
||||
expanded: icon_theme.chevron_icons.expanded,
|
||||
collapsed: icon_theme.chevron_icons.collapsed.map(resolve_icon_path),
|
||||
expanded: icon_theme.chevron_icons.expanded.map(resolve_icon_path),
|
||||
},
|
||||
file_icons: icon_theme
|
||||
.file_icons
|
||||
.into_iter()
|
||||
.map(|(key, icon)| {
|
||||
let path = icons_root_dir.join(icon.path.as_ref());
|
||||
|
||||
(
|
||||
key,
|
||||
IconDefinition {
|
||||
path: path.to_string_lossy().to_string().into(),
|
||||
path: resolve_icon_path(icon.path),
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
@@ -319,9 +319,6 @@ pub struct ThemeSettingsContent {
|
||||
#[serde(default)]
|
||||
pub theme: Option<ThemeSelection>,
|
||||
/// The name of the icon theme to use.
|
||||
///
|
||||
/// Currently not exposed to the user.
|
||||
#[serde(skip)]
|
||||
#[serde(default)]
|
||||
pub icon_theme: Option<String>,
|
||||
|
||||
|
||||
@@ -102,10 +102,10 @@ impl StatusColors {
|
||||
conflict_background: red().dark().step_9(),
|
||||
conflict_border: red().dark().step_9(),
|
||||
created: grass().dark().step_9(),
|
||||
created_background: grass().dark().step_9(),
|
||||
created_background: grass().dark().step_9().opacity(0.25),
|
||||
created_border: grass().dark().step_9(),
|
||||
deleted: red().dark().step_9(),
|
||||
deleted_background: red().dark().step_9(),
|
||||
deleted_background: red().dark().step_9().opacity(0.25),
|
||||
deleted_border: red().dark().step_9(),
|
||||
error: red().dark().step_9(),
|
||||
error_background: red().dark().step_9(),
|
||||
@@ -123,7 +123,7 @@ impl StatusColors {
|
||||
info_background: blue().dark().step_9(),
|
||||
info_border: blue().dark().step_9(),
|
||||
modified: yellow().dark().step_9(),
|
||||
modified_background: yellow().dark().step_9(),
|
||||
modified_background: yellow().dark().step_9().opacity(0.25),
|
||||
modified_border: yellow().dark().step_9(),
|
||||
predictive: neutral().dark_alpha().step_9(),
|
||||
predictive_background: neutral().dark_alpha().step_9(),
|
||||
|
||||
@@ -24,6 +24,7 @@ use std::sync::Arc;
|
||||
|
||||
use ::settings::Settings;
|
||||
use anyhow::Result;
|
||||
use fallback_themes::apply_status_color_defaults;
|
||||
use fs::Fs;
|
||||
use gpui::{
|
||||
px, App, AssetSource, HighlightStyle, Hsla, Pixels, Refineable, SharedString, WindowAppearance,
|
||||
@@ -155,7 +156,9 @@ impl ThemeFamily {
|
||||
AppearanceContent::Light => StatusColors::light(),
|
||||
AppearanceContent::Dark => StatusColors::dark(),
|
||||
};
|
||||
refined_status_colors.refine(&theme.style.status_colors_refinement());
|
||||
let mut status_colors_refinement = theme.style.status_colors_refinement();
|
||||
apply_status_color_defaults(&mut status_colors_refinement);
|
||||
refined_status_colors.refine(&status_colors_refinement);
|
||||
|
||||
let mut refined_player_colors = match theme.appearance {
|
||||
AppearanceContent::Light => PlayerColors::light(),
|
||||
|
||||
274
crates/theme_selector/src/icon_theme_selector.rs
Normal file
274
crates/theme_selector/src/icon_theme_selector.rs
Normal file
@@ -0,0 +1,274 @@
|
||||
use fs::Fs;
|
||||
use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
|
||||
use gpui::{
|
||||
App, Context, DismissEvent, Entity, EventEmitter, Focusable, Render, UpdateGlobal, WeakEntity,
|
||||
Window,
|
||||
};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use settings::{update_settings_file, Settings as _, SettingsStore};
|
||||
use std::sync::Arc;
|
||||
use theme::{IconTheme, ThemeMeta, ThemeRegistry, ThemeSettings};
|
||||
use ui::{prelude::*, v_flex, ListItem, ListItemSpacing};
|
||||
use util::ResultExt;
|
||||
use workspace::{ui::HighlightedLabel, ModalView};
|
||||
|
||||
pub(crate) struct IconThemeSelector {
|
||||
picker: Entity<Picker<IconThemeSelectorDelegate>>,
|
||||
}
|
||||
|
||||
impl EventEmitter<DismissEvent> for IconThemeSelector {}
|
||||
|
||||
impl Focusable for IconThemeSelector {
|
||||
fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
|
||||
self.picker.focus_handle(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl ModalView for IconThemeSelector {}
|
||||
|
||||
impl IconThemeSelector {
|
||||
pub fn new(
|
||||
delegate: IconThemeSelectorDelegate,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
|
||||
Self { picker }
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for IconThemeSelector {
|
||||
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||
v_flex().w(rems(34.)).child(self.picker.clone())
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct IconThemeSelectorDelegate {
|
||||
fs: Arc<dyn Fs>,
|
||||
themes: Vec<ThemeMeta>,
|
||||
matches: Vec<StringMatch>,
|
||||
original_theme: Arc<IconTheme>,
|
||||
selection_completed: bool,
|
||||
selected_index: usize,
|
||||
selector: WeakEntity<IconThemeSelector>,
|
||||
}
|
||||
|
||||
impl IconThemeSelectorDelegate {
|
||||
pub fn new(
|
||||
selector: WeakEntity<IconThemeSelector>,
|
||||
fs: Arc<dyn Fs>,
|
||||
themes_filter: Option<&Vec<String>>,
|
||||
cx: &mut Context<IconThemeSelector>,
|
||||
) -> Self {
|
||||
let theme_settings = ThemeSettings::get_global(cx);
|
||||
let original_theme = theme_settings.active_icon_theme.clone();
|
||||
|
||||
let registry = ThemeRegistry::global(cx);
|
||||
let mut themes = registry
|
||||
.list_icon_themes()
|
||||
.into_iter()
|
||||
.filter(|meta| {
|
||||
if let Some(theme_filter) = themes_filter {
|
||||
theme_filter.contains(&meta.name.to_string())
|
||||
} else {
|
||||
true
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
themes.sort_unstable_by(|a, b| {
|
||||
a.appearance
|
||||
.is_light()
|
||||
.cmp(&b.appearance.is_light())
|
||||
.then(a.name.cmp(&b.name))
|
||||
});
|
||||
let matches = themes
|
||||
.iter()
|
||||
.map(|meta| StringMatch {
|
||||
candidate_id: 0,
|
||||
score: 0.0,
|
||||
positions: Default::default(),
|
||||
string: meta.name.to_string(),
|
||||
})
|
||||
.collect();
|
||||
let mut this = Self {
|
||||
fs,
|
||||
themes,
|
||||
matches,
|
||||
original_theme: original_theme.clone(),
|
||||
selected_index: 0,
|
||||
selection_completed: false,
|
||||
selector,
|
||||
};
|
||||
|
||||
this.select_if_matching(&original_theme.name);
|
||||
this
|
||||
}
|
||||
|
||||
fn show_selected_theme(&mut self, cx: &mut Context<Picker<IconThemeSelectorDelegate>>) {
|
||||
if let Some(mat) = self.matches.get(self.selected_index) {
|
||||
let registry = ThemeRegistry::global(cx);
|
||||
match registry.get_icon_theme(&mat.string) {
|
||||
Ok(theme) => {
|
||||
Self::set_icon_theme(theme, cx);
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!("error loading icon theme {}: {err}", mat.string);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn select_if_matching(&mut self, theme_name: &str) {
|
||||
self.selected_index = self
|
||||
.matches
|
||||
.iter()
|
||||
.position(|mat| mat.string == theme_name)
|
||||
.unwrap_or(self.selected_index);
|
||||
}
|
||||
|
||||
fn set_icon_theme(theme: Arc<IconTheme>, cx: &mut App) {
|
||||
SettingsStore::update_global(cx, |store, cx| {
|
||||
let mut theme_settings = store.get::<ThemeSettings>(None).clone();
|
||||
theme_settings.active_icon_theme = theme;
|
||||
store.override_global(theme_settings);
|
||||
cx.refresh_windows();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl PickerDelegate for IconThemeSelectorDelegate {
|
||||
type ListItem = ui::ListItem;
|
||||
|
||||
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
|
||||
"Select Icon Theme...".into()
|
||||
}
|
||||
|
||||
fn match_count(&self) -> usize {
|
||||
self.matches.len()
|
||||
}
|
||||
|
||||
fn confirm(
|
||||
&mut self,
|
||||
_: bool,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Picker<IconThemeSelectorDelegate>>,
|
||||
) {
|
||||
self.selection_completed = true;
|
||||
|
||||
let theme_settings = ThemeSettings::get_global(cx);
|
||||
let theme_name = theme_settings.active_icon_theme.name.clone();
|
||||
|
||||
telemetry::event!(
|
||||
"Settings Changed",
|
||||
setting = "icon_theme",
|
||||
value = theme_name
|
||||
);
|
||||
|
||||
update_settings_file::<ThemeSettings>(self.fs.clone(), cx, move |settings, _| {
|
||||
settings.icon_theme = Some(theme_name.to_string());
|
||||
});
|
||||
|
||||
self.selector
|
||||
.update(cx, |_, cx| {
|
||||
cx.emit(DismissEvent);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<IconThemeSelectorDelegate>>) {
|
||||
if !self.selection_completed {
|
||||
Self::set_icon_theme(self.original_theme.clone(), cx);
|
||||
self.selection_completed = true;
|
||||
}
|
||||
|
||||
self.selector
|
||||
.update(cx, |_, cx| cx.emit(DismissEvent))
|
||||
.log_err();
|
||||
}
|
||||
|
||||
fn selected_index(&self) -> usize {
|
||||
self.selected_index
|
||||
}
|
||||
|
||||
fn set_selected_index(
|
||||
&mut self,
|
||||
ix: usize,
|
||||
_: &mut Window,
|
||||
cx: &mut Context<Picker<IconThemeSelectorDelegate>>,
|
||||
) {
|
||||
self.selected_index = ix;
|
||||
self.show_selected_theme(cx);
|
||||
}
|
||||
|
||||
fn update_matches(
|
||||
&mut self,
|
||||
query: String,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Picker<IconThemeSelectorDelegate>>,
|
||||
) -> gpui::Task<()> {
|
||||
let background = cx.background_executor().clone();
|
||||
let candidates = self
|
||||
.themes
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(id, meta)| StringMatchCandidate::new(id, &meta.name))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
cx.spawn_in(window, |this, mut cx| async move {
|
||||
let matches = if query.is_empty() {
|
||||
candidates
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(index, candidate)| StringMatch {
|
||||
candidate_id: index,
|
||||
string: candidate.string,
|
||||
positions: Vec::new(),
|
||||
score: 0.0,
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
match_strings(
|
||||
&candidates,
|
||||
&query,
|
||||
false,
|
||||
100,
|
||||
&Default::default(),
|
||||
background,
|
||||
)
|
||||
.await
|
||||
};
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.delegate.matches = matches;
|
||||
this.delegate.selected_index = this
|
||||
.delegate
|
||||
.selected_index
|
||||
.min(this.delegate.matches.len().saturating_sub(1));
|
||||
this.delegate.show_selected_theme(cx);
|
||||
})
|
||||
.log_err();
|
||||
})
|
||||
}
|
||||
|
||||
fn render_match(
|
||||
&self,
|
||||
ix: usize,
|
||||
selected: bool,
|
||||
_window: &mut Window,
|
||||
_cx: &mut Context<Picker<Self>>,
|
||||
) -> Option<Self::ListItem> {
|
||||
let theme_match = &self.matches[ix];
|
||||
|
||||
Some(
|
||||
ListItem::new(ix)
|
||||
.inset(true)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.toggle_state(selected)
|
||||
.child(HighlightedLabel::new(
|
||||
theme_match.string.clone(),
|
||||
theme_match.positions.clone(),
|
||||
)),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
mod icon_theme_selector;
|
||||
|
||||
use fs::Fs;
|
||||
use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
|
||||
use gpui::{
|
||||
@@ -11,22 +13,25 @@ use theme::{Appearance, Theme, ThemeMeta, ThemeRegistry, ThemeSettings};
|
||||
use ui::{prelude::*, v_flex, ListItem, ListItemSpacing};
|
||||
use util::ResultExt;
|
||||
use workspace::{ui::HighlightedLabel, ModalView, Workspace};
|
||||
use zed_actions::theme_selector::Toggle;
|
||||
|
||||
use crate::icon_theme_selector::{IconThemeSelector, IconThemeSelectorDelegate};
|
||||
|
||||
actions!(theme_selector, [Reload]);
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
cx.observe_new(
|
||||
|workspace: &mut Workspace, _window, _cx: &mut Context<Workspace>| {
|
||||
workspace.register_action(toggle);
|
||||
workspace
|
||||
.register_action(toggle_theme_selector)
|
||||
.register_action(toggle_icon_theme_selector);
|
||||
},
|
||||
)
|
||||
.detach();
|
||||
}
|
||||
|
||||
pub fn toggle(
|
||||
fn toggle_theme_selector(
|
||||
workspace: &mut Workspace,
|
||||
toggle: &Toggle,
|
||||
toggle: &zed_actions::theme_selector::Toggle,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Workspace>,
|
||||
) {
|
||||
@@ -42,9 +47,27 @@ pub fn toggle(
|
||||
});
|
||||
}
|
||||
|
||||
fn toggle_icon_theme_selector(
|
||||
workspace: &mut Workspace,
|
||||
toggle: &zed_actions::icon_theme_selector::Toggle,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Workspace>,
|
||||
) {
|
||||
let fs = workspace.app_state().fs.clone();
|
||||
workspace.toggle_modal(window, cx, |window, cx| {
|
||||
let delegate = IconThemeSelectorDelegate::new(
|
||||
cx.entity().downgrade(),
|
||||
fs,
|
||||
toggle.themes_filter.as_ref(),
|
||||
cx,
|
||||
);
|
||||
IconThemeSelector::new(delegate, window, cx)
|
||||
});
|
||||
}
|
||||
|
||||
impl ModalView for ThemeSelector {}
|
||||
|
||||
pub struct ThemeSelector {
|
||||
struct ThemeSelector {
|
||||
picker: Entity<Picker<ThemeSelectorDelegate>>,
|
||||
}
|
||||
|
||||
@@ -73,7 +96,7 @@ impl ThemeSelector {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ThemeSelectorDelegate {
|
||||
struct ThemeSelectorDelegate {
|
||||
fs: Arc<dyn Fs>,
|
||||
themes: Vec<ThemeMeta>,
|
||||
matches: Vec<StringMatch>,
|
||||
|
||||
@@ -48,6 +48,7 @@ telemetry.workspace = true
|
||||
workspace.workspace = true
|
||||
zed_actions.workspace = true
|
||||
git_ui.workspace = true
|
||||
zed_predict_onboarding.workspace = true
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
windows.workspace = true
|
||||
|
||||
@@ -37,6 +37,7 @@ use ui::{
|
||||
use util::ResultExt;
|
||||
use workspace::{notifications::NotifyResultExt, Workspace};
|
||||
use zed_actions::{OpenBrowser, OpenRecent, OpenRemote};
|
||||
use zed_predict_onboarding::ZedPredictBanner;
|
||||
|
||||
#[cfg(feature = "stories")]
|
||||
pub use stories::*;
|
||||
@@ -113,6 +114,7 @@ pub struct TitleBar {
|
||||
application_menu: Option<Entity<ApplicationMenu>>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
git_ui_enabled: Arc<AtomicBool>,
|
||||
zed_predict_banner: Entity<ZedPredictBanner>,
|
||||
}
|
||||
|
||||
impl Render for TitleBar {
|
||||
@@ -196,6 +198,7 @@ impl Render for TitleBar {
|
||||
.on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation()),
|
||||
)
|
||||
.child(self.render_collaborator_list(window, cx))
|
||||
.child(self.zed_predict_banner.clone())
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
@@ -271,6 +274,7 @@ impl TitleBar {
|
||||
let project = workspace.project().clone();
|
||||
let user_store = workspace.app_state().user_store.clone();
|
||||
let client = workspace.app_state().client.clone();
|
||||
let fs = workspace.app_state().fs.clone();
|
||||
let active_call = ActiveCall::global(cx);
|
||||
|
||||
let platform_style = PlatformStyle::platform();
|
||||
@@ -306,6 +310,16 @@ impl TitleBar {
|
||||
}
|
||||
}));
|
||||
|
||||
let zed_predict_banner = cx.new(|cx| {
|
||||
ZedPredictBanner::new(
|
||||
workspace.weak_handle(),
|
||||
user_store.clone(),
|
||||
client.clone(),
|
||||
fs.clone(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
Self {
|
||||
platform_style,
|
||||
content: div().id(id.into()),
|
||||
@@ -319,6 +333,7 @@ impl TitleBar {
|
||||
client,
|
||||
_subscriptions: subscriptions,
|
||||
git_ui_enabled: is_git_ui_enabled,
|
||||
zed_predict_banner,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -64,6 +64,11 @@ impl ContextMenuEntry {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toggleable(mut self, toggle_position: IconPosition, toggled: bool) -> Self {
|
||||
self.toggle = Some((toggle_position, toggled));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn icon(mut self, icon: IconName) -> Self {
|
||||
self.icon = Some(icon);
|
||||
self
|
||||
|
||||
@@ -49,7 +49,7 @@ impl RenderOnce for ListSubHeader {
|
||||
.px(DynamicSpacing::Base02.rems(cx))
|
||||
.child(
|
||||
div()
|
||||
.h_6()
|
||||
.h_5()
|
||||
.when(self.inset, |this| this.px_2())
|
||||
.when(self.selected, |this| {
|
||||
this.bg(cx.theme().colors().ghost_element_selected)
|
||||
@@ -70,7 +70,11 @@ impl RenderOnce for ListSubHeader {
|
||||
Icon::new(i).color(Color::Muted).size(IconSize::Small)
|
||||
}),
|
||||
)
|
||||
.child(Label::new(self.label.clone()).color(Color::Muted)),
|
||||
.child(
|
||||
Label::new(self.label.clone())
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::Small),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -379,6 +379,12 @@ pub mod simple_message_notification {
|
||||
click_message: Option<SharedString>,
|
||||
secondary_click_message: Option<SharedString>,
|
||||
secondary_on_click: Option<Arc<dyn Fn(&mut Window, &mut Context<Self>)>>,
|
||||
tertiary_click_message: Option<SharedString>,
|
||||
tertiary_on_click: Option<Arc<dyn Fn(&mut Window, &mut Context<Self>)>>,
|
||||
more_info_message: Option<SharedString>,
|
||||
more_info_url: Option<Arc<str>>,
|
||||
show_close_button: bool,
|
||||
title: Option<SharedString>,
|
||||
}
|
||||
|
||||
impl EventEmitter<DismissEvent> for MessageNotification {}
|
||||
@@ -402,6 +408,12 @@ pub mod simple_message_notification {
|
||||
click_message: None,
|
||||
secondary_on_click: None,
|
||||
secondary_click_message: None,
|
||||
tertiary_on_click: None,
|
||||
tertiary_click_message: None,
|
||||
more_info_message: None,
|
||||
more_info_url: None,
|
||||
show_close_button: true,
|
||||
title: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -437,31 +449,85 @@ pub mod simple_message_notification {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_tertiary_click_message<S>(mut self, message: S) -> Self
|
||||
where
|
||||
S: Into<SharedString>,
|
||||
{
|
||||
self.tertiary_click_message = Some(message.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_tertiary_click<F>(mut self, on_click: F) -> Self
|
||||
where
|
||||
F: 'static + Fn(&mut Window, &mut Context<Self>),
|
||||
{
|
||||
self.tertiary_on_click = Some(Arc::new(on_click));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn more_info_message<S>(mut self, message: S) -> Self
|
||||
where
|
||||
S: Into<SharedString>,
|
||||
{
|
||||
self.more_info_message = Some(message.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn more_info_url<S>(mut self, url: S) -> Self
|
||||
where
|
||||
S: Into<Arc<str>>,
|
||||
{
|
||||
self.more_info_url = Some(url.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn dismiss(&mut self, cx: &mut Context<Self>) {
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
|
||||
pub fn show_close_button(mut self, show: bool) -> Self {
|
||||
self.show_close_button = show;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_title<S>(mut self, title: S) -> Self
|
||||
where
|
||||
S: Into<SharedString>,
|
||||
{
|
||||
self.title = Some(title.into());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for MessageNotification {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
v_flex()
|
||||
.p_3()
|
||||
.gap_2()
|
||||
.gap_3()
|
||||
.elevation_3(cx)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_4()
|
||||
.justify_between()
|
||||
.items_start()
|
||||
.child(div().max_w_96().child((self.build_content)(window, cx)))
|
||||
.child(
|
||||
IconButton::new("close", IconName::Close)
|
||||
.on_click(cx.listener(|this, _, _, cx| this.dismiss(cx))),
|
||||
),
|
||||
v_flex()
|
||||
.gap_0p5()
|
||||
.when_some(self.title.clone(), |element, title| {
|
||||
element.child(Label::new(title))
|
||||
})
|
||||
.child(div().max_w_96().child((self.build_content)(window, cx))),
|
||||
)
|
||||
.when(self.show_close_button, |this| {
|
||||
this.child(
|
||||
IconButton::new("close", IconName::Close)
|
||||
.on_click(cx.listener(|this, _, _, cx| this.dismiss(cx))),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.gap_1()
|
||||
.children(self.click_message.iter().map(|message| {
|
||||
Button::new(message.clone(), message.clone())
|
||||
.label_size(LabelSize::Small)
|
||||
@@ -489,7 +555,40 @@ pub mod simple_message_notification {
|
||||
};
|
||||
this.dismiss(cx)
|
||||
}))
|
||||
})),
|
||||
}))
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.gap_1()
|
||||
.justify_end()
|
||||
.children(self.tertiary_click_message.iter().map(|message| {
|
||||
Button::new(message.clone(), message.clone())
|
||||
.label_size(LabelSize::Small)
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
if let Some(on_click) = this.tertiary_on_click.as_ref()
|
||||
{
|
||||
(on_click)(window, cx)
|
||||
};
|
||||
this.dismiss(cx)
|
||||
}))
|
||||
}))
|
||||
.children(
|
||||
self.more_info_message
|
||||
.iter()
|
||||
.zip(self.more_info_url.iter())
|
||||
.map(|(message, url)| {
|
||||
let url = url.clone();
|
||||
Button::new(message.clone(), message.clone())
|
||||
.label_size(LabelSize::Small)
|
||||
.icon(IconName::ArrowUpRight)
|
||||
.icon_size(IconSize::Indicator)
|
||||
.icon_color(Color::Muted)
|
||||
.on_click(cx.listener(move |_, _, _, cx| {
|
||||
cx.open_url(&url);
|
||||
}))
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,7 +58,9 @@ use persistence::{
|
||||
SerializedWindowBounds, DB,
|
||||
};
|
||||
use postage::stream::Stream;
|
||||
use project::{DirectoryLister, Project, ProjectEntryId, ProjectPath, ResolvedPath, Worktree};
|
||||
use project::{
|
||||
DirectoryLister, Project, ProjectEntryId, ProjectPath, ResolvedPath, Worktree, WorktreeId,
|
||||
};
|
||||
use remote::{ssh_session::ConnectionIdentifier, SshClientDelegate, SshConnectionOptions};
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
@@ -1282,7 +1284,10 @@ impl Workspace {
|
||||
.unwrap_or_default();
|
||||
|
||||
window
|
||||
.update(&mut cx, |_, window, _| window.activate_window())
|
||||
.update(&mut cx, |_, window, cx| {
|
||||
window.activate_window();
|
||||
cx.activate(true);
|
||||
})
|
||||
.log_err();
|
||||
Ok((window, opened_items))
|
||||
})
|
||||
@@ -2200,6 +2205,18 @@ impl Workspace {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn absolute_path_of_worktree(
|
||||
&self,
|
||||
worktree_id: WorktreeId,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<PathBuf> {
|
||||
self.project
|
||||
.read(cx)
|
||||
.worktree_for_id(worktree_id, cx)
|
||||
// TODO: use `abs_path` or `root_dir`
|
||||
.map(|wt| wt.read(cx).abs_path().as_ref().to_path_buf())
|
||||
}
|
||||
|
||||
fn add_folder_to_project(
|
||||
&mut self,
|
||||
_: &AddFolderToProject,
|
||||
|
||||
@@ -2670,21 +2670,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,
|
||||
@@ -2756,6 +2745,8 @@ impl Snapshot {
|
||||
self.entry_for_path("")
|
||||
}
|
||||
|
||||
/// TODO: what's the difference between `root_dir` and `abs_path`?
|
||||
/// is there any? if so, document it.
|
||||
pub fn root_dir(&self) -> Option<Arc<Path>> {
|
||||
self.root_entry()
|
||||
.filter(|entry| entry.is_dir())
|
||||
@@ -5974,7 +5965,6 @@ impl<'a> Iterator for Traversal<'a> {
|
||||
enum PathTarget<'a> {
|
||||
Path(&'a Path),
|
||||
Successor(&'a Path),
|
||||
Contains(&'a Path),
|
||||
}
|
||||
|
||||
impl<'a> PathTarget<'a> {
|
||||
@@ -5988,13 +5978,6 @@ impl<'a> PathTarget<'a> {
|
||||
Ordering::Equal
|
||||
}
|
||||
}
|
||||
PathTarget::Contains(path) => {
|
||||
if path.starts_with(other) {
|
||||
Ordering::Equal
|
||||
} else {
|
||||
Ordering::Greater
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
description = "The fast, collaborative code editor."
|
||||
edition.workspace = true
|
||||
name = "zed"
|
||||
version = "0.172.0"
|
||||
version = "0.172.11"
|
||||
publish.workspace = true
|
||||
license = "GPL-3.0-or-later"
|
||||
authors = ["Zed Team <hi@zed.dev>"]
|
||||
@@ -16,7 +16,7 @@ path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
activity_indicator.workspace = true
|
||||
zed_predict_tos.workspace = true
|
||||
zed_predict_onboarding.workspace = true
|
||||
anyhow.workspace = true
|
||||
assets.workspace = true
|
||||
assistant.workspace = true
|
||||
|
||||
@@ -1 +1 @@
|
||||
dev
|
||||
stable
|
||||
@@ -434,6 +434,7 @@ fn main() {
|
||||
inline_completion_registry::init(
|
||||
app_state.client.clone(),
|
||||
app_state.user_store.clone(),
|
||||
app_state.fs.clone(),
|
||||
cx,
|
||||
);
|
||||
let prompt_builder = PromptBuilder::load(app_state.fs.clone(), stdout_is_a_pty(), cx);
|
||||
|
||||
@@ -45,6 +45,7 @@ use settings::{
|
||||
};
|
||||
use std::any::TypeId;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::atomic::{self, AtomicBool};
|
||||
use std::{borrow::Cow, ops::Deref, path::Path, sync::Arc};
|
||||
use terminal_view::terminal_panel::{self, TerminalPanel};
|
||||
use theme::{ActiveTheme, ThemeSettings};
|
||||
@@ -176,6 +177,7 @@ pub fn initialize_workspace(
|
||||
workspace.weak_handle(),
|
||||
app_state.fs.clone(),
|
||||
app_state.user_store.clone(),
|
||||
app_state.client.clone(),
|
||||
popover_menu_handle.clone(),
|
||||
cx,
|
||||
)
|
||||
@@ -964,7 +966,12 @@ fn install_cli(
|
||||
.detach_and_prompt_err("Error installing zed cli", window, cx, |_, _, _| None);
|
||||
}
|
||||
|
||||
static WAITING_QUIT_CONFIRMATION: AtomicBool = AtomicBool::new(false);
|
||||
fn quit(_: &Quit, cx: &mut App) {
|
||||
if WAITING_QUIT_CONFIRMATION.load(atomic::Ordering::Acquire) {
|
||||
return;
|
||||
}
|
||||
|
||||
let should_confirm = WorkspaceSettings::get_global(cx).confirm_quit;
|
||||
cx.spawn(|mut cx| async move {
|
||||
let mut workspace_windows = cx.update(|cx| {
|
||||
@@ -981,23 +988,27 @@ fn quit(_: &Quit, cx: &mut App) {
|
||||
})
|
||||
.log_err();
|
||||
|
||||
if let (true, Some(workspace)) = (should_confirm, workspace_windows.first().copied()) {
|
||||
let answer = workspace
|
||||
.update(&mut cx, |_, window, cx| {
|
||||
window.prompt(
|
||||
PromptLevel::Info,
|
||||
"Are you sure you want to quit?",
|
||||
None,
|
||||
&["Quit", "Cancel"],
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.log_err();
|
||||
if should_confirm {
|
||||
if let Some(workspace) = workspace_windows.first() {
|
||||
let answer = workspace
|
||||
.update(&mut cx, |_, window, cx| {
|
||||
window.prompt(
|
||||
PromptLevel::Info,
|
||||
"Are you sure you want to quit?",
|
||||
None,
|
||||
&["Quit", "Cancel"],
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.log_err();
|
||||
|
||||
if let Some(answer) = answer {
|
||||
let answer = answer.await.ok();
|
||||
if answer != Some(0) {
|
||||
return Ok(());
|
||||
if let Some(answer) = answer {
|
||||
WAITING_QUIT_CONFIRMATION.store(true, atomic::Ordering::Release);
|
||||
let answer = answer.await.ok();
|
||||
WAITING_QUIT_CONFIRMATION.store(false, atomic::Ordering::Release);
|
||||
if answer != Some(0) {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,13 +5,17 @@ use collections::HashMap;
|
||||
use copilot::{Copilot, CopilotCompletionProvider};
|
||||
use editor::{Editor, EditorMode};
|
||||
use feature_flags::{FeatureFlagAppExt, PredictEditsFeatureFlag};
|
||||
use gpui::{AnyWindowHandle, App, AppContext as _, Context, Entity, WeakEntity, Window};
|
||||
use fs::Fs;
|
||||
use gpui::{AnyWindowHandle, App, AppContext, Context, Entity, WeakEntity};
|
||||
use language::language_settings::{all_language_settings, InlineCompletionProvider};
|
||||
use settings::SettingsStore;
|
||||
use supermaven::{Supermaven, SupermavenCompletionProvider};
|
||||
use zed_predict_tos::ZedPredictTos;
|
||||
use ui::Window;
|
||||
use workspace::Workspace;
|
||||
use zed_predict_onboarding::ZedPredictModal;
|
||||
use zeta::ProviderDataCollection;
|
||||
|
||||
pub fn init(client: Arc<Client>, user_store: Entity<UserStore>, cx: &mut App) {
|
||||
pub fn init(client: Arc<Client>, user_store: Entity<UserStore>, fs: Arc<dyn Fs>, cx: &mut App) {
|
||||
let editors: Rc<RefCell<HashMap<WeakEntity<Editor>, AnyWindowHandle>>> = Rc::default();
|
||||
cx.observe_new({
|
||||
let editors = editors.clone();
|
||||
@@ -37,6 +41,7 @@ pub fn init(client: Arc<Client>, user_store: Entity<UserStore>, cx: &mut App) {
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
editors
|
||||
.borrow_mut()
|
||||
.insert(editor_handle, window.window_handle());
|
||||
@@ -91,6 +96,7 @@ pub fn init(client: Arc<Client>, user_store: Entity<UserStore>, cx: &mut App) {
|
||||
let editors = editors.clone();
|
||||
let client = client.clone();
|
||||
let user_store = user_store.clone();
|
||||
let fs = fs.clone();
|
||||
move |cx| {
|
||||
let new_provider = all_language_settings(None, cx).inline_completions.provider;
|
||||
if new_provider != provider {
|
||||
@@ -123,9 +129,11 @@ pub fn init(client: Arc<Client>, user_store: Entity<UserStore>, cx: &mut App) {
|
||||
|
||||
window
|
||||
.update(cx, |_, window, cx| {
|
||||
ZedPredictTos::toggle(
|
||||
ZedPredictModal::toggle(
|
||||
workspace,
|
||||
user_store.clone(),
|
||||
client.clone(),
|
||||
fs.clone(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
@@ -214,17 +222,19 @@ fn register_backward_compatible_actions(editor: &mut Editor, cx: &mut Context<Ed
|
||||
|
||||
fn assign_inline_completion_provider(
|
||||
editor: &mut Editor,
|
||||
provider: language::language_settings::InlineCompletionProvider,
|
||||
provider: InlineCompletionProvider,
|
||||
client: &Arc<Client>,
|
||||
user_store: Entity<UserStore>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Editor>,
|
||||
) {
|
||||
let singleton_buffer = editor.buffer().read(cx).as_singleton();
|
||||
|
||||
match provider {
|
||||
language::language_settings::InlineCompletionProvider::None => {}
|
||||
language::language_settings::InlineCompletionProvider::Copilot => {
|
||||
InlineCompletionProvider::None => {}
|
||||
InlineCompletionProvider::Copilot => {
|
||||
if let Some(copilot) = Copilot::global(cx) {
|
||||
if let Some(buffer) = editor.buffer().read(cx).as_singleton() {
|
||||
if let Some(buffer) = singleton_buffer {
|
||||
if buffer.read(cx).file().is_some() {
|
||||
copilot.update(cx, |copilot, cx| {
|
||||
copilot.register_buffer(&buffer, cx);
|
||||
@@ -235,26 +245,35 @@ fn assign_inline_completion_provider(
|
||||
editor.set_inline_completion_provider(Some(provider), window, cx);
|
||||
}
|
||||
}
|
||||
language::language_settings::InlineCompletionProvider::Supermaven => {
|
||||
InlineCompletionProvider::Supermaven => {
|
||||
if let Some(supermaven) = Supermaven::global(cx) {
|
||||
let provider = cx.new(|_| SupermavenCompletionProvider::new(supermaven));
|
||||
editor.set_inline_completion_provider(Some(provider), window, cx);
|
||||
}
|
||||
}
|
||||
|
||||
language::language_settings::InlineCompletionProvider::Zed => {
|
||||
InlineCompletionProvider::Zed => {
|
||||
if cx.has_flag::<PredictEditsFeatureFlag>()
|
||||
|| (cfg!(debug_assertions) && client.status().borrow().is_connected())
|
||||
{
|
||||
let zeta = zeta::Zeta::register(client.clone(), user_store, cx);
|
||||
if let Some(buffer) = editor.buffer().read(cx).as_singleton() {
|
||||
if let Some(buffer) = &singleton_buffer {
|
||||
if buffer.read(cx).file().is_some() {
|
||||
zeta.update(cx, |zeta, cx| {
|
||||
zeta.register_buffer(&buffer, cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
let provider = cx.new(|_| zeta::ZetaInlineCompletionProvider::new(zeta));
|
||||
|
||||
let data_collection = ProviderDataCollection::new(
|
||||
zeta.clone(),
|
||||
window.root::<Workspace>().flatten(),
|
||||
singleton_buffer,
|
||||
cx,
|
||||
);
|
||||
|
||||
let provider =
|
||||
cx.new(|_| zeta::ZetaInlineCompletionProvider::new(zeta, data_collection));
|
||||
|
||||
editor.set_inline_completion_provider(Some(provider), window, cx);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,6 +77,20 @@ pub mod theme_selector {
|
||||
impl_actions!(theme_selector, [Toggle]);
|
||||
}
|
||||
|
||||
pub mod icon_theme_selector {
|
||||
use gpui::impl_actions;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema)]
|
||||
pub struct Toggle {
|
||||
/// A list of icon theme names to filter the theme selector down to.
|
||||
pub themes_filter: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
impl_actions!(icon_theme_selector, [Toggle]);
|
||||
}
|
||||
|
||||
pub mod assistant {
|
||||
use gpui::{actions, impl_actions};
|
||||
use schemars::JsonSchema;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
[package]
|
||||
name = "zed_predict_tos"
|
||||
name = "zed_predict_onboarding"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
@@ -9,15 +9,23 @@ license = "GPL-3.0-or-later"
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/zed_predict_tos.rs"
|
||||
path = "src/lib.rs"
|
||||
doctest = false
|
||||
|
||||
[features]
|
||||
test-support = []
|
||||
|
||||
[dependencies]
|
||||
chrono.workspace = true
|
||||
client.workspace = true
|
||||
db.workspace = true
|
||||
feature_flags.workspace = true
|
||||
fs.workspace = true
|
||||
gpui.workspace = true
|
||||
ui.workspace = true
|
||||
workspace.workspace = true
|
||||
language.workspace = true
|
||||
menu.workspace = true
|
||||
settings.workspace = true
|
||||
theme.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
workspace.workspace = true
|
||||
168
crates/zed_predict_onboarding/src/banner.rs
Normal file
168
crates/zed_predict_onboarding/src/banner.rs
Normal file
@@ -0,0 +1,168 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::ZedPredictModal;
|
||||
use chrono::Utc;
|
||||
use client::{Client, UserStore};
|
||||
use feature_flags::{FeatureFlagAppExt as _, PredictEditsFeatureFlag};
|
||||
use fs::Fs;
|
||||
use gpui::{Entity, Subscription, WeakEntity};
|
||||
use language::language_settings::{all_language_settings, InlineCompletionProvider};
|
||||
use settings::SettingsStore;
|
||||
use ui::{prelude::*, ButtonLike, Tooltip};
|
||||
use util::ResultExt;
|
||||
use workspace::Workspace;
|
||||
|
||||
/// Prompts user to try AI inline prediction feature
|
||||
pub struct ZedPredictBanner {
|
||||
workspace: WeakEntity<Workspace>,
|
||||
user_store: Entity<UserStore>,
|
||||
client: Arc<Client>,
|
||||
fs: Arc<dyn Fs>,
|
||||
dismissed: bool,
|
||||
_subscription: Subscription,
|
||||
}
|
||||
|
||||
impl ZedPredictBanner {
|
||||
pub fn new(
|
||||
workspace: WeakEntity<Workspace>,
|
||||
user_store: Entity<UserStore>,
|
||||
client: Arc<Client>,
|
||||
fs: Arc<dyn Fs>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
Self {
|
||||
workspace,
|
||||
user_store,
|
||||
client,
|
||||
fs,
|
||||
dismissed: get_dismissed(),
|
||||
_subscription: cx.observe_global::<SettingsStore>(Self::handle_settings_changed),
|
||||
}
|
||||
}
|
||||
|
||||
fn should_show(&self, cx: &mut App) -> bool {
|
||||
if !cx.has_flag::<PredictEditsFeatureFlag>() || self.dismissed {
|
||||
return false;
|
||||
}
|
||||
|
||||
let provider = all_language_settings(None, cx).inline_completions.provider;
|
||||
|
||||
match provider {
|
||||
InlineCompletionProvider::None
|
||||
| InlineCompletionProvider::Copilot
|
||||
| InlineCompletionProvider::Supermaven => true,
|
||||
InlineCompletionProvider::Zed => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_settings_changed(&mut self, cx: &mut Context<Self>) {
|
||||
if self.dismissed {
|
||||
return;
|
||||
}
|
||||
|
||||
let provider = all_language_settings(None, cx).inline_completions.provider;
|
||||
|
||||
match provider {
|
||||
InlineCompletionProvider::None
|
||||
| InlineCompletionProvider::Copilot
|
||||
| InlineCompletionProvider::Supermaven => {}
|
||||
InlineCompletionProvider::Zed => {
|
||||
self.dismiss(cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn dismiss(&mut self, cx: &mut Context<Self>) {
|
||||
persist_dismissed(cx);
|
||||
self.dismissed = true;
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
const DISMISSED_AT_KEY: &str = "zed_predict_banner_dismissed_at";
|
||||
|
||||
pub(crate) fn get_dismissed() -> bool {
|
||||
db::kvp::KEY_VALUE_STORE
|
||||
.read_kvp(DISMISSED_AT_KEY)
|
||||
.log_err()
|
||||
.map_or(false, |dismissed| dismissed.is_some())
|
||||
}
|
||||
|
||||
pub(crate) fn persist_dismissed(cx: &mut App) {
|
||||
cx.spawn(|_| {
|
||||
let time = Utc::now().to_rfc3339();
|
||||
db::kvp::KEY_VALUE_STORE.write_kvp(DISMISSED_AT_KEY.into(), time)
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
impl Render for ZedPredictBanner {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
if !self.should_show(cx) {
|
||||
return div();
|
||||
}
|
||||
|
||||
let border_color = cx.theme().colors().editor_foreground.opacity(0.3);
|
||||
let banner = h_flex()
|
||||
.rounded_md()
|
||||
.border_1()
|
||||
.border_color(border_color)
|
||||
.child(
|
||||
ButtonLike::new("try-zed-predict")
|
||||
.child(
|
||||
h_flex()
|
||||
.h_full()
|
||||
.items_center()
|
||||
.gap_1p5()
|
||||
.child(Icon::new(IconName::ZedPredict).size(IconSize::Small))
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_0p5()
|
||||
.child(
|
||||
Label::new("Introducing:")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(Label::new("Edit Prediction").size(LabelSize::Small)),
|
||||
),
|
||||
)
|
||||
.on_click({
|
||||
let workspace = self.workspace.clone();
|
||||
let user_store = self.user_store.clone();
|
||||
let client = self.client.clone();
|
||||
let fs = self.fs.clone();
|
||||
move |_, window, cx| {
|
||||
let Some(workspace) = workspace.upgrade() else {
|
||||
return;
|
||||
};
|
||||
ZedPredictModal::toggle(
|
||||
workspace,
|
||||
user_store.clone(),
|
||||
client.clone(),
|
||||
fs.clone(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
div().border_l_1().border_color(border_color).child(
|
||||
IconButton::new("close", IconName::Close)
|
||||
.icon_size(IconSize::Indicator)
|
||||
.on_click(cx.listener(|this, _, _window, cx| this.dismiss(cx)))
|
||||
.tooltip(|window, cx| {
|
||||
Tooltip::with_meta(
|
||||
"Close Announcement Banner",
|
||||
None,
|
||||
"It won't show again for this feature",
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
div().pr_1().child(banner)
|
||||
}
|
||||
}
|
||||
5
crates/zed_predict_onboarding/src/lib.rs
Normal file
5
crates/zed_predict_onboarding/src/lib.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
mod banner;
|
||||
mod modal;
|
||||
|
||||
pub use banner::ZedPredictBanner;
|
||||
pub use modal::ZedPredictModal;
|
||||
313
crates/zed_predict_onboarding/src/modal.rs
Normal file
313
crates/zed_predict_onboarding/src/modal.rs
Normal file
@@ -0,0 +1,313 @@
|
||||
use std::{sync::Arc, time::Duration};
|
||||
|
||||
use client::{Client, UserStore};
|
||||
use feature_flags::FeatureFlagAppExt as _;
|
||||
use fs::Fs;
|
||||
use gpui::{
|
||||
ease_in_out, svg, Animation, AnimationExt as _, ClickEvent, DismissEvent, Entity, EventEmitter,
|
||||
FocusHandle, Focusable, MouseDownEvent, Render,
|
||||
};
|
||||
use language::language_settings::{AllLanguageSettings, InlineCompletionProvider};
|
||||
use settings::{update_settings_file, Settings};
|
||||
use ui::{prelude::*, CheckboxWithLabel, TintColor};
|
||||
use workspace::{notifications::NotifyTaskExt, ModalView, Workspace};
|
||||
|
||||
/// Introduces user to AI inline prediction feature and terms of service
|
||||
pub struct ZedPredictModal {
|
||||
user_store: Entity<UserStore>,
|
||||
client: Arc<Client>,
|
||||
fs: Arc<dyn Fs>,
|
||||
focus_handle: FocusHandle,
|
||||
sign_in_status: SignInStatus,
|
||||
terms_of_service: bool,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq)]
|
||||
enum SignInStatus {
|
||||
/// Signed out or signed in but not from this modal
|
||||
Idle,
|
||||
/// Authentication triggered from this modal
|
||||
Waiting,
|
||||
/// Signed in after authentication from this modal
|
||||
SignedIn,
|
||||
}
|
||||
|
||||
impl ZedPredictModal {
|
||||
fn new(
|
||||
user_store: Entity<UserStore>,
|
||||
client: Arc<Client>,
|
||||
fs: Arc<dyn Fs>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
ZedPredictModal {
|
||||
user_store,
|
||||
client,
|
||||
fs,
|
||||
focus_handle: cx.focus_handle(),
|
||||
sign_in_status: SignInStatus::Idle,
|
||||
terms_of_service: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toggle(
|
||||
workspace: Entity<Workspace>,
|
||||
user_store: Entity<UserStore>,
|
||||
client: Arc<Client>,
|
||||
fs: Arc<dyn Fs>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
workspace.update(cx, |this, cx| {
|
||||
this.toggle_modal(window, cx, |_window, cx| {
|
||||
ZedPredictModal::new(user_store, client, fs, cx)
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn view_terms(&mut self, _: &ClickEvent, _: &mut Window, cx: &mut Context<Self>) {
|
||||
cx.open_url("https://zed.dev/terms-of-service");
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
fn accept_and_enable(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let task = self
|
||||
.user_store
|
||||
.update(cx, |this, cx| this.accept_terms_of_service(cx));
|
||||
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
task.await?;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
update_settings_file::<AllLanguageSettings>(this.fs.clone(), cx, move |file, _| {
|
||||
file.features
|
||||
.get_or_insert(Default::default())
|
||||
.inline_completion_provider = Some(InlineCompletionProvider::Zed);
|
||||
});
|
||||
|
||||
cx.emit(DismissEvent);
|
||||
})
|
||||
})
|
||||
.detach_and_notify_err(window, cx);
|
||||
}
|
||||
|
||||
fn sign_in(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let client = self.client.clone();
|
||||
self.sign_in_status = SignInStatus::Waiting;
|
||||
|
||||
cx.spawn(move |this, mut cx| async move {
|
||||
let result = client.authenticate_and_connect(true, &cx).await;
|
||||
|
||||
let status = match result {
|
||||
Ok(_) => SignInStatus::SignedIn,
|
||||
Err(_) => SignInStatus::Idle,
|
||||
};
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.sign_in_status = status;
|
||||
cx.notify()
|
||||
})?;
|
||||
|
||||
result
|
||||
})
|
||||
.detach_and_notify_err(window, cx);
|
||||
}
|
||||
|
||||
fn cancel(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<DismissEvent> for ZedPredictModal {}
|
||||
|
||||
impl Focusable for ZedPredictModal {
|
||||
fn focus_handle(&self, _cx: &App) -> FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl ModalView for ZedPredictModal {}
|
||||
|
||||
impl Render for ZedPredictModal {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let base = v_flex()
|
||||
.w(px(420.))
|
||||
.p_4()
|
||||
.relative()
|
||||
.gap_2()
|
||||
.overflow_hidden()
|
||||
.elevation_3(cx)
|
||||
.id("zed predict tos")
|
||||
.track_focus(&self.focus_handle(cx))
|
||||
.on_action(cx.listener(Self::cancel))
|
||||
.key_context("ZedPredictModal")
|
||||
.on_action(cx.listener(|_, _: &menu::Cancel, _window, cx| {
|
||||
cx.emit(DismissEvent);
|
||||
}))
|
||||
.on_any_mouse_down(cx.listener(|this, _: &MouseDownEvent, window, _cx| {
|
||||
this.focus_handle.focus(window);
|
||||
}))
|
||||
.child(
|
||||
div()
|
||||
.p_1p5()
|
||||
.absolute()
|
||||
.top_0()
|
||||
.left_0()
|
||||
.right_0()
|
||||
.h(px(200.))
|
||||
.child(
|
||||
svg()
|
||||
.path("icons/zed_predict_bg.svg")
|
||||
.text_color(cx.theme().colors().icon_disabled)
|
||||
.w(px(416.))
|
||||
.h(px(128.))
|
||||
.overflow_hidden(),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.mb_2()
|
||||
.justify_between()
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
Label::new("Introducing Zed AI's")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(Headline::new("Edit Prediction").size(HeadlineSize::Large)),
|
||||
)
|
||||
.child({
|
||||
let tab = |n: usize| {
|
||||
let text_color = cx.theme().colors().text;
|
||||
let border_color = cx.theme().colors().text_accent.opacity(0.4);
|
||||
|
||||
h_flex().child(
|
||||
h_flex()
|
||||
.px_4()
|
||||
.py_0p5()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.border_1()
|
||||
.border_color(border_color)
|
||||
.rounded_md()
|
||||
.font(theme::ThemeSettings::get_global(cx).buffer_font.clone())
|
||||
.text_size(TextSize::XSmall.rems(cx))
|
||||
.text_color(text_color)
|
||||
.child("tab")
|
||||
.with_animation(
|
||||
ElementId::Integer(n),
|
||||
Animation::new(Duration::from_secs(2)).repeat(),
|
||||
move |tab, delta| {
|
||||
let delta = (delta - 0.15 * n as f32) / 0.7;
|
||||
let delta = 1.0 - (0.5 - delta).abs() * 2.;
|
||||
let delta = ease_in_out(delta.clamp(0., 1.));
|
||||
let delta = 0.1 + 0.9 * delta;
|
||||
|
||||
tab.border_color(border_color.opacity(delta))
|
||||
.text_color(text_color.opacity(delta))
|
||||
},
|
||||
),
|
||||
)
|
||||
};
|
||||
|
||||
v_flex()
|
||||
.gap_2()
|
||||
.items_center()
|
||||
.pr_4()
|
||||
.child(tab(0).ml_neg_20())
|
||||
.child(tab(1))
|
||||
.child(tab(2).ml_20())
|
||||
}),
|
||||
)
|
||||
.child(h_flex().absolute().top_2().right_2().child(
|
||||
IconButton::new("cancel", IconName::X).on_click(cx.listener(
|
||||
|_, _: &ClickEvent, _window, cx| {
|
||||
cx.emit(DismissEvent);
|
||||
},
|
||||
)),
|
||||
));
|
||||
|
||||
let blog_post_button = if cx.is_staff() {
|
||||
Some(
|
||||
Button::new("view-blog", "Read the Blog Post")
|
||||
.full_width()
|
||||
.icon(IconName::ArrowUpRight)
|
||||
.icon_size(IconSize::Indicator)
|
||||
.icon_color(Color::Muted)
|
||||
.on_click(cx.listener(Self::view_blog)),
|
||||
)
|
||||
} else {
|
||||
// TODO: put back when blog post is published
|
||||
None
|
||||
};
|
||||
|
||||
if self.user_store.read(cx).current_user().is_some() {
|
||||
let copy = match self.sign_in_status {
|
||||
SignInStatus::Idle => "Get accurate and helpful edit predictions at every keystroke. To set Zed as your inline completions provider, ensure you:",
|
||||
SignInStatus::SignedIn => "Almost there! Ensure you:",
|
||||
SignInStatus::Waiting => unreachable!(),
|
||||
};
|
||||
|
||||
base.child(Label::new(copy).color(Color::Muted))
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_0p5()
|
||||
.child(CheckboxWithLabel::new(
|
||||
"tos-checkbox",
|
||||
Label::new("Have read and accepted the").color(Color::Muted),
|
||||
self.terms_of_service.into(),
|
||||
cx.listener(move |this, state, _window, cx| {
|
||||
this.terms_of_service = *state == ToggleState::Selected;
|
||||
cx.notify()
|
||||
}),
|
||||
))
|
||||
.child(
|
||||
Button::new("view-tos", "Terms of Service")
|
||||
.icon(IconName::ArrowUpRight)
|
||||
.icon_size(IconSize::Indicator)
|
||||
.icon_color(Color::Muted)
|
||||
.on_click(cx.listener(Self::view_terms)),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
.mt_2()
|
||||
.gap_2()
|
||||
.w_full()
|
||||
.child(
|
||||
Button::new("accept-tos", "Enable Edit Predictions")
|
||||
.disabled(!self.terms_of_service)
|
||||
.style(ButtonStyle::Tinted(TintColor::Accent))
|
||||
.full_width()
|
||||
.on_click(cx.listener(Self::accept_and_enable)),
|
||||
)
|
||||
.children(blog_post_button),
|
||||
)
|
||||
} else {
|
||||
base.child(
|
||||
Label::new("To set Zed as your inline completions provider, please sign in.")
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
.mt_2()
|
||||
.gap_2()
|
||||
.w_full()
|
||||
.child(
|
||||
Button::new("accept-tos", "Sign in with GitHub")
|
||||
.disabled(self.sign_in_status == SignInStatus::Waiting)
|
||||
.style(ButtonStyle::Tinted(TintColor::Accent))
|
||||
.full_width()
|
||||
.on_click(cx.listener(Self::sign_in)),
|
||||
)
|
||||
.children(blog_post_button),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,155 +0,0 @@
|
||||
//! AI service Terms of Service acceptance modal.
|
||||
|
||||
use client::UserStore;
|
||||
use gpui::{
|
||||
App, ClickEvent, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, MouseDownEvent,
|
||||
Render,
|
||||
};
|
||||
use ui::{prelude::*, TintColor};
|
||||
use workspace::{ModalView, Workspace};
|
||||
|
||||
/// Terms of acceptance for AI inline prediction.
|
||||
pub struct ZedPredictTos {
|
||||
focus_handle: FocusHandle,
|
||||
user_store: Entity<UserStore>,
|
||||
workspace: Entity<Workspace>,
|
||||
viewed: bool,
|
||||
}
|
||||
|
||||
impl ZedPredictTos {
|
||||
fn new(
|
||||
workspace: Entity<Workspace>,
|
||||
user_store: Entity<UserStore>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
ZedPredictTos {
|
||||
viewed: false,
|
||||
focus_handle: cx.focus_handle(),
|
||||
user_store,
|
||||
workspace,
|
||||
}
|
||||
}
|
||||
pub fn toggle(
|
||||
workspace: Entity<Workspace>,
|
||||
user_store: Entity<UserStore>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
workspace.update(cx, |this, cx| {
|
||||
let workspace = cx.entity().clone();
|
||||
this.toggle_modal(window, cx, |_window, cx| {
|
||||
ZedPredictTos::new(workspace, user_store, cx)
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn view_terms(&mut self, _: &ClickEvent, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.viewed = true;
|
||||
cx.open_url("https://zed.dev/terms-of-service");
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn accept_terms(&mut self, _: &ClickEvent, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
let task = self
|
||||
.user_store
|
||||
.update(cx, |this, cx| this.accept_terms_of_service(cx));
|
||||
|
||||
let workspace = self.workspace.clone();
|
||||
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
match task.await {
|
||||
Ok(_) => this.update(&mut cx, |_, cx| {
|
||||
cx.emit(DismissEvent);
|
||||
}),
|
||||
Err(err) => workspace.update(&mut cx, |this, cx| {
|
||||
this.show_error(&err, cx);
|
||||
}),
|
||||
}
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
fn cancel(&mut self, _: &menu::Cancel, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<DismissEvent> for ZedPredictTos {}
|
||||
|
||||
impl Focusable for ZedPredictTos {
|
||||
fn focus_handle(&self, _cx: &App) -> FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl ModalView for ZedPredictTos {}
|
||||
|
||||
impl Render for ZedPredictTos {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
v_flex()
|
||||
.id("zed predict tos")
|
||||
.track_focus(&self.focus_handle(cx))
|
||||
.on_action(cx.listener(Self::cancel))
|
||||
.key_context("ZedPredictTos")
|
||||
.elevation_3(cx)
|
||||
.w_96()
|
||||
.items_center()
|
||||
.p_4()
|
||||
.gap_2()
|
||||
.on_action(cx.listener(|_, _: &menu::Cancel, _window, cx| {
|
||||
cx.emit(DismissEvent);
|
||||
}))
|
||||
.on_any_mouse_down(cx.listener(|this, _: &MouseDownEvent, window, _cx| {
|
||||
this.focus_handle.focus(window);
|
||||
}))
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.justify_between()
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_0p5()
|
||||
.child(
|
||||
Label::new("Zed AI")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(Headline::new("Edit Prediction")),
|
||||
)
|
||||
.child(Icon::new(IconName::ZedPredict).size(IconSize::XLarge)),
|
||||
)
|
||||
.child(
|
||||
Label::new(
|
||||
"To use Zed AI's Edit Prediction feature, please read and accept our Terms of Service.",
|
||||
)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
.mt_2()
|
||||
.gap_0p5()
|
||||
.w_full()
|
||||
.child(if self.viewed {
|
||||
Button::new("accept-tos", "I've Read and Accept the Terms of Service")
|
||||
.style(ButtonStyle::Tinted(TintColor::Accent))
|
||||
.full_width()
|
||||
.on_click(cx.listener(Self::accept_terms))
|
||||
} else {
|
||||
Button::new("view-tos", "Read Terms of Service")
|
||||
.style(ButtonStyle::Tinted(TintColor::Accent))
|
||||
.icon(IconName::ArrowUpRight)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_position(IconPosition::End)
|
||||
.full_width()
|
||||
.on_click(cx.listener(Self::view_terms))
|
||||
})
|
||||
.child(
|
||||
Button::new("cancel", "Cancel")
|
||||
.full_width()
|
||||
.on_click(cx.listener(|_, _: &ClickEvent, _window, cx| {
|
||||
cx.emit(DismissEvent);
|
||||
})),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ arrayvec.workspace = true
|
||||
client.workspace = true
|
||||
collections.workspace = true
|
||||
command_palette_hooks.workspace = true
|
||||
db.workspace = true
|
||||
editor.workspace = true
|
||||
feature_flags.workspace = true
|
||||
futures.workspace = true
|
||||
@@ -34,6 +35,7 @@ language_models.workspace = true
|
||||
log.workspace = true
|
||||
menu.workspace = true
|
||||
rpc.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
settings.workspace = true
|
||||
similar.workspace = true
|
||||
|
||||
48
crates/zeta/src/persistence.rs
Normal file
48
crates/zeta/src/persistence.rs
Normal file
@@ -0,0 +1,48 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
use workspace::WorkspaceDb;
|
||||
|
||||
use db::sqlez_macros::sql;
|
||||
use db::{define_connection, query};
|
||||
|
||||
define_connection!(
|
||||
pub static ref DB: ZetaDb<WorkspaceDb> = &[
|
||||
sql! (
|
||||
CREATE TABLE zeta_preferences(
|
||||
worktree_path BLOB NOT NULL PRIMARY KEY,
|
||||
accepted_data_collection INTEGER
|
||||
) STRICT;
|
||||
),
|
||||
];
|
||||
);
|
||||
|
||||
impl ZetaDb {
|
||||
query! {
|
||||
pub fn get_all_data_collection_preferences() -> Result<Vec<(PathBuf, bool)>> {
|
||||
SELECT worktree_path, accepted_data_collection FROM zeta_preferences
|
||||
}
|
||||
}
|
||||
|
||||
query! {
|
||||
pub fn get_accepted_data_collection(worktree_path: &Path) -> Result<Option<bool>> {
|
||||
SELECT accepted_data_collection FROM zeta_preferences
|
||||
WHERE worktree_path = ?
|
||||
}
|
||||
}
|
||||
|
||||
query! {
|
||||
pub async fn save_data_collection_choice(worktree_path: PathBuf, accepted_data_collection: bool) -> Result<()> {
|
||||
INSERT INTO zeta_preferences
|
||||
(worktree_path, accepted_data_collection)
|
||||
VALUES
|
||||
(?1, ?2)
|
||||
ON CONFLICT (worktree_path) DO UPDATE SET
|
||||
accepted_data_collection = ?2
|
||||
}
|
||||
}
|
||||
|
||||
query! {
|
||||
pub async fn clear_all_zeta_preferences() -> Result<()> {
|
||||
DELETE FROM zeta_preferences
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,21 @@
|
||||
mod completion_diff_element;
|
||||
mod persistence;
|
||||
mod rate_completion_modal;
|
||||
|
||||
pub(crate) use completion_diff_element::*;
|
||||
use db::kvp::KEY_VALUE_STORE;
|
||||
use inline_completion::DataCollectionState;
|
||||
pub use rate_completion_modal::*;
|
||||
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use arrayvec::ArrayVec;
|
||||
use client::{Client, UserStore};
|
||||
use collections::hash_map::Entry;
|
||||
use collections::{HashMap, HashSet, VecDeque};
|
||||
use futures::AsyncReadExt;
|
||||
use gpui::{
|
||||
actions, App, AppContext as _, AsyncApp, Context, Entity, EntityId, Global, Subscription, Task,
|
||||
WeakEntity,
|
||||
};
|
||||
use http_client::{HttpClient, Method};
|
||||
use language::{
|
||||
@@ -21,24 +26,30 @@ use language_models::LlmApiToken;
|
||||
use rpc::{PredictEditsParams, PredictEditsResponse, EXPIRED_LLM_TOKEN_HEADER_NAME};
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
cmp,
|
||||
cmp, env,
|
||||
fmt::Write,
|
||||
future::Future,
|
||||
mem,
|
||||
ops::Range,
|
||||
path::Path,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use telemetry_events::InlineCompletionRating;
|
||||
use util::ResultExt;
|
||||
use uuid::Uuid;
|
||||
use workspace::{
|
||||
notifications::{simple_message_notification::MessageNotification, NotificationId},
|
||||
Workspace,
|
||||
};
|
||||
|
||||
const CURSOR_MARKER: &'static str = "<|user_cursor_is_here|>";
|
||||
const START_OF_FILE_MARKER: &'static str = "<|start_of_file|>";
|
||||
const EDITABLE_REGION_START_MARKER: &'static str = "<|editable_region_start|>";
|
||||
const EDITABLE_REGION_END_MARKER: &'static str = "<|editable_region_end|>";
|
||||
const BUFFER_CHANGE_GROUPING_INTERVAL: Duration = Duration::from_secs(1);
|
||||
const ZED_PREDICT_DATA_COLLECTION_NEVER_ASK_AGAIN_KEY: &'static str =
|
||||
"zed_predict_data_collection_never_ask_again";
|
||||
|
||||
actions!(edit_prediction, [ClearHistory]);
|
||||
|
||||
@@ -159,6 +170,7 @@ pub struct Zeta {
|
||||
registered_buffers: HashMap<gpui::EntityId, RegisteredBuffer>,
|
||||
shown_completions: VecDeque<InlineCompletion>,
|
||||
rated_completions: HashSet<InlineCompletionId>,
|
||||
data_collection_preferences: DataCollectionPreferences,
|
||||
llm_token: LlmApiToken,
|
||||
_llm_token_subscription: Subscription,
|
||||
tos_accepted: bool, // Terms of service accepted
|
||||
@@ -188,13 +200,13 @@ impl Zeta {
|
||||
|
||||
fn new(client: Arc<Client>, user_store: Entity<UserStore>, cx: &mut Context<Self>) -> Self {
|
||||
let refresh_llm_token_listener = language_models::RefreshLlmTokenListener::global(cx);
|
||||
|
||||
Self {
|
||||
client,
|
||||
events: VecDeque::new(),
|
||||
shown_completions: VecDeque::new(),
|
||||
rated_completions: HashSet::default(),
|
||||
registered_buffers: HashMap::default(),
|
||||
data_collection_preferences: Self::load_data_collection_preferences(cx),
|
||||
llm_token: LlmApiToken::default(),
|
||||
_llm_token_subscription: cx.subscribe(
|
||||
&refresh_llm_token_listener,
|
||||
@@ -212,11 +224,16 @@ impl Zeta {
|
||||
.read(cx)
|
||||
.current_user_has_accepted_terms()
|
||||
.unwrap_or(false),
|
||||
_user_store_subscription: cx.subscribe(&user_store, |this, _, event, _| match event {
|
||||
client::user::Event::TermsStatusUpdated { accepted } => {
|
||||
this.tos_accepted = *accepted;
|
||||
_user_store_subscription: cx.subscribe(&user_store, |this, user_store, event, cx| {
|
||||
match event {
|
||||
client::user::Event::PrivateUserInfoUpdated => {
|
||||
this.tos_accepted = user_store
|
||||
.read(cx)
|
||||
.current_user_has_accepted_terms()
|
||||
.unwrap_or(false);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
_ => {}
|
||||
}),
|
||||
}
|
||||
}
|
||||
@@ -282,18 +299,16 @@ impl Zeta {
|
||||
event: &language::BufferEvent,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
match event {
|
||||
language::BufferEvent::Edited => {
|
||||
self.report_changes_for_buffer(&buffer, cx);
|
||||
}
|
||||
_ => {}
|
||||
if let language::BufferEvent::Edited = event {
|
||||
self.report_changes_for_buffer(&buffer, cx);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn request_completion_impl<F, R>(
|
||||
&mut self,
|
||||
buffer: &Entity<Buffer>,
|
||||
position: language::Anchor,
|
||||
cursor: language::Anchor,
|
||||
can_collect_data: bool,
|
||||
cx: &mut Context<Self>,
|
||||
perform_predict_edits: F,
|
||||
) -> Task<Result<Option<InlineCompletion>>>
|
||||
@@ -302,7 +317,7 @@ impl Zeta {
|
||||
R: Future<Output = Result<PredictEditsResponse>> + Send + 'static,
|
||||
{
|
||||
let snapshot = self.report_changes_for_buffer(buffer, cx);
|
||||
let point = position.to_point(&snapshot);
|
||||
let point = cursor.to_point(&snapshot);
|
||||
let offset = point.to_offset(&snapshot);
|
||||
let excerpt_range = excerpt_range_for_position(point, &snapshot);
|
||||
let events = self.events.clone();
|
||||
@@ -346,6 +361,7 @@ impl Zeta {
|
||||
input_events: input_events.clone(),
|
||||
input_excerpt: input_excerpt.clone(),
|
||||
outline: Some(input_outline.clone()),
|
||||
can_collect_data,
|
||||
};
|
||||
|
||||
let response = perform_predict_edits(client, llm_token, body).await?;
|
||||
@@ -515,16 +531,23 @@ and then another
|
||||
) -> Task<Result<Option<InlineCompletion>>> {
|
||||
use std::future::ready;
|
||||
|
||||
self.request_completion_impl(buffer, position, cx, |_, _, _| ready(Ok(response)))
|
||||
self.request_completion_impl(buffer, position, false, cx, |_, _, _| ready(Ok(response)))
|
||||
}
|
||||
|
||||
pub fn request_completion(
|
||||
&mut self,
|
||||
buffer: &Entity<Buffer>,
|
||||
position: language::Anchor,
|
||||
can_collect_data: bool,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<Option<InlineCompletion>>> {
|
||||
self.request_completion_impl(buffer, position, cx, Self::perform_predict_edits)
|
||||
self.request_completion_impl(
|
||||
buffer,
|
||||
position,
|
||||
can_collect_data,
|
||||
cx,
|
||||
Self::perform_predict_edits,
|
||||
)
|
||||
}
|
||||
|
||||
fn perform_predict_edits(
|
||||
@@ -769,6 +792,85 @@ and then another
|
||||
|
||||
new_snapshot
|
||||
}
|
||||
|
||||
/// Creates a `Entity<DataCollectionChoice>` for each unique worktree abs path it sees.
|
||||
pub fn data_collection_choice_at(
|
||||
&mut self,
|
||||
worktree_abs_path: PathBuf,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Entity<DataCollectionChoice> {
|
||||
match self
|
||||
.data_collection_preferences
|
||||
.per_worktree
|
||||
.entry(worktree_abs_path)
|
||||
{
|
||||
Entry::Vacant(entry) => {
|
||||
let choice = cx.new(|_| DataCollectionChoice::NotAnswered);
|
||||
entry.insert(choice.clone());
|
||||
choice
|
||||
}
|
||||
Entry::Occupied(entry) => entry.get().clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn set_never_ask_again_for_data_collection(&mut self, cx: &mut Context<Self>) {
|
||||
self.data_collection_preferences.never_ask_again = true;
|
||||
|
||||
// persist choice
|
||||
db::write_and_log(cx, move || {
|
||||
KEY_VALUE_STORE.write_kvp(
|
||||
ZED_PREDICT_DATA_COLLECTION_NEVER_ASK_AGAIN_KEY.into(),
|
||||
"true".to_string(),
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
fn load_data_collection_preferences(cx: &mut Context<Self>) -> DataCollectionPreferences {
|
||||
if env::var("ZED_PREDICT_CLEAR_DATA_COLLECTION_PREFERENCES").is_ok() {
|
||||
db::write_and_log(cx, move || async move {
|
||||
KEY_VALUE_STORE
|
||||
.delete_kvp(ZED_PREDICT_DATA_COLLECTION_NEVER_ASK_AGAIN_KEY.into())
|
||||
.await
|
||||
.log_err();
|
||||
|
||||
persistence::DB.clear_all_zeta_preferences().await
|
||||
});
|
||||
return DataCollectionPreferences::default();
|
||||
}
|
||||
|
||||
let never_ask_again = KEY_VALUE_STORE
|
||||
.read_kvp(ZED_PREDICT_DATA_COLLECTION_NEVER_ASK_AGAIN_KEY)
|
||||
.log_err()
|
||||
.flatten()
|
||||
.map(|value| value == "true")
|
||||
.unwrap_or(false);
|
||||
|
||||
let preferences_per_worktree = persistence::DB
|
||||
.get_all_data_collection_preferences()
|
||||
.log_err()
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.map(|(path, choice)| {
|
||||
let choice = cx.new(|_| DataCollectionChoice::from(choice));
|
||||
(path, choice)
|
||||
})
|
||||
.collect();
|
||||
|
||||
DataCollectionPreferences {
|
||||
never_ask_again,
|
||||
per_worktree: preferences_per_worktree,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
struct DataCollectionPreferences {
|
||||
/// Set when a user clicks on "Never Ask Again", can never be unset.
|
||||
never_ask_again: bool,
|
||||
/// The choices for each worktree.
|
||||
///
|
||||
/// This is filled when loading from database, or when querying if no matching path is found.
|
||||
per_worktree: HashMap<PathBuf, Entity<DataCollectionChoice>>,
|
||||
}
|
||||
|
||||
fn common_prefix<T1: Iterator<Item = char>, T2: Iterator<Item = char>>(a: T1, b: T2) -> usize {
|
||||
@@ -968,22 +1070,127 @@ struct PendingCompletion {
|
||||
_task: Task<()>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum DataCollectionChoice {
|
||||
NotAnswered,
|
||||
Enabled,
|
||||
Disabled,
|
||||
}
|
||||
|
||||
impl DataCollectionChoice {
|
||||
pub fn is_enabled(self) -> bool {
|
||||
match self {
|
||||
Self::Enabled => true,
|
||||
Self::NotAnswered | Self::Disabled => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_answered(self) -> bool {
|
||||
match self {
|
||||
Self::Enabled | Self::Disabled => true,
|
||||
Self::NotAnswered => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toggle(self) -> DataCollectionChoice {
|
||||
match self {
|
||||
Self::Enabled => Self::Disabled,
|
||||
Self::Disabled => Self::Enabled,
|
||||
Self::NotAnswered => Self::Enabled,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<bool> for DataCollectionChoice {
|
||||
fn from(value: bool) -> Self {
|
||||
match value {
|
||||
true => DataCollectionChoice::Enabled,
|
||||
false => DataCollectionChoice::Disabled,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ZetaInlineCompletionProvider {
|
||||
zeta: Entity<Zeta>,
|
||||
pending_completions: ArrayVec<PendingCompletion, 2>,
|
||||
next_pending_completion_id: usize,
|
||||
current_completion: Option<CurrentInlineCompletion>,
|
||||
data_collection: Option<ProviderDataCollection>,
|
||||
}
|
||||
|
||||
pub struct ProviderDataCollection {
|
||||
workspace: WeakEntity<Workspace>,
|
||||
worktree_root_path: PathBuf,
|
||||
choice: Entity<DataCollectionChoice>,
|
||||
}
|
||||
|
||||
impl ProviderDataCollection {
|
||||
pub fn new(
|
||||
zeta: Entity<Zeta>,
|
||||
workspace: Option<Entity<Workspace>>,
|
||||
buffer: Option<Entity<Buffer>>,
|
||||
cx: &mut App,
|
||||
) -> Option<ProviderDataCollection> {
|
||||
let workspace = workspace?;
|
||||
|
||||
let worktree_root_path = buffer?.update(cx, |buffer, cx| {
|
||||
let file = buffer.file()?;
|
||||
|
||||
if !file.is_local() || file.is_private() {
|
||||
return None;
|
||||
}
|
||||
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
Some(
|
||||
workspace
|
||||
.absolute_path_of_worktree(file.worktree_id(cx), cx)?
|
||||
.to_path_buf(),
|
||||
)
|
||||
})
|
||||
})?;
|
||||
|
||||
let choice = zeta.update(cx, |zeta, cx| {
|
||||
zeta.data_collection_choice_at(worktree_root_path.clone(), cx)
|
||||
});
|
||||
|
||||
Some(ProviderDataCollection {
|
||||
workspace: workspace.downgrade(),
|
||||
worktree_root_path,
|
||||
choice,
|
||||
})
|
||||
}
|
||||
|
||||
fn set_choice(&mut self, choice: DataCollectionChoice, cx: &mut App) {
|
||||
self.choice.update(cx, |this, _| *this = choice);
|
||||
|
||||
let worktree_root_path = self.worktree_root_path.clone();
|
||||
|
||||
db::write_and_log(cx, move || {
|
||||
persistence::DB.save_data_collection_choice(worktree_root_path, choice.is_enabled())
|
||||
});
|
||||
}
|
||||
|
||||
fn toggle_choice(&mut self, cx: &mut App) {
|
||||
self.set_choice(self.choice.read(cx).toggle(), cx);
|
||||
}
|
||||
}
|
||||
|
||||
impl ZetaInlineCompletionProvider {
|
||||
pub const DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(8);
|
||||
|
||||
pub fn new(zeta: Entity<Zeta>) -> Self {
|
||||
pub fn new(zeta: Entity<Zeta>, data_collection: Option<ProviderDataCollection>) -> Self {
|
||||
Self {
|
||||
zeta,
|
||||
pending_completions: ArrayVec::new(),
|
||||
next_pending_completion_id: 0,
|
||||
current_completion: None,
|
||||
data_collection,
|
||||
}
|
||||
}
|
||||
|
||||
fn set_data_collection_choice(&mut self, choice: DataCollectionChoice, cx: &mut App) {
|
||||
if let Some(data_collection) = self.data_collection.as_mut() {
|
||||
data_collection.set_choice(choice, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -994,7 +1201,7 @@ impl inline_completion::InlineCompletionProvider for ZetaInlineCompletionProvide
|
||||
}
|
||||
|
||||
fn display_name() -> &'static str {
|
||||
"Zed Predict"
|
||||
"Zed's Edit Predictions"
|
||||
}
|
||||
|
||||
fn show_completions_in_menu() -> bool {
|
||||
@@ -1009,6 +1216,24 @@ impl inline_completion::InlineCompletionProvider for ZetaInlineCompletionProvide
|
||||
true
|
||||
}
|
||||
|
||||
fn data_collection_state(&self, cx: &App) -> DataCollectionState {
|
||||
let Some(data_collection) = self.data_collection.as_ref() else {
|
||||
return DataCollectionState::Unknown;
|
||||
};
|
||||
|
||||
if data_collection.choice.read(cx).is_enabled() {
|
||||
DataCollectionState::Enabled
|
||||
} else {
|
||||
DataCollectionState::Disabled
|
||||
}
|
||||
}
|
||||
|
||||
fn toggle_data_collection(&mut self, cx: &mut App) {
|
||||
if let Some(data_collection) = self.data_collection.as_mut() {
|
||||
data_collection.toggle_choice(cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn is_enabled(
|
||||
&self,
|
||||
buffer: &Entity<Buffer>,
|
||||
@@ -1043,6 +1268,12 @@ impl inline_completion::InlineCompletionProvider for ZetaInlineCompletionProvide
|
||||
|
||||
let pending_completion_id = self.next_pending_completion_id;
|
||||
self.next_pending_completion_id += 1;
|
||||
let can_collect_data = self
|
||||
.data_collection
|
||||
.as_ref()
|
||||
.map_or(false, |data_collection| {
|
||||
data_collection.choice.read(cx).is_enabled()
|
||||
});
|
||||
|
||||
let task = cx.spawn(|this, mut cx| async move {
|
||||
if debounce {
|
||||
@@ -1051,7 +1282,7 @@ impl inline_completion::InlineCompletionProvider for ZetaInlineCompletionProvide
|
||||
|
||||
let completion_request = this.update(&mut cx, |this, cx| {
|
||||
this.zeta.update(cx, |zeta, cx| {
|
||||
zeta.request_completion(&buffer, position, cx)
|
||||
zeta.request_completion(&buffer, position, can_collect_data, cx)
|
||||
})
|
||||
});
|
||||
|
||||
@@ -1128,8 +1359,79 @@ impl inline_completion::InlineCompletionProvider for ZetaInlineCompletionProvide
|
||||
// Right now we don't support cycling.
|
||||
}
|
||||
|
||||
fn accept(&mut self, _cx: &mut Context<Self>) {
|
||||
fn accept(&mut self, cx: &mut Context<Self>) {
|
||||
self.pending_completions.clear();
|
||||
|
||||
let Some(data_collection) = self.data_collection.as_mut() else {
|
||||
return;
|
||||
};
|
||||
|
||||
if data_collection.choice.read(cx).is_answered()
|
||||
|| self
|
||||
.zeta
|
||||
.read(cx)
|
||||
.data_collection_preferences
|
||||
.never_ask_again
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
struct ZetaDataCollectionNotification;
|
||||
let notification_id = NotificationId::unique::<ZetaDataCollectionNotification>();
|
||||
|
||||
const DATA_COLLECTION_INFO_URL: &str = "https://zed.dev/terms-of-service"; // TODO: Replace for a link that's dedicated to Edit Predictions data collection
|
||||
|
||||
let this = cx.entity();
|
||||
data_collection
|
||||
.workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace.show_notification(notification_id, cx, |cx| {
|
||||
let zeta = self.zeta.clone();
|
||||
|
||||
cx.new(move |_cx| {
|
||||
let message =
|
||||
"To allow Zed to suggest better edits, turn on data collection. You \
|
||||
can turn off at any time via the status bar menu.";
|
||||
MessageNotification::new(message)
|
||||
.with_title("Per-Project Data Collection Program")
|
||||
.show_close_button(false)
|
||||
.with_click_message("Turn On")
|
||||
.on_click({
|
||||
let this = this.clone();
|
||||
move |_window, cx| {
|
||||
this.update(cx, |this, cx| {
|
||||
this.set_data_collection_choice(
|
||||
DataCollectionChoice::Enabled,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
}
|
||||
})
|
||||
.with_secondary_click_message("Turn Off")
|
||||
.on_secondary_click({
|
||||
move |_window, cx| {
|
||||
this.update(cx, |this, cx| {
|
||||
this.set_data_collection_choice(
|
||||
DataCollectionChoice::Disabled,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
}
|
||||
})
|
||||
.with_tertiary_click_message("Never Ask Again")
|
||||
.on_tertiary_click({
|
||||
move |_window, cx| {
|
||||
zeta.update(cx, |zeta, cx| {
|
||||
zeta.set_never_ask_again_for_data_collection(cx);
|
||||
});
|
||||
}
|
||||
})
|
||||
.more_info_message("Learn More")
|
||||
.more_info_url(DATA_COLLECTION_INFO_URL)
|
||||
})
|
||||
});
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
|
||||
fn discard(&mut self, _cx: &mut Context<Self>) {
|
||||
@@ -1379,8 +1681,9 @@ mod tests {
|
||||
|
||||
let buffer = cx.new(|cx| Buffer::local(buffer_content, cx));
|
||||
let cursor = buffer.read_with(cx, |buffer, _| buffer.anchor_before(Point::new(1, 0)));
|
||||
let completion_task =
|
||||
zeta.update(cx, |zeta, cx| zeta.request_completion(&buffer, cursor, cx));
|
||||
let completion_task = zeta.update(cx, |zeta, cx| {
|
||||
zeta.request_completion(&buffer, cursor, false, cx)
|
||||
});
|
||||
|
||||
let token_request = server.receive::<proto::GetLlmToken>().await.unwrap();
|
||||
server.respond(
|
||||
|
||||
@@ -382,11 +382,16 @@ There are two options to choose from:
|
||||
- Default:
|
||||
|
||||
```json
|
||||
"inline_completions": {
|
||||
"disabled_globs": [
|
||||
".env"
|
||||
]
|
||||
}
|
||||
"inline_completions": {
|
||||
"disabled_globs": [
|
||||
"**/.env*",
|
||||
"**/*.pem",
|
||||
"**/*.key",
|
||||
"**/*.cert",
|
||||
"**/*.crt",
|
||||
"**/secrets.yml"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Options**
|
||||
|
||||
Reference in New Issue
Block a user