55import 'dart:async' ;
66import 'dart:convert' ;
77import 'dart:js_interop' ;
8- import 'dart:math' as math;
98
109import 'package:_pub_shared/data/completion.dart' ;
11- import 'package:collection/collection.dart' ;
1210import 'package:http/http.dart' deferred as http show read;
1311import 'package:web/web.dart' ;
1412
1513import '../../web_util.dart' ;
14+ import 'suggest.dart' ;
1615
1716/// Create a [_CompletionWidget] on [element] .
1817///
@@ -92,15 +91,6 @@ void create(HTMLElement element, Map<String, String> options) {
9291 });
9392}
9493
95- typedef _Suggestions = List <
96- ({
97- String value,
98- String html, // TODO: Don't create HTML manually!
99- int start,
100- int end,
101- double score,
102- })>;
103-
10494final class _State {
10595 /// Completion is not active, happens whens:
10696 /// * The input element doesn't have focus, or,
@@ -123,7 +113,7 @@ final class _State {
123113 final int caret;
124114
125115 /// Suggestions on the form: {value, html, start, end}
126- final _Suggestions suggestions;
116+ final Suggestions suggestions;
127117
128118 /// Selected suggestion
129119 final int selectedIndex;
@@ -146,7 +136,7 @@ final class _State {
146136 bool ? triggered,
147137 String ? text,
148138 int ? caret,
149- _Suggestions ? suggestions,
139+ Suggestions ? suggestions,
150140 int ? selectedIndex,
151141 }) =>
152142 _State (
@@ -245,7 +235,7 @@ final class _CompletionWidget {
245235 state.suggestions.isNotEmpty;
246236 }
247237
248- var _renderedSuggestions = _Suggestions .empty ();
238+ var _renderedSuggestions = Suggestions .empty ();
249239
250240 void update () {
251241 if (! displayDropdown) {
@@ -466,130 +456,4 @@ final class _CompletionWidget {
466456 ].join (' ' );
467457 return ctx.measureText (text).width.floor ();
468458 }
469-
470- /// Given [data] and [caret] position inside [text] what suggestions do we
471- /// want to offer and should completion be automatically triggered?
472- static ({bool trigger, _Suggestions suggestions}) suggest (
473- CompletionData data,
474- String text,
475- int caret,
476- ) {
477- // Get position before caret
478- final beforeCaret = caret > 0 ? caret - 1 : 0 ;
479- // Get position of space after the caret
480- final spaceAfterCaret = text.indexOf (' ' , caret);
481-
482- // Start and end of word we are completing
483- final start = text.lastIndexOf (' ' , beforeCaret) + 1 ;
484- final end = spaceAfterCaret != - 1 ? spaceAfterCaret : text.length;
485-
486- // If caret is not at the end, and the next character isn't space then we
487- // do not automatically trigger completion.
488- bool trigger;
489- if (caret < text.length && text[caret] != ' ' ) {
490- trigger = false ;
491- } else {
492- // If the part before the caret is matched, then we can auto trigger
493- final wordBeforeCaret = text.substring (start, caret);
494- trigger = data.completions.any (
495- (c) => ! c.forcedOnly && c.match.any (wordBeforeCaret.startsWith),
496- );
497- }
498-
499- // Get the word that we are completing
500- final word = text.substring (start, end);
501-
502- // Find the longest match for each completion entry
503- final completionWithBestMatch = data.completions.map ((c) => (
504- completion: c,
505- match: maxBy (c.match.where (word.startsWith), (m) => m.length),
506- ));
507- // Find the best completion entry
508- final (: completion, : match) = maxBy (completionWithBestMatch, (c) {
509- final m = c.match;
510- return m != null ? m.length : - 1 ;
511- }) ??
512- (completion: null , match: null );
513- if (completion == null || match == null ) {
514- return (
515- trigger: false ,
516- suggestions: [],
517- );
518- }
519-
520- // prefix to be used for completion of options
521- final prefix = word.substring (match.length);
522-
523- if (completion.options.contains (prefix)) {
524- // If prefix is an option, and there is no other options we don't have
525- // anything to suggest.
526- if (completion.options.length == 1 ) {
527- return (
528- trigger: false ,
529- suggestions: [],
530- );
531- }
532- // We don't to auto trigger completion unless there is an option that is
533- // also a prefix and longer than what prefix currently matches.
534- trigger & = completion.options.any (
535- (opt) => opt.startsWith (prefix) && opt != prefix,
536- );
537- }
538-
539- // Terminate suggestion with a ' ' suffix, if this is a terminal completion
540- final suffix = completion.terminal ? ' ' : '' ;
541-
542- return (
543- trigger: trigger,
544- suggestions: completion.options
545- .map ((option) {
546- final overlap = _lcs (prefix, option);
547- var html = option;
548- if (overlap.isNotEmpty) {
549- html = html.replaceAll (overlap, '<strong>$overlap </strong>' );
550- }
551- return (
552- value: match + option + suffix,
553- start: start,
554- end: end,
555- html: html,
556- score:
557- (option.startsWith (word) ? math.pow (overlap.length, 3 ) : 0 ) +
558- math.pow (overlap.length, 2 ) +
559- (option.startsWith (overlap) ? overlap.length : 0 ) +
560- overlap.length / option.length,
561- );
562- })
563- .sortedBy <num >((s) => s.score)
564- .reversed
565- .toList (),
566- );
567- }
568- }
569-
570- /// The longest common substring
571- String _lcs (String S , String T ) {
572- final r = S .length;
573- final n = T .length;
574- var Lp = List .filled (n, 0 ); // ignore: non_constant_identifier_names
575- var Li = List .filled (n, 0 ); // ignore: non_constant_identifier_names
576- var z = 0 ;
577- var [start, end] = [0 , 0 ];
578- for (var i = 0 ; i < r; i++ ) {
579- for (var j = 0 ; j < n; j++ ) {
580- if (S [i] == T [j]) {
581- if (i == 0 || j == 0 ) {
582- Li [j] = 1 ;
583- } else {
584- Li [j] = Lp [j - 1 ] + 1 ;
585- }
586- if (Li [j] > z) {
587- z = Li [j];
588- [start, end] = [i - z + 1 , i + 1 ];
589- }
590- }
591- }
592- [Lp , Li ] = [Li , Lp ..fillRange (0 , Lp .length, 0 )];
593- }
594- return S .substring (start, end);
595459}
0 commit comments