Compare commits

..

6 Commits

Author SHA1 Message Date
Smit Barmase
ccb14bb926 add test for window focus on startup 2025-12-04 15:15:17 +05:30
Conrad Irwin
1e4d80a21f Update fancy-regex (#44120)
Fancy regex has a max backtracking limit which defaults to 1,000,000
backtracks. This avoids spinning the CPU forever in the case that a
match is taking a long time (though does mean that some matches may be
missed).

Unfortunately the verison we depended on causes an infinite loop when
the backtracking limit is hit
(https://github.com/fancy-regex/fancy-regex/issues/137), so we got the
worse of both worlds: matches were missed *and* we spun the CPU forever.

Updating fixes this.

Excitingly regex may gain support for lookarounds
(https://github.com/rust-lang/regex/pull/1315), which will make
fancy-regex much less load bearing.

Closes #43821

Release Notes:

- Fix a bug where search regexes with look-around or backreferences
could hang
  the CPU. They will now abort after a certain number of match attempts.
2025-12-04 07:29:31 +00:00
Joseph T. Lyons
f90d9d26a5 Prefer to disable options over hiding (git panel entry context menu) (#44102)
When adding the File History option here, I used the pattern to hide the
option, since that's what another option was already doing here, but I
see other menus in the git panel (`...`) that use disabling over hiding,
which is what I think is a nicer experience (allows you to learn of
actions, the full range of actions is always visible, don't have to
worry about how multiple hidden items might interact in various
configurations, etc).

<img width="336" height="241" alt="SCR-20251203-pnpy"
src="https://github.com/user-attachments/assets/0da90b9a-c230-4ce3-87b9-553ffb83604f"
/>

<img width="332" height="265" alt="SCR-20251203-pobg"
src="https://github.com/user-attachments/assets/5da95c7d-faa9-4f0f-a069-f1d099f952b9"
/>


In general, I think it would be good to move to being more consistent
with disabling over hiding - there are other places in the app that are
hiding - some might be valid, but others might just choices made on a
whim.

Release Notes:

- N/A
2025-12-03 22:56:51 +00:00
Andrew Farkas
40a611bf34 tab_switcher: Subscribe to workspace events instead of pane events (#44101)
Closes #43171

Previously the tab switcher only subscribed to events from a single pane
so closing tabs in other panes wouldn't cause the tab switcher to
update. This PR changes that so the tab switcher subscribes to the whole
workspace and thus updates when tabs in other panes are closed.

It also modifies the work in #44006 to sync selected index across the
whole workspace instead of just the original pane in the case of the
all-panes tab switcher.

Release Notes:

- Fixed all-panes tab switcher not updating in response to changes in
other panes
2025-12-03 22:49:44 +00:00
Smit Barmase
8ad3a150c8 editor: Add active match highlight for buffer and project search (#44098)
Closes #28617

<img width="400" alt="image"
src="https://github.com/user-attachments/assets/b1c2880c-5744-4bed-a687-5c5e7aa7fef5"
/>

Release Notes:

- Improved visibility of the currently active match when browsing
results in buffer or project search.

---------

Co-authored-by: DarkMatter-999 <darkmatter999official@gmail.com>
2025-12-04 03:55:04 +05:30
Andrew Farkas
87976e91cf Add more preview tab settings and fix janky behavior (#43921)
Closes #41495

Known issues:
- File path links always open as non-preview tabs. Fixing this is not
technically too difficult but requires more invasive changes and so
should be done in a future PR.

Release Notes:

- Fixed strange behavior when reopening closed preview tabs
- Overhauled preview tabs settings:
- Added setting `preview_tabs.enable_preview_from_project_panel`
(default `true`)
- Kept setting `preview_tabs.enable_preview_from_file_finder` (default
`false`)
- Added setting `preview_tabs.enable_preview_from_multibuffer` (default
`true`)
- Added setting
`preview_tabs.enable_preview_multibuffer_from_code_navigation` (default
`false`)
- Added setting `preview_tabs.enable_preview_file_from_code_navigation`
(default `true`)
- Renamed setting `preview_tabs.enable_preview_from_code_navigation` to
`preview_tabs.enable_keep_preview_on_code_navigation` (default `false`)

---------

Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>
Co-authored-by: Cole Miller <cole@zed.dev>
2025-12-03 21:56:39 +00:00
51 changed files with 1000 additions and 1110 deletions

88
Cargo.lock generated
View File

@@ -2130,30 +2130,15 @@ dependencies = [
"syn 2.0.106",
]
[[package]]
name = "bit-set"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1"
dependencies = [
"bit-vec 0.6.3",
]
[[package]]
name = "bit-set"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3"
dependencies = [
"bit-vec 0.8.0",
"bit-vec",
]
[[package]]
name = "bit-vec"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb"
[[package]]
name = "bit-vec"
version = "0.8.0"
@@ -2332,9 +2317,9 @@ dependencies = [
[[package]]
name = "borrow-or-share"
version = "0.2.2"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3eeab4423108c5d7c744f4d234de88d18d636100093ae04caf4825134b9c3a32"
checksum = "dc0b364ead1874514c8c2855ab558056ebfeb775653e7ae45ff72f28f8f3166c"
[[package]]
name = "borsh"
@@ -6008,22 +5993,11 @@ checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
[[package]]
name = "fancy-regex"
version = "0.13.0"
version = "0.16.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "531e46835a22af56d1e3b66f04844bed63158bc094a628bec1d321d9b4c44bf2"
checksum = "998b056554fbe42e03ae0e152895cd1a7e1002aec800fdc6635d20270260c46f"
dependencies = [
"bit-set 0.5.3",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "fancy-regex"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e24cb5a94bcae1e5408b0effca5cd7172ea3c5755049c5f3af4cd283a165298"
dependencies = [
"bit-set 0.8.0",
"bit-set",
"regex-automata",
"regex-syntax",
]
@@ -6245,9 +6219,9 @@ checksum = "8bf7cc16383c4b8d58b9905a8509f02926ce3058053c056376248d958c9df1e8"
[[package]]
name = "fluent-uri"
version = "0.3.2"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1918b65d96df47d3591bed19c5cca17e3fa5d0707318e4b5ef2eae01764df7e5"
checksum = "bc74ac4d8359ae70623506d512209619e5cf8f347124910440dbc221714b328e"
dependencies = [
"borrow-or-share",
"ref-cast",
@@ -7543,6 +7517,17 @@ dependencies = [
"serde",
]
[[package]]
name = "hashbrown"
version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
dependencies = [
"allocator-api2",
"equivalent",
"foldhash 0.2.0",
]
[[package]]
name = "hashlink"
version = "0.8.4"
@@ -8632,21 +8617,21 @@ dependencies = [
[[package]]
name = "jsonschema"
version = "0.30.0"
version = "0.37.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1b46a0365a611fbf1d2143104dcf910aada96fafd295bab16c60b802bf6fa1d"
checksum = "73c9ffb2b5c56d58030e1b532d8e8389da94590515f118cf35b5cb68e4764a7e"
dependencies = [
"ahash 0.8.12",
"base64 0.22.1",
"bytecount",
"data-encoding",
"email_address",
"fancy-regex 0.14.0",
"fancy-regex",
"fraction",
"getrandom 0.3.4",
"idna",
"itoa",
"num-cmp",
"num-traits",
"once_cell",
"percent-encoding",
"referencing",
"regex",
@@ -8654,6 +8639,7 @@ dependencies = [
"reqwest 0.12.24",
"serde",
"serde_json",
"unicode-general-category",
"uuid-simd",
]
@@ -10202,7 +10188,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b977c445f26e49757f9aca3631c3b8b836942cb278d69a92e7b80d3b24da632"
dependencies = [
"arrayvec",
"bit-set 0.8.0",
"bit-set",
"bitflags 2.9.4",
"cfg_aliases 0.2.1",
"codespan-reporting 0.12.0",
@@ -13058,7 +13044,7 @@ dependencies = [
"dap",
"dap_adapters",
"extension",
"fancy-regex 0.14.0",
"fancy-regex",
"fs",
"futures 0.3.31",
"fuzzy",
@@ -13929,13 +13915,14 @@ dependencies = [
[[package]]
name = "referencing"
version = "0.30.0"
version = "0.37.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8eff4fa778b5c2a57e85c5f2fe3a709c52f0e60d23146e2151cbef5893f420e"
checksum = "4283168a506f0dcbdce31c9f9cce3129c924da4c6bca46e46707fcb746d2d70c"
dependencies = [
"ahash 0.8.12",
"fluent-uri",
"once_cell",
"getrandom 0.3.4",
"hashbrown 0.16.1",
"parking_lot",
"percent-encoding",
"serde_json",
@@ -17129,7 +17116,7 @@ dependencies = [
"alacritty_terminal",
"anyhow",
"collections",
"fancy-regex 0.14.0",
"fancy-regex",
"futures 0.3.31",
"gpui",
"itertools 0.14.0",
@@ -17363,12 +17350,12 @@ dependencies = [
[[package]]
name = "tiktoken-rs"
version = "0.9.1"
source = "git+https://github.com/zed-industries/tiktoken-rs?rev=7249f999c5fdf9bf3cc5c288c964454e4dac0c00#7249f999c5fdf9bf3cc5c288c964454e4dac0c00"
source = "git+https://github.com/zed-industries/tiktoken-rs?rev=2570c4387a8505fb8f1d3f3557454b474f1e8271#2570c4387a8505fb8f1d3f3557454b474f1e8271"
dependencies = [
"anyhow",
"base64 0.22.1",
"bstr",
"fancy-regex 0.13.0",
"fancy-regex",
"lazy_static",
"regex",
"rustc-hash 1.1.0",
@@ -18500,6 +18487,12 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce61d488bcdc9bc8b5d1772c404828b17fc481c0a582b5581e95fb233aef503e"
[[package]]
name = "unicode-general-category"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b993bddc193ae5bd0d623b49ec06ac3e9312875fdae725a975c51db1cc1677f"
[[package]]
name = "unicode-ident"
version = "1.0.19"
@@ -18734,7 +18727,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23b082222b4f6619906941c17eb2297fff4c2fb96cb60164170522942a200bd8"
dependencies = [
"outref",
"uuid",
"vsimd",
]

View File

@@ -505,7 +505,7 @@ ec4rs = "1.1"
emojis = "0.6.1"
env_logger = "0.11"
exec = "0.3.1"
fancy-regex = "0.14.0"
fancy-regex = "0.16.0"
fork = "0.4.0"
futures = "0.3"
futures-batch = "0.6.1"
@@ -531,7 +531,7 @@ indoc = "2"
inventory = "0.3.19"
itertools = "0.14.0"
json_dotpath = "1.1"
jsonschema = "0.30.0"
jsonschema = "0.37.0"
jsonwebtoken = "9.3"
jupyter-protocol = "0.10.0"
jupyter-websocket-client = "0.15.0"
@@ -658,7 +658,7 @@ sysinfo = "0.37.0"
take-until = "0.2.0"
tempfile = "3.20.0"
thiserror = "2.0.12"
tiktoken-rs = { git = "https://github.com/zed-industries/tiktoken-rs", rev = "7249f999c5fdf9bf3cc5c288c964454e4dac0c00" }
tiktoken-rs = { git = "https://github.com/zed-industries/tiktoken-rs", rev = "2570c4387a8505fb8f1d3f3557454b474f1e8271" }
time = { version = "0.3", features = [
"macros",
"parsing",

View File

@@ -1,42 +0,0 @@
{{#if language_name}}
Here's a file of {{language_name}} that the user is going to ask you to make an edit to.
{{else}}
Here's a file of text that the user is going to ask you to make an edit to.
{{/if}}
The section you'll need to rewrite is marked with <rewrite_this></rewrite_this> tags.
<document>
{{{document_content}}}
</document>
{{#if is_truncated}}
The context around the relevant section has been truncated (possibly in the middle of a line) for brevity.
{{/if}}
{{#if rewrite_section}}
And here's the section to rewrite based on that prompt again for reference:
<rewrite_this>
{{{rewrite_section}}}
</rewrite_this>
{{#if diagnostic_errors}}
Below are the diagnostic errors visible to the user. If the user requests problems to be fixed, use this information, but do not try to fix these errors if the user hasn't asked you to.
{{#each diagnostic_errors}}
<diagnostic_error>
<line_number>{{line_number}}</line_number>
<error_message>{{error_message}}</error_message>
<code_content>{{code_content}}</code_content>
</diagnostic_error>
{{/each}}
{{/if}}
{{/if}}
Only make changes that are necessary to fulfill the prompt, leave everything else as-is. All surrounding {{content_type}} will be preserved.
Start at the indentation level in the original file in the rewritten {{content_type}}.
You must use one of the provided tools to make the rewrite or to provide an explanation as to why the user's request cannot be fulfilled.

View File

@@ -1100,13 +1100,22 @@
"preview_tabs": {
// Whether preview tabs should be enabled.
// Preview tabs allow you to open files in preview mode, where they close automatically
// when you switch to another file unless you explicitly pin them.
// when you open another preview tab.
// This is useful for quickly viewing files without cluttering your workspace.
"enabled": true,
// Whether to open tabs in preview mode when opened from the project panel with a single click.
"enable_preview_from_project_panel": true,
// Whether to open tabs in preview mode when selected from the file finder.
"enable_preview_from_file_finder": false,
// Whether a preview tab gets replaced when code navigation is used to navigate away from the tab.
"enable_preview_from_code_navigation": false
// Whether to open tabs in preview mode when opened from a multibuffer.
"enable_preview_from_multibuffer": true,
// Whether to open tabs in preview mode when code navigation is used to open a multibuffer.
"enable_preview_multibuffer_from_code_navigation": false,
// Whether to open tabs in preview mode when code navigation is used to open a single file.
"enable_preview_file_from_code_navigation": true,
// Whether to keep tabs in preview mode when code navigation is used to navigate away from them.
// If `enable_preview_file_from_code_navigation` or `enable_preview_multibuffer_from_code_navigation` is also true, the new tab may replace the existing one.
"enable_keep_preview_on_code_navigation": false
},
// Settings related to the file finder.
"file_finder": {

View File

@@ -45,6 +45,7 @@
"tab.inactive_background": "#1f2127ff",
"tab.active_background": "#0d1016ff",
"search.match_background": "#5ac2fe66",
"search.active_match_background": "#ea570166",
"panel.background": "#1f2127ff",
"panel.focused_border": "#5ac1feff",
"pane.focused_border": null,
@@ -436,6 +437,7 @@
"tab.inactive_background": "#ececedff",
"tab.active_background": "#fcfcfcff",
"search.match_background": "#3b9ee566",
"search.active_match_background": "#f88b3666",
"panel.background": "#ececedff",
"panel.focused_border": "#3b9ee5ff",
"pane.focused_border": null,
@@ -827,6 +829,7 @@
"tab.inactive_background": "#353944ff",
"tab.active_background": "#242835ff",
"search.match_background": "#73cffe66",
"search.active_match_background": "#fd722b66",
"panel.background": "#353944ff",
"panel.focused_border": null,
"pane.focused_border": null,

View File

@@ -46,6 +46,7 @@
"tab.inactive_background": "#3a3735ff",
"tab.active_background": "#282828ff",
"search.match_background": "#83a59866",
"search.active_match_background": "#c09f3f66",
"panel.background": "#3a3735ff",
"panel.focused_border": "#83a598ff",
"pane.focused_border": null,
@@ -452,6 +453,7 @@
"tab.inactive_background": "#393634ff",
"tab.active_background": "#1d2021ff",
"search.match_background": "#83a59866",
"search.active_match_background": "#c9653666",
"panel.background": "#393634ff",
"panel.focused_border": "#83a598ff",
"pane.focused_border": null,
@@ -858,6 +860,7 @@
"tab.inactive_background": "#3b3735ff",
"tab.active_background": "#32302fff",
"search.match_background": "#83a59866",
"search.active_match_background": "#aea85166",
"panel.background": "#3b3735ff",
"panel.focused_border": null,
"pane.focused_border": null,
@@ -1264,6 +1267,7 @@
"tab.inactive_background": "#ecddb4ff",
"tab.active_background": "#fbf1c7ff",
"search.match_background": "#0b667866",
"search.active_match_background": "#ba2d1166",
"panel.background": "#ecddb4ff",
"panel.focused_border": null,
"pane.focused_border": null,
@@ -1670,6 +1674,7 @@
"tab.inactive_background": "#ecddb5ff",
"tab.active_background": "#f9f5d7ff",
"search.match_background": "#0b667866",
"search.active_match_background": "#dc351466",
"panel.background": "#ecddb5ff",
"panel.focused_border": null,
"pane.focused_border": null,
@@ -2076,6 +2081,7 @@
"tab.inactive_background": "#ecdcb3ff",
"tab.active_background": "#f2e5bcff",
"search.match_background": "#0b667866",
"search.active_match_background": "#d7331466",
"panel.background": "#ecdcb3ff",
"panel.focused_border": null,
"pane.focused_border": null,

View File

@@ -45,6 +45,7 @@
"tab.inactive_background": "#2f343eff",
"tab.active_background": "#282c33ff",
"search.match_background": "#74ade866",
"search.active_match_background": "#e8af7466",
"panel.background": "#2f343eff",
"panel.focused_border": null,
"pane.focused_border": null,
@@ -448,6 +449,7 @@
"tab.inactive_background": "#ebebecff",
"tab.active_background": "#fafafaff",
"search.match_background": "#5c79e266",
"search.active_match_background": "#d0a92366",
"panel.background": "#ebebecff",
"panel.focused_border": null,
"pane.focused_border": null,

View File

@@ -4,7 +4,6 @@ mod create_directory_tool;
mod delete_path_tool;
mod diagnostics_tool;
mod edit_file_tool;
mod failure_message_tool;
mod fetch_tool;
mod find_path_tool;
mod grep_tool;
@@ -13,7 +12,6 @@ mod move_path_tool;
mod now_tool;
mod open_tool;
mod read_file_tool;
mod rewrite_section_tool;
mod terminal_tool;
mod thinking_tool;
mod web_search_tool;
@@ -27,7 +25,6 @@ pub use create_directory_tool::*;
pub use delete_path_tool::*;
pub use diagnostics_tool::*;
pub use edit_file_tool::*;
pub use failure_message_tool::*;
pub use fetch_tool::*;
pub use find_path_tool::*;
pub use grep_tool::*;
@@ -36,7 +33,6 @@ pub use move_path_tool::*;
pub use now_tool::*;
pub use open_tool::*;
pub use read_file_tool::*;
pub use rewrite_section_tool::*;
pub use terminal_tool::*;
pub use thinking_tool::*;
pub use web_search_tool::*;

View File

@@ -1,52 +0,0 @@
//! This tool is intended for use with the inline assistant, not the agent panel.
use std::sync::Arc;
use agent_client_protocol as acp;
use anyhow::Result;
use gpui::{App, SharedString, Task};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::{AgentTool, ToolCallEventStream};
/// Use this tool to provide a message to the user when you're unable to complete a task.
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct FailureMessageInput {
/// A brief message to the user explaining why you're unable to fulfill the request.
///
/// The message may use markdown formatting if you wish.
pub message: String,
}
pub struct FailureMessageTool;
impl AgentTool for FailureMessageTool {
type Input = FailureMessageInput;
type Output = String;
fn name() -> &'static str {
"failure_message"
}
fn kind() -> acp::ToolKind {
acp::ToolKind::Think
}
fn initial_title(
&self,
_input: Result<Self::Input, serde_json::Value>,
_cx: &mut App,
) -> SharedString {
"".into()
}
fn run(
self: Arc<Self>,
_input: Self::Input,
_event_stream: ToolCallEventStream,
_cx: &mut App,
) -> Task<Result<String>> {
unimplemented!("This function is not used by the inline assistant")
}
}

View File

@@ -1,56 +0,0 @@
//! This tool is intended for use with the inline assistant, not the agent panel.
use std::sync::Arc;
use agent_client_protocol as acp;
use anyhow::Result;
use gpui::{App, SharedString, Task};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::{AgentTool, ToolCallEventStream};
/// Replaces text in <rewrite_this></rewrite_this> tags with your replacement_text.
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct RewriteSectionInput {
/// A brief description of the edit you have made.
///
/// The description may use markdown formatting if you wish.
/// This is optional - if the edit is simple or obvious, you should leave it empty.
pub description: String,
/// The text to replace the section with.
pub replacement_text: String,
}
pub struct RewriteSectionTool;
impl AgentTool for RewriteSectionTool {
type Input = RewriteSectionInput;
type Output = String;
fn name() -> &'static str {
"rewrite_section"
}
fn kind() -> acp::ToolKind {
acp::ToolKind::Edit
}
fn initial_title(
&self,
_input: Result<Self::Input, serde_json::Value>,
_cx: &mut App,
) -> SharedString {
"".into()
}
fn run(
self: Arc<Self>,
_input: Self::Input,
_event_stream: ToolCallEventStream,
_cx: &mut App,
) -> Task<Result<String>> {
unimplemented!("This function is not used by the inline assistant")
}
}

View File

@@ -98,7 +98,7 @@ impl Render for AgentModelSelector {
.child(
Icon::new(IconName::ChevronDown)
.color(color)
.size(IconSize::Small),
.size(IconSize::XSmall),
),
move |_window, cx| {
Tooltip::for_action_in("Change Model", &ToggleModelSelector, &focus_handle, cx)

View File

@@ -1,27 +1,21 @@
use crate::{context::LoadedContext, inline_prompt_editor::CodegenStatus};
use agent::{
AgentTool as _, FailureMessageInput, FailureMessageTool, RewriteSectionInput,
RewriteSectionTool,
};
use agent_settings::AgentSettings;
use anyhow::{Context as _, Result};
use client::telemetry::Telemetry;
use cloud_llm_client::CompletionIntent;
use collections::HashSet;
use editor::{Anchor, AnchorRangeExt, MultiBuffer, MultiBufferSnapshot, ToOffset as _, ToPoint};
use feature_flags::{FeatureFlagAppExt as _, InlineAssistantV2FeatureFlag};
use futures::{
SinkExt, Stream, StreamExt, TryStreamExt as _,
channel::mpsc,
future::{LocalBoxFuture, Shared},
join,
};
use gpui::{App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, Subscription, Task};
use gpui::{App, AppContext as _, Context, Entity, EventEmitter, Subscription, Task};
use language::{Buffer, IndentKind, Point, TransactionId, line_diff};
use language_model::{
LanguageModel, LanguageModelCompletionError, LanguageModelRegistry, LanguageModelRequest,
LanguageModelRequestMessage, LanguageModelRequestTool, LanguageModelTextStream, Role,
report_assistant_event,
LanguageModel, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage,
LanguageModelTextStream, Role, report_assistant_event,
};
use multi_buffer::MultiBufferRow;
use parking_lot::Mutex;
@@ -40,7 +34,6 @@ use std::{
};
use streaming_diff::{CharOperation, LineDiff, LineOperation, StreamingDiff};
use telemetry_events::{AssistantEventData, AssistantKind, AssistantPhase};
use ui::SharedString;
pub struct BufferCodegen {
alternatives: Vec<Entity<CodegenAlternative>>,
@@ -221,10 +214,6 @@ impl BufferCodegen {
pub fn last_equal_ranges<'a>(&self, cx: &'a App) -> &'a [Range<Anchor>] {
self.active_alternative().read(cx).last_equal_ranges()
}
pub fn model_explanation<'a>(&self, cx: &'a App) -> Option<SharedString> {
self.active_alternative().read(cx).model_explanation.clone()
}
}
impl EventEmitter<CodegenEvent> for BufferCodegen {}
@@ -249,7 +238,6 @@ pub struct CodegenAlternative {
elapsed_time: Option<f64>,
completion: Option<String>,
pub message_id: Option<String>,
pub model_explanation: Option<SharedString>,
}
impl EventEmitter<CodegenEvent> for CodegenAlternative {}
@@ -300,15 +288,14 @@ impl CodegenAlternative {
generation: Task::ready(()),
diff: Diff::default(),
telemetry,
_subscription: cx.subscribe(&buffer, Self::handle_buffer_event),
builder,
active: active,
active,
edits: Vec::new(),
line_operations: Vec::new(),
range,
elapsed_time: None,
completion: None,
model_explanation: None,
_subscription: cx.subscribe(&buffer, Self::handle_buffer_event),
}
}
@@ -371,125 +358,18 @@ impl CodegenAlternative {
let api_key = model.api_key(cx);
let telemetry_id = model.telemetry_id();
let provider_id = model.provider_id();
if cx.has_flag::<InlineAssistantV2FeatureFlag>() {
let request = self.build_request(&model, user_prompt, context_task, cx)?;
let tool_use = cx.spawn(async move |_, cx| {
Ok(model.stream_completion_tool(request.await, cx).await?)
});
self.handle_tool_use(telemetry_id, provider_id.to_string(), api_key, tool_use, cx);
} else {
let stream: LocalBoxFuture<Result<LanguageModelTextStream>> =
if user_prompt.trim().to_lowercase() == "delete" {
async { Ok(LanguageModelTextStream::default()) }.boxed_local()
} else {
let request = self.build_request(&model, user_prompt, context_task, cx)?;
cx.spawn(async move |_, cx| {
Ok(model.stream_completion_text(request.await, cx).await?)
})
.boxed_local()
};
self.handle_stream(telemetry_id, provider_id.to_string(), api_key, stream, cx);
}
Ok(())
}
fn build_request_v2(
&self,
model: &Arc<dyn LanguageModel>,
user_prompt: String,
context_task: Shared<Task<Option<LoadedContext>>>,
cx: &mut App,
) -> Result<Task<LanguageModelRequest>> {
let buffer = self.buffer.read(cx).snapshot(cx);
let language = buffer.language_at(self.range.start);
let language_name = if let Some(language) = language.as_ref() {
if Arc::ptr_eq(language, &language::PLAIN_TEXT) {
None
let stream: LocalBoxFuture<Result<LanguageModelTextStream>> =
if user_prompt.trim().to_lowercase() == "delete" {
async { Ok(LanguageModelTextStream::default()) }.boxed_local()
} else {
Some(language.name())
}
} else {
None
};
let language_name = language_name.as_ref();
let start = buffer.point_to_buffer_offset(self.range.start);
let end = buffer.point_to_buffer_offset(self.range.end);
let (buffer, range) = if let Some((start, end)) = start.zip(end) {
let (start_buffer, start_buffer_offset) = start;
let (end_buffer, end_buffer_offset) = end;
if start_buffer.remote_id() == end_buffer.remote_id() {
(start_buffer.clone(), start_buffer_offset..end_buffer_offset)
} else {
anyhow::bail!("invalid transformation range");
}
} else {
anyhow::bail!("invalid transformation range");
};
let system_prompt = self
.builder
.generate_inline_transformation_prompt_v2(
language_name,
buffer,
range.start.0..range.end.0,
)
.context("generating content prompt")?;
let temperature = AgentSettings::temperature_for_model(model, cx);
let tool_input_format = model.tool_input_format();
Ok(cx.spawn(async move |_cx| {
let mut messages = vec![LanguageModelRequestMessage {
role: Role::System,
content: vec![system_prompt.into()],
cache: false,
reasoning_details: None,
}];
let mut user_message = LanguageModelRequestMessage {
role: Role::User,
content: Vec::new(),
cache: false,
reasoning_details: None,
let request = self.build_request(&model, user_prompt, context_task, cx)?;
cx.spawn(async move |_, cx| {
Ok(model.stream_completion_text(request.await, cx).await?)
})
.boxed_local()
};
if let Some(context) = context_task.await {
context.add_to_request_message(&mut user_message);
}
user_message.content.push(user_prompt.into());
messages.push(user_message);
let tools = vec![
LanguageModelRequestTool {
name: RewriteSectionTool::name().to_string(),
description: RewriteSectionTool::description().to_string(),
input_schema: RewriteSectionTool::input_schema(tool_input_format).to_value(),
},
LanguageModelRequestTool {
name: FailureMessageTool::name().to_string(),
description: FailureMessageTool::description().to_string(),
input_schema: FailureMessageTool::input_schema(tool_input_format).to_value(),
},
];
LanguageModelRequest {
thread_id: None,
prompt_id: None,
intent: Some(CompletionIntent::InlineAssist),
mode: None,
tools,
tool_choice: None,
stop: Vec::new(),
temperature,
messages,
thinking_allowed: false,
}
}))
self.handle_stream(telemetry_id, provider_id.to_string(), api_key, stream, cx);
Ok(())
}
fn build_request(
@@ -499,10 +379,6 @@ impl CodegenAlternative {
context_task: Shared<Task<Option<LoadedContext>>>,
cx: &mut App,
) -> Result<Task<LanguageModelRequest>> {
if cx.has_flag::<InlineAssistantV2FeatureFlag>() {
return self.build_request_v2(model, user_prompt, context_task, cx);
}
let buffer = self.buffer.read(cx).snapshot(cx);
let language = buffer.language_at(self.range.start);
let language_name = if let Some(language) = language.as_ref() {
@@ -634,7 +510,6 @@ impl CodegenAlternative {
self.generation = cx.spawn(async move |codegen, cx| {
let stream = stream.await;
let token_usage = stream
.as_ref()
.ok()
@@ -1024,101 +899,6 @@ impl CodegenAlternative {
.ok();
})
}
fn handle_tool_use(
&mut self,
_telemetry_id: String,
_provider_id: String,
_api_key: Option<String>,
tool_use: impl 'static
+ Future<
Output = Result<language_model::LanguageModelToolUse, LanguageModelCompletionError>,
>,
cx: &mut Context<Self>,
) {
self.diff = Diff::default();
self.status = CodegenStatus::Pending;
self.generation = cx.spawn(async move |codegen, cx| {
let finish_with_status = |status: CodegenStatus, cx: &mut AsyncApp| {
let _ = codegen.update(cx, |this, cx| {
this.status = status;
cx.emit(CodegenEvent::Finished);
cx.notify();
});
};
let tool_use = tool_use.await;
match tool_use {
Ok(tool_use) if tool_use.name.as_ref() == "rewrite_section" => {
// Parse the input JSON into RewriteSectionInput
match serde_json::from_value::<RewriteSectionInput>(tool_use.input) {
Ok(input) => {
// Store the description if non-empty
let description = if !input.description.trim().is_empty() {
Some(input.description.clone())
} else {
None
};
// Apply the replacement text to the buffer and compute diff
let batch_diff_task = codegen
.update(cx, |this, cx| {
this.model_explanation = description.map(Into::into);
let range = this.range.clone();
this.apply_edits(
std::iter::once((range, input.replacement_text)),
cx,
);
this.reapply_batch_diff(cx)
})
.ok();
// Wait for the diff computation to complete
if let Some(diff_task) = batch_diff_task {
diff_task.await;
}
finish_with_status(CodegenStatus::Done, cx);
return;
}
Err(e) => {
finish_with_status(CodegenStatus::Error(e.into()), cx);
return;
}
}
}
Ok(tool_use) if tool_use.name.as_ref() == "failure_message" => {
// Handle failure message tool use
match serde_json::from_value::<FailureMessageInput>(tool_use.input) {
Ok(input) => {
let _ = codegen.update(cx, |this, _cx| {
// Store the failure message as the tool description
this.model_explanation = Some(input.message.into());
});
finish_with_status(CodegenStatus::Done, cx);
return;
}
Err(e) => {
finish_with_status(CodegenStatus::Error(e.into()), cx);
return;
}
}
}
Ok(_tool_use) => {
// Unexpected tool.
finish_with_status(CodegenStatus::Done, cx);
return;
}
Err(e) => {
finish_with_status(CodegenStatus::Error(e.into()), cx);
return;
}
}
});
cx.notify();
}
}
#[derive(Copy, Clone, Debug)]

View File

@@ -387,9 +387,17 @@ impl InlineAssistant {
let mut selections = Vec::<Selection<Point>>::new();
let mut newest_selection = None;
for mut selection in initial_selections {
if selection.end == selection.start
&& let Some(fold) =
snapshot.crease_for_buffer_row(MultiBufferRow(selection.end.row))
if selection.end > selection.start {
selection.start.column = 0;
// If the selection ends at the start of the line, we don't want to include it.
if selection.end.column == 0 {
selection.end.row -= 1;
}
selection.end.column = snapshot
.buffer_snapshot()
.line_len(MultiBufferRow(selection.end.row));
} else if let Some(fold) =
snapshot.crease_for_buffer_row(MultiBufferRow(selection.end.row))
{
selection.start = fold.range().start;
selection.end = fold.range().end;
@@ -416,15 +424,6 @@ impl InlineAssistant {
}
}
}
} else {
selection.start.column = 0;
// If the selection ends at the start of the line, we don't want to include it.
if selection.end.column == 0 && selection.start.row != selection.end.row {
selection.end.row -= 1;
}
selection.end.column = snapshot
.buffer_snapshot()
.line_len(MultiBufferRow(selection.end.row));
}
if let Some(prev_selection) = selections.last_mut()
@@ -545,15 +544,14 @@ impl InlineAssistant {
}
}
let [prompt_block_id, tool_description_block_id, end_block_id] =
self.insert_assist_blocks(&editor, &range, &prompt_editor, cx);
let [prompt_block_id, end_block_id] =
self.insert_assist_blocks(editor, &range, &prompt_editor, cx);
assists.push((
assist_id,
range.clone(),
prompt_editor,
prompt_block_id,
tool_description_block_id,
end_block_id,
));
}
@@ -572,15 +570,7 @@ impl InlineAssistant {
};
let mut assist_group = InlineAssistGroup::new();
for (
assist_id,
range,
prompt_editor,
prompt_block_id,
tool_description_block_id,
end_block_id,
) in assists
{
for (assist_id, range, prompt_editor, prompt_block_id, end_block_id) in assists {
let codegen = prompt_editor.read(cx).codegen().clone();
self.assists.insert(
@@ -591,7 +581,6 @@ impl InlineAssistant {
editor,
&prompt_editor,
prompt_block_id,
tool_description_block_id,
end_block_id,
range,
codegen,
@@ -700,7 +689,7 @@ impl InlineAssistant {
range: &Range<Anchor>,
prompt_editor: &Entity<PromptEditor<BufferCodegen>>,
cx: &mut App,
) -> [CustomBlockId; 3] {
) -> [CustomBlockId; 2] {
let prompt_editor_height = prompt_editor.update(cx, |prompt_editor, cx| {
prompt_editor
.editor
@@ -714,14 +703,6 @@ impl InlineAssistant {
render: build_assist_editor_renderer(prompt_editor),
priority: 0,
},
// Placeholder for tool description - will be updated dynamically
BlockProperties {
style: BlockStyle::Flex,
placement: BlockPlacement::Below(range.end),
height: Some(0),
render: Arc::new(|_cx| div().into_any_element()),
priority: 0,
},
BlockProperties {
style: BlockStyle::Sticky,
placement: BlockPlacement::Below(range.end),
@@ -740,7 +721,7 @@ impl InlineAssistant {
editor.update(cx, |editor, cx| {
let block_ids = editor.insert_blocks(assist_blocks, None, cx);
[block_ids[0], block_ids[1], block_ids[2]]
[block_ids[0], block_ids[1]]
})
}
@@ -1132,9 +1113,6 @@ impl InlineAssistant {
let mut to_remove = decorations.removed_line_block_ids;
to_remove.insert(decorations.prompt_block_id);
to_remove.insert(decorations.end_block_id);
if let Some(tool_description_block_id) = decorations.model_explanation {
to_remove.insert(tool_description_block_id);
}
editor.remove_blocks(to_remove, None, cx);
});
@@ -1455,60 +1433,8 @@ impl InlineAssistant {
let old_snapshot = codegen.snapshot(cx);
let old_buffer = codegen.old_buffer(cx);
let deleted_row_ranges = codegen.diff(cx).deleted_row_ranges.clone();
// let model_explanation = codegen.model_explanation(cx);
editor.update(cx, |editor, cx| {
// Update tool description block
// if let Some(description) = model_explanation {
// if let Some(block_id) = decorations.model_explanation {
// editor.remove_blocks(HashSet::from_iter([block_id]), None, cx);
// let new_block_id = editor.insert_blocks(
// [BlockProperties {
// style: BlockStyle::Flex,
// placement: BlockPlacement::Below(assist.range.end),
// height: Some(1),
// render: Arc::new({
// let description = description.clone();
// move |cx| {
// div()
// .w_full()
// .py_1()
// .px_2()
// .bg(cx.theme().colors().editor_background)
// .border_y_1()
// .border_color(cx.theme().status().info_border)
// .child(
// Label::new(description.clone())
// .color(Color::Muted)
// .size(LabelSize::Small),
// )
// .into_any_element()
// }
// }),
// priority: 0,
// }],
// None,
// cx,
// );
// decorations.model_explanation = new_block_id.into_iter().next();
// }
// } else if let Some(block_id) = decorations.model_explanation {
// // Hide the block if there's no description
// editor.remove_blocks(HashSet::from_iter([block_id]), None, cx);
// let new_block_id = editor.insert_blocks(
// [BlockProperties {
// style: BlockStyle::Flex,
// placement: BlockPlacement::Below(assist.range.end),
// height: Some(0),
// render: Arc::new(|_cx| div().into_any_element()),
// priority: 0,
// }],
// None,
// cx,
// );
// decorations.model_explanation = new_block_id.into_iter().next();
// }
let old_blocks = mem::take(&mut decorations.removed_line_block_ids);
editor.remove_blocks(old_blocks, None, cx);
@@ -1760,7 +1686,6 @@ impl InlineAssist {
editor: &Entity<Editor>,
prompt_editor: &Entity<PromptEditor<BufferCodegen>>,
prompt_block_id: CustomBlockId,
tool_description_block_id: CustomBlockId,
end_block_id: CustomBlockId,
range: Range<Anchor>,
codegen: Entity<BufferCodegen>,
@@ -1775,8 +1700,7 @@ impl InlineAssist {
decorations: Some(InlineAssistDecorations {
prompt_block_id,
prompt_editor: prompt_editor.clone(),
removed_line_block_ids: Default::default(),
model_explanation: Some(tool_description_block_id),
removed_line_block_ids: HashSet::default(),
end_block_id,
}),
range,
@@ -1880,7 +1804,6 @@ struct InlineAssistDecorations {
prompt_block_id: CustomBlockId,
prompt_editor: Entity<PromptEditor<BufferCodegen>>,
removed_line_block_ids: HashSet<CustomBlockId>,
model_explanation: Option<CustomBlockId>,
end_block_id: CustomBlockId,
}

View File

@@ -11,10 +11,9 @@ use editor::{
use fs::Fs;
use gpui::{
AnyElement, App, Context, CursorStyle, Entity, EventEmitter, FocusHandle, Focusable,
Subscription, TextStyle, TextStyleRefinement, WeakEntity, Window,
Subscription, TextStyle, WeakEntity, Window,
};
use language_model::{LanguageModel, LanguageModelRegistry};
use markdown::{HeadingLevelStyles, Markdown, MarkdownElement, MarkdownStyle};
use parking_lot::Mutex;
use project::Project;
use prompt_store::PromptStore;
@@ -66,7 +65,7 @@ impl<T: 'static> Render for PromptEditor<T> {
const RIGHT_PADDING: Pixels = px(9.);
let (left_gutter_width, right_padding, explanation) = match &self.mode {
let (left_gutter_width, right_padding) = match &self.mode {
PromptEditorMode::Buffer {
id: _,
codegen,
@@ -84,17 +83,11 @@ impl<T: 'static> Render for PromptEditor<T> {
let left_gutter_width = gutter.full_width() + (gutter.margin / 2.0);
let right_padding = editor_margins.right + RIGHT_PADDING;
let explanation = codegen
.active_alternative()
.read(cx)
.model_explanation
.clone();
(left_gutter_width, right_padding, explanation)
(left_gutter_width, right_padding)
}
PromptEditorMode::Terminal { .. } => {
// Give the equivalent of the same left-padding that we're using on the right
(Pixels::from(40.0), Pixels::from(24.), None)
(Pixels::from(40.0), Pixels::from(24.))
}
};
@@ -118,30 +111,18 @@ impl<T: 'static> Render for PromptEditor<T> {
this.trigger_completion_menu(window, cx);
}));
let markdown = window.use_state(cx, |_, cx| Markdown::new("".into(), None, None, cx));
if let Some(explanation) = &explanation {
markdown.update(cx, |markdown, cx| {
markdown.reset(explanation.clone(), cx);
});
}
let explanation_label = self
.render_markdown(markdown, markdown_style(window, cx))
.into_any_element();
v_flex()
.key_context("PromptEditor")
.capture_action(cx.listener(Self::paste))
.bg(cx.theme().colors().editor_background)
.block_mouse_except_scroll()
.gap_0p5()
.border_y_1()
.border_color(cx.theme().status().info_border)
.size_full()
.pt_0p5()
.pb(bottom_padding)
.pr(right_padding)
.bg(cx.theme().colors().editor_background)
.gap_0p5()
.border_y_1()
.border_color(cx.theme().colors().border)
.child(
h_flex()
.items_start()
@@ -158,12 +139,12 @@ impl<T: 'static> Render for PromptEditor<T> {
.capture_action(cx.listener(Self::cycle_next))
.child(
WithRemSize::new(ui_font_size)
.h_full()
.w(left_gutter_width)
.flex()
.flex_row()
.flex_shrink_0()
.items_center()
.h_full()
.w(left_gutter_width)
.justify_center()
.gap_2()
.child(self.render_close_button(cx))
@@ -196,82 +177,26 @@ impl<T: 'static> Render for PromptEditor<T> {
.flex_row()
.items_center()
.gap_1()
.child(add_context_button)
.child(self.model_selector.clone())
.children(buttons),
),
),
)
.when_some(explanation, |this, _| {
this.child(
h_flex()
.size_full()
.child(div().w(left_gutter_width + px(6.)))
.child(
div()
.size_full()
.min_w_0()
.pb_px()
.pl_1()
.flex_1()
.border_t_1()
.border_color(cx.theme().colors().border_variant)
.child(explanation_label),
),
)
})
}
}
fn markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
let theme_settings = ThemeSettings::get_global(cx);
let colors = cx.theme().colors();
let mut text_style = window.text_style();
text_style.refine(&TextStyleRefinement {
font_family: Some(theme_settings.ui_font.family.clone()),
color: Some(colors.text),
..Default::default()
});
MarkdownStyle {
base_text_style: text_style.clone(),
syntax: cx.theme().syntax().clone(),
selection_background_color: colors.element_selection_background,
heading_level_styles: Some(HeadingLevelStyles {
h1: Some(TextStyleRefinement {
font_size: Some(rems(1.15).into()),
..Default::default()
}),
h2: Some(TextStyleRefinement {
font_size: Some(rems(1.1).into()),
..Default::default()
}),
h3: Some(TextStyleRefinement {
font_size: Some(rems(1.05).into()),
..Default::default()
}),
h4: Some(TextStyleRefinement {
font_size: Some(rems(1.).into()),
..Default::default()
}),
h5: Some(TextStyleRefinement {
font_size: Some(rems(0.95).into()),
..Default::default()
}),
h6: Some(TextStyleRefinement {
font_size: Some(rems(0.875).into()),
..Default::default()
}),
}),
inline_code: TextStyleRefinement {
font_family: Some(theme_settings.buffer_font.family.clone()),
font_fallbacks: theme_settings.buffer_font.fallbacks.clone(),
font_features: Some(theme_settings.buffer_font.features.clone()),
background_color: Some(colors.editor_foreground.opacity(0.08)),
..Default::default()
},
..Default::default()
.child(
WithRemSize::new(ui_font_size)
.flex()
.flex_row()
.items_center()
.child(h_flex().flex_shrink_0().w(left_gutter_width))
.child(
h_flex()
.w_full()
.pl_1()
.items_start()
.justify_between()
.child(add_context_button)
.child(self.model_selector.clone()),
),
)
}
}
@@ -834,10 +759,6 @@ impl<T: 'static> PromptEditor<T> {
})
.into_any_element()
}
fn render_markdown(&self, markdown: Entity<Markdown>, style: MarkdownStyle) -> MarkdownElement {
MarkdownElement::new(markdown, style)
}
}
pub enum PromptEditorMode {

View File

@@ -2622,11 +2622,13 @@ impl SearchableItem for TextThreadEditor {
fn update_matches(
&mut self,
matches: &[Self::Match],
active_match_index: Option<usize>,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.editor
.update(cx, |editor, cx| editor.update_matches(matches, window, cx));
self.editor.update(cx, |editor, cx| {
editor.update_matches(matches, active_match_index, window, cx)
});
}
fn query_suggestion(&mut self, window: &mut Window, cx: &mut Context<Self>) -> String {

View File

@@ -1017,11 +1017,13 @@ impl SearchableItem for DapLogView {
fn update_matches(
&mut self,
matches: &[Self::Match],
active_match_index: Option<usize>,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.editor
.update(cx, |e, cx| e.update_matches(matches, window, cx))
self.editor.update(cx, |e, cx| {
e.update_matches(matches, active_match_index, window, cx)
})
}
fn query_suggestion(&mut self, window: &mut Window, cx: &mut Context<Self>) -> String {

View File

@@ -252,10 +252,11 @@ impl Console {
let start_offset = range.start;
let range = buffer.anchor_after(MultiBufferOffset(range.start))
..buffer.anchor_before(MultiBufferOffset(range.end));
let color_fn = color_fetcher(color);
console.highlight_background_key::<ConsoleAnsiHighlight>(
start_offset,
&[range],
color_fetcher(color),
move |_, theme| color_fn(theme),
cx,
);
}

View File

@@ -726,7 +726,10 @@ impl EditorActionId {
// type GetFieldEditorTheme = dyn Fn(&theme::Theme) -> theme::FieldEditor;
// type OverrideTextStyle = dyn Fn(&EditorStyle) -> Option<HighlightStyle>;
type BackgroundHighlight = (fn(&Theme) -> Hsla, Arc<[Range<Anchor>]>);
type BackgroundHighlight = (
Arc<dyn Fn(&usize, &Theme) -> Hsla + Send + Sync>,
Arc<[Range<Anchor>]>,
);
type GutterHighlight = (fn(&App) -> Hsla, Vec<Range<Anchor>>);
#[derive(Default)]
@@ -6610,7 +6613,7 @@ impl Editor {
editor.update(cx, |editor, cx| {
editor.highlight_background::<Self>(
&ranges_to_highlight,
|theme| theme.colors().editor_highlighted_line_background,
|_, theme| theme.colors().editor_highlighted_line_background,
cx,
);
});
@@ -7012,12 +7015,12 @@ impl Editor {
this.highlight_background::<DocumentHighlightRead>(
&read_ranges,
|theme| theme.colors().editor_document_highlight_read_background,
|_, theme| theme.colors().editor_document_highlight_read_background,
cx,
);
this.highlight_background::<DocumentHighlightWrite>(
&write_ranges,
|theme| theme.colors().editor_document_highlight_write_background,
|_, theme| theme.colors().editor_document_highlight_write_background,
cx,
);
cx.notify();
@@ -7125,7 +7128,7 @@ impl Editor {
if !match_ranges.is_empty() {
editor.highlight_background::<SelectedTextHighlight>(
&match_ranges,
|theme| theme.colors().editor_document_highlight_bracket_background,
|_, theme| theme.colors().editor_document_highlight_bracket_background,
cx,
)
}
@@ -17012,7 +17015,9 @@ impl Editor {
})
.collect();
let workspace = self.workspace();
let Some(workspace) = self.workspace() else {
return Task::ready(Ok(Navigated::No));
};
cx.spawn_in(window, async move |editor, cx| {
let locations: Vec<Location> = future::join_all(definitions)
@@ -17038,10 +17043,6 @@ impl Editor {
}
if num_locations > 1 {
let Some(workspace) = workspace else {
return Ok(Navigated::No);
};
let tab_kind = match kind {
Some(GotoDefinitionKind::Implementation) => "Implementations",
Some(GotoDefinitionKind::Symbol) | None => "Definitions",
@@ -17073,11 +17074,14 @@ impl Editor {
let opened = workspace
.update_in(cx, |workspace, window, cx| {
let allow_preview = PreviewTabsSettings::get_global(cx)
.enable_preview_multibuffer_from_code_navigation;
Self::open_locations_in_multibuffer(
workspace,
locations,
title,
split,
allow_preview,
MultibufferSelectionMode::First,
window,
cx,
@@ -17094,10 +17098,9 @@ impl Editor {
Ok(Navigated::Yes)
}
Some(Either::Right(path)) => {
let Some(workspace) = workspace else {
return Ok(Navigated::No);
};
// TODO(andrew): respect preview tab settings
// `enable_keep_preview_on_code_navigation` and
// `enable_preview_file_from_code_navigation`
workspace
.update_in(cx, |workspace, window, cx| {
workspace.open_resolved_path(path, window, cx)
@@ -17108,10 +17111,6 @@ impl Editor {
None => Ok(Navigated::No),
}
} else {
let Some(workspace) = workspace else {
return Ok(Navigated::No);
};
let (target_buffer, target_ranges) = locations.into_iter().next().unwrap();
let target_range = target_ranges.first().unwrap().clone();
@@ -17135,11 +17134,19 @@ impl Editor {
workspace.active_pane().clone()
};
let preview_tabs_settings = PreviewTabsSettings::get_global(cx);
let keep_old_preview = preview_tabs_settings
.enable_keep_preview_on_code_navigation;
let allow_new_preview = preview_tabs_settings
.enable_preview_file_from_code_navigation;
workspace.open_project_item(
pane,
target_buffer.clone(),
true,
true,
keep_old_preview,
allow_new_preview,
window,
cx,
)
@@ -17416,11 +17423,14 @@ impl Editor {
} else {
format!("References to {target}")
};
let allow_preview = PreviewTabsSettings::get_global(cx)
.enable_preview_multibuffer_from_code_navigation;
Self::open_locations_in_multibuffer(
workspace,
locations,
title,
false,
allow_preview,
MultibufferSelectionMode::First,
window,
cx,
@@ -17436,6 +17446,7 @@ impl Editor {
locations: std::collections::HashMap<Entity<Buffer>, Vec<Range<Point>>>,
title: String,
split: bool,
allow_preview: bool,
multibuffer_selection_mode: MultibufferSelectionMode,
window: &mut Window,
cx: &mut Context<Workspace>,
@@ -17483,6 +17494,7 @@ impl Editor {
.is_some_and(|it| *it == key)
})
});
let was_existing = existing.is_some();
let editor = existing.unwrap_or_else(|| {
cx.new(|cx| {
let mut editor = Editor::for_multibuffer(
@@ -17510,7 +17522,7 @@ impl Editor {
}
editor.highlight_background::<Self>(
&ranges,
|theme| theme.colors().editor_highlighted_line_background,
|_, theme| theme.colors().editor_highlighted_line_background,
cx,
);
}
@@ -17523,29 +17535,23 @@ impl Editor {
});
let item = Box::new(editor);
let item_id = item.item_id();
if split {
let pane = workspace.adjacent_pane(window, cx);
workspace.add_item(pane, item, None, true, true, window, cx);
} else if PreviewTabsSettings::get_global(cx).enable_preview_from_code_navigation {
let (preview_item_id, preview_item_idx) =
workspace.active_pane().read_with(cx, |pane, _| {
(pane.preview_item_id(), pane.preview_item_idx())
});
workspace.add_item_to_active_pane(item, preview_item_idx, true, window, cx);
if let Some(preview_item_id) = preview_item_id {
workspace.active_pane().update(cx, |pane, cx| {
pane.remove_item(preview_item_id, false, false, window, cx);
});
}
let pane = if split {
workspace.adjacent_pane(window, cx)
} else {
workspace.add_item_to_active_pane(item, None, true, window, cx);
}
workspace.active_pane().update(cx, |pane, cx| {
pane.set_preview_item_id(Some(item_id), cx);
workspace.active_pane().clone()
};
let activate_pane = split;
let mut destination_index = None;
pane.update(cx, |pane, cx| {
if allow_preview && !was_existing {
destination_index = pane.replace_preview_item_id(item.item_id(), window, cx);
}
if was_existing && !allow_preview {
pane.unpreview_item_if_preview(item.item_id());
}
pane.add_item(item, activate_pane, true, destination_index, window, cx);
});
}
@@ -20783,6 +20789,7 @@ impl Editor {
locations,
format!("Selections for '{title}'"),
false,
false,
MultibufferSelectionMode::All,
window,
cx,
@@ -20985,7 +20992,7 @@ impl Editor {
pub fn set_search_within_ranges(&mut self, ranges: &[Range<Anchor>], cx: &mut Context<Self>) {
self.highlight_background::<SearchWithinRange>(
ranges,
|colors| colors.colors().editor_document_highlight_read_background,
|_, colors| colors.colors().editor_document_highlight_read_background,
cx,
)
}
@@ -21001,12 +21008,12 @@ impl Editor {
pub fn highlight_background<T: 'static>(
&mut self,
ranges: &[Range<Anchor>],
color_fetcher: fn(&Theme) -> Hsla,
color_fetcher: impl Fn(&usize, &Theme) -> Hsla + Send + Sync + 'static,
cx: &mut Context<Self>,
) {
self.background_highlights.insert(
HighlightKey::Type(TypeId::of::<T>()),
(color_fetcher, Arc::from(ranges)),
(Arc::new(color_fetcher), Arc::from(ranges)),
);
self.scrollbar_marker_state.dirty = true;
cx.notify();
@@ -21016,12 +21023,12 @@ impl Editor {
&mut self,
key: usize,
ranges: &[Range<Anchor>],
color_fetcher: fn(&Theme) -> Hsla,
color_fetcher: impl Fn(&usize, &Theme) -> Hsla + Send + Sync + 'static,
cx: &mut Context<Self>,
) {
self.background_highlights.insert(
HighlightKey::TypePlus(TypeId::of::<T>(), key),
(color_fetcher, Arc::from(ranges)),
(Arc::new(color_fetcher), Arc::from(ranges)),
);
self.scrollbar_marker_state.dirty = true;
cx.notify();
@@ -21246,7 +21253,6 @@ impl Editor {
) -> Vec<(Range<DisplayPoint>, Hsla)> {
let mut results = Vec::new();
for (color_fetcher, ranges) in self.background_highlights.values() {
let color = color_fetcher(theme);
let start_ix = match ranges.binary_search_by(|probe| {
let cmp = probe
.end
@@ -21259,7 +21265,7 @@ impl Editor {
}) {
Ok(i) | Err(i) => i,
};
for range in &ranges[start_ix..] {
for (index, range) in ranges[start_ix..].iter().enumerate() {
if range
.start
.cmp(&search_range.end, &display_snapshot.buffer_snapshot())
@@ -21268,6 +21274,7 @@ impl Editor {
break;
}
let color = color_fetcher(&(start_ix + index), theme);
let start = range.start.to_display_point(display_snapshot);
let end = range.end.to_display_point(display_snapshot);
results.push((start..end, color))
@@ -22002,29 +22009,40 @@ impl Editor {
// Handle file-less buffers separately: those are not really the project items, so won't have a project path or entity id,
// so `workspace.open_project_item` will never find them, always opening a new editor.
// Instead, we try to activate the existing editor in the pane first.
let (editor, pane_item_index) =
let (editor, pane_item_index, pane_item_id) =
pane.read(cx).items().enumerate().find_map(|(i, item)| {
let editor = item.downcast::<Editor>()?;
let singleton_buffer =
editor.read(cx).buffer().read(cx).as_singleton()?;
if singleton_buffer == buffer {
Some((editor, i))
Some((editor, i, item.item_id()))
} else {
None
}
})?;
pane.update(cx, |pane, cx| {
pane.activate_item(pane_item_index, true, true, window, cx)
pane.activate_item(pane_item_index, true, true, window, cx);
if !PreviewTabsSettings::get_global(cx)
.enable_preview_from_multibuffer
{
pane.unpreview_item_if_preview(pane_item_id);
}
});
Some(editor)
})
.flatten()
.unwrap_or_else(|| {
let keep_old_preview = PreviewTabsSettings::get_global(cx)
.enable_keep_preview_on_code_navigation;
let allow_new_preview =
PreviewTabsSettings::get_global(cx).enable_preview_from_multibuffer;
workspace.open_project_item::<Self>(
pane.clone(),
buffer,
true,
true,
keep_old_preview,
allow_new_preview,
window,
cx,
)

View File

@@ -16978,7 +16978,7 @@ fn test_highlighted_ranges(cx: &mut TestAppContext) {
anchor_range(Point::new(6, 3)..Point::new(6, 5)),
anchor_range(Point::new(8, 4)..Point::new(8, 6)),
],
|_| Hsla::red(),
|_, _| Hsla::red(),
cx,
);
editor.highlight_background::<Type2>(
@@ -16988,7 +16988,7 @@ fn test_highlighted_ranges(cx: &mut TestAppContext) {
anchor_range(Point::new(7, 4)..Point::new(7, 7)),
anchor_range(Point::new(9, 5)..Point::new(9, 8)),
],
|_| Hsla::green(),
|_, _| Hsla::green(),
cx,
);
@@ -23973,7 +23973,7 @@ async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
editor.highlight_background::<DocumentHighlightRead>(
&[highlight_range],
|theme| theme.colors().editor_document_highlight_read_background,
|_, theme| theme.colors().editor_document_highlight_read_background,
cx,
);
});
@@ -24051,7 +24051,7 @@ async fn test_rename_without_prepare(cx: &mut TestAppContext) {
let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
editor.highlight_background::<DocumentHighlightRead>(
&[highlight_range],
|theme| theme.colors().editor_document_highlight_read_background,
|_, theme| theme.colors().editor_document_highlight_read_background,
cx,
);
});
@@ -27299,7 +27299,7 @@ let result = variable * 2;",
editor.highlight_background::<DocumentHighlightRead>(
&anchor_ranges,
|theme| theme.colors().editor_document_highlight_read_background,
|_, theme| theme.colors().editor_document_highlight_read_background,
cx,
);
});

View File

@@ -518,7 +518,7 @@ fn show_hover(
// Highlight the selected symbol using a background highlight
editor.highlight_background::<HoverState>(
&hover_highlights,
|theme| theme.colors().element_hover, // todo update theme
|_, theme| theme.colors().element_hover, // todo update theme
cx,
);
}

View File

@@ -1487,6 +1487,7 @@ impl SearchableItem for Editor {
fn update_matches(
&mut self,
matches: &[Range<Anchor>],
active_match_index: Option<usize>,
_: &mut Window,
cx: &mut Context<Self>,
) {
@@ -1497,7 +1498,13 @@ impl SearchableItem for Editor {
let updated = existing_range != Some(matches);
self.highlight_background::<BufferSearchHighlights>(
matches,
|theme| theme.colors().search_match_background,
move |index, theme| {
if active_match_index == Some(*index) {
theme.colors().search_active_match_background
} else {
theme.colors().search_match_background
}
},
cx,
);
if updated {

View File

@@ -17,9 +17,3 @@ pub struct PanicFeatureFlag;
impl FeatureFlag for PanicFeatureFlag {
const NAME: &'static str = "panic";
}
pub struct InlineAssistantV2FeatureFlag;
impl FeatureFlag for InlineAssistantV2FeatureFlag {
const NAME: &'static str = "inline-assistant-v2";
}

View File

@@ -4004,28 +4004,21 @@ impl GitPanel {
"Restore File"
};
let context_menu = ContextMenu::build(window, cx, |context_menu, _, _| {
let mut context_menu = context_menu
let is_created = entry.status.is_created();
context_menu
.context(self.focus_handle.clone())
.action(stage_title, ToggleStaged.boxed_clone())
.action(restore_title, git::RestoreFile::default().boxed_clone());
if entry.status.is_created() {
context_menu =
context_menu.action("Add to .gitignore", git::AddToGitignore.boxed_clone())
}
context_menu = context_menu
.action(restore_title, git::RestoreFile::default().boxed_clone())
.action_disabled_when(
!is_created,
"Add to .gitignore",
git::AddToGitignore.boxed_clone(),
)
.separator()
.action("Open Diff", Confirm.boxed_clone())
.action("Open File", SecondaryConfirm.boxed_clone());
if !entry.status.is_created() {
context_menu = context_menu
.separator()
.action("File History", Box::new(git::FileHistory));
}
context_menu
.action("Open File", SecondaryConfirm.boxed_clone())
.separator()
.action_disabled_when(is_created, "File History", Box::new(git::FileHistory))
});
self.selected_entry = Some(ix);
self.set_context_menu(context_menu, position, window, cx);

View File

@@ -707,40 +707,6 @@ pub trait LanguageModel: Send + Sync {
.boxed()
}
fn stream_completion_tool(
&self,
request: LanguageModelRequest,
cx: &AsyncApp,
) -> BoxFuture<'static, Result<LanguageModelToolUse, LanguageModelCompletionError>> {
let future = self.stream_completion(request, cx);
async move {
let events = future.await?;
let mut events = events.fuse();
// Iterate through events until we find a complete ToolUse
while let Some(event) = events.next().await {
match event {
Ok(LanguageModelCompletionEvent::ToolUse(tool_use))
if tool_use.is_input_complete =>
{
return Ok(tool_use);
}
Err(err) => {
return Err(err);
}
_ => {}
}
}
// Stream ended without a complete tool use
Err(LanguageModelCompletionError::Other(anyhow::anyhow!(
"Stream ended without receiving a complete tool use"
)))
}
.boxed()
}
fn cache_configuration(&self) -> Option<LanguageModelCacheConfiguration> {
None
}

View File

@@ -805,11 +805,13 @@ impl SearchableItem for LspLogView {
fn update_matches(
&mut self,
matches: &[Self::Match],
active_match_index: Option<usize>,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.editor
.update(cx, |e, cx| e.update_matches(matches, window, cx))
self.editor.update(cx, |e, cx| {
e.update_matches(matches, active_match_index, window, cx)
})
}
fn query_suggestion(&mut self, window: &mut Window, cx: &mut Context<Self>) -> String {

View File

@@ -459,7 +459,7 @@ impl SyntaxTreeView {
editor.clear_background_highlights::<Self>(cx);
editor.highlight_background::<Self>(
&[range],
|theme| {
|_, theme| {
theme
.colors()
.editor_document_highlight_write_background

View File

@@ -153,3 +153,9 @@ pub(crate) mod m_2025_11_25 {
pub(crate) use settings::remove_context_server_source;
}
pub(crate) mod m_2025_12_01 {
mod settings;
pub(crate) use settings::SETTINGS_PATTERNS;
}

View File

@@ -0,0 +1,55 @@
use std::ops::Range;
use tree_sitter::{Query, QueryMatch};
use crate::MigrationPatterns;
use crate::patterns::SETTINGS_NESTED_KEY_VALUE_PATTERN;
pub const SETTINGS_PATTERNS: MigrationPatterns = &[(
SETTINGS_NESTED_KEY_VALUE_PATTERN,
rename_enable_preview_from_code_navigation_setting,
)];
fn rename_enable_preview_from_code_navigation_setting(
contents: &str,
mat: &QueryMatch,
query: &Query,
) -> Option<(Range<usize>, String)> {
if !is_enable_preview_from_code_navigation(contents, mat, query) {
return None;
}
let setting_name_ix = query.capture_index_for_name("setting_name")?;
let setting_name_range = mat
.nodes_for_capture_index(setting_name_ix)
.next()?
.byte_range();
Some((
setting_name_range,
"enable_keep_preview_on_code_navigation".to_string(),
))
}
fn is_enable_preview_from_code_navigation(contents: &str, mat: &QueryMatch, query: &Query) -> bool {
let parent_key_ix = match query.capture_index_for_name("parent_key") {
Some(ix) => ix,
None => return false,
};
let parent_range = match mat.nodes_for_capture_index(parent_key_ix).next() {
Some(node) => node.byte_range(),
None => return false,
};
if contents.get(parent_range) != Some("preview_tabs") {
return false;
}
let setting_name_ix = match query.capture_index_for_name("setting_name") {
Some(ix) => ix,
None => return false,
};
let setting_name_range = match mat.nodes_for_capture_index(setting_name_ix).next() {
Some(node) => node.byte_range(),
None => return false,
};
contents.get(setting_name_range) == Some("enable_preview_from_code_navigation")
}

View File

@@ -219,6 +219,10 @@ pub fn migrate_settings(text: &str) -> Result<Option<String>> {
migrations::m_2025_11_12::SETTINGS_PATTERNS,
&SETTINGS_QUERY_2025_11_12,
),
MigrationType::TreeSitter(
migrations::m_2025_12_01::SETTINGS_PATTERNS,
&SETTINGS_QUERY_2025_12_01,
),
MigrationType::TreeSitter(
migrations::m_2025_11_20::SETTINGS_PATTERNS,
&SETTINGS_QUERY_2025_11_20,
@@ -346,6 +350,10 @@ define_query!(
SETTINGS_QUERY_2025_11_12,
migrations::m_2025_11_12::SETTINGS_PATTERNS
);
define_query!(
SETTINGS_QUERY_2025_12_01,
migrations::m_2025_12_01::SETTINGS_PATTERNS
);
define_query!(
SETTINGS_QUERY_2025_11_20,
migrations::m_2025_11_20::SETTINGS_PATTERNS
@@ -2262,51 +2270,6 @@ mod tests {
);
}
#[test]
fn test_project_panel_open_file_on_paste_migration() {
assert_migrate_settings(
&r#"
{
"project_panel": {
"open_file_on_paste": true
}
}
"#
.unindent(),
Some(
&r#"
{
"project_panel": {
"auto_open": { "on_paste": true }
}
}
"#
.unindent(),
),
);
assert_migrate_settings(
&r#"
{
"project_panel": {
"open_file_on_paste": false
}
}
"#
.unindent(),
Some(
&r#"
{
"project_panel": {
"auto_open": { "on_paste": false }
}
}
"#
.unindent(),
),
);
}
#[test]
fn test_remove_context_server_source() {
assert_migrate_settings(
@@ -2354,4 +2317,102 @@ mod tests {
),
);
}
#[test]
fn test_project_panel_open_file_on_paste_migration() {
assert_migrate_settings(
&r#"
{
"project_panel": {
"open_file_on_paste": true
}
}
"#
.unindent(),
Some(
&r#"
{
"project_panel": {
"auto_open": { "on_paste": true }
}
}
"#
.unindent(),
),
);
assert_migrate_settings(
&r#"
{
"project_panel": {
"open_file_on_paste": false
}
}
"#
.unindent(),
Some(
&r#"
{
"project_panel": {
"auto_open": { "on_paste": false }
}
}
"#
.unindent(),
),
);
}
#[test]
fn test_enable_preview_from_code_navigation_migration() {
assert_migrate_settings(
&r#"
{
"other_setting_1": 1,
"preview_tabs": {
"other_setting_2": 2,
"enable_preview_from_code_navigation": false
}
}
"#
.unindent(),
Some(
&r#"
{
"other_setting_1": 1,
"preview_tabs": {
"other_setting_2": 2,
"enable_keep_preview_on_code_navigation": false
}
}
"#
.unindent(),
),
);
assert_migrate_settings(
&r#"
{
"other_setting_1": 1,
"preview_tabs": {
"other_setting_2": 2,
"enable_preview_from_code_navigation": true
}
}
"#
.unindent(),
Some(
&r#"
{
"other_setting_1": 1,
"preview_tabs": {
"other_setting_2": 2,
"enable_keep_preview_on_code_navigation": true
}
}
"#
.unindent(),
),
);
}
}

View File

@@ -1529,7 +1529,8 @@ impl ProjectPanel {
}
fn open(&mut self, _: &Open, window: &mut Window, cx: &mut Context<Self>) {
let preview_tabs_enabled = PreviewTabsSettings::get_global(cx).enabled;
let preview_tabs_enabled =
PreviewTabsSettings::get_global(cx).enable_preview_from_project_panel;
self.open_internal(true, !preview_tabs_enabled, None, window, cx);
}
@@ -4819,7 +4820,7 @@ impl ProjectPanel {
project_panel.toggle_expanded(entry_id, window, cx);
}
} else {
let preview_tabs_enabled = PreviewTabsSettings::get_global(cx).enabled;
let preview_tabs_enabled = PreviewTabsSettings::get_global(cx).enable_preview_from_project_panel;
let click_count = event.click_count();
let focus_opened_item = click_count > 1;
let allow_preview = preview_tabs_enabled && click_count == 1;

View File

@@ -133,8 +133,9 @@ impl PickerDelegate for ProjectSymbolsDelegate {
workspace.active_pane().clone()
};
let editor =
workspace.open_project_item::<Editor>(pane, buffer, true, true, window, cx);
let editor = workspace.open_project_item::<Editor>(
pane, buffer, true, true, true, true, window, cx,
);
editor.update(cx, |editor, cx| {
editor.change_selections(

View File

@@ -94,16 +94,6 @@ pub struct ContentPromptContext {
pub diagnostic_errors: Vec<ContentPromptDiagnosticContext>,
}
#[derive(Serialize)]
pub struct ContentPromptContextV2 {
pub content_type: String,
pub language_name: Option<String>,
pub is_truncated: bool,
pub document_content: String,
pub rewrite_section: Option<String>,
pub diagnostic_errors: Vec<ContentPromptDiagnosticContext>,
}
#[derive(Serialize)]
pub struct TerminalAssistantPromptContext {
pub os: String,
@@ -286,88 +276,6 @@ impl PromptBuilder {
Ok(())
}
pub fn generate_inline_transformation_prompt_v2(
&self,
language_name: Option<&LanguageName>,
buffer: BufferSnapshot,
range: Range<usize>,
) -> Result<String, RenderError> {
let content_type = match language_name.as_ref().map(|l| l.as_ref()) {
None | Some("Markdown" | "Plain Text") => "text",
Some(_) => "code",
};
const MAX_CTX: usize = 50000;
let is_insert = range.is_empty();
let mut is_truncated = false;
let before_range = 0..range.start;
let truncated_before = if before_range.len() > MAX_CTX {
is_truncated = true;
let start = buffer.clip_offset(range.start - MAX_CTX, text::Bias::Right);
start..range.start
} else {
before_range
};
let after_range = range.end..buffer.len();
let truncated_after = if after_range.len() > MAX_CTX {
is_truncated = true;
let end = buffer.clip_offset(range.end + MAX_CTX, text::Bias::Left);
range.end..end
} else {
after_range
};
let mut document_content = String::new();
for chunk in buffer.text_for_range(truncated_before) {
document_content.push_str(chunk);
}
if is_insert {
document_content.push_str("<insert_here></insert_here>");
} else {
document_content.push_str("<rewrite_this>\n");
for chunk in buffer.text_for_range(range.clone()) {
document_content.push_str(chunk);
}
document_content.push_str("\n</rewrite_this>");
}
for chunk in buffer.text_for_range(truncated_after) {
document_content.push_str(chunk);
}
let rewrite_section = if !is_insert {
let mut section = String::new();
for chunk in buffer.text_for_range(range.clone()) {
section.push_str(chunk);
}
Some(section)
} else {
None
};
let diagnostics = buffer.diagnostics_in_range::<_, Point>(range, false);
let diagnostic_errors: Vec<ContentPromptDiagnosticContext> = diagnostics
.map(|entry| {
let start = entry.range.start;
ContentPromptDiagnosticContext {
line_number: (start.row + 1) as usize,
error_message: entry.diagnostic.message.clone(),
code_content: buffer.text_for_range(entry.range).collect(),
}
})
.collect();
let context = ContentPromptContextV2 {
content_type: content_type.to_string(),
language_name: language_name.map(|s| s.to_string()),
is_truncated,
document_content,
rewrite_section,
diagnostic_errors,
};
self.handlebars.lock().render("content_prompt_v2", &context)
}
pub fn generate_inline_transformation_prompt(
&self,
user_prompt: String,

View File

@@ -1031,7 +1031,7 @@ impl BufferSearchBar {
let new_match_index = searchable_item
.match_index_for_direction(matches, index, direction, count, window, cx);
searchable_item.update_matches(matches, window, cx);
searchable_item.update_matches(matches, Some(new_match_index), window, cx);
searchable_item.activate_match(new_match_index, matches, window, cx);
}
}
@@ -1045,7 +1045,7 @@ impl BufferSearchBar {
if matches.is_empty() {
return;
}
searchable_item.update_matches(matches, window, cx);
searchable_item.update_matches(matches, Some(0), window, cx);
searchable_item.activate_match(0, matches, window, cx);
}
}
@@ -1060,7 +1060,7 @@ impl BufferSearchBar {
return;
}
let new_match_index = matches.len() - 1;
searchable_item.update_matches(matches, window, cx);
searchable_item.update_matches(matches, Some(new_match_index), window, cx);
searchable_item.activate_match(new_match_index, matches, window, cx);
}
}
@@ -1300,7 +1300,12 @@ impl BufferSearchBar {
if matches.is_empty() {
active_searchable_item.clear_matches(window, cx);
} else {
active_searchable_item.update_matches(matches, window, cx);
active_searchable_item.update_matches(
matches,
this.active_match_index,
window,
cx,
);
}
let _ = done_tx.send(());
}
@@ -1335,6 +1340,18 @@ impl BufferSearchBar {
});
if new_index != self.active_match_index {
self.active_match_index = new_index;
if !self.dismissed {
if let Some(searchable_item) = self.active_searchable_item.as_ref() {
if let Some(matches) = self
.searchable_items_with_matches
.get(&searchable_item.downgrade())
{
if !matches.is_empty() {
searchable_item.update_matches(matches, new_index, window, cx);
}
}
}
}
cx.notify();
}
}

View File

@@ -1444,6 +1444,7 @@ impl ProjectSearchView {
s.select_ranges([range_to_select])
});
});
self.highlight_matches(&match_ranges, Some(new_index), cx);
}
}
@@ -1518,11 +1519,6 @@ impl ProjectSearchView {
});
editor.scroll(Point::default(), Some(Axis::Vertical), window, cx);
}
editor.highlight_background::<Self>(
&match_ranges,
|theme| theme.colors().search_match_background,
cx,
);
});
if is_new_search && self.query_editor.focus_handle(cx).is_focused(window) {
self.focus_results_editor(window, cx);
@@ -1535,18 +1531,41 @@ impl ProjectSearchView {
fn update_match_index(&mut self, cx: &mut Context<Self>) {
let results_editor = self.results_editor.read(cx);
let match_ranges = self.entity.read(cx).match_ranges.clone();
let new_index = active_match_index(
Direction::Next,
&self.entity.read(cx).match_ranges,
&match_ranges,
&results_editor.selections.newest_anchor().head(),
&results_editor.buffer().read(cx).snapshot(cx),
);
self.highlight_matches(&match_ranges, new_index, cx);
if self.active_match_index != new_index {
self.active_match_index = new_index;
cx.notify();
}
}
fn highlight_matches(
&self,
match_ranges: &[Range<Anchor>],
active_index: Option<usize>,
cx: &mut Context<Self>,
) {
self.results_editor.update(cx, |editor, cx| {
editor.highlight_background::<Self>(
match_ranges,
move |index, theme| {
if active_index == Some(*index) {
theme.colors().search_active_match_background
} else {
theme.colors().search_match_background
}
},
cx,
);
});
}
pub fn has_matches(&self) -> bool {
self.active_match_index.is_some()
}
@@ -2456,7 +2475,9 @@ pub mod tests {
use pretty_assertions::assert_eq;
use project::FakeFs;
use serde_json::json;
use settings::{InlayHintSettingsContent, SettingsStore};
use settings::{
InlayHintSettingsContent, SettingsStore, ThemeColorsContent, ThemeStyleContent,
};
use util::{path, paths::PathStyle, rel_path::rel_path};
use util_macros::perf;
use workspace::DeploySearch;
@@ -2464,8 +2485,105 @@ pub mod tests {
#[perf]
#[gpui::test]
async fn test_project_search(cx: &mut TestAppContext) {
fn dp(row: u32, col: u32) -> DisplayPoint {
DisplayPoint::new(DisplayRow(row), col)
}
fn assert_active_match_index(
search_view: &WindowHandle<ProjectSearchView>,
cx: &mut TestAppContext,
expected_index: usize,
) {
search_view
.update(cx, |search_view, _window, _cx| {
assert_eq!(search_view.active_match_index, Some(expected_index));
})
.unwrap();
}
fn assert_selection_range(
search_view: &WindowHandle<ProjectSearchView>,
cx: &mut TestAppContext,
expected_range: Range<DisplayPoint>,
) {
search_view
.update(cx, |search_view, _window, cx| {
assert_eq!(
search_view.results_editor.update(cx, |editor, cx| editor
.selections
.display_ranges(&editor.display_snapshot(cx))),
[expected_range]
);
})
.unwrap();
}
fn assert_highlights(
search_view: &WindowHandle<ProjectSearchView>,
cx: &mut TestAppContext,
expected_highlights: Vec<(Range<DisplayPoint>, &str)>,
) {
search_view
.update(cx, |search_view, window, cx| {
let match_bg = cx.theme().colors().search_match_background;
let active_match_bg = cx.theme().colors().search_active_match_background;
let selection_bg = cx
.theme()
.colors()
.editor_document_highlight_bracket_background;
let highlights: Vec<_> = expected_highlights
.into_iter()
.map(|(range, color_type)| {
let color = match color_type {
"active" => active_match_bg,
"match" => match_bg,
"selection" => selection_bg,
_ => panic!("Unknown color type"),
};
(range, color)
})
.collect();
assert_eq!(
search_view.results_editor.update(cx, |editor, cx| editor
.all_text_background_highlights(window, cx)),
highlights.as_slice()
);
})
.unwrap();
}
fn select_match(
search_view: &WindowHandle<ProjectSearchView>,
cx: &mut TestAppContext,
direction: Direction,
) {
search_view
.update(cx, |search_view, window, cx| {
search_view.select_match(direction, window, cx);
})
.unwrap();
}
init_test(cx);
// Override active search match color since the fallback theme uses the same color
// for normal search match and active one, which can make this test less robust.
cx.update(|cx| {
SettingsStore::update_global(cx, |settings, cx| {
settings.update_user_settings(cx, |settings| {
settings.theme.experimental_theme_overrides = Some(ThemeStyleContent {
colors: ThemeColorsContent {
search_active_match_background: Some("#ff0000ff".to_string()),
..Default::default()
},
..Default::default()
});
});
});
});
let fs = FakeFs::new(cx.background_executor.clone());
fs.insert_tree(
path!("/dir"),
@@ -2486,113 +2604,113 @@ pub mod tests {
});
perform_search(search_view, "TWO", cx);
search_view.update(cx, |search_view, window, cx| {
assert_eq!(
search_view
.results_editor
.update(cx, |editor, cx| editor.display_text(cx)),
"\n\nconst THREE: usize = one::ONE + two::TWO;\n\n\nconst TWO: usize = one::ONE + one::ONE;"
);
let match_background_color = cx.theme().colors().search_match_background;
let selection_background_color = cx.theme().colors().editor_document_highlight_bracket_background;
assert_eq!(
search_view
.results_editor
.update(cx, |editor, cx| editor.all_text_background_highlights(window, cx)),
&[
(
DisplayPoint::new(DisplayRow(2), 32)..DisplayPoint::new(DisplayRow(2), 35),
match_background_color
),
(
DisplayPoint::new(DisplayRow(2), 37)..DisplayPoint::new(DisplayRow(2), 40),
selection_background_color
),
(
DisplayPoint::new(DisplayRow(2), 37)..DisplayPoint::new(DisplayRow(2), 40),
match_background_color
),
(
DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(5), 9),
match_background_color
),
]
);
assert_eq!(search_view.active_match_index, Some(0));
assert_eq!(
search_view
.results_editor
.update(cx, |editor, cx| editor.selections.display_ranges(&editor.display_snapshot(cx))),
[DisplayPoint::new(DisplayRow(2), 32)..DisplayPoint::new(DisplayRow(2), 35)]
);
search_view.select_match(Direction::Next, window, cx);
}).unwrap();
cx.run_until_parked();
search_view
.update(cx, |search_view, window, cx| {
assert_eq!(search_view.active_match_index, Some(1));
.update(cx, |search_view, _window, cx| {
assert_eq!(
search_view.results_editor.update(cx, |editor, cx| editor
.selections
.display_ranges(&editor.display_snapshot(cx))),
[DisplayPoint::new(DisplayRow(2), 37)..DisplayPoint::new(DisplayRow(2), 40)]
);
search_view.select_match(Direction::Next, window, cx);
})
.unwrap();
search_view
.update(cx, |search_view, window, cx| {
assert_eq!(search_view.active_match_index, Some(2));
assert_eq!(
search_view.results_editor.update(cx, |editor, cx| editor
.selections
.display_ranges(&editor.display_snapshot(cx))),
[DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(5), 9)]
);
search_view.select_match(Direction::Next, window, cx);
})
.unwrap();
search_view
.update(cx, |search_view, window, cx| {
assert_eq!(search_view.active_match_index, Some(0));
assert_eq!(
search_view.results_editor.update(cx, |editor, cx| editor
.selections
.display_ranges(&editor.display_snapshot(cx))),
[DisplayPoint::new(DisplayRow(2), 32)..DisplayPoint::new(DisplayRow(2), 35)]
);
search_view.select_match(Direction::Prev, window, cx);
})
.unwrap();
search_view
.update(cx, |search_view, window, cx| {
assert_eq!(search_view.active_match_index, Some(2));
assert_eq!(
search_view.results_editor.update(cx, |editor, cx| editor
.selections
.display_ranges(&editor.display_snapshot(cx))),
[DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(5), 9)]
);
search_view.select_match(Direction::Prev, window, cx);
})
.unwrap();
search_view
.update(cx, |search_view, _, cx| {
assert_eq!(search_view.active_match_index, Some(1));
assert_eq!(
search_view.results_editor.update(cx, |editor, cx| editor
.selections
.display_ranges(&editor.display_snapshot(cx))),
[DisplayPoint::new(DisplayRow(2), 37)..DisplayPoint::new(DisplayRow(2), 40)]
search_view
.results_editor
.update(cx, |editor, cx| editor.display_text(cx)),
"\n\nconst THREE: usize = one::ONE + two::TWO;\n\n\nconst TWO: usize = one::ONE + one::ONE;"
);
})
.unwrap();
assert_active_match_index(&search_view, cx, 0);
assert_selection_range(&search_view, cx, dp(2, 32)..dp(2, 35));
assert_highlights(
&search_view,
cx,
vec![
(dp(2, 32)..dp(2, 35), "active"),
(dp(2, 37)..dp(2, 40), "selection"),
(dp(2, 37)..dp(2, 40), "match"),
(dp(5, 6)..dp(5, 9), "match"),
// TODO: we should be getting selection highlight here after project search
// but for some reason we are not getting it here
],
);
select_match(&search_view, cx, Direction::Next);
cx.run_until_parked();
assert_active_match_index(&search_view, cx, 1);
assert_selection_range(&search_view, cx, dp(2, 37)..dp(2, 40));
assert_highlights(
&search_view,
cx,
vec![
(dp(2, 32)..dp(2, 35), "selection"),
(dp(2, 32)..dp(2, 35), "match"),
(dp(2, 37)..dp(2, 40), "active"),
(dp(5, 6)..dp(5, 9), "selection"),
(dp(5, 6)..dp(5, 9), "match"),
],
);
select_match(&search_view, cx, Direction::Next);
cx.run_until_parked();
assert_active_match_index(&search_view, cx, 2);
assert_selection_range(&search_view, cx, dp(5, 6)..dp(5, 9));
assert_highlights(
&search_view,
cx,
vec![
(dp(2, 32)..dp(2, 35), "selection"),
(dp(2, 32)..dp(2, 35), "match"),
(dp(2, 37)..dp(2, 40), "selection"),
(dp(2, 37)..dp(2, 40), "match"),
(dp(5, 6)..dp(5, 9), "active"),
],
);
select_match(&search_view, cx, Direction::Next);
cx.run_until_parked();
assert_active_match_index(&search_view, cx, 0);
assert_selection_range(&search_view, cx, dp(2, 32)..dp(2, 35));
assert_highlights(
&search_view,
cx,
vec![
(dp(2, 32)..dp(2, 35), "active"),
(dp(2, 37)..dp(2, 40), "selection"),
(dp(2, 37)..dp(2, 40), "match"),
(dp(5, 6)..dp(5, 9), "selection"),
(dp(5, 6)..dp(5, 9), "match"),
],
);
select_match(&search_view, cx, Direction::Prev);
cx.run_until_parked();
assert_active_match_index(&search_view, cx, 2);
assert_selection_range(&search_view, cx, dp(5, 6)..dp(5, 9));
assert_highlights(
&search_view,
cx,
vec![
(dp(2, 32)..dp(2, 35), "selection"),
(dp(2, 32)..dp(2, 35), "match"),
(dp(2, 37)..dp(2, 40), "selection"),
(dp(2, 37)..dp(2, 40), "match"),
(dp(5, 6)..dp(5, 9), "active"),
],
);
select_match(&search_view, cx, Direction::Prev);
cx.run_until_parked();
assert_active_match_index(&search_view, cx, 1);
assert_selection_range(&search_view, cx, dp(2, 37)..dp(2, 40));
assert_highlights(
&search_view,
cx,
vec![
(dp(2, 32)..dp(2, 35), "selection"),
(dp(2, 32)..dp(2, 35), "match"),
(dp(2, 37)..dp(2, 40), "active"),
(dp(5, 6)..dp(5, 9), "selection"),
(dp(5, 6)..dp(5, 9), "match"),
],
);
}
#[perf]

View File

@@ -570,6 +570,9 @@ pub struct ThemeColorsContent {
#[serde(rename = "search.match_background")]
pub search_match_background: Option<String>,
#[serde(rename = "search.active_match_background")]
pub search_active_match_background: Option<String>,
#[serde(rename = "panel.background")]
pub panel_background: Option<String>,

View File

@@ -152,14 +152,31 @@ pub struct PreviewTabsSettingsContent {
///
/// Default: true
pub enabled: Option<bool>,
/// Whether to open tabs in preview mode when opened from the project panel with a single click.
///
/// Default: true
pub enable_preview_from_project_panel: Option<bool>,
/// Whether to open tabs in preview mode when selected from the file finder.
///
/// Default: false
pub enable_preview_from_file_finder: Option<bool>,
/// Whether a preview tab gets replaced when code navigation is used to navigate away from the tab.
/// Whether to open tabs in preview mode when opened from a multibuffer.
///
/// Default: true
pub enable_preview_from_multibuffer: Option<bool>,
/// Whether to open tabs in preview mode when code navigation is used to open a multibuffer.
///
/// Default: false
pub enable_preview_from_code_navigation: Option<bool>,
pub enable_preview_multibuffer_from_code_navigation: Option<bool>,
/// Whether to open tabs in preview mode when code navigation is used to open a single file.
///
/// Default: true
pub enable_preview_file_from_code_navigation: Option<bool>,
/// Whether to keep tabs in preview mode when code navigation is used to navigate away from them.
/// If `enable_preview_file_from_code_navigation` or `enable_preview_multibuffer_from_code_navigation` is also true, the new tab may replace the existing one.
///
/// Default: false
pub enable_keep_preview_on_code_navigation: Option<bool>,
}
#[derive(

View File

@@ -619,9 +619,13 @@ impl VsCodeSettings {
fn preview_tabs_settings_content(&self) -> Option<PreviewTabsSettingsContent> {
skip_default(PreviewTabsSettingsContent {
enabled: self.read_bool("workbench.editor.enablePreview"),
enable_preview_from_project_panel: None,
enable_preview_from_file_finder: self
.read_bool("workbench.editor.enablePreviewFromQuickOpen"),
enable_preview_from_code_navigation: self
enable_preview_from_multibuffer: None,
enable_preview_multibuffer_from_code_navigation: None,
enable_preview_file_from_code_navigation: None,
enable_keep_preview_on_code_navigation: self
.read_bool("workbench.editor.enablePreviewFromCodeNavigation"),
})
}

View File

@@ -3145,7 +3145,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
SettingsPageItem::SectionHeader("Preview Tabs"),
SettingsPageItem::SettingItem(SettingItem {
title: "Preview Tabs Enabled",
description: "Show opened editors as Preview tabs.",
description: "Show opened editors as preview tabs.",
field: Box::new(SettingField {
json_path: Some("preview_tabs.enabled"),
pick: |settings_content| {
@@ -3161,9 +3161,31 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
metadata: None,
files: USER,
}),
SettingsPageItem::SettingItem(SettingItem {
title: "Enable Preview From Project Panel",
description: "Whether to open tabs in preview mode when opened from the project panel with a single click.",
field: Box::new(SettingField {
json_path: Some("preview_tabs.enable_preview_from_project_panel"),
pick: |settings_content| {
settings_content
.preview_tabs
.as_ref()?
.enable_preview_from_project_panel
.as_ref()
},
write: |settings_content, value| {
settings_content
.preview_tabs
.get_or_insert_default()
.enable_preview_from_project_panel = value;
},
}),
metadata: None,
files: USER,
}),
SettingsPageItem::SettingItem(SettingItem {
title: "Enable Preview From File Finder",
description: "Whether to open tabs in Preview mode when selected from the file finder.",
description: "Whether to open tabs in preview mode when selected from the file finder.",
field: Box::new(SettingField {
json_path: Some("preview_tabs.enable_preview_from_file_finder"),
pick: |settings_content| {
@@ -3184,22 +3206,88 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
files: USER,
}),
SettingsPageItem::SettingItem(SettingItem {
title: "Enable Preview From Code Navigation",
description: "Whether a preview tab gets replaced when code navigation is used to navigate away from the tab.",
title: "Enable Preview From Multibuffer",
description: "Whether to open tabs in preview mode when opened from a multibuffer.",
field: Box::new(SettingField {
json_path: Some("preview_tabs.enable_preview_from_code_navigation"),
json_path: Some("preview_tabs.enable_preview_from_multibuffer"),
pick: |settings_content| {
settings_content
.preview_tabs
.as_ref()?
.enable_preview_from_code_navigation
.enable_preview_from_multibuffer
.as_ref()
},
write: |settings_content, value| {
settings_content
.preview_tabs
.get_or_insert_default()
.enable_preview_from_code_navigation = value;
.enable_preview_from_multibuffer = value;
},
}),
metadata: None,
files: USER,
}),
SettingsPageItem::SettingItem(SettingItem {
title: "Enable Preview Multibuffer From Code Navigation",
description: "Whether to open tabs in preview mode when code navigation is used to open a multibuffer.",
field: Box::new(SettingField {
json_path: Some("preview_tabs.enable_preview_multibuffer_from_code_navigation"),
pick: |settings_content| {
settings_content
.preview_tabs
.as_ref()?
.enable_preview_multibuffer_from_code_navigation
.as_ref()
},
write: |settings_content, value| {
settings_content
.preview_tabs
.get_or_insert_default()
.enable_preview_multibuffer_from_code_navigation = value;
},
}),
metadata: None,
files: USER,
}),
SettingsPageItem::SettingItem(SettingItem {
title: "Enable Preview File From Code Navigation",
description: "Whether to open tabs in preview mode when code navigation is used to open a single file.",
field: Box::new(SettingField {
json_path: Some("preview_tabs.enable_preview_file_from_code_navigation"),
pick: |settings_content| {
settings_content
.preview_tabs
.as_ref()?
.enable_preview_file_from_code_navigation
.as_ref()
},
write: |settings_content, value| {
settings_content
.preview_tabs
.get_or_insert_default()
.enable_preview_file_from_code_navigation = value;
},
}),
metadata: None,
files: USER,
}),
SettingsPageItem::SettingItem(SettingItem {
title: "Enable Keep Preview On Code Navigation",
description: "Whether to keep tabs in preview mode when code navigation is used to navigate away from them. If `enable_preview_file_from_code_navigation` or `enable_preview_multibuffer_from_code_navigation` is also true, the new tab may replace the existing one.",
field: Box::new(SettingField {
json_path: Some("preview_tabs.enable_keep_preview_on_code_navigation"),
pick: |settings_content| {
settings_content
.preview_tabs
.as_ref()?
.enable_keep_preview_on_code_navigation
.as_ref()
},
write: |settings_content, value| {
settings_content
.preview_tabs
.get_or_insert_default()
.enable_keep_preview_on_code_navigation = value;
},
}),
metadata: None,

View File

@@ -23,9 +23,9 @@ use ui::{
};
use util::ResultExt;
use workspace::{
ModalView, Pane, SaveIntent, Workspace,
Event as WorkspaceEvent, ModalView, Pane, SaveIntent, Workspace,
item::{ItemHandle, ItemSettings, ShowDiagnostics, TabContentParams},
pane::{Event as PaneEvent, render_item_indicator, tab_details},
pane::{render_item_indicator, tab_details},
};
const PANEL_WIDTH_REMS: f32 = 28.;
@@ -322,7 +322,7 @@ impl TabSwitcherDelegate {
cx: &mut Context<TabSwitcher>,
original_items: Vec<(Entity<Pane>, usize)>,
) -> Self {
Self::subscribe_to_updates(&pane, window, cx);
Self::subscribe_to_updates(&workspace, window, cx);
Self {
select_last,
tab_switcher,
@@ -338,34 +338,36 @@ impl TabSwitcherDelegate {
}
fn subscribe_to_updates(
pane: &WeakEntity<Pane>,
workspace: &WeakEntity<Workspace>,
window: &mut Window,
cx: &mut Context<TabSwitcher>,
) {
let Some(pane) = pane.upgrade() else {
let Some(workspace) = workspace.upgrade() else {
return;
};
cx.subscribe_in(&pane, window, |tab_switcher, _, event, window, cx| {
cx.subscribe_in(&workspace, window, |tab_switcher, _, event, window, cx| {
match event {
PaneEvent::AddItem { .. } | PaneEvent::Remove { .. } => {
WorkspaceEvent::ItemAdded { .. } | WorkspaceEvent::PaneRemoved => {
tab_switcher.picker.update(cx, |picker, cx| {
let query = picker.query(cx);
picker.delegate.update_matches(query, window, cx);
cx.notify();
})
}
PaneEvent::RemovedItem { .. } => tab_switcher.picker.update(cx, |picker, cx| {
let query = picker.query(cx);
picker.delegate.update_matches(query, window, cx);
WorkspaceEvent::ItemRemoved { .. } => {
tab_switcher.picker.update(cx, |picker, cx| {
let query = picker.query(cx);
picker.delegate.update_matches(query, window, cx);
// When the Tab Switcher is being used and an item is
// removed, there's a chance that the new selected index
// will not match the actual tab that is now being displayed
// by the pane, as such, the selected index needs to be
// updated to match the pane's state.
picker.delegate.sync_selected_index(cx);
cx.notify();
}),
// When the Tab Switcher is being used and an item is
// removed, there's a chance that the new selected index
// will not match the actual tab that is now being displayed
// by the pane, as such, the selected index needs to be
// updated to match the pane's state.
picker.delegate.sync_selected_index(cx);
cx.notify();
})
}
_ => {}
};
})
@@ -563,7 +565,14 @@ impl TabSwitcherDelegate {
/// as the pane's active item can be indirectly updated and this method
/// ensures that the picker can react to those changes.
fn sync_selected_index(&mut self, cx: &mut Context<Picker<TabSwitcherDelegate>>) {
let Ok(Some(item)) = self.pane.read_with(cx, |pane, _cx| pane.active_item()) else {
let item = if self.is_all_panes {
self.workspace
.read_with(cx, |workspace, cx| workspace.active_item(cx))
} else {
self.pane.read_with(cx, |pane, _cx| pane.active_item())
};
let Ok(Some(item)) = item else {
return;
};

View File

@@ -1434,6 +1434,7 @@ impl SearchableItem for TerminalView {
fn update_matches(
&mut self,
matches: &[Self::Match],
_active_match_index: Option<usize>,
_window: &mut Window,
cx: &mut Context<Self>,
) {

View File

@@ -91,6 +91,7 @@ impl ThemeColors {
tab_inactive_background: neutral().light().step_2(),
tab_active_background: neutral().light().step_1(),
search_match_background: neutral().light().step_5(),
search_active_match_background: neutral().light().step_7(),
panel_background: neutral().light().step_2(),
panel_focused_border: blue().light().step_10(),
panel_indent_guide: neutral().light_alpha().step_5(),
@@ -228,6 +229,7 @@ impl ThemeColors {
tab_inactive_background: neutral().dark().step_2(),
tab_active_background: neutral().dark().step_1(),
search_match_background: neutral().dark().step_5(),
search_active_match_background: neutral().dark().step_3(),
panel_background: neutral().dark().step_2(),
panel_focused_border: blue().dark().step_8(),
panel_indent_guide: neutral().dark_alpha().step_4(),

View File

@@ -152,6 +152,7 @@ pub(crate) fn zed_default_dark() -> Theme {
tab_inactive_background: bg,
tab_active_background: editor,
search_match_background: bg,
search_active_match_background: bg,
editor_background: editor,
editor_gutter_background: editor,

View File

@@ -287,6 +287,15 @@ pub fn theme_colors_refinement(
.panel_background
.as_ref()
.and_then(|color| try_parse_color(color).ok());
let search_match_background = this
.search_match_background
.as_ref()
.and_then(|color| try_parse_color(color).ok());
let search_active_match_background = this
.search_active_match_background
.as_ref()
.and_then(|color| try_parse_color(color).ok())
.or(search_match_background);
ThemeColorsRefinement {
border,
border_variant: this
@@ -442,10 +451,8 @@ pub fn theme_colors_refinement(
.tab_active_background
.as_ref()
.and_then(|color| try_parse_color(color).ok()),
search_match_background: this
.search_match_background
.as_ref()
.and_then(|color| try_parse_color(color).ok()),
search_match_background: search_match_background,
search_active_match_background: search_active_match_background,
panel_background,
panel_focused_border: this
.panel_focused_border

View File

@@ -128,6 +128,7 @@ pub struct ThemeColors {
pub tab_inactive_background: Hsla,
pub tab_active_background: Hsla,
pub search_match_background: Hsla,
pub search_active_match_background: Hsla,
pub panel_background: Hsla,
pub panel_focused_border: Hsla,
pub panel_indent_guide: Hsla,
@@ -352,6 +353,7 @@ pub enum ThemeColorField {
TabInactiveBackground,
TabActiveBackground,
SearchMatchBackground,
SearchActiveMatchBackground,
PanelBackground,
PanelFocusedBorder,
PanelIndentGuide,
@@ -467,6 +469,7 @@ impl ThemeColors {
ThemeColorField::TabInactiveBackground => self.tab_inactive_background,
ThemeColorField::TabActiveBackground => self.tab_active_background,
ThemeColorField::SearchMatchBackground => self.search_match_background,
ThemeColorField::SearchActiveMatchBackground => self.search_active_match_background,
ThemeColorField::PanelBackground => self.panel_background,
ThemeColorField::PanelFocusedBorder => self.panel_focused_border,
ThemeColorField::PanelIndentGuide => self.panel_indent_guide,

View File

@@ -227,7 +227,7 @@ impl Vim {
editor.highlight_background::<HighlightOnYank>(
&ranges_to_highlight,
|colors| colors.colors().editor_document_highlight_read_background,
|_, colors| colors.colors().editor_document_highlight_read_background,
cx,
);
cx.spawn(async move |this, cx| {

View File

@@ -273,7 +273,7 @@ impl Vim {
let ranges = [new_range];
editor.highlight_background::<VimExchange>(
&ranges,
|theme| theme.colors().editor_document_highlight_read_background,
|_, theme| theme.colors().editor_document_highlight_read_background,
cx,
);
}

View File

@@ -64,8 +64,12 @@ pub struct ItemSettings {
#[derive(RegisterSetting)]
pub struct PreviewTabsSettings {
pub enabled: bool,
pub enable_preview_from_project_panel: bool,
pub enable_preview_from_file_finder: bool,
pub enable_preview_from_code_navigation: bool,
pub enable_preview_from_multibuffer: bool,
pub enable_preview_multibuffer_from_code_navigation: bool,
pub enable_preview_file_from_code_navigation: bool,
pub enable_keep_preview_on_code_navigation: bool,
}
impl Settings for ItemSettings {
@@ -87,9 +91,19 @@ impl Settings for PreviewTabsSettings {
let preview_tabs = content.preview_tabs.as_ref().unwrap();
Self {
enabled: preview_tabs.enabled.unwrap(),
enable_preview_from_project_panel: preview_tabs
.enable_preview_from_project_panel
.unwrap(),
enable_preview_from_file_finder: preview_tabs.enable_preview_from_file_finder.unwrap(),
enable_preview_from_code_navigation: preview_tabs
.enable_preview_from_code_navigation
enable_preview_from_multibuffer: preview_tabs.enable_preview_from_multibuffer.unwrap(),
enable_preview_multibuffer_from_code_navigation: preview_tabs
.enable_preview_multibuffer_from_code_navigation
.unwrap(),
enable_preview_file_from_code_navigation: preview_tabs
.enable_preview_file_from_code_navigation
.unwrap(),
enable_keep_preview_on_code_navigation: preview_tabs
.enable_keep_preview_on_code_navigation
.unwrap(),
}
}

View File

@@ -873,10 +873,35 @@ impl Pane {
self.preview_item_id == Some(item_id)
}
/// Promotes the item with the given ID to not be a preview item.
/// This does nothing if it wasn't already a preview item.
pub fn unpreview_item_if_preview(&mut self, item_id: EntityId) {
if self.is_active_preview_item(item_id) {
self.preview_item_id = None;
}
}
/// Marks the item with the given ID as the preview item.
/// This will be ignored if the global setting `preview_tabs` is disabled.
pub fn set_preview_item_id(&mut self, item_id: Option<EntityId>, cx: &App) {
if PreviewTabsSettings::get_global(cx).enabled {
///
/// The old preview item (if there was one) is closed and its index is returned.
pub fn replace_preview_item_id(
&mut self,
item_id: EntityId,
window: &mut Window,
cx: &mut Context<Self>,
) -> Option<usize> {
let idx = self.close_current_preview_item(window, cx);
self.set_preview_item_id(Some(item_id), cx);
idx
}
/// Marks the item with the given ID as the preview item.
/// This will be ignored if the global setting `preview_tabs` is disabled.
///
/// This is a low-level method. Prefer `unpreview_item_if_preview()` or `set_new_preview_item()`.
pub(crate) fn set_preview_item_id(&mut self, item_id: Option<EntityId>, cx: &App) {
if item_id.is_none() || PreviewTabsSettings::get_global(cx).enabled {
self.preview_item_id = item_id;
}
}
@@ -895,7 +920,7 @@ impl Pane {
&& preview_item.item_id() == item_id
&& !preview_item.preserve_preview(cx)
{
self.set_preview_item_id(None, cx);
self.unpreview_item_if_preview(item_id);
}
}
@@ -936,14 +961,8 @@ impl Pane {
let set_up_existing_item =
|index: usize, pane: &mut Self, window: &mut Window, cx: &mut Context<Self>| {
// If the item is already open, and the item is a preview item
// and we are not allowing items to open as preview, mark the item as persistent.
if let Some(preview_item_id) = pane.preview_item_id
&& let Some(tab) = pane.items.get(index)
&& tab.item_id() == preview_item_id
&& !allow_preview
{
pane.set_preview_item_id(None, cx);
if !allow_preview && let Some(item) = pane.items.get(index) {
pane.unpreview_item_if_preview(item.item_id());
}
if activate {
pane.activate_item(index, focus_item, focus_item, window, cx);
@@ -955,7 +974,7 @@ impl Pane {
window: &mut Window,
cx: &mut Context<Self>| {
if allow_preview {
pane.set_preview_item_id(Some(new_item.item_id()), cx);
pane.replace_preview_item_id(new_item.item_id(), window, cx);
}
if let Some(text) = new_item.telemetry_event_text(cx) {
@@ -1036,6 +1055,7 @@ impl Pane {
) -> Option<usize> {
let item_idx = self.preview_item_idx()?;
let id = self.preview_item_id()?;
self.set_preview_item_id(None, cx);
let prev_active_item_index = self.active_item_index;
self.remove_item(id, false, false, window, cx);
@@ -1981,9 +2001,7 @@ impl Pane {
item.on_removed(cx);
self.nav_history.set_mode(mode);
if self.is_active_preview_item(item.item_id()) {
self.set_preview_item_id(None, cx);
}
self.unpreview_item_if_preview(item.item_id());
if let Some(path) = item.project_path(cx) {
let abs_path = self
@@ -2194,9 +2212,7 @@ impl Pane {
if can_save {
pane.update_in(cx, |pane, window, cx| {
if pane.is_active_preview_item(item.item_id()) {
pane.set_preview_item_id(None, cx);
}
pane.unpreview_item_if_preview(item.item_id());
item.save(
SaveOptions {
format: should_format,
@@ -2450,8 +2466,8 @@ impl Pane {
let id = self.item_for_index(ix)?.item_id();
let should_activate = ix == self.active_item_index;
if matches!(operation, PinOperation::Pin) && self.is_active_preview_item(id) {
self.set_preview_item_id(None, cx);
if matches!(operation, PinOperation::Pin) {
self.unpreview_item_if_preview(id);
}
match operation {
@@ -2624,12 +2640,9 @@ impl Pane {
)
.on_mouse_down(
MouseButton::Left,
cx.listener(move |pane, event: &MouseDownEvent, _, cx| {
if let Some(id) = pane.preview_item_id
&& id == item_id
&& event.click_count > 1
{
pane.set_preview_item_id(None, cx);
cx.listener(move |pane, event: &MouseDownEvent, _, _| {
if event.click_count > 1 {
pane.unpreview_item_if_preview(item_id);
}
}),
)
@@ -3272,11 +3285,7 @@ impl Pane {
let mut to_pane = cx.entity();
let split_direction = self.drag_split_direction;
let item_id = dragged_tab.item.item_id();
if let Some(preview_item_id) = self.preview_item_id
&& item_id == preview_item_id
{
self.set_preview_item_id(None, cx);
}
self.unpreview_item_if_preview(item_id);
let is_clone = cfg!(target_os = "macos") && window.modifiers().alt
|| cfg!(not(target_os = "macos")) && window.modifiers().control;
@@ -3788,15 +3797,17 @@ impl Render for Pane {
.on_action(cx.listener(Self::toggle_pin_tab))
.on_action(cx.listener(Self::unpin_all_tabs))
.when(PreviewTabsSettings::get_global(cx).enabled, |this| {
this.on_action(cx.listener(|pane: &mut Pane, _: &TogglePreviewTab, _, cx| {
if let Some(active_item_id) = pane.active_item().map(|i| i.item_id()) {
if pane.is_active_preview_item(active_item_id) {
pane.set_preview_item_id(None, cx);
} else {
pane.set_preview_item_id(Some(active_item_id), cx);
this.on_action(
cx.listener(|pane: &mut Pane, _: &TogglePreviewTab, window, cx| {
if let Some(active_item_id) = pane.active_item().map(|i| i.item_id()) {
if pane.is_active_preview_item(active_item_id) {
pane.unpreview_item_if_preview(active_item_id);
} else {
pane.replace_preview_item_id(active_item_id, window, cx);
}
}
}
}))
}),
)
})
.on_action(
cx.listener(|pane: &mut Self, action: &CloseActiveItem, window, cx| {

View File

@@ -96,6 +96,7 @@ pub trait SearchableItem: Item + EventEmitter<SearchEvent> {
fn update_matches(
&mut self,
matches: &[Self::Match],
active_match_index: Option<usize>,
window: &mut Window,
cx: &mut Context<Self>,
);
@@ -179,7 +180,13 @@ pub trait SearchableItemHandle: ItemHandle {
handler: Box<dyn Fn(&SearchEvent, &mut Window, &mut App) + Send>,
) -> Subscription;
fn clear_matches(&self, window: &mut Window, cx: &mut App);
fn update_matches(&self, matches: &AnyVec<dyn Send>, window: &mut Window, cx: &mut App);
fn update_matches(
&self,
matches: &AnyVec<dyn Send>,
active_match_index: Option<usize>,
window: &mut Window,
cx: &mut App,
);
fn query_suggestion(&self, window: &mut Window, cx: &mut App) -> String;
fn activate_match(
&self,
@@ -264,10 +271,16 @@ impl<T: SearchableItem> SearchableItemHandle for Entity<T> {
fn clear_matches(&self, window: &mut Window, cx: &mut App) {
self.update(cx, |this, cx| this.clear_matches(window, cx));
}
fn update_matches(&self, matches: &AnyVec<dyn Send>, window: &mut Window, cx: &mut App) {
fn update_matches(
&self,
matches: &AnyVec<dyn Send>,
active_match_index: Option<usize>,
window: &mut Window,
cx: &mut App,
) {
let matches = matches.downcast_ref().unwrap();
self.update(cx, |this, cx| {
this.update_matches(matches.as_slice(), window, cx)
this.update_matches(matches.as_slice(), active_match_index, window, cx)
});
}
fn query_suggestion(&self, window: &mut Window, cx: &mut App) -> String {

View File

@@ -3636,14 +3636,33 @@ impl Workspace {
project_item: Entity<T::Item>,
activate_pane: bool,
focus_item: bool,
keep_old_preview: bool,
allow_new_preview: bool,
window: &mut Window,
cx: &mut Context<Self>,
) -> Entity<T>
where
T: ProjectItem,
{
let old_item_id = pane.read(cx).active_item().map(|item| item.item_id());
if let Some(item) = self.find_project_item(&pane, &project_item, cx) {
if !keep_old_preview
&& let Some(old_id) = old_item_id
&& old_id != item.item_id()
{
// switching to a different item, so unpreview old active item
pane.update(cx, |pane, _| {
pane.unpreview_item_if_preview(old_id);
});
}
self.activate_item(&item, activate_pane, focus_item, window, cx);
if !allow_new_preview {
pane.update(cx, |pane, _| {
pane.unpreview_item_if_preview(item.item_id());
});
}
return item;
}
@@ -3652,16 +3671,14 @@ impl Workspace {
T::for_project_item(self.project().clone(), Some(pane), project_item, window, cx)
})
});
let item_id = item.item_id();
let mut destination_index = None;
pane.update(cx, |pane, cx| {
if PreviewTabsSettings::get_global(cx).enable_preview_from_code_navigation
&& let Some(preview_item_id) = pane.preview_item_id()
&& preview_item_id != item_id
{
destination_index = pane.close_current_preview_item(window, cx);
if !keep_old_preview && let Some(old_id) = old_item_id {
pane.unpreview_item_if_preview(old_id);
}
if allow_new_preview {
destination_index = pane.replace_preview_item_id(item.item_id(), window, cx);
}
pane.set_preview_item_id(Some(item.item_id()), cx)
});
self.add_item(
@@ -11457,4 +11474,26 @@ mod tests {
});
item
}
#[gpui::test]
async fn test_window_focus_set_on_startup_without_buffer(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree("/root", json!({"a": ""})).await;
let project = Project::test(fs, ["/root".as_ref()], cx).await;
let (workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
cx.run_until_parked();
workspace.update_in(cx, |_workspace, window, cx| {
let focused = window.focused(cx);
assert!(
focused.is_some(),
"Expected window focus to be set on startup, but it was None"
);
});
}
}

View File

@@ -2861,11 +2861,25 @@ Configuration object for defining settings profiles. Example:
```json [settings]
"preview_tabs": {
"enabled": true,
"enable_preview_from_project_panel": true,
"enable_preview_from_file_finder": false,
"enable_preview_from_code_navigation": false,
"enable_preview_from_multibuffer": true,
"enable_preview_multibuffer_from_code_navigation": false,
"enable_preview_file_from_code_navigation": true,
"enable_keep_preview_on_code_navigation": false,
}
```
### Enable preview from project panel
- Description: Determines whether to open files in preview mode when opened from the project panel with a single click.
- Setting: `enable_preview_from_project_panel`
- Default: `true`
**Options**
`boolean` values
### Enable preview from file finder
- Description: Determines whether to open files in preview mode when selected from the file finder.
@@ -2876,10 +2890,40 @@ Configuration object for defining settings profiles. Example:
`boolean` values
### Enable preview from code navigation
### Enable preview from multibuffer
- Description: Determines whether a preview tab gets replaced when code navigation is used to navigate away from the tab.
- Setting: `enable_preview_from_code_navigation`
- Description: Determines whether to open files in preview mode when opened from a multibuffer.
- Setting: `enable_preview_from_multibuffer`
- Default: `true`
**Options**
`boolean` values
### Enable preview multibuffer from code navigation
- Description: Determines whether to open tabs in preview mode when code navigation is used to open a multibuffer.
- Setting: `enable_preview_multibuffer_from_code_navigation`
- Default: `false`
**Options**
`boolean` values
### Enable preview file from code navigation
- Description: Determines whether to open tabs in preview mode when code navigation is used to open a single file.
- Setting: `enable_preview_file_from_code_navigation`
- Default: `true`
**Options**
`boolean` values
### Enable keep preview on code navigation
- Description: Determines whether to keep tabs in preview mode when code navigation is used to navigate away from them. If `enable_preview_file_from_code_navigation` or `enable_preview_multibuffer_from_code_navigation` is also true, the new tab may replace the existing one.
- Setting: `enable_keep_preview_on_code_navigation`
- Default: `false`
**Options**