11use ruff_db:: files:: File ;
22use ruff_db:: parsed:: parsed_module;
33use ruff_db:: source:: source_text;
4+ use ruff_python_ast:: token:: { TokenKind , Tokens } ;
45use ruff_python_ast:: visitor:: source_order:: { SourceOrderVisitor , TraversalSignal , walk_body} ;
5- use ruff_python_ast:: { AnyNodeRef , Stmt } ;
6+ use ruff_python_ast:: { AnyNodeRef , Stmt , StmtClassDef , StmtFunctionDef } ;
67use ruff_source_file:: { Line , UniversalNewlines } ;
78use ruff_text_size:: { Ranged , TextLen , TextRange , TextSize } ;
89
@@ -51,6 +52,7 @@ pub fn folding_ranges(db: &dyn Db, file: File) -> Vec<FoldingRange> {
5152 let mut visitor = FoldingRangeVisitor {
5253 source : source. as_str ( ) ,
5354 ranges : vec ! [ ] ,
55+ tokens : parsed. tokens ( ) ,
5456 } ;
5557 visitor. visit_body ( parsed. suite ( ) ) ;
5658
@@ -67,6 +69,7 @@ pub fn folding_ranges(db: &dyn Db, file: File) -> Vec<FoldingRange> {
6769struct FoldingRangeVisitor < ' a > {
6870 source : & ' a str ,
6971 ranges : Vec < FoldingRange > ,
72+ tokens : & ' a Tokens ,
7073}
7174
7275impl < ' a > FoldingRangeVisitor < ' a > {
@@ -223,14 +226,54 @@ impl<'a> FoldingRangeVisitor<'a> {
223226 }
224227 self . add_range ( FoldingRange :: from ( first_stmt. range ( ) ) . with_kind ( FoldingRangeKind :: Comment ) ) ;
225228 }
229+
230+ /// Add a folding range for the function or class definition.
231+ ///
232+ /// `target` is checked for in `search_range`, and is used as the start if found.
233+ fn add_def_range ( & mut self , target : TokenKind , search_range : TextRange , end : TextSize ) {
234+ let target_token = self
235+ . tokens
236+ . in_range ( search_range)
237+ . iter ( )
238+ . find ( |tok| tok. kind ( ) == target) ;
239+ if let Some ( tok) = target_token {
240+ let range = TextRange :: new ( tok. start ( ) , end) ;
241+ self . add_range ( range) ;
242+ }
243+ }
244+
245+ /// Add a folding range for function definitions, excluding decorators.
246+ fn add_function_def_range ( & mut self , func : & StmtFunctionDef ) {
247+ if let Some ( decorator) = func. decorator_list . last ( ) {
248+ let target = if func. is_async {
249+ TokenKind :: Async
250+ } else {
251+ TokenKind :: Def
252+ } ;
253+ let search_range = TextRange :: new ( decorator. end ( ) , func. name . start ( ) ) ;
254+ self . add_def_range ( target, search_range, func. end ( ) ) ;
255+ } else {
256+ self . add_range ( func. range ( ) ) ;
257+ }
258+ }
259+
260+ /// Add a folding range for class definitions, excluding decorators.
261+ fn add_class_def_range ( & mut self , class : & StmtClassDef ) {
262+ if let Some ( decorator) = class. decorator_list . last ( ) {
263+ let search_range = TextRange :: new ( decorator. end ( ) , class. name . start ( ) ) ;
264+ self . add_def_range ( TokenKind :: Class , search_range, class. end ( ) ) ;
265+ } else {
266+ self . add_range ( class. range ( ) ) ;
267+ }
268+ }
226269}
227270
228271impl SourceOrderVisitor < ' _ > for FoldingRangeVisitor < ' _ > {
229272 fn enter_node ( & mut self , node : AnyNodeRef < ' _ > ) -> TraversalSignal {
230273 match node {
231274 // Compound statements that create folding regions
232275 AnyNodeRef :: StmtFunctionDef ( func) => {
233- self . add_range ( func. range ( ) ) ;
276+ self . add_function_def_range ( func) ;
234277 // Note that this may be duplicative with folding
235278 // ranges added for string literals. But I don't think
236279 // the LSP protocol specifies that this is a problem.
@@ -241,7 +284,7 @@ impl SourceOrderVisitor<'_> for FoldingRangeVisitor<'_> {
241284 self . add_docstring_range ( & func. body ) ;
242285 }
243286 AnyNodeRef :: StmtClassDef ( class) => {
244- self . add_range ( class. range ( ) ) ;
287+ self . add_class_def_range ( class) ;
245288 // See comment above for class docstrings about this
246289 // being duplicative with adding folding ranges for
247290 // string literals.
@@ -1683,6 +1726,220 @@ with open("file.txt") as f:
16831726 }
16841727 }
16851728
1729+ #[ test]
1730+ fn test_folding_range_decorated_function_single ( ) {
1731+ let test = CursorTest :: builder ( )
1732+ . source (
1733+ "main.py" ,
1734+ r#"
1735+ @decorator
1736+ def my_function():
1737+ pass
1738+ <CURSOR>
1739+ "# ,
1740+ )
1741+ . build ( ) ;
1742+
1743+ assert_snapshot ! ( test. folding_ranges( ) , @"
1744+ info[folding-range]: Folding Range
1745+ --> main.py:3:1
1746+ |
1747+ 2 | @decorator
1748+ 3 | / def my_function():
1749+ 4 | | pass
1750+ | |________^
1751+ |
1752+ " ) ;
1753+ }
1754+
1755+ #[ test]
1756+ fn test_folding_range_decorated_function_multiple ( ) {
1757+ let test = CursorTest :: builder ( )
1758+ . source (
1759+ "main.py" ,
1760+ r#"
1761+ @first
1762+ @second
1763+ @third
1764+ def my_function():
1765+ pass
1766+ <CURSOR>
1767+ "# ,
1768+ )
1769+ . build ( ) ;
1770+
1771+ assert_snapshot ! ( test. folding_ranges( ) , @"
1772+ info[folding-range]: Folding Range
1773+ --> main.py:5:1
1774+ |
1775+ 3 | @second
1776+ 4 | @third
1777+ 5 | / def my_function():
1778+ 6 | | pass
1779+ | |________^
1780+ |
1781+ " ) ;
1782+ }
1783+
1784+ #[ test]
1785+ fn test_folding_range_decorated_class_single ( ) {
1786+ let test = CursorTest :: builder ( )
1787+ . source (
1788+ "main.py" ,
1789+ r#"
1790+ @dataclass
1791+ class MyClass:
1792+ value: int
1793+ name: str
1794+ <CURSOR>
1795+ "# ,
1796+ )
1797+ . build ( ) ;
1798+
1799+ // Single decorator is one line, so no decorator folding range is emitted.
1800+ assert_snapshot ! ( test. folding_ranges( ) , @"
1801+ info[folding-range]: Folding Range
1802+ --> main.py:3:1
1803+ |
1804+ 2 | @dataclass
1805+ 3 | / class MyClass:
1806+ 4 | | value: int
1807+ 5 | | name: str
1808+ | |_____________^
1809+ |
1810+ " ) ;
1811+ }
1812+
1813+ #[ test]
1814+ fn test_folding_range_decorated_class_multiple ( ) {
1815+ let test = CursorTest :: builder ( )
1816+ . source (
1817+ "main.py" ,
1818+ r#"
1819+ @decorator_a
1820+ @decorator_b
1821+ class MyClass:
1822+ value: int
1823+ <CURSOR>
1824+ "# ,
1825+ )
1826+ . build ( ) ;
1827+
1828+ assert_snapshot ! ( test. folding_ranges( ) , @"
1829+ info[folding-range]: Folding Range
1830+ --> main.py:4:1
1831+ |
1832+ 2 | @decorator_a
1833+ 3 | @decorator_b
1834+ 4 | / class MyClass:
1835+ 5 | | value: int
1836+ | |______________^
1837+ |
1838+ " ) ;
1839+ }
1840+
1841+ #[ test]
1842+ fn test_folding_range_decorated_async_function ( ) {
1843+ let test = CursorTest :: builder ( )
1844+ . source (
1845+ "main.py" ,
1846+ r#"
1847+ @decorator
1848+ async def my_async_function():
1849+ pass
1850+ <CURSOR>
1851+ "# ,
1852+ )
1853+ . build ( ) ;
1854+
1855+ assert_snapshot ! ( test. folding_ranges( ) , @"
1856+ info[folding-range]: Folding Range
1857+ --> main.py:3:1
1858+ |
1859+ 2 | @decorator
1860+ 3 | / async def my_async_function():
1861+ 4 | | pass
1862+ | |________^
1863+ |
1864+ " ) ;
1865+ }
1866+
1867+ #[ test]
1868+ fn test_folding_range_decorated_nested_function ( ) {
1869+ let test = CursorTest :: builder ( )
1870+ . source (
1871+ "main.py" ,
1872+ r#"
1873+ def outer_function():
1874+ @decorator
1875+ def inner_function():
1876+ pass
1877+ <CURSOR>
1878+ "# ,
1879+ )
1880+ . build ( ) ;
1881+
1882+ assert_snapshot ! ( test. folding_ranges( ) , @"
1883+ info[folding-range]: Folding Range
1884+ --> main.py:2:1
1885+ |
1886+ 2 | / def outer_function():
1887+ 3 | | @decorator
1888+ 4 | | def inner_function():
1889+ 5 | | pass
1890+ | |____________^
1891+ |
1892+
1893+ info[folding-range]: Folding Range
1894+ --> main.py:4:5
1895+ |
1896+ 2 | def outer_function():
1897+ 3 | @decorator
1898+ 4 | / def inner_function():
1899+ 5 | | pass
1900+ | |____________^
1901+ |
1902+ " ) ;
1903+ }
1904+
1905+ #[ test]
1906+ fn test_folding_range_decorated_async_method ( ) {
1907+ let test = CursorTest :: builder ( )
1908+ . source (
1909+ "main.py" ,
1910+ r#"
1911+ class MyClass:
1912+ @decorator
1913+ async def my_async_method(self):
1914+ pass
1915+ <CURSOR>
1916+ "# ,
1917+ )
1918+ . build ( ) ;
1919+
1920+ assert_snapshot ! ( test. folding_ranges( ) , @"
1921+ info[folding-range]: Folding Range
1922+ --> main.py:2:1
1923+ |
1924+ 2 | / class MyClass:
1925+ 3 | | @decorator
1926+ 4 | | async def my_async_method(self):
1927+ 5 | | pass
1928+ | |____________^
1929+ |
1930+
1931+ info[folding-range]: Folding Range
1932+ --> main.py:4:5
1933+ |
1934+ 2 | class MyClass:
1935+ 3 | @decorator
1936+ 4 | / async def my_async_method(self):
1937+ 5 | | pass
1938+ | |____________^
1939+ |
1940+ " ) ;
1941+ }
1942+
16861943 struct FoldingRangeDiagnostic {
16871944 file : File ,
16881945 folding_range : FoldingRange ,
0 commit comments