Add prettier support (#3122)
This commit is contained in:
commit
a50977e0fd
28 changed files with 1701 additions and 49 deletions
24
Cargo.lock
generated
24
Cargo.lock
generated
|
@ -1501,6 +1501,7 @@ dependencies = [
|
||||||
"log",
|
"log",
|
||||||
"lsp",
|
"lsp",
|
||||||
"nanoid",
|
"nanoid",
|
||||||
|
"node_runtime",
|
||||||
"parking_lot 0.11.2",
|
"parking_lot 0.11.2",
|
||||||
"pretty_assertions",
|
"pretty_assertions",
|
||||||
"project",
|
"project",
|
||||||
|
@ -5517,6 +5518,26 @@ version = "0.2.17"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
|
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "prettier"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"client",
|
||||||
|
"collections",
|
||||||
|
"fs",
|
||||||
|
"futures 0.3.28",
|
||||||
|
"gpui",
|
||||||
|
"language",
|
||||||
|
"log",
|
||||||
|
"lsp",
|
||||||
|
"node_runtime",
|
||||||
|
"serde",
|
||||||
|
"serde_derive",
|
||||||
|
"serde_json",
|
||||||
|
"util",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pretty_assertions"
|
name = "pretty_assertions"
|
||||||
version = "1.4.0"
|
version = "1.4.0"
|
||||||
|
@ -5629,8 +5650,10 @@ dependencies = [
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"log",
|
"log",
|
||||||
"lsp",
|
"lsp",
|
||||||
|
"node_runtime",
|
||||||
"parking_lot 0.11.2",
|
"parking_lot 0.11.2",
|
||||||
"postage",
|
"postage",
|
||||||
|
"prettier",
|
||||||
"pretty_assertions",
|
"pretty_assertions",
|
||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
"regex",
|
"regex",
|
||||||
|
@ -9986,6 +10009,7 @@ dependencies = [
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"log",
|
"log",
|
||||||
"menu",
|
"menu",
|
||||||
|
"node_runtime",
|
||||||
"parking_lot 0.11.2",
|
"parking_lot 0.11.2",
|
||||||
"postage",
|
"postage",
|
||||||
"project",
|
"project",
|
||||||
|
|
|
@ -52,6 +52,7 @@ members = [
|
||||||
"crates/plugin",
|
"crates/plugin",
|
||||||
"crates/plugin_macros",
|
"crates/plugin_macros",
|
||||||
"crates/plugin_runtime",
|
"crates/plugin_runtime",
|
||||||
|
"crates/prettier",
|
||||||
"crates/project",
|
"crates/project",
|
||||||
"crates/project_panel",
|
"crates/project_panel",
|
||||||
"crates/project_symbols",
|
"crates/project_symbols",
|
||||||
|
|
|
@ -199,7 +199,12 @@
|
||||||
// "arguments": ["--stdin-filepath", "{buffer_path}"]
|
// "arguments": ["--stdin-filepath", "{buffer_path}"]
|
||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
"formatter": "language_server",
|
// 3. Format code using Zed's Prettier integration:
|
||||||
|
// "formatter": "prettier"
|
||||||
|
// 4. Default. Format files using Zed's Prettier integration (if applicable),
|
||||||
|
// or falling back to formatting via language server:
|
||||||
|
// "formatter": "auto"
|
||||||
|
"formatter": "auto",
|
||||||
// How to soft-wrap long lines of text. This setting can take
|
// How to soft-wrap long lines of text. This setting can take
|
||||||
// three values:
|
// three values:
|
||||||
//
|
//
|
||||||
|
@ -429,6 +434,16 @@
|
||||||
"tab_size": 2
|
"tab_size": 2
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
// Zed's Prettier integration settings.
|
||||||
|
// If Prettier is enabled, Zed will use this its Prettier instance for any applicable file, if
|
||||||
|
// project has no other Prettier installed.
|
||||||
|
"prettier": {
|
||||||
|
// Use regular Prettier json configuration:
|
||||||
|
// "trailingComma": "es5",
|
||||||
|
// "tabWidth": 4,
|
||||||
|
// "semi": false,
|
||||||
|
// "singleQuote": true
|
||||||
|
},
|
||||||
// LSP Specific settings.
|
// LSP Specific settings.
|
||||||
"lsp": {
|
"lsp": {
|
||||||
// Specify the LSP name as a key here.
|
// Specify the LSP name as a key here.
|
||||||
|
|
|
@ -72,6 +72,7 @@ fs = { path = "../fs", features = ["test-support"] }
|
||||||
git = { path = "../git", features = ["test-support"] }
|
git = { path = "../git", features = ["test-support"] }
|
||||||
live_kit_client = { path = "../live_kit_client", features = ["test-support"] }
|
live_kit_client = { path = "../live_kit_client", features = ["test-support"] }
|
||||||
lsp = { path = "../lsp", features = ["test-support"] }
|
lsp = { path = "../lsp", features = ["test-support"] }
|
||||||
|
node_runtime = { path = "../node_runtime" }
|
||||||
project = { path = "../project", features = ["test-support"] }
|
project = { path = "../project", features = ["test-support"] }
|
||||||
rpc = { path = "../rpc", features = ["test-support"] }
|
rpc = { path = "../rpc", features = ["test-support"] }
|
||||||
settings = { path = "../settings", features = ["test-support"] }
|
settings = { path = "../settings", features = ["test-support"] }
|
||||||
|
|
|
@ -15,12 +15,14 @@ use gpui::{executor::Deterministic, test::EmptyView, AppContext, ModelHandle, Te
|
||||||
use indoc::indoc;
|
use indoc::indoc;
|
||||||
use language::{
|
use language::{
|
||||||
language_settings::{AllLanguageSettings, Formatter, InlayHintSettings},
|
language_settings::{AllLanguageSettings, Formatter, InlayHintSettings},
|
||||||
tree_sitter_rust, Anchor, Diagnostic, DiagnosticEntry, FakeLspAdapter, Language,
|
tree_sitter_rust, Anchor, BundledFormatter, Diagnostic, DiagnosticEntry, FakeLspAdapter,
|
||||||
LanguageConfig, LineEnding, OffsetRangeExt, Point, Rope,
|
Language, LanguageConfig, LineEnding, OffsetRangeExt, Point, Rope,
|
||||||
};
|
};
|
||||||
use live_kit_client::MacOSDisplay;
|
use live_kit_client::MacOSDisplay;
|
||||||
use lsp::LanguageServerId;
|
use lsp::LanguageServerId;
|
||||||
use project::{search::SearchQuery, DiagnosticSummary, HoverBlockKind, Project, ProjectPath};
|
use project::{
|
||||||
|
search::SearchQuery, DiagnosticSummary, FormatTrigger, HoverBlockKind, Project, ProjectPath,
|
||||||
|
};
|
||||||
use rand::prelude::*;
|
use rand::prelude::*;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use settings::SettingsStore;
|
use settings::SettingsStore;
|
||||||
|
@ -4407,8 +4409,6 @@ async fn test_formatting_buffer(
|
||||||
cx_a: &mut TestAppContext,
|
cx_a: &mut TestAppContext,
|
||||||
cx_b: &mut TestAppContext,
|
cx_b: &mut TestAppContext,
|
||||||
) {
|
) {
|
||||||
use project::FormatTrigger;
|
|
||||||
|
|
||||||
let mut server = TestServer::start(&deterministic).await;
|
let mut server = TestServer::start(&deterministic).await;
|
||||||
let client_a = server.create_client(cx_a, "user_a").await;
|
let client_a = server.create_client(cx_a, "user_a").await;
|
||||||
let client_b = server.create_client(cx_b, "user_b").await;
|
let client_b = server.create_client(cx_b, "user_b").await;
|
||||||
|
@ -4511,6 +4511,134 @@ async fn test_formatting_buffer(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test(iterations = 10)]
|
||||||
|
async fn test_prettier_formatting_buffer(
|
||||||
|
deterministic: Arc<Deterministic>,
|
||||||
|
cx_a: &mut TestAppContext,
|
||||||
|
cx_b: &mut TestAppContext,
|
||||||
|
) {
|
||||||
|
let mut server = TestServer::start(&deterministic).await;
|
||||||
|
let client_a = server.create_client(cx_a, "user_a").await;
|
||||||
|
let client_b = server.create_client(cx_b, "user_b").await;
|
||||||
|
server
|
||||||
|
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
|
||||||
|
.await;
|
||||||
|
let active_call_a = cx_a.read(ActiveCall::global);
|
||||||
|
|
||||||
|
// Set up a fake language server.
|
||||||
|
let mut language = Language::new(
|
||||||
|
LanguageConfig {
|
||||||
|
name: "Rust".into(),
|
||||||
|
path_suffixes: vec!["rs".to_string()],
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
Some(tree_sitter_rust::language()),
|
||||||
|
);
|
||||||
|
let test_plugin = "test_plugin";
|
||||||
|
let mut fake_language_servers = language
|
||||||
|
.set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
|
||||||
|
enabled_formatters: vec![BundledFormatter::Prettier {
|
||||||
|
parser_name: Some("test_parser"),
|
||||||
|
plugin_names: vec![test_plugin],
|
||||||
|
}],
|
||||||
|
..Default::default()
|
||||||
|
}))
|
||||||
|
.await;
|
||||||
|
let language = Arc::new(language);
|
||||||
|
client_a.language_registry().add(Arc::clone(&language));
|
||||||
|
|
||||||
|
// Here we insert a fake tree with a directory that exists on disk. This is needed
|
||||||
|
// because later we'll invoke a command, which requires passing a working directory
|
||||||
|
// that points to a valid location on disk.
|
||||||
|
let directory = env::current_dir().unwrap();
|
||||||
|
let buffer_text = "let one = \"two\"";
|
||||||
|
client_a
|
||||||
|
.fs()
|
||||||
|
.insert_tree(&directory, json!({ "a.rs": buffer_text }))
|
||||||
|
.await;
|
||||||
|
let (project_a, worktree_id) = client_a.build_local_project(&directory, cx_a).await;
|
||||||
|
let prettier_format_suffix = project_a.update(cx_a, |project, _| {
|
||||||
|
let suffix = project.enable_test_prettier(&[test_plugin]);
|
||||||
|
project.languages().add(language);
|
||||||
|
suffix
|
||||||
|
});
|
||||||
|
let buffer_a = cx_a
|
||||||
|
.background()
|
||||||
|
.spawn(project_a.update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let project_id = active_call_a
|
||||||
|
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let project_b = client_b.build_remote_project(project_id, cx_b).await;
|
||||||
|
let buffer_b = cx_b
|
||||||
|
.background()
|
||||||
|
.spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
cx_a.update(|cx| {
|
||||||
|
cx.update_global(|store: &mut SettingsStore, cx| {
|
||||||
|
store.update_user_settings::<AllLanguageSettings>(cx, |file| {
|
||||||
|
file.defaults.formatter = Some(Formatter::Auto);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
cx_b.update(|cx| {
|
||||||
|
cx.update_global(|store: &mut SettingsStore, cx| {
|
||||||
|
store.update_user_settings::<AllLanguageSettings>(cx, |file| {
|
||||||
|
file.defaults.formatter = Some(Formatter::LanguageServer);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
let fake_language_server = fake_language_servers.next().await.unwrap();
|
||||||
|
fake_language_server.handle_request::<lsp::request::Formatting, _, _>(|_, _| async move {
|
||||||
|
panic!(
|
||||||
|
"Unexpected: prettier should be preferred since it's enabled and language supports it"
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
project_b
|
||||||
|
.update(cx_b, |project, cx| {
|
||||||
|
project.format(
|
||||||
|
HashSet::from_iter([buffer_b.clone()]),
|
||||||
|
true,
|
||||||
|
FormatTrigger::Save,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
cx_a.foreground().run_until_parked();
|
||||||
|
cx_b.foreground().run_until_parked();
|
||||||
|
assert_eq!(
|
||||||
|
buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
|
||||||
|
buffer_text.to_string() + "\n" + prettier_format_suffix,
|
||||||
|
"Prettier formatting was not applied to client buffer after client's request"
|
||||||
|
);
|
||||||
|
|
||||||
|
project_a
|
||||||
|
.update(cx_a, |project, cx| {
|
||||||
|
project.format(
|
||||||
|
HashSet::from_iter([buffer_a.clone()]),
|
||||||
|
true,
|
||||||
|
FormatTrigger::Manual,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
cx_a.foreground().run_until_parked();
|
||||||
|
cx_b.foreground().run_until_parked();
|
||||||
|
assert_eq!(
|
||||||
|
buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
|
||||||
|
buffer_text.to_string() + "\n" + prettier_format_suffix + "\n" + prettier_format_suffix,
|
||||||
|
"Prettier formatting was not applied to client buffer after host's request"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[gpui::test(iterations = 10)]
|
#[gpui::test(iterations = 10)]
|
||||||
async fn test_definition(
|
async fn test_definition(
|
||||||
deterministic: Arc<Deterministic>,
|
deterministic: Arc<Deterministic>,
|
||||||
|
|
|
@ -15,6 +15,7 @@ use fs::FakeFs;
|
||||||
use futures::{channel::oneshot, StreamExt as _};
|
use futures::{channel::oneshot, StreamExt as _};
|
||||||
use gpui::{executor::Deterministic, ModelHandle, Task, TestAppContext, WindowHandle};
|
use gpui::{executor::Deterministic, ModelHandle, Task, TestAppContext, WindowHandle};
|
||||||
use language::LanguageRegistry;
|
use language::LanguageRegistry;
|
||||||
|
use node_runtime::FakeNodeRuntime;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use project::{Project, WorktreeId};
|
use project::{Project, WorktreeId};
|
||||||
use rpc::RECEIVE_TIMEOUT;
|
use rpc::RECEIVE_TIMEOUT;
|
||||||
|
@ -218,6 +219,7 @@ impl TestServer {
|
||||||
build_window_options: |_, _, _| Default::default(),
|
build_window_options: |_, _, _| Default::default(),
|
||||||
initialize_workspace: |_, _, _, _| Task::ready(Ok(())),
|
initialize_workspace: |_, _, _, _| Task::ready(Ok(())),
|
||||||
background_actions: || &[],
|
background_actions: || &[],
|
||||||
|
node_runtime: FakeNodeRuntime::new(),
|
||||||
});
|
});
|
||||||
|
|
||||||
cx.update(|cx| {
|
cx.update(|cx| {
|
||||||
|
@ -567,6 +569,7 @@ impl TestClient {
|
||||||
cx.update(|cx| {
|
cx.update(|cx| {
|
||||||
Project::local(
|
Project::local(
|
||||||
self.client().clone(),
|
self.client().clone(),
|
||||||
|
self.app_state.node_runtime.clone(),
|
||||||
self.app_state.user_store.clone(),
|
self.app_state.user_store.clone(),
|
||||||
self.app_state.languages.clone(),
|
self.app_state.languages.clone(),
|
||||||
self.app_state.fs.clone(),
|
self.app_state.fs.clone(),
|
||||||
|
|
|
@ -19,8 +19,8 @@ use gpui::{
|
||||||
use indoc::indoc;
|
use indoc::indoc;
|
||||||
use language::{
|
use language::{
|
||||||
language_settings::{AllLanguageSettings, AllLanguageSettingsContent, LanguageSettingsContent},
|
language_settings::{AllLanguageSettings, AllLanguageSettingsContent, LanguageSettingsContent},
|
||||||
BracketPairConfig, FakeLspAdapter, LanguageConfig, LanguageConfigOverride, LanguageRegistry,
|
BracketPairConfig, BundledFormatter, FakeLspAdapter, LanguageConfig, LanguageConfigOverride,
|
||||||
Override, Point,
|
LanguageRegistry, Override, Point,
|
||||||
};
|
};
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use project::project_settings::{LspSettings, ProjectSettings};
|
use project::project_settings::{LspSettings, ProjectSettings};
|
||||||
|
@ -5076,7 +5076,9 @@ async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) {
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) {
|
async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) {
|
||||||
init_test(cx, |_| {});
|
init_test(cx, |settings| {
|
||||||
|
settings.defaults.formatter = Some(language_settings::Formatter::LanguageServer)
|
||||||
|
});
|
||||||
|
|
||||||
let mut language = Language::new(
|
let mut language = Language::new(
|
||||||
LanguageConfig {
|
LanguageConfig {
|
||||||
|
@ -5092,6 +5094,12 @@ async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) {
|
||||||
document_formatting_provider: Some(lsp::OneOf::Left(true)),
|
document_formatting_provider: Some(lsp::OneOf::Left(true)),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
|
// Enable Prettier formatting for the same buffer, and ensure
|
||||||
|
// LSP is called instead of Prettier.
|
||||||
|
enabled_formatters: vec![BundledFormatter::Prettier {
|
||||||
|
parser_name: Some("test_parser"),
|
||||||
|
plugin_names: Vec::new(),
|
||||||
|
}],
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}))
|
}))
|
||||||
.await;
|
.await;
|
||||||
|
@ -5100,7 +5108,10 @@ async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) {
|
||||||
fs.insert_file("/file.rs", Default::default()).await;
|
fs.insert_file("/file.rs", Default::default()).await;
|
||||||
|
|
||||||
let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
|
let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
|
||||||
project.update(cx, |project, _| project.languages().add(Arc::new(language)));
|
project.update(cx, |project, _| {
|
||||||
|
project.enable_test_prettier(&[]);
|
||||||
|
project.languages().add(Arc::new(language));
|
||||||
|
});
|
||||||
let buffer = project
|
let buffer = project
|
||||||
.update(cx, |project, cx| project.open_local_buffer("/file.rs", cx))
|
.update(cx, |project, cx| project.open_local_buffer("/file.rs", cx))
|
||||||
.await
|
.await
|
||||||
|
@ -5218,7 +5229,9 @@ async fn test_concurrent_format_requests(cx: &mut gpui::TestAppContext) {
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_strip_whitespace_and_format_via_lsp(cx: &mut gpui::TestAppContext) {
|
async fn test_strip_whitespace_and_format_via_lsp(cx: &mut gpui::TestAppContext) {
|
||||||
init_test(cx, |_| {});
|
init_test(cx, |settings| {
|
||||||
|
settings.defaults.formatter = Some(language_settings::Formatter::Auto)
|
||||||
|
});
|
||||||
|
|
||||||
let mut cx = EditorLspTestContext::new_rust(
|
let mut cx = EditorLspTestContext::new_rust(
|
||||||
lsp::ServerCapabilities {
|
lsp::ServerCapabilities {
|
||||||
|
@ -7815,6 +7828,75 @@ async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui:
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_document_format_with_prettier(cx: &mut gpui::TestAppContext) {
|
||||||
|
init_test(cx, |settings| {
|
||||||
|
settings.defaults.formatter = Some(language_settings::Formatter::Prettier)
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut language = Language::new(
|
||||||
|
LanguageConfig {
|
||||||
|
name: "Rust".into(),
|
||||||
|
path_suffixes: vec!["rs".to_string()],
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
Some(tree_sitter_rust::language()),
|
||||||
|
);
|
||||||
|
|
||||||
|
let test_plugin = "test_plugin";
|
||||||
|
let _ = language
|
||||||
|
.set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
|
||||||
|
enabled_formatters: vec![BundledFormatter::Prettier {
|
||||||
|
parser_name: Some("test_parser"),
|
||||||
|
plugin_names: vec![test_plugin],
|
||||||
|
}],
|
||||||
|
..Default::default()
|
||||||
|
}))
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let fs = FakeFs::new(cx.background());
|
||||||
|
fs.insert_file("/file.rs", Default::default()).await;
|
||||||
|
|
||||||
|
let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
|
||||||
|
let prettier_format_suffix = project.update(cx, |project, _| {
|
||||||
|
let suffix = project.enable_test_prettier(&[test_plugin]);
|
||||||
|
project.languages().add(Arc::new(language));
|
||||||
|
suffix
|
||||||
|
});
|
||||||
|
let buffer = project
|
||||||
|
.update(cx, |project, cx| project.open_local_buffer("/file.rs", cx))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let buffer_text = "one\ntwo\nthree\n";
|
||||||
|
let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
|
||||||
|
let editor = cx.add_window(|cx| build_editor(buffer, cx)).root(cx);
|
||||||
|
editor.update(cx, |editor, cx| editor.set_text(buffer_text, cx));
|
||||||
|
|
||||||
|
let format = editor.update(cx, |editor, cx| {
|
||||||
|
editor.perform_format(project.clone(), FormatTrigger::Manual, cx)
|
||||||
|
});
|
||||||
|
format.await.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
editor.read_with(cx, |editor, cx| editor.text(cx)),
|
||||||
|
buffer_text.to_string() + prettier_format_suffix,
|
||||||
|
"Test prettier formatting was not applied to the original buffer text",
|
||||||
|
);
|
||||||
|
|
||||||
|
update_test_language_settings(cx, |settings| {
|
||||||
|
settings.defaults.formatter = Some(language_settings::Formatter::Auto)
|
||||||
|
});
|
||||||
|
let format = editor.update(cx, |editor, cx| {
|
||||||
|
editor.perform_format(project.clone(), FormatTrigger::Manual, cx)
|
||||||
|
});
|
||||||
|
format.await.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
editor.read_with(cx, |editor, cx| editor.text(cx)),
|
||||||
|
buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
|
||||||
|
"Autoformatting (via test prettier) was not applied to the original buffer text",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
|
fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
|
||||||
let point = DisplayPoint::new(row as u32, column as u32);
|
let point = DisplayPoint::new(row as u32, column as u32);
|
||||||
point..point
|
point..point
|
||||||
|
|
|
@ -85,7 +85,7 @@ pub struct RemoveOptions {
|
||||||
pub ignore_if_not_exists: bool,
|
pub ignore_if_not_exists: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Copy, Clone, Debug)]
|
||||||
pub struct Metadata {
|
pub struct Metadata {
|
||||||
pub inode: u64,
|
pub inode: u64,
|
||||||
pub mtime: SystemTime,
|
pub mtime: SystemTime,
|
||||||
|
|
|
@ -227,6 +227,10 @@ impl CachedLspAdapter {
|
||||||
) -> Option<CodeLabel> {
|
) -> Option<CodeLabel> {
|
||||||
self.adapter.label_for_symbol(name, kind, language).await
|
self.adapter.label_for_symbol(name, kind, language).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn enabled_formatters(&self) -> Vec<BundledFormatter> {
|
||||||
|
self.adapter.enabled_formatters()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait LspAdapterDelegate: Send + Sync {
|
pub trait LspAdapterDelegate: Send + Sync {
|
||||||
|
@ -333,6 +337,33 @@ pub trait LspAdapter: 'static + Send + Sync {
|
||||||
async fn language_ids(&self) -> HashMap<String, String> {
|
async fn language_ids(&self) -> HashMap<String, String> {
|
||||||
Default::default()
|
Default::default()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn enabled_formatters(&self) -> Vec<BundledFormatter> {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub enum BundledFormatter {
|
||||||
|
Prettier {
|
||||||
|
// See https://prettier.io/docs/en/options.html#parser for a list of valid values.
|
||||||
|
// Usually, every language has a single parser (standard or plugin-provided), hence `Some("parser_name")` can be used.
|
||||||
|
// There can not be multiple parsers for a single language, in case of a conflict, we would attempt to select the one with most plugins.
|
||||||
|
//
|
||||||
|
// But exceptions like Tailwind CSS exist, which uses standard parsers for CSS/JS/HTML/etc. but require an extra plugin to be installed.
|
||||||
|
// For those cases, `None` will install the plugin but apply other, regular parser defined for the language, and this would not be a conflict.
|
||||||
|
parser_name: Option<&'static str>,
|
||||||
|
plugin_names: Vec<&'static str>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BundledFormatter {
|
||||||
|
pub fn prettier(parser_name: &'static str) -> Self {
|
||||||
|
Self::Prettier {
|
||||||
|
parser_name: Some(parser_name),
|
||||||
|
plugin_names: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
@ -467,6 +498,7 @@ pub struct FakeLspAdapter {
|
||||||
pub initializer: Option<Box<dyn 'static + Send + Sync + Fn(&mut lsp::FakeLanguageServer)>>,
|
pub initializer: Option<Box<dyn 'static + Send + Sync + Fn(&mut lsp::FakeLanguageServer)>>,
|
||||||
pub disk_based_diagnostics_progress_token: Option<String>,
|
pub disk_based_diagnostics_progress_token: Option<String>,
|
||||||
pub disk_based_diagnostics_sources: Vec<String>,
|
pub disk_based_diagnostics_sources: Vec<String>,
|
||||||
|
pub enabled_formatters: Vec<BundledFormatter>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default)]
|
#[derive(Clone, Debug, Default)]
|
||||||
|
@ -1729,6 +1761,7 @@ impl Default for FakeLspAdapter {
|
||||||
disk_based_diagnostics_progress_token: None,
|
disk_based_diagnostics_progress_token: None,
|
||||||
initialization_options: None,
|
initialization_options: None,
|
||||||
disk_based_diagnostics_sources: Vec::new(),
|
disk_based_diagnostics_sources: Vec::new(),
|
||||||
|
enabled_formatters: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1785,6 +1818,10 @@ impl LspAdapter for Arc<FakeLspAdapter> {
|
||||||
async fn initialization_options(&self) -> Option<Value> {
|
async fn initialization_options(&self) -> Option<Value> {
|
||||||
self.initialization_options.clone()
|
self.initialization_options.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn enabled_formatters(&self) -> Vec<BundledFormatter> {
|
||||||
|
self.enabled_formatters.clone()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_capture_indices(query: &Query, captures: &mut [(&str, &mut Option<u32>)]) {
|
fn get_capture_indices(query: &Query, captures: &mut [(&str, &mut Option<u32>)]) {
|
||||||
|
|
|
@ -50,6 +50,7 @@ pub struct LanguageSettings {
|
||||||
pub remove_trailing_whitespace_on_save: bool,
|
pub remove_trailing_whitespace_on_save: bool,
|
||||||
pub ensure_final_newline_on_save: bool,
|
pub ensure_final_newline_on_save: bool,
|
||||||
pub formatter: Formatter,
|
pub formatter: Formatter,
|
||||||
|
pub prettier: HashMap<String, serde_json::Value>,
|
||||||
pub enable_language_server: bool,
|
pub enable_language_server: bool,
|
||||||
pub show_copilot_suggestions: bool,
|
pub show_copilot_suggestions: bool,
|
||||||
pub show_whitespaces: ShowWhitespaceSetting,
|
pub show_whitespaces: ShowWhitespaceSetting,
|
||||||
|
@ -98,6 +99,8 @@ pub struct LanguageSettingsContent {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub formatter: Option<Formatter>,
|
pub formatter: Option<Formatter>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
pub prettier: Option<HashMap<String, serde_json::Value>>,
|
||||||
|
#[serde(default)]
|
||||||
pub enable_language_server: Option<bool>,
|
pub enable_language_server: Option<bool>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub show_copilot_suggestions: Option<bool>,
|
pub show_copilot_suggestions: Option<bool>,
|
||||||
|
@ -149,10 +152,13 @@ pub enum ShowWhitespaceSetting {
|
||||||
All,
|
All,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||||
#[serde(rename_all = "snake_case")]
|
#[serde(rename_all = "snake_case")]
|
||||||
pub enum Formatter {
|
pub enum Formatter {
|
||||||
|
#[default]
|
||||||
|
Auto,
|
||||||
LanguageServer,
|
LanguageServer,
|
||||||
|
Prettier,
|
||||||
External {
|
External {
|
||||||
command: Arc<str>,
|
command: Arc<str>,
|
||||||
arguments: Arc<[String]>,
|
arguments: Arc<[String]>,
|
||||||
|
@ -392,6 +398,7 @@ fn merge_settings(settings: &mut LanguageSettings, src: &LanguageSettingsContent
|
||||||
src.preferred_line_length,
|
src.preferred_line_length,
|
||||||
);
|
);
|
||||||
merge(&mut settings.formatter, src.formatter.clone());
|
merge(&mut settings.formatter, src.formatter.clone());
|
||||||
|
merge(&mut settings.prettier, src.prettier.clone());
|
||||||
merge(&mut settings.format_on_save, src.format_on_save.clone());
|
merge(&mut settings.format_on_save, src.format_on_save.clone());
|
||||||
merge(
|
merge(
|
||||||
&mut settings.remove_trailing_whitespace_on_save,
|
&mut settings.remove_trailing_whitespace_on_save,
|
||||||
|
|
|
@ -220,29 +220,129 @@ impl NodeRuntime for RealNodeRuntime {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct FakeNodeRuntime;
|
pub struct FakeNodeRuntime(Option<PrettierSupport>);
|
||||||
|
|
||||||
|
struct PrettierSupport {
|
||||||
|
plugins: Vec<&'static str>,
|
||||||
|
}
|
||||||
|
|
||||||
impl FakeNodeRuntime {
|
impl FakeNodeRuntime {
|
||||||
pub fn new() -> Arc<dyn NodeRuntime> {
|
pub fn new() -> Arc<dyn NodeRuntime> {
|
||||||
Arc::new(FakeNodeRuntime)
|
Arc::new(FakeNodeRuntime(None))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_prettier_support(plugins: &[&'static str]) -> Arc<dyn NodeRuntime> {
|
||||||
|
Arc::new(FakeNodeRuntime(Some(PrettierSupport::new(plugins))))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
impl NodeRuntime for FakeNodeRuntime {
|
impl NodeRuntime for FakeNodeRuntime {
|
||||||
async fn binary_path(&self) -> Result<PathBuf> {
|
async fn binary_path(&self) -> anyhow::Result<PathBuf> {
|
||||||
unreachable!()
|
if let Some(prettier_support) = &self.0 {
|
||||||
|
prettier_support.binary_path().await
|
||||||
|
} else {
|
||||||
|
unreachable!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn run_npm_subcommand(
|
||||||
|
&self,
|
||||||
|
directory: Option<&Path>,
|
||||||
|
subcommand: &str,
|
||||||
|
args: &[&str],
|
||||||
|
) -> anyhow::Result<Output> {
|
||||||
|
if let Some(prettier_support) = &self.0 {
|
||||||
|
prettier_support
|
||||||
|
.run_npm_subcommand(directory, subcommand, args)
|
||||||
|
.await
|
||||||
|
} else {
|
||||||
|
unreachable!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn npm_package_latest_version(&self, name: &str) -> anyhow::Result<String> {
|
||||||
|
if let Some(prettier_support) = &self.0 {
|
||||||
|
prettier_support.npm_package_latest_version(name).await
|
||||||
|
} else {
|
||||||
|
unreachable!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn npm_install_packages(
|
||||||
|
&self,
|
||||||
|
directory: &Path,
|
||||||
|
packages: &[(&str, &str)],
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
if let Some(prettier_support) = &self.0 {
|
||||||
|
prettier_support
|
||||||
|
.npm_install_packages(directory, packages)
|
||||||
|
.await
|
||||||
|
} else {
|
||||||
|
unreachable!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PrettierSupport {
|
||||||
|
const PACKAGE_VERSION: &str = "0.0.1";
|
||||||
|
|
||||||
|
fn new(plugins: &[&'static str]) -> Self {
|
||||||
|
Self {
|
||||||
|
plugins: plugins.to_vec(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl NodeRuntime for PrettierSupport {
|
||||||
|
async fn binary_path(&self) -> anyhow::Result<PathBuf> {
|
||||||
|
Ok(PathBuf::from("prettier_fake_node"))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn run_npm_subcommand(&self, _: Option<&Path>, _: &str, _: &[&str]) -> Result<Output> {
|
async fn run_npm_subcommand(&self, _: Option<&Path>, _: &str, _: &[&str]) -> Result<Output> {
|
||||||
unreachable!()
|
unreachable!()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn npm_package_latest_version(&self, _: &str) -> Result<String> {
|
async fn npm_package_latest_version(&self, name: &str) -> anyhow::Result<String> {
|
||||||
unreachable!()
|
if name == "prettier" || self.plugins.contains(&name) {
|
||||||
|
Ok(Self::PACKAGE_VERSION.to_string())
|
||||||
|
} else {
|
||||||
|
panic!("Unexpected package name: {name}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn npm_install_packages(&self, _: &Path, _: &[(&str, &str)]) -> Result<()> {
|
async fn npm_install_packages(
|
||||||
unreachable!()
|
&self,
|
||||||
|
_: &Path,
|
||||||
|
packages: &[(&str, &str)],
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
assert_eq!(
|
||||||
|
packages.len(),
|
||||||
|
self.plugins.len() + 1,
|
||||||
|
"Unexpected packages length to install: {:?}, expected `prettier` + {:?}",
|
||||||
|
packages,
|
||||||
|
self.plugins
|
||||||
|
);
|
||||||
|
for (name, version) in packages {
|
||||||
|
assert!(
|
||||||
|
name == &"prettier" || self.plugins.contains(name),
|
||||||
|
"Unexpected package `{}` to install in packages {:?}, expected {} for `prettier` + {:?}",
|
||||||
|
name,
|
||||||
|
packages,
|
||||||
|
Self::PACKAGE_VERSION,
|
||||||
|
self.plugins
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
version,
|
||||||
|
&Self::PACKAGE_VERSION,
|
||||||
|
"Unexpected package version `{}` to install in packages {:?}, expected {} for `prettier` + {:?}",
|
||||||
|
version,
|
||||||
|
packages,
|
||||||
|
Self::PACKAGE_VERSION,
|
||||||
|
self.plugins
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
34
crates/prettier/Cargo.toml
Normal file
34
crates/prettier/Cargo.toml
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
[package]
|
||||||
|
name = "prettier"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
publish = false
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
path = "src/prettier.rs"
|
||||||
|
doctest = false
|
||||||
|
|
||||||
|
[features]
|
||||||
|
test-support = []
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
client = { path = "../client" }
|
||||||
|
collections = { path = "../collections"}
|
||||||
|
language = { path = "../language" }
|
||||||
|
gpui = { path = "../gpui" }
|
||||||
|
fs = { path = "../fs" }
|
||||||
|
lsp = { path = "../lsp" }
|
||||||
|
node_runtime = { path = "../node_runtime"}
|
||||||
|
util = { path = "../util" }
|
||||||
|
|
||||||
|
log.workspace = true
|
||||||
|
serde.workspace = true
|
||||||
|
serde_derive.workspace = true
|
||||||
|
serde_json.workspace = true
|
||||||
|
anyhow.workspace = true
|
||||||
|
futures.workspace = true
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
language = { path = "../language", features = ["test-support"] }
|
||||||
|
gpui = { path = "../gpui", features = ["test-support"] }
|
||||||
|
fs = { path = "../fs", features = ["test-support"] }
|
513
crates/prettier/src/prettier.rs
Normal file
513
crates/prettier/src/prettier.rs
Normal file
|
@ -0,0 +1,513 @@
|
||||||
|
use std::collections::VecDeque;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use anyhow::Context;
|
||||||
|
use collections::{HashMap, HashSet};
|
||||||
|
use fs::Fs;
|
||||||
|
use gpui::{AsyncAppContext, ModelHandle};
|
||||||
|
use language::language_settings::language_settings;
|
||||||
|
use language::{Buffer, BundledFormatter, Diff};
|
||||||
|
use lsp::{LanguageServer, LanguageServerId};
|
||||||
|
use node_runtime::NodeRuntime;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use util::paths::DEFAULT_PRETTIER_DIR;
|
||||||
|
|
||||||
|
pub enum Prettier {
|
||||||
|
Real(RealPrettier),
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
Test(TestPrettier),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct RealPrettier {
|
||||||
|
worktree_id: Option<usize>,
|
||||||
|
default: bool,
|
||||||
|
prettier_dir: PathBuf,
|
||||||
|
server: Arc<LanguageServer>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
pub struct TestPrettier {
|
||||||
|
worktree_id: Option<usize>,
|
||||||
|
prettier_dir: PathBuf,
|
||||||
|
default: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct LocateStart {
|
||||||
|
pub worktree_root_path: Arc<Path>,
|
||||||
|
pub starting_path: Arc<Path>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const PRETTIER_SERVER_FILE: &str = "prettier_server.js";
|
||||||
|
pub const PRETTIER_SERVER_JS: &str = include_str!("./prettier_server.js");
|
||||||
|
const PRETTIER_PACKAGE_NAME: &str = "prettier";
|
||||||
|
const TAILWIND_PRETTIER_PLUGIN_PACKAGE_NAME: &str = "prettier-plugin-tailwindcss";
|
||||||
|
|
||||||
|
impl Prettier {
|
||||||
|
pub const CONFIG_FILE_NAMES: &'static [&'static str] = &[
|
||||||
|
".prettierrc",
|
||||||
|
".prettierrc.json",
|
||||||
|
".prettierrc.json5",
|
||||||
|
".prettierrc.yaml",
|
||||||
|
".prettierrc.yml",
|
||||||
|
".prettierrc.toml",
|
||||||
|
".prettierrc.js",
|
||||||
|
".prettierrc.cjs",
|
||||||
|
"package.json",
|
||||||
|
"prettier.config.js",
|
||||||
|
"prettier.config.cjs",
|
||||||
|
".editorconfig",
|
||||||
|
];
|
||||||
|
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
pub const FORMAT_SUFFIX: &str = "\nformatted by test prettier";
|
||||||
|
|
||||||
|
pub async fn locate(
|
||||||
|
starting_path: Option<LocateStart>,
|
||||||
|
fs: Arc<dyn Fs>,
|
||||||
|
) -> anyhow::Result<PathBuf> {
|
||||||
|
let paths_to_check = match starting_path.as_ref() {
|
||||||
|
Some(starting_path) => {
|
||||||
|
let worktree_root = starting_path
|
||||||
|
.worktree_root_path
|
||||||
|
.components()
|
||||||
|
.into_iter()
|
||||||
|
.take_while(|path_component| {
|
||||||
|
path_component.as_os_str().to_string_lossy() != "node_modules"
|
||||||
|
})
|
||||||
|
.collect::<PathBuf>();
|
||||||
|
|
||||||
|
if worktree_root != starting_path.worktree_root_path.as_ref() {
|
||||||
|
vec![worktree_root]
|
||||||
|
} else {
|
||||||
|
let (worktree_root_metadata, start_path_metadata) = if starting_path
|
||||||
|
.starting_path
|
||||||
|
.as_ref()
|
||||||
|
== Path::new("")
|
||||||
|
{
|
||||||
|
let worktree_root_data =
|
||||||
|
fs.metadata(&worktree_root).await.with_context(|| {
|
||||||
|
format!(
|
||||||
|
"FS metadata fetch for worktree root path {worktree_root:?}",
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
(worktree_root_data.unwrap_or_else(|| {
|
||||||
|
panic!("cannot query prettier for non existing worktree root at {worktree_root_data:?}")
|
||||||
|
}), None)
|
||||||
|
} else {
|
||||||
|
let full_starting_path = worktree_root.join(&starting_path.starting_path);
|
||||||
|
let (worktree_root_data, start_path_data) = futures::try_join!(
|
||||||
|
fs.metadata(&worktree_root),
|
||||||
|
fs.metadata(&full_starting_path),
|
||||||
|
)
|
||||||
|
.with_context(|| {
|
||||||
|
format!("FS metadata fetch for starting path {full_starting_path:?}",)
|
||||||
|
})?;
|
||||||
|
(
|
||||||
|
worktree_root_data.unwrap_or_else(|| {
|
||||||
|
panic!("cannot query prettier for non existing worktree root at {worktree_root_data:?}")
|
||||||
|
}),
|
||||||
|
start_path_data,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
match start_path_metadata {
|
||||||
|
Some(start_path_metadata) => {
|
||||||
|
anyhow::ensure!(worktree_root_metadata.is_dir,
|
||||||
|
"For non-empty start path, worktree root {starting_path:?} should be a directory");
|
||||||
|
anyhow::ensure!(
|
||||||
|
!start_path_metadata.is_dir,
|
||||||
|
"For non-empty start path, it should not be a directory {starting_path:?}"
|
||||||
|
);
|
||||||
|
anyhow::ensure!(
|
||||||
|
!start_path_metadata.is_symlink,
|
||||||
|
"For non-empty start path, it should not be a symlink {starting_path:?}"
|
||||||
|
);
|
||||||
|
|
||||||
|
let file_to_format = starting_path.starting_path.as_ref();
|
||||||
|
let mut paths_to_check = VecDeque::from(vec![worktree_root.clone()]);
|
||||||
|
let mut current_path = worktree_root;
|
||||||
|
for path_component in file_to_format.components().into_iter() {
|
||||||
|
current_path = current_path.join(path_component);
|
||||||
|
paths_to_check.push_front(current_path.clone());
|
||||||
|
if path_component.as_os_str().to_string_lossy() == "node_modules" {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
paths_to_check.pop_front(); // last one is the file itself or node_modules, skip it
|
||||||
|
Vec::from(paths_to_check)
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
anyhow::ensure!(
|
||||||
|
!worktree_root_metadata.is_dir,
|
||||||
|
"For empty start path, worktree root should not be a directory {starting_path:?}"
|
||||||
|
);
|
||||||
|
anyhow::ensure!(
|
||||||
|
!worktree_root_metadata.is_symlink,
|
||||||
|
"For empty start path, worktree root should not be a symlink {starting_path:?}"
|
||||||
|
);
|
||||||
|
worktree_root
|
||||||
|
.parent()
|
||||||
|
.map(|path| vec![path.to_path_buf()])
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => Vec::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
match find_closest_prettier_dir(paths_to_check, fs.as_ref())
|
||||||
|
.await
|
||||||
|
.with_context(|| format!("finding prettier starting with {starting_path:?}"))?
|
||||||
|
{
|
||||||
|
Some(prettier_dir) => Ok(prettier_dir),
|
||||||
|
None => Ok(DEFAULT_PRETTIER_DIR.to_path_buf()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
pub async fn start(
|
||||||
|
worktree_id: Option<usize>,
|
||||||
|
_: LanguageServerId,
|
||||||
|
prettier_dir: PathBuf,
|
||||||
|
_: Arc<dyn NodeRuntime>,
|
||||||
|
_: AsyncAppContext,
|
||||||
|
) -> anyhow::Result<Self> {
|
||||||
|
Ok(
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
Self::Test(TestPrettier {
|
||||||
|
worktree_id,
|
||||||
|
default: prettier_dir == DEFAULT_PRETTIER_DIR.as_path(),
|
||||||
|
prettier_dir,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(any(test, feature = "test-support")))]
|
||||||
|
pub async fn start(
|
||||||
|
worktree_id: Option<usize>,
|
||||||
|
server_id: LanguageServerId,
|
||||||
|
prettier_dir: PathBuf,
|
||||||
|
node: Arc<dyn NodeRuntime>,
|
||||||
|
cx: AsyncAppContext,
|
||||||
|
) -> anyhow::Result<Self> {
|
||||||
|
use lsp::LanguageServerBinary;
|
||||||
|
|
||||||
|
let backgroud = cx.background();
|
||||||
|
anyhow::ensure!(
|
||||||
|
prettier_dir.is_dir(),
|
||||||
|
"Prettier dir {prettier_dir:?} is not a directory"
|
||||||
|
);
|
||||||
|
let prettier_server = DEFAULT_PRETTIER_DIR.join(PRETTIER_SERVER_FILE);
|
||||||
|
anyhow::ensure!(
|
||||||
|
prettier_server.is_file(),
|
||||||
|
"no prettier server package found at {prettier_server:?}"
|
||||||
|
);
|
||||||
|
|
||||||
|
let node_path = backgroud
|
||||||
|
.spawn(async move { node.binary_path().await })
|
||||||
|
.await?;
|
||||||
|
let server = LanguageServer::new(
|
||||||
|
server_id,
|
||||||
|
LanguageServerBinary {
|
||||||
|
path: node_path,
|
||||||
|
arguments: vec![prettier_server.into(), prettier_dir.as_path().into()],
|
||||||
|
},
|
||||||
|
Path::new("/"),
|
||||||
|
None,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
.context("prettier server creation")?;
|
||||||
|
let server = backgroud
|
||||||
|
.spawn(server.initialize(None))
|
||||||
|
.await
|
||||||
|
.context("prettier server initialization")?;
|
||||||
|
Ok(Self::Real(RealPrettier {
|
||||||
|
worktree_id,
|
||||||
|
server,
|
||||||
|
default: prettier_dir == DEFAULT_PRETTIER_DIR.as_path(),
|
||||||
|
prettier_dir,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn format(
|
||||||
|
&self,
|
||||||
|
buffer: &ModelHandle<Buffer>,
|
||||||
|
buffer_path: Option<PathBuf>,
|
||||||
|
cx: &AsyncAppContext,
|
||||||
|
) -> anyhow::Result<Diff> {
|
||||||
|
match self {
|
||||||
|
Self::Real(local) => {
|
||||||
|
let params = buffer.read_with(cx, |buffer, cx| {
|
||||||
|
let buffer_language = buffer.language();
|
||||||
|
let parsers_with_plugins = buffer_language
|
||||||
|
.into_iter()
|
||||||
|
.flat_map(|language| {
|
||||||
|
language
|
||||||
|
.lsp_adapters()
|
||||||
|
.iter()
|
||||||
|
.flat_map(|adapter| adapter.enabled_formatters())
|
||||||
|
.filter_map(|formatter| match formatter {
|
||||||
|
BundledFormatter::Prettier {
|
||||||
|
parser_name,
|
||||||
|
plugin_names,
|
||||||
|
} => Some((parser_name, plugin_names)),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.fold(
|
||||||
|
HashMap::default(),
|
||||||
|
|mut parsers_with_plugins, (parser_name, plugins)| {
|
||||||
|
match parser_name {
|
||||||
|
Some(parser_name) => parsers_with_plugins
|
||||||
|
.entry(parser_name)
|
||||||
|
.or_insert_with(HashSet::default)
|
||||||
|
.extend(plugins),
|
||||||
|
None => parsers_with_plugins.values_mut().for_each(|existing_plugins| {
|
||||||
|
existing_plugins.extend(plugins.iter());
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
parsers_with_plugins
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
let selected_parser_with_plugins = parsers_with_plugins.iter().max_by_key(|(_, plugins)| plugins.len());
|
||||||
|
if parsers_with_plugins.len() > 1 {
|
||||||
|
log::warn!("Found multiple parsers with plugins {parsers_with_plugins:?}, will select only one: {selected_parser_with_plugins:?}");
|
||||||
|
}
|
||||||
|
|
||||||
|
let prettier_node_modules = self.prettier_dir().join("node_modules");
|
||||||
|
anyhow::ensure!(prettier_node_modules.is_dir(), "Prettier node_modules dir does not exist: {prettier_node_modules:?}");
|
||||||
|
let plugin_name_into_path = |plugin_name: &str| {
|
||||||
|
let prettier_plugin_dir = prettier_node_modules.join(plugin_name);
|
||||||
|
for possible_plugin_path in [
|
||||||
|
prettier_plugin_dir.join("dist").join("index.mjs"),
|
||||||
|
prettier_plugin_dir.join("dist").join("index.js"),
|
||||||
|
prettier_plugin_dir.join("dist").join("plugin.js"),
|
||||||
|
prettier_plugin_dir.join("index.mjs"),
|
||||||
|
prettier_plugin_dir.join("index.js"),
|
||||||
|
prettier_plugin_dir.join("plugin.js"),
|
||||||
|
prettier_plugin_dir,
|
||||||
|
] {
|
||||||
|
if possible_plugin_path.is_file() {
|
||||||
|
return Some(possible_plugin_path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
};
|
||||||
|
let (parser, located_plugins) = match selected_parser_with_plugins {
|
||||||
|
Some((parser, plugins)) => {
|
||||||
|
// Tailwind plugin requires being added last
|
||||||
|
// https://github.com/tailwindlabs/prettier-plugin-tailwindcss#compatibility-with-other-prettier-plugins
|
||||||
|
let mut add_tailwind_back = false;
|
||||||
|
|
||||||
|
let mut plugins = plugins.into_iter().filter(|&&plugin_name| {
|
||||||
|
if plugin_name == TAILWIND_PRETTIER_PLUGIN_PACKAGE_NAME {
|
||||||
|
add_tailwind_back = true;
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}).map(|plugin_name| (plugin_name, plugin_name_into_path(plugin_name))).collect::<Vec<_>>();
|
||||||
|
if add_tailwind_back {
|
||||||
|
plugins.push((&TAILWIND_PRETTIER_PLUGIN_PACKAGE_NAME, plugin_name_into_path(TAILWIND_PRETTIER_PLUGIN_PACKAGE_NAME)));
|
||||||
|
}
|
||||||
|
(Some(parser.to_string()), plugins)
|
||||||
|
},
|
||||||
|
None => (None, Vec::new()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let prettier_options = if self.is_default() {
|
||||||
|
let language_settings = language_settings(buffer_language, buffer.file(), cx);
|
||||||
|
let mut options = language_settings.prettier.clone();
|
||||||
|
if !options.contains_key("tabWidth") {
|
||||||
|
options.insert(
|
||||||
|
"tabWidth".to_string(),
|
||||||
|
serde_json::Value::Number(serde_json::Number::from(
|
||||||
|
language_settings.tab_size.get(),
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if !options.contains_key("printWidth") {
|
||||||
|
options.insert(
|
||||||
|
"printWidth".to_string(),
|
||||||
|
serde_json::Value::Number(serde_json::Number::from(
|
||||||
|
language_settings.preferred_line_length,
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Some(options)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let plugins = located_plugins.into_iter().filter_map(|(plugin_name, located_plugin_path)| {
|
||||||
|
match located_plugin_path {
|
||||||
|
Some(path) => Some(path),
|
||||||
|
None => {
|
||||||
|
log::error!("Have not found plugin path for {plugin_name:?} inside {prettier_node_modules:?}");
|
||||||
|
None},
|
||||||
|
}
|
||||||
|
}).collect();
|
||||||
|
log::debug!("Formatting file {:?} with prettier, plugins :{plugins:?}, options: {prettier_options:?}", buffer.file().map(|f| f.full_path(cx)));
|
||||||
|
|
||||||
|
anyhow::Ok(FormatParams {
|
||||||
|
text: buffer.text(),
|
||||||
|
options: FormatOptions {
|
||||||
|
parser,
|
||||||
|
plugins,
|
||||||
|
path: buffer_path,
|
||||||
|
prettier_options,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}).context("prettier params calculation")?;
|
||||||
|
let response = local
|
||||||
|
.server
|
||||||
|
.request::<Format>(params)
|
||||||
|
.await
|
||||||
|
.context("prettier format request")?;
|
||||||
|
let diff_task = buffer.read_with(cx, |buffer, cx| buffer.diff(response.text, cx));
|
||||||
|
Ok(diff_task.await)
|
||||||
|
}
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
Self::Test(_) => Ok(buffer
|
||||||
|
.read_with(cx, |buffer, cx| {
|
||||||
|
let formatted_text = buffer.text() + Self::FORMAT_SUFFIX;
|
||||||
|
buffer.diff(formatted_text, cx)
|
||||||
|
})
|
||||||
|
.await),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn clear_cache(&self) -> anyhow::Result<()> {
|
||||||
|
match self {
|
||||||
|
Self::Real(local) => local
|
||||||
|
.server
|
||||||
|
.request::<ClearCache>(())
|
||||||
|
.await
|
||||||
|
.context("prettier clear cache"),
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
Self::Test(_) => Ok(()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn server(&self) -> Option<&Arc<LanguageServer>> {
|
||||||
|
match self {
|
||||||
|
Self::Real(local) => Some(&local.server),
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
Self::Test(_) => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_default(&self) -> bool {
|
||||||
|
match self {
|
||||||
|
Self::Real(local) => local.default,
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
Self::Test(test_prettier) => test_prettier.default,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn prettier_dir(&self) -> &Path {
|
||||||
|
match self {
|
||||||
|
Self::Real(local) => &local.prettier_dir,
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
Self::Test(test_prettier) => &test_prettier.prettier_dir,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn worktree_id(&self) -> Option<usize> {
|
||||||
|
match self {
|
||||||
|
Self::Real(local) => local.worktree_id,
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
Self::Test(test_prettier) => test_prettier.worktree_id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn find_closest_prettier_dir(
|
||||||
|
paths_to_check: Vec<PathBuf>,
|
||||||
|
fs: &dyn Fs,
|
||||||
|
) -> anyhow::Result<Option<PathBuf>> {
|
||||||
|
for path in paths_to_check {
|
||||||
|
let possible_package_json = path.join("package.json");
|
||||||
|
if let Some(package_json_metadata) = fs
|
||||||
|
.metadata(&possible_package_json)
|
||||||
|
.await
|
||||||
|
.with_context(|| format!("Fetching metadata for {possible_package_json:?}"))?
|
||||||
|
{
|
||||||
|
if !package_json_metadata.is_dir && !package_json_metadata.is_symlink {
|
||||||
|
let package_json_contents = fs
|
||||||
|
.load(&possible_package_json)
|
||||||
|
.await
|
||||||
|
.with_context(|| format!("reading {possible_package_json:?} file contents"))?;
|
||||||
|
if let Ok(json_contents) = serde_json::from_str::<HashMap<String, serde_json::Value>>(
|
||||||
|
&package_json_contents,
|
||||||
|
) {
|
||||||
|
if let Some(serde_json::Value::Object(o)) = json_contents.get("dependencies") {
|
||||||
|
if o.contains_key(PRETTIER_PACKAGE_NAME) {
|
||||||
|
return Ok(Some(path));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(serde_json::Value::Object(o)) = json_contents.get("devDependencies")
|
||||||
|
{
|
||||||
|
if o.contains_key(PRETTIER_PACKAGE_NAME) {
|
||||||
|
return Ok(Some(path));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let possible_node_modules_location = path.join("node_modules").join(PRETTIER_PACKAGE_NAME);
|
||||||
|
if let Some(node_modules_location_metadata) = fs
|
||||||
|
.metadata(&possible_node_modules_location)
|
||||||
|
.await
|
||||||
|
.with_context(|| format!("fetching metadata for {possible_node_modules_location:?}"))?
|
||||||
|
{
|
||||||
|
if node_modules_location_metadata.is_dir {
|
||||||
|
return Ok(Some(path));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Format {}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct FormatParams {
|
||||||
|
text: String,
|
||||||
|
options: FormatOptions,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct FormatOptions {
|
||||||
|
plugins: Vec<PathBuf>,
|
||||||
|
parser: Option<String>,
|
||||||
|
#[serde(rename = "filepath")]
|
||||||
|
path: Option<PathBuf>,
|
||||||
|
prettier_options: Option<HashMap<String, serde_json::Value>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct FormatResult {
|
||||||
|
text: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl lsp::request::Request for Format {
|
||||||
|
type Params = FormatParams;
|
||||||
|
type Result = FormatResult;
|
||||||
|
const METHOD: &'static str = "prettier/format";
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ClearCache {}
|
||||||
|
|
||||||
|
impl lsp::request::Request for ClearCache {
|
||||||
|
type Params = ();
|
||||||
|
type Result = ();
|
||||||
|
const METHOD: &'static str = "prettier/clear_cache";
|
||||||
|
}
|
217
crates/prettier/src/prettier_server.js
Normal file
217
crates/prettier/src/prettier_server.js
Normal file
|
@ -0,0 +1,217 @@
|
||||||
|
const { Buffer } = require('buffer');
|
||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
const { once } = require('events');
|
||||||
|
|
||||||
|
const prettierContainerPath = process.argv[2];
|
||||||
|
if (prettierContainerPath == null || prettierContainerPath.length == 0) {
|
||||||
|
process.stderr.write(`Prettier path argument was not specified or empty.\nUsage: ${process.argv[0]} ${process.argv[1]} prettier/path\n`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
fs.stat(prettierContainerPath, (err, stats) => {
|
||||||
|
if (err) {
|
||||||
|
process.stderr.write(`Path '${prettierContainerPath}' does not exist\n`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!stats.isDirectory()) {
|
||||||
|
process.stderr.write(`Path '${prettierContainerPath}' exists but is not a directory\n`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const prettierPath = path.join(prettierContainerPath, 'node_modules/prettier');
|
||||||
|
|
||||||
|
class Prettier {
|
||||||
|
constructor(path, prettier, config) {
|
||||||
|
this.path = path;
|
||||||
|
this.prettier = prettier;
|
||||||
|
this.config = config;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
let prettier;
|
||||||
|
let config;
|
||||||
|
try {
|
||||||
|
prettier = await loadPrettier(prettierPath);
|
||||||
|
config = await prettier.resolveConfig(prettierPath) || {};
|
||||||
|
} catch (e) {
|
||||||
|
process.stderr.write(`Failed to load prettier: ${e}\n`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
process.stderr.write(`Prettier at path '${prettierPath}' loaded successfully, config: ${JSON.stringify(config)}\n`);
|
||||||
|
process.stdin.resume();
|
||||||
|
handleBuffer(new Prettier(prettierPath, prettier, config));
|
||||||
|
})()
|
||||||
|
|
||||||
|
async function handleBuffer(prettier) {
|
||||||
|
for await (const messageText of readStdin()) {
|
||||||
|
let message;
|
||||||
|
try {
|
||||||
|
message = JSON.parse(messageText);
|
||||||
|
} catch (e) {
|
||||||
|
sendResponse(makeError(`Failed to parse message '${messageText}': ${e}`));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// allow concurrent request handling by not `await`ing the message handling promise (async function)
|
||||||
|
handleMessage(message, prettier).catch(e => {
|
||||||
|
sendResponse({ id: message.id, ...makeError(`error during message handling: ${e}`) });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const headerSeparator = "\r\n";
|
||||||
|
const contentLengthHeaderName = 'Content-Length';
|
||||||
|
|
||||||
|
async function* readStdin() {
|
||||||
|
let buffer = Buffer.alloc(0);
|
||||||
|
let streamEnded = false;
|
||||||
|
process.stdin.on('end', () => {
|
||||||
|
streamEnded = true;
|
||||||
|
});
|
||||||
|
process.stdin.on('data', (data) => {
|
||||||
|
buffer = Buffer.concat([buffer, data]);
|
||||||
|
});
|
||||||
|
|
||||||
|
async function handleStreamEnded(errorMessage) {
|
||||||
|
sendResponse(makeError(errorMessage));
|
||||||
|
buffer = Buffer.alloc(0);
|
||||||
|
messageLength = null;
|
||||||
|
await once(process.stdin, 'readable');
|
||||||
|
streamEnded = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
let headersLength = null;
|
||||||
|
let messageLength = null;
|
||||||
|
main_loop: while (true) {
|
||||||
|
if (messageLength === null) {
|
||||||
|
while (buffer.indexOf(`${headerSeparator}${headerSeparator}`) === -1) {
|
||||||
|
if (streamEnded) {
|
||||||
|
await handleStreamEnded('Unexpected end of stream: headers not found');
|
||||||
|
continue main_loop;
|
||||||
|
} else if (buffer.length > contentLengthHeaderName.length * 10) {
|
||||||
|
await handleStreamEnded(`Unexpected stream of bytes: no headers end found after ${buffer.length} bytes of input`);
|
||||||
|
continue main_loop;
|
||||||
|
}
|
||||||
|
await once(process.stdin, 'readable');
|
||||||
|
}
|
||||||
|
const headers = buffer.subarray(0, buffer.indexOf(`${headerSeparator}${headerSeparator}`)).toString('ascii');
|
||||||
|
const contentLengthHeader = headers.split(headerSeparator)
|
||||||
|
.map(header => header.split(':'))
|
||||||
|
.filter(header => header[2] === undefined)
|
||||||
|
.filter(header => (header[1] || '').length > 0)
|
||||||
|
.find(header => (header[0] || '').trim() === contentLengthHeaderName);
|
||||||
|
const contentLength = (contentLengthHeader || [])[1];
|
||||||
|
if (contentLength === undefined) {
|
||||||
|
await handleStreamEnded(`Missing or incorrect ${contentLengthHeaderName} header: ${headers}`);
|
||||||
|
continue main_loop;
|
||||||
|
}
|
||||||
|
headersLength = headers.length + headerSeparator.length * 2;
|
||||||
|
messageLength = parseInt(contentLength, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
while (buffer.length < (headersLength + messageLength)) {
|
||||||
|
if (streamEnded) {
|
||||||
|
await handleStreamEnded(
|
||||||
|
`Unexpected end of stream: buffer length ${buffer.length} does not match expected header length ${headersLength} + body length ${messageLength}`);
|
||||||
|
continue main_loop;
|
||||||
|
}
|
||||||
|
await once(process.stdin, 'readable');
|
||||||
|
}
|
||||||
|
|
||||||
|
const messageEnd = headersLength + messageLength;
|
||||||
|
const message = buffer.subarray(headersLength, messageEnd);
|
||||||
|
buffer = buffer.subarray(messageEnd);
|
||||||
|
headersLength = null;
|
||||||
|
messageLength = null;
|
||||||
|
yield message.toString('utf8');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
sendResponse(makeError(`Error reading stdin: ${e}`));
|
||||||
|
} finally {
|
||||||
|
process.stdin.off('data', () => { });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleMessage(message, prettier) {
|
||||||
|
const { method, id, params } = message;
|
||||||
|
if (method === undefined) {
|
||||||
|
throw new Error(`Message method is undefined: ${JSON.stringify(message)}`);
|
||||||
|
}
|
||||||
|
if (id === undefined) {
|
||||||
|
throw new Error(`Message id is undefined: ${JSON.stringify(message)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (method === 'prettier/format') {
|
||||||
|
if (params === undefined || params.text === undefined) {
|
||||||
|
throw new Error(`Message params.text is undefined: ${JSON.stringify(message)}`);
|
||||||
|
}
|
||||||
|
if (params.options === undefined) {
|
||||||
|
throw new Error(`Message params.options is undefined: ${JSON.stringify(message)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let resolvedConfig = {};
|
||||||
|
if (params.options.filepath !== undefined) {
|
||||||
|
resolvedConfig = await prettier.prettier.resolveConfig(params.options.filepath) || {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
...(params.options.prettierOptions || prettier.config),
|
||||||
|
...resolvedConfig,
|
||||||
|
parser: params.options.parser,
|
||||||
|
plugins: params.options.plugins,
|
||||||
|
path: params.options.filepath
|
||||||
|
};
|
||||||
|
process.stderr.write(`Resolved config: ${JSON.stringify(resolvedConfig)}, will format file '${params.options.filepath || ''}' with options: ${JSON.stringify(options)}\n`);
|
||||||
|
const formattedText = await prettier.prettier.format(params.text, options);
|
||||||
|
sendResponse({ id, result: { text: formattedText } });
|
||||||
|
} else if (method === 'prettier/clear_cache') {
|
||||||
|
prettier.prettier.clearConfigCache();
|
||||||
|
prettier.config = await prettier.prettier.resolveConfig(prettier.path) || {};
|
||||||
|
sendResponse({ id, result: null });
|
||||||
|
} else if (method === 'initialize') {
|
||||||
|
sendResponse({
|
||||||
|
id,
|
||||||
|
result: {
|
||||||
|
"capabilities": {}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw new Error(`Unknown method: ${method}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeError(message) {
|
||||||
|
return {
|
||||||
|
error: {
|
||||||
|
"code": -32600, // invalid request code
|
||||||
|
message,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendResponse(response) {
|
||||||
|
const responsePayloadString = JSON.stringify({
|
||||||
|
jsonrpc: "2.0",
|
||||||
|
...response
|
||||||
|
});
|
||||||
|
const headers = `${contentLengthHeaderName}: ${Buffer.byteLength(responsePayloadString)}${headerSeparator}${headerSeparator}`;
|
||||||
|
process.stdout.write(headers + responsePayloadString);
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadPrettier(prettierPath) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
fs.access(prettierPath, fs.constants.F_OK, (err) => {
|
||||||
|
if (err) {
|
||||||
|
reject(`Path '${prettierPath}' does not exist.Error: ${err}`);
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
resolve(require(prettierPath));
|
||||||
|
} catch (err) {
|
||||||
|
reject(`Error requiring prettier module from path '${prettierPath}'.Error: ${err}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
|
@ -15,6 +15,7 @@ test-support = [
|
||||||
"language/test-support",
|
"language/test-support",
|
||||||
"settings/test-support",
|
"settings/test-support",
|
||||||
"text/test-support",
|
"text/test-support",
|
||||||
|
"prettier/test-support",
|
||||||
]
|
]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
@ -31,6 +32,8 @@ git = { path = "../git" }
|
||||||
gpui = { path = "../gpui" }
|
gpui = { path = "../gpui" }
|
||||||
language = { path = "../language" }
|
language = { path = "../language" }
|
||||||
lsp = { path = "../lsp" }
|
lsp = { path = "../lsp" }
|
||||||
|
node_runtime = { path = "../node_runtime" }
|
||||||
|
prettier = { path = "../prettier" }
|
||||||
rpc = { path = "../rpc" }
|
rpc = { path = "../rpc" }
|
||||||
settings = { path = "../settings" }
|
settings = { path = "../settings" }
|
||||||
sum_tree = { path = "../sum_tree" }
|
sum_tree = { path = "../sum_tree" }
|
||||||
|
@ -73,6 +76,7 @@ gpui = { path = "../gpui", features = ["test-support"] }
|
||||||
language = { path = "../language", features = ["test-support"] }
|
language = { path = "../language", features = ["test-support"] }
|
||||||
lsp = { path = "../lsp", features = ["test-support"] }
|
lsp = { path = "../lsp", features = ["test-support"] }
|
||||||
settings = { path = "../settings", features = ["test-support"] }
|
settings = { path = "../settings", features = ["test-support"] }
|
||||||
|
prettier = { path = "../prettier", features = ["test-support"] }
|
||||||
util = { path = "../util", features = ["test-support"] }
|
util = { path = "../util", features = ["test-support"] }
|
||||||
rpc = { path = "../rpc", features = ["test-support"] }
|
rpc = { path = "../rpc", features = ["test-support"] }
|
||||||
git2.workspace = true
|
git2.workspace = true
|
||||||
|
|
|
@ -20,7 +20,7 @@ use futures::{
|
||||||
mpsc::{self, UnboundedReceiver},
|
mpsc::{self, UnboundedReceiver},
|
||||||
oneshot,
|
oneshot,
|
||||||
},
|
},
|
||||||
future::{try_join_all, Shared},
|
future::{self, try_join_all, Shared},
|
||||||
stream::FuturesUnordered,
|
stream::FuturesUnordered,
|
||||||
AsyncWriteExt, Future, FutureExt, StreamExt, TryFutureExt,
|
AsyncWriteExt, Future, FutureExt, StreamExt, TryFutureExt,
|
||||||
};
|
};
|
||||||
|
@ -31,17 +31,19 @@ use gpui::{
|
||||||
};
|
};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use language::{
|
use language::{
|
||||||
language_settings::{language_settings, FormatOnSave, Formatter, InlayHintKind},
|
language_settings::{
|
||||||
|
language_settings, FormatOnSave, Formatter, InlayHintKind, LanguageSettings,
|
||||||
|
},
|
||||||
point_to_lsp,
|
point_to_lsp,
|
||||||
proto::{
|
proto::{
|
||||||
deserialize_anchor, deserialize_fingerprint, deserialize_line_ending, deserialize_version,
|
deserialize_anchor, deserialize_fingerprint, deserialize_line_ending, deserialize_version,
|
||||||
serialize_anchor, serialize_version, split_operations,
|
serialize_anchor, serialize_version, split_operations,
|
||||||
},
|
},
|
||||||
range_from_lsp, range_to_lsp, Bias, Buffer, BufferSnapshot, CachedLspAdapter, CodeAction,
|
range_from_lsp, range_to_lsp, Bias, Buffer, BufferSnapshot, BundledFormatter, CachedLspAdapter,
|
||||||
CodeLabel, Completion, Diagnostic, DiagnosticEntry, DiagnosticSet, Diff, Event as BufferEvent,
|
CodeAction, CodeLabel, Completion, Diagnostic, DiagnosticEntry, DiagnosticSet, Diff,
|
||||||
File as _, Language, LanguageRegistry, LanguageServerName, LocalFile, LspAdapterDelegate,
|
Event as BufferEvent, File as _, Language, LanguageRegistry, LanguageServerName, LocalFile,
|
||||||
OffsetRangeExt, Operation, Patch, PendingLanguageServer, PointUtf16, TextBufferSnapshot,
|
LspAdapterDelegate, OffsetRangeExt, Operation, Patch, PendingLanguageServer, PointUtf16,
|
||||||
ToOffset, ToPointUtf16, Transaction, Unclipped,
|
TextBufferSnapshot, ToOffset, ToPointUtf16, Transaction, Unclipped,
|
||||||
};
|
};
|
||||||
use log::error;
|
use log::error;
|
||||||
use lsp::{
|
use lsp::{
|
||||||
|
@ -49,7 +51,9 @@ use lsp::{
|
||||||
DocumentHighlightKind, LanguageServer, LanguageServerBinary, LanguageServerId, OneOf,
|
DocumentHighlightKind, LanguageServer, LanguageServerBinary, LanguageServerId, OneOf,
|
||||||
};
|
};
|
||||||
use lsp_command::*;
|
use lsp_command::*;
|
||||||
|
use node_runtime::NodeRuntime;
|
||||||
use postage::watch;
|
use postage::watch;
|
||||||
|
use prettier::{LocateStart, Prettier, PRETTIER_SERVER_FILE, PRETTIER_SERVER_JS};
|
||||||
use project_settings::{LspSettings, ProjectSettings};
|
use project_settings::{LspSettings, ProjectSettings};
|
||||||
use rand::prelude::*;
|
use rand::prelude::*;
|
||||||
use search::SearchQuery;
|
use search::SearchQuery;
|
||||||
|
@ -75,10 +79,13 @@ use std::{
|
||||||
time::{Duration, Instant},
|
time::{Duration, Instant},
|
||||||
};
|
};
|
||||||
use terminals::Terminals;
|
use terminals::Terminals;
|
||||||
use text::Anchor;
|
use text::{Anchor, LineEnding, Rope};
|
||||||
use util::{
|
use util::{
|
||||||
debug_panic, defer, http::HttpClient, merge_json_value_into,
|
debug_panic, defer,
|
||||||
paths::LOCAL_SETTINGS_RELATIVE_PATH, post_inc, ResultExt, TryFutureExt as _,
|
http::HttpClient,
|
||||||
|
merge_json_value_into,
|
||||||
|
paths::{DEFAULT_PRETTIER_DIR, LOCAL_SETTINGS_RELATIVE_PATH},
|
||||||
|
post_inc, ResultExt, TryFutureExt as _,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub use fs::*;
|
pub use fs::*;
|
||||||
|
@ -152,6 +159,11 @@ pub struct Project {
|
||||||
copilot_lsp_subscription: Option<gpui::Subscription>,
|
copilot_lsp_subscription: Option<gpui::Subscription>,
|
||||||
copilot_log_subscription: Option<lsp::Subscription>,
|
copilot_log_subscription: Option<lsp::Subscription>,
|
||||||
current_lsp_settings: HashMap<Arc<str>, LspSettings>,
|
current_lsp_settings: HashMap<Arc<str>, LspSettings>,
|
||||||
|
node: Option<Arc<dyn NodeRuntime>>,
|
||||||
|
prettier_instances: HashMap<
|
||||||
|
(Option<WorktreeId>, PathBuf),
|
||||||
|
Shared<Task<Result<Arc<Prettier>, Arc<anyhow::Error>>>>,
|
||||||
|
>,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct DelayedDebounced {
|
struct DelayedDebounced {
|
||||||
|
@ -605,6 +617,7 @@ impl Project {
|
||||||
|
|
||||||
pub fn local(
|
pub fn local(
|
||||||
client: Arc<Client>,
|
client: Arc<Client>,
|
||||||
|
node: Arc<dyn NodeRuntime>,
|
||||||
user_store: ModelHandle<UserStore>,
|
user_store: ModelHandle<UserStore>,
|
||||||
languages: Arc<LanguageRegistry>,
|
languages: Arc<LanguageRegistry>,
|
||||||
fs: Arc<dyn Fs>,
|
fs: Arc<dyn Fs>,
|
||||||
|
@ -660,6 +673,8 @@ impl Project {
|
||||||
copilot_lsp_subscription,
|
copilot_lsp_subscription,
|
||||||
copilot_log_subscription: None,
|
copilot_log_subscription: None,
|
||||||
current_lsp_settings: settings::get::<ProjectSettings>(cx).lsp.clone(),
|
current_lsp_settings: settings::get::<ProjectSettings>(cx).lsp.clone(),
|
||||||
|
node: Some(node),
|
||||||
|
prettier_instances: HashMap::default(),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -757,6 +772,8 @@ impl Project {
|
||||||
copilot_lsp_subscription,
|
copilot_lsp_subscription,
|
||||||
copilot_log_subscription: None,
|
copilot_log_subscription: None,
|
||||||
current_lsp_settings: settings::get::<ProjectSettings>(cx).lsp.clone(),
|
current_lsp_settings: settings::get::<ProjectSettings>(cx).lsp.clone(),
|
||||||
|
node: None,
|
||||||
|
prettier_instances: HashMap::default(),
|
||||||
};
|
};
|
||||||
for worktree in worktrees {
|
for worktree in worktrees {
|
||||||
let _ = this.add_worktree(&worktree, cx);
|
let _ = this.add_worktree(&worktree, cx);
|
||||||
|
@ -795,8 +812,16 @@ impl Project {
|
||||||
let http_client = util::http::FakeHttpClient::with_404_response();
|
let http_client = util::http::FakeHttpClient::with_404_response();
|
||||||
let client = cx.update(|cx| client::Client::new(http_client.clone(), cx));
|
let client = cx.update(|cx| client::Client::new(http_client.clone(), cx));
|
||||||
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
|
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
|
||||||
let project =
|
let project = cx.update(|cx| {
|
||||||
cx.update(|cx| Project::local(client, user_store, Arc::new(languages), fs, cx));
|
Project::local(
|
||||||
|
client,
|
||||||
|
node_runtime::FakeNodeRuntime::new(),
|
||||||
|
user_store,
|
||||||
|
Arc::new(languages),
|
||||||
|
fs,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
});
|
||||||
for path in root_paths {
|
for path in root_paths {
|
||||||
let (tree, _) = project
|
let (tree, _) = project
|
||||||
.update(cx, |project, cx| {
|
.update(cx, |project, cx| {
|
||||||
|
@ -810,19 +835,37 @@ impl Project {
|
||||||
project
|
project
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Enables a prettier mock that avoids interacting with node runtime, prettier LSP wrapper, or any real file changes.
|
||||||
|
/// Instead, if appends the suffix to every input, this suffix is returned by this method.
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
pub fn enable_test_prettier(&mut self, plugins: &[&'static str]) -> &'static str {
|
||||||
|
self.node = Some(node_runtime::FakeNodeRuntime::with_prettier_support(
|
||||||
|
plugins,
|
||||||
|
));
|
||||||
|
Prettier::FORMAT_SUFFIX
|
||||||
|
}
|
||||||
|
|
||||||
fn on_settings_changed(&mut self, cx: &mut ModelContext<Self>) {
|
fn on_settings_changed(&mut self, cx: &mut ModelContext<Self>) {
|
||||||
let mut language_servers_to_start = Vec::new();
|
let mut language_servers_to_start = Vec::new();
|
||||||
|
let mut language_formatters_to_check = Vec::new();
|
||||||
for buffer in self.opened_buffers.values() {
|
for buffer in self.opened_buffers.values() {
|
||||||
if let Some(buffer) = buffer.upgrade(cx) {
|
if let Some(buffer) = buffer.upgrade(cx) {
|
||||||
let buffer = buffer.read(cx);
|
let buffer = buffer.read(cx);
|
||||||
if let Some((file, language)) = buffer.file().zip(buffer.language()) {
|
let buffer_file = File::from_dyn(buffer.file());
|
||||||
let settings = language_settings(Some(language), Some(file), cx);
|
let buffer_language = buffer.language();
|
||||||
|
let settings = language_settings(buffer_language, buffer.file(), cx);
|
||||||
|
if let Some(language) = buffer_language {
|
||||||
if settings.enable_language_server {
|
if settings.enable_language_server {
|
||||||
if let Some(file) = File::from_dyn(Some(file)) {
|
if let Some(file) = buffer_file {
|
||||||
language_servers_to_start
|
language_servers_to_start
|
||||||
.push((file.worktree.clone(), language.clone()));
|
.push((file.worktree.clone(), Arc::clone(language)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
language_formatters_to_check.push((
|
||||||
|
buffer_file.map(|f| f.worktree_id(cx)),
|
||||||
|
Arc::clone(language),
|
||||||
|
settings.clone(),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -875,6 +918,11 @@ impl Project {
|
||||||
.detach();
|
.detach();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (worktree, language, settings) in language_formatters_to_check {
|
||||||
|
self.install_default_formatters(worktree, &language, &settings, cx)
|
||||||
|
.detach_and_log_err(cx);
|
||||||
|
}
|
||||||
|
|
||||||
// Start all the newly-enabled language servers.
|
// Start all the newly-enabled language servers.
|
||||||
for (worktree, language) in language_servers_to_start {
|
for (worktree, language) in language_servers_to_start {
|
||||||
let worktree_path = worktree.read(cx).abs_path();
|
let worktree_path = worktree.read(cx).abs_path();
|
||||||
|
@ -2623,7 +2671,26 @@ impl Project {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if let Some(file) = File::from_dyn(buffer.read(cx).file()) {
|
let buffer_file = buffer.read(cx).file().cloned();
|
||||||
|
let settings = language_settings(Some(&new_language), buffer_file.as_ref(), cx).clone();
|
||||||
|
let buffer_file = File::from_dyn(buffer_file.as_ref());
|
||||||
|
let worktree = buffer_file.as_ref().map(|f| f.worktree_id(cx));
|
||||||
|
|
||||||
|
let task_buffer = buffer.clone();
|
||||||
|
let prettier_installation_task =
|
||||||
|
self.install_default_formatters(worktree, &new_language, &settings, cx);
|
||||||
|
cx.spawn(|project, mut cx| async move {
|
||||||
|
prettier_installation_task.await?;
|
||||||
|
let _ = project
|
||||||
|
.update(&mut cx, |project, cx| {
|
||||||
|
project.prettier_instance_for_buffer(&task_buffer, cx)
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
anyhow::Ok(())
|
||||||
|
})
|
||||||
|
.detach_and_log_err(cx);
|
||||||
|
|
||||||
|
if let Some(file) = buffer_file {
|
||||||
let worktree = file.worktree.clone();
|
let worktree = file.worktree.clone();
|
||||||
if let Some(tree) = worktree.read(cx).as_local() {
|
if let Some(tree) = worktree.read(cx).as_local() {
|
||||||
self.start_language_servers(&worktree, tree.abs_path().clone(), new_language, cx);
|
self.start_language_servers(&worktree, tree.abs_path().clone(), new_language, cx);
|
||||||
|
@ -3949,7 +4016,7 @@ impl Project {
|
||||||
push_to_history: bool,
|
push_to_history: bool,
|
||||||
trigger: FormatTrigger,
|
trigger: FormatTrigger,
|
||||||
cx: &mut ModelContext<Project>,
|
cx: &mut ModelContext<Project>,
|
||||||
) -> Task<Result<ProjectTransaction>> {
|
) -> Task<anyhow::Result<ProjectTransaction>> {
|
||||||
if self.is_local() {
|
if self.is_local() {
|
||||||
let mut buffers_with_paths_and_servers = buffers
|
let mut buffers_with_paths_and_servers = buffers
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
@ -4027,6 +4094,7 @@ impl Project {
|
||||||
enum FormatOperation {
|
enum FormatOperation {
|
||||||
Lsp(Vec<(Range<Anchor>, String)>),
|
Lsp(Vec<(Range<Anchor>, String)>),
|
||||||
External(Diff),
|
External(Diff),
|
||||||
|
Prettier(Diff),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply language-specific formatting using either a language server
|
// Apply language-specific formatting using either a language server
|
||||||
|
@ -4062,8 +4130,8 @@ impl Project {
|
||||||
| (_, FormatOnSave::External { command, arguments }) => {
|
| (_, FormatOnSave::External { command, arguments }) => {
|
||||||
if let Some(buffer_abs_path) = buffer_abs_path {
|
if let Some(buffer_abs_path) = buffer_abs_path {
|
||||||
format_operation = Self::format_via_external_command(
|
format_operation = Self::format_via_external_command(
|
||||||
&buffer,
|
buffer,
|
||||||
&buffer_abs_path,
|
buffer_abs_path,
|
||||||
&command,
|
&command,
|
||||||
&arguments,
|
&arguments,
|
||||||
&mut cx,
|
&mut cx,
|
||||||
|
@ -4076,6 +4144,69 @@ impl Project {
|
||||||
.map(FormatOperation::External);
|
.map(FormatOperation::External);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
(Formatter::Auto, FormatOnSave::On | FormatOnSave::Off) => {
|
||||||
|
if let Some(prettier_task) = this
|
||||||
|
.update(&mut cx, |project, cx| {
|
||||||
|
project.prettier_instance_for_buffer(buffer, cx)
|
||||||
|
}).await {
|
||||||
|
match prettier_task.await
|
||||||
|
{
|
||||||
|
Ok(prettier) => {
|
||||||
|
let buffer_path = buffer.read_with(&cx, |buffer, cx| {
|
||||||
|
File::from_dyn(buffer.file()).map(|file| file.abs_path(cx))
|
||||||
|
});
|
||||||
|
format_operation = Some(FormatOperation::Prettier(
|
||||||
|
prettier
|
||||||
|
.format(buffer, buffer_path, &cx)
|
||||||
|
.await
|
||||||
|
.context("formatting via prettier")?,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Err(e) => anyhow::bail!(
|
||||||
|
"Failed to create prettier instance for buffer during autoformatting: {e:#}"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
} else if let Some((language_server, buffer_abs_path)) =
|
||||||
|
language_server.as_ref().zip(buffer_abs_path.as_ref())
|
||||||
|
{
|
||||||
|
format_operation = Some(FormatOperation::Lsp(
|
||||||
|
Self::format_via_lsp(
|
||||||
|
&this,
|
||||||
|
&buffer,
|
||||||
|
buffer_abs_path,
|
||||||
|
&language_server,
|
||||||
|
tab_size,
|
||||||
|
&mut cx,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.context("failed to format via language server")?,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(Formatter::Prettier { .. }, FormatOnSave::On | FormatOnSave::Off) => {
|
||||||
|
if let Some(prettier_task) = this
|
||||||
|
.update(&mut cx, |project, cx| {
|
||||||
|
project.prettier_instance_for_buffer(buffer, cx)
|
||||||
|
}).await {
|
||||||
|
match prettier_task.await
|
||||||
|
{
|
||||||
|
Ok(prettier) => {
|
||||||
|
let buffer_path = buffer.read_with(&cx, |buffer, cx| {
|
||||||
|
File::from_dyn(buffer.file()).map(|file| file.abs_path(cx))
|
||||||
|
});
|
||||||
|
format_operation = Some(FormatOperation::Prettier(
|
||||||
|
prettier
|
||||||
|
.format(buffer, buffer_path, &cx)
|
||||||
|
.await
|
||||||
|
.context("formatting via prettier")?,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Err(e) => anyhow::bail!(
|
||||||
|
"Failed to create prettier instance for buffer during formatting: {e:#}"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
buffer.update(&mut cx, |b, cx| {
|
buffer.update(&mut cx, |b, cx| {
|
||||||
|
@ -4100,6 +4231,9 @@ impl Project {
|
||||||
FormatOperation::External(diff) => {
|
FormatOperation::External(diff) => {
|
||||||
b.apply_diff(diff, cx);
|
b.apply_diff(diff, cx);
|
||||||
}
|
}
|
||||||
|
FormatOperation::Prettier(diff) => {
|
||||||
|
b.apply_diff(diff, cx);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(transaction_id) = whitespace_transaction_id {
|
if let Some(transaction_id) = whitespace_transaction_id {
|
||||||
|
@ -5873,6 +6007,7 @@ impl Project {
|
||||||
this.update_local_worktree_buffers(&worktree, changes, cx);
|
this.update_local_worktree_buffers(&worktree, changes, cx);
|
||||||
this.update_local_worktree_language_servers(&worktree, changes, cx);
|
this.update_local_worktree_language_servers(&worktree, changes, cx);
|
||||||
this.update_local_worktree_settings(&worktree, changes, cx);
|
this.update_local_worktree_settings(&worktree, changes, cx);
|
||||||
|
this.update_prettier_settings(&worktree, changes, cx);
|
||||||
cx.emit(Event::WorktreeUpdatedEntries(
|
cx.emit(Event::WorktreeUpdatedEntries(
|
||||||
worktree.read(cx).id(),
|
worktree.read(cx).id(),
|
||||||
changes.clone(),
|
changes.clone(),
|
||||||
|
@ -6252,6 +6387,69 @@ impl Project {
|
||||||
.detach();
|
.detach();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn update_prettier_settings(
|
||||||
|
&self,
|
||||||
|
worktree: &ModelHandle<Worktree>,
|
||||||
|
changes: &[(Arc<Path>, ProjectEntryId, PathChange)],
|
||||||
|
cx: &mut ModelContext<'_, Project>,
|
||||||
|
) {
|
||||||
|
let prettier_config_files = Prettier::CONFIG_FILE_NAMES
|
||||||
|
.iter()
|
||||||
|
.map(Path::new)
|
||||||
|
.collect::<HashSet<_>>();
|
||||||
|
|
||||||
|
let prettier_config_file_changed = changes
|
||||||
|
.iter()
|
||||||
|
.filter(|(_, _, change)| !matches!(change, PathChange::Loaded))
|
||||||
|
.filter(|(path, _, _)| {
|
||||||
|
!path
|
||||||
|
.components()
|
||||||
|
.any(|component| component.as_os_str().to_string_lossy() == "node_modules")
|
||||||
|
})
|
||||||
|
.find(|(path, _, _)| prettier_config_files.contains(path.as_ref()));
|
||||||
|
let current_worktree_id = worktree.read(cx).id();
|
||||||
|
if let Some((config_path, _, _)) = prettier_config_file_changed {
|
||||||
|
log::info!(
|
||||||
|
"Prettier config file {config_path:?} changed, reloading prettier instances for worktree {current_worktree_id}"
|
||||||
|
);
|
||||||
|
let prettiers_to_reload = self
|
||||||
|
.prettier_instances
|
||||||
|
.iter()
|
||||||
|
.filter_map(|((worktree_id, prettier_path), prettier_task)| {
|
||||||
|
if worktree_id.is_none() || worktree_id == &Some(current_worktree_id) {
|
||||||
|
Some((*worktree_id, prettier_path.clone(), prettier_task.clone()))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
cx.background()
|
||||||
|
.spawn(async move {
|
||||||
|
for task_result in future::join_all(prettiers_to_reload.into_iter().map(|(worktree_id, prettier_path, prettier_task)| {
|
||||||
|
async move {
|
||||||
|
prettier_task.await?
|
||||||
|
.clear_cache()
|
||||||
|
.await
|
||||||
|
.with_context(|| {
|
||||||
|
format!(
|
||||||
|
"clearing prettier {prettier_path:?} cache for worktree {worktree_id:?} on prettier settings update"
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.map_err(Arc::new)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
if let Err(e) = task_result {
|
||||||
|
log::error!("Failed to clear cache for prettier: {e:#}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn set_active_path(&mut self, entry: Option<ProjectPath>, cx: &mut ModelContext<Self>) {
|
pub fn set_active_path(&mut self, entry: Option<ProjectPath>, cx: &mut ModelContext<Self>) {
|
||||||
let new_active_entry = entry.and_then(|project_path| {
|
let new_active_entry = entry.and_then(|project_path| {
|
||||||
let worktree = self.worktree_for_id(project_path.worktree_id, cx)?;
|
let worktree = self.worktree_for_id(project_path.worktree_id, cx)?;
|
||||||
|
@ -8109,6 +8307,236 @@ impl Project {
|
||||||
Vec::new()
|
Vec::new()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn prettier_instance_for_buffer(
|
||||||
|
&mut self,
|
||||||
|
buffer: &ModelHandle<Buffer>,
|
||||||
|
cx: &mut ModelContext<Self>,
|
||||||
|
) -> Task<Option<Shared<Task<Result<Arc<Prettier>, Arc<anyhow::Error>>>>>> {
|
||||||
|
let buffer = buffer.read(cx);
|
||||||
|
let buffer_file = buffer.file();
|
||||||
|
let Some(buffer_language) = buffer.language() else {
|
||||||
|
return Task::ready(None);
|
||||||
|
};
|
||||||
|
if !buffer_language
|
||||||
|
.lsp_adapters()
|
||||||
|
.iter()
|
||||||
|
.flat_map(|adapter| adapter.enabled_formatters())
|
||||||
|
.any(|formatter| matches!(formatter, BundledFormatter::Prettier { .. }))
|
||||||
|
{
|
||||||
|
return Task::ready(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
let buffer_file = File::from_dyn(buffer_file);
|
||||||
|
let buffer_path = buffer_file.map(|file| Arc::clone(file.path()));
|
||||||
|
let worktree_path = buffer_file
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|file| Some(file.worktree.read(cx).abs_path()));
|
||||||
|
let worktree_id = buffer_file.map(|file| file.worktree_id(cx));
|
||||||
|
if self.is_local() || worktree_id.is_none() || worktree_path.is_none() {
|
||||||
|
let Some(node) = self.node.as_ref().map(Arc::clone) else {
|
||||||
|
return Task::ready(None);
|
||||||
|
};
|
||||||
|
cx.spawn(|this, mut cx| async move {
|
||||||
|
let fs = this.update(&mut cx, |project, _| Arc::clone(&project.fs));
|
||||||
|
let prettier_dir = match cx
|
||||||
|
.background()
|
||||||
|
.spawn(Prettier::locate(
|
||||||
|
worktree_path.zip(buffer_path).map(
|
||||||
|
|(worktree_root_path, starting_path)| LocateStart {
|
||||||
|
worktree_root_path,
|
||||||
|
starting_path,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
fs,
|
||||||
|
))
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(path) => path,
|
||||||
|
Err(e) => {
|
||||||
|
return Some(
|
||||||
|
Task::ready(Err(Arc::new(e.context(
|
||||||
|
"determining prettier path for worktree {worktree_path:?}",
|
||||||
|
))))
|
||||||
|
.shared(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(existing_prettier) = this.update(&mut cx, |project, _| {
|
||||||
|
project
|
||||||
|
.prettier_instances
|
||||||
|
.get(&(worktree_id, prettier_dir.clone()))
|
||||||
|
.cloned()
|
||||||
|
}) {
|
||||||
|
return Some(existing_prettier);
|
||||||
|
}
|
||||||
|
|
||||||
|
log::info!("Found prettier in {prettier_dir:?}, starting.");
|
||||||
|
let task_prettier_dir = prettier_dir.clone();
|
||||||
|
let weak_project = this.downgrade();
|
||||||
|
let new_server_id =
|
||||||
|
this.update(&mut cx, |this, _| this.languages.next_language_server_id());
|
||||||
|
let new_prettier_task = cx
|
||||||
|
.spawn(|mut cx| async move {
|
||||||
|
let prettier = Prettier::start(
|
||||||
|
worktree_id.map(|id| id.to_usize()),
|
||||||
|
new_server_id,
|
||||||
|
task_prettier_dir,
|
||||||
|
node,
|
||||||
|
cx.clone(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.context("prettier start")
|
||||||
|
.map_err(Arc::new)?;
|
||||||
|
log::info!("Started prettier in {:?}", prettier.prettier_dir());
|
||||||
|
|
||||||
|
if let Some((project, prettier_server)) =
|
||||||
|
weak_project.upgrade(&mut cx).zip(prettier.server())
|
||||||
|
{
|
||||||
|
project.update(&mut cx, |project, cx| {
|
||||||
|
let name = if prettier.is_default() {
|
||||||
|
LanguageServerName(Arc::from("prettier (default)"))
|
||||||
|
} else {
|
||||||
|
let prettier_dir = prettier.prettier_dir();
|
||||||
|
let worktree_path = prettier
|
||||||
|
.worktree_id()
|
||||||
|
.map(WorktreeId::from_usize)
|
||||||
|
.and_then(|id| project.worktree_for_id(id, cx))
|
||||||
|
.map(|worktree| worktree.read(cx).abs_path());
|
||||||
|
match worktree_path {
|
||||||
|
Some(worktree_path) => {
|
||||||
|
if worktree_path.as_ref() == prettier_dir {
|
||||||
|
LanguageServerName(Arc::from(format!(
|
||||||
|
"prettier ({})",
|
||||||
|
prettier_dir
|
||||||
|
.file_name()
|
||||||
|
.and_then(|name| name.to_str())
|
||||||
|
.unwrap_or_default()
|
||||||
|
)))
|
||||||
|
} else {
|
||||||
|
let dir_to_display = match prettier_dir
|
||||||
|
.strip_prefix(&worktree_path)
|
||||||
|
.ok()
|
||||||
|
{
|
||||||
|
Some(relative_path) => relative_path,
|
||||||
|
None => prettier_dir,
|
||||||
|
};
|
||||||
|
LanguageServerName(Arc::from(format!(
|
||||||
|
"prettier ({})",
|
||||||
|
dir_to_display.display(),
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => LanguageServerName(Arc::from(format!(
|
||||||
|
"prettier ({})",
|
||||||
|
prettier_dir.display(),
|
||||||
|
))),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
project
|
||||||
|
.supplementary_language_servers
|
||||||
|
.insert(new_server_id, (name, Arc::clone(prettier_server)));
|
||||||
|
cx.emit(Event::LanguageServerAdded(new_server_id));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(Arc::new(prettier)).map_err(Arc::new)
|
||||||
|
})
|
||||||
|
.shared();
|
||||||
|
this.update(&mut cx, |project, _| {
|
||||||
|
project
|
||||||
|
.prettier_instances
|
||||||
|
.insert((worktree_id, prettier_dir), new_prettier_task.clone());
|
||||||
|
});
|
||||||
|
Some(new_prettier_task)
|
||||||
|
})
|
||||||
|
} else if self.remote_id().is_some() {
|
||||||
|
return Task::ready(None);
|
||||||
|
} else {
|
||||||
|
Task::ready(Some(
|
||||||
|
Task::ready(Err(Arc::new(anyhow!("project does not have a remote id")))).shared(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn install_default_formatters(
|
||||||
|
&self,
|
||||||
|
worktree: Option<WorktreeId>,
|
||||||
|
new_language: &Language,
|
||||||
|
language_settings: &LanguageSettings,
|
||||||
|
cx: &mut ModelContext<Self>,
|
||||||
|
) -> Task<anyhow::Result<()>> {
|
||||||
|
match &language_settings.formatter {
|
||||||
|
Formatter::Prettier { .. } | Formatter::Auto => {}
|
||||||
|
Formatter::LanguageServer | Formatter::External { .. } => return Task::ready(Ok(())),
|
||||||
|
};
|
||||||
|
let Some(node) = self.node.as_ref().cloned() else {
|
||||||
|
return Task::ready(Ok(()));
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut prettier_plugins = None;
|
||||||
|
for formatter in new_language
|
||||||
|
.lsp_adapters()
|
||||||
|
.into_iter()
|
||||||
|
.flat_map(|adapter| adapter.enabled_formatters())
|
||||||
|
{
|
||||||
|
match formatter {
|
||||||
|
BundledFormatter::Prettier { plugin_names, .. } => prettier_plugins
|
||||||
|
.get_or_insert_with(|| HashSet::default())
|
||||||
|
.extend(plugin_names),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let Some(prettier_plugins) = prettier_plugins else {
|
||||||
|
return Task::ready(Ok(()));
|
||||||
|
};
|
||||||
|
|
||||||
|
let default_prettier_dir = DEFAULT_PRETTIER_DIR.as_path();
|
||||||
|
let already_running_prettier = self
|
||||||
|
.prettier_instances
|
||||||
|
.get(&(worktree, default_prettier_dir.to_path_buf()))
|
||||||
|
.cloned();
|
||||||
|
|
||||||
|
let fs = Arc::clone(&self.fs);
|
||||||
|
cx.background()
|
||||||
|
.spawn(async move {
|
||||||
|
let prettier_wrapper_path = default_prettier_dir.join(PRETTIER_SERVER_FILE);
|
||||||
|
// method creates parent directory if it doesn't exist
|
||||||
|
fs.save(&prettier_wrapper_path, &Rope::from(PRETTIER_SERVER_JS), LineEnding::Unix).await
|
||||||
|
.with_context(|| format!("writing {PRETTIER_SERVER_FILE} file at {prettier_wrapper_path:?}"))?;
|
||||||
|
|
||||||
|
let packages_to_versions = future::try_join_all(
|
||||||
|
prettier_plugins
|
||||||
|
.iter()
|
||||||
|
.chain(Some(&"prettier"))
|
||||||
|
.map(|package_name| async {
|
||||||
|
let returned_package_name = package_name.to_string();
|
||||||
|
let latest_version = node.npm_package_latest_version(package_name)
|
||||||
|
.await
|
||||||
|
.with_context(|| {
|
||||||
|
format!("fetching latest npm version for package {returned_package_name}")
|
||||||
|
})?;
|
||||||
|
anyhow::Ok((returned_package_name, latest_version))
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.context("fetching latest npm versions")?;
|
||||||
|
|
||||||
|
log::info!("Fetching default prettier and plugins: {packages_to_versions:?}");
|
||||||
|
let borrowed_packages = packages_to_versions.iter().map(|(package, version)| {
|
||||||
|
(package.as_str(), version.as_str())
|
||||||
|
}).collect::<Vec<_>>();
|
||||||
|
node.npm_install_packages(default_prettier_dir, &borrowed_packages).await.context("fetching formatter packages")?;
|
||||||
|
|
||||||
|
if !prettier_plugins.is_empty() {
|
||||||
|
if let Some(prettier) = already_running_prettier {
|
||||||
|
prettier.await.map_err(|e| anyhow::anyhow!("Default prettier startup await failure: {e:#}"))?.clear_cache().await.context("clearing default prettier cache after plugins install")?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
anyhow::Ok(())
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn subscribe_for_copilot_events(
|
fn subscribe_for_copilot_events(
|
||||||
|
|
|
@ -494,6 +494,7 @@ fn main() {
|
||||||
let project = cx.update(|cx| {
|
let project = cx.update(|cx| {
|
||||||
Project::local(
|
Project::local(
|
||||||
client.clone(),
|
client.clone(),
|
||||||
|
node_runtime::FakeNodeRuntime::new(),
|
||||||
user_store.clone(),
|
user_store.clone(),
|
||||||
languages.clone(),
|
languages.clone(),
|
||||||
fs.clone(),
|
fs.clone(),
|
||||||
|
|
|
@ -11,6 +11,7 @@ lazy_static::lazy_static! {
|
||||||
pub static ref SUPPORT_DIR: PathBuf = HOME.join("Library/Application Support/Zed");
|
pub static ref SUPPORT_DIR: PathBuf = HOME.join("Library/Application Support/Zed");
|
||||||
pub static ref LANGUAGES_DIR: PathBuf = HOME.join("Library/Application Support/Zed/languages");
|
pub static ref LANGUAGES_DIR: PathBuf = HOME.join("Library/Application Support/Zed/languages");
|
||||||
pub static ref COPILOT_DIR: PathBuf = HOME.join("Library/Application Support/Zed/copilot");
|
pub static ref COPILOT_DIR: PathBuf = HOME.join("Library/Application Support/Zed/copilot");
|
||||||
|
pub static ref DEFAULT_PRETTIER_DIR: PathBuf = HOME.join("Library/Application Support/Zed/prettier");
|
||||||
pub static ref DB_DIR: PathBuf = HOME.join("Library/Application Support/Zed/db");
|
pub static ref DB_DIR: PathBuf = HOME.join("Library/Application Support/Zed/db");
|
||||||
pub static ref SETTINGS: PathBuf = CONFIG_DIR.join("settings.json");
|
pub static ref SETTINGS: PathBuf = CONFIG_DIR.join("settings.json");
|
||||||
pub static ref KEYMAP: PathBuf = CONFIG_DIR.join("keymap.json");
|
pub static ref KEYMAP: PathBuf = CONFIG_DIR.join("keymap.json");
|
||||||
|
|
|
@ -30,6 +30,7 @@ gpui = { path = "../gpui" }
|
||||||
install_cli = { path = "../install_cli" }
|
install_cli = { path = "../install_cli" }
|
||||||
language = { path = "../language" }
|
language = { path = "../language" }
|
||||||
menu = { path = "../menu" }
|
menu = { path = "../menu" }
|
||||||
|
node_runtime = { path = "../node_runtime" }
|
||||||
project = { path = "../project" }
|
project = { path = "../project" }
|
||||||
settings = { path = "../settings" }
|
settings = { path = "../settings" }
|
||||||
terminal = { path = "../terminal" }
|
terminal = { path = "../terminal" }
|
||||||
|
|
|
@ -42,6 +42,7 @@ use gpui::{
|
||||||
use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ProjectItem};
|
use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ProjectItem};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use language::{LanguageRegistry, Rope};
|
use language::{LanguageRegistry, Rope};
|
||||||
|
use node_runtime::NodeRuntime;
|
||||||
use std::{
|
use std::{
|
||||||
any::TypeId,
|
any::TypeId,
|
||||||
borrow::Cow,
|
borrow::Cow,
|
||||||
|
@ -456,6 +457,7 @@ pub struct AppState {
|
||||||
pub initialize_workspace:
|
pub initialize_workspace:
|
||||||
fn(WeakViewHandle<Workspace>, bool, Arc<AppState>, AsyncAppContext) -> Task<Result<()>>,
|
fn(WeakViewHandle<Workspace>, bool, Arc<AppState>, AsyncAppContext) -> Task<Result<()>>,
|
||||||
pub background_actions: BackgroundActions,
|
pub background_actions: BackgroundActions,
|
||||||
|
pub node_runtime: Arc<dyn NodeRuntime>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct WorkspaceStore {
|
pub struct WorkspaceStore {
|
||||||
|
@ -474,6 +476,7 @@ struct Follower {
|
||||||
impl AppState {
|
impl AppState {
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
pub fn test(cx: &mut AppContext) -> Arc<Self> {
|
pub fn test(cx: &mut AppContext) -> Arc<Self> {
|
||||||
|
use node_runtime::FakeNodeRuntime;
|
||||||
use settings::SettingsStore;
|
use settings::SettingsStore;
|
||||||
|
|
||||||
if !cx.has_global::<SettingsStore>() {
|
if !cx.has_global::<SettingsStore>() {
|
||||||
|
@ -498,6 +501,7 @@ impl AppState {
|
||||||
user_store,
|
user_store,
|
||||||
// channel_store,
|
// channel_store,
|
||||||
workspace_store,
|
workspace_store,
|
||||||
|
node_runtime: FakeNodeRuntime::new(),
|
||||||
initialize_workspace: |_, _, _, _| Task::ready(Ok(())),
|
initialize_workspace: |_, _, _, _| Task::ready(Ok(())),
|
||||||
build_window_options: |_, _, _| Default::default(),
|
build_window_options: |_, _, _| Default::default(),
|
||||||
background_actions: || &[],
|
background_actions: || &[],
|
||||||
|
@ -816,6 +820,7 @@ impl Workspace {
|
||||||
)> {
|
)> {
|
||||||
let project_handle = Project::local(
|
let project_handle = Project::local(
|
||||||
app_state.client.clone(),
|
app_state.client.clone(),
|
||||||
|
app_state.node_runtime.clone(),
|
||||||
app_state.user_store.clone(),
|
app_state.user_store.clone(),
|
||||||
app_state.languages.clone(),
|
app_state.languages.clone(),
|
||||||
app_state.fs.clone(),
|
app_state.fs.clone(),
|
||||||
|
@ -3517,6 +3522,8 @@ impl Workspace {
|
||||||
|
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
pub fn test_new(project: ModelHandle<Project>, cx: &mut ViewContext<Self>) -> Self {
|
pub fn test_new(project: ModelHandle<Project>, cx: &mut ViewContext<Self>) -> Self {
|
||||||
|
use node_runtime::FakeNodeRuntime;
|
||||||
|
|
||||||
let client = project.read(cx).client();
|
let client = project.read(cx).client();
|
||||||
let user_store = project.read(cx).user_store();
|
let user_store = project.read(cx).user_store();
|
||||||
|
|
||||||
|
@ -3530,6 +3537,7 @@ impl Workspace {
|
||||||
build_window_options: |_, _, _| Default::default(),
|
build_window_options: |_, _, _| Default::default(),
|
||||||
initialize_workspace: |_, _, _, _| Task::ready(Ok(())),
|
initialize_workspace: |_, _, _, _| Task::ready(Ok(())),
|
||||||
background_actions: || &[],
|
background_actions: || &[],
|
||||||
|
node_runtime: FakeNodeRuntime::new(),
|
||||||
});
|
});
|
||||||
Self::new(0, project, app_state, cx)
|
Self::new(0, project, app_state, cx)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
|
use language::{BundledFormatter, LanguageServerName, LspAdapter, LspAdapterDelegate};
|
||||||
use lsp::LanguageServerBinary;
|
use lsp::LanguageServerBinary;
|
||||||
use node_runtime::NodeRuntime;
|
use node_runtime::NodeRuntime;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
@ -96,6 +96,10 @@ impl LspAdapter for CssLspAdapter {
|
||||||
"provideFormatter": true
|
"provideFormatter": true
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn enabled_formatters(&self) -> Vec<BundledFormatter> {
|
||||||
|
vec![BundledFormatter::prettier("css")]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_cached_server_binary(
|
async fn get_cached_server_binary(
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
|
use language::{BundledFormatter, LanguageServerName, LspAdapter, LspAdapterDelegate};
|
||||||
use lsp::LanguageServerBinary;
|
use lsp::LanguageServerBinary;
|
||||||
use node_runtime::NodeRuntime;
|
use node_runtime::NodeRuntime;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
@ -96,6 +96,10 @@ impl LspAdapter for HtmlLspAdapter {
|
||||||
"provideFormatter": true
|
"provideFormatter": true
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn enabled_formatters(&self) -> Vec<BundledFormatter> {
|
||||||
|
vec![BundledFormatter::prettier("html")]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_cached_server_binary(
|
async fn get_cached_server_binary(
|
||||||
|
|
|
@ -4,7 +4,9 @@ use collections::HashMap;
|
||||||
use feature_flags::FeatureFlagAppExt;
|
use feature_flags::FeatureFlagAppExt;
|
||||||
use futures::{future::BoxFuture, FutureExt, StreamExt};
|
use futures::{future::BoxFuture, FutureExt, StreamExt};
|
||||||
use gpui::AppContext;
|
use gpui::AppContext;
|
||||||
use language::{LanguageRegistry, LanguageServerName, LspAdapter, LspAdapterDelegate};
|
use language::{
|
||||||
|
BundledFormatter, LanguageRegistry, LanguageServerName, LspAdapter, LspAdapterDelegate,
|
||||||
|
};
|
||||||
use lsp::LanguageServerBinary;
|
use lsp::LanguageServerBinary;
|
||||||
use node_runtime::NodeRuntime;
|
use node_runtime::NodeRuntime;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
@ -144,6 +146,10 @@ impl LspAdapter for JsonLspAdapter {
|
||||||
async fn language_ids(&self) -> HashMap<String, String> {
|
async fn language_ids(&self) -> HashMap<String, String> {
|
||||||
[("JSON".into(), "jsonc".into())].into_iter().collect()
|
[("JSON".into(), "jsonc".into())].into_iter().collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn enabled_formatters(&self) -> Vec<BundledFormatter> {
|
||||||
|
vec![BundledFormatter::prettier("json")]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_cached_server_binary(
|
async fn get_cached_server_binary(
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
|
use language::{BundledFormatter, LanguageServerName, LspAdapter, LspAdapterDelegate};
|
||||||
use lsp::LanguageServerBinary;
|
use lsp::LanguageServerBinary;
|
||||||
use node_runtime::NodeRuntime;
|
use node_runtime::NodeRuntime;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
@ -95,6 +95,13 @@ impl LspAdapter for SvelteLspAdapter {
|
||||||
"provideFormatter": true
|
"provideFormatter": true
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn enabled_formatters(&self) -> Vec<BundledFormatter> {
|
||||||
|
vec![BundledFormatter::Prettier {
|
||||||
|
parser_name: Some("svelte"),
|
||||||
|
plugin_names: vec!["prettier-plugin-svelte"],
|
||||||
|
}]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_cached_server_binary(
|
async fn get_cached_server_binary(
|
||||||
|
|
|
@ -6,7 +6,7 @@ use futures::{
|
||||||
FutureExt, StreamExt,
|
FutureExt, StreamExt,
|
||||||
};
|
};
|
||||||
use gpui::AppContext;
|
use gpui::AppContext;
|
||||||
use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
|
use language::{BundledFormatter, LanguageServerName, LspAdapter, LspAdapterDelegate};
|
||||||
use lsp::LanguageServerBinary;
|
use lsp::LanguageServerBinary;
|
||||||
use node_runtime::NodeRuntime;
|
use node_runtime::NodeRuntime;
|
||||||
use serde_json::{json, Value};
|
use serde_json::{json, Value};
|
||||||
|
@ -127,6 +127,13 @@ impl LspAdapter for TailwindLspAdapter {
|
||||||
.into_iter(),
|
.into_iter(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn enabled_formatters(&self) -> Vec<BundledFormatter> {
|
||||||
|
vec![BundledFormatter::Prettier {
|
||||||
|
parser_name: None,
|
||||||
|
plugin_names: vec!["prettier-plugin-tailwindcss"],
|
||||||
|
}]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_cached_server_binary(
|
async fn get_cached_server_binary(
|
||||||
|
|
|
@ -4,7 +4,7 @@ use async_tar::Archive;
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use futures::{future::BoxFuture, FutureExt};
|
use futures::{future::BoxFuture, FutureExt};
|
||||||
use gpui::AppContext;
|
use gpui::AppContext;
|
||||||
use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
|
use language::{BundledFormatter, LanguageServerName, LspAdapter, LspAdapterDelegate};
|
||||||
use lsp::{CodeActionKind, LanguageServerBinary};
|
use lsp::{CodeActionKind, LanguageServerBinary};
|
||||||
use node_runtime::NodeRuntime;
|
use node_runtime::NodeRuntime;
|
||||||
use serde_json::{json, Value};
|
use serde_json::{json, Value};
|
||||||
|
@ -161,6 +161,10 @@ impl LspAdapter for TypeScriptLspAdapter {
|
||||||
"provideFormatter": true
|
"provideFormatter": true
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn enabled_formatters(&self) -> Vec<BundledFormatter> {
|
||||||
|
vec![BundledFormatter::prettier("typescript")]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_cached_ts_server_binary(
|
async fn get_cached_ts_server_binary(
|
||||||
|
@ -309,6 +313,10 @@ impl LspAdapter for EsLintLspAdapter {
|
||||||
async fn initialization_options(&self) -> Option<serde_json::Value> {
|
async fn initialization_options(&self) -> Option<serde_json::Value> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn enabled_formatters(&self) -> Vec<BundledFormatter> {
|
||||||
|
vec![BundledFormatter::prettier("babel")]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_cached_eslint_server_binary(
|
async fn get_cached_eslint_server_binary(
|
||||||
|
|
|
@ -3,7 +3,8 @@ use async_trait::async_trait;
|
||||||
use futures::{future::BoxFuture, FutureExt, StreamExt};
|
use futures::{future::BoxFuture, FutureExt, StreamExt};
|
||||||
use gpui::AppContext;
|
use gpui::AppContext;
|
||||||
use language::{
|
use language::{
|
||||||
language_settings::all_language_settings, LanguageServerName, LspAdapter, LspAdapterDelegate,
|
language_settings::all_language_settings, BundledFormatter, LanguageServerName, LspAdapter,
|
||||||
|
LspAdapterDelegate,
|
||||||
};
|
};
|
||||||
use lsp::LanguageServerBinary;
|
use lsp::LanguageServerBinary;
|
||||||
use node_runtime::NodeRuntime;
|
use node_runtime::NodeRuntime;
|
||||||
|
@ -108,6 +109,10 @@ impl LspAdapter for YamlLspAdapter {
|
||||||
}))
|
}))
|
||||||
.boxed()
|
.boxed()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn enabled_formatters(&self) -> Vec<BundledFormatter> {
|
||||||
|
vec![BundledFormatter::prettier("yaml")]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_cached_server_binary(
|
async fn get_cached_server_binary(
|
||||||
|
|
|
@ -154,7 +154,12 @@ fn main() {
|
||||||
semantic_index::init(fs.clone(), http.clone(), languages.clone(), cx);
|
semantic_index::init(fs.clone(), http.clone(), languages.clone(), cx);
|
||||||
vim::init(cx);
|
vim::init(cx);
|
||||||
terminal_view::init(cx);
|
terminal_view::init(cx);
|
||||||
copilot::init(copilot_language_server_id, http.clone(), node_runtime, cx);
|
copilot::init(
|
||||||
|
copilot_language_server_id,
|
||||||
|
http.clone(),
|
||||||
|
node_runtime.clone(),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
assistant::init(cx);
|
assistant::init(cx);
|
||||||
component_test::init(cx);
|
component_test::init(cx);
|
||||||
|
|
||||||
|
@ -181,6 +186,7 @@ fn main() {
|
||||||
initialize_workspace,
|
initialize_workspace,
|
||||||
background_actions,
|
background_actions,
|
||||||
workspace_store,
|
workspace_store,
|
||||||
|
node_runtime,
|
||||||
});
|
});
|
||||||
cx.set_global(Arc::downgrade(&app_state));
|
cx.set_global(Arc::downgrade(&app_state));
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue