mirror of
https://github.com/zed-industries/zed.git
synced 2024-12-24 17:28:40 +00:00
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",
|
||||
"lsp",
|
||||
"nanoid",
|
||||
"node_runtime",
|
||||
"parking_lot 0.11.2",
|
||||
"pretty_assertions",
|
||||
"project",
|
||||
|
@ -5517,6 +5518,26 @@ version = "0.2.17"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "pretty_assertions"
|
||||
version = "1.4.0"
|
||||
|
@ -5629,8 +5650,10 @@ dependencies = [
|
|||
"lazy_static",
|
||||
"log",
|
||||
"lsp",
|
||||
"node_runtime",
|
||||
"parking_lot 0.11.2",
|
||||
"postage",
|
||||
"prettier",
|
||||
"pretty_assertions",
|
||||
"rand 0.8.5",
|
||||
"regex",
|
||||
|
@ -9986,6 +10009,7 @@ dependencies = [
|
|||
"lazy_static",
|
||||
"log",
|
||||
"menu",
|
||||
"node_runtime",
|
||||
"parking_lot 0.11.2",
|
||||
"postage",
|
||||
"project",
|
||||
|
|
|
@ -52,6 +52,7 @@ members = [
|
|||
"crates/plugin",
|
||||
"crates/plugin_macros",
|
||||
"crates/plugin_runtime",
|
||||
"crates/prettier",
|
||||
"crates/project",
|
||||
"crates/project_panel",
|
||||
"crates/project_symbols",
|
||||
|
|
|
@ -199,7 +199,12 @@
|
|||
// "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
|
||||
// three values:
|
||||
//
|
||||
|
@ -429,6 +434,16 @@
|
|||
"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": {
|
||||
// Specify the LSP name as a key here.
|
||||
|
|
|
@ -72,6 +72,7 @@ fs = { path = "../fs", features = ["test-support"] }
|
|||
git = { path = "../git", features = ["test-support"] }
|
||||
live_kit_client = { path = "../live_kit_client", features = ["test-support"] }
|
||||
lsp = { path = "../lsp", features = ["test-support"] }
|
||||
node_runtime = { path = "../node_runtime" }
|
||||
project = { path = "../project", features = ["test-support"] }
|
||||
rpc = { path = "../rpc", 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 language::{
|
||||
language_settings::{AllLanguageSettings, Formatter, InlayHintSettings},
|
||||
tree_sitter_rust, Anchor, Diagnostic, DiagnosticEntry, FakeLspAdapter, Language,
|
||||
LanguageConfig, LineEnding, OffsetRangeExt, Point, Rope,
|
||||
tree_sitter_rust, Anchor, BundledFormatter, Diagnostic, DiagnosticEntry, FakeLspAdapter,
|
||||
Language, LanguageConfig, LineEnding, OffsetRangeExt, Point, Rope,
|
||||
};
|
||||
use live_kit_client::MacOSDisplay;
|
||||
use lsp::LanguageServerId;
|
||||
use project::{search::SearchQuery, DiagnosticSummary, HoverBlockKind, Project, ProjectPath};
|
||||
use project::{
|
||||
search::SearchQuery, DiagnosticSummary, FormatTrigger, HoverBlockKind, Project, ProjectPath,
|
||||
};
|
||||
use rand::prelude::*;
|
||||
use serde_json::json;
|
||||
use settings::SettingsStore;
|
||||
|
@ -4407,8 +4409,6 @@ async fn test_formatting_buffer(
|
|||
cx_a: &mut TestAppContext,
|
||||
cx_b: &mut TestAppContext,
|
||||
) {
|
||||
use project::FormatTrigger;
|
||||
|
||||
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;
|
||||
|
@ -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)]
|
||||
async fn test_definition(
|
||||
deterministic: Arc<Deterministic>,
|
||||
|
|
|
@ -15,6 +15,7 @@ use fs::FakeFs;
|
|||
use futures::{channel::oneshot, StreamExt as _};
|
||||
use gpui::{executor::Deterministic, ModelHandle, Task, TestAppContext, WindowHandle};
|
||||
use language::LanguageRegistry;
|
||||
use node_runtime::FakeNodeRuntime;
|
||||
use parking_lot::Mutex;
|
||||
use project::{Project, WorktreeId};
|
||||
use rpc::RECEIVE_TIMEOUT;
|
||||
|
@ -218,6 +219,7 @@ impl TestServer {
|
|||
build_window_options: |_, _, _| Default::default(),
|
||||
initialize_workspace: |_, _, _, _| Task::ready(Ok(())),
|
||||
background_actions: || &[],
|
||||
node_runtime: FakeNodeRuntime::new(),
|
||||
});
|
||||
|
||||
cx.update(|cx| {
|
||||
|
@ -567,6 +569,7 @@ impl TestClient {
|
|||
cx.update(|cx| {
|
||||
Project::local(
|
||||
self.client().clone(),
|
||||
self.app_state.node_runtime.clone(),
|
||||
self.app_state.user_store.clone(),
|
||||
self.app_state.languages.clone(),
|
||||
self.app_state.fs.clone(),
|
||||
|
|
|
@ -19,8 +19,8 @@ use gpui::{
|
|||
use indoc::indoc;
|
||||
use language::{
|
||||
language_settings::{AllLanguageSettings, AllLanguageSettingsContent, LanguageSettingsContent},
|
||||
BracketPairConfig, FakeLspAdapter, LanguageConfig, LanguageConfigOverride, LanguageRegistry,
|
||||
Override, Point,
|
||||
BracketPairConfig, BundledFormatter, FakeLspAdapter, LanguageConfig, LanguageConfigOverride,
|
||||
LanguageRegistry, Override, Point,
|
||||
};
|
||||
use parking_lot::Mutex;
|
||||
use project::project_settings::{LspSettings, ProjectSettings};
|
||||
|
@ -5076,7 +5076,9 @@ async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) {
|
|||
|
||||
#[gpui::test]
|
||||
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(
|
||||
LanguageConfig {
|
||||
|
@ -5092,6 +5094,12 @@ async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) {
|
|||
document_formatting_provider: Some(lsp::OneOf::Left(true)),
|
||||
..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()
|
||||
}))
|
||||
.await;
|
||||
|
@ -5100,7 +5108,10 @@ async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) {
|
|||
fs.insert_file("/file.rs", Default::default()).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
|
||||
.update(cx, |project, cx| project.open_local_buffer("/file.rs", cx))
|
||||
.await
|
||||
|
@ -5218,7 +5229,9 @@ async fn test_concurrent_format_requests(cx: &mut gpui::TestAppContext) {
|
|||
|
||||
#[gpui::test]
|
||||
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(
|
||||
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> {
|
||||
let point = DisplayPoint::new(row as u32, column as u32);
|
||||
point..point
|
||||
|
|
|
@ -85,7 +85,7 @@ pub struct RemoveOptions {
|
|||
pub ignore_if_not_exists: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub struct Metadata {
|
||||
pub inode: u64,
|
||||
pub mtime: SystemTime,
|
||||
|
|
|
@ -227,6 +227,10 @@ impl CachedLspAdapter {
|
|||
) -> Option<CodeLabel> {
|
||||
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 {
|
||||
|
@ -333,6 +337,33 @@ pub trait LspAdapter: 'static + Send + Sync {
|
|||
async fn language_ids(&self) -> HashMap<String, String> {
|
||||
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)]
|
||||
|
@ -467,6 +498,7 @@ pub struct FakeLspAdapter {
|
|||
pub initializer: Option<Box<dyn 'static + Send + Sync + Fn(&mut lsp::FakeLanguageServer)>>,
|
||||
pub disk_based_diagnostics_progress_token: Option<String>,
|
||||
pub disk_based_diagnostics_sources: Vec<String>,
|
||||
pub enabled_formatters: Vec<BundledFormatter>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
|
@ -1729,6 +1761,7 @@ impl Default for FakeLspAdapter {
|
|||
disk_based_diagnostics_progress_token: None,
|
||||
initialization_options: None,
|
||||
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> {
|
||||
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>)]) {
|
||||
|
|
|
@ -50,6 +50,7 @@ pub struct LanguageSettings {
|
|||
pub remove_trailing_whitespace_on_save: bool,
|
||||
pub ensure_final_newline_on_save: bool,
|
||||
pub formatter: Formatter,
|
||||
pub prettier: HashMap<String, serde_json::Value>,
|
||||
pub enable_language_server: bool,
|
||||
pub show_copilot_suggestions: bool,
|
||||
pub show_whitespaces: ShowWhitespaceSetting,
|
||||
|
@ -98,6 +99,8 @@ pub struct LanguageSettingsContent {
|
|||
#[serde(default)]
|
||||
pub formatter: Option<Formatter>,
|
||||
#[serde(default)]
|
||||
pub prettier: Option<HashMap<String, serde_json::Value>>,
|
||||
#[serde(default)]
|
||||
pub enable_language_server: Option<bool>,
|
||||
#[serde(default)]
|
||||
pub show_copilot_suggestions: Option<bool>,
|
||||
|
@ -149,10 +152,13 @@ pub enum ShowWhitespaceSetting {
|
|||
All,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Formatter {
|
||||
#[default]
|
||||
Auto,
|
||||
LanguageServer,
|
||||
Prettier,
|
||||
External {
|
||||
command: Arc<str>,
|
||||
arguments: Arc<[String]>,
|
||||
|
@ -392,6 +398,7 @@ fn merge_settings(settings: &mut LanguageSettings, src: &LanguageSettingsContent
|
|||
src.preferred_line_length,
|
||||
);
|
||||
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.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 {
|
||||
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]
|
||||
impl NodeRuntime for FakeNodeRuntime {
|
||||
async fn binary_path(&self) -> Result<PathBuf> {
|
||||
unreachable!()
|
||||
async fn binary_path(&self) -> anyhow::Result<PathBuf> {
|
||||
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> {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
async fn npm_package_latest_version(&self, _: &str) -> Result<String> {
|
||||
unreachable!()
|
||||
async fn npm_package_latest_version(&self, name: &str) -> anyhow::Result<String> {
|
||||
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<()> {
|
||||
unreachable!()
|
||||
async fn npm_install_packages(
|
||||
&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",
|
||||
"settings/test-support",
|
||||
"text/test-support",
|
||||
"prettier/test-support",
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
|
@ -31,6 +32,8 @@ git = { path = "../git" }
|
|||
gpui = { path = "../gpui" }
|
||||
language = { path = "../language" }
|
||||
lsp = { path = "../lsp" }
|
||||
node_runtime = { path = "../node_runtime" }
|
||||
prettier = { path = "../prettier" }
|
||||
rpc = { path = "../rpc" }
|
||||
settings = { path = "../settings" }
|
||||
sum_tree = { path = "../sum_tree" }
|
||||
|
@ -73,6 +76,7 @@ gpui = { path = "../gpui", features = ["test-support"] }
|
|||
language = { path = "../language", features = ["test-support"] }
|
||||
lsp = { path = "../lsp", features = ["test-support"] }
|
||||
settings = { path = "../settings", features = ["test-support"] }
|
||||
prettier = { path = "../prettier", features = ["test-support"] }
|
||||
util = { path = "../util", features = ["test-support"] }
|
||||
rpc = { path = "../rpc", features = ["test-support"] }
|
||||
git2.workspace = true
|
||||
|
|
|
@ -20,7 +20,7 @@ use futures::{
|
|||
mpsc::{self, UnboundedReceiver},
|
||||
oneshot,
|
||||
},
|
||||
future::{try_join_all, Shared},
|
||||
future::{self, try_join_all, Shared},
|
||||
stream::FuturesUnordered,
|
||||
AsyncWriteExt, Future, FutureExt, StreamExt, TryFutureExt,
|
||||
};
|
||||
|
@ -31,17 +31,19 @@ use gpui::{
|
|||
};
|
||||
use itertools::Itertools;
|
||||
use language::{
|
||||
language_settings::{language_settings, FormatOnSave, Formatter, InlayHintKind},
|
||||
language_settings::{
|
||||
language_settings, FormatOnSave, Formatter, InlayHintKind, LanguageSettings,
|
||||
},
|
||||
point_to_lsp,
|
||||
proto::{
|
||||
deserialize_anchor, deserialize_fingerprint, deserialize_line_ending, deserialize_version,
|
||||
serialize_anchor, serialize_version, split_operations,
|
||||
},
|
||||
range_from_lsp, range_to_lsp, Bias, Buffer, BufferSnapshot, CachedLspAdapter, CodeAction,
|
||||
CodeLabel, Completion, Diagnostic, DiagnosticEntry, DiagnosticSet, Diff, Event as BufferEvent,
|
||||
File as _, Language, LanguageRegistry, LanguageServerName, LocalFile, LspAdapterDelegate,
|
||||
OffsetRangeExt, Operation, Patch, PendingLanguageServer, PointUtf16, TextBufferSnapshot,
|
||||
ToOffset, ToPointUtf16, Transaction, Unclipped,
|
||||
range_from_lsp, range_to_lsp, Bias, Buffer, BufferSnapshot, BundledFormatter, CachedLspAdapter,
|
||||
CodeAction, CodeLabel, Completion, Diagnostic, DiagnosticEntry, DiagnosticSet, Diff,
|
||||
Event as BufferEvent, File as _, Language, LanguageRegistry, LanguageServerName, LocalFile,
|
||||
LspAdapterDelegate, OffsetRangeExt, Operation, Patch, PendingLanguageServer, PointUtf16,
|
||||
TextBufferSnapshot, ToOffset, ToPointUtf16, Transaction, Unclipped,
|
||||
};
|
||||
use log::error;
|
||||
use lsp::{
|
||||
|
@ -49,7 +51,9 @@ use lsp::{
|
|||
DocumentHighlightKind, LanguageServer, LanguageServerBinary, LanguageServerId, OneOf,
|
||||
};
|
||||
use lsp_command::*;
|
||||
use node_runtime::NodeRuntime;
|
||||
use postage::watch;
|
||||
use prettier::{LocateStart, Prettier, PRETTIER_SERVER_FILE, PRETTIER_SERVER_JS};
|
||||
use project_settings::{LspSettings, ProjectSettings};
|
||||
use rand::prelude::*;
|
||||
use search::SearchQuery;
|
||||
|
@ -75,10 +79,13 @@ use std::{
|
|||
time::{Duration, Instant},
|
||||
};
|
||||
use terminals::Terminals;
|
||||
use text::Anchor;
|
||||
use text::{Anchor, LineEnding, Rope};
|
||||
use util::{
|
||||
debug_panic, defer, http::HttpClient, merge_json_value_into,
|
||||
paths::LOCAL_SETTINGS_RELATIVE_PATH, post_inc, ResultExt, TryFutureExt as _,
|
||||
debug_panic, defer,
|
||||
http::HttpClient,
|
||||
merge_json_value_into,
|
||||
paths::{DEFAULT_PRETTIER_DIR, LOCAL_SETTINGS_RELATIVE_PATH},
|
||||
post_inc, ResultExt, TryFutureExt as _,
|
||||
};
|
||||
|
||||
pub use fs::*;
|
||||
|
@ -152,6 +159,11 @@ pub struct Project {
|
|||
copilot_lsp_subscription: Option<gpui::Subscription>,
|
||||
copilot_log_subscription: Option<lsp::Subscription>,
|
||||
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 {
|
||||
|
@ -605,6 +617,7 @@ impl Project {
|
|||
|
||||
pub fn local(
|
||||
client: Arc<Client>,
|
||||
node: Arc<dyn NodeRuntime>,
|
||||
user_store: ModelHandle<UserStore>,
|
||||
languages: Arc<LanguageRegistry>,
|
||||
fs: Arc<dyn Fs>,
|
||||
|
@ -660,6 +673,8 @@ impl Project {
|
|||
copilot_lsp_subscription,
|
||||
copilot_log_subscription: None,
|
||||
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_log_subscription: None,
|
||||
current_lsp_settings: settings::get::<ProjectSettings>(cx).lsp.clone(),
|
||||
node: None,
|
||||
prettier_instances: HashMap::default(),
|
||||
};
|
||||
for worktree in worktrees {
|
||||
let _ = this.add_worktree(&worktree, cx);
|
||||
|
@ -795,8 +812,16 @@ impl Project {
|
|||
let http_client = util::http::FakeHttpClient::with_404_response();
|
||||
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 project =
|
||||
cx.update(|cx| Project::local(client, user_store, Arc::new(languages), fs, cx));
|
||||
let project = cx.update(|cx| {
|
||||
Project::local(
|
||||
client,
|
||||
node_runtime::FakeNodeRuntime::new(),
|
||||
user_store,
|
||||
Arc::new(languages),
|
||||
fs,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
for path in root_paths {
|
||||
let (tree, _) = project
|
||||
.update(cx, |project, cx| {
|
||||
|
@ -810,19 +835,37 @@ impl 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>) {
|
||||
let mut language_servers_to_start = Vec::new();
|
||||
let mut language_formatters_to_check = Vec::new();
|
||||
for buffer in self.opened_buffers.values() {
|
||||
if let Some(buffer) = buffer.upgrade(cx) {
|
||||
let buffer = buffer.read(cx);
|
||||
if let Some((file, language)) = buffer.file().zip(buffer.language()) {
|
||||
let settings = language_settings(Some(language), Some(file), cx);
|
||||
let buffer_file = File::from_dyn(buffer.file());
|
||||
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 let Some(file) = File::from_dyn(Some(file)) {
|
||||
if let Some(file) = buffer_file {
|
||||
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();
|
||||
}
|
||||
|
||||
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.
|
||||
for (worktree, language) in language_servers_to_start {
|
||||
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();
|
||||
if let Some(tree) = worktree.read(cx).as_local() {
|
||||
self.start_language_servers(&worktree, tree.abs_path().clone(), new_language, cx);
|
||||
|
@ -3949,7 +4016,7 @@ impl Project {
|
|||
push_to_history: bool,
|
||||
trigger: FormatTrigger,
|
||||
cx: &mut ModelContext<Project>,
|
||||
) -> Task<Result<ProjectTransaction>> {
|
||||
) -> Task<anyhow::Result<ProjectTransaction>> {
|
||||
if self.is_local() {
|
||||
let mut buffers_with_paths_and_servers = buffers
|
||||
.into_iter()
|
||||
|
@ -4027,6 +4094,7 @@ impl Project {
|
|||
enum FormatOperation {
|
||||
Lsp(Vec<(Range<Anchor>, String)>),
|
||||
External(Diff),
|
||||
Prettier(Diff),
|
||||
}
|
||||
|
||||
// Apply language-specific formatting using either a language server
|
||||
|
@ -4062,8 +4130,8 @@ impl Project {
|
|||
| (_, FormatOnSave::External { command, arguments }) => {
|
||||
if let Some(buffer_abs_path) = buffer_abs_path {
|
||||
format_operation = Self::format_via_external_command(
|
||||
&buffer,
|
||||
&buffer_abs_path,
|
||||
buffer,
|
||||
buffer_abs_path,
|
||||
&command,
|
||||
&arguments,
|
||||
&mut cx,
|
||||
|
@ -4076,6 +4144,69 @@ impl Project {
|
|||
.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| {
|
||||
|
@ -4100,6 +4231,9 @@ impl Project {
|
|||
FormatOperation::External(diff) => {
|
||||
b.apply_diff(diff, cx);
|
||||
}
|
||||
FormatOperation::Prettier(diff) => {
|
||||
b.apply_diff(diff, cx);
|
||||
}
|
||||
}
|
||||
|
||||
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_language_servers(&worktree, changes, cx);
|
||||
this.update_local_worktree_settings(&worktree, changes, cx);
|
||||
this.update_prettier_settings(&worktree, changes, cx);
|
||||
cx.emit(Event::WorktreeUpdatedEntries(
|
||||
worktree.read(cx).id(),
|
||||
changes.clone(),
|
||||
|
@ -6252,6 +6387,69 @@ impl Project {
|
|||
.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>) {
|
||||
let new_active_entry = entry.and_then(|project_path| {
|
||||
let worktree = self.worktree_for_id(project_path.worktree_id, cx)?;
|
||||
|
@ -8109,6 +8307,236 @@ impl Project {
|
|||
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(
|
||||
|
|
|
@ -494,6 +494,7 @@ fn main() {
|
|||
let project = cx.update(|cx| {
|
||||
Project::local(
|
||||
client.clone(),
|
||||
node_runtime::FakeNodeRuntime::new(),
|
||||
user_store.clone(),
|
||||
languages.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 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 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 SETTINGS: PathBuf = CONFIG_DIR.join("settings.json");
|
||||
pub static ref KEYMAP: PathBuf = CONFIG_DIR.join("keymap.json");
|
||||
|
|
|
@ -30,6 +30,7 @@ gpui = { path = "../gpui" }
|
|||
install_cli = { path = "../install_cli" }
|
||||
language = { path = "../language" }
|
||||
menu = { path = "../menu" }
|
||||
node_runtime = { path = "../node_runtime" }
|
||||
project = { path = "../project" }
|
||||
settings = { path = "../settings" }
|
||||
terminal = { path = "../terminal" }
|
||||
|
|
|
@ -42,6 +42,7 @@ use gpui::{
|
|||
use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ProjectItem};
|
||||
use itertools::Itertools;
|
||||
use language::{LanguageRegistry, Rope};
|
||||
use node_runtime::NodeRuntime;
|
||||
use std::{
|
||||
any::TypeId,
|
||||
borrow::Cow,
|
||||
|
@ -456,6 +457,7 @@ pub struct AppState {
|
|||
pub initialize_workspace:
|
||||
fn(WeakViewHandle<Workspace>, bool, Arc<AppState>, AsyncAppContext) -> Task<Result<()>>,
|
||||
pub background_actions: BackgroundActions,
|
||||
pub node_runtime: Arc<dyn NodeRuntime>,
|
||||
}
|
||||
|
||||
pub struct WorkspaceStore {
|
||||
|
@ -474,6 +476,7 @@ struct Follower {
|
|||
impl AppState {
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn test(cx: &mut AppContext) -> Arc<Self> {
|
||||
use node_runtime::FakeNodeRuntime;
|
||||
use settings::SettingsStore;
|
||||
|
||||
if !cx.has_global::<SettingsStore>() {
|
||||
|
@ -498,6 +501,7 @@ impl AppState {
|
|||
user_store,
|
||||
// channel_store,
|
||||
workspace_store,
|
||||
node_runtime: FakeNodeRuntime::new(),
|
||||
initialize_workspace: |_, _, _, _| Task::ready(Ok(())),
|
||||
build_window_options: |_, _, _| Default::default(),
|
||||
background_actions: || &[],
|
||||
|
@ -816,6 +820,7 @@ impl Workspace {
|
|||
)> {
|
||||
let project_handle = Project::local(
|
||||
app_state.client.clone(),
|
||||
app_state.node_runtime.clone(),
|
||||
app_state.user_store.clone(),
|
||||
app_state.languages.clone(),
|
||||
app_state.fs.clone(),
|
||||
|
@ -3517,6 +3522,8 @@ impl Workspace {
|
|||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn test_new(project: ModelHandle<Project>, cx: &mut ViewContext<Self>) -> Self {
|
||||
use node_runtime::FakeNodeRuntime;
|
||||
|
||||
let client = project.read(cx).client();
|
||||
let user_store = project.read(cx).user_store();
|
||||
|
||||
|
@ -3530,6 +3537,7 @@ impl Workspace {
|
|||
build_window_options: |_, _, _| Default::default(),
|
||||
initialize_workspace: |_, _, _, _| Task::ready(Ok(())),
|
||||
background_actions: || &[],
|
||||
node_runtime: FakeNodeRuntime::new(),
|
||||
});
|
||||
Self::new(0, project, app_state, cx)
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use anyhow::{anyhow, Result};
|
||||
use async_trait::async_trait;
|
||||
use futures::StreamExt;
|
||||
use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
|
||||
use language::{BundledFormatter, LanguageServerName, LspAdapter, LspAdapterDelegate};
|
||||
use lsp::LanguageServerBinary;
|
||||
use node_runtime::NodeRuntime;
|
||||
use serde_json::json;
|
||||
|
@ -96,6 +96,10 @@ impl LspAdapter for CssLspAdapter {
|
|||
"provideFormatter": true
|
||||
}))
|
||||
}
|
||||
|
||||
fn enabled_formatters(&self) -> Vec<BundledFormatter> {
|
||||
vec![BundledFormatter::prettier("css")]
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_cached_server_binary(
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use anyhow::{anyhow, Result};
|
||||
use async_trait::async_trait;
|
||||
use futures::StreamExt;
|
||||
use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
|
||||
use language::{BundledFormatter, LanguageServerName, LspAdapter, LspAdapterDelegate};
|
||||
use lsp::LanguageServerBinary;
|
||||
use node_runtime::NodeRuntime;
|
||||
use serde_json::json;
|
||||
|
@ -96,6 +96,10 @@ impl LspAdapter for HtmlLspAdapter {
|
|||
"provideFormatter": true
|
||||
}))
|
||||
}
|
||||
|
||||
fn enabled_formatters(&self) -> Vec<BundledFormatter> {
|
||||
vec![BundledFormatter::prettier("html")]
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_cached_server_binary(
|
||||
|
|
|
@ -4,7 +4,9 @@ use collections::HashMap;
|
|||
use feature_flags::FeatureFlagAppExt;
|
||||
use futures::{future::BoxFuture, FutureExt, StreamExt};
|
||||
use gpui::AppContext;
|
||||
use language::{LanguageRegistry, LanguageServerName, LspAdapter, LspAdapterDelegate};
|
||||
use language::{
|
||||
BundledFormatter, LanguageRegistry, LanguageServerName, LspAdapter, LspAdapterDelegate,
|
||||
};
|
||||
use lsp::LanguageServerBinary;
|
||||
use node_runtime::NodeRuntime;
|
||||
use serde_json::json;
|
||||
|
@ -144,6 +146,10 @@ impl LspAdapter for JsonLspAdapter {
|
|||
async fn language_ids(&self) -> HashMap<String, String> {
|
||||
[("JSON".into(), "jsonc".into())].into_iter().collect()
|
||||
}
|
||||
|
||||
fn enabled_formatters(&self) -> Vec<BundledFormatter> {
|
||||
vec![BundledFormatter::prettier("json")]
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_cached_server_binary(
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use anyhow::{anyhow, Result};
|
||||
use async_trait::async_trait;
|
||||
use futures::StreamExt;
|
||||
use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
|
||||
use language::{BundledFormatter, LanguageServerName, LspAdapter, LspAdapterDelegate};
|
||||
use lsp::LanguageServerBinary;
|
||||
use node_runtime::NodeRuntime;
|
||||
use serde_json::json;
|
||||
|
@ -95,6 +95,13 @@ impl LspAdapter for SvelteLspAdapter {
|
|||
"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(
|
||||
|
|
|
@ -6,7 +6,7 @@ use futures::{
|
|||
FutureExt, StreamExt,
|
||||
};
|
||||
use gpui::AppContext;
|
||||
use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
|
||||
use language::{BundledFormatter, LanguageServerName, LspAdapter, LspAdapterDelegate};
|
||||
use lsp::LanguageServerBinary;
|
||||
use node_runtime::NodeRuntime;
|
||||
use serde_json::{json, Value};
|
||||
|
@ -127,6 +127,13 @@ impl LspAdapter for TailwindLspAdapter {
|
|||
.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(
|
||||
|
|
|
@ -4,7 +4,7 @@ use async_tar::Archive;
|
|||
use async_trait::async_trait;
|
||||
use futures::{future::BoxFuture, FutureExt};
|
||||
use gpui::AppContext;
|
||||
use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
|
||||
use language::{BundledFormatter, LanguageServerName, LspAdapter, LspAdapterDelegate};
|
||||
use lsp::{CodeActionKind, LanguageServerBinary};
|
||||
use node_runtime::NodeRuntime;
|
||||
use serde_json::{json, Value};
|
||||
|
@ -161,6 +161,10 @@ impl LspAdapter for TypeScriptLspAdapter {
|
|||
"provideFormatter": true
|
||||
}))
|
||||
}
|
||||
|
||||
fn enabled_formatters(&self) -> Vec<BundledFormatter> {
|
||||
vec![BundledFormatter::prettier("typescript")]
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_cached_ts_server_binary(
|
||||
|
@ -309,6 +313,10 @@ impl LspAdapter for EsLintLspAdapter {
|
|||
async fn initialization_options(&self) -> Option<serde_json::Value> {
|
||||
None
|
||||
}
|
||||
|
||||
fn enabled_formatters(&self) -> Vec<BundledFormatter> {
|
||||
vec![BundledFormatter::prettier("babel")]
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_cached_eslint_server_binary(
|
||||
|
|
|
@ -3,7 +3,8 @@ use async_trait::async_trait;
|
|||
use futures::{future::BoxFuture, FutureExt, StreamExt};
|
||||
use gpui::AppContext;
|
||||
use language::{
|
||||
language_settings::all_language_settings, LanguageServerName, LspAdapter, LspAdapterDelegate,
|
||||
language_settings::all_language_settings, BundledFormatter, LanguageServerName, LspAdapter,
|
||||
LspAdapterDelegate,
|
||||
};
|
||||
use lsp::LanguageServerBinary;
|
||||
use node_runtime::NodeRuntime;
|
||||
|
@ -108,6 +109,10 @@ impl LspAdapter for YamlLspAdapter {
|
|||
}))
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn enabled_formatters(&self) -> Vec<BundledFormatter> {
|
||||
vec![BundledFormatter::prettier("yaml")]
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_cached_server_binary(
|
||||
|
|
|
@ -154,7 +154,12 @@ fn main() {
|
|||
semantic_index::init(fs.clone(), http.clone(), languages.clone(), cx);
|
||||
vim::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);
|
||||
component_test::init(cx);
|
||||
|
||||
|
@ -181,6 +186,7 @@ fn main() {
|
|||
initialize_workspace,
|
||||
background_actions,
|
||||
workspace_store,
|
||||
node_runtime,
|
||||
});
|
||||
cx.set_global(Arc::downgrade(&app_state));
|
||||
|
||||
|
|
Loading…
Reference in a new issue