3131 TEXT_DOCUMENT_DID_SAVE ,
3232 TEXT_DOCUMENT_FORMATTING ,
3333 TEXT_DOCUMENT_HOVER ,
34+ TEXT_DOCUMENT_RANGE_FORMATTING ,
3435 AnnotatedTextEdit ,
3536 ClientCapabilities ,
3637 CodeAction ,
5051 DidSaveNotebookDocumentParams ,
5152 DidSaveTextDocumentParams ,
5253 DocumentFormattingParams ,
54+ DocumentRangeFormattingParams ,
55+ DocumentRangeFormattingRegistrationOptions ,
5356 Hover ,
5457 HoverParams ,
5558 InitializeParams ,
6467 NotebookDocumentSyncOptionsNotebookSelectorType2CellsType ,
6568 OptionalVersionedTextDocumentIdentifier ,
6669 Position ,
70+ PositionEncodingKind ,
6771 Range ,
6872 TextDocumentEdit ,
73+ TextDocumentFilter_Type1 ,
6974 TextEdit ,
7075 WorkspaceEdit ,
7176)
7277from packaging .specifiers import SpecifierSet , Version
7378from pygls import server , uris , workspace
79+ from pygls .workspace .position_codec import PositionCodec
7480from typing_extensions import Literal , Self , TypedDict , assert_never
7581
7682from ruff_lsp import __version__ , utils
@@ -141,6 +147,7 @@ class VersionModified(NamedTuple):
141147# Require at least Ruff v0.0.291 for formatting, but allow older versions for linting.
142148VERSION_REQUIREMENT_FORMATTER = SpecifierSet (">=0.0.291,<0.2.0" )
143149VERSION_REQUIREMENT_LINTER = SpecifierSet (">=0.0.189,<0.2.0" )
150+ VERSION_REQUIREMENT_RANGE_FORMATTING = SpecifierSet (">=0.2.0" )
144151# Version requirement for use of the `--output-format` option
145152VERSION_REQUIREMENT_OUTPUT_FORMAT = SpecifierSet (">=0.0.291,<0.2.0" )
146153# Version requirement after which Ruff avoids writing empty output for excluded files.
@@ -1209,15 +1216,21 @@ async def apply_format(arguments: tuple[TextDocument]):
12091216
12101217
12111218@LSP_SERVER .feature (TEXT_DOCUMENT_FORMATTING )
1212- async def format_document (params : DocumentFormattingParams ) -> list [TextEdit ] | None :
1219+ async def format_document (
1220+ params : DocumentFormattingParams , range : Range | None = None
1221+ ) -> list [TextEdit ] | None :
12131222 # For a Jupyter Notebook, this request can only format a single cell as the
12141223 # request itself can only act on a text document. A cell in a Notebook is
12151224 # represented as a text document. The "Notebook: Format notebook" action calls
12161225 # this request for every cell.
12171226 document = Document .from_cell_or_text_uri (params .text_document .uri )
12181227 settings = _get_settings_by_document (document .path )
12191228
1220- result = await _run_format_on_document (document , settings )
1229+ # We don't support range formatting of notebooks yet but VS Code
1230+ # doesn't seem to respect the document filter. For now, format the entire cell.
1231+ range = None if document .kind is DocumentKind .Cell else range
1232+
1233+ result = await _run_format_on_document (document , settings , range )
12211234 if result is None :
12221235 return None
12231236
@@ -1241,6 +1254,28 @@ async def format_document(params: DocumentFormattingParams) -> list[TextEdit] |
12411254 )
12421255
12431256
1257+ @LSP_SERVER .feature (
1258+ TEXT_DOCUMENT_RANGE_FORMATTING ,
1259+ DocumentRangeFormattingRegistrationOptions (
1260+ document_selector = [
1261+ TextDocumentFilter_Type1 (language = "python" , scheme = "file" ),
1262+ TextDocumentFilter_Type1 (language = "python" , scheme = "untitled" ),
1263+ ],
1264+ ranges_support = False ,
1265+ work_done_progress = False ,
1266+ ),
1267+ )
1268+ async def format_document_range (
1269+ params : DocumentRangeFormattingParams ,
1270+ ) -> list [TextEdit ] | None :
1271+ return await format_document (
1272+ DocumentFormattingParams (
1273+ params .text_document , params .options , params .work_done_token
1274+ ),
1275+ params .range ,
1276+ )
1277+
1278+
12441279async def _fix_document_impl (
12451280 document : Document ,
12461281 settings : WorkspaceSettings ,
@@ -1824,14 +1859,19 @@ async def _run_check_on_document(
18241859
18251860
18261861async def _run_format_on_document (
1827- document : Document , settings : WorkspaceSettings
1862+ document : Document , settings : WorkspaceSettings , format_range : Range | None = None
18281863) -> ExecutableResult | None :
18291864 """Runs the Ruff `format` subcommand on the given document source."""
18301865 if settings .get ("ignoreStandardLibrary" , True ) and document .is_stdlib_file ():
18311866 log_warning (f"Skipping standard library file: { document .path } " )
18321867 return None
18331868
1834- executable = _find_ruff_binary (settings , VERSION_REQUIREMENT_FORMATTER )
1869+ version_requirement = (
1870+ VERSION_REQUIREMENT_FORMATTER
1871+ if format_range is None
1872+ else VERSION_REQUIREMENT_RANGE_FORMATTING
1873+ )
1874+ executable = _find_ruff_binary (settings , version_requirement )
18351875 argv : list [str ] = [
18361876 "format" ,
18371877 "--force-exclude" ,
@@ -1840,6 +1880,25 @@ async def _run_format_on_document(
18401880 document .path ,
18411881 ]
18421882
1883+ if format_range :
1884+ # Convert the start and end to character offsets instead of line:column
1885+ lines = document .source .splitlines (True )
1886+ codec = PositionCodec (PositionEncodingKind .Utf16 )
1887+ range = codec .range_from_client_units (lines , format_range )
1888+ start = format_range .start .character
1889+ end = format_range .end .character
1890+
1891+ for index , line in enumerate (lines ):
1892+ if index < range .start .line :
1893+ start += len (line )
1894+
1895+ if index < range .end .line :
1896+ end += len (line )
1897+ else :
1898+ break
1899+
1900+ argv .extend (["--range-start" , str (start ), "--range-end" , str (end )])
1901+
18431902 for arg in settings .get ("format" , {}).get ("args" , []):
18441903 if arg in UNSUPPORTED_FORMAT_ARGS :
18451904 log_to_output (f"Ignoring unsupported argument: { arg } " )
0 commit comments