@@ -82,15 +82,15 @@ use tower_lsp::lsp_types::{
8282use tower_lsp:: lsp_types:: {
8383 CodeActionKind , CodeActionParams , CodeActionResponse , CompletionOptions , CompletionParams ,
8484 CompletionResponse , Diagnostic , DidChangeTextDocumentParams , DidCloseTextDocumentParams ,
85- DidOpenTextDocumentParams , DocumentSymbolParams , DocumentSymbolResponse , ExecuteCommandOptions ,
86- ExecuteCommandParams , FoldingRange , FoldingRangeParams , GotoDefinitionParams ,
87- GotoDefinitionResponse , Hover , HoverParams , HoverProviderCapability , InitializeParams ,
88- InitializeResult , InitializedParams , Location , MessageType , OneOf , Position ,
89- PrepareRenameResponse , Range , ReferenceParams , RenameParams , SemanticTokenModifier ,
90- SemanticTokenType , SemanticTokensFullOptions , SemanticTokensLegend , SemanticTokensOptions ,
91- SemanticTokensParams , SemanticTokensResult , SemanticTokensServerCapabilities ,
92- ServerCapabilities , TextDocumentSyncCapability , TextDocumentSyncKind , Url ,
93- WorkDoneProgressOptions , WorkspaceEdit ,
85+ DidOpenTextDocumentParams , DocumentFormattingParams , DocumentSymbolParams ,
86+ DocumentSymbolResponse , ExecuteCommandOptions , ExecuteCommandParams , FoldingRange ,
87+ FoldingRangeParams , GotoDefinitionParams , GotoDefinitionResponse , Hover , HoverParams ,
88+ HoverProviderCapability , InitializeParams , InitializeResult , InitializedParams , Location ,
89+ MessageType , OneOf , Position , PrepareRenameResponse , Range , ReferenceParams , RenameParams ,
90+ SemanticTokenModifier , SemanticTokenType , SemanticTokensFullOptions , SemanticTokensLegend ,
91+ SemanticTokensOptions , SemanticTokensParams , SemanticTokensResult ,
92+ SemanticTokensServerCapabilities , ServerCapabilities , TextDocumentSyncCapability ,
93+ TextDocumentSyncKind , TextEdit , Url , WorkDoneProgressOptions , WorkspaceEdit ,
9494} ;
9595use tower_lsp:: lsp_types:: { DocumentLink , DocumentLinkOptions , DocumentLinkParams } ;
9696use tower_lsp:: lsp_types:: {
@@ -237,6 +237,7 @@ fn build_server_capabilities() -> ServerCapabilities {
237237 folding_range_provider : Some (
238238 tower_lsp:: lsp_types:: FoldingRangeProviderCapability :: Simple ( true ) ,
239239 ) ,
240+ document_formatting_provider : Some ( OneOf :: Left ( true ) ) ,
240241 ..Default :: default ( )
241242 }
242243}
@@ -583,6 +584,10 @@ impl LanguageServer for HewLanguageServer {
583584 async fn folding_range ( & self , params : FoldingRangeParams ) -> Result < Option < Vec < FoldingRange > > > {
584585 Ok ( handlers:: language_features:: folding_range ( self , & params) )
585586 }
587+
588+ async fn formatting ( & self , params : DocumentFormattingParams ) -> Result < Option < Vec < TextEdit > > > {
589+ Ok ( handlers:: language_features:: formatting ( self , & params) )
590+ }
586591}
587592
588593#[ cfg( test) ]
@@ -5804,4 +5809,104 @@ machine Traffic {
58045809 "for_each_hew_file should have visited def.hew in the real directory despite the symlink sibling"
58055810 ) ;
58065811 }
5812+
5813+ // ── Formatting handler tests ────────────────────────────────────────
5814+
5815+ /// Source that `format_source` does not change → handler returns `Some(vec![])`.
5816+ #[ test]
5817+ fn formatting_already_canonical_returns_empty_edits ( ) {
5818+ // The formatter appends a trailing newline; use a source that is already canonical.
5819+ let source = "fn foo() -> i32 {\n 42\n }\n " ;
5820+ let parse_result = hew_parser:: parse ( source) ;
5821+ let formatted = hew_parser:: fmt:: format_source ( source, & parse_result. program ) ;
5822+ // Pre-condition: the source must actually be canonical for this test to be meaningful.
5823+ assert_eq ! (
5824+ source, formatted,
5825+ "test pre-condition failed: source is not already canonical"
5826+ ) ;
5827+
5828+ let lo = compute_line_offsets ( source) ;
5829+ // Mimic the handler: if formatted == source, return empty edits.
5830+ let edits: Option < Vec < TextEdit > > = if formatted == source {
5831+ Some ( vec ! [ ] )
5832+ } else {
5833+ let range = offset_range_to_lsp ( source, & lo, 0 , source. len ( ) ) ;
5834+ Some ( vec ! [ TextEdit {
5835+ range,
5836+ new_text: formatted,
5837+ } ] )
5838+ } ;
5839+
5840+ assert_eq ! (
5841+ edits,
5842+ Some ( vec![ ] ) ,
5843+ "canonical source must yield empty edit list"
5844+ ) ;
5845+ }
5846+
5847+ /// Source with extra whitespace → handler returns a single whole-document `TextEdit`.
5848+ #[ test]
5849+ fn formatting_uncanonical_returns_single_edit ( ) {
5850+ // Extra spaces between tokens trigger reformatting.
5851+ let source = "fn foo() -> i32 { 42 }" ;
5852+ let parse_result = hew_parser:: parse ( source) ;
5853+ let formatted = hew_parser:: fmt:: format_source ( source, & parse_result. program ) ;
5854+ assert_ne ! (
5855+ source, formatted,
5856+ "test pre-condition failed: source must not be canonical"
5857+ ) ;
5858+
5859+ let lo = compute_line_offsets ( source) ;
5860+ let range = offset_range_to_lsp ( source, & lo, 0 , source. len ( ) ) ;
5861+ let edits: Vec < TextEdit > = vec ! [ TextEdit {
5862+ range,
5863+ new_text: formatted. clone( ) ,
5864+ } ] ;
5865+
5866+ assert_eq ! (
5867+ edits. len( ) ,
5868+ 1 ,
5869+ "expected exactly one whole-document TextEdit"
5870+ ) ;
5871+ assert_eq ! (
5872+ edits[ 0 ] . new_text, formatted,
5873+ "TextEdit new_text must equal format_source output"
5874+ ) ;
5875+ }
5876+
5877+ /// Source containing a non-ASCII character — the range's end column must be
5878+ /// UTF-16 code units, not byte count. 'é' is 2 bytes in UTF-8 but 1 UTF-16
5879+ /// code unit; the byte-based end would differ from the correct UTF-16 end.
5880+ #[ test]
5881+ fn formatting_unicode_source_uses_correct_range ( ) {
5882+ // Extra spaces before and after '-> i32' trigger reformatting,
5883+ // so the handler produces a non-empty TextEdit. The trailing comment
5884+ // preserves 'é' in the formatted output so the source lengths differ
5885+ // between UTF-8 bytes and UTF-16 code units.
5886+ let source = "fn foo() -> i32 { 42 } // héllo" ;
5887+ let parse_result = hew_parser:: parse ( source) ;
5888+ let formatted = hew_parser:: fmt:: format_source ( source, & parse_result. program ) ;
5889+ assert_ne ! (
5890+ source, formatted,
5891+ "test pre-condition failed: source must not be canonical"
5892+ ) ;
5893+
5894+ let lo = compute_line_offsets ( source) ;
5895+ let range = offset_range_to_lsp ( source, & lo, 0 , source. len ( ) ) ;
5896+
5897+ // The source fits on one line. End character must be the UTF-16 length
5898+ // of that line (not the byte length). 'é' is 2 bytes but 1 UTF-16 unit,
5899+ // so byte_len > utf16_len for this source.
5900+ let byte_len = u32:: try_from ( source. len ( ) ) . expect ( "test source fits in u32" ) ;
5901+ let utf16_len =
5902+ u32:: try_from ( source. encode_utf16 ( ) . count ( ) ) . expect ( "test source fits in u32" ) ;
5903+ assert ! (
5904+ byte_len > utf16_len,
5905+ "test pre-condition: source must have byte_len ({byte_len}) > utf16_len ({utf16_len})"
5906+ ) ;
5907+ assert_eq ! (
5908+ range. end. character, utf16_len,
5909+ "end.character must be UTF-16 code units ({utf16_len}), not byte count ({byte_len})"
5910+ ) ;
5911+ }
58075912}
0 commit comments