Files
chen08209 e131675bdf feat(editor): move code_forge into the repository and add snippets
Changelog: Editor word wrap and snippets
2026-10-02 20:32:42 +08:00

299 lines
9.3 KiB
Dart

import 'package:code_forge/code_forge.dart';
import 'package:material_ui/material_ui.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'support.dart';
const _document = 'proxies:\n - name: alpha\n type: vmess\n';
void main() {
setUpAll(initEditorNative);
Future<(CodeForgeController, List<CodeForgeSuggestionDetails>)> pumpWords(
WidgetTester tester, [
String document = _document,
]) async {
final controller = CodeForgeController()..text = document;
final popups = <CodeForgeSuggestionDetails>[];
await pumpEditor(
tester,
controller,
enableLocalSuggestions: true,
suggestionPopupBuilder: (context, details) {
popups.add(details);
return Column(
children: [
for (final suggestion in details.suggestions)
Text('suggest ${suggestion.label}'),
],
);
},
);
await focusEditor(tester);
controller.selection = TextSelection.collapsed(offset: controller.length);
await settle(tester, 2);
return (controller, popups);
}
testWidgets('typing a known prefix opens the popup with document words', (
tester,
) async {
final (controller, popups) = await pumpWords(tester);
await typeText(tester, controller, 'vm');
await settle(tester, 8);
expect(popups, isNotEmpty);
final details = popups.last;
expect(details.suggestions.map((s) => s.label), contains('vmess'));
expect(find.text('suggest vmess'), findsOneWidget);
});
testWidgets('accepting a suggestion completes the word and closes it', (
tester,
) async {
final (controller, popups) = await pumpWords(tester);
await typeText(tester, controller, 'al');
await settle(tester, 8);
final details = popups.last;
final index = details.suggestions.indexWhere((s) => s.label == 'alpha');
expect(index, isNonNegative);
details.onAccept(index);
await settle(tester, 4);
expect(controller.text, '${_document}alpha');
expect(find.textContaining('suggest '), findsNothing);
});
testWidgets('arrow keys move the highlight that Enter accepts', (
tester,
) async {
const document = 'a: [alpha, alpine, also]\n';
final (controller, popups) = await pumpWords(tester, document);
await typeText(tester, controller, 'al');
await settle(tester, 8);
final labels = popups.last.suggestions.map((s) => s.label).toList();
expect(labels, ['also', 'alpha', 'alpine']);
expect(popups.last.selectedIndex, 0);
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
await tester.pump();
expect(popups.last.selectedIndex, 2);
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
await tester.pump();
expect(popups.last.selectedIndex, 1);
await tester.sendKeyEvent(LogicalKeyboardKey.enter);
await settle(tester, 4);
expect(controller.text, '${document}alpha');
expect(find.textContaining('suggest '), findsNothing);
});
testWidgets('a newline from the IME accepts the highlighted suggestion', (
tester,
) async {
final (controller, _) = await pumpWords(tester);
await typeText(tester, controller, 'al');
await settle(tester, 8);
await typeText(tester, controller, '\n');
await settle(tester, 8);
expect(controller.text, '${_document}alpha');
});
testWidgets('a newline after a key opens no popup and breaks the line', (
tester,
) async {
final (controller, _) = await pumpWords(tester);
await typeText(tester, controller, 'key:');
await settle(tester, 8);
expect(controller.suggestions, isNull);
await typeText(tester, controller, '\n');
await settle(tester, 8);
expect(controller.text, startsWith('${_document}key:\n'));
expect(controller.lineCount, _document.split('\n').length + 1);
});
testWidgets('a newline after a word with no matches breaks the line', (
tester,
) async {
final (controller, _) = await pumpWords(tester);
await typeText(tester, controller, 'zzq');
await settle(tester, 8);
expect(controller.suggestions, isNull);
await typeText(tester, controller, '\n');
await settle(tester, 8);
expect(controller.text, '${_document}zzq\n');
});
testWidgets('the popup is given the caret line as the editor scrolls', (
tester,
) async {
final controller = CodeForgeController()
..text = [
for (var i = 0; i < 40; i++) 'filler$i ${'z' * 60}',
'type: vmess',
'name: ',
for (var i = 0; i < 40; i++) 'tail$i',
].join('\n');
const popupKey = Key('popup');
final popups = <CodeForgeSuggestionDetails>[];
await pumpEditor(
tester,
controller,
enableLocalSuggestions: true,
suggestionPopupBuilder: (_, details) {
popups.add(details);
return const SizedBox(key: popupKey);
},
);
await focusEditor(tester);
controller.selection = TextSelection.collapsed(
offset: controller.text.indexOf('name: ') + 'name: '.length,
);
await settle(tester, 2);
await typeText(tester, controller, 'vm');
await settle(tester, 8);
final before = popups.last.caretRect;
expect(before.height, editorLineHeight);
final scrollable = tester.state<TwoDimensionalScrollableState>(
find.byType(TwoDimensionalScrollable),
);
scrollable.horizontalScrollable.position.jumpTo(10);
await tester.pump();
expect(popups.last.caretRect, before.shift(const Offset(-10, 0)));
final vertical = scrollable.verticalScrollable.position;
final start = vertical.pixels;
vertical.jumpTo(start + 10);
await tester.pump();
expect(popups.last.caretRect, before.shift(const Offset(-10, -10)));
vertical.jumpTo(start + 700);
await tester.pump();
expect(find.byKey(popupKey), findsNothing);
vertical.jumpTo(start);
await tester.pump();
expect(find.byKey(popupKey), findsOneWidget);
expect(popups.last.caretRect, before.shift(const Offset(-10, 0)));
});
testWidgets('replacing the text closes the popup', (tester) async {
final (controller, _) = await pumpWords(tester);
await typeText(tester, controller, 'al');
await settle(tester, 8);
expect(controller.suggestions, isNotNull);
controller.text = 'other: 1';
expect(controller.suggestions, isNull);
await settle(tester, 8);
expect(controller.suggestions, isNull);
});
testWidgets('an undo closes the popup for good', (tester) async {
final (controller, _) = await pumpWords(tester);
await typeText(tester, controller, 'al');
await settle(tester, 8);
expect(controller.suggestions, isNotNull);
controller.undoController!.undo();
expect(controller.suggestions, isNull);
await settle(tester, 8);
expect(controller.suggestions, isNull);
});
testWidgets('losing focus closes the popup for good', (tester) async {
final (controller, _) = await pumpWords(tester);
await typeText(tester, controller, 'al');
controller.focusNode!.unfocus();
await settle(tester, 8);
expect(controller.suggestions, isNull);
controller.focusNode!.requestFocus();
await typeText(tester, controller, 'p');
await settle(tester, 8);
expect(controller.suggestions, isNotNull);
controller.focusNode!.unfocus();
await tester.pump();
expect(controller.suggestions, isNull);
});
testWidgets('accepting a suggestion takes in the word still being '
'composed', (tester) async {
final (controller, popups) = await pumpWords(tester);
await typeText(tester, controller, 'al');
await settle(tester, 8);
final value = controller.currentTextEditingValue;
final caret = value.selection.extentOffset;
await sendDeltas(tester, [
{
...textDelta(value.text, 'p', caret, caret),
'composingBase': caret,
'composingExtent': caret + 1,
},
]);
expect(controller.isComposingActive, isTrue);
final details = popups.last;
details.onAccept(details.suggestions.indexWhere((s) => s.label == 'alpha'));
await settle(tester, 4);
expect(controller.isComposingActive, isFalse);
expect(controller.text, '${_document}alpha');
});
testWidgets('a completion source learns which lines changed since', (
tester,
) async {
final requests = <CodeForgeCompletionRequest>[];
final controller = CodeForgeController()
..text = _document
..completionSource = (request) {
requests.add(request);
return null;
};
await pumpEditor(tester, controller);
await focusEditor(tester);
const nameLine = ' - name: alpha';
controller.selection = TextSelection.collapsed(
offset: controller.getLineStartOffset(1) + nameLine.length,
);
await typeText(tester, controller, 'x');
await settle(tester, 8);
final first = requests.last;
controller.replaceRange(0, 0, '# a\n# b\n');
controller.selection = TextSelection.collapsed(
offset: controller.getLineStartOffset(3) + '${nameLine}x'.length,
);
await typeText(tester, controller, 'y');
await settle(tester, 8);
final second = requests.last;
expect(second.lines[3], '${nameLine}xy');
expect(second.linesEditedSince(first.version), (
line: 0,
removed: 1,
inserted: 3,
));
expect(second.linesEditedSince(0), isNull);
});
}