From 79207d94227e72ae84f081a31c536429356d9a33 Mon Sep 17 00:00:00 2001
From: Vishesh Handa <me@vhanda.in>
Date: Thu, 28 Jan 2021 14:54:59 +0100
Subject: [PATCH] AutoCompletion: Add tests for tags autocompletion

Also move the file. I tried using the same code base for both the types
of auto-completion and this has resulted in a huge mess. It's best to
keep the logic completely seperate for the two.
---
 .../widget.dart}                              | 166 ++++++++++++++----
 lib/main_autocomplete.dart                    |   6 +-
 test/autocompletion/tags_test.dart            |  22 +++
 .../test.dart}                                |   8 +-
 4 files changed, 158 insertions(+), 44 deletions(-)
 rename lib/{widgets/autocompleter.dart => autocompletion/widget.dart} (58%)
 create mode 100644 test/autocompletion/tags_test.dart
 rename test/{autocompleter_test.dart => autocompletion/test.dart} (95%)

diff --git a/lib/widgets/autocompleter.dart b/lib/autocompletion/widget.dart
similarity index 58%
rename from lib/widgets/autocompleter.dart
rename to lib/autocompletion/widget.dart
index 449fa597..3359bf41 100644
--- a/lib/widgets/autocompleter.dart
+++ b/lib/autocompletion/widget.dart
@@ -4,33 +4,29 @@ import 'package:flutter/material.dart';
 
 import 'package:time/time.dart';
 
