Skip to content
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';
Copy link
Collaborator

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';

import 'package:highlight/highlight.dart';

import '../code/reg_exp.dart';
abstract class Autocompleter {
Mode? mode;
List<String> blacklist = [];
Comment on lines +5 to +6
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

autocompleter.dart declares concrete fields instead of abstract accessors
Problem: Autocompleter currently has

Mode? mode;
List blacklist = []; DefaultAutocompleter implements getters/setters for mode and blacklist (and uses @OverRide). You cannot override a concrete field with getters/setters — this causes analyzer/compile errors.
Fix: make those members abstract (declare getters/setters) in Autocompleter. Example replacement:

Mode? get mode; set mode(Mode? value);

List get blacklist; set blacklist(List value);

(Keep the other abstract methods: setText, getSuggestionItems, replaceText.)

Example:

import 'package:flutter/material.dart'; import 'package:highlight/highlight_core.dart';

abstract class Autocompleter { Autocompleter();

// Language mode used to extract keywords from highlight Mode Mode? get mode; set mode(Mode? value);

// Words to exclude from suggestions List<String> get blacklist; set blacklist(List<String> 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);

/// Returns items ready to be shown in popup based on current editing value Future<List<SuggestionItem>> getSuggestionItems(TextEditingValue value);

/// Replace selection/value using picked suggestion item. /// Returns a new TextEditingValue to assign to controller.value, or null if nothing changed. TextEditingValue? replaceText( TextSelection selection, TextEditingValue value, SuggestionItem item, ); }

class SuggestionItem { final String text; final String displayText; final dynamic data;


/// 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,
});
}
210 changes: 210 additions & 0 deletions lib/src/autocomplete/default_autocompleter.dart
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';
Copy link
Collaborator

Choose a reason for hiding this comment

The 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 {
Copy link
Collaborator

Choose a reason for hiding this comment

The 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,
);
}
}
Loading