Skip to content

Conversation

@ViscousPot
Copy link

@ViscousPot ViscousPot commented Nov 9, 2025

Description

If trigger is longer than one character, keepTrigger doesn't work as expected. This fixes that :)

Type of Change

  • ✨ New feature (non-breaking change which adds functionality)
  • 🛠️ Bug fix (non-breaking change which fixes an issue)
  • ❌ Breaking change (fix or feature that would cause existing functionality to change)
  • 🧹 Code refactor
  • ✅ Build configuration change
  • 📝 Documentation
  • 🗑️ Chore

Summary by CodeRabbit

  • Bug Fixes

    • Autocomplete now correctly removes multi-character trigger text when accepting a suggestion and places the cursor at the expected position.
  • Tests

    • Added widget tests covering multi-character triggers, ensuring trigger text is removed and cursor position is correct after autocomplete.

@coderabbitai
Copy link

coderabbitai bot commented Nov 9, 2025

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately reflects the main change: adjusting start offset calculation for query selection in the multi-character trigger scenario.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6662bb6 and e30fd06.

📒 Files selected for processing (1)
  • test/multi_trigger_autocomplete_test.dart (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • test/multi_trigger_autocomplete_test.dart

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6f6dc27 and a1358fe.

📒 Files selected for processing (1)
  • lib/src/multi_trigger_autocomplete.dart (1 hunks)

Comment on lines 228 to 229
var start = querySelection.baseOffset;
if (!keepTrigger) start -= 1;
if (!keepTrigger) start -= _currentTrigger.trigger.length;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

LGTM! Correctly fixes multi-character trigger handling.

The change properly accounts for triggers longer than one character. The previous hardcoded start -= 1 only worked for single-character triggers. Using _currentTrigger.trigger.length ensures the entire trigger is included in the replacement range when keepTrigger is false.

This affects both:

  • The text replacement at line 242: text.replaceRange(start, end, option)
  • The cursor position calculation at line 237: selectionOffset = start + option.length

Consider verifying that tests cover multi-character triggers (e.g., "@@", "###") to prevent regression. You can check if test files exist and validate coverage:


🏁 Script executed:

#!/bin/bash
# Find test files and search for multi-character trigger test cases
fd -e dart -t f test | xargs rg -l "acceptAutocompleteOption|keepTrigger" | head -5

# Check for test cases with multi-character triggers
rg -A 10 "keepTrigger.*false|multi.*character.*trigger" --type dart -g '*test*.dart'

Length of output: 41



🏁 Script executed:

#!/bin/bash
# Read the test file to examine test coverage
cat test/multi_trigger_autocomplete_test.dart | head -200

# Search for test groups or test functions related to acceptAutocompleteOption
rg -n "test|group|acceptAutocompleteOption" test/multi_trigger_autocomplete_test.dart | head -30

Length of output: 8267



🏁 Script executed:

#!/bin/bash
# Search for keepTrigger in test file
echo "=== Searching for keepTrigger usage ==="
rg -n "keepTrigger" test/multi_trigger_autocomplete_test.dart

# Get file length and look for multi-character trigger patterns
echo "=== File statistics and multi-character trigger patterns ==="
wc -l test/multi_trigger_autocomplete_test.dart
rg -n "trigger.*[:=].*['\"][@#]{2,}" test/multi_trigger_autocomplete_test.dart

# List all test functions/groups
echo "=== All test functions in file ==="
rg -n "testWidgets|test\(" test/multi_trigger_autocomplete_test.dart | wc -l
rg -n "testWidgets\|^.*test\(" test/multi_trigger_autocomplete_test.dart | head -20

Length of output: 184


Add tests for multi-character triggers with keepTrigger=false.

The fix correctly updates the trigger offset calculation, but verification shows zero test coverage for this scenario. The test file contains 10 test functions, none of which use the keepTrigger parameter or multi-character triggers.

Add tests covering:

  • Multi-character triggers (e.g., "@@", "###") with keepTrigger=false
  • Correct text replacement range when entire multi-character trigger is removed
  • Cursor position after autocomplete selection

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
lib/src/multi_trigger_autocomplete.dart (1)

229-229: Bug fix correctly handles multi-character triggers.

The change properly accounts for triggers longer than one character by using the actual trigger length instead of the hardcoded 1. This fixes the issue where keepTrigger=false would only remove one character of multi-character triggers like "@@" or "###".

Optional: Use the local trigger variable for consistency.

Since trigger is already null-checked at line 223, you can simplify line 229 to use the local variable directly:

-    if (!keepTrigger) start -= _currentTrigger?.trigger.length ?? 1;
+    if (!keepTrigger) start -= trigger.trigger.length;

This eliminates the unnecessary null-safety operator and fallback, making the code more consistent with the pattern established by the null check above.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b58982f and 60d5b16.

📒 Files selected for processing (1)
  • lib/src/multi_trigger_autocomplete.dart (1 hunks)

@xsahil03x
Copy link
Owner

Hey @ViscousPot , can you add some tests for this fix?

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 60d5b16 and 6662bb6.

📒 Files selected for processing (1)
  • test/multi_trigger_autocomplete_test.dart (1 hunks)

Comment on lines +301 to +324
testWidgets('handles multi-character triggers with keepTrigger=false', (tester) async {
final multiCharTrigger = AutocompleteTrigger(
trigger: '##'
optionsViewBuilder: (context, query, controller) { },
);

await tester.pumpWidget(
Boilerplate(
child: MultiTriggerAutocomplete(
autocompleteTriggers: [multiCharTrigger],
),
),
);

await tester.enterText(find.byType(TextFormField), '##');

await tester.tap(find.byType(InkWell).first);
await tester.pump();

final TextFormField field =
find.byType(TextFormField).evaluate().first.widget as TextFormField;

expect(field.controller!.text, isNot(contains('##')));
});
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Critical: Test has syntax errors and will fail at runtime.

This test has multiple issues that will cause it to fail:

  1. Syntax error on line 303: Missing comma after trigger: '##'
  2. Empty optionsViewBuilder on line 304: The function body { } doesn't return a Widget as required. It must return a widget like ListView with tappable items.
  3. Line 317 will fail: Attempting to tap find.byType(InkWell).first but the empty optionsViewBuilder doesn't create any InkWell widgets, causing the finder to fail.
  4. Missing debounce wait: After line 315's enterText, you should add await tester.pumpAndSettle(kDebounceDuration); to allow the options to appear (see line 159 for reference).
  5. No verification of options: Should verify find.byType(ListView) finds a widget before attempting to tap.

Apply this diff to fix the test:

-    testWidgets('handles multi-character triggers with keepTrigger=false', (tester) async {
-      final multiCharTrigger = AutocompleteTrigger(
-        trigger: '##'
-        optionsViewBuilder: (context, query, controller) { },
-      );
-    
-      await tester.pumpWidget(
-        Boilerplate(
-          child: MultiTriggerAutocomplete(
-            autocompleteTriggers: [multiCharTrigger],
-          ),
-        ),
-      );
-    
-      await tester.enterText(find.byType(TextFormField), '##');
-      
-      await tester.tap(find.byType(InkWell).first);
-      await tester.pump();
-    
-      final TextFormField field = 
-        find.byType(TextFormField).evaluate().first.widget as TextFormField;
-      
-      expect(field.controller!.text, isNot(contains('##')));
-    });
+    testWidgets('handles multi-character triggers with keepTrigger=false', (tester) async {
+      final multiCharTrigger = AutocompleteTrigger(
+        trigger: '##',
+        optionsViewBuilder: (context, query, controller) {
+          return ListView.builder(
+            itemCount: 1,
+            itemBuilder: (context, index) {
+              return ListTile(
+                title: const Text('Option'),
+                onTap: () {
+                  final autocomplete = MultiTriggerAutocomplete.of(context);
+                  return autocomplete.acceptAutocompleteOption('Option');
+                },
+              );
+            },
+          );
+        },
+      );
+    
+      await tester.pumpWidget(
+        Boilerplate(
+          child: MultiTriggerAutocomplete(
+            debounceDuration: kDebounceDuration,
+            autocompleteTriggers: [multiCharTrigger],
+          ),
+        ),
+      );
+    
+      await tester.enterText(find.byType(TextFormField), '##');
+      await tester.pumpAndSettle(kDebounceDuration);
+      expect(find.byType(ListView), findsOneWidget);
+      
+      await tester.tap(find.byType(InkWell).first);
+      await tester.pump();
+    
+      final TextFormField field = 
+        find.byType(TextFormField).evaluate().first.widget as TextFormField;
+      
+      expect(field.controller!.text, isNot(contains('##')));
+    });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
testWidgets('handles multi-character triggers with keepTrigger=false', (tester) async {
final multiCharTrigger = AutocompleteTrigger(
trigger: '##'
optionsViewBuilder: (context, query, controller) { },
);
await tester.pumpWidget(
Boilerplate(
child: MultiTriggerAutocomplete(
autocompleteTriggers: [multiCharTrigger],
),
),
);
await tester.enterText(find.byType(TextFormField), '##');
await tester.tap(find.byType(InkWell).first);
await tester.pump();
final TextFormField field =
find.byType(TextFormField).evaluate().first.widget as TextFormField;
expect(field.controller!.text, isNot(contains('##')));
});
testWidgets('handles multi-character triggers with keepTrigger=false', (tester) async {
final multiCharTrigger = AutocompleteTrigger(
trigger: '##',
optionsViewBuilder: (context, query, controller) {
return ListView.builder(
itemCount: 1,
itemBuilder: (context, index) {
return ListTile(
title: const Text('Option'),
onTap: () {
final autocomplete = MultiTriggerAutocomplete.of(context);
return autocomplete.acceptAutocompleteOption('Option');
},
);
},
);
},
);
await tester.pumpWidget(
Boilerplate(
child: MultiTriggerAutocomplete(
debounceDuration: kDebounceDuration,
autocompleteTriggers: [multiCharTrigger],
),
),
);
await tester.enterText(find.byType(TextFormField), '##');
await tester.pumpAndSettle(kDebounceDuration);
expect(find.byType(ListView), findsOneWidget);
await tester.tap(find.byType(InkWell).first);
await tester.pump();
final TextFormField field =
find.byType(TextFormField).evaluate().first.widget as TextFormField;
expect(field.controller!.text, isNot(contains('##')));
});

Comment on lines 326 to 350
testWidgets('cursor position correct after autocomplete with multi-character trigger', (tester) async {
final multiCharTrigger = AutocompleteTrigger(
trigger: '@@',
optionsViewBuilder: (context, query, controller) { },
);

await tester.pumpWidget(
Boilerplate(
child: MultiTriggerAutocomplete(
autocompleteTriggers: [multiCharTrigger],
),
),
);

await tester.enterText(find.byType(TextFormField), '@@sa');

await tester.tap(find.byType(InkWell).first);
await tester.pump();

final TextFormField field =
find.byType(TextFormField).evaluate().first.widget as TextFormField;

expect(field.controller!.selection.baseOffset,
equals(field.controller!.text.length));
});
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Critical: Test has syntax errors and will fail at runtime.

This test has the same issues as the previous test:

  1. Syntax error on line 328: Missing comma after trigger: '@@'
  2. Empty optionsViewBuilder on line 329: Must return a Widget like ListView with tappable items.
  3. Line 342 will fail: Attempting to tap a non-existent InkWell.
  4. Missing debounce wait: After line 340's enterText, add await tester.pumpAndSettle(kDebounceDuration);.
  5. No verification of options: Should verify ListView is shown before tapping.

Apply this diff to fix the test:

-    testWidgets('cursor position correct after autocomplete with multi-character trigger', (tester) async {
-      final multiCharTrigger = AutocompleteTrigger(
-        trigger: '@@',
-        optionsViewBuilder: (context, query, controller) { },
-      );
-    
-      await tester.pumpWidget(
-        Boilerplate(
-          child: MultiTriggerAutocomplete(
-            autocompleteTriggers: [multiCharTrigger],
-          ),
-        ),
-      );
-    
-      await tester.enterText(find.byType(TextFormField), '@@sa');
-      
-      await tester.tap(find.byType(InkWell).first);
-      await tester.pump();
-    
-      final TextFormField field = 
-        find.byType(TextFormField).evaluate().first.widget as TextFormField;
-      
-      expect(field.controller!.selection.baseOffset, 
-             equals(field.controller!.text.length));
-    });
+    testWidgets('cursor position correct after autocomplete with multi-character trigger', (tester) async {
+      final multiCharTrigger = AutocompleteTrigger(
+        trigger: '@@',
+        optionsViewBuilder: (context, query, controller) {
+          return ListView.builder(
+            itemCount: 1,
+            itemBuilder: (context, index) {
+              return ListTile(
+                title: const Text('Option'),
+                onTap: () {
+                  final autocomplete = MultiTriggerAutocomplete.of(context);
+                  return autocomplete.acceptAutocompleteOption('Option');
+                },
+              );
+            },
+          );
+        },
+      );
+    
+      await tester.pumpWidget(
+        Boilerplate(
+          child: MultiTriggerAutocomplete(
+            debounceDuration: kDebounceDuration,
+            autocompleteTriggers: [multiCharTrigger],
+          ),
+        ),
+      );
+    
+      await tester.enterText(find.byType(TextFormField), '@@sa');
+      await tester.pumpAndSettle(kDebounceDuration);
+      expect(find.byType(ListView), findsOneWidget);
+      
+      await tester.tap(find.byType(InkWell).first);
+      await tester.pump();
+    
+      final TextFormField field = 
+        find.byType(TextFormField).evaluate().first.widget as TextFormField;
+      
+      expect(field.controller!.selection.baseOffset, 
+             equals(field.controller!.text.length));
+    });

Refactor autocomplete test to include options view builder.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants