Coverage for src/debputy/lsp/text_edit.py: 84%
68 statements
« prev ^ index » next coverage.py v7.8.2, created at 2025-10-12 15:06 +0000
« prev ^ index » next coverage.py v7.8.2, created at 2025-10-12 15:06 +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, TYPE_CHECKING, Union
8from collections.abc import Sequence
10if TYPE_CHECKING:
11 import lsprotocol.types as types
12else:
13 import debputy.lsprotocol.types as types
16AnyTextEdit = Union[types.TextEdit, types.AnnotatedTextEdit]
19def get_well_formatted_range(lsp_range: types.Range) -> types.Range:
20 start = lsp_range.start
21 end = lsp_range.end
23 if start.line > end.line or ( 23 ↛ 26line 23 didn't jump to line 26 because the condition on line 23 was never true
24 start.line == end.line and start.character > end.character
25 ):
26 return types.Range(end, start)
28 return lsp_range
31def get_well_formatted_edit(text_edit: AnyTextEdit | AnyTextEdit) -> AnyTextEdit:
32 lsp_range = get_well_formatted_range(text_edit.range)
33 if lsp_range != text_edit.range: 33 ↛ 34line 33 didn't jump to line 34 because the condition on line 33 was never true
34 return types.TextEdit(new_text=text_edit.new_text, range=lsp_range)
36 return text_edit
39def compare_text_edits(a: AnyTextEdit, b: AnyTextEdit) -> int:
40 diff = a.range.start.line - b.range.start.line
41 if diff == 0:
42 return a.range.start.character - b.range.start.character
44 return diff
47def merge_sort_text_edits(text_edits: list[AnyTextEdit]) -> list[AnyTextEdit]:
48 if len(text_edits) <= 1:
49 return text_edits
51 p = len(text_edits) // 2
52 left = text_edits[:p]
53 right = text_edits[p:]
55 merge_sort_text_edits(left)
56 merge_sort_text_edits(right)
58 left_idx = 0
59 right_idx = 0
60 i = 0
61 while left_idx < len(left) and right_idx < len(right):
62 ret = compare_text_edits(left[left_idx], right[right_idx])
63 if ret <= 0: 63 ↛ 70line 63 didn't jump to line 70 because the condition on line 63 was always true
64 # smaller_equal -> take left to preserve order
65 text_edits[i] = left[left_idx]
66 i += 1
67 left_idx += 1
68 else:
69 # greater -> take right
70 text_edits[i] = right[right_idx]
71 i += 1
72 right_idx += 1
73 while left_idx < len(left): 73 ↛ 74line 73 didn't jump to line 74 because the condition on line 73 was never true
74 text_edits[i] = left[left_idx]
75 i += 1
76 left_idx += 1
77 while right_idx < len(right):
78 text_edits[i] = right[right_idx]
79 i += 1
80 right_idx += 1
81 return text_edits
84class OverLappingTextEditException(Exception):
85 """
86 Text edits are expected to be sorted
87 and compressed instead of overlapping.
88 This error is raised when two edits
89 are overlapping.
90 """
93def offset_at_position(lines: list[str], server_position: types.Position) -> int:
94 row, col = server_position.line, server_position.character
95 return col + sum(len(line) for line in lines[:row])
98def apply_text_edits(
99 text: str,
100 lines: list[str],
101 text_edits: Sequence[AnyTextEdit],
102) -> str:
103 sorted_edits = merge_sort_text_edits(
104 [get_well_formatted_edit(e) for e in text_edits]
105 )
106 last_modified_offset = 0
107 spans = []
108 for e in sorted_edits:
109 start_offset = offset_at_position(lines, e.range.start)
110 if start_offset < last_modified_offset: 110 ↛ 111line 110 didn't jump to line 111 because the condition on line 110 was never true
111 raise OverLappingTextEditException("overlapping edit")
113 if start_offset > last_modified_offset:
114 spans.append(text[last_modified_offset:start_offset])
116 if e.new_text != "": 116 ↛ 118line 116 didn't jump to line 118 because the condition on line 116 was always true
117 spans.append(e.new_text)
118 last_modified_offset = offset_at_position(lines, e.range.end)
120 spans.append(text[last_modified_offset:])
121 return "".join(spans)