|
2 | 2 | # Distributed under the terms of the Modified BSD License. |
3 | 3 |
|
4 | 4 | from collections.abc import Callable |
| 5 | +from difflib import SequenceMatcher |
5 | 6 | from functools import partial |
6 | 7 | from typing import Any |
7 | 8 |
|
@@ -64,17 +65,40 @@ def set(self, value: str) -> None: |
64 | 65 | :param value: The content of the document. |
65 | 66 | :type value: str |
66 | 67 | """ |
67 | | - if self.get() == value: |
| 68 | + old_value = self.get() |
| 69 | + if old_value == value: |
68 | 70 | # no-op if the values are already the same, |
69 | 71 | # to avoid side-effects such as cursor jumping to the top |
70 | 72 | return |
71 | 73 |
|
72 | 74 | with self._ydoc.transaction(): |
73 | | - # clear document |
74 | | - self._ysource.clear() |
75 | | - # initialize document |
76 | | - if value: |
77 | | - self._ysource += value |
| 75 | + matcher = SequenceMatcher(a=old_value, b=value) |
| 76 | + |
| 77 | + # for very different strings, just replace the whole content; |
| 78 | + # this avoids generating a huge number of operations |
| 79 | + if matcher.ratio() < 0.6: |
| 80 | + # clear document |
| 81 | + self._ysource.clear() |
| 82 | + # initialize document |
| 83 | + if value: |
| 84 | + self._ysource += value |
| 85 | + else: |
| 86 | + operations = matcher.get_opcodes() |
| 87 | + offset = 0 |
| 88 | + for tag, i1, i2, j1, j2 in operations: |
| 89 | + if tag == "replace": |
| 90 | + self._ysource[i1 + offset : i2 + offset] = value[j1:j2] |
| 91 | + offset += (j2 - j1) - (i2 - i1) |
| 92 | + elif tag == "delete": |
| 93 | + del self._ysource[i1 + offset : i2 + offset] |
| 94 | + offset -= i2 - i1 |
| 95 | + elif tag == "insert": |
| 96 | + self._ysource[i1 + offset : i2 + offset] = value[j1:j2] |
| 97 | + offset += j2 - j1 |
| 98 | + elif tag == "equal": |
| 99 | + pass |
| 100 | + else: |
| 101 | + raise ValueError(f"Unknown tag '{tag}' in sequence matcher") |
78 | 102 |
|
79 | 103 | def observe(self, callback: Callable[[str, Any], None]) -> None: |
80 | 104 | """ |
|
0 commit comments