diff --git a/Cargo.lock b/Cargo.lock index 8cc6037697..c13e488081 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -704,7 +704,9 @@ dependencies = [ name = "assistant_tools" version = "0.1.0" dependencies = [ + "aho-corasick", "anyhow", + "assistant_settings", "assistant_tool", "buffer_diff", "chrono", @@ -712,25 +714,36 @@ dependencies = [ "clock", "collections", "component", + "derive_more", "editor", + "feature_flags", + "fs", "futures 0.3.31", "gpui", + "gpui_tokio", + "handlebars 4.5.0", "html_to_markdown", "http_client", "indoc", "itertools 0.14.0", "language", "language_model", + "language_models", "linkme", "open", "pretty_assertions", "project", "rand 0.8.5", "regex", + "reqwest_client", + "rust-embed", "schemars", "serde", "serde_json", "settings", + "smallvec", + "streaming_diff", + "strsim", "task", "tempfile", "terminal", @@ -5003,6 +5016,7 @@ dependencies = [ "node_runtime", "pathdiff", "paths", + "pretty_assertions", "project", "prompt_store", "regex", @@ -6367,6 +6381,7 @@ dependencies = [ "log", "pest", "pest_derive", + "rust-embed", "serde", "serde_json", "thiserror 1.0.69", @@ -18032,6 +18047,7 @@ dependencies = [ "getrandom 0.2.15", "getrandom 0.3.2", "gimli", + "handlebars 4.5.0", "hashbrown 0.14.5", "hashbrown 0.15.2", "heck 0.4.1", diff --git a/assets/settings/default.json b/assets/settings/default.json index 2cbe10d5f0..6313b6e45d 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -657,6 +657,8 @@ }, // When enabled, the agent can run potentially destructive actions without asking for your confirmation. "always_allow_tool_actions": false, + // When enabled, the agent will stream edits. + "stream_edits": false, "default_profile": "write", "profiles": { "ask": { diff --git a/crates/assistant_settings/src/assistant_settings.rs b/crates/assistant_settings/src/assistant_settings.rs index ec15bdf6f1..80f1f1ef34 100644 --- a/crates/assistant_settings/src/assistant_settings.rs +++ b/crates/assistant_settings/src/assistant_settings.rs @@ -6,7 +6,7 @@ use ::open_ai::Model as OpenAiModel; use anthropic::Model as AnthropicModel; use anyhow::{Result, bail}; use deepseek::Model as DeepseekModel; -use feature_flags::{Assistant2FeatureFlag, FeatureFlagAppExt}; +use feature_flags::{AgentStreamEditsFeatureFlag, Assistant2FeatureFlag, FeatureFlagAppExt}; use gpui::{App, Pixels}; use indexmap::IndexMap; use language_model::{CloudModel, LanguageModel}; @@ -87,9 +87,14 @@ pub struct AssistantSettings { pub profiles: IndexMap, pub always_allow_tool_actions: bool, pub notify_when_agent_waiting: NotifyWhenAgentWaiting, + pub stream_edits: bool, } impl AssistantSettings { + pub fn stream_edits(&self, cx: &App) -> bool { + cx.has_flag::() || self.stream_edits + } + pub fn are_live_diffs_enabled(&self, cx: &App) -> bool { if cx.has_flag::() { return false; @@ -218,6 +223,7 @@ impl AssistantSettingsContent { profiles: None, always_allow_tool_actions: None, notify_when_agent_waiting: None, + stream_edits: None, }, VersionedAssistantSettingsContent::V2(ref settings) => settings.clone(), }, @@ -245,6 +251,7 @@ impl AssistantSettingsContent { profiles: None, always_allow_tool_actions: None, notify_when_agent_waiting: None, + stream_edits: None, }, None => AssistantSettingsContentV2::default(), } @@ -495,6 +502,7 @@ impl Default for VersionedAssistantSettingsContent { profiles: None, always_allow_tool_actions: None, notify_when_agent_waiting: None, + stream_edits: None, }) } } @@ -550,6 +558,10 @@ pub struct AssistantSettingsContentV2 { /// /// Default: "primary_screen" notify_when_agent_waiting: Option, + /// Whether to stream edits from the agent as they are received. + /// + /// Default: false + stream_edits: Option, } #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)] @@ -712,6 +724,7 @@ impl Settings for AssistantSettings { &mut settings.notify_when_agent_waiting, value.notify_when_agent_waiting, ); + merge(&mut settings.stream_edits, value.stream_edits); merge(&mut settings.default_profile, value.default_profile); if let Some(profiles) = value.profiles { @@ -843,6 +856,7 @@ mod tests { profiles: None, always_allow_tool_actions: None, notify_when_agent_waiting: None, + stream_edits: None, }, )), } diff --git a/crates/assistant_tools/Cargo.toml b/crates/assistant_tools/Cargo.toml index 78464a8a23..d0898e7f6a 100644 --- a/crates/assistant_tools/Cargo.toml +++ b/crates/assistant_tools/Cargo.toml @@ -11,16 +11,24 @@ workspace = true [lib] path = "src/assistant_tools.rs" +[features] +eval = [] + [dependencies] +aho-corasick.workspace = true anyhow.workspace = true assistant_tool.workspace = true +assistant_settings.workspace = true buffer_diff.workspace = true chrono.workspace = true collections.workspace = true component.workspace = true editor.workspace = true +derive_more.workspace = true +feature_flags.workspace = true futures.workspace = true gpui.workspace = true +handlebars = { workspace = true, features = ["rust-embed"] } html_to_markdown.workspace = true http_client.workspace = true indoc.workspace = true @@ -31,9 +39,14 @@ linkme.workspace = true open.workspace = true project.workspace = true regex.workspace = true +rust-embed.workspace = true schemars.workspace = true serde.workspace = true serde_json.workspace = true +settings.workspace = true +smallvec.workspace = true +streaming_diff.workspace = true +strsim.workspace = true task.workspace = true terminal.workspace = true terminal_view.workspace = true @@ -49,10 +62,15 @@ client = { workspace = true, features = ["test-support"] } clock = { workspace = true, features = ["test-support"] } collections = { workspace = true, features = ["test-support"] } gpui = { workspace = true, features = ["test-support"] } +gpui_tokio.workspace = true +fs = { workspace = true, features = ["test-support"] } language = { workspace = true, features = ["test-support"] } +language_model = { workspace = true, features = ["test-support"] } +language_models.workspace = true project = { workspace = true, features = ["test-support"] } rand.workspace = true pretty_assertions.workspace = true +reqwest_client.workspace = true settings = { workspace = true, features = ["test-support"] } task = { workspace = true, features = ["test-support"]} tempfile.workspace = true diff --git a/crates/assistant_tools/src/assistant_tools.rs b/crates/assistant_tools/src/assistant_tools.rs index edb435d7d0..29c67e28d6 100644 --- a/crates/assistant_tools/src/assistant_tools.rs +++ b/crates/assistant_tools/src/assistant_tools.rs @@ -7,6 +7,7 @@ mod create_directory_tool; mod create_file_tool; mod delete_path_tool; mod diagnostics_tool; +mod edit_agent; mod edit_file_tool; mod fetch_tool; mod find_path_tool; @@ -19,7 +20,9 @@ mod read_file_tool; mod rename_tool; mod replace; mod schema; +mod streaming_edit_file_tool; mod symbol_info_tool; +mod templates; mod terminal_tool; mod thinking_tool; mod ui; @@ -27,14 +30,19 @@ mod web_search_tool; use std::sync::Arc; +use assistant_settings::AssistantSettings; use assistant_tool::ToolRegistry; use copy_path_tool::CopyPathTool; +use feature_flags::{AgentStreamEditsFeatureFlag, FeatureFlagAppExt}; use gpui::App; use http_client::HttpClientWithUrl; use language_model::LanguageModelRegistry; use move_path_tool::MovePathTool; +use settings::{Settings, SettingsStore}; use web_search_tool::WebSearchTool; +pub(crate) use templates::*; + use crate::batch_tool::BatchTool; use crate::code_action_tool::CodeActionTool; use crate::code_symbols_tool::CodeSymbolsTool; @@ -52,6 +60,7 @@ use crate::now_tool::NowTool; use crate::open_tool::OpenTool; use crate::read_file_tool::ReadFileTool; use crate::rename_tool::RenameTool; +use crate::streaming_edit_file_tool::StreamingEditFileTool; use crate::symbol_info_tool::SymbolInfoTool; use crate::terminal_tool::TerminalTool; use crate::thinking_tool::ThinkingTool; @@ -71,7 +80,6 @@ pub fn init(http_client: Arc, cx: &mut App) { registry.register_tool(CreateFileTool); registry.register_tool(CopyPathTool); registry.register_tool(DeletePathTool); - registry.register_tool(EditFileTool); registry.register_tool(SymbolInfoTool); registry.register_tool(CodeActionTool); registry.register_tool(MovePathTool); @@ -88,6 +96,12 @@ pub fn init(http_client: Arc, cx: &mut App) { registry.register_tool(ThinkingTool); registry.register_tool(FetchTool::new(http_client)); + register_edit_file_tool(cx); + cx.observe_flag::(|_, cx| register_edit_file_tool(cx)) + .detach(); + cx.observe_global::(register_edit_file_tool) + .detach(); + cx.subscribe( &LanguageModelRegistry::global(cx), move |registry, event, cx| match event { @@ -108,6 +122,19 @@ pub fn init(http_client: Arc, cx: &mut App) { .detach(); } +fn register_edit_file_tool(cx: &mut App) { + let registry = ToolRegistry::global(cx); + + registry.unregister_tool(EditFileTool); + registry.unregister_tool(StreamingEditFileTool); + + if AssistantSettings::get_global(cx).stream_edits(cx) { + registry.register_tool(StreamingEditFileTool); + } else { + registry.register_tool(EditFileTool); + } +} + #[cfg(test)] mod tests { use super::*; @@ -146,6 +173,7 @@ mod tests { #[gpui::test] fn test_builtin_tool_schema_compatibility(cx: &mut App) { settings::init(cx); + AssistantSettings::register(cx); let client = Client::new( Arc::new(FakeSystemClock::new()), diff --git a/crates/assistant_tools/src/edit_agent.rs b/crates/assistant_tools/src/edit_agent.rs new file mode 100644 index 0000000000..3ab1d6ffc1 --- /dev/null +++ b/crates/assistant_tools/src/edit_agent.rs @@ -0,0 +1,1092 @@ +mod edit_parser; +#[cfg(test)] +mod evals; + +use crate::{Template, Templates}; +use aho_corasick::AhoCorasick; +use anyhow::Result; +use assistant_tool::ActionLog; +use edit_parser::{EditParser, EditParserEvent, EditParserMetrics}; +use futures::{ + Stream, StreamExt, + channel::mpsc::{self, UnboundedReceiver}, + stream::BoxStream, +}; +use gpui::{AppContext, AsyncApp, Entity, SharedString, Task}; +use language::{Bias, Buffer, BufferSnapshot, LineIndent, Point}; +use language_model::{ + LanguageModel, LanguageModelCompletionError, LanguageModelRequest, LanguageModelRequestMessage, + MessageContent, Role, +}; +use serde::Serialize; +use std::{cmp, iter, mem, ops::Range, path::PathBuf, sync::Arc, task::Poll}; +use streaming_diff::{CharOperation, StreamingDiff}; + +#[derive(Serialize)] +pub struct EditAgentTemplate { + path: Option, + edit_description: String, +} + +impl Template for EditAgentTemplate { + const TEMPLATE_NAME: &'static str = "edit_agent.hbs"; +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum EditAgentOutputEvent { + Edited, + HallucinatedOldText(SharedString), +} + +#[derive(Clone, Debug)] +pub struct EditAgentOutput { + pub _raw_edits: String, + pub _parser_metrics: EditParserMetrics, +} + +#[derive(Clone)] +pub struct EditAgent { + model: Arc, + action_log: Entity, + templates: Arc, +} + +impl EditAgent { + pub fn new( + model: Arc, + action_log: Entity, + templates: Arc, + ) -> Self { + EditAgent { + model, + action_log, + templates, + } + } + + pub fn edit( + &self, + buffer: Entity, + edit_description: String, + previous_messages: Vec, + cx: &mut AsyncApp, + ) -> ( + Task>, + mpsc::UnboundedReceiver, + ) { + let this = self.clone(); + let (events_tx, events_rx) = mpsc::unbounded(); + let output = cx.spawn(async move |cx| { + let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot())?; + let edit_chunks = this + .request_edits(snapshot, edit_description, previous_messages, cx) + .await?; + let (output, mut inner_events) = this.apply_edits(buffer, edit_chunks, cx); + while let Some(event) = inner_events.next().await { + events_tx.unbounded_send(event).ok(); + } + output.await + }); + (output, events_rx) + } + + fn apply_edits( + &self, + buffer: Entity, + edit_chunks: impl 'static + Send + Stream>, + cx: &mut AsyncApp, + ) -> ( + Task>, + mpsc::UnboundedReceiver, + ) { + let (output_events_tx, output_events_rx) = mpsc::unbounded(); + let this = self.clone(); + let task = cx.spawn(async move |mut cx| { + this.apply_edits_internal(buffer, edit_chunks, output_events_tx, &mut cx) + .await + }); + (task, output_events_rx) + } + + async fn apply_edits_internal( + &self, + buffer: Entity, + edit_chunks: impl 'static + Send + Stream>, + output_events: mpsc::UnboundedSender, + cx: &mut AsyncApp, + ) -> Result { + // Ensure the buffer is tracked by the action log. + self.action_log + .update(cx, |log, cx| log.track_buffer(buffer.clone(), cx))?; + + let (output, mut edit_events) = Self::parse_edit_chunks(edit_chunks, cx); + while let Some(edit_event) = edit_events.next().await { + let EditParserEvent::OldText(old_text_query) = edit_event? else { + continue; + }; + let old_text_query = SharedString::from(old_text_query); + + let (edits_tx, edits_rx) = mpsc::unbounded(); + let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?; + let old_range = cx + .background_spawn({ + let snapshot = snapshot.clone(); + let old_text_query = old_text_query.clone(); + async move { Self::resolve_location(&snapshot, &old_text_query) } + }) + .await; + let Some(old_range) = old_range else { + // We couldn't find the old text in the buffer. Report the error. + output_events + .unbounded_send(EditAgentOutputEvent::HallucinatedOldText(old_text_query)) + .ok(); + continue; + }; + + let compute_edits = cx.background_spawn(async move { + let buffer_start_indent = + snapshot.line_indent_for_row(snapshot.offset_to_point(old_range.start).row); + let old_text_start_indent = old_text_query + .lines() + .next() + .map_or(buffer_start_indent, |line| { + LineIndent::from_iter(line.chars()) + }); + let indent_delta = if buffer_start_indent.tabs > 0 { + IndentDelta::Tabs( + buffer_start_indent.tabs as isize - old_text_start_indent.tabs as isize, + ) + } else { + IndentDelta::Spaces( + buffer_start_indent.spaces as isize - old_text_start_indent.spaces as isize, + ) + }; + + let old_text = snapshot + .text_for_range(old_range.clone()) + .collect::(); + let mut diff = StreamingDiff::new(old_text); + let mut edit_start = old_range.start; + let mut new_text_chunks = + Self::reindent_new_text_chunks(indent_delta, &mut edit_events); + let mut done = false; + while !done { + let char_operations = if let Some(new_text_chunk) = new_text_chunks.next().await + { + diff.push_new(&new_text_chunk?) + } else { + done = true; + mem::take(&mut diff).finish() + }; + + for op in char_operations { + match op { + CharOperation::Insert { text } => { + let edit_start = snapshot.anchor_after(edit_start); + edits_tx.unbounded_send((edit_start..edit_start, text))?; + } + CharOperation::Delete { bytes } => { + let edit_end = edit_start + bytes; + let edit_range = snapshot.anchor_after(edit_start) + ..snapshot.anchor_before(edit_end); + edit_start = edit_end; + edits_tx.unbounded_send((edit_range, String::new()))?; + } + CharOperation::Keep { bytes } => edit_start += bytes, + } + } + } + + drop(new_text_chunks); + anyhow::Ok(edit_events) + }); + + // TODO: group all edits into one transaction + let mut edits_rx = edits_rx.ready_chunks(32); + while let Some(edits) = edits_rx.next().await { + // Edit the buffer and report edits to the action log as part of the + // same effect cycle, otherwise the edit will be reported as if the + // user made it. + cx.update(|cx| { + buffer.update(cx, |buffer, cx| buffer.edit(edits, None, cx)); + self.action_log + .update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx)) + })?; + output_events + .unbounded_send(EditAgentOutputEvent::Edited) + .ok(); + } + + edit_events = compute_edits.await?; + } + + output.await + } + + fn parse_edit_chunks( + chunks: impl 'static + Send + Stream>, + cx: &mut AsyncApp, + ) -> ( + Task>, + UnboundedReceiver>, + ) { + let (tx, rx) = mpsc::unbounded(); + let output = cx.background_spawn(async move { + futures::pin_mut!(chunks); + + let mut parser = EditParser::new(); + let mut raw_edits = String::new(); + while let Some(chunk) = chunks.next().await { + match chunk { + Ok(chunk) => { + raw_edits.push_str(&chunk); + for event in parser.push(&chunk) { + tx.unbounded_send(Ok(event))?; + } + } + Err(error) => { + tx.unbounded_send(Err(error.into()))?; + } + } + } + Ok(EditAgentOutput { + _raw_edits: raw_edits, + _parser_metrics: parser.finish(), + }) + }); + (output, rx) + } + + fn reindent_new_text_chunks( + delta: IndentDelta, + mut stream: impl Unpin + Stream>, + ) -> impl Stream> { + let mut buffer = String::new(); + let mut in_leading_whitespace = true; + let mut done = false; + futures::stream::poll_fn(move |cx| { + while !done { + let (chunk, is_last_chunk) = match stream.poll_next_unpin(cx) { + Poll::Ready(Some(Ok(EditParserEvent::NewTextChunk { chunk, done }))) => { + (chunk, done) + } + Poll::Ready(Some(Err(err))) => return Poll::Ready(Some(Err(err))), + Poll::Pending => return Poll::Pending, + _ => return Poll::Ready(None), + }; + + buffer.push_str(&chunk); + + let mut indented_new_text = String::new(); + let mut start_ix = 0; + let mut newlines = buffer.match_indices('\n').peekable(); + loop { + let (line_end, is_pending_line) = match newlines.next() { + Some((ix, _)) => (ix, false), + None => (buffer.len(), true), + }; + let line = &buffer[start_ix..line_end]; + + if in_leading_whitespace { + if let Some(non_whitespace_ix) = line.find(|c| delta.character() != c) { + // We found a non-whitespace character, adjust + // indentation based on the delta. + let new_indent_len = + cmp::max(0, non_whitespace_ix as isize + delta.len()) as usize; + indented_new_text + .extend(iter::repeat(delta.character()).take(new_indent_len)); + indented_new_text.push_str(&line[non_whitespace_ix..]); + in_leading_whitespace = false; + } else if is_pending_line { + // We're still in leading whitespace and this line is incomplete. + // Stop processing until we receive more input. + break; + } else { + // This line is entirely whitespace. Push it without indentation. + indented_new_text.push_str(line); + } + } else { + indented_new_text.push_str(line); + } + + if is_pending_line { + start_ix = line_end; + break; + } else { + in_leading_whitespace = true; + indented_new_text.push('\n'); + start_ix = line_end + 1; + } + } + buffer.replace_range(..start_ix, ""); + + // This was the last chunk, push all the buffered content as-is. + if is_last_chunk { + indented_new_text.push_str(&buffer); + buffer.clear(); + done = true; + } + + if !indented_new_text.is_empty() { + return Poll::Ready(Some(Ok(indented_new_text))); + } + } + + Poll::Ready(None) + }) + } + + async fn request_edits( + &self, + snapshot: BufferSnapshot, + edit_description: String, + mut messages: Vec, + cx: &mut AsyncApp, + ) -> Result>> { + let path = cx.update(|cx| snapshot.resolve_file_path(cx, true))?; + let prompt = EditAgentTemplate { + path, + edit_description, + } + .render(&self.templates)?; + + let mut message_content = Vec::new(); + if let Some(last_message) = messages.last_mut() { + if last_message.role == Role::Assistant { + last_message + .content + .retain(|content| !matches!(content, MessageContent::ToolUse(_))); + if last_message.content.is_empty() { + messages.pop(); + } + } + } + message_content.push(MessageContent::Text(prompt)); + messages.push(LanguageModelRequestMessage { + role: Role::User, + content: message_content, + cache: false, + }); + + let request = LanguageModelRequest { + messages, + ..Default::default() + }; + Ok(self.model.stream_completion_text(request, cx).await?.stream) + } + + fn resolve_location(buffer: &BufferSnapshot, search_query: &str) -> Option> { + let range = Self::resolve_location_exact(buffer, search_query) + .or_else(|| Self::resolve_location_fuzzy(buffer, search_query))?; + + // Expand the range to include entire lines. + let mut start = buffer.offset_to_point(buffer.clip_offset(range.start, Bias::Left)); + start.column = 0; + let mut end = buffer.offset_to_point(buffer.clip_offset(range.end, Bias::Right)); + if end.column > 0 { + end.column = buffer.line_len(end.row); + } + + Some(buffer.point_to_offset(start)..buffer.point_to_offset(end)) + } + + fn resolve_location_exact(buffer: &BufferSnapshot, search_query: &str) -> Option> { + let search = AhoCorasick::new([search_query]).ok()?; + let mat = search + .stream_find_iter(buffer.bytes_in_range(0..buffer.len())) + .next()? + .expect("buffer can't error"); + Some(mat.range()) + } + + fn resolve_location_fuzzy(buffer: &BufferSnapshot, search_query: &str) -> Option> { + const INSERTION_COST: u32 = 3; + const DELETION_COST: u32 = 10; + + let buffer_line_count = buffer.max_point().row as usize + 1; + let query_line_count = search_query.lines().count(); + let mut matrix = SearchMatrix::new(query_line_count + 1, buffer_line_count + 1); + let mut leading_deletion_cost = 0_u32; + for (row, query_line) in search_query.lines().enumerate() { + let query_line = query_line.trim(); + leading_deletion_cost = leading_deletion_cost.saturating_add(DELETION_COST); + matrix.set( + row + 1, + 0, + SearchState::new(leading_deletion_cost, SearchDirection::Diagonal), + ); + + let mut buffer_lines = buffer.as_rope().chunks().lines(); + let mut col = 0; + while let Some(buffer_line) = buffer_lines.next() { + let buffer_line = buffer_line.trim(); + let up = SearchState::new( + matrix.get(row, col + 1).cost.saturating_add(DELETION_COST), + SearchDirection::Up, + ); + let left = SearchState::new( + matrix.get(row + 1, col).cost.saturating_add(INSERTION_COST), + SearchDirection::Left, + ); + let diagonal = SearchState::new( + if fuzzy_eq(query_line, buffer_line) { + matrix.get(row, col).cost + } else { + matrix + .get(row, col) + .cost + .saturating_add(DELETION_COST + INSERTION_COST) + }, + SearchDirection::Diagonal, + ); + matrix.set(row + 1, col + 1, up.min(left).min(diagonal)); + col += 1; + } + } + + // Traceback to find the best match + let mut buffer_row_end = buffer_line_count as u32; + let mut best_cost = u32::MAX; + for col in 1..=buffer_line_count { + let cost = matrix.get(query_line_count, col).cost; + if cost < best_cost { + best_cost = cost; + buffer_row_end = col as u32; + } + } + + let mut matched_lines = 0; + let mut query_row = query_line_count; + let mut buffer_row_start = buffer_row_end; + while query_row > 0 && buffer_row_start > 0 { + let current = matrix.get(query_row, buffer_row_start as usize); + match current.direction { + SearchDirection::Diagonal => { + query_row -= 1; + buffer_row_start -= 1; + matched_lines += 1; + } + SearchDirection::Up => { + query_row -= 1; + } + SearchDirection::Left => { + buffer_row_start -= 1; + } + } + } + + let matched_buffer_row_count = buffer_row_end - buffer_row_start; + let matched_ratio = + matched_lines as f32 / (matched_buffer_row_count as f32).max(query_line_count as f32); + if matched_ratio >= 0.8 { + let buffer_start_ix = buffer.point_to_offset(Point::new(buffer_row_start, 0)); + let buffer_end_ix = buffer.point_to_offset(Point::new( + buffer_row_end - 1, + buffer.line_len(buffer_row_end - 1), + )); + Some(buffer_start_ix..buffer_end_ix) + } else { + None + } + } +} + +fn fuzzy_eq(left: &str, right: &str) -> bool { + let min_levenshtein = left.len().abs_diff(right.len()); + let min_normalized_levenshtein = + 1. - (min_levenshtein as f32 / cmp::max(left.len(), right.len()) as f32); + if min_normalized_levenshtein < 0.8 { + return false; + } + + strsim::normalized_levenshtein(left, right) >= 0.8 +} + +#[derive(Copy, Clone, Debug)] +enum IndentDelta { + Spaces(isize), + Tabs(isize), +} + +impl IndentDelta { + fn character(&self) -> char { + match self { + IndentDelta::Spaces(_) => ' ', + IndentDelta::Tabs(_) => '\t', + } + } + + fn len(&self) -> isize { + match self { + IndentDelta::Spaces(n) => *n, + IndentDelta::Tabs(n) => *n, + } + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +enum SearchDirection { + Up, + Left, + Diagonal, +} + +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] +struct SearchState { + cost: u32, + direction: SearchDirection, +} + +impl SearchState { + fn new(cost: u32, direction: SearchDirection) -> Self { + Self { cost, direction } + } +} + +struct SearchMatrix { + cols: usize, + data: Vec, +} + +impl SearchMatrix { + fn new(rows: usize, cols: usize) -> Self { + SearchMatrix { + cols, + data: vec![SearchState::new(0, SearchDirection::Diagonal); rows * cols], + } + } + + fn get(&self, row: usize, col: usize) -> SearchState { + self.data[row * self.cols + col] + } + + fn set(&mut self, row: usize, col: usize, cost: SearchState) { + self.data[row * self.cols + col] = cost; + } +} + +#[cfg(test)] +mod tests { + use super::*; + use fs::FakeFs; + use futures::stream; + use gpui::{App, AppContext, TestAppContext}; + use indoc::indoc; + use language_model::fake_provider::FakeLanguageModel; + use project::Project; + use rand::prelude::*; + use rand::rngs::StdRng; + use std::cmp; + use unindent::Unindent; + use util::test::{generate_marked_text, marked_text_ranges}; + + #[gpui::test(iterations = 100)] + async fn test_indentation(cx: &mut TestAppContext, mut rng: StdRng) { + let agent = init_test(cx).await; + let buffer = cx.new(|cx| { + Buffer::local( + indoc! {" + lorem + ipsum + dolor + sit + "}, + cx, + ) + }); + let raw_edits = simulate_llm_output( + indoc! {" + + ipsum + dolor + sit + + + ipsum + dolor + sit + amet + + "}, + &mut rng, + cx, + ); + let (apply, _events) = agent.apply_edits(buffer.clone(), raw_edits, &mut cx.to_async()); + apply.await.unwrap(); + pretty_assertions::assert_eq!( + buffer.read_with(cx, |buffer, _| buffer.snapshot().text()), + indoc! {" + lorem + ipsum + dolor + sit + amet + "} + ); + } + + #[gpui::test(iterations = 100)] + async fn test_dependent_edits(cx: &mut TestAppContext, mut rng: StdRng) { + let agent = init_test(cx).await; + let buffer = cx.new(|cx| Buffer::local("abc\ndef\nghi", cx)); + let raw_edits = simulate_llm_output( + indoc! {" + + def + + + DEF + + + + DEF + + + DeF + + "}, + &mut rng, + cx, + ); + let (apply, _events) = agent.apply_edits(buffer.clone(), raw_edits, &mut cx.to_async()); + apply.await.unwrap(); + assert_eq!( + buffer.read_with(cx, |buffer, _| buffer.snapshot().text()), + "abc\nDeF\nghi" + ); + } + + #[gpui::test(iterations = 100)] + async fn test_old_text_hallucination(cx: &mut TestAppContext, mut rng: StdRng) { + let agent = init_test(cx).await; + let buffer = cx.new(|cx| Buffer::local("abc\ndef\nghi", cx)); + let raw_edits = simulate_llm_output( + indoc! {" + + jkl + + + mno + + + + abc + + + ABC + + "}, + &mut rng, + cx, + ); + let (apply, _events) = agent.apply_edits(buffer.clone(), raw_edits, &mut cx.to_async()); + apply.await.unwrap(); + assert_eq!( + buffer.read_with(cx, |buffer, _| buffer.snapshot().text()), + "ABC\ndef\nghi" + ); + } + + #[gpui::test] + async fn test_events(cx: &mut TestAppContext) { + let agent = init_test(cx).await; + let buffer = cx.new(|cx| Buffer::local("abc\ndef\nghi", cx)); + let (chunks_tx, chunks_rx) = mpsc::unbounded(); + let (apply, mut events) = agent.apply_edits( + buffer.clone(), + chunks_rx.map(|chunk: &str| Ok(chunk.to_string())), + &mut cx.to_async(), + ); + + chunks_tx.unbounded_send("a").unwrap(); + cx.run_until_parked(); + assert_eq!(drain_events(&mut events), vec![]); + assert_eq!( + buffer.read_with(cx, |buffer, _| buffer.snapshot().text()), + "abc\ndef\nghi" + ); + + chunks_tx.unbounded_send("bc").unwrap(); + cx.run_until_parked(); + assert_eq!(drain_events(&mut events), vec![]); + assert_eq!( + buffer.read_with(cx, |buffer, _| buffer.snapshot().text()), + "abc\ndef\nghi" + ); + + chunks_tx.unbounded_send("abX").unwrap(); + cx.run_until_parked(); + assert_eq!(drain_events(&mut events), [EditAgentOutputEvent::Edited]); + assert_eq!( + buffer.read_with(cx, |buffer, _| buffer.snapshot().text()), + "abXc\ndef\nghi" + ); + + chunks_tx.unbounded_send("cY").unwrap(); + cx.run_until_parked(); + assert_eq!(drain_events(&mut events), [EditAgentOutputEvent::Edited]); + assert_eq!( + buffer.read_with(cx, |buffer, _| buffer.snapshot().text()), + "abXcY\ndef\nghi" + ); + + chunks_tx.unbounded_send("").unwrap(); + chunks_tx.unbounded_send("hall").unwrap(); + cx.run_until_parked(); + assert_eq!(drain_events(&mut events), vec![]); + assert_eq!( + buffer.read_with(cx, |buffer, _| buffer.snapshot().text()), + "abXcY\ndef\nghi" + ); + + chunks_tx.unbounded_send("ucinated old").unwrap(); + chunks_tx.unbounded_send("").unwrap(); + cx.run_until_parked(); + assert_eq!( + drain_events(&mut events), + vec![EditAgentOutputEvent::HallucinatedOldText( + "hallucinated old".into() + )] + ); + assert_eq!( + buffer.read_with(cx, |buffer, _| buffer.snapshot().text()), + "abXcY\ndef\nghi" + ); + + chunks_tx.unbounded_send("hallucinated new").unwrap(); + cx.run_until_parked(); + assert_eq!(drain_events(&mut events), vec![]); + assert_eq!( + buffer.read_with(cx, |buffer, _| buffer.snapshot().text()), + "abXcY\ndef\nghi" + ); + + chunks_tx.unbounded_send("gh").unwrap(); + chunks_tx.unbounded_send("i").unwrap(); + chunks_tx.unbounded_send("").unwrap(); + cx.run_until_parked(); + assert_eq!(drain_events(&mut events), vec![]); + assert_eq!( + buffer.read_with(cx, |buffer, _| buffer.snapshot().text()), + "abXcY\ndef\nghi" + ); + + chunks_tx.unbounded_send("GHI").unwrap(); + cx.run_until_parked(); + assert_eq!( + drain_events(&mut events), + vec![EditAgentOutputEvent::Edited] + ); + assert_eq!( + buffer.read_with(cx, |buffer, _| buffer.snapshot().text()), + "abXcY\ndef\nGHI" + ); + + drop(chunks_tx); + apply.await.unwrap(); + assert_eq!( + buffer.read_with(cx, |buffer, _| buffer.snapshot().text()), + "abXcY\ndef\nGHI" + ); + assert_eq!(drain_events(&mut events), vec![]); + + fn drain_events( + stream: &mut UnboundedReceiver, + ) -> Vec { + let mut events = Vec::new(); + while let Ok(Some(event)) = stream.try_next() { + events.push(event); + } + events + } + } + + #[gpui::test] + fn test_resolve_location(cx: &mut App) { + assert_location_resolution( + concat!( + " Lorem\n", + "« ipsum»\n", + " dolor sit amet\n", + " consecteur", + ), + "ipsum", + cx, + ); + + assert_location_resolution( + concat!( + " Lorem\n", + "« ipsum\n", + " dolor sit amet»\n", + " consecteur", + ), + "ipsum\ndolor sit amet", + cx, + ); + + assert_location_resolution( + &" + «fn foo1(a: usize) -> usize { + 40 + }» + + fn foo2(b: usize) -> usize { + 42 + } + " + .unindent(), + "fn foo1(a: usize) -> u32 {\n40\n}", + cx, + ); + + assert_location_resolution( + &" + class Something { + one() { return 1; } + « two() { return 2222; } + three() { return 333; } + four() { return 4444; } + five() { return 5555; } + six() { return 6666; }» + seven() { return 7; } + eight() { return 8; } + } + " + .unindent(), + &" + two() { return 2222; } + four() { return 4444; } + five() { return 5555; } + six() { return 6666; } + " + .unindent(), + cx, + ); + + assert_location_resolution( + &" + use std::ops::Range; + use std::sync::Mutex; + use std::{ + collections::HashMap, + env, + ffi::{OsStr, OsString}, + fs, + io::{BufRead, BufReader}, + mem, + path::{Path, PathBuf}, + process::Command, + sync::LazyLock, + time::SystemTime, + }; + " + .unindent(), + &" + use std::collections::{HashMap, HashSet}; + use std::ffi::{OsStr, OsString}; + use std::fmt::Write as _; + use std::fs; + use std::io::{BufReader, Read, Write}; + use std::mem; + use std::path::{Path, PathBuf}; + use std::process::Command; + use std::sync::Arc; + " + .unindent(), + cx, + ); + + assert_location_resolution( + indoc! {" + impl Foo { + fn new() -> Self { + Self { + subscriptions: vec![ + cx.observe_window_activation(window, |editor, window, cx| { + let active = window.is_window_active(); + editor.blink_manager.update(cx, |blink_manager, cx| { + if active { + blink_manager.enable(cx); + } else { + blink_manager.disable(cx); + } + }); + }), + ]; + } + } + } + "}, + concat!( + " editor.blink_manager.update(cx, |blink_manager, cx| {\n", + " blink_manager.enable(cx);\n", + " });", + ), + cx, + ); + + assert_location_resolution( + indoc! {r#" + let tool = cx + .update(|cx| working_set.tool(&tool_name, cx)) + .map_err(|err| { + anyhow!("Failed to look up tool '{}': {}", tool_name, err) + })?; + + let Some(tool) = tool else { + return Err(anyhow!("Tool '{}' not found", tool_name)); + }; + + let project = project.clone(); + let action_log = action_log.clone(); + let messages = messages.clone(); + let tool_result = cx + .update(|cx| tool.run(invocation.input, &messages, project, action_log, cx)) + .map_err(|err| anyhow!("Failed to start tool '{}': {}", tool_name, err))?; + + tasks.push(tool_result.output); + "#}, + concat!( + "let tool_result = cx\n", + " .update(|cx| tool.run(invocation.input, &messages, project, action_log, cx))\n", + " .output;", + ), + cx, + ); + } + + #[gpui::test(iterations = 100)] + async fn test_indent_new_text_chunks(mut rng: StdRng) { + let chunks = to_random_chunks(&mut rng, " abc\n def\n ghi"); + let new_text_chunks = stream::iter(chunks.iter().enumerate().map(|(index, chunk)| { + Ok(EditParserEvent::NewTextChunk { + chunk: chunk.clone(), + done: index == chunks.len() - 1, + }) + })); + let indented_chunks = + EditAgent::reindent_new_text_chunks(IndentDelta::Spaces(2), new_text_chunks) + .collect::>() + .await; + let new_text = indented_chunks + .into_iter() + .collect::>() + .unwrap(); + assert_eq!(new_text, " abc\n def\n ghi"); + } + + #[gpui::test(iterations = 100)] + async fn test_outdent_new_text_chunks(mut rng: StdRng) { + let chunks = to_random_chunks(&mut rng, "\t\t\t\tabc\n\t\tdef\n\t\t\t\t\t\tghi"); + let new_text_chunks = stream::iter(chunks.iter().enumerate().map(|(index, chunk)| { + Ok(EditParserEvent::NewTextChunk { + chunk: chunk.clone(), + done: index == chunks.len() - 1, + }) + })); + let indented_chunks = + EditAgent::reindent_new_text_chunks(IndentDelta::Tabs(-2), new_text_chunks) + .collect::>() + .await; + let new_text = indented_chunks + .into_iter() + .collect::>() + .unwrap(); + assert_eq!(new_text, "\t\tabc\ndef\n\t\t\t\tghi"); + } + + #[gpui::test(iterations = 100)] + async fn test_random_indents(mut rng: StdRng) { + let len = rng.gen_range(1..=100); + let new_text = util::RandomCharIter::new(&mut rng) + .with_simple_text() + .take(len) + .collect::(); + let new_text = new_text + .split('\n') + .map(|line| format!("{}{}", " ".repeat(rng.gen_range(0..=8)), line)) + .collect::>() + .join("\n"); + let delta = IndentDelta::Spaces(rng.gen_range(-4..=4)); + + let chunks = to_random_chunks(&mut rng, &new_text); + let new_text_chunks = stream::iter(chunks.iter().enumerate().map(|(index, chunk)| { + Ok(EditParserEvent::NewTextChunk { + chunk: chunk.clone(), + done: index == chunks.len() - 1, + }) + })); + let reindented_chunks = EditAgent::reindent_new_text_chunks(delta, new_text_chunks) + .collect::>() + .await; + let actual_reindented_text = reindented_chunks + .into_iter() + .collect::>() + .unwrap(); + let expected_reindented_text = new_text + .split('\n') + .map(|line| { + if let Some(ix) = line.find(|c| c != ' ') { + let new_indent = cmp::max(0, ix as isize + delta.len()) as usize; + format!("{}{}", " ".repeat(new_indent), &line[ix..]) + } else { + line.to_string() + } + }) + .collect::>() + .join("\n"); + assert_eq!(actual_reindented_text, expected_reindented_text); + } + + #[track_caller] + fn assert_location_resolution(text_with_expected_range: &str, query: &str, cx: &mut App) { + let (text, _) = marked_text_ranges(text_with_expected_range, false); + let buffer = cx.new(|cx| Buffer::local(text.clone(), cx)); + let snapshot = buffer.read(cx).snapshot(); + let mut ranges = Vec::new(); + ranges.extend(EditAgent::resolve_location(&snapshot, query)); + let text_with_actual_range = generate_marked_text(&text, &ranges, false); + pretty_assertions::assert_eq!(text_with_actual_range, text_with_expected_range); + } + + fn to_random_chunks(rng: &mut StdRng, input: &str) -> Vec { + let chunk_count = rng.gen_range(1..=cmp::min(input.len(), 50)); + let mut chunk_indices = (0..input.len()).choose_multiple(rng, chunk_count); + chunk_indices.sort(); + chunk_indices.push(input.len()); + + let mut chunks = Vec::new(); + let mut last_ix = 0; + for chunk_ix in chunk_indices { + chunks.push(input[last_ix..chunk_ix].to_string()); + last_ix = chunk_ix; + } + chunks + } + + fn simulate_llm_output( + output: &str, + rng: &mut StdRng, + cx: &mut TestAppContext, + ) -> impl 'static + Send + Stream> { + let executor = cx.executor(); + stream::iter(to_random_chunks(rng, output).into_iter().map(Ok)).then(move |chunk| { + let executor = executor.clone(); + async move { + executor.simulate_random_delay().await; + chunk + } + }) + } + + async fn init_test(cx: &mut TestAppContext) -> EditAgent { + cx.update(settings::init); + cx.update(Project::init_settings); + let project = Project::test(FakeFs::new(cx.executor()), [], cx).await; + let model = Arc::new(FakeLanguageModel::default()); + let action_log = cx.new(|_| ActionLog::new(project)); + EditAgent::new(model, action_log, Templates::new()) + } +} diff --git a/crates/assistant_tools/src/edit_agent/edit_parser.rs b/crates/assistant_tools/src/edit_agent/edit_parser.rs new file mode 100644 index 0000000000..6822b8206a --- /dev/null +++ b/crates/assistant_tools/src/edit_agent/edit_parser.rs @@ -0,0 +1,408 @@ +use derive_more::{Add, AddAssign}; +use smallvec::SmallVec; +use std::{cmp, mem, ops::Range}; + +const OLD_TEXT_END_TAG: &str = ""; +const NEW_TEXT_END_TAG: &str = ""; +const END_TAG_LEN: usize = OLD_TEXT_END_TAG.len(); +const _: () = debug_assert!(OLD_TEXT_END_TAG.len() == NEW_TEXT_END_TAG.len()); + +#[derive(Debug)] +pub enum EditParserEvent { + OldText(String), + NewTextChunk { chunk: String, done: bool }, +} + +#[derive(Clone, Debug, Default, PartialEq, Eq, Add, AddAssign)] +pub struct EditParserMetrics { + pub tags: usize, + pub mismatched_tags: usize, +} + +#[derive(Debug)] +pub struct EditParser { + state: EditParserState, + buffer: String, + metrics: EditParserMetrics, +} + +#[derive(Debug, PartialEq)] +enum EditParserState { + Pending, + WithinOldText, + AfterOldText, + WithinNewText { start: bool }, +} + +impl EditParser { + pub fn new() -> Self { + EditParser { + state: EditParserState::Pending, + buffer: String::new(), + metrics: EditParserMetrics::default(), + } + } + + pub fn push(&mut self, chunk: &str) -> SmallVec<[EditParserEvent; 1]> { + self.buffer.push_str(chunk); + + let mut edit_events = SmallVec::new(); + loop { + match &mut self.state { + EditParserState::Pending => { + if let Some(start) = self.buffer.find("") { + self.buffer.drain(..start + "".len()); + self.state = EditParserState::WithinOldText; + } else { + break; + } + } + EditParserState::WithinOldText => { + if let Some(tag_range) = self.find_end_tag() { + let mut start = 0; + if self.buffer.starts_with('\n') { + start = 1; + } + let mut old_text = self.buffer[start..tag_range.start].to_string(); + if old_text.ends_with('\n') { + old_text.pop(); + } + + self.metrics.tags += 1; + if &self.buffer[tag_range.clone()] != OLD_TEXT_END_TAG { + self.metrics.mismatched_tags += 1; + } + + self.buffer.drain(..tag_range.end); + self.state = EditParserState::AfterOldText; + edit_events.push(EditParserEvent::OldText(old_text)); + } else { + break; + } + } + EditParserState::AfterOldText => { + if let Some(start) = self.buffer.find("") { + self.buffer.drain(..start + "".len()); + self.state = EditParserState::WithinNewText { start: true }; + } else { + break; + } + } + EditParserState::WithinNewText { start } => { + if !self.buffer.is_empty() { + if *start && self.buffer.starts_with('\n') { + self.buffer.remove(0); + } + *start = false; + } + + if let Some(tag_range) = self.find_end_tag() { + let mut chunk = self.buffer[..tag_range.start].to_string(); + if chunk.ends_with('\n') { + chunk.pop(); + } + + self.metrics.tags += 1; + if &self.buffer[tag_range.clone()] != NEW_TEXT_END_TAG { + self.metrics.mismatched_tags += 1; + } + + self.buffer.drain(..tag_range.end); + self.state = EditParserState::Pending; + edit_events.push(EditParserEvent::NewTextChunk { chunk, done: true }); + } else { + let mut end_prefixes = (1..END_TAG_LEN) + .flat_map(|i| [&NEW_TEXT_END_TAG[..i], &OLD_TEXT_END_TAG[..i]]) + .chain(["\n"]); + if end_prefixes.all(|prefix| !self.buffer.ends_with(&prefix)) { + edit_events.push(EditParserEvent::NewTextChunk { + chunk: mem::take(&mut self.buffer), + done: false, + }); + } + break; + } + } + } + } + edit_events + } + + fn find_end_tag(&self) -> Option> { + let old_text_end_tag_ix = self.buffer.find(OLD_TEXT_END_TAG); + let new_text_end_tag_ix = self.buffer.find(NEW_TEXT_END_TAG); + let start_ix = if let Some((old_text_ix, new_text_ix)) = + old_text_end_tag_ix.zip(new_text_end_tag_ix) + { + cmp::min(old_text_ix, new_text_ix) + } else { + old_text_end_tag_ix.or(new_text_end_tag_ix)? + }; + Some(start_ix..start_ix + END_TAG_LEN) + } + + pub fn finish(self) -> EditParserMetrics { + self.metrics + } +} + +#[cfg(test)] +mod tests { + use super::*; + use indoc::indoc; + use rand::prelude::*; + use std::cmp; + + #[gpui::test(iterations = 1000)] + fn test_single_edit(mut rng: StdRng) { + let mut parser = EditParser::new(); + assert_eq!( + parse_random_chunks( + "originalupdated", + &mut parser, + &mut rng + ), + vec![Edit { + old_text: "original".to_string(), + new_text: "updated".to_string(), + }] + ); + assert_eq!( + parser.finish(), + EditParserMetrics { + tags: 2, + mismatched_tags: 0 + } + ); + } + + #[gpui::test(iterations = 1000)] + fn test_multiple_edits(mut rng: StdRng) { + let mut parser = EditParser::new(); + assert_eq!( + parse_random_chunks( + indoc! {" + + first old + first new + second old + second new + + "}, + &mut parser, + &mut rng + ), + vec![ + Edit { + old_text: "first old".to_string(), + new_text: "first new".to_string(), + }, + Edit { + old_text: "second old".to_string(), + new_text: "second new".to_string(), + }, + ] + ); + assert_eq!( + parser.finish(), + EditParserMetrics { + tags: 4, + mismatched_tags: 0 + } + ); + } + + #[gpui::test(iterations = 1000)] + fn test_edits_with_extra_text(mut rng: StdRng) { + let mut parser = EditParser::new(); + assert_eq!( + parse_random_chunks( + indoc! {" + ignore this + contentextra stuffupdated contenttrailing data + more text second item + middle textmodified second itemend + third caseimproved third case with trailing text + "}, + &mut parser, + &mut rng + ), + vec![ + Edit { + old_text: "content".to_string(), + new_text: "updated content".to_string(), + }, + Edit { + old_text: "second item".to_string(), + new_text: "modified second item".to_string(), + }, + Edit { + old_text: "third case".to_string(), + new_text: "improved third case".to_string(), + }, + ] + ); + assert_eq!( + parser.finish(), + EditParserMetrics { + tags: 6, + mismatched_tags: 0 + } + ); + } + + #[gpui::test(iterations = 1000)] + fn test_nested_tags(mut rng: StdRng) { + let mut parser = EditParser::new(); + assert_eq!( + parse_random_chunks( + "code with nested elementsnew content", + &mut parser, + &mut rng + ), + vec![Edit { + old_text: "code with nested elements".to_string(), + new_text: "new content".to_string(), + }] + ); + assert_eq!( + parser.finish(), + EditParserMetrics { + tags: 2, + mismatched_tags: 0 + } + ); + } + + #[gpui::test(iterations = 1000)] + fn test_empty_old_and_new_text(mut rng: StdRng) { + let mut parser = EditParser::new(); + assert_eq!( + parse_random_chunks( + "", + &mut parser, + &mut rng + ), + vec![Edit { + old_text: "".to_string(), + new_text: "".to_string(), + }] + ); + assert_eq!( + parser.finish(), + EditParserMetrics { + tags: 2, + mismatched_tags: 0 + } + ); + } + + #[gpui::test(iterations = 100)] + fn test_multiline_content(mut rng: StdRng) { + let mut parser = EditParser::new(); + assert_eq!( + parse_random_chunks( + "line1\nline2\nline3line1\nmodified line2\nline3", + &mut parser, + &mut rng + ), + vec![Edit { + old_text: "line1\nline2\nline3".to_string(), + new_text: "line1\nmodified line2\nline3".to_string(), + }] + ); + assert_eq!( + parser.finish(), + EditParserMetrics { + tags: 2, + mismatched_tags: 0 + } + ); + } + + #[gpui::test(iterations = 1000)] + fn test_mismatched_tags(mut rng: StdRng) { + let mut parser = EditParser::new(); + assert_eq!( + parse_random_chunks( + // Reduced from an actual Sonnet 3.7 output + indoc! {" + + a + b + c + + + a + B + c + + + d + e + f + + + D + e + F + + "}, + &mut parser, + &mut rng + ), + vec![ + Edit { + old_text: "a\nb\nc".to_string(), + new_text: "a\nB\nc".to_string(), + }, + Edit { + old_text: "d\ne\nf".to_string(), + new_text: "D\ne\nF".to_string(), + } + ] + ); + assert_eq!( + parser.finish(), + EditParserMetrics { + tags: 4, + mismatched_tags: 4 + } + ); + } + + #[derive(Default, Debug, PartialEq, Eq)] + struct Edit { + old_text: String, + new_text: String, + } + + fn parse_random_chunks(input: &str, parser: &mut EditParser, rng: &mut StdRng) -> Vec { + let chunk_count = rng.gen_range(1..=cmp::min(input.len(), 50)); + let mut chunk_indices = (0..input.len()).choose_multiple(rng, chunk_count); + chunk_indices.sort(); + chunk_indices.push(input.len()); + + let mut pending_edit = Edit::default(); + let mut edits = Vec::new(); + let mut last_ix = 0; + for chunk_ix in chunk_indices { + for event in parser.push(&input[last_ix..chunk_ix]) { + match event { + EditParserEvent::OldText(old_text) => { + pending_edit.old_text = old_text; + } + EditParserEvent::NewTextChunk { chunk, done } => { + pending_edit.new_text.push_str(&chunk); + if done { + edits.push(pending_edit); + pending_edit = Edit::default(); + } + } + } + } + last_ix = chunk_ix; + } + edits + } +} diff --git a/crates/assistant_tools/src/edit_agent/evals.rs b/crates/assistant_tools/src/edit_agent/evals.rs new file mode 100644 index 0000000000..d22f6bda57 --- /dev/null +++ b/crates/assistant_tools/src/edit_agent/evals.rs @@ -0,0 +1,889 @@ +use super::*; +use crate::{ + ReadFileToolInput, grep_tool::GrepToolInput, + streaming_edit_file_tool::StreamingEditFileToolInput, +}; +use Role::*; +use anyhow::{Context, anyhow}; +use client::{Client, UserStore}; +use collections::HashMap; +use fs::FakeFs; +use gpui::{AppContext, TestAppContext}; +use indoc::indoc; +use language_model::{ + LanguageModelRegistry, LanguageModelToolResult, LanguageModelToolUse, LanguageModelToolUseId, +}; +use project::Project; +use rand::prelude::*; +use reqwest_client::ReqwestClient; +use serde_json::json; +use std::{ + cmp::Reverse, + fmt::{self, Display}, + io::Write as _, + sync::mpsc, +}; +use util::path; + +#[test] +#[cfg_attr(not(feature = "eval"), ignore)] +fn eval_extract_handle_command_output() { + let input_file_path = "root/blame.rs"; + let input_file_content = include_str!("evals/fixtures/extract_handle_command_output/before.rs"); + let output_file_content = include_str!("evals/fixtures/extract_handle_command_output/after.rs"); + let edit_description = "Extract `handle_command_output` method from `run_git_blame`."; + eval( + 100, + 0.95, + EvalInput { + conversation: vec![ + message( + User, + [text(indoc! {" + Read the `{input_file_path}` file and extract a method in + the final stanza of `run_git_blame` to deal with command failures, + call it `handle_command_output` and take the std::process::Output as the only parameter. + + Add it right next to `run_git_blame` and copy it verbatim from `run_git_blame`. + "})], + ), + message( + Assistant, + [tool_use( + "tool_1", + "read_file", + ReadFileToolInput { + path: input_file_path.into(), + start_line: None, + end_line: None, + }, + )], + ), + message( + User, + [tool_result("tool_1", "read_file", input_file_content)], + ), + message( + Assistant, + [tool_use( + "tool_2", + "edit_file", + StreamingEditFileToolInput { + display_description: edit_description.into(), + path: input_file_path.into(), + }, + )], + ), + ], + input_path: input_file_path.into(), + input_content: input_file_content.into(), + edit_description: edit_description.into(), + assertion: EvalAssertion::AssertEqual(output_file_content.into()), + }, + ); +} + +#[test] +#[cfg_attr(not(feature = "eval"), ignore)] +fn eval_delete_run_git_blame() { + let input_file_path = "root/blame.rs"; + let input_file_content = include_str!("evals/fixtures/delete_run_git_blame/before.rs"); + let output_file_content = include_str!("evals/fixtures/delete_run_git_blame/after.rs"); + let edit_description = "Delete the `run_git_blame` function."; + eval( + 100, + 0.95, + EvalInput { + conversation: vec![ + message( + User, + [text(indoc! {" + Read the `{input_file_path}` file and delete `run_git_blame`. Just that + one function, not its usages. + "})], + ), + message( + Assistant, + [tool_use( + "tool_1", + "read_file", + ReadFileToolInput { + path: input_file_path.into(), + start_line: None, + end_line: None, + }, + )], + ), + message( + User, + [tool_result("tool_1", "read_file", input_file_content)], + ), + message( + Assistant, + [tool_use( + "tool_2", + "edit_file", + StreamingEditFileToolInput { + display_description: edit_description.into(), + path: input_file_path.into(), + }, + )], + ), + ], + input_path: input_file_path.into(), + input_content: input_file_content.into(), + edit_description: edit_description.into(), + assertion: EvalAssertion::AssertEqual(output_file_content.into()), + }, + ); +} + +#[test] +#[cfg_attr(not(feature = "eval"), ignore)] +fn eval_use_wasi_sdk_in_compile_parser_to_wasm() { + let input_file_path = "root/lib.rs"; + let input_file_content = + include_str!("evals/fixtures/use_wasi_sdk_in_compile_parser_to_wasm/before.rs"); + let edit_description = "Update compile_parser_to_wasm to use wasi-sdk instead of emscripten"; + eval( + 100, + 0.95, + EvalInput { + conversation: vec![ + message( + User, + [text(indoc! {" + Read the `{input_file_path}` file and change `compile_parser_to_wasm` to use `wasi-sdk` instead of emscripten. + Use `ureq` to download the SDK for the current platform and architecture. + Extract the archive into a sibling of `lib` inside the `tree-sitter` directory in the cache_dir. + Compile the parser to wasm using the `bin/clang` executable (or `bin/clang.exe` on windows) + that's inside of the archive. + Don't re-download the SDK if that executable already exists. + + Use these clang flags: -fPIC -shared -Os -Wl,--export=tree_sitter_{language_name} + + Here are the available wasi-sdk assets: + - wasi-sdk-25.0-x86_64-macos.tar.gz + - wasi-sdk-25.0-arm64-macos.tar.gz + - wasi-sdk-25.0-x86_64-linux.tar.gz + - wasi-sdk-25.0-arm64-linux.tar.gz + - wasi-sdk-25.0-x86_64-linux.tar.gz + - wasi-sdk-25.0-arm64-linux.tar.gz + - wasi-sdk-25.0-x86_64-windows.tar.gz + "})], + ), + message( + Assistant, + [tool_use( + "tool_1", + "read_file", + ReadFileToolInput { + path: input_file_path.into(), + start_line: Some(971), + end_line: Some(1050), + }, + )], + ), + message( + User, + [tool_result( + "tool_1", + "read_file", + lines(input_file_content, 971..1050), + )], + ), + message( + Assistant, + [tool_use( + "tool_2", + "read_file", + ReadFileToolInput { + path: input_file_path.into(), + start_line: Some(1050), + end_line: Some(1100), + }, + )], + ), + message( + User, + [tool_result( + "tool_2", + "read_file", + lines(input_file_content, 1050..1100), + )], + ), + message( + Assistant, + [tool_use( + "tool_3", + "read_file", + ReadFileToolInput { + path: input_file_path.into(), + start_line: Some(1100), + end_line: Some(1150), + }, + )], + ), + message( + User, + [tool_result( + "tool_3", + "read_file", + lines(input_file_content, 1100..1150), + )], + ), + message( + Assistant, + [tool_use( + "tool_4", + "edit_file", + StreamingEditFileToolInput { + display_description: edit_description.into(), + path: input_file_path.into(), + }, + )], + ), + ], + input_path: input_file_path.into(), + input_content: input_file_content.into(), + edit_description: edit_description.into(), + assertion: EvalAssertion::JudgeDiff(indoc! {" + - The compile_parser_to_wasm method has been changed to use wasi-sdk + - ureq is used to download the SDK for current platform and architecture + "}), + }, + ); +} + +#[test] +#[cfg_attr(not(feature = "eval"), ignore)] +fn eval_disable_cursor_blinking() { + let input_file_path = "root/editor.rs"; + let input_file_content = include_str!("evals/fixtures/disable_cursor_blinking/before.rs"); + let output_file_content = include_str!("evals/fixtures/disable_cursor_blinking/after.rs"); + let edit_description = "Comment out the call to `BlinkManager::enable`"; + eval( + 100, + 0.6, // TODO: make this eval better + EvalInput { + conversation: vec![ + message(User, [text("Let's research how to cursor blinking works.")]), + message( + Assistant, + [tool_use( + "tool_1", + "grep", + GrepToolInput { + regex: "blink".into(), + include_pattern: None, + offset: 0, + case_sensitive: false, + }, + )], + ), + message( + User, + [tool_result( + "tool_1", + "grep", + [ + lines(input_file_content, 100..400), + lines(input_file_content, 800..1300), + lines(input_file_content, 1600..2000), + lines(input_file_content, 5000..5500), + lines(input_file_content, 8000..9000), + lines(input_file_content, 18455..18470), + lines(input_file_content, 20000..20500), + lines(input_file_content, 21000..21300), + ] + .join("Match found:\n\n"), + )], + ), + message( + User, + [text(indoc! {" + Comment out the lines that interact with the BlinkManager. + Keep the outer `update` blocks, but comments everything that's inside (including if statements). + Don't add additional comments. + "})], + ), + message( + Assistant, + [tool_use( + "tool_4", + "edit_file", + StreamingEditFileToolInput { + display_description: edit_description.into(), + path: input_file_path.into(), + }, + )], + ), + ], + input_path: input_file_path.into(), + input_content: input_file_content.into(), + edit_description: edit_description.into(), + assertion: EvalAssertion::AssertEqual(output_file_content.into()), + }, + ); +} + +#[test] +#[cfg_attr(not(feature = "eval"), ignore)] +fn eval_from_pixels_constructor() { + let input_file_path = "root/canvas.rs"; + let input_file_content = include_str!("evals/fixtures/from_pixels_constructor/before.rs"); + let edit_description = "Implement from_pixels constructor and add tests."; + eval( + 100, + 0.95, + EvalInput { + conversation: vec![ + message( + User, + [text(indoc! {" + Introduce a new `from_pixels` constructor in Canvas and + also add tests for it in the same file. + "})], + ), + message( + Assistant, + [tool_use( + "tool_1", + "read_file", + ReadFileToolInput { + path: input_file_path.into(), + start_line: None, + end_line: None, + }, + )], + ), + message( + User, + [tool_result("tool_1", "read_file", input_file_content)], + ), + message( + Assistant, + [tool_use( + "tool_2", + "grep", + GrepToolInput { + regex: "mod\\s+tests".into(), + include_pattern: Some("font-kit/src/canvas.rs".into()), + offset: 0, + case_sensitive: false, + }, + )], + ), + message(User, [tool_result("tool_2", "grep", "No matches found")]), + message( + Assistant, + [tool_use( + "tool_3", + "grep", + GrepToolInput { + regex: "mod\\s+tests".into(), + include_pattern: Some("font-kit/src/**/*.rs".into()), + offset: 0, + case_sensitive: false, + }, + )], + ), + message(User, [tool_result("tool_3", "grep", "No matches found")]), + message( + Assistant, + [tool_use( + "tool_4", + "grep", + GrepToolInput { + regex: "#\\[test\\]".into(), + include_pattern: Some("font-kit/src/**/*.rs".into()), + offset: 0, + case_sensitive: false, + }, + )], + ), + message( + User, + [tool_result( + "tool_4", + "grep", + indoc! {" + Found 6 matches: + + ## Matches in font-kit/src/loaders/core_text.rs + + ### mod test › L926-936 + ``` + mod test { + use super::Font; + use crate::properties::{Stretch, Weight}; + + #[cfg(feature = \"source\")] + use crate::source::SystemSource; + + static TEST_FONT_POSTSCRIPT_NAME: &'static str = \"ArialMT\"; + + #[cfg(feature = \"source\")] + #[test] + ``` + + 55 lines remaining in ancestor node. Read the file to see all. + + ### mod test › L947-951 + ``` + } + + #[test] + fn test_core_text_to_css_font_weight() { + // Exact matches + ``` + + ### mod test › L959-963 + ``` + } + + #[test] + fn test_core_text_to_css_font_stretch() { + // Exact matches + ``` + + ## Matches in font-kit/src/loaders/freetype.rs + + ### mod test › L1238-1248 + ``` + mod test { + use crate::loaders::freetype::Font; + + static PCF_FONT_PATH: &str = \"resources/tests/times-roman-pcf/timR12.pcf\"; + static PCF_FONT_POSTSCRIPT_NAME: &str = \"Times-Roman\"; + + #[test] + fn get_pcf_postscript_name() { + let font = Font::from_path(PCF_FONT_PATH, 0).unwrap(); + assert_eq!(font.postscript_name().unwrap(), PCF_FONT_POSTSCRIPT_NAME); + } + ``` + + 1 lines remaining in ancestor node. Read the file to see all. + + ## Matches in font-kit/src/sources/core_text.rs + + ### mod test › L265-275 + ``` + mod test { + use crate::properties::{Stretch, Weight}; + + #[test] + fn test_css_to_core_text_font_weight() { + // Exact matches + assert_eq!(super::css_to_core_text_font_weight(Weight(100.0)), -0.7); + assert_eq!(super::css_to_core_text_font_weight(Weight(400.0)), 0.0); + assert_eq!(super::css_to_core_text_font_weight(Weight(700.0)), 0.4); + assert_eq!(super::css_to_core_text_font_weight(Weight(900.0)), 0.8); + + ``` + + 27 lines remaining in ancestor node. Read the file to see all. + + ### mod test › L278-282 + ``` + } + + #[test] + fn test_css_to_core_text_font_stretch() { + // Exact matches + ``` + "}, + )], + ), + message( + Assistant, + [tool_use( + "tool_5", + "edit_file", + StreamingEditFileToolInput { + display_description: edit_description.into(), + path: input_file_path.into(), + }, + )], + ), + ], + input_path: input_file_path.into(), + input_content: input_file_content.into(), + edit_description: edit_description.into(), + assertion: EvalAssertion::JudgeDiff(indoc! {" + - The diff contains a new `from_pixels` constructor + - The diff contains new tests for the `from_pixels` constructor + "}), + }, + ); +} + +fn message( + role: Role, + contents: impl IntoIterator, +) -> LanguageModelRequestMessage { + LanguageModelRequestMessage { + role, + content: contents.into_iter().collect(), + cache: false, + } +} + +fn text(text: impl Into) -> MessageContent { + MessageContent::Text(text.into()) +} + +fn lines(input: &str, range: Range) -> String { + input + .lines() + .skip(range.start) + .take(range.len()) + .collect::>() + .join("\n") +} + +fn tool_use( + id: impl Into>, + name: impl Into>, + input: impl Serialize, +) -> MessageContent { + MessageContent::ToolUse(LanguageModelToolUse { + id: LanguageModelToolUseId::from(id.into()), + name: name.into(), + raw_input: serde_json::to_string_pretty(&input).unwrap(), + input: serde_json::to_value(input).unwrap(), + is_input_complete: true, + }) +} + +fn tool_result( + id: impl Into>, + name: impl Into>, + result: impl Into>, +) -> MessageContent { + MessageContent::ToolResult(LanguageModelToolResult { + tool_use_id: LanguageModelToolUseId::from(id.into()), + tool_name: name.into(), + is_error: false, + content: result.into(), + }) +} + +#[derive(Clone)] +struct EvalInput { + conversation: Vec, + input_path: PathBuf, + input_content: String, + edit_description: String, + assertion: EvalAssertion, +} + +fn eval(iterations: usize, expected_pass_ratio: f32, mut eval: EvalInput) { + let mut evaluated_count = 0; + report_progress(evaluated_count, iterations); + + let (tx, rx) = mpsc::channel(); + + // Cache the last message in the conversation, and run one instance of the eval so that + // all the next ones are cached. + eval.conversation.last_mut().unwrap().cache = true; + run_eval(eval.clone(), tx.clone()); + + let executor = gpui::background_executor(); + for _ in 1..iterations { + let eval = eval.clone(); + let tx = tx.clone(); + executor.spawn(async move { run_eval(eval, tx) }).detach(); + } + drop(tx); + + let mut failed_count = 0; + let mut failed_evals = HashMap::default(); + let mut errored_evals = HashMap::default(); + let mut eval_outputs = Vec::new(); + let mut cumulative_parser_metrics = EditParserMetrics::default(); + while let Ok(output) = rx.recv() { + match output { + Ok(output) => { + cumulative_parser_metrics += output.edit_output._parser_metrics.clone(); + eval_outputs.push(output.clone()); + if output.assertion.score < 80 { + failed_count += 1; + failed_evals + .entry(output.buffer_text.clone()) + .or_insert(Vec::new()) + .push(output); + } + } + Err(error) => { + failed_count += 1; + *errored_evals.entry(format!("{:?}", error)).or_insert(0) += 1; + } + } + + evaluated_count += 1; + report_progress(evaluated_count, iterations); + } + + let actual_pass_ratio = (iterations - failed_count) as f32 / iterations as f32; + println!("Actual pass ratio: {}\n", actual_pass_ratio); + if actual_pass_ratio < expected_pass_ratio { + let mut errored_evals = errored_evals.into_iter().collect::>(); + errored_evals.sort_by_key(|(_, count)| Reverse(*count)); + for (error, count) in errored_evals { + println!("Eval errored {} times. Error: {}", count, error); + } + + let mut failed_evals = failed_evals.into_iter().collect::>(); + failed_evals.sort_by_key(|(_, evals)| Reverse(evals.len())); + for (_buffer_output, failed_evals) in failed_evals { + let eval_output = failed_evals.first().unwrap(); + println!("Eval failed {} times", failed_evals.len()); + println!("{}", eval_output); + } + + panic!( + "Actual pass ratio: {}\nExpected pass ratio: {}", + actual_pass_ratio, expected_pass_ratio + ); + } + + let mismatched_tag_ratio = + cumulative_parser_metrics.mismatched_tags as f32 / cumulative_parser_metrics.tags as f32; + if mismatched_tag_ratio > 0.02 { + for eval_output in eval_outputs { + println!("{}", eval_output); + } + panic!("Too many mismatched tags: {:?}", cumulative_parser_metrics); + } +} + +fn run_eval(eval: EvalInput, tx: mpsc::Sender>) { + let dispatcher = gpui::TestDispatcher::new(StdRng::from_entropy()); + let mut cx = TestAppContext::build(dispatcher, None); + let output = cx.executor().block_test(async { + let test = EditAgentTest::new(&mut cx).await; + test.eval(eval, &mut cx).await + }); + tx.send(output).unwrap(); +} + +#[derive(Clone)] +struct EvalOutput { + assertion: EvalAssertionResult, + buffer_text: String, + edit_output: EditAgentOutput, + diff: String, +} + +impl Display for EvalOutput { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + writeln!(f, "Score: {:?}", self.assertion.score)?; + if let Some(message) = self.assertion.message.as_ref() { + writeln!(f, "Message: {}", message)?; + } + + writeln!(f, "Diff:\n{}", self.diff)?; + + writeln!( + f, + "Parser Metrics:\n{:#?}", + self.edit_output._parser_metrics + )?; + writeln!(f, "Raw Edits:\n{}", self.edit_output._raw_edits)?; + Ok(()) + } +} + +fn report_progress(evaluated_count: usize, iterations: usize) { + print!("\r\x1b[KEvaluated {}/{}", evaluated_count, iterations); + std::io::stdout().flush().unwrap(); +} + +struct EditAgentTest { + agent: EditAgent, + project: Entity, + judge_model: Arc, +} + +impl EditAgentTest { + async fn new(cx: &mut TestAppContext) -> Self { + cx.executor().allow_parking(); + cx.update(settings::init); + cx.update(Project::init_settings); + cx.update(language::init); + cx.update(gpui_tokio::init); + cx.update(client::init_settings); + + let fs = FakeFs::new(cx.executor().clone()); + fs.insert_tree("/root", json!({})).await; + let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; + let (agent_model, judge_model) = cx + .update(|cx| { + let http_client = ReqwestClient::user_agent("agent tests").unwrap(); + cx.set_http_client(Arc::new(http_client)); + + let client = Client::production(cx); + let user_store = cx.new(|cx| UserStore::new(client.clone(), cx)); + language_model::init(client.clone(), cx); + language_models::init(user_store.clone(), client.clone(), fs.clone(), cx); + + cx.spawn(async move |cx| { + let agent_model = + Self::load_model("anthropic", "claude-3-7-sonnet-latest", cx).await; + let judge_model = + Self::load_model("anthropic", "claude-3-7-sonnet-latest", cx).await; + (agent_model.unwrap(), judge_model.unwrap()) + }) + }) + .await; + let action_log = cx.new(|_| ActionLog::new(project.clone())); + + Self { + agent: EditAgent::new(agent_model, action_log, Templates::new()), + project, + judge_model, + } + } + + async fn load_model( + provider: &str, + id: &str, + cx: &mut AsyncApp, + ) -> Result> { + let (provider, model) = cx.update(|cx| { + let models = LanguageModelRegistry::read_global(cx); + let model = models + .available_models(cx) + .find(|model| model.provider_id().0 == provider && model.id().0 == id) + .unwrap(); + let provider = models.provider(&model.provider_id()).unwrap(); + (provider, model) + })?; + cx.update(|cx| provider.authenticate(cx))?.await?; + Ok(model) + } + + async fn eval(&self, eval: EvalInput, cx: &mut TestAppContext) -> Result { + let path = self + .project + .read_with(cx, |project, cx| { + project.find_project_path(eval.input_path, cx) + }) + .unwrap(); + let buffer = self + .project + .update(cx, |project, cx| project.open_buffer(path, cx)) + .await + .unwrap(); + buffer.update(cx, |buffer, cx| { + buffer.set_text(eval.input_content.clone(), cx) + }); + let (edit_output, _events) = self.agent.edit( + buffer.clone(), + eval.edit_description, + eval.conversation, + &mut cx.to_async(), + ); + let edit_output = edit_output.await?; + let buffer_text = buffer.read_with(cx, |buffer, _| buffer.text()); + let actual_diff = language::unified_diff(&eval.input_content, &buffer_text); + let assertion = match eval.assertion { + EvalAssertion::AssertEqual(expected_output) => EvalAssertionResult { + score: if strip_empty_lines(&buffer_text) == strip_empty_lines(&expected_output) { + 100 + } else { + 0 + }, + message: None, + }, + EvalAssertion::JudgeDiff(assertions) => self + .judge_diff(&actual_diff, assertions, &cx.to_async()) + .await + .context("failed comparing diffs")?, + }; + + Ok(EvalOutput { + assertion, + diff: actual_diff, + buffer_text, + edit_output, + }) + } + + async fn judge_diff( + &self, + diff: &str, + assertions: &'static str, + cx: &AsyncApp, + ) -> Result { + let prompt = DiffJudgeTemplate { + diff: diff.to_string(), + assertions, + } + .render(&self.agent.templates) + .unwrap(); + + let request = LanguageModelRequest { + messages: vec![LanguageModelRequestMessage { + role: Role::User, + content: vec![prompt.into()], + cache: false, + }], + ..Default::default() + }; + let mut response = self.judge_model.stream_completion_text(request, cx).await?; + let mut output = String::new(); + while let Some(chunk) = response.stream.next().await { + let chunk = chunk?; + output.push_str(&chunk); + } + + // Parse the score from the response + let re = regex::Regex::new(r"(\d+)").unwrap(); + if let Some(captures) = re.captures(&output) { + if let Some(score_match) = captures.get(1) { + let score = score_match.as_str().parse().unwrap_or(0); + return Ok(EvalAssertionResult { + score, + message: Some(output), + }); + } + } + + Err(anyhow!( + "No score found in response. Raw output: {}", + output + )) + } +} + +#[derive(Clone, Debug, Eq, PartialEq, Hash)] +enum EvalAssertion { + AssertEqual(String), + JudgeDiff(&'static str), +} + +#[derive(Clone, Debug, Eq, PartialEq, Hash)] +struct EvalAssertionResult { + score: usize, + message: Option, +} + +#[derive(Serialize)] +pub struct DiffJudgeTemplate { + diff: String, + assertions: &'static str, +} + +impl Template for DiffJudgeTemplate { + const TEMPLATE_NAME: &'static str = "diff_judge.hbs"; +} + +fn strip_empty_lines(text: &str) -> String { + text.lines() + .filter(|line| !line.trim().is_empty()) + .collect::>() + .join("\n") +} diff --git a/crates/assistant_tools/src/edit_agent/evals/fixtures/delete_run_git_blame/after.rs b/crates/assistant_tools/src/edit_agent/evals/fixtures/delete_run_git_blame/after.rs new file mode 100644 index 0000000000..1951e17bfa --- /dev/null +++ b/crates/assistant_tools/src/edit_agent/evals/fixtures/delete_run_git_blame/after.rs @@ -0,0 +1,328 @@ +use crate::commit::get_messages; +use crate::{GitRemote, Oid}; +use anyhow::{Context as _, Result, anyhow}; +use collections::{HashMap, HashSet}; +use futures::AsyncWriteExt; +use gpui::SharedString; +use serde::{Deserialize, Serialize}; +use std::process::Stdio; +use std::{ops::Range, path::Path}; +use text::Rope; +use time::OffsetDateTime; +use time::UtcOffset; +use time::macros::format_description; + +pub use git2 as libgit; + +#[derive(Debug, Clone, Default)] +pub struct Blame { + pub entries: Vec, + pub messages: HashMap, + pub remote_url: Option, +} + +#[derive(Clone, Debug, Default)] +pub struct ParsedCommitMessage { + pub message: SharedString, + pub permalink: Option, + pub pull_request: Option, + pub remote: Option, +} + +impl Blame { + pub async fn for_path( + git_binary: &Path, + working_directory: &Path, + path: &Path, + content: &Rope, + remote_url: Option, + ) -> Result { + let output = run_git_blame(git_binary, working_directory, path, content).await?; + let mut entries = parse_git_blame(&output)?; + entries.sort_unstable_by(|a, b| a.range.start.cmp(&b.range.start)); + + let mut unique_shas = HashSet::default(); + + for entry in entries.iter_mut() { + unique_shas.insert(entry.sha); + } + + let shas = unique_shas.into_iter().collect::>(); + let messages = get_messages(working_directory, &shas) + .await + .context("failed to get commit messages")?; + + Ok(Self { + entries, + messages, + remote_url, + }) + } +} + +const GIT_BLAME_NO_COMMIT_ERROR: &str = "fatal: no such ref: HEAD"; +const GIT_BLAME_NO_PATH: &str = "fatal: no such path"; + +#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)] +pub struct BlameEntry { + pub sha: Oid, + + pub range: Range, + + pub original_line_number: u32, + + pub author: Option, + pub author_mail: Option, + pub author_time: Option, + pub author_tz: Option, + + pub committer_name: Option, + pub committer_email: Option, + pub committer_time: Option, + pub committer_tz: Option, + + pub summary: Option, + + pub previous: Option, + pub filename: String, +} + +impl BlameEntry { + // Returns a BlameEntry by parsing the first line of a `git blame --incremental` + // entry. The line MUST have this format: + // + // <40-byte-hex-sha1> + fn new_from_blame_line(line: &str) -> Result { + let mut parts = line.split_whitespace(); + + let sha = parts + .next() + .and_then(|line| line.parse::().ok()) + .ok_or_else(|| anyhow!("failed to parse sha"))?; + + let original_line_number = parts + .next() + .and_then(|line| line.parse::().ok()) + .ok_or_else(|| anyhow!("Failed to parse original line number"))?; + let final_line_number = parts + .next() + .and_then(|line| line.parse::().ok()) + .ok_or_else(|| anyhow!("Failed to parse final line number"))?; + + let line_count = parts + .next() + .and_then(|line| line.parse::().ok()) + .ok_or_else(|| anyhow!("Failed to parse final line number"))?; + + let start_line = final_line_number.saturating_sub(1); + let end_line = start_line + line_count; + let range = start_line..end_line; + + Ok(Self { + sha, + range, + original_line_number, + ..Default::default() + }) + } + + pub fn author_offset_date_time(&self) -> Result { + if let (Some(author_time), Some(author_tz)) = (self.author_time, &self.author_tz) { + let format = format_description!("[offset_hour][offset_minute]"); + let offset = UtcOffset::parse(author_tz, &format)?; + let date_time_utc = OffsetDateTime::from_unix_timestamp(author_time)?; + + Ok(date_time_utc.to_offset(offset)) + } else { + // Directly return current time in UTC if there's no committer time or timezone + Ok(time::OffsetDateTime::now_utc()) + } + } +} + +// parse_git_blame parses the output of `git blame --incremental`, which returns +// all the blame-entries for a given path incrementally, as it finds them. +// +// Each entry *always* starts with: +// +// <40-byte-hex-sha1> +// +// Each entry *always* ends with: +// +// filename +// +// Line numbers are 1-indexed. +// +// A `git blame --incremental` entry looks like this: +// +// 6ad46b5257ba16d12c5ca9f0d4900320959df7f4 2 2 1 +// author Joe Schmoe +// author-mail +// author-time 1709741400 +// author-tz +0100 +// committer Joe Schmoe +// committer-mail +// committer-time 1709741400 +// committer-tz +0100 +// summary Joe's cool commit +// previous 486c2409237a2c627230589e567024a96751d475 index.js +// filename index.js +// +// If the entry has the same SHA as an entry that was already printed then no +// signature information is printed: +// +// 6ad46b5257ba16d12c5ca9f0d4900320959df7f4 3 4 1 +// previous 486c2409237a2c627230589e567024a96751d475 index.js +// filename index.js +// +// More about `--incremental` output: https://mirrors.edge.kernel.org/pub/software/scm/git/docs/git-blame.html +fn parse_git_blame(output: &str) -> Result> { + let mut entries: Vec = Vec::new(); + let mut index: HashMap = HashMap::default(); + + let mut current_entry: Option = None; + + for line in output.lines() { + let mut done = false; + + match &mut current_entry { + None => { + let mut new_entry = BlameEntry::new_from_blame_line(line)?; + + if let Some(existing_entry) = index + .get(&new_entry.sha) + .and_then(|slot| entries.get(*slot)) + { + new_entry.author.clone_from(&existing_entry.author); + new_entry + .author_mail + .clone_from(&existing_entry.author_mail); + new_entry.author_time = existing_entry.author_time; + new_entry.author_tz.clone_from(&existing_entry.author_tz); + new_entry + .committer_name + .clone_from(&existing_entry.committer_name); + new_entry + .committer_email + .clone_from(&existing_entry.committer_email); + new_entry.committer_time = existing_entry.committer_time; + new_entry + .committer_tz + .clone_from(&existing_entry.committer_tz); + new_entry.summary.clone_from(&existing_entry.summary); + } + + current_entry.replace(new_entry); + } + Some(entry) => { + let Some((key, value)) = line.split_once(' ') else { + continue; + }; + let is_committed = !entry.sha.is_zero(); + match key { + "filename" => { + entry.filename = value.into(); + done = true; + } + "previous" => entry.previous = Some(value.into()), + + "summary" if is_committed => entry.summary = Some(value.into()), + "author" if is_committed => entry.author = Some(value.into()), + "author-mail" if is_committed => entry.author_mail = Some(value.into()), + "author-time" if is_committed => { + entry.author_time = Some(value.parse::()?) + } + "author-tz" if is_committed => entry.author_tz = Some(value.into()), + + "committer" if is_committed => entry.committer_name = Some(value.into()), + "committer-mail" if is_committed => entry.committer_email = Some(value.into()), + "committer-time" if is_committed => { + entry.committer_time = Some(value.parse::()?) + } + "committer-tz" if is_committed => entry.committer_tz = Some(value.into()), + _ => {} + } + } + }; + + if done { + if let Some(entry) = current_entry.take() { + index.insert(entry.sha, entries.len()); + + // We only want annotations that have a commit. + if !entry.sha.is_zero() { + entries.push(entry); + } + } + } + } + + Ok(entries) +} + +#[cfg(test)] +mod tests { + use std::path::PathBuf; + + use super::BlameEntry; + use super::parse_git_blame; + + fn read_test_data(filename: &str) -> String { + let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + path.push("test_data"); + path.push(filename); + + std::fs::read_to_string(&path) + .unwrap_or_else(|_| panic!("Could not read test data at {:?}. Is it generated?", path)) + } + + fn assert_eq_golden(entries: &Vec, golden_filename: &str) { + let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + path.push("test_data"); + path.push("golden"); + path.push(format!("{}.json", golden_filename)); + + let mut have_json = + serde_json::to_string_pretty(&entries).expect("could not serialize entries to JSON"); + // We always want to save with a trailing newline. + have_json.push('\n'); + + let update = std::env::var("UPDATE_GOLDEN") + .map(|val| val.eq_ignore_ascii_case("true")) + .unwrap_or(false); + + if update { + std::fs::create_dir_all(path.parent().unwrap()) + .expect("could not create golden test data directory"); + std::fs::write(&path, have_json).expect("could not write out golden data"); + } else { + let want_json = + std::fs::read_to_string(&path).unwrap_or_else(|_| { + panic!("could not read golden test data file at {:?}. Did you run the test with UPDATE_GOLDEN=true before?", path); + }).replace("\r\n", "\n"); + + pretty_assertions::assert_eq!(have_json, want_json, "wrong blame entries"); + } + } + + #[test] + fn test_parse_git_blame_not_committed() { + let output = read_test_data("blame_incremental_not_committed"); + let entries = parse_git_blame(&output).unwrap(); + assert_eq_golden(&entries, "blame_incremental_not_committed"); + } + + #[test] + fn test_parse_git_blame_simple() { + let output = read_test_data("blame_incremental_simple"); + let entries = parse_git_blame(&output).unwrap(); + assert_eq_golden(&entries, "blame_incremental_simple"); + } + + #[test] + fn test_parse_git_blame_complex() { + let output = read_test_data("blame_incremental_complex"); + let entries = parse_git_blame(&output).unwrap(); + assert_eq_golden(&entries, "blame_incremental_complex"); + } +} diff --git a/crates/assistant_tools/src/edit_agent/evals/fixtures/delete_run_git_blame/before.rs b/crates/assistant_tools/src/edit_agent/evals/fixtures/delete_run_git_blame/before.rs new file mode 100644 index 0000000000..185acd4a82 --- /dev/null +++ b/crates/assistant_tools/src/edit_agent/evals/fixtures/delete_run_git_blame/before.rs @@ -0,0 +1,374 @@ +use crate::commit::get_messages; +use crate::{GitRemote, Oid}; +use anyhow::{Context as _, Result, anyhow}; +use collections::{HashMap, HashSet}; +use futures::AsyncWriteExt; +use gpui::SharedString; +use serde::{Deserialize, Serialize}; +use std::process::Stdio; +use std::{ops::Range, path::Path}; +use text::Rope; +use time::OffsetDateTime; +use time::UtcOffset; +use time::macros::format_description; + +pub use git2 as libgit; + +#[derive(Debug, Clone, Default)] +pub struct Blame { + pub entries: Vec, + pub messages: HashMap, + pub remote_url: Option, +} + +#[derive(Clone, Debug, Default)] +pub struct ParsedCommitMessage { + pub message: SharedString, + pub permalink: Option, + pub pull_request: Option, + pub remote: Option, +} + +impl Blame { + pub async fn for_path( + git_binary: &Path, + working_directory: &Path, + path: &Path, + content: &Rope, + remote_url: Option, + ) -> Result { + let output = run_git_blame(git_binary, working_directory, path, content).await?; + let mut entries = parse_git_blame(&output)?; + entries.sort_unstable_by(|a, b| a.range.start.cmp(&b.range.start)); + + let mut unique_shas = HashSet::default(); + + for entry in entries.iter_mut() { + unique_shas.insert(entry.sha); + } + + let shas = unique_shas.into_iter().collect::>(); + let messages = get_messages(working_directory, &shas) + .await + .context("failed to get commit messages")?; + + Ok(Self { + entries, + messages, + remote_url, + }) + } +} + +const GIT_BLAME_NO_COMMIT_ERROR: &str = "fatal: no such ref: HEAD"; +const GIT_BLAME_NO_PATH: &str = "fatal: no such path"; + +async fn run_git_blame( + git_binary: &Path, + working_directory: &Path, + path: &Path, + contents: &Rope, +) -> Result { + let mut child = util::command::new_smol_command(git_binary) + .current_dir(working_directory) + .arg("blame") + .arg("--incremental") + .arg("--contents") + .arg("-") + .arg(path.as_os_str()) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .map_err(|e| anyhow!("Failed to start git blame process: {}", e))?; + + let stdin = child + .stdin + .as_mut() + .context("failed to get pipe to stdin of git blame command")?; + + for chunk in contents.chunks() { + stdin.write_all(chunk.as_bytes()).await?; + } + stdin.flush().await?; + + let output = child + .output() + .await + .map_err(|e| anyhow!("Failed to read git blame output: {}", e))?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + let trimmed = stderr.trim(); + if trimmed == GIT_BLAME_NO_COMMIT_ERROR || trimmed.contains(GIT_BLAME_NO_PATH) { + return Ok(String::new()); + } + return Err(anyhow!("git blame process failed: {}", stderr)); + } + + Ok(String::from_utf8(output.stdout)?) +} + +#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)] +pub struct BlameEntry { + pub sha: Oid, + + pub range: Range, + + pub original_line_number: u32, + + pub author: Option, + pub author_mail: Option, + pub author_time: Option, + pub author_tz: Option, + + pub committer_name: Option, + pub committer_email: Option, + pub committer_time: Option, + pub committer_tz: Option, + + pub summary: Option, + + pub previous: Option, + pub filename: String, +} + +impl BlameEntry { + // Returns a BlameEntry by parsing the first line of a `git blame --incremental` + // entry. The line MUST have this format: + // + // <40-byte-hex-sha1> + fn new_from_blame_line(line: &str) -> Result { + let mut parts = line.split_whitespace(); + + let sha = parts + .next() + .and_then(|line| line.parse::().ok()) + .ok_or_else(|| anyhow!("failed to parse sha"))?; + + let original_line_number = parts + .next() + .and_then(|line| line.parse::().ok()) + .ok_or_else(|| anyhow!("Failed to parse original line number"))?; + let final_line_number = parts + .next() + .and_then(|line| line.parse::().ok()) + .ok_or_else(|| anyhow!("Failed to parse final line number"))?; + + let line_count = parts + .next() + .and_then(|line| line.parse::().ok()) + .ok_or_else(|| anyhow!("Failed to parse final line number"))?; + + let start_line = final_line_number.saturating_sub(1); + let end_line = start_line + line_count; + let range = start_line..end_line; + + Ok(Self { + sha, + range, + original_line_number, + ..Default::default() + }) + } + + pub fn author_offset_date_time(&self) -> Result { + if let (Some(author_time), Some(author_tz)) = (self.author_time, &self.author_tz) { + let format = format_description!("[offset_hour][offset_minute]"); + let offset = UtcOffset::parse(author_tz, &format)?; + let date_time_utc = OffsetDateTime::from_unix_timestamp(author_time)?; + + Ok(date_time_utc.to_offset(offset)) + } else { + // Directly return current time in UTC if there's no committer time or timezone + Ok(time::OffsetDateTime::now_utc()) + } + } +} + +// parse_git_blame parses the output of `git blame --incremental`, which returns +// all the blame-entries for a given path incrementally, as it finds them. +// +// Each entry *always* starts with: +// +// <40-byte-hex-sha1> +// +// Each entry *always* ends with: +// +// filename +// +// Line numbers are 1-indexed. +// +// A `git blame --incremental` entry looks like this: +// +// 6ad46b5257ba16d12c5ca9f0d4900320959df7f4 2 2 1 +// author Joe Schmoe +// author-mail +// author-time 1709741400 +// author-tz +0100 +// committer Joe Schmoe +// committer-mail +// committer-time 1709741400 +// committer-tz +0100 +// summary Joe's cool commit +// previous 486c2409237a2c627230589e567024a96751d475 index.js +// filename index.js +// +// If the entry has the same SHA as an entry that was already printed then no +// signature information is printed: +// +// 6ad46b5257ba16d12c5ca9f0d4900320959df7f4 3 4 1 +// previous 486c2409237a2c627230589e567024a96751d475 index.js +// filename index.js +// +// More about `--incremental` output: https://mirrors.edge.kernel.org/pub/software/scm/git/docs/git-blame.html +fn parse_git_blame(output: &str) -> Result> { + let mut entries: Vec = Vec::new(); + let mut index: HashMap = HashMap::default(); + + let mut current_entry: Option = None; + + for line in output.lines() { + let mut done = false; + + match &mut current_entry { + None => { + let mut new_entry = BlameEntry::new_from_blame_line(line)?; + + if let Some(existing_entry) = index + .get(&new_entry.sha) + .and_then(|slot| entries.get(*slot)) + { + new_entry.author.clone_from(&existing_entry.author); + new_entry + .author_mail + .clone_from(&existing_entry.author_mail); + new_entry.author_time = existing_entry.author_time; + new_entry.author_tz.clone_from(&existing_entry.author_tz); + new_entry + .committer_name + .clone_from(&existing_entry.committer_name); + new_entry + .committer_email + .clone_from(&existing_entry.committer_email); + new_entry.committer_time = existing_entry.committer_time; + new_entry + .committer_tz + .clone_from(&existing_entry.committer_tz); + new_entry.summary.clone_from(&existing_entry.summary); + } + + current_entry.replace(new_entry); + } + Some(entry) => { + let Some((key, value)) = line.split_once(' ') else { + continue; + }; + let is_committed = !entry.sha.is_zero(); + match key { + "filename" => { + entry.filename = value.into(); + done = true; + } + "previous" => entry.previous = Some(value.into()), + + "summary" if is_committed => entry.summary = Some(value.into()), + "author" if is_committed => entry.author = Some(value.into()), + "author-mail" if is_committed => entry.author_mail = Some(value.into()), + "author-time" if is_committed => { + entry.author_time = Some(value.parse::()?) + } + "author-tz" if is_committed => entry.author_tz = Some(value.into()), + + "committer" if is_committed => entry.committer_name = Some(value.into()), + "committer-mail" if is_committed => entry.committer_email = Some(value.into()), + "committer-time" if is_committed => { + entry.committer_time = Some(value.parse::()?) + } + "committer-tz" if is_committed => entry.committer_tz = Some(value.into()), + _ => {} + } + } + }; + + if done { + if let Some(entry) = current_entry.take() { + index.insert(entry.sha, entries.len()); + + // We only want annotations that have a commit. + if !entry.sha.is_zero() { + entries.push(entry); + } + } + } + } + + Ok(entries) +} + +#[cfg(test)] +mod tests { + use std::path::PathBuf; + + use super::BlameEntry; + use super::parse_git_blame; + + fn read_test_data(filename: &str) -> String { + let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + path.push("test_data"); + path.push(filename); + + std::fs::read_to_string(&path) + .unwrap_or_else(|_| panic!("Could not read test data at {:?}. Is it generated?", path)) + } + + fn assert_eq_golden(entries: &Vec, golden_filename: &str) { + let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + path.push("test_data"); + path.push("golden"); + path.push(format!("{}.json", golden_filename)); + + let mut have_json = + serde_json::to_string_pretty(&entries).expect("could not serialize entries to JSON"); + // We always want to save with a trailing newline. + have_json.push('\n'); + + let update = std::env::var("UPDATE_GOLDEN") + .map(|val| val.eq_ignore_ascii_case("true")) + .unwrap_or(false); + + if update { + std::fs::create_dir_all(path.parent().unwrap()) + .expect("could not create golden test data directory"); + std::fs::write(&path, have_json).expect("could not write out golden data"); + } else { + let want_json = + std::fs::read_to_string(&path).unwrap_or_else(|_| { + panic!("could not read golden test data file at {:?}. Did you run the test with UPDATE_GOLDEN=true before?", path); + }).replace("\r\n", "\n"); + + pretty_assertions::assert_eq!(have_json, want_json, "wrong blame entries"); + } + } + + #[test] + fn test_parse_git_blame_not_committed() { + let output = read_test_data("blame_incremental_not_committed"); + let entries = parse_git_blame(&output).unwrap(); + assert_eq_golden(&entries, "blame_incremental_not_committed"); + } + + #[test] + fn test_parse_git_blame_simple() { + let output = read_test_data("blame_incremental_simple"); + let entries = parse_git_blame(&output).unwrap(); + assert_eq_golden(&entries, "blame_incremental_simple"); + } + + #[test] + fn test_parse_git_blame_complex() { + let output = read_test_data("blame_incremental_complex"); + let entries = parse_git_blame(&output).unwrap(); + assert_eq_golden(&entries, "blame_incremental_complex"); + } +} diff --git a/crates/assistant_tools/src/edit_agent/evals/fixtures/disable_cursor_blinking/after.rs b/crates/assistant_tools/src/edit_agent/evals/fixtures/disable_cursor_blinking/after.rs new file mode 100644 index 0000000000..432cb541ce --- /dev/null +++ b/crates/assistant_tools/src/edit_agent/evals/fixtures/disable_cursor_blinking/after.rs @@ -0,0 +1,21343 @@ +#![allow(rustdoc::private_intra_doc_links)] +//! This is the place where everything editor-related is stored (data-wise) and displayed (ui-wise). +//! The main point of interest in this crate is [`Editor`] type, which is used in every other Zed part as a user input element. +//! It comes in different flavors: single line, multiline and a fixed height one. +//! +//! Editor contains of multiple large submodules: +//! * [`element`] — the place where all rendering happens +//! * [`display_map`] - chunks up text in the editor into the logical blocks, establishes coordinates and mapping between each of them. +//! Contains all metadata related to text transformations (folds, fake inlay text insertions, soft wraps, tab markup, etc.). +//! * [`inlay_hint_cache`] - is a storage of inlay hints out of LSP requests, responsible for querying LSP and updating `display_map`'s state accordingly. +//! +//! All other submodules and structs are mostly concerned with holding editor data about the way it displays current buffer region(s). +//! +//! If you're looking to improve Vim mode, you should check out Vim crate that wraps Editor and overrides its behavior. +pub mod actions; +mod blink_manager; +mod clangd_ext; +mod code_context_menus; +pub mod display_map; +mod editor_settings; +mod editor_settings_controls; +mod element; +mod git; +mod highlight_matching_bracket; +mod hover_links; +pub mod hover_popover; +mod indent_guides; +mod inlay_hint_cache; +pub mod items; +mod jsx_tag_auto_close; +mod linked_editing_ranges; +mod lsp_ext; +mod mouse_context_menu; +pub mod movement; +mod persistence; +mod proposed_changes_editor; +mod rust_analyzer_ext; +pub mod scroll; +mod selections_collection; +pub mod tasks; + +#[cfg(test)] +mod code_completion_tests; +#[cfg(test)] +mod editor_tests; +#[cfg(test)] +mod inline_completion_tests; +mod signature_help; +#[cfg(any(test, feature = "test-support"))] +pub mod test; + +pub(crate) use actions::*; +pub use actions::{AcceptEditPrediction, OpenExcerpts, OpenExcerptsSplit}; +use aho_corasick::AhoCorasick; +use anyhow::{Context as _, Result, anyhow}; +use blink_manager::BlinkManager; +use buffer_diff::DiffHunkStatus; +use client::{Collaborator, ParticipantIndex}; +use clock::ReplicaId; +use collections::{BTreeMap, HashMap, HashSet, VecDeque}; +use convert_case::{Case, Casing}; +use display_map::*; +pub use display_map::{ChunkRenderer, ChunkRendererContext, DisplayPoint, FoldPlaceholder}; +use editor_settings::GoToDefinitionFallback; +pub use editor_settings::{ + CurrentLineHighlight, EditorSettings, HideMouseMode, ScrollBeyondLastLine, SearchSettings, + ShowScrollbar, +}; +pub use editor_settings_controls::*; +use element::{AcceptEditPredictionBinding, LineWithInvisibles, PositionMap, layout_line}; +pub use element::{ + CursorLayout, EditorElement, HighlightedRange, HighlightedRangeLine, PointForPosition, +}; +use feature_flags::{DebuggerFeatureFlag, FeatureFlagAppExt}; +use futures::{ + FutureExt, + future::{self, Shared, join}, +}; +use fuzzy::StringMatchCandidate; + +use ::git::blame::BlameEntry; +use ::git::{Restore, blame::ParsedCommitMessage}; +use code_context_menus::{ + AvailableCodeAction, CodeActionContents, CodeActionsItem, CodeActionsMenu, CodeContextMenu, + CompletionsMenu, ContextMenuOrigin, +}; +use git::blame::{GitBlame, GlobalBlameRenderer}; +use gpui::{ + Action, Animation, AnimationExt, AnyElement, App, AppContext, AsyncWindowContext, + AvailableSpace, Background, Bounds, ClickEvent, ClipboardEntry, ClipboardItem, Context, + DispatchPhase, Edges, Entity, EntityInputHandler, EventEmitter, FocusHandle, FocusOutEvent, + Focusable, FontId, FontWeight, Global, HighlightStyle, Hsla, KeyContext, Modifiers, + MouseButton, MouseDownEvent, PaintQuad, ParentElement, Pixels, Render, ScrollHandle, + SharedString, Size, Stateful, Styled, Subscription, Task, TextStyle, TextStyleRefinement, + UTF16Selection, UnderlineStyle, UniformListScrollHandle, WeakEntity, WeakFocusHandle, Window, + div, impl_actions, point, prelude::*, pulsating_between, px, relative, size, +}; +use highlight_matching_bracket::refresh_matching_bracket_highlights; +use hover_links::{HoverLink, HoveredLinkState, InlayHighlight, find_file}; +pub use hover_popover::hover_markdown_style; +use hover_popover::{HoverState, hide_hover}; +use indent_guides::ActiveIndentGuidesState; +use inlay_hint_cache::{InlayHintCache, InlaySplice, InvalidationStrategy}; +pub use inline_completion::Direction; +use inline_completion::{EditPredictionProvider, InlineCompletionProviderHandle}; +pub use items::MAX_TAB_TITLE_LEN; +use itertools::Itertools; +use language::{ + AutoindentMode, BracketMatch, BracketPair, Buffer, Capability, CharKind, CodeLabel, + CursorShape, DiagnosticEntry, DiffOptions, EditPredictionsMode, EditPreview, HighlightedText, + IndentKind, IndentSize, Language, OffsetRangeExt, Point, Selection, SelectionGoal, TextObject, + TransactionId, TreeSitterOptions, WordsQuery, + language_settings::{ + self, InlayHintSettings, LspInsertMode, RewrapBehavior, WordsCompletionMode, + all_language_settings, language_settings, + }, + point_from_lsp, text_diff_with_options, +}; +use language::{BufferRow, CharClassifier, Runnable, RunnableRange, point_to_lsp}; +use linked_editing_ranges::refresh_linked_ranges; +use markdown::Markdown; +use mouse_context_menu::MouseContextMenu; +use persistence::DB; +use project::{ + ProjectPath, + debugger::{ + breakpoint_store::{ + BreakpointEditAction, BreakpointState, BreakpointStore, BreakpointStoreEvent, + }, + session::{Session, SessionEvent}, + }, +}; + +pub use git::blame::BlameRenderer; +pub use proposed_changes_editor::{ + ProposedChangeLocation, ProposedChangesEditor, ProposedChangesEditorToolbar, +}; +use smallvec::smallvec; +use std::{cell::OnceCell, iter::Peekable}; +use task::{ResolvedTask, RunnableTag, TaskTemplate, TaskVariables}; + +pub use lsp::CompletionContext; +use lsp::{ + CodeActionKind, CompletionItemKind, CompletionTriggerKind, DiagnosticSeverity, + InsertTextFormat, InsertTextMode, LanguageServerId, LanguageServerName, +}; + +use language::BufferSnapshot; +pub use lsp_ext::lsp_tasks; +use movement::TextLayoutDetails; +pub use multi_buffer::{ + Anchor, AnchorRangeExt, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, PathKey, + RowInfo, ToOffset, ToPoint, +}; +use multi_buffer::{ + ExcerptInfo, ExpandExcerptDirection, MultiBufferDiffHunk, MultiBufferPoint, MultiBufferRow, + MultiOrSingleBufferOffsetRange, ToOffsetUtf16, +}; +use parking_lot::Mutex; +use project::{ + CodeAction, Completion, CompletionIntent, CompletionSource, DocumentHighlight, InlayHint, + Location, LocationLink, PrepareRenameResponse, Project, ProjectItem, ProjectTransaction, + TaskSourceKind, + debugger::breakpoint_store::Breakpoint, + lsp_store::{CompletionDocumentation, FormatTrigger, LspFormatTarget, OpenLspBufferHandle}, + project_settings::{GitGutterSetting, ProjectSettings}, +}; +use rand::prelude::*; +use rpc::{ErrorExt, proto::*}; +use scroll::{Autoscroll, OngoingScroll, ScrollAnchor, ScrollManager, ScrollbarAutoHide}; +use selections_collection::{ + MutableSelectionsCollection, SelectionsCollection, resolve_selections, +}; +use serde::{Deserialize, Serialize}; +use settings::{Settings, SettingsLocation, SettingsStore, update_settings_file}; +use smallvec::SmallVec; +use snippet::Snippet; +use std::sync::Arc; +use std::{ + any::TypeId, + borrow::Cow, + cell::RefCell, + cmp::{self, Ordering, Reverse}, + mem, + num::NonZeroU32, + ops::{ControlFlow, Deref, DerefMut, Not as _, Range, RangeInclusive}, + path::{Path, PathBuf}, + rc::Rc, + time::{Duration, Instant}, +}; +pub use sum_tree::Bias; +use sum_tree::TreeMap; +use text::{BufferId, FromAnchor, OffsetUtf16, Rope}; +use theme::{ + ActiveTheme, PlayerColor, StatusColors, SyntaxTheme, ThemeColors, ThemeSettings, + observe_buffer_font_size_adjustment, +}; +use ui::{ + ButtonSize, ButtonStyle, ContextMenu, Disclosure, IconButton, IconButtonShape, IconName, + IconSize, Key, Tooltip, h_flex, prelude::*, +}; +use util::{RangeExt, ResultExt, TryFutureExt, maybe, post_inc}; +use workspace::{ + Item as WorkspaceItem, ItemId, ItemNavHistory, OpenInTerminal, OpenTerminal, + RestoreOnStartupBehavior, SERIALIZATION_THROTTLE_TIME, SplitDirection, TabBarSettings, Toast, + ViewId, Workspace, WorkspaceId, WorkspaceSettings, + item::{ItemHandle, PreviewTabsSettings}, + notifications::{DetachAndPromptErr, NotificationId, NotifyTaskExt}, + searchable::SearchEvent, +}; + +use crate::hover_links::{find_url, find_url_from_range}; +use crate::signature_help::{SignatureHelpHiddenBy, SignatureHelpState}; + +pub const FILE_HEADER_HEIGHT: u32 = 2; +pub const MULTI_BUFFER_EXCERPT_HEADER_HEIGHT: u32 = 1; +pub const DEFAULT_MULTIBUFFER_CONTEXT: u32 = 2; +const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500); +const MAX_LINE_LEN: usize = 1024; +const MIN_NAVIGATION_HISTORY_ROW_DELTA: i64 = 10; +const MAX_SELECTION_HISTORY_LEN: usize = 1024; +pub(crate) const CURSORS_VISIBLE_FOR: Duration = Duration::from_millis(2000); +#[doc(hidden)] +pub const CODE_ACTIONS_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(250); +const SELECTION_HIGHLIGHT_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(100); + +pub(crate) const CODE_ACTION_TIMEOUT: Duration = Duration::from_secs(5); +pub(crate) const FORMAT_TIMEOUT: Duration = Duration::from_secs(5); +pub(crate) const SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT: Duration = Duration::from_secs(1); + +pub(crate) const EDIT_PREDICTION_KEY_CONTEXT: &str = "edit_prediction"; +pub(crate) const EDIT_PREDICTION_CONFLICT_KEY_CONTEXT: &str = "edit_prediction_conflict"; +pub(crate) const MIN_LINE_NUMBER_DIGITS: u32 = 4; + +pub type RenderDiffHunkControlsFn = Arc< + dyn Fn( + u32, + &DiffHunkStatus, + Range, + bool, + Pixels, + &Entity, + &mut Window, + &mut App, + ) -> AnyElement, +>; + +const COLUMNAR_SELECTION_MODIFIERS: Modifiers = Modifiers { + alt: true, + shift: true, + control: false, + platform: false, + function: false, +}; + +struct InlineValueCache { + enabled: bool, + inlays: Vec, + refresh_task: Task>, +} + +impl InlineValueCache { + fn new(enabled: bool) -> Self { + Self { + enabled, + inlays: Vec::new(), + refresh_task: Task::ready(None), + } + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum InlayId { + InlineCompletion(usize), + Hint(usize), + DebuggerValue(usize), +} + +impl InlayId { + fn id(&self) -> usize { + match self { + Self::InlineCompletion(id) => *id, + Self::Hint(id) => *id, + Self::DebuggerValue(id) => *id, + } + } +} + +pub enum ActiveDebugLine {} +enum DocumentHighlightRead {} +enum DocumentHighlightWrite {} +enum InputComposition {} +enum SelectedTextHighlight {} + +pub enum ConflictsOuter {} +pub enum ConflictsOurs {} +pub enum ConflictsTheirs {} +pub enum ConflictsOursMarker {} +pub enum ConflictsTheirsMarker {} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum Navigated { + Yes, + No, +} + +impl Navigated { + pub fn from_bool(yes: bool) -> Navigated { + if yes { Navigated::Yes } else { Navigated::No } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +enum DisplayDiffHunk { + Folded { + display_row: DisplayRow, + }, + Unfolded { + is_created_file: bool, + diff_base_byte_range: Range, + display_row_range: Range, + multi_buffer_range: Range, + status: DiffHunkStatus, + }, +} + +pub enum HideMouseCursorOrigin { + TypingAction, + MovementAction, +} + +pub fn init_settings(cx: &mut App) { + EditorSettings::register(cx); +} + +pub fn init(cx: &mut App) { + init_settings(cx); + + cx.set_global(GlobalBlameRenderer(Arc::new(()))); + + workspace::register_project_item::(cx); + workspace::FollowableViewRegistry::register::(cx); + workspace::register_serializable_item::(cx); + + cx.observe_new( + |workspace: &mut Workspace, _: Option<&mut Window>, _cx: &mut Context| { + workspace.register_action(Editor::new_file); + workspace.register_action(Editor::new_file_vertical); + workspace.register_action(Editor::new_file_horizontal); + workspace.register_action(Editor::cancel_language_server_work); + }, + ) + .detach(); + + cx.on_action(move |_: &workspace::NewFile, cx| { + let app_state = workspace::AppState::global(cx); + if let Some(app_state) = app_state.upgrade() { + workspace::open_new( + Default::default(), + app_state, + cx, + |workspace, window, cx| { + Editor::new_file(workspace, &Default::default(), window, cx) + }, + ) + .detach(); + } + }); + cx.on_action(move |_: &workspace::NewWindow, cx| { + let app_state = workspace::AppState::global(cx); + if let Some(app_state) = app_state.upgrade() { + workspace::open_new( + Default::default(), + app_state, + cx, + |workspace, window, cx| { + cx.activate(true); + Editor::new_file(workspace, &Default::default(), window, cx) + }, + ) + .detach(); + } + }); +} + +pub fn set_blame_renderer(renderer: impl BlameRenderer + 'static, cx: &mut App) { + cx.set_global(GlobalBlameRenderer(Arc::new(renderer))); +} + +pub trait DiagnosticRenderer { + fn render_group( + &self, + diagnostic_group: Vec>, + buffer_id: BufferId, + snapshot: EditorSnapshot, + editor: WeakEntity, + cx: &mut App, + ) -> Vec>; + + fn render_hover( + &self, + diagnostic_group: Vec>, + range: Range, + buffer_id: BufferId, + cx: &mut App, + ) -> Option>; + + fn open_link( + &self, + editor: &mut Editor, + link: SharedString, + window: &mut Window, + cx: &mut Context, + ); +} + +pub(crate) struct GlobalDiagnosticRenderer(pub Arc); + +impl GlobalDiagnosticRenderer { + fn global(cx: &App) -> Option> { + cx.try_global::().map(|g| g.0.clone()) + } +} + +impl gpui::Global for GlobalDiagnosticRenderer {} +pub fn set_diagnostic_renderer(renderer: impl DiagnosticRenderer + 'static, cx: &mut App) { + cx.set_global(GlobalDiagnosticRenderer(Arc::new(renderer))); +} + +pub struct SearchWithinRange; + +trait InvalidationRegion { + fn ranges(&self) -> &[Range]; +} + +#[derive(Clone, Debug, PartialEq)] +pub enum SelectPhase { + Begin { + position: DisplayPoint, + add: bool, + click_count: usize, + }, + BeginColumnar { + position: DisplayPoint, + reset: bool, + goal_column: u32, + }, + Extend { + position: DisplayPoint, + click_count: usize, + }, + Update { + position: DisplayPoint, + goal_column: u32, + scroll_delta: gpui::Point, + }, + End, +} + +#[derive(Clone, Debug)] +pub enum SelectMode { + Character, + Word(Range), + Line(Range), + All, +} + +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +pub enum EditorMode { + SingleLine { + auto_width: bool, + }, + AutoHeight { + max_lines: usize, + }, + Full { + /// When set to `true`, the editor will scale its UI elements with the buffer font size. + scale_ui_elements_with_buffer_font_size: bool, + /// When set to `true`, the editor will render a background for the active line. + show_active_line_background: bool, + /// When set to `true`, the editor's height will be determined by its content. + sized_by_content: bool, + }, +} + +impl EditorMode { + pub fn full() -> Self { + Self::Full { + scale_ui_elements_with_buffer_font_size: true, + show_active_line_background: true, + sized_by_content: false, + } + } + + pub fn is_full(&self) -> bool { + matches!(self, Self::Full { .. }) + } +} + +#[derive(Copy, Clone, Debug)] +pub enum SoftWrap { + /// Prefer not to wrap at all. + /// + /// Note: this is currently internal, as actually limited by [`crate::MAX_LINE_LEN`] until it wraps. + /// The mode is used inside git diff hunks, where it's seems currently more useful to not wrap as much as possible. + GitDiff, + /// Prefer a single line generally, unless an overly long line is encountered. + None, + /// Soft wrap lines that exceed the editor width. + EditorWidth, + /// Soft wrap lines at the preferred line length. + Column(u32), + /// Soft wrap line at the preferred line length or the editor width (whichever is smaller). + Bounded(u32), +} + +#[derive(Clone)] +pub struct EditorStyle { + pub background: Hsla, + pub local_player: PlayerColor, + pub text: TextStyle, + pub scrollbar_width: Pixels, + pub syntax: Arc, + pub status: StatusColors, + pub inlay_hints_style: HighlightStyle, + pub inline_completion_styles: InlineCompletionStyles, + pub unnecessary_code_fade: f32, +} + +impl Default for EditorStyle { + fn default() -> Self { + Self { + background: Hsla::default(), + local_player: PlayerColor::default(), + text: TextStyle::default(), + scrollbar_width: Pixels::default(), + syntax: Default::default(), + // HACK: Status colors don't have a real default. + // We should look into removing the status colors from the editor + // style and retrieve them directly from the theme. + status: StatusColors::dark(), + inlay_hints_style: HighlightStyle::default(), + inline_completion_styles: InlineCompletionStyles { + insertion: HighlightStyle::default(), + whitespace: HighlightStyle::default(), + }, + unnecessary_code_fade: Default::default(), + } + } +} + +pub fn make_inlay_hints_style(cx: &mut App) -> HighlightStyle { + let show_background = language_settings::language_settings(None, None, cx) + .inlay_hints + .show_background; + + HighlightStyle { + color: Some(cx.theme().status().hint), + background_color: show_background.then(|| cx.theme().status().hint_background), + ..HighlightStyle::default() + } +} + +pub fn make_suggestion_styles(cx: &mut App) -> InlineCompletionStyles { + InlineCompletionStyles { + insertion: HighlightStyle { + color: Some(cx.theme().status().predictive), + ..HighlightStyle::default() + }, + whitespace: HighlightStyle { + background_color: Some(cx.theme().status().created_background), + ..HighlightStyle::default() + }, + } +} + +type CompletionId = usize; + +pub(crate) enum EditDisplayMode { + TabAccept, + DiffPopover, + Inline, +} + +enum InlineCompletion { + Edit { + edits: Vec<(Range, String)>, + edit_preview: Option, + display_mode: EditDisplayMode, + snapshot: BufferSnapshot, + }, + Move { + target: Anchor, + snapshot: BufferSnapshot, + }, +} + +struct InlineCompletionState { + inlay_ids: Vec, + completion: InlineCompletion, + completion_id: Option, + invalidation_range: Range, +} + +enum EditPredictionSettings { + Disabled, + Enabled { + show_in_menu: bool, + preview_requires_modifier: bool, + }, +} + +enum InlineCompletionHighlight {} + +#[derive(Debug, Clone)] +struct InlineDiagnostic { + message: SharedString, + group_id: usize, + is_primary: bool, + start: Point, + severity: DiagnosticSeverity, +} + +pub enum MenuInlineCompletionsPolicy { + Never, + ByProvider, +} + +pub enum EditPredictionPreview { + /// Modifier is not pressed + Inactive { released_too_fast: bool }, + /// Modifier pressed + Active { + since: Instant, + previous_scroll_position: Option, + }, +} + +impl EditPredictionPreview { + pub fn released_too_fast(&self) -> bool { + match self { + EditPredictionPreview::Inactive { released_too_fast } => *released_too_fast, + EditPredictionPreview::Active { .. } => false, + } + } + + pub fn set_previous_scroll_position(&mut self, scroll_position: Option) { + if let EditPredictionPreview::Active { + previous_scroll_position, + .. + } = self + { + *previous_scroll_position = scroll_position; + } + } +} + +pub struct ContextMenuOptions { + pub min_entries_visible: usize, + pub max_entries_visible: usize, + pub placement: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ContextMenuPlacement { + Above, + Below, +} + +#[derive(Copy, Clone, Eq, PartialEq, PartialOrd, Ord, Debug, Default)] +struct EditorActionId(usize); + +impl EditorActionId { + pub fn post_inc(&mut self) -> Self { + let answer = self.0; + + *self = Self(answer + 1); + + Self(answer) + } +} + +// type GetFieldEditorTheme = dyn Fn(&theme::Theme) -> theme::FieldEditor; +// type OverrideTextStyle = dyn Fn(&EditorStyle) -> Option; + +type BackgroundHighlight = (fn(&ThemeColors) -> Hsla, Arc<[Range]>); +type GutterHighlight = (fn(&App) -> Hsla, Arc<[Range]>); + +#[derive(Default)] +struct ScrollbarMarkerState { + scrollbar_size: Size, + dirty: bool, + markers: Arc<[PaintQuad]>, + pending_refresh: Option>>, +} + +impl ScrollbarMarkerState { + fn should_refresh(&self, scrollbar_size: Size) -> bool { + self.pending_refresh.is_none() && (self.scrollbar_size != scrollbar_size || self.dirty) + } +} + +#[derive(Clone, Debug)] +struct RunnableTasks { + templates: Vec<(TaskSourceKind, TaskTemplate)>, + offset: multi_buffer::Anchor, + // We need the column at which the task context evaluation should take place (when we're spawning it via gutter). + column: u32, + // Values of all named captures, including those starting with '_' + extra_variables: HashMap, + // Full range of the tagged region. We use it to determine which `extra_variables` to grab for context resolution in e.g. a modal. + context_range: Range, +} + +impl RunnableTasks { + fn resolve<'a>( + &'a self, + cx: &'a task::TaskContext, + ) -> impl Iterator + 'a { + self.templates.iter().filter_map(|(kind, template)| { + template + .resolve_task(&kind.to_id_base(), cx) + .map(|task| (kind.clone(), task)) + }) + } +} + +#[derive(Clone)] +struct ResolvedTasks { + templates: SmallVec<[(TaskSourceKind, ResolvedTask); 1]>, + position: Anchor, +} + +#[derive(Copy, Clone, Debug, PartialEq, PartialOrd)] +struct BufferOffset(usize); + +// Addons allow storing per-editor state in other crates (e.g. Vim) +pub trait Addon: 'static { + fn extend_key_context(&self, _: &mut KeyContext, _: &App) {} + + fn render_buffer_header_controls( + &self, + _: &ExcerptInfo, + _: &Window, + _: &App, + ) -> Option { + None + } + + fn to_any(&self) -> &dyn std::any::Any; + + fn to_any_mut(&mut self) -> Option<&mut dyn std::any::Any> { + None + } +} + +/// A set of caret positions, registered when the editor was edited. +pub struct ChangeList { + changes: Vec>, + /// Currently "selected" change. + position: Option, +} + +impl ChangeList { + pub fn new() -> Self { + Self { + changes: Vec::new(), + position: None, + } + } + + /// Moves to the next change in the list (based on the direction given) and returns the caret positions for the next change. + /// If reaches the end of the list in the direction, returns the corresponding change until called for a different direction. + pub fn next_change(&mut self, count: usize, direction: Direction) -> Option<&[Anchor]> { + if self.changes.is_empty() { + return None; + } + + let prev = self.position.unwrap_or(self.changes.len()); + let next = if direction == Direction::Prev { + prev.saturating_sub(count) + } else { + (prev + count).min(self.changes.len() - 1) + }; + self.position = Some(next); + self.changes.get(next).map(|anchors| anchors.as_slice()) + } + + /// Adds a new change to the list, resetting the change list position. + pub fn push_to_change_list(&mut self, pop_state: bool, new_positions: Vec) { + self.position.take(); + if pop_state { + self.changes.pop(); + } + self.changes.push(new_positions.clone()); + } + + pub fn last(&self) -> Option<&[Anchor]> { + self.changes.last().map(|anchors| anchors.as_slice()) + } +} + +#[derive(Clone)] +struct InlineBlamePopoverState { + scroll_handle: ScrollHandle, + commit_message: Option, + markdown: Entity, +} + +struct InlineBlamePopover { + position: gpui::Point, + show_task: Option>, + hide_task: Option>, + popover_bounds: Option>, + popover_state: InlineBlamePopoverState, +} + +/// Represents a breakpoint indicator that shows up when hovering over lines in the gutter that don't have +/// a breakpoint on them. +#[derive(Clone, Copy, Debug)] +struct PhantomBreakpointIndicator { + display_row: DisplayRow, + /// There's a small debounce between hovering over the line and showing the indicator. + /// We don't want to show the indicator when moving the mouse from editor to e.g. project panel. + is_active: bool, + collides_with_existing_breakpoint: bool, +} +/// Zed's primary implementation of text input, allowing users to edit a [`MultiBuffer`]. +/// +/// See the [module level documentation](self) for more information. +pub struct Editor { + focus_handle: FocusHandle, + last_focused_descendant: Option, + /// The text buffer being edited + buffer: Entity, + /// Map of how text in the buffer should be displayed. + /// Handles soft wraps, folds, fake inlay text insertions, etc. + pub display_map: Entity, + pub selections: SelectionsCollection, + pub scroll_manager: ScrollManager, + /// When inline assist editors are linked, they all render cursors because + /// typing enters text into each of them, even the ones that aren't focused. + pub(crate) show_cursor_when_unfocused: bool, + columnar_selection_tail: Option, + add_selections_state: Option, + select_next_state: Option, + select_prev_state: Option, + selection_history: SelectionHistory, + autoclose_regions: Vec, + snippet_stack: InvalidationStack, + select_syntax_node_history: SelectSyntaxNodeHistory, + ime_transaction: Option, + active_diagnostics: ActiveDiagnostic, + show_inline_diagnostics: bool, + inline_diagnostics_update: Task<()>, + inline_diagnostics_enabled: bool, + inline_diagnostics: Vec<(Anchor, InlineDiagnostic)>, + soft_wrap_mode_override: Option, + hard_wrap: Option, + + // TODO: make this a access method + pub project: Option>, + semantics_provider: Option>, + completion_provider: Option>, + collaboration_hub: Option>, + blink_manager: Entity, + show_cursor_names: bool, + hovered_cursors: HashMap>, + pub show_local_selections: bool, + mode: EditorMode, + show_breadcrumbs: bool, + show_gutter: bool, + show_scrollbars: bool, + disable_scrolling: bool, + disable_expand_excerpt_buttons: bool, + show_line_numbers: Option, + use_relative_line_numbers: Option, + show_git_diff_gutter: Option, + show_code_actions: Option, + show_runnables: Option, + show_breakpoints: Option, + show_wrap_guides: Option, + show_indent_guides: Option, + placeholder_text: Option>, + highlight_order: usize, + highlighted_rows: HashMap>, + background_highlights: TreeMap, + gutter_highlights: TreeMap, + scrollbar_marker_state: ScrollbarMarkerState, + active_indent_guides_state: ActiveIndentGuidesState, + nav_history: Option, + context_menu: RefCell>, + context_menu_options: Option, + mouse_context_menu: Option, + completion_tasks: Vec<(CompletionId, Task>)>, + inline_blame_popover: Option, + signature_help_state: SignatureHelpState, + auto_signature_help: Option, + find_all_references_task_sources: Vec, + next_completion_id: CompletionId, + available_code_actions: Option<(Location, Rc<[AvailableCodeAction]>)>, + code_actions_task: Option>>, + quick_selection_highlight_task: Option<(Range, Task<()>)>, + debounced_selection_highlight_task: Option<(Range, Task<()>)>, + document_highlights_task: Option>, + linked_editing_range_task: Option>>, + linked_edit_ranges: linked_editing_ranges::LinkedEditingRanges, + pending_rename: Option, + searchable: bool, + cursor_shape: CursorShape, + current_line_highlight: Option, + collapse_matches: bool, + autoindent_mode: Option, + workspace: Option<(WeakEntity, Option)>, + input_enabled: bool, + use_modal_editing: bool, + read_only: bool, + leader_peer_id: Option, + remote_id: Option, + pub hover_state: HoverState, + pending_mouse_down: Option>>>, + gutter_hovered: bool, + hovered_link_state: Option, + edit_prediction_provider: Option, + code_action_providers: Vec>, + active_inline_completion: Option, + /// Used to prevent flickering as the user types while the menu is open + stale_inline_completion_in_menu: Option, + edit_prediction_settings: EditPredictionSettings, + inline_completions_hidden_for_vim_mode: bool, + show_inline_completions_override: Option, + menu_inline_completions_policy: MenuInlineCompletionsPolicy, + edit_prediction_preview: EditPredictionPreview, + edit_prediction_indent_conflict: bool, + edit_prediction_requires_modifier_in_indent_conflict: bool, + inlay_hint_cache: InlayHintCache, + next_inlay_id: usize, + _subscriptions: Vec, + pixel_position_of_newest_cursor: Option>, + gutter_dimensions: GutterDimensions, + style: Option, + text_style_refinement: Option, + next_editor_action_id: EditorActionId, + editor_actions: + Rc)>>>>, + use_autoclose: bool, + use_auto_surround: bool, + auto_replace_emoji_shortcode: bool, + jsx_tag_auto_close_enabled_in_any_buffer: bool, + show_git_blame_gutter: bool, + show_git_blame_inline: bool, + show_git_blame_inline_delay_task: Option>, + git_blame_inline_enabled: bool, + render_diff_hunk_controls: RenderDiffHunkControlsFn, + serialize_dirty_buffers: bool, + show_selection_menu: Option, + blame: Option>, + blame_subscription: Option, + custom_context_menu: Option< + Box< + dyn 'static + + Fn( + &mut Self, + DisplayPoint, + &mut Window, + &mut Context, + ) -> Option>, + >, + >, + last_bounds: Option>, + last_position_map: Option>, + expect_bounds_change: Option>, + tasks: BTreeMap<(BufferId, BufferRow), RunnableTasks>, + tasks_update_task: Option>, + breakpoint_store: Option>, + gutter_breakpoint_indicator: (Option, Option>), + in_project_search: bool, + previous_search_ranges: Option]>>, + breadcrumb_header: Option, + focused_block: Option, + next_scroll_position: NextScrollCursorCenterTopBottom, + addons: HashMap>, + registered_buffers: HashMap, + load_diff_task: Option>>, + selection_mark_mode: bool, + toggle_fold_multiple_buffers: Task<()>, + _scroll_cursor_center_top_bottom_task: Task<()>, + serialize_selections: Task<()>, + serialize_folds: Task<()>, + mouse_cursor_hidden: bool, + hide_mouse_mode: HideMouseMode, + pub change_list: ChangeList, + inline_value_cache: InlineValueCache, +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)] +enum NextScrollCursorCenterTopBottom { + #[default] + Center, + Top, + Bottom, +} + +impl NextScrollCursorCenterTopBottom { + fn next(&self) -> Self { + match self { + Self::Center => Self::Top, + Self::Top => Self::Bottom, + Self::Bottom => Self::Center, + } + } +} + +#[derive(Clone)] +pub struct EditorSnapshot { + pub mode: EditorMode, + show_gutter: bool, + show_line_numbers: Option, + show_git_diff_gutter: Option, + show_code_actions: Option, + show_runnables: Option, + show_breakpoints: Option, + git_blame_gutter_max_author_length: Option, + pub display_snapshot: DisplaySnapshot, + pub placeholder_text: Option>, + is_focused: bool, + scroll_anchor: ScrollAnchor, + ongoing_scroll: OngoingScroll, + current_line_highlight: CurrentLineHighlight, + gutter_hovered: bool, +} + +#[derive(Default, Debug, Clone, Copy)] +pub struct GutterDimensions { + pub left_padding: Pixels, + pub right_padding: Pixels, + pub width: Pixels, + pub margin: Pixels, + pub git_blame_entries_width: Option, +} + +impl GutterDimensions { + /// The full width of the space taken up by the gutter. + pub fn full_width(&self) -> Pixels { + self.margin + self.width + } + + /// The width of the space reserved for the fold indicators, + /// use alongside 'justify_end' and `gutter_width` to + /// right align content with the line numbers + pub fn fold_area_width(&self) -> Pixels { + self.margin + self.right_padding + } +} + +#[derive(Debug)] +pub struct RemoteSelection { + pub replica_id: ReplicaId, + pub selection: Selection, + pub cursor_shape: CursorShape, + pub peer_id: PeerId, + pub line_mode: bool, + pub participant_index: Option, + pub user_name: Option, +} + +#[derive(Clone, Debug)] +struct SelectionHistoryEntry { + selections: Arc<[Selection]>, + select_next_state: Option, + select_prev_state: Option, + add_selections_state: Option, +} + +enum SelectionHistoryMode { + Normal, + Undoing, + Redoing, +} + +#[derive(Clone, PartialEq, Eq, Hash)] +struct HoveredCursor { + replica_id: u16, + selection_id: usize, +} + +impl Default for SelectionHistoryMode { + fn default() -> Self { + Self::Normal + } +} + +#[derive(Default)] +struct SelectionHistory { + #[allow(clippy::type_complexity)] + selections_by_transaction: + HashMap]>, Option]>>)>, + mode: SelectionHistoryMode, + undo_stack: VecDeque, + redo_stack: VecDeque, +} + +impl SelectionHistory { + fn insert_transaction( + &mut self, + transaction_id: TransactionId, + selections: Arc<[Selection]>, + ) { + self.selections_by_transaction + .insert(transaction_id, (selections, None)); + } + + #[allow(clippy::type_complexity)] + fn transaction( + &self, + transaction_id: TransactionId, + ) -> Option<&(Arc<[Selection]>, Option]>>)> { + self.selections_by_transaction.get(&transaction_id) + } + + #[allow(clippy::type_complexity)] + fn transaction_mut( + &mut self, + transaction_id: TransactionId, + ) -> Option<&mut (Arc<[Selection]>, Option]>>)> { + self.selections_by_transaction.get_mut(&transaction_id) + } + + fn push(&mut self, entry: SelectionHistoryEntry) { + if !entry.selections.is_empty() { + match self.mode { + SelectionHistoryMode::Normal => { + self.push_undo(entry); + self.redo_stack.clear(); + } + SelectionHistoryMode::Undoing => self.push_redo(entry), + SelectionHistoryMode::Redoing => self.push_undo(entry), + } + } + } + + fn push_undo(&mut self, entry: SelectionHistoryEntry) { + if self + .undo_stack + .back() + .map_or(true, |e| e.selections != entry.selections) + { + self.undo_stack.push_back(entry); + if self.undo_stack.len() > MAX_SELECTION_HISTORY_LEN { + self.undo_stack.pop_front(); + } + } + } + + fn push_redo(&mut self, entry: SelectionHistoryEntry) { + if self + .redo_stack + .back() + .map_or(true, |e| e.selections != entry.selections) + { + self.redo_stack.push_back(entry); + if self.redo_stack.len() > MAX_SELECTION_HISTORY_LEN { + self.redo_stack.pop_front(); + } + } + } +} + +#[derive(Clone, Copy)] +pub struct RowHighlightOptions { + pub autoscroll: bool, + pub include_gutter: bool, +} + +impl Default for RowHighlightOptions { + fn default() -> Self { + Self { + autoscroll: Default::default(), + include_gutter: true, + } + } +} + +struct RowHighlight { + index: usize, + range: Range, + color: Hsla, + options: RowHighlightOptions, + type_id: TypeId, +} + +#[derive(Clone, Debug)] +struct AddSelectionsState { + above: bool, + stack: Vec, +} + +#[derive(Clone)] +struct SelectNextState { + query: AhoCorasick, + wordwise: bool, + done: bool, +} + +impl std::fmt::Debug for SelectNextState { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct(std::any::type_name::()) + .field("wordwise", &self.wordwise) + .field("done", &self.done) + .finish() + } +} + +#[derive(Debug)] +struct AutocloseRegion { + selection_id: usize, + range: Range, + pair: BracketPair, +} + +#[derive(Debug)] +struct SnippetState { + ranges: Vec>>, + active_index: usize, + choices: Vec>>, +} + +#[doc(hidden)] +pub struct RenameState { + pub range: Range, + pub old_name: Arc, + pub editor: Entity, + block_id: CustomBlockId, +} + +struct InvalidationStack(Vec); + +struct RegisteredInlineCompletionProvider { + provider: Arc, + _subscription: Subscription, +} + +#[derive(Debug, PartialEq, Eq)] +pub struct ActiveDiagnosticGroup { + pub active_range: Range, + pub active_message: String, + pub group_id: usize, + pub blocks: HashSet, +} + +#[derive(Debug, PartialEq, Eq)] +#[allow(clippy::large_enum_variant)] +pub(crate) enum ActiveDiagnostic { + None, + All, + Group(ActiveDiagnosticGroup), +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct ClipboardSelection { + /// The number of bytes in this selection. + pub len: usize, + /// Whether this was a full-line selection. + pub is_entire_line: bool, + /// The indentation of the first line when this content was originally copied. + pub first_line_indent: u32, +} + +// selections, scroll behavior, was newest selection reversed +type SelectSyntaxNodeHistoryState = ( + Box<[Selection]>, + SelectSyntaxNodeScrollBehavior, + bool, +); + +#[derive(Default)] +struct SelectSyntaxNodeHistory { + stack: Vec, + // disable temporarily to allow changing selections without losing the stack + pub disable_clearing: bool, +} + +impl SelectSyntaxNodeHistory { + pub fn try_clear(&mut self) { + if !self.disable_clearing { + self.stack.clear(); + } + } + + pub fn push(&mut self, selection: SelectSyntaxNodeHistoryState) { + self.stack.push(selection); + } + + pub fn pop(&mut self) -> Option { + self.stack.pop() + } +} + +enum SelectSyntaxNodeScrollBehavior { + CursorTop, + FitSelection, + CursorBottom, +} + +#[derive(Debug)] +pub(crate) struct NavigationData { + cursor_anchor: Anchor, + cursor_position: Point, + scroll_anchor: ScrollAnchor, + scroll_top_row: u32, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum GotoDefinitionKind { + Symbol, + Declaration, + Type, + Implementation, +} + +#[derive(Debug, Clone)] +enum InlayHintRefreshReason { + ModifiersChanged(bool), + Toggle(bool), + SettingsChange(InlayHintSettings), + NewLinesShown, + BufferEdited(HashSet>), + RefreshRequested, + ExcerptsRemoved(Vec), +} + +impl InlayHintRefreshReason { + fn description(&self) -> &'static str { + match self { + Self::ModifiersChanged(_) => "modifiers changed", + Self::Toggle(_) => "toggle", + Self::SettingsChange(_) => "settings change", + Self::NewLinesShown => "new lines shown", + Self::BufferEdited(_) => "buffer edited", + Self::RefreshRequested => "refresh requested", + Self::ExcerptsRemoved(_) => "excerpts removed", + } + } +} + +pub enum FormatTarget { + Buffers, + Ranges(Vec>), +} + +pub(crate) struct FocusedBlock { + id: BlockId, + focus_handle: WeakFocusHandle, +} + +#[derive(Clone)] +enum JumpData { + MultiBufferRow { + row: MultiBufferRow, + line_offset_from_top: u32, + }, + MultiBufferPoint { + excerpt_id: ExcerptId, + position: Point, + anchor: text::Anchor, + line_offset_from_top: u32, + }, +} + +pub enum MultibufferSelectionMode { + First, + All, +} + +#[derive(Clone, Copy, Debug, Default)] +pub struct RewrapOptions { + pub override_language_settings: bool, + pub preserve_existing_whitespace: bool, +} + +impl Editor { + pub fn single_line(window: &mut Window, cx: &mut Context) -> Self { + let buffer = cx.new(|cx| Buffer::local("", cx)); + let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); + Self::new( + EditorMode::SingleLine { auto_width: false }, + buffer, + None, + window, + cx, + ) + } + + pub fn multi_line(window: &mut Window, cx: &mut Context) -> Self { + let buffer = cx.new(|cx| Buffer::local("", cx)); + let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); + Self::new(EditorMode::full(), buffer, None, window, cx) + } + + pub fn auto_width(window: &mut Window, cx: &mut Context) -> Self { + let buffer = cx.new(|cx| Buffer::local("", cx)); + let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); + Self::new( + EditorMode::SingleLine { auto_width: true }, + buffer, + None, + window, + cx, + ) + } + + pub fn auto_height(max_lines: usize, window: &mut Window, cx: &mut Context) -> Self { + let buffer = cx.new(|cx| Buffer::local("", cx)); + let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); + Self::new( + EditorMode::AutoHeight { max_lines }, + buffer, + None, + window, + cx, + ) + } + + pub fn for_buffer( + buffer: Entity, + project: Option>, + window: &mut Window, + cx: &mut Context, + ) -> Self { + let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); + Self::new(EditorMode::full(), buffer, project, window, cx) + } + + pub fn for_multibuffer( + buffer: Entity, + project: Option>, + window: &mut Window, + cx: &mut Context, + ) -> Self { + Self::new(EditorMode::full(), buffer, project, window, cx) + } + + pub fn clone(&self, window: &mut Window, cx: &mut Context) -> Self { + let mut clone = Self::new( + self.mode, + self.buffer.clone(), + self.project.clone(), + window, + cx, + ); + self.display_map.update(cx, |display_map, cx| { + let snapshot = display_map.snapshot(cx); + clone.display_map.update(cx, |display_map, cx| { + display_map.set_state(&snapshot, cx); + }); + }); + clone.folds_did_change(cx); + clone.selections.clone_state(&self.selections); + clone.scroll_manager.clone_state(&self.scroll_manager); + clone.searchable = self.searchable; + clone.read_only = self.read_only; + clone + } + + pub fn new( + mode: EditorMode, + buffer: Entity, + project: Option>, + window: &mut Window, + cx: &mut Context, + ) -> Self { + let style = window.text_style(); + let font_size = style.font_size.to_pixels(window.rem_size()); + let editor = cx.entity().downgrade(); + let fold_placeholder = FoldPlaceholder { + constrain_width: true, + render: Arc::new(move |fold_id, fold_range, cx| { + let editor = editor.clone(); + div() + .id(fold_id) + .bg(cx.theme().colors().ghost_element_background) + .hover(|style| style.bg(cx.theme().colors().ghost_element_hover)) + .active(|style| style.bg(cx.theme().colors().ghost_element_active)) + .rounded_xs() + .size_full() + .cursor_pointer() + .child("⋯") + .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation()) + .on_click(move |_, _window, cx| { + editor + .update(cx, |editor, cx| { + editor.unfold_ranges( + &[fold_range.start..fold_range.end], + true, + false, + cx, + ); + cx.stop_propagation(); + }) + .ok(); + }) + .into_any() + }), + merge_adjacent: true, + ..Default::default() + }; + let display_map = cx.new(|cx| { + DisplayMap::new( + buffer.clone(), + style.font(), + font_size, + None, + FILE_HEADER_HEIGHT, + MULTI_BUFFER_EXCERPT_HEADER_HEIGHT, + fold_placeholder, + cx, + ) + }); + + let selections = SelectionsCollection::new(display_map.clone(), buffer.clone()); + + let blink_manager = cx.new(|cx| BlinkManager::new(CURSOR_BLINK_INTERVAL, cx)); + + let soft_wrap_mode_override = matches!(mode, EditorMode::SingleLine { .. }) + .then(|| language_settings::SoftWrap::None); + + let mut project_subscriptions = Vec::new(); + if mode.is_full() { + if let Some(project) = project.as_ref() { + project_subscriptions.push(cx.subscribe_in( + project, + window, + |editor, _, event, window, cx| match event { + project::Event::RefreshCodeLens => { + // we always query lens with actions, without storing them, always refreshing them + } + project::Event::RefreshInlayHints => { + editor + .refresh_inlay_hints(InlayHintRefreshReason::RefreshRequested, cx); + } + project::Event::SnippetEdit(id, snippet_edits) => { + if let Some(buffer) = editor.buffer.read(cx).buffer(*id) { + let focus_handle = editor.focus_handle(cx); + if focus_handle.is_focused(window) { + let snapshot = buffer.read(cx).snapshot(); + for (range, snippet) in snippet_edits { + let editor_range = + language::range_from_lsp(*range).to_offset(&snapshot); + editor + .insert_snippet( + &[editor_range], + snippet.clone(), + window, + cx, + ) + .ok(); + } + } + } + } + _ => {} + }, + )); + if let Some(task_inventory) = project + .read(cx) + .task_store() + .read(cx) + .task_inventory() + .cloned() + { + project_subscriptions.push(cx.observe_in( + &task_inventory, + window, + |editor, _, window, cx| { + editor.tasks_update_task = Some(editor.refresh_runnables(window, cx)); + }, + )); + }; + + project_subscriptions.push(cx.subscribe_in( + &project.read(cx).breakpoint_store(), + window, + |editor, _, event, window, cx| match event { + BreakpointStoreEvent::ClearDebugLines => { + editor.clear_row_highlights::(); + editor.refresh_inline_values(cx); + } + BreakpointStoreEvent::SetDebugLine => { + if editor.go_to_active_debug_line(window, cx) { + cx.stop_propagation(); + } + + editor.refresh_inline_values(cx); + } + _ => {} + }, + )); + } + } + + let buffer_snapshot = buffer.read(cx).snapshot(cx); + + let inlay_hint_settings = + inlay_hint_settings(selections.newest_anchor().head(), &buffer_snapshot, cx); + let focus_handle = cx.focus_handle(); + cx.on_focus(&focus_handle, window, Self::handle_focus) + .detach(); + cx.on_focus_in(&focus_handle, window, Self::handle_focus_in) + .detach(); + cx.on_focus_out(&focus_handle, window, Self::handle_focus_out) + .detach(); + cx.on_blur(&focus_handle, window, Self::handle_blur) + .detach(); + + let show_indent_guides = if matches!(mode, EditorMode::SingleLine { .. }) { + Some(false) + } else { + None + }; + + let breakpoint_store = match (mode, project.as_ref()) { + (EditorMode::Full { .. }, Some(project)) => Some(project.read(cx).breakpoint_store()), + _ => None, + }; + + let mut code_action_providers = Vec::new(); + let mut load_uncommitted_diff = None; + if let Some(project) = project.clone() { + load_uncommitted_diff = Some( + get_uncommitted_diff_for_buffer( + &project, + buffer.read(cx).all_buffers(), + buffer.clone(), + cx, + ) + .shared(), + ); + code_action_providers.push(Rc::new(project) as Rc<_>); + } + + let mut this = Self { + focus_handle, + show_cursor_when_unfocused: false, + last_focused_descendant: None, + buffer: buffer.clone(), + display_map: display_map.clone(), + selections, + scroll_manager: ScrollManager::new(cx), + columnar_selection_tail: None, + add_selections_state: None, + select_next_state: None, + select_prev_state: None, + selection_history: Default::default(), + autoclose_regions: Default::default(), + snippet_stack: Default::default(), + select_syntax_node_history: SelectSyntaxNodeHistory::default(), + ime_transaction: Default::default(), + active_diagnostics: ActiveDiagnostic::None, + show_inline_diagnostics: ProjectSettings::get_global(cx).diagnostics.inline.enabled, + inline_diagnostics_update: Task::ready(()), + inline_diagnostics: Vec::new(), + soft_wrap_mode_override, + hard_wrap: None, + completion_provider: project.clone().map(|project| Box::new(project) as _), + semantics_provider: project.clone().map(|project| Rc::new(project) as _), + collaboration_hub: project.clone().map(|project| Box::new(project) as _), + project, + blink_manager: blink_manager.clone(), + show_local_selections: true, + show_scrollbars: true, + disable_scrolling: false, + mode, + show_breadcrumbs: EditorSettings::get_global(cx).toolbar.breadcrumbs, + show_gutter: mode.is_full(), + show_line_numbers: None, + use_relative_line_numbers: None, + disable_expand_excerpt_buttons: false, + show_git_diff_gutter: None, + show_code_actions: None, + show_runnables: None, + show_breakpoints: None, + show_wrap_guides: None, + show_indent_guides, + placeholder_text: None, + highlight_order: 0, + highlighted_rows: HashMap::default(), + background_highlights: Default::default(), + gutter_highlights: TreeMap::default(), + scrollbar_marker_state: ScrollbarMarkerState::default(), + active_indent_guides_state: ActiveIndentGuidesState::default(), + nav_history: None, + context_menu: RefCell::new(None), + context_menu_options: None, + mouse_context_menu: None, + completion_tasks: Default::default(), + inline_blame_popover: Default::default(), + signature_help_state: SignatureHelpState::default(), + auto_signature_help: None, + find_all_references_task_sources: Vec::new(), + next_completion_id: 0, + next_inlay_id: 0, + code_action_providers, + available_code_actions: Default::default(), + code_actions_task: Default::default(), + quick_selection_highlight_task: Default::default(), + debounced_selection_highlight_task: Default::default(), + document_highlights_task: Default::default(), + linked_editing_range_task: Default::default(), + pending_rename: Default::default(), + searchable: true, + cursor_shape: EditorSettings::get_global(cx) + .cursor_shape + .unwrap_or_default(), + current_line_highlight: None, + autoindent_mode: Some(AutoindentMode::EachLine), + collapse_matches: false, + workspace: None, + input_enabled: true, + use_modal_editing: mode.is_full(), + read_only: false, + use_autoclose: true, + use_auto_surround: true, + auto_replace_emoji_shortcode: false, + jsx_tag_auto_close_enabled_in_any_buffer: false, + leader_peer_id: None, + remote_id: None, + hover_state: Default::default(), + pending_mouse_down: None, + hovered_link_state: Default::default(), + edit_prediction_provider: None, + active_inline_completion: None, + stale_inline_completion_in_menu: None, + edit_prediction_preview: EditPredictionPreview::Inactive { + released_too_fast: false, + }, + inline_diagnostics_enabled: mode.is_full(), + inline_value_cache: InlineValueCache::new(inlay_hint_settings.show_value_hints), + inlay_hint_cache: InlayHintCache::new(inlay_hint_settings), + + gutter_hovered: false, + pixel_position_of_newest_cursor: None, + last_bounds: None, + last_position_map: None, + expect_bounds_change: None, + gutter_dimensions: GutterDimensions::default(), + style: None, + show_cursor_names: false, + hovered_cursors: Default::default(), + next_editor_action_id: EditorActionId::default(), + editor_actions: Rc::default(), + inline_completions_hidden_for_vim_mode: false, + show_inline_completions_override: None, + menu_inline_completions_policy: MenuInlineCompletionsPolicy::ByProvider, + edit_prediction_settings: EditPredictionSettings::Disabled, + edit_prediction_indent_conflict: false, + edit_prediction_requires_modifier_in_indent_conflict: true, + custom_context_menu: None, + show_git_blame_gutter: false, + show_git_blame_inline: false, + show_selection_menu: None, + show_git_blame_inline_delay_task: None, + git_blame_inline_enabled: ProjectSettings::get_global(cx).git.inline_blame_enabled(), + render_diff_hunk_controls: Arc::new(render_diff_hunk_controls), + serialize_dirty_buffers: ProjectSettings::get_global(cx) + .session + .restore_unsaved_buffers, + blame: None, + blame_subscription: None, + tasks: Default::default(), + + breakpoint_store, + gutter_breakpoint_indicator: (None, None), + _subscriptions: vec![ + cx.observe(&buffer, Self::on_buffer_changed), + cx.subscribe_in(&buffer, window, Self::on_buffer_event), + cx.observe_in(&display_map, window, Self::on_display_map_changed), + cx.observe(&blink_manager, |_, _, cx| cx.notify()), + cx.observe_global_in::(window, Self::settings_changed), + observe_buffer_font_size_adjustment(cx, |_, cx| cx.notify()), + cx.observe_window_activation(window, |editor, window, cx| { + let active = window.is_window_active(); + editor.blink_manager.update(cx, |blink_manager, cx| { + // if active { + // blink_manager.enable(cx); + // } else { + // blink_manager.disable(cx); + // } + }); + }), + ], + tasks_update_task: None, + linked_edit_ranges: Default::default(), + in_project_search: false, + previous_search_ranges: None, + breadcrumb_header: None, + focused_block: None, + next_scroll_position: NextScrollCursorCenterTopBottom::default(), + addons: HashMap::default(), + registered_buffers: HashMap::default(), + _scroll_cursor_center_top_bottom_task: Task::ready(()), + selection_mark_mode: false, + toggle_fold_multiple_buffers: Task::ready(()), + serialize_selections: Task::ready(()), + serialize_folds: Task::ready(()), + text_style_refinement: None, + load_diff_task: load_uncommitted_diff, + mouse_cursor_hidden: false, + hide_mouse_mode: EditorSettings::get_global(cx) + .hide_mouse + .unwrap_or_default(), + change_list: ChangeList::new(), + }; + if let Some(breakpoints) = this.breakpoint_store.as_ref() { + this._subscriptions + .push(cx.observe(breakpoints, |_, _, cx| { + cx.notify(); + })); + } + this.tasks_update_task = Some(this.refresh_runnables(window, cx)); + this._subscriptions.extend(project_subscriptions); + + this._subscriptions.push(cx.subscribe_in( + &cx.entity(), + window, + |editor, _, e: &EditorEvent, window, cx| match e { + EditorEvent::ScrollPositionChanged { local, .. } => { + if *local { + let new_anchor = editor.scroll_manager.anchor(); + let snapshot = editor.snapshot(window, cx); + editor.update_restoration_data(cx, move |data| { + data.scroll_position = ( + new_anchor.top_row(&snapshot.buffer_snapshot), + new_anchor.offset, + ); + }); + editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape); + editor.inline_blame_popover.take(); + } + } + EditorEvent::Edited { .. } => { + if !vim_enabled(cx) { + let (map, selections) = editor.selections.all_adjusted_display(cx); + let pop_state = editor + .change_list + .last() + .map(|previous| { + previous.len() == selections.len() + && previous.iter().enumerate().all(|(ix, p)| { + p.to_display_point(&map).row() + == selections[ix].head().row() + }) + }) + .unwrap_or(false); + let new_positions = selections + .into_iter() + .map(|s| map.display_point_to_anchor(s.head(), Bias::Left)) + .collect(); + editor + .change_list + .push_to_change_list(pop_state, new_positions); + } + } + _ => (), + }, + )); + + if let Some(dap_store) = this + .project + .as_ref() + .map(|project| project.read(cx).dap_store()) + { + let weak_editor = cx.weak_entity(); + + this._subscriptions + .push( + cx.observe_new::(move |_, _, cx| { + let session_entity = cx.entity(); + weak_editor + .update(cx, |editor, cx| { + editor._subscriptions.push( + cx.subscribe(&session_entity, Self::on_debug_session_event), + ); + }) + .ok(); + }), + ); + + for session in dap_store.read(cx).sessions().cloned().collect::>() { + this._subscriptions + .push(cx.subscribe(&session, Self::on_debug_session_event)); + } + } + + this.end_selection(window, cx); + this.scroll_manager.show_scrollbars(window, cx); + jsx_tag_auto_close::refresh_enabled_in_any_buffer(&mut this, &buffer, cx); + + if mode.is_full() { + let should_auto_hide_scrollbars = cx.should_auto_hide_scrollbars(); + cx.set_global(ScrollbarAutoHide(should_auto_hide_scrollbars)); + + if this.git_blame_inline_enabled { + this.git_blame_inline_enabled = true; + this.start_git_blame_inline(false, window, cx); + } + + this.go_to_active_debug_line(window, cx); + + if let Some(buffer) = buffer.read(cx).as_singleton() { + if let Some(project) = this.project.as_ref() { + let handle = project.update(cx, |project, cx| { + project.register_buffer_with_language_servers(&buffer, cx) + }); + this.registered_buffers + .insert(buffer.read(cx).remote_id(), handle); + } + } + } + + this.report_editor_event("Editor Opened", None, cx); + this + } + + pub fn deploy_mouse_context_menu( + &mut self, + position: gpui::Point, + context_menu: Entity, + window: &mut Window, + cx: &mut Context, + ) { + self.mouse_context_menu = Some(MouseContextMenu::new( + self, + crate::mouse_context_menu::MenuPosition::PinnedToScreen(position), + context_menu, + window, + cx, + )); + } + + pub fn mouse_menu_is_focused(&self, window: &Window, cx: &App) -> bool { + self.mouse_context_menu + .as_ref() + .is_some_and(|menu| menu.context_menu.focus_handle(cx).is_focused(window)) + } + + fn key_context(&self, window: &Window, cx: &App) -> KeyContext { + self.key_context_internal(self.has_active_inline_completion(), window, cx) + } + + fn key_context_internal( + &self, + has_active_edit_prediction: bool, + window: &Window, + cx: &App, + ) -> KeyContext { + let mut key_context = KeyContext::new_with_defaults(); + key_context.add("Editor"); + let mode = match self.mode { + EditorMode::SingleLine { .. } => "single_line", + EditorMode::AutoHeight { .. } => "auto_height", + EditorMode::Full { .. } => "full", + }; + + if EditorSettings::jupyter_enabled(cx) { + key_context.add("jupyter"); + } + + key_context.set("mode", mode); + if self.pending_rename.is_some() { + key_context.add("renaming"); + } + + match self.context_menu.borrow().as_ref() { + Some(CodeContextMenu::Completions(_)) => { + key_context.add("menu"); + key_context.add("showing_completions"); + } + Some(CodeContextMenu::CodeActions(_)) => { + key_context.add("menu"); + key_context.add("showing_code_actions") + } + None => {} + } + + // Disable vim contexts when a sub-editor (e.g. rename/inline assistant) is focused. + if !self.focus_handle(cx).contains_focused(window, cx) + || (self.is_focused(window) || self.mouse_menu_is_focused(window, cx)) + { + for addon in self.addons.values() { + addon.extend_key_context(&mut key_context, cx) + } + } + + if let Some(singleton_buffer) = self.buffer.read(cx).as_singleton() { + if let Some(extension) = singleton_buffer + .read(cx) + .file() + .and_then(|file| file.path().extension()?.to_str()) + { + key_context.set("extension", extension.to_string()); + } + } else { + key_context.add("multibuffer"); + } + + if has_active_edit_prediction { + if self.edit_prediction_in_conflict() { + key_context.add(EDIT_PREDICTION_CONFLICT_KEY_CONTEXT); + } else { + key_context.add(EDIT_PREDICTION_KEY_CONTEXT); + key_context.add("copilot_suggestion"); + } + } + + if self.selection_mark_mode { + key_context.add("selection_mode"); + } + + key_context + } + + pub fn hide_mouse_cursor(&mut self, origin: &HideMouseCursorOrigin) { + self.mouse_cursor_hidden = match origin { + HideMouseCursorOrigin::TypingAction => { + matches!( + self.hide_mouse_mode, + HideMouseMode::OnTyping | HideMouseMode::OnTypingAndMovement + ) + } + HideMouseCursorOrigin::MovementAction => { + matches!(self.hide_mouse_mode, HideMouseMode::OnTypingAndMovement) + } + }; + } + + pub fn edit_prediction_in_conflict(&self) -> bool { + if !self.show_edit_predictions_in_menu() { + return false; + } + + let showing_completions = self + .context_menu + .borrow() + .as_ref() + .map_or(false, |context| { + matches!(context, CodeContextMenu::Completions(_)) + }); + + showing_completions + || self.edit_prediction_requires_modifier() + // Require modifier key when the cursor is on leading whitespace, to allow `tab` + // bindings to insert tab characters. + || (self.edit_prediction_requires_modifier_in_indent_conflict && self.edit_prediction_indent_conflict) + } + + pub fn accept_edit_prediction_keybind( + &self, + window: &Window, + cx: &App, + ) -> AcceptEditPredictionBinding { + let key_context = self.key_context_internal(true, window, cx); + let in_conflict = self.edit_prediction_in_conflict(); + + AcceptEditPredictionBinding( + window + .bindings_for_action_in_context(&AcceptEditPrediction, key_context) + .into_iter() + .filter(|binding| { + !in_conflict + || binding + .keystrokes() + .first() + .map_or(false, |keystroke| keystroke.modifiers.modified()) + }) + .rev() + .min_by_key(|binding| { + binding + .keystrokes() + .first() + .map_or(u8::MAX, |k| k.modifiers.number_of_modifiers()) + }), + ) + } + + pub fn new_file( + workspace: &mut Workspace, + _: &workspace::NewFile, + window: &mut Window, + cx: &mut Context, + ) { + Self::new_in_workspace(workspace, window, cx).detach_and_prompt_err( + "Failed to create buffer", + window, + cx, + |e, _, _| match e.error_code() { + ErrorCode::RemoteUpgradeRequired => Some(format!( + "The remote instance of Zed does not support this yet. It must be upgraded to {}", + e.error_tag("required").unwrap_or("the latest version") + )), + _ => None, + }, + ); + } + + pub fn new_in_workspace( + workspace: &mut Workspace, + window: &mut Window, + cx: &mut Context, + ) -> Task>> { + let project = workspace.project().clone(); + let create = project.update(cx, |project, cx| project.create_buffer(cx)); + + cx.spawn_in(window, async move |workspace, cx| { + let buffer = create.await?; + workspace.update_in(cx, |workspace, window, cx| { + let editor = + cx.new(|cx| Editor::for_buffer(buffer, Some(project.clone()), window, cx)); + workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx); + editor + }) + }) + } + + fn new_file_vertical( + workspace: &mut Workspace, + _: &workspace::NewFileSplitVertical, + window: &mut Window, + cx: &mut Context, + ) { + Self::new_file_in_direction(workspace, SplitDirection::vertical(cx), window, cx) + } + + fn new_file_horizontal( + workspace: &mut Workspace, + _: &workspace::NewFileSplitHorizontal, + window: &mut Window, + cx: &mut Context, + ) { + Self::new_file_in_direction(workspace, SplitDirection::horizontal(cx), window, cx) + } + + fn new_file_in_direction( + workspace: &mut Workspace, + direction: SplitDirection, + window: &mut Window, + cx: &mut Context, + ) { + let project = workspace.project().clone(); + let create = project.update(cx, |project, cx| project.create_buffer(cx)); + + cx.spawn_in(window, async move |workspace, cx| { + let buffer = create.await?; + workspace.update_in(cx, move |workspace, window, cx| { + workspace.split_item( + direction, + Box::new( + cx.new(|cx| Editor::for_buffer(buffer, Some(project.clone()), window, cx)), + ), + window, + cx, + ) + })?; + anyhow::Ok(()) + }) + .detach_and_prompt_err("Failed to create buffer", window, cx, |e, _, _| { + match e.error_code() { + ErrorCode::RemoteUpgradeRequired => Some(format!( + "The remote instance of Zed does not support this yet. It must be upgraded to {}", + e.error_tag("required").unwrap_or("the latest version") + )), + _ => None, + } + }); + } + + pub fn leader_peer_id(&self) -> Option { + self.leader_peer_id + } + + pub fn buffer(&self) -> &Entity { + &self.buffer + } + + pub fn workspace(&self) -> Option> { + self.workspace.as_ref()?.0.upgrade() + } + + pub fn title<'a>(&self, cx: &'a App) -> Cow<'a, str> { + self.buffer().read(cx).title(cx) + } + + pub fn snapshot(&self, window: &mut Window, cx: &mut App) -> EditorSnapshot { + let git_blame_gutter_max_author_length = self + .render_git_blame_gutter(cx) + .then(|| { + if let Some(blame) = self.blame.as_ref() { + let max_author_length = + blame.update(cx, |blame, cx| blame.max_author_length(cx)); + Some(max_author_length) + } else { + None + } + }) + .flatten(); + + EditorSnapshot { + mode: self.mode, + show_gutter: self.show_gutter, + show_line_numbers: self.show_line_numbers, + show_git_diff_gutter: self.show_git_diff_gutter, + show_code_actions: self.show_code_actions, + show_runnables: self.show_runnables, + show_breakpoints: self.show_breakpoints, + git_blame_gutter_max_author_length, + display_snapshot: self.display_map.update(cx, |map, cx| map.snapshot(cx)), + scroll_anchor: self.scroll_manager.anchor(), + ongoing_scroll: self.scroll_manager.ongoing_scroll(), + placeholder_text: self.placeholder_text.clone(), + is_focused: self.focus_handle.is_focused(window), + current_line_highlight: self + .current_line_highlight + .unwrap_or_else(|| EditorSettings::get_global(cx).current_line_highlight), + gutter_hovered: self.gutter_hovered, + } + } + + pub fn language_at(&self, point: T, cx: &App) -> Option> { + self.buffer.read(cx).language_at(point, cx) + } + + pub fn file_at(&self, point: T, cx: &App) -> Option> { + self.buffer.read(cx).read(cx).file_at(point).cloned() + } + + pub fn active_excerpt( + &self, + cx: &App, + ) -> Option<(ExcerptId, Entity, Range)> { + self.buffer + .read(cx) + .excerpt_containing(self.selections.newest_anchor().head(), cx) + } + + pub fn mode(&self) -> EditorMode { + self.mode + } + + pub fn set_mode(&mut self, mode: EditorMode) { + self.mode = mode; + } + + pub fn collaboration_hub(&self) -> Option<&dyn CollaborationHub> { + self.collaboration_hub.as_deref() + } + + pub fn set_collaboration_hub(&mut self, hub: Box) { + self.collaboration_hub = Some(hub); + } + + pub fn set_in_project_search(&mut self, in_project_search: bool) { + self.in_project_search = in_project_search; + } + + pub fn set_custom_context_menu( + &mut self, + f: impl 'static + + Fn( + &mut Self, + DisplayPoint, + &mut Window, + &mut Context, + ) -> Option>, + ) { + self.custom_context_menu = Some(Box::new(f)) + } + + pub fn set_completion_provider(&mut self, provider: Option>) { + self.completion_provider = provider; + } + + pub fn semantics_provider(&self) -> Option> { + self.semantics_provider.clone() + } + + pub fn set_semantics_provider(&mut self, provider: Option>) { + self.semantics_provider = provider; + } + + pub fn set_edit_prediction_provider( + &mut self, + provider: Option>, + window: &mut Window, + cx: &mut Context, + ) where + T: EditPredictionProvider, + { + self.edit_prediction_provider = + provider.map(|provider| RegisteredInlineCompletionProvider { + _subscription: cx.observe_in(&provider, window, |this, _, window, cx| { + if this.focus_handle.is_focused(window) { + this.update_visible_inline_completion(window, cx); + } + }), + provider: Arc::new(provider), + }); + self.update_edit_prediction_settings(cx); + self.refresh_inline_completion(false, false, window, cx); + } + + pub fn placeholder_text(&self) -> Option<&str> { + self.placeholder_text.as_deref() + } + + pub fn set_placeholder_text( + &mut self, + placeholder_text: impl Into>, + cx: &mut Context, + ) { + let placeholder_text = Some(placeholder_text.into()); + if self.placeholder_text != placeholder_text { + self.placeholder_text = placeholder_text; + cx.notify(); + } + } + + pub fn set_cursor_shape(&mut self, cursor_shape: CursorShape, cx: &mut Context) { + self.cursor_shape = cursor_shape; + + // Disrupt blink for immediate user feedback that the cursor shape has changed + self.blink_manager.update(cx, BlinkManager::show_cursor); + + cx.notify(); + } + + pub fn set_current_line_highlight( + &mut self, + current_line_highlight: Option, + ) { + self.current_line_highlight = current_line_highlight; + } + + pub fn set_collapse_matches(&mut self, collapse_matches: bool) { + self.collapse_matches = collapse_matches; + } + + fn register_buffers_with_language_servers(&mut self, cx: &mut Context) { + let buffers = self.buffer.read(cx).all_buffers(); + let Some(project) = self.project.as_ref() else { + return; + }; + project.update(cx, |project, cx| { + for buffer in buffers { + self.registered_buffers + .entry(buffer.read(cx).remote_id()) + .or_insert_with(|| project.register_buffer_with_language_servers(&buffer, cx)); + } + }) + } + + pub fn range_for_match(&self, range: &Range) -> Range { + if self.collapse_matches { + return range.start..range.start; + } + range.clone() + } + + pub fn set_clip_at_line_ends(&mut self, clip: bool, cx: &mut Context) { + if self.display_map.read(cx).clip_at_line_ends != clip { + self.display_map + .update(cx, |map, _| map.clip_at_line_ends = clip); + } + } + + pub fn set_input_enabled(&mut self, input_enabled: bool) { + self.input_enabled = input_enabled; + } + + pub fn set_inline_completions_hidden_for_vim_mode( + &mut self, + hidden: bool, + window: &mut Window, + cx: &mut Context, + ) { + if hidden != self.inline_completions_hidden_for_vim_mode { + self.inline_completions_hidden_for_vim_mode = hidden; + if hidden { + self.update_visible_inline_completion(window, cx); + } else { + self.refresh_inline_completion(true, false, window, cx); + } + } + } + + pub fn set_menu_inline_completions_policy(&mut self, value: MenuInlineCompletionsPolicy) { + self.menu_inline_completions_policy = value; + } + + pub fn set_autoindent(&mut self, autoindent: bool) { + if autoindent { + self.autoindent_mode = Some(AutoindentMode::EachLine); + } else { + self.autoindent_mode = None; + } + } + + pub fn read_only(&self, cx: &App) -> bool { + self.read_only || self.buffer.read(cx).read_only() + } + + pub fn set_read_only(&mut self, read_only: bool) { + self.read_only = read_only; + } + + pub fn set_use_autoclose(&mut self, autoclose: bool) { + self.use_autoclose = autoclose; + } + + pub fn set_use_auto_surround(&mut self, auto_surround: bool) { + self.use_auto_surround = auto_surround; + } + + pub fn set_auto_replace_emoji_shortcode(&mut self, auto_replace: bool) { + self.auto_replace_emoji_shortcode = auto_replace; + } + + pub fn toggle_edit_predictions( + &mut self, + _: &ToggleEditPrediction, + window: &mut Window, + cx: &mut Context, + ) { + if self.show_inline_completions_override.is_some() { + self.set_show_edit_predictions(None, window, cx); + } else { + let show_edit_predictions = !self.edit_predictions_enabled(); + self.set_show_edit_predictions(Some(show_edit_predictions), window, cx); + } + } + + pub fn set_show_edit_predictions( + &mut self, + show_edit_predictions: Option, + window: &mut Window, + cx: &mut Context, + ) { + self.show_inline_completions_override = show_edit_predictions; + self.update_edit_prediction_settings(cx); + + if let Some(false) = show_edit_predictions { + self.discard_inline_completion(false, cx); + } else { + self.refresh_inline_completion(false, true, window, cx); + } + } + + fn inline_completions_disabled_in_scope( + &self, + buffer: &Entity, + buffer_position: language::Anchor, + cx: &App, + ) -> bool { + let snapshot = buffer.read(cx).snapshot(); + let settings = snapshot.settings_at(buffer_position, cx); + + let Some(scope) = snapshot.language_scope_at(buffer_position) else { + return false; + }; + + scope.override_name().map_or(false, |scope_name| { + settings + .edit_predictions_disabled_in + .iter() + .any(|s| s == scope_name) + }) + } + + pub fn set_use_modal_editing(&mut self, to: bool) { + self.use_modal_editing = to; + } + + pub fn use_modal_editing(&self) -> bool { + self.use_modal_editing + } + + fn selections_did_change( + &mut self, + local: bool, + old_cursor_position: &Anchor, + show_completions: bool, + window: &mut Window, + cx: &mut Context, + ) { + window.invalidate_character_coordinates(); + + // Copy selections to primary selection buffer + #[cfg(any(target_os = "linux", target_os = "freebsd"))] + if local { + let selections = self.selections.all::(cx); + let buffer_handle = self.buffer.read(cx).read(cx); + + let mut text = String::new(); + for (index, selection) in selections.iter().enumerate() { + let text_for_selection = buffer_handle + .text_for_range(selection.start..selection.end) + .collect::(); + + text.push_str(&text_for_selection); + if index != selections.len() - 1 { + text.push('\n'); + } + } + + if !text.is_empty() { + cx.write_to_primary(ClipboardItem::new_string(text)); + } + } + + if self.focus_handle.is_focused(window) && self.leader_peer_id.is_none() { + self.buffer.update(cx, |buffer, cx| { + buffer.set_active_selections( + &self.selections.disjoint_anchors(), + self.selections.line_mode, + self.cursor_shape, + cx, + ) + }); + } + let display_map = self + .display_map + .update(cx, |display_map, cx| display_map.snapshot(cx)); + let buffer = &display_map.buffer_snapshot; + self.add_selections_state = None; + self.select_next_state = None; + self.select_prev_state = None; + self.select_syntax_node_history.try_clear(); + self.invalidate_autoclose_regions(&self.selections.disjoint_anchors(), buffer); + self.snippet_stack + .invalidate(&self.selections.disjoint_anchors(), buffer); + self.take_rename(false, window, cx); + + let new_cursor_position = self.selections.newest_anchor().head(); + + self.push_to_nav_history( + *old_cursor_position, + Some(new_cursor_position.to_point(buffer)), + false, + cx, + ); + + if local { + let new_cursor_position = self.selections.newest_anchor().head(); + let mut context_menu = self.context_menu.borrow_mut(); + let completion_menu = match context_menu.as_ref() { + Some(CodeContextMenu::Completions(menu)) => Some(menu), + _ => { + *context_menu = None; + None + } + }; + if let Some(buffer_id) = new_cursor_position.buffer_id { + if !self.registered_buffers.contains_key(&buffer_id) { + if let Some(project) = self.project.as_ref() { + project.update(cx, |project, cx| { + let Some(buffer) = self.buffer.read(cx).buffer(buffer_id) else { + return; + }; + self.registered_buffers.insert( + buffer_id, + project.register_buffer_with_language_servers(&buffer, cx), + ); + }) + } + } + } + + if let Some(completion_menu) = completion_menu { + let cursor_position = new_cursor_position.to_offset(buffer); + let (word_range, kind) = + buffer.surrounding_word(completion_menu.initial_position, true); + if kind == Some(CharKind::Word) + && word_range.to_inclusive().contains(&cursor_position) + { + let mut completion_menu = completion_menu.clone(); + drop(context_menu); + + let query = Self::completion_query(buffer, cursor_position); + cx.spawn(async move |this, cx| { + completion_menu + .filter(query.as_deref(), cx.background_executor().clone()) + .await; + + this.update(cx, |this, cx| { + let mut context_menu = this.context_menu.borrow_mut(); + let Some(CodeContextMenu::Completions(menu)) = context_menu.as_ref() + else { + return; + }; + + if menu.id > completion_menu.id { + return; + } + + *context_menu = Some(CodeContextMenu::Completions(completion_menu)); + drop(context_menu); + cx.notify(); + }) + }) + .detach(); + + if show_completions { + self.show_completions(&ShowCompletions { trigger: None }, window, cx); + } + } else { + drop(context_menu); + self.hide_context_menu(window, cx); + } + } else { + drop(context_menu); + } + + hide_hover(self, cx); + + if old_cursor_position.to_display_point(&display_map).row() + != new_cursor_position.to_display_point(&display_map).row() + { + self.available_code_actions.take(); + } + self.refresh_code_actions(window, cx); + self.refresh_document_highlights(cx); + self.refresh_selected_text_highlights(false, window, cx); + refresh_matching_bracket_highlights(self, window, cx); + self.update_visible_inline_completion(window, cx); + self.edit_prediction_requires_modifier_in_indent_conflict = true; + linked_editing_ranges::refresh_linked_ranges(self, window, cx); + self.inline_blame_popover.take(); + if self.git_blame_inline_enabled { + self.start_inline_blame_timer(window, cx); + } + } + + self.blink_manager.update(cx, BlinkManager::pause_blinking); + cx.emit(EditorEvent::SelectionsChanged { local }); + + let selections = &self.selections.disjoint; + if selections.len() == 1 { + cx.emit(SearchEvent::ActiveMatchChanged) + } + if local { + if let Some((_, _, buffer_snapshot)) = buffer.as_singleton() { + let inmemory_selections = selections + .iter() + .map(|s| { + text::ToPoint::to_point(&s.range().start.text_anchor, buffer_snapshot) + ..text::ToPoint::to_point(&s.range().end.text_anchor, buffer_snapshot) + }) + .collect(); + self.update_restoration_data(cx, |data| { + data.selections = inmemory_selections; + }); + + if WorkspaceSettings::get(None, cx).restore_on_startup + != RestoreOnStartupBehavior::None + { + if let Some(workspace_id) = + self.workspace.as_ref().and_then(|workspace| workspace.1) + { + let snapshot = self.buffer().read(cx).snapshot(cx); + let selections = selections.clone(); + let background_executor = cx.background_executor().clone(); + let editor_id = cx.entity().entity_id().as_u64() as ItemId; + self.serialize_selections = cx.background_spawn(async move { + background_executor.timer(SERIALIZATION_THROTTLE_TIME).await; + let db_selections = selections + .iter() + .map(|selection| { + ( + selection.start.to_offset(&snapshot), + selection.end.to_offset(&snapshot), + ) + }) + .collect(); + + DB.save_editor_selections(editor_id, workspace_id, db_selections) + .await + .with_context(|| format!("persisting editor selections for editor {editor_id}, workspace {workspace_id:?}")) + .log_err(); + }); + } + } + } + } + + cx.notify(); + } + + fn folds_did_change(&mut self, cx: &mut Context) { + use text::ToOffset as _; + use text::ToPoint as _; + + if WorkspaceSettings::get(None, cx).restore_on_startup == RestoreOnStartupBehavior::None { + return; + } + + let Some(singleton) = self.buffer().read(cx).as_singleton() else { + return; + }; + + let snapshot = singleton.read(cx).snapshot(); + let inmemory_folds = self.display_map.update(cx, |display_map, cx| { + let display_snapshot = display_map.snapshot(cx); + + display_snapshot + .folds_in_range(0..display_snapshot.buffer_snapshot.len()) + .map(|fold| { + fold.range.start.text_anchor.to_point(&snapshot) + ..fold.range.end.text_anchor.to_point(&snapshot) + }) + .collect() + }); + self.update_restoration_data(cx, |data| { + data.folds = inmemory_folds; + }); + + let Some(workspace_id) = self.workspace.as_ref().and_then(|workspace| workspace.1) else { + return; + }; + let background_executor = cx.background_executor().clone(); + let editor_id = cx.entity().entity_id().as_u64() as ItemId; + let db_folds = self.display_map.update(cx, |display_map, cx| { + display_map + .snapshot(cx) + .folds_in_range(0..snapshot.len()) + .map(|fold| { + ( + fold.range.start.text_anchor.to_offset(&snapshot), + fold.range.end.text_anchor.to_offset(&snapshot), + ) + }) + .collect() + }); + self.serialize_folds = cx.background_spawn(async move { + background_executor.timer(SERIALIZATION_THROTTLE_TIME).await; + DB.save_editor_folds(editor_id, workspace_id, db_folds) + .await + .with_context(|| { + format!( + "persisting editor folds for editor {editor_id}, workspace {workspace_id:?}" + ) + }) + .log_err(); + }); + } + + pub fn sync_selections( + &mut self, + other: Entity, + cx: &mut Context, + ) -> gpui::Subscription { + let other_selections = other.read(cx).selections.disjoint.to_vec(); + self.selections.change_with(cx, |selections| { + selections.select_anchors(other_selections); + }); + + let other_subscription = + cx.subscribe(&other, |this, other, other_evt, cx| match other_evt { + EditorEvent::SelectionsChanged { local: true } => { + let other_selections = other.read(cx).selections.disjoint.to_vec(); + if other_selections.is_empty() { + return; + } + this.selections.change_with(cx, |selections| { + selections.select_anchors(other_selections); + }); + } + _ => {} + }); + + let this_subscription = + cx.subscribe_self::(move |this, this_evt, cx| match this_evt { + EditorEvent::SelectionsChanged { local: true } => { + let these_selections = this.selections.disjoint.to_vec(); + if these_selections.is_empty() { + return; + } + other.update(cx, |other_editor, cx| { + other_editor.selections.change_with(cx, |selections| { + selections.select_anchors(these_selections); + }) + }); + } + _ => {} + }); + + Subscription::join(other_subscription, this_subscription) + } + + pub fn change_selections( + &mut self, + autoscroll: Option, + window: &mut Window, + cx: &mut Context, + change: impl FnOnce(&mut MutableSelectionsCollection<'_>) -> R, + ) -> R { + self.change_selections_inner(autoscroll, true, window, cx, change) + } + + fn change_selections_inner( + &mut self, + autoscroll: Option, + request_completions: bool, + window: &mut Window, + cx: &mut Context, + change: impl FnOnce(&mut MutableSelectionsCollection<'_>) -> R, + ) -> R { + let old_cursor_position = self.selections.newest_anchor().head(); + self.push_to_selection_history(); + + let (changed, result) = self.selections.change_with(cx, change); + + if changed { + if let Some(autoscroll) = autoscroll { + self.request_autoscroll(autoscroll, cx); + } + self.selections_did_change(true, &old_cursor_position, request_completions, window, cx); + + if self.should_open_signature_help_automatically( + &old_cursor_position, + self.signature_help_state.backspace_pressed(), + cx, + ) { + self.show_signature_help(&ShowSignatureHelp, window, cx); + } + self.signature_help_state.set_backspace_pressed(false); + } + + result + } + + pub fn edit(&mut self, edits: I, cx: &mut Context) + where + I: IntoIterator, T)>, + S: ToOffset, + T: Into>, + { + if self.read_only(cx) { + return; + } + + self.buffer + .update(cx, |buffer, cx| buffer.edit(edits, None, cx)); + } + + pub fn edit_with_autoindent(&mut self, edits: I, cx: &mut Context) + where + I: IntoIterator, T)>, + S: ToOffset, + T: Into>, + { + if self.read_only(cx) { + return; + } + + self.buffer.update(cx, |buffer, cx| { + buffer.edit(edits, self.autoindent_mode.clone(), cx) + }); + } + + pub fn edit_with_block_indent( + &mut self, + edits: I, + original_indent_columns: Vec>, + cx: &mut Context, + ) where + I: IntoIterator, T)>, + S: ToOffset, + T: Into>, + { + if self.read_only(cx) { + return; + } + + self.buffer.update(cx, |buffer, cx| { + buffer.edit( + edits, + Some(AutoindentMode::Block { + original_indent_columns, + }), + cx, + ) + }); + } + + fn select(&mut self, phase: SelectPhase, window: &mut Window, cx: &mut Context) { + self.hide_context_menu(window, cx); + + match phase { + SelectPhase::Begin { + position, + add, + click_count, + } => self.begin_selection(position, add, click_count, window, cx), + SelectPhase::BeginColumnar { + position, + goal_column, + reset, + } => self.begin_columnar_selection(position, goal_column, reset, window, cx), + SelectPhase::Extend { + position, + click_count, + } => self.extend_selection(position, click_count, window, cx), + SelectPhase::Update { + position, + goal_column, + scroll_delta, + } => self.update_selection(position, goal_column, scroll_delta, window, cx), + SelectPhase::End => self.end_selection(window, cx), + } + } + + fn extend_selection( + &mut self, + position: DisplayPoint, + click_count: usize, + window: &mut Window, + cx: &mut Context, + ) { + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let tail = self.selections.newest::(cx).tail(); + self.begin_selection(position, false, click_count, window, cx); + + let position = position.to_offset(&display_map, Bias::Left); + let tail_anchor = display_map.buffer_snapshot.anchor_before(tail); + + let mut pending_selection = self + .selections + .pending_anchor() + .expect("extend_selection not called with pending selection"); + if position >= tail { + pending_selection.start = tail_anchor; + } else { + pending_selection.end = tail_anchor; + pending_selection.reversed = true; + } + + let mut pending_mode = self.selections.pending_mode().unwrap(); + match &mut pending_mode { + SelectMode::Word(range) | SelectMode::Line(range) => *range = tail_anchor..tail_anchor, + _ => {} + } + + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.set_pending(pending_selection, pending_mode) + }); + } + + fn begin_selection( + &mut self, + position: DisplayPoint, + add: bool, + click_count: usize, + window: &mut Window, + cx: &mut Context, + ) { + if !self.focus_handle.is_focused(window) { + self.last_focused_descendant = None; + window.focus(&self.focus_handle); + } + + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let buffer = &display_map.buffer_snapshot; + let newest_selection = self.selections.newest_anchor().clone(); + let position = display_map.clip_point(position, Bias::Left); + + let start; + let end; + let mode; + let mut auto_scroll; + match click_count { + 1 => { + start = buffer.anchor_before(position.to_point(&display_map)); + end = start; + mode = SelectMode::Character; + auto_scroll = true; + } + 2 => { + let range = movement::surrounding_word(&display_map, position); + start = buffer.anchor_before(range.start.to_point(&display_map)); + end = buffer.anchor_before(range.end.to_point(&display_map)); + mode = SelectMode::Word(start..end); + auto_scroll = true; + } + 3 => { + let position = display_map + .clip_point(position, Bias::Left) + .to_point(&display_map); + let line_start = display_map.prev_line_boundary(position).0; + let next_line_start = buffer.clip_point( + display_map.next_line_boundary(position).0 + Point::new(1, 0), + Bias::Left, + ); + start = buffer.anchor_before(line_start); + end = buffer.anchor_before(next_line_start); + mode = SelectMode::Line(start..end); + auto_scroll = true; + } + _ => { + start = buffer.anchor_before(0); + end = buffer.anchor_before(buffer.len()); + mode = SelectMode::All; + auto_scroll = false; + } + } + auto_scroll &= EditorSettings::get_global(cx).autoscroll_on_clicks; + + let point_to_delete: Option = { + let selected_points: Vec> = + self.selections.disjoint_in_range(start..end, cx); + + if !add || click_count > 1 { + None + } else if !selected_points.is_empty() { + Some(selected_points[0].id) + } else { + let clicked_point_already_selected = + self.selections.disjoint.iter().find(|selection| { + selection.start.to_point(buffer) == start.to_point(buffer) + || selection.end.to_point(buffer) == end.to_point(buffer) + }); + + clicked_point_already_selected.map(|selection| selection.id) + } + }; + + let selections_count = self.selections.count(); + + self.change_selections(auto_scroll.then(Autoscroll::newest), window, cx, |s| { + if let Some(point_to_delete) = point_to_delete { + s.delete(point_to_delete); + + if selections_count == 1 { + s.set_pending_anchor_range(start..end, mode); + } + } else { + if !add { + s.clear_disjoint(); + } else if click_count > 1 { + s.delete(newest_selection.id) + } + + s.set_pending_anchor_range(start..end, mode); + } + }); + } + + fn begin_columnar_selection( + &mut self, + position: DisplayPoint, + goal_column: u32, + reset: bool, + window: &mut Window, + cx: &mut Context, + ) { + if !self.focus_handle.is_focused(window) { + self.last_focused_descendant = None; + window.focus(&self.focus_handle); + } + + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + + if reset { + let pointer_position = display_map + .buffer_snapshot + .anchor_before(position.to_point(&display_map)); + + self.change_selections(Some(Autoscroll::newest()), window, cx, |s| { + s.clear_disjoint(); + s.set_pending_anchor_range( + pointer_position..pointer_position, + SelectMode::Character, + ); + }); + } + + let tail = self.selections.newest::(cx).tail(); + self.columnar_selection_tail = Some(display_map.buffer_snapshot.anchor_before(tail)); + + if !reset { + self.select_columns( + tail.to_display_point(&display_map), + position, + goal_column, + &display_map, + window, + cx, + ); + } + } + + fn update_selection( + &mut self, + position: DisplayPoint, + goal_column: u32, + scroll_delta: gpui::Point, + window: &mut Window, + cx: &mut Context, + ) { + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + + if let Some(tail) = self.columnar_selection_tail.as_ref() { + let tail = tail.to_display_point(&display_map); + self.select_columns(tail, position, goal_column, &display_map, window, cx); + } else if let Some(mut pending) = self.selections.pending_anchor() { + let buffer = self.buffer.read(cx).snapshot(cx); + let head; + let tail; + let mode = self.selections.pending_mode().unwrap(); + match &mode { + SelectMode::Character => { + head = position.to_point(&display_map); + tail = pending.tail().to_point(&buffer); + } + SelectMode::Word(original_range) => { + let original_display_range = original_range.start.to_display_point(&display_map) + ..original_range.end.to_display_point(&display_map); + let original_buffer_range = original_display_range.start.to_point(&display_map) + ..original_display_range.end.to_point(&display_map); + if movement::is_inside_word(&display_map, position) + || original_display_range.contains(&position) + { + let word_range = movement::surrounding_word(&display_map, position); + if word_range.start < original_display_range.start { + head = word_range.start.to_point(&display_map); + } else { + head = word_range.end.to_point(&display_map); + } + } else { + head = position.to_point(&display_map); + } + + if head <= original_buffer_range.start { + tail = original_buffer_range.end; + } else { + tail = original_buffer_range.start; + } + } + SelectMode::Line(original_range) => { + let original_range = original_range.to_point(&display_map.buffer_snapshot); + + let position = display_map + .clip_point(position, Bias::Left) + .to_point(&display_map); + let line_start = display_map.prev_line_boundary(position).0; + let next_line_start = buffer.clip_point( + display_map.next_line_boundary(position).0 + Point::new(1, 0), + Bias::Left, + ); + + if line_start < original_range.start { + head = line_start + } else { + head = next_line_start + } + + if head <= original_range.start { + tail = original_range.end; + } else { + tail = original_range.start; + } + } + SelectMode::All => { + return; + } + }; + + if head < tail { + pending.start = buffer.anchor_before(head); + pending.end = buffer.anchor_before(tail); + pending.reversed = true; + } else { + pending.start = buffer.anchor_before(tail); + pending.end = buffer.anchor_before(head); + pending.reversed = false; + } + + self.change_selections(None, window, cx, |s| { + s.set_pending(pending, mode); + }); + } else { + log::error!("update_selection dispatched with no pending selection"); + return; + } + + self.apply_scroll_delta(scroll_delta, window, cx); + cx.notify(); + } + + fn end_selection(&mut self, window: &mut Window, cx: &mut Context) { + self.columnar_selection_tail.take(); + if self.selections.pending_anchor().is_some() { + let selections = self.selections.all::(cx); + self.change_selections(None, window, cx, |s| { + s.select(selections); + s.clear_pending(); + }); + } + } + + fn select_columns( + &mut self, + tail: DisplayPoint, + head: DisplayPoint, + goal_column: u32, + display_map: &DisplaySnapshot, + window: &mut Window, + cx: &mut Context, + ) { + let start_row = cmp::min(tail.row(), head.row()); + let end_row = cmp::max(tail.row(), head.row()); + let start_column = cmp::min(tail.column(), goal_column); + let end_column = cmp::max(tail.column(), goal_column); + let reversed = start_column < tail.column(); + + let selection_ranges = (start_row.0..=end_row.0) + .map(DisplayRow) + .filter_map(|row| { + if start_column <= display_map.line_len(row) && !display_map.is_block_line(row) { + let start = display_map + .clip_point(DisplayPoint::new(row, start_column), Bias::Left) + .to_point(display_map); + let end = display_map + .clip_point(DisplayPoint::new(row, end_column), Bias::Right) + .to_point(display_map); + if reversed { + Some(end..start) + } else { + Some(start..end) + } + } else { + None + } + }) + .collect::>(); + + self.change_selections(None, window, cx, |s| { + s.select_ranges(selection_ranges); + }); + cx.notify(); + } + + pub fn has_non_empty_selection(&self, cx: &mut App) -> bool { + self.selections + .all_adjusted(cx) + .iter() + .any(|selection| !selection.is_empty()) + } + + pub fn has_pending_nonempty_selection(&self) -> bool { + let pending_nonempty_selection = match self.selections.pending_anchor() { + Some(Selection { start, end, .. }) => start != end, + None => false, + }; + + pending_nonempty_selection + || (self.columnar_selection_tail.is_some() && self.selections.disjoint.len() > 1) + } + + pub fn has_pending_selection(&self) -> bool { + self.selections.pending_anchor().is_some() || self.columnar_selection_tail.is_some() + } + + pub fn cancel(&mut self, _: &Cancel, window: &mut Window, cx: &mut Context) { + self.selection_mark_mode = false; + + if self.clear_expanded_diff_hunks(cx) { + cx.notify(); + return; + } + if self.dismiss_menus_and_popups(true, window, cx) { + return; + } + + if self.mode.is_full() + && self.change_selections(Some(Autoscroll::fit()), window, cx, |s| s.try_cancel()) + { + return; + } + + cx.propagate(); + } + + pub fn dismiss_menus_and_popups( + &mut self, + is_user_requested: bool, + window: &mut Window, + cx: &mut Context, + ) -> bool { + if self.take_rename(false, window, cx).is_some() { + return true; + } + + if hide_hover(self, cx) { + return true; + } + + if self.hide_signature_help(cx, SignatureHelpHiddenBy::Escape) { + return true; + } + + if self.hide_context_menu(window, cx).is_some() { + return true; + } + + if self.mouse_context_menu.take().is_some() { + return true; + } + + if is_user_requested && self.discard_inline_completion(true, cx) { + return true; + } + + if self.snippet_stack.pop().is_some() { + return true; + } + + if self.mode.is_full() && matches!(self.active_diagnostics, ActiveDiagnostic::Group(_)) { + self.dismiss_diagnostics(cx); + return true; + } + + false + } + + fn linked_editing_ranges_for( + &self, + selection: Range, + cx: &App, + ) -> Option, Vec>>> { + if self.linked_edit_ranges.is_empty() { + return None; + } + let ((base_range, linked_ranges), buffer_snapshot, buffer) = + selection.end.buffer_id.and_then(|end_buffer_id| { + if selection.start.buffer_id != Some(end_buffer_id) { + return None; + } + let buffer = self.buffer.read(cx).buffer(end_buffer_id)?; + let snapshot = buffer.read(cx).snapshot(); + self.linked_edit_ranges + .get(end_buffer_id, selection.start..selection.end, &snapshot) + .map(|ranges| (ranges, snapshot, buffer)) + })?; + use text::ToOffset as TO; + // find offset from the start of current range to current cursor position + let start_byte_offset = TO::to_offset(&base_range.start, &buffer_snapshot); + + let start_offset = TO::to_offset(&selection.start, &buffer_snapshot); + let start_difference = start_offset - start_byte_offset; + let end_offset = TO::to_offset(&selection.end, &buffer_snapshot); + let end_difference = end_offset - start_byte_offset; + // Current range has associated linked ranges. + let mut linked_edits = HashMap::<_, Vec<_>>::default(); + for range in linked_ranges.iter() { + let start_offset = TO::to_offset(&range.start, &buffer_snapshot); + let end_offset = start_offset + end_difference; + let start_offset = start_offset + start_difference; + if start_offset > buffer_snapshot.len() || end_offset > buffer_snapshot.len() { + continue; + } + if self.selections.disjoint_anchor_ranges().any(|s| { + if s.start.buffer_id != selection.start.buffer_id + || s.end.buffer_id != selection.end.buffer_id + { + return false; + } + TO::to_offset(&s.start.text_anchor, &buffer_snapshot) <= end_offset + && TO::to_offset(&s.end.text_anchor, &buffer_snapshot) >= start_offset + }) { + continue; + } + let start = buffer_snapshot.anchor_after(start_offset); + let end = buffer_snapshot.anchor_after(end_offset); + linked_edits + .entry(buffer.clone()) + .or_default() + .push(start..end); + } + Some(linked_edits) + } + + pub fn handle_input(&mut self, text: &str, window: &mut Window, cx: &mut Context) { + let text: Arc = text.into(); + + if self.read_only(cx) { + return; + } + + self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + + let selections = self.selections.all_adjusted(cx); + let mut bracket_inserted = false; + let mut edits = Vec::new(); + let mut linked_edits = HashMap::<_, Vec<_>>::default(); + let mut new_selections = Vec::with_capacity(selections.len()); + let mut new_autoclose_regions = Vec::new(); + let snapshot = self.buffer.read(cx).read(cx); + let mut clear_linked_edit_ranges = false; + + for (selection, autoclose_region) in + self.selections_with_autoclose_regions(selections, &snapshot) + { + if let Some(scope) = snapshot.language_scope_at(selection.head()) { + // Determine if the inserted text matches the opening or closing + // bracket of any of this language's bracket pairs. + let mut bracket_pair = None; + let mut is_bracket_pair_start = false; + let mut is_bracket_pair_end = false; + if !text.is_empty() { + let mut bracket_pair_matching_end = None; + // `text` can be empty when a user is using IME (e.g. Chinese Wubi Simplified) + // and they are removing the character that triggered IME popup. + for (pair, enabled) in scope.brackets() { + if !pair.close && !pair.surround { + continue; + } + + if enabled && pair.start.ends_with(text.as_ref()) { + let prefix_len = pair.start.len() - text.len(); + let preceding_text_matches_prefix = prefix_len == 0 + || (selection.start.column >= (prefix_len as u32) + && snapshot.contains_str_at( + Point::new( + selection.start.row, + selection.start.column - (prefix_len as u32), + ), + &pair.start[..prefix_len], + )); + if preceding_text_matches_prefix { + bracket_pair = Some(pair.clone()); + is_bracket_pair_start = true; + break; + } + } + if pair.end.as_str() == text.as_ref() && bracket_pair_matching_end.is_none() + { + // take first bracket pair matching end, but don't break in case a later bracket + // pair matches start + bracket_pair_matching_end = Some(pair.clone()); + } + } + if bracket_pair.is_none() && bracket_pair_matching_end.is_some() { + bracket_pair = Some(bracket_pair_matching_end.unwrap()); + is_bracket_pair_end = true; + } + } + + if let Some(bracket_pair) = bracket_pair { + let snapshot_settings = snapshot.language_settings_at(selection.start, cx); + let autoclose = self.use_autoclose && snapshot_settings.use_autoclose; + let auto_surround = + self.use_auto_surround && snapshot_settings.use_auto_surround; + if selection.is_empty() { + if is_bracket_pair_start { + // If the inserted text is a suffix of an opening bracket and the + // selection is preceded by the rest of the opening bracket, then + // insert the closing bracket. + let following_text_allows_autoclose = snapshot + .chars_at(selection.start) + .next() + .map_or(true, |c| scope.should_autoclose_before(c)); + + let preceding_text_allows_autoclose = selection.start.column == 0 + || snapshot.reversed_chars_at(selection.start).next().map_or( + true, + |c| { + bracket_pair.start != bracket_pair.end + || !snapshot + .char_classifier_at(selection.start) + .is_word(c) + }, + ); + + let is_closing_quote = if bracket_pair.end == bracket_pair.start + && bracket_pair.start.len() == 1 + { + let target = bracket_pair.start.chars().next().unwrap(); + let current_line_count = snapshot + .reversed_chars_at(selection.start) + .take_while(|&c| c != '\n') + .filter(|&c| c == target) + .count(); + current_line_count % 2 == 1 + } else { + false + }; + + if autoclose + && bracket_pair.close + && following_text_allows_autoclose + && preceding_text_allows_autoclose + && !is_closing_quote + { + let anchor = snapshot.anchor_before(selection.end); + new_selections.push((selection.map(|_| anchor), text.len())); + new_autoclose_regions.push(( + anchor, + text.len(), + selection.id, + bracket_pair.clone(), + )); + edits.push(( + selection.range(), + format!("{}{}", text, bracket_pair.end).into(), + )); + bracket_inserted = true; + continue; + } + } + + if let Some(region) = autoclose_region { + // If the selection is followed by an auto-inserted closing bracket, + // then don't insert that closing bracket again; just move the selection + // past the closing bracket. + let should_skip = selection.end == region.range.end.to_point(&snapshot) + && text.as_ref() == region.pair.end.as_str(); + if should_skip { + let anchor = snapshot.anchor_after(selection.end); + new_selections + .push((selection.map(|_| anchor), region.pair.end.len())); + continue; + } + } + + let always_treat_brackets_as_autoclosed = snapshot + .language_settings_at(selection.start, cx) + .always_treat_brackets_as_autoclosed; + if always_treat_brackets_as_autoclosed + && is_bracket_pair_end + && snapshot.contains_str_at(selection.end, text.as_ref()) + { + // Otherwise, when `always_treat_brackets_as_autoclosed` is set to `true + // and the inserted text is a closing bracket and the selection is followed + // by the closing bracket then move the selection past the closing bracket. + let anchor = snapshot.anchor_after(selection.end); + new_selections.push((selection.map(|_| anchor), text.len())); + continue; + } + } + // If an opening bracket is 1 character long and is typed while + // text is selected, then surround that text with the bracket pair. + else if auto_surround + && bracket_pair.surround + && is_bracket_pair_start + && bracket_pair.start.chars().count() == 1 + { + edits.push((selection.start..selection.start, text.clone())); + edits.push(( + selection.end..selection.end, + bracket_pair.end.as_str().into(), + )); + bracket_inserted = true; + new_selections.push(( + Selection { + id: selection.id, + start: snapshot.anchor_after(selection.start), + end: snapshot.anchor_before(selection.end), + reversed: selection.reversed, + goal: selection.goal, + }, + 0, + )); + continue; + } + } + } + + if self.auto_replace_emoji_shortcode + && selection.is_empty() + && text.as_ref().ends_with(':') + { + if let Some(possible_emoji_short_code) = + Self::find_possible_emoji_shortcode_at_position(&snapshot, selection.start) + { + if !possible_emoji_short_code.is_empty() { + if let Some(emoji) = emojis::get_by_shortcode(&possible_emoji_short_code) { + let emoji_shortcode_start = Point::new( + selection.start.row, + selection.start.column - possible_emoji_short_code.len() as u32 - 1, + ); + + // Remove shortcode from buffer + edits.push(( + emoji_shortcode_start..selection.start, + "".to_string().into(), + )); + new_selections.push(( + Selection { + id: selection.id, + start: snapshot.anchor_after(emoji_shortcode_start), + end: snapshot.anchor_before(selection.start), + reversed: selection.reversed, + goal: selection.goal, + }, + 0, + )); + + // Insert emoji + let selection_start_anchor = snapshot.anchor_after(selection.start); + new_selections.push((selection.map(|_| selection_start_anchor), 0)); + edits.push((selection.start..selection.end, emoji.to_string().into())); + + continue; + } + } + } + } + + // If not handling any auto-close operation, then just replace the selected + // text with the given input and move the selection to the end of the + // newly inserted text. + let anchor = snapshot.anchor_after(selection.end); + if !self.linked_edit_ranges.is_empty() { + let start_anchor = snapshot.anchor_before(selection.start); + + let is_word_char = text.chars().next().map_or(true, |char| { + let classifier = snapshot.char_classifier_at(start_anchor.to_offset(&snapshot)); + classifier.is_word(char) + }); + + if is_word_char { + if let Some(ranges) = self + .linked_editing_ranges_for(start_anchor.text_anchor..anchor.text_anchor, cx) + { + for (buffer, edits) in ranges { + linked_edits + .entry(buffer.clone()) + .or_default() + .extend(edits.into_iter().map(|range| (range, text.clone()))); + } + } + } else { + clear_linked_edit_ranges = true; + } + } + + new_selections.push((selection.map(|_| anchor), 0)); + edits.push((selection.start..selection.end, text.clone())); + } + + drop(snapshot); + + self.transact(window, cx, |this, window, cx| { + if clear_linked_edit_ranges { + this.linked_edit_ranges.clear(); + } + let initial_buffer_versions = + jsx_tag_auto_close::construct_initial_buffer_versions_map(this, &edits, cx); + + this.buffer.update(cx, |buffer, cx| { + buffer.edit(edits, this.autoindent_mode.clone(), cx); + }); + for (buffer, edits) in linked_edits { + buffer.update(cx, |buffer, cx| { + let snapshot = buffer.snapshot(); + let edits = edits + .into_iter() + .map(|(range, text)| { + use text::ToPoint as TP; + let end_point = TP::to_point(&range.end, &snapshot); + let start_point = TP::to_point(&range.start, &snapshot); + (start_point..end_point, text) + }) + .sorted_by_key(|(range, _)| range.start); + buffer.edit(edits, None, cx); + }) + } + let new_anchor_selections = new_selections.iter().map(|e| &e.0); + let new_selection_deltas = new_selections.iter().map(|e| e.1); + let map = this.display_map.update(cx, |map, cx| map.snapshot(cx)); + let new_selections = resolve_selections::(new_anchor_selections, &map) + .zip(new_selection_deltas) + .map(|(selection, delta)| Selection { + id: selection.id, + start: selection.start + delta, + end: selection.end + delta, + reversed: selection.reversed, + goal: SelectionGoal::None, + }) + .collect::>(); + + let mut i = 0; + for (position, delta, selection_id, pair) in new_autoclose_regions { + let position = position.to_offset(&map.buffer_snapshot) + delta; + let start = map.buffer_snapshot.anchor_before(position); + let end = map.buffer_snapshot.anchor_after(position); + while let Some(existing_state) = this.autoclose_regions.get(i) { + match existing_state.range.start.cmp(&start, &map.buffer_snapshot) { + Ordering::Less => i += 1, + Ordering::Greater => break, + Ordering::Equal => { + match end.cmp(&existing_state.range.end, &map.buffer_snapshot) { + Ordering::Less => i += 1, + Ordering::Equal => break, + Ordering::Greater => break, + } + } + } + } + this.autoclose_regions.insert( + i, + AutocloseRegion { + selection_id, + range: start..end, + pair, + }, + ); + } + + let had_active_inline_completion = this.has_active_inline_completion(); + this.change_selections_inner(Some(Autoscroll::fit()), false, window, cx, |s| { + s.select(new_selections) + }); + + if !bracket_inserted { + if let Some(on_type_format_task) = + this.trigger_on_type_formatting(text.to_string(), window, cx) + { + on_type_format_task.detach_and_log_err(cx); + } + } + + let editor_settings = EditorSettings::get_global(cx); + if bracket_inserted + && (editor_settings.auto_signature_help + || editor_settings.show_signature_help_after_edits) + { + this.show_signature_help(&ShowSignatureHelp, window, cx); + } + + let trigger_in_words = + this.show_edit_predictions_in_menu() || !had_active_inline_completion; + if this.hard_wrap.is_some() { + let latest: Range = this.selections.newest(cx).range(); + if latest.is_empty() + && this + .buffer() + .read(cx) + .snapshot(cx) + .line_len(MultiBufferRow(latest.start.row)) + == latest.start.column + { + this.rewrap_impl( + RewrapOptions { + override_language_settings: true, + preserve_existing_whitespace: true, + }, + cx, + ) + } + } + this.trigger_completion_on_input(&text, trigger_in_words, window, cx); + linked_editing_ranges::refresh_linked_ranges(this, window, cx); + this.refresh_inline_completion(true, false, window, cx); + jsx_tag_auto_close::handle_from(this, initial_buffer_versions, window, cx); + }); + } + + fn find_possible_emoji_shortcode_at_position( + snapshot: &MultiBufferSnapshot, + position: Point, + ) -> Option { + let mut chars = Vec::new(); + let mut found_colon = false; + for char in snapshot.reversed_chars_at(position).take(100) { + // Found a possible emoji shortcode in the middle of the buffer + if found_colon { + if char.is_whitespace() { + chars.reverse(); + return Some(chars.iter().collect()); + } + // If the previous character is not a whitespace, we are in the middle of a word + // and we only want to complete the shortcode if the word is made up of other emojis + let mut containing_word = String::new(); + for ch in snapshot + .reversed_chars_at(position) + .skip(chars.len() + 1) + .take(100) + { + if ch.is_whitespace() { + break; + } + containing_word.push(ch); + } + let containing_word = containing_word.chars().rev().collect::(); + if util::word_consists_of_emojis(containing_word.as_str()) { + chars.reverse(); + return Some(chars.iter().collect()); + } + } + + if char.is_whitespace() || !char.is_ascii() { + return None; + } + if char == ':' { + found_colon = true; + } else { + chars.push(char); + } + } + // Found a possible emoji shortcode at the beginning of the buffer + chars.reverse(); + Some(chars.iter().collect()) + } + + pub fn newline(&mut self, _: &Newline, window: &mut Window, cx: &mut Context) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + self.transact(window, cx, |this, window, cx| { + let (edits, selection_fixup_info): (Vec<_>, Vec<_>) = { + let selections = this.selections.all::(cx); + let multi_buffer = this.buffer.read(cx); + let buffer = multi_buffer.snapshot(cx); + selections + .iter() + .map(|selection| { + let start_point = selection.start.to_point(&buffer); + let mut indent = + buffer.indent_size_for_line(MultiBufferRow(start_point.row)); + indent.len = cmp::min(indent.len, start_point.column); + let start = selection.start; + let end = selection.end; + let selection_is_empty = start == end; + let language_scope = buffer.language_scope_at(start); + let (comment_delimiter, insert_extra_newline) = if let Some(language) = + &language_scope + { + let insert_extra_newline = + insert_extra_newline_brackets(&buffer, start..end, language) + || insert_extra_newline_tree_sitter(&buffer, start..end); + + // Comment extension on newline is allowed only for cursor selections + let comment_delimiter = maybe!({ + if !selection_is_empty { + return None; + } + + if !multi_buffer.language_settings(cx).extend_comment_on_newline { + return None; + } + + let delimiters = language.line_comment_prefixes(); + let max_len_of_delimiter = + delimiters.iter().map(|delimiter| delimiter.len()).max()?; + let (snapshot, range) = + buffer.buffer_line_for_row(MultiBufferRow(start_point.row))?; + + let mut index_of_first_non_whitespace = 0; + let comment_candidate = snapshot + .chars_for_range(range) + .skip_while(|c| { + let should_skip = c.is_whitespace(); + if should_skip { + index_of_first_non_whitespace += 1; + } + should_skip + }) + .take(max_len_of_delimiter) + .collect::(); + let comment_prefix = delimiters.iter().find(|comment_prefix| { + comment_candidate.starts_with(comment_prefix.as_ref()) + })?; + let cursor_is_placed_after_comment_marker = + index_of_first_non_whitespace + comment_prefix.len() + <= start_point.column as usize; + if cursor_is_placed_after_comment_marker { + Some(comment_prefix.clone()) + } else { + None + } + }); + (comment_delimiter, insert_extra_newline) + } else { + (None, false) + }; + + let capacity_for_delimiter = comment_delimiter + .as_deref() + .map(str::len) + .unwrap_or_default(); + let mut new_text = + String::with_capacity(1 + capacity_for_delimiter + indent.len as usize); + new_text.push('\n'); + new_text.extend(indent.chars()); + if let Some(delimiter) = &comment_delimiter { + new_text.push_str(delimiter); + } + if insert_extra_newline { + new_text = new_text.repeat(2); + } + + let anchor = buffer.anchor_after(end); + let new_selection = selection.map(|_| anchor); + ( + (start..end, new_text), + (insert_extra_newline, new_selection), + ) + }) + .unzip() + }; + + this.edit_with_autoindent(edits, cx); + let buffer = this.buffer.read(cx).snapshot(cx); + let new_selections = selection_fixup_info + .into_iter() + .map(|(extra_newline_inserted, new_selection)| { + let mut cursor = new_selection.end.to_point(&buffer); + if extra_newline_inserted { + cursor.row -= 1; + cursor.column = buffer.line_len(MultiBufferRow(cursor.row)); + } + new_selection.map(|_| cursor) + }) + .collect(); + + this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.select(new_selections) + }); + this.refresh_inline_completion(true, false, window, cx); + }); + } + + pub fn newline_above(&mut self, _: &NewlineAbove, window: &mut Window, cx: &mut Context) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + + let buffer = self.buffer.read(cx); + let snapshot = buffer.snapshot(cx); + + let mut edits = Vec::new(); + let mut rows = Vec::new(); + + for (rows_inserted, selection) in self.selections.all_adjusted(cx).into_iter().enumerate() { + let cursor = selection.head(); + let row = cursor.row; + + let start_of_line = snapshot.clip_point(Point::new(row, 0), Bias::Left); + + let newline = "\n".to_string(); + edits.push((start_of_line..start_of_line, newline)); + + rows.push(row + rows_inserted as u32); + } + + self.transact(window, cx, |editor, window, cx| { + editor.edit(edits, cx); + + editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + let mut index = 0; + s.move_cursors_with(|map, _, _| { + let row = rows[index]; + index += 1; + + let point = Point::new(row, 0); + let boundary = map.next_line_boundary(point).1; + let clipped = map.clip_point(boundary, Bias::Left); + + (clipped, SelectionGoal::None) + }); + }); + + let mut indent_edits = Vec::new(); + let multibuffer_snapshot = editor.buffer.read(cx).snapshot(cx); + for row in rows { + let indents = multibuffer_snapshot.suggested_indents(row..row + 1, cx); + for (row, indent) in indents { + if indent.len == 0 { + continue; + } + + let text = match indent.kind { + IndentKind::Space => " ".repeat(indent.len as usize), + IndentKind::Tab => "\t".repeat(indent.len as usize), + }; + let point = Point::new(row.0, 0); + indent_edits.push((point..point, text)); + } + } + editor.edit(indent_edits, cx); + }); + } + + pub fn newline_below(&mut self, _: &NewlineBelow, window: &mut Window, cx: &mut Context) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + + let buffer = self.buffer.read(cx); + let snapshot = buffer.snapshot(cx); + + let mut edits = Vec::new(); + let mut rows = Vec::new(); + let mut rows_inserted = 0; + + for selection in self.selections.all_adjusted(cx) { + let cursor = selection.head(); + let row = cursor.row; + + let point = Point::new(row + 1, 0); + let start_of_line = snapshot.clip_point(point, Bias::Left); + + let newline = "\n".to_string(); + edits.push((start_of_line..start_of_line, newline)); + + rows_inserted += 1; + rows.push(row + rows_inserted); + } + + self.transact(window, cx, |editor, window, cx| { + editor.edit(edits, cx); + + editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + let mut index = 0; + s.move_cursors_with(|map, _, _| { + let row = rows[index]; + index += 1; + + let point = Point::new(row, 0); + let boundary = map.next_line_boundary(point).1; + let clipped = map.clip_point(boundary, Bias::Left); + + (clipped, SelectionGoal::None) + }); + }); + + let mut indent_edits = Vec::new(); + let multibuffer_snapshot = editor.buffer.read(cx).snapshot(cx); + for row in rows { + let indents = multibuffer_snapshot.suggested_indents(row..row + 1, cx); + for (row, indent) in indents { + if indent.len == 0 { + continue; + } + + let text = match indent.kind { + IndentKind::Space => " ".repeat(indent.len as usize), + IndentKind::Tab => "\t".repeat(indent.len as usize), + }; + let point = Point::new(row.0, 0); + indent_edits.push((point..point, text)); + } + } + editor.edit(indent_edits, cx); + }); + } + + pub fn insert(&mut self, text: &str, window: &mut Window, cx: &mut Context) { + let autoindent = text.is_empty().not().then(|| AutoindentMode::Block { + original_indent_columns: Vec::new(), + }); + self.insert_with_autoindent_mode(text, autoindent, window, cx); + } + + fn insert_with_autoindent_mode( + &mut self, + text: &str, + autoindent_mode: Option, + window: &mut Window, + cx: &mut Context, + ) { + if self.read_only(cx) { + return; + } + + let text: Arc = text.into(); + self.transact(window, cx, |this, window, cx| { + let old_selections = this.selections.all_adjusted(cx); + let selection_anchors = this.buffer.update(cx, |buffer, cx| { + let anchors = { + let snapshot = buffer.read(cx); + old_selections + .iter() + .map(|s| { + let anchor = snapshot.anchor_after(s.head()); + s.map(|_| anchor) + }) + .collect::>() + }; + buffer.edit( + old_selections + .iter() + .map(|s| (s.start..s.end, text.clone())), + autoindent_mode, + cx, + ); + anchors + }); + + this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.select_anchors(selection_anchors); + }); + + cx.notify(); + }); + } + + fn trigger_completion_on_input( + &mut self, + text: &str, + trigger_in_words: bool, + window: &mut Window, + cx: &mut Context, + ) { + let ignore_completion_provider = self + .context_menu + .borrow() + .as_ref() + .map(|menu| match menu { + CodeContextMenu::Completions(completions_menu) => { + completions_menu.ignore_completion_provider + } + CodeContextMenu::CodeActions(_) => false, + }) + .unwrap_or(false); + + if ignore_completion_provider { + self.show_word_completions(&ShowWordCompletions, window, cx); + } else if self.is_completion_trigger(text, trigger_in_words, cx) { + self.show_completions( + &ShowCompletions { + trigger: Some(text.to_owned()).filter(|x| !x.is_empty()), + }, + window, + cx, + ); + } else { + self.hide_context_menu(window, cx); + } + } + + fn is_completion_trigger( + &self, + text: &str, + trigger_in_words: bool, + cx: &mut Context, + ) -> bool { + let position = self.selections.newest_anchor().head(); + let multibuffer = self.buffer.read(cx); + let Some(buffer) = position + .buffer_id + .and_then(|buffer_id| multibuffer.buffer(buffer_id).clone()) + else { + return false; + }; + + if let Some(completion_provider) = &self.completion_provider { + completion_provider.is_completion_trigger( + &buffer, + position.text_anchor, + text, + trigger_in_words, + cx, + ) + } else { + false + } + } + + /// If any empty selections is touching the start of its innermost containing autoclose + /// region, expand it to select the brackets. + fn select_autoclose_pair(&mut self, window: &mut Window, cx: &mut Context) { + let selections = self.selections.all::(cx); + let buffer = self.buffer.read(cx).read(cx); + let new_selections = self + .selections_with_autoclose_regions(selections, &buffer) + .map(|(mut selection, region)| { + if !selection.is_empty() { + return selection; + } + + if let Some(region) = region { + let mut range = region.range.to_offset(&buffer); + if selection.start == range.start && range.start >= region.pair.start.len() { + range.start -= region.pair.start.len(); + if buffer.contains_str_at(range.start, ®ion.pair.start) + && buffer.contains_str_at(range.end, ®ion.pair.end) + { + range.end += region.pair.end.len(); + selection.start = range.start; + selection.end = range.end; + + return selection; + } + } + } + + let always_treat_brackets_as_autoclosed = buffer + .language_settings_at(selection.start, cx) + .always_treat_brackets_as_autoclosed; + + if !always_treat_brackets_as_autoclosed { + return selection; + } + + if let Some(scope) = buffer.language_scope_at(selection.start) { + for (pair, enabled) in scope.brackets() { + if !enabled || !pair.close { + continue; + } + + if buffer.contains_str_at(selection.start, &pair.end) { + let pair_start_len = pair.start.len(); + if buffer.contains_str_at( + selection.start.saturating_sub(pair_start_len), + &pair.start, + ) { + selection.start -= pair_start_len; + selection.end += pair.end.len(); + + return selection; + } + } + } + } + + selection + }) + .collect(); + + drop(buffer); + self.change_selections(None, window, cx, |selections| { + selections.select(new_selections) + }); + } + + /// Iterate the given selections, and for each one, find the smallest surrounding + /// autoclose region. This uses the ordering of the selections and the autoclose + /// regions to avoid repeated comparisons. + fn selections_with_autoclose_regions<'a, D: ToOffset + Clone>( + &'a self, + selections: impl IntoIterator>, + buffer: &'a MultiBufferSnapshot, + ) -> impl Iterator, Option<&'a AutocloseRegion>)> { + let mut i = 0; + let mut regions = self.autoclose_regions.as_slice(); + selections.into_iter().map(move |selection| { + let range = selection.start.to_offset(buffer)..selection.end.to_offset(buffer); + + let mut enclosing = None; + while let Some(pair_state) = regions.get(i) { + if pair_state.range.end.to_offset(buffer) < range.start { + regions = ®ions[i + 1..]; + i = 0; + } else if pair_state.range.start.to_offset(buffer) > range.end { + break; + } else { + if pair_state.selection_id == selection.id { + enclosing = Some(pair_state); + } + i += 1; + } + } + + (selection, enclosing) + }) + } + + /// Remove any autoclose regions that no longer contain their selection. + fn invalidate_autoclose_regions( + &mut self, + mut selections: &[Selection], + buffer: &MultiBufferSnapshot, + ) { + self.autoclose_regions.retain(|state| { + let mut i = 0; + while let Some(selection) = selections.get(i) { + if selection.end.cmp(&state.range.start, buffer).is_lt() { + selections = &selections[1..]; + continue; + } + if selection.start.cmp(&state.range.end, buffer).is_gt() { + break; + } + if selection.id == state.selection_id { + return true; + } else { + i += 1; + } + } + false + }); + } + + fn completion_query(buffer: &MultiBufferSnapshot, position: impl ToOffset) -> Option { + let offset = position.to_offset(buffer); + let (word_range, kind) = buffer.surrounding_word(offset, true); + if offset > word_range.start && kind == Some(CharKind::Word) { + Some( + buffer + .text_for_range(word_range.start..offset) + .collect::(), + ) + } else { + None + } + } + + pub fn toggle_inline_values( + &mut self, + _: &ToggleInlineValues, + _: &mut Window, + cx: &mut Context, + ) { + self.inline_value_cache.enabled = !self.inline_value_cache.enabled; + + self.refresh_inline_values(cx); + } + + pub fn toggle_inlay_hints( + &mut self, + _: &ToggleInlayHints, + _: &mut Window, + cx: &mut Context, + ) { + self.refresh_inlay_hints( + InlayHintRefreshReason::Toggle(!self.inlay_hints_enabled()), + cx, + ); + } + + pub fn inlay_hints_enabled(&self) -> bool { + self.inlay_hint_cache.enabled + } + + pub fn inline_values_enabled(&self) -> bool { + self.inline_value_cache.enabled + } + + fn refresh_inlay_hints(&mut self, reason: InlayHintRefreshReason, cx: &mut Context) { + if self.semantics_provider.is_none() || !self.mode.is_full() { + return; + } + + let reason_description = reason.description(); + let ignore_debounce = matches!( + reason, + InlayHintRefreshReason::SettingsChange(_) + | InlayHintRefreshReason::Toggle(_) + | InlayHintRefreshReason::ExcerptsRemoved(_) + | InlayHintRefreshReason::ModifiersChanged(_) + ); + let (invalidate_cache, required_languages) = match reason { + InlayHintRefreshReason::ModifiersChanged(enabled) => { + match self.inlay_hint_cache.modifiers_override(enabled) { + Some(enabled) => { + if enabled { + (InvalidationStrategy::RefreshRequested, None) + } else { + self.splice_inlays( + &self + .visible_inlay_hints(cx) + .iter() + .map(|inlay| inlay.id) + .collect::>(), + Vec::new(), + cx, + ); + return; + } + } + None => return, + } + } + InlayHintRefreshReason::Toggle(enabled) => { + if self.inlay_hint_cache.toggle(enabled) { + if enabled { + (InvalidationStrategy::RefreshRequested, None) + } else { + self.splice_inlays( + &self + .visible_inlay_hints(cx) + .iter() + .map(|inlay| inlay.id) + .collect::>(), + Vec::new(), + cx, + ); + return; + } + } else { + return; + } + } + InlayHintRefreshReason::SettingsChange(new_settings) => { + match self.inlay_hint_cache.update_settings( + &self.buffer, + new_settings, + self.visible_inlay_hints(cx), + cx, + ) { + ControlFlow::Break(Some(InlaySplice { + to_remove, + to_insert, + })) => { + self.splice_inlays(&to_remove, to_insert, cx); + return; + } + ControlFlow::Break(None) => return, + ControlFlow::Continue(()) => (InvalidationStrategy::RefreshRequested, None), + } + } + InlayHintRefreshReason::ExcerptsRemoved(excerpts_removed) => { + if let Some(InlaySplice { + to_remove, + to_insert, + }) = self.inlay_hint_cache.remove_excerpts(&excerpts_removed) + { + self.splice_inlays(&to_remove, to_insert, cx); + } + self.display_map.update(cx, |display_map, _| { + display_map.remove_inlays_for_excerpts(&excerpts_removed) + }); + return; + } + InlayHintRefreshReason::NewLinesShown => (InvalidationStrategy::None, None), + InlayHintRefreshReason::BufferEdited(buffer_languages) => { + (InvalidationStrategy::BufferEdited, Some(buffer_languages)) + } + InlayHintRefreshReason::RefreshRequested => { + (InvalidationStrategy::RefreshRequested, None) + } + }; + + if let Some(InlaySplice { + to_remove, + to_insert, + }) = self.inlay_hint_cache.spawn_hint_refresh( + reason_description, + self.excerpts_for_inlay_hints_query(required_languages.as_ref(), cx), + invalidate_cache, + ignore_debounce, + cx, + ) { + self.splice_inlays(&to_remove, to_insert, cx); + } + } + + fn visible_inlay_hints(&self, cx: &Context) -> Vec { + self.display_map + .read(cx) + .current_inlays() + .filter(move |inlay| matches!(inlay.id, InlayId::Hint(_))) + .cloned() + .collect() + } + + pub fn excerpts_for_inlay_hints_query( + &self, + restrict_to_languages: Option<&HashSet>>, + cx: &mut Context, + ) -> HashMap, clock::Global, Range)> { + let Some(project) = self.project.as_ref() else { + return HashMap::default(); + }; + let project = project.read(cx); + let multi_buffer = self.buffer().read(cx); + let multi_buffer_snapshot = multi_buffer.snapshot(cx); + let multi_buffer_visible_start = self + .scroll_manager + .anchor() + .anchor + .to_point(&multi_buffer_snapshot); + let multi_buffer_visible_end = multi_buffer_snapshot.clip_point( + multi_buffer_visible_start + + Point::new(self.visible_line_count().unwrap_or(0.).ceil() as u32, 0), + Bias::Left, + ); + let multi_buffer_visible_range = multi_buffer_visible_start..multi_buffer_visible_end; + multi_buffer_snapshot + .range_to_buffer_ranges(multi_buffer_visible_range) + .into_iter() + .filter(|(_, excerpt_visible_range, _)| !excerpt_visible_range.is_empty()) + .filter_map(|(buffer, excerpt_visible_range, excerpt_id)| { + let buffer_file = project::File::from_dyn(buffer.file())?; + let buffer_worktree = project.worktree_for_id(buffer_file.worktree_id(cx), cx)?; + let worktree_entry = buffer_worktree + .read(cx) + .entry_for_id(buffer_file.project_entry_id(cx)?)?; + if worktree_entry.is_ignored { + return None; + } + + let language = buffer.language()?; + if let Some(restrict_to_languages) = restrict_to_languages { + if !restrict_to_languages.contains(language) { + return None; + } + } + Some(( + excerpt_id, + ( + multi_buffer.buffer(buffer.remote_id()).unwrap(), + buffer.version().clone(), + excerpt_visible_range, + ), + )) + }) + .collect() + } + + pub fn text_layout_details(&self, window: &mut Window) -> TextLayoutDetails { + TextLayoutDetails { + text_system: window.text_system().clone(), + editor_style: self.style.clone().unwrap(), + rem_size: window.rem_size(), + scroll_anchor: self.scroll_manager.anchor(), + visible_rows: self.visible_line_count(), + vertical_scroll_margin: self.scroll_manager.vertical_scroll_margin, + } + } + + pub fn splice_inlays( + &self, + to_remove: &[InlayId], + to_insert: Vec, + cx: &mut Context, + ) { + self.display_map.update(cx, |display_map, cx| { + display_map.splice_inlays(to_remove, to_insert, cx) + }); + cx.notify(); + } + + fn trigger_on_type_formatting( + &self, + input: String, + window: &mut Window, + cx: &mut Context, + ) -> Option>> { + if input.len() != 1 { + return None; + } + + let project = self.project.as_ref()?; + let position = self.selections.newest_anchor().head(); + let (buffer, buffer_position) = self + .buffer + .read(cx) + .text_anchor_for_position(position, cx)?; + + let settings = language_settings::language_settings( + buffer + .read(cx) + .language_at(buffer_position) + .map(|l| l.name()), + buffer.read(cx).file(), + cx, + ); + if !settings.use_on_type_format { + return None; + } + + // OnTypeFormatting returns a list of edits, no need to pass them between Zed instances, + // hence we do LSP request & edit on host side only — add formats to host's history. + let push_to_lsp_host_history = true; + // If this is not the host, append its history with new edits. + let push_to_client_history = project.read(cx).is_via_collab(); + + let on_type_formatting = project.update(cx, |project, cx| { + project.on_type_format( + buffer.clone(), + buffer_position, + input, + push_to_lsp_host_history, + cx, + ) + }); + Some(cx.spawn_in(window, async move |editor, cx| { + if let Some(transaction) = on_type_formatting.await? { + if push_to_client_history { + buffer + .update(cx, |buffer, _| { + buffer.push_transaction(transaction, Instant::now()); + buffer.finalize_last_transaction(); + }) + .ok(); + } + editor.update(cx, |editor, cx| { + editor.refresh_document_highlights(cx); + })?; + } + Ok(()) + })) + } + + pub fn show_word_completions( + &mut self, + _: &ShowWordCompletions, + window: &mut Window, + cx: &mut Context, + ) { + self.open_completions_menu(true, None, window, cx); + } + + pub fn show_completions( + &mut self, + options: &ShowCompletions, + window: &mut Window, + cx: &mut Context, + ) { + self.open_completions_menu(false, options.trigger.as_deref(), window, cx); + } + + fn open_completions_menu( + &mut self, + ignore_completion_provider: bool, + trigger: Option<&str>, + window: &mut Window, + cx: &mut Context, + ) { + if self.pending_rename.is_some() { + return; + } + if !self.snippet_stack.is_empty() && self.context_menu.borrow().as_ref().is_some() { + return; + } + + let position = self.selections.newest_anchor().head(); + if position.diff_base_anchor.is_some() { + return; + } + let (buffer, buffer_position) = + if let Some(output) = self.buffer.read(cx).text_anchor_for_position(position, cx) { + output + } else { + return; + }; + let buffer_snapshot = buffer.read(cx).snapshot(); + let show_completion_documentation = buffer_snapshot + .settings_at(buffer_position, cx) + .show_completion_documentation; + + let query = Self::completion_query(&self.buffer.read(cx).read(cx), position); + + let trigger_kind = match trigger { + Some(trigger) if buffer.read(cx).completion_triggers().contains(trigger) => { + CompletionTriggerKind::TRIGGER_CHARACTER + } + _ => CompletionTriggerKind::INVOKED, + }; + let completion_context = CompletionContext { + trigger_character: trigger.and_then(|trigger| { + if trigger_kind == CompletionTriggerKind::TRIGGER_CHARACTER { + Some(String::from(trigger)) + } else { + None + } + }), + trigger_kind, + }; + + let (old_range, word_kind) = buffer_snapshot.surrounding_word(buffer_position); + let (old_range, word_to_exclude) = if word_kind == Some(CharKind::Word) { + let word_to_exclude = buffer_snapshot + .text_for_range(old_range.clone()) + .collect::(); + ( + buffer_snapshot.anchor_before(old_range.start) + ..buffer_snapshot.anchor_after(old_range.end), + Some(word_to_exclude), + ) + } else { + (buffer_position..buffer_position, None) + }; + + let completion_settings = language_settings( + buffer_snapshot + .language_at(buffer_position) + .map(|language| language.name()), + buffer_snapshot.file(), + cx, + ) + .completions; + + // The document can be large, so stay in reasonable bounds when searching for words, + // otherwise completion pop-up might be slow to appear. + const WORD_LOOKUP_ROWS: u32 = 5_000; + let buffer_row = text::ToPoint::to_point(&buffer_position, &buffer_snapshot).row; + let min_word_search = buffer_snapshot.clip_point( + Point::new(buffer_row.saturating_sub(WORD_LOOKUP_ROWS), 0), + Bias::Left, + ); + let max_word_search = buffer_snapshot.clip_point( + Point::new(buffer_row + WORD_LOOKUP_ROWS, 0).min(buffer_snapshot.max_point()), + Bias::Right, + ); + let word_search_range = buffer_snapshot.point_to_offset(min_word_search) + ..buffer_snapshot.point_to_offset(max_word_search); + + let provider = self + .completion_provider + .as_ref() + .filter(|_| !ignore_completion_provider); + let skip_digits = query + .as_ref() + .map_or(true, |query| !query.chars().any(|c| c.is_digit(10))); + + let (mut words, provided_completions) = match provider { + Some(provider) => { + let completions = provider.completions( + position.excerpt_id, + &buffer, + buffer_position, + completion_context, + window, + cx, + ); + + let words = match completion_settings.words { + WordsCompletionMode::Disabled => Task::ready(BTreeMap::default()), + WordsCompletionMode::Enabled | WordsCompletionMode::Fallback => cx + .background_spawn(async move { + buffer_snapshot.words_in_range(WordsQuery { + fuzzy_contents: None, + range: word_search_range, + skip_digits, + }) + }), + }; + + (words, completions) + } + None => ( + cx.background_spawn(async move { + buffer_snapshot.words_in_range(WordsQuery { + fuzzy_contents: None, + range: word_search_range, + skip_digits, + }) + }), + Task::ready(Ok(None)), + ), + }; + + let sort_completions = provider + .as_ref() + .map_or(false, |provider| provider.sort_completions()); + + let filter_completions = provider + .as_ref() + .map_or(true, |provider| provider.filter_completions()); + + let snippet_sort_order = EditorSettings::get_global(cx).snippet_sort_order; + + let id = post_inc(&mut self.next_completion_id); + let task = cx.spawn_in(window, async move |editor, cx| { + async move { + editor.update(cx, |this, _| { + this.completion_tasks.retain(|(task_id, _)| *task_id >= id); + })?; + + let mut completions = Vec::new(); + if let Some(provided_completions) = provided_completions.await.log_err().flatten() { + completions.extend(provided_completions); + if completion_settings.words == WordsCompletionMode::Fallback { + words = Task::ready(BTreeMap::default()); + } + } + + let mut words = words.await; + if let Some(word_to_exclude) = &word_to_exclude { + words.remove(word_to_exclude); + } + for lsp_completion in &completions { + words.remove(&lsp_completion.new_text); + } + completions.extend(words.into_iter().map(|(word, word_range)| Completion { + replace_range: old_range.clone(), + new_text: word.clone(), + label: CodeLabel::plain(word, None), + icon_path: None, + documentation: None, + source: CompletionSource::BufferWord { + word_range, + resolved: false, + }, + insert_text_mode: Some(InsertTextMode::AS_IS), + confirm: None, + })); + + let menu = if completions.is_empty() { + None + } else { + let mut menu = CompletionsMenu::new( + id, + sort_completions, + show_completion_documentation, + ignore_completion_provider, + position, + buffer.clone(), + completions.into(), + snippet_sort_order, + ); + + menu.filter( + if filter_completions { + query.as_deref() + } else { + None + }, + cx.background_executor().clone(), + ) + .await; + + menu.visible().then_some(menu) + }; + + editor.update_in(cx, |editor, window, cx| { + match editor.context_menu.borrow().as_ref() { + None => {} + Some(CodeContextMenu::Completions(prev_menu)) => { + if prev_menu.id > id { + return; + } + } + _ => return, + } + + if editor.focus_handle.is_focused(window) && menu.is_some() { + let mut menu = menu.unwrap(); + menu.resolve_visible_completions(editor.completion_provider.as_deref(), cx); + + *editor.context_menu.borrow_mut() = + Some(CodeContextMenu::Completions(menu)); + + if editor.show_edit_predictions_in_menu() { + editor.update_visible_inline_completion(window, cx); + } else { + editor.discard_inline_completion(false, cx); + } + + cx.notify(); + } else if editor.completion_tasks.len() <= 1 { + // If there are no more completion tasks and the last menu was + // empty, we should hide it. + let was_hidden = editor.hide_context_menu(window, cx).is_none(); + // If it was already hidden and we don't show inline + // completions in the menu, we should also show the + // inline-completion when available. + if was_hidden && editor.show_edit_predictions_in_menu() { + editor.update_visible_inline_completion(window, cx); + } + } + })?; + + anyhow::Ok(()) + } + .log_err() + .await + }); + + self.completion_tasks.push((id, task)); + } + + #[cfg(feature = "test-support")] + pub fn current_completions(&self) -> Option> { + let menu = self.context_menu.borrow(); + if let CodeContextMenu::Completions(menu) = menu.as_ref()? { + let completions = menu.completions.borrow(); + Some(completions.to_vec()) + } else { + None + } + } + + pub fn confirm_completion( + &mut self, + action: &ConfirmCompletion, + window: &mut Window, + cx: &mut Context, + ) -> Option>> { + self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + self.do_completion(action.item_ix, CompletionIntent::Complete, window, cx) + } + + pub fn confirm_completion_insert( + &mut self, + _: &ConfirmCompletionInsert, + window: &mut Window, + cx: &mut Context, + ) -> Option>> { + self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + self.do_completion(None, CompletionIntent::CompleteWithInsert, window, cx) + } + + pub fn confirm_completion_replace( + &mut self, + _: &ConfirmCompletionReplace, + window: &mut Window, + cx: &mut Context, + ) -> Option>> { + self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + self.do_completion(None, CompletionIntent::CompleteWithReplace, window, cx) + } + + pub fn compose_completion( + &mut self, + action: &ComposeCompletion, + window: &mut Window, + cx: &mut Context, + ) -> Option>> { + self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + self.do_completion(action.item_ix, CompletionIntent::Compose, window, cx) + } + + fn do_completion( + &mut self, + item_ix: Option, + intent: CompletionIntent, + window: &mut Window, + cx: &mut Context, + ) -> Option>> { + use language::ToOffset as _; + + let CodeContextMenu::Completions(completions_menu) = self.hide_context_menu(window, cx)? + else { + return None; + }; + + let candidate_id = { + let entries = completions_menu.entries.borrow(); + let mat = entries.get(item_ix.unwrap_or(completions_menu.selected_item))?; + if self.show_edit_predictions_in_menu() { + self.discard_inline_completion(true, cx); + } + mat.candidate_id + }; + + let buffer_handle = completions_menu.buffer; + let completion = completions_menu + .completions + .borrow() + .get(candidate_id)? + .clone(); + cx.stop_propagation(); + + let snippet; + let new_text; + if completion.is_snippet() { + snippet = Some(Snippet::parse(&completion.new_text).log_err()?); + new_text = snippet.as_ref().unwrap().text.clone(); + } else { + snippet = None; + new_text = completion.new_text.clone(); + }; + + let replace_range = choose_completion_range(&completion, intent, &buffer_handle, cx); + let buffer = buffer_handle.read(cx); + let snapshot = self.buffer.read(cx).snapshot(cx); + let replace_range_multibuffer = { + let excerpt = snapshot + .excerpt_containing(self.selections.newest_anchor().range()) + .unwrap(); + let multibuffer_anchor = snapshot + .anchor_in_excerpt(excerpt.id(), buffer.anchor_before(replace_range.start)) + .unwrap() + ..snapshot + .anchor_in_excerpt(excerpt.id(), buffer.anchor_before(replace_range.end)) + .unwrap(); + multibuffer_anchor.start.to_offset(&snapshot) + ..multibuffer_anchor.end.to_offset(&snapshot) + }; + let newest_anchor = self.selections.newest_anchor(); + if newest_anchor.head().buffer_id != Some(buffer.remote_id()) { + return None; + } + + let old_text = buffer + .text_for_range(replace_range.clone()) + .collect::(); + let lookbehind = newest_anchor + .start + .text_anchor + .to_offset(buffer) + .saturating_sub(replace_range.start); + let lookahead = replace_range + .end + .saturating_sub(newest_anchor.end.text_anchor.to_offset(buffer)); + let prefix = &old_text[..old_text.len().saturating_sub(lookahead)]; + let suffix = &old_text[lookbehind.min(old_text.len())..]; + + let selections = self.selections.all::(cx); + let mut ranges = Vec::new(); + let mut linked_edits = HashMap::<_, Vec<_>>::default(); + + for selection in &selections { + let range = if selection.id == newest_anchor.id { + replace_range_multibuffer.clone() + } else { + let mut range = selection.range(); + + // if prefix is present, don't duplicate it + if snapshot.contains_str_at(range.start.saturating_sub(lookbehind), prefix) { + range.start = range.start.saturating_sub(lookbehind); + + // if suffix is also present, mimic the newest cursor and replace it + if selection.id != newest_anchor.id + && snapshot.contains_str_at(range.end, suffix) + { + range.end += lookahead; + } + } + range + }; + + ranges.push(range); + + if !self.linked_edit_ranges.is_empty() { + let start_anchor = snapshot.anchor_before(selection.head()); + let end_anchor = snapshot.anchor_after(selection.tail()); + if let Some(ranges) = self + .linked_editing_ranges_for(start_anchor.text_anchor..end_anchor.text_anchor, cx) + { + for (buffer, edits) in ranges { + linked_edits + .entry(buffer.clone()) + .or_default() + .extend(edits.into_iter().map(|range| (range, new_text.to_owned()))); + } + } + } + } + + cx.emit(EditorEvent::InputHandled { + utf16_range_to_replace: None, + text: new_text.clone().into(), + }); + + self.transact(window, cx, |this, window, cx| { + if let Some(mut snippet) = snippet { + snippet.text = new_text.to_string(); + this.insert_snippet(&ranges, snippet, window, cx).log_err(); + } else { + this.buffer.update(cx, |buffer, cx| { + let auto_indent = match completion.insert_text_mode { + Some(InsertTextMode::AS_IS) => None, + _ => this.autoindent_mode.clone(), + }; + let edits = ranges.into_iter().map(|range| (range, new_text.as_str())); + buffer.edit(edits, auto_indent, cx); + }); + } + for (buffer, edits) in linked_edits { + buffer.update(cx, |buffer, cx| { + let snapshot = buffer.snapshot(); + let edits = edits + .into_iter() + .map(|(range, text)| { + use text::ToPoint as TP; + let end_point = TP::to_point(&range.end, &snapshot); + let start_point = TP::to_point(&range.start, &snapshot); + (start_point..end_point, text) + }) + .sorted_by_key(|(range, _)| range.start); + buffer.edit(edits, None, cx); + }) + } + + this.refresh_inline_completion(true, false, window, cx); + }); + + let show_new_completions_on_confirm = completion + .confirm + .as_ref() + .map_or(false, |confirm| confirm(intent, window, cx)); + if show_new_completions_on_confirm { + self.show_completions(&ShowCompletions { trigger: None }, window, cx); + } + + let provider = self.completion_provider.as_ref()?; + drop(completion); + let apply_edits = provider.apply_additional_edits_for_completion( + buffer_handle, + completions_menu.completions.clone(), + candidate_id, + true, + cx, + ); + + let editor_settings = EditorSettings::get_global(cx); + if editor_settings.show_signature_help_after_edits || editor_settings.auto_signature_help { + // After the code completion is finished, users often want to know what signatures are needed. + // so we should automatically call signature_help + self.show_signature_help(&ShowSignatureHelp, window, cx); + } + + Some(cx.foreground_executor().spawn(async move { + apply_edits.await?; + Ok(()) + })) + } + + pub fn toggle_code_actions( + &mut self, + action: &ToggleCodeActions, + window: &mut Window, + cx: &mut Context, + ) { + let quick_launch = action.quick_launch; + let mut context_menu = self.context_menu.borrow_mut(); + if let Some(CodeContextMenu::CodeActions(code_actions)) = context_menu.as_ref() { + if code_actions.deployed_from_indicator == action.deployed_from_indicator { + // Toggle if we're selecting the same one + *context_menu = None; + cx.notify(); + return; + } else { + // Otherwise, clear it and start a new one + *context_menu = None; + cx.notify(); + } + } + drop(context_menu); + let snapshot = self.snapshot(window, cx); + let deployed_from_indicator = action.deployed_from_indicator; + let mut task = self.code_actions_task.take(); + let action = action.clone(); + cx.spawn_in(window, async move |editor, cx| { + while let Some(prev_task) = task { + prev_task.await.log_err(); + task = editor.update(cx, |this, _| this.code_actions_task.take())?; + } + + let spawned_test_task = editor.update_in(cx, |editor, window, cx| { + if editor.focus_handle.is_focused(window) { + let multibuffer_point = action + .deployed_from_indicator + .map(|row| DisplayPoint::new(row, 0).to_point(&snapshot)) + .unwrap_or_else(|| editor.selections.newest::(cx).head()); + let (buffer, buffer_row) = snapshot + .buffer_snapshot + .buffer_line_for_row(MultiBufferRow(multibuffer_point.row)) + .and_then(|(buffer_snapshot, range)| { + editor + .buffer + .read(cx) + .buffer(buffer_snapshot.remote_id()) + .map(|buffer| (buffer, range.start.row)) + })?; + let (_, code_actions) = editor + .available_code_actions + .clone() + .and_then(|(location, code_actions)| { + let snapshot = location.buffer.read(cx).snapshot(); + let point_range = location.range.to_point(&snapshot); + let point_range = point_range.start.row..=point_range.end.row; + if point_range.contains(&buffer_row) { + Some((location, code_actions)) + } else { + None + } + }) + .unzip(); + let buffer_id = buffer.read(cx).remote_id(); + let tasks = editor + .tasks + .get(&(buffer_id, buffer_row)) + .map(|t| Arc::new(t.to_owned())); + if tasks.is_none() && code_actions.is_none() { + return None; + } + + editor.completion_tasks.clear(); + editor.discard_inline_completion(false, cx); + let task_context = + tasks + .as_ref() + .zip(editor.project.clone()) + .map(|(tasks, project)| { + Self::build_tasks_context(&project, &buffer, buffer_row, tasks, cx) + }); + + Some(cx.spawn_in(window, async move |editor, cx| { + let task_context = match task_context { + Some(task_context) => task_context.await, + None => None, + }; + let resolved_tasks = + tasks + .zip(task_context.clone()) + .map(|(tasks, task_context)| ResolvedTasks { + templates: tasks.resolve(&task_context).collect(), + position: snapshot.buffer_snapshot.anchor_before(Point::new( + multibuffer_point.row, + tasks.column, + )), + }); + let spawn_straight_away = quick_launch + && resolved_tasks + .as_ref() + .map_or(false, |tasks| tasks.templates.len() == 1) + && code_actions + .as_ref() + .map_or(true, |actions| actions.is_empty()); + let debug_scenarios = editor.update(cx, |editor, cx| { + if cx.has_flag::() { + maybe!({ + let project = editor.project.as_ref()?; + let dap_store = project.read(cx).dap_store(); + let mut scenarios = vec![]; + let resolved_tasks = resolved_tasks.as_ref()?; + let debug_adapter: SharedString = buffer + .read(cx) + .language()? + .context_provider()? + .debug_adapter()? + .into(); + dap_store.update(cx, |this, cx| { + for (_, task) in &resolved_tasks.templates { + if let Some(scenario) = this + .debug_scenario_for_build_task( + task.resolved.clone(), + SharedString::from( + task.original_task().label.clone(), + ), + debug_adapter.clone(), + cx, + ) + { + scenarios.push(scenario); + } + } + }); + Some(scenarios) + }) + .unwrap_or_default() + } else { + vec![] + } + })?; + if let Ok(task) = editor.update_in(cx, |editor, window, cx| { + *editor.context_menu.borrow_mut() = + Some(CodeContextMenu::CodeActions(CodeActionsMenu { + buffer, + actions: CodeActionContents::new( + resolved_tasks, + code_actions, + debug_scenarios, + task_context.unwrap_or_default(), + ), + selected_item: Default::default(), + scroll_handle: UniformListScrollHandle::default(), + deployed_from_indicator, + })); + if spawn_straight_away { + if let Some(task) = editor.confirm_code_action( + &ConfirmCodeAction { item_ix: Some(0) }, + window, + cx, + ) { + cx.notify(); + return task; + } + } + cx.notify(); + Task::ready(Ok(())) + }) { + task.await + } else { + Ok(()) + } + })) + } else { + Some(Task::ready(Ok(()))) + } + })?; + if let Some(task) = spawned_test_task { + task.await?; + } + + Ok::<_, anyhow::Error>(()) + }) + .detach_and_log_err(cx); + } + + pub fn confirm_code_action( + &mut self, + action: &ConfirmCodeAction, + window: &mut Window, + cx: &mut Context, + ) -> Option>> { + self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + + let actions_menu = + if let CodeContextMenu::CodeActions(menu) = self.hide_context_menu(window, cx)? { + menu + } else { + return None; + }; + + let action_ix = action.item_ix.unwrap_or(actions_menu.selected_item); + let action = actions_menu.actions.get(action_ix)?; + let title = action.label(); + let buffer = actions_menu.buffer; + let workspace = self.workspace()?; + + match action { + CodeActionsItem::Task(task_source_kind, resolved_task) => { + workspace.update(cx, |workspace, cx| { + workspace.schedule_resolved_task( + task_source_kind, + resolved_task, + false, + window, + cx, + ); + + Some(Task::ready(Ok(()))) + }) + } + CodeActionsItem::CodeAction { + excerpt_id, + action, + provider, + } => { + let apply_code_action = + provider.apply_code_action(buffer, action, excerpt_id, true, window, cx); + let workspace = workspace.downgrade(); + Some(cx.spawn_in(window, async move |editor, cx| { + let project_transaction = apply_code_action.await?; + Self::open_project_transaction( + &editor, + workspace, + project_transaction, + title, + cx, + ) + .await + })) + } + CodeActionsItem::DebugScenario(scenario) => { + let context = actions_menu.actions.context.clone(); + + workspace.update(cx, |workspace, cx| { + workspace.start_debug_session(scenario, context, Some(buffer), window, cx); + }); + Some(Task::ready(Ok(()))) + } + } + } + + pub async fn open_project_transaction( + this: &WeakEntity, + workspace: WeakEntity, + transaction: ProjectTransaction, + title: String, + cx: &mut AsyncWindowContext, + ) -> Result<()> { + let mut entries = transaction.0.into_iter().collect::>(); + cx.update(|_, cx| { + entries.sort_unstable_by_key(|(buffer, _)| { + buffer.read(cx).file().map(|f| f.path().clone()) + }); + })?; + + // If the project transaction's edits are all contained within this editor, then + // avoid opening a new editor to display them. + + if let Some((buffer, transaction)) = entries.first() { + if entries.len() == 1 { + let excerpt = this.update(cx, |editor, cx| { + editor + .buffer() + .read(cx) + .excerpt_containing(editor.selections.newest_anchor().head(), cx) + })?; + if let Some((_, excerpted_buffer, excerpt_range)) = excerpt { + if excerpted_buffer == *buffer { + let all_edits_within_excerpt = buffer.read_with(cx, |buffer, _| { + let excerpt_range = excerpt_range.to_offset(buffer); + buffer + .edited_ranges_for_transaction::(transaction) + .all(|range| { + excerpt_range.start <= range.start + && excerpt_range.end >= range.end + }) + })?; + + if all_edits_within_excerpt { + return Ok(()); + } + } + } + } + } else { + return Ok(()); + } + + let mut ranges_to_highlight = Vec::new(); + let excerpt_buffer = cx.new(|cx| { + let mut multibuffer = MultiBuffer::new(Capability::ReadWrite).with_title(title); + for (buffer_handle, transaction) in &entries { + let edited_ranges = buffer_handle + .read(cx) + .edited_ranges_for_transaction::(transaction) + .collect::>(); + let (ranges, _) = multibuffer.set_excerpts_for_path( + PathKey::for_buffer(buffer_handle, cx), + buffer_handle.clone(), + edited_ranges, + DEFAULT_MULTIBUFFER_CONTEXT, + cx, + ); + + ranges_to_highlight.extend(ranges); + } + multibuffer.push_transaction(entries.iter().map(|(b, t)| (b, t)), cx); + multibuffer + })?; + + workspace.update_in(cx, |workspace, window, cx| { + let project = workspace.project().clone(); + let editor = + cx.new(|cx| Editor::for_multibuffer(excerpt_buffer, Some(project), window, cx)); + workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx); + editor.update(cx, |editor, cx| { + editor.highlight_background::( + &ranges_to_highlight, + |theme| theme.editor_highlighted_line_background, + cx, + ); + }); + })?; + + Ok(()) + } + + pub fn clear_code_action_providers(&mut self) { + self.code_action_providers.clear(); + self.available_code_actions.take(); + } + + pub fn add_code_action_provider( + &mut self, + provider: Rc, + window: &mut Window, + cx: &mut Context, + ) { + if self + .code_action_providers + .iter() + .any(|existing_provider| existing_provider.id() == provider.id()) + { + return; + } + + self.code_action_providers.push(provider); + self.refresh_code_actions(window, cx); + } + + pub fn remove_code_action_provider( + &mut self, + id: Arc, + window: &mut Window, + cx: &mut Context, + ) { + self.code_action_providers + .retain(|provider| provider.id() != id); + self.refresh_code_actions(window, cx); + } + + fn refresh_code_actions(&mut self, window: &mut Window, cx: &mut Context) -> Option<()> { + let newest_selection = self.selections.newest_anchor().clone(); + let newest_selection_adjusted = self.selections.newest_adjusted(cx).clone(); + let buffer = self.buffer.read(cx); + if newest_selection.head().diff_base_anchor.is_some() { + return None; + } + let (start_buffer, start) = + buffer.text_anchor_for_position(newest_selection_adjusted.start, cx)?; + let (end_buffer, end) = + buffer.text_anchor_for_position(newest_selection_adjusted.end, cx)?; + if start_buffer != end_buffer { + return None; + } + + self.code_actions_task = Some(cx.spawn_in(window, async move |this, cx| { + cx.background_executor() + .timer(CODE_ACTIONS_DEBOUNCE_TIMEOUT) + .await; + + let (providers, tasks) = this.update_in(cx, |this, window, cx| { + let providers = this.code_action_providers.clone(); + let tasks = this + .code_action_providers + .iter() + .map(|provider| provider.code_actions(&start_buffer, start..end, window, cx)) + .collect::>(); + (providers, tasks) + })?; + + let mut actions = Vec::new(); + for (provider, provider_actions) in + providers.into_iter().zip(future::join_all(tasks).await) + { + if let Some(provider_actions) = provider_actions.log_err() { + actions.extend(provider_actions.into_iter().map(|action| { + AvailableCodeAction { + excerpt_id: newest_selection.start.excerpt_id, + action, + provider: provider.clone(), + } + })); + } + } + + this.update(cx, |this, cx| { + this.available_code_actions = if actions.is_empty() { + None + } else { + Some(( + Location { + buffer: start_buffer, + range: start..end, + }, + actions.into(), + )) + }; + cx.notify(); + }) + })); + None + } + + fn start_inline_blame_timer(&mut self, window: &mut Window, cx: &mut Context) { + if let Some(delay) = ProjectSettings::get_global(cx).git.inline_blame_delay() { + self.show_git_blame_inline = false; + + self.show_git_blame_inline_delay_task = + Some(cx.spawn_in(window, async move |this, cx| { + cx.background_executor().timer(delay).await; + + this.update(cx, |this, cx| { + this.show_git_blame_inline = true; + cx.notify(); + }) + .log_err(); + })); + } + } + + fn show_blame_popover( + &mut self, + blame_entry: &BlameEntry, + position: gpui::Point, + cx: &mut Context, + ) { + if let Some(state) = &mut self.inline_blame_popover { + state.hide_task.take(); + cx.notify(); + } else { + let delay = EditorSettings::get_global(cx).hover_popover_delay; + let show_task = cx.spawn(async move |editor, cx| { + cx.background_executor() + .timer(std::time::Duration::from_millis(delay)) + .await; + editor + .update(cx, |editor, cx| { + if let Some(state) = &mut editor.inline_blame_popover { + state.show_task = None; + cx.notify(); + } + }) + .ok(); + }); + let Some(blame) = self.blame.as_ref() else { + return; + }; + let blame = blame.read(cx); + let details = blame.details_for_entry(&blame_entry); + let markdown = cx.new(|cx| { + Markdown::new( + details + .as_ref() + .map(|message| message.message.clone()) + .unwrap_or_default(), + None, + None, + cx, + ) + }); + self.inline_blame_popover = Some(InlineBlamePopover { + position, + show_task: Some(show_task), + hide_task: None, + popover_bounds: None, + popover_state: InlineBlamePopoverState { + scroll_handle: ScrollHandle::new(), + commit_message: details, + markdown, + }, + }); + } + } + + fn hide_blame_popover(&mut self, cx: &mut Context) { + if let Some(state) = &mut self.inline_blame_popover { + if state.show_task.is_some() { + self.inline_blame_popover.take(); + cx.notify(); + } else { + let hide_task = cx.spawn(async move |editor, cx| { + cx.background_executor() + .timer(std::time::Duration::from_millis(100)) + .await; + editor + .update(cx, |editor, cx| { + editor.inline_blame_popover.take(); + cx.notify(); + }) + .ok(); + }); + state.hide_task = Some(hide_task); + } + } + } + + fn refresh_document_highlights(&mut self, cx: &mut Context) -> Option<()> { + if self.pending_rename.is_some() { + return None; + } + + let provider = self.semantics_provider.clone()?; + let buffer = self.buffer.read(cx); + let newest_selection = self.selections.newest_anchor().clone(); + let cursor_position = newest_selection.head(); + let (cursor_buffer, cursor_buffer_position) = + buffer.text_anchor_for_position(cursor_position, cx)?; + let (tail_buffer, _) = buffer.text_anchor_for_position(newest_selection.tail(), cx)?; + if cursor_buffer != tail_buffer { + return None; + } + let debounce = EditorSettings::get_global(cx).lsp_highlight_debounce; + self.document_highlights_task = Some(cx.spawn(async move |this, cx| { + cx.background_executor() + .timer(Duration::from_millis(debounce)) + .await; + + let highlights = if let Some(highlights) = cx + .update(|cx| { + provider.document_highlights(&cursor_buffer, cursor_buffer_position, cx) + }) + .ok() + .flatten() + { + highlights.await.log_err() + } else { + None + }; + + if let Some(highlights) = highlights { + this.update(cx, |this, cx| { + if this.pending_rename.is_some() { + return; + } + + let buffer_id = cursor_position.buffer_id; + let buffer = this.buffer.read(cx); + if !buffer + .text_anchor_for_position(cursor_position, cx) + .map_or(false, |(buffer, _)| buffer == cursor_buffer) + { + return; + } + + let cursor_buffer_snapshot = cursor_buffer.read(cx); + let mut write_ranges = Vec::new(); + let mut read_ranges = Vec::new(); + for highlight in highlights { + for (excerpt_id, excerpt_range) in + buffer.excerpts_for_buffer(cursor_buffer.read(cx).remote_id(), cx) + { + let start = highlight + .range + .start + .max(&excerpt_range.context.start, cursor_buffer_snapshot); + let end = highlight + .range + .end + .min(&excerpt_range.context.end, cursor_buffer_snapshot); + if start.cmp(&end, cursor_buffer_snapshot).is_ge() { + continue; + } + + let range = Anchor { + buffer_id, + excerpt_id, + text_anchor: start, + diff_base_anchor: None, + }..Anchor { + buffer_id, + excerpt_id, + text_anchor: end, + diff_base_anchor: None, + }; + if highlight.kind == lsp::DocumentHighlightKind::WRITE { + write_ranges.push(range); + } else { + read_ranges.push(range); + } + } + } + + this.highlight_background::( + &read_ranges, + |theme| theme.editor_document_highlight_read_background, + cx, + ); + this.highlight_background::( + &write_ranges, + |theme| theme.editor_document_highlight_write_background, + cx, + ); + cx.notify(); + }) + .log_err(); + } + })); + None + } + + fn prepare_highlight_query_from_selection( + &mut self, + cx: &mut Context, + ) -> Option<(String, Range)> { + if matches!(self.mode, EditorMode::SingleLine { .. }) { + return None; + } + if !EditorSettings::get_global(cx).selection_highlight { + return None; + } + if self.selections.count() != 1 || self.selections.line_mode { + return None; + } + let selection = self.selections.newest::(cx); + if selection.is_empty() || selection.start.row != selection.end.row { + return None; + } + let multi_buffer_snapshot = self.buffer().read(cx).snapshot(cx); + let selection_anchor_range = selection.range().to_anchors(&multi_buffer_snapshot); + let query = multi_buffer_snapshot + .text_for_range(selection_anchor_range.clone()) + .collect::(); + if query.trim().is_empty() { + return None; + } + Some((query, selection_anchor_range)) + } + + fn update_selection_occurrence_highlights( + &mut self, + query_text: String, + query_range: Range, + multi_buffer_range_to_query: Range, + use_debounce: bool, + window: &mut Window, + cx: &mut Context, + ) -> Task<()> { + let multi_buffer_snapshot = self.buffer().read(cx).snapshot(cx); + cx.spawn_in(window, async move |editor, cx| { + if use_debounce { + cx.background_executor() + .timer(SELECTION_HIGHLIGHT_DEBOUNCE_TIMEOUT) + .await; + } + let match_task = cx.background_spawn(async move { + let buffer_ranges = multi_buffer_snapshot + .range_to_buffer_ranges(multi_buffer_range_to_query) + .into_iter() + .filter(|(_, excerpt_visible_range, _)| !excerpt_visible_range.is_empty()); + let mut match_ranges = Vec::new(); + for (buffer_snapshot, search_range, excerpt_id) in buffer_ranges { + match_ranges.extend( + project::search::SearchQuery::text( + query_text.clone(), + false, + false, + false, + Default::default(), + Default::default(), + false, + None, + ) + .unwrap() + .search(&buffer_snapshot, Some(search_range.clone())) + .await + .into_iter() + .filter_map(|match_range| { + let match_start = buffer_snapshot + .anchor_after(search_range.start + match_range.start); + let match_end = + buffer_snapshot.anchor_before(search_range.start + match_range.end); + let match_anchor_range = Anchor::range_in_buffer( + excerpt_id, + buffer_snapshot.remote_id(), + match_start..match_end, + ); + (match_anchor_range != query_range).then_some(match_anchor_range) + }), + ); + } + match_ranges + }); + let match_ranges = match_task.await; + editor + .update_in(cx, |editor, _, cx| { + editor.clear_background_highlights::(cx); + if !match_ranges.is_empty() { + editor.highlight_background::( + &match_ranges, + |theme| theme.editor_document_highlight_bracket_background, + cx, + ) + } + }) + .log_err(); + }) + } + + fn refresh_selected_text_highlights( + &mut self, + on_buffer_edit: bool, + window: &mut Window, + cx: &mut Context, + ) { + let Some((query_text, query_range)) = self.prepare_highlight_query_from_selection(cx) + else { + self.clear_background_highlights::(cx); + self.quick_selection_highlight_task.take(); + self.debounced_selection_highlight_task.take(); + return; + }; + let multi_buffer_snapshot = self.buffer().read(cx).snapshot(cx); + if on_buffer_edit + || self + .quick_selection_highlight_task + .as_ref() + .map_or(true, |(prev_anchor_range, _)| { + prev_anchor_range != &query_range + }) + { + let multi_buffer_visible_start = self + .scroll_manager + .anchor() + .anchor + .to_point(&multi_buffer_snapshot); + let multi_buffer_visible_end = multi_buffer_snapshot.clip_point( + multi_buffer_visible_start + + Point::new(self.visible_line_count().unwrap_or(0.).ceil() as u32, 0), + Bias::Left, + ); + let multi_buffer_visible_range = multi_buffer_visible_start..multi_buffer_visible_end; + self.quick_selection_highlight_task = Some(( + query_range.clone(), + self.update_selection_occurrence_highlights( + query_text.clone(), + query_range.clone(), + multi_buffer_visible_range, + false, + window, + cx, + ), + )); + } + if on_buffer_edit + || self + .debounced_selection_highlight_task + .as_ref() + .map_or(true, |(prev_anchor_range, _)| { + prev_anchor_range != &query_range + }) + { + let multi_buffer_start = multi_buffer_snapshot + .anchor_before(0) + .to_point(&multi_buffer_snapshot); + let multi_buffer_end = multi_buffer_snapshot + .anchor_after(multi_buffer_snapshot.len()) + .to_point(&multi_buffer_snapshot); + let multi_buffer_full_range = multi_buffer_start..multi_buffer_end; + self.debounced_selection_highlight_task = Some(( + query_range.clone(), + self.update_selection_occurrence_highlights( + query_text, + query_range, + multi_buffer_full_range, + true, + window, + cx, + ), + )); + } + } + + pub fn refresh_inline_completion( + &mut self, + debounce: bool, + user_requested: bool, + window: &mut Window, + cx: &mut Context, + ) -> Option<()> { + let provider = self.edit_prediction_provider()?; + let cursor = self.selections.newest_anchor().head(); + let (buffer, cursor_buffer_position) = + self.buffer.read(cx).text_anchor_for_position(cursor, cx)?; + + if !self.edit_predictions_enabled_in_buffer(&buffer, cursor_buffer_position, cx) { + self.discard_inline_completion(false, cx); + return None; + } + + if !user_requested + && (!self.should_show_edit_predictions() + || !self.is_focused(window) + || buffer.read(cx).is_empty()) + { + self.discard_inline_completion(false, cx); + return None; + } + + self.update_visible_inline_completion(window, cx); + provider.refresh( + self.project.clone(), + buffer, + cursor_buffer_position, + debounce, + cx, + ); + Some(()) + } + + fn show_edit_predictions_in_menu(&self) -> bool { + match self.edit_prediction_settings { + EditPredictionSettings::Disabled => false, + EditPredictionSettings::Enabled { show_in_menu, .. } => show_in_menu, + } + } + + pub fn edit_predictions_enabled(&self) -> bool { + match self.edit_prediction_settings { + EditPredictionSettings::Disabled => false, + EditPredictionSettings::Enabled { .. } => true, + } + } + + fn edit_prediction_requires_modifier(&self) -> bool { + match self.edit_prediction_settings { + EditPredictionSettings::Disabled => false, + EditPredictionSettings::Enabled { + preview_requires_modifier, + .. + } => preview_requires_modifier, + } + } + + pub fn update_edit_prediction_settings(&mut self, cx: &mut Context) { + if self.edit_prediction_provider.is_none() { + self.edit_prediction_settings = EditPredictionSettings::Disabled; + } else { + let selection = self.selections.newest_anchor(); + let cursor = selection.head(); + + if let Some((buffer, cursor_buffer_position)) = + self.buffer.read(cx).text_anchor_for_position(cursor, cx) + { + self.edit_prediction_settings = + self.edit_prediction_settings_at_position(&buffer, cursor_buffer_position, cx); + } + } + } + + fn edit_prediction_settings_at_position( + &self, + buffer: &Entity, + buffer_position: language::Anchor, + cx: &App, + ) -> EditPredictionSettings { + if !self.mode.is_full() + || !self.show_inline_completions_override.unwrap_or(true) + || self.inline_completions_disabled_in_scope(buffer, buffer_position, cx) + { + return EditPredictionSettings::Disabled; + } + + let buffer = buffer.read(cx); + + let file = buffer.file(); + + if !language_settings(buffer.language().map(|l| l.name()), file, cx).show_edit_predictions { + return EditPredictionSettings::Disabled; + }; + + let by_provider = matches!( + self.menu_inline_completions_policy, + MenuInlineCompletionsPolicy::ByProvider + ); + + let show_in_menu = by_provider + && self + .edit_prediction_provider + .as_ref() + .map_or(false, |provider| { + provider.provider.show_completions_in_menu() + }); + + let preview_requires_modifier = + all_language_settings(file, cx).edit_predictions_mode() == EditPredictionsMode::Subtle; + + EditPredictionSettings::Enabled { + show_in_menu, + preview_requires_modifier, + } + } + + fn should_show_edit_predictions(&self) -> bool { + self.snippet_stack.is_empty() && self.edit_predictions_enabled() + } + + pub fn edit_prediction_preview_is_active(&self) -> bool { + matches!( + self.edit_prediction_preview, + EditPredictionPreview::Active { .. } + ) + } + + pub fn edit_predictions_enabled_at_cursor(&self, cx: &App) -> bool { + let cursor = self.selections.newest_anchor().head(); + if let Some((buffer, cursor_position)) = + self.buffer.read(cx).text_anchor_for_position(cursor, cx) + { + self.edit_predictions_enabled_in_buffer(&buffer, cursor_position, cx) + } else { + false + } + } + + fn edit_predictions_enabled_in_buffer( + &self, + buffer: &Entity, + buffer_position: language::Anchor, + cx: &App, + ) -> bool { + maybe!({ + if self.read_only(cx) { + return Some(false); + } + let provider = self.edit_prediction_provider()?; + if !provider.is_enabled(&buffer, buffer_position, cx) { + return Some(false); + } + let buffer = buffer.read(cx); + let Some(file) = buffer.file() else { + return Some(true); + }; + let settings = all_language_settings(Some(file), cx); + Some(settings.edit_predictions_enabled_for_file(file, cx)) + }) + .unwrap_or(false) + } + + fn cycle_inline_completion( + &mut self, + direction: Direction, + window: &mut Window, + cx: &mut Context, + ) -> Option<()> { + let provider = self.edit_prediction_provider()?; + let cursor = self.selections.newest_anchor().head(); + let (buffer, cursor_buffer_position) = + self.buffer.read(cx).text_anchor_for_position(cursor, cx)?; + if self.inline_completions_hidden_for_vim_mode || !self.should_show_edit_predictions() { + return None; + } + + provider.cycle(buffer, cursor_buffer_position, direction, cx); + self.update_visible_inline_completion(window, cx); + + Some(()) + } + + pub fn show_inline_completion( + &mut self, + _: &ShowEditPrediction, + window: &mut Window, + cx: &mut Context, + ) { + if !self.has_active_inline_completion() { + self.refresh_inline_completion(false, true, window, cx); + return; + } + + self.update_visible_inline_completion(window, cx); + } + + pub fn display_cursor_names( + &mut self, + _: &DisplayCursorNames, + window: &mut Window, + cx: &mut Context, + ) { + self.show_cursor_names(window, cx); + } + + fn show_cursor_names(&mut self, window: &mut Window, cx: &mut Context) { + self.show_cursor_names = true; + cx.notify(); + cx.spawn_in(window, async move |this, cx| { + cx.background_executor().timer(CURSORS_VISIBLE_FOR).await; + this.update(cx, |this, cx| { + this.show_cursor_names = false; + cx.notify() + }) + .ok() + }) + .detach(); + } + + pub fn next_edit_prediction( + &mut self, + _: &NextEditPrediction, + window: &mut Window, + cx: &mut Context, + ) { + if self.has_active_inline_completion() { + self.cycle_inline_completion(Direction::Next, window, cx); + } else { + let is_copilot_disabled = self + .refresh_inline_completion(false, true, window, cx) + .is_none(); + if is_copilot_disabled { + cx.propagate(); + } + } + } + + pub fn previous_edit_prediction( + &mut self, + _: &PreviousEditPrediction, + window: &mut Window, + cx: &mut Context, + ) { + if self.has_active_inline_completion() { + self.cycle_inline_completion(Direction::Prev, window, cx); + } else { + let is_copilot_disabled = self + .refresh_inline_completion(false, true, window, cx) + .is_none(); + if is_copilot_disabled { + cx.propagate(); + } + } + } + + pub fn accept_edit_prediction( + &mut self, + _: &AcceptEditPrediction, + window: &mut Window, + cx: &mut Context, + ) { + if self.show_edit_predictions_in_menu() { + self.hide_context_menu(window, cx); + } + + let Some(active_inline_completion) = self.active_inline_completion.as_ref() else { + return; + }; + + self.report_inline_completion_event( + active_inline_completion.completion_id.clone(), + true, + cx, + ); + + match &active_inline_completion.completion { + InlineCompletion::Move { target, .. } => { + let target = *target; + + if let Some(position_map) = &self.last_position_map { + if position_map + .visible_row_range + .contains(&target.to_display_point(&position_map.snapshot).row()) + || !self.edit_prediction_requires_modifier() + { + self.unfold_ranges(&[target..target], true, false, cx); + // Note that this is also done in vim's handler of the Tab action. + self.change_selections( + Some(Autoscroll::newest()), + window, + cx, + |selections| { + selections.select_anchor_ranges([target..target]); + }, + ); + self.clear_row_highlights::(); + + self.edit_prediction_preview + .set_previous_scroll_position(None); + } else { + self.edit_prediction_preview + .set_previous_scroll_position(Some( + position_map.snapshot.scroll_anchor, + )); + + self.highlight_rows::( + target..target, + cx.theme().colors().editor_highlighted_line_background, + RowHighlightOptions { + autoscroll: true, + ..Default::default() + }, + cx, + ); + self.request_autoscroll(Autoscroll::fit(), cx); + } + } + } + InlineCompletion::Edit { edits, .. } => { + if let Some(provider) = self.edit_prediction_provider() { + provider.accept(cx); + } + + let snapshot = self.buffer.read(cx).snapshot(cx); + let last_edit_end = edits.last().unwrap().0.end.bias_right(&snapshot); + + self.buffer.update(cx, |buffer, cx| { + buffer.edit(edits.iter().cloned(), None, cx) + }); + + self.change_selections(None, window, cx, |s| { + s.select_anchor_ranges([last_edit_end..last_edit_end]) + }); + + self.update_visible_inline_completion(window, cx); + if self.active_inline_completion.is_none() { + self.refresh_inline_completion(true, true, window, cx); + } + + cx.notify(); + } + } + + self.edit_prediction_requires_modifier_in_indent_conflict = false; + } + + pub fn accept_partial_inline_completion( + &mut self, + _: &AcceptPartialEditPrediction, + window: &mut Window, + cx: &mut Context, + ) { + let Some(active_inline_completion) = self.active_inline_completion.as_ref() else { + return; + }; + if self.selections.count() != 1 { + return; + } + + self.report_inline_completion_event( + active_inline_completion.completion_id.clone(), + true, + cx, + ); + + match &active_inline_completion.completion { + InlineCompletion::Move { target, .. } => { + let target = *target; + self.change_selections(Some(Autoscroll::newest()), window, cx, |selections| { + selections.select_anchor_ranges([target..target]); + }); + } + InlineCompletion::Edit { edits, .. } => { + // Find an insertion that starts at the cursor position. + let snapshot = self.buffer.read(cx).snapshot(cx); + let cursor_offset = self.selections.newest::(cx).head(); + let insertion = edits.iter().find_map(|(range, text)| { + let range = range.to_offset(&snapshot); + if range.is_empty() && range.start == cursor_offset { + Some(text) + } else { + None + } + }); + + if let Some(text) = insertion { + let mut partial_completion = text + .chars() + .by_ref() + .take_while(|c| c.is_alphabetic()) + .collect::(); + if partial_completion.is_empty() { + partial_completion = text + .chars() + .by_ref() + .take_while(|c| c.is_whitespace() || !c.is_alphabetic()) + .collect::(); + } + + cx.emit(EditorEvent::InputHandled { + utf16_range_to_replace: None, + text: partial_completion.clone().into(), + }); + + self.insert_with_autoindent_mode(&partial_completion, None, window, cx); + + self.refresh_inline_completion(true, true, window, cx); + cx.notify(); + } else { + self.accept_edit_prediction(&Default::default(), window, cx); + } + } + } + } + + fn discard_inline_completion( + &mut self, + should_report_inline_completion_event: bool, + cx: &mut Context, + ) -> bool { + if should_report_inline_completion_event { + let completion_id = self + .active_inline_completion + .as_ref() + .and_then(|active_completion| active_completion.completion_id.clone()); + + self.report_inline_completion_event(completion_id, false, cx); + } + + if let Some(provider) = self.edit_prediction_provider() { + provider.discard(cx); + } + + self.take_active_inline_completion(cx) + } + + fn report_inline_completion_event(&self, id: Option, accepted: bool, cx: &App) { + let Some(provider) = self.edit_prediction_provider() else { + return; + }; + + let Some((_, buffer, _)) = self + .buffer + .read(cx) + .excerpt_containing(self.selections.newest_anchor().head(), cx) + else { + return; + }; + + let extension = buffer + .read(cx) + .file() + .and_then(|file| Some(file.path().extension()?.to_string_lossy().to_string())); + + let event_type = match accepted { + true => "Edit Prediction Accepted", + false => "Edit Prediction Discarded", + }; + telemetry::event!( + event_type, + provider = provider.name(), + prediction_id = id, + suggestion_accepted = accepted, + file_extension = extension, + ); + } + + pub fn has_active_inline_completion(&self) -> bool { + self.active_inline_completion.is_some() + } + + fn take_active_inline_completion(&mut self, cx: &mut Context) -> bool { + let Some(active_inline_completion) = self.active_inline_completion.take() else { + return false; + }; + + self.splice_inlays(&active_inline_completion.inlay_ids, Default::default(), cx); + self.clear_highlights::(cx); + self.stale_inline_completion_in_menu = Some(active_inline_completion); + true + } + + /// Returns true when we're displaying the edit prediction popover below the cursor + /// like we are not previewing and the LSP autocomplete menu is visible + /// or we are in `when_holding_modifier` mode. + pub fn edit_prediction_visible_in_cursor_popover(&self, has_completion: bool) -> bool { + if self.edit_prediction_preview_is_active() + || !self.show_edit_predictions_in_menu() + || !self.edit_predictions_enabled() + { + return false; + } + + if self.has_visible_completions_menu() { + return true; + } + + has_completion && self.edit_prediction_requires_modifier() + } + + fn handle_modifiers_changed( + &mut self, + modifiers: Modifiers, + position_map: &PositionMap, + window: &mut Window, + cx: &mut Context, + ) { + if self.show_edit_predictions_in_menu() { + self.update_edit_prediction_preview(&modifiers, window, cx); + } + + self.update_selection_mode(&modifiers, position_map, window, cx); + + let mouse_position = window.mouse_position(); + if !position_map.text_hitbox.is_hovered(window) { + return; + } + + self.update_hovered_link( + position_map.point_for_position(mouse_position), + &position_map.snapshot, + modifiers, + window, + cx, + ) + } + + fn update_selection_mode( + &mut self, + modifiers: &Modifiers, + position_map: &PositionMap, + window: &mut Window, + cx: &mut Context, + ) { + if modifiers != &COLUMNAR_SELECTION_MODIFIERS || self.selections.pending.is_none() { + return; + } + + let mouse_position = window.mouse_position(); + let point_for_position = position_map.point_for_position(mouse_position); + let position = point_for_position.previous_valid; + + self.select( + SelectPhase::BeginColumnar { + position, + reset: false, + goal_column: point_for_position.exact_unclipped.column(), + }, + window, + cx, + ); + } + + fn update_edit_prediction_preview( + &mut self, + modifiers: &Modifiers, + window: &mut Window, + cx: &mut Context, + ) { + let accept_keybind = self.accept_edit_prediction_keybind(window, cx); + let Some(accept_keystroke) = accept_keybind.keystroke() else { + return; + }; + + if &accept_keystroke.modifiers == modifiers && accept_keystroke.modifiers.modified() { + if matches!( + self.edit_prediction_preview, + EditPredictionPreview::Inactive { .. } + ) { + self.edit_prediction_preview = EditPredictionPreview::Active { + previous_scroll_position: None, + since: Instant::now(), + }; + + self.update_visible_inline_completion(window, cx); + cx.notify(); + } + } else if let EditPredictionPreview::Active { + previous_scroll_position, + since, + } = self.edit_prediction_preview + { + if let (Some(previous_scroll_position), Some(position_map)) = + (previous_scroll_position, self.last_position_map.as_ref()) + { + self.set_scroll_position( + previous_scroll_position + .scroll_position(&position_map.snapshot.display_snapshot), + window, + cx, + ); + } + + self.edit_prediction_preview = EditPredictionPreview::Inactive { + released_too_fast: since.elapsed() < Duration::from_millis(200), + }; + self.clear_row_highlights::(); + self.update_visible_inline_completion(window, cx); + cx.notify(); + } + } + + fn update_visible_inline_completion( + &mut self, + _window: &mut Window, + cx: &mut Context, + ) -> Option<()> { + let selection = self.selections.newest_anchor(); + let cursor = selection.head(); + let multibuffer = self.buffer.read(cx).snapshot(cx); + let offset_selection = selection.map(|endpoint| endpoint.to_offset(&multibuffer)); + let excerpt_id = cursor.excerpt_id; + + let show_in_menu = self.show_edit_predictions_in_menu(); + let completions_menu_has_precedence = !show_in_menu + && (self.context_menu.borrow().is_some() + || (!self.completion_tasks.is_empty() && !self.has_active_inline_completion())); + + if completions_menu_has_precedence + || !offset_selection.is_empty() + || self + .active_inline_completion + .as_ref() + .map_or(false, |completion| { + let invalidation_range = completion.invalidation_range.to_offset(&multibuffer); + let invalidation_range = invalidation_range.start..=invalidation_range.end; + !invalidation_range.contains(&offset_selection.head()) + }) + { + self.discard_inline_completion(false, cx); + return None; + } + + self.take_active_inline_completion(cx); + let Some(provider) = self.edit_prediction_provider() else { + self.edit_prediction_settings = EditPredictionSettings::Disabled; + return None; + }; + + let (buffer, cursor_buffer_position) = + self.buffer.read(cx).text_anchor_for_position(cursor, cx)?; + + self.edit_prediction_settings = + self.edit_prediction_settings_at_position(&buffer, cursor_buffer_position, cx); + + self.edit_prediction_indent_conflict = multibuffer.is_line_whitespace_upto(cursor); + + if self.edit_prediction_indent_conflict { + let cursor_point = cursor.to_point(&multibuffer); + + let indents = multibuffer.suggested_indents(cursor_point.row..cursor_point.row + 1, cx); + + if let Some((_, indent)) = indents.iter().next() { + if indent.len == cursor_point.column { + self.edit_prediction_indent_conflict = false; + } + } + } + + let inline_completion = provider.suggest(&buffer, cursor_buffer_position, cx)?; + let edits = inline_completion + .edits + .into_iter() + .flat_map(|(range, new_text)| { + let start = multibuffer.anchor_in_excerpt(excerpt_id, range.start)?; + let end = multibuffer.anchor_in_excerpt(excerpt_id, range.end)?; + Some((start..end, new_text)) + }) + .collect::>(); + if edits.is_empty() { + return None; + } + + let first_edit_start = edits.first().unwrap().0.start; + let first_edit_start_point = first_edit_start.to_point(&multibuffer); + let edit_start_row = first_edit_start_point.row.saturating_sub(2); + + let last_edit_end = edits.last().unwrap().0.end; + let last_edit_end_point = last_edit_end.to_point(&multibuffer); + let edit_end_row = cmp::min(multibuffer.max_point().row, last_edit_end_point.row + 2); + + let cursor_row = cursor.to_point(&multibuffer).row; + + let snapshot = multibuffer.buffer_for_excerpt(excerpt_id).cloned()?; + + let mut inlay_ids = Vec::new(); + let invalidation_row_range; + let move_invalidation_row_range = if cursor_row < edit_start_row { + Some(cursor_row..edit_end_row) + } else if cursor_row > edit_end_row { + Some(edit_start_row..cursor_row) + } else { + None + }; + let is_move = + move_invalidation_row_range.is_some() || self.inline_completions_hidden_for_vim_mode; + let completion = if is_move { + invalidation_row_range = + move_invalidation_row_range.unwrap_or(edit_start_row..edit_end_row); + let target = first_edit_start; + InlineCompletion::Move { target, snapshot } + } else { + let show_completions_in_buffer = !self.edit_prediction_visible_in_cursor_popover(true) + && !self.inline_completions_hidden_for_vim_mode; + + if show_completions_in_buffer { + if edits + .iter() + .all(|(range, _)| range.to_offset(&multibuffer).is_empty()) + { + let mut inlays = Vec::new(); + for (range, new_text) in &edits { + let inlay = Inlay::inline_completion( + post_inc(&mut self.next_inlay_id), + range.start, + new_text.as_str(), + ); + inlay_ids.push(inlay.id); + inlays.push(inlay); + } + + self.splice_inlays(&[], inlays, cx); + } else { + let background_color = cx.theme().status().deleted_background; + self.highlight_text::( + edits.iter().map(|(range, _)| range.clone()).collect(), + HighlightStyle { + background_color: Some(background_color), + ..Default::default() + }, + cx, + ); + } + } + + invalidation_row_range = edit_start_row..edit_end_row; + + let display_mode = if all_edits_insertions_or_deletions(&edits, &multibuffer) { + if provider.show_tab_accept_marker() { + EditDisplayMode::TabAccept + } else { + EditDisplayMode::Inline + } + } else { + EditDisplayMode::DiffPopover + }; + + InlineCompletion::Edit { + edits, + edit_preview: inline_completion.edit_preview, + display_mode, + snapshot, + } + }; + + let invalidation_range = multibuffer + .anchor_before(Point::new(invalidation_row_range.start, 0)) + ..multibuffer.anchor_after(Point::new( + invalidation_row_range.end, + multibuffer.line_len(MultiBufferRow(invalidation_row_range.end)), + )); + + self.stale_inline_completion_in_menu = None; + self.active_inline_completion = Some(InlineCompletionState { + inlay_ids, + completion, + completion_id: inline_completion.id, + invalidation_range, + }); + + cx.notify(); + + Some(()) + } + + pub fn edit_prediction_provider(&self) -> Option> { + Some(self.edit_prediction_provider.as_ref()?.provider.clone()) + } + + fn render_code_actions_indicator( + &self, + _style: &EditorStyle, + row: DisplayRow, + is_active: bool, + breakpoint: Option<&(Anchor, Breakpoint)>, + cx: &mut Context, + ) -> Option { + let color = Color::Muted; + let position = breakpoint.as_ref().map(|(anchor, _)| *anchor); + let show_tooltip = !self.context_menu_visible(); + + if self.available_code_actions.is_some() { + Some( + IconButton::new("code_actions_indicator", ui::IconName::Bolt) + .shape(ui::IconButtonShape::Square) + .icon_size(IconSize::XSmall) + .icon_color(color) + .toggle_state(is_active) + .when(show_tooltip, |this| { + this.tooltip({ + let focus_handle = self.focus_handle.clone(); + move |window, cx| { + Tooltip::for_action_in( + "Toggle Code Actions", + &ToggleCodeActions { + deployed_from_indicator: None, + quick_launch: false, + }, + &focus_handle, + window, + cx, + ) + } + }) + }) + .on_click(cx.listener(move |editor, e: &ClickEvent, window, cx| { + let quick_launch = e.down.button == MouseButton::Left; + window.focus(&editor.focus_handle(cx)); + editor.toggle_code_actions( + &ToggleCodeActions { + deployed_from_indicator: Some(row), + quick_launch, + }, + window, + cx, + ); + })) + .on_right_click(cx.listener(move |editor, event: &ClickEvent, window, cx| { + editor.set_breakpoint_context_menu( + row, + position, + event.down.position, + window, + cx, + ); + })), + ) + } else { + None + } + } + + fn clear_tasks(&mut self) { + self.tasks.clear() + } + + fn insert_tasks(&mut self, key: (BufferId, BufferRow), value: RunnableTasks) { + if self.tasks.insert(key, value).is_some() { + // This case should hopefully be rare, but just in case... + log::error!( + "multiple different run targets found on a single line, only the last target will be rendered" + ) + } + } + + /// Get all display points of breakpoints that will be rendered within editor + /// + /// This function is used to handle overlaps between breakpoints and Code action/runner symbol. + /// It's also used to set the color of line numbers with breakpoints to the breakpoint color. + /// TODO debugger: Use this function to color toggle symbols that house nested breakpoints + fn active_breakpoints( + &self, + range: Range, + window: &mut Window, + cx: &mut Context, + ) -> HashMap { + let mut breakpoint_display_points = HashMap::default(); + + let Some(breakpoint_store) = self.breakpoint_store.clone() else { + return breakpoint_display_points; + }; + + let snapshot = self.snapshot(window, cx); + + let multi_buffer_snapshot = &snapshot.display_snapshot.buffer_snapshot; + let Some(project) = self.project.as_ref() else { + return breakpoint_display_points; + }; + + let range = snapshot.display_point_to_point(DisplayPoint::new(range.start, 0), Bias::Left) + ..snapshot.display_point_to_point(DisplayPoint::new(range.end, 0), Bias::Right); + + for (buffer_snapshot, range, excerpt_id) in + multi_buffer_snapshot.range_to_buffer_ranges(range) + { + let Some(buffer) = project.read_with(cx, |this, cx| { + this.buffer_for_id(buffer_snapshot.remote_id(), cx) + }) else { + continue; + }; + let breakpoints = breakpoint_store.read(cx).breakpoints( + &buffer, + Some( + buffer_snapshot.anchor_before(range.start) + ..buffer_snapshot.anchor_after(range.end), + ), + buffer_snapshot, + cx, + ); + for (anchor, breakpoint) in breakpoints { + let multi_buffer_anchor = + Anchor::in_buffer(excerpt_id, buffer_snapshot.remote_id(), *anchor); + let position = multi_buffer_anchor + .to_point(&multi_buffer_snapshot) + .to_display_point(&snapshot); + + breakpoint_display_points + .insert(position.row(), (multi_buffer_anchor, breakpoint.clone())); + } + } + + breakpoint_display_points + } + + fn breakpoint_context_menu( + &self, + anchor: Anchor, + window: &mut Window, + cx: &mut Context, + ) -> Entity { + let weak_editor = cx.weak_entity(); + let focus_handle = self.focus_handle(cx); + + let row = self + .buffer + .read(cx) + .snapshot(cx) + .summary_for_anchor::(&anchor) + .row; + + let breakpoint = self + .breakpoint_at_row(row, window, cx) + .map(|(anchor, bp)| (anchor, Arc::from(bp))); + + let log_breakpoint_msg = if breakpoint.as_ref().is_some_and(|bp| bp.1.message.is_some()) { + "Edit Log Breakpoint" + } else { + "Set Log Breakpoint" + }; + + let condition_breakpoint_msg = if breakpoint + .as_ref() + .is_some_and(|bp| bp.1.condition.is_some()) + { + "Edit Condition Breakpoint" + } else { + "Set Condition Breakpoint" + }; + + let hit_condition_breakpoint_msg = if breakpoint + .as_ref() + .is_some_and(|bp| bp.1.hit_condition.is_some()) + { + "Edit Hit Condition Breakpoint" + } else { + "Set Hit Condition Breakpoint" + }; + + let set_breakpoint_msg = if breakpoint.as_ref().is_some() { + "Unset Breakpoint" + } else { + "Set Breakpoint" + }; + + let run_to_cursor = command_palette_hooks::CommandPaletteFilter::try_global(cx) + .map_or(false, |filter| !filter.is_hidden(&DebuggerRunToCursor)); + + let toggle_state_msg = breakpoint.as_ref().map_or(None, |bp| match bp.1.state { + BreakpointState::Enabled => Some("Disable"), + BreakpointState::Disabled => Some("Enable"), + }); + + let (anchor, breakpoint) = + breakpoint.unwrap_or_else(|| (anchor, Arc::new(Breakpoint::new_standard()))); + + ui::ContextMenu::build(window, cx, |menu, _, _cx| { + menu.on_blur_subscription(Subscription::new(|| {})) + .context(focus_handle) + .when(run_to_cursor, |this| { + let weak_editor = weak_editor.clone(); + this.entry("Run to cursor", None, move |window, cx| { + weak_editor + .update(cx, |editor, cx| { + editor.change_selections(None, window, cx, |s| { + s.select_ranges([Point::new(row, 0)..Point::new(row, 0)]) + }); + }) + .ok(); + + window.dispatch_action(Box::new(DebuggerRunToCursor), cx); + }) + .separator() + }) + .when_some(toggle_state_msg, |this, msg| { + this.entry(msg, None, { + let weak_editor = weak_editor.clone(); + let breakpoint = breakpoint.clone(); + move |_window, cx| { + weak_editor + .update(cx, |this, cx| { + this.edit_breakpoint_at_anchor( + anchor, + breakpoint.as_ref().clone(), + BreakpointEditAction::InvertState, + cx, + ); + }) + .log_err(); + } + }) + }) + .entry(set_breakpoint_msg, None, { + let weak_editor = weak_editor.clone(); + let breakpoint = breakpoint.clone(); + move |_window, cx| { + weak_editor + .update(cx, |this, cx| { + this.edit_breakpoint_at_anchor( + anchor, + breakpoint.as_ref().clone(), + BreakpointEditAction::Toggle, + cx, + ); + }) + .log_err(); + } + }) + .entry(log_breakpoint_msg, None, { + let breakpoint = breakpoint.clone(); + let weak_editor = weak_editor.clone(); + move |window, cx| { + weak_editor + .update(cx, |this, cx| { + this.add_edit_breakpoint_block( + anchor, + breakpoint.as_ref(), + BreakpointPromptEditAction::Log, + window, + cx, + ); + }) + .log_err(); + } + }) + .entry(condition_breakpoint_msg, None, { + let breakpoint = breakpoint.clone(); + let weak_editor = weak_editor.clone(); + move |window, cx| { + weak_editor + .update(cx, |this, cx| { + this.add_edit_breakpoint_block( + anchor, + breakpoint.as_ref(), + BreakpointPromptEditAction::Condition, + window, + cx, + ); + }) + .log_err(); + } + }) + .entry(hit_condition_breakpoint_msg, None, move |window, cx| { + weak_editor + .update(cx, |this, cx| { + this.add_edit_breakpoint_block( + anchor, + breakpoint.as_ref(), + BreakpointPromptEditAction::HitCondition, + window, + cx, + ); + }) + .log_err(); + }) + }) + } + + fn render_breakpoint( + &self, + position: Anchor, + row: DisplayRow, + breakpoint: &Breakpoint, + cx: &mut Context, + ) -> IconButton { + // Is it a breakpoint that shows up when hovering over gutter? + let (is_phantom, collides_with_existing) = self.gutter_breakpoint_indicator.0.map_or( + (false, false), + |PhantomBreakpointIndicator { + is_active, + display_row, + collides_with_existing_breakpoint, + }| { + ( + is_active && display_row == row, + collides_with_existing_breakpoint, + ) + }, + ); + + let (color, icon) = { + let icon = match (&breakpoint.message.is_some(), breakpoint.is_disabled()) { + (false, false) => ui::IconName::DebugBreakpoint, + (true, false) => ui::IconName::DebugLogBreakpoint, + (false, true) => ui::IconName::DebugDisabledBreakpoint, + (true, true) => ui::IconName::DebugDisabledLogBreakpoint, + }; + + let color = if is_phantom { + Color::Hint + } else { + Color::Debugger + }; + + (color, icon) + }; + + let breakpoint = Arc::from(breakpoint.clone()); + + let alt_as_text = gpui::Keystroke { + modifiers: Modifiers::secondary_key(), + ..Default::default() + }; + let primary_action_text = if breakpoint.is_disabled() { + "enable" + } else if is_phantom && !collides_with_existing { + "set" + } else { + "unset" + }; + let mut primary_text = format!("Click to {primary_action_text}"); + if collides_with_existing && !breakpoint.is_disabled() { + use std::fmt::Write; + write!(primary_text, ", {alt_as_text}-click to disable").ok(); + } + let primary_text = SharedString::from(primary_text); + let focus_handle = self.focus_handle.clone(); + IconButton::new(("breakpoint_indicator", row.0 as usize), icon) + .icon_size(IconSize::XSmall) + .size(ui::ButtonSize::None) + .icon_color(color) + .style(ButtonStyle::Transparent) + .on_click(cx.listener({ + let breakpoint = breakpoint.clone(); + + move |editor, event: &ClickEvent, window, cx| { + let edit_action = if event.modifiers().platform || breakpoint.is_disabled() { + BreakpointEditAction::InvertState + } else { + BreakpointEditAction::Toggle + }; + + window.focus(&editor.focus_handle(cx)); + editor.edit_breakpoint_at_anchor( + position, + breakpoint.as_ref().clone(), + edit_action, + cx, + ); + } + })) + .on_right_click(cx.listener(move |editor, event: &ClickEvent, window, cx| { + editor.set_breakpoint_context_menu( + row, + Some(position), + event.down.position, + window, + cx, + ); + })) + .tooltip(move |window, cx| { + Tooltip::with_meta_in( + primary_text.clone(), + None, + "Right-click for more options", + &focus_handle, + window, + cx, + ) + }) + } + + fn build_tasks_context( + project: &Entity, + buffer: &Entity, + buffer_row: u32, + tasks: &Arc, + cx: &mut Context, + ) -> Task> { + let position = Point::new(buffer_row, tasks.column); + let range_start = buffer.read(cx).anchor_at(position, Bias::Right); + let location = Location { + buffer: buffer.clone(), + range: range_start..range_start, + }; + // Fill in the environmental variables from the tree-sitter captures + let mut captured_task_variables = TaskVariables::default(); + for (capture_name, value) in tasks.extra_variables.clone() { + captured_task_variables.insert( + task::VariableName::Custom(capture_name.into()), + value.clone(), + ); + } + project.update(cx, |project, cx| { + project.task_store().update(cx, |task_store, cx| { + task_store.task_context_for_location(captured_task_variables, location, cx) + }) + }) + } + + pub fn spawn_nearest_task( + &mut self, + action: &SpawnNearestTask, + window: &mut Window, + cx: &mut Context, + ) { + let Some((workspace, _)) = self.workspace.clone() else { + return; + }; + let Some(project) = self.project.clone() else { + return; + }; + + // Try to find a closest, enclosing node using tree-sitter that has a + // task + let Some((buffer, buffer_row, tasks)) = self + .find_enclosing_node_task(cx) + // Or find the task that's closest in row-distance. + .or_else(|| self.find_closest_task(cx)) + else { + return; + }; + + let reveal_strategy = action.reveal; + let task_context = Self::build_tasks_context(&project, &buffer, buffer_row, &tasks, cx); + cx.spawn_in(window, async move |_, cx| { + let context = task_context.await?; + let (task_source_kind, mut resolved_task) = tasks.resolve(&context).next()?; + + let resolved = &mut resolved_task.resolved; + resolved.reveal = reveal_strategy; + + workspace + .update_in(cx, |workspace, window, cx| { + workspace.schedule_resolved_task( + task_source_kind, + resolved_task, + false, + window, + cx, + ); + }) + .ok() + }) + .detach(); + } + + fn find_closest_task( + &mut self, + cx: &mut Context, + ) -> Option<(Entity, u32, Arc)> { + let cursor_row = self.selections.newest_adjusted(cx).head().row; + + let ((buffer_id, row), tasks) = self + .tasks + .iter() + .min_by_key(|((_, row), _)| cursor_row.abs_diff(*row))?; + + let buffer = self.buffer.read(cx).buffer(*buffer_id)?; + let tasks = Arc::new(tasks.to_owned()); + Some((buffer, *row, tasks)) + } + + fn find_enclosing_node_task( + &mut self, + cx: &mut Context, + ) -> Option<(Entity, u32, Arc)> { + let snapshot = self.buffer.read(cx).snapshot(cx); + let offset = self.selections.newest::(cx).head(); + let excerpt = snapshot.excerpt_containing(offset..offset)?; + let buffer_id = excerpt.buffer().remote_id(); + + let layer = excerpt.buffer().syntax_layer_at(offset)?; + let mut cursor = layer.node().walk(); + + while cursor.goto_first_child_for_byte(offset).is_some() { + if cursor.node().end_byte() == offset { + cursor.goto_next_sibling(); + } + } + + // Ascend to the smallest ancestor that contains the range and has a task. + loop { + let node = cursor.node(); + let node_range = node.byte_range(); + let symbol_start_row = excerpt.buffer().offset_to_point(node.start_byte()).row; + + // Check if this node contains our offset + if node_range.start <= offset && node_range.end >= offset { + // If it contains offset, check for task + if let Some(tasks) = self.tasks.get(&(buffer_id, symbol_start_row)) { + let buffer = self.buffer.read(cx).buffer(buffer_id)?; + return Some((buffer, symbol_start_row, Arc::new(tasks.to_owned()))); + } + } + + if !cursor.goto_parent() { + break; + } + } + None + } + + fn render_run_indicator( + &self, + _style: &EditorStyle, + is_active: bool, + row: DisplayRow, + breakpoint: Option<(Anchor, Breakpoint)>, + cx: &mut Context, + ) -> IconButton { + let color = Color::Muted; + let position = breakpoint.as_ref().map(|(anchor, _)| *anchor); + + IconButton::new(("run_indicator", row.0 as usize), ui::IconName::Play) + .shape(ui::IconButtonShape::Square) + .icon_size(IconSize::XSmall) + .icon_color(color) + .toggle_state(is_active) + .on_click(cx.listener(move |editor, e: &ClickEvent, window, cx| { + let quick_launch = e.down.button == MouseButton::Left; + window.focus(&editor.focus_handle(cx)); + editor.toggle_code_actions( + &ToggleCodeActions { + deployed_from_indicator: Some(row), + quick_launch, + }, + window, + cx, + ); + })) + .on_right_click(cx.listener(move |editor, event: &ClickEvent, window, cx| { + editor.set_breakpoint_context_menu(row, position, event.down.position, window, cx); + })) + } + + pub fn context_menu_visible(&self) -> bool { + !self.edit_prediction_preview_is_active() + && self + .context_menu + .borrow() + .as_ref() + .map_or(false, |menu| menu.visible()) + } + + fn context_menu_origin(&self) -> Option { + self.context_menu + .borrow() + .as_ref() + .map(|menu| menu.origin()) + } + + pub fn set_context_menu_options(&mut self, options: ContextMenuOptions) { + self.context_menu_options = Some(options); + } + + const EDIT_PREDICTION_POPOVER_PADDING_X: Pixels = Pixels(24.); + const EDIT_PREDICTION_POPOVER_PADDING_Y: Pixels = Pixels(2.); + + fn render_edit_prediction_popover( + &mut self, + text_bounds: &Bounds, + content_origin: gpui::Point, + editor_snapshot: &EditorSnapshot, + visible_row_range: Range, + scroll_top: f32, + scroll_bottom: f32, + line_layouts: &[LineWithInvisibles], + line_height: Pixels, + scroll_pixel_position: gpui::Point, + newest_selection_head: Option, + editor_width: Pixels, + style: &EditorStyle, + window: &mut Window, + cx: &mut App, + ) -> Option<(AnyElement, gpui::Point)> { + let active_inline_completion = self.active_inline_completion.as_ref()?; + + if self.edit_prediction_visible_in_cursor_popover(true) { + return None; + } + + match &active_inline_completion.completion { + InlineCompletion::Move { target, .. } => { + let target_display_point = target.to_display_point(editor_snapshot); + + if self.edit_prediction_requires_modifier() { + if !self.edit_prediction_preview_is_active() { + return None; + } + + self.render_edit_prediction_modifier_jump_popover( + text_bounds, + content_origin, + visible_row_range, + line_layouts, + line_height, + scroll_pixel_position, + newest_selection_head, + target_display_point, + window, + cx, + ) + } else { + self.render_edit_prediction_eager_jump_popover( + text_bounds, + content_origin, + editor_snapshot, + visible_row_range, + scroll_top, + scroll_bottom, + line_height, + scroll_pixel_position, + target_display_point, + editor_width, + window, + cx, + ) + } + } + InlineCompletion::Edit { + display_mode: EditDisplayMode::Inline, + .. + } => None, + InlineCompletion::Edit { + display_mode: EditDisplayMode::TabAccept, + edits, + .. + } => { + let range = &edits.first()?.0; + let target_display_point = range.end.to_display_point(editor_snapshot); + + self.render_edit_prediction_end_of_line_popover( + "Accept", + editor_snapshot, + visible_row_range, + target_display_point, + line_height, + scroll_pixel_position, + content_origin, + editor_width, + window, + cx, + ) + } + InlineCompletion::Edit { + edits, + edit_preview, + display_mode: EditDisplayMode::DiffPopover, + snapshot, + } => self.render_edit_prediction_diff_popover( + text_bounds, + content_origin, + editor_snapshot, + visible_row_range, + line_layouts, + line_height, + scroll_pixel_position, + newest_selection_head, + editor_width, + style, + edits, + edit_preview, + snapshot, + window, + cx, + ), + } + } + + fn render_edit_prediction_modifier_jump_popover( + &mut self, + text_bounds: &Bounds, + content_origin: gpui::Point, + visible_row_range: Range, + line_layouts: &[LineWithInvisibles], + line_height: Pixels, + scroll_pixel_position: gpui::Point, + newest_selection_head: Option, + target_display_point: DisplayPoint, + window: &mut Window, + cx: &mut App, + ) -> Option<(AnyElement, gpui::Point)> { + let scrolled_content_origin = + content_origin - gpui::Point::new(scroll_pixel_position.x, Pixels(0.0)); + + const SCROLL_PADDING_Y: Pixels = px(12.); + + if target_display_point.row() < visible_row_range.start { + return self.render_edit_prediction_scroll_popover( + |_| SCROLL_PADDING_Y, + IconName::ArrowUp, + visible_row_range, + line_layouts, + newest_selection_head, + scrolled_content_origin, + window, + cx, + ); + } else if target_display_point.row() >= visible_row_range.end { + return self.render_edit_prediction_scroll_popover( + |size| text_bounds.size.height - size.height - SCROLL_PADDING_Y, + IconName::ArrowDown, + visible_row_range, + line_layouts, + newest_selection_head, + scrolled_content_origin, + window, + cx, + ); + } + + const POLE_WIDTH: Pixels = px(2.); + + let line_layout = + line_layouts.get(target_display_point.row().minus(visible_row_range.start) as usize)?; + let target_column = target_display_point.column() as usize; + + let target_x = line_layout.x_for_index(target_column); + let target_y = + (target_display_point.row().as_f32() * line_height) - scroll_pixel_position.y; + + let flag_on_right = target_x < text_bounds.size.width / 2.; + + let mut border_color = Self::edit_prediction_callout_popover_border_color(cx); + border_color.l += 0.001; + + let mut element = v_flex() + .items_end() + .when(flag_on_right, |el| el.items_start()) + .child(if flag_on_right { + self.render_edit_prediction_line_popover("Jump", None, window, cx)? + .rounded_bl(px(0.)) + .rounded_tl(px(0.)) + .border_l_2() + .border_color(border_color) + } else { + self.render_edit_prediction_line_popover("Jump", None, window, cx)? + .rounded_br(px(0.)) + .rounded_tr(px(0.)) + .border_r_2() + .border_color(border_color) + }) + .child(div().w(POLE_WIDTH).bg(border_color).h(line_height)) + .into_any(); + + let size = element.layout_as_root(AvailableSpace::min_size(), window, cx); + + let mut origin = scrolled_content_origin + point(target_x, target_y) + - point( + if flag_on_right { + POLE_WIDTH + } else { + size.width - POLE_WIDTH + }, + size.height - line_height, + ); + + origin.x = origin.x.max(content_origin.x); + + element.prepaint_at(origin, window, cx); + + Some((element, origin)) + } + + fn render_edit_prediction_scroll_popover( + &mut self, + to_y: impl Fn(Size) -> Pixels, + scroll_icon: IconName, + visible_row_range: Range, + line_layouts: &[LineWithInvisibles], + newest_selection_head: Option, + scrolled_content_origin: gpui::Point, + window: &mut Window, + cx: &mut App, + ) -> Option<(AnyElement, gpui::Point)> { + let mut element = self + .render_edit_prediction_line_popover("Scroll", Some(scroll_icon), window, cx)? + .into_any(); + + let size = element.layout_as_root(AvailableSpace::min_size(), window, cx); + + let cursor = newest_selection_head?; + let cursor_row_layout = + line_layouts.get(cursor.row().minus(visible_row_range.start) as usize)?; + let cursor_column = cursor.column() as usize; + + let cursor_character_x = cursor_row_layout.x_for_index(cursor_column); + + let origin = scrolled_content_origin + point(cursor_character_x, to_y(size)); + + element.prepaint_at(origin, window, cx); + Some((element, origin)) + } + + fn render_edit_prediction_eager_jump_popover( + &mut self, + text_bounds: &Bounds, + content_origin: gpui::Point, + editor_snapshot: &EditorSnapshot, + visible_row_range: Range, + scroll_top: f32, + scroll_bottom: f32, + line_height: Pixels, + scroll_pixel_position: gpui::Point, + target_display_point: DisplayPoint, + editor_width: Pixels, + window: &mut Window, + cx: &mut App, + ) -> Option<(AnyElement, gpui::Point)> { + if target_display_point.row().as_f32() < scroll_top { + let mut element = self + .render_edit_prediction_line_popover( + "Jump to Edit", + Some(IconName::ArrowUp), + window, + cx, + )? + .into_any(); + + let size = element.layout_as_root(AvailableSpace::min_size(), window, cx); + let offset = point( + (text_bounds.size.width - size.width) / 2., + Self::EDIT_PREDICTION_POPOVER_PADDING_Y, + ); + + let origin = text_bounds.origin + offset; + element.prepaint_at(origin, window, cx); + Some((element, origin)) + } else if (target_display_point.row().as_f32() + 1.) > scroll_bottom { + let mut element = self + .render_edit_prediction_line_popover( + "Jump to Edit", + Some(IconName::ArrowDown), + window, + cx, + )? + .into_any(); + + let size = element.layout_as_root(AvailableSpace::min_size(), window, cx); + let offset = point( + (text_bounds.size.width - size.width) / 2., + text_bounds.size.height - size.height - Self::EDIT_PREDICTION_POPOVER_PADDING_Y, + ); + + let origin = text_bounds.origin + offset; + element.prepaint_at(origin, window, cx); + Some((element, origin)) + } else { + self.render_edit_prediction_end_of_line_popover( + "Jump to Edit", + editor_snapshot, + visible_row_range, + target_display_point, + line_height, + scroll_pixel_position, + content_origin, + editor_width, + window, + cx, + ) + } + } + + fn render_edit_prediction_end_of_line_popover( + self: &mut Editor, + label: &'static str, + editor_snapshot: &EditorSnapshot, + visible_row_range: Range, + target_display_point: DisplayPoint, + line_height: Pixels, + scroll_pixel_position: gpui::Point, + content_origin: gpui::Point, + editor_width: Pixels, + window: &mut Window, + cx: &mut App, + ) -> Option<(AnyElement, gpui::Point)> { + let target_line_end = DisplayPoint::new( + target_display_point.row(), + editor_snapshot.line_len(target_display_point.row()), + ); + + let mut element = self + .render_edit_prediction_line_popover(label, None, window, cx)? + .into_any(); + + let size = element.layout_as_root(AvailableSpace::min_size(), window, cx); + + let line_origin = self.display_to_pixel_point(target_line_end, editor_snapshot, window)?; + + let start_point = content_origin - point(scroll_pixel_position.x, Pixels::ZERO); + let mut origin = start_point + + line_origin + + point(Self::EDIT_PREDICTION_POPOVER_PADDING_X, Pixels::ZERO); + origin.x = origin.x.max(content_origin.x); + + let max_x = content_origin.x + editor_width - size.width; + + if origin.x > max_x { + let offset = line_height + Self::EDIT_PREDICTION_POPOVER_PADDING_Y; + + let icon = if visible_row_range.contains(&(target_display_point.row() + 2)) { + origin.y += offset; + IconName::ArrowUp + } else { + origin.y -= offset; + IconName::ArrowDown + }; + + element = self + .render_edit_prediction_line_popover(label, Some(icon), window, cx)? + .into_any(); + + let size = element.layout_as_root(AvailableSpace::min_size(), window, cx); + + origin.x = content_origin.x + editor_width - size.width - px(2.); + } + + element.prepaint_at(origin, window, cx); + Some((element, origin)) + } + + fn render_edit_prediction_diff_popover( + self: &Editor, + text_bounds: &Bounds, + content_origin: gpui::Point, + editor_snapshot: &EditorSnapshot, + visible_row_range: Range, + line_layouts: &[LineWithInvisibles], + line_height: Pixels, + scroll_pixel_position: gpui::Point, + newest_selection_head: Option, + editor_width: Pixels, + style: &EditorStyle, + edits: &Vec<(Range, String)>, + edit_preview: &Option, + snapshot: &language::BufferSnapshot, + window: &mut Window, + cx: &mut App, + ) -> Option<(AnyElement, gpui::Point)> { + let edit_start = edits + .first() + .unwrap() + .0 + .start + .to_display_point(editor_snapshot); + let edit_end = edits + .last() + .unwrap() + .0 + .end + .to_display_point(editor_snapshot); + + let is_visible = visible_row_range.contains(&edit_start.row()) + || visible_row_range.contains(&edit_end.row()); + if !is_visible { + return None; + } + + let highlighted_edits = + crate::inline_completion_edit_text(&snapshot, edits, edit_preview.as_ref()?, false, cx); + + let styled_text = highlighted_edits.to_styled_text(&style.text); + let line_count = highlighted_edits.text.lines().count(); + + const BORDER_WIDTH: Pixels = px(1.); + + let keybind = self.render_edit_prediction_accept_keybind(window, cx); + let has_keybind = keybind.is_some(); + + let mut element = h_flex() + .items_start() + .child( + h_flex() + .bg(cx.theme().colors().editor_background) + .border(BORDER_WIDTH) + .shadow_sm() + .border_color(cx.theme().colors().border) + .rounded_l_lg() + .when(line_count > 1, |el| el.rounded_br_lg()) + .pr_1() + .child(styled_text), + ) + .child( + h_flex() + .h(line_height + BORDER_WIDTH * 2.) + .px_1p5() + .gap_1() + // Workaround: For some reason, there's a gap if we don't do this + .ml(-BORDER_WIDTH) + .shadow(smallvec![gpui::BoxShadow { + color: gpui::black().opacity(0.05), + offset: point(px(1.), px(1.)), + blur_radius: px(2.), + spread_radius: px(0.), + }]) + .bg(Editor::edit_prediction_line_popover_bg_color(cx)) + .border(BORDER_WIDTH) + .border_color(cx.theme().colors().border) + .rounded_r_lg() + .id("edit_prediction_diff_popover_keybind") + .when(!has_keybind, |el| { + let status_colors = cx.theme().status(); + + el.bg(status_colors.error_background) + .border_color(status_colors.error.opacity(0.6)) + .child(Icon::new(IconName::Info).color(Color::Error)) + .cursor_default() + .hoverable_tooltip(move |_window, cx| { + cx.new(|_| MissingEditPredictionKeybindingTooltip).into() + }) + }) + .children(keybind), + ) + .into_any(); + + let longest_row = + editor_snapshot.longest_row_in_range(edit_start.row()..edit_end.row() + 1); + let longest_line_width = if visible_row_range.contains(&longest_row) { + line_layouts[(longest_row.0 - visible_row_range.start.0) as usize].width + } else { + layout_line( + longest_row, + editor_snapshot, + style, + editor_width, + |_| false, + window, + cx, + ) + .width + }; + + let viewport_bounds = + Bounds::new(Default::default(), window.viewport_size()).extend(Edges { + right: -EditorElement::SCROLLBAR_WIDTH, + ..Default::default() + }); + + let x_after_longest = + text_bounds.origin.x + longest_line_width + Self::EDIT_PREDICTION_POPOVER_PADDING_X + - scroll_pixel_position.x; + + let element_bounds = element.layout_as_root(AvailableSpace::min_size(), window, cx); + + // Fully visible if it can be displayed within the window (allow overlapping other + // panes). However, this is only allowed if the popover starts within text_bounds. + let can_position_to_the_right = x_after_longest < text_bounds.right() + && x_after_longest + element_bounds.width < viewport_bounds.right(); + + let mut origin = if can_position_to_the_right { + point( + x_after_longest, + text_bounds.origin.y + edit_start.row().as_f32() * line_height + - scroll_pixel_position.y, + ) + } else { + let cursor_row = newest_selection_head.map(|head| head.row()); + let above_edit = edit_start + .row() + .0 + .checked_sub(line_count as u32) + .map(DisplayRow); + let below_edit = Some(edit_end.row() + 1); + let above_cursor = + cursor_row.and_then(|row| row.0.checked_sub(line_count as u32).map(DisplayRow)); + let below_cursor = cursor_row.map(|cursor_row| cursor_row + 1); + + // Place the edit popover adjacent to the edit if there is a location + // available that is onscreen and does not obscure the cursor. Otherwise, + // place it adjacent to the cursor. + let row_target = [above_edit, below_edit, above_cursor, below_cursor] + .into_iter() + .flatten() + .find(|&start_row| { + let end_row = start_row + line_count as u32; + visible_row_range.contains(&start_row) + && visible_row_range.contains(&end_row) + && cursor_row.map_or(true, |cursor_row| { + !((start_row..end_row).contains(&cursor_row)) + }) + })?; + + content_origin + + point( + -scroll_pixel_position.x, + row_target.as_f32() * line_height - scroll_pixel_position.y, + ) + }; + + origin.x -= BORDER_WIDTH; + + window.defer_draw(element, origin, 1); + + // Do not return an element, since it will already be drawn due to defer_draw. + None + } + + fn edit_prediction_cursor_popover_height(&self) -> Pixels { + px(30.) + } + + fn current_user_player_color(&self, cx: &mut App) -> PlayerColor { + if self.read_only(cx) { + cx.theme().players().read_only() + } else { + self.style.as_ref().unwrap().local_player + } + } + + fn render_edit_prediction_accept_keybind( + &self, + window: &mut Window, + cx: &App, + ) -> Option { + let accept_binding = self.accept_edit_prediction_keybind(window, cx); + let accept_keystroke = accept_binding.keystroke()?; + + let is_platform_style_mac = PlatformStyle::platform() == PlatformStyle::Mac; + + let modifiers_color = if accept_keystroke.modifiers == window.modifiers() { + Color::Accent + } else { + Color::Muted + }; + + h_flex() + .px_0p5() + .when(is_platform_style_mac, |parent| parent.gap_0p5()) + .font(theme::ThemeSettings::get_global(cx).buffer_font.clone()) + .text_size(TextSize::XSmall.rems(cx)) + .child(h_flex().children(ui::render_modifiers( + &accept_keystroke.modifiers, + PlatformStyle::platform(), + Some(modifiers_color), + Some(IconSize::XSmall.rems().into()), + true, + ))) + .when(is_platform_style_mac, |parent| { + parent.child(accept_keystroke.key.clone()) + }) + .when(!is_platform_style_mac, |parent| { + parent.child( + Key::new( + util::capitalize(&accept_keystroke.key), + Some(Color::Default), + ) + .size(Some(IconSize::XSmall.rems().into())), + ) + }) + .into_any() + .into() + } + + fn render_edit_prediction_line_popover( + &self, + label: impl Into, + icon: Option, + window: &mut Window, + cx: &App, + ) -> Option> { + let padding_right = if icon.is_some() { px(4.) } else { px(8.) }; + + let keybind = self.render_edit_prediction_accept_keybind(window, cx); + let has_keybind = keybind.is_some(); + + let result = h_flex() + .id("ep-line-popover") + .py_0p5() + .pl_1() + .pr(padding_right) + .gap_1() + .rounded_md() + .border_1() + .bg(Self::edit_prediction_line_popover_bg_color(cx)) + .border_color(Self::edit_prediction_callout_popover_border_color(cx)) + .shadow_sm() + .when(!has_keybind, |el| { + let status_colors = cx.theme().status(); + + el.bg(status_colors.error_background) + .border_color(status_colors.error.opacity(0.6)) + .pl_2() + .child(Icon::new(IconName::ZedPredictError).color(Color::Error)) + .cursor_default() + .hoverable_tooltip(move |_window, cx| { + cx.new(|_| MissingEditPredictionKeybindingTooltip).into() + }) + }) + .children(keybind) + .child( + Label::new(label) + .size(LabelSize::Small) + .when(!has_keybind, |el| { + el.color(cx.theme().status().error.into()).strikethrough() + }), + ) + .when(!has_keybind, |el| { + el.child( + h_flex().ml_1().child( + Icon::new(IconName::Info) + .size(IconSize::Small) + .color(cx.theme().status().error.into()), + ), + ) + }) + .when_some(icon, |element, icon| { + element.child( + div() + .mt(px(1.5)) + .child(Icon::new(icon).size(IconSize::Small)), + ) + }); + + Some(result) + } + + fn edit_prediction_line_popover_bg_color(cx: &App) -> Hsla { + let accent_color = cx.theme().colors().text_accent; + let editor_bg_color = cx.theme().colors().editor_background; + editor_bg_color.blend(accent_color.opacity(0.1)) + } + + fn edit_prediction_callout_popover_border_color(cx: &App) -> Hsla { + let accent_color = cx.theme().colors().text_accent; + let editor_bg_color = cx.theme().colors().editor_background; + editor_bg_color.blend(accent_color.opacity(0.6)) + } + + fn render_edit_prediction_cursor_popover( + &self, + min_width: Pixels, + max_width: Pixels, + cursor_point: Point, + style: &EditorStyle, + accept_keystroke: Option<&gpui::Keystroke>, + _window: &Window, + cx: &mut Context, + ) -> Option { + let provider = self.edit_prediction_provider.as_ref()?; + + if provider.provider.needs_terms_acceptance(cx) { + return Some( + h_flex() + .min_w(min_width) + .flex_1() + .px_2() + .py_1() + .gap_3() + .elevation_2(cx) + .hover(|style| style.bg(cx.theme().colors().element_hover)) + .id("accept-terms") + .cursor_pointer() + .on_mouse_down(MouseButton::Left, |_, window, _| window.prevent_default()) + .on_click(cx.listener(|this, _event, window, cx| { + cx.stop_propagation(); + this.report_editor_event("Edit Prediction Provider ToS Clicked", None, cx); + window.dispatch_action( + zed_actions::OpenZedPredictOnboarding.boxed_clone(), + cx, + ); + })) + .child( + h_flex() + .flex_1() + .gap_2() + .child(Icon::new(IconName::ZedPredict)) + .child(Label::new("Accept Terms of Service")) + .child(div().w_full()) + .child( + Icon::new(IconName::ArrowUpRight) + .color(Color::Muted) + .size(IconSize::Small), + ) + .into_any_element(), + ) + .into_any(), + ); + } + + let is_refreshing = provider.provider.is_refreshing(cx); + + fn pending_completion_container() -> Div { + h_flex() + .h_full() + .flex_1() + .gap_2() + .child(Icon::new(IconName::ZedPredict)) + } + + let completion = match &self.active_inline_completion { + Some(prediction) => { + if !self.has_visible_completions_menu() { + const RADIUS: Pixels = px(6.); + const BORDER_WIDTH: Pixels = px(1.); + + return Some( + h_flex() + .elevation_2(cx) + .border(BORDER_WIDTH) + .border_color(cx.theme().colors().border) + .when(accept_keystroke.is_none(), |el| { + el.border_color(cx.theme().status().error) + }) + .rounded(RADIUS) + .rounded_tl(px(0.)) + .overflow_hidden() + .child(div().px_1p5().child(match &prediction.completion { + InlineCompletion::Move { target, snapshot } => { + use text::ToPoint as _; + if target.text_anchor.to_point(&snapshot).row > cursor_point.row + { + Icon::new(IconName::ZedPredictDown) + } else { + Icon::new(IconName::ZedPredictUp) + } + } + InlineCompletion::Edit { .. } => Icon::new(IconName::ZedPredict), + })) + .child( + h_flex() + .gap_1() + .py_1() + .px_2() + .rounded_r(RADIUS - BORDER_WIDTH) + .border_l_1() + .border_color(cx.theme().colors().border) + .bg(Self::edit_prediction_line_popover_bg_color(cx)) + .when(self.edit_prediction_preview.released_too_fast(), |el| { + el.child( + Label::new("Hold") + .size(LabelSize::Small) + .when(accept_keystroke.is_none(), |el| { + el.strikethrough() + }) + .line_height_style(LineHeightStyle::UiLabel), + ) + }) + .id("edit_prediction_cursor_popover_keybind") + .when(accept_keystroke.is_none(), |el| { + let status_colors = cx.theme().status(); + + el.bg(status_colors.error_background) + .border_color(status_colors.error.opacity(0.6)) + .child(Icon::new(IconName::Info).color(Color::Error)) + .cursor_default() + .hoverable_tooltip(move |_window, cx| { + cx.new(|_| MissingEditPredictionKeybindingTooltip) + .into() + }) + }) + .when_some( + accept_keystroke.as_ref(), + |el, accept_keystroke| { + el.child(h_flex().children(ui::render_modifiers( + &accept_keystroke.modifiers, + PlatformStyle::platform(), + Some(Color::Default), + Some(IconSize::XSmall.rems().into()), + false, + ))) + }, + ), + ) + .into_any(), + ); + } + + self.render_edit_prediction_cursor_popover_preview( + prediction, + cursor_point, + style, + cx, + )? + } + + None if is_refreshing => match &self.stale_inline_completion_in_menu { + Some(stale_completion) => self.render_edit_prediction_cursor_popover_preview( + stale_completion, + cursor_point, + style, + cx, + )?, + + None => { + pending_completion_container().child(Label::new("...").size(LabelSize::Small)) + } + }, + + None => pending_completion_container().child(Label::new("No Prediction")), + }; + + let completion = if is_refreshing { + completion + .with_animation( + "loading-completion", + Animation::new(Duration::from_secs(2)) + .repeat() + .with_easing(pulsating_between(0.4, 0.8)), + |label, delta| label.opacity(delta), + ) + .into_any_element() + } else { + completion.into_any_element() + }; + + let has_completion = self.active_inline_completion.is_some(); + + let is_platform_style_mac = PlatformStyle::platform() == PlatformStyle::Mac; + Some( + h_flex() + .min_w(min_width) + .max_w(max_width) + .flex_1() + .elevation_2(cx) + .border_color(cx.theme().colors().border) + .child( + div() + .flex_1() + .py_1() + .px_2() + .overflow_hidden() + .child(completion), + ) + .when_some(accept_keystroke, |el, accept_keystroke| { + if !accept_keystroke.modifiers.modified() { + return el; + } + + el.child( + h_flex() + .h_full() + .border_l_1() + .rounded_r_lg() + .border_color(cx.theme().colors().border) + .bg(Self::edit_prediction_line_popover_bg_color(cx)) + .gap_1() + .py_1() + .px_2() + .child( + h_flex() + .font(theme::ThemeSettings::get_global(cx).buffer_font.clone()) + .when(is_platform_style_mac, |parent| parent.gap_1()) + .child(h_flex().children(ui::render_modifiers( + &accept_keystroke.modifiers, + PlatformStyle::platform(), + Some(if !has_completion { + Color::Muted + } else { + Color::Default + }), + None, + false, + ))), + ) + .child(Label::new("Preview").into_any_element()) + .opacity(if has_completion { 1.0 } else { 0.4 }), + ) + }) + .into_any(), + ) + } + + fn render_edit_prediction_cursor_popover_preview( + &self, + completion: &InlineCompletionState, + cursor_point: Point, + style: &EditorStyle, + cx: &mut Context, + ) -> Option
{ + use text::ToPoint as _; + + fn render_relative_row_jump( + prefix: impl Into, + current_row: u32, + target_row: u32, + ) -> Div { + let (row_diff, arrow) = if target_row < current_row { + (current_row - target_row, IconName::ArrowUp) + } else { + (target_row - current_row, IconName::ArrowDown) + }; + + h_flex() + .child( + Label::new(format!("{}{}", prefix.into(), row_diff)) + .color(Color::Muted) + .size(LabelSize::Small), + ) + .child(Icon::new(arrow).color(Color::Muted).size(IconSize::Small)) + } + + match &completion.completion { + InlineCompletion::Move { + target, snapshot, .. + } => Some( + h_flex() + .px_2() + .gap_2() + .flex_1() + .child( + if target.text_anchor.to_point(&snapshot).row > cursor_point.row { + Icon::new(IconName::ZedPredictDown) + } else { + Icon::new(IconName::ZedPredictUp) + }, + ) + .child(Label::new("Jump to Edit")), + ), + + InlineCompletion::Edit { + edits, + edit_preview, + snapshot, + display_mode: _, + } => { + let first_edit_row = edits.first()?.0.start.text_anchor.to_point(&snapshot).row; + + let (highlighted_edits, has_more_lines) = crate::inline_completion_edit_text( + &snapshot, + &edits, + edit_preview.as_ref()?, + true, + cx, + ) + .first_line_preview(); + + let styled_text = gpui::StyledText::new(highlighted_edits.text) + .with_default_highlights(&style.text, highlighted_edits.highlights); + + let preview = h_flex() + .gap_1() + .min_w_16() + .child(styled_text) + .when(has_more_lines, |parent| parent.child("…")); + + let left = if first_edit_row != cursor_point.row { + render_relative_row_jump("", cursor_point.row, first_edit_row) + .into_any_element() + } else { + Icon::new(IconName::ZedPredict).into_any_element() + }; + + Some( + h_flex() + .h_full() + .flex_1() + .gap_2() + .pr_1() + .overflow_x_hidden() + .font(theme::ThemeSettings::get_global(cx).buffer_font.clone()) + .child(left) + .child(preview), + ) + } + } + } + + fn render_context_menu( + &self, + style: &EditorStyle, + max_height_in_lines: u32, + window: &mut Window, + cx: &mut Context, + ) -> Option { + let menu = self.context_menu.borrow(); + let menu = menu.as_ref()?; + if !menu.visible() { + return None; + }; + Some(menu.render(style, max_height_in_lines, window, cx)) + } + + fn render_context_menu_aside( + &mut self, + max_size: Size, + window: &mut Window, + cx: &mut Context, + ) -> Option { + self.context_menu.borrow_mut().as_mut().and_then(|menu| { + if menu.visible() { + menu.render_aside(self, max_size, window, cx) + } else { + None + } + }) + } + + fn hide_context_menu( + &mut self, + window: &mut Window, + cx: &mut Context, + ) -> Option { + cx.notify(); + self.completion_tasks.clear(); + let context_menu = self.context_menu.borrow_mut().take(); + self.stale_inline_completion_in_menu.take(); + self.update_visible_inline_completion(window, cx); + context_menu + } + + fn show_snippet_choices( + &mut self, + choices: &Vec, + selection: Range, + cx: &mut Context, + ) { + if selection.start.buffer_id.is_none() { + return; + } + let buffer_id = selection.start.buffer_id.unwrap(); + let buffer = self.buffer().read(cx).buffer(buffer_id); + let id = post_inc(&mut self.next_completion_id); + let snippet_sort_order = EditorSettings::get_global(cx).snippet_sort_order; + + if let Some(buffer) = buffer { + *self.context_menu.borrow_mut() = Some(CodeContextMenu::Completions( + CompletionsMenu::new_snippet_choices( + id, + true, + choices, + selection, + buffer, + snippet_sort_order, + ), + )); + } + } + + pub fn insert_snippet( + &mut self, + insertion_ranges: &[Range], + snippet: Snippet, + window: &mut Window, + cx: &mut Context, + ) -> Result<()> { + struct Tabstop { + is_end_tabstop: bool, + ranges: Vec>, + choices: Option>, + } + + let tabstops = self.buffer.update(cx, |buffer, cx| { + let snippet_text: Arc = snippet.text.clone().into(); + let edits = insertion_ranges + .iter() + .cloned() + .map(|range| (range, snippet_text.clone())); + buffer.edit(edits, Some(AutoindentMode::EachLine), cx); + + let snapshot = &*buffer.read(cx); + let snippet = &snippet; + snippet + .tabstops + .iter() + .map(|tabstop| { + let is_end_tabstop = tabstop.ranges.first().map_or(false, |tabstop| { + tabstop.is_empty() && tabstop.start == snippet.text.len() as isize + }); + let mut tabstop_ranges = tabstop + .ranges + .iter() + .flat_map(|tabstop_range| { + let mut delta = 0_isize; + insertion_ranges.iter().map(move |insertion_range| { + let insertion_start = insertion_range.start as isize + delta; + delta += + snippet.text.len() as isize - insertion_range.len() as isize; + + let start = ((insertion_start + tabstop_range.start) as usize) + .min(snapshot.len()); + let end = ((insertion_start + tabstop_range.end) as usize) + .min(snapshot.len()); + snapshot.anchor_before(start)..snapshot.anchor_after(end) + }) + }) + .collect::>(); + tabstop_ranges.sort_unstable_by(|a, b| a.start.cmp(&b.start, snapshot)); + + Tabstop { + is_end_tabstop, + ranges: tabstop_ranges, + choices: tabstop.choices.clone(), + } + }) + .collect::>() + }); + if let Some(tabstop) = tabstops.first() { + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.select_ranges(tabstop.ranges.iter().cloned()); + }); + + if let Some(choices) = &tabstop.choices { + if let Some(selection) = tabstop.ranges.first() { + self.show_snippet_choices(choices, selection.clone(), cx) + } + } + + // If we're already at the last tabstop and it's at the end of the snippet, + // we're done, we don't need to keep the state around. + if !tabstop.is_end_tabstop { + let choices = tabstops + .iter() + .map(|tabstop| tabstop.choices.clone()) + .collect(); + + let ranges = tabstops + .into_iter() + .map(|tabstop| tabstop.ranges) + .collect::>(); + + self.snippet_stack.push(SnippetState { + active_index: 0, + ranges, + choices, + }); + } + + // Check whether the just-entered snippet ends with an auto-closable bracket. + if self.autoclose_regions.is_empty() { + let snapshot = self.buffer.read(cx).snapshot(cx); + for selection in &mut self.selections.all::(cx) { + let selection_head = selection.head(); + let Some(scope) = snapshot.language_scope_at(selection_head) else { + continue; + }; + + let mut bracket_pair = None; + let next_chars = snapshot.chars_at(selection_head).collect::(); + let prev_chars = snapshot + .reversed_chars_at(selection_head) + .collect::(); + for (pair, enabled) in scope.brackets() { + if enabled + && pair.close + && prev_chars.starts_with(pair.start.as_str()) + && next_chars.starts_with(pair.end.as_str()) + { + bracket_pair = Some(pair.clone()); + break; + } + } + if let Some(pair) = bracket_pair { + let snapshot_settings = snapshot.language_settings_at(selection_head, cx); + let autoclose_enabled = + self.use_autoclose && snapshot_settings.use_autoclose; + if autoclose_enabled { + let start = snapshot.anchor_after(selection_head); + let end = snapshot.anchor_after(selection_head); + self.autoclose_regions.push(AutocloseRegion { + selection_id: selection.id, + range: start..end, + pair, + }); + } + } + } + } + } + Ok(()) + } + + pub fn move_to_next_snippet_tabstop( + &mut self, + window: &mut Window, + cx: &mut Context, + ) -> bool { + self.move_to_snippet_tabstop(Bias::Right, window, cx) + } + + pub fn move_to_prev_snippet_tabstop( + &mut self, + window: &mut Window, + cx: &mut Context, + ) -> bool { + self.move_to_snippet_tabstop(Bias::Left, window, cx) + } + + pub fn move_to_snippet_tabstop( + &mut self, + bias: Bias, + window: &mut Window, + cx: &mut Context, + ) -> bool { + if let Some(mut snippet) = self.snippet_stack.pop() { + match bias { + Bias::Left => { + if snippet.active_index > 0 { + snippet.active_index -= 1; + } else { + self.snippet_stack.push(snippet); + return false; + } + } + Bias::Right => { + if snippet.active_index + 1 < snippet.ranges.len() { + snippet.active_index += 1; + } else { + self.snippet_stack.push(snippet); + return false; + } + } + } + if let Some(current_ranges) = snippet.ranges.get(snippet.active_index) { + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.select_anchor_ranges(current_ranges.iter().cloned()) + }); + + if let Some(choices) = &snippet.choices[snippet.active_index] { + if let Some(selection) = current_ranges.first() { + self.show_snippet_choices(&choices, selection.clone(), cx); + } + } + + // If snippet state is not at the last tabstop, push it back on the stack + if snippet.active_index + 1 < snippet.ranges.len() { + self.snippet_stack.push(snippet); + } + return true; + } + } + + false + } + + pub fn clear(&mut self, window: &mut Window, cx: &mut Context) { + self.transact(window, cx, |this, window, cx| { + this.select_all(&SelectAll, window, cx); + this.insert("", window, cx); + }); + } + + pub fn backspace(&mut self, _: &Backspace, window: &mut Window, cx: &mut Context) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + self.transact(window, cx, |this, window, cx| { + this.select_autoclose_pair(window, cx); + let mut linked_ranges = HashMap::<_, Vec<_>>::default(); + if !this.linked_edit_ranges.is_empty() { + let selections = this.selections.all::(cx); + let snapshot = this.buffer.read(cx).snapshot(cx); + + for selection in selections.iter() { + let selection_start = snapshot.anchor_before(selection.start).text_anchor; + let selection_end = snapshot.anchor_after(selection.end).text_anchor; + if selection_start.buffer_id != selection_end.buffer_id { + continue; + } + if let Some(ranges) = + this.linked_editing_ranges_for(selection_start..selection_end, cx) + { + for (buffer, entries) in ranges { + linked_ranges.entry(buffer).or_default().extend(entries); + } + } + } + } + + let mut selections = this.selections.all::(cx); + let display_map = this.display_map.update(cx, |map, cx| map.snapshot(cx)); + for selection in &mut selections { + if selection.is_empty() { + let old_head = selection.head(); + let mut new_head = + movement::left(&display_map, old_head.to_display_point(&display_map)) + .to_point(&display_map); + if let Some((buffer, line_buffer_range)) = display_map + .buffer_snapshot + .buffer_line_for_row(MultiBufferRow(old_head.row)) + { + let indent_size = buffer.indent_size_for_line(line_buffer_range.start.row); + let indent_len = match indent_size.kind { + IndentKind::Space => { + buffer.settings_at(line_buffer_range.start, cx).tab_size + } + IndentKind::Tab => NonZeroU32::new(1).unwrap(), + }; + if old_head.column <= indent_size.len && old_head.column > 0 { + let indent_len = indent_len.get(); + new_head = cmp::min( + new_head, + MultiBufferPoint::new( + old_head.row, + ((old_head.column - 1) / indent_len) * indent_len, + ), + ); + } + } + + selection.set_head(new_head, SelectionGoal::None); + } + } + + this.signature_help_state.set_backspace_pressed(true); + this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.select(selections) + }); + this.insert("", window, cx); + let empty_str: Arc = Arc::from(""); + for (buffer, edits) in linked_ranges { + let snapshot = buffer.read(cx).snapshot(); + use text::ToPoint as TP; + + let edits = edits + .into_iter() + .map(|range| { + let end_point = TP::to_point(&range.end, &snapshot); + let mut start_point = TP::to_point(&range.start, &snapshot); + + if end_point == start_point { + let offset = text::ToOffset::to_offset(&range.start, &snapshot) + .saturating_sub(1); + start_point = + snapshot.clip_point(TP::to_point(&offset, &snapshot), Bias::Left); + }; + + (start_point..end_point, empty_str.clone()) + }) + .sorted_by_key(|(range, _)| range.start) + .collect::>(); + buffer.update(cx, |this, cx| { + this.edit(edits, None, cx); + }) + } + this.refresh_inline_completion(true, false, window, cx); + linked_editing_ranges::refresh_linked_ranges(this, window, cx); + }); + } + + pub fn delete(&mut self, _: &Delete, window: &mut Window, cx: &mut Context) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + self.transact(window, cx, |this, window, cx| { + this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.move_with(|map, selection| { + if selection.is_empty() { + let cursor = movement::right(map, selection.head()); + selection.end = cursor; + selection.reversed = true; + selection.goal = SelectionGoal::None; + } + }) + }); + this.insert("", window, cx); + this.refresh_inline_completion(true, false, window, cx); + }); + } + + pub fn backtab(&mut self, _: &Backtab, window: &mut Window, cx: &mut Context) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + if self.move_to_prev_snippet_tabstop(window, cx) { + return; + } + self.outdent(&Outdent, window, cx); + } + + pub fn tab(&mut self, _: &Tab, window: &mut Window, cx: &mut Context) { + if self.move_to_next_snippet_tabstop(window, cx) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + return; + } + if self.read_only(cx) { + return; + } + self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + let mut selections = self.selections.all_adjusted(cx); + let buffer = self.buffer.read(cx); + let snapshot = buffer.snapshot(cx); + let rows_iter = selections.iter().map(|s| s.head().row); + let suggested_indents = snapshot.suggested_indents(rows_iter, cx); + + let has_some_cursor_in_whitespace = selections + .iter() + .filter(|selection| selection.is_empty()) + .any(|selection| { + let cursor = selection.head(); + let current_indent = snapshot.indent_size_for_line(MultiBufferRow(cursor.row)); + cursor.column < current_indent.len + }); + + let mut edits = Vec::new(); + let mut prev_edited_row = 0; + let mut row_delta = 0; + for selection in &mut selections { + if selection.start.row != prev_edited_row { + row_delta = 0; + } + prev_edited_row = selection.end.row; + + // If the selection is non-empty, then increase the indentation of the selected lines. + if !selection.is_empty() { + row_delta = + Self::indent_selection(buffer, &snapshot, selection, &mut edits, row_delta, cx); + continue; + } + + // If the selection is empty and the cursor is in the leading whitespace before the + // suggested indentation, then auto-indent the line. + let cursor = selection.head(); + let current_indent = snapshot.indent_size_for_line(MultiBufferRow(cursor.row)); + if let Some(suggested_indent) = + suggested_indents.get(&MultiBufferRow(cursor.row)).copied() + { + // If there exist any empty selection in the leading whitespace, then skip + // indent for selections at the boundary. + if has_some_cursor_in_whitespace + && cursor.column == current_indent.len + && current_indent.len == suggested_indent.len + { + continue; + } + + if cursor.column < suggested_indent.len + && cursor.column <= current_indent.len + && current_indent.len <= suggested_indent.len + { + selection.start = Point::new(cursor.row, suggested_indent.len); + selection.end = selection.start; + if row_delta == 0 { + edits.extend(Buffer::edit_for_indent_size_adjustment( + cursor.row, + current_indent, + suggested_indent, + )); + row_delta = suggested_indent.len - current_indent.len; + } + continue; + } + } + + // Otherwise, insert a hard or soft tab. + let settings = buffer.language_settings_at(cursor, cx); + let tab_size = if settings.hard_tabs { + IndentSize::tab() + } else { + let tab_size = settings.tab_size.get(); + let indent_remainder = snapshot + .text_for_range(Point::new(cursor.row, 0)..cursor) + .flat_map(str::chars) + .fold(row_delta % tab_size, |counter: u32, c| { + if c == '\t' { + 0 + } else { + (counter + 1) % tab_size + } + }); + + let chars_to_next_tab_stop = tab_size - indent_remainder; + IndentSize::spaces(chars_to_next_tab_stop) + }; + selection.start = Point::new(cursor.row, cursor.column + row_delta + tab_size.len); + selection.end = selection.start; + edits.push((cursor..cursor, tab_size.chars().collect::())); + row_delta += tab_size.len; + } + + self.transact(window, cx, |this, window, cx| { + this.buffer.update(cx, |b, cx| b.edit(edits, None, cx)); + this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.select(selections) + }); + this.refresh_inline_completion(true, false, window, cx); + }); + } + + pub fn indent(&mut self, _: &Indent, window: &mut Window, cx: &mut Context) { + if self.read_only(cx) { + return; + } + self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + let mut selections = self.selections.all::(cx); + let mut prev_edited_row = 0; + let mut row_delta = 0; + let mut edits = Vec::new(); + let buffer = self.buffer.read(cx); + let snapshot = buffer.snapshot(cx); + for selection in &mut selections { + if selection.start.row != prev_edited_row { + row_delta = 0; + } + prev_edited_row = selection.end.row; + + row_delta = + Self::indent_selection(buffer, &snapshot, selection, &mut edits, row_delta, cx); + } + + self.transact(window, cx, |this, window, cx| { + this.buffer.update(cx, |b, cx| b.edit(edits, None, cx)); + this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.select(selections) + }); + }); + } + + fn indent_selection( + buffer: &MultiBuffer, + snapshot: &MultiBufferSnapshot, + selection: &mut Selection, + edits: &mut Vec<(Range, String)>, + delta_for_start_row: u32, + cx: &App, + ) -> u32 { + let settings = buffer.language_settings_at(selection.start, cx); + let tab_size = settings.tab_size.get(); + let indent_kind = if settings.hard_tabs { + IndentKind::Tab + } else { + IndentKind::Space + }; + let mut start_row = selection.start.row; + let mut end_row = selection.end.row + 1; + + // If a selection ends at the beginning of a line, don't indent + // that last line. + if selection.end.column == 0 && selection.end.row > selection.start.row { + end_row -= 1; + } + + // Avoid re-indenting a row that has already been indented by a + // previous selection, but still update this selection's column + // to reflect that indentation. + if delta_for_start_row > 0 { + start_row += 1; + selection.start.column += delta_for_start_row; + if selection.end.row == selection.start.row { + selection.end.column += delta_for_start_row; + } + } + + let mut delta_for_end_row = 0; + let has_multiple_rows = start_row + 1 != end_row; + for row in start_row..end_row { + let current_indent = snapshot.indent_size_for_line(MultiBufferRow(row)); + let indent_delta = match (current_indent.kind, indent_kind) { + (IndentKind::Space, IndentKind::Space) => { + let columns_to_next_tab_stop = tab_size - (current_indent.len % tab_size); + IndentSize::spaces(columns_to_next_tab_stop) + } + (IndentKind::Tab, IndentKind::Space) => IndentSize::spaces(tab_size), + (_, IndentKind::Tab) => IndentSize::tab(), + }; + + let start = if has_multiple_rows || current_indent.len < selection.start.column { + 0 + } else { + selection.start.column + }; + let row_start = Point::new(row, start); + edits.push(( + row_start..row_start, + indent_delta.chars().collect::(), + )); + + // Update this selection's endpoints to reflect the indentation. + if row == selection.start.row { + selection.start.column += indent_delta.len; + } + if row == selection.end.row { + selection.end.column += indent_delta.len; + delta_for_end_row = indent_delta.len; + } + } + + if selection.start.row == selection.end.row { + delta_for_start_row + delta_for_end_row + } else { + delta_for_end_row + } + } + + pub fn outdent(&mut self, _: &Outdent, window: &mut Window, cx: &mut Context) { + if self.read_only(cx) { + return; + } + self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let selections = self.selections.all::(cx); + let mut deletion_ranges = Vec::new(); + let mut last_outdent = None; + { + let buffer = self.buffer.read(cx); + let snapshot = buffer.snapshot(cx); + for selection in &selections { + let settings = buffer.language_settings_at(selection.start, cx); + let tab_size = settings.tab_size.get(); + let mut rows = selection.spanned_rows(false, &display_map); + + // Avoid re-outdenting a row that has already been outdented by a + // previous selection. + if let Some(last_row) = last_outdent { + if last_row == rows.start { + rows.start = rows.start.next_row(); + } + } + let has_multiple_rows = rows.len() > 1; + for row in rows.iter_rows() { + let indent_size = snapshot.indent_size_for_line(row); + if indent_size.len > 0 { + let deletion_len = match indent_size.kind { + IndentKind::Space => { + let columns_to_prev_tab_stop = indent_size.len % tab_size; + if columns_to_prev_tab_stop == 0 { + tab_size + } else { + columns_to_prev_tab_stop + } + } + IndentKind::Tab => 1, + }; + let start = if has_multiple_rows + || deletion_len > selection.start.column + || indent_size.len < selection.start.column + { + 0 + } else { + selection.start.column - deletion_len + }; + deletion_ranges.push( + Point::new(row.0, start)..Point::new(row.0, start + deletion_len), + ); + last_outdent = Some(row); + } + } + } + } + + self.transact(window, cx, |this, window, cx| { + this.buffer.update(cx, |buffer, cx| { + let empty_str: Arc = Arc::default(); + buffer.edit( + deletion_ranges + .into_iter() + .map(|range| (range, empty_str.clone())), + None, + cx, + ); + }); + let selections = this.selections.all::(cx); + this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.select(selections) + }); + }); + } + + pub fn autoindent(&mut self, _: &AutoIndent, window: &mut Window, cx: &mut Context) { + if self.read_only(cx) { + return; + } + self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + let selections = self + .selections + .all::(cx) + .into_iter() + .map(|s| s.range()); + + self.transact(window, cx, |this, window, cx| { + this.buffer.update(cx, |buffer, cx| { + buffer.autoindent_ranges(selections, cx); + }); + let selections = this.selections.all::(cx); + this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.select(selections) + }); + }); + } + + pub fn delete_line(&mut self, _: &DeleteLine, window: &mut Window, cx: &mut Context) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let selections = self.selections.all::(cx); + + let mut new_cursors = Vec::new(); + let mut edit_ranges = Vec::new(); + let mut selections = selections.iter().peekable(); + while let Some(selection) = selections.next() { + let mut rows = selection.spanned_rows(false, &display_map); + let goal_display_column = selection.head().to_display_point(&display_map).column(); + + // Accumulate contiguous regions of rows that we want to delete. + while let Some(next_selection) = selections.peek() { + let next_rows = next_selection.spanned_rows(false, &display_map); + if next_rows.start <= rows.end { + rows.end = next_rows.end; + selections.next().unwrap(); + } else { + break; + } + } + + let buffer = &display_map.buffer_snapshot; + let mut edit_start = Point::new(rows.start.0, 0).to_offset(buffer); + let edit_end; + let cursor_buffer_row; + if buffer.max_point().row >= rows.end.0 { + // If there's a line after the range, delete the \n from the end of the row range + // and position the cursor on the next line. + edit_end = Point::new(rows.end.0, 0).to_offset(buffer); + cursor_buffer_row = rows.end; + } else { + // If there isn't a line after the range, delete the \n from the line before the + // start of the row range and position the cursor there. + edit_start = edit_start.saturating_sub(1); + edit_end = buffer.len(); + cursor_buffer_row = rows.start.previous_row(); + } + + let mut cursor = Point::new(cursor_buffer_row.0, 0).to_display_point(&display_map); + *cursor.column_mut() = + cmp::min(goal_display_column, display_map.line_len(cursor.row())); + + new_cursors.push(( + selection.id, + buffer.anchor_after(cursor.to_point(&display_map)), + )); + edit_ranges.push(edit_start..edit_end); + } + + self.transact(window, cx, |this, window, cx| { + let buffer = this.buffer.update(cx, |buffer, cx| { + let empty_str: Arc = Arc::default(); + buffer.edit( + edit_ranges + .into_iter() + .map(|range| (range, empty_str.clone())), + None, + cx, + ); + buffer.snapshot(cx) + }); + let new_selections = new_cursors + .into_iter() + .map(|(id, cursor)| { + let cursor = cursor.to_point(&buffer); + Selection { + id, + start: cursor, + end: cursor, + reversed: false, + goal: SelectionGoal::None, + } + }) + .collect(); + + this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.select(new_selections); + }); + }); + } + + pub fn join_lines_impl( + &mut self, + insert_whitespace: bool, + window: &mut Window, + cx: &mut Context, + ) { + if self.read_only(cx) { + return; + } + let mut row_ranges = Vec::>::new(); + for selection in self.selections.all::(cx) { + let start = MultiBufferRow(selection.start.row); + // Treat single line selections as if they include the next line. Otherwise this action + // would do nothing for single line selections individual cursors. + let end = if selection.start.row == selection.end.row { + MultiBufferRow(selection.start.row + 1) + } else { + MultiBufferRow(selection.end.row) + }; + + if let Some(last_row_range) = row_ranges.last_mut() { + if start <= last_row_range.end { + last_row_range.end = end; + continue; + } + } + row_ranges.push(start..end); + } + + let snapshot = self.buffer.read(cx).snapshot(cx); + let mut cursor_positions = Vec::new(); + for row_range in &row_ranges { + let anchor = snapshot.anchor_before(Point::new( + row_range.end.previous_row().0, + snapshot.line_len(row_range.end.previous_row()), + )); + cursor_positions.push(anchor..anchor); + } + + self.transact(window, cx, |this, window, cx| { + for row_range in row_ranges.into_iter().rev() { + for row in row_range.iter_rows().rev() { + let end_of_line = Point::new(row.0, snapshot.line_len(row)); + let next_line_row = row.next_row(); + let indent = snapshot.indent_size_for_line(next_line_row); + let start_of_next_line = Point::new(next_line_row.0, indent.len); + + let replace = + if snapshot.line_len(next_line_row) > indent.len && insert_whitespace { + " " + } else { + "" + }; + + this.buffer.update(cx, |buffer, cx| { + buffer.edit([(end_of_line..start_of_next_line, replace)], None, cx) + }); + } + } + + this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.select_anchor_ranges(cursor_positions) + }); + }); + } + + pub fn join_lines(&mut self, _: &JoinLines, window: &mut Window, cx: &mut Context) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + self.join_lines_impl(true, window, cx); + } + + pub fn sort_lines_case_sensitive( + &mut self, + _: &SortLinesCaseSensitive, + window: &mut Window, + cx: &mut Context, + ) { + self.manipulate_lines(window, cx, |lines| lines.sort()) + } + + pub fn sort_lines_case_insensitive( + &mut self, + _: &SortLinesCaseInsensitive, + window: &mut Window, + cx: &mut Context, + ) { + self.manipulate_lines(window, cx, |lines| { + lines.sort_by_key(|line| line.to_lowercase()) + }) + } + + pub fn unique_lines_case_insensitive( + &mut self, + _: &UniqueLinesCaseInsensitive, + window: &mut Window, + cx: &mut Context, + ) { + self.manipulate_lines(window, cx, |lines| { + let mut seen = HashSet::default(); + lines.retain(|line| seen.insert(line.to_lowercase())); + }) + } + + pub fn unique_lines_case_sensitive( + &mut self, + _: &UniqueLinesCaseSensitive, + window: &mut Window, + cx: &mut Context, + ) { + self.manipulate_lines(window, cx, |lines| { + let mut seen = HashSet::default(); + lines.retain(|line| seen.insert(*line)); + }) + } + + pub fn reload_file(&mut self, _: &ReloadFile, window: &mut Window, cx: &mut Context) { + let Some(project) = self.project.clone() else { + return; + }; + self.reload(project, window, cx) + .detach_and_notify_err(window, cx); + } + + pub fn restore_file( + &mut self, + _: &::git::RestoreFile, + window: &mut Window, + cx: &mut Context, + ) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + let mut buffer_ids = HashSet::default(); + let snapshot = self.buffer().read(cx).snapshot(cx); + for selection in self.selections.all::(cx) { + buffer_ids.extend(snapshot.buffer_ids_for_range(selection.range())) + } + + let buffer = self.buffer().read(cx); + let ranges = buffer_ids + .into_iter() + .flat_map(|buffer_id| buffer.excerpt_ranges_for_buffer(buffer_id, cx)) + .collect::>(); + + self.restore_hunks_in_ranges(ranges, window, cx); + } + + pub fn git_restore(&mut self, _: &Restore, window: &mut Window, cx: &mut Context) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + let selections = self + .selections + .all(cx) + .into_iter() + .map(|s| s.range()) + .collect(); + self.restore_hunks_in_ranges(selections, window, cx); + } + + pub fn restore_hunks_in_ranges( + &mut self, + ranges: Vec>, + window: &mut Window, + cx: &mut Context, + ) { + let mut revert_changes = HashMap::default(); + let chunk_by = self + .snapshot(window, cx) + .hunks_for_ranges(ranges) + .into_iter() + .chunk_by(|hunk| hunk.buffer_id); + for (buffer_id, hunks) in &chunk_by { + let hunks = hunks.collect::>(); + for hunk in &hunks { + self.prepare_restore_change(&mut revert_changes, hunk, cx); + } + self.do_stage_or_unstage(false, buffer_id, hunks.into_iter(), cx); + } + drop(chunk_by); + if !revert_changes.is_empty() { + self.transact(window, cx, |editor, window, cx| { + editor.restore(revert_changes, window, cx); + }); + } + } + + pub fn open_active_item_in_terminal( + &mut self, + _: &OpenInTerminal, + window: &mut Window, + cx: &mut Context, + ) { + if let Some(working_directory) = self.active_excerpt(cx).and_then(|(_, buffer, _)| { + let project_path = buffer.read(cx).project_path(cx)?; + let project = self.project.as_ref()?.read(cx); + let entry = project.entry_for_path(&project_path, cx)?; + let parent = match &entry.canonical_path { + Some(canonical_path) => canonical_path.to_path_buf(), + None => project.absolute_path(&project_path, cx)?, + } + .parent()? + .to_path_buf(); + Some(parent) + }) { + window.dispatch_action(OpenTerminal { working_directory }.boxed_clone(), cx); + } + } + + fn set_breakpoint_context_menu( + &mut self, + display_row: DisplayRow, + position: Option, + clicked_point: gpui::Point, + window: &mut Window, + cx: &mut Context, + ) { + if !cx.has_flag::() { + return; + } + let source = self + .buffer + .read(cx) + .snapshot(cx) + .anchor_before(Point::new(display_row.0, 0u32)); + + let context_menu = self.breakpoint_context_menu(position.unwrap_or(source), window, cx); + + self.mouse_context_menu = MouseContextMenu::pinned_to_editor( + self, + source, + clicked_point, + context_menu, + window, + cx, + ); + } + + fn add_edit_breakpoint_block( + &mut self, + anchor: Anchor, + breakpoint: &Breakpoint, + edit_action: BreakpointPromptEditAction, + window: &mut Window, + cx: &mut Context, + ) { + let weak_editor = cx.weak_entity(); + let bp_prompt = cx.new(|cx| { + BreakpointPromptEditor::new( + weak_editor, + anchor, + breakpoint.clone(), + edit_action, + window, + cx, + ) + }); + + let height = bp_prompt.update(cx, |this, cx| { + this.prompt + .update(cx, |prompt, cx| prompt.max_point(cx).row().0 + 1 + 2) + }); + let cloned_prompt = bp_prompt.clone(); + let blocks = vec![BlockProperties { + style: BlockStyle::Sticky, + placement: BlockPlacement::Above(anchor), + height: Some(height), + render: Arc::new(move |cx| { + *cloned_prompt.read(cx).gutter_dimensions.lock() = *cx.gutter_dimensions; + cloned_prompt.clone().into_any_element() + }), + priority: 0, + }]; + + let focus_handle = bp_prompt.focus_handle(cx); + window.focus(&focus_handle); + + let block_ids = self.insert_blocks(blocks, None, cx); + bp_prompt.update(cx, |prompt, _| { + prompt.add_block_ids(block_ids); + }); + } + + pub(crate) fn breakpoint_at_row( + &self, + row: u32, + window: &mut Window, + cx: &mut Context, + ) -> Option<(Anchor, Breakpoint)> { + let snapshot = self.snapshot(window, cx); + let breakpoint_position = snapshot.buffer_snapshot.anchor_before(Point::new(row, 0)); + + self.breakpoint_at_anchor(breakpoint_position, &snapshot, cx) + } + + pub(crate) fn breakpoint_at_anchor( + &self, + breakpoint_position: Anchor, + snapshot: &EditorSnapshot, + cx: &mut Context, + ) -> Option<(Anchor, Breakpoint)> { + let project = self.project.clone()?; + + let buffer_id = breakpoint_position.buffer_id.or_else(|| { + snapshot + .buffer_snapshot + .buffer_id_for_excerpt(breakpoint_position.excerpt_id) + })?; + + let enclosing_excerpt = breakpoint_position.excerpt_id; + let buffer = project.read_with(cx, |project, cx| project.buffer_for_id(buffer_id, cx))?; + let buffer_snapshot = buffer.read(cx).snapshot(); + + let row = buffer_snapshot + .summary_for_anchor::(&breakpoint_position.text_anchor) + .row; + + let line_len = snapshot.buffer_snapshot.line_len(MultiBufferRow(row)); + let anchor_end = snapshot + .buffer_snapshot + .anchor_after(Point::new(row, line_len)); + + let bp = self + .breakpoint_store + .as_ref()? + .read_with(cx, |breakpoint_store, cx| { + breakpoint_store + .breakpoints( + &buffer, + Some(breakpoint_position.text_anchor..anchor_end.text_anchor), + &buffer_snapshot, + cx, + ) + .next() + .and_then(|(anchor, bp)| { + let breakpoint_row = buffer_snapshot + .summary_for_anchor::(anchor) + .row; + + if breakpoint_row == row { + snapshot + .buffer_snapshot + .anchor_in_excerpt(enclosing_excerpt, *anchor) + .map(|anchor| (anchor, bp.clone())) + } else { + None + } + }) + }); + bp + } + + pub fn edit_log_breakpoint( + &mut self, + _: &EditLogBreakpoint, + window: &mut Window, + cx: &mut Context, + ) { + for (anchor, breakpoint) in self.breakpoints_at_cursors(window, cx) { + let breakpoint = breakpoint.unwrap_or_else(|| Breakpoint { + message: None, + state: BreakpointState::Enabled, + condition: None, + hit_condition: None, + }); + + self.add_edit_breakpoint_block( + anchor, + &breakpoint, + BreakpointPromptEditAction::Log, + window, + cx, + ); + } + } + + fn breakpoints_at_cursors( + &self, + window: &mut Window, + cx: &mut Context, + ) -> Vec<(Anchor, Option)> { + let snapshot = self.snapshot(window, cx); + let cursors = self + .selections + .disjoint_anchors() + .into_iter() + .map(|selection| { + let cursor_position: Point = selection.head().to_point(&snapshot.buffer_snapshot); + + let breakpoint_position = self + .breakpoint_at_row(cursor_position.row, window, cx) + .map(|bp| bp.0) + .unwrap_or_else(|| { + snapshot + .display_snapshot + .buffer_snapshot + .anchor_after(Point::new(cursor_position.row, 0)) + }); + + let breakpoint = self + .breakpoint_at_anchor(breakpoint_position, &snapshot, cx) + .map(|(anchor, breakpoint)| (anchor, Some(breakpoint))); + + breakpoint.unwrap_or_else(|| (breakpoint_position, None)) + }) + // There might be multiple cursors on the same line; all of them should have the same anchors though as their breakpoints positions, which makes it possible to sort and dedup the list. + .collect::>(); + + cursors.into_iter().collect() + } + + pub fn enable_breakpoint( + &mut self, + _: &crate::actions::EnableBreakpoint, + window: &mut Window, + cx: &mut Context, + ) { + for (anchor, breakpoint) in self.breakpoints_at_cursors(window, cx) { + let Some(breakpoint) = breakpoint.filter(|breakpoint| breakpoint.is_disabled()) else { + continue; + }; + self.edit_breakpoint_at_anchor( + anchor, + breakpoint, + BreakpointEditAction::InvertState, + cx, + ); + } + } + + pub fn disable_breakpoint( + &mut self, + _: &crate::actions::DisableBreakpoint, + window: &mut Window, + cx: &mut Context, + ) { + for (anchor, breakpoint) in self.breakpoints_at_cursors(window, cx) { + let Some(breakpoint) = breakpoint.filter(|breakpoint| breakpoint.is_enabled()) else { + continue; + }; + self.edit_breakpoint_at_anchor( + anchor, + breakpoint, + BreakpointEditAction::InvertState, + cx, + ); + } + } + + pub fn toggle_breakpoint( + &mut self, + _: &crate::actions::ToggleBreakpoint, + window: &mut Window, + cx: &mut Context, + ) { + for (anchor, breakpoint) in self.breakpoints_at_cursors(window, cx) { + if let Some(breakpoint) = breakpoint { + self.edit_breakpoint_at_anchor( + anchor, + breakpoint, + BreakpointEditAction::Toggle, + cx, + ); + } else { + self.edit_breakpoint_at_anchor( + anchor, + Breakpoint::new_standard(), + BreakpointEditAction::Toggle, + cx, + ); + } + } + } + + pub fn edit_breakpoint_at_anchor( + &mut self, + breakpoint_position: Anchor, + breakpoint: Breakpoint, + edit_action: BreakpointEditAction, + cx: &mut Context, + ) { + let Some(breakpoint_store) = &self.breakpoint_store else { + return; + }; + + let Some(buffer_id) = breakpoint_position.buffer_id.or_else(|| { + if breakpoint_position == Anchor::min() { + self.buffer() + .read(cx) + .excerpt_buffer_ids() + .into_iter() + .next() + } else { + None + } + }) else { + return; + }; + + let Some(buffer) = self.buffer().read(cx).buffer(buffer_id) else { + return; + }; + + breakpoint_store.update(cx, |breakpoint_store, cx| { + breakpoint_store.toggle_breakpoint( + buffer, + (breakpoint_position.text_anchor, breakpoint), + edit_action, + cx, + ); + }); + + cx.notify(); + } + + #[cfg(any(test, feature = "test-support"))] + pub fn breakpoint_store(&self) -> Option> { + self.breakpoint_store.clone() + } + + pub fn prepare_restore_change( + &self, + revert_changes: &mut HashMap, Rope)>>, + hunk: &MultiBufferDiffHunk, + cx: &mut App, + ) -> Option<()> { + if hunk.is_created_file() { + return None; + } + let buffer = self.buffer.read(cx); + let diff = buffer.diff_for(hunk.buffer_id)?; + let buffer = buffer.buffer(hunk.buffer_id)?; + let buffer = buffer.read(cx); + let original_text = diff + .read(cx) + .base_text() + .as_rope() + .slice(hunk.diff_base_byte_range.clone()); + let buffer_snapshot = buffer.snapshot(); + let buffer_revert_changes = revert_changes.entry(buffer.remote_id()).or_default(); + if let Err(i) = buffer_revert_changes.binary_search_by(|probe| { + probe + .0 + .start + .cmp(&hunk.buffer_range.start, &buffer_snapshot) + .then(probe.0.end.cmp(&hunk.buffer_range.end, &buffer_snapshot)) + }) { + buffer_revert_changes.insert(i, (hunk.buffer_range.clone(), original_text)); + Some(()) + } else { + None + } + } + + pub fn reverse_lines(&mut self, _: &ReverseLines, window: &mut Window, cx: &mut Context) { + self.manipulate_lines(window, cx, |lines| lines.reverse()) + } + + pub fn shuffle_lines(&mut self, _: &ShuffleLines, window: &mut Window, cx: &mut Context) { + self.manipulate_lines(window, cx, |lines| lines.shuffle(&mut thread_rng())) + } + + fn manipulate_lines( + &mut self, + window: &mut Window, + cx: &mut Context, + mut callback: Fn, + ) where + Fn: FnMut(&mut Vec<&str>), + { + self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let buffer = self.buffer.read(cx).snapshot(cx); + + let mut edits = Vec::new(); + + let selections = self.selections.all::(cx); + let mut selections = selections.iter().peekable(); + let mut contiguous_row_selections = Vec::new(); + let mut new_selections = Vec::new(); + let mut added_lines = 0; + let mut removed_lines = 0; + + while let Some(selection) = selections.next() { + let (start_row, end_row) = consume_contiguous_rows( + &mut contiguous_row_selections, + selection, + &display_map, + &mut selections, + ); + + let start_point = Point::new(start_row.0, 0); + let end_point = Point::new( + end_row.previous_row().0, + buffer.line_len(end_row.previous_row()), + ); + let text = buffer + .text_for_range(start_point..end_point) + .collect::(); + + let mut lines = text.split('\n').collect_vec(); + + let lines_before = lines.len(); + callback(&mut lines); + let lines_after = lines.len(); + + edits.push((start_point..end_point, lines.join("\n"))); + + // Selections must change based on added and removed line count + let start_row = + MultiBufferRow(start_point.row + added_lines as u32 - removed_lines as u32); + let end_row = MultiBufferRow(start_row.0 + lines_after.saturating_sub(1) as u32); + new_selections.push(Selection { + id: selection.id, + start: start_row, + end: end_row, + goal: SelectionGoal::None, + reversed: selection.reversed, + }); + + if lines_after > lines_before { + added_lines += lines_after - lines_before; + } else if lines_before > lines_after { + removed_lines += lines_before - lines_after; + } + } + + self.transact(window, cx, |this, window, cx| { + let buffer = this.buffer.update(cx, |buffer, cx| { + buffer.edit(edits, None, cx); + buffer.snapshot(cx) + }); + + // Recalculate offsets on newly edited buffer + let new_selections = new_selections + .iter() + .map(|s| { + let start_point = Point::new(s.start.0, 0); + let end_point = Point::new(s.end.0, buffer.line_len(s.end)); + Selection { + id: s.id, + start: buffer.point_to_offset(start_point), + end: buffer.point_to_offset(end_point), + goal: s.goal, + reversed: s.reversed, + } + }) + .collect(); + + this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.select(new_selections); + }); + + this.request_autoscroll(Autoscroll::fit(), cx); + }); + } + + pub fn toggle_case(&mut self, _: &ToggleCase, window: &mut Window, cx: &mut Context) { + self.manipulate_text(window, cx, |text| { + let has_upper_case_characters = text.chars().any(|c| c.is_uppercase()); + if has_upper_case_characters { + text.to_lowercase() + } else { + text.to_uppercase() + } + }) + } + + pub fn convert_to_upper_case( + &mut self, + _: &ConvertToUpperCase, + window: &mut Window, + cx: &mut Context, + ) { + self.manipulate_text(window, cx, |text| text.to_uppercase()) + } + + pub fn convert_to_lower_case( + &mut self, + _: &ConvertToLowerCase, + window: &mut Window, + cx: &mut Context, + ) { + self.manipulate_text(window, cx, |text| text.to_lowercase()) + } + + pub fn convert_to_title_case( + &mut self, + _: &ConvertToTitleCase, + window: &mut Window, + cx: &mut Context, + ) { + self.manipulate_text(window, cx, |text| { + text.split('\n') + .map(|line| line.to_case(Case::Title)) + .join("\n") + }) + } + + pub fn convert_to_snake_case( + &mut self, + _: &ConvertToSnakeCase, + window: &mut Window, + cx: &mut Context, + ) { + self.manipulate_text(window, cx, |text| text.to_case(Case::Snake)) + } + + pub fn convert_to_kebab_case( + &mut self, + _: &ConvertToKebabCase, + window: &mut Window, + cx: &mut Context, + ) { + self.manipulate_text(window, cx, |text| text.to_case(Case::Kebab)) + } + + pub fn convert_to_upper_camel_case( + &mut self, + _: &ConvertToUpperCamelCase, + window: &mut Window, + cx: &mut Context, + ) { + self.manipulate_text(window, cx, |text| { + text.split('\n') + .map(|line| line.to_case(Case::UpperCamel)) + .join("\n") + }) + } + + pub fn convert_to_lower_camel_case( + &mut self, + _: &ConvertToLowerCamelCase, + window: &mut Window, + cx: &mut Context, + ) { + self.manipulate_text(window, cx, |text| text.to_case(Case::Camel)) + } + + pub fn convert_to_opposite_case( + &mut self, + _: &ConvertToOppositeCase, + window: &mut Window, + cx: &mut Context, + ) { + self.manipulate_text(window, cx, |text| { + text.chars() + .fold(String::with_capacity(text.len()), |mut t, c| { + if c.is_uppercase() { + t.extend(c.to_lowercase()); + } else { + t.extend(c.to_uppercase()); + } + t + }) + }) + } + + pub fn convert_to_rot13( + &mut self, + _: &ConvertToRot13, + window: &mut Window, + cx: &mut Context, + ) { + self.manipulate_text(window, cx, |text| { + text.chars() + .map(|c| match c { + 'A'..='M' | 'a'..='m' => ((c as u8) + 13) as char, + 'N'..='Z' | 'n'..='z' => ((c as u8) - 13) as char, + _ => c, + }) + .collect() + }) + } + + pub fn convert_to_rot47( + &mut self, + _: &ConvertToRot47, + window: &mut Window, + cx: &mut Context, + ) { + self.manipulate_text(window, cx, |text| { + text.chars() + .map(|c| { + let code_point = c as u32; + if code_point >= 33 && code_point <= 126 { + return char::from_u32(33 + ((code_point + 14) % 94)).unwrap(); + } + c + }) + .collect() + }) + } + + fn manipulate_text(&mut self, window: &mut Window, cx: &mut Context, mut callback: Fn) + where + Fn: FnMut(&str) -> String, + { + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let buffer = self.buffer.read(cx).snapshot(cx); + + let mut new_selections = Vec::new(); + let mut edits = Vec::new(); + let mut selection_adjustment = 0i32; + + for selection in self.selections.all::(cx) { + let selection_is_empty = selection.is_empty(); + + let (start, end) = if selection_is_empty { + let word_range = movement::surrounding_word( + &display_map, + selection.start.to_display_point(&display_map), + ); + let start = word_range.start.to_offset(&display_map, Bias::Left); + let end = word_range.end.to_offset(&display_map, Bias::Left); + (start, end) + } else { + (selection.start, selection.end) + }; + + let text = buffer.text_for_range(start..end).collect::(); + let old_length = text.len() as i32; + let text = callback(&text); + + new_selections.push(Selection { + start: (start as i32 - selection_adjustment) as usize, + end: ((start + text.len()) as i32 - selection_adjustment) as usize, + goal: SelectionGoal::None, + ..selection + }); + + selection_adjustment += old_length - text.len() as i32; + + edits.push((start..end, text)); + } + + self.transact(window, cx, |this, window, cx| { + this.buffer.update(cx, |buffer, cx| { + buffer.edit(edits, None, cx); + }); + + this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.select(new_selections); + }); + + this.request_autoscroll(Autoscroll::fit(), cx); + }); + } + + pub fn duplicate( + &mut self, + upwards: bool, + whole_lines: bool, + window: &mut Window, + cx: &mut Context, + ) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let buffer = &display_map.buffer_snapshot; + let selections = self.selections.all::(cx); + + let mut edits = Vec::new(); + let mut selections_iter = selections.iter().peekable(); + while let Some(selection) = selections_iter.next() { + let mut rows = selection.spanned_rows(false, &display_map); + // duplicate line-wise + if whole_lines || selection.start == selection.end { + // Avoid duplicating the same lines twice. + while let Some(next_selection) = selections_iter.peek() { + let next_rows = next_selection.spanned_rows(false, &display_map); + if next_rows.start < rows.end { + rows.end = next_rows.end; + selections_iter.next().unwrap(); + } else { + break; + } + } + + // Copy the text from the selected row region and splice it either at the start + // or end of the region. + let start = Point::new(rows.start.0, 0); + let end = Point::new( + rows.end.previous_row().0, + buffer.line_len(rows.end.previous_row()), + ); + let text = buffer + .text_for_range(start..end) + .chain(Some("\n")) + .collect::(); + let insert_location = if upwards { + Point::new(rows.end.0, 0) + } else { + start + }; + edits.push((insert_location..insert_location, text)); + } else { + // duplicate character-wise + let start = selection.start; + let end = selection.end; + let text = buffer.text_for_range(start..end).collect::(); + edits.push((selection.end..selection.end, text)); + } + } + + self.transact(window, cx, |this, _, cx| { + this.buffer.update(cx, |buffer, cx| { + buffer.edit(edits, None, cx); + }); + + this.request_autoscroll(Autoscroll::fit(), cx); + }); + } + + pub fn duplicate_line_up( + &mut self, + _: &DuplicateLineUp, + window: &mut Window, + cx: &mut Context, + ) { + self.duplicate(true, true, window, cx); + } + + pub fn duplicate_line_down( + &mut self, + _: &DuplicateLineDown, + window: &mut Window, + cx: &mut Context, + ) { + self.duplicate(false, true, window, cx); + } + + pub fn duplicate_selection( + &mut self, + _: &DuplicateSelection, + window: &mut Window, + cx: &mut Context, + ) { + self.duplicate(false, false, window, cx); + } + + pub fn move_line_up(&mut self, _: &MoveLineUp, window: &mut Window, cx: &mut Context) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let buffer = self.buffer.read(cx).snapshot(cx); + + let mut edits = Vec::new(); + let mut unfold_ranges = Vec::new(); + let mut refold_creases = Vec::new(); + + let selections = self.selections.all::(cx); + let mut selections = selections.iter().peekable(); + let mut contiguous_row_selections = Vec::new(); + let mut new_selections = Vec::new(); + + while let Some(selection) = selections.next() { + // Find all the selections that span a contiguous row range + let (start_row, end_row) = consume_contiguous_rows( + &mut contiguous_row_selections, + selection, + &display_map, + &mut selections, + ); + + // Move the text spanned by the row range to be before the line preceding the row range + if start_row.0 > 0 { + let range_to_move = Point::new( + start_row.previous_row().0, + buffer.line_len(start_row.previous_row()), + ) + ..Point::new( + end_row.previous_row().0, + buffer.line_len(end_row.previous_row()), + ); + let insertion_point = display_map + .prev_line_boundary(Point::new(start_row.previous_row().0, 0)) + .0; + + // Don't move lines across excerpts + if buffer + .excerpt_containing(insertion_point..range_to_move.end) + .is_some() + { + let text = buffer + .text_for_range(range_to_move.clone()) + .flat_map(|s| s.chars()) + .skip(1) + .chain(['\n']) + .collect::(); + + edits.push(( + buffer.anchor_after(range_to_move.start) + ..buffer.anchor_before(range_to_move.end), + String::new(), + )); + let insertion_anchor = buffer.anchor_after(insertion_point); + edits.push((insertion_anchor..insertion_anchor, text)); + + let row_delta = range_to_move.start.row - insertion_point.row + 1; + + // Move selections up + new_selections.extend(contiguous_row_selections.drain(..).map( + |mut selection| { + selection.start.row -= row_delta; + selection.end.row -= row_delta; + selection + }, + )); + + // Move folds up + unfold_ranges.push(range_to_move.clone()); + for fold in display_map.folds_in_range( + buffer.anchor_before(range_to_move.start) + ..buffer.anchor_after(range_to_move.end), + ) { + let mut start = fold.range.start.to_point(&buffer); + let mut end = fold.range.end.to_point(&buffer); + start.row -= row_delta; + end.row -= row_delta; + refold_creases.push(Crease::simple(start..end, fold.placeholder.clone())); + } + } + } + + // If we didn't move line(s), preserve the existing selections + new_selections.append(&mut contiguous_row_selections); + } + + self.transact(window, cx, |this, window, cx| { + this.unfold_ranges(&unfold_ranges, true, true, cx); + this.buffer.update(cx, |buffer, cx| { + for (range, text) in edits { + buffer.edit([(range, text)], None, cx); + } + }); + this.fold_creases(refold_creases, true, window, cx); + this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.select(new_selections); + }) + }); + } + + pub fn move_line_down( + &mut self, + _: &MoveLineDown, + window: &mut Window, + cx: &mut Context, + ) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let buffer = self.buffer.read(cx).snapshot(cx); + + let mut edits = Vec::new(); + let mut unfold_ranges = Vec::new(); + let mut refold_creases = Vec::new(); + + let selections = self.selections.all::(cx); + let mut selections = selections.iter().peekable(); + let mut contiguous_row_selections = Vec::new(); + let mut new_selections = Vec::new(); + + while let Some(selection) = selections.next() { + // Find all the selections that span a contiguous row range + let (start_row, end_row) = consume_contiguous_rows( + &mut contiguous_row_selections, + selection, + &display_map, + &mut selections, + ); + + // Move the text spanned by the row range to be after the last line of the row range + if end_row.0 <= buffer.max_point().row { + let range_to_move = + MultiBufferPoint::new(start_row.0, 0)..MultiBufferPoint::new(end_row.0, 0); + let insertion_point = display_map + .next_line_boundary(MultiBufferPoint::new(end_row.0, 0)) + .0; + + // Don't move lines across excerpt boundaries + if buffer + .excerpt_containing(range_to_move.start..insertion_point) + .is_some() + { + let mut text = String::from("\n"); + text.extend(buffer.text_for_range(range_to_move.clone())); + text.pop(); // Drop trailing newline + edits.push(( + buffer.anchor_after(range_to_move.start) + ..buffer.anchor_before(range_to_move.end), + String::new(), + )); + let insertion_anchor = buffer.anchor_after(insertion_point); + edits.push((insertion_anchor..insertion_anchor, text)); + + let row_delta = insertion_point.row - range_to_move.end.row + 1; + + // Move selections down + new_selections.extend(contiguous_row_selections.drain(..).map( + |mut selection| { + selection.start.row += row_delta; + selection.end.row += row_delta; + selection + }, + )); + + // Move folds down + unfold_ranges.push(range_to_move.clone()); + for fold in display_map.folds_in_range( + buffer.anchor_before(range_to_move.start) + ..buffer.anchor_after(range_to_move.end), + ) { + let mut start = fold.range.start.to_point(&buffer); + let mut end = fold.range.end.to_point(&buffer); + start.row += row_delta; + end.row += row_delta; + refold_creases.push(Crease::simple(start..end, fold.placeholder.clone())); + } + } + } + + // If we didn't move line(s), preserve the existing selections + new_selections.append(&mut contiguous_row_selections); + } + + self.transact(window, cx, |this, window, cx| { + this.unfold_ranges(&unfold_ranges, true, true, cx); + this.buffer.update(cx, |buffer, cx| { + for (range, text) in edits { + buffer.edit([(range, text)], None, cx); + } + }); + this.fold_creases(refold_creases, true, window, cx); + this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.select(new_selections) + }); + }); + } + + pub fn transpose(&mut self, _: &Transpose, window: &mut Window, cx: &mut Context) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + let text_layout_details = &self.text_layout_details(window); + self.transact(window, cx, |this, window, cx| { + let edits = this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + let mut edits: Vec<(Range, String)> = Default::default(); + s.move_with(|display_map, selection| { + if !selection.is_empty() { + return; + } + + let mut head = selection.head(); + let mut transpose_offset = head.to_offset(display_map, Bias::Right); + if head.column() == display_map.line_len(head.row()) { + transpose_offset = display_map + .buffer_snapshot + .clip_offset(transpose_offset.saturating_sub(1), Bias::Left); + } + + if transpose_offset == 0 { + return; + } + + *head.column_mut() += 1; + head = display_map.clip_point(head, Bias::Right); + let goal = SelectionGoal::HorizontalPosition( + display_map + .x_for_display_point(head, text_layout_details) + .into(), + ); + selection.collapse_to(head, goal); + + let transpose_start = display_map + .buffer_snapshot + .clip_offset(transpose_offset.saturating_sub(1), Bias::Left); + if edits.last().map_or(true, |e| e.0.end <= transpose_start) { + let transpose_end = display_map + .buffer_snapshot + .clip_offset(transpose_offset + 1, Bias::Right); + if let Some(ch) = + display_map.buffer_snapshot.chars_at(transpose_start).next() + { + edits.push((transpose_start..transpose_offset, String::new())); + edits.push((transpose_end..transpose_end, ch.to_string())); + } + } + }); + edits + }); + this.buffer + .update(cx, |buffer, cx| buffer.edit(edits, None, cx)); + let selections = this.selections.all::(cx); + this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.select(selections); + }); + }); + } + + pub fn rewrap(&mut self, _: &Rewrap, _: &mut Window, cx: &mut Context) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + self.rewrap_impl(RewrapOptions::default(), cx) + } + + pub fn rewrap_impl(&mut self, options: RewrapOptions, cx: &mut Context) { + let buffer = self.buffer.read(cx).snapshot(cx); + let selections = self.selections.all::(cx); + let mut selections = selections.iter().peekable(); + + let mut edits = Vec::new(); + let mut rewrapped_row_ranges = Vec::>::new(); + + while let Some(selection) = selections.next() { + let mut start_row = selection.start.row; + let mut end_row = selection.end.row; + + // Skip selections that overlap with a range that has already been rewrapped. + let selection_range = start_row..end_row; + if rewrapped_row_ranges + .iter() + .any(|range| range.overlaps(&selection_range)) + { + continue; + } + + let tab_size = buffer.language_settings_at(selection.head(), cx).tab_size; + + // Since not all lines in the selection may be at the same indent + // level, choose the indent size that is the most common between all + // of the lines. + // + // If there is a tie, we use the deepest indent. + let (indent_size, indent_end) = { + let mut indent_size_occurrences = HashMap::default(); + let mut rows_by_indent_size = HashMap::>::default(); + + for row in start_row..=end_row { + let indent = buffer.indent_size_for_line(MultiBufferRow(row)); + rows_by_indent_size.entry(indent).or_default().push(row); + *indent_size_occurrences.entry(indent).or_insert(0) += 1; + } + + let indent_size = indent_size_occurrences + .into_iter() + .max_by_key(|(indent, count)| (*count, indent.len_with_expanded_tabs(tab_size))) + .map(|(indent, _)| indent) + .unwrap_or_default(); + let row = rows_by_indent_size[&indent_size][0]; + let indent_end = Point::new(row, indent_size.len); + + (indent_size, indent_end) + }; + + let mut line_prefix = indent_size.chars().collect::(); + + let mut inside_comment = false; + if let Some(comment_prefix) = + buffer + .language_scope_at(selection.head()) + .and_then(|language| { + language + .line_comment_prefixes() + .iter() + .find(|prefix| buffer.contains_str_at(indent_end, prefix)) + .cloned() + }) + { + line_prefix.push_str(&comment_prefix); + inside_comment = true; + } + + let language_settings = buffer.language_settings_at(selection.head(), cx); + let allow_rewrap_based_on_language = match language_settings.allow_rewrap { + RewrapBehavior::InComments => inside_comment, + RewrapBehavior::InSelections => !selection.is_empty(), + RewrapBehavior::Anywhere => true, + }; + + let should_rewrap = options.override_language_settings + || allow_rewrap_based_on_language + || self.hard_wrap.is_some(); + if !should_rewrap { + continue; + } + + if selection.is_empty() { + 'expand_upwards: while start_row > 0 { + let prev_row = start_row - 1; + if buffer.contains_str_at(Point::new(prev_row, 0), &line_prefix) + && buffer.line_len(MultiBufferRow(prev_row)) as usize > line_prefix.len() + { + start_row = prev_row; + } else { + break 'expand_upwards; + } + } + + 'expand_downwards: while end_row < buffer.max_point().row { + let next_row = end_row + 1; + if buffer.contains_str_at(Point::new(next_row, 0), &line_prefix) + && buffer.line_len(MultiBufferRow(next_row)) as usize > line_prefix.len() + { + end_row = next_row; + } else { + break 'expand_downwards; + } + } + } + + let start = Point::new(start_row, 0); + let start_offset = start.to_offset(&buffer); + let end = Point::new(end_row, buffer.line_len(MultiBufferRow(end_row))); + let selection_text = buffer.text_for_range(start..end).collect::(); + let Some(lines_without_prefixes) = selection_text + .lines() + .map(|line| { + line.strip_prefix(&line_prefix) + .or_else(|| line.trim_start().strip_prefix(&line_prefix.trim_start())) + .ok_or_else(|| { + anyhow!("line did not start with prefix {line_prefix:?}: {line:?}") + }) + }) + .collect::, _>>() + .log_err() + else { + continue; + }; + + let wrap_column = self.hard_wrap.unwrap_or_else(|| { + buffer + .language_settings_at(Point::new(start_row, 0), cx) + .preferred_line_length as usize + }); + let wrapped_text = wrap_with_prefix( + line_prefix, + lines_without_prefixes.join("\n"), + wrap_column, + tab_size, + options.preserve_existing_whitespace, + ); + + // TODO: should always use char-based diff while still supporting cursor behavior that + // matches vim. + let mut diff_options = DiffOptions::default(); + if options.override_language_settings { + diff_options.max_word_diff_len = 0; + diff_options.max_word_diff_line_count = 0; + } else { + diff_options.max_word_diff_len = usize::MAX; + diff_options.max_word_diff_line_count = usize::MAX; + } + + for (old_range, new_text) in + text_diff_with_options(&selection_text, &wrapped_text, diff_options) + { + let edit_start = buffer.anchor_after(start_offset + old_range.start); + let edit_end = buffer.anchor_after(start_offset + old_range.end); + edits.push((edit_start..edit_end, new_text)); + } + + rewrapped_row_ranges.push(start_row..=end_row); + } + + self.buffer + .update(cx, |buffer, cx| buffer.edit(edits, None, cx)); + } + + pub fn cut_common(&mut self, window: &mut Window, cx: &mut Context) -> ClipboardItem { + let mut text = String::new(); + let buffer = self.buffer.read(cx).snapshot(cx); + let mut selections = self.selections.all::(cx); + let mut clipboard_selections = Vec::with_capacity(selections.len()); + { + let max_point = buffer.max_point(); + let mut is_first = true; + for selection in &mut selections { + let is_entire_line = selection.is_empty() || self.selections.line_mode; + if is_entire_line { + selection.start = Point::new(selection.start.row, 0); + if !selection.is_empty() && selection.end.column == 0 { + selection.end = cmp::min(max_point, selection.end); + } else { + selection.end = cmp::min(max_point, Point::new(selection.end.row + 1, 0)); + } + selection.goal = SelectionGoal::None; + } + if is_first { + is_first = false; + } else { + text += "\n"; + } + let mut len = 0; + for chunk in buffer.text_for_range(selection.start..selection.end) { + text.push_str(chunk); + len += chunk.len(); + } + clipboard_selections.push(ClipboardSelection { + len, + is_entire_line, + first_line_indent: buffer + .indent_size_for_line(MultiBufferRow(selection.start.row)) + .len, + }); + } + } + + self.transact(window, cx, |this, window, cx| { + this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.select(selections); + }); + this.insert("", window, cx); + }); + ClipboardItem::new_string_with_json_metadata(text, clipboard_selections) + } + + pub fn cut(&mut self, _: &Cut, window: &mut Window, cx: &mut Context) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + let item = self.cut_common(window, cx); + cx.write_to_clipboard(item); + } + + pub fn kill_ring_cut(&mut self, _: &KillRingCut, window: &mut Window, cx: &mut Context) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + self.change_selections(None, window, cx, |s| { + s.move_with(|snapshot, sel| { + if sel.is_empty() { + sel.end = DisplayPoint::new(sel.end.row(), snapshot.line_len(sel.end.row())) + } + }); + }); + let item = self.cut_common(window, cx); + cx.set_global(KillRing(item)) + } + + pub fn kill_ring_yank( + &mut self, + _: &KillRingYank, + window: &mut Window, + cx: &mut Context, + ) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + let (text, metadata) = if let Some(KillRing(item)) = cx.try_global() { + if let Some(ClipboardEntry::String(kill_ring)) = item.entries().first() { + (kill_ring.text().to_string(), kill_ring.metadata_json()) + } else { + return; + } + } else { + return; + }; + self.do_paste(&text, metadata, false, window, cx); + } + + pub fn copy_and_trim(&mut self, _: &CopyAndTrim, _: &mut Window, cx: &mut Context) { + self.do_copy(true, cx); + } + + pub fn copy(&mut self, _: &Copy, _: &mut Window, cx: &mut Context) { + self.do_copy(false, cx); + } + + fn do_copy(&self, strip_leading_indents: bool, cx: &mut Context) { + let selections = self.selections.all::(cx); + let buffer = self.buffer.read(cx).read(cx); + let mut text = String::new(); + + let mut clipboard_selections = Vec::with_capacity(selections.len()); + { + let max_point = buffer.max_point(); + let mut is_first = true; + for selection in &selections { + let mut start = selection.start; + let mut end = selection.end; + let is_entire_line = selection.is_empty() || self.selections.line_mode; + if is_entire_line { + start = Point::new(start.row, 0); + end = cmp::min(max_point, Point::new(end.row + 1, 0)); + } + + let mut trimmed_selections = Vec::new(); + if strip_leading_indents && end.row.saturating_sub(start.row) > 0 { + let row = MultiBufferRow(start.row); + let first_indent = buffer.indent_size_for_line(row); + if first_indent.len == 0 || start.column > first_indent.len { + trimmed_selections.push(start..end); + } else { + trimmed_selections.push( + Point::new(row.0, first_indent.len) + ..Point::new(row.0, buffer.line_len(row)), + ); + for row in start.row + 1..=end.row { + let mut line_len = buffer.line_len(MultiBufferRow(row)); + if row == end.row { + line_len = end.column; + } + if line_len == 0 { + trimmed_selections + .push(Point::new(row, 0)..Point::new(row, line_len)); + continue; + } + let row_indent_size = buffer.indent_size_for_line(MultiBufferRow(row)); + if row_indent_size.len >= first_indent.len { + trimmed_selections.push( + Point::new(row, first_indent.len)..Point::new(row, line_len), + ); + } else { + trimmed_selections.clear(); + trimmed_selections.push(start..end); + break; + } + } + } + } else { + trimmed_selections.push(start..end); + } + + for trimmed_range in trimmed_selections { + if is_first { + is_first = false; + } else { + text += "\n"; + } + let mut len = 0; + for chunk in buffer.text_for_range(trimmed_range.start..trimmed_range.end) { + text.push_str(chunk); + len += chunk.len(); + } + clipboard_selections.push(ClipboardSelection { + len, + is_entire_line, + first_line_indent: buffer + .indent_size_for_line(MultiBufferRow(trimmed_range.start.row)) + .len, + }); + } + } + } + + cx.write_to_clipboard(ClipboardItem::new_string_with_json_metadata( + text, + clipboard_selections, + )); + } + + pub fn do_paste( + &mut self, + text: &String, + clipboard_selections: Option>, + handle_entire_lines: bool, + window: &mut Window, + cx: &mut Context, + ) { + if self.read_only(cx) { + return; + } + + let clipboard_text = Cow::Borrowed(text); + + self.transact(window, cx, |this, window, cx| { + if let Some(mut clipboard_selections) = clipboard_selections { + let old_selections = this.selections.all::(cx); + let all_selections_were_entire_line = + clipboard_selections.iter().all(|s| s.is_entire_line); + let first_selection_indent_column = + clipboard_selections.first().map(|s| s.first_line_indent); + if clipboard_selections.len() != old_selections.len() { + clipboard_selections.drain(..); + } + let cursor_offset = this.selections.last::(cx).head(); + let mut auto_indent_on_paste = true; + + this.buffer.update(cx, |buffer, cx| { + let snapshot = buffer.read(cx); + auto_indent_on_paste = snapshot + .language_settings_at(cursor_offset, cx) + .auto_indent_on_paste; + + let mut start_offset = 0; + let mut edits = Vec::new(); + let mut original_indent_columns = Vec::new(); + for (ix, selection) in old_selections.iter().enumerate() { + let to_insert; + let entire_line; + let original_indent_column; + if let Some(clipboard_selection) = clipboard_selections.get(ix) { + let end_offset = start_offset + clipboard_selection.len; + to_insert = &clipboard_text[start_offset..end_offset]; + entire_line = clipboard_selection.is_entire_line; + start_offset = end_offset + 1; + original_indent_column = Some(clipboard_selection.first_line_indent); + } else { + to_insert = clipboard_text.as_str(); + entire_line = all_selections_were_entire_line; + original_indent_column = first_selection_indent_column + } + + // If the corresponding selection was empty when this slice of the + // clipboard text was written, then the entire line containing the + // selection was copied. If this selection is also currently empty, + // then paste the line before the current line of the buffer. + let range = if selection.is_empty() && handle_entire_lines && entire_line { + let column = selection.start.to_point(&snapshot).column as usize; + let line_start = selection.start - column; + line_start..line_start + } else { + selection.range() + }; + + edits.push((range, to_insert)); + original_indent_columns.push(original_indent_column); + } + drop(snapshot); + + buffer.edit( + edits, + if auto_indent_on_paste { + Some(AutoindentMode::Block { + original_indent_columns, + }) + } else { + None + }, + cx, + ); + }); + + let selections = this.selections.all::(cx); + this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.select(selections) + }); + } else { + this.insert(&clipboard_text, window, cx); + } + }); + } + + pub fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + if let Some(item) = cx.read_from_clipboard() { + let entries = item.entries(); + + match entries.first() { + // For now, we only support applying metadata if there's one string. In the future, we can incorporate all the selections + // of all the pasted entries. + Some(ClipboardEntry::String(clipboard_string)) if entries.len() == 1 => self + .do_paste( + clipboard_string.text(), + clipboard_string.metadata_json::>(), + true, + window, + cx, + ), + _ => self.do_paste(&item.text().unwrap_or_default(), None, true, window, cx), + } + } + } + + pub fn undo(&mut self, _: &Undo, window: &mut Window, cx: &mut Context) { + if self.read_only(cx) { + return; + } + + self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + + if let Some(transaction_id) = self.buffer.update(cx, |buffer, cx| buffer.undo(cx)) { + if let Some((selections, _)) = + self.selection_history.transaction(transaction_id).cloned() + { + self.change_selections(None, window, cx, |s| { + s.select_anchors(selections.to_vec()); + }); + } else { + log::error!( + "No entry in selection_history found for undo. \ + This may correspond to a bug where undo does not update the selection. \ + If this is occurring, please add details to \ + https://github.com/zed-industries/zed/issues/22692" + ); + } + self.request_autoscroll(Autoscroll::fit(), cx); + self.unmark_text(window, cx); + self.refresh_inline_completion(true, false, window, cx); + cx.emit(EditorEvent::Edited { transaction_id }); + cx.emit(EditorEvent::TransactionUndone { transaction_id }); + } + } + + pub fn redo(&mut self, _: &Redo, window: &mut Window, cx: &mut Context) { + if self.read_only(cx) { + return; + } + + self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + + if let Some(transaction_id) = self.buffer.update(cx, |buffer, cx| buffer.redo(cx)) { + if let Some((_, Some(selections))) = + self.selection_history.transaction(transaction_id).cloned() + { + self.change_selections(None, window, cx, |s| { + s.select_anchors(selections.to_vec()); + }); + } else { + log::error!( + "No entry in selection_history found for redo. \ + This may correspond to a bug where undo does not update the selection. \ + If this is occurring, please add details to \ + https://github.com/zed-industries/zed/issues/22692" + ); + } + self.request_autoscroll(Autoscroll::fit(), cx); + self.unmark_text(window, cx); + self.refresh_inline_completion(true, false, window, cx); + cx.emit(EditorEvent::Edited { transaction_id }); + } + } + + pub fn finalize_last_transaction(&mut self, cx: &mut Context) { + self.buffer + .update(cx, |buffer, cx| buffer.finalize_last_transaction(cx)); + } + + pub fn group_until_transaction(&mut self, tx_id: TransactionId, cx: &mut Context) { + self.buffer + .update(cx, |buffer, cx| buffer.group_until_transaction(tx_id, cx)); + } + + pub fn move_left(&mut self, _: &MoveLeft, window: &mut Window, cx: &mut Context) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.move_with(|map, selection| { + let cursor = if selection.is_empty() { + movement::left(map, selection.start) + } else { + selection.start + }; + selection.collapse_to(cursor, SelectionGoal::None); + }); + }) + } + + pub fn select_left(&mut self, _: &SelectLeft, window: &mut Window, cx: &mut Context) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.move_heads_with(|map, head, _| (movement::left(map, head), SelectionGoal::None)); + }) + } + + pub fn move_right(&mut self, _: &MoveRight, window: &mut Window, cx: &mut Context) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.move_with(|map, selection| { + let cursor = if selection.is_empty() { + movement::right(map, selection.end) + } else { + selection.end + }; + selection.collapse_to(cursor, SelectionGoal::None) + }); + }) + } + + pub fn select_right(&mut self, _: &SelectRight, window: &mut Window, cx: &mut Context) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.move_heads_with(|map, head, _| (movement::right(map, head), SelectionGoal::None)); + }) + } + + pub fn move_up(&mut self, _: &MoveUp, window: &mut Window, cx: &mut Context) { + if self.take_rename(true, window, cx).is_some() { + return; + } + + if matches!(self.mode, EditorMode::SingleLine { .. }) { + cx.propagate(); + return; + } + + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + + let text_layout_details = &self.text_layout_details(window); + let selection_count = self.selections.count(); + let first_selection = self.selections.first_anchor(); + + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.move_with(|map, selection| { + if !selection.is_empty() { + selection.goal = SelectionGoal::None; + } + let (cursor, goal) = movement::up( + map, + selection.start, + selection.goal, + false, + text_layout_details, + ); + selection.collapse_to(cursor, goal); + }); + }); + + if selection_count == 1 && first_selection.range() == self.selections.first_anchor().range() + { + cx.propagate(); + } + } + + pub fn move_up_by_lines( + &mut self, + action: &MoveUpByLines, + window: &mut Window, + cx: &mut Context, + ) { + if self.take_rename(true, window, cx).is_some() { + return; + } + + if matches!(self.mode, EditorMode::SingleLine { .. }) { + cx.propagate(); + return; + } + + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + + let text_layout_details = &self.text_layout_details(window); + + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.move_with(|map, selection| { + if !selection.is_empty() { + selection.goal = SelectionGoal::None; + } + let (cursor, goal) = movement::up_by_rows( + map, + selection.start, + action.lines, + selection.goal, + false, + text_layout_details, + ); + selection.collapse_to(cursor, goal); + }); + }) + } + + pub fn move_down_by_lines( + &mut self, + action: &MoveDownByLines, + window: &mut Window, + cx: &mut Context, + ) { + if self.take_rename(true, window, cx).is_some() { + return; + } + + if matches!(self.mode, EditorMode::SingleLine { .. }) { + cx.propagate(); + return; + } + + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + + let text_layout_details = &self.text_layout_details(window); + + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.move_with(|map, selection| { + if !selection.is_empty() { + selection.goal = SelectionGoal::None; + } + let (cursor, goal) = movement::down_by_rows( + map, + selection.start, + action.lines, + selection.goal, + false, + text_layout_details, + ); + selection.collapse_to(cursor, goal); + }); + }) + } + + pub fn select_down_by_lines( + &mut self, + action: &SelectDownByLines, + window: &mut Window, + cx: &mut Context, + ) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + let text_layout_details = &self.text_layout_details(window); + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.move_heads_with(|map, head, goal| { + movement::down_by_rows(map, head, action.lines, goal, false, text_layout_details) + }) + }) + } + + pub fn select_up_by_lines( + &mut self, + action: &SelectUpByLines, + window: &mut Window, + cx: &mut Context, + ) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + let text_layout_details = &self.text_layout_details(window); + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.move_heads_with(|map, head, goal| { + movement::up_by_rows(map, head, action.lines, goal, false, text_layout_details) + }) + }) + } + + pub fn select_page_up( + &mut self, + _: &SelectPageUp, + window: &mut Window, + cx: &mut Context, + ) { + let Some(row_count) = self.visible_row_count() else { + return; + }; + + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + + let text_layout_details = &self.text_layout_details(window); + + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.move_heads_with(|map, head, goal| { + movement::up_by_rows(map, head, row_count, goal, false, text_layout_details) + }) + }) + } + + pub fn move_page_up( + &mut self, + action: &MovePageUp, + window: &mut Window, + cx: &mut Context, + ) { + if self.take_rename(true, window, cx).is_some() { + return; + } + + if self + .context_menu + .borrow_mut() + .as_mut() + .map(|menu| menu.select_first(self.completion_provider.as_deref(), cx)) + .unwrap_or(false) + { + return; + } + + if matches!(self.mode, EditorMode::SingleLine { .. }) { + cx.propagate(); + return; + } + + let Some(row_count) = self.visible_row_count() else { + return; + }; + + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + + let autoscroll = if action.center_cursor { + Autoscroll::center() + } else { + Autoscroll::fit() + }; + + let text_layout_details = &self.text_layout_details(window); + + self.change_selections(Some(autoscroll), window, cx, |s| { + s.move_with(|map, selection| { + if !selection.is_empty() { + selection.goal = SelectionGoal::None; + } + let (cursor, goal) = movement::up_by_rows( + map, + selection.end, + row_count, + selection.goal, + false, + text_layout_details, + ); + selection.collapse_to(cursor, goal); + }); + }); + } + + pub fn select_up(&mut self, _: &SelectUp, window: &mut Window, cx: &mut Context) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + let text_layout_details = &self.text_layout_details(window); + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.move_heads_with(|map, head, goal| { + movement::up(map, head, goal, false, text_layout_details) + }) + }) + } + + pub fn move_down(&mut self, _: &MoveDown, window: &mut Window, cx: &mut Context) { + self.take_rename(true, window, cx); + + if matches!(self.mode, EditorMode::SingleLine { .. }) { + cx.propagate(); + return; + } + + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + + let text_layout_details = &self.text_layout_details(window); + let selection_count = self.selections.count(); + let first_selection = self.selections.first_anchor(); + + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.move_with(|map, selection| { + if !selection.is_empty() { + selection.goal = SelectionGoal::None; + } + let (cursor, goal) = movement::down( + map, + selection.end, + selection.goal, + false, + text_layout_details, + ); + selection.collapse_to(cursor, goal); + }); + }); + + if selection_count == 1 && first_selection.range() == self.selections.first_anchor().range() + { + cx.propagate(); + } + } + + pub fn select_page_down( + &mut self, + _: &SelectPageDown, + window: &mut Window, + cx: &mut Context, + ) { + let Some(row_count) = self.visible_row_count() else { + return; + }; + + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + + let text_layout_details = &self.text_layout_details(window); + + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.move_heads_with(|map, head, goal| { + movement::down_by_rows(map, head, row_count, goal, false, text_layout_details) + }) + }) + } + + pub fn move_page_down( + &mut self, + action: &MovePageDown, + window: &mut Window, + cx: &mut Context, + ) { + if self.take_rename(true, window, cx).is_some() { + return; + } + + if self + .context_menu + .borrow_mut() + .as_mut() + .map(|menu| menu.select_last(self.completion_provider.as_deref(), cx)) + .unwrap_or(false) + { + return; + } + + if matches!(self.mode, EditorMode::SingleLine { .. }) { + cx.propagate(); + return; + } + + let Some(row_count) = self.visible_row_count() else { + return; + }; + + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + + let autoscroll = if action.center_cursor { + Autoscroll::center() + } else { + Autoscroll::fit() + }; + + let text_layout_details = &self.text_layout_details(window); + self.change_selections(Some(autoscroll), window, cx, |s| { + s.move_with(|map, selection| { + if !selection.is_empty() { + selection.goal = SelectionGoal::None; + } + let (cursor, goal) = movement::down_by_rows( + map, + selection.end, + row_count, + selection.goal, + false, + text_layout_details, + ); + selection.collapse_to(cursor, goal); + }); + }); + } + + pub fn select_down(&mut self, _: &SelectDown, window: &mut Window, cx: &mut Context) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + let text_layout_details = &self.text_layout_details(window); + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.move_heads_with(|map, head, goal| { + movement::down(map, head, goal, false, text_layout_details) + }) + }); + } + + pub fn context_menu_first( + &mut self, + _: &ContextMenuFirst, + _window: &mut Window, + cx: &mut Context, + ) { + if let Some(context_menu) = self.context_menu.borrow_mut().as_mut() { + context_menu.select_first(self.completion_provider.as_deref(), cx); + } + } + + pub fn context_menu_prev( + &mut self, + _: &ContextMenuPrevious, + _window: &mut Window, + cx: &mut Context, + ) { + if let Some(context_menu) = self.context_menu.borrow_mut().as_mut() { + context_menu.select_prev(self.completion_provider.as_deref(), cx); + } + } + + pub fn context_menu_next( + &mut self, + _: &ContextMenuNext, + _window: &mut Window, + cx: &mut Context, + ) { + if let Some(context_menu) = self.context_menu.borrow_mut().as_mut() { + context_menu.select_next(self.completion_provider.as_deref(), cx); + } + } + + pub fn context_menu_last( + &mut self, + _: &ContextMenuLast, + _window: &mut Window, + cx: &mut Context, + ) { + if let Some(context_menu) = self.context_menu.borrow_mut().as_mut() { + context_menu.select_last(self.completion_provider.as_deref(), cx); + } + } + + pub fn move_to_previous_word_start( + &mut self, + _: &MoveToPreviousWordStart, + window: &mut Window, + cx: &mut Context, + ) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.move_cursors_with(|map, head, _| { + ( + movement::previous_word_start(map, head), + SelectionGoal::None, + ) + }); + }) + } + + pub fn move_to_previous_subword_start( + &mut self, + _: &MoveToPreviousSubwordStart, + window: &mut Window, + cx: &mut Context, + ) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.move_cursors_with(|map, head, _| { + ( + movement::previous_subword_start(map, head), + SelectionGoal::None, + ) + }); + }) + } + + pub fn select_to_previous_word_start( + &mut self, + _: &SelectToPreviousWordStart, + window: &mut Window, + cx: &mut Context, + ) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.move_heads_with(|map, head, _| { + ( + movement::previous_word_start(map, head), + SelectionGoal::None, + ) + }); + }) + } + + pub fn select_to_previous_subword_start( + &mut self, + _: &SelectToPreviousSubwordStart, + window: &mut Window, + cx: &mut Context, + ) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.move_heads_with(|map, head, _| { + ( + movement::previous_subword_start(map, head), + SelectionGoal::None, + ) + }); + }) + } + + pub fn delete_to_previous_word_start( + &mut self, + action: &DeleteToPreviousWordStart, + window: &mut Window, + cx: &mut Context, + ) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + self.transact(window, cx, |this, window, cx| { + this.select_autoclose_pair(window, cx); + this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.move_with(|map, selection| { + if selection.is_empty() { + let cursor = if action.ignore_newlines { + movement::previous_word_start(map, selection.head()) + } else { + movement::previous_word_start_or_newline(map, selection.head()) + }; + selection.set_head(cursor, SelectionGoal::None); + } + }); + }); + this.insert("", window, cx); + }); + } + + pub fn delete_to_previous_subword_start( + &mut self, + _: &DeleteToPreviousSubwordStart, + window: &mut Window, + cx: &mut Context, + ) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + self.transact(window, cx, |this, window, cx| { + this.select_autoclose_pair(window, cx); + this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.move_with(|map, selection| { + if selection.is_empty() { + let cursor = movement::previous_subword_start(map, selection.head()); + selection.set_head(cursor, SelectionGoal::None); + } + }); + }); + this.insert("", window, cx); + }); + } + + pub fn move_to_next_word_end( + &mut self, + _: &MoveToNextWordEnd, + window: &mut Window, + cx: &mut Context, + ) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.move_cursors_with(|map, head, _| { + (movement::next_word_end(map, head), SelectionGoal::None) + }); + }) + } + + pub fn move_to_next_subword_end( + &mut self, + _: &MoveToNextSubwordEnd, + window: &mut Window, + cx: &mut Context, + ) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.move_cursors_with(|map, head, _| { + (movement::next_subword_end(map, head), SelectionGoal::None) + }); + }) + } + + pub fn select_to_next_word_end( + &mut self, + _: &SelectToNextWordEnd, + window: &mut Window, + cx: &mut Context, + ) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.move_heads_with(|map, head, _| { + (movement::next_word_end(map, head), SelectionGoal::None) + }); + }) + } + + pub fn select_to_next_subword_end( + &mut self, + _: &SelectToNextSubwordEnd, + window: &mut Window, + cx: &mut Context, + ) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.move_heads_with(|map, head, _| { + (movement::next_subword_end(map, head), SelectionGoal::None) + }); + }) + } + + pub fn delete_to_next_word_end( + &mut self, + action: &DeleteToNextWordEnd, + window: &mut Window, + cx: &mut Context, + ) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + self.transact(window, cx, |this, window, cx| { + this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.move_with(|map, selection| { + if selection.is_empty() { + let cursor = if action.ignore_newlines { + movement::next_word_end(map, selection.head()) + } else { + movement::next_word_end_or_newline(map, selection.head()) + }; + selection.set_head(cursor, SelectionGoal::None); + } + }); + }); + this.insert("", window, cx); + }); + } + + pub fn delete_to_next_subword_end( + &mut self, + _: &DeleteToNextSubwordEnd, + window: &mut Window, + cx: &mut Context, + ) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + self.transact(window, cx, |this, window, cx| { + this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.move_with(|map, selection| { + if selection.is_empty() { + let cursor = movement::next_subword_end(map, selection.head()); + selection.set_head(cursor, SelectionGoal::None); + } + }); + }); + this.insert("", window, cx); + }); + } + + pub fn move_to_beginning_of_line( + &mut self, + action: &MoveToBeginningOfLine, + window: &mut Window, + cx: &mut Context, + ) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.move_cursors_with(|map, head, _| { + ( + movement::indented_line_beginning( + map, + head, + action.stop_at_soft_wraps, + action.stop_at_indent, + ), + SelectionGoal::None, + ) + }); + }) + } + + pub fn select_to_beginning_of_line( + &mut self, + action: &SelectToBeginningOfLine, + window: &mut Window, + cx: &mut Context, + ) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.move_heads_with(|map, head, _| { + ( + movement::indented_line_beginning( + map, + head, + action.stop_at_soft_wraps, + action.stop_at_indent, + ), + SelectionGoal::None, + ) + }); + }); + } + + pub fn delete_to_beginning_of_line( + &mut self, + action: &DeleteToBeginningOfLine, + window: &mut Window, + cx: &mut Context, + ) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + self.transact(window, cx, |this, window, cx| { + this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.move_with(|_, selection| { + selection.reversed = true; + }); + }); + + this.select_to_beginning_of_line( + &SelectToBeginningOfLine { + stop_at_soft_wraps: false, + stop_at_indent: action.stop_at_indent, + }, + window, + cx, + ); + this.backspace(&Backspace, window, cx); + }); + } + + pub fn move_to_end_of_line( + &mut self, + action: &MoveToEndOfLine, + window: &mut Window, + cx: &mut Context, + ) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.move_cursors_with(|map, head, _| { + ( + movement::line_end(map, head, action.stop_at_soft_wraps), + SelectionGoal::None, + ) + }); + }) + } + + pub fn select_to_end_of_line( + &mut self, + action: &SelectToEndOfLine, + window: &mut Window, + cx: &mut Context, + ) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.move_heads_with(|map, head, _| { + ( + movement::line_end(map, head, action.stop_at_soft_wraps), + SelectionGoal::None, + ) + }); + }) + } + + pub fn delete_to_end_of_line( + &mut self, + _: &DeleteToEndOfLine, + window: &mut Window, + cx: &mut Context, + ) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + self.transact(window, cx, |this, window, cx| { + this.select_to_end_of_line( + &SelectToEndOfLine { + stop_at_soft_wraps: false, + }, + window, + cx, + ); + this.delete(&Delete, window, cx); + }); + } + + pub fn cut_to_end_of_line( + &mut self, + _: &CutToEndOfLine, + window: &mut Window, + cx: &mut Context, + ) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + self.transact(window, cx, |this, window, cx| { + this.select_to_end_of_line( + &SelectToEndOfLine { + stop_at_soft_wraps: false, + }, + window, + cx, + ); + this.cut(&Cut, window, cx); + }); + } + + pub fn move_to_start_of_paragraph( + &mut self, + _: &MoveToStartOfParagraph, + window: &mut Window, + cx: &mut Context, + ) { + if matches!(self.mode, EditorMode::SingleLine { .. }) { + cx.propagate(); + return; + } + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.move_with(|map, selection| { + selection.collapse_to( + movement::start_of_paragraph(map, selection.head(), 1), + SelectionGoal::None, + ) + }); + }) + } + + pub fn move_to_end_of_paragraph( + &mut self, + _: &MoveToEndOfParagraph, + window: &mut Window, + cx: &mut Context, + ) { + if matches!(self.mode, EditorMode::SingleLine { .. }) { + cx.propagate(); + return; + } + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.move_with(|map, selection| { + selection.collapse_to( + movement::end_of_paragraph(map, selection.head(), 1), + SelectionGoal::None, + ) + }); + }) + } + + pub fn select_to_start_of_paragraph( + &mut self, + _: &SelectToStartOfParagraph, + window: &mut Window, + cx: &mut Context, + ) { + if matches!(self.mode, EditorMode::SingleLine { .. }) { + cx.propagate(); + return; + } + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.move_heads_with(|map, head, _| { + ( + movement::start_of_paragraph(map, head, 1), + SelectionGoal::None, + ) + }); + }) + } + + pub fn select_to_end_of_paragraph( + &mut self, + _: &SelectToEndOfParagraph, + window: &mut Window, + cx: &mut Context, + ) { + if matches!(self.mode, EditorMode::SingleLine { .. }) { + cx.propagate(); + return; + } + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.move_heads_with(|map, head, _| { + ( + movement::end_of_paragraph(map, head, 1), + SelectionGoal::None, + ) + }); + }) + } + + pub fn move_to_start_of_excerpt( + &mut self, + _: &MoveToStartOfExcerpt, + window: &mut Window, + cx: &mut Context, + ) { + if matches!(self.mode, EditorMode::SingleLine { .. }) { + cx.propagate(); + return; + } + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.move_with(|map, selection| { + selection.collapse_to( + movement::start_of_excerpt( + map, + selection.head(), + workspace::searchable::Direction::Prev, + ), + SelectionGoal::None, + ) + }); + }) + } + + pub fn move_to_start_of_next_excerpt( + &mut self, + _: &MoveToStartOfNextExcerpt, + window: &mut Window, + cx: &mut Context, + ) { + if matches!(self.mode, EditorMode::SingleLine { .. }) { + cx.propagate(); + return; + } + + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.move_with(|map, selection| { + selection.collapse_to( + movement::start_of_excerpt( + map, + selection.head(), + workspace::searchable::Direction::Next, + ), + SelectionGoal::None, + ) + }); + }) + } + + pub fn move_to_end_of_excerpt( + &mut self, + _: &MoveToEndOfExcerpt, + window: &mut Window, + cx: &mut Context, + ) { + if matches!(self.mode, EditorMode::SingleLine { .. }) { + cx.propagate(); + return; + } + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.move_with(|map, selection| { + selection.collapse_to( + movement::end_of_excerpt( + map, + selection.head(), + workspace::searchable::Direction::Next, + ), + SelectionGoal::None, + ) + }); + }) + } + + pub fn move_to_end_of_previous_excerpt( + &mut self, + _: &MoveToEndOfPreviousExcerpt, + window: &mut Window, + cx: &mut Context, + ) { + if matches!(self.mode, EditorMode::SingleLine { .. }) { + cx.propagate(); + return; + } + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.move_with(|map, selection| { + selection.collapse_to( + movement::end_of_excerpt( + map, + selection.head(), + workspace::searchable::Direction::Prev, + ), + SelectionGoal::None, + ) + }); + }) + } + + pub fn select_to_start_of_excerpt( + &mut self, + _: &SelectToStartOfExcerpt, + window: &mut Window, + cx: &mut Context, + ) { + if matches!(self.mode, EditorMode::SingleLine { .. }) { + cx.propagate(); + return; + } + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.move_heads_with(|map, head, _| { + ( + movement::start_of_excerpt(map, head, workspace::searchable::Direction::Prev), + SelectionGoal::None, + ) + }); + }) + } + + pub fn select_to_start_of_next_excerpt( + &mut self, + _: &SelectToStartOfNextExcerpt, + window: &mut Window, + cx: &mut Context, + ) { + if matches!(self.mode, EditorMode::SingleLine { .. }) { + cx.propagate(); + return; + } + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.move_heads_with(|map, head, _| { + ( + movement::start_of_excerpt(map, head, workspace::searchable::Direction::Next), + SelectionGoal::None, + ) + }); + }) + } + + pub fn select_to_end_of_excerpt( + &mut self, + _: &SelectToEndOfExcerpt, + window: &mut Window, + cx: &mut Context, + ) { + if matches!(self.mode, EditorMode::SingleLine { .. }) { + cx.propagate(); + return; + } + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.move_heads_with(|map, head, _| { + ( + movement::end_of_excerpt(map, head, workspace::searchable::Direction::Next), + SelectionGoal::None, + ) + }); + }) + } + + pub fn select_to_end_of_previous_excerpt( + &mut self, + _: &SelectToEndOfPreviousExcerpt, + window: &mut Window, + cx: &mut Context, + ) { + if matches!(self.mode, EditorMode::SingleLine { .. }) { + cx.propagate(); + return; + } + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.move_heads_with(|map, head, _| { + ( + movement::end_of_excerpt(map, head, workspace::searchable::Direction::Prev), + SelectionGoal::None, + ) + }); + }) + } + + pub fn move_to_beginning( + &mut self, + _: &MoveToBeginning, + window: &mut Window, + cx: &mut Context, + ) { + if matches!(self.mode, EditorMode::SingleLine { .. }) { + cx.propagate(); + return; + } + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.select_ranges(vec![0..0]); + }); + } + + pub fn select_to_beginning( + &mut self, + _: &SelectToBeginning, + window: &mut Window, + cx: &mut Context, + ) { + let mut selection = self.selections.last::(cx); + selection.set_head(Point::zero(), SelectionGoal::None); + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.select(vec![selection]); + }); + } + + pub fn move_to_end(&mut self, _: &MoveToEnd, window: &mut Window, cx: &mut Context) { + if matches!(self.mode, EditorMode::SingleLine { .. }) { + cx.propagate(); + return; + } + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + let cursor = self.buffer.read(cx).read(cx).len(); + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.select_ranges(vec![cursor..cursor]) + }); + } + + pub fn set_nav_history(&mut self, nav_history: Option) { + self.nav_history = nav_history; + } + + pub fn nav_history(&self) -> Option<&ItemNavHistory> { + self.nav_history.as_ref() + } + + pub fn create_nav_history_entry(&mut self, cx: &mut Context) { + self.push_to_nav_history(self.selections.newest_anchor().head(), None, false, cx); + } + + fn push_to_nav_history( + &mut self, + cursor_anchor: Anchor, + new_position: Option, + is_deactivate: bool, + cx: &mut Context, + ) { + if let Some(nav_history) = self.nav_history.as_mut() { + let buffer = self.buffer.read(cx).read(cx); + let cursor_position = cursor_anchor.to_point(&buffer); + let scroll_state = self.scroll_manager.anchor(); + let scroll_top_row = scroll_state.top_row(&buffer); + drop(buffer); + + if let Some(new_position) = new_position { + let row_delta = (new_position.row as i64 - cursor_position.row as i64).abs(); + if row_delta < MIN_NAVIGATION_HISTORY_ROW_DELTA { + return; + } + } + + nav_history.push( + Some(NavigationData { + cursor_anchor, + cursor_position, + scroll_anchor: scroll_state, + scroll_top_row, + }), + cx, + ); + cx.emit(EditorEvent::PushedToNavHistory { + anchor: cursor_anchor, + is_deactivate, + }) + } + } + + pub fn select_to_end(&mut self, _: &SelectToEnd, window: &mut Window, cx: &mut Context) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + let buffer = self.buffer.read(cx).snapshot(cx); + let mut selection = self.selections.first::(cx); + selection.set_head(buffer.len(), SelectionGoal::None); + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.select(vec![selection]); + }); + } + + pub fn select_all(&mut self, _: &SelectAll, window: &mut Window, cx: &mut Context) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + let end = self.buffer.read(cx).read(cx).len(); + self.change_selections(None, window, cx, |s| { + s.select_ranges(vec![0..end]); + }); + } + + pub fn select_line(&mut self, _: &SelectLine, window: &mut Window, cx: &mut Context) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let mut selections = self.selections.all::(cx); + let max_point = display_map.buffer_snapshot.max_point(); + for selection in &mut selections { + let rows = selection.spanned_rows(true, &display_map); + selection.start = Point::new(rows.start.0, 0); + selection.end = cmp::min(max_point, Point::new(rows.end.0, 0)); + selection.reversed = false; + } + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.select(selections); + }); + } + + pub fn split_selection_into_lines( + &mut self, + _: &SplitSelectionIntoLines, + window: &mut Window, + cx: &mut Context, + ) { + let selections = self + .selections + .all::(cx) + .into_iter() + .map(|selection| selection.start..selection.end) + .collect::>(); + self.unfold_ranges(&selections, true, true, cx); + + let mut new_selection_ranges = Vec::new(); + { + let buffer = self.buffer.read(cx).read(cx); + for selection in selections { + for row in selection.start.row..selection.end.row { + let cursor = Point::new(row, buffer.line_len(MultiBufferRow(row))); + new_selection_ranges.push(cursor..cursor); + } + + let is_multiline_selection = selection.start.row != selection.end.row; + // Don't insert last one if it's a multi-line selection ending at the start of a line, + // so this action feels more ergonomic when paired with other selection operations + let should_skip_last = is_multiline_selection && selection.end.column == 0; + if !should_skip_last { + new_selection_ranges.push(selection.end..selection.end); + } + } + } + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.select_ranges(new_selection_ranges); + }); + } + + pub fn add_selection_above( + &mut self, + _: &AddSelectionAbove, + window: &mut Window, + cx: &mut Context, + ) { + self.add_selection(true, window, cx); + } + + pub fn add_selection_below( + &mut self, + _: &AddSelectionBelow, + window: &mut Window, + cx: &mut Context, + ) { + self.add_selection(false, window, cx); + } + + fn add_selection(&mut self, above: bool, window: &mut Window, cx: &mut Context) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let mut selections = self.selections.all::(cx); + let text_layout_details = self.text_layout_details(window); + let mut state = self.add_selections_state.take().unwrap_or_else(|| { + let oldest_selection = selections.iter().min_by_key(|s| s.id).unwrap().clone(); + let range = oldest_selection.display_range(&display_map).sorted(); + + let start_x = display_map.x_for_display_point(range.start, &text_layout_details); + let end_x = display_map.x_for_display_point(range.end, &text_layout_details); + let positions = start_x.min(end_x)..start_x.max(end_x); + + selections.clear(); + let mut stack = Vec::new(); + for row in range.start.row().0..=range.end.row().0 { + if let Some(selection) = self.selections.build_columnar_selection( + &display_map, + DisplayRow(row), + &positions, + oldest_selection.reversed, + &text_layout_details, + ) { + stack.push(selection.id); + selections.push(selection); + } + } + + if above { + stack.reverse(); + } + + AddSelectionsState { above, stack } + }); + + let last_added_selection = *state.stack.last().unwrap(); + let mut new_selections = Vec::new(); + if above == state.above { + let end_row = if above { + DisplayRow(0) + } else { + display_map.max_point().row() + }; + + 'outer: for selection in selections { + if selection.id == last_added_selection { + let range = selection.display_range(&display_map).sorted(); + debug_assert_eq!(range.start.row(), range.end.row()); + let mut row = range.start.row(); + let positions = + if let SelectionGoal::HorizontalRange { start, end } = selection.goal { + px(start)..px(end) + } else { + let start_x = + display_map.x_for_display_point(range.start, &text_layout_details); + let end_x = + display_map.x_for_display_point(range.end, &text_layout_details); + start_x.min(end_x)..start_x.max(end_x) + }; + + while row != end_row { + if above { + row.0 -= 1; + } else { + row.0 += 1; + } + + if let Some(new_selection) = self.selections.build_columnar_selection( + &display_map, + row, + &positions, + selection.reversed, + &text_layout_details, + ) { + state.stack.push(new_selection.id); + if above { + new_selections.push(new_selection); + new_selections.push(selection); + } else { + new_selections.push(selection); + new_selections.push(new_selection); + } + + continue 'outer; + } + } + } + + new_selections.push(selection); + } + } else { + new_selections = selections; + new_selections.retain(|s| s.id != last_added_selection); + state.stack.pop(); + } + + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.select(new_selections); + }); + if state.stack.len() > 1 { + self.add_selections_state = Some(state); + } + } + + pub fn select_next_match_internal( + &mut self, + display_map: &DisplaySnapshot, + replace_newest: bool, + autoscroll: Option, + window: &mut Window, + cx: &mut Context, + ) -> Result<()> { + fn select_next_match_ranges( + this: &mut Editor, + range: Range, + reversed: bool, + replace_newest: bool, + auto_scroll: Option, + window: &mut Window, + cx: &mut Context, + ) { + this.unfold_ranges(&[range.clone()], false, auto_scroll.is_some(), cx); + this.change_selections(auto_scroll, window, cx, |s| { + if replace_newest { + s.delete(s.newest_anchor().id); + } + if reversed { + s.insert_range(range.end..range.start); + } else { + s.insert_range(range); + } + }); + } + + let buffer = &display_map.buffer_snapshot; + let mut selections = self.selections.all::(cx); + if let Some(mut select_next_state) = self.select_next_state.take() { + let query = &select_next_state.query; + if !select_next_state.done { + let first_selection = selections.iter().min_by_key(|s| s.id).unwrap(); + let last_selection = selections.iter().max_by_key(|s| s.id).unwrap(); + let mut next_selected_range = None; + + let bytes_after_last_selection = + buffer.bytes_in_range(last_selection.end..buffer.len()); + let bytes_before_first_selection = buffer.bytes_in_range(0..first_selection.start); + let query_matches = query + .stream_find_iter(bytes_after_last_selection) + .map(|result| (last_selection.end, result)) + .chain( + query + .stream_find_iter(bytes_before_first_selection) + .map(|result| (0, result)), + ); + + for (start_offset, query_match) in query_matches { + let query_match = query_match.unwrap(); // can only fail due to I/O + let offset_range = + start_offset + query_match.start()..start_offset + query_match.end(); + let display_range = offset_range.start.to_display_point(display_map) + ..offset_range.end.to_display_point(display_map); + + if !select_next_state.wordwise + || (!movement::is_inside_word(display_map, display_range.start) + && !movement::is_inside_word(display_map, display_range.end)) + { + // TODO: This is n^2, because we might check all the selections + if !selections + .iter() + .any(|selection| selection.range().overlaps(&offset_range)) + { + next_selected_range = Some(offset_range); + break; + } + } + } + + if let Some(next_selected_range) = next_selected_range { + select_next_match_ranges( + self, + next_selected_range, + last_selection.reversed, + replace_newest, + autoscroll, + window, + cx, + ); + } else { + select_next_state.done = true; + } + } + + self.select_next_state = Some(select_next_state); + } else { + let mut only_carets = true; + let mut same_text_selected = true; + let mut selected_text = None; + + let mut selections_iter = selections.iter().peekable(); + while let Some(selection) = selections_iter.next() { + if selection.start != selection.end { + only_carets = false; + } + + if same_text_selected { + if selected_text.is_none() { + selected_text = + Some(buffer.text_for_range(selection.range()).collect::()); + } + + if let Some(next_selection) = selections_iter.peek() { + if next_selection.range().len() == selection.range().len() { + let next_selected_text = buffer + .text_for_range(next_selection.range()) + .collect::(); + if Some(next_selected_text) != selected_text { + same_text_selected = false; + selected_text = None; + } + } else { + same_text_selected = false; + selected_text = None; + } + } + } + } + + if only_carets { + for selection in &mut selections { + let word_range = movement::surrounding_word( + display_map, + selection.start.to_display_point(display_map), + ); + selection.start = word_range.start.to_offset(display_map, Bias::Left); + selection.end = word_range.end.to_offset(display_map, Bias::Left); + selection.goal = SelectionGoal::None; + selection.reversed = false; + select_next_match_ranges( + self, + selection.start..selection.end, + selection.reversed, + replace_newest, + autoscroll, + window, + cx, + ); + } + + if selections.len() == 1 { + let selection = selections + .last() + .expect("ensured that there's only one selection"); + let query = buffer + .text_for_range(selection.start..selection.end) + .collect::(); + let is_empty = query.is_empty(); + let select_state = SelectNextState { + query: AhoCorasick::new(&[query])?, + wordwise: true, + done: is_empty, + }; + self.select_next_state = Some(select_state); + } else { + self.select_next_state = None; + } + } else if let Some(selected_text) = selected_text { + self.select_next_state = Some(SelectNextState { + query: AhoCorasick::new(&[selected_text])?, + wordwise: false, + done: false, + }); + self.select_next_match_internal( + display_map, + replace_newest, + autoscroll, + window, + cx, + )?; + } + } + Ok(()) + } + + pub fn select_all_matches( + &mut self, + _action: &SelectAllMatches, + window: &mut Window, + cx: &mut Context, + ) -> Result<()> { + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + + self.push_to_selection_history(); + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + + self.select_next_match_internal(&display_map, false, None, window, cx)?; + let Some(select_next_state) = self.select_next_state.as_mut() else { + return Ok(()); + }; + if select_next_state.done { + return Ok(()); + } + + let mut new_selections = Vec::new(); + + let reversed = self.selections.oldest::(cx).reversed; + let buffer = &display_map.buffer_snapshot; + let query_matches = select_next_state + .query + .stream_find_iter(buffer.bytes_in_range(0..buffer.len())); + + for query_match in query_matches.into_iter() { + let query_match = query_match.context("query match for select all action")?; // can only fail due to I/O + let offset_range = if reversed { + query_match.end()..query_match.start() + } else { + query_match.start()..query_match.end() + }; + let display_range = offset_range.start.to_display_point(&display_map) + ..offset_range.end.to_display_point(&display_map); + + if !select_next_state.wordwise + || (!movement::is_inside_word(&display_map, display_range.start) + && !movement::is_inside_word(&display_map, display_range.end)) + { + new_selections.push(offset_range.start..offset_range.end); + } + } + + select_next_state.done = true; + self.unfold_ranges(&new_selections.clone(), false, false, cx); + self.change_selections(None, window, cx, |selections| { + selections.select_ranges(new_selections) + }); + + Ok(()) + } + + pub fn select_next( + &mut self, + action: &SelectNext, + window: &mut Window, + cx: &mut Context, + ) -> Result<()> { + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + self.push_to_selection_history(); + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + self.select_next_match_internal( + &display_map, + action.replace_newest, + Some(Autoscroll::newest()), + window, + cx, + )?; + Ok(()) + } + + pub fn select_previous( + &mut self, + action: &SelectPrevious, + window: &mut Window, + cx: &mut Context, + ) -> Result<()> { + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + self.push_to_selection_history(); + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let buffer = &display_map.buffer_snapshot; + let mut selections = self.selections.all::(cx); + if let Some(mut select_prev_state) = self.select_prev_state.take() { + let query = &select_prev_state.query; + if !select_prev_state.done { + let first_selection = selections.iter().min_by_key(|s| s.id).unwrap(); + let last_selection = selections.iter().max_by_key(|s| s.id).unwrap(); + let mut next_selected_range = None; + // When we're iterating matches backwards, the oldest match will actually be the furthest one in the buffer. + let bytes_before_last_selection = + buffer.reversed_bytes_in_range(0..last_selection.start); + let bytes_after_first_selection = + buffer.reversed_bytes_in_range(first_selection.end..buffer.len()); + let query_matches = query + .stream_find_iter(bytes_before_last_selection) + .map(|result| (last_selection.start, result)) + .chain( + query + .stream_find_iter(bytes_after_first_selection) + .map(|result| (buffer.len(), result)), + ); + for (end_offset, query_match) in query_matches { + let query_match = query_match.unwrap(); // can only fail due to I/O + let offset_range = + end_offset - query_match.end()..end_offset - query_match.start(); + let display_range = offset_range.start.to_display_point(&display_map) + ..offset_range.end.to_display_point(&display_map); + + if !select_prev_state.wordwise + || (!movement::is_inside_word(&display_map, display_range.start) + && !movement::is_inside_word(&display_map, display_range.end)) + { + next_selected_range = Some(offset_range); + break; + } + } + + if let Some(next_selected_range) = next_selected_range { + self.unfold_ranges(&[next_selected_range.clone()], false, true, cx); + self.change_selections(Some(Autoscroll::newest()), window, cx, |s| { + if action.replace_newest { + s.delete(s.newest_anchor().id); + } + if last_selection.reversed { + s.insert_range(next_selected_range.end..next_selected_range.start); + } else { + s.insert_range(next_selected_range); + } + }); + } else { + select_prev_state.done = true; + } + } + + self.select_prev_state = Some(select_prev_state); + } else { + let mut only_carets = true; + let mut same_text_selected = true; + let mut selected_text = None; + + let mut selections_iter = selections.iter().peekable(); + while let Some(selection) = selections_iter.next() { + if selection.start != selection.end { + only_carets = false; + } + + if same_text_selected { + if selected_text.is_none() { + selected_text = + Some(buffer.text_for_range(selection.range()).collect::()); + } + + if let Some(next_selection) = selections_iter.peek() { + if next_selection.range().len() == selection.range().len() { + let next_selected_text = buffer + .text_for_range(next_selection.range()) + .collect::(); + if Some(next_selected_text) != selected_text { + same_text_selected = false; + selected_text = None; + } + } else { + same_text_selected = false; + selected_text = None; + } + } + } + } + + if only_carets { + for selection in &mut selections { + let word_range = movement::surrounding_word( + &display_map, + selection.start.to_display_point(&display_map), + ); + selection.start = word_range.start.to_offset(&display_map, Bias::Left); + selection.end = word_range.end.to_offset(&display_map, Bias::Left); + selection.goal = SelectionGoal::None; + selection.reversed = false; + } + if selections.len() == 1 { + let selection = selections + .last() + .expect("ensured that there's only one selection"); + let query = buffer + .text_for_range(selection.start..selection.end) + .collect::(); + let is_empty = query.is_empty(); + let select_state = SelectNextState { + query: AhoCorasick::new(&[query.chars().rev().collect::()])?, + wordwise: true, + done: is_empty, + }; + self.select_prev_state = Some(select_state); + } else { + self.select_prev_state = None; + } + + self.unfold_ranges( + &selections.iter().map(|s| s.range()).collect::>(), + false, + true, + cx, + ); + self.change_selections(Some(Autoscroll::newest()), window, cx, |s| { + s.select(selections); + }); + } else if let Some(selected_text) = selected_text { + self.select_prev_state = Some(SelectNextState { + query: AhoCorasick::new(&[selected_text.chars().rev().collect::()])?, + wordwise: false, + done: false, + }); + self.select_previous(action, window, cx)?; + } + } + Ok(()) + } + + pub fn find_next_match( + &mut self, + _: &FindNextMatch, + window: &mut Window, + cx: &mut Context, + ) -> Result<()> { + let selections = self.selections.disjoint_anchors(); + match selections.first() { + Some(first) if selections.len() >= 2 => { + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.select_ranges([first.range()]); + }); + } + _ => self.select_next( + &SelectNext { + replace_newest: true, + }, + window, + cx, + )?, + } + Ok(()) + } + + pub fn find_previous_match( + &mut self, + _: &FindPreviousMatch, + window: &mut Window, + cx: &mut Context, + ) -> Result<()> { + let selections = self.selections.disjoint_anchors(); + match selections.last() { + Some(last) if selections.len() >= 2 => { + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.select_ranges([last.range()]); + }); + } + _ => self.select_previous( + &SelectPrevious { + replace_newest: true, + }, + window, + cx, + )?, + } + Ok(()) + } + + pub fn toggle_comments( + &mut self, + action: &ToggleComments, + window: &mut Window, + cx: &mut Context, + ) { + if self.read_only(cx) { + return; + } + self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + let text_layout_details = &self.text_layout_details(window); + self.transact(window, cx, |this, window, cx| { + let mut selections = this.selections.all::(cx); + let mut edits = Vec::new(); + let mut selection_edit_ranges = Vec::new(); + let mut last_toggled_row = None; + let snapshot = this.buffer.read(cx).read(cx); + let empty_str: Arc = Arc::default(); + let mut suffixes_inserted = Vec::new(); + let ignore_indent = action.ignore_indent; + + fn comment_prefix_range( + snapshot: &MultiBufferSnapshot, + row: MultiBufferRow, + comment_prefix: &str, + comment_prefix_whitespace: &str, + ignore_indent: bool, + ) -> Range { + let indent_size = if ignore_indent { + 0 + } else { + snapshot.indent_size_for_line(row).len + }; + + let start = Point::new(row.0, indent_size); + + let mut line_bytes = snapshot + .bytes_in_range(start..snapshot.max_point()) + .flatten() + .copied(); + + // If this line currently begins with the line comment prefix, then record + // the range containing the prefix. + if line_bytes + .by_ref() + .take(comment_prefix.len()) + .eq(comment_prefix.bytes()) + { + // Include any whitespace that matches the comment prefix. + let matching_whitespace_len = line_bytes + .zip(comment_prefix_whitespace.bytes()) + .take_while(|(a, b)| a == b) + .count() as u32; + let end = Point::new( + start.row, + start.column + comment_prefix.len() as u32 + matching_whitespace_len, + ); + start..end + } else { + start..start + } + } + + fn comment_suffix_range( + snapshot: &MultiBufferSnapshot, + row: MultiBufferRow, + comment_suffix: &str, + comment_suffix_has_leading_space: bool, + ) -> Range { + let end = Point::new(row.0, snapshot.line_len(row)); + let suffix_start_column = end.column.saturating_sub(comment_suffix.len() as u32); + + let mut line_end_bytes = snapshot + .bytes_in_range(Point::new(end.row, suffix_start_column.saturating_sub(1))..end) + .flatten() + .copied(); + + let leading_space_len = if suffix_start_column > 0 + && line_end_bytes.next() == Some(b' ') + && comment_suffix_has_leading_space + { + 1 + } else { + 0 + }; + + // If this line currently begins with the line comment prefix, then record + // the range containing the prefix. + if line_end_bytes.by_ref().eq(comment_suffix.bytes()) { + let start = Point::new(end.row, suffix_start_column - leading_space_len); + start..end + } else { + end..end + } + } + + // TODO: Handle selections that cross excerpts + for selection in &mut selections { + let start_column = snapshot + .indent_size_for_line(MultiBufferRow(selection.start.row)) + .len; + let language = if let Some(language) = + snapshot.language_scope_at(Point::new(selection.start.row, start_column)) + { + language + } else { + continue; + }; + + selection_edit_ranges.clear(); + + // If multiple selections contain a given row, avoid processing that + // row more than once. + let mut start_row = MultiBufferRow(selection.start.row); + if last_toggled_row == Some(start_row) { + start_row = start_row.next_row(); + } + let end_row = + if selection.end.row > selection.start.row && selection.end.column == 0 { + MultiBufferRow(selection.end.row - 1) + } else { + MultiBufferRow(selection.end.row) + }; + last_toggled_row = Some(end_row); + + if start_row > end_row { + continue; + } + + // If the language has line comments, toggle those. + let mut full_comment_prefixes = language.line_comment_prefixes().to_vec(); + + // If ignore_indent is set, trim spaces from the right side of all full_comment_prefixes + if ignore_indent { + full_comment_prefixes = full_comment_prefixes + .into_iter() + .map(|s| Arc::from(s.trim_end())) + .collect(); + } + + if !full_comment_prefixes.is_empty() { + let first_prefix = full_comment_prefixes + .first() + .expect("prefixes is non-empty"); + let prefix_trimmed_lengths = full_comment_prefixes + .iter() + .map(|p| p.trim_end_matches(' ').len()) + .collect::>(); + + let mut all_selection_lines_are_comments = true; + + for row in start_row.0..=end_row.0 { + let row = MultiBufferRow(row); + if start_row < end_row && snapshot.is_line_blank(row) { + continue; + } + + let prefix_range = full_comment_prefixes + .iter() + .zip(prefix_trimmed_lengths.iter().copied()) + .map(|(prefix, trimmed_prefix_len)| { + comment_prefix_range( + snapshot.deref(), + row, + &prefix[..trimmed_prefix_len], + &prefix[trimmed_prefix_len..], + ignore_indent, + ) + }) + .max_by_key(|range| range.end.column - range.start.column) + .expect("prefixes is non-empty"); + + if prefix_range.is_empty() { + all_selection_lines_are_comments = false; + } + + selection_edit_ranges.push(prefix_range); + } + + if all_selection_lines_are_comments { + edits.extend( + selection_edit_ranges + .iter() + .cloned() + .map(|range| (range, empty_str.clone())), + ); + } else { + let min_column = selection_edit_ranges + .iter() + .map(|range| range.start.column) + .min() + .unwrap_or(0); + edits.extend(selection_edit_ranges.iter().map(|range| { + let position = Point::new(range.start.row, min_column); + (position..position, first_prefix.clone()) + })); + } + } else if let Some((full_comment_prefix, comment_suffix)) = + language.block_comment_delimiters() + { + let comment_prefix = full_comment_prefix.trim_end_matches(' '); + let comment_prefix_whitespace = &full_comment_prefix[comment_prefix.len()..]; + let prefix_range = comment_prefix_range( + snapshot.deref(), + start_row, + comment_prefix, + comment_prefix_whitespace, + ignore_indent, + ); + let suffix_range = comment_suffix_range( + snapshot.deref(), + end_row, + comment_suffix.trim_start_matches(' '), + comment_suffix.starts_with(' '), + ); + + if prefix_range.is_empty() || suffix_range.is_empty() { + edits.push(( + prefix_range.start..prefix_range.start, + full_comment_prefix.clone(), + )); + edits.push((suffix_range.end..suffix_range.end, comment_suffix.clone())); + suffixes_inserted.push((end_row, comment_suffix.len())); + } else { + edits.push((prefix_range, empty_str.clone())); + edits.push((suffix_range, empty_str.clone())); + } + } else { + continue; + } + } + + drop(snapshot); + this.buffer.update(cx, |buffer, cx| { + buffer.edit(edits, None, cx); + }); + + // Adjust selections so that they end before any comment suffixes that + // were inserted. + let mut suffixes_inserted = suffixes_inserted.into_iter().peekable(); + let mut selections = this.selections.all::(cx); + let snapshot = this.buffer.read(cx).read(cx); + for selection in &mut selections { + while let Some((row, suffix_len)) = suffixes_inserted.peek().copied() { + match row.cmp(&MultiBufferRow(selection.end.row)) { + Ordering::Less => { + suffixes_inserted.next(); + continue; + } + Ordering::Greater => break, + Ordering::Equal => { + if selection.end.column == snapshot.line_len(row) { + if selection.is_empty() { + selection.start.column -= suffix_len as u32; + } + selection.end.column -= suffix_len as u32; + } + break; + } + } + } + } + + drop(snapshot); + this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.select(selections) + }); + + let selections = this.selections.all::(cx); + let selections_on_single_row = selections.windows(2).all(|selections| { + selections[0].start.row == selections[1].start.row + && selections[0].end.row == selections[1].end.row + && selections[0].start.row == selections[0].end.row + }); + let selections_selecting = selections + .iter() + .any(|selection| selection.start != selection.end); + let advance_downwards = action.advance_downwards + && selections_on_single_row + && !selections_selecting + && !matches!(this.mode, EditorMode::SingleLine { .. }); + + if advance_downwards { + let snapshot = this.buffer.read(cx).snapshot(cx); + + this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.move_cursors_with(|display_snapshot, display_point, _| { + let mut point = display_point.to_point(display_snapshot); + point.row += 1; + point = snapshot.clip_point(point, Bias::Left); + let display_point = point.to_display_point(display_snapshot); + let goal = SelectionGoal::HorizontalPosition( + display_snapshot + .x_for_display_point(display_point, text_layout_details) + .into(), + ); + (display_point, goal) + }) + }); + } + }); + } + + pub fn select_enclosing_symbol( + &mut self, + _: &SelectEnclosingSymbol, + window: &mut Window, + cx: &mut Context, + ) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + + let buffer = self.buffer.read(cx).snapshot(cx); + let old_selections = self.selections.all::(cx).into_boxed_slice(); + + fn update_selection( + selection: &Selection, + buffer_snap: &MultiBufferSnapshot, + ) -> Option> { + let cursor = selection.head(); + let (_buffer_id, symbols) = buffer_snap.symbols_containing(cursor, None)?; + for symbol in symbols.iter().rev() { + let start = symbol.range.start.to_offset(buffer_snap); + let end = symbol.range.end.to_offset(buffer_snap); + let new_range = start..end; + if start < selection.start || end > selection.end { + return Some(Selection { + id: selection.id, + start: new_range.start, + end: new_range.end, + goal: SelectionGoal::None, + reversed: selection.reversed, + }); + } + } + None + } + + let mut selected_larger_symbol = false; + let new_selections = old_selections + .iter() + .map(|selection| match update_selection(selection, &buffer) { + Some(new_selection) => { + if new_selection.range() != selection.range() { + selected_larger_symbol = true; + } + new_selection + } + None => selection.clone(), + }) + .collect::>(); + + if selected_larger_symbol { + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.select(new_selections); + }); + } + } + + pub fn select_larger_syntax_node( + &mut self, + _: &SelectLargerSyntaxNode, + window: &mut Window, + cx: &mut Context, + ) { + let Some(visible_row_count) = self.visible_row_count() else { + return; + }; + let old_selections: Box<[_]> = self.selections.all::(cx).into(); + if old_selections.is_empty() { + return; + } + + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let buffer = self.buffer.read(cx).snapshot(cx); + + let mut selected_larger_node = false; + let mut new_selections = old_selections + .iter() + .map(|selection| { + let old_range = selection.start..selection.end; + + if let Some((node, _)) = buffer.syntax_ancestor(old_range.clone()) { + // manually select word at selection + if ["string_content", "inline"].contains(&node.kind()) { + let word_range = { + let display_point = buffer + .offset_to_point(old_range.start) + .to_display_point(&display_map); + let Range { start, end } = + movement::surrounding_word(&display_map, display_point); + start.to_point(&display_map).to_offset(&buffer) + ..end.to_point(&display_map).to_offset(&buffer) + }; + // ignore if word is already selected + if !word_range.is_empty() && old_range != word_range { + let last_word_range = { + let display_point = buffer + .offset_to_point(old_range.end) + .to_display_point(&display_map); + let Range { start, end } = + movement::surrounding_word(&display_map, display_point); + start.to_point(&display_map).to_offset(&buffer) + ..end.to_point(&display_map).to_offset(&buffer) + }; + // only select word if start and end point belongs to same word + if word_range == last_word_range { + selected_larger_node = true; + return Selection { + id: selection.id, + start: word_range.start, + end: word_range.end, + goal: SelectionGoal::None, + reversed: selection.reversed, + }; + } + } + } + } + + let mut new_range = old_range.clone(); + let mut new_node = None; + while let Some((node, containing_range)) = buffer.syntax_ancestor(new_range.clone()) + { + new_node = Some(node); + new_range = match containing_range { + MultiOrSingleBufferOffsetRange::Single(_) => break, + MultiOrSingleBufferOffsetRange::Multi(range) => range, + }; + if !display_map.intersects_fold(new_range.start) + && !display_map.intersects_fold(new_range.end) + { + break; + } + } + + if let Some(node) = new_node { + // Log the ancestor, to support using this action as a way to explore TreeSitter + // nodes. Parent and grandparent are also logged because this operation will not + // visit nodes that have the same range as their parent. + log::info!("Node: {node:?}"); + let parent = node.parent(); + log::info!("Parent: {parent:?}"); + let grandparent = parent.and_then(|x| x.parent()); + log::info!("Grandparent: {grandparent:?}"); + } + + selected_larger_node |= new_range != old_range; + Selection { + id: selection.id, + start: new_range.start, + end: new_range.end, + goal: SelectionGoal::None, + reversed: selection.reversed, + } + }) + .collect::>(); + + if !selected_larger_node { + return; // don't put this call in the history + } + + // scroll based on transformation done to the last selection created by the user + let (last_old, last_new) = old_selections + .last() + .zip(new_selections.last().cloned()) + .expect("old_selections isn't empty"); + + // revert selection + let is_selection_reversed = { + let should_newest_selection_be_reversed = last_old.start != last_new.start; + new_selections.last_mut().expect("checked above").reversed = + should_newest_selection_be_reversed; + should_newest_selection_be_reversed + }; + + if selected_larger_node { + self.select_syntax_node_history.disable_clearing = true; + self.change_selections(None, window, cx, |s| { + s.select(new_selections.clone()); + }); + self.select_syntax_node_history.disable_clearing = false; + } + + let start_row = last_new.start.to_display_point(&display_map).row().0; + let end_row = last_new.end.to_display_point(&display_map).row().0; + let selection_height = end_row - start_row + 1; + let scroll_margin_rows = self.vertical_scroll_margin() as u32; + + let fits_on_the_screen = visible_row_count >= selection_height + scroll_margin_rows * 2; + let scroll_behavior = if fits_on_the_screen { + self.request_autoscroll(Autoscroll::fit(), cx); + SelectSyntaxNodeScrollBehavior::FitSelection + } else if is_selection_reversed { + self.scroll_cursor_top(&ScrollCursorTop, window, cx); + SelectSyntaxNodeScrollBehavior::CursorTop + } else { + self.scroll_cursor_bottom(&ScrollCursorBottom, window, cx); + SelectSyntaxNodeScrollBehavior::CursorBottom + }; + + self.select_syntax_node_history.push(( + old_selections, + scroll_behavior, + is_selection_reversed, + )); + } + + pub fn select_smaller_syntax_node( + &mut self, + _: &SelectSmallerSyntaxNode, + window: &mut Window, + cx: &mut Context, + ) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + + if let Some((mut selections, scroll_behavior, is_selection_reversed)) = + self.select_syntax_node_history.pop() + { + if let Some(selection) = selections.last_mut() { + selection.reversed = is_selection_reversed; + } + + self.select_syntax_node_history.disable_clearing = true; + self.change_selections(None, window, cx, |s| { + s.select(selections.to_vec()); + }); + self.select_syntax_node_history.disable_clearing = false; + + match scroll_behavior { + SelectSyntaxNodeScrollBehavior::CursorTop => { + self.scroll_cursor_top(&ScrollCursorTop, window, cx); + } + SelectSyntaxNodeScrollBehavior::FitSelection => { + self.request_autoscroll(Autoscroll::fit(), cx); + } + SelectSyntaxNodeScrollBehavior::CursorBottom => { + self.scroll_cursor_bottom(&ScrollCursorBottom, window, cx); + } + } + } + } + + fn refresh_runnables(&mut self, window: &mut Window, cx: &mut Context) -> Task<()> { + if !EditorSettings::get_global(cx).gutter.runnables { + self.clear_tasks(); + return Task::ready(()); + } + let project = self.project.as_ref().map(Entity::downgrade); + let task_sources = self.lsp_task_sources(cx); + cx.spawn_in(window, async move |editor, cx| { + cx.background_executor().timer(UPDATE_DEBOUNCE).await; + let Some(project) = project.and_then(|p| p.upgrade()) else { + return; + }; + let Ok(display_snapshot) = editor.update(cx, |this, cx| { + this.display_map.update(cx, |map, cx| map.snapshot(cx)) + }) else { + return; + }; + + let hide_runnables = project + .update(cx, |project, cx| { + // Do not display any test indicators in non-dev server remote projects. + project.is_via_collab() && project.ssh_connection_string(cx).is_none() + }) + .unwrap_or(true); + if hide_runnables { + return; + } + let new_rows = + cx.background_spawn({ + let snapshot = display_snapshot.clone(); + async move { + Self::fetch_runnable_ranges(&snapshot, Anchor::min()..Anchor::max()) + } + }) + .await; + let Ok(lsp_tasks) = + cx.update(|_, cx| crate::lsp_tasks(project.clone(), &task_sources, None, cx)) + else { + return; + }; + let lsp_tasks = lsp_tasks.await; + + let Ok(mut lsp_tasks_by_rows) = cx.update(|_, cx| { + lsp_tasks + .into_iter() + .flat_map(|(kind, tasks)| { + tasks.into_iter().filter_map(move |(location, task)| { + Some((kind.clone(), location?, task)) + }) + }) + .fold(HashMap::default(), |mut acc, (kind, location, task)| { + let buffer = location.target.buffer; + let buffer_snapshot = buffer.read(cx).snapshot(); + let offset = display_snapshot.buffer_snapshot.excerpts().find_map( + |(excerpt_id, snapshot, _)| { + if snapshot.remote_id() == buffer_snapshot.remote_id() { + display_snapshot + .buffer_snapshot + .anchor_in_excerpt(excerpt_id, location.target.range.start) + } else { + None + } + }, + ); + if let Some(offset) = offset { + let task_buffer_range = + location.target.range.to_point(&buffer_snapshot); + let context_buffer_range = + task_buffer_range.to_offset(&buffer_snapshot); + let context_range = BufferOffset(context_buffer_range.start) + ..BufferOffset(context_buffer_range.end); + + acc.entry((buffer_snapshot.remote_id(), task_buffer_range.start.row)) + .or_insert_with(|| RunnableTasks { + templates: Vec::new(), + offset, + column: task_buffer_range.start.column, + extra_variables: HashMap::default(), + context_range, + }) + .templates + .push((kind, task.original_task().clone())); + } + + acc + }) + }) else { + return; + }; + + let rows = Self::runnable_rows(project, display_snapshot, new_rows, cx.clone()); + editor + .update(cx, |editor, _| { + editor.clear_tasks(); + for (key, mut value) in rows { + if let Some(lsp_tasks) = lsp_tasks_by_rows.remove(&key) { + value.templates.extend(lsp_tasks.templates); + } + + editor.insert_tasks(key, value); + } + for (key, value) in lsp_tasks_by_rows { + editor.insert_tasks(key, value); + } + }) + .ok(); + }) + } + fn fetch_runnable_ranges( + snapshot: &DisplaySnapshot, + range: Range, + ) -> Vec { + snapshot.buffer_snapshot.runnable_ranges(range).collect() + } + + fn runnable_rows( + project: Entity, + snapshot: DisplaySnapshot, + runnable_ranges: Vec, + mut cx: AsyncWindowContext, + ) -> Vec<((BufferId, BufferRow), RunnableTasks)> { + runnable_ranges + .into_iter() + .filter_map(|mut runnable| { + let tasks = cx + .update(|_, cx| Self::templates_with_tags(&project, &mut runnable.runnable, cx)) + .ok()?; + if tasks.is_empty() { + return None; + } + + let point = runnable.run_range.start.to_point(&snapshot.buffer_snapshot); + + let row = snapshot + .buffer_snapshot + .buffer_line_for_row(MultiBufferRow(point.row))? + .1 + .start + .row; + + let context_range = + BufferOffset(runnable.full_range.start)..BufferOffset(runnable.full_range.end); + Some(( + (runnable.buffer_id, row), + RunnableTasks { + templates: tasks, + offset: snapshot + .buffer_snapshot + .anchor_before(runnable.run_range.start), + context_range, + column: point.column, + extra_variables: runnable.extra_captures, + }, + )) + }) + .collect() + } + + fn templates_with_tags( + project: &Entity, + runnable: &mut Runnable, + cx: &mut App, + ) -> Vec<(TaskSourceKind, TaskTemplate)> { + let (inventory, worktree_id, file) = project.read_with(cx, |project, cx| { + let (worktree_id, file) = project + .buffer_for_id(runnable.buffer, cx) + .and_then(|buffer| buffer.read(cx).file()) + .map(|file| (file.worktree_id(cx), file.clone())) + .unzip(); + + ( + project.task_store().read(cx).task_inventory().cloned(), + worktree_id, + file, + ) + }); + + let mut templates_with_tags = mem::take(&mut runnable.tags) + .into_iter() + .flat_map(|RunnableTag(tag)| { + inventory + .as_ref() + .into_iter() + .flat_map(|inventory| { + inventory.read(cx).list_tasks( + file.clone(), + Some(runnable.language.clone()), + worktree_id, + cx, + ) + }) + .filter(move |(_, template)| { + template.tags.iter().any(|source_tag| source_tag == &tag) + }) + }) + .sorted_by_key(|(kind, _)| kind.to_owned()) + .collect::>(); + if let Some((leading_tag_source, _)) = templates_with_tags.first() { + // Strongest source wins; if we have worktree tag binding, prefer that to + // global and language bindings; + // if we have a global binding, prefer that to language binding. + let first_mismatch = templates_with_tags + .iter() + .position(|(tag_source, _)| tag_source != leading_tag_source); + if let Some(index) = first_mismatch { + templates_with_tags.truncate(index); + } + } + + templates_with_tags + } + + pub fn move_to_enclosing_bracket( + &mut self, + _: &MoveToEnclosingBracket, + window: &mut Window, + cx: &mut Context, + ) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.move_offsets_with(|snapshot, selection| { + let Some(enclosing_bracket_ranges) = + snapshot.enclosing_bracket_ranges(selection.start..selection.end) + else { + return; + }; + + let mut best_length = usize::MAX; + let mut best_inside = false; + let mut best_in_bracket_range = false; + let mut best_destination = None; + for (open, close) in enclosing_bracket_ranges { + let close = close.to_inclusive(); + let length = close.end() - open.start; + let inside = selection.start >= open.end && selection.end <= *close.start(); + let in_bracket_range = open.to_inclusive().contains(&selection.head()) + || close.contains(&selection.head()); + + // If best is next to a bracket and current isn't, skip + if !in_bracket_range && best_in_bracket_range { + continue; + } + + // Prefer smaller lengths unless best is inside and current isn't + if length > best_length && (best_inside || !inside) { + continue; + } + + best_length = length; + best_inside = inside; + best_in_bracket_range = in_bracket_range; + best_destination = Some( + if close.contains(&selection.start) && close.contains(&selection.end) { + if inside { open.end } else { open.start } + } else if inside { + *close.start() + } else { + *close.end() + }, + ); + } + + if let Some(destination) = best_destination { + selection.collapse_to(destination, SelectionGoal::None); + } + }) + }); + } + + pub fn undo_selection( + &mut self, + _: &UndoSelection, + window: &mut Window, + cx: &mut Context, + ) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + self.end_selection(window, cx); + self.selection_history.mode = SelectionHistoryMode::Undoing; + if let Some(entry) = self.selection_history.undo_stack.pop_back() { + self.change_selections(None, window, cx, |s| { + s.select_anchors(entry.selections.to_vec()) + }); + self.select_next_state = entry.select_next_state; + self.select_prev_state = entry.select_prev_state; + self.add_selections_state = entry.add_selections_state; + self.request_autoscroll(Autoscroll::newest(), cx); + } + self.selection_history.mode = SelectionHistoryMode::Normal; + } + + pub fn redo_selection( + &mut self, + _: &RedoSelection, + window: &mut Window, + cx: &mut Context, + ) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + self.end_selection(window, cx); + self.selection_history.mode = SelectionHistoryMode::Redoing; + if let Some(entry) = self.selection_history.redo_stack.pop_back() { + self.change_selections(None, window, cx, |s| { + s.select_anchors(entry.selections.to_vec()) + }); + self.select_next_state = entry.select_next_state; + self.select_prev_state = entry.select_prev_state; + self.add_selections_state = entry.add_selections_state; + self.request_autoscroll(Autoscroll::newest(), cx); + } + self.selection_history.mode = SelectionHistoryMode::Normal; + } + + pub fn expand_excerpts( + &mut self, + action: &ExpandExcerpts, + _: &mut Window, + cx: &mut Context, + ) { + self.expand_excerpts_for_direction(action.lines, ExpandExcerptDirection::UpAndDown, cx) + } + + pub fn expand_excerpts_down( + &mut self, + action: &ExpandExcerptsDown, + _: &mut Window, + cx: &mut Context, + ) { + self.expand_excerpts_for_direction(action.lines, ExpandExcerptDirection::Down, cx) + } + + pub fn expand_excerpts_up( + &mut self, + action: &ExpandExcerptsUp, + _: &mut Window, + cx: &mut Context, + ) { + self.expand_excerpts_for_direction(action.lines, ExpandExcerptDirection::Up, cx) + } + + pub fn expand_excerpts_for_direction( + &mut self, + lines: u32, + direction: ExpandExcerptDirection, + + cx: &mut Context, + ) { + let selections = self.selections.disjoint_anchors(); + + let lines = if lines == 0 { + EditorSettings::get_global(cx).expand_excerpt_lines + } else { + lines + }; + + self.buffer.update(cx, |buffer, cx| { + let snapshot = buffer.snapshot(cx); + let mut excerpt_ids = selections + .iter() + .flat_map(|selection| snapshot.excerpt_ids_for_range(selection.range())) + .collect::>(); + excerpt_ids.sort(); + excerpt_ids.dedup(); + buffer.expand_excerpts(excerpt_ids, lines, direction, cx) + }) + } + + pub fn expand_excerpt( + &mut self, + excerpt: ExcerptId, + direction: ExpandExcerptDirection, + window: &mut Window, + cx: &mut Context, + ) { + let current_scroll_position = self.scroll_position(cx); + let lines_to_expand = EditorSettings::get_global(cx).expand_excerpt_lines; + let mut should_scroll_up = false; + + if direction == ExpandExcerptDirection::Down { + let multi_buffer = self.buffer.read(cx); + let snapshot = multi_buffer.snapshot(cx); + if let Some(buffer_id) = snapshot.buffer_id_for_excerpt(excerpt) { + if let Some(buffer) = multi_buffer.buffer(buffer_id) { + if let Some(excerpt_range) = snapshot.buffer_range_for_excerpt(excerpt) { + let buffer_snapshot = buffer.read(cx).snapshot(); + let excerpt_end_row = + Point::from_anchor(&excerpt_range.end, &buffer_snapshot).row; + let last_row = buffer_snapshot.max_point().row; + let lines_below = last_row.saturating_sub(excerpt_end_row); + should_scroll_up = lines_below >= lines_to_expand; + } + } + } + } + + self.buffer.update(cx, |buffer, cx| { + buffer.expand_excerpts([excerpt], lines_to_expand, direction, cx) + }); + + if should_scroll_up { + let new_scroll_position = + current_scroll_position + gpui::Point::new(0.0, lines_to_expand as f32); + self.set_scroll_position(new_scroll_position, window, cx); + } + } + + pub fn go_to_singleton_buffer_point( + &mut self, + point: Point, + window: &mut Window, + cx: &mut Context, + ) { + self.go_to_singleton_buffer_range(point..point, window, cx); + } + + pub fn go_to_singleton_buffer_range( + &mut self, + range: Range, + window: &mut Window, + cx: &mut Context, + ) { + let multibuffer = self.buffer().read(cx); + let Some(buffer) = multibuffer.as_singleton() else { + return; + }; + let Some(start) = multibuffer.buffer_point_to_anchor(&buffer, range.start, cx) else { + return; + }; + let Some(end) = multibuffer.buffer_point_to_anchor(&buffer, range.end, cx) else { + return; + }; + self.change_selections(Some(Autoscroll::center()), window, cx, |s| { + s.select_anchor_ranges([start..end]) + }); + } + + pub fn go_to_diagnostic( + &mut self, + _: &GoToDiagnostic, + window: &mut Window, + cx: &mut Context, + ) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + self.go_to_diagnostic_impl(Direction::Next, window, cx) + } + + pub fn go_to_prev_diagnostic( + &mut self, + _: &GoToPreviousDiagnostic, + window: &mut Window, + cx: &mut Context, + ) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + self.go_to_diagnostic_impl(Direction::Prev, window, cx) + } + + pub fn go_to_diagnostic_impl( + &mut self, + direction: Direction, + window: &mut Window, + cx: &mut Context, + ) { + let buffer = self.buffer.read(cx).snapshot(cx); + let selection = self.selections.newest::(cx); + + let mut active_group_id = None; + if let ActiveDiagnostic::Group(active_group) = &self.active_diagnostics { + if active_group.active_range.start.to_offset(&buffer) == selection.start { + active_group_id = Some(active_group.group_id); + } + } + + fn filtered( + snapshot: EditorSnapshot, + diagnostics: impl Iterator>, + ) -> impl Iterator> { + diagnostics + .filter(|entry| entry.range.start != entry.range.end) + .filter(|entry| !entry.diagnostic.is_unnecessary) + .filter(move |entry| !snapshot.intersects_fold(entry.range.start)) + } + + let snapshot = self.snapshot(window, cx); + let before = filtered( + snapshot.clone(), + buffer + .diagnostics_in_range(0..selection.start) + .filter(|entry| entry.range.start <= selection.start), + ); + let after = filtered( + snapshot, + buffer + .diagnostics_in_range(selection.start..buffer.len()) + .filter(|entry| entry.range.start >= selection.start), + ); + + let mut found: Option> = None; + if direction == Direction::Prev { + 'outer: for prev_diagnostics in [before.collect::>(), after.collect::>()] + { + for diagnostic in prev_diagnostics.into_iter().rev() { + if diagnostic.range.start != selection.start + || active_group_id + .is_some_and(|active| diagnostic.diagnostic.group_id < active) + { + found = Some(diagnostic); + break 'outer; + } + } + } + } else { + for diagnostic in after.chain(before) { + if diagnostic.range.start != selection.start + || active_group_id.is_some_and(|active| diagnostic.diagnostic.group_id > active) + { + found = Some(diagnostic); + break; + } + } + } + let Some(next_diagnostic) = found else { + return; + }; + + let Some(buffer_id) = buffer.anchor_after(next_diagnostic.range.start).buffer_id else { + return; + }; + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.select_ranges(vec![ + next_diagnostic.range.start..next_diagnostic.range.start, + ]) + }); + self.activate_diagnostics(buffer_id, next_diagnostic, window, cx); + self.refresh_inline_completion(false, true, window, cx); + } + + fn go_to_next_hunk(&mut self, _: &GoToHunk, window: &mut Window, cx: &mut Context) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + let snapshot = self.snapshot(window, cx); + let selection = self.selections.newest::(cx); + self.go_to_hunk_before_or_after_position( + &snapshot, + selection.head(), + Direction::Next, + window, + cx, + ); + } + + pub fn go_to_hunk_before_or_after_position( + &mut self, + snapshot: &EditorSnapshot, + position: Point, + direction: Direction, + window: &mut Window, + cx: &mut Context, + ) { + let row = if direction == Direction::Next { + self.hunk_after_position(snapshot, position) + .map(|hunk| hunk.row_range.start) + } else { + self.hunk_before_position(snapshot, position) + }; + + if let Some(row) = row { + let destination = Point::new(row.0, 0); + let autoscroll = Autoscroll::center(); + + self.unfold_ranges(&[destination..destination], false, false, cx); + self.change_selections(Some(autoscroll), window, cx, |s| { + s.select_ranges([destination..destination]); + }); + } + } + + fn hunk_after_position( + &mut self, + snapshot: &EditorSnapshot, + position: Point, + ) -> Option { + snapshot + .buffer_snapshot + .diff_hunks_in_range(position..snapshot.buffer_snapshot.max_point()) + .find(|hunk| hunk.row_range.start.0 > position.row) + .or_else(|| { + snapshot + .buffer_snapshot + .diff_hunks_in_range(Point::zero()..position) + .find(|hunk| hunk.row_range.end.0 < position.row) + }) + } + + fn go_to_prev_hunk( + &mut self, + _: &GoToPreviousHunk, + window: &mut Window, + cx: &mut Context, + ) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + let snapshot = self.snapshot(window, cx); + let selection = self.selections.newest::(cx); + self.go_to_hunk_before_or_after_position( + &snapshot, + selection.head(), + Direction::Prev, + window, + cx, + ); + } + + fn hunk_before_position( + &mut self, + snapshot: &EditorSnapshot, + position: Point, + ) -> Option { + snapshot + .buffer_snapshot + .diff_hunk_before(position) + .or_else(|| snapshot.buffer_snapshot.diff_hunk_before(Point::MAX)) + } + + fn go_to_next_change( + &mut self, + _: &GoToNextChange, + window: &mut Window, + cx: &mut Context, + ) { + if let Some(selections) = self + .change_list + .next_change(1, Direction::Next) + .map(|s| s.to_vec()) + { + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + let map = s.display_map(); + s.select_display_ranges(selections.iter().map(|a| { + let point = a.to_display_point(&map); + point..point + })) + }) + } + } + + fn go_to_previous_change( + &mut self, + _: &GoToPreviousChange, + window: &mut Window, + cx: &mut Context, + ) { + if let Some(selections) = self + .change_list + .next_change(1, Direction::Prev) + .map(|s| s.to_vec()) + { + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + let map = s.display_map(); + s.select_display_ranges(selections.iter().map(|a| { + let point = a.to_display_point(&map); + point..point + })) + }) + } + } + + fn go_to_line( + &mut self, + position: Anchor, + highlight_color: Option, + window: &mut Window, + cx: &mut Context, + ) { + let snapshot = self.snapshot(window, cx).display_snapshot; + let position = position.to_point(&snapshot.buffer_snapshot); + let start = snapshot + .buffer_snapshot + .clip_point(Point::new(position.row, 0), Bias::Left); + let end = start + Point::new(1, 0); + let start = snapshot.buffer_snapshot.anchor_before(start); + let end = snapshot.buffer_snapshot.anchor_before(end); + + self.highlight_rows::( + start..end, + highlight_color + .unwrap_or_else(|| cx.theme().colors().editor_highlighted_line_background), + Default::default(), + cx, + ); + self.request_autoscroll(Autoscroll::center().for_anchor(start), cx); + } + + pub fn go_to_definition( + &mut self, + _: &GoToDefinition, + window: &mut Window, + cx: &mut Context, + ) -> Task> { + let definition = + self.go_to_definition_of_kind(GotoDefinitionKind::Symbol, false, window, cx); + let fallback_strategy = EditorSettings::get_global(cx).go_to_definition_fallback; + cx.spawn_in(window, async move |editor, cx| { + if definition.await? == Navigated::Yes { + return Ok(Navigated::Yes); + } + match fallback_strategy { + GoToDefinitionFallback::None => Ok(Navigated::No), + GoToDefinitionFallback::FindAllReferences => { + match editor.update_in(cx, |editor, window, cx| { + editor.find_all_references(&FindAllReferences, window, cx) + })? { + Some(references) => references.await, + None => Ok(Navigated::No), + } + } + } + }) + } + + pub fn go_to_declaration( + &mut self, + _: &GoToDeclaration, + window: &mut Window, + cx: &mut Context, + ) -> Task> { + self.go_to_definition_of_kind(GotoDefinitionKind::Declaration, false, window, cx) + } + + pub fn go_to_declaration_split( + &mut self, + _: &GoToDeclaration, + window: &mut Window, + cx: &mut Context, + ) -> Task> { + self.go_to_definition_of_kind(GotoDefinitionKind::Declaration, true, window, cx) + } + + pub fn go_to_implementation( + &mut self, + _: &GoToImplementation, + window: &mut Window, + cx: &mut Context, + ) -> Task> { + self.go_to_definition_of_kind(GotoDefinitionKind::Implementation, false, window, cx) + } + + pub fn go_to_implementation_split( + &mut self, + _: &GoToImplementationSplit, + window: &mut Window, + cx: &mut Context, + ) -> Task> { + self.go_to_definition_of_kind(GotoDefinitionKind::Implementation, true, window, cx) + } + + pub fn go_to_type_definition( + &mut self, + _: &GoToTypeDefinition, + window: &mut Window, + cx: &mut Context, + ) -> Task> { + self.go_to_definition_of_kind(GotoDefinitionKind::Type, false, window, cx) + } + + pub fn go_to_definition_split( + &mut self, + _: &GoToDefinitionSplit, + window: &mut Window, + cx: &mut Context, + ) -> Task> { + self.go_to_definition_of_kind(GotoDefinitionKind::Symbol, true, window, cx) + } + + pub fn go_to_type_definition_split( + &mut self, + _: &GoToTypeDefinitionSplit, + window: &mut Window, + cx: &mut Context, + ) -> Task> { + self.go_to_definition_of_kind(GotoDefinitionKind::Type, true, window, cx) + } + + fn go_to_definition_of_kind( + &mut self, + kind: GotoDefinitionKind, + split: bool, + window: &mut Window, + cx: &mut Context, + ) -> Task> { + let Some(provider) = self.semantics_provider.clone() else { + return Task::ready(Ok(Navigated::No)); + }; + let head = self.selections.newest::(cx).head(); + let buffer = self.buffer.read(cx); + let (buffer, head) = if let Some(text_anchor) = buffer.text_anchor_for_position(head, cx) { + text_anchor + } else { + return Task::ready(Ok(Navigated::No)); + }; + + let Some(definitions) = provider.definitions(&buffer, head, kind, cx) else { + return Task::ready(Ok(Navigated::No)); + }; + + cx.spawn_in(window, async move |editor, cx| { + let definitions = definitions.await?; + let navigated = editor + .update_in(cx, |editor, window, cx| { + editor.navigate_to_hover_links( + Some(kind), + definitions + .into_iter() + .filter(|location| { + hover_links::exclude_link_to_position(&buffer, &head, location, cx) + }) + .map(HoverLink::Text) + .collect::>(), + split, + window, + cx, + ) + })? + .await?; + anyhow::Ok(navigated) + }) + } + + pub fn open_url(&mut self, _: &OpenUrl, window: &mut Window, cx: &mut Context) { + let selection = self.selections.newest_anchor(); + let head = selection.head(); + let tail = selection.tail(); + + let Some((buffer, start_position)) = + self.buffer.read(cx).text_anchor_for_position(head, cx) + else { + return; + }; + + let end_position = if head != tail { + let Some((_, pos)) = self.buffer.read(cx).text_anchor_for_position(tail, cx) else { + return; + }; + Some(pos) + } else { + None + }; + + let url_finder = cx.spawn_in(window, async move |editor, cx| { + let url = if let Some(end_pos) = end_position { + find_url_from_range(&buffer, start_position..end_pos, cx.clone()) + } else { + find_url(&buffer, start_position, cx.clone()).map(|(_, url)| url) + }; + + if let Some(url) = url { + editor.update(cx, |_, cx| { + cx.open_url(&url); + }) + } else { + Ok(()) + } + }); + + url_finder.detach(); + } + + pub fn open_selected_filename( + &mut self, + _: &OpenSelectedFilename, + window: &mut Window, + cx: &mut Context, + ) { + let Some(workspace) = self.workspace() else { + return; + }; + + let position = self.selections.newest_anchor().head(); + + let Some((buffer, buffer_position)) = + self.buffer.read(cx).text_anchor_for_position(position, cx) + else { + return; + }; + + let project = self.project.clone(); + + cx.spawn_in(window, async move |_, cx| { + let result = find_file(&buffer, project, buffer_position, cx).await; + + if let Some((_, path)) = result { + workspace + .update_in(cx, |workspace, window, cx| { + workspace.open_resolved_path(path, window, cx) + })? + .await?; + } + anyhow::Ok(()) + }) + .detach(); + } + + pub(crate) fn navigate_to_hover_links( + &mut self, + kind: Option, + mut definitions: Vec, + split: bool, + window: &mut Window, + cx: &mut Context, + ) -> Task> { + // If there is one definition, just open it directly + if definitions.len() == 1 { + let definition = definitions.pop().unwrap(); + + enum TargetTaskResult { + Location(Option), + AlreadyNavigated, + } + + let target_task = match definition { + HoverLink::Text(link) => { + Task::ready(anyhow::Ok(TargetTaskResult::Location(Some(link.target)))) + } + HoverLink::InlayHint(lsp_location, server_id) => { + let computation = + self.compute_target_location(lsp_location, server_id, window, cx); + cx.background_spawn(async move { + let location = computation.await?; + Ok(TargetTaskResult::Location(location)) + }) + } + HoverLink::Url(url) => { + cx.open_url(&url); + Task::ready(Ok(TargetTaskResult::AlreadyNavigated)) + } + HoverLink::File(path) => { + if let Some(workspace) = self.workspace() { + cx.spawn_in(window, async move |_, cx| { + workspace + .update_in(cx, |workspace, window, cx| { + workspace.open_resolved_path(path, window, cx) + })? + .await + .map(|_| TargetTaskResult::AlreadyNavigated) + }) + } else { + Task::ready(Ok(TargetTaskResult::Location(None))) + } + } + }; + cx.spawn_in(window, async move |editor, cx| { + let target = match target_task.await.context("target resolution task")? { + TargetTaskResult::AlreadyNavigated => return Ok(Navigated::Yes), + TargetTaskResult::Location(None) => return Ok(Navigated::No), + TargetTaskResult::Location(Some(target)) => target, + }; + + editor.update_in(cx, |editor, window, cx| { + let Some(workspace) = editor.workspace() else { + return Navigated::No; + }; + let pane = workspace.read(cx).active_pane().clone(); + + let range = target.range.to_point(target.buffer.read(cx)); + let range = editor.range_for_match(&range); + let range = collapse_multiline_range(range); + + if !split + && Some(&target.buffer) == editor.buffer.read(cx).as_singleton().as_ref() + { + editor.go_to_singleton_buffer_range(range.clone(), window, cx); + } else { + window.defer(cx, move |window, cx| { + let target_editor: Entity = + workspace.update(cx, |workspace, cx| { + let pane = if split { + workspace.adjacent_pane(window, cx) + } else { + workspace.active_pane().clone() + }; + + workspace.open_project_item( + pane, + target.buffer.clone(), + true, + true, + window, + cx, + ) + }); + target_editor.update(cx, |target_editor, cx| { + // When selecting a definition in a different buffer, disable the nav history + // to avoid creating a history entry at the previous cursor location. + pane.update(cx, |pane, _| pane.disable_history()); + target_editor.go_to_singleton_buffer_range(range, window, cx); + pane.update(cx, |pane, _| pane.enable_history()); + }); + }); + } + Navigated::Yes + }) + }) + } else if !definitions.is_empty() { + cx.spawn_in(window, async move |editor, cx| { + let (title, location_tasks, workspace) = editor + .update_in(cx, |editor, window, cx| { + let tab_kind = match kind { + Some(GotoDefinitionKind::Implementation) => "Implementations", + _ => "Definitions", + }; + let title = definitions + .iter() + .find_map(|definition| match definition { + HoverLink::Text(link) => link.origin.as_ref().map(|origin| { + let buffer = origin.buffer.read(cx); + format!( + "{} for {}", + tab_kind, + buffer + .text_for_range(origin.range.clone()) + .collect::() + ) + }), + HoverLink::InlayHint(_, _) => None, + HoverLink::Url(_) => None, + HoverLink::File(_) => None, + }) + .unwrap_or(tab_kind.to_string()); + let location_tasks = definitions + .into_iter() + .map(|definition| match definition { + HoverLink::Text(link) => Task::ready(Ok(Some(link.target))), + HoverLink::InlayHint(lsp_location, server_id) => editor + .compute_target_location(lsp_location, server_id, window, cx), + HoverLink::Url(_) => Task::ready(Ok(None)), + HoverLink::File(_) => Task::ready(Ok(None)), + }) + .collect::>(); + (title, location_tasks, editor.workspace().clone()) + }) + .context("location tasks preparation")?; + + let locations = future::join_all(location_tasks) + .await + .into_iter() + .filter_map(|location| location.transpose()) + .collect::>() + .context("location tasks")?; + + let Some(workspace) = workspace else { + return Ok(Navigated::No); + }; + let opened = workspace + .update_in(cx, |workspace, window, cx| { + Self::open_locations_in_multibuffer( + workspace, + locations, + title, + split, + MultibufferSelectionMode::First, + window, + cx, + ) + }) + .ok(); + + anyhow::Ok(Navigated::from_bool(opened.is_some())) + }) + } else { + Task::ready(Ok(Navigated::No)) + } + } + + fn compute_target_location( + &self, + lsp_location: lsp::Location, + server_id: LanguageServerId, + window: &mut Window, + cx: &mut Context, + ) -> Task>> { + let Some(project) = self.project.clone() else { + return Task::ready(Ok(None)); + }; + + cx.spawn_in(window, async move |editor, cx| { + let location_task = editor.update(cx, |_, cx| { + project.update(cx, |project, cx| { + let language_server_name = project + .language_server_statuses(cx) + .find(|(id, _)| server_id == *id) + .map(|(_, status)| LanguageServerName::from(status.name.as_str())); + language_server_name.map(|language_server_name| { + project.open_local_buffer_via_lsp( + lsp_location.uri.clone(), + server_id, + language_server_name, + cx, + ) + }) + }) + })?; + let location = match location_task { + Some(task) => Some({ + let target_buffer_handle = task.await.context("open local buffer")?; + let range = target_buffer_handle.update(cx, |target_buffer, _| { + let target_start = target_buffer + .clip_point_utf16(point_from_lsp(lsp_location.range.start), Bias::Left); + let target_end = target_buffer + .clip_point_utf16(point_from_lsp(lsp_location.range.end), Bias::Left); + target_buffer.anchor_after(target_start) + ..target_buffer.anchor_before(target_end) + })?; + Location { + buffer: target_buffer_handle, + range, + } + }), + None => None, + }; + Ok(location) + }) + } + + pub fn find_all_references( + &mut self, + _: &FindAllReferences, + window: &mut Window, + cx: &mut Context, + ) -> Option>> { + let selection = self.selections.newest::(cx); + let multi_buffer = self.buffer.read(cx); + let head = selection.head(); + + let multi_buffer_snapshot = multi_buffer.snapshot(cx); + let head_anchor = multi_buffer_snapshot.anchor_at( + head, + if head < selection.tail() { + Bias::Right + } else { + Bias::Left + }, + ); + + match self + .find_all_references_task_sources + .binary_search_by(|anchor| anchor.cmp(&head_anchor, &multi_buffer_snapshot)) + { + Ok(_) => { + log::info!( + "Ignoring repeated FindAllReferences invocation with the position of already running task" + ); + return None; + } + Err(i) => { + self.find_all_references_task_sources.insert(i, head_anchor); + } + } + + let (buffer, head) = multi_buffer.text_anchor_for_position(head, cx)?; + let workspace = self.workspace()?; + let project = workspace.read(cx).project().clone(); + let references = project.update(cx, |project, cx| project.references(&buffer, head, cx)); + Some(cx.spawn_in(window, async move |editor, cx| { + let _cleanup = cx.on_drop(&editor, move |editor, _| { + if let Ok(i) = editor + .find_all_references_task_sources + .binary_search_by(|anchor| anchor.cmp(&head_anchor, &multi_buffer_snapshot)) + { + editor.find_all_references_task_sources.remove(i); + } + }); + + let locations = references.await?; + if locations.is_empty() { + return anyhow::Ok(Navigated::No); + } + + workspace.update_in(cx, |workspace, window, cx| { + let title = locations + .first() + .as_ref() + .map(|location| { + let buffer = location.buffer.read(cx); + format!( + "References to `{}`", + buffer + .text_for_range(location.range.clone()) + .collect::() + ) + }) + .unwrap(); + Self::open_locations_in_multibuffer( + workspace, + locations, + title, + false, + MultibufferSelectionMode::First, + window, + cx, + ); + Navigated::Yes + }) + })) + } + + /// Opens a multibuffer with the given project locations in it + pub fn open_locations_in_multibuffer( + workspace: &mut Workspace, + mut locations: Vec, + title: String, + split: bool, + multibuffer_selection_mode: MultibufferSelectionMode, + window: &mut Window, + cx: &mut Context, + ) { + // If there are multiple definitions, open them in a multibuffer + locations.sort_by_key(|location| location.buffer.read(cx).remote_id()); + let mut locations = locations.into_iter().peekable(); + let mut ranges: Vec> = Vec::new(); + let capability = workspace.project().read(cx).capability(); + + let excerpt_buffer = cx.new(|cx| { + let mut multibuffer = MultiBuffer::new(capability); + while let Some(location) = locations.next() { + let buffer = location.buffer.read(cx); + let mut ranges_for_buffer = Vec::new(); + let range = location.range.to_point(buffer); + ranges_for_buffer.push(range.clone()); + + while let Some(next_location) = locations.peek() { + if next_location.buffer == location.buffer { + ranges_for_buffer.push(next_location.range.to_point(buffer)); + locations.next(); + } else { + break; + } + } + + ranges_for_buffer.sort_by_key(|range| (range.start, Reverse(range.end))); + let (new_ranges, _) = multibuffer.set_excerpts_for_path( + PathKey::for_buffer(&location.buffer, cx), + location.buffer.clone(), + ranges_for_buffer, + DEFAULT_MULTIBUFFER_CONTEXT, + cx, + ); + ranges.extend(new_ranges) + } + + multibuffer.with_title(title) + }); + + let editor = cx.new(|cx| { + Editor::for_multibuffer( + excerpt_buffer, + Some(workspace.project().clone()), + window, + cx, + ) + }); + editor.update(cx, |editor, cx| { + match multibuffer_selection_mode { + MultibufferSelectionMode::First => { + if let Some(first_range) = ranges.first() { + editor.change_selections(None, window, cx, |selections| { + selections.clear_disjoint(); + selections.select_anchor_ranges(std::iter::once(first_range.clone())); + }); + } + editor.highlight_background::( + &ranges, + |theme| theme.editor_highlighted_line_background, + cx, + ); + } + MultibufferSelectionMode::All => { + editor.change_selections(None, window, cx, |selections| { + selections.clear_disjoint(); + selections.select_anchor_ranges(ranges); + }); + } + } + editor.register_buffers_with_language_servers(cx); + }); + + let item = Box::new(editor); + let item_id = item.item_id(); + + if split { + workspace.split_item(SplitDirection::Right, item.clone(), window, cx); + } else { + if PreviewTabsSettings::get_global(cx).enable_preview_from_code_navigation { + let (preview_item_id, preview_item_idx) = + workspace.active_pane().update(cx, |pane, _| { + (pane.preview_item_id(), pane.preview_item_idx()) + }); + + workspace.add_item_to_active_pane(item.clone(), 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); + }); + } + } else { + workspace.add_item_to_active_pane(item.clone(), None, true, window, cx); + } + } + workspace.active_pane().update(cx, |pane, cx| { + pane.set_preview_item_id(Some(item_id), cx); + }); + } + + pub fn rename( + &mut self, + _: &Rename, + window: &mut Window, + cx: &mut Context, + ) -> Option>> { + use language::ToOffset as _; + + let provider = self.semantics_provider.clone()?; + let selection = self.selections.newest_anchor().clone(); + let (cursor_buffer, cursor_buffer_position) = self + .buffer + .read(cx) + .text_anchor_for_position(selection.head(), cx)?; + let (tail_buffer, cursor_buffer_position_end) = self + .buffer + .read(cx) + .text_anchor_for_position(selection.tail(), cx)?; + if tail_buffer != cursor_buffer { + return None; + } + + let snapshot = cursor_buffer.read(cx).snapshot(); + let cursor_buffer_offset = cursor_buffer_position.to_offset(&snapshot); + let cursor_buffer_offset_end = cursor_buffer_position_end.to_offset(&snapshot); + let prepare_rename = provider + .range_for_rename(&cursor_buffer, cursor_buffer_position, cx) + .unwrap_or_else(|| Task::ready(Ok(None))); + drop(snapshot); + + Some(cx.spawn_in(window, async move |this, cx| { + let rename_range = if let Some(range) = prepare_rename.await? { + Some(range) + } else { + this.update(cx, |this, cx| { + let buffer = this.buffer.read(cx).snapshot(cx); + let mut buffer_highlights = this + .document_highlights_for_position(selection.head(), &buffer) + .filter(|highlight| { + highlight.start.excerpt_id == selection.head().excerpt_id + && highlight.end.excerpt_id == selection.head().excerpt_id + }); + buffer_highlights + .next() + .map(|highlight| highlight.start.text_anchor..highlight.end.text_anchor) + })? + }; + if let Some(rename_range) = rename_range { + this.update_in(cx, |this, window, cx| { + let snapshot = cursor_buffer.read(cx).snapshot(); + let rename_buffer_range = rename_range.to_offset(&snapshot); + let cursor_offset_in_rename_range = + cursor_buffer_offset.saturating_sub(rename_buffer_range.start); + let cursor_offset_in_rename_range_end = + cursor_buffer_offset_end.saturating_sub(rename_buffer_range.start); + + this.take_rename(false, window, cx); + let buffer = this.buffer.read(cx).read(cx); + let cursor_offset = selection.head().to_offset(&buffer); + let rename_start = cursor_offset.saturating_sub(cursor_offset_in_rename_range); + let rename_end = rename_start + rename_buffer_range.len(); + let range = buffer.anchor_before(rename_start)..buffer.anchor_after(rename_end); + let mut old_highlight_id = None; + let old_name: Arc = buffer + .chunks(rename_start..rename_end, true) + .map(|chunk| { + if old_highlight_id.is_none() { + old_highlight_id = chunk.syntax_highlight_id; + } + chunk.text + }) + .collect::() + .into(); + + drop(buffer); + + // Position the selection in the rename editor so that it matches the current selection. + this.show_local_selections = false; + let rename_editor = cx.new(|cx| { + let mut editor = Editor::single_line(window, cx); + editor.buffer.update(cx, |buffer, cx| { + buffer.edit([(0..0, old_name.clone())], None, cx) + }); + let rename_selection_range = match cursor_offset_in_rename_range + .cmp(&cursor_offset_in_rename_range_end) + { + Ordering::Equal => { + editor.select_all(&SelectAll, window, cx); + return editor; + } + Ordering::Less => { + cursor_offset_in_rename_range..cursor_offset_in_rename_range_end + } + Ordering::Greater => { + cursor_offset_in_rename_range_end..cursor_offset_in_rename_range + } + }; + if rename_selection_range.end > old_name.len() { + editor.select_all(&SelectAll, window, cx); + } else { + editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.select_ranges([rename_selection_range]); + }); + } + editor + }); + cx.subscribe(&rename_editor, |_, _, e: &EditorEvent, cx| { + if e == &EditorEvent::Focused { + cx.emit(EditorEvent::FocusedIn) + } + }) + .detach(); + + let write_highlights = + this.clear_background_highlights::(cx); + let read_highlights = + this.clear_background_highlights::(cx); + let ranges = write_highlights + .iter() + .flat_map(|(_, ranges)| ranges.iter()) + .chain(read_highlights.iter().flat_map(|(_, ranges)| ranges.iter())) + .cloned() + .collect(); + + this.highlight_text::( + ranges, + HighlightStyle { + fade_out: Some(0.6), + ..Default::default() + }, + cx, + ); + let rename_focus_handle = rename_editor.focus_handle(cx); + window.focus(&rename_focus_handle); + let block_id = this.insert_blocks( + [BlockProperties { + style: BlockStyle::Flex, + placement: BlockPlacement::Below(range.start), + height: Some(1), + render: Arc::new({ + let rename_editor = rename_editor.clone(); + move |cx: &mut BlockContext| { + let mut text_style = cx.editor_style.text.clone(); + if let Some(highlight_style) = old_highlight_id + .and_then(|h| h.style(&cx.editor_style.syntax)) + { + text_style = text_style.highlight(highlight_style); + } + div() + .block_mouse_down() + .pl(cx.anchor_x) + .child(EditorElement::new( + &rename_editor, + EditorStyle { + background: cx.theme().system().transparent, + local_player: cx.editor_style.local_player, + text: text_style, + scrollbar_width: cx.editor_style.scrollbar_width, + syntax: cx.editor_style.syntax.clone(), + status: cx.editor_style.status.clone(), + inlay_hints_style: HighlightStyle { + font_weight: Some(FontWeight::BOLD), + ..make_inlay_hints_style(cx.app) + }, + inline_completion_styles: make_suggestion_styles( + cx.app, + ), + ..EditorStyle::default() + }, + )) + .into_any_element() + } + }), + priority: 0, + }], + Some(Autoscroll::fit()), + cx, + )[0]; + this.pending_rename = Some(RenameState { + range, + old_name, + editor: rename_editor, + block_id, + }); + })?; + } + + Ok(()) + })) + } + + pub fn confirm_rename( + &mut self, + _: &ConfirmRename, + window: &mut Window, + cx: &mut Context, + ) -> Option>> { + let rename = self.take_rename(false, window, cx)?; + let workspace = self.workspace()?.downgrade(); + let (buffer, start) = self + .buffer + .read(cx) + .text_anchor_for_position(rename.range.start, cx)?; + let (end_buffer, _) = self + .buffer + .read(cx) + .text_anchor_for_position(rename.range.end, cx)?; + if buffer != end_buffer { + return None; + } + + let old_name = rename.old_name; + let new_name = rename.editor.read(cx).text(cx); + + let rename = self.semantics_provider.as_ref()?.perform_rename( + &buffer, + start, + new_name.clone(), + cx, + )?; + + Some(cx.spawn_in(window, async move |editor, cx| { + let project_transaction = rename.await?; + Self::open_project_transaction( + &editor, + workspace, + project_transaction, + format!("Rename: {} → {}", old_name, new_name), + cx, + ) + .await?; + + editor.update(cx, |editor, cx| { + editor.refresh_document_highlights(cx); + })?; + Ok(()) + })) + } + + fn take_rename( + &mut self, + moving_cursor: bool, + window: &mut Window, + cx: &mut Context, + ) -> Option { + let rename = self.pending_rename.take()?; + if rename.editor.focus_handle(cx).is_focused(window) { + window.focus(&self.focus_handle); + } + + self.remove_blocks( + [rename.block_id].into_iter().collect(), + Some(Autoscroll::fit()), + cx, + ); + self.clear_highlights::(cx); + self.show_local_selections = true; + + if moving_cursor { + let cursor_in_rename_editor = rename.editor.update(cx, |editor, cx| { + editor.selections.newest::(cx).head() + }); + + // Update the selection to match the position of the selection inside + // the rename editor. + let snapshot = self.buffer.read(cx).read(cx); + let rename_range = rename.range.to_offset(&snapshot); + let cursor_in_editor = snapshot + .clip_offset(rename_range.start + cursor_in_rename_editor, Bias::Left) + .min(rename_range.end); + drop(snapshot); + + self.change_selections(None, window, cx, |s| { + s.select_ranges(vec![cursor_in_editor..cursor_in_editor]) + }); + } else { + self.refresh_document_highlights(cx); + } + + Some(rename) + } + + pub fn pending_rename(&self) -> Option<&RenameState> { + self.pending_rename.as_ref() + } + + fn format( + &mut self, + _: &Format, + window: &mut Window, + cx: &mut Context, + ) -> Option>> { + self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + + let project = match &self.project { + Some(project) => project.clone(), + None => return None, + }; + + Some(self.perform_format( + project, + FormatTrigger::Manual, + FormatTarget::Buffers, + window, + cx, + )) + } + + fn format_selections( + &mut self, + _: &FormatSelections, + window: &mut Window, + cx: &mut Context, + ) -> Option>> { + self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + + let project = match &self.project { + Some(project) => project.clone(), + None => return None, + }; + + let ranges = self + .selections + .all_adjusted(cx) + .into_iter() + .map(|selection| selection.range()) + .collect_vec(); + + Some(self.perform_format( + project, + FormatTrigger::Manual, + FormatTarget::Ranges(ranges), + window, + cx, + )) + } + + fn perform_format( + &mut self, + project: Entity, + trigger: FormatTrigger, + target: FormatTarget, + window: &mut Window, + cx: &mut Context, + ) -> Task> { + let buffer = self.buffer.clone(); + let (buffers, target) = match target { + FormatTarget::Buffers => { + let mut buffers = buffer.read(cx).all_buffers(); + if trigger == FormatTrigger::Save { + buffers.retain(|buffer| buffer.read(cx).is_dirty()); + } + (buffers, LspFormatTarget::Buffers) + } + FormatTarget::Ranges(selection_ranges) => { + let multi_buffer = buffer.read(cx); + let snapshot = multi_buffer.read(cx); + let mut buffers = HashSet::default(); + let mut buffer_id_to_ranges: BTreeMap>> = + BTreeMap::new(); + for selection_range in selection_ranges { + for (buffer, buffer_range, _) in + snapshot.range_to_buffer_ranges(selection_range) + { + let buffer_id = buffer.remote_id(); + let start = buffer.anchor_before(buffer_range.start); + let end = buffer.anchor_after(buffer_range.end); + buffers.insert(multi_buffer.buffer(buffer_id).unwrap()); + buffer_id_to_ranges + .entry(buffer_id) + .and_modify(|buffer_ranges| buffer_ranges.push(start..end)) + .or_insert_with(|| vec![start..end]); + } + } + (buffers, LspFormatTarget::Ranges(buffer_id_to_ranges)) + } + }; + + let transaction_id_prev = buffer.read_with(cx, |b, cx| b.last_transaction_id(cx)); + let selections_prev = transaction_id_prev + .and_then(|transaction_id_prev| { + // default to selections as they were after the last edit, if we have them, + // instead of how they are now. + // This will make it so that editing, moving somewhere else, formatting, then undoing the format + // will take you back to where you made the last edit, instead of staying where you scrolled + self.selection_history + .transaction(transaction_id_prev) + .map(|t| t.0.clone()) + }) + .unwrap_or_else(|| { + log::info!("Failed to determine selections from before format. Falling back to selections when format was initiated"); + self.selections.disjoint_anchors() + }); + + let mut timeout = cx.background_executor().timer(FORMAT_TIMEOUT).fuse(); + let format = project.update(cx, |project, cx| { + project.format(buffers, target, true, trigger, cx) + }); + + cx.spawn_in(window, async move |editor, cx| { + let transaction = futures::select_biased! { + transaction = format.log_err().fuse() => transaction, + () = timeout => { + log::warn!("timed out waiting for formatting"); + None + } + }; + + buffer + .update(cx, |buffer, cx| { + if let Some(transaction) = transaction { + if !buffer.is_singleton() { + buffer.push_transaction(&transaction.0, cx); + } + } + cx.notify(); + }) + .ok(); + + if let Some(transaction_id_now) = + buffer.read_with(cx, |b, cx| b.last_transaction_id(cx))? + { + let has_new_transaction = transaction_id_prev != Some(transaction_id_now); + if has_new_transaction { + _ = editor.update(cx, |editor, _| { + editor + .selection_history + .insert_transaction(transaction_id_now, selections_prev); + }); + } + } + + Ok(()) + }) + } + + fn organize_imports( + &mut self, + _: &OrganizeImports, + window: &mut Window, + cx: &mut Context, + ) -> Option>> { + self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + let project = match &self.project { + Some(project) => project.clone(), + None => return None, + }; + Some(self.perform_code_action_kind( + project, + CodeActionKind::SOURCE_ORGANIZE_IMPORTS, + window, + cx, + )) + } + + fn perform_code_action_kind( + &mut self, + project: Entity, + kind: CodeActionKind, + window: &mut Window, + cx: &mut Context, + ) -> Task> { + let buffer = self.buffer.clone(); + let buffers = buffer.read(cx).all_buffers(); + let mut timeout = cx.background_executor().timer(CODE_ACTION_TIMEOUT).fuse(); + let apply_action = project.update(cx, |project, cx| { + project.apply_code_action_kind(buffers, kind, true, cx) + }); + cx.spawn_in(window, async move |_, cx| { + let transaction = futures::select_biased! { + () = timeout => { + log::warn!("timed out waiting for executing code action"); + None + } + transaction = apply_action.log_err().fuse() => transaction, + }; + buffer + .update(cx, |buffer, cx| { + // check if we need this + if let Some(transaction) = transaction { + if !buffer.is_singleton() { + buffer.push_transaction(&transaction.0, cx); + } + } + cx.notify(); + }) + .ok(); + Ok(()) + }) + } + + fn restart_language_server( + &mut self, + _: &RestartLanguageServer, + _: &mut Window, + cx: &mut Context, + ) { + if let Some(project) = self.project.clone() { + self.buffer.update(cx, |multi_buffer, cx| { + project.update(cx, |project, cx| { + project.restart_language_servers_for_buffers( + multi_buffer.all_buffers().into_iter().collect(), + cx, + ); + }); + }) + } + } + + fn stop_language_server( + &mut self, + _: &StopLanguageServer, + _: &mut Window, + cx: &mut Context, + ) { + if let Some(project) = self.project.clone() { + self.buffer.update(cx, |multi_buffer, cx| { + project.update(cx, |project, cx| { + project.stop_language_servers_for_buffers( + multi_buffer.all_buffers().into_iter().collect(), + cx, + ); + cx.emit(project::Event::RefreshInlayHints); + }); + }); + } + } + + fn cancel_language_server_work( + workspace: &mut Workspace, + _: &actions::CancelLanguageServerWork, + _: &mut Window, + cx: &mut Context, + ) { + let project = workspace.project(); + let buffers = workspace + .active_item(cx) + .and_then(|item| item.act_as::(cx)) + .map_or(HashSet::default(), |editor| { + editor.read(cx).buffer.read(cx).all_buffers() + }); + project.update(cx, |project, cx| { + project.cancel_language_server_work_for_buffers(buffers, cx); + }); + } + + fn show_character_palette( + &mut self, + _: &ShowCharacterPalette, + window: &mut Window, + _: &mut Context, + ) { + window.show_character_palette(); + } + + fn refresh_active_diagnostics(&mut self, cx: &mut Context) { + if let ActiveDiagnostic::Group(active_diagnostics) = &mut self.active_diagnostics { + let buffer = self.buffer.read(cx).snapshot(cx); + let primary_range_start = active_diagnostics.active_range.start.to_offset(&buffer); + let primary_range_end = active_diagnostics.active_range.end.to_offset(&buffer); + let is_valid = buffer + .diagnostics_in_range::(primary_range_start..primary_range_end) + .any(|entry| { + entry.diagnostic.is_primary + && !entry.range.is_empty() + && entry.range.start == primary_range_start + && entry.diagnostic.message == active_diagnostics.active_message + }); + + if !is_valid { + self.dismiss_diagnostics(cx); + } + } + } + + pub fn active_diagnostic_group(&self) -> Option<&ActiveDiagnosticGroup> { + match &self.active_diagnostics { + ActiveDiagnostic::Group(group) => Some(group), + _ => None, + } + } + + pub fn set_all_diagnostics_active(&mut self, cx: &mut Context) { + self.dismiss_diagnostics(cx); + self.active_diagnostics = ActiveDiagnostic::All; + } + + fn activate_diagnostics( + &mut self, + buffer_id: BufferId, + diagnostic: DiagnosticEntry, + window: &mut Window, + cx: &mut Context, + ) { + if matches!(self.active_diagnostics, ActiveDiagnostic::All) { + return; + } + self.dismiss_diagnostics(cx); + let snapshot = self.snapshot(window, cx); + let buffer = self.buffer.read(cx).snapshot(cx); + let Some(renderer) = GlobalDiagnosticRenderer::global(cx) else { + return; + }; + + let diagnostic_group = buffer + .diagnostic_group(buffer_id, diagnostic.diagnostic.group_id) + .collect::>(); + + let blocks = + renderer.render_group(diagnostic_group, buffer_id, snapshot, cx.weak_entity(), cx); + + let blocks = self.display_map.update(cx, |display_map, cx| { + display_map.insert_blocks(blocks, cx).into_iter().collect() + }); + self.active_diagnostics = ActiveDiagnostic::Group(ActiveDiagnosticGroup { + active_range: buffer.anchor_before(diagnostic.range.start) + ..buffer.anchor_after(diagnostic.range.end), + active_message: diagnostic.diagnostic.message.clone(), + group_id: diagnostic.diagnostic.group_id, + blocks, + }); + cx.notify(); + } + + fn dismiss_diagnostics(&mut self, cx: &mut Context) { + if matches!(self.active_diagnostics, ActiveDiagnostic::All) { + return; + }; + + let prev = mem::replace(&mut self.active_diagnostics, ActiveDiagnostic::None); + if let ActiveDiagnostic::Group(group) = prev { + self.display_map.update(cx, |display_map, cx| { + display_map.remove_blocks(group.blocks, cx); + }); + cx.notify(); + } + } + + /// Disable inline diagnostics rendering for this editor. + pub fn disable_inline_diagnostics(&mut self) { + self.inline_diagnostics_enabled = false; + self.inline_diagnostics_update = Task::ready(()); + self.inline_diagnostics.clear(); + } + + pub fn inline_diagnostics_enabled(&self) -> bool { + self.inline_diagnostics_enabled + } + + pub fn show_inline_diagnostics(&self) -> bool { + self.show_inline_diagnostics + } + + pub fn toggle_inline_diagnostics( + &mut self, + _: &ToggleInlineDiagnostics, + window: &mut Window, + cx: &mut Context, + ) { + self.show_inline_diagnostics = !self.show_inline_diagnostics; + self.refresh_inline_diagnostics(false, window, cx); + } + + fn refresh_inline_diagnostics( + &mut self, + debounce: bool, + window: &mut Window, + cx: &mut Context, + ) { + if !self.inline_diagnostics_enabled || !self.show_inline_diagnostics { + self.inline_diagnostics_update = Task::ready(()); + self.inline_diagnostics.clear(); + return; + } + + let debounce_ms = ProjectSettings::get_global(cx) + .diagnostics + .inline + .update_debounce_ms; + let debounce = if debounce && debounce_ms > 0 { + Some(Duration::from_millis(debounce_ms)) + } else { + None + }; + self.inline_diagnostics_update = cx.spawn_in(window, async move |editor, cx| { + let editor = editor.upgrade().unwrap(); + + if let Some(debounce) = debounce { + cx.background_executor().timer(debounce).await; + } + let Some(snapshot) = editor + .update(cx, |editor, cx| editor.buffer().read(cx).snapshot(cx)) + .ok() + else { + return; + }; + + let new_inline_diagnostics = cx + .background_spawn(async move { + let mut inline_diagnostics = Vec::<(Anchor, InlineDiagnostic)>::new(); + for diagnostic_entry in snapshot.diagnostics_in_range(0..snapshot.len()) { + let message = diagnostic_entry + .diagnostic + .message + .split_once('\n') + .map(|(line, _)| line) + .map(SharedString::new) + .unwrap_or_else(|| { + SharedString::from(diagnostic_entry.diagnostic.message) + }); + let start_anchor = snapshot.anchor_before(diagnostic_entry.range.start); + let (Ok(i) | Err(i)) = inline_diagnostics + .binary_search_by(|(probe, _)| probe.cmp(&start_anchor, &snapshot)); + inline_diagnostics.insert( + i, + ( + start_anchor, + InlineDiagnostic { + message, + group_id: diagnostic_entry.diagnostic.group_id, + start: diagnostic_entry.range.start.to_point(&snapshot), + is_primary: diagnostic_entry.diagnostic.is_primary, + severity: diagnostic_entry.diagnostic.severity, + }, + ), + ); + } + inline_diagnostics + }) + .await; + + editor + .update(cx, |editor, cx| { + editor.inline_diagnostics = new_inline_diagnostics; + cx.notify(); + }) + .ok(); + }); + } + + pub fn set_selections_from_remote( + &mut self, + selections: Vec>, + pending_selection: Option>, + window: &mut Window, + cx: &mut Context, + ) { + let old_cursor_position = self.selections.newest_anchor().head(); + self.selections.change_with(cx, |s| { + s.select_anchors(selections); + if let Some(pending_selection) = pending_selection { + s.set_pending(pending_selection, SelectMode::Character); + } else { + s.clear_pending(); + } + }); + self.selections_did_change(false, &old_cursor_position, true, window, cx); + } + + fn push_to_selection_history(&mut self) { + self.selection_history.push(SelectionHistoryEntry { + selections: self.selections.disjoint_anchors(), + select_next_state: self.select_next_state.clone(), + select_prev_state: self.select_prev_state.clone(), + add_selections_state: self.add_selections_state.clone(), + }); + } + + pub fn transact( + &mut self, + window: &mut Window, + cx: &mut Context, + update: impl FnOnce(&mut Self, &mut Window, &mut Context), + ) -> Option { + self.start_transaction_at(Instant::now(), window, cx); + update(self, window, cx); + self.end_transaction_at(Instant::now(), cx) + } + + pub fn start_transaction_at( + &mut self, + now: Instant, + window: &mut Window, + cx: &mut Context, + ) { + self.end_selection(window, cx); + if let Some(tx_id) = self + .buffer + .update(cx, |buffer, cx| buffer.start_transaction_at(now, cx)) + { + self.selection_history + .insert_transaction(tx_id, self.selections.disjoint_anchors()); + cx.emit(EditorEvent::TransactionBegun { + transaction_id: tx_id, + }) + } + } + + pub fn end_transaction_at( + &mut self, + now: Instant, + cx: &mut Context, + ) -> Option { + if let Some(transaction_id) = self + .buffer + .update(cx, |buffer, cx| buffer.end_transaction_at(now, cx)) + { + if let Some((_, end_selections)) = + self.selection_history.transaction_mut(transaction_id) + { + *end_selections = Some(self.selections.disjoint_anchors()); + } else { + log::error!("unexpectedly ended a transaction that wasn't started by this editor"); + } + + cx.emit(EditorEvent::Edited { transaction_id }); + Some(transaction_id) + } else { + None + } + } + + pub fn set_mark(&mut self, _: &actions::SetMark, window: &mut Window, cx: &mut Context) { + if self.selection_mark_mode { + self.change_selections(None, window, cx, |s| { + s.move_with(|_, sel| { + sel.collapse_to(sel.head(), SelectionGoal::None); + }); + }) + } + self.selection_mark_mode = true; + cx.notify(); + } + + pub fn swap_selection_ends( + &mut self, + _: &actions::SwapSelectionEnds, + window: &mut Window, + cx: &mut Context, + ) { + self.change_selections(None, window, cx, |s| { + s.move_with(|_, sel| { + if sel.start != sel.end { + sel.reversed = !sel.reversed + } + }); + }); + self.request_autoscroll(Autoscroll::newest(), cx); + cx.notify(); + } + + pub fn toggle_fold( + &mut self, + _: &actions::ToggleFold, + window: &mut Window, + cx: &mut Context, + ) { + if self.is_singleton(cx) { + let selection = self.selections.newest::(cx); + + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let range = if selection.is_empty() { + let point = selection.head().to_display_point(&display_map); + let start = DisplayPoint::new(point.row(), 0).to_point(&display_map); + let end = DisplayPoint::new(point.row(), display_map.line_len(point.row())) + .to_point(&display_map); + start..end + } else { + selection.range() + }; + if display_map.folds_in_range(range).next().is_some() { + self.unfold_lines(&Default::default(), window, cx) + } else { + self.fold(&Default::default(), window, cx) + } + } else { + let multi_buffer_snapshot = self.buffer.read(cx).snapshot(cx); + let buffer_ids: HashSet<_> = self + .selections + .disjoint_anchor_ranges() + .flat_map(|range| multi_buffer_snapshot.buffer_ids_for_range(range)) + .collect(); + + let should_unfold = buffer_ids + .iter() + .any(|buffer_id| self.is_buffer_folded(*buffer_id, cx)); + + for buffer_id in buffer_ids { + if should_unfold { + self.unfold_buffer(buffer_id, cx); + } else { + self.fold_buffer(buffer_id, cx); + } + } + } + } + + pub fn toggle_fold_recursive( + &mut self, + _: &actions::ToggleFoldRecursive, + window: &mut Window, + cx: &mut Context, + ) { + let selection = self.selections.newest::(cx); + + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let range = if selection.is_empty() { + let point = selection.head().to_display_point(&display_map); + let start = DisplayPoint::new(point.row(), 0).to_point(&display_map); + let end = DisplayPoint::new(point.row(), display_map.line_len(point.row())) + .to_point(&display_map); + start..end + } else { + selection.range() + }; + if display_map.folds_in_range(range).next().is_some() { + self.unfold_recursive(&Default::default(), window, cx) + } else { + self.fold_recursive(&Default::default(), window, cx) + } + } + + pub fn fold(&mut self, _: &actions::Fold, window: &mut Window, cx: &mut Context) { + if self.is_singleton(cx) { + let mut to_fold = Vec::new(); + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let selections = self.selections.all_adjusted(cx); + + for selection in selections { + let range = selection.range().sorted(); + let buffer_start_row = range.start.row; + + if range.start.row != range.end.row { + let mut found = false; + let mut row = range.start.row; + while row <= range.end.row { + if let Some(crease) = display_map.crease_for_buffer_row(MultiBufferRow(row)) + { + found = true; + row = crease.range().end.row + 1; + to_fold.push(crease); + } else { + row += 1 + } + } + if found { + continue; + } + } + + for row in (0..=range.start.row).rev() { + if let Some(crease) = display_map.crease_for_buffer_row(MultiBufferRow(row)) { + if crease.range().end.row >= buffer_start_row { + to_fold.push(crease); + if row <= range.start.row { + break; + } + } + } + } + } + + self.fold_creases(to_fold, true, window, cx); + } else { + let multi_buffer_snapshot = self.buffer.read(cx).snapshot(cx); + let buffer_ids = self + .selections + .disjoint_anchor_ranges() + .flat_map(|range| multi_buffer_snapshot.buffer_ids_for_range(range)) + .collect::>(); + for buffer_id in buffer_ids { + self.fold_buffer(buffer_id, cx); + } + } + } + + fn fold_at_level( + &mut self, + fold_at: &FoldAtLevel, + window: &mut Window, + cx: &mut Context, + ) { + if !self.buffer.read(cx).is_singleton() { + return; + } + + let fold_at_level = fold_at.0; + let snapshot = self.buffer.read(cx).snapshot(cx); + let mut to_fold = Vec::new(); + let mut stack = vec![(0, snapshot.max_row().0, 1)]; + + while let Some((mut start_row, end_row, current_level)) = stack.pop() { + while start_row < end_row { + match self + .snapshot(window, cx) + .crease_for_buffer_row(MultiBufferRow(start_row)) + { + Some(crease) => { + let nested_start_row = crease.range().start.row + 1; + let nested_end_row = crease.range().end.row; + + if current_level < fold_at_level { + stack.push((nested_start_row, nested_end_row, current_level + 1)); + } else if current_level == fold_at_level { + to_fold.push(crease); + } + + start_row = nested_end_row + 1; + } + None => start_row += 1, + } + } + } + + self.fold_creases(to_fold, true, window, cx); + } + + pub fn fold_all(&mut self, _: &actions::FoldAll, window: &mut Window, cx: &mut Context) { + if self.buffer.read(cx).is_singleton() { + let mut fold_ranges = Vec::new(); + let snapshot = self.buffer.read(cx).snapshot(cx); + + for row in 0..snapshot.max_row().0 { + if let Some(foldable_range) = self + .snapshot(window, cx) + .crease_for_buffer_row(MultiBufferRow(row)) + { + fold_ranges.push(foldable_range); + } + } + + self.fold_creases(fold_ranges, true, window, cx); + } else { + self.toggle_fold_multiple_buffers = cx.spawn_in(window, async move |editor, cx| { + editor + .update_in(cx, |editor, _, cx| { + for buffer_id in editor.buffer.read(cx).excerpt_buffer_ids() { + editor.fold_buffer(buffer_id, cx); + } + }) + .ok(); + }); + } + } + + pub fn fold_function_bodies( + &mut self, + _: &actions::FoldFunctionBodies, + window: &mut Window, + cx: &mut Context, + ) { + let snapshot = self.buffer.read(cx).snapshot(cx); + + let ranges = snapshot + .text_object_ranges(0..snapshot.len(), TreeSitterOptions::default()) + .filter_map(|(range, obj)| (obj == TextObject::InsideFunction).then_some(range)) + .collect::>(); + + let creases = ranges + .into_iter() + .map(|range| Crease::simple(range, self.display_map.read(cx).fold_placeholder.clone())) + .collect(); + + self.fold_creases(creases, true, window, cx); + } + + pub fn fold_recursive( + &mut self, + _: &actions::FoldRecursive, + window: &mut Window, + cx: &mut Context, + ) { + let mut to_fold = Vec::new(); + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let selections = self.selections.all_adjusted(cx); + + for selection in selections { + let range = selection.range().sorted(); + let buffer_start_row = range.start.row; + + if range.start.row != range.end.row { + let mut found = false; + for row in range.start.row..=range.end.row { + if let Some(crease) = display_map.crease_for_buffer_row(MultiBufferRow(row)) { + found = true; + to_fold.push(crease); + } + } + if found { + continue; + } + } + + for row in (0..=range.start.row).rev() { + if let Some(crease) = display_map.crease_for_buffer_row(MultiBufferRow(row)) { + if crease.range().end.row >= buffer_start_row { + to_fold.push(crease); + } else { + break; + } + } + } + } + + self.fold_creases(to_fold, true, window, cx); + } + + pub fn fold_at( + &mut self, + buffer_row: MultiBufferRow, + window: &mut Window, + cx: &mut Context, + ) { + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + + if let Some(crease) = display_map.crease_for_buffer_row(buffer_row) { + let autoscroll = self + .selections + .all::(cx) + .iter() + .any(|selection| crease.range().overlaps(&selection.range())); + + self.fold_creases(vec![crease], autoscroll, window, cx); + } + } + + pub fn unfold_lines(&mut self, _: &UnfoldLines, _window: &mut Window, cx: &mut Context) { + if self.is_singleton(cx) { + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let buffer = &display_map.buffer_snapshot; + let selections = self.selections.all::(cx); + let ranges = selections + .iter() + .map(|s| { + let range = s.display_range(&display_map).sorted(); + let mut start = range.start.to_point(&display_map); + let mut end = range.end.to_point(&display_map); + start.column = 0; + end.column = buffer.line_len(MultiBufferRow(end.row)); + start..end + }) + .collect::>(); + + self.unfold_ranges(&ranges, true, true, cx); + } else { + let multi_buffer_snapshot = self.buffer.read(cx).snapshot(cx); + let buffer_ids = self + .selections + .disjoint_anchor_ranges() + .flat_map(|range| multi_buffer_snapshot.buffer_ids_for_range(range)) + .collect::>(); + for buffer_id in buffer_ids { + self.unfold_buffer(buffer_id, cx); + } + } + } + + pub fn unfold_recursive( + &mut self, + _: &UnfoldRecursive, + _window: &mut Window, + cx: &mut Context, + ) { + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let selections = self.selections.all::(cx); + let ranges = selections + .iter() + .map(|s| { + let mut range = s.display_range(&display_map).sorted(); + *range.start.column_mut() = 0; + *range.end.column_mut() = display_map.line_len(range.end.row()); + let start = range.start.to_point(&display_map); + let end = range.end.to_point(&display_map); + start..end + }) + .collect::>(); + + self.unfold_ranges(&ranges, true, true, cx); + } + + pub fn unfold_at( + &mut self, + buffer_row: MultiBufferRow, + _window: &mut Window, + cx: &mut Context, + ) { + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + + let intersection_range = Point::new(buffer_row.0, 0) + ..Point::new( + buffer_row.0, + display_map.buffer_snapshot.line_len(buffer_row), + ); + + let autoscroll = self + .selections + .all::(cx) + .iter() + .any(|selection| RangeExt::overlaps(&selection.range(), &intersection_range)); + + self.unfold_ranges(&[intersection_range], true, autoscroll, cx); + } + + pub fn unfold_all( + &mut self, + _: &actions::UnfoldAll, + _window: &mut Window, + cx: &mut Context, + ) { + if self.buffer.read(cx).is_singleton() { + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + self.unfold_ranges(&[0..display_map.buffer_snapshot.len()], true, true, cx); + } else { + self.toggle_fold_multiple_buffers = cx.spawn(async move |editor, cx| { + editor + .update(cx, |editor, cx| { + for buffer_id in editor.buffer.read(cx).excerpt_buffer_ids() { + editor.unfold_buffer(buffer_id, cx); + } + }) + .ok(); + }); + } + } + + pub fn fold_selected_ranges( + &mut self, + _: &FoldSelectedRanges, + window: &mut Window, + cx: &mut Context, + ) { + let selections = self.selections.all_adjusted(cx); + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let ranges = selections + .into_iter() + .map(|s| Crease::simple(s.range(), display_map.fold_placeholder.clone())) + .collect::>(); + self.fold_creases(ranges, true, window, cx); + } + + pub fn fold_ranges( + &mut self, + ranges: Vec>, + auto_scroll: bool, + window: &mut Window, + cx: &mut Context, + ) { + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let ranges = ranges + .into_iter() + .map(|r| Crease::simple(r, display_map.fold_placeholder.clone())) + .collect::>(); + self.fold_creases(ranges, auto_scroll, window, cx); + } + + pub fn fold_creases( + &mut self, + creases: Vec>, + auto_scroll: bool, + _window: &mut Window, + cx: &mut Context, + ) { + if creases.is_empty() { + return; + } + + let mut buffers_affected = HashSet::default(); + let multi_buffer = self.buffer().read(cx); + for crease in &creases { + if let Some((_, buffer, _)) = + multi_buffer.excerpt_containing(crease.range().start.clone(), cx) + { + buffers_affected.insert(buffer.read(cx).remote_id()); + }; + } + + self.display_map.update(cx, |map, cx| map.fold(creases, cx)); + + if auto_scroll { + self.request_autoscroll(Autoscroll::fit(), cx); + } + + cx.notify(); + + self.scrollbar_marker_state.dirty = true; + self.folds_did_change(cx); + } + + /// Removes any folds whose ranges intersect any of the given ranges. + pub fn unfold_ranges( + &mut self, + ranges: &[Range], + inclusive: bool, + auto_scroll: bool, + cx: &mut Context, + ) { + self.remove_folds_with(ranges, auto_scroll, cx, |map, cx| { + map.unfold_intersecting(ranges.iter().cloned(), inclusive, cx) + }); + self.folds_did_change(cx); + } + + pub fn fold_buffer(&mut self, buffer_id: BufferId, cx: &mut Context) { + if self.buffer().read(cx).is_singleton() || self.is_buffer_folded(buffer_id, cx) { + return; + } + let folded_excerpts = self.buffer().read(cx).excerpts_for_buffer(buffer_id, cx); + self.display_map.update(cx, |display_map, cx| { + display_map.fold_buffers([buffer_id], cx) + }); + cx.emit(EditorEvent::BufferFoldToggled { + ids: folded_excerpts.iter().map(|&(id, _)| id).collect(), + folded: true, + }); + cx.notify(); + } + + pub fn unfold_buffer(&mut self, buffer_id: BufferId, cx: &mut Context) { + if self.buffer().read(cx).is_singleton() || !self.is_buffer_folded(buffer_id, cx) { + return; + } + let unfolded_excerpts = self.buffer().read(cx).excerpts_for_buffer(buffer_id, cx); + self.display_map.update(cx, |display_map, cx| { + display_map.unfold_buffers([buffer_id], cx); + }); + cx.emit(EditorEvent::BufferFoldToggled { + ids: unfolded_excerpts.iter().map(|&(id, _)| id).collect(), + folded: false, + }); + cx.notify(); + } + + pub fn is_buffer_folded(&self, buffer: BufferId, cx: &App) -> bool { + self.display_map.read(cx).is_buffer_folded(buffer) + } + + pub fn folded_buffers<'a>(&self, cx: &'a App) -> &'a HashSet { + self.display_map.read(cx).folded_buffers() + } + + pub fn disable_header_for_buffer(&mut self, buffer_id: BufferId, cx: &mut Context) { + self.display_map.update(cx, |display_map, cx| { + display_map.disable_header_for_buffer(buffer_id, cx); + }); + cx.notify(); + } + + /// Removes any folds with the given ranges. + pub fn remove_folds_with_type( + &mut self, + ranges: &[Range], + type_id: TypeId, + auto_scroll: bool, + cx: &mut Context, + ) { + self.remove_folds_with(ranges, auto_scroll, cx, |map, cx| { + map.remove_folds_with_type(ranges.iter().cloned(), type_id, cx) + }); + self.folds_did_change(cx); + } + + fn remove_folds_with( + &mut self, + ranges: &[Range], + auto_scroll: bool, + cx: &mut Context, + update: impl FnOnce(&mut DisplayMap, &mut Context), + ) { + if ranges.is_empty() { + return; + } + + let mut buffers_affected = HashSet::default(); + let multi_buffer = self.buffer().read(cx); + for range in ranges { + if let Some((_, buffer, _)) = multi_buffer.excerpt_containing(range.start.clone(), cx) { + buffers_affected.insert(buffer.read(cx).remote_id()); + }; + } + + self.display_map.update(cx, update); + + if auto_scroll { + self.request_autoscroll(Autoscroll::fit(), cx); + } + + cx.notify(); + self.scrollbar_marker_state.dirty = true; + self.active_indent_guides_state.dirty = true; + } + + pub fn update_fold_widths( + &mut self, + widths: impl IntoIterator, + cx: &mut Context, + ) -> bool { + self.display_map + .update(cx, |map, cx| map.update_fold_widths(widths, cx)) + } + + pub fn default_fold_placeholder(&self, cx: &App) -> FoldPlaceholder { + self.display_map.read(cx).fold_placeholder.clone() + } + + pub fn set_expand_all_diff_hunks(&mut self, cx: &mut App) { + self.buffer.update(cx, |buffer, cx| { + buffer.set_all_diff_hunks_expanded(cx); + }); + } + + pub fn expand_all_diff_hunks( + &mut self, + _: &ExpandAllDiffHunks, + _window: &mut Window, + cx: &mut Context, + ) { + self.buffer.update(cx, |buffer, cx| { + buffer.expand_diff_hunks(vec![Anchor::min()..Anchor::max()], cx) + }); + } + + pub fn toggle_selected_diff_hunks( + &mut self, + _: &ToggleSelectedDiffHunks, + _window: &mut Window, + cx: &mut Context, + ) { + let ranges: Vec<_> = self.selections.disjoint.iter().map(|s| s.range()).collect(); + self.toggle_diff_hunks_in_ranges(ranges, cx); + } + + pub fn diff_hunks_in_ranges<'a>( + &'a self, + ranges: &'a [Range], + buffer: &'a MultiBufferSnapshot, + ) -> impl 'a + Iterator { + ranges.iter().flat_map(move |range| { + let end_excerpt_id = range.end.excerpt_id; + let range = range.to_point(buffer); + let mut peek_end = range.end; + if range.end.row < buffer.max_row().0 { + peek_end = Point::new(range.end.row + 1, 0); + } + buffer + .diff_hunks_in_range(range.start..peek_end) + .filter(move |hunk| hunk.excerpt_id.cmp(&end_excerpt_id, buffer).is_le()) + }) + } + + pub fn has_stageable_diff_hunks_in_ranges( + &self, + ranges: &[Range], + snapshot: &MultiBufferSnapshot, + ) -> bool { + let mut hunks = self.diff_hunks_in_ranges(ranges, &snapshot); + hunks.any(|hunk| hunk.status().has_secondary_hunk()) + } + + pub fn toggle_staged_selected_diff_hunks( + &mut self, + _: &::git::ToggleStaged, + _: &mut Window, + cx: &mut Context, + ) { + let snapshot = self.buffer.read(cx).snapshot(cx); + let ranges: Vec<_> = self.selections.disjoint.iter().map(|s| s.range()).collect(); + let stage = self.has_stageable_diff_hunks_in_ranges(&ranges, &snapshot); + self.stage_or_unstage_diff_hunks(stage, ranges, cx); + } + + pub fn set_render_diff_hunk_controls( + &mut self, + render_diff_hunk_controls: RenderDiffHunkControlsFn, + cx: &mut Context, + ) { + self.render_diff_hunk_controls = render_diff_hunk_controls; + cx.notify(); + } + + pub fn stage_and_next( + &mut self, + _: &::git::StageAndNext, + window: &mut Window, + cx: &mut Context, + ) { + self.do_stage_or_unstage_and_next(true, window, cx); + } + + pub fn unstage_and_next( + &mut self, + _: &::git::UnstageAndNext, + window: &mut Window, + cx: &mut Context, + ) { + self.do_stage_or_unstage_and_next(false, window, cx); + } + + pub fn stage_or_unstage_diff_hunks( + &mut self, + stage: bool, + ranges: Vec>, + cx: &mut Context, + ) { + let task = self.save_buffers_for_ranges_if_needed(&ranges, cx); + cx.spawn(async move |this, cx| { + task.await?; + this.update(cx, |this, cx| { + let snapshot = this.buffer.read(cx).snapshot(cx); + let chunk_by = this + .diff_hunks_in_ranges(&ranges, &snapshot) + .chunk_by(|hunk| hunk.buffer_id); + for (buffer_id, hunks) in &chunk_by { + this.do_stage_or_unstage(stage, buffer_id, hunks, cx); + } + }) + }) + .detach_and_log_err(cx); + } + + fn save_buffers_for_ranges_if_needed( + &mut self, + ranges: &[Range], + cx: &mut Context, + ) -> Task> { + let multibuffer = self.buffer.read(cx); + let snapshot = multibuffer.read(cx); + let buffer_ids: HashSet<_> = ranges + .iter() + .flat_map(|range| snapshot.buffer_ids_for_range(range.clone())) + .collect(); + drop(snapshot); + + let mut buffers = HashSet::default(); + for buffer_id in buffer_ids { + if let Some(buffer_entity) = multibuffer.buffer(buffer_id) { + let buffer = buffer_entity.read(cx); + if buffer.file().is_some_and(|file| file.disk_state().exists()) && buffer.is_dirty() + { + buffers.insert(buffer_entity); + } + } + } + + if let Some(project) = &self.project { + project.update(cx, |project, cx| project.save_buffers(buffers, cx)) + } else { + Task::ready(Ok(())) + } + } + + fn do_stage_or_unstage_and_next( + &mut self, + stage: bool, + window: &mut Window, + cx: &mut Context, + ) { + let ranges = self.selections.disjoint_anchor_ranges().collect::>(); + + if ranges.iter().any(|range| range.start != range.end) { + self.stage_or_unstage_diff_hunks(stage, ranges, cx); + return; + } + + self.stage_or_unstage_diff_hunks(stage, ranges, cx); + let snapshot = self.snapshot(window, cx); + let position = self.selections.newest::(cx).head(); + let mut row = snapshot + .buffer_snapshot + .diff_hunks_in_range(position..snapshot.buffer_snapshot.max_point()) + .find(|hunk| hunk.row_range.start.0 > position.row) + .map(|hunk| hunk.row_range.start); + + let all_diff_hunks_expanded = self.buffer().read(cx).all_diff_hunks_expanded(); + // Outside of the project diff editor, wrap around to the beginning. + if !all_diff_hunks_expanded { + row = row.or_else(|| { + snapshot + .buffer_snapshot + .diff_hunks_in_range(Point::zero()..position) + .find(|hunk| hunk.row_range.end.0 < position.row) + .map(|hunk| hunk.row_range.start) + }); + } + + if let Some(row) = row { + let destination = Point::new(row.0, 0); + let autoscroll = Autoscroll::center(); + + self.unfold_ranges(&[destination..destination], false, false, cx); + self.change_selections(Some(autoscroll), window, cx, |s| { + s.select_ranges([destination..destination]); + }); + } + } + + fn do_stage_or_unstage( + &self, + stage: bool, + buffer_id: BufferId, + hunks: impl Iterator, + cx: &mut App, + ) -> Option<()> { + let project = self.project.as_ref()?; + let buffer = project.read(cx).buffer_for_id(buffer_id, cx)?; + let diff = self.buffer.read(cx).diff_for(buffer_id)?; + let buffer_snapshot = buffer.read(cx).snapshot(); + let file_exists = buffer_snapshot + .file() + .is_some_and(|file| file.disk_state().exists()); + diff.update(cx, |diff, cx| { + diff.stage_or_unstage_hunks( + stage, + &hunks + .map(|hunk| buffer_diff::DiffHunk { + buffer_range: hunk.buffer_range, + diff_base_byte_range: hunk.diff_base_byte_range, + secondary_status: hunk.secondary_status, + range: Point::zero()..Point::zero(), // unused + }) + .collect::>(), + &buffer_snapshot, + file_exists, + cx, + ) + }); + None + } + + pub fn expand_selected_diff_hunks(&mut self, cx: &mut Context) { + let ranges: Vec<_> = self.selections.disjoint.iter().map(|s| s.range()).collect(); + self.buffer + .update(cx, |buffer, cx| buffer.expand_diff_hunks(ranges, cx)) + } + + pub fn clear_expanded_diff_hunks(&mut self, cx: &mut Context) -> bool { + self.buffer.update(cx, |buffer, cx| { + let ranges = vec![Anchor::min()..Anchor::max()]; + if !buffer.all_diff_hunks_expanded() + && buffer.has_expanded_diff_hunks_in_ranges(&ranges, cx) + { + buffer.collapse_diff_hunks(ranges, cx); + true + } else { + false + } + }) + } + + fn toggle_diff_hunks_in_ranges( + &mut self, + ranges: Vec>, + cx: &mut Context, + ) { + self.buffer.update(cx, |buffer, cx| { + let expand = !buffer.has_expanded_diff_hunks_in_ranges(&ranges, cx); + buffer.expand_or_collapse_diff_hunks(ranges, expand, cx); + }) + } + + fn toggle_single_diff_hunk(&mut self, range: Range, cx: &mut Context) { + self.buffer.update(cx, |buffer, cx| { + let snapshot = buffer.snapshot(cx); + let excerpt_id = range.end.excerpt_id; + let point_range = range.to_point(&snapshot); + let expand = !buffer.single_hunk_is_expanded(range, cx); + buffer.expand_or_collapse_diff_hunks_inner([(point_range, excerpt_id)], expand, cx); + }) + } + + pub(crate) fn apply_all_diff_hunks( + &mut self, + _: &ApplyAllDiffHunks, + window: &mut Window, + cx: &mut Context, + ) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + + let buffers = self.buffer.read(cx).all_buffers(); + for branch_buffer in buffers { + branch_buffer.update(cx, |branch_buffer, cx| { + branch_buffer.merge_into_base(Vec::new(), cx); + }); + } + + if let Some(project) = self.project.clone() { + self.save(true, project, window, cx).detach_and_log_err(cx); + } + } + + pub(crate) fn apply_selected_diff_hunks( + &mut self, + _: &ApplyDiffHunk, + window: &mut Window, + cx: &mut Context, + ) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + let snapshot = self.snapshot(window, cx); + let hunks = snapshot.hunks_for_ranges(self.selections.ranges(cx)); + let mut ranges_by_buffer = HashMap::default(); + self.transact(window, cx, |editor, _window, cx| { + for hunk in hunks { + if let Some(buffer) = editor.buffer.read(cx).buffer(hunk.buffer_id) { + ranges_by_buffer + .entry(buffer.clone()) + .or_insert_with(Vec::new) + .push(hunk.buffer_range.to_offset(buffer.read(cx))); + } + } + + for (buffer, ranges) in ranges_by_buffer { + buffer.update(cx, |buffer, cx| { + buffer.merge_into_base(ranges, cx); + }); + } + }); + + if let Some(project) = self.project.clone() { + self.save(true, project, window, cx).detach_and_log_err(cx); + } + } + + pub fn set_gutter_hovered(&mut self, hovered: bool, cx: &mut Context) { + if hovered != self.gutter_hovered { + self.gutter_hovered = hovered; + cx.notify(); + } + } + + pub fn insert_blocks( + &mut self, + blocks: impl IntoIterator>, + autoscroll: Option, + cx: &mut Context, + ) -> Vec { + let blocks = self + .display_map + .update(cx, |display_map, cx| display_map.insert_blocks(blocks, cx)); + if let Some(autoscroll) = autoscroll { + self.request_autoscroll(autoscroll, cx); + } + cx.notify(); + blocks + } + + pub fn resize_blocks( + &mut self, + heights: HashMap, + autoscroll: Option, + cx: &mut Context, + ) { + self.display_map + .update(cx, |display_map, cx| display_map.resize_blocks(heights, cx)); + if let Some(autoscroll) = autoscroll { + self.request_autoscroll(autoscroll, cx); + } + cx.notify(); + } + + pub fn replace_blocks( + &mut self, + renderers: HashMap, + autoscroll: Option, + cx: &mut Context, + ) { + self.display_map + .update(cx, |display_map, _cx| display_map.replace_blocks(renderers)); + if let Some(autoscroll) = autoscroll { + self.request_autoscroll(autoscroll, cx); + } + cx.notify(); + } + + pub fn remove_blocks( + &mut self, + block_ids: HashSet, + autoscroll: Option, + cx: &mut Context, + ) { + self.display_map.update(cx, |display_map, cx| { + display_map.remove_blocks(block_ids, cx) + }); + if let Some(autoscroll) = autoscroll { + self.request_autoscroll(autoscroll, cx); + } + cx.notify(); + } + + pub fn row_for_block( + &self, + block_id: CustomBlockId, + cx: &mut Context, + ) -> Option { + self.display_map + .update(cx, |map, cx| map.row_for_block(block_id, cx)) + } + + pub(crate) fn set_focused_block(&mut self, focused_block: FocusedBlock) { + self.focused_block = Some(focused_block); + } + + pub(crate) fn take_focused_block(&mut self) -> Option { + self.focused_block.take() + } + + pub fn insert_creases( + &mut self, + creases: impl IntoIterator>, + cx: &mut Context, + ) -> Vec { + self.display_map + .update(cx, |map, cx| map.insert_creases(creases, cx)) + } + + pub fn remove_creases( + &mut self, + ids: impl IntoIterator, + cx: &mut Context, + ) { + self.display_map + .update(cx, |map, cx| map.remove_creases(ids, cx)); + } + + pub fn longest_row(&self, cx: &mut App) -> DisplayRow { + self.display_map + .update(cx, |map, cx| map.snapshot(cx)) + .longest_row() + } + + pub fn max_point(&self, cx: &mut App) -> DisplayPoint { + self.display_map + .update(cx, |map, cx| map.snapshot(cx)) + .max_point() + } + + pub fn text(&self, cx: &App) -> String { + self.buffer.read(cx).read(cx).text() + } + + pub fn is_empty(&self, cx: &App) -> bool { + self.buffer.read(cx).read(cx).is_empty() + } + + pub fn text_option(&self, cx: &App) -> Option { + let text = self.text(cx); + let text = text.trim(); + + if text.is_empty() { + return None; + } + + Some(text.to_string()) + } + + pub fn set_text( + &mut self, + text: impl Into>, + window: &mut Window, + cx: &mut Context, + ) { + self.transact(window, cx, |this, _, cx| { + this.buffer + .read(cx) + .as_singleton() + .expect("you can only call set_text on editors for singleton buffers") + .update(cx, |buffer, cx| buffer.set_text(text, cx)); + }); + } + + pub fn display_text(&self, cx: &mut App) -> String { + self.display_map + .update(cx, |map, cx| map.snapshot(cx)) + .text() + } + + pub fn wrap_guides(&self, cx: &App) -> SmallVec<[(usize, bool); 2]> { + let mut wrap_guides = smallvec::smallvec![]; + + if self.show_wrap_guides == Some(false) { + return wrap_guides; + } + + let settings = self.buffer.read(cx).language_settings(cx); + if settings.show_wrap_guides { + match self.soft_wrap_mode(cx) { + SoftWrap::Column(soft_wrap) => { + wrap_guides.push((soft_wrap as usize, true)); + } + SoftWrap::Bounded(soft_wrap) => { + wrap_guides.push((soft_wrap as usize, true)); + } + SoftWrap::GitDiff | SoftWrap::None | SoftWrap::EditorWidth => {} + } + wrap_guides.extend(settings.wrap_guides.iter().map(|guide| (*guide, false))) + } + + wrap_guides + } + + pub fn soft_wrap_mode(&self, cx: &App) -> SoftWrap { + let settings = self.buffer.read(cx).language_settings(cx); + let mode = self.soft_wrap_mode_override.unwrap_or(settings.soft_wrap); + match mode { + language_settings::SoftWrap::PreferLine | language_settings::SoftWrap::None => { + SoftWrap::None + } + language_settings::SoftWrap::EditorWidth => SoftWrap::EditorWidth, + language_settings::SoftWrap::PreferredLineLength => { + SoftWrap::Column(settings.preferred_line_length) + } + language_settings::SoftWrap::Bounded => { + SoftWrap::Bounded(settings.preferred_line_length) + } + } + } + + pub fn set_soft_wrap_mode( + &mut self, + mode: language_settings::SoftWrap, + + cx: &mut Context, + ) { + self.soft_wrap_mode_override = Some(mode); + cx.notify(); + } + + pub fn set_hard_wrap(&mut self, hard_wrap: Option, cx: &mut Context) { + self.hard_wrap = hard_wrap; + cx.notify(); + } + + pub fn set_text_style_refinement(&mut self, style: TextStyleRefinement) { + self.text_style_refinement = Some(style); + } + + /// called by the Element so we know what style we were most recently rendered with. + pub(crate) fn set_style( + &mut self, + style: EditorStyle, + window: &mut Window, + cx: &mut Context, + ) { + let rem_size = window.rem_size(); + self.display_map.update(cx, |map, cx| { + map.set_font( + style.text.font(), + style.text.font_size.to_pixels(rem_size), + cx, + ) + }); + self.style = Some(style); + } + + pub fn style(&self) -> Option<&EditorStyle> { + self.style.as_ref() + } + + // Called by the element. This method is not designed to be called outside of the editor + // element's layout code because it does not notify when rewrapping is computed synchronously. + pub(crate) fn set_wrap_width(&self, width: Option, cx: &mut App) -> bool { + self.display_map + .update(cx, |map, cx| map.set_wrap_width(width, cx)) + } + + pub fn set_soft_wrap(&mut self) { + self.soft_wrap_mode_override = Some(language_settings::SoftWrap::EditorWidth) + } + + pub fn toggle_soft_wrap(&mut self, _: &ToggleSoftWrap, _: &mut Window, cx: &mut Context) { + if self.soft_wrap_mode_override.is_some() { + self.soft_wrap_mode_override.take(); + } else { + let soft_wrap = match self.soft_wrap_mode(cx) { + SoftWrap::GitDiff => return, + SoftWrap::None => language_settings::SoftWrap::EditorWidth, + SoftWrap::EditorWidth | SoftWrap::Column(_) | SoftWrap::Bounded(_) => { + language_settings::SoftWrap::None + } + }; + self.soft_wrap_mode_override = Some(soft_wrap); + } + cx.notify(); + } + + pub fn toggle_tab_bar(&mut self, _: &ToggleTabBar, _: &mut Window, cx: &mut Context) { + let Some(workspace) = self.workspace() else { + return; + }; + let fs = workspace.read(cx).app_state().fs.clone(); + let current_show = TabBarSettings::get_global(cx).show; + update_settings_file::(fs, cx, move |setting, _| { + setting.show = Some(!current_show); + }); + } + + pub fn toggle_indent_guides( + &mut self, + _: &ToggleIndentGuides, + _: &mut Window, + cx: &mut Context, + ) { + let currently_enabled = self.should_show_indent_guides().unwrap_or_else(|| { + self.buffer + .read(cx) + .language_settings(cx) + .indent_guides + .enabled + }); + self.show_indent_guides = Some(!currently_enabled); + cx.notify(); + } + + fn should_show_indent_guides(&self) -> Option { + self.show_indent_guides + } + + pub fn toggle_line_numbers( + &mut self, + _: &ToggleLineNumbers, + _: &mut Window, + cx: &mut Context, + ) { + let mut editor_settings = EditorSettings::get_global(cx).clone(); + editor_settings.gutter.line_numbers = !editor_settings.gutter.line_numbers; + EditorSettings::override_global(editor_settings, cx); + } + + pub fn line_numbers_enabled(&self, cx: &App) -> bool { + if let Some(show_line_numbers) = self.show_line_numbers { + return show_line_numbers; + } + EditorSettings::get_global(cx).gutter.line_numbers + } + + pub fn should_use_relative_line_numbers(&self, cx: &mut App) -> bool { + self.use_relative_line_numbers + .unwrap_or(EditorSettings::get_global(cx).relative_line_numbers) + } + + pub fn toggle_relative_line_numbers( + &mut self, + _: &ToggleRelativeLineNumbers, + _: &mut Window, + cx: &mut Context, + ) { + let is_relative = self.should_use_relative_line_numbers(cx); + self.set_relative_line_number(Some(!is_relative), cx) + } + + pub fn set_relative_line_number(&mut self, is_relative: Option, cx: &mut Context) { + self.use_relative_line_numbers = is_relative; + cx.notify(); + } + + pub fn set_show_gutter(&mut self, show_gutter: bool, cx: &mut Context) { + self.show_gutter = show_gutter; + cx.notify(); + } + + pub fn set_show_scrollbars(&mut self, show_scrollbars: bool, cx: &mut Context) { + self.show_scrollbars = show_scrollbars; + cx.notify(); + } + + pub fn disable_scrolling(&mut self, cx: &mut Context) { + self.disable_scrolling = true; + cx.notify(); + } + + pub fn set_show_line_numbers(&mut self, show_line_numbers: bool, cx: &mut Context) { + self.show_line_numbers = Some(show_line_numbers); + cx.notify(); + } + + pub fn disable_expand_excerpt_buttons(&mut self, cx: &mut Context) { + self.disable_expand_excerpt_buttons = true; + cx.notify(); + } + + pub fn set_show_git_diff_gutter(&mut self, show_git_diff_gutter: bool, cx: &mut Context) { + self.show_git_diff_gutter = Some(show_git_diff_gutter); + cx.notify(); + } + + pub fn set_show_code_actions(&mut self, show_code_actions: bool, cx: &mut Context) { + self.show_code_actions = Some(show_code_actions); + cx.notify(); + } + + pub fn set_show_runnables(&mut self, show_runnables: bool, cx: &mut Context) { + self.show_runnables = Some(show_runnables); + cx.notify(); + } + + pub fn set_show_breakpoints(&mut self, show_breakpoints: bool, cx: &mut Context) { + self.show_breakpoints = Some(show_breakpoints); + cx.notify(); + } + + pub fn set_masked(&mut self, masked: bool, cx: &mut Context) { + if self.display_map.read(cx).masked != masked { + self.display_map.update(cx, |map, _| map.masked = masked); + } + cx.notify() + } + + pub fn set_show_wrap_guides(&mut self, show_wrap_guides: bool, cx: &mut Context) { + self.show_wrap_guides = Some(show_wrap_guides); + cx.notify(); + } + + pub fn set_show_indent_guides(&mut self, show_indent_guides: bool, cx: &mut Context) { + self.show_indent_guides = Some(show_indent_guides); + cx.notify(); + } + + pub fn working_directory(&self, cx: &App) -> Option { + if let Some(buffer) = self.buffer().read(cx).as_singleton() { + if let Some(file) = buffer.read(cx).file().and_then(|f| f.as_local()) { + if let Some(dir) = file.abs_path(cx).parent() { + return Some(dir.to_owned()); + } + } + + if let Some(project_path) = buffer.read(cx).project_path(cx) { + return Some(project_path.path.to_path_buf()); + } + } + + None + } + + fn target_file<'a>(&self, cx: &'a App) -> Option<&'a dyn language::LocalFile> { + self.active_excerpt(cx)? + .1 + .read(cx) + .file() + .and_then(|f| f.as_local()) + } + + pub fn target_file_abs_path(&self, cx: &mut Context) -> Option { + self.active_excerpt(cx).and_then(|(_, buffer, _)| { + let buffer = buffer.read(cx); + if let Some(project_path) = buffer.project_path(cx) { + let project = self.project.as_ref()?.read(cx); + project.absolute_path(&project_path, cx) + } else { + buffer + .file() + .and_then(|file| file.as_local().map(|file| file.abs_path(cx))) + } + }) + } + + fn target_file_path(&self, cx: &mut Context) -> Option { + self.active_excerpt(cx).and_then(|(_, buffer, _)| { + let project_path = buffer.read(cx).project_path(cx)?; + let project = self.project.as_ref()?.read(cx); + let entry = project.entry_for_path(&project_path, cx)?; + let path = entry.path.to_path_buf(); + Some(path) + }) + } + + pub fn reveal_in_finder( + &mut self, + _: &RevealInFileManager, + _window: &mut Window, + cx: &mut Context, + ) { + if let Some(target) = self.target_file(cx) { + cx.reveal_path(&target.abs_path(cx)); + } + } + + pub fn copy_path( + &mut self, + _: &zed_actions::workspace::CopyPath, + _window: &mut Window, + cx: &mut Context, + ) { + if let Some(path) = self.target_file_abs_path(cx) { + if let Some(path) = path.to_str() { + cx.write_to_clipboard(ClipboardItem::new_string(path.to_string())); + } + } + } + + pub fn copy_relative_path( + &mut self, + _: &zed_actions::workspace::CopyRelativePath, + _window: &mut Window, + cx: &mut Context, + ) { + if let Some(path) = self.target_file_path(cx) { + if let Some(path) = path.to_str() { + cx.write_to_clipboard(ClipboardItem::new_string(path.to_string())); + } + } + } + + pub fn project_path(&self, cx: &App) -> Option { + if let Some(buffer) = self.buffer.read(cx).as_singleton() { + buffer.read(cx).project_path(cx) + } else { + None + } + } + + // Returns true if the editor handled a go-to-line request + pub fn go_to_active_debug_line(&mut self, window: &mut Window, cx: &mut Context) -> bool { + maybe!({ + let breakpoint_store = self.breakpoint_store.as_ref()?; + + let Some(active_stack_frame) = breakpoint_store.read(cx).active_position().cloned() + else { + self.clear_row_highlights::(); + return None; + }; + + let position = active_stack_frame.position; + let buffer_id = position.buffer_id?; + let snapshot = self + .project + .as_ref()? + .read(cx) + .buffer_for_id(buffer_id, cx)? + .read(cx) + .snapshot(); + + let mut handled = false; + for (id, ExcerptRange { context, .. }) in + self.buffer.read(cx).excerpts_for_buffer(buffer_id, cx) + { + if context.start.cmp(&position, &snapshot).is_ge() + || context.end.cmp(&position, &snapshot).is_lt() + { + continue; + } + let snapshot = self.buffer.read(cx).snapshot(cx); + let multibuffer_anchor = snapshot.anchor_in_excerpt(id, position)?; + + handled = true; + self.clear_row_highlights::(); + self.go_to_line::( + multibuffer_anchor, + Some(cx.theme().colors().editor_debugger_active_line_background), + window, + cx, + ); + + cx.notify(); + } + + handled.then_some(()) + }) + .is_some() + } + + pub fn copy_file_name_without_extension( + &mut self, + _: &CopyFileNameWithoutExtension, + _: &mut Window, + cx: &mut Context, + ) { + if let Some(file) = self.target_file(cx) { + if let Some(file_stem) = file.path().file_stem() { + if let Some(name) = file_stem.to_str() { + cx.write_to_clipboard(ClipboardItem::new_string(name.to_string())); + } + } + } + } + + pub fn copy_file_name(&mut self, _: &CopyFileName, _: &mut Window, cx: &mut Context) { + if let Some(file) = self.target_file(cx) { + if let Some(file_name) = file.path().file_name() { + if let Some(name) = file_name.to_str() { + cx.write_to_clipboard(ClipboardItem::new_string(name.to_string())); + } + } + } + } + + pub fn toggle_git_blame( + &mut self, + _: &::git::Blame, + window: &mut Window, + cx: &mut Context, + ) { + self.show_git_blame_gutter = !self.show_git_blame_gutter; + + if self.show_git_blame_gutter && !self.has_blame_entries(cx) { + self.start_git_blame(true, window, cx); + } + + cx.notify(); + } + + pub fn toggle_git_blame_inline( + &mut self, + _: &ToggleGitBlameInline, + window: &mut Window, + cx: &mut Context, + ) { + self.toggle_git_blame_inline_internal(true, window, cx); + cx.notify(); + } + + pub fn open_git_blame_commit( + &mut self, + _: &OpenGitBlameCommit, + window: &mut Window, + cx: &mut Context, + ) { + self.open_git_blame_commit_internal(window, cx); + } + + fn open_git_blame_commit_internal( + &mut self, + window: &mut Window, + cx: &mut Context, + ) -> Option<()> { + let blame = self.blame.as_ref()?; + let snapshot = self.snapshot(window, cx); + let cursor = self.selections.newest::(cx).head(); + let (buffer, point, _) = snapshot.buffer_snapshot.point_to_buffer_point(cursor)?; + let blame_entry = blame + .update(cx, |blame, cx| { + blame + .blame_for_rows( + &[RowInfo { + buffer_id: Some(buffer.remote_id()), + buffer_row: Some(point.row), + ..Default::default() + }], + cx, + ) + .next() + }) + .flatten()?; + let renderer = cx.global::().0.clone(); + let repo = blame.read(cx).repository(cx)?; + let workspace = self.workspace()?.downgrade(); + renderer.open_blame_commit(blame_entry, repo, workspace, window, cx); + None + } + + pub fn git_blame_inline_enabled(&self) -> bool { + self.git_blame_inline_enabled + } + + pub fn toggle_selection_menu( + &mut self, + _: &ToggleSelectionMenu, + _: &mut Window, + cx: &mut Context, + ) { + self.show_selection_menu = self + .show_selection_menu + .map(|show_selections_menu| !show_selections_menu) + .or_else(|| Some(!EditorSettings::get_global(cx).toolbar.selections_menu)); + + cx.notify(); + } + + pub fn selection_menu_enabled(&self, cx: &App) -> bool { + self.show_selection_menu + .unwrap_or_else(|| EditorSettings::get_global(cx).toolbar.selections_menu) + } + + fn start_git_blame( + &mut self, + user_triggered: bool, + window: &mut Window, + cx: &mut Context, + ) { + if let Some(project) = self.project.as_ref() { + let Some(buffer) = self.buffer().read(cx).as_singleton() else { + return; + }; + + if buffer.read(cx).file().is_none() { + return; + } + + let focused = self.focus_handle(cx).contains_focused(window, cx); + + let project = project.clone(); + let blame = cx.new(|cx| GitBlame::new(buffer, project, user_triggered, focused, cx)); + self.blame_subscription = + Some(cx.observe_in(&blame, window, |_, _, _, cx| cx.notify())); + self.blame = Some(blame); + } + } + + fn toggle_git_blame_inline_internal( + &mut self, + user_triggered: bool, + window: &mut Window, + cx: &mut Context, + ) { + if self.git_blame_inline_enabled { + self.git_blame_inline_enabled = false; + self.show_git_blame_inline = false; + self.show_git_blame_inline_delay_task.take(); + } else { + self.git_blame_inline_enabled = true; + self.start_git_blame_inline(user_triggered, window, cx); + } + + cx.notify(); + } + + fn start_git_blame_inline( + &mut self, + user_triggered: bool, + window: &mut Window, + cx: &mut Context, + ) { + self.start_git_blame(user_triggered, window, cx); + + if ProjectSettings::get_global(cx) + .git + .inline_blame_delay() + .is_some() + { + self.start_inline_blame_timer(window, cx); + } else { + self.show_git_blame_inline = true + } + } + + pub fn blame(&self) -> Option<&Entity> { + self.blame.as_ref() + } + + pub fn show_git_blame_gutter(&self) -> bool { + self.show_git_blame_gutter + } + + pub fn render_git_blame_gutter(&self, cx: &App) -> bool { + self.show_git_blame_gutter && self.has_blame_entries(cx) + } + + pub fn render_git_blame_inline(&self, window: &Window, cx: &App) -> bool { + self.show_git_blame_inline + && (self.focus_handle.is_focused(window) || self.inline_blame_popover.is_some()) + && !self.newest_selection_head_on_empty_line(cx) + && self.has_blame_entries(cx) + } + + fn has_blame_entries(&self, cx: &App) -> bool { + self.blame() + .map_or(false, |blame| blame.read(cx).has_generated_entries()) + } + + fn newest_selection_head_on_empty_line(&self, cx: &App) -> bool { + let cursor_anchor = self.selections.newest_anchor().head(); + + let snapshot = self.buffer.read(cx).snapshot(cx); + let buffer_row = MultiBufferRow(cursor_anchor.to_point(&snapshot).row); + + snapshot.line_len(buffer_row) == 0 + } + + fn get_permalink_to_line(&self, cx: &mut Context) -> Task> { + let buffer_and_selection = maybe!({ + let selection = self.selections.newest::(cx); + let selection_range = selection.range(); + + let multi_buffer = self.buffer().read(cx); + let multi_buffer_snapshot = multi_buffer.snapshot(cx); + let buffer_ranges = multi_buffer_snapshot.range_to_buffer_ranges(selection_range); + + let (buffer, range, _) = if selection.reversed { + buffer_ranges.first() + } else { + buffer_ranges.last() + }?; + + let selection = text::ToPoint::to_point(&range.start, &buffer).row + ..text::ToPoint::to_point(&range.end, &buffer).row; + Some(( + multi_buffer.buffer(buffer.remote_id()).unwrap().clone(), + selection, + )) + }); + + let Some((buffer, selection)) = buffer_and_selection else { + return Task::ready(Err(anyhow!("failed to determine buffer and selection"))); + }; + + let Some(project) = self.project.as_ref() else { + return Task::ready(Err(anyhow!("editor does not have project"))); + }; + + project.update(cx, |project, cx| { + project.get_permalink_to_line(&buffer, selection, cx) + }) + } + + pub fn copy_permalink_to_line( + &mut self, + _: &CopyPermalinkToLine, + window: &mut Window, + cx: &mut Context, + ) { + let permalink_task = self.get_permalink_to_line(cx); + let workspace = self.workspace(); + + cx.spawn_in(window, async move |_, cx| match permalink_task.await { + Ok(permalink) => { + cx.update(|_, cx| { + cx.write_to_clipboard(ClipboardItem::new_string(permalink.to_string())); + }) + .ok(); + } + Err(err) => { + let message = format!("Failed to copy permalink: {err}"); + + Err::<(), anyhow::Error>(err).log_err(); + + if let Some(workspace) = workspace { + workspace + .update_in(cx, |workspace, _, cx| { + struct CopyPermalinkToLine; + + workspace.show_toast( + Toast::new( + NotificationId::unique::(), + message, + ), + cx, + ) + }) + .ok(); + } + } + }) + .detach(); + } + + pub fn copy_file_location( + &mut self, + _: &CopyFileLocation, + _: &mut Window, + cx: &mut Context, + ) { + let selection = self.selections.newest::(cx).start.row + 1; + if let Some(file) = self.target_file(cx) { + if let Some(path) = file.path().to_str() { + cx.write_to_clipboard(ClipboardItem::new_string(format!("{path}:{selection}"))); + } + } + } + + pub fn open_permalink_to_line( + &mut self, + _: &OpenPermalinkToLine, + window: &mut Window, + cx: &mut Context, + ) { + let permalink_task = self.get_permalink_to_line(cx); + let workspace = self.workspace(); + + cx.spawn_in(window, async move |_, cx| match permalink_task.await { + Ok(permalink) => { + cx.update(|_, cx| { + cx.open_url(permalink.as_ref()); + }) + .ok(); + } + Err(err) => { + let message = format!("Failed to open permalink: {err}"); + + Err::<(), anyhow::Error>(err).log_err(); + + if let Some(workspace) = workspace { + workspace + .update(cx, |workspace, cx| { + struct OpenPermalinkToLine; + + workspace.show_toast( + Toast::new( + NotificationId::unique::(), + message, + ), + cx, + ) + }) + .ok(); + } + } + }) + .detach(); + } + + pub fn insert_uuid_v4( + &mut self, + _: &InsertUuidV4, + window: &mut Window, + cx: &mut Context, + ) { + self.insert_uuid(UuidVersion::V4, window, cx); + } + + pub fn insert_uuid_v7( + &mut self, + _: &InsertUuidV7, + window: &mut Window, + cx: &mut Context, + ) { + self.insert_uuid(UuidVersion::V7, window, cx); + } + + fn insert_uuid(&mut self, version: UuidVersion, window: &mut Window, cx: &mut Context) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + self.transact(window, cx, |this, window, cx| { + let edits = this + .selections + .all::(cx) + .into_iter() + .map(|selection| { + let uuid = match version { + UuidVersion::V4 => uuid::Uuid::new_v4(), + UuidVersion::V7 => uuid::Uuid::now_v7(), + }; + + (selection.range(), uuid.to_string()) + }); + this.edit(edits, cx); + this.refresh_inline_completion(true, false, window, cx); + }); + } + + pub fn open_selections_in_multibuffer( + &mut self, + _: &OpenSelectionsInMultibuffer, + window: &mut Window, + cx: &mut Context, + ) { + let multibuffer = self.buffer.read(cx); + + let Some(buffer) = multibuffer.as_singleton() else { + return; + }; + + let Some(workspace) = self.workspace() else { + return; + }; + + let locations = self + .selections + .disjoint_anchors() + .iter() + .map(|range| Location { + buffer: buffer.clone(), + range: range.start.text_anchor..range.end.text_anchor, + }) + .collect::>(); + + let title = multibuffer.title(cx).to_string(); + + cx.spawn_in(window, async move |_, cx| { + workspace.update_in(cx, |workspace, window, cx| { + Self::open_locations_in_multibuffer( + workspace, + locations, + format!("Selections for '{title}'"), + false, + MultibufferSelectionMode::All, + window, + cx, + ); + }) + }) + .detach(); + } + + /// Adds a row highlight for the given range. If a row has multiple highlights, the + /// last highlight added will be used. + /// + /// If the range ends at the beginning of a line, then that line will not be highlighted. + pub fn highlight_rows( + &mut self, + range: Range, + color: Hsla, + options: RowHighlightOptions, + cx: &mut Context, + ) { + let snapshot = self.buffer().read(cx).snapshot(cx); + let row_highlights = self.highlighted_rows.entry(TypeId::of::()).or_default(); + let ix = row_highlights.binary_search_by(|highlight| { + Ordering::Equal + .then_with(|| highlight.range.start.cmp(&range.start, &snapshot)) + .then_with(|| highlight.range.end.cmp(&range.end, &snapshot)) + }); + + if let Err(mut ix) = ix { + let index = post_inc(&mut self.highlight_order); + + // If this range intersects with the preceding highlight, then merge it with + // the preceding highlight. Otherwise insert a new highlight. + let mut merged = false; + if ix > 0 { + let prev_highlight = &mut row_highlights[ix - 1]; + if prev_highlight + .range + .end + .cmp(&range.start, &snapshot) + .is_ge() + { + ix -= 1; + if prev_highlight.range.end.cmp(&range.end, &snapshot).is_lt() { + prev_highlight.range.end = range.end; + } + merged = true; + prev_highlight.index = index; + prev_highlight.color = color; + prev_highlight.options = options; + } + } + + if !merged { + row_highlights.insert( + ix, + RowHighlight { + range: range.clone(), + index, + color, + options, + type_id: TypeId::of::(), + }, + ); + } + + // If any of the following highlights intersect with this one, merge them. + while let Some(next_highlight) = row_highlights.get(ix + 1) { + let highlight = &row_highlights[ix]; + if next_highlight + .range + .start + .cmp(&highlight.range.end, &snapshot) + .is_le() + { + if next_highlight + .range + .end + .cmp(&highlight.range.end, &snapshot) + .is_gt() + { + row_highlights[ix].range.end = next_highlight.range.end; + } + row_highlights.remove(ix + 1); + } else { + break; + } + } + } + } + + /// Remove any highlighted row ranges of the given type that intersect the + /// given ranges. + pub fn remove_highlighted_rows( + &mut self, + ranges_to_remove: Vec>, + cx: &mut Context, + ) { + let snapshot = self.buffer().read(cx).snapshot(cx); + let row_highlights = self.highlighted_rows.entry(TypeId::of::()).or_default(); + let mut ranges_to_remove = ranges_to_remove.iter().peekable(); + row_highlights.retain(|highlight| { + while let Some(range_to_remove) = ranges_to_remove.peek() { + match range_to_remove.end.cmp(&highlight.range.start, &snapshot) { + Ordering::Less | Ordering::Equal => { + ranges_to_remove.next(); + } + Ordering::Greater => { + match range_to_remove.start.cmp(&highlight.range.end, &snapshot) { + Ordering::Less | Ordering::Equal => { + return false; + } + Ordering::Greater => break, + } + } + } + } + + true + }) + } + + /// Clear all anchor ranges for a certain highlight context type, so no corresponding rows will be highlighted. + pub fn clear_row_highlights(&mut self) { + self.highlighted_rows.remove(&TypeId::of::()); + } + + /// For a highlight given context type, gets all anchor ranges that will be used for row highlighting. + pub fn highlighted_rows(&self) -> impl '_ + Iterator, Hsla)> { + self.highlighted_rows + .get(&TypeId::of::()) + .map_or(&[] as &[_], |vec| vec.as_slice()) + .iter() + .map(|highlight| (highlight.range.clone(), highlight.color)) + } + + /// Merges all anchor ranges for all context types ever set, picking the last highlight added in case of a row conflict. + /// Returns a map of display rows that are highlighted and their corresponding highlight color. + /// Allows to ignore certain kinds of highlights. + pub fn highlighted_display_rows( + &self, + window: &mut Window, + cx: &mut App, + ) -> BTreeMap { + let snapshot = self.snapshot(window, cx); + let mut used_highlight_orders = HashMap::default(); + self.highlighted_rows + .iter() + .flat_map(|(_, highlighted_rows)| highlighted_rows.iter()) + .fold( + BTreeMap::::new(), + |mut unique_rows, highlight| { + let start = highlight.range.start.to_display_point(&snapshot); + let end = highlight.range.end.to_display_point(&snapshot); + let start_row = start.row().0; + let end_row = if highlight.range.end.text_anchor != text::Anchor::MAX + && end.column() == 0 + { + end.row().0.saturating_sub(1) + } else { + end.row().0 + }; + for row in start_row..=end_row { + let used_index = + used_highlight_orders.entry(row).or_insert(highlight.index); + if highlight.index >= *used_index { + *used_index = highlight.index; + unique_rows.insert( + DisplayRow(row), + LineHighlight { + include_gutter: highlight.options.include_gutter, + border: None, + background: highlight.color.into(), + type_id: Some(highlight.type_id), + }, + ); + } + } + unique_rows + }, + ) + } + + pub fn highlighted_display_row_for_autoscroll( + &self, + snapshot: &DisplaySnapshot, + ) -> Option { + self.highlighted_rows + .values() + .flat_map(|highlighted_rows| highlighted_rows.iter()) + .filter_map(|highlight| { + if highlight.options.autoscroll { + Some(highlight.range.start.to_display_point(snapshot).row()) + } else { + None + } + }) + .min() + } + + pub fn set_search_within_ranges(&mut self, ranges: &[Range], cx: &mut Context) { + self.highlight_background::( + ranges, + |colors| colors.editor_document_highlight_read_background, + cx, + ) + } + + pub fn set_breadcrumb_header(&mut self, new_header: String) { + self.breadcrumb_header = Some(new_header); + } + + pub fn clear_search_within_ranges(&mut self, cx: &mut Context) { + self.clear_background_highlights::(cx); + } + + pub fn highlight_background( + &mut self, + ranges: &[Range], + color_fetcher: fn(&ThemeColors) -> Hsla, + cx: &mut Context, + ) { + self.background_highlights + .insert(TypeId::of::(), (color_fetcher, Arc::from(ranges))); + self.scrollbar_marker_state.dirty = true; + cx.notify(); + } + + pub fn clear_background_highlights( + &mut self, + cx: &mut Context, + ) -> Option { + let text_highlights = self.background_highlights.remove(&TypeId::of::())?; + if !text_highlights.1.is_empty() { + self.scrollbar_marker_state.dirty = true; + cx.notify(); + } + Some(text_highlights) + } + + pub fn highlight_gutter( + &mut self, + ranges: &[Range], + color_fetcher: fn(&App) -> Hsla, + cx: &mut Context, + ) { + self.gutter_highlights + .insert(TypeId::of::(), (color_fetcher, Arc::from(ranges))); + cx.notify(); + } + + pub fn clear_gutter_highlights( + &mut self, + cx: &mut Context, + ) -> Option { + cx.notify(); + self.gutter_highlights.remove(&TypeId::of::()) + } + + #[cfg(feature = "test-support")] + pub fn all_text_background_highlights( + &self, + window: &mut Window, + cx: &mut Context, + ) -> Vec<(Range, Hsla)> { + let snapshot = self.snapshot(window, cx); + let buffer = &snapshot.buffer_snapshot; + let start = buffer.anchor_before(0); + let end = buffer.anchor_after(buffer.len()); + let theme = cx.theme().colors(); + self.background_highlights_in_range(start..end, &snapshot, theme) + } + + #[cfg(feature = "test-support")] + pub fn search_background_highlights(&mut self, cx: &mut Context) -> Vec> { + let snapshot = self.buffer().read(cx).snapshot(cx); + + let highlights = self + .background_highlights + .get(&TypeId::of::()); + + if let Some((_color, ranges)) = highlights { + ranges + .iter() + .map(|range| range.start.to_point(&snapshot)..range.end.to_point(&snapshot)) + .collect_vec() + } else { + vec![] + } + } + + fn document_highlights_for_position<'a>( + &'a self, + position: Anchor, + buffer: &'a MultiBufferSnapshot, + ) -> impl 'a + Iterator> { + let read_highlights = self + .background_highlights + .get(&TypeId::of::()) + .map(|h| &h.1); + let write_highlights = self + .background_highlights + .get(&TypeId::of::()) + .map(|h| &h.1); + let left_position = position.bias_left(buffer); + let right_position = position.bias_right(buffer); + read_highlights + .into_iter() + .chain(write_highlights) + .flat_map(move |ranges| { + let start_ix = match ranges.binary_search_by(|probe| { + let cmp = probe.end.cmp(&left_position, buffer); + if cmp.is_ge() { + Ordering::Greater + } else { + Ordering::Less + } + }) { + Ok(i) | Err(i) => i, + }; + + ranges[start_ix..] + .iter() + .take_while(move |range| range.start.cmp(&right_position, buffer).is_le()) + }) + } + + pub fn has_background_highlights(&self) -> bool { + self.background_highlights + .get(&TypeId::of::()) + .map_or(false, |(_, highlights)| !highlights.is_empty()) + } + + pub fn background_highlights_in_range( + &self, + search_range: Range, + display_snapshot: &DisplaySnapshot, + theme: &ThemeColors, + ) -> Vec<(Range, 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 + .cmp(&search_range.start, &display_snapshot.buffer_snapshot); + if cmp.is_gt() { + Ordering::Greater + } else { + Ordering::Less + } + }) { + Ok(i) | Err(i) => i, + }; + for range in &ranges[start_ix..] { + if range + .start + .cmp(&search_range.end, &display_snapshot.buffer_snapshot) + .is_ge() + { + break; + } + + let start = range.start.to_display_point(display_snapshot); + let end = range.end.to_display_point(display_snapshot); + results.push((start..end, color)) + } + } + results + } + + pub fn background_highlight_row_ranges( + &self, + search_range: Range, + display_snapshot: &DisplaySnapshot, + count: usize, + ) -> Vec> { + let mut results = Vec::new(); + let Some((_, ranges)) = self.background_highlights.get(&TypeId::of::()) else { + return vec![]; + }; + + let start_ix = match ranges.binary_search_by(|probe| { + let cmp = probe + .end + .cmp(&search_range.start, &display_snapshot.buffer_snapshot); + if cmp.is_gt() { + Ordering::Greater + } else { + Ordering::Less + } + }) { + Ok(i) | Err(i) => i, + }; + let mut push_region = |start: Option, end: Option| { + if let (Some(start_display), Some(end_display)) = (start, end) { + results.push( + start_display.to_display_point(display_snapshot) + ..=end_display.to_display_point(display_snapshot), + ); + } + }; + let mut start_row: Option = None; + let mut end_row: Option = None; + if ranges.len() > count { + return Vec::new(); + } + for range in &ranges[start_ix..] { + if range + .start + .cmp(&search_range.end, &display_snapshot.buffer_snapshot) + .is_ge() + { + break; + } + let end = range.end.to_point(&display_snapshot.buffer_snapshot); + if let Some(current_row) = &end_row { + if end.row == current_row.row { + continue; + } + } + let start = range.start.to_point(&display_snapshot.buffer_snapshot); + if start_row.is_none() { + assert_eq!(end_row, None); + start_row = Some(start); + end_row = Some(end); + continue; + } + if let Some(current_end) = end_row.as_mut() { + if start.row > current_end.row + 1 { + push_region(start_row, end_row); + start_row = Some(start); + end_row = Some(end); + } else { + // Merge two hunks. + *current_end = end; + } + } else { + unreachable!(); + } + } + // We might still have a hunk that was not rendered (if there was a search hit on the last line) + push_region(start_row, end_row); + results + } + + pub fn gutter_highlights_in_range( + &self, + search_range: Range, + display_snapshot: &DisplaySnapshot, + cx: &App, + ) -> Vec<(Range, Hsla)> { + let mut results = Vec::new(); + for (color_fetcher, ranges) in self.gutter_highlights.values() { + let color = color_fetcher(cx); + let start_ix = match ranges.binary_search_by(|probe| { + let cmp = probe + .end + .cmp(&search_range.start, &display_snapshot.buffer_snapshot); + if cmp.is_gt() { + Ordering::Greater + } else { + Ordering::Less + } + }) { + Ok(i) | Err(i) => i, + }; + for range in &ranges[start_ix..] { + if range + .start + .cmp(&search_range.end, &display_snapshot.buffer_snapshot) + .is_ge() + { + break; + } + + let start = range.start.to_display_point(display_snapshot); + let end = range.end.to_display_point(display_snapshot); + results.push((start..end, color)) + } + } + results + } + + /// Get the text ranges corresponding to the redaction query + pub fn redacted_ranges( + &self, + search_range: Range, + display_snapshot: &DisplaySnapshot, + cx: &App, + ) -> Vec> { + display_snapshot + .buffer_snapshot + .redacted_ranges(search_range, |file| { + if let Some(file) = file { + file.is_private() + && EditorSettings::get( + Some(SettingsLocation { + worktree_id: file.worktree_id(cx), + path: file.path().as_ref(), + }), + cx, + ) + .redact_private_values + } else { + false + } + }) + .map(|range| { + range.start.to_display_point(display_snapshot) + ..range.end.to_display_point(display_snapshot) + }) + .collect() + } + + pub fn highlight_text( + &mut self, + ranges: Vec>, + style: HighlightStyle, + cx: &mut Context, + ) { + self.display_map.update(cx, |map, _| { + map.highlight_text(TypeId::of::(), ranges, style) + }); + cx.notify(); + } + + pub(crate) fn highlight_inlays( + &mut self, + highlights: Vec, + style: HighlightStyle, + cx: &mut Context, + ) { + self.display_map.update(cx, |map, _| { + map.highlight_inlays(TypeId::of::(), highlights, style) + }); + cx.notify(); + } + + pub fn text_highlights<'a, T: 'static>( + &'a self, + cx: &'a App, + ) -> Option<(HighlightStyle, &'a [Range])> { + self.display_map.read(cx).text_highlights(TypeId::of::()) + } + + pub fn clear_highlights(&mut self, cx: &mut Context) { + let cleared = self + .display_map + .update(cx, |map, _| map.clear_highlights(TypeId::of::())); + if cleared { + cx.notify(); + } + } + + pub fn show_local_cursors(&self, window: &mut Window, cx: &mut App) -> bool { + (self.read_only(cx) || self.blink_manager.read(cx).visible()) + && self.focus_handle.is_focused(window) + } + + pub fn set_show_cursor_when_unfocused(&mut self, is_enabled: bool, cx: &mut Context) { + self.show_cursor_when_unfocused = is_enabled; + cx.notify(); + } + + fn on_buffer_changed(&mut self, _: Entity, cx: &mut Context) { + cx.notify(); + } + + fn on_debug_session_event( + &mut self, + _session: Entity, + event: &SessionEvent, + cx: &mut Context, + ) { + match event { + SessionEvent::InvalidateInlineValue => { + self.refresh_inline_values(cx); + } + _ => {} + } + } + + fn refresh_inline_values(&mut self, cx: &mut Context) { + let Some(project) = self.project.clone() else { + return; + }; + let Some(buffer) = self.buffer.read(cx).as_singleton() else { + return; + }; + if !self.inline_value_cache.enabled { + let inlays = std::mem::take(&mut self.inline_value_cache.inlays); + self.splice_inlays(&inlays, Vec::new(), cx); + return; + } + + let current_execution_position = self + .highlighted_rows + .get(&TypeId::of::()) + .and_then(|lines| lines.last().map(|line| line.range.start)); + + self.inline_value_cache.refresh_task = cx.spawn(async move |editor, cx| { + let snapshot = editor + .update(cx, |editor, cx| editor.buffer().read(cx).snapshot(cx)) + .ok()?; + + let inline_values = editor + .update(cx, |_, cx| { + let Some(current_execution_position) = current_execution_position else { + return Some(Task::ready(Ok(Vec::new()))); + }; + + // todo(debugger) when introducing multi buffer inline values check execution position's buffer id to make sure the text + // anchor is in the same buffer + let range = + buffer.read(cx).anchor_before(0)..current_execution_position.text_anchor; + project.inline_values(buffer, range, cx) + }) + .ok() + .flatten()? + .await + .context("refreshing debugger inlays") + .log_err()?; + + let (excerpt_id, buffer_id) = snapshot + .excerpts() + .next() + .map(|excerpt| (excerpt.0, excerpt.1.remote_id()))?; + editor + .update(cx, |editor, cx| { + let new_inlays = inline_values + .into_iter() + .map(|debugger_value| { + Inlay::debugger_hint( + post_inc(&mut editor.next_inlay_id), + Anchor::in_buffer(excerpt_id, buffer_id, debugger_value.position), + debugger_value.text(), + ) + }) + .collect::>(); + let mut inlay_ids = new_inlays.iter().map(|inlay| inlay.id).collect(); + std::mem::swap(&mut editor.inline_value_cache.inlays, &mut inlay_ids); + + editor.splice_inlays(&inlay_ids, new_inlays, cx); + }) + .ok()?; + Some(()) + }); + } + + fn on_buffer_event( + &mut self, + multibuffer: &Entity, + event: &multi_buffer::Event, + window: &mut Window, + cx: &mut Context, + ) { + match event { + multi_buffer::Event::Edited { + singleton_buffer_edited, + edited_buffer: buffer_edited, + } => { + self.scrollbar_marker_state.dirty = true; + self.active_indent_guides_state.dirty = true; + self.refresh_active_diagnostics(cx); + self.refresh_code_actions(window, cx); + self.refresh_selected_text_highlights(true, window, cx); + refresh_matching_bracket_highlights(self, window, cx); + if self.has_active_inline_completion() { + self.update_visible_inline_completion(window, cx); + } + if let Some(buffer) = buffer_edited { + let buffer_id = buffer.read(cx).remote_id(); + if !self.registered_buffers.contains_key(&buffer_id) { + if let Some(project) = self.project.as_ref() { + project.update(cx, |project, cx| { + self.registered_buffers.insert( + buffer_id, + project.register_buffer_with_language_servers(&buffer, cx), + ); + }) + } + } + } + cx.emit(EditorEvent::BufferEdited); + cx.emit(SearchEvent::MatchesInvalidated); + if *singleton_buffer_edited { + if let Some(project) = &self.project { + #[allow(clippy::mutable_key_type)] + let languages_affected = multibuffer.update(cx, |multibuffer, cx| { + multibuffer + .all_buffers() + .into_iter() + .filter_map(|buffer| { + buffer.update(cx, |buffer, cx| { + let language = buffer.language()?; + let should_discard = project.update(cx, |project, cx| { + project.is_local() + && !project.has_language_servers_for(buffer, cx) + }); + should_discard.not().then_some(language.clone()) + }) + }) + .collect::>() + }); + if !languages_affected.is_empty() { + self.refresh_inlay_hints( + InlayHintRefreshReason::BufferEdited(languages_affected), + cx, + ); + } + } + } + + let Some(project) = &self.project else { return }; + let (telemetry, is_via_ssh) = { + let project = project.read(cx); + let telemetry = project.client().telemetry().clone(); + let is_via_ssh = project.is_via_ssh(); + (telemetry, is_via_ssh) + }; + refresh_linked_ranges(self, window, cx); + telemetry.log_edit_event("editor", is_via_ssh); + } + multi_buffer::Event::ExcerptsAdded { + buffer, + predecessor, + excerpts, + } => { + self.tasks_update_task = Some(self.refresh_runnables(window, cx)); + let buffer_id = buffer.read(cx).remote_id(); + if self.buffer.read(cx).diff_for(buffer_id).is_none() { + if let Some(project) = &self.project { + get_uncommitted_diff_for_buffer( + project, + [buffer.clone()], + self.buffer.clone(), + cx, + ) + .detach(); + } + } + cx.emit(EditorEvent::ExcerptsAdded { + buffer: buffer.clone(), + predecessor: *predecessor, + excerpts: excerpts.clone(), + }); + self.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx); + } + multi_buffer::Event::ExcerptsRemoved { + ids, + removed_buffer_ids, + } => { + self.refresh_inlay_hints(InlayHintRefreshReason::ExcerptsRemoved(ids.clone()), cx); + let buffer = self.buffer.read(cx); + self.registered_buffers + .retain(|buffer_id, _| buffer.buffer(*buffer_id).is_some()); + jsx_tag_auto_close::refresh_enabled_in_any_buffer(self, multibuffer, cx); + cx.emit(EditorEvent::ExcerptsRemoved { + ids: ids.clone(), + removed_buffer_ids: removed_buffer_ids.clone(), + }) + } + multi_buffer::Event::ExcerptsEdited { + excerpt_ids, + buffer_ids, + } => { + self.display_map.update(cx, |map, cx| { + map.unfold_buffers(buffer_ids.iter().copied(), cx) + }); + cx.emit(EditorEvent::ExcerptsEdited { + ids: excerpt_ids.clone(), + }) + } + multi_buffer::Event::ExcerptsExpanded { ids } => { + self.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx); + cx.emit(EditorEvent::ExcerptsExpanded { ids: ids.clone() }) + } + multi_buffer::Event::Reparsed(buffer_id) => { + self.tasks_update_task = Some(self.refresh_runnables(window, cx)); + jsx_tag_auto_close::refresh_enabled_in_any_buffer(self, multibuffer, cx); + + cx.emit(EditorEvent::Reparsed(*buffer_id)); + } + multi_buffer::Event::DiffHunksToggled => { + self.tasks_update_task = Some(self.refresh_runnables(window, cx)); + } + multi_buffer::Event::LanguageChanged(buffer_id) => { + linked_editing_ranges::refresh_linked_ranges(self, window, cx); + jsx_tag_auto_close::refresh_enabled_in_any_buffer(self, multibuffer, cx); + cx.emit(EditorEvent::Reparsed(*buffer_id)); + cx.notify(); + } + multi_buffer::Event::DirtyChanged => cx.emit(EditorEvent::DirtyChanged), + multi_buffer::Event::Saved => cx.emit(EditorEvent::Saved), + multi_buffer::Event::FileHandleChanged + | multi_buffer::Event::Reloaded + | multi_buffer::Event::BufferDiffChanged => cx.emit(EditorEvent::TitleChanged), + multi_buffer::Event::Closed => cx.emit(EditorEvent::Closed), + multi_buffer::Event::DiagnosticsUpdated => { + self.refresh_active_diagnostics(cx); + self.refresh_inline_diagnostics(true, window, cx); + self.scrollbar_marker_state.dirty = true; + cx.notify(); + } + _ => {} + }; + } + + fn on_display_map_changed( + &mut self, + _: Entity, + _: &mut Window, + cx: &mut Context, + ) { + cx.notify(); + } + + fn settings_changed(&mut self, window: &mut Window, cx: &mut Context) { + self.tasks_update_task = Some(self.refresh_runnables(window, cx)); + self.update_edit_prediction_settings(cx); + self.refresh_inline_completion(true, false, window, cx); + self.refresh_inlay_hints( + InlayHintRefreshReason::SettingsChange(inlay_hint_settings( + self.selections.newest_anchor().head(), + &self.buffer.read(cx).snapshot(cx), + cx, + )), + cx, + ); + + let old_cursor_shape = self.cursor_shape; + + { + let editor_settings = EditorSettings::get_global(cx); + self.scroll_manager.vertical_scroll_margin = editor_settings.vertical_scroll_margin; + self.show_breadcrumbs = editor_settings.toolbar.breadcrumbs; + self.cursor_shape = editor_settings.cursor_shape.unwrap_or_default(); + self.hide_mouse_mode = editor_settings.hide_mouse.unwrap_or_default(); + } + + if old_cursor_shape != self.cursor_shape { + cx.emit(EditorEvent::CursorShapeChanged); + } + + let project_settings = ProjectSettings::get_global(cx); + self.serialize_dirty_buffers = project_settings.session.restore_unsaved_buffers; + + if self.mode.is_full() { + let show_inline_diagnostics = project_settings.diagnostics.inline.enabled; + let inline_blame_enabled = project_settings.git.inline_blame_enabled(); + if self.show_inline_diagnostics != show_inline_diagnostics { + self.show_inline_diagnostics = show_inline_diagnostics; + self.refresh_inline_diagnostics(false, window, cx); + } + + if self.git_blame_inline_enabled != inline_blame_enabled { + self.toggle_git_blame_inline_internal(false, window, cx); + } + } + + cx.notify(); + } + + pub fn set_searchable(&mut self, searchable: bool) { + self.searchable = searchable; + } + + pub fn searchable(&self) -> bool { + self.searchable + } + + fn open_proposed_changes_editor( + &mut self, + _: &OpenProposedChangesEditor, + window: &mut Window, + cx: &mut Context, + ) { + let Some(workspace) = self.workspace() else { + cx.propagate(); + return; + }; + + let selections = self.selections.all::(cx); + let multi_buffer = self.buffer.read(cx); + let multi_buffer_snapshot = multi_buffer.snapshot(cx); + let mut new_selections_by_buffer = HashMap::default(); + for selection in selections { + for (buffer, range, _) in + multi_buffer_snapshot.range_to_buffer_ranges(selection.start..selection.end) + { + let mut range = range.to_point(buffer); + range.start.column = 0; + range.end.column = buffer.line_len(range.end.row); + new_selections_by_buffer + .entry(multi_buffer.buffer(buffer.remote_id()).unwrap()) + .or_insert(Vec::new()) + .push(range) + } + } + + let proposed_changes_buffers = new_selections_by_buffer + .into_iter() + .map(|(buffer, ranges)| ProposedChangeLocation { buffer, ranges }) + .collect::>(); + let proposed_changes_editor = cx.new(|cx| { + ProposedChangesEditor::new( + "Proposed changes", + proposed_changes_buffers, + self.project.clone(), + window, + cx, + ) + }); + + window.defer(cx, move |window, cx| { + workspace.update(cx, |workspace, cx| { + workspace.active_pane().update(cx, |pane, cx| { + pane.add_item( + Box::new(proposed_changes_editor), + true, + true, + None, + window, + cx, + ); + }); + }); + }); + } + + pub fn open_excerpts_in_split( + &mut self, + _: &OpenExcerptsSplit, + window: &mut Window, + cx: &mut Context, + ) { + self.open_excerpts_common(None, true, window, cx) + } + + pub fn open_excerpts(&mut self, _: &OpenExcerpts, window: &mut Window, cx: &mut Context) { + self.open_excerpts_common(None, false, window, cx) + } + + fn open_excerpts_common( + &mut self, + jump_data: Option, + split: bool, + window: &mut Window, + cx: &mut Context, + ) { + let Some(workspace) = self.workspace() else { + cx.propagate(); + return; + }; + + if self.buffer.read(cx).is_singleton() { + cx.propagate(); + return; + } + + let mut new_selections_by_buffer = HashMap::default(); + match &jump_data { + Some(JumpData::MultiBufferPoint { + excerpt_id, + position, + anchor, + line_offset_from_top, + }) => { + let multi_buffer_snapshot = self.buffer.read(cx).snapshot(cx); + if let Some(buffer) = multi_buffer_snapshot + .buffer_id_for_excerpt(*excerpt_id) + .and_then(|buffer_id| self.buffer.read(cx).buffer(buffer_id)) + { + let buffer_snapshot = buffer.read(cx).snapshot(); + let jump_to_point = if buffer_snapshot.can_resolve(anchor) { + language::ToPoint::to_point(anchor, &buffer_snapshot) + } else { + buffer_snapshot.clip_point(*position, Bias::Left) + }; + let jump_to_offset = buffer_snapshot.point_to_offset(jump_to_point); + new_selections_by_buffer.insert( + buffer, + ( + vec![jump_to_offset..jump_to_offset], + Some(*line_offset_from_top), + ), + ); + } + } + Some(JumpData::MultiBufferRow { + row, + line_offset_from_top, + }) => { + let point = MultiBufferPoint::new(row.0, 0); + if let Some((buffer, buffer_point, _)) = + self.buffer.read(cx).point_to_buffer_point(point, cx) + { + let buffer_offset = buffer.read(cx).point_to_offset(buffer_point); + new_selections_by_buffer + .entry(buffer) + .or_insert((Vec::new(), Some(*line_offset_from_top))) + .0 + .push(buffer_offset..buffer_offset) + } + } + None => { + let selections = self.selections.all::(cx); + let multi_buffer = self.buffer.read(cx); + for selection in selections { + for (snapshot, range, _, anchor) in multi_buffer + .snapshot(cx) + .range_to_buffer_ranges_with_deleted_hunks(selection.range()) + { + if let Some(anchor) = anchor { + // selection is in a deleted hunk + let Some(buffer_id) = anchor.buffer_id else { + continue; + }; + let Some(buffer_handle) = multi_buffer.buffer(buffer_id) else { + continue; + }; + let offset = text::ToOffset::to_offset( + &anchor.text_anchor, + &buffer_handle.read(cx).snapshot(), + ); + let range = offset..offset; + new_selections_by_buffer + .entry(buffer_handle) + .or_insert((Vec::new(), None)) + .0 + .push(range) + } else { + let Some(buffer_handle) = multi_buffer.buffer(snapshot.remote_id()) + else { + continue; + }; + new_selections_by_buffer + .entry(buffer_handle) + .or_insert((Vec::new(), None)) + .0 + .push(range) + } + } + } + } + } + + new_selections_by_buffer + .retain(|buffer, _| Self::can_open_excerpts_in_file(buffer.read(cx).file())); + + if new_selections_by_buffer.is_empty() { + return; + } + + // We defer the pane interaction because we ourselves are a workspace item + // and activating a new item causes the pane to call a method on us reentrantly, + // which panics if we're on the stack. + window.defer(cx, move |window, cx| { + workspace.update(cx, |workspace, cx| { + let pane = if split { + workspace.adjacent_pane(window, cx) + } else { + workspace.active_pane().clone() + }; + + for (buffer, (ranges, scroll_offset)) in new_selections_by_buffer { + let editor = buffer + .read(cx) + .file() + .is_none() + .then(|| { + // 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) = + pane.read(cx).items().enumerate().find_map(|(i, item)| { + let editor = item.downcast::()?; + let singleton_buffer = + editor.read(cx).buffer().read(cx).as_singleton()?; + if singleton_buffer == buffer { + Some((editor, i)) + } else { + None + } + })?; + pane.update(cx, |pane, cx| { + pane.activate_item(pane_item_index, true, true, window, cx) + }); + Some(editor) + }) + .flatten() + .unwrap_or_else(|| { + workspace.open_project_item::( + pane.clone(), + buffer, + true, + true, + window, + cx, + ) + }); + + editor.update(cx, |editor, cx| { + let autoscroll = match scroll_offset { + Some(scroll_offset) => Autoscroll::top_relative(scroll_offset as usize), + None => Autoscroll::newest(), + }; + let nav_history = editor.nav_history.take(); + editor.change_selections(Some(autoscroll), window, cx, |s| { + s.select_ranges(ranges); + }); + editor.nav_history = nav_history; + }); + } + }) + }); + } + + // For now, don't allow opening excerpts in buffers that aren't backed by + // regular project files. + fn can_open_excerpts_in_file(file: Option<&Arc>) -> bool { + file.map_or(true, |file| project::File::from_dyn(Some(file)).is_some()) + } + + fn marked_text_ranges(&self, cx: &App) -> Option>> { + let snapshot = self.buffer.read(cx).read(cx); + let (_, ranges) = self.text_highlights::(cx)?; + Some( + ranges + .iter() + .map(move |range| { + range.start.to_offset_utf16(&snapshot)..range.end.to_offset_utf16(&snapshot) + }) + .collect(), + ) + } + + fn selection_replacement_ranges( + &self, + range: Range, + cx: &mut App, + ) -> Vec> { + let selections = self.selections.all::(cx); + let newest_selection = selections + .iter() + .max_by_key(|selection| selection.id) + .unwrap(); + let start_delta = range.start.0 as isize - newest_selection.start.0 as isize; + let end_delta = range.end.0 as isize - newest_selection.end.0 as isize; + let snapshot = self.buffer.read(cx).read(cx); + selections + .into_iter() + .map(|mut selection| { + selection.start.0 = + (selection.start.0 as isize).saturating_add(start_delta) as usize; + selection.end.0 = (selection.end.0 as isize).saturating_add(end_delta) as usize; + snapshot.clip_offset_utf16(selection.start, Bias::Left) + ..snapshot.clip_offset_utf16(selection.end, Bias::Right) + }) + .collect() + } + + fn report_editor_event( + &self, + event_type: &'static str, + file_extension: Option, + cx: &App, + ) { + if cfg!(any(test, feature = "test-support")) { + return; + } + + let Some(project) = &self.project else { return }; + + // If None, we are in a file without an extension + let file = self + .buffer + .read(cx) + .as_singleton() + .and_then(|b| b.read(cx).file()); + let file_extension = file_extension.or(file + .as_ref() + .and_then(|file| Path::new(file.file_name(cx)).extension()) + .and_then(|e| e.to_str()) + .map(|a| a.to_string())); + + let vim_mode = vim_enabled(cx); + + let edit_predictions_provider = all_language_settings(file, cx).edit_predictions.provider; + let copilot_enabled = edit_predictions_provider + == language::language_settings::EditPredictionProvider::Copilot; + let copilot_enabled_for_language = self + .buffer + .read(cx) + .language_settings(cx) + .show_edit_predictions; + + let project = project.read(cx); + telemetry::event!( + event_type, + file_extension, + vim_mode, + copilot_enabled, + copilot_enabled_for_language, + edit_predictions_provider, + is_via_ssh = project.is_via_ssh(), + ); + } + + /// Copy the highlighted chunks to the clipboard as JSON. The format is an array of lines, + /// with each line being an array of {text, highlight} objects. + fn copy_highlight_json( + &mut self, + _: &CopyHighlightJson, + window: &mut Window, + cx: &mut Context, + ) { + #[derive(Serialize)] + struct Chunk<'a> { + text: String, + highlight: Option<&'a str>, + } + + let snapshot = self.buffer.read(cx).snapshot(cx); + let range = self + .selected_text_range(false, window, cx) + .and_then(|selection| { + if selection.range.is_empty() { + None + } else { + Some(selection.range) + } + }) + .unwrap_or_else(|| 0..snapshot.len()); + + let chunks = snapshot.chunks(range, true); + let mut lines = Vec::new(); + let mut line: VecDeque = VecDeque::new(); + + let Some(style) = self.style.as_ref() else { + return; + }; + + for chunk in chunks { + let highlight = chunk + .syntax_highlight_id + .and_then(|id| id.name(&style.syntax)); + let mut chunk_lines = chunk.text.split('\n').peekable(); + while let Some(text) = chunk_lines.next() { + let mut merged_with_last_token = false; + if let Some(last_token) = line.back_mut() { + if last_token.highlight == highlight { + last_token.text.push_str(text); + merged_with_last_token = true; + } + } + + if !merged_with_last_token { + line.push_back(Chunk { + text: text.into(), + highlight, + }); + } + + if chunk_lines.peek().is_some() { + if line.len() > 1 && line.front().unwrap().text.is_empty() { + line.pop_front(); + } + if line.len() > 1 && line.back().unwrap().text.is_empty() { + line.pop_back(); + } + + lines.push(mem::take(&mut line)); + } + } + } + + let Some(lines) = serde_json::to_string_pretty(&lines).log_err() else { + return; + }; + cx.write_to_clipboard(ClipboardItem::new_string(lines)); + } + + pub fn open_context_menu( + &mut self, + _: &OpenContextMenu, + window: &mut Window, + cx: &mut Context, + ) { + self.request_autoscroll(Autoscroll::newest(), cx); + let position = self.selections.newest_display(cx).start; + mouse_context_menu::deploy_context_menu(self, None, position, window, cx); + } + + pub fn inlay_hint_cache(&self) -> &InlayHintCache { + &self.inlay_hint_cache + } + + pub fn replay_insert_event( + &mut self, + text: &str, + relative_utf16_range: Option>, + window: &mut Window, + cx: &mut Context, + ) { + if !self.input_enabled { + cx.emit(EditorEvent::InputIgnored { text: text.into() }); + return; + } + if let Some(relative_utf16_range) = relative_utf16_range { + let selections = self.selections.all::(cx); + self.change_selections(None, window, cx, |s| { + let new_ranges = selections.into_iter().map(|range| { + let start = OffsetUtf16( + range + .head() + .0 + .saturating_add_signed(relative_utf16_range.start), + ); + let end = OffsetUtf16( + range + .head() + .0 + .saturating_add_signed(relative_utf16_range.end), + ); + start..end + }); + s.select_ranges(new_ranges); + }); + } + + self.handle_input(text, window, cx); + } + + pub fn supports_inlay_hints(&self, cx: &mut App) -> bool { + let Some(provider) = self.semantics_provider.as_ref() else { + return false; + }; + + let mut supports = false; + self.buffer().update(cx, |this, cx| { + this.for_each_buffer(|buffer| { + supports |= provider.supports_inlay_hints(buffer, cx); + }); + }); + + supports + } + + pub fn is_focused(&self, window: &Window) -> bool { + self.focus_handle.is_focused(window) + } + + fn handle_focus(&mut self, window: &mut Window, cx: &mut Context) { + cx.emit(EditorEvent::Focused); + + if let Some(descendant) = self + .last_focused_descendant + .take() + .and_then(|descendant| descendant.upgrade()) + { + window.focus(&descendant); + } else { + if let Some(blame) = self.blame.as_ref() { + blame.update(cx, GitBlame::focus) + } + + self.blink_manager.update(cx, |blink_manager, cx| { + // blink_manager.enable(cx); + }); + self.show_cursor_names(window, cx); + self.buffer.update(cx, |buffer, cx| { + buffer.finalize_last_transaction(cx); + if self.leader_peer_id.is_none() { + buffer.set_active_selections( + &self.selections.disjoint_anchors(), + self.selections.line_mode, + self.cursor_shape, + cx, + ); + } + }); + } + } + + fn handle_focus_in(&mut self, _: &mut Window, cx: &mut Context) { + cx.emit(EditorEvent::FocusedIn) + } + + fn handle_focus_out( + &mut self, + event: FocusOutEvent, + _window: &mut Window, + cx: &mut Context, + ) { + if event.blurred != self.focus_handle { + self.last_focused_descendant = Some(event.blurred); + } + self.refresh_inlay_hints(InlayHintRefreshReason::ModifiersChanged(false), cx); + } + + pub fn handle_blur(&mut self, window: &mut Window, cx: &mut Context) { + self.blink_manager.update(cx, BlinkManager::disable); + self.buffer + .update(cx, |buffer, cx| buffer.remove_active_selections(cx)); + + if let Some(blame) = self.blame.as_ref() { + blame.update(cx, GitBlame::blur) + } + if !self.hover_state.focused(window, cx) { + hide_hover(self, cx); + } + if !self + .context_menu + .borrow() + .as_ref() + .is_some_and(|context_menu| context_menu.focused(window, cx)) + { + self.hide_context_menu(window, cx); + } + self.discard_inline_completion(false, cx); + cx.emit(EditorEvent::Blurred); + cx.notify(); + } + + pub fn register_action( + &mut self, + listener: impl Fn(&A, &mut Window, &mut App) + 'static, + ) -> Subscription { + let id = self.next_editor_action_id.post_inc(); + let listener = Arc::new(listener); + self.editor_actions.borrow_mut().insert( + id, + Box::new(move |window, _| { + let listener = listener.clone(); + window.on_action(TypeId::of::(), move |action, phase, window, cx| { + let action = action.downcast_ref().unwrap(); + if phase == DispatchPhase::Bubble { + listener(action, window, cx) + } + }) + }), + ); + + let editor_actions = self.editor_actions.clone(); + Subscription::new(move || { + editor_actions.borrow_mut().remove(&id); + }) + } + + pub fn file_header_size(&self) -> u32 { + FILE_HEADER_HEIGHT + } + + pub fn restore( + &mut self, + revert_changes: HashMap, Rope)>>, + window: &mut Window, + cx: &mut Context, + ) { + let workspace = self.workspace(); + let project = self.project.as_ref(); + let save_tasks = self.buffer().update(cx, |multi_buffer, cx| { + let mut tasks = Vec::new(); + for (buffer_id, changes) in revert_changes { + if let Some(buffer) = multi_buffer.buffer(buffer_id) { + buffer.update(cx, |buffer, cx| { + buffer.edit( + changes + .into_iter() + .map(|(range, text)| (range, text.to_string())), + None, + cx, + ); + }); + + if let Some(project) = + project.filter(|_| multi_buffer.all_diff_hunks_expanded()) + { + project.update(cx, |project, cx| { + tasks.push((buffer.clone(), project.save_buffer(buffer, cx))); + }) + } + } + } + tasks + }); + cx.spawn_in(window, async move |_, cx| { + for (buffer, task) in save_tasks { + let result = task.await; + if result.is_err() { + let Some(path) = buffer + .read_with(cx, |buffer, cx| buffer.project_path(cx)) + .ok() + else { + continue; + }; + if let Some((workspace, path)) = workspace.as_ref().zip(path) { + let Some(task) = cx + .update_window_entity(&workspace, |workspace, window, cx| { + workspace + .open_path_preview(path, None, false, false, false, window, cx) + }) + .ok() + else { + continue; + }; + task.await.log_err(); + } + } + } + }) + .detach(); + self.change_selections(None, window, cx, |selections| selections.refresh()); + } + + pub fn to_pixel_point( + &self, + source: multi_buffer::Anchor, + editor_snapshot: &EditorSnapshot, + window: &mut Window, + ) -> Option> { + let source_point = source.to_display_point(editor_snapshot); + self.display_to_pixel_point(source_point, editor_snapshot, window) + } + + pub fn display_to_pixel_point( + &self, + source: DisplayPoint, + editor_snapshot: &EditorSnapshot, + window: &mut Window, + ) -> Option> { + let line_height = self.style()?.text.line_height_in_pixels(window.rem_size()); + let text_layout_details = self.text_layout_details(window); + let scroll_top = text_layout_details + .scroll_anchor + .scroll_position(editor_snapshot) + .y; + + if source.row().as_f32() < scroll_top.floor() { + return None; + } + let source_x = editor_snapshot.x_for_display_point(source, &text_layout_details); + let source_y = line_height * (source.row().as_f32() - scroll_top); + Some(gpui::Point::new(source_x, source_y)) + } + + pub fn has_visible_completions_menu(&self) -> bool { + !self.edit_prediction_preview_is_active() + && self.context_menu.borrow().as_ref().map_or(false, |menu| { + menu.visible() && matches!(menu, CodeContextMenu::Completions(_)) + }) + } + + pub fn register_addon(&mut self, instance: T) { + self.addons + .insert(std::any::TypeId::of::(), Box::new(instance)); + } + + pub fn unregister_addon(&mut self) { + self.addons.remove(&std::any::TypeId::of::()); + } + + pub fn addon(&self) -> Option<&T> { + let type_id = std::any::TypeId::of::(); + self.addons + .get(&type_id) + .and_then(|item| item.to_any().downcast_ref::()) + } + + pub fn addon_mut(&mut self) -> Option<&mut T> { + let type_id = std::any::TypeId::of::(); + self.addons + .get_mut(&type_id) + .and_then(|item| item.to_any_mut()?.downcast_mut::()) + } + + fn character_size(&self, window: &mut Window) -> gpui::Size { + let text_layout_details = self.text_layout_details(window); + let style = &text_layout_details.editor_style; + let font_id = window.text_system().resolve_font(&style.text.font()); + let font_size = style.text.font_size.to_pixels(window.rem_size()); + let line_height = style.text.line_height_in_pixels(window.rem_size()); + let em_width = window.text_system().em_width(font_id, font_size).unwrap(); + + gpui::Size::new(em_width, line_height) + } + + pub fn wait_for_diff_to_load(&self) -> Option>> { + self.load_diff_task.clone() + } + + fn read_metadata_from_db( + &mut self, + item_id: u64, + workspace_id: WorkspaceId, + window: &mut Window, + cx: &mut Context, + ) { + if self.is_singleton(cx) + && WorkspaceSettings::get(None, cx).restore_on_startup != RestoreOnStartupBehavior::None + { + let buffer_snapshot = OnceCell::new(); + + if let Some(folds) = DB.get_editor_folds(item_id, workspace_id).log_err() { + if !folds.is_empty() { + let snapshot = + buffer_snapshot.get_or_init(|| self.buffer.read(cx).snapshot(cx)); + self.fold_ranges( + folds + .into_iter() + .map(|(start, end)| { + snapshot.clip_offset(start, Bias::Left) + ..snapshot.clip_offset(end, Bias::Right) + }) + .collect(), + false, + window, + cx, + ); + } + } + + if let Some(selections) = DB.get_editor_selections(item_id, workspace_id).log_err() { + if !selections.is_empty() { + let snapshot = + buffer_snapshot.get_or_init(|| self.buffer.read(cx).snapshot(cx)); + self.change_selections(None, window, cx, |s| { + s.select_ranges(selections.into_iter().map(|(start, end)| { + snapshot.clip_offset(start, Bias::Left) + ..snapshot.clip_offset(end, Bias::Right) + })); + }); + } + }; + } + + self.read_scroll_position_from_db(item_id, workspace_id, window, cx); + } +} + +fn vim_enabled(cx: &App) -> bool { + cx.global::() + .raw_user_settings() + .get("vim_mode") + == Some(&serde_json::Value::Bool(true)) +} + +// Consider user intent and default settings +fn choose_completion_range( + completion: &Completion, + intent: CompletionIntent, + buffer: &Entity, + cx: &mut Context, +) -> Range { + fn should_replace( + completion: &Completion, + insert_range: &Range, + intent: CompletionIntent, + completion_mode_setting: LspInsertMode, + buffer: &Buffer, + ) -> bool { + // specific actions take precedence over settings + match intent { + CompletionIntent::CompleteWithInsert => return false, + CompletionIntent::CompleteWithReplace => return true, + CompletionIntent::Complete | CompletionIntent::Compose => {} + } + + match completion_mode_setting { + LspInsertMode::Insert => false, + LspInsertMode::Replace => true, + LspInsertMode::ReplaceSubsequence => { + let mut text_to_replace = buffer.chars_for_range( + buffer.anchor_before(completion.replace_range.start) + ..buffer.anchor_after(completion.replace_range.end), + ); + let mut completion_text = completion.new_text.chars(); + + // is `text_to_replace` a subsequence of `completion_text` + text_to_replace + .all(|needle_ch| completion_text.any(|haystack_ch| haystack_ch == needle_ch)) + } + LspInsertMode::ReplaceSuffix => { + let range_after_cursor = insert_range.end..completion.replace_range.end; + + let text_after_cursor = buffer + .text_for_range( + buffer.anchor_before(range_after_cursor.start) + ..buffer.anchor_after(range_after_cursor.end), + ) + .collect::(); + completion.new_text.ends_with(&text_after_cursor) + } + } + } + + let buffer = buffer.read(cx); + + if let CompletionSource::Lsp { + insert_range: Some(insert_range), + .. + } = &completion.source + { + let completion_mode_setting = + language_settings(buffer.language().map(|l| l.name()), buffer.file(), cx) + .completions + .lsp_insert_mode; + + if !should_replace( + completion, + &insert_range, + intent, + completion_mode_setting, + buffer, + ) { + return insert_range.to_offset(buffer); + } + } + + completion.replace_range.to_offset(buffer) +} + +fn insert_extra_newline_brackets( + buffer: &MultiBufferSnapshot, + range: Range, + language: &language::LanguageScope, +) -> bool { + let leading_whitespace_len = buffer + .reversed_chars_at(range.start) + .take_while(|c| c.is_whitespace() && *c != '\n') + .map(|c| c.len_utf8()) + .sum::(); + let trailing_whitespace_len = buffer + .chars_at(range.end) + .take_while(|c| c.is_whitespace() && *c != '\n') + .map(|c| c.len_utf8()) + .sum::(); + let range = range.start - leading_whitespace_len..range.end + trailing_whitespace_len; + + language.brackets().any(|(pair, enabled)| { + let pair_start = pair.start.trim_end(); + let pair_end = pair.end.trim_start(); + + enabled + && pair.newline + && buffer.contains_str_at(range.end, pair_end) + && buffer.contains_str_at(range.start.saturating_sub(pair_start.len()), pair_start) + }) +} + +fn insert_extra_newline_tree_sitter(buffer: &MultiBufferSnapshot, range: Range) -> bool { + let (buffer, range) = match buffer.range_to_buffer_ranges(range).as_slice() { + [(buffer, range, _)] => (*buffer, range.clone()), + _ => return false, + }; + let pair = { + let mut result: Option = None; + + for pair in buffer + .all_bracket_ranges(range.clone()) + .filter(move |pair| { + pair.open_range.start <= range.start && pair.close_range.end >= range.end + }) + { + let len = pair.close_range.end - pair.open_range.start; + + if let Some(existing) = &result { + let existing_len = existing.close_range.end - existing.open_range.start; + if len > existing_len { + continue; + } + } + + result = Some(pair); + } + + result + }; + let Some(pair) = pair else { + return false; + }; + pair.newline_only + && buffer + .chars_for_range(pair.open_range.end..range.start) + .chain(buffer.chars_for_range(range.end..pair.close_range.start)) + .all(|c| c.is_whitespace() && c != '\n') +} + +fn get_uncommitted_diff_for_buffer( + project: &Entity, + buffers: impl IntoIterator>, + buffer: Entity, + cx: &mut App, +) -> Task<()> { + let mut tasks = Vec::new(); + project.update(cx, |project, cx| { + for buffer in buffers { + if project::File::from_dyn(buffer.read(cx).file()).is_some() { + tasks.push(project.open_uncommitted_diff(buffer.clone(), cx)) + } + } + }); + cx.spawn(async move |cx| { + let diffs = future::join_all(tasks).await; + buffer + .update(cx, |buffer, cx| { + for diff in diffs.into_iter().flatten() { + buffer.add_diff(diff, cx); + } + }) + .ok(); + }) +} + +fn char_len_with_expanded_tabs(offset: usize, text: &str, tab_size: NonZeroU32) -> usize { + let tab_size = tab_size.get() as usize; + let mut width = offset; + + for ch in text.chars() { + width += if ch == '\t' { + tab_size - (width % tab_size) + } else { + 1 + }; + } + + width - offset +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_string_size_with_expanded_tabs() { + let nz = |val| NonZeroU32::new(val).unwrap(); + assert_eq!(char_len_with_expanded_tabs(0, "", nz(4)), 0); + assert_eq!(char_len_with_expanded_tabs(0, "hello", nz(4)), 5); + assert_eq!(char_len_with_expanded_tabs(0, "\thello", nz(4)), 9); + assert_eq!(char_len_with_expanded_tabs(0, "abc\tab", nz(4)), 6); + assert_eq!(char_len_with_expanded_tabs(0, "hello\t", nz(4)), 8); + assert_eq!(char_len_with_expanded_tabs(0, "\t\t", nz(8)), 16); + assert_eq!(char_len_with_expanded_tabs(0, "x\t", nz(8)), 8); + assert_eq!(char_len_with_expanded_tabs(7, "x\t", nz(8)), 9); + } +} + +/// Tokenizes a string into runs of text that should stick together, or that is whitespace. +struct WordBreakingTokenizer<'a> { + input: &'a str, +} + +impl<'a> WordBreakingTokenizer<'a> { + fn new(input: &'a str) -> Self { + Self { input } + } +} + +fn is_char_ideographic(ch: char) -> bool { + use unicode_script::Script::*; + use unicode_script::UnicodeScript; + matches!(ch.script(), Han | Tangut | Yi) +} + +fn is_grapheme_ideographic(text: &str) -> bool { + text.chars().any(is_char_ideographic) +} + +fn is_grapheme_whitespace(text: &str) -> bool { + text.chars().any(|x| x.is_whitespace()) +} + +fn should_stay_with_preceding_ideograph(text: &str) -> bool { + text.chars().next().map_or(false, |ch| { + matches!(ch, '。' | '、' | ',' | '?' | '!' | ':' | ';' | '…') + }) +} + +#[derive(PartialEq, Eq, Debug, Clone, Copy)] +enum WordBreakToken<'a> { + Word { token: &'a str, grapheme_len: usize }, + InlineWhitespace { token: &'a str, grapheme_len: usize }, + Newline, +} + +impl<'a> Iterator for WordBreakingTokenizer<'a> { + /// Yields a span, the count of graphemes in the token, and whether it was + /// whitespace. Note that it also breaks at word boundaries. + type Item = WordBreakToken<'a>; + + fn next(&mut self) -> Option { + use unicode_segmentation::UnicodeSegmentation; + if self.input.is_empty() { + return None; + } + + let mut iter = self.input.graphemes(true).peekable(); + let mut offset = 0; + let mut grapheme_len = 0; + if let Some(first_grapheme) = iter.next() { + let is_newline = first_grapheme == "\n"; + let is_whitespace = is_grapheme_whitespace(first_grapheme); + offset += first_grapheme.len(); + grapheme_len += 1; + if is_grapheme_ideographic(first_grapheme) && !is_whitespace { + if let Some(grapheme) = iter.peek().copied() { + if should_stay_with_preceding_ideograph(grapheme) { + offset += grapheme.len(); + grapheme_len += 1; + } + } + } else { + let mut words = self.input[offset..].split_word_bound_indices().peekable(); + let mut next_word_bound = words.peek().copied(); + if next_word_bound.map_or(false, |(i, _)| i == 0) { + next_word_bound = words.next(); + } + while let Some(grapheme) = iter.peek().copied() { + if next_word_bound.map_or(false, |(i, _)| i == offset) { + break; + }; + if is_grapheme_whitespace(grapheme) != is_whitespace + || (grapheme == "\n") != is_newline + { + break; + }; + offset += grapheme.len(); + grapheme_len += 1; + iter.next(); + } + } + let token = &self.input[..offset]; + self.input = &self.input[offset..]; + if token == "\n" { + Some(WordBreakToken::Newline) + } else if is_whitespace { + Some(WordBreakToken::InlineWhitespace { + token, + grapheme_len, + }) + } else { + Some(WordBreakToken::Word { + token, + grapheme_len, + }) + } + } else { + None + } + } +} + +#[test] +fn test_word_breaking_tokenizer() { + let tests: &[(&str, &[WordBreakToken<'static>])] = &[ + ("", &[]), + (" ", &[whitespace(" ", 2)]), + ("Ʒ", &[word("Ʒ", 1)]), + ("Ǽ", &[word("Ǽ", 1)]), + ("⋑", &[word("⋑", 1)]), + ("⋑⋑", &[word("⋑⋑", 2)]), + ( + "原理,进而", + &[word("原", 1), word("理,", 2), word("进", 1), word("而", 1)], + ), + ( + "hello world", + &[word("hello", 5), whitespace(" ", 1), word("world", 5)], + ), + ( + "hello, world", + &[word("hello,", 6), whitespace(" ", 1), word("world", 5)], + ), + ( + " hello world", + &[ + whitespace(" ", 2), + word("hello", 5), + whitespace(" ", 1), + word("world", 5), + ], + ), + ( + "这是什么 \n 钢笔", + &[ + word("这", 1), + word("是", 1), + word("什", 1), + word("么", 1), + whitespace(" ", 1), + newline(), + whitespace(" ", 1), + word("钢", 1), + word("笔", 1), + ], + ), + (" mutton", &[whitespace(" ", 1), word("mutton", 6)]), + ]; + + fn word(token: &'static str, grapheme_len: usize) -> WordBreakToken<'static> { + WordBreakToken::Word { + token, + grapheme_len, + } + } + + fn whitespace(token: &'static str, grapheme_len: usize) -> WordBreakToken<'static> { + WordBreakToken::InlineWhitespace { + token, + grapheme_len, + } + } + + fn newline() -> WordBreakToken<'static> { + WordBreakToken::Newline + } + + for (input, result) in tests { + assert_eq!( + WordBreakingTokenizer::new(input) + .collect::>() + .as_slice(), + *result, + ); + } +} + +fn wrap_with_prefix( + line_prefix: String, + unwrapped_text: String, + wrap_column: usize, + tab_size: NonZeroU32, + preserve_existing_whitespace: bool, +) -> String { + let line_prefix_len = char_len_with_expanded_tabs(0, &line_prefix, tab_size); + let mut wrapped_text = String::new(); + let mut current_line = line_prefix.clone(); + + let tokenizer = WordBreakingTokenizer::new(&unwrapped_text); + let mut current_line_len = line_prefix_len; + let mut in_whitespace = false; + for token in tokenizer { + let have_preceding_whitespace = in_whitespace; + match token { + WordBreakToken::Word { + token, + grapheme_len, + } => { + in_whitespace = false; + if current_line_len + grapheme_len > wrap_column + && current_line_len != line_prefix_len + { + wrapped_text.push_str(current_line.trim_end()); + wrapped_text.push('\n'); + current_line.truncate(line_prefix.len()); + current_line_len = line_prefix_len; + } + current_line.push_str(token); + current_line_len += grapheme_len; + } + WordBreakToken::InlineWhitespace { + mut token, + mut grapheme_len, + } => { + in_whitespace = true; + if have_preceding_whitespace && !preserve_existing_whitespace { + continue; + } + if !preserve_existing_whitespace { + token = " "; + grapheme_len = 1; + } + if current_line_len + grapheme_len > wrap_column { + wrapped_text.push_str(current_line.trim_end()); + wrapped_text.push('\n'); + current_line.truncate(line_prefix.len()); + current_line_len = line_prefix_len; + } else if current_line_len != line_prefix_len || preserve_existing_whitespace { + current_line.push_str(token); + current_line_len += grapheme_len; + } + } + WordBreakToken::Newline => { + in_whitespace = true; + if preserve_existing_whitespace { + wrapped_text.push_str(current_line.trim_end()); + wrapped_text.push('\n'); + current_line.truncate(line_prefix.len()); + current_line_len = line_prefix_len; + } else if have_preceding_whitespace { + continue; + } else if current_line_len + 1 > wrap_column && current_line_len != line_prefix_len + { + wrapped_text.push_str(current_line.trim_end()); + wrapped_text.push('\n'); + current_line.truncate(line_prefix.len()); + current_line_len = line_prefix_len; + } else if current_line_len != line_prefix_len { + current_line.push(' '); + current_line_len += 1; + } + } + } + } + + if !current_line.is_empty() { + wrapped_text.push_str(¤t_line); + } + wrapped_text +} + +#[test] +fn test_wrap_with_prefix() { + assert_eq!( + wrap_with_prefix( + "# ".to_string(), + "abcdefg".to_string(), + 4, + NonZeroU32::new(4).unwrap(), + false, + ), + "# abcdefg" + ); + assert_eq!( + wrap_with_prefix( + "".to_string(), + "\thello world".to_string(), + 8, + NonZeroU32::new(4).unwrap(), + false, + ), + "hello\nworld" + ); + assert_eq!( + wrap_with_prefix( + "// ".to_string(), + "xx \nyy zz aa bb cc".to_string(), + 12, + NonZeroU32::new(4).unwrap(), + false, + ), + "// xx yy zz\n// aa bb cc" + ); + assert_eq!( + wrap_with_prefix( + String::new(), + "这是什么 \n 钢笔".to_string(), + 3, + NonZeroU32::new(4).unwrap(), + false, + ), + "这是什\n么 钢\n笔" + ); +} + +pub trait CollaborationHub { + fn collaborators<'a>(&self, cx: &'a App) -> &'a HashMap; + fn user_participant_indices<'a>(&self, cx: &'a App) -> &'a HashMap; + fn user_names(&self, cx: &App) -> HashMap; +} + +impl CollaborationHub for Entity { + fn collaborators<'a>(&self, cx: &'a App) -> &'a HashMap { + self.read(cx).collaborators() + } + + fn user_participant_indices<'a>(&self, cx: &'a App) -> &'a HashMap { + self.read(cx).user_store().read(cx).participant_indices() + } + + fn user_names(&self, cx: &App) -> HashMap { + let this = self.read(cx); + let user_ids = this.collaborators().values().map(|c| c.user_id); + this.user_store().read_with(cx, |user_store, cx| { + user_store.participant_names(user_ids, cx) + }) + } +} + +pub trait SemanticsProvider { + fn hover( + &self, + buffer: &Entity, + position: text::Anchor, + cx: &mut App, + ) -> Option>>; + + fn inline_values( + &self, + buffer_handle: Entity, + range: Range, + cx: &mut App, + ) -> Option>>>; + + fn inlay_hints( + &self, + buffer_handle: Entity, + range: Range, + cx: &mut App, + ) -> Option>>>; + + fn resolve_inlay_hint( + &self, + hint: InlayHint, + buffer_handle: Entity, + server_id: LanguageServerId, + cx: &mut App, + ) -> Option>>; + + fn supports_inlay_hints(&self, buffer: &Entity, cx: &mut App) -> bool; + + fn document_highlights( + &self, + buffer: &Entity, + position: text::Anchor, + cx: &mut App, + ) -> Option>>>; + + fn definitions( + &self, + buffer: &Entity, + position: text::Anchor, + kind: GotoDefinitionKind, + cx: &mut App, + ) -> Option>>>; + + fn range_for_rename( + &self, + buffer: &Entity, + position: text::Anchor, + cx: &mut App, + ) -> Option>>>>; + + fn perform_rename( + &self, + buffer: &Entity, + position: text::Anchor, + new_name: String, + cx: &mut App, + ) -> Option>>; +} + +pub trait CompletionProvider { + fn completions( + &self, + excerpt_id: ExcerptId, + buffer: &Entity, + buffer_position: text::Anchor, + trigger: CompletionContext, + window: &mut Window, + cx: &mut Context, + ) -> Task>>>; + + fn resolve_completions( + &self, + buffer: Entity, + completion_indices: Vec, + completions: Rc>>, + cx: &mut Context, + ) -> Task>; + + fn apply_additional_edits_for_completion( + &self, + _buffer: Entity, + _completions: Rc>>, + _completion_index: usize, + _push_to_history: bool, + _cx: &mut Context, + ) -> Task>> { + Task::ready(Ok(None)) + } + + fn is_completion_trigger( + &self, + buffer: &Entity, + position: language::Anchor, + text: &str, + trigger_in_words: bool, + cx: &mut Context, + ) -> bool; + + fn sort_completions(&self) -> bool { + true + } + + fn filter_completions(&self) -> bool { + true + } +} + +pub trait CodeActionProvider { + fn id(&self) -> Arc; + + fn code_actions( + &self, + buffer: &Entity, + range: Range, + window: &mut Window, + cx: &mut App, + ) -> Task>>; + + fn apply_code_action( + &self, + buffer_handle: Entity, + action: CodeAction, + excerpt_id: ExcerptId, + push_to_history: bool, + window: &mut Window, + cx: &mut App, + ) -> Task>; +} + +impl CodeActionProvider for Entity { + fn id(&self) -> Arc { + "project".into() + } + + fn code_actions( + &self, + buffer: &Entity, + range: Range, + _window: &mut Window, + cx: &mut App, + ) -> Task>> { + self.update(cx, |project, cx| { + let code_lens = project.code_lens(buffer, range.clone(), cx); + let code_actions = project.code_actions(buffer, range, None, cx); + cx.background_spawn(async move { + let (code_lens, code_actions) = join(code_lens, code_actions).await; + Ok(code_lens + .context("code lens fetch")? + .into_iter() + .chain(code_actions.context("code action fetch")?) + .collect()) + }) + }) + } + + fn apply_code_action( + &self, + buffer_handle: Entity, + action: CodeAction, + _excerpt_id: ExcerptId, + push_to_history: bool, + _window: &mut Window, + cx: &mut App, + ) -> Task> { + self.update(cx, |project, cx| { + project.apply_code_action(buffer_handle, action, push_to_history, cx) + }) + } +} + +fn snippet_completions( + project: &Project, + buffer: &Entity, + buffer_position: text::Anchor, + cx: &mut App, +) -> Task>> { + let languages = buffer.read(cx).languages_at(buffer_position); + let snippet_store = project.snippets().read(cx); + + let scopes: Vec<_> = languages + .iter() + .filter_map(|language| { + let language_name = language.lsp_id(); + let snippets = snippet_store.snippets_for(Some(language_name), cx); + + if snippets.is_empty() { + None + } else { + Some((language.default_scope(), snippets)) + } + }) + .collect(); + + if scopes.is_empty() { + return Task::ready(Ok(vec![])); + } + + let snapshot = buffer.read(cx).text_snapshot(); + let chars: String = snapshot + .reversed_chars_for_range(text::Anchor::MIN..buffer_position) + .collect(); + let executor = cx.background_executor().clone(); + + cx.background_spawn(async move { + let mut all_results: Vec = Vec::new(); + for (scope, snippets) in scopes.into_iter() { + let classifier = CharClassifier::new(Some(scope)).for_completion(true); + let mut last_word = chars + .chars() + .take_while(|c| classifier.is_word(*c)) + .collect::(); + last_word = last_word.chars().rev().collect(); + + if last_word.is_empty() { + return Ok(vec![]); + } + + let as_offset = text::ToOffset::to_offset(&buffer_position, &snapshot); + let to_lsp = |point: &text::Anchor| { + let end = text::ToPointUtf16::to_point_utf16(point, &snapshot); + point_to_lsp(end) + }; + let lsp_end = to_lsp(&buffer_position); + + let candidates = snippets + .iter() + .enumerate() + .flat_map(|(ix, snippet)| { + snippet + .prefix + .iter() + .map(move |prefix| StringMatchCandidate::new(ix, &prefix)) + }) + .collect::>(); + + let mut matches = fuzzy::match_strings( + &candidates, + &last_word, + last_word.chars().any(|c| c.is_uppercase()), + 100, + &Default::default(), + executor.clone(), + ) + .await; + + // Remove all candidates where the query's start does not match the start of any word in the candidate + if let Some(query_start) = last_word.chars().next() { + matches.retain(|string_match| { + split_words(&string_match.string).any(|word| { + // Check that the first codepoint of the word as lowercase matches the first + // codepoint of the query as lowercase + word.chars() + .flat_map(|codepoint| codepoint.to_lowercase()) + .zip(query_start.to_lowercase()) + .all(|(word_cp, query_cp)| word_cp == query_cp) + }) + }); + } + + let matched_strings = matches + .into_iter() + .map(|m| m.string) + .collect::>(); + + let mut result: Vec = snippets + .iter() + .filter_map(|snippet| { + let matching_prefix = snippet + .prefix + .iter() + .find(|prefix| matched_strings.contains(*prefix))?; + let start = as_offset - last_word.len(); + let start = snapshot.anchor_before(start); + let range = start..buffer_position; + let lsp_start = to_lsp(&start); + let lsp_range = lsp::Range { + start: lsp_start, + end: lsp_end, + }; + Some(Completion { + replace_range: range, + new_text: snippet.body.clone(), + source: CompletionSource::Lsp { + insert_range: None, + server_id: LanguageServerId(usize::MAX), + resolved: true, + lsp_completion: Box::new(lsp::CompletionItem { + label: snippet.prefix.first().unwrap().clone(), + kind: Some(CompletionItemKind::SNIPPET), + label_details: snippet.description.as_ref().map(|description| { + lsp::CompletionItemLabelDetails { + detail: Some(description.clone()), + description: None, + } + }), + insert_text_format: Some(InsertTextFormat::SNIPPET), + text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace( + lsp::InsertReplaceEdit { + new_text: snippet.body.clone(), + insert: lsp_range, + replace: lsp_range, + }, + )), + filter_text: Some(snippet.body.clone()), + sort_text: Some(char::MAX.to_string()), + ..lsp::CompletionItem::default() + }), + lsp_defaults: None, + }, + label: CodeLabel { + text: matching_prefix.clone(), + runs: Vec::new(), + filter_range: 0..matching_prefix.len(), + }, + icon_path: None, + documentation: snippet.description.clone().map(|description| { + CompletionDocumentation::SingleLine(description.into()) + }), + insert_text_mode: None, + confirm: None, + }) + }) + .collect(); + + all_results.append(&mut result); + } + + Ok(all_results) + }) +} + +impl CompletionProvider for Entity { + fn completions( + &self, + _excerpt_id: ExcerptId, + buffer: &Entity, + buffer_position: text::Anchor, + options: CompletionContext, + _window: &mut Window, + cx: &mut Context, + ) -> Task>>> { + self.update(cx, |project, cx| { + let snippets = snippet_completions(project, buffer, buffer_position, cx); + let project_completions = project.completions(buffer, buffer_position, options, cx); + cx.background_spawn(async move { + let snippets_completions = snippets.await?; + match project_completions.await? { + Some(mut completions) => { + completions.extend(snippets_completions); + Ok(Some(completions)) + } + None => { + if snippets_completions.is_empty() { + Ok(None) + } else { + Ok(Some(snippets_completions)) + } + } + } + }) + }) + } + + fn resolve_completions( + &self, + buffer: Entity, + completion_indices: Vec, + completions: Rc>>, + cx: &mut Context, + ) -> Task> { + self.update(cx, |project, cx| { + project.lsp_store().update(cx, |lsp_store, cx| { + lsp_store.resolve_completions(buffer, completion_indices, completions, cx) + }) + }) + } + + fn apply_additional_edits_for_completion( + &self, + buffer: Entity, + completions: Rc>>, + completion_index: usize, + push_to_history: bool, + cx: &mut Context, + ) -> Task>> { + self.update(cx, |project, cx| { + project.lsp_store().update(cx, |lsp_store, cx| { + lsp_store.apply_additional_edits_for_completion( + buffer, + completions, + completion_index, + push_to_history, + cx, + ) + }) + }) + } + + fn is_completion_trigger( + &self, + buffer: &Entity, + position: language::Anchor, + text: &str, + trigger_in_words: bool, + cx: &mut Context, + ) -> bool { + let mut chars = text.chars(); + let char = if let Some(char) = chars.next() { + char + } else { + return false; + }; + if chars.next().is_some() { + return false; + } + + let buffer = buffer.read(cx); + let snapshot = buffer.snapshot(); + if !snapshot.settings_at(position, cx).show_completions_on_input { + return false; + } + let classifier = snapshot.char_classifier_at(position).for_completion(true); + if trigger_in_words && classifier.is_word(char) { + return true; + } + + buffer.completion_triggers().contains(text) + } +} + +impl SemanticsProvider for Entity { + fn hover( + &self, + buffer: &Entity, + position: text::Anchor, + cx: &mut App, + ) -> Option>> { + Some(self.update(cx, |project, cx| project.hover(buffer, position, cx))) + } + + fn document_highlights( + &self, + buffer: &Entity, + position: text::Anchor, + cx: &mut App, + ) -> Option>>> { + Some(self.update(cx, |project, cx| { + project.document_highlights(buffer, position, cx) + })) + } + + fn definitions( + &self, + buffer: &Entity, + position: text::Anchor, + kind: GotoDefinitionKind, + cx: &mut App, + ) -> Option>>> { + Some(self.update(cx, |project, cx| match kind { + GotoDefinitionKind::Symbol => project.definition(&buffer, position, cx), + GotoDefinitionKind::Declaration => project.declaration(&buffer, position, cx), + GotoDefinitionKind::Type => project.type_definition(&buffer, position, cx), + GotoDefinitionKind::Implementation => project.implementation(&buffer, position, cx), + })) + } + + fn supports_inlay_hints(&self, buffer: &Entity, cx: &mut App) -> bool { + // TODO: make this work for remote projects + self.update(cx, |project, cx| { + if project + .active_debug_session(cx) + .is_some_and(|(session, _)| session.read(cx).any_stopped_thread()) + { + return true; + } + + buffer.update(cx, |buffer, cx| { + project.any_language_server_supports_inlay_hints(buffer, cx) + }) + }) + } + + fn inline_values( + &self, + buffer_handle: Entity, + range: Range, + cx: &mut App, + ) -> Option>>> { + self.update(cx, |project, cx| { + let (session, active_stack_frame) = project.active_debug_session(cx)?; + + Some(project.inline_values(session, active_stack_frame, buffer_handle, range, cx)) + }) + } + + fn inlay_hints( + &self, + buffer_handle: Entity, + range: Range, + cx: &mut App, + ) -> Option>>> { + Some(self.update(cx, |project, cx| { + project.inlay_hints(buffer_handle, range, cx) + })) + } + + fn resolve_inlay_hint( + &self, + hint: InlayHint, + buffer_handle: Entity, + server_id: LanguageServerId, + cx: &mut App, + ) -> Option>> { + Some(self.update(cx, |project, cx| { + project.resolve_inlay_hint(hint, buffer_handle, server_id, cx) + })) + } + + fn range_for_rename( + &self, + buffer: &Entity, + position: text::Anchor, + cx: &mut App, + ) -> Option>>>> { + Some(self.update(cx, |project, cx| { + let buffer = buffer.clone(); + let task = project.prepare_rename(buffer.clone(), position, cx); + cx.spawn(async move |_, cx| { + Ok(match task.await? { + PrepareRenameResponse::Success(range) => Some(range), + PrepareRenameResponse::InvalidPosition => None, + PrepareRenameResponse::OnlyUnpreparedRenameSupported => { + // Fallback on using TreeSitter info to determine identifier range + buffer.update(cx, |buffer, _| { + let snapshot = buffer.snapshot(); + let (range, kind) = snapshot.surrounding_word(position); + if kind != Some(CharKind::Word) { + return None; + } + Some( + snapshot.anchor_before(range.start) + ..snapshot.anchor_after(range.end), + ) + })? + } + }) + }) + })) + } + + fn perform_rename( + &self, + buffer: &Entity, + position: text::Anchor, + new_name: String, + cx: &mut App, + ) -> Option>> { + Some(self.update(cx, |project, cx| { + project.perform_rename(buffer.clone(), position, new_name, cx) + })) + } +} + +fn inlay_hint_settings( + location: Anchor, + snapshot: &MultiBufferSnapshot, + cx: &mut Context, +) -> InlayHintSettings { + let file = snapshot.file_at(location); + let language = snapshot.language_at(location).map(|l| l.name()); + language_settings(language, file, cx).inlay_hints +} + +fn consume_contiguous_rows( + contiguous_row_selections: &mut Vec>, + selection: &Selection, + display_map: &DisplaySnapshot, + selections: &mut Peekable>>, +) -> (MultiBufferRow, MultiBufferRow) { + contiguous_row_selections.push(selection.clone()); + let start_row = MultiBufferRow(selection.start.row); + let mut end_row = ending_row(selection, display_map); + + while let Some(next_selection) = selections.peek() { + if next_selection.start.row <= end_row.0 { + end_row = ending_row(next_selection, display_map); + contiguous_row_selections.push(selections.next().unwrap().clone()); + } else { + break; + } + } + (start_row, end_row) +} + +fn ending_row(next_selection: &Selection, display_map: &DisplaySnapshot) -> MultiBufferRow { + if next_selection.end.column > 0 || next_selection.is_empty() { + MultiBufferRow(display_map.next_line_boundary(next_selection.end).0.row + 1) + } else { + MultiBufferRow(next_selection.end.row) + } +} + +impl EditorSnapshot { + pub fn remote_selections_in_range<'a>( + &'a self, + range: &'a Range, + collaboration_hub: &dyn CollaborationHub, + cx: &'a App, + ) -> impl 'a + Iterator { + let participant_names = collaboration_hub.user_names(cx); + let participant_indices = collaboration_hub.user_participant_indices(cx); + let collaborators_by_peer_id = collaboration_hub.collaborators(cx); + let collaborators_by_replica_id = collaborators_by_peer_id + .iter() + .map(|(_, collaborator)| (collaborator.replica_id, collaborator)) + .collect::>(); + self.buffer_snapshot + .selections_in_range(range, false) + .filter_map(move |(replica_id, line_mode, cursor_shape, selection)| { + let collaborator = collaborators_by_replica_id.get(&replica_id)?; + let participant_index = participant_indices.get(&collaborator.user_id).copied(); + let user_name = participant_names.get(&collaborator.user_id).cloned(); + Some(RemoteSelection { + replica_id, + selection, + cursor_shape, + line_mode, + participant_index, + peer_id: collaborator.peer_id, + user_name, + }) + }) + } + + pub fn hunks_for_ranges( + &self, + ranges: impl IntoIterator>, + ) -> Vec { + let mut hunks = Vec::new(); + let mut processed_buffer_rows: HashMap>> = + HashMap::default(); + for query_range in ranges { + let query_rows = + MultiBufferRow(query_range.start.row)..MultiBufferRow(query_range.end.row + 1); + for hunk in self.buffer_snapshot.diff_hunks_in_range( + Point::new(query_rows.start.0, 0)..Point::new(query_rows.end.0, 0), + ) { + // Include deleted hunks that are adjacent to the query range, because + // otherwise they would be missed. + let mut intersects_range = hunk.row_range.overlaps(&query_rows); + if hunk.status().is_deleted() { + intersects_range |= hunk.row_range.start == query_rows.end; + intersects_range |= hunk.row_range.end == query_rows.start; + } + if intersects_range { + if !processed_buffer_rows + .entry(hunk.buffer_id) + .or_default() + .insert(hunk.buffer_range.start..hunk.buffer_range.end) + { + continue; + } + hunks.push(hunk); + } + } + } + + hunks + } + + fn display_diff_hunks_for_rows<'a>( + &'a self, + display_rows: Range, + folded_buffers: &'a HashSet, + ) -> impl 'a + Iterator { + let buffer_start = DisplayPoint::new(display_rows.start, 0).to_point(self); + let buffer_end = DisplayPoint::new(display_rows.end, 0).to_point(self); + + self.buffer_snapshot + .diff_hunks_in_range(buffer_start..buffer_end) + .filter_map(|hunk| { + if folded_buffers.contains(&hunk.buffer_id) { + return None; + } + + let hunk_start_point = Point::new(hunk.row_range.start.0, 0); + let hunk_end_point = Point::new(hunk.row_range.end.0, 0); + + let hunk_display_start = self.point_to_display_point(hunk_start_point, Bias::Left); + let hunk_display_end = self.point_to_display_point(hunk_end_point, Bias::Right); + + let display_hunk = if hunk_display_start.column() != 0 { + DisplayDiffHunk::Folded { + display_row: hunk_display_start.row(), + } + } else { + let mut end_row = hunk_display_end.row(); + if hunk_display_end.column() > 0 { + end_row.0 += 1; + } + let is_created_file = hunk.is_created_file(); + DisplayDiffHunk::Unfolded { + status: hunk.status(), + diff_base_byte_range: hunk.diff_base_byte_range, + display_row_range: hunk_display_start.row()..end_row, + multi_buffer_range: Anchor::range_in_buffer( + hunk.excerpt_id, + hunk.buffer_id, + hunk.buffer_range, + ), + is_created_file, + } + }; + + Some(display_hunk) + }) + } + + pub fn language_at(&self, position: T) -> Option<&Arc> { + self.display_snapshot.buffer_snapshot.language_at(position) + } + + pub fn is_focused(&self) -> bool { + self.is_focused + } + + pub fn placeholder_text(&self) -> Option<&Arc> { + self.placeholder_text.as_ref() + } + + pub fn scroll_position(&self) -> gpui::Point { + self.scroll_anchor.scroll_position(&self.display_snapshot) + } + + fn gutter_dimensions( + &self, + font_id: FontId, + font_size: Pixels, + max_line_number_width: Pixels, + cx: &App, + ) -> Option { + if !self.show_gutter { + return None; + } + + let descent = cx.text_system().descent(font_id, font_size); + let em_width = cx.text_system().em_width(font_id, font_size).log_err()?; + let em_advance = cx.text_system().em_advance(font_id, font_size).log_err()?; + + let show_git_gutter = self.show_git_diff_gutter.unwrap_or_else(|| { + matches!( + ProjectSettings::get_global(cx).git.git_gutter, + Some(GitGutterSetting::TrackedFiles) + ) + }); + let gutter_settings = EditorSettings::get_global(cx).gutter; + let show_line_numbers = self + .show_line_numbers + .unwrap_or(gutter_settings.line_numbers); + let line_gutter_width = if show_line_numbers { + // Avoid flicker-like gutter resizes when the line number gains another digit and only resize the gutter on files with N*10^5 lines. + let min_width_for_number_on_gutter = em_advance * MIN_LINE_NUMBER_DIGITS as f32; + max_line_number_width.max(min_width_for_number_on_gutter) + } else { + 0.0.into() + }; + + let show_code_actions = self + .show_code_actions + .unwrap_or(gutter_settings.code_actions); + + let show_runnables = self.show_runnables.unwrap_or(gutter_settings.runnables); + let show_breakpoints = self.show_breakpoints.unwrap_or(gutter_settings.breakpoints); + + let git_blame_entries_width = + self.git_blame_gutter_max_author_length + .map(|max_author_length| { + let renderer = cx.global::().0.clone(); + const MAX_RELATIVE_TIMESTAMP: &str = "60 minutes ago"; + + /// The number of characters to dedicate to gaps and margins. + const SPACING_WIDTH: usize = 4; + + let max_char_count = max_author_length.min(renderer.max_author_length()) + + ::git::SHORT_SHA_LENGTH + + MAX_RELATIVE_TIMESTAMP.len() + + SPACING_WIDTH; + + em_advance * max_char_count + }); + + let is_singleton = self.buffer_snapshot.is_singleton(); + + let mut left_padding = git_blame_entries_width.unwrap_or(Pixels::ZERO); + left_padding += if !is_singleton { + em_width * 4.0 + } else if show_code_actions || show_runnables || show_breakpoints { + em_width * 3.0 + } else if show_git_gutter && show_line_numbers { + em_width * 2.0 + } else if show_git_gutter || show_line_numbers { + em_width + } else { + px(0.) + }; + + let shows_folds = is_singleton && gutter_settings.folds; + + let right_padding = if shows_folds && show_line_numbers { + em_width * 4.0 + } else if shows_folds || (!is_singleton && show_line_numbers) { + em_width * 3.0 + } else if show_line_numbers { + em_width + } else { + px(0.) + }; + + Some(GutterDimensions { + left_padding, + right_padding, + width: line_gutter_width + left_padding + right_padding, + margin: -descent, + git_blame_entries_width, + }) + } + + pub fn render_crease_toggle( + &self, + buffer_row: MultiBufferRow, + row_contains_cursor: bool, + editor: Entity, + window: &mut Window, + cx: &mut App, + ) -> Option { + let folded = self.is_line_folded(buffer_row); + let mut is_foldable = false; + + if let Some(crease) = self + .crease_snapshot + .query_row(buffer_row, &self.buffer_snapshot) + { + is_foldable = true; + match crease { + Crease::Inline { render_toggle, .. } | Crease::Block { render_toggle, .. } => { + if let Some(render_toggle) = render_toggle { + let toggle_callback = + Arc::new(move |folded, window: &mut Window, cx: &mut App| { + if folded { + editor.update(cx, |editor, cx| { + editor.fold_at(buffer_row, window, cx) + }); + } else { + editor.update(cx, |editor, cx| { + editor.unfold_at(buffer_row, window, cx) + }); + } + }); + return Some((render_toggle)( + buffer_row, + folded, + toggle_callback, + window, + cx, + )); + } + } + } + } + + is_foldable |= self.starts_indent(buffer_row); + + if folded || (is_foldable && (row_contains_cursor || self.gutter_hovered)) { + Some( + Disclosure::new(("gutter_crease", buffer_row.0), !folded) + .toggle_state(folded) + .on_click(window.listener_for(&editor, move |this, _e, window, cx| { + if folded { + this.unfold_at(buffer_row, window, cx); + } else { + this.fold_at(buffer_row, window, cx); + } + })) + .into_any_element(), + ) + } else { + None + } + } + + pub fn render_crease_trailer( + &self, + buffer_row: MultiBufferRow, + window: &mut Window, + cx: &mut App, + ) -> Option { + let folded = self.is_line_folded(buffer_row); + if let Crease::Inline { render_trailer, .. } = self + .crease_snapshot + .query_row(buffer_row, &self.buffer_snapshot)? + { + let render_trailer = render_trailer.as_ref()?; + Some(render_trailer(buffer_row, folded, window, cx)) + } else { + None + } + } +} + +impl Deref for EditorSnapshot { + type Target = DisplaySnapshot; + + fn deref(&self) -> &Self::Target { + &self.display_snapshot + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum EditorEvent { + InputIgnored { + text: Arc, + }, + InputHandled { + utf16_range_to_replace: Option>, + text: Arc, + }, + ExcerptsAdded { + buffer: Entity, + predecessor: ExcerptId, + excerpts: Vec<(ExcerptId, ExcerptRange)>, + }, + ExcerptsRemoved { + ids: Vec, + removed_buffer_ids: Vec, + }, + BufferFoldToggled { + ids: Vec, + folded: bool, + }, + ExcerptsEdited { + ids: Vec, + }, + ExcerptsExpanded { + ids: Vec, + }, + BufferEdited, + Edited { + transaction_id: clock::Lamport, + }, + Reparsed(BufferId), + Focused, + FocusedIn, + Blurred, + DirtyChanged, + Saved, + TitleChanged, + DiffBaseChanged, + SelectionsChanged { + local: bool, + }, + ScrollPositionChanged { + local: bool, + autoscroll: bool, + }, + Closed, + TransactionUndone { + transaction_id: clock::Lamport, + }, + TransactionBegun { + transaction_id: clock::Lamport, + }, + Reloaded, + CursorShapeChanged, + PushedToNavHistory { + anchor: Anchor, + is_deactivate: bool, + }, +} + +impl EventEmitter for Editor {} + +impl Focusable for Editor { + fn focus_handle(&self, _cx: &App) -> FocusHandle { + self.focus_handle.clone() + } +} + +impl Render for Editor { + fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { + let settings = ThemeSettings::get_global(cx); + + let mut text_style = match self.mode { + EditorMode::SingleLine { .. } | EditorMode::AutoHeight { .. } => TextStyle { + color: cx.theme().colors().editor_foreground, + font_family: settings.ui_font.family.clone(), + font_features: settings.ui_font.features.clone(), + font_fallbacks: settings.ui_font.fallbacks.clone(), + font_size: rems(0.875).into(), + font_weight: settings.ui_font.weight, + line_height: relative(settings.buffer_line_height.value()), + ..Default::default() + }, + EditorMode::Full { .. } => TextStyle { + color: cx.theme().colors().editor_foreground, + font_family: settings.buffer_font.family.clone(), + font_features: settings.buffer_font.features.clone(), + font_fallbacks: settings.buffer_font.fallbacks.clone(), + font_size: settings.buffer_font_size(cx).into(), + font_weight: settings.buffer_font.weight, + line_height: relative(settings.buffer_line_height.value()), + ..Default::default() + }, + }; + if let Some(text_style_refinement) = &self.text_style_refinement { + text_style.refine(text_style_refinement) + } + + let background = match self.mode { + EditorMode::SingleLine { .. } => cx.theme().system().transparent, + EditorMode::AutoHeight { max_lines: _ } => cx.theme().system().transparent, + EditorMode::Full { .. } => cx.theme().colors().editor_background, + }; + + EditorElement::new( + &cx.entity(), + EditorStyle { + background, + local_player: cx.theme().players().local(), + text: text_style, + scrollbar_width: EditorElement::SCROLLBAR_WIDTH, + syntax: cx.theme().syntax().clone(), + status: cx.theme().status().clone(), + inlay_hints_style: make_inlay_hints_style(cx), + inline_completion_styles: make_suggestion_styles(cx), + unnecessary_code_fade: ThemeSettings::get_global(cx).unnecessary_code_fade, + }, + ) + } +} + +impl EntityInputHandler for Editor { + fn text_for_range( + &mut self, + range_utf16: Range, + adjusted_range: &mut Option>, + _: &mut Window, + cx: &mut Context, + ) -> Option { + let snapshot = self.buffer.read(cx).read(cx); + let start = snapshot.clip_offset_utf16(OffsetUtf16(range_utf16.start), Bias::Left); + let end = snapshot.clip_offset_utf16(OffsetUtf16(range_utf16.end), Bias::Right); + if (start.0..end.0) != range_utf16 { + adjusted_range.replace(start.0..end.0); + } + Some(snapshot.text_for_range(start..end).collect()) + } + + fn selected_text_range( + &mut self, + ignore_disabled_input: bool, + _: &mut Window, + cx: &mut Context, + ) -> Option { + // Prevent the IME menu from appearing when holding down an alphabetic key + // while input is disabled. + if !ignore_disabled_input && !self.input_enabled { + return None; + } + + let selection = self.selections.newest::(cx); + let range = selection.range(); + + Some(UTF16Selection { + range: range.start.0..range.end.0, + reversed: selection.reversed, + }) + } + + fn marked_text_range(&self, _: &mut Window, cx: &mut Context) -> Option> { + let snapshot = self.buffer.read(cx).read(cx); + let range = self.text_highlights::(cx)?.1.first()?; + Some(range.start.to_offset_utf16(&snapshot).0..range.end.to_offset_utf16(&snapshot).0) + } + + fn unmark_text(&mut self, _: &mut Window, cx: &mut Context) { + self.clear_highlights::(cx); + self.ime_transaction.take(); + } + + fn replace_text_in_range( + &mut self, + range_utf16: Option>, + text: &str, + window: &mut Window, + cx: &mut Context, + ) { + if !self.input_enabled { + cx.emit(EditorEvent::InputIgnored { text: text.into() }); + return; + } + + self.transact(window, cx, |this, window, cx| { + let new_selected_ranges = if let Some(range_utf16) = range_utf16 { + let range_utf16 = OffsetUtf16(range_utf16.start)..OffsetUtf16(range_utf16.end); + Some(this.selection_replacement_ranges(range_utf16, cx)) + } else { + this.marked_text_ranges(cx) + }; + + let range_to_replace = new_selected_ranges.as_ref().and_then(|ranges_to_replace| { + let newest_selection_id = this.selections.newest_anchor().id; + this.selections + .all::(cx) + .iter() + .zip(ranges_to_replace.iter()) + .find_map(|(selection, range)| { + if selection.id == newest_selection_id { + Some( + (range.start.0 as isize - selection.head().0 as isize) + ..(range.end.0 as isize - selection.head().0 as isize), + ) + } else { + None + } + }) + }); + + cx.emit(EditorEvent::InputHandled { + utf16_range_to_replace: range_to_replace, + text: text.into(), + }); + + if let Some(new_selected_ranges) = new_selected_ranges { + this.change_selections(None, window, cx, |selections| { + selections.select_ranges(new_selected_ranges) + }); + this.backspace(&Default::default(), window, cx); + } + + this.handle_input(text, window, cx); + }); + + if let Some(transaction) = self.ime_transaction { + self.buffer.update(cx, |buffer, cx| { + buffer.group_until_transaction(transaction, cx); + }); + } + + self.unmark_text(window, cx); + } + + fn replace_and_mark_text_in_range( + &mut self, + range_utf16: Option>, + text: &str, + new_selected_range_utf16: Option>, + window: &mut Window, + cx: &mut Context, + ) { + if !self.input_enabled { + return; + } + + let transaction = self.transact(window, cx, |this, window, cx| { + let ranges_to_replace = if let Some(mut marked_ranges) = this.marked_text_ranges(cx) { + let snapshot = this.buffer.read(cx).read(cx); + if let Some(relative_range_utf16) = range_utf16.as_ref() { + for marked_range in &mut marked_ranges { + marked_range.end.0 = marked_range.start.0 + relative_range_utf16.end; + marked_range.start.0 += relative_range_utf16.start; + marked_range.start = + snapshot.clip_offset_utf16(marked_range.start, Bias::Left); + marked_range.end = + snapshot.clip_offset_utf16(marked_range.end, Bias::Right); + } + } + Some(marked_ranges) + } else if let Some(range_utf16) = range_utf16 { + let range_utf16 = OffsetUtf16(range_utf16.start)..OffsetUtf16(range_utf16.end); + Some(this.selection_replacement_ranges(range_utf16, cx)) + } else { + None + }; + + let range_to_replace = ranges_to_replace.as_ref().and_then(|ranges_to_replace| { + let newest_selection_id = this.selections.newest_anchor().id; + this.selections + .all::(cx) + .iter() + .zip(ranges_to_replace.iter()) + .find_map(|(selection, range)| { + if selection.id == newest_selection_id { + Some( + (range.start.0 as isize - selection.head().0 as isize) + ..(range.end.0 as isize - selection.head().0 as isize), + ) + } else { + None + } + }) + }); + + cx.emit(EditorEvent::InputHandled { + utf16_range_to_replace: range_to_replace, + text: text.into(), + }); + + if let Some(ranges) = ranges_to_replace { + this.change_selections(None, window, cx, |s| s.select_ranges(ranges)); + } + + let marked_ranges = { + let snapshot = this.buffer.read(cx).read(cx); + this.selections + .disjoint_anchors() + .iter() + .map(|selection| { + selection.start.bias_left(&snapshot)..selection.end.bias_right(&snapshot) + }) + .collect::>() + }; + + if text.is_empty() { + this.unmark_text(window, cx); + } else { + this.highlight_text::( + marked_ranges.clone(), + HighlightStyle { + underline: Some(UnderlineStyle { + thickness: px(1.), + color: None, + wavy: false, + }), + ..Default::default() + }, + cx, + ); + } + + // Disable auto-closing when composing text (i.e. typing a `"` on a Brazilian keyboard) + let use_autoclose = this.use_autoclose; + let use_auto_surround = this.use_auto_surround; + this.set_use_autoclose(false); + this.set_use_auto_surround(false); + this.handle_input(text, window, cx); + this.set_use_autoclose(use_autoclose); + this.set_use_auto_surround(use_auto_surround); + + if let Some(new_selected_range) = new_selected_range_utf16 { + let snapshot = this.buffer.read(cx).read(cx); + let new_selected_ranges = marked_ranges + .into_iter() + .map(|marked_range| { + let insertion_start = marked_range.start.to_offset_utf16(&snapshot).0; + let new_start = OffsetUtf16(new_selected_range.start + insertion_start); + let new_end = OffsetUtf16(new_selected_range.end + insertion_start); + snapshot.clip_offset_utf16(new_start, Bias::Left) + ..snapshot.clip_offset_utf16(new_end, Bias::Right) + }) + .collect::>(); + + drop(snapshot); + this.change_selections(None, window, cx, |selections| { + selections.select_ranges(new_selected_ranges) + }); + } + }); + + self.ime_transaction = self.ime_transaction.or(transaction); + if let Some(transaction) = self.ime_transaction { + self.buffer.update(cx, |buffer, cx| { + buffer.group_until_transaction(transaction, cx); + }); + } + + if self.text_highlights::(cx).is_none() { + self.ime_transaction.take(); + } + } + + fn bounds_for_range( + &mut self, + range_utf16: Range, + element_bounds: gpui::Bounds, + window: &mut Window, + cx: &mut Context, + ) -> Option> { + let text_layout_details = self.text_layout_details(window); + let gpui::Size { + width: em_width, + height: line_height, + } = self.character_size(window); + + let snapshot = self.snapshot(window, cx); + let scroll_position = snapshot.scroll_position(); + let scroll_left = scroll_position.x * em_width; + + let start = OffsetUtf16(range_utf16.start).to_display_point(&snapshot); + let x = snapshot.x_for_display_point(start, &text_layout_details) - scroll_left + + self.gutter_dimensions.width + + self.gutter_dimensions.margin; + let y = line_height * (start.row().as_f32() - scroll_position.y); + + Some(Bounds { + origin: element_bounds.origin + point(x, y), + size: size(em_width, line_height), + }) + } + + fn character_index_for_point( + &mut self, + point: gpui::Point, + _window: &mut Window, + _cx: &mut Context, + ) -> Option { + let position_map = self.last_position_map.as_ref()?; + if !position_map.text_hitbox.contains(&point) { + return None; + } + let display_point = position_map.point_for_position(point).previous_valid; + let anchor = position_map + .snapshot + .display_point_to_anchor(display_point, Bias::Left); + let utf16_offset = anchor.to_offset_utf16(&position_map.snapshot.buffer_snapshot); + Some(utf16_offset.0) + } +} + +trait SelectionExt { + fn display_range(&self, map: &DisplaySnapshot) -> Range; + fn spanned_rows( + &self, + include_end_if_at_line_start: bool, + map: &DisplaySnapshot, + ) -> Range; +} + +impl SelectionExt for Selection { + fn display_range(&self, map: &DisplaySnapshot) -> Range { + let start = self + .start + .to_point(&map.buffer_snapshot) + .to_display_point(map); + let end = self + .end + .to_point(&map.buffer_snapshot) + .to_display_point(map); + if self.reversed { + end..start + } else { + start..end + } + } + + fn spanned_rows( + &self, + include_end_if_at_line_start: bool, + map: &DisplaySnapshot, + ) -> Range { + let start = self.start.to_point(&map.buffer_snapshot); + let mut end = self.end.to_point(&map.buffer_snapshot); + if !include_end_if_at_line_start && start.row != end.row && end.column == 0 { + end.row -= 1; + } + + let buffer_start = map.prev_line_boundary(start).0; + let buffer_end = map.next_line_boundary(end).0; + MultiBufferRow(buffer_start.row)..MultiBufferRow(buffer_end.row + 1) + } +} + +impl InvalidationStack { + fn invalidate(&mut self, selections: &[Selection], buffer: &MultiBufferSnapshot) + where + S: Clone + ToOffset, + { + while let Some(region) = self.last() { + let all_selections_inside_invalidation_ranges = + if selections.len() == region.ranges().len() { + selections + .iter() + .zip(region.ranges().iter().map(|r| r.to_offset(buffer))) + .all(|(selection, invalidation_range)| { + let head = selection.head().to_offset(buffer); + invalidation_range.start <= head && invalidation_range.end >= head + }) + } else { + false + }; + + if all_selections_inside_invalidation_ranges { + break; + } else { + self.pop(); + } + } + } +} + +impl Default for InvalidationStack { + fn default() -> Self { + Self(Default::default()) + } +} + +impl Deref for InvalidationStack { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for InvalidationStack { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl InvalidationRegion for SnippetState { + fn ranges(&self) -> &[Range] { + &self.ranges[self.active_index] + } +} + +fn inline_completion_edit_text( + current_snapshot: &BufferSnapshot, + edits: &[(Range, String)], + edit_preview: &EditPreview, + include_deletions: bool, + cx: &App, +) -> HighlightedText { + let edits = edits + .iter() + .map(|(anchor, text)| { + ( + anchor.start.text_anchor..anchor.end.text_anchor, + text.clone(), + ) + }) + .collect::>(); + + edit_preview.highlight_edits(current_snapshot, &edits, include_deletions, cx) +} + +pub fn diagnostic_style(severity: DiagnosticSeverity, colors: &StatusColors) -> Hsla { + match severity { + DiagnosticSeverity::ERROR => colors.error, + DiagnosticSeverity::WARNING => colors.warning, + DiagnosticSeverity::INFORMATION => colors.info, + DiagnosticSeverity::HINT => colors.info, + _ => colors.ignored, + } +} + +pub fn styled_runs_for_code_label<'a>( + label: &'a CodeLabel, + syntax_theme: &'a theme::SyntaxTheme, +) -> impl 'a + Iterator, HighlightStyle)> { + let fade_out = HighlightStyle { + fade_out: Some(0.35), + ..Default::default() + }; + + let mut prev_end = label.filter_range.end; + label + .runs + .iter() + .enumerate() + .flat_map(move |(ix, (range, highlight_id))| { + let style = if let Some(style) = highlight_id.style(syntax_theme) { + style + } else { + return Default::default(); + }; + let mut muted_style = style; + muted_style.highlight(fade_out); + + let mut runs = SmallVec::<[(Range, HighlightStyle); 3]>::new(); + if range.start >= label.filter_range.end { + if range.start > prev_end { + runs.push((prev_end..range.start, fade_out)); + } + runs.push((range.clone(), muted_style)); + } else if range.end <= label.filter_range.end { + runs.push((range.clone(), style)); + } else { + runs.push((range.start..label.filter_range.end, style)); + runs.push((label.filter_range.end..range.end, muted_style)); + } + prev_end = cmp::max(prev_end, range.end); + + if ix + 1 == label.runs.len() && label.text.len() > prev_end { + runs.push((prev_end..label.text.len(), fade_out)); + } + + runs + }) +} + +pub(crate) fn split_words(text: &str) -> impl std::iter::Iterator + '_ { + let mut prev_index = 0; + let mut prev_codepoint: Option = None; + text.char_indices() + .chain([(text.len(), '\0')]) + .filter_map(move |(index, codepoint)| { + let prev_codepoint = prev_codepoint.replace(codepoint)?; + let is_boundary = index == text.len() + || !prev_codepoint.is_uppercase() && codepoint.is_uppercase() + || !prev_codepoint.is_alphanumeric() && codepoint.is_alphanumeric(); + if is_boundary { + let chunk = &text[prev_index..index]; + prev_index = index; + Some(chunk) + } else { + None + } + }) +} + +pub trait RangeToAnchorExt: Sized { + fn to_anchors(self, snapshot: &MultiBufferSnapshot) -> Range; + + fn to_display_points(self, snapshot: &EditorSnapshot) -> Range { + let anchor_range = self.to_anchors(&snapshot.buffer_snapshot); + anchor_range.start.to_display_point(snapshot)..anchor_range.end.to_display_point(snapshot) + } +} + +impl RangeToAnchorExt for Range { + fn to_anchors(self, snapshot: &MultiBufferSnapshot) -> Range { + let start_offset = self.start.to_offset(snapshot); + let end_offset = self.end.to_offset(snapshot); + if start_offset == end_offset { + snapshot.anchor_before(start_offset)..snapshot.anchor_before(end_offset) + } else { + snapshot.anchor_after(self.start)..snapshot.anchor_before(self.end) + } + } +} + +pub trait RowExt { + fn as_f32(&self) -> f32; + + fn next_row(&self) -> Self; + + fn previous_row(&self) -> Self; + + fn minus(&self, other: Self) -> u32; +} + +impl RowExt for DisplayRow { + fn as_f32(&self) -> f32 { + self.0 as f32 + } + + fn next_row(&self) -> Self { + Self(self.0 + 1) + } + + fn previous_row(&self) -> Self { + Self(self.0.saturating_sub(1)) + } + + fn minus(&self, other: Self) -> u32 { + self.0 - other.0 + } +} + +impl RowExt for MultiBufferRow { + fn as_f32(&self) -> f32 { + self.0 as f32 + } + + fn next_row(&self) -> Self { + Self(self.0 + 1) + } + + fn previous_row(&self) -> Self { + Self(self.0.saturating_sub(1)) + } + + fn minus(&self, other: Self) -> u32 { + self.0 - other.0 + } +} + +trait RowRangeExt { + type Row; + + fn len(&self) -> usize; + + fn iter_rows(&self) -> impl DoubleEndedIterator; +} + +impl RowRangeExt for Range { + type Row = MultiBufferRow; + + fn len(&self) -> usize { + (self.end.0 - self.start.0) as usize + } + + fn iter_rows(&self) -> impl DoubleEndedIterator { + (self.start.0..self.end.0).map(MultiBufferRow) + } +} + +impl RowRangeExt for Range { + type Row = DisplayRow; + + fn len(&self) -> usize { + (self.end.0 - self.start.0) as usize + } + + fn iter_rows(&self) -> impl DoubleEndedIterator { + (self.start.0..self.end.0).map(DisplayRow) + } +} + +/// If select range has more than one line, we +/// just point the cursor to range.start. +fn collapse_multiline_range(range: Range) -> Range { + if range.start.row == range.end.row { + range + } else { + range.start..range.start + } +} +pub struct KillRing(ClipboardItem); +impl Global for KillRing {} + +const UPDATE_DEBOUNCE: Duration = Duration::from_millis(50); + +enum BreakpointPromptEditAction { + Log, + Condition, + HitCondition, +} + +struct BreakpointPromptEditor { + pub(crate) prompt: Entity, + editor: WeakEntity, + breakpoint_anchor: Anchor, + breakpoint: Breakpoint, + edit_action: BreakpointPromptEditAction, + block_ids: HashSet, + gutter_dimensions: Arc>, + _subscriptions: Vec, +} + +impl BreakpointPromptEditor { + const MAX_LINES: u8 = 4; + + fn new( + editor: WeakEntity, + breakpoint_anchor: Anchor, + breakpoint: Breakpoint, + edit_action: BreakpointPromptEditAction, + window: &mut Window, + cx: &mut Context, + ) -> Self { + let base_text = match edit_action { + BreakpointPromptEditAction::Log => breakpoint.message.as_ref(), + BreakpointPromptEditAction::Condition => breakpoint.condition.as_ref(), + BreakpointPromptEditAction::HitCondition => breakpoint.hit_condition.as_ref(), + } + .map(|msg| msg.to_string()) + .unwrap_or_default(); + + let buffer = cx.new(|cx| Buffer::local(base_text, cx)); + let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); + + let prompt = cx.new(|cx| { + let mut prompt = Editor::new( + EditorMode::AutoHeight { + max_lines: Self::MAX_LINES as usize, + }, + buffer, + None, + window, + cx, + ); + prompt.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx); + prompt.set_show_cursor_when_unfocused(false, cx); + prompt.set_placeholder_text( + match edit_action { + BreakpointPromptEditAction::Log => "Message to log when a breakpoint is hit. Expressions within {} are interpolated.", + BreakpointPromptEditAction::Condition => "Condition when a breakpoint is hit. Expressions within {} are interpolated.", + BreakpointPromptEditAction::HitCondition => "How many breakpoint hits to ignore", + }, + cx, + ); + + prompt + }); + + Self { + prompt, + editor, + breakpoint_anchor, + breakpoint, + edit_action, + gutter_dimensions: Arc::new(Mutex::new(GutterDimensions::default())), + block_ids: Default::default(), + _subscriptions: vec![], + } + } + + pub(crate) fn add_block_ids(&mut self, block_ids: Vec) { + self.block_ids.extend(block_ids) + } + + fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context) { + if let Some(editor) = self.editor.upgrade() { + let message = self + .prompt + .read(cx) + .buffer + .read(cx) + .as_singleton() + .expect("A multi buffer in breakpoint prompt isn't possible") + .read(cx) + .as_rope() + .to_string(); + + editor.update(cx, |editor, cx| { + editor.edit_breakpoint_at_anchor( + self.breakpoint_anchor, + self.breakpoint.clone(), + match self.edit_action { + BreakpointPromptEditAction::Log => { + BreakpointEditAction::EditLogMessage(message.into()) + } + BreakpointPromptEditAction::Condition => { + BreakpointEditAction::EditCondition(message.into()) + } + BreakpointPromptEditAction::HitCondition => { + BreakpointEditAction::EditHitCondition(message.into()) + } + }, + cx, + ); + + editor.remove_blocks(self.block_ids.clone(), None, cx); + cx.focus_self(window); + }); + } + } + + fn cancel(&mut self, _: &menu::Cancel, window: &mut Window, cx: &mut Context) { + self.editor + .update(cx, |editor, cx| { + editor.remove_blocks(self.block_ids.clone(), None, cx); + window.focus(&editor.focus_handle); + }) + .log_err(); + } + + fn render_prompt_editor(&self, cx: &mut Context) -> impl IntoElement { + let settings = ThemeSettings::get_global(cx); + let text_style = TextStyle { + color: if self.prompt.read(cx).read_only(cx) { + cx.theme().colors().text_disabled + } else { + cx.theme().colors().text + }, + font_family: settings.buffer_font.family.clone(), + font_fallbacks: settings.buffer_font.fallbacks.clone(), + font_size: settings.buffer_font_size(cx).into(), + font_weight: settings.buffer_font.weight, + line_height: relative(settings.buffer_line_height.value()), + ..Default::default() + }; + EditorElement::new( + &self.prompt, + EditorStyle { + background: cx.theme().colors().editor_background, + local_player: cx.theme().players().local(), + text: text_style, + ..Default::default() + }, + ) + } +} + +impl Render for BreakpointPromptEditor { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let gutter_dimensions = *self.gutter_dimensions.lock(); + h_flex() + .key_context("Editor") + .bg(cx.theme().colors().editor_background) + .border_y_1() + .border_color(cx.theme().status().info_border) + .size_full() + .py(window.line_height() / 2.5) + .on_action(cx.listener(Self::confirm)) + .on_action(cx.listener(Self::cancel)) + .child(h_flex().w(gutter_dimensions.full_width() + (gutter_dimensions.margin / 2.0))) + .child(div().flex_1().child(self.render_prompt_editor(cx))) + } +} + +impl Focusable for BreakpointPromptEditor { + fn focus_handle(&self, cx: &App) -> FocusHandle { + self.prompt.focus_handle(cx) + } +} + +fn all_edits_insertions_or_deletions( + edits: &Vec<(Range, String)>, + snapshot: &MultiBufferSnapshot, +) -> bool { + let mut all_insertions = true; + let mut all_deletions = true; + + for (range, new_text) in edits.iter() { + let range_is_empty = range.to_offset(&snapshot).is_empty(); + let text_is_empty = new_text.is_empty(); + + if range_is_empty != text_is_empty { + if range_is_empty { + all_deletions = false; + } else { + all_insertions = false; + } + } else { + return false; + } + + if !all_insertions && !all_deletions { + return false; + } + } + all_insertions || all_deletions +} + +struct MissingEditPredictionKeybindingTooltip; + +impl Render for MissingEditPredictionKeybindingTooltip { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + ui::tooltip_container(window, cx, |container, _, cx| { + container + .flex_shrink_0() + .max_w_80() + .min_h(rems_from_px(124.)) + .justify_between() + .child( + v_flex() + .flex_1() + .text_ui_sm(cx) + .child(Label::new("Conflict with Accept Keybinding")) + .child("Your keymap currently overrides the default accept keybinding. To continue, assign one keybinding for the `editor::AcceptEditPrediction` action.") + ) + .child( + h_flex() + .pb_1() + .gap_1() + .items_end() + .w_full() + .child(Button::new("open-keymap", "Assign Keybinding").size(ButtonSize::Compact).on_click(|_ev, window, cx| { + window.dispatch_action(zed_actions::OpenKeymap.boxed_clone(), cx) + })) + .child(Button::new("see-docs", "See Docs").size(ButtonSize::Compact).on_click(|_ev, _window, cx| { + cx.open_url("https://zed.dev/docs/completions#edit-predictions-missing-keybinding"); + })), + ) + }) + } +} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct LineHighlight { + pub background: Background, + pub border: Option, + pub include_gutter: bool, + pub type_id: Option, +} + +fn render_diff_hunk_controls( + row: u32, + status: &DiffHunkStatus, + hunk_range: Range, + is_created_file: bool, + line_height: Pixels, + editor: &Entity, + _window: &mut Window, + cx: &mut App, +) -> AnyElement { + h_flex() + .h(line_height) + .mr_1() + .gap_1() + .px_0p5() + .pb_1() + .border_x_1() + .border_b_1() + .border_color(cx.theme().colors().border_variant) + .rounded_b_lg() + .bg(cx.theme().colors().editor_background) + .gap_1() + .occlude() + .shadow_md() + .child(if status.has_secondary_hunk() { + Button::new(("stage", row as u64), "Stage") + .alpha(if status.is_pending() { 0.66 } else { 1.0 }) + .tooltip({ + let focus_handle = editor.focus_handle(cx); + move |window, cx| { + Tooltip::for_action_in( + "Stage Hunk", + &::git::ToggleStaged, + &focus_handle, + window, + cx, + ) + } + }) + .on_click({ + let editor = editor.clone(); + move |_event, _window, cx| { + editor.update(cx, |editor, cx| { + editor.stage_or_unstage_diff_hunks( + true, + vec![hunk_range.start..hunk_range.start], + cx, + ); + }); + } + }) + } else { + Button::new(("unstage", row as u64), "Unstage") + .alpha(if status.is_pending() { 0.66 } else { 1.0 }) + .tooltip({ + let focus_handle = editor.focus_handle(cx); + move |window, cx| { + Tooltip::for_action_in( + "Unstage Hunk", + &::git::ToggleStaged, + &focus_handle, + window, + cx, + ) + } + }) + .on_click({ + let editor = editor.clone(); + move |_event, _window, cx| { + editor.update(cx, |editor, cx| { + editor.stage_or_unstage_diff_hunks( + false, + vec![hunk_range.start..hunk_range.start], + cx, + ); + }); + } + }) + }) + .child( + Button::new(("restore", row as u64), "Restore") + .tooltip({ + let focus_handle = editor.focus_handle(cx); + move |window, cx| { + Tooltip::for_action_in( + "Restore Hunk", + &::git::Restore, + &focus_handle, + window, + cx, + ) + } + }) + .on_click({ + let editor = editor.clone(); + move |_event, window, cx| { + editor.update(cx, |editor, cx| { + let snapshot = editor.snapshot(window, cx); + let point = hunk_range.start.to_point(&snapshot.buffer_snapshot); + editor.restore_hunks_in_ranges(vec![point..point], window, cx); + }); + } + }) + .disabled(is_created_file), + ) + .when( + !editor.read(cx).buffer().read(cx).all_diff_hunks_expanded(), + |el| { + el.child( + IconButton::new(("next-hunk", row as u64), IconName::ArrowDown) + .shape(IconButtonShape::Square) + .icon_size(IconSize::Small) + // .disabled(!has_multiple_hunks) + .tooltip({ + let focus_handle = editor.focus_handle(cx); + move |window, cx| { + Tooltip::for_action_in( + "Next Hunk", + &GoToHunk, + &focus_handle, + window, + cx, + ) + } + }) + .on_click({ + let editor = editor.clone(); + move |_event, window, cx| { + editor.update(cx, |editor, cx| { + let snapshot = editor.snapshot(window, cx); + let position = + hunk_range.end.to_point(&snapshot.buffer_snapshot); + editor.go_to_hunk_before_or_after_position( + &snapshot, + position, + Direction::Next, + window, + cx, + ); + editor.expand_selected_diff_hunks(cx); + }); + } + }), + ) + .child( + IconButton::new(("prev-hunk", row as u64), IconName::ArrowUp) + .shape(IconButtonShape::Square) + .icon_size(IconSize::Small) + // .disabled(!has_multiple_hunks) + .tooltip({ + let focus_handle = editor.focus_handle(cx); + move |window, cx| { + Tooltip::for_action_in( + "Previous Hunk", + &GoToPreviousHunk, + &focus_handle, + window, + cx, + ) + } + }) + .on_click({ + let editor = editor.clone(); + move |_event, window, cx| { + editor.update(cx, |editor, cx| { + let snapshot = editor.snapshot(window, cx); + let point = + hunk_range.start.to_point(&snapshot.buffer_snapshot); + editor.go_to_hunk_before_or_after_position( + &snapshot, + point, + Direction::Prev, + window, + cx, + ); + editor.expand_selected_diff_hunks(cx); + }); + } + }), + ) + }, + ) + .into_any_element() +} diff --git a/crates/assistant_tools/src/edit_agent/evals/fixtures/disable_cursor_blinking/before.rs b/crates/assistant_tools/src/edit_agent/evals/fixtures/disable_cursor_blinking/before.rs new file mode 100644 index 0000000000..afd47ed300 --- /dev/null +++ b/crates/assistant_tools/src/edit_agent/evals/fixtures/disable_cursor_blinking/before.rs @@ -0,0 +1,21343 @@ +#![allow(rustdoc::private_intra_doc_links)] +//! This is the place where everything editor-related is stored (data-wise) and displayed (ui-wise). +//! The main point of interest in this crate is [`Editor`] type, which is used in every other Zed part as a user input element. +//! It comes in different flavors: single line, multiline and a fixed height one. +//! +//! Editor contains of multiple large submodules: +//! * [`element`] — the place where all rendering happens +//! * [`display_map`] - chunks up text in the editor into the logical blocks, establishes coordinates and mapping between each of them. +//! Contains all metadata related to text transformations (folds, fake inlay text insertions, soft wraps, tab markup, etc.). +//! * [`inlay_hint_cache`] - is a storage of inlay hints out of LSP requests, responsible for querying LSP and updating `display_map`'s state accordingly. +//! +//! All other submodules and structs are mostly concerned with holding editor data about the way it displays current buffer region(s). +//! +//! If you're looking to improve Vim mode, you should check out Vim crate that wraps Editor and overrides its behavior. +pub mod actions; +mod blink_manager; +mod clangd_ext; +mod code_context_menus; +pub mod display_map; +mod editor_settings; +mod editor_settings_controls; +mod element; +mod git; +mod highlight_matching_bracket; +mod hover_links; +pub mod hover_popover; +mod indent_guides; +mod inlay_hint_cache; +pub mod items; +mod jsx_tag_auto_close; +mod linked_editing_ranges; +mod lsp_ext; +mod mouse_context_menu; +pub mod movement; +mod persistence; +mod proposed_changes_editor; +mod rust_analyzer_ext; +pub mod scroll; +mod selections_collection; +pub mod tasks; + +#[cfg(test)] +mod code_completion_tests; +#[cfg(test)] +mod editor_tests; +#[cfg(test)] +mod inline_completion_tests; +mod signature_help; +#[cfg(any(test, feature = "test-support"))] +pub mod test; + +pub(crate) use actions::*; +pub use actions::{AcceptEditPrediction, OpenExcerpts, OpenExcerptsSplit}; +use aho_corasick::AhoCorasick; +use anyhow::{Context as _, Result, anyhow}; +use blink_manager::BlinkManager; +use buffer_diff::DiffHunkStatus; +use client::{Collaborator, ParticipantIndex}; +use clock::ReplicaId; +use collections::{BTreeMap, HashMap, HashSet, VecDeque}; +use convert_case::{Case, Casing}; +use display_map::*; +pub use display_map::{ChunkRenderer, ChunkRendererContext, DisplayPoint, FoldPlaceholder}; +use editor_settings::GoToDefinitionFallback; +pub use editor_settings::{ + CurrentLineHighlight, EditorSettings, HideMouseMode, ScrollBeyondLastLine, SearchSettings, + ShowScrollbar, +}; +pub use editor_settings_controls::*; +use element::{AcceptEditPredictionBinding, LineWithInvisibles, PositionMap, layout_line}; +pub use element::{ + CursorLayout, EditorElement, HighlightedRange, HighlightedRangeLine, PointForPosition, +}; +use feature_flags::{DebuggerFeatureFlag, FeatureFlagAppExt}; +use futures::{ + FutureExt, + future::{self, Shared, join}, +}; +use fuzzy::StringMatchCandidate; + +use ::git::blame::BlameEntry; +use ::git::{Restore, blame::ParsedCommitMessage}; +use code_context_menus::{ + AvailableCodeAction, CodeActionContents, CodeActionsItem, CodeActionsMenu, CodeContextMenu, + CompletionsMenu, ContextMenuOrigin, +}; +use git::blame::{GitBlame, GlobalBlameRenderer}; +use gpui::{ + Action, Animation, AnimationExt, AnyElement, App, AppContext, AsyncWindowContext, + AvailableSpace, Background, Bounds, ClickEvent, ClipboardEntry, ClipboardItem, Context, + DispatchPhase, Edges, Entity, EntityInputHandler, EventEmitter, FocusHandle, FocusOutEvent, + Focusable, FontId, FontWeight, Global, HighlightStyle, Hsla, KeyContext, Modifiers, + MouseButton, MouseDownEvent, PaintQuad, ParentElement, Pixels, Render, ScrollHandle, + SharedString, Size, Stateful, Styled, Subscription, Task, TextStyle, TextStyleRefinement, + UTF16Selection, UnderlineStyle, UniformListScrollHandle, WeakEntity, WeakFocusHandle, Window, + div, impl_actions, point, prelude::*, pulsating_between, px, relative, size, +}; +use highlight_matching_bracket::refresh_matching_bracket_highlights; +use hover_links::{HoverLink, HoveredLinkState, InlayHighlight, find_file}; +pub use hover_popover::hover_markdown_style; +use hover_popover::{HoverState, hide_hover}; +use indent_guides::ActiveIndentGuidesState; +use inlay_hint_cache::{InlayHintCache, InlaySplice, InvalidationStrategy}; +pub use inline_completion::Direction; +use inline_completion::{EditPredictionProvider, InlineCompletionProviderHandle}; +pub use items::MAX_TAB_TITLE_LEN; +use itertools::Itertools; +use language::{ + AutoindentMode, BracketMatch, BracketPair, Buffer, Capability, CharKind, CodeLabel, + CursorShape, DiagnosticEntry, DiffOptions, EditPredictionsMode, EditPreview, HighlightedText, + IndentKind, IndentSize, Language, OffsetRangeExt, Point, Selection, SelectionGoal, TextObject, + TransactionId, TreeSitterOptions, WordsQuery, + language_settings::{ + self, InlayHintSettings, LspInsertMode, RewrapBehavior, WordsCompletionMode, + all_language_settings, language_settings, + }, + point_from_lsp, text_diff_with_options, +}; +use language::{BufferRow, CharClassifier, Runnable, RunnableRange, point_to_lsp}; +use linked_editing_ranges::refresh_linked_ranges; +use markdown::Markdown; +use mouse_context_menu::MouseContextMenu; +use persistence::DB; +use project::{ + ProjectPath, + debugger::{ + breakpoint_store::{ + BreakpointEditAction, BreakpointState, BreakpointStore, BreakpointStoreEvent, + }, + session::{Session, SessionEvent}, + }, +}; + +pub use git::blame::BlameRenderer; +pub use proposed_changes_editor::{ + ProposedChangeLocation, ProposedChangesEditor, ProposedChangesEditorToolbar, +}; +use smallvec::smallvec; +use std::{cell::OnceCell, iter::Peekable}; +use task::{ResolvedTask, RunnableTag, TaskTemplate, TaskVariables}; + +pub use lsp::CompletionContext; +use lsp::{ + CodeActionKind, CompletionItemKind, CompletionTriggerKind, DiagnosticSeverity, + InsertTextFormat, InsertTextMode, LanguageServerId, LanguageServerName, +}; + +use language::BufferSnapshot; +pub use lsp_ext::lsp_tasks; +use movement::TextLayoutDetails; +pub use multi_buffer::{ + Anchor, AnchorRangeExt, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, PathKey, + RowInfo, ToOffset, ToPoint, +}; +use multi_buffer::{ + ExcerptInfo, ExpandExcerptDirection, MultiBufferDiffHunk, MultiBufferPoint, MultiBufferRow, + MultiOrSingleBufferOffsetRange, ToOffsetUtf16, +}; +use parking_lot::Mutex; +use project::{ + CodeAction, Completion, CompletionIntent, CompletionSource, DocumentHighlight, InlayHint, + Location, LocationLink, PrepareRenameResponse, Project, ProjectItem, ProjectTransaction, + TaskSourceKind, + debugger::breakpoint_store::Breakpoint, + lsp_store::{CompletionDocumentation, FormatTrigger, LspFormatTarget, OpenLspBufferHandle}, + project_settings::{GitGutterSetting, ProjectSettings}, +}; +use rand::prelude::*; +use rpc::{ErrorExt, proto::*}; +use scroll::{Autoscroll, OngoingScroll, ScrollAnchor, ScrollManager, ScrollbarAutoHide}; +use selections_collection::{ + MutableSelectionsCollection, SelectionsCollection, resolve_selections, +}; +use serde::{Deserialize, Serialize}; +use settings::{Settings, SettingsLocation, SettingsStore, update_settings_file}; +use smallvec::SmallVec; +use snippet::Snippet; +use std::sync::Arc; +use std::{ + any::TypeId, + borrow::Cow, + cell::RefCell, + cmp::{self, Ordering, Reverse}, + mem, + num::NonZeroU32, + ops::{ControlFlow, Deref, DerefMut, Not as _, Range, RangeInclusive}, + path::{Path, PathBuf}, + rc::Rc, + time::{Duration, Instant}, +}; +pub use sum_tree::Bias; +use sum_tree::TreeMap; +use text::{BufferId, FromAnchor, OffsetUtf16, Rope}; +use theme::{ + ActiveTheme, PlayerColor, StatusColors, SyntaxTheme, ThemeColors, ThemeSettings, + observe_buffer_font_size_adjustment, +}; +use ui::{ + ButtonSize, ButtonStyle, ContextMenu, Disclosure, IconButton, IconButtonShape, IconName, + IconSize, Key, Tooltip, h_flex, prelude::*, +}; +use util::{RangeExt, ResultExt, TryFutureExt, maybe, post_inc}; +use workspace::{ + Item as WorkspaceItem, ItemId, ItemNavHistory, OpenInTerminal, OpenTerminal, + RestoreOnStartupBehavior, SERIALIZATION_THROTTLE_TIME, SplitDirection, TabBarSettings, Toast, + ViewId, Workspace, WorkspaceId, WorkspaceSettings, + item::{ItemHandle, PreviewTabsSettings}, + notifications::{DetachAndPromptErr, NotificationId, NotifyTaskExt}, + searchable::SearchEvent, +}; + +use crate::hover_links::{find_url, find_url_from_range}; +use crate::signature_help::{SignatureHelpHiddenBy, SignatureHelpState}; + +pub const FILE_HEADER_HEIGHT: u32 = 2; +pub const MULTI_BUFFER_EXCERPT_HEADER_HEIGHT: u32 = 1; +pub const DEFAULT_MULTIBUFFER_CONTEXT: u32 = 2; +const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500); +const MAX_LINE_LEN: usize = 1024; +const MIN_NAVIGATION_HISTORY_ROW_DELTA: i64 = 10; +const MAX_SELECTION_HISTORY_LEN: usize = 1024; +pub(crate) const CURSORS_VISIBLE_FOR: Duration = Duration::from_millis(2000); +#[doc(hidden)] +pub const CODE_ACTIONS_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(250); +const SELECTION_HIGHLIGHT_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(100); + +pub(crate) const CODE_ACTION_TIMEOUT: Duration = Duration::from_secs(5); +pub(crate) const FORMAT_TIMEOUT: Duration = Duration::from_secs(5); +pub(crate) const SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT: Duration = Duration::from_secs(1); + +pub(crate) const EDIT_PREDICTION_KEY_CONTEXT: &str = "edit_prediction"; +pub(crate) const EDIT_PREDICTION_CONFLICT_KEY_CONTEXT: &str = "edit_prediction_conflict"; +pub(crate) const MIN_LINE_NUMBER_DIGITS: u32 = 4; + +pub type RenderDiffHunkControlsFn = Arc< + dyn Fn( + u32, + &DiffHunkStatus, + Range, + bool, + Pixels, + &Entity, + &mut Window, + &mut App, + ) -> AnyElement, +>; + +const COLUMNAR_SELECTION_MODIFIERS: Modifiers = Modifiers { + alt: true, + shift: true, + control: false, + platform: false, + function: false, +}; + +struct InlineValueCache { + enabled: bool, + inlays: Vec, + refresh_task: Task>, +} + +impl InlineValueCache { + fn new(enabled: bool) -> Self { + Self { + enabled, + inlays: Vec::new(), + refresh_task: Task::ready(None), + } + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum InlayId { + InlineCompletion(usize), + Hint(usize), + DebuggerValue(usize), +} + +impl InlayId { + fn id(&self) -> usize { + match self { + Self::InlineCompletion(id) => *id, + Self::Hint(id) => *id, + Self::DebuggerValue(id) => *id, + } + } +} + +pub enum ActiveDebugLine {} +enum DocumentHighlightRead {} +enum DocumentHighlightWrite {} +enum InputComposition {} +enum SelectedTextHighlight {} + +pub enum ConflictsOuter {} +pub enum ConflictsOurs {} +pub enum ConflictsTheirs {} +pub enum ConflictsOursMarker {} +pub enum ConflictsTheirsMarker {} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum Navigated { + Yes, + No, +} + +impl Navigated { + pub fn from_bool(yes: bool) -> Navigated { + if yes { Navigated::Yes } else { Navigated::No } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +enum DisplayDiffHunk { + Folded { + display_row: DisplayRow, + }, + Unfolded { + is_created_file: bool, + diff_base_byte_range: Range, + display_row_range: Range, + multi_buffer_range: Range, + status: DiffHunkStatus, + }, +} + +pub enum HideMouseCursorOrigin { + TypingAction, + MovementAction, +} + +pub fn init_settings(cx: &mut App) { + EditorSettings::register(cx); +} + +pub fn init(cx: &mut App) { + init_settings(cx); + + cx.set_global(GlobalBlameRenderer(Arc::new(()))); + + workspace::register_project_item::(cx); + workspace::FollowableViewRegistry::register::(cx); + workspace::register_serializable_item::(cx); + + cx.observe_new( + |workspace: &mut Workspace, _: Option<&mut Window>, _cx: &mut Context| { + workspace.register_action(Editor::new_file); + workspace.register_action(Editor::new_file_vertical); + workspace.register_action(Editor::new_file_horizontal); + workspace.register_action(Editor::cancel_language_server_work); + }, + ) + .detach(); + + cx.on_action(move |_: &workspace::NewFile, cx| { + let app_state = workspace::AppState::global(cx); + if let Some(app_state) = app_state.upgrade() { + workspace::open_new( + Default::default(), + app_state, + cx, + |workspace, window, cx| { + Editor::new_file(workspace, &Default::default(), window, cx) + }, + ) + .detach(); + } + }); + cx.on_action(move |_: &workspace::NewWindow, cx| { + let app_state = workspace::AppState::global(cx); + if let Some(app_state) = app_state.upgrade() { + workspace::open_new( + Default::default(), + app_state, + cx, + |workspace, window, cx| { + cx.activate(true); + Editor::new_file(workspace, &Default::default(), window, cx) + }, + ) + .detach(); + } + }); +} + +pub fn set_blame_renderer(renderer: impl BlameRenderer + 'static, cx: &mut App) { + cx.set_global(GlobalBlameRenderer(Arc::new(renderer))); +} + +pub trait DiagnosticRenderer { + fn render_group( + &self, + diagnostic_group: Vec>, + buffer_id: BufferId, + snapshot: EditorSnapshot, + editor: WeakEntity, + cx: &mut App, + ) -> Vec>; + + fn render_hover( + &self, + diagnostic_group: Vec>, + range: Range, + buffer_id: BufferId, + cx: &mut App, + ) -> Option>; + + fn open_link( + &self, + editor: &mut Editor, + link: SharedString, + window: &mut Window, + cx: &mut Context, + ); +} + +pub(crate) struct GlobalDiagnosticRenderer(pub Arc); + +impl GlobalDiagnosticRenderer { + fn global(cx: &App) -> Option> { + cx.try_global::().map(|g| g.0.clone()) + } +} + +impl gpui::Global for GlobalDiagnosticRenderer {} +pub fn set_diagnostic_renderer(renderer: impl DiagnosticRenderer + 'static, cx: &mut App) { + cx.set_global(GlobalDiagnosticRenderer(Arc::new(renderer))); +} + +pub struct SearchWithinRange; + +trait InvalidationRegion { + fn ranges(&self) -> &[Range]; +} + +#[derive(Clone, Debug, PartialEq)] +pub enum SelectPhase { + Begin { + position: DisplayPoint, + add: bool, + click_count: usize, + }, + BeginColumnar { + position: DisplayPoint, + reset: bool, + goal_column: u32, + }, + Extend { + position: DisplayPoint, + click_count: usize, + }, + Update { + position: DisplayPoint, + goal_column: u32, + scroll_delta: gpui::Point, + }, + End, +} + +#[derive(Clone, Debug)] +pub enum SelectMode { + Character, + Word(Range), + Line(Range), + All, +} + +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +pub enum EditorMode { + SingleLine { + auto_width: bool, + }, + AutoHeight { + max_lines: usize, + }, + Full { + /// When set to `true`, the editor will scale its UI elements with the buffer font size. + scale_ui_elements_with_buffer_font_size: bool, + /// When set to `true`, the editor will render a background for the active line. + show_active_line_background: bool, + /// When set to `true`, the editor's height will be determined by its content. + sized_by_content: bool, + }, +} + +impl EditorMode { + pub fn full() -> Self { + Self::Full { + scale_ui_elements_with_buffer_font_size: true, + show_active_line_background: true, + sized_by_content: false, + } + } + + pub fn is_full(&self) -> bool { + matches!(self, Self::Full { .. }) + } +} + +#[derive(Copy, Clone, Debug)] +pub enum SoftWrap { + /// Prefer not to wrap at all. + /// + /// Note: this is currently internal, as actually limited by [`crate::MAX_LINE_LEN`] until it wraps. + /// The mode is used inside git diff hunks, where it's seems currently more useful to not wrap as much as possible. + GitDiff, + /// Prefer a single line generally, unless an overly long line is encountered. + None, + /// Soft wrap lines that exceed the editor width. + EditorWidth, + /// Soft wrap lines at the preferred line length. + Column(u32), + /// Soft wrap line at the preferred line length or the editor width (whichever is smaller). + Bounded(u32), +} + +#[derive(Clone)] +pub struct EditorStyle { + pub background: Hsla, + pub local_player: PlayerColor, + pub text: TextStyle, + pub scrollbar_width: Pixels, + pub syntax: Arc, + pub status: StatusColors, + pub inlay_hints_style: HighlightStyle, + pub inline_completion_styles: InlineCompletionStyles, + pub unnecessary_code_fade: f32, +} + +impl Default for EditorStyle { + fn default() -> Self { + Self { + background: Hsla::default(), + local_player: PlayerColor::default(), + text: TextStyle::default(), + scrollbar_width: Pixels::default(), + syntax: Default::default(), + // HACK: Status colors don't have a real default. + // We should look into removing the status colors from the editor + // style and retrieve them directly from the theme. + status: StatusColors::dark(), + inlay_hints_style: HighlightStyle::default(), + inline_completion_styles: InlineCompletionStyles { + insertion: HighlightStyle::default(), + whitespace: HighlightStyle::default(), + }, + unnecessary_code_fade: Default::default(), + } + } +} + +pub fn make_inlay_hints_style(cx: &mut App) -> HighlightStyle { + let show_background = language_settings::language_settings(None, None, cx) + .inlay_hints + .show_background; + + HighlightStyle { + color: Some(cx.theme().status().hint), + background_color: show_background.then(|| cx.theme().status().hint_background), + ..HighlightStyle::default() + } +} + +pub fn make_suggestion_styles(cx: &mut App) -> InlineCompletionStyles { + InlineCompletionStyles { + insertion: HighlightStyle { + color: Some(cx.theme().status().predictive), + ..HighlightStyle::default() + }, + whitespace: HighlightStyle { + background_color: Some(cx.theme().status().created_background), + ..HighlightStyle::default() + }, + } +} + +type CompletionId = usize; + +pub(crate) enum EditDisplayMode { + TabAccept, + DiffPopover, + Inline, +} + +enum InlineCompletion { + Edit { + edits: Vec<(Range, String)>, + edit_preview: Option, + display_mode: EditDisplayMode, + snapshot: BufferSnapshot, + }, + Move { + target: Anchor, + snapshot: BufferSnapshot, + }, +} + +struct InlineCompletionState { + inlay_ids: Vec, + completion: InlineCompletion, + completion_id: Option, + invalidation_range: Range, +} + +enum EditPredictionSettings { + Disabled, + Enabled { + show_in_menu: bool, + preview_requires_modifier: bool, + }, +} + +enum InlineCompletionHighlight {} + +#[derive(Debug, Clone)] +struct InlineDiagnostic { + message: SharedString, + group_id: usize, + is_primary: bool, + start: Point, + severity: DiagnosticSeverity, +} + +pub enum MenuInlineCompletionsPolicy { + Never, + ByProvider, +} + +pub enum EditPredictionPreview { + /// Modifier is not pressed + Inactive { released_too_fast: bool }, + /// Modifier pressed + Active { + since: Instant, + previous_scroll_position: Option, + }, +} + +impl EditPredictionPreview { + pub fn released_too_fast(&self) -> bool { + match self { + EditPredictionPreview::Inactive { released_too_fast } => *released_too_fast, + EditPredictionPreview::Active { .. } => false, + } + } + + pub fn set_previous_scroll_position(&mut self, scroll_position: Option) { + if let EditPredictionPreview::Active { + previous_scroll_position, + .. + } = self + { + *previous_scroll_position = scroll_position; + } + } +} + +pub struct ContextMenuOptions { + pub min_entries_visible: usize, + pub max_entries_visible: usize, + pub placement: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ContextMenuPlacement { + Above, + Below, +} + +#[derive(Copy, Clone, Eq, PartialEq, PartialOrd, Ord, Debug, Default)] +struct EditorActionId(usize); + +impl EditorActionId { + pub fn post_inc(&mut self) -> Self { + let answer = self.0; + + *self = Self(answer + 1); + + Self(answer) + } +} + +// type GetFieldEditorTheme = dyn Fn(&theme::Theme) -> theme::FieldEditor; +// type OverrideTextStyle = dyn Fn(&EditorStyle) -> Option; + +type BackgroundHighlight = (fn(&ThemeColors) -> Hsla, Arc<[Range]>); +type GutterHighlight = (fn(&App) -> Hsla, Arc<[Range]>); + +#[derive(Default)] +struct ScrollbarMarkerState { + scrollbar_size: Size, + dirty: bool, + markers: Arc<[PaintQuad]>, + pending_refresh: Option>>, +} + +impl ScrollbarMarkerState { + fn should_refresh(&self, scrollbar_size: Size) -> bool { + self.pending_refresh.is_none() && (self.scrollbar_size != scrollbar_size || self.dirty) + } +} + +#[derive(Clone, Debug)] +struct RunnableTasks { + templates: Vec<(TaskSourceKind, TaskTemplate)>, + offset: multi_buffer::Anchor, + // We need the column at which the task context evaluation should take place (when we're spawning it via gutter). + column: u32, + // Values of all named captures, including those starting with '_' + extra_variables: HashMap, + // Full range of the tagged region. We use it to determine which `extra_variables` to grab for context resolution in e.g. a modal. + context_range: Range, +} + +impl RunnableTasks { + fn resolve<'a>( + &'a self, + cx: &'a task::TaskContext, + ) -> impl Iterator + 'a { + self.templates.iter().filter_map(|(kind, template)| { + template + .resolve_task(&kind.to_id_base(), cx) + .map(|task| (kind.clone(), task)) + }) + } +} + +#[derive(Clone)] +struct ResolvedTasks { + templates: SmallVec<[(TaskSourceKind, ResolvedTask); 1]>, + position: Anchor, +} + +#[derive(Copy, Clone, Debug, PartialEq, PartialOrd)] +struct BufferOffset(usize); + +// Addons allow storing per-editor state in other crates (e.g. Vim) +pub trait Addon: 'static { + fn extend_key_context(&self, _: &mut KeyContext, _: &App) {} + + fn render_buffer_header_controls( + &self, + _: &ExcerptInfo, + _: &Window, + _: &App, + ) -> Option { + None + } + + fn to_any(&self) -> &dyn std::any::Any; + + fn to_any_mut(&mut self) -> Option<&mut dyn std::any::Any> { + None + } +} + +/// A set of caret positions, registered when the editor was edited. +pub struct ChangeList { + changes: Vec>, + /// Currently "selected" change. + position: Option, +} + +impl ChangeList { + pub fn new() -> Self { + Self { + changes: Vec::new(), + position: None, + } + } + + /// Moves to the next change in the list (based on the direction given) and returns the caret positions for the next change. + /// If reaches the end of the list in the direction, returns the corresponding change until called for a different direction. + pub fn next_change(&mut self, count: usize, direction: Direction) -> Option<&[Anchor]> { + if self.changes.is_empty() { + return None; + } + + let prev = self.position.unwrap_or(self.changes.len()); + let next = if direction == Direction::Prev { + prev.saturating_sub(count) + } else { + (prev + count).min(self.changes.len() - 1) + }; + self.position = Some(next); + self.changes.get(next).map(|anchors| anchors.as_slice()) + } + + /// Adds a new change to the list, resetting the change list position. + pub fn push_to_change_list(&mut self, pop_state: bool, new_positions: Vec) { + self.position.take(); + if pop_state { + self.changes.pop(); + } + self.changes.push(new_positions.clone()); + } + + pub fn last(&self) -> Option<&[Anchor]> { + self.changes.last().map(|anchors| anchors.as_slice()) + } +} + +#[derive(Clone)] +struct InlineBlamePopoverState { + scroll_handle: ScrollHandle, + commit_message: Option, + markdown: Entity, +} + +struct InlineBlamePopover { + position: gpui::Point, + show_task: Option>, + hide_task: Option>, + popover_bounds: Option>, + popover_state: InlineBlamePopoverState, +} + +/// Represents a breakpoint indicator that shows up when hovering over lines in the gutter that don't have +/// a breakpoint on them. +#[derive(Clone, Copy, Debug)] +struct PhantomBreakpointIndicator { + display_row: DisplayRow, + /// There's a small debounce between hovering over the line and showing the indicator. + /// We don't want to show the indicator when moving the mouse from editor to e.g. project panel. + is_active: bool, + collides_with_existing_breakpoint: bool, +} +/// Zed's primary implementation of text input, allowing users to edit a [`MultiBuffer`]. +/// +/// See the [module level documentation](self) for more information. +pub struct Editor { + focus_handle: FocusHandle, + last_focused_descendant: Option, + /// The text buffer being edited + buffer: Entity, + /// Map of how text in the buffer should be displayed. + /// Handles soft wraps, folds, fake inlay text insertions, etc. + pub display_map: Entity, + pub selections: SelectionsCollection, + pub scroll_manager: ScrollManager, + /// When inline assist editors are linked, they all render cursors because + /// typing enters text into each of them, even the ones that aren't focused. + pub(crate) show_cursor_when_unfocused: bool, + columnar_selection_tail: Option, + add_selections_state: Option, + select_next_state: Option, + select_prev_state: Option, + selection_history: SelectionHistory, + autoclose_regions: Vec, + snippet_stack: InvalidationStack, + select_syntax_node_history: SelectSyntaxNodeHistory, + ime_transaction: Option, + active_diagnostics: ActiveDiagnostic, + show_inline_diagnostics: bool, + inline_diagnostics_update: Task<()>, + inline_diagnostics_enabled: bool, + inline_diagnostics: Vec<(Anchor, InlineDiagnostic)>, + soft_wrap_mode_override: Option, + hard_wrap: Option, + + // TODO: make this a access method + pub project: Option>, + semantics_provider: Option>, + completion_provider: Option>, + collaboration_hub: Option>, + blink_manager: Entity, + show_cursor_names: bool, + hovered_cursors: HashMap>, + pub show_local_selections: bool, + mode: EditorMode, + show_breadcrumbs: bool, + show_gutter: bool, + show_scrollbars: bool, + disable_scrolling: bool, + disable_expand_excerpt_buttons: bool, + show_line_numbers: Option, + use_relative_line_numbers: Option, + show_git_diff_gutter: Option, + show_code_actions: Option, + show_runnables: Option, + show_breakpoints: Option, + show_wrap_guides: Option, + show_indent_guides: Option, + placeholder_text: Option>, + highlight_order: usize, + highlighted_rows: HashMap>, + background_highlights: TreeMap, + gutter_highlights: TreeMap, + scrollbar_marker_state: ScrollbarMarkerState, + active_indent_guides_state: ActiveIndentGuidesState, + nav_history: Option, + context_menu: RefCell>, + context_menu_options: Option, + mouse_context_menu: Option, + completion_tasks: Vec<(CompletionId, Task>)>, + inline_blame_popover: Option, + signature_help_state: SignatureHelpState, + auto_signature_help: Option, + find_all_references_task_sources: Vec, + next_completion_id: CompletionId, + available_code_actions: Option<(Location, Rc<[AvailableCodeAction]>)>, + code_actions_task: Option>>, + quick_selection_highlight_task: Option<(Range, Task<()>)>, + debounced_selection_highlight_task: Option<(Range, Task<()>)>, + document_highlights_task: Option>, + linked_editing_range_task: Option>>, + linked_edit_ranges: linked_editing_ranges::LinkedEditingRanges, + pending_rename: Option, + searchable: bool, + cursor_shape: CursorShape, + current_line_highlight: Option, + collapse_matches: bool, + autoindent_mode: Option, + workspace: Option<(WeakEntity, Option)>, + input_enabled: bool, + use_modal_editing: bool, + read_only: bool, + leader_peer_id: Option, + remote_id: Option, + pub hover_state: HoverState, + pending_mouse_down: Option>>>, + gutter_hovered: bool, + hovered_link_state: Option, + edit_prediction_provider: Option, + code_action_providers: Vec>, + active_inline_completion: Option, + /// Used to prevent flickering as the user types while the menu is open + stale_inline_completion_in_menu: Option, + edit_prediction_settings: EditPredictionSettings, + inline_completions_hidden_for_vim_mode: bool, + show_inline_completions_override: Option, + menu_inline_completions_policy: MenuInlineCompletionsPolicy, + edit_prediction_preview: EditPredictionPreview, + edit_prediction_indent_conflict: bool, + edit_prediction_requires_modifier_in_indent_conflict: bool, + inlay_hint_cache: InlayHintCache, + next_inlay_id: usize, + _subscriptions: Vec, + pixel_position_of_newest_cursor: Option>, + gutter_dimensions: GutterDimensions, + style: Option, + text_style_refinement: Option, + next_editor_action_id: EditorActionId, + editor_actions: + Rc)>>>>, + use_autoclose: bool, + use_auto_surround: bool, + auto_replace_emoji_shortcode: bool, + jsx_tag_auto_close_enabled_in_any_buffer: bool, + show_git_blame_gutter: bool, + show_git_blame_inline: bool, + show_git_blame_inline_delay_task: Option>, + git_blame_inline_enabled: bool, + render_diff_hunk_controls: RenderDiffHunkControlsFn, + serialize_dirty_buffers: bool, + show_selection_menu: Option, + blame: Option>, + blame_subscription: Option, + custom_context_menu: Option< + Box< + dyn 'static + + Fn( + &mut Self, + DisplayPoint, + &mut Window, + &mut Context, + ) -> Option>, + >, + >, + last_bounds: Option>, + last_position_map: Option>, + expect_bounds_change: Option>, + tasks: BTreeMap<(BufferId, BufferRow), RunnableTasks>, + tasks_update_task: Option>, + breakpoint_store: Option>, + gutter_breakpoint_indicator: (Option, Option>), + in_project_search: bool, + previous_search_ranges: Option]>>, + breadcrumb_header: Option, + focused_block: Option, + next_scroll_position: NextScrollCursorCenterTopBottom, + addons: HashMap>, + registered_buffers: HashMap, + load_diff_task: Option>>, + selection_mark_mode: bool, + toggle_fold_multiple_buffers: Task<()>, + _scroll_cursor_center_top_bottom_task: Task<()>, + serialize_selections: Task<()>, + serialize_folds: Task<()>, + mouse_cursor_hidden: bool, + hide_mouse_mode: HideMouseMode, + pub change_list: ChangeList, + inline_value_cache: InlineValueCache, +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)] +enum NextScrollCursorCenterTopBottom { + #[default] + Center, + Top, + Bottom, +} + +impl NextScrollCursorCenterTopBottom { + fn next(&self) -> Self { + match self { + Self::Center => Self::Top, + Self::Top => Self::Bottom, + Self::Bottom => Self::Center, + } + } +} + +#[derive(Clone)] +pub struct EditorSnapshot { + pub mode: EditorMode, + show_gutter: bool, + show_line_numbers: Option, + show_git_diff_gutter: Option, + show_code_actions: Option, + show_runnables: Option, + show_breakpoints: Option, + git_blame_gutter_max_author_length: Option, + pub display_snapshot: DisplaySnapshot, + pub placeholder_text: Option>, + is_focused: bool, + scroll_anchor: ScrollAnchor, + ongoing_scroll: OngoingScroll, + current_line_highlight: CurrentLineHighlight, + gutter_hovered: bool, +} + +#[derive(Default, Debug, Clone, Copy)] +pub struct GutterDimensions { + pub left_padding: Pixels, + pub right_padding: Pixels, + pub width: Pixels, + pub margin: Pixels, + pub git_blame_entries_width: Option, +} + +impl GutterDimensions { + /// The full width of the space taken up by the gutter. + pub fn full_width(&self) -> Pixels { + self.margin + self.width + } + + /// The width of the space reserved for the fold indicators, + /// use alongside 'justify_end' and `gutter_width` to + /// right align content with the line numbers + pub fn fold_area_width(&self) -> Pixels { + self.margin + self.right_padding + } +} + +#[derive(Debug)] +pub struct RemoteSelection { + pub replica_id: ReplicaId, + pub selection: Selection, + pub cursor_shape: CursorShape, + pub peer_id: PeerId, + pub line_mode: bool, + pub participant_index: Option, + pub user_name: Option, +} + +#[derive(Clone, Debug)] +struct SelectionHistoryEntry { + selections: Arc<[Selection]>, + select_next_state: Option, + select_prev_state: Option, + add_selections_state: Option, +} + +enum SelectionHistoryMode { + Normal, + Undoing, + Redoing, +} + +#[derive(Clone, PartialEq, Eq, Hash)] +struct HoveredCursor { + replica_id: u16, + selection_id: usize, +} + +impl Default for SelectionHistoryMode { + fn default() -> Self { + Self::Normal + } +} + +#[derive(Default)] +struct SelectionHistory { + #[allow(clippy::type_complexity)] + selections_by_transaction: + HashMap]>, Option]>>)>, + mode: SelectionHistoryMode, + undo_stack: VecDeque, + redo_stack: VecDeque, +} + +impl SelectionHistory { + fn insert_transaction( + &mut self, + transaction_id: TransactionId, + selections: Arc<[Selection]>, + ) { + self.selections_by_transaction + .insert(transaction_id, (selections, None)); + } + + #[allow(clippy::type_complexity)] + fn transaction( + &self, + transaction_id: TransactionId, + ) -> Option<&(Arc<[Selection]>, Option]>>)> { + self.selections_by_transaction.get(&transaction_id) + } + + #[allow(clippy::type_complexity)] + fn transaction_mut( + &mut self, + transaction_id: TransactionId, + ) -> Option<&mut (Arc<[Selection]>, Option]>>)> { + self.selections_by_transaction.get_mut(&transaction_id) + } + + fn push(&mut self, entry: SelectionHistoryEntry) { + if !entry.selections.is_empty() { + match self.mode { + SelectionHistoryMode::Normal => { + self.push_undo(entry); + self.redo_stack.clear(); + } + SelectionHistoryMode::Undoing => self.push_redo(entry), + SelectionHistoryMode::Redoing => self.push_undo(entry), + } + } + } + + fn push_undo(&mut self, entry: SelectionHistoryEntry) { + if self + .undo_stack + .back() + .map_or(true, |e| e.selections != entry.selections) + { + self.undo_stack.push_back(entry); + if self.undo_stack.len() > MAX_SELECTION_HISTORY_LEN { + self.undo_stack.pop_front(); + } + } + } + + fn push_redo(&mut self, entry: SelectionHistoryEntry) { + if self + .redo_stack + .back() + .map_or(true, |e| e.selections != entry.selections) + { + self.redo_stack.push_back(entry); + if self.redo_stack.len() > MAX_SELECTION_HISTORY_LEN { + self.redo_stack.pop_front(); + } + } + } +} + +#[derive(Clone, Copy)] +pub struct RowHighlightOptions { + pub autoscroll: bool, + pub include_gutter: bool, +} + +impl Default for RowHighlightOptions { + fn default() -> Self { + Self { + autoscroll: Default::default(), + include_gutter: true, + } + } +} + +struct RowHighlight { + index: usize, + range: Range, + color: Hsla, + options: RowHighlightOptions, + type_id: TypeId, +} + +#[derive(Clone, Debug)] +struct AddSelectionsState { + above: bool, + stack: Vec, +} + +#[derive(Clone)] +struct SelectNextState { + query: AhoCorasick, + wordwise: bool, + done: bool, +} + +impl std::fmt::Debug for SelectNextState { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct(std::any::type_name::()) + .field("wordwise", &self.wordwise) + .field("done", &self.done) + .finish() + } +} + +#[derive(Debug)] +struct AutocloseRegion { + selection_id: usize, + range: Range, + pair: BracketPair, +} + +#[derive(Debug)] +struct SnippetState { + ranges: Vec>>, + active_index: usize, + choices: Vec>>, +} + +#[doc(hidden)] +pub struct RenameState { + pub range: Range, + pub old_name: Arc, + pub editor: Entity, + block_id: CustomBlockId, +} + +struct InvalidationStack(Vec); + +struct RegisteredInlineCompletionProvider { + provider: Arc, + _subscription: Subscription, +} + +#[derive(Debug, PartialEq, Eq)] +pub struct ActiveDiagnosticGroup { + pub active_range: Range, + pub active_message: String, + pub group_id: usize, + pub blocks: HashSet, +} + +#[derive(Debug, PartialEq, Eq)] +#[allow(clippy::large_enum_variant)] +pub(crate) enum ActiveDiagnostic { + None, + All, + Group(ActiveDiagnosticGroup), +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct ClipboardSelection { + /// The number of bytes in this selection. + pub len: usize, + /// Whether this was a full-line selection. + pub is_entire_line: bool, + /// The indentation of the first line when this content was originally copied. + pub first_line_indent: u32, +} + +// selections, scroll behavior, was newest selection reversed +type SelectSyntaxNodeHistoryState = ( + Box<[Selection]>, + SelectSyntaxNodeScrollBehavior, + bool, +); + +#[derive(Default)] +struct SelectSyntaxNodeHistory { + stack: Vec, + // disable temporarily to allow changing selections without losing the stack + pub disable_clearing: bool, +} + +impl SelectSyntaxNodeHistory { + pub fn try_clear(&mut self) { + if !self.disable_clearing { + self.stack.clear(); + } + } + + pub fn push(&mut self, selection: SelectSyntaxNodeHistoryState) { + self.stack.push(selection); + } + + pub fn pop(&mut self) -> Option { + self.stack.pop() + } +} + +enum SelectSyntaxNodeScrollBehavior { + CursorTop, + FitSelection, + CursorBottom, +} + +#[derive(Debug)] +pub(crate) struct NavigationData { + cursor_anchor: Anchor, + cursor_position: Point, + scroll_anchor: ScrollAnchor, + scroll_top_row: u32, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum GotoDefinitionKind { + Symbol, + Declaration, + Type, + Implementation, +} + +#[derive(Debug, Clone)] +enum InlayHintRefreshReason { + ModifiersChanged(bool), + Toggle(bool), + SettingsChange(InlayHintSettings), + NewLinesShown, + BufferEdited(HashSet>), + RefreshRequested, + ExcerptsRemoved(Vec), +} + +impl InlayHintRefreshReason { + fn description(&self) -> &'static str { + match self { + Self::ModifiersChanged(_) => "modifiers changed", + Self::Toggle(_) => "toggle", + Self::SettingsChange(_) => "settings change", + Self::NewLinesShown => "new lines shown", + Self::BufferEdited(_) => "buffer edited", + Self::RefreshRequested => "refresh requested", + Self::ExcerptsRemoved(_) => "excerpts removed", + } + } +} + +pub enum FormatTarget { + Buffers, + Ranges(Vec>), +} + +pub(crate) struct FocusedBlock { + id: BlockId, + focus_handle: WeakFocusHandle, +} + +#[derive(Clone)] +enum JumpData { + MultiBufferRow { + row: MultiBufferRow, + line_offset_from_top: u32, + }, + MultiBufferPoint { + excerpt_id: ExcerptId, + position: Point, + anchor: text::Anchor, + line_offset_from_top: u32, + }, +} + +pub enum MultibufferSelectionMode { + First, + All, +} + +#[derive(Clone, Copy, Debug, Default)] +pub struct RewrapOptions { + pub override_language_settings: bool, + pub preserve_existing_whitespace: bool, +} + +impl Editor { + pub fn single_line(window: &mut Window, cx: &mut Context) -> Self { + let buffer = cx.new(|cx| Buffer::local("", cx)); + let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); + Self::new( + EditorMode::SingleLine { auto_width: false }, + buffer, + None, + window, + cx, + ) + } + + pub fn multi_line(window: &mut Window, cx: &mut Context) -> Self { + let buffer = cx.new(|cx| Buffer::local("", cx)); + let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); + Self::new(EditorMode::full(), buffer, None, window, cx) + } + + pub fn auto_width(window: &mut Window, cx: &mut Context) -> Self { + let buffer = cx.new(|cx| Buffer::local("", cx)); + let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); + Self::new( + EditorMode::SingleLine { auto_width: true }, + buffer, + None, + window, + cx, + ) + } + + pub fn auto_height(max_lines: usize, window: &mut Window, cx: &mut Context) -> Self { + let buffer = cx.new(|cx| Buffer::local("", cx)); + let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); + Self::new( + EditorMode::AutoHeight { max_lines }, + buffer, + None, + window, + cx, + ) + } + + pub fn for_buffer( + buffer: Entity, + project: Option>, + window: &mut Window, + cx: &mut Context, + ) -> Self { + let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); + Self::new(EditorMode::full(), buffer, project, window, cx) + } + + pub fn for_multibuffer( + buffer: Entity, + project: Option>, + window: &mut Window, + cx: &mut Context, + ) -> Self { + Self::new(EditorMode::full(), buffer, project, window, cx) + } + + pub fn clone(&self, window: &mut Window, cx: &mut Context) -> Self { + let mut clone = Self::new( + self.mode, + self.buffer.clone(), + self.project.clone(), + window, + cx, + ); + self.display_map.update(cx, |display_map, cx| { + let snapshot = display_map.snapshot(cx); + clone.display_map.update(cx, |display_map, cx| { + display_map.set_state(&snapshot, cx); + }); + }); + clone.folds_did_change(cx); + clone.selections.clone_state(&self.selections); + clone.scroll_manager.clone_state(&self.scroll_manager); + clone.searchable = self.searchable; + clone.read_only = self.read_only; + clone + } + + pub fn new( + mode: EditorMode, + buffer: Entity, + project: Option>, + window: &mut Window, + cx: &mut Context, + ) -> Self { + let style = window.text_style(); + let font_size = style.font_size.to_pixels(window.rem_size()); + let editor = cx.entity().downgrade(); + let fold_placeholder = FoldPlaceholder { + constrain_width: true, + render: Arc::new(move |fold_id, fold_range, cx| { + let editor = editor.clone(); + div() + .id(fold_id) + .bg(cx.theme().colors().ghost_element_background) + .hover(|style| style.bg(cx.theme().colors().ghost_element_hover)) + .active(|style| style.bg(cx.theme().colors().ghost_element_active)) + .rounded_xs() + .size_full() + .cursor_pointer() + .child("⋯") + .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation()) + .on_click(move |_, _window, cx| { + editor + .update(cx, |editor, cx| { + editor.unfold_ranges( + &[fold_range.start..fold_range.end], + true, + false, + cx, + ); + cx.stop_propagation(); + }) + .ok(); + }) + .into_any() + }), + merge_adjacent: true, + ..Default::default() + }; + let display_map = cx.new(|cx| { + DisplayMap::new( + buffer.clone(), + style.font(), + font_size, + None, + FILE_HEADER_HEIGHT, + MULTI_BUFFER_EXCERPT_HEADER_HEIGHT, + fold_placeholder, + cx, + ) + }); + + let selections = SelectionsCollection::new(display_map.clone(), buffer.clone()); + + let blink_manager = cx.new(|cx| BlinkManager::new(CURSOR_BLINK_INTERVAL, cx)); + + let soft_wrap_mode_override = matches!(mode, EditorMode::SingleLine { .. }) + .then(|| language_settings::SoftWrap::None); + + let mut project_subscriptions = Vec::new(); + if mode.is_full() { + if let Some(project) = project.as_ref() { + project_subscriptions.push(cx.subscribe_in( + project, + window, + |editor, _, event, window, cx| match event { + project::Event::RefreshCodeLens => { + // we always query lens with actions, without storing them, always refreshing them + } + project::Event::RefreshInlayHints => { + editor + .refresh_inlay_hints(InlayHintRefreshReason::RefreshRequested, cx); + } + project::Event::SnippetEdit(id, snippet_edits) => { + if let Some(buffer) = editor.buffer.read(cx).buffer(*id) { + let focus_handle = editor.focus_handle(cx); + if focus_handle.is_focused(window) { + let snapshot = buffer.read(cx).snapshot(); + for (range, snippet) in snippet_edits { + let editor_range = + language::range_from_lsp(*range).to_offset(&snapshot); + editor + .insert_snippet( + &[editor_range], + snippet.clone(), + window, + cx, + ) + .ok(); + } + } + } + } + _ => {} + }, + )); + if let Some(task_inventory) = project + .read(cx) + .task_store() + .read(cx) + .task_inventory() + .cloned() + { + project_subscriptions.push(cx.observe_in( + &task_inventory, + window, + |editor, _, window, cx| { + editor.tasks_update_task = Some(editor.refresh_runnables(window, cx)); + }, + )); + }; + + project_subscriptions.push(cx.subscribe_in( + &project.read(cx).breakpoint_store(), + window, + |editor, _, event, window, cx| match event { + BreakpointStoreEvent::ClearDebugLines => { + editor.clear_row_highlights::(); + editor.refresh_inline_values(cx); + } + BreakpointStoreEvent::SetDebugLine => { + if editor.go_to_active_debug_line(window, cx) { + cx.stop_propagation(); + } + + editor.refresh_inline_values(cx); + } + _ => {} + }, + )); + } + } + + let buffer_snapshot = buffer.read(cx).snapshot(cx); + + let inlay_hint_settings = + inlay_hint_settings(selections.newest_anchor().head(), &buffer_snapshot, cx); + let focus_handle = cx.focus_handle(); + cx.on_focus(&focus_handle, window, Self::handle_focus) + .detach(); + cx.on_focus_in(&focus_handle, window, Self::handle_focus_in) + .detach(); + cx.on_focus_out(&focus_handle, window, Self::handle_focus_out) + .detach(); + cx.on_blur(&focus_handle, window, Self::handle_blur) + .detach(); + + let show_indent_guides = if matches!(mode, EditorMode::SingleLine { .. }) { + Some(false) + } else { + None + }; + + let breakpoint_store = match (mode, project.as_ref()) { + (EditorMode::Full { .. }, Some(project)) => Some(project.read(cx).breakpoint_store()), + _ => None, + }; + + let mut code_action_providers = Vec::new(); + let mut load_uncommitted_diff = None; + if let Some(project) = project.clone() { + load_uncommitted_diff = Some( + get_uncommitted_diff_for_buffer( + &project, + buffer.read(cx).all_buffers(), + buffer.clone(), + cx, + ) + .shared(), + ); + code_action_providers.push(Rc::new(project) as Rc<_>); + } + + let mut this = Self { + focus_handle, + show_cursor_when_unfocused: false, + last_focused_descendant: None, + buffer: buffer.clone(), + display_map: display_map.clone(), + selections, + scroll_manager: ScrollManager::new(cx), + columnar_selection_tail: None, + add_selections_state: None, + select_next_state: None, + select_prev_state: None, + selection_history: Default::default(), + autoclose_regions: Default::default(), + snippet_stack: Default::default(), + select_syntax_node_history: SelectSyntaxNodeHistory::default(), + ime_transaction: Default::default(), + active_diagnostics: ActiveDiagnostic::None, + show_inline_diagnostics: ProjectSettings::get_global(cx).diagnostics.inline.enabled, + inline_diagnostics_update: Task::ready(()), + inline_diagnostics: Vec::new(), + soft_wrap_mode_override, + hard_wrap: None, + completion_provider: project.clone().map(|project| Box::new(project) as _), + semantics_provider: project.clone().map(|project| Rc::new(project) as _), + collaboration_hub: project.clone().map(|project| Box::new(project) as _), + project, + blink_manager: blink_manager.clone(), + show_local_selections: true, + show_scrollbars: true, + disable_scrolling: false, + mode, + show_breadcrumbs: EditorSettings::get_global(cx).toolbar.breadcrumbs, + show_gutter: mode.is_full(), + show_line_numbers: None, + use_relative_line_numbers: None, + disable_expand_excerpt_buttons: false, + show_git_diff_gutter: None, + show_code_actions: None, + show_runnables: None, + show_breakpoints: None, + show_wrap_guides: None, + show_indent_guides, + placeholder_text: None, + highlight_order: 0, + highlighted_rows: HashMap::default(), + background_highlights: Default::default(), + gutter_highlights: TreeMap::default(), + scrollbar_marker_state: ScrollbarMarkerState::default(), + active_indent_guides_state: ActiveIndentGuidesState::default(), + nav_history: None, + context_menu: RefCell::new(None), + context_menu_options: None, + mouse_context_menu: None, + completion_tasks: Default::default(), + inline_blame_popover: Default::default(), + signature_help_state: SignatureHelpState::default(), + auto_signature_help: None, + find_all_references_task_sources: Vec::new(), + next_completion_id: 0, + next_inlay_id: 0, + code_action_providers, + available_code_actions: Default::default(), + code_actions_task: Default::default(), + quick_selection_highlight_task: Default::default(), + debounced_selection_highlight_task: Default::default(), + document_highlights_task: Default::default(), + linked_editing_range_task: Default::default(), + pending_rename: Default::default(), + searchable: true, + cursor_shape: EditorSettings::get_global(cx) + .cursor_shape + .unwrap_or_default(), + current_line_highlight: None, + autoindent_mode: Some(AutoindentMode::EachLine), + collapse_matches: false, + workspace: None, + input_enabled: true, + use_modal_editing: mode.is_full(), + read_only: false, + use_autoclose: true, + use_auto_surround: true, + auto_replace_emoji_shortcode: false, + jsx_tag_auto_close_enabled_in_any_buffer: false, + leader_peer_id: None, + remote_id: None, + hover_state: Default::default(), + pending_mouse_down: None, + hovered_link_state: Default::default(), + edit_prediction_provider: None, + active_inline_completion: None, + stale_inline_completion_in_menu: None, + edit_prediction_preview: EditPredictionPreview::Inactive { + released_too_fast: false, + }, + inline_diagnostics_enabled: mode.is_full(), + inline_value_cache: InlineValueCache::new(inlay_hint_settings.show_value_hints), + inlay_hint_cache: InlayHintCache::new(inlay_hint_settings), + + gutter_hovered: false, + pixel_position_of_newest_cursor: None, + last_bounds: None, + last_position_map: None, + expect_bounds_change: None, + gutter_dimensions: GutterDimensions::default(), + style: None, + show_cursor_names: false, + hovered_cursors: Default::default(), + next_editor_action_id: EditorActionId::default(), + editor_actions: Rc::default(), + inline_completions_hidden_for_vim_mode: false, + show_inline_completions_override: None, + menu_inline_completions_policy: MenuInlineCompletionsPolicy::ByProvider, + edit_prediction_settings: EditPredictionSettings::Disabled, + edit_prediction_indent_conflict: false, + edit_prediction_requires_modifier_in_indent_conflict: true, + custom_context_menu: None, + show_git_blame_gutter: false, + show_git_blame_inline: false, + show_selection_menu: None, + show_git_blame_inline_delay_task: None, + git_blame_inline_enabled: ProjectSettings::get_global(cx).git.inline_blame_enabled(), + render_diff_hunk_controls: Arc::new(render_diff_hunk_controls), + serialize_dirty_buffers: ProjectSettings::get_global(cx) + .session + .restore_unsaved_buffers, + blame: None, + blame_subscription: None, + tasks: Default::default(), + + breakpoint_store, + gutter_breakpoint_indicator: (None, None), + _subscriptions: vec![ + cx.observe(&buffer, Self::on_buffer_changed), + cx.subscribe_in(&buffer, window, Self::on_buffer_event), + cx.observe_in(&display_map, window, Self::on_display_map_changed), + cx.observe(&blink_manager, |_, _, cx| cx.notify()), + cx.observe_global_in::(window, Self::settings_changed), + observe_buffer_font_size_adjustment(cx, |_, cx| cx.notify()), + cx.observe_window_activation(window, |editor, window, cx| { + let active = window.is_window_active(); + editor.blink_manager.update(cx, |blink_manager, cx| { + if active { + blink_manager.enable(cx); + } else { + blink_manager.disable(cx); + } + }); + }), + ], + tasks_update_task: None, + linked_edit_ranges: Default::default(), + in_project_search: false, + previous_search_ranges: None, + breadcrumb_header: None, + focused_block: None, + next_scroll_position: NextScrollCursorCenterTopBottom::default(), + addons: HashMap::default(), + registered_buffers: HashMap::default(), + _scroll_cursor_center_top_bottom_task: Task::ready(()), + selection_mark_mode: false, + toggle_fold_multiple_buffers: Task::ready(()), + serialize_selections: Task::ready(()), + serialize_folds: Task::ready(()), + text_style_refinement: None, + load_diff_task: load_uncommitted_diff, + mouse_cursor_hidden: false, + hide_mouse_mode: EditorSettings::get_global(cx) + .hide_mouse + .unwrap_or_default(), + change_list: ChangeList::new(), + }; + if let Some(breakpoints) = this.breakpoint_store.as_ref() { + this._subscriptions + .push(cx.observe(breakpoints, |_, _, cx| { + cx.notify(); + })); + } + this.tasks_update_task = Some(this.refresh_runnables(window, cx)); + this._subscriptions.extend(project_subscriptions); + + this._subscriptions.push(cx.subscribe_in( + &cx.entity(), + window, + |editor, _, e: &EditorEvent, window, cx| match e { + EditorEvent::ScrollPositionChanged { local, .. } => { + if *local { + let new_anchor = editor.scroll_manager.anchor(); + let snapshot = editor.snapshot(window, cx); + editor.update_restoration_data(cx, move |data| { + data.scroll_position = ( + new_anchor.top_row(&snapshot.buffer_snapshot), + new_anchor.offset, + ); + }); + editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape); + editor.inline_blame_popover.take(); + } + } + EditorEvent::Edited { .. } => { + if !vim_enabled(cx) { + let (map, selections) = editor.selections.all_adjusted_display(cx); + let pop_state = editor + .change_list + .last() + .map(|previous| { + previous.len() == selections.len() + && previous.iter().enumerate().all(|(ix, p)| { + p.to_display_point(&map).row() + == selections[ix].head().row() + }) + }) + .unwrap_or(false); + let new_positions = selections + .into_iter() + .map(|s| map.display_point_to_anchor(s.head(), Bias::Left)) + .collect(); + editor + .change_list + .push_to_change_list(pop_state, new_positions); + } + } + _ => (), + }, + )); + + if let Some(dap_store) = this + .project + .as_ref() + .map(|project| project.read(cx).dap_store()) + { + let weak_editor = cx.weak_entity(); + + this._subscriptions + .push( + cx.observe_new::(move |_, _, cx| { + let session_entity = cx.entity(); + weak_editor + .update(cx, |editor, cx| { + editor._subscriptions.push( + cx.subscribe(&session_entity, Self::on_debug_session_event), + ); + }) + .ok(); + }), + ); + + for session in dap_store.read(cx).sessions().cloned().collect::>() { + this._subscriptions + .push(cx.subscribe(&session, Self::on_debug_session_event)); + } + } + + this.end_selection(window, cx); + this.scroll_manager.show_scrollbars(window, cx); + jsx_tag_auto_close::refresh_enabled_in_any_buffer(&mut this, &buffer, cx); + + if mode.is_full() { + let should_auto_hide_scrollbars = cx.should_auto_hide_scrollbars(); + cx.set_global(ScrollbarAutoHide(should_auto_hide_scrollbars)); + + if this.git_blame_inline_enabled { + this.git_blame_inline_enabled = true; + this.start_git_blame_inline(false, window, cx); + } + + this.go_to_active_debug_line(window, cx); + + if let Some(buffer) = buffer.read(cx).as_singleton() { + if let Some(project) = this.project.as_ref() { + let handle = project.update(cx, |project, cx| { + project.register_buffer_with_language_servers(&buffer, cx) + }); + this.registered_buffers + .insert(buffer.read(cx).remote_id(), handle); + } + } + } + + this.report_editor_event("Editor Opened", None, cx); + this + } + + pub fn deploy_mouse_context_menu( + &mut self, + position: gpui::Point, + context_menu: Entity, + window: &mut Window, + cx: &mut Context, + ) { + self.mouse_context_menu = Some(MouseContextMenu::new( + self, + crate::mouse_context_menu::MenuPosition::PinnedToScreen(position), + context_menu, + window, + cx, + )); + } + + pub fn mouse_menu_is_focused(&self, window: &Window, cx: &App) -> bool { + self.mouse_context_menu + .as_ref() + .is_some_and(|menu| menu.context_menu.focus_handle(cx).is_focused(window)) + } + + fn key_context(&self, window: &Window, cx: &App) -> KeyContext { + self.key_context_internal(self.has_active_inline_completion(), window, cx) + } + + fn key_context_internal( + &self, + has_active_edit_prediction: bool, + window: &Window, + cx: &App, + ) -> KeyContext { + let mut key_context = KeyContext::new_with_defaults(); + key_context.add("Editor"); + let mode = match self.mode { + EditorMode::SingleLine { .. } => "single_line", + EditorMode::AutoHeight { .. } => "auto_height", + EditorMode::Full { .. } => "full", + }; + + if EditorSettings::jupyter_enabled(cx) { + key_context.add("jupyter"); + } + + key_context.set("mode", mode); + if self.pending_rename.is_some() { + key_context.add("renaming"); + } + + match self.context_menu.borrow().as_ref() { + Some(CodeContextMenu::Completions(_)) => { + key_context.add("menu"); + key_context.add("showing_completions"); + } + Some(CodeContextMenu::CodeActions(_)) => { + key_context.add("menu"); + key_context.add("showing_code_actions") + } + None => {} + } + + // Disable vim contexts when a sub-editor (e.g. rename/inline assistant) is focused. + if !self.focus_handle(cx).contains_focused(window, cx) + || (self.is_focused(window) || self.mouse_menu_is_focused(window, cx)) + { + for addon in self.addons.values() { + addon.extend_key_context(&mut key_context, cx) + } + } + + if let Some(singleton_buffer) = self.buffer.read(cx).as_singleton() { + if let Some(extension) = singleton_buffer + .read(cx) + .file() + .and_then(|file| file.path().extension()?.to_str()) + { + key_context.set("extension", extension.to_string()); + } + } else { + key_context.add("multibuffer"); + } + + if has_active_edit_prediction { + if self.edit_prediction_in_conflict() { + key_context.add(EDIT_PREDICTION_CONFLICT_KEY_CONTEXT); + } else { + key_context.add(EDIT_PREDICTION_KEY_CONTEXT); + key_context.add("copilot_suggestion"); + } + } + + if self.selection_mark_mode { + key_context.add("selection_mode"); + } + + key_context + } + + pub fn hide_mouse_cursor(&mut self, origin: &HideMouseCursorOrigin) { + self.mouse_cursor_hidden = match origin { + HideMouseCursorOrigin::TypingAction => { + matches!( + self.hide_mouse_mode, + HideMouseMode::OnTyping | HideMouseMode::OnTypingAndMovement + ) + } + HideMouseCursorOrigin::MovementAction => { + matches!(self.hide_mouse_mode, HideMouseMode::OnTypingAndMovement) + } + }; + } + + pub fn edit_prediction_in_conflict(&self) -> bool { + if !self.show_edit_predictions_in_menu() { + return false; + } + + let showing_completions = self + .context_menu + .borrow() + .as_ref() + .map_or(false, |context| { + matches!(context, CodeContextMenu::Completions(_)) + }); + + showing_completions + || self.edit_prediction_requires_modifier() + // Require modifier key when the cursor is on leading whitespace, to allow `tab` + // bindings to insert tab characters. + || (self.edit_prediction_requires_modifier_in_indent_conflict && self.edit_prediction_indent_conflict) + } + + pub fn accept_edit_prediction_keybind( + &self, + window: &Window, + cx: &App, + ) -> AcceptEditPredictionBinding { + let key_context = self.key_context_internal(true, window, cx); + let in_conflict = self.edit_prediction_in_conflict(); + + AcceptEditPredictionBinding( + window + .bindings_for_action_in_context(&AcceptEditPrediction, key_context) + .into_iter() + .filter(|binding| { + !in_conflict + || binding + .keystrokes() + .first() + .map_or(false, |keystroke| keystroke.modifiers.modified()) + }) + .rev() + .min_by_key(|binding| { + binding + .keystrokes() + .first() + .map_or(u8::MAX, |k| k.modifiers.number_of_modifiers()) + }), + ) + } + + pub fn new_file( + workspace: &mut Workspace, + _: &workspace::NewFile, + window: &mut Window, + cx: &mut Context, + ) { + Self::new_in_workspace(workspace, window, cx).detach_and_prompt_err( + "Failed to create buffer", + window, + cx, + |e, _, _| match e.error_code() { + ErrorCode::RemoteUpgradeRequired => Some(format!( + "The remote instance of Zed does not support this yet. It must be upgraded to {}", + e.error_tag("required").unwrap_or("the latest version") + )), + _ => None, + }, + ); + } + + pub fn new_in_workspace( + workspace: &mut Workspace, + window: &mut Window, + cx: &mut Context, + ) -> Task>> { + let project = workspace.project().clone(); + let create = project.update(cx, |project, cx| project.create_buffer(cx)); + + cx.spawn_in(window, async move |workspace, cx| { + let buffer = create.await?; + workspace.update_in(cx, |workspace, window, cx| { + let editor = + cx.new(|cx| Editor::for_buffer(buffer, Some(project.clone()), window, cx)); + workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx); + editor + }) + }) + } + + fn new_file_vertical( + workspace: &mut Workspace, + _: &workspace::NewFileSplitVertical, + window: &mut Window, + cx: &mut Context, + ) { + Self::new_file_in_direction(workspace, SplitDirection::vertical(cx), window, cx) + } + + fn new_file_horizontal( + workspace: &mut Workspace, + _: &workspace::NewFileSplitHorizontal, + window: &mut Window, + cx: &mut Context, + ) { + Self::new_file_in_direction(workspace, SplitDirection::horizontal(cx), window, cx) + } + + fn new_file_in_direction( + workspace: &mut Workspace, + direction: SplitDirection, + window: &mut Window, + cx: &mut Context, + ) { + let project = workspace.project().clone(); + let create = project.update(cx, |project, cx| project.create_buffer(cx)); + + cx.spawn_in(window, async move |workspace, cx| { + let buffer = create.await?; + workspace.update_in(cx, move |workspace, window, cx| { + workspace.split_item( + direction, + Box::new( + cx.new(|cx| Editor::for_buffer(buffer, Some(project.clone()), window, cx)), + ), + window, + cx, + ) + })?; + anyhow::Ok(()) + }) + .detach_and_prompt_err("Failed to create buffer", window, cx, |e, _, _| { + match e.error_code() { + ErrorCode::RemoteUpgradeRequired => Some(format!( + "The remote instance of Zed does not support this yet. It must be upgraded to {}", + e.error_tag("required").unwrap_or("the latest version") + )), + _ => None, + } + }); + } + + pub fn leader_peer_id(&self) -> Option { + self.leader_peer_id + } + + pub fn buffer(&self) -> &Entity { + &self.buffer + } + + pub fn workspace(&self) -> Option> { + self.workspace.as_ref()?.0.upgrade() + } + + pub fn title<'a>(&self, cx: &'a App) -> Cow<'a, str> { + self.buffer().read(cx).title(cx) + } + + pub fn snapshot(&self, window: &mut Window, cx: &mut App) -> EditorSnapshot { + let git_blame_gutter_max_author_length = self + .render_git_blame_gutter(cx) + .then(|| { + if let Some(blame) = self.blame.as_ref() { + let max_author_length = + blame.update(cx, |blame, cx| blame.max_author_length(cx)); + Some(max_author_length) + } else { + None + } + }) + .flatten(); + + EditorSnapshot { + mode: self.mode, + show_gutter: self.show_gutter, + show_line_numbers: self.show_line_numbers, + show_git_diff_gutter: self.show_git_diff_gutter, + show_code_actions: self.show_code_actions, + show_runnables: self.show_runnables, + show_breakpoints: self.show_breakpoints, + git_blame_gutter_max_author_length, + display_snapshot: self.display_map.update(cx, |map, cx| map.snapshot(cx)), + scroll_anchor: self.scroll_manager.anchor(), + ongoing_scroll: self.scroll_manager.ongoing_scroll(), + placeholder_text: self.placeholder_text.clone(), + is_focused: self.focus_handle.is_focused(window), + current_line_highlight: self + .current_line_highlight + .unwrap_or_else(|| EditorSettings::get_global(cx).current_line_highlight), + gutter_hovered: self.gutter_hovered, + } + } + + pub fn language_at(&self, point: T, cx: &App) -> Option> { + self.buffer.read(cx).language_at(point, cx) + } + + pub fn file_at(&self, point: T, cx: &App) -> Option> { + self.buffer.read(cx).read(cx).file_at(point).cloned() + } + + pub fn active_excerpt( + &self, + cx: &App, + ) -> Option<(ExcerptId, Entity, Range)> { + self.buffer + .read(cx) + .excerpt_containing(self.selections.newest_anchor().head(), cx) + } + + pub fn mode(&self) -> EditorMode { + self.mode + } + + pub fn set_mode(&mut self, mode: EditorMode) { + self.mode = mode; + } + + pub fn collaboration_hub(&self) -> Option<&dyn CollaborationHub> { + self.collaboration_hub.as_deref() + } + + pub fn set_collaboration_hub(&mut self, hub: Box) { + self.collaboration_hub = Some(hub); + } + + pub fn set_in_project_search(&mut self, in_project_search: bool) { + self.in_project_search = in_project_search; + } + + pub fn set_custom_context_menu( + &mut self, + f: impl 'static + + Fn( + &mut Self, + DisplayPoint, + &mut Window, + &mut Context, + ) -> Option>, + ) { + self.custom_context_menu = Some(Box::new(f)) + } + + pub fn set_completion_provider(&mut self, provider: Option>) { + self.completion_provider = provider; + } + + pub fn semantics_provider(&self) -> Option> { + self.semantics_provider.clone() + } + + pub fn set_semantics_provider(&mut self, provider: Option>) { + self.semantics_provider = provider; + } + + pub fn set_edit_prediction_provider( + &mut self, + provider: Option>, + window: &mut Window, + cx: &mut Context, + ) where + T: EditPredictionProvider, + { + self.edit_prediction_provider = + provider.map(|provider| RegisteredInlineCompletionProvider { + _subscription: cx.observe_in(&provider, window, |this, _, window, cx| { + if this.focus_handle.is_focused(window) { + this.update_visible_inline_completion(window, cx); + } + }), + provider: Arc::new(provider), + }); + self.update_edit_prediction_settings(cx); + self.refresh_inline_completion(false, false, window, cx); + } + + pub fn placeholder_text(&self) -> Option<&str> { + self.placeholder_text.as_deref() + } + + pub fn set_placeholder_text( + &mut self, + placeholder_text: impl Into>, + cx: &mut Context, + ) { + let placeholder_text = Some(placeholder_text.into()); + if self.placeholder_text != placeholder_text { + self.placeholder_text = placeholder_text; + cx.notify(); + } + } + + pub fn set_cursor_shape(&mut self, cursor_shape: CursorShape, cx: &mut Context) { + self.cursor_shape = cursor_shape; + + // Disrupt blink for immediate user feedback that the cursor shape has changed + self.blink_manager.update(cx, BlinkManager::show_cursor); + + cx.notify(); + } + + pub fn set_current_line_highlight( + &mut self, + current_line_highlight: Option, + ) { + self.current_line_highlight = current_line_highlight; + } + + pub fn set_collapse_matches(&mut self, collapse_matches: bool) { + self.collapse_matches = collapse_matches; + } + + fn register_buffers_with_language_servers(&mut self, cx: &mut Context) { + let buffers = self.buffer.read(cx).all_buffers(); + let Some(project) = self.project.as_ref() else { + return; + }; + project.update(cx, |project, cx| { + for buffer in buffers { + self.registered_buffers + .entry(buffer.read(cx).remote_id()) + .or_insert_with(|| project.register_buffer_with_language_servers(&buffer, cx)); + } + }) + } + + pub fn range_for_match(&self, range: &Range) -> Range { + if self.collapse_matches { + return range.start..range.start; + } + range.clone() + } + + pub fn set_clip_at_line_ends(&mut self, clip: bool, cx: &mut Context) { + if self.display_map.read(cx).clip_at_line_ends != clip { + self.display_map + .update(cx, |map, _| map.clip_at_line_ends = clip); + } + } + + pub fn set_input_enabled(&mut self, input_enabled: bool) { + self.input_enabled = input_enabled; + } + + pub fn set_inline_completions_hidden_for_vim_mode( + &mut self, + hidden: bool, + window: &mut Window, + cx: &mut Context, + ) { + if hidden != self.inline_completions_hidden_for_vim_mode { + self.inline_completions_hidden_for_vim_mode = hidden; + if hidden { + self.update_visible_inline_completion(window, cx); + } else { + self.refresh_inline_completion(true, false, window, cx); + } + } + } + + pub fn set_menu_inline_completions_policy(&mut self, value: MenuInlineCompletionsPolicy) { + self.menu_inline_completions_policy = value; + } + + pub fn set_autoindent(&mut self, autoindent: bool) { + if autoindent { + self.autoindent_mode = Some(AutoindentMode::EachLine); + } else { + self.autoindent_mode = None; + } + } + + pub fn read_only(&self, cx: &App) -> bool { + self.read_only || self.buffer.read(cx).read_only() + } + + pub fn set_read_only(&mut self, read_only: bool) { + self.read_only = read_only; + } + + pub fn set_use_autoclose(&mut self, autoclose: bool) { + self.use_autoclose = autoclose; + } + + pub fn set_use_auto_surround(&mut self, auto_surround: bool) { + self.use_auto_surround = auto_surround; + } + + pub fn set_auto_replace_emoji_shortcode(&mut self, auto_replace: bool) { + self.auto_replace_emoji_shortcode = auto_replace; + } + + pub fn toggle_edit_predictions( + &mut self, + _: &ToggleEditPrediction, + window: &mut Window, + cx: &mut Context, + ) { + if self.show_inline_completions_override.is_some() { + self.set_show_edit_predictions(None, window, cx); + } else { + let show_edit_predictions = !self.edit_predictions_enabled(); + self.set_show_edit_predictions(Some(show_edit_predictions), window, cx); + } + } + + pub fn set_show_edit_predictions( + &mut self, + show_edit_predictions: Option, + window: &mut Window, + cx: &mut Context, + ) { + self.show_inline_completions_override = show_edit_predictions; + self.update_edit_prediction_settings(cx); + + if let Some(false) = show_edit_predictions { + self.discard_inline_completion(false, cx); + } else { + self.refresh_inline_completion(false, true, window, cx); + } + } + + fn inline_completions_disabled_in_scope( + &self, + buffer: &Entity, + buffer_position: language::Anchor, + cx: &App, + ) -> bool { + let snapshot = buffer.read(cx).snapshot(); + let settings = snapshot.settings_at(buffer_position, cx); + + let Some(scope) = snapshot.language_scope_at(buffer_position) else { + return false; + }; + + scope.override_name().map_or(false, |scope_name| { + settings + .edit_predictions_disabled_in + .iter() + .any(|s| s == scope_name) + }) + } + + pub fn set_use_modal_editing(&mut self, to: bool) { + self.use_modal_editing = to; + } + + pub fn use_modal_editing(&self) -> bool { + self.use_modal_editing + } + + fn selections_did_change( + &mut self, + local: bool, + old_cursor_position: &Anchor, + show_completions: bool, + window: &mut Window, + cx: &mut Context, + ) { + window.invalidate_character_coordinates(); + + // Copy selections to primary selection buffer + #[cfg(any(target_os = "linux", target_os = "freebsd"))] + if local { + let selections = self.selections.all::(cx); + let buffer_handle = self.buffer.read(cx).read(cx); + + let mut text = String::new(); + for (index, selection) in selections.iter().enumerate() { + let text_for_selection = buffer_handle + .text_for_range(selection.start..selection.end) + .collect::(); + + text.push_str(&text_for_selection); + if index != selections.len() - 1 { + text.push('\n'); + } + } + + if !text.is_empty() { + cx.write_to_primary(ClipboardItem::new_string(text)); + } + } + + if self.focus_handle.is_focused(window) && self.leader_peer_id.is_none() { + self.buffer.update(cx, |buffer, cx| { + buffer.set_active_selections( + &self.selections.disjoint_anchors(), + self.selections.line_mode, + self.cursor_shape, + cx, + ) + }); + } + let display_map = self + .display_map + .update(cx, |display_map, cx| display_map.snapshot(cx)); + let buffer = &display_map.buffer_snapshot; + self.add_selections_state = None; + self.select_next_state = None; + self.select_prev_state = None; + self.select_syntax_node_history.try_clear(); + self.invalidate_autoclose_regions(&self.selections.disjoint_anchors(), buffer); + self.snippet_stack + .invalidate(&self.selections.disjoint_anchors(), buffer); + self.take_rename(false, window, cx); + + let new_cursor_position = self.selections.newest_anchor().head(); + + self.push_to_nav_history( + *old_cursor_position, + Some(new_cursor_position.to_point(buffer)), + false, + cx, + ); + + if local { + let new_cursor_position = self.selections.newest_anchor().head(); + let mut context_menu = self.context_menu.borrow_mut(); + let completion_menu = match context_menu.as_ref() { + Some(CodeContextMenu::Completions(menu)) => Some(menu), + _ => { + *context_menu = None; + None + } + }; + if let Some(buffer_id) = new_cursor_position.buffer_id { + if !self.registered_buffers.contains_key(&buffer_id) { + if let Some(project) = self.project.as_ref() { + project.update(cx, |project, cx| { + let Some(buffer) = self.buffer.read(cx).buffer(buffer_id) else { + return; + }; + self.registered_buffers.insert( + buffer_id, + project.register_buffer_with_language_servers(&buffer, cx), + ); + }) + } + } + } + + if let Some(completion_menu) = completion_menu { + let cursor_position = new_cursor_position.to_offset(buffer); + let (word_range, kind) = + buffer.surrounding_word(completion_menu.initial_position, true); + if kind == Some(CharKind::Word) + && word_range.to_inclusive().contains(&cursor_position) + { + let mut completion_menu = completion_menu.clone(); + drop(context_menu); + + let query = Self::completion_query(buffer, cursor_position); + cx.spawn(async move |this, cx| { + completion_menu + .filter(query.as_deref(), cx.background_executor().clone()) + .await; + + this.update(cx, |this, cx| { + let mut context_menu = this.context_menu.borrow_mut(); + let Some(CodeContextMenu::Completions(menu)) = context_menu.as_ref() + else { + return; + }; + + if menu.id > completion_menu.id { + return; + } + + *context_menu = Some(CodeContextMenu::Completions(completion_menu)); + drop(context_menu); + cx.notify(); + }) + }) + .detach(); + + if show_completions { + self.show_completions(&ShowCompletions { trigger: None }, window, cx); + } + } else { + drop(context_menu); + self.hide_context_menu(window, cx); + } + } else { + drop(context_menu); + } + + hide_hover(self, cx); + + if old_cursor_position.to_display_point(&display_map).row() + != new_cursor_position.to_display_point(&display_map).row() + { + self.available_code_actions.take(); + } + self.refresh_code_actions(window, cx); + self.refresh_document_highlights(cx); + self.refresh_selected_text_highlights(false, window, cx); + refresh_matching_bracket_highlights(self, window, cx); + self.update_visible_inline_completion(window, cx); + self.edit_prediction_requires_modifier_in_indent_conflict = true; + linked_editing_ranges::refresh_linked_ranges(self, window, cx); + self.inline_blame_popover.take(); + if self.git_blame_inline_enabled { + self.start_inline_blame_timer(window, cx); + } + } + + self.blink_manager.update(cx, BlinkManager::pause_blinking); + cx.emit(EditorEvent::SelectionsChanged { local }); + + let selections = &self.selections.disjoint; + if selections.len() == 1 { + cx.emit(SearchEvent::ActiveMatchChanged) + } + if local { + if let Some((_, _, buffer_snapshot)) = buffer.as_singleton() { + let inmemory_selections = selections + .iter() + .map(|s| { + text::ToPoint::to_point(&s.range().start.text_anchor, buffer_snapshot) + ..text::ToPoint::to_point(&s.range().end.text_anchor, buffer_snapshot) + }) + .collect(); + self.update_restoration_data(cx, |data| { + data.selections = inmemory_selections; + }); + + if WorkspaceSettings::get(None, cx).restore_on_startup + != RestoreOnStartupBehavior::None + { + if let Some(workspace_id) = + self.workspace.as_ref().and_then(|workspace| workspace.1) + { + let snapshot = self.buffer().read(cx).snapshot(cx); + let selections = selections.clone(); + let background_executor = cx.background_executor().clone(); + let editor_id = cx.entity().entity_id().as_u64() as ItemId; + self.serialize_selections = cx.background_spawn(async move { + background_executor.timer(SERIALIZATION_THROTTLE_TIME).await; + let db_selections = selections + .iter() + .map(|selection| { + ( + selection.start.to_offset(&snapshot), + selection.end.to_offset(&snapshot), + ) + }) + .collect(); + + DB.save_editor_selections(editor_id, workspace_id, db_selections) + .await + .with_context(|| format!("persisting editor selections for editor {editor_id}, workspace {workspace_id:?}")) + .log_err(); + }); + } + } + } + } + + cx.notify(); + } + + fn folds_did_change(&mut self, cx: &mut Context) { + use text::ToOffset as _; + use text::ToPoint as _; + + if WorkspaceSettings::get(None, cx).restore_on_startup == RestoreOnStartupBehavior::None { + return; + } + + let Some(singleton) = self.buffer().read(cx).as_singleton() else { + return; + }; + + let snapshot = singleton.read(cx).snapshot(); + let inmemory_folds = self.display_map.update(cx, |display_map, cx| { + let display_snapshot = display_map.snapshot(cx); + + display_snapshot + .folds_in_range(0..display_snapshot.buffer_snapshot.len()) + .map(|fold| { + fold.range.start.text_anchor.to_point(&snapshot) + ..fold.range.end.text_anchor.to_point(&snapshot) + }) + .collect() + }); + self.update_restoration_data(cx, |data| { + data.folds = inmemory_folds; + }); + + let Some(workspace_id) = self.workspace.as_ref().and_then(|workspace| workspace.1) else { + return; + }; + let background_executor = cx.background_executor().clone(); + let editor_id = cx.entity().entity_id().as_u64() as ItemId; + let db_folds = self.display_map.update(cx, |display_map, cx| { + display_map + .snapshot(cx) + .folds_in_range(0..snapshot.len()) + .map(|fold| { + ( + fold.range.start.text_anchor.to_offset(&snapshot), + fold.range.end.text_anchor.to_offset(&snapshot), + ) + }) + .collect() + }); + self.serialize_folds = cx.background_spawn(async move { + background_executor.timer(SERIALIZATION_THROTTLE_TIME).await; + DB.save_editor_folds(editor_id, workspace_id, db_folds) + .await + .with_context(|| { + format!( + "persisting editor folds for editor {editor_id}, workspace {workspace_id:?}" + ) + }) + .log_err(); + }); + } + + pub fn sync_selections( + &mut self, + other: Entity, + cx: &mut Context, + ) -> gpui::Subscription { + let other_selections = other.read(cx).selections.disjoint.to_vec(); + self.selections.change_with(cx, |selections| { + selections.select_anchors(other_selections); + }); + + let other_subscription = + cx.subscribe(&other, |this, other, other_evt, cx| match other_evt { + EditorEvent::SelectionsChanged { local: true } => { + let other_selections = other.read(cx).selections.disjoint.to_vec(); + if other_selections.is_empty() { + return; + } + this.selections.change_with(cx, |selections| { + selections.select_anchors(other_selections); + }); + } + _ => {} + }); + + let this_subscription = + cx.subscribe_self::(move |this, this_evt, cx| match this_evt { + EditorEvent::SelectionsChanged { local: true } => { + let these_selections = this.selections.disjoint.to_vec(); + if these_selections.is_empty() { + return; + } + other.update(cx, |other_editor, cx| { + other_editor.selections.change_with(cx, |selections| { + selections.select_anchors(these_selections); + }) + }); + } + _ => {} + }); + + Subscription::join(other_subscription, this_subscription) + } + + pub fn change_selections( + &mut self, + autoscroll: Option, + window: &mut Window, + cx: &mut Context, + change: impl FnOnce(&mut MutableSelectionsCollection<'_>) -> R, + ) -> R { + self.change_selections_inner(autoscroll, true, window, cx, change) + } + + fn change_selections_inner( + &mut self, + autoscroll: Option, + request_completions: bool, + window: &mut Window, + cx: &mut Context, + change: impl FnOnce(&mut MutableSelectionsCollection<'_>) -> R, + ) -> R { + let old_cursor_position = self.selections.newest_anchor().head(); + self.push_to_selection_history(); + + let (changed, result) = self.selections.change_with(cx, change); + + if changed { + if let Some(autoscroll) = autoscroll { + self.request_autoscroll(autoscroll, cx); + } + self.selections_did_change(true, &old_cursor_position, request_completions, window, cx); + + if self.should_open_signature_help_automatically( + &old_cursor_position, + self.signature_help_state.backspace_pressed(), + cx, + ) { + self.show_signature_help(&ShowSignatureHelp, window, cx); + } + self.signature_help_state.set_backspace_pressed(false); + } + + result + } + + pub fn edit(&mut self, edits: I, cx: &mut Context) + where + I: IntoIterator, T)>, + S: ToOffset, + T: Into>, + { + if self.read_only(cx) { + return; + } + + self.buffer + .update(cx, |buffer, cx| buffer.edit(edits, None, cx)); + } + + pub fn edit_with_autoindent(&mut self, edits: I, cx: &mut Context) + where + I: IntoIterator, T)>, + S: ToOffset, + T: Into>, + { + if self.read_only(cx) { + return; + } + + self.buffer.update(cx, |buffer, cx| { + buffer.edit(edits, self.autoindent_mode.clone(), cx) + }); + } + + pub fn edit_with_block_indent( + &mut self, + edits: I, + original_indent_columns: Vec>, + cx: &mut Context, + ) where + I: IntoIterator, T)>, + S: ToOffset, + T: Into>, + { + if self.read_only(cx) { + return; + } + + self.buffer.update(cx, |buffer, cx| { + buffer.edit( + edits, + Some(AutoindentMode::Block { + original_indent_columns, + }), + cx, + ) + }); + } + + fn select(&mut self, phase: SelectPhase, window: &mut Window, cx: &mut Context) { + self.hide_context_menu(window, cx); + + match phase { + SelectPhase::Begin { + position, + add, + click_count, + } => self.begin_selection(position, add, click_count, window, cx), + SelectPhase::BeginColumnar { + position, + goal_column, + reset, + } => self.begin_columnar_selection(position, goal_column, reset, window, cx), + SelectPhase::Extend { + position, + click_count, + } => self.extend_selection(position, click_count, window, cx), + SelectPhase::Update { + position, + goal_column, + scroll_delta, + } => self.update_selection(position, goal_column, scroll_delta, window, cx), + SelectPhase::End => self.end_selection(window, cx), + } + } + + fn extend_selection( + &mut self, + position: DisplayPoint, + click_count: usize, + window: &mut Window, + cx: &mut Context, + ) { + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let tail = self.selections.newest::(cx).tail(); + self.begin_selection(position, false, click_count, window, cx); + + let position = position.to_offset(&display_map, Bias::Left); + let tail_anchor = display_map.buffer_snapshot.anchor_before(tail); + + let mut pending_selection = self + .selections + .pending_anchor() + .expect("extend_selection not called with pending selection"); + if position >= tail { + pending_selection.start = tail_anchor; + } else { + pending_selection.end = tail_anchor; + pending_selection.reversed = true; + } + + let mut pending_mode = self.selections.pending_mode().unwrap(); + match &mut pending_mode { + SelectMode::Word(range) | SelectMode::Line(range) => *range = tail_anchor..tail_anchor, + _ => {} + } + + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.set_pending(pending_selection, pending_mode) + }); + } + + fn begin_selection( + &mut self, + position: DisplayPoint, + add: bool, + click_count: usize, + window: &mut Window, + cx: &mut Context, + ) { + if !self.focus_handle.is_focused(window) { + self.last_focused_descendant = None; + window.focus(&self.focus_handle); + } + + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let buffer = &display_map.buffer_snapshot; + let newest_selection = self.selections.newest_anchor().clone(); + let position = display_map.clip_point(position, Bias::Left); + + let start; + let end; + let mode; + let mut auto_scroll; + match click_count { + 1 => { + start = buffer.anchor_before(position.to_point(&display_map)); + end = start; + mode = SelectMode::Character; + auto_scroll = true; + } + 2 => { + let range = movement::surrounding_word(&display_map, position); + start = buffer.anchor_before(range.start.to_point(&display_map)); + end = buffer.anchor_before(range.end.to_point(&display_map)); + mode = SelectMode::Word(start..end); + auto_scroll = true; + } + 3 => { + let position = display_map + .clip_point(position, Bias::Left) + .to_point(&display_map); + let line_start = display_map.prev_line_boundary(position).0; + let next_line_start = buffer.clip_point( + display_map.next_line_boundary(position).0 + Point::new(1, 0), + Bias::Left, + ); + start = buffer.anchor_before(line_start); + end = buffer.anchor_before(next_line_start); + mode = SelectMode::Line(start..end); + auto_scroll = true; + } + _ => { + start = buffer.anchor_before(0); + end = buffer.anchor_before(buffer.len()); + mode = SelectMode::All; + auto_scroll = false; + } + } + auto_scroll &= EditorSettings::get_global(cx).autoscroll_on_clicks; + + let point_to_delete: Option = { + let selected_points: Vec> = + self.selections.disjoint_in_range(start..end, cx); + + if !add || click_count > 1 { + None + } else if !selected_points.is_empty() { + Some(selected_points[0].id) + } else { + let clicked_point_already_selected = + self.selections.disjoint.iter().find(|selection| { + selection.start.to_point(buffer) == start.to_point(buffer) + || selection.end.to_point(buffer) == end.to_point(buffer) + }); + + clicked_point_already_selected.map(|selection| selection.id) + } + }; + + let selections_count = self.selections.count(); + + self.change_selections(auto_scroll.then(Autoscroll::newest), window, cx, |s| { + if let Some(point_to_delete) = point_to_delete { + s.delete(point_to_delete); + + if selections_count == 1 { + s.set_pending_anchor_range(start..end, mode); + } + } else { + if !add { + s.clear_disjoint(); + } else if click_count > 1 { + s.delete(newest_selection.id) + } + + s.set_pending_anchor_range(start..end, mode); + } + }); + } + + fn begin_columnar_selection( + &mut self, + position: DisplayPoint, + goal_column: u32, + reset: bool, + window: &mut Window, + cx: &mut Context, + ) { + if !self.focus_handle.is_focused(window) { + self.last_focused_descendant = None; + window.focus(&self.focus_handle); + } + + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + + if reset { + let pointer_position = display_map + .buffer_snapshot + .anchor_before(position.to_point(&display_map)); + + self.change_selections(Some(Autoscroll::newest()), window, cx, |s| { + s.clear_disjoint(); + s.set_pending_anchor_range( + pointer_position..pointer_position, + SelectMode::Character, + ); + }); + } + + let tail = self.selections.newest::(cx).tail(); + self.columnar_selection_tail = Some(display_map.buffer_snapshot.anchor_before(tail)); + + if !reset { + self.select_columns( + tail.to_display_point(&display_map), + position, + goal_column, + &display_map, + window, + cx, + ); + } + } + + fn update_selection( + &mut self, + position: DisplayPoint, + goal_column: u32, + scroll_delta: gpui::Point, + window: &mut Window, + cx: &mut Context, + ) { + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + + if let Some(tail) = self.columnar_selection_tail.as_ref() { + let tail = tail.to_display_point(&display_map); + self.select_columns(tail, position, goal_column, &display_map, window, cx); + } else if let Some(mut pending) = self.selections.pending_anchor() { + let buffer = self.buffer.read(cx).snapshot(cx); + let head; + let tail; + let mode = self.selections.pending_mode().unwrap(); + match &mode { + SelectMode::Character => { + head = position.to_point(&display_map); + tail = pending.tail().to_point(&buffer); + } + SelectMode::Word(original_range) => { + let original_display_range = original_range.start.to_display_point(&display_map) + ..original_range.end.to_display_point(&display_map); + let original_buffer_range = original_display_range.start.to_point(&display_map) + ..original_display_range.end.to_point(&display_map); + if movement::is_inside_word(&display_map, position) + || original_display_range.contains(&position) + { + let word_range = movement::surrounding_word(&display_map, position); + if word_range.start < original_display_range.start { + head = word_range.start.to_point(&display_map); + } else { + head = word_range.end.to_point(&display_map); + } + } else { + head = position.to_point(&display_map); + } + + if head <= original_buffer_range.start { + tail = original_buffer_range.end; + } else { + tail = original_buffer_range.start; + } + } + SelectMode::Line(original_range) => { + let original_range = original_range.to_point(&display_map.buffer_snapshot); + + let position = display_map + .clip_point(position, Bias::Left) + .to_point(&display_map); + let line_start = display_map.prev_line_boundary(position).0; + let next_line_start = buffer.clip_point( + display_map.next_line_boundary(position).0 + Point::new(1, 0), + Bias::Left, + ); + + if line_start < original_range.start { + head = line_start + } else { + head = next_line_start + } + + if head <= original_range.start { + tail = original_range.end; + } else { + tail = original_range.start; + } + } + SelectMode::All => { + return; + } + }; + + if head < tail { + pending.start = buffer.anchor_before(head); + pending.end = buffer.anchor_before(tail); + pending.reversed = true; + } else { + pending.start = buffer.anchor_before(tail); + pending.end = buffer.anchor_before(head); + pending.reversed = false; + } + + self.change_selections(None, window, cx, |s| { + s.set_pending(pending, mode); + }); + } else { + log::error!("update_selection dispatched with no pending selection"); + return; + } + + self.apply_scroll_delta(scroll_delta, window, cx); + cx.notify(); + } + + fn end_selection(&mut self, window: &mut Window, cx: &mut Context) { + self.columnar_selection_tail.take(); + if self.selections.pending_anchor().is_some() { + let selections = self.selections.all::(cx); + self.change_selections(None, window, cx, |s| { + s.select(selections); + s.clear_pending(); + }); + } + } + + fn select_columns( + &mut self, + tail: DisplayPoint, + head: DisplayPoint, + goal_column: u32, + display_map: &DisplaySnapshot, + window: &mut Window, + cx: &mut Context, + ) { + let start_row = cmp::min(tail.row(), head.row()); + let end_row = cmp::max(tail.row(), head.row()); + let start_column = cmp::min(tail.column(), goal_column); + let end_column = cmp::max(tail.column(), goal_column); + let reversed = start_column < tail.column(); + + let selection_ranges = (start_row.0..=end_row.0) + .map(DisplayRow) + .filter_map(|row| { + if start_column <= display_map.line_len(row) && !display_map.is_block_line(row) { + let start = display_map + .clip_point(DisplayPoint::new(row, start_column), Bias::Left) + .to_point(display_map); + let end = display_map + .clip_point(DisplayPoint::new(row, end_column), Bias::Right) + .to_point(display_map); + if reversed { + Some(end..start) + } else { + Some(start..end) + } + } else { + None + } + }) + .collect::>(); + + self.change_selections(None, window, cx, |s| { + s.select_ranges(selection_ranges); + }); + cx.notify(); + } + + pub fn has_non_empty_selection(&self, cx: &mut App) -> bool { + self.selections + .all_adjusted(cx) + .iter() + .any(|selection| !selection.is_empty()) + } + + pub fn has_pending_nonempty_selection(&self) -> bool { + let pending_nonempty_selection = match self.selections.pending_anchor() { + Some(Selection { start, end, .. }) => start != end, + None => false, + }; + + pending_nonempty_selection + || (self.columnar_selection_tail.is_some() && self.selections.disjoint.len() > 1) + } + + pub fn has_pending_selection(&self) -> bool { + self.selections.pending_anchor().is_some() || self.columnar_selection_tail.is_some() + } + + pub fn cancel(&mut self, _: &Cancel, window: &mut Window, cx: &mut Context) { + self.selection_mark_mode = false; + + if self.clear_expanded_diff_hunks(cx) { + cx.notify(); + return; + } + if self.dismiss_menus_and_popups(true, window, cx) { + return; + } + + if self.mode.is_full() + && self.change_selections(Some(Autoscroll::fit()), window, cx, |s| s.try_cancel()) + { + return; + } + + cx.propagate(); + } + + pub fn dismiss_menus_and_popups( + &mut self, + is_user_requested: bool, + window: &mut Window, + cx: &mut Context, + ) -> bool { + if self.take_rename(false, window, cx).is_some() { + return true; + } + + if hide_hover(self, cx) { + return true; + } + + if self.hide_signature_help(cx, SignatureHelpHiddenBy::Escape) { + return true; + } + + if self.hide_context_menu(window, cx).is_some() { + return true; + } + + if self.mouse_context_menu.take().is_some() { + return true; + } + + if is_user_requested && self.discard_inline_completion(true, cx) { + return true; + } + + if self.snippet_stack.pop().is_some() { + return true; + } + + if self.mode.is_full() && matches!(self.active_diagnostics, ActiveDiagnostic::Group(_)) { + self.dismiss_diagnostics(cx); + return true; + } + + false + } + + fn linked_editing_ranges_for( + &self, + selection: Range, + cx: &App, + ) -> Option, Vec>>> { + if self.linked_edit_ranges.is_empty() { + return None; + } + let ((base_range, linked_ranges), buffer_snapshot, buffer) = + selection.end.buffer_id.and_then(|end_buffer_id| { + if selection.start.buffer_id != Some(end_buffer_id) { + return None; + } + let buffer = self.buffer.read(cx).buffer(end_buffer_id)?; + let snapshot = buffer.read(cx).snapshot(); + self.linked_edit_ranges + .get(end_buffer_id, selection.start..selection.end, &snapshot) + .map(|ranges| (ranges, snapshot, buffer)) + })?; + use text::ToOffset as TO; + // find offset from the start of current range to current cursor position + let start_byte_offset = TO::to_offset(&base_range.start, &buffer_snapshot); + + let start_offset = TO::to_offset(&selection.start, &buffer_snapshot); + let start_difference = start_offset - start_byte_offset; + let end_offset = TO::to_offset(&selection.end, &buffer_snapshot); + let end_difference = end_offset - start_byte_offset; + // Current range has associated linked ranges. + let mut linked_edits = HashMap::<_, Vec<_>>::default(); + for range in linked_ranges.iter() { + let start_offset = TO::to_offset(&range.start, &buffer_snapshot); + let end_offset = start_offset + end_difference; + let start_offset = start_offset + start_difference; + if start_offset > buffer_snapshot.len() || end_offset > buffer_snapshot.len() { + continue; + } + if self.selections.disjoint_anchor_ranges().any(|s| { + if s.start.buffer_id != selection.start.buffer_id + || s.end.buffer_id != selection.end.buffer_id + { + return false; + } + TO::to_offset(&s.start.text_anchor, &buffer_snapshot) <= end_offset + && TO::to_offset(&s.end.text_anchor, &buffer_snapshot) >= start_offset + }) { + continue; + } + let start = buffer_snapshot.anchor_after(start_offset); + let end = buffer_snapshot.anchor_after(end_offset); + linked_edits + .entry(buffer.clone()) + .or_default() + .push(start..end); + } + Some(linked_edits) + } + + pub fn handle_input(&mut self, text: &str, window: &mut Window, cx: &mut Context) { + let text: Arc = text.into(); + + if self.read_only(cx) { + return; + } + + self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + + let selections = self.selections.all_adjusted(cx); + let mut bracket_inserted = false; + let mut edits = Vec::new(); + let mut linked_edits = HashMap::<_, Vec<_>>::default(); + let mut new_selections = Vec::with_capacity(selections.len()); + let mut new_autoclose_regions = Vec::new(); + let snapshot = self.buffer.read(cx).read(cx); + let mut clear_linked_edit_ranges = false; + + for (selection, autoclose_region) in + self.selections_with_autoclose_regions(selections, &snapshot) + { + if let Some(scope) = snapshot.language_scope_at(selection.head()) { + // Determine if the inserted text matches the opening or closing + // bracket of any of this language's bracket pairs. + let mut bracket_pair = None; + let mut is_bracket_pair_start = false; + let mut is_bracket_pair_end = false; + if !text.is_empty() { + let mut bracket_pair_matching_end = None; + // `text` can be empty when a user is using IME (e.g. Chinese Wubi Simplified) + // and they are removing the character that triggered IME popup. + for (pair, enabled) in scope.brackets() { + if !pair.close && !pair.surround { + continue; + } + + if enabled && pair.start.ends_with(text.as_ref()) { + let prefix_len = pair.start.len() - text.len(); + let preceding_text_matches_prefix = prefix_len == 0 + || (selection.start.column >= (prefix_len as u32) + && snapshot.contains_str_at( + Point::new( + selection.start.row, + selection.start.column - (prefix_len as u32), + ), + &pair.start[..prefix_len], + )); + if preceding_text_matches_prefix { + bracket_pair = Some(pair.clone()); + is_bracket_pair_start = true; + break; + } + } + if pair.end.as_str() == text.as_ref() && bracket_pair_matching_end.is_none() + { + // take first bracket pair matching end, but don't break in case a later bracket + // pair matches start + bracket_pair_matching_end = Some(pair.clone()); + } + } + if bracket_pair.is_none() && bracket_pair_matching_end.is_some() { + bracket_pair = Some(bracket_pair_matching_end.unwrap()); + is_bracket_pair_end = true; + } + } + + if let Some(bracket_pair) = bracket_pair { + let snapshot_settings = snapshot.language_settings_at(selection.start, cx); + let autoclose = self.use_autoclose && snapshot_settings.use_autoclose; + let auto_surround = + self.use_auto_surround && snapshot_settings.use_auto_surround; + if selection.is_empty() { + if is_bracket_pair_start { + // If the inserted text is a suffix of an opening bracket and the + // selection is preceded by the rest of the opening bracket, then + // insert the closing bracket. + let following_text_allows_autoclose = snapshot + .chars_at(selection.start) + .next() + .map_or(true, |c| scope.should_autoclose_before(c)); + + let preceding_text_allows_autoclose = selection.start.column == 0 + || snapshot.reversed_chars_at(selection.start).next().map_or( + true, + |c| { + bracket_pair.start != bracket_pair.end + || !snapshot + .char_classifier_at(selection.start) + .is_word(c) + }, + ); + + let is_closing_quote = if bracket_pair.end == bracket_pair.start + && bracket_pair.start.len() == 1 + { + let target = bracket_pair.start.chars().next().unwrap(); + let current_line_count = snapshot + .reversed_chars_at(selection.start) + .take_while(|&c| c != '\n') + .filter(|&c| c == target) + .count(); + current_line_count % 2 == 1 + } else { + false + }; + + if autoclose + && bracket_pair.close + && following_text_allows_autoclose + && preceding_text_allows_autoclose + && !is_closing_quote + { + let anchor = snapshot.anchor_before(selection.end); + new_selections.push((selection.map(|_| anchor), text.len())); + new_autoclose_regions.push(( + anchor, + text.len(), + selection.id, + bracket_pair.clone(), + )); + edits.push(( + selection.range(), + format!("{}{}", text, bracket_pair.end).into(), + )); + bracket_inserted = true; + continue; + } + } + + if let Some(region) = autoclose_region { + // If the selection is followed by an auto-inserted closing bracket, + // then don't insert that closing bracket again; just move the selection + // past the closing bracket. + let should_skip = selection.end == region.range.end.to_point(&snapshot) + && text.as_ref() == region.pair.end.as_str(); + if should_skip { + let anchor = snapshot.anchor_after(selection.end); + new_selections + .push((selection.map(|_| anchor), region.pair.end.len())); + continue; + } + } + + let always_treat_brackets_as_autoclosed = snapshot + .language_settings_at(selection.start, cx) + .always_treat_brackets_as_autoclosed; + if always_treat_brackets_as_autoclosed + && is_bracket_pair_end + && snapshot.contains_str_at(selection.end, text.as_ref()) + { + // Otherwise, when `always_treat_brackets_as_autoclosed` is set to `true + // and the inserted text is a closing bracket and the selection is followed + // by the closing bracket then move the selection past the closing bracket. + let anchor = snapshot.anchor_after(selection.end); + new_selections.push((selection.map(|_| anchor), text.len())); + continue; + } + } + // If an opening bracket is 1 character long and is typed while + // text is selected, then surround that text with the bracket pair. + else if auto_surround + && bracket_pair.surround + && is_bracket_pair_start + && bracket_pair.start.chars().count() == 1 + { + edits.push((selection.start..selection.start, text.clone())); + edits.push(( + selection.end..selection.end, + bracket_pair.end.as_str().into(), + )); + bracket_inserted = true; + new_selections.push(( + Selection { + id: selection.id, + start: snapshot.anchor_after(selection.start), + end: snapshot.anchor_before(selection.end), + reversed: selection.reversed, + goal: selection.goal, + }, + 0, + )); + continue; + } + } + } + + if self.auto_replace_emoji_shortcode + && selection.is_empty() + && text.as_ref().ends_with(':') + { + if let Some(possible_emoji_short_code) = + Self::find_possible_emoji_shortcode_at_position(&snapshot, selection.start) + { + if !possible_emoji_short_code.is_empty() { + if let Some(emoji) = emojis::get_by_shortcode(&possible_emoji_short_code) { + let emoji_shortcode_start = Point::new( + selection.start.row, + selection.start.column - possible_emoji_short_code.len() as u32 - 1, + ); + + // Remove shortcode from buffer + edits.push(( + emoji_shortcode_start..selection.start, + "".to_string().into(), + )); + new_selections.push(( + Selection { + id: selection.id, + start: snapshot.anchor_after(emoji_shortcode_start), + end: snapshot.anchor_before(selection.start), + reversed: selection.reversed, + goal: selection.goal, + }, + 0, + )); + + // Insert emoji + let selection_start_anchor = snapshot.anchor_after(selection.start); + new_selections.push((selection.map(|_| selection_start_anchor), 0)); + edits.push((selection.start..selection.end, emoji.to_string().into())); + + continue; + } + } + } + } + + // If not handling any auto-close operation, then just replace the selected + // text with the given input and move the selection to the end of the + // newly inserted text. + let anchor = snapshot.anchor_after(selection.end); + if !self.linked_edit_ranges.is_empty() { + let start_anchor = snapshot.anchor_before(selection.start); + + let is_word_char = text.chars().next().map_or(true, |char| { + let classifier = snapshot.char_classifier_at(start_anchor.to_offset(&snapshot)); + classifier.is_word(char) + }); + + if is_word_char { + if let Some(ranges) = self + .linked_editing_ranges_for(start_anchor.text_anchor..anchor.text_anchor, cx) + { + for (buffer, edits) in ranges { + linked_edits + .entry(buffer.clone()) + .or_default() + .extend(edits.into_iter().map(|range| (range, text.clone()))); + } + } + } else { + clear_linked_edit_ranges = true; + } + } + + new_selections.push((selection.map(|_| anchor), 0)); + edits.push((selection.start..selection.end, text.clone())); + } + + drop(snapshot); + + self.transact(window, cx, |this, window, cx| { + if clear_linked_edit_ranges { + this.linked_edit_ranges.clear(); + } + let initial_buffer_versions = + jsx_tag_auto_close::construct_initial_buffer_versions_map(this, &edits, cx); + + this.buffer.update(cx, |buffer, cx| { + buffer.edit(edits, this.autoindent_mode.clone(), cx); + }); + for (buffer, edits) in linked_edits { + buffer.update(cx, |buffer, cx| { + let snapshot = buffer.snapshot(); + let edits = edits + .into_iter() + .map(|(range, text)| { + use text::ToPoint as TP; + let end_point = TP::to_point(&range.end, &snapshot); + let start_point = TP::to_point(&range.start, &snapshot); + (start_point..end_point, text) + }) + .sorted_by_key(|(range, _)| range.start); + buffer.edit(edits, None, cx); + }) + } + let new_anchor_selections = new_selections.iter().map(|e| &e.0); + let new_selection_deltas = new_selections.iter().map(|e| e.1); + let map = this.display_map.update(cx, |map, cx| map.snapshot(cx)); + let new_selections = resolve_selections::(new_anchor_selections, &map) + .zip(new_selection_deltas) + .map(|(selection, delta)| Selection { + id: selection.id, + start: selection.start + delta, + end: selection.end + delta, + reversed: selection.reversed, + goal: SelectionGoal::None, + }) + .collect::>(); + + let mut i = 0; + for (position, delta, selection_id, pair) in new_autoclose_regions { + let position = position.to_offset(&map.buffer_snapshot) + delta; + let start = map.buffer_snapshot.anchor_before(position); + let end = map.buffer_snapshot.anchor_after(position); + while let Some(existing_state) = this.autoclose_regions.get(i) { + match existing_state.range.start.cmp(&start, &map.buffer_snapshot) { + Ordering::Less => i += 1, + Ordering::Greater => break, + Ordering::Equal => { + match end.cmp(&existing_state.range.end, &map.buffer_snapshot) { + Ordering::Less => i += 1, + Ordering::Equal => break, + Ordering::Greater => break, + } + } + } + } + this.autoclose_regions.insert( + i, + AutocloseRegion { + selection_id, + range: start..end, + pair, + }, + ); + } + + let had_active_inline_completion = this.has_active_inline_completion(); + this.change_selections_inner(Some(Autoscroll::fit()), false, window, cx, |s| { + s.select(new_selections) + }); + + if !bracket_inserted { + if let Some(on_type_format_task) = + this.trigger_on_type_formatting(text.to_string(), window, cx) + { + on_type_format_task.detach_and_log_err(cx); + } + } + + let editor_settings = EditorSettings::get_global(cx); + if bracket_inserted + && (editor_settings.auto_signature_help + || editor_settings.show_signature_help_after_edits) + { + this.show_signature_help(&ShowSignatureHelp, window, cx); + } + + let trigger_in_words = + this.show_edit_predictions_in_menu() || !had_active_inline_completion; + if this.hard_wrap.is_some() { + let latest: Range = this.selections.newest(cx).range(); + if latest.is_empty() + && this + .buffer() + .read(cx) + .snapshot(cx) + .line_len(MultiBufferRow(latest.start.row)) + == latest.start.column + { + this.rewrap_impl( + RewrapOptions { + override_language_settings: true, + preserve_existing_whitespace: true, + }, + cx, + ) + } + } + this.trigger_completion_on_input(&text, trigger_in_words, window, cx); + linked_editing_ranges::refresh_linked_ranges(this, window, cx); + this.refresh_inline_completion(true, false, window, cx); + jsx_tag_auto_close::handle_from(this, initial_buffer_versions, window, cx); + }); + } + + fn find_possible_emoji_shortcode_at_position( + snapshot: &MultiBufferSnapshot, + position: Point, + ) -> Option { + let mut chars = Vec::new(); + let mut found_colon = false; + for char in snapshot.reversed_chars_at(position).take(100) { + // Found a possible emoji shortcode in the middle of the buffer + if found_colon { + if char.is_whitespace() { + chars.reverse(); + return Some(chars.iter().collect()); + } + // If the previous character is not a whitespace, we are in the middle of a word + // and we only want to complete the shortcode if the word is made up of other emojis + let mut containing_word = String::new(); + for ch in snapshot + .reversed_chars_at(position) + .skip(chars.len() + 1) + .take(100) + { + if ch.is_whitespace() { + break; + } + containing_word.push(ch); + } + let containing_word = containing_word.chars().rev().collect::(); + if util::word_consists_of_emojis(containing_word.as_str()) { + chars.reverse(); + return Some(chars.iter().collect()); + } + } + + if char.is_whitespace() || !char.is_ascii() { + return None; + } + if char == ':' { + found_colon = true; + } else { + chars.push(char); + } + } + // Found a possible emoji shortcode at the beginning of the buffer + chars.reverse(); + Some(chars.iter().collect()) + } + + pub fn newline(&mut self, _: &Newline, window: &mut Window, cx: &mut Context) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + self.transact(window, cx, |this, window, cx| { + let (edits, selection_fixup_info): (Vec<_>, Vec<_>) = { + let selections = this.selections.all::(cx); + let multi_buffer = this.buffer.read(cx); + let buffer = multi_buffer.snapshot(cx); + selections + .iter() + .map(|selection| { + let start_point = selection.start.to_point(&buffer); + let mut indent = + buffer.indent_size_for_line(MultiBufferRow(start_point.row)); + indent.len = cmp::min(indent.len, start_point.column); + let start = selection.start; + let end = selection.end; + let selection_is_empty = start == end; + let language_scope = buffer.language_scope_at(start); + let (comment_delimiter, insert_extra_newline) = if let Some(language) = + &language_scope + { + let insert_extra_newline = + insert_extra_newline_brackets(&buffer, start..end, language) + || insert_extra_newline_tree_sitter(&buffer, start..end); + + // Comment extension on newline is allowed only for cursor selections + let comment_delimiter = maybe!({ + if !selection_is_empty { + return None; + } + + if !multi_buffer.language_settings(cx).extend_comment_on_newline { + return None; + } + + let delimiters = language.line_comment_prefixes(); + let max_len_of_delimiter = + delimiters.iter().map(|delimiter| delimiter.len()).max()?; + let (snapshot, range) = + buffer.buffer_line_for_row(MultiBufferRow(start_point.row))?; + + let mut index_of_first_non_whitespace = 0; + let comment_candidate = snapshot + .chars_for_range(range) + .skip_while(|c| { + let should_skip = c.is_whitespace(); + if should_skip { + index_of_first_non_whitespace += 1; + } + should_skip + }) + .take(max_len_of_delimiter) + .collect::(); + let comment_prefix = delimiters.iter().find(|comment_prefix| { + comment_candidate.starts_with(comment_prefix.as_ref()) + })?; + let cursor_is_placed_after_comment_marker = + index_of_first_non_whitespace + comment_prefix.len() + <= start_point.column as usize; + if cursor_is_placed_after_comment_marker { + Some(comment_prefix.clone()) + } else { + None + } + }); + (comment_delimiter, insert_extra_newline) + } else { + (None, false) + }; + + let capacity_for_delimiter = comment_delimiter + .as_deref() + .map(str::len) + .unwrap_or_default(); + let mut new_text = + String::with_capacity(1 + capacity_for_delimiter + indent.len as usize); + new_text.push('\n'); + new_text.extend(indent.chars()); + if let Some(delimiter) = &comment_delimiter { + new_text.push_str(delimiter); + } + if insert_extra_newline { + new_text = new_text.repeat(2); + } + + let anchor = buffer.anchor_after(end); + let new_selection = selection.map(|_| anchor); + ( + (start..end, new_text), + (insert_extra_newline, new_selection), + ) + }) + .unzip() + }; + + this.edit_with_autoindent(edits, cx); + let buffer = this.buffer.read(cx).snapshot(cx); + let new_selections = selection_fixup_info + .into_iter() + .map(|(extra_newline_inserted, new_selection)| { + let mut cursor = new_selection.end.to_point(&buffer); + if extra_newline_inserted { + cursor.row -= 1; + cursor.column = buffer.line_len(MultiBufferRow(cursor.row)); + } + new_selection.map(|_| cursor) + }) + .collect(); + + this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.select(new_selections) + }); + this.refresh_inline_completion(true, false, window, cx); + }); + } + + pub fn newline_above(&mut self, _: &NewlineAbove, window: &mut Window, cx: &mut Context) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + + let buffer = self.buffer.read(cx); + let snapshot = buffer.snapshot(cx); + + let mut edits = Vec::new(); + let mut rows = Vec::new(); + + for (rows_inserted, selection) in self.selections.all_adjusted(cx).into_iter().enumerate() { + let cursor = selection.head(); + let row = cursor.row; + + let start_of_line = snapshot.clip_point(Point::new(row, 0), Bias::Left); + + let newline = "\n".to_string(); + edits.push((start_of_line..start_of_line, newline)); + + rows.push(row + rows_inserted as u32); + } + + self.transact(window, cx, |editor, window, cx| { + editor.edit(edits, cx); + + editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + let mut index = 0; + s.move_cursors_with(|map, _, _| { + let row = rows[index]; + index += 1; + + let point = Point::new(row, 0); + let boundary = map.next_line_boundary(point).1; + let clipped = map.clip_point(boundary, Bias::Left); + + (clipped, SelectionGoal::None) + }); + }); + + let mut indent_edits = Vec::new(); + let multibuffer_snapshot = editor.buffer.read(cx).snapshot(cx); + for row in rows { + let indents = multibuffer_snapshot.suggested_indents(row..row + 1, cx); + for (row, indent) in indents { + if indent.len == 0 { + continue; + } + + let text = match indent.kind { + IndentKind::Space => " ".repeat(indent.len as usize), + IndentKind::Tab => "\t".repeat(indent.len as usize), + }; + let point = Point::new(row.0, 0); + indent_edits.push((point..point, text)); + } + } + editor.edit(indent_edits, cx); + }); + } + + pub fn newline_below(&mut self, _: &NewlineBelow, window: &mut Window, cx: &mut Context) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + + let buffer = self.buffer.read(cx); + let snapshot = buffer.snapshot(cx); + + let mut edits = Vec::new(); + let mut rows = Vec::new(); + let mut rows_inserted = 0; + + for selection in self.selections.all_adjusted(cx) { + let cursor = selection.head(); + let row = cursor.row; + + let point = Point::new(row + 1, 0); + let start_of_line = snapshot.clip_point(point, Bias::Left); + + let newline = "\n".to_string(); + edits.push((start_of_line..start_of_line, newline)); + + rows_inserted += 1; + rows.push(row + rows_inserted); + } + + self.transact(window, cx, |editor, window, cx| { + editor.edit(edits, cx); + + editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + let mut index = 0; + s.move_cursors_with(|map, _, _| { + let row = rows[index]; + index += 1; + + let point = Point::new(row, 0); + let boundary = map.next_line_boundary(point).1; + let clipped = map.clip_point(boundary, Bias::Left); + + (clipped, SelectionGoal::None) + }); + }); + + let mut indent_edits = Vec::new(); + let multibuffer_snapshot = editor.buffer.read(cx).snapshot(cx); + for row in rows { + let indents = multibuffer_snapshot.suggested_indents(row..row + 1, cx); + for (row, indent) in indents { + if indent.len == 0 { + continue; + } + + let text = match indent.kind { + IndentKind::Space => " ".repeat(indent.len as usize), + IndentKind::Tab => "\t".repeat(indent.len as usize), + }; + let point = Point::new(row.0, 0); + indent_edits.push((point..point, text)); + } + } + editor.edit(indent_edits, cx); + }); + } + + pub fn insert(&mut self, text: &str, window: &mut Window, cx: &mut Context) { + let autoindent = text.is_empty().not().then(|| AutoindentMode::Block { + original_indent_columns: Vec::new(), + }); + self.insert_with_autoindent_mode(text, autoindent, window, cx); + } + + fn insert_with_autoindent_mode( + &mut self, + text: &str, + autoindent_mode: Option, + window: &mut Window, + cx: &mut Context, + ) { + if self.read_only(cx) { + return; + } + + let text: Arc = text.into(); + self.transact(window, cx, |this, window, cx| { + let old_selections = this.selections.all_adjusted(cx); + let selection_anchors = this.buffer.update(cx, |buffer, cx| { + let anchors = { + let snapshot = buffer.read(cx); + old_selections + .iter() + .map(|s| { + let anchor = snapshot.anchor_after(s.head()); + s.map(|_| anchor) + }) + .collect::>() + }; + buffer.edit( + old_selections + .iter() + .map(|s| (s.start..s.end, text.clone())), + autoindent_mode, + cx, + ); + anchors + }); + + this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.select_anchors(selection_anchors); + }); + + cx.notify(); + }); + } + + fn trigger_completion_on_input( + &mut self, + text: &str, + trigger_in_words: bool, + window: &mut Window, + cx: &mut Context, + ) { + let ignore_completion_provider = self + .context_menu + .borrow() + .as_ref() + .map(|menu| match menu { + CodeContextMenu::Completions(completions_menu) => { + completions_menu.ignore_completion_provider + } + CodeContextMenu::CodeActions(_) => false, + }) + .unwrap_or(false); + + if ignore_completion_provider { + self.show_word_completions(&ShowWordCompletions, window, cx); + } else if self.is_completion_trigger(text, trigger_in_words, cx) { + self.show_completions( + &ShowCompletions { + trigger: Some(text.to_owned()).filter(|x| !x.is_empty()), + }, + window, + cx, + ); + } else { + self.hide_context_menu(window, cx); + } + } + + fn is_completion_trigger( + &self, + text: &str, + trigger_in_words: bool, + cx: &mut Context, + ) -> bool { + let position = self.selections.newest_anchor().head(); + let multibuffer = self.buffer.read(cx); + let Some(buffer) = position + .buffer_id + .and_then(|buffer_id| multibuffer.buffer(buffer_id).clone()) + else { + return false; + }; + + if let Some(completion_provider) = &self.completion_provider { + completion_provider.is_completion_trigger( + &buffer, + position.text_anchor, + text, + trigger_in_words, + cx, + ) + } else { + false + } + } + + /// If any empty selections is touching the start of its innermost containing autoclose + /// region, expand it to select the brackets. + fn select_autoclose_pair(&mut self, window: &mut Window, cx: &mut Context) { + let selections = self.selections.all::(cx); + let buffer = self.buffer.read(cx).read(cx); + let new_selections = self + .selections_with_autoclose_regions(selections, &buffer) + .map(|(mut selection, region)| { + if !selection.is_empty() { + return selection; + } + + if let Some(region) = region { + let mut range = region.range.to_offset(&buffer); + if selection.start == range.start && range.start >= region.pair.start.len() { + range.start -= region.pair.start.len(); + if buffer.contains_str_at(range.start, ®ion.pair.start) + && buffer.contains_str_at(range.end, ®ion.pair.end) + { + range.end += region.pair.end.len(); + selection.start = range.start; + selection.end = range.end; + + return selection; + } + } + } + + let always_treat_brackets_as_autoclosed = buffer + .language_settings_at(selection.start, cx) + .always_treat_brackets_as_autoclosed; + + if !always_treat_brackets_as_autoclosed { + return selection; + } + + if let Some(scope) = buffer.language_scope_at(selection.start) { + for (pair, enabled) in scope.brackets() { + if !enabled || !pair.close { + continue; + } + + if buffer.contains_str_at(selection.start, &pair.end) { + let pair_start_len = pair.start.len(); + if buffer.contains_str_at( + selection.start.saturating_sub(pair_start_len), + &pair.start, + ) { + selection.start -= pair_start_len; + selection.end += pair.end.len(); + + return selection; + } + } + } + } + + selection + }) + .collect(); + + drop(buffer); + self.change_selections(None, window, cx, |selections| { + selections.select(new_selections) + }); + } + + /// Iterate the given selections, and for each one, find the smallest surrounding + /// autoclose region. This uses the ordering of the selections and the autoclose + /// regions to avoid repeated comparisons. + fn selections_with_autoclose_regions<'a, D: ToOffset + Clone>( + &'a self, + selections: impl IntoIterator>, + buffer: &'a MultiBufferSnapshot, + ) -> impl Iterator, Option<&'a AutocloseRegion>)> { + let mut i = 0; + let mut regions = self.autoclose_regions.as_slice(); + selections.into_iter().map(move |selection| { + let range = selection.start.to_offset(buffer)..selection.end.to_offset(buffer); + + let mut enclosing = None; + while let Some(pair_state) = regions.get(i) { + if pair_state.range.end.to_offset(buffer) < range.start { + regions = ®ions[i + 1..]; + i = 0; + } else if pair_state.range.start.to_offset(buffer) > range.end { + break; + } else { + if pair_state.selection_id == selection.id { + enclosing = Some(pair_state); + } + i += 1; + } + } + + (selection, enclosing) + }) + } + + /// Remove any autoclose regions that no longer contain their selection. + fn invalidate_autoclose_regions( + &mut self, + mut selections: &[Selection], + buffer: &MultiBufferSnapshot, + ) { + self.autoclose_regions.retain(|state| { + let mut i = 0; + while let Some(selection) = selections.get(i) { + if selection.end.cmp(&state.range.start, buffer).is_lt() { + selections = &selections[1..]; + continue; + } + if selection.start.cmp(&state.range.end, buffer).is_gt() { + break; + } + if selection.id == state.selection_id { + return true; + } else { + i += 1; + } + } + false + }); + } + + fn completion_query(buffer: &MultiBufferSnapshot, position: impl ToOffset) -> Option { + let offset = position.to_offset(buffer); + let (word_range, kind) = buffer.surrounding_word(offset, true); + if offset > word_range.start && kind == Some(CharKind::Word) { + Some( + buffer + .text_for_range(word_range.start..offset) + .collect::(), + ) + } else { + None + } + } + + pub fn toggle_inline_values( + &mut self, + _: &ToggleInlineValues, + _: &mut Window, + cx: &mut Context, + ) { + self.inline_value_cache.enabled = !self.inline_value_cache.enabled; + + self.refresh_inline_values(cx); + } + + pub fn toggle_inlay_hints( + &mut self, + _: &ToggleInlayHints, + _: &mut Window, + cx: &mut Context, + ) { + self.refresh_inlay_hints( + InlayHintRefreshReason::Toggle(!self.inlay_hints_enabled()), + cx, + ); + } + + pub fn inlay_hints_enabled(&self) -> bool { + self.inlay_hint_cache.enabled + } + + pub fn inline_values_enabled(&self) -> bool { + self.inline_value_cache.enabled + } + + fn refresh_inlay_hints(&mut self, reason: InlayHintRefreshReason, cx: &mut Context) { + if self.semantics_provider.is_none() || !self.mode.is_full() { + return; + } + + let reason_description = reason.description(); + let ignore_debounce = matches!( + reason, + InlayHintRefreshReason::SettingsChange(_) + | InlayHintRefreshReason::Toggle(_) + | InlayHintRefreshReason::ExcerptsRemoved(_) + | InlayHintRefreshReason::ModifiersChanged(_) + ); + let (invalidate_cache, required_languages) = match reason { + InlayHintRefreshReason::ModifiersChanged(enabled) => { + match self.inlay_hint_cache.modifiers_override(enabled) { + Some(enabled) => { + if enabled { + (InvalidationStrategy::RefreshRequested, None) + } else { + self.splice_inlays( + &self + .visible_inlay_hints(cx) + .iter() + .map(|inlay| inlay.id) + .collect::>(), + Vec::new(), + cx, + ); + return; + } + } + None => return, + } + } + InlayHintRefreshReason::Toggle(enabled) => { + if self.inlay_hint_cache.toggle(enabled) { + if enabled { + (InvalidationStrategy::RefreshRequested, None) + } else { + self.splice_inlays( + &self + .visible_inlay_hints(cx) + .iter() + .map(|inlay| inlay.id) + .collect::>(), + Vec::new(), + cx, + ); + return; + } + } else { + return; + } + } + InlayHintRefreshReason::SettingsChange(new_settings) => { + match self.inlay_hint_cache.update_settings( + &self.buffer, + new_settings, + self.visible_inlay_hints(cx), + cx, + ) { + ControlFlow::Break(Some(InlaySplice { + to_remove, + to_insert, + })) => { + self.splice_inlays(&to_remove, to_insert, cx); + return; + } + ControlFlow::Break(None) => return, + ControlFlow::Continue(()) => (InvalidationStrategy::RefreshRequested, None), + } + } + InlayHintRefreshReason::ExcerptsRemoved(excerpts_removed) => { + if let Some(InlaySplice { + to_remove, + to_insert, + }) = self.inlay_hint_cache.remove_excerpts(&excerpts_removed) + { + self.splice_inlays(&to_remove, to_insert, cx); + } + self.display_map.update(cx, |display_map, _| { + display_map.remove_inlays_for_excerpts(&excerpts_removed) + }); + return; + } + InlayHintRefreshReason::NewLinesShown => (InvalidationStrategy::None, None), + InlayHintRefreshReason::BufferEdited(buffer_languages) => { + (InvalidationStrategy::BufferEdited, Some(buffer_languages)) + } + InlayHintRefreshReason::RefreshRequested => { + (InvalidationStrategy::RefreshRequested, None) + } + }; + + if let Some(InlaySplice { + to_remove, + to_insert, + }) = self.inlay_hint_cache.spawn_hint_refresh( + reason_description, + self.excerpts_for_inlay_hints_query(required_languages.as_ref(), cx), + invalidate_cache, + ignore_debounce, + cx, + ) { + self.splice_inlays(&to_remove, to_insert, cx); + } + } + + fn visible_inlay_hints(&self, cx: &Context) -> Vec { + self.display_map + .read(cx) + .current_inlays() + .filter(move |inlay| matches!(inlay.id, InlayId::Hint(_))) + .cloned() + .collect() + } + + pub fn excerpts_for_inlay_hints_query( + &self, + restrict_to_languages: Option<&HashSet>>, + cx: &mut Context, + ) -> HashMap, clock::Global, Range)> { + let Some(project) = self.project.as_ref() else { + return HashMap::default(); + }; + let project = project.read(cx); + let multi_buffer = self.buffer().read(cx); + let multi_buffer_snapshot = multi_buffer.snapshot(cx); + let multi_buffer_visible_start = self + .scroll_manager + .anchor() + .anchor + .to_point(&multi_buffer_snapshot); + let multi_buffer_visible_end = multi_buffer_snapshot.clip_point( + multi_buffer_visible_start + + Point::new(self.visible_line_count().unwrap_or(0.).ceil() as u32, 0), + Bias::Left, + ); + let multi_buffer_visible_range = multi_buffer_visible_start..multi_buffer_visible_end; + multi_buffer_snapshot + .range_to_buffer_ranges(multi_buffer_visible_range) + .into_iter() + .filter(|(_, excerpt_visible_range, _)| !excerpt_visible_range.is_empty()) + .filter_map(|(buffer, excerpt_visible_range, excerpt_id)| { + let buffer_file = project::File::from_dyn(buffer.file())?; + let buffer_worktree = project.worktree_for_id(buffer_file.worktree_id(cx), cx)?; + let worktree_entry = buffer_worktree + .read(cx) + .entry_for_id(buffer_file.project_entry_id(cx)?)?; + if worktree_entry.is_ignored { + return None; + } + + let language = buffer.language()?; + if let Some(restrict_to_languages) = restrict_to_languages { + if !restrict_to_languages.contains(language) { + return None; + } + } + Some(( + excerpt_id, + ( + multi_buffer.buffer(buffer.remote_id()).unwrap(), + buffer.version().clone(), + excerpt_visible_range, + ), + )) + }) + .collect() + } + + pub fn text_layout_details(&self, window: &mut Window) -> TextLayoutDetails { + TextLayoutDetails { + text_system: window.text_system().clone(), + editor_style: self.style.clone().unwrap(), + rem_size: window.rem_size(), + scroll_anchor: self.scroll_manager.anchor(), + visible_rows: self.visible_line_count(), + vertical_scroll_margin: self.scroll_manager.vertical_scroll_margin, + } + } + + pub fn splice_inlays( + &self, + to_remove: &[InlayId], + to_insert: Vec, + cx: &mut Context, + ) { + self.display_map.update(cx, |display_map, cx| { + display_map.splice_inlays(to_remove, to_insert, cx) + }); + cx.notify(); + } + + fn trigger_on_type_formatting( + &self, + input: String, + window: &mut Window, + cx: &mut Context, + ) -> Option>> { + if input.len() != 1 { + return None; + } + + let project = self.project.as_ref()?; + let position = self.selections.newest_anchor().head(); + let (buffer, buffer_position) = self + .buffer + .read(cx) + .text_anchor_for_position(position, cx)?; + + let settings = language_settings::language_settings( + buffer + .read(cx) + .language_at(buffer_position) + .map(|l| l.name()), + buffer.read(cx).file(), + cx, + ); + if !settings.use_on_type_format { + return None; + } + + // OnTypeFormatting returns a list of edits, no need to pass them between Zed instances, + // hence we do LSP request & edit on host side only — add formats to host's history. + let push_to_lsp_host_history = true; + // If this is not the host, append its history with new edits. + let push_to_client_history = project.read(cx).is_via_collab(); + + let on_type_formatting = project.update(cx, |project, cx| { + project.on_type_format( + buffer.clone(), + buffer_position, + input, + push_to_lsp_host_history, + cx, + ) + }); + Some(cx.spawn_in(window, async move |editor, cx| { + if let Some(transaction) = on_type_formatting.await? { + if push_to_client_history { + buffer + .update(cx, |buffer, _| { + buffer.push_transaction(transaction, Instant::now()); + buffer.finalize_last_transaction(); + }) + .ok(); + } + editor.update(cx, |editor, cx| { + editor.refresh_document_highlights(cx); + })?; + } + Ok(()) + })) + } + + pub fn show_word_completions( + &mut self, + _: &ShowWordCompletions, + window: &mut Window, + cx: &mut Context, + ) { + self.open_completions_menu(true, None, window, cx); + } + + pub fn show_completions( + &mut self, + options: &ShowCompletions, + window: &mut Window, + cx: &mut Context, + ) { + self.open_completions_menu(false, options.trigger.as_deref(), window, cx); + } + + fn open_completions_menu( + &mut self, + ignore_completion_provider: bool, + trigger: Option<&str>, + window: &mut Window, + cx: &mut Context, + ) { + if self.pending_rename.is_some() { + return; + } + if !self.snippet_stack.is_empty() && self.context_menu.borrow().as_ref().is_some() { + return; + } + + let position = self.selections.newest_anchor().head(); + if position.diff_base_anchor.is_some() { + return; + } + let (buffer, buffer_position) = + if let Some(output) = self.buffer.read(cx).text_anchor_for_position(position, cx) { + output + } else { + return; + }; + let buffer_snapshot = buffer.read(cx).snapshot(); + let show_completion_documentation = buffer_snapshot + .settings_at(buffer_position, cx) + .show_completion_documentation; + + let query = Self::completion_query(&self.buffer.read(cx).read(cx), position); + + let trigger_kind = match trigger { + Some(trigger) if buffer.read(cx).completion_triggers().contains(trigger) => { + CompletionTriggerKind::TRIGGER_CHARACTER + } + _ => CompletionTriggerKind::INVOKED, + }; + let completion_context = CompletionContext { + trigger_character: trigger.and_then(|trigger| { + if trigger_kind == CompletionTriggerKind::TRIGGER_CHARACTER { + Some(String::from(trigger)) + } else { + None + } + }), + trigger_kind, + }; + + let (old_range, word_kind) = buffer_snapshot.surrounding_word(buffer_position); + let (old_range, word_to_exclude) = if word_kind == Some(CharKind::Word) { + let word_to_exclude = buffer_snapshot + .text_for_range(old_range.clone()) + .collect::(); + ( + buffer_snapshot.anchor_before(old_range.start) + ..buffer_snapshot.anchor_after(old_range.end), + Some(word_to_exclude), + ) + } else { + (buffer_position..buffer_position, None) + }; + + let completion_settings = language_settings( + buffer_snapshot + .language_at(buffer_position) + .map(|language| language.name()), + buffer_snapshot.file(), + cx, + ) + .completions; + + // The document can be large, so stay in reasonable bounds when searching for words, + // otherwise completion pop-up might be slow to appear. + const WORD_LOOKUP_ROWS: u32 = 5_000; + let buffer_row = text::ToPoint::to_point(&buffer_position, &buffer_snapshot).row; + let min_word_search = buffer_snapshot.clip_point( + Point::new(buffer_row.saturating_sub(WORD_LOOKUP_ROWS), 0), + Bias::Left, + ); + let max_word_search = buffer_snapshot.clip_point( + Point::new(buffer_row + WORD_LOOKUP_ROWS, 0).min(buffer_snapshot.max_point()), + Bias::Right, + ); + let word_search_range = buffer_snapshot.point_to_offset(min_word_search) + ..buffer_snapshot.point_to_offset(max_word_search); + + let provider = self + .completion_provider + .as_ref() + .filter(|_| !ignore_completion_provider); + let skip_digits = query + .as_ref() + .map_or(true, |query| !query.chars().any(|c| c.is_digit(10))); + + let (mut words, provided_completions) = match provider { + Some(provider) => { + let completions = provider.completions( + position.excerpt_id, + &buffer, + buffer_position, + completion_context, + window, + cx, + ); + + let words = match completion_settings.words { + WordsCompletionMode::Disabled => Task::ready(BTreeMap::default()), + WordsCompletionMode::Enabled | WordsCompletionMode::Fallback => cx + .background_spawn(async move { + buffer_snapshot.words_in_range(WordsQuery { + fuzzy_contents: None, + range: word_search_range, + skip_digits, + }) + }), + }; + + (words, completions) + } + None => ( + cx.background_spawn(async move { + buffer_snapshot.words_in_range(WordsQuery { + fuzzy_contents: None, + range: word_search_range, + skip_digits, + }) + }), + Task::ready(Ok(None)), + ), + }; + + let sort_completions = provider + .as_ref() + .map_or(false, |provider| provider.sort_completions()); + + let filter_completions = provider + .as_ref() + .map_or(true, |provider| provider.filter_completions()); + + let snippet_sort_order = EditorSettings::get_global(cx).snippet_sort_order; + + let id = post_inc(&mut self.next_completion_id); + let task = cx.spawn_in(window, async move |editor, cx| { + async move { + editor.update(cx, |this, _| { + this.completion_tasks.retain(|(task_id, _)| *task_id >= id); + })?; + + let mut completions = Vec::new(); + if let Some(provided_completions) = provided_completions.await.log_err().flatten() { + completions.extend(provided_completions); + if completion_settings.words == WordsCompletionMode::Fallback { + words = Task::ready(BTreeMap::default()); + } + } + + let mut words = words.await; + if let Some(word_to_exclude) = &word_to_exclude { + words.remove(word_to_exclude); + } + for lsp_completion in &completions { + words.remove(&lsp_completion.new_text); + } + completions.extend(words.into_iter().map(|(word, word_range)| Completion { + replace_range: old_range.clone(), + new_text: word.clone(), + label: CodeLabel::plain(word, None), + icon_path: None, + documentation: None, + source: CompletionSource::BufferWord { + word_range, + resolved: false, + }, + insert_text_mode: Some(InsertTextMode::AS_IS), + confirm: None, + })); + + let menu = if completions.is_empty() { + None + } else { + let mut menu = CompletionsMenu::new( + id, + sort_completions, + show_completion_documentation, + ignore_completion_provider, + position, + buffer.clone(), + completions.into(), + snippet_sort_order, + ); + + menu.filter( + if filter_completions { + query.as_deref() + } else { + None + }, + cx.background_executor().clone(), + ) + .await; + + menu.visible().then_some(menu) + }; + + editor.update_in(cx, |editor, window, cx| { + match editor.context_menu.borrow().as_ref() { + None => {} + Some(CodeContextMenu::Completions(prev_menu)) => { + if prev_menu.id > id { + return; + } + } + _ => return, + } + + if editor.focus_handle.is_focused(window) && menu.is_some() { + let mut menu = menu.unwrap(); + menu.resolve_visible_completions(editor.completion_provider.as_deref(), cx); + + *editor.context_menu.borrow_mut() = + Some(CodeContextMenu::Completions(menu)); + + if editor.show_edit_predictions_in_menu() { + editor.update_visible_inline_completion(window, cx); + } else { + editor.discard_inline_completion(false, cx); + } + + cx.notify(); + } else if editor.completion_tasks.len() <= 1 { + // If there are no more completion tasks and the last menu was + // empty, we should hide it. + let was_hidden = editor.hide_context_menu(window, cx).is_none(); + // If it was already hidden and we don't show inline + // completions in the menu, we should also show the + // inline-completion when available. + if was_hidden && editor.show_edit_predictions_in_menu() { + editor.update_visible_inline_completion(window, cx); + } + } + })?; + + anyhow::Ok(()) + } + .log_err() + .await + }); + + self.completion_tasks.push((id, task)); + } + + #[cfg(feature = "test-support")] + pub fn current_completions(&self) -> Option> { + let menu = self.context_menu.borrow(); + if let CodeContextMenu::Completions(menu) = menu.as_ref()? { + let completions = menu.completions.borrow(); + Some(completions.to_vec()) + } else { + None + } + } + + pub fn confirm_completion( + &mut self, + action: &ConfirmCompletion, + window: &mut Window, + cx: &mut Context, + ) -> Option>> { + self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + self.do_completion(action.item_ix, CompletionIntent::Complete, window, cx) + } + + pub fn confirm_completion_insert( + &mut self, + _: &ConfirmCompletionInsert, + window: &mut Window, + cx: &mut Context, + ) -> Option>> { + self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + self.do_completion(None, CompletionIntent::CompleteWithInsert, window, cx) + } + + pub fn confirm_completion_replace( + &mut self, + _: &ConfirmCompletionReplace, + window: &mut Window, + cx: &mut Context, + ) -> Option>> { + self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + self.do_completion(None, CompletionIntent::CompleteWithReplace, window, cx) + } + + pub fn compose_completion( + &mut self, + action: &ComposeCompletion, + window: &mut Window, + cx: &mut Context, + ) -> Option>> { + self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + self.do_completion(action.item_ix, CompletionIntent::Compose, window, cx) + } + + fn do_completion( + &mut self, + item_ix: Option, + intent: CompletionIntent, + window: &mut Window, + cx: &mut Context, + ) -> Option>> { + use language::ToOffset as _; + + let CodeContextMenu::Completions(completions_menu) = self.hide_context_menu(window, cx)? + else { + return None; + }; + + let candidate_id = { + let entries = completions_menu.entries.borrow(); + let mat = entries.get(item_ix.unwrap_or(completions_menu.selected_item))?; + if self.show_edit_predictions_in_menu() { + self.discard_inline_completion(true, cx); + } + mat.candidate_id + }; + + let buffer_handle = completions_menu.buffer; + let completion = completions_menu + .completions + .borrow() + .get(candidate_id)? + .clone(); + cx.stop_propagation(); + + let snippet; + let new_text; + if completion.is_snippet() { + snippet = Some(Snippet::parse(&completion.new_text).log_err()?); + new_text = snippet.as_ref().unwrap().text.clone(); + } else { + snippet = None; + new_text = completion.new_text.clone(); + }; + + let replace_range = choose_completion_range(&completion, intent, &buffer_handle, cx); + let buffer = buffer_handle.read(cx); + let snapshot = self.buffer.read(cx).snapshot(cx); + let replace_range_multibuffer = { + let excerpt = snapshot + .excerpt_containing(self.selections.newest_anchor().range()) + .unwrap(); + let multibuffer_anchor = snapshot + .anchor_in_excerpt(excerpt.id(), buffer.anchor_before(replace_range.start)) + .unwrap() + ..snapshot + .anchor_in_excerpt(excerpt.id(), buffer.anchor_before(replace_range.end)) + .unwrap(); + multibuffer_anchor.start.to_offset(&snapshot) + ..multibuffer_anchor.end.to_offset(&snapshot) + }; + let newest_anchor = self.selections.newest_anchor(); + if newest_anchor.head().buffer_id != Some(buffer.remote_id()) { + return None; + } + + let old_text = buffer + .text_for_range(replace_range.clone()) + .collect::(); + let lookbehind = newest_anchor + .start + .text_anchor + .to_offset(buffer) + .saturating_sub(replace_range.start); + let lookahead = replace_range + .end + .saturating_sub(newest_anchor.end.text_anchor.to_offset(buffer)); + let prefix = &old_text[..old_text.len().saturating_sub(lookahead)]; + let suffix = &old_text[lookbehind.min(old_text.len())..]; + + let selections = self.selections.all::(cx); + let mut ranges = Vec::new(); + let mut linked_edits = HashMap::<_, Vec<_>>::default(); + + for selection in &selections { + let range = if selection.id == newest_anchor.id { + replace_range_multibuffer.clone() + } else { + let mut range = selection.range(); + + // if prefix is present, don't duplicate it + if snapshot.contains_str_at(range.start.saturating_sub(lookbehind), prefix) { + range.start = range.start.saturating_sub(lookbehind); + + // if suffix is also present, mimic the newest cursor and replace it + if selection.id != newest_anchor.id + && snapshot.contains_str_at(range.end, suffix) + { + range.end += lookahead; + } + } + range + }; + + ranges.push(range); + + if !self.linked_edit_ranges.is_empty() { + let start_anchor = snapshot.anchor_before(selection.head()); + let end_anchor = snapshot.anchor_after(selection.tail()); + if let Some(ranges) = self + .linked_editing_ranges_for(start_anchor.text_anchor..end_anchor.text_anchor, cx) + { + for (buffer, edits) in ranges { + linked_edits + .entry(buffer.clone()) + .or_default() + .extend(edits.into_iter().map(|range| (range, new_text.to_owned()))); + } + } + } + } + + cx.emit(EditorEvent::InputHandled { + utf16_range_to_replace: None, + text: new_text.clone().into(), + }); + + self.transact(window, cx, |this, window, cx| { + if let Some(mut snippet) = snippet { + snippet.text = new_text.to_string(); + this.insert_snippet(&ranges, snippet, window, cx).log_err(); + } else { + this.buffer.update(cx, |buffer, cx| { + let auto_indent = match completion.insert_text_mode { + Some(InsertTextMode::AS_IS) => None, + _ => this.autoindent_mode.clone(), + }; + let edits = ranges.into_iter().map(|range| (range, new_text.as_str())); + buffer.edit(edits, auto_indent, cx); + }); + } + for (buffer, edits) in linked_edits { + buffer.update(cx, |buffer, cx| { + let snapshot = buffer.snapshot(); + let edits = edits + .into_iter() + .map(|(range, text)| { + use text::ToPoint as TP; + let end_point = TP::to_point(&range.end, &snapshot); + let start_point = TP::to_point(&range.start, &snapshot); + (start_point..end_point, text) + }) + .sorted_by_key(|(range, _)| range.start); + buffer.edit(edits, None, cx); + }) + } + + this.refresh_inline_completion(true, false, window, cx); + }); + + let show_new_completions_on_confirm = completion + .confirm + .as_ref() + .map_or(false, |confirm| confirm(intent, window, cx)); + if show_new_completions_on_confirm { + self.show_completions(&ShowCompletions { trigger: None }, window, cx); + } + + let provider = self.completion_provider.as_ref()?; + drop(completion); + let apply_edits = provider.apply_additional_edits_for_completion( + buffer_handle, + completions_menu.completions.clone(), + candidate_id, + true, + cx, + ); + + let editor_settings = EditorSettings::get_global(cx); + if editor_settings.show_signature_help_after_edits || editor_settings.auto_signature_help { + // After the code completion is finished, users often want to know what signatures are needed. + // so we should automatically call signature_help + self.show_signature_help(&ShowSignatureHelp, window, cx); + } + + Some(cx.foreground_executor().spawn(async move { + apply_edits.await?; + Ok(()) + })) + } + + pub fn toggle_code_actions( + &mut self, + action: &ToggleCodeActions, + window: &mut Window, + cx: &mut Context, + ) { + let quick_launch = action.quick_launch; + let mut context_menu = self.context_menu.borrow_mut(); + if let Some(CodeContextMenu::CodeActions(code_actions)) = context_menu.as_ref() { + if code_actions.deployed_from_indicator == action.deployed_from_indicator { + // Toggle if we're selecting the same one + *context_menu = None; + cx.notify(); + return; + } else { + // Otherwise, clear it and start a new one + *context_menu = None; + cx.notify(); + } + } + drop(context_menu); + let snapshot = self.snapshot(window, cx); + let deployed_from_indicator = action.deployed_from_indicator; + let mut task = self.code_actions_task.take(); + let action = action.clone(); + cx.spawn_in(window, async move |editor, cx| { + while let Some(prev_task) = task { + prev_task.await.log_err(); + task = editor.update(cx, |this, _| this.code_actions_task.take())?; + } + + let spawned_test_task = editor.update_in(cx, |editor, window, cx| { + if editor.focus_handle.is_focused(window) { + let multibuffer_point = action + .deployed_from_indicator + .map(|row| DisplayPoint::new(row, 0).to_point(&snapshot)) + .unwrap_or_else(|| editor.selections.newest::(cx).head()); + let (buffer, buffer_row) = snapshot + .buffer_snapshot + .buffer_line_for_row(MultiBufferRow(multibuffer_point.row)) + .and_then(|(buffer_snapshot, range)| { + editor + .buffer + .read(cx) + .buffer(buffer_snapshot.remote_id()) + .map(|buffer| (buffer, range.start.row)) + })?; + let (_, code_actions) = editor + .available_code_actions + .clone() + .and_then(|(location, code_actions)| { + let snapshot = location.buffer.read(cx).snapshot(); + let point_range = location.range.to_point(&snapshot); + let point_range = point_range.start.row..=point_range.end.row; + if point_range.contains(&buffer_row) { + Some((location, code_actions)) + } else { + None + } + }) + .unzip(); + let buffer_id = buffer.read(cx).remote_id(); + let tasks = editor + .tasks + .get(&(buffer_id, buffer_row)) + .map(|t| Arc::new(t.to_owned())); + if tasks.is_none() && code_actions.is_none() { + return None; + } + + editor.completion_tasks.clear(); + editor.discard_inline_completion(false, cx); + let task_context = + tasks + .as_ref() + .zip(editor.project.clone()) + .map(|(tasks, project)| { + Self::build_tasks_context(&project, &buffer, buffer_row, tasks, cx) + }); + + Some(cx.spawn_in(window, async move |editor, cx| { + let task_context = match task_context { + Some(task_context) => task_context.await, + None => None, + }; + let resolved_tasks = + tasks + .zip(task_context.clone()) + .map(|(tasks, task_context)| ResolvedTasks { + templates: tasks.resolve(&task_context).collect(), + position: snapshot.buffer_snapshot.anchor_before(Point::new( + multibuffer_point.row, + tasks.column, + )), + }); + let spawn_straight_away = quick_launch + && resolved_tasks + .as_ref() + .map_or(false, |tasks| tasks.templates.len() == 1) + && code_actions + .as_ref() + .map_or(true, |actions| actions.is_empty()); + let debug_scenarios = editor.update(cx, |editor, cx| { + if cx.has_flag::() { + maybe!({ + let project = editor.project.as_ref()?; + let dap_store = project.read(cx).dap_store(); + let mut scenarios = vec![]; + let resolved_tasks = resolved_tasks.as_ref()?; + let debug_adapter: SharedString = buffer + .read(cx) + .language()? + .context_provider()? + .debug_adapter()? + .into(); + dap_store.update(cx, |this, cx| { + for (_, task) in &resolved_tasks.templates { + if let Some(scenario) = this + .debug_scenario_for_build_task( + task.resolved.clone(), + SharedString::from( + task.original_task().label.clone(), + ), + debug_adapter.clone(), + cx, + ) + { + scenarios.push(scenario); + } + } + }); + Some(scenarios) + }) + .unwrap_or_default() + } else { + vec![] + } + })?; + if let Ok(task) = editor.update_in(cx, |editor, window, cx| { + *editor.context_menu.borrow_mut() = + Some(CodeContextMenu::CodeActions(CodeActionsMenu { + buffer, + actions: CodeActionContents::new( + resolved_tasks, + code_actions, + debug_scenarios, + task_context.unwrap_or_default(), + ), + selected_item: Default::default(), + scroll_handle: UniformListScrollHandle::default(), + deployed_from_indicator, + })); + if spawn_straight_away { + if let Some(task) = editor.confirm_code_action( + &ConfirmCodeAction { item_ix: Some(0) }, + window, + cx, + ) { + cx.notify(); + return task; + } + } + cx.notify(); + Task::ready(Ok(())) + }) { + task.await + } else { + Ok(()) + } + })) + } else { + Some(Task::ready(Ok(()))) + } + })?; + if let Some(task) = spawned_test_task { + task.await?; + } + + Ok::<_, anyhow::Error>(()) + }) + .detach_and_log_err(cx); + } + + pub fn confirm_code_action( + &mut self, + action: &ConfirmCodeAction, + window: &mut Window, + cx: &mut Context, + ) -> Option>> { + self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + + let actions_menu = + if let CodeContextMenu::CodeActions(menu) = self.hide_context_menu(window, cx)? { + menu + } else { + return None; + }; + + let action_ix = action.item_ix.unwrap_or(actions_menu.selected_item); + let action = actions_menu.actions.get(action_ix)?; + let title = action.label(); + let buffer = actions_menu.buffer; + let workspace = self.workspace()?; + + match action { + CodeActionsItem::Task(task_source_kind, resolved_task) => { + workspace.update(cx, |workspace, cx| { + workspace.schedule_resolved_task( + task_source_kind, + resolved_task, + false, + window, + cx, + ); + + Some(Task::ready(Ok(()))) + }) + } + CodeActionsItem::CodeAction { + excerpt_id, + action, + provider, + } => { + let apply_code_action = + provider.apply_code_action(buffer, action, excerpt_id, true, window, cx); + let workspace = workspace.downgrade(); + Some(cx.spawn_in(window, async move |editor, cx| { + let project_transaction = apply_code_action.await?; + Self::open_project_transaction( + &editor, + workspace, + project_transaction, + title, + cx, + ) + .await + })) + } + CodeActionsItem::DebugScenario(scenario) => { + let context = actions_menu.actions.context.clone(); + + workspace.update(cx, |workspace, cx| { + workspace.start_debug_session(scenario, context, Some(buffer), window, cx); + }); + Some(Task::ready(Ok(()))) + } + } + } + + pub async fn open_project_transaction( + this: &WeakEntity, + workspace: WeakEntity, + transaction: ProjectTransaction, + title: String, + cx: &mut AsyncWindowContext, + ) -> Result<()> { + let mut entries = transaction.0.into_iter().collect::>(); + cx.update(|_, cx| { + entries.sort_unstable_by_key(|(buffer, _)| { + buffer.read(cx).file().map(|f| f.path().clone()) + }); + })?; + + // If the project transaction's edits are all contained within this editor, then + // avoid opening a new editor to display them. + + if let Some((buffer, transaction)) = entries.first() { + if entries.len() == 1 { + let excerpt = this.update(cx, |editor, cx| { + editor + .buffer() + .read(cx) + .excerpt_containing(editor.selections.newest_anchor().head(), cx) + })?; + if let Some((_, excerpted_buffer, excerpt_range)) = excerpt { + if excerpted_buffer == *buffer { + let all_edits_within_excerpt = buffer.read_with(cx, |buffer, _| { + let excerpt_range = excerpt_range.to_offset(buffer); + buffer + .edited_ranges_for_transaction::(transaction) + .all(|range| { + excerpt_range.start <= range.start + && excerpt_range.end >= range.end + }) + })?; + + if all_edits_within_excerpt { + return Ok(()); + } + } + } + } + } else { + return Ok(()); + } + + let mut ranges_to_highlight = Vec::new(); + let excerpt_buffer = cx.new(|cx| { + let mut multibuffer = MultiBuffer::new(Capability::ReadWrite).with_title(title); + for (buffer_handle, transaction) in &entries { + let edited_ranges = buffer_handle + .read(cx) + .edited_ranges_for_transaction::(transaction) + .collect::>(); + let (ranges, _) = multibuffer.set_excerpts_for_path( + PathKey::for_buffer(buffer_handle, cx), + buffer_handle.clone(), + edited_ranges, + DEFAULT_MULTIBUFFER_CONTEXT, + cx, + ); + + ranges_to_highlight.extend(ranges); + } + multibuffer.push_transaction(entries.iter().map(|(b, t)| (b, t)), cx); + multibuffer + })?; + + workspace.update_in(cx, |workspace, window, cx| { + let project = workspace.project().clone(); + let editor = + cx.new(|cx| Editor::for_multibuffer(excerpt_buffer, Some(project), window, cx)); + workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx); + editor.update(cx, |editor, cx| { + editor.highlight_background::( + &ranges_to_highlight, + |theme| theme.editor_highlighted_line_background, + cx, + ); + }); + })?; + + Ok(()) + } + + pub fn clear_code_action_providers(&mut self) { + self.code_action_providers.clear(); + self.available_code_actions.take(); + } + + pub fn add_code_action_provider( + &mut self, + provider: Rc, + window: &mut Window, + cx: &mut Context, + ) { + if self + .code_action_providers + .iter() + .any(|existing_provider| existing_provider.id() == provider.id()) + { + return; + } + + self.code_action_providers.push(provider); + self.refresh_code_actions(window, cx); + } + + pub fn remove_code_action_provider( + &mut self, + id: Arc, + window: &mut Window, + cx: &mut Context, + ) { + self.code_action_providers + .retain(|provider| provider.id() != id); + self.refresh_code_actions(window, cx); + } + + fn refresh_code_actions(&mut self, window: &mut Window, cx: &mut Context) -> Option<()> { + let newest_selection = self.selections.newest_anchor().clone(); + let newest_selection_adjusted = self.selections.newest_adjusted(cx).clone(); + let buffer = self.buffer.read(cx); + if newest_selection.head().diff_base_anchor.is_some() { + return None; + } + let (start_buffer, start) = + buffer.text_anchor_for_position(newest_selection_adjusted.start, cx)?; + let (end_buffer, end) = + buffer.text_anchor_for_position(newest_selection_adjusted.end, cx)?; + if start_buffer != end_buffer { + return None; + } + + self.code_actions_task = Some(cx.spawn_in(window, async move |this, cx| { + cx.background_executor() + .timer(CODE_ACTIONS_DEBOUNCE_TIMEOUT) + .await; + + let (providers, tasks) = this.update_in(cx, |this, window, cx| { + let providers = this.code_action_providers.clone(); + let tasks = this + .code_action_providers + .iter() + .map(|provider| provider.code_actions(&start_buffer, start..end, window, cx)) + .collect::>(); + (providers, tasks) + })?; + + let mut actions = Vec::new(); + for (provider, provider_actions) in + providers.into_iter().zip(future::join_all(tasks).await) + { + if let Some(provider_actions) = provider_actions.log_err() { + actions.extend(provider_actions.into_iter().map(|action| { + AvailableCodeAction { + excerpt_id: newest_selection.start.excerpt_id, + action, + provider: provider.clone(), + } + })); + } + } + + this.update(cx, |this, cx| { + this.available_code_actions = if actions.is_empty() { + None + } else { + Some(( + Location { + buffer: start_buffer, + range: start..end, + }, + actions.into(), + )) + }; + cx.notify(); + }) + })); + None + } + + fn start_inline_blame_timer(&mut self, window: &mut Window, cx: &mut Context) { + if let Some(delay) = ProjectSettings::get_global(cx).git.inline_blame_delay() { + self.show_git_blame_inline = false; + + self.show_git_blame_inline_delay_task = + Some(cx.spawn_in(window, async move |this, cx| { + cx.background_executor().timer(delay).await; + + this.update(cx, |this, cx| { + this.show_git_blame_inline = true; + cx.notify(); + }) + .log_err(); + })); + } + } + + fn show_blame_popover( + &mut self, + blame_entry: &BlameEntry, + position: gpui::Point, + cx: &mut Context, + ) { + if let Some(state) = &mut self.inline_blame_popover { + state.hide_task.take(); + cx.notify(); + } else { + let delay = EditorSettings::get_global(cx).hover_popover_delay; + let show_task = cx.spawn(async move |editor, cx| { + cx.background_executor() + .timer(std::time::Duration::from_millis(delay)) + .await; + editor + .update(cx, |editor, cx| { + if let Some(state) = &mut editor.inline_blame_popover { + state.show_task = None; + cx.notify(); + } + }) + .ok(); + }); + let Some(blame) = self.blame.as_ref() else { + return; + }; + let blame = blame.read(cx); + let details = blame.details_for_entry(&blame_entry); + let markdown = cx.new(|cx| { + Markdown::new( + details + .as_ref() + .map(|message| message.message.clone()) + .unwrap_or_default(), + None, + None, + cx, + ) + }); + self.inline_blame_popover = Some(InlineBlamePopover { + position, + show_task: Some(show_task), + hide_task: None, + popover_bounds: None, + popover_state: InlineBlamePopoverState { + scroll_handle: ScrollHandle::new(), + commit_message: details, + markdown, + }, + }); + } + } + + fn hide_blame_popover(&mut self, cx: &mut Context) { + if let Some(state) = &mut self.inline_blame_popover { + if state.show_task.is_some() { + self.inline_blame_popover.take(); + cx.notify(); + } else { + let hide_task = cx.spawn(async move |editor, cx| { + cx.background_executor() + .timer(std::time::Duration::from_millis(100)) + .await; + editor + .update(cx, |editor, cx| { + editor.inline_blame_popover.take(); + cx.notify(); + }) + .ok(); + }); + state.hide_task = Some(hide_task); + } + } + } + + fn refresh_document_highlights(&mut self, cx: &mut Context) -> Option<()> { + if self.pending_rename.is_some() { + return None; + } + + let provider = self.semantics_provider.clone()?; + let buffer = self.buffer.read(cx); + let newest_selection = self.selections.newest_anchor().clone(); + let cursor_position = newest_selection.head(); + let (cursor_buffer, cursor_buffer_position) = + buffer.text_anchor_for_position(cursor_position, cx)?; + let (tail_buffer, _) = buffer.text_anchor_for_position(newest_selection.tail(), cx)?; + if cursor_buffer != tail_buffer { + return None; + } + let debounce = EditorSettings::get_global(cx).lsp_highlight_debounce; + self.document_highlights_task = Some(cx.spawn(async move |this, cx| { + cx.background_executor() + .timer(Duration::from_millis(debounce)) + .await; + + let highlights = if let Some(highlights) = cx + .update(|cx| { + provider.document_highlights(&cursor_buffer, cursor_buffer_position, cx) + }) + .ok() + .flatten() + { + highlights.await.log_err() + } else { + None + }; + + if let Some(highlights) = highlights { + this.update(cx, |this, cx| { + if this.pending_rename.is_some() { + return; + } + + let buffer_id = cursor_position.buffer_id; + let buffer = this.buffer.read(cx); + if !buffer + .text_anchor_for_position(cursor_position, cx) + .map_or(false, |(buffer, _)| buffer == cursor_buffer) + { + return; + } + + let cursor_buffer_snapshot = cursor_buffer.read(cx); + let mut write_ranges = Vec::new(); + let mut read_ranges = Vec::new(); + for highlight in highlights { + for (excerpt_id, excerpt_range) in + buffer.excerpts_for_buffer(cursor_buffer.read(cx).remote_id(), cx) + { + let start = highlight + .range + .start + .max(&excerpt_range.context.start, cursor_buffer_snapshot); + let end = highlight + .range + .end + .min(&excerpt_range.context.end, cursor_buffer_snapshot); + if start.cmp(&end, cursor_buffer_snapshot).is_ge() { + continue; + } + + let range = Anchor { + buffer_id, + excerpt_id, + text_anchor: start, + diff_base_anchor: None, + }..Anchor { + buffer_id, + excerpt_id, + text_anchor: end, + diff_base_anchor: None, + }; + if highlight.kind == lsp::DocumentHighlightKind::WRITE { + write_ranges.push(range); + } else { + read_ranges.push(range); + } + } + } + + this.highlight_background::( + &read_ranges, + |theme| theme.editor_document_highlight_read_background, + cx, + ); + this.highlight_background::( + &write_ranges, + |theme| theme.editor_document_highlight_write_background, + cx, + ); + cx.notify(); + }) + .log_err(); + } + })); + None + } + + fn prepare_highlight_query_from_selection( + &mut self, + cx: &mut Context, + ) -> Option<(String, Range)> { + if matches!(self.mode, EditorMode::SingleLine { .. }) { + return None; + } + if !EditorSettings::get_global(cx).selection_highlight { + return None; + } + if self.selections.count() != 1 || self.selections.line_mode { + return None; + } + let selection = self.selections.newest::(cx); + if selection.is_empty() || selection.start.row != selection.end.row { + return None; + } + let multi_buffer_snapshot = self.buffer().read(cx).snapshot(cx); + let selection_anchor_range = selection.range().to_anchors(&multi_buffer_snapshot); + let query = multi_buffer_snapshot + .text_for_range(selection_anchor_range.clone()) + .collect::(); + if query.trim().is_empty() { + return None; + } + Some((query, selection_anchor_range)) + } + + fn update_selection_occurrence_highlights( + &mut self, + query_text: String, + query_range: Range, + multi_buffer_range_to_query: Range, + use_debounce: bool, + window: &mut Window, + cx: &mut Context, + ) -> Task<()> { + let multi_buffer_snapshot = self.buffer().read(cx).snapshot(cx); + cx.spawn_in(window, async move |editor, cx| { + if use_debounce { + cx.background_executor() + .timer(SELECTION_HIGHLIGHT_DEBOUNCE_TIMEOUT) + .await; + } + let match_task = cx.background_spawn(async move { + let buffer_ranges = multi_buffer_snapshot + .range_to_buffer_ranges(multi_buffer_range_to_query) + .into_iter() + .filter(|(_, excerpt_visible_range, _)| !excerpt_visible_range.is_empty()); + let mut match_ranges = Vec::new(); + for (buffer_snapshot, search_range, excerpt_id) in buffer_ranges { + match_ranges.extend( + project::search::SearchQuery::text( + query_text.clone(), + false, + false, + false, + Default::default(), + Default::default(), + false, + None, + ) + .unwrap() + .search(&buffer_snapshot, Some(search_range.clone())) + .await + .into_iter() + .filter_map(|match_range| { + let match_start = buffer_snapshot + .anchor_after(search_range.start + match_range.start); + let match_end = + buffer_snapshot.anchor_before(search_range.start + match_range.end); + let match_anchor_range = Anchor::range_in_buffer( + excerpt_id, + buffer_snapshot.remote_id(), + match_start..match_end, + ); + (match_anchor_range != query_range).then_some(match_anchor_range) + }), + ); + } + match_ranges + }); + let match_ranges = match_task.await; + editor + .update_in(cx, |editor, _, cx| { + editor.clear_background_highlights::(cx); + if !match_ranges.is_empty() { + editor.highlight_background::( + &match_ranges, + |theme| theme.editor_document_highlight_bracket_background, + cx, + ) + } + }) + .log_err(); + }) + } + + fn refresh_selected_text_highlights( + &mut self, + on_buffer_edit: bool, + window: &mut Window, + cx: &mut Context, + ) { + let Some((query_text, query_range)) = self.prepare_highlight_query_from_selection(cx) + else { + self.clear_background_highlights::(cx); + self.quick_selection_highlight_task.take(); + self.debounced_selection_highlight_task.take(); + return; + }; + let multi_buffer_snapshot = self.buffer().read(cx).snapshot(cx); + if on_buffer_edit + || self + .quick_selection_highlight_task + .as_ref() + .map_or(true, |(prev_anchor_range, _)| { + prev_anchor_range != &query_range + }) + { + let multi_buffer_visible_start = self + .scroll_manager + .anchor() + .anchor + .to_point(&multi_buffer_snapshot); + let multi_buffer_visible_end = multi_buffer_snapshot.clip_point( + multi_buffer_visible_start + + Point::new(self.visible_line_count().unwrap_or(0.).ceil() as u32, 0), + Bias::Left, + ); + let multi_buffer_visible_range = multi_buffer_visible_start..multi_buffer_visible_end; + self.quick_selection_highlight_task = Some(( + query_range.clone(), + self.update_selection_occurrence_highlights( + query_text.clone(), + query_range.clone(), + multi_buffer_visible_range, + false, + window, + cx, + ), + )); + } + if on_buffer_edit + || self + .debounced_selection_highlight_task + .as_ref() + .map_or(true, |(prev_anchor_range, _)| { + prev_anchor_range != &query_range + }) + { + let multi_buffer_start = multi_buffer_snapshot + .anchor_before(0) + .to_point(&multi_buffer_snapshot); + let multi_buffer_end = multi_buffer_snapshot + .anchor_after(multi_buffer_snapshot.len()) + .to_point(&multi_buffer_snapshot); + let multi_buffer_full_range = multi_buffer_start..multi_buffer_end; + self.debounced_selection_highlight_task = Some(( + query_range.clone(), + self.update_selection_occurrence_highlights( + query_text, + query_range, + multi_buffer_full_range, + true, + window, + cx, + ), + )); + } + } + + pub fn refresh_inline_completion( + &mut self, + debounce: bool, + user_requested: bool, + window: &mut Window, + cx: &mut Context, + ) -> Option<()> { + let provider = self.edit_prediction_provider()?; + let cursor = self.selections.newest_anchor().head(); + let (buffer, cursor_buffer_position) = + self.buffer.read(cx).text_anchor_for_position(cursor, cx)?; + + if !self.edit_predictions_enabled_in_buffer(&buffer, cursor_buffer_position, cx) { + self.discard_inline_completion(false, cx); + return None; + } + + if !user_requested + && (!self.should_show_edit_predictions() + || !self.is_focused(window) + || buffer.read(cx).is_empty()) + { + self.discard_inline_completion(false, cx); + return None; + } + + self.update_visible_inline_completion(window, cx); + provider.refresh( + self.project.clone(), + buffer, + cursor_buffer_position, + debounce, + cx, + ); + Some(()) + } + + fn show_edit_predictions_in_menu(&self) -> bool { + match self.edit_prediction_settings { + EditPredictionSettings::Disabled => false, + EditPredictionSettings::Enabled { show_in_menu, .. } => show_in_menu, + } + } + + pub fn edit_predictions_enabled(&self) -> bool { + match self.edit_prediction_settings { + EditPredictionSettings::Disabled => false, + EditPredictionSettings::Enabled { .. } => true, + } + } + + fn edit_prediction_requires_modifier(&self) -> bool { + match self.edit_prediction_settings { + EditPredictionSettings::Disabled => false, + EditPredictionSettings::Enabled { + preview_requires_modifier, + .. + } => preview_requires_modifier, + } + } + + pub fn update_edit_prediction_settings(&mut self, cx: &mut Context) { + if self.edit_prediction_provider.is_none() { + self.edit_prediction_settings = EditPredictionSettings::Disabled; + } else { + let selection = self.selections.newest_anchor(); + let cursor = selection.head(); + + if let Some((buffer, cursor_buffer_position)) = + self.buffer.read(cx).text_anchor_for_position(cursor, cx) + { + self.edit_prediction_settings = + self.edit_prediction_settings_at_position(&buffer, cursor_buffer_position, cx); + } + } + } + + fn edit_prediction_settings_at_position( + &self, + buffer: &Entity, + buffer_position: language::Anchor, + cx: &App, + ) -> EditPredictionSettings { + if !self.mode.is_full() + || !self.show_inline_completions_override.unwrap_or(true) + || self.inline_completions_disabled_in_scope(buffer, buffer_position, cx) + { + return EditPredictionSettings::Disabled; + } + + let buffer = buffer.read(cx); + + let file = buffer.file(); + + if !language_settings(buffer.language().map(|l| l.name()), file, cx).show_edit_predictions { + return EditPredictionSettings::Disabled; + }; + + let by_provider = matches!( + self.menu_inline_completions_policy, + MenuInlineCompletionsPolicy::ByProvider + ); + + let show_in_menu = by_provider + && self + .edit_prediction_provider + .as_ref() + .map_or(false, |provider| { + provider.provider.show_completions_in_menu() + }); + + let preview_requires_modifier = + all_language_settings(file, cx).edit_predictions_mode() == EditPredictionsMode::Subtle; + + EditPredictionSettings::Enabled { + show_in_menu, + preview_requires_modifier, + } + } + + fn should_show_edit_predictions(&self) -> bool { + self.snippet_stack.is_empty() && self.edit_predictions_enabled() + } + + pub fn edit_prediction_preview_is_active(&self) -> bool { + matches!( + self.edit_prediction_preview, + EditPredictionPreview::Active { .. } + ) + } + + pub fn edit_predictions_enabled_at_cursor(&self, cx: &App) -> bool { + let cursor = self.selections.newest_anchor().head(); + if let Some((buffer, cursor_position)) = + self.buffer.read(cx).text_anchor_for_position(cursor, cx) + { + self.edit_predictions_enabled_in_buffer(&buffer, cursor_position, cx) + } else { + false + } + } + + fn edit_predictions_enabled_in_buffer( + &self, + buffer: &Entity, + buffer_position: language::Anchor, + cx: &App, + ) -> bool { + maybe!({ + if self.read_only(cx) { + return Some(false); + } + let provider = self.edit_prediction_provider()?; + if !provider.is_enabled(&buffer, buffer_position, cx) { + return Some(false); + } + let buffer = buffer.read(cx); + let Some(file) = buffer.file() else { + return Some(true); + }; + let settings = all_language_settings(Some(file), cx); + Some(settings.edit_predictions_enabled_for_file(file, cx)) + }) + .unwrap_or(false) + } + + fn cycle_inline_completion( + &mut self, + direction: Direction, + window: &mut Window, + cx: &mut Context, + ) -> Option<()> { + let provider = self.edit_prediction_provider()?; + let cursor = self.selections.newest_anchor().head(); + let (buffer, cursor_buffer_position) = + self.buffer.read(cx).text_anchor_for_position(cursor, cx)?; + if self.inline_completions_hidden_for_vim_mode || !self.should_show_edit_predictions() { + return None; + } + + provider.cycle(buffer, cursor_buffer_position, direction, cx); + self.update_visible_inline_completion(window, cx); + + Some(()) + } + + pub fn show_inline_completion( + &mut self, + _: &ShowEditPrediction, + window: &mut Window, + cx: &mut Context, + ) { + if !self.has_active_inline_completion() { + self.refresh_inline_completion(false, true, window, cx); + return; + } + + self.update_visible_inline_completion(window, cx); + } + + pub fn display_cursor_names( + &mut self, + _: &DisplayCursorNames, + window: &mut Window, + cx: &mut Context, + ) { + self.show_cursor_names(window, cx); + } + + fn show_cursor_names(&mut self, window: &mut Window, cx: &mut Context) { + self.show_cursor_names = true; + cx.notify(); + cx.spawn_in(window, async move |this, cx| { + cx.background_executor().timer(CURSORS_VISIBLE_FOR).await; + this.update(cx, |this, cx| { + this.show_cursor_names = false; + cx.notify() + }) + .ok() + }) + .detach(); + } + + pub fn next_edit_prediction( + &mut self, + _: &NextEditPrediction, + window: &mut Window, + cx: &mut Context, + ) { + if self.has_active_inline_completion() { + self.cycle_inline_completion(Direction::Next, window, cx); + } else { + let is_copilot_disabled = self + .refresh_inline_completion(false, true, window, cx) + .is_none(); + if is_copilot_disabled { + cx.propagate(); + } + } + } + + pub fn previous_edit_prediction( + &mut self, + _: &PreviousEditPrediction, + window: &mut Window, + cx: &mut Context, + ) { + if self.has_active_inline_completion() { + self.cycle_inline_completion(Direction::Prev, window, cx); + } else { + let is_copilot_disabled = self + .refresh_inline_completion(false, true, window, cx) + .is_none(); + if is_copilot_disabled { + cx.propagate(); + } + } + } + + pub fn accept_edit_prediction( + &mut self, + _: &AcceptEditPrediction, + window: &mut Window, + cx: &mut Context, + ) { + if self.show_edit_predictions_in_menu() { + self.hide_context_menu(window, cx); + } + + let Some(active_inline_completion) = self.active_inline_completion.as_ref() else { + return; + }; + + self.report_inline_completion_event( + active_inline_completion.completion_id.clone(), + true, + cx, + ); + + match &active_inline_completion.completion { + InlineCompletion::Move { target, .. } => { + let target = *target; + + if let Some(position_map) = &self.last_position_map { + if position_map + .visible_row_range + .contains(&target.to_display_point(&position_map.snapshot).row()) + || !self.edit_prediction_requires_modifier() + { + self.unfold_ranges(&[target..target], true, false, cx); + // Note that this is also done in vim's handler of the Tab action. + self.change_selections( + Some(Autoscroll::newest()), + window, + cx, + |selections| { + selections.select_anchor_ranges([target..target]); + }, + ); + self.clear_row_highlights::(); + + self.edit_prediction_preview + .set_previous_scroll_position(None); + } else { + self.edit_prediction_preview + .set_previous_scroll_position(Some( + position_map.snapshot.scroll_anchor, + )); + + self.highlight_rows::( + target..target, + cx.theme().colors().editor_highlighted_line_background, + RowHighlightOptions { + autoscroll: true, + ..Default::default() + }, + cx, + ); + self.request_autoscroll(Autoscroll::fit(), cx); + } + } + } + InlineCompletion::Edit { edits, .. } => { + if let Some(provider) = self.edit_prediction_provider() { + provider.accept(cx); + } + + let snapshot = self.buffer.read(cx).snapshot(cx); + let last_edit_end = edits.last().unwrap().0.end.bias_right(&snapshot); + + self.buffer.update(cx, |buffer, cx| { + buffer.edit(edits.iter().cloned(), None, cx) + }); + + self.change_selections(None, window, cx, |s| { + s.select_anchor_ranges([last_edit_end..last_edit_end]) + }); + + self.update_visible_inline_completion(window, cx); + if self.active_inline_completion.is_none() { + self.refresh_inline_completion(true, true, window, cx); + } + + cx.notify(); + } + } + + self.edit_prediction_requires_modifier_in_indent_conflict = false; + } + + pub fn accept_partial_inline_completion( + &mut self, + _: &AcceptPartialEditPrediction, + window: &mut Window, + cx: &mut Context, + ) { + let Some(active_inline_completion) = self.active_inline_completion.as_ref() else { + return; + }; + if self.selections.count() != 1 { + return; + } + + self.report_inline_completion_event( + active_inline_completion.completion_id.clone(), + true, + cx, + ); + + match &active_inline_completion.completion { + InlineCompletion::Move { target, .. } => { + let target = *target; + self.change_selections(Some(Autoscroll::newest()), window, cx, |selections| { + selections.select_anchor_ranges([target..target]); + }); + } + InlineCompletion::Edit { edits, .. } => { + // Find an insertion that starts at the cursor position. + let snapshot = self.buffer.read(cx).snapshot(cx); + let cursor_offset = self.selections.newest::(cx).head(); + let insertion = edits.iter().find_map(|(range, text)| { + let range = range.to_offset(&snapshot); + if range.is_empty() && range.start == cursor_offset { + Some(text) + } else { + None + } + }); + + if let Some(text) = insertion { + let mut partial_completion = text + .chars() + .by_ref() + .take_while(|c| c.is_alphabetic()) + .collect::(); + if partial_completion.is_empty() { + partial_completion = text + .chars() + .by_ref() + .take_while(|c| c.is_whitespace() || !c.is_alphabetic()) + .collect::(); + } + + cx.emit(EditorEvent::InputHandled { + utf16_range_to_replace: None, + text: partial_completion.clone().into(), + }); + + self.insert_with_autoindent_mode(&partial_completion, None, window, cx); + + self.refresh_inline_completion(true, true, window, cx); + cx.notify(); + } else { + self.accept_edit_prediction(&Default::default(), window, cx); + } + } + } + } + + fn discard_inline_completion( + &mut self, + should_report_inline_completion_event: bool, + cx: &mut Context, + ) -> bool { + if should_report_inline_completion_event { + let completion_id = self + .active_inline_completion + .as_ref() + .and_then(|active_completion| active_completion.completion_id.clone()); + + self.report_inline_completion_event(completion_id, false, cx); + } + + if let Some(provider) = self.edit_prediction_provider() { + provider.discard(cx); + } + + self.take_active_inline_completion(cx) + } + + fn report_inline_completion_event(&self, id: Option, accepted: bool, cx: &App) { + let Some(provider) = self.edit_prediction_provider() else { + return; + }; + + let Some((_, buffer, _)) = self + .buffer + .read(cx) + .excerpt_containing(self.selections.newest_anchor().head(), cx) + else { + return; + }; + + let extension = buffer + .read(cx) + .file() + .and_then(|file| Some(file.path().extension()?.to_string_lossy().to_string())); + + let event_type = match accepted { + true => "Edit Prediction Accepted", + false => "Edit Prediction Discarded", + }; + telemetry::event!( + event_type, + provider = provider.name(), + prediction_id = id, + suggestion_accepted = accepted, + file_extension = extension, + ); + } + + pub fn has_active_inline_completion(&self) -> bool { + self.active_inline_completion.is_some() + } + + fn take_active_inline_completion(&mut self, cx: &mut Context) -> bool { + let Some(active_inline_completion) = self.active_inline_completion.take() else { + return false; + }; + + self.splice_inlays(&active_inline_completion.inlay_ids, Default::default(), cx); + self.clear_highlights::(cx); + self.stale_inline_completion_in_menu = Some(active_inline_completion); + true + } + + /// Returns true when we're displaying the edit prediction popover below the cursor + /// like we are not previewing and the LSP autocomplete menu is visible + /// or we are in `when_holding_modifier` mode. + pub fn edit_prediction_visible_in_cursor_popover(&self, has_completion: bool) -> bool { + if self.edit_prediction_preview_is_active() + || !self.show_edit_predictions_in_menu() + || !self.edit_predictions_enabled() + { + return false; + } + + if self.has_visible_completions_menu() { + return true; + } + + has_completion && self.edit_prediction_requires_modifier() + } + + fn handle_modifiers_changed( + &mut self, + modifiers: Modifiers, + position_map: &PositionMap, + window: &mut Window, + cx: &mut Context, + ) { + if self.show_edit_predictions_in_menu() { + self.update_edit_prediction_preview(&modifiers, window, cx); + } + + self.update_selection_mode(&modifiers, position_map, window, cx); + + let mouse_position = window.mouse_position(); + if !position_map.text_hitbox.is_hovered(window) { + return; + } + + self.update_hovered_link( + position_map.point_for_position(mouse_position), + &position_map.snapshot, + modifiers, + window, + cx, + ) + } + + fn update_selection_mode( + &mut self, + modifiers: &Modifiers, + position_map: &PositionMap, + window: &mut Window, + cx: &mut Context, + ) { + if modifiers != &COLUMNAR_SELECTION_MODIFIERS || self.selections.pending.is_none() { + return; + } + + let mouse_position = window.mouse_position(); + let point_for_position = position_map.point_for_position(mouse_position); + let position = point_for_position.previous_valid; + + self.select( + SelectPhase::BeginColumnar { + position, + reset: false, + goal_column: point_for_position.exact_unclipped.column(), + }, + window, + cx, + ); + } + + fn update_edit_prediction_preview( + &mut self, + modifiers: &Modifiers, + window: &mut Window, + cx: &mut Context, + ) { + let accept_keybind = self.accept_edit_prediction_keybind(window, cx); + let Some(accept_keystroke) = accept_keybind.keystroke() else { + return; + }; + + if &accept_keystroke.modifiers == modifiers && accept_keystroke.modifiers.modified() { + if matches!( + self.edit_prediction_preview, + EditPredictionPreview::Inactive { .. } + ) { + self.edit_prediction_preview = EditPredictionPreview::Active { + previous_scroll_position: None, + since: Instant::now(), + }; + + self.update_visible_inline_completion(window, cx); + cx.notify(); + } + } else if let EditPredictionPreview::Active { + previous_scroll_position, + since, + } = self.edit_prediction_preview + { + if let (Some(previous_scroll_position), Some(position_map)) = + (previous_scroll_position, self.last_position_map.as_ref()) + { + self.set_scroll_position( + previous_scroll_position + .scroll_position(&position_map.snapshot.display_snapshot), + window, + cx, + ); + } + + self.edit_prediction_preview = EditPredictionPreview::Inactive { + released_too_fast: since.elapsed() < Duration::from_millis(200), + }; + self.clear_row_highlights::(); + self.update_visible_inline_completion(window, cx); + cx.notify(); + } + } + + fn update_visible_inline_completion( + &mut self, + _window: &mut Window, + cx: &mut Context, + ) -> Option<()> { + let selection = self.selections.newest_anchor(); + let cursor = selection.head(); + let multibuffer = self.buffer.read(cx).snapshot(cx); + let offset_selection = selection.map(|endpoint| endpoint.to_offset(&multibuffer)); + let excerpt_id = cursor.excerpt_id; + + let show_in_menu = self.show_edit_predictions_in_menu(); + let completions_menu_has_precedence = !show_in_menu + && (self.context_menu.borrow().is_some() + || (!self.completion_tasks.is_empty() && !self.has_active_inline_completion())); + + if completions_menu_has_precedence + || !offset_selection.is_empty() + || self + .active_inline_completion + .as_ref() + .map_or(false, |completion| { + let invalidation_range = completion.invalidation_range.to_offset(&multibuffer); + let invalidation_range = invalidation_range.start..=invalidation_range.end; + !invalidation_range.contains(&offset_selection.head()) + }) + { + self.discard_inline_completion(false, cx); + return None; + } + + self.take_active_inline_completion(cx); + let Some(provider) = self.edit_prediction_provider() else { + self.edit_prediction_settings = EditPredictionSettings::Disabled; + return None; + }; + + let (buffer, cursor_buffer_position) = + self.buffer.read(cx).text_anchor_for_position(cursor, cx)?; + + self.edit_prediction_settings = + self.edit_prediction_settings_at_position(&buffer, cursor_buffer_position, cx); + + self.edit_prediction_indent_conflict = multibuffer.is_line_whitespace_upto(cursor); + + if self.edit_prediction_indent_conflict { + let cursor_point = cursor.to_point(&multibuffer); + + let indents = multibuffer.suggested_indents(cursor_point.row..cursor_point.row + 1, cx); + + if let Some((_, indent)) = indents.iter().next() { + if indent.len == cursor_point.column { + self.edit_prediction_indent_conflict = false; + } + } + } + + let inline_completion = provider.suggest(&buffer, cursor_buffer_position, cx)?; + let edits = inline_completion + .edits + .into_iter() + .flat_map(|(range, new_text)| { + let start = multibuffer.anchor_in_excerpt(excerpt_id, range.start)?; + let end = multibuffer.anchor_in_excerpt(excerpt_id, range.end)?; + Some((start..end, new_text)) + }) + .collect::>(); + if edits.is_empty() { + return None; + } + + let first_edit_start = edits.first().unwrap().0.start; + let first_edit_start_point = first_edit_start.to_point(&multibuffer); + let edit_start_row = first_edit_start_point.row.saturating_sub(2); + + let last_edit_end = edits.last().unwrap().0.end; + let last_edit_end_point = last_edit_end.to_point(&multibuffer); + let edit_end_row = cmp::min(multibuffer.max_point().row, last_edit_end_point.row + 2); + + let cursor_row = cursor.to_point(&multibuffer).row; + + let snapshot = multibuffer.buffer_for_excerpt(excerpt_id).cloned()?; + + let mut inlay_ids = Vec::new(); + let invalidation_row_range; + let move_invalidation_row_range = if cursor_row < edit_start_row { + Some(cursor_row..edit_end_row) + } else if cursor_row > edit_end_row { + Some(edit_start_row..cursor_row) + } else { + None + }; + let is_move = + move_invalidation_row_range.is_some() || self.inline_completions_hidden_for_vim_mode; + let completion = if is_move { + invalidation_row_range = + move_invalidation_row_range.unwrap_or(edit_start_row..edit_end_row); + let target = first_edit_start; + InlineCompletion::Move { target, snapshot } + } else { + let show_completions_in_buffer = !self.edit_prediction_visible_in_cursor_popover(true) + && !self.inline_completions_hidden_for_vim_mode; + + if show_completions_in_buffer { + if edits + .iter() + .all(|(range, _)| range.to_offset(&multibuffer).is_empty()) + { + let mut inlays = Vec::new(); + for (range, new_text) in &edits { + let inlay = Inlay::inline_completion( + post_inc(&mut self.next_inlay_id), + range.start, + new_text.as_str(), + ); + inlay_ids.push(inlay.id); + inlays.push(inlay); + } + + self.splice_inlays(&[], inlays, cx); + } else { + let background_color = cx.theme().status().deleted_background; + self.highlight_text::( + edits.iter().map(|(range, _)| range.clone()).collect(), + HighlightStyle { + background_color: Some(background_color), + ..Default::default() + }, + cx, + ); + } + } + + invalidation_row_range = edit_start_row..edit_end_row; + + let display_mode = if all_edits_insertions_or_deletions(&edits, &multibuffer) { + if provider.show_tab_accept_marker() { + EditDisplayMode::TabAccept + } else { + EditDisplayMode::Inline + } + } else { + EditDisplayMode::DiffPopover + }; + + InlineCompletion::Edit { + edits, + edit_preview: inline_completion.edit_preview, + display_mode, + snapshot, + } + }; + + let invalidation_range = multibuffer + .anchor_before(Point::new(invalidation_row_range.start, 0)) + ..multibuffer.anchor_after(Point::new( + invalidation_row_range.end, + multibuffer.line_len(MultiBufferRow(invalidation_row_range.end)), + )); + + self.stale_inline_completion_in_menu = None; + self.active_inline_completion = Some(InlineCompletionState { + inlay_ids, + completion, + completion_id: inline_completion.id, + invalidation_range, + }); + + cx.notify(); + + Some(()) + } + + pub fn edit_prediction_provider(&self) -> Option> { + Some(self.edit_prediction_provider.as_ref()?.provider.clone()) + } + + fn render_code_actions_indicator( + &self, + _style: &EditorStyle, + row: DisplayRow, + is_active: bool, + breakpoint: Option<&(Anchor, Breakpoint)>, + cx: &mut Context, + ) -> Option { + let color = Color::Muted; + let position = breakpoint.as_ref().map(|(anchor, _)| *anchor); + let show_tooltip = !self.context_menu_visible(); + + if self.available_code_actions.is_some() { + Some( + IconButton::new("code_actions_indicator", ui::IconName::Bolt) + .shape(ui::IconButtonShape::Square) + .icon_size(IconSize::XSmall) + .icon_color(color) + .toggle_state(is_active) + .when(show_tooltip, |this| { + this.tooltip({ + let focus_handle = self.focus_handle.clone(); + move |window, cx| { + Tooltip::for_action_in( + "Toggle Code Actions", + &ToggleCodeActions { + deployed_from_indicator: None, + quick_launch: false, + }, + &focus_handle, + window, + cx, + ) + } + }) + }) + .on_click(cx.listener(move |editor, e: &ClickEvent, window, cx| { + let quick_launch = e.down.button == MouseButton::Left; + window.focus(&editor.focus_handle(cx)); + editor.toggle_code_actions( + &ToggleCodeActions { + deployed_from_indicator: Some(row), + quick_launch, + }, + window, + cx, + ); + })) + .on_right_click(cx.listener(move |editor, event: &ClickEvent, window, cx| { + editor.set_breakpoint_context_menu( + row, + position, + event.down.position, + window, + cx, + ); + })), + ) + } else { + None + } + } + + fn clear_tasks(&mut self) { + self.tasks.clear() + } + + fn insert_tasks(&mut self, key: (BufferId, BufferRow), value: RunnableTasks) { + if self.tasks.insert(key, value).is_some() { + // This case should hopefully be rare, but just in case... + log::error!( + "multiple different run targets found on a single line, only the last target will be rendered" + ) + } + } + + /// Get all display points of breakpoints that will be rendered within editor + /// + /// This function is used to handle overlaps between breakpoints and Code action/runner symbol. + /// It's also used to set the color of line numbers with breakpoints to the breakpoint color. + /// TODO debugger: Use this function to color toggle symbols that house nested breakpoints + fn active_breakpoints( + &self, + range: Range, + window: &mut Window, + cx: &mut Context, + ) -> HashMap { + let mut breakpoint_display_points = HashMap::default(); + + let Some(breakpoint_store) = self.breakpoint_store.clone() else { + return breakpoint_display_points; + }; + + let snapshot = self.snapshot(window, cx); + + let multi_buffer_snapshot = &snapshot.display_snapshot.buffer_snapshot; + let Some(project) = self.project.as_ref() else { + return breakpoint_display_points; + }; + + let range = snapshot.display_point_to_point(DisplayPoint::new(range.start, 0), Bias::Left) + ..snapshot.display_point_to_point(DisplayPoint::new(range.end, 0), Bias::Right); + + for (buffer_snapshot, range, excerpt_id) in + multi_buffer_snapshot.range_to_buffer_ranges(range) + { + let Some(buffer) = project.read_with(cx, |this, cx| { + this.buffer_for_id(buffer_snapshot.remote_id(), cx) + }) else { + continue; + }; + let breakpoints = breakpoint_store.read(cx).breakpoints( + &buffer, + Some( + buffer_snapshot.anchor_before(range.start) + ..buffer_snapshot.anchor_after(range.end), + ), + buffer_snapshot, + cx, + ); + for (anchor, breakpoint) in breakpoints { + let multi_buffer_anchor = + Anchor::in_buffer(excerpt_id, buffer_snapshot.remote_id(), *anchor); + let position = multi_buffer_anchor + .to_point(&multi_buffer_snapshot) + .to_display_point(&snapshot); + + breakpoint_display_points + .insert(position.row(), (multi_buffer_anchor, breakpoint.clone())); + } + } + + breakpoint_display_points + } + + fn breakpoint_context_menu( + &self, + anchor: Anchor, + window: &mut Window, + cx: &mut Context, + ) -> Entity { + let weak_editor = cx.weak_entity(); + let focus_handle = self.focus_handle(cx); + + let row = self + .buffer + .read(cx) + .snapshot(cx) + .summary_for_anchor::(&anchor) + .row; + + let breakpoint = self + .breakpoint_at_row(row, window, cx) + .map(|(anchor, bp)| (anchor, Arc::from(bp))); + + let log_breakpoint_msg = if breakpoint.as_ref().is_some_and(|bp| bp.1.message.is_some()) { + "Edit Log Breakpoint" + } else { + "Set Log Breakpoint" + }; + + let condition_breakpoint_msg = if breakpoint + .as_ref() + .is_some_and(|bp| bp.1.condition.is_some()) + { + "Edit Condition Breakpoint" + } else { + "Set Condition Breakpoint" + }; + + let hit_condition_breakpoint_msg = if breakpoint + .as_ref() + .is_some_and(|bp| bp.1.hit_condition.is_some()) + { + "Edit Hit Condition Breakpoint" + } else { + "Set Hit Condition Breakpoint" + }; + + let set_breakpoint_msg = if breakpoint.as_ref().is_some() { + "Unset Breakpoint" + } else { + "Set Breakpoint" + }; + + let run_to_cursor = command_palette_hooks::CommandPaletteFilter::try_global(cx) + .map_or(false, |filter| !filter.is_hidden(&DebuggerRunToCursor)); + + let toggle_state_msg = breakpoint.as_ref().map_or(None, |bp| match bp.1.state { + BreakpointState::Enabled => Some("Disable"), + BreakpointState::Disabled => Some("Enable"), + }); + + let (anchor, breakpoint) = + breakpoint.unwrap_or_else(|| (anchor, Arc::new(Breakpoint::new_standard()))); + + ui::ContextMenu::build(window, cx, |menu, _, _cx| { + menu.on_blur_subscription(Subscription::new(|| {})) + .context(focus_handle) + .when(run_to_cursor, |this| { + let weak_editor = weak_editor.clone(); + this.entry("Run to cursor", None, move |window, cx| { + weak_editor + .update(cx, |editor, cx| { + editor.change_selections(None, window, cx, |s| { + s.select_ranges([Point::new(row, 0)..Point::new(row, 0)]) + }); + }) + .ok(); + + window.dispatch_action(Box::new(DebuggerRunToCursor), cx); + }) + .separator() + }) + .when_some(toggle_state_msg, |this, msg| { + this.entry(msg, None, { + let weak_editor = weak_editor.clone(); + let breakpoint = breakpoint.clone(); + move |_window, cx| { + weak_editor + .update(cx, |this, cx| { + this.edit_breakpoint_at_anchor( + anchor, + breakpoint.as_ref().clone(), + BreakpointEditAction::InvertState, + cx, + ); + }) + .log_err(); + } + }) + }) + .entry(set_breakpoint_msg, None, { + let weak_editor = weak_editor.clone(); + let breakpoint = breakpoint.clone(); + move |_window, cx| { + weak_editor + .update(cx, |this, cx| { + this.edit_breakpoint_at_anchor( + anchor, + breakpoint.as_ref().clone(), + BreakpointEditAction::Toggle, + cx, + ); + }) + .log_err(); + } + }) + .entry(log_breakpoint_msg, None, { + let breakpoint = breakpoint.clone(); + let weak_editor = weak_editor.clone(); + move |window, cx| { + weak_editor + .update(cx, |this, cx| { + this.add_edit_breakpoint_block( + anchor, + breakpoint.as_ref(), + BreakpointPromptEditAction::Log, + window, + cx, + ); + }) + .log_err(); + } + }) + .entry(condition_breakpoint_msg, None, { + let breakpoint = breakpoint.clone(); + let weak_editor = weak_editor.clone(); + move |window, cx| { + weak_editor + .update(cx, |this, cx| { + this.add_edit_breakpoint_block( + anchor, + breakpoint.as_ref(), + BreakpointPromptEditAction::Condition, + window, + cx, + ); + }) + .log_err(); + } + }) + .entry(hit_condition_breakpoint_msg, None, move |window, cx| { + weak_editor + .update(cx, |this, cx| { + this.add_edit_breakpoint_block( + anchor, + breakpoint.as_ref(), + BreakpointPromptEditAction::HitCondition, + window, + cx, + ); + }) + .log_err(); + }) + }) + } + + fn render_breakpoint( + &self, + position: Anchor, + row: DisplayRow, + breakpoint: &Breakpoint, + cx: &mut Context, + ) -> IconButton { + // Is it a breakpoint that shows up when hovering over gutter? + let (is_phantom, collides_with_existing) = self.gutter_breakpoint_indicator.0.map_or( + (false, false), + |PhantomBreakpointIndicator { + is_active, + display_row, + collides_with_existing_breakpoint, + }| { + ( + is_active && display_row == row, + collides_with_existing_breakpoint, + ) + }, + ); + + let (color, icon) = { + let icon = match (&breakpoint.message.is_some(), breakpoint.is_disabled()) { + (false, false) => ui::IconName::DebugBreakpoint, + (true, false) => ui::IconName::DebugLogBreakpoint, + (false, true) => ui::IconName::DebugDisabledBreakpoint, + (true, true) => ui::IconName::DebugDisabledLogBreakpoint, + }; + + let color = if is_phantom { + Color::Hint + } else { + Color::Debugger + }; + + (color, icon) + }; + + let breakpoint = Arc::from(breakpoint.clone()); + + let alt_as_text = gpui::Keystroke { + modifiers: Modifiers::secondary_key(), + ..Default::default() + }; + let primary_action_text = if breakpoint.is_disabled() { + "enable" + } else if is_phantom && !collides_with_existing { + "set" + } else { + "unset" + }; + let mut primary_text = format!("Click to {primary_action_text}"); + if collides_with_existing && !breakpoint.is_disabled() { + use std::fmt::Write; + write!(primary_text, ", {alt_as_text}-click to disable").ok(); + } + let primary_text = SharedString::from(primary_text); + let focus_handle = self.focus_handle.clone(); + IconButton::new(("breakpoint_indicator", row.0 as usize), icon) + .icon_size(IconSize::XSmall) + .size(ui::ButtonSize::None) + .icon_color(color) + .style(ButtonStyle::Transparent) + .on_click(cx.listener({ + let breakpoint = breakpoint.clone(); + + move |editor, event: &ClickEvent, window, cx| { + let edit_action = if event.modifiers().platform || breakpoint.is_disabled() { + BreakpointEditAction::InvertState + } else { + BreakpointEditAction::Toggle + }; + + window.focus(&editor.focus_handle(cx)); + editor.edit_breakpoint_at_anchor( + position, + breakpoint.as_ref().clone(), + edit_action, + cx, + ); + } + })) + .on_right_click(cx.listener(move |editor, event: &ClickEvent, window, cx| { + editor.set_breakpoint_context_menu( + row, + Some(position), + event.down.position, + window, + cx, + ); + })) + .tooltip(move |window, cx| { + Tooltip::with_meta_in( + primary_text.clone(), + None, + "Right-click for more options", + &focus_handle, + window, + cx, + ) + }) + } + + fn build_tasks_context( + project: &Entity, + buffer: &Entity, + buffer_row: u32, + tasks: &Arc, + cx: &mut Context, + ) -> Task> { + let position = Point::new(buffer_row, tasks.column); + let range_start = buffer.read(cx).anchor_at(position, Bias::Right); + let location = Location { + buffer: buffer.clone(), + range: range_start..range_start, + }; + // Fill in the environmental variables from the tree-sitter captures + let mut captured_task_variables = TaskVariables::default(); + for (capture_name, value) in tasks.extra_variables.clone() { + captured_task_variables.insert( + task::VariableName::Custom(capture_name.into()), + value.clone(), + ); + } + project.update(cx, |project, cx| { + project.task_store().update(cx, |task_store, cx| { + task_store.task_context_for_location(captured_task_variables, location, cx) + }) + }) + } + + pub fn spawn_nearest_task( + &mut self, + action: &SpawnNearestTask, + window: &mut Window, + cx: &mut Context, + ) { + let Some((workspace, _)) = self.workspace.clone() else { + return; + }; + let Some(project) = self.project.clone() else { + return; + }; + + // Try to find a closest, enclosing node using tree-sitter that has a + // task + let Some((buffer, buffer_row, tasks)) = self + .find_enclosing_node_task(cx) + // Or find the task that's closest in row-distance. + .or_else(|| self.find_closest_task(cx)) + else { + return; + }; + + let reveal_strategy = action.reveal; + let task_context = Self::build_tasks_context(&project, &buffer, buffer_row, &tasks, cx); + cx.spawn_in(window, async move |_, cx| { + let context = task_context.await?; + let (task_source_kind, mut resolved_task) = tasks.resolve(&context).next()?; + + let resolved = &mut resolved_task.resolved; + resolved.reveal = reveal_strategy; + + workspace + .update_in(cx, |workspace, window, cx| { + workspace.schedule_resolved_task( + task_source_kind, + resolved_task, + false, + window, + cx, + ); + }) + .ok() + }) + .detach(); + } + + fn find_closest_task( + &mut self, + cx: &mut Context, + ) -> Option<(Entity, u32, Arc)> { + let cursor_row = self.selections.newest_adjusted(cx).head().row; + + let ((buffer_id, row), tasks) = self + .tasks + .iter() + .min_by_key(|((_, row), _)| cursor_row.abs_diff(*row))?; + + let buffer = self.buffer.read(cx).buffer(*buffer_id)?; + let tasks = Arc::new(tasks.to_owned()); + Some((buffer, *row, tasks)) + } + + fn find_enclosing_node_task( + &mut self, + cx: &mut Context, + ) -> Option<(Entity, u32, Arc)> { + let snapshot = self.buffer.read(cx).snapshot(cx); + let offset = self.selections.newest::(cx).head(); + let excerpt = snapshot.excerpt_containing(offset..offset)?; + let buffer_id = excerpt.buffer().remote_id(); + + let layer = excerpt.buffer().syntax_layer_at(offset)?; + let mut cursor = layer.node().walk(); + + while cursor.goto_first_child_for_byte(offset).is_some() { + if cursor.node().end_byte() == offset { + cursor.goto_next_sibling(); + } + } + + // Ascend to the smallest ancestor that contains the range and has a task. + loop { + let node = cursor.node(); + let node_range = node.byte_range(); + let symbol_start_row = excerpt.buffer().offset_to_point(node.start_byte()).row; + + // Check if this node contains our offset + if node_range.start <= offset && node_range.end >= offset { + // If it contains offset, check for task + if let Some(tasks) = self.tasks.get(&(buffer_id, symbol_start_row)) { + let buffer = self.buffer.read(cx).buffer(buffer_id)?; + return Some((buffer, symbol_start_row, Arc::new(tasks.to_owned()))); + } + } + + if !cursor.goto_parent() { + break; + } + } + None + } + + fn render_run_indicator( + &self, + _style: &EditorStyle, + is_active: bool, + row: DisplayRow, + breakpoint: Option<(Anchor, Breakpoint)>, + cx: &mut Context, + ) -> IconButton { + let color = Color::Muted; + let position = breakpoint.as_ref().map(|(anchor, _)| *anchor); + + IconButton::new(("run_indicator", row.0 as usize), ui::IconName::Play) + .shape(ui::IconButtonShape::Square) + .icon_size(IconSize::XSmall) + .icon_color(color) + .toggle_state(is_active) + .on_click(cx.listener(move |editor, e: &ClickEvent, window, cx| { + let quick_launch = e.down.button == MouseButton::Left; + window.focus(&editor.focus_handle(cx)); + editor.toggle_code_actions( + &ToggleCodeActions { + deployed_from_indicator: Some(row), + quick_launch, + }, + window, + cx, + ); + })) + .on_right_click(cx.listener(move |editor, event: &ClickEvent, window, cx| { + editor.set_breakpoint_context_menu(row, position, event.down.position, window, cx); + })) + } + + pub fn context_menu_visible(&self) -> bool { + !self.edit_prediction_preview_is_active() + && self + .context_menu + .borrow() + .as_ref() + .map_or(false, |menu| menu.visible()) + } + + fn context_menu_origin(&self) -> Option { + self.context_menu + .borrow() + .as_ref() + .map(|menu| menu.origin()) + } + + pub fn set_context_menu_options(&mut self, options: ContextMenuOptions) { + self.context_menu_options = Some(options); + } + + const EDIT_PREDICTION_POPOVER_PADDING_X: Pixels = Pixels(24.); + const EDIT_PREDICTION_POPOVER_PADDING_Y: Pixels = Pixels(2.); + + fn render_edit_prediction_popover( + &mut self, + text_bounds: &Bounds, + content_origin: gpui::Point, + editor_snapshot: &EditorSnapshot, + visible_row_range: Range, + scroll_top: f32, + scroll_bottom: f32, + line_layouts: &[LineWithInvisibles], + line_height: Pixels, + scroll_pixel_position: gpui::Point, + newest_selection_head: Option, + editor_width: Pixels, + style: &EditorStyle, + window: &mut Window, + cx: &mut App, + ) -> Option<(AnyElement, gpui::Point)> { + let active_inline_completion = self.active_inline_completion.as_ref()?; + + if self.edit_prediction_visible_in_cursor_popover(true) { + return None; + } + + match &active_inline_completion.completion { + InlineCompletion::Move { target, .. } => { + let target_display_point = target.to_display_point(editor_snapshot); + + if self.edit_prediction_requires_modifier() { + if !self.edit_prediction_preview_is_active() { + return None; + } + + self.render_edit_prediction_modifier_jump_popover( + text_bounds, + content_origin, + visible_row_range, + line_layouts, + line_height, + scroll_pixel_position, + newest_selection_head, + target_display_point, + window, + cx, + ) + } else { + self.render_edit_prediction_eager_jump_popover( + text_bounds, + content_origin, + editor_snapshot, + visible_row_range, + scroll_top, + scroll_bottom, + line_height, + scroll_pixel_position, + target_display_point, + editor_width, + window, + cx, + ) + } + } + InlineCompletion::Edit { + display_mode: EditDisplayMode::Inline, + .. + } => None, + InlineCompletion::Edit { + display_mode: EditDisplayMode::TabAccept, + edits, + .. + } => { + let range = &edits.first()?.0; + let target_display_point = range.end.to_display_point(editor_snapshot); + + self.render_edit_prediction_end_of_line_popover( + "Accept", + editor_snapshot, + visible_row_range, + target_display_point, + line_height, + scroll_pixel_position, + content_origin, + editor_width, + window, + cx, + ) + } + InlineCompletion::Edit { + edits, + edit_preview, + display_mode: EditDisplayMode::DiffPopover, + snapshot, + } => self.render_edit_prediction_diff_popover( + text_bounds, + content_origin, + editor_snapshot, + visible_row_range, + line_layouts, + line_height, + scroll_pixel_position, + newest_selection_head, + editor_width, + style, + edits, + edit_preview, + snapshot, + window, + cx, + ), + } + } + + fn render_edit_prediction_modifier_jump_popover( + &mut self, + text_bounds: &Bounds, + content_origin: gpui::Point, + visible_row_range: Range, + line_layouts: &[LineWithInvisibles], + line_height: Pixels, + scroll_pixel_position: gpui::Point, + newest_selection_head: Option, + target_display_point: DisplayPoint, + window: &mut Window, + cx: &mut App, + ) -> Option<(AnyElement, gpui::Point)> { + let scrolled_content_origin = + content_origin - gpui::Point::new(scroll_pixel_position.x, Pixels(0.0)); + + const SCROLL_PADDING_Y: Pixels = px(12.); + + if target_display_point.row() < visible_row_range.start { + return self.render_edit_prediction_scroll_popover( + |_| SCROLL_PADDING_Y, + IconName::ArrowUp, + visible_row_range, + line_layouts, + newest_selection_head, + scrolled_content_origin, + window, + cx, + ); + } else if target_display_point.row() >= visible_row_range.end { + return self.render_edit_prediction_scroll_popover( + |size| text_bounds.size.height - size.height - SCROLL_PADDING_Y, + IconName::ArrowDown, + visible_row_range, + line_layouts, + newest_selection_head, + scrolled_content_origin, + window, + cx, + ); + } + + const POLE_WIDTH: Pixels = px(2.); + + let line_layout = + line_layouts.get(target_display_point.row().minus(visible_row_range.start) as usize)?; + let target_column = target_display_point.column() as usize; + + let target_x = line_layout.x_for_index(target_column); + let target_y = + (target_display_point.row().as_f32() * line_height) - scroll_pixel_position.y; + + let flag_on_right = target_x < text_bounds.size.width / 2.; + + let mut border_color = Self::edit_prediction_callout_popover_border_color(cx); + border_color.l += 0.001; + + let mut element = v_flex() + .items_end() + .when(flag_on_right, |el| el.items_start()) + .child(if flag_on_right { + self.render_edit_prediction_line_popover("Jump", None, window, cx)? + .rounded_bl(px(0.)) + .rounded_tl(px(0.)) + .border_l_2() + .border_color(border_color) + } else { + self.render_edit_prediction_line_popover("Jump", None, window, cx)? + .rounded_br(px(0.)) + .rounded_tr(px(0.)) + .border_r_2() + .border_color(border_color) + }) + .child(div().w(POLE_WIDTH).bg(border_color).h(line_height)) + .into_any(); + + let size = element.layout_as_root(AvailableSpace::min_size(), window, cx); + + let mut origin = scrolled_content_origin + point(target_x, target_y) + - point( + if flag_on_right { + POLE_WIDTH + } else { + size.width - POLE_WIDTH + }, + size.height - line_height, + ); + + origin.x = origin.x.max(content_origin.x); + + element.prepaint_at(origin, window, cx); + + Some((element, origin)) + } + + fn render_edit_prediction_scroll_popover( + &mut self, + to_y: impl Fn(Size) -> Pixels, + scroll_icon: IconName, + visible_row_range: Range, + line_layouts: &[LineWithInvisibles], + newest_selection_head: Option, + scrolled_content_origin: gpui::Point, + window: &mut Window, + cx: &mut App, + ) -> Option<(AnyElement, gpui::Point)> { + let mut element = self + .render_edit_prediction_line_popover("Scroll", Some(scroll_icon), window, cx)? + .into_any(); + + let size = element.layout_as_root(AvailableSpace::min_size(), window, cx); + + let cursor = newest_selection_head?; + let cursor_row_layout = + line_layouts.get(cursor.row().minus(visible_row_range.start) as usize)?; + let cursor_column = cursor.column() as usize; + + let cursor_character_x = cursor_row_layout.x_for_index(cursor_column); + + let origin = scrolled_content_origin + point(cursor_character_x, to_y(size)); + + element.prepaint_at(origin, window, cx); + Some((element, origin)) + } + + fn render_edit_prediction_eager_jump_popover( + &mut self, + text_bounds: &Bounds, + content_origin: gpui::Point, + editor_snapshot: &EditorSnapshot, + visible_row_range: Range, + scroll_top: f32, + scroll_bottom: f32, + line_height: Pixels, + scroll_pixel_position: gpui::Point, + target_display_point: DisplayPoint, + editor_width: Pixels, + window: &mut Window, + cx: &mut App, + ) -> Option<(AnyElement, gpui::Point)> { + if target_display_point.row().as_f32() < scroll_top { + let mut element = self + .render_edit_prediction_line_popover( + "Jump to Edit", + Some(IconName::ArrowUp), + window, + cx, + )? + .into_any(); + + let size = element.layout_as_root(AvailableSpace::min_size(), window, cx); + let offset = point( + (text_bounds.size.width - size.width) / 2., + Self::EDIT_PREDICTION_POPOVER_PADDING_Y, + ); + + let origin = text_bounds.origin + offset; + element.prepaint_at(origin, window, cx); + Some((element, origin)) + } else if (target_display_point.row().as_f32() + 1.) > scroll_bottom { + let mut element = self + .render_edit_prediction_line_popover( + "Jump to Edit", + Some(IconName::ArrowDown), + window, + cx, + )? + .into_any(); + + let size = element.layout_as_root(AvailableSpace::min_size(), window, cx); + let offset = point( + (text_bounds.size.width - size.width) / 2., + text_bounds.size.height - size.height - Self::EDIT_PREDICTION_POPOVER_PADDING_Y, + ); + + let origin = text_bounds.origin + offset; + element.prepaint_at(origin, window, cx); + Some((element, origin)) + } else { + self.render_edit_prediction_end_of_line_popover( + "Jump to Edit", + editor_snapshot, + visible_row_range, + target_display_point, + line_height, + scroll_pixel_position, + content_origin, + editor_width, + window, + cx, + ) + } + } + + fn render_edit_prediction_end_of_line_popover( + self: &mut Editor, + label: &'static str, + editor_snapshot: &EditorSnapshot, + visible_row_range: Range, + target_display_point: DisplayPoint, + line_height: Pixels, + scroll_pixel_position: gpui::Point, + content_origin: gpui::Point, + editor_width: Pixels, + window: &mut Window, + cx: &mut App, + ) -> Option<(AnyElement, gpui::Point)> { + let target_line_end = DisplayPoint::new( + target_display_point.row(), + editor_snapshot.line_len(target_display_point.row()), + ); + + let mut element = self + .render_edit_prediction_line_popover(label, None, window, cx)? + .into_any(); + + let size = element.layout_as_root(AvailableSpace::min_size(), window, cx); + + let line_origin = self.display_to_pixel_point(target_line_end, editor_snapshot, window)?; + + let start_point = content_origin - point(scroll_pixel_position.x, Pixels::ZERO); + let mut origin = start_point + + line_origin + + point(Self::EDIT_PREDICTION_POPOVER_PADDING_X, Pixels::ZERO); + origin.x = origin.x.max(content_origin.x); + + let max_x = content_origin.x + editor_width - size.width; + + if origin.x > max_x { + let offset = line_height + Self::EDIT_PREDICTION_POPOVER_PADDING_Y; + + let icon = if visible_row_range.contains(&(target_display_point.row() + 2)) { + origin.y += offset; + IconName::ArrowUp + } else { + origin.y -= offset; + IconName::ArrowDown + }; + + element = self + .render_edit_prediction_line_popover(label, Some(icon), window, cx)? + .into_any(); + + let size = element.layout_as_root(AvailableSpace::min_size(), window, cx); + + origin.x = content_origin.x + editor_width - size.width - px(2.); + } + + element.prepaint_at(origin, window, cx); + Some((element, origin)) + } + + fn render_edit_prediction_diff_popover( + self: &Editor, + text_bounds: &Bounds, + content_origin: gpui::Point, + editor_snapshot: &EditorSnapshot, + visible_row_range: Range, + line_layouts: &[LineWithInvisibles], + line_height: Pixels, + scroll_pixel_position: gpui::Point, + newest_selection_head: Option, + editor_width: Pixels, + style: &EditorStyle, + edits: &Vec<(Range, String)>, + edit_preview: &Option, + snapshot: &language::BufferSnapshot, + window: &mut Window, + cx: &mut App, + ) -> Option<(AnyElement, gpui::Point)> { + let edit_start = edits + .first() + .unwrap() + .0 + .start + .to_display_point(editor_snapshot); + let edit_end = edits + .last() + .unwrap() + .0 + .end + .to_display_point(editor_snapshot); + + let is_visible = visible_row_range.contains(&edit_start.row()) + || visible_row_range.contains(&edit_end.row()); + if !is_visible { + return None; + } + + let highlighted_edits = + crate::inline_completion_edit_text(&snapshot, edits, edit_preview.as_ref()?, false, cx); + + let styled_text = highlighted_edits.to_styled_text(&style.text); + let line_count = highlighted_edits.text.lines().count(); + + const BORDER_WIDTH: Pixels = px(1.); + + let keybind = self.render_edit_prediction_accept_keybind(window, cx); + let has_keybind = keybind.is_some(); + + let mut element = h_flex() + .items_start() + .child( + h_flex() + .bg(cx.theme().colors().editor_background) + .border(BORDER_WIDTH) + .shadow_sm() + .border_color(cx.theme().colors().border) + .rounded_l_lg() + .when(line_count > 1, |el| el.rounded_br_lg()) + .pr_1() + .child(styled_text), + ) + .child( + h_flex() + .h(line_height + BORDER_WIDTH * 2.) + .px_1p5() + .gap_1() + // Workaround: For some reason, there's a gap if we don't do this + .ml(-BORDER_WIDTH) + .shadow(smallvec![gpui::BoxShadow { + color: gpui::black().opacity(0.05), + offset: point(px(1.), px(1.)), + blur_radius: px(2.), + spread_radius: px(0.), + }]) + .bg(Editor::edit_prediction_line_popover_bg_color(cx)) + .border(BORDER_WIDTH) + .border_color(cx.theme().colors().border) + .rounded_r_lg() + .id("edit_prediction_diff_popover_keybind") + .when(!has_keybind, |el| { + let status_colors = cx.theme().status(); + + el.bg(status_colors.error_background) + .border_color(status_colors.error.opacity(0.6)) + .child(Icon::new(IconName::Info).color(Color::Error)) + .cursor_default() + .hoverable_tooltip(move |_window, cx| { + cx.new(|_| MissingEditPredictionKeybindingTooltip).into() + }) + }) + .children(keybind), + ) + .into_any(); + + let longest_row = + editor_snapshot.longest_row_in_range(edit_start.row()..edit_end.row() + 1); + let longest_line_width = if visible_row_range.contains(&longest_row) { + line_layouts[(longest_row.0 - visible_row_range.start.0) as usize].width + } else { + layout_line( + longest_row, + editor_snapshot, + style, + editor_width, + |_| false, + window, + cx, + ) + .width + }; + + let viewport_bounds = + Bounds::new(Default::default(), window.viewport_size()).extend(Edges { + right: -EditorElement::SCROLLBAR_WIDTH, + ..Default::default() + }); + + let x_after_longest = + text_bounds.origin.x + longest_line_width + Self::EDIT_PREDICTION_POPOVER_PADDING_X + - scroll_pixel_position.x; + + let element_bounds = element.layout_as_root(AvailableSpace::min_size(), window, cx); + + // Fully visible if it can be displayed within the window (allow overlapping other + // panes). However, this is only allowed if the popover starts within text_bounds. + let can_position_to_the_right = x_after_longest < text_bounds.right() + && x_after_longest + element_bounds.width < viewport_bounds.right(); + + let mut origin = if can_position_to_the_right { + point( + x_after_longest, + text_bounds.origin.y + edit_start.row().as_f32() * line_height + - scroll_pixel_position.y, + ) + } else { + let cursor_row = newest_selection_head.map(|head| head.row()); + let above_edit = edit_start + .row() + .0 + .checked_sub(line_count as u32) + .map(DisplayRow); + let below_edit = Some(edit_end.row() + 1); + let above_cursor = + cursor_row.and_then(|row| row.0.checked_sub(line_count as u32).map(DisplayRow)); + let below_cursor = cursor_row.map(|cursor_row| cursor_row + 1); + + // Place the edit popover adjacent to the edit if there is a location + // available that is onscreen and does not obscure the cursor. Otherwise, + // place it adjacent to the cursor. + let row_target = [above_edit, below_edit, above_cursor, below_cursor] + .into_iter() + .flatten() + .find(|&start_row| { + let end_row = start_row + line_count as u32; + visible_row_range.contains(&start_row) + && visible_row_range.contains(&end_row) + && cursor_row.map_or(true, |cursor_row| { + !((start_row..end_row).contains(&cursor_row)) + }) + })?; + + content_origin + + point( + -scroll_pixel_position.x, + row_target.as_f32() * line_height - scroll_pixel_position.y, + ) + }; + + origin.x -= BORDER_WIDTH; + + window.defer_draw(element, origin, 1); + + // Do not return an element, since it will already be drawn due to defer_draw. + None + } + + fn edit_prediction_cursor_popover_height(&self) -> Pixels { + px(30.) + } + + fn current_user_player_color(&self, cx: &mut App) -> PlayerColor { + if self.read_only(cx) { + cx.theme().players().read_only() + } else { + self.style.as_ref().unwrap().local_player + } + } + + fn render_edit_prediction_accept_keybind( + &self, + window: &mut Window, + cx: &App, + ) -> Option { + let accept_binding = self.accept_edit_prediction_keybind(window, cx); + let accept_keystroke = accept_binding.keystroke()?; + + let is_platform_style_mac = PlatformStyle::platform() == PlatformStyle::Mac; + + let modifiers_color = if accept_keystroke.modifiers == window.modifiers() { + Color::Accent + } else { + Color::Muted + }; + + h_flex() + .px_0p5() + .when(is_platform_style_mac, |parent| parent.gap_0p5()) + .font(theme::ThemeSettings::get_global(cx).buffer_font.clone()) + .text_size(TextSize::XSmall.rems(cx)) + .child(h_flex().children(ui::render_modifiers( + &accept_keystroke.modifiers, + PlatformStyle::platform(), + Some(modifiers_color), + Some(IconSize::XSmall.rems().into()), + true, + ))) + .when(is_platform_style_mac, |parent| { + parent.child(accept_keystroke.key.clone()) + }) + .when(!is_platform_style_mac, |parent| { + parent.child( + Key::new( + util::capitalize(&accept_keystroke.key), + Some(Color::Default), + ) + .size(Some(IconSize::XSmall.rems().into())), + ) + }) + .into_any() + .into() + } + + fn render_edit_prediction_line_popover( + &self, + label: impl Into, + icon: Option, + window: &mut Window, + cx: &App, + ) -> Option> { + let padding_right = if icon.is_some() { px(4.) } else { px(8.) }; + + let keybind = self.render_edit_prediction_accept_keybind(window, cx); + let has_keybind = keybind.is_some(); + + let result = h_flex() + .id("ep-line-popover") + .py_0p5() + .pl_1() + .pr(padding_right) + .gap_1() + .rounded_md() + .border_1() + .bg(Self::edit_prediction_line_popover_bg_color(cx)) + .border_color(Self::edit_prediction_callout_popover_border_color(cx)) + .shadow_sm() + .when(!has_keybind, |el| { + let status_colors = cx.theme().status(); + + el.bg(status_colors.error_background) + .border_color(status_colors.error.opacity(0.6)) + .pl_2() + .child(Icon::new(IconName::ZedPredictError).color(Color::Error)) + .cursor_default() + .hoverable_tooltip(move |_window, cx| { + cx.new(|_| MissingEditPredictionKeybindingTooltip).into() + }) + }) + .children(keybind) + .child( + Label::new(label) + .size(LabelSize::Small) + .when(!has_keybind, |el| { + el.color(cx.theme().status().error.into()).strikethrough() + }), + ) + .when(!has_keybind, |el| { + el.child( + h_flex().ml_1().child( + Icon::new(IconName::Info) + .size(IconSize::Small) + .color(cx.theme().status().error.into()), + ), + ) + }) + .when_some(icon, |element, icon| { + element.child( + div() + .mt(px(1.5)) + .child(Icon::new(icon).size(IconSize::Small)), + ) + }); + + Some(result) + } + + fn edit_prediction_line_popover_bg_color(cx: &App) -> Hsla { + let accent_color = cx.theme().colors().text_accent; + let editor_bg_color = cx.theme().colors().editor_background; + editor_bg_color.blend(accent_color.opacity(0.1)) + } + + fn edit_prediction_callout_popover_border_color(cx: &App) -> Hsla { + let accent_color = cx.theme().colors().text_accent; + let editor_bg_color = cx.theme().colors().editor_background; + editor_bg_color.blend(accent_color.opacity(0.6)) + } + + fn render_edit_prediction_cursor_popover( + &self, + min_width: Pixels, + max_width: Pixels, + cursor_point: Point, + style: &EditorStyle, + accept_keystroke: Option<&gpui::Keystroke>, + _window: &Window, + cx: &mut Context, + ) -> Option { + let provider = self.edit_prediction_provider.as_ref()?; + + if provider.provider.needs_terms_acceptance(cx) { + return Some( + h_flex() + .min_w(min_width) + .flex_1() + .px_2() + .py_1() + .gap_3() + .elevation_2(cx) + .hover(|style| style.bg(cx.theme().colors().element_hover)) + .id("accept-terms") + .cursor_pointer() + .on_mouse_down(MouseButton::Left, |_, window, _| window.prevent_default()) + .on_click(cx.listener(|this, _event, window, cx| { + cx.stop_propagation(); + this.report_editor_event("Edit Prediction Provider ToS Clicked", None, cx); + window.dispatch_action( + zed_actions::OpenZedPredictOnboarding.boxed_clone(), + cx, + ); + })) + .child( + h_flex() + .flex_1() + .gap_2() + .child(Icon::new(IconName::ZedPredict)) + .child(Label::new("Accept Terms of Service")) + .child(div().w_full()) + .child( + Icon::new(IconName::ArrowUpRight) + .color(Color::Muted) + .size(IconSize::Small), + ) + .into_any_element(), + ) + .into_any(), + ); + } + + let is_refreshing = provider.provider.is_refreshing(cx); + + fn pending_completion_container() -> Div { + h_flex() + .h_full() + .flex_1() + .gap_2() + .child(Icon::new(IconName::ZedPredict)) + } + + let completion = match &self.active_inline_completion { + Some(prediction) => { + if !self.has_visible_completions_menu() { + const RADIUS: Pixels = px(6.); + const BORDER_WIDTH: Pixels = px(1.); + + return Some( + h_flex() + .elevation_2(cx) + .border(BORDER_WIDTH) + .border_color(cx.theme().colors().border) + .when(accept_keystroke.is_none(), |el| { + el.border_color(cx.theme().status().error) + }) + .rounded(RADIUS) + .rounded_tl(px(0.)) + .overflow_hidden() + .child(div().px_1p5().child(match &prediction.completion { + InlineCompletion::Move { target, snapshot } => { + use text::ToPoint as _; + if target.text_anchor.to_point(&snapshot).row > cursor_point.row + { + Icon::new(IconName::ZedPredictDown) + } else { + Icon::new(IconName::ZedPredictUp) + } + } + InlineCompletion::Edit { .. } => Icon::new(IconName::ZedPredict), + })) + .child( + h_flex() + .gap_1() + .py_1() + .px_2() + .rounded_r(RADIUS - BORDER_WIDTH) + .border_l_1() + .border_color(cx.theme().colors().border) + .bg(Self::edit_prediction_line_popover_bg_color(cx)) + .when(self.edit_prediction_preview.released_too_fast(), |el| { + el.child( + Label::new("Hold") + .size(LabelSize::Small) + .when(accept_keystroke.is_none(), |el| { + el.strikethrough() + }) + .line_height_style(LineHeightStyle::UiLabel), + ) + }) + .id("edit_prediction_cursor_popover_keybind") + .when(accept_keystroke.is_none(), |el| { + let status_colors = cx.theme().status(); + + el.bg(status_colors.error_background) + .border_color(status_colors.error.opacity(0.6)) + .child(Icon::new(IconName::Info).color(Color::Error)) + .cursor_default() + .hoverable_tooltip(move |_window, cx| { + cx.new(|_| MissingEditPredictionKeybindingTooltip) + .into() + }) + }) + .when_some( + accept_keystroke.as_ref(), + |el, accept_keystroke| { + el.child(h_flex().children(ui::render_modifiers( + &accept_keystroke.modifiers, + PlatformStyle::platform(), + Some(Color::Default), + Some(IconSize::XSmall.rems().into()), + false, + ))) + }, + ), + ) + .into_any(), + ); + } + + self.render_edit_prediction_cursor_popover_preview( + prediction, + cursor_point, + style, + cx, + )? + } + + None if is_refreshing => match &self.stale_inline_completion_in_menu { + Some(stale_completion) => self.render_edit_prediction_cursor_popover_preview( + stale_completion, + cursor_point, + style, + cx, + )?, + + None => { + pending_completion_container().child(Label::new("...").size(LabelSize::Small)) + } + }, + + None => pending_completion_container().child(Label::new("No Prediction")), + }; + + let completion = if is_refreshing { + completion + .with_animation( + "loading-completion", + Animation::new(Duration::from_secs(2)) + .repeat() + .with_easing(pulsating_between(0.4, 0.8)), + |label, delta| label.opacity(delta), + ) + .into_any_element() + } else { + completion.into_any_element() + }; + + let has_completion = self.active_inline_completion.is_some(); + + let is_platform_style_mac = PlatformStyle::platform() == PlatformStyle::Mac; + Some( + h_flex() + .min_w(min_width) + .max_w(max_width) + .flex_1() + .elevation_2(cx) + .border_color(cx.theme().colors().border) + .child( + div() + .flex_1() + .py_1() + .px_2() + .overflow_hidden() + .child(completion), + ) + .when_some(accept_keystroke, |el, accept_keystroke| { + if !accept_keystroke.modifiers.modified() { + return el; + } + + el.child( + h_flex() + .h_full() + .border_l_1() + .rounded_r_lg() + .border_color(cx.theme().colors().border) + .bg(Self::edit_prediction_line_popover_bg_color(cx)) + .gap_1() + .py_1() + .px_2() + .child( + h_flex() + .font(theme::ThemeSettings::get_global(cx).buffer_font.clone()) + .when(is_platform_style_mac, |parent| parent.gap_1()) + .child(h_flex().children(ui::render_modifiers( + &accept_keystroke.modifiers, + PlatformStyle::platform(), + Some(if !has_completion { + Color::Muted + } else { + Color::Default + }), + None, + false, + ))), + ) + .child(Label::new("Preview").into_any_element()) + .opacity(if has_completion { 1.0 } else { 0.4 }), + ) + }) + .into_any(), + ) + } + + fn render_edit_prediction_cursor_popover_preview( + &self, + completion: &InlineCompletionState, + cursor_point: Point, + style: &EditorStyle, + cx: &mut Context, + ) -> Option
{ + use text::ToPoint as _; + + fn render_relative_row_jump( + prefix: impl Into, + current_row: u32, + target_row: u32, + ) -> Div { + let (row_diff, arrow) = if target_row < current_row { + (current_row - target_row, IconName::ArrowUp) + } else { + (target_row - current_row, IconName::ArrowDown) + }; + + h_flex() + .child( + Label::new(format!("{}{}", prefix.into(), row_diff)) + .color(Color::Muted) + .size(LabelSize::Small), + ) + .child(Icon::new(arrow).color(Color::Muted).size(IconSize::Small)) + } + + match &completion.completion { + InlineCompletion::Move { + target, snapshot, .. + } => Some( + h_flex() + .px_2() + .gap_2() + .flex_1() + .child( + if target.text_anchor.to_point(&snapshot).row > cursor_point.row { + Icon::new(IconName::ZedPredictDown) + } else { + Icon::new(IconName::ZedPredictUp) + }, + ) + .child(Label::new("Jump to Edit")), + ), + + InlineCompletion::Edit { + edits, + edit_preview, + snapshot, + display_mode: _, + } => { + let first_edit_row = edits.first()?.0.start.text_anchor.to_point(&snapshot).row; + + let (highlighted_edits, has_more_lines) = crate::inline_completion_edit_text( + &snapshot, + &edits, + edit_preview.as_ref()?, + true, + cx, + ) + .first_line_preview(); + + let styled_text = gpui::StyledText::new(highlighted_edits.text) + .with_default_highlights(&style.text, highlighted_edits.highlights); + + let preview = h_flex() + .gap_1() + .min_w_16() + .child(styled_text) + .when(has_more_lines, |parent| parent.child("…")); + + let left = if first_edit_row != cursor_point.row { + render_relative_row_jump("", cursor_point.row, first_edit_row) + .into_any_element() + } else { + Icon::new(IconName::ZedPredict).into_any_element() + }; + + Some( + h_flex() + .h_full() + .flex_1() + .gap_2() + .pr_1() + .overflow_x_hidden() + .font(theme::ThemeSettings::get_global(cx).buffer_font.clone()) + .child(left) + .child(preview), + ) + } + } + } + + fn render_context_menu( + &self, + style: &EditorStyle, + max_height_in_lines: u32, + window: &mut Window, + cx: &mut Context, + ) -> Option { + let menu = self.context_menu.borrow(); + let menu = menu.as_ref()?; + if !menu.visible() { + return None; + }; + Some(menu.render(style, max_height_in_lines, window, cx)) + } + + fn render_context_menu_aside( + &mut self, + max_size: Size, + window: &mut Window, + cx: &mut Context, + ) -> Option { + self.context_menu.borrow_mut().as_mut().and_then(|menu| { + if menu.visible() { + menu.render_aside(self, max_size, window, cx) + } else { + None + } + }) + } + + fn hide_context_menu( + &mut self, + window: &mut Window, + cx: &mut Context, + ) -> Option { + cx.notify(); + self.completion_tasks.clear(); + let context_menu = self.context_menu.borrow_mut().take(); + self.stale_inline_completion_in_menu.take(); + self.update_visible_inline_completion(window, cx); + context_menu + } + + fn show_snippet_choices( + &mut self, + choices: &Vec, + selection: Range, + cx: &mut Context, + ) { + if selection.start.buffer_id.is_none() { + return; + } + let buffer_id = selection.start.buffer_id.unwrap(); + let buffer = self.buffer().read(cx).buffer(buffer_id); + let id = post_inc(&mut self.next_completion_id); + let snippet_sort_order = EditorSettings::get_global(cx).snippet_sort_order; + + if let Some(buffer) = buffer { + *self.context_menu.borrow_mut() = Some(CodeContextMenu::Completions( + CompletionsMenu::new_snippet_choices( + id, + true, + choices, + selection, + buffer, + snippet_sort_order, + ), + )); + } + } + + pub fn insert_snippet( + &mut self, + insertion_ranges: &[Range], + snippet: Snippet, + window: &mut Window, + cx: &mut Context, + ) -> Result<()> { + struct Tabstop { + is_end_tabstop: bool, + ranges: Vec>, + choices: Option>, + } + + let tabstops = self.buffer.update(cx, |buffer, cx| { + let snippet_text: Arc = snippet.text.clone().into(); + let edits = insertion_ranges + .iter() + .cloned() + .map(|range| (range, snippet_text.clone())); + buffer.edit(edits, Some(AutoindentMode::EachLine), cx); + + let snapshot = &*buffer.read(cx); + let snippet = &snippet; + snippet + .tabstops + .iter() + .map(|tabstop| { + let is_end_tabstop = tabstop.ranges.first().map_or(false, |tabstop| { + tabstop.is_empty() && tabstop.start == snippet.text.len() as isize + }); + let mut tabstop_ranges = tabstop + .ranges + .iter() + .flat_map(|tabstop_range| { + let mut delta = 0_isize; + insertion_ranges.iter().map(move |insertion_range| { + let insertion_start = insertion_range.start as isize + delta; + delta += + snippet.text.len() as isize - insertion_range.len() as isize; + + let start = ((insertion_start + tabstop_range.start) as usize) + .min(snapshot.len()); + let end = ((insertion_start + tabstop_range.end) as usize) + .min(snapshot.len()); + snapshot.anchor_before(start)..snapshot.anchor_after(end) + }) + }) + .collect::>(); + tabstop_ranges.sort_unstable_by(|a, b| a.start.cmp(&b.start, snapshot)); + + Tabstop { + is_end_tabstop, + ranges: tabstop_ranges, + choices: tabstop.choices.clone(), + } + }) + .collect::>() + }); + if let Some(tabstop) = tabstops.first() { + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.select_ranges(tabstop.ranges.iter().cloned()); + }); + + if let Some(choices) = &tabstop.choices { + if let Some(selection) = tabstop.ranges.first() { + self.show_snippet_choices(choices, selection.clone(), cx) + } + } + + // If we're already at the last tabstop and it's at the end of the snippet, + // we're done, we don't need to keep the state around. + if !tabstop.is_end_tabstop { + let choices = tabstops + .iter() + .map(|tabstop| tabstop.choices.clone()) + .collect(); + + let ranges = tabstops + .into_iter() + .map(|tabstop| tabstop.ranges) + .collect::>(); + + self.snippet_stack.push(SnippetState { + active_index: 0, + ranges, + choices, + }); + } + + // Check whether the just-entered snippet ends with an auto-closable bracket. + if self.autoclose_regions.is_empty() { + let snapshot = self.buffer.read(cx).snapshot(cx); + for selection in &mut self.selections.all::(cx) { + let selection_head = selection.head(); + let Some(scope) = snapshot.language_scope_at(selection_head) else { + continue; + }; + + let mut bracket_pair = None; + let next_chars = snapshot.chars_at(selection_head).collect::(); + let prev_chars = snapshot + .reversed_chars_at(selection_head) + .collect::(); + for (pair, enabled) in scope.brackets() { + if enabled + && pair.close + && prev_chars.starts_with(pair.start.as_str()) + && next_chars.starts_with(pair.end.as_str()) + { + bracket_pair = Some(pair.clone()); + break; + } + } + if let Some(pair) = bracket_pair { + let snapshot_settings = snapshot.language_settings_at(selection_head, cx); + let autoclose_enabled = + self.use_autoclose && snapshot_settings.use_autoclose; + if autoclose_enabled { + let start = snapshot.anchor_after(selection_head); + let end = snapshot.anchor_after(selection_head); + self.autoclose_regions.push(AutocloseRegion { + selection_id: selection.id, + range: start..end, + pair, + }); + } + } + } + } + } + Ok(()) + } + + pub fn move_to_next_snippet_tabstop( + &mut self, + window: &mut Window, + cx: &mut Context, + ) -> bool { + self.move_to_snippet_tabstop(Bias::Right, window, cx) + } + + pub fn move_to_prev_snippet_tabstop( + &mut self, + window: &mut Window, + cx: &mut Context, + ) -> bool { + self.move_to_snippet_tabstop(Bias::Left, window, cx) + } + + pub fn move_to_snippet_tabstop( + &mut self, + bias: Bias, + window: &mut Window, + cx: &mut Context, + ) -> bool { + if let Some(mut snippet) = self.snippet_stack.pop() { + match bias { + Bias::Left => { + if snippet.active_index > 0 { + snippet.active_index -= 1; + } else { + self.snippet_stack.push(snippet); + return false; + } + } + Bias::Right => { + if snippet.active_index + 1 < snippet.ranges.len() { + snippet.active_index += 1; + } else { + self.snippet_stack.push(snippet); + return false; + } + } + } + if let Some(current_ranges) = snippet.ranges.get(snippet.active_index) { + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.select_anchor_ranges(current_ranges.iter().cloned()) + }); + + if let Some(choices) = &snippet.choices[snippet.active_index] { + if let Some(selection) = current_ranges.first() { + self.show_snippet_choices(&choices, selection.clone(), cx); + } + } + + // If snippet state is not at the last tabstop, push it back on the stack + if snippet.active_index + 1 < snippet.ranges.len() { + self.snippet_stack.push(snippet); + } + return true; + } + } + + false + } + + pub fn clear(&mut self, window: &mut Window, cx: &mut Context) { + self.transact(window, cx, |this, window, cx| { + this.select_all(&SelectAll, window, cx); + this.insert("", window, cx); + }); + } + + pub fn backspace(&mut self, _: &Backspace, window: &mut Window, cx: &mut Context) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + self.transact(window, cx, |this, window, cx| { + this.select_autoclose_pair(window, cx); + let mut linked_ranges = HashMap::<_, Vec<_>>::default(); + if !this.linked_edit_ranges.is_empty() { + let selections = this.selections.all::(cx); + let snapshot = this.buffer.read(cx).snapshot(cx); + + for selection in selections.iter() { + let selection_start = snapshot.anchor_before(selection.start).text_anchor; + let selection_end = snapshot.anchor_after(selection.end).text_anchor; + if selection_start.buffer_id != selection_end.buffer_id { + continue; + } + if let Some(ranges) = + this.linked_editing_ranges_for(selection_start..selection_end, cx) + { + for (buffer, entries) in ranges { + linked_ranges.entry(buffer).or_default().extend(entries); + } + } + } + } + + let mut selections = this.selections.all::(cx); + let display_map = this.display_map.update(cx, |map, cx| map.snapshot(cx)); + for selection in &mut selections { + if selection.is_empty() { + let old_head = selection.head(); + let mut new_head = + movement::left(&display_map, old_head.to_display_point(&display_map)) + .to_point(&display_map); + if let Some((buffer, line_buffer_range)) = display_map + .buffer_snapshot + .buffer_line_for_row(MultiBufferRow(old_head.row)) + { + let indent_size = buffer.indent_size_for_line(line_buffer_range.start.row); + let indent_len = match indent_size.kind { + IndentKind::Space => { + buffer.settings_at(line_buffer_range.start, cx).tab_size + } + IndentKind::Tab => NonZeroU32::new(1).unwrap(), + }; + if old_head.column <= indent_size.len && old_head.column > 0 { + let indent_len = indent_len.get(); + new_head = cmp::min( + new_head, + MultiBufferPoint::new( + old_head.row, + ((old_head.column - 1) / indent_len) * indent_len, + ), + ); + } + } + + selection.set_head(new_head, SelectionGoal::None); + } + } + + this.signature_help_state.set_backspace_pressed(true); + this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.select(selections) + }); + this.insert("", window, cx); + let empty_str: Arc = Arc::from(""); + for (buffer, edits) in linked_ranges { + let snapshot = buffer.read(cx).snapshot(); + use text::ToPoint as TP; + + let edits = edits + .into_iter() + .map(|range| { + let end_point = TP::to_point(&range.end, &snapshot); + let mut start_point = TP::to_point(&range.start, &snapshot); + + if end_point == start_point { + let offset = text::ToOffset::to_offset(&range.start, &snapshot) + .saturating_sub(1); + start_point = + snapshot.clip_point(TP::to_point(&offset, &snapshot), Bias::Left); + }; + + (start_point..end_point, empty_str.clone()) + }) + .sorted_by_key(|(range, _)| range.start) + .collect::>(); + buffer.update(cx, |this, cx| { + this.edit(edits, None, cx); + }) + } + this.refresh_inline_completion(true, false, window, cx); + linked_editing_ranges::refresh_linked_ranges(this, window, cx); + }); + } + + pub fn delete(&mut self, _: &Delete, window: &mut Window, cx: &mut Context) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + self.transact(window, cx, |this, window, cx| { + this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.move_with(|map, selection| { + if selection.is_empty() { + let cursor = movement::right(map, selection.head()); + selection.end = cursor; + selection.reversed = true; + selection.goal = SelectionGoal::None; + } + }) + }); + this.insert("", window, cx); + this.refresh_inline_completion(true, false, window, cx); + }); + } + + pub fn backtab(&mut self, _: &Backtab, window: &mut Window, cx: &mut Context) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + if self.move_to_prev_snippet_tabstop(window, cx) { + return; + } + self.outdent(&Outdent, window, cx); + } + + pub fn tab(&mut self, _: &Tab, window: &mut Window, cx: &mut Context) { + if self.move_to_next_snippet_tabstop(window, cx) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + return; + } + if self.read_only(cx) { + return; + } + self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + let mut selections = self.selections.all_adjusted(cx); + let buffer = self.buffer.read(cx); + let snapshot = buffer.snapshot(cx); + let rows_iter = selections.iter().map(|s| s.head().row); + let suggested_indents = snapshot.suggested_indents(rows_iter, cx); + + let has_some_cursor_in_whitespace = selections + .iter() + .filter(|selection| selection.is_empty()) + .any(|selection| { + let cursor = selection.head(); + let current_indent = snapshot.indent_size_for_line(MultiBufferRow(cursor.row)); + cursor.column < current_indent.len + }); + + let mut edits = Vec::new(); + let mut prev_edited_row = 0; + let mut row_delta = 0; + for selection in &mut selections { + if selection.start.row != prev_edited_row { + row_delta = 0; + } + prev_edited_row = selection.end.row; + + // If the selection is non-empty, then increase the indentation of the selected lines. + if !selection.is_empty() { + row_delta = + Self::indent_selection(buffer, &snapshot, selection, &mut edits, row_delta, cx); + continue; + } + + // If the selection is empty and the cursor is in the leading whitespace before the + // suggested indentation, then auto-indent the line. + let cursor = selection.head(); + let current_indent = snapshot.indent_size_for_line(MultiBufferRow(cursor.row)); + if let Some(suggested_indent) = + suggested_indents.get(&MultiBufferRow(cursor.row)).copied() + { + // If there exist any empty selection in the leading whitespace, then skip + // indent for selections at the boundary. + if has_some_cursor_in_whitespace + && cursor.column == current_indent.len + && current_indent.len == suggested_indent.len + { + continue; + } + + if cursor.column < suggested_indent.len + && cursor.column <= current_indent.len + && current_indent.len <= suggested_indent.len + { + selection.start = Point::new(cursor.row, suggested_indent.len); + selection.end = selection.start; + if row_delta == 0 { + edits.extend(Buffer::edit_for_indent_size_adjustment( + cursor.row, + current_indent, + suggested_indent, + )); + row_delta = suggested_indent.len - current_indent.len; + } + continue; + } + } + + // Otherwise, insert a hard or soft tab. + let settings = buffer.language_settings_at(cursor, cx); + let tab_size = if settings.hard_tabs { + IndentSize::tab() + } else { + let tab_size = settings.tab_size.get(); + let indent_remainder = snapshot + .text_for_range(Point::new(cursor.row, 0)..cursor) + .flat_map(str::chars) + .fold(row_delta % tab_size, |counter: u32, c| { + if c == '\t' { + 0 + } else { + (counter + 1) % tab_size + } + }); + + let chars_to_next_tab_stop = tab_size - indent_remainder; + IndentSize::spaces(chars_to_next_tab_stop) + }; + selection.start = Point::new(cursor.row, cursor.column + row_delta + tab_size.len); + selection.end = selection.start; + edits.push((cursor..cursor, tab_size.chars().collect::())); + row_delta += tab_size.len; + } + + self.transact(window, cx, |this, window, cx| { + this.buffer.update(cx, |b, cx| b.edit(edits, None, cx)); + this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.select(selections) + }); + this.refresh_inline_completion(true, false, window, cx); + }); + } + + pub fn indent(&mut self, _: &Indent, window: &mut Window, cx: &mut Context) { + if self.read_only(cx) { + return; + } + self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + let mut selections = self.selections.all::(cx); + let mut prev_edited_row = 0; + let mut row_delta = 0; + let mut edits = Vec::new(); + let buffer = self.buffer.read(cx); + let snapshot = buffer.snapshot(cx); + for selection in &mut selections { + if selection.start.row != prev_edited_row { + row_delta = 0; + } + prev_edited_row = selection.end.row; + + row_delta = + Self::indent_selection(buffer, &snapshot, selection, &mut edits, row_delta, cx); + } + + self.transact(window, cx, |this, window, cx| { + this.buffer.update(cx, |b, cx| b.edit(edits, None, cx)); + this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.select(selections) + }); + }); + } + + fn indent_selection( + buffer: &MultiBuffer, + snapshot: &MultiBufferSnapshot, + selection: &mut Selection, + edits: &mut Vec<(Range, String)>, + delta_for_start_row: u32, + cx: &App, + ) -> u32 { + let settings = buffer.language_settings_at(selection.start, cx); + let tab_size = settings.tab_size.get(); + let indent_kind = if settings.hard_tabs { + IndentKind::Tab + } else { + IndentKind::Space + }; + let mut start_row = selection.start.row; + let mut end_row = selection.end.row + 1; + + // If a selection ends at the beginning of a line, don't indent + // that last line. + if selection.end.column == 0 && selection.end.row > selection.start.row { + end_row -= 1; + } + + // Avoid re-indenting a row that has already been indented by a + // previous selection, but still update this selection's column + // to reflect that indentation. + if delta_for_start_row > 0 { + start_row += 1; + selection.start.column += delta_for_start_row; + if selection.end.row == selection.start.row { + selection.end.column += delta_for_start_row; + } + } + + let mut delta_for_end_row = 0; + let has_multiple_rows = start_row + 1 != end_row; + for row in start_row..end_row { + let current_indent = snapshot.indent_size_for_line(MultiBufferRow(row)); + let indent_delta = match (current_indent.kind, indent_kind) { + (IndentKind::Space, IndentKind::Space) => { + let columns_to_next_tab_stop = tab_size - (current_indent.len % tab_size); + IndentSize::spaces(columns_to_next_tab_stop) + } + (IndentKind::Tab, IndentKind::Space) => IndentSize::spaces(tab_size), + (_, IndentKind::Tab) => IndentSize::tab(), + }; + + let start = if has_multiple_rows || current_indent.len < selection.start.column { + 0 + } else { + selection.start.column + }; + let row_start = Point::new(row, start); + edits.push(( + row_start..row_start, + indent_delta.chars().collect::(), + )); + + // Update this selection's endpoints to reflect the indentation. + if row == selection.start.row { + selection.start.column += indent_delta.len; + } + if row == selection.end.row { + selection.end.column += indent_delta.len; + delta_for_end_row = indent_delta.len; + } + } + + if selection.start.row == selection.end.row { + delta_for_start_row + delta_for_end_row + } else { + delta_for_end_row + } + } + + pub fn outdent(&mut self, _: &Outdent, window: &mut Window, cx: &mut Context) { + if self.read_only(cx) { + return; + } + self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let selections = self.selections.all::(cx); + let mut deletion_ranges = Vec::new(); + let mut last_outdent = None; + { + let buffer = self.buffer.read(cx); + let snapshot = buffer.snapshot(cx); + for selection in &selections { + let settings = buffer.language_settings_at(selection.start, cx); + let tab_size = settings.tab_size.get(); + let mut rows = selection.spanned_rows(false, &display_map); + + // Avoid re-outdenting a row that has already been outdented by a + // previous selection. + if let Some(last_row) = last_outdent { + if last_row == rows.start { + rows.start = rows.start.next_row(); + } + } + let has_multiple_rows = rows.len() > 1; + for row in rows.iter_rows() { + let indent_size = snapshot.indent_size_for_line(row); + if indent_size.len > 0 { + let deletion_len = match indent_size.kind { + IndentKind::Space => { + let columns_to_prev_tab_stop = indent_size.len % tab_size; + if columns_to_prev_tab_stop == 0 { + tab_size + } else { + columns_to_prev_tab_stop + } + } + IndentKind::Tab => 1, + }; + let start = if has_multiple_rows + || deletion_len > selection.start.column + || indent_size.len < selection.start.column + { + 0 + } else { + selection.start.column - deletion_len + }; + deletion_ranges.push( + Point::new(row.0, start)..Point::new(row.0, start + deletion_len), + ); + last_outdent = Some(row); + } + } + } + } + + self.transact(window, cx, |this, window, cx| { + this.buffer.update(cx, |buffer, cx| { + let empty_str: Arc = Arc::default(); + buffer.edit( + deletion_ranges + .into_iter() + .map(|range| (range, empty_str.clone())), + None, + cx, + ); + }); + let selections = this.selections.all::(cx); + this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.select(selections) + }); + }); + } + + pub fn autoindent(&mut self, _: &AutoIndent, window: &mut Window, cx: &mut Context) { + if self.read_only(cx) { + return; + } + self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + let selections = self + .selections + .all::(cx) + .into_iter() + .map(|s| s.range()); + + self.transact(window, cx, |this, window, cx| { + this.buffer.update(cx, |buffer, cx| { + buffer.autoindent_ranges(selections, cx); + }); + let selections = this.selections.all::(cx); + this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.select(selections) + }); + }); + } + + pub fn delete_line(&mut self, _: &DeleteLine, window: &mut Window, cx: &mut Context) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let selections = self.selections.all::(cx); + + let mut new_cursors = Vec::new(); + let mut edit_ranges = Vec::new(); + let mut selections = selections.iter().peekable(); + while let Some(selection) = selections.next() { + let mut rows = selection.spanned_rows(false, &display_map); + let goal_display_column = selection.head().to_display_point(&display_map).column(); + + // Accumulate contiguous regions of rows that we want to delete. + while let Some(next_selection) = selections.peek() { + let next_rows = next_selection.spanned_rows(false, &display_map); + if next_rows.start <= rows.end { + rows.end = next_rows.end; + selections.next().unwrap(); + } else { + break; + } + } + + let buffer = &display_map.buffer_snapshot; + let mut edit_start = Point::new(rows.start.0, 0).to_offset(buffer); + let edit_end; + let cursor_buffer_row; + if buffer.max_point().row >= rows.end.0 { + // If there's a line after the range, delete the \n from the end of the row range + // and position the cursor on the next line. + edit_end = Point::new(rows.end.0, 0).to_offset(buffer); + cursor_buffer_row = rows.end; + } else { + // If there isn't a line after the range, delete the \n from the line before the + // start of the row range and position the cursor there. + edit_start = edit_start.saturating_sub(1); + edit_end = buffer.len(); + cursor_buffer_row = rows.start.previous_row(); + } + + let mut cursor = Point::new(cursor_buffer_row.0, 0).to_display_point(&display_map); + *cursor.column_mut() = + cmp::min(goal_display_column, display_map.line_len(cursor.row())); + + new_cursors.push(( + selection.id, + buffer.anchor_after(cursor.to_point(&display_map)), + )); + edit_ranges.push(edit_start..edit_end); + } + + self.transact(window, cx, |this, window, cx| { + let buffer = this.buffer.update(cx, |buffer, cx| { + let empty_str: Arc = Arc::default(); + buffer.edit( + edit_ranges + .into_iter() + .map(|range| (range, empty_str.clone())), + None, + cx, + ); + buffer.snapshot(cx) + }); + let new_selections = new_cursors + .into_iter() + .map(|(id, cursor)| { + let cursor = cursor.to_point(&buffer); + Selection { + id, + start: cursor, + end: cursor, + reversed: false, + goal: SelectionGoal::None, + } + }) + .collect(); + + this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.select(new_selections); + }); + }); + } + + pub fn join_lines_impl( + &mut self, + insert_whitespace: bool, + window: &mut Window, + cx: &mut Context, + ) { + if self.read_only(cx) { + return; + } + let mut row_ranges = Vec::>::new(); + for selection in self.selections.all::(cx) { + let start = MultiBufferRow(selection.start.row); + // Treat single line selections as if they include the next line. Otherwise this action + // would do nothing for single line selections individual cursors. + let end = if selection.start.row == selection.end.row { + MultiBufferRow(selection.start.row + 1) + } else { + MultiBufferRow(selection.end.row) + }; + + if let Some(last_row_range) = row_ranges.last_mut() { + if start <= last_row_range.end { + last_row_range.end = end; + continue; + } + } + row_ranges.push(start..end); + } + + let snapshot = self.buffer.read(cx).snapshot(cx); + let mut cursor_positions = Vec::new(); + for row_range in &row_ranges { + let anchor = snapshot.anchor_before(Point::new( + row_range.end.previous_row().0, + snapshot.line_len(row_range.end.previous_row()), + )); + cursor_positions.push(anchor..anchor); + } + + self.transact(window, cx, |this, window, cx| { + for row_range in row_ranges.into_iter().rev() { + for row in row_range.iter_rows().rev() { + let end_of_line = Point::new(row.0, snapshot.line_len(row)); + let next_line_row = row.next_row(); + let indent = snapshot.indent_size_for_line(next_line_row); + let start_of_next_line = Point::new(next_line_row.0, indent.len); + + let replace = + if snapshot.line_len(next_line_row) > indent.len && insert_whitespace { + " " + } else { + "" + }; + + this.buffer.update(cx, |buffer, cx| { + buffer.edit([(end_of_line..start_of_next_line, replace)], None, cx) + }); + } + } + + this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.select_anchor_ranges(cursor_positions) + }); + }); + } + + pub fn join_lines(&mut self, _: &JoinLines, window: &mut Window, cx: &mut Context) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + self.join_lines_impl(true, window, cx); + } + + pub fn sort_lines_case_sensitive( + &mut self, + _: &SortLinesCaseSensitive, + window: &mut Window, + cx: &mut Context, + ) { + self.manipulate_lines(window, cx, |lines| lines.sort()) + } + + pub fn sort_lines_case_insensitive( + &mut self, + _: &SortLinesCaseInsensitive, + window: &mut Window, + cx: &mut Context, + ) { + self.manipulate_lines(window, cx, |lines| { + lines.sort_by_key(|line| line.to_lowercase()) + }) + } + + pub fn unique_lines_case_insensitive( + &mut self, + _: &UniqueLinesCaseInsensitive, + window: &mut Window, + cx: &mut Context, + ) { + self.manipulate_lines(window, cx, |lines| { + let mut seen = HashSet::default(); + lines.retain(|line| seen.insert(line.to_lowercase())); + }) + } + + pub fn unique_lines_case_sensitive( + &mut self, + _: &UniqueLinesCaseSensitive, + window: &mut Window, + cx: &mut Context, + ) { + self.manipulate_lines(window, cx, |lines| { + let mut seen = HashSet::default(); + lines.retain(|line| seen.insert(*line)); + }) + } + + pub fn reload_file(&mut self, _: &ReloadFile, window: &mut Window, cx: &mut Context) { + let Some(project) = self.project.clone() else { + return; + }; + self.reload(project, window, cx) + .detach_and_notify_err(window, cx); + } + + pub fn restore_file( + &mut self, + _: &::git::RestoreFile, + window: &mut Window, + cx: &mut Context, + ) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + let mut buffer_ids = HashSet::default(); + let snapshot = self.buffer().read(cx).snapshot(cx); + for selection in self.selections.all::(cx) { + buffer_ids.extend(snapshot.buffer_ids_for_range(selection.range())) + } + + let buffer = self.buffer().read(cx); + let ranges = buffer_ids + .into_iter() + .flat_map(|buffer_id| buffer.excerpt_ranges_for_buffer(buffer_id, cx)) + .collect::>(); + + self.restore_hunks_in_ranges(ranges, window, cx); + } + + pub fn git_restore(&mut self, _: &Restore, window: &mut Window, cx: &mut Context) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + let selections = self + .selections + .all(cx) + .into_iter() + .map(|s| s.range()) + .collect(); + self.restore_hunks_in_ranges(selections, window, cx); + } + + pub fn restore_hunks_in_ranges( + &mut self, + ranges: Vec>, + window: &mut Window, + cx: &mut Context, + ) { + let mut revert_changes = HashMap::default(); + let chunk_by = self + .snapshot(window, cx) + .hunks_for_ranges(ranges) + .into_iter() + .chunk_by(|hunk| hunk.buffer_id); + for (buffer_id, hunks) in &chunk_by { + let hunks = hunks.collect::>(); + for hunk in &hunks { + self.prepare_restore_change(&mut revert_changes, hunk, cx); + } + self.do_stage_or_unstage(false, buffer_id, hunks.into_iter(), cx); + } + drop(chunk_by); + if !revert_changes.is_empty() { + self.transact(window, cx, |editor, window, cx| { + editor.restore(revert_changes, window, cx); + }); + } + } + + pub fn open_active_item_in_terminal( + &mut self, + _: &OpenInTerminal, + window: &mut Window, + cx: &mut Context, + ) { + if let Some(working_directory) = self.active_excerpt(cx).and_then(|(_, buffer, _)| { + let project_path = buffer.read(cx).project_path(cx)?; + let project = self.project.as_ref()?.read(cx); + let entry = project.entry_for_path(&project_path, cx)?; + let parent = match &entry.canonical_path { + Some(canonical_path) => canonical_path.to_path_buf(), + None => project.absolute_path(&project_path, cx)?, + } + .parent()? + .to_path_buf(); + Some(parent) + }) { + window.dispatch_action(OpenTerminal { working_directory }.boxed_clone(), cx); + } + } + + fn set_breakpoint_context_menu( + &mut self, + display_row: DisplayRow, + position: Option, + clicked_point: gpui::Point, + window: &mut Window, + cx: &mut Context, + ) { + if !cx.has_flag::() { + return; + } + let source = self + .buffer + .read(cx) + .snapshot(cx) + .anchor_before(Point::new(display_row.0, 0u32)); + + let context_menu = self.breakpoint_context_menu(position.unwrap_or(source), window, cx); + + self.mouse_context_menu = MouseContextMenu::pinned_to_editor( + self, + source, + clicked_point, + context_menu, + window, + cx, + ); + } + + fn add_edit_breakpoint_block( + &mut self, + anchor: Anchor, + breakpoint: &Breakpoint, + edit_action: BreakpointPromptEditAction, + window: &mut Window, + cx: &mut Context, + ) { + let weak_editor = cx.weak_entity(); + let bp_prompt = cx.new(|cx| { + BreakpointPromptEditor::new( + weak_editor, + anchor, + breakpoint.clone(), + edit_action, + window, + cx, + ) + }); + + let height = bp_prompt.update(cx, |this, cx| { + this.prompt + .update(cx, |prompt, cx| prompt.max_point(cx).row().0 + 1 + 2) + }); + let cloned_prompt = bp_prompt.clone(); + let blocks = vec![BlockProperties { + style: BlockStyle::Sticky, + placement: BlockPlacement::Above(anchor), + height: Some(height), + render: Arc::new(move |cx| { + *cloned_prompt.read(cx).gutter_dimensions.lock() = *cx.gutter_dimensions; + cloned_prompt.clone().into_any_element() + }), + priority: 0, + }]; + + let focus_handle = bp_prompt.focus_handle(cx); + window.focus(&focus_handle); + + let block_ids = self.insert_blocks(blocks, None, cx); + bp_prompt.update(cx, |prompt, _| { + prompt.add_block_ids(block_ids); + }); + } + + pub(crate) fn breakpoint_at_row( + &self, + row: u32, + window: &mut Window, + cx: &mut Context, + ) -> Option<(Anchor, Breakpoint)> { + let snapshot = self.snapshot(window, cx); + let breakpoint_position = snapshot.buffer_snapshot.anchor_before(Point::new(row, 0)); + + self.breakpoint_at_anchor(breakpoint_position, &snapshot, cx) + } + + pub(crate) fn breakpoint_at_anchor( + &self, + breakpoint_position: Anchor, + snapshot: &EditorSnapshot, + cx: &mut Context, + ) -> Option<(Anchor, Breakpoint)> { + let project = self.project.clone()?; + + let buffer_id = breakpoint_position.buffer_id.or_else(|| { + snapshot + .buffer_snapshot + .buffer_id_for_excerpt(breakpoint_position.excerpt_id) + })?; + + let enclosing_excerpt = breakpoint_position.excerpt_id; + let buffer = project.read_with(cx, |project, cx| project.buffer_for_id(buffer_id, cx))?; + let buffer_snapshot = buffer.read(cx).snapshot(); + + let row = buffer_snapshot + .summary_for_anchor::(&breakpoint_position.text_anchor) + .row; + + let line_len = snapshot.buffer_snapshot.line_len(MultiBufferRow(row)); + let anchor_end = snapshot + .buffer_snapshot + .anchor_after(Point::new(row, line_len)); + + let bp = self + .breakpoint_store + .as_ref()? + .read_with(cx, |breakpoint_store, cx| { + breakpoint_store + .breakpoints( + &buffer, + Some(breakpoint_position.text_anchor..anchor_end.text_anchor), + &buffer_snapshot, + cx, + ) + .next() + .and_then(|(anchor, bp)| { + let breakpoint_row = buffer_snapshot + .summary_for_anchor::(anchor) + .row; + + if breakpoint_row == row { + snapshot + .buffer_snapshot + .anchor_in_excerpt(enclosing_excerpt, *anchor) + .map(|anchor| (anchor, bp.clone())) + } else { + None + } + }) + }); + bp + } + + pub fn edit_log_breakpoint( + &mut self, + _: &EditLogBreakpoint, + window: &mut Window, + cx: &mut Context, + ) { + for (anchor, breakpoint) in self.breakpoints_at_cursors(window, cx) { + let breakpoint = breakpoint.unwrap_or_else(|| Breakpoint { + message: None, + state: BreakpointState::Enabled, + condition: None, + hit_condition: None, + }); + + self.add_edit_breakpoint_block( + anchor, + &breakpoint, + BreakpointPromptEditAction::Log, + window, + cx, + ); + } + } + + fn breakpoints_at_cursors( + &self, + window: &mut Window, + cx: &mut Context, + ) -> Vec<(Anchor, Option)> { + let snapshot = self.snapshot(window, cx); + let cursors = self + .selections + .disjoint_anchors() + .into_iter() + .map(|selection| { + let cursor_position: Point = selection.head().to_point(&snapshot.buffer_snapshot); + + let breakpoint_position = self + .breakpoint_at_row(cursor_position.row, window, cx) + .map(|bp| bp.0) + .unwrap_or_else(|| { + snapshot + .display_snapshot + .buffer_snapshot + .anchor_after(Point::new(cursor_position.row, 0)) + }); + + let breakpoint = self + .breakpoint_at_anchor(breakpoint_position, &snapshot, cx) + .map(|(anchor, breakpoint)| (anchor, Some(breakpoint))); + + breakpoint.unwrap_or_else(|| (breakpoint_position, None)) + }) + // There might be multiple cursors on the same line; all of them should have the same anchors though as their breakpoints positions, which makes it possible to sort and dedup the list. + .collect::>(); + + cursors.into_iter().collect() + } + + pub fn enable_breakpoint( + &mut self, + _: &crate::actions::EnableBreakpoint, + window: &mut Window, + cx: &mut Context, + ) { + for (anchor, breakpoint) in self.breakpoints_at_cursors(window, cx) { + let Some(breakpoint) = breakpoint.filter(|breakpoint| breakpoint.is_disabled()) else { + continue; + }; + self.edit_breakpoint_at_anchor( + anchor, + breakpoint, + BreakpointEditAction::InvertState, + cx, + ); + } + } + + pub fn disable_breakpoint( + &mut self, + _: &crate::actions::DisableBreakpoint, + window: &mut Window, + cx: &mut Context, + ) { + for (anchor, breakpoint) in self.breakpoints_at_cursors(window, cx) { + let Some(breakpoint) = breakpoint.filter(|breakpoint| breakpoint.is_enabled()) else { + continue; + }; + self.edit_breakpoint_at_anchor( + anchor, + breakpoint, + BreakpointEditAction::InvertState, + cx, + ); + } + } + + pub fn toggle_breakpoint( + &mut self, + _: &crate::actions::ToggleBreakpoint, + window: &mut Window, + cx: &mut Context, + ) { + for (anchor, breakpoint) in self.breakpoints_at_cursors(window, cx) { + if let Some(breakpoint) = breakpoint { + self.edit_breakpoint_at_anchor( + anchor, + breakpoint, + BreakpointEditAction::Toggle, + cx, + ); + } else { + self.edit_breakpoint_at_anchor( + anchor, + Breakpoint::new_standard(), + BreakpointEditAction::Toggle, + cx, + ); + } + } + } + + pub fn edit_breakpoint_at_anchor( + &mut self, + breakpoint_position: Anchor, + breakpoint: Breakpoint, + edit_action: BreakpointEditAction, + cx: &mut Context, + ) { + let Some(breakpoint_store) = &self.breakpoint_store else { + return; + }; + + let Some(buffer_id) = breakpoint_position.buffer_id.or_else(|| { + if breakpoint_position == Anchor::min() { + self.buffer() + .read(cx) + .excerpt_buffer_ids() + .into_iter() + .next() + } else { + None + } + }) else { + return; + }; + + let Some(buffer) = self.buffer().read(cx).buffer(buffer_id) else { + return; + }; + + breakpoint_store.update(cx, |breakpoint_store, cx| { + breakpoint_store.toggle_breakpoint( + buffer, + (breakpoint_position.text_anchor, breakpoint), + edit_action, + cx, + ); + }); + + cx.notify(); + } + + #[cfg(any(test, feature = "test-support"))] + pub fn breakpoint_store(&self) -> Option> { + self.breakpoint_store.clone() + } + + pub fn prepare_restore_change( + &self, + revert_changes: &mut HashMap, Rope)>>, + hunk: &MultiBufferDiffHunk, + cx: &mut App, + ) -> Option<()> { + if hunk.is_created_file() { + return None; + } + let buffer = self.buffer.read(cx); + let diff = buffer.diff_for(hunk.buffer_id)?; + let buffer = buffer.buffer(hunk.buffer_id)?; + let buffer = buffer.read(cx); + let original_text = diff + .read(cx) + .base_text() + .as_rope() + .slice(hunk.diff_base_byte_range.clone()); + let buffer_snapshot = buffer.snapshot(); + let buffer_revert_changes = revert_changes.entry(buffer.remote_id()).or_default(); + if let Err(i) = buffer_revert_changes.binary_search_by(|probe| { + probe + .0 + .start + .cmp(&hunk.buffer_range.start, &buffer_snapshot) + .then(probe.0.end.cmp(&hunk.buffer_range.end, &buffer_snapshot)) + }) { + buffer_revert_changes.insert(i, (hunk.buffer_range.clone(), original_text)); + Some(()) + } else { + None + } + } + + pub fn reverse_lines(&mut self, _: &ReverseLines, window: &mut Window, cx: &mut Context) { + self.manipulate_lines(window, cx, |lines| lines.reverse()) + } + + pub fn shuffle_lines(&mut self, _: &ShuffleLines, window: &mut Window, cx: &mut Context) { + self.manipulate_lines(window, cx, |lines| lines.shuffle(&mut thread_rng())) + } + + fn manipulate_lines( + &mut self, + window: &mut Window, + cx: &mut Context, + mut callback: Fn, + ) where + Fn: FnMut(&mut Vec<&str>), + { + self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let buffer = self.buffer.read(cx).snapshot(cx); + + let mut edits = Vec::new(); + + let selections = self.selections.all::(cx); + let mut selections = selections.iter().peekable(); + let mut contiguous_row_selections = Vec::new(); + let mut new_selections = Vec::new(); + let mut added_lines = 0; + let mut removed_lines = 0; + + while let Some(selection) = selections.next() { + let (start_row, end_row) = consume_contiguous_rows( + &mut contiguous_row_selections, + selection, + &display_map, + &mut selections, + ); + + let start_point = Point::new(start_row.0, 0); + let end_point = Point::new( + end_row.previous_row().0, + buffer.line_len(end_row.previous_row()), + ); + let text = buffer + .text_for_range(start_point..end_point) + .collect::(); + + let mut lines = text.split('\n').collect_vec(); + + let lines_before = lines.len(); + callback(&mut lines); + let lines_after = lines.len(); + + edits.push((start_point..end_point, lines.join("\n"))); + + // Selections must change based on added and removed line count + let start_row = + MultiBufferRow(start_point.row + added_lines as u32 - removed_lines as u32); + let end_row = MultiBufferRow(start_row.0 + lines_after.saturating_sub(1) as u32); + new_selections.push(Selection { + id: selection.id, + start: start_row, + end: end_row, + goal: SelectionGoal::None, + reversed: selection.reversed, + }); + + if lines_after > lines_before { + added_lines += lines_after - lines_before; + } else if lines_before > lines_after { + removed_lines += lines_before - lines_after; + } + } + + self.transact(window, cx, |this, window, cx| { + let buffer = this.buffer.update(cx, |buffer, cx| { + buffer.edit(edits, None, cx); + buffer.snapshot(cx) + }); + + // Recalculate offsets on newly edited buffer + let new_selections = new_selections + .iter() + .map(|s| { + let start_point = Point::new(s.start.0, 0); + let end_point = Point::new(s.end.0, buffer.line_len(s.end)); + Selection { + id: s.id, + start: buffer.point_to_offset(start_point), + end: buffer.point_to_offset(end_point), + goal: s.goal, + reversed: s.reversed, + } + }) + .collect(); + + this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.select(new_selections); + }); + + this.request_autoscroll(Autoscroll::fit(), cx); + }); + } + + pub fn toggle_case(&mut self, _: &ToggleCase, window: &mut Window, cx: &mut Context) { + self.manipulate_text(window, cx, |text| { + let has_upper_case_characters = text.chars().any(|c| c.is_uppercase()); + if has_upper_case_characters { + text.to_lowercase() + } else { + text.to_uppercase() + } + }) + } + + pub fn convert_to_upper_case( + &mut self, + _: &ConvertToUpperCase, + window: &mut Window, + cx: &mut Context, + ) { + self.manipulate_text(window, cx, |text| text.to_uppercase()) + } + + pub fn convert_to_lower_case( + &mut self, + _: &ConvertToLowerCase, + window: &mut Window, + cx: &mut Context, + ) { + self.manipulate_text(window, cx, |text| text.to_lowercase()) + } + + pub fn convert_to_title_case( + &mut self, + _: &ConvertToTitleCase, + window: &mut Window, + cx: &mut Context, + ) { + self.manipulate_text(window, cx, |text| { + text.split('\n') + .map(|line| line.to_case(Case::Title)) + .join("\n") + }) + } + + pub fn convert_to_snake_case( + &mut self, + _: &ConvertToSnakeCase, + window: &mut Window, + cx: &mut Context, + ) { + self.manipulate_text(window, cx, |text| text.to_case(Case::Snake)) + } + + pub fn convert_to_kebab_case( + &mut self, + _: &ConvertToKebabCase, + window: &mut Window, + cx: &mut Context, + ) { + self.manipulate_text(window, cx, |text| text.to_case(Case::Kebab)) + } + + pub fn convert_to_upper_camel_case( + &mut self, + _: &ConvertToUpperCamelCase, + window: &mut Window, + cx: &mut Context, + ) { + self.manipulate_text(window, cx, |text| { + text.split('\n') + .map(|line| line.to_case(Case::UpperCamel)) + .join("\n") + }) + } + + pub fn convert_to_lower_camel_case( + &mut self, + _: &ConvertToLowerCamelCase, + window: &mut Window, + cx: &mut Context, + ) { + self.manipulate_text(window, cx, |text| text.to_case(Case::Camel)) + } + + pub fn convert_to_opposite_case( + &mut self, + _: &ConvertToOppositeCase, + window: &mut Window, + cx: &mut Context, + ) { + self.manipulate_text(window, cx, |text| { + text.chars() + .fold(String::with_capacity(text.len()), |mut t, c| { + if c.is_uppercase() { + t.extend(c.to_lowercase()); + } else { + t.extend(c.to_uppercase()); + } + t + }) + }) + } + + pub fn convert_to_rot13( + &mut self, + _: &ConvertToRot13, + window: &mut Window, + cx: &mut Context, + ) { + self.manipulate_text(window, cx, |text| { + text.chars() + .map(|c| match c { + 'A'..='M' | 'a'..='m' => ((c as u8) + 13) as char, + 'N'..='Z' | 'n'..='z' => ((c as u8) - 13) as char, + _ => c, + }) + .collect() + }) + } + + pub fn convert_to_rot47( + &mut self, + _: &ConvertToRot47, + window: &mut Window, + cx: &mut Context, + ) { + self.manipulate_text(window, cx, |text| { + text.chars() + .map(|c| { + let code_point = c as u32; + if code_point >= 33 && code_point <= 126 { + return char::from_u32(33 + ((code_point + 14) % 94)).unwrap(); + } + c + }) + .collect() + }) + } + + fn manipulate_text(&mut self, window: &mut Window, cx: &mut Context, mut callback: Fn) + where + Fn: FnMut(&str) -> String, + { + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let buffer = self.buffer.read(cx).snapshot(cx); + + let mut new_selections = Vec::new(); + let mut edits = Vec::new(); + let mut selection_adjustment = 0i32; + + for selection in self.selections.all::(cx) { + let selection_is_empty = selection.is_empty(); + + let (start, end) = if selection_is_empty { + let word_range = movement::surrounding_word( + &display_map, + selection.start.to_display_point(&display_map), + ); + let start = word_range.start.to_offset(&display_map, Bias::Left); + let end = word_range.end.to_offset(&display_map, Bias::Left); + (start, end) + } else { + (selection.start, selection.end) + }; + + let text = buffer.text_for_range(start..end).collect::(); + let old_length = text.len() as i32; + let text = callback(&text); + + new_selections.push(Selection { + start: (start as i32 - selection_adjustment) as usize, + end: ((start + text.len()) as i32 - selection_adjustment) as usize, + goal: SelectionGoal::None, + ..selection + }); + + selection_adjustment += old_length - text.len() as i32; + + edits.push((start..end, text)); + } + + self.transact(window, cx, |this, window, cx| { + this.buffer.update(cx, |buffer, cx| { + buffer.edit(edits, None, cx); + }); + + this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.select(new_selections); + }); + + this.request_autoscroll(Autoscroll::fit(), cx); + }); + } + + pub fn duplicate( + &mut self, + upwards: bool, + whole_lines: bool, + window: &mut Window, + cx: &mut Context, + ) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let buffer = &display_map.buffer_snapshot; + let selections = self.selections.all::(cx); + + let mut edits = Vec::new(); + let mut selections_iter = selections.iter().peekable(); + while let Some(selection) = selections_iter.next() { + let mut rows = selection.spanned_rows(false, &display_map); + // duplicate line-wise + if whole_lines || selection.start == selection.end { + // Avoid duplicating the same lines twice. + while let Some(next_selection) = selections_iter.peek() { + let next_rows = next_selection.spanned_rows(false, &display_map); + if next_rows.start < rows.end { + rows.end = next_rows.end; + selections_iter.next().unwrap(); + } else { + break; + } + } + + // Copy the text from the selected row region and splice it either at the start + // or end of the region. + let start = Point::new(rows.start.0, 0); + let end = Point::new( + rows.end.previous_row().0, + buffer.line_len(rows.end.previous_row()), + ); + let text = buffer + .text_for_range(start..end) + .chain(Some("\n")) + .collect::(); + let insert_location = if upwards { + Point::new(rows.end.0, 0) + } else { + start + }; + edits.push((insert_location..insert_location, text)); + } else { + // duplicate character-wise + let start = selection.start; + let end = selection.end; + let text = buffer.text_for_range(start..end).collect::(); + edits.push((selection.end..selection.end, text)); + } + } + + self.transact(window, cx, |this, _, cx| { + this.buffer.update(cx, |buffer, cx| { + buffer.edit(edits, None, cx); + }); + + this.request_autoscroll(Autoscroll::fit(), cx); + }); + } + + pub fn duplicate_line_up( + &mut self, + _: &DuplicateLineUp, + window: &mut Window, + cx: &mut Context, + ) { + self.duplicate(true, true, window, cx); + } + + pub fn duplicate_line_down( + &mut self, + _: &DuplicateLineDown, + window: &mut Window, + cx: &mut Context, + ) { + self.duplicate(false, true, window, cx); + } + + pub fn duplicate_selection( + &mut self, + _: &DuplicateSelection, + window: &mut Window, + cx: &mut Context, + ) { + self.duplicate(false, false, window, cx); + } + + pub fn move_line_up(&mut self, _: &MoveLineUp, window: &mut Window, cx: &mut Context) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let buffer = self.buffer.read(cx).snapshot(cx); + + let mut edits = Vec::new(); + let mut unfold_ranges = Vec::new(); + let mut refold_creases = Vec::new(); + + let selections = self.selections.all::(cx); + let mut selections = selections.iter().peekable(); + let mut contiguous_row_selections = Vec::new(); + let mut new_selections = Vec::new(); + + while let Some(selection) = selections.next() { + // Find all the selections that span a contiguous row range + let (start_row, end_row) = consume_contiguous_rows( + &mut contiguous_row_selections, + selection, + &display_map, + &mut selections, + ); + + // Move the text spanned by the row range to be before the line preceding the row range + if start_row.0 > 0 { + let range_to_move = Point::new( + start_row.previous_row().0, + buffer.line_len(start_row.previous_row()), + ) + ..Point::new( + end_row.previous_row().0, + buffer.line_len(end_row.previous_row()), + ); + let insertion_point = display_map + .prev_line_boundary(Point::new(start_row.previous_row().0, 0)) + .0; + + // Don't move lines across excerpts + if buffer + .excerpt_containing(insertion_point..range_to_move.end) + .is_some() + { + let text = buffer + .text_for_range(range_to_move.clone()) + .flat_map(|s| s.chars()) + .skip(1) + .chain(['\n']) + .collect::(); + + edits.push(( + buffer.anchor_after(range_to_move.start) + ..buffer.anchor_before(range_to_move.end), + String::new(), + )); + let insertion_anchor = buffer.anchor_after(insertion_point); + edits.push((insertion_anchor..insertion_anchor, text)); + + let row_delta = range_to_move.start.row - insertion_point.row + 1; + + // Move selections up + new_selections.extend(contiguous_row_selections.drain(..).map( + |mut selection| { + selection.start.row -= row_delta; + selection.end.row -= row_delta; + selection + }, + )); + + // Move folds up + unfold_ranges.push(range_to_move.clone()); + for fold in display_map.folds_in_range( + buffer.anchor_before(range_to_move.start) + ..buffer.anchor_after(range_to_move.end), + ) { + let mut start = fold.range.start.to_point(&buffer); + let mut end = fold.range.end.to_point(&buffer); + start.row -= row_delta; + end.row -= row_delta; + refold_creases.push(Crease::simple(start..end, fold.placeholder.clone())); + } + } + } + + // If we didn't move line(s), preserve the existing selections + new_selections.append(&mut contiguous_row_selections); + } + + self.transact(window, cx, |this, window, cx| { + this.unfold_ranges(&unfold_ranges, true, true, cx); + this.buffer.update(cx, |buffer, cx| { + for (range, text) in edits { + buffer.edit([(range, text)], None, cx); + } + }); + this.fold_creases(refold_creases, true, window, cx); + this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.select(new_selections); + }) + }); + } + + pub fn move_line_down( + &mut self, + _: &MoveLineDown, + window: &mut Window, + cx: &mut Context, + ) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let buffer = self.buffer.read(cx).snapshot(cx); + + let mut edits = Vec::new(); + let mut unfold_ranges = Vec::new(); + let mut refold_creases = Vec::new(); + + let selections = self.selections.all::(cx); + let mut selections = selections.iter().peekable(); + let mut contiguous_row_selections = Vec::new(); + let mut new_selections = Vec::new(); + + while let Some(selection) = selections.next() { + // Find all the selections that span a contiguous row range + let (start_row, end_row) = consume_contiguous_rows( + &mut contiguous_row_selections, + selection, + &display_map, + &mut selections, + ); + + // Move the text spanned by the row range to be after the last line of the row range + if end_row.0 <= buffer.max_point().row { + let range_to_move = + MultiBufferPoint::new(start_row.0, 0)..MultiBufferPoint::new(end_row.0, 0); + let insertion_point = display_map + .next_line_boundary(MultiBufferPoint::new(end_row.0, 0)) + .0; + + // Don't move lines across excerpt boundaries + if buffer + .excerpt_containing(range_to_move.start..insertion_point) + .is_some() + { + let mut text = String::from("\n"); + text.extend(buffer.text_for_range(range_to_move.clone())); + text.pop(); // Drop trailing newline + edits.push(( + buffer.anchor_after(range_to_move.start) + ..buffer.anchor_before(range_to_move.end), + String::new(), + )); + let insertion_anchor = buffer.anchor_after(insertion_point); + edits.push((insertion_anchor..insertion_anchor, text)); + + let row_delta = insertion_point.row - range_to_move.end.row + 1; + + // Move selections down + new_selections.extend(contiguous_row_selections.drain(..).map( + |mut selection| { + selection.start.row += row_delta; + selection.end.row += row_delta; + selection + }, + )); + + // Move folds down + unfold_ranges.push(range_to_move.clone()); + for fold in display_map.folds_in_range( + buffer.anchor_before(range_to_move.start) + ..buffer.anchor_after(range_to_move.end), + ) { + let mut start = fold.range.start.to_point(&buffer); + let mut end = fold.range.end.to_point(&buffer); + start.row += row_delta; + end.row += row_delta; + refold_creases.push(Crease::simple(start..end, fold.placeholder.clone())); + } + } + } + + // If we didn't move line(s), preserve the existing selections + new_selections.append(&mut contiguous_row_selections); + } + + self.transact(window, cx, |this, window, cx| { + this.unfold_ranges(&unfold_ranges, true, true, cx); + this.buffer.update(cx, |buffer, cx| { + for (range, text) in edits { + buffer.edit([(range, text)], None, cx); + } + }); + this.fold_creases(refold_creases, true, window, cx); + this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.select(new_selections) + }); + }); + } + + pub fn transpose(&mut self, _: &Transpose, window: &mut Window, cx: &mut Context) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + let text_layout_details = &self.text_layout_details(window); + self.transact(window, cx, |this, window, cx| { + let edits = this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + let mut edits: Vec<(Range, String)> = Default::default(); + s.move_with(|display_map, selection| { + if !selection.is_empty() { + return; + } + + let mut head = selection.head(); + let mut transpose_offset = head.to_offset(display_map, Bias::Right); + if head.column() == display_map.line_len(head.row()) { + transpose_offset = display_map + .buffer_snapshot + .clip_offset(transpose_offset.saturating_sub(1), Bias::Left); + } + + if transpose_offset == 0 { + return; + } + + *head.column_mut() += 1; + head = display_map.clip_point(head, Bias::Right); + let goal = SelectionGoal::HorizontalPosition( + display_map + .x_for_display_point(head, text_layout_details) + .into(), + ); + selection.collapse_to(head, goal); + + let transpose_start = display_map + .buffer_snapshot + .clip_offset(transpose_offset.saturating_sub(1), Bias::Left); + if edits.last().map_or(true, |e| e.0.end <= transpose_start) { + let transpose_end = display_map + .buffer_snapshot + .clip_offset(transpose_offset + 1, Bias::Right); + if let Some(ch) = + display_map.buffer_snapshot.chars_at(transpose_start).next() + { + edits.push((transpose_start..transpose_offset, String::new())); + edits.push((transpose_end..transpose_end, ch.to_string())); + } + } + }); + edits + }); + this.buffer + .update(cx, |buffer, cx| buffer.edit(edits, None, cx)); + let selections = this.selections.all::(cx); + this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.select(selections); + }); + }); + } + + pub fn rewrap(&mut self, _: &Rewrap, _: &mut Window, cx: &mut Context) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + self.rewrap_impl(RewrapOptions::default(), cx) + } + + pub fn rewrap_impl(&mut self, options: RewrapOptions, cx: &mut Context) { + let buffer = self.buffer.read(cx).snapshot(cx); + let selections = self.selections.all::(cx); + let mut selections = selections.iter().peekable(); + + let mut edits = Vec::new(); + let mut rewrapped_row_ranges = Vec::>::new(); + + while let Some(selection) = selections.next() { + let mut start_row = selection.start.row; + let mut end_row = selection.end.row; + + // Skip selections that overlap with a range that has already been rewrapped. + let selection_range = start_row..end_row; + if rewrapped_row_ranges + .iter() + .any(|range| range.overlaps(&selection_range)) + { + continue; + } + + let tab_size = buffer.language_settings_at(selection.head(), cx).tab_size; + + // Since not all lines in the selection may be at the same indent + // level, choose the indent size that is the most common between all + // of the lines. + // + // If there is a tie, we use the deepest indent. + let (indent_size, indent_end) = { + let mut indent_size_occurrences = HashMap::default(); + let mut rows_by_indent_size = HashMap::>::default(); + + for row in start_row..=end_row { + let indent = buffer.indent_size_for_line(MultiBufferRow(row)); + rows_by_indent_size.entry(indent).or_default().push(row); + *indent_size_occurrences.entry(indent).or_insert(0) += 1; + } + + let indent_size = indent_size_occurrences + .into_iter() + .max_by_key(|(indent, count)| (*count, indent.len_with_expanded_tabs(tab_size))) + .map(|(indent, _)| indent) + .unwrap_or_default(); + let row = rows_by_indent_size[&indent_size][0]; + let indent_end = Point::new(row, indent_size.len); + + (indent_size, indent_end) + }; + + let mut line_prefix = indent_size.chars().collect::(); + + let mut inside_comment = false; + if let Some(comment_prefix) = + buffer + .language_scope_at(selection.head()) + .and_then(|language| { + language + .line_comment_prefixes() + .iter() + .find(|prefix| buffer.contains_str_at(indent_end, prefix)) + .cloned() + }) + { + line_prefix.push_str(&comment_prefix); + inside_comment = true; + } + + let language_settings = buffer.language_settings_at(selection.head(), cx); + let allow_rewrap_based_on_language = match language_settings.allow_rewrap { + RewrapBehavior::InComments => inside_comment, + RewrapBehavior::InSelections => !selection.is_empty(), + RewrapBehavior::Anywhere => true, + }; + + let should_rewrap = options.override_language_settings + || allow_rewrap_based_on_language + || self.hard_wrap.is_some(); + if !should_rewrap { + continue; + } + + if selection.is_empty() { + 'expand_upwards: while start_row > 0 { + let prev_row = start_row - 1; + if buffer.contains_str_at(Point::new(prev_row, 0), &line_prefix) + && buffer.line_len(MultiBufferRow(prev_row)) as usize > line_prefix.len() + { + start_row = prev_row; + } else { + break 'expand_upwards; + } + } + + 'expand_downwards: while end_row < buffer.max_point().row { + let next_row = end_row + 1; + if buffer.contains_str_at(Point::new(next_row, 0), &line_prefix) + && buffer.line_len(MultiBufferRow(next_row)) as usize > line_prefix.len() + { + end_row = next_row; + } else { + break 'expand_downwards; + } + } + } + + let start = Point::new(start_row, 0); + let start_offset = start.to_offset(&buffer); + let end = Point::new(end_row, buffer.line_len(MultiBufferRow(end_row))); + let selection_text = buffer.text_for_range(start..end).collect::(); + let Some(lines_without_prefixes) = selection_text + .lines() + .map(|line| { + line.strip_prefix(&line_prefix) + .or_else(|| line.trim_start().strip_prefix(&line_prefix.trim_start())) + .ok_or_else(|| { + anyhow!("line did not start with prefix {line_prefix:?}: {line:?}") + }) + }) + .collect::, _>>() + .log_err() + else { + continue; + }; + + let wrap_column = self.hard_wrap.unwrap_or_else(|| { + buffer + .language_settings_at(Point::new(start_row, 0), cx) + .preferred_line_length as usize + }); + let wrapped_text = wrap_with_prefix( + line_prefix, + lines_without_prefixes.join("\n"), + wrap_column, + tab_size, + options.preserve_existing_whitespace, + ); + + // TODO: should always use char-based diff while still supporting cursor behavior that + // matches vim. + let mut diff_options = DiffOptions::default(); + if options.override_language_settings { + diff_options.max_word_diff_len = 0; + diff_options.max_word_diff_line_count = 0; + } else { + diff_options.max_word_diff_len = usize::MAX; + diff_options.max_word_diff_line_count = usize::MAX; + } + + for (old_range, new_text) in + text_diff_with_options(&selection_text, &wrapped_text, diff_options) + { + let edit_start = buffer.anchor_after(start_offset + old_range.start); + let edit_end = buffer.anchor_after(start_offset + old_range.end); + edits.push((edit_start..edit_end, new_text)); + } + + rewrapped_row_ranges.push(start_row..=end_row); + } + + self.buffer + .update(cx, |buffer, cx| buffer.edit(edits, None, cx)); + } + + pub fn cut_common(&mut self, window: &mut Window, cx: &mut Context) -> ClipboardItem { + let mut text = String::new(); + let buffer = self.buffer.read(cx).snapshot(cx); + let mut selections = self.selections.all::(cx); + let mut clipboard_selections = Vec::with_capacity(selections.len()); + { + let max_point = buffer.max_point(); + let mut is_first = true; + for selection in &mut selections { + let is_entire_line = selection.is_empty() || self.selections.line_mode; + if is_entire_line { + selection.start = Point::new(selection.start.row, 0); + if !selection.is_empty() && selection.end.column == 0 { + selection.end = cmp::min(max_point, selection.end); + } else { + selection.end = cmp::min(max_point, Point::new(selection.end.row + 1, 0)); + } + selection.goal = SelectionGoal::None; + } + if is_first { + is_first = false; + } else { + text += "\n"; + } + let mut len = 0; + for chunk in buffer.text_for_range(selection.start..selection.end) { + text.push_str(chunk); + len += chunk.len(); + } + clipboard_selections.push(ClipboardSelection { + len, + is_entire_line, + first_line_indent: buffer + .indent_size_for_line(MultiBufferRow(selection.start.row)) + .len, + }); + } + } + + self.transact(window, cx, |this, window, cx| { + this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.select(selections); + }); + this.insert("", window, cx); + }); + ClipboardItem::new_string_with_json_metadata(text, clipboard_selections) + } + + pub fn cut(&mut self, _: &Cut, window: &mut Window, cx: &mut Context) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + let item = self.cut_common(window, cx); + cx.write_to_clipboard(item); + } + + pub fn kill_ring_cut(&mut self, _: &KillRingCut, window: &mut Window, cx: &mut Context) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + self.change_selections(None, window, cx, |s| { + s.move_with(|snapshot, sel| { + if sel.is_empty() { + sel.end = DisplayPoint::new(sel.end.row(), snapshot.line_len(sel.end.row())) + } + }); + }); + let item = self.cut_common(window, cx); + cx.set_global(KillRing(item)) + } + + pub fn kill_ring_yank( + &mut self, + _: &KillRingYank, + window: &mut Window, + cx: &mut Context, + ) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + let (text, metadata) = if let Some(KillRing(item)) = cx.try_global() { + if let Some(ClipboardEntry::String(kill_ring)) = item.entries().first() { + (kill_ring.text().to_string(), kill_ring.metadata_json()) + } else { + return; + } + } else { + return; + }; + self.do_paste(&text, metadata, false, window, cx); + } + + pub fn copy_and_trim(&mut self, _: &CopyAndTrim, _: &mut Window, cx: &mut Context) { + self.do_copy(true, cx); + } + + pub fn copy(&mut self, _: &Copy, _: &mut Window, cx: &mut Context) { + self.do_copy(false, cx); + } + + fn do_copy(&self, strip_leading_indents: bool, cx: &mut Context) { + let selections = self.selections.all::(cx); + let buffer = self.buffer.read(cx).read(cx); + let mut text = String::new(); + + let mut clipboard_selections = Vec::with_capacity(selections.len()); + { + let max_point = buffer.max_point(); + let mut is_first = true; + for selection in &selections { + let mut start = selection.start; + let mut end = selection.end; + let is_entire_line = selection.is_empty() || self.selections.line_mode; + if is_entire_line { + start = Point::new(start.row, 0); + end = cmp::min(max_point, Point::new(end.row + 1, 0)); + } + + let mut trimmed_selections = Vec::new(); + if strip_leading_indents && end.row.saturating_sub(start.row) > 0 { + let row = MultiBufferRow(start.row); + let first_indent = buffer.indent_size_for_line(row); + if first_indent.len == 0 || start.column > first_indent.len { + trimmed_selections.push(start..end); + } else { + trimmed_selections.push( + Point::new(row.0, first_indent.len) + ..Point::new(row.0, buffer.line_len(row)), + ); + for row in start.row + 1..=end.row { + let mut line_len = buffer.line_len(MultiBufferRow(row)); + if row == end.row { + line_len = end.column; + } + if line_len == 0 { + trimmed_selections + .push(Point::new(row, 0)..Point::new(row, line_len)); + continue; + } + let row_indent_size = buffer.indent_size_for_line(MultiBufferRow(row)); + if row_indent_size.len >= first_indent.len { + trimmed_selections.push( + Point::new(row, first_indent.len)..Point::new(row, line_len), + ); + } else { + trimmed_selections.clear(); + trimmed_selections.push(start..end); + break; + } + } + } + } else { + trimmed_selections.push(start..end); + } + + for trimmed_range in trimmed_selections { + if is_first { + is_first = false; + } else { + text += "\n"; + } + let mut len = 0; + for chunk in buffer.text_for_range(trimmed_range.start..trimmed_range.end) { + text.push_str(chunk); + len += chunk.len(); + } + clipboard_selections.push(ClipboardSelection { + len, + is_entire_line, + first_line_indent: buffer + .indent_size_for_line(MultiBufferRow(trimmed_range.start.row)) + .len, + }); + } + } + } + + cx.write_to_clipboard(ClipboardItem::new_string_with_json_metadata( + text, + clipboard_selections, + )); + } + + pub fn do_paste( + &mut self, + text: &String, + clipboard_selections: Option>, + handle_entire_lines: bool, + window: &mut Window, + cx: &mut Context, + ) { + if self.read_only(cx) { + return; + } + + let clipboard_text = Cow::Borrowed(text); + + self.transact(window, cx, |this, window, cx| { + if let Some(mut clipboard_selections) = clipboard_selections { + let old_selections = this.selections.all::(cx); + let all_selections_were_entire_line = + clipboard_selections.iter().all(|s| s.is_entire_line); + let first_selection_indent_column = + clipboard_selections.first().map(|s| s.first_line_indent); + if clipboard_selections.len() != old_selections.len() { + clipboard_selections.drain(..); + } + let cursor_offset = this.selections.last::(cx).head(); + let mut auto_indent_on_paste = true; + + this.buffer.update(cx, |buffer, cx| { + let snapshot = buffer.read(cx); + auto_indent_on_paste = snapshot + .language_settings_at(cursor_offset, cx) + .auto_indent_on_paste; + + let mut start_offset = 0; + let mut edits = Vec::new(); + let mut original_indent_columns = Vec::new(); + for (ix, selection) in old_selections.iter().enumerate() { + let to_insert; + let entire_line; + let original_indent_column; + if let Some(clipboard_selection) = clipboard_selections.get(ix) { + let end_offset = start_offset + clipboard_selection.len; + to_insert = &clipboard_text[start_offset..end_offset]; + entire_line = clipboard_selection.is_entire_line; + start_offset = end_offset + 1; + original_indent_column = Some(clipboard_selection.first_line_indent); + } else { + to_insert = clipboard_text.as_str(); + entire_line = all_selections_were_entire_line; + original_indent_column = first_selection_indent_column + } + + // If the corresponding selection was empty when this slice of the + // clipboard text was written, then the entire line containing the + // selection was copied. If this selection is also currently empty, + // then paste the line before the current line of the buffer. + let range = if selection.is_empty() && handle_entire_lines && entire_line { + let column = selection.start.to_point(&snapshot).column as usize; + let line_start = selection.start - column; + line_start..line_start + } else { + selection.range() + }; + + edits.push((range, to_insert)); + original_indent_columns.push(original_indent_column); + } + drop(snapshot); + + buffer.edit( + edits, + if auto_indent_on_paste { + Some(AutoindentMode::Block { + original_indent_columns, + }) + } else { + None + }, + cx, + ); + }); + + let selections = this.selections.all::(cx); + this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.select(selections) + }); + } else { + this.insert(&clipboard_text, window, cx); + } + }); + } + + pub fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + if let Some(item) = cx.read_from_clipboard() { + let entries = item.entries(); + + match entries.first() { + // For now, we only support applying metadata if there's one string. In the future, we can incorporate all the selections + // of all the pasted entries. + Some(ClipboardEntry::String(clipboard_string)) if entries.len() == 1 => self + .do_paste( + clipboard_string.text(), + clipboard_string.metadata_json::>(), + true, + window, + cx, + ), + _ => self.do_paste(&item.text().unwrap_or_default(), None, true, window, cx), + } + } + } + + pub fn undo(&mut self, _: &Undo, window: &mut Window, cx: &mut Context) { + if self.read_only(cx) { + return; + } + + self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + + if let Some(transaction_id) = self.buffer.update(cx, |buffer, cx| buffer.undo(cx)) { + if let Some((selections, _)) = + self.selection_history.transaction(transaction_id).cloned() + { + self.change_selections(None, window, cx, |s| { + s.select_anchors(selections.to_vec()); + }); + } else { + log::error!( + "No entry in selection_history found for undo. \ + This may correspond to a bug where undo does not update the selection. \ + If this is occurring, please add details to \ + https://github.com/zed-industries/zed/issues/22692" + ); + } + self.request_autoscroll(Autoscroll::fit(), cx); + self.unmark_text(window, cx); + self.refresh_inline_completion(true, false, window, cx); + cx.emit(EditorEvent::Edited { transaction_id }); + cx.emit(EditorEvent::TransactionUndone { transaction_id }); + } + } + + pub fn redo(&mut self, _: &Redo, window: &mut Window, cx: &mut Context) { + if self.read_only(cx) { + return; + } + + self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + + if let Some(transaction_id) = self.buffer.update(cx, |buffer, cx| buffer.redo(cx)) { + if let Some((_, Some(selections))) = + self.selection_history.transaction(transaction_id).cloned() + { + self.change_selections(None, window, cx, |s| { + s.select_anchors(selections.to_vec()); + }); + } else { + log::error!( + "No entry in selection_history found for redo. \ + This may correspond to a bug where undo does not update the selection. \ + If this is occurring, please add details to \ + https://github.com/zed-industries/zed/issues/22692" + ); + } + self.request_autoscroll(Autoscroll::fit(), cx); + self.unmark_text(window, cx); + self.refresh_inline_completion(true, false, window, cx); + cx.emit(EditorEvent::Edited { transaction_id }); + } + } + + pub fn finalize_last_transaction(&mut self, cx: &mut Context) { + self.buffer + .update(cx, |buffer, cx| buffer.finalize_last_transaction(cx)); + } + + pub fn group_until_transaction(&mut self, tx_id: TransactionId, cx: &mut Context) { + self.buffer + .update(cx, |buffer, cx| buffer.group_until_transaction(tx_id, cx)); + } + + pub fn move_left(&mut self, _: &MoveLeft, window: &mut Window, cx: &mut Context) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.move_with(|map, selection| { + let cursor = if selection.is_empty() { + movement::left(map, selection.start) + } else { + selection.start + }; + selection.collapse_to(cursor, SelectionGoal::None); + }); + }) + } + + pub fn select_left(&mut self, _: &SelectLeft, window: &mut Window, cx: &mut Context) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.move_heads_with(|map, head, _| (movement::left(map, head), SelectionGoal::None)); + }) + } + + pub fn move_right(&mut self, _: &MoveRight, window: &mut Window, cx: &mut Context) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.move_with(|map, selection| { + let cursor = if selection.is_empty() { + movement::right(map, selection.end) + } else { + selection.end + }; + selection.collapse_to(cursor, SelectionGoal::None) + }); + }) + } + + pub fn select_right(&mut self, _: &SelectRight, window: &mut Window, cx: &mut Context) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.move_heads_with(|map, head, _| (movement::right(map, head), SelectionGoal::None)); + }) + } + + pub fn move_up(&mut self, _: &MoveUp, window: &mut Window, cx: &mut Context) { + if self.take_rename(true, window, cx).is_some() { + return; + } + + if matches!(self.mode, EditorMode::SingleLine { .. }) { + cx.propagate(); + return; + } + + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + + let text_layout_details = &self.text_layout_details(window); + let selection_count = self.selections.count(); + let first_selection = self.selections.first_anchor(); + + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.move_with(|map, selection| { + if !selection.is_empty() { + selection.goal = SelectionGoal::None; + } + let (cursor, goal) = movement::up( + map, + selection.start, + selection.goal, + false, + text_layout_details, + ); + selection.collapse_to(cursor, goal); + }); + }); + + if selection_count == 1 && first_selection.range() == self.selections.first_anchor().range() + { + cx.propagate(); + } + } + + pub fn move_up_by_lines( + &mut self, + action: &MoveUpByLines, + window: &mut Window, + cx: &mut Context, + ) { + if self.take_rename(true, window, cx).is_some() { + return; + } + + if matches!(self.mode, EditorMode::SingleLine { .. }) { + cx.propagate(); + return; + } + + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + + let text_layout_details = &self.text_layout_details(window); + + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.move_with(|map, selection| { + if !selection.is_empty() { + selection.goal = SelectionGoal::None; + } + let (cursor, goal) = movement::up_by_rows( + map, + selection.start, + action.lines, + selection.goal, + false, + text_layout_details, + ); + selection.collapse_to(cursor, goal); + }); + }) + } + + pub fn move_down_by_lines( + &mut self, + action: &MoveDownByLines, + window: &mut Window, + cx: &mut Context, + ) { + if self.take_rename(true, window, cx).is_some() { + return; + } + + if matches!(self.mode, EditorMode::SingleLine { .. }) { + cx.propagate(); + return; + } + + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + + let text_layout_details = &self.text_layout_details(window); + + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.move_with(|map, selection| { + if !selection.is_empty() { + selection.goal = SelectionGoal::None; + } + let (cursor, goal) = movement::down_by_rows( + map, + selection.start, + action.lines, + selection.goal, + false, + text_layout_details, + ); + selection.collapse_to(cursor, goal); + }); + }) + } + + pub fn select_down_by_lines( + &mut self, + action: &SelectDownByLines, + window: &mut Window, + cx: &mut Context, + ) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + let text_layout_details = &self.text_layout_details(window); + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.move_heads_with(|map, head, goal| { + movement::down_by_rows(map, head, action.lines, goal, false, text_layout_details) + }) + }) + } + + pub fn select_up_by_lines( + &mut self, + action: &SelectUpByLines, + window: &mut Window, + cx: &mut Context, + ) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + let text_layout_details = &self.text_layout_details(window); + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.move_heads_with(|map, head, goal| { + movement::up_by_rows(map, head, action.lines, goal, false, text_layout_details) + }) + }) + } + + pub fn select_page_up( + &mut self, + _: &SelectPageUp, + window: &mut Window, + cx: &mut Context, + ) { + let Some(row_count) = self.visible_row_count() else { + return; + }; + + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + + let text_layout_details = &self.text_layout_details(window); + + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.move_heads_with(|map, head, goal| { + movement::up_by_rows(map, head, row_count, goal, false, text_layout_details) + }) + }) + } + + pub fn move_page_up( + &mut self, + action: &MovePageUp, + window: &mut Window, + cx: &mut Context, + ) { + if self.take_rename(true, window, cx).is_some() { + return; + } + + if self + .context_menu + .borrow_mut() + .as_mut() + .map(|menu| menu.select_first(self.completion_provider.as_deref(), cx)) + .unwrap_or(false) + { + return; + } + + if matches!(self.mode, EditorMode::SingleLine { .. }) { + cx.propagate(); + return; + } + + let Some(row_count) = self.visible_row_count() else { + return; + }; + + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + + let autoscroll = if action.center_cursor { + Autoscroll::center() + } else { + Autoscroll::fit() + }; + + let text_layout_details = &self.text_layout_details(window); + + self.change_selections(Some(autoscroll), window, cx, |s| { + s.move_with(|map, selection| { + if !selection.is_empty() { + selection.goal = SelectionGoal::None; + } + let (cursor, goal) = movement::up_by_rows( + map, + selection.end, + row_count, + selection.goal, + false, + text_layout_details, + ); + selection.collapse_to(cursor, goal); + }); + }); + } + + pub fn select_up(&mut self, _: &SelectUp, window: &mut Window, cx: &mut Context) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + let text_layout_details = &self.text_layout_details(window); + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.move_heads_with(|map, head, goal| { + movement::up(map, head, goal, false, text_layout_details) + }) + }) + } + + pub fn move_down(&mut self, _: &MoveDown, window: &mut Window, cx: &mut Context) { + self.take_rename(true, window, cx); + + if matches!(self.mode, EditorMode::SingleLine { .. }) { + cx.propagate(); + return; + } + + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + + let text_layout_details = &self.text_layout_details(window); + let selection_count = self.selections.count(); + let first_selection = self.selections.first_anchor(); + + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.move_with(|map, selection| { + if !selection.is_empty() { + selection.goal = SelectionGoal::None; + } + let (cursor, goal) = movement::down( + map, + selection.end, + selection.goal, + false, + text_layout_details, + ); + selection.collapse_to(cursor, goal); + }); + }); + + if selection_count == 1 && first_selection.range() == self.selections.first_anchor().range() + { + cx.propagate(); + } + } + + pub fn select_page_down( + &mut self, + _: &SelectPageDown, + window: &mut Window, + cx: &mut Context, + ) { + let Some(row_count) = self.visible_row_count() else { + return; + }; + + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + + let text_layout_details = &self.text_layout_details(window); + + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.move_heads_with(|map, head, goal| { + movement::down_by_rows(map, head, row_count, goal, false, text_layout_details) + }) + }) + } + + pub fn move_page_down( + &mut self, + action: &MovePageDown, + window: &mut Window, + cx: &mut Context, + ) { + if self.take_rename(true, window, cx).is_some() { + return; + } + + if self + .context_menu + .borrow_mut() + .as_mut() + .map(|menu| menu.select_last(self.completion_provider.as_deref(), cx)) + .unwrap_or(false) + { + return; + } + + if matches!(self.mode, EditorMode::SingleLine { .. }) { + cx.propagate(); + return; + } + + let Some(row_count) = self.visible_row_count() else { + return; + }; + + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + + let autoscroll = if action.center_cursor { + Autoscroll::center() + } else { + Autoscroll::fit() + }; + + let text_layout_details = &self.text_layout_details(window); + self.change_selections(Some(autoscroll), window, cx, |s| { + s.move_with(|map, selection| { + if !selection.is_empty() { + selection.goal = SelectionGoal::None; + } + let (cursor, goal) = movement::down_by_rows( + map, + selection.end, + row_count, + selection.goal, + false, + text_layout_details, + ); + selection.collapse_to(cursor, goal); + }); + }); + } + + pub fn select_down(&mut self, _: &SelectDown, window: &mut Window, cx: &mut Context) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + let text_layout_details = &self.text_layout_details(window); + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.move_heads_with(|map, head, goal| { + movement::down(map, head, goal, false, text_layout_details) + }) + }); + } + + pub fn context_menu_first( + &mut self, + _: &ContextMenuFirst, + _window: &mut Window, + cx: &mut Context, + ) { + if let Some(context_menu) = self.context_menu.borrow_mut().as_mut() { + context_menu.select_first(self.completion_provider.as_deref(), cx); + } + } + + pub fn context_menu_prev( + &mut self, + _: &ContextMenuPrevious, + _window: &mut Window, + cx: &mut Context, + ) { + if let Some(context_menu) = self.context_menu.borrow_mut().as_mut() { + context_menu.select_prev(self.completion_provider.as_deref(), cx); + } + } + + pub fn context_menu_next( + &mut self, + _: &ContextMenuNext, + _window: &mut Window, + cx: &mut Context, + ) { + if let Some(context_menu) = self.context_menu.borrow_mut().as_mut() { + context_menu.select_next(self.completion_provider.as_deref(), cx); + } + } + + pub fn context_menu_last( + &mut self, + _: &ContextMenuLast, + _window: &mut Window, + cx: &mut Context, + ) { + if let Some(context_menu) = self.context_menu.borrow_mut().as_mut() { + context_menu.select_last(self.completion_provider.as_deref(), cx); + } + } + + pub fn move_to_previous_word_start( + &mut self, + _: &MoveToPreviousWordStart, + window: &mut Window, + cx: &mut Context, + ) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.move_cursors_with(|map, head, _| { + ( + movement::previous_word_start(map, head), + SelectionGoal::None, + ) + }); + }) + } + + pub fn move_to_previous_subword_start( + &mut self, + _: &MoveToPreviousSubwordStart, + window: &mut Window, + cx: &mut Context, + ) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.move_cursors_with(|map, head, _| { + ( + movement::previous_subword_start(map, head), + SelectionGoal::None, + ) + }); + }) + } + + pub fn select_to_previous_word_start( + &mut self, + _: &SelectToPreviousWordStart, + window: &mut Window, + cx: &mut Context, + ) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.move_heads_with(|map, head, _| { + ( + movement::previous_word_start(map, head), + SelectionGoal::None, + ) + }); + }) + } + + pub fn select_to_previous_subword_start( + &mut self, + _: &SelectToPreviousSubwordStart, + window: &mut Window, + cx: &mut Context, + ) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.move_heads_with(|map, head, _| { + ( + movement::previous_subword_start(map, head), + SelectionGoal::None, + ) + }); + }) + } + + pub fn delete_to_previous_word_start( + &mut self, + action: &DeleteToPreviousWordStart, + window: &mut Window, + cx: &mut Context, + ) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + self.transact(window, cx, |this, window, cx| { + this.select_autoclose_pair(window, cx); + this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.move_with(|map, selection| { + if selection.is_empty() { + let cursor = if action.ignore_newlines { + movement::previous_word_start(map, selection.head()) + } else { + movement::previous_word_start_or_newline(map, selection.head()) + }; + selection.set_head(cursor, SelectionGoal::None); + } + }); + }); + this.insert("", window, cx); + }); + } + + pub fn delete_to_previous_subword_start( + &mut self, + _: &DeleteToPreviousSubwordStart, + window: &mut Window, + cx: &mut Context, + ) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + self.transact(window, cx, |this, window, cx| { + this.select_autoclose_pair(window, cx); + this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.move_with(|map, selection| { + if selection.is_empty() { + let cursor = movement::previous_subword_start(map, selection.head()); + selection.set_head(cursor, SelectionGoal::None); + } + }); + }); + this.insert("", window, cx); + }); + } + + pub fn move_to_next_word_end( + &mut self, + _: &MoveToNextWordEnd, + window: &mut Window, + cx: &mut Context, + ) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.move_cursors_with(|map, head, _| { + (movement::next_word_end(map, head), SelectionGoal::None) + }); + }) + } + + pub fn move_to_next_subword_end( + &mut self, + _: &MoveToNextSubwordEnd, + window: &mut Window, + cx: &mut Context, + ) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.move_cursors_with(|map, head, _| { + (movement::next_subword_end(map, head), SelectionGoal::None) + }); + }) + } + + pub fn select_to_next_word_end( + &mut self, + _: &SelectToNextWordEnd, + window: &mut Window, + cx: &mut Context, + ) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.move_heads_with(|map, head, _| { + (movement::next_word_end(map, head), SelectionGoal::None) + }); + }) + } + + pub fn select_to_next_subword_end( + &mut self, + _: &SelectToNextSubwordEnd, + window: &mut Window, + cx: &mut Context, + ) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.move_heads_with(|map, head, _| { + (movement::next_subword_end(map, head), SelectionGoal::None) + }); + }) + } + + pub fn delete_to_next_word_end( + &mut self, + action: &DeleteToNextWordEnd, + window: &mut Window, + cx: &mut Context, + ) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + self.transact(window, cx, |this, window, cx| { + this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.move_with(|map, selection| { + if selection.is_empty() { + let cursor = if action.ignore_newlines { + movement::next_word_end(map, selection.head()) + } else { + movement::next_word_end_or_newline(map, selection.head()) + }; + selection.set_head(cursor, SelectionGoal::None); + } + }); + }); + this.insert("", window, cx); + }); + } + + pub fn delete_to_next_subword_end( + &mut self, + _: &DeleteToNextSubwordEnd, + window: &mut Window, + cx: &mut Context, + ) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + self.transact(window, cx, |this, window, cx| { + this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.move_with(|map, selection| { + if selection.is_empty() { + let cursor = movement::next_subword_end(map, selection.head()); + selection.set_head(cursor, SelectionGoal::None); + } + }); + }); + this.insert("", window, cx); + }); + } + + pub fn move_to_beginning_of_line( + &mut self, + action: &MoveToBeginningOfLine, + window: &mut Window, + cx: &mut Context, + ) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.move_cursors_with(|map, head, _| { + ( + movement::indented_line_beginning( + map, + head, + action.stop_at_soft_wraps, + action.stop_at_indent, + ), + SelectionGoal::None, + ) + }); + }) + } + + pub fn select_to_beginning_of_line( + &mut self, + action: &SelectToBeginningOfLine, + window: &mut Window, + cx: &mut Context, + ) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.move_heads_with(|map, head, _| { + ( + movement::indented_line_beginning( + map, + head, + action.stop_at_soft_wraps, + action.stop_at_indent, + ), + SelectionGoal::None, + ) + }); + }); + } + + pub fn delete_to_beginning_of_line( + &mut self, + action: &DeleteToBeginningOfLine, + window: &mut Window, + cx: &mut Context, + ) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + self.transact(window, cx, |this, window, cx| { + this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.move_with(|_, selection| { + selection.reversed = true; + }); + }); + + this.select_to_beginning_of_line( + &SelectToBeginningOfLine { + stop_at_soft_wraps: false, + stop_at_indent: action.stop_at_indent, + }, + window, + cx, + ); + this.backspace(&Backspace, window, cx); + }); + } + + pub fn move_to_end_of_line( + &mut self, + action: &MoveToEndOfLine, + window: &mut Window, + cx: &mut Context, + ) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.move_cursors_with(|map, head, _| { + ( + movement::line_end(map, head, action.stop_at_soft_wraps), + SelectionGoal::None, + ) + }); + }) + } + + pub fn select_to_end_of_line( + &mut self, + action: &SelectToEndOfLine, + window: &mut Window, + cx: &mut Context, + ) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.move_heads_with(|map, head, _| { + ( + movement::line_end(map, head, action.stop_at_soft_wraps), + SelectionGoal::None, + ) + }); + }) + } + + pub fn delete_to_end_of_line( + &mut self, + _: &DeleteToEndOfLine, + window: &mut Window, + cx: &mut Context, + ) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + self.transact(window, cx, |this, window, cx| { + this.select_to_end_of_line( + &SelectToEndOfLine { + stop_at_soft_wraps: false, + }, + window, + cx, + ); + this.delete(&Delete, window, cx); + }); + } + + pub fn cut_to_end_of_line( + &mut self, + _: &CutToEndOfLine, + window: &mut Window, + cx: &mut Context, + ) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + self.transact(window, cx, |this, window, cx| { + this.select_to_end_of_line( + &SelectToEndOfLine { + stop_at_soft_wraps: false, + }, + window, + cx, + ); + this.cut(&Cut, window, cx); + }); + } + + pub fn move_to_start_of_paragraph( + &mut self, + _: &MoveToStartOfParagraph, + window: &mut Window, + cx: &mut Context, + ) { + if matches!(self.mode, EditorMode::SingleLine { .. }) { + cx.propagate(); + return; + } + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.move_with(|map, selection| { + selection.collapse_to( + movement::start_of_paragraph(map, selection.head(), 1), + SelectionGoal::None, + ) + }); + }) + } + + pub fn move_to_end_of_paragraph( + &mut self, + _: &MoveToEndOfParagraph, + window: &mut Window, + cx: &mut Context, + ) { + if matches!(self.mode, EditorMode::SingleLine { .. }) { + cx.propagate(); + return; + } + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.move_with(|map, selection| { + selection.collapse_to( + movement::end_of_paragraph(map, selection.head(), 1), + SelectionGoal::None, + ) + }); + }) + } + + pub fn select_to_start_of_paragraph( + &mut self, + _: &SelectToStartOfParagraph, + window: &mut Window, + cx: &mut Context, + ) { + if matches!(self.mode, EditorMode::SingleLine { .. }) { + cx.propagate(); + return; + } + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.move_heads_with(|map, head, _| { + ( + movement::start_of_paragraph(map, head, 1), + SelectionGoal::None, + ) + }); + }) + } + + pub fn select_to_end_of_paragraph( + &mut self, + _: &SelectToEndOfParagraph, + window: &mut Window, + cx: &mut Context, + ) { + if matches!(self.mode, EditorMode::SingleLine { .. }) { + cx.propagate(); + return; + } + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.move_heads_with(|map, head, _| { + ( + movement::end_of_paragraph(map, head, 1), + SelectionGoal::None, + ) + }); + }) + } + + pub fn move_to_start_of_excerpt( + &mut self, + _: &MoveToStartOfExcerpt, + window: &mut Window, + cx: &mut Context, + ) { + if matches!(self.mode, EditorMode::SingleLine { .. }) { + cx.propagate(); + return; + } + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.move_with(|map, selection| { + selection.collapse_to( + movement::start_of_excerpt( + map, + selection.head(), + workspace::searchable::Direction::Prev, + ), + SelectionGoal::None, + ) + }); + }) + } + + pub fn move_to_start_of_next_excerpt( + &mut self, + _: &MoveToStartOfNextExcerpt, + window: &mut Window, + cx: &mut Context, + ) { + if matches!(self.mode, EditorMode::SingleLine { .. }) { + cx.propagate(); + return; + } + + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.move_with(|map, selection| { + selection.collapse_to( + movement::start_of_excerpt( + map, + selection.head(), + workspace::searchable::Direction::Next, + ), + SelectionGoal::None, + ) + }); + }) + } + + pub fn move_to_end_of_excerpt( + &mut self, + _: &MoveToEndOfExcerpt, + window: &mut Window, + cx: &mut Context, + ) { + if matches!(self.mode, EditorMode::SingleLine { .. }) { + cx.propagate(); + return; + } + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.move_with(|map, selection| { + selection.collapse_to( + movement::end_of_excerpt( + map, + selection.head(), + workspace::searchable::Direction::Next, + ), + SelectionGoal::None, + ) + }); + }) + } + + pub fn move_to_end_of_previous_excerpt( + &mut self, + _: &MoveToEndOfPreviousExcerpt, + window: &mut Window, + cx: &mut Context, + ) { + if matches!(self.mode, EditorMode::SingleLine { .. }) { + cx.propagate(); + return; + } + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.move_with(|map, selection| { + selection.collapse_to( + movement::end_of_excerpt( + map, + selection.head(), + workspace::searchable::Direction::Prev, + ), + SelectionGoal::None, + ) + }); + }) + } + + pub fn select_to_start_of_excerpt( + &mut self, + _: &SelectToStartOfExcerpt, + window: &mut Window, + cx: &mut Context, + ) { + if matches!(self.mode, EditorMode::SingleLine { .. }) { + cx.propagate(); + return; + } + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.move_heads_with(|map, head, _| { + ( + movement::start_of_excerpt(map, head, workspace::searchable::Direction::Prev), + SelectionGoal::None, + ) + }); + }) + } + + pub fn select_to_start_of_next_excerpt( + &mut self, + _: &SelectToStartOfNextExcerpt, + window: &mut Window, + cx: &mut Context, + ) { + if matches!(self.mode, EditorMode::SingleLine { .. }) { + cx.propagate(); + return; + } + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.move_heads_with(|map, head, _| { + ( + movement::start_of_excerpt(map, head, workspace::searchable::Direction::Next), + SelectionGoal::None, + ) + }); + }) + } + + pub fn select_to_end_of_excerpt( + &mut self, + _: &SelectToEndOfExcerpt, + window: &mut Window, + cx: &mut Context, + ) { + if matches!(self.mode, EditorMode::SingleLine { .. }) { + cx.propagate(); + return; + } + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.move_heads_with(|map, head, _| { + ( + movement::end_of_excerpt(map, head, workspace::searchable::Direction::Next), + SelectionGoal::None, + ) + }); + }) + } + + pub fn select_to_end_of_previous_excerpt( + &mut self, + _: &SelectToEndOfPreviousExcerpt, + window: &mut Window, + cx: &mut Context, + ) { + if matches!(self.mode, EditorMode::SingleLine { .. }) { + cx.propagate(); + return; + } + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.move_heads_with(|map, head, _| { + ( + movement::end_of_excerpt(map, head, workspace::searchable::Direction::Prev), + SelectionGoal::None, + ) + }); + }) + } + + pub fn move_to_beginning( + &mut self, + _: &MoveToBeginning, + window: &mut Window, + cx: &mut Context, + ) { + if matches!(self.mode, EditorMode::SingleLine { .. }) { + cx.propagate(); + return; + } + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.select_ranges(vec![0..0]); + }); + } + + pub fn select_to_beginning( + &mut self, + _: &SelectToBeginning, + window: &mut Window, + cx: &mut Context, + ) { + let mut selection = self.selections.last::(cx); + selection.set_head(Point::zero(), SelectionGoal::None); + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.select(vec![selection]); + }); + } + + pub fn move_to_end(&mut self, _: &MoveToEnd, window: &mut Window, cx: &mut Context) { + if matches!(self.mode, EditorMode::SingleLine { .. }) { + cx.propagate(); + return; + } + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + let cursor = self.buffer.read(cx).read(cx).len(); + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.select_ranges(vec![cursor..cursor]) + }); + } + + pub fn set_nav_history(&mut self, nav_history: Option) { + self.nav_history = nav_history; + } + + pub fn nav_history(&self) -> Option<&ItemNavHistory> { + self.nav_history.as_ref() + } + + pub fn create_nav_history_entry(&mut self, cx: &mut Context) { + self.push_to_nav_history(self.selections.newest_anchor().head(), None, false, cx); + } + + fn push_to_nav_history( + &mut self, + cursor_anchor: Anchor, + new_position: Option, + is_deactivate: bool, + cx: &mut Context, + ) { + if let Some(nav_history) = self.nav_history.as_mut() { + let buffer = self.buffer.read(cx).read(cx); + let cursor_position = cursor_anchor.to_point(&buffer); + let scroll_state = self.scroll_manager.anchor(); + let scroll_top_row = scroll_state.top_row(&buffer); + drop(buffer); + + if let Some(new_position) = new_position { + let row_delta = (new_position.row as i64 - cursor_position.row as i64).abs(); + if row_delta < MIN_NAVIGATION_HISTORY_ROW_DELTA { + return; + } + } + + nav_history.push( + Some(NavigationData { + cursor_anchor, + cursor_position, + scroll_anchor: scroll_state, + scroll_top_row, + }), + cx, + ); + cx.emit(EditorEvent::PushedToNavHistory { + anchor: cursor_anchor, + is_deactivate, + }) + } + } + + pub fn select_to_end(&mut self, _: &SelectToEnd, window: &mut Window, cx: &mut Context) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + let buffer = self.buffer.read(cx).snapshot(cx); + let mut selection = self.selections.first::(cx); + selection.set_head(buffer.len(), SelectionGoal::None); + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.select(vec![selection]); + }); + } + + pub fn select_all(&mut self, _: &SelectAll, window: &mut Window, cx: &mut Context) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + let end = self.buffer.read(cx).read(cx).len(); + self.change_selections(None, window, cx, |s| { + s.select_ranges(vec![0..end]); + }); + } + + pub fn select_line(&mut self, _: &SelectLine, window: &mut Window, cx: &mut Context) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let mut selections = self.selections.all::(cx); + let max_point = display_map.buffer_snapshot.max_point(); + for selection in &mut selections { + let rows = selection.spanned_rows(true, &display_map); + selection.start = Point::new(rows.start.0, 0); + selection.end = cmp::min(max_point, Point::new(rows.end.0, 0)); + selection.reversed = false; + } + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.select(selections); + }); + } + + pub fn split_selection_into_lines( + &mut self, + _: &SplitSelectionIntoLines, + window: &mut Window, + cx: &mut Context, + ) { + let selections = self + .selections + .all::(cx) + .into_iter() + .map(|selection| selection.start..selection.end) + .collect::>(); + self.unfold_ranges(&selections, true, true, cx); + + let mut new_selection_ranges = Vec::new(); + { + let buffer = self.buffer.read(cx).read(cx); + for selection in selections { + for row in selection.start.row..selection.end.row { + let cursor = Point::new(row, buffer.line_len(MultiBufferRow(row))); + new_selection_ranges.push(cursor..cursor); + } + + let is_multiline_selection = selection.start.row != selection.end.row; + // Don't insert last one if it's a multi-line selection ending at the start of a line, + // so this action feels more ergonomic when paired with other selection operations + let should_skip_last = is_multiline_selection && selection.end.column == 0; + if !should_skip_last { + new_selection_ranges.push(selection.end..selection.end); + } + } + } + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.select_ranges(new_selection_ranges); + }); + } + + pub fn add_selection_above( + &mut self, + _: &AddSelectionAbove, + window: &mut Window, + cx: &mut Context, + ) { + self.add_selection(true, window, cx); + } + + pub fn add_selection_below( + &mut self, + _: &AddSelectionBelow, + window: &mut Window, + cx: &mut Context, + ) { + self.add_selection(false, window, cx); + } + + fn add_selection(&mut self, above: bool, window: &mut Window, cx: &mut Context) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let mut selections = self.selections.all::(cx); + let text_layout_details = self.text_layout_details(window); + let mut state = self.add_selections_state.take().unwrap_or_else(|| { + let oldest_selection = selections.iter().min_by_key(|s| s.id).unwrap().clone(); + let range = oldest_selection.display_range(&display_map).sorted(); + + let start_x = display_map.x_for_display_point(range.start, &text_layout_details); + let end_x = display_map.x_for_display_point(range.end, &text_layout_details); + let positions = start_x.min(end_x)..start_x.max(end_x); + + selections.clear(); + let mut stack = Vec::new(); + for row in range.start.row().0..=range.end.row().0 { + if let Some(selection) = self.selections.build_columnar_selection( + &display_map, + DisplayRow(row), + &positions, + oldest_selection.reversed, + &text_layout_details, + ) { + stack.push(selection.id); + selections.push(selection); + } + } + + if above { + stack.reverse(); + } + + AddSelectionsState { above, stack } + }); + + let last_added_selection = *state.stack.last().unwrap(); + let mut new_selections = Vec::new(); + if above == state.above { + let end_row = if above { + DisplayRow(0) + } else { + display_map.max_point().row() + }; + + 'outer: for selection in selections { + if selection.id == last_added_selection { + let range = selection.display_range(&display_map).sorted(); + debug_assert_eq!(range.start.row(), range.end.row()); + let mut row = range.start.row(); + let positions = + if let SelectionGoal::HorizontalRange { start, end } = selection.goal { + px(start)..px(end) + } else { + let start_x = + display_map.x_for_display_point(range.start, &text_layout_details); + let end_x = + display_map.x_for_display_point(range.end, &text_layout_details); + start_x.min(end_x)..start_x.max(end_x) + }; + + while row != end_row { + if above { + row.0 -= 1; + } else { + row.0 += 1; + } + + if let Some(new_selection) = self.selections.build_columnar_selection( + &display_map, + row, + &positions, + selection.reversed, + &text_layout_details, + ) { + state.stack.push(new_selection.id); + if above { + new_selections.push(new_selection); + new_selections.push(selection); + } else { + new_selections.push(selection); + new_selections.push(new_selection); + } + + continue 'outer; + } + } + } + + new_selections.push(selection); + } + } else { + new_selections = selections; + new_selections.retain(|s| s.id != last_added_selection); + state.stack.pop(); + } + + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.select(new_selections); + }); + if state.stack.len() > 1 { + self.add_selections_state = Some(state); + } + } + + pub fn select_next_match_internal( + &mut self, + display_map: &DisplaySnapshot, + replace_newest: bool, + autoscroll: Option, + window: &mut Window, + cx: &mut Context, + ) -> Result<()> { + fn select_next_match_ranges( + this: &mut Editor, + range: Range, + reversed: bool, + replace_newest: bool, + auto_scroll: Option, + window: &mut Window, + cx: &mut Context, + ) { + this.unfold_ranges(&[range.clone()], false, auto_scroll.is_some(), cx); + this.change_selections(auto_scroll, window, cx, |s| { + if replace_newest { + s.delete(s.newest_anchor().id); + } + if reversed { + s.insert_range(range.end..range.start); + } else { + s.insert_range(range); + } + }); + } + + let buffer = &display_map.buffer_snapshot; + let mut selections = self.selections.all::(cx); + if let Some(mut select_next_state) = self.select_next_state.take() { + let query = &select_next_state.query; + if !select_next_state.done { + let first_selection = selections.iter().min_by_key(|s| s.id).unwrap(); + let last_selection = selections.iter().max_by_key(|s| s.id).unwrap(); + let mut next_selected_range = None; + + let bytes_after_last_selection = + buffer.bytes_in_range(last_selection.end..buffer.len()); + let bytes_before_first_selection = buffer.bytes_in_range(0..first_selection.start); + let query_matches = query + .stream_find_iter(bytes_after_last_selection) + .map(|result| (last_selection.end, result)) + .chain( + query + .stream_find_iter(bytes_before_first_selection) + .map(|result| (0, result)), + ); + + for (start_offset, query_match) in query_matches { + let query_match = query_match.unwrap(); // can only fail due to I/O + let offset_range = + start_offset + query_match.start()..start_offset + query_match.end(); + let display_range = offset_range.start.to_display_point(display_map) + ..offset_range.end.to_display_point(display_map); + + if !select_next_state.wordwise + || (!movement::is_inside_word(display_map, display_range.start) + && !movement::is_inside_word(display_map, display_range.end)) + { + // TODO: This is n^2, because we might check all the selections + if !selections + .iter() + .any(|selection| selection.range().overlaps(&offset_range)) + { + next_selected_range = Some(offset_range); + break; + } + } + } + + if let Some(next_selected_range) = next_selected_range { + select_next_match_ranges( + self, + next_selected_range, + last_selection.reversed, + replace_newest, + autoscroll, + window, + cx, + ); + } else { + select_next_state.done = true; + } + } + + self.select_next_state = Some(select_next_state); + } else { + let mut only_carets = true; + let mut same_text_selected = true; + let mut selected_text = None; + + let mut selections_iter = selections.iter().peekable(); + while let Some(selection) = selections_iter.next() { + if selection.start != selection.end { + only_carets = false; + } + + if same_text_selected { + if selected_text.is_none() { + selected_text = + Some(buffer.text_for_range(selection.range()).collect::()); + } + + if let Some(next_selection) = selections_iter.peek() { + if next_selection.range().len() == selection.range().len() { + let next_selected_text = buffer + .text_for_range(next_selection.range()) + .collect::(); + if Some(next_selected_text) != selected_text { + same_text_selected = false; + selected_text = None; + } + } else { + same_text_selected = false; + selected_text = None; + } + } + } + } + + if only_carets { + for selection in &mut selections { + let word_range = movement::surrounding_word( + display_map, + selection.start.to_display_point(display_map), + ); + selection.start = word_range.start.to_offset(display_map, Bias::Left); + selection.end = word_range.end.to_offset(display_map, Bias::Left); + selection.goal = SelectionGoal::None; + selection.reversed = false; + select_next_match_ranges( + self, + selection.start..selection.end, + selection.reversed, + replace_newest, + autoscroll, + window, + cx, + ); + } + + if selections.len() == 1 { + let selection = selections + .last() + .expect("ensured that there's only one selection"); + let query = buffer + .text_for_range(selection.start..selection.end) + .collect::(); + let is_empty = query.is_empty(); + let select_state = SelectNextState { + query: AhoCorasick::new(&[query])?, + wordwise: true, + done: is_empty, + }; + self.select_next_state = Some(select_state); + } else { + self.select_next_state = None; + } + } else if let Some(selected_text) = selected_text { + self.select_next_state = Some(SelectNextState { + query: AhoCorasick::new(&[selected_text])?, + wordwise: false, + done: false, + }); + self.select_next_match_internal( + display_map, + replace_newest, + autoscroll, + window, + cx, + )?; + } + } + Ok(()) + } + + pub fn select_all_matches( + &mut self, + _action: &SelectAllMatches, + window: &mut Window, + cx: &mut Context, + ) -> Result<()> { + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + + self.push_to_selection_history(); + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + + self.select_next_match_internal(&display_map, false, None, window, cx)?; + let Some(select_next_state) = self.select_next_state.as_mut() else { + return Ok(()); + }; + if select_next_state.done { + return Ok(()); + } + + let mut new_selections = Vec::new(); + + let reversed = self.selections.oldest::(cx).reversed; + let buffer = &display_map.buffer_snapshot; + let query_matches = select_next_state + .query + .stream_find_iter(buffer.bytes_in_range(0..buffer.len())); + + for query_match in query_matches.into_iter() { + let query_match = query_match.context("query match for select all action")?; // can only fail due to I/O + let offset_range = if reversed { + query_match.end()..query_match.start() + } else { + query_match.start()..query_match.end() + }; + let display_range = offset_range.start.to_display_point(&display_map) + ..offset_range.end.to_display_point(&display_map); + + if !select_next_state.wordwise + || (!movement::is_inside_word(&display_map, display_range.start) + && !movement::is_inside_word(&display_map, display_range.end)) + { + new_selections.push(offset_range.start..offset_range.end); + } + } + + select_next_state.done = true; + self.unfold_ranges(&new_selections.clone(), false, false, cx); + self.change_selections(None, window, cx, |selections| { + selections.select_ranges(new_selections) + }); + + Ok(()) + } + + pub fn select_next( + &mut self, + action: &SelectNext, + window: &mut Window, + cx: &mut Context, + ) -> Result<()> { + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + self.push_to_selection_history(); + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + self.select_next_match_internal( + &display_map, + action.replace_newest, + Some(Autoscroll::newest()), + window, + cx, + )?; + Ok(()) + } + + pub fn select_previous( + &mut self, + action: &SelectPrevious, + window: &mut Window, + cx: &mut Context, + ) -> Result<()> { + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + self.push_to_selection_history(); + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let buffer = &display_map.buffer_snapshot; + let mut selections = self.selections.all::(cx); + if let Some(mut select_prev_state) = self.select_prev_state.take() { + let query = &select_prev_state.query; + if !select_prev_state.done { + let first_selection = selections.iter().min_by_key(|s| s.id).unwrap(); + let last_selection = selections.iter().max_by_key(|s| s.id).unwrap(); + let mut next_selected_range = None; + // When we're iterating matches backwards, the oldest match will actually be the furthest one in the buffer. + let bytes_before_last_selection = + buffer.reversed_bytes_in_range(0..last_selection.start); + let bytes_after_first_selection = + buffer.reversed_bytes_in_range(first_selection.end..buffer.len()); + let query_matches = query + .stream_find_iter(bytes_before_last_selection) + .map(|result| (last_selection.start, result)) + .chain( + query + .stream_find_iter(bytes_after_first_selection) + .map(|result| (buffer.len(), result)), + ); + for (end_offset, query_match) in query_matches { + let query_match = query_match.unwrap(); // can only fail due to I/O + let offset_range = + end_offset - query_match.end()..end_offset - query_match.start(); + let display_range = offset_range.start.to_display_point(&display_map) + ..offset_range.end.to_display_point(&display_map); + + if !select_prev_state.wordwise + || (!movement::is_inside_word(&display_map, display_range.start) + && !movement::is_inside_word(&display_map, display_range.end)) + { + next_selected_range = Some(offset_range); + break; + } + } + + if let Some(next_selected_range) = next_selected_range { + self.unfold_ranges(&[next_selected_range.clone()], false, true, cx); + self.change_selections(Some(Autoscroll::newest()), window, cx, |s| { + if action.replace_newest { + s.delete(s.newest_anchor().id); + } + if last_selection.reversed { + s.insert_range(next_selected_range.end..next_selected_range.start); + } else { + s.insert_range(next_selected_range); + } + }); + } else { + select_prev_state.done = true; + } + } + + self.select_prev_state = Some(select_prev_state); + } else { + let mut only_carets = true; + let mut same_text_selected = true; + let mut selected_text = None; + + let mut selections_iter = selections.iter().peekable(); + while let Some(selection) = selections_iter.next() { + if selection.start != selection.end { + only_carets = false; + } + + if same_text_selected { + if selected_text.is_none() { + selected_text = + Some(buffer.text_for_range(selection.range()).collect::()); + } + + if let Some(next_selection) = selections_iter.peek() { + if next_selection.range().len() == selection.range().len() { + let next_selected_text = buffer + .text_for_range(next_selection.range()) + .collect::(); + if Some(next_selected_text) != selected_text { + same_text_selected = false; + selected_text = None; + } + } else { + same_text_selected = false; + selected_text = None; + } + } + } + } + + if only_carets { + for selection in &mut selections { + let word_range = movement::surrounding_word( + &display_map, + selection.start.to_display_point(&display_map), + ); + selection.start = word_range.start.to_offset(&display_map, Bias::Left); + selection.end = word_range.end.to_offset(&display_map, Bias::Left); + selection.goal = SelectionGoal::None; + selection.reversed = false; + } + if selections.len() == 1 { + let selection = selections + .last() + .expect("ensured that there's only one selection"); + let query = buffer + .text_for_range(selection.start..selection.end) + .collect::(); + let is_empty = query.is_empty(); + let select_state = SelectNextState { + query: AhoCorasick::new(&[query.chars().rev().collect::()])?, + wordwise: true, + done: is_empty, + }; + self.select_prev_state = Some(select_state); + } else { + self.select_prev_state = None; + } + + self.unfold_ranges( + &selections.iter().map(|s| s.range()).collect::>(), + false, + true, + cx, + ); + self.change_selections(Some(Autoscroll::newest()), window, cx, |s| { + s.select(selections); + }); + } else if let Some(selected_text) = selected_text { + self.select_prev_state = Some(SelectNextState { + query: AhoCorasick::new(&[selected_text.chars().rev().collect::()])?, + wordwise: false, + done: false, + }); + self.select_previous(action, window, cx)?; + } + } + Ok(()) + } + + pub fn find_next_match( + &mut self, + _: &FindNextMatch, + window: &mut Window, + cx: &mut Context, + ) -> Result<()> { + let selections = self.selections.disjoint_anchors(); + match selections.first() { + Some(first) if selections.len() >= 2 => { + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.select_ranges([first.range()]); + }); + } + _ => self.select_next( + &SelectNext { + replace_newest: true, + }, + window, + cx, + )?, + } + Ok(()) + } + + pub fn find_previous_match( + &mut self, + _: &FindPreviousMatch, + window: &mut Window, + cx: &mut Context, + ) -> Result<()> { + let selections = self.selections.disjoint_anchors(); + match selections.last() { + Some(last) if selections.len() >= 2 => { + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.select_ranges([last.range()]); + }); + } + _ => self.select_previous( + &SelectPrevious { + replace_newest: true, + }, + window, + cx, + )?, + } + Ok(()) + } + + pub fn toggle_comments( + &mut self, + action: &ToggleComments, + window: &mut Window, + cx: &mut Context, + ) { + if self.read_only(cx) { + return; + } + self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + let text_layout_details = &self.text_layout_details(window); + self.transact(window, cx, |this, window, cx| { + let mut selections = this.selections.all::(cx); + let mut edits = Vec::new(); + let mut selection_edit_ranges = Vec::new(); + let mut last_toggled_row = None; + let snapshot = this.buffer.read(cx).read(cx); + let empty_str: Arc = Arc::default(); + let mut suffixes_inserted = Vec::new(); + let ignore_indent = action.ignore_indent; + + fn comment_prefix_range( + snapshot: &MultiBufferSnapshot, + row: MultiBufferRow, + comment_prefix: &str, + comment_prefix_whitespace: &str, + ignore_indent: bool, + ) -> Range { + let indent_size = if ignore_indent { + 0 + } else { + snapshot.indent_size_for_line(row).len + }; + + let start = Point::new(row.0, indent_size); + + let mut line_bytes = snapshot + .bytes_in_range(start..snapshot.max_point()) + .flatten() + .copied(); + + // If this line currently begins with the line comment prefix, then record + // the range containing the prefix. + if line_bytes + .by_ref() + .take(comment_prefix.len()) + .eq(comment_prefix.bytes()) + { + // Include any whitespace that matches the comment prefix. + let matching_whitespace_len = line_bytes + .zip(comment_prefix_whitespace.bytes()) + .take_while(|(a, b)| a == b) + .count() as u32; + let end = Point::new( + start.row, + start.column + comment_prefix.len() as u32 + matching_whitespace_len, + ); + start..end + } else { + start..start + } + } + + fn comment_suffix_range( + snapshot: &MultiBufferSnapshot, + row: MultiBufferRow, + comment_suffix: &str, + comment_suffix_has_leading_space: bool, + ) -> Range { + let end = Point::new(row.0, snapshot.line_len(row)); + let suffix_start_column = end.column.saturating_sub(comment_suffix.len() as u32); + + let mut line_end_bytes = snapshot + .bytes_in_range(Point::new(end.row, suffix_start_column.saturating_sub(1))..end) + .flatten() + .copied(); + + let leading_space_len = if suffix_start_column > 0 + && line_end_bytes.next() == Some(b' ') + && comment_suffix_has_leading_space + { + 1 + } else { + 0 + }; + + // If this line currently begins with the line comment prefix, then record + // the range containing the prefix. + if line_end_bytes.by_ref().eq(comment_suffix.bytes()) { + let start = Point::new(end.row, suffix_start_column - leading_space_len); + start..end + } else { + end..end + } + } + + // TODO: Handle selections that cross excerpts + for selection in &mut selections { + let start_column = snapshot + .indent_size_for_line(MultiBufferRow(selection.start.row)) + .len; + let language = if let Some(language) = + snapshot.language_scope_at(Point::new(selection.start.row, start_column)) + { + language + } else { + continue; + }; + + selection_edit_ranges.clear(); + + // If multiple selections contain a given row, avoid processing that + // row more than once. + let mut start_row = MultiBufferRow(selection.start.row); + if last_toggled_row == Some(start_row) { + start_row = start_row.next_row(); + } + let end_row = + if selection.end.row > selection.start.row && selection.end.column == 0 { + MultiBufferRow(selection.end.row - 1) + } else { + MultiBufferRow(selection.end.row) + }; + last_toggled_row = Some(end_row); + + if start_row > end_row { + continue; + } + + // If the language has line comments, toggle those. + let mut full_comment_prefixes = language.line_comment_prefixes().to_vec(); + + // If ignore_indent is set, trim spaces from the right side of all full_comment_prefixes + if ignore_indent { + full_comment_prefixes = full_comment_prefixes + .into_iter() + .map(|s| Arc::from(s.trim_end())) + .collect(); + } + + if !full_comment_prefixes.is_empty() { + let first_prefix = full_comment_prefixes + .first() + .expect("prefixes is non-empty"); + let prefix_trimmed_lengths = full_comment_prefixes + .iter() + .map(|p| p.trim_end_matches(' ').len()) + .collect::>(); + + let mut all_selection_lines_are_comments = true; + + for row in start_row.0..=end_row.0 { + let row = MultiBufferRow(row); + if start_row < end_row && snapshot.is_line_blank(row) { + continue; + } + + let prefix_range = full_comment_prefixes + .iter() + .zip(prefix_trimmed_lengths.iter().copied()) + .map(|(prefix, trimmed_prefix_len)| { + comment_prefix_range( + snapshot.deref(), + row, + &prefix[..trimmed_prefix_len], + &prefix[trimmed_prefix_len..], + ignore_indent, + ) + }) + .max_by_key(|range| range.end.column - range.start.column) + .expect("prefixes is non-empty"); + + if prefix_range.is_empty() { + all_selection_lines_are_comments = false; + } + + selection_edit_ranges.push(prefix_range); + } + + if all_selection_lines_are_comments { + edits.extend( + selection_edit_ranges + .iter() + .cloned() + .map(|range| (range, empty_str.clone())), + ); + } else { + let min_column = selection_edit_ranges + .iter() + .map(|range| range.start.column) + .min() + .unwrap_or(0); + edits.extend(selection_edit_ranges.iter().map(|range| { + let position = Point::new(range.start.row, min_column); + (position..position, first_prefix.clone()) + })); + } + } else if let Some((full_comment_prefix, comment_suffix)) = + language.block_comment_delimiters() + { + let comment_prefix = full_comment_prefix.trim_end_matches(' '); + let comment_prefix_whitespace = &full_comment_prefix[comment_prefix.len()..]; + let prefix_range = comment_prefix_range( + snapshot.deref(), + start_row, + comment_prefix, + comment_prefix_whitespace, + ignore_indent, + ); + let suffix_range = comment_suffix_range( + snapshot.deref(), + end_row, + comment_suffix.trim_start_matches(' '), + comment_suffix.starts_with(' '), + ); + + if prefix_range.is_empty() || suffix_range.is_empty() { + edits.push(( + prefix_range.start..prefix_range.start, + full_comment_prefix.clone(), + )); + edits.push((suffix_range.end..suffix_range.end, comment_suffix.clone())); + suffixes_inserted.push((end_row, comment_suffix.len())); + } else { + edits.push((prefix_range, empty_str.clone())); + edits.push((suffix_range, empty_str.clone())); + } + } else { + continue; + } + } + + drop(snapshot); + this.buffer.update(cx, |buffer, cx| { + buffer.edit(edits, None, cx); + }); + + // Adjust selections so that they end before any comment suffixes that + // were inserted. + let mut suffixes_inserted = suffixes_inserted.into_iter().peekable(); + let mut selections = this.selections.all::(cx); + let snapshot = this.buffer.read(cx).read(cx); + for selection in &mut selections { + while let Some((row, suffix_len)) = suffixes_inserted.peek().copied() { + match row.cmp(&MultiBufferRow(selection.end.row)) { + Ordering::Less => { + suffixes_inserted.next(); + continue; + } + Ordering::Greater => break, + Ordering::Equal => { + if selection.end.column == snapshot.line_len(row) { + if selection.is_empty() { + selection.start.column -= suffix_len as u32; + } + selection.end.column -= suffix_len as u32; + } + break; + } + } + } + } + + drop(snapshot); + this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.select(selections) + }); + + let selections = this.selections.all::(cx); + let selections_on_single_row = selections.windows(2).all(|selections| { + selections[0].start.row == selections[1].start.row + && selections[0].end.row == selections[1].end.row + && selections[0].start.row == selections[0].end.row + }); + let selections_selecting = selections + .iter() + .any(|selection| selection.start != selection.end); + let advance_downwards = action.advance_downwards + && selections_on_single_row + && !selections_selecting + && !matches!(this.mode, EditorMode::SingleLine { .. }); + + if advance_downwards { + let snapshot = this.buffer.read(cx).snapshot(cx); + + this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.move_cursors_with(|display_snapshot, display_point, _| { + let mut point = display_point.to_point(display_snapshot); + point.row += 1; + point = snapshot.clip_point(point, Bias::Left); + let display_point = point.to_display_point(display_snapshot); + let goal = SelectionGoal::HorizontalPosition( + display_snapshot + .x_for_display_point(display_point, text_layout_details) + .into(), + ); + (display_point, goal) + }) + }); + } + }); + } + + pub fn select_enclosing_symbol( + &mut self, + _: &SelectEnclosingSymbol, + window: &mut Window, + cx: &mut Context, + ) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + + let buffer = self.buffer.read(cx).snapshot(cx); + let old_selections = self.selections.all::(cx).into_boxed_slice(); + + fn update_selection( + selection: &Selection, + buffer_snap: &MultiBufferSnapshot, + ) -> Option> { + let cursor = selection.head(); + let (_buffer_id, symbols) = buffer_snap.symbols_containing(cursor, None)?; + for symbol in symbols.iter().rev() { + let start = symbol.range.start.to_offset(buffer_snap); + let end = symbol.range.end.to_offset(buffer_snap); + let new_range = start..end; + if start < selection.start || end > selection.end { + return Some(Selection { + id: selection.id, + start: new_range.start, + end: new_range.end, + goal: SelectionGoal::None, + reversed: selection.reversed, + }); + } + } + None + } + + let mut selected_larger_symbol = false; + let new_selections = old_selections + .iter() + .map(|selection| match update_selection(selection, &buffer) { + Some(new_selection) => { + if new_selection.range() != selection.range() { + selected_larger_symbol = true; + } + new_selection + } + None => selection.clone(), + }) + .collect::>(); + + if selected_larger_symbol { + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.select(new_selections); + }); + } + } + + pub fn select_larger_syntax_node( + &mut self, + _: &SelectLargerSyntaxNode, + window: &mut Window, + cx: &mut Context, + ) { + let Some(visible_row_count) = self.visible_row_count() else { + return; + }; + let old_selections: Box<[_]> = self.selections.all::(cx).into(); + if old_selections.is_empty() { + return; + } + + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let buffer = self.buffer.read(cx).snapshot(cx); + + let mut selected_larger_node = false; + let mut new_selections = old_selections + .iter() + .map(|selection| { + let old_range = selection.start..selection.end; + + if let Some((node, _)) = buffer.syntax_ancestor(old_range.clone()) { + // manually select word at selection + if ["string_content", "inline"].contains(&node.kind()) { + let word_range = { + let display_point = buffer + .offset_to_point(old_range.start) + .to_display_point(&display_map); + let Range { start, end } = + movement::surrounding_word(&display_map, display_point); + start.to_point(&display_map).to_offset(&buffer) + ..end.to_point(&display_map).to_offset(&buffer) + }; + // ignore if word is already selected + if !word_range.is_empty() && old_range != word_range { + let last_word_range = { + let display_point = buffer + .offset_to_point(old_range.end) + .to_display_point(&display_map); + let Range { start, end } = + movement::surrounding_word(&display_map, display_point); + start.to_point(&display_map).to_offset(&buffer) + ..end.to_point(&display_map).to_offset(&buffer) + }; + // only select word if start and end point belongs to same word + if word_range == last_word_range { + selected_larger_node = true; + return Selection { + id: selection.id, + start: word_range.start, + end: word_range.end, + goal: SelectionGoal::None, + reversed: selection.reversed, + }; + } + } + } + } + + let mut new_range = old_range.clone(); + let mut new_node = None; + while let Some((node, containing_range)) = buffer.syntax_ancestor(new_range.clone()) + { + new_node = Some(node); + new_range = match containing_range { + MultiOrSingleBufferOffsetRange::Single(_) => break, + MultiOrSingleBufferOffsetRange::Multi(range) => range, + }; + if !display_map.intersects_fold(new_range.start) + && !display_map.intersects_fold(new_range.end) + { + break; + } + } + + if let Some(node) = new_node { + // Log the ancestor, to support using this action as a way to explore TreeSitter + // nodes. Parent and grandparent are also logged because this operation will not + // visit nodes that have the same range as their parent. + log::info!("Node: {node:?}"); + let parent = node.parent(); + log::info!("Parent: {parent:?}"); + let grandparent = parent.and_then(|x| x.parent()); + log::info!("Grandparent: {grandparent:?}"); + } + + selected_larger_node |= new_range != old_range; + Selection { + id: selection.id, + start: new_range.start, + end: new_range.end, + goal: SelectionGoal::None, + reversed: selection.reversed, + } + }) + .collect::>(); + + if !selected_larger_node { + return; // don't put this call in the history + } + + // scroll based on transformation done to the last selection created by the user + let (last_old, last_new) = old_selections + .last() + .zip(new_selections.last().cloned()) + .expect("old_selections isn't empty"); + + // revert selection + let is_selection_reversed = { + let should_newest_selection_be_reversed = last_old.start != last_new.start; + new_selections.last_mut().expect("checked above").reversed = + should_newest_selection_be_reversed; + should_newest_selection_be_reversed + }; + + if selected_larger_node { + self.select_syntax_node_history.disable_clearing = true; + self.change_selections(None, window, cx, |s| { + s.select(new_selections.clone()); + }); + self.select_syntax_node_history.disable_clearing = false; + } + + let start_row = last_new.start.to_display_point(&display_map).row().0; + let end_row = last_new.end.to_display_point(&display_map).row().0; + let selection_height = end_row - start_row + 1; + let scroll_margin_rows = self.vertical_scroll_margin() as u32; + + let fits_on_the_screen = visible_row_count >= selection_height + scroll_margin_rows * 2; + let scroll_behavior = if fits_on_the_screen { + self.request_autoscroll(Autoscroll::fit(), cx); + SelectSyntaxNodeScrollBehavior::FitSelection + } else if is_selection_reversed { + self.scroll_cursor_top(&ScrollCursorTop, window, cx); + SelectSyntaxNodeScrollBehavior::CursorTop + } else { + self.scroll_cursor_bottom(&ScrollCursorBottom, window, cx); + SelectSyntaxNodeScrollBehavior::CursorBottom + }; + + self.select_syntax_node_history.push(( + old_selections, + scroll_behavior, + is_selection_reversed, + )); + } + + pub fn select_smaller_syntax_node( + &mut self, + _: &SelectSmallerSyntaxNode, + window: &mut Window, + cx: &mut Context, + ) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + + if let Some((mut selections, scroll_behavior, is_selection_reversed)) = + self.select_syntax_node_history.pop() + { + if let Some(selection) = selections.last_mut() { + selection.reversed = is_selection_reversed; + } + + self.select_syntax_node_history.disable_clearing = true; + self.change_selections(None, window, cx, |s| { + s.select(selections.to_vec()); + }); + self.select_syntax_node_history.disable_clearing = false; + + match scroll_behavior { + SelectSyntaxNodeScrollBehavior::CursorTop => { + self.scroll_cursor_top(&ScrollCursorTop, window, cx); + } + SelectSyntaxNodeScrollBehavior::FitSelection => { + self.request_autoscroll(Autoscroll::fit(), cx); + } + SelectSyntaxNodeScrollBehavior::CursorBottom => { + self.scroll_cursor_bottom(&ScrollCursorBottom, window, cx); + } + } + } + } + + fn refresh_runnables(&mut self, window: &mut Window, cx: &mut Context) -> Task<()> { + if !EditorSettings::get_global(cx).gutter.runnables { + self.clear_tasks(); + return Task::ready(()); + } + let project = self.project.as_ref().map(Entity::downgrade); + let task_sources = self.lsp_task_sources(cx); + cx.spawn_in(window, async move |editor, cx| { + cx.background_executor().timer(UPDATE_DEBOUNCE).await; + let Some(project) = project.and_then(|p| p.upgrade()) else { + return; + }; + let Ok(display_snapshot) = editor.update(cx, |this, cx| { + this.display_map.update(cx, |map, cx| map.snapshot(cx)) + }) else { + return; + }; + + let hide_runnables = project + .update(cx, |project, cx| { + // Do not display any test indicators in non-dev server remote projects. + project.is_via_collab() && project.ssh_connection_string(cx).is_none() + }) + .unwrap_or(true); + if hide_runnables { + return; + } + let new_rows = + cx.background_spawn({ + let snapshot = display_snapshot.clone(); + async move { + Self::fetch_runnable_ranges(&snapshot, Anchor::min()..Anchor::max()) + } + }) + .await; + let Ok(lsp_tasks) = + cx.update(|_, cx| crate::lsp_tasks(project.clone(), &task_sources, None, cx)) + else { + return; + }; + let lsp_tasks = lsp_tasks.await; + + let Ok(mut lsp_tasks_by_rows) = cx.update(|_, cx| { + lsp_tasks + .into_iter() + .flat_map(|(kind, tasks)| { + tasks.into_iter().filter_map(move |(location, task)| { + Some((kind.clone(), location?, task)) + }) + }) + .fold(HashMap::default(), |mut acc, (kind, location, task)| { + let buffer = location.target.buffer; + let buffer_snapshot = buffer.read(cx).snapshot(); + let offset = display_snapshot.buffer_snapshot.excerpts().find_map( + |(excerpt_id, snapshot, _)| { + if snapshot.remote_id() == buffer_snapshot.remote_id() { + display_snapshot + .buffer_snapshot + .anchor_in_excerpt(excerpt_id, location.target.range.start) + } else { + None + } + }, + ); + if let Some(offset) = offset { + let task_buffer_range = + location.target.range.to_point(&buffer_snapshot); + let context_buffer_range = + task_buffer_range.to_offset(&buffer_snapshot); + let context_range = BufferOffset(context_buffer_range.start) + ..BufferOffset(context_buffer_range.end); + + acc.entry((buffer_snapshot.remote_id(), task_buffer_range.start.row)) + .or_insert_with(|| RunnableTasks { + templates: Vec::new(), + offset, + column: task_buffer_range.start.column, + extra_variables: HashMap::default(), + context_range, + }) + .templates + .push((kind, task.original_task().clone())); + } + + acc + }) + }) else { + return; + }; + + let rows = Self::runnable_rows(project, display_snapshot, new_rows, cx.clone()); + editor + .update(cx, |editor, _| { + editor.clear_tasks(); + for (key, mut value) in rows { + if let Some(lsp_tasks) = lsp_tasks_by_rows.remove(&key) { + value.templates.extend(lsp_tasks.templates); + } + + editor.insert_tasks(key, value); + } + for (key, value) in lsp_tasks_by_rows { + editor.insert_tasks(key, value); + } + }) + .ok(); + }) + } + fn fetch_runnable_ranges( + snapshot: &DisplaySnapshot, + range: Range, + ) -> Vec { + snapshot.buffer_snapshot.runnable_ranges(range).collect() + } + + fn runnable_rows( + project: Entity, + snapshot: DisplaySnapshot, + runnable_ranges: Vec, + mut cx: AsyncWindowContext, + ) -> Vec<((BufferId, BufferRow), RunnableTasks)> { + runnable_ranges + .into_iter() + .filter_map(|mut runnable| { + let tasks = cx + .update(|_, cx| Self::templates_with_tags(&project, &mut runnable.runnable, cx)) + .ok()?; + if tasks.is_empty() { + return None; + } + + let point = runnable.run_range.start.to_point(&snapshot.buffer_snapshot); + + let row = snapshot + .buffer_snapshot + .buffer_line_for_row(MultiBufferRow(point.row))? + .1 + .start + .row; + + let context_range = + BufferOffset(runnable.full_range.start)..BufferOffset(runnable.full_range.end); + Some(( + (runnable.buffer_id, row), + RunnableTasks { + templates: tasks, + offset: snapshot + .buffer_snapshot + .anchor_before(runnable.run_range.start), + context_range, + column: point.column, + extra_variables: runnable.extra_captures, + }, + )) + }) + .collect() + } + + fn templates_with_tags( + project: &Entity, + runnable: &mut Runnable, + cx: &mut App, + ) -> Vec<(TaskSourceKind, TaskTemplate)> { + let (inventory, worktree_id, file) = project.read_with(cx, |project, cx| { + let (worktree_id, file) = project + .buffer_for_id(runnable.buffer, cx) + .and_then(|buffer| buffer.read(cx).file()) + .map(|file| (file.worktree_id(cx), file.clone())) + .unzip(); + + ( + project.task_store().read(cx).task_inventory().cloned(), + worktree_id, + file, + ) + }); + + let mut templates_with_tags = mem::take(&mut runnable.tags) + .into_iter() + .flat_map(|RunnableTag(tag)| { + inventory + .as_ref() + .into_iter() + .flat_map(|inventory| { + inventory.read(cx).list_tasks( + file.clone(), + Some(runnable.language.clone()), + worktree_id, + cx, + ) + }) + .filter(move |(_, template)| { + template.tags.iter().any(|source_tag| source_tag == &tag) + }) + }) + .sorted_by_key(|(kind, _)| kind.to_owned()) + .collect::>(); + if let Some((leading_tag_source, _)) = templates_with_tags.first() { + // Strongest source wins; if we have worktree tag binding, prefer that to + // global and language bindings; + // if we have a global binding, prefer that to language binding. + let first_mismatch = templates_with_tags + .iter() + .position(|(tag_source, _)| tag_source != leading_tag_source); + if let Some(index) = first_mismatch { + templates_with_tags.truncate(index); + } + } + + templates_with_tags + } + + pub fn move_to_enclosing_bracket( + &mut self, + _: &MoveToEnclosingBracket, + window: &mut Window, + cx: &mut Context, + ) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.move_offsets_with(|snapshot, selection| { + let Some(enclosing_bracket_ranges) = + snapshot.enclosing_bracket_ranges(selection.start..selection.end) + else { + return; + }; + + let mut best_length = usize::MAX; + let mut best_inside = false; + let mut best_in_bracket_range = false; + let mut best_destination = None; + for (open, close) in enclosing_bracket_ranges { + let close = close.to_inclusive(); + let length = close.end() - open.start; + let inside = selection.start >= open.end && selection.end <= *close.start(); + let in_bracket_range = open.to_inclusive().contains(&selection.head()) + || close.contains(&selection.head()); + + // If best is next to a bracket and current isn't, skip + if !in_bracket_range && best_in_bracket_range { + continue; + } + + // Prefer smaller lengths unless best is inside and current isn't + if length > best_length && (best_inside || !inside) { + continue; + } + + best_length = length; + best_inside = inside; + best_in_bracket_range = in_bracket_range; + best_destination = Some( + if close.contains(&selection.start) && close.contains(&selection.end) { + if inside { open.end } else { open.start } + } else if inside { + *close.start() + } else { + *close.end() + }, + ); + } + + if let Some(destination) = best_destination { + selection.collapse_to(destination, SelectionGoal::None); + } + }) + }); + } + + pub fn undo_selection( + &mut self, + _: &UndoSelection, + window: &mut Window, + cx: &mut Context, + ) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + self.end_selection(window, cx); + self.selection_history.mode = SelectionHistoryMode::Undoing; + if let Some(entry) = self.selection_history.undo_stack.pop_back() { + self.change_selections(None, window, cx, |s| { + s.select_anchors(entry.selections.to_vec()) + }); + self.select_next_state = entry.select_next_state; + self.select_prev_state = entry.select_prev_state; + self.add_selections_state = entry.add_selections_state; + self.request_autoscroll(Autoscroll::newest(), cx); + } + self.selection_history.mode = SelectionHistoryMode::Normal; + } + + pub fn redo_selection( + &mut self, + _: &RedoSelection, + window: &mut Window, + cx: &mut Context, + ) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + self.end_selection(window, cx); + self.selection_history.mode = SelectionHistoryMode::Redoing; + if let Some(entry) = self.selection_history.redo_stack.pop_back() { + self.change_selections(None, window, cx, |s| { + s.select_anchors(entry.selections.to_vec()) + }); + self.select_next_state = entry.select_next_state; + self.select_prev_state = entry.select_prev_state; + self.add_selections_state = entry.add_selections_state; + self.request_autoscroll(Autoscroll::newest(), cx); + } + self.selection_history.mode = SelectionHistoryMode::Normal; + } + + pub fn expand_excerpts( + &mut self, + action: &ExpandExcerpts, + _: &mut Window, + cx: &mut Context, + ) { + self.expand_excerpts_for_direction(action.lines, ExpandExcerptDirection::UpAndDown, cx) + } + + pub fn expand_excerpts_down( + &mut self, + action: &ExpandExcerptsDown, + _: &mut Window, + cx: &mut Context, + ) { + self.expand_excerpts_for_direction(action.lines, ExpandExcerptDirection::Down, cx) + } + + pub fn expand_excerpts_up( + &mut self, + action: &ExpandExcerptsUp, + _: &mut Window, + cx: &mut Context, + ) { + self.expand_excerpts_for_direction(action.lines, ExpandExcerptDirection::Up, cx) + } + + pub fn expand_excerpts_for_direction( + &mut self, + lines: u32, + direction: ExpandExcerptDirection, + + cx: &mut Context, + ) { + let selections = self.selections.disjoint_anchors(); + + let lines = if lines == 0 { + EditorSettings::get_global(cx).expand_excerpt_lines + } else { + lines + }; + + self.buffer.update(cx, |buffer, cx| { + let snapshot = buffer.snapshot(cx); + let mut excerpt_ids = selections + .iter() + .flat_map(|selection| snapshot.excerpt_ids_for_range(selection.range())) + .collect::>(); + excerpt_ids.sort(); + excerpt_ids.dedup(); + buffer.expand_excerpts(excerpt_ids, lines, direction, cx) + }) + } + + pub fn expand_excerpt( + &mut self, + excerpt: ExcerptId, + direction: ExpandExcerptDirection, + window: &mut Window, + cx: &mut Context, + ) { + let current_scroll_position = self.scroll_position(cx); + let lines_to_expand = EditorSettings::get_global(cx).expand_excerpt_lines; + let mut should_scroll_up = false; + + if direction == ExpandExcerptDirection::Down { + let multi_buffer = self.buffer.read(cx); + let snapshot = multi_buffer.snapshot(cx); + if let Some(buffer_id) = snapshot.buffer_id_for_excerpt(excerpt) { + if let Some(buffer) = multi_buffer.buffer(buffer_id) { + if let Some(excerpt_range) = snapshot.buffer_range_for_excerpt(excerpt) { + let buffer_snapshot = buffer.read(cx).snapshot(); + let excerpt_end_row = + Point::from_anchor(&excerpt_range.end, &buffer_snapshot).row; + let last_row = buffer_snapshot.max_point().row; + let lines_below = last_row.saturating_sub(excerpt_end_row); + should_scroll_up = lines_below >= lines_to_expand; + } + } + } + } + + self.buffer.update(cx, |buffer, cx| { + buffer.expand_excerpts([excerpt], lines_to_expand, direction, cx) + }); + + if should_scroll_up { + let new_scroll_position = + current_scroll_position + gpui::Point::new(0.0, lines_to_expand as f32); + self.set_scroll_position(new_scroll_position, window, cx); + } + } + + pub fn go_to_singleton_buffer_point( + &mut self, + point: Point, + window: &mut Window, + cx: &mut Context, + ) { + self.go_to_singleton_buffer_range(point..point, window, cx); + } + + pub fn go_to_singleton_buffer_range( + &mut self, + range: Range, + window: &mut Window, + cx: &mut Context, + ) { + let multibuffer = self.buffer().read(cx); + let Some(buffer) = multibuffer.as_singleton() else { + return; + }; + let Some(start) = multibuffer.buffer_point_to_anchor(&buffer, range.start, cx) else { + return; + }; + let Some(end) = multibuffer.buffer_point_to_anchor(&buffer, range.end, cx) else { + return; + }; + self.change_selections(Some(Autoscroll::center()), window, cx, |s| { + s.select_anchor_ranges([start..end]) + }); + } + + pub fn go_to_diagnostic( + &mut self, + _: &GoToDiagnostic, + window: &mut Window, + cx: &mut Context, + ) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + self.go_to_diagnostic_impl(Direction::Next, window, cx) + } + + pub fn go_to_prev_diagnostic( + &mut self, + _: &GoToPreviousDiagnostic, + window: &mut Window, + cx: &mut Context, + ) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + self.go_to_diagnostic_impl(Direction::Prev, window, cx) + } + + pub fn go_to_diagnostic_impl( + &mut self, + direction: Direction, + window: &mut Window, + cx: &mut Context, + ) { + let buffer = self.buffer.read(cx).snapshot(cx); + let selection = self.selections.newest::(cx); + + let mut active_group_id = None; + if let ActiveDiagnostic::Group(active_group) = &self.active_diagnostics { + if active_group.active_range.start.to_offset(&buffer) == selection.start { + active_group_id = Some(active_group.group_id); + } + } + + fn filtered( + snapshot: EditorSnapshot, + diagnostics: impl Iterator>, + ) -> impl Iterator> { + diagnostics + .filter(|entry| entry.range.start != entry.range.end) + .filter(|entry| !entry.diagnostic.is_unnecessary) + .filter(move |entry| !snapshot.intersects_fold(entry.range.start)) + } + + let snapshot = self.snapshot(window, cx); + let before = filtered( + snapshot.clone(), + buffer + .diagnostics_in_range(0..selection.start) + .filter(|entry| entry.range.start <= selection.start), + ); + let after = filtered( + snapshot, + buffer + .diagnostics_in_range(selection.start..buffer.len()) + .filter(|entry| entry.range.start >= selection.start), + ); + + let mut found: Option> = None; + if direction == Direction::Prev { + 'outer: for prev_diagnostics in [before.collect::>(), after.collect::>()] + { + for diagnostic in prev_diagnostics.into_iter().rev() { + if diagnostic.range.start != selection.start + || active_group_id + .is_some_and(|active| diagnostic.diagnostic.group_id < active) + { + found = Some(diagnostic); + break 'outer; + } + } + } + } else { + for diagnostic in after.chain(before) { + if diagnostic.range.start != selection.start + || active_group_id.is_some_and(|active| diagnostic.diagnostic.group_id > active) + { + found = Some(diagnostic); + break; + } + } + } + let Some(next_diagnostic) = found else { + return; + }; + + let Some(buffer_id) = buffer.anchor_after(next_diagnostic.range.start).buffer_id else { + return; + }; + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.select_ranges(vec![ + next_diagnostic.range.start..next_diagnostic.range.start, + ]) + }); + self.activate_diagnostics(buffer_id, next_diagnostic, window, cx); + self.refresh_inline_completion(false, true, window, cx); + } + + fn go_to_next_hunk(&mut self, _: &GoToHunk, window: &mut Window, cx: &mut Context) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + let snapshot = self.snapshot(window, cx); + let selection = self.selections.newest::(cx); + self.go_to_hunk_before_or_after_position( + &snapshot, + selection.head(), + Direction::Next, + window, + cx, + ); + } + + pub fn go_to_hunk_before_or_after_position( + &mut self, + snapshot: &EditorSnapshot, + position: Point, + direction: Direction, + window: &mut Window, + cx: &mut Context, + ) { + let row = if direction == Direction::Next { + self.hunk_after_position(snapshot, position) + .map(|hunk| hunk.row_range.start) + } else { + self.hunk_before_position(snapshot, position) + }; + + if let Some(row) = row { + let destination = Point::new(row.0, 0); + let autoscroll = Autoscroll::center(); + + self.unfold_ranges(&[destination..destination], false, false, cx); + self.change_selections(Some(autoscroll), window, cx, |s| { + s.select_ranges([destination..destination]); + }); + } + } + + fn hunk_after_position( + &mut self, + snapshot: &EditorSnapshot, + position: Point, + ) -> Option { + snapshot + .buffer_snapshot + .diff_hunks_in_range(position..snapshot.buffer_snapshot.max_point()) + .find(|hunk| hunk.row_range.start.0 > position.row) + .or_else(|| { + snapshot + .buffer_snapshot + .diff_hunks_in_range(Point::zero()..position) + .find(|hunk| hunk.row_range.end.0 < position.row) + }) + } + + fn go_to_prev_hunk( + &mut self, + _: &GoToPreviousHunk, + window: &mut Window, + cx: &mut Context, + ) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + let snapshot = self.snapshot(window, cx); + let selection = self.selections.newest::(cx); + self.go_to_hunk_before_or_after_position( + &snapshot, + selection.head(), + Direction::Prev, + window, + cx, + ); + } + + fn hunk_before_position( + &mut self, + snapshot: &EditorSnapshot, + position: Point, + ) -> Option { + snapshot + .buffer_snapshot + .diff_hunk_before(position) + .or_else(|| snapshot.buffer_snapshot.diff_hunk_before(Point::MAX)) + } + + fn go_to_next_change( + &mut self, + _: &GoToNextChange, + window: &mut Window, + cx: &mut Context, + ) { + if let Some(selections) = self + .change_list + .next_change(1, Direction::Next) + .map(|s| s.to_vec()) + { + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + let map = s.display_map(); + s.select_display_ranges(selections.iter().map(|a| { + let point = a.to_display_point(&map); + point..point + })) + }) + } + } + + fn go_to_previous_change( + &mut self, + _: &GoToPreviousChange, + window: &mut Window, + cx: &mut Context, + ) { + if let Some(selections) = self + .change_list + .next_change(1, Direction::Prev) + .map(|s| s.to_vec()) + { + self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + let map = s.display_map(); + s.select_display_ranges(selections.iter().map(|a| { + let point = a.to_display_point(&map); + point..point + })) + }) + } + } + + fn go_to_line( + &mut self, + position: Anchor, + highlight_color: Option, + window: &mut Window, + cx: &mut Context, + ) { + let snapshot = self.snapshot(window, cx).display_snapshot; + let position = position.to_point(&snapshot.buffer_snapshot); + let start = snapshot + .buffer_snapshot + .clip_point(Point::new(position.row, 0), Bias::Left); + let end = start + Point::new(1, 0); + let start = snapshot.buffer_snapshot.anchor_before(start); + let end = snapshot.buffer_snapshot.anchor_before(end); + + self.highlight_rows::( + start..end, + highlight_color + .unwrap_or_else(|| cx.theme().colors().editor_highlighted_line_background), + Default::default(), + cx, + ); + self.request_autoscroll(Autoscroll::center().for_anchor(start), cx); + } + + pub fn go_to_definition( + &mut self, + _: &GoToDefinition, + window: &mut Window, + cx: &mut Context, + ) -> Task> { + let definition = + self.go_to_definition_of_kind(GotoDefinitionKind::Symbol, false, window, cx); + let fallback_strategy = EditorSettings::get_global(cx).go_to_definition_fallback; + cx.spawn_in(window, async move |editor, cx| { + if definition.await? == Navigated::Yes { + return Ok(Navigated::Yes); + } + match fallback_strategy { + GoToDefinitionFallback::None => Ok(Navigated::No), + GoToDefinitionFallback::FindAllReferences => { + match editor.update_in(cx, |editor, window, cx| { + editor.find_all_references(&FindAllReferences, window, cx) + })? { + Some(references) => references.await, + None => Ok(Navigated::No), + } + } + } + }) + } + + pub fn go_to_declaration( + &mut self, + _: &GoToDeclaration, + window: &mut Window, + cx: &mut Context, + ) -> Task> { + self.go_to_definition_of_kind(GotoDefinitionKind::Declaration, false, window, cx) + } + + pub fn go_to_declaration_split( + &mut self, + _: &GoToDeclaration, + window: &mut Window, + cx: &mut Context, + ) -> Task> { + self.go_to_definition_of_kind(GotoDefinitionKind::Declaration, true, window, cx) + } + + pub fn go_to_implementation( + &mut self, + _: &GoToImplementation, + window: &mut Window, + cx: &mut Context, + ) -> Task> { + self.go_to_definition_of_kind(GotoDefinitionKind::Implementation, false, window, cx) + } + + pub fn go_to_implementation_split( + &mut self, + _: &GoToImplementationSplit, + window: &mut Window, + cx: &mut Context, + ) -> Task> { + self.go_to_definition_of_kind(GotoDefinitionKind::Implementation, true, window, cx) + } + + pub fn go_to_type_definition( + &mut self, + _: &GoToTypeDefinition, + window: &mut Window, + cx: &mut Context, + ) -> Task> { + self.go_to_definition_of_kind(GotoDefinitionKind::Type, false, window, cx) + } + + pub fn go_to_definition_split( + &mut self, + _: &GoToDefinitionSplit, + window: &mut Window, + cx: &mut Context, + ) -> Task> { + self.go_to_definition_of_kind(GotoDefinitionKind::Symbol, true, window, cx) + } + + pub fn go_to_type_definition_split( + &mut self, + _: &GoToTypeDefinitionSplit, + window: &mut Window, + cx: &mut Context, + ) -> Task> { + self.go_to_definition_of_kind(GotoDefinitionKind::Type, true, window, cx) + } + + fn go_to_definition_of_kind( + &mut self, + kind: GotoDefinitionKind, + split: bool, + window: &mut Window, + cx: &mut Context, + ) -> Task> { + let Some(provider) = self.semantics_provider.clone() else { + return Task::ready(Ok(Navigated::No)); + }; + let head = self.selections.newest::(cx).head(); + let buffer = self.buffer.read(cx); + let (buffer, head) = if let Some(text_anchor) = buffer.text_anchor_for_position(head, cx) { + text_anchor + } else { + return Task::ready(Ok(Navigated::No)); + }; + + let Some(definitions) = provider.definitions(&buffer, head, kind, cx) else { + return Task::ready(Ok(Navigated::No)); + }; + + cx.spawn_in(window, async move |editor, cx| { + let definitions = definitions.await?; + let navigated = editor + .update_in(cx, |editor, window, cx| { + editor.navigate_to_hover_links( + Some(kind), + definitions + .into_iter() + .filter(|location| { + hover_links::exclude_link_to_position(&buffer, &head, location, cx) + }) + .map(HoverLink::Text) + .collect::>(), + split, + window, + cx, + ) + })? + .await?; + anyhow::Ok(navigated) + }) + } + + pub fn open_url(&mut self, _: &OpenUrl, window: &mut Window, cx: &mut Context) { + let selection = self.selections.newest_anchor(); + let head = selection.head(); + let tail = selection.tail(); + + let Some((buffer, start_position)) = + self.buffer.read(cx).text_anchor_for_position(head, cx) + else { + return; + }; + + let end_position = if head != tail { + let Some((_, pos)) = self.buffer.read(cx).text_anchor_for_position(tail, cx) else { + return; + }; + Some(pos) + } else { + None + }; + + let url_finder = cx.spawn_in(window, async move |editor, cx| { + let url = if let Some(end_pos) = end_position { + find_url_from_range(&buffer, start_position..end_pos, cx.clone()) + } else { + find_url(&buffer, start_position, cx.clone()).map(|(_, url)| url) + }; + + if let Some(url) = url { + editor.update(cx, |_, cx| { + cx.open_url(&url); + }) + } else { + Ok(()) + } + }); + + url_finder.detach(); + } + + pub fn open_selected_filename( + &mut self, + _: &OpenSelectedFilename, + window: &mut Window, + cx: &mut Context, + ) { + let Some(workspace) = self.workspace() else { + return; + }; + + let position = self.selections.newest_anchor().head(); + + let Some((buffer, buffer_position)) = + self.buffer.read(cx).text_anchor_for_position(position, cx) + else { + return; + }; + + let project = self.project.clone(); + + cx.spawn_in(window, async move |_, cx| { + let result = find_file(&buffer, project, buffer_position, cx).await; + + if let Some((_, path)) = result { + workspace + .update_in(cx, |workspace, window, cx| { + workspace.open_resolved_path(path, window, cx) + })? + .await?; + } + anyhow::Ok(()) + }) + .detach(); + } + + pub(crate) fn navigate_to_hover_links( + &mut self, + kind: Option, + mut definitions: Vec, + split: bool, + window: &mut Window, + cx: &mut Context, + ) -> Task> { + // If there is one definition, just open it directly + if definitions.len() == 1 { + let definition = definitions.pop().unwrap(); + + enum TargetTaskResult { + Location(Option), + AlreadyNavigated, + } + + let target_task = match definition { + HoverLink::Text(link) => { + Task::ready(anyhow::Ok(TargetTaskResult::Location(Some(link.target)))) + } + HoverLink::InlayHint(lsp_location, server_id) => { + let computation = + self.compute_target_location(lsp_location, server_id, window, cx); + cx.background_spawn(async move { + let location = computation.await?; + Ok(TargetTaskResult::Location(location)) + }) + } + HoverLink::Url(url) => { + cx.open_url(&url); + Task::ready(Ok(TargetTaskResult::AlreadyNavigated)) + } + HoverLink::File(path) => { + if let Some(workspace) = self.workspace() { + cx.spawn_in(window, async move |_, cx| { + workspace + .update_in(cx, |workspace, window, cx| { + workspace.open_resolved_path(path, window, cx) + })? + .await + .map(|_| TargetTaskResult::AlreadyNavigated) + }) + } else { + Task::ready(Ok(TargetTaskResult::Location(None))) + } + } + }; + cx.spawn_in(window, async move |editor, cx| { + let target = match target_task.await.context("target resolution task")? { + TargetTaskResult::AlreadyNavigated => return Ok(Navigated::Yes), + TargetTaskResult::Location(None) => return Ok(Navigated::No), + TargetTaskResult::Location(Some(target)) => target, + }; + + editor.update_in(cx, |editor, window, cx| { + let Some(workspace) = editor.workspace() else { + return Navigated::No; + }; + let pane = workspace.read(cx).active_pane().clone(); + + let range = target.range.to_point(target.buffer.read(cx)); + let range = editor.range_for_match(&range); + let range = collapse_multiline_range(range); + + if !split + && Some(&target.buffer) == editor.buffer.read(cx).as_singleton().as_ref() + { + editor.go_to_singleton_buffer_range(range.clone(), window, cx); + } else { + window.defer(cx, move |window, cx| { + let target_editor: Entity = + workspace.update(cx, |workspace, cx| { + let pane = if split { + workspace.adjacent_pane(window, cx) + } else { + workspace.active_pane().clone() + }; + + workspace.open_project_item( + pane, + target.buffer.clone(), + true, + true, + window, + cx, + ) + }); + target_editor.update(cx, |target_editor, cx| { + // When selecting a definition in a different buffer, disable the nav history + // to avoid creating a history entry at the previous cursor location. + pane.update(cx, |pane, _| pane.disable_history()); + target_editor.go_to_singleton_buffer_range(range, window, cx); + pane.update(cx, |pane, _| pane.enable_history()); + }); + }); + } + Navigated::Yes + }) + }) + } else if !definitions.is_empty() { + cx.spawn_in(window, async move |editor, cx| { + let (title, location_tasks, workspace) = editor + .update_in(cx, |editor, window, cx| { + let tab_kind = match kind { + Some(GotoDefinitionKind::Implementation) => "Implementations", + _ => "Definitions", + }; + let title = definitions + .iter() + .find_map(|definition| match definition { + HoverLink::Text(link) => link.origin.as_ref().map(|origin| { + let buffer = origin.buffer.read(cx); + format!( + "{} for {}", + tab_kind, + buffer + .text_for_range(origin.range.clone()) + .collect::() + ) + }), + HoverLink::InlayHint(_, _) => None, + HoverLink::Url(_) => None, + HoverLink::File(_) => None, + }) + .unwrap_or(tab_kind.to_string()); + let location_tasks = definitions + .into_iter() + .map(|definition| match definition { + HoverLink::Text(link) => Task::ready(Ok(Some(link.target))), + HoverLink::InlayHint(lsp_location, server_id) => editor + .compute_target_location(lsp_location, server_id, window, cx), + HoverLink::Url(_) => Task::ready(Ok(None)), + HoverLink::File(_) => Task::ready(Ok(None)), + }) + .collect::>(); + (title, location_tasks, editor.workspace().clone()) + }) + .context("location tasks preparation")?; + + let locations = future::join_all(location_tasks) + .await + .into_iter() + .filter_map(|location| location.transpose()) + .collect::>() + .context("location tasks")?; + + let Some(workspace) = workspace else { + return Ok(Navigated::No); + }; + let opened = workspace + .update_in(cx, |workspace, window, cx| { + Self::open_locations_in_multibuffer( + workspace, + locations, + title, + split, + MultibufferSelectionMode::First, + window, + cx, + ) + }) + .ok(); + + anyhow::Ok(Navigated::from_bool(opened.is_some())) + }) + } else { + Task::ready(Ok(Navigated::No)) + } + } + + fn compute_target_location( + &self, + lsp_location: lsp::Location, + server_id: LanguageServerId, + window: &mut Window, + cx: &mut Context, + ) -> Task>> { + let Some(project) = self.project.clone() else { + return Task::ready(Ok(None)); + }; + + cx.spawn_in(window, async move |editor, cx| { + let location_task = editor.update(cx, |_, cx| { + project.update(cx, |project, cx| { + let language_server_name = project + .language_server_statuses(cx) + .find(|(id, _)| server_id == *id) + .map(|(_, status)| LanguageServerName::from(status.name.as_str())); + language_server_name.map(|language_server_name| { + project.open_local_buffer_via_lsp( + lsp_location.uri.clone(), + server_id, + language_server_name, + cx, + ) + }) + }) + })?; + let location = match location_task { + Some(task) => Some({ + let target_buffer_handle = task.await.context("open local buffer")?; + let range = target_buffer_handle.update(cx, |target_buffer, _| { + let target_start = target_buffer + .clip_point_utf16(point_from_lsp(lsp_location.range.start), Bias::Left); + let target_end = target_buffer + .clip_point_utf16(point_from_lsp(lsp_location.range.end), Bias::Left); + target_buffer.anchor_after(target_start) + ..target_buffer.anchor_before(target_end) + })?; + Location { + buffer: target_buffer_handle, + range, + } + }), + None => None, + }; + Ok(location) + }) + } + + pub fn find_all_references( + &mut self, + _: &FindAllReferences, + window: &mut Window, + cx: &mut Context, + ) -> Option>> { + let selection = self.selections.newest::(cx); + let multi_buffer = self.buffer.read(cx); + let head = selection.head(); + + let multi_buffer_snapshot = multi_buffer.snapshot(cx); + let head_anchor = multi_buffer_snapshot.anchor_at( + head, + if head < selection.tail() { + Bias::Right + } else { + Bias::Left + }, + ); + + match self + .find_all_references_task_sources + .binary_search_by(|anchor| anchor.cmp(&head_anchor, &multi_buffer_snapshot)) + { + Ok(_) => { + log::info!( + "Ignoring repeated FindAllReferences invocation with the position of already running task" + ); + return None; + } + Err(i) => { + self.find_all_references_task_sources.insert(i, head_anchor); + } + } + + let (buffer, head) = multi_buffer.text_anchor_for_position(head, cx)?; + let workspace = self.workspace()?; + let project = workspace.read(cx).project().clone(); + let references = project.update(cx, |project, cx| project.references(&buffer, head, cx)); + Some(cx.spawn_in(window, async move |editor, cx| { + let _cleanup = cx.on_drop(&editor, move |editor, _| { + if let Ok(i) = editor + .find_all_references_task_sources + .binary_search_by(|anchor| anchor.cmp(&head_anchor, &multi_buffer_snapshot)) + { + editor.find_all_references_task_sources.remove(i); + } + }); + + let locations = references.await?; + if locations.is_empty() { + return anyhow::Ok(Navigated::No); + } + + workspace.update_in(cx, |workspace, window, cx| { + let title = locations + .first() + .as_ref() + .map(|location| { + let buffer = location.buffer.read(cx); + format!( + "References to `{}`", + buffer + .text_for_range(location.range.clone()) + .collect::() + ) + }) + .unwrap(); + Self::open_locations_in_multibuffer( + workspace, + locations, + title, + false, + MultibufferSelectionMode::First, + window, + cx, + ); + Navigated::Yes + }) + })) + } + + /// Opens a multibuffer with the given project locations in it + pub fn open_locations_in_multibuffer( + workspace: &mut Workspace, + mut locations: Vec, + title: String, + split: bool, + multibuffer_selection_mode: MultibufferSelectionMode, + window: &mut Window, + cx: &mut Context, + ) { + // If there are multiple definitions, open them in a multibuffer + locations.sort_by_key(|location| location.buffer.read(cx).remote_id()); + let mut locations = locations.into_iter().peekable(); + let mut ranges: Vec> = Vec::new(); + let capability = workspace.project().read(cx).capability(); + + let excerpt_buffer = cx.new(|cx| { + let mut multibuffer = MultiBuffer::new(capability); + while let Some(location) = locations.next() { + let buffer = location.buffer.read(cx); + let mut ranges_for_buffer = Vec::new(); + let range = location.range.to_point(buffer); + ranges_for_buffer.push(range.clone()); + + while let Some(next_location) = locations.peek() { + if next_location.buffer == location.buffer { + ranges_for_buffer.push(next_location.range.to_point(buffer)); + locations.next(); + } else { + break; + } + } + + ranges_for_buffer.sort_by_key(|range| (range.start, Reverse(range.end))); + let (new_ranges, _) = multibuffer.set_excerpts_for_path( + PathKey::for_buffer(&location.buffer, cx), + location.buffer.clone(), + ranges_for_buffer, + DEFAULT_MULTIBUFFER_CONTEXT, + cx, + ); + ranges.extend(new_ranges) + } + + multibuffer.with_title(title) + }); + + let editor = cx.new(|cx| { + Editor::for_multibuffer( + excerpt_buffer, + Some(workspace.project().clone()), + window, + cx, + ) + }); + editor.update(cx, |editor, cx| { + match multibuffer_selection_mode { + MultibufferSelectionMode::First => { + if let Some(first_range) = ranges.first() { + editor.change_selections(None, window, cx, |selections| { + selections.clear_disjoint(); + selections.select_anchor_ranges(std::iter::once(first_range.clone())); + }); + } + editor.highlight_background::( + &ranges, + |theme| theme.editor_highlighted_line_background, + cx, + ); + } + MultibufferSelectionMode::All => { + editor.change_selections(None, window, cx, |selections| { + selections.clear_disjoint(); + selections.select_anchor_ranges(ranges); + }); + } + } + editor.register_buffers_with_language_servers(cx); + }); + + let item = Box::new(editor); + let item_id = item.item_id(); + + if split { + workspace.split_item(SplitDirection::Right, item.clone(), window, cx); + } else { + if PreviewTabsSettings::get_global(cx).enable_preview_from_code_navigation { + let (preview_item_id, preview_item_idx) = + workspace.active_pane().update(cx, |pane, _| { + (pane.preview_item_id(), pane.preview_item_idx()) + }); + + workspace.add_item_to_active_pane(item.clone(), 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); + }); + } + } else { + workspace.add_item_to_active_pane(item.clone(), None, true, window, cx); + } + } + workspace.active_pane().update(cx, |pane, cx| { + pane.set_preview_item_id(Some(item_id), cx); + }); + } + + pub fn rename( + &mut self, + _: &Rename, + window: &mut Window, + cx: &mut Context, + ) -> Option>> { + use language::ToOffset as _; + + let provider = self.semantics_provider.clone()?; + let selection = self.selections.newest_anchor().clone(); + let (cursor_buffer, cursor_buffer_position) = self + .buffer + .read(cx) + .text_anchor_for_position(selection.head(), cx)?; + let (tail_buffer, cursor_buffer_position_end) = self + .buffer + .read(cx) + .text_anchor_for_position(selection.tail(), cx)?; + if tail_buffer != cursor_buffer { + return None; + } + + let snapshot = cursor_buffer.read(cx).snapshot(); + let cursor_buffer_offset = cursor_buffer_position.to_offset(&snapshot); + let cursor_buffer_offset_end = cursor_buffer_position_end.to_offset(&snapshot); + let prepare_rename = provider + .range_for_rename(&cursor_buffer, cursor_buffer_position, cx) + .unwrap_or_else(|| Task::ready(Ok(None))); + drop(snapshot); + + Some(cx.spawn_in(window, async move |this, cx| { + let rename_range = if let Some(range) = prepare_rename.await? { + Some(range) + } else { + this.update(cx, |this, cx| { + let buffer = this.buffer.read(cx).snapshot(cx); + let mut buffer_highlights = this + .document_highlights_for_position(selection.head(), &buffer) + .filter(|highlight| { + highlight.start.excerpt_id == selection.head().excerpt_id + && highlight.end.excerpt_id == selection.head().excerpt_id + }); + buffer_highlights + .next() + .map(|highlight| highlight.start.text_anchor..highlight.end.text_anchor) + })? + }; + if let Some(rename_range) = rename_range { + this.update_in(cx, |this, window, cx| { + let snapshot = cursor_buffer.read(cx).snapshot(); + let rename_buffer_range = rename_range.to_offset(&snapshot); + let cursor_offset_in_rename_range = + cursor_buffer_offset.saturating_sub(rename_buffer_range.start); + let cursor_offset_in_rename_range_end = + cursor_buffer_offset_end.saturating_sub(rename_buffer_range.start); + + this.take_rename(false, window, cx); + let buffer = this.buffer.read(cx).read(cx); + let cursor_offset = selection.head().to_offset(&buffer); + let rename_start = cursor_offset.saturating_sub(cursor_offset_in_rename_range); + let rename_end = rename_start + rename_buffer_range.len(); + let range = buffer.anchor_before(rename_start)..buffer.anchor_after(rename_end); + let mut old_highlight_id = None; + let old_name: Arc = buffer + .chunks(rename_start..rename_end, true) + .map(|chunk| { + if old_highlight_id.is_none() { + old_highlight_id = chunk.syntax_highlight_id; + } + chunk.text + }) + .collect::() + .into(); + + drop(buffer); + + // Position the selection in the rename editor so that it matches the current selection. + this.show_local_selections = false; + let rename_editor = cx.new(|cx| { + let mut editor = Editor::single_line(window, cx); + editor.buffer.update(cx, |buffer, cx| { + buffer.edit([(0..0, old_name.clone())], None, cx) + }); + let rename_selection_range = match cursor_offset_in_rename_range + .cmp(&cursor_offset_in_rename_range_end) + { + Ordering::Equal => { + editor.select_all(&SelectAll, window, cx); + return editor; + } + Ordering::Less => { + cursor_offset_in_rename_range..cursor_offset_in_rename_range_end + } + Ordering::Greater => { + cursor_offset_in_rename_range_end..cursor_offset_in_rename_range + } + }; + if rename_selection_range.end > old_name.len() { + editor.select_all(&SelectAll, window, cx); + } else { + editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.select_ranges([rename_selection_range]); + }); + } + editor + }); + cx.subscribe(&rename_editor, |_, _, e: &EditorEvent, cx| { + if e == &EditorEvent::Focused { + cx.emit(EditorEvent::FocusedIn) + } + }) + .detach(); + + let write_highlights = + this.clear_background_highlights::(cx); + let read_highlights = + this.clear_background_highlights::(cx); + let ranges = write_highlights + .iter() + .flat_map(|(_, ranges)| ranges.iter()) + .chain(read_highlights.iter().flat_map(|(_, ranges)| ranges.iter())) + .cloned() + .collect(); + + this.highlight_text::( + ranges, + HighlightStyle { + fade_out: Some(0.6), + ..Default::default() + }, + cx, + ); + let rename_focus_handle = rename_editor.focus_handle(cx); + window.focus(&rename_focus_handle); + let block_id = this.insert_blocks( + [BlockProperties { + style: BlockStyle::Flex, + placement: BlockPlacement::Below(range.start), + height: Some(1), + render: Arc::new({ + let rename_editor = rename_editor.clone(); + move |cx: &mut BlockContext| { + let mut text_style = cx.editor_style.text.clone(); + if let Some(highlight_style) = old_highlight_id + .and_then(|h| h.style(&cx.editor_style.syntax)) + { + text_style = text_style.highlight(highlight_style); + } + div() + .block_mouse_down() + .pl(cx.anchor_x) + .child(EditorElement::new( + &rename_editor, + EditorStyle { + background: cx.theme().system().transparent, + local_player: cx.editor_style.local_player, + text: text_style, + scrollbar_width: cx.editor_style.scrollbar_width, + syntax: cx.editor_style.syntax.clone(), + status: cx.editor_style.status.clone(), + inlay_hints_style: HighlightStyle { + font_weight: Some(FontWeight::BOLD), + ..make_inlay_hints_style(cx.app) + }, + inline_completion_styles: make_suggestion_styles( + cx.app, + ), + ..EditorStyle::default() + }, + )) + .into_any_element() + } + }), + priority: 0, + }], + Some(Autoscroll::fit()), + cx, + )[0]; + this.pending_rename = Some(RenameState { + range, + old_name, + editor: rename_editor, + block_id, + }); + })?; + } + + Ok(()) + })) + } + + pub fn confirm_rename( + &mut self, + _: &ConfirmRename, + window: &mut Window, + cx: &mut Context, + ) -> Option>> { + let rename = self.take_rename(false, window, cx)?; + let workspace = self.workspace()?.downgrade(); + let (buffer, start) = self + .buffer + .read(cx) + .text_anchor_for_position(rename.range.start, cx)?; + let (end_buffer, _) = self + .buffer + .read(cx) + .text_anchor_for_position(rename.range.end, cx)?; + if buffer != end_buffer { + return None; + } + + let old_name = rename.old_name; + let new_name = rename.editor.read(cx).text(cx); + + let rename = self.semantics_provider.as_ref()?.perform_rename( + &buffer, + start, + new_name.clone(), + cx, + )?; + + Some(cx.spawn_in(window, async move |editor, cx| { + let project_transaction = rename.await?; + Self::open_project_transaction( + &editor, + workspace, + project_transaction, + format!("Rename: {} → {}", old_name, new_name), + cx, + ) + .await?; + + editor.update(cx, |editor, cx| { + editor.refresh_document_highlights(cx); + })?; + Ok(()) + })) + } + + fn take_rename( + &mut self, + moving_cursor: bool, + window: &mut Window, + cx: &mut Context, + ) -> Option { + let rename = self.pending_rename.take()?; + if rename.editor.focus_handle(cx).is_focused(window) { + window.focus(&self.focus_handle); + } + + self.remove_blocks( + [rename.block_id].into_iter().collect(), + Some(Autoscroll::fit()), + cx, + ); + self.clear_highlights::(cx); + self.show_local_selections = true; + + if moving_cursor { + let cursor_in_rename_editor = rename.editor.update(cx, |editor, cx| { + editor.selections.newest::(cx).head() + }); + + // Update the selection to match the position of the selection inside + // the rename editor. + let snapshot = self.buffer.read(cx).read(cx); + let rename_range = rename.range.to_offset(&snapshot); + let cursor_in_editor = snapshot + .clip_offset(rename_range.start + cursor_in_rename_editor, Bias::Left) + .min(rename_range.end); + drop(snapshot); + + self.change_selections(None, window, cx, |s| { + s.select_ranges(vec![cursor_in_editor..cursor_in_editor]) + }); + } else { + self.refresh_document_highlights(cx); + } + + Some(rename) + } + + pub fn pending_rename(&self) -> Option<&RenameState> { + self.pending_rename.as_ref() + } + + fn format( + &mut self, + _: &Format, + window: &mut Window, + cx: &mut Context, + ) -> Option>> { + self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + + let project = match &self.project { + Some(project) => project.clone(), + None => return None, + }; + + Some(self.perform_format( + project, + FormatTrigger::Manual, + FormatTarget::Buffers, + window, + cx, + )) + } + + fn format_selections( + &mut self, + _: &FormatSelections, + window: &mut Window, + cx: &mut Context, + ) -> Option>> { + self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + + let project = match &self.project { + Some(project) => project.clone(), + None => return None, + }; + + let ranges = self + .selections + .all_adjusted(cx) + .into_iter() + .map(|selection| selection.range()) + .collect_vec(); + + Some(self.perform_format( + project, + FormatTrigger::Manual, + FormatTarget::Ranges(ranges), + window, + cx, + )) + } + + fn perform_format( + &mut self, + project: Entity, + trigger: FormatTrigger, + target: FormatTarget, + window: &mut Window, + cx: &mut Context, + ) -> Task> { + let buffer = self.buffer.clone(); + let (buffers, target) = match target { + FormatTarget::Buffers => { + let mut buffers = buffer.read(cx).all_buffers(); + if trigger == FormatTrigger::Save { + buffers.retain(|buffer| buffer.read(cx).is_dirty()); + } + (buffers, LspFormatTarget::Buffers) + } + FormatTarget::Ranges(selection_ranges) => { + let multi_buffer = buffer.read(cx); + let snapshot = multi_buffer.read(cx); + let mut buffers = HashSet::default(); + let mut buffer_id_to_ranges: BTreeMap>> = + BTreeMap::new(); + for selection_range in selection_ranges { + for (buffer, buffer_range, _) in + snapshot.range_to_buffer_ranges(selection_range) + { + let buffer_id = buffer.remote_id(); + let start = buffer.anchor_before(buffer_range.start); + let end = buffer.anchor_after(buffer_range.end); + buffers.insert(multi_buffer.buffer(buffer_id).unwrap()); + buffer_id_to_ranges + .entry(buffer_id) + .and_modify(|buffer_ranges| buffer_ranges.push(start..end)) + .or_insert_with(|| vec![start..end]); + } + } + (buffers, LspFormatTarget::Ranges(buffer_id_to_ranges)) + } + }; + + let transaction_id_prev = buffer.read_with(cx, |b, cx| b.last_transaction_id(cx)); + let selections_prev = transaction_id_prev + .and_then(|transaction_id_prev| { + // default to selections as they were after the last edit, if we have them, + // instead of how they are now. + // This will make it so that editing, moving somewhere else, formatting, then undoing the format + // will take you back to where you made the last edit, instead of staying where you scrolled + self.selection_history + .transaction(transaction_id_prev) + .map(|t| t.0.clone()) + }) + .unwrap_or_else(|| { + log::info!("Failed to determine selections from before format. Falling back to selections when format was initiated"); + self.selections.disjoint_anchors() + }); + + let mut timeout = cx.background_executor().timer(FORMAT_TIMEOUT).fuse(); + let format = project.update(cx, |project, cx| { + project.format(buffers, target, true, trigger, cx) + }); + + cx.spawn_in(window, async move |editor, cx| { + let transaction = futures::select_biased! { + transaction = format.log_err().fuse() => transaction, + () = timeout => { + log::warn!("timed out waiting for formatting"); + None + } + }; + + buffer + .update(cx, |buffer, cx| { + if let Some(transaction) = transaction { + if !buffer.is_singleton() { + buffer.push_transaction(&transaction.0, cx); + } + } + cx.notify(); + }) + .ok(); + + if let Some(transaction_id_now) = + buffer.read_with(cx, |b, cx| b.last_transaction_id(cx))? + { + let has_new_transaction = transaction_id_prev != Some(transaction_id_now); + if has_new_transaction { + _ = editor.update(cx, |editor, _| { + editor + .selection_history + .insert_transaction(transaction_id_now, selections_prev); + }); + } + } + + Ok(()) + }) + } + + fn organize_imports( + &mut self, + _: &OrganizeImports, + window: &mut Window, + cx: &mut Context, + ) -> Option>> { + self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + let project = match &self.project { + Some(project) => project.clone(), + None => return None, + }; + Some(self.perform_code_action_kind( + project, + CodeActionKind::SOURCE_ORGANIZE_IMPORTS, + window, + cx, + )) + } + + fn perform_code_action_kind( + &mut self, + project: Entity, + kind: CodeActionKind, + window: &mut Window, + cx: &mut Context, + ) -> Task> { + let buffer = self.buffer.clone(); + let buffers = buffer.read(cx).all_buffers(); + let mut timeout = cx.background_executor().timer(CODE_ACTION_TIMEOUT).fuse(); + let apply_action = project.update(cx, |project, cx| { + project.apply_code_action_kind(buffers, kind, true, cx) + }); + cx.spawn_in(window, async move |_, cx| { + let transaction = futures::select_biased! { + () = timeout => { + log::warn!("timed out waiting for executing code action"); + None + } + transaction = apply_action.log_err().fuse() => transaction, + }; + buffer + .update(cx, |buffer, cx| { + // check if we need this + if let Some(transaction) = transaction { + if !buffer.is_singleton() { + buffer.push_transaction(&transaction.0, cx); + } + } + cx.notify(); + }) + .ok(); + Ok(()) + }) + } + + fn restart_language_server( + &mut self, + _: &RestartLanguageServer, + _: &mut Window, + cx: &mut Context, + ) { + if let Some(project) = self.project.clone() { + self.buffer.update(cx, |multi_buffer, cx| { + project.update(cx, |project, cx| { + project.restart_language_servers_for_buffers( + multi_buffer.all_buffers().into_iter().collect(), + cx, + ); + }); + }) + } + } + + fn stop_language_server( + &mut self, + _: &StopLanguageServer, + _: &mut Window, + cx: &mut Context, + ) { + if let Some(project) = self.project.clone() { + self.buffer.update(cx, |multi_buffer, cx| { + project.update(cx, |project, cx| { + project.stop_language_servers_for_buffers( + multi_buffer.all_buffers().into_iter().collect(), + cx, + ); + cx.emit(project::Event::RefreshInlayHints); + }); + }); + } + } + + fn cancel_language_server_work( + workspace: &mut Workspace, + _: &actions::CancelLanguageServerWork, + _: &mut Window, + cx: &mut Context, + ) { + let project = workspace.project(); + let buffers = workspace + .active_item(cx) + .and_then(|item| item.act_as::(cx)) + .map_or(HashSet::default(), |editor| { + editor.read(cx).buffer.read(cx).all_buffers() + }); + project.update(cx, |project, cx| { + project.cancel_language_server_work_for_buffers(buffers, cx); + }); + } + + fn show_character_palette( + &mut self, + _: &ShowCharacterPalette, + window: &mut Window, + _: &mut Context, + ) { + window.show_character_palette(); + } + + fn refresh_active_diagnostics(&mut self, cx: &mut Context) { + if let ActiveDiagnostic::Group(active_diagnostics) = &mut self.active_diagnostics { + let buffer = self.buffer.read(cx).snapshot(cx); + let primary_range_start = active_diagnostics.active_range.start.to_offset(&buffer); + let primary_range_end = active_diagnostics.active_range.end.to_offset(&buffer); + let is_valid = buffer + .diagnostics_in_range::(primary_range_start..primary_range_end) + .any(|entry| { + entry.diagnostic.is_primary + && !entry.range.is_empty() + && entry.range.start == primary_range_start + && entry.diagnostic.message == active_diagnostics.active_message + }); + + if !is_valid { + self.dismiss_diagnostics(cx); + } + } + } + + pub fn active_diagnostic_group(&self) -> Option<&ActiveDiagnosticGroup> { + match &self.active_diagnostics { + ActiveDiagnostic::Group(group) => Some(group), + _ => None, + } + } + + pub fn set_all_diagnostics_active(&mut self, cx: &mut Context) { + self.dismiss_diagnostics(cx); + self.active_diagnostics = ActiveDiagnostic::All; + } + + fn activate_diagnostics( + &mut self, + buffer_id: BufferId, + diagnostic: DiagnosticEntry, + window: &mut Window, + cx: &mut Context, + ) { + if matches!(self.active_diagnostics, ActiveDiagnostic::All) { + return; + } + self.dismiss_diagnostics(cx); + let snapshot = self.snapshot(window, cx); + let buffer = self.buffer.read(cx).snapshot(cx); + let Some(renderer) = GlobalDiagnosticRenderer::global(cx) else { + return; + }; + + let diagnostic_group = buffer + .diagnostic_group(buffer_id, diagnostic.diagnostic.group_id) + .collect::>(); + + let blocks = + renderer.render_group(diagnostic_group, buffer_id, snapshot, cx.weak_entity(), cx); + + let blocks = self.display_map.update(cx, |display_map, cx| { + display_map.insert_blocks(blocks, cx).into_iter().collect() + }); + self.active_diagnostics = ActiveDiagnostic::Group(ActiveDiagnosticGroup { + active_range: buffer.anchor_before(diagnostic.range.start) + ..buffer.anchor_after(diagnostic.range.end), + active_message: diagnostic.diagnostic.message.clone(), + group_id: diagnostic.diagnostic.group_id, + blocks, + }); + cx.notify(); + } + + fn dismiss_diagnostics(&mut self, cx: &mut Context) { + if matches!(self.active_diagnostics, ActiveDiagnostic::All) { + return; + }; + + let prev = mem::replace(&mut self.active_diagnostics, ActiveDiagnostic::None); + if let ActiveDiagnostic::Group(group) = prev { + self.display_map.update(cx, |display_map, cx| { + display_map.remove_blocks(group.blocks, cx); + }); + cx.notify(); + } + } + + /// Disable inline diagnostics rendering for this editor. + pub fn disable_inline_diagnostics(&mut self) { + self.inline_diagnostics_enabled = false; + self.inline_diagnostics_update = Task::ready(()); + self.inline_diagnostics.clear(); + } + + pub fn inline_diagnostics_enabled(&self) -> bool { + self.inline_diagnostics_enabled + } + + pub fn show_inline_diagnostics(&self) -> bool { + self.show_inline_diagnostics + } + + pub fn toggle_inline_diagnostics( + &mut self, + _: &ToggleInlineDiagnostics, + window: &mut Window, + cx: &mut Context, + ) { + self.show_inline_diagnostics = !self.show_inline_diagnostics; + self.refresh_inline_diagnostics(false, window, cx); + } + + fn refresh_inline_diagnostics( + &mut self, + debounce: bool, + window: &mut Window, + cx: &mut Context, + ) { + if !self.inline_diagnostics_enabled || !self.show_inline_diagnostics { + self.inline_diagnostics_update = Task::ready(()); + self.inline_diagnostics.clear(); + return; + } + + let debounce_ms = ProjectSettings::get_global(cx) + .diagnostics + .inline + .update_debounce_ms; + let debounce = if debounce && debounce_ms > 0 { + Some(Duration::from_millis(debounce_ms)) + } else { + None + }; + self.inline_diagnostics_update = cx.spawn_in(window, async move |editor, cx| { + let editor = editor.upgrade().unwrap(); + + if let Some(debounce) = debounce { + cx.background_executor().timer(debounce).await; + } + let Some(snapshot) = editor + .update(cx, |editor, cx| editor.buffer().read(cx).snapshot(cx)) + .ok() + else { + return; + }; + + let new_inline_diagnostics = cx + .background_spawn(async move { + let mut inline_diagnostics = Vec::<(Anchor, InlineDiagnostic)>::new(); + for diagnostic_entry in snapshot.diagnostics_in_range(0..snapshot.len()) { + let message = diagnostic_entry + .diagnostic + .message + .split_once('\n') + .map(|(line, _)| line) + .map(SharedString::new) + .unwrap_or_else(|| { + SharedString::from(diagnostic_entry.diagnostic.message) + }); + let start_anchor = snapshot.anchor_before(diagnostic_entry.range.start); + let (Ok(i) | Err(i)) = inline_diagnostics + .binary_search_by(|(probe, _)| probe.cmp(&start_anchor, &snapshot)); + inline_diagnostics.insert( + i, + ( + start_anchor, + InlineDiagnostic { + message, + group_id: diagnostic_entry.diagnostic.group_id, + start: diagnostic_entry.range.start.to_point(&snapshot), + is_primary: diagnostic_entry.diagnostic.is_primary, + severity: diagnostic_entry.diagnostic.severity, + }, + ), + ); + } + inline_diagnostics + }) + .await; + + editor + .update(cx, |editor, cx| { + editor.inline_diagnostics = new_inline_diagnostics; + cx.notify(); + }) + .ok(); + }); + } + + pub fn set_selections_from_remote( + &mut self, + selections: Vec>, + pending_selection: Option>, + window: &mut Window, + cx: &mut Context, + ) { + let old_cursor_position = self.selections.newest_anchor().head(); + self.selections.change_with(cx, |s| { + s.select_anchors(selections); + if let Some(pending_selection) = pending_selection { + s.set_pending(pending_selection, SelectMode::Character); + } else { + s.clear_pending(); + } + }); + self.selections_did_change(false, &old_cursor_position, true, window, cx); + } + + fn push_to_selection_history(&mut self) { + self.selection_history.push(SelectionHistoryEntry { + selections: self.selections.disjoint_anchors(), + select_next_state: self.select_next_state.clone(), + select_prev_state: self.select_prev_state.clone(), + add_selections_state: self.add_selections_state.clone(), + }); + } + + pub fn transact( + &mut self, + window: &mut Window, + cx: &mut Context, + update: impl FnOnce(&mut Self, &mut Window, &mut Context), + ) -> Option { + self.start_transaction_at(Instant::now(), window, cx); + update(self, window, cx); + self.end_transaction_at(Instant::now(), cx) + } + + pub fn start_transaction_at( + &mut self, + now: Instant, + window: &mut Window, + cx: &mut Context, + ) { + self.end_selection(window, cx); + if let Some(tx_id) = self + .buffer + .update(cx, |buffer, cx| buffer.start_transaction_at(now, cx)) + { + self.selection_history + .insert_transaction(tx_id, self.selections.disjoint_anchors()); + cx.emit(EditorEvent::TransactionBegun { + transaction_id: tx_id, + }) + } + } + + pub fn end_transaction_at( + &mut self, + now: Instant, + cx: &mut Context, + ) -> Option { + if let Some(transaction_id) = self + .buffer + .update(cx, |buffer, cx| buffer.end_transaction_at(now, cx)) + { + if let Some((_, end_selections)) = + self.selection_history.transaction_mut(transaction_id) + { + *end_selections = Some(self.selections.disjoint_anchors()); + } else { + log::error!("unexpectedly ended a transaction that wasn't started by this editor"); + } + + cx.emit(EditorEvent::Edited { transaction_id }); + Some(transaction_id) + } else { + None + } + } + + pub fn set_mark(&mut self, _: &actions::SetMark, window: &mut Window, cx: &mut Context) { + if self.selection_mark_mode { + self.change_selections(None, window, cx, |s| { + s.move_with(|_, sel| { + sel.collapse_to(sel.head(), SelectionGoal::None); + }); + }) + } + self.selection_mark_mode = true; + cx.notify(); + } + + pub fn swap_selection_ends( + &mut self, + _: &actions::SwapSelectionEnds, + window: &mut Window, + cx: &mut Context, + ) { + self.change_selections(None, window, cx, |s| { + s.move_with(|_, sel| { + if sel.start != sel.end { + sel.reversed = !sel.reversed + } + }); + }); + self.request_autoscroll(Autoscroll::newest(), cx); + cx.notify(); + } + + pub fn toggle_fold( + &mut self, + _: &actions::ToggleFold, + window: &mut Window, + cx: &mut Context, + ) { + if self.is_singleton(cx) { + let selection = self.selections.newest::(cx); + + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let range = if selection.is_empty() { + let point = selection.head().to_display_point(&display_map); + let start = DisplayPoint::new(point.row(), 0).to_point(&display_map); + let end = DisplayPoint::new(point.row(), display_map.line_len(point.row())) + .to_point(&display_map); + start..end + } else { + selection.range() + }; + if display_map.folds_in_range(range).next().is_some() { + self.unfold_lines(&Default::default(), window, cx) + } else { + self.fold(&Default::default(), window, cx) + } + } else { + let multi_buffer_snapshot = self.buffer.read(cx).snapshot(cx); + let buffer_ids: HashSet<_> = self + .selections + .disjoint_anchor_ranges() + .flat_map(|range| multi_buffer_snapshot.buffer_ids_for_range(range)) + .collect(); + + let should_unfold = buffer_ids + .iter() + .any(|buffer_id| self.is_buffer_folded(*buffer_id, cx)); + + for buffer_id in buffer_ids { + if should_unfold { + self.unfold_buffer(buffer_id, cx); + } else { + self.fold_buffer(buffer_id, cx); + } + } + } + } + + pub fn toggle_fold_recursive( + &mut self, + _: &actions::ToggleFoldRecursive, + window: &mut Window, + cx: &mut Context, + ) { + let selection = self.selections.newest::(cx); + + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let range = if selection.is_empty() { + let point = selection.head().to_display_point(&display_map); + let start = DisplayPoint::new(point.row(), 0).to_point(&display_map); + let end = DisplayPoint::new(point.row(), display_map.line_len(point.row())) + .to_point(&display_map); + start..end + } else { + selection.range() + }; + if display_map.folds_in_range(range).next().is_some() { + self.unfold_recursive(&Default::default(), window, cx) + } else { + self.fold_recursive(&Default::default(), window, cx) + } + } + + pub fn fold(&mut self, _: &actions::Fold, window: &mut Window, cx: &mut Context) { + if self.is_singleton(cx) { + let mut to_fold = Vec::new(); + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let selections = self.selections.all_adjusted(cx); + + for selection in selections { + let range = selection.range().sorted(); + let buffer_start_row = range.start.row; + + if range.start.row != range.end.row { + let mut found = false; + let mut row = range.start.row; + while row <= range.end.row { + if let Some(crease) = display_map.crease_for_buffer_row(MultiBufferRow(row)) + { + found = true; + row = crease.range().end.row + 1; + to_fold.push(crease); + } else { + row += 1 + } + } + if found { + continue; + } + } + + for row in (0..=range.start.row).rev() { + if let Some(crease) = display_map.crease_for_buffer_row(MultiBufferRow(row)) { + if crease.range().end.row >= buffer_start_row { + to_fold.push(crease); + if row <= range.start.row { + break; + } + } + } + } + } + + self.fold_creases(to_fold, true, window, cx); + } else { + let multi_buffer_snapshot = self.buffer.read(cx).snapshot(cx); + let buffer_ids = self + .selections + .disjoint_anchor_ranges() + .flat_map(|range| multi_buffer_snapshot.buffer_ids_for_range(range)) + .collect::>(); + for buffer_id in buffer_ids { + self.fold_buffer(buffer_id, cx); + } + } + } + + fn fold_at_level( + &mut self, + fold_at: &FoldAtLevel, + window: &mut Window, + cx: &mut Context, + ) { + if !self.buffer.read(cx).is_singleton() { + return; + } + + let fold_at_level = fold_at.0; + let snapshot = self.buffer.read(cx).snapshot(cx); + let mut to_fold = Vec::new(); + let mut stack = vec![(0, snapshot.max_row().0, 1)]; + + while let Some((mut start_row, end_row, current_level)) = stack.pop() { + while start_row < end_row { + match self + .snapshot(window, cx) + .crease_for_buffer_row(MultiBufferRow(start_row)) + { + Some(crease) => { + let nested_start_row = crease.range().start.row + 1; + let nested_end_row = crease.range().end.row; + + if current_level < fold_at_level { + stack.push((nested_start_row, nested_end_row, current_level + 1)); + } else if current_level == fold_at_level { + to_fold.push(crease); + } + + start_row = nested_end_row + 1; + } + None => start_row += 1, + } + } + } + + self.fold_creases(to_fold, true, window, cx); + } + + pub fn fold_all(&mut self, _: &actions::FoldAll, window: &mut Window, cx: &mut Context) { + if self.buffer.read(cx).is_singleton() { + let mut fold_ranges = Vec::new(); + let snapshot = self.buffer.read(cx).snapshot(cx); + + for row in 0..snapshot.max_row().0 { + if let Some(foldable_range) = self + .snapshot(window, cx) + .crease_for_buffer_row(MultiBufferRow(row)) + { + fold_ranges.push(foldable_range); + } + } + + self.fold_creases(fold_ranges, true, window, cx); + } else { + self.toggle_fold_multiple_buffers = cx.spawn_in(window, async move |editor, cx| { + editor + .update_in(cx, |editor, _, cx| { + for buffer_id in editor.buffer.read(cx).excerpt_buffer_ids() { + editor.fold_buffer(buffer_id, cx); + } + }) + .ok(); + }); + } + } + + pub fn fold_function_bodies( + &mut self, + _: &actions::FoldFunctionBodies, + window: &mut Window, + cx: &mut Context, + ) { + let snapshot = self.buffer.read(cx).snapshot(cx); + + let ranges = snapshot + .text_object_ranges(0..snapshot.len(), TreeSitterOptions::default()) + .filter_map(|(range, obj)| (obj == TextObject::InsideFunction).then_some(range)) + .collect::>(); + + let creases = ranges + .into_iter() + .map(|range| Crease::simple(range, self.display_map.read(cx).fold_placeholder.clone())) + .collect(); + + self.fold_creases(creases, true, window, cx); + } + + pub fn fold_recursive( + &mut self, + _: &actions::FoldRecursive, + window: &mut Window, + cx: &mut Context, + ) { + let mut to_fold = Vec::new(); + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let selections = self.selections.all_adjusted(cx); + + for selection in selections { + let range = selection.range().sorted(); + let buffer_start_row = range.start.row; + + if range.start.row != range.end.row { + let mut found = false; + for row in range.start.row..=range.end.row { + if let Some(crease) = display_map.crease_for_buffer_row(MultiBufferRow(row)) { + found = true; + to_fold.push(crease); + } + } + if found { + continue; + } + } + + for row in (0..=range.start.row).rev() { + if let Some(crease) = display_map.crease_for_buffer_row(MultiBufferRow(row)) { + if crease.range().end.row >= buffer_start_row { + to_fold.push(crease); + } else { + break; + } + } + } + } + + self.fold_creases(to_fold, true, window, cx); + } + + pub fn fold_at( + &mut self, + buffer_row: MultiBufferRow, + window: &mut Window, + cx: &mut Context, + ) { + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + + if let Some(crease) = display_map.crease_for_buffer_row(buffer_row) { + let autoscroll = self + .selections + .all::(cx) + .iter() + .any(|selection| crease.range().overlaps(&selection.range())); + + self.fold_creases(vec![crease], autoscroll, window, cx); + } + } + + pub fn unfold_lines(&mut self, _: &UnfoldLines, _window: &mut Window, cx: &mut Context) { + if self.is_singleton(cx) { + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let buffer = &display_map.buffer_snapshot; + let selections = self.selections.all::(cx); + let ranges = selections + .iter() + .map(|s| { + let range = s.display_range(&display_map).sorted(); + let mut start = range.start.to_point(&display_map); + let mut end = range.end.to_point(&display_map); + start.column = 0; + end.column = buffer.line_len(MultiBufferRow(end.row)); + start..end + }) + .collect::>(); + + self.unfold_ranges(&ranges, true, true, cx); + } else { + let multi_buffer_snapshot = self.buffer.read(cx).snapshot(cx); + let buffer_ids = self + .selections + .disjoint_anchor_ranges() + .flat_map(|range| multi_buffer_snapshot.buffer_ids_for_range(range)) + .collect::>(); + for buffer_id in buffer_ids { + self.unfold_buffer(buffer_id, cx); + } + } + } + + pub fn unfold_recursive( + &mut self, + _: &UnfoldRecursive, + _window: &mut Window, + cx: &mut Context, + ) { + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let selections = self.selections.all::(cx); + let ranges = selections + .iter() + .map(|s| { + let mut range = s.display_range(&display_map).sorted(); + *range.start.column_mut() = 0; + *range.end.column_mut() = display_map.line_len(range.end.row()); + let start = range.start.to_point(&display_map); + let end = range.end.to_point(&display_map); + start..end + }) + .collect::>(); + + self.unfold_ranges(&ranges, true, true, cx); + } + + pub fn unfold_at( + &mut self, + buffer_row: MultiBufferRow, + _window: &mut Window, + cx: &mut Context, + ) { + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + + let intersection_range = Point::new(buffer_row.0, 0) + ..Point::new( + buffer_row.0, + display_map.buffer_snapshot.line_len(buffer_row), + ); + + let autoscroll = self + .selections + .all::(cx) + .iter() + .any(|selection| RangeExt::overlaps(&selection.range(), &intersection_range)); + + self.unfold_ranges(&[intersection_range], true, autoscroll, cx); + } + + pub fn unfold_all( + &mut self, + _: &actions::UnfoldAll, + _window: &mut Window, + cx: &mut Context, + ) { + if self.buffer.read(cx).is_singleton() { + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + self.unfold_ranges(&[0..display_map.buffer_snapshot.len()], true, true, cx); + } else { + self.toggle_fold_multiple_buffers = cx.spawn(async move |editor, cx| { + editor + .update(cx, |editor, cx| { + for buffer_id in editor.buffer.read(cx).excerpt_buffer_ids() { + editor.unfold_buffer(buffer_id, cx); + } + }) + .ok(); + }); + } + } + + pub fn fold_selected_ranges( + &mut self, + _: &FoldSelectedRanges, + window: &mut Window, + cx: &mut Context, + ) { + let selections = self.selections.all_adjusted(cx); + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let ranges = selections + .into_iter() + .map(|s| Crease::simple(s.range(), display_map.fold_placeholder.clone())) + .collect::>(); + self.fold_creases(ranges, true, window, cx); + } + + pub fn fold_ranges( + &mut self, + ranges: Vec>, + auto_scroll: bool, + window: &mut Window, + cx: &mut Context, + ) { + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let ranges = ranges + .into_iter() + .map(|r| Crease::simple(r, display_map.fold_placeholder.clone())) + .collect::>(); + self.fold_creases(ranges, auto_scroll, window, cx); + } + + pub fn fold_creases( + &mut self, + creases: Vec>, + auto_scroll: bool, + _window: &mut Window, + cx: &mut Context, + ) { + if creases.is_empty() { + return; + } + + let mut buffers_affected = HashSet::default(); + let multi_buffer = self.buffer().read(cx); + for crease in &creases { + if let Some((_, buffer, _)) = + multi_buffer.excerpt_containing(crease.range().start.clone(), cx) + { + buffers_affected.insert(buffer.read(cx).remote_id()); + }; + } + + self.display_map.update(cx, |map, cx| map.fold(creases, cx)); + + if auto_scroll { + self.request_autoscroll(Autoscroll::fit(), cx); + } + + cx.notify(); + + self.scrollbar_marker_state.dirty = true; + self.folds_did_change(cx); + } + + /// Removes any folds whose ranges intersect any of the given ranges. + pub fn unfold_ranges( + &mut self, + ranges: &[Range], + inclusive: bool, + auto_scroll: bool, + cx: &mut Context, + ) { + self.remove_folds_with(ranges, auto_scroll, cx, |map, cx| { + map.unfold_intersecting(ranges.iter().cloned(), inclusive, cx) + }); + self.folds_did_change(cx); + } + + pub fn fold_buffer(&mut self, buffer_id: BufferId, cx: &mut Context) { + if self.buffer().read(cx).is_singleton() || self.is_buffer_folded(buffer_id, cx) { + return; + } + let folded_excerpts = self.buffer().read(cx).excerpts_for_buffer(buffer_id, cx); + self.display_map.update(cx, |display_map, cx| { + display_map.fold_buffers([buffer_id], cx) + }); + cx.emit(EditorEvent::BufferFoldToggled { + ids: folded_excerpts.iter().map(|&(id, _)| id).collect(), + folded: true, + }); + cx.notify(); + } + + pub fn unfold_buffer(&mut self, buffer_id: BufferId, cx: &mut Context) { + if self.buffer().read(cx).is_singleton() || !self.is_buffer_folded(buffer_id, cx) { + return; + } + let unfolded_excerpts = self.buffer().read(cx).excerpts_for_buffer(buffer_id, cx); + self.display_map.update(cx, |display_map, cx| { + display_map.unfold_buffers([buffer_id], cx); + }); + cx.emit(EditorEvent::BufferFoldToggled { + ids: unfolded_excerpts.iter().map(|&(id, _)| id).collect(), + folded: false, + }); + cx.notify(); + } + + pub fn is_buffer_folded(&self, buffer: BufferId, cx: &App) -> bool { + self.display_map.read(cx).is_buffer_folded(buffer) + } + + pub fn folded_buffers<'a>(&self, cx: &'a App) -> &'a HashSet { + self.display_map.read(cx).folded_buffers() + } + + pub fn disable_header_for_buffer(&mut self, buffer_id: BufferId, cx: &mut Context) { + self.display_map.update(cx, |display_map, cx| { + display_map.disable_header_for_buffer(buffer_id, cx); + }); + cx.notify(); + } + + /// Removes any folds with the given ranges. + pub fn remove_folds_with_type( + &mut self, + ranges: &[Range], + type_id: TypeId, + auto_scroll: bool, + cx: &mut Context, + ) { + self.remove_folds_with(ranges, auto_scroll, cx, |map, cx| { + map.remove_folds_with_type(ranges.iter().cloned(), type_id, cx) + }); + self.folds_did_change(cx); + } + + fn remove_folds_with( + &mut self, + ranges: &[Range], + auto_scroll: bool, + cx: &mut Context, + update: impl FnOnce(&mut DisplayMap, &mut Context), + ) { + if ranges.is_empty() { + return; + } + + let mut buffers_affected = HashSet::default(); + let multi_buffer = self.buffer().read(cx); + for range in ranges { + if let Some((_, buffer, _)) = multi_buffer.excerpt_containing(range.start.clone(), cx) { + buffers_affected.insert(buffer.read(cx).remote_id()); + }; + } + + self.display_map.update(cx, update); + + if auto_scroll { + self.request_autoscroll(Autoscroll::fit(), cx); + } + + cx.notify(); + self.scrollbar_marker_state.dirty = true; + self.active_indent_guides_state.dirty = true; + } + + pub fn update_fold_widths( + &mut self, + widths: impl IntoIterator, + cx: &mut Context, + ) -> bool { + self.display_map + .update(cx, |map, cx| map.update_fold_widths(widths, cx)) + } + + pub fn default_fold_placeholder(&self, cx: &App) -> FoldPlaceholder { + self.display_map.read(cx).fold_placeholder.clone() + } + + pub fn set_expand_all_diff_hunks(&mut self, cx: &mut App) { + self.buffer.update(cx, |buffer, cx| { + buffer.set_all_diff_hunks_expanded(cx); + }); + } + + pub fn expand_all_diff_hunks( + &mut self, + _: &ExpandAllDiffHunks, + _window: &mut Window, + cx: &mut Context, + ) { + self.buffer.update(cx, |buffer, cx| { + buffer.expand_diff_hunks(vec![Anchor::min()..Anchor::max()], cx) + }); + } + + pub fn toggle_selected_diff_hunks( + &mut self, + _: &ToggleSelectedDiffHunks, + _window: &mut Window, + cx: &mut Context, + ) { + let ranges: Vec<_> = self.selections.disjoint.iter().map(|s| s.range()).collect(); + self.toggle_diff_hunks_in_ranges(ranges, cx); + } + + pub fn diff_hunks_in_ranges<'a>( + &'a self, + ranges: &'a [Range], + buffer: &'a MultiBufferSnapshot, + ) -> impl 'a + Iterator { + ranges.iter().flat_map(move |range| { + let end_excerpt_id = range.end.excerpt_id; + let range = range.to_point(buffer); + let mut peek_end = range.end; + if range.end.row < buffer.max_row().0 { + peek_end = Point::new(range.end.row + 1, 0); + } + buffer + .diff_hunks_in_range(range.start..peek_end) + .filter(move |hunk| hunk.excerpt_id.cmp(&end_excerpt_id, buffer).is_le()) + }) + } + + pub fn has_stageable_diff_hunks_in_ranges( + &self, + ranges: &[Range], + snapshot: &MultiBufferSnapshot, + ) -> bool { + let mut hunks = self.diff_hunks_in_ranges(ranges, &snapshot); + hunks.any(|hunk| hunk.status().has_secondary_hunk()) + } + + pub fn toggle_staged_selected_diff_hunks( + &mut self, + _: &::git::ToggleStaged, + _: &mut Window, + cx: &mut Context, + ) { + let snapshot = self.buffer.read(cx).snapshot(cx); + let ranges: Vec<_> = self.selections.disjoint.iter().map(|s| s.range()).collect(); + let stage = self.has_stageable_diff_hunks_in_ranges(&ranges, &snapshot); + self.stage_or_unstage_diff_hunks(stage, ranges, cx); + } + + pub fn set_render_diff_hunk_controls( + &mut self, + render_diff_hunk_controls: RenderDiffHunkControlsFn, + cx: &mut Context, + ) { + self.render_diff_hunk_controls = render_diff_hunk_controls; + cx.notify(); + } + + pub fn stage_and_next( + &mut self, + _: &::git::StageAndNext, + window: &mut Window, + cx: &mut Context, + ) { + self.do_stage_or_unstage_and_next(true, window, cx); + } + + pub fn unstage_and_next( + &mut self, + _: &::git::UnstageAndNext, + window: &mut Window, + cx: &mut Context, + ) { + self.do_stage_or_unstage_and_next(false, window, cx); + } + + pub fn stage_or_unstage_diff_hunks( + &mut self, + stage: bool, + ranges: Vec>, + cx: &mut Context, + ) { + let task = self.save_buffers_for_ranges_if_needed(&ranges, cx); + cx.spawn(async move |this, cx| { + task.await?; + this.update(cx, |this, cx| { + let snapshot = this.buffer.read(cx).snapshot(cx); + let chunk_by = this + .diff_hunks_in_ranges(&ranges, &snapshot) + .chunk_by(|hunk| hunk.buffer_id); + for (buffer_id, hunks) in &chunk_by { + this.do_stage_or_unstage(stage, buffer_id, hunks, cx); + } + }) + }) + .detach_and_log_err(cx); + } + + fn save_buffers_for_ranges_if_needed( + &mut self, + ranges: &[Range], + cx: &mut Context, + ) -> Task> { + let multibuffer = self.buffer.read(cx); + let snapshot = multibuffer.read(cx); + let buffer_ids: HashSet<_> = ranges + .iter() + .flat_map(|range| snapshot.buffer_ids_for_range(range.clone())) + .collect(); + drop(snapshot); + + let mut buffers = HashSet::default(); + for buffer_id in buffer_ids { + if let Some(buffer_entity) = multibuffer.buffer(buffer_id) { + let buffer = buffer_entity.read(cx); + if buffer.file().is_some_and(|file| file.disk_state().exists()) && buffer.is_dirty() + { + buffers.insert(buffer_entity); + } + } + } + + if let Some(project) = &self.project { + project.update(cx, |project, cx| project.save_buffers(buffers, cx)) + } else { + Task::ready(Ok(())) + } + } + + fn do_stage_or_unstage_and_next( + &mut self, + stage: bool, + window: &mut Window, + cx: &mut Context, + ) { + let ranges = self.selections.disjoint_anchor_ranges().collect::>(); + + if ranges.iter().any(|range| range.start != range.end) { + self.stage_or_unstage_diff_hunks(stage, ranges, cx); + return; + } + + self.stage_or_unstage_diff_hunks(stage, ranges, cx); + let snapshot = self.snapshot(window, cx); + let position = self.selections.newest::(cx).head(); + let mut row = snapshot + .buffer_snapshot + .diff_hunks_in_range(position..snapshot.buffer_snapshot.max_point()) + .find(|hunk| hunk.row_range.start.0 > position.row) + .map(|hunk| hunk.row_range.start); + + let all_diff_hunks_expanded = self.buffer().read(cx).all_diff_hunks_expanded(); + // Outside of the project diff editor, wrap around to the beginning. + if !all_diff_hunks_expanded { + row = row.or_else(|| { + snapshot + .buffer_snapshot + .diff_hunks_in_range(Point::zero()..position) + .find(|hunk| hunk.row_range.end.0 < position.row) + .map(|hunk| hunk.row_range.start) + }); + } + + if let Some(row) = row { + let destination = Point::new(row.0, 0); + let autoscroll = Autoscroll::center(); + + self.unfold_ranges(&[destination..destination], false, false, cx); + self.change_selections(Some(autoscroll), window, cx, |s| { + s.select_ranges([destination..destination]); + }); + } + } + + fn do_stage_or_unstage( + &self, + stage: bool, + buffer_id: BufferId, + hunks: impl Iterator, + cx: &mut App, + ) -> Option<()> { + let project = self.project.as_ref()?; + let buffer = project.read(cx).buffer_for_id(buffer_id, cx)?; + let diff = self.buffer.read(cx).diff_for(buffer_id)?; + let buffer_snapshot = buffer.read(cx).snapshot(); + let file_exists = buffer_snapshot + .file() + .is_some_and(|file| file.disk_state().exists()); + diff.update(cx, |diff, cx| { + diff.stage_or_unstage_hunks( + stage, + &hunks + .map(|hunk| buffer_diff::DiffHunk { + buffer_range: hunk.buffer_range, + diff_base_byte_range: hunk.diff_base_byte_range, + secondary_status: hunk.secondary_status, + range: Point::zero()..Point::zero(), // unused + }) + .collect::>(), + &buffer_snapshot, + file_exists, + cx, + ) + }); + None + } + + pub fn expand_selected_diff_hunks(&mut self, cx: &mut Context) { + let ranges: Vec<_> = self.selections.disjoint.iter().map(|s| s.range()).collect(); + self.buffer + .update(cx, |buffer, cx| buffer.expand_diff_hunks(ranges, cx)) + } + + pub fn clear_expanded_diff_hunks(&mut self, cx: &mut Context) -> bool { + self.buffer.update(cx, |buffer, cx| { + let ranges = vec![Anchor::min()..Anchor::max()]; + if !buffer.all_diff_hunks_expanded() + && buffer.has_expanded_diff_hunks_in_ranges(&ranges, cx) + { + buffer.collapse_diff_hunks(ranges, cx); + true + } else { + false + } + }) + } + + fn toggle_diff_hunks_in_ranges( + &mut self, + ranges: Vec>, + cx: &mut Context, + ) { + self.buffer.update(cx, |buffer, cx| { + let expand = !buffer.has_expanded_diff_hunks_in_ranges(&ranges, cx); + buffer.expand_or_collapse_diff_hunks(ranges, expand, cx); + }) + } + + fn toggle_single_diff_hunk(&mut self, range: Range, cx: &mut Context) { + self.buffer.update(cx, |buffer, cx| { + let snapshot = buffer.snapshot(cx); + let excerpt_id = range.end.excerpt_id; + let point_range = range.to_point(&snapshot); + let expand = !buffer.single_hunk_is_expanded(range, cx); + buffer.expand_or_collapse_diff_hunks_inner([(point_range, excerpt_id)], expand, cx); + }) + } + + pub(crate) fn apply_all_diff_hunks( + &mut self, + _: &ApplyAllDiffHunks, + window: &mut Window, + cx: &mut Context, + ) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + + let buffers = self.buffer.read(cx).all_buffers(); + for branch_buffer in buffers { + branch_buffer.update(cx, |branch_buffer, cx| { + branch_buffer.merge_into_base(Vec::new(), cx); + }); + } + + if let Some(project) = self.project.clone() { + self.save(true, project, window, cx).detach_and_log_err(cx); + } + } + + pub(crate) fn apply_selected_diff_hunks( + &mut self, + _: &ApplyDiffHunk, + window: &mut Window, + cx: &mut Context, + ) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + let snapshot = self.snapshot(window, cx); + let hunks = snapshot.hunks_for_ranges(self.selections.ranges(cx)); + let mut ranges_by_buffer = HashMap::default(); + self.transact(window, cx, |editor, _window, cx| { + for hunk in hunks { + if let Some(buffer) = editor.buffer.read(cx).buffer(hunk.buffer_id) { + ranges_by_buffer + .entry(buffer.clone()) + .or_insert_with(Vec::new) + .push(hunk.buffer_range.to_offset(buffer.read(cx))); + } + } + + for (buffer, ranges) in ranges_by_buffer { + buffer.update(cx, |buffer, cx| { + buffer.merge_into_base(ranges, cx); + }); + } + }); + + if let Some(project) = self.project.clone() { + self.save(true, project, window, cx).detach_and_log_err(cx); + } + } + + pub fn set_gutter_hovered(&mut self, hovered: bool, cx: &mut Context) { + if hovered != self.gutter_hovered { + self.gutter_hovered = hovered; + cx.notify(); + } + } + + pub fn insert_blocks( + &mut self, + blocks: impl IntoIterator>, + autoscroll: Option, + cx: &mut Context, + ) -> Vec { + let blocks = self + .display_map + .update(cx, |display_map, cx| display_map.insert_blocks(blocks, cx)); + if let Some(autoscroll) = autoscroll { + self.request_autoscroll(autoscroll, cx); + } + cx.notify(); + blocks + } + + pub fn resize_blocks( + &mut self, + heights: HashMap, + autoscroll: Option, + cx: &mut Context, + ) { + self.display_map + .update(cx, |display_map, cx| display_map.resize_blocks(heights, cx)); + if let Some(autoscroll) = autoscroll { + self.request_autoscroll(autoscroll, cx); + } + cx.notify(); + } + + pub fn replace_blocks( + &mut self, + renderers: HashMap, + autoscroll: Option, + cx: &mut Context, + ) { + self.display_map + .update(cx, |display_map, _cx| display_map.replace_blocks(renderers)); + if let Some(autoscroll) = autoscroll { + self.request_autoscroll(autoscroll, cx); + } + cx.notify(); + } + + pub fn remove_blocks( + &mut self, + block_ids: HashSet, + autoscroll: Option, + cx: &mut Context, + ) { + self.display_map.update(cx, |display_map, cx| { + display_map.remove_blocks(block_ids, cx) + }); + if let Some(autoscroll) = autoscroll { + self.request_autoscroll(autoscroll, cx); + } + cx.notify(); + } + + pub fn row_for_block( + &self, + block_id: CustomBlockId, + cx: &mut Context, + ) -> Option { + self.display_map + .update(cx, |map, cx| map.row_for_block(block_id, cx)) + } + + pub(crate) fn set_focused_block(&mut self, focused_block: FocusedBlock) { + self.focused_block = Some(focused_block); + } + + pub(crate) fn take_focused_block(&mut self) -> Option { + self.focused_block.take() + } + + pub fn insert_creases( + &mut self, + creases: impl IntoIterator>, + cx: &mut Context, + ) -> Vec { + self.display_map + .update(cx, |map, cx| map.insert_creases(creases, cx)) + } + + pub fn remove_creases( + &mut self, + ids: impl IntoIterator, + cx: &mut Context, + ) { + self.display_map + .update(cx, |map, cx| map.remove_creases(ids, cx)); + } + + pub fn longest_row(&self, cx: &mut App) -> DisplayRow { + self.display_map + .update(cx, |map, cx| map.snapshot(cx)) + .longest_row() + } + + pub fn max_point(&self, cx: &mut App) -> DisplayPoint { + self.display_map + .update(cx, |map, cx| map.snapshot(cx)) + .max_point() + } + + pub fn text(&self, cx: &App) -> String { + self.buffer.read(cx).read(cx).text() + } + + pub fn is_empty(&self, cx: &App) -> bool { + self.buffer.read(cx).read(cx).is_empty() + } + + pub fn text_option(&self, cx: &App) -> Option { + let text = self.text(cx); + let text = text.trim(); + + if text.is_empty() { + return None; + } + + Some(text.to_string()) + } + + pub fn set_text( + &mut self, + text: impl Into>, + window: &mut Window, + cx: &mut Context, + ) { + self.transact(window, cx, |this, _, cx| { + this.buffer + .read(cx) + .as_singleton() + .expect("you can only call set_text on editors for singleton buffers") + .update(cx, |buffer, cx| buffer.set_text(text, cx)); + }); + } + + pub fn display_text(&self, cx: &mut App) -> String { + self.display_map + .update(cx, |map, cx| map.snapshot(cx)) + .text() + } + + pub fn wrap_guides(&self, cx: &App) -> SmallVec<[(usize, bool); 2]> { + let mut wrap_guides = smallvec::smallvec![]; + + if self.show_wrap_guides == Some(false) { + return wrap_guides; + } + + let settings = self.buffer.read(cx).language_settings(cx); + if settings.show_wrap_guides { + match self.soft_wrap_mode(cx) { + SoftWrap::Column(soft_wrap) => { + wrap_guides.push((soft_wrap as usize, true)); + } + SoftWrap::Bounded(soft_wrap) => { + wrap_guides.push((soft_wrap as usize, true)); + } + SoftWrap::GitDiff | SoftWrap::None | SoftWrap::EditorWidth => {} + } + wrap_guides.extend(settings.wrap_guides.iter().map(|guide| (*guide, false))) + } + + wrap_guides + } + + pub fn soft_wrap_mode(&self, cx: &App) -> SoftWrap { + let settings = self.buffer.read(cx).language_settings(cx); + let mode = self.soft_wrap_mode_override.unwrap_or(settings.soft_wrap); + match mode { + language_settings::SoftWrap::PreferLine | language_settings::SoftWrap::None => { + SoftWrap::None + } + language_settings::SoftWrap::EditorWidth => SoftWrap::EditorWidth, + language_settings::SoftWrap::PreferredLineLength => { + SoftWrap::Column(settings.preferred_line_length) + } + language_settings::SoftWrap::Bounded => { + SoftWrap::Bounded(settings.preferred_line_length) + } + } + } + + pub fn set_soft_wrap_mode( + &mut self, + mode: language_settings::SoftWrap, + + cx: &mut Context, + ) { + self.soft_wrap_mode_override = Some(mode); + cx.notify(); + } + + pub fn set_hard_wrap(&mut self, hard_wrap: Option, cx: &mut Context) { + self.hard_wrap = hard_wrap; + cx.notify(); + } + + pub fn set_text_style_refinement(&mut self, style: TextStyleRefinement) { + self.text_style_refinement = Some(style); + } + + /// called by the Element so we know what style we were most recently rendered with. + pub(crate) fn set_style( + &mut self, + style: EditorStyle, + window: &mut Window, + cx: &mut Context, + ) { + let rem_size = window.rem_size(); + self.display_map.update(cx, |map, cx| { + map.set_font( + style.text.font(), + style.text.font_size.to_pixels(rem_size), + cx, + ) + }); + self.style = Some(style); + } + + pub fn style(&self) -> Option<&EditorStyle> { + self.style.as_ref() + } + + // Called by the element. This method is not designed to be called outside of the editor + // element's layout code because it does not notify when rewrapping is computed synchronously. + pub(crate) fn set_wrap_width(&self, width: Option, cx: &mut App) -> bool { + self.display_map + .update(cx, |map, cx| map.set_wrap_width(width, cx)) + } + + pub fn set_soft_wrap(&mut self) { + self.soft_wrap_mode_override = Some(language_settings::SoftWrap::EditorWidth) + } + + pub fn toggle_soft_wrap(&mut self, _: &ToggleSoftWrap, _: &mut Window, cx: &mut Context) { + if self.soft_wrap_mode_override.is_some() { + self.soft_wrap_mode_override.take(); + } else { + let soft_wrap = match self.soft_wrap_mode(cx) { + SoftWrap::GitDiff => return, + SoftWrap::None => language_settings::SoftWrap::EditorWidth, + SoftWrap::EditorWidth | SoftWrap::Column(_) | SoftWrap::Bounded(_) => { + language_settings::SoftWrap::None + } + }; + self.soft_wrap_mode_override = Some(soft_wrap); + } + cx.notify(); + } + + pub fn toggle_tab_bar(&mut self, _: &ToggleTabBar, _: &mut Window, cx: &mut Context) { + let Some(workspace) = self.workspace() else { + return; + }; + let fs = workspace.read(cx).app_state().fs.clone(); + let current_show = TabBarSettings::get_global(cx).show; + update_settings_file::(fs, cx, move |setting, _| { + setting.show = Some(!current_show); + }); + } + + pub fn toggle_indent_guides( + &mut self, + _: &ToggleIndentGuides, + _: &mut Window, + cx: &mut Context, + ) { + let currently_enabled = self.should_show_indent_guides().unwrap_or_else(|| { + self.buffer + .read(cx) + .language_settings(cx) + .indent_guides + .enabled + }); + self.show_indent_guides = Some(!currently_enabled); + cx.notify(); + } + + fn should_show_indent_guides(&self) -> Option { + self.show_indent_guides + } + + pub fn toggle_line_numbers( + &mut self, + _: &ToggleLineNumbers, + _: &mut Window, + cx: &mut Context, + ) { + let mut editor_settings = EditorSettings::get_global(cx).clone(); + editor_settings.gutter.line_numbers = !editor_settings.gutter.line_numbers; + EditorSettings::override_global(editor_settings, cx); + } + + pub fn line_numbers_enabled(&self, cx: &App) -> bool { + if let Some(show_line_numbers) = self.show_line_numbers { + return show_line_numbers; + } + EditorSettings::get_global(cx).gutter.line_numbers + } + + pub fn should_use_relative_line_numbers(&self, cx: &mut App) -> bool { + self.use_relative_line_numbers + .unwrap_or(EditorSettings::get_global(cx).relative_line_numbers) + } + + pub fn toggle_relative_line_numbers( + &mut self, + _: &ToggleRelativeLineNumbers, + _: &mut Window, + cx: &mut Context, + ) { + let is_relative = self.should_use_relative_line_numbers(cx); + self.set_relative_line_number(Some(!is_relative), cx) + } + + pub fn set_relative_line_number(&mut self, is_relative: Option, cx: &mut Context) { + self.use_relative_line_numbers = is_relative; + cx.notify(); + } + + pub fn set_show_gutter(&mut self, show_gutter: bool, cx: &mut Context) { + self.show_gutter = show_gutter; + cx.notify(); + } + + pub fn set_show_scrollbars(&mut self, show_scrollbars: bool, cx: &mut Context) { + self.show_scrollbars = show_scrollbars; + cx.notify(); + } + + pub fn disable_scrolling(&mut self, cx: &mut Context) { + self.disable_scrolling = true; + cx.notify(); + } + + pub fn set_show_line_numbers(&mut self, show_line_numbers: bool, cx: &mut Context) { + self.show_line_numbers = Some(show_line_numbers); + cx.notify(); + } + + pub fn disable_expand_excerpt_buttons(&mut self, cx: &mut Context) { + self.disable_expand_excerpt_buttons = true; + cx.notify(); + } + + pub fn set_show_git_diff_gutter(&mut self, show_git_diff_gutter: bool, cx: &mut Context) { + self.show_git_diff_gutter = Some(show_git_diff_gutter); + cx.notify(); + } + + pub fn set_show_code_actions(&mut self, show_code_actions: bool, cx: &mut Context) { + self.show_code_actions = Some(show_code_actions); + cx.notify(); + } + + pub fn set_show_runnables(&mut self, show_runnables: bool, cx: &mut Context) { + self.show_runnables = Some(show_runnables); + cx.notify(); + } + + pub fn set_show_breakpoints(&mut self, show_breakpoints: bool, cx: &mut Context) { + self.show_breakpoints = Some(show_breakpoints); + cx.notify(); + } + + pub fn set_masked(&mut self, masked: bool, cx: &mut Context) { + if self.display_map.read(cx).masked != masked { + self.display_map.update(cx, |map, _| map.masked = masked); + } + cx.notify() + } + + pub fn set_show_wrap_guides(&mut self, show_wrap_guides: bool, cx: &mut Context) { + self.show_wrap_guides = Some(show_wrap_guides); + cx.notify(); + } + + pub fn set_show_indent_guides(&mut self, show_indent_guides: bool, cx: &mut Context) { + self.show_indent_guides = Some(show_indent_guides); + cx.notify(); + } + + pub fn working_directory(&self, cx: &App) -> Option { + if let Some(buffer) = self.buffer().read(cx).as_singleton() { + if let Some(file) = buffer.read(cx).file().and_then(|f| f.as_local()) { + if let Some(dir) = file.abs_path(cx).parent() { + return Some(dir.to_owned()); + } + } + + if let Some(project_path) = buffer.read(cx).project_path(cx) { + return Some(project_path.path.to_path_buf()); + } + } + + None + } + + fn target_file<'a>(&self, cx: &'a App) -> Option<&'a dyn language::LocalFile> { + self.active_excerpt(cx)? + .1 + .read(cx) + .file() + .and_then(|f| f.as_local()) + } + + pub fn target_file_abs_path(&self, cx: &mut Context) -> Option { + self.active_excerpt(cx).and_then(|(_, buffer, _)| { + let buffer = buffer.read(cx); + if let Some(project_path) = buffer.project_path(cx) { + let project = self.project.as_ref()?.read(cx); + project.absolute_path(&project_path, cx) + } else { + buffer + .file() + .and_then(|file| file.as_local().map(|file| file.abs_path(cx))) + } + }) + } + + fn target_file_path(&self, cx: &mut Context) -> Option { + self.active_excerpt(cx).and_then(|(_, buffer, _)| { + let project_path = buffer.read(cx).project_path(cx)?; + let project = self.project.as_ref()?.read(cx); + let entry = project.entry_for_path(&project_path, cx)?; + let path = entry.path.to_path_buf(); + Some(path) + }) + } + + pub fn reveal_in_finder( + &mut self, + _: &RevealInFileManager, + _window: &mut Window, + cx: &mut Context, + ) { + if let Some(target) = self.target_file(cx) { + cx.reveal_path(&target.abs_path(cx)); + } + } + + pub fn copy_path( + &mut self, + _: &zed_actions::workspace::CopyPath, + _window: &mut Window, + cx: &mut Context, + ) { + if let Some(path) = self.target_file_abs_path(cx) { + if let Some(path) = path.to_str() { + cx.write_to_clipboard(ClipboardItem::new_string(path.to_string())); + } + } + } + + pub fn copy_relative_path( + &mut self, + _: &zed_actions::workspace::CopyRelativePath, + _window: &mut Window, + cx: &mut Context, + ) { + if let Some(path) = self.target_file_path(cx) { + if let Some(path) = path.to_str() { + cx.write_to_clipboard(ClipboardItem::new_string(path.to_string())); + } + } + } + + pub fn project_path(&self, cx: &App) -> Option { + if let Some(buffer) = self.buffer.read(cx).as_singleton() { + buffer.read(cx).project_path(cx) + } else { + None + } + } + + // Returns true if the editor handled a go-to-line request + pub fn go_to_active_debug_line(&mut self, window: &mut Window, cx: &mut Context) -> bool { + maybe!({ + let breakpoint_store = self.breakpoint_store.as_ref()?; + + let Some(active_stack_frame) = breakpoint_store.read(cx).active_position().cloned() + else { + self.clear_row_highlights::(); + return None; + }; + + let position = active_stack_frame.position; + let buffer_id = position.buffer_id?; + let snapshot = self + .project + .as_ref()? + .read(cx) + .buffer_for_id(buffer_id, cx)? + .read(cx) + .snapshot(); + + let mut handled = false; + for (id, ExcerptRange { context, .. }) in + self.buffer.read(cx).excerpts_for_buffer(buffer_id, cx) + { + if context.start.cmp(&position, &snapshot).is_ge() + || context.end.cmp(&position, &snapshot).is_lt() + { + continue; + } + let snapshot = self.buffer.read(cx).snapshot(cx); + let multibuffer_anchor = snapshot.anchor_in_excerpt(id, position)?; + + handled = true; + self.clear_row_highlights::(); + self.go_to_line::( + multibuffer_anchor, + Some(cx.theme().colors().editor_debugger_active_line_background), + window, + cx, + ); + + cx.notify(); + } + + handled.then_some(()) + }) + .is_some() + } + + pub fn copy_file_name_without_extension( + &mut self, + _: &CopyFileNameWithoutExtension, + _: &mut Window, + cx: &mut Context, + ) { + if let Some(file) = self.target_file(cx) { + if let Some(file_stem) = file.path().file_stem() { + if let Some(name) = file_stem.to_str() { + cx.write_to_clipboard(ClipboardItem::new_string(name.to_string())); + } + } + } + } + + pub fn copy_file_name(&mut self, _: &CopyFileName, _: &mut Window, cx: &mut Context) { + if let Some(file) = self.target_file(cx) { + if let Some(file_name) = file.path().file_name() { + if let Some(name) = file_name.to_str() { + cx.write_to_clipboard(ClipboardItem::new_string(name.to_string())); + } + } + } + } + + pub fn toggle_git_blame( + &mut self, + _: &::git::Blame, + window: &mut Window, + cx: &mut Context, + ) { + self.show_git_blame_gutter = !self.show_git_blame_gutter; + + if self.show_git_blame_gutter && !self.has_blame_entries(cx) { + self.start_git_blame(true, window, cx); + } + + cx.notify(); + } + + pub fn toggle_git_blame_inline( + &mut self, + _: &ToggleGitBlameInline, + window: &mut Window, + cx: &mut Context, + ) { + self.toggle_git_blame_inline_internal(true, window, cx); + cx.notify(); + } + + pub fn open_git_blame_commit( + &mut self, + _: &OpenGitBlameCommit, + window: &mut Window, + cx: &mut Context, + ) { + self.open_git_blame_commit_internal(window, cx); + } + + fn open_git_blame_commit_internal( + &mut self, + window: &mut Window, + cx: &mut Context, + ) -> Option<()> { + let blame = self.blame.as_ref()?; + let snapshot = self.snapshot(window, cx); + let cursor = self.selections.newest::(cx).head(); + let (buffer, point, _) = snapshot.buffer_snapshot.point_to_buffer_point(cursor)?; + let blame_entry = blame + .update(cx, |blame, cx| { + blame + .blame_for_rows( + &[RowInfo { + buffer_id: Some(buffer.remote_id()), + buffer_row: Some(point.row), + ..Default::default() + }], + cx, + ) + .next() + }) + .flatten()?; + let renderer = cx.global::().0.clone(); + let repo = blame.read(cx).repository(cx)?; + let workspace = self.workspace()?.downgrade(); + renderer.open_blame_commit(blame_entry, repo, workspace, window, cx); + None + } + + pub fn git_blame_inline_enabled(&self) -> bool { + self.git_blame_inline_enabled + } + + pub fn toggle_selection_menu( + &mut self, + _: &ToggleSelectionMenu, + _: &mut Window, + cx: &mut Context, + ) { + self.show_selection_menu = self + .show_selection_menu + .map(|show_selections_menu| !show_selections_menu) + .or_else(|| Some(!EditorSettings::get_global(cx).toolbar.selections_menu)); + + cx.notify(); + } + + pub fn selection_menu_enabled(&self, cx: &App) -> bool { + self.show_selection_menu + .unwrap_or_else(|| EditorSettings::get_global(cx).toolbar.selections_menu) + } + + fn start_git_blame( + &mut self, + user_triggered: bool, + window: &mut Window, + cx: &mut Context, + ) { + if let Some(project) = self.project.as_ref() { + let Some(buffer) = self.buffer().read(cx).as_singleton() else { + return; + }; + + if buffer.read(cx).file().is_none() { + return; + } + + let focused = self.focus_handle(cx).contains_focused(window, cx); + + let project = project.clone(); + let blame = cx.new(|cx| GitBlame::new(buffer, project, user_triggered, focused, cx)); + self.blame_subscription = + Some(cx.observe_in(&blame, window, |_, _, _, cx| cx.notify())); + self.blame = Some(blame); + } + } + + fn toggle_git_blame_inline_internal( + &mut self, + user_triggered: bool, + window: &mut Window, + cx: &mut Context, + ) { + if self.git_blame_inline_enabled { + self.git_blame_inline_enabled = false; + self.show_git_blame_inline = false; + self.show_git_blame_inline_delay_task.take(); + } else { + self.git_blame_inline_enabled = true; + self.start_git_blame_inline(user_triggered, window, cx); + } + + cx.notify(); + } + + fn start_git_blame_inline( + &mut self, + user_triggered: bool, + window: &mut Window, + cx: &mut Context, + ) { + self.start_git_blame(user_triggered, window, cx); + + if ProjectSettings::get_global(cx) + .git + .inline_blame_delay() + .is_some() + { + self.start_inline_blame_timer(window, cx); + } else { + self.show_git_blame_inline = true + } + } + + pub fn blame(&self) -> Option<&Entity> { + self.blame.as_ref() + } + + pub fn show_git_blame_gutter(&self) -> bool { + self.show_git_blame_gutter + } + + pub fn render_git_blame_gutter(&self, cx: &App) -> bool { + self.show_git_blame_gutter && self.has_blame_entries(cx) + } + + pub fn render_git_blame_inline(&self, window: &Window, cx: &App) -> bool { + self.show_git_blame_inline + && (self.focus_handle.is_focused(window) || self.inline_blame_popover.is_some()) + && !self.newest_selection_head_on_empty_line(cx) + && self.has_blame_entries(cx) + } + + fn has_blame_entries(&self, cx: &App) -> bool { + self.blame() + .map_or(false, |blame| blame.read(cx).has_generated_entries()) + } + + fn newest_selection_head_on_empty_line(&self, cx: &App) -> bool { + let cursor_anchor = self.selections.newest_anchor().head(); + + let snapshot = self.buffer.read(cx).snapshot(cx); + let buffer_row = MultiBufferRow(cursor_anchor.to_point(&snapshot).row); + + snapshot.line_len(buffer_row) == 0 + } + + fn get_permalink_to_line(&self, cx: &mut Context) -> Task> { + let buffer_and_selection = maybe!({ + let selection = self.selections.newest::(cx); + let selection_range = selection.range(); + + let multi_buffer = self.buffer().read(cx); + let multi_buffer_snapshot = multi_buffer.snapshot(cx); + let buffer_ranges = multi_buffer_snapshot.range_to_buffer_ranges(selection_range); + + let (buffer, range, _) = if selection.reversed { + buffer_ranges.first() + } else { + buffer_ranges.last() + }?; + + let selection = text::ToPoint::to_point(&range.start, &buffer).row + ..text::ToPoint::to_point(&range.end, &buffer).row; + Some(( + multi_buffer.buffer(buffer.remote_id()).unwrap().clone(), + selection, + )) + }); + + let Some((buffer, selection)) = buffer_and_selection else { + return Task::ready(Err(anyhow!("failed to determine buffer and selection"))); + }; + + let Some(project) = self.project.as_ref() else { + return Task::ready(Err(anyhow!("editor does not have project"))); + }; + + project.update(cx, |project, cx| { + project.get_permalink_to_line(&buffer, selection, cx) + }) + } + + pub fn copy_permalink_to_line( + &mut self, + _: &CopyPermalinkToLine, + window: &mut Window, + cx: &mut Context, + ) { + let permalink_task = self.get_permalink_to_line(cx); + let workspace = self.workspace(); + + cx.spawn_in(window, async move |_, cx| match permalink_task.await { + Ok(permalink) => { + cx.update(|_, cx| { + cx.write_to_clipboard(ClipboardItem::new_string(permalink.to_string())); + }) + .ok(); + } + Err(err) => { + let message = format!("Failed to copy permalink: {err}"); + + Err::<(), anyhow::Error>(err).log_err(); + + if let Some(workspace) = workspace { + workspace + .update_in(cx, |workspace, _, cx| { + struct CopyPermalinkToLine; + + workspace.show_toast( + Toast::new( + NotificationId::unique::(), + message, + ), + cx, + ) + }) + .ok(); + } + } + }) + .detach(); + } + + pub fn copy_file_location( + &mut self, + _: &CopyFileLocation, + _: &mut Window, + cx: &mut Context, + ) { + let selection = self.selections.newest::(cx).start.row + 1; + if let Some(file) = self.target_file(cx) { + if let Some(path) = file.path().to_str() { + cx.write_to_clipboard(ClipboardItem::new_string(format!("{path}:{selection}"))); + } + } + } + + pub fn open_permalink_to_line( + &mut self, + _: &OpenPermalinkToLine, + window: &mut Window, + cx: &mut Context, + ) { + let permalink_task = self.get_permalink_to_line(cx); + let workspace = self.workspace(); + + cx.spawn_in(window, async move |_, cx| match permalink_task.await { + Ok(permalink) => { + cx.update(|_, cx| { + cx.open_url(permalink.as_ref()); + }) + .ok(); + } + Err(err) => { + let message = format!("Failed to open permalink: {err}"); + + Err::<(), anyhow::Error>(err).log_err(); + + if let Some(workspace) = workspace { + workspace + .update(cx, |workspace, cx| { + struct OpenPermalinkToLine; + + workspace.show_toast( + Toast::new( + NotificationId::unique::(), + message, + ), + cx, + ) + }) + .ok(); + } + } + }) + .detach(); + } + + pub fn insert_uuid_v4( + &mut self, + _: &InsertUuidV4, + window: &mut Window, + cx: &mut Context, + ) { + self.insert_uuid(UuidVersion::V4, window, cx); + } + + pub fn insert_uuid_v7( + &mut self, + _: &InsertUuidV7, + window: &mut Window, + cx: &mut Context, + ) { + self.insert_uuid(UuidVersion::V7, window, cx); + } + + fn insert_uuid(&mut self, version: UuidVersion, window: &mut Window, cx: &mut Context) { + self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + self.transact(window, cx, |this, window, cx| { + let edits = this + .selections + .all::(cx) + .into_iter() + .map(|selection| { + let uuid = match version { + UuidVersion::V4 => uuid::Uuid::new_v4(), + UuidVersion::V7 => uuid::Uuid::now_v7(), + }; + + (selection.range(), uuid.to_string()) + }); + this.edit(edits, cx); + this.refresh_inline_completion(true, false, window, cx); + }); + } + + pub fn open_selections_in_multibuffer( + &mut self, + _: &OpenSelectionsInMultibuffer, + window: &mut Window, + cx: &mut Context, + ) { + let multibuffer = self.buffer.read(cx); + + let Some(buffer) = multibuffer.as_singleton() else { + return; + }; + + let Some(workspace) = self.workspace() else { + return; + }; + + let locations = self + .selections + .disjoint_anchors() + .iter() + .map(|range| Location { + buffer: buffer.clone(), + range: range.start.text_anchor..range.end.text_anchor, + }) + .collect::>(); + + let title = multibuffer.title(cx).to_string(); + + cx.spawn_in(window, async move |_, cx| { + workspace.update_in(cx, |workspace, window, cx| { + Self::open_locations_in_multibuffer( + workspace, + locations, + format!("Selections for '{title}'"), + false, + MultibufferSelectionMode::All, + window, + cx, + ); + }) + }) + .detach(); + } + + /// Adds a row highlight for the given range. If a row has multiple highlights, the + /// last highlight added will be used. + /// + /// If the range ends at the beginning of a line, then that line will not be highlighted. + pub fn highlight_rows( + &mut self, + range: Range, + color: Hsla, + options: RowHighlightOptions, + cx: &mut Context, + ) { + let snapshot = self.buffer().read(cx).snapshot(cx); + let row_highlights = self.highlighted_rows.entry(TypeId::of::()).or_default(); + let ix = row_highlights.binary_search_by(|highlight| { + Ordering::Equal + .then_with(|| highlight.range.start.cmp(&range.start, &snapshot)) + .then_with(|| highlight.range.end.cmp(&range.end, &snapshot)) + }); + + if let Err(mut ix) = ix { + let index = post_inc(&mut self.highlight_order); + + // If this range intersects with the preceding highlight, then merge it with + // the preceding highlight. Otherwise insert a new highlight. + let mut merged = false; + if ix > 0 { + let prev_highlight = &mut row_highlights[ix - 1]; + if prev_highlight + .range + .end + .cmp(&range.start, &snapshot) + .is_ge() + { + ix -= 1; + if prev_highlight.range.end.cmp(&range.end, &snapshot).is_lt() { + prev_highlight.range.end = range.end; + } + merged = true; + prev_highlight.index = index; + prev_highlight.color = color; + prev_highlight.options = options; + } + } + + if !merged { + row_highlights.insert( + ix, + RowHighlight { + range: range.clone(), + index, + color, + options, + type_id: TypeId::of::(), + }, + ); + } + + // If any of the following highlights intersect with this one, merge them. + while let Some(next_highlight) = row_highlights.get(ix + 1) { + let highlight = &row_highlights[ix]; + if next_highlight + .range + .start + .cmp(&highlight.range.end, &snapshot) + .is_le() + { + if next_highlight + .range + .end + .cmp(&highlight.range.end, &snapshot) + .is_gt() + { + row_highlights[ix].range.end = next_highlight.range.end; + } + row_highlights.remove(ix + 1); + } else { + break; + } + } + } + } + + /// Remove any highlighted row ranges of the given type that intersect the + /// given ranges. + pub fn remove_highlighted_rows( + &mut self, + ranges_to_remove: Vec>, + cx: &mut Context, + ) { + let snapshot = self.buffer().read(cx).snapshot(cx); + let row_highlights = self.highlighted_rows.entry(TypeId::of::()).or_default(); + let mut ranges_to_remove = ranges_to_remove.iter().peekable(); + row_highlights.retain(|highlight| { + while let Some(range_to_remove) = ranges_to_remove.peek() { + match range_to_remove.end.cmp(&highlight.range.start, &snapshot) { + Ordering::Less | Ordering::Equal => { + ranges_to_remove.next(); + } + Ordering::Greater => { + match range_to_remove.start.cmp(&highlight.range.end, &snapshot) { + Ordering::Less | Ordering::Equal => { + return false; + } + Ordering::Greater => break, + } + } + } + } + + true + }) + } + + /// Clear all anchor ranges for a certain highlight context type, so no corresponding rows will be highlighted. + pub fn clear_row_highlights(&mut self) { + self.highlighted_rows.remove(&TypeId::of::()); + } + + /// For a highlight given context type, gets all anchor ranges that will be used for row highlighting. + pub fn highlighted_rows(&self) -> impl '_ + Iterator, Hsla)> { + self.highlighted_rows + .get(&TypeId::of::()) + .map_or(&[] as &[_], |vec| vec.as_slice()) + .iter() + .map(|highlight| (highlight.range.clone(), highlight.color)) + } + + /// Merges all anchor ranges for all context types ever set, picking the last highlight added in case of a row conflict. + /// Returns a map of display rows that are highlighted and their corresponding highlight color. + /// Allows to ignore certain kinds of highlights. + pub fn highlighted_display_rows( + &self, + window: &mut Window, + cx: &mut App, + ) -> BTreeMap { + let snapshot = self.snapshot(window, cx); + let mut used_highlight_orders = HashMap::default(); + self.highlighted_rows + .iter() + .flat_map(|(_, highlighted_rows)| highlighted_rows.iter()) + .fold( + BTreeMap::::new(), + |mut unique_rows, highlight| { + let start = highlight.range.start.to_display_point(&snapshot); + let end = highlight.range.end.to_display_point(&snapshot); + let start_row = start.row().0; + let end_row = if highlight.range.end.text_anchor != text::Anchor::MAX + && end.column() == 0 + { + end.row().0.saturating_sub(1) + } else { + end.row().0 + }; + for row in start_row..=end_row { + let used_index = + used_highlight_orders.entry(row).or_insert(highlight.index); + if highlight.index >= *used_index { + *used_index = highlight.index; + unique_rows.insert( + DisplayRow(row), + LineHighlight { + include_gutter: highlight.options.include_gutter, + border: None, + background: highlight.color.into(), + type_id: Some(highlight.type_id), + }, + ); + } + } + unique_rows + }, + ) + } + + pub fn highlighted_display_row_for_autoscroll( + &self, + snapshot: &DisplaySnapshot, + ) -> Option { + self.highlighted_rows + .values() + .flat_map(|highlighted_rows| highlighted_rows.iter()) + .filter_map(|highlight| { + if highlight.options.autoscroll { + Some(highlight.range.start.to_display_point(snapshot).row()) + } else { + None + } + }) + .min() + } + + pub fn set_search_within_ranges(&mut self, ranges: &[Range], cx: &mut Context) { + self.highlight_background::( + ranges, + |colors| colors.editor_document_highlight_read_background, + cx, + ) + } + + pub fn set_breadcrumb_header(&mut self, new_header: String) { + self.breadcrumb_header = Some(new_header); + } + + pub fn clear_search_within_ranges(&mut self, cx: &mut Context) { + self.clear_background_highlights::(cx); + } + + pub fn highlight_background( + &mut self, + ranges: &[Range], + color_fetcher: fn(&ThemeColors) -> Hsla, + cx: &mut Context, + ) { + self.background_highlights + .insert(TypeId::of::(), (color_fetcher, Arc::from(ranges))); + self.scrollbar_marker_state.dirty = true; + cx.notify(); + } + + pub fn clear_background_highlights( + &mut self, + cx: &mut Context, + ) -> Option { + let text_highlights = self.background_highlights.remove(&TypeId::of::())?; + if !text_highlights.1.is_empty() { + self.scrollbar_marker_state.dirty = true; + cx.notify(); + } + Some(text_highlights) + } + + pub fn highlight_gutter( + &mut self, + ranges: &[Range], + color_fetcher: fn(&App) -> Hsla, + cx: &mut Context, + ) { + self.gutter_highlights + .insert(TypeId::of::(), (color_fetcher, Arc::from(ranges))); + cx.notify(); + } + + pub fn clear_gutter_highlights( + &mut self, + cx: &mut Context, + ) -> Option { + cx.notify(); + self.gutter_highlights.remove(&TypeId::of::()) + } + + #[cfg(feature = "test-support")] + pub fn all_text_background_highlights( + &self, + window: &mut Window, + cx: &mut Context, + ) -> Vec<(Range, Hsla)> { + let snapshot = self.snapshot(window, cx); + let buffer = &snapshot.buffer_snapshot; + let start = buffer.anchor_before(0); + let end = buffer.anchor_after(buffer.len()); + let theme = cx.theme().colors(); + self.background_highlights_in_range(start..end, &snapshot, theme) + } + + #[cfg(feature = "test-support")] + pub fn search_background_highlights(&mut self, cx: &mut Context) -> Vec> { + let snapshot = self.buffer().read(cx).snapshot(cx); + + let highlights = self + .background_highlights + .get(&TypeId::of::()); + + if let Some((_color, ranges)) = highlights { + ranges + .iter() + .map(|range| range.start.to_point(&snapshot)..range.end.to_point(&snapshot)) + .collect_vec() + } else { + vec![] + } + } + + fn document_highlights_for_position<'a>( + &'a self, + position: Anchor, + buffer: &'a MultiBufferSnapshot, + ) -> impl 'a + Iterator> { + let read_highlights = self + .background_highlights + .get(&TypeId::of::()) + .map(|h| &h.1); + let write_highlights = self + .background_highlights + .get(&TypeId::of::()) + .map(|h| &h.1); + let left_position = position.bias_left(buffer); + let right_position = position.bias_right(buffer); + read_highlights + .into_iter() + .chain(write_highlights) + .flat_map(move |ranges| { + let start_ix = match ranges.binary_search_by(|probe| { + let cmp = probe.end.cmp(&left_position, buffer); + if cmp.is_ge() { + Ordering::Greater + } else { + Ordering::Less + } + }) { + Ok(i) | Err(i) => i, + }; + + ranges[start_ix..] + .iter() + .take_while(move |range| range.start.cmp(&right_position, buffer).is_le()) + }) + } + + pub fn has_background_highlights(&self) -> bool { + self.background_highlights + .get(&TypeId::of::()) + .map_or(false, |(_, highlights)| !highlights.is_empty()) + } + + pub fn background_highlights_in_range( + &self, + search_range: Range, + display_snapshot: &DisplaySnapshot, + theme: &ThemeColors, + ) -> Vec<(Range, 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 + .cmp(&search_range.start, &display_snapshot.buffer_snapshot); + if cmp.is_gt() { + Ordering::Greater + } else { + Ordering::Less + } + }) { + Ok(i) | Err(i) => i, + }; + for range in &ranges[start_ix..] { + if range + .start + .cmp(&search_range.end, &display_snapshot.buffer_snapshot) + .is_ge() + { + break; + } + + let start = range.start.to_display_point(display_snapshot); + let end = range.end.to_display_point(display_snapshot); + results.push((start..end, color)) + } + } + results + } + + pub fn background_highlight_row_ranges( + &self, + search_range: Range, + display_snapshot: &DisplaySnapshot, + count: usize, + ) -> Vec> { + let mut results = Vec::new(); + let Some((_, ranges)) = self.background_highlights.get(&TypeId::of::()) else { + return vec![]; + }; + + let start_ix = match ranges.binary_search_by(|probe| { + let cmp = probe + .end + .cmp(&search_range.start, &display_snapshot.buffer_snapshot); + if cmp.is_gt() { + Ordering::Greater + } else { + Ordering::Less + } + }) { + Ok(i) | Err(i) => i, + }; + let mut push_region = |start: Option, end: Option| { + if let (Some(start_display), Some(end_display)) = (start, end) { + results.push( + start_display.to_display_point(display_snapshot) + ..=end_display.to_display_point(display_snapshot), + ); + } + }; + let mut start_row: Option = None; + let mut end_row: Option = None; + if ranges.len() > count { + return Vec::new(); + } + for range in &ranges[start_ix..] { + if range + .start + .cmp(&search_range.end, &display_snapshot.buffer_snapshot) + .is_ge() + { + break; + } + let end = range.end.to_point(&display_snapshot.buffer_snapshot); + if let Some(current_row) = &end_row { + if end.row == current_row.row { + continue; + } + } + let start = range.start.to_point(&display_snapshot.buffer_snapshot); + if start_row.is_none() { + assert_eq!(end_row, None); + start_row = Some(start); + end_row = Some(end); + continue; + } + if let Some(current_end) = end_row.as_mut() { + if start.row > current_end.row + 1 { + push_region(start_row, end_row); + start_row = Some(start); + end_row = Some(end); + } else { + // Merge two hunks. + *current_end = end; + } + } else { + unreachable!(); + } + } + // We might still have a hunk that was not rendered (if there was a search hit on the last line) + push_region(start_row, end_row); + results + } + + pub fn gutter_highlights_in_range( + &self, + search_range: Range, + display_snapshot: &DisplaySnapshot, + cx: &App, + ) -> Vec<(Range, Hsla)> { + let mut results = Vec::new(); + for (color_fetcher, ranges) in self.gutter_highlights.values() { + let color = color_fetcher(cx); + let start_ix = match ranges.binary_search_by(|probe| { + let cmp = probe + .end + .cmp(&search_range.start, &display_snapshot.buffer_snapshot); + if cmp.is_gt() { + Ordering::Greater + } else { + Ordering::Less + } + }) { + Ok(i) | Err(i) => i, + }; + for range in &ranges[start_ix..] { + if range + .start + .cmp(&search_range.end, &display_snapshot.buffer_snapshot) + .is_ge() + { + break; + } + + let start = range.start.to_display_point(display_snapshot); + let end = range.end.to_display_point(display_snapshot); + results.push((start..end, color)) + } + } + results + } + + /// Get the text ranges corresponding to the redaction query + pub fn redacted_ranges( + &self, + search_range: Range, + display_snapshot: &DisplaySnapshot, + cx: &App, + ) -> Vec> { + display_snapshot + .buffer_snapshot + .redacted_ranges(search_range, |file| { + if let Some(file) = file { + file.is_private() + && EditorSettings::get( + Some(SettingsLocation { + worktree_id: file.worktree_id(cx), + path: file.path().as_ref(), + }), + cx, + ) + .redact_private_values + } else { + false + } + }) + .map(|range| { + range.start.to_display_point(display_snapshot) + ..range.end.to_display_point(display_snapshot) + }) + .collect() + } + + pub fn highlight_text( + &mut self, + ranges: Vec>, + style: HighlightStyle, + cx: &mut Context, + ) { + self.display_map.update(cx, |map, _| { + map.highlight_text(TypeId::of::(), ranges, style) + }); + cx.notify(); + } + + pub(crate) fn highlight_inlays( + &mut self, + highlights: Vec, + style: HighlightStyle, + cx: &mut Context, + ) { + self.display_map.update(cx, |map, _| { + map.highlight_inlays(TypeId::of::(), highlights, style) + }); + cx.notify(); + } + + pub fn text_highlights<'a, T: 'static>( + &'a self, + cx: &'a App, + ) -> Option<(HighlightStyle, &'a [Range])> { + self.display_map.read(cx).text_highlights(TypeId::of::()) + } + + pub fn clear_highlights(&mut self, cx: &mut Context) { + let cleared = self + .display_map + .update(cx, |map, _| map.clear_highlights(TypeId::of::())); + if cleared { + cx.notify(); + } + } + + pub fn show_local_cursors(&self, window: &mut Window, cx: &mut App) -> bool { + (self.read_only(cx) || self.blink_manager.read(cx).visible()) + && self.focus_handle.is_focused(window) + } + + pub fn set_show_cursor_when_unfocused(&mut self, is_enabled: bool, cx: &mut Context) { + self.show_cursor_when_unfocused = is_enabled; + cx.notify(); + } + + fn on_buffer_changed(&mut self, _: Entity, cx: &mut Context) { + cx.notify(); + } + + fn on_debug_session_event( + &mut self, + _session: Entity, + event: &SessionEvent, + cx: &mut Context, + ) { + match event { + SessionEvent::InvalidateInlineValue => { + self.refresh_inline_values(cx); + } + _ => {} + } + } + + fn refresh_inline_values(&mut self, cx: &mut Context) { + let Some(project) = self.project.clone() else { + return; + }; + let Some(buffer) = self.buffer.read(cx).as_singleton() else { + return; + }; + if !self.inline_value_cache.enabled { + let inlays = std::mem::take(&mut self.inline_value_cache.inlays); + self.splice_inlays(&inlays, Vec::new(), cx); + return; + } + + let current_execution_position = self + .highlighted_rows + .get(&TypeId::of::()) + .and_then(|lines| lines.last().map(|line| line.range.start)); + + self.inline_value_cache.refresh_task = cx.spawn(async move |editor, cx| { + let snapshot = editor + .update(cx, |editor, cx| editor.buffer().read(cx).snapshot(cx)) + .ok()?; + + let inline_values = editor + .update(cx, |_, cx| { + let Some(current_execution_position) = current_execution_position else { + return Some(Task::ready(Ok(Vec::new()))); + }; + + // todo(debugger) when introducing multi buffer inline values check execution position's buffer id to make sure the text + // anchor is in the same buffer + let range = + buffer.read(cx).anchor_before(0)..current_execution_position.text_anchor; + project.inline_values(buffer, range, cx) + }) + .ok() + .flatten()? + .await + .context("refreshing debugger inlays") + .log_err()?; + + let (excerpt_id, buffer_id) = snapshot + .excerpts() + .next() + .map(|excerpt| (excerpt.0, excerpt.1.remote_id()))?; + editor + .update(cx, |editor, cx| { + let new_inlays = inline_values + .into_iter() + .map(|debugger_value| { + Inlay::debugger_hint( + post_inc(&mut editor.next_inlay_id), + Anchor::in_buffer(excerpt_id, buffer_id, debugger_value.position), + debugger_value.text(), + ) + }) + .collect::>(); + let mut inlay_ids = new_inlays.iter().map(|inlay| inlay.id).collect(); + std::mem::swap(&mut editor.inline_value_cache.inlays, &mut inlay_ids); + + editor.splice_inlays(&inlay_ids, new_inlays, cx); + }) + .ok()?; + Some(()) + }); + } + + fn on_buffer_event( + &mut self, + multibuffer: &Entity, + event: &multi_buffer::Event, + window: &mut Window, + cx: &mut Context, + ) { + match event { + multi_buffer::Event::Edited { + singleton_buffer_edited, + edited_buffer: buffer_edited, + } => { + self.scrollbar_marker_state.dirty = true; + self.active_indent_guides_state.dirty = true; + self.refresh_active_diagnostics(cx); + self.refresh_code_actions(window, cx); + self.refresh_selected_text_highlights(true, window, cx); + refresh_matching_bracket_highlights(self, window, cx); + if self.has_active_inline_completion() { + self.update_visible_inline_completion(window, cx); + } + if let Some(buffer) = buffer_edited { + let buffer_id = buffer.read(cx).remote_id(); + if !self.registered_buffers.contains_key(&buffer_id) { + if let Some(project) = self.project.as_ref() { + project.update(cx, |project, cx| { + self.registered_buffers.insert( + buffer_id, + project.register_buffer_with_language_servers(&buffer, cx), + ); + }) + } + } + } + cx.emit(EditorEvent::BufferEdited); + cx.emit(SearchEvent::MatchesInvalidated); + if *singleton_buffer_edited { + if let Some(project) = &self.project { + #[allow(clippy::mutable_key_type)] + let languages_affected = multibuffer.update(cx, |multibuffer, cx| { + multibuffer + .all_buffers() + .into_iter() + .filter_map(|buffer| { + buffer.update(cx, |buffer, cx| { + let language = buffer.language()?; + let should_discard = project.update(cx, |project, cx| { + project.is_local() + && !project.has_language_servers_for(buffer, cx) + }); + should_discard.not().then_some(language.clone()) + }) + }) + .collect::>() + }); + if !languages_affected.is_empty() { + self.refresh_inlay_hints( + InlayHintRefreshReason::BufferEdited(languages_affected), + cx, + ); + } + } + } + + let Some(project) = &self.project else { return }; + let (telemetry, is_via_ssh) = { + let project = project.read(cx); + let telemetry = project.client().telemetry().clone(); + let is_via_ssh = project.is_via_ssh(); + (telemetry, is_via_ssh) + }; + refresh_linked_ranges(self, window, cx); + telemetry.log_edit_event("editor", is_via_ssh); + } + multi_buffer::Event::ExcerptsAdded { + buffer, + predecessor, + excerpts, + } => { + self.tasks_update_task = Some(self.refresh_runnables(window, cx)); + let buffer_id = buffer.read(cx).remote_id(); + if self.buffer.read(cx).diff_for(buffer_id).is_none() { + if let Some(project) = &self.project { + get_uncommitted_diff_for_buffer( + project, + [buffer.clone()], + self.buffer.clone(), + cx, + ) + .detach(); + } + } + cx.emit(EditorEvent::ExcerptsAdded { + buffer: buffer.clone(), + predecessor: *predecessor, + excerpts: excerpts.clone(), + }); + self.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx); + } + multi_buffer::Event::ExcerptsRemoved { + ids, + removed_buffer_ids, + } => { + self.refresh_inlay_hints(InlayHintRefreshReason::ExcerptsRemoved(ids.clone()), cx); + let buffer = self.buffer.read(cx); + self.registered_buffers + .retain(|buffer_id, _| buffer.buffer(*buffer_id).is_some()); + jsx_tag_auto_close::refresh_enabled_in_any_buffer(self, multibuffer, cx); + cx.emit(EditorEvent::ExcerptsRemoved { + ids: ids.clone(), + removed_buffer_ids: removed_buffer_ids.clone(), + }) + } + multi_buffer::Event::ExcerptsEdited { + excerpt_ids, + buffer_ids, + } => { + self.display_map.update(cx, |map, cx| { + map.unfold_buffers(buffer_ids.iter().copied(), cx) + }); + cx.emit(EditorEvent::ExcerptsEdited { + ids: excerpt_ids.clone(), + }) + } + multi_buffer::Event::ExcerptsExpanded { ids } => { + self.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx); + cx.emit(EditorEvent::ExcerptsExpanded { ids: ids.clone() }) + } + multi_buffer::Event::Reparsed(buffer_id) => { + self.tasks_update_task = Some(self.refresh_runnables(window, cx)); + jsx_tag_auto_close::refresh_enabled_in_any_buffer(self, multibuffer, cx); + + cx.emit(EditorEvent::Reparsed(*buffer_id)); + } + multi_buffer::Event::DiffHunksToggled => { + self.tasks_update_task = Some(self.refresh_runnables(window, cx)); + } + multi_buffer::Event::LanguageChanged(buffer_id) => { + linked_editing_ranges::refresh_linked_ranges(self, window, cx); + jsx_tag_auto_close::refresh_enabled_in_any_buffer(self, multibuffer, cx); + cx.emit(EditorEvent::Reparsed(*buffer_id)); + cx.notify(); + } + multi_buffer::Event::DirtyChanged => cx.emit(EditorEvent::DirtyChanged), + multi_buffer::Event::Saved => cx.emit(EditorEvent::Saved), + multi_buffer::Event::FileHandleChanged + | multi_buffer::Event::Reloaded + | multi_buffer::Event::BufferDiffChanged => cx.emit(EditorEvent::TitleChanged), + multi_buffer::Event::Closed => cx.emit(EditorEvent::Closed), + multi_buffer::Event::DiagnosticsUpdated => { + self.refresh_active_diagnostics(cx); + self.refresh_inline_diagnostics(true, window, cx); + self.scrollbar_marker_state.dirty = true; + cx.notify(); + } + _ => {} + }; + } + + fn on_display_map_changed( + &mut self, + _: Entity, + _: &mut Window, + cx: &mut Context, + ) { + cx.notify(); + } + + fn settings_changed(&mut self, window: &mut Window, cx: &mut Context) { + self.tasks_update_task = Some(self.refresh_runnables(window, cx)); + self.update_edit_prediction_settings(cx); + self.refresh_inline_completion(true, false, window, cx); + self.refresh_inlay_hints( + InlayHintRefreshReason::SettingsChange(inlay_hint_settings( + self.selections.newest_anchor().head(), + &self.buffer.read(cx).snapshot(cx), + cx, + )), + cx, + ); + + let old_cursor_shape = self.cursor_shape; + + { + let editor_settings = EditorSettings::get_global(cx); + self.scroll_manager.vertical_scroll_margin = editor_settings.vertical_scroll_margin; + self.show_breadcrumbs = editor_settings.toolbar.breadcrumbs; + self.cursor_shape = editor_settings.cursor_shape.unwrap_or_default(); + self.hide_mouse_mode = editor_settings.hide_mouse.unwrap_or_default(); + } + + if old_cursor_shape != self.cursor_shape { + cx.emit(EditorEvent::CursorShapeChanged); + } + + let project_settings = ProjectSettings::get_global(cx); + self.serialize_dirty_buffers = project_settings.session.restore_unsaved_buffers; + + if self.mode.is_full() { + let show_inline_diagnostics = project_settings.diagnostics.inline.enabled; + let inline_blame_enabled = project_settings.git.inline_blame_enabled(); + if self.show_inline_diagnostics != show_inline_diagnostics { + self.show_inline_diagnostics = show_inline_diagnostics; + self.refresh_inline_diagnostics(false, window, cx); + } + + if self.git_blame_inline_enabled != inline_blame_enabled { + self.toggle_git_blame_inline_internal(false, window, cx); + } + } + + cx.notify(); + } + + pub fn set_searchable(&mut self, searchable: bool) { + self.searchable = searchable; + } + + pub fn searchable(&self) -> bool { + self.searchable + } + + fn open_proposed_changes_editor( + &mut self, + _: &OpenProposedChangesEditor, + window: &mut Window, + cx: &mut Context, + ) { + let Some(workspace) = self.workspace() else { + cx.propagate(); + return; + }; + + let selections = self.selections.all::(cx); + let multi_buffer = self.buffer.read(cx); + let multi_buffer_snapshot = multi_buffer.snapshot(cx); + let mut new_selections_by_buffer = HashMap::default(); + for selection in selections { + for (buffer, range, _) in + multi_buffer_snapshot.range_to_buffer_ranges(selection.start..selection.end) + { + let mut range = range.to_point(buffer); + range.start.column = 0; + range.end.column = buffer.line_len(range.end.row); + new_selections_by_buffer + .entry(multi_buffer.buffer(buffer.remote_id()).unwrap()) + .or_insert(Vec::new()) + .push(range) + } + } + + let proposed_changes_buffers = new_selections_by_buffer + .into_iter() + .map(|(buffer, ranges)| ProposedChangeLocation { buffer, ranges }) + .collect::>(); + let proposed_changes_editor = cx.new(|cx| { + ProposedChangesEditor::new( + "Proposed changes", + proposed_changes_buffers, + self.project.clone(), + window, + cx, + ) + }); + + window.defer(cx, move |window, cx| { + workspace.update(cx, |workspace, cx| { + workspace.active_pane().update(cx, |pane, cx| { + pane.add_item( + Box::new(proposed_changes_editor), + true, + true, + None, + window, + cx, + ); + }); + }); + }); + } + + pub fn open_excerpts_in_split( + &mut self, + _: &OpenExcerptsSplit, + window: &mut Window, + cx: &mut Context, + ) { + self.open_excerpts_common(None, true, window, cx) + } + + pub fn open_excerpts(&mut self, _: &OpenExcerpts, window: &mut Window, cx: &mut Context) { + self.open_excerpts_common(None, false, window, cx) + } + + fn open_excerpts_common( + &mut self, + jump_data: Option, + split: bool, + window: &mut Window, + cx: &mut Context, + ) { + let Some(workspace) = self.workspace() else { + cx.propagate(); + return; + }; + + if self.buffer.read(cx).is_singleton() { + cx.propagate(); + return; + } + + let mut new_selections_by_buffer = HashMap::default(); + match &jump_data { + Some(JumpData::MultiBufferPoint { + excerpt_id, + position, + anchor, + line_offset_from_top, + }) => { + let multi_buffer_snapshot = self.buffer.read(cx).snapshot(cx); + if let Some(buffer) = multi_buffer_snapshot + .buffer_id_for_excerpt(*excerpt_id) + .and_then(|buffer_id| self.buffer.read(cx).buffer(buffer_id)) + { + let buffer_snapshot = buffer.read(cx).snapshot(); + let jump_to_point = if buffer_snapshot.can_resolve(anchor) { + language::ToPoint::to_point(anchor, &buffer_snapshot) + } else { + buffer_snapshot.clip_point(*position, Bias::Left) + }; + let jump_to_offset = buffer_snapshot.point_to_offset(jump_to_point); + new_selections_by_buffer.insert( + buffer, + ( + vec![jump_to_offset..jump_to_offset], + Some(*line_offset_from_top), + ), + ); + } + } + Some(JumpData::MultiBufferRow { + row, + line_offset_from_top, + }) => { + let point = MultiBufferPoint::new(row.0, 0); + if let Some((buffer, buffer_point, _)) = + self.buffer.read(cx).point_to_buffer_point(point, cx) + { + let buffer_offset = buffer.read(cx).point_to_offset(buffer_point); + new_selections_by_buffer + .entry(buffer) + .or_insert((Vec::new(), Some(*line_offset_from_top))) + .0 + .push(buffer_offset..buffer_offset) + } + } + None => { + let selections = self.selections.all::(cx); + let multi_buffer = self.buffer.read(cx); + for selection in selections { + for (snapshot, range, _, anchor) in multi_buffer + .snapshot(cx) + .range_to_buffer_ranges_with_deleted_hunks(selection.range()) + { + if let Some(anchor) = anchor { + // selection is in a deleted hunk + let Some(buffer_id) = anchor.buffer_id else { + continue; + }; + let Some(buffer_handle) = multi_buffer.buffer(buffer_id) else { + continue; + }; + let offset = text::ToOffset::to_offset( + &anchor.text_anchor, + &buffer_handle.read(cx).snapshot(), + ); + let range = offset..offset; + new_selections_by_buffer + .entry(buffer_handle) + .or_insert((Vec::new(), None)) + .0 + .push(range) + } else { + let Some(buffer_handle) = multi_buffer.buffer(snapshot.remote_id()) + else { + continue; + }; + new_selections_by_buffer + .entry(buffer_handle) + .or_insert((Vec::new(), None)) + .0 + .push(range) + } + } + } + } + } + + new_selections_by_buffer + .retain(|buffer, _| Self::can_open_excerpts_in_file(buffer.read(cx).file())); + + if new_selections_by_buffer.is_empty() { + return; + } + + // We defer the pane interaction because we ourselves are a workspace item + // and activating a new item causes the pane to call a method on us reentrantly, + // which panics if we're on the stack. + window.defer(cx, move |window, cx| { + workspace.update(cx, |workspace, cx| { + let pane = if split { + workspace.adjacent_pane(window, cx) + } else { + workspace.active_pane().clone() + }; + + for (buffer, (ranges, scroll_offset)) in new_selections_by_buffer { + let editor = buffer + .read(cx) + .file() + .is_none() + .then(|| { + // 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) = + pane.read(cx).items().enumerate().find_map(|(i, item)| { + let editor = item.downcast::()?; + let singleton_buffer = + editor.read(cx).buffer().read(cx).as_singleton()?; + if singleton_buffer == buffer { + Some((editor, i)) + } else { + None + } + })?; + pane.update(cx, |pane, cx| { + pane.activate_item(pane_item_index, true, true, window, cx) + }); + Some(editor) + }) + .flatten() + .unwrap_or_else(|| { + workspace.open_project_item::( + pane.clone(), + buffer, + true, + true, + window, + cx, + ) + }); + + editor.update(cx, |editor, cx| { + let autoscroll = match scroll_offset { + Some(scroll_offset) => Autoscroll::top_relative(scroll_offset as usize), + None => Autoscroll::newest(), + }; + let nav_history = editor.nav_history.take(); + editor.change_selections(Some(autoscroll), window, cx, |s| { + s.select_ranges(ranges); + }); + editor.nav_history = nav_history; + }); + } + }) + }); + } + + // For now, don't allow opening excerpts in buffers that aren't backed by + // regular project files. + fn can_open_excerpts_in_file(file: Option<&Arc>) -> bool { + file.map_or(true, |file| project::File::from_dyn(Some(file)).is_some()) + } + + fn marked_text_ranges(&self, cx: &App) -> Option>> { + let snapshot = self.buffer.read(cx).read(cx); + let (_, ranges) = self.text_highlights::(cx)?; + Some( + ranges + .iter() + .map(move |range| { + range.start.to_offset_utf16(&snapshot)..range.end.to_offset_utf16(&snapshot) + }) + .collect(), + ) + } + + fn selection_replacement_ranges( + &self, + range: Range, + cx: &mut App, + ) -> Vec> { + let selections = self.selections.all::(cx); + let newest_selection = selections + .iter() + .max_by_key(|selection| selection.id) + .unwrap(); + let start_delta = range.start.0 as isize - newest_selection.start.0 as isize; + let end_delta = range.end.0 as isize - newest_selection.end.0 as isize; + let snapshot = self.buffer.read(cx).read(cx); + selections + .into_iter() + .map(|mut selection| { + selection.start.0 = + (selection.start.0 as isize).saturating_add(start_delta) as usize; + selection.end.0 = (selection.end.0 as isize).saturating_add(end_delta) as usize; + snapshot.clip_offset_utf16(selection.start, Bias::Left) + ..snapshot.clip_offset_utf16(selection.end, Bias::Right) + }) + .collect() + } + + fn report_editor_event( + &self, + event_type: &'static str, + file_extension: Option, + cx: &App, + ) { + if cfg!(any(test, feature = "test-support")) { + return; + } + + let Some(project) = &self.project else { return }; + + // If None, we are in a file without an extension + let file = self + .buffer + .read(cx) + .as_singleton() + .and_then(|b| b.read(cx).file()); + let file_extension = file_extension.or(file + .as_ref() + .and_then(|file| Path::new(file.file_name(cx)).extension()) + .and_then(|e| e.to_str()) + .map(|a| a.to_string())); + + let vim_mode = vim_enabled(cx); + + let edit_predictions_provider = all_language_settings(file, cx).edit_predictions.provider; + let copilot_enabled = edit_predictions_provider + == language::language_settings::EditPredictionProvider::Copilot; + let copilot_enabled_for_language = self + .buffer + .read(cx) + .language_settings(cx) + .show_edit_predictions; + + let project = project.read(cx); + telemetry::event!( + event_type, + file_extension, + vim_mode, + copilot_enabled, + copilot_enabled_for_language, + edit_predictions_provider, + is_via_ssh = project.is_via_ssh(), + ); + } + + /// Copy the highlighted chunks to the clipboard as JSON. The format is an array of lines, + /// with each line being an array of {text, highlight} objects. + fn copy_highlight_json( + &mut self, + _: &CopyHighlightJson, + window: &mut Window, + cx: &mut Context, + ) { + #[derive(Serialize)] + struct Chunk<'a> { + text: String, + highlight: Option<&'a str>, + } + + let snapshot = self.buffer.read(cx).snapshot(cx); + let range = self + .selected_text_range(false, window, cx) + .and_then(|selection| { + if selection.range.is_empty() { + None + } else { + Some(selection.range) + } + }) + .unwrap_or_else(|| 0..snapshot.len()); + + let chunks = snapshot.chunks(range, true); + let mut lines = Vec::new(); + let mut line: VecDeque = VecDeque::new(); + + let Some(style) = self.style.as_ref() else { + return; + }; + + for chunk in chunks { + let highlight = chunk + .syntax_highlight_id + .and_then(|id| id.name(&style.syntax)); + let mut chunk_lines = chunk.text.split('\n').peekable(); + while let Some(text) = chunk_lines.next() { + let mut merged_with_last_token = false; + if let Some(last_token) = line.back_mut() { + if last_token.highlight == highlight { + last_token.text.push_str(text); + merged_with_last_token = true; + } + } + + if !merged_with_last_token { + line.push_back(Chunk { + text: text.into(), + highlight, + }); + } + + if chunk_lines.peek().is_some() { + if line.len() > 1 && line.front().unwrap().text.is_empty() { + line.pop_front(); + } + if line.len() > 1 && line.back().unwrap().text.is_empty() { + line.pop_back(); + } + + lines.push(mem::take(&mut line)); + } + } + } + + let Some(lines) = serde_json::to_string_pretty(&lines).log_err() else { + return; + }; + cx.write_to_clipboard(ClipboardItem::new_string(lines)); + } + + pub fn open_context_menu( + &mut self, + _: &OpenContextMenu, + window: &mut Window, + cx: &mut Context, + ) { + self.request_autoscroll(Autoscroll::newest(), cx); + let position = self.selections.newest_display(cx).start; + mouse_context_menu::deploy_context_menu(self, None, position, window, cx); + } + + pub fn inlay_hint_cache(&self) -> &InlayHintCache { + &self.inlay_hint_cache + } + + pub fn replay_insert_event( + &mut self, + text: &str, + relative_utf16_range: Option>, + window: &mut Window, + cx: &mut Context, + ) { + if !self.input_enabled { + cx.emit(EditorEvent::InputIgnored { text: text.into() }); + return; + } + if let Some(relative_utf16_range) = relative_utf16_range { + let selections = self.selections.all::(cx); + self.change_selections(None, window, cx, |s| { + let new_ranges = selections.into_iter().map(|range| { + let start = OffsetUtf16( + range + .head() + .0 + .saturating_add_signed(relative_utf16_range.start), + ); + let end = OffsetUtf16( + range + .head() + .0 + .saturating_add_signed(relative_utf16_range.end), + ); + start..end + }); + s.select_ranges(new_ranges); + }); + } + + self.handle_input(text, window, cx); + } + + pub fn supports_inlay_hints(&self, cx: &mut App) -> bool { + let Some(provider) = self.semantics_provider.as_ref() else { + return false; + }; + + let mut supports = false; + self.buffer().update(cx, |this, cx| { + this.for_each_buffer(|buffer| { + supports |= provider.supports_inlay_hints(buffer, cx); + }); + }); + + supports + } + + pub fn is_focused(&self, window: &Window) -> bool { + self.focus_handle.is_focused(window) + } + + fn handle_focus(&mut self, window: &mut Window, cx: &mut Context) { + cx.emit(EditorEvent::Focused); + + if let Some(descendant) = self + .last_focused_descendant + .take() + .and_then(|descendant| descendant.upgrade()) + { + window.focus(&descendant); + } else { + if let Some(blame) = self.blame.as_ref() { + blame.update(cx, GitBlame::focus) + } + + self.blink_manager.update(cx, |blink_manager, cx| { + blink_manager.enable(cx); + }); + self.show_cursor_names(window, cx); + self.buffer.update(cx, |buffer, cx| { + buffer.finalize_last_transaction(cx); + if self.leader_peer_id.is_none() { + buffer.set_active_selections( + &self.selections.disjoint_anchors(), + self.selections.line_mode, + self.cursor_shape, + cx, + ); + } + }); + } + } + + fn handle_focus_in(&mut self, _: &mut Window, cx: &mut Context) { + cx.emit(EditorEvent::FocusedIn) + } + + fn handle_focus_out( + &mut self, + event: FocusOutEvent, + _window: &mut Window, + cx: &mut Context, + ) { + if event.blurred != self.focus_handle { + self.last_focused_descendant = Some(event.blurred); + } + self.refresh_inlay_hints(InlayHintRefreshReason::ModifiersChanged(false), cx); + } + + pub fn handle_blur(&mut self, window: &mut Window, cx: &mut Context) { + self.blink_manager.update(cx, BlinkManager::disable); + self.buffer + .update(cx, |buffer, cx| buffer.remove_active_selections(cx)); + + if let Some(blame) = self.blame.as_ref() { + blame.update(cx, GitBlame::blur) + } + if !self.hover_state.focused(window, cx) { + hide_hover(self, cx); + } + if !self + .context_menu + .borrow() + .as_ref() + .is_some_and(|context_menu| context_menu.focused(window, cx)) + { + self.hide_context_menu(window, cx); + } + self.discard_inline_completion(false, cx); + cx.emit(EditorEvent::Blurred); + cx.notify(); + } + + pub fn register_action( + &mut self, + listener: impl Fn(&A, &mut Window, &mut App) + 'static, + ) -> Subscription { + let id = self.next_editor_action_id.post_inc(); + let listener = Arc::new(listener); + self.editor_actions.borrow_mut().insert( + id, + Box::new(move |window, _| { + let listener = listener.clone(); + window.on_action(TypeId::of::(), move |action, phase, window, cx| { + let action = action.downcast_ref().unwrap(); + if phase == DispatchPhase::Bubble { + listener(action, window, cx) + } + }) + }), + ); + + let editor_actions = self.editor_actions.clone(); + Subscription::new(move || { + editor_actions.borrow_mut().remove(&id); + }) + } + + pub fn file_header_size(&self) -> u32 { + FILE_HEADER_HEIGHT + } + + pub fn restore( + &mut self, + revert_changes: HashMap, Rope)>>, + window: &mut Window, + cx: &mut Context, + ) { + let workspace = self.workspace(); + let project = self.project.as_ref(); + let save_tasks = self.buffer().update(cx, |multi_buffer, cx| { + let mut tasks = Vec::new(); + for (buffer_id, changes) in revert_changes { + if let Some(buffer) = multi_buffer.buffer(buffer_id) { + buffer.update(cx, |buffer, cx| { + buffer.edit( + changes + .into_iter() + .map(|(range, text)| (range, text.to_string())), + None, + cx, + ); + }); + + if let Some(project) = + project.filter(|_| multi_buffer.all_diff_hunks_expanded()) + { + project.update(cx, |project, cx| { + tasks.push((buffer.clone(), project.save_buffer(buffer, cx))); + }) + } + } + } + tasks + }); + cx.spawn_in(window, async move |_, cx| { + for (buffer, task) in save_tasks { + let result = task.await; + if result.is_err() { + let Some(path) = buffer + .read_with(cx, |buffer, cx| buffer.project_path(cx)) + .ok() + else { + continue; + }; + if let Some((workspace, path)) = workspace.as_ref().zip(path) { + let Some(task) = cx + .update_window_entity(&workspace, |workspace, window, cx| { + workspace + .open_path_preview(path, None, false, false, false, window, cx) + }) + .ok() + else { + continue; + }; + task.await.log_err(); + } + } + } + }) + .detach(); + self.change_selections(None, window, cx, |selections| selections.refresh()); + } + + pub fn to_pixel_point( + &self, + source: multi_buffer::Anchor, + editor_snapshot: &EditorSnapshot, + window: &mut Window, + ) -> Option> { + let source_point = source.to_display_point(editor_snapshot); + self.display_to_pixel_point(source_point, editor_snapshot, window) + } + + pub fn display_to_pixel_point( + &self, + source: DisplayPoint, + editor_snapshot: &EditorSnapshot, + window: &mut Window, + ) -> Option> { + let line_height = self.style()?.text.line_height_in_pixels(window.rem_size()); + let text_layout_details = self.text_layout_details(window); + let scroll_top = text_layout_details + .scroll_anchor + .scroll_position(editor_snapshot) + .y; + + if source.row().as_f32() < scroll_top.floor() { + return None; + } + let source_x = editor_snapshot.x_for_display_point(source, &text_layout_details); + let source_y = line_height * (source.row().as_f32() - scroll_top); + Some(gpui::Point::new(source_x, source_y)) + } + + pub fn has_visible_completions_menu(&self) -> bool { + !self.edit_prediction_preview_is_active() + && self.context_menu.borrow().as_ref().map_or(false, |menu| { + menu.visible() && matches!(menu, CodeContextMenu::Completions(_)) + }) + } + + pub fn register_addon(&mut self, instance: T) { + self.addons + .insert(std::any::TypeId::of::(), Box::new(instance)); + } + + pub fn unregister_addon(&mut self) { + self.addons.remove(&std::any::TypeId::of::()); + } + + pub fn addon(&self) -> Option<&T> { + let type_id = std::any::TypeId::of::(); + self.addons + .get(&type_id) + .and_then(|item| item.to_any().downcast_ref::()) + } + + pub fn addon_mut(&mut self) -> Option<&mut T> { + let type_id = std::any::TypeId::of::(); + self.addons + .get_mut(&type_id) + .and_then(|item| item.to_any_mut()?.downcast_mut::()) + } + + fn character_size(&self, window: &mut Window) -> gpui::Size { + let text_layout_details = self.text_layout_details(window); + let style = &text_layout_details.editor_style; + let font_id = window.text_system().resolve_font(&style.text.font()); + let font_size = style.text.font_size.to_pixels(window.rem_size()); + let line_height = style.text.line_height_in_pixels(window.rem_size()); + let em_width = window.text_system().em_width(font_id, font_size).unwrap(); + + gpui::Size::new(em_width, line_height) + } + + pub fn wait_for_diff_to_load(&self) -> Option>> { + self.load_diff_task.clone() + } + + fn read_metadata_from_db( + &mut self, + item_id: u64, + workspace_id: WorkspaceId, + window: &mut Window, + cx: &mut Context, + ) { + if self.is_singleton(cx) + && WorkspaceSettings::get(None, cx).restore_on_startup != RestoreOnStartupBehavior::None + { + let buffer_snapshot = OnceCell::new(); + + if let Some(folds) = DB.get_editor_folds(item_id, workspace_id).log_err() { + if !folds.is_empty() { + let snapshot = + buffer_snapshot.get_or_init(|| self.buffer.read(cx).snapshot(cx)); + self.fold_ranges( + folds + .into_iter() + .map(|(start, end)| { + snapshot.clip_offset(start, Bias::Left) + ..snapshot.clip_offset(end, Bias::Right) + }) + .collect(), + false, + window, + cx, + ); + } + } + + if let Some(selections) = DB.get_editor_selections(item_id, workspace_id).log_err() { + if !selections.is_empty() { + let snapshot = + buffer_snapshot.get_or_init(|| self.buffer.read(cx).snapshot(cx)); + self.change_selections(None, window, cx, |s| { + s.select_ranges(selections.into_iter().map(|(start, end)| { + snapshot.clip_offset(start, Bias::Left) + ..snapshot.clip_offset(end, Bias::Right) + })); + }); + } + }; + } + + self.read_scroll_position_from_db(item_id, workspace_id, window, cx); + } +} + +fn vim_enabled(cx: &App) -> bool { + cx.global::() + .raw_user_settings() + .get("vim_mode") + == Some(&serde_json::Value::Bool(true)) +} + +// Consider user intent and default settings +fn choose_completion_range( + completion: &Completion, + intent: CompletionIntent, + buffer: &Entity, + cx: &mut Context, +) -> Range { + fn should_replace( + completion: &Completion, + insert_range: &Range, + intent: CompletionIntent, + completion_mode_setting: LspInsertMode, + buffer: &Buffer, + ) -> bool { + // specific actions take precedence over settings + match intent { + CompletionIntent::CompleteWithInsert => return false, + CompletionIntent::CompleteWithReplace => return true, + CompletionIntent::Complete | CompletionIntent::Compose => {} + } + + match completion_mode_setting { + LspInsertMode::Insert => false, + LspInsertMode::Replace => true, + LspInsertMode::ReplaceSubsequence => { + let mut text_to_replace = buffer.chars_for_range( + buffer.anchor_before(completion.replace_range.start) + ..buffer.anchor_after(completion.replace_range.end), + ); + let mut completion_text = completion.new_text.chars(); + + // is `text_to_replace` a subsequence of `completion_text` + text_to_replace + .all(|needle_ch| completion_text.any(|haystack_ch| haystack_ch == needle_ch)) + } + LspInsertMode::ReplaceSuffix => { + let range_after_cursor = insert_range.end..completion.replace_range.end; + + let text_after_cursor = buffer + .text_for_range( + buffer.anchor_before(range_after_cursor.start) + ..buffer.anchor_after(range_after_cursor.end), + ) + .collect::(); + completion.new_text.ends_with(&text_after_cursor) + } + } + } + + let buffer = buffer.read(cx); + + if let CompletionSource::Lsp { + insert_range: Some(insert_range), + .. + } = &completion.source + { + let completion_mode_setting = + language_settings(buffer.language().map(|l| l.name()), buffer.file(), cx) + .completions + .lsp_insert_mode; + + if !should_replace( + completion, + &insert_range, + intent, + completion_mode_setting, + buffer, + ) { + return insert_range.to_offset(buffer); + } + } + + completion.replace_range.to_offset(buffer) +} + +fn insert_extra_newline_brackets( + buffer: &MultiBufferSnapshot, + range: Range, + language: &language::LanguageScope, +) -> bool { + let leading_whitespace_len = buffer + .reversed_chars_at(range.start) + .take_while(|c| c.is_whitespace() && *c != '\n') + .map(|c| c.len_utf8()) + .sum::(); + let trailing_whitespace_len = buffer + .chars_at(range.end) + .take_while(|c| c.is_whitespace() && *c != '\n') + .map(|c| c.len_utf8()) + .sum::(); + let range = range.start - leading_whitespace_len..range.end + trailing_whitespace_len; + + language.brackets().any(|(pair, enabled)| { + let pair_start = pair.start.trim_end(); + let pair_end = pair.end.trim_start(); + + enabled + && pair.newline + && buffer.contains_str_at(range.end, pair_end) + && buffer.contains_str_at(range.start.saturating_sub(pair_start.len()), pair_start) + }) +} + +fn insert_extra_newline_tree_sitter(buffer: &MultiBufferSnapshot, range: Range) -> bool { + let (buffer, range) = match buffer.range_to_buffer_ranges(range).as_slice() { + [(buffer, range, _)] => (*buffer, range.clone()), + _ => return false, + }; + let pair = { + let mut result: Option = None; + + for pair in buffer + .all_bracket_ranges(range.clone()) + .filter(move |pair| { + pair.open_range.start <= range.start && pair.close_range.end >= range.end + }) + { + let len = pair.close_range.end - pair.open_range.start; + + if let Some(existing) = &result { + let existing_len = existing.close_range.end - existing.open_range.start; + if len > existing_len { + continue; + } + } + + result = Some(pair); + } + + result + }; + let Some(pair) = pair else { + return false; + }; + pair.newline_only + && buffer + .chars_for_range(pair.open_range.end..range.start) + .chain(buffer.chars_for_range(range.end..pair.close_range.start)) + .all(|c| c.is_whitespace() && c != '\n') +} + +fn get_uncommitted_diff_for_buffer( + project: &Entity, + buffers: impl IntoIterator>, + buffer: Entity, + cx: &mut App, +) -> Task<()> { + let mut tasks = Vec::new(); + project.update(cx, |project, cx| { + for buffer in buffers { + if project::File::from_dyn(buffer.read(cx).file()).is_some() { + tasks.push(project.open_uncommitted_diff(buffer.clone(), cx)) + } + } + }); + cx.spawn(async move |cx| { + let diffs = future::join_all(tasks).await; + buffer + .update(cx, |buffer, cx| { + for diff in diffs.into_iter().flatten() { + buffer.add_diff(diff, cx); + } + }) + .ok(); + }) +} + +fn char_len_with_expanded_tabs(offset: usize, text: &str, tab_size: NonZeroU32) -> usize { + let tab_size = tab_size.get() as usize; + let mut width = offset; + + for ch in text.chars() { + width += if ch == '\t' { + tab_size - (width % tab_size) + } else { + 1 + }; + } + + width - offset +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_string_size_with_expanded_tabs() { + let nz = |val| NonZeroU32::new(val).unwrap(); + assert_eq!(char_len_with_expanded_tabs(0, "", nz(4)), 0); + assert_eq!(char_len_with_expanded_tabs(0, "hello", nz(4)), 5); + assert_eq!(char_len_with_expanded_tabs(0, "\thello", nz(4)), 9); + assert_eq!(char_len_with_expanded_tabs(0, "abc\tab", nz(4)), 6); + assert_eq!(char_len_with_expanded_tabs(0, "hello\t", nz(4)), 8); + assert_eq!(char_len_with_expanded_tabs(0, "\t\t", nz(8)), 16); + assert_eq!(char_len_with_expanded_tabs(0, "x\t", nz(8)), 8); + assert_eq!(char_len_with_expanded_tabs(7, "x\t", nz(8)), 9); + } +} + +/// Tokenizes a string into runs of text that should stick together, or that is whitespace. +struct WordBreakingTokenizer<'a> { + input: &'a str, +} + +impl<'a> WordBreakingTokenizer<'a> { + fn new(input: &'a str) -> Self { + Self { input } + } +} + +fn is_char_ideographic(ch: char) -> bool { + use unicode_script::Script::*; + use unicode_script::UnicodeScript; + matches!(ch.script(), Han | Tangut | Yi) +} + +fn is_grapheme_ideographic(text: &str) -> bool { + text.chars().any(is_char_ideographic) +} + +fn is_grapheme_whitespace(text: &str) -> bool { + text.chars().any(|x| x.is_whitespace()) +} + +fn should_stay_with_preceding_ideograph(text: &str) -> bool { + text.chars().next().map_or(false, |ch| { + matches!(ch, '。' | '、' | ',' | '?' | '!' | ':' | ';' | '…') + }) +} + +#[derive(PartialEq, Eq, Debug, Clone, Copy)] +enum WordBreakToken<'a> { + Word { token: &'a str, grapheme_len: usize }, + InlineWhitespace { token: &'a str, grapheme_len: usize }, + Newline, +} + +impl<'a> Iterator for WordBreakingTokenizer<'a> { + /// Yields a span, the count of graphemes in the token, and whether it was + /// whitespace. Note that it also breaks at word boundaries. + type Item = WordBreakToken<'a>; + + fn next(&mut self) -> Option { + use unicode_segmentation::UnicodeSegmentation; + if self.input.is_empty() { + return None; + } + + let mut iter = self.input.graphemes(true).peekable(); + let mut offset = 0; + let mut grapheme_len = 0; + if let Some(first_grapheme) = iter.next() { + let is_newline = first_grapheme == "\n"; + let is_whitespace = is_grapheme_whitespace(first_grapheme); + offset += first_grapheme.len(); + grapheme_len += 1; + if is_grapheme_ideographic(first_grapheme) && !is_whitespace { + if let Some(grapheme) = iter.peek().copied() { + if should_stay_with_preceding_ideograph(grapheme) { + offset += grapheme.len(); + grapheme_len += 1; + } + } + } else { + let mut words = self.input[offset..].split_word_bound_indices().peekable(); + let mut next_word_bound = words.peek().copied(); + if next_word_bound.map_or(false, |(i, _)| i == 0) { + next_word_bound = words.next(); + } + while let Some(grapheme) = iter.peek().copied() { + if next_word_bound.map_or(false, |(i, _)| i == offset) { + break; + }; + if is_grapheme_whitespace(grapheme) != is_whitespace + || (grapheme == "\n") != is_newline + { + break; + }; + offset += grapheme.len(); + grapheme_len += 1; + iter.next(); + } + } + let token = &self.input[..offset]; + self.input = &self.input[offset..]; + if token == "\n" { + Some(WordBreakToken::Newline) + } else if is_whitespace { + Some(WordBreakToken::InlineWhitespace { + token, + grapheme_len, + }) + } else { + Some(WordBreakToken::Word { + token, + grapheme_len, + }) + } + } else { + None + } + } +} + +#[test] +fn test_word_breaking_tokenizer() { + let tests: &[(&str, &[WordBreakToken<'static>])] = &[ + ("", &[]), + (" ", &[whitespace(" ", 2)]), + ("Ʒ", &[word("Ʒ", 1)]), + ("Ǽ", &[word("Ǽ", 1)]), + ("⋑", &[word("⋑", 1)]), + ("⋑⋑", &[word("⋑⋑", 2)]), + ( + "原理,进而", + &[word("原", 1), word("理,", 2), word("进", 1), word("而", 1)], + ), + ( + "hello world", + &[word("hello", 5), whitespace(" ", 1), word("world", 5)], + ), + ( + "hello, world", + &[word("hello,", 6), whitespace(" ", 1), word("world", 5)], + ), + ( + " hello world", + &[ + whitespace(" ", 2), + word("hello", 5), + whitespace(" ", 1), + word("world", 5), + ], + ), + ( + "这是什么 \n 钢笔", + &[ + word("这", 1), + word("是", 1), + word("什", 1), + word("么", 1), + whitespace(" ", 1), + newline(), + whitespace(" ", 1), + word("钢", 1), + word("笔", 1), + ], + ), + (" mutton", &[whitespace(" ", 1), word("mutton", 6)]), + ]; + + fn word(token: &'static str, grapheme_len: usize) -> WordBreakToken<'static> { + WordBreakToken::Word { + token, + grapheme_len, + } + } + + fn whitespace(token: &'static str, grapheme_len: usize) -> WordBreakToken<'static> { + WordBreakToken::InlineWhitespace { + token, + grapheme_len, + } + } + + fn newline() -> WordBreakToken<'static> { + WordBreakToken::Newline + } + + for (input, result) in tests { + assert_eq!( + WordBreakingTokenizer::new(input) + .collect::>() + .as_slice(), + *result, + ); + } +} + +fn wrap_with_prefix( + line_prefix: String, + unwrapped_text: String, + wrap_column: usize, + tab_size: NonZeroU32, + preserve_existing_whitespace: bool, +) -> String { + let line_prefix_len = char_len_with_expanded_tabs(0, &line_prefix, tab_size); + let mut wrapped_text = String::new(); + let mut current_line = line_prefix.clone(); + + let tokenizer = WordBreakingTokenizer::new(&unwrapped_text); + let mut current_line_len = line_prefix_len; + let mut in_whitespace = false; + for token in tokenizer { + let have_preceding_whitespace = in_whitespace; + match token { + WordBreakToken::Word { + token, + grapheme_len, + } => { + in_whitespace = false; + if current_line_len + grapheme_len > wrap_column + && current_line_len != line_prefix_len + { + wrapped_text.push_str(current_line.trim_end()); + wrapped_text.push('\n'); + current_line.truncate(line_prefix.len()); + current_line_len = line_prefix_len; + } + current_line.push_str(token); + current_line_len += grapheme_len; + } + WordBreakToken::InlineWhitespace { + mut token, + mut grapheme_len, + } => { + in_whitespace = true; + if have_preceding_whitespace && !preserve_existing_whitespace { + continue; + } + if !preserve_existing_whitespace { + token = " "; + grapheme_len = 1; + } + if current_line_len + grapheme_len > wrap_column { + wrapped_text.push_str(current_line.trim_end()); + wrapped_text.push('\n'); + current_line.truncate(line_prefix.len()); + current_line_len = line_prefix_len; + } else if current_line_len != line_prefix_len || preserve_existing_whitespace { + current_line.push_str(token); + current_line_len += grapheme_len; + } + } + WordBreakToken::Newline => { + in_whitespace = true; + if preserve_existing_whitespace { + wrapped_text.push_str(current_line.trim_end()); + wrapped_text.push('\n'); + current_line.truncate(line_prefix.len()); + current_line_len = line_prefix_len; + } else if have_preceding_whitespace { + continue; + } else if current_line_len + 1 > wrap_column && current_line_len != line_prefix_len + { + wrapped_text.push_str(current_line.trim_end()); + wrapped_text.push('\n'); + current_line.truncate(line_prefix.len()); + current_line_len = line_prefix_len; + } else if current_line_len != line_prefix_len { + current_line.push(' '); + current_line_len += 1; + } + } + } + } + + if !current_line.is_empty() { + wrapped_text.push_str(¤t_line); + } + wrapped_text +} + +#[test] +fn test_wrap_with_prefix() { + assert_eq!( + wrap_with_prefix( + "# ".to_string(), + "abcdefg".to_string(), + 4, + NonZeroU32::new(4).unwrap(), + false, + ), + "# abcdefg" + ); + assert_eq!( + wrap_with_prefix( + "".to_string(), + "\thello world".to_string(), + 8, + NonZeroU32::new(4).unwrap(), + false, + ), + "hello\nworld" + ); + assert_eq!( + wrap_with_prefix( + "// ".to_string(), + "xx \nyy zz aa bb cc".to_string(), + 12, + NonZeroU32::new(4).unwrap(), + false, + ), + "// xx yy zz\n// aa bb cc" + ); + assert_eq!( + wrap_with_prefix( + String::new(), + "这是什么 \n 钢笔".to_string(), + 3, + NonZeroU32::new(4).unwrap(), + false, + ), + "这是什\n么 钢\n笔" + ); +} + +pub trait CollaborationHub { + fn collaborators<'a>(&self, cx: &'a App) -> &'a HashMap; + fn user_participant_indices<'a>(&self, cx: &'a App) -> &'a HashMap; + fn user_names(&self, cx: &App) -> HashMap; +} + +impl CollaborationHub for Entity { + fn collaborators<'a>(&self, cx: &'a App) -> &'a HashMap { + self.read(cx).collaborators() + } + + fn user_participant_indices<'a>(&self, cx: &'a App) -> &'a HashMap { + self.read(cx).user_store().read(cx).participant_indices() + } + + fn user_names(&self, cx: &App) -> HashMap { + let this = self.read(cx); + let user_ids = this.collaborators().values().map(|c| c.user_id); + this.user_store().read_with(cx, |user_store, cx| { + user_store.participant_names(user_ids, cx) + }) + } +} + +pub trait SemanticsProvider { + fn hover( + &self, + buffer: &Entity, + position: text::Anchor, + cx: &mut App, + ) -> Option>>; + + fn inline_values( + &self, + buffer_handle: Entity, + range: Range, + cx: &mut App, + ) -> Option>>>; + + fn inlay_hints( + &self, + buffer_handle: Entity, + range: Range, + cx: &mut App, + ) -> Option>>>; + + fn resolve_inlay_hint( + &self, + hint: InlayHint, + buffer_handle: Entity, + server_id: LanguageServerId, + cx: &mut App, + ) -> Option>>; + + fn supports_inlay_hints(&self, buffer: &Entity, cx: &mut App) -> bool; + + fn document_highlights( + &self, + buffer: &Entity, + position: text::Anchor, + cx: &mut App, + ) -> Option>>>; + + fn definitions( + &self, + buffer: &Entity, + position: text::Anchor, + kind: GotoDefinitionKind, + cx: &mut App, + ) -> Option>>>; + + fn range_for_rename( + &self, + buffer: &Entity, + position: text::Anchor, + cx: &mut App, + ) -> Option>>>>; + + fn perform_rename( + &self, + buffer: &Entity, + position: text::Anchor, + new_name: String, + cx: &mut App, + ) -> Option>>; +} + +pub trait CompletionProvider { + fn completions( + &self, + excerpt_id: ExcerptId, + buffer: &Entity, + buffer_position: text::Anchor, + trigger: CompletionContext, + window: &mut Window, + cx: &mut Context, + ) -> Task>>>; + + fn resolve_completions( + &self, + buffer: Entity, + completion_indices: Vec, + completions: Rc>>, + cx: &mut Context, + ) -> Task>; + + fn apply_additional_edits_for_completion( + &self, + _buffer: Entity, + _completions: Rc>>, + _completion_index: usize, + _push_to_history: bool, + _cx: &mut Context, + ) -> Task>> { + Task::ready(Ok(None)) + } + + fn is_completion_trigger( + &self, + buffer: &Entity, + position: language::Anchor, + text: &str, + trigger_in_words: bool, + cx: &mut Context, + ) -> bool; + + fn sort_completions(&self) -> bool { + true + } + + fn filter_completions(&self) -> bool { + true + } +} + +pub trait CodeActionProvider { + fn id(&self) -> Arc; + + fn code_actions( + &self, + buffer: &Entity, + range: Range, + window: &mut Window, + cx: &mut App, + ) -> Task>>; + + fn apply_code_action( + &self, + buffer_handle: Entity, + action: CodeAction, + excerpt_id: ExcerptId, + push_to_history: bool, + window: &mut Window, + cx: &mut App, + ) -> Task>; +} + +impl CodeActionProvider for Entity { + fn id(&self) -> Arc { + "project".into() + } + + fn code_actions( + &self, + buffer: &Entity, + range: Range, + _window: &mut Window, + cx: &mut App, + ) -> Task>> { + self.update(cx, |project, cx| { + let code_lens = project.code_lens(buffer, range.clone(), cx); + let code_actions = project.code_actions(buffer, range, None, cx); + cx.background_spawn(async move { + let (code_lens, code_actions) = join(code_lens, code_actions).await; + Ok(code_lens + .context("code lens fetch")? + .into_iter() + .chain(code_actions.context("code action fetch")?) + .collect()) + }) + }) + } + + fn apply_code_action( + &self, + buffer_handle: Entity, + action: CodeAction, + _excerpt_id: ExcerptId, + push_to_history: bool, + _window: &mut Window, + cx: &mut App, + ) -> Task> { + self.update(cx, |project, cx| { + project.apply_code_action(buffer_handle, action, push_to_history, cx) + }) + } +} + +fn snippet_completions( + project: &Project, + buffer: &Entity, + buffer_position: text::Anchor, + cx: &mut App, +) -> Task>> { + let languages = buffer.read(cx).languages_at(buffer_position); + let snippet_store = project.snippets().read(cx); + + let scopes: Vec<_> = languages + .iter() + .filter_map(|language| { + let language_name = language.lsp_id(); + let snippets = snippet_store.snippets_for(Some(language_name), cx); + + if snippets.is_empty() { + None + } else { + Some((language.default_scope(), snippets)) + } + }) + .collect(); + + if scopes.is_empty() { + return Task::ready(Ok(vec![])); + } + + let snapshot = buffer.read(cx).text_snapshot(); + let chars: String = snapshot + .reversed_chars_for_range(text::Anchor::MIN..buffer_position) + .collect(); + let executor = cx.background_executor().clone(); + + cx.background_spawn(async move { + let mut all_results: Vec = Vec::new(); + for (scope, snippets) in scopes.into_iter() { + let classifier = CharClassifier::new(Some(scope)).for_completion(true); + let mut last_word = chars + .chars() + .take_while(|c| classifier.is_word(*c)) + .collect::(); + last_word = last_word.chars().rev().collect(); + + if last_word.is_empty() { + return Ok(vec![]); + } + + let as_offset = text::ToOffset::to_offset(&buffer_position, &snapshot); + let to_lsp = |point: &text::Anchor| { + let end = text::ToPointUtf16::to_point_utf16(point, &snapshot); + point_to_lsp(end) + }; + let lsp_end = to_lsp(&buffer_position); + + let candidates = snippets + .iter() + .enumerate() + .flat_map(|(ix, snippet)| { + snippet + .prefix + .iter() + .map(move |prefix| StringMatchCandidate::new(ix, &prefix)) + }) + .collect::>(); + + let mut matches = fuzzy::match_strings( + &candidates, + &last_word, + last_word.chars().any(|c| c.is_uppercase()), + 100, + &Default::default(), + executor.clone(), + ) + .await; + + // Remove all candidates where the query's start does not match the start of any word in the candidate + if let Some(query_start) = last_word.chars().next() { + matches.retain(|string_match| { + split_words(&string_match.string).any(|word| { + // Check that the first codepoint of the word as lowercase matches the first + // codepoint of the query as lowercase + word.chars() + .flat_map(|codepoint| codepoint.to_lowercase()) + .zip(query_start.to_lowercase()) + .all(|(word_cp, query_cp)| word_cp == query_cp) + }) + }); + } + + let matched_strings = matches + .into_iter() + .map(|m| m.string) + .collect::>(); + + let mut result: Vec = snippets + .iter() + .filter_map(|snippet| { + let matching_prefix = snippet + .prefix + .iter() + .find(|prefix| matched_strings.contains(*prefix))?; + let start = as_offset - last_word.len(); + let start = snapshot.anchor_before(start); + let range = start..buffer_position; + let lsp_start = to_lsp(&start); + let lsp_range = lsp::Range { + start: lsp_start, + end: lsp_end, + }; + Some(Completion { + replace_range: range, + new_text: snippet.body.clone(), + source: CompletionSource::Lsp { + insert_range: None, + server_id: LanguageServerId(usize::MAX), + resolved: true, + lsp_completion: Box::new(lsp::CompletionItem { + label: snippet.prefix.first().unwrap().clone(), + kind: Some(CompletionItemKind::SNIPPET), + label_details: snippet.description.as_ref().map(|description| { + lsp::CompletionItemLabelDetails { + detail: Some(description.clone()), + description: None, + } + }), + insert_text_format: Some(InsertTextFormat::SNIPPET), + text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace( + lsp::InsertReplaceEdit { + new_text: snippet.body.clone(), + insert: lsp_range, + replace: lsp_range, + }, + )), + filter_text: Some(snippet.body.clone()), + sort_text: Some(char::MAX.to_string()), + ..lsp::CompletionItem::default() + }), + lsp_defaults: None, + }, + label: CodeLabel { + text: matching_prefix.clone(), + runs: Vec::new(), + filter_range: 0..matching_prefix.len(), + }, + icon_path: None, + documentation: snippet.description.clone().map(|description| { + CompletionDocumentation::SingleLine(description.into()) + }), + insert_text_mode: None, + confirm: None, + }) + }) + .collect(); + + all_results.append(&mut result); + } + + Ok(all_results) + }) +} + +impl CompletionProvider for Entity { + fn completions( + &self, + _excerpt_id: ExcerptId, + buffer: &Entity, + buffer_position: text::Anchor, + options: CompletionContext, + _window: &mut Window, + cx: &mut Context, + ) -> Task>>> { + self.update(cx, |project, cx| { + let snippets = snippet_completions(project, buffer, buffer_position, cx); + let project_completions = project.completions(buffer, buffer_position, options, cx); + cx.background_spawn(async move { + let snippets_completions = snippets.await?; + match project_completions.await? { + Some(mut completions) => { + completions.extend(snippets_completions); + Ok(Some(completions)) + } + None => { + if snippets_completions.is_empty() { + Ok(None) + } else { + Ok(Some(snippets_completions)) + } + } + } + }) + }) + } + + fn resolve_completions( + &self, + buffer: Entity, + completion_indices: Vec, + completions: Rc>>, + cx: &mut Context, + ) -> Task> { + self.update(cx, |project, cx| { + project.lsp_store().update(cx, |lsp_store, cx| { + lsp_store.resolve_completions(buffer, completion_indices, completions, cx) + }) + }) + } + + fn apply_additional_edits_for_completion( + &self, + buffer: Entity, + completions: Rc>>, + completion_index: usize, + push_to_history: bool, + cx: &mut Context, + ) -> Task>> { + self.update(cx, |project, cx| { + project.lsp_store().update(cx, |lsp_store, cx| { + lsp_store.apply_additional_edits_for_completion( + buffer, + completions, + completion_index, + push_to_history, + cx, + ) + }) + }) + } + + fn is_completion_trigger( + &self, + buffer: &Entity, + position: language::Anchor, + text: &str, + trigger_in_words: bool, + cx: &mut Context, + ) -> bool { + let mut chars = text.chars(); + let char = if let Some(char) = chars.next() { + char + } else { + return false; + }; + if chars.next().is_some() { + return false; + } + + let buffer = buffer.read(cx); + let snapshot = buffer.snapshot(); + if !snapshot.settings_at(position, cx).show_completions_on_input { + return false; + } + let classifier = snapshot.char_classifier_at(position).for_completion(true); + if trigger_in_words && classifier.is_word(char) { + return true; + } + + buffer.completion_triggers().contains(text) + } +} + +impl SemanticsProvider for Entity { + fn hover( + &self, + buffer: &Entity, + position: text::Anchor, + cx: &mut App, + ) -> Option>> { + Some(self.update(cx, |project, cx| project.hover(buffer, position, cx))) + } + + fn document_highlights( + &self, + buffer: &Entity, + position: text::Anchor, + cx: &mut App, + ) -> Option>>> { + Some(self.update(cx, |project, cx| { + project.document_highlights(buffer, position, cx) + })) + } + + fn definitions( + &self, + buffer: &Entity, + position: text::Anchor, + kind: GotoDefinitionKind, + cx: &mut App, + ) -> Option>>> { + Some(self.update(cx, |project, cx| match kind { + GotoDefinitionKind::Symbol => project.definition(&buffer, position, cx), + GotoDefinitionKind::Declaration => project.declaration(&buffer, position, cx), + GotoDefinitionKind::Type => project.type_definition(&buffer, position, cx), + GotoDefinitionKind::Implementation => project.implementation(&buffer, position, cx), + })) + } + + fn supports_inlay_hints(&self, buffer: &Entity, cx: &mut App) -> bool { + // TODO: make this work for remote projects + self.update(cx, |project, cx| { + if project + .active_debug_session(cx) + .is_some_and(|(session, _)| session.read(cx).any_stopped_thread()) + { + return true; + } + + buffer.update(cx, |buffer, cx| { + project.any_language_server_supports_inlay_hints(buffer, cx) + }) + }) + } + + fn inline_values( + &self, + buffer_handle: Entity, + range: Range, + cx: &mut App, + ) -> Option>>> { + self.update(cx, |project, cx| { + let (session, active_stack_frame) = project.active_debug_session(cx)?; + + Some(project.inline_values(session, active_stack_frame, buffer_handle, range, cx)) + }) + } + + fn inlay_hints( + &self, + buffer_handle: Entity, + range: Range, + cx: &mut App, + ) -> Option>>> { + Some(self.update(cx, |project, cx| { + project.inlay_hints(buffer_handle, range, cx) + })) + } + + fn resolve_inlay_hint( + &self, + hint: InlayHint, + buffer_handle: Entity, + server_id: LanguageServerId, + cx: &mut App, + ) -> Option>> { + Some(self.update(cx, |project, cx| { + project.resolve_inlay_hint(hint, buffer_handle, server_id, cx) + })) + } + + fn range_for_rename( + &self, + buffer: &Entity, + position: text::Anchor, + cx: &mut App, + ) -> Option>>>> { + Some(self.update(cx, |project, cx| { + let buffer = buffer.clone(); + let task = project.prepare_rename(buffer.clone(), position, cx); + cx.spawn(async move |_, cx| { + Ok(match task.await? { + PrepareRenameResponse::Success(range) => Some(range), + PrepareRenameResponse::InvalidPosition => None, + PrepareRenameResponse::OnlyUnpreparedRenameSupported => { + // Fallback on using TreeSitter info to determine identifier range + buffer.update(cx, |buffer, _| { + let snapshot = buffer.snapshot(); + let (range, kind) = snapshot.surrounding_word(position); + if kind != Some(CharKind::Word) { + return None; + } + Some( + snapshot.anchor_before(range.start) + ..snapshot.anchor_after(range.end), + ) + })? + } + }) + }) + })) + } + + fn perform_rename( + &self, + buffer: &Entity, + position: text::Anchor, + new_name: String, + cx: &mut App, + ) -> Option>> { + Some(self.update(cx, |project, cx| { + project.perform_rename(buffer.clone(), position, new_name, cx) + })) + } +} + +fn inlay_hint_settings( + location: Anchor, + snapshot: &MultiBufferSnapshot, + cx: &mut Context, +) -> InlayHintSettings { + let file = snapshot.file_at(location); + let language = snapshot.language_at(location).map(|l| l.name()); + language_settings(language, file, cx).inlay_hints +} + +fn consume_contiguous_rows( + contiguous_row_selections: &mut Vec>, + selection: &Selection, + display_map: &DisplaySnapshot, + selections: &mut Peekable>>, +) -> (MultiBufferRow, MultiBufferRow) { + contiguous_row_selections.push(selection.clone()); + let start_row = MultiBufferRow(selection.start.row); + let mut end_row = ending_row(selection, display_map); + + while let Some(next_selection) = selections.peek() { + if next_selection.start.row <= end_row.0 { + end_row = ending_row(next_selection, display_map); + contiguous_row_selections.push(selections.next().unwrap().clone()); + } else { + break; + } + } + (start_row, end_row) +} + +fn ending_row(next_selection: &Selection, display_map: &DisplaySnapshot) -> MultiBufferRow { + if next_selection.end.column > 0 || next_selection.is_empty() { + MultiBufferRow(display_map.next_line_boundary(next_selection.end).0.row + 1) + } else { + MultiBufferRow(next_selection.end.row) + } +} + +impl EditorSnapshot { + pub fn remote_selections_in_range<'a>( + &'a self, + range: &'a Range, + collaboration_hub: &dyn CollaborationHub, + cx: &'a App, + ) -> impl 'a + Iterator { + let participant_names = collaboration_hub.user_names(cx); + let participant_indices = collaboration_hub.user_participant_indices(cx); + let collaborators_by_peer_id = collaboration_hub.collaborators(cx); + let collaborators_by_replica_id = collaborators_by_peer_id + .iter() + .map(|(_, collaborator)| (collaborator.replica_id, collaborator)) + .collect::>(); + self.buffer_snapshot + .selections_in_range(range, false) + .filter_map(move |(replica_id, line_mode, cursor_shape, selection)| { + let collaborator = collaborators_by_replica_id.get(&replica_id)?; + let participant_index = participant_indices.get(&collaborator.user_id).copied(); + let user_name = participant_names.get(&collaborator.user_id).cloned(); + Some(RemoteSelection { + replica_id, + selection, + cursor_shape, + line_mode, + participant_index, + peer_id: collaborator.peer_id, + user_name, + }) + }) + } + + pub fn hunks_for_ranges( + &self, + ranges: impl IntoIterator>, + ) -> Vec { + let mut hunks = Vec::new(); + let mut processed_buffer_rows: HashMap>> = + HashMap::default(); + for query_range in ranges { + let query_rows = + MultiBufferRow(query_range.start.row)..MultiBufferRow(query_range.end.row + 1); + for hunk in self.buffer_snapshot.diff_hunks_in_range( + Point::new(query_rows.start.0, 0)..Point::new(query_rows.end.0, 0), + ) { + // Include deleted hunks that are adjacent to the query range, because + // otherwise they would be missed. + let mut intersects_range = hunk.row_range.overlaps(&query_rows); + if hunk.status().is_deleted() { + intersects_range |= hunk.row_range.start == query_rows.end; + intersects_range |= hunk.row_range.end == query_rows.start; + } + if intersects_range { + if !processed_buffer_rows + .entry(hunk.buffer_id) + .or_default() + .insert(hunk.buffer_range.start..hunk.buffer_range.end) + { + continue; + } + hunks.push(hunk); + } + } + } + + hunks + } + + fn display_diff_hunks_for_rows<'a>( + &'a self, + display_rows: Range, + folded_buffers: &'a HashSet, + ) -> impl 'a + Iterator { + let buffer_start = DisplayPoint::new(display_rows.start, 0).to_point(self); + let buffer_end = DisplayPoint::new(display_rows.end, 0).to_point(self); + + self.buffer_snapshot + .diff_hunks_in_range(buffer_start..buffer_end) + .filter_map(|hunk| { + if folded_buffers.contains(&hunk.buffer_id) { + return None; + } + + let hunk_start_point = Point::new(hunk.row_range.start.0, 0); + let hunk_end_point = Point::new(hunk.row_range.end.0, 0); + + let hunk_display_start = self.point_to_display_point(hunk_start_point, Bias::Left); + let hunk_display_end = self.point_to_display_point(hunk_end_point, Bias::Right); + + let display_hunk = if hunk_display_start.column() != 0 { + DisplayDiffHunk::Folded { + display_row: hunk_display_start.row(), + } + } else { + let mut end_row = hunk_display_end.row(); + if hunk_display_end.column() > 0 { + end_row.0 += 1; + } + let is_created_file = hunk.is_created_file(); + DisplayDiffHunk::Unfolded { + status: hunk.status(), + diff_base_byte_range: hunk.diff_base_byte_range, + display_row_range: hunk_display_start.row()..end_row, + multi_buffer_range: Anchor::range_in_buffer( + hunk.excerpt_id, + hunk.buffer_id, + hunk.buffer_range, + ), + is_created_file, + } + }; + + Some(display_hunk) + }) + } + + pub fn language_at(&self, position: T) -> Option<&Arc> { + self.display_snapshot.buffer_snapshot.language_at(position) + } + + pub fn is_focused(&self) -> bool { + self.is_focused + } + + pub fn placeholder_text(&self) -> Option<&Arc> { + self.placeholder_text.as_ref() + } + + pub fn scroll_position(&self) -> gpui::Point { + self.scroll_anchor.scroll_position(&self.display_snapshot) + } + + fn gutter_dimensions( + &self, + font_id: FontId, + font_size: Pixels, + max_line_number_width: Pixels, + cx: &App, + ) -> Option { + if !self.show_gutter { + return None; + } + + let descent = cx.text_system().descent(font_id, font_size); + let em_width = cx.text_system().em_width(font_id, font_size).log_err()?; + let em_advance = cx.text_system().em_advance(font_id, font_size).log_err()?; + + let show_git_gutter = self.show_git_diff_gutter.unwrap_or_else(|| { + matches!( + ProjectSettings::get_global(cx).git.git_gutter, + Some(GitGutterSetting::TrackedFiles) + ) + }); + let gutter_settings = EditorSettings::get_global(cx).gutter; + let show_line_numbers = self + .show_line_numbers + .unwrap_or(gutter_settings.line_numbers); + let line_gutter_width = if show_line_numbers { + // Avoid flicker-like gutter resizes when the line number gains another digit and only resize the gutter on files with N*10^5 lines. + let min_width_for_number_on_gutter = em_advance * MIN_LINE_NUMBER_DIGITS as f32; + max_line_number_width.max(min_width_for_number_on_gutter) + } else { + 0.0.into() + }; + + let show_code_actions = self + .show_code_actions + .unwrap_or(gutter_settings.code_actions); + + let show_runnables = self.show_runnables.unwrap_or(gutter_settings.runnables); + let show_breakpoints = self.show_breakpoints.unwrap_or(gutter_settings.breakpoints); + + let git_blame_entries_width = + self.git_blame_gutter_max_author_length + .map(|max_author_length| { + let renderer = cx.global::().0.clone(); + const MAX_RELATIVE_TIMESTAMP: &str = "60 minutes ago"; + + /// The number of characters to dedicate to gaps and margins. + const SPACING_WIDTH: usize = 4; + + let max_char_count = max_author_length.min(renderer.max_author_length()) + + ::git::SHORT_SHA_LENGTH + + MAX_RELATIVE_TIMESTAMP.len() + + SPACING_WIDTH; + + em_advance * max_char_count + }); + + let is_singleton = self.buffer_snapshot.is_singleton(); + + let mut left_padding = git_blame_entries_width.unwrap_or(Pixels::ZERO); + left_padding += if !is_singleton { + em_width * 4.0 + } else if show_code_actions || show_runnables || show_breakpoints { + em_width * 3.0 + } else if show_git_gutter && show_line_numbers { + em_width * 2.0 + } else if show_git_gutter || show_line_numbers { + em_width + } else { + px(0.) + }; + + let shows_folds = is_singleton && gutter_settings.folds; + + let right_padding = if shows_folds && show_line_numbers { + em_width * 4.0 + } else if shows_folds || (!is_singleton && show_line_numbers) { + em_width * 3.0 + } else if show_line_numbers { + em_width + } else { + px(0.) + }; + + Some(GutterDimensions { + left_padding, + right_padding, + width: line_gutter_width + left_padding + right_padding, + margin: -descent, + git_blame_entries_width, + }) + } + + pub fn render_crease_toggle( + &self, + buffer_row: MultiBufferRow, + row_contains_cursor: bool, + editor: Entity, + window: &mut Window, + cx: &mut App, + ) -> Option { + let folded = self.is_line_folded(buffer_row); + let mut is_foldable = false; + + if let Some(crease) = self + .crease_snapshot + .query_row(buffer_row, &self.buffer_snapshot) + { + is_foldable = true; + match crease { + Crease::Inline { render_toggle, .. } | Crease::Block { render_toggle, .. } => { + if let Some(render_toggle) = render_toggle { + let toggle_callback = + Arc::new(move |folded, window: &mut Window, cx: &mut App| { + if folded { + editor.update(cx, |editor, cx| { + editor.fold_at(buffer_row, window, cx) + }); + } else { + editor.update(cx, |editor, cx| { + editor.unfold_at(buffer_row, window, cx) + }); + } + }); + return Some((render_toggle)( + buffer_row, + folded, + toggle_callback, + window, + cx, + )); + } + } + } + } + + is_foldable |= self.starts_indent(buffer_row); + + if folded || (is_foldable && (row_contains_cursor || self.gutter_hovered)) { + Some( + Disclosure::new(("gutter_crease", buffer_row.0), !folded) + .toggle_state(folded) + .on_click(window.listener_for(&editor, move |this, _e, window, cx| { + if folded { + this.unfold_at(buffer_row, window, cx); + } else { + this.fold_at(buffer_row, window, cx); + } + })) + .into_any_element(), + ) + } else { + None + } + } + + pub fn render_crease_trailer( + &self, + buffer_row: MultiBufferRow, + window: &mut Window, + cx: &mut App, + ) -> Option { + let folded = self.is_line_folded(buffer_row); + if let Crease::Inline { render_trailer, .. } = self + .crease_snapshot + .query_row(buffer_row, &self.buffer_snapshot)? + { + let render_trailer = render_trailer.as_ref()?; + Some(render_trailer(buffer_row, folded, window, cx)) + } else { + None + } + } +} + +impl Deref for EditorSnapshot { + type Target = DisplaySnapshot; + + fn deref(&self) -> &Self::Target { + &self.display_snapshot + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum EditorEvent { + InputIgnored { + text: Arc, + }, + InputHandled { + utf16_range_to_replace: Option>, + text: Arc, + }, + ExcerptsAdded { + buffer: Entity, + predecessor: ExcerptId, + excerpts: Vec<(ExcerptId, ExcerptRange)>, + }, + ExcerptsRemoved { + ids: Vec, + removed_buffer_ids: Vec, + }, + BufferFoldToggled { + ids: Vec, + folded: bool, + }, + ExcerptsEdited { + ids: Vec, + }, + ExcerptsExpanded { + ids: Vec, + }, + BufferEdited, + Edited { + transaction_id: clock::Lamport, + }, + Reparsed(BufferId), + Focused, + FocusedIn, + Blurred, + DirtyChanged, + Saved, + TitleChanged, + DiffBaseChanged, + SelectionsChanged { + local: bool, + }, + ScrollPositionChanged { + local: bool, + autoscroll: bool, + }, + Closed, + TransactionUndone { + transaction_id: clock::Lamport, + }, + TransactionBegun { + transaction_id: clock::Lamport, + }, + Reloaded, + CursorShapeChanged, + PushedToNavHistory { + anchor: Anchor, + is_deactivate: bool, + }, +} + +impl EventEmitter for Editor {} + +impl Focusable for Editor { + fn focus_handle(&self, _cx: &App) -> FocusHandle { + self.focus_handle.clone() + } +} + +impl Render for Editor { + fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { + let settings = ThemeSettings::get_global(cx); + + let mut text_style = match self.mode { + EditorMode::SingleLine { .. } | EditorMode::AutoHeight { .. } => TextStyle { + color: cx.theme().colors().editor_foreground, + font_family: settings.ui_font.family.clone(), + font_features: settings.ui_font.features.clone(), + font_fallbacks: settings.ui_font.fallbacks.clone(), + font_size: rems(0.875).into(), + font_weight: settings.ui_font.weight, + line_height: relative(settings.buffer_line_height.value()), + ..Default::default() + }, + EditorMode::Full { .. } => TextStyle { + color: cx.theme().colors().editor_foreground, + font_family: settings.buffer_font.family.clone(), + font_features: settings.buffer_font.features.clone(), + font_fallbacks: settings.buffer_font.fallbacks.clone(), + font_size: settings.buffer_font_size(cx).into(), + font_weight: settings.buffer_font.weight, + line_height: relative(settings.buffer_line_height.value()), + ..Default::default() + }, + }; + if let Some(text_style_refinement) = &self.text_style_refinement { + text_style.refine(text_style_refinement) + } + + let background = match self.mode { + EditorMode::SingleLine { .. } => cx.theme().system().transparent, + EditorMode::AutoHeight { max_lines: _ } => cx.theme().system().transparent, + EditorMode::Full { .. } => cx.theme().colors().editor_background, + }; + + EditorElement::new( + &cx.entity(), + EditorStyle { + background, + local_player: cx.theme().players().local(), + text: text_style, + scrollbar_width: EditorElement::SCROLLBAR_WIDTH, + syntax: cx.theme().syntax().clone(), + status: cx.theme().status().clone(), + inlay_hints_style: make_inlay_hints_style(cx), + inline_completion_styles: make_suggestion_styles(cx), + unnecessary_code_fade: ThemeSettings::get_global(cx).unnecessary_code_fade, + }, + ) + } +} + +impl EntityInputHandler for Editor { + fn text_for_range( + &mut self, + range_utf16: Range, + adjusted_range: &mut Option>, + _: &mut Window, + cx: &mut Context, + ) -> Option { + let snapshot = self.buffer.read(cx).read(cx); + let start = snapshot.clip_offset_utf16(OffsetUtf16(range_utf16.start), Bias::Left); + let end = snapshot.clip_offset_utf16(OffsetUtf16(range_utf16.end), Bias::Right); + if (start.0..end.0) != range_utf16 { + adjusted_range.replace(start.0..end.0); + } + Some(snapshot.text_for_range(start..end).collect()) + } + + fn selected_text_range( + &mut self, + ignore_disabled_input: bool, + _: &mut Window, + cx: &mut Context, + ) -> Option { + // Prevent the IME menu from appearing when holding down an alphabetic key + // while input is disabled. + if !ignore_disabled_input && !self.input_enabled { + return None; + } + + let selection = self.selections.newest::(cx); + let range = selection.range(); + + Some(UTF16Selection { + range: range.start.0..range.end.0, + reversed: selection.reversed, + }) + } + + fn marked_text_range(&self, _: &mut Window, cx: &mut Context) -> Option> { + let snapshot = self.buffer.read(cx).read(cx); + let range = self.text_highlights::(cx)?.1.first()?; + Some(range.start.to_offset_utf16(&snapshot).0..range.end.to_offset_utf16(&snapshot).0) + } + + fn unmark_text(&mut self, _: &mut Window, cx: &mut Context) { + self.clear_highlights::(cx); + self.ime_transaction.take(); + } + + fn replace_text_in_range( + &mut self, + range_utf16: Option>, + text: &str, + window: &mut Window, + cx: &mut Context, + ) { + if !self.input_enabled { + cx.emit(EditorEvent::InputIgnored { text: text.into() }); + return; + } + + self.transact(window, cx, |this, window, cx| { + let new_selected_ranges = if let Some(range_utf16) = range_utf16 { + let range_utf16 = OffsetUtf16(range_utf16.start)..OffsetUtf16(range_utf16.end); + Some(this.selection_replacement_ranges(range_utf16, cx)) + } else { + this.marked_text_ranges(cx) + }; + + let range_to_replace = new_selected_ranges.as_ref().and_then(|ranges_to_replace| { + let newest_selection_id = this.selections.newest_anchor().id; + this.selections + .all::(cx) + .iter() + .zip(ranges_to_replace.iter()) + .find_map(|(selection, range)| { + if selection.id == newest_selection_id { + Some( + (range.start.0 as isize - selection.head().0 as isize) + ..(range.end.0 as isize - selection.head().0 as isize), + ) + } else { + None + } + }) + }); + + cx.emit(EditorEvent::InputHandled { + utf16_range_to_replace: range_to_replace, + text: text.into(), + }); + + if let Some(new_selected_ranges) = new_selected_ranges { + this.change_selections(None, window, cx, |selections| { + selections.select_ranges(new_selected_ranges) + }); + this.backspace(&Default::default(), window, cx); + } + + this.handle_input(text, window, cx); + }); + + if let Some(transaction) = self.ime_transaction { + self.buffer.update(cx, |buffer, cx| { + buffer.group_until_transaction(transaction, cx); + }); + } + + self.unmark_text(window, cx); + } + + fn replace_and_mark_text_in_range( + &mut self, + range_utf16: Option>, + text: &str, + new_selected_range_utf16: Option>, + window: &mut Window, + cx: &mut Context, + ) { + if !self.input_enabled { + return; + } + + let transaction = self.transact(window, cx, |this, window, cx| { + let ranges_to_replace = if let Some(mut marked_ranges) = this.marked_text_ranges(cx) { + let snapshot = this.buffer.read(cx).read(cx); + if let Some(relative_range_utf16) = range_utf16.as_ref() { + for marked_range in &mut marked_ranges { + marked_range.end.0 = marked_range.start.0 + relative_range_utf16.end; + marked_range.start.0 += relative_range_utf16.start; + marked_range.start = + snapshot.clip_offset_utf16(marked_range.start, Bias::Left); + marked_range.end = + snapshot.clip_offset_utf16(marked_range.end, Bias::Right); + } + } + Some(marked_ranges) + } else if let Some(range_utf16) = range_utf16 { + let range_utf16 = OffsetUtf16(range_utf16.start)..OffsetUtf16(range_utf16.end); + Some(this.selection_replacement_ranges(range_utf16, cx)) + } else { + None + }; + + let range_to_replace = ranges_to_replace.as_ref().and_then(|ranges_to_replace| { + let newest_selection_id = this.selections.newest_anchor().id; + this.selections + .all::(cx) + .iter() + .zip(ranges_to_replace.iter()) + .find_map(|(selection, range)| { + if selection.id == newest_selection_id { + Some( + (range.start.0 as isize - selection.head().0 as isize) + ..(range.end.0 as isize - selection.head().0 as isize), + ) + } else { + None + } + }) + }); + + cx.emit(EditorEvent::InputHandled { + utf16_range_to_replace: range_to_replace, + text: text.into(), + }); + + if let Some(ranges) = ranges_to_replace { + this.change_selections(None, window, cx, |s| s.select_ranges(ranges)); + } + + let marked_ranges = { + let snapshot = this.buffer.read(cx).read(cx); + this.selections + .disjoint_anchors() + .iter() + .map(|selection| { + selection.start.bias_left(&snapshot)..selection.end.bias_right(&snapshot) + }) + .collect::>() + }; + + if text.is_empty() { + this.unmark_text(window, cx); + } else { + this.highlight_text::( + marked_ranges.clone(), + HighlightStyle { + underline: Some(UnderlineStyle { + thickness: px(1.), + color: None, + wavy: false, + }), + ..Default::default() + }, + cx, + ); + } + + // Disable auto-closing when composing text (i.e. typing a `"` on a Brazilian keyboard) + let use_autoclose = this.use_autoclose; + let use_auto_surround = this.use_auto_surround; + this.set_use_autoclose(false); + this.set_use_auto_surround(false); + this.handle_input(text, window, cx); + this.set_use_autoclose(use_autoclose); + this.set_use_auto_surround(use_auto_surround); + + if let Some(new_selected_range) = new_selected_range_utf16 { + let snapshot = this.buffer.read(cx).read(cx); + let new_selected_ranges = marked_ranges + .into_iter() + .map(|marked_range| { + let insertion_start = marked_range.start.to_offset_utf16(&snapshot).0; + let new_start = OffsetUtf16(new_selected_range.start + insertion_start); + let new_end = OffsetUtf16(new_selected_range.end + insertion_start); + snapshot.clip_offset_utf16(new_start, Bias::Left) + ..snapshot.clip_offset_utf16(new_end, Bias::Right) + }) + .collect::>(); + + drop(snapshot); + this.change_selections(None, window, cx, |selections| { + selections.select_ranges(new_selected_ranges) + }); + } + }); + + self.ime_transaction = self.ime_transaction.or(transaction); + if let Some(transaction) = self.ime_transaction { + self.buffer.update(cx, |buffer, cx| { + buffer.group_until_transaction(transaction, cx); + }); + } + + if self.text_highlights::(cx).is_none() { + self.ime_transaction.take(); + } + } + + fn bounds_for_range( + &mut self, + range_utf16: Range, + element_bounds: gpui::Bounds, + window: &mut Window, + cx: &mut Context, + ) -> Option> { + let text_layout_details = self.text_layout_details(window); + let gpui::Size { + width: em_width, + height: line_height, + } = self.character_size(window); + + let snapshot = self.snapshot(window, cx); + let scroll_position = snapshot.scroll_position(); + let scroll_left = scroll_position.x * em_width; + + let start = OffsetUtf16(range_utf16.start).to_display_point(&snapshot); + let x = snapshot.x_for_display_point(start, &text_layout_details) - scroll_left + + self.gutter_dimensions.width + + self.gutter_dimensions.margin; + let y = line_height * (start.row().as_f32() - scroll_position.y); + + Some(Bounds { + origin: element_bounds.origin + point(x, y), + size: size(em_width, line_height), + }) + } + + fn character_index_for_point( + &mut self, + point: gpui::Point, + _window: &mut Window, + _cx: &mut Context, + ) -> Option { + let position_map = self.last_position_map.as_ref()?; + if !position_map.text_hitbox.contains(&point) { + return None; + } + let display_point = position_map.point_for_position(point).previous_valid; + let anchor = position_map + .snapshot + .display_point_to_anchor(display_point, Bias::Left); + let utf16_offset = anchor.to_offset_utf16(&position_map.snapshot.buffer_snapshot); + Some(utf16_offset.0) + } +} + +trait SelectionExt { + fn display_range(&self, map: &DisplaySnapshot) -> Range; + fn spanned_rows( + &self, + include_end_if_at_line_start: bool, + map: &DisplaySnapshot, + ) -> Range; +} + +impl SelectionExt for Selection { + fn display_range(&self, map: &DisplaySnapshot) -> Range { + let start = self + .start + .to_point(&map.buffer_snapshot) + .to_display_point(map); + let end = self + .end + .to_point(&map.buffer_snapshot) + .to_display_point(map); + if self.reversed { + end..start + } else { + start..end + } + } + + fn spanned_rows( + &self, + include_end_if_at_line_start: bool, + map: &DisplaySnapshot, + ) -> Range { + let start = self.start.to_point(&map.buffer_snapshot); + let mut end = self.end.to_point(&map.buffer_snapshot); + if !include_end_if_at_line_start && start.row != end.row && end.column == 0 { + end.row -= 1; + } + + let buffer_start = map.prev_line_boundary(start).0; + let buffer_end = map.next_line_boundary(end).0; + MultiBufferRow(buffer_start.row)..MultiBufferRow(buffer_end.row + 1) + } +} + +impl InvalidationStack { + fn invalidate(&mut self, selections: &[Selection], buffer: &MultiBufferSnapshot) + where + S: Clone + ToOffset, + { + while let Some(region) = self.last() { + let all_selections_inside_invalidation_ranges = + if selections.len() == region.ranges().len() { + selections + .iter() + .zip(region.ranges().iter().map(|r| r.to_offset(buffer))) + .all(|(selection, invalidation_range)| { + let head = selection.head().to_offset(buffer); + invalidation_range.start <= head && invalidation_range.end >= head + }) + } else { + false + }; + + if all_selections_inside_invalidation_ranges { + break; + } else { + self.pop(); + } + } + } +} + +impl Default for InvalidationStack { + fn default() -> Self { + Self(Default::default()) + } +} + +impl Deref for InvalidationStack { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for InvalidationStack { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl InvalidationRegion for SnippetState { + fn ranges(&self) -> &[Range] { + &self.ranges[self.active_index] + } +} + +fn inline_completion_edit_text( + current_snapshot: &BufferSnapshot, + edits: &[(Range, String)], + edit_preview: &EditPreview, + include_deletions: bool, + cx: &App, +) -> HighlightedText { + let edits = edits + .iter() + .map(|(anchor, text)| { + ( + anchor.start.text_anchor..anchor.end.text_anchor, + text.clone(), + ) + }) + .collect::>(); + + edit_preview.highlight_edits(current_snapshot, &edits, include_deletions, cx) +} + +pub fn diagnostic_style(severity: DiagnosticSeverity, colors: &StatusColors) -> Hsla { + match severity { + DiagnosticSeverity::ERROR => colors.error, + DiagnosticSeverity::WARNING => colors.warning, + DiagnosticSeverity::INFORMATION => colors.info, + DiagnosticSeverity::HINT => colors.info, + _ => colors.ignored, + } +} + +pub fn styled_runs_for_code_label<'a>( + label: &'a CodeLabel, + syntax_theme: &'a theme::SyntaxTheme, +) -> impl 'a + Iterator, HighlightStyle)> { + let fade_out = HighlightStyle { + fade_out: Some(0.35), + ..Default::default() + }; + + let mut prev_end = label.filter_range.end; + label + .runs + .iter() + .enumerate() + .flat_map(move |(ix, (range, highlight_id))| { + let style = if let Some(style) = highlight_id.style(syntax_theme) { + style + } else { + return Default::default(); + }; + let mut muted_style = style; + muted_style.highlight(fade_out); + + let mut runs = SmallVec::<[(Range, HighlightStyle); 3]>::new(); + if range.start >= label.filter_range.end { + if range.start > prev_end { + runs.push((prev_end..range.start, fade_out)); + } + runs.push((range.clone(), muted_style)); + } else if range.end <= label.filter_range.end { + runs.push((range.clone(), style)); + } else { + runs.push((range.start..label.filter_range.end, style)); + runs.push((label.filter_range.end..range.end, muted_style)); + } + prev_end = cmp::max(prev_end, range.end); + + if ix + 1 == label.runs.len() && label.text.len() > prev_end { + runs.push((prev_end..label.text.len(), fade_out)); + } + + runs + }) +} + +pub(crate) fn split_words(text: &str) -> impl std::iter::Iterator + '_ { + let mut prev_index = 0; + let mut prev_codepoint: Option = None; + text.char_indices() + .chain([(text.len(), '\0')]) + .filter_map(move |(index, codepoint)| { + let prev_codepoint = prev_codepoint.replace(codepoint)?; + let is_boundary = index == text.len() + || !prev_codepoint.is_uppercase() && codepoint.is_uppercase() + || !prev_codepoint.is_alphanumeric() && codepoint.is_alphanumeric(); + if is_boundary { + let chunk = &text[prev_index..index]; + prev_index = index; + Some(chunk) + } else { + None + } + }) +} + +pub trait RangeToAnchorExt: Sized { + fn to_anchors(self, snapshot: &MultiBufferSnapshot) -> Range; + + fn to_display_points(self, snapshot: &EditorSnapshot) -> Range { + let anchor_range = self.to_anchors(&snapshot.buffer_snapshot); + anchor_range.start.to_display_point(snapshot)..anchor_range.end.to_display_point(snapshot) + } +} + +impl RangeToAnchorExt for Range { + fn to_anchors(self, snapshot: &MultiBufferSnapshot) -> Range { + let start_offset = self.start.to_offset(snapshot); + let end_offset = self.end.to_offset(snapshot); + if start_offset == end_offset { + snapshot.anchor_before(start_offset)..snapshot.anchor_before(end_offset) + } else { + snapshot.anchor_after(self.start)..snapshot.anchor_before(self.end) + } + } +} + +pub trait RowExt { + fn as_f32(&self) -> f32; + + fn next_row(&self) -> Self; + + fn previous_row(&self) -> Self; + + fn minus(&self, other: Self) -> u32; +} + +impl RowExt for DisplayRow { + fn as_f32(&self) -> f32 { + self.0 as f32 + } + + fn next_row(&self) -> Self { + Self(self.0 + 1) + } + + fn previous_row(&self) -> Self { + Self(self.0.saturating_sub(1)) + } + + fn minus(&self, other: Self) -> u32 { + self.0 - other.0 + } +} + +impl RowExt for MultiBufferRow { + fn as_f32(&self) -> f32 { + self.0 as f32 + } + + fn next_row(&self) -> Self { + Self(self.0 + 1) + } + + fn previous_row(&self) -> Self { + Self(self.0.saturating_sub(1)) + } + + fn minus(&self, other: Self) -> u32 { + self.0 - other.0 + } +} + +trait RowRangeExt { + type Row; + + fn len(&self) -> usize; + + fn iter_rows(&self) -> impl DoubleEndedIterator; +} + +impl RowRangeExt for Range { + type Row = MultiBufferRow; + + fn len(&self) -> usize { + (self.end.0 - self.start.0) as usize + } + + fn iter_rows(&self) -> impl DoubleEndedIterator { + (self.start.0..self.end.0).map(MultiBufferRow) + } +} + +impl RowRangeExt for Range { + type Row = DisplayRow; + + fn len(&self) -> usize { + (self.end.0 - self.start.0) as usize + } + + fn iter_rows(&self) -> impl DoubleEndedIterator { + (self.start.0..self.end.0).map(DisplayRow) + } +} + +/// If select range has more than one line, we +/// just point the cursor to range.start. +fn collapse_multiline_range(range: Range) -> Range { + if range.start.row == range.end.row { + range + } else { + range.start..range.start + } +} +pub struct KillRing(ClipboardItem); +impl Global for KillRing {} + +const UPDATE_DEBOUNCE: Duration = Duration::from_millis(50); + +enum BreakpointPromptEditAction { + Log, + Condition, + HitCondition, +} + +struct BreakpointPromptEditor { + pub(crate) prompt: Entity, + editor: WeakEntity, + breakpoint_anchor: Anchor, + breakpoint: Breakpoint, + edit_action: BreakpointPromptEditAction, + block_ids: HashSet, + gutter_dimensions: Arc>, + _subscriptions: Vec, +} + +impl BreakpointPromptEditor { + const MAX_LINES: u8 = 4; + + fn new( + editor: WeakEntity, + breakpoint_anchor: Anchor, + breakpoint: Breakpoint, + edit_action: BreakpointPromptEditAction, + window: &mut Window, + cx: &mut Context, + ) -> Self { + let base_text = match edit_action { + BreakpointPromptEditAction::Log => breakpoint.message.as_ref(), + BreakpointPromptEditAction::Condition => breakpoint.condition.as_ref(), + BreakpointPromptEditAction::HitCondition => breakpoint.hit_condition.as_ref(), + } + .map(|msg| msg.to_string()) + .unwrap_or_default(); + + let buffer = cx.new(|cx| Buffer::local(base_text, cx)); + let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); + + let prompt = cx.new(|cx| { + let mut prompt = Editor::new( + EditorMode::AutoHeight { + max_lines: Self::MAX_LINES as usize, + }, + buffer, + None, + window, + cx, + ); + prompt.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx); + prompt.set_show_cursor_when_unfocused(false, cx); + prompt.set_placeholder_text( + match edit_action { + BreakpointPromptEditAction::Log => "Message to log when a breakpoint is hit. Expressions within {} are interpolated.", + BreakpointPromptEditAction::Condition => "Condition when a breakpoint is hit. Expressions within {} are interpolated.", + BreakpointPromptEditAction::HitCondition => "How many breakpoint hits to ignore", + }, + cx, + ); + + prompt + }); + + Self { + prompt, + editor, + breakpoint_anchor, + breakpoint, + edit_action, + gutter_dimensions: Arc::new(Mutex::new(GutterDimensions::default())), + block_ids: Default::default(), + _subscriptions: vec![], + } + } + + pub(crate) fn add_block_ids(&mut self, block_ids: Vec) { + self.block_ids.extend(block_ids) + } + + fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context) { + if let Some(editor) = self.editor.upgrade() { + let message = self + .prompt + .read(cx) + .buffer + .read(cx) + .as_singleton() + .expect("A multi buffer in breakpoint prompt isn't possible") + .read(cx) + .as_rope() + .to_string(); + + editor.update(cx, |editor, cx| { + editor.edit_breakpoint_at_anchor( + self.breakpoint_anchor, + self.breakpoint.clone(), + match self.edit_action { + BreakpointPromptEditAction::Log => { + BreakpointEditAction::EditLogMessage(message.into()) + } + BreakpointPromptEditAction::Condition => { + BreakpointEditAction::EditCondition(message.into()) + } + BreakpointPromptEditAction::HitCondition => { + BreakpointEditAction::EditHitCondition(message.into()) + } + }, + cx, + ); + + editor.remove_blocks(self.block_ids.clone(), None, cx); + cx.focus_self(window); + }); + } + } + + fn cancel(&mut self, _: &menu::Cancel, window: &mut Window, cx: &mut Context) { + self.editor + .update(cx, |editor, cx| { + editor.remove_blocks(self.block_ids.clone(), None, cx); + window.focus(&editor.focus_handle); + }) + .log_err(); + } + + fn render_prompt_editor(&self, cx: &mut Context) -> impl IntoElement { + let settings = ThemeSettings::get_global(cx); + let text_style = TextStyle { + color: if self.prompt.read(cx).read_only(cx) { + cx.theme().colors().text_disabled + } else { + cx.theme().colors().text + }, + font_family: settings.buffer_font.family.clone(), + font_fallbacks: settings.buffer_font.fallbacks.clone(), + font_size: settings.buffer_font_size(cx).into(), + font_weight: settings.buffer_font.weight, + line_height: relative(settings.buffer_line_height.value()), + ..Default::default() + }; + EditorElement::new( + &self.prompt, + EditorStyle { + background: cx.theme().colors().editor_background, + local_player: cx.theme().players().local(), + text: text_style, + ..Default::default() + }, + ) + } +} + +impl Render for BreakpointPromptEditor { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let gutter_dimensions = *self.gutter_dimensions.lock(); + h_flex() + .key_context("Editor") + .bg(cx.theme().colors().editor_background) + .border_y_1() + .border_color(cx.theme().status().info_border) + .size_full() + .py(window.line_height() / 2.5) + .on_action(cx.listener(Self::confirm)) + .on_action(cx.listener(Self::cancel)) + .child(h_flex().w(gutter_dimensions.full_width() + (gutter_dimensions.margin / 2.0))) + .child(div().flex_1().child(self.render_prompt_editor(cx))) + } +} + +impl Focusable for BreakpointPromptEditor { + fn focus_handle(&self, cx: &App) -> FocusHandle { + self.prompt.focus_handle(cx) + } +} + +fn all_edits_insertions_or_deletions( + edits: &Vec<(Range, String)>, + snapshot: &MultiBufferSnapshot, +) -> bool { + let mut all_insertions = true; + let mut all_deletions = true; + + for (range, new_text) in edits.iter() { + let range_is_empty = range.to_offset(&snapshot).is_empty(); + let text_is_empty = new_text.is_empty(); + + if range_is_empty != text_is_empty { + if range_is_empty { + all_deletions = false; + } else { + all_insertions = false; + } + } else { + return false; + } + + if !all_insertions && !all_deletions { + return false; + } + } + all_insertions || all_deletions +} + +struct MissingEditPredictionKeybindingTooltip; + +impl Render for MissingEditPredictionKeybindingTooltip { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + ui::tooltip_container(window, cx, |container, _, cx| { + container + .flex_shrink_0() + .max_w_80() + .min_h(rems_from_px(124.)) + .justify_between() + .child( + v_flex() + .flex_1() + .text_ui_sm(cx) + .child(Label::new("Conflict with Accept Keybinding")) + .child("Your keymap currently overrides the default accept keybinding. To continue, assign one keybinding for the `editor::AcceptEditPrediction` action.") + ) + .child( + h_flex() + .pb_1() + .gap_1() + .items_end() + .w_full() + .child(Button::new("open-keymap", "Assign Keybinding").size(ButtonSize::Compact).on_click(|_ev, window, cx| { + window.dispatch_action(zed_actions::OpenKeymap.boxed_clone(), cx) + })) + .child(Button::new("see-docs", "See Docs").size(ButtonSize::Compact).on_click(|_ev, _window, cx| { + cx.open_url("https://zed.dev/docs/completions#edit-predictions-missing-keybinding"); + })), + ) + }) + } +} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct LineHighlight { + pub background: Background, + pub border: Option, + pub include_gutter: bool, + pub type_id: Option, +} + +fn render_diff_hunk_controls( + row: u32, + status: &DiffHunkStatus, + hunk_range: Range, + is_created_file: bool, + line_height: Pixels, + editor: &Entity, + _window: &mut Window, + cx: &mut App, +) -> AnyElement { + h_flex() + .h(line_height) + .mr_1() + .gap_1() + .px_0p5() + .pb_1() + .border_x_1() + .border_b_1() + .border_color(cx.theme().colors().border_variant) + .rounded_b_lg() + .bg(cx.theme().colors().editor_background) + .gap_1() + .occlude() + .shadow_md() + .child(if status.has_secondary_hunk() { + Button::new(("stage", row as u64), "Stage") + .alpha(if status.is_pending() { 0.66 } else { 1.0 }) + .tooltip({ + let focus_handle = editor.focus_handle(cx); + move |window, cx| { + Tooltip::for_action_in( + "Stage Hunk", + &::git::ToggleStaged, + &focus_handle, + window, + cx, + ) + } + }) + .on_click({ + let editor = editor.clone(); + move |_event, _window, cx| { + editor.update(cx, |editor, cx| { + editor.stage_or_unstage_diff_hunks( + true, + vec![hunk_range.start..hunk_range.start], + cx, + ); + }); + } + }) + } else { + Button::new(("unstage", row as u64), "Unstage") + .alpha(if status.is_pending() { 0.66 } else { 1.0 }) + .tooltip({ + let focus_handle = editor.focus_handle(cx); + move |window, cx| { + Tooltip::for_action_in( + "Unstage Hunk", + &::git::ToggleStaged, + &focus_handle, + window, + cx, + ) + } + }) + .on_click({ + let editor = editor.clone(); + move |_event, _window, cx| { + editor.update(cx, |editor, cx| { + editor.stage_or_unstage_diff_hunks( + false, + vec![hunk_range.start..hunk_range.start], + cx, + ); + }); + } + }) + }) + .child( + Button::new(("restore", row as u64), "Restore") + .tooltip({ + let focus_handle = editor.focus_handle(cx); + move |window, cx| { + Tooltip::for_action_in( + "Restore Hunk", + &::git::Restore, + &focus_handle, + window, + cx, + ) + } + }) + .on_click({ + let editor = editor.clone(); + move |_event, window, cx| { + editor.update(cx, |editor, cx| { + let snapshot = editor.snapshot(window, cx); + let point = hunk_range.start.to_point(&snapshot.buffer_snapshot); + editor.restore_hunks_in_ranges(vec![point..point], window, cx); + }); + } + }) + .disabled(is_created_file), + ) + .when( + !editor.read(cx).buffer().read(cx).all_diff_hunks_expanded(), + |el| { + el.child( + IconButton::new(("next-hunk", row as u64), IconName::ArrowDown) + .shape(IconButtonShape::Square) + .icon_size(IconSize::Small) + // .disabled(!has_multiple_hunks) + .tooltip({ + let focus_handle = editor.focus_handle(cx); + move |window, cx| { + Tooltip::for_action_in( + "Next Hunk", + &GoToHunk, + &focus_handle, + window, + cx, + ) + } + }) + .on_click({ + let editor = editor.clone(); + move |_event, window, cx| { + editor.update(cx, |editor, cx| { + let snapshot = editor.snapshot(window, cx); + let position = + hunk_range.end.to_point(&snapshot.buffer_snapshot); + editor.go_to_hunk_before_or_after_position( + &snapshot, + position, + Direction::Next, + window, + cx, + ); + editor.expand_selected_diff_hunks(cx); + }); + } + }), + ) + .child( + IconButton::new(("prev-hunk", row as u64), IconName::ArrowUp) + .shape(IconButtonShape::Square) + .icon_size(IconSize::Small) + // .disabled(!has_multiple_hunks) + .tooltip({ + let focus_handle = editor.focus_handle(cx); + move |window, cx| { + Tooltip::for_action_in( + "Previous Hunk", + &GoToPreviousHunk, + &focus_handle, + window, + cx, + ) + } + }) + .on_click({ + let editor = editor.clone(); + move |_event, window, cx| { + editor.update(cx, |editor, cx| { + let snapshot = editor.snapshot(window, cx); + let point = + hunk_range.start.to_point(&snapshot.buffer_snapshot); + editor.go_to_hunk_before_or_after_position( + &snapshot, + point, + Direction::Prev, + window, + cx, + ); + editor.expand_selected_diff_hunks(cx); + }); + } + }), + ) + }, + ) + .into_any_element() +} diff --git a/crates/assistant_tools/src/edit_agent/evals/fixtures/extract_handle_command_output/after.rs b/crates/assistant_tools/src/edit_agent/evals/fixtures/extract_handle_command_output/after.rs new file mode 100644 index 0000000000..4640fab1e8 --- /dev/null +++ b/crates/assistant_tools/src/edit_agent/evals/fixtures/extract_handle_command_output/after.rs @@ -0,0 +1,378 @@ +use crate::commit::get_messages; +use crate::{GitRemote, Oid}; +use anyhow::{Context as _, Result, anyhow}; +use collections::{HashMap, HashSet}; +use futures::AsyncWriteExt; +use gpui::SharedString; +use serde::{Deserialize, Serialize}; +use std::process::Stdio; +use std::{ops::Range, path::Path}; +use text::Rope; +use time::OffsetDateTime; +use time::UtcOffset; +use time::macros::format_description; + +pub use git2 as libgit; + +#[derive(Debug, Clone, Default)] +pub struct Blame { + pub entries: Vec, + pub messages: HashMap, + pub remote_url: Option, +} + +#[derive(Clone, Debug, Default)] +pub struct ParsedCommitMessage { + pub message: SharedString, + pub permalink: Option, + pub pull_request: Option, + pub remote: Option, +} + +impl Blame { + pub async fn for_path( + git_binary: &Path, + working_directory: &Path, + path: &Path, + content: &Rope, + remote_url: Option, + ) -> Result { + let output = run_git_blame(git_binary, working_directory, path, content).await?; + let mut entries = parse_git_blame(&output)?; + entries.sort_unstable_by(|a, b| a.range.start.cmp(&b.range.start)); + + let mut unique_shas = HashSet::default(); + + for entry in entries.iter_mut() { + unique_shas.insert(entry.sha); + } + + let shas = unique_shas.into_iter().collect::>(); + let messages = get_messages(working_directory, &shas) + .await + .context("failed to get commit messages")?; + + Ok(Self { + entries, + messages, + remote_url, + }) + } +} + +const GIT_BLAME_NO_COMMIT_ERROR: &str = "fatal: no such ref: HEAD"; +const GIT_BLAME_NO_PATH: &str = "fatal: no such path"; + +async fn run_git_blame( + git_binary: &Path, + working_directory: &Path, + path: &Path, + contents: &Rope, +) -> Result { + let mut child = util::command::new_smol_command(git_binary) + .current_dir(working_directory) + .arg("blame") + .arg("--incremental") + .arg("--contents") + .arg("-") + .arg(path.as_os_str()) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .map_err(|e| anyhow!("Failed to start git blame process: {}", e))?; + + let stdin = child + .stdin + .as_mut() + .context("failed to get pipe to stdin of git blame command")?; + + for chunk in contents.chunks() { + stdin.write_all(chunk.as_bytes()).await?; + } + stdin.flush().await?; + + let output = child + .output() + .await + .map_err(|e| anyhow!("Failed to read git blame output: {}", e))?; + + handle_command_output(output) +} + +fn handle_command_output(output: std::process::Output) -> Result { + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + let trimmed = stderr.trim(); + if trimmed == GIT_BLAME_NO_COMMIT_ERROR || trimmed.contains(GIT_BLAME_NO_PATH) { + return Ok(String::new()); + } + return Err(anyhow!("git blame process failed: {}", stderr)); + } + + Ok(String::from_utf8(output.stdout)?) +} + +#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)] +pub struct BlameEntry { + pub sha: Oid, + + pub range: Range, + + pub original_line_number: u32, + + pub author: Option, + pub author_mail: Option, + pub author_time: Option, + pub author_tz: Option, + + pub committer_name: Option, + pub committer_email: Option, + pub committer_time: Option, + pub committer_tz: Option, + + pub summary: Option, + + pub previous: Option, + pub filename: String, +} + +impl BlameEntry { + // Returns a BlameEntry by parsing the first line of a `git blame --incremental` + // entry. The line MUST have this format: + // + // <40-byte-hex-sha1> + fn new_from_blame_line(line: &str) -> Result { + let mut parts = line.split_whitespace(); + + let sha = parts + .next() + .and_then(|line| line.parse::().ok()) + .ok_or_else(|| anyhow!("failed to parse sha"))?; + + let original_line_number = parts + .next() + .and_then(|line| line.parse::().ok()) + .ok_or_else(|| anyhow!("Failed to parse original line number"))?; + let final_line_number = parts + .next() + .and_then(|line| line.parse::().ok()) + .ok_or_else(|| anyhow!("Failed to parse final line number"))?; + + let line_count = parts + .next() + .and_then(|line| line.parse::().ok()) + .ok_or_else(|| anyhow!("Failed to parse final line number"))?; + + let start_line = final_line_number.saturating_sub(1); + let end_line = start_line + line_count; + let range = start_line..end_line; + + Ok(Self { + sha, + range, + original_line_number, + ..Default::default() + }) + } + + pub fn author_offset_date_time(&self) -> Result { + if let (Some(author_time), Some(author_tz)) = (self.author_time, &self.author_tz) { + let format = format_description!("[offset_hour][offset_minute]"); + let offset = UtcOffset::parse(author_tz, &format)?; + let date_time_utc = OffsetDateTime::from_unix_timestamp(author_time)?; + + Ok(date_time_utc.to_offset(offset)) + } else { + // Directly return current time in UTC if there's no committer time or timezone + Ok(time::OffsetDateTime::now_utc()) + } + } +} + +// parse_git_blame parses the output of `git blame --incremental`, which returns +// all the blame-entries for a given path incrementally, as it finds them. +// +// Each entry *always* starts with: +// +// <40-byte-hex-sha1> +// +// Each entry *always* ends with: +// +// filename +// +// Line numbers are 1-indexed. +// +// A `git blame --incremental` entry looks like this: +// +// 6ad46b5257ba16d12c5ca9f0d4900320959df7f4 2 2 1 +// author Joe Schmoe +// author-mail +// author-time 1709741400 +// author-tz +0100 +// committer Joe Schmoe +// committer-mail +// committer-time 1709741400 +// committer-tz +0100 +// summary Joe's cool commit +// previous 486c2409237a2c627230589e567024a96751d475 index.js +// filename index.js +// +// If the entry has the same SHA as an entry that was already printed then no +// signature information is printed: +// +// 6ad46b5257ba16d12c5ca9f0d4900320959df7f4 3 4 1 +// previous 486c2409237a2c627230589e567024a96751d475 index.js +// filename index.js +// +// More about `--incremental` output: https://mirrors.edge.kernel.org/pub/software/scm/git/docs/git-blame.html +fn parse_git_blame(output: &str) -> Result> { + let mut entries: Vec = Vec::new(); + let mut index: HashMap = HashMap::default(); + + let mut current_entry: Option = None; + + for line in output.lines() { + let mut done = false; + + match &mut current_entry { + None => { + let mut new_entry = BlameEntry::new_from_blame_line(line)?; + + if let Some(existing_entry) = index + .get(&new_entry.sha) + .and_then(|slot| entries.get(*slot)) + { + new_entry.author.clone_from(&existing_entry.author); + new_entry + .author_mail + .clone_from(&existing_entry.author_mail); + new_entry.author_time = existing_entry.author_time; + new_entry.author_tz.clone_from(&existing_entry.author_tz); + new_entry + .committer_name + .clone_from(&existing_entry.committer_name); + new_entry + .committer_email + .clone_from(&existing_entry.committer_email); + new_entry.committer_time = existing_entry.committer_time; + new_entry + .committer_tz + .clone_from(&existing_entry.committer_tz); + new_entry.summary.clone_from(&existing_entry.summary); + } + + current_entry.replace(new_entry); + } + Some(entry) => { + let Some((key, value)) = line.split_once(' ') else { + continue; + }; + let is_committed = !entry.sha.is_zero(); + match key { + "filename" => { + entry.filename = value.into(); + done = true; + } + "previous" => entry.previous = Some(value.into()), + + "summary" if is_committed => entry.summary = Some(value.into()), + "author" if is_committed => entry.author = Some(value.into()), + "author-mail" if is_committed => entry.author_mail = Some(value.into()), + "author-time" if is_committed => { + entry.author_time = Some(value.parse::()?) + } + "author-tz" if is_committed => entry.author_tz = Some(value.into()), + + "committer" if is_committed => entry.committer_name = Some(value.into()), + "committer-mail" if is_committed => entry.committer_email = Some(value.into()), + "committer-time" if is_committed => { + entry.committer_time = Some(value.parse::()?) + } + "committer-tz" if is_committed => entry.committer_tz = Some(value.into()), + _ => {} + } + } + }; + + if done { + if let Some(entry) = current_entry.take() { + index.insert(entry.sha, entries.len()); + + // We only want annotations that have a commit. + if !entry.sha.is_zero() { + entries.push(entry); + } + } + } + } + + Ok(entries) +} + +#[cfg(test)] +mod tests { + use std::path::PathBuf; + + use super::BlameEntry; + use super::parse_git_blame; + + fn read_test_data(filename: &str) -> String { + let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + path.push("test_data"); + path.push(filename); + + std::fs::read_to_string(&path) + .unwrap_or_else(|_| panic!("Could not read test data at {:?}. Is it generated?", path)) + } + + fn assert_eq_golden(entries: &Vec, golden_filename: &str) { + let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + path.push("test_data"); + path.push("golden"); + path.push(format!("{}.json", golden_filename)); + + let mut have_json = + serde_json::to_string_pretty(&entries).expect("could not serialize entries to JSON"); + // We always want to save with a trailing newline. + have_json.push('\n'); + + let update = std::env::var("UPDATE_GOLDEN") + .map(|val| val.eq_ignore_ascii_case("true")) + .unwrap_or(false); + + if update { + std::fs::create_dir_all(path.parent().unwrap()) + .expect("could not create golden test data directory"); + std::fs::write(&path, have_json).expect("could not write out golden data"); + } else { + let want_json = + std::fs::read_to_string(&path).unwrap_or_else(|_| { + panic!("could not read golden test data file at {:?}. Did you run the test with UPDATE_GOLDEN=true before?", path); + }).replace("\r\n", "\n"); + + pretty_assertions::assert_eq!(have_json, want_json, "wrong blame entries"); + } + } + + #[test] + fn test_parse_git_blame_not_committed() { + let output = read_test_data("blame_incremental_not_committed"); + let entries = parse_git_blame(&output).unwrap(); + assert_eq_golden(&entries, "blame_incremental_not_committed"); + } + + #[test] + fn test_parse_git_blame_simple() { + let output = read_test_data("blame_incremental_simple"); + let entries = parse_git_blame(&output).unwrap(); + assert_eq_golden(&entries, "blame_incremental_simple"); + } + + #[test] + fn test_parse_git_blame_complex() { + let output = read_test_data("blame_incremental_complex"); + let entries = parse_git_blame(&output).unwrap(); + assert_eq_golden(&entries, "blame_incremental_complex"); + } +} diff --git a/crates/assistant_tools/src/edit_agent/evals/fixtures/extract_handle_command_output/before.rs b/crates/assistant_tools/src/edit_agent/evals/fixtures/extract_handle_command_output/before.rs new file mode 100644 index 0000000000..185acd4a82 --- /dev/null +++ b/crates/assistant_tools/src/edit_agent/evals/fixtures/extract_handle_command_output/before.rs @@ -0,0 +1,374 @@ +use crate::commit::get_messages; +use crate::{GitRemote, Oid}; +use anyhow::{Context as _, Result, anyhow}; +use collections::{HashMap, HashSet}; +use futures::AsyncWriteExt; +use gpui::SharedString; +use serde::{Deserialize, Serialize}; +use std::process::Stdio; +use std::{ops::Range, path::Path}; +use text::Rope; +use time::OffsetDateTime; +use time::UtcOffset; +use time::macros::format_description; + +pub use git2 as libgit; + +#[derive(Debug, Clone, Default)] +pub struct Blame { + pub entries: Vec, + pub messages: HashMap, + pub remote_url: Option, +} + +#[derive(Clone, Debug, Default)] +pub struct ParsedCommitMessage { + pub message: SharedString, + pub permalink: Option, + pub pull_request: Option, + pub remote: Option, +} + +impl Blame { + pub async fn for_path( + git_binary: &Path, + working_directory: &Path, + path: &Path, + content: &Rope, + remote_url: Option, + ) -> Result { + let output = run_git_blame(git_binary, working_directory, path, content).await?; + let mut entries = parse_git_blame(&output)?; + entries.sort_unstable_by(|a, b| a.range.start.cmp(&b.range.start)); + + let mut unique_shas = HashSet::default(); + + for entry in entries.iter_mut() { + unique_shas.insert(entry.sha); + } + + let shas = unique_shas.into_iter().collect::>(); + let messages = get_messages(working_directory, &shas) + .await + .context("failed to get commit messages")?; + + Ok(Self { + entries, + messages, + remote_url, + }) + } +} + +const GIT_BLAME_NO_COMMIT_ERROR: &str = "fatal: no such ref: HEAD"; +const GIT_BLAME_NO_PATH: &str = "fatal: no such path"; + +async fn run_git_blame( + git_binary: &Path, + working_directory: &Path, + path: &Path, + contents: &Rope, +) -> Result { + let mut child = util::command::new_smol_command(git_binary) + .current_dir(working_directory) + .arg("blame") + .arg("--incremental") + .arg("--contents") + .arg("-") + .arg(path.as_os_str()) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .map_err(|e| anyhow!("Failed to start git blame process: {}", e))?; + + let stdin = child + .stdin + .as_mut() + .context("failed to get pipe to stdin of git blame command")?; + + for chunk in contents.chunks() { + stdin.write_all(chunk.as_bytes()).await?; + } + stdin.flush().await?; + + let output = child + .output() + .await + .map_err(|e| anyhow!("Failed to read git blame output: {}", e))?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + let trimmed = stderr.trim(); + if trimmed == GIT_BLAME_NO_COMMIT_ERROR || trimmed.contains(GIT_BLAME_NO_PATH) { + return Ok(String::new()); + } + return Err(anyhow!("git blame process failed: {}", stderr)); + } + + Ok(String::from_utf8(output.stdout)?) +} + +#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)] +pub struct BlameEntry { + pub sha: Oid, + + pub range: Range, + + pub original_line_number: u32, + + pub author: Option, + pub author_mail: Option, + pub author_time: Option, + pub author_tz: Option, + + pub committer_name: Option, + pub committer_email: Option, + pub committer_time: Option, + pub committer_tz: Option, + + pub summary: Option, + + pub previous: Option, + pub filename: String, +} + +impl BlameEntry { + // Returns a BlameEntry by parsing the first line of a `git blame --incremental` + // entry. The line MUST have this format: + // + // <40-byte-hex-sha1> + fn new_from_blame_line(line: &str) -> Result { + let mut parts = line.split_whitespace(); + + let sha = parts + .next() + .and_then(|line| line.parse::().ok()) + .ok_or_else(|| anyhow!("failed to parse sha"))?; + + let original_line_number = parts + .next() + .and_then(|line| line.parse::().ok()) + .ok_or_else(|| anyhow!("Failed to parse original line number"))?; + let final_line_number = parts + .next() + .and_then(|line| line.parse::().ok()) + .ok_or_else(|| anyhow!("Failed to parse final line number"))?; + + let line_count = parts + .next() + .and_then(|line| line.parse::().ok()) + .ok_or_else(|| anyhow!("Failed to parse final line number"))?; + + let start_line = final_line_number.saturating_sub(1); + let end_line = start_line + line_count; + let range = start_line..end_line; + + Ok(Self { + sha, + range, + original_line_number, + ..Default::default() + }) + } + + pub fn author_offset_date_time(&self) -> Result { + if let (Some(author_time), Some(author_tz)) = (self.author_time, &self.author_tz) { + let format = format_description!("[offset_hour][offset_minute]"); + let offset = UtcOffset::parse(author_tz, &format)?; + let date_time_utc = OffsetDateTime::from_unix_timestamp(author_time)?; + + Ok(date_time_utc.to_offset(offset)) + } else { + // Directly return current time in UTC if there's no committer time or timezone + Ok(time::OffsetDateTime::now_utc()) + } + } +} + +// parse_git_blame parses the output of `git blame --incremental`, which returns +// all the blame-entries for a given path incrementally, as it finds them. +// +// Each entry *always* starts with: +// +// <40-byte-hex-sha1> +// +// Each entry *always* ends with: +// +// filename +// +// Line numbers are 1-indexed. +// +// A `git blame --incremental` entry looks like this: +// +// 6ad46b5257ba16d12c5ca9f0d4900320959df7f4 2 2 1 +// author Joe Schmoe +// author-mail +// author-time 1709741400 +// author-tz +0100 +// committer Joe Schmoe +// committer-mail +// committer-time 1709741400 +// committer-tz +0100 +// summary Joe's cool commit +// previous 486c2409237a2c627230589e567024a96751d475 index.js +// filename index.js +// +// If the entry has the same SHA as an entry that was already printed then no +// signature information is printed: +// +// 6ad46b5257ba16d12c5ca9f0d4900320959df7f4 3 4 1 +// previous 486c2409237a2c627230589e567024a96751d475 index.js +// filename index.js +// +// More about `--incremental` output: https://mirrors.edge.kernel.org/pub/software/scm/git/docs/git-blame.html +fn parse_git_blame(output: &str) -> Result> { + let mut entries: Vec = Vec::new(); + let mut index: HashMap = HashMap::default(); + + let mut current_entry: Option = None; + + for line in output.lines() { + let mut done = false; + + match &mut current_entry { + None => { + let mut new_entry = BlameEntry::new_from_blame_line(line)?; + + if let Some(existing_entry) = index + .get(&new_entry.sha) + .and_then(|slot| entries.get(*slot)) + { + new_entry.author.clone_from(&existing_entry.author); + new_entry + .author_mail + .clone_from(&existing_entry.author_mail); + new_entry.author_time = existing_entry.author_time; + new_entry.author_tz.clone_from(&existing_entry.author_tz); + new_entry + .committer_name + .clone_from(&existing_entry.committer_name); + new_entry + .committer_email + .clone_from(&existing_entry.committer_email); + new_entry.committer_time = existing_entry.committer_time; + new_entry + .committer_tz + .clone_from(&existing_entry.committer_tz); + new_entry.summary.clone_from(&existing_entry.summary); + } + + current_entry.replace(new_entry); + } + Some(entry) => { + let Some((key, value)) = line.split_once(' ') else { + continue; + }; + let is_committed = !entry.sha.is_zero(); + match key { + "filename" => { + entry.filename = value.into(); + done = true; + } + "previous" => entry.previous = Some(value.into()), + + "summary" if is_committed => entry.summary = Some(value.into()), + "author" if is_committed => entry.author = Some(value.into()), + "author-mail" if is_committed => entry.author_mail = Some(value.into()), + "author-time" if is_committed => { + entry.author_time = Some(value.parse::()?) + } + "author-tz" if is_committed => entry.author_tz = Some(value.into()), + + "committer" if is_committed => entry.committer_name = Some(value.into()), + "committer-mail" if is_committed => entry.committer_email = Some(value.into()), + "committer-time" if is_committed => { + entry.committer_time = Some(value.parse::()?) + } + "committer-tz" if is_committed => entry.committer_tz = Some(value.into()), + _ => {} + } + } + }; + + if done { + if let Some(entry) = current_entry.take() { + index.insert(entry.sha, entries.len()); + + // We only want annotations that have a commit. + if !entry.sha.is_zero() { + entries.push(entry); + } + } + } + } + + Ok(entries) +} + +#[cfg(test)] +mod tests { + use std::path::PathBuf; + + use super::BlameEntry; + use super::parse_git_blame; + + fn read_test_data(filename: &str) -> String { + let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + path.push("test_data"); + path.push(filename); + + std::fs::read_to_string(&path) + .unwrap_or_else(|_| panic!("Could not read test data at {:?}. Is it generated?", path)) + } + + fn assert_eq_golden(entries: &Vec, golden_filename: &str) { + let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + path.push("test_data"); + path.push("golden"); + path.push(format!("{}.json", golden_filename)); + + let mut have_json = + serde_json::to_string_pretty(&entries).expect("could not serialize entries to JSON"); + // We always want to save with a trailing newline. + have_json.push('\n'); + + let update = std::env::var("UPDATE_GOLDEN") + .map(|val| val.eq_ignore_ascii_case("true")) + .unwrap_or(false); + + if update { + std::fs::create_dir_all(path.parent().unwrap()) + .expect("could not create golden test data directory"); + std::fs::write(&path, have_json).expect("could not write out golden data"); + } else { + let want_json = + std::fs::read_to_string(&path).unwrap_or_else(|_| { + panic!("could not read golden test data file at {:?}. Did you run the test with UPDATE_GOLDEN=true before?", path); + }).replace("\r\n", "\n"); + + pretty_assertions::assert_eq!(have_json, want_json, "wrong blame entries"); + } + } + + #[test] + fn test_parse_git_blame_not_committed() { + let output = read_test_data("blame_incremental_not_committed"); + let entries = parse_git_blame(&output).unwrap(); + assert_eq_golden(&entries, "blame_incremental_not_committed"); + } + + #[test] + fn test_parse_git_blame_simple() { + let output = read_test_data("blame_incremental_simple"); + let entries = parse_git_blame(&output).unwrap(); + assert_eq_golden(&entries, "blame_incremental_simple"); + } + + #[test] + fn test_parse_git_blame_complex() { + let output = read_test_data("blame_incremental_complex"); + let entries = parse_git_blame(&output).unwrap(); + assert_eq_golden(&entries, "blame_incremental_complex"); + } +} diff --git a/crates/assistant_tools/src/edit_agent/evals/fixtures/from_pixels_constructor/before.rs b/crates/assistant_tools/src/edit_agent/evals/fixtures/from_pixels_constructor/before.rs new file mode 100644 index 0000000000..12590fe6e9 --- /dev/null +++ b/crates/assistant_tools/src/edit_agent/evals/fixtures/from_pixels_constructor/before.rs @@ -0,0 +1,339 @@ +// font-kit/src/canvas.rs +// +// Copyright © 2018 The Pathfinder Project Developers. +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +//! An in-memory bitmap surface for glyph rasterization. + +use lazy_static::lazy_static; +use pathfinder_geometry::rect::RectI; +use pathfinder_geometry::vector::Vector2I; +use std::cmp; +use std::fmt; + +use crate::utils; + +lazy_static! { + static ref BITMAP_1BPP_TO_8BPP_LUT: [[u8; 8]; 256] = { + let mut lut = [[0; 8]; 256]; + for byte in 0..0x100 { + let mut value = [0; 8]; + for bit in 0..8 { + if (byte & (0x80 >> bit)) != 0 { + value[bit] = 0xff; + } + } + lut[byte] = value + } + lut + }; +} + +/// An in-memory bitmap surface for glyph rasterization. +pub struct Canvas { + /// The raw pixel data. + pub pixels: Vec, + /// The size of the buffer, in pixels. + pub size: Vector2I, + /// The number of *bytes* between successive rows. + pub stride: usize, + /// The image format of the canvas. + pub format: Format, +} + +impl Canvas { + /// Creates a new blank canvas with the given pixel size and format. + /// + /// Stride is automatically calculated from width. + /// + /// The canvas is initialized with transparent black (all values 0). + #[inline] + pub fn new(size: Vector2I, format: Format) -> Canvas { + Canvas::with_stride( + size, + size.x() as usize * format.bytes_per_pixel() as usize, + format, + ) + } + + /// Creates a new blank canvas with the given pixel size, stride (number of bytes between + /// successive rows), and format. + /// + /// The canvas is initialized with transparent black (all values 0). + pub fn with_stride(size: Vector2I, stride: usize, format: Format) -> Canvas { + Canvas { + pixels: vec![0; stride * size.y() as usize], + size, + stride, + format, + } + } + + #[allow(dead_code)] + pub(crate) fn blit_from_canvas(&mut self, src: &Canvas) { + self.blit_from( + Vector2I::default(), + &src.pixels, + src.size, + src.stride, + src.format, + ) + } + + /// Blits to a rectangle with origin at `dst_point` and size according to `src_size`. + /// If the target area overlaps the boundaries of the canvas, only the drawable region is blitted. + /// `dst_point` and `src_size` are specified in pixels. `src_stride` is specified in bytes. + /// `src_stride` must be equal or larger than the actual data length. + #[allow(dead_code)] + pub(crate) fn blit_from( + &mut self, + dst_point: Vector2I, + src_bytes: &[u8], + src_size: Vector2I, + src_stride: usize, + src_format: Format, + ) { + assert_eq!( + src_stride * src_size.y() as usize, + src_bytes.len(), + "Number of pixels in src_bytes does not match stride and size." + ); + assert!( + src_stride >= src_size.x() as usize * src_format.bytes_per_pixel() as usize, + "src_stride must be >= than src_size.x()" + ); + + let dst_rect = RectI::new(dst_point, src_size); + let dst_rect = dst_rect.intersection(RectI::new(Vector2I::default(), self.size)); + let dst_rect = match dst_rect { + Some(dst_rect) => dst_rect, + None => return, + }; + + match (self.format, src_format) { + (Format::A8, Format::A8) + | (Format::Rgb24, Format::Rgb24) + | (Format::Rgba32, Format::Rgba32) => { + self.blit_from_with::(dst_rect, src_bytes, src_stride, src_format) + } + (Format::A8, Format::Rgb24) => { + self.blit_from_with::(dst_rect, src_bytes, src_stride, src_format) + } + (Format::Rgb24, Format::A8) => { + self.blit_from_with::(dst_rect, src_bytes, src_stride, src_format) + } + (Format::Rgb24, Format::Rgba32) => self + .blit_from_with::(dst_rect, src_bytes, src_stride, src_format), + (Format::Rgba32, Format::Rgb24) => self + .blit_from_with::(dst_rect, src_bytes, src_stride, src_format), + (Format::Rgba32, Format::A8) | (Format::A8, Format::Rgba32) => unimplemented!(), + } + } + + #[allow(dead_code)] + pub(crate) fn blit_from_bitmap_1bpp( + &mut self, + dst_point: Vector2I, + src_bytes: &[u8], + src_size: Vector2I, + src_stride: usize, + ) { + if self.format != Format::A8 { + unimplemented!() + } + + let dst_rect = RectI::new(dst_point, src_size); + let dst_rect = dst_rect.intersection(RectI::new(Vector2I::default(), self.size)); + let dst_rect = match dst_rect { + Some(dst_rect) => dst_rect, + None => return, + }; + + let size = dst_rect.size(); + + let dest_bytes_per_pixel = self.format.bytes_per_pixel() as usize; + let dest_row_stride = size.x() as usize * dest_bytes_per_pixel; + let src_row_stride = utils::div_round_up(size.x() as usize, 8); + + for y in 0..size.y() { + let (dest_row_start, src_row_start) = ( + (y + dst_rect.origin_y()) as usize * self.stride + + dst_rect.origin_x() as usize * dest_bytes_per_pixel, + y as usize * src_stride, + ); + let dest_row_end = dest_row_start + dest_row_stride; + let src_row_end = src_row_start + src_row_stride; + let dest_row_pixels = &mut self.pixels[dest_row_start..dest_row_end]; + let src_row_pixels = &src_bytes[src_row_start..src_row_end]; + for x in 0..src_row_stride { + let pattern = &BITMAP_1BPP_TO_8BPP_LUT[src_row_pixels[x] as usize]; + let dest_start = x * 8; + let dest_end = cmp::min(dest_start + 8, dest_row_stride); + let src = &pattern[0..(dest_end - dest_start)]; + dest_row_pixels[dest_start..dest_end].clone_from_slice(src); + } + } + } + + /// Blits to area `rect` using the data given in the buffer `src_bytes`. + /// `src_stride` must be specified in bytes. + /// The dimensions of `rect` must be in pixels. + fn blit_from_with( + &mut self, + rect: RectI, + src_bytes: &[u8], + src_stride: usize, + src_format: Format, + ) { + let src_bytes_per_pixel = src_format.bytes_per_pixel() as usize; + let dest_bytes_per_pixel = self.format.bytes_per_pixel() as usize; + + for y in 0..rect.height() { + let (dest_row_start, src_row_start) = ( + (y + rect.origin_y()) as usize * self.stride + + rect.origin_x() as usize * dest_bytes_per_pixel, + y as usize * src_stride, + ); + let dest_row_end = dest_row_start + rect.width() as usize * dest_bytes_per_pixel; + let src_row_end = src_row_start + rect.width() as usize * src_bytes_per_pixel; + let dest_row_pixels = &mut self.pixels[dest_row_start..dest_row_end]; + let src_row_pixels = &src_bytes[src_row_start..src_row_end]; + B::blit(dest_row_pixels, src_row_pixels) + } + } +} + +impl fmt::Debug for Canvas { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_struct("Canvas") + .field("pixels", &self.pixels.len()) // Do not dump a vector content. + .field("size", &self.size) + .field("stride", &self.stride) + .field("format", &self.format) + .finish() + } +} + +/// The image format for the canvas. +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum Format { + /// Premultiplied R8G8B8A8, little-endian. + Rgba32, + /// R8G8B8, little-endian. + Rgb24, + /// A8. + A8, +} + +impl Format { + /// Returns the number of bits per pixel that this image format corresponds to. + #[inline] + pub fn bits_per_pixel(self) -> u8 { + match self { + Format::Rgba32 => 32, + Format::Rgb24 => 24, + Format::A8 => 8, + } + } + + /// Returns the number of color channels per pixel that this image format corresponds to. + #[inline] + pub fn components_per_pixel(self) -> u8 { + match self { + Format::Rgba32 => 4, + Format::Rgb24 => 3, + Format::A8 => 1, + } + } + + /// Returns the number of bits per color channel that this image format contains. + #[inline] + pub fn bits_per_component(self) -> u8 { + self.bits_per_pixel() / self.components_per_pixel() + } + + /// Returns the number of bytes per pixel that this image format corresponds to. + #[inline] + pub fn bytes_per_pixel(self) -> u8 { + self.bits_per_pixel() / 8 + } +} + +/// The antialiasing strategy that should be used when rasterizing glyphs. +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum RasterizationOptions { + /// "Black-and-white" rendering. Each pixel is either entirely on or off. + Bilevel, + /// Grayscale antialiasing. Only one channel is used. + GrayscaleAa, + /// Subpixel RGB antialiasing, for LCD screens. + SubpixelAa, +} + +trait Blit { + fn blit(dest: &mut [u8], src: &[u8]); +} + +struct BlitMemcpy; + +impl Blit for BlitMemcpy { + #[inline] + fn blit(dest: &mut [u8], src: &[u8]) { + dest.clone_from_slice(src) + } +} + +struct BlitRgb24ToA8; + +impl Blit for BlitRgb24ToA8 { + #[inline] + fn blit(dest: &mut [u8], src: &[u8]) { + // TODO(pcwalton): SIMD. + for (dest, src) in dest.iter_mut().zip(src.chunks(3)) { + *dest = src[1] + } + } +} + +struct BlitA8ToRgb24; + +impl Blit for BlitA8ToRgb24 { + #[inline] + fn blit(dest: &mut [u8], src: &[u8]) { + for (dest, src) in dest.chunks_mut(3).zip(src.iter()) { + dest[0] = *src; + dest[1] = *src; + dest[2] = *src; + } + } +} + +struct BlitRgba32ToRgb24; + +impl Blit for BlitRgba32ToRgb24 { + #[inline] + fn blit(dest: &mut [u8], src: &[u8]) { + // TODO(pcwalton): SIMD. + for (dest, src) in dest.chunks_mut(3).zip(src.chunks(4)) { + dest.copy_from_slice(&src[0..3]) + } + } +} + +struct BlitRgb24ToRgba32; + +impl Blit for BlitRgb24ToRgba32 { + fn blit(dest: &mut [u8], src: &[u8]) { + for (dest, src) in dest.chunks_mut(4).zip(src.chunks(3)) { + dest[0] = src[0]; + dest[1] = src[1]; + dest[2] = src[2]; + dest[3] = 255; + } + } +} diff --git a/crates/assistant_tools/src/edit_agent/evals/fixtures/use_wasi_sdk_in_compile_parser_to_wasm/before.rs b/crates/assistant_tools/src/edit_agent/evals/fixtures/use_wasi_sdk_in_compile_parser_to_wasm/before.rs new file mode 100644 index 0000000000..4607237190 --- /dev/null +++ b/crates/assistant_tools/src/edit_agent/evals/fixtures/use_wasi_sdk_in_compile_parser_to_wasm/before.rs @@ -0,0 +1,1643 @@ +#![doc = include_str!("../README.md")] +#![cfg_attr(docsrs, feature(doc_cfg))] + +#[cfg(any(feature = "tree-sitter-highlight", feature = "tree-sitter-tags"))] +use std::ops::Range; +#[cfg(feature = "tree-sitter-highlight")] +use std::sync::Mutex; +use std::{ + collections::HashMap, + env, + ffi::{OsStr, OsString}, + fs, + io::{BufRead, BufReader}, + mem, + path::{Path, PathBuf}, + process::Command, + sync::LazyLock, + time::SystemTime, +}; + +#[cfg(any(feature = "tree-sitter-highlight", feature = "tree-sitter-tags"))] +use anyhow::Error; +use anyhow::{Context, Result, anyhow}; +use etcetera::BaseStrategy as _; +use fs4::fs_std::FileExt; +use indoc::indoc; +use libloading::{Library, Symbol}; +use once_cell::unsync::OnceCell; +use path_slash::PathBufExt as _; +use regex::{Regex, RegexBuilder}; +use semver::Version; +use serde::{Deserialize, Deserializer, Serialize}; +use tree_sitter::Language; +#[cfg(any(feature = "tree-sitter-highlight", feature = "tree-sitter-tags"))] +use tree_sitter::QueryError; +#[cfg(feature = "tree-sitter-highlight")] +use tree_sitter::QueryErrorKind; +#[cfg(feature = "tree-sitter-highlight")] +use tree_sitter_highlight::HighlightConfiguration; +#[cfg(feature = "tree-sitter-tags")] +use tree_sitter_tags::{Error as TagsError, TagsConfiguration}; +use url::Url; + +static GRAMMAR_NAME_REGEX: LazyLock = + LazyLock::new(|| Regex::new(r#""name":\s*"(.*?)""#).unwrap()); + +pub const EMSCRIPTEN_TAG: &str = concat!("docker.io/emscripten/emsdk:", env!("EMSCRIPTEN_VERSION")); + +#[derive(Default, Deserialize, Serialize)] +pub struct Config { + #[serde(default)] + #[serde( + rename = "parser-directories", + deserialize_with = "deserialize_parser_directories" + )] + pub parser_directories: Vec, +} + +#[derive(Serialize, Deserialize, Clone, Default)] +#[serde(untagged)] +pub enum PathsJSON { + #[default] + Empty, + Single(PathBuf), + Multiple(Vec), +} + +impl PathsJSON { + fn into_vec(self) -> Option> { + match self { + Self::Empty => None, + Self::Single(s) => Some(vec![s]), + Self::Multiple(s) => Some(s), + } + } + + const fn is_empty(&self) -> bool { + matches!(self, Self::Empty) + } +} + +#[derive(Serialize, Deserialize, Clone)] +#[serde(untagged)] +pub enum PackageJSONAuthor { + String(String), + Object { + name: String, + email: Option, + url: Option, + }, +} + +#[derive(Serialize, Deserialize, Clone)] +#[serde(untagged)] +pub enum PackageJSONRepository { + String(String), + Object { url: String }, +} + +#[derive(Serialize, Deserialize)] +pub struct PackageJSON { + pub name: String, + pub version: Version, + pub description: Option, + pub author: Option, + pub maintainers: Option>, + pub license: Option, + pub repository: Option, + #[serde(default)] + #[serde(rename = "tree-sitter", skip_serializing_if = "Option::is_none")] + pub tree_sitter: Option>, +} + +fn default_path() -> PathBuf { + PathBuf::from(".") +} + +#[derive(Serialize, Deserialize, Clone)] +#[serde(rename_all = "kebab-case")] +pub struct LanguageConfigurationJSON { + #[serde(default = "default_path")] + pub path: PathBuf, + pub scope: Option, + pub file_types: Option>, + pub content_regex: Option, + pub first_line_regex: Option, + pub injection_regex: Option, + #[serde(default, skip_serializing_if = "PathsJSON::is_empty")] + pub highlights: PathsJSON, + #[serde(default, skip_serializing_if = "PathsJSON::is_empty")] + pub injections: PathsJSON, + #[serde(default, skip_serializing_if = "PathsJSON::is_empty")] + pub locals: PathsJSON, + #[serde(default, skip_serializing_if = "PathsJSON::is_empty")] + pub tags: PathsJSON, + #[serde(default, skip_serializing_if = "PathsJSON::is_empty")] + pub external_files: PathsJSON, +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub struct TreeSitterJSON { + #[serde(rename = "$schema")] + pub schema: Option, + pub grammars: Vec, + pub metadata: Metadata, + #[serde(default)] + pub bindings: Bindings, +} + +impl TreeSitterJSON { + pub fn from_file(path: &Path) -> Result { + Ok(serde_json::from_str(&fs::read_to_string( + path.join("tree-sitter.json"), + )?)?) + } + + #[must_use] + pub fn has_multiple_language_configs(&self) -> bool { + self.grammars.len() > 1 + } +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub struct Grammar { + pub name: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub camelcase: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub title: Option, + pub scope: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub path: Option, + #[serde(default, skip_serializing_if = "PathsJSON::is_empty")] + pub external_files: PathsJSON, + pub file_types: Option>, + #[serde(default, skip_serializing_if = "PathsJSON::is_empty")] + pub highlights: PathsJSON, + #[serde(default, skip_serializing_if = "PathsJSON::is_empty")] + pub injections: PathsJSON, + #[serde(default, skip_serializing_if = "PathsJSON::is_empty")] + pub locals: PathsJSON, + #[serde(default, skip_serializing_if = "PathsJSON::is_empty")] + pub tags: PathsJSON, + #[serde(skip_serializing_if = "Option::is_none")] + pub injection_regex: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub first_line_regex: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub content_regex: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub class_name: Option, +} + +#[derive(Serialize, Deserialize)] +pub struct Metadata { + pub version: Version, + #[serde(skip_serializing_if = "Option::is_none")] + pub license: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub authors: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub links: Option, + #[serde(skip)] + pub namespace: Option, +} + +#[derive(Serialize, Deserialize)] +pub struct Author { + pub name: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub email: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub url: Option, +} + +#[derive(Serialize, Deserialize)] +pub struct Links { + pub repository: Url, + #[serde(skip_serializing_if = "Option::is_none")] + pub funding: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub homepage: Option, +} + +#[derive(Serialize, Deserialize)] +#[serde(default)] +pub struct Bindings { + pub c: bool, + pub go: bool, + #[serde(skip)] + pub java: bool, + #[serde(skip)] + pub kotlin: bool, + pub node: bool, + pub python: bool, + pub rust: bool, + pub swift: bool, + pub zig: bool, +} + +impl Default for Bindings { + fn default() -> Self { + Self { + c: true, + go: true, + java: false, + kotlin: false, + node: true, + python: true, + rust: true, + swift: true, + zig: false, + } + } +} + +// Replace `~` or `$HOME` with home path string. +// (While paths like "~/.tree-sitter/config.json" can be deserialized, +// they're not valid path for I/O modules.) +fn deserialize_parser_directories<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let paths = Vec::::deserialize(deserializer)?; + let Ok(home) = etcetera::home_dir() else { + return Ok(paths); + }; + let standardized = paths + .into_iter() + .map(|path| standardize_path(path, &home)) + .collect(); + Ok(standardized) +} + +fn standardize_path(path: PathBuf, home: &Path) -> PathBuf { + if let Ok(p) = path.strip_prefix("~") { + return home.join(p); + } + if let Ok(p) = path.strip_prefix("$HOME") { + return home.join(p); + } + path +} + +impl Config { + #[must_use] + pub fn initial() -> Self { + let home_dir = etcetera::home_dir().expect("Cannot determine home directory"); + Self { + parser_directories: vec![ + home_dir.join("github"), + home_dir.join("src"), + home_dir.join("source"), + home_dir.join("projects"), + home_dir.join("dev"), + home_dir.join("git"), + ], + } + } +} + +const BUILD_TARGET: &str = env!("BUILD_TARGET"); +const BUILD_HOST: &str = env!("BUILD_HOST"); + +pub struct LanguageConfiguration<'a> { + pub scope: Option, + pub content_regex: Option, + pub first_line_regex: Option, + pub injection_regex: Option, + pub file_types: Vec, + pub root_path: PathBuf, + pub highlights_filenames: Option>, + pub injections_filenames: Option>, + pub locals_filenames: Option>, + pub tags_filenames: Option>, + pub language_name: String, + language_id: usize, + #[cfg(feature = "tree-sitter-highlight")] + highlight_config: OnceCell>, + #[cfg(feature = "tree-sitter-tags")] + tags_config: OnceCell>, + #[cfg(feature = "tree-sitter-highlight")] + highlight_names: &'a Mutex>, + #[cfg(feature = "tree-sitter-highlight")] + use_all_highlight_names: bool, +} + +pub struct Loader { + pub parser_lib_path: PathBuf, + languages_by_id: Vec<(PathBuf, OnceCell, Option>)>, + language_configurations: Vec>, + language_configuration_ids_by_file_type: HashMap>, + language_configuration_in_current_path: Option, + language_configuration_ids_by_first_line_regex: HashMap>, + #[cfg(feature = "tree-sitter-highlight")] + highlight_names: Box>>, + #[cfg(feature = "tree-sitter-highlight")] + use_all_highlight_names: bool, + debug_build: bool, + sanitize_build: bool, + force_rebuild: bool, + + #[cfg(feature = "wasm")] + wasm_store: Mutex>, +} + +pub struct CompileConfig<'a> { + pub src_path: &'a Path, + pub header_paths: Vec<&'a Path>, + pub parser_path: PathBuf, + pub scanner_path: Option, + pub external_files: Option<&'a [PathBuf]>, + pub output_path: Option, + pub flags: &'a [&'a str], + pub sanitize: bool, + pub name: String, +} + +impl<'a> CompileConfig<'a> { + #[must_use] + pub fn new( + src_path: &'a Path, + externals: Option<&'a [PathBuf]>, + output_path: Option, + ) -> Self { + Self { + src_path, + header_paths: vec![src_path], + parser_path: src_path.join("parser.c"), + scanner_path: None, + external_files: externals, + output_path, + flags: &[], + sanitize: false, + name: String::new(), + } + } +} + +unsafe impl Sync for Loader {} + +impl Loader { + pub fn new() -> Result { + let parser_lib_path = if let Ok(path) = env::var("TREE_SITTER_LIBDIR") { + PathBuf::from(path) + } else { + if cfg!(target_os = "macos") { + let legacy_apple_path = etcetera::base_strategy::Apple::new()? + .cache_dir() // `$HOME/Library/Caches/` + .join("tree-sitter"); + if legacy_apple_path.exists() && legacy_apple_path.is_dir() { + std::fs::remove_dir_all(legacy_apple_path)?; + } + } + + etcetera::choose_base_strategy()? + .cache_dir() + .join("tree-sitter") + .join("lib") + }; + Ok(Self::with_parser_lib_path(parser_lib_path)) + } + + #[must_use] + pub fn with_parser_lib_path(parser_lib_path: PathBuf) -> Self { + Self { + parser_lib_path, + languages_by_id: Vec::new(), + language_configurations: Vec::new(), + language_configuration_ids_by_file_type: HashMap::new(), + language_configuration_in_current_path: None, + language_configuration_ids_by_first_line_regex: HashMap::new(), + #[cfg(feature = "tree-sitter-highlight")] + highlight_names: Box::new(Mutex::new(Vec::new())), + #[cfg(feature = "tree-sitter-highlight")] + use_all_highlight_names: true, + debug_build: false, + sanitize_build: false, + force_rebuild: false, + + #[cfg(feature = "wasm")] + wasm_store: Mutex::default(), + } + } + + #[cfg(feature = "tree-sitter-highlight")] + #[cfg_attr(docsrs, doc(cfg(feature = "tree-sitter-highlight")))] + pub fn configure_highlights(&mut self, names: &[String]) { + self.use_all_highlight_names = false; + let mut highlights = self.highlight_names.lock().unwrap(); + highlights.clear(); + highlights.extend(names.iter().cloned()); + } + + #[must_use] + #[cfg(feature = "tree-sitter-highlight")] + #[cfg_attr(docsrs, doc(cfg(feature = "tree-sitter-highlight")))] + pub fn highlight_names(&self) -> Vec { + self.highlight_names.lock().unwrap().clone() + } + + pub fn find_all_languages(&mut self, config: &Config) -> Result<()> { + if config.parser_directories.is_empty() { + eprintln!("Warning: You have not configured any parser directories!"); + eprintln!("Please run `tree-sitter init-config` and edit the resulting"); + eprintln!("configuration file to indicate where we should look for"); + eprintln!("language grammars.\n"); + } + for parser_container_dir in &config.parser_directories { + if let Ok(entries) = fs::read_dir(parser_container_dir) { + for entry in entries { + let entry = entry?; + if let Some(parser_dir_name) = entry.file_name().to_str() { + if parser_dir_name.starts_with("tree-sitter-") { + self.find_language_configurations_at_path( + &parser_container_dir.join(parser_dir_name), + false, + ) + .ok(); + } + } + } + } + } + Ok(()) + } + + pub fn languages_at_path(&mut self, path: &Path) -> Result> { + if let Ok(configurations) = self.find_language_configurations_at_path(path, true) { + let mut language_ids = configurations + .iter() + .map(|c| (c.language_id, c.language_name.clone())) + .collect::>(); + language_ids.sort_unstable(); + language_ids.dedup(); + language_ids + .into_iter() + .map(|(id, name)| Ok((self.language_for_id(id)?, name))) + .collect::>>() + } else { + Ok(Vec::new()) + } + } + + #[must_use] + pub fn get_all_language_configurations(&self) -> Vec<(&LanguageConfiguration, &Path)> { + self.language_configurations + .iter() + .map(|c| (c, self.languages_by_id[c.language_id].0.as_ref())) + .collect() + } + + pub fn language_configuration_for_scope( + &self, + scope: &str, + ) -> Result> { + for configuration in &self.language_configurations { + if configuration.scope.as_ref().is_some_and(|s| s == scope) { + let language = self.language_for_id(configuration.language_id)?; + return Ok(Some((language, configuration))); + } + } + Ok(None) + } + + pub fn language_configuration_for_first_line_regex( + &self, + path: &Path, + ) -> Result> { + self.language_configuration_ids_by_first_line_regex + .iter() + .try_fold(None, |_, (regex, ids)| { + if let Some(regex) = Self::regex(Some(regex)) { + let file = fs::File::open(path)?; + let reader = BufReader::new(file); + let first_line = reader.lines().next().transpose()?; + if let Some(first_line) = first_line { + if regex.is_match(&first_line) && !ids.is_empty() { + let configuration = &self.language_configurations[ids[0]]; + let language = self.language_for_id(configuration.language_id)?; + return Ok(Some((language, configuration))); + } + } + } + + Ok(None) + }) + } + + pub fn language_configuration_for_file_name( + &self, + path: &Path, + ) -> Result> { + // Find all the language configurations that match this file name + // or a suffix of the file name. + let configuration_ids = path + .file_name() + .and_then(|n| n.to_str()) + .and_then(|file_name| self.language_configuration_ids_by_file_type.get(file_name)) + .or_else(|| { + let mut path = path.to_owned(); + let mut extensions = Vec::with_capacity(2); + while let Some(extension) = path.extension() { + extensions.push(extension.to_str()?.to_string()); + path = PathBuf::from(path.file_stem()?.to_os_string()); + } + extensions.reverse(); + self.language_configuration_ids_by_file_type + .get(&extensions.join(".")) + }); + + if let Some(configuration_ids) = configuration_ids { + if !configuration_ids.is_empty() { + let configuration = if configuration_ids.len() == 1 { + &self.language_configurations[configuration_ids[0]] + } + // If multiple language configurations match, then determine which + // one to use by applying the configurations' content regexes. + else { + let file_contents = fs::read(path) + .with_context(|| format!("Failed to read path {}", path.display()))?; + let file_contents = String::from_utf8_lossy(&file_contents); + let mut best_score = -2isize; + let mut best_configuration_id = None; + for configuration_id in configuration_ids { + let config = &self.language_configurations[*configuration_id]; + + // If the language configuration has a content regex, assign + // a score based on the length of the first match. + let score; + if let Some(content_regex) = &config.content_regex { + if let Some(mat) = content_regex.find(&file_contents) { + score = (mat.end() - mat.start()) as isize; + } + // If the content regex does not match, then *penalize* this + // language configuration, so that language configurations + // without content regexes are preferred over those with + // non-matching content regexes. + else { + score = -1; + } + } else { + score = 0; + } + if score > best_score { + best_configuration_id = Some(*configuration_id); + best_score = score; + } + } + + &self.language_configurations[best_configuration_id.unwrap()] + }; + + let language = self.language_for_id(configuration.language_id)?; + return Ok(Some((language, configuration))); + } + } + + Ok(None) + } + + pub fn language_configuration_for_injection_string( + &self, + string: &str, + ) -> Result> { + let mut best_match_length = 0; + let mut best_match_position = None; + for (i, configuration) in self.language_configurations.iter().enumerate() { + if let Some(injection_regex) = &configuration.injection_regex { + if let Some(mat) = injection_regex.find(string) { + let length = mat.end() - mat.start(); + if length > best_match_length { + best_match_position = Some(i); + best_match_length = length; + } + } + } + } + + if let Some(i) = best_match_position { + let configuration = &self.language_configurations[i]; + let language = self.language_for_id(configuration.language_id)?; + Ok(Some((language, configuration))) + } else { + Ok(None) + } + } + + pub fn language_for_configuration( + &self, + configuration: &LanguageConfiguration, + ) -> Result { + self.language_for_id(configuration.language_id) + } + + fn language_for_id(&self, id: usize) -> Result { + let (path, language, externals) = &self.languages_by_id[id]; + language + .get_or_try_init(|| { + let src_path = path.join("src"); + self.load_language_at_path(CompileConfig::new( + &src_path, + externals.as_deref(), + None, + )) + }) + .cloned() + } + + pub fn compile_parser_at_path( + &self, + grammar_path: &Path, + output_path: PathBuf, + flags: &[&str], + ) -> Result<()> { + let src_path = grammar_path.join("src"); + let mut config = CompileConfig::new(&src_path, None, Some(output_path)); + config.flags = flags; + self.load_language_at_path(config).map(|_| ()) + } + + pub fn load_language_at_path(&self, mut config: CompileConfig) -> Result { + let grammar_path = config.src_path.join("grammar.json"); + config.name = Self::grammar_json_name(&grammar_path)?; + self.load_language_at_path_with_name(config) + } + + pub fn load_language_at_path_with_name(&self, mut config: CompileConfig) -> Result { + let mut lib_name = config.name.to_string(); + let language_fn_name = format!( + "tree_sitter_{}", + replace_dashes_with_underscores(&config.name) + ); + if self.debug_build { + lib_name.push_str(".debug._"); + } + + if self.sanitize_build { + lib_name.push_str(".sanitize._"); + config.sanitize = true; + } + + if config.output_path.is_none() { + fs::create_dir_all(&self.parser_lib_path)?; + } + + let mut recompile = self.force_rebuild || config.output_path.is_some(); // if specified, always recompile + + let output_path = config.output_path.unwrap_or_else(|| { + let mut path = self.parser_lib_path.join(lib_name); + path.set_extension(env::consts::DLL_EXTENSION); + #[cfg(feature = "wasm")] + if self.wasm_store.lock().unwrap().is_some() { + path.set_extension("wasm"); + } + path + }); + config.output_path = Some(output_path.clone()); + + let parser_path = config.src_path.join("parser.c"); + config.scanner_path = self.get_scanner_path(config.src_path); + + let mut paths_to_check = vec![parser_path]; + + if let Some(scanner_path) = config.scanner_path.as_ref() { + paths_to_check.push(scanner_path.clone()); + } + + paths_to_check.extend( + config + .external_files + .unwrap_or_default() + .iter() + .map(|p| config.src_path.join(p)), + ); + + if !recompile { + recompile = needs_recompile(&output_path, &paths_to_check) + .with_context(|| "Failed to compare source and binary timestamps")?; + } + + #[cfg(feature = "wasm")] + if let Some(wasm_store) = self.wasm_store.lock().unwrap().as_mut() { + if recompile { + self.compile_parser_to_wasm( + &config.name, + None, + config.src_path, + config + .scanner_path + .as_ref() + .and_then(|p| p.strip_prefix(config.src_path).ok()), + &output_path, + false, + )?; + } + + let wasm_bytes = fs::read(&output_path)?; + return Ok(wasm_store.load_language(&config.name, &wasm_bytes)?); + } + + let lock_path = if env::var("CROSS_RUNNER").is_ok() { + tempfile::tempdir() + .unwrap() + .path() + .join("tree-sitter") + .join("lock") + .join(format!("{}.lock", config.name)) + } else { + etcetera::choose_base_strategy()? + .cache_dir() + .join("tree-sitter") + .join("lock") + .join(format!("{}.lock", config.name)) + }; + + if let Ok(lock_file) = fs::OpenOptions::new().write(true).open(&lock_path) { + recompile = false; + if lock_file.try_lock_exclusive().is_err() { + // if we can't acquire the lock, another process is compiling the parser, wait for + // it and don't recompile + lock_file.lock_exclusive()?; + recompile = false; + } else { + // if we can acquire the lock, check if the lock file is older than 30 seconds, a + // run that was interrupted and left the lock file behind should not block + // subsequent runs + let time = lock_file.metadata()?.modified()?.elapsed()?.as_secs(); + if time > 30 { + fs::remove_file(&lock_path)?; + recompile = true; + } + } + } + + if recompile { + fs::create_dir_all(lock_path.parent().unwrap()).with_context(|| { + format!( + "Failed to create directory {}", + lock_path.parent().unwrap().display() + ) + })?; + let lock_file = fs::OpenOptions::new() + .create(true) + .truncate(true) + .write(true) + .open(&lock_path)?; + lock_file.lock_exclusive()?; + + self.compile_parser_to_dylib(&config, &lock_file, &lock_path)?; + + if config.scanner_path.is_some() { + self.check_external_scanner(&config.name, &output_path)?; + } + } + + let library = unsafe { Library::new(&output_path) } + .with_context(|| format!("Error opening dynamic library {}", output_path.display()))?; + let language = unsafe { + let language_fn = library + .get:: Language>>(language_fn_name.as_bytes()) + .with_context(|| format!("Failed to load symbol {language_fn_name}"))?; + language_fn() + }; + mem::forget(library); + Ok(language) + } + + fn compile_parser_to_dylib( + &self, + config: &CompileConfig, + lock_file: &fs::File, + lock_path: &Path, + ) -> Result<(), Error> { + let mut cc_config = cc::Build::new(); + cc_config + .cargo_metadata(false) + .cargo_warnings(false) + .target(BUILD_TARGET) + .host(BUILD_HOST) + .debug(self.debug_build) + .file(&config.parser_path) + .includes(&config.header_paths) + .std("c11"); + + if let Some(scanner_path) = config.scanner_path.as_ref() { + cc_config.file(scanner_path); + } + + if self.debug_build { + cc_config.opt_level(0).extra_warnings(true); + } else { + cc_config.opt_level(2).extra_warnings(false); + } + + for flag in config.flags { + cc_config.define(flag, None); + } + + let compiler = cc_config.get_compiler(); + let mut command = Command::new(compiler.path()); + command.args(compiler.args()); + for (key, value) in compiler.env() { + command.env(key, value); + } + + let output_path = config.output_path.as_ref().unwrap(); + + if compiler.is_like_msvc() { + let out = format!("-out:{}", output_path.to_str().unwrap()); + command.arg(if self.debug_build { "-LDd" } else { "-LD" }); + command.arg("-utf-8"); + command.args(cc_config.get_files()); + command.arg("-link").arg(out); + } else { + command.arg("-Werror=implicit-function-declaration"); + if cfg!(any(target_os = "macos", target_os = "ios")) { + command.arg("-dynamiclib"); + // TODO: remove when supported + command.arg("-UTREE_SITTER_REUSE_ALLOCATOR"); + } else { + command.arg("-shared"); + } + command.args(cc_config.get_files()); + command.arg("-o").arg(output_path); + } + + let output = command.output().with_context(|| { + format!("Failed to execute the C compiler with the following command:\n{command:?}") + })?; + + FileExt::unlock(lock_file)?; + fs::remove_file(lock_path)?; + + if output.status.success() { + Ok(()) + } else { + Err(anyhow!( + "Parser compilation failed.\nStdout: {}\nStderr: {}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + )) + } + } + + #[cfg(unix)] + fn check_external_scanner(&self, name: &str, library_path: &Path) -> Result<()> { + let prefix = if cfg!(any(target_os = "macos", target_os = "ios")) { + "_" + } else { + "" + }; + let mut must_have = vec![ + format!("{prefix}tree_sitter_{name}_external_scanner_create"), + format!("{prefix}tree_sitter_{name}_external_scanner_destroy"), + format!("{prefix}tree_sitter_{name}_external_scanner_serialize"), + format!("{prefix}tree_sitter_{name}_external_scanner_deserialize"), + format!("{prefix}tree_sitter_{name}_external_scanner_scan"), + ]; + + let command = Command::new("nm") + .arg("-W") + .arg("-U") + .arg(library_path) + .output(); + if let Ok(output) = command { + if output.status.success() { + let mut found_non_static = false; + for line in String::from_utf8_lossy(&output.stdout).lines() { + if line.contains(" T ") { + if let Some(function_name) = + line.split_whitespace().collect::>().get(2) + { + if !line.contains("tree_sitter_") { + if !found_non_static { + found_non_static = true; + eprintln!( + "Warning: Found non-static non-tree-sitter functions in the external scannner" + ); + } + eprintln!(" `{function_name}`"); + } else { + must_have.retain(|f| f != function_name); + } + } + } + } + if found_non_static { + eprintln!( + "Consider making these functions static, they can cause conflicts when another tree-sitter project uses the same function name" + ); + } + + if !must_have.is_empty() { + let missing = must_have + .iter() + .map(|f| format!(" `{f}`")) + .collect::>() + .join("\n"); + + return Err(anyhow!(format!( + indoc! {" + Missing required functions in the external scanner, parsing won't work without these! + + {} + + You can read more about this at https://tree-sitter.github.io/tree-sitter/creating-parsers/4-external-scanners + "}, + missing, + ))); + } + } + } + + Ok(()) + } + + #[cfg(windows)] + fn check_external_scanner(&self, _name: &str, _library_path: &Path) -> Result<()> { + // TODO: there's no nm command on windows, whoever wants to implement this can and should :) + + // let mut must_have = vec![ + // format!("tree_sitter_{name}_external_scanner_create"), + // format!("tree_sitter_{name}_external_scanner_destroy"), + // format!("tree_sitter_{name}_external_scanner_serialize"), + // format!("tree_sitter_{name}_external_scanner_deserialize"), + // format!("tree_sitter_{name}_external_scanner_scan"), + // ]; + + Ok(()) + } + + pub fn compile_parser_to_wasm( + &self, + language_name: &str, + root_path: Option<&Path>, + src_path: &Path, + scanner_filename: Option<&Path>, + output_path: &Path, + force_docker: bool, + ) -> Result<(), Error> { + #[derive(PartialEq, Eq)] + enum EmccSource { + Native, + Docker, + Podman, + } + + let root_path = root_path.unwrap_or(src_path); + let emcc_name = if cfg!(windows) { "emcc.bat" } else { "emcc" }; + + // Order of preference: emscripten > docker > podman > error + let source = if !force_docker && Command::new(emcc_name).output().is_ok() { + EmccSource::Native + } else if Command::new("docker") + .output() + .is_ok_and(|out| out.status.success()) + { + EmccSource::Docker + } else if Command::new("podman") + .arg("--version") + .output() + .is_ok_and(|out| out.status.success()) + { + EmccSource::Podman + } else { + return Err(anyhow!( + "You must have either emcc, docker, or podman on your PATH to run this command" + )); + }; + + let mut command = match source { + EmccSource::Native => { + let mut command = Command::new(emcc_name); + command.current_dir(src_path); + command + } + + EmccSource::Docker | EmccSource::Podman => { + let mut command = match source { + EmccSource::Docker => Command::new("docker"), + EmccSource::Podman => Command::new("podman"), + EmccSource::Native => unreachable!(), + }; + command.args(["run", "--rm"]); + + // The working directory is the directory containing the parser itself + let workdir = if root_path == src_path { + PathBuf::from("/src") + } else { + let mut path = PathBuf::from("/src"); + path.push(src_path.strip_prefix(root_path).unwrap()); + path + }; + command.args(["--workdir", &workdir.to_slash_lossy()]); + + // Mount the root directory as a volume, which is the repo root + let mut volume_string = OsString::from(&root_path); + volume_string.push(":/src:Z"); + command.args([OsStr::new("--volume"), &volume_string]); + + // In case `docker` is an alias to `podman`, ensure that podman + // mounts the current directory as writable by the container + // user which has the same uid as the host user. Setting the + // podman-specific variable is more reliable than attempting to + // detect whether `docker` is an alias for `podman`. + // see https://docs.podman.io/en/latest/markdown/podman-run.1.html#userns-mode + command.env("PODMAN_USERNS", "keep-id"); + + // Get the current user id so that files created in the docker container will have + // the same owner. + #[cfg(unix)] + { + #[link(name = "c")] + extern "C" { + fn getuid() -> u32; + } + // don't need to set user for podman since PODMAN_USERNS=keep-id is already set + if source == EmccSource::Docker { + let user_id = unsafe { getuid() }; + command.args(["--user", &user_id.to_string()]); + } + }; + + // Run `emcc` in a container using the `emscripten-slim` image + command.args([EMSCRIPTEN_TAG, "emcc"]); + command + } + }; + + let output_name = "output.wasm"; + + command.args([ + "-o", + output_name, + "-Os", + "-s", + "WASM=1", + "-s", + "SIDE_MODULE=2", + "-s", + "TOTAL_MEMORY=33554432", + "-s", + "NODEJS_CATCH_EXIT=0", + "-s", + &format!("EXPORTED_FUNCTIONS=[\"_tree_sitter_{language_name}\"]"), + "-fno-exceptions", + "-fvisibility=hidden", + "-I", + ".", + ]); + + if let Some(scanner_filename) = scanner_filename { + command.arg(scanner_filename); + } + + command.arg("parser.c"); + let status = command + .spawn() + .with_context(|| "Failed to run emcc command")? + .wait()?; + if !status.success() { + return Err(anyhow!("emcc command failed")); + } + + fs::rename(src_path.join(output_name), output_path) + .context("failed to rename wasm output file")?; + + Ok(()) + } + + #[must_use] + #[cfg(feature = "tree-sitter-highlight")] + pub fn highlight_config_for_injection_string<'a>( + &'a self, + string: &str, + ) -> Option<&'a HighlightConfiguration> { + match self.language_configuration_for_injection_string(string) { + Err(e) => { + eprintln!("Failed to load language for injection string '{string}': {e}",); + None + } + Ok(None) => None, + Ok(Some((language, configuration))) => { + match configuration.highlight_config(language, None) { + Err(e) => { + eprintln!( + "Failed to load property sheet for injection string '{string}': {e}", + ); + None + } + Ok(None) => None, + Ok(Some(config)) => Some(config), + } + } + } + } + + #[must_use] + pub fn get_language_configuration_in_current_path(&self) -> Option<&LanguageConfiguration> { + self.language_configuration_in_current_path + .map(|i| &self.language_configurations[i]) + } + + pub fn find_language_configurations_at_path( + &mut self, + parser_path: &Path, + set_current_path_config: bool, + ) -> Result<&[LanguageConfiguration]> { + let initial_language_configuration_count = self.language_configurations.len(); + + let ts_json = TreeSitterJSON::from_file(parser_path); + if let Ok(config) = ts_json { + let language_count = self.languages_by_id.len(); + for grammar in config.grammars { + // Determine the path to the parser directory. This can be specified in + // the tree-sitter.json, but defaults to the directory containing the + // tree-sitter.json. + let language_path = parser_path.join(grammar.path.unwrap_or(PathBuf::from("."))); + + // Determine if a previous language configuration in this package.json file + // already uses the same language. + let mut language_id = None; + for (id, (path, _, _)) in + self.languages_by_id.iter().enumerate().skip(language_count) + { + if language_path == *path { + language_id = Some(id); + } + } + + // If not, add a new language path to the list. + let language_id = if let Some(language_id) = language_id { + language_id + } else { + self.languages_by_id.push(( + language_path, + OnceCell::new(), + grammar.external_files.clone().into_vec().map(|files| { + files.into_iter() + .map(|path| { + let path = parser_path.join(path); + // prevent p being above/outside of parser_path + if path.starts_with(parser_path) { + Ok(path) + } else { + Err(anyhow!("External file path {path:?} is outside of parser directory {parser_path:?}")) + } + }) + .collect::>>() + }).transpose()?, + )); + self.languages_by_id.len() - 1 + }; + + let configuration = LanguageConfiguration { + root_path: parser_path.to_path_buf(), + language_name: grammar.name, + scope: Some(grammar.scope), + language_id, + file_types: grammar.file_types.unwrap_or_default(), + content_regex: Self::regex(grammar.content_regex.as_deref()), + first_line_regex: Self::regex(grammar.first_line_regex.as_deref()), + injection_regex: Self::regex(grammar.injection_regex.as_deref()), + injections_filenames: grammar.injections.into_vec(), + locals_filenames: grammar.locals.into_vec(), + tags_filenames: grammar.tags.into_vec(), + highlights_filenames: grammar.highlights.into_vec(), + #[cfg(feature = "tree-sitter-highlight")] + highlight_config: OnceCell::new(), + #[cfg(feature = "tree-sitter-tags")] + tags_config: OnceCell::new(), + #[cfg(feature = "tree-sitter-highlight")] + highlight_names: &self.highlight_names, + #[cfg(feature = "tree-sitter-highlight")] + use_all_highlight_names: self.use_all_highlight_names, + }; + + for file_type in &configuration.file_types { + self.language_configuration_ids_by_file_type + .entry(file_type.to_string()) + .or_default() + .push(self.language_configurations.len()); + } + if let Some(first_line_regex) = &configuration.first_line_regex { + self.language_configuration_ids_by_first_line_regex + .entry(first_line_regex.to_string()) + .or_default() + .push(self.language_configurations.len()); + } + + self.language_configurations.push(unsafe { + mem::transmute::, LanguageConfiguration<'static>>( + configuration, + ) + }); + + if set_current_path_config && self.language_configuration_in_current_path.is_none() + { + self.language_configuration_in_current_path = + Some(self.language_configurations.len() - 1); + } + } + } else if let Err(e) = ts_json { + match e.downcast_ref::() { + // This is noisy, and not really an issue. + Some(e) if e.kind() == std::io::ErrorKind::NotFound => {} + _ => { + eprintln!( + "Warning: Failed to parse {} -- {e}", + parser_path.join("tree-sitter.json").display() + ); + } + } + } + + // If we didn't find any language configurations in the tree-sitter.json file, + // but there is a grammar.json file, then use the grammar file to form a simple + // language configuration. + if self.language_configurations.len() == initial_language_configuration_count + && parser_path.join("src").join("grammar.json").exists() + { + let grammar_path = parser_path.join("src").join("grammar.json"); + let language_name = Self::grammar_json_name(&grammar_path)?; + let configuration = LanguageConfiguration { + root_path: parser_path.to_owned(), + language_name, + language_id: self.languages_by_id.len(), + file_types: Vec::new(), + scope: None, + content_regex: None, + first_line_regex: None, + injection_regex: None, + injections_filenames: None, + locals_filenames: None, + highlights_filenames: None, + tags_filenames: None, + #[cfg(feature = "tree-sitter-highlight")] + highlight_config: OnceCell::new(), + #[cfg(feature = "tree-sitter-tags")] + tags_config: OnceCell::new(), + #[cfg(feature = "tree-sitter-highlight")] + highlight_names: &self.highlight_names, + #[cfg(feature = "tree-sitter-highlight")] + use_all_highlight_names: self.use_all_highlight_names, + }; + self.language_configurations.push(unsafe { + mem::transmute::, LanguageConfiguration<'static>>( + configuration, + ) + }); + self.languages_by_id + .push((parser_path.to_owned(), OnceCell::new(), None)); + } + + Ok(&self.language_configurations[initial_language_configuration_count..]) + } + + fn regex(pattern: Option<&str>) -> Option { + pattern.and_then(|r| RegexBuilder::new(r).multi_line(true).build().ok()) + } + + fn grammar_json_name(grammar_path: &Path) -> Result { + let file = fs::File::open(grammar_path).with_context(|| { + format!("Failed to open grammar.json at {}", grammar_path.display()) + })?; + + let first_three_lines = BufReader::new(file) + .lines() + .take(3) + .collect::, _>>() + .with_context(|| { + format!( + "Failed to read the first three lines of grammar.json at {}", + grammar_path.display() + ) + })? + .join("\n"); + + let name = GRAMMAR_NAME_REGEX + .captures(&first_three_lines) + .and_then(|c| c.get(1)) + .ok_or_else(|| { + anyhow!( + "Failed to parse the language name from grammar.json at {}", + grammar_path.display() + ) + })?; + + Ok(name.as_str().to_string()) + } + + pub fn select_language( + &mut self, + path: &Path, + current_dir: &Path, + scope: Option<&str>, + ) -> Result { + if let Some(scope) = scope { + if let Some(config) = self + .language_configuration_for_scope(scope) + .with_context(|| format!("Failed to load language for scope '{scope}'"))? + { + Ok(config.0) + } else { + Err(anyhow!("Unknown scope '{scope}'")) + } + } else if let Some((lang, _)) = self + .language_configuration_for_file_name(path) + .with_context(|| { + format!( + "Failed to load language for file name {}", + path.file_name().unwrap().to_string_lossy() + ) + })? + { + Ok(lang) + } else if let Some(id) = self.language_configuration_in_current_path { + Ok(self.language_for_id(self.language_configurations[id].language_id)?) + } else if let Some(lang) = self + .languages_at_path(current_dir) + .with_context(|| "Failed to load language in current directory")? + .first() + .cloned() + { + Ok(lang.0) + } else if let Some(lang) = self.language_configuration_for_first_line_regex(path)? { + Ok(lang.0) + } else { + Err(anyhow!("No language found")) + } + } + + pub fn debug_build(&mut self, flag: bool) { + self.debug_build = flag; + } + + pub fn sanitize_build(&mut self, flag: bool) { + self.sanitize_build = flag; + } + + pub fn force_rebuild(&mut self, rebuild: bool) { + self.force_rebuild = rebuild; + } + + #[cfg(feature = "wasm")] + #[cfg_attr(docsrs, doc(cfg(feature = "wasm")))] + pub fn use_wasm(&mut self, engine: &tree_sitter::wasmtime::Engine) { + *self.wasm_store.lock().unwrap() = Some(tree_sitter::WasmStore::new(engine).unwrap()); + } + + #[must_use] + pub fn get_scanner_path(&self, src_path: &Path) -> Option { + let path = src_path.join("scanner.c"); + path.exists().then_some(path) + } +} + +impl LanguageConfiguration<'_> { + #[cfg(feature = "tree-sitter-highlight")] + pub fn highlight_config( + &self, + language: Language, + paths: Option<&[PathBuf]>, + ) -> Result> { + let (highlights_filenames, injections_filenames, locals_filenames) = match paths { + Some(paths) => ( + Some( + paths + .iter() + .filter(|p| p.ends_with("highlights.scm")) + .cloned() + .collect::>(), + ), + Some( + paths + .iter() + .filter(|p| p.ends_with("tags.scm")) + .cloned() + .collect::>(), + ), + Some( + paths + .iter() + .filter(|p| p.ends_with("locals.scm")) + .cloned() + .collect::>(), + ), + ), + None => (None, None, None), + }; + self.highlight_config + .get_or_try_init(|| { + let (highlights_query, highlight_ranges) = self.read_queries( + if highlights_filenames.is_some() { + highlights_filenames.as_deref() + } else { + self.highlights_filenames.as_deref() + }, + "highlights.scm", + )?; + let (injections_query, injection_ranges) = self.read_queries( + if injections_filenames.is_some() { + injections_filenames.as_deref() + } else { + self.injections_filenames.as_deref() + }, + "injections.scm", + )?; + let (locals_query, locals_ranges) = self.read_queries( + if locals_filenames.is_some() { + locals_filenames.as_deref() + } else { + self.locals_filenames.as_deref() + }, + "locals.scm", + )?; + + if highlights_query.is_empty() { + Ok(None) + } else { + let mut result = HighlightConfiguration::new( + language, + &self.language_name, + &highlights_query, + &injections_query, + &locals_query, + ) + .map_err(|error| match error.kind { + QueryErrorKind::Language => Error::from(error), + _ => { + if error.offset < injections_query.len() { + Self::include_path_in_query_error( + error, + &injection_ranges, + &injections_query, + 0, + ) + } else if error.offset < injections_query.len() + locals_query.len() { + Self::include_path_in_query_error( + error, + &locals_ranges, + &locals_query, + injections_query.len(), + ) + } else { + Self::include_path_in_query_error( + error, + &highlight_ranges, + &highlights_query, + injections_query.len() + locals_query.len(), + ) + } + } + })?; + let mut all_highlight_names = self.highlight_names.lock().unwrap(); + if self.use_all_highlight_names { + for capture_name in result.query.capture_names() { + if !all_highlight_names.iter().any(|x| x == capture_name) { + all_highlight_names.push((*capture_name).to_string()); + } + } + } + result.configure(all_highlight_names.as_slice()); + drop(all_highlight_names); + Ok(Some(result)) + } + }) + .map(Option::as_ref) + } + + #[cfg(feature = "tree-sitter-tags")] + pub fn tags_config(&self, language: Language) -> Result> { + self.tags_config + .get_or_try_init(|| { + let (tags_query, tags_ranges) = + self.read_queries(self.tags_filenames.as_deref(), "tags.scm")?; + let (locals_query, locals_ranges) = + self.read_queries(self.locals_filenames.as_deref(), "locals.scm")?; + if tags_query.is_empty() { + Ok(None) + } else { + TagsConfiguration::new(language, &tags_query, &locals_query) + .map(Some) + .map_err(|error| { + if let TagsError::Query(error) = error { + if error.offset < locals_query.len() { + Self::include_path_in_query_error( + error, + &locals_ranges, + &locals_query, + 0, + ) + } else { + Self::include_path_in_query_error( + error, + &tags_ranges, + &tags_query, + locals_query.len(), + ) + } + } else { + error.into() + } + }) + } + }) + .map(Option::as_ref) + } + + #[cfg(any(feature = "tree-sitter-highlight", feature = "tree-sitter-tags"))] + fn include_path_in_query_error( + mut error: QueryError, + ranges: &[(PathBuf, Range)], + source: &str, + start_offset: usize, + ) -> Error { + let offset_within_section = error.offset - start_offset; + let (path, range) = ranges + .iter() + .find(|(_, range)| range.contains(&offset_within_section)) + .unwrap_or_else(|| ranges.last().unwrap()); + error.offset = offset_within_section - range.start; + error.row = source[range.start..offset_within_section] + .matches('\n') + .count(); + Error::from(error).context(format!("Error in query file {}", path.display())) + } + + #[allow(clippy::type_complexity)] + #[cfg(any(feature = "tree-sitter-highlight", feature = "tree-sitter-tags"))] + fn read_queries( + &self, + paths: Option<&[PathBuf]>, + default_path: &str, + ) -> Result<(String, Vec<(PathBuf, Range)>)> { + let mut query = String::new(); + let mut path_ranges = Vec::new(); + if let Some(paths) = paths { + for path in paths { + let abs_path = self.root_path.join(path); + let prev_query_len = query.len(); + query += &fs::read_to_string(&abs_path) + .with_context(|| format!("Failed to read query file {}", path.display()))?; + path_ranges.push((path.clone(), prev_query_len..query.len())); + } + } else { + // highlights.scm is needed to test highlights, and tags.scm to test tags + if default_path == "highlights.scm" || default_path == "tags.scm" { + eprintln!( + indoc! {" + Warning: you should add a `{}` entry pointing to the highlights path in the `tree-sitter` object in the grammar's tree-sitter.json file. + See more here: https://tree-sitter.github.io/tree-sitter/3-syntax-highlighting#query-paths + "}, + default_path.replace(".scm", "") + ); + } + let queries_path = self.root_path.join("queries"); + let path = queries_path.join(default_path); + if path.exists() { + query = fs::read_to_string(&path) + .with_context(|| format!("Failed to read query file {}", path.display()))?; + path_ranges.push((PathBuf::from(default_path), 0..query.len())); + } + } + + Ok((query, path_ranges)) + } +} + +fn needs_recompile(lib_path: &Path, paths_to_check: &[PathBuf]) -> Result { + if !lib_path.exists() { + return Ok(true); + } + let lib_mtime = mtime(lib_path) + .with_context(|| format!("Failed to read mtime of {}", lib_path.display()))?; + for path in paths_to_check { + if mtime(path)? > lib_mtime { + return Ok(true); + } + } + Ok(false) +} + +fn mtime(path: &Path) -> Result { + Ok(fs::metadata(path)?.modified()?) +} + +fn replace_dashes_with_underscores(name: &str) -> String { + let mut result = String::with_capacity(name.len()); + for c in name.chars() { + if c == '-' { + result.push('_'); + } else { + result.push(c); + } + } + result +} diff --git a/crates/assistant_tools/src/edit_file_tool.rs b/crates/assistant_tools/src/edit_file_tool.rs index a11398a167..0c995a58fc 100644 --- a/crates/assistant_tools/src/edit_file_tool.rs +++ b/crates/assistant_tools/src/edit_file_tool.rs @@ -282,7 +282,7 @@ pub struct EditFileToolCard { } impl EditFileToolCard { - fn new(path: PathBuf, project: Entity, window: &mut Window, cx: &mut App) -> Self { + pub fn new(path: PathBuf, project: Entity, window: &mut Window, cx: &mut App) -> Self { let multibuffer = cx.new(|_| MultiBuffer::without_headers(Capability::ReadOnly)); let editor = cx.new(|cx| { let mut editor = Editor::new( @@ -323,7 +323,7 @@ impl EditFileToolCard { } } - fn set_diff( + pub fn set_diff( &mut self, path: Arc, old_text: String, @@ -343,6 +343,7 @@ impl EditFileToolCard { .hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot, cx) .map(|diff_hunk| diff_hunk.buffer_range.to_point(&snapshot)) .collect::>(); + multibuffer.clear(cx); let (_, is_newly_added) = multibuffer.set_excerpts_for_path( PathKey::for_buffer(&buffer, cx), buffer, diff --git a/crates/assistant_tools/src/streaming_edit_file_tool.rs b/crates/assistant_tools/src/streaming_edit_file_tool.rs new file mode 100644 index 0000000000..668237fba3 --- /dev/null +++ b/crates/assistant_tools/src/streaming_edit_file_tool.rs @@ -0,0 +1,339 @@ +use crate::{ + Templates, + edit_agent::{EditAgent, EditAgentOutputEvent}, + edit_file_tool::EditFileToolCard, + schema::json_schema_for, +}; +use anyhow::{Context as _, Result, anyhow}; +use assistant_tool::{ActionLog, AnyToolCard, Tool, ToolResult}; +use futures::StreamExt; +use gpui::{AnyWindowHandle, App, AppContext, AsyncApp, Entity, Task}; +use indoc::formatdoc; +use language_model::{ + LanguageModelRegistry, LanguageModelRequestMessage, LanguageModelToolSchemaFormat, +}; +use project::Project; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use std::{path::PathBuf, sync::Arc}; +use ui::prelude::*; +use util::ResultExt; + +pub struct StreamingEditFileTool; + +#[derive(Debug, Serialize, Deserialize, JsonSchema)] +pub struct StreamingEditFileToolInput { + /// A one-line, user-friendly markdown description of the edit. This will be + /// shown in the UI and also passed to another model to perform the edit. + /// + /// Be terse, but also descriptive in what you want to achieve with this + /// edit. Avoid generic instructions. + /// + /// NEVER mention the file path in this description. + /// + /// Fix API endpoint URLs + /// Update copyright year in `page_footer` + /// + /// Make sure to include this field before all the others in the input object + /// so that we can display it immediately. + pub display_description: String, + + /// The full path of the file to modify in the project. + /// + /// WARNING: When specifying which file path need changing, you MUST + /// start each path with one of the project's root directories. + /// + /// The following examples assume we have two root directories in the project: + /// - backend + /// - frontend + /// + /// + /// `backend/src/main.rs` + /// + /// Notice how the file path starts with root-1. Without that, the path + /// would be ambiguous and the call would fail! + /// + /// + /// + /// `frontend/db.js` + /// + pub path: PathBuf, +} + +#[derive(Debug, Serialize, Deserialize, JsonSchema)] +struct PartialInput { + #[serde(default)] + path: String, + #[serde(default)] + display_description: String, +} + +const DEFAULT_UI_TEXT: &str = "Editing file"; + +impl Tool for StreamingEditFileTool { + fn name(&self) -> String { + "edit_file".into() + } + + fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool { + false + } + + fn description(&self) -> String { + include_str!("streaming_edit_file_tool/description.md").to_string() + } + + fn icon(&self) -> IconName { + IconName::Pencil + } + + fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result { + json_schema_for::(format) + } + + fn ui_text(&self, input: &serde_json::Value) -> String { + match serde_json::from_value::(input.clone()) { + Ok(input) => input.display_description, + Err(_) => "Editing file".to_string(), + } + } + + fn still_streaming_ui_text(&self, input: &serde_json::Value) -> String { + if let Some(input) = serde_json::from_value::(input.clone()).ok() { + let description = input.display_description.trim(); + if !description.is_empty() { + return description.to_string(); + } + + let path = input.path.trim(); + if !path.is_empty() { + return path.to_string(); + } + } + + DEFAULT_UI_TEXT.to_string() + } + + fn run( + self: Arc, + input: serde_json::Value, + messages: &[LanguageModelRequestMessage], + project: Entity, + action_log: Entity, + window: Option, + cx: &mut App, + ) -> ToolResult { + let input = match serde_json::from_value::(input) { + Ok(input) => input, + Err(err) => return Task::ready(Err(anyhow!(err))).into(), + }; + + let Some(project_path) = project.read(cx).find_project_path(&input.path, cx) else { + return Task::ready(Err(anyhow!( + "Path {} not found in project", + input.path.display() + ))) + .into(); + }; + let Some(worktree) = project + .read(cx) + .worktree_for_id(project_path.worktree_id, cx) + else { + return Task::ready(Err(anyhow!("Worktree not found for project path"))).into(); + }; + let exists = worktree.update(cx, |worktree, cx| { + worktree.file_exists(&project_path.path, cx) + }); + + let card = window.and_then(|window| { + window + .update(cx, |_, window, cx| { + cx.new(|cx| { + EditFileToolCard::new(input.path.clone(), project.clone(), window, cx) + }) + }) + .ok() + }); + + let card_clone = card.clone(); + let messages = messages.to_vec(); + let task = cx.spawn(async move |cx: &mut AsyncApp| { + if !exists.await? { + return Err(anyhow!("{} not found", input.path.display())); + } + + let model = cx + .update(|cx| LanguageModelRegistry::read_global(cx).default_model())? + .context("default model not set")? + .model; + let edit_agent = EditAgent::new(model, action_log, Templates::new()); + + let buffer = project + .update(cx, |project, cx| { + project.open_buffer(project_path.clone(), cx) + })? + .await?; + + let old_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?; + let old_text = cx + .background_spawn({ + let old_snapshot = old_snapshot.clone(); + async move { old_snapshot.text() } + }) + .await; + + let (output, mut events) = edit_agent.edit( + buffer.clone(), + input.display_description.clone(), + messages, + cx, + ); + + let mut hallucinated_old_text = false; + while let Some(event) = events.next().await { + match event { + EditAgentOutputEvent::Edited => { + if let Some(card) = card_clone.as_ref() { + let new_snapshot = + buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?; + let new_text = cx + .background_spawn({ + let new_snapshot = new_snapshot.clone(); + async move { new_snapshot.text() } + }) + .await; + card.update(cx, |card, cx| { + card.set_diff( + project_path.path.clone(), + old_text.clone(), + new_text, + cx, + ); + }) + .log_err(); + } + } + EditAgentOutputEvent::HallucinatedOldText(_) => hallucinated_old_text = true, + } + } + output.await?; + + project + .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))? + .await?; + + let new_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?; + let new_text = cx.background_spawn({ + let new_snapshot = new_snapshot.clone(); + async move { new_snapshot.text() } + }); + let diff = cx.background_spawn(async move { + language::unified_diff(&old_snapshot.text(), &new_snapshot.text()) + }); + let (new_text, diff) = futures::join!(new_text, diff); + + if let Some(card) = card_clone { + card.update(cx, |card, cx| { + card.set_diff(project_path.path.clone(), old_text, new_text, cx); + }) + .log_err(); + } + + let input_path = input.path.display(); + if diff.is_empty() { + if hallucinated_old_text { + Err(anyhow!(formatdoc! {" + Some edits were produced but none of them could be applied. + Read the relevant sections of {input_path} again so that + I can perform the requested edits. + "})) + } else { + Ok("No edits were made.".to_string()) + } + } else { + Ok(format!("Edited {}:\n\n```diff\n{}\n```", input_path, diff)) + } + }); + + ToolResult { + output: task, + card: card.map(AnyToolCard::from), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + + #[test] + fn still_streaming_ui_text_with_path() { + let input = json!({ + "path": "src/main.rs", + "display_description": "", + "old_string": "old code", + "new_string": "new code" + }); + + assert_eq!( + StreamingEditFileTool.still_streaming_ui_text(&input), + "src/main.rs" + ); + } + + #[test] + fn still_streaming_ui_text_with_description() { + let input = json!({ + "path": "", + "display_description": "Fix error handling", + "old_string": "old code", + "new_string": "new code" + }); + + assert_eq!( + StreamingEditFileTool.still_streaming_ui_text(&input), + "Fix error handling", + ); + } + + #[test] + fn still_streaming_ui_text_with_path_and_description() { + let input = json!({ + "path": "src/main.rs", + "display_description": "Fix error handling", + "old_string": "old code", + "new_string": "new code" + }); + + assert_eq!( + StreamingEditFileTool.still_streaming_ui_text(&input), + "Fix error handling", + ); + } + + #[test] + fn still_streaming_ui_text_no_path_or_description() { + let input = json!({ + "path": "", + "display_description": "", + "old_string": "old code", + "new_string": "new code" + }); + + assert_eq!( + StreamingEditFileTool.still_streaming_ui_text(&input), + DEFAULT_UI_TEXT, + ); + } + + #[test] + fn still_streaming_ui_text_with_null() { + let input = serde_json::Value::Null; + + assert_eq!( + StreamingEditFileTool.still_streaming_ui_text(&input), + DEFAULT_UI_TEXT, + ); + } +} diff --git a/crates/assistant_tools/src/streaming_edit_file_tool/description.md b/crates/assistant_tools/src/streaming_edit_file_tool/description.md new file mode 100644 index 0000000000..14185b8eee --- /dev/null +++ b/crates/assistant_tools/src/streaming_edit_file_tool/description.md @@ -0,0 +1,8 @@ +This is a tool for editing files. For moving or renaming files, you should generally use the `terminal` tool with the 'mv' command instead. For larger edits, use the `create_file` tool to overwrite files. + +Before using this tool: + +1. Use the `read_file` tool to understand the file's contents and context + +2. Verify the directory path is correct (only applicable when creating new files): + - Use the `list_directory` tool to verify the parent directory exists and is the correct location diff --git a/crates/assistant_tools/src/templates.rs b/crates/assistant_tools/src/templates.rs new file mode 100644 index 0000000000..c83601199c --- /dev/null +++ b/crates/assistant_tools/src/templates.rs @@ -0,0 +1,32 @@ +use anyhow::Result; +use handlebars::Handlebars; +use rust_embed::RustEmbed; +use serde::Serialize; +use std::sync::Arc; + +#[derive(RustEmbed)] +#[folder = "src/templates"] +#[include = "*.hbs"] +struct Assets; + +pub struct Templates(Handlebars<'static>); + +impl Templates { + pub fn new() -> Arc { + let mut handlebars = Handlebars::new(); + handlebars.register_embed_templates::().unwrap(); + handlebars.register_escape_fn(|text| text.into()); + Arc::new(Self(handlebars)) + } +} + +pub trait Template: Sized { + const TEMPLATE_NAME: &'static str; + + fn render(&self, templates: &Templates) -> Result + where + Self: Serialize + Sized, + { + Ok(templates.0.render(Self::TEMPLATE_NAME, self)?) + } +} diff --git a/crates/assistant_tools/src/templates/diff_judge.hbs b/crates/assistant_tools/src/templates/diff_judge.hbs new file mode 100644 index 0000000000..0106cb4217 --- /dev/null +++ b/crates/assistant_tools/src/templates/diff_judge.hbs @@ -0,0 +1,23 @@ +You are an expert coder, and have been tasked with looking at the following diff: + + +{{diff}} + + +Evaluate the following assertions: + + +{{assertions}} + + +You must respond with a short analysis and a score between 0 and 100, where: +- 0 means no assertions pass +- 100 means all the assertions pass perfectly + + +- Assertion 1: one line describing why the first assertion passes or fails (even partially) +- Assertion 2: one line describing why the second assertion passes or fails (even partially) +- ... +- Assertion N: one line describing why the Nth assertion passes or fails (even partially) + +YOUR FINAL SCORE HERE diff --git a/crates/assistant_tools/src/templates/edit_agent.hbs b/crates/assistant_tools/src/templates/edit_agent.hbs new file mode 100644 index 0000000000..2cfa2b6501 --- /dev/null +++ b/crates/assistant_tools/src/templates/edit_agent.hbs @@ -0,0 +1,49 @@ +You are an expert text editor and your task is to produce a series of edits to a file given a description of the changes you need to make. + +You MUST respond with a series of edits to that one file in the following format: + +``` + + + +OLD TEXT 1 HERE + + +NEW TEXT 1 HERE + + + +OLD TEXT 2 HERE + + +NEW TEXT 2 HERE + + + +OLD TEXT 3 HERE + + +NEW TEXT 3 HERE + + + +``` + +Rules for editing: + +- `old_text` represents lines in the input file that will be replaced with `new_text`. `old_text` MUST exactly match the existing file content, character for character, including indentation. +- Always include enough context around the lines you want to replace in `old_text` such that it's impossible to mistake them for other lines. +- If you want to replace many occurrences of the same text, repeat the same `old_text`/`new_text` pair multiple times and I will apply them sequentially, one occurrence at a time. +- When reporting multiple edits, each edit assumes the previous one has already been applied! Therefore, you must ensure `old_text` doesn't reference text that has already been modified by a previous edit. +- Don't explain the edits, just report them. +- Only edit the file specified in `` and NEVER include edits to other files! +- If you open an tag, you MUST close it using +- If you open an tag, you MUST close it using + + +{{path}} + + + +{{edit_description}} + diff --git a/crates/eval/Cargo.toml b/crates/eval/Cargo.toml index 8c8d3a7830..77fd920866 100644 --- a/crates/eval/Cargo.toml +++ b/crates/eval/Cargo.toml @@ -48,6 +48,7 @@ markdown.workspace = true node_runtime.workspace = true pathdiff.workspace = true paths.workspace = true +pretty_assertions.workspace = true project.workspace = true prompt_store.workspace = true regex.workspace = true diff --git a/crates/eval/runner_settings.json b/crates/eval/runner_settings.json index 53d853023c..91f193d7b3 100644 --- a/crates/eval/runner_settings.json +++ b/crates/eval/runner_settings.json @@ -1,6 +1,7 @@ { "assistant": { "always_allow_tool_actions": true, + "stream_edits": true, "version": "2" } } diff --git a/crates/eval/src/eval.rs b/crates/eval/src/eval.rs index df3ed691ae..f19f8de23a 100644 --- a/crates/eval/src/eval.rs +++ b/crates/eval/src/eval.rs @@ -420,12 +420,12 @@ pub fn init(cx: &mut App) -> Arc { language_model::init(client.clone(), cx); language_models::init(user_store.clone(), client.clone(), fs.clone(), cx); languages::init(languages.clone(), node_runtime.clone(), cx); - assistant_tools::init(client.http_client(), cx); context_server::init(cx); prompt_store::init(cx); let stdout_is_a_pty = false; let prompt_builder = PromptBuilder::load(fs.clone(), stdout_is_a_pty, cx); agent::init(fs.clone(), client.clone(), prompt_builder.clone(), cx); + assistant_tools::init(client.http_client(), cx); SettingsStore::update_global(cx, |store, cx| { store.set_user_settings(include_str!("../runner_settings.json"), cx) diff --git a/crates/eval/src/example.rs b/crates/eval/src/example.rs index 497836b0fc..a943be6166 100644 --- a/crates/eval/src/example.rs +++ b/crates/eval/src/example.rs @@ -160,7 +160,11 @@ impl ExampleContext { if left == right { Ok(()) } else { - println!("{}{:#?} != {:#?}", self.log_prefix, left, right); + println!( + "{}{}", + self.log_prefix, + pretty_assertions::Comparison::new(&left, &right) + ); Err(anyhow::Error::from(FailedAssertion(message.clone()))) }, message, @@ -334,8 +338,8 @@ impl ExampleContext { } pub fn edits(&self) -> HashMap, FileEdits> { - self.app - .read_entity(&self.agent_thread, |thread, cx| { + self.agent_thread + .read_with(&self.app, |thread, cx| { let action_log = thread.action_log().read(cx); HashMap::from_iter(action_log.changed_buffers(cx).into_iter().map( |(buffer, diff)| { @@ -503,16 +507,16 @@ impl ToolUse { } } -#[derive(Debug)] +#[derive(Debug, Eq, PartialEq)] pub struct FileEdits { - hunks: Vec, + pub hunks: Vec, } -#[derive(Debug)] -struct FileEditHunk { - base_text: String, - text: String, - status: DiffHunkStatus, +#[derive(Debug, Eq, PartialEq)] +pub struct FileEditHunk { + pub base_text: String, + pub text: String, + pub status: DiffHunkStatus, } impl FileEdits { diff --git a/crates/feature_flags/src/feature_flags.rs b/crates/feature_flags/src/feature_flags.rs index 71c967d558..9751dd4148 100644 --- a/crates/feature_flags/src/feature_flags.rs +++ b/crates/feature_flags/src/feature_flags.rs @@ -59,6 +59,12 @@ impl FeatureFlag for Assistant2FeatureFlag { const NAME: &'static str = "assistant2"; } +pub struct AgentStreamEditsFeatureFlag; + +impl FeatureFlag for AgentStreamEditsFeatureFlag { + const NAME: &'static str = "agent-stream-edits"; +} + pub struct NewBillingFeatureFlag; impl FeatureFlag for NewBillingFeatureFlag { diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index ae37bf9adb..ae2ee2f2d1 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -78,7 +78,7 @@ pub(crate) use test::*; pub(crate) use windows::*; #[cfg(any(test, feature = "test-support"))] -pub use test::TestScreenCaptureSource; +pub use test::{TestDispatcher, TestScreenCaptureSource}; /// Returns a background executor for the current platform. pub fn background_executor() -> BackgroundExecutor { diff --git a/crates/gpui/src/platform/test.rs b/crates/gpui/src/platform/test.rs index 70462cb5e2..e4173b7c6b 100644 --- a/crates/gpui/src/platform/test.rs +++ b/crates/gpui/src/platform/test.rs @@ -3,7 +3,7 @@ mod display; mod platform; mod window; -pub(crate) use dispatcher::*; +pub use dispatcher::*; pub(crate) use display::*; pub(crate) use platform::*; pub(crate) use window::*; diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index dc3bd40de7..b84a0273e9 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -59,9 +59,9 @@ use text::operation_queue::OperationQueue; use text::*; pub use text::{ Anchor, Bias, Buffer as TextBuffer, BufferId, BufferSnapshot as TextBufferSnapshot, Edit, - OffsetRangeExt, OffsetUtf16, Patch, Point, PointUtf16, Rope, Selection, SelectionGoal, - Subscription, TextDimension, TextSummary, ToOffset, ToOffsetUtf16, ToPoint, ToPointUtf16, - Transaction, TransactionId, Unclipped, + LineIndent, OffsetRangeExt, OffsetUtf16, Patch, Point, PointUtf16, Rope, Selection, + SelectionGoal, Subscription, TextDimension, TextSummary, ToOffset, ToOffsetUtf16, ToPoint, + ToPointUtf16, Transaction, TransactionId, Unclipped, }; use theme::{ActiveTheme as _, SyntaxTheme}; #[cfg(any(test, feature = "test-support"))] diff --git a/crates/streaming_diff/src/streaming_diff.rs b/crates/streaming_diff/src/streaming_diff.rs index 5c20dccadb..f7649b1bf1 100644 --- a/crates/streaming_diff/src/streaming_diff.rs +++ b/crates/streaming_diff/src/streaming_diff.rs @@ -7,6 +7,7 @@ use std::{ ops::Range, }; +#[derive(Default)] struct Matrix { cells: Vec, rows: usize, @@ -95,6 +96,7 @@ pub enum CharOperation { Keep { bytes: usize }, } +#[derive(Default)] pub struct StreamingDiff { old: Vec, new: Vec, diff --git a/tooling/workspace-hack/Cargo.toml b/tooling/workspace-hack/Cargo.toml index cf1d8fa3a1..e0265af728 100644 --- a/tooling/workspace-hack/Cargo.toml +++ b/tooling/workspace-hack/Cargo.toml @@ -60,6 +60,7 @@ futures-sink = { version = "0.3" } futures-task = { version = "0.3", default-features = false, features = ["std"] } futures-util = { version = "0.3", features = ["channel", "io-compat", "sink"] } getrandom-6f8ce4dd05d13bba = { package = "getrandom", version = "0.2", default-features = false, features = ["std"] } +handlebars = { version = "4", features = ["rust-embed"] } hashbrown-3575ec1268b04181 = { package = "hashbrown", version = "0.15", features = ["serde"] } hashbrown-582f2526e08bb6a0 = { package = "hashbrown", version = "0.14", features = ["raw"] } hmac = { version = "0.12", default-features = false, features = ["reset"] } @@ -170,6 +171,7 @@ futures-sink = { version = "0.3" } futures-task = { version = "0.3", default-features = false, features = ["std"] } futures-util = { version = "0.3", features = ["channel", "io-compat", "sink"] } getrandom-6f8ce4dd05d13bba = { package = "getrandom", version = "0.2", default-features = false, features = ["std"] } +handlebars = { version = "4", features = ["rust-embed"] } hashbrown-3575ec1268b04181 = { package = "hashbrown", version = "0.15", features = ["serde"] } hashbrown-582f2526e08bb6a0 = { package = "hashbrown", version = "0.14", features = ["raw"] } heck = { version = "0.4", features = ["unicode"] }