Compare commits
1 Commits
v0.172.8
...
ordered-mu
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
111583b863 |
15
.github/ISSUE_TEMPLATE/0_feature_request.yml
vendored
Normal file
15
.github/ISSUE_TEMPLATE/0_feature_request.yml
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
name: Feature Request
|
||||
description: "Tip: open this issue template from within Zed with the `request feature` command palette action"
|
||||
type: "Feature"
|
||||
body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe the feature
|
||||
description: A one line summary, and description of what you want to happen.
|
||||
value: |
|
||||
Summary:
|
||||
|
||||
Description:
|
||||
|
||||
Screenshots:
|
||||
<!-- drag files here -->
|
||||
3
.github/ISSUE_TEMPLATE/config.yml
vendored
3
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,9 +1,6 @@
|
||||
# yaml-language-server: $schema=https://json.schemastore.org/github-issue-config.json
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Feature Request
|
||||
url: https://github.com/zed-industries/zed/discussions/new/choose
|
||||
about: To request a feature, open a new Discussion in one of the appropriate Discussion categories
|
||||
- name: Zed Discussion Forum
|
||||
url: https://github.com/zed-industries/zed/discussions
|
||||
about: A community discussion forum
|
||||
|
||||
@@ -29,7 +29,8 @@ jobs:
|
||||
maxLength: 2000
|
||||
truncationSymbol: "..."
|
||||
- name: Discord Webhook Action
|
||||
uses: tsickert/discord-webhook@c840d45a03a323fbc3f7507ac7769dbd91bfb164 # v5.3.0
|
||||
uses: tsickert/discord-webhook@86dc739f3f165f16dadc5666051c367efa1692f4 # v6.0.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_onboarding",
|
||||
"zed_predict_tos",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6388,7 +6388,7 @@ dependencies = [
|
||||
"ui",
|
||||
"workspace",
|
||||
"zed_actions",
|
||||
"zed_predict_onboarding",
|
||||
"zed_predict_tos",
|
||||
"zeta",
|
||||
]
|
||||
|
||||
@@ -13412,7 +13412,6 @@ dependencies = [
|
||||
"windows 0.58.0",
|
||||
"workspace",
|
||||
"zed_actions",
|
||||
"zed_predict_onboarding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -16287,7 +16286,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed"
|
||||
version = "0.172.8"
|
||||
version = "0.172.0"
|
||||
dependencies = [
|
||||
"activity_indicator",
|
||||
"anyhow",
|
||||
@@ -16410,7 +16409,7 @@ dependencies = [
|
||||
"winresource",
|
||||
"workspace",
|
||||
"zed_actions",
|
||||
"zed_predict_onboarding",
|
||||
"zed_predict_tos",
|
||||
"zeta",
|
||||
]
|
||||
|
||||
@@ -16525,21 +16524,13 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zed_predict_onboarding"
|
||||
name = "zed_predict_tos"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"client",
|
||||
"db",
|
||||
"feature_flags",
|
||||
"fs",
|
||||
"gpui",
|
||||
"language",
|
||||
"menu",
|
||||
"settings",
|
||||
"theme",
|
||||
"ui",
|
||||
"util",
|
||||
"workspace",
|
||||
]
|
||||
|
||||
@@ -16740,7 +16731,6 @@ dependencies = [
|
||||
"collections",
|
||||
"command_palette_hooks",
|
||||
"ctor",
|
||||
"db",
|
||||
"editor",
|
||||
"env_logger 0.11.6",
|
||||
"feature_flags",
|
||||
@@ -16755,7 +16745,6 @@ dependencies = [
|
||||
"menu",
|
||||
"reqwest_client",
|
||||
"rpc",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"settings",
|
||||
"similar",
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
resolver = "2"
|
||||
members = [
|
||||
"crates/activity_indicator",
|
||||
"crates/zed_predict_tos",
|
||||
"crates/anthropic",
|
||||
"crates/assets",
|
||||
"crates/assistant",
|
||||
@@ -150,7 +151,6 @@ members = [
|
||||
"crates/worktree",
|
||||
"crates/zed",
|
||||
"crates/zed_actions",
|
||||
"crates/zed_predict_onboarding",
|
||||
"crates/zeta",
|
||||
"crates/git_ui",
|
||||
|
||||
@@ -201,6 +201,7 @@ 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" }
|
||||
@@ -348,7 +349,6 @@ 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: 342 B After Width: | Height: | Size: 334 B |
@@ -1,19 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 971 B |
@@ -122,7 +122,8 @@
|
||||
"ctrl-i": "editor::ShowSignatureHelp",
|
||||
"alt-g b": "editor::ToggleGitBlame",
|
||||
"menu": "editor::OpenContextMenu",
|
||||
"shift-f10": "editor::OpenContextMenu"
|
||||
"shift-f10": "editor::OpenContextMenu",
|
||||
"alt-enter": "editor::OpenSelectionsInMultibuffer"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -140,8 +141,7 @@
|
||||
// "cmd-e": ["buffer_search::Deploy", { "focus": false }],
|
||||
"ctrl->": "assistant::QuoteSelection",
|
||||
"ctrl-<": "assistant::InsertIntoEditor",
|
||||
"ctrl-alt-e": "editor::SelectEnclosingSymbol",
|
||||
"alt-enter": "editor::OpenSelectionsInMultibuffer"
|
||||
"ctrl-alt-e": "editor::SelectEnclosingSymbol"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -821,12 +821,5 @@
|
||||
"shift-end": "terminal::ScrollToBottom",
|
||||
"ctrl-shift-space": "terminal::ToggleViMode"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ZedPredictModal",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"escape": "menu::Cancel"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -132,7 +132,8 @@
|
||||
"cmd-alt-g b": "editor::ToggleGitBlame",
|
||||
"cmd-i": "editor::ShowSignatureHelp",
|
||||
"ctrl-f12": "editor::GoToDeclaration",
|
||||
"alt-ctrl-f12": "editor::GoToDeclarationSplit"
|
||||
"alt-ctrl-f12": "editor::GoToDeclarationSplit",
|
||||
"alt-enter": "editor::OpenSelectionsInMultibuffer"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -150,8 +151,7 @@
|
||||
"cmd-e": ["buffer_search::Deploy", { "focus": false }],
|
||||
"cmd->": "assistant::QuoteSelection",
|
||||
"cmd-<": "assistant::InsertIntoEditor",
|
||||
"cmd-alt-e": "editor::SelectEnclosingSymbol",
|
||||
"alt-enter": "editor::OpenSelectionsInMultibuffer"
|
||||
"cmd-alt-e": "editor::SelectEnclosingSymbol"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -881,7 +881,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ZedPredictModal",
|
||||
"context": "ZedPredictTos",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"escape": "menu::Cancel"
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
"cmd-b": "editor::GoToDefinition",
|
||||
"cmd-j": "editor::ScrollCursorCenter",
|
||||
"cmd-enter": "editor::NewlineBelow",
|
||||
"cmd-alt-enter": "editor::NewlineAbove",
|
||||
"cmd-alt-enter": "editor::NewLineAbove",
|
||||
"cmd-shift-l": "editor::SelectLine",
|
||||
"cmd-shift-t": "outline::Toggle",
|
||||
"alt-backspace": "editor::DeleteToPreviousWordStart",
|
||||
@@ -70,7 +70,7 @@
|
||||
"bindings": {
|
||||
"cmd-backspace": ["project_panel::Trash", { "skip_prompt": true }],
|
||||
"cmd-d": "project_panel::Duplicate",
|
||||
"cmd-n": "project_panel::NewDirectory",
|
||||
"cmd-n": "project_panel::NewFolder",
|
||||
"return": "project_panel::Rename",
|
||||
"cmd-c": "project_panel::Copy",
|
||||
"cmd-v": "project_panel::Paste",
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
"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:
|
||||
@@ -776,14 +775,7 @@
|
||||
"load_direnv": "direct",
|
||||
"inline_completions": {
|
||||
// A list of globs representing files that inline completions should be disabled for.
|
||||
"disabled_globs": [
|
||||
"**/.env*",
|
||||
"**/*.pem",
|
||||
"**/*.key",
|
||||
"**/*.cert",
|
||||
"**/*.crt",
|
||||
"**/secrets.yml"
|
||||
]
|
||||
"disabled_globs": [".env"]
|
||||
},
|
||||
// Settings specific to journaling
|
||||
"journal": {
|
||||
|
||||
@@ -121,7 +121,9 @@ pub enum Event {
|
||||
},
|
||||
ShowContacts,
|
||||
ParticipantIndicesChanged,
|
||||
PrivateUserInfoUpdated,
|
||||
TermsStatusUpdated {
|
||||
accepted: bool,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
@@ -225,7 +227,9 @@ impl UserStore {
|
||||
};
|
||||
|
||||
this.set_current_user_accepted_tos_at(accepted_tos_at);
|
||||
cx.emit(Event::PrivateUserInfoUpdated);
|
||||
cx.emit(Event::TermsStatusUpdated {
|
||||
accepted: accepted_tos_at.is_some(),
|
||||
});
|
||||
})
|
||||
} else {
|
||||
anyhow::Ok(())
|
||||
@@ -240,8 +244,6 @@ 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()
|
||||
})?
|
||||
@@ -712,7 +714,7 @@ impl UserStore {
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.set_current_user_accepted_tos_at(Some(response.accepted_tos_at));
|
||||
cx.emit(Event::PrivateUserInfoUpdated);
|
||||
cx.emit(Event::TermsStatusUpdated { accepted: true });
|
||||
})
|
||||
} else {
|
||||
Err(anyhow!("client not found"))
|
||||
|
||||
@@ -5,7 +5,6 @@ pub mod extensions;
|
||||
pub mod ips_file;
|
||||
pub mod slack;
|
||||
|
||||
use crate::api::events::SnowflakeRow;
|
||||
use crate::{
|
||||
auth,
|
||||
db::{User, UserId},
|
||||
@@ -100,7 +99,6 @@ pub fn routes(rpc_server: Arc<rpc::Server>) -> Router<(), Body> {
|
||||
.route("/user", get(get_authenticated_user))
|
||||
.route("/users/:id/access_tokens", post(create_access_token))
|
||||
.route("/rpc_server_snapshot", get(get_rpc_server_snapshot))
|
||||
.route("/snowflake/events", post(write_snowflake_event))
|
||||
.merge(billing::router())
|
||||
.merge(contributors::router())
|
||||
.layer(
|
||||
@@ -247,19 +245,3 @@ async fn create_access_token(
|
||||
encrypted_access_token,
|
||||
}))
|
||||
}
|
||||
|
||||
/// An endpoint that writes a Snowflake event to our event stream.
|
||||
///
|
||||
/// This endpoint is exposed such that other internal services can write
|
||||
/// telemetry events without needing to talk to AWS Kinesis directly.
|
||||
async fn write_snowflake_event(
|
||||
Extension(app): Extension<Arc<AppState>>,
|
||||
Json(event): Json<SnowflakeRow>,
|
||||
) -> Result<()> {
|
||||
let kinesis_client = app.kinesis_client.clone();
|
||||
let kinesis_stream = app.config.kinesis_stream.clone();
|
||||
|
||||
event.write(&kinesis_client, &kinesis_stream).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -447,7 +447,7 @@ async fn predict_edits(
|
||||
));
|
||||
}
|
||||
|
||||
let should_sample = claims.is_staff || params.can_collect_data;
|
||||
let sample_input_output = claims.is_staff && rand::random::<f32>() < 0.1;
|
||||
|
||||
let api_url = state
|
||||
.config
|
||||
@@ -541,7 +541,7 @@ async fn predict_edits(
|
||||
let output = choice.text.clone();
|
||||
|
||||
async move {
|
||||
let properties = if should_sample {
|
||||
let properties = if sample_input_output {
|
||||
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 = "o3-mini")]
|
||||
O3Mini,
|
||||
#[serde(alias = "o1-mini", rename = "o1-mini")]
|
||||
O1Mini,
|
||||
#[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::O3Mini | Self::O1 => false,
|
||||
Self::O1Mini | 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),
|
||||
"o3-mini" => Ok(Self::O3Mini),
|
||||
"o1-mini" => Ok(Self::O1Mini),
|
||||
"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::O3Mini => "o3-mini",
|
||||
Self::O1Mini => "o1-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::O3Mini => "o3-mini",
|
||||
Self::O1Mini => "o1-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::O3Mini => 20000,
|
||||
Self::O1Mini => 20000,
|
||||
Self::O1 => 20000,
|
||||
Self::Claude3_5Sonnet => 200_000,
|
||||
}
|
||||
|
||||
@@ -256,6 +256,7 @@ impl InlineCompletionProvider for CopilotCompletionProvider {
|
||||
let position = cursor_position.bias_right(buffer);
|
||||
Some(InlineCompletion {
|
||||
edits: vec![(position..position, completion_text.into())],
|
||||
edit_preview: None,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -466,8 +466,7 @@ impl ProjectDiagnosticsEditor {
|
||||
}
|
||||
|
||||
let excerpt_id = excerpts
|
||||
.insert_excerpts_after(
|
||||
prev_excerpt_id,
|
||||
.push_excerpts(
|
||||
buffer.clone(),
|
||||
[ExcerptRange {
|
||||
context: context_range.clone(),
|
||||
|
||||
@@ -88,7 +88,7 @@ url.workspace = true
|
||||
util.workspace = true
|
||||
uuid.workspace = true
|
||||
workspace.workspace = true
|
||||
zed_predict_onboarding.workspace = true
|
||||
zed_predict_tos.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
ctor.workspace = true
|
||||
|
||||
@@ -397,4 +397,4 @@ gpui::actions!(
|
||||
action_as!(go_to_line, ToggleGoToLine as Toggle);
|
||||
|
||||
action_with_deprecated_aliases!(editor, OpenSelectedFilename, ["editor::OpenFile"]);
|
||||
action_with_deprecated_aliases!(editor, ToggleSelectedDiffHunks, ["editor::ToggleHunkDiff"]);
|
||||
action_with_deprecated_aliases!(editor, ToggleSelectedDiffHunks, ["editor::ToggleDiffHunk"]);
|
||||
|
||||
@@ -652,7 +652,7 @@ impl CompletionsMenu {
|
||||
)
|
||||
.on_click(cx.listener(move |editor, _event, window, cx| {
|
||||
cx.stop_propagation();
|
||||
editor.toggle_zed_predict_onboarding(window, cx);
|
||||
editor.toggle_zed_predict_tos(window, cx);
|
||||
})),
|
||||
),
|
||||
|
||||
@@ -728,13 +728,13 @@ impl CompletionsMenu {
|
||||
}
|
||||
CompletionEntry::InlineCompletionHint(InlineCompletionMenuHint::Loaded { text }) => {
|
||||
match text {
|
||||
InlineCompletionText::Edit { text, highlights } => div()
|
||||
InlineCompletionText::Edit(highlighted_edits) => div()
|
||||
.mx_1()
|
||||
.rounded_md()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.child(
|
||||
gpui::StyledText::new(text.clone())
|
||||
.with_highlights(&style.text, highlights.clone()),
|
||||
gpui::StyledText::new(highlighted_edits.text.clone())
|
||||
.with_highlights(&style.text, highlighted_edits.highlights.clone()),
|
||||
),
|
||||
InlineCompletionText::Move(text) => div().child(text.clone()),
|
||||
}
|
||||
|
||||
@@ -1140,7 +1140,12 @@ impl DisplaySnapshot {
|
||||
}
|
||||
|
||||
pub fn line_indent_for_buffer_row(&self, buffer_row: MultiBufferRow) -> LineIndent {
|
||||
self.buffer_snapshot.line_indent_for_row(buffer_row)
|
||||
let (buffer, range) = self
|
||||
.buffer_snapshot
|
||||
.buffer_line_for_row(buffer_row)
|
||||
.unwrap();
|
||||
|
||||
buffer.line_indent_for_row(range.start.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_onboarding::ZedPredictModal;
|
||||
use zed_predict_tos::ZedPredictTos;
|
||||
|
||||
use code_context_menus::{
|
||||
AvailableCodeAction, CodeActionContents, CodeActionsItem, CodeActionsMenu, CodeContextMenu,
|
||||
@@ -96,8 +96,9 @@ use itertools::Itertools;
|
||||
use language::{
|
||||
language_settings::{self, all_language_settings, language_settings, InlayHintSettings},
|
||||
markdown, point_from_lsp, AutoindentMode, BracketPair, Buffer, Capability, CharKind, CodeLabel,
|
||||
CursorShape, Diagnostic, Documentation, IndentKind, IndentSize, Language, OffsetRangeExt,
|
||||
Point, Selection, SelectionGoal, TextObject, TransactionId, TreeSitterOptions,
|
||||
CursorShape, Diagnostic, Documentation, EditPreview, HighlightedEdits, IndentKind, IndentSize,
|
||||
Language, OffsetRangeExt, Point, Selection, SelectionGoal, TextObject, TransactionId,
|
||||
TreeSitterOptions,
|
||||
};
|
||||
use language::{point_to_lsp, BufferRow, CharClassifier, Runnable, RunnableRange};
|
||||
use linked_editing_ranges::refresh_linked_ranges;
|
||||
@@ -116,6 +117,7 @@ use lsp::{
|
||||
LanguageServerId, LanguageServerName,
|
||||
};
|
||||
|
||||
use language::BufferSnapshot;
|
||||
use movement::TextLayoutDetails;
|
||||
pub use multi_buffer::{
|
||||
Anchor, AnchorRangeExt, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, RowInfo,
|
||||
@@ -486,10 +488,7 @@ impl InlineCompletionMenuHint {
|
||||
#[derive(Clone, Debug)]
|
||||
enum InlineCompletionText {
|
||||
Move(SharedString),
|
||||
Edit {
|
||||
text: SharedString,
|
||||
highlights: Vec<(Range<usize>, HighlightStyle)>,
|
||||
},
|
||||
Edit(HighlightedEdits),
|
||||
}
|
||||
|
||||
pub(crate) enum EditDisplayMode {
|
||||
@@ -501,7 +500,9 @@ pub(crate) enum EditDisplayMode {
|
||||
enum InlineCompletion {
|
||||
Edit {
|
||||
edits: Vec<(Range<Anchor>, String)>,
|
||||
edit_preview: Option<EditPreview>,
|
||||
display_mode: EditDisplayMode,
|
||||
snapshot: BufferSnapshot,
|
||||
},
|
||||
Move(Anchor),
|
||||
}
|
||||
@@ -723,7 +724,6 @@ 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<()>>,
|
||||
@@ -1383,7 +1383,6 @@ 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,
|
||||
@@ -3949,21 +3948,12 @@ impl Editor {
|
||||
self.do_completion(action.item_ix, CompletionIntent::Compose, window, cx)
|
||||
}
|
||||
|
||||
fn toggle_zed_predict_onboarding(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
fn toggle_zed_predict_tos(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let (Some(workspace), Some(project)) = (self.workspace(), self.project.as_ref()) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let project = project.read(cx);
|
||||
|
||||
ZedPredictModal::toggle(
|
||||
workspace,
|
||||
project.user_store().clone(),
|
||||
project.client().clone(),
|
||||
project.fs().clone(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
ZedPredictTos::toggle(workspace, project.read(cx).user_store().clone(), window, cx);
|
||||
}
|
||||
|
||||
fn do_completion(
|
||||
@@ -3995,7 +3985,7 @@ impl Editor {
|
||||
)) => {
|
||||
drop(entries);
|
||||
drop(context_menu);
|
||||
self.toggle_zed_predict_onboarding(window, cx);
|
||||
self.toggle_zed_predict_tos(window, cx);
|
||||
return Some(Task::ready(Ok(())));
|
||||
}
|
||||
_ => {}
|
||||
@@ -4858,10 +4848,7 @@ impl Editor {
|
||||
selections.select_anchor_ranges([position..position]);
|
||||
});
|
||||
}
|
||||
InlineCompletion::Edit {
|
||||
edits,
|
||||
display_mode: _,
|
||||
} => {
|
||||
InlineCompletion::Edit { edits, .. } => {
|
||||
if let Some(provider) = self.inline_completion_provider() {
|
||||
provider.accept(cx);
|
||||
}
|
||||
@@ -4909,10 +4896,7 @@ impl Editor {
|
||||
selections.select_anchor_ranges([position..position]);
|
||||
});
|
||||
}
|
||||
InlineCompletion::Edit {
|
||||
edits,
|
||||
display_mode: _,
|
||||
} => {
|
||||
InlineCompletion::Edit { edits, .. } => {
|
||||
// Find an insertion that starts at the cursor position.
|
||||
let snapshot = self.buffer.read(cx).snapshot(cx);
|
||||
let cursor_offset = self.selections.newest::<usize>(cx).head();
|
||||
@@ -5051,8 +5035,8 @@ impl Editor {
|
||||
let (buffer, cursor_buffer_position) =
|
||||
self.buffer.read(cx).text_anchor_for_position(cursor, cx)?;
|
||||
|
||||
let completion = provider.suggest(&buffer, cursor_buffer_position, cx)?;
|
||||
let edits = completion
|
||||
let inline_completion = provider.suggest(&buffer, cursor_buffer_position, cx)?;
|
||||
let edits = inline_completion
|
||||
.edits
|
||||
.into_iter()
|
||||
.flat_map(|(range, new_text)| {
|
||||
@@ -5077,13 +5061,12 @@ impl Editor {
|
||||
|
||||
let mut inlay_ids = Vec::new();
|
||||
let invalidation_row_range;
|
||||
let completion;
|
||||
if cursor_row < edit_start_row {
|
||||
let completion = if cursor_row < edit_start_row {
|
||||
invalidation_row_range = cursor_row..edit_end_row;
|
||||
completion = InlineCompletion::Move(first_edit_start);
|
||||
InlineCompletion::Move(first_edit_start)
|
||||
} else if cursor_row > edit_end_row {
|
||||
invalidation_row_range = edit_start_row..cursor_row;
|
||||
completion = InlineCompletion::Move(first_edit_start);
|
||||
InlineCompletion::Move(first_edit_start)
|
||||
} else {
|
||||
if edits
|
||||
.iter()
|
||||
@@ -5128,10 +5111,14 @@ impl Editor {
|
||||
EditDisplayMode::DiffPopover
|
||||
};
|
||||
|
||||
completion = InlineCompletion::Edit {
|
||||
let snapshot = multibuffer.buffer_for_excerpt(excerpt_id).cloned()?;
|
||||
|
||||
InlineCompletion::Edit {
|
||||
edits,
|
||||
edit_preview: inline_completion.edit_preview,
|
||||
display_mode,
|
||||
};
|
||||
snapshot,
|
||||
}
|
||||
};
|
||||
|
||||
let invalidation_range = multibuffer
|
||||
@@ -5175,19 +5162,26 @@ impl Editor {
|
||||
let text = match &self.active_inline_completion.as_ref()?.completion {
|
||||
InlineCompletion::Edit {
|
||||
edits,
|
||||
edit_preview,
|
||||
display_mode: _,
|
||||
} => inline_completion_edit_text(&editor_snapshot, edits, true, cx),
|
||||
snapshot,
|
||||
} => edit_preview
|
||||
.as_ref()
|
||||
.and_then(|edit_preview| {
|
||||
inline_completion_edit_text(&snapshot, &edits, edit_preview, true, cx)
|
||||
})
|
||||
.map(InlineCompletionText::Edit),
|
||||
InlineCompletion::Move(target) => {
|
||||
let target_point =
|
||||
target.to_point(&editor_snapshot.display_snapshot.buffer_snapshot);
|
||||
let target_line = target_point.row + 1;
|
||||
InlineCompletionText::Move(
|
||||
Some(InlineCompletionText::Move(
|
||||
format!("Jump to edit in line {}", target_line).into(),
|
||||
)
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
Some(InlineCompletionMenuHint::Loaded { text })
|
||||
Some(InlineCompletionMenuHint::Loaded { text: text? })
|
||||
} else if provider.is_refreshing(cx) {
|
||||
Some(InlineCompletionMenuHint::Loading)
|
||||
} else if provider.needs_terms_acceptance(cx) {
|
||||
@@ -13344,11 +13338,7 @@ impl Editor {
|
||||
refresh_linked_ranges(self, window, cx);
|
||||
telemetry.log_edit_event("editor", is_via_ssh);
|
||||
}
|
||||
multi_buffer::Event::ExcerptsAdded {
|
||||
buffer,
|
||||
predecessor,
|
||||
excerpts,
|
||||
} => {
|
||||
multi_buffer::Event::ExcerptsAdded { buffer, excerpts } => {
|
||||
self.tasks_update_task = Some(self.refresh_runnables(window, cx));
|
||||
let buffer_id = buffer.read(cx).remote_id();
|
||||
if self.buffer.read(cx).change_set_for(buffer_id).is_none() {
|
||||
@@ -13363,7 +13353,6 @@ impl Editor {
|
||||
}
|
||||
cx.emit(EditorEvent::ExcerptsAdded {
|
||||
buffer: buffer.clone(),
|
||||
predecessor: *predecessor,
|
||||
excerpts: excerpts.clone(),
|
||||
});
|
||||
self.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx);
|
||||
@@ -14097,7 +14086,7 @@ impl Editor {
|
||||
.and_then(|item| item.to_any().downcast_ref::<T>())
|
||||
}
|
||||
|
||||
fn character_size(&self, window: &mut Window) -> gpui::Size<Pixels> {
|
||||
fn character_size(&self, window: &mut Window) -> gpui::Point<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());
|
||||
@@ -14111,7 +14100,7 @@ impl Editor {
|
||||
.size
|
||||
.width;
|
||||
|
||||
gpui::Size::new(em_width, line_height)
|
||||
gpui::Point::new(em_width, line_height)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15257,7 +15246,6 @@ pub enum EditorEvent {
|
||||
},
|
||||
ExcerptsAdded {
|
||||
buffer: Entity<Buffer>,
|
||||
predecessor: ExcerptId,
|
||||
excerpts: Vec<(ExcerptId, ExcerptRange<language::Anchor>)>,
|
||||
},
|
||||
ExcerptsRemoved {
|
||||
@@ -15612,9 +15600,9 @@ impl EntityInputHandler for Editor {
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<gpui::Bounds<Pixels>> {
|
||||
let text_layout_details = self.text_layout_details(window);
|
||||
let gpui::Size {
|
||||
width: em_width,
|
||||
height: line_height,
|
||||
let gpui::Point {
|
||||
x: em_width,
|
||||
y: line_height,
|
||||
} = self.character_size(window);
|
||||
|
||||
let snapshot = self.snapshot(window, cx);
|
||||
@@ -15632,24 +15620,6 @@ 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 {
|
||||
@@ -15848,74 +15818,23 @@ pub fn diagnostic_block_renderer(
|
||||
}
|
||||
|
||||
fn inline_completion_edit_text(
|
||||
editor_snapshot: &EditorSnapshot,
|
||||
edits: &Vec<(Range<Anchor>, String)>,
|
||||
current_snapshot: &BufferSnapshot,
|
||||
edits: &[(Range<Anchor>, String)],
|
||||
edit_preview: &EditPreview,
|
||||
include_deletions: bool,
|
||||
cx: &App,
|
||||
) -> InlineCompletionText {
|
||||
let edit_start = edits
|
||||
.first()
|
||||
.unwrap()
|
||||
.0
|
||||
.start
|
||||
.to_display_point(editor_snapshot);
|
||||
) -> Option<HighlightedEdits> {
|
||||
let edits = edits
|
||||
.iter()
|
||||
.map(|(anchor, text)| {
|
||||
(
|
||||
anchor.start.text_anchor..anchor.end.text_anchor,
|
||||
text.clone(),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let mut text = String::new();
|
||||
let mut offset = DisplayPoint::new(edit_start.row(), 0).to_offset(editor_snapshot, Bias::Left);
|
||||
let mut highlights = Vec::new();
|
||||
for (old_range, new_text) in edits {
|
||||
let old_offset_range = old_range.to_offset(&editor_snapshot.buffer_snapshot);
|
||||
text.extend(
|
||||
editor_snapshot
|
||||
.buffer_snapshot
|
||||
.chunks(offset..old_offset_range.start, false)
|
||||
.map(|chunk| chunk.text),
|
||||
);
|
||||
offset = old_offset_range.end;
|
||||
|
||||
let start = text.len();
|
||||
let color = if include_deletions && new_text.is_empty() {
|
||||
text.extend(
|
||||
editor_snapshot
|
||||
.buffer_snapshot
|
||||
.chunks(old_offset_range.start..offset, false)
|
||||
.map(|chunk| chunk.text),
|
||||
);
|
||||
cx.theme().status().deleted_background
|
||||
} else {
|
||||
text.push_str(new_text);
|
||||
cx.theme().status().created_background
|
||||
};
|
||||
let end = text.len();
|
||||
|
||||
highlights.push((
|
||||
start..end,
|
||||
HighlightStyle {
|
||||
background_color: Some(color),
|
||||
..Default::default()
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
let edit_end = edits
|
||||
.last()
|
||||
.unwrap()
|
||||
.0
|
||||
.end
|
||||
.to_display_point(editor_snapshot);
|
||||
let end_of_line = DisplayPoint::new(edit_end.row(), editor_snapshot.line_len(edit_end.row()))
|
||||
.to_offset(editor_snapshot, Bias::Right);
|
||||
text.extend(
|
||||
editor_snapshot
|
||||
.buffer_snapshot
|
||||
.chunks(offset..end_of_line, false)
|
||||
.map(|chunk| chunk.text),
|
||||
);
|
||||
|
||||
InlineCompletionText::Edit {
|
||||
text: text.into(),
|
||||
highlights,
|
||||
}
|
||||
Some(edit_preview.highlight_edits(current_snapshot, &edits, include_deletions, cx))
|
||||
}
|
||||
|
||||
pub fn highlight_diagnostic_message(
|
||||
|
||||
@@ -10431,8 +10431,7 @@ async fn test_following_with_multiple_excerpts(cx: &mut gpui::TestAppContext) {
|
||||
],
|
||||
cx,
|
||||
);
|
||||
multibuffer.insert_excerpts_after(
|
||||
excerpt_ids[0],
|
||||
multibuffer.push_excerpts(
|
||||
buffer_2.clone(),
|
||||
[
|
||||
ExcerptRange {
|
||||
@@ -15258,241 +15257,205 @@ async fn test_multi_buffer_with_single_excerpt_folding(cx: &mut gpui::TestAppCon
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_inline_completion_text(cx: &mut TestAppContext) {
|
||||
async fn test_inline_completion_text(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
// Simple insertion
|
||||
{
|
||||
let window = cx.add_window(|window, cx| {
|
||||
let buffer = MultiBuffer::build_simple("Hello, world!", cx);
|
||||
Editor::new(EditorMode::Full, buffer, None, true, window, cx)
|
||||
});
|
||||
let cx = &mut VisualTestContext::from_window(*window, cx);
|
||||
|
||||
window
|
||||
.update(cx, |editor, window, cx| {
|
||||
let snapshot = editor.snapshot(window, cx);
|
||||
let edit_range = snapshot.buffer_snapshot.anchor_after(Point::new(0, 6))
|
||||
..snapshot.buffer_snapshot.anchor_before(Point::new(0, 6));
|
||||
let edits = vec![(edit_range, " beautiful".to_string())];
|
||||
|
||||
let InlineCompletionText::Edit { text, highlights } =
|
||||
inline_completion_edit_text(&snapshot, &edits, false, cx)
|
||||
else {
|
||||
panic!("Failed to generate inline completion text");
|
||||
};
|
||||
|
||||
assert_eq!(text, "Hello, beautiful world!");
|
||||
assert_eq!(highlights.len(), 1);
|
||||
assert_eq!(highlights[0].0, 6..16);
|
||||
assert_eq!(
|
||||
highlights[0].1.background_color,
|
||||
Some(cx.theme().status().created_background)
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
assert_highlighted_edits(
|
||||
"Hello, world!",
|
||||
vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
|
||||
true,
|
||||
cx,
|
||||
|highlighted_edits, cx| {
|
||||
assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
|
||||
assert_eq!(highlighted_edits.highlights.len(), 1);
|
||||
assert_eq!(highlighted_edits.highlights[0].0, 6..16);
|
||||
assert_eq!(
|
||||
highlighted_edits.highlights[0].1.background_color,
|
||||
Some(cx.theme().status().created_background)
|
||||
);
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
// Replacement
|
||||
{
|
||||
let window = cx.add_window(|window, cx| {
|
||||
let buffer = MultiBuffer::build_simple("This is a test.", cx);
|
||||
Editor::new(EditorMode::Full, buffer, None, true, window, cx)
|
||||
});
|
||||
let cx = &mut VisualTestContext::from_window(*window, cx);
|
||||
|
||||
window
|
||||
.update(cx, |editor, window, cx| {
|
||||
let snapshot = editor.snapshot(window, cx);
|
||||
let edits = vec![(
|
||||
snapshot.buffer_snapshot.anchor_after(Point::new(0, 0))
|
||||
..snapshot.buffer_snapshot.anchor_before(Point::new(0, 4)),
|
||||
"That".to_string(),
|
||||
)];
|
||||
|
||||
let InlineCompletionText::Edit { text, highlights } =
|
||||
inline_completion_edit_text(&snapshot, &edits, false, cx)
|
||||
else {
|
||||
panic!("Failed to generate inline completion text");
|
||||
};
|
||||
|
||||
assert_eq!(text, "That is a test.");
|
||||
assert_eq!(highlights.len(), 1);
|
||||
assert_eq!(highlights[0].0, 0..4);
|
||||
assert_eq!(
|
||||
highlights[0].1.background_color,
|
||||
Some(cx.theme().status().created_background)
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
assert_highlighted_edits(
|
||||
"This is a test.",
|
||||
vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
|
||||
false,
|
||||
cx,
|
||||
|highlighted_edits, cx| {
|
||||
assert_eq!(highlighted_edits.text, "That is a test.");
|
||||
assert_eq!(highlighted_edits.highlights.len(), 1);
|
||||
assert_eq!(highlighted_edits.highlights[0].0, 0..4);
|
||||
assert_eq!(
|
||||
highlighted_edits.highlights[0].1.background_color,
|
||||
Some(cx.theme().status().created_background)
|
||||
);
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
// Multiple edits
|
||||
{
|
||||
let window = cx.add_window(|window, cx| {
|
||||
let buffer = MultiBuffer::build_simple("Hello, world!", cx);
|
||||
Editor::new(EditorMode::Full, buffer, None, true, window, cx)
|
||||
});
|
||||
let cx = &mut VisualTestContext::from_window(*window, cx);
|
||||
|
||||
window
|
||||
.update(cx, |editor, window, cx| {
|
||||
let snapshot = editor.snapshot(window, cx);
|
||||
let edits = vec![
|
||||
(
|
||||
snapshot.buffer_snapshot.anchor_after(Point::new(0, 0))
|
||||
..snapshot.buffer_snapshot.anchor_before(Point::new(0, 5)),
|
||||
"Greetings".into(),
|
||||
),
|
||||
(
|
||||
snapshot.buffer_snapshot.anchor_after(Point::new(0, 12))
|
||||
..snapshot.buffer_snapshot.anchor_before(Point::new(0, 12)),
|
||||
" and universe".into(),
|
||||
),
|
||||
];
|
||||
|
||||
let InlineCompletionText::Edit { text, highlights } =
|
||||
inline_completion_edit_text(&snapshot, &edits, false, cx)
|
||||
else {
|
||||
panic!("Failed to generate inline completion text");
|
||||
};
|
||||
|
||||
assert_eq!(text, "Greetings, world and universe!");
|
||||
assert_eq!(highlights.len(), 2);
|
||||
assert_eq!(highlights[0].0, 0..9);
|
||||
assert_eq!(highlights[1].0, 16..29);
|
||||
assert_eq!(
|
||||
highlights[0].1.background_color,
|
||||
Some(cx.theme().status().created_background)
|
||||
);
|
||||
assert_eq!(
|
||||
highlights[1].1.background_color,
|
||||
Some(cx.theme().status().created_background)
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
assert_highlighted_edits(
|
||||
"Hello, world!",
|
||||
vec![
|
||||
(Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
|
||||
(Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
|
||||
],
|
||||
false,
|
||||
cx,
|
||||
|highlighted_edits, cx| {
|
||||
assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
|
||||
assert_eq!(highlighted_edits.highlights.len(), 2);
|
||||
assert_eq!(highlighted_edits.highlights[0].0, 0..9);
|
||||
assert_eq!(highlighted_edits.highlights[1].0, 16..29);
|
||||
assert_eq!(
|
||||
highlighted_edits.highlights[0].1.background_color,
|
||||
Some(cx.theme().status().created_background)
|
||||
);
|
||||
assert_eq!(
|
||||
highlighted_edits.highlights[1].1.background_color,
|
||||
Some(cx.theme().status().created_background)
|
||||
);
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
// Multiple lines with edits
|
||||
{
|
||||
let window = cx.add_window(|window, cx| {
|
||||
let buffer =
|
||||
MultiBuffer::build_simple("First line\nSecond line\nThird line\nFourth line", cx);
|
||||
Editor::new(EditorMode::Full, buffer, None, true, window, cx)
|
||||
});
|
||||
let cx = &mut VisualTestContext::from_window(*window, cx);
|
||||
|
||||
window
|
||||
.update(cx, |editor, window, cx| {
|
||||
let snapshot = editor.snapshot(window, cx);
|
||||
let edits = vec![
|
||||
(
|
||||
snapshot.buffer_snapshot.anchor_before(Point::new(1, 7))
|
||||
..snapshot.buffer_snapshot.anchor_before(Point::new(1, 11)),
|
||||
"modified".to_string(),
|
||||
),
|
||||
(
|
||||
snapshot.buffer_snapshot.anchor_before(Point::new(2, 0))
|
||||
..snapshot.buffer_snapshot.anchor_before(Point::new(2, 10)),
|
||||
"New third line".to_string(),
|
||||
),
|
||||
(
|
||||
snapshot.buffer_snapshot.anchor_before(Point::new(3, 6))
|
||||
..snapshot.buffer_snapshot.anchor_before(Point::new(3, 6)),
|
||||
" updated".to_string(),
|
||||
),
|
||||
];
|
||||
|
||||
let InlineCompletionText::Edit { text, highlights } =
|
||||
inline_completion_edit_text(&snapshot, &edits, false, cx)
|
||||
else {
|
||||
panic!("Failed to generate inline completion text");
|
||||
};
|
||||
|
||||
assert_eq!(text, "Second modified\nNew third line\nFourth updated line");
|
||||
assert_eq!(highlights.len(), 3);
|
||||
assert_eq!(highlights[0].0, 7..15); // "modified"
|
||||
assert_eq!(highlights[1].0, 16..30); // "New third line"
|
||||
assert_eq!(highlights[2].0, 37..45); // " updated"
|
||||
|
||||
for highlight in &highlights {
|
||||
assert_eq!(
|
||||
highlight.1.background_color,
|
||||
Some(cx.theme().status().created_background)
|
||||
);
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
assert_highlighted_edits(
|
||||
"First line\nSecond line\nThird line\nFourth line",
|
||||
vec![
|
||||
(Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
|
||||
(
|
||||
Point::new(2, 0)..Point::new(2, 10),
|
||||
"New third line".to_string(),
|
||||
),
|
||||
(Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
|
||||
],
|
||||
false,
|
||||
cx,
|
||||
|highlighted_edits, cx| {
|
||||
assert_eq!(
|
||||
highlighted_edits.text,
|
||||
"Second modified\nNew third line\nFourth updated line"
|
||||
);
|
||||
assert_eq!(highlighted_edits.highlights.len(), 3);
|
||||
assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
|
||||
assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
|
||||
assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
|
||||
for highlight in &highlighted_edits.highlights {
|
||||
assert_eq!(
|
||||
highlight.1.background_color,
|
||||
Some(cx.theme().status().created_background)
|
||||
);
|
||||
}
|
||||
},
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_inline_completion_text_with_deletions(cx: &mut TestAppContext) {
|
||||
async fn test_inline_completion_text_with_deletions(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
// Deletion
|
||||
{
|
||||
let window = cx.add_window(|window, cx| {
|
||||
let buffer = MultiBuffer::build_simple("Hello, world!", cx);
|
||||
Editor::new(EditorMode::Full, buffer, None, true, window, cx)
|
||||
});
|
||||
let cx = &mut VisualTestContext::from_window(*window, cx);
|
||||
|
||||
window
|
||||
.update(cx, |editor, window, cx| {
|
||||
let snapshot = editor.snapshot(window, cx);
|
||||
let edit_range = snapshot.buffer_snapshot.anchor_after(Point::new(0, 5))
|
||||
..snapshot.buffer_snapshot.anchor_before(Point::new(0, 11));
|
||||
let edits = vec![(edit_range, "".to_string())];
|
||||
|
||||
let InlineCompletionText::Edit { text, highlights } =
|
||||
inline_completion_edit_text(&snapshot, &edits, true, cx)
|
||||
else {
|
||||
panic!("Failed to generate inline completion text");
|
||||
};
|
||||
|
||||
assert_eq!(text, "Hello, world!");
|
||||
assert_eq!(highlights.len(), 1);
|
||||
assert_eq!(highlights[0].0, 5..11);
|
||||
assert_eq!(
|
||||
highlights[0].1.background_color,
|
||||
Some(cx.theme().status().deleted_background)
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
assert_highlighted_edits(
|
||||
"Hello, world!",
|
||||
vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
|
||||
true,
|
||||
cx,
|
||||
|highlighted_edits, cx| {
|
||||
assert_eq!(highlighted_edits.text, "Hello, world!");
|
||||
assert_eq!(highlighted_edits.highlights.len(), 1);
|
||||
assert_eq!(highlighted_edits.highlights[0].0, 5..11);
|
||||
assert_eq!(
|
||||
highlighted_edits.highlights[0].1.background_color,
|
||||
Some(cx.theme().status().deleted_background)
|
||||
);
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
// Insertion
|
||||
{
|
||||
let window = cx.add_window(|window, cx| {
|
||||
let buffer = MultiBuffer::build_simple("Hello, world!", cx);
|
||||
Editor::new(EditorMode::Full, buffer, None, true, window, cx)
|
||||
});
|
||||
let cx = &mut VisualTestContext::from_window(*window, cx);
|
||||
assert_highlighted_edits(
|
||||
"Hello, world!",
|
||||
vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
|
||||
true,
|
||||
cx,
|
||||
|highlighted_edits, cx| {
|
||||
assert_eq!(highlighted_edits.highlights.len(), 1);
|
||||
assert_eq!(highlighted_edits.highlights[0].0, 6..14);
|
||||
assert_eq!(
|
||||
highlighted_edits.highlights[0].1.background_color,
|
||||
Some(cx.theme().status().created_background)
|
||||
);
|
||||
},
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
window
|
||||
.update(cx, |editor, window, cx| {
|
||||
let snapshot = editor.snapshot(window, cx);
|
||||
let edit_range = snapshot.buffer_snapshot.anchor_after(Point::new(0, 6))
|
||||
..snapshot.buffer_snapshot.anchor_before(Point::new(0, 6));
|
||||
let edits = vec![(edit_range, " digital".to_string())];
|
||||
async fn assert_highlighted_edits(
|
||||
text: &str,
|
||||
edits: Vec<(Range<Point>, String)>,
|
||||
include_deletions: bool,
|
||||
cx: &mut TestAppContext,
|
||||
assertion_fn: impl Fn(HighlightedEdits, &App),
|
||||
) {
|
||||
let window = cx.add_window(|window, cx| {
|
||||
let buffer = MultiBuffer::build_simple(text, cx);
|
||||
Editor::new(EditorMode::Full, buffer, None, true, window, cx)
|
||||
});
|
||||
let cx = &mut VisualTestContext::from_window(*window, cx);
|
||||
|
||||
let InlineCompletionText::Edit { text, highlights } =
|
||||
inline_completion_edit_text(&snapshot, &edits, true, cx)
|
||||
else {
|
||||
panic!("Failed to generate inline completion text");
|
||||
};
|
||||
let (buffer, snapshot) = window
|
||||
.update(cx, |editor, _window, cx| {
|
||||
(
|
||||
editor.buffer().clone(),
|
||||
editor.buffer().read(cx).snapshot(cx),
|
||||
)
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(text, "Hello, digital world!");
|
||||
assert_eq!(highlights.len(), 1);
|
||||
assert_eq!(highlights[0].0, 6..14);
|
||||
assert_eq!(
|
||||
highlights[0].1.background_color,
|
||||
Some(cx.theme().status().created_background)
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
let edits = edits
|
||||
.into_iter()
|
||||
.map(|(range, edit)| {
|
||||
(
|
||||
snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
|
||||
edit,
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let text_anchor_edits = edits
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let edit_preview = window
|
||||
.update(cx, |_, _window, cx| {
|
||||
buffer
|
||||
.read(cx)
|
||||
.as_singleton()
|
||||
.unwrap()
|
||||
.read(cx)
|
||||
.preview_edits(text_anchor_edits.into(), cx)
|
||||
})
|
||||
.unwrap()
|
||||
.await;
|
||||
|
||||
cx.update(|_window, cx| {
|
||||
let highlighted_edits = inline_completion_edit_text(
|
||||
&snapshot.as_singleton().unwrap().2,
|
||||
&edits,
|
||||
&edit_preview,
|
||||
include_deletions,
|
||||
cx,
|
||||
)
|
||||
.expect("Missing highlighted edits");
|
||||
assertion_fn(highlighted_edits, cx)
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
|
||||
@@ -502,6 +502,7 @@ 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;
|
||||
@@ -510,7 +511,7 @@ impl EditorElement {
|
||||
if editor.hover_state.focused(window, cx) {
|
||||
return;
|
||||
}
|
||||
Self::modifiers_changed(editor, event, &position_map, window, cx)
|
||||
Self::modifiers_changed(editor, event, &position_map, &text_hitbox, window, cx)
|
||||
})
|
||||
}
|
||||
});
|
||||
@@ -520,16 +521,17 @@ 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 !position_map.text_hitbox.is_hovered(window) {
|
||||
if !text_hitbox.is_hovered(window) {
|
||||
return;
|
||||
}
|
||||
|
||||
editor.update_hovered_link(
|
||||
position_map.point_for_position(mouse_position),
|
||||
position_map.point_for_position(text_hitbox.bounds, mouse_position),
|
||||
&position_map.snapshot,
|
||||
event.modifiers,
|
||||
window,
|
||||
@@ -537,11 +539,14 @@ 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>,
|
||||
@@ -550,8 +555,6 @@ 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;
|
||||
|
||||
@@ -608,7 +611,8 @@ impl EditorElement {
|
||||
}
|
||||
}
|
||||
|
||||
let point_for_position = position_map.point_for_position(event.position);
|
||||
let point_for_position =
|
||||
position_map.point_for_position(text_hitbox.bounds, event.position);
|
||||
let position = point_for_position.previous_valid;
|
||||
if modifiers.shift && modifiers.alt {
|
||||
editor.select(
|
||||
@@ -683,13 +687,15 @@ impl EditorElement {
|
||||
editor: &mut Editor,
|
||||
event: &MouseDownEvent,
|
||||
position_map: &PositionMap,
|
||||
text_hitbox: &Hitbox,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Editor>,
|
||||
) {
|
||||
if !position_map.text_hitbox.is_hovered(window) {
|
||||
if !text_hitbox.is_hovered(window) {
|
||||
return;
|
||||
}
|
||||
let point_for_position = position_map.point_for_position(event.position);
|
||||
let point_for_position =
|
||||
position_map.point_for_position(text_hitbox.bounds, event.position);
|
||||
mouse_context_menu::deploy_context_menu(
|
||||
editor,
|
||||
Some(event.position),
|
||||
@@ -704,14 +710,16 @@ impl EditorElement {
|
||||
editor: &mut Editor,
|
||||
event: &MouseDownEvent,
|
||||
position_map: &PositionMap,
|
||||
text_hitbox: &Hitbox,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Editor>,
|
||||
) {
|
||||
if !position_map.text_hitbox.is_hovered(window) || window.default_prevented() {
|
||||
if !text_hitbox.is_hovered(window) || window.default_prevented() {
|
||||
return;
|
||||
}
|
||||
|
||||
let point_for_position = position_map.point_for_position(event.position);
|
||||
let point_for_position =
|
||||
position_map.point_for_position(text_hitbox.bounds, event.position);
|
||||
let position = point_for_position.previous_valid;
|
||||
|
||||
editor.select(
|
||||
@@ -729,10 +737,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();
|
||||
|
||||
@@ -747,7 +755,7 @@ impl EditorElement {
|
||||
};
|
||||
|
||||
if !pending_nonempty_selections && multi_cursor_modifier && text_hitbox.is_hovered(window) {
|
||||
let point = position_map.point_for_position(event.position);
|
||||
let point = position_map.point_for_position(text_hitbox.bounds, event.position);
|
||||
editor.handle_click_hovered_link(point, event.modifiers, window, cx);
|
||||
|
||||
cx.stop_propagation();
|
||||
@@ -763,7 +771,8 @@ 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(event.position);
|
||||
let point_for_position =
|
||||
position_map.point_for_position(text_hitbox.bounds, event.position);
|
||||
let position = point_for_position.previous_valid;
|
||||
|
||||
editor.select(
|
||||
@@ -786,6 +795,7 @@ impl EditorElement {
|
||||
editor: &mut Editor,
|
||||
event: &MouseMoveEvent,
|
||||
position_map: &PositionMap,
|
||||
text_bounds: Bounds<Pixels>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Editor>,
|
||||
) {
|
||||
@@ -793,8 +803,7 @@ impl EditorElement {
|
||||
return;
|
||||
}
|
||||
|
||||
let text_bounds = position_map.text_hitbox.bounds;
|
||||
let point_for_position = position_map.point_for_position(event.position);
|
||||
let point_for_position = position_map.point_for_position(text_bounds, 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;
|
||||
@@ -846,18 +855,19 @@ 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(event.position);
|
||||
let point_for_position =
|
||||
position_map.point_for_position(text_hitbox.bounds, event.position);
|
||||
|
||||
editor.update_hovered_link(
|
||||
point_for_position,
|
||||
@@ -3457,7 +3467,9 @@ impl EditorElement {
|
||||
}
|
||||
InlineCompletion::Edit {
|
||||
edits,
|
||||
edit_preview,
|
||||
display_mode,
|
||||
snapshot,
|
||||
} => {
|
||||
if self.editor.read(cx).has_active_completions_menu() {
|
||||
return None;
|
||||
@@ -3510,13 +3522,11 @@ impl EditorElement {
|
||||
EditDisplayMode::DiffPopover => {}
|
||||
}
|
||||
|
||||
let crate::InlineCompletionText::Edit { text, highlights } =
|
||||
crate::inline_completion_edit_text(editor_snapshot, edits, false, cx)
|
||||
else {
|
||||
return None;
|
||||
};
|
||||
let highlighted_edits = edit_preview.as_ref().and_then(|edit_preview| {
|
||||
crate::inline_completion_edit_text(&snapshot, edits, edit_preview, false, cx)
|
||||
})?;
|
||||
|
||||
let line_count = text.lines().count() + 1;
|
||||
let line_count = highlighted_edits.text.lines().count() + 1;
|
||||
|
||||
let longest_row =
|
||||
editor_snapshot.longest_row_in_range(edit_start.row()..edit_end.row() + 1);
|
||||
@@ -3535,15 +3545,14 @@ impl EditorElement {
|
||||
.width
|
||||
};
|
||||
|
||||
let styled_text =
|
||||
gpui::StyledText::new(text.clone()).with_highlights(&style.text, highlights);
|
||||
let styled_text = gpui::StyledText::new(highlighted_edits.text.clone())
|
||||
.with_highlights(&style.text, highlighted_edits.highlights);
|
||||
|
||||
let mut element = div()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.rounded_md()
|
||||
.px_1()
|
||||
.child(styled_text)
|
||||
.into_any();
|
||||
|
||||
@@ -3779,7 +3788,8 @@ impl EditorElement {
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Vec<AnyElement> {
|
||||
let point_for_position = position_map.point_for_position(window.mouse_position());
|
||||
let point_for_position =
|
||||
position_map.point_for_position(text_hitbox.bounds, window.mouse_position());
|
||||
|
||||
let mut controls = vec![];
|
||||
|
||||
@@ -3916,10 +3926,7 @@ 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.position_map.text_hitbox.bounds,
|
||||
self.style.background,
|
||||
));
|
||||
window.paint_quad(fill(layout.text_hitbox.bounds, self.style.background));
|
||||
|
||||
if let EditorMode::Full = layout.mode {
|
||||
let mut active_rows = layout.active_rows.iter().peekable();
|
||||
@@ -3944,8 +3951,8 @@ impl EditorElement {
|
||||
end: layout.gutter_hitbox.right(),
|
||||
}),
|
||||
CurrentLineHighlight::Line => Some(Range {
|
||||
start: layout.position_map.text_hitbox.bounds.left(),
|
||||
end: layout.position_map.text_hitbox.bounds.right(),
|
||||
start: layout.text_hitbox.bounds.left(),
|
||||
end: layout.text_hitbox.bounds.right(),
|
||||
}),
|
||||
CurrentLineHighlight::All => Some(Range {
|
||||
start: layout.hitbox.left(),
|
||||
@@ -4019,7 +4026,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.position_map.text_hitbox.origin.x
|
||||
let x = (layout.text_hitbox.origin.x
|
||||
+ *wrap_position
|
||||
+ layout.position_map.em_width / 2.)
|
||||
- scroll_left;
|
||||
@@ -4031,7 +4038,7 @@ impl EditorElement {
|
||||
|| scrollbar_y.as_ref().map_or(false, |sy| sy.visible)
|
||||
};
|
||||
|
||||
if x < layout.position_map.text_hitbox.origin.x
|
||||
if x < layout.text_hitbox.origin.x
|
||||
|| (show_scrollbars && x > self.scrollbar_left(&layout.hitbox.bounds))
|
||||
{
|
||||
continue;
|
||||
@@ -4044,8 +4051,8 @@ impl EditorElement {
|
||||
};
|
||||
window.paint_quad(fill(
|
||||
Bounds {
|
||||
origin: point(x, layout.position_map.text_hitbox.origin.y),
|
||||
size: size(px(1.), layout.position_map.text_hitbox.size.height),
|
||||
origin: point(x, layout.text_hitbox.origin.y),
|
||||
size: size(px(1.), layout.text_hitbox.size.height),
|
||||
},
|
||||
color,
|
||||
));
|
||||
@@ -4420,7 +4427,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.position_map.text_hitbox.bounds,
|
||||
bounds: layout.text_hitbox.bounds,
|
||||
}),
|
||||
|window| {
|
||||
let cursor_style = if self
|
||||
@@ -4434,7 +4441,7 @@ impl EditorElement {
|
||||
} else {
|
||||
CursorStyle::IBeam
|
||||
};
|
||||
window.set_cursor_style(cursor_style, &layout.position_map.text_hitbox);
|
||||
window.set_cursor_style(cursor_style, &layout.text_hitbox);
|
||||
|
||||
let invisible_display_ranges = self.paint_highlights(layout, window);
|
||||
self.paint_lines(&invisible_display_ranges, layout, window, cx);
|
||||
@@ -4456,7 +4463,7 @@ impl EditorElement {
|
||||
layout: &mut EditorLayout,
|
||||
window: &mut Window,
|
||||
) -> SmallVec<[Range<DisplayPoint>; 32]> {
|
||||
window.paint_layer(layout.position_map.text_hitbox.bounds, |window| {
|
||||
window.paint_layer(layout.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 {
|
||||
@@ -4535,7 +4542,7 @@ impl EditorElement {
|
||||
// A softer than perfect black
|
||||
let redaction_color = gpui::rgb(0x0e1111);
|
||||
|
||||
window.paint_layer(layout.position_map.text_hitbox.bounds, |window| {
|
||||
window.paint_layer(layout.text_hitbox.bounds, |window| {
|
||||
for range in layout.redacted_ranges.iter() {
|
||||
self.paint_highlighted_range(
|
||||
range.clone(),
|
||||
@@ -5109,13 +5116,13 @@ impl EditorElement {
|
||||
.collect(),
|
||||
};
|
||||
|
||||
highlighted_range.paint(layout.position_map.text_hitbox.bounds, window);
|
||||
highlighted_range.paint(layout.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.position_map.text_hitbox.bounds, |window| {
|
||||
window.paint_layer(layout.text_hitbox.bounds, |window| {
|
||||
inline_blame.paint(window, cx);
|
||||
})
|
||||
}
|
||||
@@ -5234,6 +5241,8 @@ 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
|
||||
@@ -5265,16 +5274,32 @@ 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, window, cx);
|
||||
Self::mouse_right_down(
|
||||
editor,
|
||||
event,
|
||||
&position_map,
|
||||
&text_hitbox,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}),
|
||||
MouseButton::Middle => editor.update(cx, |editor, cx| {
|
||||
Self::mouse_middle_down(editor, event, &position_map, window, cx);
|
||||
Self::mouse_middle_down(
|
||||
editor,
|
||||
event,
|
||||
&position_map,
|
||||
&text_hitbox,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}),
|
||||
_ => {}
|
||||
};
|
||||
@@ -5285,11 +5310,12 @@ 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, window, cx)
|
||||
Self::mouse_up(editor, event, &position_map, &text_hitbox, window, cx)
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -5297,6 +5323,8 @@ 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 {
|
||||
@@ -5307,10 +5335,25 @@ impl EditorElement {
|
||||
if event.pressed_button == Some(MouseButton::Left)
|
||||
|| event.pressed_button == Some(MouseButton::Middle)
|
||||
{
|
||||
Self::mouse_dragged(editor, event, &position_map, window, cx)
|
||||
Self::mouse_dragged(
|
||||
editor,
|
||||
event,
|
||||
&position_map,
|
||||
text_hitbox.bounds,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
|
||||
Self::mouse_moved(editor, event, &position_map, window, cx)
|
||||
Self::mouse_moved(
|
||||
editor,
|
||||
event,
|
||||
&position_map,
|
||||
&text_hitbox,
|
||||
&gutter_hitbox,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -7115,12 +7158,6 @@ 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(
|
||||
@@ -7144,6 +7181,7 @@ impl Element for EditorElement {
|
||||
wrap_guides,
|
||||
indent_guides,
|
||||
hitbox,
|
||||
text_hitbox,
|
||||
gutter_hitbox,
|
||||
display_hunks,
|
||||
content_origin,
|
||||
@@ -7315,6 +7353,7 @@ 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>>,
|
||||
@@ -7494,17 +7533,15 @@ struct CreaseTrailerLayout {
|
||||
bounds: Bounds<Pixels>,
|
||||
}
|
||||
|
||||
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,
|
||||
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,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
@@ -7526,8 +7563,11 @@ impl PointForPosition {
|
||||
}
|
||||
|
||||
impl PositionMap {
|
||||
pub(crate) fn point_for_position(&self, position: gpui::Point<Pixels>) -> PointForPosition {
|
||||
let text_bounds = self.text_hitbox.bounds;
|
||||
fn point_for_position(
|
||||
&self,
|
||||
text_bounds: Bounds<Pixels>,
|
||||
position: gpui::Point<Pixels>,
|
||||
) -> PointForPosition {
|
||||
let scroll_position = self.snapshot.scroll_position();
|
||||
let position = position - text_bounds.origin;
|
||||
let y = position.y.max(px(0.)).min(self.size.height);
|
||||
|
||||
@@ -856,8 +856,7 @@ impl ProjectDiffEditor {
|
||||
for (_, buffer, hunk_ranges) in excerpts_to_add {
|
||||
let buffer_snapshot = buffer.read(cx).snapshot();
|
||||
let max_point = buffer_snapshot.max_point();
|
||||
let new_excerpts = multi_buffer.insert_excerpts_after(
|
||||
after_excerpt_id,
|
||||
let new_excerpts = multi_buffer.push_excerpts(
|
||||
buffer,
|
||||
hunk_ranges.into_iter().map(|range| {
|
||||
let mut extended_point_range = range.to_point(&buffer_snapshot);
|
||||
|
||||
@@ -333,6 +333,7 @@ fn propose_edits<T: ToOffset>(
|
||||
provider.update(cx, |provider, _| {
|
||||
provider.set_inline_completion(Some(inline_completion::InlineCompletion {
|
||||
edits: edits.collect(),
|
||||
edit_preview: None,
|
||||
}))
|
||||
})
|
||||
});
|
||||
|
||||
@@ -255,16 +255,12 @@ impl FollowableItem for Editor {
|
||||
|
||||
match update {
|
||||
proto::update_view::Variant::Editor(update) => match event {
|
||||
EditorEvent::ExcerptsAdded {
|
||||
buffer,
|
||||
predecessor,
|
||||
excerpts,
|
||||
} => {
|
||||
EditorEvent::ExcerptsAdded { buffer, excerpts } => {
|
||||
let buffer_id = buffer.read(cx).remote_id();
|
||||
let mut excerpts = excerpts.iter();
|
||||
if let Some((id, range)) = excerpts.next() {
|
||||
update.inserted_excerpts.push(proto::ExcerptInsertion {
|
||||
previous_excerpt_id: Some(predecessor.to_proto()),
|
||||
previous_excerpt_id: None,
|
||||
excerpt: serialize_excerpt(buffer_id, id, range),
|
||||
});
|
||||
update.inserted_excerpts.extend(excerpts.map(|(id, range)| {
|
||||
@@ -394,8 +390,7 @@ async fn update_editor_from_message(
|
||||
}
|
||||
});
|
||||
|
||||
multibuffer.insert_excerpts_with_ids_after(
|
||||
ExcerptId::from_proto(previous_excerpt_id),
|
||||
multibuffer.insert_excerpts_with_ids(
|
||||
buffer,
|
||||
[excerpt]
|
||||
.into_iter()
|
||||
@@ -1040,10 +1035,10 @@ impl SerializableItem for Editor {
|
||||
} => window.spawn(cx, |mut cx| {
|
||||
let project = project.clone();
|
||||
async move {
|
||||
let language_registry =
|
||||
project.update(&mut cx, |project, _| project.languages().clone())?;
|
||||
|
||||
let language = if let Some(language_name) = language {
|
||||
let language_registry =
|
||||
project.update(&mut cx, |project, _| project.languages().clone())?;
|
||||
|
||||
// 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,7 +1056,6 @@ 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,10 +229,9 @@ pub fn deploy_context_menu(
|
||||
cx,
|
||||
),
|
||||
None => {
|
||||
let character_size = editor.character_size(window);
|
||||
let menu_position = MenuPosition::PinnedToEditor {
|
||||
source: source_anchor,
|
||||
offset: gpui::point(character_size.width, character_size.height),
|
||||
offset: editor.character_size(window),
|
||||
};
|
||||
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,23 +444,6 @@ 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,25 +295,6 @@ 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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ const fn zed_repo_url() -> &'static str {
|
||||
}
|
||||
|
||||
fn request_feature_url() -> String {
|
||||
"https://github.com/zed-industries/zed/discussions/new/choose".to_string()
|
||||
"https://github.com/zed-industries/zed/issues/new?template=0_feature_request.yml".to_string()
|
||||
}
|
||||
|
||||
fn file_bug_report_url(specs: &SystemSpecs) -> String {
|
||||
|
||||
@@ -364,20 +364,6 @@ 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 {
|
||||
|
||||
@@ -217,19 +217,6 @@ 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,
|
||||
@@ -389,13 +376,6 @@ 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
|
||||
@@ -1476,17 +1456,6 @@ 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,14 +62,6 @@ 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`]
|
||||
@@ -167,15 +159,4 @@ 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,14 +789,6 @@ 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.
|
||||
@@ -887,16 +879,6 @@ 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,8 +186,6 @@ 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>,
|
||||
@@ -436,7 +434,6 @@ 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,
|
||||
@@ -870,12 +867,11 @@ impl X11Client {
|
||||
}
|
||||
|
||||
let modifiers = Modifiers::from_xkb(&state.xkb);
|
||||
if state.last_modifiers_changed_event == modifiers {
|
||||
if state.modifiers == 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, NSNotFound,
|
||||
NSPoint, NSRect, NSSize, NSString, NSUInteger,
|
||||
NSArray, NSAutoreleasePool, NSDictionary, NSFastEnumeration, NSInteger, NSPoint, NSRect,
|
||||
NSSize, NSString, NSUInteger,
|
||||
},
|
||||
};
|
||||
use core_graphics::display::{CGDirectDisplayID, CGPoint, CGRect};
|
||||
@@ -223,11 +223,6 @@ 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()
|
||||
};
|
||||
}
|
||||
@@ -1688,7 +1683,17 @@ extern "C" fn first_rect_for_character_range(
|
||||
range: NSRange,
|
||||
_: id,
|
||||
) -> NSRect {
|
||||
let frame = get_frame(this);
|
||||
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
|
||||
};
|
||||
with_input_handler(this, |input_handler| {
|
||||
input_handler.bounds_for_range(range.to_range()?)
|
||||
})
|
||||
@@ -1709,20 +1714,6 @@ 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 =
|
||||
@@ -1836,24 +1827,6 @@ 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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -15,31 +15,7 @@ pub enum Direction {
|
||||
#[derive(Clone)]
|
||||
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 edit_preview: Option<language::EditPreview>,
|
||||
}
|
||||
|
||||
pub trait InlineCompletionProvider: 'static + Sized {
|
||||
@@ -50,10 +26,6 @@ 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>,
|
||||
@@ -100,8 +72,6 @@ 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(
|
||||
@@ -152,14 +122,6 @@ 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_onboarding.workspace = true
|
||||
zed_predict_tos.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
copilot = { workspace = true, features = ["test-support"] }
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
use anyhow::Result;
|
||||
use client::{Client, UserStore};
|
||||
use client::UserStore;
|
||||
use copilot::{Copilot, Status};
|
||||
use editor::{actions::ShowInlineCompletion, scroll::Autoscroll, Editor};
|
||||
use editor::{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, FocusHandle, Focusable, IntoElement, ParentElement, Render, Subscription,
|
||||
WeakEntity,
|
||||
Corner, Entity, IntoElement, ParentElement, Render, Subscription, WeakEntity,
|
||||
};
|
||||
use language::{
|
||||
language_settings::{
|
||||
@@ -20,16 +19,18 @@ 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, Clickable, ContextMenu, ContextMenuEntry, IconButton,
|
||||
IconWithIndicator, Indicator, PopoverMenu, PopoverMenuHandle, Tooltip,
|
||||
};
|
||||
use ui::{prelude::*, ButtonLike, Color, Icon, IconWithIndicator, Indicator, PopoverMenuHandle};
|
||||
use workspace::{
|
||||
create_and_open_local_file, item::ItemHandle, notifications::NotificationId, StatusItemView,
|
||||
Toast, Workspace,
|
||||
create_and_open_local_file,
|
||||
item::ItemHandle,
|
||||
notifications::NotificationId,
|
||||
ui::{
|
||||
ButtonCommon, Clickable, ContextMenu, IconButton, IconName, IconSize, PopoverMenu, Tooltip,
|
||||
},
|
||||
StatusItemView, Toast, Workspace,
|
||||
};
|
||||
use zed_actions::OpenBrowser;
|
||||
use zed_predict_onboarding::ZedPredictModal;
|
||||
use zed_predict_tos::ZedPredictTos;
|
||||
use zeta::RateCompletionModal;
|
||||
|
||||
actions!(zeta, [RateCompletions]);
|
||||
@@ -42,11 +43,9 @@ 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>,
|
||||
@@ -230,16 +229,14 @@ impl Render for InlineCompletionButton {
|
||||
return div();
|
||||
}
|
||||
|
||||
let current_user_terms_accepted =
|
||||
self.user_store.read(cx).current_user_has_accepted_terms();
|
||||
|
||||
if !current_user_terms_accepted.unwrap_or(false) {
|
||||
if !self
|
||||
.user_store
|
||||
.read(cx)
|
||||
.current_user_has_accepted_terms()
|
||||
.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")
|
||||
@@ -253,29 +250,20 @@ impl Render for InlineCompletionButton {
|
||||
))
|
||||
.into_any_element(),
|
||||
)
|
||||
.tooltip(move |window, cx| {
|
||||
.tooltip(|window, cx| {
|
||||
Tooltip::with_meta(
|
||||
"Edit Predictions",
|
||||
None,
|
||||
if signed_in {
|
||||
"Read Terms of Service"
|
||||
} else {
|
||||
"Sign in to use"
|
||||
},
|
||||
"Read Terms of Service",
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.on_click(cx.listener(move |_, _, window, cx| {
|
||||
let user_store = user_store.clone();
|
||||
|
||||
if let Some(workspace) = workspace.upgrade() {
|
||||
ZedPredictModal::toggle(
|
||||
workspace,
|
||||
user_store.clone(),
|
||||
client.clone(),
|
||||
fs.clone(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
ZedPredictTos::toggle(workspace, user_store, window, cx);
|
||||
}
|
||||
})),
|
||||
);
|
||||
@@ -328,7 +316,6 @@ 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 {
|
||||
@@ -342,13 +329,11 @@ 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,
|
||||
}
|
||||
@@ -379,26 +364,21 @@ 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.toggleable_entry(
|
||||
language.name(),
|
||||
language_enabled,
|
||||
IconPosition::Start,
|
||||
menu = menu.entry(
|
||||
format!(
|
||||
"{} Inline Completions for {}",
|
||||
if language_enabled { "Hide" } else { "Show" },
|
||||
language.name()
|
||||
),
|
||||
None,
|
||||
move |_, cx| {
|
||||
toggle_inline_completions_for_language(language.clone(), fs.clone(), cx)
|
||||
@@ -407,14 +387,16 @@ 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.toggleable_entry(
|
||||
"This File",
|
||||
path_enabled,
|
||||
IconPosition::Start,
|
||||
menu = menu.entry(
|
||||
format!(
|
||||
"{} Inline Completions for This Path",
|
||||
if path_enabled { "Hide" } else { "Show" }
|
||||
),
|
||||
None,
|
||||
move |window, cx| {
|
||||
if let Some(workspace) = window.root().flatten() {
|
||||
@@ -434,48 +416,15 @@ impl InlineCompletionButton {
|
||||
}
|
||||
|
||||
let globally_enabled = settings.inline_completions_enabled(None, None, cx);
|
||||
menu = menu.toggleable_entry(
|
||||
"All Files",
|
||||
globally_enabled,
|
||||
IconPosition::Start,
|
||||
menu.entry(
|
||||
if globally_enabled {
|
||||
"Hide Inline Completions for All Files"
|
||||
} else {
|
||||
"Show Inline Completions for All Files"
|
||||
},
|
||||
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(
|
||||
@@ -519,7 +468,7 @@ impl InlineCompletionButton {
|
||||
self.build_language_settings_menu(menu, cx).when(
|
||||
cx.has_flag::<PredictEditsRateCompletionsFeatureFlag>(),
|
||||
|this| {
|
||||
this.entry(
|
||||
this.separator().entry(
|
||||
"Rate Completions",
|
||||
Some(RateCompletions.boxed_clone()),
|
||||
move |window, cx| {
|
||||
@@ -555,7 +504,6 @@ 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();
|
||||
}
|
||||
|
||||
@@ -25,8 +25,8 @@ use collections::HashMap;
|
||||
use fs::MTime;
|
||||
use futures::channel::oneshot;
|
||||
use gpui::{
|
||||
AnyElement, App, AppContext as _, Context, Entity, EventEmitter, HighlightStyle, Pixels, Task,
|
||||
TaskLabel, Window,
|
||||
AnyElement, App, AppContext as _, Context, Entity, EventEmitter, HighlightStyle, Pixels,
|
||||
SharedString, Task, TaskLabel, Window,
|
||||
};
|
||||
use lsp::LanguageServerId;
|
||||
use parking_lot::Mutex;
|
||||
@@ -65,7 +65,7 @@ pub use text::{
|
||||
Subscription, TextDimension, TextSummary, ToOffset, ToOffsetUtf16, ToPoint, ToPointUtf16,
|
||||
Transaction, TransactionId, Unclipped,
|
||||
};
|
||||
use theme::SyntaxTheme;
|
||||
use theme::{ActiveTheme as _, SyntaxTheme};
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
use util::RandomCharIter;
|
||||
use util::{debug_panic, maybe, RangeExt};
|
||||
@@ -588,6 +588,162 @@ pub struct Runnable {
|
||||
pub buffer: BufferId,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct EditPreview {
|
||||
applied_edits_snapshot: text::BufferSnapshot,
|
||||
syntax_snapshot: SyntaxSnapshot,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Debug)]
|
||||
pub struct HighlightedEdits {
|
||||
pub text: SharedString,
|
||||
pub highlights: Vec<(Range<usize>, HighlightStyle)>,
|
||||
}
|
||||
|
||||
impl EditPreview {
|
||||
pub fn highlight_edits(
|
||||
&self,
|
||||
current_snapshot: &BufferSnapshot,
|
||||
edits: &[(Range<Anchor>, String)],
|
||||
include_deletions: bool,
|
||||
cx: &App,
|
||||
) -> HighlightedEdits {
|
||||
let mut text = String::new();
|
||||
let mut highlights = Vec::new();
|
||||
let Some(range) = self.compute_visible_range(edits, current_snapshot) else {
|
||||
return HighlightedEdits::default();
|
||||
};
|
||||
let mut offset = range.start;
|
||||
let mut delta = 0isize;
|
||||
|
||||
let status_colors = cx.theme().status();
|
||||
|
||||
for (range, edit_text) in edits {
|
||||
let edit_range = range.to_offset(current_snapshot);
|
||||
let new_edit_start = (edit_range.start as isize + delta) as usize;
|
||||
let new_edit_range = new_edit_start..new_edit_start + edit_text.len();
|
||||
|
||||
let prev_range = offset..new_edit_start;
|
||||
|
||||
if !prev_range.is_empty() {
|
||||
let start = text.len();
|
||||
self.highlight_text(prev_range, &mut text, &mut highlights, None, cx);
|
||||
offset += text.len() - start;
|
||||
}
|
||||
|
||||
if include_deletions && !edit_range.is_empty() {
|
||||
let start = text.len();
|
||||
text.extend(current_snapshot.text_for_range(edit_range.clone()));
|
||||
let end = text.len();
|
||||
|
||||
highlights.push((
|
||||
start..end,
|
||||
HighlightStyle {
|
||||
background_color: Some(status_colors.deleted_background),
|
||||
..Default::default()
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
if !edit_text.is_empty() {
|
||||
self.highlight_text(
|
||||
new_edit_range,
|
||||
&mut text,
|
||||
&mut highlights,
|
||||
Some(HighlightStyle {
|
||||
background_color: Some(status_colors.created_background),
|
||||
..Default::default()
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
|
||||
offset += edit_text.len();
|
||||
}
|
||||
|
||||
delta += edit_text.len() as isize - edit_range.len() as isize;
|
||||
}
|
||||
|
||||
self.highlight_text(
|
||||
offset..(range.end as isize + delta) as usize,
|
||||
&mut text,
|
||||
&mut highlights,
|
||||
None,
|
||||
cx,
|
||||
);
|
||||
|
||||
HighlightedEdits {
|
||||
text: text.into(),
|
||||
highlights,
|
||||
}
|
||||
}
|
||||
|
||||
fn highlight_text(
|
||||
&self,
|
||||
range: Range<usize>,
|
||||
text: &mut String,
|
||||
highlights: &mut Vec<(Range<usize>, HighlightStyle)>,
|
||||
override_style: Option<HighlightStyle>,
|
||||
cx: &App,
|
||||
) {
|
||||
for chunk in self.highlighted_chunks(range) {
|
||||
let start = text.len();
|
||||
text.push_str(chunk.text);
|
||||
let end = text.len();
|
||||
|
||||
if let Some(mut highlight_style) = chunk
|
||||
.syntax_highlight_id
|
||||
.and_then(|id| id.style(cx.theme().syntax()))
|
||||
{
|
||||
if let Some(override_style) = override_style {
|
||||
highlight_style.highlight(override_style);
|
||||
}
|
||||
highlights.push((start..end, highlight_style));
|
||||
} else if let Some(override_style) = override_style {
|
||||
highlights.push((start..end, override_style));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn highlighted_chunks(&self, range: Range<usize>) -> BufferChunks {
|
||||
let captures =
|
||||
self.syntax_snapshot
|
||||
.captures(range.clone(), &self.applied_edits_snapshot, |grammar| {
|
||||
grammar.highlights_query.as_ref()
|
||||
});
|
||||
|
||||
let highlight_maps = captures
|
||||
.grammars()
|
||||
.iter()
|
||||
.map(|grammar| grammar.highlight_map())
|
||||
.collect();
|
||||
|
||||
BufferChunks::new(
|
||||
self.applied_edits_snapshot.as_rope(),
|
||||
range,
|
||||
Some((captures, highlight_maps)),
|
||||
false,
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
fn compute_visible_range(
|
||||
&self,
|
||||
edits: &[(Range<Anchor>, String)],
|
||||
snapshot: &BufferSnapshot,
|
||||
) -> Option<Range<usize>> {
|
||||
let (first, _) = edits.first()?;
|
||||
let (last, _) = edits.last()?;
|
||||
|
||||
let start = first.start.to_point(snapshot);
|
||||
let end = last.end.to_point(snapshot);
|
||||
|
||||
// Ensure that the first line of the first edit and the last line of the last edit are always fully visible
|
||||
let range = Point::new(start.row, 0)..Point::new(end.row, snapshot.line_len(end.row));
|
||||
|
||||
Some(range.to_offset(&snapshot))
|
||||
}
|
||||
}
|
||||
|
||||
impl Buffer {
|
||||
/// Create a new buffer with the given base text.
|
||||
pub fn local<T: Into<String>>(base_text: T, cx: &Context<Self>) -> Self {
|
||||
@@ -840,6 +996,33 @@ impl Buffer {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn preview_edits(
|
||||
&self,
|
||||
edits: Arc<[(Range<Anchor>, String)]>,
|
||||
cx: &App,
|
||||
) -> Task<EditPreview> {
|
||||
let registry = self.language_registry();
|
||||
let language = self.language().cloned();
|
||||
|
||||
let mut branch_buffer = self.text.branch();
|
||||
let mut syntax_snapshot = self.syntax_map.lock().snapshot();
|
||||
cx.background_executor().spawn(async move {
|
||||
if !edits.is_empty() {
|
||||
branch_buffer.edit(edits.iter().cloned());
|
||||
let snapshot = branch_buffer.snapshot();
|
||||
syntax_snapshot.interpolate(&snapshot);
|
||||
|
||||
if let Some(language) = language {
|
||||
syntax_snapshot.reparse(&snapshot, registry, language);
|
||||
}
|
||||
}
|
||||
EditPreview {
|
||||
applied_edits_snapshot: branch_buffer.snapshot(),
|
||||
syntax_snapshot,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Applies all of the changes in this buffer that intersect any of the
|
||||
/// given `ranges` to its base buffer.
|
||||
///
|
||||
|
||||
@@ -2627,6 +2627,148 @@ fn test_undo_after_merge_into_base(cx: &mut TestAppContext) {
|
||||
branch.read_with(cx, |branch, _| assert_eq!(branch.text(), "ABCdefgHIjk"));
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_preview_edits(cx: &mut TestAppContext) {
|
||||
cx.update(|cx| {
|
||||
init_settings(cx, |_| {});
|
||||
theme::init(theme::LoadThemes::JustBase, cx);
|
||||
});
|
||||
|
||||
let text = indoc! {r#"
|
||||
struct Person {
|
||||
first_name: String,
|
||||
}
|
||||
|
||||
impl Person {
|
||||
fn last_name(&self) -> &String {
|
||||
&self.last_name
|
||||
}
|
||||
}"#
|
||||
};
|
||||
|
||||
let language = Arc::new(Language::new(
|
||||
LanguageConfig::default(),
|
||||
Some(tree_sitter_rust::LANGUAGE.into()),
|
||||
));
|
||||
let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
|
||||
let highlighted_edits = preview_edits(
|
||||
&buffer,
|
||||
cx,
|
||||
[
|
||||
(Point::new(5, 7)..Point::new(5, 11), "first"),
|
||||
(Point::new(6, 14)..Point::new(6, 18), "first"),
|
||||
],
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
highlighted_edits.text,
|
||||
" fn lastfirst_name(&self) -> &String {\n &self.lastfirst_name"
|
||||
);
|
||||
|
||||
async fn preview_edits(
|
||||
buffer: &Entity<Buffer>,
|
||||
cx: &mut TestAppContext,
|
||||
edits: impl IntoIterator<Item = (Range<Point>, &'static str)>,
|
||||
) -> HighlightedEdits {
|
||||
let edits = buffer.read_with(cx, |buffer, _| {
|
||||
edits
|
||||
.into_iter()
|
||||
.map(|(range, text)| {
|
||||
(
|
||||
buffer.anchor_before(range.start)..buffer.anchor_after(range.end),
|
||||
text.to_string(),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
let edit_preview = buffer
|
||||
.read_with(cx, |buffer, cx| {
|
||||
buffer.preview_edits(edits.clone().into(), cx)
|
||||
})
|
||||
.await;
|
||||
cx.read(|cx| edit_preview.highlight_edits(&buffer.read(cx).snapshot(), &edits, true, cx))
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_preview_edits_interpolate(cx: &mut TestAppContext) {
|
||||
use theme::ActiveTheme;
|
||||
cx.update(|cx| {
|
||||
init_settings(cx, |_| {});
|
||||
theme::init(theme::LoadThemes::JustBase, cx);
|
||||
});
|
||||
|
||||
let text = indoc! {r#"
|
||||
struct Person {
|
||||
_name: String
|
||||
}"#
|
||||
};
|
||||
|
||||
let language = Arc::new(Language::new(
|
||||
LanguageConfig::default(),
|
||||
Some(tree_sitter_rust::LANGUAGE.into()),
|
||||
));
|
||||
let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
|
||||
|
||||
let edits = construct_edits(&buffer, [(Point::new(1, 4)..Point::new(1, 4), "first")], cx);
|
||||
let edit_preview = buffer
|
||||
.read_with(cx, |buffer, cx| buffer.preview_edits(edits.clone(), cx))
|
||||
.await;
|
||||
|
||||
let highlighted_edits =
|
||||
cx.read(|cx| edit_preview.highlight_edits(&buffer.read(cx).snapshot(), &edits, false, cx));
|
||||
|
||||
let created_background = cx.read(|cx| cx.theme().status().created_background);
|
||||
|
||||
assert_eq!(highlighted_edits.text, " first_name: String");
|
||||
assert_eq!(highlighted_edits.highlights.len(), 1);
|
||||
assert_eq!(highlighted_edits.highlights[0].0, 4..9);
|
||||
assert_eq!(
|
||||
highlighted_edits.highlights[0].1.background_color,
|
||||
Some(created_background)
|
||||
);
|
||||
|
||||
let edits = construct_edits(&buffer, [(Point::new(1, 4)..Point::new(1, 4), "f")], cx);
|
||||
cx.update(|cx| {
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.edit(edits.iter().cloned(), None, cx);
|
||||
})
|
||||
});
|
||||
|
||||
let edits = construct_edits(&buffer, [(Point::new(1, 5)..Point::new(1, 5), "irst")], cx);
|
||||
let highlighted_edits =
|
||||
cx.read(|cx| edit_preview.highlight_edits(&buffer.read(cx).snapshot(), &edits, false, cx));
|
||||
|
||||
assert_eq!(highlighted_edits.text, " first_name: String");
|
||||
assert_eq!(highlighted_edits.highlights.len(), 1);
|
||||
assert_eq!(highlighted_edits.highlights[0].0, (5..9));
|
||||
assert_eq!(
|
||||
highlighted_edits.highlights[0].1.background_color,
|
||||
Some(created_background)
|
||||
);
|
||||
|
||||
fn construct_edits(
|
||||
buffer: &Entity<Buffer>,
|
||||
edits: impl IntoIterator<Item = (Range<Point>, &'static str)>,
|
||||
cx: &mut TestAppContext,
|
||||
) -> Arc<[(Range<Anchor>, String)]> {
|
||||
buffer
|
||||
.read_with(cx, |buffer, _| {
|
||||
edits
|
||||
.into_iter()
|
||||
.map(|(range, text)| {
|
||||
(
|
||||
buffer.anchor_after(range.start)..buffer.anchor_before(range.end),
|
||||
text.to_string(),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 100)]
|
||||
fn test_random_collaboration(cx: &mut App, mut rng: StdRng) {
|
||||
let min_peers = env::var("MIN_PEERS")
|
||||
|
||||
@@ -263,7 +263,7 @@ impl SyntaxSnapshot {
|
||||
self.layers.is_empty()
|
||||
}
|
||||
|
||||
fn interpolate(&mut self, text: &BufferSnapshot) {
|
||||
pub fn interpolate(&mut self, text: &BufferSnapshot) {
|
||||
let edits = text
|
||||
.anchored_edits_since::<(usize, Point)>(&self.interpolated_version)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
@@ -82,7 +82,6 @@ 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)
|
||||
}
|
||||
|
||||
@@ -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::O3Mini => open_ai::Model::Four,
|
||||
CopilotChatModel::O1 | CopilotChatModel::O1Mini => open_ai::Model::Four,
|
||||
CopilotChatModel::Claude3_5Sonnet => unreachable!(),
|
||||
};
|
||||
count_open_ai_tokens(request, model, cx)
|
||||
|
||||
@@ -361,10 +361,7 @@ 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::O3Mini => {
|
||||
open_ai::Model::Custom { .. } | open_ai::Model::O1Mini | open_ai::Model::O1 => {
|
||||
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: Option<String>,
|
||||
pub quantization: String,
|
||||
pub state: ModelState,
|
||||
pub max_context_length: Option<u32>,
|
||||
pub loaded_context_length: Option<u32>,
|
||||
|
||||
@@ -5,6 +5,7 @@ mod position;
|
||||
|
||||
pub use anchor::{Anchor, AnchorRangeExt, Offset};
|
||||
pub use position::{TypedOffset, TypedPoint, TypedRow};
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use clock::ReplicaId;
|
||||
@@ -79,7 +80,6 @@ pub struct MultiBuffer {
|
||||
pub enum Event {
|
||||
ExcerptsAdded {
|
||||
buffer: Entity<Buffer>,
|
||||
predecessor: ExcerptId,
|
||||
excerpts: Vec<(ExcerptId, ExcerptRange<language::Anchor>)>,
|
||||
},
|
||||
ExcerptsRemoved {
|
||||
@@ -195,11 +195,39 @@ pub trait ToPointUtf16: 'static + fmt::Debug {
|
||||
fn to_point_utf16(&self, snapshot: &MultiBufferSnapshot) -> PointUtf16;
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
|
||||
enum BufferLocation {
|
||||
#[default]
|
||||
Min,
|
||||
Unsaved(BufferId),
|
||||
Path(Arc<Path>),
|
||||
Max,
|
||||
}
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
|
||||
struct ExcerptLocator {
|
||||
buffer_location: BufferLocation,
|
||||
locator: Option<Locator>,
|
||||
}
|
||||
|
||||
impl ExcerptLocator {
|
||||
const MIN: ExcerptLocator = ExcerptLocator {
|
||||
buffer_location: BufferLocation::Min,
|
||||
locator: None,
|
||||
};
|
||||
|
||||
const MAX: ExcerptLocator = ExcerptLocator {
|
||||
buffer_location: BufferLocation::Max,
|
||||
locator: None,
|
||||
};
|
||||
}
|
||||
|
||||
struct BufferState {
|
||||
buffer: Entity<Buffer>,
|
||||
last_version: clock::Global,
|
||||
last_non_text_state_update_count: usize,
|
||||
excerpts: Vec<Locator>,
|
||||
buffer_location: BufferLocation,
|
||||
excerpts: Vec<ExcerptLocator>,
|
||||
start_anchors: Vec<text::Anchor>,
|
||||
_subscriptions: [gpui::Subscription; 2],
|
||||
}
|
||||
|
||||
@@ -298,7 +326,7 @@ struct Excerpt {
|
||||
/// The unique identifier for this excerpt
|
||||
id: ExcerptId,
|
||||
/// The location of the excerpt in the [`MultiBuffer`]
|
||||
locator: Locator,
|
||||
locator: ExcerptLocator,
|
||||
/// The buffer being excerpted
|
||||
buffer_id: BufferId,
|
||||
/// A snapshot of the buffer being excerpted
|
||||
@@ -329,7 +357,7 @@ pub struct MultiBufferExcerpt<'a> {
|
||||
#[derive(Clone, Debug)]
|
||||
struct ExcerptIdMapping {
|
||||
id: ExcerptId,
|
||||
locator: Locator,
|
||||
locator: ExcerptLocator,
|
||||
}
|
||||
|
||||
/// A range of text from a single [`Buffer`], to be shown as an [`Excerpt`].
|
||||
@@ -347,7 +375,7 @@ pub struct ExcerptRange<T> {
|
||||
pub struct ExcerptSummary {
|
||||
excerpt_id: ExcerptId,
|
||||
/// The location of the last [`Excerpt`] being summarized
|
||||
excerpt_locator: Locator,
|
||||
excerpt_locator: ExcerptLocator,
|
||||
widest_line_number: u32,
|
||||
text: TextSummary,
|
||||
}
|
||||
@@ -530,10 +558,12 @@ impl MultiBuffer {
|
||||
buffers.insert(
|
||||
*buffer_id,
|
||||
BufferState {
|
||||
buffer_location: buffer_state.buffer_location.clone(),
|
||||
buffer: buffer_state.buffer.clone(),
|
||||
last_version: buffer_state.last_version.clone(),
|
||||
last_non_text_state_update_count: buffer_state.last_non_text_state_update_count,
|
||||
excerpts: buffer_state.excerpts.clone(),
|
||||
start_anchors: buffer_state.start_anchors.clone(),
|
||||
_subscriptions: [
|
||||
new_cx.observe(&buffer_state.buffer, |_, _, cx| cx.notify()),
|
||||
new_cx.subscribe(&buffer_state.buffer, Self::on_buffer_event),
|
||||
@@ -1172,7 +1202,7 @@ impl MultiBuffer {
|
||||
let mut selections_by_buffer: HashMap<BufferId, Vec<Selection<text::Anchor>>> =
|
||||
Default::default();
|
||||
let snapshot = self.read(cx);
|
||||
let mut cursor = snapshot.excerpts.cursor::<Option<&Locator>>(&());
|
||||
let mut cursor = snapshot.excerpts.cursor::<Option<&ExcerptLocator>>(&());
|
||||
for selection in selections {
|
||||
let start_locator = snapshot.excerpt_locator_for_id(selection.start.excerpt_id);
|
||||
let end_locator = snapshot.excerpt_locator_for_id(selection.end.excerpt_id);
|
||||
@@ -1337,18 +1367,6 @@ impl MultiBuffer {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn push_excerpts<O>(
|
||||
&mut self,
|
||||
buffer: Entity<Buffer>,
|
||||
ranges: impl IntoIterator<Item = ExcerptRange<O>>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Vec<ExcerptId>
|
||||
where
|
||||
O: text::ToOffset,
|
||||
{
|
||||
self.insert_excerpts_after(ExcerptId::max(), buffer, ranges, cx)
|
||||
}
|
||||
|
||||
pub fn push_excerpts_with_context_lines<O>(
|
||||
&mut self,
|
||||
buffer: Entity<Buffer>,
|
||||
@@ -1475,9 +1493,8 @@ impl MultiBuffer {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn insert_excerpts_after<O>(
|
||||
pub fn push_excerpts<O>(
|
||||
&mut self,
|
||||
prev_excerpt_id: ExcerptId,
|
||||
buffer: Entity<Buffer>,
|
||||
ranges: impl IntoIterator<Item = ExcerptRange<O>>,
|
||||
cx: &mut Context<Self>,
|
||||
@@ -1492,8 +1509,7 @@ impl MultiBuffer {
|
||||
} else {
|
||||
1
|
||||
};
|
||||
self.insert_excerpts_with_ids_after(
|
||||
prev_excerpt_id,
|
||||
self.insert_excerpts_with_ids(
|
||||
buffer,
|
||||
ranges.into_iter().map(|range| {
|
||||
let id = ExcerptId(post_inc(&mut next_excerpt_id));
|
||||
@@ -1505,9 +1521,8 @@ impl MultiBuffer {
|
||||
ids
|
||||
}
|
||||
|
||||
pub fn insert_excerpts_with_ids_after<O>(
|
||||
pub fn insert_excerpts_with_ids<O>(
|
||||
&mut self,
|
||||
prev_excerpt_id: ExcerptId,
|
||||
buffer: Entity<Buffer>,
|
||||
ranges: impl IntoIterator<Item = (ExcerptId, ExcerptRange<O>)>,
|
||||
cx: &mut Context<Self>,
|
||||
@@ -1529,7 +1544,12 @@ impl MultiBuffer {
|
||||
let buffer_state = buffers.entry(buffer_id).or_insert_with(|| BufferState {
|
||||
last_version: buffer_snapshot.version().clone(),
|
||||
last_non_text_state_update_count: buffer_snapshot.non_text_state_update_count(),
|
||||
buffer_location: buffer_snapshot
|
||||
.file()
|
||||
.map(|f| BufferLocation::Path(Arc::from(f.full_path(cx))))
|
||||
.unwrap_or(BufferLocation::Unsaved(buffer_id)),
|
||||
excerpts: Default::default(),
|
||||
start_anchors: Default::default(),
|
||||
_subscriptions: [
|
||||
cx.observe(&buffer, |_, _, cx| cx.notify()),
|
||||
cx.subscribe(&buffer, Self::on_buffer_event),
|
||||
@@ -1539,35 +1559,46 @@ impl MultiBuffer {
|
||||
|
||||
let mut snapshot = self.snapshot.borrow_mut();
|
||||
|
||||
let mut prev_locator = snapshot.excerpt_locator_for_id(prev_excerpt_id).clone();
|
||||
let mut new_excerpt_ids = mem::take(&mut snapshot.excerpt_ids);
|
||||
let mut cursor = snapshot.excerpts.cursor::<Option<&Locator>>(&());
|
||||
let mut new_excerpts = cursor.slice(&prev_locator, Bias::Right, &());
|
||||
prev_locator = cursor.start().unwrap_or(Locator::min_ref()).clone();
|
||||
let mut cursor = snapshot.excerpts.cursor::<Option<&ExcerptLocator>>(&());
|
||||
|
||||
let edit_start = ExcerptOffset::new(new_excerpts.summary().text.len);
|
||||
new_excerpts.update_last(
|
||||
|excerpt| {
|
||||
excerpt.has_trailing_newline = true;
|
||||
},
|
||||
&(),
|
||||
);
|
||||
let mut edit_start = None;
|
||||
|
||||
let next_locator = if let Some(excerpt) = cursor.item() {
|
||||
excerpt.locator.clone()
|
||||
} else {
|
||||
Locator::max()
|
||||
};
|
||||
let mut new_excerpts = SumTree::default();
|
||||
|
||||
let mut excerpts = Vec::new();
|
||||
while let Some((id, range)) = ranges.next() {
|
||||
let locator = Locator::between(&prev_locator, &next_locator);
|
||||
if let Err(ix) = buffer_state.excerpts.binary_search(&locator) {
|
||||
buffer_state.excerpts.insert(ix, locator.clone());
|
||||
}
|
||||
let context_start = buffer_snapshot.anchor_before(&range.context.start);
|
||||
let locator = if let Err(ix) = buffer_state
|
||||
.start_anchors
|
||||
.binary_search_by(|anchor| anchor.cmp(&context_start, &buffer_snapshot))
|
||||
{
|
||||
let prev_locator = if ix > 0 {
|
||||
buffer_state.excerpts[ix - 1].locator.as_ref().unwrap()
|
||||
} else {
|
||||
Locator::min_ref()
|
||||
};
|
||||
let next_locator = if ix < buffer_state.excerpts.len() {
|
||||
buffer_state.excerpts[ix].locator.as_ref().unwrap()
|
||||
} else {
|
||||
Locator::max_ref()
|
||||
};
|
||||
let locator = Locator::between(prev_locator, next_locator);
|
||||
// todo: we shouldn't need to create these, but the dimension is defined by reference...
|
||||
let excerpt_locator = ExcerptLocator {
|
||||
buffer_location: buffer_state.buffer_location.clone(),
|
||||
locator: Some(locator),
|
||||
};
|
||||
|
||||
buffer_state.excerpts.insert(ix, excerpt_locator.clone());
|
||||
buffer_state.start_anchors.insert(ix, context_start);
|
||||
excerpt_locator
|
||||
} else {
|
||||
panic!("excerpt already exists")
|
||||
};
|
||||
new_excerpts.append(cursor.slice(&locator, Bias::Right, &()), &());
|
||||
let range = ExcerptRange {
|
||||
context: buffer_snapshot.anchor_before(&range.context.start)
|
||||
..buffer_snapshot.anchor_after(&range.context.end),
|
||||
context: context_start..buffer_snapshot.anchor_after(&range.context.end),
|
||||
primary: range.primary.map(|primary| {
|
||||
buffer_snapshot.anchor_before(&primary.start)
|
||||
..buffer_snapshot.anchor_after(&primary.end)
|
||||
@@ -1582,8 +1613,16 @@ impl MultiBuffer {
|
||||
range,
|
||||
ranges.peek().is_some() || cursor.item().is_some(),
|
||||
);
|
||||
if edit_start == None {
|
||||
edit_start = Some(ExcerptOffset::new(new_excerpts.summary().text.len));
|
||||
}
|
||||
new_excerpts.update_last(
|
||||
|excerpt| {
|
||||
excerpt.has_trailing_newline = true;
|
||||
},
|
||||
&(),
|
||||
);
|
||||
new_excerpts.push(excerpt, &());
|
||||
prev_locator = locator.clone();
|
||||
|
||||
if let Some(last_mapping_entry) = new_excerpt_ids.last() {
|
||||
assert!(id > last_mapping_entry.id, "excerpt ids must be increasing");
|
||||
@@ -1606,8 +1645,8 @@ impl MultiBuffer {
|
||||
self.sync_diff_transforms(
|
||||
snapshot,
|
||||
vec![Edit {
|
||||
old: edit_start..edit_start,
|
||||
new: edit_start..edit_end,
|
||||
old: edit_start.unwrap()..edit_start.unwrap(),
|
||||
new: edit_start.unwrap()..edit_end,
|
||||
}],
|
||||
DiffChangeKind::BufferEdited,
|
||||
);
|
||||
@@ -1615,11 +1654,7 @@ impl MultiBuffer {
|
||||
singleton_buffer_edited: false,
|
||||
edited_buffer: None,
|
||||
});
|
||||
cx.emit(Event::ExcerptsAdded {
|
||||
buffer,
|
||||
predecessor: prev_excerpt_id,
|
||||
excerpts,
|
||||
});
|
||||
cx.emit(Event::ExcerptsAdded { buffer, excerpts });
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
@@ -1660,7 +1695,7 @@ impl MultiBuffer {
|
||||
let mut excerpts = Vec::new();
|
||||
let snapshot = self.read(cx);
|
||||
let buffers = self.buffers.borrow();
|
||||
let mut cursor = snapshot.excerpts.cursor::<Option<&Locator>>(&());
|
||||
let mut cursor = snapshot.excerpts.cursor::<Option<&ExcerptLocator>>(&());
|
||||
for locator in buffers
|
||||
.get(&buffer.read(cx).remote_id())
|
||||
.map(|state| &state.excerpts)
|
||||
@@ -1683,7 +1718,7 @@ impl MultiBuffer {
|
||||
let buffers = self.buffers.borrow();
|
||||
let mut excerpts = snapshot
|
||||
.excerpts
|
||||
.cursor::<(Option<&Locator>, ExcerptDimension<Point>)>(&());
|
||||
.cursor::<(Option<&ExcerptLocator>, ExcerptDimension<Point>)>(&());
|
||||
let mut diff_transforms = snapshot
|
||||
.diff_transforms
|
||||
.cursor::<(ExcerptDimension<Point>, OutputDimension<Point>)>(&());
|
||||
@@ -1849,7 +1884,7 @@ impl MultiBuffer {
|
||||
let mut new_excerpts = SumTree::default();
|
||||
let mut cursor = snapshot
|
||||
.excerpts
|
||||
.cursor::<(Option<&Locator>, ExcerptOffset)>(&());
|
||||
.cursor::<(Option<&ExcerptLocator>, ExcerptOffset)>(&());
|
||||
let mut edits = Vec::new();
|
||||
let mut excerpt_ids = ids.iter().copied().peekable();
|
||||
|
||||
@@ -2047,7 +2082,7 @@ impl MultiBuffer {
|
||||
for locator in &buffer_state.excerpts {
|
||||
let mut cursor = snapshot
|
||||
.excerpts
|
||||
.cursor::<(Option<&Locator>, ExcerptOffset)>(&());
|
||||
.cursor::<(Option<&ExcerptLocator>, ExcerptOffset)>(&());
|
||||
cursor.seek_forward(&Some(locator), Bias::Left, &());
|
||||
if let Some(excerpt) = cursor.item() {
|
||||
if excerpt.locator == *locator {
|
||||
@@ -2308,7 +2343,7 @@ impl MultiBuffer {
|
||||
let mut new_excerpts = SumTree::default();
|
||||
let mut cursor = snapshot
|
||||
.excerpts
|
||||
.cursor::<(Option<&Locator>, ExcerptOffset)>(&());
|
||||
.cursor::<(Option<&ExcerptLocator>, ExcerptOffset)>(&());
|
||||
let mut edits = Vec::<Edit<ExcerptOffset>>::new();
|
||||
|
||||
let prefix = cursor.slice(&Some(locator), Bias::Left, &());
|
||||
@@ -2380,7 +2415,7 @@ impl MultiBuffer {
|
||||
let mut new_excerpts = SumTree::default();
|
||||
let mut cursor = snapshot
|
||||
.excerpts
|
||||
.cursor::<(Option<&Locator>, ExcerptOffset)>(&());
|
||||
.cursor::<(Option<&ExcerptLocator>, ExcerptOffset)>(&());
|
||||
let mut edits = Vec::<Edit<ExcerptOffset>>::new();
|
||||
|
||||
for locator in &locators {
|
||||
@@ -2515,7 +2550,7 @@ impl MultiBuffer {
|
||||
let mut new_excerpts = SumTree::default();
|
||||
let mut cursor = snapshot
|
||||
.excerpts
|
||||
.cursor::<(Option<&Locator>, ExcerptOffset)>(&());
|
||||
.cursor::<(Option<&ExcerptLocator>, ExcerptOffset)>(&());
|
||||
|
||||
for (locator, buffer, buffer_edited) in excerpts_to_edit {
|
||||
new_excerpts.append(cursor.slice(&Some(locator), Bias::Left, &()), &());
|
||||
@@ -3484,8 +3519,7 @@ 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)
|
||||
.min(region.buffer_range.end.key)
|
||||
region.buffer_range.start.key + start_overshoot
|
||||
} else {
|
||||
cursor.main_buffer_position()?.key
|
||||
};
|
||||
@@ -4319,7 +4353,7 @@ impl MultiBufferSnapshot {
|
||||
fn excerpt_offset_for_anchor(&self, anchor: &Anchor) -> ExcerptOffset {
|
||||
let mut cursor = self
|
||||
.excerpts
|
||||
.cursor::<(Option<&Locator>, ExcerptOffset)>(&());
|
||||
.cursor::<(Option<&ExcerptLocator>, ExcerptOffset)>(&());
|
||||
let locator = self.excerpt_locator_for_id(anchor.excerpt_id);
|
||||
|
||||
cursor.seek(&Some(locator), Bias::Left, &());
|
||||
@@ -4468,7 +4502,7 @@ impl MultiBufferSnapshot {
|
||||
I: 'a + IntoIterator<Item = &'a Anchor>,
|
||||
{
|
||||
let mut anchors = anchors.into_iter().enumerate().peekable();
|
||||
let mut cursor = self.excerpts.cursor::<Option<&Locator>>(&());
|
||||
let mut cursor = self.excerpts.cursor::<Option<&ExcerptLocator>>(&());
|
||||
cursor.next(&());
|
||||
|
||||
let mut result = Vec::new();
|
||||
@@ -4666,7 +4700,7 @@ impl MultiBufferSnapshot {
|
||||
text_anchor: text::Anchor,
|
||||
) -> Option<Anchor> {
|
||||
let locator = self.excerpt_locator_for_id(excerpt_id);
|
||||
let mut cursor = self.excerpts.cursor::<Option<&Locator>>(&());
|
||||
let mut cursor = self.excerpts.cursor::<Option<&ExcerptLocator>>(&());
|
||||
cursor.seek(locator, Bias::Left, &());
|
||||
if let Some(excerpt) = cursor.item() {
|
||||
if excerpt.id == excerpt_id {
|
||||
@@ -4720,7 +4754,7 @@ impl MultiBufferSnapshot {
|
||||
let start_locator = self.excerpt_locator_for_id(id);
|
||||
let mut excerpts = self
|
||||
.excerpts
|
||||
.cursor::<(Option<&Locator>, ExcerptDimension<usize>)>(&());
|
||||
.cursor::<(Option<&ExcerptLocator>, ExcerptDimension<usize>)>(&());
|
||||
excerpts.seek(&Some(start_locator), Bias::Left, &());
|
||||
excerpts.prev(&());
|
||||
|
||||
@@ -5511,11 +5545,11 @@ impl MultiBufferSnapshot {
|
||||
))
|
||||
}
|
||||
|
||||
fn excerpt_locator_for_id(&self, id: ExcerptId) -> &Locator {
|
||||
fn excerpt_locator_for_id(&self, id: ExcerptId) -> &ExcerptLocator {
|
||||
if id == ExcerptId::min() {
|
||||
Locator::min_ref()
|
||||
&ExcerptLocator::MIN
|
||||
} else if id == ExcerptId::max() {
|
||||
Locator::max_ref()
|
||||
&ExcerptLocator::MAX
|
||||
} else {
|
||||
let mut cursor = self.excerpt_ids.cursor::<ExcerptId>(&());
|
||||
cursor.seek(&id, Bias::Left, &());
|
||||
@@ -5532,7 +5566,7 @@ impl MultiBufferSnapshot {
|
||||
fn excerpt_locators_for_ids(
|
||||
&self,
|
||||
ids: impl IntoIterator<Item = ExcerptId>,
|
||||
) -> SmallVec<[Locator; 1]> {
|
||||
) -> SmallVec<[ExcerptLocator; 1]> {
|
||||
let mut sorted_ids = ids.into_iter().collect::<SmallVec<[_; 1]>>();
|
||||
sorted_ids.sort_unstable();
|
||||
let mut locators = SmallVec::new();
|
||||
@@ -5576,7 +5610,7 @@ impl MultiBufferSnapshot {
|
||||
pub fn range_for_excerpt(&self, excerpt_id: ExcerptId) -> Option<Range<Point>> {
|
||||
let mut cursor = self
|
||||
.excerpts
|
||||
.cursor::<(Option<&Locator>, ExcerptDimension<Point>)>(&());
|
||||
.cursor::<(Option<&ExcerptLocator>, ExcerptDimension<Point>)>(&());
|
||||
let locator = self.excerpt_locator_for_id(excerpt_id);
|
||||
if cursor.seek(&Some(locator), Bias::Left, &()) {
|
||||
let start = cursor.start().1.clone();
|
||||
@@ -5597,7 +5631,7 @@ impl MultiBufferSnapshot {
|
||||
}
|
||||
|
||||
pub fn buffer_range_for_excerpt(&self, excerpt_id: ExcerptId) -> Option<Range<text::Anchor>> {
|
||||
let mut cursor = self.excerpts.cursor::<Option<&Locator>>(&());
|
||||
let mut cursor = self.excerpts.cursor::<Option<&ExcerptLocator>>(&());
|
||||
let locator = self.excerpt_locator_for_id(excerpt_id);
|
||||
if cursor.seek(&Some(locator), Bias::Left, &()) {
|
||||
if let Some(excerpt) = cursor.item() {
|
||||
@@ -5608,7 +5642,7 @@ impl MultiBufferSnapshot {
|
||||
}
|
||||
|
||||
fn excerpt(&self, excerpt_id: ExcerptId) -> Option<&Excerpt> {
|
||||
let mut cursor = self.excerpts.cursor::<Option<&Locator>>(&());
|
||||
let mut cursor = self.excerpts.cursor::<Option<&ExcerptLocator>>(&());
|
||||
let locator = self.excerpt_locator_for_id(excerpt_id);
|
||||
cursor.seek(&Some(locator), Bias::Left, &());
|
||||
if let Some(excerpt) = cursor.item() {
|
||||
@@ -5728,7 +5762,7 @@ impl MultiBufferSnapshot {
|
||||
|
||||
for (ix, excerpt) in excerpts.iter().enumerate() {
|
||||
if ix == 0 {
|
||||
if excerpt.locator <= Locator::min() {
|
||||
if excerpt.locator <= ExcerptLocator::MIN {
|
||||
panic!("invalid first excerpt locator {:?}", excerpt.locator);
|
||||
}
|
||||
} else if excerpt.locator <= excerpts[ix - 1].locator {
|
||||
@@ -6203,7 +6237,7 @@ impl History {
|
||||
impl Excerpt {
|
||||
fn new(
|
||||
id: ExcerptId,
|
||||
locator: Locator,
|
||||
locator: ExcerptLocator,
|
||||
buffer_id: BufferId,
|
||||
buffer: BufferSnapshot,
|
||||
range: ExcerptRange<text::Anchor>,
|
||||
@@ -6586,13 +6620,13 @@ impl<'a> sum_tree::SeekTarget<'a, ExcerptSummary, ExcerptSummary> for ExcerptOff
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> sum_tree::SeekTarget<'a, ExcerptSummary, Option<&'a Locator>> for Locator {
|
||||
fn cmp(&self, cursor_location: &Option<&'a Locator>, _: &()) -> cmp::Ordering {
|
||||
impl<'a> sum_tree::SeekTarget<'a, ExcerptSummary, Option<&'a ExcerptLocator>> for ExcerptLocator {
|
||||
fn cmp(&self, cursor_location: &Option<&'a ExcerptLocator>, _: &()) -> cmp::Ordering {
|
||||
Ord::cmp(&Some(self), cursor_location)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> sum_tree::SeekTarget<'a, ExcerptSummary, ExcerptSummary> for Locator {
|
||||
impl<'a> sum_tree::SeekTarget<'a, ExcerptSummary, ExcerptSummary> for ExcerptLocator {
|
||||
fn cmp(&self, cursor_location: &ExcerptSummary, _: &()) -> cmp::Ordering {
|
||||
Ord::cmp(self, &cursor_location.excerpt_locator)
|
||||
}
|
||||
@@ -6620,7 +6654,7 @@ impl<'a, D: TextDimension + Default> sum_tree::Dimension<'a, ExcerptSummary>
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> sum_tree::Dimension<'a, ExcerptSummary> for Option<&'a Locator> {
|
||||
impl<'a> sum_tree::Dimension<'a, ExcerptSummary> for Option<&'a ExcerptLocator> {
|
||||
fn zero(_cx: &()) -> Self {
|
||||
Default::default()
|
||||
}
|
||||
|
||||
@@ -669,10 +669,8 @@ fn test_excerpt_events(cx: &mut App) {
|
||||
&leader_multibuffer,
|
||||
move |follower, _, event, cx| match event.clone() {
|
||||
Event::ExcerptsAdded {
|
||||
buffer,
|
||||
predecessor,
|
||||
excerpts,
|
||||
} => follower.insert_excerpts_with_ids_after(predecessor, buffer, excerpts, cx),
|
||||
buffer, excerpts, ..
|
||||
} => follower.insert_excerpts_with_ids(buffer, excerpts, cx),
|
||||
Event::ExcerptsRemoved { ids } => follower.remove_excerpts(ids, cx),
|
||||
Event::Edited { .. } => {
|
||||
*follower_edit_event_count.write() += 1;
|
||||
@@ -698,8 +696,7 @@ fn test_excerpt_events(cx: &mut App) {
|
||||
],
|
||||
cx,
|
||||
);
|
||||
leader.insert_excerpts_after(
|
||||
leader.excerpt_ids()[0],
|
||||
leader.push_excerpts(
|
||||
buffer_2.clone(),
|
||||
[
|
||||
ExcerptRange {
|
||||
@@ -1155,8 +1152,7 @@ fn test_resolving_anchors_after_replacing_their_excerpts(cx: &mut App) {
|
||||
let excerpt_id_5 = multibuffer.update(cx, |multibuffer, cx| {
|
||||
multibuffer.remove_excerpts([excerpt_id_3], cx);
|
||||
multibuffer
|
||||
.insert_excerpts_after(
|
||||
excerpt_id_2,
|
||||
.push_excerpts(
|
||||
buffer_2.clone(),
|
||||
[ExcerptRange {
|
||||
context: 5..8,
|
||||
@@ -2249,8 +2245,7 @@ async fn test_random_multibuffer(cx: &mut TestAppContext, mut rng: StdRng) {
|
||||
|
||||
let excerpt_id = multibuffer.update(cx, |multibuffer, cx| {
|
||||
multibuffer
|
||||
.insert_excerpts_after(
|
||||
prev_excerpt_id,
|
||||
.push_excerpts(
|
||||
buffer_handle.clone(),
|
||||
[ExcerptRange {
|
||||
context: range,
|
||||
|
||||
@@ -78,8 +78,6 @@ pub enum Model {
|
||||
O1Preview,
|
||||
#[serde(rename = "o1-mini", alias = "o1-mini")]
|
||||
O1Mini,
|
||||
#[serde(rename = "o3-mini", alias = "o3-mini")]
|
||||
O3Mini,
|
||||
|
||||
#[serde(rename = "custom")]
|
||||
Custom {
|
||||
@@ -117,7 +115,6 @@ impl Model {
|
||||
Self::O1 => "o1",
|
||||
Self::O1Preview => "o1-preview",
|
||||
Self::O1Mini => "o1-mini",
|
||||
Self::O3Mini => "o3-mini",
|
||||
Self::Custom { name, .. } => name,
|
||||
}
|
||||
}
|
||||
@@ -132,7 +129,6 @@ 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),
|
||||
@@ -149,7 +145,6 @@ impl Model {
|
||||
Self::O1 => 200000,
|
||||
Self::O1Preview => 128000,
|
||||
Self::O1Mini => 128000,
|
||||
Self::O3Mini => 200000,
|
||||
Self::Custom { max_tokens, .. } => *max_tokens,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2223,9 +2223,7 @@ impl BufferChangeSet {
|
||||
let base_text = cx.background_executor().spawn(snapshot).await;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.base_text = Some(base_text);
|
||||
cx.emit(BufferChangeSetEvent::DiffChanged {
|
||||
changed_range: text::Anchor::MIN..text::Anchor::MAX,
|
||||
});
|
||||
cx.notify();
|
||||
})
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -39,9 +39,6 @@ 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)]
|
||||
|
||||
@@ -3,6 +3,7 @@ use fs::Fs;
|
||||
use futures::{channel::mpsc, StreamExt};
|
||||
use gpui::{App, BackgroundExecutor, ReadGlobal, UpdateGlobal};
|
||||
use std::{path::PathBuf, sync::Arc, time::Duration};
|
||||
use util::ResultExt;
|
||||
|
||||
pub const EMPTY_THEME_NAME: &str = "empty-theme";
|
||||
|
||||
@@ -72,11 +73,9 @@ pub fn handle_settings_file_changes(
|
||||
.block(user_settings_file_rx.next())
|
||||
.unwrap();
|
||||
SettingsStore::update_global(cx, |store, cx| {
|
||||
let result = store.set_user_settings(&user_settings_content, cx);
|
||||
if let Err(err) = &result {
|
||||
log::error!("Failed to load user settings: {err}");
|
||||
}
|
||||
settings_changed(result.err(), cx);
|
||||
store
|
||||
.set_user_settings(&user_settings_content, cx)
|
||||
.log_err();
|
||||
});
|
||||
cx.spawn(move |cx| async move {
|
||||
while let Some(user_settings_content) = user_settings_file_rx.next().await {
|
||||
|
||||
@@ -90,7 +90,10 @@ fn completion_from_diff(
|
||||
edits.push((edit_range, edit_text));
|
||||
}
|
||||
|
||||
InlineCompletion { edits }
|
||||
InlineCompletion {
|
||||
edits,
|
||||
edit_preview: None,
|
||||
}
|
||||
}
|
||||
|
||||
impl InlineCompletionProvider for SupermavenCompletionProvider {
|
||||
|
||||
@@ -1075,15 +1075,6 @@ 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 {
|
||||
|
||||
@@ -35,10 +35,10 @@ use workspace::{
|
||||
item::SerializableItem,
|
||||
move_active_item, move_item, pane,
|
||||
ui::IconName,
|
||||
ActivateNextPane, ActivatePane, ActivatePaneInDirection, ActivatePreviousPane,
|
||||
DraggedSelection, DraggedTab, ItemId, MoveItemToPane, MoveItemToPaneInDirection, NewTerminal,
|
||||
Pane, PaneGroup, SplitDirection, SplitDown, SplitLeft, SplitRight, SplitUp,
|
||||
SwapPaneInDirection, ToggleZoom, Workspace,
|
||||
ActivateNextPane, ActivatePane, ActivatePaneInDirection, ActivatePreviousPane, DraggedTab,
|
||||
ItemId, MoveItemToPane, MoveItemToPaneInDirection, NewTerminal, Pane, PaneGroup,
|
||||
SplitDirection, SplitDown, SplitLeft, SplitRight, SplitUp, SwapPaneInDirection, ToggleZoom,
|
||||
Workspace,
|
||||
};
|
||||
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
@@ -1037,17 +1037,6 @@ pub fn new_terminal_pane(
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if let Some(selection) = dropped_item.downcast_ref::<DraggedSelection>() {
|
||||
let project = project.read(cx);
|
||||
let paths_to_add = selection
|
||||
.items()
|
||||
.map(|selected_entry| selected_entry.entry_id)
|
||||
.filter_map(|entry_id| project.path_for_entry(entry_id, cx))
|
||||
.filter_map(|project_path| project.absolute_path(&project_path, cx))
|
||||
.collect::<Vec<_>>();
|
||||
if !paths_to_add.is_empty() {
|
||||
add_paths_to_terminal(pane, &paths_to_add, window, cx);
|
||||
}
|
||||
} else if let Some(&entry_id) = dropped_item.downcast_ref::<ProjectEntryId>() {
|
||||
if let Some(entry_path) = project
|
||||
.read(cx)
|
||||
|
||||
@@ -1130,6 +1130,7 @@ impl Render for TerminalView {
|
||||
let focused = self.focus_handle.is_focused(window);
|
||||
|
||||
div()
|
||||
.occlude()
|
||||
.id("terminal-view")
|
||||
.size_full()
|
||||
.relative()
|
||||
|
||||
@@ -3,9 +3,8 @@ use std::sync::Arc;
|
||||
use gpui::{hsla, FontStyle, FontWeight, HighlightStyle, Hsla, WindowBackgroundAppearance};
|
||||
|
||||
use crate::{
|
||||
default_color_scales, AccentColors, Appearance, PlayerColors, StatusColors,
|
||||
StatusColorsRefinement, SyntaxTheme, SystemColors, Theme, ThemeColors, ThemeFamily,
|
||||
ThemeStyles,
|
||||
default_color_scales, AccentColors, Appearance, PlayerColors, StatusColors, SyntaxTheme,
|
||||
SystemColors, Theme, ThemeColors, ThemeFamily, ThemeStyles,
|
||||
};
|
||||
|
||||
/// The default theme family for Zed.
|
||||
@@ -22,26 +21,6 @@ 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,19 +212,6 @@ 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
|
||||
@@ -255,14 +242,6 @@ 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 {
|
||||
@@ -273,21 +252,23 @@ impl ThemeRegistry {
|
||||
AppearanceContent::Dark => Appearance::Dark,
|
||||
},
|
||||
directory_icons: DirectoryIcons {
|
||||
collapsed: icon_theme.directory_icons.collapsed.map(resolve_icon_path),
|
||||
expanded: icon_theme.directory_icons.expanded.map(resolve_icon_path),
|
||||
collapsed: icon_theme.directory_icons.collapsed,
|
||||
expanded: icon_theme.directory_icons.expanded,
|
||||
},
|
||||
chevron_icons: ChevronIcons {
|
||||
collapsed: icon_theme.chevron_icons.collapsed.map(resolve_icon_path),
|
||||
expanded: icon_theme.chevron_icons.expanded.map(resolve_icon_path),
|
||||
collapsed: icon_theme.chevron_icons.collapsed,
|
||||
expanded: icon_theme.chevron_icons.expanded,
|
||||
},
|
||||
file_icons: icon_theme
|
||||
.file_icons
|
||||
.into_iter()
|
||||
.map(|(key, icon)| {
|
||||
let path = icons_root_dir.join(icon.path.as_ref());
|
||||
|
||||
(
|
||||
key,
|
||||
IconDefinition {
|
||||
path: resolve_icon_path(icon.path),
|
||||
path: path.to_string_lossy().to_string().into(),
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
@@ -319,6 +319,9 @@ 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().opacity(0.25),
|
||||
created_background: grass().dark().step_9(),
|
||||
created_border: grass().dark().step_9(),
|
||||
deleted: red().dark().step_9(),
|
||||
deleted_background: red().dark().step_9().opacity(0.25),
|
||||
deleted_background: red().dark().step_9(),
|
||||
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().opacity(0.25),
|
||||
modified_background: yellow().dark().step_9(),
|
||||
modified_border: yellow().dark().step_9(),
|
||||
predictive: neutral().dark_alpha().step_9(),
|
||||
predictive_background: neutral().dark_alpha().step_9(),
|
||||
|
||||
@@ -24,7 +24,6 @@ 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,
|
||||
@@ -156,9 +155,7 @@ impl ThemeFamily {
|
||||
AppearanceContent::Light => StatusColors::light(),
|
||||
AppearanceContent::Dark => StatusColors::dark(),
|
||||
};
|
||||
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);
|
||||
refined_status_colors.refine(&theme.style.status_colors_refinement());
|
||||
|
||||
let mut refined_player_colors = match theme.appearance {
|
||||
AppearanceContent::Light => PlayerColors::light(),
|
||||
|
||||
@@ -1,274 +0,0 @@
|
||||
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,5 +1,3 @@
|
||||
mod icon_theme_selector;
|
||||
|
||||
use fs::Fs;
|
||||
use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
|
||||
use gpui::{
|
||||
@@ -13,25 +11,22 @@ use theme::{Appearance, Theme, ThemeMeta, ThemeRegistry, ThemeSettings};
|
||||
use ui::{prelude::*, v_flex, ListItem, ListItemSpacing};
|
||||
use util::ResultExt;
|
||||
use workspace::{ui::HighlightedLabel, ModalView, Workspace};
|
||||
|
||||
use crate::icon_theme_selector::{IconThemeSelector, IconThemeSelectorDelegate};
|
||||
use zed_actions::theme_selector::Toggle;
|
||||
|
||||
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_theme_selector)
|
||||
.register_action(toggle_icon_theme_selector);
|
||||
workspace.register_action(toggle);
|
||||
},
|
||||
)
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn toggle_theme_selector(
|
||||
pub fn toggle(
|
||||
workspace: &mut Workspace,
|
||||
toggle: &zed_actions::theme_selector::Toggle,
|
||||
toggle: &Toggle,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Workspace>,
|
||||
) {
|
||||
@@ -47,27 +42,9 @@ fn toggle_theme_selector(
|
||||
});
|
||||
}
|
||||
|
||||
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 {}
|
||||
|
||||
struct ThemeSelector {
|
||||
pub struct ThemeSelector {
|
||||
picker: Entity<Picker<ThemeSelectorDelegate>>,
|
||||
}
|
||||
|
||||
@@ -96,7 +73,7 @@ impl ThemeSelector {
|
||||
}
|
||||
}
|
||||
|
||||
struct ThemeSelectorDelegate {
|
||||
pub struct ThemeSelectorDelegate {
|
||||
fs: Arc<dyn Fs>,
|
||||
themes: Vec<ThemeMeta>,
|
||||
matches: Vec<StringMatch>,
|
||||
|
||||
@@ -48,7 +48,6 @@ 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,7 +37,6 @@ 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::*;
|
||||
@@ -114,7 +113,6 @@ 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 {
|
||||
@@ -198,7 +196,6 @@ 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()
|
||||
@@ -274,7 +271,6 @@ 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();
|
||||
@@ -310,16 +306,6 @@ 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()),
|
||||
@@ -333,7 +319,6 @@ impl TitleBar {
|
||||
client,
|
||||
_subscriptions: subscriptions,
|
||||
git_ui_enabled: is_git_ui_enabled,
|
||||
zed_predict_banner,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -64,11 +64,6 @@ 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_5()
|
||||
.h_6()
|
||||
.when(self.inset, |this| this.px_2())
|
||||
.when(self.selected, |this| {
|
||||
this.bg(cx.theme().colors().ghost_element_selected)
|
||||
@@ -70,11 +70,7 @@ impl RenderOnce for ListSubHeader {
|
||||
Icon::new(i).color(Color::Muted).size(IconSize::Small)
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
Label::new(self.label.clone())
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::Small),
|
||||
),
|
||||
.child(Label::new(self.label.clone()).color(Color::Muted)),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -81,7 +81,6 @@ pub enum Operator {
|
||||
first_char: Option<char>,
|
||||
},
|
||||
AddSurrounds {
|
||||
// Typically no need to configure this as `SendKeystrokes` can be used - see #23088.
|
||||
#[serde(skip)]
|
||||
target: Option<SurroundsType>,
|
||||
},
|
||||
|
||||
@@ -3,12 +3,17 @@ use gpui::{
|
||||
svg, AnyView, App, AppContext as _, AsyncWindowContext, ClipboardItem, Context, DismissEvent,
|
||||
Entity, EventEmitter, Global, PromptLevel, Render, ScrollHandle, Task,
|
||||
};
|
||||
use parking_lot::Mutex;
|
||||
use std::sync::{Arc, LazyLock};
|
||||
use std::rc::Rc;
|
||||
use std::{any::TypeId, time::Duration};
|
||||
use ui::{prelude::*, Tooltip};
|
||||
use util::ResultExt;
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
cx.set_global(GlobalAppNotifications {
|
||||
app_notifications: Vec::new(),
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub enum NotificationId {
|
||||
Unique(TypeId),
|
||||
@@ -157,7 +162,7 @@ impl Workspace {
|
||||
pub fn show_initial_notifications(&mut self, cx: &mut Context<Self>) {
|
||||
// Allow absence of the global so that tests don't need to initialize it.
|
||||
let app_notifications = cx
|
||||
.try_global::<AppNotifications>()
|
||||
.try_global::<GlobalAppNotifications>()
|
||||
.iter()
|
||||
.flat_map(|global| global.app_notifications.iter().cloned())
|
||||
.collect::<Vec<_>>();
|
||||
@@ -379,12 +384,6 @@ 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 {}
|
||||
@@ -408,12 +407,6 @@ 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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -449,85 +442,31 @@ 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_3()
|
||||
.gap_2()
|
||||
.elevation_3(cx)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_4()
|
||||
.justify_between()
|
||||
.items_start()
|
||||
.child(div().max_w_96().child((self.build_content)(window, cx)))
|
||||
.child(
|
||||
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))),
|
||||
)
|
||||
}),
|
||||
IconButton::new("close", IconName::Close)
|
||||
.on_click(cx.listener(|this, _, _, cx| this.dismiss(cx))),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.gap_2()
|
||||
.children(self.click_message.iter().map(|message| {
|
||||
Button::new(message.clone(), message.clone())
|
||||
.label_size(LabelSize::Small)
|
||||
@@ -555,66 +494,27 @@ 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);
|
||||
}))
|
||||
}),
|
||||
),
|
||||
),
|
||||
})),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static GLOBAL_APP_NOTIFICATIONS: LazyLock<Mutex<AppNotifications>> = LazyLock::new(|| {
|
||||
Mutex::new(AppNotifications {
|
||||
app_notifications: Vec::new(),
|
||||
})
|
||||
});
|
||||
|
||||
/// Stores app notifications so that they can be shown in new workspaces.
|
||||
struct AppNotifications {
|
||||
struct GlobalAppNotifications {
|
||||
app_notifications: Vec<(
|
||||
NotificationId,
|
||||
Arc<dyn Fn(&mut Context<Workspace>) -> AnyView + Send + Sync>,
|
||||
Rc<dyn Fn(&mut Context<Workspace>) -> AnyView>,
|
||||
)>,
|
||||
}
|
||||
|
||||
impl Global for AppNotifications {}
|
||||
impl Global for GlobalAppNotifications {}
|
||||
|
||||
impl AppNotifications {
|
||||
impl GlobalAppNotifications {
|
||||
pub fn insert(
|
||||
&mut self,
|
||||
id: NotificationId,
|
||||
build_notification: Arc<dyn Fn(&mut Context<Workspace>) -> AnyView + Send + Sync>,
|
||||
build_notification: Rc<dyn Fn(&mut Context<Workspace>) -> AnyView>,
|
||||
) {
|
||||
self.remove(&id);
|
||||
self.app_notifications.push((id, build_notification))
|
||||
@@ -632,30 +532,28 @@ impl AppNotifications {
|
||||
pub fn show_app_notification<V: Notification + 'static>(
|
||||
id: NotificationId,
|
||||
cx: &mut App,
|
||||
build_notification: impl Fn(&mut Context<Workspace>) -> Entity<V> + 'static + Send + Sync,
|
||||
build_notification: impl Fn(&mut Context<Workspace>) -> Entity<V> + 'static,
|
||||
) {
|
||||
// Defer notification creation so that windows on the stack can be returned to GPUI
|
||||
cx.defer(move |cx| {
|
||||
// Handle dismiss events by removing the notification from all workspaces.
|
||||
let build_notification: Arc<dyn Fn(&mut Context<Workspace>) -> AnyView + Send + Sync> =
|
||||
Arc::new({
|
||||
let id = id.clone();
|
||||
move |cx| {
|
||||
let notification = build_notification(cx);
|
||||
cx.subscribe(¬ification, {
|
||||
let id = id.clone();
|
||||
move |_, _, _: &DismissEvent, cx| {
|
||||
dismiss_app_notification(&id, cx);
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
notification.into()
|
||||
}
|
||||
});
|
||||
let build_notification: Rc<dyn Fn(&mut Context<Workspace>) -> AnyView> = Rc::new({
|
||||
let id = id.clone();
|
||||
move |cx| {
|
||||
let notification = build_notification(cx);
|
||||
cx.subscribe(¬ification, {
|
||||
let id = id.clone();
|
||||
move |_, _, _: &DismissEvent, cx| {
|
||||
dismiss_app_notification(&id, cx);
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
notification.into()
|
||||
}
|
||||
});
|
||||
|
||||
// Store the notification so that new workspaces also receive it.
|
||||
GLOBAL_APP_NOTIFICATIONS
|
||||
.lock()
|
||||
cx.global_mut::<GlobalAppNotifications>()
|
||||
.insert(id.clone(), build_notification.clone());
|
||||
|
||||
for window in cx.windows() {
|
||||
@@ -678,7 +576,7 @@ pub fn dismiss_app_notification(id: &NotificationId, cx: &mut App) {
|
||||
let id = id.clone();
|
||||
// Defer notification dismissal so that windows on the stack can be returned to GPUI
|
||||
cx.defer(move |cx| {
|
||||
GLOBAL_APP_NOTIFICATIONS.lock().remove(&id);
|
||||
cx.global_mut::<GlobalAppNotifications>().remove(&id);
|
||||
for window in cx.windows() {
|
||||
if let Some(workspace_window) = window.downcast::<Workspace>() {
|
||||
let id = id.clone();
|
||||
|
||||
@@ -58,9 +58,7 @@ use persistence::{
|
||||
SerializedWindowBounds, DB,
|
||||
};
|
||||
use postage::stream::Stream;
|
||||
use project::{
|
||||
DirectoryLister, Project, ProjectEntryId, ProjectPath, ResolvedPath, Worktree, WorktreeId,
|
||||
};
|
||||
use project::{DirectoryLister, Project, ProjectEntryId, ProjectPath, ResolvedPath, Worktree};
|
||||
use remote::{ssh_session::ConnectionIdentifier, SshClientDelegate, SshConnectionOptions};
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
@@ -367,6 +365,7 @@ fn prompt_and_open_paths(app_state: Arc<AppState>, options: PathPromptOptions, c
|
||||
|
||||
pub fn init(app_state: Arc<AppState>, cx: &mut App) {
|
||||
init_settings(cx);
|
||||
notifications::init(cx);
|
||||
theme_preview::init(cx);
|
||||
|
||||
cx.on_action(Workspace::close_global);
|
||||
@@ -1284,10 +1283,7 @@ impl Workspace {
|
||||
.unwrap_or_default();
|
||||
|
||||
window
|
||||
.update(&mut cx, |_, window, cx| {
|
||||
window.activate_window();
|
||||
cx.activate(true);
|
||||
})
|
||||
.update(&mut cx, |_, window, _| window.activate_window())
|
||||
.log_err();
|
||||
Ok((window, opened_items))
|
||||
})
|
||||
@@ -2205,18 +2201,6 @@ 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,10 +2670,21 @@ impl Snapshot {
|
||||
|
||||
/// Get the repository whose work directory contains the given path.
|
||||
pub fn repository_for_path(&self, path: &Path) -> Option<&RepositoryEntry> {
|
||||
self.repositories
|
||||
.iter()
|
||||
.filter(|repo| repo.work_directory.directory_contains(path))
|
||||
.last()
|
||||
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
|
||||
}
|
||||
|
||||
/// Given an ordered iterator of entries, returns an iterator of those entries,
|
||||
@@ -2745,8 +2756,6 @@ 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())
|
||||
@@ -5965,6 +5974,7 @@ impl<'a> Iterator for Traversal<'a> {
|
||||
enum PathTarget<'a> {
|
||||
Path(&'a Path),
|
||||
Successor(&'a Path),
|
||||
Contains(&'a Path),
|
||||
}
|
||||
|
||||
impl<'a> PathTarget<'a> {
|
||||
@@ -5978,6 +5988,13 @@ 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.8"
|
||||
version = "0.172.0"
|
||||
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_onboarding.workspace = true
|
||||
zed_predict_tos.workspace = true
|
||||
anyhow.workspace = true
|
||||
assets.workspace = true
|
||||
assistant.workspace = true
|
||||
|
||||
@@ -1 +1 @@
|
||||
stable
|
||||
dev
|
||||
|
||||
@@ -18,7 +18,7 @@ use extension::ExtensionHostProxy;
|
||||
use fs::{Fs, RealFs};
|
||||
use futures::{future, StreamExt};
|
||||
use git::GitHostingProviderRegistry;
|
||||
use gpui::{App, AppContext as _, Application, AsyncApp, UpdateGlobal as _};
|
||||
use gpui::{Action, App, AppContext as _, Application, AsyncApp, DismissEvent, UpdateGlobal as _};
|
||||
|
||||
use http_client::{read_proxy_from_env, Uri};
|
||||
use language::LanguageRegistry;
|
||||
@@ -33,7 +33,9 @@ use project::project_settings::ProjectSettings;
|
||||
use recent_projects::{open_ssh_project, SshSettings};
|
||||
use release_channel::{AppCommitSha, AppVersion, ReleaseChannel};
|
||||
use session::{AppSession, Session};
|
||||
use settings::{handle_settings_file_changes, watch_config_file, Settings, SettingsStore};
|
||||
use settings::{
|
||||
handle_settings_file_changes, watch_config_file, InvalidSettingsError, Settings, SettingsStore,
|
||||
};
|
||||
use simplelog::ConfigBuilder;
|
||||
use std::{
|
||||
env,
|
||||
@@ -48,13 +50,18 @@ use time::UtcOffset;
|
||||
use util::{maybe, ResultExt, TryFutureExt};
|
||||
use uuid::Uuid;
|
||||
use welcome::{show_welcome_view, BaseKeymap, FIRST_OPEN};
|
||||
use workspace::{AppState, SerializedWorkspaceLocation, WorkspaceSettings, WorkspaceStore};
|
||||
use workspace::{
|
||||
notifications::{simple_message_notification::MessageNotification, NotificationId},
|
||||
AppState, SerializedWorkspaceLocation, WorkspaceSettings, WorkspaceStore,
|
||||
};
|
||||
use zed::{
|
||||
app_menus, build_window_options, derive_paths_with_position, handle_cli_connection,
|
||||
handle_keymap_file_changes, handle_settings_changed, initialize_workspace,
|
||||
inline_completion_registry, open_paths_with_positions, OpenListener, OpenRequest,
|
||||
handle_keymap_file_changes, initialize_workspace, open_paths_with_positions, OpenListener,
|
||||
OpenRequest,
|
||||
};
|
||||
|
||||
use crate::zed::inline_completion_registry;
|
||||
|
||||
#[cfg(unix)]
|
||||
use util::{load_login_shell_environment, load_shell_from_passwd};
|
||||
|
||||
@@ -434,7 +441,6 @@ 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);
|
||||
@@ -608,6 +614,44 @@ fn main() {
|
||||
});
|
||||
}
|
||||
|
||||
fn handle_settings_changed(error: Option<anyhow::Error>, cx: &mut App) {
|
||||
struct SettingsParseErrorNotification;
|
||||
let id = NotificationId::unique::<SettingsParseErrorNotification>();
|
||||
|
||||
for workspace in workspace::local_workspace_windows(cx) {
|
||||
workspace
|
||||
.update(cx, |workspace, _, cx| {
|
||||
match error.as_ref() {
|
||||
Some(error) => {
|
||||
if let Some(InvalidSettingsError::LocalSettings { .. }) =
|
||||
error.downcast_ref::<InvalidSettingsError>()
|
||||
{
|
||||
// Local settings will be displayed by the projects
|
||||
} else {
|
||||
workspace.show_notification(id.clone(), cx, |cx| {
|
||||
cx.new(|_cx| {
|
||||
MessageNotification::new(format!(
|
||||
"Invalid user settings file\n{error}"
|
||||
))
|
||||
.with_click_message("Open settings file")
|
||||
.on_click(|window, cx| {
|
||||
window.dispatch_action(
|
||||
zed_actions::OpenSettings.boxed_clone(),
|
||||
cx,
|
||||
);
|
||||
cx.emit(DismissEvent);
|
||||
})
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
None => workspace.dismiss_notification(&id, cx),
|
||||
}
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_open_request(request: OpenRequest, app_state: Arc<AppState>, cx: &mut App) {
|
||||
if let Some(connection) = request.cli_connection {
|
||||
let app_state = app_state.clone();
|
||||
|
||||
@@ -39,13 +39,12 @@ use release_channel::{AppCommitSha, ReleaseChannel};
|
||||
use rope::Rope;
|
||||
use search::project_search::ProjectSearchBar;
|
||||
use settings::{
|
||||
initial_project_settings_content, initial_tasks_content, update_settings_file,
|
||||
InvalidSettingsError, KeymapFile, KeymapFileLoadResult, Settings, SettingsStore,
|
||||
DEFAULT_KEYMAP_PATH, VIM_KEYMAP_PATH,
|
||||
initial_project_settings_content, initial_tasks_content, update_settings_file, KeymapFile,
|
||||
KeymapFileLoadResult, Settings, SettingsStore, DEFAULT_KEYMAP_PATH, VIM_KEYMAP_PATH,
|
||||
};
|
||||
use std::any::TypeId;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::atomic::{self, AtomicBool};
|
||||
use std::rc::Rc;
|
||||
use std::{borrow::Cow, ops::Deref, path::Path, sync::Arc};
|
||||
use terminal_view::terminal_panel::{self, TerminalPanel};
|
||||
use theme::{ActiveTheme, ThemeSettings};
|
||||
@@ -177,7 +176,6 @@ 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,
|
||||
)
|
||||
@@ -966,12 +964,7 @@ 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| {
|
||||
@@ -988,27 +981,23 @@ fn quit(_: &Quit, cx: &mut App) {
|
||||
})
|
||||
.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 (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 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(());
|
||||
}
|
||||
if let Some(answer) = answer {
|
||||
let answer = answer.await.ok();
|
||||
if answer != Some(0) {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1231,7 +1220,7 @@ fn show_keymap_file_load_error(
|
||||
});
|
||||
|
||||
cx.spawn(move |cx| async move {
|
||||
let parsed_markdown = Arc::new(parsed_markdown.await);
|
||||
let parsed_markdown = Rc::new(parsed_markdown.await);
|
||||
cx.update(|cx| {
|
||||
show_app_notification(notification_id, cx, move |cx| {
|
||||
let workspace_handle = cx.entity().downgrade();
|
||||
@@ -1285,33 +1274,6 @@ pub fn load_default_keymap(cx: &mut App) {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_settings_changed(error: Option<anyhow::Error>, cx: &mut App) {
|
||||
struct SettingsParseErrorNotification;
|
||||
let id = NotificationId::unique::<SettingsParseErrorNotification>();
|
||||
|
||||
match error {
|
||||
Some(error) => {
|
||||
if let Some(InvalidSettingsError::LocalSettings { .. }) =
|
||||
error.downcast_ref::<InvalidSettingsError>()
|
||||
{
|
||||
// Local settings errors are displayed by the projects
|
||||
return;
|
||||
}
|
||||
show_app_notification(id, cx, move |cx| {
|
||||
cx.new(|_cx| {
|
||||
MessageNotification::new(format!("Invalid user settings file\n{error}"))
|
||||
.with_click_message("Open settings file")
|
||||
.on_click(|window, cx| {
|
||||
window.dispatch_action(zed_actions::OpenSettings.boxed_clone(), cx);
|
||||
cx.emit(DismissEvent);
|
||||
})
|
||||
})
|
||||
});
|
||||
}
|
||||
None => dismiss_app_notification(&id, cx),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn open_new_ssh_project_from_project(
|
||||
workspace: &mut Workspace,
|
||||
paths: Vec<PathBuf>,
|
||||
|
||||
@@ -5,17 +5,13 @@ use collections::HashMap;
|
||||
use copilot::{Copilot, CopilotCompletionProvider};
|
||||
use editor::{Editor, EditorMode};
|
||||
use feature_flags::{FeatureFlagAppExt, PredictEditsFeatureFlag};
|
||||
use fs::Fs;
|
||||
use gpui::{AnyWindowHandle, App, AppContext, Context, Entity, WeakEntity};
|
||||
use gpui::{AnyWindowHandle, App, AppContext as _, Context, Entity, WeakEntity, Window};
|
||||
use language::language_settings::{all_language_settings, InlineCompletionProvider};
|
||||
use settings::SettingsStore;
|
||||
use supermaven::{Supermaven, SupermavenCompletionProvider};
|
||||
use ui::Window;
|
||||
use workspace::Workspace;
|
||||
use zed_predict_onboarding::ZedPredictModal;
|
||||
use zeta::ProviderDataCollection;
|
||||
use zed_predict_tos::ZedPredictTos;
|
||||
|
||||
pub fn init(client: Arc<Client>, user_store: Entity<UserStore>, fs: Arc<dyn Fs>, cx: &mut App) {
|
||||
pub fn init(client: Arc<Client>, user_store: Entity<UserStore>, cx: &mut App) {
|
||||
let editors: Rc<RefCell<HashMap<WeakEntity<Editor>, AnyWindowHandle>>> = Rc::default();
|
||||
cx.observe_new({
|
||||
let editors = editors.clone();
|
||||
@@ -41,7 +37,6 @@ pub fn init(client: Arc<Client>, user_store: Entity<UserStore>, fs: Arc<dyn Fs>,
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
editors
|
||||
.borrow_mut()
|
||||
.insert(editor_handle, window.window_handle());
|
||||
@@ -96,7 +91,6 @@ pub fn init(client: Arc<Client>, user_store: Entity<UserStore>, fs: Arc<dyn Fs>,
|
||||
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 {
|
||||
@@ -129,11 +123,9 @@ pub fn init(client: Arc<Client>, user_store: Entity<UserStore>, fs: Arc<dyn Fs>,
|
||||
|
||||
window
|
||||
.update(cx, |_, window, cx| {
|
||||
ZedPredictModal::toggle(
|
||||
ZedPredictTos::toggle(
|
||||
workspace,
|
||||
user_store.clone(),
|
||||
client.clone(),
|
||||
fs.clone(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
@@ -222,19 +214,17 @@ fn register_backward_compatible_actions(editor: &mut Editor, cx: &mut Context<Ed
|
||||
|
||||
fn assign_inline_completion_provider(
|
||||
editor: &mut Editor,
|
||||
provider: InlineCompletionProvider,
|
||||
provider: language::language_settings::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 {
|
||||
InlineCompletionProvider::None => {}
|
||||
InlineCompletionProvider::Copilot => {
|
||||
language::language_settings::InlineCompletionProvider::None => {}
|
||||
language::language_settings::InlineCompletionProvider::Copilot => {
|
||||
if let Some(copilot) = Copilot::global(cx) {
|
||||
if let Some(buffer) = singleton_buffer {
|
||||
if let Some(buffer) = editor.buffer().read(cx).as_singleton() {
|
||||
if buffer.read(cx).file().is_some() {
|
||||
copilot.update(cx, |copilot, cx| {
|
||||
copilot.register_buffer(&buffer, cx);
|
||||
@@ -245,35 +235,26 @@ fn assign_inline_completion_provider(
|
||||
editor.set_inline_completion_provider(Some(provider), window, cx);
|
||||
}
|
||||
}
|
||||
InlineCompletionProvider::Supermaven => {
|
||||
language::language_settings::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);
|
||||
}
|
||||
}
|
||||
InlineCompletionProvider::Zed => {
|
||||
|
||||
language::language_settings::InlineCompletionProvider::Zed => {
|
||||
if cx.has_flag::<PredictEditsFeatureFlag>()
|
||||
|| (cfg!(debug_assertions) && client.status().borrow().is_connected())
|
||||
{
|
||||
let zeta = zeta::Zeta::register(client.clone(), user_store, cx);
|
||||
if let Some(buffer) = &singleton_buffer {
|
||||
if let Some(buffer) = editor.buffer().read(cx).as_singleton() {
|
||||
if buffer.read(cx).file().is_some() {
|
||||
zeta.update(cx, |zeta, cx| {
|
||||
zeta.register_buffer(&buffer, cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let data_collection = ProviderDataCollection::new(
|
||||
zeta.clone(),
|
||||
window.root::<Workspace>().flatten(),
|
||||
singleton_buffer,
|
||||
cx,
|
||||
);
|
||||
|
||||
let provider =
|
||||
cx.new(|_| zeta::ZetaInlineCompletionProvider::new(zeta, data_collection));
|
||||
|
||||
let provider = cx.new(|_| zeta::ZetaInlineCompletionProvider::new(zeta));
|
||||
editor.set_inline_completion_provider(Some(provider), window, cx);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,20 +77,6 @@ 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,168 +0,0 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
mod banner;
|
||||
mod modal;
|
||||
|
||||
pub use banner::ZedPredictBanner;
|
||||
pub use modal::ZedPredictModal;
|
||||
@@ -1,313 +0,0 @@
|
||||
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,5 +1,5 @@
|
||||
[package]
|
||||
name = "zed_predict_onboarding"
|
||||
name = "zed_predict_tos"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
@@ -9,23 +9,15 @@ license = "GPL-3.0-or-later"
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/lib.rs"
|
||||
path = "src/zed_predict_tos.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
|
||||
language.workspace = true
|
||||
menu.workspace = true
|
||||
settings.workspace = true
|
||||
theme.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
workspace.workspace = true
|
||||
menu.workspace = true
|
||||
155
crates/zed_predict_tos/src/zed_predict_tos.rs
Normal file
155
crates/zed_predict_tos/src/zed_predict_tos.rs
Normal file
@@ -0,0 +1,155 @@
|
||||
//! 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,7 +22,6 @@ 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
|
||||
@@ -35,7 +34,6 @@ 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
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -382,16 +382,11 @@ There are two options to choose from:
|
||||
- Default:
|
||||
|
||||
```json
|
||||
"inline_completions": {
|
||||
"disabled_globs": [
|
||||
"**/.env*",
|
||||
"**/*.pem",
|
||||
"**/*.key",
|
||||
"**/*.cert",
|
||||
"**/*.crt",
|
||||
"**/secrets.yml"
|
||||
]
|
||||
}
|
||||
"inline_completions": {
|
||||
"disabled_globs": [
|
||||
".env"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Options**
|
||||
|
||||
Reference in New Issue
Block a user