Skip to content

Commit 149888a

Browse files
Issue248 backspace (akvelon#249)
* wip * Change from String.getChangedRangeAroundSelection to TextEditingValue.getChangedRange (akvelon#248) * Clean up (akvelon#248) --------- Co-authored-by: Darkhan Nausharipov <nausharipov@gmail.com>
1 parent f52ca62 commit 149888a

7 files changed

+456
-86
lines changed

lib/src/code/code.dart

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
55
import 'package:highlight/highlight_core.dart';
66

77
import '../../src/highlight/result.dart';
8+
import '../code_field/text_editing_value.dart';
89
import '../folding/foldable_block.dart';
910
import '../folding/foldable_block_matcher.dart';
1011
import '../folding/invalid_foldable_block.dart';
@@ -325,7 +326,7 @@ class Code {
325326
TextSelection oldSelection,
326327
TextEditingValue visibleAfter,
327328
) {
328-
final visibleRangeAfter = visibleAfter.text.getChangedRangeAroundSelection(
329+
final visibleRangeAfter = visibleAfter.getChangedRange(
329330
TextEditingValue(text: visibleText, selection: oldSelection),
330331
) ??
331332
visibleAfter.text.getChangedRange(
@@ -385,7 +386,7 @@ class Code {
385386
foldedBlocks.any(
386387
(block) =>
387388
block.lastLine >= firstChangedLine &&
388-
block.lastLine <= lastChangedLine,
389+
block.lastLine < lastChangedLine,
389390
)) {
390391
return CodeEditResult(
391392
fullTextAfter: text,

lib/src/code/string.dart

Lines changed: 2 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -5,41 +5,6 @@ import 'package:flutter/widgets.dart';
55
import '../code_field/text_editing_value.dart';
66

77
extension StringExtension on String {
8-
/// Returns a [TextRange] round selection in [oldValue]
9-
/// that is different from this.
10-
///
11-
/// This is useful if the ordinary diff with [getChangedRange]
12-
/// may be ambiguous. This method tests for the most common changes
13-
/// and snaps the changed range to the old selection.
14-
///
15-
/// Handles these special cases:
16-
/// - Selected text is replaced / deleted / inserted, outside is unchanged.
17-
/// - Insertion at a collapsed selection.
18-
///
19-
/// Returns null if neither of the above cases apply.
20-
TextRange? getChangedRangeAroundSelection(TextEditingValue oldValue) {
21-
final oldBefore = oldValue.beforeSelection;
22-
final oldAfter = oldValue.afterSelection;
23-
24-
if (length < oldBefore.length + oldAfter.length) {
25-
// The outside of the selection has shortened.
26-
// definitely not an above special case.
27-
return null;
28-
}
29-
30-
final newBefore = substring(0, oldBefore.length);
31-
final newAfter = substring(length - oldAfter.length);
32-
33-
if (oldBefore == newBefore && oldAfter == newAfter) {
34-
return TextRange(
35-
start: newBefore.length,
36-
end: length - newAfter.length,
37-
);
38-
}
39-
40-
return null;
41-
}
42-
438
/// Returns the widest [TextRange] of this that is different from [old].
449
///
4510
/// `start` refers to the common prefix. It is the first character of this
@@ -55,7 +20,8 @@ extension StringExtension on String {
5520
/// - Inserting a duplicate: aBc -> aBBc
5621
/// - Deletion of a duplicate: aBBc -> aBc
5722
///
58-
/// This method should be used if [getChangedRangeAroundSelection] failed.
23+
/// This method should be used
24+
/// if [TextEditingValueExtension.getChangedRange] failed.
5925
TextRange getChangedRange(
6026
String old, {
6127
required TextAffinity attributeChangeTo,

lib/src/code_field/text_editing_value.dart

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import 'package:flutter/widgets.dart';
33
import '../code/reg_exp.dart';
44
import '../code/string.dart';
55
import '../code/text_range.dart';
6+
import '../code_field/code_controller.dart';
7+
import '../util/edit_type.dart';
68
import 'text_selection.dart';
79

810
extension TextEditingValueExtension on TextEditingValue {
@@ -185,4 +187,73 @@ extension TextEditingValueExtension on TextEditingValue {
185187
text: text,
186188
);
187189
}
190+
191+
/// Returns the widest [TextRange] of this that is different from [oldValue]
192+
/// if it can be produced by any of common edits allowed for user input.
193+
/// These are all edits that go through [CodeController.value] setter
194+
/// and do not include undo/redo.
195+
///
196+
/// Returns null if the change cannot be produced by such user edits.
197+
TextRange? getChangedRange(TextEditingValue oldValue) {
198+
switch (getEditType(oldValue)) {
199+
case EditType.backspaceBeforeCollapsedSelection:
200+
return TextRange.collapsed(
201+
text.length - oldValue.afterSelection.length,
202+
);
203+
204+
case EditType.deleteSelection:
205+
case EditType.deleteAfterCollapsedSelection:
206+
return TextRange.collapsed(oldValue.beforeSelection.length);
207+
208+
case EditType.replaceSelection:
209+
case EditType.insertAtCollapsedSelection:
210+
return TextRange(
211+
start: oldValue.beforeSelection.length,
212+
end: text.length - oldValue.afterSelection.length,
213+
);
214+
215+
case EditType.unchanged:
216+
case EditType.other:
217+
return null;
218+
}
219+
}
220+
221+
EditType getEditType(TextEditingValue oldValue) {
222+
if (oldValue.text == text) {
223+
return EditType.unchanged;
224+
}
225+
226+
final oldBefore = oldValue.beforeSelection;
227+
final oldAfter = oldValue.afterSelection;
228+
final oldUnselectedLength = oldBefore.length + oldAfter.length;
229+
230+
if (text.length < oldUnselectedLength) {
231+
if (text.startsWith(oldBefore) && selection == oldValue.selection) {
232+
return EditType.deleteAfterCollapsedSelection;
233+
}
234+
235+
if (text.endsWith(oldAfter) &&
236+
selection.isCollapsed &&
237+
selection.start ==
238+
text.length - oldValue.text.length + oldValue.selection.start) {
239+
return EditType.backspaceBeforeCollapsedSelection;
240+
}
241+
242+
return EditType.other;
243+
}
244+
245+
if (text.startsWith(oldBefore) && text.endsWith(oldAfter)) {
246+
if (oldValue.selection.isCollapsed) {
247+
return EditType.insertAtCollapsedSelection;
248+
}
249+
250+
if (text.length == oldUnselectedLength) {
251+
return EditType.deleteSelection;
252+
}
253+
254+
return EditType.replaceSelection;
255+
}
256+
257+
return EditType.other;
258+
}
188259
}

lib/src/util/edit_type.dart

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
enum EditType {
2+
backspaceBeforeCollapsedSelection,
3+
deleteAfterCollapsedSelection,
4+
deleteSelection,
5+
insertAtCollapsedSelection,
6+
replaceSelection,
7+
unchanged,
8+
9+
/// A change beyond a user's ability to interact with the editor
10+
/// like replacing an unselected text in one action.
11+
other,
12+
}

test/src/code/code_get_edit_result_test.dart

Lines changed: 65 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ void main() {
4242
'Empty -> Something',
4343
fullTextBefore: '',
4444
visibleValueAfter: TextEditingValue(text: _visibleText1),
45+
visibleSelectionBefore: TextSelection.collapsed(offset: 0),
4546
expected: CodeEditResult(
4647
fullTextAfter: _visibleText1,
4748
linesChanged: TextRange(start: 0, end: 0),
@@ -52,6 +53,7 @@ void main() {
5253
'Something -> Empty',
5354
fullTextBefore: _fullText1,
5455
visibleValueAfter: TextEditingValue.empty,
56+
visibleSelectionBefore: TextSelection.collapsed(offset: 0), // any
5557
expected: CodeEditResult(
5658
fullTextAfter: '',
5759
linesChanged: TextRange(start: 0, end: 8), // Empty line 9 is intact.
@@ -61,6 +63,7 @@ void main() {
6163
_Example(
6264
'Change not touching hidden range borders',
6365
fullTextBefore: _fullText1,
66+
visibleSelectionBefore: TextSelection.collapsed(offset: 64),
6467
visibleValueAfter: TextEditingValue(
6568
// Each blank line has two spaces here:
6669
text: '''
@@ -69,11 +72,12 @@ public class MyClass {
6972
}
7073
7174
72-
method(int a) {{
73-
}}
75+
voidmethod(int a) {
76+
}
7477
7578
}
7679
''',
80+
selection: TextSelection.collapsed(offset: 63),
7781
),
7882
expected: CodeEditResult(
7983
fullTextAfter: '''
@@ -82,18 +86,19 @@ public class MyClass {
8286
}
8387
// [END section1]
8488
// [START section2]
85-
method(int a) {{
86-
}}
89+
voidmethod(int a) {
90+
}
8791
// [END section2]
8892
}
8993
''',
90-
linesChanged: TextRange(start: 5, end: 6),
94+
linesChanged: TextRange(start: 5, end: 5),
9195
),
9296
),
9397

9498
_Example(
9599
'Insertion at a range collapse - Inserts before the range',
96100
fullTextBefore: _fullText1,
101+
visibleSelectionBefore: TextSelection.collapsed(offset: 81),
97102
visibleValueAfter: TextEditingValue(
98103
// Each blank line has two spaces here:
99104
text: '''
@@ -107,6 +112,7 @@ public class MyClass {
107112
;
108113
}
109114
''',
115+
selection: TextSelection.collapsed(offset: 82),
110116
),
111117
expected: CodeEditResult(
112118
fullTextAfter: '''
@@ -125,18 +131,21 @@ public class MyClass {
125131
),
126132

127133
_Example(
128-
'Removing a block that is both before and after a hidden range - '
134+
'Backspace on a block that is both before and after a hidden range - '
129135
'Removes it before',
136+
// block == '\n'
130137
fullTextBefore: '''
131138
{
132139
//[START section1]
133140
}
134141
''',
142+
visibleSelectionBefore: TextSelection.collapsed(offset: 2),
135143
visibleValueAfter: TextEditingValue(
136144
text: '''
137145
{
138146
}
139147
''',
148+
selection: TextSelection.collapsed(offset: 1),
140149
),
141150
expected: CodeEditResult(
142151
fullTextAfter: '''
@@ -148,13 +157,40 @@ public class MyClass {
148157
),
149158

150159
_Example(
151-
'Replacing between ranges keeps the ranges - '
160+
'Delete on a block that is both before and after a hidden range - '
161+
'Removes it before',
162+
// block == '\n'
163+
fullTextBefore: '''
164+
{
165+
//[START section1]
166+
}
167+
''',
168+
visibleSelectionBefore: TextSelection.collapsed(offset: 1),
169+
visibleValueAfter: TextEditingValue(
170+
text: '''
171+
{
172+
}
173+
''',
174+
selection: TextSelection.collapsed(offset: 1),
175+
),
176+
expected: CodeEditResult(
177+
fullTextAfter: '''
178+
{//[START section1]
179+
}
180+
''',
181+
linesChanged: TextRange(start: 0, end: 1),
182+
),
183+
),
184+
185+
_Example(
186+
'Replacing between ranges - '
152187
'Keeps the range after, Deletes the range before',
153188
fullTextBefore: '''
154189
{//[START section1]
155190
;//[END section1]
156191
}
157192
''',
193+
visibleSelectionBefore: TextSelection(baseOffset: 1, extentOffset: 3),
158194
visibleValueAfter: TextEditingValue(
159195
text: '''
160196
{()
@@ -173,8 +209,10 @@ public class MyClass {
173209
_Example(
174210
'If all text is a single hidden range, insert before it',
175211
fullTextBefore: '//[START section1]',
212+
visibleSelectionBefore: TextSelection.collapsed(offset: 0),
176213
visibleValueAfter: TextEditingValue(
177214
text: ';',
215+
selection: TextSelection.collapsed(offset: 1),
178216
),
179217
expected: CodeEditResult(
180218
fullTextAfter: ';//[START section1]',
@@ -196,38 +234,39 @@ public class MyClass {
196234
namedSectionParser: const BracketsStartEndNamedSectionParser(),
197235
);
198236

199-
final selections = [
200-
for (int n = code.visibleText.length; --n >= -1;)
201-
TextSelection.collapsed(offset: n),
202-
];
203-
204-
for (final selection in selections) {
205-
expect(
206-
() => code.getEditResult(selection, example.visibleValueAfter),
207-
returnsNormally,
208-
reason: example.name,
209-
);
210-
211-
final result = code.getEditResult(selection, example.visibleValueAfter);
212-
expect(
213-
result,
214-
example.expected,
215-
reason: example.name,
216-
);
217-
}
237+
expect(
238+
() => code.getEditResult(
239+
example.visibleSelectionBefore,
240+
example.visibleValueAfter,
241+
),
242+
returnsNormally,
243+
reason: example.name,
244+
);
245+
246+
final result = code.getEditResult(
247+
example.visibleSelectionBefore,
248+
example.visibleValueAfter,
249+
);
250+
expect(
251+
result,
252+
example.expected,
253+
reason: example.name,
254+
);
218255
}
219256
});
220257
}
221258

222259
class _Example {
223260
final String name;
224261
final String fullTextBefore;
262+
final TextSelection visibleSelectionBefore;
225263
final TextEditingValue visibleValueAfter;
226264
final CodeEditResult? expected;
227265

228266
const _Example(
229267
this.name, {
230268
required this.fullTextBefore,
269+
required this.visibleSelectionBefore,
231270
required this.visibleValueAfter,
232271
this.expected,
233272
});

0 commit comments

Comments
 (0)