diff --git a/Cargo.lock b/Cargo.lock index 347976691d..a185542c63 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -102,14 +102,20 @@ dependencies = [ "anyhow", "chrono", "collections", + "ctor", "editor", + "env_logger 0.9.3", "fs", "futures 0.3.28", "gpui", + "indoc", "isahc", "language", + "log", "menu", + "ordered-float", "project", + "rand 0.8.5", "regex", "schemars", "search", @@ -1447,7 +1453,7 @@ dependencies = [ [[package]] name = "collab" -version = "0.18.0" +version = "0.19.0" dependencies = [ "anyhow", "async-tungstenite", @@ -2762,6 +2768,7 @@ dependencies = [ "smol", "sum_tree", "tempfile", + "text", "time 0.3.27", "util", ] @@ -4170,8 +4177,7 @@ dependencies = [ [[package]] name = "lsp-types" version = "0.94.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c66bfd44a06ae10647fe3f8214762e9369fd4248df1350924b4ef9e770a85ea1" +source = "git+https://github.com/zed-industries/lsp-types?branch=updated-completion-list-item-defaults#90a040a1d195687bd19e1df47463320a44e93d7a" dependencies = [ "bitflags 1.3.2", "serde", @@ -5649,6 +5655,7 @@ dependencies = [ name = "quick_action_bar" version = "0.1.0" dependencies = [ + "ai", "editor", "gpui", "search", @@ -7629,7 +7636,6 @@ dependencies = [ "ctor", "digest 0.9.0", "env_logger 0.9.3", - "fs", "gpui", "lazy_static", "log", @@ -9695,7 +9701,7 @@ dependencies = [ [[package]] name = "zed" -version = "0.102.0" +version = "0.103.0" dependencies = [ "activity_indicator", "ai", diff --git a/assets/icons/check_circle.svg b/assets/icons/check_circle.svg index 85ba2e1f37..b48fe34631 100644 --- a/assets/icons/check_circle.svg +++ b/assets/icons/check_circle.svg @@ -1,4 +1,4 @@ - - + + diff --git a/assets/icons/error.svg b/assets/icons/error.svg index 82b9401d08..593629beee 100644 --- a/assets/icons/error.svg +++ b/assets/icons/error.svg @@ -1,4 +1,4 @@ - - + + diff --git a/assets/icons/warning.svg b/assets/icons/warning.svg index 6b3d0fd41e..e581def0d0 100644 --- a/assets/icons/warning.svg +++ b/assets/icons/warning.svg @@ -1,5 +1,6 @@ - - - + + + + diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index a1c771da02..1b2d8ce419 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -522,7 +522,7 @@ // TODO: Move this to a dock open action "cmd-shift-c": "collab_panel::ToggleFocus", "cmd-alt-i": "zed::DebugElements", - "ctrl-shift-:": "editor::ToggleInlayHints", + "ctrl-:": "editor::ToggleInlayHints", } }, { @@ -530,7 +530,8 @@ "bindings": { "alt-enter": "editor::OpenExcerpts", "cmd-f8": "editor::GoToHunk", - "cmd-shift-f8": "editor::GoToPrevHunk" + "cmd-shift-f8": "editor::GoToPrevHunk", + "ctrl-enter": "assistant::InlineAssist" } }, { diff --git a/crates/ai/Cargo.toml b/crates/ai/Cargo.toml index 013565e14f..4438f88108 100644 --- a/crates/ai/Cargo.toml +++ b/crates/ai/Cargo.toml @@ -24,7 +24,9 @@ workspace = { path = "../workspace" } anyhow.workspace = true chrono = { version = "0.4", features = ["serde"] } futures.workspace = true +indoc.workspace = true isahc.workspace = true +ordered-float.workspace = true regex.workspace = true schemars.workspace = true serde.workspace = true @@ -35,3 +37,8 @@ tiktoken-rs = "0.4" [dev-dependencies] editor = { path = "../editor", features = ["test-support"] } project = { path = "../project", features = ["test-support"] } + +ctor.workspace = true +env_logger.workspace = true +log.workspace = true +rand.workspace = true diff --git a/crates/ai/src/ai.rs b/crates/ai/src/ai.rs index d2be651bd5..2c2d7e774e 100644 --- a/crates/ai/src/ai.rs +++ b/crates/ai/src/ai.rs @@ -1,28 +1,33 @@ pub mod assistant; mod assistant_settings; +mod streaming_diff; -use anyhow::Result; +use anyhow::{anyhow, Result}; pub use assistant::AssistantPanel; use assistant_settings::OpenAIModel; use chrono::{DateTime, Local}; use collections::HashMap; use fs::Fs; -use futures::StreamExt; -use gpui::AppContext; +use futures::{io::BufReader, AsyncBufReadExt, AsyncReadExt, Stream, StreamExt}; +use gpui::{executor::Background, AppContext}; +use isahc::{http::StatusCode, Request, RequestExt}; use regex::Regex; use serde::{Deserialize, Serialize}; use std::{ cmp::Reverse, ffi::OsStr, fmt::{self, Display}, + io, path::PathBuf, sync::Arc, }; use util::paths::CONVERSATIONS_DIR; +const OPENAI_API_URL: &'static str = "https://api.openai.com/v1"; + // Data types for chat completion requests #[derive(Debug, Serialize)] -struct OpenAIRequest { +pub struct OpenAIRequest { model: String, messages: Vec, stream: bool, @@ -116,7 +121,7 @@ struct RequestMessage { } #[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] -struct ResponseMessage { +pub struct ResponseMessage { role: Option, content: Option, } @@ -150,7 +155,7 @@ impl Display for Role { } #[derive(Deserialize, Debug)] -struct OpenAIResponseStreamEvent { +pub struct OpenAIResponseStreamEvent { pub id: Option, pub object: String, pub created: u32, @@ -160,14 +165,14 @@ struct OpenAIResponseStreamEvent { } #[derive(Deserialize, Debug)] -struct Usage { +pub struct Usage { pub prompt_tokens: u32, pub completion_tokens: u32, pub total_tokens: u32, } #[derive(Deserialize, Debug)] -struct ChatChoiceDelta { +pub struct ChatChoiceDelta { pub index: u32, pub delta: ResponseMessage, pub finish_reason: Option, @@ -191,3 +196,97 @@ struct OpenAIChoice { pub fn init(cx: &mut AppContext) { assistant::init(cx); } + +pub async fn stream_completion( + api_key: String, + executor: Arc, + mut request: OpenAIRequest, +) -> Result>> { + request.stream = true; + + let (tx, rx) = futures::channel::mpsc::unbounded::>(); + + let json_data = serde_json::to_string(&request)?; + let mut response = Request::post(format!("{OPENAI_API_URL}/chat/completions")) + .header("Content-Type", "application/json") + .header("Authorization", format!("Bearer {}", api_key)) + .body(json_data)? + .send_async() + .await?; + + let status = response.status(); + if status == StatusCode::OK { + executor + .spawn(async move { + let mut lines = BufReader::new(response.body_mut()).lines(); + + fn parse_line( + line: Result, + ) -> Result> { + if let Some(data) = line?.strip_prefix("data: ") { + let event = serde_json::from_str(&data)?; + Ok(Some(event)) + } else { + Ok(None) + } + } + + while let Some(line) = lines.next().await { + if let Some(event) = parse_line(line).transpose() { + let done = event.as_ref().map_or(false, |event| { + event + .choices + .last() + .map_or(false, |choice| choice.finish_reason.is_some()) + }); + if tx.unbounded_send(event).is_err() { + break; + } + + if done { + break; + } + } + } + + anyhow::Ok(()) + }) + .detach(); + + Ok(rx) + } else { + let mut body = String::new(); + response.body_mut().read_to_string(&mut body).await?; + + #[derive(Deserialize)] + struct OpenAIResponse { + error: OpenAIError, + } + + #[derive(Deserialize)] + struct OpenAIError { + message: String, + } + + match serde_json::from_str::(&body) { + Ok(response) if !response.error.message.is_empty() => Err(anyhow!( + "Failed to connect to OpenAI API: {}", + response.error.message, + )), + + _ => Err(anyhow!( + "Failed to connect to OpenAI API: {} {}", + response.status(), + body, + )), + } + } +} + +#[cfg(test)] +#[ctor::ctor] +fn init_logger() { + if std::env::var("RUST_LOG").is_ok() { + env_logger::init(); + } +} diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index 3c561b0e03..9b384252fc 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -1,53 +1,63 @@ use crate::{ assistant_settings::{AssistantDockPosition, AssistantSettings, OpenAIModel}, - MessageId, MessageMetadata, MessageStatus, OpenAIRequest, OpenAIResponseStreamEvent, - RequestMessage, Role, SavedConversation, SavedConversationMetadata, SavedMessage, + stream_completion, + streaming_diff::{Hunk, StreamingDiff}, + MessageId, MessageMetadata, MessageStatus, OpenAIRequest, RequestMessage, Role, + SavedConversation, SavedConversationMetadata, SavedMessage, OPENAI_API_URL, }; use anyhow::{anyhow, Result}; use chrono::{DateTime, Local}; -use collections::{HashMap, HashSet}; +use collections::{hash_map, HashMap, HashSet, VecDeque}; use editor::{ - display_map::{BlockDisposition, BlockId, BlockProperties, BlockStyle, ToDisplayPoint}, + display_map::{ + BlockContext, BlockDisposition, BlockId, BlockProperties, BlockStyle, ToDisplayPoint, + }, scroll::autoscroll::{Autoscroll, AutoscrollStrategy}, - Anchor, Editor, ToOffset, + Anchor, Editor, MoveDown, MoveUp, MultiBufferSnapshot, ToOffset, ToPoint, }; use fs::Fs; -use futures::{io::BufReader, AsyncBufReadExt, AsyncReadExt, Stream, StreamExt}; +use futures::{channel::mpsc, SinkExt, Stream, StreamExt}; use gpui::{ actions, - elements::*, - executor::Background, + elements::{ + ChildView, Component, Empty, Flex, Label, MouseEventHandler, ParentElement, SafeStylable, + Stack, Svg, Text, UniformList, UniformListState, + }, + fonts::HighlightStyle, geometry::vector::{vec2f, Vector2F}, platform::{CursorStyle, MouseButton}, - Action, AppContext, AsyncAppContext, ClipboardItem, Entity, ModelContext, ModelHandle, - Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle, WindowContext, + Action, AnyElement, AppContext, AsyncAppContext, ClipboardItem, Element, Entity, ModelContext, + ModelHandle, SizeConstraint, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle, + WindowContext, +}; +use language::{ + language_settings::SoftWrap, Buffer, LanguageRegistry, Point, Rope, ToOffset as _, + TransactionId, }; -use isahc::{http::StatusCode, Request, RequestExt}; -use language::{language_settings::SoftWrap, Buffer, LanguageRegistry, ToOffset as _}; use search::BufferSearchBar; -use serde::Deserialize; use settings::SettingsStore; use std::{ - cell::RefCell, + cell::{Cell, RefCell}, cmp, env, fmt::Write, - io, iter, + future, iter, ops::Range, path::{Path, PathBuf}, rc::Rc, sync::Arc, time::Duration, }; -use theme::AssistantStyle; +use theme::{ + components::{action_button::Button, ComponentExt}, + AssistantStyle, +}; use util::{paths::CONVERSATIONS_DIR, post_inc, ResultExt, TryFutureExt}; use workspace::{ dock::{DockPosition, Panel}, searchable::Direction, - Save, ToggleZoom, Toolbar, Workspace, + Save, Toast, ToggleZoom, Toolbar, Workspace, }; -const OPENAI_API_URL: &'static str = "https://api.openai.com/v1"; - actions!( assistant, [ @@ -58,6 +68,8 @@ actions!( QuoteSelection, ToggleFocus, ResetKey, + InlineAssist, + ToggleIncludeConversation, ] ); @@ -89,6 +101,13 @@ pub fn init(cx: &mut AppContext) { workspace.toggle_panel_focus::(cx); }, ); + cx.add_action(AssistantPanel::inline_assist); + cx.add_action(AssistantPanel::cancel_last_inline_assist); + cx.add_action(InlineAssistant::confirm); + cx.add_action(InlineAssistant::cancel); + cx.add_action(InlineAssistant::toggle_include_conversation); + cx.add_action(InlineAssistant::move_up); + cx.add_action(InlineAssistant::move_down); } #[derive(Debug)] @@ -118,10 +137,17 @@ pub struct AssistantPanel { languages: Arc, fs: Arc, subscriptions: Vec, + next_inline_assist_id: usize, + pending_inline_assists: HashMap, + pending_inline_assist_ids_by_editor: HashMap, Vec>, + include_conversation_in_next_inline_assist: bool, + inline_prompt_history: VecDeque, _watch_saved_conversations: Task>, } impl AssistantPanel { + const INLINE_PROMPT_HISTORY_MAX_LEN: usize = 20; + pub fn load( workspace: WeakViewHandle, cx: AsyncAppContext, @@ -181,6 +207,11 @@ impl AssistantPanel { width: None, height: None, subscriptions: Default::default(), + next_inline_assist_id: 0, + pending_inline_assists: Default::default(), + pending_inline_assist_ids_by_editor: Default::default(), + include_conversation_in_next_inline_assist: false, + inline_prompt_history: Default::default(), _watch_saved_conversations, }; @@ -201,6 +232,717 @@ impl AssistantPanel { }) } + pub fn inline_assist( + workspace: &mut Workspace, + _: &InlineAssist, + cx: &mut ViewContext, + ) { + let this = if let Some(this) = workspace.panel::(cx) { + if this + .update(cx, |assistant, cx| assistant.load_api_key(cx)) + .is_some() + { + this + } else { + workspace.focus_panel::(cx); + return; + } + } else { + return; + }; + + let active_editor = if let Some(active_editor) = workspace + .active_item(cx) + .and_then(|item| item.act_as::(cx)) + { + active_editor + } else { + return; + }; + + this.update(cx, |assistant, cx| { + assistant.new_inline_assist(&active_editor, cx) + }); + } + + fn new_inline_assist(&mut self, editor: &ViewHandle, cx: &mut ViewContext) { + let inline_assist_id = post_inc(&mut self.next_inline_assist_id); + let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx); + let selection = editor.read(cx).selections.newest_anchor().clone(); + let range = selection.start.bias_left(&snapshot)..selection.end.bias_right(&snapshot); + let assist_kind = if editor.read(cx).selections.newest::(cx).is_empty() { + InlineAssistKind::Generate + } else { + InlineAssistKind::Transform + }; + let measurements = Rc::new(Cell::new(BlockMeasurements::default())); + let inline_assistant = cx.add_view(|cx| { + let assistant = InlineAssistant::new( + inline_assist_id, + assist_kind, + measurements.clone(), + self.include_conversation_in_next_inline_assist, + self.inline_prompt_history.clone(), + cx, + ); + cx.focus_self(); + assistant + }); + let block_id = editor.update(cx, |editor, cx| { + editor.change_selections(None, cx, |selections| { + selections.select_anchor_ranges([selection.head()..selection.head()]) + }); + editor.insert_blocks( + [BlockProperties { + style: BlockStyle::Flex, + position: selection.head().bias_left(&snapshot), + height: 2, + render: Arc::new({ + let inline_assistant = inline_assistant.clone(); + move |cx: &mut BlockContext| { + measurements.set(BlockMeasurements { + anchor_x: cx.anchor_x, + gutter_width: cx.gutter_width, + }); + ChildView::new(&inline_assistant, cx).into_any() + } + }), + disposition: if selection.reversed { + BlockDisposition::Above + } else { + BlockDisposition::Below + }, + }], + Some(Autoscroll::Strategy(AutoscrollStrategy::Newest)), + cx, + )[0] + }); + + self.pending_inline_assists.insert( + inline_assist_id, + PendingInlineAssist { + kind: assist_kind, + editor: editor.downgrade(), + range, + highlighted_ranges: Default::default(), + inline_assistant: Some((block_id, inline_assistant.clone())), + code_generation: Task::ready(None), + transaction_id: None, + _subscriptions: vec![ + cx.subscribe(&inline_assistant, Self::handle_inline_assistant_event), + cx.subscribe(editor, { + let inline_assistant = inline_assistant.downgrade(); + move |this, editor, event, cx| { + if let Some(inline_assistant) = inline_assistant.upgrade(cx) { + match event { + editor::Event::SelectionsChanged { local } => { + if *local && inline_assistant.read(cx).has_focus { + cx.focus(&editor); + } + } + editor::Event::TransactionUndone { + transaction_id: tx_id, + } => { + if let Some(pending_assist) = + this.pending_inline_assists.get(&inline_assist_id) + { + if pending_assist.transaction_id == Some(*tx_id) { + // Notice we are supplying `undo: false` here. This + // is because there's no need to undo the transaction + // because the user just did so. + this.close_inline_assist( + inline_assist_id, + false, + cx, + ); + } + } + } + _ => {} + } + } + } + }), + ], + }, + ); + self.pending_inline_assist_ids_by_editor + .entry(editor.downgrade()) + .or_default() + .push(inline_assist_id); + self.update_highlights_for_editor(&editor, cx); + } + + fn handle_inline_assistant_event( + &mut self, + inline_assistant: ViewHandle, + event: &InlineAssistantEvent, + cx: &mut ViewContext, + ) { + let assist_id = inline_assistant.read(cx).id; + match event { + InlineAssistantEvent::Confirmed { + prompt, + include_conversation, + } => { + self.confirm_inline_assist(assist_id, prompt, *include_conversation, cx); + } + InlineAssistantEvent::Canceled => { + self.close_inline_assist(assist_id, true, cx); + } + InlineAssistantEvent::Dismissed => { + self.hide_inline_assist(assist_id, cx); + } + InlineAssistantEvent::IncludeConversationToggled { + include_conversation, + } => { + self.include_conversation_in_next_inline_assist = *include_conversation; + } + } + } + + fn cancel_last_inline_assist( + workspace: &mut Workspace, + _: &editor::Cancel, + cx: &mut ViewContext, + ) { + if let Some(panel) = workspace.panel::(cx) { + if let Some(editor) = workspace + .active_item(cx) + .and_then(|item| item.downcast::()) + { + let handled = panel.update(cx, |panel, cx| { + if let Some(assist_id) = panel + .pending_inline_assist_ids_by_editor + .get(&editor.downgrade()) + .and_then(|assist_ids| assist_ids.last().copied()) + { + panel.close_inline_assist(assist_id, true, cx); + true + } else { + false + } + }); + if handled { + return; + } + } + } + + cx.propagate_action(); + } + + fn close_inline_assist(&mut self, assist_id: usize, undo: bool, cx: &mut ViewContext) { + self.hide_inline_assist(assist_id, cx); + + if let Some(pending_assist) = self.pending_inline_assists.remove(&assist_id) { + if let hash_map::Entry::Occupied(mut entry) = self + .pending_inline_assist_ids_by_editor + .entry(pending_assist.editor) + { + entry.get_mut().retain(|id| *id != assist_id); + if entry.get().is_empty() { + entry.remove(); + } + } + + if let Some(editor) = pending_assist.editor.upgrade(cx) { + self.update_highlights_for_editor(&editor, cx); + + if undo { + if let Some(transaction_id) = pending_assist.transaction_id { + editor.update(cx, |editor, cx| { + editor.buffer().update(cx, |buffer, cx| { + buffer.undo_transaction(transaction_id, cx) + }); + }); + } + } + } + } + } + + fn hide_inline_assist(&mut self, assist_id: usize, cx: &mut ViewContext) { + if let Some(pending_assist) = self.pending_inline_assists.get_mut(&assist_id) { + if let Some(editor) = pending_assist.editor.upgrade(cx) { + if let Some((block_id, _)) = pending_assist.inline_assistant.take() { + editor.update(cx, |editor, cx| { + editor.remove_blocks(HashSet::from_iter([block_id]), None, cx); + }); + } + } + } + } + + fn confirm_inline_assist( + &mut self, + inline_assist_id: usize, + user_prompt: &str, + include_conversation: bool, + cx: &mut ViewContext, + ) { + let api_key = if let Some(api_key) = self.api_key.borrow().clone() { + api_key + } else { + return; + }; + + let conversation = if include_conversation { + self.active_editor() + .map(|editor| editor.read(cx).conversation.clone()) + } else { + None + }; + + let pending_assist = + if let Some(pending_assist) = self.pending_inline_assists.get_mut(&inline_assist_id) { + pending_assist + } else { + return; + }; + + let editor = if let Some(editor) = pending_assist.editor.upgrade(cx) { + editor + } else { + return; + }; + + self.inline_prompt_history + .retain(|prompt| prompt != user_prompt); + self.inline_prompt_history.push_back(user_prompt.into()); + if self.inline_prompt_history.len() > Self::INLINE_PROMPT_HISTORY_MAX_LEN { + self.inline_prompt_history.pop_front(); + } + + let range = pending_assist.range.clone(); + let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx); + let selected_text = snapshot + .text_for_range(range.start..range.end) + .collect::(); + + let selection_start = range.start.to_point(&snapshot); + let selection_end = range.end.to_point(&snapshot); + + let mut base_indent: Option = None; + let mut start_row = selection_start.row; + if snapshot.is_line_blank(start_row) { + if let Some(prev_non_blank_row) = snapshot.prev_non_blank_row(start_row) { + start_row = prev_non_blank_row; + } + } + for row in start_row..=selection_end.row { + if snapshot.is_line_blank(row) { + continue; + } + + let line_indent = snapshot.indent_size_for_line(row); + if let Some(base_indent) = base_indent.as_mut() { + if line_indent.len < base_indent.len { + *base_indent = line_indent; + } + } else { + base_indent = Some(line_indent); + } + } + + let mut normalized_selected_text = selected_text.clone(); + if let Some(base_indent) = base_indent { + for row in selection_start.row..=selection_end.row { + let selection_row = row - selection_start.row; + let line_start = + normalized_selected_text.point_to_offset(Point::new(selection_row, 0)); + let indent_len = if row == selection_start.row { + base_indent.len.saturating_sub(selection_start.column) + } else { + let line_len = normalized_selected_text.line_len(selection_row); + cmp::min(line_len, base_indent.len) + }; + let indent_end = cmp::min( + line_start + indent_len as usize, + normalized_selected_text.len(), + ); + normalized_selected_text.replace(line_start..indent_end, ""); + } + } + + let language = snapshot.language_at(range.start); + let language_name = if let Some(language) = language.as_ref() { + if Arc::ptr_eq(language, &language::PLAIN_TEXT) { + None + } else { + Some(language.name()) + } + } else { + None + }; + let language_name = language_name.as_deref(); + + let mut prompt = String::new(); + if let Some(language_name) = language_name { + writeln!(prompt, "You're an expert {language_name} engineer.").unwrap(); + } + match pending_assist.kind { + InlineAssistKind::Transform => { + writeln!( + prompt, + "You're currently working inside an editor on this file:" + ) + .unwrap(); + if let Some(language_name) = language_name { + writeln!(prompt, "```{language_name}").unwrap(); + } else { + writeln!(prompt, "```").unwrap(); + } + for chunk in snapshot.text_for_range(Anchor::min()..Anchor::max()) { + write!(prompt, "{chunk}").unwrap(); + } + writeln!(prompt, "```").unwrap(); + + writeln!( + prompt, + "In particular, the user has selected the following text:" + ) + .unwrap(); + if let Some(language_name) = language_name { + writeln!(prompt, "```{language_name}").unwrap(); + } else { + writeln!(prompt, "```").unwrap(); + } + writeln!(prompt, "{normalized_selected_text}").unwrap(); + writeln!(prompt, "```").unwrap(); + writeln!(prompt).unwrap(); + writeln!( + prompt, + "Modify the selected text given the user prompt: {user_prompt}" + ) + .unwrap(); + writeln!( + prompt, + "You MUST reply only with the edited selected text, not the entire file." + ) + .unwrap(); + } + InlineAssistKind::Generate => { + writeln!( + prompt, + "You're currently working inside an editor on this file:" + ) + .unwrap(); + if let Some(language_name) = language_name { + writeln!(prompt, "```{language_name}").unwrap(); + } else { + writeln!(prompt, "```").unwrap(); + } + for chunk in snapshot.text_for_range(Anchor::min()..range.start) { + write!(prompt, "{chunk}").unwrap(); + } + write!(prompt, "<|>").unwrap(); + for chunk in snapshot.text_for_range(range.start..Anchor::max()) { + write!(prompt, "{chunk}").unwrap(); + } + writeln!(prompt).unwrap(); + writeln!(prompt, "```").unwrap(); + writeln!( + prompt, + "Assume the cursor is located where the `<|>` marker is." + ) + .unwrap(); + writeln!( + prompt, + "Text can't be replaced, so assume your answer will be inserted at the cursor." + ) + .unwrap(); + writeln!( + prompt, + "Complete the text given the user prompt: {user_prompt}" + ) + .unwrap(); + } + } + if let Some(language_name) = language_name { + writeln!(prompt, "Your answer MUST always be valid {language_name}.").unwrap(); + } + writeln!(prompt, "Always wrap your response in a Markdown codeblock.").unwrap(); + writeln!(prompt, "Never make remarks about the output.").unwrap(); + + let mut messages = Vec::new(); + let mut model = settings::get::(cx) + .default_open_ai_model + .clone(); + if let Some(conversation) = conversation { + let conversation = conversation.read(cx); + let buffer = conversation.buffer.read(cx); + messages.extend( + conversation + .messages(cx) + .map(|message| message.to_open_ai_message(buffer)), + ); + model = conversation.model.clone(); + } + + messages.push(RequestMessage { + role: Role::User, + content: prompt, + }); + let request = OpenAIRequest { + model: model.full_name().into(), + messages, + stream: true, + }; + let response = stream_completion(api_key, cx.background().clone(), request); + let editor = editor.downgrade(); + + pending_assist.code_generation = cx.spawn(|this, mut cx| { + async move { + let mut edit_start = range.start.to_offset(&snapshot); + + let (mut hunks_tx, mut hunks_rx) = mpsc::channel(1); + let diff = cx.background().spawn(async move { + let chunks = strip_markdown_codeblock(response.await?.filter_map( + |message| async move { + match message { + Ok(mut message) => Some(Ok(message.choices.pop()?.delta.content?)), + Err(error) => Some(Err(error)), + } + }, + )); + futures::pin_mut!(chunks); + let mut diff = StreamingDiff::new(selected_text.to_string()); + + let mut indent_len; + let indent_text; + if let Some(base_indent) = base_indent { + indent_len = base_indent.len; + indent_text = match base_indent.kind { + language::IndentKind::Space => " ", + language::IndentKind::Tab => "\t", + }; + } else { + indent_len = 0; + indent_text = ""; + }; + + let mut first_line_len = 0; + let mut first_line_non_whitespace_char_ix = None; + let mut first_line = true; + let mut new_text = String::new(); + + while let Some(chunk) = chunks.next().await { + let chunk = chunk?; + + let mut lines = chunk.split('\n'); + if let Some(mut line) = lines.next() { + if first_line { + if first_line_non_whitespace_char_ix.is_none() { + if let Some(mut char_ix) = + line.find(|ch: char| !ch.is_whitespace()) + { + line = &line[char_ix..]; + char_ix += first_line_len; + first_line_non_whitespace_char_ix = Some(char_ix); + let first_line_indent = char_ix + .saturating_sub(selection_start.column as usize) + as usize; + new_text.push_str(&indent_text.repeat(first_line_indent)); + indent_len = indent_len.saturating_sub(char_ix as u32); + } + } + first_line_len += line.len(); + } + + if first_line_non_whitespace_char_ix.is_some() { + new_text.push_str(line); + } + } + + for line in lines { + first_line = false; + new_text.push('\n'); + if !line.is_empty() { + new_text.push_str(&indent_text.repeat(indent_len as usize)); + } + new_text.push_str(line); + } + + let hunks = diff.push_new(&new_text); + hunks_tx.send(hunks).await?; + new_text.clear(); + } + hunks_tx.send(diff.finish()).await?; + + anyhow::Ok(()) + }); + + while let Some(hunks) = hunks_rx.next().await { + let editor = if let Some(editor) = editor.upgrade(&cx) { + editor + } else { + break; + }; + + let this = if let Some(this) = this.upgrade(&cx) { + this + } else { + break; + }; + + this.update(&mut cx, |this, cx| { + let pending_assist = if let Some(pending_assist) = + this.pending_inline_assists.get_mut(&inline_assist_id) + { + pending_assist + } else { + return; + }; + + pending_assist.highlighted_ranges.clear(); + editor.update(cx, |editor, cx| { + let transaction = editor.buffer().update(cx, |buffer, cx| { + // Avoid grouping assistant edits with user edits. + buffer.finalize_last_transaction(cx); + + buffer.start_transaction(cx); + buffer.edit( + hunks.into_iter().filter_map(|hunk| match hunk { + Hunk::Insert { text } => { + let edit_start = snapshot.anchor_after(edit_start); + Some((edit_start..edit_start, text)) + } + Hunk::Remove { len } => { + let edit_end = edit_start + len; + let edit_range = snapshot.anchor_after(edit_start) + ..snapshot.anchor_before(edit_end); + edit_start = edit_end; + Some((edit_range, String::new())) + } + Hunk::Keep { len } => { + let edit_end = edit_start + len; + let edit_range = snapshot.anchor_after(edit_start) + ..snapshot.anchor_before(edit_end); + edit_start += len; + pending_assist.highlighted_ranges.push(edit_range); + None + } + }), + None, + cx, + ); + + buffer.end_transaction(cx) + }); + + if let Some(transaction) = transaction { + if let Some(first_transaction) = pending_assist.transaction_id { + // Group all assistant edits into the first transaction. + editor.buffer().update(cx, |buffer, cx| { + buffer.merge_transactions( + transaction, + first_transaction, + cx, + ) + }); + } else { + pending_assist.transaction_id = Some(transaction); + editor.buffer().update(cx, |buffer, cx| { + buffer.finalize_last_transaction(cx) + }); + } + } + }); + + this.update_highlights_for_editor(&editor, cx); + }); + } + + if let Err(error) = diff.await { + this.update(&mut cx, |this, cx| { + let pending_assist = if let Some(pending_assist) = + this.pending_inline_assists.get_mut(&inline_assist_id) + { + pending_assist + } else { + return; + }; + + if let Some((_, inline_assistant)) = + pending_assist.inline_assistant.as_ref() + { + inline_assistant.update(cx, |inline_assistant, cx| { + inline_assistant.set_error(error, cx); + }); + } else if let Some(workspace) = this.workspace.upgrade(cx) { + workspace.update(cx, |workspace, cx| { + workspace.show_toast( + Toast::new( + inline_assist_id, + format!("Inline assistant error: {}", error), + ), + cx, + ); + }) + } + })?; + } else { + let _ = this.update(&mut cx, |this, cx| { + this.close_inline_assist(inline_assist_id, false, cx) + }); + } + + anyhow::Ok(()) + } + .log_err() + }); + } + + fn update_highlights_for_editor( + &self, + editor: &ViewHandle, + cx: &mut ViewContext, + ) { + let mut background_ranges = Vec::new(); + let mut foreground_ranges = Vec::new(); + let empty_inline_assist_ids = Vec::new(); + let inline_assist_ids = self + .pending_inline_assist_ids_by_editor + .get(&editor.downgrade()) + .unwrap_or(&empty_inline_assist_ids); + + for inline_assist_id in inline_assist_ids { + if let Some(pending_assist) = self.pending_inline_assists.get(inline_assist_id) { + background_ranges.push(pending_assist.range.clone()); + foreground_ranges.extend(pending_assist.highlighted_ranges.iter().cloned()); + } + } + + let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx); + merge_ranges(&mut background_ranges, &snapshot); + merge_ranges(&mut foreground_ranges, &snapshot); + editor.update(cx, |editor, cx| { + if background_ranges.is_empty() { + editor.clear_background_highlights::(cx); + } else { + editor.highlight_background::( + background_ranges, + |theme| theme.assistant.inline.pending_edit_background, + cx, + ); + } + + if foreground_ranges.is_empty() { + editor.clear_text_highlights::(cx); + } else { + editor.highlight_text::( + foreground_ranges, + HighlightStyle { + fade_out: Some(0.6), + ..Default::default() + }, + cx, + ); + } + }); + } + fn new_conversation(&mut self, cx: &mut ViewContext) -> ViewHandle { let editor = cx.add_view(|cx| { ConversationEditor::new( @@ -570,6 +1312,32 @@ impl AssistantPanel { .iter() .position(|editor| editor.read(cx).conversation.read(cx).path.as_deref() == Some(path)) } + + fn load_api_key(&mut self, cx: &mut ViewContext) -> Option { + if self.api_key.borrow().is_none() && !self.has_read_credentials { + self.has_read_credentials = true; + let api_key = if let Ok(api_key) = env::var("OPENAI_API_KEY") { + Some(api_key) + } else if let Some((_, api_key)) = cx + .platform() + .read_credentials(OPENAI_API_URL) + .log_err() + .flatten() + { + String::from_utf8(api_key).log_err() + } else { + None + }; + if let Some(api_key) = api_key { + *self.api_key.borrow_mut() = Some(api_key); + } else if self.api_key_editor.is_none() { + self.api_key_editor = Some(build_api_key_editor(cx)); + cx.notify(); + } + } + + self.api_key.borrow().clone() + } } fn build_api_key_editor(cx: &mut ViewContext) -> ViewHandle { @@ -753,27 +1521,7 @@ impl Panel for AssistantPanel { fn set_active(&mut self, active: bool, cx: &mut ViewContext) { if active { - if self.api_key.borrow().is_none() && !self.has_read_credentials { - self.has_read_credentials = true; - let api_key = if let Ok(api_key) = env::var("OPENAI_API_KEY") { - Some(api_key) - } else if let Some((_, api_key)) = cx - .platform() - .read_credentials(OPENAI_API_URL) - .log_err() - .flatten() - { - String::from_utf8(api_key).log_err() - } else { - None - }; - if let Some(api_key) = api_key { - *self.api_key.borrow_mut() = Some(api_key); - } else if self.api_key_editor.is_none() { - self.api_key_editor = Some(build_api_key_editor(cx)); - cx.notify(); - } - } + self.load_api_key(cx); if self.editors.is_empty() { self.new_conversation(cx); @@ -1068,15 +1816,20 @@ impl Conversation { cx: &mut ModelContext, ) -> Vec { let mut user_messages = Vec::new(); - let mut tasks = Vec::new(); - let last_message_id = self.message_anchors.iter().rev().find_map(|message| { - message - .start - .is_valid(self.buffer.read(cx)) - .then_some(message.id) - }); + let last_message_id = if let Some(last_message_id) = + self.message_anchors.iter().rev().find_map(|message| { + message + .start + .is_valid(self.buffer.read(cx)) + .then_some(message.id) + }) { + last_message_id + } else { + return Default::default(); + }; + let mut should_assist = false; for selected_message_id in selected_messages { let selected_message_role = if let Some(metadata) = self.messages_metadata.get(&selected_message_id) { @@ -1093,144 +1846,111 @@ impl Conversation { cx, ) { user_messages.push(user_message); - } else { - continue; } } else { - let request = OpenAIRequest { - model: self.model.full_name().to_string(), - messages: self - .messages(cx) - .filter(|message| matches!(message.status, MessageStatus::Done)) - .flat_map(|message| { - let mut system_message = None; - if message.id == selected_message_id { - system_message = Some(RequestMessage { - role: Role::System, - content: concat!( - "Treat the following messages as additional knowledge you have learned about, ", - "but act as if they were not part of this conversation. That is, treat them ", - "as if the user didn't see them and couldn't possibly inquire about them." - ).into() - }); - } - - Some(message.to_open_ai_message(self.buffer.read(cx))).into_iter().chain(system_message) - }) - .chain(Some(RequestMessage { - role: Role::System, - content: format!( - "Direct your reply to message with id {}. Do not include a [Message X] header.", - selected_message_id.0 - ), - })) - .collect(), - stream: true, - }; - - let Some(api_key) = self.api_key.borrow().clone() else { - continue; - }; - let stream = stream_completion(api_key, cx.background().clone(), request); - let assistant_message = self - .insert_message_after( - selected_message_id, - Role::Assistant, - MessageStatus::Pending, - cx, - ) - .unwrap(); - - // Queue up the user's next reply - if Some(selected_message_id) == last_message_id { - let user_message = self - .insert_message_after( - assistant_message.id, - Role::User, - MessageStatus::Done, - cx, - ) - .unwrap(); - user_messages.push(user_message); - } - - tasks.push(cx.spawn_weak({ - |this, mut cx| async move { - let assistant_message_id = assistant_message.id; - let stream_completion = async { - let mut messages = stream.await?; - - while let Some(message) = messages.next().await { - let mut message = message?; - if let Some(choice) = message.choices.pop() { - this.upgrade(&cx) - .ok_or_else(|| anyhow!("conversation was dropped"))? - .update(&mut cx, |this, cx| { - let text: Arc = choice.delta.content?.into(); - let message_ix = this.message_anchors.iter().position( - |message| message.id == assistant_message_id, - )?; - this.buffer.update(cx, |buffer, cx| { - let offset = this.message_anchors[message_ix + 1..] - .iter() - .find(|message| message.start.is_valid(buffer)) - .map_or(buffer.len(), |message| { - message - .start - .to_offset(buffer) - .saturating_sub(1) - }); - buffer.edit([(offset..offset, text)], None, cx); - }); - cx.emit(ConversationEvent::StreamedCompletion); - - Some(()) - }); - } - smol::future::yield_now().await; - } - - this.upgrade(&cx) - .ok_or_else(|| anyhow!("conversation was dropped"))? - .update(&mut cx, |this, cx| { - this.pending_completions.retain(|completion| { - completion.id != this.completion_count - }); - this.summarize(cx); - }); - - anyhow::Ok(()) - }; - - let result = stream_completion.await; - if let Some(this) = this.upgrade(&cx) { - this.update(&mut cx, |this, cx| { - if let Some(metadata) = - this.messages_metadata.get_mut(&assistant_message.id) - { - match result { - Ok(_) => { - metadata.status = MessageStatus::Done; - } - Err(error) => { - metadata.status = MessageStatus::Error( - error.to_string().trim().into(), - ); - } - } - cx.notify(); - } - }); - } - } - })); + should_assist = true; } } - if !tasks.is_empty() { + if should_assist { + let Some(api_key) = self.api_key.borrow().clone() else { + return Default::default(); + }; + + let request = OpenAIRequest { + model: self.model.full_name().to_string(), + messages: self + .messages(cx) + .filter(|message| matches!(message.status, MessageStatus::Done)) + .map(|message| message.to_open_ai_message(self.buffer.read(cx))) + .collect(), + stream: true, + }; + + let stream = stream_completion(api_key, cx.background().clone(), request); + let assistant_message = self + .insert_message_after(last_message_id, Role::Assistant, MessageStatus::Pending, cx) + .unwrap(); + + // Queue up the user's next reply. + let user_message = self + .insert_message_after(assistant_message.id, Role::User, MessageStatus::Done, cx) + .unwrap(); + user_messages.push(user_message); + + let task = cx.spawn_weak({ + |this, mut cx| async move { + let assistant_message_id = assistant_message.id; + let stream_completion = async { + let mut messages = stream.await?; + + while let Some(message) = messages.next().await { + let mut message = message?; + if let Some(choice) = message.choices.pop() { + this.upgrade(&cx) + .ok_or_else(|| anyhow!("conversation was dropped"))? + .update(&mut cx, |this, cx| { + let text: Arc = choice.delta.content?.into(); + let message_ix = + this.message_anchors.iter().position(|message| { + message.id == assistant_message_id + })?; + this.buffer.update(cx, |buffer, cx| { + let offset = this.message_anchors[message_ix + 1..] + .iter() + .find(|message| message.start.is_valid(buffer)) + .map_or(buffer.len(), |message| { + message + .start + .to_offset(buffer) + .saturating_sub(1) + }); + buffer.edit([(offset..offset, text)], None, cx); + }); + cx.emit(ConversationEvent::StreamedCompletion); + + Some(()) + }); + } + smol::future::yield_now().await; + } + + this.upgrade(&cx) + .ok_or_else(|| anyhow!("conversation was dropped"))? + .update(&mut cx, |this, cx| { + this.pending_completions + .retain(|completion| completion.id != this.completion_count); + this.summarize(cx); + }); + + anyhow::Ok(()) + }; + + let result = stream_completion.await; + if let Some(this) = this.upgrade(&cx) { + this.update(&mut cx, |this, cx| { + if let Some(metadata) = + this.messages_metadata.get_mut(&assistant_message.id) + { + match result { + Ok(_) => { + metadata.status = MessageStatus::Done; + } + Err(error) => { + metadata.status = + MessageStatus::Error(error.to_string().trim().into()); + } + } + cx.notify(); + } + }); + } + } + }); + self.pending_completions.push(PendingCompletion { id: post_inc(&mut self.completion_count), - _tasks: tasks, + _task: task, }); } @@ -1597,7 +2317,7 @@ impl Conversation { struct PendingCompletion { id: usize, - _tasks: Vec>, + _task: Task<()>, } enum ConversationEditorEvent { @@ -2145,8 +2865,9 @@ pub struct Message { impl Message { fn to_open_ai_message(&self, buffer: &Buffer) -> RequestMessage { - let mut content = format!("[Message {}]\n", self.id.0).to_string(); - content.extend(buffer.text_for_range(self.offset_range.clone())); + let content = buffer + .text_for_range(self.offset_range.clone()) + .collect::(); RequestMessage { role: self.role, content: content.trim_end().into(), @@ -2154,96 +2875,374 @@ impl Message { } } -async fn stream_completion( - api_key: String, - executor: Arc, - mut request: OpenAIRequest, -) -> Result>> { - request.stream = true; +enum InlineAssistantEvent { + Confirmed { + prompt: String, + include_conversation: bool, + }, + Canceled, + Dismissed, + IncludeConversationToggled { + include_conversation: bool, + }, +} - let (tx, rx) = futures::channel::mpsc::unbounded::>(); +#[derive(Copy, Clone)] +enum InlineAssistKind { + Transform, + Generate, +} - let json_data = serde_json::to_string(&request)?; - let mut response = Request::post(format!("{OPENAI_API_URL}/chat/completions")) - .header("Content-Type", "application/json") - .header("Authorization", format!("Bearer {}", api_key)) - .body(json_data)? - .send_async() - .await?; +struct InlineAssistant { + id: usize, + prompt_editor: ViewHandle, + confirmed: bool, + has_focus: bool, + include_conversation: bool, + measurements: Rc>, + error: Option, + prompt_history: VecDeque, + prompt_history_ix: Option, + pending_prompt: String, + _subscription: Subscription, +} - let status = response.status(); - if status == StatusCode::OK { - executor - .spawn(async move { - let mut lines = BufReader::new(response.body_mut()).lines(); +impl Entity for InlineAssistant { + type Event = InlineAssistantEvent; +} - fn parse_line( - line: Result, - ) -> Result> { - if let Some(data) = line?.strip_prefix("data: ") { - let event = serde_json::from_str(&data)?; - Ok(Some(event)) +impl View for InlineAssistant { + fn ui_name() -> &'static str { + "InlineAssistant" + } + + fn render(&mut self, cx: &mut ViewContext) -> AnyElement { + enum ErrorIcon {} + let theme = theme::current(cx); + + Flex::row() + .with_child( + Flex::row() + .with_child( + Button::action(ToggleIncludeConversation) + .with_tooltip("Include Conversation", theme.tooltip.clone()) + .with_id(self.id) + .with_contents(theme::components::svg::Svg::new("icons/ai.svg")) + .toggleable(self.include_conversation) + .with_style(theme.assistant.inline.include_conversation.clone()) + .element() + .aligned(), + ) + .with_children(if let Some(error) = self.error.as_ref() { + Some( + Svg::new("icons/circle_x_mark_12.svg") + .with_color(theme.assistant.error_icon.color) + .constrained() + .with_width(theme.assistant.error_icon.width) + .contained() + .with_style(theme.assistant.error_icon.container) + .with_tooltip::( + self.id, + error.to_string(), + None, + theme.tooltip.clone(), + cx, + ) + .aligned(), + ) } else { - Ok(None) + None + }) + .aligned() + .constrained() + .dynamically({ + let measurements = self.measurements.clone(); + move |constraint, _, _| { + let measurements = measurements.get(); + SizeConstraint { + min: vec2f(measurements.gutter_width, constraint.min.y()), + max: vec2f(measurements.gutter_width, constraint.max.y()), + } + } + }), + ) + .with_child(Empty::new().constrained().dynamically({ + let measurements = self.measurements.clone(); + move |constraint, _, _| { + let measurements = measurements.get(); + SizeConstraint { + min: vec2f( + measurements.anchor_x - measurements.gutter_width, + constraint.min.y(), + ), + max: vec2f( + measurements.anchor_x - measurements.gutter_width, + constraint.max.y(), + ), } } + })) + .with_child( + ChildView::new(&self.prompt_editor, cx) + .aligned() + .left() + .flex(1., true), + ) + .contained() + .with_style(theme.assistant.inline.container) + .into_any() + .into_any() + } - while let Some(line) = lines.next().await { - if let Some(event) = parse_line(line).transpose() { - let done = event.as_ref().map_or(false, |event| { - event - .choices - .last() - .map_or(false, |choice| choice.finish_reason.is_some()) - }); - if tx.unbounded_send(event).is_err() { - break; - } + fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext) { + cx.focus(&self.prompt_editor); + self.has_focus = true; + } - if done { - break; - } - } - } + fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext) { + self.has_focus = false; + } +} - anyhow::Ok(()) - }) - .detach(); - - Ok(rx) - } else { - let mut body = String::new(); - response.body_mut().read_to_string(&mut body).await?; - - #[derive(Deserialize)] - struct OpenAIResponse { - error: OpenAIError, - } - - #[derive(Deserialize)] - struct OpenAIError { - message: String, - } - - match serde_json::from_str::(&body) { - Ok(response) if !response.error.message.is_empty() => Err(anyhow!( - "Failed to connect to OpenAI API: {}", - response.error.message, - )), - - _ => Err(anyhow!( - "Failed to connect to OpenAI API: {} {}", - response.status(), - body, - )), +impl InlineAssistant { + fn new( + id: usize, + kind: InlineAssistKind, + measurements: Rc>, + include_conversation: bool, + prompt_history: VecDeque, + cx: &mut ViewContext, + ) -> Self { + let prompt_editor = cx.add_view(|cx| { + let mut editor = Editor::single_line( + Some(Arc::new(|theme| theme.assistant.inline.editor.clone())), + cx, + ); + let placeholder = match kind { + InlineAssistKind::Transform => "Enter transformation prompt…", + InlineAssistKind::Generate => "Enter generation prompt…", + }; + editor.set_placeholder_text(placeholder, cx); + editor + }); + let subscription = cx.subscribe(&prompt_editor, Self::handle_prompt_editor_events); + Self { + id, + prompt_editor, + confirmed: false, + has_focus: false, + include_conversation, + measurements, + error: None, + prompt_history, + prompt_history_ix: None, + pending_prompt: String::new(), + _subscription: subscription, } } + + fn handle_prompt_editor_events( + &mut self, + _: ViewHandle, + event: &editor::Event, + cx: &mut ViewContext, + ) { + if let editor::Event::Edited = event { + self.pending_prompt = self.prompt_editor.read(cx).text(cx); + cx.notify(); + } + } + + fn cancel(&mut self, _: &editor::Cancel, cx: &mut ViewContext) { + cx.emit(InlineAssistantEvent::Canceled); + } + + fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext) { + if self.confirmed { + cx.emit(InlineAssistantEvent::Dismissed); + } else { + let prompt = self.prompt_editor.read(cx).text(cx); + self.prompt_editor.update(cx, |editor, cx| { + editor.set_read_only(true); + editor.set_field_editor_style( + Some(Arc::new(|theme| { + theme.assistant.inline.disabled_editor.clone() + })), + cx, + ); + }); + cx.emit(InlineAssistantEvent::Confirmed { + prompt, + include_conversation: self.include_conversation, + }); + self.confirmed = true; + self.error = None; + cx.notify(); + } + } + + fn toggle_include_conversation( + &mut self, + _: &ToggleIncludeConversation, + cx: &mut ViewContext, + ) { + self.include_conversation = !self.include_conversation; + cx.emit(InlineAssistantEvent::IncludeConversationToggled { + include_conversation: self.include_conversation, + }); + cx.notify(); + } + + fn set_error(&mut self, error: anyhow::Error, cx: &mut ViewContext) { + self.error = Some(error); + self.confirmed = false; + self.prompt_editor.update(cx, |editor, cx| { + editor.set_read_only(false); + editor.set_field_editor_style( + Some(Arc::new(|theme| theme.assistant.inline.editor.clone())), + cx, + ); + }); + cx.notify(); + } + + fn move_up(&mut self, _: &MoveUp, cx: &mut ViewContext) { + if let Some(ix) = self.prompt_history_ix { + if ix > 0 { + self.prompt_history_ix = Some(ix - 1); + let prompt = self.prompt_history[ix - 1].clone(); + self.set_prompt(&prompt, cx); + } + } else if !self.prompt_history.is_empty() { + self.prompt_history_ix = Some(self.prompt_history.len() - 1); + let prompt = self.prompt_history[self.prompt_history.len() - 1].clone(); + self.set_prompt(&prompt, cx); + } + } + + fn move_down(&mut self, _: &MoveDown, cx: &mut ViewContext) { + if let Some(ix) = self.prompt_history_ix { + if ix < self.prompt_history.len() - 1 { + self.prompt_history_ix = Some(ix + 1); + let prompt = self.prompt_history[ix + 1].clone(); + self.set_prompt(&prompt, cx); + } else { + self.prompt_history_ix = None; + let pending_prompt = self.pending_prompt.clone(); + self.set_prompt(&pending_prompt, cx); + } + } + } + + fn set_prompt(&mut self, prompt: &str, cx: &mut ViewContext) { + self.prompt_editor.update(cx, |editor, cx| { + editor.buffer().update(cx, |buffer, cx| { + let len = buffer.len(cx); + buffer.edit([(0..len, prompt)], None, cx); + }); + }); + } +} + +// This wouldn't need to exist if we could pass parameters when rendering child views. +#[derive(Copy, Clone, Default)] +struct BlockMeasurements { + anchor_x: f32, + gutter_width: f32, +} + +struct PendingInlineAssist { + kind: InlineAssistKind, + editor: WeakViewHandle, + range: Range, + highlighted_ranges: Vec>, + inline_assistant: Option<(BlockId, ViewHandle)>, + code_generation: Task>, + transaction_id: Option, + _subscriptions: Vec, +} + +fn merge_ranges(ranges: &mut Vec>, buffer: &MultiBufferSnapshot) { + ranges.sort_unstable_by(|a, b| { + a.start + .cmp(&b.start, buffer) + .then_with(|| b.end.cmp(&a.end, buffer)) + }); + + let mut ix = 0; + while ix + 1 < ranges.len() { + let b = ranges[ix + 1].clone(); + let a = &mut ranges[ix]; + if a.end.cmp(&b.start, buffer).is_gt() { + if a.end.cmp(&b.end, buffer).is_lt() { + a.end = b.end; + } + ranges.remove(ix + 1); + } else { + ix += 1; + } + } +} + +fn strip_markdown_codeblock( + stream: impl Stream>, +) -> impl Stream> { + let mut first_line = true; + let mut buffer = String::new(); + let mut starts_with_fenced_code_block = false; + stream.filter_map(move |chunk| { + let chunk = match chunk { + Ok(chunk) => chunk, + Err(err) => return future::ready(Some(Err(err))), + }; + buffer.push_str(&chunk); + + if first_line { + if buffer == "" || buffer == "`" || buffer == "``" { + return future::ready(None); + } else if buffer.starts_with("```") { + starts_with_fenced_code_block = true; + if let Some(newline_ix) = buffer.find('\n') { + buffer.replace_range(..newline_ix + 1, ""); + first_line = false; + } else { + return future::ready(None); + } + } + } + + let text = if starts_with_fenced_code_block { + buffer + .strip_suffix("\n```\n") + .or_else(|| buffer.strip_suffix("\n```")) + .or_else(|| buffer.strip_suffix("\n``")) + .or_else(|| buffer.strip_suffix("\n`")) + .or_else(|| buffer.strip_suffix('\n')) + .unwrap_or(&buffer) + } else { + &buffer + }; + + if text.contains('\n') { + first_line = false; + } + + let remainder = buffer.split_off(text.len()); + let result = if buffer.is_empty() { + None + } else { + Some(Ok(buffer.clone())) + }; + buffer = remainder; + future::ready(result) + }) } #[cfg(test)] mod tests { use super::*; use crate::MessageId; + use futures::stream; use gpui::AppContext; #[gpui::test] @@ -2612,6 +3611,62 @@ mod tests { ); } + #[gpui::test] + async fn test_strip_markdown_codeblock() { + assert_eq!( + strip_markdown_codeblock(chunks("Lorem ipsum dolor", 2)) + .map(|chunk| chunk.unwrap()) + .collect::() + .await, + "Lorem ipsum dolor" + ); + assert_eq!( + strip_markdown_codeblock(chunks("```\nLorem ipsum dolor", 2)) + .map(|chunk| chunk.unwrap()) + .collect::() + .await, + "Lorem ipsum dolor" + ); + assert_eq!( + strip_markdown_codeblock(chunks("```\nLorem ipsum dolor\n```", 2)) + .map(|chunk| chunk.unwrap()) + .collect::() + .await, + "Lorem ipsum dolor" + ); + assert_eq!( + strip_markdown_codeblock(chunks("```\nLorem ipsum dolor\n```\n", 2)) + .map(|chunk| chunk.unwrap()) + .collect::() + .await, + "Lorem ipsum dolor" + ); + assert_eq!( + strip_markdown_codeblock(chunks("```html\n```js\nLorem ipsum dolor\n```\n```", 2)) + .map(|chunk| chunk.unwrap()) + .collect::() + .await, + "```js\nLorem ipsum dolor\n```" + ); + assert_eq!( + strip_markdown_codeblock(chunks("``\nLorem ipsum dolor\n```", 2)) + .map(|chunk| chunk.unwrap()) + .collect::() + .await, + "``\nLorem ipsum dolor\n```" + ); + + fn chunks(text: &str, size: usize) -> impl Stream> { + stream::iter( + text.chars() + .collect::>() + .chunks(size) + .map(|chunk| Ok(chunk.iter().collect::())) + .collect::>(), + ) + } + } + fn messages( conversation: &ModelHandle, cx: &AppContext, diff --git a/crates/ai/src/streaming_diff.rs b/crates/ai/src/streaming_diff.rs new file mode 100644 index 0000000000..7399a7b4fa --- /dev/null +++ b/crates/ai/src/streaming_diff.rs @@ -0,0 +1,293 @@ +use collections::HashMap; +use ordered_float::OrderedFloat; +use std::{ + cmp, + fmt::{self, Debug}, + ops::Range, +}; + +struct Matrix { + cells: Vec, + rows: usize, + cols: usize, +} + +impl Matrix { + fn new() -> Self { + Self { + cells: Vec::new(), + rows: 0, + cols: 0, + } + } + + fn resize(&mut self, rows: usize, cols: usize) { + self.cells.resize(rows * cols, 0.); + self.rows = rows; + self.cols = cols; + } + + fn get(&self, row: usize, col: usize) -> f64 { + if row >= self.rows { + panic!("row out of bounds") + } + + if col >= self.cols { + panic!("col out of bounds") + } + self.cells[col * self.rows + row] + } + + fn set(&mut self, row: usize, col: usize, value: f64) { + if row >= self.rows { + panic!("row out of bounds") + } + + if col >= self.cols { + panic!("col out of bounds") + } + + self.cells[col * self.rows + row] = value; + } +} + +impl Debug for Matrix { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + writeln!(f)?; + for i in 0..self.rows { + for j in 0..self.cols { + write!(f, "{:5}", self.get(i, j))?; + } + writeln!(f)?; + } + Ok(()) + } +} + +#[derive(Debug)] +pub enum Hunk { + Insert { text: String }, + Remove { len: usize }, + Keep { len: usize }, +} + +pub struct StreamingDiff { + old: Vec, + new: Vec, + scores: Matrix, + old_text_ix: usize, + new_text_ix: usize, + equal_runs: HashMap<(usize, usize), u32>, +} + +impl StreamingDiff { + const INSERTION_SCORE: f64 = -1.; + const DELETION_SCORE: f64 = -20.; + const EQUALITY_BASE: f64 = 1.8; + const MAX_EQUALITY_EXPONENT: i32 = 16; + + pub fn new(old: String) -> Self { + let old = old.chars().collect::>(); + let mut scores = Matrix::new(); + scores.resize(old.len() + 1, 1); + for i in 0..=old.len() { + scores.set(i, 0, i as f64 * Self::DELETION_SCORE); + } + Self { + old, + new: Vec::new(), + scores, + old_text_ix: 0, + new_text_ix: 0, + equal_runs: Default::default(), + } + } + + pub fn push_new(&mut self, text: &str) -> Vec { + self.new.extend(text.chars()); + self.scores.resize(self.old.len() + 1, self.new.len() + 1); + + for j in self.new_text_ix + 1..=self.new.len() { + self.scores.set(0, j, j as f64 * Self::INSERTION_SCORE); + for i in 1..=self.old.len() { + let insertion_score = self.scores.get(i, j - 1) + Self::INSERTION_SCORE; + let deletion_score = self.scores.get(i - 1, j) + Self::DELETION_SCORE; + let equality_score = if self.old[i - 1] == self.new[j - 1] { + let mut equal_run = self.equal_runs.get(&(i - 1, j - 1)).copied().unwrap_or(0); + equal_run += 1; + self.equal_runs.insert((i, j), equal_run); + + let exponent = cmp::min(equal_run as i32 / 4, Self::MAX_EQUALITY_EXPONENT); + self.scores.get(i - 1, j - 1) + Self::EQUALITY_BASE.powi(exponent) + } else { + f64::NEG_INFINITY + }; + + let score = insertion_score.max(deletion_score).max(equality_score); + self.scores.set(i, j, score); + } + } + + let mut max_score = f64::NEG_INFINITY; + let mut next_old_text_ix = self.old_text_ix; + let next_new_text_ix = self.new.len(); + for i in self.old_text_ix..=self.old.len() { + let score = self.scores.get(i, next_new_text_ix); + if score > max_score { + max_score = score; + next_old_text_ix = i; + } + } + + let hunks = self.backtrack(next_old_text_ix, next_new_text_ix); + self.old_text_ix = next_old_text_ix; + self.new_text_ix = next_new_text_ix; + hunks + } + + fn backtrack(&self, old_text_ix: usize, new_text_ix: usize) -> Vec { + let mut pending_insert: Option> = None; + let mut hunks = Vec::new(); + let mut i = old_text_ix; + let mut j = new_text_ix; + while (i, j) != (self.old_text_ix, self.new_text_ix) { + let insertion_score = if j > self.new_text_ix { + Some((i, j - 1)) + } else { + None + }; + let deletion_score = if i > self.old_text_ix { + Some((i - 1, j)) + } else { + None + }; + let equality_score = if i > self.old_text_ix && j > self.new_text_ix { + if self.old[i - 1] == self.new[j - 1] { + Some((i - 1, j - 1)) + } else { + None + } + } else { + None + }; + + let (prev_i, prev_j) = [insertion_score, deletion_score, equality_score] + .iter() + .max_by_key(|cell| cell.map(|(i, j)| OrderedFloat(self.scores.get(i, j)))) + .unwrap() + .unwrap(); + + if prev_i == i && prev_j == j - 1 { + if let Some(pending_insert) = pending_insert.as_mut() { + pending_insert.start = prev_j; + } else { + pending_insert = Some(prev_j..j); + } + } else { + if let Some(range) = pending_insert.take() { + hunks.push(Hunk::Insert { + text: self.new[range].iter().collect(), + }); + } + + let char_len = self.old[i - 1].len_utf8(); + if prev_i == i - 1 && prev_j == j { + if let Some(Hunk::Remove { len }) = hunks.last_mut() { + *len += char_len; + } else { + hunks.push(Hunk::Remove { len: char_len }) + } + } else { + if let Some(Hunk::Keep { len }) = hunks.last_mut() { + *len += char_len; + } else { + hunks.push(Hunk::Keep { len: char_len }) + } + } + } + + i = prev_i; + j = prev_j; + } + + if let Some(range) = pending_insert.take() { + hunks.push(Hunk::Insert { + text: self.new[range].iter().collect(), + }); + } + + hunks.reverse(); + hunks + } + + pub fn finish(self) -> Vec { + self.backtrack(self.old.len(), self.new.len()) + } +} + +#[cfg(test)] +mod tests { + use std::env; + + use super::*; + use rand::prelude::*; + + #[gpui::test(iterations = 100)] + fn test_random_diffs(mut rng: StdRng) { + let old_text_len = env::var("OLD_TEXT_LEN") + .map(|i| i.parse().expect("invalid `OLD_TEXT_LEN` variable")) + .unwrap_or(10); + let new_text_len = env::var("NEW_TEXT_LEN") + .map(|i| i.parse().expect("invalid `NEW_TEXT_LEN` variable")) + .unwrap_or(10); + + let old = util::RandomCharIter::new(&mut rng) + .take(old_text_len) + .collect::(); + log::info!("old text: {:?}", old); + + let mut diff = StreamingDiff::new(old.clone()); + let mut hunks = Vec::new(); + let mut new_len = 0; + let mut new = String::new(); + while new_len < new_text_len { + let new_chunk_len = rng.gen_range(1..=new_text_len - new_len); + let new_chunk = util::RandomCharIter::new(&mut rng) + .take(new_len) + .collect::(); + log::info!("new chunk: {:?}", new_chunk); + new_len += new_chunk_len; + new.push_str(&new_chunk); + let new_hunks = diff.push_new(&new_chunk); + log::info!("hunks: {:?}", new_hunks); + hunks.extend(new_hunks); + } + let final_hunks = diff.finish(); + log::info!("final hunks: {:?}", final_hunks); + hunks.extend(final_hunks); + + log::info!("new text: {:?}", new); + let mut old_ix = 0; + let mut new_ix = 0; + let mut patched = String::new(); + for hunk in hunks { + match hunk { + Hunk::Keep { len } => { + assert_eq!(&old[old_ix..old_ix + len], &new[new_ix..new_ix + len]); + patched.push_str(&old[old_ix..old_ix + len]); + old_ix += len; + new_ix += len; + } + Hunk::Remove { len } => { + old_ix += len; + } + Hunk::Insert { text } => { + assert_eq!(text, &new[new_ix..new_ix + text.len()]); + patched.push_str(&text); + new_ix += text.len(); + } + } + } + assert_eq!(patched, new); + } +} diff --git a/crates/breadcrumbs/src/breadcrumbs.rs b/crates/breadcrumbs/src/breadcrumbs.rs index 615e238648..41985edb75 100644 --- a/crates/breadcrumbs/src/breadcrumbs.rs +++ b/crates/breadcrumbs/src/breadcrumbs.rs @@ -50,7 +50,7 @@ impl View for Breadcrumbs { let not_editor = active_item.downcast::().is_none(); let theme = theme::current(cx).clone(); - let style = &theme.workspace.breadcrumbs; + let style = &theme.workspace.toolbar.breadcrumbs; let breadcrumbs = match active_item.breadcrumbs(&theme, cx) { Some(breadcrumbs) => breadcrumbs, @@ -60,7 +60,7 @@ impl View for Breadcrumbs { .map(|breadcrumb| { Text::new( breadcrumb.text, - theme.workspace.breadcrumbs.default.text.clone(), + theme.workspace.toolbar.breadcrumbs.default.text.clone(), ) .with_highlights(breadcrumb.highlights.unwrap_or_default()) .into_any() @@ -68,10 +68,10 @@ impl View for Breadcrumbs { let crumbs = Flex::row() .with_children(Itertools::intersperse_with(breadcrumbs, || { - Label::new(" 〉 ", style.default.text.clone()).into_any() + Label::new(" › ", style.default.text.clone()).into_any() })) .constrained() - .with_height(theme.workspace.breadcrumb_height) + .with_height(theme.workspace.toolbar.breadcrumb_height) .contained(); if not_editor || !self.pane_focused { diff --git a/crates/clock/src/clock.rs b/crates/clock/src/clock.rs index bc936fcb99..3cbf8d6594 100644 --- a/crates/clock/src/clock.rs +++ b/crates/clock/src/clock.rs @@ -2,70 +2,17 @@ use smallvec::SmallVec; use std::{ cmp::{self, Ordering}, fmt, iter, - ops::{Add, AddAssign}, }; pub type ReplicaId = u16; pub type Seq = u32; -#[derive(Clone, Copy, Default, Eq, Hash, PartialEq, Ord, PartialOrd)] -pub struct Local { - pub replica_id: ReplicaId, - pub value: Seq, -} - #[derive(Clone, Copy, Default, Eq, Hash, PartialEq)] pub struct Lamport { pub replica_id: ReplicaId, pub value: Seq, } -impl Local { - pub const MIN: Self = Self { - replica_id: ReplicaId::MIN, - value: Seq::MIN, - }; - pub const MAX: Self = Self { - replica_id: ReplicaId::MAX, - value: Seq::MAX, - }; - - pub fn new(replica_id: ReplicaId) -> Self { - Self { - replica_id, - value: 1, - } - } - - pub fn tick(&mut self) -> Self { - let timestamp = *self; - self.value += 1; - timestamp - } - - pub fn observe(&mut self, timestamp: Self) { - if timestamp.replica_id == self.replica_id { - self.value = cmp::max(self.value, timestamp.value + 1); - } - } -} - -impl<'a> Add<&'a Self> for Local { - type Output = Local; - - fn add(self, other: &'a Self) -> Self::Output { - *cmp::max(&self, other) - } -} - -impl<'a> AddAssign<&'a Local> for Local { - fn add_assign(&mut self, other: &Self) { - if *self < *other { - *self = *other; - } - } -} - /// A vector clock #[derive(Clone, Default, Hash, Eq, PartialEq)] pub struct Global(SmallVec<[u32; 8]>); @@ -79,7 +26,7 @@ impl Global { self.0.get(replica_id as usize).copied().unwrap_or(0) as Seq } - pub fn observe(&mut self, timestamp: Local) { + pub fn observe(&mut self, timestamp: Lamport) { if timestamp.value > 0 { let new_len = timestamp.replica_id as usize + 1; if new_len > self.0.len() { @@ -126,7 +73,7 @@ impl Global { self.0.resize(new_len, 0); } - pub fn observed(&self, timestamp: Local) -> bool { + pub fn observed(&self, timestamp: Lamport) -> bool { self.get(timestamp.replica_id) >= timestamp.value } @@ -178,16 +125,16 @@ impl Global { false } - pub fn iter(&self) -> impl Iterator + '_ { - self.0.iter().enumerate().map(|(replica_id, seq)| Local { + pub fn iter(&self) -> impl Iterator + '_ { + self.0.iter().enumerate().map(|(replica_id, seq)| Lamport { replica_id: replica_id as ReplicaId, value: *seq, }) } } -impl FromIterator for Global { - fn from_iter>(locals: T) -> Self { +impl FromIterator for Global { + fn from_iter>(locals: T) -> Self { let mut result = Self::new(); for local in locals { result.observe(local); @@ -212,6 +159,16 @@ impl PartialOrd for Lamport { } impl Lamport { + pub const MIN: Self = Self { + replica_id: ReplicaId::MIN, + value: Seq::MIN, + }; + + pub const MAX: Self = Self { + replica_id: ReplicaId::MAX, + value: Seq::MAX, + }; + pub fn new(replica_id: ReplicaId) -> Self { Self { value: 1, @@ -230,12 +187,6 @@ impl Lamport { } } -impl fmt::Debug for Local { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "Local {{{}: {}}}", self.replica_id, self.value) - } -} - impl fmt::Debug for Lamport { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "Lamport {{{}: {}}}", self.replica_id, self.value) diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index 8adc38615c..914e3f2dfb 100644 --- a/crates/collab/Cargo.toml +++ b/crates/collab/Cargo.toml @@ -3,7 +3,7 @@ authors = ["Nathan Sobo "] default-run = "collab" edition = "2021" name = "collab" -version = "0.18.0" +version = "0.19.0" publish = false [[bin]] diff --git a/crates/collab/src/db/queries/buffers.rs b/crates/collab/src/db/queries/buffers.rs index 354accc01a..f120aea1c5 100644 --- a/crates/collab/src/db/queries/buffers.rs +++ b/crates/collab/src/db/queries/buffers.rs @@ -1,6 +1,6 @@ use super::*; use prost::Message; -use text::{EditOperation, InsertionTimestamp, UndoOperation}; +use text::{EditOperation, UndoOperation}; impl Database { pub async fn join_channel_buffer( @@ -182,7 +182,6 @@ impl Database { .await } - #[cfg(debug_assertions)] pub async fn get_channel_buffer_collaborators( &self, channel_id: ChannelId, @@ -370,7 +369,6 @@ fn operation_to_storage( operation.replica_id, operation.lamport_timestamp, storage::Operation { - local_timestamp: operation.local_timestamp, version: version_to_storage(&operation.version), is_undo: false, edit_ranges: operation @@ -389,7 +387,6 @@ fn operation_to_storage( operation.replica_id, operation.lamport_timestamp, storage::Operation { - local_timestamp: operation.local_timestamp, version: version_to_storage(&operation.version), is_undo: true, edit_ranges: Vec::new(), @@ -399,7 +396,7 @@ fn operation_to_storage( .iter() .map(|entry| storage::UndoCount { replica_id: entry.replica_id, - local_timestamp: entry.local_timestamp, + lamport_timestamp: entry.lamport_timestamp, count: entry.count, }) .collect(), @@ -427,7 +424,6 @@ fn operation_from_storage( Ok(if operation.is_undo { proto::operation::Variant::Undo(proto::operation::Undo { replica_id: row.replica_id as u32, - local_timestamp: operation.local_timestamp as u32, lamport_timestamp: row.lamport_timestamp as u32, version, counts: operation @@ -435,7 +431,7 @@ fn operation_from_storage( .iter() .map(|entry| proto::UndoCount { replica_id: entry.replica_id, - local_timestamp: entry.local_timestamp, + lamport_timestamp: entry.lamport_timestamp, count: entry.count, }) .collect(), @@ -443,7 +439,6 @@ fn operation_from_storage( } else { proto::operation::Variant::Edit(proto::operation::Edit { replica_id: row.replica_id as u32, - local_timestamp: operation.local_timestamp as u32, lamport_timestamp: row.lamport_timestamp as u32, version, ranges: operation @@ -483,10 +478,9 @@ fn version_from_storage(version: &Vec) -> Vec Option { match operation.variant? { proto::operation::Variant::Edit(edit) => Some(text::Operation::Edit(EditOperation { - timestamp: InsertionTimestamp { + timestamp: clock::Lamport { replica_id: edit.replica_id as text::ReplicaId, - local: edit.local_timestamp, - lamport: edit.lamport_timestamp, + value: edit.lamport_timestamp, }, version: version_from_wire(&edit.version), ranges: edit @@ -498,32 +492,26 @@ pub fn operation_from_wire(operation: proto::Operation) -> Option Some(text::Operation::Undo { - lamport_timestamp: clock::Lamport { + proto::operation::Variant::Undo(undo) => Some(text::Operation::Undo(UndoOperation { + timestamp: clock::Lamport { replica_id: undo.replica_id as text::ReplicaId, value: undo.lamport_timestamp, }, - undo: UndoOperation { - id: clock::Local { - replica_id: undo.replica_id as text::ReplicaId, - value: undo.local_timestamp, - }, - version: version_from_wire(&undo.version), - counts: undo - .counts - .into_iter() - .map(|c| { - ( - clock::Local { - replica_id: c.replica_id as text::ReplicaId, - value: c.local_timestamp, - }, - c.count, - ) - }) - .collect(), - }, - }), + version: version_from_wire(&undo.version), + counts: undo + .counts + .into_iter() + .map(|c| { + ( + clock::Lamport { + replica_id: c.replica_id as text::ReplicaId, + value: c.lamport_timestamp, + }, + c.count, + ) + }) + .collect(), + })), _ => None, } } @@ -531,7 +519,7 @@ pub fn operation_from_wire(operation: proto::Operation) -> Option clock::Global { let mut version = clock::Global::new(); for entry in message { - version.observe(clock::Local { + version.observe(clock::Lamport { replica_id: entry.replica_id as text::ReplicaId, value: entry.timestamp, }); @@ -546,8 +534,6 @@ mod storage { #[derive(Message)] pub struct Operation { - #[prost(uint32, tag = "1")] - pub local_timestamp: u32, #[prost(message, repeated, tag = "2")] pub version: Vec, #[prost(bool, tag = "3")] @@ -581,7 +567,7 @@ mod storage { #[prost(uint32, tag = "1")] pub replica_id: u32, #[prost(uint32, tag = "2")] - pub local_timestamp: u32, + pub lamport_timestamp: u32, #[prost(uint32, tag = "3")] pub count: u32, } diff --git a/crates/collab/src/db/queries/users.rs b/crates/collab/src/db/queries/users.rs index bd7c3e9ffd..5cb1ef6ea3 100644 --- a/crates/collab/src/db/queries/users.rs +++ b/crates/collab/src/db/queries/users.rs @@ -241,7 +241,6 @@ impl Database { result } - #[cfg(debug_assertions)] pub async fn create_user_flag(&self, flag: &str) -> Result { self.transaction(|tx| async move { let flag = feature_flag::Entity::insert(feature_flag::ActiveModel { @@ -257,7 +256,6 @@ impl Database { .await } - #[cfg(debug_assertions)] pub async fn add_user_flag(&self, user: UserId, flag: FlagId) -> Result<()> { self.transaction(|tx| async move { user_feature::Entity::insert(user_feature::ActiveModel { diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index f64a82e32e..8121b0ac91 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -9,7 +9,7 @@ use editor::{ test::editor_test_context::EditorTestContext, ConfirmCodeAction, ConfirmCompletion, ConfirmRename, Editor, ExcerptRange, MultiBuffer, Redo, Rename, ToggleCodeActions, Undo, }; -use fs::{repository::GitFileStatus, FakeFs, Fs as _, LineEnding, RemoveOptions}; +use fs::{repository::GitFileStatus, FakeFs, Fs as _, RemoveOptions}; use futures::StreamExt as _; use gpui::{ executor::Deterministic, geometry::vector::vec2f, test::EmptyView, AppContext, ModelHandle, @@ -19,7 +19,7 @@ use indoc::indoc; use language::{ language_settings::{AllLanguageSettings, Formatter, InlayHintSettings}, tree_sitter_rust, Anchor, Diagnostic, DiagnosticEntry, FakeLspAdapter, Language, - LanguageConfig, OffsetRangeExt, Point, Rope, + LanguageConfig, LineEnding, OffsetRangeExt, Point, Rope, }; use live_kit_client::MacOSDisplay; use lsp::LanguageServerId; @@ -33,7 +33,7 @@ use std::{ path::{Path, PathBuf}, rc::Rc, sync::{ - atomic::{AtomicBool, AtomicU32, Ordering::SeqCst}, + atomic::{self, AtomicBool, AtomicUsize, Ordering::SeqCst}, Arc, }, }; @@ -7799,7 +7799,7 @@ async fn test_on_input_format_from_guest_to_host( }); } -#[gpui::test] +#[gpui::test(iterations = 10)] async fn test_mutual_editor_inlay_hint_cache_update( deterministic: Arc, cx_a: &mut TestAppContext, @@ -7913,30 +7913,27 @@ async fn test_mutual_editor_inlay_hint_cache_update( .unwrap(); // Set up the language server to return an additional inlay hint on each request. - let next_call_id = Arc::new(AtomicU32::new(0)); + let edits_made = Arc::new(AtomicUsize::new(0)); + let closure_edits_made = Arc::clone(&edits_made); fake_language_server .handle_request::(move |params, _| { - let task_next_call_id = Arc::clone(&next_call_id); + let task_edits_made = Arc::clone(&closure_edits_made); async move { assert_eq!( params.text_document.uri, lsp::Url::from_file_path("/a/main.rs").unwrap(), ); - let call_count = task_next_call_id.fetch_add(1, SeqCst); - Ok(Some( - (0..=call_count) - .map(|ix| lsp::InlayHint { - position: lsp::Position::new(0, ix), - label: lsp::InlayHintLabel::String(ix.to_string()), - kind: None, - text_edits: None, - tooltip: None, - padding_left: None, - padding_right: None, - data: None, - }) - .collect(), - )) + let edits_made = task_edits_made.load(atomic::Ordering::Acquire); + Ok(Some(vec![lsp::InlayHint { + position: lsp::Position::new(0, edits_made as u32), + label: lsp::InlayHintLabel::String(edits_made.to_string()), + kind: None, + text_edits: None, + tooltip: None, + padding_left: None, + padding_right: None, + data: None, + }])) } }) .next() @@ -7945,17 +7942,17 @@ async fn test_mutual_editor_inlay_hint_cache_update( deterministic.run_until_parked(); - let mut edits_made = 1; + let initial_edit = edits_made.load(atomic::Ordering::Acquire); editor_a.update(cx_a, |editor, _| { assert_eq!( - vec!["0".to_string()], + vec![initial_edit.to_string()], extract_hint_labels(editor), "Host should get its first hints when opens an editor" ); let inlay_cache = editor.inlay_hint_cache(); assert_eq!( inlay_cache.version(), - edits_made, + 1, "Host editor update the cache version after every cache/view change", ); }); @@ -7972,144 +7969,104 @@ async fn test_mutual_editor_inlay_hint_cache_update( deterministic.run_until_parked(); editor_b.update(cx_b, |editor, _| { assert_eq!( - vec!["0".to_string(), "1".to_string()], + vec![initial_edit.to_string()], extract_hint_labels(editor), "Client should get its first hints when opens an editor" ); let inlay_cache = editor.inlay_hint_cache(); assert_eq!( inlay_cache.version(), - edits_made, + 1, "Guest editor update the cache version after every cache/view change" ); }); + let after_client_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1; editor_b.update(cx_b, |editor, cx| { editor.change_selections(None, cx, |s| s.select_ranges([13..13].clone())); editor.handle_input(":", cx); cx.focus(&editor_b); - edits_made += 1; }); deterministic.run_until_parked(); editor_a.update(cx_a, |editor, _| { assert_eq!( - vec![ - "0".to_string(), - "1".to_string(), - "2".to_string(), - "3".to_string() - ], + vec![after_client_edit.to_string()], extract_hint_labels(editor), - "Guest should get hints the 1st edit and 2nd LSP query" ); let inlay_cache = editor.inlay_hint_cache(); - assert_eq!(inlay_cache.version(), edits_made); + assert_eq!(inlay_cache.version(), 2); }); editor_b.update(cx_b, |editor, _| { assert_eq!( - vec!["0".to_string(), "1".to_string(), "2".to_string(),], + vec![after_client_edit.to_string()], extract_hint_labels(editor), - "Guest should get hints the 1st edit and 2nd LSP query" ); let inlay_cache = editor.inlay_hint_cache(); - assert_eq!(inlay_cache.version(), edits_made); + assert_eq!(inlay_cache.version(), 2); }); + let after_host_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1; editor_a.update(cx_a, |editor, cx| { editor.change_selections(None, cx, |s| s.select_ranges([13..13])); editor.handle_input("a change to increment both buffers' versions", cx); cx.focus(&editor_a); - edits_made += 1; }); deterministic.run_until_parked(); editor_a.update(cx_a, |editor, _| { assert_eq!( - vec![ - "0".to_string(), - "1".to_string(), - "2".to_string(), - "3".to_string(), - "4".to_string() - ], + vec![after_host_edit.to_string()], extract_hint_labels(editor), - "Host should get hints from 3rd edit, 5th LSP query: \ -4th query was made by guest (but not applied) due to cache invalidation logic" ); let inlay_cache = editor.inlay_hint_cache(); - assert_eq!(inlay_cache.version(), edits_made); + assert_eq!(inlay_cache.version(), 3); }); editor_b.update(cx_b, |editor, _| { assert_eq!( - vec![ - "0".to_string(), - "1".to_string(), - "2".to_string(), - "3".to_string(), - "4".to_string(), - "5".to_string(), - ], + vec![after_host_edit.to_string()], extract_hint_labels(editor), - "Guest should get hints from 3rd edit, 6th LSP query" ); let inlay_cache = editor.inlay_hint_cache(); - assert_eq!(inlay_cache.version(), edits_made); + assert_eq!(inlay_cache.version(), 3); }); + let after_special_edit_for_refresh = edits_made.fetch_add(1, atomic::Ordering::Release) + 1; fake_language_server .request::(()) .await .expect("inlay refresh request failed"); - edits_made += 1; deterministic.run_until_parked(); editor_a.update(cx_a, |editor, _| { assert_eq!( - vec![ - "0".to_string(), - "1".to_string(), - "2".to_string(), - "3".to_string(), - "4".to_string(), - "5".to_string(), - "6".to_string(), - ], + vec![after_special_edit_for_refresh.to_string()], extract_hint_labels(editor), - "Host should react to /refresh LSP request and get new hints from 7th LSP query" + "Host should react to /refresh LSP request" ); let inlay_cache = editor.inlay_hint_cache(); assert_eq!( inlay_cache.version(), - edits_made, + 4, "Host should accepted all edits and bump its cache version every time" ); }); editor_b.update(cx_b, |editor, _| { assert_eq!( - vec![ - "0".to_string(), - "1".to_string(), - "2".to_string(), - "3".to_string(), - "4".to_string(), - "5".to_string(), - "6".to_string(), - "7".to_string(), - ], + vec![after_special_edit_for_refresh.to_string()], extract_hint_labels(editor), - "Guest should get a /refresh LSP request propagated by host and get new hints from 8th LSP query" + "Guest should get a /refresh LSP request propagated by host" ); let inlay_cache = editor.inlay_hint_cache(); assert_eq!( inlay_cache.version(), - edits_made, + 4, "Guest should accepted all edits and bump its cache version every time" ); }); } -#[gpui::test] +#[gpui::test(iterations = 10)] async fn test_inlay_hint_refresh_is_forwarded( deterministic: Arc, cx_a: &mut TestAppContext, @@ -8223,35 +8180,34 @@ async fn test_inlay_hint_refresh_is_forwarded( .downcast::() .unwrap(); + let other_hints = Arc::new(AtomicBool::new(false)); let fake_language_server = fake_language_servers.next().await.unwrap(); - let next_call_id = Arc::new(AtomicU32::new(0)); + let closure_other_hints = Arc::clone(&other_hints); fake_language_server .handle_request::(move |params, _| { - let task_next_call_id = Arc::clone(&next_call_id); + let task_other_hints = Arc::clone(&closure_other_hints); async move { assert_eq!( params.text_document.uri, lsp::Url::from_file_path("/a/main.rs").unwrap(), ); - let mut current_call_id = Arc::clone(&task_next_call_id).fetch_add(1, SeqCst); - let mut new_hints = Vec::with_capacity(current_call_id as usize); - loop { - new_hints.push(lsp::InlayHint { - position: lsp::Position::new(0, current_call_id), - label: lsp::InlayHintLabel::String(current_call_id.to_string()), - kind: None, - text_edits: None, - tooltip: None, - padding_left: None, - padding_right: None, - data: None, - }); - if current_call_id == 0 { - break; - } - current_call_id -= 1; - } - Ok(Some(new_hints)) + let other_hints = task_other_hints.load(atomic::Ordering::Acquire); + let character = if other_hints { 0 } else { 2 }; + let label = if other_hints { + "other hint" + } else { + "initial hint" + }; + Ok(Some(vec![lsp::InlayHint { + position: lsp::Position::new(0, character), + label: lsp::InlayHintLabel::String(label.to_string()), + kind: None, + text_edits: None, + tooltip: None, + padding_left: None, + padding_right: None, + data: None, + }])) } }) .next() @@ -8270,26 +8226,26 @@ async fn test_inlay_hint_refresh_is_forwarded( assert_eq!( inlay_cache.version(), 0, - "Host should not increment its cache version due to no changes", + "Turned off hints should not generate version updates" ); }); - let mut edits_made = 1; cx_b.foreground().run_until_parked(); editor_b.update(cx_b, |editor, _| { assert_eq!( - vec!["0".to_string()], + vec!["initial hint".to_string()], extract_hint_labels(editor), "Client should get its first hints when opens an editor" ); let inlay_cache = editor.inlay_hint_cache(); assert_eq!( inlay_cache.version(), - edits_made, - "Guest editor update the cache version after every cache/view change" + 1, + "Should update cache verison after first hints" ); }); + other_hints.fetch_or(true, atomic::Ordering::Release); fake_language_server .request::(()) .await @@ -8304,22 +8260,21 @@ async fn test_inlay_hint_refresh_is_forwarded( assert_eq!( inlay_cache.version(), 0, - "Host should not increment its cache version due to no changes", + "Turned off hints should not generate version updates, again" ); }); - edits_made += 1; cx_b.foreground().run_until_parked(); editor_b.update(cx_b, |editor, _| { assert_eq!( - vec!["0".to_string(), "1".to_string(),], + vec!["other hint".to_string()], extract_hint_labels(editor), "Guest should get a /refresh LSP request propagated by host despite host hints are off" ); let inlay_cache = editor.inlay_hint_cache(); assert_eq!( inlay_cache.version(), - edits_made, + 2, "Guest should accepted all edits and bump its cache version every time" ); }); diff --git a/crates/collab/src/tests/randomized_integration_tests.rs b/crates/collab/src/tests/randomized_integration_tests.rs index 814f248b6d..e48753ed41 100644 --- a/crates/collab/src/tests/randomized_integration_tests.rs +++ b/crates/collab/src/tests/randomized_integration_tests.rs @@ -762,7 +762,7 @@ async fn apply_client_operation( client .fs() - .save(&path, &content.as_str().into(), fs::LineEnding::Unix) + .save(&path, &content.as_str().into(), text::LineEnding::Unix) .await .unwrap(); } diff --git a/crates/collab_ui/src/channel_view.rs b/crates/collab_ui/src/channel_view.rs index a34f10b2db..5086cc8b37 100644 --- a/crates/collab_ui/src/channel_view.rs +++ b/crates/collab_ui/src/channel_view.rs @@ -213,7 +213,7 @@ impl Item for ChannelView { } fn is_singleton(&self, _cx: &AppContext) -> bool { - true + false } fn navigate(&mut self, data: Box, cx: &mut ViewContext) -> bool { diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 0593bfcb1f..daaa483975 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -1106,23 +1106,17 @@ impl CollabPanel { ) -> AnyElement { enum OpenSharedScreen {} - let font_cache = cx.font_cache(); - let host_avatar_height = theme + let host_avatar_width = theme .contact_avatar .width .or(theme.contact_avatar.height) .unwrap_or(0.); - let row = &theme.project_row.inactive_state().default; let tree_branch = theme.tree_branch; - let line_height = row.name.text.line_height(font_cache); - let cap_height = row.name.text.cap_height(font_cache); - let baseline_offset = - row.name.text.baseline_offset(font_cache) + (theme.row_height - line_height) / 2.; MouseEventHandler::new::( peer_id.as_u64() as usize, cx, - |mouse_state, _| { + |mouse_state, cx| { let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state); let row = theme .project_row @@ -1130,49 +1124,20 @@ impl CollabPanel { .style_for(mouse_state); Flex::row() - .with_child( - Stack::new() - .with_child(Canvas::new(move |scene, bounds, _, _, _| { - let start_x = bounds.min_x() + (bounds.width() / 2.) - - (tree_branch.width / 2.); - let end_x = bounds.max_x(); - let start_y = bounds.min_y(); - let end_y = bounds.min_y() + baseline_offset - (cap_height / 2.); - - scene.push_quad(gpui::Quad { - bounds: RectF::from_points( - vec2f(start_x, start_y), - vec2f( - start_x + tree_branch.width, - if is_last { end_y } else { bounds.max_y() }, - ), - ), - background: Some(tree_branch.color), - border: gpui::Border::default(), - corner_radii: (0.).into(), - }); - scene.push_quad(gpui::Quad { - bounds: RectF::from_points( - vec2f(start_x, end_y), - vec2f(end_x, end_y + tree_branch.width), - ), - background: Some(tree_branch.color), - border: gpui::Border::default(), - corner_radii: (0.).into(), - }); - })) - .constrained() - .with_width(host_avatar_height), - ) + .with_child(render_tree_branch( + tree_branch, + &row.name.text, + is_last, + vec2f(host_avatar_width, theme.row_height), + cx.font_cache(), + )) .with_child( Svg::new("icons/disable_screen_sharing_12.svg") - .with_color(row.icon.color) + .with_color(theme.channel_hash.color) .constrained() - .with_width(row.icon.width) + .with_width(theme.channel_hash.width) .aligned() - .left() - .contained() - .with_style(row.icon.container), + .left(), ) .with_child( Label::new("Screen", row.name.text.clone()) @@ -2553,27 +2518,16 @@ impl View for CollabPanel { .with_child( Flex::column() .with_child( - Flex::row() - .with_child( - ChildView::new(&self.filter_editor, cx) - .contained() - .with_style(theme.user_query_editor.container) - .flex(1.0, true), - ) - .constrained() - .with_width(self.size(cx)), - ) - .with_child( - List::new(self.list_state.clone()) - .constrained() - .with_width(self.size(cx)) - .flex(1., true) - .into_any(), + Flex::row().with_child( + ChildView::new(&self.filter_editor, cx) + .contained() + .with_style(theme.user_query_editor.container) + .flex(1.0, true), + ), ) + .with_child(List::new(self.list_state.clone()).flex(1., true).into_any()) .contained() .with_style(theme.container) - .constrained() - .with_width(self.size(cx)) .into_any(), ) .with_children( diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index 684ddca08d..95b9868937 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -213,7 +213,6 @@ impl CollabTitlebarItem { .map(|branch| util::truncate_and_trailoff(&branch, MAX_BRANCH_NAME_LENGTH)); let project_style = theme.titlebar.project_menu_button.clone(); let git_style = theme.titlebar.git_menu_button.clone(); - let divider_style = theme.titlebar.project_name_divider.clone(); let item_spacing = theme.titlebar.item_spacing; let mut ret = Flex::row().with_child( @@ -248,49 +247,37 @@ impl CollabTitlebarItem { ); if let Some(git_branch) = branch_prepended { ret = ret.with_child( - Flex::row() - .with_child( - Label::new("/", divider_style.text) - .contained() - .with_style(divider_style.container) - .aligned() - .left(), - ) - .with_child( - Stack::new() - .with_child( - MouseEventHandler::new::( - 0, - cx, - |mouse_state, cx| { - enum BranchPopoverTooltip {} - let style = git_style - .in_state(self.branch_popover.is_some()) - .style_for(mouse_state); - Label::new(git_branch, style.text.clone()) - .contained() - .with_style(style.container.clone()) - .with_margin_right(item_spacing) - .aligned() - .left() - .with_tooltip::( - 0, - "Recent branches", - Some(Box::new(ToggleVcsMenu)), - theme.tooltip.clone(), - cx, - ) - .into_any_named("title-project-branch") - }, - ) - .with_cursor_style(CursorStyle::PointingHand) - .on_down(MouseButton::Left, move |_, this, cx| { - this.toggle_vcs_menu(&Default::default(), cx) - }) - .on_click(MouseButton::Left, move |_, _, _| {}), - ) - .with_children(self.render_branches_popover_host(&theme.titlebar, cx)), - ), + Flex::row().with_child( + Stack::new() + .with_child( + MouseEventHandler::new::(0, cx, |mouse_state, cx| { + enum BranchPopoverTooltip {} + let style = git_style + .in_state(self.branch_popover.is_some()) + .style_for(mouse_state); + Label::new(git_branch, style.text.clone()) + .contained() + .with_style(style.container.clone()) + .with_margin_right(item_spacing) + .aligned() + .left() + .with_tooltip::( + 0, + "Recent branches", + Some(Box::new(ToggleVcsMenu)), + theme.tooltip.clone(), + cx, + ) + .into_any_named("title-project-branch") + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_down(MouseButton::Left, move |_, this, cx| { + this.toggle_vcs_menu(&Default::default(), cx) + }) + .on_click(MouseButton::Left, move |_, _, _| {}), + ) + .with_children(self.render_branches_popover_host(&theme.titlebar, cx)), + ), ) } ret.into_any() diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index 427134894f..499ae2e808 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -1188,7 +1188,7 @@ mod tests { _: u64, _: &clock::Global, _: language::RopeFingerprint, - _: ::fs::LineEnding, + _: language::LineEnding, _: std::time::SystemTime, _: &mut AppContext, ) { diff --git a/crates/editor/src/blink_manager.rs b/crates/editor/src/blink_manager.rs index 24ea4774aa..fa5a3af0c6 100644 --- a/crates/editor/src/blink_manager.rs +++ b/crates/editor/src/blink_manager.rs @@ -37,10 +37,7 @@ impl BlinkManager { } pub fn pause_blinking(&mut self, cx: &mut ModelContext) { - if !self.visible { - self.visible = true; - cx.notify(); - } + self.show_cursor(cx); let epoch = self.next_blink_epoch(); let interval = self.blink_interval; @@ -82,7 +79,13 @@ impl BlinkManager { }) .detach(); } - } else if !self.visible { + } else { + self.show_cursor(cx); + } + } + + pub fn show_cursor(&mut self, cx: &mut ModelContext<'_, BlinkManager>) { + if !self.visible { self.visible = true; cx.notify(); } diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index fe8e4e338c..d331b0a268 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -44,7 +44,7 @@ use gpui::{ elements::*, executor, fonts::{self, HighlightStyle, TextStyle}, - geometry::vector::Vector2F, + geometry::vector::{vec2f, Vector2F}, impl_actions, keymap_matcher::KeymapContext, platform::{CursorStyle, MouseButton}, @@ -820,6 +820,7 @@ struct CompletionsMenu { id: CompletionId, initial_position: Anchor, buffer: ModelHandle, + project: Option>, completions: Arc<[Completion]>, match_candidates: Vec, matches: Arc<[StringMatch]>, @@ -863,6 +864,48 @@ impl CompletionsMenu { fn render(&self, style: EditorStyle, cx: &mut ViewContext) -> AnyElement { enum CompletionTag {} + let language_servers = self.project.as_ref().map(|project| { + project + .read(cx) + .language_servers_for_buffer(self.buffer.read(cx), cx) + .filter(|(_, server)| server.capabilities().completion_provider.is_some()) + .map(|(adapter, server)| (server.server_id(), adapter.short_name)) + .collect::>() + }); + let needs_server_name = language_servers + .as_ref() + .map_or(false, |servers| servers.len() > 1); + + let get_server_name = + move |lookup_server_id: lsp::LanguageServerId| -> Option<&'static str> { + language_servers + .iter() + .flatten() + .find_map(|(server_id, server_name)| { + if *server_id == lookup_server_id { + Some(*server_name) + } else { + None + } + }) + }; + + let widest_completion_ix = self + .matches + .iter() + .enumerate() + .max_by_key(|(_, mat)| { + let completion = &self.completions[mat.candidate_id]; + let mut len = completion.label.text.chars().count(); + + if let Some(server_name) = get_server_name(completion.server_id) { + len += server_name.chars().count(); + } + + len + }) + .map(|(ix, _)| ix); + let completions = self.completions.clone(); let matches = self.matches.clone(); let selected_item = self.selected_item; @@ -889,19 +932,83 @@ impl CompletionsMenu { style.autocomplete.item }; - Text::new(completion.label.text.clone(), style.text.clone()) - .with_soft_wrap(false) - .with_highlights(combine_syntax_and_fuzzy_match_highlights( - &completion.label.text, - style.text.color.into(), - styled_runs_for_code_label( - &completion.label, - &style.syntax, - ), - &mat.positions, - )) - .contained() - .with_style(item_style) + let completion_label = + Text::new(completion.label.text.clone(), style.text.clone()) + .with_soft_wrap(false) + .with_highlights( + combine_syntax_and_fuzzy_match_highlights( + &completion.label.text, + style.text.color.into(), + styled_runs_for_code_label( + &completion.label, + &style.syntax, + ), + &mat.positions, + ), + ); + + if let Some(server_name) = get_server_name(completion.server_id) { + Flex::row() + .with_child(completion_label) + .with_children((|| { + if !needs_server_name { + return None; + } + + let text_style = TextStyle { + color: style.autocomplete.server_name_color, + font_size: style.text.font_size + * style.autocomplete.server_name_size_percent, + ..style.text.clone() + }; + + let label = Text::new(server_name, text_style) + .aligned() + .constrained() + .dynamically(move |constraint, _, _| { + gpui::SizeConstraint { + min: constraint.min, + max: vec2f( + constraint.max.x(), + constraint.min.y(), + ), + } + }); + + if Some(item_ix) == widest_completion_ix { + Some( + label + .contained() + .with_style( + style + .autocomplete + .server_name_container, + ) + .into_any(), + ) + } else { + Some(label.flex_float().into_any()) + } + })()) + .into_any() + } else { + completion_label.into_any() + } + .contained() + .with_style(item_style) + .constrained() + .dynamically( + move |constraint, _, _| { + if Some(item_ix) == widest_completion_ix { + constraint + } else { + gpui::SizeConstraint { + min: constraint.min, + max: constraint.min, + } + } + }, + ) }, ) .with_cursor_style(CursorStyle::PointingHand) @@ -918,19 +1025,7 @@ impl CompletionsMenu { } }, ) - .with_width_from_item( - self.matches - .iter() - .enumerate() - .max_by_key(|(_, mat)| { - self.completions[mat.candidate_id] - .label - .text - .chars() - .count() - }) - .map(|(ix, _)| ix), - ) + .with_width_from_item(widest_completion_ix) .contained() .with_style(container_style) .into_any() @@ -1454,6 +1549,16 @@ impl Editor { cx.observe(&display_map, Self::on_display_map_changed), cx.observe(&blink_manager, |_, _, cx| cx.notify()), cx.observe_global::(Self::settings_changed), + cx.observe_window_activation(|editor, active, cx| { + editor.blink_manager.update(cx, |blink_manager, cx| { + if active { + blink_manager.enable(cx); + } else { + blink_manager.show_cursor(cx); + blink_manager.disable(cx); + } + }); + }), ], }; @@ -1549,7 +1654,7 @@ impl Editor { .excerpt_containing(self.selections.newest_anchor().head(), cx) } - fn style(&self, cx: &AppContext) -> EditorStyle { + pub fn style(&self, cx: &AppContext) -> EditorStyle { build_style( settings::get::(cx), self.get_field_editor_theme.as_deref(), @@ -1625,6 +1730,15 @@ impl Editor { self.read_only = read_only; } + pub fn set_field_editor_style( + &mut self, + style: Option>, + cx: &mut ViewContext, + ) { + self.get_field_editor_theme = style; + cx.notify(); + } + pub fn replica_id_map(&self) -> Option<&HashMap> { self.replica_id_mapping.as_ref() } @@ -2964,6 +3078,7 @@ impl Editor { }); let id = post_inc(&mut self.next_completion_id); + let project = self.project.clone(); let task = cx.spawn(|this, mut cx| { async move { let menu = if let Some(completions) = completions.await.log_err() { @@ -2982,6 +3097,7 @@ impl Editor { }) .collect(), buffer, + project, completions: completions.into(), matches: Vec::new().into(), selected_item: 0, @@ -4979,6 +5095,9 @@ impl Editor { self.unmark_text(cx); self.refresh_copilot_suggestions(true, cx); cx.emit(Event::Edited); + cx.emit(Event::TransactionUndone { + transaction_id: tx_id, + }); } } @@ -8418,6 +8537,9 @@ pub enum Event { local: bool, autoscroll: bool, }, + TransactionUndone { + transaction_id: TransactionId, + }, Closed, } @@ -8458,7 +8580,7 @@ impl View for Editor { "Editor" } - fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext) { + fn focus_in(&mut self, focused: AnyViewHandle, cx: &mut ViewContext) { if cx.is_self_focused() { let focused_event = EditorFocused(cx.handle()); cx.emit(Event::Focused); @@ -8466,7 +8588,7 @@ impl View for Editor { } if let Some(rename) = self.pending_rename.as_ref() { cx.focus(&rename.editor); - } else { + } else if cx.is_self_focused() || !focused.is::() { if !self.focused { self.blink_manager.update(cx, BlinkManager::enable); } @@ -9161,6 +9283,7 @@ pub fn split_words<'a>(text: &'a str) -> impl std::iter::Iterator(move |_, _| async move { + Ok(Some(lsp::CompletionResponse::Array(vec![ + lsp::CompletionItem { + label: "bg-blue".into(), + ..Default::default() + }, + lsp::CompletionItem { + label: "bg-red".into(), + ..Default::default() + }, + lsp::CompletionItem { + label: "bg-yellow".into(), + ..Default::default() + }, + ]))) + }); + + cx.set_state(r#"

"#); + + // Trigger completion when typing a dash, because the dash is an extra + // word character in the 'element' scope, which contains the cursor. + cx.simulate_keystroke("-"); + cx.foreground().run_until_parked(); + cx.update_editor(|editor, _| { + if let Some(ContextMenu::Completions(menu)) = &editor.context_menu { + assert_eq!( + menu.matches.iter().map(|m| &m.string).collect::>(), + &["bg-red", "bg-blue", "bg-yellow"] + ); + } else { + panic!("expected completion menu to be open"); + } + }); + + cx.simulate_keystroke("l"); + cx.foreground().run_until_parked(); + cx.update_editor(|editor, _| { + if let Some(ContextMenu::Completions(menu)) = &editor.context_menu { + assert_eq!( + menu.matches.iter().map(|m| &m.string).collect::>(), + &["bg-blue", "bg-yellow"] + ); + } else { + panic!("expected completion menu to be open"); + } + }); + + // When filtering completions, consider the character after the '-' to + // be the start of a subword. + cx.set_state(r#"

"#); + cx.simulate_keystroke("l"); + cx.foreground().run_until_parked(); + cx.update_editor(|editor, _| { + if let Some(ContextMenu::Completions(menu)) = &editor.context_menu { + assert_eq!( + menu.matches.iter().map(|m| &m.string).collect::>(), + &["bg-yellow"] + ); + } else { + panic!("expected completion menu to be open"); + } + }); +} + fn empty_range(row: usize, column: usize) -> Range { let point = DisplayPoint::new(row as u32, column as u32); point..point diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 62f4c8c806..90fe6ccc52 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -2251,7 +2251,7 @@ impl Element for EditorElement { let replica_id = if let Some(mapping) = &editor.replica_id_mapping { mapping.get(&replica_id).copied() } else { - None + Some(replica_id) }; // The local selections match the leader's selections. diff --git a/crates/editor/src/movement.rs b/crates/editor/src/movement.rs index 915da7b23f..974af4bc24 100644 --- a/crates/editor/src/movement.rs +++ b/crates/editor/src/movement.rs @@ -183,20 +183,21 @@ pub fn line_end( pub fn previous_word_start(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint { let raw_point = point.to_point(map); - let language = map.buffer_snapshot.language_at(raw_point); + let scope = map.buffer_snapshot.language_scope_at(raw_point); find_preceding_boundary(map, point, FindRange::MultiLine, |left, right| { - (char_kind(language, left) != char_kind(language, right) && !right.is_whitespace()) + (char_kind(&scope, left) != char_kind(&scope, right) && !right.is_whitespace()) || left == '\n' }) } pub fn previous_subword_start(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint { let raw_point = point.to_point(map); - let language = map.buffer_snapshot.language_at(raw_point); + let scope = map.buffer_snapshot.language_scope_at(raw_point); + find_preceding_boundary(map, point, FindRange::MultiLine, |left, right| { let is_word_start = - char_kind(language, left) != char_kind(language, right) && !right.is_whitespace(); + char_kind(&scope, left) != char_kind(&scope, right) && !right.is_whitespace(); let is_subword_start = left == '_' && right != '_' || left.is_lowercase() && right.is_uppercase(); is_word_start || is_subword_start || left == '\n' @@ -205,19 +206,21 @@ pub fn previous_subword_start(map: &DisplaySnapshot, point: DisplayPoint) -> Dis pub fn next_word_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint { let raw_point = point.to_point(map); - let language = map.buffer_snapshot.language_at(raw_point); + let scope = map.buffer_snapshot.language_scope_at(raw_point); + find_boundary(map, point, FindRange::MultiLine, |left, right| { - (char_kind(language, left) != char_kind(language, right) && !left.is_whitespace()) + (char_kind(&scope, left) != char_kind(&scope, right) && !left.is_whitespace()) || right == '\n' }) } pub fn next_subword_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint { let raw_point = point.to_point(map); - let language = map.buffer_snapshot.language_at(raw_point); + let scope = map.buffer_snapshot.language_scope_at(raw_point); + find_boundary(map, point, FindRange::MultiLine, |left, right| { let is_word_end = - (char_kind(language, left) != char_kind(language, right)) && !left.is_whitespace(); + (char_kind(&scope, left) != char_kind(&scope, right)) && !left.is_whitespace(); let is_subword_end = left != '_' && right == '_' || left.is_lowercase() && right.is_uppercase(); is_word_end || is_subword_end || right == '\n' @@ -339,14 +342,14 @@ pub fn find_boundary( pub fn is_inside_word(map: &DisplaySnapshot, point: DisplayPoint) -> bool { let raw_point = point.to_point(map); - let language = map.buffer_snapshot.language_at(raw_point); + let scope = map.buffer_snapshot.language_scope_at(raw_point); let ix = map.clip_point(point, Bias::Left).to_offset(map, Bias::Left); let text = &map.buffer_snapshot; - let next_char_kind = text.chars_at(ix).next().map(|c| char_kind(language, c)); + let next_char_kind = text.chars_at(ix).next().map(|c| char_kind(&scope, c)); let prev_char_kind = text .reversed_chars_at(ix) .next() - .map(|c| char_kind(language, c)); + .map(|c| char_kind(&scope, c)); prev_char_kind.zip(next_char_kind) == Some((CharKind::Word, CharKind::Word)) } diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index 3ace5adbc7..74283fd778 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -617,6 +617,42 @@ impl MultiBuffer { } } + pub fn merge_transactions( + &mut self, + transaction: TransactionId, + destination: TransactionId, + cx: &mut ModelContext, + ) { + if let Some(buffer) = self.as_singleton() { + buffer.update(cx, |buffer, _| { + buffer.merge_transactions(transaction, destination) + }); + } else { + if let Some(transaction) = self.history.forget(transaction) { + if let Some(destination) = self.history.transaction_mut(destination) { + for (buffer_id, buffer_transaction_id) in transaction.buffer_transactions { + if let Some(destination_buffer_transaction_id) = + destination.buffer_transactions.get(&buffer_id) + { + if let Some(state) = self.buffers.borrow().get(&buffer_id) { + state.buffer.update(cx, |buffer, _| { + buffer.merge_transactions( + buffer_transaction_id, + *destination_buffer_transaction_id, + ) + }); + } + } else { + destination + .buffer_transactions + .insert(buffer_id, buffer_transaction_id); + } + } + } + } + } + } + pub fn finalize_last_transaction(&mut self, cx: &mut ModelContext) { self.history.finalize_last_transaction(); for BufferState { buffer, .. } in self.buffers.borrow().values() { @@ -788,6 +824,20 @@ impl MultiBuffer { None } + pub fn undo_transaction(&mut self, transaction_id: TransactionId, cx: &mut ModelContext) { + if let Some(buffer) = self.as_singleton() { + buffer.update(cx, |buffer, cx| buffer.undo_transaction(transaction_id, cx)); + } else if let Some(transaction) = self.history.remove_from_undo(transaction_id) { + for (buffer_id, transaction_id) in &transaction.buffer_transactions { + if let Some(BufferState { buffer, .. }) = self.buffers.borrow().get(buffer_id) { + buffer.update(cx, |buffer, cx| { + buffer.undo_transaction(*transaction_id, cx) + }); + } + } + } + } + pub fn stream_excerpts_with_context_lines( &mut self, buffer: ModelHandle, @@ -1367,13 +1417,13 @@ impl MultiBuffer { return false; } - let language = self.language_at(position.clone(), cx); - - if char_kind(language.as_ref(), char) == CharKind::Word { + let snapshot = self.snapshot(cx); + let position = position.to_offset(&snapshot); + let scope = snapshot.language_scope_at(position); + if char_kind(&scope, char) == CharKind::Word { return true; } - let snapshot = self.snapshot(cx); let anchor = snapshot.anchor_before(position); anchor .buffer_id @@ -1875,8 +1925,8 @@ impl MultiBufferSnapshot { let mut next_chars = self.chars_at(start).peekable(); let mut prev_chars = self.reversed_chars_at(start).peekable(); - let language = self.language_at(start); - let kind = |c| char_kind(language, c); + let scope = self.language_scope_at(start); + let kind = |c| char_kind(&scope, c); let word_kind = cmp::max( prev_chars.peek().copied().map(kind), next_chars.peek().copied().map(kind), @@ -2316,6 +2366,16 @@ impl MultiBufferSnapshot { } } + pub fn prev_non_blank_row(&self, mut row: u32) -> Option { + while row > 0 { + row -= 1; + if !self.is_line_blank(row) { + return Some(row); + } + } + None + } + pub fn line_len(&self, row: u32) -> u32 { if let Some((_, range)) = self.buffer_line_for_row(row) { range.end.column - range.start.column @@ -3347,6 +3407,35 @@ impl History { } } + fn forget(&mut self, transaction_id: TransactionId) -> Option { + if let Some(ix) = self + .undo_stack + .iter() + .rposition(|transaction| transaction.id == transaction_id) + { + Some(self.undo_stack.remove(ix)) + } else if let Some(ix) = self + .redo_stack + .iter() + .rposition(|transaction| transaction.id == transaction_id) + { + Some(self.redo_stack.remove(ix)) + } else { + None + } + } + + fn transaction_mut(&mut self, transaction_id: TransactionId) -> Option<&mut Transaction> { + self.undo_stack + .iter_mut() + .find(|transaction| transaction.id == transaction_id) + .or_else(|| { + self.redo_stack + .iter_mut() + .find(|transaction| transaction.id == transaction_id) + }) + } + fn pop_undo(&mut self) -> Option<&mut Transaction> { assert_eq!(self.transaction_depth, 0); if let Some(transaction) = self.undo_stack.pop() { @@ -3367,6 +3456,16 @@ impl History { } } + fn remove_from_undo(&mut self, transaction_id: TransactionId) -> Option<&Transaction> { + let ix = self + .undo_stack + .iter() + .rposition(|transaction| transaction.id == transaction_id)?; + let transaction = self.undo_stack.remove(ix); + self.redo_stack.push(transaction); + self.redo_stack.last() + } + fn group(&mut self) -> Option { let mut count = 0; let mut transactions = self.undo_stack.iter(); diff --git a/crates/editor/src/scroll/scroll_amount.rs b/crates/editor/src/scroll/scroll_amount.rs index f9d09adcf5..cadf37b31d 100644 --- a/crates/editor/src/scroll/scroll_amount.rs +++ b/crates/editor/src/scroll/scroll_amount.rs @@ -39,7 +39,7 @@ impl ScrollAmount { .visible_line_count() // subtract one to leave an anchor line // round towards zero (so page-up and page-down are symmetric) - .map(|l| ((l - 1.) * count).trunc()) + .map(|l| (l * count).trunc() - count.signum()) .unwrap_or(0.), } } diff --git a/crates/editor/src/test/editor_lsp_test_context.rs b/crates/editor/src/test/editor_lsp_test_context.rs index 668d6abf21..bbc4911f35 100644 --- a/crates/editor/src/test/editor_lsp_test_context.rs +++ b/crates/editor/src/test/editor_lsp_test_context.rs @@ -51,7 +51,7 @@ impl<'a> EditorLspTestContext<'a> { language .path_suffixes() .first() - .unwrap_or(&"txt".to_string()) + .expect("language must have a path suffix for EditorLspTestContext") ); let mut fake_servers = language diff --git a/crates/fs/Cargo.toml b/crates/fs/Cargo.toml index b3ebd224b0..7584dec21a 100644 --- a/crates/fs/Cargo.toml +++ b/crates/fs/Cargo.toml @@ -12,6 +12,7 @@ collections = { path = "../collections" } gpui = { path = "../gpui" } lsp = { path = "../lsp" } rope = { path = "../rope" } +text = { path = "../text" } util = { path = "../util" } sum_tree = { path = "../sum_tree" } rpc = { path = "../rpc" } diff --git a/crates/fs/src/fs.rs b/crates/fs/src/fs.rs index ec8a249ff4..ecaee4534e 100644 --- a/crates/fs/src/fs.rs +++ b/crates/fs/src/fs.rs @@ -4,14 +4,10 @@ use anyhow::{anyhow, Result}; use fsevent::EventStream; use futures::{future::BoxFuture, Stream, StreamExt}; use git2::Repository as LibGitRepository; -use lazy_static::lazy_static; use parking_lot::Mutex; -use regex::Regex; use repository::GitRepository; use rope::Rope; use smol::io::{AsyncReadExt, AsyncWriteExt}; -use std::borrow::Cow; -use std::cmp; use std::io::Write; use std::sync::Arc; use std::{ @@ -22,6 +18,7 @@ use std::{ time::{Duration, SystemTime}, }; use tempfile::NamedTempFile; +use text::LineEnding; use util::ResultExt; #[cfg(any(test, feature = "test-support"))] @@ -33,66 +30,6 @@ use std::ffi::OsStr; #[cfg(any(test, feature = "test-support"))] use std::sync::Weak; -lazy_static! { - static ref LINE_SEPARATORS_REGEX: Regex = Regex::new("\r\n|\r|\u{2028}|\u{2029}").unwrap(); -} - -#[derive(Clone, Copy, Debug, PartialEq)] -pub enum LineEnding { - Unix, - Windows, -} - -impl Default for LineEnding { - fn default() -> Self { - #[cfg(unix)] - return Self::Unix; - - #[cfg(not(unix))] - return Self::CRLF; - } -} - -impl LineEnding { - pub fn as_str(&self) -> &'static str { - match self { - LineEnding::Unix => "\n", - LineEnding::Windows => "\r\n", - } - } - - pub fn detect(text: &str) -> Self { - let mut max_ix = cmp::min(text.len(), 1000); - while !text.is_char_boundary(max_ix) { - max_ix -= 1; - } - - if let Some(ix) = text[..max_ix].find(&['\n']) { - if ix > 0 && text.as_bytes()[ix - 1] == b'\r' { - Self::Windows - } else { - Self::Unix - } - } else { - Self::default() - } - } - - pub fn normalize(text: &mut String) { - if let Cow::Owned(replaced) = LINE_SEPARATORS_REGEX.replace_all(text, "\n") { - *text = replaced; - } - } - - pub fn normalize_arc(text: Arc) -> Arc { - if let Cow::Owned(replaced) = LINE_SEPARATORS_REGEX.replace_all(&text, "\n") { - replaced.into() - } else { - text - } - } -} - #[async_trait::async_trait] pub trait Fs: Send + Sync { async fn create_dir(&self, path: &Path) -> Result<()>; @@ -520,7 +457,7 @@ impl FakeFsState { } #[cfg(any(test, feature = "test-support"))] -lazy_static! { +lazy_static::lazy_static! { pub static ref FS_DOT_GIT: &'static OsStr = OsStr::new(".git"); } diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 902ed26b57..38b2842c12 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -15,7 +15,6 @@ use crate::{ }; use anyhow::{anyhow, Result}; pub use clock::ReplicaId; -use fs::LineEnding; use futures::FutureExt as _; use gpui::{fonts::HighlightStyle, AppContext, Entity, ModelContext, Task}; use lsp::LanguageServerId; @@ -149,6 +148,7 @@ pub struct Completion { pub old_range: Range, pub new_text: String, pub label: CodeLabel, + pub server_id: LanguageServerId, pub lsp_completion: lsp::CompletionItem, } @@ -439,7 +439,7 @@ impl Buffer { operations.extend( text_operations .iter() - .filter(|(_, op)| !since.observed(op.local_timestamp())) + .filter(|(_, op)| !since.observed(op.timestamp())) .map(|(_, op)| proto::serialize_operation(&Operation::Buffer(op.clone()))), ); operations.sort_unstable_by_key(proto::lamport_timestamp_for_operation); @@ -1298,9 +1298,13 @@ impl Buffer { self.text.forget_transaction(transaction_id); } + pub fn merge_transactions(&mut self, transaction: TransactionId, destination: TransactionId) { + self.text.merge_transactions(transaction, destination); + } + pub fn wait_for_edits( &mut self, - edit_ids: impl IntoIterator, + edit_ids: impl IntoIterator, ) -> impl Future> { self.text.wait_for_edits(edit_ids) } @@ -1358,7 +1362,7 @@ impl Buffer { } } - pub fn set_text(&mut self, text: T, cx: &mut ModelContext) -> Option + pub fn set_text(&mut self, text: T, cx: &mut ModelContext) -> Option where T: Into>, { @@ -1371,7 +1375,7 @@ impl Buffer { edits_iter: I, autoindent_mode: Option, cx: &mut ModelContext, - ) -> Option + ) -> Option where I: IntoIterator, T)>, S: ToOffset, @@ -1408,7 +1412,7 @@ impl Buffer { .and_then(|mode| self.language.as_ref().map(|_| (self.snapshot(), mode))); let edit_operation = self.text.edit(edits.iter().cloned()); - let edit_id = edit_operation.local_timestamp(); + let edit_id = edit_operation.timestamp(); if let Some((before_edit, mode)) = autoindent_request { let mut delta = 0isize; @@ -1664,6 +1668,22 @@ impl Buffer { } } + pub fn undo_transaction( + &mut self, + transaction_id: TransactionId, + cx: &mut ModelContext, + ) -> bool { + let was_dirty = self.is_dirty(); + let old_version = self.version.clone(); + if let Some(operation) = self.text.undo_transaction(transaction_id) { + self.send_operation(Operation::Buffer(operation), cx); + self.did_edit(&old_version, was_dirty, cx); + true + } else { + false + } + } + pub fn undo_to_transaction( &mut self, transaction_id: TransactionId, @@ -2197,8 +2217,8 @@ impl BufferSnapshot { let mut next_chars = self.chars_at(start).peekable(); let mut prev_chars = self.reversed_chars_at(start).peekable(); - let language = self.language_at(start); - let kind = |c| char_kind(language, c); + let scope = self.language_scope_at(start); + let kind = |c| char_kind(&scope, c); let word_kind = cmp::max( prev_chars.peek().copied().map(kind), next_chars.peek().copied().map(kind), @@ -3012,17 +3032,21 @@ pub fn contiguous_ranges( }) } -pub fn char_kind(language: Option<&Arc>, c: char) -> CharKind { +pub fn char_kind(scope: &Option, c: char) -> CharKind { if c.is_whitespace() { return CharKind::Whitespace; } else if c.is_alphanumeric() || c == '_' { return CharKind::Word; } - if let Some(language) = language { - if language.config.word_characters.contains(&c) { - return CharKind::Word; + + if let Some(scope) = scope { + if let Some(characters) = scope.word_characters() { + if characters.contains(&c) { + return CharKind::Word; + } } } + CharKind::Punctuation } diff --git a/crates/language/src/buffer_tests.rs b/crates/language/src/buffer_tests.rs index db3749aa25..3bedf5b7a8 100644 --- a/crates/language/src/buffer_tests.rs +++ b/crates/language/src/buffer_tests.rs @@ -5,7 +5,6 @@ use crate::language_settings::{ use super::*; use clock::ReplicaId; use collections::BTreeMap; -use fs::LineEnding; use gpui::{AppContext, ModelHandle}; use indoc::indoc; use proto::deserialize_operation; @@ -20,6 +19,7 @@ use std::{ time::{Duration, Instant}, }; use text::network::Network; +use text::LineEnding; use unindent::Unindent as _; use util::{assert_set_eq, post_inc, test::marked_text_ranges, RandomCharIter}; diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 7a9e6b83ce..2193b5c07e 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -46,7 +46,7 @@ use theme::{SyntaxTheme, Theme}; use tree_sitter::{self, Query}; use unicase::UniCase; use util::{http::HttpClient, paths::PathExt}; -use util::{merge_json_value_into, post_inc, ResultExt, TryFutureExt as _, UnwrapFuture}; +use util::{post_inc, ResultExt, TryFutureExt as _, UnwrapFuture}; #[cfg(any(test, feature = "test-support"))] use futures::channel::mpsc; @@ -57,6 +57,7 @@ pub use diagnostic_set::DiagnosticEntry; pub use lsp::LanguageServerId; pub use outline::{Outline, OutlineItem}; pub use syntax_map::{OwnedSyntaxLayerInfo, SyntaxLayerInfo}; +pub use text::LineEnding; pub use tree_sitter::{Parser, Tree}; pub fn init(cx: &mut AppContext) { @@ -90,6 +91,7 @@ pub struct LanguageServerName(pub Arc); /// once at startup, and caches the results. pub struct CachedLspAdapter { pub name: LanguageServerName, + pub short_name: &'static str, pub initialization_options: Option, pub disk_based_diagnostic_sources: Vec, pub disk_based_diagnostics_progress_token: Option, @@ -100,6 +102,7 @@ pub struct CachedLspAdapter { impl CachedLspAdapter { pub async fn new(adapter: Arc) -> Arc { let name = adapter.name().await; + let short_name = adapter.short_name(); let initialization_options = adapter.initialization_options().await; let disk_based_diagnostic_sources = adapter.disk_based_diagnostic_sources().await; let disk_based_diagnostics_progress_token = @@ -108,6 +111,7 @@ impl CachedLspAdapter { Arc::new(CachedLspAdapter { name, + short_name, initialization_options, disk_based_diagnostic_sources, disk_based_diagnostics_progress_token, @@ -175,10 +179,7 @@ impl CachedLspAdapter { self.adapter.code_action_kinds() } - pub fn workspace_configuration( - &self, - cx: &mut AppContext, - ) -> Option> { + pub fn workspace_configuration(&self, cx: &mut AppContext) -> BoxFuture<'static, Value> { self.adapter.workspace_configuration(cx) } @@ -219,6 +220,8 @@ pub trait LspAdapterDelegate: Send + Sync { pub trait LspAdapter: 'static + Send + Sync { async fn name(&self) -> LanguageServerName; + fn short_name(&self) -> &'static str; + async fn fetch_latest_server_version( &self, delegate: &dyn LspAdapterDelegate, @@ -287,8 +290,8 @@ pub trait LspAdapter: 'static + Send + Sync { None } - fn workspace_configuration(&self, _: &mut AppContext) -> Option> { - None + fn workspace_configuration(&self, _: &mut AppContext) -> BoxFuture<'static, Value> { + futures::future::ready(serde_json::json!({})).boxed() } fn code_action_kinds(&self) -> Option> { @@ -343,6 +346,8 @@ pub struct LanguageConfig { #[serde(default)] pub block_comment: Option<(Arc, Arc)>, #[serde(default)] + pub scope_opt_in_language_servers: Vec, + #[serde(default)] pub overrides: HashMap, #[serde(default)] pub word_characters: HashSet, @@ -373,6 +378,10 @@ pub struct LanguageConfigOverride { pub block_comment: Override<(Arc, Arc)>, #[serde(skip_deserializing)] pub disabled_bracket_ixs: Vec, + #[serde(default)] + pub word_characters: Override>, + #[serde(default)] + pub opt_into_language_servers: Vec, } #[derive(Clone, Deserialize, Debug)] @@ -411,6 +420,7 @@ impl Default for LanguageConfig { autoclose_before: Default::default(), line_comment: Default::default(), block_comment: Default::default(), + scope_opt_in_language_servers: Default::default(), overrides: Default::default(), collapsed_placeholder: Default::default(), word_characters: Default::default(), @@ -685,41 +695,6 @@ impl LanguageRegistry { result } - pub fn workspace_configuration(&self, cx: &mut AppContext) -> Task { - let lsp_adapters = { - let state = self.state.read(); - state - .available_languages - .iter() - .filter(|l| !l.loaded) - .flat_map(|l| l.lsp_adapters.clone()) - .chain( - state - .languages - .iter() - .flat_map(|language| &language.adapters) - .map(|adapter| adapter.adapter.clone()), - ) - .collect::>() - }; - - let mut language_configs = Vec::new(); - for adapter in &lsp_adapters { - if let Some(language_config) = adapter.workspace_configuration(cx) { - language_configs.push(language_config); - } - } - - cx.background().spawn(async move { - let mut config = serde_json::json!({}); - let language_configs = futures::future::join_all(language_configs).await; - for language_config in language_configs { - merge_json_value_into(language_config, &mut config); - } - config - }) - } - pub fn add(&self, language: Arc) { self.state.write().add(language); } @@ -1383,13 +1358,23 @@ impl Language { Ok(self) } - pub fn with_override_query(mut self, source: &str) -> Result { + pub fn with_override_query(mut self, source: &str) -> anyhow::Result { let query = Query::new(self.grammar_mut().ts_language, source)?; let mut override_configs_by_id = HashMap::default(); for (ix, name) in query.capture_names().iter().enumerate() { if !name.starts_with('_') { let value = self.config.overrides.remove(name).unwrap_or_default(); + for server_name in &value.opt_into_language_servers { + if !self + .config + .scope_opt_in_language_servers + .contains(server_name) + { + util::debug_panic!("Server {server_name:?} has been opted-in by scope {name:?} but has not been marked as an opt-in server"); + } + } + override_configs_by_id.insert(ix as u32, (name.clone(), value)); } } @@ -1595,6 +1580,13 @@ impl LanguageScope { .map(|e| (&e.0, &e.1)) } + pub fn word_characters(&self) -> Option<&HashSet> { + Override::as_option( + self.config_override().map(|o| &o.word_characters), + Some(&self.language.config.word_characters), + ) + } + pub fn brackets(&self) -> impl Iterator { let mut disabled_ids = self .config_override() @@ -1621,6 +1613,20 @@ impl LanguageScope { c.is_whitespace() || self.language.config.autoclose_before.contains(c) } + pub fn language_allowed(&self, name: &LanguageServerName) -> bool { + let config = &self.language.config; + let opt_in_servers = &config.scope_opt_in_language_servers; + if opt_in_servers.iter().any(|o| *o == *name.0) { + if let Some(over) = self.config_override() { + over.opt_into_language_servers.iter().any(|o| *o == *name.0) + } else { + false + } + } else { + true + } + } + fn config_override(&self) -> Option<&LanguageConfigOverride> { let id = self.override_id?; let grammar = self.language.grammar.as_ref()?; @@ -1725,6 +1731,10 @@ impl LspAdapter for Arc { LanguageServerName(self.name.into()) } + fn short_name(&self) -> &'static str { + "FakeLspAdapter" + } + async fn fetch_latest_server_version( &self, _: &dyn LspAdapterDelegate, diff --git a/crates/language/src/proto.rs b/crates/language/src/proto.rs index 09c5ec7fc3..80eb972f42 100644 --- a/crates/language/src/proto.rs +++ b/crates/language/src/proto.rs @@ -20,17 +20,17 @@ pub fn deserialize_fingerprint(fingerprint: &str) -> Result { .map_err(|error| anyhow!("invalid fingerprint: {}", error)) } -pub fn deserialize_line_ending(message: proto::LineEnding) -> fs::LineEnding { +pub fn deserialize_line_ending(message: proto::LineEnding) -> text::LineEnding { match message { - proto::LineEnding::Unix => fs::LineEnding::Unix, - proto::LineEnding::Windows => fs::LineEnding::Windows, + proto::LineEnding::Unix => text::LineEnding::Unix, + proto::LineEnding::Windows => text::LineEnding::Windows, } } -pub fn serialize_line_ending(message: fs::LineEnding) -> proto::LineEnding { +pub fn serialize_line_ending(message: text::LineEnding) -> proto::LineEnding { match message { - fs::LineEnding::Unix => proto::LineEnding::Unix, - fs::LineEnding::Windows => proto::LineEnding::Windows, + text::LineEnding::Unix => proto::LineEnding::Unix, + text::LineEnding::Windows => proto::LineEnding::Windows, } } @@ -41,24 +41,22 @@ pub fn serialize_operation(operation: &crate::Operation) -> proto::Operation { proto::operation::Variant::Edit(serialize_edit_operation(edit)) } - crate::Operation::Buffer(text::Operation::Undo { - undo, - lamport_timestamp, - }) => proto::operation::Variant::Undo(proto::operation::Undo { - replica_id: undo.id.replica_id as u32, - local_timestamp: undo.id.value, - lamport_timestamp: lamport_timestamp.value, - version: serialize_version(&undo.version), - counts: undo - .counts - .iter() - .map(|(edit_id, count)| proto::UndoCount { - replica_id: edit_id.replica_id as u32, - local_timestamp: edit_id.value, - count: *count, - }) - .collect(), - }), + crate::Operation::Buffer(text::Operation::Undo(undo)) => { + proto::operation::Variant::Undo(proto::operation::Undo { + replica_id: undo.timestamp.replica_id as u32, + lamport_timestamp: undo.timestamp.value, + version: serialize_version(&undo.version), + counts: undo + .counts + .iter() + .map(|(edit_id, count)| proto::UndoCount { + replica_id: edit_id.replica_id as u32, + lamport_timestamp: edit_id.value, + count: *count, + }) + .collect(), + }) + } crate::Operation::UpdateSelections { selections, @@ -101,8 +99,7 @@ pub fn serialize_operation(operation: &crate::Operation) -> proto::Operation { pub fn serialize_edit_operation(operation: &EditOperation) -> proto::operation::Edit { proto::operation::Edit { replica_id: operation.timestamp.replica_id as u32, - local_timestamp: operation.timestamp.local, - lamport_timestamp: operation.timestamp.lamport, + lamport_timestamp: operation.timestamp.value, version: serialize_version(&operation.version), ranges: operation.ranges.iter().map(serialize_range).collect(), new_text: operation @@ -114,7 +111,7 @@ pub fn serialize_edit_operation(operation: &EditOperation) -> proto::operation:: } pub fn serialize_undo_map_entry( - (edit_id, counts): (&clock::Local, &[(clock::Local, u32)]), + (edit_id, counts): (&clock::Lamport, &[(clock::Lamport, u32)]), ) -> proto::UndoMapEntry { proto::UndoMapEntry { replica_id: edit_id.replica_id as u32, @@ -123,7 +120,7 @@ pub fn serialize_undo_map_entry( .iter() .map(|(undo_id, count)| proto::UndoCount { replica_id: undo_id.replica_id as u32, - local_timestamp: undo_id.value, + lamport_timestamp: undo_id.value, count: *count, }) .collect(), @@ -197,7 +194,7 @@ pub fn serialize_diagnostics<'a>( pub fn serialize_anchor(anchor: &Anchor) -> proto::Anchor { proto::Anchor { replica_id: anchor.timestamp.replica_id as u32, - local_timestamp: anchor.timestamp.value, + timestamp: anchor.timestamp.value, offset: anchor.offset as u64, bias: match anchor.bias { Bias::Left => proto::Bias::Left as i32, @@ -218,32 +215,26 @@ pub fn deserialize_operation(message: proto::Operation) -> Result { - crate::Operation::Buffer(text::Operation::Undo { - lamport_timestamp: clock::Lamport { + crate::Operation::Buffer(text::Operation::Undo(UndoOperation { + timestamp: clock::Lamport { replica_id: undo.replica_id as ReplicaId, value: undo.lamport_timestamp, }, - undo: UndoOperation { - id: clock::Local { - replica_id: undo.replica_id as ReplicaId, - value: undo.local_timestamp, - }, - version: deserialize_version(&undo.version), - counts: undo - .counts - .into_iter() - .map(|c| { - ( - clock::Local { - replica_id: c.replica_id as ReplicaId, - value: c.local_timestamp, - }, - c.count, - ) - }) - .collect(), - }, - }) + version: deserialize_version(&undo.version), + counts: undo + .counts + .into_iter() + .map(|c| { + ( + clock::Lamport { + replica_id: c.replica_id as ReplicaId, + value: c.lamport_timestamp, + }, + c.count, + ) + }) + .collect(), + })) } proto::operation::Variant::UpdateSelections(message) => { let selections = message @@ -298,10 +289,9 @@ pub fn deserialize_operation(message: proto::Operation) -> Result EditOperation { EditOperation { - timestamp: InsertionTimestamp { + timestamp: clock::Lamport { replica_id: edit.replica_id as ReplicaId, - local: edit.local_timestamp, - lamport: edit.lamport_timestamp, + value: edit.lamport_timestamp, }, version: deserialize_version(&edit.version), ranges: edit.ranges.into_iter().map(deserialize_range).collect(), @@ -311,9 +301,9 @@ pub fn deserialize_edit_operation(edit: proto::operation::Edit) -> EditOperation pub fn deserialize_undo_map_entry( entry: proto::UndoMapEntry, -) -> (clock::Local, Vec<(clock::Local, u32)>) { +) -> (clock::Lamport, Vec<(clock::Lamport, u32)>) { ( - clock::Local { + clock::Lamport { replica_id: entry.replica_id as u16, value: entry.local_timestamp, }, @@ -322,9 +312,9 @@ pub fn deserialize_undo_map_entry( .into_iter() .map(|undo_count| { ( - clock::Local { + clock::Lamport { replica_id: undo_count.replica_id as u16, - value: undo_count.local_timestamp, + value: undo_count.lamport_timestamp, }, undo_count.count, ) @@ -384,9 +374,9 @@ pub fn deserialize_diagnostics( pub fn deserialize_anchor(anchor: proto::Anchor) -> Option { Some(Anchor { - timestamp: clock::Local { + timestamp: clock::Lamport { replica_id: anchor.replica_id as ReplicaId, - value: anchor.local_timestamp, + value: anchor.timestamp, }, offset: anchor.offset as usize, bias: match proto::Bias::from_i32(anchor.bias)? { @@ -434,6 +424,7 @@ pub fn serialize_completion(completion: &Completion) -> proto::Completion { old_start: Some(serialize_anchor(&completion.old_range.start)), old_end: Some(serialize_anchor(&completion.old_range.end)), new_text: completion.new_text.clone(), + server_id: completion.server_id.0 as u64, lsp_completion: serde_json::to_vec(&completion.lsp_completion).unwrap(), } } @@ -466,6 +457,7 @@ pub async fn deserialize_completion( lsp_completion.filter_text.as_deref(), ) }), + server_id: LanguageServerId(completion.server_id as usize), lsp_completion, }) } @@ -498,12 +490,12 @@ pub fn deserialize_code_action(action: proto::CodeAction) -> Result pub fn serialize_transaction(transaction: &Transaction) -> proto::Transaction { proto::Transaction { - id: Some(serialize_local_timestamp(transaction.id)), + id: Some(serialize_timestamp(transaction.id)), edit_ids: transaction .edit_ids .iter() .copied() - .map(serialize_local_timestamp) + .map(serialize_timestamp) .collect(), start: serialize_version(&transaction.start), } @@ -511,7 +503,7 @@ pub fn serialize_transaction(transaction: &Transaction) -> proto::Transaction { pub fn deserialize_transaction(transaction: proto::Transaction) -> Result { Ok(Transaction { - id: deserialize_local_timestamp( + id: deserialize_timestamp( transaction .id .ok_or_else(|| anyhow!("missing transaction id"))?, @@ -519,21 +511,21 @@ pub fn deserialize_transaction(transaction: proto::Transaction) -> Result proto::LocalTimestamp { - proto::LocalTimestamp { +pub fn serialize_timestamp(timestamp: clock::Lamport) -> proto::LamportTimestamp { + proto::LamportTimestamp { replica_id: timestamp.replica_id as u32, value: timestamp.value, } } -pub fn deserialize_local_timestamp(timestamp: proto::LocalTimestamp) -> clock::Local { - clock::Local { +pub fn deserialize_timestamp(timestamp: proto::LamportTimestamp) -> clock::Lamport { + clock::Lamport { replica_id: timestamp.replica_id as ReplicaId, value: timestamp.value, } @@ -553,7 +545,7 @@ pub fn deserialize_range(range: proto::Range) -> Range { pub fn deserialize_version(message: &[proto::VectorClockEntry]) -> clock::Global { let mut version = clock::Global::new(); for entry in message { - version.observe(clock::Local { + version.observe(clock::Lamport { replica_id: entry.replica_id as ReplicaId, value: entry.timestamp, }); diff --git a/crates/language_tools/src/lsp_log.rs b/crates/language_tools/src/lsp_log.rs index 51bdb4c5ce..60c4e41666 100644 --- a/crates/language_tools/src/lsp_log.rs +++ b/crates/language_tools/src/lsp_log.rs @@ -12,6 +12,7 @@ use gpui::{ ViewHandle, WeakModelHandle, }; use language::{Buffer, LanguageServerId, LanguageServerName}; +use lsp::IoKind; use project::{Project, Worktree}; use std::{borrow::Cow, sync::Arc}; use theme::{ui, Theme}; @@ -26,7 +27,7 @@ const RECEIVE_LINE: &str = "// Receive:\n"; pub struct LogStore { projects: HashMap, ProjectState>, - io_tx: mpsc::UnboundedSender<(WeakModelHandle, LanguageServerId, bool, String)>, + io_tx: mpsc::UnboundedSender<(WeakModelHandle, LanguageServerId, IoKind, String)>, } struct ProjectState { @@ -37,12 +38,12 @@ struct ProjectState { struct LanguageServerState { log_buffer: ModelHandle, rpc_state: Option, + _subscription: Option, } struct LanguageServerRpcState { buffer: ModelHandle, last_message_kind: Option, - _subscription: lsp::Subscription, } pub struct LspLogView { @@ -118,11 +119,11 @@ impl LogStore { io_tx, }; cx.spawn_weak(|this, mut cx| async move { - while let Some((project, server_id, is_output, mut message)) = io_rx.next().await { + while let Some((project, server_id, io_kind, mut message)) = io_rx.next().await { if let Some(this) = this.upgrade(&cx) { this.update(&mut cx, |this, cx| { message.push('\n'); - this.on_io(project, server_id, is_output, &message, cx); + this.on_io(project, server_id, io_kind, &message, cx); }); } } @@ -168,22 +169,29 @@ impl LogStore { cx: &mut ModelContext, ) -> Option> { let project_state = self.projects.get_mut(&project.downgrade())?; - Some( - project_state - .servers - .entry(id) - .or_insert_with(|| { - cx.notify(); - LanguageServerState { - rpc_state: None, - log_buffer: cx - .add_model(|cx| Buffer::new(0, cx.model_id() as u64, "")) - .clone(), - } - }) - .log_buffer - .clone(), - ) + let server_state = project_state.servers.entry(id).or_insert_with(|| { + cx.notify(); + LanguageServerState { + rpc_state: None, + log_buffer: cx + .add_model(|cx| Buffer::new(0, cx.model_id() as u64, "")) + .clone(), + _subscription: None, + } + }); + + let server = project.read(cx).language_server_for_id(id); + let weak_project = project.downgrade(); + let io_tx = self.io_tx.clone(); + server_state._subscription = server.map(|server| { + server.on_io(move |io_kind, message| { + io_tx + .unbounded_send((weak_project, id, io_kind, message.to_string())) + .ok(); + }) + }); + + Some(server_state.log_buffer.clone()) } fn add_language_server_log( @@ -230,7 +238,7 @@ impl LogStore { Some(server_state.log_buffer.clone()) } - pub fn enable_rpc_trace_for_language_server( + fn enable_rpc_trace_for_language_server( &mut self, project: &ModelHandle, server_id: LanguageServerId, @@ -239,9 +247,7 @@ impl LogStore { let weak_project = project.downgrade(); let project_state = self.projects.get_mut(&weak_project)?; let server_state = project_state.servers.get_mut(&server_id)?; - let server = project.read(cx).language_server_for_id(server_id)?; let rpc_state = server_state.rpc_state.get_or_insert_with(|| { - let io_tx = self.io_tx.clone(); let language = project.read(cx).languages().language_for_name("JSON"); let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "")); cx.spawn_weak({ @@ -258,11 +264,6 @@ impl LogStore { LanguageServerRpcState { buffer, last_message_kind: None, - _subscription: server.on_io(move |is_received, json| { - io_tx - .unbounded_send((weak_project, server_id, is_received, json.to_string())) - .ok(); - }), } }); Some(rpc_state.buffer.clone()) @@ -285,10 +286,25 @@ impl LogStore { &mut self, project: WeakModelHandle, language_server_id: LanguageServerId, - is_received: bool, + io_kind: IoKind, message: &str, cx: &mut AppContext, ) -> Option<()> { + let is_received = match io_kind { + IoKind::StdOut => true, + IoKind::StdIn => false, + IoKind::StdErr => { + let project = project.upgrade(cx)?; + project.update(cx, |_, cx| { + cx.emit(project::Event::LanguageServerLog( + language_server_id, + format!("stderr: {}\n", message.trim()), + )) + }); + return Some(()); + } + }; + let state = self .projects .get_mut(&project)? diff --git a/crates/lsp/Cargo.toml b/crates/lsp/Cargo.toml index 47e0995c85..653c25b7bb 100644 --- a/crates/lsp/Cargo.toml +++ b/crates/lsp/Cargo.toml @@ -20,7 +20,7 @@ anyhow.workspace = true async-pipe = { git = "https://github.com/zed-industries/async-pipe-rs", rev = "82d00a04211cf4e1236029aa03e6b6ce2a74c553", optional = true } futures.workspace = true log.workspace = true -lsp-types = "0.94" +lsp-types = { git = "https://github.com/zed-industries/lsp-types", branch = "updated-completion-list-item-defaults" } parking_lot.workspace = true postage.workspace = true serde.workspace = true diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index d49dafff2f..dcfce4f1fb 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -4,7 +4,7 @@ pub use lsp_types::*; use anyhow::{anyhow, Context, Result}; use collections::HashMap; -use futures::{channel::oneshot, io::BufWriter, AsyncRead, AsyncWrite}; +use futures::{channel::oneshot, io::BufWriter, AsyncRead, AsyncWrite, FutureExt}; use gpui::{executor, AsyncAppContext, Task}; use parking_lot::Mutex; use postage::{barrier, prelude::Stream}; @@ -26,16 +26,25 @@ use std::{ atomic::{AtomicUsize, Ordering::SeqCst}, Arc, Weak, }, + time::{Duration, Instant}, }; use std::{path::Path, process::Stdio}; use util::{ResultExt, TryFutureExt}; const JSON_RPC_VERSION: &str = "2.0"; const CONTENT_LEN_HEADER: &str = "Content-Length: "; +const LSP_REQUEST_TIMEOUT: Duration = Duration::from_secs(60 * 2); type NotificationHandler = Box, &str, AsyncAppContext)>; type ResponseHandler = Box)>; -type IoHandler = Box; +type IoHandler = Box; + +#[derive(Debug, Clone, Copy)] +pub enum IoKind { + StdOut, + StdIn, + StdErr, +} #[derive(Debug, Clone, Deserialize)] pub struct LanguageServerBinary { @@ -144,16 +153,18 @@ impl LanguageServer { .args(binary.arguments) .stdin(Stdio::piped()) .stdout(Stdio::piped()) - .stderr(Stdio::inherit()) + .stderr(Stdio::piped()) .kill_on_drop(true) .spawn()?; let stdin = server.stdin.take().unwrap(); - let stout = server.stdout.take().unwrap(); + let stdout = server.stdout.take().unwrap(); + let stderr = server.stderr.take().unwrap(); let mut server = Self::new_internal( server_id.clone(), stdin, - stout, + stdout, + Some(stderr), Some(server), root_path, code_action_kinds, @@ -181,10 +192,11 @@ impl LanguageServer { Ok(server) } - fn new_internal( + fn new_internal( server_id: LanguageServerId, stdin: Stdin, stdout: Stdout, + stderr: Option, server: Option, root_path: &Path, code_action_kinds: Option>, @@ -194,7 +206,8 @@ impl LanguageServer { where Stdin: AsyncWrite + Unpin + Send + 'static, Stdout: AsyncRead + Unpin + Send + 'static, - F: FnMut(AnyNotification) + 'static + Send, + Stderr: AsyncRead + Unpin + Send + 'static, + F: FnMut(AnyNotification) + 'static + Send + Clone, { let (outbound_tx, outbound_rx) = channel::unbounded::(); let (output_done_tx, output_done_rx) = barrier::channel(); @@ -203,17 +216,27 @@ impl LanguageServer { let response_handlers = Arc::new(Mutex::new(Some(HashMap::<_, ResponseHandler>::default()))); let io_handlers = Arc::new(Mutex::new(HashMap::default())); - let input_task = cx.spawn(|cx| { - Self::handle_input( - stdout, - on_unhandled_notification, - notification_handlers.clone(), - response_handlers.clone(), - io_handlers.clone(), - cx, - ) + + let stdout_input_task = cx.spawn(|cx| { + { + Self::handle_input( + stdout, + on_unhandled_notification.clone(), + notification_handlers.clone(), + response_handlers.clone(), + io_handlers.clone(), + cx, + ) + } .log_err() }); + let stderr_input_task = stderr + .map(|stderr| cx.spawn(|_| Self::handle_stderr(stderr, io_handlers.clone()).log_err())) + .unwrap_or_else(|| Task::Ready(Some(None))); + let input_task = cx.spawn(|_| async move { + let (stdout, stderr) = futures::join!(stdout_input_task, stderr_input_task); + stdout.or(stderr) + }); let output_task = cx.background().spawn({ Self::handle_output( stdin, @@ -282,9 +305,9 @@ impl LanguageServer { stdout.read_exact(&mut buffer).await?; if let Ok(message) = str::from_utf8(&buffer) { - log::trace!("incoming message:{}", message); + log::trace!("incoming message: {}", message); for handler in io_handlers.lock().values_mut() { - handler(true, message); + handler(IoKind::StdOut, message); } } @@ -327,6 +350,30 @@ impl LanguageServer { } } + async fn handle_stderr( + stderr: Stderr, + io_handlers: Arc>>, + ) -> anyhow::Result<()> + where + Stderr: AsyncRead + Unpin + Send + 'static, + { + let mut stderr = BufReader::new(stderr); + let mut buffer = Vec::new(); + loop { + buffer.clear(); + stderr.read_until(b'\n', &mut buffer).await?; + if let Ok(message) = str::from_utf8(&buffer) { + log::trace!("incoming stderr message:{message}"); + for handler in io_handlers.lock().values_mut() { + handler(IoKind::StdErr, message); + } + } + + // Don't starve the main thread when receiving lots of messages at once. + smol::future::yield_now().await; + } + } + async fn handle_output( stdin: Stdin, outbound_rx: channel::Receiver, @@ -348,7 +395,7 @@ impl LanguageServer { while let Ok(message) = outbound_rx.recv().await { log::trace!("outgoing message:{}", message); for handler in io_handlers.lock().values_mut() { - handler(false, &message); + handler(IoKind::StdIn, &message); } content_len_buffer.clear(); @@ -423,6 +470,14 @@ impl LanguageServer { }), ..Default::default() }), + completion_list: Some(CompletionListCapability { + item_defaults: Some(vec![ + "commitCharacters".to_owned(), + "editRange".to_owned(), + "insertTextMode".to_owned(), + "data".to_owned(), + ]), + }), ..Default::default() }), rename: Some(RenameClientCapabilities { @@ -532,7 +587,7 @@ impl LanguageServer { #[must_use] pub fn on_io(&self, f: F) -> Subscription where - F: 'static + Send + FnMut(bool, &str), + F: 'static + Send + FnMut(IoKind, &str), { let id = self.next_id.fetch_add(1, SeqCst); self.io_handlers.lock().insert(id, Box::new(f)); @@ -695,7 +750,7 @@ impl LanguageServer { outbound_tx: &channel::Sender, executor: &Arc, params: T::Params, - ) -> impl 'static + Future> + ) -> impl 'static + Future> where T::Result: 'static + Send, { @@ -736,10 +791,25 @@ impl LanguageServer { .try_send(message) .context("failed to write to language server's stdin"); + let mut timeout = executor.timer(LSP_REQUEST_TIMEOUT).fuse(); + let started = Instant::now(); async move { handle_response?; send?; - rx.await? + + let method = T::METHOD; + futures::select! { + response = rx.fuse() => { + let elapsed = started.elapsed(); + log::trace!("Took {elapsed:?} to recieve response to {method:?} id {id}"); + response? + } + + _ = timeout => { + log::error!("Cancelled LSP request task for {method:?} id {id} which took over {LSP_REQUEST_TIMEOUT:?}"); + anyhow::bail!("LSP request timeout"); + } + } } } @@ -851,6 +921,7 @@ impl LanguageServer { LanguageServerId(0), stdin_writer, stdout_reader, + None::, None, Path::new("/"), None, @@ -862,6 +933,7 @@ impl LanguageServer { LanguageServerId(0), stdout_writer, stdin_reader, + None::, None, Path::new("/"), None, diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index 8239cf8690..8beaea5031 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -6,7 +6,6 @@ use crate::{ use anyhow::{anyhow, Context, Result}; use async_trait::async_trait; use client::proto::{self, PeerId}; -use fs::LineEnding; use futures::future; use gpui::{AppContext, AsyncAppContext, ModelHandle}; use language::{ @@ -17,8 +16,12 @@ use language::{ CodeAction, Completion, OffsetRangeExt, PointUtf16, ToOffset, ToPointUtf16, Transaction, Unclipped, }; -use lsp::{DocumentHighlightKind, LanguageServer, LanguageServerId, OneOf, ServerCapabilities}; +use lsp::{ + CompletionListItemDefaultsEditRange, DocumentHighlightKind, LanguageServer, LanguageServerId, + OneOf, ServerCapabilities, +}; use std::{cmp::Reverse, ops::Range, path::Path, sync::Arc}; +use text::LineEnding; pub fn lsp_formatting_options(tab_size: u32) -> lsp::FormattingOptions { lsp::FormattingOptions { @@ -1340,13 +1343,19 @@ impl LspCommand for GetCompletions { completions: Option, _: ModelHandle, buffer: ModelHandle, - _: LanguageServerId, + server_id: LanguageServerId, cx: AsyncAppContext, ) -> Result> { + let mut response_list = None; let completions = if let Some(completions) = completions { match completions { lsp::CompletionResponse::Array(completions) => completions, - lsp::CompletionResponse::List(list) => list.items, + + lsp::CompletionResponse::List(mut list) => { + let items = std::mem::take(&mut list.items); + response_list = Some(list); + items + } } } else { Default::default() @@ -1356,6 +1365,7 @@ impl LspCommand for GetCompletions { let language = buffer.language().cloned(); let snapshot = buffer.snapshot(); let clipped_position = buffer.clip_point_utf16(Unclipped(self.position), Bias::Left); + let mut range_for_token = None; completions .into_iter() @@ -1376,6 +1386,7 @@ impl LspCommand for GetCompletions { edit.new_text.clone(), ) } + // If the language server does not provide a range, then infer // the range based on the syntax tree. None => { @@ -1383,27 +1394,51 @@ impl LspCommand for GetCompletions { log::info!("completion out of expected range"); return None; } - let Range { start, end } = range_for_token - .get_or_insert_with(|| { - let offset = self.position.to_offset(&snapshot); - let (range, kind) = snapshot.surrounding_word(offset); - if kind == Some(CharKind::Word) { - range - } else { - offset..offset - } - }) - .clone(); + + let default_edit_range = response_list + .as_ref() + .and_then(|list| list.item_defaults.as_ref()) + .and_then(|defaults| defaults.edit_range.as_ref()) + .and_then(|range| match range { + CompletionListItemDefaultsEditRange::Range(r) => Some(r), + _ => None, + }); + + let range = if let Some(range) = default_edit_range { + let range = range_from_lsp(range.clone()); + let start = snapshot.clip_point_utf16(range.start, Bias::Left); + let end = snapshot.clip_point_utf16(range.end, Bias::Left); + if start != range.start.0 || end != range.end.0 { + log::info!("completion out of expected range"); + return None; + } + + snapshot.anchor_before(start)..snapshot.anchor_after(end) + } else { + range_for_token + .get_or_insert_with(|| { + let offset = self.position.to_offset(&snapshot); + let (range, kind) = snapshot.surrounding_word(offset); + let range = if kind == Some(CharKind::Word) { + range + } else { + offset..offset + }; + + snapshot.anchor_before(range.start) + ..snapshot.anchor_after(range.end) + }) + .clone() + }; + let text = lsp_completion .insert_text .as_ref() .unwrap_or(&lsp_completion.label) .clone(); - ( - snapshot.anchor_before(start)..snapshot.anchor_after(end), - text, - ) + (range, text) } + Some(lsp::CompletionTextEdit::InsertAndReplace(_)) => { log::info!("unsupported insert/replace completion"); return None; @@ -1427,6 +1462,7 @@ impl LspCommand for GetCompletions { lsp_completion.filter_text.as_deref(), ) }), + server_id, lsp_completion, } }) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index f839c8d5c5..5cd13b8be8 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -156,6 +156,11 @@ struct DelayedDebounced { cancel_channel: Option>, } +enum LanguageServerToQuery { + Primary, + Other(LanguageServerId), +} + impl DelayedDebounced { fn new() -> DelayedDebounced { DelayedDebounced { @@ -634,7 +639,7 @@ impl Project { cx.observe_global::(Self::on_settings_changed) ], _maintain_buffer_languages: Self::maintain_buffer_languages(languages.clone(), cx), - _maintain_workspace_config: Self::maintain_workspace_config(languages.clone(), cx), + _maintain_workspace_config: Self::maintain_workspace_config(cx), active_entry: None, languages, client, @@ -704,7 +709,7 @@ impl Project { collaborators: Default::default(), join_project_response_message_id: response.message_id, _maintain_buffer_languages: Self::maintain_buffer_languages(languages.clone(), cx), - _maintain_workspace_config: Self::maintain_workspace_config(languages.clone(), cx), + _maintain_workspace_config: Self::maintain_workspace_config(cx), languages, user_store: user_store.clone(), fs, @@ -2472,35 +2477,42 @@ impl Project { }) } - fn maintain_workspace_config( - languages: Arc, - cx: &mut ModelContext, - ) -> Task<()> { + fn maintain_workspace_config(cx: &mut ModelContext) -> Task<()> { let (mut settings_changed_tx, mut settings_changed_rx) = watch::channel(); let _ = postage::stream::Stream::try_recv(&mut settings_changed_rx); let settings_observation = cx.observe_global::(move |_, _| { *settings_changed_tx.borrow_mut() = (); }); + cx.spawn_weak(|this, mut cx| async move { while let Some(_) = settings_changed_rx.next().await { - let workspace_config = cx.update(|cx| languages.workspace_configuration(cx)).await; - if let Some(this) = this.upgrade(&cx) { - this.read_with(&cx, |this, _| { - for server_state in this.language_servers.values() { - if let LanguageServerState::Running { server, .. } = server_state { - server - .notify::( - lsp::DidChangeConfigurationParams { - settings: workspace_config.clone(), - }, - ) - .ok(); - } - } - }) - } else { + let Some(this) = this.upgrade(&cx) else { break; + }; + + let servers: Vec<_> = this.read_with(&cx, |this, _| { + this.language_servers + .values() + .filter_map(|state| match state { + LanguageServerState::Starting(_) => None, + LanguageServerState::Running { + adapter, server, .. + } => Some((adapter.clone(), server.clone())), + }) + .collect() + }); + + for (adapter, server) in servers { + let workspace_config = + cx.update(|cx| adapter.workspace_configuration(cx)).await; + server + .notify::( + lsp::DidChangeConfigurationParams { + settings: workspace_config.clone(), + }, + ) + .ok(); } } @@ -2615,7 +2627,6 @@ impl Project { let state = LanguageServerState::Starting({ let adapter = adapter.clone(); let server_name = adapter.name.0.clone(); - let languages = self.languages.clone(); let language = language.clone(); let key = key.clone(); @@ -2625,7 +2636,6 @@ impl Project { initialization_options, pending_server, adapter.clone(), - languages, language.clone(), server_id, key, @@ -2729,7 +2739,6 @@ impl Project { initialization_options: Option, pending_server: PendingLanguageServer, adapter: Arc, - languages: Arc, language: Arc, server_id: LanguageServerId, key: (WorktreeId, LanguageServerName), @@ -2740,7 +2749,6 @@ impl Project { initialization_options, pending_server, adapter.clone(), - languages, server_id, cx, ); @@ -2773,16 +2781,13 @@ impl Project { initialization_options: Option, pending_server: PendingLanguageServer, adapter: Arc, - languages: Arc, server_id: LanguageServerId, cx: &mut AsyncAppContext, ) -> Result>> { - let workspace_config = cx.update(|cx| languages.workspace_configuration(cx)).await; + let workspace_config = cx.update(|cx| adapter.workspace_configuration(cx)).await; let language_server = match pending_server.task.await? { - Some(server) => server.initialize(initialization_options).await?, - None => { - return Ok(None); - } + Some(server) => server, + None => return Ok(None), }; language_server @@ -2821,12 +2826,12 @@ impl Project { language_server .on_request::({ - let languages = languages.clone(); + let adapter = adapter.clone(); move |params, mut cx| { - let languages = languages.clone(); + let adapter = adapter.clone(); async move { let workspace_config = - cx.update(|cx| languages.workspace_configuration(cx)).await; + cx.update(|cx| adapter.workspace_configuration(cx)).await; Ok(params .items .into_iter() @@ -2932,6 +2937,8 @@ impl Project { }) .detach(); + let language_server = language_server.initialize(initialization_options).await?; + language_server .notify::( lsp::DidChangeConfigurationParams { @@ -3892,7 +3899,7 @@ impl Project { let file = File::from_dyn(buffer.file())?; let buffer_abs_path = file.as_local().map(|f| f.abs_path(cx)); let server = self - .primary_language_servers_for_buffer(buffer, cx) + .primary_language_server_for_buffer(buffer, cx) .map(|s| s.1.clone()); Some((buffer_handle, buffer_abs_path, server)) }) @@ -4197,7 +4204,12 @@ impl Project { cx: &mut ModelContext, ) -> Task>> { let position = position.to_point_utf16(buffer.read(cx)); - self.request_lsp(buffer.clone(), GetDefinition { position }, cx) + self.request_lsp( + buffer.clone(), + LanguageServerToQuery::Primary, + GetDefinition { position }, + cx, + ) } pub fn type_definition( @@ -4207,7 +4219,12 @@ impl Project { cx: &mut ModelContext, ) -> Task>> { let position = position.to_point_utf16(buffer.read(cx)); - self.request_lsp(buffer.clone(), GetTypeDefinition { position }, cx) + self.request_lsp( + buffer.clone(), + LanguageServerToQuery::Primary, + GetTypeDefinition { position }, + cx, + ) } pub fn references( @@ -4217,7 +4234,12 @@ impl Project { cx: &mut ModelContext, ) -> Task>> { let position = position.to_point_utf16(buffer.read(cx)); - self.request_lsp(buffer.clone(), GetReferences { position }, cx) + self.request_lsp( + buffer.clone(), + LanguageServerToQuery::Primary, + GetReferences { position }, + cx, + ) } pub fn document_highlights( @@ -4227,7 +4249,12 @@ impl Project { cx: &mut ModelContext, ) -> Task>> { let position = position.to_point_utf16(buffer.read(cx)); - self.request_lsp(buffer.clone(), GetDocumentHighlights { position }, cx) + self.request_lsp( + buffer.clone(), + LanguageServerToQuery::Primary, + GetDocumentHighlights { position }, + cx, + ) } pub fn symbols(&self, query: &str, cx: &mut ModelContext) -> Task>> { @@ -4455,17 +4482,66 @@ impl Project { cx: &mut ModelContext, ) -> Task>> { let position = position.to_point_utf16(buffer.read(cx)); - self.request_lsp(buffer.clone(), GetHover { position }, cx) + self.request_lsp( + buffer.clone(), + LanguageServerToQuery::Primary, + GetHover { position }, + cx, + ) } - pub fn completions( + pub fn completions( &self, buffer: &ModelHandle, position: T, cx: &mut ModelContext, ) -> Task>> { let position = position.to_point_utf16(buffer.read(cx)); - self.request_lsp(buffer.clone(), GetCompletions { position }, cx) + if self.is_local() { + let snapshot = buffer.read(cx).snapshot(); + let offset = position.to_offset(&snapshot); + let scope = snapshot.language_scope_at(offset); + + let server_ids: Vec<_> = self + .language_servers_for_buffer(buffer.read(cx), cx) + .filter(|(_, server)| server.capabilities().completion_provider.is_some()) + .filter(|(adapter, _)| { + scope + .as_ref() + .map(|scope| scope.language_allowed(&adapter.name)) + .unwrap_or(true) + }) + .map(|(_, server)| server.server_id()) + .collect(); + + let buffer = buffer.clone(); + cx.spawn(|this, mut cx| async move { + let mut tasks = Vec::with_capacity(server_ids.len()); + this.update(&mut cx, |this, cx| { + for server_id in server_ids { + tasks.push(this.request_lsp( + buffer.clone(), + LanguageServerToQuery::Other(server_id), + GetCompletions { position }, + cx, + )); + } + }); + + let mut completions = Vec::new(); + for task in tasks { + if let Ok(new_completions) = task.await { + completions.extend_from_slice(&new_completions); + } + } + + Ok(completions) + }) + } else if let Some(project_id) = self.remote_id() { + self.send_lsp_proto_request(buffer.clone(), project_id, GetCompletions { position }, cx) + } else { + Task::ready(Ok(Default::default())) + } } pub fn apply_additional_edits_for_completion( @@ -4479,7 +4555,8 @@ impl Project { let buffer_id = buffer.remote_id(); if self.is_local() { - let lang_server = match self.primary_language_servers_for_buffer(buffer, cx) { + let server_id = completion.server_id; + let lang_server = match self.language_server_for_buffer(buffer, server_id, cx) { Some((_, server)) => server.clone(), _ => return Task::ready(Ok(Default::default())), }; @@ -4586,7 +4663,12 @@ impl Project { ) -> Task>> { let buffer = buffer_handle.read(cx); let range = buffer.anchor_before(range.start)..buffer.anchor_before(range.end); - self.request_lsp(buffer_handle.clone(), GetCodeActions { range }, cx) + self.request_lsp( + buffer_handle.clone(), + LanguageServerToQuery::Primary, + GetCodeActions { range }, + cx, + ) } pub fn apply_code_action( @@ -4942,7 +5024,12 @@ impl Project { cx: &mut ModelContext, ) -> Task>>> { let position = position.to_point_utf16(buffer.read(cx)); - self.request_lsp(buffer, PrepareRename { position }, cx) + self.request_lsp( + buffer, + LanguageServerToQuery::Primary, + PrepareRename { position }, + cx, + ) } pub fn perform_rename( @@ -4956,6 +5043,7 @@ impl Project { let position = position.to_point_utf16(buffer.read(cx)); self.request_lsp( buffer, + LanguageServerToQuery::Primary, PerformRename { position, new_name, @@ -4983,6 +5071,7 @@ impl Project { }); self.request_lsp( buffer.clone(), + LanguageServerToQuery::Primary, OnTypeFormatting { position, trigger, @@ -5008,7 +5097,12 @@ impl Project { let lsp_request = InlayHints { range }; if self.is_local() { - let lsp_request_task = self.request_lsp(buffer_handle.clone(), lsp_request, cx); + let lsp_request_task = self.request_lsp( + buffer_handle.clone(), + LanguageServerToQuery::Primary, + lsp_request, + cx, + ); cx.spawn(|_, mut cx| async move { buffer_handle .update(&mut cx, |buffer, _| { @@ -5441,10 +5535,10 @@ impl Project { .await; } - // TODO: Wire this up to allow selecting a server? fn request_lsp( &self, buffer_handle: ModelHandle, + server: LanguageServerToQuery, request: R, cx: &mut ModelContext, ) -> Task> @@ -5453,11 +5547,19 @@ impl Project { { let buffer = buffer_handle.read(cx); if self.is_local() { + let language_server = match server { + LanguageServerToQuery::Primary => { + match self.primary_language_server_for_buffer(buffer, cx) { + Some((_, server)) => Some(Arc::clone(server)), + None => return Task::ready(Ok(Default::default())), + } + } + LanguageServerToQuery::Other(id) => self + .language_server_for_buffer(buffer, id, cx) + .map(|(_, server)| Arc::clone(server)), + }; let file = File::from_dyn(buffer.file()).and_then(File::as_local); - if let Some((file, language_server)) = file.zip( - self.primary_language_servers_for_buffer(buffer, cx) - .map(|(_, server)| server.clone()), - ) { + if let (Some(file), Some(language_server)) = (file, language_server) { let lsp_params = request.to_lsp(&file.abs_path(cx), buffer, &language_server, cx); return cx.spawn(|this, cx| async move { if !request.check_capabilities(language_server.capabilities()) { @@ -5490,31 +5592,40 @@ impl Project { }); } } else if let Some(project_id) = self.remote_id() { - let rpc = self.client.clone(); - let message = request.to_proto(project_id, buffer); - return cx.spawn_weak(|this, cx| async move { - // Ensure the project is still alive by the time the task - // is scheduled. - this.upgrade(&cx) - .ok_or_else(|| anyhow!("project dropped"))?; - - let response = rpc.request(message).await?; - - let this = this - .upgrade(&cx) - .ok_or_else(|| anyhow!("project dropped"))?; - if this.read_with(&cx, |this, _| this.is_read_only()) { - Err(anyhow!("disconnected before completing request")) - } else { - request - .response_from_proto(response, this, buffer_handle, cx) - .await - } - }); + return self.send_lsp_proto_request(buffer_handle, project_id, request, cx); } + Task::ready(Ok(Default::default())) } + fn send_lsp_proto_request( + &self, + buffer: ModelHandle, + project_id: u64, + request: R, + cx: &mut ModelContext<'_, Project>, + ) -> Task::Response>> { + let rpc = self.client.clone(); + let message = request.to_proto(project_id, buffer.read(cx)); + cx.spawn_weak(|this, cx| async move { + // Ensure the project is still alive by the time the task + // is scheduled. + this.upgrade(&cx) + .ok_or_else(|| anyhow!("project dropped"))?; + let response = rpc.request(message).await?; + let this = this + .upgrade(&cx) + .ok_or_else(|| anyhow!("project dropped"))?; + if this.read_with(&cx, |this, _| this.is_read_only()) { + Err(anyhow!("disconnected before completing request")) + } else { + request + .response_from_proto(response, this, buffer, cx) + .await + } + }) + } + fn sort_candidates_and_open_buffers( mut matching_paths_rx: Receiver, cx: &mut ModelContext, @@ -7150,7 +7261,7 @@ impl Project { let buffer_version = buffer_handle.read_with(&cx, |buffer, _| buffer.version()); let response = this .update(&mut cx, |this, cx| { - this.request_lsp(buffer_handle, request, cx) + this.request_lsp(buffer_handle, LanguageServerToQuery::Primary, request, cx) }) .await?; this.update(&mut cx, |this, cx| { @@ -7867,7 +7978,7 @@ impl Project { }) } - fn primary_language_servers_for_buffer( + fn primary_language_server_for_buffer( &self, buffer: &Buffer, cx: &AppContext, diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index 7c5983a0a9..b6adb371e1 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -1,11 +1,11 @@ use crate::{search::PathMatcher, worktree::WorktreeModelHandle, Event, *}; -use fs::{FakeFs, LineEnding, RealFs}; +use fs::{FakeFs, RealFs}; use futures::{future, StreamExt}; use gpui::{executor::Deterministic, test::subscribe, AppContext}; use language::{ language_settings::{AllLanguageSettings, LanguageSettingsContent}, tree_sitter_rust, tree_sitter_typescript, Diagnostic, FakeLspAdapter, LanguageConfig, - OffsetRangeExt, Point, ToPoint, + LineEnding, OffsetRangeExt, Point, ToPoint, }; use lsp::Url; use parking_lot::Mutex; @@ -2272,7 +2272,18 @@ async fn test_completions_without_edit_ranges(cx: &mut gpui::TestAppContext) { }, Some(tree_sitter_typescript::language_typescript()), ); - let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await; + let mut fake_language_servers = language + .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + capabilities: lsp::ServerCapabilities { + completion_provider: Some(lsp::CompletionOptions { + trigger_characters: Some(vec![":".to_string()]), + ..Default::default() + }), + ..Default::default() + }, + ..Default::default() + })) + .await; let fs = FakeFs::new(cx.background()); fs.insert_tree( @@ -2358,7 +2369,18 @@ async fn test_completions_with_carriage_returns(cx: &mut gpui::TestAppContext) { }, Some(tree_sitter_typescript::language_typescript()), ); - let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await; + let mut fake_language_servers = language + .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + capabilities: lsp::ServerCapabilities { + completion_provider: Some(lsp::CompletionOptions { + trigger_characters: Some(vec![":".to_string()]), + ..Default::default() + }), + ..Default::default() + }, + ..Default::default() + })) + .await; let fs = FakeFs::new(cx.background()); fs.insert_tree( diff --git a/crates/project/src/search.rs b/crates/project/src/search.rs index a3c6583052..6c53d2e934 100644 --- a/crates/project/src/search.rs +++ b/crates/project/src/search.rs @@ -225,15 +225,14 @@ impl SearchQuery { if self.as_str().is_empty() { return Default::default(); } - let language = buffer.language_at(0); + + let range_offset = subrange.as_ref().map(|r| r.start).unwrap_or(0); let rope = if let Some(range) = subrange { buffer.as_rope().slice(range) } else { buffer.as_rope().clone() }; - let kind = |c| char_kind(language, c); - let mut matches = Vec::new(); match self { Self::Text { @@ -249,6 +248,9 @@ impl SearchQuery { let mat = mat.unwrap(); if *whole_word { + let scope = buffer.language_scope_at(range_offset + mat.start()); + let kind = |c| char_kind(&scope, c); + let prev_kind = rope.reversed_chars_at(mat.start()).next().map(kind); let start_kind = kind(rope.chars_at(mat.start()).next().unwrap()); let end_kind = kind(rope.reversed_chars_at(mat.end()).next().unwrap()); diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index e6e0f37cc7..2de3671033 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -8,7 +8,7 @@ use clock::ReplicaId; use collections::{HashMap, HashSet, VecDeque}; use fs::{ repository::{GitFileStatus, GitRepository, RepoPath}, - Fs, LineEnding, + Fs, }; use futures::{ channel::{ @@ -27,7 +27,7 @@ use language::{ deserialize_fingerprint, deserialize_version, serialize_fingerprint, serialize_line_ending, serialize_version, }, - Buffer, DiagnosticEntry, File as _, PointUtf16, Rope, RopeFingerprint, Unclipped, + Buffer, DiagnosticEntry, File as _, LineEnding, PointUtf16, Rope, RopeFingerprint, Unclipped, }; use lsp::LanguageServerId; use parking_lot::Mutex; diff --git a/crates/quick_action_bar/Cargo.toml b/crates/quick_action_bar/Cargo.toml index 6953ac0e02..1f8ec4e92b 100644 --- a/crates/quick_action_bar/Cargo.toml +++ b/crates/quick_action_bar/Cargo.toml @@ -9,6 +9,7 @@ path = "src/quick_action_bar.rs" doctest = false [dependencies] +ai = { path = "../ai" } editor = { path = "../editor" } gpui = { path = "../gpui" } search = { path = "../search" } diff --git a/crates/quick_action_bar/src/quick_action_bar.rs b/crates/quick_action_bar/src/quick_action_bar.rs index 8595645e59..b3d9784f1f 100644 --- a/crates/quick_action_bar/src/quick_action_bar.rs +++ b/crates/quick_action_bar/src/quick_action_bar.rs @@ -1,25 +1,29 @@ +use ai::{assistant::InlineAssist, AssistantPanel}; use editor::Editor; use gpui::{ elements::{Empty, Flex, MouseEventHandler, ParentElement, Svg}, platform::{CursorStyle, MouseButton}, Action, AnyElement, Element, Entity, EventContext, Subscription, View, ViewContext, ViewHandle, + WeakViewHandle, }; use search::{buffer_search, BufferSearchBar}; -use workspace::{item::ItemHandle, ToolbarItemLocation, ToolbarItemView}; +use workspace::{item::ItemHandle, ToolbarItemLocation, ToolbarItemView, Workspace}; pub struct QuickActionBar { buffer_search_bar: ViewHandle, active_item: Option>, _inlay_hints_enabled_subscription: Option, + workspace: WeakViewHandle, } impl QuickActionBar { - pub fn new(buffer_search_bar: ViewHandle) -> Self { + pub fn new(buffer_search_bar: ViewHandle, workspace: &Workspace) -> Self { Self { buffer_search_bar, active_item: None, _inlay_hints_enabled_subscription: None, + workspace: workspace.weak_handle(), } } @@ -88,6 +92,21 @@ impl View for QuickActionBar { )); } + bar.add_child(render_quick_action_bar_button( + 2, + "icons/radix/magic-wand.svg", + false, + ("Inline Assist".into(), Some(Box::new(InlineAssist))), + cx, + move |this, cx| { + if let Some(workspace) = this.workspace.upgrade(cx) { + workspace.update(cx, |workspace, cx| { + AssistantPanel::inline_assist(workspace, &Default::default(), cx); + }); + } + }, + )); + bar.into_any() } } diff --git a/crates/rope/src/rope.rs b/crates/rope/src/rope.rs index 2bfb090bb2..9c764c468e 100644 --- a/crates/rope/src/rope.rs +++ b/crates/rope/src/rope.rs @@ -384,6 +384,16 @@ impl<'a> From<&'a str> for Rope { } } +impl<'a> FromIterator<&'a str> for Rope { + fn from_iter>(iter: T) -> Self { + let mut rope = Rope::new(); + for chunk in iter { + rope.push(chunk); + } + rope + } +} + impl From for Rope { fn from(text: String) -> Self { Rope::from(text.as_str()) diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 94d6075ecf..92c85677f6 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -657,7 +657,8 @@ message Completion { Anchor old_start = 1; Anchor old_end = 2; string new_text = 3; - bytes lsp_completion = 4; + uint64 server_id = 4; + bytes lsp_completion = 5; } message GetCodeActions { @@ -860,12 +861,12 @@ message ProjectTransaction { } message Transaction { - LocalTimestamp id = 1; - repeated LocalTimestamp edit_ids = 2; + LamportTimestamp id = 1; + repeated LamportTimestamp edit_ids = 2; repeated VectorClockEntry start = 3; } -message LocalTimestamp { +message LamportTimestamp { uint32 replica_id = 1; uint32 value = 2; } @@ -1279,7 +1280,7 @@ message Excerpt { message Anchor { uint32 replica_id = 1; - uint32 local_timestamp = 2; + uint32 timestamp = 2; uint64 offset = 3; Bias bias = 4; optional uint64 buffer_id = 5; @@ -1323,19 +1324,17 @@ message Operation { message Edit { uint32 replica_id = 1; - uint32 local_timestamp = 2; - uint32 lamport_timestamp = 3; - repeated VectorClockEntry version = 4; - repeated Range ranges = 5; - repeated string new_text = 6; + uint32 lamport_timestamp = 2; + repeated VectorClockEntry version = 3; + repeated Range ranges = 4; + repeated string new_text = 5; } message Undo { uint32 replica_id = 1; - uint32 local_timestamp = 2; - uint32 lamport_timestamp = 3; - repeated VectorClockEntry version = 4; - repeated UndoCount counts = 5; + uint32 lamport_timestamp = 2; + repeated VectorClockEntry version = 3; + repeated UndoCount counts = 4; } message UpdateSelections { @@ -1361,7 +1360,7 @@ message UndoMapEntry { message UndoCount { uint32 replica_id = 1; - uint32 local_timestamp = 2; + uint32 lamport_timestamp = 2; uint32 count = 3; } diff --git a/crates/rpc/src/rpc.rs b/crates/rpc/src/rpc.rs index bc9dd6f80b..d64cbae929 100644 --- a/crates/rpc/src/rpc.rs +++ b/crates/rpc/src/rpc.rs @@ -6,4 +6,4 @@ pub use conn::Connection; pub use peer::*; mod macros; -pub const PROTOCOL_VERSION: u32 = 61; +pub const PROTOCOL_VERSION: u32 = 62; diff --git a/crates/text/Cargo.toml b/crates/text/Cargo.toml index adec2d4fd0..d1bc6cc8f8 100644 --- a/crates/text/Cargo.toml +++ b/crates/text/Cargo.toml @@ -14,7 +14,6 @@ test-support = ["rand"] [dependencies] clock = { path = "../clock" } collections = { path = "../collections" } -fs = { path = "../fs" } rope = { path = "../rope" } sum_tree = { path = "../sum_tree" } util = { path = "../util" } @@ -32,6 +31,7 @@ regex.workspace = true [dev-dependencies] collections = { path = "../collections", features = ["test-support"] } gpui = { path = "../gpui", features = ["test-support"] } +util = { path = "../util", features = ["test-support"] } ctor.workspace = true env_logger.workspace = true rand.workspace = true diff --git a/crates/text/src/anchor.rs b/crates/text/src/anchor.rs index b5f4fb24ec..084be0e336 100644 --- a/crates/text/src/anchor.rs +++ b/crates/text/src/anchor.rs @@ -8,7 +8,7 @@ use sum_tree::Bias; #[derive(Copy, Clone, Eq, PartialEq, Debug, Hash, Default)] pub struct Anchor { - pub timestamp: clock::Local, + pub timestamp: clock::Lamport, pub offset: usize, pub bias: Bias, pub buffer_id: Option, @@ -16,14 +16,14 @@ pub struct Anchor { impl Anchor { pub const MIN: Self = Self { - timestamp: clock::Local::MIN, + timestamp: clock::Lamport::MIN, offset: usize::MIN, bias: Bias::Left, buffer_id: None, }; pub const MAX: Self = Self { - timestamp: clock::Local::MAX, + timestamp: clock::Lamport::MAX, offset: usize::MAX, bias: Bias::Right, buffer_id: None, diff --git a/crates/text/src/text.rs b/crates/text/src/text.rs index 4a97faf015..c05ea1109c 100644 --- a/crates/text/src/text.rs +++ b/crates/text/src/text.rs @@ -14,16 +14,17 @@ pub use anchor::*; use anyhow::{anyhow, Result}; pub use clock::ReplicaId; use collections::{HashMap, HashSet}; -use fs::LineEnding; use locator::Locator; use operation_queue::OperationQueue; pub use patch::Patch; use postage::{oneshot, prelude::*}; +use lazy_static::lazy_static; +use regex::Regex; pub use rope::*; pub use selection::*; - use std::{ + borrow::Cow, cmp::{self, Ordering, Reverse}, future::Future, iter::Iterator, @@ -36,22 +37,25 @@ pub use subscription::*; pub use sum_tree::Bias; use sum_tree::{FilterCursor, SumTree, TreeMap}; use undo_map::UndoMap; +use util::ResultExt; #[cfg(any(test, feature = "test-support"))] use util::RandomCharIter; -pub type TransactionId = clock::Local; +lazy_static! { + static ref LINE_SEPARATORS_REGEX: Regex = Regex::new("\r\n|\r|\u{2028}|\u{2029}").unwrap(); +} + +pub type TransactionId = clock::Lamport; pub struct Buffer { snapshot: BufferSnapshot, history: History, deferred_ops: OperationQueue, deferred_replicas: HashSet, - replica_id: ReplicaId, - local_clock: clock::Local, pub lamport_clock: clock::Lamport, subscriptions: Topic, - edit_id_resolvers: HashMap>>, + edit_id_resolvers: HashMap>>, wait_for_version_txs: Vec<(clock::Global, oneshot::Sender<()>)>, } @@ -79,7 +83,7 @@ pub struct HistoryEntry { #[derive(Clone, Debug)] pub struct Transaction { pub id: TransactionId, - pub edit_ids: Vec, + pub edit_ids: Vec, pub start: clock::Global, } @@ -91,8 +95,8 @@ impl HistoryEntry { struct History { base_text: Rope, - operations: TreeMap, - insertion_slices: HashMap>, + operations: TreeMap, + insertion_slices: HashMap>, undo_stack: Vec, redo_stack: Vec, transaction_depth: usize, @@ -101,7 +105,7 @@ struct History { #[derive(Clone, Debug)] struct InsertionSlice { - insertion_id: clock::Local, + insertion_id: clock::Lamport, range: Range, } @@ -123,18 +127,18 @@ impl History { } fn push(&mut self, op: Operation) { - self.operations.insert(op.local_timestamp(), op); + self.operations.insert(op.timestamp(), op); } fn start_transaction( &mut self, start: clock::Global, now: Instant, - local_clock: &mut clock::Local, + clock: &mut clock::Lamport, ) -> Option { self.transaction_depth += 1; if self.transaction_depth == 1 { - let id = local_clock.tick(); + let id = clock.tick(); self.undo_stack.push(HistoryEntry { transaction: Transaction { id, @@ -245,7 +249,7 @@ impl History { self.redo_stack.clear(); } - fn push_undo(&mut self, op_id: clock::Local) { + fn push_undo(&mut self, op_id: clock::Lamport) { assert_ne!(self.transaction_depth, 0); if let Some(Operation::Edit(_)) = self.operations.get(&op_id) { let last_transaction = self.undo_stack.last_mut().unwrap(); @@ -263,7 +267,19 @@ impl History { } } - fn remove_from_undo(&mut self, transaction_id: TransactionId) -> &[HistoryEntry] { + fn remove_from_undo(&mut self, transaction_id: TransactionId) -> Option<&HistoryEntry> { + assert_eq!(self.transaction_depth, 0); + + let entry_ix = self + .undo_stack + .iter() + .rposition(|entry| entry.transaction.id == transaction_id)?; + let entry = self.undo_stack.remove(entry_ix); + self.redo_stack.push(entry); + self.redo_stack.last() + } + + fn remove_from_undo_until(&mut self, transaction_id: TransactionId) -> &[HistoryEntry] { assert_eq!(self.transaction_depth, 0); let redo_stack_start_len = self.redo_stack.len(); @@ -278,20 +294,43 @@ impl History { &self.redo_stack[redo_stack_start_len..] } - fn forget(&mut self, transaction_id: TransactionId) { + fn forget(&mut self, transaction_id: TransactionId) -> Option { assert_eq!(self.transaction_depth, 0); if let Some(entry_ix) = self .undo_stack .iter() .rposition(|entry| entry.transaction.id == transaction_id) { - self.undo_stack.remove(entry_ix); + Some(self.undo_stack.remove(entry_ix).transaction) } else if let Some(entry_ix) = self .redo_stack .iter() .rposition(|entry| entry.transaction.id == transaction_id) { - self.undo_stack.remove(entry_ix); + Some(self.redo_stack.remove(entry_ix).transaction) + } else { + None + } + } + + fn transaction_mut(&mut self, transaction_id: TransactionId) -> Option<&mut Transaction> { + let entry = self + .undo_stack + .iter_mut() + .rfind(|entry| entry.transaction.id == transaction_id) + .or_else(|| { + self.redo_stack + .iter_mut() + .rfind(|entry| entry.transaction.id == transaction_id) + })?; + Some(&mut entry.transaction) + } + + fn merge_transactions(&mut self, transaction: TransactionId, destination: TransactionId) { + if let Some(transaction) = self.forget(transaction) { + if let Some(destination) = self.transaction_mut(destination) { + destination.edit_ids.extend(transaction.edit_ids); + } } } @@ -371,37 +410,14 @@ impl Edit<(D1, D2)> { } } -#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, PartialOrd, Ord)] -pub struct InsertionTimestamp { - pub replica_id: ReplicaId, - pub local: clock::Seq, - pub lamport: clock::Seq, -} - -impl InsertionTimestamp { - pub fn local(&self) -> clock::Local { - clock::Local { - replica_id: self.replica_id, - value: self.local, - } - } - - pub fn lamport(&self) -> clock::Lamport { - clock::Lamport { - replica_id: self.replica_id, - value: self.lamport, - } - } -} - #[derive(Eq, PartialEq, Clone, Debug)] pub struct Fragment { pub id: Locator, - pub insertion_timestamp: InsertionTimestamp, + pub timestamp: clock::Lamport, pub insertion_offset: usize, pub len: usize, pub visible: bool, - pub deletions: HashSet, + pub deletions: HashSet, pub max_undos: clock::Global, } @@ -429,29 +445,26 @@ impl<'a> sum_tree::Dimension<'a, FragmentSummary> for FragmentTextSummary { #[derive(Eq, PartialEq, Clone, Debug)] struct InsertionFragment { - timestamp: clock::Local, + timestamp: clock::Lamport, split_offset: usize, fragment_id: Locator, } #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] struct InsertionFragmentKey { - timestamp: clock::Local, + timestamp: clock::Lamport, split_offset: usize, } #[derive(Clone, Debug, Eq, PartialEq)] pub enum Operation { Edit(EditOperation), - Undo { - undo: UndoOperation, - lamport_timestamp: clock::Lamport, - }, + Undo(UndoOperation), } #[derive(Clone, Debug, Eq, PartialEq)] pub struct EditOperation { - pub timestamp: InsertionTimestamp, + pub timestamp: clock::Lamport, pub version: clock::Global, pub ranges: Vec>, pub new_text: Vec>, @@ -459,9 +472,9 @@ pub struct EditOperation { #[derive(Clone, Debug, Eq, PartialEq)] pub struct UndoOperation { - pub id: clock::Local, - pub counts: HashMap, + pub timestamp: clock::Lamport, pub version: clock::Global, + pub counts: HashMap, } impl Buffer { @@ -473,24 +486,21 @@ impl Buffer { let mut fragments = SumTree::new(); let mut insertions = SumTree::new(); - let mut local_clock = clock::Local::new(replica_id); let mut lamport_clock = clock::Lamport::new(replica_id); let mut version = clock::Global::new(); let visible_text = history.base_text.clone(); if !visible_text.is_empty() { - let insertion_timestamp = InsertionTimestamp { + let insertion_timestamp = clock::Lamport { replica_id: 0, - local: 1, - lamport: 1, + value: 1, }; - local_clock.observe(insertion_timestamp.local()); - lamport_clock.observe(insertion_timestamp.lamport()); - version.observe(insertion_timestamp.local()); + lamport_clock.observe(insertion_timestamp); + version.observe(insertion_timestamp); let fragment_id = Locator::between(&Locator::min(), &Locator::max()); let fragment = Fragment { id: fragment_id, - insertion_timestamp, + timestamp: insertion_timestamp, insertion_offset: 0, len: visible_text.len(), visible: true, @@ -516,8 +526,6 @@ impl Buffer { history, deferred_ops: OperationQueue::new(), deferred_replicas: HashSet::default(), - replica_id, - local_clock, lamport_clock, subscriptions: Default::default(), edit_id_resolvers: Default::default(), @@ -534,7 +542,7 @@ impl Buffer { } pub fn replica_id(&self) -> ReplicaId { - self.local_clock.replica_id + self.lamport_clock.replica_id } pub fn remote_id(&self) -> u64 { @@ -561,16 +569,12 @@ impl Buffer { .map(|(range, new_text)| (range, new_text.into())); self.start_transaction(); - let timestamp = InsertionTimestamp { - replica_id: self.replica_id, - local: self.local_clock.tick().value, - lamport: self.lamport_clock.tick().value, - }; + let timestamp = self.lamport_clock.tick(); let operation = Operation::Edit(self.apply_local_edit(edits, timestamp)); self.history.push(operation.clone()); - self.history.push_undo(operation.local_timestamp()); - self.snapshot.version.observe(operation.local_timestamp()); + self.history.push_undo(operation.timestamp()); + self.snapshot.version.observe(operation.timestamp()); self.end_transaction(); operation } @@ -578,7 +582,7 @@ impl Buffer { fn apply_local_edit>>( &mut self, edits: impl ExactSizeIterator, T)>, - timestamp: InsertionTimestamp, + timestamp: clock::Lamport, ) -> EditOperation { let mut edits_patch = Patch::default(); let mut edit_op = EditOperation { @@ -655,7 +659,7 @@ impl Buffer { .item() .map_or(&Locator::max(), |old_fragment| &old_fragment.id), ), - insertion_timestamp: timestamp, + timestamp, insertion_offset, len: new_text.len(), deletions: Default::default(), @@ -685,7 +689,7 @@ impl Buffer { intersection.insertion_offset += fragment_start - old_fragments.start().visible; intersection.id = Locator::between(&new_fragments.summary().max_id, &intersection.id); - intersection.deletions.insert(timestamp.local()); + intersection.deletions.insert(timestamp); intersection.visible = false; } if intersection.len > 0 { @@ -740,7 +744,7 @@ impl Buffer { self.subscriptions.publish_mut(&edits_patch); self.history .insertion_slices - .insert(timestamp.local(), insertion_slices); + .insert(timestamp, insertion_slices); edit_op } @@ -767,28 +771,23 @@ impl Buffer { fn apply_op(&mut self, op: Operation) -> Result<()> { match op { Operation::Edit(edit) => { - if !self.version.observed(edit.timestamp.local()) { + if !self.version.observed(edit.timestamp) { self.apply_remote_edit( &edit.version, &edit.ranges, &edit.new_text, edit.timestamp, ); - self.snapshot.version.observe(edit.timestamp.local()); - self.local_clock.observe(edit.timestamp.local()); - self.lamport_clock.observe(edit.timestamp.lamport()); - self.resolve_edit(edit.timestamp.local()); + self.snapshot.version.observe(edit.timestamp); + self.lamport_clock.observe(edit.timestamp); + self.resolve_edit(edit.timestamp); } } - Operation::Undo { - undo, - lamport_timestamp, - } => { - if !self.version.observed(undo.id) { + Operation::Undo(undo) => { + if !self.version.observed(undo.timestamp) { self.apply_undo(&undo)?; - self.snapshot.version.observe(undo.id); - self.local_clock.observe(undo.id); - self.lamport_clock.observe(lamport_timestamp); + self.snapshot.version.observe(undo.timestamp); + self.lamport_clock.observe(undo.timestamp); } } } @@ -808,7 +807,7 @@ impl Buffer { version: &clock::Global, ranges: &[Range], new_text: &[Arc], - timestamp: InsertionTimestamp, + timestamp: clock::Lamport, ) { if ranges.is_empty() { return; @@ -875,9 +874,7 @@ impl Buffer { // Skip over insertions that are concurrent to this edit, but have a lower lamport // timestamp. while let Some(fragment) = old_fragments.item() { - if fragment_start == range.start - && fragment.insertion_timestamp.lamport() > timestamp.lamport() - { + if fragment_start == range.start && fragment.timestamp > timestamp { new_ropes.push_fragment(fragment, fragment.visible); new_fragments.push(fragment.clone(), &None); old_fragments.next(&cx); @@ -914,7 +911,7 @@ impl Buffer { .item() .map_or(&Locator::max(), |old_fragment| &old_fragment.id), ), - insertion_timestamp: timestamp, + timestamp, insertion_offset, len: new_text.len(), deletions: Default::default(), @@ -945,7 +942,7 @@ impl Buffer { fragment_start - old_fragments.start().0.full_offset(); intersection.id = Locator::between(&new_fragments.summary().max_id, &intersection.id); - intersection.deletions.insert(timestamp.local()); + intersection.deletions.insert(timestamp); intersection.visible = false; insertion_slices.push(intersection.insertion_slice()); } @@ -997,13 +994,13 @@ impl Buffer { self.snapshot.insertions.edit(new_insertions, &()); self.history .insertion_slices - .insert(timestamp.local(), insertion_slices); + .insert(timestamp, insertion_slices); self.subscriptions.publish_mut(&edits_patch) } fn fragment_ids_for_edits<'a>( &'a self, - edit_ids: impl Iterator, + edit_ids: impl Iterator, ) -> Vec<&'a Locator> { // Get all of the insertion slices changed by the given edits. let mut insertion_slices = Vec::new(); @@ -1064,7 +1061,7 @@ impl Buffer { let fragment_was_visible = fragment.visible; fragment.visible = fragment.is_visible(&self.undo_map); - fragment.max_undos.observe(undo.id); + fragment.max_undos.observe(undo.timestamp); let old_start = old_fragments.start().1; let new_start = new_fragments.summary().text.visible; @@ -1118,10 +1115,10 @@ impl Buffer { if self.deferred_replicas.contains(&op.replica_id()) { false } else { - match op { - Operation::Edit(edit) => self.version.observed_all(&edit.version), - Operation::Undo { undo, .. } => self.version.observed_all(&undo.version), - } + self.version.observed_all(match op { + Operation::Edit(edit) => &edit.version, + Operation::Undo(undo) => &undo.version, + }) } } @@ -1139,7 +1136,7 @@ impl Buffer { pub fn start_transaction_at(&mut self, now: Instant) -> Option { self.history - .start_transaction(self.version.clone(), now, &mut self.local_clock) + .start_transaction(self.version.clone(), now, &mut self.lamport_clock) } pub fn end_transaction(&mut self) -> Option<(TransactionId, clock::Global)> { @@ -1168,7 +1165,7 @@ impl Buffer { &self.history.base_text } - pub fn operations(&self) -> &TreeMap { + pub fn operations(&self) -> &TreeMap { &self.history.operations } @@ -1183,11 +1180,20 @@ impl Buffer { } } + pub fn undo_transaction(&mut self, transaction_id: TransactionId) -> Option { + let transaction = self + .history + .remove_from_undo(transaction_id)? + .transaction + .clone(); + self.undo_or_redo(transaction).log_err() + } + #[allow(clippy::needless_collect)] pub fn undo_to_transaction(&mut self, transaction_id: TransactionId) -> Vec { let transactions = self .history - .remove_from_undo(transaction_id) + .remove_from_undo_until(transaction_id) .iter() .map(|entry| entry.transaction.clone()) .collect::>(); @@ -1202,6 +1208,10 @@ impl Buffer { self.history.forget(transaction_id); } + pub fn merge_transactions(&mut self, transaction: TransactionId, destination: TransactionId) { + self.history.merge_transactions(transaction, destination); + } + pub fn redo(&mut self) -> Option<(TransactionId, Operation)> { if let Some(entry) = self.history.pop_redo() { let transaction = entry.transaction.clone(); @@ -1235,16 +1245,13 @@ impl Buffer { } let undo = UndoOperation { - id: self.local_clock.tick(), + timestamp: self.lamport_clock.tick(), version: self.version(), counts, }; self.apply_undo(&undo)?; - let operation = Operation::Undo { - undo, - lamport_timestamp: self.lamport_clock.tick(), - }; - self.snapshot.version.observe(operation.local_timestamp()); + self.snapshot.version.observe(undo.timestamp); + let operation = Operation::Undo(undo); self.history.push(operation.clone()); Ok(operation) } @@ -1309,7 +1316,7 @@ impl Buffer { pub fn wait_for_edits( &mut self, - edit_ids: impl IntoIterator, + edit_ids: impl IntoIterator, ) -> impl 'static + Future> { let mut futures = Vec::new(); for edit_id in edit_ids { @@ -1381,7 +1388,7 @@ impl Buffer { self.wait_for_version_txs.clear(); } - fn resolve_edit(&mut self, edit_id: clock::Local) { + fn resolve_edit(&mut self, edit_id: clock::Lamport) { for mut tx in self .edit_id_resolvers .remove(&edit_id) @@ -1459,7 +1466,7 @@ impl Buffer { .insertions .get( &InsertionFragmentKey { - timestamp: fragment.insertion_timestamp.local(), + timestamp: fragment.timestamp, split_offset: fragment.insertion_offset, }, &(), @@ -1942,7 +1949,7 @@ impl BufferSnapshot { let fragment = fragment_cursor.item().unwrap(); let overshoot = offset - *fragment_cursor.start(); Anchor { - timestamp: fragment.insertion_timestamp.local(), + timestamp: fragment.timestamp, offset: fragment.insertion_offset + overshoot, bias, buffer_id: Some(self.remote_id), @@ -2134,15 +2141,14 @@ impl<'a, D: TextDimension + Ord, F: FnMut(&FragmentSummary) -> bool> Iterator fo break; } - let timestamp = fragment.insertion_timestamp.local(); let start_anchor = Anchor { - timestamp, + timestamp: fragment.timestamp, offset: fragment.insertion_offset, bias: Bias::Right, buffer_id: Some(self.buffer_id), }; let end_anchor = Anchor { - timestamp, + timestamp: fragment.timestamp, offset: fragment.insertion_offset + fragment.len, bias: Bias::Left, buffer_id: Some(self.buffer_id), @@ -2215,19 +2221,17 @@ impl<'a, D: TextDimension + Ord, F: FnMut(&FragmentSummary) -> bool> Iterator fo impl Fragment { fn insertion_slice(&self) -> InsertionSlice { InsertionSlice { - insertion_id: self.insertion_timestamp.local(), + insertion_id: self.timestamp, range: self.insertion_offset..self.insertion_offset + self.len, } } fn is_visible(&self, undos: &UndoMap) -> bool { - !undos.is_undone(self.insertion_timestamp.local()) - && self.deletions.iter().all(|d| undos.is_undone(*d)) + !undos.is_undone(self.timestamp) && self.deletions.iter().all(|d| undos.is_undone(*d)) } fn was_visible(&self, version: &clock::Global, undos: &UndoMap) -> bool { - (version.observed(self.insertion_timestamp.local()) - && !undos.was_undone(self.insertion_timestamp.local(), version)) + (version.observed(self.timestamp) && !undos.was_undone(self.timestamp, version)) && self .deletions .iter() @@ -2240,14 +2244,14 @@ impl sum_tree::Item for Fragment { fn summary(&self) -> Self::Summary { let mut max_version = clock::Global::new(); - max_version.observe(self.insertion_timestamp.local()); + max_version.observe(self.timestamp); for deletion in &self.deletions { max_version.observe(*deletion); } max_version.join(&self.max_undos); let mut min_insertion_version = clock::Global::new(); - min_insertion_version.observe(self.insertion_timestamp.local()); + min_insertion_version.observe(self.timestamp); let max_insertion_version = min_insertion_version.clone(); if self.visible { FragmentSummary { @@ -2324,7 +2328,7 @@ impl sum_tree::KeyedItem for InsertionFragment { impl InsertionFragment { fn new(fragment: &Fragment) -> Self { Self { - timestamp: fragment.insertion_timestamp.local(), + timestamp: fragment.timestamp, split_offset: fragment.insertion_offset, fragment_id: fragment.id.clone(), } @@ -2447,10 +2451,10 @@ impl Operation { operation_queue::Operation::lamport_timestamp(self).replica_id } - pub fn local_timestamp(&self) -> clock::Local { + pub fn timestamp(&self) -> clock::Lamport { match self { - Operation::Edit(edit) => edit.timestamp.local(), - Operation::Undo { undo, .. } => undo.id, + Operation::Edit(edit) => edit.timestamp, + Operation::Undo(undo) => undo.timestamp, } } @@ -2469,10 +2473,8 @@ impl Operation { impl operation_queue::Operation for Operation { fn lamport_timestamp(&self) -> clock::Lamport { match self { - Operation::Edit(edit) => edit.timestamp.lamport(), - Operation::Undo { - lamport_timestamp, .. - } => *lamport_timestamp, + Operation::Edit(edit) => edit.timestamp, + Operation::Undo(undo) => undo.timestamp, } } } @@ -2622,3 +2624,59 @@ impl FromAnchor for usize { snapshot.summary_for_anchor(anchor) } } + +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum LineEnding { + Unix, + Windows, +} + +impl Default for LineEnding { + fn default() -> Self { + #[cfg(unix)] + return Self::Unix; + + #[cfg(not(unix))] + return Self::CRLF; + } +} + +impl LineEnding { + pub fn as_str(&self) -> &'static str { + match self { + LineEnding::Unix => "\n", + LineEnding::Windows => "\r\n", + } + } + + pub fn detect(text: &str) -> Self { + let mut max_ix = cmp::min(text.len(), 1000); + while !text.is_char_boundary(max_ix) { + max_ix -= 1; + } + + if let Some(ix) = text[..max_ix].find(&['\n']) { + if ix > 0 && text.as_bytes()[ix - 1] == b'\r' { + Self::Windows + } else { + Self::Unix + } + } else { + Self::default() + } + } + + pub fn normalize(text: &mut String) { + if let Cow::Owned(replaced) = LINE_SEPARATORS_REGEX.replace_all(text, "\n") { + *text = replaced; + } + } + + pub fn normalize_arc(text: Arc) -> Arc { + if let Cow::Owned(replaced) = LINE_SEPARATORS_REGEX.replace_all(&text, "\n") { + replaced.into() + } else { + text + } + } +} diff --git a/crates/text/src/undo_map.rs b/crates/text/src/undo_map.rs index ff1b241e73..f95809c02e 100644 --- a/crates/text/src/undo_map.rs +++ b/crates/text/src/undo_map.rs @@ -26,8 +26,8 @@ impl sum_tree::KeyedItem for UndoMapEntry { #[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] struct UndoMapKey { - edit_id: clock::Local, - undo_id: clock::Local, + edit_id: clock::Lamport, + undo_id: clock::Lamport, } impl sum_tree::Summary for UndoMapKey { @@ -50,7 +50,7 @@ impl UndoMap { sum_tree::Edit::Insert(UndoMapEntry { key: UndoMapKey { edit_id: *edit_id, - undo_id: undo.id, + undo_id: undo.timestamp, }, undo_count: *count, }) @@ -59,11 +59,11 @@ impl UndoMap { self.0.edit(edits, &()); } - pub fn is_undone(&self, edit_id: clock::Local) -> bool { + pub fn is_undone(&self, edit_id: clock::Lamport) -> bool { self.undo_count(edit_id) % 2 == 1 } - pub fn was_undone(&self, edit_id: clock::Local, version: &clock::Global) -> bool { + pub fn was_undone(&self, edit_id: clock::Lamport, version: &clock::Global) -> bool { let mut cursor = self.0.cursor::(); cursor.seek( &UndoMapKey { @@ -88,7 +88,7 @@ impl UndoMap { undo_count % 2 == 1 } - pub fn undo_count(&self, edit_id: clock::Local) -> u32 { + pub fn undo_count(&self, edit_id: clock::Lamport) -> u32 { let mut cursor = self.0.cursor::(); cursor.seek( &UndoMapKey { diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index a5faba8eaf..a51f18c4db 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -88,8 +88,6 @@ pub struct Workspace { pub dock: Dock, pub status_bar: StatusBar, pub toolbar: Toolbar, - pub breadcrumb_height: f32, - pub breadcrumbs: Interactive, pub disconnected_overlay: ContainedText, pub modal: ContainerStyle, pub zoomed_panel_foreground: ContainerStyle, @@ -120,7 +118,6 @@ pub struct Titlebar { pub height: f32, pub menu: TitlebarMenu, pub project_menu_button: Toggleable>, - pub project_name_divider: ContainedText, pub git_menu_button: Toggleable>, pub item_spacing: f32, pub face_pile_spacing: f32, @@ -411,6 +408,8 @@ pub struct Toolbar { pub height: f32, pub item_spacing: f32, pub toggleable_tool: Toggleable>, + pub breadcrumb_height: f32, + pub breadcrumbs: Interactive, } #[derive(Clone, Deserialize, Default, JsonSchema)] @@ -835,6 +834,9 @@ pub struct AutocompleteStyle { pub selected_item: ContainerStyle, pub hovered_item: ContainerStyle, pub match_highlight: HighlightStyle, + pub server_name_container: ContainerStyle, + pub server_name_color: Color, + pub server_name_size_percent: f32, } #[derive(Clone, Copy, Default, Deserialize, JsonSchema)] @@ -1150,6 +1152,17 @@ pub struct AssistantStyle { pub api_key_editor: FieldEditor, pub api_key_prompt: ContainedText, pub saved_conversation: SavedConversation, + pub inline: InlineAssistantStyle, +} + +#[derive(Clone, Deserialize, Default, JsonSchema)] +pub struct InlineAssistantStyle { + #[serde(flatten)] + pub container: ContainerStyle, + pub editor: FieldEditor, + pub disabled_editor: FieldEditor, + pub pending_edit_background: Color, + pub include_conversation: ToggleIconButtonStyle, } #[derive(Clone, Deserialize, Default, JsonSchema)] diff --git a/crates/vim/src/motion.rs b/crates/vim/src/motion.rs index 6f28430796..16bccb6963 100644 --- a/crates/vim/src/motion.rs +++ b/crates/vim/src/motion.rs @@ -590,12 +590,12 @@ pub(crate) fn next_word_start( ignore_punctuation: bool, times: usize, ) -> DisplayPoint { - let language = map.buffer_snapshot.language_at(point.to_point(map)); + let scope = map.buffer_snapshot.language_scope_at(point.to_point(map)); for _ in 0..times { let mut crossed_newline = false; point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| { - let left_kind = char_kind(language, left).coerce_punctuation(ignore_punctuation); - let right_kind = char_kind(language, right).coerce_punctuation(ignore_punctuation); + let left_kind = char_kind(&scope, left).coerce_punctuation(ignore_punctuation); + let right_kind = char_kind(&scope, right).coerce_punctuation(ignore_punctuation); let at_newline = right == '\n'; let found = (left_kind != right_kind && right_kind != CharKind::Whitespace) @@ -615,7 +615,7 @@ fn next_word_end( ignore_punctuation: bool, times: usize, ) -> DisplayPoint { - let language = map.buffer_snapshot.language_at(point.to_point(map)); + let scope = map.buffer_snapshot.language_scope_at(point.to_point(map)); for _ in 0..times { if point.column() < map.line_len(point.row()) { *point.column_mut() += 1; @@ -623,10 +623,9 @@ fn next_word_end( *point.row_mut() += 1; *point.column_mut() = 0; } - // *point.column_mut() += 1; point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| { - let left_kind = char_kind(language, left).coerce_punctuation(ignore_punctuation); - let right_kind = char_kind(language, right).coerce_punctuation(ignore_punctuation); + let left_kind = char_kind(&scope, left).coerce_punctuation(ignore_punctuation); + let right_kind = char_kind(&scope, right).coerce_punctuation(ignore_punctuation); left_kind != right_kind && left_kind != CharKind::Whitespace }); @@ -652,14 +651,14 @@ fn previous_word_start( ignore_punctuation: bool, times: usize, ) -> DisplayPoint { - let language = map.buffer_snapshot.language_at(point.to_point(map)); + let scope = map.buffer_snapshot.language_scope_at(point.to_point(map)); for _ in 0..times { // This works even though find_preceding_boundary is called for every character in the line containing // cursor because the newline is checked only once. point = movement::find_preceding_boundary(map, point, FindRange::MultiLine, |left, right| { - let left_kind = char_kind(language, left).coerce_punctuation(ignore_punctuation); - let right_kind = char_kind(language, right).coerce_punctuation(ignore_punctuation); + let left_kind = char_kind(&scope, left).coerce_punctuation(ignore_punctuation); + let right_kind = char_kind(&scope, right).coerce_punctuation(ignore_punctuation); (left_kind != right_kind && !right.is_whitespace()) || left == '\n' }); @@ -673,7 +672,7 @@ fn first_non_whitespace( from: DisplayPoint, ) -> DisplayPoint { let mut last_point = start_of_line(map, display_lines, from); - let language = map.buffer_snapshot.language_at(from.to_point(map)); + let scope = map.buffer_snapshot.language_scope_at(from.to_point(map)); for (ch, point) in map.chars_at(last_point) { if ch == '\n' { return from; @@ -681,7 +680,7 @@ fn first_non_whitespace( last_point = point; - if char_kind(language, ch) != CharKind::Whitespace { + if char_kind(&scope, ch) != CharKind::Whitespace { break; } } diff --git a/crates/vim/src/normal/change.rs b/crates/vim/src/normal/change.rs index 6e64b050d1..836ce1492b 100644 --- a/crates/vim/src/normal/change.rs +++ b/crates/vim/src/normal/change.rs @@ -89,22 +89,21 @@ fn expand_changed_word_selection( ignore_punctuation: bool, ) -> bool { if times.is_none() || times.unwrap() == 1 { - let language = map + let scope = map .buffer_snapshot - .language_at(selection.start.to_point(map)); + .language_scope_at(selection.start.to_point(map)); let in_word = map .chars_at(selection.head()) .next() - .map(|(c, _)| char_kind(language, c) != CharKind::Whitespace) + .map(|(c, _)| char_kind(&scope, c) != CharKind::Whitespace) .unwrap_or_default(); if in_word { selection.end = movement::find_boundary(map, selection.end, FindRange::MultiLine, |left, right| { - let left_kind = - char_kind(language, left).coerce_punctuation(ignore_punctuation); + let left_kind = char_kind(&scope, left).coerce_punctuation(ignore_punctuation); let right_kind = - char_kind(language, right).coerce_punctuation(ignore_punctuation); + char_kind(&scope, right).coerce_punctuation(ignore_punctuation); left_kind != right_kind && left_kind != CharKind::Whitespace }); diff --git a/crates/vim/src/normal/scroll.rs b/crates/vim/src/normal/scroll.rs index a2bbab0478..1b3dcee6ad 100644 --- a/crates/vim/src/normal/scroll.rs +++ b/crates/vim/src/normal/scroll.rs @@ -67,7 +67,8 @@ fn scroll_editor(editor: &mut Editor, amount: &ScrollAmount, cx: &mut ViewContex let top_anchor = editor.scroll_manager.anchor().anchor; editor.change_selections(None, cx, |s| { - s.move_heads_with(|map, head, goal| { + s.move_with(|map, selection| { + let head = selection.head(); let top = top_anchor.to_display_point(map); let min_row = top.row() + VERTICAL_SCROLL_MARGIN as u32; let max_row = top.row() + visible_rows - VERTICAL_SCROLL_MARGIN as u32 - 1; @@ -79,7 +80,11 @@ fn scroll_editor(editor: &mut Editor, amount: &ScrollAmount, cx: &mut ViewContex } else { head }; - (new_head, goal) + if selection.is_empty() { + selection.collapse_to(new_head, selection.goal) + } else { + selection.set_head(new_head, selection.goal) + }; }) }); } @@ -90,12 +95,35 @@ mod test { use crate::{state::Mode, test::VimTestContext}; use gpui::geometry::vector::vec2f; use indoc::indoc; + use language::Point; #[gpui::test] async fn test_scroll(cx: &mut gpui::TestAppContext) { let mut cx = VimTestContext::new(cx, true).await; - cx.set_state(indoc! {"ˇa\nb\nc\nd\ne\n"}, Mode::Normal); + let window = cx.window; + let line_height = + cx.editor(|editor, cx| editor.style(cx).text.line_height(cx.font_cache())); + window.simulate_resize(vec2f(1000., 8.0 * line_height - 1.0), &mut cx); + + cx.set_state( + indoc!( + "ˇone + two + three + four + five + six + seven + eight + nine + ten + eleven + twelve + " + ), + Mode::Normal, + ); cx.update_editor(|editor, cx| { assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 0.)) @@ -112,5 +140,33 @@ mod test { cx.update_editor(|editor, cx| { assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 2.)) }); + + // does not select in normal mode + cx.simulate_keystrokes(["g", "g"]); + cx.update_editor(|editor, cx| { + assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 0.)) + }); + cx.simulate_keystrokes(["ctrl-d"]); + cx.update_editor(|editor, cx| { + assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 2.0)); + assert_eq!( + editor.selections.newest(cx).range(), + Point::new(5, 0)..Point::new(5, 0) + ) + }); + + // does select in visual mode + cx.simulate_keystrokes(["g", "g"]); + cx.update_editor(|editor, cx| { + assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 0.)) + }); + cx.simulate_keystrokes(["v", "ctrl-d"]); + cx.update_editor(|editor, cx| { + assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 2.0)); + assert_eq!( + editor.selections.newest(cx).range(), + Point::new(0, 0)..Point::new(5, 1) + ) + }); } } diff --git a/crates/vim/src/object.rs b/crates/vim/src/object.rs index 94906a1e80..653d4ca7b6 100644 --- a/crates/vim/src/object.rs +++ b/crates/vim/src/object.rs @@ -182,19 +182,22 @@ fn in_word( ignore_punctuation: bool, ) -> Option> { // Use motion::right so that we consider the character under the cursor when looking for the start - let language = map.buffer_snapshot.language_at(relative_to.to_point(map)); + let scope = map + .buffer_snapshot + .language_scope_at(relative_to.to_point(map)); let start = movement::find_preceding_boundary( map, right(map, relative_to, 1), movement::FindRange::SingleLine, |left, right| { - char_kind(language, left).coerce_punctuation(ignore_punctuation) - != char_kind(language, right).coerce_punctuation(ignore_punctuation) + char_kind(&scope, left).coerce_punctuation(ignore_punctuation) + != char_kind(&scope, right).coerce_punctuation(ignore_punctuation) }, ); + let end = movement::find_boundary(map, relative_to, FindRange::SingleLine, |left, right| { - char_kind(language, left).coerce_punctuation(ignore_punctuation) - != char_kind(language, right).coerce_punctuation(ignore_punctuation) + char_kind(&scope, left).coerce_punctuation(ignore_punctuation) + != char_kind(&scope, right).coerce_punctuation(ignore_punctuation) }); Some(start..end) @@ -217,11 +220,13 @@ fn around_word( relative_to: DisplayPoint, ignore_punctuation: bool, ) -> Option> { - let language = map.buffer_snapshot.language_at(relative_to.to_point(map)); + let scope = map + .buffer_snapshot + .language_scope_at(relative_to.to_point(map)); let in_word = map .chars_at(relative_to) .next() - .map(|(c, _)| char_kind(language, c) != CharKind::Whitespace) + .map(|(c, _)| char_kind(&scope, c) != CharKind::Whitespace) .unwrap_or(false); if in_word { @@ -245,22 +250,24 @@ fn around_next_word( relative_to: DisplayPoint, ignore_punctuation: bool, ) -> Option> { - let language = map.buffer_snapshot.language_at(relative_to.to_point(map)); + let scope = map + .buffer_snapshot + .language_scope_at(relative_to.to_point(map)); // Get the start of the word let start = movement::find_preceding_boundary( map, right(map, relative_to, 1), FindRange::SingleLine, |left, right| { - char_kind(language, left).coerce_punctuation(ignore_punctuation) - != char_kind(language, right).coerce_punctuation(ignore_punctuation) + char_kind(&scope, left).coerce_punctuation(ignore_punctuation) + != char_kind(&scope, right).coerce_punctuation(ignore_punctuation) }, ); let mut word_found = false; let end = movement::find_boundary(map, relative_to, FindRange::MultiLine, |left, right| { - let left_kind = char_kind(language, left).coerce_punctuation(ignore_punctuation); - let right_kind = char_kind(language, right).coerce_punctuation(ignore_punctuation); + let left_kind = char_kind(&scope, left).coerce_punctuation(ignore_punctuation); + let right_kind = char_kind(&scope, right).coerce_punctuation(ignore_punctuation); let found = (word_found && left_kind != right_kind) || right == '\n' && left == '\n'; diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 2a97764647..66d55b38f0 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -3,7 +3,7 @@ authors = ["Nathan Sobo "] description = "The fast, collaborative code editor." edition = "2021" name = "zed" -version = "0.102.0" +version = "0.103.0" publish = false [lib] diff --git a/crates/zed/src/languages.rs b/crates/zed/src/languages.rs index eb31c08dd2..f0b8a1444a 100644 --- a/crates/zed/src/languages.rs +++ b/crates/zed/src/languages.rs @@ -6,6 +6,7 @@ use std::{borrow::Cow, str, sync::Arc}; use util::asset_str; mod c; +mod css; mod elixir; mod go; mod html; @@ -18,6 +19,7 @@ mod python; mod ruby; mod rust; mod svelte; +mod tailwind; mod typescript; mod yaml; @@ -51,7 +53,14 @@ pub fn init(languages: Arc, node_runtime: Arc) { tree_sitter_cpp::language(), vec![Arc::new(c::CLspAdapter)], ); - language("css", tree_sitter_css::language(), vec![]); + language( + "css", + tree_sitter_css::language(), + vec![ + Arc::new(css::CssLspAdapter::new(node_runtime.clone())), + Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())), + ], + ); language( "elixir", tree_sitter_elixir::language(), @@ -95,6 +104,7 @@ pub fn init(languages: Arc, node_runtime: Arc) { vec![ Arc::new(typescript::TypeScriptLspAdapter::new(node_runtime.clone())), Arc::new(typescript::EsLintLspAdapter::new(node_runtime.clone())), + Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())), ], ); language( @@ -111,12 +121,16 @@ pub fn init(languages: Arc, node_runtime: Arc) { vec![ Arc::new(typescript::TypeScriptLspAdapter::new(node_runtime.clone())), Arc::new(typescript::EsLintLspAdapter::new(node_runtime.clone())), + Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())), ], ); language( "html", tree_sitter_html::language(), - vec![Arc::new(html::HtmlLspAdapter::new(node_runtime.clone()))], + vec![ + Arc::new(html::HtmlLspAdapter::new(node_runtime.clone())), + Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())), + ], ); language( "ruby", diff --git a/crates/zed/src/languages/c.rs b/crates/zed/src/languages/c.rs index c5041136c9..27a65570b6 100644 --- a/crates/zed/src/languages/c.rs +++ b/crates/zed/src/languages/c.rs @@ -19,6 +19,10 @@ impl super::LspAdapter for CLspAdapter { LanguageServerName("clangd".into()) } + fn short_name(&self) -> &'static str { + "clangd" + } + async fn fetch_latest_server_version( &self, delegate: &dyn LspAdapterDelegate, diff --git a/crates/zed/src/languages/css.rs b/crates/zed/src/languages/css.rs new file mode 100644 index 0000000000..f2103050f3 --- /dev/null +++ b/crates/zed/src/languages/css.rs @@ -0,0 +1,130 @@ +use anyhow::{anyhow, Result}; +use async_trait::async_trait; +use futures::StreamExt; +use language::{LanguageServerName, LspAdapter, LspAdapterDelegate}; +use lsp::LanguageServerBinary; +use node_runtime::NodeRuntime; +use serde_json::json; +use smol::fs; +use std::{ + any::Any, + ffi::OsString, + path::{Path, PathBuf}, + sync::Arc, +}; +use util::ResultExt; + +const SERVER_PATH: &'static str = + "node_modules/vscode-langservers-extracted/bin/vscode-css-language-server"; + +fn server_binary_arguments(server_path: &Path) -> Vec { + vec![server_path.into(), "--stdio".into()] +} + +pub struct CssLspAdapter { + node: Arc, +} + +impl CssLspAdapter { + pub fn new(node: Arc) -> Self { + CssLspAdapter { node } + } +} + +#[async_trait] +impl LspAdapter for CssLspAdapter { + async fn name(&self) -> LanguageServerName { + LanguageServerName("vscode-css-language-server".into()) + } + + fn short_name(&self) -> &'static str { + "css" + } + + async fn fetch_latest_server_version( + &self, + _: &dyn LspAdapterDelegate, + ) -> Result> { + Ok(Box::new( + self.node + .npm_package_latest_version("vscode-langservers-extracted") + .await?, + ) as Box<_>) + } + + async fn fetch_server_binary( + &self, + version: Box, + container_dir: PathBuf, + _: &dyn LspAdapterDelegate, + ) -> Result { + let version = version.downcast::().unwrap(); + let server_path = container_dir.join(SERVER_PATH); + + if fs::metadata(&server_path).await.is_err() { + self.node + .npm_install_packages( + &container_dir, + [("vscode-langservers-extracted", version.as_str())], + ) + .await?; + } + + Ok(LanguageServerBinary { + path: self.node.binary_path().await?, + arguments: server_binary_arguments(&server_path), + }) + } + + async fn cached_server_binary( + &self, + container_dir: PathBuf, + _: &dyn LspAdapterDelegate, + ) -> Option { + get_cached_server_binary(container_dir, &self.node).await + } + + async fn installation_test_binary( + &self, + container_dir: PathBuf, + ) -> Option { + get_cached_server_binary(container_dir, &self.node).await + } + + async fn initialization_options(&self) -> Option { + Some(json!({ + "provideFormatter": true + })) + } +} + +async fn get_cached_server_binary( + container_dir: PathBuf, + node: &NodeRuntime, +) -> Option { + (|| async move { + let mut last_version_dir = None; + let mut entries = fs::read_dir(&container_dir).await?; + while let Some(entry) = entries.next().await { + let entry = entry?; + if entry.file_type().await?.is_dir() { + last_version_dir = Some(entry.path()); + } + } + let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?; + let server_path = last_version_dir.join(SERVER_PATH); + if server_path.exists() { + Ok(LanguageServerBinary { + path: node.binary_path().await?, + arguments: server_binary_arguments(&server_path), + }) + } else { + Err(anyhow!( + "missing executable in directory {:?}", + last_version_dir + )) + } + })() + .await + .log_err() +} diff --git a/crates/zed/src/languages/css/config.toml b/crates/zed/src/languages/css/config.toml index ba9660c4ed..05de4be8a3 100644 --- a/crates/zed/src/languages/css/config.toml +++ b/crates/zed/src/languages/css/config.toml @@ -8,3 +8,4 @@ brackets = [ { start = "\"", end = "\"", close = true, newline = false, not_in = ["string", "comment"] }, { start = "'", end = "'", close = true, newline = false, not_in = ["string", "comment"] }, ] +word_characters = ["-"] diff --git a/crates/zed/src/languages/elixir.rs b/crates/zed/src/languages/elixir.rs index c32927e15c..b166feda76 100644 --- a/crates/zed/src/languages/elixir.rs +++ b/crates/zed/src/languages/elixir.rs @@ -27,6 +27,10 @@ impl LspAdapter for ElixirLspAdapter { LanguageServerName("elixir-ls".into()) } + fn short_name(&self) -> &'static str { + "elixir-ls" + } + fn will_start_server( &self, delegate: &Arc, diff --git a/crates/zed/src/languages/go.rs b/crates/zed/src/languages/go.rs index d7982f7bdb..19b7013709 100644 --- a/crates/zed/src/languages/go.rs +++ b/crates/zed/src/languages/go.rs @@ -37,6 +37,10 @@ impl super::LspAdapter for GoLspAdapter { LanguageServerName("gopls".into()) } + fn short_name(&self) -> &'static str { + "gopls" + } + async fn fetch_latest_server_version( &self, delegate: &dyn LspAdapterDelegate, diff --git a/crates/zed/src/languages/html.rs b/crates/zed/src/languages/html.rs index ecc839fca6..cfb6a5dde9 100644 --- a/crates/zed/src/languages/html.rs +++ b/crates/zed/src/languages/html.rs @@ -37,6 +37,10 @@ impl LspAdapter for HtmlLspAdapter { LanguageServerName("vscode-html-language-server".into()) } + fn short_name(&self) -> &'static str { + "html" + } + async fn fetch_latest_server_version( &self, _: &dyn LspAdapterDelegate, diff --git a/crates/zed/src/languages/html/config.toml b/crates/zed/src/languages/html/config.toml index 077a421ce1..164e095cee 100644 --- a/crates/zed/src/languages/html/config.toml +++ b/crates/zed/src/languages/html/config.toml @@ -10,3 +10,4 @@ brackets = [ { start = "<", end = ">", close = true, newline = true, not_in = ["comment", "string"] }, { start = "!--", end = " --", close = true, newline = false, not_in = ["comment", "string"] }, ] +word_characters = ["-"] diff --git a/crates/zed/src/languages/javascript/config.toml b/crates/zed/src/languages/javascript/config.toml index 0435f96c92..2394c57539 100644 --- a/crates/zed/src/languages/javascript/config.toml +++ b/crates/zed/src/languages/javascript/config.toml @@ -14,7 +14,12 @@ brackets = [ { start = "/*", end = " */", close = true, newline = false, not_in = ["comment", "string"] }, ] word_characters = ["$", "#"] +scope_opt_in_language_servers = ["tailwindcss-language-server"] [overrides.element] line_comment = { remove = true } block_comment = ["{/* ", " */}"] + +[overrides.string] +word_characters = ["-"] +opt_into_language_servers = ["tailwindcss-language-server"] diff --git a/crates/zed/src/languages/json.rs b/crates/zed/src/languages/json.rs index 61d19ce5b6..049549ac5d 100644 --- a/crates/zed/src/languages/json.rs +++ b/crates/zed/src/languages/json.rs @@ -43,6 +43,10 @@ impl LspAdapter for JsonLspAdapter { LanguageServerName("json-language-server".into()) } + fn short_name(&self) -> &'static str { + "json" + } + async fn fetch_latest_server_version( &self, _: &dyn LspAdapterDelegate, @@ -102,7 +106,7 @@ impl LspAdapter for JsonLspAdapter { fn workspace_configuration( &self, cx: &mut AppContext, - ) -> Option> { + ) -> BoxFuture<'static, serde_json::Value> { let action_names = cx.all_action_names().collect::>(); let staff_mode = cx.is_staff(); let language_names = &self.languages.language_names(); @@ -113,29 +117,28 @@ impl LspAdapter for JsonLspAdapter { }, cx, ); - Some( - future::ready(serde_json::json!({ - "json": { - "format": { - "enable": true, + + future::ready(serde_json::json!({ + "json": { + "format": { + "enable": true, + }, + "schemas": [ + { + "fileMatch": [ + schema_file_match(&paths::SETTINGS), + &*paths::LOCAL_SETTINGS_RELATIVE_PATH, + ], + "schema": settings_schema, }, - "schemas": [ - { - "fileMatch": [ - schema_file_match(&paths::SETTINGS), - &*paths::LOCAL_SETTINGS_RELATIVE_PATH, - ], - "schema": settings_schema, - }, - { - "fileMatch": [schema_file_match(&paths::KEYMAP)], - "schema": KeymapFile::generate_json_schema(&action_names), - } - ] - } - })) - .boxed(), - ) + { + "fileMatch": [schema_file_match(&paths::KEYMAP)], + "schema": KeymapFile::generate_json_schema(&action_names), + } + ] + } + })) + .boxed() } async fn language_ids(&self) -> HashMap { diff --git a/crates/zed/src/languages/language_plugin.rs b/crates/zed/src/languages/language_plugin.rs index b071936392..b2405d8bb8 100644 --- a/crates/zed/src/languages/language_plugin.rs +++ b/crates/zed/src/languages/language_plugin.rs @@ -70,6 +70,10 @@ impl LspAdapter for PluginLspAdapter { LanguageServerName(name.into()) } + fn short_name(&self) -> &'static str { + "PluginLspAdapter" + } + async fn fetch_latest_server_version( &self, _: &dyn LspAdapterDelegate, diff --git a/crates/zed/src/languages/lua.rs b/crates/zed/src/languages/lua.rs index 7c5c7179d0..8187847c9a 100644 --- a/crates/zed/src/languages/lua.rs +++ b/crates/zed/src/languages/lua.rs @@ -6,7 +6,7 @@ use futures::{io::BufReader, StreamExt}; use language::{LanguageServerName, LspAdapterDelegate}; use lsp::LanguageServerBinary; use smol::fs; -use std::{any::Any, env::consts, ffi::OsString, path::PathBuf}; +use std::{any::Any, env::consts, path::PathBuf}; use util::{ async_iife, github::{latest_github_release, GitHubLspBinaryVersion}, @@ -16,19 +16,16 @@ use util::{ #[derive(Copy, Clone)] pub struct LuaLspAdapter; -fn server_binary_arguments() -> Vec { - vec![ - "--logpath=~/lua-language-server.log".into(), - "--loglevel=trace".into(), - ] -} - #[async_trait] impl super::LspAdapter for LuaLspAdapter { async fn name(&self) -> LanguageServerName { LanguageServerName("lua-language-server".into()) } + fn short_name(&self) -> &'static str { + "lua" + } + async fn fetch_latest_server_version( &self, delegate: &dyn LspAdapterDelegate, @@ -83,7 +80,7 @@ impl super::LspAdapter for LuaLspAdapter { .await?; Ok(LanguageServerBinary { path: binary_path, - arguments: server_binary_arguments(), + arguments: Vec::new(), }) } @@ -127,7 +124,7 @@ async fn get_cached_server_binary(container_dir: PathBuf) -> Option &'static str { + "php" + } + async fn fetch_latest_server_version( &self, _delegate: &dyn LspAdapterDelegate, diff --git a/crates/zed/src/languages/python.rs b/crates/zed/src/languages/python.rs index d89a4171e9..956cf49551 100644 --- a/crates/zed/src/languages/python.rs +++ b/crates/zed/src/languages/python.rs @@ -35,6 +35,10 @@ impl LspAdapter for PythonLspAdapter { LanguageServerName("pyright".into()) } + fn short_name(&self) -> &'static str { + "pyright" + } + async fn fetch_latest_server_version( &self, _: &dyn LspAdapterDelegate, diff --git a/crates/zed/src/languages/ruby.rs b/crates/zed/src/languages/ruby.rs index 358441352a..3890b90dbd 100644 --- a/crates/zed/src/languages/ruby.rs +++ b/crates/zed/src/languages/ruby.rs @@ -12,6 +12,10 @@ impl LspAdapter for RubyLanguageServer { LanguageServerName("solargraph".into()) } + fn short_name(&self) -> &'static str { + "solargraph" + } + async fn fetch_latest_server_version( &self, _: &dyn LspAdapterDelegate, diff --git a/crates/zed/src/languages/rust.rs b/crates/zed/src/languages/rust.rs index d550d126bb..854eeb7e08 100644 --- a/crates/zed/src/languages/rust.rs +++ b/crates/zed/src/languages/rust.rs @@ -22,6 +22,10 @@ impl LspAdapter for RustLspAdapter { LanguageServerName("rust-analyzer".into()) } + fn short_name(&self) -> &'static str { + "rust" + } + async fn fetch_latest_server_version( &self, delegate: &dyn LspAdapterDelegate, diff --git a/crates/zed/src/languages/svelte.rs b/crates/zed/src/languages/svelte.rs index 8416859f5a..35665e864f 100644 --- a/crates/zed/src/languages/svelte.rs +++ b/crates/zed/src/languages/svelte.rs @@ -36,6 +36,10 @@ impl LspAdapter for SvelteLspAdapter { LanguageServerName("svelte-language-server".into()) } + fn short_name(&self) -> &'static str { + "svelte" + } + async fn fetch_latest_server_version( &self, _: &dyn LspAdapterDelegate, diff --git a/crates/zed/src/languages/tailwind.rs b/crates/zed/src/languages/tailwind.rs new file mode 100644 index 0000000000..12a0a4e3b8 --- /dev/null +++ b/crates/zed/src/languages/tailwind.rs @@ -0,0 +1,161 @@ +use anyhow::{anyhow, Result}; +use async_trait::async_trait; +use collections::HashMap; +use futures::{ + future::{self, BoxFuture}, + FutureExt, StreamExt, +}; +use gpui::AppContext; +use language::{LanguageServerName, LspAdapter, LspAdapterDelegate}; +use lsp::LanguageServerBinary; +use node_runtime::NodeRuntime; +use serde_json::{json, Value}; +use smol::fs; +use std::{ + any::Any, + ffi::OsString, + path::{Path, PathBuf}, + sync::Arc, +}; +use util::ResultExt; + +const SERVER_PATH: &'static str = "node_modules/.bin/tailwindcss-language-server"; + +fn server_binary_arguments(server_path: &Path) -> Vec { + vec![server_path.into(), "--stdio".into()] +} + +pub struct TailwindLspAdapter { + node: Arc, +} + +impl TailwindLspAdapter { + pub fn new(node: Arc) -> Self { + TailwindLspAdapter { node } + } +} + +#[async_trait] +impl LspAdapter for TailwindLspAdapter { + async fn name(&self) -> LanguageServerName { + LanguageServerName("tailwindcss-language-server".into()) + } + + fn short_name(&self) -> &'static str { + "tailwind" + } + + async fn fetch_latest_server_version( + &self, + _: &dyn LspAdapterDelegate, + ) -> Result> { + Ok(Box::new( + self.node + .npm_package_latest_version("@tailwindcss/language-server") + .await?, + ) as Box<_>) + } + + async fn fetch_server_binary( + &self, + version: Box, + container_dir: PathBuf, + _: &dyn LspAdapterDelegate, + ) -> Result { + let version = version.downcast::().unwrap(); + let server_path = container_dir.join(SERVER_PATH); + + if fs::metadata(&server_path).await.is_err() { + self.node + .npm_install_packages( + &container_dir, + [("@tailwindcss/language-server", version.as_str())], + ) + .await?; + } + + Ok(LanguageServerBinary { + path: self.node.binary_path().await?, + arguments: server_binary_arguments(&server_path), + }) + } + + async fn cached_server_binary( + &self, + container_dir: PathBuf, + _: &dyn LspAdapterDelegate, + ) -> Option { + get_cached_server_binary(container_dir, &self.node).await + } + + async fn installation_test_binary( + &self, + container_dir: PathBuf, + ) -> Option { + get_cached_server_binary(container_dir, &self.node).await + } + + async fn initialization_options(&self) -> Option { + Some(json!({ + "provideFormatter": true, + "userLanguages": { + "html": "html", + "css": "css", + "javascript": "javascript", + "typescriptreact": "typescriptreact", + }, + })) + } + + fn workspace_configuration(&self, _: &mut AppContext) -> BoxFuture<'static, Value> { + future::ready(json!({ + "tailwindCSS": { + "emmetCompletions": true, + } + })) + .boxed() + } + + async fn language_ids(&self) -> HashMap { + HashMap::from_iter( + [ + ("HTML".to_string(), "html".to_string()), + ("CSS".to_string(), "css".to_string()), + ("JavaScript".to_string(), "javascript".to_string()), + ("TSX".to_string(), "typescriptreact".to_string()), + ] + .into_iter(), + ) + } +} + +async fn get_cached_server_binary( + container_dir: PathBuf, + node: &NodeRuntime, +) -> Option { + (|| async move { + let mut last_version_dir = None; + let mut entries = fs::read_dir(&container_dir).await?; + while let Some(entry) = entries.next().await { + let entry = entry?; + if entry.file_type().await?.is_dir() { + last_version_dir = Some(entry.path()); + } + } + let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?; + let server_path = last_version_dir.join(SERVER_PATH); + if server_path.exists() { + Ok(LanguageServerBinary { + path: node.binary_path().await?, + arguments: server_binary_arguments(&server_path), + }) + } else { + Err(anyhow!( + "missing executable in directory {:?}", + last_version_dir + )) + } + })() + .await + .log_err() +} diff --git a/crates/zed/src/languages/tsx/config.toml b/crates/zed/src/languages/tsx/config.toml index 63d1f85e64..a7f99bef5e 100644 --- a/crates/zed/src/languages/tsx/config.toml +++ b/crates/zed/src/languages/tsx/config.toml @@ -13,7 +13,12 @@ brackets = [ { start = "/*", end = " */", close = true, newline = false, not_in = ["string", "comment"] }, ] word_characters = ["#", "$"] +scope_opt_in_language_servers = ["tailwindcss-language-server"] [overrides.element] line_comment = { remove = true } block_comment = ["{/* ", " */}"] + +[overrides.string] +word_characters = ["-"] +opt_into_language_servers = ["tailwindcss-language-server"] diff --git a/crates/zed/src/languages/typescript.rs b/crates/zed/src/languages/typescript.rs index 34a512f300..27074e164b 100644 --- a/crates/zed/src/languages/typescript.rs +++ b/crates/zed/src/languages/typescript.rs @@ -56,6 +56,10 @@ impl LspAdapter for TypeScriptLspAdapter { LanguageServerName("typescript-language-server".into()) } + fn short_name(&self) -> &'static str { + "tsserver" + } + async fn fetch_latest_server_version( &self, _: &dyn LspAdapterDelegate, @@ -202,24 +206,26 @@ impl EsLintLspAdapter { #[async_trait] impl LspAdapter for EsLintLspAdapter { - fn workspace_configuration(&self, _: &mut AppContext) -> Option> { - Some( - future::ready(json!({ - "": { - "validate": "on", - "rulesCustomizations": [], - "run": "onType", - "nodePath": null, - } - })) - .boxed(), - ) + fn workspace_configuration(&self, _: &mut AppContext) -> BoxFuture<'static, Value> { + future::ready(json!({ + "": { + "validate": "on", + "rulesCustomizations": [], + "run": "onType", + "nodePath": null, + } + })) + .boxed() } async fn name(&self) -> LanguageServerName { LanguageServerName("eslint".into()) } + fn short_name(&self) -> &'static str { + "eslint" + } + async fn fetch_latest_server_version( &self, delegate: &dyn LspAdapterDelegate, diff --git a/crates/zed/src/languages/yaml.rs b/crates/zed/src/languages/yaml.rs index b57c6f5699..21155cc231 100644 --- a/crates/zed/src/languages/yaml.rs +++ b/crates/zed/src/languages/yaml.rs @@ -40,6 +40,10 @@ impl LspAdapter for YamlLspAdapter { LanguageServerName("yaml-language-server".into()) } + fn short_name(&self) -> &'static str { + "yaml" + } + async fn fetch_latest_server_version( &self, _: &dyn LspAdapterDelegate, @@ -86,21 +90,20 @@ impl LspAdapter for YamlLspAdapter { ) -> Option { get_cached_server_binary(container_dir, &self.node).await } - fn workspace_configuration(&self, cx: &mut AppContext) -> Option> { + fn workspace_configuration(&self, cx: &mut AppContext) -> BoxFuture<'static, Value> { let tab_size = all_language_settings(None, cx) .language(Some("YAML")) .tab_size; - Some( - future::ready(serde_json::json!({ - "yaml": { - "keyOrdering": false - }, - "[yaml]": { - "editor.tabSize": tab_size, - } - })) - .boxed(), - ) + + future::ready(serde_json::json!({ + "yaml": { + "keyOrdering": false + }, + "[yaml]": { + "editor.tabSize": tab_size, + } + })) + .boxed() } } diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 9ea406fc3e..ba8fa840f5 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -264,8 +264,9 @@ pub fn initialize_workspace( toolbar.add_item(breadcrumbs, cx); let buffer_search_bar = cx.add_view(BufferSearchBar::new); toolbar.add_item(buffer_search_bar.clone(), cx); - let quick_action_bar = - cx.add_view(|_| QuickActionBar::new(buffer_search_bar)); + let quick_action_bar = cx.add_view(|_| { + QuickActionBar::new(buffer_search_bar, workspace) + }); toolbar.add_item(quick_action_bar, cx); let project_search_bar = cx.add_view(|_| ProjectSearchBar::new()); toolbar.add_item(project_search_bar, cx); diff --git a/styles/src/build_themes.ts b/styles/src/build_themes.ts index 17575663a1..4d262f8146 100644 --- a/styles/src/build_themes.ts +++ b/styles/src/build_themes.ts @@ -21,9 +21,7 @@ function clear_themes(theme_directory: string) { } } -const all_themes: Theme[] = themes.map((theme) => - create_theme(theme) -) +const all_themes: Theme[] = themes.map((theme) => create_theme(theme)) function write_themes(themes: Theme[], output_directory: string) { clear_themes(output_directory) @@ -34,10 +32,7 @@ function write_themes(themes: Theme[], output_directory: string) { const style_tree = app() const style_tree_json = JSON.stringify(style_tree, null, 2) const temp_path = path.join(temp_directory, `${theme.name}.json`) - const out_path = path.join( - output_directory, - `${theme.name}.json` - ) + const out_path = path.join(output_directory, `${theme.name}.json`) fs.writeFileSync(temp_path, style_tree_json) fs.renameSync(temp_path, out_path) console.log(`- ${out_path} created`) diff --git a/styles/src/build_tokens.ts b/styles/src/build_tokens.ts index fd6aa18ced..3c52b6d989 100644 --- a/styles/src/build_tokens.ts +++ b/styles/src/build_tokens.ts @@ -83,8 +83,6 @@ function write_tokens(themes: Theme[], tokens_directory: string) { console.log(`- ${METADATA_FILE} created`) } -const all_themes: Theme[] = themes.map((theme) => - create_theme(theme) -) +const all_themes: Theme[] = themes.map((theme) => create_theme(theme)) write_tokens(all_themes, TOKENS_DIRECTORY) diff --git a/styles/src/component/button.ts b/styles/src/component/button.ts index 3b554ae37a..e0e831b082 100644 --- a/styles/src/component/button.ts +++ b/styles/src/component/button.ts @@ -5,7 +5,7 @@ import { TextStyle, background } from "../style_tree/components" // eslint-disable-next-line @typescript-eslint/no-namespace export namespace Button { export type Options = { - layer: Layer, + layer: Layer background: keyof Theme["lowest"] color: keyof Theme["lowest"] variant: Button.Variant @@ -16,13 +16,13 @@ export namespace Button { bottom?: number left?: number right?: number - }, + } states: { - enabled?: boolean, - hovered?: boolean, - pressed?: boolean, - focused?: boolean, - disabled?: boolean, + enabled?: boolean + hovered?: boolean + pressed?: boolean + focused?: boolean + disabled?: boolean } } @@ -38,26 +38,26 @@ export namespace Button { export const CORNER_RADIUS = 6 export const variant = { - Default: 'filled', - Outline: 'outline', - Ghost: 'ghost' + Default: "filled", + Outline: "outline", + Ghost: "ghost", } as const - export type Variant = typeof variant[keyof typeof variant] + export type Variant = (typeof variant)[keyof typeof variant] export const shape = { - Rectangle: 'rectangle', - Square: 'square' + Rectangle: "rectangle", + Square: "square", } as const - export type Shape = typeof shape[keyof typeof shape] + export type Shape = (typeof shape)[keyof typeof shape] export const size = { Small: "sm", - Medium: "md" + Medium: "md", } as const - export type Size = typeof size[keyof typeof size] + export type Size = (typeof size)[keyof typeof size] export type BaseStyle = { corder_radius: number @@ -67,8 +67,8 @@ export namespace Button { bottom: number left: number right: number - }, - margin: Button.Options['margin'] + } + margin: Button.Options["margin"] button_height: number } @@ -81,15 +81,18 @@ export namespace Button { shape: Button.shape.Rectangle, states: { hovered: true, - pressed: true - } + pressed: true, + }, } ): BaseStyle => { const theme = useTheme() const layer = options.layer ?? theme.middle const color = options.color ?? "base" - const background_color = options.variant === Button.variant.Ghost ? null : background(layer, options.background ?? color) + const background_color = + options.variant === Button.variant.Ghost + ? null + : background(layer, options.background ?? color) const m = { top: options.margin?.top ?? 0, @@ -106,8 +109,14 @@ export namespace Button { padding: { top: padding, bottom: padding, - left: options.shape === Button.shape.Rectangle ? padding + Button.RECTANGLE_PADDING : padding, - right: options.shape === Button.shape.Rectangle ? padding + Button.RECTANGLE_PADDING : padding + left: + options.shape === Button.shape.Rectangle + ? padding + Button.RECTANGLE_PADDING + : padding, + right: + options.shape === Button.shape.Rectangle + ? padding + Button.RECTANGLE_PADDING + : padding, }, margin: m, button_height: 16, diff --git a/styles/src/component/icon_button.ts b/styles/src/component/icon_button.ts index 935909afdb..5b7c61b17c 100644 --- a/styles/src/component/icon_button.ts +++ b/styles/src/component/icon_button.ts @@ -11,11 +11,9 @@ export type Margin = { } interface IconButtonOptions { - layer?: - | Theme["lowest"] - | Theme["middle"] - | Theme["highest"] + layer?: Theme["lowest"] | Theme["middle"] | Theme["highest"] color?: keyof Theme["lowest"] + background_color?: keyof Theme["lowest"] margin?: Partial variant?: Button.Variant size?: Button.Size @@ -23,18 +21,25 @@ interface IconButtonOptions { type ToggleableIconButtonOptions = IconButtonOptions & { active_color?: keyof Theme["lowest"] + active_background_color?: keyof Theme["lowest"] active_layer?: Layer + active_variant?: Button.Variant } -export function icon_button({ color, margin, layer, variant, size }: IconButtonOptions = { - variant: Button.variant.Default, - size: Button.size.Medium, -}) { +export function icon_button( + { color, background_color, margin, layer, variant, size }: IconButtonOptions = { + variant: Button.variant.Default, + size: Button.size.Medium, + } +) { const theme = useTheme() if (!color) color = "base" - const background_color = variant === Button.variant.Ghost ? null : background(layer ?? theme.lowest, color) + const default_background = + variant === Button.variant.Ghost + ? null + : background(layer ?? theme.lowest, background_color ?? color) const m = { top: margin?.top ?? 0, @@ -55,42 +60,51 @@ export function icon_button({ color, margin, layer, variant, size }: IconButtonO corner_radius: 6, padding: padding, margin: m, - icon_width: 12, + icon_width: 14, icon_height: 14, button_width: size === Button.size.Small ? 16 : 20, button_height: 14, }, state: { default: { - background: background_color, + background: default_background, color: foreground(layer ?? theme.lowest, color), }, hovered: { - background: background(layer ?? theme.lowest, color, "hovered"), + background: background(layer ?? theme.lowest, background_color ?? color, "hovered"), color: foreground(layer ?? theme.lowest, color, "hovered"), }, clicked: { - background: background(layer ?? theme.lowest, color, "pressed"), + background: background(layer ?? theme.lowest, background_color ?? color, "pressed"), color: foreground(layer ?? theme.lowest, color, "pressed"), }, }, }) } -export function toggleable_icon_button( - theme: Theme, - { color, active_color, margin, variant, size, active_layer }: ToggleableIconButtonOptions -) { +export function toggleable_icon_button({ + color, + background_color, + active_color, + active_background_color, + active_variant, + margin, + variant, + size, + active_layer, +}: ToggleableIconButtonOptions) { if (!color) color = "base" return toggleable({ state: { - inactive: icon_button({ color, margin, variant, size }), + inactive: icon_button({ color, background_color, margin, variant, size }), active: icon_button({ color: active_color ? active_color : color, + background_color: active_background_color ? active_background_color : background_color, margin, layer: active_layer, - size + variant: active_variant || variant, + size, }), }, }) diff --git a/styles/src/component/index.ts b/styles/src/component/index.ts new file mode 100644 index 0000000000..f2cbc7b26a --- /dev/null +++ b/styles/src/component/index.ts @@ -0,0 +1,6 @@ +export * from "./icon_button" +export * from "./indicator" +export * from "./input" +export * from "./tab" +export * from "./tab_bar_button" +export * from "./text_button" diff --git a/styles/src/component/indicator.ts b/styles/src/component/indicator.ts index 81a3b40da7..b3d2105f6a 100644 --- a/styles/src/component/indicator.ts +++ b/styles/src/component/indicator.ts @@ -1,7 +1,13 @@ import { foreground } from "../style_tree/components" import { Layer, StyleSets } from "../theme" -export const indicator = ({ layer, color }: { layer: Layer, color: StyleSets }) => ({ +export const indicator = ({ + layer, + color, +}: { + layer: Layer + color: StyleSets +}) => ({ corner_radius: 4, padding: 4, margin: { top: 12, left: 12 }, diff --git a/styles/src/component/input.ts b/styles/src/component/input.ts index cadfcc8d4a..5921210f88 100644 --- a/styles/src/component/input.ts +++ b/styles/src/component/input.ts @@ -18,6 +18,6 @@ export const input = () => { bottom: 3, left: 12, right: 8, - } + }, } } diff --git a/styles/src/component/tab.ts b/styles/src/component/tab.ts index 9938fb9311..6f73b6f3fb 100644 --- a/styles/src/component/tab.ts +++ b/styles/src/component/tab.ts @@ -9,7 +9,7 @@ type TabProps = { export const tab = ({ layer }: TabProps) => { const active_color = text(layer, "sans", "base").color const inactive_border: Border = { - color: '#FFFFFF00', + color: "#FFFFFF00", width: 1, bottom: true, left: false, @@ -27,7 +27,7 @@ export const tab = ({ layer }: TabProps) => { top: 8, left: 8, right: 8, - bottom: 6 + bottom: 6, }, border: inactive_border, } @@ -35,17 +35,17 @@ export const tab = ({ layer }: TabProps) => { const i = interactive({ state: { default: { - ...base + ...base, }, hovered: { ...base, - ...text(layer, "sans", "base", "hovered") + ...text(layer, "sans", "base", "hovered"), }, clicked: { ...base, - ...text(layer, "sans", "base", "pressed") + ...text(layer, "sans", "base", "pressed"), }, - } + }, }) return toggleable({ @@ -60,14 +60,14 @@ export const tab = ({ layer }: TabProps) => { hovered: { ...i, ...text(layer, "sans", "base", "hovered"), - border: active_border + border: active_border, }, clicked: { ...i, ...text(layer, "sans", "base", "pressed"), - border: active_border + border: active_border, }, - } - } + }, + }, }) } diff --git a/styles/src/component/tab_bar_button.ts b/styles/src/component/tab_bar_button.ts index 0c43e7010e..9e7f9acfc3 100644 --- a/styles/src/component/tab_bar_button.ts +++ b/styles/src/component/tab_bar_button.ts @@ -12,44 +12,47 @@ type TabBarButtonProps = TabBarButtonOptions & { state?: Partial>> } -export function tab_bar_button(theme: Theme, { icon, color = "base" }: TabBarButtonProps) { +export function tab_bar_button( + theme: Theme, + { icon, color = "base" }: TabBarButtonProps +) { const button_spacing = 8 - return ( - interactive({ - base: { - icon: { - color: foreground(theme.middle, color), - asset: icon, - dimensions: { - width: 15, - height: 15, - }, + return interactive({ + base: { + icon: { + color: foreground(theme.middle, color), + asset: icon, + dimensions: { + width: 15, + height: 15, }, + }, + container: { + corner_radius: 4, + padding: { + top: 4, + bottom: 4, + left: 4, + right: 4, + }, + margin: { + left: button_spacing / 2, + right: button_spacing / 2, + }, + }, + }, + state: { + hovered: { container: { - corner_radius: 4, - padding: { - top: 4, bottom: 4, left: 4, right: 4 - }, - margin: { - left: button_spacing / 2, - right: button_spacing / 2, - }, + background: background(theme.middle, color, "hovered"), }, }, - state: { - hovered: { - container: { - background: background(theme.middle, color, "hovered"), - - } - }, - clicked: { - container: { - background: background(theme.middle, color, "pressed"), - } + clicked: { + container: { + background: background(theme.middle, color, "pressed"), }, }, - }) - ) + }, + }) } diff --git a/styles/src/component/text_button.ts b/styles/src/component/text_button.ts index ead017a803..8333d9e81a 100644 --- a/styles/src/component/text_button.ts +++ b/styles/src/component/text_button.ts @@ -10,10 +10,7 @@ import { Button } from "./button" import { Margin } from "./icon_button" interface TextButtonOptions { - layer?: - | Theme["lowest"] - | Theme["middle"] - | Theme["highest"] + layer?: Theme["lowest"] | Theme["middle"] | Theme["highest"] variant?: Button.Variant color?: keyof Theme["lowest"] margin?: Partial @@ -36,7 +33,10 @@ export function text_button({ const theme = useTheme() if (!color) color = "base" - const background_color = variant === Button.variant.Ghost ? null : background(layer ?? theme.lowest, color) + const background_color = + variant === Button.variant.Ghost + ? null + : background(layer ?? theme.lowest, color) const text_options: TextProperties = { size: "xs", @@ -67,20 +67,38 @@ export function text_button({ state: { default: { background: background_color, - color: - disabled - ? foreground(layer ?? theme.lowest, "disabled") - : foreground(layer ?? theme.lowest, color), + color: disabled + ? foreground(layer ?? theme.lowest, "disabled") + : foreground(layer ?? theme.lowest, color), }, - hovered: - disabled ? {} : { - background: background(layer ?? theme.lowest, color, "hovered"), - color: foreground(layer ?? theme.lowest, color, "hovered"), + hovered: disabled + ? {} + : { + background: background( + layer ?? theme.lowest, + color, + "hovered" + ), + color: foreground( + layer ?? theme.lowest, + color, + "hovered" + ), + }, + clicked: disabled + ? {} + : { + background: background( + layer ?? theme.lowest, + color, + "pressed" + ), + color: foreground( + layer ?? theme.lowest, + color, + "pressed" + ), }, - clicked: disabled ? {} : { - background: background(layer ?? theme.lowest, color, "pressed"), - color: foreground(layer ?? theme.lowest, color, "pressed"), - }, }, }) } diff --git a/styles/src/element/index.ts b/styles/src/element/index.ts index d41b4e2cc3..0586399fb1 100644 --- a/styles/src/element/index.ts +++ b/styles/src/element/index.ts @@ -1,4 +1,6 @@ import { interactive, Interactive } from "./interactive" import { toggleable, Toggleable } from "./toggle" +export * from "./padding" +export * from "./margin" export { interactive, Interactive, toggleable, Toggleable } diff --git a/styles/src/component/margin.ts b/styles/src/element/margin.ts similarity index 54% rename from styles/src/component/margin.ts rename to styles/src/element/margin.ts index f6262405f0..5bbdd646a8 100644 --- a/styles/src/component/margin.ts +++ b/styles/src/element/margin.ts @@ -16,19 +16,26 @@ export type MarginStyle = { export const margin_style = (options: MarginOptions): MarginStyle => { const { all, top, bottom, left, right } = options - if (all !== undefined) return { - top: all, - bottom: all, - left: all, - right: all - } + if (all !== undefined) + return { + top: all, + bottom: all, + left: all, + right: all, + } - if (top === undefined && bottom === undefined && left === undefined && right === undefined) throw new Error("Margin must have at least one value") + if ( + top === undefined && + bottom === undefined && + left === undefined && + right === undefined + ) + throw new Error("Margin must have at least one value") return { top: top || 0, bottom: bottom || 0, left: left || 0, - right: right || 0 + right: right || 0, } } diff --git a/styles/src/component/padding.ts b/styles/src/element/padding.ts similarity index 54% rename from styles/src/component/padding.ts rename to styles/src/element/padding.ts index 96792bf766..b94e263922 100644 --- a/styles/src/component/padding.ts +++ b/styles/src/element/padding.ts @@ -16,19 +16,26 @@ export type PaddingStyle = { export const padding_style = (options: PaddingOptions): PaddingStyle => { const { all, top, bottom, left, right } = options - if (all !== undefined) return { - top: all, - bottom: all, - left: all, - right: all - } + if (all !== undefined) + return { + top: all, + bottom: all, + left: all, + right: all, + } - if (top === undefined && bottom === undefined && left === undefined && right === undefined) throw new Error("Padding must have at least one value") + if ( + top === undefined && + bottom === undefined && + left === undefined && + right === undefined + ) + throw new Error("Padding must have at least one value") return { top: top || 0, bottom: bottom || 0, left: left || 0, - right: right || 0 + right: right || 0, } } diff --git a/styles/src/style_tree/assistant.ts b/styles/src/style_tree/assistant.ts index cfc1f8d813..7a41f45e53 100644 --- a/styles/src/style_tree/assistant.ts +++ b/styles/src/style_tree/assistant.ts @@ -1,5 +1,5 @@ import { text, border, background, foreground, TextStyle } from "./components" -import { Interactive, interactive } from "../element" +import { Interactive, interactive, toggleable } from "../element" import { tab_bar_button } from "../component/tab_bar_button" import { StyleSets, useTheme } from "../theme" @@ -8,50 +8,48 @@ type RoleCycleButton = TextStyle & { } // TODO: Replace these with zed types type RemainingTokens = TextStyle & { - background: string, - margin: { top: number, right: number }, + background: string + margin: { top: number; right: number } padding: { - right: number, - left: number, - top: number, - bottom: number, - }, - corner_radius: number, + right: number + left: number + top: number + bottom: number + } + corner_radius: number } export default function assistant(): any { const theme = useTheme() - const interactive_role = (color: StyleSets): Interactive => { - return ( - interactive({ - base: { + const interactive_role = ( + color: StyleSets + ): Interactive => { + return interactive({ + base: { + ...text(theme.highest, "sans", color, { size: "sm" }), + }, + state: { + hovered: { ...text(theme.highest, "sans", color, { size: "sm" }), + background: background(theme.highest, color, "hovered"), }, - state: { - hovered: { - ...text(theme.highest, "sans", color, { size: "sm" }), - background: background(theme.highest, color, "hovered"), - }, - clicked: { - ...text(theme.highest, "sans", color, { size: "sm" }), - background: background(theme.highest, color, "pressed"), - } + clicked: { + ...text(theme.highest, "sans", color, { size: "sm" }), + background: background(theme.highest, color, "pressed"), }, - }) - ) + }, + }) } const tokens_remaining = (color: StyleSets): RemainingTokens => { - return ( - { - ...text(theme.highest, "mono", color, { size: "xs" }), - background: background(theme.highest, "on", "default"), - margin: { top: 12, right: 20 }, - padding: { right: 4, left: 4, top: 1, bottom: 1 }, - corner_radius: 6, - } - ) + return { + ...text(theme.highest, "mono", color, { size: "xs" }), + background: background(theme.highest, "on", "default"), + margin: { top: 12, right: 20 }, + padding: { right: 4, left: 4, top: 1, bottom: 1 }, + corner_radius: 6, + } } return { @@ -59,6 +57,85 @@ export default function assistant(): any { background: background(theme.highest), padding: { left: 12 }, }, + inline: { + background: background(theme.highest), + margin: { top: 3, bottom: 3 }, + border: border(theme.lowest, "on", { + top: true, + bottom: true, + overlay: true, + }), + editor: { + text: text(theme.highest, "mono", "default", { size: "sm" }), + placeholder_text: text(theme.highest, "sans", "on", "disabled"), + selection: theme.players[0], + }, + disabled_editor: { + text: text(theme.highest, "mono", "disabled", { size: "sm" }), + placeholder_text: text(theme.highest, "sans", "on", "disabled"), + selection: { + cursor: text(theme.highest, "mono", "disabled").color, + selection: theme.players[0].selection, + }, + }, + pending_edit_background: background(theme.highest, "positive"), + include_conversation: toggleable({ + base: interactive({ + base: { + icon_size: 12, + color: foreground(theme.highest, "variant"), + + button_width: 12, + background: background(theme.highest, "on"), + corner_radius: 2, + border: { + width: 1., color: background(theme.highest, "on") + }, + padding: { + left: 4, + right: 4, + top: 4, + bottom: 4, + }, + }, + state: { + hovered: { + ...text(theme.highest, "mono", "variant", "hovered"), + background: background(theme.highest, "on", "hovered"), + border: { + width: 1., color: background(theme.highest, "on", "hovered") + }, + }, + clicked: { + ...text(theme.highest, "mono", "variant", "pressed"), + background: background(theme.highest, "on", "pressed"), + border: { + width: 1., color: background(theme.highest, "on", "pressed") + }, + }, + }, + }), + state: { + active: { + default: { + icon_size: 12, + button_width: 12, + color: foreground(theme.highest, "variant"), + background: background(theme.highest, "accent"), + border: border(theme.highest, "accent"), + }, + hovered: { + background: background(theme.highest, "accent", "hovered"), + border: border(theme.highest, "accent", "hovered"), + }, + clicked: { + background: background(theme.highest, "accent", "pressed"), + border: border(theme.highest, "accent", "pressed"), + }, + }, + }, + }), + }, message_header: { margin: { bottom: 4, top: 4 }, background: background(theme.highest), @@ -93,7 +170,10 @@ export default function assistant(): any { base: { background: background(theme.middle), padding: { top: 4, bottom: 4 }, - border: border(theme.middle, "default", { top: true, overlay: true }), + border: border(theme.middle, "default", { + top: true, + overlay: true, + }), }, state: { hovered: { @@ -101,7 +181,7 @@ export default function assistant(): any { }, clicked: { background: background(theme.middle, "pressed"), - } + }, }, }), saved_at: { diff --git a/styles/src/style_tree/collab_modals.ts b/styles/src/style_tree/collab_modals.ts index 0f50e01a39..f9b22b6867 100644 --- a/styles/src/style_tree/collab_modals.ts +++ b/styles/src/style_tree/collab_modals.ts @@ -39,7 +39,12 @@ export default function channel_modal(): any { row_height: ITEM_HEIGHT, header: { background: background(theme.lowest), - border: border(theme.middle, { "bottom": true, "top": false, left: false, right: false }), + border: border(theme.middle, { + bottom: true, + top: false, + left: false, + right: false, + }), padding: { top: SPACING, left: SPACING - BUTTON_OFFSET, @@ -48,7 +53,7 @@ export default function channel_modal(): any { corner_radii: { top_right: 12, top_left: 12, - } + }, }, body: { background: background(theme.middle), @@ -57,12 +62,11 @@ export default function channel_modal(): any { left: SPACING, right: SPACING, bottom: SPACING, - }, corner_radii: { bottom_right: 12, bottom_left: 12, - } + }, }, modal: { background: background(theme.middle), @@ -74,7 +78,6 @@ export default function channel_modal(): any { right: 0, top: 0, }, - }, // FIXME: due to a bug in the picker's size calculation, this must be 600 max_height: 600, @@ -83,7 +86,7 @@ export default function channel_modal(): any { ...text(theme.middle, "sans", "on", { size: "lg" }), padding: { left: BUTTON_OFFSET, - } + }, }, picker: { empty_container: {}, @@ -108,8 +111,8 @@ export default function channel_modal(): any { background: background(theme.middle), padding: { left: 7, - right: 7 - } + right: 7, + }, }, cancel_invite_button: { ...text(theme.middle, "sans", { size: "xs" }), @@ -125,7 +128,7 @@ export default function channel_modal(): any { padding: { left: 4, right: 4, - } + }, }, contact_avatar: { corner_radius: 10, @@ -147,6 +150,6 @@ export default function channel_modal(): any { background: background(theme.middle, "disabled"), color: foreground(theme.middle, "disabled"), }, - } + }, } } diff --git a/styles/src/style_tree/collab_panel.ts b/styles/src/style_tree/collab_panel.ts index 07f367c8af..4d605d118c 100644 --- a/styles/src/style_tree/collab_panel.ts +++ b/styles/src/style_tree/collab_panel.ts @@ -27,7 +27,7 @@ export default function contacts_panel(): any { color: foreground(layer, "on"), icon_width: 14, button_width: 16, - corner_radius: 8 + corner_radius: 8, } const project_row = { @@ -61,7 +61,7 @@ export default function contacts_panel(): any { width: 14, } - const header_icon_button = toggleable_icon_button(theme, { + const header_icon_button = toggleable_icon_button({ variant: "ghost", size: "sm", active_layer: theme.lowest, @@ -275,7 +275,7 @@ export default function contacts_panel(): any { list_empty_label_container: { margin: { left: NAME_MARGIN, - } + }, }, list_empty_icon: { color: foreground(layer, "variant"), @@ -289,7 +289,7 @@ export default function contacts_panel(): any { top: SPACING / 2, bottom: SPACING / 2, left: SPACING, - right: SPACING + right: SPACING, }, }, state: { @@ -330,7 +330,7 @@ export default function contacts_panel(): any { right: 4, }, background: background(layer, "hovered"), - ...text(layer, "sans", "hovered", { size: "xs" }) + ...text(layer, "sans", "hovered", { size: "xs" }), }, contact_status_free: indicator({ layer, color: "positive" }), contact_status_busy: indicator({ layer, color: "negative" }), @@ -404,7 +404,7 @@ export default function contacts_panel(): any { channel_editor: { padding: { left: NAME_MARGIN, - } - } + }, + }, } } diff --git a/styles/src/style_tree/component_test.ts b/styles/src/style_tree/component_test.ts index e2bb0915c1..71057c67ea 100644 --- a/styles/src/style_tree/component_test.ts +++ b/styles/src/style_tree/component_test.ts @@ -1,4 +1,3 @@ - import { useTheme } from "../common" import { text_button } from "../component/text_button" import { icon_button } from "../component/icon_button" @@ -14,14 +13,14 @@ export default function contacts_panel(): any { base: text_button({}), state: { active: { - ...text_button({ color: "accent" }) - } - } + ...text_button({ color: "accent" }), + }, + }, }), disclosure: { ...text(theme.lowest, "sans", "base"), button: icon_button({ variant: "ghost" }), spacing: 4, - } + }, } } diff --git a/styles/src/style_tree/contacts_popover.ts b/styles/src/style_tree/contacts_popover.ts index 0e76bbb38a..dcd84c3252 100644 --- a/styles/src/style_tree/contacts_popover.ts +++ b/styles/src/style_tree/contacts_popover.ts @@ -3,5 +3,4 @@ import { background, border } from "./components" export default function contacts_popover(): any { const theme = useTheme() - } diff --git a/styles/src/style_tree/editor.ts b/styles/src/style_tree/editor.ts index 9277a2e7a1..8fd512c5d4 100644 --- a/styles/src/style_tree/editor.ts +++ b/styles/src/style_tree/editor.ts @@ -206,6 +206,9 @@ export default function editor(): any { match_highlight: foreground(theme.middle, "accent", "active"), background: background(theme.middle, "active"), }, + server_name_container: { padding: { left: 40 } }, + server_name_color: text(theme.middle, "sans", "disabled", {}).color, + server_name_size_percent: 0.75, }, diagnostic_header: { background: background(theme.middle), @@ -307,7 +310,7 @@ export default function editor(): any { ? with_opacity(theme.ramps.green(0.5).hex(), 0.8) : with_opacity(theme.ramps.green(0.4).hex(), 0.8), }, - selections: foreground(layer, "accent") + selections: foreground(layer, "accent"), }, composition_mark: { underline: { diff --git a/styles/src/style_tree/feedback.ts b/styles/src/style_tree/feedback.ts index b1bd96e165..0349359533 100644 --- a/styles/src/style_tree/feedback.ts +++ b/styles/src/style_tree/feedback.ts @@ -37,7 +37,7 @@ export default function feedback(): any { ...text(theme.highest, "mono", "on", "disabled"), background: background(theme.highest, "on", "disabled"), border: border(theme.highest, "on", "disabled"), - } + }, }, }), button_margin: 8, diff --git a/styles/src/style_tree/picker.ts b/styles/src/style_tree/picker.ts index 28ae854787..317f600b1e 100644 --- a/styles/src/style_tree/picker.ts +++ b/styles/src/style_tree/picker.ts @@ -152,7 +152,7 @@ export default function picker(): any { 0.5 ), }, - } + }, }), } } diff --git a/styles/src/style_tree/project_panel.ts b/styles/src/style_tree/project_panel.ts index e239f9a840..51958af145 100644 --- a/styles/src/style_tree/project_panel.ts +++ b/styles/src/style_tree/project_panel.ts @@ -64,17 +64,17 @@ export default function project_panel(): any { const unselected_default_style = merge( base_properties, unselected?.default ?? {}, - {}, + {} ) const unselected_hovered_style = merge( base_properties, { background: background(theme.middle, "hovered") }, - unselected?.hovered ?? {}, + unselected?.hovered ?? {} ) const unselected_clicked_style = merge( base_properties, { background: background(theme.middle, "pressed") }, - unselected?.clicked ?? {}, + unselected?.clicked ?? {} ) const selected_default_style = merge( base_properties, @@ -82,7 +82,7 @@ export default function project_panel(): any { background: background(theme.lowest), text: text(theme.lowest, "sans", { size: "sm" }), }, - selected_style?.default ?? {}, + selected_style?.default ?? {} ) const selected_hovered_style = merge( base_properties, @@ -90,7 +90,7 @@ export default function project_panel(): any { background: background(theme.lowest, "hovered"), text: text(theme.lowest, "sans", { size: "sm" }), }, - selected_style?.hovered ?? {}, + selected_style?.hovered ?? {} ) const selected_clicked_style = merge( base_properties, @@ -98,7 +98,7 @@ export default function project_panel(): any { background: background(theme.lowest, "pressed"), text: text(theme.lowest, "sans", { size: "sm" }), }, - selected_style?.clicked ?? {}, + selected_style?.clicked ?? {} ) return toggleable({ @@ -175,7 +175,7 @@ export default function project_panel(): any { default: { icon_color: foreground(theme.middle, "variant"), }, - }, + } ), cut_entry: entry( { @@ -190,7 +190,7 @@ export default function project_panel(): any { size: "sm", }), }, - }, + } ), filename_editor: { background: background(theme.middle, "on"), diff --git a/styles/src/style_tree/search.ts b/styles/src/style_tree/search.ts index c37a4e4b9a..a93aab4ea8 100644 --- a/styles/src/style_tree/search.ts +++ b/styles/src/style_tree/search.ts @@ -31,7 +31,7 @@ export default function search(): any { text: text(theme.highest, "mono", "default"), border: border(theme.highest), margin: { - right: 9, + right: SEARCH_ROW_SPACING, }, padding: { top: 4, @@ -48,7 +48,7 @@ export default function search(): any { } return { - padding: { top: 4, bottom: 4 }, + padding: { top: 0, bottom: 0 }, option_button: toggleable({ base: interactive({ @@ -60,7 +60,8 @@ export default function search(): any { corner_radius: 2, margin: { right: 2 }, border: { - width: 1., color: background(theme.highest, "on") + width: 1, + color: background(theme.highest, "on"), }, padding: { left: 4, @@ -74,14 +75,16 @@ export default function search(): any { ...text(theme.highest, "mono", "variant", "hovered"), background: background(theme.highest, "on", "hovered"), border: { - width: 1., color: background(theme.highest, "on", "hovered") + width: 1, + color: background(theme.highest, "on", "hovered"), }, }, clicked: { ...text(theme.highest, "mono", "variant", "pressed"), background: background(theme.highest, "on", "pressed"), border: { - width: 1., color: background(theme.highest, "on", "pressed") + width: 1, + color: background(theme.highest, "on", "pressed"), }, }, }, @@ -96,11 +99,19 @@ export default function search(): any { border: border(theme.highest, "accent"), }, hovered: { - background: background(theme.highest, "accent", "hovered"), + background: background( + theme.highest, + "accent", + "hovered" + ), border: border(theme.highest, "accent", "hovered"), }, clicked: { - background: background(theme.highest, "accent", "pressed"), + background: background( + theme.highest, + "accent", + "pressed" + ), border: border(theme.highest, "accent", "pressed"), }, }, @@ -117,7 +128,8 @@ export default function search(): any { corner_radius: 2, margin: { right: 2 }, border: { - width: 1., color: background(theme.highest, "on") + width: 1, + color: background(theme.highest, "on"), }, padding: { left: 4, @@ -131,14 +143,16 @@ export default function search(): any { ...text(theme.highest, "mono", "variant", "hovered"), background: background(theme.highest, "on", "hovered"), border: { - width: 1., color: background(theme.highest, "on", "hovered") + width: 1, + color: background(theme.highest, "on", "hovered"), }, }, clicked: { ...text(theme.highest, "mono", "variant", "pressed"), background: background(theme.highest, "on", "pressed"), border: { - width: 1., color: background(theme.highest, "on", "pressed") + width: 1, + color: background(theme.highest, "on", "pressed"), }, }, }, @@ -153,11 +167,19 @@ export default function search(): any { border: border(theme.highest, "accent"), }, hovered: { - background: background(theme.highest, "accent", "hovered"), + background: background( + theme.highest, + "accent", + "hovered" + ), border: border(theme.highest, "accent", "hovered"), }, clicked: { - background: background(theme.highest, "accent", "pressed"), + background: background( + theme.highest, + "accent", + "pressed" + ), border: border(theme.highest, "accent", "pressed"), }, }, @@ -168,9 +190,20 @@ export default function search(): any { // Disabled elements should use a disabled state of an interactive element, not a toggleable element with the inactive state being disabled action_button: toggleable({ state: { - inactive: text_button({ variant: "ghost", layer: theme.highest, disabled: true, margin: { right: SEARCH_ROW_SPACING }, text_properties: { size: "sm" } }), - active: text_button({ variant: "ghost", layer: theme.highest, margin: { right: SEARCH_ROW_SPACING }, text_properties: { size: "sm" } }) - } + inactive: text_button({ + variant: "ghost", + layer: theme.highest, + disabled: true, + margin: { right: SEARCH_ROW_SPACING }, + text_properties: { size: "sm" }, + }), + active: text_button({ + variant: "ghost", + layer: theme.highest, + margin: { right: SEARCH_ROW_SPACING }, + text_properties: { size: "sm" }, + }), + }, }), editor, invalid_editor: { @@ -216,12 +249,12 @@ export default function search(): any { dimensions: { width: 14, height: 14, - } + }, }, container: { margin: { right: 4 }, padding: { left: 1, right: 1 }, - } + }, }, // Toggle group buttons - Text | Regex | Semantic mode_button: toggleable({ @@ -233,27 +266,39 @@ export default function search(): any { border: { ...border(theme.highest, "on"), left: false, - right: false + right: false, }, margin: { top: 1, bottom: 1, }, padding: { - left: 12, - right: 12, + left: 10, + right: 10, }, corner_radius: 6, }, state: { hovered: { - ...text(theme.highest, "mono", "variant", "hovered", { size: "sm" }), - background: background(theme.highest, "variant", "hovered"), + ...text(theme.highest, "mono", "variant", "hovered", { + size: "sm", + }), + background: background( + theme.highest, + "variant", + "hovered" + ), border: border(theme.highest, "on", "hovered"), }, clicked: { - ...text(theme.highest, "mono", "variant", "pressed", { size: "sm" }), - background: background(theme.highest, "variant", "pressed"), + ...text(theme.highest, "mono", "variant", "pressed", { + size: "sm", + }), + background: background( + theme.highest, + "variant", + "pressed" + ), border: border(theme.highest, "on", "pressed"), }, }, @@ -262,15 +307,19 @@ export default function search(): any { active: { default: { ...text(theme.highest, "mono", "on", { size: "sm" }), - background: background(theme.highest, "on") + background: background(theme.highest, "on"), }, hovered: { - ...text(theme.highest, "mono", "on", "hovered", { size: "sm" }), - background: background(theme.highest, "on", "hovered") + ...text(theme.highest, "mono", "on", "hovered", { + size: "sm", + }), + background: background(theme.highest, "on", "hovered"), }, clicked: { - ...text(theme.highest, "mono", "on", "pressed", { size: "sm" }), - background: background(theme.highest, "on", "pressed") + ...text(theme.highest, "mono", "on", "pressed", { + size: "sm", + }), + background: background(theme.highest, "on", "pressed"), }, }, }, @@ -300,8 +349,8 @@ export default function search(): any { }, }, state: { - hovered: {} - } + hovered: {}, + }, }), active: interactive({ base: { @@ -325,22 +374,30 @@ export default function search(): any { state: { hovered: { ...text(theme.highest, "mono", "on", "hovered"), - background: background(theme.highest, "on", "hovered"), + background: background( + theme.highest, + "on", + "hovered" + ), border: border(theme.highest, "on", "hovered"), }, clicked: { ...text(theme.highest, "mono", "on", "pressed"), - background: background(theme.highest, "on", "pressed"), + background: background( + theme.highest, + "on", + "pressed" + ), border: border(theme.highest, "on", "pressed"), }, }, - }) - } + }), + }, }), - search_bar_row_height: 34, + search_bar_row_height: 32, search_row_spacing: 8, option_button_height: 22, modes_container: {}, - ...search_results() + ...search_results(), } } diff --git a/styles/src/style_tree/status_bar.ts b/styles/src/style_tree/status_bar.ts index 2d3b81f7c2..b279bbac14 100644 --- a/styles/src/style_tree/status_bar.ts +++ b/styles/src/style_tree/status_bar.ts @@ -34,9 +34,11 @@ export default function status_bar(): any { ...text(layer, "mono", "base", { size: "xs" }), }, active_language: text_button({ - color: "base" + color: "base", + }), + auto_update_progress_message: text(layer, "sans", "base", { + size: "xs", }), - auto_update_progress_message: text(layer, "sans", "base", { size: "xs" }), auto_update_done_message: text(layer, "sans", "base", { size: "xs" }), lsp_status: interactive({ base: { @@ -73,34 +75,36 @@ export default function status_bar(): any { icon_color_error: foreground(layer, "negative"), container_ok: { corner_radius: 6, - padding: { top: 3, bottom: 3, left: 7, right: 7 }, - }, - container_warning: { - ...diagnostic_status_container, - background: background(layer, "warning"), - border: border(layer, "warning"), - }, - container_error: { - ...diagnostic_status_container, - background: background(layer, "negative"), - border: border(layer, "negative"), + padding: { top: 2, bottom: 2, left: 6, right: 6 }, }, + container_warning: diagnostic_status_container, + container_error: diagnostic_status_container }, state: { hovered: { icon_color_ok: foreground(layer, "on"), container_ok: { - background: background(layer, "on", "hovered"), + background: background(layer, "hovered") }, container_warning: { - background: background(layer, "warning", "hovered"), - border: border(layer, "warning", "hovered"), + background: background(layer, "hovered") }, container_error: { - background: background(layer, "negative", "hovered"), - border: border(layer, "negative", "hovered"), + background: background(layer, "hovered") }, }, + clicked: { + icon_color_ok: foreground(layer, "on"), + container_ok: { + background: background(layer, "pressed") + }, + container_warning: { + background: background(layer, "pressed") + }, + container_error: { + background: background(layer, "pressed") + } + } }, }), panel_buttons: { @@ -125,7 +129,7 @@ export default function status_bar(): any { }, clicked: { background: background(layer, "pressed"), - } + }, }, }), state: { diff --git a/styles/src/style_tree/tab_bar.ts b/styles/src/style_tree/tab_bar.ts index 129bd17869..23ff03a6a3 100644 --- a/styles/src/style_tree/tab_bar.ts +++ b/styles/src/style_tree/tab_bar.ts @@ -93,7 +93,7 @@ export default function tab_bar(): any { border: border(theme.lowest, "on", { bottom: true, overlay: true, - }) + }), }, state: { hovered: { @@ -101,7 +101,7 @@ export default function tab_bar(): any { background: background(theme.highest, "on", "hovered"), }, disabled: { - color: foreground(theme.highest, "on", "disabled") + color: foreground(theme.highest, "on", "disabled"), }, }, }) @@ -162,6 +162,6 @@ export default function tab_bar(): any { right: false, }, }, - nav_button: nav_button + nav_button: nav_button, } } diff --git a/styles/src/style_tree/titlebar.ts b/styles/src/style_tree/titlebar.ts index 0a0b69e596..672907b22c 100644 --- a/styles/src/style_tree/titlebar.ts +++ b/styles/src/style_tree/titlebar.ts @@ -1,8 +1,6 @@ -import { icon_button, toggleable_icon_button } from "../component/icon_button" -import { toggleable_text_button } from "../component/text_button" +import { icon_button, toggleable_icon_button, toggleable_text_button } from "../component" import { interactive, toggleable } from "../element" -import { useTheme } from "../theme" -import { with_opacity } from "../theme/color" +import { useTheme, with_opacity } from "../theme" import { background, border, foreground, text } from "./components" const ITEM_SPACING = 8 @@ -34,16 +32,17 @@ function call_controls() { } return { - toggle_microphone_button: toggleable_icon_button(theme, { + toggle_microphone_button: toggleable_icon_button({ margin: { ...margin_y, left: space.group, right: space.half_item, }, active_color: "negative", + active_background_color: "negative", }), - toggle_speakers_button: toggleable_icon_button(theme, { + toggle_speakers_button: toggleable_icon_button({ margin: { ...margin_y, left: space.half_item, @@ -51,13 +50,14 @@ function call_controls() { }, }), - screen_share_button: toggleable_icon_button(theme, { + screen_share_button: toggleable_icon_button({ margin: { ...margin_y, left: space.half_item, right: space.group, }, active_color: "accent", + active_background_color: "accent", }), muted: foreground(theme.lowest, "negative"), @@ -183,14 +183,12 @@ export function titlebar(): any { height: 400, }, - // Project - project_name_divider: text(theme.lowest, "sans", "variant"), - project_menu_button: toggleable_text_button(theme, { - color: 'base', + color: "base" }), + git_menu_button: toggleable_text_button(theme, { - color: 'variant', + color: "variant", }), // Collaborators @@ -263,7 +261,7 @@ export function titlebar(): any { ...call_controls(), - toggle_contacts_button: toggleable_icon_button(theme, { + toggle_contacts_button: toggleable_icon_button({ margin: { left: ITEM_SPACING, }, diff --git a/styles/src/style_tree/toolbar.ts b/styles/src/style_tree/toolbar.ts new file mode 100644 index 0000000000..7292a220a8 --- /dev/null +++ b/styles/src/style_tree/toolbar.ts @@ -0,0 +1,38 @@ +import { useTheme } from "../common" +import { toggleable_icon_button } from "../component/icon_button" +import { interactive } from "../element" +import { background, border, foreground, text } from "./components" + +export const toolbar = () => { + const theme = useTheme() + + return { + height: 32, + padding: { left: 4, right: 4, top: 4, bottom: 4 }, + background: background(theme.highest), + border: border(theme.highest, { bottom: true }), + item_spacing: 4, + toggleable_tool: toggleable_icon_button({ + margin: { left: 4 }, + variant: "ghost", + active_color: "accent", + }), + breadcrumb_height: 24, + breadcrumbs: interactive({ + base: { + ...text(theme.highest, "sans", "variant"), + corner_radius: 6, + padding: { + left: 6, + right: 6, + }, + }, + state: { + hovered: { + color: foreground(theme.highest, "on", "hovered"), + background: background(theme.highest, "on", "hovered"), + }, + }, + }), + } +} diff --git a/styles/src/style_tree/workspace.ts b/styles/src/style_tree/workspace.ts index 43a6cec585..ba89c7b05f 100644 --- a/styles/src/style_tree/workspace.ts +++ b/styles/src/style_tree/workspace.ts @@ -12,7 +12,7 @@ import tabBar from "./tab_bar" import { interactive } from "../element" import { titlebar } from "./titlebar" import { useTheme } from "../theme" -import { toggleable_icon_button } from "../component/icon_button" +import { toolbar } from "./toolbar" export default function workspace(): any { const theme = useTheme() @@ -128,35 +128,7 @@ export default function workspace(): any { }, status_bar: statusBar(), titlebar: titlebar(), - toolbar: { - height: 42, - background: background(theme.highest), - border: border(theme.highest, { bottom: true }), - item_spacing: 8, - toggleable_tool: toggleable_icon_button(theme, { - margin: { left: 8 }, - variant: "ghost", - active_color: "accent", - }), - padding: { left: 8, right: 8 }, - }, - breadcrumb_height: 24, - breadcrumbs: interactive({ - base: { - ...text(theme.highest, "sans", "variant"), - corner_radius: 6, - padding: { - left: 6, - right: 6, - }, - }, - state: { - hovered: { - color: foreground(theme.highest, "on", "hovered"), - background: background(theme.highest, "on", "hovered"), - }, - }, - }), + toolbar: toolbar(), disconnected_overlay: { ...text(theme.lowest, "sans"), background: with_opacity(background(theme.lowest), 0.8), diff --git a/styles/src/theme/create_theme.ts b/styles/src/theme/create_theme.ts index ab3c96f280..61471616fb 100644 --- a/styles/src/theme/create_theme.ts +++ b/styles/src/theme/create_theme.ts @@ -13,16 +13,16 @@ export interface Theme { is_light: boolean /** - * App background, other elements that should sit directly on top of the background. - */ + * App background, other elements that should sit directly on top of the background. + */ lowest: Layer /** - * Panels, tabs, other UI surfaces that sit on top of the background. - */ + * Panels, tabs, other UI surfaces that sit on top of the background. + */ middle: Layer /** - * Editors like code buffers, conversation editors, etc. - */ + * Editors like code buffers, conversation editors, etc. + */ highest: Layer ramps: RampSet @@ -206,7 +206,10 @@ function build_color_family(ramps: RampSet): ColorFamily { for (const ramp in ramps) { const ramp_value = ramps[ramp as keyof RampSet] - const lightnessValues = [ramp_value(0).get('hsl.l') * 100, ramp_value(1).get('hsl.l') * 100] + const lightnessValues = [ + ramp_value(0).get("hsl.l") * 100, + ramp_value(1).get("hsl.l") * 100, + ] const low = Math.min(...lightnessValues) const high = Math.max(...lightnessValues) const range = high - low diff --git a/styles/src/theme/index.ts b/styles/src/theme/index.ts index ca8aaa461f..47110940f5 100644 --- a/styles/src/theme/index.ts +++ b/styles/src/theme/index.ts @@ -23,3 +23,4 @@ export * from "./create_theme" export * from "./ramps" export * from "./syntax" export * from "./theme_config" +export * from "./color" diff --git a/styles/src/theme/tokens/theme.ts b/styles/src/theme/tokens/theme.ts index f759bc8139..e2c3bb33d3 100644 --- a/styles/src/theme/tokens/theme.ts +++ b/styles/src/theme/tokens/theme.ts @@ -4,11 +4,7 @@ import { SingleOtherToken, TokenTypes, } from "@tokens-studio/types" -import { - Shadow, - SyntaxHighlightStyle, - ThemeSyntax, -} from "../create_theme" +import { Shadow, SyntaxHighlightStyle, ThemeSyntax } from "../create_theme" import { LayerToken, layer_token } from "./layer" import { PlayersToken, players_token } from "./players" import { color_token } from "./token" diff --git a/styles/tsconfig.json b/styles/tsconfig.json index c7eaa50eed..940442e1b7 100644 --- a/styles/tsconfig.json +++ b/styles/tsconfig.json @@ -23,7 +23,5 @@ "skipLibCheck": true, "useUnknownInCatchVariables": false }, - "exclude": [ - "node_modules" - ] + "exclude": ["node_modules"] }