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

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 

8 

9if TYPE_CHECKING: 

10 import lsprotocol.types as types 

11else: 

12 import debputy.lsprotocol.types as types 

13 

14 

15AnyTextEdit = Union[types.TextEdit, types.AnnotatedTextEdit] 

16 

17 

18def get_well_formatted_range(lsp_range: types.Range) -> types.Range: 

19 start = lsp_range.start 

20 end = lsp_range.end 

21 

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) 

26 

27 return lsp_range 

28 

29 

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) 

34 

35 return text_edit 

36 

37 

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 

42 

43 return diff 

44 

45 

46def merge_sort_text_edits(text_edits: List[AnyTextEdit]) -> List[AnyTextEdit]: 

47 if len(text_edits) <= 1: 

48 return text_edits 

49 

50 p = len(text_edits) // 2 

51 left = text_edits[:p] 

52 right = text_edits[p:] 

53 

54 merge_sort_text_edits(left) 

55 merge_sort_text_edits(right) 

56 

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 

81 

82 

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 """ 

90 

91 

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]) 

95 

96 

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") 

111 

112 if start_offset > last_modified_offset: 

113 spans.append(text[last_modified_offset:start_offset]) 

114 

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) 

118 

119 spans.append(text[last_modified_offset:]) 

120 return "".join(spans)