File: C:/Users/fred/anaconda3/Lib/site-packages/pylsp/plugins/yapf_format.py
# Copyright 2017-2020 Palantir Technologies, Inc.
# Copyright 2021- Python Language Server Contributors.
import logging
import os
import whatthepatch
from yapf.yapflib import file_resources, style
from yapf.yapflib.yapf_api import FormatCode
from pylsp import hookimpl
from pylsp._utils import get_eol_chars
log = logging.getLogger(__name__)
@hookimpl
def pylsp_format_document(workspace, document, options):
log.info("Formatting document %s with yapf", document)
with workspace.report_progress("format: yapf"):
return _format(document, options=options)
@hookimpl
def pylsp_format_range(document, range, options):
log.info("Formatting document %s in range %s with yapf", document, range)
# First we 'round' the range up/down to full lines only
range["start"]["character"] = 0
range["end"]["line"] += 1
range["end"]["character"] = 0
# From Yapf docs:
# lines: (list of tuples of integers) A list of tuples of lines, [start, end],
# that we want to format. The lines are 1-based indexed. It can be used by
# third-party code (e.g., IDEs) when reformatting a snippet of code rather
# than a whole file.
# Add 1 for 1-indexing vs LSP's 0-indexing
lines = [(range["start"]["line"] + 1, range["end"]["line"] + 1)]
return _format(document, lines=lines, options=options)
def get_style_config(document_path, options=None):
# Exclude file if it follows the patterns for that
exclude_patterns_from_ignore_file = file_resources.GetExcludePatternsForDir(
os.getcwd()
)
if file_resources.IsIgnored(document_path, exclude_patterns_from_ignore_file):
return []
# 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 None:
return style_config
# 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
return style_config
def diff_to_text_edits(diff, eol_chars):
# To keep things simple our text edits will be line based.
# We will also return the edits uncompacted, meaning a
# line replacement will come in as a line remove followed
# by a line add instead of a line replace.
text_edits = []
# keep track of line number since additions
# don't include the line number it's being added
# to in diffs. lsp is 0-indexed so we'll start with -1
prev_line_no = -1
for change in diff.changes:
if change.old and change.new:
# old and new are the same line, no change
# diffs are 1-indexed
prev_line_no = change.old - 1
elif change.new:
# addition
text_edits.append(
{
"range": {
"start": {"line": prev_line_no + 1, "character": 0},
"end": {"line": prev_line_no + 1, "character": 0},
},
"newText": change.line + eol_chars,
}
)
elif change.old:
# remove
lsp_line_no = change.old - 1
text_edits.append(
{
"range": {
"start": {"line": lsp_line_no, "character": 0},
"end": {
# From LSP spec:
# If you want to specify a range that contains a line
# including the line ending character(s) then use an
# end position denoting the start of the next line.
"line": lsp_line_no + 1,
"character": 0,
},
},
"newText": "",
}
)
prev_line_no = lsp_line_no
return text_edits
def ensure_eof_new_line(document, eol_chars, text_edits):
# diffs don't include EOF newline https://github.com/google/yapf/issues/1008
# we'll add it ourselves if our document doesn't already have it and the diff
# does not change the last line.
if document.source.endswith(eol_chars):
return
lines = document.lines
last_line_number = len(lines) - 1
if text_edits and text_edits[-1]["range"]["start"]["line"] >= last_line_number:
return
text_edits.append(
{
"range": {
"start": {"line": last_line_number, "character": 0},
"end": {"line": last_line_number + 1, "character": 0},
},
"newText": lines[-1] + eol_chars,
}
)
def _format(document, lines=None, options=None):
source = document.source
# Yapf doesn't work with CRLF/CR line endings, so we replace them by '\n'
# and restore them below when adding new lines
eol_chars = get_eol_chars(source)
if eol_chars in ["\r", "\r\n"]:
source = source.replace(eol_chars, "\n")
else:
eol_chars = "\n"
style_config = get_style_config(document_path=document.path, options=options)
diff_txt, changed = FormatCode(
source,
lines=lines,
filename=document.filename,
print_diff=True,
style_config=style_config,
)
if not changed:
return []
patch_generator = whatthepatch.parse_patch(diff_txt)
diff = next(patch_generator)
patch_generator.close()
text_edits = diff_to_text_edits(diff=diff, eol_chars=eol_chars)
ensure_eof_new_line(document=document, eol_chars=eol_chars, text_edits=text_edits)
return text_edits