Merge branch 'styles-in-typescript' of https://github.com/zed-industries/zed into styles-in-typescript
This commit is contained in:
Generated
+13
-1
@@ -727,6 +727,7 @@ dependencies = [
|
||||
"editor",
|
||||
"gpui",
|
||||
"language",
|
||||
"project",
|
||||
"search",
|
||||
"theme",
|
||||
"workspace",
|
||||
@@ -5428,6 +5429,16 @@ dependencies = [
|
||||
"tree-sitter",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tree-sitter-typescript"
|
||||
version = "0.20.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4e8ed0ecb931cdff13c6a13f45ccd615156e2779d9ffb0395864e05505e6e86d"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"tree-sitter",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ttf-parser"
|
||||
version = "0.9.0"
|
||||
@@ -5970,7 +5981,7 @@ checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9"
|
||||
|
||||
[[package]]
|
||||
name = "zed"
|
||||
version = "0.23.0"
|
||||
version = "0.24.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-compression",
|
||||
@@ -6039,6 +6050,7 @@ dependencies = [
|
||||
"tree-sitter-json",
|
||||
"tree-sitter-markdown",
|
||||
"tree-sitter-rust",
|
||||
"tree-sitter-typescript",
|
||||
"unindent",
|
||||
"url",
|
||||
"util",
|
||||
|
||||
@@ -12,6 +12,7 @@ collections = { path = "../collections" }
|
||||
editor = { path = "../editor" }
|
||||
gpui = { path = "../gpui" }
|
||||
language = { path = "../language" }
|
||||
project = { path = "../project" }
|
||||
search = { path = "../search" }
|
||||
theme = { path = "../theme" }
|
||||
workspace = { path = "../workspace" }
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
use editor::{Anchor, Editor};
|
||||
use gpui::{
|
||||
elements::*, AppContext, Entity, RenderContext, Subscription, View, ViewContext, ViewHandle,
|
||||
elements::*, AppContext, Entity, ModelHandle, RenderContext, Subscription, View, ViewContext,
|
||||
ViewHandle,
|
||||
};
|
||||
use language::{BufferSnapshot, OutlineItem};
|
||||
use language::{Buffer, OutlineItem};
|
||||
use project::Project;
|
||||
use search::ProjectSearchView;
|
||||
use std::borrow::Cow;
|
||||
use theme::SyntaxTheme;
|
||||
use workspace::{ItemHandle, Settings, ToolbarItemLocation, ToolbarItemView};
|
||||
|
||||
@@ -13,14 +14,16 @@ pub enum Event {
|
||||
}
|
||||
|
||||
pub struct Breadcrumbs {
|
||||
project: ModelHandle<Project>,
|
||||
editor: Option<ViewHandle<Editor>>,
|
||||
project_search: Option<ViewHandle<ProjectSearchView>>,
|
||||
subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
impl Breadcrumbs {
|
||||
pub fn new() -> Self {
|
||||
pub fn new(project: ModelHandle<Project>) -> Self {
|
||||
Self {
|
||||
project,
|
||||
editor: Default::default(),
|
||||
subscriptions: Default::default(),
|
||||
project_search: Default::default(),
|
||||
@@ -31,14 +34,14 @@ impl Breadcrumbs {
|
||||
&self,
|
||||
theme: &SyntaxTheme,
|
||||
cx: &AppContext,
|
||||
) -> Option<(BufferSnapshot, Vec<OutlineItem<Anchor>>)> {
|
||||
) -> Option<(ModelHandle<Buffer>, Vec<OutlineItem<Anchor>>)> {
|
||||
let editor = self.editor.as_ref()?.read(cx);
|
||||
let cursor = editor.newest_anchor_selection().head();
|
||||
let (buffer, symbols) = editor
|
||||
.buffer()
|
||||
.read(cx)
|
||||
let multibuffer = &editor.buffer().read(cx);
|
||||
let (buffer_id, symbols) = multibuffer
|
||||
.read(cx)
|
||||
.symbols_containing(cursor, Some(theme))?;
|
||||
let buffer = multibuffer.buffer(buffer_id)?;
|
||||
Some((buffer, symbols))
|
||||
}
|
||||
}
|
||||
@@ -60,15 +63,21 @@ impl View for Breadcrumbs {
|
||||
} else {
|
||||
return Empty::new().boxed();
|
||||
};
|
||||
|
||||
let filename = if let Some(path) = buffer.path() {
|
||||
path.to_string_lossy()
|
||||
let buffer = buffer.read(cx);
|
||||
let filename = if let Some(file) = buffer.file() {
|
||||
if file.path().file_name().is_none()
|
||||
|| self.project.read(cx).visible_worktrees(cx).count() > 1
|
||||
{
|
||||
file.full_path(cx).to_string_lossy().to_string()
|
||||
} else {
|
||||
file.path().to_string_lossy().to_string()
|
||||
}
|
||||
} else {
|
||||
Cow::Borrowed("untitled")
|
||||
"untitled".to_string()
|
||||
};
|
||||
|
||||
Flex::row()
|
||||
.with_child(Label::new(filename.to_string(), theme.breadcrumbs.text.clone()).boxed())
|
||||
.with_child(Label::new(filename, theme.breadcrumbs.text.clone()).boxed())
|
||||
.with_children(symbols.into_iter().flat_map(|symbol| {
|
||||
[
|
||||
Label::new(" 〉 ".to_string(), theme.breadcrumbs.text.clone()).boxed(),
|
||||
@@ -99,7 +108,10 @@ impl ToolbarItemView for Breadcrumbs {
|
||||
if let Some(editor) = item.act_as::<Editor>(cx) {
|
||||
self.subscriptions
|
||||
.push(cx.subscribe(&editor, |_, _, event, cx| match event {
|
||||
editor::Event::BufferEdited => cx.notify(),
|
||||
editor::Event::BufferEdited
|
||||
| editor::Event::TitleChanged
|
||||
| editor::Event::Saved
|
||||
| editor::Event::Reparsed => cx.notify(),
|
||||
editor::Event::SelectionsChanged { local } if *local => cx.notify(),
|
||||
_ => {}
|
||||
}));
|
||||
|
||||
@@ -478,6 +478,14 @@ impl workspace::Item for ProjectDiagnosticsEditor {
|
||||
self.editor.save(project, cx)
|
||||
}
|
||||
|
||||
fn reload(
|
||||
&mut self,
|
||||
project: ModelHandle<Project>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
self.editor.reload(project, cx)
|
||||
}
|
||||
|
||||
fn can_save_as(&self, _: &AppContext) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
+59
-44
@@ -1388,6 +1388,24 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn replace_selections_with(
|
||||
&mut self,
|
||||
cx: &mut ViewContext<Self>,
|
||||
find_replacement: impl Fn(&DisplaySnapshot) -> DisplayPoint,
|
||||
) {
|
||||
let display_map = self.snapshot(cx);
|
||||
let cursor = find_replacement(&display_map);
|
||||
let selection = Selection {
|
||||
id: post_inc(&mut self.next_selection_id),
|
||||
start: cursor,
|
||||
end: cursor,
|
||||
reversed: false,
|
||||
goal: SelectionGoal::None,
|
||||
}
|
||||
.map(|display_point| display_point.to_point(&display_map));
|
||||
self.update_selections(vec![selection], None, cx);
|
||||
}
|
||||
|
||||
pub fn move_selections(
|
||||
&mut self,
|
||||
cx: &mut ViewContext<Self>,
|
||||
@@ -1398,21 +1416,9 @@ impl Editor {
|
||||
.local_selections::<Point>(cx)
|
||||
.into_iter()
|
||||
.map(|selection| {
|
||||
let mut selection = Selection {
|
||||
id: selection.id,
|
||||
start: selection.start.to_display_point(&display_map),
|
||||
end: selection.end.to_display_point(&display_map),
|
||||
reversed: selection.reversed,
|
||||
goal: selection.goal,
|
||||
};
|
||||
let mut selection = selection.map(|point| point.to_display_point(&display_map));
|
||||
move_selection(&display_map, &mut selection);
|
||||
Selection {
|
||||
id: selection.id,
|
||||
start: selection.start.to_point(&display_map),
|
||||
end: selection.end.to_point(&display_map),
|
||||
reversed: selection.reversed,
|
||||
goal: selection.goal,
|
||||
}
|
||||
selection.map(|display_point| display_point.to_point(&display_map))
|
||||
})
|
||||
.collect();
|
||||
self.update_selections(selections, Some(Autoscroll::Fit), cx);
|
||||
@@ -2593,6 +2599,8 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut ranges_to_highlight = Vec::new();
|
||||
@@ -5858,6 +5866,7 @@ impl Editor {
|
||||
self.refresh_code_actions(cx);
|
||||
cx.emit(Event::BufferEdited);
|
||||
}
|
||||
language::Event::Reparsed => cx.emit(Event::Reparsed),
|
||||
language::Event::Dirtied => cx.emit(Event::Dirtied),
|
||||
language::Event::Saved => cx.emit(Event::Saved),
|
||||
language::Event::FileHandleChanged => cx.emit(Event::TitleChanged),
|
||||
@@ -5985,6 +5994,7 @@ pub enum Event {
|
||||
Activate,
|
||||
BufferEdited,
|
||||
Edited,
|
||||
Reparsed,
|
||||
Blurred,
|
||||
Dirtied,
|
||||
Saved,
|
||||
@@ -6451,13 +6461,12 @@ pub fn styled_runs_for_code_label<'a>(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use super::*;
|
||||
use gpui::{
|
||||
geometry::rect::RectF,
|
||||
platform::{WindowBounds, WindowOptions},
|
||||
};
|
||||
use language::{LanguageConfig, LanguageServerConfig};
|
||||
use language::{FakeLspAdapter, LanguageConfig};
|
||||
use lsp::FakeLanguageServer;
|
||||
use project::FakeFs;
|
||||
use smol::stream::StreamExt;
|
||||
@@ -8893,26 +8902,27 @@ mod tests {
|
||||
cx.foreground().forbid_parking();
|
||||
cx.update(populate_settings);
|
||||
|
||||
let (mut language_server_config, mut fake_servers) = LanguageServerConfig::fake();
|
||||
language_server_config.set_fake_capabilities(lsp::ServerCapabilities {
|
||||
document_formatting_provider: Some(lsp::OneOf::Left(true)),
|
||||
..Default::default()
|
||||
});
|
||||
let language = Arc::new(Language::new(
|
||||
let mut language = Language::new(
|
||||
LanguageConfig {
|
||||
name: "Rust".into(),
|
||||
path_suffixes: vec!["rs".to_string()],
|
||||
language_server: Some(language_server_config),
|
||||
..Default::default()
|
||||
},
|
||||
Some(tree_sitter_rust::language()),
|
||||
));
|
||||
);
|
||||
let mut fake_servers = language.set_fake_lsp_adapter(FakeLspAdapter {
|
||||
capabilities: lsp::ServerCapabilities {
|
||||
document_formatting_provider: Some(lsp::OneOf::Left(true)),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let fs = FakeFs::new(cx.background().clone());
|
||||
fs.insert_file("/file.rs", Default::default()).await;
|
||||
|
||||
let project = Project::test(fs, cx);
|
||||
project.update(cx, |project, _| project.languages().add(language));
|
||||
project.update(cx, |project, _| project.languages().add(Arc::new(language)));
|
||||
|
||||
let worktree_id = project
|
||||
.update(cx, |project, cx| {
|
||||
@@ -8926,7 +8936,9 @@ mod tests {
|
||||
.update(cx, |project, cx| project.open_buffer((worktree_id, ""), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
let mut fake_server = fake_servers.next().await.unwrap();
|
||||
|
||||
cx.foreground().start_waiting();
|
||||
let 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));
|
||||
@@ -8940,13 +8952,14 @@ mod tests {
|
||||
params.text_document.uri,
|
||||
lsp::Url::from_file_path("/file.rs").unwrap()
|
||||
);
|
||||
Some(vec![lsp::TextEdit::new(
|
||||
Ok(Some(vec![lsp::TextEdit::new(
|
||||
lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
|
||||
", ".to_string(),
|
||||
)])
|
||||
)]))
|
||||
})
|
||||
.next()
|
||||
.await;
|
||||
cx.foreground().start_waiting();
|
||||
save.await.unwrap();
|
||||
assert_eq!(
|
||||
editor.read_with(cx, |editor, cx| editor.text(cx)),
|
||||
@@ -8968,6 +8981,7 @@ mod tests {
|
||||
});
|
||||
let save = cx.update(|cx| editor.save(project.clone(), cx));
|
||||
cx.foreground().advance_clock(items::FORMAT_TIMEOUT);
|
||||
cx.foreground().start_waiting();
|
||||
save.await.unwrap();
|
||||
assert_eq!(
|
||||
editor.read_with(cx, |editor, cx| editor.text(cx)),
|
||||
@@ -8980,23 +8994,24 @@ mod tests {
|
||||
async fn test_completion(cx: &mut gpui::TestAppContext) {
|
||||
cx.update(populate_settings);
|
||||
|
||||
let (mut language_server_config, mut fake_servers) = LanguageServerConfig::fake();
|
||||
language_server_config.set_fake_capabilities(lsp::ServerCapabilities {
|
||||
completion_provider: Some(lsp::CompletionOptions {
|
||||
trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
});
|
||||
let language = Arc::new(Language::new(
|
||||
let mut language = Language::new(
|
||||
LanguageConfig {
|
||||
name: "Rust".into(),
|
||||
path_suffixes: vec!["rs".to_string()],
|
||||
language_server: Some(language_server_config),
|
||||
..Default::default()
|
||||
},
|
||||
Some(tree_sitter_rust::language()),
|
||||
));
|
||||
);
|
||||
let mut fake_servers = language.set_fake_lsp_adapter(FakeLspAdapter {
|
||||
capabilities: lsp::ServerCapabilities {
|
||||
completion_provider: Some(lsp::CompletionOptions {
|
||||
trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let text = "
|
||||
one
|
||||
@@ -9009,7 +9024,7 @@ mod tests {
|
||||
fs.insert_file("/file.rs", text).await;
|
||||
|
||||
let project = Project::test(fs, cx);
|
||||
project.update(cx, |project, _| project.languages().add(language));
|
||||
project.update(cx, |project, _| project.languages().add(Arc::new(language)));
|
||||
|
||||
let worktree_id = project
|
||||
.update(cx, |project, cx| {
|
||||
@@ -9168,7 +9183,7 @@ mod tests {
|
||||
params.text_document_position.position,
|
||||
lsp::Position::new(position.row, position.column)
|
||||
);
|
||||
Some(lsp::CompletionResponse::Array(
|
||||
Ok(Some(lsp::CompletionResponse::Array(
|
||||
completions
|
||||
.iter()
|
||||
.map(|(range, new_text)| lsp::CompletionItem {
|
||||
@@ -9183,7 +9198,7 @@ mod tests {
|
||||
..Default::default()
|
||||
})
|
||||
.collect(),
|
||||
))
|
||||
)))
|
||||
}
|
||||
})
|
||||
.next()
|
||||
@@ -9197,7 +9212,7 @@ mod tests {
|
||||
fake.handle_request::<lsp::request::ResolveCompletionItem, _, _>(move |_, _| {
|
||||
let edit = edit.clone();
|
||||
async move {
|
||||
lsp::CompletionItem {
|
||||
Ok(lsp::CompletionItem {
|
||||
additional_text_edits: edit.map(|(range, new_text)| {
|
||||
vec![lsp::TextEdit::new(
|
||||
lsp::Range::new(
|
||||
@@ -9208,7 +9223,7 @@ mod tests {
|
||||
)]
|
||||
}),
|
||||
..Default::default()
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
.next()
|
||||
|
||||
@@ -371,6 +371,31 @@ impl Item for Editor {
|
||||
})
|
||||
}
|
||||
|
||||
fn reload(
|
||||
&mut self,
|
||||
project: ModelHandle<Project>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
let buffer = self.buffer().clone();
|
||||
let buffers = self.buffer.read(cx).all_buffers();
|
||||
let reload_buffers =
|
||||
project.update(cx, |project, cx| project.reload_buffers(buffers, true, cx));
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let transaction = reload_buffers.log_err().await;
|
||||
this.update(&mut cx, |editor, cx| {
|
||||
editor.request_autoscroll(Autoscroll::Fit, cx)
|
||||
});
|
||||
buffer.update(&mut cx, |buffer, _| {
|
||||
if let Some(transaction) = transaction {
|
||||
if !buffer.is_singleton() {
|
||||
buffer.push_transaction(&transaction.0);
|
||||
}
|
||||
}
|
||||
});
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn should_activate_item_on_event(event: &Event) -> bool {
|
||||
matches!(event, Event::Activate)
|
||||
}
|
||||
|
||||
@@ -1001,6 +1001,13 @@ impl MultiBuffer {
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn buffer(&self, buffer_id: usize) -> Option<ModelHandle<Buffer>> {
|
||||
self.buffers
|
||||
.borrow()
|
||||
.get(&buffer_id)
|
||||
.map(|state| state.buffer.clone())
|
||||
}
|
||||
|
||||
pub fn save(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
|
||||
let mut save_tasks = Vec::new();
|
||||
for BufferState { buffer, .. } in self.buffers.borrow().values() {
|
||||
@@ -2268,12 +2275,12 @@ impl MultiBufferSnapshot {
|
||||
&self,
|
||||
offset: T,
|
||||
theme: Option<&SyntaxTheme>,
|
||||
) -> Option<(BufferSnapshot, Vec<OutlineItem<Anchor>>)> {
|
||||
) -> Option<(usize, Vec<OutlineItem<Anchor>>)> {
|
||||
let anchor = self.anchor_before(offset);
|
||||
let excerpt_id = anchor.excerpt_id();
|
||||
let excerpt = self.excerpt(excerpt_id)?;
|
||||
Some((
|
||||
excerpt.buffer.clone(),
|
||||
excerpt.buffer_id,
|
||||
excerpt
|
||||
.buffer
|
||||
.symbols_containing(anchor.text_anchor, theme)
|
||||
|
||||
@@ -30,6 +30,7 @@ impl Event {
|
||||
let alt = modifiers.contains(NSEventModifierFlags::NSAlternateKeyMask);
|
||||
let shift = modifiers.contains(NSEventModifierFlags::NSShiftKeyMask);
|
||||
let cmd = modifiers.contains(NSEventModifierFlags::NSCommandKeyMask);
|
||||
let function = modifiers.contains(NSEventModifierFlags::NSFunctionKeyMask);
|
||||
|
||||
let unmodified_chars = CStr::from_ptr(
|
||||
native_event.charactersIgnoringModifiers().UTF8String() as *mut c_char,
|
||||
@@ -80,7 +81,7 @@ impl Event {
|
||||
NSF12FunctionKey => "f12",
|
||||
|
||||
_ => {
|
||||
if !cmd && !ctrl {
|
||||
if !cmd && !ctrl && !function {
|
||||
input = Some(
|
||||
CStr::from_ptr(
|
||||
native_event.characters().UTF8String() as *mut c_char
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
pub use crate::{
|
||||
diagnostic_set::DiagnosticSet,
|
||||
highlight_map::{HighlightId, HighlightMap},
|
||||
proto, BracketPair, Grammar, Language, LanguageConfig, LanguageRegistry, LanguageServerConfig,
|
||||
PLAIN_TEXT,
|
||||
proto, BracketPair, Grammar, Language, LanguageConfig, LanguageRegistry, PLAIN_TEXT,
|
||||
};
|
||||
use crate::{
|
||||
diagnostic_set::{DiagnosticEntry, DiagnosticGroup},
|
||||
@@ -276,6 +275,16 @@ pub enum CharKind {
|
||||
Word,
|
||||
}
|
||||
|
||||
impl CharKind {
|
||||
pub fn coerce_punctuation(self, treat_punctuation_as_word: bool) -> Self {
|
||||
if treat_punctuation_as_word && self == CharKind::Punctuation {
|
||||
CharKind::Word
|
||||
} else {
|
||||
self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Buffer {
|
||||
pub fn new<T: Into<Arc<str>>>(
|
||||
replica_id: ReplicaId,
|
||||
@@ -498,6 +507,30 @@ impl Buffer {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn reload(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<Option<Transaction>>> {
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
if let Some((new_mtime, new_text)) = this.read_with(&cx, |this, cx| {
|
||||
let file = this.file.as_ref()?.as_local()?;
|
||||
Some((file.mtime(), file.load(cx)))
|
||||
}) {
|
||||
let new_text = new_text.await?;
|
||||
let diff = this
|
||||
.read_with(&cx, |this, cx| this.diff(new_text.into(), cx))
|
||||
.await;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
if let Some(transaction) = this.apply_diff(diff, cx).cloned() {
|
||||
this.did_reload(this.version(), new_mtime, cx);
|
||||
Ok(Some(transaction))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn did_reload(
|
||||
&mut self,
|
||||
version: clock::Global,
|
||||
@@ -543,29 +576,8 @@ impl Buffer {
|
||||
file_changed = true;
|
||||
|
||||
if !self.is_dirty() {
|
||||
task = cx.spawn(|this, mut cx| {
|
||||
async move {
|
||||
let new_text = this.read_with(&cx, |this, cx| {
|
||||
this.file
|
||||
.as_ref()
|
||||
.and_then(|file| file.as_local().map(|f| f.load(cx)))
|
||||
});
|
||||
if let Some(new_text) = new_text {
|
||||
let new_text = new_text.await?;
|
||||
let diff = this
|
||||
.read_with(&cx, |this, cx| this.diff(new_text.into(), cx))
|
||||
.await;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
if this.apply_diff(diff, cx) {
|
||||
this.did_reload(this.version(), new_mtime, cx);
|
||||
}
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
.log_err()
|
||||
.map(drop)
|
||||
});
|
||||
let reload = self.reload(cx).log_err().map(drop);
|
||||
task = cx.foreground().spawn(reload);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -902,8 +914,13 @@ impl Buffer {
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn apply_diff(&mut self, diff: Diff, cx: &mut ModelContext<Self>) -> bool {
|
||||
pub(crate) fn apply_diff(
|
||||
&mut self,
|
||||
diff: Diff,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Option<&Transaction> {
|
||||
if self.version == diff.base_version {
|
||||
self.finalize_last_transaction();
|
||||
self.start_transaction();
|
||||
let mut offset = diff.start_offset;
|
||||
for (tag, len) in diff.changes {
|
||||
@@ -924,10 +941,13 @@ impl Buffer {
|
||||
}
|
||||
}
|
||||
}
|
||||
self.end_transaction(cx);
|
||||
true
|
||||
if self.end_transaction(cx).is_some() {
|
||||
self.finalize_last_transaction()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
false
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -34,6 +34,23 @@ pub struct Summary {
|
||||
count: usize,
|
||||
}
|
||||
|
||||
impl<T> DiagnosticEntry<T> {
|
||||
// Used to provide diagnostic context to lsp codeAction request
|
||||
pub fn to_lsp_diagnostic_stub(&self) -> lsp::Diagnostic {
|
||||
let code = self
|
||||
.diagnostic
|
||||
.code
|
||||
.clone()
|
||||
.map(lsp::NumberOrString::String);
|
||||
|
||||
lsp::Diagnostic {
|
||||
code,
|
||||
severity: Some(self.diagnostic.severity),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DiagnosticSet {
|
||||
pub fn from_sorted_entries<I>(iter: I, buffer: &text::BufferSnapshot) -> Self
|
||||
where
|
||||
|
||||
+137
-103
@@ -7,8 +7,8 @@ pub mod proto;
|
||||
mod tests;
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use client::http::{self, HttpClient};
|
||||
use collections::HashSet;
|
||||
use client::http::HttpClient;
|
||||
use collections::HashMap;
|
||||
use futures::{
|
||||
future::{BoxFuture, Shared},
|
||||
FutureExt, TryFutureExt,
|
||||
@@ -20,6 +20,7 @@ use parking_lot::{Mutex, RwLock};
|
||||
use serde::Deserialize;
|
||||
use serde_json::Value;
|
||||
use std::{
|
||||
any::Any,
|
||||
cell::RefCell,
|
||||
ops::Range,
|
||||
path::{Path, PathBuf},
|
||||
@@ -51,7 +52,6 @@ lazy_static! {
|
||||
brackets: Default::default(),
|
||||
autoclose_before: Default::default(),
|
||||
line_comment: None,
|
||||
language_server: None,
|
||||
},
|
||||
None,
|
||||
));
|
||||
@@ -61,20 +61,18 @@ pub trait ToLspPosition {
|
||||
fn to_lsp_position(self) -> lsp::Position;
|
||||
}
|
||||
|
||||
pub struct LspBinaryVersion {
|
||||
pub name: String,
|
||||
pub url: Option<http::Url>,
|
||||
}
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct LanguageServerName(pub Arc<str>);
|
||||
|
||||
pub trait LspAdapter: 'static + Send + Sync {
|
||||
fn name(&self) -> &'static str;
|
||||
fn name(&self) -> LanguageServerName;
|
||||
fn fetch_latest_server_version(
|
||||
&self,
|
||||
http: Arc<dyn HttpClient>,
|
||||
) -> BoxFuture<'static, Result<LspBinaryVersion>>;
|
||||
) -> BoxFuture<'static, Result<Box<dyn 'static + Send + Any>>>;
|
||||
fn fetch_server_binary(
|
||||
&self,
|
||||
version: LspBinaryVersion,
|
||||
version: Box<dyn 'static + Send + Any>,
|
||||
http: Arc<dyn HttpClient>,
|
||||
container_dir: PathBuf,
|
||||
) -> BoxFuture<'static, Result<PathBuf>>;
|
||||
@@ -96,6 +94,14 @@ pub trait LspAdapter: 'static + Send + Sync {
|
||||
fn initialization_options(&self) -> Option<Value> {
|
||||
None
|
||||
}
|
||||
|
||||
fn disk_based_diagnostic_sources(&self) -> &'static [&'static str] {
|
||||
Default::default()
|
||||
}
|
||||
|
||||
fn disk_based_diagnostics_progress_token(&self) -> Option<&'static str> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
@@ -113,7 +119,6 @@ pub struct LanguageConfig {
|
||||
#[serde(default)]
|
||||
pub autoclose_before: String,
|
||||
pub line_comment: Option<String>,
|
||||
pub language_server: Option<LanguageServerConfig>,
|
||||
}
|
||||
|
||||
impl Default for LanguageConfig {
|
||||
@@ -124,25 +129,17 @@ impl Default for LanguageConfig {
|
||||
brackets: Default::default(),
|
||||
autoclose_before: Default::default(),
|
||||
line_comment: Default::default(),
|
||||
language_server: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Deserialize)]
|
||||
pub struct LanguageServerConfig {
|
||||
pub disk_based_diagnostic_sources: HashSet<String>,
|
||||
pub disk_based_diagnostics_progress_token: Option<String>,
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
#[serde(skip)]
|
||||
fake_config: Option<FakeLanguageServerConfig>,
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
struct FakeLanguageServerConfig {
|
||||
servers_tx: mpsc::UnboundedSender<lsp::FakeLanguageServer>,
|
||||
capabilities: lsp::ServerCapabilities,
|
||||
initializer: Option<Box<dyn 'static + Send + Sync + Fn(&mut lsp::FakeLanguageServer)>>,
|
||||
pub struct FakeLspAdapter {
|
||||
pub name: &'static str,
|
||||
pub capabilities: lsp::ServerCapabilities,
|
||||
pub initializer: Option<Box<dyn 'static + Send + Sync + Fn(&mut lsp::FakeLanguageServer)>>,
|
||||
pub disk_based_diagnostics_progress_token: Option<&'static str>,
|
||||
pub disk_based_diagnostics_sources: &'static [&'static str],
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
@@ -157,7 +154,12 @@ pub struct Language {
|
||||
pub(crate) config: LanguageConfig,
|
||||
pub(crate) grammar: Option<Arc<Grammar>>,
|
||||
pub(crate) adapter: Option<Arc<dyn LspAdapter>>,
|
||||
lsp_binary_path: Mutex<Option<Shared<BoxFuture<'static, Result<PathBuf, Arc<anyhow::Error>>>>>>,
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
fake_adapter: Option<(
|
||||
mpsc::UnboundedSender<lsp::FakeLanguageServer>,
|
||||
Arc<FakeLspAdapter>,
|
||||
)>,
|
||||
}
|
||||
|
||||
pub struct Grammar {
|
||||
@@ -184,6 +186,12 @@ pub struct LanguageRegistry {
|
||||
lsp_binary_statuses_tx: async_broadcast::Sender<(Arc<Language>, LanguageServerBinaryStatus)>,
|
||||
lsp_binary_statuses_rx: async_broadcast::Receiver<(Arc<Language>, LanguageServerBinaryStatus)>,
|
||||
login_shell_env_loaded: Shared<Task<()>>,
|
||||
lsp_binary_paths: Mutex<
|
||||
HashMap<
|
||||
LanguageServerName,
|
||||
Shared<BoxFuture<'static, Result<PathBuf, Arc<anyhow::Error>>>>,
|
||||
>,
|
||||
>,
|
||||
}
|
||||
|
||||
impl LanguageRegistry {
|
||||
@@ -195,6 +203,7 @@ impl LanguageRegistry {
|
||||
lsp_binary_statuses_tx,
|
||||
lsp_binary_statuses_rx,
|
||||
login_shell_env_loaded: login_shell_env_loaded.shared(),
|
||||
lsp_binary_paths: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -244,7 +253,7 @@ impl LanguageRegistry {
|
||||
}
|
||||
|
||||
pub fn start_language_server(
|
||||
&self,
|
||||
self: &Arc<Self>,
|
||||
server_id: usize,
|
||||
language: Arc<Language>,
|
||||
root_path: Arc<Path>,
|
||||
@@ -252,34 +261,20 @@ impl LanguageRegistry {
|
||||
cx: &mut MutableAppContext,
|
||||
) -> Option<Task<Result<lsp::LanguageServer>>> {
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
if language
|
||||
.config
|
||||
.language_server
|
||||
.as_ref()
|
||||
.and_then(|config| config.fake_config.as_ref())
|
||||
.is_some()
|
||||
{
|
||||
if language.fake_adapter.is_some() {
|
||||
let language = language.clone();
|
||||
return Some(cx.spawn(|mut cx| async move {
|
||||
let fake_config = language
|
||||
.config
|
||||
.language_server
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.fake_config
|
||||
.as_ref()
|
||||
.unwrap();
|
||||
let (server, mut fake_server) = cx.update(|cx| {
|
||||
lsp::LanguageServer::fake_with_capabilities(
|
||||
fake_config.capabilities.clone(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
if let Some(initializer) = &fake_config.initializer {
|
||||
return Some(cx.spawn(|cx| async move {
|
||||
let (servers_tx, fake_adapter) = language.fake_adapter.as_ref().unwrap();
|
||||
let (server, mut fake_server) = lsp::LanguageServer::fake_with_capabilities(
|
||||
fake_adapter.capabilities.clone(),
|
||||
cx.clone(),
|
||||
);
|
||||
|
||||
if let Some(initializer) = &fake_adapter.initializer {
|
||||
initializer(&mut fake_server);
|
||||
}
|
||||
|
||||
let servers_tx = fake_config.servers_tx.clone();
|
||||
let servers_tx = servers_tx.clone();
|
||||
cx.background()
|
||||
.spawn(async move {
|
||||
fake_server
|
||||
@@ -298,16 +293,17 @@ impl LanguageRegistry {
|
||||
.ok_or_else(|| anyhow!("language server download directory has not been assigned"))
|
||||
.log_err()?;
|
||||
|
||||
let this = self.clone();
|
||||
let adapter = language.adapter.clone()?;
|
||||
let background = cx.background().clone();
|
||||
let lsp_binary_statuses = self.lsp_binary_statuses_tx.clone();
|
||||
let login_shell_env_loaded = self.login_shell_env_loaded.clone();
|
||||
Some(cx.background().spawn(async move {
|
||||
Some(cx.spawn(|cx| async move {
|
||||
login_shell_env_loaded.await;
|
||||
let server_binary_path = language
|
||||
.lsp_binary_path
|
||||
let server_binary_path = this
|
||||
.lsp_binary_paths
|
||||
.lock()
|
||||
.get_or_insert_with(|| {
|
||||
.entry(adapter.name())
|
||||
.or_insert_with(|| {
|
||||
get_server_binary_path(
|
||||
adapter.clone(),
|
||||
language.clone(),
|
||||
@@ -329,8 +325,7 @@ impl LanguageRegistry {
|
||||
&server_binary_path,
|
||||
server_args,
|
||||
&root_path,
|
||||
adapter.initialization_options(),
|
||||
background,
|
||||
cx,
|
||||
)?;
|
||||
Ok(server)
|
||||
}))
|
||||
@@ -350,7 +345,7 @@ async fn get_server_binary_path(
|
||||
download_dir: Arc<Path>,
|
||||
statuses: async_broadcast::Sender<(Arc<Language>, LanguageServerBinaryStatus)>,
|
||||
) -> Result<PathBuf> {
|
||||
let container_dir = download_dir.join(adapter.name());
|
||||
let container_dir = download_dir.join(adapter.name().0.as_ref());
|
||||
if !container_dir.exists() {
|
||||
smol::fs::create_dir_all(&container_dir)
|
||||
.await
|
||||
@@ -423,10 +418,16 @@ impl Language {
|
||||
})
|
||||
}),
|
||||
adapter: None,
|
||||
lsp_binary_path: Default::default(),
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
fake_adapter: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn lsp_adapter(&self) -> Option<Arc<dyn LspAdapter>> {
|
||||
self.adapter.clone()
|
||||
}
|
||||
|
||||
pub fn with_highlights_query(mut self, source: &str) -> Result<Self> {
|
||||
let grammar = self
|
||||
.grammar
|
||||
@@ -467,11 +468,23 @@ impl Language {
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
pub fn with_lsp_adapter(mut self, lsp_adapter: impl LspAdapter) -> Self {
|
||||
self.adapter = Some(Arc::new(lsp_adapter));
|
||||
pub fn with_lsp_adapter(mut self, lsp_adapter: Arc<dyn LspAdapter>) -> Self {
|
||||
self.adapter = Some(lsp_adapter);
|
||||
self
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn set_fake_lsp_adapter(
|
||||
&mut self,
|
||||
fake_lsp_adapter: FakeLspAdapter,
|
||||
) -> mpsc::UnboundedReceiver<lsp::FakeLanguageServer> {
|
||||
let (servers_tx, servers_rx) = mpsc::unbounded();
|
||||
let adapter = Arc::new(fake_lsp_adapter);
|
||||
self.fake_adapter = Some((servers_tx, adapter.clone()));
|
||||
self.adapter = Some(adapter);
|
||||
servers_rx
|
||||
}
|
||||
|
||||
pub fn name(&self) -> Arc<str> {
|
||||
self.config.name.clone()
|
||||
}
|
||||
@@ -480,18 +493,16 @@ impl Language {
|
||||
self.config.line_comment.as_deref()
|
||||
}
|
||||
|
||||
pub fn disk_based_diagnostic_sources(&self) -> Option<&HashSet<String>> {
|
||||
self.config
|
||||
.language_server
|
||||
.as_ref()
|
||||
.map(|config| &config.disk_based_diagnostic_sources)
|
||||
pub fn disk_based_diagnostic_sources(&self) -> &'static [&'static str] {
|
||||
self.adapter.as_ref().map_or(&[] as &[_], |adapter| {
|
||||
adapter.disk_based_diagnostic_sources()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn disk_based_diagnostics_progress_token(&self) -> Option<&String> {
|
||||
self.config
|
||||
.language_server
|
||||
pub fn disk_based_diagnostics_progress_token(&self) -> Option<&'static str> {
|
||||
self.adapter
|
||||
.as_ref()
|
||||
.and_then(|config| config.disk_based_diagnostics_progress_token.as_ref())
|
||||
.and_then(|adapter| adapter.disk_based_diagnostics_progress_token())
|
||||
}
|
||||
|
||||
pub fn process_diagnostics(&self, diagnostics: &mut lsp::PublishDiagnosticsParams) {
|
||||
@@ -598,47 +609,70 @@ impl CodeLabel {
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
impl LanguageServerConfig {
|
||||
pub fn fake() -> (Self, mpsc::UnboundedReceiver<lsp::FakeLanguageServer>) {
|
||||
let (servers_tx, servers_rx) = mpsc::unbounded();
|
||||
(
|
||||
Self {
|
||||
fake_config: Some(FakeLanguageServerConfig {
|
||||
servers_tx,
|
||||
capabilities: lsp::LanguageServer::full_capabilities(),
|
||||
initializer: None,
|
||||
}),
|
||||
disk_based_diagnostics_progress_token: Some("fakeServer/check".to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
servers_rx,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn set_fake_capabilities(&mut self, capabilities: lsp::ServerCapabilities) {
|
||||
self.fake_config.as_mut().unwrap().capabilities = capabilities;
|
||||
}
|
||||
|
||||
pub fn set_fake_initializer(
|
||||
&mut self,
|
||||
initializer: impl 'static + Send + Sync + Fn(&mut lsp::FakeLanguageServer),
|
||||
) {
|
||||
self.fake_config.as_mut().unwrap().initializer = Some(Box::new(initializer));
|
||||
impl Default for FakeLspAdapter {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
name: "the-fake-language-server",
|
||||
capabilities: lsp::LanguageServer::full_capabilities(),
|
||||
initializer: None,
|
||||
disk_based_diagnostics_progress_token: None,
|
||||
disk_based_diagnostics_sources: &[],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToLspPosition for PointUtf16 {
|
||||
fn to_lsp_position(self) -> lsp::Position {
|
||||
lsp::Position::new(self.row, self.column)
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
impl LspAdapter for FakeLspAdapter {
|
||||
fn name(&self) -> LanguageServerName {
|
||||
LanguageServerName(self.name.into())
|
||||
}
|
||||
|
||||
fn fetch_latest_server_version(
|
||||
&self,
|
||||
_: Arc<dyn HttpClient>,
|
||||
) -> BoxFuture<'static, Result<Box<dyn 'static + Send + Any>>> {
|
||||
unreachable!();
|
||||
}
|
||||
|
||||
fn fetch_server_binary(
|
||||
&self,
|
||||
_: Box<dyn 'static + Send + Any>,
|
||||
_: Arc<dyn HttpClient>,
|
||||
_: PathBuf,
|
||||
) -> BoxFuture<'static, Result<PathBuf>> {
|
||||
unreachable!();
|
||||
}
|
||||
|
||||
fn cached_server_binary(&self, _: PathBuf) -> BoxFuture<'static, Option<PathBuf>> {
|
||||
unreachable!();
|
||||
}
|
||||
|
||||
fn process_diagnostics(&self, _: &mut lsp::PublishDiagnosticsParams) {}
|
||||
|
||||
fn disk_based_diagnostic_sources(&self) -> &'static [&'static str] {
|
||||
self.disk_based_diagnostics_sources
|
||||
}
|
||||
|
||||
fn disk_based_diagnostics_progress_token(&self) -> Option<&'static str> {
|
||||
self.disk_based_diagnostics_progress_token
|
||||
}
|
||||
}
|
||||
|
||||
pub fn point_to_lsp(point: PointUtf16) -> lsp::Position {
|
||||
lsp::Position::new(point.row, point.column)
|
||||
}
|
||||
|
||||
pub fn point_from_lsp(point: lsp::Position) -> PointUtf16 {
|
||||
PointUtf16::new(point.line, point.character)
|
||||
}
|
||||
|
||||
pub fn range_to_lsp(range: Range<PointUtf16>) -> lsp::Range {
|
||||
lsp::Range {
|
||||
start: point_to_lsp(range.start),
|
||||
end: point_to_lsp(range.end),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn range_from_lsp(range: lsp::Range) -> Range<PointUtf16> {
|
||||
let start = PointUtf16::new(range.start.line, range.start.character);
|
||||
let end = PointUtf16::new(range.end.line, range.end.character);
|
||||
start..end
|
||||
point_from_lsp(range.start)..point_from_lsp(range.end)
|
||||
}
|
||||
|
||||
@@ -136,12 +136,16 @@ async fn test_apply_diff(cx: &mut gpui::TestAppContext) {
|
||||
|
||||
let text = "a\nccc\ndddd\nffffff\n";
|
||||
let diff = buffer.read_with(cx, |b, cx| b.diff(text.into(), cx)).await;
|
||||
buffer.update(cx, |b, cx| b.apply_diff(diff, cx));
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.apply_diff(diff, cx).unwrap();
|
||||
});
|
||||
cx.read(|cx| assert_eq!(buffer.read(cx).text(), text));
|
||||
|
||||
let text = "a\n1\n\nccc\ndd2dd\nffffff\n";
|
||||
let diff = buffer.read_with(cx, |b, cx| b.diff(text.into(), cx)).await;
|
||||
buffer.update(cx, |b, cx| b.apply_diff(diff, cx));
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.apply_diff(diff, cx).unwrap();
|
||||
});
|
||||
cx.read(|cx| assert_eq!(buffer.read(cx).text(), text));
|
||||
}
|
||||
|
||||
@@ -927,7 +931,6 @@ fn rust_lang() -> Language {
|
||||
LanguageConfig {
|
||||
name: "Rust".into(),
|
||||
path_suffixes: vec!["rs".to_string()],
|
||||
language_server: None,
|
||||
..Default::default()
|
||||
},
|
||||
Some(tree_sitter_rust::language()),
|
||||
|
||||
+201
-304
@@ -1,15 +1,17 @@
|
||||
pub use lsp_types::*;
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use collections::HashMap;
|
||||
use futures::{channel::oneshot, io::BufWriter, AsyncRead, AsyncWrite};
|
||||
use gpui::{executor, Task};
|
||||
use parking_lot::{Mutex, RwLock};
|
||||
use gpui::{executor, AsyncAppContext, Task};
|
||||
use parking_lot::Mutex;
|
||||
use postage::{barrier, prelude::Stream};
|
||||
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||
use serde_json::{json, value::RawValue, Value};
|
||||
use smol::{
|
||||
channel,
|
||||
io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader},
|
||||
process::Command,
|
||||
process,
|
||||
};
|
||||
use std::{
|
||||
future::Future,
|
||||
@@ -22,15 +24,12 @@ use std::{
|
||||
},
|
||||
};
|
||||
use std::{path::Path, process::Stdio};
|
||||
use util::TryFutureExt;
|
||||
|
||||
pub use lsp_types::*;
|
||||
use util::{ResultExt, TryFutureExt};
|
||||
|
||||
const JSON_RPC_VERSION: &'static str = "2.0";
|
||||
const CONTENT_LEN_HEADER: &'static str = "Content-Length: ";
|
||||
|
||||
type NotificationHandler =
|
||||
Box<dyn Send + Sync + FnMut(Option<usize>, &str, &mut channel::Sender<Vec<u8>>) -> Result<()>>;
|
||||
type NotificationHandler = Box<dyn Send + FnMut(Option<usize>, &str, AsyncAppContext)>;
|
||||
type ResponseHandler = Box<dyn Send + FnOnce(Result<&str, Error>)>;
|
||||
|
||||
pub struct LanguageServer {
|
||||
@@ -39,18 +38,17 @@ pub struct LanguageServer {
|
||||
outbound_tx: channel::Sender<Vec<u8>>,
|
||||
name: String,
|
||||
capabilities: ServerCapabilities,
|
||||
notification_handlers: Arc<RwLock<HashMap<&'static str, NotificationHandler>>>,
|
||||
notification_handlers: Arc<Mutex<HashMap<&'static str, NotificationHandler>>>,
|
||||
response_handlers: Arc<Mutex<HashMap<usize, ResponseHandler>>>,
|
||||
executor: Arc<executor::Background>,
|
||||
io_tasks: Mutex<Option<(Task<Option<()>>, Task<Option<()>>)>>,
|
||||
output_done_rx: Mutex<Option<barrier::Receiver>>,
|
||||
root_path: PathBuf,
|
||||
options: Option<Value>,
|
||||
}
|
||||
|
||||
pub struct Subscription {
|
||||
method: &'static str,
|
||||
notification_handlers: Arc<RwLock<HashMap<&'static str, NotificationHandler>>>,
|
||||
notification_handlers: Arc<Mutex<HashMap<&'static str, NotificationHandler>>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
@@ -61,18 +59,6 @@ struct Request<'a, T> {
|
||||
params: T,
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
#[derive(Deserialize)]
|
||||
struct AnyRequest<'a> {
|
||||
id: usize,
|
||||
#[serde(borrow)]
|
||||
jsonrpc: &'a str,
|
||||
#[serde(borrow)]
|
||||
method: &'a str,
|
||||
#[serde(borrow)]
|
||||
params: &'a RawValue,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct AnyResponse<'a> {
|
||||
id: usize,
|
||||
@@ -85,7 +71,8 @@ struct AnyResponse<'a> {
|
||||
#[derive(Serialize)]
|
||||
struct Response<T> {
|
||||
id: usize,
|
||||
result: T,
|
||||
result: Option<T>,
|
||||
error: Option<Error>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
@@ -118,15 +105,14 @@ impl LanguageServer {
|
||||
binary_path: &Path,
|
||||
args: &[&str],
|
||||
root_path: &Path,
|
||||
options: Option<Value>,
|
||||
background: Arc<executor::Background>,
|
||||
cx: AsyncAppContext,
|
||||
) -> Result<Self> {
|
||||
let working_dir = if root_path.is_dir() {
|
||||
root_path
|
||||
} else {
|
||||
root_path.parent().unwrap_or(Path::new("/"))
|
||||
};
|
||||
let mut server = Command::new(binary_path)
|
||||
let mut server = process::Command::new(binary_path)
|
||||
.current_dir(working_dir)
|
||||
.args(args)
|
||||
.stdin(Stdio::piped())
|
||||
@@ -136,99 +122,97 @@ impl LanguageServer {
|
||||
let stdin = server.stdin.take().unwrap();
|
||||
let stdout = server.stdout.take().unwrap();
|
||||
let mut server =
|
||||
Self::new_internal(server_id, stdin, stdout, root_path, options, background);
|
||||
Self::new_internal(server_id, stdin, stdout, root_path, cx, |notification| {
|
||||
log::info!(
|
||||
"unhandled notification {}:\n{}",
|
||||
notification.method,
|
||||
serde_json::to_string_pretty(
|
||||
&Value::from_str(notification.params.get()).unwrap()
|
||||
)
|
||||
.unwrap()
|
||||
);
|
||||
});
|
||||
if let Some(name) = binary_path.file_name() {
|
||||
server.name = name.to_string_lossy().to_string();
|
||||
}
|
||||
Ok(server)
|
||||
}
|
||||
|
||||
fn new_internal<Stdin, Stdout>(
|
||||
fn new_internal<Stdin, Stdout, F>(
|
||||
server_id: usize,
|
||||
stdin: Stdin,
|
||||
stdout: Stdout,
|
||||
root_path: &Path,
|
||||
options: Option<Value>,
|
||||
executor: Arc<executor::Background>,
|
||||
cx: AsyncAppContext,
|
||||
mut on_unhandled_notification: F,
|
||||
) -> Self
|
||||
where
|
||||
Stdin: AsyncWrite + Unpin + Send + 'static,
|
||||
Stdout: AsyncRead + Unpin + Send + 'static,
|
||||
F: FnMut(AnyNotification) + 'static + Send,
|
||||
{
|
||||
let mut stdin = BufWriter::new(stdin);
|
||||
let mut stdout = BufReader::new(stdout);
|
||||
let (outbound_tx, outbound_rx) = channel::unbounded::<Vec<u8>>();
|
||||
let notification_handlers =
|
||||
Arc::new(RwLock::new(HashMap::<_, NotificationHandler>::default()));
|
||||
Arc::new(Mutex::new(HashMap::<_, NotificationHandler>::default()));
|
||||
let response_handlers = Arc::new(Mutex::new(HashMap::<_, ResponseHandler>::default()));
|
||||
let input_task = executor.spawn(
|
||||
{
|
||||
let notification_handlers = notification_handlers.clone();
|
||||
let response_handlers = response_handlers.clone();
|
||||
let mut outbound_tx = outbound_tx.clone();
|
||||
async move {
|
||||
let _clear_response_handlers = ClearResponseHandlers(response_handlers.clone());
|
||||
let mut buffer = Vec::new();
|
||||
loop {
|
||||
buffer.clear();
|
||||
stdout.read_until(b'\n', &mut buffer).await?;
|
||||
stdout.read_until(b'\n', &mut buffer).await?;
|
||||
let message_len: usize = std::str::from_utf8(&buffer)?
|
||||
.strip_prefix(CONTENT_LEN_HEADER)
|
||||
.ok_or_else(|| anyhow!("invalid header"))?
|
||||
.trim_end()
|
||||
.parse()?;
|
||||
let input_task = cx.spawn(|cx| {
|
||||
let notification_handlers = notification_handlers.clone();
|
||||
let response_handlers = response_handlers.clone();
|
||||
async move {
|
||||
let _clear_response_handlers = ClearResponseHandlers(response_handlers.clone());
|
||||
let mut buffer = Vec::new();
|
||||
loop {
|
||||
buffer.clear();
|
||||
stdout.read_until(b'\n', &mut buffer).await?;
|
||||
stdout.read_until(b'\n', &mut buffer).await?;
|
||||
let message_len: usize = std::str::from_utf8(&buffer)?
|
||||
.strip_prefix(CONTENT_LEN_HEADER)
|
||||
.ok_or_else(|| anyhow!("invalid header"))?
|
||||
.trim_end()
|
||||
.parse()?;
|
||||
|
||||
buffer.resize(message_len, 0);
|
||||
stdout.read_exact(&mut buffer).await?;
|
||||
buffer.resize(message_len, 0);
|
||||
stdout.read_exact(&mut buffer).await?;
|
||||
log::trace!("incoming message:{}", String::from_utf8_lossy(&buffer));
|
||||
|
||||
if let Ok(AnyNotification { id, method, params }) =
|
||||
serde_json::from_slice(&buffer)
|
||||
{
|
||||
if let Some(handler) = notification_handlers.write().get_mut(method) {
|
||||
if let Err(e) = handler(id, params.get(), &mut outbound_tx) {
|
||||
log::error!("error handling {} message: {:?}", method, e);
|
||||
}
|
||||
} else {
|
||||
log::info!(
|
||||
"unhandled notification {}:\n{}",
|
||||
method,
|
||||
serde_json::to_string_pretty(
|
||||
&Value::from_str(params.get()).unwrap()
|
||||
)
|
||||
.unwrap()
|
||||
);
|
||||
}
|
||||
} else if let Ok(AnyResponse { id, error, result }) =
|
||||
serde_json::from_slice(&buffer)
|
||||
{
|
||||
if let Some(handler) = response_handlers.lock().remove(&id) {
|
||||
if let Some(error) = error {
|
||||
handler(Err(error));
|
||||
} else if let Some(result) = result {
|
||||
handler(Ok(result.get()));
|
||||
} else {
|
||||
handler(Ok("null"));
|
||||
}
|
||||
}
|
||||
if let Ok(msg) = serde_json::from_slice::<AnyNotification>(&buffer) {
|
||||
if let Some(handler) = notification_handlers.lock().get_mut(msg.method) {
|
||||
handler(msg.id, msg.params.get(), cx.clone());
|
||||
} else {
|
||||
return Err(anyhow!(
|
||||
"failed to deserialize message:\n{}",
|
||||
std::str::from_utf8(&buffer)?
|
||||
));
|
||||
on_unhandled_notification(msg);
|
||||
}
|
||||
} else if let Ok(AnyResponse { id, error, result }) =
|
||||
serde_json::from_slice(&buffer)
|
||||
{
|
||||
if let Some(handler) = response_handlers.lock().remove(&id) {
|
||||
if let Some(error) = error {
|
||||
handler(Err(error));
|
||||
} else if let Some(result) = result {
|
||||
handler(Ok(result.get()));
|
||||
} else {
|
||||
handler(Ok("null"));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return Err(anyhow!(
|
||||
"failed to deserialize message:\n{}",
|
||||
std::str::from_utf8(&buffer)?
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
.log_err(),
|
||||
);
|
||||
.log_err()
|
||||
});
|
||||
let (output_done_tx, output_done_rx) = barrier::channel();
|
||||
let output_task = executor.spawn({
|
||||
let output_task = cx.background().spawn({
|
||||
let response_handlers = response_handlers.clone();
|
||||
async move {
|
||||
let _clear_response_handlers = ClearResponseHandlers(response_handlers);
|
||||
let mut content_len_buffer = Vec::new();
|
||||
while let Ok(message) = outbound_rx.recv().await {
|
||||
log::trace!("outgoing message:{}", String::from_utf8_lossy(&message));
|
||||
content_len_buffer.clear();
|
||||
write!(content_len_buffer, "{}", message.len()).unwrap();
|
||||
stdin.write_all(CONTENT_LEN_HEADER.as_bytes()).await?;
|
||||
@@ -251,18 +235,15 @@ impl LanguageServer {
|
||||
capabilities: Default::default(),
|
||||
next_id: Default::default(),
|
||||
outbound_tx,
|
||||
executor: executor.clone(),
|
||||
executor: cx.background().clone(),
|
||||
io_tasks: Mutex::new(Some((input_task, output_task))),
|
||||
output_done_rx: Mutex::new(Some(output_done_rx)),
|
||||
root_path: root_path.to_path_buf(),
|
||||
options,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn initialize(mut self) -> Result<Arc<Self>> {
|
||||
let options = self.options.take();
|
||||
let mut this = Arc::new(self);
|
||||
let root_uri = Url::from_file_path(&this.root_path).unwrap();
|
||||
pub async fn initialize(mut self, options: Option<Value>) -> Result<Arc<Self>> {
|
||||
let root_uri = Url::from_file_path(&self.root_path).unwrap();
|
||||
#[allow(deprecated)]
|
||||
let params = InitializeParams {
|
||||
process_id: Default::default(),
|
||||
@@ -288,12 +269,13 @@ impl LanguageServer {
|
||||
value_set: vec![
|
||||
CodeActionKind::REFACTOR.as_str().into(),
|
||||
CodeActionKind::QUICKFIX.as_str().into(),
|
||||
CodeActionKind::SOURCE.as_str().into(),
|
||||
],
|
||||
},
|
||||
}),
|
||||
data_support: Some(true),
|
||||
resolve_support: Some(CodeActionCapabilityResolveSupport {
|
||||
properties: vec!["edit".to_string()],
|
||||
properties: vec!["edit".to_string(), "command".to_string()],
|
||||
}),
|
||||
..Default::default()
|
||||
}),
|
||||
@@ -324,16 +306,14 @@ impl LanguageServer {
|
||||
locale: Default::default(),
|
||||
};
|
||||
|
||||
let response = this.request::<request::Initialize>(params).await?;
|
||||
{
|
||||
let this = Arc::get_mut(&mut this).unwrap();
|
||||
if let Some(info) = response.server_info {
|
||||
this.name = info.name;
|
||||
}
|
||||
this.capabilities = response.capabilities;
|
||||
let response = self.request::<request::Initialize>(params).await?;
|
||||
if let Some(info) = response.server_info {
|
||||
self.name = info.name;
|
||||
}
|
||||
this.notify::<notification::Initialized>(InitializedParams {})?;
|
||||
Ok(this)
|
||||
self.capabilities = response.capabilities;
|
||||
|
||||
self.notify::<notification::Initialized>(InitializedParams {})?;
|
||||
Ok(Arc::new(self))
|
||||
}
|
||||
|
||||
pub fn shutdown(&self) -> Option<impl 'static + Send + Future<Output = Option<()>>> {
|
||||
@@ -368,37 +348,42 @@ impl LanguageServer {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_notification<T, F>(&mut self, f: F) -> Subscription
|
||||
#[must_use]
|
||||
pub fn on_notification<T, F>(&self, f: F) -> Subscription
|
||||
where
|
||||
T: notification::Notification,
|
||||
F: 'static + Send + Sync + FnMut(T::Params),
|
||||
F: 'static + Send + FnMut(T::Params, AsyncAppContext),
|
||||
{
|
||||
self.on_custom_notification(T::METHOD, f)
|
||||
}
|
||||
|
||||
pub fn on_request<T, F>(&mut self, f: F) -> Subscription
|
||||
#[must_use]
|
||||
pub fn on_request<T, F, Fut>(&self, f: F) -> Subscription
|
||||
where
|
||||
T: request::Request,
|
||||
F: 'static + Send + Sync + FnMut(T::Params) -> Result<T::Result>,
|
||||
T::Params: 'static + Send,
|
||||
F: 'static + Send + FnMut(T::Params, AsyncAppContext) -> Fut,
|
||||
Fut: 'static + Future<Output = Result<T::Result>>,
|
||||
{
|
||||
self.on_custom_request(T::METHOD, f)
|
||||
}
|
||||
|
||||
pub fn on_custom_notification<Params, F>(
|
||||
&mut self,
|
||||
method: &'static str,
|
||||
mut f: F,
|
||||
) -> Subscription
|
||||
pub fn remove_request_handler<T: request::Request>(&self) {
|
||||
self.notification_handlers.lock().remove(T::METHOD);
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn on_custom_notification<Params, F>(&self, method: &'static str, mut f: F) -> Subscription
|
||||
where
|
||||
F: 'static + Send + Sync + FnMut(Params),
|
||||
F: 'static + Send + FnMut(Params, AsyncAppContext),
|
||||
Params: DeserializeOwned,
|
||||
{
|
||||
let prev_handler = self.notification_handlers.write().insert(
|
||||
let prev_handler = self.notification_handlers.lock().insert(
|
||||
method,
|
||||
Box::new(move |_, params, _| {
|
||||
let params = serde_json::from_str(params)?;
|
||||
f(params);
|
||||
Ok(())
|
||||
Box::new(move |_, params, cx| {
|
||||
if let Some(params) = serde_json::from_str(params).log_err() {
|
||||
f(params, cx);
|
||||
}
|
||||
}),
|
||||
);
|
||||
assert!(
|
||||
@@ -411,26 +396,52 @@ impl LanguageServer {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_custom_request<Params, Res, F>(
|
||||
&mut self,
|
||||
#[must_use]
|
||||
pub fn on_custom_request<Params, Res, Fut, F>(
|
||||
&self,
|
||||
method: &'static str,
|
||||
mut f: F,
|
||||
) -> Subscription
|
||||
where
|
||||
F: 'static + Send + Sync + FnMut(Params) -> Result<Res>,
|
||||
Params: DeserializeOwned,
|
||||
F: 'static + Send + FnMut(Params, AsyncAppContext) -> Fut,
|
||||
Fut: 'static + Future<Output = Result<Res>>,
|
||||
Params: DeserializeOwned + Send + 'static,
|
||||
Res: Serialize,
|
||||
{
|
||||
let prev_handler = self.notification_handlers.write().insert(
|
||||
let outbound_tx = self.outbound_tx.clone();
|
||||
let prev_handler = self.notification_handlers.lock().insert(
|
||||
method,
|
||||
Box::new(move |id, params, tx| {
|
||||
Box::new(move |id, params, cx| {
|
||||
if let Some(id) = id {
|
||||
let params = serde_json::from_str(params)?;
|
||||
let result = f(params)?;
|
||||
let response = serde_json::to_vec(&Response { id, result })?;
|
||||
tx.try_send(response)?;
|
||||
if let Some(params) = serde_json::from_str(params).log_err() {
|
||||
let response = f(params, cx.clone());
|
||||
cx.foreground()
|
||||
.spawn({
|
||||
let outbound_tx = outbound_tx.clone();
|
||||
async move {
|
||||
let response = match response.await {
|
||||
Ok(result) => Response {
|
||||
id,
|
||||
result: Some(result),
|
||||
error: None,
|
||||
},
|
||||
Err(error) => Response {
|
||||
id,
|
||||
result: None,
|
||||
error: Some(Error {
|
||||
message: error.to_string(),
|
||||
}),
|
||||
},
|
||||
};
|
||||
if let Some(response) = serde_json::to_vec(&response).log_err()
|
||||
{
|
||||
outbound_tx.try_send(response).ok();
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}),
|
||||
);
|
||||
assert!(
|
||||
@@ -456,7 +467,7 @@ impl LanguageServer {
|
||||
}
|
||||
|
||||
pub fn request<T: request::Request>(
|
||||
self: &Arc<Self>,
|
||||
&self,
|
||||
params: T::Params,
|
||||
) -> impl Future<Output = Result<T::Result>>
|
||||
where
|
||||
@@ -547,36 +558,17 @@ impl Subscription {
|
||||
|
||||
impl Drop for Subscription {
|
||||
fn drop(&mut self) {
|
||||
self.notification_handlers.write().remove(self.method);
|
||||
self.notification_handlers.lock().remove(self.method);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
#[derive(Clone)]
|
||||
pub struct FakeLanguageServer {
|
||||
handlers: FakeLanguageServerHandlers,
|
||||
outgoing_tx: futures::channel::mpsc::UnboundedSender<Vec<u8>>,
|
||||
incoming_rx: futures::channel::mpsc::UnboundedReceiver<Vec<u8>>,
|
||||
_input_task: Task<Result<()>>,
|
||||
_output_task: Task<Result<()>>,
|
||||
pub server: Arc<LanguageServer>,
|
||||
notifications_rx: channel::Receiver<(String, String)>,
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
type FakeLanguageServerHandlers = Arc<
|
||||
Mutex<
|
||||
HashMap<
|
||||
&'static str,
|
||||
Box<
|
||||
dyn Send
|
||||
+ FnMut(
|
||||
usize,
|
||||
&[u8],
|
||||
gpui::AsyncAppContext,
|
||||
) -> futures::future::BoxFuture<'static, Vec<u8>>,
|
||||
>,
|
||||
>,
|
||||
>,
|
||||
>;
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
impl LanguageServer {
|
||||
pub fn full_capabilities() -> ServerCapabilities {
|
||||
@@ -589,177 +581,101 @@ impl LanguageServer {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn fake(cx: &mut gpui::MutableAppContext) -> (Self, FakeLanguageServer) {
|
||||
pub fn fake(cx: AsyncAppContext) -> (Self, FakeLanguageServer) {
|
||||
Self::fake_with_capabilities(Self::full_capabilities(), cx)
|
||||
}
|
||||
|
||||
pub fn fake_with_capabilities(
|
||||
capabilities: ServerCapabilities,
|
||||
cx: &mut gpui::MutableAppContext,
|
||||
cx: AsyncAppContext,
|
||||
) -> (Self, FakeLanguageServer) {
|
||||
let (stdin_writer, stdin_reader) = async_pipe::pipe();
|
||||
let (stdout_writer, stdout_reader) = async_pipe::pipe();
|
||||
let (notifications_tx, notifications_rx) = channel::unbounded();
|
||||
|
||||
let mut fake = FakeLanguageServer::new(stdin_reader, stdout_writer, cx);
|
||||
fake.handle_request::<request::Initialize, _, _>({
|
||||
let capabilities = capabilities.clone();
|
||||
move |_, _| {
|
||||
let capabilities = capabilities.clone();
|
||||
async move {
|
||||
InitializeResult {
|
||||
capabilities,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let executor = cx.background().clone();
|
||||
let server = Self::new_internal(
|
||||
0,
|
||||
stdin_writer,
|
||||
stdout_reader,
|
||||
Path::new("/"),
|
||||
None,
|
||||
executor,
|
||||
cx.clone(),
|
||||
|_| {},
|
||||
);
|
||||
let fake = FakeLanguageServer {
|
||||
server: Arc::new(Self::new_internal(
|
||||
0,
|
||||
stdout_writer,
|
||||
stdin_reader,
|
||||
Path::new("/"),
|
||||
cx.clone(),
|
||||
move |msg| {
|
||||
notifications_tx
|
||||
.try_send((msg.method.to_string(), msg.params.get().to_string()))
|
||||
.ok();
|
||||
},
|
||||
)),
|
||||
notifications_rx,
|
||||
};
|
||||
fake.handle_request::<request::Initialize, _, _>({
|
||||
let capabilities = capabilities.clone();
|
||||
move |_, _| {
|
||||
let capabilities = capabilities.clone();
|
||||
async move {
|
||||
Ok(InitializeResult {
|
||||
capabilities,
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
(server, fake)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
impl FakeLanguageServer {
|
||||
fn new(
|
||||
stdin: async_pipe::PipeReader,
|
||||
stdout: async_pipe::PipeWriter,
|
||||
cx: &mut gpui::MutableAppContext,
|
||||
) -> Self {
|
||||
use futures::StreamExt as _;
|
||||
|
||||
let (incoming_tx, incoming_rx) = futures::channel::mpsc::unbounded();
|
||||
let (outgoing_tx, mut outgoing_rx) = futures::channel::mpsc::unbounded();
|
||||
let handlers = FakeLanguageServerHandlers::default();
|
||||
|
||||
let input_task = cx.spawn(|cx| {
|
||||
let handlers = handlers.clone();
|
||||
let outgoing_tx = outgoing_tx.clone();
|
||||
async move {
|
||||
let mut buffer = Vec::new();
|
||||
let mut stdin = smol::io::BufReader::new(stdin);
|
||||
while Self::receive(&mut stdin, &mut buffer).await.is_ok() {
|
||||
cx.background().simulate_random_delay().await;
|
||||
|
||||
if let Ok(request) = serde_json::from_slice::<AnyRequest>(&buffer) {
|
||||
assert_eq!(request.jsonrpc, JSON_RPC_VERSION);
|
||||
|
||||
let response;
|
||||
if let Some(handler) = handlers.lock().get_mut(request.method) {
|
||||
response =
|
||||
handler(request.id, request.params.get().as_bytes(), cx.clone())
|
||||
.await;
|
||||
log::debug!("handled lsp request. method:{}", request.method);
|
||||
} else {
|
||||
response = serde_json::to_vec(&AnyResponse {
|
||||
id: request.id,
|
||||
error: Some(Error {
|
||||
message: "no handler".to_string(),
|
||||
}),
|
||||
result: None,
|
||||
})
|
||||
.unwrap();
|
||||
log::debug!("unhandled lsp request. method:{}", request.method);
|
||||
}
|
||||
outgoing_tx.unbounded_send(response)?;
|
||||
} else {
|
||||
incoming_tx.unbounded_send(buffer.clone())?;
|
||||
}
|
||||
}
|
||||
Ok::<_, anyhow::Error>(())
|
||||
}
|
||||
});
|
||||
|
||||
let output_task = cx.background().spawn(async move {
|
||||
let mut stdout = smol::io::BufWriter::new(stdout);
|
||||
while let Some(message) = outgoing_rx.next().await {
|
||||
stdout.write_all(CONTENT_LEN_HEADER.as_bytes()).await?;
|
||||
stdout
|
||||
.write_all((format!("{}", message.len())).as_bytes())
|
||||
.await?;
|
||||
stdout.write_all("\r\n\r\n".as_bytes()).await?;
|
||||
stdout.write_all(&message).await?;
|
||||
stdout.flush().await?;
|
||||
}
|
||||
Ok(())
|
||||
});
|
||||
|
||||
Self {
|
||||
outgoing_tx,
|
||||
incoming_rx,
|
||||
handlers,
|
||||
_input_task: input_task,
|
||||
_output_task: output_task,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn notify<T: notification::Notification>(&mut self, params: T::Params) {
|
||||
let message = serde_json::to_vec(&Notification {
|
||||
jsonrpc: JSON_RPC_VERSION,
|
||||
method: T::METHOD,
|
||||
params,
|
||||
})
|
||||
.unwrap();
|
||||
self.outgoing_tx.unbounded_send(message).unwrap();
|
||||
pub fn notify<T: notification::Notification>(&self, params: T::Params) {
|
||||
self.server.notify::<T>(params).ok();
|
||||
}
|
||||
|
||||
pub async fn receive_notification<T: notification::Notification>(&mut self) -> T::Params {
|
||||
use futures::StreamExt as _;
|
||||
|
||||
loop {
|
||||
let bytes = self.incoming_rx.next().await.unwrap();
|
||||
if let Ok(notification) = serde_json::from_slice::<Notification<T::Params>>(&bytes) {
|
||||
assert_eq!(notification.method, T::METHOD);
|
||||
return notification.params;
|
||||
let (method, params) = self.notifications_rx.next().await.unwrap();
|
||||
if &method == T::METHOD {
|
||||
return serde_json::from_str::<T::Params>(¶ms).unwrap();
|
||||
} else {
|
||||
log::info!(
|
||||
"skipping message in fake language server {:?}",
|
||||
std::str::from_utf8(&bytes)
|
||||
);
|
||||
log::info!("skipping message in fake language server {:?}", params);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_request<T, F, Fut>(
|
||||
&mut self,
|
||||
&self,
|
||||
mut handler: F,
|
||||
) -> futures::channel::mpsc::UnboundedReceiver<()>
|
||||
where
|
||||
T: 'static + request::Request,
|
||||
T::Params: 'static + Send,
|
||||
F: 'static + Send + FnMut(T::Params, gpui::AsyncAppContext) -> Fut,
|
||||
Fut: 'static + Send + Future<Output = T::Result>,
|
||||
Fut: 'static + Send + Future<Output = Result<T::Result>>,
|
||||
{
|
||||
use futures::FutureExt as _;
|
||||
|
||||
let (responded_tx, responded_rx) = futures::channel::mpsc::unbounded();
|
||||
self.handlers.lock().insert(
|
||||
T::METHOD,
|
||||
Box::new(move |id, params, cx| {
|
||||
let result = handler(serde_json::from_slice::<T::Params>(params).unwrap(), cx);
|
||||
self.server.remove_request_handler::<T>();
|
||||
self.server
|
||||
.on_request::<T, _, _>(move |params, cx| {
|
||||
let result = handler(params, cx.clone());
|
||||
let responded_tx = responded_tx.clone();
|
||||
async move {
|
||||
cx.background().simulate_random_delay().await;
|
||||
let result = result.await;
|
||||
let result = serde_json::to_string(&result).unwrap();
|
||||
let result = serde_json::from_str::<&RawValue>(&result).unwrap();
|
||||
let response = AnyResponse {
|
||||
id,
|
||||
error: None,
|
||||
result: Some(result),
|
||||
};
|
||||
responded_tx.unbounded_send(()).ok();
|
||||
serde_json::to_vec(&response).unwrap()
|
||||
result
|
||||
}
|
||||
.boxed()
|
||||
}),
|
||||
);
|
||||
})
|
||||
.detach();
|
||||
responded_rx
|
||||
}
|
||||
|
||||
@@ -767,7 +683,7 @@ impl FakeLanguageServer {
|
||||
where
|
||||
T: 'static + request::Request,
|
||||
{
|
||||
self.handlers.lock().remove(T::METHOD);
|
||||
self.server.remove_request_handler::<T>();
|
||||
}
|
||||
|
||||
pub async fn start_progress(&mut self, token: impl Into<String>) {
|
||||
@@ -783,25 +699,6 @@ impl FakeLanguageServer {
|
||||
value: ProgressParamsValue::WorkDone(WorkDoneProgress::End(Default::default())),
|
||||
});
|
||||
}
|
||||
|
||||
async fn receive(
|
||||
stdin: &mut smol::io::BufReader<async_pipe::PipeReader>,
|
||||
buffer: &mut Vec<u8>,
|
||||
) -> Result<()> {
|
||||
buffer.clear();
|
||||
stdin.read_until(b'\n', buffer).await?;
|
||||
stdin.read_until(b'\n', buffer).await?;
|
||||
let message_len: usize = std::str::from_utf8(buffer)
|
||||
.unwrap()
|
||||
.strip_prefix(CONTENT_LEN_HEADER)
|
||||
.ok_or_else(|| anyhow!("invalid content length header"))?
|
||||
.trim_end()
|
||||
.parse()
|
||||
.unwrap();
|
||||
buffer.resize(message_len, 0);
|
||||
stdin.read_exact(buffer).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
struct ClearResponseHandlers(Arc<Mutex<HashMap<usize, ResponseHandler>>>);
|
||||
@@ -826,22 +723,22 @@ mod tests {
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_fake(cx: &mut TestAppContext) {
|
||||
let (mut server, mut fake) = cx.update(LanguageServer::fake);
|
||||
let (server, mut fake) = LanguageServer::fake(cx.to_async());
|
||||
|
||||
let (message_tx, message_rx) = channel::unbounded();
|
||||
let (diagnostics_tx, diagnostics_rx) = channel::unbounded();
|
||||
server
|
||||
.on_notification::<notification::ShowMessage, _>(move |params| {
|
||||
.on_notification::<notification::ShowMessage, _>(move |params, _| {
|
||||
message_tx.try_send(params).unwrap()
|
||||
})
|
||||
.detach();
|
||||
server
|
||||
.on_notification::<notification::PublishDiagnostics, _>(move |params| {
|
||||
.on_notification::<notification::PublishDiagnostics, _>(move |params, _| {
|
||||
diagnostics_tx.try_send(params).unwrap()
|
||||
})
|
||||
.detach();
|
||||
|
||||
let server = server.initialize().await.unwrap();
|
||||
let server = server.initialize(None).await.unwrap();
|
||||
server
|
||||
.notify::<notification::DidOpenTextDocument>(DidOpenTextDocumentParams {
|
||||
text_document: TextDocumentItem::new(
|
||||
@@ -876,7 +773,7 @@ mod tests {
|
||||
"file://b/c"
|
||||
);
|
||||
|
||||
fake.handle_request::<request::Shutdown, _, _>(|_, _| async move {});
|
||||
fake.handle_request::<request::Shutdown, _, _>(|_, _| async move { Ok(()) });
|
||||
|
||||
drop(server);
|
||||
fake.receive_notification::<notification::Exit>().await;
|
||||
|
||||
@@ -4,9 +4,9 @@ use async_trait::async_trait;
|
||||
use client::{proto, PeerId};
|
||||
use gpui::{AppContext, AsyncAppContext, ModelHandle};
|
||||
use language::{
|
||||
point_from_lsp,
|
||||
point_from_lsp, point_to_lsp,
|
||||
proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version},
|
||||
range_from_lsp, Anchor, Bias, Buffer, PointUtf16, ToLspPosition, ToPointUtf16,
|
||||
range_from_lsp, Anchor, Bias, Buffer, PointUtf16, ToPointUtf16,
|
||||
};
|
||||
use lsp::{DocumentHighlightKind, ServerCapabilities};
|
||||
use std::{cmp::Reverse, ops::Range, path::Path};
|
||||
@@ -91,7 +91,7 @@ impl LspCommand for PrepareRename {
|
||||
text_document: lsp::TextDocumentIdentifier {
|
||||
uri: lsp::Url::from_file_path(path).unwrap(),
|
||||
},
|
||||
position: self.position.to_lsp_position(),
|
||||
position: point_to_lsp(self.position),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,7 +208,7 @@ impl LspCommand for PerformRename {
|
||||
text_document: lsp::TextDocumentIdentifier {
|
||||
uri: lsp::Url::from_file_path(path).unwrap(),
|
||||
},
|
||||
position: self.position.to_lsp_position(),
|
||||
position: point_to_lsp(self.position),
|
||||
},
|
||||
new_name: self.new_name.clone(),
|
||||
work_done_progress_params: Default::default(),
|
||||
@@ -223,22 +223,19 @@ impl LspCommand for PerformRename {
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<ProjectTransaction> {
|
||||
if let Some(edit) = message {
|
||||
let language_server = project
|
||||
let (lsp_adapter, lsp_server) = project
|
||||
.read_with(&cx, |project, cx| {
|
||||
project
|
||||
.language_server_for_buffer(buffer.read(cx), cx)
|
||||
.cloned()
|
||||
})
|
||||
.ok_or_else(|| anyhow!("no language server found for buffer"))?;
|
||||
let language = buffer
|
||||
.read_with(&cx, |buffer, _| buffer.language().cloned())
|
||||
.ok_or_else(|| anyhow!("no language for buffer"))?;
|
||||
Project::deserialize_workspace_edit(
|
||||
project,
|
||||
edit,
|
||||
self.push_to_history,
|
||||
language.name(),
|
||||
language_server,
|
||||
lsp_adapter,
|
||||
lsp_server,
|
||||
&mut cx,
|
||||
)
|
||||
.await
|
||||
@@ -328,7 +325,7 @@ impl LspCommand for GetDefinition {
|
||||
text_document: lsp::TextDocumentIdentifier {
|
||||
uri: lsp::Url::from_file_path(path).unwrap(),
|
||||
},
|
||||
position: self.position.to_lsp_position(),
|
||||
position: point_to_lsp(self.position),
|
||||
},
|
||||
work_done_progress_params: Default::default(),
|
||||
partial_result_params: Default::default(),
|
||||
@@ -343,16 +340,13 @@ impl LspCommand for GetDefinition {
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<Vec<Location>> {
|
||||
let mut definitions = Vec::new();
|
||||
let language_server = project
|
||||
let (lsp_adapter, language_server) = project
|
||||
.read_with(&cx, |project, cx| {
|
||||
project
|
||||
.language_server_for_buffer(buffer.read(cx), cx)
|
||||
.cloned()
|
||||
})
|
||||
.ok_or_else(|| anyhow!("no language server found for buffer"))?;
|
||||
let language = buffer
|
||||
.read_with(&cx, |buffer, _| buffer.language().cloned())
|
||||
.ok_or_else(|| anyhow!("no language for buffer"))?;
|
||||
|
||||
if let Some(message) = message {
|
||||
let mut unresolved_locations = Vec::new();
|
||||
@@ -377,7 +371,7 @@ impl LspCommand for GetDefinition {
|
||||
.update(&mut cx, |this, cx| {
|
||||
this.open_local_buffer_via_lsp(
|
||||
target_uri,
|
||||
language.name(),
|
||||
lsp_adapter.clone(),
|
||||
language_server.clone(),
|
||||
cx,
|
||||
)
|
||||
@@ -503,7 +497,7 @@ impl LspCommand for GetReferences {
|
||||
text_document: lsp::TextDocumentIdentifier {
|
||||
uri: lsp::Url::from_file_path(path).unwrap(),
|
||||
},
|
||||
position: self.position.to_lsp_position(),
|
||||
position: point_to_lsp(self.position),
|
||||
},
|
||||
work_done_progress_params: Default::default(),
|
||||
partial_result_params: Default::default(),
|
||||
@@ -521,16 +515,13 @@ impl LspCommand for GetReferences {
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<Vec<Location>> {
|
||||
let mut references = Vec::new();
|
||||
let language_server = project
|
||||
let (lsp_adapter, language_server) = project
|
||||
.read_with(&cx, |project, cx| {
|
||||
project
|
||||
.language_server_for_buffer(buffer.read(cx), cx)
|
||||
.cloned()
|
||||
})
|
||||
.ok_or_else(|| anyhow!("no language server found for buffer"))?;
|
||||
let language = buffer
|
||||
.read_with(&cx, |buffer, _| buffer.language().cloned())
|
||||
.ok_or_else(|| anyhow!("no language for buffer"))?;
|
||||
|
||||
if let Some(locations) = locations {
|
||||
for lsp_location in locations {
|
||||
@@ -538,7 +529,7 @@ impl LspCommand for GetReferences {
|
||||
.update(&mut cx, |this, cx| {
|
||||
this.open_local_buffer_via_lsp(
|
||||
lsp_location.uri,
|
||||
language.name(),
|
||||
lsp_adapter.clone(),
|
||||
language_server.clone(),
|
||||
cx,
|
||||
)
|
||||
@@ -668,7 +659,7 @@ impl LspCommand for GetDocumentHighlights {
|
||||
text_document: lsp::TextDocumentIdentifier {
|
||||
uri: lsp::Url::from_file_path(path).unwrap(),
|
||||
},
|
||||
position: self.position.to_lsp_position(),
|
||||
position: point_to_lsp(self.position),
|
||||
},
|
||||
work_done_progress_params: Default::default(),
|
||||
partial_result_params: Default::default(),
|
||||
|
||||
+673
-371
File diff suppressed because it is too large
Load Diff
+45
-34
@@ -48,43 +48,45 @@ message Envelope {
|
||||
SaveBuffer save_buffer = 40;
|
||||
BufferSaved buffer_saved = 41;
|
||||
BufferReloaded buffer_reloaded = 42;
|
||||
FormatBuffers format_buffers = 43;
|
||||
FormatBuffersResponse format_buffers_response = 44;
|
||||
GetCompletions get_completions = 45;
|
||||
GetCompletionsResponse get_completions_response = 46;
|
||||
ApplyCompletionAdditionalEdits apply_completion_additional_edits = 47;
|
||||
ApplyCompletionAdditionalEditsResponse apply_completion_additional_edits_response = 48;
|
||||
GetCodeActions get_code_actions = 49;
|
||||
GetCodeActionsResponse get_code_actions_response = 50;
|
||||
ApplyCodeAction apply_code_action = 51;
|
||||
ApplyCodeActionResponse apply_code_action_response = 52;
|
||||
PrepareRename prepare_rename = 53;
|
||||
PrepareRenameResponse prepare_rename_response = 54;
|
||||
PerformRename perform_rename = 55;
|
||||
PerformRenameResponse perform_rename_response = 56;
|
||||
SearchProject search_project = 57;
|
||||
SearchProjectResponse search_project_response = 58;
|
||||
ReloadBuffers reload_buffers = 43;
|
||||
ReloadBuffersResponse reload_buffers_response = 44;
|
||||
FormatBuffers format_buffers = 45;
|
||||
FormatBuffersResponse format_buffers_response = 46;
|
||||
GetCompletions get_completions = 47;
|
||||
GetCompletionsResponse get_completions_response = 48;
|
||||
ApplyCompletionAdditionalEdits apply_completion_additional_edits = 49;
|
||||
ApplyCompletionAdditionalEditsResponse apply_completion_additional_edits_response = 50;
|
||||
GetCodeActions get_code_actions = 51;
|
||||
GetCodeActionsResponse get_code_actions_response = 52;
|
||||
ApplyCodeAction apply_code_action = 53;
|
||||
ApplyCodeActionResponse apply_code_action_response = 54;
|
||||
PrepareRename prepare_rename = 55;
|
||||
PrepareRenameResponse prepare_rename_response = 56;
|
||||
PerformRename perform_rename = 57;
|
||||
PerformRenameResponse perform_rename_response = 58;
|
||||
SearchProject search_project = 59;
|
||||
SearchProjectResponse search_project_response = 60;
|
||||
|
||||
GetChannels get_channels = 59;
|
||||
GetChannelsResponse get_channels_response = 60;
|
||||
JoinChannel join_channel = 61;
|
||||
JoinChannelResponse join_channel_response = 62;
|
||||
LeaveChannel leave_channel = 63;
|
||||
SendChannelMessage send_channel_message = 64;
|
||||
SendChannelMessageResponse send_channel_message_response = 65;
|
||||
ChannelMessageSent channel_message_sent = 66;
|
||||
GetChannelMessages get_channel_messages = 67;
|
||||
GetChannelMessagesResponse get_channel_messages_response = 68;
|
||||
GetChannels get_channels = 61;
|
||||
GetChannelsResponse get_channels_response = 62;
|
||||
JoinChannel join_channel = 63;
|
||||
JoinChannelResponse join_channel_response = 64;
|
||||
LeaveChannel leave_channel = 65;
|
||||
SendChannelMessage send_channel_message = 66;
|
||||
SendChannelMessageResponse send_channel_message_response = 67;
|
||||
ChannelMessageSent channel_message_sent = 68;
|
||||
GetChannelMessages get_channel_messages = 69;
|
||||
GetChannelMessagesResponse get_channel_messages_response = 70;
|
||||
|
||||
UpdateContacts update_contacts = 69;
|
||||
UpdateContacts update_contacts = 71;
|
||||
|
||||
GetUsers get_users = 70;
|
||||
GetUsersResponse get_users_response = 71;
|
||||
GetUsers get_users = 72;
|
||||
GetUsersResponse get_users_response = 73;
|
||||
|
||||
Follow follow = 72;
|
||||
FollowResponse follow_response = 73;
|
||||
UpdateFollowers update_followers = 74;
|
||||
Unfollow unfollow = 75;
|
||||
Follow follow = 74;
|
||||
FollowResponse follow_response = 75;
|
||||
UpdateFollowers update_followers = 76;
|
||||
Unfollow unfollow = 77;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -229,7 +231,7 @@ message GetProjectSymbolsResponse {
|
||||
message Symbol {
|
||||
uint64 source_worktree_id = 1;
|
||||
uint64 worktree_id = 2;
|
||||
string language_name = 3;
|
||||
string language_server_name = 3;
|
||||
string name = 4;
|
||||
int32 kind = 5;
|
||||
string path = 6;
|
||||
@@ -299,6 +301,15 @@ message BufferReloaded {
|
||||
Timestamp mtime = 4;
|
||||
}
|
||||
|
||||
message ReloadBuffers {
|
||||
uint64 project_id = 1;
|
||||
repeated uint64 buffer_ids = 2;
|
||||
}
|
||||
|
||||
message ReloadBuffersResponse {
|
||||
ProjectTransaction transaction = 1;
|
||||
}
|
||||
|
||||
message FormatBuffers {
|
||||
uint64 project_id = 1;
|
||||
repeated uint64 buffer_ids = 2;
|
||||
|
||||
@@ -190,6 +190,8 @@ messages!(
|
||||
(Ping, Foreground),
|
||||
(RegisterProject, Foreground),
|
||||
(RegisterWorktree, Foreground),
|
||||
(ReloadBuffers, Foreground),
|
||||
(ReloadBuffersResponse, Foreground),
|
||||
(RemoveProjectCollaborator, Foreground),
|
||||
(SaveBuffer, Foreground),
|
||||
(SearchProject, Background),
|
||||
@@ -237,6 +239,7 @@ request_messages!(
|
||||
(PrepareRename, PrepareRenameResponse),
|
||||
(RegisterProject, RegisterProjectResponse),
|
||||
(RegisterWorktree, Ack),
|
||||
(ReloadBuffers, ReloadBuffersResponse),
|
||||
(SaveBuffer, BufferSaved),
|
||||
(SearchProject, SearchProjectResponse),
|
||||
(SendChannelMessage, SendChannelMessageResponse),
|
||||
@@ -268,6 +271,7 @@ entity_messages!(
|
||||
OpenBufferForSymbol,
|
||||
PerformRename,
|
||||
PrepareRename,
|
||||
ReloadBuffers,
|
||||
RemoveProjectCollaborator,
|
||||
SaveBuffer,
|
||||
SearchProject,
|
||||
|
||||
@@ -5,4 +5,4 @@ pub mod proto;
|
||||
pub use conn::Connection;
|
||||
pub use peer::*;
|
||||
|
||||
pub const PROTOCOL_VERSION: u32 = 12;
|
||||
pub const PROTOCOL_VERSION: u32 = 13;
|
||||
|
||||
@@ -280,6 +280,15 @@ impl Item for ProjectSearchView {
|
||||
unreachable!("save_as should not have been called")
|
||||
}
|
||||
|
||||
fn reload(
|
||||
&mut self,
|
||||
project: ModelHandle<Project>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Task<anyhow::Result<()>> {
|
||||
self.results_editor
|
||||
.update(cx, |editor, cx| editor.reload(project, cx))
|
||||
}
|
||||
|
||||
fn clone_on_split(&self, cx: &mut ViewContext<Self>) -> Option<Self>
|
||||
where
|
||||
Self: Sized,
|
||||
|
||||
+513
-414
File diff suppressed because it is too large
Load Diff
@@ -164,6 +164,55 @@ fn test_line_len() {
|
||||
assert_eq!(buffer.line_len(5), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_common_prefix_at_positionn() {
|
||||
let text = "a = str; b = δα";
|
||||
let buffer = Buffer::new(0, 0, History::new(text.into()));
|
||||
|
||||
let offset1 = offset_after(text, "str");
|
||||
let offset2 = offset_after(text, "δα");
|
||||
|
||||
// the preceding word is a prefix of the suggestion
|
||||
assert_eq!(
|
||||
buffer.common_prefix_at(offset1, "string"),
|
||||
range_of(text, "str"),
|
||||
);
|
||||
// a suffix of the preceding word is a prefix of the suggestion
|
||||
assert_eq!(
|
||||
buffer.common_prefix_at(offset1, "tree"),
|
||||
range_of(text, "tr"),
|
||||
);
|
||||
// the preceding word is a substring of the suggestion, but not a prefix
|
||||
assert_eq!(
|
||||
buffer.common_prefix_at(offset1, "astro"),
|
||||
empty_range_after(text, "str"),
|
||||
);
|
||||
|
||||
// prefix matching is case insenstive.
|
||||
assert_eq!(
|
||||
buffer.common_prefix_at(offset1, "Strαngε"),
|
||||
range_of(text, "str"),
|
||||
);
|
||||
assert_eq!(
|
||||
buffer.common_prefix_at(offset2, "ΔΑΜΝ"),
|
||||
range_of(text, "δα"),
|
||||
);
|
||||
|
||||
fn offset_after(text: &str, part: &str) -> usize {
|
||||
text.find(part).unwrap() + part.len()
|
||||
}
|
||||
|
||||
fn empty_range_after(text: &str, part: &str) -> Range<usize> {
|
||||
let offset = offset_after(text, part);
|
||||
offset..offset
|
||||
}
|
||||
|
||||
fn range_of(text: &str, part: &str) -> Range<usize> {
|
||||
let start = text.find(part).unwrap();
|
||||
start..start + part.len()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_text_summary_for_range() {
|
||||
let buffer = Buffer::new(0, 0, History::new("ab\nefg\nhklm\nnopqrs\ntuvwxyz".into()));
|
||||
|
||||
@@ -1508,6 +1508,30 @@ impl BufferSnapshot {
|
||||
.eq(needle.bytes())
|
||||
}
|
||||
|
||||
pub fn common_prefix_at<T>(&self, position: T, needle: &str) -> Range<T>
|
||||
where
|
||||
T: ToOffset + TextDimension,
|
||||
{
|
||||
let offset = position.to_offset(self);
|
||||
let common_prefix_len = needle
|
||||
.char_indices()
|
||||
.map(|(index, _)| index)
|
||||
.chain([needle.len()])
|
||||
.take_while(|&len| len <= offset)
|
||||
.filter(|&len| {
|
||||
let left = self
|
||||
.chars_for_range(offset - len..offset)
|
||||
.flat_map(|c| char::to_lowercase(c));
|
||||
let right = needle[..len].chars().flat_map(|c| char::to_lowercase(c));
|
||||
left.eq(right)
|
||||
})
|
||||
.last()
|
||||
.unwrap_or(0);
|
||||
let start_offset = offset - common_prefix_len;
|
||||
let start = self.text_summary_for_range(0..start_offset);
|
||||
start..position
|
||||
}
|
||||
|
||||
pub fn text(&self) -> String {
|
||||
self.visible_text.to_string()
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ fn editor_focused(EditorFocused(editor): &EditorFocused, cx: &mut MutableAppCont
|
||||
let mode = if matches!(editor.read(cx).mode(), EditorMode::SingleLine) {
|
||||
Mode::Insert
|
||||
} else {
|
||||
Mode::Normal
|
||||
Mode::normal()
|
||||
};
|
||||
|
||||
VimState::update_global(cx, |state, cx| {
|
||||
|
||||
@@ -25,6 +25,23 @@ fn normal_before(_: &mut Workspace, _: &NormalBefore, cx: &mut ViewContext<Works
|
||||
(map.clip_point(cursor, Bias::Left), SelectionGoal::None)
|
||||
});
|
||||
});
|
||||
state.switch_mode(&SwitchMode(Mode::Normal), cx);
|
||||
state.switch_mode(&SwitchMode(Mode::normal()), cx);
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::{mode::Mode, vim_test_context::VimTestContext};
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_enter_and_exit_insert_mode(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true, "").await;
|
||||
cx.simulate_keystroke("i");
|
||||
assert_eq!(cx.mode(), Mode::Insert);
|
||||
cx.simulate_keystrokes(&["T", "e", "s", "t"]);
|
||||
cx.assert_editor_state("Test|");
|
||||
cx.simulate_keystroke("escape");
|
||||
assert_eq!(cx.mode(), Mode::normal());
|
||||
cx.assert_editor_state("Tes|t");
|
||||
}
|
||||
}
|
||||
|
||||
+40
-4
@@ -3,14 +3,14 @@ use gpui::keymap::Context;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Mode {
|
||||
Normal,
|
||||
Normal(NormalState),
|
||||
Insert,
|
||||
}
|
||||
|
||||
impl Mode {
|
||||
pub fn cursor_shape(&self) -> CursorShape {
|
||||
match self {
|
||||
Mode::Normal => CursorShape::Block,
|
||||
Mode::Normal(_) => CursorShape::Block,
|
||||
Mode::Insert => CursorShape::Bar,
|
||||
}
|
||||
}
|
||||
@@ -20,17 +20,53 @@ impl Mode {
|
||||
context.map.insert(
|
||||
"vim_mode".to_string(),
|
||||
match self {
|
||||
Self::Normal => "normal",
|
||||
Self::Normal(_) => "normal",
|
||||
Self::Insert => "insert",
|
||||
}
|
||||
.to_string(),
|
||||
);
|
||||
|
||||
match self {
|
||||
Self::Normal(normal_state) => normal_state.set_context(&mut context),
|
||||
_ => {}
|
||||
}
|
||||
context
|
||||
}
|
||||
|
||||
pub fn normal() -> Mode {
|
||||
Mode::Normal(Default::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Mode {
|
||||
fn default() -> Self {
|
||||
Self::Normal
|
||||
Self::Normal(Default::default())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum NormalState {
|
||||
None,
|
||||
GPrefix,
|
||||
}
|
||||
|
||||
impl NormalState {
|
||||
pub fn set_context(&self, context: &mut Context) {
|
||||
let submode = match self {
|
||||
Self::GPrefix => Some("g"),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
if let Some(submode) = submode {
|
||||
context
|
||||
.map
|
||||
.insert("vim_submode".to_string(), submode.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for NormalState {
|
||||
fn default() -> Self {
|
||||
NormalState::None
|
||||
}
|
||||
}
|
||||
|
||||
+373
-3
@@ -1,30 +1,55 @@
|
||||
use editor::{movement, Bias};
|
||||
mod g_prefix;
|
||||
|
||||
use editor::{char_kind, movement, Bias};
|
||||
use gpui::{action, keymap::Binding, MutableAppContext, ViewContext};
|
||||
use language::SelectionGoal;
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::{Mode, SwitchMode, VimState};
|
||||
use crate::{mode::NormalState, Mode, SwitchMode, VimState};
|
||||
|
||||
action!(InsertBefore);
|
||||
action!(GPrefix);
|
||||
action!(MoveLeft);
|
||||
action!(MoveDown);
|
||||
action!(MoveUp);
|
||||
action!(MoveRight);
|
||||
action!(MoveToStartOfLine);
|
||||
action!(MoveToEndOfLine);
|
||||
action!(MoveToEnd);
|
||||
action!(MoveToNextWordStart, bool);
|
||||
action!(MoveToNextWordEnd, bool);
|
||||
action!(MoveToPreviousWordStart, bool);
|
||||
|
||||
pub fn init(cx: &mut MutableAppContext) {
|
||||
let context = Some("Editor && vim_mode == normal");
|
||||
cx.add_bindings(vec![
|
||||
Binding::new("i", SwitchMode(Mode::Insert), context),
|
||||
Binding::new("g", SwitchMode(Mode::Normal(NormalState::GPrefix)), context),
|
||||
Binding::new("h", MoveLeft, context),
|
||||
Binding::new("j", MoveDown, context),
|
||||
Binding::new("k", MoveUp, context),
|
||||
Binding::new("l", MoveRight, context),
|
||||
Binding::new("0", MoveToStartOfLine, context),
|
||||
Binding::new("shift-$", MoveToEndOfLine, context),
|
||||
Binding::new("shift-G", MoveToEnd, context),
|
||||
Binding::new("w", MoveToNextWordStart(false), context),
|
||||
Binding::new("shift-W", MoveToNextWordStart(true), context),
|
||||
Binding::new("e", MoveToNextWordEnd(false), context),
|
||||
Binding::new("shift-E", MoveToNextWordEnd(true), context),
|
||||
Binding::new("b", MoveToPreviousWordStart(false), context),
|
||||
Binding::new("shift-B", MoveToPreviousWordStart(true), context),
|
||||
]);
|
||||
g_prefix::init(cx);
|
||||
|
||||
cx.add_action(move_left);
|
||||
cx.add_action(move_down);
|
||||
cx.add_action(move_up);
|
||||
cx.add_action(move_right);
|
||||
cx.add_action(move_to_start_of_line);
|
||||
cx.add_action(move_to_end_of_line);
|
||||
cx.add_action(move_to_end);
|
||||
cx.add_action(move_to_next_word_start);
|
||||
cx.add_action(move_to_next_word_end);
|
||||
cx.add_action(move_to_previous_word_start);
|
||||
}
|
||||
|
||||
fn move_left(_: &mut Workspace, _: &MoveLeft, cx: &mut ViewContext<Workspace>) {
|
||||
@@ -64,3 +89,348 @@ fn move_right(_: &mut Workspace, _: &MoveRight, cx: &mut ViewContext<Workspace>)
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn move_to_start_of_line(
|
||||
_: &mut Workspace,
|
||||
_: &MoveToStartOfLine,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) {
|
||||
VimState::update_global(cx, |state, cx| {
|
||||
state.update_active_editor(cx, |editor, cx| {
|
||||
editor.move_cursors(cx, |map, cursor, _| {
|
||||
(
|
||||
movement::line_beginning(map, cursor, false),
|
||||
SelectionGoal::None,
|
||||
)
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn move_to_end_of_line(_: &mut Workspace, _: &MoveToEndOfLine, cx: &mut ViewContext<Workspace>) {
|
||||
VimState::update_global(cx, |state, cx| {
|
||||
state.update_active_editor(cx, |editor, cx| {
|
||||
editor.move_cursors(cx, |map, cursor, _| {
|
||||
(
|
||||
map.clip_point(movement::line_end(map, cursor, false), Bias::Left),
|
||||
SelectionGoal::None,
|
||||
)
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn move_to_end(_: &mut Workspace, _: &MoveToEnd, cx: &mut ViewContext<Workspace>) {
|
||||
VimState::update_global(cx, |state, cx| {
|
||||
state.update_active_editor(cx, |editor, cx| {
|
||||
editor.replace_selections_with(cx, |map| map.clip_point(map.max_point(), Bias::Left));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn move_to_next_word_start(
|
||||
_: &mut Workspace,
|
||||
&MoveToNextWordStart(treat_punctuation_as_word): &MoveToNextWordStart,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) {
|
||||
VimState::update_global(cx, |state, cx| {
|
||||
state.update_active_editor(cx, |editor, cx| {
|
||||
editor.move_cursors(cx, |map, mut cursor, _| {
|
||||
let mut crossed_newline = false;
|
||||
cursor = movement::find_boundary(map, cursor, |left, right| {
|
||||
let left_kind = char_kind(left).coerce_punctuation(treat_punctuation_as_word);
|
||||
let right_kind = char_kind(right).coerce_punctuation(treat_punctuation_as_word);
|
||||
let at_newline = right == '\n';
|
||||
|
||||
let found = (left_kind != right_kind && !right.is_whitespace())
|
||||
|| (at_newline && crossed_newline)
|
||||
|| (at_newline && left == '\n'); // Prevents skipping repeated empty lines
|
||||
|
||||
if at_newline {
|
||||
crossed_newline = true;
|
||||
}
|
||||
found
|
||||
});
|
||||
(cursor, SelectionGoal::None)
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn move_to_next_word_end(
|
||||
_: &mut Workspace,
|
||||
&MoveToNextWordEnd(treat_punctuation_as_word): &MoveToNextWordEnd,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) {
|
||||
VimState::update_global(cx, |state, cx| {
|
||||
state.update_active_editor(cx, |editor, cx| {
|
||||
editor.move_cursors(cx, |map, mut cursor, _| {
|
||||
*cursor.column_mut() += 1;
|
||||
cursor = movement::find_boundary(map, cursor, |left, right| {
|
||||
let left_kind = char_kind(left).coerce_punctuation(treat_punctuation_as_word);
|
||||
let right_kind = char_kind(right).coerce_punctuation(treat_punctuation_as_word);
|
||||
|
||||
left_kind != right_kind && !left.is_whitespace()
|
||||
});
|
||||
// find_boundary clips, so if the character after the next character is a newline or at the end of the document, we know
|
||||
// we have backtraced already
|
||||
if !map
|
||||
.chars_at(cursor)
|
||||
.skip(1)
|
||||
.next()
|
||||
.map(|c| c == '\n')
|
||||
.unwrap_or(true)
|
||||
{
|
||||
*cursor.column_mut() = cursor.column().saturating_sub(1);
|
||||
}
|
||||
(map.clip_point(cursor, Bias::Left), SelectionGoal::None)
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn move_to_previous_word_start(
|
||||
_: &mut Workspace,
|
||||
&MoveToPreviousWordStart(treat_punctuation_as_word): &MoveToPreviousWordStart,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) {
|
||||
VimState::update_global(cx, |state, cx| {
|
||||
state.update_active_editor(cx, |editor, cx| {
|
||||
editor.move_cursors(cx, |map, mut cursor, _| {
|
||||
// This works even though find_preceding_boundary is called for every character in the line containing
|
||||
// cursor because the newline is checked only once.
|
||||
cursor = movement::find_preceding_boundary(map, cursor, |left, right| {
|
||||
let left_kind = char_kind(left).coerce_punctuation(treat_punctuation_as_word);
|
||||
let right_kind = char_kind(right).coerce_punctuation(treat_punctuation_as_word);
|
||||
|
||||
(left_kind != right_kind && !right.is_whitespace()) || left == '\n'
|
||||
});
|
||||
(cursor, SelectionGoal::None)
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use indoc::indoc;
|
||||
use util::test::marked_text;
|
||||
|
||||
use crate::vim_test_context::VimTestContext;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_hjkl(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true, "Test\nTestTest\nTest").await;
|
||||
cx.simulate_keystroke("l");
|
||||
cx.assert_editor_state(indoc! {"
|
||||
T|est
|
||||
TestTest
|
||||
Test"});
|
||||
cx.simulate_keystroke("h");
|
||||
cx.assert_editor_state(indoc! {"
|
||||
|Test
|
||||
TestTest
|
||||
Test"});
|
||||
cx.simulate_keystroke("j");
|
||||
cx.assert_editor_state(indoc! {"
|
||||
Test
|
||||
|TestTest
|
||||
Test"});
|
||||
cx.simulate_keystroke("k");
|
||||
cx.assert_editor_state(indoc! {"
|
||||
|Test
|
||||
TestTest
|
||||
Test"});
|
||||
cx.simulate_keystroke("j");
|
||||
cx.assert_editor_state(indoc! {"
|
||||
Test
|
||||
|TestTest
|
||||
Test"});
|
||||
|
||||
// When moving left, cursor does not wrap to the previous line
|
||||
cx.simulate_keystroke("h");
|
||||
cx.assert_editor_state(indoc! {"
|
||||
Test
|
||||
|TestTest
|
||||
Test"});
|
||||
|
||||
// When moving right, cursor does not reach the line end or wrap to the next line
|
||||
for _ in 0..9 {
|
||||
cx.simulate_keystroke("l");
|
||||
}
|
||||
cx.assert_editor_state(indoc! {"
|
||||
Test
|
||||
TestTes|t
|
||||
Test"});
|
||||
|
||||
// Goal column respects the inability to reach the end of the line
|
||||
cx.simulate_keystroke("k");
|
||||
cx.assert_editor_state(indoc! {"
|
||||
Tes|t
|
||||
TestTest
|
||||
Test"});
|
||||
cx.simulate_keystroke("j");
|
||||
cx.assert_editor_state(indoc! {"
|
||||
Test
|
||||
TestTes|t
|
||||
Test"});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_jump_to_line_boundaries(cx: &mut gpui::TestAppContext) {
|
||||
let initial_content = indoc! {"
|
||||
Test Test
|
||||
|
||||
T"};
|
||||
let mut cx = VimTestContext::new(cx, true, initial_content).await;
|
||||
|
||||
cx.simulate_keystroke("shift-$");
|
||||
cx.assert_editor_state(indoc! {"
|
||||
Test Tes|t
|
||||
|
||||
T"});
|
||||
cx.simulate_keystroke("0");
|
||||
cx.assert_editor_state(indoc! {"
|
||||
|Test Test
|
||||
|
||||
T"});
|
||||
|
||||
cx.simulate_keystroke("j");
|
||||
cx.simulate_keystroke("shift-$");
|
||||
cx.assert_editor_state(indoc! {"
|
||||
Test Test
|
||||
|
|
||||
T"});
|
||||
cx.simulate_keystroke("0");
|
||||
cx.assert_editor_state(indoc! {"
|
||||
Test Test
|
||||
|
|
||||
T"});
|
||||
|
||||
cx.simulate_keystroke("j");
|
||||
cx.simulate_keystroke("shift-$");
|
||||
cx.assert_editor_state(indoc! {"
|
||||
Test Test
|
||||
|
||||
|T"});
|
||||
cx.simulate_keystroke("0");
|
||||
cx.assert_editor_state(indoc! {"
|
||||
Test Test
|
||||
|
||||
|T"});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_jump_to_end(cx: &mut gpui::TestAppContext) {
|
||||
let initial_content = indoc! {"
|
||||
The quick
|
||||
|
||||
brown fox jumps
|
||||
over the lazy dog"};
|
||||
let mut cx = VimTestContext::new(cx, true, initial_content).await;
|
||||
|
||||
cx.simulate_keystroke("shift-G");
|
||||
cx.assert_editor_state(indoc! {"
|
||||
The quick
|
||||
|
||||
brown fox jumps
|
||||
over the lazy do|g"});
|
||||
|
||||
// Repeat the action doesn't move
|
||||
cx.simulate_keystroke("shift-G");
|
||||
cx.assert_editor_state(indoc! {"
|
||||
The quick
|
||||
|
||||
brown fox jumps
|
||||
over the lazy do|g"});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_next_word_start(cx: &mut gpui::TestAppContext) {
|
||||
let (initial_content, cursor_offsets) = marked_text(indoc! {"
|
||||
The |quick|-|brown
|
||||
|
|
||||
|
|
||||
|fox_jumps |over
|
||||
|th||e"});
|
||||
let mut cx = VimTestContext::new(cx, true, &initial_content).await;
|
||||
|
||||
for cursor_offset in cursor_offsets {
|
||||
cx.simulate_keystroke("w");
|
||||
cx.assert_newest_selection_head_offset(cursor_offset);
|
||||
}
|
||||
|
||||
// Reset and test ignoring punctuation
|
||||
cx.simulate_keystrokes(&["g", "g"]);
|
||||
let (_, cursor_offsets) = marked_text(indoc! {"
|
||||
The |quick-brown
|
||||
|
|
||||
|
|
||||
|fox_jumps |over
|
||||
|th||e"});
|
||||
|
||||
for cursor_offset in cursor_offsets {
|
||||
cx.simulate_keystroke("shift-W");
|
||||
cx.assert_newest_selection_head_offset(cursor_offset);
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_next_word_end(cx: &mut gpui::TestAppContext) {
|
||||
let (initial_content, cursor_offsets) = marked_text(indoc! {"
|
||||
Th|e quic|k|-brow|n
|
||||
|
||||
|
||||
fox_jump|s ove|r
|
||||
th|e"});
|
||||
let mut cx = VimTestContext::new(cx, true, &initial_content).await;
|
||||
|
||||
for cursor_offset in cursor_offsets {
|
||||
cx.simulate_keystroke("e");
|
||||
cx.assert_newest_selection_head_offset(cursor_offset);
|
||||
}
|
||||
|
||||
// Reset and test ignoring punctuation
|
||||
cx.simulate_keystrokes(&["g", "g"]);
|
||||
let (_, cursor_offsets) = marked_text(indoc! {"
|
||||
Th|e quick-brow|n
|
||||
|
||||
|
||||
fox_jump|s ove|r
|
||||
th||e"});
|
||||
for cursor_offset in cursor_offsets {
|
||||
cx.simulate_keystroke("shift-E");
|
||||
cx.assert_newest_selection_head_offset(cursor_offset);
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_previous_word_start(cx: &mut gpui::TestAppContext) {
|
||||
let (initial_content, cursor_offsets) = marked_text(indoc! {"
|
||||
||The |quick|-|brown
|
||||
|
|
||||
|
|
||||
|fox_jumps |over
|
||||
|the"});
|
||||
let mut cx = VimTestContext::new(cx, true, &initial_content).await;
|
||||
cx.simulate_keystroke("shift-G");
|
||||
|
||||
for cursor_offset in cursor_offsets.into_iter().rev() {
|
||||
cx.simulate_keystroke("b");
|
||||
cx.assert_newest_selection_head_offset(cursor_offset);
|
||||
}
|
||||
|
||||
// Reset and test ignoring punctuation
|
||||
cx.simulate_keystroke("shift-G");
|
||||
let (_, cursor_offsets) = marked_text(indoc! {"
|
||||
||The |quick-brown
|
||||
|
|
||||
|
|
||||
|fox_jumps |over
|
||||
|the"});
|
||||
for cursor_offset in cursor_offsets.into_iter().rev() {
|
||||
cx.simulate_keystroke("shift-B");
|
||||
cx.assert_newest_selection_head_offset(cursor_offset);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
use gpui::{action, keymap::Binding, MutableAppContext, ViewContext};
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::{mode::Mode, SwitchMode, VimState};
|
||||
|
||||
action!(MoveToStart);
|
||||
|
||||
pub fn init(cx: &mut MutableAppContext) {
|
||||
let context = Some("Editor && vim_mode == normal && vim_submode == g");
|
||||
cx.add_bindings(vec![
|
||||
Binding::new("g", MoveToStart, context),
|
||||
Binding::new("escape", SwitchMode(Mode::normal()), context),
|
||||
]);
|
||||
|
||||
cx.add_action(move_to_start);
|
||||
}
|
||||
|
||||
fn move_to_start(_: &mut Workspace, _: &MoveToStart, cx: &mut ViewContext<Workspace>) {
|
||||
VimState::update_global(cx, |state, cx| {
|
||||
state.update_active_editor(cx, |editor, cx| {
|
||||
editor.move_to_beginning(&editor::MoveToBeginning, cx);
|
||||
});
|
||||
state.switch_mode(&SwitchMode(Mode::normal()), cx);
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use indoc::indoc;
|
||||
|
||||
use crate::{
|
||||
mode::{Mode, NormalState},
|
||||
vim_test_context::VimTestContext,
|
||||
};
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_g_prefix_and_abort(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true, "").await;
|
||||
|
||||
// Can abort with escape to get back to normal mode
|
||||
cx.simulate_keystroke("g");
|
||||
assert_eq!(cx.mode(), Mode::Normal(NormalState::GPrefix));
|
||||
cx.simulate_keystroke("escape");
|
||||
assert_eq!(cx.mode(), Mode::normal());
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_move_to_start(cx: &mut gpui::TestAppContext) {
|
||||
let initial_content = indoc! {"
|
||||
The quick
|
||||
|
||||
brown fox jumps
|
||||
over the lazy dog"};
|
||||
let mut cx = VimTestContext::new(cx, true, initial_content).await;
|
||||
|
||||
// Jump to the end to
|
||||
cx.simulate_keystroke("shift-G");
|
||||
cx.assert_editor_state(indoc! {"
|
||||
The quick
|
||||
|
||||
brown fox jumps
|
||||
over the lazy do|g"});
|
||||
|
||||
// Jump to the start
|
||||
cx.simulate_keystrokes(&["g", "g"]);
|
||||
cx.assert_editor_state(indoc! {"
|
||||
|The quick
|
||||
|
||||
brown fox jumps
|
||||
over the lazy dog"});
|
||||
assert_eq!(cx.mode(), Mode::normal());
|
||||
|
||||
// Repeat action doesn't change
|
||||
cx.simulate_keystrokes(&["g", "g"]);
|
||||
cx.assert_editor_state(indoc! {"
|
||||
|The quick
|
||||
|
||||
brown fox jumps
|
||||
over the lazy dog"});
|
||||
assert_eq!(cx.mode(), Mode::normal());
|
||||
}
|
||||
}
|
||||
+43
-2
@@ -3,7 +3,7 @@ mod insert;
|
||||
mod mode;
|
||||
mod normal;
|
||||
#[cfg(test)]
|
||||
mod vim_tests;
|
||||
mod vim_test_context;
|
||||
|
||||
use collections::HashMap;
|
||||
use editor::{CursorShape, Editor};
|
||||
@@ -65,8 +65,9 @@ impl VimState {
|
||||
fn set_enabled(&mut self, enabled: bool, cx: &mut MutableAppContext) {
|
||||
if self.enabled != enabled {
|
||||
self.enabled = enabled;
|
||||
self.mode = Default::default();
|
||||
if enabled {
|
||||
self.mode = Mode::Normal;
|
||||
self.mode = Mode::normal();
|
||||
}
|
||||
self.sync_editor_options(cx);
|
||||
}
|
||||
@@ -95,3 +96,43 @@ impl VimState {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::{mode::Mode, vim_test_context::VimTestContext};
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_initially_disabled(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, false, "").await;
|
||||
cx.simulate_keystrokes(&["h", "j", "k", "l"]);
|
||||
cx.assert_editor_state("hjkl|");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_toggle_through_settings(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true, "").await;
|
||||
|
||||
cx.simulate_keystroke("i");
|
||||
assert_eq!(cx.mode(), Mode::Insert);
|
||||
|
||||
// Editor acts as though vim is disabled
|
||||
cx.disable_vim();
|
||||
cx.simulate_keystrokes(&["h", "j", "k", "l"]);
|
||||
cx.assert_editor_state("hjkl|");
|
||||
|
||||
// Enabling dynamically sets vim mode again and restores normal mode
|
||||
cx.enable_vim();
|
||||
assert_eq!(cx.mode(), Mode::normal());
|
||||
cx.simulate_keystrokes(&["h", "h", "h", "l"]);
|
||||
assert_eq!(cx.editor_text(), "hjkl".to_owned());
|
||||
cx.assert_editor_state("hj|kl");
|
||||
cx.simulate_keystrokes(&["i", "T", "e", "s", "t"]);
|
||||
cx.assert_editor_state("hjTest|kl");
|
||||
|
||||
// Disabling and enabling resets to normal mode
|
||||
assert_eq!(cx.mode(), Mode::Insert);
|
||||
cx.disable_vim();
|
||||
cx.enable_vim();
|
||||
assert_eq!(cx.mode(), Mode::normal());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
use std::ops::Deref;
|
||||
|
||||
use editor::{display_map::ToDisplayPoint, Bias, DisplayPoint};
|
||||
use gpui::{json::json, keymap::Keystroke, ViewHandle};
|
||||
use language::{Point, Selection};
|
||||
use util::test::marked_text;
|
||||
use workspace::{WorkspaceHandle, WorkspaceParams};
|
||||
|
||||
use crate::*;
|
||||
|
||||
pub struct VimTestContext<'a> {
|
||||
cx: &'a mut gpui::TestAppContext,
|
||||
window_id: usize,
|
||||
editor: ViewHandle<Editor>,
|
||||
}
|
||||
|
||||
impl<'a> VimTestContext<'a> {
|
||||
pub async fn new(
|
||||
cx: &'a mut gpui::TestAppContext,
|
||||
enabled: bool,
|
||||
initial_editor_text: &str,
|
||||
) -> VimTestContext<'a> {
|
||||
cx.update(|cx| {
|
||||
editor::init(cx);
|
||||
crate::init(cx);
|
||||
});
|
||||
let params = cx.update(WorkspaceParams::test);
|
||||
|
||||
cx.update(|cx| {
|
||||
cx.update_global(|settings: &mut Settings, _| {
|
||||
settings.vim_mode = enabled;
|
||||
});
|
||||
});
|
||||
|
||||
params
|
||||
.fs
|
||||
.as_fake()
|
||||
.insert_tree(
|
||||
"/root",
|
||||
json!({ "dir": { "test.txt": initial_editor_text } }),
|
||||
)
|
||||
.await;
|
||||
|
||||
let (window_id, workspace) = cx.add_window(|cx| Workspace::new(¶ms, cx));
|
||||
params
|
||||
.project
|
||||
.update(cx, |project, cx| {
|
||||
project.find_or_create_local_worktree("/root", true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx))
|
||||
.await;
|
||||
|
||||
let file = cx.read(|cx| workspace.file_project_paths(cx)[0].clone());
|
||||
let item = workspace
|
||||
.update(cx, |workspace, cx| workspace.open_path(file, cx))
|
||||
.await
|
||||
.expect("Could not open test file");
|
||||
|
||||
let editor = cx.update(|cx| {
|
||||
item.act_as::<Editor>(cx)
|
||||
.expect("Opened test file wasn't an editor")
|
||||
});
|
||||
editor.update(cx, |_, cx| cx.focus_self());
|
||||
|
||||
Self {
|
||||
cx,
|
||||
window_id,
|
||||
editor,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn enable_vim(&mut self) {
|
||||
self.cx.update(|cx| {
|
||||
cx.update_global(|settings: &mut Settings, _| {
|
||||
settings.vim_mode = true;
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
pub fn disable_vim(&mut self) {
|
||||
self.cx.update(|cx| {
|
||||
cx.update_global(|settings: &mut Settings, _| {
|
||||
settings.vim_mode = false;
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
pub fn newest_selection(&mut self) -> Selection<DisplayPoint> {
|
||||
self.editor.update(self.cx, |editor, cx| {
|
||||
let snapshot = editor.snapshot(cx);
|
||||
editor
|
||||
.newest_selection::<Point>(cx)
|
||||
.map(|point| point.to_display_point(&snapshot.display_snapshot))
|
||||
})
|
||||
}
|
||||
|
||||
pub fn mode(&mut self) -> Mode {
|
||||
self.cx.update(|cx| cx.global::<VimState>().mode)
|
||||
}
|
||||
|
||||
pub fn editor_text(&mut self) -> String {
|
||||
self.editor
|
||||
.update(self.cx, |editor, cx| editor.snapshot(cx).text())
|
||||
}
|
||||
|
||||
pub fn simulate_keystroke(&mut self, keystroke_text: &str) {
|
||||
let keystroke = Keystroke::parse(keystroke_text).unwrap();
|
||||
let input = if keystroke.modified() {
|
||||
None
|
||||
} else {
|
||||
Some(keystroke.key.clone())
|
||||
};
|
||||
self.cx
|
||||
.dispatch_keystroke(self.window_id, keystroke, input, false);
|
||||
}
|
||||
|
||||
pub fn simulate_keystrokes(&mut self, keystroke_texts: &[&str]) {
|
||||
for keystroke_text in keystroke_texts.into_iter() {
|
||||
self.simulate_keystroke(keystroke_text);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn assert_newest_selection_head_offset(&mut self, expected_offset: usize) {
|
||||
let actual_head = self.newest_selection().head();
|
||||
let (actual_offset, expected_head) = self.editor.update(self.cx, |editor, cx| {
|
||||
let snapshot = editor.snapshot(cx);
|
||||
(
|
||||
actual_head.to_offset(&snapshot, Bias::Left),
|
||||
expected_offset.to_display_point(&snapshot),
|
||||
)
|
||||
});
|
||||
let mut actual_position_text = self.editor_text();
|
||||
let mut expected_position_text = actual_position_text.clone();
|
||||
actual_position_text.insert(actual_offset, '|');
|
||||
expected_position_text.insert(expected_offset, '|');
|
||||
assert_eq!(
|
||||
actual_head, expected_head,
|
||||
"\nActual Position: {}\nExpected Position: {}",
|
||||
actual_position_text, expected_position_text
|
||||
)
|
||||
}
|
||||
|
||||
pub fn assert_editor_state(&mut self, text: &str) {
|
||||
let (unmarked_text, markers) = marked_text(&text);
|
||||
let editor_text = self.editor_text();
|
||||
assert_eq!(
|
||||
editor_text, unmarked_text,
|
||||
"Unmarked text doesn't match editor text"
|
||||
);
|
||||
let expected_offset = markers[0];
|
||||
let actual_head = self.newest_selection().head();
|
||||
let (actual_offset, expected_head) = self.editor.update(self.cx, |editor, cx| {
|
||||
let snapshot = editor.snapshot(cx);
|
||||
(
|
||||
actual_head.to_offset(&snapshot, Bias::Left),
|
||||
expected_offset.to_display_point(&snapshot),
|
||||
)
|
||||
});
|
||||
let mut actual_position_text = self.editor_text();
|
||||
let mut expected_position_text = actual_position_text.clone();
|
||||
actual_position_text.insert(actual_offset, '|');
|
||||
expected_position_text.insert(expected_offset, '|');
|
||||
assert_eq!(
|
||||
actual_head, expected_head,
|
||||
"\nActual Position: {}\nExpected Position: {}",
|
||||
actual_position_text, expected_position_text
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Deref for VimTestContext<'a> {
|
||||
type Target = gpui::TestAppContext;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.cx
|
||||
}
|
||||
}
|
||||
@@ -1,253 +0,0 @@
|
||||
use indoc::indoc;
|
||||
use std::ops::Deref;
|
||||
|
||||
use editor::{display_map::ToDisplayPoint, DisplayPoint};
|
||||
use gpui::{json::json, keymap::Keystroke, ViewHandle};
|
||||
use language::{Point, Selection};
|
||||
use util::test::marked_text;
|
||||
use workspace::{WorkspaceHandle, WorkspaceParams};
|
||||
|
||||
use crate::*;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_insert_mode(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestAppContext::new(cx, true, "").await;
|
||||
cx.simulate_keystroke("i");
|
||||
assert_eq!(cx.mode(), Mode::Insert);
|
||||
cx.simulate_keystrokes(&["T", "e", "s", "t"]);
|
||||
cx.assert_newest_selection_head("Test|");
|
||||
cx.simulate_keystroke("escape");
|
||||
assert_eq!(cx.mode(), Mode::Normal);
|
||||
cx.assert_newest_selection_head("Tes|t");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_normal_hjkl(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestAppContext::new(cx, true, "Test\nTestTest\nTest").await;
|
||||
cx.simulate_keystroke("l");
|
||||
cx.assert_newest_selection_head(indoc! {"
|
||||
T|est
|
||||
TestTest
|
||||
Test"});
|
||||
cx.simulate_keystroke("h");
|
||||
cx.assert_newest_selection_head(indoc! {"
|
||||
|Test
|
||||
TestTest
|
||||
Test"});
|
||||
cx.simulate_keystroke("j");
|
||||
cx.assert_newest_selection_head(indoc! {"
|
||||
Test
|
||||
|TestTest
|
||||
Test"});
|
||||
cx.simulate_keystroke("k");
|
||||
cx.assert_newest_selection_head(indoc! {"
|
||||
|Test
|
||||
TestTest
|
||||
Test"});
|
||||
cx.simulate_keystroke("j");
|
||||
cx.assert_newest_selection_head(indoc! {"
|
||||
Test
|
||||
|TestTest
|
||||
Test"});
|
||||
|
||||
// When moving left, cursor does not wrap to the previous line
|
||||
cx.simulate_keystroke("h");
|
||||
cx.assert_newest_selection_head(indoc! {"
|
||||
Test
|
||||
|TestTest
|
||||
Test"});
|
||||
|
||||
// When moving right, cursor does not reach the line end or wrap to the next line
|
||||
for _ in 0..9 {
|
||||
cx.simulate_keystroke("l");
|
||||
}
|
||||
cx.assert_newest_selection_head(indoc! {"
|
||||
Test
|
||||
TestTes|t
|
||||
Test"});
|
||||
|
||||
// Goal column respects the inability to reach the end of the line
|
||||
cx.simulate_keystroke("k");
|
||||
cx.assert_newest_selection_head(indoc! {"
|
||||
Tes|t
|
||||
TestTest
|
||||
Test"});
|
||||
cx.simulate_keystroke("j");
|
||||
cx.assert_newest_selection_head(indoc! {"
|
||||
Test
|
||||
TestTes|t
|
||||
Test"});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_toggle_through_settings(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestAppContext::new(cx, true, "").await;
|
||||
|
||||
cx.simulate_keystroke("i");
|
||||
assert_eq!(cx.mode(), Mode::Insert);
|
||||
|
||||
// Editor acts as though vim is disabled
|
||||
cx.disable_vim();
|
||||
cx.simulate_keystrokes(&["h", "j", "k", "l"]);
|
||||
cx.assert_newest_selection_head("hjkl|");
|
||||
|
||||
// Enabling dynamically sets vim mode again and restores normal mode
|
||||
cx.enable_vim();
|
||||
assert_eq!(cx.mode(), Mode::Normal);
|
||||
cx.simulate_keystrokes(&["h", "h", "h", "l"]);
|
||||
assert_eq!(cx.editor_text(), "hjkl".to_owned());
|
||||
cx.assert_newest_selection_head("hj|kl");
|
||||
cx.simulate_keystrokes(&["i", "T", "e", "s", "t"]);
|
||||
cx.assert_newest_selection_head("hjTest|kl");
|
||||
|
||||
// Disabling and enabling resets to normal mode
|
||||
assert_eq!(cx.mode(), Mode::Insert);
|
||||
cx.disable_vim();
|
||||
assert_eq!(cx.mode(), Mode::Insert);
|
||||
cx.enable_vim();
|
||||
assert_eq!(cx.mode(), Mode::Normal);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_initially_disabled(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestAppContext::new(cx, false, "").await;
|
||||
cx.simulate_keystrokes(&["h", "j", "k", "l"]);
|
||||
cx.assert_newest_selection_head("hjkl|");
|
||||
}
|
||||
|
||||
struct VimTestAppContext<'a> {
|
||||
cx: &'a mut gpui::TestAppContext,
|
||||
window_id: usize,
|
||||
editor: ViewHandle<Editor>,
|
||||
}
|
||||
|
||||
impl<'a> VimTestAppContext<'a> {
|
||||
async fn new(
|
||||
cx: &'a mut gpui::TestAppContext,
|
||||
enabled: bool,
|
||||
initial_editor_text: &str,
|
||||
) -> VimTestAppContext<'a> {
|
||||
cx.update(|cx| {
|
||||
editor::init(cx);
|
||||
crate::init(cx);
|
||||
});
|
||||
let params = cx.update(WorkspaceParams::test);
|
||||
|
||||
cx.update(|cx| {
|
||||
cx.update_global(|settings: &mut Settings, _| {
|
||||
settings.vim_mode = enabled;
|
||||
});
|
||||
});
|
||||
|
||||
params
|
||||
.fs
|
||||
.as_fake()
|
||||
.insert_tree(
|
||||
"/root",
|
||||
json!({ "dir": { "test.txt": initial_editor_text } }),
|
||||
)
|
||||
.await;
|
||||
|
||||
let (window_id, workspace) = cx.add_window(|cx| Workspace::new(¶ms, cx));
|
||||
params
|
||||
.project
|
||||
.update(cx, |project, cx| {
|
||||
project.find_or_create_local_worktree("/root", true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx))
|
||||
.await;
|
||||
|
||||
let file = cx.read(|cx| workspace.file_project_paths(cx)[0].clone());
|
||||
let item = workspace
|
||||
.update(cx, |workspace, cx| workspace.open_path(file, cx))
|
||||
.await
|
||||
.expect("Could not open test file");
|
||||
|
||||
let editor = cx.update(|cx| {
|
||||
item.act_as::<Editor>(cx)
|
||||
.expect("Opened test file wasn't an editor")
|
||||
});
|
||||
editor.update(cx, |_, cx| cx.focus_self());
|
||||
|
||||
Self {
|
||||
cx,
|
||||
window_id,
|
||||
editor,
|
||||
}
|
||||
}
|
||||
|
||||
fn enable_vim(&mut self) {
|
||||
self.cx.update(|cx| {
|
||||
cx.update_global(|settings: &mut Settings, _| {
|
||||
settings.vim_mode = true;
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
fn disable_vim(&mut self) {
|
||||
self.cx.update(|cx| {
|
||||
cx.update_global(|settings: &mut Settings, _| {
|
||||
settings.vim_mode = false;
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
fn newest_selection(&mut self) -> Selection<DisplayPoint> {
|
||||
self.editor.update(self.cx, |editor, cx| {
|
||||
let snapshot = editor.snapshot(cx);
|
||||
editor
|
||||
.newest_selection::<Point>(cx)
|
||||
.map(|point| point.to_display_point(&snapshot.display_snapshot))
|
||||
})
|
||||
}
|
||||
|
||||
fn mode(&mut self) -> Mode {
|
||||
self.cx.update(|cx| cx.global::<VimState>().mode)
|
||||
}
|
||||
|
||||
fn editor_text(&mut self) -> String {
|
||||
self.editor
|
||||
.update(self.cx, |editor, cx| editor.snapshot(cx).text())
|
||||
}
|
||||
|
||||
fn simulate_keystroke(&mut self, keystroke_text: &str) {
|
||||
let keystroke = Keystroke::parse(keystroke_text).unwrap();
|
||||
let input = if keystroke.modified() {
|
||||
None
|
||||
} else {
|
||||
Some(keystroke.key.clone())
|
||||
};
|
||||
self.cx
|
||||
.dispatch_keystroke(self.window_id, keystroke, input, false);
|
||||
}
|
||||
|
||||
fn simulate_keystrokes(&mut self, keystroke_texts: &[&str]) {
|
||||
for keystroke_text in keystroke_texts.into_iter() {
|
||||
self.simulate_keystroke(keystroke_text);
|
||||
}
|
||||
}
|
||||
|
||||
fn assert_newest_selection_head(&mut self, text: &str) {
|
||||
let (unmarked_text, markers) = marked_text(&text);
|
||||
assert_eq!(
|
||||
self.editor_text(),
|
||||
unmarked_text,
|
||||
"Unmarked text doesn't match editor text"
|
||||
);
|
||||
let newest_selection = self.newest_selection();
|
||||
let expected_head = self.editor.update(self.cx, |editor, cx| {
|
||||
markers[0].to_display_point(&editor.snapshot(cx))
|
||||
});
|
||||
assert_eq!(newest_selection.head(), expected_head)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Deref for VimTestAppContext<'a> {
|
||||
type Target = gpui::TestAppContext;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.cx
|
||||
}
|
||||
}
|
||||
+371
-49
@@ -1,17 +1,18 @@
|
||||
use super::{ItemHandle, SplitDirection};
|
||||
use crate::{toolbar::Toolbar, Item, Settings, WeakItemHandle, Workspace};
|
||||
use collections::{HashMap, VecDeque};
|
||||
use futures::StreamExt;
|
||||
use gpui::{
|
||||
action,
|
||||
elements::*,
|
||||
geometry::{rect::RectF, vector::vec2f},
|
||||
keymap::Binding,
|
||||
platform::{CursorStyle, NavigationDirection},
|
||||
AppContext, Entity, MutableAppContext, Quad, RenderContext, Task, View, ViewContext,
|
||||
ViewHandle, WeakViewHandle,
|
||||
AppContext, Entity, ModelHandle, MutableAppContext, PromptLevel, Quad, RenderContext, Task,
|
||||
View, ViewContext, ViewHandle, WeakViewHandle,
|
||||
};
|
||||
use project::{ProjectEntryId, ProjectPath};
|
||||
use std::{any::Any, cell::RefCell, cmp, mem, rc::Rc};
|
||||
use project::{Project, ProjectEntryId, ProjectPath};
|
||||
use std::{any::Any, cell::RefCell, cmp, mem, path::Path, rc::Rc};
|
||||
use util::ResultExt;
|
||||
|
||||
action!(Split, SplitDirection);
|
||||
@@ -37,13 +38,13 @@ pub fn init(cx: &mut MutableAppContext) {
|
||||
pane.activate_next_item(cx);
|
||||
});
|
||||
cx.add_action(|pane: &mut Pane, _: &CloseActiveItem, cx| {
|
||||
pane.close_active_item(cx);
|
||||
pane.close_active_item(cx).detach();
|
||||
});
|
||||
cx.add_action(|pane: &mut Pane, _: &CloseInactiveItems, cx| {
|
||||
pane.close_inactive_items(cx);
|
||||
pane.close_inactive_items(cx).detach();
|
||||
});
|
||||
cx.add_action(|pane: &mut Pane, action: &CloseItem, cx| {
|
||||
pane.close_item(action.0, cx);
|
||||
pane.close_item(action.0, cx).detach();
|
||||
});
|
||||
cx.add_action(|pane: &mut Pane, action: &Split, cx| {
|
||||
pane.split(action.0, cx);
|
||||
@@ -97,6 +98,7 @@ pub struct Pane {
|
||||
active_item_index: usize,
|
||||
nav_history: Rc<RefCell<NavHistory>>,
|
||||
toolbar: ViewHandle<Toolbar>,
|
||||
project: ModelHandle<Project>,
|
||||
}
|
||||
|
||||
pub struct ItemNavHistory {
|
||||
@@ -132,12 +134,13 @@ pub struct NavigationEntry {
|
||||
}
|
||||
|
||||
impl Pane {
|
||||
pub fn new(cx: &mut ViewContext<Self>) -> Self {
|
||||
pub fn new(project: ModelHandle<Project>, cx: &mut ViewContext<Self>) -> Self {
|
||||
Self {
|
||||
items: Vec::new(),
|
||||
active_item_index: 0,
|
||||
nav_history: Default::default(),
|
||||
toolbar: cx.add_view(|_| Toolbar::new()),
|
||||
project,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,13 +207,16 @@ impl Pane {
|
||||
|
||||
let prev_active_index = mem::replace(&mut pane.active_item_index, index);
|
||||
pane.focus_active_item(cx);
|
||||
pane.update_toolbar(cx);
|
||||
cx.emit(Event::ActivateItem { local: true });
|
||||
cx.notify();
|
||||
|
||||
let mut navigated = prev_active_index != pane.active_item_index;
|
||||
if let Some(data) = entry.data {
|
||||
navigated |= pane.active_item()?.navigate(data, cx);
|
||||
}
|
||||
|
||||
if navigated {
|
||||
cx.notify();
|
||||
break None;
|
||||
}
|
||||
}
|
||||
@@ -403,65 +409,173 @@ impl Pane {
|
||||
self.activate_item(index, true, cx);
|
||||
}
|
||||
|
||||
pub fn close_active_item(&mut self, cx: &mut ViewContext<Self>) {
|
||||
if !self.items.is_empty() {
|
||||
pub fn close_active_item(&mut self, cx: &mut ViewContext<Self>) -> Task<()> {
|
||||
if self.items.is_empty() {
|
||||
Task::ready(())
|
||||
} else {
|
||||
self.close_item(self.items[self.active_item_index].id(), cx)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn close_inactive_items(&mut self, cx: &mut ViewContext<Self>) {
|
||||
if !self.items.is_empty() {
|
||||
pub fn close_inactive_items(&mut self, cx: &mut ViewContext<Self>) -> Task<()> {
|
||||
if self.items.is_empty() {
|
||||
Task::ready(())
|
||||
} else {
|
||||
let active_item_id = self.items[self.active_item_index].id();
|
||||
self.close_items(cx, |id| id != active_item_id);
|
||||
self.close_items(cx, move |id| id != active_item_id)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn close_item(&mut self, view_id_to_close: usize, cx: &mut ViewContext<Self>) {
|
||||
self.close_items(cx, |view_id| view_id == view_id_to_close);
|
||||
pub fn close_item(&mut self, view_id_to_close: usize, cx: &mut ViewContext<Self>) -> Task<()> {
|
||||
self.close_items(cx, move |view_id| view_id == view_id_to_close)
|
||||
}
|
||||
|
||||
pub fn close_items(
|
||||
&mut self,
|
||||
cx: &mut ViewContext<Self>,
|
||||
should_close: impl Fn(usize) -> bool,
|
||||
) {
|
||||
let mut item_ix = 0;
|
||||
let mut new_active_item_index = self.active_item_index;
|
||||
self.items.retain(|item| {
|
||||
if should_close(item.id()) {
|
||||
if item_ix == self.active_item_index {
|
||||
item.deactivated(cx);
|
||||
should_close: impl 'static + Fn(usize) -> bool,
|
||||
) -> Task<()> {
|
||||
const CONFLICT_MESSAGE: &'static str = "This file has changed on disk since you started editing it. Do you want to overwrite it?";
|
||||
const DIRTY_MESSAGE: &'static str =
|
||||
"This file contains unsaved edits. Do you want to save it?";
|
||||
|
||||
let project = self.project.clone();
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
while let Some(item_to_close_ix) = this.read_with(&cx, |this, _| {
|
||||
this.items.iter().position(|item| should_close(item.id()))
|
||||
}) {
|
||||
let item =
|
||||
this.read_with(&cx, |this, _| this.items[item_to_close_ix].boxed_clone());
|
||||
if cx.read(|cx| item.is_dirty(cx)) {
|
||||
if cx.read(|cx| item.can_save(cx)) {
|
||||
let mut answer = this.update(&mut cx, |this, cx| {
|
||||
this.activate_item(item_to_close_ix, true, cx);
|
||||
cx.prompt(
|
||||
PromptLevel::Warning,
|
||||
DIRTY_MESSAGE,
|
||||
&["Save", "Don't Save", "Cancel"],
|
||||
)
|
||||
});
|
||||
|
||||
match answer.next().await {
|
||||
Some(0) => {
|
||||
if cx
|
||||
.update(|cx| item.save(project.clone(), cx))
|
||||
.await
|
||||
.log_err()
|
||||
.is_none()
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
Some(1) => {}
|
||||
_ => break,
|
||||
}
|
||||
} else if cx.read(|cx| item.can_save_as(cx)) {
|
||||
let mut answer = this.update(&mut cx, |this, cx| {
|
||||
this.activate_item(item_to_close_ix, true, cx);
|
||||
cx.prompt(
|
||||
PromptLevel::Warning,
|
||||
DIRTY_MESSAGE,
|
||||
&["Save", "Don't Save", "Cancel"],
|
||||
)
|
||||
});
|
||||
|
||||
match answer.next().await {
|
||||
Some(0) => {
|
||||
let start_abs_path = project
|
||||
.read_with(&cx, |project, cx| {
|
||||
let worktree = project.visible_worktrees(cx).next()?;
|
||||
Some(worktree.read(cx).as_local()?.abs_path().to_path_buf())
|
||||
})
|
||||
.unwrap_or(Path::new("").into());
|
||||
|
||||
let mut abs_path =
|
||||
cx.update(|cx| cx.prompt_for_new_path(&start_abs_path));
|
||||
if let Some(abs_path) = abs_path.next().await.flatten() {
|
||||
if cx
|
||||
.update(|cx| item.save_as(project.clone(), abs_path, cx))
|
||||
.await
|
||||
.log_err()
|
||||
.is_none()
|
||||
{
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Some(1) => {}
|
||||
_ => break,
|
||||
}
|
||||
}
|
||||
} else if cx.read(|cx| item.has_conflict(cx) && item.can_save(cx)) {
|
||||
let mut answer = this.update(&mut cx, |this, cx| {
|
||||
this.activate_item(item_to_close_ix, true, cx);
|
||||
cx.prompt(
|
||||
PromptLevel::Warning,
|
||||
CONFLICT_MESSAGE,
|
||||
&["Overwrite", "Discard", "Cancel"],
|
||||
)
|
||||
});
|
||||
|
||||
match answer.next().await {
|
||||
Some(0) => {
|
||||
if cx
|
||||
.update(|cx| item.save(project.clone(), cx))
|
||||
.await
|
||||
.log_err()
|
||||
.is_none()
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
Some(1) => {
|
||||
if cx
|
||||
.update(|cx| item.reload(project.clone(), cx))
|
||||
.await
|
||||
.log_err()
|
||||
.is_none()
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
_ => break,
|
||||
}
|
||||
}
|
||||
|
||||
if item_ix < self.active_item_index {
|
||||
new_active_item_index -= 1;
|
||||
}
|
||||
this.update(&mut cx, |this, cx| {
|
||||
if let Some(item_ix) = this.items.iter().position(|i| i.id() == item.id()) {
|
||||
if item_ix == this.active_item_index {
|
||||
if item_ix + 1 < this.items.len() {
|
||||
this.activate_next_item(cx);
|
||||
} else if item_ix > 0 {
|
||||
this.activate_prev_item(cx);
|
||||
}
|
||||
}
|
||||
|
||||
let mut nav_history = self.nav_history.borrow_mut();
|
||||
if let Some(path) = item.project_path(cx) {
|
||||
nav_history.paths_by_item.insert(item.id(), path);
|
||||
} else {
|
||||
nav_history.paths_by_item.remove(&item.id());
|
||||
}
|
||||
let item = this.items.remove(item_ix);
|
||||
if this.items.is_empty() {
|
||||
item.deactivated(cx);
|
||||
cx.emit(Event::Remove);
|
||||
}
|
||||
|
||||
item_ix += 1;
|
||||
false
|
||||
} else {
|
||||
item_ix += 1;
|
||||
true
|
||||
if item_ix < this.active_item_index {
|
||||
this.active_item_index -= 1;
|
||||
}
|
||||
|
||||
let mut nav_history = this.nav_history.borrow_mut();
|
||||
if let Some(path) = item.project_path(cx) {
|
||||
nav_history.paths_by_item.insert(item.id(), path);
|
||||
} else {
|
||||
nav_history.paths_by_item.remove(&item.id());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if self.items.is_empty() {
|
||||
cx.emit(Event::Remove);
|
||||
} else {
|
||||
self.active_item_index = cmp::min(new_active_item_index, self.items.len() - 1);
|
||||
self.focus_active_item(cx);
|
||||
self.activate(cx);
|
||||
}
|
||||
self.update_toolbar(cx);
|
||||
|
||||
cx.notify();
|
||||
this.update(&mut cx, |_, cx| cx.notify());
|
||||
})
|
||||
}
|
||||
|
||||
pub fn focus_active_item(&mut self, cx: &mut ViewContext<Self>) {
|
||||
@@ -743,3 +857,211 @@ impl NavHistory {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::WorkspaceParams;
|
||||
|
||||
use super::*;
|
||||
use gpui::TestAppContext;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_close_items(cx: &mut TestAppContext) {
|
||||
cx.foreground().forbid_parking();
|
||||
|
||||
let params = cx.update(WorkspaceParams::test);
|
||||
let (window_id, workspace) = cx.add_window(|cx| Workspace::new(¶ms, cx));
|
||||
let item1 = cx.add_view(window_id, |_| {
|
||||
let mut item = TestItem::new();
|
||||
item.has_conflict = true;
|
||||
item
|
||||
});
|
||||
let item2 = cx.add_view(window_id, |_| {
|
||||
let mut item = TestItem::new();
|
||||
item.is_dirty = true;
|
||||
item.has_conflict = true;
|
||||
item
|
||||
});
|
||||
let item3 = cx.add_view(window_id, |_| {
|
||||
let mut item = TestItem::new();
|
||||
item.has_conflict = true;
|
||||
item
|
||||
});
|
||||
let item4 = cx.add_view(window_id, |_| {
|
||||
let mut item = TestItem::new();
|
||||
item.is_dirty = true;
|
||||
item
|
||||
});
|
||||
let item5 = cx.add_view(window_id, |_| {
|
||||
let mut item = TestItem::new();
|
||||
item.is_dirty = true;
|
||||
item.can_save = false;
|
||||
item
|
||||
});
|
||||
let pane = workspace.update(cx, |workspace, cx| {
|
||||
workspace.add_item(Box::new(item1.clone()), cx);
|
||||
workspace.add_item(Box::new(item2.clone()), cx);
|
||||
workspace.add_item(Box::new(item3.clone()), cx);
|
||||
workspace.add_item(Box::new(item4.clone()), cx);
|
||||
workspace.add_item(Box::new(item5.clone()), cx);
|
||||
workspace.active_pane().clone()
|
||||
});
|
||||
|
||||
let close_items = pane.update(cx, |pane, cx| {
|
||||
pane.activate_item(1, true, cx);
|
||||
assert_eq!(pane.active_item().unwrap().id(), item2.id());
|
||||
|
||||
let item1_id = item1.id();
|
||||
let item3_id = item3.id();
|
||||
let item4_id = item4.id();
|
||||
let item5_id = item5.id();
|
||||
pane.close_items(cx, move |id| {
|
||||
[item1_id, item3_id, item4_id, item5_id].contains(&id)
|
||||
})
|
||||
});
|
||||
|
||||
cx.foreground().run_until_parked();
|
||||
pane.read_with(cx, |pane, _| {
|
||||
assert_eq!(pane.items.len(), 5);
|
||||
assert_eq!(pane.active_item().unwrap().id(), item1.id());
|
||||
});
|
||||
|
||||
cx.simulate_prompt_answer(window_id, 0);
|
||||
cx.foreground().run_until_parked();
|
||||
pane.read_with(cx, |pane, cx| {
|
||||
assert_eq!(item1.read(cx).save_count, 1);
|
||||
assert_eq!(item1.read(cx).save_as_count, 0);
|
||||
assert_eq!(item1.read(cx).reload_count, 0);
|
||||
assert_eq!(pane.items.len(), 4);
|
||||
assert_eq!(pane.active_item().unwrap().id(), item3.id());
|
||||
});
|
||||
|
||||
cx.simulate_prompt_answer(window_id, 1);
|
||||
cx.foreground().run_until_parked();
|
||||
pane.read_with(cx, |pane, cx| {
|
||||
assert_eq!(item3.read(cx).save_count, 0);
|
||||
assert_eq!(item3.read(cx).save_as_count, 0);
|
||||
assert_eq!(item3.read(cx).reload_count, 1);
|
||||
assert_eq!(pane.items.len(), 3);
|
||||
assert_eq!(pane.active_item().unwrap().id(), item4.id());
|
||||
});
|
||||
|
||||
cx.simulate_prompt_answer(window_id, 0);
|
||||
cx.foreground().run_until_parked();
|
||||
pane.read_with(cx, |pane, cx| {
|
||||
assert_eq!(item4.read(cx).save_count, 1);
|
||||
assert_eq!(item4.read(cx).save_as_count, 0);
|
||||
assert_eq!(item4.read(cx).reload_count, 0);
|
||||
assert_eq!(pane.items.len(), 2);
|
||||
assert_eq!(pane.active_item().unwrap().id(), item5.id());
|
||||
});
|
||||
|
||||
cx.simulate_prompt_answer(window_id, 0);
|
||||
cx.foreground().run_until_parked();
|
||||
cx.simulate_new_path_selection(|_| Some(Default::default()));
|
||||
close_items.await;
|
||||
pane.read_with(cx, |pane, cx| {
|
||||
assert_eq!(item5.read(cx).save_count, 0);
|
||||
assert_eq!(item5.read(cx).save_as_count, 1);
|
||||
assert_eq!(item5.read(cx).reload_count, 0);
|
||||
assert_eq!(pane.items.len(), 1);
|
||||
assert_eq!(pane.active_item().unwrap().id(), item2.id());
|
||||
});
|
||||
}
|
||||
|
||||
struct TestItem {
|
||||
save_count: usize,
|
||||
save_as_count: usize,
|
||||
reload_count: usize,
|
||||
is_dirty: bool,
|
||||
has_conflict: bool,
|
||||
can_save: bool,
|
||||
}
|
||||
|
||||
impl TestItem {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
save_count: 0,
|
||||
save_as_count: 0,
|
||||
reload_count: 0,
|
||||
is_dirty: false,
|
||||
has_conflict: false,
|
||||
can_save: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for TestItem {
|
||||
type Event = ();
|
||||
}
|
||||
|
||||
impl View for TestItem {
|
||||
fn ui_name() -> &'static str {
|
||||
"TestItem"
|
||||
}
|
||||
|
||||
fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
|
||||
Empty::new().boxed()
|
||||
}
|
||||
}
|
||||
|
||||
impl Item for TestItem {
|
||||
fn tab_content(&self, _: &theme::Tab, _: &AppContext) -> ElementBox {
|
||||
Empty::new().boxed()
|
||||
}
|
||||
|
||||
fn project_path(&self, _: &AppContext) -> Option<ProjectPath> {
|
||||
None
|
||||
}
|
||||
|
||||
fn project_entry_id(&self, _: &AppContext) -> Option<ProjectEntryId> {
|
||||
None
|
||||
}
|
||||
|
||||
fn set_nav_history(&mut self, _: ItemNavHistory, _: &mut ViewContext<Self>) {}
|
||||
|
||||
fn is_dirty(&self, _: &AppContext) -> bool {
|
||||
self.is_dirty
|
||||
}
|
||||
|
||||
fn has_conflict(&self, _: &AppContext) -> bool {
|
||||
self.has_conflict
|
||||
}
|
||||
|
||||
fn can_save(&self, _: &AppContext) -> bool {
|
||||
self.can_save
|
||||
}
|
||||
|
||||
fn save(
|
||||
&mut self,
|
||||
_: ModelHandle<Project>,
|
||||
_: &mut ViewContext<Self>,
|
||||
) -> Task<anyhow::Result<()>> {
|
||||
self.save_count += 1;
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
|
||||
fn can_save_as(&self, _: &AppContext) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn save_as(
|
||||
&mut self,
|
||||
_: ModelHandle<Project>,
|
||||
_: std::path::PathBuf,
|
||||
_: &mut ViewContext<Self>,
|
||||
) -> Task<anyhow::Result<()>> {
|
||||
self.save_as_count += 1;
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
|
||||
fn reload(
|
||||
&mut self,
|
||||
_: ModelHandle<Project>,
|
||||
_: &mut ViewContext<Self>,
|
||||
) -> Task<anyhow::Result<()>> {
|
||||
self.reload_count += 1;
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -237,6 +237,11 @@ pub trait Item: View {
|
||||
abs_path: PathBuf,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Task<Result<()>>;
|
||||
fn reload(
|
||||
&mut self,
|
||||
project: ModelHandle<Project>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Task<Result<()>>;
|
||||
fn should_activate_item_on_event(_: &Self::Event) -> bool {
|
||||
false
|
||||
}
|
||||
@@ -380,6 +385,8 @@ pub trait ItemHandle: 'static + fmt::Debug {
|
||||
abs_path: PathBuf,
|
||||
cx: &mut MutableAppContext,
|
||||
) -> Task<Result<()>>;
|
||||
fn reload(&self, project: ModelHandle<Project>, cx: &mut MutableAppContext)
|
||||
-> Task<Result<()>>;
|
||||
fn act_as_type(&self, type_id: TypeId, cx: &AppContext) -> Option<AnyViewHandle>;
|
||||
fn to_followable_item_handle(&self, cx: &AppContext) -> Option<Box<dyn FollowableItemHandle>>;
|
||||
}
|
||||
@@ -490,7 +497,8 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
|
||||
}
|
||||
|
||||
if T::should_close_item_on_event(event) {
|
||||
pane.update(cx, |pane, cx| pane.close_item(item.id(), cx));
|
||||
pane.update(cx, |pane, cx| pane.close_item(item.id(), cx))
|
||||
.detach();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -531,6 +539,14 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
|
||||
self.update(cx, |item, cx| item.save_as(project, abs_path, cx))
|
||||
}
|
||||
|
||||
fn reload(
|
||||
&self,
|
||||
project: ModelHandle<Project>,
|
||||
cx: &mut MutableAppContext,
|
||||
) -> Task<Result<()>> {
|
||||
self.update(cx, |item, cx| item.reload(project, cx))
|
||||
}
|
||||
|
||||
fn is_dirty(&self, cx: &AppContext) -> bool {
|
||||
self.read(cx).is_dirty(cx)
|
||||
}
|
||||
@@ -722,7 +738,7 @@ impl Workspace {
|
||||
})
|
||||
.detach();
|
||||
|
||||
let pane = cx.add_view(|cx| Pane::new(cx));
|
||||
let pane = cx.add_view(|cx| Pane::new(params.project.clone(), cx));
|
||||
let pane_id = pane.id();
|
||||
cx.observe(&pane, move |me, _, cx| {
|
||||
let active_entry = me.active_project_path(cx);
|
||||
@@ -1054,7 +1070,7 @@ impl Workspace {
|
||||
}
|
||||
|
||||
fn add_pane(&mut self, cx: &mut ViewContext<Self>) -> ViewHandle<Pane> {
|
||||
let pane = cx.add_view(|cx| Pane::new(cx));
|
||||
let pane = cx.add_view(|cx| Pane::new(self.project.clone(), cx));
|
||||
let pane_id = pane.id();
|
||||
cx.observe(&pane, move |me, _, cx| {
|
||||
let active_entry = me.active_project_path(cx);
|
||||
|
||||
@@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathansobo@gmail.com>"]
|
||||
description = "The fast, collaborative code editor."
|
||||
edition = "2021"
|
||||
name = "zed"
|
||||
version = "0.23.0"
|
||||
version = "0.24.0"
|
||||
|
||||
[lib]
|
||||
name = "zed"
|
||||
@@ -99,6 +99,7 @@ tree-sitter-c = "0.20.1"
|
||||
tree-sitter-json = "0.19.0"
|
||||
tree-sitter-rust = "0.20.1"
|
||||
tree-sitter-markdown = { git = "https://github.com/MDeiml/tree-sitter-markdown", rev = "330ecab87a3e3a7211ac69bbadc19eabecdb1cca" }
|
||||
tree-sitter-typescript = "0.20.1"
|
||||
url = "2.2"
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
use gpui::Task;
|
||||
pub use language::*;
|
||||
use rust_embed::RustEmbed;
|
||||
use std::{borrow::Cow, str, sync::Arc};
|
||||
|
||||
mod c;
|
||||
mod installation;
|
||||
mod json;
|
||||
mod rust;
|
||||
mod typescript;
|
||||
|
||||
#[derive(RustEmbed)]
|
||||
#[folder = "src/languages"]
|
||||
#[exclude = "*.rs"]
|
||||
struct LanguageDir;
|
||||
|
||||
pub fn build_language_registry(login_shell_env_loaded: Task<()>) -> LanguageRegistry {
|
||||
let languages = LanguageRegistry::new(login_shell_env_loaded);
|
||||
for (name, grammar, lsp_adapter) in [
|
||||
(
|
||||
"c",
|
||||
tree_sitter_c::language(),
|
||||
Some(Arc::new(c::CLspAdapter) as Arc<dyn LspAdapter>),
|
||||
),
|
||||
(
|
||||
"json",
|
||||
tree_sitter_json::language(),
|
||||
Some(Arc::new(json::JsonLspAdapter)),
|
||||
),
|
||||
(
|
||||
"markdown",
|
||||
tree_sitter_markdown::language(),
|
||||
None, //
|
||||
),
|
||||
(
|
||||
"rust",
|
||||
tree_sitter_rust::language(),
|
||||
Some(Arc::new(rust::RustLspAdapter)),
|
||||
),
|
||||
(
|
||||
"tsx",
|
||||
tree_sitter_typescript::language_tsx(),
|
||||
Some(Arc::new(typescript::TypeScriptLspAdapter)),
|
||||
),
|
||||
(
|
||||
"typescript",
|
||||
tree_sitter_typescript::language_typescript(),
|
||||
Some(Arc::new(typescript::TypeScriptLspAdapter)),
|
||||
),
|
||||
] {
|
||||
languages.add(Arc::new(language(name, grammar, lsp_adapter)));
|
||||
}
|
||||
languages
|
||||
}
|
||||
|
||||
fn language(
|
||||
name: &str,
|
||||
grammar: tree_sitter::Language,
|
||||
lsp_adapter: Option<Arc<dyn LspAdapter>>,
|
||||
) -> Language {
|
||||
let config = toml::from_slice(
|
||||
&LanguageDir::get(&format!("{}/config.toml", name))
|
||||
.unwrap()
|
||||
.data,
|
||||
)
|
||||
.unwrap();
|
||||
let mut language = Language::new(config, Some(grammar));
|
||||
|
||||
if let Some(query) = load_query(name, "/highlights") {
|
||||
language = language
|
||||
.with_highlights_query(query.as_ref())
|
||||
.expect("failed to evaluate highlights query");
|
||||
}
|
||||
if let Some(query) = load_query(name, "/brackets") {
|
||||
language = language
|
||||
.with_brackets_query(query.as_ref())
|
||||
.expect("failed to load brackets query");
|
||||
}
|
||||
if let Some(query) = load_query(name, "/indents") {
|
||||
language = language
|
||||
.with_indents_query(query.as_ref())
|
||||
.expect("failed to load indents query");
|
||||
}
|
||||
if let Some(query) = load_query(name, "/outline") {
|
||||
language = language
|
||||
.with_outline_query(query.as_ref())
|
||||
.expect("failed to load outline query");
|
||||
}
|
||||
if let Some(lsp_adapter) = lsp_adapter {
|
||||
language = language.with_lsp_adapter(lsp_adapter)
|
||||
}
|
||||
language
|
||||
}
|
||||
|
||||
fn load_query(name: &str, filename_prefix: &str) -> Option<Cow<'static, str>> {
|
||||
let mut result = None;
|
||||
for path in LanguageDir::iter() {
|
||||
if let Some(remainder) = path.strip_prefix(name) {
|
||||
if remainder.starts_with(filename_prefix) {
|
||||
let contents = match LanguageDir::get(path.as_ref()).unwrap().data {
|
||||
Cow::Borrowed(s) => Cow::Borrowed(str::from_utf8(s).unwrap()),
|
||||
Cow::Owned(s) => Cow::Owned(String::from_utf8(s).unwrap()),
|
||||
};
|
||||
match &mut result {
|
||||
None => result = Some(contents),
|
||||
Some(r) => r.to_mut().push_str(contents.as_ref()),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
use super::installation::{latest_github_release, GitHubLspBinaryVersion};
|
||||
use anyhow::{anyhow, Result};
|
||||
use client::http::{HttpClient, Method};
|
||||
use futures::{future::BoxFuture, FutureExt, StreamExt};
|
||||
pub use language::*;
|
||||
use smol::fs::{self, File};
|
||||
use std::{any::Any, path::PathBuf, sync::Arc};
|
||||
use util::{ResultExt, TryFutureExt};
|
||||
|
||||
pub struct CLspAdapter;
|
||||
|
||||
impl super::LspAdapter for CLspAdapter {
|
||||
fn name(&self) -> LanguageServerName {
|
||||
LanguageServerName("clangd".into())
|
||||
}
|
||||
|
||||
fn fetch_latest_server_version(
|
||||
&self,
|
||||
http: Arc<dyn HttpClient>,
|
||||
) -> BoxFuture<'static, Result<Box<dyn 'static + Send + Any>>> {
|
||||
async move {
|
||||
let version = latest_github_release("clangd/clangd", http, |release_name| {
|
||||
format!("clangd-mac-{release_name}.zip")
|
||||
})
|
||||
.await?;
|
||||
Ok(Box::new(version) as Box<_>)
|
||||
}
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn fetch_server_binary(
|
||||
&self,
|
||||
version: Box<dyn 'static + Send + Any>,
|
||||
http: Arc<dyn HttpClient>,
|
||||
container_dir: PathBuf,
|
||||
) -> BoxFuture<'static, Result<PathBuf>> {
|
||||
let version = version.downcast::<GitHubLspBinaryVersion>().unwrap();
|
||||
async move {
|
||||
let zip_path = container_dir.join(format!("clangd_{}.zip", version.name));
|
||||
let version_dir = container_dir.join(format!("clangd_{}", version.name));
|
||||
let binary_path = version_dir.join("bin/clangd");
|
||||
|
||||
if fs::metadata(&binary_path).await.is_err() {
|
||||
let response = http
|
||||
.send(
|
||||
surf::RequestBuilder::new(Method::Get, version.url)
|
||||
.middleware(surf::middleware::Redirect::default())
|
||||
.build(),
|
||||
)
|
||||
.await
|
||||
.map_err(|err| anyhow!("error downloading release: {}", err))?;
|
||||
let mut file = File::create(&zip_path).await?;
|
||||
if !response.status().is_success() {
|
||||
Err(anyhow!(
|
||||
"download failed with status {}",
|
||||
response.status().to_string()
|
||||
))?;
|
||||
}
|
||||
futures::io::copy(response, &mut file).await?;
|
||||
|
||||
let unzip_status = smol::process::Command::new("unzip")
|
||||
.current_dir(&container_dir)
|
||||
.arg(&zip_path)
|
||||
.output()
|
||||
.await?
|
||||
.status;
|
||||
if !unzip_status.success() {
|
||||
Err(anyhow!("failed to unzip clangd archive"))?;
|
||||
}
|
||||
|
||||
if let Some(mut entries) = fs::read_dir(&container_dir).await.log_err() {
|
||||
while let Some(entry) = entries.next().await {
|
||||
if let Some(entry) = entry.log_err() {
|
||||
let entry_path = entry.path();
|
||||
if entry_path.as_path() != version_dir {
|
||||
fs::remove_dir_all(&entry_path).await.log_err();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(binary_path)
|
||||
}
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn cached_server_binary(&self, container_dir: PathBuf) -> BoxFuture<'static, Option<PathBuf>> {
|
||||
async move {
|
||||
let mut last_clangd_dir = None;
|
||||
let mut entries = fs::read_dir(&container_dir).await?;
|
||||
while let Some(entry) = entries.next().await {
|
||||
let entry = entry?;
|
||||
if entry.file_type().await?.is_dir() {
|
||||
last_clangd_dir = Some(entry.path());
|
||||
}
|
||||
}
|
||||
let clangd_dir = last_clangd_dir.ok_or_else(|| anyhow!("no cached binary"))?;
|
||||
let clangd_bin = clangd_dir.join("bin/clangd");
|
||||
if clangd_bin.exists() {
|
||||
Ok(clangd_bin)
|
||||
} else {
|
||||
Err(anyhow!(
|
||||
"missing clangd binary in directory {:?}",
|
||||
clangd_dir
|
||||
))
|
||||
}
|
||||
}
|
||||
.log_err()
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn process_diagnostics(&self, _: &mut lsp::PublishDiagnosticsParams) {}
|
||||
}
|
||||
@@ -9,6 +9,3 @@ brackets = [
|
||||
{ start = "\"", end = "\"", close = true, newline = false },
|
||||
{ start = "/*", end = " */", close = true, newline = false },
|
||||
]
|
||||
|
||||
[language_server]
|
||||
disk_based_diagnostic_sources = []
|
||||
@@ -0,0 +1,111 @@
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use client::http::{self, HttpClient, Method};
|
||||
use serde::Deserialize;
|
||||
use std::{path::Path, sync::Arc};
|
||||
|
||||
pub struct GitHubLspBinaryVersion {
|
||||
pub name: String,
|
||||
pub url: http::Url,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
struct NpmInfo {
|
||||
#[serde(default)]
|
||||
dist_tags: NpmInfoDistTags,
|
||||
versions: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default)]
|
||||
struct NpmInfoDistTags {
|
||||
latest: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub(crate) struct GithubRelease {
|
||||
name: String,
|
||||
assets: Vec<GithubReleaseAsset>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub(crate) struct GithubReleaseAsset {
|
||||
name: String,
|
||||
browser_download_url: http::Url,
|
||||
}
|
||||
|
||||
pub async fn npm_package_latest_version(name: &str) -> Result<String> {
|
||||
let output = smol::process::Command::new("npm")
|
||||
.args(["info", name, "--json"])
|
||||
.output()
|
||||
.await?;
|
||||
if !output.status.success() {
|
||||
Err(anyhow!(
|
||||
"failed to execute npm info: {:?}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
))?;
|
||||
}
|
||||
let mut info: NpmInfo = serde_json::from_slice(&output.stdout)?;
|
||||
info.dist_tags
|
||||
.latest
|
||||
.or_else(|| info.versions.pop())
|
||||
.ok_or_else(|| anyhow!("no version found for npm package {}", name))
|
||||
}
|
||||
|
||||
pub async fn npm_install_packages(
|
||||
packages: impl IntoIterator<Item = (&str, &str)>,
|
||||
directory: &Path,
|
||||
) -> Result<()> {
|
||||
let output = smol::process::Command::new("npm")
|
||||
.arg("install")
|
||||
.arg("--prefix")
|
||||
.arg(directory)
|
||||
.args(
|
||||
packages
|
||||
.into_iter()
|
||||
.map(|(name, version)| format!("{name}@{version}")),
|
||||
)
|
||||
.output()
|
||||
.await
|
||||
.context("failed to run npm install")?;
|
||||
if !output.status.success() {
|
||||
Err(anyhow!(
|
||||
"failed to execute npm install: {:?}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn latest_github_release(
|
||||
repo_name_with_owner: &str,
|
||||
http: Arc<dyn HttpClient>,
|
||||
asset_name: impl Fn(&str) -> String,
|
||||
) -> Result<GitHubLspBinaryVersion> {
|
||||
let release = http
|
||||
.send(
|
||||
surf::RequestBuilder::new(
|
||||
Method::Get,
|
||||
http::Url::parse(&format!(
|
||||
"https://api.github.com/repos/{repo_name_with_owner}/releases/latest"
|
||||
))
|
||||
.unwrap(),
|
||||
)
|
||||
.middleware(surf::middleware::Redirect::default())
|
||||
.build(),
|
||||
)
|
||||
.await
|
||||
.map_err(|err| anyhow!("error fetching latest release: {}", err))?
|
||||
.body_json::<GithubRelease>()
|
||||
.await
|
||||
.map_err(|err| anyhow!("error parsing latest release: {}", err))?;
|
||||
let asset_name = asset_name(&release.name);
|
||||
let asset = release
|
||||
.assets
|
||||
.iter()
|
||||
.find(|asset| asset.name == asset_name)
|
||||
.ok_or_else(|| anyhow!("no asset found matching {:?}", asset_name))?;
|
||||
Ok(GitHubLspBinaryVersion {
|
||||
name: release.name,
|
||||
url: asset.browser_download_url.clone(),
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use client::http::HttpClient;
|
||||
use futures::{future::BoxFuture, FutureExt, StreamExt};
|
||||
use language::{LanguageServerName, LspAdapter};
|
||||
use serde::Deserialize;
|
||||
use serde_json::json;
|
||||
use smol::fs;
|
||||
use std::{any::Any, path::PathBuf, sync::Arc};
|
||||
use util::{ResultExt, TryFutureExt};
|
||||
|
||||
pub struct JsonLspAdapter;
|
||||
|
||||
impl JsonLspAdapter {
|
||||
const BIN_PATH: &'static str =
|
||||
"node_modules/vscode-json-languageserver/bin/vscode-json-languageserver";
|
||||
}
|
||||
|
||||
impl LspAdapter for JsonLspAdapter {
|
||||
fn name(&self) -> LanguageServerName {
|
||||
LanguageServerName("vscode-json-languageserver".into())
|
||||
}
|
||||
|
||||
fn server_args(&self) -> &[&str] {
|
||||
&["--stdio"]
|
||||
}
|
||||
|
||||
fn fetch_latest_server_version(
|
||||
&self,
|
||||
_: Arc<dyn HttpClient>,
|
||||
) -> BoxFuture<'static, Result<Box<dyn 'static + Any + Send>>> {
|
||||
async move {
|
||||
#[derive(Deserialize)]
|
||||
struct NpmInfo {
|
||||
versions: Vec<String>,
|
||||
}
|
||||
|
||||
let output = smol::process::Command::new("npm")
|
||||
.args(["info", "vscode-json-languageserver", "--json"])
|
||||
.output()
|
||||
.await?;
|
||||
if !output.status.success() {
|
||||
Err(anyhow!("failed to execute npm info"))?;
|
||||
}
|
||||
let mut info: NpmInfo = serde_json::from_slice(&output.stdout)?;
|
||||
|
||||
Ok(Box::new(
|
||||
info.versions
|
||||
.pop()
|
||||
.ok_or_else(|| anyhow!("no versions found in npm info"))?,
|
||||
) as Box<_>)
|
||||
}
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn fetch_server_binary(
|
||||
&self,
|
||||
version: Box<dyn 'static + Send + Any>,
|
||||
_: Arc<dyn HttpClient>,
|
||||
container_dir: PathBuf,
|
||||
) -> BoxFuture<'static, Result<PathBuf>> {
|
||||
let version = version.downcast::<String>().unwrap();
|
||||
async move {
|
||||
let version_dir = container_dir.join(version.as_str());
|
||||
fs::create_dir_all(&version_dir)
|
||||
.await
|
||||
.context("failed to create version directory")?;
|
||||
let binary_path = version_dir.join(Self::BIN_PATH);
|
||||
|
||||
if fs::metadata(&binary_path).await.is_err() {
|
||||
let output = smol::process::Command::new("npm")
|
||||
.current_dir(&version_dir)
|
||||
.arg("install")
|
||||
.arg(format!("vscode-json-languageserver@{}", version))
|
||||
.output()
|
||||
.await
|
||||
.context("failed to run npm install")?;
|
||||
if !output.status.success() {
|
||||
Err(anyhow!("failed to install vscode-json-languageserver"))?;
|
||||
}
|
||||
|
||||
if let Some(mut entries) = fs::read_dir(&container_dir).await.log_err() {
|
||||
while let Some(entry) = entries.next().await {
|
||||
if let Some(entry) = entry.log_err() {
|
||||
let entry_path = entry.path();
|
||||
if entry_path.as_path() != version_dir {
|
||||
fs::remove_dir_all(&entry_path).await.log_err();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(binary_path)
|
||||
}
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn cached_server_binary(&self, container_dir: PathBuf) -> BoxFuture<'static, Option<PathBuf>> {
|
||||
async move {
|
||||
let mut last_version_dir = None;
|
||||
let mut entries = fs::read_dir(&container_dir).await?;
|
||||
while let Some(entry) = entries.next().await {
|
||||
let entry = entry?;
|
||||
if entry.file_type().await?.is_dir() {
|
||||
last_version_dir = Some(entry.path());
|
||||
}
|
||||
}
|
||||
let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?;
|
||||
let bin_path = last_version_dir.join(Self::BIN_PATH);
|
||||
if bin_path.exists() {
|
||||
Ok(bin_path)
|
||||
} else {
|
||||
Err(anyhow!(
|
||||
"missing executable in directory {:?}",
|
||||
last_version_dir
|
||||
))
|
||||
}
|
||||
}
|
||||
.log_err()
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn process_diagnostics(&self, _: &mut lsp::PublishDiagnosticsParams) {}
|
||||
|
||||
fn initialization_options(&self) -> Option<serde_json::Value> {
|
||||
Some(json!({
|
||||
"provideFormatter": true
|
||||
}))
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,3 @@ brackets = [
|
||||
{ start = "[", end = "]", close = true, newline = true },
|
||||
{ start = "\"", end = "\"", close = true, newline = false },
|
||||
]
|
||||
|
||||
[language_server]
|
||||
disk_based_diagnostic_sources = []
|
||||
@@ -1,92 +1,50 @@
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use super::installation::{latest_github_release, GitHubLspBinaryVersion};
|
||||
use anyhow::{anyhow, Result};
|
||||
use async_compression::futures::bufread::GzipDecoder;
|
||||
use client::http::{self, HttpClient, Method};
|
||||
use client::http::{HttpClient, Method};
|
||||
use futures::{future::BoxFuture, FutureExt, StreamExt};
|
||||
use gpui::Task;
|
||||
pub use language::*;
|
||||
use lazy_static::lazy_static;
|
||||
use regex::Regex;
|
||||
use rust_embed::RustEmbed;
|
||||
use serde::Deserialize;
|
||||
use serde_json::json;
|
||||
use smol::fs::{self, File};
|
||||
use std::{borrow::Cow, env::consts, path::PathBuf, str, sync::Arc};
|
||||
use std::{any::Any, borrow::Cow, env::consts, path::PathBuf, str, sync::Arc};
|
||||
use util::{ResultExt, TryFutureExt};
|
||||
|
||||
#[derive(RustEmbed)]
|
||||
#[folder = "languages"]
|
||||
struct LanguageDir;
|
||||
|
||||
struct RustLspAdapter;
|
||||
struct CLspAdapter;
|
||||
struct JsonLspAdapter;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct GithubRelease {
|
||||
name: String,
|
||||
assets: Vec<GithubReleaseAsset>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct GithubReleaseAsset {
|
||||
name: String,
|
||||
browser_download_url: http::Url,
|
||||
}
|
||||
pub struct RustLspAdapter;
|
||||
|
||||
impl LspAdapter for RustLspAdapter {
|
||||
fn name(&self) -> &'static str {
|
||||
"rust-analyzer"
|
||||
fn name(&self) -> LanguageServerName {
|
||||
LanguageServerName("rust-analyzer".into())
|
||||
}
|
||||
|
||||
fn fetch_latest_server_version(
|
||||
&self,
|
||||
http: Arc<dyn HttpClient>,
|
||||
) -> BoxFuture<'static, Result<LspBinaryVersion>> {
|
||||
) -> BoxFuture<'static, Result<Box<dyn 'static + Send + Any>>> {
|
||||
async move {
|
||||
let release = http
|
||||
.send(
|
||||
surf::RequestBuilder::new(
|
||||
Method::Get,
|
||||
http::Url::parse(
|
||||
"https://api.github.com/repos/rust-analyzer/rust-analyzer/releases/latest",
|
||||
)
|
||||
.unwrap(),
|
||||
)
|
||||
.middleware(surf::middleware::Redirect::default())
|
||||
.build(),
|
||||
)
|
||||
.await
|
||||
.map_err(|err| anyhow!("error fetching latest release: {}", err))?
|
||||
.body_json::<GithubRelease>()
|
||||
.await
|
||||
.map_err(|err| anyhow!("error parsing latest release: {}", err))?;
|
||||
let asset_name = format!("rust-analyzer-{}-apple-darwin.gz", consts::ARCH);
|
||||
let asset = release
|
||||
.assets
|
||||
.iter()
|
||||
.find(|asset| asset.name == asset_name)
|
||||
.ok_or_else(|| anyhow!("no release found matching {:?}", asset_name))?;
|
||||
Ok(LspBinaryVersion {
|
||||
name: release.name,
|
||||
url: Some(asset.browser_download_url.clone()),
|
||||
let version = latest_github_release("rust-analyzer/rust-analyzer", http, |_| {
|
||||
format!("rust-analyzer-{}-apple-darwin.gz", consts::ARCH)
|
||||
})
|
||||
.await?;
|
||||
Ok(Box::new(version) as Box<_>)
|
||||
}
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn fetch_server_binary(
|
||||
&self,
|
||||
version: LspBinaryVersion,
|
||||
version: Box<dyn 'static + Send + Any>,
|
||||
http: Arc<dyn HttpClient>,
|
||||
container_dir: PathBuf,
|
||||
) -> BoxFuture<'static, Result<PathBuf>> {
|
||||
async move {
|
||||
let version = version.downcast::<GitHubLspBinaryVersion>().unwrap();
|
||||
let destination_path = container_dir.join(format!("rust-analyzer-{}", version.name));
|
||||
|
||||
if fs::metadata(&destination_path).await.is_err() {
|
||||
let response = http
|
||||
.send(
|
||||
surf::RequestBuilder::new(Method::Get, version.url.unwrap())
|
||||
surf::RequestBuilder::new(Method::Get, version.url)
|
||||
.middleware(surf::middleware::Redirect::default())
|
||||
.build(),
|
||||
)
|
||||
@@ -131,6 +89,14 @@ impl LspAdapter for RustLspAdapter {
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn disk_based_diagnostic_sources(&self) -> &'static [&'static str] {
|
||||
&["rustc"]
|
||||
}
|
||||
|
||||
fn disk_based_diagnostics_progress_token(&self) -> Option<&'static str> {
|
||||
Some("rustAnalyzer/cargo check")
|
||||
}
|
||||
|
||||
fn process_diagnostics(&self, params: &mut lsp::PublishDiagnosticsParams) {
|
||||
lazy_static! {
|
||||
static ref REGEX: Regex = Regex::new("(?m)`([^`]+)\n`$").unwrap();
|
||||
@@ -287,325 +253,11 @@ impl LspAdapter for RustLspAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
impl LspAdapter for CLspAdapter {
|
||||
fn name(&self) -> &'static str {
|
||||
"clangd"
|
||||
}
|
||||
|
||||
fn fetch_latest_server_version(
|
||||
&self,
|
||||
http: Arc<dyn HttpClient>,
|
||||
) -> BoxFuture<'static, Result<LspBinaryVersion>> {
|
||||
async move {
|
||||
let release = http
|
||||
.send(
|
||||
surf::RequestBuilder::new(
|
||||
Method::Get,
|
||||
http::Url::parse(
|
||||
"https://api.github.com/repos/clangd/clangd/releases/latest",
|
||||
)
|
||||
.unwrap(),
|
||||
)
|
||||
.middleware(surf::middleware::Redirect::default())
|
||||
.build(),
|
||||
)
|
||||
.await
|
||||
.map_err(|err| anyhow!("error fetching latest release: {}", err))?
|
||||
.body_json::<GithubRelease>()
|
||||
.await
|
||||
.map_err(|err| anyhow!("error parsing latest release: {}", err))?;
|
||||
let asset_name = format!("clangd-mac-{}.zip", release.name);
|
||||
let asset = release
|
||||
.assets
|
||||
.iter()
|
||||
.find(|asset| asset.name == asset_name)
|
||||
.ok_or_else(|| anyhow!("no release found matching {:?}", asset_name))?;
|
||||
Ok(LspBinaryVersion {
|
||||
name: release.name,
|
||||
url: Some(asset.browser_download_url.clone()),
|
||||
})
|
||||
}
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn fetch_server_binary(
|
||||
&self,
|
||||
version: LspBinaryVersion,
|
||||
http: Arc<dyn HttpClient>,
|
||||
container_dir: PathBuf,
|
||||
) -> BoxFuture<'static, Result<PathBuf>> {
|
||||
async move {
|
||||
let zip_path = container_dir.join(format!("clangd_{}.zip", version.name));
|
||||
let version_dir = container_dir.join(format!("clangd_{}", version.name));
|
||||
let binary_path = version_dir.join("bin/clangd");
|
||||
|
||||
if fs::metadata(&binary_path).await.is_err() {
|
||||
let response = http
|
||||
.send(
|
||||
surf::RequestBuilder::new(Method::Get, version.url.unwrap())
|
||||
.middleware(surf::middleware::Redirect::default())
|
||||
.build(),
|
||||
)
|
||||
.await
|
||||
.map_err(|err| anyhow!("error downloading release: {}", err))?;
|
||||
let mut file = File::create(&zip_path).await?;
|
||||
if !response.status().is_success() {
|
||||
Err(anyhow!(
|
||||
"download failed with status {}",
|
||||
response.status().to_string()
|
||||
))?;
|
||||
}
|
||||
futures::io::copy(response, &mut file).await?;
|
||||
|
||||
let unzip_status = smol::process::Command::new("unzip")
|
||||
.current_dir(&container_dir)
|
||||
.arg(&zip_path)
|
||||
.output()
|
||||
.await?
|
||||
.status;
|
||||
if !unzip_status.success() {
|
||||
Err(anyhow!("failed to unzip clangd archive"))?;
|
||||
}
|
||||
|
||||
if let Some(mut entries) = fs::read_dir(&container_dir).await.log_err() {
|
||||
while let Some(entry) = entries.next().await {
|
||||
if let Some(entry) = entry.log_err() {
|
||||
let entry_path = entry.path();
|
||||
if entry_path.as_path() != version_dir {
|
||||
fs::remove_dir_all(&entry_path).await.log_err();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(binary_path)
|
||||
}
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn cached_server_binary(&self, container_dir: PathBuf) -> BoxFuture<'static, Option<PathBuf>> {
|
||||
async move {
|
||||
let mut last_clangd_dir = None;
|
||||
let mut entries = fs::read_dir(&container_dir).await?;
|
||||
while let Some(entry) = entries.next().await {
|
||||
let entry = entry?;
|
||||
if entry.file_type().await?.is_dir() {
|
||||
last_clangd_dir = Some(entry.path());
|
||||
}
|
||||
}
|
||||
let clangd_dir = last_clangd_dir.ok_or_else(|| anyhow!("no cached binary"))?;
|
||||
let clangd_bin = clangd_dir.join("bin/clangd");
|
||||
if clangd_bin.exists() {
|
||||
Ok(clangd_bin)
|
||||
} else {
|
||||
Err(anyhow!(
|
||||
"missing clangd binary in directory {:?}",
|
||||
clangd_dir
|
||||
))
|
||||
}
|
||||
}
|
||||
.log_err()
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn process_diagnostics(&self, _: &mut lsp::PublishDiagnosticsParams) {}
|
||||
}
|
||||
|
||||
impl JsonLspAdapter {
|
||||
const BIN_PATH: &'static str =
|
||||
"node_modules/vscode-json-languageserver/bin/vscode-json-languageserver";
|
||||
}
|
||||
|
||||
impl LspAdapter for JsonLspAdapter {
|
||||
fn name(&self) -> &'static str {
|
||||
"vscode-json-languageserver"
|
||||
}
|
||||
|
||||
fn server_args(&self) -> &[&str] {
|
||||
&["--stdio"]
|
||||
}
|
||||
|
||||
fn fetch_latest_server_version(
|
||||
&self,
|
||||
_: Arc<dyn HttpClient>,
|
||||
) -> BoxFuture<'static, Result<LspBinaryVersion>> {
|
||||
async move {
|
||||
#[derive(Deserialize)]
|
||||
struct NpmInfo {
|
||||
versions: Vec<String>,
|
||||
}
|
||||
|
||||
let output = smol::process::Command::new("npm")
|
||||
.args(["info", "vscode-json-languageserver", "--json"])
|
||||
.output()
|
||||
.await?;
|
||||
if !output.status.success() {
|
||||
Err(anyhow!("failed to execute npm info"))?;
|
||||
}
|
||||
let mut info: NpmInfo = serde_json::from_slice(&output.stdout)?;
|
||||
|
||||
Ok(LspBinaryVersion {
|
||||
name: info
|
||||
.versions
|
||||
.pop()
|
||||
.ok_or_else(|| anyhow!("no versions found in npm info"))?,
|
||||
url: Default::default(),
|
||||
})
|
||||
}
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn fetch_server_binary(
|
||||
&self,
|
||||
version: LspBinaryVersion,
|
||||
_: Arc<dyn HttpClient>,
|
||||
container_dir: PathBuf,
|
||||
) -> BoxFuture<'static, Result<PathBuf>> {
|
||||
async move {
|
||||
let version_dir = container_dir.join(&version.name);
|
||||
fs::create_dir_all(&version_dir)
|
||||
.await
|
||||
.context("failed to create version directory")?;
|
||||
let binary_path = version_dir.join(Self::BIN_PATH);
|
||||
|
||||
if fs::metadata(&binary_path).await.is_err() {
|
||||
let output = smol::process::Command::new("npm")
|
||||
.current_dir(&version_dir)
|
||||
.arg("install")
|
||||
.arg(format!("vscode-json-languageserver@{}", version.name))
|
||||
.output()
|
||||
.await
|
||||
.context("failed to run npm install")?;
|
||||
if !output.status.success() {
|
||||
Err(anyhow!("failed to install vscode-json-languageserver"))?;
|
||||
}
|
||||
|
||||
if let Some(mut entries) = fs::read_dir(&container_dir).await.log_err() {
|
||||
while let Some(entry) = entries.next().await {
|
||||
if let Some(entry) = entry.log_err() {
|
||||
let entry_path = entry.path();
|
||||
if entry_path.as_path() != version_dir {
|
||||
fs::remove_dir_all(&entry_path).await.log_err();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(binary_path)
|
||||
}
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn cached_server_binary(&self, container_dir: PathBuf) -> BoxFuture<'static, Option<PathBuf>> {
|
||||
async move {
|
||||
let mut last_version_dir = None;
|
||||
let mut entries = fs::read_dir(&container_dir).await?;
|
||||
while let Some(entry) = entries.next().await {
|
||||
let entry = entry?;
|
||||
if entry.file_type().await?.is_dir() {
|
||||
last_version_dir = Some(entry.path());
|
||||
}
|
||||
}
|
||||
let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?;
|
||||
let bin_path = last_version_dir.join(Self::BIN_PATH);
|
||||
if bin_path.exists() {
|
||||
Ok(bin_path)
|
||||
} else {
|
||||
Err(anyhow!(
|
||||
"missing executable in directory {:?}",
|
||||
last_version_dir
|
||||
))
|
||||
}
|
||||
}
|
||||
.log_err()
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn process_diagnostics(&self, _: &mut lsp::PublishDiagnosticsParams) {}
|
||||
|
||||
fn initialization_options(&self) -> Option<serde_json::Value> {
|
||||
Some(json!({
|
||||
"provideFormatter": true
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build_language_registry(login_shell_env_loaded: Task<()>) -> LanguageRegistry {
|
||||
let languages = LanguageRegistry::new(login_shell_env_loaded);
|
||||
languages.add(Arc::new(c()));
|
||||
languages.add(Arc::new(json()));
|
||||
languages.add(Arc::new(rust()));
|
||||
languages.add(Arc::new(markdown()));
|
||||
languages
|
||||
}
|
||||
|
||||
fn rust() -> Language {
|
||||
let grammar = tree_sitter_rust::language();
|
||||
let config = toml::from_slice(&LanguageDir::get("rust/config.toml").unwrap().data).unwrap();
|
||||
Language::new(config, Some(grammar))
|
||||
.with_highlights_query(load_query("rust/highlights.scm").as_ref())
|
||||
.unwrap()
|
||||
.with_brackets_query(load_query("rust/brackets.scm").as_ref())
|
||||
.unwrap()
|
||||
.with_indents_query(load_query("rust/indents.scm").as_ref())
|
||||
.unwrap()
|
||||
.with_outline_query(load_query("rust/outline.scm").as_ref())
|
||||
.unwrap()
|
||||
.with_lsp_adapter(RustLspAdapter)
|
||||
}
|
||||
|
||||
fn c() -> Language {
|
||||
let grammar = tree_sitter_c::language();
|
||||
let config = toml::from_slice(&LanguageDir::get("c/config.toml").unwrap().data).unwrap();
|
||||
Language::new(config, Some(grammar))
|
||||
.with_highlights_query(load_query("c/highlights.scm").as_ref())
|
||||
.unwrap()
|
||||
.with_brackets_query(load_query("c/brackets.scm").as_ref())
|
||||
.unwrap()
|
||||
.with_indents_query(load_query("c/indents.scm").as_ref())
|
||||
.unwrap()
|
||||
.with_outline_query(load_query("c/outline.scm").as_ref())
|
||||
.unwrap()
|
||||
.with_lsp_adapter(CLspAdapter)
|
||||
}
|
||||
|
||||
fn json() -> Language {
|
||||
let grammar = tree_sitter_json::language();
|
||||
let config = toml::from_slice(&LanguageDir::get("json/config.toml").unwrap().data).unwrap();
|
||||
Language::new(config, Some(grammar))
|
||||
.with_highlights_query(load_query("json/highlights.scm").as_ref())
|
||||
.unwrap()
|
||||
.with_brackets_query(load_query("json/brackets.scm").as_ref())
|
||||
.unwrap()
|
||||
.with_indents_query(load_query("json/indents.scm").as_ref())
|
||||
.unwrap()
|
||||
.with_outline_query(load_query("json/outline.scm").as_ref())
|
||||
.unwrap()
|
||||
.with_lsp_adapter(JsonLspAdapter)
|
||||
}
|
||||
|
||||
fn markdown() -> Language {
|
||||
let grammar = tree_sitter_markdown::language();
|
||||
let config = toml::from_slice(&LanguageDir::get("markdown/config.toml").unwrap().data).unwrap();
|
||||
Language::new(config, Some(grammar))
|
||||
.with_highlights_query(load_query("markdown/highlights.scm").as_ref())
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn load_query(path: &str) -> Cow<'static, str> {
|
||||
match LanguageDir::get(path).unwrap().data {
|
||||
Cow::Borrowed(s) => Cow::Borrowed(str::from_utf8(s).unwrap()),
|
||||
Cow::Owned(s) => Cow::Owned(String::from_utf8(s).unwrap()),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::languages::{language, LspAdapter};
|
||||
use gpui::color::Color;
|
||||
use language::LspAdapter;
|
||||
use theme::SyntaxTheme;
|
||||
|
||||
#[test]
|
||||
@@ -651,7 +303,11 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_rust_label_for_completion() {
|
||||
let language = rust();
|
||||
let language = language(
|
||||
"rust",
|
||||
tree_sitter_rust::language(),
|
||||
Some(Arc::new(RustLspAdapter)),
|
||||
);
|
||||
let grammar = language.grammar().unwrap();
|
||||
let theme = SyntaxTheme::new(vec![
|
||||
("type".into(), Color::green().into()),
|
||||
@@ -726,7 +382,11 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_rust_label_for_symbol() {
|
||||
let language = rust();
|
||||
let language = language(
|
||||
"rust",
|
||||
tree_sitter_rust::language(),
|
||||
Some(Arc::new(RustLspAdapter)),
|
||||
);
|
||||
let grammar = language.grammar().unwrap();
|
||||
let theme = SyntaxTheme::new(vec![
|
||||
("type".into(), Color::green().into()),
|
||||
@@ -10,7 +10,3 @@ brackets = [
|
||||
{ start = "\"", end = "\"", close = true, newline = false },
|
||||
{ start = "/*", end = " */", close = true, newline = false },
|
||||
]
|
||||
|
||||
[language_server]
|
||||
disk_based_diagnostic_sources = ["rustc"]
|
||||
disk_based_diagnostics_progress_token = "rustAnalyzer/cargo check"
|
||||
@@ -0,0 +1 @@
|
||||
../typescript/brackets.scm
|
||||
@@ -0,0 +1,12 @@
|
||||
name = "TSX"
|
||||
path_suffixes = ["tsx", "js"]
|
||||
line_comment = "// "
|
||||
autoclose_before = ";:.,=}])>"
|
||||
brackets = [
|
||||
{ start = "{", end = "}", close = true, newline = true },
|
||||
{ start = "[", end = "]", close = true, newline = true },
|
||||
{ start = "(", end = ")", close = true, newline = true },
|
||||
{ start = "<", end = ">", close = false, newline = true },
|
||||
{ start = "\"", end = "\"", close = true, newline = false },
|
||||
{ start = "/*", end = " */", close = true, newline = false },
|
||||
]
|
||||
@@ -0,0 +1 @@
|
||||
../typescript/highlights.scm
|
||||
@@ -0,0 +1 @@
|
||||
../typescript/indents.scm
|
||||
@@ -0,0 +1 @@
|
||||
../typescript/outline.scm
|
||||
@@ -0,0 +1,146 @@
|
||||
use super::installation::{npm_install_packages, npm_package_latest_version};
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use client::http::HttpClient;
|
||||
use futures::{future::BoxFuture, FutureExt, StreamExt};
|
||||
use language::{LanguageServerName, LspAdapter};
|
||||
use serde_json::json;
|
||||
use smol::fs;
|
||||
use std::{any::Any, path::PathBuf, sync::Arc};
|
||||
use util::{ResultExt, TryFutureExt};
|
||||
|
||||
pub struct TypeScriptLspAdapter;
|
||||
|
||||
impl TypeScriptLspAdapter {
|
||||
const BIN_PATH: &'static str = "node_modules/typescript-language-server/lib/cli.js";
|
||||
}
|
||||
|
||||
struct Versions {
|
||||
typescript_version: String,
|
||||
server_version: String,
|
||||
}
|
||||
|
||||
impl LspAdapter for TypeScriptLspAdapter {
|
||||
fn name(&self) -> LanguageServerName {
|
||||
LanguageServerName("typescript-language-server".into())
|
||||
}
|
||||
|
||||
fn server_args(&self) -> &[&str] {
|
||||
&["--stdio", "--tsserver-path", "node_modules/typescript/lib"]
|
||||
}
|
||||
|
||||
fn fetch_latest_server_version(
|
||||
&self,
|
||||
_: Arc<dyn HttpClient>,
|
||||
) -> BoxFuture<'static, Result<Box<dyn 'static + Send + Any>>> {
|
||||
async move {
|
||||
Ok(Box::new(Versions {
|
||||
typescript_version: npm_package_latest_version("typescript").await?,
|
||||
server_version: npm_package_latest_version("typescript-language-server").await?,
|
||||
}) as Box<_>)
|
||||
}
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn fetch_server_binary(
|
||||
&self,
|
||||
versions: Box<dyn 'static + Send + Any>,
|
||||
_: Arc<dyn HttpClient>,
|
||||
container_dir: PathBuf,
|
||||
) -> BoxFuture<'static, Result<PathBuf>> {
|
||||
let versions = versions.downcast::<Versions>().unwrap();
|
||||
async move {
|
||||
let version_dir = container_dir.join(&format!(
|
||||
"typescript-{}:server-{}",
|
||||
versions.typescript_version, versions.server_version
|
||||
));
|
||||
fs::create_dir_all(&version_dir)
|
||||
.await
|
||||
.context("failed to create version directory")?;
|
||||
let binary_path = version_dir.join(Self::BIN_PATH);
|
||||
|
||||
if fs::metadata(&binary_path).await.is_err() {
|
||||
npm_install_packages(
|
||||
[
|
||||
("typescript", versions.typescript_version.as_str()),
|
||||
(
|
||||
"typescript-language-server",
|
||||
&versions.server_version.as_str(),
|
||||
),
|
||||
],
|
||||
&version_dir,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let Some(mut entries) = fs::read_dir(&container_dir).await.log_err() {
|
||||
while let Some(entry) = entries.next().await {
|
||||
if let Some(entry) = entry.log_err() {
|
||||
let entry_path = entry.path();
|
||||
if entry_path.as_path() != version_dir {
|
||||
fs::remove_dir_all(&entry_path).await.log_err();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(binary_path)
|
||||
}
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn cached_server_binary(&self, container_dir: PathBuf) -> BoxFuture<'static, Option<PathBuf>> {
|
||||
async move {
|
||||
let mut last_version_dir = None;
|
||||
let mut entries = fs::read_dir(&container_dir).await?;
|
||||
while let Some(entry) = entries.next().await {
|
||||
let entry = entry?;
|
||||
if entry.file_type().await?.is_dir() {
|
||||
last_version_dir = Some(entry.path());
|
||||
}
|
||||
}
|
||||
let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?;
|
||||
let bin_path = last_version_dir.join(Self::BIN_PATH);
|
||||
if bin_path.exists() {
|
||||
Ok(bin_path)
|
||||
} else {
|
||||
Err(anyhow!(
|
||||
"missing executable in directory {:?}",
|
||||
last_version_dir
|
||||
))
|
||||
}
|
||||
}
|
||||
.log_err()
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn process_diagnostics(&self, _: &mut lsp::PublishDiagnosticsParams) {}
|
||||
|
||||
fn label_for_completion(
|
||||
&self,
|
||||
item: &lsp::CompletionItem,
|
||||
language: &language::Language,
|
||||
) -> Option<language::CodeLabel> {
|
||||
use lsp::CompletionItemKind as Kind;
|
||||
let len = item.label.len();
|
||||
let grammar = language.grammar()?;
|
||||
let highlight_id = match item.kind? {
|
||||
Kind::CLASS | Kind::INTERFACE => grammar.highlight_id_for_name("type"),
|
||||
Kind::CONSTRUCTOR => grammar.highlight_id_for_name("type"),
|
||||
Kind::CONSTANT => grammar.highlight_id_for_name("constant"),
|
||||
Kind::FUNCTION | Kind::METHOD => grammar.highlight_id_for_name("function"),
|
||||
Kind::PROPERTY | Kind::FIELD => grammar.highlight_id_for_name("property"),
|
||||
_ => None,
|
||||
}?;
|
||||
Some(language::CodeLabel {
|
||||
text: item.label.clone(),
|
||||
runs: vec![(0..len, highlight_id)],
|
||||
filter_range: 0..len,
|
||||
})
|
||||
}
|
||||
|
||||
fn initialization_options(&self) -> Option<serde_json::Value> {
|
||||
Some(json!({
|
||||
"provideFormatter": true
|
||||
}))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
("(" @open ")" @close)
|
||||
("[" @open "]" @close)
|
||||
("{" @open "}" @close)
|
||||
("<" @open ">" @close)
|
||||
("\"" @open "\"" @close)
|
||||
@@ -0,0 +1,12 @@
|
||||
name = "TypeScript"
|
||||
path_suffixes = ["ts"]
|
||||
line_comment = "// "
|
||||
autoclose_before = ";:.,=}])>"
|
||||
brackets = [
|
||||
{ start = "{", end = "}", close = true, newline = true },
|
||||
{ start = "[", end = "]", close = true, newline = true },
|
||||
{ start = "(", end = ")", close = true, newline = true },
|
||||
{ start = "<", end = ">", close = false, newline = true },
|
||||
{ start = "\"", end = "\"", close = true, newline = false },
|
||||
{ start = "/*", end = " */", close = true, newline = false },
|
||||
]
|
||||
@@ -0,0 +1,219 @@
|
||||
; Variables
|
||||
|
||||
(identifier) @variable
|
||||
|
||||
; Properties
|
||||
|
||||
(property_identifier) @property
|
||||
|
||||
; Function and method calls
|
||||
|
||||
(call_expression
|
||||
function: (identifier) @function)
|
||||
|
||||
(call_expression
|
||||
function: (member_expression
|
||||
property: (property_identifier) @function.method))
|
||||
|
||||
; Function and method definitions
|
||||
|
||||
(function
|
||||
name: (identifier) @function)
|
||||
(function_declaration
|
||||
name: (identifier) @function)
|
||||
(method_definition
|
||||
name: (property_identifier) @function.method)
|
||||
|
||||
(pair
|
||||
key: (property_identifier) @function.method
|
||||
value: [(function) (arrow_function)])
|
||||
|
||||
(assignment_expression
|
||||
left: (member_expression
|
||||
property: (property_identifier) @function.method)
|
||||
right: [(function) (arrow_function)])
|
||||
|
||||
(variable_declarator
|
||||
name: (identifier) @function
|
||||
value: [(function) (arrow_function)])
|
||||
|
||||
(assignment_expression
|
||||
left: (identifier) @function
|
||||
right: [(function) (arrow_function)])
|
||||
|
||||
; Special identifiers
|
||||
|
||||
((identifier) @constructor
|
||||
(#match? @constructor "^[A-Z]"))
|
||||
|
||||
([
|
||||
(identifier)
|
||||
(shorthand_property_identifier)
|
||||
(shorthand_property_identifier_pattern)
|
||||
] @constant
|
||||
(#match? @constant "^[A-Z_][A-Z\\d_]+$"))
|
||||
|
||||
; Literals
|
||||
|
||||
(this) @variable.builtin
|
||||
(super) @variable.builtin
|
||||
|
||||
[
|
||||
(true)
|
||||
(false)
|
||||
(null)
|
||||
(undefined)
|
||||
] @constant.builtin
|
||||
|
||||
(comment) @comment
|
||||
|
||||
[
|
||||
(string)
|
||||
(template_string)
|
||||
] @string
|
||||
|
||||
(regex) @string.special
|
||||
(number) @number
|
||||
|
||||
; Tokens
|
||||
|
||||
(template_substitution
|
||||
"${" @punctuation.special
|
||||
"}" @punctuation.special) @embedded
|
||||
|
||||
[
|
||||
";"
|
||||
"?."
|
||||
"."
|
||||
","
|
||||
] @punctuation.delimiter
|
||||
|
||||
[
|
||||
"-"
|
||||
"--"
|
||||
"-="
|
||||
"+"
|
||||
"++"
|
||||
"+="
|
||||
"*"
|
||||
"*="
|
||||
"**"
|
||||
"**="
|
||||
"/"
|
||||
"/="
|
||||
"%"
|
||||
"%="
|
||||
"<"
|
||||
"<="
|
||||
"<<"
|
||||
"<<="
|
||||
"="
|
||||
"=="
|
||||
"==="
|
||||
"!"
|
||||
"!="
|
||||
"!=="
|
||||
"=>"
|
||||
">"
|
||||
">="
|
||||
">>"
|
||||
">>="
|
||||
">>>"
|
||||
">>>="
|
||||
"~"
|
||||
"^"
|
||||
"&"
|
||||
"|"
|
||||
"^="
|
||||
"&="
|
||||
"|="
|
||||
"&&"
|
||||
"||"
|
||||
"??"
|
||||
"&&="
|
||||
"||="
|
||||
"??="
|
||||
] @operator
|
||||
|
||||
[
|
||||
"("
|
||||
")"
|
||||
"["
|
||||
"]"
|
||||
"{"
|
||||
"}"
|
||||
] @punctuation.bracket
|
||||
|
||||
[
|
||||
"as"
|
||||
"async"
|
||||
"await"
|
||||
"break"
|
||||
"case"
|
||||
"catch"
|
||||
"class"
|
||||
"const"
|
||||
"continue"
|
||||
"debugger"
|
||||
"default"
|
||||
"delete"
|
||||
"do"
|
||||
"else"
|
||||
"export"
|
||||
"extends"
|
||||
"finally"
|
||||
"for"
|
||||
"from"
|
||||
"function"
|
||||
"get"
|
||||
"if"
|
||||
"import"
|
||||
"in"
|
||||
"instanceof"
|
||||
"let"
|
||||
"new"
|
||||
"of"
|
||||
"return"
|
||||
"set"
|
||||
"static"
|
||||
"switch"
|
||||
"target"
|
||||
"throw"
|
||||
"try"
|
||||
"typeof"
|
||||
"var"
|
||||
"void"
|
||||
"while"
|
||||
"with"
|
||||
"yield"
|
||||
] @keyword
|
||||
|
||||
; Types
|
||||
|
||||
(type_identifier) @type
|
||||
(predefined_type) @type.builtin
|
||||
|
||||
((identifier) @type
|
||||
(#match? @type "^[A-Z]"))
|
||||
|
||||
(type_arguments
|
||||
"<" @punctuation.bracket
|
||||
">" @punctuation.bracket)
|
||||
|
||||
; Keywords
|
||||
|
||||
[ "abstract"
|
||||
"declare"
|
||||
"enum"
|
||||
"export"
|
||||
"implements"
|
||||
"interface"
|
||||
"keyof"
|
||||
"namespace"
|
||||
"private"
|
||||
"protected"
|
||||
"public"
|
||||
"type"
|
||||
"readonly"
|
||||
"override"
|
||||
] @keyword
|
||||
@@ -0,0 +1,15 @@
|
||||
[
|
||||
(call_expression)
|
||||
(assignment_expression)
|
||||
(member_expression)
|
||||
(lexical_declaration)
|
||||
(variable_declaration)
|
||||
(assignment_expression)
|
||||
(if_statement)
|
||||
(for_statement)
|
||||
] @indent
|
||||
|
||||
(_ "[" "]" @end) @indent
|
||||
(_ "<" ">" @end) @indent
|
||||
(_ "{" "}" @end) @indent
|
||||
(_ "(" ")" @end) @indent
|
||||
@@ -0,0 +1,55 @@
|
||||
(internal_module
|
||||
"namespace" @context
|
||||
name: (_) @name) @item
|
||||
|
||||
(enum_declaration
|
||||
"enum" @context
|
||||
name: (_) @name) @item
|
||||
|
||||
(function_declaration
|
||||
"async"? @context
|
||||
"function" @context
|
||||
name: (_) @name
|
||||
parameters: (formal_parameters
|
||||
"(" @context
|
||||
")" @context)) @item
|
||||
|
||||
(interface_declaration
|
||||
"interface" @context
|
||||
name: (_) @name) @item
|
||||
|
||||
(program
|
||||
(lexical_declaration
|
||||
["let" "const"] @context
|
||||
(variable_declarator
|
||||
name: (_) @name) @item))
|
||||
|
||||
(class_declaration
|
||||
"class" @context
|
||||
name: (_) @name) @item
|
||||
|
||||
(method_definition
|
||||
[
|
||||
"get"
|
||||
"set"
|
||||
"async"
|
||||
"*"
|
||||
"readonly"
|
||||
"static"
|
||||
(override_modifier)
|
||||
(accessibility_modifier)
|
||||
]* @context
|
||||
name: (_) @name
|
||||
parameters: (formal_parameters
|
||||
"(" @context
|
||||
")" @context)) @item
|
||||
|
||||
(public_field_definition
|
||||
[
|
||||
"declare"
|
||||
"readonly"
|
||||
"abstract"
|
||||
"static"
|
||||
(accessibility_modifier)
|
||||
]* @context
|
||||
name: (_) @name) @item
|
||||
@@ -19,7 +19,7 @@ use workspace::{
|
||||
AppState, OpenNew, OpenParams, OpenPaths, Settings,
|
||||
};
|
||||
use zed::{
|
||||
self, assets::Assets, build_window_options, build_workspace, fs::RealFs, language, menus,
|
||||
self, assets::Assets, build_window_options, build_workspace, fs::RealFs, languages, menus,
|
||||
};
|
||||
|
||||
fn main() {
|
||||
@@ -34,7 +34,7 @@ fn main() {
|
||||
let default_settings = Settings::new("Zed Mono", &app.font_cache(), theme)
|
||||
.unwrap()
|
||||
.with_overrides(
|
||||
language::PLAIN_TEXT.name(),
|
||||
languages::PLAIN_TEXT.name(),
|
||||
settings::LanguageOverride {
|
||||
soft_wrap: Some(settings::SoftWrap::PreferredLineLength),
|
||||
..Default::default()
|
||||
@@ -60,7 +60,7 @@ fn main() {
|
||||
app.run(move |cx| {
|
||||
let http = http::client();
|
||||
let client = client::Client::new(http.clone());
|
||||
let mut languages = language::build_language_registry(login_shell_env_loaded);
|
||||
let mut languages = languages::build_language_registry(login_shell_env_loaded);
|
||||
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http.clone(), cx));
|
||||
let channel_list =
|
||||
cx.add_model(|cx| ChannelList::new(user_store.clone(), client.clone(), cx));
|
||||
|
||||
+40
-28
@@ -1,5 +1,5 @@
|
||||
pub mod assets;
|
||||
pub mod language;
|
||||
pub mod languages;
|
||||
pub mod menus;
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub mod test;
|
||||
@@ -106,18 +106,21 @@ pub fn build_workspace(
|
||||
app_state: &Arc<AppState>,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) -> Workspace {
|
||||
cx.subscribe(&cx.handle(), |_, _, event, cx| {
|
||||
let workspace::Event::PaneAdded(pane) = event;
|
||||
pane.update(cx, |pane, cx| {
|
||||
pane.toolbar().update(cx, |toolbar, cx| {
|
||||
let breadcrumbs = cx.add_view(|_| Breadcrumbs::new());
|
||||
toolbar.add_item(breadcrumbs, cx);
|
||||
let buffer_search_bar = cx.add_view(|cx| BufferSearchBar::new(cx));
|
||||
toolbar.add_item(buffer_search_bar, cx);
|
||||
let project_search_bar = cx.add_view(|_| ProjectSearchBar::new());
|
||||
toolbar.add_item(project_search_bar, cx);
|
||||
})
|
||||
});
|
||||
cx.subscribe(&cx.handle(), {
|
||||
let project = project.clone();
|
||||
move |_, _, event, cx| {
|
||||
let workspace::Event::PaneAdded(pane) = event;
|
||||
pane.update(cx, |pane, cx| {
|
||||
pane.toolbar().update(cx, |toolbar, cx| {
|
||||
let breadcrumbs = cx.add_view(|_| Breadcrumbs::new(project.clone()));
|
||||
toolbar.add_item(breadcrumbs, cx);
|
||||
let buffer_search_bar = cx.add_view(|cx| BufferSearchBar::new(cx));
|
||||
toolbar.add_item(buffer_search_bar, cx);
|
||||
let project_search_bar = cx.add_view(|_| ProjectSearchBar::new());
|
||||
toolbar.add_item(project_search_bar, cx);
|
||||
})
|
||||
});
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
@@ -574,7 +577,7 @@ mod tests {
|
||||
assert_eq!(editor.title(cx), "untitled");
|
||||
assert!(Arc::ptr_eq(
|
||||
editor.language(cx).unwrap(),
|
||||
&language::PLAIN_TEXT
|
||||
&languages::PLAIN_TEXT
|
||||
));
|
||||
editor.handle_input(&editor::Input("hi".into()), cx);
|
||||
assert!(editor.is_dirty(cx));
|
||||
@@ -664,7 +667,7 @@ mod tests {
|
||||
editor.update(cx, |editor, cx| {
|
||||
assert!(Arc::ptr_eq(
|
||||
editor.language(cx).unwrap(),
|
||||
&language::PLAIN_TEXT
|
||||
&languages::PLAIN_TEXT
|
||||
));
|
||||
editor.handle_input(&editor::Input("hi".into()), cx);
|
||||
assert!(editor.is_dirty(cx.as_ref()));
|
||||
@@ -683,6 +686,8 @@ mod tests {
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_pane_actions(cx: &mut TestAppContext) {
|
||||
cx.foreground().forbid_parking();
|
||||
|
||||
cx.update(|cx| pane::init(cx));
|
||||
let app_state = cx.update(test_app_state);
|
||||
app_state
|
||||
@@ -740,7 +745,9 @@ mod tests {
|
||||
assert_eq!(pane2_item.project_path(cx.as_ref()), Some(file1.clone()));
|
||||
|
||||
cx.dispatch_action(window_id, vec![pane_2.id()], &workspace::CloseActiveItem);
|
||||
let workspace = workspace.read(cx);
|
||||
});
|
||||
cx.foreground().run_until_parked();
|
||||
workspace.read_with(cx, |workspace, _| {
|
||||
assert_eq!(workspace.panes().len(), 1);
|
||||
assert_eq!(workspace.active_pane(), &pane_1);
|
||||
});
|
||||
@@ -867,12 +874,15 @@ mod tests {
|
||||
|
||||
// Go forward to an item that has been closed, ensuring it gets re-opened at the same
|
||||
// location.
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace
|
||||
.active_pane()
|
||||
.update(cx, |pane, cx| pane.close_item(editor3.id(), cx));
|
||||
drop(editor3);
|
||||
});
|
||||
workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
let editor3_id = editor3.id();
|
||||
drop(editor3);
|
||||
workspace
|
||||
.active_pane()
|
||||
.update(cx, |pane, cx| pane.close_item(editor3_id, cx))
|
||||
})
|
||||
.await;
|
||||
workspace
|
||||
.update(cx, |w, cx| Pane::go_forward(w, None, cx))
|
||||
.await;
|
||||
@@ -884,15 +894,17 @@ mod tests {
|
||||
// Go back to an item that has been closed and removed from disk, ensuring it gets skipped.
|
||||
workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
let editor2_id = editor2.id();
|
||||
drop(editor2);
|
||||
workspace
|
||||
.active_pane()
|
||||
.update(cx, |pane, cx| pane.close_item(editor2.id(), cx));
|
||||
drop(editor2);
|
||||
app_state
|
||||
.fs
|
||||
.as_fake()
|
||||
.remove_file(Path::new("/root/a/file2"), Default::default())
|
||||
.update(cx, |pane, cx| pane.close_item(editor2_id, cx))
|
||||
})
|
||||
.await;
|
||||
app_state
|
||||
.fs
|
||||
.as_fake()
|
||||
.remove_file(Path::new("/root/a/file2"), Default::default())
|
||||
.await
|
||||
.unwrap();
|
||||
workspace
|
||||
|
||||
Reference in New Issue
Block a user