summaryrefslogtreecommitdiff
path: root/extension/react-app/src
diff options
context:
space:
mode:
Diffstat (limited to 'extension/react-app/src')
-rw-r--r--extension/react-app/src/components/ComboBox.tsx240
1 files changed, 193 insertions, 47 deletions
diff --git a/extension/react-app/src/components/ComboBox.tsx b/extension/react-app/src/components/ComboBox.tsx
index 41b44684..cf7c4298 100644
--- a/extension/react-app/src/components/ComboBox.tsx
+++ b/extension/react-app/src/components/ComboBox.tsx
@@ -1,4 +1,5 @@
import React, {
+ useCallback,
useContext,
useEffect,
useImperativeHandle,
@@ -19,6 +20,7 @@ import {
BookmarkIcon,
DocumentPlusIcon,
FolderArrowDownIcon,
+ ArrowLeftIcon,
} from "@heroicons/react/24/outline";
import { ContextItem } from "../../../schema/FullState";
import { postVscMessage } from "../vscode";
@@ -164,37 +166,53 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
const [items, setItems] = React.useState(props.items);
const inputRef = React.useRef<HTMLInputElement>(null);
- const [inputBoxHeight, setInputBoxHeight] = useState<string | undefined>(
- undefined
- );
// Whether the current input follows an '@' and should be treated as context query
const [currentlyInContextQuery, setCurrentlyInContextQuery] = useState(false);
+ const [nestedContextProvider, setNestedContextProvider] = useState<
+ any | undefined
+ >(undefined);
- const { getInputProps, ...downshiftProps } = useCombobox({
- 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("@");
+ useEffect(() => {
+ if (!currentlyInContextQuery) {
+ setNestedContextProvider(undefined);
+ }
+ }, [currentlyInContextQuery]);
- // Tell server the context item was selected
- client?.selectContextItem(selectedItem.id, query);
+ const contextProviders = useSelector(
+ (state: RootStore) => state.serverState.context_providers
+ ) as any[];
- // Remove the '@' and the context query from the input
- if (downshiftProps.inputValue.includes("@")) {
- downshiftProps.setInputValue(restOfInput);
- }
- }
- },
- onInputValueChange({ inputValue, highlightedIndex }) {
+ const goBackToContextProviders = () => {
+ setCurrentlyInContextQuery(false);
+ setNestedContextProvider(undefined);
+ downshiftProps.setInputValue("@");
+ };
+
+ useEffect(() => {
+ if (!nestedContextProvider) {
+ console.log("setting items", nestedContextProvider);
+ setItems(
+ contextProviders?.map((provider) => ({
+ name: provider.display_title,
+ description: provider.description,
+ id: provider.title,
+ })) || []
+ );
+ }
+ }, [nestedContextProvider]);
+
+ const onInputValueChangeCallback = useCallback(
+ ({ inputValue, highlightedIndex }: any) => {
+ // Clear the input
if (!inputValue) {
setItems([]);
+ setNestedContextProvider(undefined);
return;
}
props.onInputValueChange(inputValue);
+ // Handle context selection
if (inputValue.endsWith("@") || currentlyInContextQuery) {
const segs = inputValue?.split("@") || [];
@@ -202,46 +220,114 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
// Get search results and return
setCurrentlyInContextQuery(true);
const providerAndQuery = segs[segs.length - 1] || "";
- // Only return context items from the current workspace - the index is currently shared between all sessions
- const workspaceFilter =
- workspacePaths && workspacePaths.length > 0
- ? `workspace_dir IN [ ${workspacePaths
- .map((path) => `"${path}"`)
- .join(", ")} ]`
- : undefined;
- searchClient
- .index(SEARCH_INDEX_NAME)
- .search(providerAndQuery, {
- filter: workspaceFilter,
- })
- .then((res) => {
- setItems(
- res.hits.map((hit) => {
- return {
- name: hit.name,
- description: hit.description,
- id: hit.id,
- content: hit.content,
- };
- })
- );
- })
- .catch(() => {
- // Swallow errors, because this simply is not supported on Windows at the moment
+
+ if (nestedContextProvider && !inputValue.endsWith("@")) {
+ // Search only within this specific context provider
+ getFilteredContextItemsForProvider(
+ nestedContextProvider.title,
+ providerAndQuery
+ ).then((res) => {
+ setItems(res);
});
+ } else {
+ // Search through the list of context providers
+ const filteredItems =
+ contextProviders
+ ?.filter(
+ (provider) =>
+ `@${provider.title}`
+ .toLowerCase()
+ .startsWith(inputValue.toLowerCase()) ||
+ `@${provider.display_title}`
+ .toLowerCase()
+ .startsWith(inputValue.toLowerCase())
+ )
+ .map((provider) => ({
+ name: provider.display_title,
+ description: provider.description,
+ id: provider.title,
+ })) || [];
+ setItems(filteredItems);
+ setCurrentlyInContextQuery(true);
+ }
return;
} else {
// Exit the '@' context menu
setCurrentlyInContextQuery(false);
- setItems;
+ setNestedContextProvider(undefined);
}
}
+
+ setNestedContextProvider(undefined);
+
+ // Handle slash commands
setItems(
props.items.filter((item) =>
item.name.toLowerCase().startsWith(inputValue.toLowerCase())
)
);
},
+ [props.items, currentlyInContextQuery, nestedContextProvider]
+ );
+
+ const getFilteredContextItemsForProvider = async (
+ provider: string,
+ query: string
+ ) => {
+ // Only return context items from the current workspace - the index is currently shared between all sessions
+ const workspaceFilter =
+ workspacePaths && workspacePaths.length > 0
+ ? `workspace_dir IN [ ${workspacePaths
+ .map((path) => `"${path}"`)
+ .join(", ")} ] AND provider_name = '${provider}'`
+ : undefined;
+ try {
+ const res = await searchClient.index(SEARCH_INDEX_NAME).search(query, {
+ filter: workspaceFilter,
+ });
+ return (
+ res?.hits.map((hit) => {
+ return {
+ name: hit.name,
+ description: hit.description,
+ id: hit.id,
+ content: hit.content,
+ };
+ }) || []
+ );
+ } catch (e) {
+ console.log("Error searching context items", e);
+ return [];
+ }
+ };
+
+ const { getInputProps, ...downshiftProps } = useCombobox({
+ onSelectedItemChange: ({ selectedItem }) => {
+ if (!selectedItem) return;
+ if (selectedItem.id) {
+ // Get the query from the input value
+ const segs = downshiftProps.inputValue.split("@");
+ const query = segs[segs.length - 1];
+
+ // Tell server the context item was selected
+ client?.selectContextItem(selectedItem.id, query);
+ if (downshiftProps.inputValue.includes("@")) {
+ const selectedNestedContextProvider = contextProviders.find(
+ (provider) => provider.title === selectedItem.id
+ );
+ if (
+ !nestedContextProvider &&
+ !selectedNestedContextProvider?.dynamic
+ ) {
+ downshiftProps.setInputValue(`@${selectedItem.id} `);
+ setNestedContextProvider(selectedNestedContextProvider);
+ } else {
+ downshiftProps.setInputValue("");
+ }
+ }
+ }
+ },
+ onInputValueChange: onInputValueChangeCallback,
items,
itemToString(item) {
return item ? item.name : "";
@@ -467,7 +553,6 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
target.scrollHeight,
300
).toString()}px`;
- setInputBoxHeight(target.style.height);
// setShowContextDropdown(target.value.endsWith("@"));
},
@@ -498,6 +583,31 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
props.onEnter(event);
}
setCurrentlyInContextQuery(false);
+ } else if (
+ event.key === "Enter" &&
+ currentlyInContextQuery &&
+ nestedContextProvider === undefined
+ ) {
+ const newProviderName =
+ items[downshiftProps.highlightedIndex].name;
+ const newProvider = contextProviders.find(
+ (provider) => provider.display_title === newProviderName
+ );
+
+ if (!newProvider) {
+ (event.nativeEvent as any).preventDownshiftDefault = true;
+ return;
+ } else if (newProvider.dynamic) {
+ return;
+ }
+
+ setNestedContextProvider(newProvider);
+ downshiftProps.setInputValue(`@${newProvider.title} `);
+ (event.nativeEvent as any).preventDownshiftDefault = true;
+ event.preventDefault();
+ getFilteredContextItemsForProvider(newProvider.title, "").then(
+ (items) => setItems(items)
+ );
} else if (event.key === "Tab" && items.length > 0) {
downshiftProps.setInputValue(items[0].name);
event.preventDefault();
@@ -545,6 +655,12 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
);
setCurrentlyInContextQuery(false);
} else if (event.key === "Escape") {
+ if (nestedContextProvider) {
+ goBackToContextProviders();
+ (event.nativeEvent as any).preventDownshiftDefault = true;
+ return;
+ }
+
setCurrentlyInContextQuery(false);
if (downshiftProps.isOpen && items.length > 0) {
downshiftProps.closeMenu();
@@ -578,6 +694,30 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
ulHeightPixels={ulRef.current?.getBoundingClientRect().height || 0}
hidden={!downshiftProps.isOpen || items.length === 0}
>
+ {nestedContextProvider && (
+ <div
+ style={{
+ backgroundColor: secondaryDark,
+ borderBottom: `1px solid ${lightGray}`,
+ display: "flex",
+ gap: "4px",
+ position: "sticky",
+ top: "0px",
+ }}
+ className="py-2 px-4 my-0"
+ >
+ <ArrowLeftIcon
+ width="1.4em"
+ height="1.4em"
+ className="cursor-pointer"
+ onClick={() => {
+ goBackToContextProviders();
+ }}
+ />
+ {nestedContextProvider.display_title} -{" "}
+ {nestedContextProvider.description}
+ </div>
+ )}
{downshiftProps.isOpen &&
items.map((item, index) => (
<Li
@@ -586,6 +726,12 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
{...downshiftProps.getItemProps({ item, index })}
highlighted={downshiftProps.highlightedIndex === index}
selected={downshiftProps.selectedItem === item}
+ onClick={(e) => {
+ e.stopPropagation();
+ e.preventDefault();
+ (e.nativeEvent as any).preventDownshiftDefault = true;
+ downshiftProps.selectItem(item);
+ }}
>
<span>
{item.name}