@@ -38,6 +38,18 @@ abstract class TextBoundary {
3838 end: getTrailingTextBoundaryAt (position).offset,
3939 );
4040 }
41+
42+ /// Gets the boundary by calling the left-hand side and pipe the result to
43+ /// right-hand side.
44+ ///
45+ /// Combining two text boundaries can be useful if one wants to ignore certain
46+ /// text before finding the text boundary. For example, use
47+ /// [WhitespaceBoundary] + [WordBoundary] to ignores any white space before
48+ /// finding word boundary if the input position happens to be a whitespace
49+ /// character.
50+ TextBoundary operator + (TextBoundary other) {
51+ return _ExpandedTextBoundary (inner: other, outer: this );
52+ }
4153}
4254
4355/// A text boundary that uses characters as logical boundaries.
@@ -110,7 +122,7 @@ class CharacterBoundary extends TextBoundary {
110122/// This class uses [UAX #29] (https://unicode.org/reports/tr29/) defined word
111123/// boundaries to calculate its logical boundaries.
112124class WordBoundary extends TextBoundary {
113- /// Creates a [CharacterBoundary ] with the text and layout information.
125+ /// Creates a [WordBoundary ] with the text and layout information.
114126 const WordBoundary (this ._textLayout);
115127
116128 final TextLayoutMetrics _textLayout;
@@ -122,21 +134,27 @@ class WordBoundary extends TextBoundary {
122134 affinity: TextAffinity .downstream, // ignore: avoid_redundant_argument_values
123135 );
124136 }
137+
125138 @override
126139 TextPosition getTrailingTextBoundaryAt (TextPosition position) {
127140 return TextPosition (
128141 offset: _textLayout.getWordBoundary (position).end,
129142 affinity: TextAffinity .upstream,
130143 );
131144 }
145+
146+ @override
147+ TextRange getTextBoundaryAt (TextPosition position) {
148+ return _textLayout.getWordBoundary (position);
149+ }
132150}
133151
134152/// A text boundary that uses line breaks as logical boundaries.
135153///
136154/// The input [TextPosition] s will be interpreted as caret locations if
137155/// [TextLayoutMetrics.getLineAtOffset] is text-affinity-aware.
138156class LineBreak extends TextBoundary {
139- /// Creates a [CharacterBoundary ] with the text and layout information.
157+ /// Creates a [LineBreak ] with the text and layout information.
140158 const LineBreak (this ._textLayout);
141159
142160 final TextLayoutMetrics _textLayout;
@@ -155,14 +173,19 @@ class LineBreak extends TextBoundary {
155173 affinity: TextAffinity .upstream,
156174 );
157175 }
176+
177+ @override
178+ TextRange getTextBoundaryAt (TextPosition position) {
179+ return _textLayout.getLineAtOffset (position);
180+ }
158181}
159182
160183/// A text boundary that uses the entire document as logical boundary.
161184///
162185/// The document boundary is unique and is a constant function of the input
163186/// position.
164187class DocumentBoundary extends TextBoundary {
165- /// Creates a [CharacterBoundary ] with the text
188+ /// Creates a [DocumentBoundary ] with the text
166189 const DocumentBoundary (this ._text);
167190
168191 final String _text;
@@ -177,3 +200,149 @@ class DocumentBoundary extends TextBoundary {
177200 );
178201 }
179202}
203+
204+ /// A text boundary that uses the first non-whitespace character as the logical
205+ /// boundary.
206+ ///
207+ /// This text boundary uses [TextLayoutMetrics.isWhitespace] to identify white
208+ /// spaces, this includes newline characters from ASCII and separators from the
209+ /// [unicode separator category] (https://en.wikipedia.org/wiki/Whitespace_character).
210+ class WhitespaceBoundary extends TextBoundary {
211+ /// Creates a [WhitespaceBoundary] with the text.
212+ const WhitespaceBoundary (this ._text);
213+
214+ final String _text;
215+
216+ @override
217+ TextPosition getLeadingTextBoundaryAt (TextPosition position) {
218+ // Handles outside of string end.
219+ if (position.offset > _text.length || (position.offset == _text.length && position.affinity == TextAffinity .downstream)) {
220+ position = TextPosition (offset: _text.length, affinity: TextAffinity .upstream);
221+ }
222+ // Handles outside of string start.
223+ if (position.offset <= 0 ) {
224+ return const TextPosition (offset: 0 );
225+ }
226+ int index = position.offset;
227+ if (position.affinity == TextAffinity .downstream && ! TextLayoutMetrics .isWhitespace (_text.codeUnitAt (index))) {
228+ return position;
229+ }
230+
231+ while ((index -= 1 ) >= 0 ) {
232+ if (! TextLayoutMetrics .isWhitespace (_text.codeUnitAt (index))) {
233+ return TextPosition (offset: index + 1 , affinity: TextAffinity .upstream);
234+ }
235+ }
236+ return const TextPosition (offset: 0 );
237+ }
238+
239+ @override
240+ TextPosition getTrailingTextBoundaryAt (TextPosition position) {
241+ // Handles outside of right bound.
242+ if (position.offset >= _text.length) {
243+ return TextPosition (offset: _text.length, affinity: TextAffinity .upstream);
244+ }
245+ // Handles outside of left bound.
246+ if (position.offset < 0 || (position.offset == 0 && position.affinity == TextAffinity .upstream)) {
247+ position = const TextPosition (offset: 0 );
248+ }
249+
250+ int index = position.offset;
251+ if (position.affinity == TextAffinity .upstream && ! TextLayoutMetrics .isWhitespace (_text.codeUnitAt (index - 1 ))) {
252+ return position;
253+ }
254+
255+ for (; index < _text.length; index += 1 ) {
256+ if (! TextLayoutMetrics .isWhitespace (_text.codeUnitAt (index))) {
257+ return TextPosition (offset: index);
258+ }
259+ }
260+ return TextPosition (offset: _text.length, affinity: TextAffinity .upstream);
261+ }
262+ }
263+
264+ /// Gets the boundary by calling the [outer] and pipe the result to
265+ /// [inner] .
266+ class _ExpandedTextBoundary extends TextBoundary {
267+ /// Creates a [_ExpandedTextBoundary] with inner and outter boundaries
268+ const _ExpandedTextBoundary ({required this .inner, required this .outer});
269+
270+ /// The inner boundary to call with the result from [outer] .
271+ final TextBoundary inner;
272+
273+ /// The outer boundary to call with the input position.
274+ ///
275+ /// The result is piped to the [inner] before returning to the caller.
276+ final TextBoundary outer;
277+
278+ @override
279+ TextPosition getLeadingTextBoundaryAt (TextPosition position) {
280+ return inner.getLeadingTextBoundaryAt (
281+ outer.getLeadingTextBoundaryAt (position),
282+ );
283+ }
284+
285+ @override
286+ TextPosition getTrailingTextBoundaryAt (TextPosition position) {
287+ return inner.getTrailingTextBoundaryAt (
288+ outer.getTrailingTextBoundaryAt (position),
289+ );
290+ }
291+ }
292+
293+ /// A text boundary that will push input text position forward or backward
294+ /// one affinity
295+ ///
296+ /// To push a text position forward one affinity unit, this proxy converts
297+ /// affinity to downstream if it is upstream; otherwise it increase the offset
298+ /// by one with its affinity sets to upstream. For example,
299+ /// `TextPosition(1, upstream)` becomes `TextPosition(1, downstream)` ,
300+ /// `TextPosition(4, downstream)` becomes `TextPosition(5, upstream)` .
301+ ///
302+ /// See also:
303+ /// * [PushTextPosition.forward] , a text boundary to push the input position
304+ /// forward.
305+ /// * [PushTextPosition.backward] , a text boundary to push the input position
306+ /// backward.
307+ class PushTextPosition extends TextBoundary {
308+ const PushTextPosition ._(this ._forward);
309+
310+ /// A text boundary that pushes the input position forward.
311+ static const TextBoundary forward = PushTextPosition ._(true );
312+
313+ /// A text boundary that pushes the input position backward.
314+ static const TextBoundary backward = PushTextPosition ._(false );
315+
316+ /// Whether to push the input position forward or backward.
317+ final bool _forward;
318+
319+ TextPosition _calculateTargetPosition (TextPosition position) {
320+ if (_forward) {
321+ switch (position.affinity) {
322+ case TextAffinity .upstream:
323+ return TextPosition (offset: position.offset);
324+ case TextAffinity .downstream:
325+ return position = TextPosition (
326+ offset: position.offset + 1 ,
327+ affinity: TextAffinity .upstream,
328+ );
329+ }
330+ } else {
331+ switch (position.affinity) {
332+ case TextAffinity .upstream:
333+ return position = TextPosition (offset: position.offset - 1 );
334+ case TextAffinity .downstream:
335+ return TextPosition (
336+ offset: position.offset,
337+ affinity: TextAffinity .upstream,
338+ );
339+ }
340+ }
341+ }
342+
343+ @override
344+ TextPosition getLeadingTextBoundaryAt (TextPosition position) => _calculateTargetPosition (position);
345+
346+ @override
347+ TextPosition getTrailingTextBoundaryAt (TextPosition position) => _calculateTargetPosition (position);
348+ }
0 commit comments