From 4d7e72970f770eb49627589fb142c93dfb6fd73b Mon Sep 17 00:00:00 2001 From: Nate Sesti 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) => 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([]); // 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(0); const [items, setItems] = React.useState(props.items); - const [highlightedCodeSections, setHighlightedCodeSections] = React.useState( - props.highlightedCodeSections || [] - ); + const inputRef = React.useRef(null); + const [inputBoxHeight, setInputBoxHeight] = useState( + 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 ( <>
- {/* {highlightedCodeSections.length > 1 && ( - <> - { - props.deleteContextItems( - highlightedCodeSections.map((_, idx) => idx) - ); - }} - > - - - - )} */} - {highlightedCodeSections.map((section, idx) => ( - 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 ( + 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 ? ( { @@ -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} > - {item.name}: {item.description} + {item.name}:{" "} + {item.description} ))}
- {highlightedCodeSections.length === 0 && + {props.selectedContextItems.length === 0 && (downshiftProps.inputValue?.startsWith("/edit") || (focused && metaKeyPressed && -- cgit v1.2.3-70-g09d2