diff options
author | Ty Dunn <ty@tydunn.com> | 2023-07-05 23:20:06 -0700 |
---|---|---|
committer | Ty Dunn <ty@tydunn.com> | 2023-07-05 23:20:06 -0700 |
commit | a5386d7897f5e3f3f7443246de3a443f5b2179d0 (patch) | |
tree | aa51a50475d06efc4d1f039dad8068363e08fee4 /extension | |
parent | c0177d013e79593e6444069feec08bc2dff9c157 (diff) | |
parent | 22b02641b4b14ffad32914d046e645cf6f850253 (diff) | |
download | sncontinue-a5386d7897f5e3f3f7443246de3a443f5b2179d0.tar.gz sncontinue-a5386d7897f5e3f3f7443246de3a443f5b2179d0.tar.bz2 sncontinue-a5386d7897f5e3f3f7443246de3a443f5b2179d0.zip |
Merge branch 'main' of github.com:continuedev/continue
Diffstat (limited to 'extension')
-rw-r--r-- | extension/package-lock.json | 31 | ||||
-rw-r--r-- | extension/package.json | 18 | ||||
-rw-r--r-- | extension/react-app/package-lock.json | 27 | ||||
-rw-r--r-- | extension/react-app/package.json | 1 | ||||
-rw-r--r-- | extension/react-app/src/components/ComboBox.tsx | 36 | ||||
-rw-r--r-- | extension/react-app/src/components/HeaderButtonWithText.tsx | 4 | ||||
-rw-r--r-- | extension/react-app/src/components/PillButton.tsx | 27 | ||||
-rw-r--r-- | extension/react-app/src/components/StepContainer.tsx | 12 | ||||
-rw-r--r-- | extension/react-app/src/components/UserInputContainer.tsx | 6 | ||||
-rw-r--r-- | extension/react-app/src/hooks/ContinueGUIClientProtocol.ts | 2 | ||||
-rw-r--r-- | extension/react-app/src/hooks/useContinueGUIProtocol.ts | 4 | ||||
-rw-r--r-- | extension/react-app/src/main.tsx | 4 | ||||
-rw-r--r-- | extension/react-app/src/tabs/gui.tsx | 24 | ||||
-rw-r--r-- | extension/src/commands.ts | 4 | ||||
-rw-r--r-- | extension/src/continueIdeClient.ts | 38 | ||||
-rw-r--r-- | extension/src/diffs.ts | 140 | ||||
-rw-r--r-- | extension/src/lang-server/codeLens.ts | 56 |
17 files changed, 355 insertions, 79 deletions
diff --git a/extension/package-lock.json b/extension/package-lock.json index c4a930de..b322acb7 100644 --- a/extension/package-lock.json +++ b/extension/package-lock.json @@ -1,12 +1,12 @@ { "name": "continue", - "version": "0.0.108", + "version": "0.0.113", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "continue", - "version": "0.0.108", + "version": "0.0.113", "license": "Apache-2.0", "dependencies": { "@electron/rebuild": "^3.2.10", @@ -15,6 +15,7 @@ "@segment/analytics-node": "^0.0.1-beta.16", "@sentry/node": "^7.57.0", "@styled-icons/heroicons-outline": "^10.47.0", + "@styled-icons/heroicons-solid": "^10.47.0", "@vitejs/plugin-react-swc": "^3.3.2", "axios": "^1.2.5", "downshift": "^7.6.0", @@ -2238,6 +2239,23 @@ "styled-components": "*" } }, + "node_modules/@styled-icons/heroicons-solid": { + "version": "10.47.0", + "resolved": "https://registry.npmjs.org/@styled-icons/heroicons-solid/-/heroicons-solid-10.47.0.tgz", + "integrity": "sha512-j+tJx2NzLG2tc91IXJVwKNjsI/osxmak+wmLfnfBsB+49srpxMYjuLPMtl9ZY/xgbNsWO36O+/N5Zf5bkgiKcQ==", + "dependencies": { + "@babel/runtime": "^7.20.7", + "@styled-icons/styled-icon": "^10.7.0" + }, + "funding": { + "type": "GitHub", + "url": "https://github.com/sponsors/jacobwgillespie" + }, + "peerDependencies": { + "react": "*", + "styled-components": "*" + } + }, "node_modules/@styled-icons/styled-icon": { "version": "10.7.0", "resolved": "https://registry.npmjs.org/@styled-icons/styled-icon/-/styled-icon-10.7.0.tgz", @@ -13120,6 +13138,15 @@ "@styled-icons/styled-icon": "^10.7.0" } }, + "@styled-icons/heroicons-solid": { + "version": "10.47.0", + "resolved": "https://registry.npmjs.org/@styled-icons/heroicons-solid/-/heroicons-solid-10.47.0.tgz", + "integrity": "sha512-j+tJx2NzLG2tc91IXJVwKNjsI/osxmak+wmLfnfBsB+49srpxMYjuLPMtl9ZY/xgbNsWO36O+/N5Zf5bkgiKcQ==", + "requires": { + "@babel/runtime": "^7.20.7", + "@styled-icons/styled-icon": "^10.7.0" + } + }, "@styled-icons/styled-icon": { "version": "10.7.0", "resolved": "https://registry.npmjs.org/@styled-icons/styled-icon/-/styled-icon-10.7.0.tgz", diff --git a/extension/package.json b/extension/package.json index 87dd7ba6..09703da4 100644 --- a/extension/package.json +++ b/extension/package.json @@ -14,7 +14,7 @@ "displayName": "Continue", "pricing": "Free", "description": "The open-source coding autopilot", - "version": "0.0.108", + "version": "0.0.113", "publisher": "Continue", "engines": { "vscode": "^1.67.0" @@ -39,6 +39,7 @@ "onView:continueGUIView" ], "main": "./out/extension.js", + "browser": "./out/extension.js", "contributes": { "configuration": { "title": "Continue", @@ -87,6 +88,16 @@ "title": "Reject Suggestion" }, { + "command": "continue.acceptDiff", + "category": "Continue", + "title": "Accept Diff" + }, + { + "command": "continue.rejectDiff", + "category": "Continue", + "title": "Reject Diff" + }, + { "command": "continue.acceptAllSuggestions", "category": "Continue", "title": "Accept All Suggestions" @@ -119,12 +130,12 @@ "key": "shift+ctrl+enter" }, { - "command": "continue.acceptAllSuggestions", + "command": "continue.acceptDiff", "mac": "shift+cmd+enter", "key": "shift+ctrl+enter" }, { - "command": "continue.rejectAllSuggestions", + "command": "continue.rejectDiff", "mac": "shift+cmd+backspace", "key": "shift+ctrl+backspace" } @@ -242,6 +253,7 @@ "@segment/analytics-node": "^0.0.1-beta.16", "@sentry/node": "^7.57.0", "@styled-icons/heroicons-outline": "^10.47.0", + "@styled-icons/heroicons-solid": "^10.47.0", "@vitejs/plugin-react-swc": "^3.3.2", "axios": "^1.2.5", "downshift": "^7.6.0", diff --git a/extension/react-app/package-lock.json b/extension/react-app/package-lock.json index 85b8633b..fb13dffd 100644 --- a/extension/react-app/package-lock.json +++ b/extension/react-app/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.0", "dependencies": { "@styled-icons/heroicons-outline": "^10.47.0", + "@styled-icons/heroicons-solid": "^10.47.0", "@types/vscode-webview": "^1.57.1", "downshift": "^7.6.0", "posthog-js": "^1.58.0", @@ -691,6 +692,23 @@ "styled-components": "*" } }, + "node_modules/@styled-icons/heroicons-solid": { + "version": "10.47.0", + "resolved": "https://registry.npmjs.org/@styled-icons/heroicons-solid/-/heroicons-solid-10.47.0.tgz", + "integrity": "sha512-j+tJx2NzLG2tc91IXJVwKNjsI/osxmak+wmLfnfBsB+49srpxMYjuLPMtl9ZY/xgbNsWO36O+/N5Zf5bkgiKcQ==", + "dependencies": { + "@babel/runtime": "^7.20.7", + "@styled-icons/styled-icon": "^10.7.0" + }, + "funding": { + "type": "GitHub", + "url": "https://github.com/sponsors/jacobwgillespie" + }, + "peerDependencies": { + "react": "*", + "styled-components": "*" + } + }, "node_modules/@styled-icons/styled-icon": { "version": "10.7.0", "resolved": "https://registry.npmjs.org/@styled-icons/styled-icon/-/styled-icon-10.7.0.tgz", @@ -3937,6 +3955,15 @@ "@styled-icons/styled-icon": "^10.7.0" } }, + "@styled-icons/heroicons-solid": { + "version": "10.47.0", + "resolved": "https://registry.npmjs.org/@styled-icons/heroicons-solid/-/heroicons-solid-10.47.0.tgz", + "integrity": "sha512-j+tJx2NzLG2tc91IXJVwKNjsI/osxmak+wmLfnfBsB+49srpxMYjuLPMtl9ZY/xgbNsWO36O+/N5Zf5bkgiKcQ==", + "requires": { + "@babel/runtime": "^7.20.7", + "@styled-icons/styled-icon": "^10.7.0" + } + }, "@styled-icons/styled-icon": { "version": "10.7.0", "resolved": "https://registry.npmjs.org/@styled-icons/styled-icon/-/styled-icon-10.7.0.tgz", diff --git a/extension/react-app/package.json b/extension/react-app/package.json index e46fdc8c..12701906 100644 --- a/extension/react-app/package.json +++ b/extension/react-app/package.json @@ -10,6 +10,7 @@ }, "dependencies": { "@styled-icons/heroicons-outline": "^10.47.0", + "@styled-icons/heroicons-solid": "^10.47.0", "@types/vscode-webview": "^1.57.1", "downshift": "^7.6.0", "posthog-js": "^1.58.0", diff --git a/extension/react-app/src/components/ComboBox.tsx b/extension/react-app/src/components/ComboBox.tsx index 3e1f3e16..81b148b9 100644 --- a/extension/react-app/src/components/ComboBox.tsx +++ b/extension/react-app/src/components/ComboBox.tsx @@ -11,7 +11,12 @@ import CodeBlock from "./CodeBlock"; import { RangeInFile } from "../../../src/client"; import PillButton from "./PillButton"; import HeaderButtonWithText from "./HeaderButtonWithText"; -import { Trash, LockClosed, LockOpen } from "@styled-icons/heroicons-outline"; +import { + Trash, + LockClosed, + LockOpen, + Plus, +} from "@styled-icons/heroicons-outline"; // #region styled components const mainInputFontSize = 16; @@ -50,7 +55,7 @@ const MainTextInput = styled.textarea` } `; -const UlMaxHeight = 200; +const UlMaxHeight = 400; const Ul = styled.ul<{ hidden: boolean; showAbove: boolean; @@ -100,6 +105,8 @@ interface ComboBoxProps { highlightedCodeSections: (RangeInFile & { contents: string })[]; deleteContextItems: (indices: number[]) => void; onTogglePin: () => void; + onToggleAddContext: () => void; + addingHighlightedCode: boolean; } const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => { @@ -188,6 +195,11 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => { ) { // Prevent Downshift's default 'Enter' behavior. (event.nativeEvent as any).preventDownshiftDefault = true; + + // cmd+enter to /edit + if (event.metaKey) { + event.currentTarget.value = `/edit ${event.currentTarget.value}`; + } if (props.onEnter) props.onEnter(event); setInputValue(""); const value = event.currentTarget.value; @@ -249,6 +261,19 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => { </Ul> </div> <div className="px-2 flex gap-2 items-center flex-wrap"> + {highlightedCodeSections.length === 0 && ( + <HeaderButtonWithText + text={ + props.addingHighlightedCode ? "Adding Context" : "Add Context" + } + onClick={() => { + props.onToggleAddContext(); + }} + inverted={props.addingHighlightedCode} + > + <Plus size="1.6em" /> + </HeaderButtonWithText> + )} {highlightedCodeSections.length > 0 && ( <> <HeaderButtonWithText @@ -304,10 +329,9 @@ const ComboBox = React.forwardRef((props: ComboBoxProps, ref) => { /> ))} - <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 className="text-trueGray-400 ml-auto mr-4 text-xs text-right"> + Highlight code to include as context. Currently open file included by + default. {highlightedCodeSections.length === 0 && ""} </span> </div> <ContextDropdown diff --git a/extension/react-app/src/components/HeaderButtonWithText.tsx b/extension/react-app/src/components/HeaderButtonWithText.tsx index 3ddac93c..72a653c5 100644 --- a/extension/react-app/src/components/HeaderButtonWithText.tsx +++ b/extension/react-app/src/components/HeaderButtonWithText.tsx @@ -8,15 +8,17 @@ interface HeaderButtonWithTextProps { children: React.ReactNode; disabled?: boolean; inverted?: boolean; + active?: boolean; } const HeaderButtonWithText = (props: HeaderButtonWithTextProps) => { const [hover, setHover] = useState(false); + const paddingLeft = (props.disabled ? (props.active ? "3px" : "1px"): (hover ? "4px" : "1px")); return ( <HeaderButton inverted={props.inverted} disabled={props.disabled} - style={{ padding: "1px", paddingLeft: hover ? "4px" : "1px" }} + style={{ padding: (props.active ? "3px" : "1px"), paddingLeft, borderRadius: (props.active ? "50%" : undefined) }} onMouseEnter={() => { if (!props.disabled) { setHover(true); diff --git a/extension/react-app/src/components/PillButton.tsx b/extension/react-app/src/components/PillButton.tsx index 2352c3ad..5a02c6b2 100644 --- a/extension/react-app/src/components/PillButton.tsx +++ b/extension/react-app/src/components/PillButton.tsx @@ -15,6 +15,8 @@ const Button = styled.button` background-color: white; color: black; } + + cursor: pointer; `; interface PillButtonProps { @@ -39,26 +41,13 @@ const PillButton = (props: PillButtonProps) => { props.onHover(false); } }} + onClick={() => { + if (props.onDelete) { + props.onDelete(); + } + }} > - <div - style={{ display: "grid", gridTemplateColumns: "1fr auto", gap: "4px" }} - > - <span - style={{ - cursor: "pointer", - color: "red", - borderRight: "1px solid black", - paddingRight: "4px", - }} - onClick={() => { - props.onDelete?.(); - props.onHover?.(false); - }} - > - <XMark style={{ padding: "0px" }} size="1.2em" strokeWidth="2px" /> - </span> - <span>{props.title}</span> - </div> + {props.title} </Button> ); }; diff --git a/extension/react-app/src/components/StepContainer.tsx b/extension/react-app/src/components/StepContainer.tsx index 35d34976..2aed2e72 100644 --- a/extension/react-app/src/components/StepContainer.tsx +++ b/extension/react-app/src/components/StepContainer.tsx @@ -10,9 +10,10 @@ import { import { ChevronDown, ChevronRight, - XMark, ArrowPath, + XMark, } from "@styled-icons/heroicons-outline"; +import { Stop } from "@styled-icons/heroicons-solid"; import { HistoryNode } from "../../../schema/HistoryNode"; import ReactMarkdown from "react-markdown"; import HeaderButtonWithText from "./HeaderButtonWithText"; @@ -207,9 +208,14 @@ function StepContainer(props: StepContainerProps) { e.stopPropagation(); props.onDelete(); }} - text="Delete" + text={props.historyNode.active ? "Stop" : "Delete"} + active={props.historyNode.active} > - <XMark size="1.6em" onClick={props.onDelete} /> + {props.historyNode.active ? ( + <Stop size="1.2em" onClick={props.onDelete} /> + ) : ( + <XMark size="1.6em" onClick={props.onDelete} /> + )} </HeaderButtonWithText> {props.historyNode.observation?.error ? ( <HeaderButtonWithText diff --git a/extension/react-app/src/components/UserInputContainer.tsx b/extension/react-app/src/components/UserInputContainer.tsx index 44fdba38..28437d35 100644 --- a/extension/react-app/src/components/UserInputContainer.tsx +++ b/extension/react-app/src/components/UserInputContainer.tsx @@ -15,7 +15,7 @@ interface UserInputContainerProps { } const StyledDiv = styled.div` - background-color: rgb(50 50 50); + background-color: rgb(45 45 45); padding: 8px; padding-left: 16px; padding-right: 16px; @@ -28,8 +28,8 @@ const StyledDiv = styled.div` const UserInputContainer = (props: UserInputContainerProps) => { return ( - <StyledDiv hidden={props.historyNode.step.hide as any}> - {props.children} + <StyledDiv> + <b>{props.children}</b> <div style={{ marginLeft: "auto" }}> <HeaderButtonWithText onClick={(e) => { diff --git a/extension/react-app/src/hooks/ContinueGUIClientProtocol.ts b/extension/react-app/src/hooks/ContinueGUIClientProtocol.ts index 96ea7ab3..f123bb2b 100644 --- a/extension/react-app/src/hooks/ContinueGUIClientProtocol.ts +++ b/extension/react-app/src/hooks/ContinueGUIClientProtocol.ts @@ -22,6 +22,8 @@ abstract class AbstractContinueGUIClientProtocol { abstract deleteAtIndex(index: number): void; abstract deleteContextAtIndices(indices: number[]): void; + + abstract toggleAddingHighlightedCode(): void; } export default AbstractContinueGUIClientProtocol; diff --git a/extension/react-app/src/hooks/useContinueGUIProtocol.ts b/extension/react-app/src/hooks/useContinueGUIProtocol.ts index e950387c..49f200ae 100644 --- a/extension/react-app/src/hooks/useContinueGUIProtocol.ts +++ b/extension/react-app/src/hooks/useContinueGUIProtocol.ts @@ -74,6 +74,10 @@ class ContinueGUIClientProtocol extends AbstractContinueGUIClientProtocol { deleteContextAtIndices(indices: number[]) { this.messenger.send("delete_context_at_indices", { indices }); } + + toggleAddingHighlightedCode(): void { + this.messenger.send("toggle_adding_highlighted_code", {}); + } } export default ContinueGUIClientProtocol; diff --git a/extension/react-app/src/main.tsx b/extension/react-app/src/main.tsx index 1b94dc82..0b02575c 100644 --- a/extension/react-app/src/main.tsx +++ b/extension/react-app/src/main.tsx @@ -8,6 +8,10 @@ import { PostHogProvider } from "posthog-js/react"; posthog.init("phc_JS6XFROuNbhJtVCEdTSYk6gl5ArRrTNMpCcguAXlSPs", { api_host: "https://app.posthog.com", + session_recording: { + // WARNING: Only enable this if you understand the security implications + recordCrossOriginIframes: true, + } as any, }); ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( diff --git a/extension/react-app/src/tabs/gui.tsx b/extension/react-app/src/tabs/gui.tsx index bbf0b126..e5320c6a 100644 --- a/extension/react-app/src/tabs/gui.tsx +++ b/extension/react-app/src/tabs/gui.tsx @@ -71,6 +71,7 @@ function GUI(props: GUIProps) { const [waitingForSteps, setWaitingForSteps] = useState(false); const [userInputQueue, setUserInputQueue] = useState<string[]>([]); const [highlightedRanges, setHighlightedRanges] = useState([]); + const [addingHighlightedCode, setAddingHighlightedCode] = useState(false); const [availableSlashCommands, setAvailableSlashCommands] = useState< { name: string; description: string }[] >([]); @@ -157,6 +158,7 @@ function GUI(props: GUIProps) { setHistory(state.history); setHighlightedRanges(state.highlighted_ranges); setUserInputQueue(state.user_input_queue); + setAddingHighlightedCode(state.adding_highlighted_code); setAvailableSlashCommands( state.slash_commands.map((c: any) => { return { @@ -293,14 +295,16 @@ function GUI(props: GUIProps) { )} {history?.timeline.map((node: HistoryNode, index: number) => { return node.step.name === "User Input" ? ( - <UserInputContainer - onDelete={() => { - client?.deleteAtIndex(index); - }} - historyNode={node} - > - {node.step.description as string} - </UserInputContainer> + node.step.hide || ( + <UserInputContainer + onDelete={() => { + client?.deleteAtIndex(index); + }} + historyNode={node} + > + {node.step.description as string} + </UserInputContainer> + ) ) : ( <StepContainer isLast={index === history.timeline.length - 1} @@ -361,6 +365,10 @@ function GUI(props: GUIProps) { onTogglePin={() => { setPinned((prev: boolean) => !prev); }} + onToggleAddContext={() => { + client?.toggleAddingHighlightedCode(); + }} + addingHighlightedCode={addingHighlightedCode} /> <ContinueButton onClick={onMainTextInput} /> </TopGUIDiv> diff --git a/extension/src/commands.ts b/extension/src/commands.ts index 8072353b..4414a171 100644 --- a/extension/src/commands.ts +++ b/extension/src/commands.ts @@ -12,6 +12,8 @@ import { acceptAllSuggestionsCommand, rejectAllSuggestionsCommand, } from "./suggestions"; + +import { acceptDiffCommand, rejectDiffCommand } from "./diffs"; import * as bridge from "./bridge"; import { debugPanelWebview } from "./debugPanel"; import { sendTelemetryEvent, TelemetryEvent } from "./telemetry"; @@ -51,6 +53,8 @@ const commandsMap: { [command: string]: (...args: any) => any } = { "continue.suggestionUp": suggestionUpCommand, "continue.acceptSuggestion": acceptSuggestionCommand, "continue.rejectSuggestion": rejectSuggestionCommand, + "continue.acceptDiff": acceptDiffCommand, + "continue.rejectDiff": rejectDiffCommand, "continue.acceptAllSuggestions": acceptAllSuggestionsCommand, "continue.rejectAllSuggestions": rejectAllSuggestionsCommand, "continue.focusContinueInput": async () => { diff --git a/extension/src/continueIdeClient.ts b/extension/src/continueIdeClient.ts index b9969858..90547edc 100644 --- a/extension/src/continueIdeClient.ts +++ b/extension/src/continueIdeClient.ts @@ -15,6 +15,10 @@ import { import { FileEditWithFullContents } from "../schema/FileEditWithFullContents"; import fs = require("fs"); import { WebsocketMessenger } from "./util/messenger"; +import * as path from "path"; +import * as os from "os"; +import { diffManager } from "./diffs"; + class IdeProtocolClient { private messenger: WebsocketMessenger | null = null; private readonly context: vscode.ExtensionContext; @@ -239,40 +243,8 @@ class IdeProtocolClient { ); } - contentProvider: vscode.Disposable | null = null; - showDiff(filepath: string, replacement: string) { - const myProvider = new (class - implements vscode.TextDocumentContentProvider - { - onDidChangeEmitter = new vscode.EventEmitter<vscode.Uri>(); - onDidChange = this.onDidChangeEmitter.event; - provideTextDocumentContent = (uri: vscode.Uri) => { - return replacement; - }; - })(); - this.contentProvider = vscode.workspace.registerTextDocumentContentProvider( - "continueDiff", - myProvider - ); - - // Call the event fire - const diffFilename = `continueDiff://${filepath}`; - myProvider.onDidChangeEmitter.fire(vscode.Uri.parse(diffFilename)); - - const leftUri = vscode.Uri.file(filepath); - const rightUri = vscode.Uri.parse(diffFilename); - const title = "Continue Diff"; - vscode.commands - .executeCommand("vscode.diff", leftUri, rightUri, title) - .then( - () => { - console.log("Diff view opened successfully"); - }, - (error) => { - console.error("Error opening diff view:", error); - } - ); + diffManager.writeDiff(filepath, replacement); } openFile(filepath: string) { diff --git a/extension/src/diffs.ts b/extension/src/diffs.ts new file mode 100644 index 00000000..1b8888e8 --- /dev/null +++ b/extension/src/diffs.ts @@ -0,0 +1,140 @@ +import * as os from "os"; +import * as path from "path"; +import * as fs from "fs"; +import * as vscode from "vscode"; + +interface DiffInfo { + originalFilepath: string; + newFilepath: string; + editor?: vscode.TextEditor; +} + +export const DIFF_DIRECTORY = path.join(os.homedir(), ".continue", "diffs"); + +class DiffManager { + // Create a temporary file in the global .continue directory which displays the updated version + // Doing this because virtual files are read-only + private diffs: Map<string, DiffInfo> = new Map(); + + private setupDirectory() { + // Make sure the diff directory exists + if (!fs.existsSync(DIFF_DIRECTORY)) { + fs.mkdirSync(DIFF_DIRECTORY, { + recursive: true, + }); + } + } + + constructor() { + this.setupDirectory(); + } + + private escapeFilepath(filepath: string): string { + return filepath.replace(/\\/g, "_").replace(/\//g, "_"); + } + + private openDiffEditor( + originalFilepath: string, + newFilepath: string, + newContent: string + ): vscode.TextEditor { + const rightUri = vscode.Uri.parse(newFilepath); + const leftUri = vscode.Uri.file(originalFilepath); + const title = "Continue Diff"; + vscode.commands.executeCommand("vscode.diff", leftUri, rightUri, title); + + const editor = vscode.window.activeTextEditor; + if (!editor) { + throw new Error("No active text editor found for Continue Diff"); + } + + // Change the vscode setting to allow codeLens in diff editor + vscode.workspace + .getConfiguration("diffEditor", editor.document.uri) + .update("codeLens", true, vscode.ConfigurationTarget.Global); + + return editor; + } + + writeDiff(originalFilepath: string, newContent: string): string { + this.setupDirectory(); + + // Create or update existing diff + const newFilepath = path.join( + DIFF_DIRECTORY, + this.escapeFilepath(originalFilepath) + ); + fs.writeFileSync(newFilepath, newContent); + + // Open the diff editor if this is a new diff + if (!this.diffs.has(newFilepath)) { + const diffInfo: DiffInfo = { + originalFilepath, + newFilepath, + }; + diffInfo.editor = this.openDiffEditor( + originalFilepath, + newFilepath, + newContent + ); + this.diffs.set(newFilepath, diffInfo); + } + return newFilepath; + } + + cleanUpDiff(diffInfo: DiffInfo) { + // Close the editor, remove the record, delete the file + if (diffInfo.editor) { + vscode.window.showTextDocument(diffInfo.editor.document); + vscode.commands.executeCommand("workbench.action.closeActiveEditor"); + } + this.diffs.delete(diffInfo.newFilepath); + fs.unlinkSync(diffInfo.newFilepath); + } + + acceptDiff(newFilepath?: string) { + // If no newFilepath is provided and there is only one in the dictionary, use that + if (!newFilepath && this.diffs.size === 1) { + newFilepath = Array.from(this.diffs.keys())[0]; + } + if (!newFilepath) { + return; + } + // Get the diff info, copy new file to original, then delete from record and close the corresponding editor + const diffInfo = this.diffs.get(newFilepath); + if (!diffInfo) { + return; + } + fs.writeFileSync( + diffInfo.originalFilepath, + fs.readFileSync(diffInfo.newFilepath) + ); + this.cleanUpDiff(diffInfo); + } + + rejectDiff(newFilepath?: string) { + // If no newFilepath is provided and there is only one in the dictionary, use that + if (!newFilepath && this.diffs.size === 1) { + newFilepath = Array.from(this.diffs.keys())[0]; + } + if (!newFilepath) { + return; + } + const diffInfo = this.diffs.get(newFilepath); + if (!diffInfo) { + return; + } + + this.cleanUpDiff(diffInfo); + } +} + +export const diffManager = new DiffManager(); + +export async function acceptDiffCommand(newFilepath?: string) { + diffManager.acceptDiff(newFilepath); +} + +export async function rejectDiffCommand(newFilepath?: string) { + diffManager.rejectDiff(newFilepath); +} diff --git a/extension/src/lang-server/codeLens.ts b/extension/src/lang-server/codeLens.ts index 3bd4f153..381a0084 100644 --- a/extension/src/lang-server/codeLens.ts +++ b/extension/src/lang-server/codeLens.ts @@ -1,6 +1,8 @@ import * as vscode from "vscode"; import { editorToSuggestions, editorSuggestionsLocked } from "../suggestions"; - +import * as path from "path"; +import * as os from "os"; +import { DIFF_DIRECTORY } from "../diffs"; class SuggestionsCodeLensProvider implements vscode.CodeLensProvider { public provideCodeLenses( document: vscode.TextDocument, @@ -60,15 +62,67 @@ class SuggestionsCodeLensProvider implements vscode.CodeLensProvider { } } +class DiffViewerCodeLensProvider implements vscode.CodeLensProvider { + public provideCodeLenses( + document: vscode.TextDocument, + token: vscode.CancellationToken + ): vscode.CodeLens[] | Thenable<vscode.CodeLens[]> { + if (path.dirname(document.uri.fsPath) === DIFF_DIRECTORY) { + const codeLenses: vscode.CodeLens[] = []; + const range = new vscode.Range(0, 0, 1, 0); + codeLenses.push( + new vscode.CodeLens(range, { + title: "Accept ✅", + command: "continue.acceptDiff", + arguments: [document.uri.fsPath], + }), + new vscode.CodeLens(range, { + title: "Reject ❌", + command: "continue.rejectDiff", + arguments: [document.uri.fsPath], + }) + ); + return codeLenses; + } else { + return []; + } + } + + onDidChangeCodeLenses?: vscode.Event<void> | undefined; + + constructor(emitter?: vscode.EventEmitter<void>) { + if (emitter) { + this.onDidChangeCodeLenses = emitter.event; + this.onDidChangeCodeLenses(() => { + if (vscode.window.activeTextEditor) { + this.provideCodeLenses( + vscode.window.activeTextEditor.document, + new vscode.CancellationTokenSource().token + ); + } + }); + } + } +} + +let diffsCodeLensDisposable: vscode.Disposable | undefined = undefined; let suggestionsCodeLensDisposable: vscode.Disposable | undefined = undefined; export function registerAllCodeLensProviders(context: vscode.ExtensionContext) { if (suggestionsCodeLensDisposable) { suggestionsCodeLensDisposable.dispose(); } + if (diffsCodeLensDisposable) { + diffsCodeLensDisposable.dispose(); + } suggestionsCodeLensDisposable = vscode.languages.registerCodeLensProvider( "*", new SuggestionsCodeLensProvider() ); + diffsCodeLensDisposable = vscode.languages.registerCodeLensProvider( + "*", + new DiffViewerCodeLensProvider() + ); context.subscriptions.push(suggestionsCodeLensDisposable); + context.subscriptions.push(diffsCodeLensDisposable); } |