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

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 

9 

10if TYPE_CHECKING: 

11 import lsprotocol.types as types 

12else: 

13 import debputy.lsprotocol.types as types 

14 

15 

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

17 

18 

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

20 start = lsp_range.start 

21 end = lsp_range.end 

22 

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) 

27 

28 return lsp_range 

29 

30 

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) 

35 

36 return text_edit 

37 

38 

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 

43 

44 return diff 

45 

46 

47def merge_sort_text_edits(text_edits: list[AnyTextEdit]) -> list[AnyTextEdit]: 

48 if len(text_edits) <= 1: 

49 return text_edits 

50 

51 p = len(text_edits) // 2 

52 left = text_edits[:p] 

53 right = text_edits[p:] 

54 

55 merge_sort_text_edits(left) 

56 merge_sort_text_edits(right) 

57 

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 

82 

83 

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

91 

92 

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

96 

97 

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

112 

113 if start_offset > last_modified_offset: 

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

115 

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) 

119 

120 spans.append(text[last_modified_offset:]) 

121 return "".join(spans)