-class AutoCompleter extends StatefulWidget {
+class AutoCompletionWidget extends StatefulWidget {
   final FocusNode textFieldFocusNode;
   final GlobalKey textFieldKey;
   final TextStyle textFieldStyle;
   final TextEditingController textController;
   final Widget child;
 
-  final String startToken;
-  final String endToken;
-
-  AutoCompleter({
+  AutoCompletionWidget({
     @required this.textFieldFocusNode,
     @required this.textFieldKey,
     @required this.textFieldStyle,
     @required this.textController,
     @required this.child,
-    @required this.startToken,
-    @required this.endToken,
   });
 
   @override
-  _AutoCompleterState createState() => _AutoCompleterState();
+  _AutoCompletionWidgetState createState() => _AutoCompletionWidgetState();
 }
 
-class _AutoCompleterState extends State<AutoCompleter> {
+class _AutoCompletionWidgetState extends State<AutoCompletionWidget> {
   OverlayEntry overlayEntry;
-  String prevText;
+
+  var autoCompleter = TagsAutoCompleter();
 
   @override
   void initState() {
@@ -52,20 +48,25 @@ class _AutoCompleterState extends State<AutoCompleter> {
 
   void _textChanged() {
     var selection = widget.textController.selection;
-    var cursorPos = selection.baseOffset;
     var text = widget.textController.text;
 
-    var start = text.lastIndexOf(RegExp(r' |^'), cursorPos - 1) + 1;
-    var word = text.substring(start, cursorPos);
-    print('text: $word');
-    if (word.startsWith(widget.startToken)) {
-      _showOverlayTag(context, text.substring(0, cursorPos));
-    } else if (word.endsWith(widget.endToken)) {
-      // Hide when ]] is added
-      _hideOverlay();
+    var prefix = "";
+    try {
+      prefix =
+          autoCompleter.textChanged(EditorState(text, selection.baseOffset));
+    } catch (e) {
+      print(e);
     }
 
-    prevText = text;
+    if (prefix.isEmpty) {
+      _hideOverlay();
+      return;
+    }
+
+    if (prefix == "\n") {
+    } else {
+      _showOverlayTag(context, prefix);
+    }
   }
 
   /// newText is used to calculate where to put the completion box
@@ -134,19 +135,28 @@ class _AutoCompleterState extends State<AutoCompleter> {
   }
 }
 
+/*
 /// if endToken is empty, then the token can only be alpha numeric
 String extractToken(
     String text, int cursorPos, String startToken, String endToken) {
-  var start = text.lastIndexOf(RegExp(r' |^'), cursorPos - 1);
+  var start = text.lastIndexOf(RegExp(r' '), cursorPos - 1);
   if (start == -1) {
-    var word = text.substring(0, cursorPos);
-    if (word.startsWith('[[')) {
-      return word.substring(2, cursorPos);
-    }
-    return "";
+    start = 0;
+  }
+  print("start: $start");
+
+  var end = text.indexOf(RegExp(r' |$'), cursorPos);
+  if (end == -1) {
+    end = cursorPos;
+  }
+  print("end: $end");
+
+  var word = text.substring(start, end);
+  if (word.startsWith('[[')) {
+    return word.substring(2, cursorPos);
   }
 
-  return text;
+  return "";
 }
 
 bool enterPressed(String oldText, String newText, int cursorPos) {
@@ -160,20 +170,13 @@ bool enterPressed(String oldText, String newText, int cursorPos) {
   }
   return false;
 }
+*/
 
-class CompletionResult {
+class EditorState {
   String text;
   int cursorPos;
 
-  CompletionResult(this.text, this.cursorPos);
-}
-
-CompletionResult completeText(String oldText, String newText, int cursorPos) {
-  return null;
-}
-
-bool hideAutoCompleter(String oldText, String newText, int cursorPos) {
-  return false;
+  EditorState(this.text, this.cursorPos);
 }
 
 // https://levelup.gitconnected.com/flutter-medium-like-text-editor-b41157f50f0e
@@ -189,3 +192,92 @@ bool hideAutoCompleter(String oldText, String newText, int cursorPos) {
 //        or an existing wiki link which has the closing brackets
 // Bug  : Show auto-completion on top if no space at the bottom
 // Bug  : Handle physical tab or Enter key
+
+abstract class AutoCompletionLogic {
+  /// Return an empty string if the overlay should be hidden
+  /// Return \n if enter has been pressed
+  /// Return the prefix if an overlay should be shown
+  String textChanged(EditorState es);
+
+  EditorState completeText(String text);
+}
+
+/*
+class WikiLinksAutoCompleter implements AutoCompletionLogic {
+  var _oldState = EditorState("", 0);
+
+  bool inBracket1 = false;
+  bool inBracket2 = false;
+  bool outBracket2 = false;
+
+  var newText = "";
+
+  @override
+  void textChanged(EditorState es) {
+    var oldState = _oldState;
+    _oldState = es;
+
+    // This could result in an Add / Remove / Replace
+
+    if (es.text.length > oldState.text.length) {
+      // Probably an add
+      if (oldState.cursorPos < es.cursorPos) {
+        newText = es.text.substring(oldState.cursorPos, es.cursorPos);
+        return;
+      }
+      return;
+    }
+  }
+
+  @override
+  EditorState completeText(String text) {
+    return null;
+  }
+
+  @override
+  bool get enterPressed => false;
+
+  @override
+  bool get showOverlay => false;
+}
+*/
+
+class TagsAutoCompleter implements AutoCompletionLogic {
+  var _oldState = EditorState("", 0);
+
+  @override
+  String textChanged(EditorState es) {
+    _oldState = es;
+
+    //print("${es.text} ${es.cursorPos}");
+    var start = es.text.lastIndexOf(RegExp(r'^|[ #.?!]'), es.cursorPos);
+    if (start < 0) {
+      start = 0;
+    }
+
+    var end = es.text.indexOf(RegExp(r' |$'), es.cursorPos);
+    if (end == -1) {
+      end = es.cursorPos;
+    }
+
+    // print("start end: $start $end");
+    var text = es.text.substring(start, end).trim();
+    // print("text $text");
+    if (!text.startsWith('#')) {
+      return "";
+    }
+
+    return text.substring(1);
+  }
+
+  @override
+  EditorState completeText(String text) {
+    var start = _oldState.text.lastIndexOf(r'#', _oldState.cursorPos);
+    if (start == -1) {
+      throw Exception("completeText should not have been called");
+    }
+
+    var es = _oldState;
+    return es;
+  }
+}
diff --git a/lib/main_autocomplete.dart b/lib/main_autocomplete.dart
index 7468b6cf..7ee861aa 100644
--- a/lib/main_autocomplete.dart
+++ b/lib/main_autocomplete.dart
@@ -2,7 +2,7 @@ import 'dart:ui';
 
 import 'package:flutter/material.dart';
 
-import 'package:gitjournal/widgets/autocompleter.dart';
+import 'package:gitjournal/autocompletion/widget.dart';
 
 void main() => runApp(MyApp());
 
@@ -52,13 +52,11 @@ class _MyHomePageState extends State<MyHomePage> {
       maxLines: 300,
     );
 
-    textField = AutoCompleter(
+    textField = AutoCompletionWidget(
       textFieldStyle: _textFieldStyle,
       textFieldKey: _textFieldKey,
       textFieldFocusNode: _focusNode,
       textController: _textController,
-      startToken: '[[',
-      endToken: ']]',
       child: textField,
     );
     return Scaffold(
diff --git a/test/autocompletion/tags_test.dart b/test/autocompletion/tags_test.dart
new file mode 100644
index 00000000..94f597dc
--- /dev/null
+++ b/test/autocompletion/tags_test.dart
@@ -0,0 +1,22 @@
+import 'package:test/test.dart';
+
+import 'package:gitjournal/autocompletion/widget.dart';
+
+void main() {
+  var c = TagsAutoCompleter();
+
+  test('Extract second word', () {
+    var p = c.textChanged(EditorState("Hi #Hel", 7));
+    expect(p, "Hel");
+  });
+
+  test('Extract second word - cursor not at end', () {
+    var p = c.textChanged(EditorState("Hi #Hell", 7));
+    expect(p, "Hell");
+  });
+
+  test("Second word with dot", () {
+    var p = c.textChanged(EditorState("Hi.#Hel", 6));
+    expect(p, "Hel");
+  });
+}
diff --git a/test/autocompleter_test.dart b/test/autocompletion/test.dart
similarity index 95%
rename from test/autocompleter_test.dart
rename to test/autocompletion/test.dart
index 4abab153..848b75c2 100644
--- a/test/autocompleter_test.dart
+++ b/test/autocompletion/test.dart
@@ -1,6 +1,6 @@
-import 'package:test/test.dart';
+/*
 
-import 'package:gitjournal/widgets/autocompleter.dart';
+import 'package:test/test.dart';
 
 void main() {
   test('Extract word at start', () {
@@ -11,7 +11,7 @@ void main() {
   test('Extract word at start - cursor not at end', () {
     var result = extractToken("[[Hel", 4, '[[', ']]');
     expect(result, "Hel");
-  });
+  }, solo: true);
 
   test('Extract second word', () {
     var result = extractToken("Hi [[Hel", 8, '[[', ']]');
@@ -43,3 +43,5 @@ void main() {
     expect(result, "Hello There");
   });
 }
+
+*/