Skip to content

Commit a9cc116

Browse files
committed
integrate robotframework-tidy for formatting, closes #6
1 parent 74fd904 commit a9cc116

File tree

7 files changed

+157
-56
lines changed

7 files changed

+157
-56
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ All notable changes to the "robotcode" extension will be documented in this file
44

55
## [Unreleased]
66

7+
### added
8+
9+
- integrate robotframework-tidy for formatting
10+
711
## 0.2.1
812

913
### added

package.json

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -206,12 +206,6 @@
206206
"description": "Default setting for where to launch the debug target: internal console, integrated terminal, or external terminal.",
207207
"scope": "resource"
208208
},
209-
"robotcode.robocop.enabled": {
210-
"type": "boolean",
211-
"default": true,
212-
"description": "Enables 'robocop' code analysis, if installed.",
213-
"scope": "resource"
214-
},
215209
"robotcode.syntax.sectionStyle": {
216210
"type": "string",
217211
"default": "*** {name}s ***",
@@ -224,6 +218,12 @@
224218
"description": "Defines if the test report should be opened a run session automatically.",
225219
"scope": "resource"
226220
},
221+
"robotcode.robocop.enabled": {
222+
"type": "boolean",
223+
"default": true,
224+
"markdownDescription": "Enables 'robocop' code analysis, if installed. See [robocop](https://github.com/MarketSquare/robotframework-robocop)",
225+
"scope": "resource"
226+
},
227227
"robotcode.robocop.include": {
228228
"type": "array",
229229
"default": [],
@@ -250,6 +250,12 @@
250250
},
251251
"description": "Configure 'robocop' checker with parameter value.",
252252
"scope": "resource"
253+
},
254+
"robotcode.robotidy.enabled": {
255+
"type": "boolean",
256+
"default": true,
257+
"markdownDescription": "Enables 'robotidy' code formatting, if installed. See [robotidy](https://github.com/MarketSquare/robotframework-tidy)",
258+
"scope": "resource"
253259
}
254260
}
255261
}

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ coloredlogs = "^15.0.1"
2727
robotremoteserver = "^1.1"
2828
Cython = "^0.29.24"
2929
robotframework-robocop = "^1.7.1"
30+
robotframework-tidy = "^1.5.1"
3031

3132

3233
[tool.poetry-dynamic-versioning]

robotcode/language_server/common/parts/formatting.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
DocumentFormattingOptions,
1414
DocumentFormattingParams,
1515
DocumentRangeFormattingOptions,
16+
DocumentRangeFormattingParams,
1617
FormattingOptions,
1718
ProgressToken,
1819
Range,
@@ -84,11 +85,12 @@ async def _text_document_formatting(
8485

8586
return None
8687

87-
@rpc_method(name="textDocument/rangeFormatting", param_type=DocumentFormattingParams)
88+
@rpc_method(name="textDocument/rangeFormatting", param_type=DocumentRangeFormattingParams)
8889
async def _text_document_range_formatting(
8990
self,
9091
params: DocumentFormattingParams,
9192
text_document: TextDocumentIdentifier,
93+
range: Range,
9294
options: FormattingOptions,
9395
work_done_token: Optional[ProgressToken],
9496
*args: Any,

robotcode/language_server/robotframework/configuration.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@ class RoboCopConfig(ConfigBase):
3131
configurations: List[str]
3232

3333

34+
@config_section("robotcode.robotidy")
35+
class RoboTidyConfig(ConfigBase):
36+
enabled: bool
37+
38+
3439
@config_section("robotcode")
3540
class RobotCodeConfig(ConfigBase):
3641
language_server: LanguageServerConfig

robotcode/language_server/robotframework/parts/formatting.py

Lines changed: 83 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,34 +7,111 @@
77
from ....utils.logging import LoggingDescriptor
88
from ...common.language import language_id
99
from ...common.text_document import TextDocument
10-
from ...common.types import FormattingOptions, Position, Range, TextEdit
10+
from ...common.types import FormattingOptions, MessageType, Position, Range, TextEdit
1111

1212
if TYPE_CHECKING:
1313
from ..protocol import RobotLanguageServerProtocol
1414

15+
from ..configuration import RoboTidyConfig
1516
from .model_helper import ModelHelperMixin
1617
from .protocol_part import RobotLanguageServerProtocolPart
1718

1819

20+
def robotidy_installed() -> bool:
21+
try:
22+
__import__("robotidy")
23+
except ImportError:
24+
return False
25+
return True
26+
27+
1928
class RobotFormattingProtocolPart(RobotLanguageServerProtocolPart, ModelHelperMixin):
2029
_logger = LoggingDescriptor()
2130

2231
def __init__(self, parent: RobotLanguageServerProtocol) -> None:
2332
super().__init__(parent)
2433

2534
parent.formatting.format.add(self.format)
26-
parent.formatting.format_range.add(self.format_range)
35+
# TODO implement range formatting
36+
# parent.formatting.format_range.add(self.format_range)
2737

2838
self.space_count = 4
2939
self.use_pipes = False
3040
self.line_separator = os.linesep
3141
self.short_test_name_length = 18
3242
self.setting_and_variable_name_length = 14
3343

44+
async def get_config(self, document: TextDocument) -> Optional[RoboTidyConfig]:
45+
folder = self.parent.workspace.get_workspace_folder(document.uri)
46+
if folder is None:
47+
return None
48+
49+
return await self.parent.workspace.get_configuration(RoboTidyConfig, folder.uri)
50+
3451
@language_id("robotframework")
3552
async def format(
3653
self, sender: Any, document: TextDocument, options: FormattingOptions, **further_options: Any
3754
) -> Optional[List[TextEdit]]:
55+
config = await self.get_config(document)
56+
if config and config.enabled and robotidy_installed():
57+
return await self.format_robot_tidy(document, options, **further_options)
58+
return await self.format_internal(document, options, **further_options)
59+
60+
async def format_robot_tidy(
61+
self, document: TextDocument, options: FormattingOptions, **further_options: Any
62+
) -> Optional[List[TextEdit]]:
63+
64+
from difflib import SequenceMatcher
65+
66+
from robotidy.api import RobotidyAPI
67+
68+
try:
69+
model = await self.parent.documents_cache.get_model(document)
70+
71+
robot_tidy = RobotidyAPI(document.uri.to_path(), None)
72+
73+
changed, _, new = robot_tidy.transform(model)
74+
75+
if not changed:
76+
return None
77+
78+
new_lines = new.text.splitlines()
79+
80+
result: List[TextEdit] = []
81+
matcher = SequenceMatcher(a=document.lines, b=new_lines, autojunk=False)
82+
for code, old_start, old_end, new_start, new_end in matcher.get_opcodes():
83+
if code == "insert" or code == "replace":
84+
result.append(
85+
TextEdit(
86+
range=Range(
87+
start=Position(line=old_start, character=0),
88+
end=Position(line=old_end, character=0),
89+
),
90+
new_text=os.linesep.join(new_lines[new_start:new_end]) + os.linesep,
91+
)
92+
)
93+
94+
elif code == "delete":
95+
result.append(
96+
TextEdit(
97+
range=Range(
98+
start=Position(line=old_start, character=0),
99+
end=Position(line=old_end, character=0),
100+
),
101+
new_text="",
102+
)
103+
)
104+
105+
if result:
106+
return result
107+
108+
except BaseException as e:
109+
self.parent.window.show_message(str(e), MessageType.Error)
110+
return None
111+
112+
async def format_internal(
113+
self, document: TextDocument, options: FormattingOptions, **further_options: Any
114+
) -> Optional[List[TextEdit]]:
38115

39116
from robot.parsing.model.blocks import File
40117
from robot.tidypkg import (
@@ -68,4 +145,8 @@ async def format(
68145
async def format_range(
69146
self, sender: Any, document: TextDocument, range: Range, options: FormattingOptions, **further_options: Any
70147
) -> Optional[List[TextEdit]]:
148+
# TODO implement range formatting
149+
# config = await self.get_config(document)
150+
# if config and config.enabled and robotidy_installed():
151+
# return await self.format_robot_tidy(document, options, range=range, **further_options)
71152
return None

robotcode/language_server/robotframework/parts/robocop_diagnostics.py

Lines changed: 49 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -51,52 +51,54 @@ async def collect_diagnostics(self, sender: Any, document: TextDocument) -> Diag
5151
from robocop.run import Robocop
5252

5353
result: List[Diagnostic] = []
54-
55-
workspace_folder = self.parent.workspace.get_workspace_folder(document.uri)
56-
if workspace_folder is not None:
57-
extension_config = await self.get_config(document)
58-
59-
if extension_config is not None and extension_config.enabled:
60-
61-
with io.StringIO("") as output:
62-
config = Config(str(workspace_folder.uri.to_path()))
63-
64-
config.exec_dir = str(workspace_folder.uri.to_path())
65-
66-
config.output = output
67-
68-
if extension_config.include:
69-
config.include = set(extension_config.include)
70-
if extension_config.exclude:
71-
config.exclude = set(extension_config.exclude)
72-
if extension_config.configurations:
73-
config.configure = set(extension_config.configurations)
74-
75-
analyser = Robocop(from_cli=False, config=config)
76-
analyser.reload_config()
77-
78-
model = await self.parent.documents_cache.get_model(document)
79-
80-
issues = analyser.run_check(model, str(document.uri.to_path()), document.text)
81-
82-
for issue in issues:
83-
d = Diagnostic(
84-
range=Range(
85-
start=Position(line=max(0, issue.line - 1), character=issue.col),
86-
end=Position(line=max(0, issue.end_line - 1), character=issue.end_col),
87-
),
88-
message=issue.desc,
89-
severity=DiagnosticSeverity.INFORMATION
90-
if issue.severity == RuleSeverity.INFO
91-
else DiagnosticSeverity.WARNING
92-
if issue.severity == RuleSeverity.WARNING
93-
else DiagnosticSeverity.ERROR
94-
if issue.severity == RuleSeverity.ERROR
95-
else DiagnosticSeverity.HINT,
96-
source=self.source_name,
97-
code=f"{issue.severity.value}{issue.rule_id}",
98-
)
99-
100-
result.append(d)
54+
try:
55+
workspace_folder = self.parent.workspace.get_workspace_folder(document.uri)
56+
if workspace_folder is not None:
57+
extension_config = await self.get_config(document)
58+
59+
if extension_config is not None and extension_config.enabled:
60+
61+
with io.StringIO("") as output:
62+
config = Config(str(workspace_folder.uri.to_path()))
63+
64+
config.exec_dir = str(workspace_folder.uri.to_path())
65+
66+
config.output = output
67+
68+
if extension_config.include:
69+
config.include = set(extension_config.include)
70+
if extension_config.exclude:
71+
config.exclude = set(extension_config.exclude)
72+
if extension_config.configurations:
73+
config.configure = set(extension_config.configurations)
74+
75+
analyser = Robocop(from_cli=False, config=config)
76+
analyser.reload_config()
77+
78+
model = await self.parent.documents_cache.get_model(document)
79+
80+
issues = analyser.run_check(model, str(document.uri.to_path()), document.text)
81+
82+
for issue in issues:
83+
d = Diagnostic(
84+
range=Range(
85+
start=Position(line=max(0, issue.line - 1), character=issue.col),
86+
end=Position(line=max(0, issue.end_line - 1), character=issue.end_col),
87+
),
88+
message=issue.desc,
89+
severity=DiagnosticSeverity.INFORMATION
90+
if issue.severity == RuleSeverity.INFO
91+
else DiagnosticSeverity.WARNING
92+
if issue.severity == RuleSeverity.WARNING
93+
else DiagnosticSeverity.ERROR
94+
if issue.severity == RuleSeverity.ERROR
95+
else DiagnosticSeverity.HINT,
96+
source=self.source_name,
97+
code=f"{issue.severity.value}{issue.rule_id}",
98+
)
99+
100+
result.append(d)
101+
except BaseException:
102+
pass
101103

102104
return DiagnosticsResult(self.collect_diagnostics, result)

0 commit comments

Comments
 (0)