Merge branch 'main' into vim-softwrap-word

Conrad Irwin created

Change summary

Cargo.lock                                              |   16 
assets/icons/check_circle.svg                           |    4 
assets/icons/error.svg                                  |    4 
assets/icons/warning.svg                                |    7 
assets/keymaps/default.json                             |    5 
crates/ai/Cargo.toml                                    |    7 
crates/ai/src/ai.rs                                     |  115 +
crates/ai/src/assistant.rs                              | 1058 +++++++++-
crates/ai/src/streaming_diff.rs                         |  293 +++
crates/breadcrumbs/src/breadcrumbs.rs                   |    8 
crates/clock/src/clock.rs                               |   81 
crates/collab/Cargo.toml                                |    2 
crates/collab/src/db/queries/buffers.rs                 |   62 
crates/collab/src/db/queries/users.rs                   |    2 
crates/collab/src/tests/integration_tests.rs            |  183 -
crates/collab/src/tests/randomized_integration_tests.rs |    2 
crates/collab_ui/src/channel_view.rs                    |    2 
crates/collab_ui/src/collab_panel.rs                    |   84 
crates/collab_ui/src/collab_titlebar_item.rs            |   75 
crates/copilot/src/copilot.rs                           |    2 
crates/editor/src/blink_manager.rs                      |   13 
crates/editor/src/editor.rs                             |  183 +
crates/editor/src/editor_tests.rs                       |  102 +
crates/editor/src/element.rs                            |    2 
crates/editor/src/movement.rs                           |   25 
crates/editor/src/multi_buffer.rs                       |  111 +
crates/editor/src/scroll/scroll_amount.rs               |    2 
crates/editor/src/test/editor_lsp_test_context.rs       |    2 
crates/fs/Cargo.toml                                    |    1 
crates/fs/src/fs.rs                                     |   67 
crates/language/src/buffer.rs                           |   48 
crates/language/src/buffer_tests.rs                     |    2 
crates/language/src/language.rs                         |   96 
crates/language/src/proto.rs                            |  132 
crates/language_tools/src/lsp_log.rs                    |   74 
crates/lsp/Cargo.toml                                   |    2 
crates/lsp/src/lsp.rs                                   |  116 
crates/project/src/lsp_command.rs                       |   74 
crates/project/src/project.rs                           |  261 +
crates/project/src/project_tests.rs                     |   30 
crates/project/src/search.rs                            |    8 
crates/project/src/worktree.rs                          |    4 
crates/quick_action_bar/Cargo.toml                      |    1 
crates/quick_action_bar/src/quick_action_bar.rs         |   23 
crates/rope/src/rope.rs                                 |   10 
crates/rpc/proto/zed.proto                              |   29 
crates/rpc/src/rpc.rs                                   |    2 
crates/text/Cargo.toml                                  |    2 
crates/text/src/anchor.rs                               |    6 
crates/text/src/text.rs                                 |  320 +-
crates/text/src/undo_map.rs                             |   12 
crates/theme/src/theme.rs                               |   19 
crates/vim/src/motion.rs                                |   23 
crates/vim/src/normal/change.rs                         |   11 
crates/vim/src/normal/scroll.rs                         |   62 
crates/vim/src/object.rs                                |   31 
crates/zed/Cargo.toml                                   |    2 
crates/zed/src/languages.rs                             |   18 
crates/zed/src/languages/c.rs                           |    4 
crates/zed/src/languages/css.rs                         |  130 +
crates/zed/src/languages/css/config.toml                |    1 
crates/zed/src/languages/elixir.rs                      |    4 
crates/zed/src/languages/go.rs                          |    4 
crates/zed/src/languages/html.rs                        |    4 
crates/zed/src/languages/html/config.toml               |    1 
crates/zed/src/languages/javascript/config.toml         |    5 
crates/zed/src/languages/json.rs                        |   49 
crates/zed/src/languages/language_plugin.rs             |    4 
crates/zed/src/languages/lua.rs                         |   17 
crates/zed/src/languages/php.rs                         |    4 
crates/zed/src/languages/python.rs                      |    4 
crates/zed/src/languages/ruby.rs                        |    4 
crates/zed/src/languages/rust.rs                        |    4 
crates/zed/src/languages/svelte.rs                      |    4 
crates/zed/src/languages/tailwind.rs                    |  161 +
crates/zed/src/languages/tsx/config.toml                |    5 
crates/zed/src/languages/typescript.rs                  |   30 
crates/zed/src/languages/yaml.rs                        |   27 
crates/zed/src/zed.rs                                   |    5 
styles/src/build_themes.ts                              |    9 
styles/src/build_tokens.ts                              |    4 
styles/src/component/button.ts                          |   55 
styles/src/component/icon_button.ts                     |   52 
styles/src/component/index.ts                           |    6 
styles/src/component/indicator.ts                       |    8 
styles/src/component/input.ts                           |    2 
styles/src/component/tab.ts                             |   20 
styles/src/component/tab_bar_button.ts                  |   67 
styles/src/component/text_button.ts                     |   52 
styles/src/element/index.ts                             |    2 
styles/src/element/margin.ts                            |   23 
styles/src/element/padding.ts                           |   23 
styles/src/style_tree/assistant.ts                      |  150 +
styles/src/style_tree/collab_modals.ts                  |   23 
styles/src/style_tree/collab_panel.ts                   |   14 
styles/src/style_tree/component_test.ts                 |    9 
styles/src/style_tree/contacts_popover.ts               |    1 
styles/src/style_tree/editor.ts                         |    5 
styles/src/style_tree/feedback.ts                       |    2 
styles/src/style_tree/picker.ts                         |    2 
styles/src/style_tree/project_panel.ts                  |   16 
styles/src/style_tree/search.ts                         |  131 
styles/src/style_tree/status_bar.ts                     |   42 
styles/src/style_tree/tab_bar.ts                        |    6 
styles/src/style_tree/titlebar.ts                       |   24 
styles/src/style_tree/toolbar.ts                        |   38 
styles/src/style_tree/workspace.ts                      |   32 
styles/src/theme/create_theme.ts                        |   17 
styles/src/theme/index.ts                               |    1 
styles/src/theme/tokens/theme.ts                        |    6 
styles/tsconfig.json                                    |    4 
111 files changed, 3,754 insertions(+), 1,501 deletions(-)

