-
Notifications
You must be signed in to change notification settings - Fork 91
Autocomplete features #276
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,133 +1,29 @@ | ||
| import 'package:autotrie/autotrie.dart'; | ||
| import 'package:highlight/highlight_core.dart'; | ||
| import 'package:flutter/material.dart'; | ||
| import 'package:highlight/highlight.dart'; | ||
|
|
||
| import '../code/reg_exp.dart'; | ||
| abstract class Autocompleter { | ||
| Mode? mode; | ||
| List<String> blacklist = []; | ||
|
Comment on lines
+5
to
+6
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. autocompleter.dart declares concrete fields instead of abstract accessors Mode? mode; Mode? get mode; set mode(Mode? value); List get blacklist; set blacklist(List value); (Keep the other abstract methods: setText, getSuggestionItems, replaceText.) Example: |
||
|
|
||
| /// Accumulates textual data and suggests autocompletion based on it. | ||
| class Autocompleter { | ||
| Mode? _mode; | ||
| final _customAutocomplete = AutoComplete(engine: SortEngine.entriesOnly()); | ||
| final _keywordsAutocomplete = AutoComplete(engine: SortEngine.entriesOnly()); | ||
| final _textAutocompletes = <Object, AutoComplete>{}; | ||
| final _lastTexts = <Object, String>{}; | ||
| Set<String> _blacklistSet = const {}; | ||
| Autocompleter(); | ||
|
|
||
| static final _whitespacesRe = RegExp(r'\s+'); | ||
| void setText(Object key, String? text); | ||
|
|
||
| /// The language to automatically extract keywords from. | ||
| Mode? get mode => _mode; | ||
| Future<List<SuggestionItem>> getSuggestionItems(TextEditingValue value); | ||
|
|
||
| set mode(Mode? value) { | ||
| _mode = value; | ||
| _parseKeywords(); | ||
| } | ||
|
|
||
| void _parseKeywords() { | ||
| _keywordsAutocomplete.clearEntries(); | ||
|
|
||
| final keywords = mode?.keywords; | ||
| if (keywords == null) { | ||
| return; | ||
| } | ||
|
|
||
| if (keywords is String) { | ||
| _parseStringKeywords(keywords); | ||
| } else if (keywords is Map<String, String>) { | ||
| _parseStringStringKeywords(keywords); | ||
| } else if (keywords is Map<String, dynamic>) { | ||
| _parseStringDynamicKeywords(keywords); | ||
| } else { | ||
| throw Exception( | ||
| 'Unknown keywords type: ${keywords.runtimeType}, $keywords', | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| void _parseStringKeywords(String keywords) { | ||
| _keywordsAutocomplete.enterList( | ||
| [...keywords.split(_whitespacesRe).where((k) => k.isNotEmpty)], | ||
| ); | ||
| } | ||
|
|
||
| void _addKeywords(Iterable<String> keywords) { | ||
| _keywordsAutocomplete.enterList( | ||
| keywords.where((k) => k.isNotEmpty).toList(growable: false), | ||
| ); | ||
| } | ||
|
|
||
| void _parseStringStringKeywords(Map<String, String> map) { | ||
| map.values.forEach(_parseStringKeywords); | ||
| } | ||
|
|
||
| void _parseStringDynamicKeywords(Map<String, dynamic> map) { | ||
| _addKeywords(map.keys); | ||
| } | ||
|
|
||
| /// The words to exclude from suggestions if they are otherwise present. | ||
| List<String> get blacklist => _blacklistSet.toList(growable: false); | ||
|
|
||
| set blacklist(List<String> value) { | ||
| _blacklistSet = {...value}; | ||
| } | ||
|
|
||
| /// Sets the [text] to parse all words from. | ||
| /// Multiple texts are supported, each with its own [key]. | ||
| /// Use this to set current texts from multiple controllers. | ||
| void setText(Object key, String? text) { | ||
| if (text == null) { | ||
| _textAutocompletes.remove(key); | ||
| _lastTexts.remove(key); | ||
| return; | ||
| } | ||
|
|
||
| if (text == _lastTexts[key]) { | ||
| return; | ||
| } | ||
|
|
||
| final ac = _getOrCreateTextAutoComplete(key); | ||
| _updateText(ac, text); | ||
| _lastTexts[key] = text; | ||
| } | ||
|
|
||
| AutoComplete _getOrCreateTextAutoComplete(Object key) { | ||
| return _textAutocompletes[key] ?? _createTextAutoComplete(key); | ||
| } | ||
|
|
||
| AutoComplete _createTextAutoComplete(Object key) { | ||
| final result = AutoComplete(engine: SortEngine.entriesOnly()); | ||
| _textAutocompletes[key] = result; | ||
| return result; | ||
| } | ||
|
|
||
| void _updateText(AutoComplete ac, String text) { | ||
| ac.clearEntries(); | ||
| ac.enterList( | ||
| text | ||
| // https://github.com/akvelon/flutter-code-editor/issues/61 | ||
| //.split(RegExps.wordSplit) | ||
| .split(RegExp(RegExps.wordSplit.pattern)) | ||
| .where((t) => t.isNotEmpty) | ||
| .toList(growable: false), | ||
| ); | ||
| } | ||
|
|
||
| /// Sets additional words to suggest. | ||
| /// Fill this with your library's symbols. | ||
| void setCustomWords(List<String> words) { | ||
| _customAutocomplete.clearEntries(); | ||
| _customAutocomplete.enterList(words); | ||
| } | ||
| TextEditingValue? replaceText( | ||
| TextSelection selection, TextEditingValue value, SuggestionItem item, | ||
| ); | ||
| } | ||
|
|
||
| Future<List<String>> getSuggestions(String prefix) async { | ||
| final result = { | ||
| ..._customAutocomplete.suggest(prefix), | ||
| ..._keywordsAutocomplete.suggest(prefix), | ||
| ..._textAutocompletes.values | ||
| .map((ac) => ac.suggest(prefix)) | ||
| .expand((e) => e), | ||
| }.where((e) => !_blacklistSet.contains(e)).toList(growable: false); | ||
| class SuggestionItem { | ||
| final String text; | ||
| final String displayText; | ||
| final dynamic data; | ||
|
|
||
| result.sort(); | ||
| return result; | ||
| } | ||
| SuggestionItem({ | ||
| required this.text, | ||
| required this.displayText, | ||
| this.data, | ||
| }); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,210 @@ | ||
| import 'package:autotrie/autotrie.dart'; | ||
| import 'package:flutter/material.dart'; | ||
| import 'package:highlight/highlight_core.dart'; | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Autocompleter imports package:highlight/highlight.dart while DefaultAutocompleter imports package:highlight/highlight_core.dart. Choose the correct import that exposes Mode (typically highlight_core.dart) and use it in autocompleter.dart so Mode is known. |
||
|
|
||
| import '../code/reg_exp.dart'; | ||
| import '../code_field/text_editing_value.dart'; | ||
| import '../util/string_util.dart'; | ||
| import 'autocompleter.dart'; | ||
|
|
||
| /// Accumulates textual data and suggests autocompletion based on it. | ||
| class DefaultAutocompleter extends Autocompleter { | ||
| Mode? _mode; | ||
| final _customAutocomplete = AutoComplete(engine: SortEngine.entriesOnly()); | ||
| final _keywordsAutocomplete = AutoComplete(engine: SortEngine.entriesOnly()); | ||
| final _textAutocompletes = <Object, AutoComplete>{}; | ||
| final _lastTexts = <Object, String>{}; | ||
| Set<String> _blacklistSet = const {}; | ||
| String text = ''; | ||
|
|
||
| static final _whitespacesRe = RegExp(r'\s+'); | ||
|
|
||
| DefaultAutocompleter(); | ||
|
|
||
| /// The language to automatically extract keywords from. | ||
| @override | ||
| Mode? get mode => _mode; | ||
| @override | ||
| set mode(Mode? value) { | ||
| _mode = value; | ||
| _parseKeywords(); | ||
| } | ||
|
|
||
| void _parseKeywords() { | ||
| _keywordsAutocomplete.clearEntries(); | ||
|
|
||
| final keywords = mode?.keywords; | ||
| if (keywords == null) { | ||
| return; | ||
| } | ||
|
|
||
| if (keywords is String) { | ||
| _parseStringKeywords(keywords); | ||
| } else if (keywords is Map<String, String>) { | ||
| _parseStringStringKeywords(keywords); | ||
| } else if (keywords is Map<String, dynamic>) { | ||
| _parseStringDynamicKeywords(keywords); | ||
| } else { | ||
| throw Exception( | ||
| 'Unknown keywords type: ${keywords.runtimeType}, $keywords', | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| void _parseStringKeywords(String keywords) { | ||
| _keywordsAutocomplete.enterList( | ||
| [...keywords.split(_whitespacesRe).where((k) => k.isNotEmpty)], | ||
| ); | ||
| } | ||
|
|
||
| void _addKeywords(Iterable<String> keywords) { | ||
| _keywordsAutocomplete.enterList( | ||
| keywords.where((k) => k.isNotEmpty).toList(growable: false), | ||
| ); | ||
| } | ||
|
|
||
| void _parseStringStringKeywords(Map<String, String> map) { | ||
| map.values.forEach(_parseStringKeywords); | ||
| } | ||
|
|
||
| void _parseStringDynamicKeywords(Map<String, dynamic> map) { | ||
| _addKeywords(map.keys); | ||
| } | ||
|
|
||
| /// The words to exclude from suggestions if they are otherwise present. | ||
| @override | ||
| List<String> get blacklist => _blacklistSet.toList(growable: false); | ||
|
|
||
| @override | ||
| set blacklist(List<String> value) { | ||
| _blacklistSet = {...value}; | ||
| } | ||
|
|
||
| /// Sets the [text] to parse all words from. | ||
| /// Multiple texts are supported, each with its own [key]. | ||
| /// Use this to set current texts from multiple controllers. | ||
| @override | ||
| void setText(Object key, String? text) { | ||
| this.text = text ?? ''; | ||
| if (text == null) { | ||
| _textAutocompletes.remove(key); | ||
| _lastTexts.remove(key); | ||
| return; | ||
| } | ||
|
|
||
| if (text == _lastTexts[key]) { | ||
| return; | ||
| } | ||
|
|
||
| final ac = _getOrCreateTextAutoComplete(key); | ||
| _updateText(ac, text); | ||
| _lastTexts[key] = text; | ||
| } | ||
|
|
||
| AutoComplete _getOrCreateTextAutoComplete(Object key) { | ||
| return _textAutocompletes[key] ?? _createTextAutoComplete(key); | ||
| } | ||
|
|
||
| AutoComplete _createTextAutoComplete(Object key) { | ||
| final result = AutoComplete(engine: SortEngine.entriesOnly()); | ||
| _textAutocompletes[key] = result; | ||
| return result; | ||
| } | ||
|
|
||
| void _updateText(AutoComplete ac, String text) { | ||
| ac.clearEntries(); | ||
| ac.enterList( | ||
| text | ||
| // https://github.com/akvelon/flutter-code-editor/issues/61 | ||
| //.split(RegExps.wordSplit) | ||
| .split(RegExp(RegExps.wordSplit.pattern)) | ||
| .where((t) => t.isNotEmpty) | ||
| .toList(growable: false), | ||
| ); | ||
| } | ||
|
|
||
| /// Sets additional words to suggest. | ||
| /// Fill this with your library's symbols. | ||
| void setCustomWords(List<String> words) { | ||
| _customAutocomplete.clearEntries(); | ||
| _customAutocomplete.enterList(words); | ||
| } | ||
|
|
||
| @override | ||
| Future<List<SuggestionItem>> getSuggestionItems(TextEditingValue value) async { | ||
| final prefix = value.wordToCursor; | ||
| if (prefix == null) { | ||
| return []; | ||
| } | ||
|
|
||
| final result = await getSuggestions(prefix); | ||
|
|
||
| return result | ||
| .map((e) => SuggestionItem(text: e, displayText: e)) | ||
| .toList(); | ||
| } | ||
|
|
||
| Future<List<String>> getSuggestions(String prefix) async { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Decide whether getSuggestions(String) should be part of the Autocompleter interface (currently only DefaultAutocompleter exposes it; CodeController uses getSuggestionItems, so this is optional). |
||
| final result = { | ||
| ..._customAutocomplete.suggest(prefix), | ||
| ..._keywordsAutocomplete.suggest(prefix), | ||
| ..._textAutocompletes.values | ||
| .map((ac) => ac.suggest(prefix)) | ||
| .expand((e) => e), | ||
| }.where((e) => !_blacklistSet.contains(e)).toList(growable: false); | ||
|
|
||
| result.sort(); | ||
|
|
||
| return result; | ||
| } | ||
|
|
||
|
|
||
| @override | ||
| TextEditingValue? replaceText( | ||
| TextSelection selection, | ||
| TextEditingValue value, | ||
| SuggestionItem item, | ||
| ) { | ||
| final previousSelection = selection; | ||
| final selectedWord = item.text; | ||
| final startPosition = value.wordAtCursorStart; | ||
| final currentWord = value.wordAtCursor; | ||
|
|
||
| if (startPosition == null || currentWord == null) { | ||
| return null; | ||
| } | ||
|
|
||
| final endReplacingPosition = startPosition + currentWord.length; | ||
| final endSelectionPosition = startPosition + selectedWord.length; | ||
|
|
||
| var additionalSpaceIfEnd = ''; | ||
| var offsetIfEndsWithSpace = 1; | ||
| if (text.length < endReplacingPosition + 1) { | ||
| additionalSpaceIfEnd = ' '; | ||
| } else { | ||
| final charAfterText = text[endReplacingPosition]; | ||
| if (charAfterText != ' ' && | ||
| !StringUtil.isDigit(charAfterText) && | ||
| !StringUtil.isLetterEng(charAfterText)) { | ||
| // ex. case ';' or other finalizer, or symbol | ||
| offsetIfEndsWithSpace = 0; | ||
| } | ||
| } | ||
|
|
||
| final replacedText = text.replaceRange( | ||
| startPosition, | ||
| endReplacingPosition, | ||
| '$selectedWord$additionalSpaceIfEnd', | ||
| ); | ||
|
|
||
| final adjustedSelection = previousSelection.copyWith( | ||
| baseOffset: endSelectionPosition + offsetIfEndsWithSpace, | ||
| extentOffset: endSelectionPosition + offsetIfEndsWithSpace, | ||
| ); | ||
|
|
||
| return TextEditingValue( | ||
| text: replacedText, | ||
| selection: adjustedSelection, | ||
| ); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since autocompleter.dart is a non-UI, library-level file (it only needs the types, not Material widgets), prefer the smaller import for TextEditingValue and TextSelection to avoid pulling Material into a low-level lib file: import 'package:flutter/services.dart';