Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'dart:math' as math;

import 'package:flutter/material.dart';
import 'package:word_segmenter/word_segmenter.dart';

import '../core/document.dart';
import '../selection/structured_block_selection.dart';
Expand Down Expand Up @@ -1118,8 +1119,8 @@ enum _SelectionCornerQuadrant {
/// A word is a run of characters between whitespace and the separators of
/// [_isWordCharacter], which include CJK punctuation. Text such as Chinese
/// does not put spaces between words, so a run with non-ASCII characters is
/// narrowed to its ICU word boundary: the word of the character after
/// [offset], or before it when [upstream]. ASCII runs keep the separator rule.
/// narrowed to the word of the character after [offset], or before it when
/// [upstream]. ASCII runs keep the separator rule.
({int start, int end}) markdownWordRangeAt(
String text,
int offset, {
Expand All @@ -1138,18 +1139,33 @@ enum _SelectionCornerQuadrant {
}

final run = text.substring(start, end);
final word = _wordInRun(
run,
(upstream ? offset - start - 1 : offset - start).clamp(0, run.length - 1),
);
return (start: start + word.start, end: start + word.end);
}

/// The word containing [index] in [run].
///
/// Flutter's bundled ICU has no CJK dictionary and splits Chinese into single
/// characters, so the platform's segmenter is asked first.
TextRange _wordInRun(String run, int index) {
final segments = wordBoundaries(run);
if (segments != null) {
for (final segment in segments) {
if (index < segment.end) {
return segment;
}
}
}
final painter = TextPainter(
text: TextSpan(text: run),
textDirection: TextDirection.ltr,
)..layout();
final boundary = painter.getWordBoundary(
TextPosition(
offset: (upstream ? offset - start - 1 : offset - start)
.clamp(0, run.length - 1),
),
);
final boundary = painter.getWordBoundary(TextPosition(offset: index));
painter.dispose();
return (start: start + boundary.start, end: start + boundary.end);
return boundary;
}

bool _isWordCharacter(String character) {
Expand Down
1 change: 1 addition & 0 deletions packages/mixin_markdown_widget/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ dependencies:
pretext: ^0.1.0
re_highlight: ^0.0.3
flutter_math_fork: ^0.7.4
word_segmenter: ^0.1.0

dev_dependencies:
flutter_test:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,15 +95,18 @@ void main() {
expect('你好', contains(_wordAt(text, 2)));
});

testWidgets('Chinese text breaks at ICU word boundaries', (tester) async {
testWidgets('Chinese text breaks at dictionary words', (tester) async {
const text = '我们使用Flutter开发桌面应用';
final flutterStart = text.indexOf('Flutter');
for (var offset = flutterStart; offset < flutterStart + 7; offset += 1) {
expect(_wordAt(text, offset), 'Flutter');
for (final word in ['我们', '使用', 'Flutter', '开发', '桌面', '应用']) {
final start = text.indexOf(word);
for (var offset = start; offset < start + word.length; offset += 1) {
expect(_wordAt(text, offset), word, reason: '$offset');
}
// The pointer on the right half of the word's last character.
final end = start + word.length;
final range = markdownWordRangeAt(text, end, upstream: true);
expect(text.substring(range.start, range.end), word);
}
final develop = _wordAt(text, text.indexOf('开'));
expect(develop, startsWith('开'));
expect(develop.length, lessThan(text.length - flutterStart - 7));
});

testWidgets('double click selects a word in a Chinese paragraph',
Expand Down Expand Up @@ -134,11 +137,15 @@ void main() {
containing: '我们使用',
character: '效',
);
final selected = selectionController.selectedPlainText;
expect(selected, startsWith('效'));
expect(selected, isNot(contains(',')));
expect(selected, isNot(contains('。')));
expect(selected.length, lessThan('效果很好'.length + 1));
expect(selectionController.selectedPlainText, '效果');

await _resetTapCount(tester);
await _doubleTapCharacter(
tester,
containing: '我们使用',
character: '桌',
);
expect(selectionController.selectedPlainText, '桌面');
});

testWidgets('double click selects a word in Chinese list text',
Expand Down
Loading