Detailed changes

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

assets/icons/check_circle.svg 🔗

@@ -1,4 +1,4 @@
 <svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M5 8L6.5 9L9 5.5" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
-<circle cx="7" cy="7" r="4.875" stroke="black" stroke-width="1.25"/>
+<path d="M5 8L6.5 9L9 5.5" stroke="#11181C" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
+<circle cx="7" cy="7" r="4.875" stroke="#11181C" stroke-width="1.25"/>
 </svg>

assets/icons/error.svg 🔗

@@ -1,4 +1,4 @@
 <svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M8.86396 2C8.99657 2 9.12375 2.05268 9.21751 2.14645L11.8536 4.78249C11.9473 4.87625 12 5.00343 12 5.13604L12 8.86396C12 8.99657 11.9473 9.12375 11.8536 9.21751L9.21751 11.8536C9.12375 11.9473 8.99657 12 8.86396 12L5.13604 12C5.00343 12 4.87625 11.9473 4.78249 11.8536L2.14645 9.21751C2.05268 9.12375 2 8.99657 2 8.86396L2 5.13604C2 5.00343 2.05268 4.87625 2.14645 4.78249L4.78249 2.14645C4.87625 2.05268 5.00343 2 5.13604 2L8.86396 2Z" stroke="black" stroke-width="1.25" stroke-linejoin="round"/>
-<path d="M8.89063 5.10938L5.10937 8.89063M8.89063 8.89063L5.10937 5.10938" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
+<path d="M8.86396 2C8.99657 2 9.12375 2.05268 9.21751 2.14645L11.8536 4.78249C11.9473 4.87625 12 5.00343 12 5.13604L12 8.86396C12 8.99657 11.9473 9.12375 11.8536 9.21751L9.21751 11.8536C9.12375 11.9473 8.99657 12 8.86396 12L5.13604 12C5.00343 12 4.87625 11.9473 4.78249 11.8536L2.14645 9.21751C2.05268 9.12375 2 8.99657 2 8.86396L2 5.13604C2 5.00343 2.05268 4.87625 2.14645 4.78249L4.78249 2.14645C4.87625 2.05268 5.00343 2 5.13604 2L8.86396 2Z" fill="#001A33" fill-opacity="0.157" stroke="#11181C" stroke-width="1.25" stroke-linejoin="round"/>
+<path d="M8.89063 5.10938L5.10937 8.89063M8.89063 8.89063L5.10937 5.10938" stroke="#11181C" stroke-width="1.25" stroke-linecap="round"/>
 </svg>

assets/icons/warning.svg 🔗

@@ -1,5 +1,6 @@
 <svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M9.5 6.5L11.994 11.625C12.1556 11.9571 11.9137 12.3438 11.5444 12.3438H2.45563C2.08628 12.3438 1.84442 11.9571 2.00603 11.625L4.5 6.5" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M7 7L7 2" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
