From 4d7e72970f770eb49627589fb142c93dfb6fd73b Mon Sep 17 00:00:00 2001
From: Nate Sesti <sestinj@gmail.com>
Date: Sat, 22 Jul 2023 22:37:13 -0700
Subject: @ feature (very large commit)

---
 extension/react-app/src/components/ComboBox.tsx | 174 ++++++++++++++----------
 1 file changed, 102 insertions(+), 72 deletions(-)

(limited to 'extension/react-app/src/components/ComboBox.tsx')

diff --git a/extension/react-app/src/components/ComboBox.tsx b/extension/react-app/src/components/ComboBox.tsx
index f327e3a3..22a6d000 100644
--- a/extension/react-app/src/components/ComboBox.tsx
+++ b/extension/react-app/src/components/ComboBox.tsx
@@ -1,4 +1,9 @@
-import React, { useEffect, useImperativeHandle, useState } from "react";
+import React, {
+  useContext,
+  useEffect,
+  useImperativeHandle,
+  useState,
+} from "react";
 import { useCombobox } from "downshift";
 import styled from "styled-components";
 import {
@@ -8,13 +13,15 @@ import {
   vscBackground,
   vscForeground,
 } from ".";
-import CodeBlock from "./CodeBlock";
 import PillButton from "./PillButton";
 import HeaderButtonWithText from "./HeaderButtonWithText";
 import { DocumentPlus } from "@styled-icons/heroicons-outline";
-import { HighlightedRangeContext } from "../../../schema/FullState";
+import { ContextItem } from "../../../schema/FullState";
 import { postVscMessage } from "../vscode";
-import { getMetaKeyLabel } from "../util";
+import { GUIClientContext } from "../App";
+import { MeiliSearch } from "meilisearch";
+
+const SEARCH_INDEX_NAME = "continue_context_items";
 
 // #region styled components
 const mainInputFontSize = 13;
@@ -64,14 +71,14 @@ const Ul = styled.ul<{
   hidden: boolean;
   showAbove: boolean;
   ulHeightPixels: number;
+  inputBoxHeight?: string;
 }>`
   ${(props) =>
     props.showAbove
       ? `transform: translateY(-${props.ulHeightPixels + 8}px);`
-      : `transform: translateY(${2 * mainInputFontSize}px);`}
+      : `transform: translateY(${5 * mainInputFontSize}px);`}
   position: absolute;
   background: ${vscBackground};
-  background-color: ${secondaryDark};
   color: ${vscForeground};
   max-height: ${UlMaxHeight}px;
   width: calc(100% - 16px);
@@ -80,15 +87,9 @@ const Ul = styled.ul<{
   padding: 0;
   ${({ hidden }) => hidden && "display: none;"}
   border-radius: ${defaultBorderRadius};
-  outline: 0.5px solid gray;
+  outline: 1px solid ${lightGray};
   z-index: 2;
-  // Get rid of scrollbar and its padding
-  scrollbar-width: none;
   -ms-overflow-style: none;
-  &::-webkit-scrollbar {
-    width: 0px;
-    background: transparent; /* make scrollbar transparent */
-  }
 `;
 
 const Li = styled.li<{
@@ -96,49 +97,90 @@ const Li = styled.li<{
   selected: boolean;
   isLastItem: boolean;
 }>`
-  background-color: ${secondaryDark};
-  ${({ highlighted }) => highlighted && "background: #ff000066;"}
+  background-color: ${({ highlighted }) =>
+    highlighted ? lightGray : secondaryDark};
+  ${({ highlighted }) => highlighted && `background: ${vscBackground};`}
   ${({ selected }) => selected && "font-weight: bold;"}
     padding: 0.5rem 0.75rem;
   display: flex;
   flex-direction: column;
   ${({ isLastItem }) => isLastItem && "border-bottom: 1px solid gray;"}
-  border-top: 1px solid gray;
+  /* border-top: 1px solid gray; */
   cursor: pointer;
 `;
 
 // #endregion
 
 interface ComboBoxProps {
-  items: { name: string; description: string }[];
+  items: { name: string; description: string; id?: string }[];
   onInputValueChange: (inputValue: string) => void;
   disabled?: boolean;
   onEnter: (e: React.KeyboardEvent<HTMLInputElement>) => void;
-  highlightedCodeSections: HighlightedRangeContext[];
-  deleteContextItems: (indices: number[]) => void;
-  onTogglePin: () => void;
+  selectedContextItems: ContextItem[];
   onToggleAddContext: () => void;
   addingHighlightedCode: boolean;
 }
 
 const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
+  const searchClient = new MeiliSearch({ host: "http://127.0.0.1:7700" });
+  const client = useContext(GUIClientContext);
+
   const [history, setHistory] = React.useState<string[]>([]);
   // The position of the current command you are typing now, so the one that will be appended to history once you press enter
   const [positionInHistory, setPositionInHistory] = React.useState<number>(0);
   const [items, setItems] = React.useState(props.items);
-  const [highlightedCodeSections, setHighlightedCodeSections] = React.useState(
-    props.highlightedCodeSections || []
-  );
+
   const inputRef = React.useRef<HTMLInputElement>(null);
+  const [inputBoxHeight, setInputBoxHeight] = useState<string | undefined>(
+    undefined
+  );
 
-  useEffect(() => {
-    setHighlightedCodeSections(props.highlightedCodeSections || []);
-  }, [props.highlightedCodeSections]);
+  // Whether the current input follows an '@' and should be treated as context query
+  const [currentlyInContextQuery, setCurrentlyInContextQuery] = useState(false);
 
   const { getInputProps, ...downshiftProps } = useCombobox({
-    onInputValueChange({ inputValue }) {
+    onSelectedItemChange: ({ selectedItem }) => {
+      if (selectedItem?.id) {
+        // Get the query from the input value
+        const segs = downshiftProps.inputValue.split("@");
+        const query = segs[segs.length - 1];
+        const restOfInput = segs.splice(0, segs.length - 1).join("@");
+
+        // Tell server the context item was selected
+        client?.selectContextItem(selectedItem.id, query);
+
+        // Remove the '@' and the context query from the input
+        if (downshiftProps.inputValue.includes("@")) {
+          downshiftProps.setInputValue(restOfInput);
+        }
+      }
+    },
+    onInputValueChange({ inputValue, highlightedIndex }) {
       if (!inputValue) return;
       props.onInputValueChange(inputValue);
+
+      if (inputValue.endsWith("@") || currentlyInContextQuery) {
+        setCurrentlyInContextQuery(true);
+
+        const segs = inputValue.split("@");
+        const providerAndQuery = segs[segs.length - 1];
+        const [provider, query] = providerAndQuery.split(" ");
+        searchClient
+          .index(SEARCH_INDEX_NAME)
+          .search(providerAndQuery)
+          .then((res) => {
+            setItems(
+              res.hits.map((hit) => {
+                return {
+                  name: hit.name,
+                  description: hit.description,
+                  id: hit.id,
+                };
+              })
+            );
+          });
+        return;
+      }
       setItems(
         props.items.filter((item) =>
           item.name.toLowerCase().startsWith(inputValue.toLowerCase())
@@ -151,6 +193,16 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
     },
   });
 
+  useEffect(() => {
+    console.log(
+      "downshiftProps.highlightedIndex",
+      downshiftProps.highlightedIndex
+    );
+    if (downshiftProps.highlightedIndex < 0) {
+      downshiftProps.setHighlightedIndex(0);
+    }
+  }, [downshiftProps.inputValue]);
+
   useImperativeHandle(ref, () => downshiftProps, [downshiftProps]);
 
   const [metaKeyPressed, setMetaKeyPressed] = useState(false);
@@ -199,50 +251,22 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
   return (
     <>
       <div className="px-2 flex gap-2 items-center flex-wrap mt-2">
-        {/* {highlightedCodeSections.length > 1 && (
-          <>
-            <HeaderButtonWithText
-              text="Clear Context"
-              onClick={() => {
-                props.deleteContextItems(
-                  highlightedCodeSections.map((_, idx) => idx)
-                );
-              }}
-            >
-              <Trash size="1.6em" />
-            </HeaderButtonWithText>
-          </>
-        )} */}
-        {highlightedCodeSections.map((section, idx) => (
-          <PillButton
-            warning={
-              section.range.contents.length > 4000 && section.editing
-                ? "Editing such a large range may be slow"
-                : undefined
-            }
-            onlyShowDelete={
-              highlightedCodeSections.length <= 1 || section.editing
-            }
-            editing={section.editing}
-            pinned={section.pinned}
-            index={idx}
-            key={`${section.display_name}${idx}`}
-            title={`${section.display_name} (${
-              section.range.range.start.line + 1
-            }-${section.range.range.end.line + 1})`}
-            onDelete={() => {
-              if (props.deleteContextItems) {
-                props.deleteContextItems([idx]);
+        {props.selectedContextItems.map((item, idx) => {
+          return (
+            <PillButton
+              key={`${item.description.id.item_id}${idx}`}
+              item={item}
+              warning={
+                item.content.length > 4000 && item.editing
+                  ? "Editing such a large range may be slow"
+                  : undefined
               }
-              setHighlightedCodeSections((prev) => {
-                const newSections = [...prev];
-                newSections.splice(idx, 1);
-                return newSections;
-              });
-            }}
-          />
-        ))}
-        {props.highlightedCodeSections.length > 0 &&
+              addingHighlightedCode={props.addingHighlightedCode}
+              index={idx}
+            />
+          );
+        })}
+        {props.selectedContextItems.length > 0 &&
           (props.addingHighlightedCode ? (
             <EmptyPillDiv
               onClick={() => {
@@ -275,6 +299,7 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
                 target.scrollHeight,
                 300
               ).toString()}px`;
+              setInputBoxHeight(target.style.height);
 
               // setShowContextDropdown(target.value.endsWith("@"));
             },
@@ -289,6 +314,7 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
               if (event.key === "Enter" && event.shiftKey) {
                 // Prevent Downshift's default 'Enter' behavior.
                 (event.nativeEvent as any).preventDownshiftDefault = true;
+                setCurrentlyInContextQuery(false);
               } else if (
                 event.key === "Enter" &&
                 (!downshiftProps.isOpen || items.length === 0)
@@ -302,6 +328,7 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
                 (event.nativeEvent as any).preventDownshiftDefault = true;
 
                 if (props.onEnter) props.onEnter(event);
+                setCurrentlyInContextQuery(false);
               } else if (event.key === "Tab" && items.length > 0) {
                 downshiftProps.setInputValue(items[0].name);
                 event.preventDefault();
@@ -321,6 +348,7 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
                 }
                 downshiftProps.setInputValue(history[positionInHistory - 1]);
                 setPositionInHistory((prev) => prev - 1);
+                setCurrentlyInContextQuery(false);
               } else if (event.key === "ArrowDown") {
                 if (positionInHistory < history.length) {
                   downshiftProps.setInputValue(history[positionInHistory + 1]);
@@ -328,6 +356,7 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
                 setPositionInHistory((prev) =>
                   Math.min(prev + 1, history.length)
                 );
+                setCurrentlyInContextQuery(false);
               }
             },
             ref: inputRef,
@@ -351,13 +380,14 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
                 selected={downshiftProps.selectedItem === item}
               >
                 <span>
-                  {item.name}: {item.description}
+                  {item.name}:{"  "}
+                  <span style={{ color: lightGray }}>{item.description}</span>
                 </span>
               </Li>
             ))}
         </Ul>
       </div>
-      {highlightedCodeSections.length === 0 &&
+      {props.selectedContextItems.length === 0 &&
         (downshiftProps.inputValue?.startsWith("/edit") ||
           (focused &&
             metaKeyPressed &&
-- 
cgit v1.2.3-70-g09d2

