Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added

- Added option `block_newline_gaps` to determine whether newline gaps at the start / end of blocks should be preserved. Defaults to `Never`, which is the original behaviour. ([#857](https://github.com/JohnnyMorganz/StyLua/pull/857))
- StyLua can now run in a language server mode. Start StyLua with `stylua --lsp` and connect with a language client. ([#936](https://github.com/JohnnyMorganz/StyLua/issues/936))

### Changed

Expand Down
87 changes: 73 additions & 14 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ thiserror = "1.0.49"
threadpool = "1.8.1"
toml = "0.8.1"

lsp-server = "0.7"
lsp-types = "0.97"
lsp-textdocument = "0.4.2"

[target.'cfg(target_arch = "wasm32")'.dependencies]
wasm-bindgen = { version = "0.2.81", optional = true }

Expand Down
167 changes: 167 additions & 0 deletions src/cli/lsp.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
use std::convert::TryInto;

use lsp_server::{Connection, ErrorCode, Message, Response};
use lsp_textdocument::{FullTextDocument, TextDocuments};
use lsp_types::{
request::{Formatting, RangeFormatting, Request},
DocumentFormattingParams, DocumentRangeFormattingParams, OneOf, Position, Range,
ServerCapabilities, TextDocumentSyncCapability, TextDocumentSyncKind, TextEdit, Uri,
};
use stylua_lib::{format_code, OutputVerification};

use crate::{config::ConfigResolver, opt};

fn handle_formatting(
uri: &Uri,
document: &FullTextDocument,
range: Option<stylua_lib::Range>,
config_resolver: &mut ConfigResolver,
) -> Option<Vec<TextEdit>> {
if document.language_id() != "lua" && document.language_id() != "luau" {
return None;
}

let contents = document.get_content(None);

let config = config_resolver
.load_configuration(uri.path().as_str().as_ref())
.unwrap_or_default();

let formatted_contents = format_code(contents, config, range, OutputVerification::None).ok()?;

let last_line_idx = document.line_count().saturating_sub(1);
let last_line_offset = document.offset_at(Position::new(last_line_idx, 0));
let last_col = document.content_len() - last_line_offset;

// TODO: We can be smarter about this in the future, and update only the parts that changed (using output_diff)
Some(vec![TextEdit {
range: Range {
start: Position::new(0, 0),
end: Position::new(last_line_idx, last_col),
},
new_text: formatted_contents,
}])
}

fn handle_request(
request: lsp_server::Request,
documents: &TextDocuments,
config_resolver: &mut ConfigResolver,
) -> Response {
match request.method.as_str() {
Formatting::METHOD => {
match serde_json::from_value::<DocumentFormattingParams>(request.params) {
Ok(params) => {
let Some(document) = documents.get_document(&params.text_document.uri) else {
return Response::new_err(
request.id,
ErrorCode::RequestFailed as i32,
format!(
"no document found for '{}'",
params.text_document.uri.as_str()
),
);
};

match handle_formatting(
&params.text_document.uri,
document,
None,
config_resolver,
) {
Some(edits) => Response::new_ok(request.id, edits),
None => Response::new_ok(request.id, serde_json::Value::Null),
}
}
Err(err) => Response::new_err(
request.id,
lsp_server::ErrorCode::RequestFailed as i32,
err.to_string(),
),
}
}
RangeFormatting::METHOD => {
match serde_json::from_value::<DocumentRangeFormattingParams>(request.params) {
Ok(params) => {
let Some(document) = documents.get_document(&params.text_document.uri) else {
return Response::new_err(
request.id,
1,
format!(
"no document found for '{}'",
params.text_document.uri.as_str()
),
);
};

let range = stylua_lib::Range::from_values(
Some(document.offset_at(params.range.start).try_into().unwrap()),
Some(document.offset_at(params.range.end).try_into().unwrap()),
);

match handle_formatting(
&params.text_document.uri,
document,
Some(range),
config_resolver,
) {
Some(edits) => Response::new_ok(request.id, edits),
None => Response::new_ok(request.id, serde_json::Value::Null),
}
}
Err(err) => Response::new_err(
request.id,
lsp_server::ErrorCode::RequestFailed as i32,
err.to_string(),
),
}
}
_ => Response::new_err(
request.id,
lsp_server::ErrorCode::MethodNotFound as i32,
format!("server does not support method '{}'", request.method),
),
}
}

pub fn run(opt: opt::Opt) -> anyhow::Result<()> {
// Load the configuration
let opt_for_config_resolver = opt.clone();
let mut config_resolver = ConfigResolver::new(&opt_for_config_resolver)?;

let (connection, _io_threads) = Connection::stdio();

let capabilities = ServerCapabilities {
document_formatting_provider: Some(OneOf::Left(true)),
text_document_sync: Some(TextDocumentSyncCapability::Kind(
TextDocumentSyncKind::INCREMENTAL,
)),
..Default::default()
};

connection.initialize(serde_json::to_value(capabilities)?)?;

let mut documents = TextDocuments::new();

for msg in &connection.receiver {
match msg {
Message::Request(req) => {
if connection.handle_shutdown(&req)? {
break;
}

let response = handle_request(req, &documents, &mut config_resolver);
connection.sender.send(Message::Response(response))?
}
Message::Response(_) => {}
Message::Notification(notification) => {
documents.listen(notification.method.as_str(), &notification.params);
}
}
}

// HACK: to get our integration tests working and not hanging. We should probably join...
// io_threads.join()?;

Ok(())
}
6 changes: 6 additions & 0 deletions src/cli/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ use stylua_lib::{format_code, Config, OutputVerification, Range};
use crate::config::find_ignore_file_path;

mod config;
mod lsp;
mod opt;
mod output_diff;

Expand Down Expand Up @@ -265,6 +266,11 @@ fn path_is_stylua_ignored(path: &Path, search_parent_directories: bool) -> Resul
fn format(opt: opt::Opt) -> Result<i32> {
debug!("resolved options: {:#?}", opt);

if opt.lsp {
lsp::run(opt)?;
return Ok(0);
}

if opt.files.is_empty() {
bail!("no files provided");
}
Expand Down
Loading
Loading