-<circle cx="7" cy="9.24219" r="0.75" fill="black"/>
+<path d="M2.45563 12.3438H11.5444C11.9137 12.3438 12.1556 11.9571 11.994 11.625L10.2346 8.00952C9.77174 7.05841 8.89104 6.37821 7.85383 6.17077C7.29019 6.05804 6.70981 6.05804 6.14617 6.17077C5.10896 6.37821 4.22826 7.05841 3.76542 8.00952L2.00603 11.625C1.84442 11.9571 2.08628 12.3438 2.45563 12.3438Z" fill="#001A33" fill-opacity="0.157"/>
+<path d="M9.5 6.5L11.994 11.625C12.1556 11.9571 11.9137 12.3438 11.5444 12.3438H2.45563C2.08628 12.3438 1.84442 11.9571 2.00603 11.625L4.5 6.5" stroke="#11181C" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M7 7L7 2" stroke="#11181C" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
+<circle cx="7" cy="9.24219" r="0.75" fill="#11181C"/>
 </svg>

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"
     }
   },
   {

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

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<RequestMessage>,
     stream: bool,
@@ -116,7 +121,7 @@ struct RequestMessage {
 }
 
 #[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
-struct ResponseMessage {
+pub struct ResponseMessage {
     role: Option<Role>,
     content: Option<String>,
 }
@@ -150,7 +155,7 @@ impl Display for Role {
 }
 
 #[derive(Deserialize, Debug)]
-struct OpenAIResponseStreamEvent {
+pub struct OpenAIResponseStreamEvent {
     pub id: Option<String>,
     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<String>,
@@ -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<Background>,
+    mut request: OpenAIRequest,
+) -> Result<impl Stream<Item = Result<OpenAIResponseStreamEvent>>> {
+    request.stream = true;
+
+    let (tx, rx) = futures::channel::mpsc::unbounded::<Result<OpenAIResponseStreamEvent>>();
+
+    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<String, io::Error>,
+                ) -> Result<Option<OpenAIResponseStreamEvent>> {
+                    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::<OpenAIResponse>(&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();
+    }
+}

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::<AssistantPanel>(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<LanguageRegistry>,
     fs: Arc<dyn Fs>,
     subscriptions: Vec<Subscription>,
+    next_inline_assist_id: usize,
+    pending_inline_assists: HashMap<usize, PendingInlineAssist>,
+    pending_inline_assist_ids_by_editor: HashMap<WeakViewHandle<Editor>, Vec<usize>>,
+    include_conversation_in_next_inline_assist: bool,
+    inline_prompt_history: VecDeque<String>,
     _watch_saved_conversations: Task<Result<()>>,
 }
 
 impl AssistantPanel {
+    const INLINE_PROMPT_HISTORY_MAX_LEN: usize = 20;
+
     pub fn load(
         workspace: WeakViewHandle<Workspace>,
         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<Workspace>,
+    ) {
+        let this = if let Some(this) = workspace.panel::<AssistantPanel>(cx) {
+            if this
+                .update(cx, |assistant, cx| assistant.load_api_key(cx))
+                .is_some()
+            {
+                this
+            } else {
+                workspace.focus_panel::<AssistantPanel>(cx);
+                return;
+            }
+        } else {
+            return;
+        };
+
+        let active_editor = if let Some(active_editor) = workspace
+            .active_item(cx)
+            .and_then(|item| item.act_as::<Editor>(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<Editor>, cx: &mut ViewContext<Self>) {
+        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::<usize>(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<InlineAssistant>,
+        event: &InlineAssistantEvent,
+        cx: &mut ViewContext<Self>,
+    ) {
+        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<Workspace>,
+    ) {
+        if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
+            if let Some(editor) = workspace
+                .active_item(cx)
+                .and_then(|item| item.downcast::<Editor>())
+            {
+                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>) {
+        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<Self>) {
+        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<Self>,
+    ) {
+        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::<Rope>();
+
+        let selection_start = range.start.to_point(&snapshot);
+        let selection_end = range.end.to_point(&snapshot);
+
+        let mut base_indent: Option<language::IndentSize> = 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::<AssistantSettings>(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<Editor>,
+        cx: &mut ViewContext<Self>,
+    ) {
+        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::<PendingInlineAssist>(cx);
+            } else {
+                editor.highlight_background::<PendingInlineAssist>(
+                    background_ranges,
+                    |theme| theme.assistant.inline.pending_edit_background,
+                    cx,
+                );
+            }
+
+            if foreground_ranges.is_empty() {
+                editor.clear_text_highlights::<PendingInlineAssist>(cx);
+            } else {
+                editor.highlight_text::<PendingInlineAssist>(
+                    foreground_ranges,
+                    HighlightStyle {
+                        fade_out: Some(0.6),
+                        ..Default::default()
+                    },
+                    cx,
+                );
+            }
+        });
+    }
+
     fn new_conversation(&mut self, cx: &mut ViewContext<Self>) -> ViewHandle<ConversationEditor> {
         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<Self>) -> Option<String> {
+        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<AssistantPanel>) -> ViewHandle<Editor> {
@@ -753,27 +1521,7 @@ impl Panel for AssistantPanel {
 
     fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {
         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<Self>,
     ) -> Vec<MessageAnchor> {
         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()
-                                });
-                            }
+                should_assist = true;
+            }
+        }
 
-                            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,
-                };
+        if should_assist {
+            let Some(api_key) = self.api_key.borrow().clone() else {
+                return Default::default();
+            };
 
-                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);
-                }
+            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,
+            };
 
-                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<str> = 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(())
+            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<str> = 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);
                                         });
-                                }
-                                smol::future::yield_now().await;
-                            }
+                                        cx.emit(ConversationEvent::StreamedCompletion);
 
-                            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
+                                        Some(())
                                     });
-                                    this.summarize(cx);
-                                });
+                            }
+                            smol::future::yield_now().await;
+                        }
 
-                            anyhow::Ok(())
-                        };
+                        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);
+                            });
 
-                        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(),
-                                            );
-                                        }
+                        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();
                                 }
-                            });
-                        }
+                                cx.notify();
+                            }
+                        });
                     }
-                }));
-            }
-        }
+                }
+            });
 
-        if !tasks.is_empty() {
             self.pending_completions.push(PendingCompletion {
                 id: post_inc(&mut self.completion_count),
-                _tasks: tasks,
+                _task: task,
             });
         }
 

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<f64>,
+    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<char>,
+    new: Vec<char>,
+    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::<Vec<_>>();
+        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<Hunk> {
+        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<Hunk> {
+        let mut pending_insert: Option<Range<usize>> = 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<Hunk> {
+        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::<String>();
+        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::<String>();
+            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);
+    }
+}

crates/breadcrumbs/src/breadcrumbs.rs 🔗

