Merge branch 'main' into keybindings-grind
This commit is contained in:
commit
f38206f819
38 changed files with 882 additions and 468 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -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
3
Cargo.lock
generated
|
@ -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",
|
||||||
|
|
1
Procfile
1
Procfile
|
@ -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
|
||||||
|
|
|
@ -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.
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
|
@ -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": {
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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" }
|
||||||
|
|
|
@ -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(),
|
completion_provider: Some(lsp::CompletionOptions {
|
||||||
path_suffixes: vec!["rs".to_string()],
|
trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
Some(tree_sitter_rust::language()),
|
cx,
|
||||||
);
|
|
||||||
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()
|
|
||||||
}),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
..Default::default()
|
|
||||||
}))
|
|
||||||
.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! {"
|
||||||
|
one|
|
||||||
|
two
|
||||||
|
three"});
|
||||||
|
cx.simulate_keystroke(".");
|
||||||
|
handle_completion_request(
|
||||||
|
&mut cx,
|
||||||
|
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.move_down(&MoveDown, cx);
|
||||||
let apply_additional_edits = editor
|
editor
|
||||||
.confirm_completion(&ConfirmCompletion::default(), cx)
|
.confirm_completion(&ConfirmCompletion::default(), cx)
|
||||||
.unwrap();
|
.unwrap()
|
||||||
assert_eq!(
|
|
||||||
editor.text(cx),
|
|
||||||
"
|
|
||||||
one.second_completion
|
|
||||||
two
|
|
||||||
three
|
|
||||||
"
|
|
||||||
.unindent()
|
|
||||||
);
|
|
||||||
apply_additional_edits
|
|
||||||
});
|
});
|
||||||
|
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
|
|
||||||
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),
|
|
||||||
])
|
|
||||||
});
|
|
||||||
|
|
||||||
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
|
"\nadditional edit",
|
||||||
"
|
)),
|
||||||
.unindent()
|
)
|
||||||
);
|
.await;
|
||||||
apply_additional_edits
|
apply_additional_edits.await.unwrap();
|
||||||
|
cx.assert_editor_state(indoc! {"
|
||||||
|
one.second_completion|
|
||||||
|
two
|
||||||
|
three
|
||||||
|
additional edit"});
|
||||||
|
|
||||||
|
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 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()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -342,17 +342,16 @@ 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: url.clone(),
|
||||||
target_uri: lsp::Url::from_file_path("/root/dir/file.rs").unwrap(),
|
target_range,
|
||||||
target_range,
|
target_selection_range: target_range,
|
||||||
target_selection_range: target_range,
|
},
|
||||||
},
|
])))
|
||||||
])))
|
});
|
||||||
});
|
|
||||||
cx.update_editor(|editor, cx| {
|
cx.update_editor(|editor, cx| {
|
||||||
update_go_to_definition_link(
|
update_go_to_definition_link(
|
||||||
editor,
|
editor,
|
||||||
|
@ -387,18 +386,17 @@ 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: url.clone(),
|
||||||
target_uri: lsp::Url::from_file_path("/root/dir/file.rs").unwrap(),
|
target_range,
|
||||||
target_range,
|
target_selection_range: target_range,
|
||||||
target_selection_range: target_range,
|
},
|
||||||
},
|
])))
|
||||||
])))
|
});
|
||||||
});
|
|
||||||
cx.update_editor(|editor, cx| {
|
cx.update_editor(|editor, cx| {
|
||||||
update_go_to_definition_link(
|
update_go_to_definition_link(
|
||||||
editor,
|
editor,
|
||||||
|
@ -495,17 +493,16 @@ 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: url,
|
||||||
target_uri: lsp::Url::from_file_path("/root/dir/file.rs").unwrap(),
|
target_range,
|
||||||
target_range,
|
target_selection_range: target_range,
|
||||||
target_selection_range: target_range,
|
},
|
||||||
},
|
])))
|
||||||
])))
|
});
|
||||||
});
|
|
||||||
cx.update_editor(|editor, cx| {
|
cx.update_editor(|editor, cx| {
|
||||||
cmd_changed(editor, &CmdChanged { cmd_down: true }, cx);
|
cmd_changed(editor, &CmdChanged { cmd_down: true }, cx);
|
||||||
});
|
});
|
||||||
|
@ -584,17 +581,16 @@ 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: url,
|
||||||
target_uri: lsp::Url::from_file_path("/root/dir/file.rs").unwrap(),
|
target_range,
|
||||||
target_range,
|
target_selection_range: target_range,
|
||||||
target_selection_range: target_range,
|
},
|
||||||
},
|
])))
|
||||||
])))
|
});
|
||||||
});
|
|
||||||
cx.update_workspace(|workspace, cx| {
|
cx.update_workspace(|workspace, cx| {
|
||||||
go_to_fetched_definition(workspace, &GoToFetchedDefinition { point: hover_point }, cx);
|
go_to_fetched_definition(workspace, &GoToFetchedDefinition { point: hover_point }, cx);
|
||||||
});
|
});
|
||||||
|
|
103
crates/editor/src/mouse_context_menu.rs
Normal file
103
crates/editor/src/mouse_context_menu.rs
Normal 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()));
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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> {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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"] }
|
||||||
|
|
|
@ -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.
|
||||||
|
|
||||||
|
|
|
@ -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");
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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?;
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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()
|
||||||
})),
|
})),
|
||||||
|
|
|
@ -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| {
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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 [
|
||||||
(
|
(
|
||||||
|
|
|
@ -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")?,
|
||||||
|
|
|
@ -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<_>>();
|
||||||
|
|
|
@ -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(),
|
||||||
|
|
|
@ -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 = {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue