diff --git a/pylsp/hookspecs.py b/pylsp/hookspecs.py index 736cf931..d1a2458e 100644 --- a/pylsp/hookspecs.py +++ b/pylsp/hookspecs.py @@ -80,12 +80,12 @@ def pylsp_folding_range(config, workspace, document): @hookspec(firstresult=True) -def pylsp_format_document(config, workspace, document): +def pylsp_format_document(config, workspace, document, options): pass @hookspec(firstresult=True) -def pylsp_format_range(config, workspace, document, range): +def pylsp_format_range(config, workspace, document, range, options): pass diff --git a/pylsp/plugins/autopep8_format.py b/pylsp/plugins/autopep8_format.py index 8915fb72..f605f830 100644 --- a/pylsp/plugins/autopep8_format.py +++ b/pylsp/plugins/autopep8_format.py @@ -13,13 +13,13 @@ @hookimpl(tryfirst=True) # Prefer autopep8 over YAPF -def pylsp_format_document(config, document): +def pylsp_format_document(config, document, options=None): # pylint: disable=unused-argument log.info("Formatting document %s with autopep8", document) return _format(config, document) @hookimpl(tryfirst=True) # Prefer autopep8 over YAPF -def pylsp_format_range(config, document, range): # pylint: disable=redefined-builtin +def pylsp_format_range(config, document, range, options=None): # pylint: disable=redefined-builtin,unused-argument log.info("Formatting document %s in range %s with autopep8", document, range) # First we 'round' the range up/down to full lines only diff --git a/pylsp/plugins/yapf_format.py b/pylsp/plugins/yapf_format.py index 7c477816..e4267a7c 100644 --- a/pylsp/plugins/yapf_format.py +++ b/pylsp/plugins/yapf_format.py @@ -4,7 +4,7 @@ import logging import os -from yapf.yapflib import file_resources +from yapf.yapflib import file_resources, style from yapf.yapflib.yapf_api import FormatCode from pylsp import hookimpl @@ -14,12 +14,12 @@ @hookimpl -def pylsp_format_document(document): - return _format(document) +def pylsp_format_document(document, options=None): + return _format(document, options=options) @hookimpl -def pylsp_format_range(document, range): # pylint: disable=redefined-builtin +def pylsp_format_range(document, range, options=None): # pylint: disable=redefined-builtin # First we 'round' the range up/down to full lines only range['start']['character'] = 0 range['end']['line'] += 1 @@ -33,10 +33,10 @@ def pylsp_format_range(document, range): # pylint: disable=redefined-builtin # Add 1 for 1-indexing vs LSP's 0-indexing lines = [(range['start']['line'] + 1, range['end']['line'] + 1)] - return _format(document, lines=lines) + return _format(document, lines=lines, options=options) -def _format(document, lines=None): +def _format(document, lines=None, options=None): # Yapf doesn't work with CRLF/CR line endings, so we replace them by '\n' # and restore them below. replace_eols = False @@ -46,13 +46,50 @@ def _format(document, lines=None): replace_eols = True source = source.replace(eol_chars, '\n') + # Get the default styles as a string + # for a preset configuration, i.e. "pep8" + style_config = file_resources.GetDefaultStyleForDir( + os.path.dirname(document.path) + ) + if options is not None: + # We have options passed from LSP format request + # let's pass them to the formatter. + # First we want to get a dictionary of the preset style + # to pass instead of a string so that we can modify it + style_config = style.CreateStyleFromConfig(style_config) + + use_tabs = style_config['USE_TABS'] + indent_width = style_config['INDENT_WIDTH'] + + if options.get('tabSize') is not None: + indent_width = max(int(options.get('tabSize')), 1) + + if options.get('insertSpaces') is not None: + # TODO is it guaranteed to be a boolean, or can it be a string? + use_tabs = not options.get('insertSpaces') + + if use_tabs: + # Indent width doesn't make sense when using tabs + # the specifications state: "Size of a tab in spaces" + indent_width = 1 + + style_config['USE_TABS'] = use_tabs + style_config['INDENT_WIDTH'] = indent_width + style_config['CONTINUATION_INDENT_WIDTH'] = indent_width + + for style_option, value in options.items(): + # Apply arbitrary options passed as formatter options + if style_option not in style_config: + # ignore if it's not a known yapf config + continue + + style_config[style_option] = value + new_source, changed = FormatCode( source, lines=lines, filename=document.filename, - style_config=file_resources.GetDefaultStyleForDir( - os.path.dirname(document.path) - ) + style_config=style_config ) if not changed: diff --git a/pylsp/python_lsp.py b/pylsp/python_lsp.py index a11f6119..81e93bdc 100644 --- a/pylsp/python_lsp.py +++ b/pylsp/python_lsp.py @@ -277,11 +277,11 @@ def document_symbols(self, doc_uri): def execute_command(self, command, arguments): return self._hook('pylsp_execute_command', command=command, arguments=arguments) - def format_document(self, doc_uri): - return self._hook('pylsp_format_document', doc_uri) + def format_document(self, doc_uri, options): + return self._hook('pylsp_format_document', doc_uri, options=options) - def format_range(self, doc_uri, range): - return self._hook('pylsp_format_range', doc_uri, range=range) + def format_range(self, doc_uri, range, options): + return self._hook('pylsp_format_range', doc_uri, range=range, options=options) def highlight(self, doc_uri, position): return flatten(self._hook('pylsp_document_highlight', doc_uri, position=position)) or None @@ -362,9 +362,8 @@ def m_text_document__hover(self, textDocument=None, position=None, **_kwargs): def m_text_document__document_symbol(self, textDocument=None, **_kwargs): return self.document_symbols(textDocument['uri']) - def m_text_document__formatting(self, textDocument=None, _options=None, **_kwargs): - # For now we're ignoring formatting options. - return self.format_document(textDocument['uri']) + def m_text_document__formatting(self, textDocument=None, options=None, **_kwargs): + return self.format_document(textDocument['uri'], options) def m_text_document__rename(self, textDocument=None, position=None, newName=None, **_kwargs): return self.rename(textDocument['uri'], position, newName) @@ -372,9 +371,8 @@ def m_text_document__rename(self, textDocument=None, position=None, newName=None def m_text_document__folding_range(self, textDocument=None, **_kwargs): return self.folding(textDocument['uri']) - def m_text_document__range_formatting(self, textDocument=None, range=None, _options=None, **_kwargs): - # Again, we'll ignore formatting options for now. - return self.format_range(textDocument['uri'], range) + def m_text_document__range_formatting(self, textDocument=None, range=None, options=None, **_kwargs): + return self.format_range(textDocument['uri'], range, options) def m_text_document__references(self, textDocument=None, position=None, context=None, **_kwargs): exclude_declaration = not context['includeDeclaration'] diff --git a/test/plugins/test_yapf_format.py b/test/plugins/test_yapf_format.py index 94f619e8..cf4d9655 100644 --- a/test/plugins/test_yapf_format.py +++ b/test/plugins/test_yapf_format.py @@ -21,6 +21,9 @@ """ GOOD_DOC = """A = ['hello', 'world']\n""" +FOUR_SPACE_DOC = """def hello(): + pass +""" def test_format(workspace): @@ -68,3 +71,27 @@ def test_line_endings(workspace, newline): res = pylsp_format_document(doc) assert res[0]['newText'] == f'import os{newline}import sys{2 * newline}dict(a=1){newline}' + + +def test_format_with_tab_size_option(workspace): + doc = Document(DOC_URI, workspace, FOUR_SPACE_DOC) + res = pylsp_format_document(doc, {"tabSize": "8"}) + + assert len(res) == 1 + assert res[0]['newText'] == FOUR_SPACE_DOC.replace(" ", " ") + + +def test_format_with_insert_spaces_option(workspace): + doc = Document(DOC_URI, workspace, FOUR_SPACE_DOC) + res = pylsp_format_document(doc, {"insertSpaces": False}) + + assert len(res) == 1 + assert res[0]['newText'] == FOUR_SPACE_DOC.replace(" ", "\t") + + +def test_format_with_yapf_specific_option(workspace): + doc = Document(DOC_URI, workspace, FOUR_SPACE_DOC) + res = pylsp_format_document(doc, {"USE_TABS": True}) + + assert len(res) == 1 + assert res[0]['newText'] == FOUR_SPACE_DOC.replace(" ", "\t")