summaryrefslogtreecommitdiffstats
path: root/src/debputy/lsp/text_edit.py
blob: 770a8375d70cacd1772e66cad09d0944388fda84 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
# Copied and adapted from on python-lsp-server
#
# Copyright 2017-2020 Palantir Technologies, Inc.
# Copyright 2021- Python Language Server Contributors.
# License: Expat (MIT/X11)
#
from typing import List

from lsprotocol.types import Range, TextEdit, Position


def get_well_formatted_range(lsp_range: Range) -> Range:
    start = lsp_range.start
    end = lsp_range.end

    if start.line > end.line or (
        start.line == end.line and start.character > end.character
    ):
        return Range(end, start)

    return lsp_range


def get_well_formatted_edit(text_edit: TextEdit) -> TextEdit:
    lsp_range = get_well_formatted_range(text_edit.range)
    if lsp_range != text_edit.range:
        return TextEdit(new_text=text_edit.new_text, range=lsp_range)

    return text_edit


def compare_text_edits(a: TextEdit, b: TextEdit) -> int:
    diff = a.range.start.line - b.range.start.line
    if diff == 0:
        return a.range.start.character - b.range.start.character

    return diff


def merge_sort_text_edits(text_edits: List[TextEdit]) -> List[TextEdit]:
    if len(text_edits) <= 1:
        return text_edits

    p = len(text_edits) // 2
    left = text_edits[:p]
    right = text_edits[p:]

    merge_sort_text_edits(left)
    merge_sort_text_edits(right)

    left_idx = 0
    right_idx = 0
    i = 0
    while left_idx < len(left) and right_idx < len(right):
        ret = compare_text_edits(left[left_idx], right[right_idx])
        if ret <= 0:
            #  smaller_equal -> take left to preserve order
            text_edits[i] = left[left_idx]
            i += 1
            left_idx += 1
        else:
            # greater -> take right
            text_edits[i] = right[right_idx]
            i += 1
            right_idx += 1
    while left_idx < len(left):
        text_edits[i] = left[left_idx]
        i += 1
        left_idx += 1
    while right_idx < len(right):
        text_edits[i] = right[right_idx]
        i += 1
        right_idx += 1
    return text_edits


class OverLappingTextEditException(Exception):
    """
    Text edits are expected to be sorted
    and compressed instead of overlapping.
    This error is raised when two edits
    are overlapping.
    """


def offset_at_position(lines: List[str], server_position: Position) -> int:
    row, col = server_position.line, server_position.character
    return col + sum(len(line) for line in lines[:row])


def apply_text_edits(text: str, lines: List[str], text_edits: List[TextEdit]) -> str:
    sorted_edits = merge_sort_text_edits(
        [get_well_formatted_edit(e) for e in text_edits]
    )
    last_modified_offset = 0
    spans = []
    for e in sorted_edits:
        start_offset = offset_at_position(lines, e.range.start)
        if start_offset < last_modified_offset:
            raise OverLappingTextEditException("overlapping edit")

        if start_offset > last_modified_offset:
            spans.append(text[last_modified_offset:start_offset])

        if e.new_text != "":
            spans.append(e.new_text)
        last_modified_offset = offset_at_position(lines, e.range.end)

    spans.append(text[last_modified_offset:])
    return "".join(spans)