Merge branch 'main' into keybindings-grind

Mikayla Maki created

Change summary

.gitignore                                  |   1 
Cargo.lock                                  |   3 
Procfile                                    |   1 
README.md                                   |   6 
assets/settings/default.json                |  22 -
assets/settings/initial_user_settings.json  |   3 
crates/context_menu/src/context_menu.rs     |   4 
crates/diagnostics/src/diagnostics.rs       |   7 
crates/editor/Cargo.toml                    |   1 
crates/editor/src/editor.rs                 | 341 +++++++++++-----------
crates/editor/src/element.rs                |  24 +
crates/editor/src/items.rs                  |  92 +++++
crates/editor/src/link_go_to_definition.rs  |  86 ++---
crates/editor/src/mouse_context_menu.rs     | 103 ++++++
crates/editor/src/multi_buffer.rs           |   9 
crates/editor/src/selections_collection.rs  |  38 ++
crates/editor/src/test.rs                   |  56 +++
crates/language/src/buffer.rs               |   4 
crates/plugin_runtime/Cargo.toml            |   2 
crates/plugin_runtime/README.md             |  11 
crates/plugin_runtime/build.rs              |  22 +
crates/plugin_runtime/src/lib.rs            |   2 
crates/plugin_runtime/src/plugin.rs         | 154 +--------
crates/project/src/worktree.rs              |   5 
crates/search/src/project_search.rs         |  11 
crates/settings/src/settings.rs             |  20 +
crates/terminal/src/modal.rs                |   5 
crates/terminal/src/terminal.rs             |  11 
crates/theme/src/theme.rs                   |   1 
crates/util/src/test/marked_text.rs         |   2 
crates/workspace/src/pane.rs                |  94 +++++-
crates/workspace/src/workspace.rs           |  96 +++++
crates/zed/Cargo.toml                       |   2 
crates/zed/src/languages.rs                 |  16 +
crates/zed/src/languages/language_plugin.rs |  11 
crates/zed/src/zed.rs                       |  18 
styles/src/styleTree/terminal.ts            |  22 +
styles/src/styleTree/workspace.ts           |   4 
38 files changed, 862 insertions(+), 448 deletions(-)

Detailed changes

.gitignore 🔗

