|
7 | 7 | from yapf.yapflib import file_resources, style
|
8 | 8 | from yapf.yapflib.yapf_api import FormatCode
|
9 | 9 |
|
| 10 | +import whatthepatch |
| 11 | + |
10 | 12 | from pylsp import hookimpl
|
11 | 13 | from pylsp._utils import get_eol_chars
|
12 | 14 |
|
@@ -36,75 +38,164 @@ def pylsp_format_range(document, range, options=None): # pylint: disable=redefi
|
36 | 38 | return _format(document, lines=lines, options=options)
|
37 | 39 |
|
38 | 40 |
|
39 |
| -def _format(document, lines=None, options=None): |
40 |
| - # Yapf doesn't work with CRLF/CR line endings, so we replace them by '\n' |
41 |
| - # and restore them below. |
42 |
| - replace_eols = False |
43 |
| - source = document.source |
44 |
| - eol_chars = get_eol_chars(source) |
45 |
| - if eol_chars in ['\r', '\r\n']: |
46 |
| - replace_eols = True |
47 |
| - source = source.replace(eol_chars, '\n') |
48 |
| - |
| 41 | +def get_style_config(document_path, options=None): |
49 | 42 | # Get the default styles as a string
|
50 | 43 | # for a preset configuration, i.e. "pep8"
|
51 | 44 | style_config = file_resources.GetDefaultStyleForDir(
|
52 |
| - os.path.dirname(document.path) |
| 45 | + os.path.dirname(document_path) |
53 | 46 | )
|
54 |
| - if options is not None: |
55 |
| - # We have options passed from LSP format request |
56 |
| - # let's pass them to the formatter. |
57 |
| - # First we want to get a dictionary of the preset style |
58 |
| - # to pass instead of a string so that we can modify it |
59 |
| - style_config = style.CreateStyleFromConfig(style_config) |
60 |
| - |
61 |
| - use_tabs = style_config['USE_TABS'] |
62 |
| - indent_width = style_config['INDENT_WIDTH'] |
63 |
| - |
64 |
| - if options.get('tabSize') is not None: |
65 |
| - indent_width = max(int(options.get('tabSize')), 1) |
66 |
| - |
67 |
| - if options.get('insertSpaces') is not None: |
68 |
| - # TODO is it guaranteed to be a boolean, or can it be a string? |
69 |
| - use_tabs = not options.get('insertSpaces') |
70 |
| - |
71 |
| - if use_tabs: |
72 |
| - # Indent width doesn't make sense when using tabs |
73 |
| - # the specifications state: "Size of a tab in spaces" |
74 |
| - indent_width = 1 |
| 47 | + if options is None: |
| 48 | + return style_config |
| 49 | + |
| 50 | + # We have options passed from LSP format request |
| 51 | + # let's pass them to the formatter. |
| 52 | + # First we want to get a dictionary of the preset style |
| 53 | + # to pass instead of a string so that we can modify it |
| 54 | + style_config = style.CreateStyleFromConfig(style_config) |
| 55 | + |
| 56 | + use_tabs = style_config['USE_TABS'] |
| 57 | + indent_width = style_config['INDENT_WIDTH'] |
| 58 | + |
| 59 | + if options.get('tabSize') is not None: |
| 60 | + indent_width = max(int(options.get('tabSize')), 1) |
| 61 | + |
| 62 | + if options.get('insertSpaces') is not None: |
| 63 | + # TODO is it guaranteed to be a boolean, or can it be a string? |
| 64 | + use_tabs = not options.get('insertSpaces') |
| 65 | + |
| 66 | + if use_tabs: |
| 67 | + # Indent width doesn't make sense when using tabs |
| 68 | + # the specifications state: "Size of a tab in spaces" |
| 69 | + indent_width = 1 |
| 70 | + |
| 71 | + style_config['USE_TABS'] = use_tabs |
| 72 | + style_config['INDENT_WIDTH'] = indent_width |
| 73 | + style_config['CONTINUATION_INDENT_WIDTH'] = indent_width |
| 74 | + |
| 75 | + for style_option, value in options.items(): |
| 76 | + # Apply arbitrary options passed as formatter options |
| 77 | + if style_option not in style_config: |
| 78 | + # ignore if it's not a known yapf config |
| 79 | + continue |
| 80 | + |
| 81 | + style_config[style_option] = value |
| 82 | + |
| 83 | + return style_config |
| 84 | + |
| 85 | + |
| 86 | +def diff_to_text_edits(diff, eol_chars): |
| 87 | + # To keep things simple our text edits will be line based. |
| 88 | + # We will also return the edits uncompacted, meaning a |
| 89 | + # line replacement will come in as a line remove followed |
| 90 | + # by a line add instead of a line replace. |
| 91 | + text_edits = [] |
| 92 | + # keep track of line number since additions |
| 93 | + # don't include the line number it's being added |
| 94 | + # to in diffs. lsp is 0-indexed so we'll start with -1 |
| 95 | + prev_line_no = -1 |
| 96 | + |
| 97 | + for change in diff.changes: |
| 98 | + if change.old and change.new: |
| 99 | + # old and new are the same line, no change |
| 100 | + # diffs are 1-indexed |
| 101 | + prev_line_no = change.old - 1 |
| 102 | + elif change.new: |
| 103 | + # addition |
| 104 | + text_edits.append({ |
| 105 | + 'range': { |
| 106 | + 'start': { |
| 107 | + 'line': prev_line_no + 1, |
| 108 | + 'character': 0 |
| 109 | + }, |
| 110 | + 'end': { |
| 111 | + 'line': prev_line_no + 1, |
| 112 | + 'character': 0 |
| 113 | + } |
| 114 | + }, |
| 115 | + 'newText': change.line + eol_chars |
| 116 | + }) |
| 117 | + elif change.old: |
| 118 | + # remove |
| 119 | + lsp_line_no = change.old - 1 |
| 120 | + text_edits.append({ |
| 121 | + 'range': { |
| 122 | + 'start': { |
| 123 | + 'line': lsp_line_no, |
| 124 | + 'character': 0 |
| 125 | + }, |
| 126 | + 'end': { |
| 127 | + # From LSP spec: |
| 128 | + # If you want to specify a range that contains a line |
| 129 | + # including the line ending character(s) then use an |
| 130 | + # end position denoting the start of the next line. |
| 131 | + 'line': lsp_line_no + 1, |
| 132 | + 'character': 0 |
| 133 | + } |
| 134 | + }, |
| 135 | + 'newText': '' |
| 136 | + }) |
| 137 | + prev_line_no = lsp_line_no |
| 138 | + |
| 139 | + return text_edits |
| 140 | + |
| 141 | + |
| 142 | +def ensure_eof_new_line(document, eol_chars, text_edits): |
| 143 | + # diffs don't include EOF newline https://github.com/google/yapf/issues/1008 |
| 144 | + # we'll add it ourselves if our document doesn't already have it and the diff |
| 145 | + # does not change the last line. |
| 146 | + if document.source.endswith(eol_chars): |
| 147 | + return |
| 148 | + |
| 149 | + lines = document.lines |
| 150 | + last_line_number = len(lines) - 1 |
| 151 | + |
| 152 | + if text_edits and text_edits[-1]['range']['start']['line'] >= last_line_number: |
| 153 | + return |
| 154 | + |
| 155 | + text_edits.append({ |
| 156 | + 'range': { |
| 157 | + 'start': { |
| 158 | + 'line': last_line_number, |
| 159 | + 'character': 0 |
| 160 | + }, |
| 161 | + 'end': { |
| 162 | + 'line': last_line_number + 1, |
| 163 | + 'character': 0 |
| 164 | + } |
| 165 | + }, |
| 166 | + 'newText': lines[-1] + eol_chars |
| 167 | + }) |
75 | 168 |
|
76 |
| - style_config['USE_TABS'] = use_tabs |
77 |
| - style_config['INDENT_WIDTH'] = indent_width |
78 |
| - style_config['CONTINUATION_INDENT_WIDTH'] = indent_width |
79 | 169 |
|
80 |
| - for style_option, value in options.items(): |
81 |
| - # Apply arbitrary options passed as formatter options |
82 |
| - if style_option not in style_config: |
83 |
| - # ignore if it's not a known yapf config |
84 |
| - continue |
| 170 | +def _format(document, lines=None, options=None): |
| 171 | + source = document.source |
| 172 | + # Yapf doesn't work with CRLF/CR line endings, so we replace them by '\n' |
| 173 | + # and restore them below when adding new lines |
| 174 | + eol_chars = get_eol_chars(source) |
| 175 | + if eol_chars in ['\r', '\r\n']: |
| 176 | + source = source.replace(eol_chars, '\n') |
| 177 | + else: |
| 178 | + eol_chars = '\n' |
85 | 179 |
|
86 |
| - style_config[style_option] = value |
| 180 | + style_config = get_style_config(document_path=document.path, options=options) |
87 | 181 |
|
88 |
| - new_source, changed = FormatCode( |
| 182 | + diff_txt, changed = FormatCode( |
89 | 183 | source,
|
90 | 184 | lines=lines,
|
91 | 185 | filename=document.filename,
|
| 186 | + print_diff=True, |
92 | 187 | style_config=style_config
|
93 | 188 | )
|
94 | 189 |
|
95 | 190 | if not changed:
|
96 | 191 | return []
|
97 | 192 |
|
98 |
| - if replace_eols: |
99 |
| - new_source = new_source.replace('\n', eol_chars) |
| 193 | + patch_generator = whatthepatch.parse_patch(diff_txt) |
| 194 | + diff = next(patch_generator) |
| 195 | + patch_generator.close() |
100 | 196 |
|
101 |
| - # I'm too lazy at the moment to parse diffs into TextEdit items |
102 |
| - # So let's just return the entire file... |
103 |
| - return [{ |
104 |
| - 'range': { |
105 |
| - 'start': {'line': 0, 'character': 0}, |
106 |
| - # End char 0 of the line after our document |
107 |
| - 'end': {'line': len(document.lines), 'character': 0} |
108 |
| - }, |
109 |
| - 'newText': new_source |
110 |
| - }] |
| 197 | + text_edits = diff_to_text_edits(diff=diff, eol_chars=eol_chars) |
| 198 | + |
| 199 | + ensure_eof_new_line(document=document, eol_chars=eol_chars, text_edits=text_edits) |
| 200 | + |
| 201 | + return text_edits |
0 commit comments