From 36577b8e94809da47a540499132774a0fe2c085d Mon Sep 17 00:00:00 2001
From: Nate Sesti <sestinj@gmail.com>
Date: Sat, 1 Jul 2023 12:36:57 -0700
Subject: explicit context pill buttons

---
 extension/react-app/src/components/ComboBox.tsx | 258 ++++++++++++++++--------
 1 file changed, 175 insertions(+), 83 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 3816cee8..34027a42 100644
--- a/extension/react-app/src/components/ComboBox.tsx
+++ b/extension/react-app/src/components/ComboBox.tsx
@@ -1,4 +1,4 @@
-import React, { useCallback } from "react";
+import React, { useCallback, useEffect } from "react";
 import { useCombobox } from "downshift";
 import styled from "styled-components";
 import {
@@ -7,8 +7,44 @@ import {
   secondaryDark,
   vscBackground,
 } from ".";
+import CodeBlock from "./CodeBlock";
+import { RangeInFile } from "../../../src/client";
 
 const mainInputFontSize = 16;
+
+const ContextDropdown = styled.div`
+  position: absolute;
+  padding: 4px;
+  width: calc(100% - 16px - 8px);
+  background-color: ${secondaryDark};
+  color: white;
+  border-bottom-right-radius: ${defaultBorderRadius};
+  border-bottom-left-radius: ${defaultBorderRadius};
+  /* border: 1px solid white; */
+  border-top: none;
+  margin-left: 8px;
+  margin-right: 8px;
+  margin-top: -12px;
+  outline: 1px solid orange;
+`;
+
+const PillButton = styled.button`
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  border: none;
+  color: white;
+  background-color: gray;
+  border-radius: 50px;
+  padding: 5px 10px;
+  margin: 5px 0;
+  cursor: pointer;
+
+  &:hover {
+    background-color: ${buttonColor};
+  }
+`;
+
 const MainTextInput = styled.textarea`
   resize: none;
 
@@ -20,6 +56,7 @@ const MainTextInput = styled.textarea`
   width: 100%;
   background-color: ${vscBackground};
   color: white;
+  z-index: 1;
 
   &:focus {
     border: 1px solid transparent;
@@ -49,6 +86,7 @@ const Ul = styled.ul<{
   border-radius: ${defaultBorderRadius};
   overflow: hidden;
   border: 0.5px solid gray;
+  z-index: 2;
 `;
 
 const Li = styled.li<{
@@ -71,6 +109,8 @@ interface ComboBoxProps {
   onInputValueChange: (inputValue: string) => void;
   disabled?: boolean;
   onEnter?: (e: React.KeyboardEvent<HTMLInputElement>) => void;
+  highlightedCodeSections?: (RangeInFile & { contents: string })[];
+  deleteContextItem?: (idx: number) => void;
 }
 
 const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
@@ -78,6 +118,24 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
   // 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 [showContextDropdown, setShowContextDropdown] = React.useState(false);
+  const [highlightedCodeSections, setHighlightedCodeSections] = React.useState(
+    props.highlightedCodeSections || [
+      {
+        filepath: "test.ts",
+        range: {
+          start: { line: 0, character: 0 },
+          end: { line: 0, character: 0 },
+        },
+        contents: "import * as a from 'a';",
+      },
+    ]
+  );
+
+  useEffect(() => {
+    setHighlightedCodeSections(props.highlightedCodeSections || []);
+  }, [props.highlightedCodeSections]);
+
   const {
     isOpen,
     getToggleButtonProps,
@@ -111,90 +169,124 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
   };
 
   return (
-    <div className="flex px-2" ref={divRef} hidden={!isOpen}>
-      <MainTextInput
-        disabled={props.disabled}
-        placeholder="Type '/' to see available slash commands."
-        {...getInputProps({
-          onChange: (e) => {
-            const target = e.target as HTMLTextAreaElement;
-            // Update the height of the textarea to match the content, up to a max of 200px.
-            target.style.height = "auto";
-            target.style.height = `${Math.min(
-              target.scrollHeight,
-              300
-            ).toString()}px`;
-          },
-          onKeyDown: (event) => {
-            if (event.key === "Enter" && event.shiftKey) {
-              // Prevent Downshift's default 'Enter' behavior.
-              (event.nativeEvent as any).preventDownshiftDefault = true;
-            } else if (
-              event.key === "Enter" &&
-              (!isOpen || items.length === 0)
-            ) {
-              // Prevent Downshift's default 'Enter' behavior.
-              (event.nativeEvent as any).preventDownshiftDefault = true;
-              if (props.onEnter) props.onEnter(event);
-              setInputValue("");
-              const value = event.currentTarget.value;
-              if (value !== "") {
-                setPositionInHistory(history.length + 1);
-                setHistory([...history, value]);
+    <>
+      <div className="flex px-2" ref={divRef} hidden={!isOpen}>
+        <MainTextInput
+          disabled={props.disabled}
+          placeholder="Type '/' to see available slash commands."
+          {...getInputProps({
+            onChange: (e) => {
+              const target = e.target as HTMLTextAreaElement;
+              // Update the height of the textarea to match the content, up to a max of 200px.
+              target.style.height = "auto";
+              target.style.height = `${Math.min(
+                target.scrollHeight,
+                300
+              ).toString()}px`;
+
+              setShowContextDropdown(target.value.endsWith("@"));
+            },
+            onKeyDown: (event) => {
+              if (event.key === "Enter" && event.shiftKey) {
+                // Prevent Downshift's default 'Enter' behavior.
+                (event.nativeEvent as any).preventDownshiftDefault = true;
+              } else if (
+                event.key === "Enter" &&
+                (!isOpen || items.length === 0)
+              ) {
+                // Prevent Downshift's default 'Enter' behavior.
+                (event.nativeEvent as any).preventDownshiftDefault = true;
+                if (props.onEnter) props.onEnter(event);
+                setInputValue("");
+                const value = event.currentTarget.value;
+                if (value !== "") {
+                  setPositionInHistory(history.length + 1);
+                  setHistory([...history, value]);
+                }
+              } else if (event.key === "Tab" && items.length > 0) {
+                setInputValue(items[0].name);
+                event.preventDefault();
+              } else if (
+                event.key === "ArrowUp" ||
+                (event.key === "ArrowDown" &&
+                  event.currentTarget.value.split("\n").length > 1)
+              ) {
+                (event.nativeEvent as any).preventDownshiftDefault = true;
+              } else if (
+                event.key === "ArrowUp" &&
+                event.currentTarget.value.split("\n").length > 1
+              ) {
+                if (positionInHistory == 0) return;
+                setInputValue(history[positionInHistory - 1]);
+                setPositionInHistory((prev) => prev - 1);
+              } else if (
+                event.key === "ArrowDown" &&
+                event.currentTarget.value.split("\n").length > 1
+              ) {
+                if (positionInHistory < history.length - 1) {
+                  setInputValue(history[positionInHistory + 1]);
+                }
+                setPositionInHistory((prev) =>
+                  Math.min(prev + 1, history.length)
+                );
               }
-            } else if (event.key === "Tab" && items.length > 0) {
-              setInputValue(items[0].name);
-              event.preventDefault();
-            } else if (
-              event.key === "ArrowUp" ||
-              (event.key === "ArrowDown" &&
-                event.currentTarget.value.split("\n").length > 1)
-            ) {
-              (event.nativeEvent as any).preventDownshiftDefault = true;
-            } else if (
-              event.key === "ArrowUp" &&
-              event.currentTarget.value.split("\n").length > 1
-            ) {
-              if (positionInHistory == 0) return;
-              setInputValue(history[positionInHistory - 1]);
-              setPositionInHistory((prev) => prev - 1);
-            } else if (
-              event.key === "ArrowDown" &&
-              event.currentTarget.value.split("\n").length > 1
-            ) {
-              if (positionInHistory < history.length - 1) {
-                setInputValue(history[positionInHistory + 1]);
+            },
+            ref: ref as any,
+          })}
+        />
+        <Ul
+          {...getMenuProps({
+            ref: ulRef,
+          })}
+          showAbove={showAbove()}
+          ulHeightPixels={ulRef.current?.getBoundingClientRect().height || 0}
+        >
+          {isOpen &&
+            items.map((item, index) => (
+              <Li
+                key={`${item.name}${index}`}
+                {...getItemProps({ item, index })}
+                highlighted={highlightedIndex === index}
+                selected={selectedItem === item}
+              >
+                <span>
+                  {item.name}: {item.description}
+                </span>
+              </Li>
+            ))}
+        </Ul>
+      </div>
+      <ContextDropdown hidden={!showContextDropdown}>
+        <p>Highlight code to include as context:</p>
+        {highlightedCodeSections.map((section, idx) => (
+          <>
+            <p>{section.filepath}</p>
+            <CodeBlock showCopy={false} key={idx}>
+              {section.contents}
+            </CodeBlock>
+          </>
+        ))}
+      </ContextDropdown>
+      <div className="px-2">
+        {highlightedCodeSections.map((section, idx) => (
+          <PillButton
+            onClick={() => {
+              console.log("delete context item", idx);
+              if (props.deleteContextItem) {
+                props.deleteContextItem(idx);
               }
-              setPositionInHistory((prev) =>
-                Math.min(prev + 1, history.length)
-              );
-            }
-          },
-          ref: ref as any,
-        })}
-      />
-      <Ul
-        {...getMenuProps({
-          ref: ulRef,
-        })}
-        showAbove={showAbove()}
-        ulHeightPixels={ulRef.current?.getBoundingClientRect().height || 0}
-      >
-        {isOpen &&
-          items.map((item, index) => (
-            <Li
-              key={`${item.name}${index}`}
-              {...getItemProps({ item, index })}
-              highlighted={highlightedIndex === index}
-              selected={selectedItem === item}
-            >
-              <span>
-                {item.name}: {item.description}
-              </span>
-            </Li>
-          ))}
-      </Ul>
-    </div>
+              setHighlightedCodeSections((prev) => {
+                const newSections = [...prev];
+                newSections.splice(idx, 1);
+                return newSections;
+              });
+            }}
+          >
+            {section.filepath}
+          </PillButton>
+        ))}
+      </div>
+    </>
   );
 });
 
-- 
cgit v1.2.3-70-g09d2


From a606c13ca75f0c9177b3d04f20dcf7211d81f083 Mon Sep 17 00:00:00 2001
From: Nate Sesti <sestinj@gmail.com>
Date: Sun, 2 Jul 2023 20:14:27 -0700
Subject: finishing up explicit context

---
 continuedev/src/continuedev/core/autopilot.py      | 17 ++---
 continuedev/src/continuedev/models/filesystem.py   | 16 ++---
 continuedev/src/continuedev/models/main.py         |  5 ++
 continuedev/src/continuedev/steps/chat.py          |  2 +-
 extension/react-app/src/components/ComboBox.tsx    | 83 ++++++++++++----------
 .../react-app/src/components/ContinueButton.tsx    |  1 +
 extension/react-app/src/components/PillButton.tsx  | 66 +++++++++++++++++
 7 files changed, 131 insertions(+), 59 deletions(-)
 create mode 100644 extension/react-app/src/components/PillButton.tsx

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

diff --git a/continuedev/src/continuedev/core/autopilot.py b/continuedev/src/continuedev/core/autopilot.py
index b9e61c63..1a77ca64 100644
--- a/continuedev/src/continuedev/core/autopilot.py
+++ b/continuedev/src/continuedev/core/autopilot.py
@@ -139,21 +139,22 @@ class Autopilot(ContinueBaseModel):
         for rif in range_in_files:
             rif.filepath = os.path.relpath(rif.filepath, workspace_path)
 
+        old_ranges = self._highlighted_ranges + range_in_files
         new_ranges = []
-        for rif in range_in_files:
+
+        while len(old_ranges) > 0:
+            old_range = old_ranges.pop(0)
             found_overlap = False
-            for i in range(len(self._highlighted_ranges)):
-                hr = self._highlighted_ranges[i]
-                if hr.filepath == rif.filepath and hr.range.overlaps_with(rif.range):
-                    new_ranges.append(rif.union(hr))
+            for i in range(len(new_ranges)):
+                if old_range.filepath == new_ranges[i].filepath and old_range.range.overlaps_with(new_ranges[i].range):
+                    new_ranges[i] = old_range.union(new_ranges[i])
                     found_overlap = True
-                    self._highlighted_ranges.pop(i)
                     break
 
             if not found_overlap:
-                new_ranges.append(rif)
+                new_ranges.append(old_range)
 
-        self._highlighted_ranges += new_ranges
+        self._highlighted_ranges = new_ranges
         await self.update_subscribers()
 
     _step_depth: int = 0
diff --git a/continuedev/src/continuedev/models/filesystem.py b/continuedev/src/continuedev/models/filesystem.py
index fc1c3f13..df0b15d7 100644
--- a/continuedev/src/continuedev/models/filesystem.py
+++ b/continuedev/src/continuedev/models/filesystem.py
@@ -40,21 +40,15 @@ class RangeInFileWithContents(RangeInFile):
 
         assert first.filepath == second.filepath
 
-        # Calculate the start and end positions of the overlap
-        overlap_start = max(first.range.start,
-                            second.range.start) - first.range.start
-        overlap_end = min(first.range.end, second.range.end) - \
-            first.range.start
-
-        # Calculate the new contents by removing the overlap
-        union_contents = first.contents[:overlap_start] + \
-            second.contents[overlap_start:overlap_end] + \
-            first.contents[overlap_end:]
+        # Calculate union of contents
+        num_overlapping_lines = first.range.end.line - second.range.start.line + 1
+        union_lines = first.contents.splitlines()[:-num_overlapping_lines] + \
+            second.contents.splitlines()
 
         return RangeInFileWithContents(
             filepath=first.filepath,
             range=first.range.union(second.range),
-            contents=union_contents
+            contents="\n".join(union_lines)
         )
 
     @staticmethod
diff --git a/continuedev/src/continuedev/models/main.py b/continuedev/src/continuedev/models/main.py
index 101be4ae..fa736772 100644
--- a/continuedev/src/continuedev/models/main.py
+++ b/continuedev/src/continuedev/models/main.py
@@ -43,6 +43,11 @@ class Position(BaseModel):
     def from_end_of_file(contents: str) -> "Position":
         return Position.from_index(contents, len(contents))
 
+    def to_index(self, string: str) -> int:
+        """Convert line and character to index in string"""
+        lines = string.splitlines()
+        return sum(map(len, lines[:self.line])) + self.character
+
 
 class Range(BaseModel):
     """A range in a file. 0-indexed."""
diff --git a/continuedev/src/continuedev/steps/chat.py b/continuedev/src/continuedev/steps/chat.py
index 8494563b..b10ec3d7 100644
--- a/continuedev/src/continuedev/steps/chat.py
+++ b/continuedev/src/continuedev/steps/chat.py
@@ -106,7 +106,7 @@ class RunTerminalCommandStep(Step):
 
 class ViewDirectoryTreeStep(Step):
     name: str = "View Directory Tree"
-    description: str = "View the directory tree to learn which folder and files exist."
+    description: str = "View the directory tree to learn which folder and files exist. You should always do this before adding new files."
 
     async def describe(self, models: Models) -> Coroutine[Any, Any, Coroutine[str, None, None]]:
         return f"Viewed the directory tree."
diff --git a/extension/react-app/src/components/ComboBox.tsx b/extension/react-app/src/components/ComboBox.tsx
index 34027a42..f299c3a2 100644
--- a/extension/react-app/src/components/ComboBox.tsx
+++ b/extension/react-app/src/components/ComboBox.tsx
@@ -9,6 +9,7 @@ import {
 } from ".";
 import CodeBlock from "./CodeBlock";
 import { RangeInFile } from "../../../src/client";
+import PillButton from "./PillButton";
 
 const mainInputFontSize = 16;
 
@@ -22,27 +23,9 @@ const ContextDropdown = styled.div`
   border-bottom-left-radius: ${defaultBorderRadius};
   /* border: 1px solid white; */
   border-top: none;
-  margin-left: 8px;
-  margin-right: 8px;
-  margin-top: -12px;
+  margin: 8px;
   outline: 1px solid orange;
-`;
-
-const PillButton = styled.button`
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  border: none;
-  color: white;
-  background-color: gray;
-  border-radius: 50px;
-  padding: 5px 10px;
-  margin: 5px 0;
-  cursor: pointer;
-
-  &:hover {
-    background-color: ${buttonColor};
-  }
+  z-index: 5;
 `;
 
 const MainTextInput = styled.textarea`
@@ -118,7 +101,9 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
   // 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 [showContextDropdown, setShowContextDropdown] = React.useState(false);
+  const [hoveringButton, setHoveringButton] = React.useState(false);
+  const [hoveringContextDropdown, setHoveringContextDropdown] =
+    React.useState(false);
   const [highlightedCodeSections, setHighlightedCodeSections] = React.useState(
     props.highlightedCodeSections || [
       {
@@ -184,7 +169,7 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
                 300
               ).toString()}px`;
 
-              setShowContextDropdown(target.value.endsWith("@"));
+              // setShowContextDropdown(target.value.endsWith("@"));
             },
             onKeyDown: (event) => {
               if (event.key === "Enter" && event.shiftKey) {
@@ -256,22 +241,11 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
             ))}
         </Ul>
       </div>
-      <ContextDropdown hidden={!showContextDropdown}>
-        <p>Highlight code to include as context:</p>
-        {highlightedCodeSections.map((section, idx) => (
-          <>
-            <p>{section.filepath}</p>
-            <CodeBlock showCopy={false} key={idx}>
-              {section.contents}
-            </CodeBlock>
-          </>
-        ))}
-      </ContextDropdown>
-      <div className="px-2">
+      <div className="px-2 flex gap-2 items-center flex-wrap">
         {highlightedCodeSections.map((section, idx) => (
           <PillButton
-            onClick={() => {
-              console.log("delete context item", idx);
+            title={section.filepath}
+            onDelete={() => {
               if (props.deleteContextItem) {
                 props.deleteContextItem(idx);
               }
@@ -281,11 +255,42 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => {
                 return newSections;
               });
             }}
-          >
-            {section.filepath}
-          </PillButton>
+            onHover={(val: boolean) => {
+              if (val) {
+                setHoveringButton(val);
+              } else {
+                setTimeout(() => {
+                  setHoveringButton(val);
+                }, 100);
+              }
+            }}
+          />
         ))}
+
+        <span className="text-trueGray-400 ml-auto mr-4 text-xs">
+          Highlight code to include as context.{" "}
+          {highlightedCodeSections.length > 0 &&
+            "Otherwise using entire currently open file."}
+        </span>
       </div>
+      <ContextDropdown
+        onMouseEnter={() => {
+          setHoveringContextDropdown(true);
+        }}
+        onMouseLeave={() => {
+          setHoveringContextDropdown(false);
+        }}
+        hidden={!hoveringContextDropdown && !hoveringButton}
+      >
+        {highlightedCodeSections.map((section, idx) => (
+          <>
+            <p>{section.filepath}</p>
+            <CodeBlock showCopy={false} key={idx}>
+              {section.contents}
+            </CodeBlock>
+          </>
+        ))}
+      </ContextDropdown>
     </>
   );
 });
diff --git a/extension/react-app/src/components/ContinueButton.tsx b/extension/react-app/src/components/ContinueButton.tsx
index ef6719b7..5295799a 100644
--- a/extension/react-app/src/components/ContinueButton.tsx
+++ b/extension/react-app/src/components/ContinueButton.tsx
@@ -6,6 +6,7 @@ import { RootStore } from "../redux/store";
 
 let StyledButton = styled(Button)`
   margin: auto;
+  margin-top: 8px;
   display: grid;
   grid-template-columns: 30px 1fr;
   align-items: center;
diff --git a/extension/react-app/src/components/PillButton.tsx b/extension/react-app/src/components/PillButton.tsx
new file mode 100644
index 00000000..33451db5
--- /dev/null
+++ b/extension/react-app/src/components/PillButton.tsx
@@ -0,0 +1,66 @@
+import { useState } from "react";
+import styled from "styled-components";
+import { defaultBorderRadius } from ".";
+
+const Button = styled.button`
+  border: none;
+  color: white;
+  background-color: transparent;
+  border: 1px solid white;
+  border-radius: ${defaultBorderRadius};
+  padding: 3px 6px;
+
+  &:hover {
+    background-color: white;
+    color: black;
+  }
+`;
+
+interface PillButtonProps {
+  onHover?: (arg0: boolean) => void;
+  onDelete?: () => void;
+  title: string;
+}
+
+const PillButton = (props: PillButtonProps) => {
+  const [isHovered, setIsHovered] = useState(false);
+  return (
+    <Button
+      onMouseEnter={() => {
+        setIsHovered(true);
+        if (props.onHover) {
+          props.onHover(true);
+        }
+      }}
+      onMouseLeave={() => {
+        setIsHovered(false);
+        if (props.onHover) {
+          props.onHover(false);
+        }
+      }}
+    >
+      <div
+        style={{ display: "grid", gridTemplateColumns: "1fr auto", gap: "4px" }}
+      >
+        <span>{props.title}</span>
+        <span
+          style={{
+            cursor: "pointer",
+            color: "red",
+            borderLeft: "1px solid black",
+            paddingLeft: "4px",
+          }}
+          hidden={!isHovered}
+          onClick={() => {
+            props.onDelete?.();
+            props.onHover?.(false);
+          }}
+        >
+          X
+        </span>
+      </div>
+    </Button>
+  );
+};
+
+export default PillButton;
-- 
cgit v1.2.3-70-g09d2