@@ -8,3 +8,4 @@
 /crates/collab/static/styles.css
 /vendor/bin
 /assets/themes/*.json
+dump.rdb

Cargo.lock 🔗

@@ -1611,6 +1611,7 @@ dependencies = [
  "anyhow",
  "clock",
  "collections",
+ "context_menu",
  "ctor",
  "env_logger",
  "futures",
@@ -6990,7 +6991,7 @@ dependencies = [
 
 [[package]]
 name = "zed"
-version = "0.46.0"
+version = "0.47.1"
 dependencies = [
  "activity_indicator",
  "anyhow",

Procfile 🔗

@@ -1,2 +1,3 @@
 web: cd ../zed.dev && PORT=3000 npx next dev
 collab: cd crates/collab && cargo run
+redis: redis-server

README.md 🔗

@@ -23,6 +23,12 @@ script/sqlx migrate run
 script/seed-db
 ```
 
+Install Redis:
+
+```
+brew install redis
+```
+
 Run the web frontend and the collaboration server.
 
 ```

assets/settings/default.json 🔗

@@ -1,29 +1,25 @@
 {
     // The name of the Zed theme to use for the UI
     "theme": "cave-dark",
-
     // The name of a font to use for rendering text in the editor
     "buffer_font_family": "Zed Mono",
-
     // The default font size for text in the editor
     "buffer_font_size": 15,
-
     // Whether to enable vim modes and key bindings
     "vim_mode": false,
-
     // Whether to show the informational hover box when moving the mouse
     // over symbols in the editor.
     "hover_popover_enabled": true,
-
+    // Whether to pop the completions menu while typing in an editor without
+    // explicitly requesting it.
+    "show_completions_on_input": true,
     // Whether new projects should start out 'online'. Online projects
     // appear in the contacts panel under your name, so that your contacts
     // can see which projects you are working on. Regardless of this
     // setting, projects keep their last online status when you reopen them.
     "projects_online_by_default": true,
-
     // Whether to use language servers to provide code intelligence.
     "enable_language_server": true,
-
     // When to automatically save edited buffers. This setting can
     // take four values.
     //
@@ -36,7 +32,6 @@
     // 4. Save when idle for a certain amount of time:
     //     "autosave": { "after_delay": {"milliseconds": 500} },
     "autosave": "off",
-
     // How to auto-format modified buffers when saving them. This
     // setting can take three values:
     //
@@ -47,12 +42,11 @@
     // 3. Format code using an external command:
     //     "format_on_save": {
     //       "external": {
-    //         "command": "sed",
-    //         "arguments": ["-e", "s/ *$//"]
+    //         "command": "prettier",
+    //         "arguments": ["--stdin-filepath", "{buffer_path}"]
     //       }
-    //     },
+    //     }
     "format_on_save": "language_server",
-
     // How to soft-wrap long lines of text. This setting can take
     // three values:
     //
@@ -63,18 +57,14 @@
     // 2. Soft wrap lines at the preferred line length
     //      "soft_wrap": "preferred_line_length",
     "soft_wrap": "none",
-
     // The column at which to soft-wrap lines, for buffers where soft-wrap
     // is enabled.
     "preferred_line_length": 80,
-
     // Whether to indent lines using tab characters, as opposed to multiple
     // spaces.
     "hard_tabs": false,
-
     // How many columns a tab should occupy.
     "tab_size": 4,
-
     // Different settings for specific languages.
     "languages": {
         "Plain Text": {

assets/settings/header-comments.json → assets/settings/initial_user_settings.json 🔗

@@ -6,3 +6,6 @@
 // To see all of Zed's default settings without changing your
 // custom settings, run the `open default settings` command
 // from the command palette or from `Zed` application menu.
+{
+    "buffer_font_size": 15
+}

crates/context_menu/src/context_menu.rs 🔗

@@ -124,6 +124,10 @@ impl ContextMenu {
         }
     }
 
+    pub fn visible(&self) -> bool {
+        self.visible
+    }
+
     fn action_dispatched(&mut self, action_id: TypeId, cx: &mut ViewContext<Self>) {
         if let Some(ix) = self
             .items

crates/diagnostics/src/diagnostics.rs 🔗

@@ -501,7 +501,12 @@ impl ProjectDiagnosticsEditor {
 }
 
 impl workspace::Item for ProjectDiagnosticsEditor {
-    fn tab_content(&self, style: &theme::Tab, cx: &AppContext) -> ElementBox {
+    fn tab_content(
+        &self,
+        _detail: Option<usize>,
+        style: &theme::Tab,
+        cx: &AppContext,
+    ) -> ElementBox {
         render_summary(
             &self.summary,
             &style.label.text,

crates/editor/Cargo.toml 🔗

@@ -23,6 +23,7 @@ test-support = [
 text = { path = "../text" }
 clock = { path = "../clock" }
 collections = { path = "../collections" }
+context_menu = { path = "../context_menu" }
 fuzzy = { path = "../fuzzy" }
 gpui = { path = "../gpui" }
 language = { path = "../language" }

crates/editor/src/editor.rs 🔗

@@ -4,6 +4,7 @@ mod highlight_matching_bracket;
 mod hover_popover;
 pub mod items;
 mod link_go_to_definition;
+mod mouse_context_menu;
 pub mod movement;
 mod multi_buffer;
 pub mod selections_collection;
@@ -34,6 +35,7 @@ use gpui::{
 };
 use highlight_matching_bracket::refresh_matching_bracket_highlights;
 use hover_popover::{hide_hover, HoverState};
+pub use items::MAX_TAB_TITLE_LEN;
 pub use language::{char_kind, CharKind};
 use language::{
     BracketPair, Buffer, CodeAction, CodeLabel, Completion, Diagnostic, DiagnosticSeverity,
@@ -319,6 +321,7 @@ pub fn init(cx: &mut MutableAppContext) {
 
     hover_popover::init(cx);
     link_go_to_definition::init(cx);
+    mouse_context_menu::init(cx);
 
     workspace::register_project_item::<Editor>(cx);
     workspace::register_followable_item::<Editor>(cx);
@@ -425,6 +428,7 @@ pub struct Editor {
     background_highlights: BTreeMap<TypeId, (fn(&Theme) -> Color, Vec<Range<Anchor>>)>,
     nav_history: Option<ItemNavHistory>,
     context_menu: Option<ContextMenu>,
+    mouse_context_menu: ViewHandle<context_menu::ContextMenu>,
     completion_tasks: Vec<(CompletionId, Task<Option<()>>)>,
     next_completion_id: CompletionId,
     available_code_actions: Option<(ModelHandle<Buffer>, Arc<[CodeAction]>)>,
@@ -1010,11 +1014,11 @@ impl Editor {
             background_highlights: Default::default(),
             nav_history: None,
             context_menu: None,
+            mouse_context_menu: cx.add_view(|cx| context_menu::ContextMenu::new(cx)),
             completion_tasks: Default::default(),
             next_completion_id: 0,
             available_code_actions: Default::default(),
             code_actions_task: Default::default(),
-
             document_highlights_task: Default::default(),
             pending_rename: Default::default(),
             searchable: true,
@@ -1070,7 +1074,7 @@ impl Editor {
         &self.buffer
     }
 
-    pub fn title(&self, cx: &AppContext) -> String {
+    pub fn title<'a>(&self, cx: &'a AppContext) -> Cow<'a, str> {
         self.buffer().read(cx).title(cx)
     }
 
@@ -1596,7 +1600,7 @@ impl Editor {
                 s.delete(newest_selection.id)
             }
 
-            s.set_pending_range(start..end, mode);
+            s.set_pending_anchor_range(start..end, mode);
         });
     }
 
@@ -1937,6 +1941,10 @@ impl Editor {
     }
 
     fn trigger_completion_on_input(&mut self, text: &str, cx: &mut ViewContext<Self>) {
+        if !cx.global::<Settings>().show_completions_on_input {
+            return;
+        }
+
         let selection = self.selections.newest_anchor();
         if self
             .buffer
@@ -5780,7 +5788,12 @@ impl View for Editor {
             });
         }
 
-        EditorElement::new(self.handle.clone(), style.clone(), self.cursor_shape).boxed()
+        Stack::new()
+            .with_child(
+                EditorElement::new(self.handle.clone(), style.clone(), self.cursor_shape).boxed(),
+            )
+            .with_child(ChildView::new(&self.mouse_context_menu).boxed())
+            .boxed()
     }
 
     fn ui_name() -> &'static str {
@@ -6225,7 +6238,8 @@ pub fn styled_runs_for_code_label<'a>(
 #[cfg(test)]
 mod tests {
     use crate::test::{
-        assert_text_with_selections, build_editor, select_ranges, EditorTestContext,
+        assert_text_with_selections, build_editor, select_ranges, EditorLspTestContext,
+        EditorTestContext,
     };
 
     use super::*;
@@ -6236,7 +6250,6 @@ mod tests {
     };
     use indoc::indoc;
     use language::{FakeLspAdapter, LanguageConfig};
-    use lsp::FakeLanguageServer;
     use project::FakeFs;
     use settings::EditorSettings;
     use std::{cell::RefCell, rc::Rc, time::Instant};
@@ -6244,7 +6257,9 @@ mod tests {
     use unindent::Unindent;
     use util::{
         assert_set_eq,
-        test::{marked_text_by, marked_text_ranges, marked_text_ranges_by, sample_text},
+        test::{
+            marked_text_by, marked_text_ranges, marked_text_ranges_by, sample_text, TextRangeMarker,
+        },
     };
     use workspace::{FollowableItem, ItemHandle, NavigationEntry, Pane};
 
@@ -9524,199 +9539,182 @@ mod tests {
 
     #[gpui::test]
     async fn test_completion(cx: &mut gpui::TestAppContext) {
-        let mut language = Language::new(
-            LanguageConfig {
-                name: "Rust".into(),
-                path_suffixes: vec!["rs".to_string()],
-                ..Default::default()
-            },
-            Some(tree_sitter_rust::language()),
-        );
-        let mut fake_servers = language
-            .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
-                capabilities: lsp::ServerCapabilities {
-                    completion_provider: Some(lsp::CompletionOptions {
-                        trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
-                        ..Default::default()
-                    }),
+        let mut cx = EditorLspTestContext::new_rust(
+            lsp::ServerCapabilities {
+                completion_provider: Some(lsp::CompletionOptions {
+                    trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
                     ..Default::default()
-                },
+                }),
                 ..Default::default()
-            }))
-            .await;
+            },
+            cx,
+        )
+        .await;
 
-        let text = "
-            one
+        cx.set_state(indoc! {"
+            one|
             two
-            three
-        "
-        .unindent();
-
-        let fs = FakeFs::new(cx.background().clone());
-        fs.insert_file("/file.rs", text).await;
-
-        let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
-        project.update(cx, |project, _| project.languages().add(Arc::new(language)));
-        let buffer = project
-            .update(cx, |project, cx| project.open_local_buffer("/file.rs", cx))
-            .await
-            .unwrap();
-        let mut fake_server = fake_servers.next().await.unwrap();
-
-        let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
-        let (_, editor) = cx.add_window(|cx| build_editor(buffer, cx));
-
-        editor.update(cx, |editor, cx| {
-            editor.project = Some(project);
-            editor.change_selections(None, cx, |s| {
-                s.select_ranges([Point::new(0, 3)..Point::new(0, 3)])
-            });
-            editor.handle_input(&Input(".".to_string()), cx);
-        });
-
+            three"});
+        cx.simulate_keystroke(".");
         handle_completion_request(
-            &mut fake_server,
-            "/file.rs",
-            Point::new(0, 4),
-            vec![
-                (Point::new(0, 4)..Point::new(0, 4), "first_completion"),
-                (Point::new(0, 4)..Point::new(0, 4), "second_completion"),
-            ],
+            &mut cx,
+            indoc! {"
+                one.|<>
+                two
+                three"},
+            vec!["first_completion", "second_completion"],
         )
         .await;
-        editor
-            .condition(&cx, |editor, _| editor.context_menu_visible())
+        cx.condition(|editor, _| editor.context_menu_visible())
             .await;
-
-        let apply_additional_edits = editor.update(cx, |editor, cx| {
+        let apply_additional_edits = cx.update_editor(|editor, cx| {
             editor.move_down(&MoveDown, cx);
-            let apply_additional_edits = editor
+            editor
                 .confirm_completion(&ConfirmCompletion::default(), cx)
-                .unwrap();
-            assert_eq!(
-                editor.text(cx),
-                "
-                    one.second_completion
-                    two
-                    three
-                "
-                .unindent()
-            );
-            apply_additional_edits
+                .unwrap()
         });
+        cx.assert_editor_state(indoc! {"
+            one.second_completion|
+            two
+            three"});
 
         handle_resolve_completion_request(
-            &mut fake_server,
-            Some((Point::new(2, 5)..Point::new(2, 5), "\nadditional edit")),
+            &mut cx,
+            Some((
+                indoc! {"
+                    one.second_completion
+                    two
+                    three<>"},
+                "\nadditional edit",
+            )),
         )
         .await;
         apply_additional_edits.await.unwrap();
-        assert_eq!(
-            editor.read_with(cx, |editor, cx| editor.text(cx)),
-            "
-                one.second_completion
-                two
-                three
-                additional edit
-            "
-            .unindent()
-        );
-
-        editor.update(cx, |editor, cx| {
-            editor.change_selections(None, cx, |s| {
-                s.select_ranges([
-                    Point::new(1, 3)..Point::new(1, 3),
-                    Point::new(2, 5)..Point::new(2, 5),
-                ])
-            });
+        cx.assert_editor_state(indoc! {"
+            one.second_completion|
+            two
+            three
+            additional edit"});
 
-            editor.handle_input(&Input(" ".to_string()), cx);
-            assert!(editor.context_menu.is_none());
-            editor.handle_input(&Input("s".to_string()), cx);
-            assert!(editor.context_menu.is_none());
-        });
+        cx.set_state(indoc! {"
+            one.second_completion
+            two|
+            three|
+            additional edit"});
+        cx.simulate_keystroke(" ");
+        assert!(cx.editor(|e, _| e.context_menu.is_none()));
+        cx.simulate_keystroke("s");
+        assert!(cx.editor(|e, _| e.context_menu.is_none()));
 
+        cx.assert_editor_state(indoc! {"
+            one.second_completion
+            two s|
+            three s|
+            additional edit"});
         handle_completion_request(
-            &mut fake_server,
-            "/file.rs",
-            Point::new(2, 7),
-            vec![
-                (Point::new(2, 6)..Point::new(2, 7), "fourth_completion"),
-                (Point::new(2, 6)..Point::new(2, 7), "fifth_completion"),
-                (Point::new(2, 6)..Point::new(2, 7), "sixth_completion"),
-            ],
+            &mut cx,
+            indoc! {"
+                one.second_completion
+                two s
+                three <s|>
+                additional edit"},
+            vec!["fourth_completion", "fifth_completion", "sixth_completion"],
         )
         .await;
-        editor
-            .condition(&cx, |editor, _| editor.context_menu_visible())
+        cx.condition(|editor, _| editor.context_menu_visible())
             .await;
 
-        editor.update(cx, |editor, cx| {
-            editor.handle_input(&Input("i".to_string()), cx);
-        });
+        cx.simulate_keystroke("i");
 
         handle_completion_request(
-            &mut fake_server,
-            "/file.rs",
-            Point::new(2, 8),
-            vec![
-                (Point::new(2, 6)..Point::new(2, 8), "fourth_completion"),
-                (Point::new(2, 6)..Point::new(2, 8), "fifth_completion"),
-                (Point::new(2, 6)..Point::new(2, 8), "sixth_completion"),
-            ],
+            &mut cx,
+            indoc! {"
+                one.second_completion
+                two si
+                three <si|>
+                additional edit"},
+            vec!["fourth_completion", "fifth_completion", "sixth_completion"],
         )
         .await;
-        editor
-            .condition(&cx, |editor, _| editor.context_menu_visible())
+        cx.condition(|editor, _| editor.context_menu_visible())
             .await;
 
-        let apply_additional_edits = editor.update(cx, |editor, cx| {
-            let apply_additional_edits = editor
+        let apply_additional_edits = cx.update_editor(|editor, cx| {
+            editor
                 .confirm_completion(&ConfirmCompletion::default(), cx)
-                .unwrap();
-            assert_eq!(
-                editor.text(cx),
-                "
-                    one.second_completion
-                    two sixth_completion
-                    three sixth_completion
-                    additional edit
-                "
-                .unindent()
-            );
-            apply_additional_edits
+                .unwrap()
         });
-        handle_resolve_completion_request(&mut fake_server, None).await;
+        cx.assert_editor_state(indoc! {"
+            one.second_completion
+            two sixth_completion|
+            three sixth_completion|
+            additional edit"});
+
+        handle_resolve_completion_request(&mut cx, None).await;
         apply_additional_edits.await.unwrap();
 
-        async fn handle_completion_request(
-            fake: &mut FakeLanguageServer,
-            path: &'static str,
-            position: Point,
-            completions: Vec<(Range<Point>, &'static str)>,
+        cx.update(|cx| {
+            cx.update_global::<Settings, _, _>(|settings, _| {
+                settings.show_completions_on_input = false;
+            })
+        });
+        cx.set_state("editor|");
+        cx.simulate_keystroke(".");
+        assert!(cx.editor(|e, _| e.context_menu.is_none()));
+        cx.simulate_keystrokes(["c", "l", "o"]);
+        cx.assert_editor_state("editor.clo|");
+        assert!(cx.editor(|e, _| e.context_menu.is_none()));
+        cx.update_editor(|editor, cx| {
+            editor.show_completions(&ShowCompletions, cx);
+        });
+        handle_completion_request(&mut cx, "editor.<clo|>", vec!["close", "clobber"]).await;
+        cx.condition(|editor, _| editor.context_menu_visible())
+            .await;
+        let apply_additional_edits = cx.update_editor(|editor, cx| {
+            editor
+                .confirm_completion(&ConfirmCompletion::default(), cx)
+                .unwrap()
+        });
+        cx.assert_editor_state("editor.close|");
+        handle_resolve_completion_request(&mut cx, None).await;
+        apply_additional_edits.await.unwrap();
+
+        // Handle completion request passing a marked string specifying where the completion
+        // should be triggered from using '|' character, what range should be replaced, and what completions
+        // should be returned using '<' and '>' to delimit the range
+        async fn handle_completion_request<'a>(
+            cx: &mut EditorLspTestContext<'a>,
+            marked_string: &str,
+            completions: Vec<&'static str>,
         ) {
-            fake.handle_request::<lsp::request::Completion, _, _>(move |params, _| {
+            let complete_from_marker: TextRangeMarker = '|'.into();
+            let replace_range_marker: TextRangeMarker = ('<', '>').into();
+            let (_, mut marked_ranges) = marked_text_ranges_by(
+                marked_string,
+                vec![complete_from_marker.clone(), replace_range_marker.clone()],
+            );
+
+            let complete_from_position =
+                cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
+            let replace_range =
+                cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
+
+            cx.handle_request::<lsp::request::Completion, _, _>(move |url, params, _| {
                 let completions = completions.clone();
                 async move {
-                    assert_eq!(
-                        params.text_document_position.text_document.uri,
-                        lsp::Url::from_file_path(path).unwrap()
-                    );
+                    assert_eq!(params.text_document_position.text_document.uri, url.clone());
                     assert_eq!(
                         params.text_document_position.position,
-                        lsp::Position::new(position.row, position.column)
+                        complete_from_position
                     );
                     Ok(Some(lsp::CompletionResponse::Array(
                         completions
                             .iter()
-                            .map(|(range, new_text)| lsp::CompletionItem {
-                                label: new_text.to_string(),
+                            .map(|completion_text| lsp::CompletionItem {
+                                label: completion_text.to_string(),
                                 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
-                                    range: lsp::Range::new(
-                                        lsp::Position::new(range.start.row, range.start.column),
-                                        lsp::Position::new(range.start.row, range.start.column),
-                                    ),
-                                    new_text: new_text.to_string(),
+                                    range: replace_range.clone(),
+                                    new_text: completion_text.to_string(),
                                 })),
                                 ..Default::default()
                             })
@@ -9728,23 +9726,26 @@ mod tests {
             .await;
         }
 
-        async fn handle_resolve_completion_request(
-            fake: &mut FakeLanguageServer,
-            edit: Option<(Range<Point>, &'static str)>,
+        async fn handle_resolve_completion_request<'a>(
+            cx: &mut EditorLspTestContext<'a>,
+            edit: Option<(&'static str, &'static str)>,
         ) {
-            fake.handle_request::<lsp::request::ResolveCompletionItem, _, _>(move |_, _| {
+            let edit = edit.map(|(marked_string, new_text)| {
+                let replace_range_marker: TextRangeMarker = ('<', '>').into();
+                let (_, mut marked_ranges) =
+                    marked_text_ranges_by(marked_string, vec![replace_range_marker.clone()]);
+
+                let replace_range = cx
+                    .to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
+
+                vec![lsp::TextEdit::new(replace_range, new_text.to_string())]
+            });
+
+            cx.handle_request::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
                 let edit = edit.clone();
                 async move {
                     Ok(lsp::CompletionItem {
-                        additional_text_edits: edit.map(|(range, new_text)| {
-                            vec![lsp::TextEdit::new(
-                                lsp::Range::new(
-                                    lsp::Position::new(range.start.row, range.start.column),
-                                    lsp::Position::new(range.end.row, range.end.column),
-                                ),
-                                new_text.to_string(),
-                            )]
-                        }),
+                        additional_text_edits: edit,
                         ..Default::default()
                     })
                 }

crates/editor/src/element.rs 🔗

@@ -7,6 +7,7 @@ use crate::{
     display_map::{BlockStyle, DisplaySnapshot, TransformBlock},
     hover_popover::HoverAt,
     link_go_to_definition::{CmdChanged, GoToFetchedDefinition, UpdateGoToDefinitionLink},
+    mouse_context_menu::DeployMouseContextMenu,
     EditorStyle,
 };
 use clock::ReplicaId;
@@ -152,6 +153,24 @@ impl EditorElement {
         true
     }
 
+    fn mouse_right_down(
+        &self,
+        position: Vector2F,
+        layout: &mut LayoutState,
+        paint: &mut PaintState,
+        cx: &mut EventContext,
+    ) -> bool {
+        if !paint.text_bounds.contains_point(position) {
+            return false;
+        }
+
+        let snapshot = self.snapshot(cx.app);
+        let (point, _) = paint.point_for_position(&snapshot, layout, position);
+
+        cx.dispatch_action(DeployMouseContextMenu { position, point });
+        true
+    }
+
     fn mouse_up(&self, _position: Vector2F, cx: &mut EventContext) -> bool {
         if self.view(cx.app.as_ref()).is_selecting() {
             cx.dispatch_action(Select(SelectPhase::End));
@@ -1482,6 +1501,11 @@ impl Element for EditorElement {
                 paint,
                 cx,
             ),
+            Event::MouseDown(MouseEvent {
+                button: MouseButton::Right,
+                position,
+                ..
+            }) => self.mouse_right_down(*position, layout, paint, cx),
             Event::MouseUp(MouseEvent {
                 button: MouseButton::Left,
                 position,

crates/editor/src/items.rs 🔗

@@ -1,4 +1,6 @@
-use crate::{Anchor, Autoscroll, Editor, Event, ExcerptId, NavigationData, ToPoint as _};
+use crate::{
+    Anchor, Autoscroll, Editor, Event, ExcerptId, MultiBuffer, NavigationData, ToPoint as _,
+};
 use anyhow::{anyhow, Result};
 use futures::FutureExt;
 use gpui::{
@@ -10,12 +12,18 @@ use project::{File, Project, ProjectEntryId, ProjectPath};
 use rpc::proto::{self, update_view};
 use settings::Settings;
 use smallvec::SmallVec;
-use std::{fmt::Write, path::PathBuf, time::Duration};
+use std::{
+    borrow::Cow,
+    fmt::Write,
+    path::{Path, PathBuf},
+    time::Duration,
+};
 use text::{Point, Selection};
 use util::TryFutureExt;
 use workspace::{FollowableItem, Item, ItemHandle, ItemNavHistory, ProjectItem, StatusItemView};
 
 pub const FORMAT_TIMEOUT: Duration = Duration::from_secs(2);
+pub const MAX_TAB_TITLE_LEN: usize = 24;
 
 impl FollowableItem for Editor {
     fn from_state_proto(
@@ -292,9 +300,44 @@ impl Item for Editor {
         }
     }
 
-    fn tab_content(&self, style: &theme::Tab, cx: &AppContext) -> ElementBox {
-        let title = self.title(cx);
-        Label::new(title, style.label.clone()).boxed()
+    fn tab_description<'a>(&'a self, detail: usize, cx: &'a AppContext) -> Option<Cow<'a, str>> {
+        match path_for_buffer(&self.buffer, detail, true, cx)? {
+            Cow::Borrowed(path) => Some(path.to_string_lossy()),
+            Cow::Owned(path) => Some(path.to_string_lossy().to_string().into()),
+        }
+    }
+
+    fn tab_content(
+        &self,
+        detail: Option<usize>,
+        style: &theme::Tab,
+        cx: &AppContext,
+    ) -> ElementBox {
+        Flex::row()
+            .with_child(
+                Label::new(self.title(cx).into(), style.label.clone())
+                    .aligned()
+                    .boxed(),
+            )
+            .with_children(detail.and_then(|detail| {
+                let path = path_for_buffer(&self.buffer, detail, false, cx)?;
+                let description = path.to_string_lossy();
+                Some(
+                    Label::new(
+                        if description.len() > MAX_TAB_TITLE_LEN {
+                            description[..MAX_TAB_TITLE_LEN].to_string() + "…"
+                        } else {
+                            description.into()
+                        },
+                        style.description.text.clone(),
+                    )
+                    .contained()
+                    .with_style(style.description.container)
+                    .aligned()
+                    .boxed(),
+                )
+            }))
+            .boxed()
     }
 
     fn project_path(&self, cx: &AppContext) -> Option<ProjectPath> {
@@ -534,3 +577,42 @@ impl StatusItemView for CursorPosition {
         cx.notify();
     }
 }
+
+fn path_for_buffer<'a>(
+    buffer: &ModelHandle<MultiBuffer>,
+    mut height: usize,
+    include_filename: bool,
+    cx: &'a AppContext,
+) -> Option<Cow<'a, Path>> {
+    let file = buffer.read(cx).as_singleton()?.read(cx).file()?;
+    // Ensure we always render at least the filename.
+    height += 1;
+
+    let mut prefix = file.path().as_ref();
+    while height > 0 {
+        if let Some(parent) = prefix.parent() {
+            prefix = parent;
+            height -= 1;
+        } else {
+            break;
+        }
+    }
+
+    // Here we could have just always used `full_path`, but that is very
+    // allocation-heavy and so we try to use a `Cow<Path>` if we haven't
+    // traversed all the way up to the worktree's root.
+    if height > 0 {
+        let full_path = file.full_path(cx);
+        if include_filename {
+            Some(full_path.into())
+        } else {
+            Some(full_path.parent().unwrap().to_path_buf().into())
+        }
+    } else {
+        let mut path = file.path().strip_prefix(prefix).unwrap();
+        if !include_filename {
+            path = path.parent().unwrap();
+        }
+        Some(path.into())
+    }
+}
@@ -342,17 +342,16 @@ mod tests {
                 test();"});
 
         let mut requests =
-            cx.lsp
-                .handle_request::<lsp::request::GotoDefinition, _, _>(move |_, _| async move {
-                    Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
-                        lsp::LocationLink {
-                            origin_selection_range: Some(symbol_range),
-                            target_uri: lsp::Url::from_file_path("/root/dir/file.rs").unwrap(),
-                            target_range,
-                            target_selection_range: target_range,
-                        },
-                    ])))
-                });
+            cx.handle_request::<lsp::request::GotoDefinition, _, _>(move |url, _, _| async move {
+                Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
+                    lsp::LocationLink {
+                        origin_selection_range: Some(symbol_range),
+                        target_uri: url.clone(),
+                        target_range,
+                        target_selection_range: target_range,
+                    },
+                ])))
+            });
         cx.update_editor(|editor, cx| {
             update_go_to_definition_link(
                 editor,
@@ -387,18 +386,17 @@ mod tests {
         // Response without source range still highlights word
         cx.update_editor(|editor, _| editor.link_go_to_definition_state.last_mouse_location = None);
         let mut requests =
-            cx.lsp
-                .handle_request::<lsp::request::GotoDefinition, _, _>(move |_, _| async move {
-                    Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
-                        lsp::LocationLink {
-                            // No origin range
-                            origin_selection_range: None,
-                            target_uri: lsp::Url::from_file_path("/root/dir/file.rs").unwrap(),
-                            target_range,
-                            target_selection_range: target_range,
-                        },
-                    ])))
-                });
+            cx.handle_request::<lsp::request::GotoDefinition, _, _>(move |url, _, _| async move {
+                Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
+                    lsp::LocationLink {
+                        // No origin range
+                        origin_selection_range: None,
+                        target_uri: url.clone(),
+                        target_range,
+                        target_selection_range: target_range,
+                    },
+                ])))
+            });
         cx.update_editor(|editor, cx| {
             update_go_to_definition_link(
                 editor,
@@ -495,17 +493,16 @@ mod tests {
                 test();"});
 
         let mut requests =
-            cx.lsp
-                .handle_request::<lsp::request::GotoDefinition, _, _>(move |_, _| async move {
-                    Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
-                        lsp::LocationLink {
-                            origin_selection_range: Some(symbol_range),
-                            target_uri: lsp::Url::from_file_path("/root/dir/file.rs").unwrap(),
-                            target_range,
-                            target_selection_range: target_range,
-                        },
-                    ])))
-                });
+            cx.handle_request::<lsp::request::GotoDefinition, _, _>(move |url, _, _| async move {
+                Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
+                    lsp::LocationLink {
+                        origin_selection_range: Some(symbol_range),
+                        target_uri: url,
+                        target_range,
+                        target_selection_range: target_range,
+                    },
+                ])))
+            });
         cx.update_editor(|editor, cx| {
             cmd_changed(editor, &CmdChanged { cmd_down: true }, cx);
         });
@@ -584,17 +581,16 @@ mod tests {
                 test();"});
 
         let mut requests =
-            cx.lsp
-                .handle_request::<lsp::request::GotoDefinition, _, _>(move |_, _| async move {
-                    Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
-                        lsp::LocationLink {
-                            origin_selection_range: None,
-                            target_uri: lsp::Url::from_file_path("/root/dir/file.rs").unwrap(),
-                            target_range,
-                            target_selection_range: target_range,
-                        },
-                    ])))
-                });
+            cx.handle_request::<lsp::request::GotoDefinition, _, _>(move |url, _, _| async move {
+                Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
+                    lsp::LocationLink {
+                        origin_selection_range: None,
+                        target_uri: url,
+                        target_range,
+                        target_selection_range: target_range,
+                    },
+                ])))
+            });
         cx.update_workspace(|workspace, cx| {
             go_to_fetched_definition(workspace, &GoToFetchedDefinition { point: hover_point }, cx);
         });

crates/editor/src/mouse_context_menu.rs 🔗

@@ -0,0 +1,103 @@
+use context_menu::ContextMenuItem;
+use gpui::{geometry::vector::Vector2F, impl_internal_actions, MutableAppContext, ViewContext};
+
+use crate::{
+    DisplayPoint, Editor, EditorMode, FindAllReferences, GoToDefinition, Rename, SelectMode,
+    ToggleCodeActions,
+};
+
+#[derive(Clone, PartialEq)]
+pub struct DeployMouseContextMenu {
+    pub position: Vector2F,
+    pub point: DisplayPoint,
+}
+
+impl_internal_actions!(editor, [DeployMouseContextMenu]);
+
+pub fn init(cx: &mut MutableAppContext) {
+    cx.add_action(deploy_context_menu);
+}
+
+pub fn deploy_context_menu(
+    editor: &mut Editor,
+    &DeployMouseContextMenu { position, point }: &DeployMouseContextMenu,
+    cx: &mut ViewContext<Editor>,
+) {
+    // Don't show context menu for inline editors
+    if editor.mode() != EditorMode::Full {
+        return;
+    }
+
+    // Don't show the context menu if there isn't a project associated with this editor
+    if editor.project.is_none() {
+        return;
+    }
+
+    // Move the cursor to the clicked location so that dispatched actions make sense
+    editor.change_selections(None, cx, |s| {
+        s.clear_disjoint();
+        s.set_pending_display_range(point..point, SelectMode::Character);
+    });
+
+    editor.mouse_context_menu.update(cx, |menu, cx| {
+        menu.show(
+            position,
+            vec![
+                ContextMenuItem::item("Rename Symbol", Rename),
+                ContextMenuItem::item("Go To Definition", GoToDefinition),
+                ContextMenuItem::item("Find All References", FindAllReferences),
+                ContextMenuItem::item(
+                    "Code Actions",
+                    ToggleCodeActions {
+                        deployed_from_indicator: false,
+                    },
+                ),
+            ],
+            cx,
+        );
+    });
+    cx.notify();
+}
+
+#[cfg(test)]
+mod tests {
+    use indoc::indoc;
+
+    use crate::test::EditorLspTestContext;
+
+    use super::*;
+
+    #[gpui::test]
+    async fn test_mouse_context_menu(cx: &mut gpui::TestAppContext) {
+        let mut cx = EditorLspTestContext::new_rust(
+            lsp::ServerCapabilities {
+                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
+                ..Default::default()
+            },
+            cx,
+        )
+        .await;
+
+        cx.set_state(indoc! {"
+            fn te|st()
+                do_work();"});
+        let point = cx.display_point(indoc! {"
+            fn test()
+                do_w|ork();"});
+        cx.update_editor(|editor, cx| {
+            deploy_context_menu(
+                editor,
+                &DeployMouseContextMenu {
+                    position: Default::default(),
+                    point,
+                },
+                cx,
+            )
+        });
+
+        cx.assert_editor_state(indoc! {"
+            fn test()
+                do_w|ork();"});
+        cx.editor(|editor, app| assert!(editor.mouse_context_menu.read(app).visible()));
+    }
+}

crates/editor/src/multi_buffer.rs 🔗

@@ -14,6 +14,7 @@ use language::{
 use settings::Settings;
 use smallvec::SmallVec;
 use std::{
+    borrow::Cow,
     cell::{Ref, RefCell},
     cmp, fmt, io,
     iter::{self, FromIterator},
@@ -1194,14 +1195,14 @@ impl MultiBuffer {
             .collect()
     }
 
-    pub fn title(&self, cx: &AppContext) -> String {
-        if let Some(title) = self.title.clone() {
-            return title;
+    pub fn title<'a>(&'a self, cx: &'a AppContext) -> Cow<'a, str> {
+        if let Some(title) = self.title.as_ref() {
+            return title.into();
         }
 
         if let Some(buffer) = self.as_singleton() {
             if let Some(file) = buffer.read(cx).file() {
-                return file.file_name(cx).to_string_lossy().into();
+                return file.file_name(cx).to_string_lossy();
             }
         }
 

crates/editor/src/selections_collection.rs 🔗

@@ -384,7 +384,7 @@ impl<'a> MutableSelectionsCollection<'a> {
         }
     }
 
-    pub fn set_pending_range(&mut self, range: Range<Anchor>, mode: SelectMode) {
+    pub fn set_pending_anchor_range(&mut self, range: Range<Anchor>, mode: SelectMode) {
         self.collection.pending = Some(PendingSelection {
             selection: Selection {
                 id: post_inc(&mut self.collection.next_selection_id),
@@ -398,6 +398,42 @@ impl<'a> MutableSelectionsCollection<'a> {
         self.selections_changed = true;
     }
 
+    pub fn set_pending_display_range(&mut self, range: Range<DisplayPoint>, mode: SelectMode) {
+        let (start, end, reversed) = {
+            let display_map = self.display_map();
+            let buffer = self.buffer();
+            let mut start = range.start;
+            let mut end = range.end;
+            let reversed = if start > end {
+                mem::swap(&mut start, &mut end);
+                true
+            } else {
+                false
+            };
+
+            let end_bias = if end > start { Bias::Left } else { Bias::Right };
+            (
+                buffer.anchor_before(start.to_point(&display_map)),
+                buffer.anchor_at(end.to_point(&display_map), end_bias),
+                reversed,
+            )
+        };
+
+        let new_pending = PendingSelection {
+            selection: Selection {
+                id: post_inc(&mut self.collection.next_selection_id),
+                start,
+                end,
+                reversed,
+                goal: SelectionGoal::None,
+            },
+            mode,
+        };
+
+        self.collection.pending = Some(new_pending);
+        self.selections_changed = true;
+    }
+
     pub fn set_pending(&mut self, selection: Selection<Anchor>, mode: SelectMode) {
         self.collection.pending = Some(PendingSelection { selection, mode });
         self.selections_changed = true;

crates/editor/src/test.rs 🔗

@@ -4,12 +4,14 @@ use std::{
     sync::Arc,
 };
 
-use futures::StreamExt;
+use anyhow::Result;
+use futures::{Future, StreamExt};
 use indoc::indoc;
 
 use collections::BTreeMap;
 use gpui::{json, keymap::Keystroke, AppContext, ModelHandle, ViewContext, ViewHandle};
 use language::{point_to_lsp, FakeLspAdapter, Language, LanguageConfig, Selection};
+use lsp::request;
 use project::Project;
 use settings::Settings;
 use util::{
@@ -110,6 +112,13 @@ impl<'a> EditorTestContext<'a> {
         }
     }
 
+    pub fn condition(
+        &self,
+        predicate: impl FnMut(&Editor, &AppContext) -> bool,
+    ) -> impl Future<Output = ()> {
+        self.editor.condition(self.cx, predicate)
+    }
+
     pub fn editor<F, T>(&mut self, read: F) -> T
     where
         F: FnOnce(&Editor, &AppContext) -> T,
@@ -424,6 +433,7 @@ pub struct EditorLspTestContext<'a> {
     pub cx: EditorTestContext<'a>,
     pub lsp: lsp::FakeLanguageServer,
     pub workspace: ViewHandle<Workspace>,
+    pub editor_lsp_url: lsp::Url,
 }
 
 impl<'a> EditorLspTestContext<'a> {
@@ -497,6 +507,7 @@ impl<'a> EditorLspTestContext<'a> {
             },
             lsp,
             workspace,
+            editor_lsp_url: lsp::Url::from_file_path("/root/dir/file.rs").unwrap(),
         }
     }
 
@@ -520,11 +531,15 @@ impl<'a> EditorLspTestContext<'a> {
     pub fn lsp_range(&mut self, marked_text: &str) -> lsp::Range {
         let (unmarked, mut ranges) = marked_text_ranges_by(marked_text, vec![('[', ']').into()]);
         assert_eq!(unmarked, self.cx.buffer_text());
+        let offset_range = ranges.remove(&('[', ']').into()).unwrap()[0].clone();
+        self.to_lsp_range(offset_range)
+    }
+
+    pub fn to_lsp_range(&mut self, range: Range<usize>) -> lsp::Range {
         let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx));
+        let start_point = range.start.to_point(&snapshot.buffer_snapshot);
+        let end_point = range.end.to_point(&snapshot.buffer_snapshot);
 
-        let offset_range = ranges.remove(&('[', ']').into()).unwrap()[0].clone();
-        let start_point = offset_range.start.to_point(&snapshot.buffer_snapshot);
-        let end_point = offset_range.end.to_point(&snapshot.buffer_snapshot);
         self.editor(|editor, cx| {
             let buffer = editor.buffer().read(cx);
             let start = point_to_lsp(
@@ -546,12 +561,45 @@ impl<'a> EditorLspTestContext<'a> {
         })
     }
 
+    pub fn to_lsp(&mut self, offset: usize) -> lsp::Position {
+        let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx));
+        let point = offset.to_point(&snapshot.buffer_snapshot);
+
+        self.editor(|editor, cx| {
+            let buffer = editor.buffer().read(cx);
+            point_to_lsp(
+                buffer
+                    .point_to_buffer_offset(point, cx)
+                    .unwrap()
+                    .1
+                    .to_point_utf16(&buffer.read(cx)),
+            )
+        })
+    }
+
     pub fn update_workspace<F, T>(&mut self, update: F) -> T
     where
         F: FnOnce(&mut Workspace, &mut ViewContext<Workspace>) -> T,
     {
         self.workspace.update(self.cx.cx, update)
     }
+
+    pub fn handle_request<T, F, Fut>(
+        &self,
+        mut handler: F,
+    ) -> futures::channel::mpsc::UnboundedReceiver<()>
+    where
+        T: 'static + request::Request,
+        T::Params: 'static + Send,
+        F: 'static + Send + FnMut(lsp::Url, T::Params, gpui::AsyncAppContext) -> Fut,
+        Fut: 'static + Send + Future<Output = Result<T::Result>>,
+    {
+        let url = self.editor_lsp_url.clone();
+        self.lsp.handle_request::<T, _, _>(move |params, cx| {
+            let url = url.clone();
+            handler(url, params, cx)
+        })
+    }
 }
 
 impl<'a> Deref for EditorLspTestContext<'a> {

crates/language/src/buffer.rs 🔗

@@ -20,7 +20,7 @@ use std::{
     any::Any,
     cmp::{self, Ordering},
     collections::{BTreeMap, HashMap},
-    ffi::OsString,
+    ffi::OsStr,
     future::Future,
     iter::{self, Iterator, Peekable},
     mem,
@@ -185,7 +185,7 @@ pub trait File: Send + Sync {
 
     /// Returns the last component of this handle's absolute path. If this handle refers to the root
     /// of its worktree, then this method will return the name of the worktree itself.
-    fn file_name(&self, cx: &AppContext) -> OsString;
+    fn file_name<'a>(&'a self, cx: &'a AppContext) -> &'a OsStr;
 
     fn is_deleted(&self) -> bool;
 

crates/plugin_runtime/Cargo.toml 🔗

@@ -15,4 +15,4 @@ pollster = "0.2.5"
 smol = "1.2.5"
 
 [build-dependencies]
-wasmtime = "0.38"
+wasmtime = { version = "0.38", features = ["all-arch"] }

crates/plugin_runtime/README.md 🔗

@@ -152,7 +152,7 @@ Plugins in the `plugins` directory are automatically recompiled and serialized t
 
 - `plugin.wasm` is the plugin compiled to Wasm. As a baseline, this should be about 4MB for debug builds and 2MB for release builds, but it depends on the specific plugin being built.
 
-- `plugin.wasm.pre` is the plugin compiled to Wasm *and additionally* precompiled to host-platform-agnostic cranelift-specific IR. This should be about 700KB for debug builds and 500KB in release builds. Each plugin takes about 1 or 2 seconds to compile to native code using cranelift, so precompiling plugins drastically reduces the startup time required to begin to run a plugin.
+- `plugin.wasm.pre` is the plugin compiled to Wasm *and additionally* precompiled to host-platform-specific native code, determined by the `TARGET` cargo exposes at compile-time. This should be about 700KB for debug builds and 500KB in release builds. Each plugin takes about 1 or 2 seconds to compile to native code using cranelift, so precompiling plugins drastically reduces the startup time required to begin to run a plugin.
 
 For all intents and purposes, it is *highly recommended* that you use precompiled plugins where possible, as they are much more lightweight and take much less time to instantiate.
 
@@ -246,18 +246,17 @@ Once all imports are marked, we can instantiate the plugin. To instantiate the p
 ```rust
 let plugin = builder
     .init(
-        true,
-        include_bytes!("../../../plugins/bin/cool_plugin.wasm.pre"),
+        PluginBinary::Precompiled(bytes),
     )
     .await
     .unwrap();
 ```
 
-The `.init` method currently takes two arguments:
+The `.init` method takes a single argument containing the plugin binary. 
 
-1. First, the 'precompiled' flag, indicating whether the plugin is *normal* (`.wasm`) or precompiled (`.wasm.pre`). When using a precompiled plugin, set this flag to `true`.
+1. If not precompiled, use `PluginBinary::Wasm(bytes)`. This supports both the WebAssembly Textual format (`.wat`) and the WebAssembly Binary format (`.wasm`). 
 
-2. Second, the raw plugin Wasm itself, as an array of bytes. When not precompiled, this can be either the Wasm binary format (`.wasm`) or the Wasm textual format (`.wat`). When precompiled, this must be the precompiled plugin (`.wasm.pre`).
+2. If precompiled, use `PluginBinary::Precompiled(bytes)`. This supports precompiled plugins ending in `.wasm.pre`. You need to be extra-careful when using precompiled plugins to ensure that the plugin target matches the target of the binary you are compiling.
 
 The `.init` method is asynchronous, and must be `.await`ed upon. If the plugin is malformed or doesn't import the right functions, an error will be raised.
 

crates/plugin_runtime/build.rs 🔗

@@ -26,7 +26,6 @@ fn main() {
         "release" => (&["--release"][..], "release"),
         unknown => panic!("unknown profile `{}`", unknown),
     };
-
     // Invoke cargo to build the plugins
     let build_successful = std::process::Command::new("cargo")
         .args([
@@ -42,8 +41,13 @@ fn main() {
         .success();
     assert!(build_successful);
 
+    // Get the target architecture for pre-cross-compilation of plugins
+    // and create and engine with the appropriate config
+    let target_triple = std::env::var("TARGET").unwrap().to_string();
+    println!("cargo:rerun-if-env-changed=TARGET");
+    let engine = create_default_engine(&target_triple);
+
     // Find all compiled binaries
-    let engine = create_default_engine();
     let binaries = std::fs::read_dir(base.join("target/wasm32-wasi").join(profile_target))
         .expect("Could not find compiled plugins in target");
 
@@ -66,11 +70,17 @@ fn main() {
     }
 }
 
-/// Creates a default engine for compiling Wasm.
-fn create_default_engine() -> Engine {
+/// Creates an engine with the default configuration.
+/// N.B. This must create an engine with the same config as the one
+/// in `plugin_runtime/src/plugin.rs`.
+fn create_default_engine(target_triple: &str) -> Engine {
     let mut config = Config::default();
+    config
+        .target(target_triple)
+        .expect(&format!("Could not set target to `{}`", target_triple));
     config.async_support(true);
-    Engine::new(&config).expect("Could not create engine")
+    config.consume_fuel(true);
+    Engine::new(&config).expect("Could not create precompilation engine")
 }
 
 fn precompile(path: &Path, engine: &Engine) {
@@ -80,7 +90,7 @@ fn precompile(path: &Path, engine: &Engine) {
         .expect("Could not precompile module");
     let out_path = path.parent().unwrap().join(&format!(
         "{}.pre",
-        path.file_name().unwrap().to_string_lossy()
+        path.file_name().unwrap().to_string_lossy(),
     ));
     let mut out_file = std::fs::File::create(out_path)
         .expect("Could not create output file for precompiled module");

crates/plugin_runtime/src/lib.rs 🔗

@@ -23,7 +23,7 @@ mod tests {
         }
 
         async {
-            let mut runtime = PluginBuilder::new_fuel_with_default_ctx(PluginYield::default_fuel())
+            let mut runtime = PluginBuilder::new_default()
                 .unwrap()
                 .host_function("mystery_number", |input: u32| input + 7)
                 .unwrap()

crates/plugin_runtime/src/plugin.rs 🔗

@@ -1,6 +1,5 @@
 use std::future::Future;
 
-use std::time::Duration;
 use std::{fs::File, marker::PhantomData, path::Path};
 
 use anyhow::{anyhow, Error};
@@ -55,34 +54,14 @@ impl<A: Serialize, R: DeserializeOwned> Clone for WasiFn<A, R> {
     }
 }
 
-pub struct PluginYieldEpoch {
-    delta: u64,
-    epoch: std::time::Duration,
-}
-
-pub struct PluginYieldFuel {
+pub struct Metering {
     initial: u64,
     refill: u64,
 }
 
-pub enum PluginYield {
-    Epoch {
-        yield_epoch: PluginYieldEpoch,
-        initialize_incrementer: Box<dyn FnOnce(Engine) -> () + Send>,
-    },
-    Fuel(PluginYieldFuel),
-}
-
-impl PluginYield {
-    pub fn default_epoch() -> PluginYieldEpoch {
-        PluginYieldEpoch {
-            delta: 1,
-            epoch: Duration::from_millis(1),
-        }
-    }
-
-    pub fn default_fuel() -> PluginYieldFuel {
-        PluginYieldFuel {
+impl Default for Metering {
+    fn default() -> Self {
+        Metering {
             initial: 1000,
             refill: 1000,
         }
@@ -97,110 +76,44 @@ pub struct PluginBuilder {
     wasi_ctx: WasiCtx,
     engine: Engine,
     linker: Linker<WasiCtxAlloc>,
-    yield_when: PluginYield,
+    metering: Metering,
 }
 
-impl PluginBuilder {
-    /// Creates an engine with the proper configuration given the yield mechanism in use
-    fn create_engine(yield_when: &PluginYield) -> Result<(Engine, Linker<WasiCtxAlloc>), Error> {
-        let mut config = Config::default();
-        config.async_support(true);
-
-        match yield_when {
-            PluginYield::Epoch { .. } => {
-                config.epoch_interruption(true);
-            }
-            PluginYield::Fuel(_) => {
-                config.consume_fuel(true);
-            }
-        }
-
-        let engine = Engine::new(&config)?;
-        let linker = Linker::new(&engine);
-        Ok((engine, linker))
-    }
-
-    /// Create a new [`PluginBuilder`] with the given WASI context.
-    /// Using the default context is a safe bet, see [`new_with_default_context`].
-    /// This plugin will yield after each fixed configurable epoch.
-    pub fn new_epoch<C>(
-        wasi_ctx: WasiCtx,
-        yield_epoch: PluginYieldEpoch,
-        spawn_detached_future: C,
-    ) -> Result<Self, Error>
-    where
-        C: FnOnce(std::pin::Pin<Box<dyn Future<Output = ()> + Send + 'static>>) -> ()
-            + Send
-            + 'static,
-    {
-        // we can't create the future until after initializing
-        // because we need the engine to load the plugin
-        let epoch = yield_epoch.epoch;
-        let initialize_incrementer = Box::new(move |engine: Engine| {
-            spawn_detached_future(Box::pin(async move {
-                loop {
-                    smol::Timer::after(epoch).await;
-                    engine.increment_epoch();
-                }
-            }))
-        });
-
-        let yield_when = PluginYield::Epoch {
-            yield_epoch,
-            initialize_incrementer,
-        };
-        let (engine, linker) = Self::create_engine(&yield_when)?;
-
-        Ok(PluginBuilder {
-            wasi_ctx,
-            engine,
-            linker,
-            yield_when,
-        })
-    }
+/// Creates an engine with the default configuration.
+/// N.B. This must create an engine with the same config as the one
+/// in `plugin_runtime/build.rs`.
+fn create_default_engine() -> Result<Engine, Error> {
+    let mut config = Config::default();
+    config.async_support(true);
+    config.consume_fuel(true);
+    Engine::new(&config)
+}
 
+impl PluginBuilder {
     /// Create a new [`PluginBuilder`] with the given WASI context.
     /// Using the default context is a safe bet, see [`new_with_default_context`].
     /// This plugin will yield after a configurable amount of fuel is consumed.
-    pub fn new_fuel(wasi_ctx: WasiCtx, yield_fuel: PluginYieldFuel) -> Result<Self, Error> {
-        let yield_when = PluginYield::Fuel(yield_fuel);
-        let (engine, linker) = Self::create_engine(&yield_when)?;
+    pub fn new(wasi_ctx: WasiCtx, metering: Metering) -> Result<Self, Error> {
+        let engine = create_default_engine()?;
+        let linker = Linker::new(&engine);
 
         Ok(PluginBuilder {
             wasi_ctx,
             engine,
             linker,
-            yield_when,
+            metering,
         })
     }
 
-    /// Create a new `WasiCtx` that inherits the
-    /// host processes' access to `stdout` and `stderr`.
-    fn default_ctx() -> WasiCtx {
-        WasiCtxBuilder::new()
-            .inherit_stdout()
-            .inherit_stderr()
-            .build()
-    }
-
-    /// Create a new `PluginBuilder` with the default `WasiCtx` (see [`default_ctx`]).
-    /// This plugin will yield after each fixed configurable epoch.
-    pub fn new_epoch_with_default_ctx<C>(
-        yield_epoch: PluginYieldEpoch,
-        spawn_detached_future: C,
-    ) -> Result<Self, Error>
-    where
-        C: FnOnce(std::pin::Pin<Box<dyn Future<Output = ()> + Send + 'static>>) -> ()
-            + Send
-            + 'static,
-    {
-        Self::new_epoch(Self::default_ctx(), yield_epoch, spawn_detached_future)
-    }
-
     /// Create a new `PluginBuilder` with the default `WasiCtx` (see [`default_ctx`]).
     /// This plugin will yield after a configurable amount of fuel is consumed.
-    pub fn new_fuel_with_default_ctx(yield_fuel: PluginYieldFuel) -> Result<Self, Error> {
-        Self::new_fuel(Self::default_ctx(), yield_fuel)
+    pub fn new_default() -> Result<Self, Error> {
+        let default_ctx = WasiCtxBuilder::new()
+            .inherit_stdout()
+            .inherit_stderr()
+            .build();
+        let metering = Metering::default();
+        Self::new(default_ctx, metering)
     }
 
     /// Add an `async` host function. See [`host_function`] for details.
@@ -433,19 +346,8 @@ impl Plugin {
         };
 
         // set up automatic yielding based on configuration
-        match plugin.yield_when {
-            PluginYield::Epoch {
-                yield_epoch: PluginYieldEpoch { delta, .. },
-                initialize_incrementer,
-            } => {
-                store.epoch_deadline_async_yield_and_update(delta);
-                initialize_incrementer(engine);
-            }
-            PluginYield::Fuel(PluginYieldFuel { initial, refill }) => {
-                store.add_fuel(initial).unwrap();
-                store.out_of_fuel_async_yield(u64::MAX, refill);
-            }
-        }
+        store.add_fuel(plugin.metering.initial).unwrap();
+        store.out_of_fuel_async_yield(u64::MAX, plugin.metering.refill);
 
         // load the provided module into the asynchronous runtime
         linker.module_async(&mut store, "", &module).await?;

crates/project/src/worktree.rs 🔗

@@ -1646,11 +1646,10 @@ impl language::File for File {
 
     /// Returns the last component of this handle's absolute path. If this handle refers to the root
     /// of its worktree, then this method will return the name of the worktree itself.
-    fn file_name(&self, cx: &AppContext) -> OsString {
+    fn file_name<'a>(&'a self, cx: &'a AppContext) -> &'a OsStr {
         self.path
             .file_name()
-            .map(|name| name.into())
-            .unwrap_or_else(|| OsString::from(&self.worktree.read(cx).root_name))
+            .unwrap_or_else(|| OsStr::new(&self.worktree.read(cx).root_name))
     }
 
     fn is_deleted(&self) -> bool {

crates/search/src/project_search.rs 🔗

@@ -4,7 +4,7 @@ use crate::{
     ToggleWholeWord,
 };
 use collections::HashMap;
-use editor::{Anchor, Autoscroll, Editor, MultiBuffer, SelectAll};
+use editor::{Anchor, Autoscroll, Editor, MultiBuffer, SelectAll, MAX_TAB_TITLE_LEN};
 use gpui::{
     actions, elements::*, platform::CursorStyle, Action, AppContext, ElementBox, Entity,
     ModelContext, ModelHandle, MutableAppContext, RenderContext, Subscription, Task, View,
@@ -26,8 +26,6 @@ use workspace::{
 
 actions!(project_search, [Deploy, SearchInNew, ToggleFocus]);
 
-const MAX_TAB_TITLE_LEN: usize = 24;
-
 #[derive(Default)]
 struct ActiveSearches(HashMap<WeakModelHandle<Project>, WeakViewHandle<ProjectSearchView>>);
 
@@ -220,7 +218,12 @@ impl Item for ProjectSearchView {
             .update(cx, |editor, cx| editor.deactivated(cx));
     }
 
-    fn tab_content(&self, tab_theme: &theme::Tab, cx: &gpui::AppContext) -> ElementBox {
+    fn tab_content(
+        &self,
+        _detail: Option<usize>,
+        tab_theme: &theme::Tab,
+        cx: &gpui::AppContext,
+    ) -> ElementBox {
         let settings = cx.global::<Settings>();
         let search_theme = &settings.theme.search;
         Flex::row()

crates/settings/src/settings.rs 🔗

@@ -25,6 +25,7 @@ pub struct Settings {
     pub buffer_font_size: f32,
     pub default_buffer_font_size: f32,
     pub hover_popover_enabled: bool,
+    pub show_completions_on_input: bool,
     pub vim_mode: bool,
     pub autosave: Autosave,
     pub editor_defaults: EditorSettings,
@@ -83,6 +84,8 @@ pub struct SettingsFileContent {
     #[serde(default)]
     pub hover_popover_enabled: Option<bool>,
     #[serde(default)]
+    pub show_completions_on_input: Option<bool>,
+    #[serde(default)]
     pub vim_mode: Option<bool>,
     #[serde(default)]
     pub autosave: Option<Autosave>,
@@ -118,6 +121,7 @@ impl Settings {
             buffer_font_size: defaults.buffer_font_size.unwrap(),
             default_buffer_font_size: defaults.buffer_font_size.unwrap(),
             hover_popover_enabled: defaults.hover_popover_enabled.unwrap(),
+            show_completions_on_input: defaults.show_completions_on_input.unwrap(),
             projects_online_by_default: defaults.projects_online_by_default.unwrap(),
             vim_mode: defaults.vim_mode.unwrap(),
             autosave: defaults.autosave.unwrap(),
@@ -160,6 +164,10 @@ impl Settings {
         merge(&mut self.buffer_font_size, data.buffer_font_size);
         merge(&mut self.default_buffer_font_size, data.buffer_font_size);
         merge(&mut self.hover_popover_enabled, data.hover_popover_enabled);
+        merge(
+            &mut self.show_completions_on_input,
+            data.show_completions_on_input,
+        );
         merge(&mut self.vim_mode, data.vim_mode);
         merge(&mut self.autosave, data.autosave);
 
@@ -219,6 +227,7 @@ impl Settings {
             buffer_font_size: 14.,
             default_buffer_font_size: 14.,
             hover_popover_enabled: true,
+            show_completions_on_input: true,
             vim_mode: false,
             autosave: Autosave::Off,
             editor_defaults: EditorSettings {
@@ -248,7 +257,7 @@ impl Settings {
 
 pub fn settings_file_json_schema(
     theme_names: Vec<String>,
-    language_names: Vec<String>,
+    language_names: &[String],
 ) -> serde_json::Value {
     let settings = SchemaSettings::draft07().with(|settings| {
         settings.option_add_null_type = false;
@@ -275,8 +284,13 @@ pub fn settings_file_json_schema(
         instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Object))),
         object: Some(Box::new(ObjectValidation {
             properties: language_names
-                .into_iter()
-                .map(|name| (name, Schema::new_ref("#/definitions/EditorSettings".into())))
+                .iter()
+                .map(|name| {
+                    (
+                        name.clone(),
+                        Schema::new_ref("#/definitions/EditorSettings".into()),
+                    )
+                })
                 .collect(),
             ..Default::default()
         })),

crates/terminal/src/modal.rs 🔗

@@ -16,8 +16,11 @@ pub fn deploy_modal(workspace: &mut Workspace, _: &DeployModal, cx: &mut ViewCon
     if let Some(StoredConnection(stored_connection)) = possible_connection {
         // Create a view from the stored connection
         workspace.toggle_modal(cx, |_, cx| {
-            cx.add_view(|cx| Terminal::from_connection(stored_connection, true, cx))
+            cx.add_view(|cx| Terminal::from_connection(stored_connection.clone(), true, cx))
         });
+        cx.set_global::<Option<StoredConnection>>(Some(StoredConnection(
+            stored_connection.clone(),
+        )));
     } else {
         // No connection was stored, create a new terminal
         if let Some(closed_terminal_handle) = workspace.toggle_modal(cx, |workspace, cx| {

crates/terminal/src/terminal.rs 🔗

@@ -261,7 +261,12 @@ impl View for Terminal {
 }
 
 impl Item for Terminal {
-    fn tab_content(&self, tab_theme: &theme::Tab, cx: &gpui::AppContext) -> ElementBox {
+    fn tab_content(
+        &self,
+        _detail: Option<usize>,
+        tab_theme: &theme::Tab,
+        cx: &gpui::AppContext,
+    ) -> ElementBox {
         let settings = cx.global::<Settings>();
         let search_theme = &settings.theme.search; //TODO properly integrate themes
 
@@ -405,7 +410,7 @@ mod tests {
 
     ///Basic integration test, can we get the terminal to show up, execute a command,
     //and produce noticable output?
-    #[gpui::test]
+    #[gpui::test(retries = 5)]
     async fn test_terminal(cx: &mut TestAppContext) {
         let terminal = cx.add_view(Default::default(), |cx| Terminal::new(None, false, cx));
 
@@ -416,7 +421,7 @@ mod tests {
             terminal.enter(&Enter, cx);
         });
 
-        cx.set_condition_duration(Some(Duration::from_secs(2)));
+        cx.set_condition_duration(Some(Duration::from_secs(5)));
         terminal
             .condition(cx, |terminal, cx| {
                 let term = terminal.connection.read(cx).term.clone();

crates/theme/src/theme.rs 🔗

@@ -93,6 +93,7 @@ pub struct Tab {
     pub container: ContainerStyle,
     #[serde(flatten)]
     pub label: LabelStyle,
+    pub description: ContainedText,
     pub spacing: f32,
     pub icon_width: f32,
     pub icon_close: Color,

crates/util/src/test/marked_text.rs 🔗

@@ -24,7 +24,7 @@ pub fn marked_text(marked_text: &str) -> (String, Vec<usize>) {
     (unmarked_text, markers.remove(&'|').unwrap_or_default())
 }
 
-#[derive(Eq, PartialEq, Hash)]
+#[derive(Clone, Eq, PartialEq, Hash)]
 pub enum TextRangeMarker {
     Empty(char),
     Range(char, char),

crates/workspace/src/pane.rs 🔗

@@ -71,10 +71,10 @@ const MAX_NAVIGATION_HISTORY_LEN: usize = 1024;
 
 pub fn init(cx: &mut MutableAppContext) {
     cx.add_action(|pane: &mut Pane, action: &ActivateItem, cx| {
-        pane.activate_item(action.0, true, true, cx);
+        pane.activate_item(action.0, true, true, false, cx);
     });
     cx.add_action(|pane: &mut Pane, _: &ActivateLastItem, cx| {
-        pane.activate_item(pane.items.len() - 1, true, true, cx);
+        pane.activate_item(pane.items.len() - 1, true, true, false, cx);
     });
     cx.add_action(|pane: &mut Pane, _: &ActivatePrevItem, cx| {
         pane.activate_prev_item(cx);
@@ -288,7 +288,7 @@ impl Pane {
                 {
                     let prev_active_item_index = pane.active_item_index;
                     pane.nav_history.borrow_mut().set_mode(mode);
-                    pane.activate_item(index, true, true, cx);
+                    pane.activate_item(index, true, true, false, cx);
                     pane.nav_history
                         .borrow_mut()
                         .set_mode(NavigationMode::Normal);
@@ -380,7 +380,7 @@ impl Pane {
                     && item.project_entry_ids(cx).as_slice() == &[project_entry_id]
                 {
                     let item = item.boxed_clone();
-                    pane.activate_item(ix, true, focus_item, cx);
+                    pane.activate_item(ix, true, focus_item, true, cx);
                     return Some(item);
                 }
             }
@@ -404,9 +404,11 @@ impl Pane {
         cx: &mut ViewContext<Workspace>,
     ) {
         // Prevent adding the same item to the pane more than once.
+        // If there is already an active item, reorder the desired item to be after it
+        // and activate it.
         if let Some(item_ix) = pane.read(cx).items.iter().position(|i| i.id() == item.id()) {
             pane.update(cx, |pane, cx| {
-                pane.activate_item(item_ix, activate_pane, focus_item, cx)
+                pane.activate_item(item_ix, activate_pane, focus_item, true, cx)
             });
             return;
         }
@@ -426,7 +428,7 @@ impl Pane {
             };
 
             pane.items.insert(item_ix, item);
-            pane.activate_item(item_ix, activate_pane, focus_item, cx);
+            pane.activate_item(item_ix, activate_pane, focus_item, false, cx);
             cx.notify();
         });
     }
@@ -465,13 +467,31 @@ impl Pane {
 
     pub fn activate_item(
         &mut self,
-        index: usize,
+        mut index: usize,
         activate_pane: bool,
         focus_item: bool,
+        move_after_current_active: bool,
         cx: &mut ViewContext<Self>,
     ) {
         use NavigationMode::{GoingBack, GoingForward};
         if index < self.items.len() {
+            if move_after_current_active {
+                // If there is already an active item, reorder the desired item to be after it
+                // and activate it.
+                if self.active_item_index != index && self.active_item_index < self.items.len() {
+                    let pane_to_activate = self.items.remove(index);
+                    if self.active_item_index < index {
+                        index = self.active_item_index + 1;
+                    } else if self.active_item_index < self.items.len() + 1 {
+                        index = self.active_item_index;
+                        // Index is less than active_item_index. Reordering will decrement the
+                        // active_item_index, so adjust it accordingly
+                        self.active_item_index = index - 1;
+                    }
+                    self.items.insert(index, pane_to_activate);
+                }
+            }
+
             let prev_active_item_ix = mem::replace(&mut self.active_item_index, index);
             if prev_active_item_ix != self.active_item_index
                 || matches!(self.nav_history.borrow().mode, GoingBack | GoingForward)
@@ -502,7 +522,7 @@ impl Pane {
         } else if self.items.len() > 0 {
             index = self.items.len() - 1;
         }
-        self.activate_item(index, true, true, cx);
+        self.activate_item(index, true, true, false, cx);
     }
 
     pub fn activate_next_item(&mut self, cx: &mut ViewContext<Self>) {
@@ -512,7 +532,7 @@ impl Pane {
         } else {
             index = 0;
         }
-        self.activate_item(index, true, true, cx);
+        self.activate_item(index, true, true, false, cx);
     }
 
     pub fn close_active_item(
@@ -641,10 +661,13 @@ impl Pane {
                 pane.update(&mut cx, |pane, cx| {
                     if let Some(item_ix) = pane.items.iter().position(|i| i.id() == item.id()) {
                         if item_ix == pane.active_item_index {
-                            if item_ix + 1 < pane.items.len() {
-                                pane.activate_next_item(cx);
-                            } else if item_ix > 0 {
+                            // Activate the previous item if possible.
+                            // This returns the user to the previously opened tab if they closed
+                            // a ne item they just navigated to.
+                            if item_ix > 0 {
                                 pane.activate_prev_item(cx);
+                            } else if item_ix + 1 < pane.items.len() {
+                                pane.activate_next_item(cx);
                             }
                         }
 
@@ -712,7 +735,7 @@ impl Pane {
 
         if has_conflict && can_save {
             let mut answer = pane.update(cx, |pane, cx| {
-                pane.activate_item(item_ix, true, true, cx);
+                pane.activate_item(item_ix, true, true, false, cx);
                 cx.prompt(
                     PromptLevel::Warning,
                     CONFLICT_MESSAGE,
@@ -733,7 +756,7 @@ impl Pane {
             });
             let should_save = if should_prompt_for_save && !will_autosave {
                 let mut answer = pane.update(cx, |pane, cx| {
-                    pane.activate_item(item_ix, true, true, cx);
+                    pane.activate_item(item_ix, true, true, false, cx);
                     cx.prompt(
                         PromptLevel::Warning,
                         DIRTY_MESSAGE,
@@ -840,8 +863,10 @@ impl Pane {
             } else {
                 None
             };
+
             let mut row = Flex::row().scrollable::<Tabs, _>(1, autoscroll, cx);
-            for (ix, item) in self.items.iter().enumerate() {
+            for (ix, (item, detail)) in self.items.iter().zip(self.tab_details(cx)).enumerate() {
+                let detail = if detail == 0 { None } else { Some(detail) };
                 let is_active = ix == self.active_item_index;
 
                 row.add_child({
@@ -850,7 +875,7 @@ impl Pane {
                     } else {
                         theme.workspace.tab.clone()
                     };
-                    let title = item.tab_content(&tab_style, cx);
+                    let title = item.tab_content(detail, &tab_style, cx);
 
                     let mut style = if is_active {
                         theme.workspace.active_tab.clone()
@@ -971,6 +996,43 @@ impl Pane {
             row.boxed()
         })
     }
+
+    fn tab_details(&self, cx: &AppContext) -> Vec<usize> {
+        let mut tab_details = (0..self.items.len()).map(|_| 0).collect::<Vec<_>>();
+
+        let mut tab_descriptions = HashMap::default();
+        let mut done = false;
+        while !done {
+            done = true;
+
+            // Store item indices by their tab description.
+            for (ix, (item, detail)) in self.items.iter().zip(&tab_details).enumerate() {
+                if let Some(description) = item.tab_description(*detail, cx) {
+                    if *detail == 0
+                        || Some(&description) != item.tab_description(detail - 1, cx).as_ref()
+                    {
+                        tab_descriptions
+                            .entry(description)
+                            .or_insert(Vec::new())
+                            .push(ix);
+                    }
+                }
+            }
+
+            // If two or more items have the same tab description, increase their level
+            // of detail and try again.
+            for (_, item_ixs) in tab_descriptions.drain() {
+                if item_ixs.len() > 1 {
+                    done = false;
+                    for ix in item_ixs {
+                        tab_details[ix] += 1;
+                    }
+                }
+            }
+        }
+
+        tab_details
+    }
 }
 
 impl Entity for Pane {

crates/workspace/src/workspace.rs 🔗

@@ -256,7 +256,11 @@ pub trait Item: View {
     fn navigate(&mut self, _: Box<dyn Any>, _: &mut ViewContext<Self>) -> bool {
         false
     }
-    fn tab_content(&self, style: &theme::Tab, cx: &AppContext) -> ElementBox;
+    fn tab_description<'a>(&'a self, _: usize, _: &'a AppContext) -> Option<Cow<'a, str>> {
+        None
+    }
+    fn tab_content(&self, detail: Option<usize>, style: &theme::Tab, cx: &AppContext)
+        -> ElementBox;
     fn project_path(&self, cx: &AppContext) -> Option<ProjectPath>;
     fn project_entry_ids(&self, cx: &AppContext) -> SmallVec<[ProjectEntryId; 3]>;
     fn is_singleton(&self, cx: &AppContext) -> bool;
@@ -409,7 +413,9 @@ impl<T: FollowableItem> FollowableItemHandle for ViewHandle<T> {
 }
 
 pub trait ItemHandle: 'static + fmt::Debug {
-    fn tab_content(&self, style: &theme::Tab, cx: &AppContext) -> ElementBox;
+    fn tab_description<'a>(&self, detail: usize, cx: &'a AppContext) -> Option<Cow<'a, str>>;
+    fn tab_content(&self, detail: Option<usize>, style: &theme::Tab, cx: &AppContext)
+        -> ElementBox;
     fn project_path(&self, cx: &AppContext) -> Option<ProjectPath>;
     fn project_entry_ids(&self, cx: &AppContext) -> SmallVec<[ProjectEntryId; 3]>;
     fn is_singleton(&self, cx: &AppContext) -> bool;
@@ -463,8 +469,17 @@ impl dyn ItemHandle {
 }
 
 impl<T: Item> ItemHandle for ViewHandle<T> {
-    fn tab_content(&self, style: &theme::Tab, cx: &AppContext) -> ElementBox {
-        self.read(cx).tab_content(style, cx)
+    fn tab_description<'a>(&self, detail: usize, cx: &'a AppContext) -> Option<Cow<'a, str>> {
+        self.read(cx).tab_description(detail, cx)
+    }
+
+    fn tab_content(
+        &self,
+        detail: Option<usize>,
+        style: &theme::Tab,
+        cx: &AppContext,
+    ) -> ElementBox {
+        self.read(cx).tab_content(detail, style, cx)
     }
 
     fn project_path(&self, cx: &AppContext) -> Option<ProjectPath> {
@@ -562,7 +577,7 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
             if T::should_activate_item_on_event(event) {
                 pane.update(cx, |pane, cx| {
                     if let Some(ix) = pane.index_for_item(&item) {
-                        pane.activate_item(ix, true, true, cx);
+                        pane.activate_item(ix, true, true, false, cx);
                         pane.activate(cx);
                     }
                 });
@@ -1507,7 +1522,7 @@ impl Workspace {
         });
         if let Some((pane, ix)) = result {
             self.activate_pane(pane.clone(), cx);
-            pane.update(cx, |pane, cx| pane.activate_item(ix, true, true, cx));
+            pane.update(cx, |pane, cx| pane.activate_item(ix, true, true, false, cx));
             true
         } else {
             false
@@ -2686,11 +2701,62 @@ fn open_new(app_state: &Arc<AppState>, cx: &mut MutableAppContext) {
 
 #[cfg(test)]
 mod tests {
+    use std::cell::Cell;
+
     use super::*;
     use gpui::{executor::Deterministic, ModelHandle, TestAppContext, ViewContext};
     use project::{FakeFs, Project, ProjectEntryId};
     use serde_json::json;
 
+    #[gpui::test]
+    async fn test_tab_disambiguation(cx: &mut TestAppContext) {
+        cx.foreground().forbid_parking();
+        Settings::test_async(cx);
+
+        let fs = FakeFs::new(cx.background());
+        let project = Project::test(fs, [], cx).await;
+        let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project.clone(), cx));
+
+        // Adding an item with no ambiguity renders the tab without detail.
+        let item1 = cx.add_view(window_id, |_| {
+            let mut item = TestItem::new();
+            item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]);
+            item
+        });
+        workspace.update(cx, |workspace, cx| {
+            workspace.add_item(Box::new(item1.clone()), cx);
+        });
+        item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), None));
+
+        // Adding an item that creates ambiguity increases the level of detail on
+        // both tabs.
+        let item2 = cx.add_view(window_id, |_| {
+            let mut item = TestItem::new();
+            item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
+            item
+        });
+        workspace.update(cx, |workspace, cx| {
+            workspace.add_item(Box::new(item2.clone()), cx);
+        });
+        item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
+        item2.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
+
+        // Adding an item that creates ambiguity increases the level of detail only
+        // on the ambiguous tabs. In this case, the ambiguity can't be resolved so
+        // we stop at the highest detail available.
+        let item3 = cx.add_view(window_id, |_| {
+            let mut item = TestItem::new();
+            item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
+            item
+        });
+        workspace.update(cx, |workspace, cx| {
+            workspace.add_item(Box::new(item3.clone()), cx);
+        });
+        item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
+        item2.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
+        item3.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
+    }
+
     #[gpui::test]
     async fn test_tracking_active_path(cx: &mut TestAppContext) {
         cx.foreground().forbid_parking();
@@ -2880,7 +2946,7 @@ mod tests {
 
         let close_items = workspace.update(cx, |workspace, cx| {
             pane.update(cx, |pane, cx| {
-                pane.activate_item(1, true, true, cx);
+                pane.activate_item(1, true, true, false, cx);
                 assert_eq!(pane.active_item().unwrap().id(), item2.id());
             });
 
@@ -3211,6 +3277,8 @@ mod tests {
         project_entry_ids: Vec<ProjectEntryId>,
         project_path: Option<ProjectPath>,
         nav_history: Option<ItemNavHistory>,
+        tab_descriptions: Option<Vec<&'static str>>,
+        tab_detail: Cell<Option<usize>>,
     }
 
     enum TestItemEvent {
@@ -3230,6 +3298,8 @@ mod tests {
                 project_entry_ids: self.project_entry_ids.clone(),
                 project_path: self.project_path.clone(),
                 nav_history: None,
+                tab_descriptions: None,
+                tab_detail: Default::default(),
             }
         }
     }
@@ -3247,6 +3317,8 @@ mod tests {
                 project_path: None,
                 is_singleton: true,
                 nav_history: None,
+                tab_descriptions: None,
+                tab_detail: Default::default(),
             }
         }
 
@@ -3277,7 +3349,15 @@ mod tests {
     }
 
     impl Item for TestItem {
-        fn tab_content(&self, _: &theme::Tab, _: &AppContext) -> ElementBox {
+        fn tab_description<'a>(&'a self, detail: usize, _: &'a AppContext) -> Option<Cow<'a, str>> {
+            self.tab_descriptions.as_ref().and_then(|descriptions| {
+                let description = *descriptions.get(detail).or(descriptions.last())?;
+                Some(description.into())
+            })
+        }
+
+        fn tab_content(&self, detail: Option<usize>, _: &theme::Tab, _: &AppContext) -> ElementBox {
+            self.tab_detail.set(detail);
             Empty::new().boxed()
         }
 

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.46.0"
+version = "0.47.1"
 
 [lib]
 name = "zed"

crates/zed/src/languages.rs 🔗

@@ -1,5 +1,6 @@
 use gpui::executor::Background;
 pub use language::*;
+use lazy_static::lazy_static;
 use rust_embed::RustEmbed;
 use std::{borrow::Cow, str, sync::Arc};
 use util::ResultExt;
@@ -17,6 +18,21 @@ mod typescript;
 #[exclude = "*.rs"]
 struct LanguageDir;
 
+// TODO - Remove this once the `init` function is synchronous again.
+lazy_static! {
+    pub static ref LANGUAGE_NAMES: Vec<String> = LanguageDir::iter()
+        .filter_map(|path| {
+            if path.ends_with("config.toml") {
+                let config = LanguageDir::get(&path)?;
+                let config = toml::from_slice::<LanguageConfig>(&config.data).ok()?;
+                Some(config.name.to_string())
+            } else {
+                None
+            }
+        })
+        .collect();
+}
+
 pub async fn init(languages: Arc<LanguageRegistry>, executor: Arc<Background>) {
     for (name, grammar, lsp_adapter) in [
         (

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

@@ -5,16 +5,12 @@ use collections::HashMap;
 use futures::lock::Mutex;
 use gpui::executor::Background;
 use language::{LanguageServerName, LspAdapter};
-use plugin_runtime::{Plugin, PluginBinary, PluginBuilder, PluginYield, WasiFn};
+use plugin_runtime::{Plugin, PluginBinary, PluginBuilder, WasiFn};
 use std::{any::Any, path::PathBuf, sync::Arc};
 use util::ResultExt;
 
 pub async fn new_json(executor: Arc<Background>) -> Result<PluginLspAdapter> {
-    let executor_ref = executor.clone();
-    let plugin =
-        PluginBuilder::new_epoch_with_default_ctx(PluginYield::default_epoch(), move |future| {
-            executor_ref.spawn(future).detach()
-        })?
+    let plugin = PluginBuilder::new_default()?
         .host_function_async("command", |command: String| async move {
             let mut args = command.split(' ');
             let command = args.next().unwrap();
@@ -26,7 +22,7 @@ pub async fn new_json(executor: Arc<Background>) -> Result<PluginLspAdapter> {
                 .map(|output| output.stdout)
         })?
         .init(PluginBinary::Precompiled(include_bytes!(
-            "../../../../plugins/bin/json_language.wasm.pre"
+            "../../../../plugins/bin/json_language.wasm.pre",
         )))
         .await?;
 
@@ -46,6 +42,7 @@ pub struct PluginLspAdapter {
 }
 
 impl PluginLspAdapter {
+    #[allow(unused)]
     pub async fn new(mut plugin: Plugin, executor: Arc<Background>) -> Result<Self> {
         Ok(Self {
             name: plugin.function("name")?,

crates/zed/src/zed.rs 🔗

@@ -102,14 +102,14 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::MutableAppContext) {
         let app_state = app_state.clone();
         move |_: &mut Workspace, _: &OpenSettings, cx: &mut ViewContext<Workspace>| {
             open_config_file(&SETTINGS_PATH, app_state.clone(), cx, || {
-                let header = Assets.load("settings/header-comments.json").unwrap();
-                let json = Assets.load("settings/default.json").unwrap();
-                let header = str::from_utf8(header.as_ref()).unwrap();
-                let json = str::from_utf8(json.as_ref()).unwrap();
-                let mut content = Rope::new();
-                content.push(header);
-                content.push(json);
-                content
+                str::from_utf8(
+                    Assets
+                        .load("settings/initial_user_settings.json")
+                        .unwrap()
+                        .as_ref(),
+                )
+                .unwrap()
+                .into()
             });
         }
     });
@@ -209,7 +209,7 @@ pub fn initialize_workspace(
     cx.emit(workspace::Event::PaneAdded(workspace.active_pane().clone()));
 
     let theme_names = app_state.themes.list().collect();
-    let language_names = app_state.languages.language_names();
+    let language_names = &languages::LANGUAGE_NAMES;
 
     workspace.project().update(cx, |project, cx| {
         let action_names = cx.all_action_names().collect::<Vec<_>>();

styles/src/styleTree/terminal.ts 🔗

@@ -1,7 +1,14 @@
 import Theme from "../themes/common/theme";
-import { border, modalShadow } from "./components";
+import { border, modalShadow, player } from "./components";
 
 export default function terminal(theme: Theme) {
+  /**
+  * Colors are controlled per-cell in the terminal grid. 
+  * Cells can be set to any of these more 'theme-capable' colors
+  * or can be set directly with RGB values. 
+  * Here are the common interpretations of these names:
+  * https://en.wikipedia.org/wiki/ANSI_escape_code#Colors
+  */
   let colors = {
     black: theme.ramps.neutral(0).hex(),
     red: theme.ramps.red(0.5).hex(),
@@ -11,7 +18,7 @@ export default function terminal(theme: Theme) {
     magenta: theme.ramps.magenta(0.5).hex(),
     cyan: theme.ramps.cyan(0.5).hex(),
     white: theme.ramps.neutral(7).hex(),
-    brightBlack: theme.ramps.neutral(2).hex(),
+    brightBlack: theme.ramps.neutral(4).hex(),
     brightRed: theme.ramps.red(0.25).hex(),
     brightGreen: theme.ramps.green(0.25).hex(),
     brightYellow: theme.ramps.yellow(0.25).hex(),
@@ -19,10 +26,19 @@ export default function terminal(theme: Theme) {
     brightMagenta: theme.ramps.magenta(0.25).hex(),
     brightCyan: theme.ramps.cyan(0.25).hex(),
     brightWhite: theme.ramps.neutral(7).hex(),
+    /**
+     * Default color for characters
+     */
     foreground: theme.ramps.neutral(7).hex(),
+    /**
+     * Default color for the rectangle background of a cell
+     */
     background: theme.ramps.neutral(0).hex(),
     modalBackground: theme.ramps.neutral(1).hex(),
-    cursor: theme.ramps.neutral(7).hex(),
+    /**
+     * Default color for the cursor
+     */
+    cursor: player(theme, 1).selection.cursor,
     dimBlack: theme.ramps.neutral(7).hex(),
     dimRed: theme.ramps.red(0.75).hex(),
     dimGreen: theme.ramps.green(0.75).hex(),

styles/src/styleTree/workspace.ts 🔗

@@ -27,6 +27,10 @@ export default function workspace(theme: Theme) {
       left: 8,
       right: 8,
     },
+    description: {
+      margin: { left: 6, top: 1 },
+      ...text(theme, "sans", "muted", { size: "2xs" })
+    }
   };
 
   const activeTab = {