diff --git a/Cargo.lock b/Cargo.lock index 18ecec774..5d7a81191 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2445,6 +2445,7 @@ dependencies = [ name = "squawk-fmt" version = "2.63.0" dependencies = [ + "annotate-snippets", "anyhow", "camino", "clap", @@ -2556,6 +2557,7 @@ dependencies = [ "serde", "serde_json", "simplelog", + "squawk-fmt", "squawk-ide", "squawk-lexer", "squawk-line-index", diff --git a/Cargo.toml b/Cargo.toml index 63b28af5e..2a705b3b3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -83,6 +83,7 @@ rustc-hash = "2.1.1" # local # we have to make the versions explicit otherwise `cargo publish` won't work +squawk-fmt = { path = "./crates/squawk_fmt", version = "2.63.0" } squawk-github = { path = "./crates/squawk_github", version = "2.63.0" } squawk-ide = { path = "./crates/squawk_ide", version = "2.63.0" } squawk-lexer = { path = "./crates/squawk_lexer", version = "2.63.0" } diff --git a/crates/squawk_fmt/Cargo.toml b/crates/squawk_fmt/Cargo.toml index 1634ccc7a..a193907fb 100644 --- a/crates/squawk_fmt/Cargo.toml +++ b/crates/squawk_fmt/Cargo.toml @@ -25,6 +25,7 @@ squawk-line-index.workspace = true rowan.workspace = true clap.workspace = true anyhow.workspace = true +annotate-snippets.workspace = true [dev-dependencies] insta.workspace = true diff --git a/crates/squawk_fmt/src/fmt.rs b/crates/squawk_fmt/src/fmt.rs index ca49fdea3..e37fc18bb 100644 --- a/crates/squawk_fmt/src/fmt.rs +++ b/crates/squawk_fmt/src/fmt.rs @@ -22951,25 +22951,13 @@ fn build_target<'a>(target: ast::Target) -> Option> { Some(doc) } -pub fn fmt(text: &str) -> Result { - let line_ending = find_newline(text) - .map(|(_, ending)| ending) - .unwrap_or_default(); - +pub fn fmt(source_file: &ast::SourceFile, line_ending: LineEnding) -> Result { let line_break = match line_ending { LineEnding::Cr => LineBreak::Cr, LineEnding::CrLf => LineBreak::Crlf, LineEnding::Lf => LineBreak::Lf, }; - - let parse = ast::SourceFile::parse(text); - let file = parse.tree(); - debug_assert_eq!( - parse.errors(), - vec![], - "should bail out when there's parse errors" - ); - let doc = build_source_file(&file); + let doc = build_source_file(source_file); Ok(print( &doc, @@ -22979,3 +22967,17 @@ pub fn fmt(text: &str) -> Result { }, )) } + +pub fn fmt_str(text: &str) -> Result { + let line_ending = find_newline(text) + .map(|(_, ending)| ending) + .unwrap_or_default(); + let parse = ast::SourceFile::parse(text); + let errors = parse.errors(); + if !errors.is_empty() { + let messages = errors.iter().map(ToString::to_string).join("\n"); + anyhow::bail!(messages); + } + + fmt(&parse.tree(), line_ending) +} diff --git a/crates/squawk_fmt/src/lib.rs b/crates/squawk_fmt/src/lib.rs index c665cfd6e..69247dd87 100644 --- a/crates/squawk_fmt/src/lib.rs +++ b/crates/squawk_fmt/src/lib.rs @@ -1,3 +1,3 @@ mod fmt; -pub use fmt::fmt; +pub use fmt::{fmt, fmt_str}; diff --git a/crates/squawk_fmt/src/main.rs b/crates/squawk_fmt/src/main.rs index d66289c91..23d5c2c5a 100644 --- a/crates/squawk_fmt/src/main.rs +++ b/crates/squawk_fmt/src/main.rs @@ -1,8 +1,11 @@ -use std::io::{self, Read}; +use std::io::{self, Read, Write}; use std::path::PathBuf; +use std::process::ExitCode; +use annotate_snippets::{AnnotationKind, Level, Renderer, Snippet, renderer::DecorStyle}; use anyhow::Result; use clap::Parser; +use squawk_syntax::SourceFile; #[derive(Parser)] #[command(name = "squawk-fmt")] @@ -11,18 +14,43 @@ struct Cli { file: Option, } -fn main() -> Result<()> { +fn main() -> Result { let cli = Cli::parse(); - let input = match cli.file { - Some(path) => std::fs::read_to_string(&path)?, + let (input, path) = match cli.file { + Some(path) => { + let input = std::fs::read_to_string(&path)?; + (input, path.display().to_string()) + } None => { let mut buf = String::new(); io::stdin().read_to_string(&mut buf)?; - buf + (buf, "stdin".to_string()) } }; - print!("{}", squawk_fmt::fmt(&input)?); - Ok(()) + let parse = SourceFile::parse(&input); + let errors = parse.errors(); + if !errors.is_empty() { + let renderer = Renderer::styled().decor_style(DecorStyle::Unicode); + let stderr = io::stderr(); + let mut stderr = stderr.lock(); + + for error in errors { + let snippet = Snippet::source(&input) + .path(&path) + .fold(true) + .annotation(AnnotationKind::Primary.span(error.range().into())); + let group = Level::ERROR + .primary_title(error.message()) + .id("syntax-error") + .element(snippet); + writeln!(stderr, "{}", renderer.render(&[group]))?; + } + + return Ok(ExitCode::FAILURE); + } + + write!(io::stdout().lock(), "{}", squawk_fmt::fmt_str(&input)?)?; + Ok(ExitCode::SUCCESS) } diff --git a/crates/squawk_fmt/tests/tests.rs b/crates/squawk_fmt/tests/tests.rs index 217f4a0dc..cb055bde6 100644 --- a/crates/squawk_fmt/tests/tests.rs +++ b/crates/squawk_fmt/tests/tests.rs @@ -15,7 +15,7 @@ fn fmt(fixture: Fixture<&str>) { .and_then(|x| x.strip_suffix(".sql")) .unwrap(); - let formatted = squawk_fmt::fmt(content).unwrap(); + let formatted = squawk_fmt::fmt_str(content).unwrap(); assert_no_dropped_tokens(content, &formatted); @@ -42,7 +42,7 @@ fn fmt_with_line_ending(line_ending: &str) -> String { ] .join(line_ending); - match squawk_fmt::fmt(&sql) { + match squawk_fmt::fmt_str(&sql) { Ok(formatted) => { assert_no_dropped_tokens(&sql, &formatted); formatted.replace('\r', "") diff --git a/crates/squawk_server/Cargo.toml b/crates/squawk_server/Cargo.toml index f9f8d2367..fbc138bcc 100644 --- a/crates/squawk_server/Cargo.toml +++ b/crates/squawk_server/Cargo.toml @@ -24,6 +24,7 @@ rowan.workspace = true salsa.workspace = true serde.workspace = true serde_json.workspace = true +squawk-fmt.workspace = true squawk-ide.workspace = true squawk-lexer.workspace = true squawk-linter.workspace = true diff --git a/crates/squawk_server/src/global_state.rs b/crates/squawk_server/src/global_state.rs index 874a1ca57..5ece87124 100644 --- a/crates/squawk_server/src/global_state.rs +++ b/crates/squawk_server/src/global_state.rs @@ -17,17 +17,19 @@ use url::Url; use gen_lsp_types::{ CodeActionRequest, CompletionRequest, DefinitionRequest, DocumentDiagnosticRequest, - DocumentSymbolRequest, FoldingRangeRequest, HoverRequest, InlayHintRequest, ReferencesRequest, - SelectionRangeRequest, SemanticTokensRangeRequest, SemanticTokensRequest, ShutdownRequest, + DocumentFormattingRequest, DocumentSymbolRequest, FoldingRangeRequest, HoverRequest, + InlayHintRequest, ReferencesRequest, SelectionRangeRequest, SemanticTokensRangeRequest, + SemanticTokensRequest, ShutdownRequest, }; use crate::dispatch::{NotificationDispatcher, RequestDispatcher}; use crate::handlers::{ SyntaxTreeRequest, TokensRequest, handle_cancel, handle_code_action, handle_completion, handle_did_change, handle_did_close, handle_did_open, handle_document_diagnostic, - handle_document_symbol, handle_folding_range, handle_goto_definition, handle_hover, - handle_inlay_hints, handle_references, handle_selection_range, handle_semantic_tokens_full, - handle_semantic_tokens_range, handle_shutdown, handle_syntax_tree, handle_tokens, + handle_document_symbol, handle_folding_range, handle_formatting, handle_goto_definition, + handle_hover, handle_inlay_hints, handle_references, handle_selection_range, + handle_semantic_tokens_full, handle_semantic_tokens_range, handle_shutdown, handle_syntax_tree, + handle_tokens, }; type ReqQueue = lsp_server::ReqQueue<(String, Instant), ()>; @@ -242,6 +244,7 @@ impl GlobalState { .on::(handle_code_action) .on::(handle_inlay_hints) .on::(handle_document_symbol) + .on::(handle_formatting) .on::(handle_folding_range) .on::(handle_document_diagnostic) .on::(handle_syntax_tree) diff --git a/crates/squawk_server/src/handlers.rs b/crates/squawk_server/src/handlers.rs index 9c4af5e66..e99ee4463 100644 --- a/crates/squawk_server/src/handlers.rs +++ b/crates/squawk_server/src/handlers.rs @@ -3,6 +3,7 @@ mod completion; mod diagnostic; mod document_symbol; mod folding_range; +mod formatting; mod goto_definition; mod hover; mod inlay_hints; @@ -19,6 +20,7 @@ pub(crate) use completion::handle_completion; pub(crate) use diagnostic::handle_document_diagnostic; pub(crate) use document_symbol::handle_document_symbol; pub(crate) use folding_range::handle_folding_range; +pub(crate) use formatting::handle_formatting; pub(crate) use goto_definition::handle_goto_definition; pub(crate) use hover::handle_hover; pub(crate) use inlay_hints::handle_inlay_hints; diff --git a/crates/squawk_server/src/handlers/formatting.rs b/crates/squawk_server/src/handlers/formatting.rs new file mode 100644 index 000000000..2593f7fe3 --- /dev/null +++ b/crates/squawk_server/src/handlers/formatting.rs @@ -0,0 +1,37 @@ +use anyhow::Result; +use gen_lsp_types::{DocumentFormattingParams, TextEdit}; +use rowan::{TextRange, TextSize}; +use squawk_ide::db::{line_index, parse}; +use squawk_line_index::find_newline; + +use crate::global_state::Snapshot; +use crate::lsp_utils; + +pub(crate) fn handle_formatting( + snapshot: &Snapshot, + params: DocumentFormattingParams, +) -> Result>> { + let db = snapshot.db(); + let file = snapshot.file(¶ms.text_document.uri).unwrap(); + let content = file.content(db); + let line_ending = find_newline(content) + .map(|(_, ending)| ending) + .unwrap_or_default(); + let parse = parse(db, file); + if !parse.errors().is_empty() { + return Ok(Some(Vec::new())); + } + let formatted = squawk_fmt::fmt(&parse.tree(), line_ending)?; + + if formatted == content.as_ref() { + return Ok(Some(Vec::new())); + } + + let range = TextRange::new(TextSize::default(), TextSize::try_from(content.len())?); + let range = lsp_utils::range(&line_index(db, file), range); + + Ok(Some(vec![TextEdit { + range, + new_text: formatted, + }])) +} diff --git a/crates/squawk_server/src/server.rs b/crates/squawk_server/src/server.rs index 75520a800..af45bbdb6 100644 --- a/crates/squawk_server/src/server.rs +++ b/crates/squawk_server/src/server.rs @@ -1,11 +1,11 @@ use anyhow::Result; use gen_lsp_types::{ CodeActionKind, CodeActionOptions, CodeActionProvider, CompletionOptions, DefinitionProvider, - DiagnosticOptions, DiagnosticProvider, DocumentSymbolProvider, FoldingRangeProvider, Full, - HoverProvider, InitializeParams, InlayHintProvider, ReferencesProvider, SelectionRangeProvider, - SemanticTokensLegend, SemanticTokensOptions, SemanticTokensOptionsRange, - SemanticTokensProvider, ServerCapabilities, TextDocumentSync, TextDocumentSyncKind, - WorkDoneProgressOptions, + DiagnosticOptions, DiagnosticProvider, DocumentFormattingProvider, DocumentSymbolProvider, + FoldingRangeProvider, Full, HoverProvider, InitializeParams, InlayHintProvider, + ReferencesProvider, SelectionRangeProvider, SemanticTokensLegend, SemanticTokensOptions, + SemanticTokensOptionsRange, SemanticTokensProvider, ServerCapabilities, TextDocumentSync, + TextDocumentSyncKind, WorkDoneProgressOptions, }; use log::info; use lsp_server::Connection; @@ -47,6 +47,7 @@ pub fn run() -> Result<()> { }, })), document_symbol_provider: Some(DocumentSymbolProvider::Bool(true)), + document_formatting_provider: Some(DocumentFormattingProvider::Bool(true)), folding_range_provider: Some(FoldingRangeProvider::Bool(true)), completion_provider: Some(CompletionOptions { resolve_provider: Some(false),