Skip to content

Commit 8b85881

Browse files
betaboonjhossbach
authored andcommitted
feat: support formatting
1 parent a1b21f2 commit 8b85881

File tree

4 files changed

+109
-19
lines changed

4 files changed

+109
-19
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,5 +44,6 @@ the valid configuration keys:
4444
- `pylsp.plugins.ruff.perFileIgnores`: File-specific error codes to be ignored.
4545
- `pylsp.plugins.ruff.select`: List of error codes to enable.
4646
- `pylsp.plugins.ruff.extendSelect`: Same as select, but append to existing error codes.
47+
- `pylsp.plugins.ruff.format`: List of error codes to fix during formatting.
4748

4849
For more information on the configuration visit [Ruff's homepage](https://beta.ruff.rs/docs/configuration/).

pylsp_ruff/plugin.py

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import sys
55
from pathlib import PurePath
66
from subprocess import PIPE, Popen
7-
from typing import Dict, List
7+
from typing import Dict, Generator, List, Optional
88

99
from lsprotocol.types import (
1010
CodeAction,
@@ -64,6 +64,34 @@ def pylsp_settings():
6464
return converter.unstructure(settings)
6565

6666

67+
@hookimpl(hookwrapper=True)
68+
def pylsp_format_document(workspace: Workspace, document: Document) -> Generator:
69+
"""
70+
Provide formatting through ruff.
71+
72+
Parameters
73+
----------
74+
workspace : pylsp.workspace.Workspace
75+
Current workspace.
76+
document : pylsp.workspace.Document
77+
Document to apply ruff on.
78+
"""
79+
log.debug(f"textDocument/formatting: {document}")
80+
outcome = yield
81+
results = outcome.get_result()
82+
if results:
83+
document.source = results[0]["new_text"]
84+
85+
new_text = run_ruff_format(workspace, document)
86+
range = Range(
87+
start=Position(line=0, character=0),
88+
end=Position(line=len(document.lines), character=0),
89+
)
90+
text_edit = TextEdit(range=range, new_text=new_text)
91+
92+
outcome.force_result(converter.unstructure([text_edit]))
93+
94+
6795
@hookimpl
6896
def pylsp_lint(workspace: Workspace, document: Document) -> List[Dict]:
6997
"""
@@ -315,7 +343,23 @@ def run_ruff_fix(workspace: Workspace, document: Document) -> str:
315343
return result
316344

317345

318-
def run_ruff(workspace: Workspace, document: Document, fix: bool = False) -> str:
346+
def run_ruff_format(workspace: Workspace, document: Document) -> str:
347+
settings = load_settings(workspace, document)
348+
extra_arguments = []
349+
if settings.format:
350+
extra_arguments.append(f"--fixable={','.join(settings.format)}")
351+
else:
352+
extra_arguments.append("--unfixable=ALL")
353+
result = run_ruff(workspace, document, fix=True, extra_arguments=extra_arguments)
354+
return result
355+
356+
357+
def run_ruff(
358+
workspace: Workspace,
359+
document: Document,
360+
fix: bool = False,
361+
extra_arguments: Optional[List[str]] = None,
362+
) -> str:
319363
"""
320364
Run ruff on the given document and the given arguments.
321365
@@ -327,14 +371,16 @@ def run_ruff(workspace: Workspace, document: Document, fix: bool = False) -> str
327371
File to run ruff on.
328372
fix : bool
329373
Whether to run fix or no-fix.
374+
extra_arguments : List[str]
375+
Extra arguments to pass to ruff.
330376
331377
Returns
332378
-------
333379
String containing the result in json format.
334380
"""
335381
settings = load_settings(workspace, document)
336382
executable = settings.executable
337-
arguments = build_arguments(document, settings, fix)
383+
arguments = build_arguments(document, settings, fix, extra_arguments)
338384

339385
log.debug(f"Calling {executable} with args: {arguments} on '{document.path}'")
340386
try:
@@ -358,6 +404,7 @@ def build_arguments(
358404
document: Document,
359405
settings: PluginSettings,
360406
fix: bool = False,
407+
extra_arguments: Optional[List[str]] = None,
361408
) -> List[str]:
362409
"""
363410
Build arguments for ruff.
@@ -368,6 +415,10 @@ def build_arguments(
368415
Document to apply ruff on.
369416
settings : PluginSettings
370417
Settings to use for arguments to pass to ruff.
418+
fix : bool
419+
Whether to execute with --fix.
420+
extra_arguments : List[str]
421+
Extra arguments to pass to ruff.
371422
372423
Returns
373424
-------
@@ -416,6 +467,9 @@ def build_arguments(
416467
continue
417468
args.append(f"--ignore={','.join(errors)}")
418469

470+
if extra_arguments:
471+
args.extend(extra_arguments)
472+
419473
args.extend(["--", "-"])
420474

421475
return args
@@ -456,6 +510,7 @@ def load_settings(workspace: Workspace, document: Document) -> PluginSettings:
456510
executable=plugin_settings.executable,
457511
extend_ignore=plugin_settings.extend_ignore,
458512
extend_select=plugin_settings.extend_select,
513+
format=plugin_settings.format,
459514
)
460515

461516
return plugin_settings

pylsp_ruff/settings.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ class PluginSettings:
2222
extend_ignore: Optional[List[str]] = None
2323
per_file_ignores: Optional[Dict[str, List[str]]] = None
2424

25+
format: Optional[List[str]] = None
26+
2527

2628
def to_camel_case(snake_str: str) -> str:
2729
components = snake_str.split("_")

tests/test_code_actions.py

Lines changed: 48 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
# Copyright 2021- Python Language Server Contributors.
33

44
import tempfile
5+
from textwrap import dedent
56
from typing import List
67
from unittest.mock import Mock
78

@@ -26,16 +27,20 @@ def workspace(tmp_path):
2627
return ws
2728

2829

29-
codeaction_str = r"""
30-
import os
31-
def f():
32-
a = 2
33-
"""
30+
codeaction_str = dedent(
31+
"""
32+
import os
33+
def f():
34+
a = 2
35+
"""
36+
)
3437

35-
import_str = r"""
36-
import pathlib
37-
import os
38-
"""
38+
import_str = dedent(
39+
"""
40+
import pathlib
41+
import os
42+
"""
43+
)
3944

4045
codeactions = [
4146
"Ruff (F401): Remove unused import: `os`",
@@ -103,13 +108,40 @@ def test_import_action(workspace):
103108

104109

105110
def test_fix_all(workspace):
111+
expected_str = dedent(
112+
"""
113+
def f():
114+
pass
115+
"""
116+
)
106117
_, doc = temp_document(codeaction_str, workspace)
118+
fixed_str = ruff_lint.run_ruff_fix(workspace, doc)
119+
assert fixed_str == expected_str
120+
121+
122+
def test_format_document_default_settings(workspace):
123+
_, doc = temp_document(import_str, workspace)
124+
formatted_str = ruff_lint.run_ruff_format(workspace, doc)
125+
assert formatted_str == import_str
107126

108-
fix_all = ruff_lint.run_ruff_fix(workspace, doc)
109-
assert (
110-
fix_all
111-
== r"""
112-
def f():
113-
pass
114-
"""
127+
128+
def test_format_document_settings(workspace):
129+
expected_str = dedent(
130+
"""
131+
import os
132+
import pathlib
133+
"""
134+
)
135+
workspace._config.update(
136+
{
137+
"plugins": {
138+
"ruff": {
139+
"select": ["I"],
140+
"format": ["I001"],
141+
}
142+
}
143+
}
115144
)
145+
_, doc = temp_document(import_str, workspace)
146+
formatted_str = ruff_lint.run_ruff_format(workspace, doc)
147+
assert formatted_str == expected_str

0 commit comments

Comments
 (0)