@@ -50,7 +50,7 @@ impl View for Breadcrumbs {
         let not_editor = active_item.downcast::<editor::Editor>().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 {

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<Item = Local> + '_ {
-        self.0.iter().enumerate().map(|(replica_id, seq)| Local {
+    pub fn iter(&self) -> impl Iterator<Item = Lamport> + '_ {
+        self.0.iter().enumerate().map(|(replica_id, seq)| Lamport {
             replica_id: replica_id as ReplicaId,
             value: *seq,
         })
     }
 }
 
-impl FromIterator<Local> for Global {
-    fn from_iter<T: IntoIterator<Item = Local>>(locals: T) -> Self {
+impl FromIterator<Lamport> for Global {
+    fn from_iter<T: IntoIterator<Item = Lamport>>(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)

crates/collab/Cargo.toml 🔗

@@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathan@zed.dev>"]
 default-run = "collab"
 edition = "2021"
 name = "collab"
-version = "0.18.0"
+version = "0.19.0"
 publish = false
 
 [[bin]]

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<storage::VectorClockEntry>) -> Vec<proto::
 pub fn operation_from_wire(operation: proto::Operation) -> Option<text::Operation> {
     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<text::Operatio
                 .collect(),
             new_text: edit.new_text.into_iter().map(Arc::from).collect(),
         })),
-        proto::operation::Variant::Undo(undo) => 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<text::Operatio
 fn version_from_wire(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 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<VectorClockEntry>,
         #[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,
     }

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

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<Deterministic>,
     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::<lsp::request::InlayHintRequest, _, _>(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::<lsp::request::InlayHintRefreshRequest>(())
         .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<Deterministic>,
     cx_a: &mut TestAppContext,
@@ -8223,35 +8180,34 @@ async fn test_inlay_hint_refresh_is_forwarded(
         .downcast::<Editor>()
         .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::<lsp::request::InlayHintRequest, _, _>(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::<lsp::request::InlayHintRefreshRequest>(())
         .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"
         );
     });

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<dyn Any>, cx: &mut ViewContext<Self>) -> bool {

crates/collab_ui/src/collab_panel.rs 🔗

@@ -1106,23 +1106,17 @@ impl CollabPanel {
     ) -> AnyElement<Self> {
         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::<OpenSharedScreen, _>(
             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(

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::<ToggleVcsMenu, _>(
-                                    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::<BranchPopoverTooltip>(
-                                                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::<ToggleVcsMenu, _>(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::<BranchPopoverTooltip>(
+                                        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()

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,
         ) {
@@ -37,10 +37,7 @@ impl BlinkManager {
     }
 
     pub fn pause_blinking(&mut self, cx: &mut ModelContext<Self>) {
-        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();
         }

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<Buffer>,
+    project: Option<ModelHandle<Project>>,
     completions: Arc<[Completion]>,
     match_candidates: Vec<StringMatchCandidate>,
     matches: Arc<[StringMatch]>,
@@ -863,6 +864,48 @@ impl CompletionsMenu {
     fn render(&self, style: EditorStyle, cx: &mut ViewContext<Editor>) -> AnyElement<Editor> {
         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::<Vec<_>>()
+        });
+        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::<SettingsStore, _>(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::<ThemeSettings>(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<Arc<GetFieldEditorTheme>>,
+        cx: &mut ViewContext<Self>,
+    ) {
+        self.get_field_editor_theme = style;
+        cx.notify();
+    }
+
     pub fn replica_id_map(&self) -> Option<&HashMap<ReplicaId, ReplicaId>> {
         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<Self>) {
+    fn focus_in(&mut self, focused: AnyViewHandle, cx: &mut ViewContext<Self>) {
         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::<Editor>() {
             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<Item = &'a str
         None
     })
     .flat_map(|word| word.split_inclusive('_'))
+    .flat_map(|word| word.split_inclusive('-'))
 }
 
 trait RangeToAnchorExt {

crates/editor/src/editor_tests.rs 🔗

@@ -19,7 +19,8 @@ use gpui::{
 use indoc::indoc;
 use language::{
     language_settings::{AllLanguageSettings, AllLanguageSettingsContent, LanguageSettingsContent},
-    BracketPairConfig, FakeLspAdapter, LanguageConfig, LanguageRegistry, Point,
+    BracketPairConfig, FakeLspAdapter, LanguageConfig, LanguageConfigOverride, LanguageRegistry,
+    Override, Point,
 };
 use parking_lot::Mutex;
 use project::project_settings::{LspSettings, ProjectSettings};
@@ -7688,6 +7689,105 @@ async fn test_completions_with_additional_edits(cx: &mut gpui::TestAppContext) {
     cx.assert_editor_state(indoc! {"fn main() { let a = Some(2)ˇ; }"});
 }
 
+#[gpui::test]
+async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |_| {});
+
+    let mut cx = EditorLspTestContext::new(
+        Language::new(
+            LanguageConfig {
+                path_suffixes: vec!["jsx".into()],
+                overrides: [(
+                    "element".into(),
+                    LanguageConfigOverride {
+                        word_characters: Override::Set(['-'].into_iter().collect()),
+                        ..Default::default()
+                    },
+                )]
+                .into_iter()
+                .collect(),
+                ..Default::default()
+            },
+            Some(tree_sitter_typescript::language_tsx()),
+        )
+        .with_override_query("(jsx_self_closing_element) @element")
+        .unwrap(),
+        lsp::ServerCapabilities {
+            completion_provider: Some(lsp::CompletionOptions {
+                trigger_characters: Some(vec![":".to_string()]),
+                ..Default::default()
+            }),
+            ..Default::default()
+        },
+        cx,
+    )
+    .await;
+
+    cx.lsp
+        .handle_request::<lsp::request::Completion, _, _>(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#"<p class="bgˇ" />"#);
+
+    // 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::<Vec<_>>(),
+                &["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::<Vec<_>>(),
+                &["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#"<p class="yelˇ" />"#);
+    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::<Vec<_>>(),
+                &["bg-yellow"]
+            );
+        } else {
+            panic!("expected completion menu to be open");
+        }
+    });
+}
+
 fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
     let point = DisplayPoint::new(row as u32, column as u32);
     point..point

crates/editor/src/element.rs 🔗

@@ -2251,7 +2251,7 @@ impl Element<Editor> 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.

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

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<Self>,
+    ) {
+        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>) {
         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<Self>) {
+        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<Buffer>,
@@ -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<u32> {
+        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<Transaction> {
+        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<TransactionId> {
         let mut count = 0;
         let mut transactions = self.undo_stack.iter();

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

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

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

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<str>) -> Arc<str> {
-        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");
 }
 

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<Anchor>,
     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<Item = clock::Local>,
+        edit_ids: impl IntoIterator<Item = clock::Lamport>,
     ) -> impl Future<Output = Result<()>> {
         self.text.wait_for_edits(edit_ids)
     }
@@ -1358,7 +1362,7 @@ impl Buffer {
         }
     }
 
-    pub fn set_text<T>(&mut self, text: T, cx: &mut ModelContext<Self>) -> Option<clock::Local>
+    pub fn set_text<T>(&mut self, text: T, cx: &mut ModelContext<Self>) -> Option<clock::Lamport>
     where
         T: Into<Arc<str>>,
     {
@@ -1371,7 +1375,7 @@ impl Buffer {
         edits_iter: I,
         autoindent_mode: Option<AutoindentMode>,
         cx: &mut ModelContext<Self>,
-    ) -> Option<clock::Local>
+    ) -> Option<clock::Lamport>
     where
         I: IntoIterator<Item = (Range<S>, 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<Self>,
+    ) -> 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<Language>>, c: char) -> CharKind {
+pub fn char_kind(scope: &Option<LanguageScope>, 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
 }
 

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

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<str>);
 /// once at startup, and caches the results.
 pub struct CachedLspAdapter {
     pub name: LanguageServerName,
+    pub short_name: &'static str,
     pub initialization_options: Option<Value>,
     pub disk_based_diagnostic_sources: Vec<String>,
     pub disk_based_diagnostics_progress_token: Option<String>,
@@ -100,6 +102,7 @@ pub struct CachedLspAdapter {
 impl CachedLspAdapter {
     pub async fn new(adapter: Arc<dyn LspAdapter>) -> Arc<Self> {
         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<BoxFuture<'static, Value>> {
+    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<BoxFuture<'static, Value>> {
-        None
+    fn workspace_configuration(&self, _: &mut AppContext) -> BoxFuture<'static, Value> {
+        futures::future::ready(serde_json::json!({})).boxed()
     }
 
     fn code_action_kinds(&self) -> Option<Vec<CodeActionKind>> {
@@ -343,6 +346,8 @@ pub struct LanguageConfig {
     #[serde(default)]
     pub block_comment: Option<(Arc<str>, Arc<str>)>,
     #[serde(default)]
+    pub scope_opt_in_language_servers: Vec<String>,
+    #[serde(default)]
     pub overrides: HashMap<String, LanguageConfigOverride>,
     #[serde(default)]
     pub word_characters: HashSet<char>,
@@ -373,6 +378,10 @@ pub struct LanguageConfigOverride {
     pub block_comment: Override<(Arc<str>, Arc<str>)>,
     #[serde(skip_deserializing)]
     pub disabled_bracket_ixs: Vec<u16>,
+    #[serde(default)]
+    pub word_characters: Override<HashSet<char>>,
+    #[serde(default)]
+    pub opt_into_language_servers: Vec<String>,
 }
 
 #[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<serde_json::Value> {
-        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::<Vec<_>>()
-        };
-
-        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<Language>) {
         self.state.write().add(language);
     }
@@ -1383,13 +1358,23 @@ impl Language {
         Ok(self)
     }
 
-    pub fn with_override_query(mut self, source: &str) -> Result<Self> {
+    pub fn with_override_query(mut self, source: &str) -> anyhow::Result<Self> {
         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<char>> {
+        Override::as_option(
+            self.config_override().map(|o| &o.word_characters),
+            Some(&self.language.config.word_characters),
+        )
+    }
+
     pub fn brackets(&self) -> impl Iterator<Item = (&BracketPair, bool)> {
         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<FakeLspAdapter> {
         LanguageServerName(self.name.into())
     }
 
+    fn short_name(&self) -> &'static str {
+        "FakeLspAdapter"
+    }
+
     async fn fetch_latest_server_version(
         &self,
         _: &dyn LspAdapterDelegate,

crates/language/src/proto.rs 🔗

@@ -20,17 +20,17 @@ pub fn deserialize_fingerprint(fingerprint: &str) -> Result<RopeFingerprint> {
         .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::Operati
                 crate::Operation::Buffer(text::Operation::Edit(deserialize_edit_operation(edit)))
             }
             proto::operation::Variant::Undo(undo) => {
-                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<crate::Operati
 
 pub fn deserialize_edit_operation(edit: proto::operation::Edit) -> 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<Anchor> {
     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<CodeAction>
 
 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<Transaction> {
     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<Transa
         edit_ids: transaction
             .edit_ids
             .into_iter()
-            .map(deserialize_local_timestamp)
+            .map(deserialize_timestamp)
             .collect(),
         start: deserialize_version(&transaction.start),
     })
 }
 
-pub fn serialize_local_timestamp(timestamp: clock::Local) -> 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<FullOffset> {
 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,
         });

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<WeakModelHandle<Project>, ProjectState>,
-    io_tx: mpsc::UnboundedSender<(WeakModelHandle<Project>, LanguageServerId, bool, String)>,
+    io_tx: mpsc::UnboundedSender<(WeakModelHandle<Project>, LanguageServerId, IoKind, String)>,
 }
 
 struct ProjectState {
@@ -37,12 +38,12 @@ struct ProjectState {
 struct LanguageServerState {
     log_buffer: ModelHandle<Buffer>,
     rpc_state: Option<LanguageServerRpcState>,
+    _subscription: Option<lsp::Subscription>,
 }
 
 struct LanguageServerRpcState {
     buffer: ModelHandle<Buffer>,
     last_message_kind: Option<MessageKind>,
-    _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<Self>,
     ) -> Option<ModelHandle<Buffer>> {
         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<Project>,
         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<Project>,
         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)?

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

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<dyn Send + FnMut(Option<usize>, &str, AsyncAppContext)>;
 type ResponseHandler = Box<dyn Send + FnOnce(Result<String, Error>)>;
-type IoHandler = Box<dyn Send + FnMut(bool, &str)>;
+type IoHandler = Box<dyn Send + FnMut(IoKind, &str)>;
+
+#[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<Stdin, Stdout, F>(
+    fn new_internal<Stdin, Stdout, Stderr, F>(
         server_id: LanguageServerId,
         stdin: Stdin,
         stdout: Stdout,
+        stderr: Option<Stderr>,
         server: Option<Child>,
         root_path: &Path,
         code_action_kinds: Option<Vec<CodeActionKind>>,
@@ -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::<String>();
         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: Stderr,
+        io_handlers: Arc<Mutex<HashMap<usize, IoHandler>>>,
+    ) -> 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: Stdin,
         outbound_rx: channel::Receiver<String>,
@@ -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<F>(&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<String>,
         executor: &Arc<executor::Background>,
         params: T::Params,
-    ) -> impl 'static + Future<Output = Result<T::Result>>
+    ) -> impl 'static + Future<Output = anyhow::Result<T::Result>>
     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::<async_pipe::PipeReader>,
             None,
             Path::new("/"),
             None,
@@ -862,6 +933,7 @@ impl LanguageServer {
                 LanguageServerId(0),
                 stdout_writer,
                 stdin_reader,
+                None::<async_pipe::PipeReader>,
                 None,
                 Path::new("/"),
                 None,

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<lsp::CompletionResponse>,
         _: ModelHandle<Project>,
         buffer: ModelHandle<Buffer>,
-        _: LanguageServerId,
+        server_id: LanguageServerId,
         cx: AsyncAppContext,
     ) -> Result<Vec<Completion>> {
+        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,
                         }
                     })

crates/project/src/project.rs 🔗

@@ -156,6 +156,11 @@ struct DelayedDebounced {
     cancel_channel: Option<oneshot::Sender<()>>,
 }
 
+enum LanguageServerToQuery {
+    Primary,
+    Other(LanguageServerId),
+}
+
 impl DelayedDebounced {
     fn new() -> DelayedDebounced {
         DelayedDebounced {
@@ -634,7 +639,7 @@ impl Project {
                     cx.observe_global::<SettingsStore, _>(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<LanguageRegistry>,
-        cx: &mut ModelContext<Project>,
-    ) -> Task<()> {
+    fn maintain_workspace_config(cx: &mut ModelContext<Project>) -> 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::<SettingsStore, _>(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::notification::DidChangeConfiguration>(
-                                        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::notification::DidChangeConfiguration>(
+                            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<serde_json::Value>,
         pending_server: PendingLanguageServer,
         adapter: Arc<CachedLspAdapter>,
-        languages: Arc<LanguageRegistry>,
         language: Arc<Language>,
         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<serde_json::Value>,
         pending_server: PendingLanguageServer,
         adapter: Arc<CachedLspAdapter>,
-        languages: Arc<LanguageRegistry>,
         server_id: LanguageServerId,
         cx: &mut AsyncAppContext,
     ) -> Result<Option<Arc<LanguageServer>>> {
-        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::<lsp::request::WorkspaceConfiguration, _, _>({
-                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::notification::DidChangeConfiguration>(
                 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<Self>,
     ) -> Task<Result<Vec<LocationLink>>> {
         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<T: ToPointUtf16>(
@@ -4207,7 +4219,12 @@ impl Project {
         cx: &mut ModelContext<Self>,
     ) -> Task<Result<Vec<LocationLink>>> {
         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<T: ToPointUtf16>(
@@ -4217,7 +4234,12 @@ impl Project {
         cx: &mut ModelContext<Self>,
     ) -> Task<Result<Vec<Location>>> {
         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<T: ToPointUtf16>(
@@ -4227,7 +4249,12 @@ impl Project {
         cx: &mut ModelContext<Self>,
     ) -> Task<Result<Vec<DocumentHighlight>>> {
         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<Self>) -> Task<Result<Vec<Symbol>>> {
@@ -4455,17 +4482,66 @@ impl Project {
         cx: &mut ModelContext<Self>,
     ) -> Task<Result<Option<Hover>>> {
         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<T: ToPointUtf16>(
+    pub fn completions<T: ToOffset + ToPointUtf16>(
         &self,
         buffer: &ModelHandle<Buffer>,
         position: T,
         cx: &mut ModelContext<Self>,
     ) -> Task<Result<Vec<Completion>>> {
         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<Result<Vec<CodeAction>>> {
         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<Self>,
     ) -> Task<Result<Option<Range<Anchor>>>> {
         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<T: ToPointUtf16>(
@@ -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<R: LspCommand>(
         &self,
         buffer_handle: ModelHandle<Buffer>,
+        server: LanguageServerToQuery,
         request: R,
         cx: &mut ModelContext<Self>,
     ) -> Task<Result<R::Response>>
@@ -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<R: LspCommand>(
+        &self,
+        buffer: ModelHandle<Buffer>,
+        project_id: u64,
+        request: R,
+        cx: &mut ModelContext<'_, Project>,
+    ) -> Task<anyhow::Result<<R as LspCommand>::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<SearchMatchCandidate>,
         cx: &mut ModelContext<Self>,
@@ -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,

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(

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

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;

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

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<BufferSearchBar>,
     active_item: Option<Box<dyn ItemHandle>>,
     _inlay_hints_enabled_subscription: Option<Subscription>,
+    workspace: WeakViewHandle<Workspace>,
 }
 
 impl QuickActionBar {
-    pub fn new(buffer_search_bar: ViewHandle<BufferSearchBar>) -> Self {
+    pub fn new(buffer_search_bar: ViewHandle<BufferSearchBar>, 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()
     }
 }

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<T: IntoIterator<Item = &'a str>>(iter: T) -> Self {
+        let mut rope = Rope::new();
+        for chunk in iter {
+            rope.push(chunk);
+        }
+        rope
+    }
+}
+
 impl From<String> for Rope {
     fn from(text: String) -> Self {
         Rope::from(text.as_str())

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

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;

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

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

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<Operation>,
     deferred_replicas: HashSet<ReplicaId>,
-    replica_id: ReplicaId,
-    local_clock: clock::Local,
     pub lamport_clock: clock::Lamport,
     subscriptions: Topic,
-    edit_id_resolvers: HashMap<clock::Local, Vec<oneshot::Sender<()>>>,
+    edit_id_resolvers: HashMap<clock::Lamport, Vec<oneshot::Sender<()>>>,
     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<clock::Local>,
+    pub edit_ids: Vec<clock::Lamport>,
     pub start: clock::Global,
 }
 
@@ -91,8 +95,8 @@ impl HistoryEntry {
 
 struct History {
     base_text: Rope,
-    operations: TreeMap<clock::Local, Operation>,
-    insertion_slices: HashMap<clock::Local, Vec<InsertionSlice>>,
+    operations: TreeMap<clock::Lamport, Operation>,
+    insertion_slices: HashMap<clock::Lamport, Vec<InsertionSlice>>,
     undo_stack: Vec<HistoryEntry>,
     redo_stack: Vec<HistoryEntry>,
     transaction_depth: usize,
@@ -101,7 +105,7 @@ struct History {
 
 #[derive(Clone, Debug)]
 struct InsertionSlice {
-    insertion_id: clock::Local,
+    insertion_id: clock::Lamport,
     range: Range<usize>,
 }
 
@@ -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<TransactionId> {
         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<Transaction> {
         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<D1, D2> 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<clock::Local>,
+    pub deletions: HashSet<clock::Lamport>,
     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<Range<FullOffset>>,
     pub new_text: Vec<Arc<str>>,
@@ -459,9 +472,9 @@ pub struct EditOperation {
 
 #[derive(Clone, Debug, Eq, PartialEq)]
 pub struct UndoOperation {
-    pub id: clock::Local,
-    pub counts: HashMap<clock::Local, u32>,
+    pub timestamp: clock::Lamport,
     pub version: clock::Global,
+    pub counts: HashMap<clock::Lamport, u32>,
 }
 
 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<S: ToOffset, T: Into<Arc<str>>>(
         &mut self,
         edits: impl ExactSizeIterator<Item = (Range<S>, 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<FullOffset>],
         new_text: &[Arc<str>],
-        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<Item = &'a clock::Local>,
+        edit_ids: impl Iterator<Item = &'a clock::Lamport>,
     ) -> 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<TransactionId> {
         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<clock::Local, Operation> {
+    pub fn operations(&self) -> &TreeMap<clock::Lamport, Operation> {
         &self.history.operations
     }
 
@@ -1183,11 +1180,20 @@ impl Buffer {
         }
     }
 
+    pub fn undo_transaction(&mut self, transaction_id: TransactionId) -> Option<Operation> {
+        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<Operation> {
         let transactions = self
             .history
-            .remove_from_undo(transaction_id)
+            .remove_from_undo_until(transaction_id)
             .iter()
             .map(|entry| entry.transaction.clone())
             .collect::<Vec<_>>();
@@ -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<Item = clock::Local>,
+        edit_ids: impl IntoIterator<Item = clock::Lamport>,
     ) -> impl 'static + Future<Output = Result<()>> {
         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<str>) -> Arc<str> {
+        if let Cow::Owned(replaced) = LINE_SEPARATORS_REGEX.replace_all(&text, "\n") {
+            replaced.into()
+        } else {
+            text
+        }
+    }
+}

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::<UndoMapKey>();
         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::<UndoMapKey>();
         cursor.seek(
             &UndoMapKey {

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<ContainedText>,
     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<Interactive<ContainedText>>,
-    pub project_name_divider: ContainedText,
     pub git_menu_button: Toggleable<Interactive<ContainedText>>,
     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<Interactive<IconButton>>,
+    pub breadcrumb_height: f32,
+    pub breadcrumbs: Interactive<ContainedText>,
 }
 
 #[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)]

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

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

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

crates/vim/src/object.rs 🔗

@@ -182,19 +182,22 @@ fn in_word(
     ignore_punctuation: bool,
 ) -> Option<Range<DisplayPoint>> {
     // 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<Range<DisplayPoint>> {
-    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<Range<DisplayPoint>> {
-    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';
 

crates/zed/Cargo.toml 🔗

@@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathansobo@gmail.com>"]
 description = "The fast, collaborative code editor."
 edition = "2021"
 name = "zed"
-version = "0.102.0"
+version = "0.103.0"
 publish = false
 
 [lib]

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<LanguageRegistry>, node_runtime: Arc<NodeRuntime>) {
         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<LanguageRegistry>, node_runtime: Arc<NodeRuntime>) {
         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<LanguageRegistry>, node_runtime: Arc<NodeRuntime>) {
         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",

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,

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<OsString> {
+    vec![server_path.into(), "--stdio".into()]
+}
+
+pub struct CssLspAdapter {
+    node: Arc<NodeRuntime>,
+}
+
+impl CssLspAdapter {
+    pub fn new(node: Arc<NodeRuntime>) -> 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<Box<dyn 'static + Any + Send>> {
+        Ok(Box::new(
+            self.node
+                .npm_package_latest_version("vscode-langservers-extracted")
+                .await?,
+        ) as Box<_>)
+    }
+
+    async fn fetch_server_binary(
+        &self,
+        version: Box<dyn 'static + Send + Any>,
+        container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Result<LanguageServerBinary> {
+        let version = version.downcast::<String>().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<LanguageServerBinary> {
+        get_cached_server_binary(container_dir, &self.node).await
+    }
+
+    async fn installation_test_binary(
+        &self,
+        container_dir: PathBuf,
+    ) -> Option<LanguageServerBinary> {
+        get_cached_server_binary(container_dir, &self.node).await
+    }
+
+    async fn initialization_options(&self) -> Option<serde_json::Value> {
+        Some(json!({
+            "provideFormatter": true
+        }))
+    }
+}
+
+async fn get_cached_server_binary(
+    container_dir: PathBuf,
+    node: &NodeRuntime,
+) -> Option<LanguageServerBinary> {
+    (|| 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()
+}

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 = ["-"]

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<dyn LspAdapterDelegate>,

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,

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,

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 = ["-"]

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

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>> {
+    ) -> BoxFuture<'static, serde_json::Value> {
         let action_names = cx.all_action_names().collect::<Vec<_>>();
         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<String, String> {

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,

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<OsString> {
-    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<LanguageServ
         if let Some(path) = last_binary_path {
             Ok(LanguageServerBinary {
                 path,
-                arguments: server_binary_arguments(),
+                arguments: Vec::new(),
             })
         } else {
             Err(anyhow!("no cached binary"))

crates/zed/src/languages/php.rs 🔗

@@ -41,6 +41,10 @@ impl LspAdapter for IntelephenseLspAdapter {
         LanguageServerName("intelephense".into())
     }
 
+    fn short_name(&self) -> &'static str {
+        "php"
+    }
+
     async fn fetch_latest_server_version(
         &self,
         _delegate: &dyn LspAdapterDelegate,

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,

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,

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,

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,

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<OsString> {
+    vec![server_path.into(), "--stdio".into()]
+}
+
+pub struct TailwindLspAdapter {
+    node: Arc<NodeRuntime>,
+}
+
+impl TailwindLspAdapter {
+    pub fn new(node: Arc<NodeRuntime>) -> 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<Box<dyn 'static + Any + Send>> {
+        Ok(Box::new(
+            self.node
+                .npm_package_latest_version("@tailwindcss/language-server")
+                .await?,
+        ) as Box<_>)
+    }
+
+    async fn fetch_server_binary(
+        &self,
+        version: Box<dyn 'static + Send + Any>,
+        container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Result<LanguageServerBinary> {
+        let version = version.downcast::<String>().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<LanguageServerBinary> {
+        get_cached_server_binary(container_dir, &self.node).await
+    }
+
+    async fn installation_test_binary(
+        &self,
+        container_dir: PathBuf,
+    ) -> Option<LanguageServerBinary> {
+        get_cached_server_binary(container_dir, &self.node).await
+    }
+
+    async fn initialization_options(&self) -> Option<serde_json::Value> {
+        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<String, String> {
+        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<LanguageServerBinary> {
+    (|| 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()
+}

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

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<BoxFuture<'static, Value>> {
-        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,

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<LanguageServerBinary> {
         get_cached_server_binary(container_dir, &self.node).await
     }
-    fn workspace_configuration(&self, cx: &mut AppContext) -> Option<BoxFuture<'static, Value>> {
+    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()
     }
 }
 

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

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

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)

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,

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

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"

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

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

styles/src/component/tab_bar_button.ts 🔗

@@ -12,44 +12,47 @@ type TabBarButtonProps = TabBarButtonOptions & {
     state?: Partial<Record<InteractiveState, Partial<TabBarButtonOptions>>>
 }
 
-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,
-                    },
+            },
+            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: {
-                        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"),
                 },
             },
-        })
-    )
+        },
+    })
 }

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

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 }

styles/src/component/margin.ts → 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,
     }
 }

styles/src/component/padding.ts → 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,
     }
 }

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<RoleCycleButton> => {
-        return (
-            interactive({
-                base: {
+    const interactive_role = (
+        color: StyleSets
+    ): Interactive<RoleCycleButton> => {
+        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: {

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"),
             },
-        }
+        },
     }
 }

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

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

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

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,

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

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

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

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

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

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"),
+                },
+            },
+        }),
+    }
+}

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

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

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"

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"

styles/tsconfig.json 🔗

@@ -23,7 +23,5 @@
         "skipLibCheck": true,
         "useUnknownInCatchVariables": false
     },
-    "exclude": [
-        "node_modules"
-    ]
+    "exclude": ["node_modules"]
 }