@@ -6,7 +6,7 @@ use ruff_python_ast::visitor::source_order::{
66 SourceOrderVisitor , TraversalSignal , walk_body, walk_node,
77} ;
88use ruff_python_ast:: { AnyNodeRef , Stmt , StmtClassDef , StmtFunctionDef } ;
9- use ruff_python_trivia:: CommentLinePosition ;
9+ use ruff_python_trivia:: { CommentLinePosition , is_python_whitespace } ;
1010use ruff_source_file:: { LineRanges , UniversalNewlines } ;
1111use ruff_text_size:: { Ranged , TextRange , TextSize } ;
1212
@@ -190,7 +190,7 @@ impl FoldingRangeVisitor<'_> {
190190
191191 /// Check if there's a blank line appearing anywhere between two positions.
192192 fn has_blank_line_between ( & self , start : TextSize , end : TextSize ) -> bool {
193- let mut count = 0 ;
193+ let mut count = 0usize ;
194194 for line in self . source [ TextRange :: new ( start, end) ] . universal_newlines ( ) {
195195 if !line. is_empty ( ) {
196196 return count >= 2 ;
@@ -244,9 +244,18 @@ impl FoldingRangeVisitor<'_> {
244244 !text. starts_with ( "region" ) && !text. starts_with ( "endregion" ) ;
245245
246246 if is_non_region_comment {
247- // Extend the current comment block unless a blank line forces a new folding range .
247+ // Extend the current comment block unless a blank line or a statement separates the comments .
248248 if let Some ( ref mut comment_block_range) = comment_block_range {
249- if self . has_blank_line_between ( comment_block_range. end ( ) , comment_range. start ( ) )
249+ let has_text_between = !self . source
250+ [ TextRange :: new ( comment_block_range. end ( ) , comment_range. start ( ) ) ]
251+ . trim_matches ( |c| matches ! ( c, '\n' | '\r' ) || is_python_whitespace ( c) )
252+ . is_empty ( ) ;
253+
254+ if has_text_between
255+ || self . has_blank_line_between (
256+ comment_block_range. end ( ) ,
257+ comment_range. start ( ) ,
258+ )
250259 {
251260 self . add_range (
252261 FoldingRange :: from ( * comment_block_range)
@@ -1758,6 +1767,112 @@ with open("file.txt") as f:
17581767 "# ) ;
17591768 }
17601769
1770+ /// Regression test for <https://github.com/astral-sh/ty/issues/3106>
1771+ #[ test]
1772+ fn folding_range_comment_block ( ) {
1773+ let test = CursorTest :: builder ( )
1774+ . source (
1775+ "main.py" ,
1776+ r#"
1777+ def chunk_date_range():
1778+ """Split a date range into chunks respecting the maximum days limit.
1779+
1780+ The API has a 1-month limit, so this function splits larger ranges
1781+ into smaller chunks that can be requested individually.
1782+ """
1783+ # Handle both date-only and datetime strings
1784+ a = 10
1785+
1786+ while current <= end:
1787+ # Calculate the end of the current chunk
1788+ # Go to the last day of the current month
1789+ b = 20
1790+
1791+ # Don't exceed the overall end date or max_days limit
1792+ c = 30
1793+ <CURSOR>
1794+ "# ,
1795+ )
1796+ . build ( ) ;
1797+
1798+ assert_snapshot ! ( test. folding_ranges( ) , @r#"
1799+ info[folding-range]: Folding Range
1800+ --> main.py:2:17
1801+ |
1802+ 2 | / def chunk_date_range():
1803+ 3 | | """Split a date range into chunks respecting the maximum days limit.
1804+ 4 | |
1805+ 5 | | The API has a 1-month limit, so this function splits larger ranges
1806+ 6 | | into smaller chunks that can be requested individually.
1807+ 7 | | """
1808+ 8 | | # Handle both date-only and datetime strings
1809+ 9 | | a = 10
1810+ 10 | |
1811+ 11 | | while current <= end:
1812+ 12 | | # Calculate the end of the current chunk
1813+ 13 | | # Go to the last day of the current month
1814+ 14 | | b = 20
1815+ 15 | |
1816+ 16 | | # Don't exceed the overall end date or max_days limit
1817+ 17 | | c = 30
1818+ | |______________________________^
1819+ |
1820+
1821+ info[folding-range]: Folding Range (comment)
1822+ --> main.py:3:21
1823+ |
1824+ 2 | def chunk_date_range():
1825+ 3 | / """Split a date range into chunks respecting the maximum days limit.
1826+ 4 | |
1827+ 5 | | The API has a 1-month limit, so this function splits larger ranges
1828+ 6 | | into smaller chunks that can be requested individually.
1829+ 7 | | """
1830+ | |_______________________^
1831+ 8 | # Handle both date-only and datetime strings
1832+ 9 | a = 10
1833+ |
1834+
1835+ info[folding-range]: Folding Range
1836+ --> main.py:3:21
1837+ |
1838+ 2 | def chunk_date_range():
1839+ 3 | / """Split a date range into chunks respecting the maximum days limit.
1840+ 4 | |
1841+ 5 | | The API has a 1-month limit, so this function splits larger ranges
1842+ 6 | | into smaller chunks that can be requested individually.
1843+ 7 | | """
1844+ | |_______________________^
1845+ 8 | # Handle both date-only and datetime strings
1846+ 9 | a = 10
1847+ |
1848+
1849+ info[folding-range]: Folding Range
1850+ --> main.py:11:21
1851+ |
1852+ 9 | a = 10
1853+ 10 |
1854+ 11 | / while current <= end:
1855+ 12 | | # Calculate the end of the current chunk
1856+ 13 | | # Go to the last day of the current month
1857+ 14 | | b = 20
1858+ 15 | |
1859+ 16 | | # Don't exceed the overall end date or max_days limit
1860+ 17 | | c = 30
1861+ | |______________________________^
1862+ |
1863+
1864+ info[folding-range]: Folding Range (comment)
1865+ --> main.py:12:1
1866+ |
1867+ 11 | while current <= end:
1868+ 12 | / # Calculate the end of the current chunk
1869+ 13 | | # Go to the last day of the current month
1870+ | |_________________________________________________________________^
1871+ 14 | b = 20
1872+ |
1873+ "# ) ;
1874+ }
1875+
17611876 #[ test]
17621877 fn test_folding_multiline ( ) {
17631878 // A class definition on a single line shouldn't have
0 commit comments