Skip to content

Commit 2af292d

Browse files
committed
feat: Stylua LSP server
1 parent 7c07c54 commit 2af292d

File tree

5 files changed

+195
-14
lines changed

5 files changed

+195
-14
lines changed

Cargo.lock

Lines changed: 73 additions & 14 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@ thiserror = "1.0.49"
5656
threadpool = "1.8.1"
5757
toml = "0.8.1"
5858

59+
lsp-server = "0.7"
60+
lsp-types = "0.97"
61+
lsp-textdocument = "0.4.2"
62+
5963
[target.'cfg(target_arch = "wasm32")'.dependencies]
6064
wasm-bindgen = { version = "0.2.81", optional = true }
6165

src/cli/lsp.rs

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
use lsp_server::{Connection, Message, RequestId, Response};
2+
use lsp_textdocument::TextDocuments;
3+
use lsp_types::{
4+
request::{Formatting, RangeFormatting, Request},
5+
DocumentFormattingParams, OneOf, Position, Range, ServerCapabilities,
6+
TextDocumentSyncCapability, TextDocumentSyncKind, TextEdit,
7+
};
8+
9+
use crate::{config::ConfigResolver, opt};
10+
11+
pub fn run(opt: opt::Opt) -> anyhow::Result<()> {
12+
// Load the configuration
13+
let opt_for_config_resolver = opt.clone();
14+
let mut config_resolver = ConfigResolver::new(&opt_for_config_resolver)?;
15+
16+
let (connection, io_threads) = Connection::stdio();
17+
18+
let capabilities = ServerCapabilities {
19+
document_formatting_provider: Some(OneOf::Left(true)),
20+
text_document_sync: Some(TextDocumentSyncCapability::Kind(
21+
TextDocumentSyncKind::INCREMENTAL,
22+
)),
23+
..Default::default()
24+
};
25+
26+
connection.initialize(serde_json::to_value(capabilities)?)?;
27+
28+
let mut documents = TextDocuments::new();
29+
30+
for msg in &connection.receiver {
31+
match msg {
32+
Message::Request(req) => {
33+
if connection.handle_shutdown(&req)? {
34+
break;
35+
}
36+
37+
match req.method.as_str() {
38+
Formatting::METHOD => {
39+
let result =
40+
match serde_json::from_value::<DocumentFormattingParams>(req.params) {
41+
Ok(params) => {
42+
let res = handle_formatting(
43+
req.id.clone(),
44+
params,
45+
&mut config_resolver,
46+
&documents,
47+
);
48+
49+
res.unwrap_or(Response::new_ok(req.id, serde_json::Value::Null))
50+
}
51+
Err(err) => Response::new_err(req.id, 1, err.to_string()),
52+
};
53+
54+
connection.sender.send(Message::Response(result))?;
55+
}
56+
RangeFormatting::METHOD => {
57+
// TODO: Would be cool to support this in the future
58+
}
59+
_ => {}
60+
}
61+
}
62+
Message::Response(_) => {}
63+
Message::Notification(notification) => {
64+
documents.listen(notification.method.as_str(), &notification.params);
65+
}
66+
}
67+
}
68+
69+
io_threads.join()?;
70+
71+
Ok(())
72+
}
73+
74+
fn handle_formatting(
75+
id: RequestId,
76+
params: DocumentFormattingParams,
77+
config_resolver: &mut ConfigResolver,
78+
documents: &TextDocuments,
79+
) -> Option<Response> {
80+
let uri = params.text_document.uri;
81+
let path = uri.path().as_str();
82+
83+
let src = documents.get_document_content(&uri, None)?;
84+
85+
let config = config_resolver
86+
.load_configuration(path.as_ref())
87+
.unwrap_or_default();
88+
89+
let new_text =
90+
stylua_lib::format_code(src, config, None, stylua_lib::OutputVerification::None).ok()?;
91+
92+
if new_text == src {
93+
return None;
94+
}
95+
96+
// TODO: We can be smarter about this in the future, and update only the parts that changed
97+
let edit = TextEdit {
98+
range: Range {
99+
start: Position::new(0, 0),
100+
end: Position::new(u32::MAX, 0),
101+
},
102+
new_text,
103+
};
104+
105+
let edits: Vec<TextEdit> = vec![edit];
106+
107+
Some(Response::new_ok(id, edits))
108+
}

src/cli/main.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ use stylua_lib::{format_code, Config, OutputVerification, Range};
1919
use crate::config::find_ignore_file_path;
2020

2121
mod config;
22+
mod lsp;
2223
mod opt;
2324
mod output_diff;
2425

@@ -255,6 +256,11 @@ fn path_is_stylua_ignored(path: &Path, search_parent_directories: bool) -> Resul
255256
fn format(opt: opt::Opt) -> Result<i32> {
256257
debug!("resolved options: {:#?}", opt);
257258

259+
if opt.lsp {
260+
lsp::run(opt)?;
261+
return Ok(0);
262+
}
263+
258264
if opt.files.is_empty() {
259265
bail!("no files provided");
260266
}

src/cli/opt.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,10 @@ pub struct Opt {
108108
/// Respect .styluaignore and glob matching for file paths provided directly to the tool
109109
#[structopt(long)]
110110
pub respect_ignores: bool,
111+
112+
/// Run Stylua LSP server
113+
#[structopt(long)]
114+
pub lsp: bool,
111115
}
112116

113117
#[derive(ArgEnum, Clone, Copy, Debug, PartialEq, Eq)]

0 commit comments

Comments
 (0)