Merge branch 'main' into keybindings-grind

This commit is contained in:
Mikayla Maki 2022-07-15 11:24:16 -07:00 committed by GitHub
commit f38206f819
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
38 changed files with 882 additions and 468 deletions

1
.gitignore vendored
View file

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

3
Cargo.lock generated
View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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>) { fn action_dispatched(&mut self, action_id: TypeId, cx: &mut ViewContext<Self>) {
if let Some(ix) = self if let Some(ix) = self
.items .items

View file

@ -501,7 +501,12 @@ impl ProjectDiagnosticsEditor {
} }
impl workspace::Item for 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( render_summary(
&self.summary, &self.summary,
&style.label.text, &style.label.text,

View file

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

View file

@ -4,6 +4,7 @@ mod highlight_matching_bracket;
mod hover_popover; mod hover_popover;
pub mod items; pub mod items;
mod link_go_to_definition; mod link_go_to_definition;
mod mouse_context_menu;
pub mod movement; pub mod movement;
mod multi_buffer; mod multi_buffer;
pub mod selections_collection; pub mod selections_collection;
@ -34,6 +35,7 @@ use gpui::{
}; };
use highlight_matching_bracket::refresh_matching_bracket_highlights; use highlight_matching_bracket::refresh_matching_bracket_highlights;
use hover_popover::{hide_hover, HoverState}; use hover_popover::{hide_hover, HoverState};
pub use items::MAX_TAB_TITLE_LEN;
pub use language::{char_kind, CharKind}; pub use language::{char_kind, CharKind};
use language::{ use language::{
BracketPair, Buffer, CodeAction, CodeLabel, Completion, Diagnostic, DiagnosticSeverity, BracketPair, Buffer, CodeAction, CodeLabel, Completion, Diagnostic, DiagnosticSeverity,
@ -319,6 +321,7 @@ pub fn init(cx: &mut MutableAppContext) {
hover_popover::init(cx); hover_popover::init(cx);
link_go_to_definition::init(cx); link_go_to_definition::init(cx);
mouse_context_menu::init(cx);
workspace::register_project_item::<Editor>(cx); workspace::register_project_item::<Editor>(cx);
workspace::register_followable_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>>)>, background_highlights: BTreeMap<TypeId, (fn(&Theme) -> Color, Vec<Range<Anchor>>)>,
nav_history: Option<ItemNavHistory>, nav_history: Option<ItemNavHistory>,
context_menu: Option<ContextMenu>, context_menu: Option<ContextMenu>,
mouse_context_menu: ViewHandle<context_menu::ContextMenu>,
completion_tasks: Vec<(CompletionId, Task<Option<()>>)>, completion_tasks: Vec<(CompletionId, Task<Option<()>>)>,
next_completion_id: CompletionId, next_completion_id: CompletionId,
available_code_actions: Option<(ModelHandle<Buffer>, Arc<[CodeAction]>)>, available_code_actions: Option<(ModelHandle<Buffer>, Arc<[CodeAction]>)>,
@ -1010,11 +1014,11 @@ impl Editor {
background_highlights: Default::default(), background_highlights: Default::default(),
nav_history: None, nav_history: None,
context_menu: None, context_menu: None,
mouse_context_menu: cx.add_view(|cx| context_menu::ContextMenu::new(cx)),
completion_tasks: Default::default(), completion_tasks: Default::default(),
next_completion_id: 0, next_completion_id: 0,
available_code_actions: Default::default(), available_code_actions: Default::default(),
code_actions_task: Default::default(), code_actions_task: Default::default(),
document_highlights_task: Default::default(), document_highlights_task: Default::default(),
pending_rename: Default::default(), pending_rename: Default::default(),
searchable: true, searchable: true,
@ -1070,7 +1074,7 @@ impl Editor {
&self.buffer &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) self.buffer().read(cx).title(cx)
} }
@ -1596,7 +1600,7 @@ impl Editor {
s.delete(newest_selection.id) 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>) { 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(); let selection = self.selections.newest_anchor();
if self if self
.buffer .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 { fn ui_name() -> &'static str {
@ -6225,7 +6238,8 @@ pub fn styled_runs_for_code_label<'a>(
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::test::{ use crate::test::{
assert_text_with_selections, build_editor, select_ranges, EditorTestContext, assert_text_with_selections, build_editor, select_ranges, EditorLspTestContext,
EditorTestContext,
}; };
use super::*; use super::*;
@ -6236,7 +6250,6 @@ mod tests {
}; };
use indoc::indoc; use indoc::indoc;
use language::{FakeLspAdapter, LanguageConfig}; use language::{FakeLspAdapter, LanguageConfig};
use lsp::FakeLanguageServer;
use project::FakeFs; use project::FakeFs;
use settings::EditorSettings; use settings::EditorSettings;
use std::{cell::RefCell, rc::Rc, time::Instant}; use std::{cell::RefCell, rc::Rc, time::Instant};
@ -6244,7 +6257,9 @@ mod tests {
use unindent::Unindent; use unindent::Unindent;
use util::{ use util::{
assert_set_eq, 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}; use workspace::{FollowableItem, ItemHandle, NavigationEntry, Pane};
@ -9524,199 +9539,182 @@ mod tests {
#[gpui::test] #[gpui::test]
async fn test_completion(cx: &mut gpui::TestAppContext) { async fn test_completion(cx: &mut gpui::TestAppContext) {
let mut language = Language::new( let mut cx = EditorLspTestContext::new_rust(
LanguageConfig { lsp::ServerCapabilities {
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 { completion_provider: Some(lsp::CompletionOptions {
trigger_characters: Some(vec![".".to_string(), ":".to_string()]), trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
..Default::default() ..Default::default()
}), }),
..Default::default() ..Default::default()
}, },
..Default::default() cx,
}))
.await;
let text = "
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);
});
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"),
],
) )
.await; .await;
editor
.condition(&cx, |editor, _| editor.context_menu_visible())
.await;
let apply_additional_edits = editor.update(cx, |editor, cx| { cx.set_state(indoc! {"
editor.move_down(&MoveDown, cx); one|
let apply_additional_edits = editor
.confirm_completion(&ConfirmCompletion::default(), cx)
.unwrap();
assert_eq!(
editor.text(cx),
"
one.second_completion
two two
three three"});
" cx.simulate_keystroke(".");
.unindent() handle_completion_request(
); &mut cx,
apply_additional_edits indoc! {"
one.|<>
two
three"},
vec!["first_completion", "second_completion"],
)
.await;
cx.condition(|editor, _| editor.context_menu_visible())
.await;
let apply_additional_edits = cx.update_editor(|editor, cx| {
editor.move_down(&MoveDown, cx);
editor
.confirm_completion(&ConfirmCompletion::default(), cx)
.unwrap()
}); });
cx.assert_editor_state(indoc! {"
one.second_completion|
two
three"});
handle_resolve_completion_request( handle_resolve_completion_request(
&mut fake_server, &mut cx,
Some((Point::new(2, 5)..Point::new(2, 5), "\nadditional edit")), Some((
) indoc! {"
.await;
apply_additional_edits.await.unwrap();
assert_eq!(
editor.read_with(cx, |editor, cx| editor.text(cx)),
"
one.second_completion one.second_completion
two two
three<>"},
"\nadditional edit",
)),
)
.await;
apply_additional_edits.await.unwrap();
cx.assert_editor_state(indoc! {"
one.second_completion|
two
three three
additional edit additional edit"});
"
.unindent()
);
editor.update(cx, |editor, cx| { cx.set_state(indoc! {"
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),
])
});
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());
});
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"),
],
)
.await;
editor
.condition(&cx, |editor, _| editor.context_menu_visible())
.await;
editor.update(cx, |editor, cx| {
editor.handle_input(&Input("i".to_string()), cx);
});
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"),
],
)
.await;
editor
.condition(&cx, |editor, _| editor.context_menu_visible())
.await;
let apply_additional_edits = editor.update(cx, |editor, cx| {
let apply_additional_edits = editor
.confirm_completion(&ConfirmCompletion::default(), cx)
.unwrap();
assert_eq!(
editor.text(cx),
"
one.second_completion one.second_completion
two sixth_completion two|
three sixth_completion three|
additional edit additional edit"});
" cx.simulate_keystroke(" ");
.unindent() assert!(cx.editor(|e, _| e.context_menu.is_none()));
); cx.simulate_keystroke("s");
apply_additional_edits 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 cx,
indoc! {"
one.second_completion
two s
three <s|>
additional edit"},
vec!["fourth_completion", "fifth_completion", "sixth_completion"],
)
.await;
cx.condition(|editor, _| editor.context_menu_visible())
.await;
cx.simulate_keystroke("i");
handle_completion_request(
&mut cx,
indoc! {"
one.second_completion
two si
three <si|>
additional edit"},
vec!["fourth_completion", "fifth_completion", "sixth_completion"],
)
.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()
}); });
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(); apply_additional_edits.await.unwrap();
async fn handle_completion_request( cx.update(|cx| {
fake: &mut FakeLanguageServer, cx.update_global::<Settings, _, _>(|settings, _| {
path: &'static str, settings.show_completions_on_input = false;
position: Point, })
completions: Vec<(Range<Point>, &'static str)>, });
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(); let completions = completions.clone();
async move { async move {
assert_eq!( assert_eq!(params.text_document_position.text_document.uri, url.clone());
params.text_document_position.text_document.uri,
lsp::Url::from_file_path(path).unwrap()
);
assert_eq!( assert_eq!(
params.text_document_position.position, params.text_document_position.position,
lsp::Position::new(position.row, position.column) complete_from_position
); );
Ok(Some(lsp::CompletionResponse::Array( Ok(Some(lsp::CompletionResponse::Array(
completions completions
.iter() .iter()
.map(|(range, new_text)| lsp::CompletionItem { .map(|completion_text| lsp::CompletionItem {
label: new_text.to_string(), label: completion_text.to_string(),
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
range: lsp::Range::new( range: replace_range.clone(),
lsp::Position::new(range.start.row, range.start.column), new_text: completion_text.to_string(),
lsp::Position::new(range.start.row, range.start.column),
),
new_text: new_text.to_string(),
})), })),
..Default::default() ..Default::default()
}) })
@ -9728,23 +9726,26 @@ mod tests {
.await; .await;
} }
async fn handle_resolve_completion_request( async fn handle_resolve_completion_request<'a>(
fake: &mut FakeLanguageServer, cx: &mut EditorLspTestContext<'a>,
edit: Option<(Range<Point>, &'static str)>, 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(); let edit = edit.clone();
async move { async move {
Ok(lsp::CompletionItem { Ok(lsp::CompletionItem {
additional_text_edits: edit.map(|(range, new_text)| { additional_text_edits: edit,
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(),
)]
}),
..Default::default() ..Default::default()
}) })
} }

View file

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

View file

@ -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 anyhow::{anyhow, Result};
use futures::FutureExt; use futures::FutureExt;
use gpui::{ use gpui::{
@ -10,12 +12,18 @@ use project::{File, Project, ProjectEntryId, ProjectPath};
use rpc::proto::{self, update_view}; use rpc::proto::{self, update_view};
use settings::Settings; use settings::Settings;
use smallvec::SmallVec; 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 text::{Point, Selection};
use util::TryFutureExt; use util::TryFutureExt;
use workspace::{FollowableItem, Item, ItemHandle, ItemNavHistory, ProjectItem, StatusItemView}; use workspace::{FollowableItem, Item, ItemHandle, ItemNavHistory, ProjectItem, StatusItemView};
pub const FORMAT_TIMEOUT: Duration = Duration::from_secs(2); pub const FORMAT_TIMEOUT: Duration = Duration::from_secs(2);
pub const MAX_TAB_TITLE_LEN: usize = 24;
impl FollowableItem for Editor { impl FollowableItem for Editor {
fn from_state_proto( fn from_state_proto(
@ -292,9 +300,44 @@ impl Item for Editor {
} }
} }
fn tab_content(&self, style: &theme::Tab, cx: &AppContext) -> ElementBox { fn tab_description<'a>(&'a self, detail: usize, cx: &'a AppContext) -> Option<Cow<'a, str>> {
let title = self.title(cx); match path_for_buffer(&self.buffer, detail, true, cx)? {
Label::new(title, style.label.clone()).boxed() 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> { fn project_path(&self, cx: &AppContext) -> Option<ProjectPath> {
@ -534,3 +577,42 @@ impl StatusItemView for CursorPosition {
cx.notify(); 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())
}
}

View file

@ -342,12 +342,11 @@ mod tests {
test();"}); test();"});
let mut requests = let mut requests =
cx.lsp cx.handle_request::<lsp::request::GotoDefinition, _, _>(move |url, _, _| async move {
.handle_request::<lsp::request::GotoDefinition, _, _>(move |_, _| async move {
Ok(Some(lsp::GotoDefinitionResponse::Link(vec![ Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
lsp::LocationLink { lsp::LocationLink {
origin_selection_range: Some(symbol_range), origin_selection_range: Some(symbol_range),
target_uri: lsp::Url::from_file_path("/root/dir/file.rs").unwrap(), target_uri: url.clone(),
target_range, target_range,
target_selection_range: target_range, target_selection_range: target_range,
}, },
@ -387,13 +386,12 @@ mod tests {
// Response without source range still highlights word // Response without source range still highlights word
cx.update_editor(|editor, _| editor.link_go_to_definition_state.last_mouse_location = None); cx.update_editor(|editor, _| editor.link_go_to_definition_state.last_mouse_location = None);
let mut requests = let mut requests =
cx.lsp cx.handle_request::<lsp::request::GotoDefinition, _, _>(move |url, _, _| async move {
.handle_request::<lsp::request::GotoDefinition, _, _>(move |_, _| async move {
Ok(Some(lsp::GotoDefinitionResponse::Link(vec![ Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
lsp::LocationLink { lsp::LocationLink {
// No origin range // No origin range
origin_selection_range: None, origin_selection_range: None,
target_uri: lsp::Url::from_file_path("/root/dir/file.rs").unwrap(), target_uri: url.clone(),
target_range, target_range,
target_selection_range: target_range, target_selection_range: target_range,
}, },
@ -495,12 +493,11 @@ mod tests {
test();"}); test();"});
let mut requests = let mut requests =
cx.lsp cx.handle_request::<lsp::request::GotoDefinition, _, _>(move |url, _, _| async move {
.handle_request::<lsp::request::GotoDefinition, _, _>(move |_, _| async move {
Ok(Some(lsp::GotoDefinitionResponse::Link(vec![ Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
lsp::LocationLink { lsp::LocationLink {
origin_selection_range: Some(symbol_range), origin_selection_range: Some(symbol_range),
target_uri: lsp::Url::from_file_path("/root/dir/file.rs").unwrap(), target_uri: url,
target_range, target_range,
target_selection_range: target_range, target_selection_range: target_range,
}, },
@ -584,12 +581,11 @@ mod tests {
test();"}); test();"});
let mut requests = let mut requests =
cx.lsp cx.handle_request::<lsp::request::GotoDefinition, _, _>(move |url, _, _| async move {
.handle_request::<lsp::request::GotoDefinition, _, _>(move |_, _| async move {
Ok(Some(lsp::GotoDefinitionResponse::Link(vec![ Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
lsp::LocationLink { lsp::LocationLink {
origin_selection_range: None, origin_selection_range: None,
target_uri: lsp::Url::from_file_path("/root/dir/file.rs").unwrap(), target_uri: url,
target_range, target_range,
target_selection_range: target_range, target_selection_range: target_range,
}, },

View file

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

View file

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

View file

@ -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 { self.collection.pending = Some(PendingSelection {
selection: Selection { selection: Selection {
id: post_inc(&mut self.collection.next_selection_id), id: post_inc(&mut self.collection.next_selection_id),
@ -398,6 +398,42 @@ impl<'a> MutableSelectionsCollection<'a> {
self.selections_changed = true; 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) { pub fn set_pending(&mut self, selection: Selection<Anchor>, mode: SelectMode) {
self.collection.pending = Some(PendingSelection { selection, mode }); self.collection.pending = Some(PendingSelection { selection, mode });
self.selections_changed = true; self.selections_changed = true;

View file

@ -4,12 +4,14 @@ use std::{
sync::Arc, sync::Arc,
}; };
use futures::StreamExt; use anyhow::Result;
use futures::{Future, StreamExt};
use indoc::indoc; use indoc::indoc;
use collections::BTreeMap; use collections::BTreeMap;
use gpui::{json, keymap::Keystroke, AppContext, ModelHandle, ViewContext, ViewHandle}; use gpui::{json, keymap::Keystroke, AppContext, ModelHandle, ViewContext, ViewHandle};
use language::{point_to_lsp, FakeLspAdapter, Language, LanguageConfig, Selection}; use language::{point_to_lsp, FakeLspAdapter, Language, LanguageConfig, Selection};
use lsp::request;
use project::Project; use project::Project;
use settings::Settings; use settings::Settings;
use util::{ 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 pub fn editor<F, T>(&mut self, read: F) -> T
where where
F: FnOnce(&Editor, &AppContext) -> T, F: FnOnce(&Editor, &AppContext) -> T,
@ -424,6 +433,7 @@ pub struct EditorLspTestContext<'a> {
pub cx: EditorTestContext<'a>, pub cx: EditorTestContext<'a>,
pub lsp: lsp::FakeLanguageServer, pub lsp: lsp::FakeLanguageServer,
pub workspace: ViewHandle<Workspace>, pub workspace: ViewHandle<Workspace>,
pub editor_lsp_url: lsp::Url,
} }
impl<'a> EditorLspTestContext<'a> { impl<'a> EditorLspTestContext<'a> {
@ -497,6 +507,7 @@ impl<'a> EditorLspTestContext<'a> {
}, },
lsp, lsp,
workspace, 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 { pub fn lsp_range(&mut self, marked_text: &str) -> lsp::Range {
let (unmarked, mut ranges) = marked_text_ranges_by(marked_text, vec![('[', ']').into()]); let (unmarked, mut ranges) = marked_text_ranges_by(marked_text, vec![('[', ']').into()]);
assert_eq!(unmarked, self.cx.buffer_text()); assert_eq!(unmarked, self.cx.buffer_text());
let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx));
let offset_range = ranges.remove(&('[', ']').into()).unwrap()[0].clone(); let offset_range = ranges.remove(&('[', ']').into()).unwrap()[0].clone();
let start_point = offset_range.start.to_point(&snapshot.buffer_snapshot); self.to_lsp_range(offset_range)
let end_point = offset_range.end.to_point(&snapshot.buffer_snapshot); }
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);
self.editor(|editor, cx| { self.editor(|editor, cx| {
let buffer = editor.buffer().read(cx); let buffer = editor.buffer().read(cx);
let start = point_to_lsp( 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 pub fn update_workspace<F, T>(&mut self, update: F) -> T
where where
F: FnOnce(&mut Workspace, &mut ViewContext<Workspace>) -> T, F: FnOnce(&mut Workspace, &mut ViewContext<Workspace>) -> T,
{ {
self.workspace.update(self.cx.cx, update) 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> { impl<'a> Deref for EditorLspTestContext<'a> {

View file

@ -20,7 +20,7 @@ use std::{
any::Any, any::Any,
cmp::{self, Ordering}, cmp::{self, Ordering},
collections::{BTreeMap, HashMap}, collections::{BTreeMap, HashMap},
ffi::OsString, ffi::OsStr,
future::Future, future::Future,
iter::{self, Iterator, Peekable}, iter::{self, Iterator, Peekable},
mem, 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 /// 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. /// 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; fn is_deleted(&self) -> bool;

View file

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

View file

@ -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` 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. 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 ```rust
let plugin = builder let plugin = builder
.init( .init(
true, PluginBinary::Precompiled(bytes),
include_bytes!("../../../plugins/bin/cool_plugin.wasm.pre"),
) )
.await .await
.unwrap(); .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. 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.

View file

@ -26,7 +26,6 @@ fn main() {
"release" => (&["--release"][..], "release"), "release" => (&["--release"][..], "release"),
unknown => panic!("unknown profile `{}`", unknown), unknown => panic!("unknown profile `{}`", unknown),
}; };
// Invoke cargo to build the plugins // Invoke cargo to build the plugins
let build_successful = std::process::Command::new("cargo") let build_successful = std::process::Command::new("cargo")
.args([ .args([
@ -42,8 +41,13 @@ fn main() {
.success(); .success();
assert!(build_successful); 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 // Find all compiled binaries
let engine = create_default_engine();
let binaries = std::fs::read_dir(base.join("target/wasm32-wasi").join(profile_target)) let binaries = std::fs::read_dir(base.join("target/wasm32-wasi").join(profile_target))
.expect("Could not find compiled plugins in target"); .expect("Could not find compiled plugins in target");
@ -66,11 +70,17 @@ fn main() {
} }
} }
/// Creates a default engine for compiling Wasm. /// Creates an engine with the default configuration.
fn create_default_engine() -> Engine { /// 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(); let mut config = Config::default();
config
.target(target_triple)
.expect(&format!("Could not set target to `{}`", target_triple));
config.async_support(true); 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) { fn precompile(path: &Path, engine: &Engine) {
@ -80,7 +90,7 @@ fn precompile(path: &Path, engine: &Engine) {
.expect("Could not precompile module"); .expect("Could not precompile module");
let out_path = path.parent().unwrap().join(&format!( let out_path = path.parent().unwrap().join(&format!(
"{}.pre", "{}.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) let mut out_file = std::fs::File::create(out_path)
.expect("Could not create output file for precompiled module"); .expect("Could not create output file for precompiled module");

View file

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

View file

@ -1,6 +1,5 @@
use std::future::Future; use std::future::Future;
use std::time::Duration;
use std::{fs::File, marker::PhantomData, path::Path}; use std::{fs::File, marker::PhantomData, path::Path};
use anyhow::{anyhow, Error}; use anyhow::{anyhow, Error};
@ -55,34 +54,14 @@ impl<A: Serialize, R: DeserializeOwned> Clone for WasiFn<A, R> {
} }
} }
pub struct PluginYieldEpoch { pub struct Metering {
delta: u64,
epoch: std::time::Duration,
}
pub struct PluginYieldFuel {
initial: u64, initial: u64,
refill: u64, refill: u64,
} }
pub enum PluginYield { impl Default for Metering {
Epoch { fn default() -> Self {
yield_epoch: PluginYieldEpoch, Metering {
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 {
initial: 1000, initial: 1000,
refill: 1000, refill: 1000,
} }
@ -97,110 +76,44 @@ pub struct PluginBuilder {
wasi_ctx: WasiCtx, wasi_ctx: WasiCtx,
engine: Engine, engine: Engine,
linker: Linker<WasiCtxAlloc>, linker: Linker<WasiCtxAlloc>,
yield_when: PluginYield, metering: Metering,
}
/// 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 { 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,
})
}
/// Create a new [`PluginBuilder`] with the given WASI context. /// Create a new [`PluginBuilder`] with the given WASI context.
/// Using the default context is a safe bet, see [`new_with_default_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. /// 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> { pub fn new(wasi_ctx: WasiCtx, metering: Metering) -> Result<Self, Error> {
let yield_when = PluginYield::Fuel(yield_fuel); let engine = create_default_engine()?;
let (engine, linker) = Self::create_engine(&yield_when)?; let linker = Linker::new(&engine);
Ok(PluginBuilder { Ok(PluginBuilder {
wasi_ctx, wasi_ctx,
engine, engine,
linker, linker,
yield_when, metering,
}) })
} }
/// Create a new `WasiCtx` that inherits the /// Create a new `PluginBuilder` with the default `WasiCtx` (see [`default_ctx`]).
/// host processes' access to `stdout` and `stderr`. /// This plugin will yield after a configurable amount of fuel is consumed.
fn default_ctx() -> WasiCtx { pub fn new_default() -> Result<Self, Error> {
WasiCtxBuilder::new() let default_ctx = WasiCtxBuilder::new()
.inherit_stdout() .inherit_stdout()
.inherit_stderr() .inherit_stderr()
.build() .build();
} let metering = Metering::default();
Self::new(default_ctx, metering)
/// 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)
} }
/// Add an `async` host function. See [`host_function`] for details. /// Add an `async` host function. See [`host_function`] for details.
@ -433,19 +346,8 @@ impl Plugin {
}; };
// set up automatic yielding based on configuration // set up automatic yielding based on configuration
match plugin.yield_when { store.add_fuel(plugin.metering.initial).unwrap();
PluginYield::Epoch { store.out_of_fuel_async_yield(u64::MAX, plugin.metering.refill);
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);
}
}
// load the provided module into the asynchronous runtime // load the provided module into the asynchronous runtime
linker.module_async(&mut store, "", &module).await?; linker.module_async(&mut store, "", &module).await?;

View file

@ -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 /// 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. /// 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 self.path
.file_name() .file_name()
.map(|name| name.into()) .unwrap_or_else(|| OsStr::new(&self.worktree.read(cx).root_name))
.unwrap_or_else(|| OsString::from(&self.worktree.read(cx).root_name))
} }
fn is_deleted(&self) -> bool { fn is_deleted(&self) -> bool {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -71,10 +71,10 @@ const MAX_NAVIGATION_HISTORY_LEN: usize = 1024;
pub fn init(cx: &mut MutableAppContext) { pub fn init(cx: &mut MutableAppContext) {
cx.add_action(|pane: &mut Pane, action: &ActivateItem, cx| { 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| { 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| { cx.add_action(|pane: &mut Pane, _: &ActivatePrevItem, cx| {
pane.activate_prev_item(cx); pane.activate_prev_item(cx);
@ -288,7 +288,7 @@ impl Pane {
{ {
let prev_active_item_index = pane.active_item_index; let prev_active_item_index = pane.active_item_index;
pane.nav_history.borrow_mut().set_mode(mode); 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 pane.nav_history
.borrow_mut() .borrow_mut()
.set_mode(NavigationMode::Normal); .set_mode(NavigationMode::Normal);
@ -380,7 +380,7 @@ impl Pane {
&& item.project_entry_ids(cx).as_slice() == &[project_entry_id] && item.project_entry_ids(cx).as_slice() == &[project_entry_id]
{ {
let item = item.boxed_clone(); 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); return Some(item);
} }
} }
@ -404,9 +404,11 @@ impl Pane {
cx: &mut ViewContext<Workspace>, cx: &mut ViewContext<Workspace>,
) { ) {
// Prevent adding the same item to the pane more than once. // 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()) { if let Some(item_ix) = pane.read(cx).items.iter().position(|i| i.id() == item.id()) {
pane.update(cx, |pane, cx| { 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; return;
} }
@ -426,7 +428,7 @@ impl Pane {
}; };
pane.items.insert(item_ix, item); 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(); cx.notify();
}); });
} }
@ -465,13 +467,31 @@ impl Pane {
pub fn activate_item( pub fn activate_item(
&mut self, &mut self,
index: usize, mut index: usize,
activate_pane: bool, activate_pane: bool,
focus_item: bool, focus_item: bool,
move_after_current_active: bool,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) { ) {
use NavigationMode::{GoingBack, GoingForward}; use NavigationMode::{GoingBack, GoingForward};
if index < self.items.len() { 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); let prev_active_item_ix = mem::replace(&mut self.active_item_index, index);
if prev_active_item_ix != self.active_item_index if prev_active_item_ix != self.active_item_index
|| matches!(self.nav_history.borrow().mode, GoingBack | GoingForward) || matches!(self.nav_history.borrow().mode, GoingBack | GoingForward)
@ -502,7 +522,7 @@ impl Pane {
} else if self.items.len() > 0 { } else if self.items.len() > 0 {
index = self.items.len() - 1; 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>) { pub fn activate_next_item(&mut self, cx: &mut ViewContext<Self>) {
@ -512,7 +532,7 @@ impl Pane {
} else { } else {
index = 0; index = 0;
} }
self.activate_item(index, true, true, cx); self.activate_item(index, true, true, false, cx);
} }
pub fn close_active_item( pub fn close_active_item(
@ -641,10 +661,13 @@ impl Pane {
pane.update(&mut cx, |pane, cx| { pane.update(&mut cx, |pane, cx| {
if let Some(item_ix) = pane.items.iter().position(|i| i.id() == item.id()) { if let Some(item_ix) = pane.items.iter().position(|i| i.id() == item.id()) {
if item_ix == pane.active_item_index { if item_ix == pane.active_item_index {
if item_ix + 1 < pane.items.len() { // Activate the previous item if possible.
pane.activate_next_item(cx); // This returns the user to the previously opened tab if they closed
} else if item_ix > 0 { // a ne item they just navigated to.
if item_ix > 0 {
pane.activate_prev_item(cx); 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 { if has_conflict && can_save {
let mut answer = pane.update(cx, |pane, cx| { 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( cx.prompt(
PromptLevel::Warning, PromptLevel::Warning,
CONFLICT_MESSAGE, CONFLICT_MESSAGE,
@ -733,7 +756,7 @@ impl Pane {
}); });
let should_save = if should_prompt_for_save && !will_autosave { let should_save = if should_prompt_for_save && !will_autosave {
let mut answer = pane.update(cx, |pane, cx| { 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( cx.prompt(
PromptLevel::Warning, PromptLevel::Warning,
DIRTY_MESSAGE, DIRTY_MESSAGE,
@ -840,8 +863,10 @@ impl Pane {
} else { } else {
None None
}; };
let mut row = Flex::row().scrollable::<Tabs, _>(1, autoscroll, cx); 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; let is_active = ix == self.active_item_index;
row.add_child({ row.add_child({
@ -850,7 +875,7 @@ impl Pane {
} else { } else {
theme.workspace.tab.clone() 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 { let mut style = if is_active {
theme.workspace.active_tab.clone() theme.workspace.active_tab.clone()
@ -971,6 +996,43 @@ impl Pane {
row.boxed() 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 { impl Entity for Pane {

View file

@ -256,7 +256,11 @@ pub trait Item: View {
fn navigate(&mut self, _: Box<dyn Any>, _: &mut ViewContext<Self>) -> bool { fn navigate(&mut self, _: Box<dyn Any>, _: &mut ViewContext<Self>) -> bool {
false 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_path(&self, cx: &AppContext) -> Option<ProjectPath>;
fn project_entry_ids(&self, cx: &AppContext) -> SmallVec<[ProjectEntryId; 3]>; fn project_entry_ids(&self, cx: &AppContext) -> SmallVec<[ProjectEntryId; 3]>;
fn is_singleton(&self, cx: &AppContext) -> bool; fn is_singleton(&self, cx: &AppContext) -> bool;
@ -409,7 +413,9 @@ impl<T: FollowableItem> FollowableItemHandle for ViewHandle<T> {
} }
pub trait ItemHandle: 'static + fmt::Debug { 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_path(&self, cx: &AppContext) -> Option<ProjectPath>;
fn project_entry_ids(&self, cx: &AppContext) -> SmallVec<[ProjectEntryId; 3]>; fn project_entry_ids(&self, cx: &AppContext) -> SmallVec<[ProjectEntryId; 3]>;
fn is_singleton(&self, cx: &AppContext) -> bool; fn is_singleton(&self, cx: &AppContext) -> bool;
@ -463,8 +469,17 @@ impl dyn ItemHandle {
} }
impl<T: Item> ItemHandle for ViewHandle<T> { impl<T: Item> ItemHandle for ViewHandle<T> {
fn tab_content(&self, style: &theme::Tab, cx: &AppContext) -> ElementBox { fn tab_description<'a>(&self, detail: usize, cx: &'a AppContext) -> Option<Cow<'a, str>> {
self.read(cx).tab_content(style, cx) 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> { 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) { if T::should_activate_item_on_event(event) {
pane.update(cx, |pane, cx| { pane.update(cx, |pane, cx| {
if let Some(ix) = pane.index_for_item(&item) { 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); pane.activate(cx);
} }
}); });
@ -1507,7 +1522,7 @@ impl Workspace {
}); });
if let Some((pane, ix)) = result { if let Some((pane, ix)) = result {
self.activate_pane(pane.clone(), cx); 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 true
} else { } else {
false false
@ -2686,11 +2701,62 @@ fn open_new(app_state: &Arc<AppState>, cx: &mut MutableAppContext) {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::cell::Cell;
use super::*; use super::*;
use gpui::{executor::Deterministic, ModelHandle, TestAppContext, ViewContext}; use gpui::{executor::Deterministic, ModelHandle, TestAppContext, ViewContext};
use project::{FakeFs, Project, ProjectEntryId}; use project::{FakeFs, Project, ProjectEntryId};
use serde_json::json; 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] #[gpui::test]
async fn test_tracking_active_path(cx: &mut TestAppContext) { async fn test_tracking_active_path(cx: &mut TestAppContext) {
cx.foreground().forbid_parking(); cx.foreground().forbid_parking();
@ -2880,7 +2946,7 @@ mod tests {
let close_items = workspace.update(cx, |workspace, cx| { let close_items = workspace.update(cx, |workspace, cx| {
pane.update(cx, |pane, 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()); assert_eq!(pane.active_item().unwrap().id(), item2.id());
}); });
@ -3211,6 +3277,8 @@ mod tests {
project_entry_ids: Vec<ProjectEntryId>, project_entry_ids: Vec<ProjectEntryId>,
project_path: Option<ProjectPath>, project_path: Option<ProjectPath>,
nav_history: Option<ItemNavHistory>, nav_history: Option<ItemNavHistory>,
tab_descriptions: Option<Vec<&'static str>>,
tab_detail: Cell<Option<usize>>,
} }
enum TestItemEvent { enum TestItemEvent {
@ -3230,6 +3298,8 @@ mod tests {
project_entry_ids: self.project_entry_ids.clone(), project_entry_ids: self.project_entry_ids.clone(),
project_path: self.project_path.clone(), project_path: self.project_path.clone(),
nav_history: None, nav_history: None,
tab_descriptions: None,
tab_detail: Default::default(),
} }
} }
} }
@ -3247,6 +3317,8 @@ mod tests {
project_path: None, project_path: None,
is_singleton: true, is_singleton: true,
nav_history: None, nav_history: None,
tab_descriptions: None,
tab_detail: Default::default(),
} }
} }
@ -3277,7 +3349,15 @@ mod tests {
} }
impl Item for TestItem { 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() Empty::new().boxed()
} }

View file

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

View file

@ -1,5 +1,6 @@
use gpui::executor::Background; use gpui::executor::Background;
pub use language::*; pub use language::*;
use lazy_static::lazy_static;
use rust_embed::RustEmbed; use rust_embed::RustEmbed;
use std::{borrow::Cow, str, sync::Arc}; use std::{borrow::Cow, str, sync::Arc};
use util::ResultExt; use util::ResultExt;
@ -17,6 +18,21 @@ mod typescript;
#[exclude = "*.rs"] #[exclude = "*.rs"]
struct LanguageDir; 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>) { pub async fn init(languages: Arc<LanguageRegistry>, executor: Arc<Background>) {
for (name, grammar, lsp_adapter) in [ for (name, grammar, lsp_adapter) in [
( (

View file

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

View file

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

View file

@ -1,7 +1,14 @@
import Theme from "../themes/common/theme"; import Theme from "../themes/common/theme";
import { border, modalShadow } from "./components"; import { border, modalShadow, player } from "./components";
export default function terminal(theme: Theme) { 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 = { let colors = {
black: theme.ramps.neutral(0).hex(), black: theme.ramps.neutral(0).hex(),
red: theme.ramps.red(0.5).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(), magenta: theme.ramps.magenta(0.5).hex(),
cyan: theme.ramps.cyan(0.5).hex(), cyan: theme.ramps.cyan(0.5).hex(),
white: theme.ramps.neutral(7).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(), brightRed: theme.ramps.red(0.25).hex(),
brightGreen: theme.ramps.green(0.25).hex(), brightGreen: theme.ramps.green(0.25).hex(),
brightYellow: theme.ramps.yellow(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(), brightMagenta: theme.ramps.magenta(0.25).hex(),
brightCyan: theme.ramps.cyan(0.25).hex(), brightCyan: theme.ramps.cyan(0.25).hex(),
brightWhite: theme.ramps.neutral(7).hex(), brightWhite: theme.ramps.neutral(7).hex(),
/**
* Default color for characters
*/
foreground: theme.ramps.neutral(7).hex(), foreground: theme.ramps.neutral(7).hex(),
/**
* Default color for the rectangle background of a cell
*/
background: theme.ramps.neutral(0).hex(), background: theme.ramps.neutral(0).hex(),
modalBackground: theme.ramps.neutral(1).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(), dimBlack: theme.ramps.neutral(7).hex(),
dimRed: theme.ramps.red(0.75).hex(), dimRed: theme.ramps.red(0.75).hex(),
dimGreen: theme.ramps.green(0.75).hex(), dimGreen: theme.ramps.green(0.75).hex(),

View file

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