.gitignore 🔗
@@ -8,3 +8,4 @@
/crates/collab/static/styles.css
/vendor/bin
/assets/themes/*.json
+dump.rdb
Mikayla Maki created
.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(-)
@@ -8,3 +8,4 @@
/crates/collab/static/styles.css
/vendor/bin
/assets/themes/*.json
+dump.rdb
@@ -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",
@@ -1,2 +1,3 @@
web: cd ../zed.dev && PORT=3000 npx next dev
collab: cd crates/collab && cargo run
+redis: redis-server
@@ -23,6 +23,12 @@ script/sqlx migrate run
script/seed-db
```
+Install Redis:
+
+```
+brew install redis
+```
+
Run the web frontend and the collaboration server.
```
@@ -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": {
@@ -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
+}
@@ -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
@@ -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,
@@ -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" }
@@ -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()
})
}
@@ -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,
@@ -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);
});
@@ -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()));
+ }
+}
@@ -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();
}
}
@@ -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;
@@ -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> {
@@ -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;
@@ -15,4 +15,4 @@ pollster = "0.2.5"
smol = "1.2.5"
[build-dependencies]
-wasmtime = "0.38"
+wasmtime = { version = "0.38", features = ["all-arch"] }
@@ -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.
@@ -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");
@@ -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()
@@ -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?;
@@ -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 {
@@ -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()
@@ -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()
})),
@@ -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| {
@@ -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();
@@ -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,
@@ -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),
@@ -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 {
@@ -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()
}
@@ -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"
@@ -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 [
(
@@ -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")?,
@@ -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<_>>();
@@ -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(),
@@ -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 = {