Coverage for src/debputy/lsp/text_edit.py: 84%
67 statements
« prev ^ index » next coverage.py v7.8.2, created at 2025-09-07 09:27 +0000
« prev ^ index » next coverage.py v7.8.2, created at 2025-09-07 09:27 +0000
1# Copied and adapted from on python-lsp-server
2#
3# Copyright 2017-2020 Palantir Technologies, Inc.
4# Copyright 2021- Python Language Server Contributors.
5# License: Expat (MIT/X11)
6#
7from typing import List, Sequence, TYPE_CHECKING, Union
9if TYPE_CHECKING:
10 import lsprotocol.types as types
11else:
12 import debputy.lsprotocol.types as types
15AnyTextEdit = Union[types.TextEdit, types.AnnotatedTextEdit]
18def get_well_formatted_range(lsp_range: types.Range) -> types.Range:
19 start = lsp_range.start
20 end = lsp_range.end
22 if start.line > end.line or ( 22 ↛ 25line 22 didn't jump to line 25 because the condition on line 22 was never true
23 start.line == end.line and start.character > end.character
24 ):
25 return types.Range(end, start)
27 return lsp_range
30def get_well_formatted_edit(text_edit: Union[AnyTextEdit, AnyTextEdit]) -> AnyTextEdit:
31 lsp_range = get_well_formatted_range(text_edit.range)
32 if lsp_range != text_edit.range: 32 ↛ 33line 32 didn't jump to line 33 because the condition on line 32 was never true
33 return types.TextEdit(new_text=text_edit.new_text, range=lsp_range)
35 return text_edit
38def compare_text_edits(a: AnyTextEdit, b: AnyTextEdit) -> int:
39 diff = a.range.start.line - b.range.start.line
40 if diff == 0:
41 return a.range.start.character - b.range.start.character
43 return diff
46def merge_sort_text_edits(text_edits: List[AnyTextEdit]) -> List[AnyTextEdit]:
47 if len(text_edits) <= 1:
48 return text_edits
50 p = len(text_edits) // 2
51 left = text_edits[:p]
52 right = text_edits[p:]
54 merge_sort_text_edits(left)
55 merge_sort_text_edits(right)
57 left_idx = 0
58 right_idx = 0
59 i = 0
60 while left_idx < len(left) and right_idx < len(right):
61 ret = compare_text_edits(left[left_idx], right[right_idx])
62 if ret <= 0: 62 ↛ 69line 62 didn't jump to line 69 because the condition on line 62 was always true
63 # smaller_equal -> take left to preserve order
64 text_edits[i] = left[left_idx]
65 i += 1
66 left_idx += 1
67 else:
68 # greater -> take right
69 text_edits[i] = right[right_idx]
70 i += 1
71 right_idx += 1
72 while left_idx < len(left): 72 ↛ 73line 72 didn't jump to line 73 because the condition on line 72 was never true
73 text_edits[i] = left[left_idx]
74 i += 1
75 left_idx += 1
76 while right_idx < len(right):
77 text_edits[i] = right[right_idx]
78 i += 1
79 right_idx += 1
80 return text_edits
83class OverLappingTextEditException(Exception):
84 """
85 Text edits are expected to be sorted
86 and compressed instead of overlapping.
87 This error is raised when two edits
88 are overlapping.
89 """
92def offset_at_position(lines: List[str], server_position: types.Position) -> int:
93 row, col = server_position.line, server_position.character
94 return col + sum(len(line) for line in lines[:row])
97def apply_text_edits(
98 text: str,
99 lines: List[str],
100 text_edits: Sequence[AnyTextEdit],
101) -> str:
102 sorted_edits = merge_sort_text_edits(
103 [get_well_formatted_edit(e) for e in text_edits]
104 )
105 last_modified_offset = 0
106 spans = []
107 for e in sorted_edits:
108 start_offset = offset_at_position(lines, e.range.start)
109 if start_offset < last_modified_offset: 109 ↛ 110line 109 didn't jump to line 110 because the condition on line 109 was never true
110 raise OverLappingTextEditException("overlapping edit")
112 if start_offset > last_modified_offset:
113 spans.append(text[last_modified_offset:start_offset])
115 if e.new_text != "": 115 ↛ 117line 115 didn't jump to line 117 because the condition on line 115 was always true
116 spans.append(e.new_text)
117 last_modified_offset = offset_at_position(lines, e.range.end)
119 spans.append(text[last_modified_offset:])
120 return "".join(spans)