Skip to content
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

Autocomplete features #276

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
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
1 change: 1 addition & 0 deletions lib/flutter_code_editor.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export 'src/analyzer/default_analyzer.dart';
export 'src/analyzer/models/analysis_result.dart';
export 'src/analyzer/models/issue.dart';
export 'src/analyzer/models/issue_type.dart';
export 'src/autocomplete/autocompleter.dart';

export 'src/code/code.dart';
export 'src/code/code_line.dart';
Expand Down
146 changes: 21 additions & 125 deletions lib/src/autocomplete/autocompleter.dart
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 = [];

/// 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,
});
}
189 changes: 189 additions & 0 deletions lib/src/autocomplete/default_autocompleter.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
import 'package:autotrie/autotrie.dart';
import 'package:flutter/material.dart';
import 'package:highlight/highlight_core.dart';

import '../code/reg_exp.dart';
import '../code_field/text_editing_value.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 {
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 startPosition = value.wordAtCursorStart;

if (startPosition != null) {
final replacedText = text.replaceRange(
startPosition,
selection.baseOffset,
item.text,
);

final adjustedSelection = previousSelection.copyWith(
baseOffset: startPosition + item.text.length,
extentOffset: startPosition + item.text.length,
);

return TextEditingValue(
text: replacedText,
selection: adjustedSelection,
);
}
return null;
}
}
Loading