Skip to content

Commit

Permalink
Preserve selection when folding and unfolding blocks (#95)
Browse files Browse the repository at this point in the history
* Preserve selection when folding and unfolding blocks (#81)

* Minor fixes (#81)

* Fix after review (#81)
  • Loading branch information
alexeyinkin authored Oct 26, 2022
1 parent 176d006 commit ba101db
Show file tree
Hide file tree
Showing 6 changed files with 455 additions and 51 deletions.
12 changes: 10 additions & 2 deletions lib/src/code_field/code_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -505,18 +505,26 @@ class CodeController extends TextEditingController {
}

void foldAt(int line) {
final oldCode = _code;
_code = _code.foldedAt(line);

super.value = TextEditingValue(
text: _code.visibleText,
// TODO(alexeyinkin): Preserve selection, https://github.com/akvelon/flutter-code-editor/issues/81
selection: _code.hiddenRanges.cutSelection(
oldCode.hiddenRanges.recoverSelection(value.selection),
),
);
}

void unfoldAt(int line) {
final oldCode = _code;
_code = _code.unfoldedAt(line);

super.value = TextEditingValue(
text: _code.visibleText,
// TODO(alexeyinkin): Preserve selection, https://github.com/akvelon/flutter-code-editor/issues/81
selection: _code.hiddenRanges.cutSelection(
oldCode.hiddenRanges.recoverSelection(value.selection),
),
);
}

Expand Down
104 changes: 103 additions & 1 deletion lib/src/hidden_ranges/hidden_ranges.dart
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,67 @@ class HiddenRanges {
);
}

/// Translates the [position] in the full text to the position in
/// the visible text.
///
/// If [position] is hidden, returns the last visible position before it.
int cutPosition(int position) {
if (ranges.isEmpty || position < ranges.first.start) {
return position;
}

if (position >= ranges.last.end) {
return position - hiddenCharactersBeforeRanges.last;
}

int lowerRange = 0;
int upperRange = ranges.length - 1;

// Linear interpolation search.
while (upperRange > lowerRange) {
// Full characters at the lower range start and at the upper range end.
final lowerChar = ranges[lowerRange].start;
final upperChar = ranges[upperRange].end;

final rangeIndex = lowerRange +
((position - lowerChar) /
(upperChar - lowerChar) *
(upperRange - lowerRange))
.floor();

if (rangeIndex < lowerRange) {
return position - hiddenCharactersBeforeRanges[lowerRange];
}
if (rangeIndex > upperRange) {
return position - hiddenCharactersBeforeRanges[upperRange + 1];
}

final range = ranges[rangeIndex];

switch (range.compareToPosition(position)) {
case 0:
return range.start - hiddenCharactersBeforeRanges[rangeIndex];
case -1:
lowerRange = rangeIndex + 1;
break;
case 1:
upperRange = rangeIndex - 1;
break;
}
}

// upper == lower - 1, or lower, or lower + 1
final range = ranges[upperRange];
switch (range.compareToPosition(position)) {
case -1:
return position - hiddenCharactersBeforeRanges[upperRange + 1];
case 1:
return position - hiddenCharactersBeforeRanges[upperRange];
}

return range.start - hiddenCharactersBeforeRanges[upperRange];
}

/// Translates the [position] in the visible text to the position in
/// the full text.
///
Expand All @@ -178,8 +239,9 @@ class HiddenRanges {
int lowerRange = 0;
int upperRange = ranges.length - 1;

// Linear interpolation search.
while (upperRange > lowerRange) {
// Visible characters if the range collapse positions.
// Visible characters at the ranges' collapse positions.
final lowerChar =
ranges[lowerRange].end - hiddenCharactersBeforeRanges[lowerRange + 1];
final upperChar =
Expand Down Expand Up @@ -245,6 +307,46 @@ class HiddenRanges {
}
}

TextSelection cutSelection(TextSelection selection) {
if (selection.isCollapsed) {
final position = cutPosition(selection.start);
return selection.copyWith(
baseOffset: position,
extentOffset: position,
);
}

return selection.copyWith(
baseOffset: cutPosition(selection.baseOffset),
extentOffset: cutPosition(selection.extentOffset),
);
}

TextSelection recoverSelection(TextSelection selection) {
if (selection.isCollapsed) {
final position = recoverPosition(
selection.start,
placeHiddenRanges: TextAffinity.downstream,
);

return selection.copyWith(
baseOffset: position,
extentOffset: position,
);
}

return selection.copyWith(
baseOffset: recoverPosition(
selection.baseOffset,
placeHiddenRanges: TextAffinity.downstream,
),
extentOffset: recoverPosition(
selection.extentOffset,
placeHiddenRanges: TextAffinity.downstream,
),
);
}

@override
int get hashCode => Object.hash(
Object.hashAll(ranges),
Expand Down
15 changes: 5 additions & 10 deletions test/src/code_field/code_controller_folding_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -152,8 +152,7 @@ int c;
controller.value,
const TextEditingValue(
text: _codeFolded1,
// TODO(alexeyinkin): Selection.
//selection: TextSelection(baseOffset: 1, extentOffset: 2),
selection: TextSelection(baseOffset: 1, extentOffset: 2),
),
);

Expand All @@ -163,8 +162,7 @@ int c;
controller.value,
TextEditingValue(
text: textBefore,
// TODO(alexeyinkin): Selection.
//selection: TextSelection(baseOffset: 1, extentOffset: 2),
selection: const TextSelection(baseOffset: 1, extentOffset: 2),
),
);
});
Expand Down Expand Up @@ -212,8 +210,7 @@ public class MyClass {
}
}
''',
// TODO(alexeyinkin): Selection.
//selection: TextSelection(baseOffset: 1, extentOffset: 2),
selection: TextSelection(baseOffset: 0, extentOffset: 6),
),
);
});
Expand Down Expand Up @@ -253,8 +250,7 @@ public class MyClass {
}
}
''',
// TODO(alexeyinkin): Selection.
//selection: TextSelection(baseOffset: 1, extentOffset: 2),
selection: TextSelection(baseOffset: 0, extentOffset: 6),
),
);
});
Expand Down Expand Up @@ -302,8 +298,7 @@ int n;
}
}
''',
// TODO(alexeyinkin): Selection.
//selection: TextSelection(baseOffset: 1, extentOffset: 2),
selection: TextSelection.collapsed(offset: 91),
),
);
});
Expand Down
20 changes: 20 additions & 0 deletions test/src/hidden_ranges/common.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import 'package:flutter_code_editor/src/hidden_ranges/hidden_range.dart';
import 'package:flutter_code_editor/src/hidden_ranges/hidden_ranges.dart';

final hiddenRanges = HiddenRanges(
ranges: const [
// How many characters are hidden by the beginning of this range:
HiddenRange(20, 23, firstLine: 0, lastLine: 0, wholeFirstLine: true), // 0
HiddenRange(31, 42, firstLine: 0, lastLine: 0, wholeFirstLine: true), // 3
HiddenRange(67, 91, firstLine: 0, lastLine: 0, wholeFirstLine: true), // 14
HiddenRange(100, 101, firstLine: 0, lastLine: 0, wholeFirstLine: true), //38
HiddenRange(102, 103, firstLine: 0, lastLine: 0, wholeFirstLine: true), //39
HiddenRange(104, 105, firstLine: 0, lastLine: 0, wholeFirstLine: true), //40
HiddenRange(106, 107, firstLine: 0, lastLine: 0, wholeFirstLine: true), //41
HiddenRange(108, 109, firstLine: 0, lastLine: 0, wholeFirstLine: true), //42
HiddenRange(110, 111, firstLine: 0, lastLine: 0, wholeFirstLine: true), //43
HiddenRange(113, 123, firstLine: 0, lastLine: 0, wholeFirstLine: true), //44
// 54
],
textLength: 140,
);
Loading

0 comments on commit ba101db

Please sign in to comment.