Compare commits

..

1 Commits

Author SHA1 Message Date
Conrad Irwin
111583b863 WIP: enforce buffer ordering in multibuffer 2025-01-29 22:44:17 -07:00
90 changed files with 1532 additions and 2710 deletions

View 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 -->

View File

@@ -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

View File

@@ -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
View File

@@ -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",

View File

@@ -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" }
#

View File

@@ -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

View File

@@ -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

View File

@@ -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"
}
}
]

View File

@@ -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"

View File

@@ -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",

View File

@@ -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": {

View File

@@ -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"))

View File

@@ -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(())
}

View File

@@ -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,

View File

@@ -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,
}

View File

@@ -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 {

View File

@@ -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(),

View File

@@ -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

View File

@@ -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"]);

View File

@@ -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()),
}

View File

@@ -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 {

View File

@@ -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(

View File

@@ -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]

View File

@@ -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);

View File

@@ -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);

View File

@@ -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,
}))
})
});

View File

@@ -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);
}

View File

@@ -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,

View File

@@ -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 {

View File

@@ -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>,

View File

@@ -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();
}
}

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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

View File

@@ -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)
})
}
}

View File

@@ -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

View File

@@ -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)?;

View File

@@ -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);

View File

@@ -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 {

View File

@@ -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>,

View File

@@ -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"] }

View File

@@ -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();
}

View File

@@ -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.
///

View File

@@ -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")

View File

@@ -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<_>>();

View File

@@ -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)
}

View File

@@ -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)

View File

@@ -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),

View File

@@ -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>,

View File

@@ -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()
}

View File

@@ -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,

View File

@@ -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,
}
}

View File

@@ -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();
})
}));
}

View File

@@ -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)]

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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)

View File

@@ -1130,6 +1130,7 @@ impl Render for TerminalView {
let focused = self.focus_handle.is_focused(window);
div()
.occlude()
.id("terminal-view")
.size_full()
.relative()

View File

@@ -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.);

View File

@@ -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(),
},
)
})

View File

@@ -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>,

View File

@@ -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(),

View File

@@ -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(),

View File

@@ -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(),
)),
)
}
}

View File

@@ -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>,

View File

@@ -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

View File

@@ -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,
}
}

View File

@@ -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

View File

@@ -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)),
),
)
}

View File

@@ -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>,
},

View File

@@ -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(&notification, {
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(&notification, {
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();

View File

@@ -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,

View File

@@ -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
}
}
}
}
}

View File

@@ -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

View File

@@ -1 +1 @@
stable
dev

View File

@@ -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();

View File

@@ -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>,

View File

@@ -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);
}
}

View File

@@ -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;

View File

@@ -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)
}
}

View File

@@ -1,5 +0,0 @@
mod banner;
mod modal;
pub use banner::ZedPredictBanner;
pub use modal::ZedPredictModal;

View File

@@ -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),
)
}
}
}

View File

@@ -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

View 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);
})),
),
)
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -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**