Repository: bokeh/bokeh

Issue:
AutocompleteInput has no case insensitive option  
 Is there a way to have AutocompleteInput to be case insensitive?

For example when I type in "book" and in the completions there is a keyword named "Book of the Jungle" then there is no match. 

Is there an option that I am missing ?


Discussion:
@gmanolia You are correct the only current option is for case-sensitive comparison. I think this should be a fairly straightforward add, so I have marked it `good first issue`. Would you be interested to work on this, with some help/guidance? 
Some notes for any new contributor:

* Dev Guide for getting set up and running is [here](https://docs.bokeh.org/en/latest/docs/dev_guide/setup.html)

* On the Python side, the `AutocompleteInput` is defined in here:

  https://github.com/bokeh/bokeh/blob/master/bokeh/models/widgets/inputs.py

  It woudl need to get a new property to control this new behaviour. Possibly a `Bool` to toggle case sensitive or not. But if we imagine more other kinds of policies it might be better to start with a policy `Enum`

* The BokehJS code to find "startswith" completions is here:

  https://github.com/bokeh/bokeh/blob/379f644e5133a1ecdba3ecf73395df0957213aa8/bokehjs/src/lib/models/widgets/autocomplete_input.ts#L141-L145

 Best would be to factor that into a method that conditions on the policy(s) that is set (case insensitive, startswith vs inclusion, etc.) to return an appropriate set of comlpetions.
I'm looking to help with this issue. 

Resolving patch:
diff --git a/bokeh/models/widgets/inputs.py b/bokeh/models/widgets/inputs.py
--- a/bokeh/models/widgets/inputs.py
+++ b/bokeh/models/widgets/inputs.py
@@ -228,6 +228,7 @@ class AutocompleteInput(TextInput):
     The number of characters a user must type before completions are presented.
     """)
 
+    case_sensitive = Bool(default=True, help="""Enable or disable case sensitivity""")
 
 class Select(InputWidget):
     ''' Single-select widget.


Test patch:
diff --git a/tests/integration/widgets/test_autocomplete_input.py b/tests/integration/widgets/test_autocomplete_input.py
--- a/tests/integration/widgets/test_autocomplete_input.py
+++ b/tests/integration/widgets/test_autocomplete_input.py
@@ -125,7 +125,7 @@ def test_displays_menu(self, bokeh_model_page) -> None:
 
     def test_min_characters(self, bokeh_model_page) -> None:
         text_input = AutocompleteInput(title="title", css_classes=["foo"],
-                                       completions = ["100001", "12344556", "12344557", "3194567289", "209374209374"],
+                                       completions = ["100001", "12344556", "12344557", "3194567289", "209374209374", "aaaaaa", "aaabbb", "AAAaAA", "AAABbB"],
                                        min_characters=1)
 
         page = bokeh_model_page(text_input)
@@ -149,6 +149,97 @@ def test_min_characters(self, bokeh_model_page) -> None:
         assert "bk-active" not in items[1].get_attribute('class')
         assert "bk-active" not in items[2].get_attribute('class')
 
+    def test_case_insensitivity(self, bokeh_model_page) -> None:
+        text_input = AutocompleteInput(title="title", css_classes=["foo"], case_sensitive=False, completions = ["100001", "aaaaaa", "aaabbb", "AAAaAA", "AAABbB"])
+
+        page = bokeh_model_page(text_input)
+
+        el = page.driver.find_element_by_css_selector('.foo .bk-menu')
+        assert 'display: none;' in el.get_attribute('style')
+
+        # double click to highlight and overwrite old text
+        el = page.driver.find_element_by_css_selector('.foo input')
+        enter_text_in_element(page.driver, el, "aAa", click=2, enter=False)
+
+        el = page.driver.find_element_by_css_selector('.foo .bk-menu')
+        assert 'display: none;' not in el.get_attribute('style')
+
+        items = el.find_elements_by_tag_name("div")
+        assert len(items) == 4
+        assert items[0].text == "aaaaaa"
+        assert items[1].text == "aaabbb"
+        assert items[2].text == "AAAaAA"
+        assert items[3].text == "AAABbB"
+        assert "bk-active" in items[0].get_attribute('class')
+
+        el = page.driver.find_element_by_css_selector('.foo input')
+        enter_text_in_element(page.driver, el, "aAaB", click=2, enter=False)
+
+        el = page.driver.find_element_by_css_selector('.foo .bk-menu')
+        assert 'display: none;' not in el.get_attribute('style')
+
+        items = el.find_elements_by_tag_name("div")
+        assert len(items) == 2
+        assert items[0].text == "aaabbb"
+        assert items[1].text == "AAABbB"
+        assert "bk-active" in items[0].get_attribute('class')
+        assert "bk-active" not in items[1].get_attribute('class')
+
+        enter_text_in_element(page.driver, el, Keys.DOWN, click=0, enter=False)
+        items = el.find_elements_by_tag_name("div")
+        assert len(items) == 2
+        assert items[0].text == "aaabbb"
+        assert items[1].text == "AAABbB"
+        assert "bk-active" not in items[0].get_attribute('class')
+        assert "bk-active" in items[1].get_attribute('class')
+
+        assert page.has_no_console_errors()
+
+    def test_case_sensitivity(self, bokeh_model_page) -> None:
+        # case_sensitive=True by default
+        text_input = AutocompleteInput(title="title", css_classes=["foo"], completions = ["100001", "aAaaaa", "aAaBbb", "AAAaAA", "aAaBbB"])
+
+        page = bokeh_model_page(text_input)
+
+        el = page.driver.find_element_by_css_selector('.foo .bk-menu')
+        assert 'display: none;' in el.get_attribute('style')
+
+        # double click to highlight and overwrite old text
+        el = page.driver.find_element_by_css_selector('.foo input')
+        enter_text_in_element(page.driver, el, "aAa", click=2, enter=False)
+
+        el = page.driver.find_element_by_css_selector('.foo .bk-menu')
+        assert 'display: none;' not in el.get_attribute('style')
+
+        items = el.find_elements_by_tag_name("div")
+        assert len(items) == 3
+        assert items[0].text == "aAaaaa"
+        assert items[1].text == "aAaBbb"
+        assert items[2].text == "aAaBbB"
+        assert "bk-active" in items[0].get_attribute('class')
+
+        el = page.driver.find_element_by_css_selector('.foo input')
+        enter_text_in_element(page.driver, el, "aAaB", click=2, enter=False)
+
+        el = page.driver.find_element_by_css_selector('.foo .bk-menu')
+        assert 'display: none;' not in el.get_attribute('style')
+
+        items = el.find_elements_by_tag_name("div")
+        assert len(items) == 2
+        assert items[0].text == "aAaBbb"
+        assert items[1].text == "aAaBbB"
+        assert "bk-active" in items[0].get_attribute('class')
+
+        enter_text_in_element(page.driver, el, Keys.DOWN, click=0, enter=False)
+        items = el.find_elements_by_tag_name("div")
+        assert len(items) == 2
+        assert items[0].text == "aAaBbb"
+        assert items[1].text == "aAaBbB"
+        assert "bk-active" not in items[0].get_attribute('class')
+        assert "bk-active" in items[1].get_attribute('class')
+
+        assert page.has_no_console_errors()
+
     def test_arrow_cannot_escape_menu(self, bokeh_model_page) -> None:
         text_input = AutocompleteInput(title="title", css_classes=["foo"], completions = ["100001", "12344556", "12344557", "3194567289", "209374209374"])